Author Topic: Arduino tap tempo LED/LDR output w. 6 waveform presets, ramp up/down, etc...  (Read 881 times)

any

  • Great Contributor!
  • ***
  • Posts: 362
  • Total likes: 13
  • a.k.a. whythisreason? Niels H.
    • http://www.anyplace.nl
Ok, managed to get this working  ;D

A momentary OR latching mode that will 'pump' back and forth between ramp-up and ramp-down if there is a state change. Indefinitely and from the value where the state change occurs.

So the scenario for latching is is:
1. Footswitch press on, LED starts to ramp up to full brightness (based on tap tempo, currently period/16, which is quite slow as it will take 255 x period/16 to reach full brightness)
2. Footswitch pressed again during ramp up period, say half way = abort ramp up, LED will now start to ramp back down from the moment the switch was pressed.
  (so say the pwm value got to 130 when the switch was pressed, it will start to decrease from there)
3. Footswitch pressed again during ramp-down period = re-engage ramp up, and the LED will start to increase brightness.
4. Press again, see 2. etc...

- If it reaches full brightness (completes the ramp up) it switches to the next function
(either 'updateeffect' or 'effectlongpress' depending on latching or momentary engagement of the first instance).
- If it reaches '0' on the 'abort' ramp down and it will reset and go back to loop.

Just had a little play and I think it's really useful, especially with very slow ramp times as it's a bit like having an
actual foot expression pedal connected for slow swells. I will need to start thinking about ways to select ramp times,
either based on tapped tempo or in msec, bars etc. (probably LCD with rotary encoder so there's a larger selection of
waveforms to choose, midi functions?) This could get quite funky!

Code below (just for all the button events, which uses the onebutton library)
Cleaned up and annotated so it's (hopefully) clear what's going on:
Feedback welcome, wouldn't be surprised if there is some streamlining possible.

Code: [Select]
void effectsingleclick()
{   
  buttonPushCounter++;      // every time the effect turns on, increment the counter
  Serial.println(buttonPushCounter);
  if (buttonPushCounter %2 != 0 && doubleTapCounter == 0 )
  {  // Determines if the effect is currently off and is not in the double click function
   effectlongpressstart();  // Run through longpressstart in case 'ramp up' function is selected
   if (cancelrampup == 0){
   update_effect();         // Run the chosen waveform until the effect is turned off
   }
   if (cancelrampup == 1){  // If 'effectlongpressstart' was canceled and 'ramped down' to zero, this will reset everything for the next effectbutton event
    ledon.Stop();
    ledrandom.Stop();
    ledsquare.Stop();
    ledsine.Stop();
    ledrampup.Stop();
    ledrampdown.Stop();
    cancelrampup = 0;
    buttonPushCounter = 0;
   }
  }
  /* If a doubleclick is active, make sure the led stays off and fire a momentary pulse
  (in my case to a CD4066 cmos switch) to switch the Line6 effect and tap buttons   
  simultenously to trigger the preset select.*/
  else if (doubleTapCounter != 0 )
  { 
  ledon.Stop();
  ledrandom.Stop();
  ledsquare.Stop();
  ledsine.Stop();
  ledrampup.Stop();
  ledrampdown.Stop();
  digitalWrite(line6effect, !digitalRead(line6effect));         // Pulls Line6 Effect On/Off button low (to ground) via CD4066 switch
  digitalWrite(line6tap, !digitalRead(line6tap));               // Pulls Line6 Tap button low (to ground) via CD4066 switch
  delay(100);                                                   // little pause to simulate a momentary footswitch press/depress
  digitalWrite(line6effect, !digitalRead(line6effect));         // Pulls Line6 Effect On/Off button low (to ground) via CD4066 switch
  digitalWrite(line6tap, !digitalRead(line6tap));               // Pulls Line6 Tap button low (to ground) via CD4066 switch
  }
/* If there's no doubleclick active, another single click will go to longpresstop,
in case there is a 'ramp down' selected.*/
  else
  {
    effectlongpressstop();
  }
}

