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

any

  • Great Contributor!
  • ***
  • Posts: 362
  • Total likes: 15
  • a.k.a. whythisreason? Niels H.
    • http://www.anyplace.nl
Hi,

Been working my first forage into 'programmed' fx related things..
Partly based/inspired on a previous post (https://www.diystompboxes.com/smfforum/index.php?topic=121506.20) but with some added functionality.

I'm building this into a Line6 M5 which accepts a 10k pot as expression input.

So far, what it does is the following:
- tap tempo (working on syncing this up with Line6 internal tap)
- manual 'speed' override for tempo (also needed to set toe up/down settings on the line6 as it needs to 'see' a 10k resistance max on the expression input to recognise something)
- 6 selectable waveforms (6 position mini rotary) on(just max brightness), square, sine, ramp up, ramp down, random
- momentary or latching (not fully implemented in v1.0 code yet, just momentary for now, so effect on while foot is down, will implement a "double tap function" to switch between momentary or latching)
- When in momentary mode, a single tap will bring up the M5 preset menu. (only useful for Line6 M5/9 owners?)
- Ramp up and ramp down (Currently on all waveforms in v1.0 but will be selectable fast/slow/off)

Uses the Onebutton and JLed libraries so very easy to modify.

I'll post a slightly cleaned up version of current code tonight, but still working through the full feature set.
I have already tested the current version with a Line6 M5 and it sounds great!

More to come :-)
 
It's supposed to sound that way.

Blackaddr

Demo video?
Blackaddr Audio
Digital Modelling Enthusiast
www.blackaddr.com

niektb

Following! I have an M5 also and I'm working on a tiny midi controller 😋 (3 foot switches in a 1590A)

any

  • Great Contributor!
  • ***
  • Posts: 362
  • Total likes: 15
  • a.k.a. whythisreason? Niels H.
    • http://www.anyplace.nl
Yep, will do a demo when it is functionally complete.
Currently slightly difficult w. Breadboard,  trimpots, etc...
It's supposed to sound that way.

any

  • Great Contributor!
  • ***
  • Posts: 362
  • Total likes: 15
  • a.k.a. whythisreason? Niels H.
    • http://www.anyplace.nl
No demo yet, but finished a lot of the functionalities.

- Latching and Momentary option for engaging the LED/LDR ('single tap on/single tap off' for latching, 'longpress' for momentary)
- Double tap to switch (with indicator led) to an alternate mode where single tap is not latching the effect, but switches the Line6 M5/M9 presets. Longpress is still momentary effect.
- 6 Waveforms, but depending on your switch arrangement can be anything you like. ;-)
- Tap tempo with pot override
- Needs a 10 Pot inline with the LDR for the Line6 to recognise it, which doubles as and 'intensity' control (manually set fully clockwise (10) for full effect.)
- Ramp up/Ramp down, currently only on the 'always on' led as it's doesn't sound that good with modulated waveforms but works quite well.

Here's the code I got so far, if anyone wants to see if things can be simplified or cleaned up comments are welcome.
I've tried to add as much annotations as possible to explain what everything does.

V1.2
Code: [Select]
#include <OneButton.h> // The OneButton Library
#include <jled.h> // The Jled Library

OneButton effectbutton = OneButton(
  7,        // Input pin for the button
  true,     // Button is active LOW
  true      // Enable internal pull-up resistor
);

#define THRESHOLD 30    // Threshold for the 'tempo pot' to overtake the tapped tempo
#define EXP1 A0         // Tempo pot attached to pin A0
#define MAXDELAY 3000   // Maximum delay time in Msec that can be achieved with the tempo pot
#define MINDELAY 1      // Maximum delay time in Msec that can be achieved with the tempo pot
#define MAXTAP 8000      // Maximum delay time in Msec that can be achieved with the tap tempo

//Declaring the names of the waveforms and pins
/* This will vary on how you set up the waveform selection. I've used a 6 position rotary switch that has
two pairs of common pins at opposite sides having 4 contacts each.(bit of a weird setup, but that's what I had lying around)
Should also work with a standard 2 pin 6 position rotary dial wired to 3 digital inputs. Each has a unique combination
of pin states. Pins are high by default and pulled to ground to set. Based on what type of waveform selection solution
you come up with you can theoretically have x amount of options as Jled allows for 'user function' waveforms :-)*/
auto ledtempo = JLed(6);      // This Led is always on and provides visual tempo feedback
auto ledon = JLed(10);        // Technically just 'always on' so not really a 'waveform'
auto ledsquare = JLed(10);    // 50% on 50% off
auto ledsine = JLed(10);      // Smooth cycle between 0% and 100%
auto ledrampup = JLed(10);    // Smooth cycle up to 100% for the duration of the tap, then start over
auto ledrampdown = JLed(10);  // Start at 100% with a smooth cycle down to 0%, then start over
auto ledrandom = JLed(10);    // Based on the Jled 'candle' setting, basically random values with user setting for 'jitter'

int period = 2000;            // Default start time for the tempo
int lastVal = 0;
int tempAnalog = 0;
boolean PotControls = true;   // Allows the tempo pot to take over the tapped tempo

