Strobe Light Controller Midi clock read - Needs refining

Hey fellow forum members!

A good long time ago I built a box for controlling a strobe light with the help of some people on here.

I've just started an update to the project to get the strobe light to synchronise with Virtual DJ using the Midi Clock.

The controller is an UNO R3 running dual MocoLufa firmware on the ATMega16U2.
I have 3 arcade buttons with pullup resistors on the input pins that will give me 1, 2 or 4 flashes per beat. I also have 1 button that will just flash once when pushed.
The strobe is wired up to the LED Pin13.

I've run into several problems getting the light to synchronise with the downbeat because VDJ doesn't send Song Position Pointer messages as far as I'm aware, so whilst it can count the midi clock messages it can't align them to the music. So I've parked that for now and am focusing on using the midi clock to calculate the BPM to set the intervals between flashes.
So far I've used a combination of google searching and ChatGPT to build some code that is almost there.

The problem I'm having with the code now is that If I push and hold a button it doesn't always flash immediately, sometimes there's a delay, it's a pain if I'm trying to manually synchronise by timing the button push.
Also if I hold the button the flashes begin to drift, I have a feeling it's because of the code that's counting the midi messages.

Can anyone help with these two problems?

Thanks for your time.

byte midi_clock = 0xf8;
unsigned long int startTime;
int beatCount;
bool counting = false;
long int bpm;
long int noteTime;

const int StrobePin = 13;
const int buttonPins[] = {2, 3, 4};
long int intervals[3];  // Interval values for 1, 2, and 4 flashes per beat

#define buttonPin11 11
const int numButtons = sizeof(buttonPins) / sizeof(buttonPins[0]);

boolean isButtonPressed = false;
long previousMillis = 0;
long interval = 1000;
boolean isStrobeOn = LOW;

void setup() {
  Serial.begin(31250); // Set baud rate to 31250 for MIDI communication

  pinMode(StrobePin, OUTPUT);
  for (int i = 0; i < numButtons; i++) {
    pinMode(buttonPins[i], INPUT);
  }
  pinMode(buttonPin11, INPUT);
  
  // Set default intervals
  intervals[0] = 200;  // 1 flash per beat
  intervals[1] = 100;  // 2 flashes per beat
  intervals[2] = 50;   // 4 flashes per beat
}

void loop() {
  while (Serial.available()) {
    byte midiByte = Serial.read();
    if (midiByte == midi_clock) {
      if (!counting) {  // First time you have seen a clock message in this batch
        startTime = millis();
        counting = true;
        beatCount = 1;
      } else {
        beatCount += 1;
        if (beatCount >= 96) {
          noteTime = millis() - startTime;
          counting = false;
          // You now have the time for one note, so calculate the BPM
          bpm = 240000 / noteTime;

          // Calculate the interval values for 1, 2, and 4 flashes per beat
          intervals[0] = 30000 / bpm;  // 1 flash per beat
          intervals[1] = 15000 / bpm;  // 2 flashes per beat
          intervals[2] = 7500 / bpm;   // 4 flashes per beat
        }
      }
    }
  }

  unsigned long currentTime = millis();

  for (int i = 0; i < numButtons; i++) {
    if (digitalRead(buttonPins[i]) == HIGH) {
      isButtonPressed = true;
      interval = getInterval(i);
      break;
    }
    isButtonPressed = false;
  }

  if (isButtonPressed) {
    if (currentTime - previousMillis > interval || previousMillis == 0) {
      previousMillis = currentTime;
      isStrobeOn = !isStrobeOn;
      digitalWrite(StrobePin, isStrobeOn);
    }
  } else {
    digitalWrite(StrobePin, LOW);
  }

  // Check if Button 5 is pressed
  if (digitalRead(buttonPin11) == HIGH) {
    digitalWrite(StrobePin, HIGH);
  } else {
    digitalWrite(StrobePin, LOW);
    delay(10); // Delay to prevent extra flashes after button release
  }
}

long int getInterval(int buttonIndex) {
  if (buttonIndex >= 0 && buttonIndex < numButtons) {
    return intervals[buttonIndex];
  }
  return 0;  // Return a default value if the buttonIndex is out of range
}