void effectlongpressstart()
{
  /* At the start of momentary or latching (they both cycle through longpresstart)
  Reset the LED so the timing starts from the moment you turn it on, and ramp up
  to the waveform if 'ramp up' is selected.*/
  ledon.Reset();
  ledrandom.Reset();
  ledsquare.Reset();
  ledsine.Reset();
  ledrampup.Reset();
  ledrampdown.Reset();
  int rampvalue = 0;                                // This value stores the current brightness of the LED during ramp up/down, always starts at 0 (off)
  int buttonState = digitalRead(effectbutton);      // This value stores the current state of the button for state change detection
  int lastButtonState = digitalRead(effectbutton);  // This value stores the previous state of the button for state change detection
  bool rampback = false;                            // rampback is used to toggle between ramp up / ramp down as long as either is not finished.
 
  // 3bit value that selects the right switch case based on sum of the waveselect pins read below
  int waveVal = digitalRead(waveselect1) << 2 | digitalRead(waveselect2) << 1 | digitalRead(waveselect3);
  /*Currently there is only one switch case (3), that corresponds to the 'always on' waveform setting.
  As all the other wave forms either start at 0, are random, or do contunous ramp up or ramp down.
  So it didn't seem as useful there, but might add functionalities to this in future versions*/
  switch (waveVal)
    {
      case 3:
      // Fading in the LED
      // This while loop makes sure the only ways to exit the loop are a completed 'abort' back to off, or a succesful 'ramp up' to full brightness
      while ( rampback == true && rampvalue != 0 || rampback == false && rampvalue != 255 )
         {
           // This while loop makes sure we chose to 'ramp up' the LED and it's not already at full brightness (not sure if it needs the brightness statement)
           while ( rampback == false && rampvalue != 255)
            {
            // Reset the button states (default state is high, state change on low)
            buttonState = 1;
            lastButtonState = 1;
            // A bit of delay to make sure there is no bounce interference from the button
            delay(300);
            // Until rampvalue reaches 255 (full brightness) keep increasing brightness
            for(rampvalue; rampvalue<255; rampvalue++)
              {
                buttonState = digitalRead(effectbutton);        // Read the effect button to see if it has changed state
                analogWrite(effectled, rampvalue);              // Rampvalue keeps increasing until full brightness (255)
                delay(period/16);                               // Increasing this value will give a longer ramp up time, can set to time in msec (10 default) or 'period/xxx' for tap related length
                lastButtonState = digitalRead(effectbutton);    // Read the effect button to see if it has changed state
                // If during ramp up the switch changes state, we initiate an 'abort function'
                if(buttonState != lastButtonState)
                  {
                   rampback = true;                             // Set rampback to true will change the while loop from 'ramp up' to 'ramp down'
                   break;                                       // break the while loop to initiate based on the new boolean statement (rampback = true)
                  }
              }
          }
      // This while loop makes sure we chose to 'ramp down' the LED and it's not already off (not sure if it needs the brightness statement)
      while ( rampback == true && rampvalue != 0 )
          {
          // Reset the button states (default state is high, state change on low)
          buttonState = 1;
          lastButtonState = 1;
          // A bit of delay to make sure there is no bounce interference from the button
          delay(300);
          // Until rampvalue reaches 0 (off) keep decreasing brightness
          for(rampvalue; rampvalue>0; rampvalue--)
            {
                buttonState = digitalRead(effectbutton);        // Read the effect button to see if it has changed state
                analogWrite(effectled, rampvalue);              // Rampvalue keeps increasing until full brightness (255)
                delay(period/16);                               // Increasing this value will give a longer ramp up time, can set to time in msec (10 default) or 'period/xxx' for tap related length
                lastButtonState = digitalRead(effectbutton);    // Read the effect button to see if it has changed state
                // If during ramp up the switch changes state, we switch back to the 'ramp up function'
                if(buttonState != lastButtonState)
                {
                  rampback = false;                             // Set rampback to true will change the while loop from 'ramp down' to 'ramp up'
                  break;                                        // break the while loop to initiate based on the new boolean statement (rampback = false)
                }
            }
           // If the ramp up is canceled in full (aborted during ramp up and ramped down to '0') we need to do a couple of things to reset the system
           if (rampvalue == 0)
            {
            cancelrampup = 1;                                   // Set a boolean so the 'single click' event is reset when it returns from here.
//            buttonPushCounter = 2;                            // I think we don't need this as it's set to '0' when we do the 'return'
            digitalWrite(effectled,LOW);                        // Switch off the LED
            return;                                             // Go back to the function that called it, in this case effectsingleclick
            }
          }
        }
       break;
    }
}