// Variables will change:

int buttonPushCounter = 1;    // counter for the number of button presses
int doubleClickCounter = 0;   // counter for the number of doubleclicks
int buttonState = 0;          // current state of the button
int lastButtonState = 0;      // previous state of the button

void setup()
{
 
  effectbutton.attachClick(effectsingleclick);                  // link the function to be called on a singleclick event.
  effectbutton.attachDoubleClick(effectdoubleclick);            // link the function to be called on a doubleclick event.
  effectbutton.attachLongPressStart(effectlongpressstart);      // link the function to be called at the start of a longpress event.
  effectbutton.attachLongPressStop(effectlongpressstop);        // link the function to be called at the end of a longpress event.
  effectbutton.attachDuringLongPress(effectlongpress);          // link the function to be called during a longpress event.
 
  effectbutton.setClickTicks(200);   // Time for registering single Click
  effectbutton.setPressTicks(500);   // Time for registering Long Press
 
  pinMode(2, INPUT_PULLUP );         // Used for waveform selection with a two pin, 6 position rotary dial
  pinMode(3, INPUT_PULLUP );         // Used for waveform selection with a two pin, 6 position rotary dial
  pinMode(4, INPUT_PULLUP );         // Used for waveform selection with a two pin, 6 position rotary dial
  pinMode(5, INPUT_PULLUP );         // tap button - press it to set the tempo
  pinMode(6, OUTPUT);                // This is the LED showing the current tempo
  digitalWrite(8, LOW);              // Make sure this starts low to not trigger the Line6 effect on/off button
  pinMode(8, OUTPUT);                // This goes to Line6 Effect On/Off button
  digitalWrite(9, LOW);              // Make sure this starts low to not trigger the Line6 tap button
  pinMode(9, OUTPUT);                // This goes to Line6 Tap button
  pinMode(10, OUTPUT);               // This goes to the LED/LDR that controls the expression pedal
  digitalWrite(13, LOW);             // Make sure this starts low at startup
  pinMode(13, OUTPUT);               // LED that shows when doubleclick function is active
 
  buttonPushCounter = 1;              // counter for the number of button presses
  doubleClickCounter = 0;             // counter for the number of double clicks
 
  // initialize serial communication (for debugging):
  Serial.begin(9600);
 
}
int lastTapState = LOW;  /* the last tap button state */
unsigned long currentTimer[2] = { 500, 500 };  /* array of most recent tap counts */

void loop()
{
  /* read the button on pin 5, and only pay attention to the
     HIGH-LOW transition so that we only register when the
     button is first pressed down */
  buttonState = digitalRead(7);
  int tapState = digitalRead( 5 );
  if( tapState == LOW && tapState != lastTapState )
    {
      tap(); /* we got a HIGH-LOW transition, call our tap() function */
    }
  lastTapState = tapState; /* keep track of the state */
 
  ledtempo.Update();    // update leds
  delay(1);
 
  effectbutton.tick();  // Check the effect button for click/doubleclick/longpress events
 
  poll_pot();           // check pot value

  /* If the effect is switched on in latching mode, toggle between continuously updating the waveform
  or incrementing the counter when switched off so it's ready for the next event*/
 
  if (buttonPushCounter %2 == 0) {
    update_effect(); }
  else {
      buttonPushCounter = 1;
    }
}

unsigned long lastTap = 0; /* when the last tap happened */

void tap()
{
  /* we keep two of these around to average together later */
  currentTimer[1] = currentTimer[0];
  currentTimer[0] = millis() - lastTap;
  lastTap = millis();
 
  period = ((currentTimer[0] + currentTimer[1])/2);   // establish new period

  PotControls = false;   // override the pot when tapping a tempo

  update_leds();   // update leds period
}

  void update_leds(){   // Update the led's tempo information after tapping or setting the tempo pot
 
  ledtempo = JLed(6).Blink(period/20, period/20*19).Forever(),
  ledon = JLed(10).On().Forever();
  ledsquare = JLed(10).Blink(period/2, period/2).Forever();
  ledsine = JLed(10).Breathe(period).DelayAfter(0).Forever();
  ledrampup = JLed(10).FadeOn(period).DelayBefore(0).Forever();
  ledrampdown = JLed(10).FadeOff(period).DelayBefore(0).Forever();
  ledrandom = JLed(10).Candle(period/286 /*speed*/, 255 /* jitter*/);
}

void update_effect(){   // Keep the led waveform updated in the loop until it's turned off
 
   if (digitalRead(2) == LOW && digitalRead(3) == HIGH && digitalRead(4) == HIGH) { // Always on LED is selected
      ledon.Update();
   }
    if (digitalRead(2) == HIGH && digitalRead(3) == LOW && digitalRead(4) == HIGH) { // Random LED is selected
      ledrandom.Update();
   }
    if (digitalRead(2) == LOW && digitalRead(3) == HIGH && digitalRead(4) == LOW) { // Square wave LED is selected
      ledsquare.Update();
   }
    if (digitalRead(2) == HIGH && digitalRead(3) == LOW && digitalRead(4) == LOW) { // Sine wave LED is selected
      ledsine.Update();
   }
    if (digitalRead(2) == HIGH && digitalRead(3) == HIGH && digitalRead(4) == LOW) { // Ramp up LED is selected
      ledrampup.Update();
   }
    if (digitalRead(2) == HIGH && digitalRead(3) == HIGH && digitalRead(4) == HIGH) { // Ramp down LED is selected
      ledrampdown.Update();
   }
 
}