Ok, obvious fix for the drifting flashes, the 10ms delay I put on the manual buttonPin11.
Both my own attempt at implementing the IDE debounce example and ChatGPT have failed to get that working.

Keep asking the robot until he/she/it gives you the correct result you expect.

One answer could probably be 42.

Thanks paul

i'm confused by the code. i thought the selection of the interval by pressing one of the 3 interval buttons should persist, cause the StrobePin to be toggled every interval

  • but isButtonPressed is set back to false when a button is not pressed
  • the code checking for buttonPin11 also set the StrobePin, overwriting what is done in the timer code

the code that checks for a button press will execute for as long as the button is held down which causes startTime to actually capture the time when the button is released

the more conventional approach is the recognize when a button is pressed by keeping track of the button state and when it changes check if it went HIGH in your case

my hardware configures the button with the built in pullup resistor so the switch pulls the pin to ground

a ~10ms delay is needed for debounce after the timestamp is cpatured

    for (int i = 0; i < numButtons; i++) {
        byte but = digitalRead (buttonPins[i]);
        if (butLst [i] != but)  {
            butLst [i] = but;
#ifdef MyHW
            if (LOW == but) {
#else
            if (HIGH == but) {
#endif
                isButtonPressed = true;
                interval = getInterval(i);
                Serial.print   ("interval = ");
                Serial.println (interval);
                break;
            }
        }
#if 0
        isButtonPressed = false;
#endif
    }

You're a DJ only playing MIDI???

I don't know THAT much about MIDI but I don't think there is a "beat" or "downbeat" message. There are note-on and note-off messages and you could trigger on that, or make a calculation/estimation based-on note-on.

You could capture the time between note-on messages and throw-out the outliers (rests and short notes) and take an average. Then you could anticipate the "next beat" and keep adjusting as you go.

Or you could make a separate MIDI track (for each song) just for the lighting... That would give you complete control. (MIDI is not "normal" for lighting... Most "programmed" or "controlled" lighting uses DMX 512.

IMO - A light flashing to the exact-beat gets boring... Personally, I'd prefer it to react to extra beats or a rests. But ANY single-effect gets boring pretty quickly...

I made World's Simplest Lighting Effect. It's audio triggered (not MIDI) and it "flickers" the light on when the loudness is above average and off when it's below average. So it approximately follows the beat. It's also boring after a song or two so my "real" effects are more randomly varied and each effect has options/variations. (I have a variation of the "simplest" effect that turns the light off when louder than average, and a variation that turns one light on while another is off.)

P.S.
There are also "beat detection" algorithms (for audio). I don't have a handy link, but again you could react to every-actual beat or do some calculations/estimations to track the tempo ignoring extra beats & rests.

Ok let me clarify, the strobe light only needs to see some positive voltage on it's trigger input to flash once, so if the code runs and sees a button is pressed and momentarily switches the output pin to high, I'll get a flash, and the code then sets it to low.

I am making some assumptions about how this logic works (because I'm learning), the break in my for loop, does that not bypass the "isButtonPressed = false;" ?

Essentially it's constantly looping, checking the Midi Clock messages to calculate the BPM, assigning intervals to the relevant buttons based on that BPM, checking if enough time has passed for the button that's currently being pressed.

I have just noticed whilst writing the debounce for the 4th button, it's been toggling the ledpin!
this also explains why I couldn't get the math to add up with the bpm/interval calcs.

gcjr, I don't understand, what's my hardware?

DVDdoug, I am Djing regular music! but the DJ software can output midi clock info so I thought it would be nice to use that to calculate the BPM of my music, the old controller had 9 buttons, 3 predefined BPM values I was most likely playing, and 3 speeds of strobe for each.
I just press and hold a button and can build or release strobe flashes, usually for a few seconds.

Using a midi monitor I can see that Virtual DJ sends the start, clock and stop midi messages, 24 clock messages per beat. I got the BPM calculation and midi code from this thread:

VDJ can communicate with DMX software but that's a rabbit hole I just had to climb out of, I think it would be simpler to tidy up what I'm already working with.

So I've got the debounce working, removing the built in delay, but I'm still getting drift if I hold the button for long enough. Is this just the latency of the code looping round and round? creating slight delays and stacking up over time?

byte midi_clock = 0xf8;
unsigned long int startTime;
int beatCount;
bool counting = false;
long int bpm;
long int noteTime;
#define buttonPin11 11
const int StrobePin = 13;
const int buttonPins[] = {2, 3, 4};
long int intervals[3];  // Interval values for 1, 2, and 4 flashes per beat

const int debounceDelay = 50; // Debounce delay in milliseconds
unsigned long lastDebounceTime = 0; // Time of the last button state change


const int numButtons = sizeof(buttonPins) / sizeof(buttonPins[0]);

boolean isButtonPressed = false;
long previousMillis = 0;
long interval = 1000;
boolean isStrobeOn = LOW;

void setup() {
  Serial.begin(31250); // Set baud rate to 31250 for MIDI communication

  pinMode(StrobePin, OUTPUT);
  for (int i = 0; i < numButtons; i++) {
    pinMode(buttonPins[i], INPUT);
  }
  pinMode(buttonPin11, INPUT);
  
  // Set default intervals
  intervals[0] = 500;  // 1 flash per beat
  intervals[1] = 250;  // 2 flashes per beat
  intervals[2] = 125;   // 4 flashes per beat
}

void loop() {
  while (Serial.available()) {
    byte midiByte = Serial.read();
    if (midiByte == midi_clock) {
      if (!counting) {  // First time you have seen a clock message in this batch
        startTime = millis();
        counting = true;
        beatCount = 1;
      } else {
        beatCount += 1;
        if (beatCount >= 96) {
          noteTime = millis() - startTime;
          counting = false;
          // You now have the time for one note, so calculate the BPM
          bpm = 240000 / noteTime;

          // Calculate the interval values for 1, 2, and 4 flashes per beat
          intervals[0] = 60000 / bpm;  // 1 flash per beat
          intervals[1] = 30000 / bpm;  // 2 flashes per beat
          intervals[2] = 15000 / bpm;   // 4 flashes per beat
        }
      }
    }
  }

  unsigned long currentTime = millis();

  for (int i = 0; i < numButtons; i++) {
    if (digitalRead(buttonPins[i]) == HIGH) {
      isButtonPressed = true;
      interval = getInterval(i);
      break;
    }
    isButtonPressed = false;
  }

  if (isButtonPressed) {
    if (currentTime - previousMillis > interval || previousMillis == 0) {
      previousMillis = currentTime;
      digitalWrite(StrobePin, HIGH);
    }
  } else {
    digitalWrite(StrobePin, LOW);
  }

  // Check if Button 5 is pressed
  if (digitalRead(buttonPin11) == HIGH) {
    if ((millis() - lastDebounceTime) > debounceDelay) {
      digitalWrite(StrobePin, HIGH);
    }
  } else {
    digitalWrite(StrobePin, LOW);
    lastDebounceTime = millis(); // Update the last debounce time
  }
}

long int getInterval(int buttonIndex) {
  if (buttonIndex >= 0 && buttonIndex < numButtons) {
    return intervals[buttonIndex];
  }
  return 0;  // Return a default value if the buttonIndex is out of range
}

I ran your code and just fed 'x' to it. It seemed to run plausibly and produced accurate intervals[] after seeing 96 'x' characters.

Why do you recalculate intervals every time through the loop?

If you use

  while (Serial.available()) {
    byte midiByte = Serial.read();
    if (midiByte == midi_clock) {

it will see characters as they arrive, and calculate after 96 midi_clock characters no matter it took a few times through the loop or not.

I don't see any other path for inexact reaction to the 96th character. The rest of the loop seems trivially not slow. You got rid of a delay which could have thrown things off.

a7

With pullup resistors a digitalRead of a button pin not being pressed will be HIGH..
Your code seems to imply otherwise??

sorry.. ~q

changes necessary to work on my hardware, a multifunction shield pins #s and button switches connected between ground and pins configured with pullup resistors

what input is the trigger?

not really clear what you're debouncing.

code looks like it's waiting for the to no longer be LOW before capturing a timestamp, lastDebounceTime, and setting the StrobePin HIGH debounceDelay after buttonPin11 becomes HIGH

what do you expect this to do?

Out of interest how would I see the values it's storing for the intervals?

Well the answer is that I don't know what I'm doing and used ChatGPT... frustrating for everyone involved in the end!
I had fed it the code from that other thread and asked to integrate it with my older code, stuff got minced up on the way through.

My mistake, I built the box about 10 years ago and had forgotten how it was wired, they're pull down. I didn't know there were internal pullup resistors on the arduino, I'll probably rewrite when I build the next version for myself.

Pin13, the LEDPin, Output on the arduino, input on the strobe light.

Do you mean it's good habit to make it clear what the code is doing? by way of good naming? Because it is debouncing the extra button which acts as a manual flash, like a drum pad/tap.

I changed the "while" to an "if" which might have been the last detail.

Code looks like it works. Begins flashing immediately, no weird stray flashes, intervals are about right.

I recorded the time between the clicking sound the strobes make when it fires, at 118 bpm I measured 507ms, which is only a couple of ms off, 118.3bpm. 143bpm I measured 415ms, which is about 144.5bpm, about 4-5ms different.

Close enough, should I be aiming for better?

I do apologise to everyone, I'm impatient and like to go at things headfirst, which is probably why it's all so confusing.

byte midi_clock = 0xf8;
unsigned long int startTime;
int beatCount;
bool counting = false;
long int bpm;
long int noteTime;
#define buttonPin11 11
const int StrobePin = 13;
const int buttonPins[] = {2, 3, 4};
long int intervals[3];  // Interval values for 1, 2, and 4 flashes per beat

const int debounceDelay = 50; // Debounce delay in milliseconds
unsigned long lastDebounceTime = 0; // Time of the last button state change


const int numButtons = sizeof(buttonPins) / sizeof(buttonPins[0]);

boolean isButtonPressed = false;
long previousMillis = 0;
long interval = 1000;
boolean isStrobeOn = LOW;

void setup() {
  Serial.begin(31250); // Set baud rate to 31250 for MIDI communication

  pinMode(StrobePin, OUTPUT);
  for (int i = 0; i < numButtons; i++) {
    pinMode(buttonPins[i], INPUT);
  }
  pinMode(buttonPin11, INPUT);
  
  // Set default intervals
  intervals[0] = 500;  // 1 flash per beat
  intervals[1] = 250;  // 2 flashes per beat
  intervals[2] = 125;   // 4 flashes per beat
}

void loop() {
  if (Serial.available()) {
    byte midiByte = Serial.read();
    if (midiByte == midi_clock) {
      if (!counting) {  // First time you have seen a clock message in this batch
        startTime = millis();
        counting = true;
        beatCount = 1;
      } else {
        beatCount += 1;
        if (beatCount >= 96) {
          noteTime = millis() - startTime;
          counting = false;
          // You now have the time for one note, so calculate the BPM
          bpm = 240000 / noteTime;

          // Calculate the interval values for 1, 2, and 4 flashes per beat
          intervals[0] = 60000 / bpm;  // 1 flash per beat
          intervals[1] = 30000 / bpm;  // 2 flashes per beat
          intervals[2] = 15000 / bpm;   // 4 flashes per beat
        }
      }
    }
  }

  unsigned long currentTime = millis();

  for (int i = 0; i < numButtons; i++) {
    if (digitalRead(buttonPins[i]) == HIGH) {
      isButtonPressed = true;
      interval = getInterval(i);
      break;
    }
    isButtonPressed = false;
  }

  if (isButtonPressed) {
    if (currentTime - previousMillis > interval || previousMillis == 0) {
      previousMillis = currentTime;
      digitalWrite(StrobePin, HIGH);
    }
  } else {
    digitalWrite(StrobePin, LOW);
  }

  // Check if Button 5 is pressed
  if (digitalRead(buttonPin11) == HIGH) {
    if ((millis() - lastDebounceTime) > debounceDelay) {
      digitalWrite(StrobePin, HIGH);
    }
  } else {
    digitalWrite(StrobePin, LOW);
    lastDebounceTime = millis(); // Update the last debounce time
  }
}

long int getInterval(int buttonIndex) {
  if (buttonIndex >= 0 && buttonIndex < numButtons) {
    return intervals[buttonIndex];
  }
  return 0;  // Return a default value if the buttonIndex is out of range
}

I had the advantage of not actually using any MIDI device, so my serial input and output were through the serial monitor. I could feed the incoming characters by typing them in the input area, and still have the serial output which is so convenient... I don't think I've ever gotten something to work, and convinced myself that it is doing things the way I want, without some kind of feedback beyond what the normal behaviour of the sketch would reveal by itself. LEDs, logic analyser, frequency meter and oscilloscope can be useful when you just don't have or can't use a serial port.

You could have serial port debugging if you used an additional UART if your board has one or the SoftwareSerial library and a terminal emulator program like PuTTY or CoolTerm running on the PC and watching an approriate port.

I used serial input to feed a number of 'x' characters (instead of midi_clock or whatever) into your algorithm, and at the point where it sees it has 96 20 (life too short) let it do the computation then put print statements after that to see the array intervals[] values.

I could only manually take 10 seconds from the first 'x' to the 20th (cut down from 96, but so what?), so that could be a source of error

        if (beatCount >= 20) {
          noteTime = millis() - startTime;
          counting = false;
          // You now have the time for one note, so calculate the BPM
          bpm = 240000 / noteTime;

          // Calculate the interval values for 1, 2, and 4 flashes per beat
          intervals[0] = 60000 / bpm;  // 1 flash per beat
          intervals[1] = 30000 / bpm;  // 2 flashes per beat
          intervals[2] = 15000 / bpm;   // 4 flashes per beat

          Serial.println(noteTime);

          Serial.println(intervals[0]);
          Serial.println(intervals[1]);
          Serial.println(intervals[2]);
        }

I got 2500, 1250 and 625 which means I hit the 10 seconds pretty close becuase you use integer arithmetic there's a bit of inaccuracy.

I chagned intervals to float type, and got this

10043
2608.00
1304.00
652.00

but here bpm (240000 / 10043) comes out as 23.

I think perhaps some of the problem, if it is still a problem, stems from where you lose precision because you are using integers. There might be very slight changes to the tempo that would be the unfortunate places where the code makes intervals just miss (or hit) the next integer, resulting in unexpected large changes elsewhere.

a7

1 Like

debouncing typically recognize an initial press and ignores additional changes within some debounce period.

what you've written is novel. i can see how it appears adequate. but since lastDebounceTime is repeatedly capture in the else case, it's use actually delays processing of the button press and doesn't ignore continually pressing the button. (try setting debounceDelay to 500)

in your case a delay of 50 msec, debounceDelay, may not be noticeable and the repeated setting of StrobePin doesn't matter to you as well. the duration of the StrobePin pulse is also determined by the length of time the button is held after the debounceDelay is exceeded

so while this may be acceptable in this case, this would not work to recognize a single press of a button.

look this over

byte butLst;
void
press (void)
{
    byte but = digitalRead (buttonPin11);
    if (butLst != but)  {
        butLst = but;

        if (LOW == but) {
            digitalWrite (StrobePin, On);
            delay (50);
            digitalWrite (StrobePin, Off);
            Serial.println (++cnt);
        }
        else
            delay (20);     // debounce
    }
}

Thanks! :rofl:
I tried using the IDE example for debounce but whatever the reason, I didn't get the expected behaviour with the example code and tried to make some changes.

I can see what you mean now, it's delaying the strobe flash, I'll have another play with it.

do you understand the code i posted? how it is different from what you did

byte butLst; //define a byte called butLst (buttons last state)
void
press (void) // syntax is a little different, but a function "press"
{
    byte but = digitalRead (buttonPin11);  //set the byte to the reading from the button, high or low
    if (butLst != but)  { // if the last button state is different to the current button state
        butLst = but; // set the last button state to the current state.

        if (LOW == but) { // if button is low
            digitalWrite (StrobePin, On); //turn on the strobe pin
            delay (50); //wait 50ms
            digitalWrite (StrobePin, Off); //turn off the strobe pin
            Serial.println (++cnt);  // not sure about this bit, print something!
        }
        else
            delay (20);     // if the button states match, wait 20ms
    }
}

Best guess anyway!

yes.
does it do what you expect? (pulse strobe on button press)?
( the cnt was to verify that that condition only executed once for each button press)

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.