void effectlongpress()
// Effectlongpress is a momentary action, so as long as the footswitch is pressed it will loop and update the led
{
  // 3bit value that selects the right switch case based on sum of the waveselect pins read below
  int waveVal = digitalRead(waveselect1) << 2 | digitalRead(waveselect2) << 1 | digitalRead(waveselect3);
  // Light the correct LED
  switch (waveVal)
  {
  case 2:
    ledsquare.Update();
    break;
  case 3:
    ledon.Update();
    break;
  case 4:
    ledsine.Update();
    break;
  case 5:
    ledrandom.Update();
    break;
  case 6:
    ledrampup.Update();
    break;
  case 7:
    ledrampdown.Update();
    break;
  } 
}

void effectlongpressstop()  // what happens when momentary effect engagement ends (ramp down)
{
  /* At the end of momentary or latching (they both cycle through longpresstop)
  Increment the counter, and ramp down if 'ramp down' is selected.
  Then turn off the LED completely with a digitalwrite as sometimes it kept glowing. :-) */
  // 3bit value that selects the right preset based on sum of the waveselect read below
  int waveVal = digitalRead(waveselect1) << 2 | digitalRead(waveselect2) << 1 | digitalRead(waveselect3);
//  if (cancelrampup == 1)
//  {
//    int rampvalue = 0;
//    cancelrampup = 0;
//    buttonPushCounter = 0;
//    return;
//  }
  switch (waveVal)
  {
    case 3:
     //Fading out the LED
    for(int rampvalue=255; rampvalue>0; rampvalue--)
          {
          analogWrite(effectled, rampvalue);
          delay(period/64); //Increasing this value will give a longer ramp up time, can set to time in msec (10 default) or 'period/xxx' for tap related length
          }
    digitalWrite(effectled,LOW);  // Switch off the LED
    buttonPushCounter = 0;        // Reset the counter
    break;
  }
}

void effectdoubleclick()
{
/* When a double click is detected increment the counter, otherwise reset the counter to 0,
 effectively a toggle. Double click initiates a 'shift function' where the latching
 (single click) performs a different function, the momentary functions don't change */
  if (doubleTapCounter %2 == 0)
  {
    doubleTapCounter++;         // Turn on the 'shift function'
  }
  else
  {
    doubleTapCounter = 0;       // Reset the 'shift function' (off)
  }
  digitalWrite(shiftfunction, !digitalRead(shiftfunction));         // Toggle the indicator light for doubleclick on/off
}
« Last Edit: June 02, 2021, 04:43:05 AM by any »
It's supposed to sound that way.

any

  • Great Contributor!
  • ***
  • Posts: 362
  • Total likes: 13
  • a.k.a. whythisreason? Niels H.
    • http://www.anyplace.nl
Heh, just talking to myself at the moment... is it ok to post follow ups to myself?
Anyone still keeping tabs on this? anyway... Here's where it's at.