void poll_pot() {                   // Allow tempo pot reading to override tapped tempo

  tempAnalog = analogRead(EXP1);    // Read analog value
 
  // Convert 10 bit to milliseconds, 3 seconds maximum
  tempAnalog = map(tempAnalog, 0, 1023, 0, MAXDELAY);
  tempAnalog = constrain(tempAnalog, MINDELAY, MAXDELAY);
 
  // Check pot value and compare variation with threshold
  if((abs(tempAnalog-lastVal)>THRESHOLD))
  {
    // update period with pot value
    period = tempAnalog;
    //return control to Pot   
    PotControls = true;
    // Store current value to be used laters
    lastVal = tempAnalog;
    // update leds with new timing
    update_leds();
  }
}


// effect button actions

void effectsingleclick(){
  buttonPushCounter++;      // every time the effect turns on, increment the counter
  if (buttonPushCounter %2 == 0 && doubleClickCounter == 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
   update_effect();         // Run the chosen waveform until the effect is turned off
  }
/* 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 (doubleClickCounter != 0 ) { 
  ledon.Stop();
  ledrandom.Stop();
  ledsquare.Stop();
  ledsine.Stop();
  ledrampup.Stop();
  ledrampdown.Stop();
  digitalWrite(8, !digitalRead(8));         // This goes to Line6 Effect On/Off button
  digitalWrite(9, !digitalRead(9));        // This goes to Line6 Tap button
  delay(100);
  digitalWrite(8, !digitalRead(8));         // This goes to Line6 Effect On/Off button
  digitalWrite(9, !digitalRead(9));        // This goes to Line6 Tap button
  }
/* If there's no doubleclick active, another single click will go to longpresstop,
in case there is a 'ramp down' selected.*/
  else {
    effectlongpressstop();
  }
 
    }

 void effectdoubleclick(){
/* When a double click is detected increment the counter, otherwise reset the
counter to 0, effectively a toggle.*/
  if (doubleClickCounter %2 == 0) {
    doubleClickCounter++;
     Serial.print("double click on");
     Serial.println(doubleClickCounter);
  }
  else {
    doubleClickCounter = 0;
    Serial.print("reset the counter");
  }
  digitalWrite(13, !digitalRead(13));         // Toggle the indicator light for doubleclick on/off
}
   

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();
  // what happens when momentary effect engagement starts (ramp up)
 if (digitalRead(2) == LOW && digitalRead(3) == HIGH && digitalRead(4) == HIGH) { // if Always on LED is selected
  //Fading in the LED
  for(int i=0; i<255; i++){
    analogWrite(10, i);
    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
  }
}
}

void effectlongpress(){
  Serial.println(buttonState);

if (digitalRead(2) == LOW && digitalRead(3) == HIGH && digitalRead(4) == HIGH) { // Always on LED is selected
  ledon.Update();
}
if (digitalRead(2) == HIGH && digitalRead(3) == LOW && digitalRead(4) == HIGH) { // Random LED is selected
  ledrandom.Update();
}
if (digitalRead(2) == LOW && digitalRead(3) == HIGH && digitalRead(4) == LOW) { // Square wave LED is selected
  ledsquare.Update();
}
if (digitalRead(2) == HIGH && digitalRead(3) == LOW && digitalRead(4) == LOW) { // Sine wave LED is selected
  ledsine.Update();
}
if (digitalRead(2) == HIGH && digitalRead(3) == HIGH && digitalRead(4) == LOW) { // Ramp up LED is selected
  ledrampup.Update();
}
if (digitalRead(2) == HIGH && digitalRead(3) == HIGH && digitalRead(4) == HIGH) { // Ramp down LED is selected
  ledrampdown.Update();
}

}

void effectlongpressstop(){
    /* 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. :-) */
  buttonPushCounter = 1;

  // what happens when momentary effect engagement ends (ramp down)
 if (digitalRead(2) == LOW && digitalRead(3) == HIGH && digitalRead(4) == HIGH) { // Always On LED is selected
  for(int i=255; i>0; i--){
    analogWrite(10, i);
    delay(10);  //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(10,LOW);  // Switch off the LED

}
It's supposed to sound that way.

ElectricDruid

Nice work. I think the code is very easy to read. It's very explicit about what it's doing and how, which is definitely a good thing!

There's one or two bits where I wondered if there might not be a more efficient way of doing it, but the "long hand" way you've done it is functional and clear. One example:

Code: [Select]
if (digitalRead(2) == LOW && digitalRead(3) == HIGH && digitalRead(4) == HIGH) { // Always on LED is selected
      ledon.Update();
   }
    if (digitalRead(2) == HIGH && digitalRead(3) == LOW && digitalRead(4) == HIGH) { // Random LED is selected
      ledrandom.Update();
   }
    if (digitalRead(2) == LOW && digitalRead(3) == HIGH && digitalRead(4) == LOW) { // Square wave LED is selected
      ledsquare.Update();
   }
    if (digitalRead(2) == HIGH && digitalRead(3) == LOW && digitalRead(4) == LOW) { // Sine wave LED is selected
      ledsine.Update();
   }
  // etc etc

This should definitely have "elseif"s for the subsequent statements, so the code doesn't have to test every single one. Only one waveform is selected at once, right?

I wondered if it wouldn't be possible to convert the digitalRead states to a integer and then do a Switch statement. I mean, the intent here is to read a three-bit binary number from the inputs, so all this logic testing stuff seems a bit excessive.

something like:

Code: [Select]
// Evaluate the binary value on the input and convert to 0-7
waveVal = 0;
if (digitalRead(2) == HIGH) { waveVal += 4; }
if (digitalRead(3) == HIGH) { waveVal += 2; }
if (digitalRead(4) == HIGH) { waveVal += 1; }
// It's a shame there isn't a neater way to do this, but I can't think of one without reading the port directly.

// Light the correct LED
switch (waveVal) {
case 0:
  // Light an LED
  ledsine.Update();
  break;
case 1:
  // Light an LED
  ledon.Update();
  break;
case 2:
  // etc etc
  break;
case 3:
  // etc etc
  break;
// etc etc

// You choose if you need this:
default:
  // statements
  break;
}

Whether this is an improvement is an open question. More a question of style, I'd say.
What I'd really like would be something like the following:

Code: [Select]
// Evaluate the binary value on the input and convert to 0-7
waveVal = 0;
if (digitalRead(2) == HIGH) { waveVal += 4; }
if (digitalRead(3) == HIGH) { waveVal += 2; }
if (digitalRead(4) == HIGH) { waveVal += 1; }

// Light the correct LED by passing the value to the function
UpdateLed(waveVal) {

The UpdateLed function is left as an exercise for the reader! I have no idea how you'd do that for Arduino code, sorry!

niektb

@the issue ElectricDruid raised. All the digitalReads definitely make it more difficult to read. Not using elseifs can also cause timing issues (like the digitalRead changing when it's in the body of an if-statement such that the body of a 2nd if-statement might also be executed).

Me personally, I would read the 3 ports once, convert them into a hex value using a bit shift operation, and test against that hex value :)

Firesledge

You could replace all the pin numbers with constants, too. The code will be more meaningful and this will ease pin number changes.
Pédale Vite, multi-FX pedalboard

any

  • Great Contributor!
  • ***
  • Posts: 362
  • Total likes: 15
  • a.k.a. whythisreason? Niels H.
    • http://www.anyplace.nl
@ElectricDruid, @Niektb, @Firesledge Thanks For your comments ;-) Literally my first foray into Arduino, or programming for that matter, so plenty to learn... Yes I understand now how the waveform selection could be streamlined for both switching and updating the led.
Also, the Jled library has plenty of scope to add more useful waveforms quite easily, as well as Multi tap patterns. Might even try have 5 presets and one ‘manual’ waveshape with pot controls? ( Attack, decay, peak, etc)

I’ll work on this some more when I have some time, and probably do a ‘boxed’ version in an old project box so I can demo it with my M9 prior to building it in to the M5. I’m planning to leave the led/ldr socketed as well as the tempo pot on a connector so I can tweak/test on different effect units and circuits by swapping the ‘interface end’ of it.

Will try and do a demo soon ;-)
It's supposed to sound that way.

any

  • Great Contributor!
  • ***
  • Posts: 362
  • Total likes: 15
  • a.k.a. whythisreason? Niels H.
    • http://www.anyplace.nl
Bit of a demo with what it currently does. ( I haven't tweaked any presets specifically for this type of expression modulation)
I'll have more of a play when the tempo pot is useable and it's in some kind of enclosure.

Also thinking about a 'shift button' that will allow 12 waveform presets as I like the idea of a 'one shot' function
that does a ramp, sine, etc.. over a longer period (4x the tap time?) and then stops. anyway... you get the idea.

It's supposed to sound that way.

any

  • Great Contributor!
  • ***
  • Posts: 362
  • Total likes: 15
  • a.k.a. whythisreason? Niels H.
    • http://www.anyplace.nl
Demo video?

See previous post, not really optimised any patches on the M9, but should give you an idea.
Think I’ll implement a ‘bar-time’ function as well as i think it could sound cool if the rate takes a full bar (tempo x 4) for more sweeping type effects.
It's supposed to sound that way.

any

  • Great Contributor!
  • ***
  • Posts: 362
  • Total likes: 15
  • a.k.a. whythisreason? Niels H.
    • http://www.anyplace.nl
Cleaned up the code a bit more, and implemented the suggestions
(in this case replacing all pin numbers with constants and the way the presets are selected)

Thanks for the input so far, working well and working on a couple of patches on my M9 that show
some of the cool stuff you can do when you automate the expression pedal.  :icon_biggrin:
Imma calling it "Tap Express" for now, as it sounds kinda nice.

Thinking hard about what is most useful vs complexity, so still working on that.
Need to implement something that cancels the ramp up function if momentary is released or latching is pressed again while it's still ramping up.
Currently there's no interrupt so it'll keep going until it's done the 0 to 255 before it accepts anything else.

v2 code below, and a quick video demo of the breadboard prototype a couple of posts back.

Code: [Select]
#include <OneButton.h> // The OneButton Library
#include <jled.h> // The Jled Library

// Naming the pins
/* If you're using different pins then an Arduino Nano (I'm using a chinese DCCduino)
You only have to change pin numbers here, all further instances will go back here.*/

int effectbutton = 7;    // Footswitch to turn the effect on and off (and with 'shift function' active, switch to the Line6 preset screen.)
int tapbutton = 5;       // Footswitch tap tempo input
int effectled = 10;      // The output driving the LED/LDR
int tempoled = 6;        // The Led providing visual tempo feedback
int tempopot = A0;       // The connection for the manual tempo pot (10kB, middle lug to the pin, with ground and 5v either side)

/* I've included 3 inputs for waveform selection, which (if you have the right switching setup)
gives you 8 selectable presets. However, if using a 'shift function' (one extra input) this doubles to 16.
A future version is planned with some 'single shot' presets that might use such a setup.
Pins are high by default and pulled to ground to set.*/

int waveselect1 = 2;      // First input line for waveform selection
int waveselect2 = 3;      // Second input line for waveform selection
int waveselect3 = 4;      // Third input line for waveform selection

int line6effect = 8;      // This DOES NOT go straight to the Line6 footswitch (as it uses 3.3v logic) first to a CD4066 that pulls the Line6 footswitch LOW
int line6tap = 9;         // This DOES NOT go straight to the Line6 footswitch (as it uses 3.3v logic) first to a CD4066 that pulls the Line6 footswitch LOW

/* You can use the shift function for the above mentioned expanded presets (from 8 to 16)
if you're not using the footswitch to select presets on a Line6 M5 or M9. Will need the appropriate coding, which is currently in development*/
int shiftfunction = 13;   // Double tapping the effect footswitch engages the 'shift function' and lights the shift LED to show it is active

//Declaring the waveforms
/* This will vary on how you set up the waveform selection. Based on what type of waveform selection solution
you come up with you can theoretically have x amount of options as Jled is extremely flexible when it comes
to generating waveforms :-) check out: https://github.com/jandelgado/jled */

auto ledtempo = JLed(tempoled);      // This Led is always on and provides visual tempo feedback
auto ledon = JLed(effectled);        // Technically just 'always on' so not really a 'waveform'
auto ledsquare = JLed(effectled);    // 50% on 50% off
auto ledsine = JLed(effectled);      // Smooth cycle between 0% and 100%
auto ledrampup = JLed(effectled);    // Smooth cycle up to 100% for the duration of the tap, then start over
auto ledrampdown = JLed(effectled);  // Start at 100% with a smooth cycle down to 0%, then start over
auto ledrandom = JLed(effectled);    // Based on the Jled 'candle' setting, basically random values with user setting for 'jitter'

//Defining values for the tap tempo functions

#define THRESHOLD 30    // Threshold for the 'tempo pot' to overtake the tapped tempo. 20 is default, increase if too glitchy
#define MAXDELAY 3000   // Maximum delay time in Msec that can be achieved with the tempo pot
#define MINDELAY 10     // Maximum delay time in Msec that can be achieved with the tempo pot
#define MAXTAP 8000     // Maximum delay time in Msec that can be achieved with the tap tempo

//Declaring the 'Onebutton' footswitch
OneButton effect = OneButton(
  effectbutton,   // Input pin for the button
  true,           // Button is active LOW
  true            // Enable internal pull-up resistor
);

// Variables that will change when running:

int buttonPushCounter = 0;    // counter for the number of button presses
int doubleTapCounter = 0;     // counter for the number of doubletaps (heh, Zombieland! :-)
int lastTapState = LOW;       // the last tap button state
int period = 2000;            // Default start time for the tempo
int lastVal = 0;              // Initialise the tap tempo button
int tempAnalog = 0;           // Initialise the manual tempo pot
boolean PotControls = true;   // Allows the tempo pot to take over the tapped tempo
unsigned long currentTimer[2] = { 500, 500 };  // array of most recent tap counts
unsigned long lastTap = 0;    // when the last tap happened */

void setup()

  // Binding functions to what happens to the effectbutton
  effect.attachClick(effectsingleclick);                  // link the function to be called on a singleclick event.
  effect.attachDoubleClick(effectdoubleclick);            // link the function to be called on a doubleclick event.
  effect.attachLongPressStart(effectlongpressstart);      // link the function to be called at the start of a longpress event.
  effect.attachLongPressStop(effectlongpressstop);        // link the function to be called at the end of a longpress event.
  effect.attachDuringLongPress(effectlongpress);          // link the function to be called during a longpress event.

  // Set timings for the effectbutton, you can tweak these to get it working just how you like it
  effect.setClickTicks(200);   // Time for registering single click. 200 works well but if your double clicks aren't reliable increase it a bit (start with 100 increment)
  effect.setPressTicks(500);   // Time for registering Long Press. 500 (half a second) is good enough for me, but see if shorter still works for you)

  // Set pin modes for all the pins, and pull them high or low at the start if needed
  pinMode(waveselect1, INPUT_PULLUP );         // Used for waveform selection with a two pin, 6 position rotary dial
  pinMode(waveselect2, INPUT_PULLUP );         // Used for waveform selection with a two pin, 6 position rotary dial
  pinMode(waveselect3, INPUT_PULLUP );         // Used for waveform selection with a two pin, 6 position rotary dial
  pinMode(tapbutton, INPUT_PULLUP );           // tap button - press it to set the tempo
  pinMode(tempoled, OUTPUT);                   // This is the LED showing the current tempo
  digitalWrite(line6effect, LOW);              // Make sure this starts low to not trigger the Line6 effect on/off button
  pinMode(line6effect, OUTPUT);                // This goes to Line6 Effect On/Off button
  digitalWrite(line6tap, LOW);                 // Make sure this starts low to not trigger the Line6 tap button
  pinMode(line6tap, OUTPUT);                   // This goes to Line6 Tap button
  pinMode(effectled, OUTPUT);                  // This goes to the LED/LDR that controls the expression pedal
  digitalWrite(shiftfunction, LOW);            // Make sure this starts low at startup
  pinMode(shiftfunction, OUTPUT);              // LED that shows when doubleclick function is active
 
  // initialize serial communication (for debugging):
  Serial.begin(9600);
}

void loop()
{
  /* read the button on pin 5, and only pay attention to the
     HIGH-LOW transition so that we only register when the
     button is first pressed down */
  int tapState = digitalRead( tapbutton );
  if( tapState == LOW && tapState != lastTapState )
    {
      tap(); /* we got a HIGH-LOW transition, call our tap() function */
    }
  lastTapState = tapState; /* keep track of the state */
 
  ledtempo.Update();    // update leds
  delay(1);
 
  effect.tick();  // Check the effect button for click/doubleclick/longpress events
 
  poll_pot();           // check pot value

  /* If the effect is switched on in latching mode, toggle between continuously updating the waveform
  or incrementing the counter when switched off so it's ready for the next event*/
 
  if (buttonPushCounter %2 != 0) {
    update_effect(); }
  else {
      buttonPushCounter = 0;
    }
}

void tap()
{
  /* we keep two of these around to average together later */
  currentTimer[1] = currentTimer[0];
  currentTimer[0] = millis() - lastTap;
  lastTap = millis();
 
  period = ((currentTimer[0] + currentTimer[1])/2);   // establish new period

  PotControls = false;   // override the pot when tapping a tempo

  update_leds();   // update leds period
}

void update_leds(){   // Update the led's tempo information after tapping or setting the tempo pot
 
  ledtempo = JLed(tempoled).Blink(period/20, period/20*19).Forever(),
  ledon = JLed(effectled).On().Forever();
  ledsquare = JLed(effectled).Blink(period/2, period/2).Forever();
  ledsine = JLed(effectled).Breathe(period).DelayAfter(0).Forever();
  ledrampup = JLed(effectled).FadeOn(period).DelayBefore(0).Forever();
  ledrampdown = JLed(effectled).FadeOff(period).DelayBefore(0).Forever();
  ledrandom = JLed(effectled).Candle(period/286 /*speed*/, 255 /* jitter*/);
}

void update_effect(){   // Keep the led waveform updated in the loop until it's turned off
 
  int waveVal = 0;          // 3bit value that selects the right preset based on sum of the waveselect read below
  if (digitalRead(2) == HIGH) { waveVal += 4; }
  if (digitalRead(3) == HIGH) { waveVal += 2; }
  if (digitalRead(4) == HIGH) { waveVal += 1; }

  // 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 poll_pot() {                   // Allow tempo pot reading to override tapped tempo

  tempAnalog = analogRead(tempopot);    // Read analog value
 
  // Convert 10 bit to milliseconds, 3 seconds maximum
  tempAnalog = map(tempAnalog, 0, 1023, 0, MAXDELAY);
  tempAnalog = constrain(tempAnalog, MINDELAY, MAXDELAY);
 
  // Check pot value and compare variation with threshold
  if((abs(tempAnalog-lastVal)>THRESHOLD))
  {
    // update period with pot value
    period = tempAnalog;
    //return control to Pot   
    PotControls = true;
    // Store current value to be used laters
    lastVal = tempAnalog;
    // update leds with new timing
    update_leds();
  }
}


// effect button actions

void effectsingleclick(){
   
  buttonPushCounter++;      // every time the effect turns on, increment the counter
  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
   update_effect();         // Run the chosen waveform until the effect is turned off
  }
  /* 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));         // This goes to Line6 Effect On/Off button
  digitalWrite(line6tap, !digitalRead(line6tap));        // This goes to Line6 Tap button
  delay(100);
  digitalWrite(line6effect, !digitalRead(line6effect));         // This goes to Line6 Effect On/Off button
  digitalWrite(line6tap, !digitalRead(line6tap));        // This goes to Line6 Tap button
  }
/* If there's no doubleclick active, another single click will go to longpresstop,
in case there is a 'ramp down' selected.*/
  else {
    effectlongpressstop();
  }
}