I have the Line6 M5 v1 pretty much done based on an Arduino Pro Mini. All function seem to work well, and I'm currently cramming this onto a 15x15 through hole project board.
I might need to ditch socketing the Pro mini for height restrictions inside the M5 (will post pictures of the finished thing, it'll be tight)

I'll quickly list controls for the M5:
- Effect footswitch (on/off momentary or latching, or after double tap, only on/off latching for the modulation and a single tap brings up the presets on the M5)
- Tempo Pot
- Expression Pot (for manual control, and for editing patches at min/max values when modulation is off. also works as intensity control as pot at 0 resistance gives no modulation)
- Waveform selection (mini rotary dial with 6 positions) always on (with ramp function), square, sine, ramp up, ramp down, random
- 3 way mini toggle for 'multiplier' select, either 1x, 2x, or 4x the tapped duration (these values might still change after testing)
- 3 way mini toggle for 'ramp' select, either off, fast(no cancel function), long
  ('long' can be manually 'cancelled' at any time during ramp up or down which will toggle direction of the ramp indefinitely until it's either fully up or down)

The only question I have at this stage, and I wonder if anyone can confirm/feed back/advice on the following is:
The Line6 M5 already has a 'tap' button, so I want to use the Line6 Tap button to pull both inputs LOW and set the same tempo that way on both circuits.
The Line6 is 3.3v logic though, and the Pro Mini is a 5v version.

Is there a way I can easily and safely pull both low with the same button? I was wondering if a diode or similar would allow the 5v on the footswitch without feeding into the 3.3v
going to the Line6. Otherwise I still have a spare switch on the 4066 so might utilise that to decouple the voltages and still pull them to ground simultaneously, but since it requires
switching logic which might (?) introduce timing issues, it would be ideal if both can be pulled low at the same time in a more direct way.

Current (in progress) layout of the board attached as well, for the curious folk ;-)


It's supposed to sound that way.

ElectricDruid

Feedback welcome, wouldn't be surprised if there is some streamlining possible.

I'm still following along! It's fascinating to see someone else working through stuff that you've done yourself. Different people get to similar places by quite different routes most often, which keeps things interesting!

I've got a few comments, but they're mostly just about code style. You'll get away with doing it however you like for so long, but it ultimately pays off to develop a good style because it means you have less things to think about, and that helps when debugging and saves you time.

Code: [Select]
   if (cancelrampup == 0){
   update_effect();         // Run the chosen waveform until the effect is turned off
   }
   if (cancelrampup == 1){  // If 'effectlongpressstart' was canceled and 'ramped down' to zero, this will reset everything for the next effectbutton event
This bit should have an "elseif", since they can't happen together.

Your if statements and use of brackets is very variable. Sometimes you do:
Code: [Select]
if (condition) {
 // Stuff in here
}

Other times you do:
Code: [Select]
if (condition)
{
 // Stuff in here
}

It doesn't really matter which you do, but BE CONSISTENT! If you're trying to match up brackets in nested if statements or loops, it's a nightmare unless you're strict about stuff like this. I also set my tabs to 4 spaces so I get a nice big visible indent for each section and sub-section. That makes it visually easier to see what's connected to where.

Here's a messy example:
Code: [Select]
if (condiion)
{
  // Now we do some stuff
  // Ok, so do it.
  if (subcondition) {
    // Do some other stuff
  }
}

And here's a clearer example:
Code: [Select]
if (condiion) {
    // Now we do some stuff
    // Ok, so do it.
    if (subcondition) {
        // Do some other stuff
    }
}

I think you're in danger of getting lost in your clauses and sub-clauses. As an example, this statement does nothing:
Code: [Select]
  while ( rampback == true && rampvalue != 0 || rampback == false && rampvalue != 255 )

because it is followed by two statements that say:
Code: [Select]
while ( rampback == false && rampvalue != 255)
and
Code: [Select]
while ( rampback == true && rampvalue != 0 )
Clearly neither of those subsections could happen unless those initial conditions are true, so testing them on the way in is redundant - you're adding an extra layer of indenting to no purpose.

The logic is getting quite tangled here with all the options and sub-options you have. Well done for making it work, but I'd wonder if it's not time to sit down with a cool drink and a piece of blank paper and see if you can't find a neater way of capturing the required behaviour.

I think you've really gone far very quickly, so you have my respect and congratulations, and my comments are intended to help you get further whilst avoiding writing messy code that will lead to you getting bogged down in tedious debugging. That takes the fun out of it, and we don't want that!

Good luck!

any

  • Great Contributor!
  • ***
  • Posts: 362
  • Total likes: 13
  • a.k.a. whythisreason? Niels H.
    • http://www.anyplace.nl
I'm still following along! It's fascinating to see someone else working through stuff that you've done yourself. Different people get to similar places by quite different routes most often, which keeps things interesting!

Ha, good I was envisaging myself as some recluse in a cave working on something that only made sense in my own little world... hahaha!

Quote
I've got a few comments, but they're mostly just about code style. You'll get away with doing it however you like for so long, but it ultimately pays off to develop a good style because it means you have less things to think about, and that helps when debugging and saves you time.

Duly noted, I've got a session planned for that. Want to release a nice clean understandable sketch that everyone can read/adapt. ;-)

Quote
I think you're in danger of getting lost in your clauses and sub-clauses. As an example, this statement does nothing:
Code: [Select]
  while ( rampback == true && rampvalue != 0 || rampback == false && rampvalue != 255 )

because it is followed by two statements that say:
Code: [Select]
while ( rampback == false && rampvalue != 255)
and
Code: [Select]
while ( rampback == true && rampvalue != 0 )
Clearly neither of those subsections could happen unless those initial conditions are true, so testing them on the way in is redundant - you're adding an extra layer of indenting to no purpose.

The logic is getting quite tangled here with all the options and sub-options you have. Well done for making it work, but I'd wonder if it's not time to sit down with a cool drink and a piece of blank paper and see if you can't find a neater way of capturing the required behaviour.

Yeah, I know that all looks a bit convoluted  :icon_lol:
The logic behind it though (at least to me ) makes sense. I want the longer ramp time to have this 'infinite' flip/flop type switch
until it is either '0' (off) or '255' (on) to make them 'exit' the ramp function to the appropriate next step.
So with the two specific 'while' loops it will keep ramping either up or down continuously until it reaches that 'end' condition.

I did find a bug yesterday that had me add another condition (lol), but this is due to how I've utilised the 'onebutton' library.
After cancelling a 'ramp down' in latching mode, it goes back to the 'single click' function , which in turn would start turning on the LED again.

So, yes I agree the logic is getting a bit tangled  ;D and Sitting down with a cold drink and looking at this with fresh eyes might
present a more streamlined solution, ha!

Quote
I think you've really gone far very quickly, so you have my respect and congratulations, and my comments are intended to help you get further whilst avoiding writing messy code that will lead to you getting bogged down in tedious debugging. That takes the fun out of it, and we don't want that!

Good luck!

Thanks, no worries there, I completely see it as words of encouragement (My dayjob deals with lots of critique, and it is very useful). I knew I was getting into trouble when I started dreaming up this latched/momentary up/down ramping... I'll be keen to do a demo video of it though, as I think it's a very 'musical' and useful way to engage an effect. I'm going to use the same logic on my rehoused Digitech Multiplay (which is working great now btw!) But that'll need more work to interface. (although feedback would be easy enough as I already had a 'slam to 10' footswitch on it previously)
It's supposed to sound that way.

niektb

I'm still following along as well! I haven't been able to start coding my MIDI Controller (specifically designed for the M5 as well  :icon_mrgreen: ) Although I'm probably going down an entirely different route as well  :icon_mrgreen:
(as I have 3 buttons and I also want preset selection)

any

  • Great Contributor!
  • ***
  • Posts: 362
  • Total likes: 13
  • a.k.a. whythisreason? Niels H.
    • http://www.anyplace.nl
I'm still following along as well! I haven't been able to start coding my MIDI Controller (specifically designed for the M5 as well  :icon_mrgreen: ) Although I'm probably going down an entirely different route as well  :icon_mrgreen:
(as I have 3 buttons and I also want preset selection)

I found using the Onebutton library really easy to implement, and the 'double tap' on the effect button switches between 'preset select' and 'latching mode for the modulation'
I'm planning another version for the M9 as an external unit, which will have midi, and more user control with a 16x2 display and rotary encoder.
But first let's get this one done and dusted!

Picture from the workbench, practically finished the prototype pcb and decided on a ribbon cable interconnect as all controls will be fixed to the housing and the board will live just above the DSP.
Still wondering if I need some kind of shielding from the DSP and if it will get hot, but it looks like the best place and it would still allow removing the main pcb for any future servicing.


It's supposed to sound that way.

potul

I also use OneButton. If you are interested I have a modified version of OneButton that is able to handle two button pushes at the same time triggering different actions.

any

  • Great Contributor!
  • ***
  • Posts: 362
  • Total likes: 13
  • a.k.a. whythisreason? Niels H.
    • http://www.anyplace.nl
I also use OneButton. If you are interested I have a modified version of OneButton that is able to handle two button pushes at the same time triggering different actions.

Sure, can come in handy some time. Although I was actually trying to get rid of that very functionality on the M5...  :icon_lol:

Cheers
It's supposed to sound that way.