void effectdoubleclick(){
/* When a double click is detected increment the counter, otherwise reset the
counter to 0, effectively a toggle.*/
  if (doubleTapCounter %2 == 0) {
    doubleTapCounter++;
     Serial.print("double click on");
     Serial.println(doubleTapCounter);
  }
  else {
    doubleTapCounter = 0;
    Serial.print("reset the counter");
  }
  digitalWrite(shiftfunction, !digitalRead(shiftfunction));         // Toggle the indicator light for doubleclick on/off
}
   

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();
  // what happens when momentary effect engagement starts (ramp up)
 if (digitalRead(waveselect1) == LOW && digitalRead(waveselect2) == HIGH && digitalRead(waveselect3) == HIGH) { // if Always on LED is selected
  //Fading in the LED
  for(int i=0; i<255; i++){
    analogWrite(10, i);
    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
  }
}
}

void effectlongpress(){

  int waveVal = 0;          // 3bit value that selects the right preset based on sum of the waveselect read below
  if (digitalRead(2) == HIGH) { waveVal += 4; }
  if (digitalRead(3) == HIGH) { waveVal += 2; }
  if (digitalRead(4) == HIGH) { waveVal += 1; }

  // 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(){
  /* 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. :-) */
  buttonPushCounter = 0;

  // what happens when momentary effect engagement ends (ramp down)
  if (digitalRead(waveselect1) == LOW && digitalRead(waveselect2) == HIGH && digitalRead(waveselect3) == HIGH) { // Always On LED is selected
  for(int i=255; i>0; i--){
    analogWrite(10, i);
    delay(10);  //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
}
« Last Edit: May 27, 2021, 07:26:52 AM by any »
It's supposed to sound that way.

niektb

You could abbreviate the triple if-statement to
Code: [Select]
int waveVal = digitalRead(4) << 2 | digitalRead(3) << 1 | digitalRead(2);Also, you're not very consistent with where you put your brackets, it will improve readability if you do :)
But these are details, looks pretty cool overal!

ElectricDruid

One other small nit-pick:

The pin set-up definitions should be defined as constants, like this:

Code: [Select]
const int tapbutton = 5;       // Footswitch tap tempo input

The reason for doing this is that if the compiler knows that these are constants, it will give you a useful error message if tapbutton gets set to some other value by accident later on in the code.

+1 what Niektb said. These are details and it's looking good now.

niektb

Do the Line6 M-series convert the exp input to discrete values? Does anybody know that?
So could it for example be possible to achieve the same functionality using MIDI?

any

  • Great Contributor!
  • ***
  • Posts: 362
  • Total likes: 15
  • a.k.a. whythisreason? Niels H.
    • http://www.anyplace.nl
Do the Line6 M-series convert the exp input to discrete values? Does anybody know that?
So could it for example be possible to achieve the same functionality using MIDI?

Actually, just checked and yes, they accept midi CC 0-127 for expression input.
Would that convert easily from pwm or is it a whole other approach?
It's supposed to sound that way.

niektb

Do the Line6 M-series convert the exp input to discrete values? Does anybody know that?
So could it for example be possible to achieve the same functionality using MIDI?

Actually, just checked and yes, they accept midi CC 0-127 for expression input.
Would that convert easily from pwm or is it a whole other approach?

No MIDI is quite different from that, you need to use the uart on your Arduino :)

any

  • Great Contributor!
  • ***
  • Posts: 362
  • Total likes: 15
  • a.k.a. whythisreason? Niels H.
    • http://www.anyplace.nl
Do the Line6 M-series convert the exp input to discrete values? Does anybody know that?
So could it for example be possible to achieve the same functionality using MIDI?

Actually, just checked and yes, they accept midi CC 0-127 for expression input.
Would that convert easily from pwm or is it a whole other approach?


No MIDI is quite different from that, you need to use the uart on your Arduino :)

Ok, so for the sake of fun&games I've done the following:
- re-arranged the pins to free up d2 which is an interrupt pin
- attached an interrupt
Code: [Select]
  Serial.begin(115200);
  // when pin D2 goes high, call the rising function
  attachInterrupt(0, rising, RISING);
- the added the following functions
Code: [Select]
void rising() {
  attachInterrupt(0, falling, FALLING);
  prev_time = micros();
}
 
void falling() {
  attachInterrupt(0, rising, RISING);
  pwm_value = micros()-prev_time;
  pwm_value = map(pwm_value, 8, 1828, 0, 127);
  Serial.println(pwm_value);
}
- Connected LED output to the LDR to pin 2

Looking at the serial monitor I now get 0-127 values when the effect is engaged (aside from 'always on' and 'squarewave')
This gives me a mapped value 0 to 127 that I can then send over midi no? (not implemented yet, but looks easy enough)
So that way you can have either midi or analog 'expression control modulation'

Only thing i notice at the moment is it doesn't read continous on the square wave as it's only on or off so no good for the interrupt?
(or is there a way to make that work)

Also, is this ok performance wise? or is it inefficient to have a routine like that running?

Mind you, I'm new to this, so just trying things out... Ha!
« Last Edit: May 27, 2021, 07:43:17 PM by any »
It's supposed to sound that way.

any

  • Great Contributor!
  • ***
  • Posts: 362
  • Total likes: 15
  • a.k.a. whythisreason? Niels H.
    • http://www.anyplace.nl
Ok, apart from the above folley (would be nice to have both midi and analog options available, and would definitely expand compatibility ) I’m having a hard time trying to ‘abort’ the ramp up function in ‘longpressstart’. That is, i can get a state change detection working that switches successfully to ‘longpressstop’ (So during the ramp up, i can press the footswitch again and it will immediately ramp down from 255 to 0, so that works) but it then returns to the function instead of the loop and the led stays on no matter what i’ve tried (if, if else, break, return, etc) might just be looking at it the wrong way?

What i’m trying to achieve is a ‘ramp up’ that - when aborted (a state change during the ramping up) will ramp down again from the value where it was aborted. So assume i need to keep the current value as a ‘int rampbrightness = X;’ or something so when longpressstop takes over it will not start at 255 but rather the stored value. Any ideas how to make that work? I’ll keep digging but any input appreciated. ;-) already learned a lot over the past week. Cheers
« Last Edit: May 29, 2021, 08:09:48 AM by any »
It's supposed to sound that way.

any

  • Great Contributor!
  • ***
  • Posts: 362
  • Total likes: 15
  • a.k.a. whythisreason? Niels H.
    • http://www.anyplace.nl
Ok, apart from the above folley (would be nice to have both midi and analog options available, and would definitely expand compatibility ) I’m having a hard time trying to ‘abort’ the ramp up function in ‘longpressstart’. That is, i can get a state change detection working that switches successfully to ‘longpressstop’ (So during the ramp up, i can press the footswitch again and it will immediately ramp down from 255 to 0, so that works) but it then returns to the function instead of the loop and the led stays on no matter what i’ve tried (if, if else, break, return, etc) might just be looking at it the wrong way?

What i’m trying to achieve is a ‘ramp up’ that - when aborted (a state change during the ramping up) will ramp down again from the value where it was aborted. So assume i need to keep the current value as a ‘int rampbrightness = X;’ or something so when longpressstop takes over it will not start at 255 but rather the stored value. Any ideas how to make that work? I’ll keep digging but any input appreciated. ;-) already learned a lot over the past week. Cheers

Ok, Think I cracked that one... the ramp-up can be 'aborted' at any time (both in momentary and latching) and it will fade the LED back to off and reset (back to loop) from whatever value the ramp-up function left it. Ideally I would have it endlessly back-and-forth but for now it will not allow the 'abort' function to be interrupted until it is back to the main loop. Still, interesting possibilities as you can 'pump' the effect manually now for short swells.

Code: [Select]
void effectsingleclick()
{   
  buttonPushCounter++;      // every time the effect turns on, increment the counter
  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){
    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));         // This goes to Line6 Effect On/Off button
  digitalWrite(line6tap, !digitalRead(line6tap));               // This goes to Line6 Tap button
  delay(100);
  digitalWrite(line6effect, !digitalRead(line6effect));         // This goes to Line6 Effect On/Off button
  digitalWrite(line6tap, !digitalRead(line6tap));               // This goes to Line6 Tap button
  }
/* 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;
  int buttonState = digitalRead(effectbutton);
  int lastButtonState = digitalRead(effectbutton);
  // what happens when momentary effect engagement starts (ramp up)
  int waveVal = digitalRead(waveselect1) << 2 | digitalRead(waveselect2) << 1 | digitalRead(waveselect3); // 3bit value that selects the right preset based on sum of the waveselect read below
  // Light the correct LED
  switch (waveVal)
  {
    case 3:
     //Fading in the LED
    for(rampvalue; rampvalue<255; rampvalue++)
    {
      buttonState = digitalRead(effectbutton);
      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
      lastButtonState = digitalRead(effectbutton);
      if(buttonState != lastButtonState)
        {
          for(rampvalue; 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
          }
         cancelrampup = 1;
         buttonPushCounter = 2;
         return;
        }
    }
    break;
  }
}

void effectlongpress()
{
  int waveVal = digitalRead(waveselect1) << 2 | digitalRead(waveselect2) << 1 | digitalRead(waveselect3); // 3bit value that selects the right preset based on sum of the waveselect read below
  // 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()
{
  /* 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. :-) */

  // what happens when momentary effect engagement ends (ramp down)
  int waveVal = digitalRead(waveselect1) << 2 | digitalRead(waveselect2) << 1 | digitalRead(waveselect3); // 3bit value that selects the right preset based on sum of the waveselect read below
  Serial.println(cancelrampup);
  if (cancelrampup == 1)
  {
    int rampvalue = 0;
    cancelrampup = 0;
    buttonPushCounter = 0;
    return;
  }
  // Light the correct LED
  switch (waveVal)
  {
    case 3:
     //Fading in 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;
    break;
  }
}
It's supposed to sound that way.