Attempting to make an internal bpm/clock

Status
Not open for further replies.

Hitachii

Active member
Hello,

I'm attempting to make a sequencer with the bpm determined by the value of an encoder. Currently, the encoder is constrained to the values of 0 - 127, making a theoretical maximum 127 bpm. This is temporary, but may be contributing to the issue(?).

The problem is that, based off of the code that I'm using, the tempo/bpm is slightly more than twice as fast as it should be. When the value of "bpm" in the code below is multiplied by 2 (bpm = encoderCount * 2), it is close but still noticeably faster than the bpm that I'm comparing it to from another device.

Here is the code, without the multiplication mentioned above. Sorry about the very long case break portion. Currently the only thing that the tempo does is light up each LED in a 4x4 button matrix sequentially.

Code:
void sequencer(void) {

  long bpm = encoderCount; //encoderCount is a global variable
  long tempo = 1000.0 / (bpm / 60.0);
  float interval = tempo / 24.0;

  Serial.println(encoderCount);

  unsigned long t = millis();


  if ((t - lastTime) >= interval) { //lastTime is a global variable
    lastTime = t;
    stepSq++; //stepSq is a global variable
    if (stepSq > 15) {
      stepSq = 0;
    }
    switch (stepSq) {

      case 0:
        trellisoff(15);
        trellison(0);
        break;

      case 1:
        trellisoff(0);
        trellison(1);
        break;

      case 2:
        trellisoff(1);
        trellison(2);
        break;

      case 3:
        trellisoff(2);
        trellison(3);
        break;

      case 4:
        trellisoff(3);
        trellison(4);
        break;

      case 5:
        trellisoff(4);
        trellison(5);
        break;

      case 6:
        trellisoff(5);
        trellison(6);
        break;

      case 7:
        trellisoff(6);
        trellison(7);
        break;

      case 8:
        trellisoff(7);
        trellison(8);
        break;

      case 9:
        trellisoff(8);
        trellison(9);
        break;

      case 10:
        trellisoff(9);
        trellison(10);
        break;

      case 11:
        trellisoff(10);
        trellison(11);
        break;

      case 12:
        trellisoff(11);
        trellison(12);
        break;

      case 13:
        trellisoff(12);
        trellison(13);
        break;

      case 14:
        trellisoff(13);
        trellison(14);
        break;

      case 15:
        trellisoff(14);
        trellison(15);
        break;
    }
  }
}

Thanks.
 
You have 16 leds in the matrix and 24 steps per beat, that makes it a bit tricky to count the beats, every beat is 24=6*4 steps that is one and a half full matrix of steps.

Also the lastTime should be updated as
Code:
lastTime += interval;
making lastTime always a multiple of interval, then the timing will not drift if the sequencer() is called a bit late.
 
This is how I'd do it:
Code:
  long bpm = encoderCount ;
  float tempo = 1e6 / (bpm / 60.0);   // beat period in microseconds
  unsigned long interval = (unsigned long) (round (tempo / 24.0));

  if (micros() - lastTime >= interval)
  {
    lastTime += interval ;
    
    stepSq = (stepSq + 1) % 16 ;
    switch (stepSq)
...
...
Note that micros() is used for greater accuracy - if you use millis, then 200bpm gives interval = 12.5ms, which
would round up or down, giving error (the tempo would be discretized into noticable steps)

Using microseconds gives plenty of resolution.

Also the timing logic has been corrected as by miu's post, and stepSq is handled in a single line using
the mod operator.
 
Okay, now I see where the confusion was! The 24 steps in my code are meant to represent 24 parts per quarter note. I should have mentioned that before, whoops! My intention is that the LEDs advance a single step for every 24 ppq, the way I would expect a 24 ppq MIDI sequencer would.

Ignoring the 24 ppq for a moment, I removed the "/ 24.0" from the interval formula, and followed the rest of the advice given here, and it kept time like a charm!

My intention is to eventually use this part of the code as a MIDI clock, so I put "/ 24.0" back into the formula for "interval", then added a mod operator so that the LED would advance 1 for every 24 ppq. It doesn't work correctly :(. Anywhere above 60 bpm, the tempo doesn't track with the metronome. I'm assuming that this is because of some integer math that I'm not understanding.

Code:
  long bpm = encoderCount;
  float tempo = 1e6 / (bpm / 60.0);
  unsigned long interval = (unsigned long) (round (tempo / 24.0));

  if (micros() - beatStep >= interval) {
    beatStep += interval;

    ppqCounter = (ppqCounter + 1) % 24;   //this is how I attempted to make it advance once for every 24 steps
    if (ppqCounter == 0) {

      stepSq = (stepSq + 1) % 16;

      switch (stepSq) {... ... ...

How do I make it so that there is one beat per every 24 ppq, while keeping accurate beats per minute. Or, if I've explained my intentions well enough and I'm doing it wrong, how do I do it right? Thanks again!
 
Instead of calling your sequencer from loop I'd use an interval timer to generate the calls. You can then simply change the timer period when the encoder value changes. IIRC you are using the EncoderTool for your application? In this case you can handle the changes of the timer period in the encoder-changed callback function. Thus, you have everything running event based in the background.

I thought this makes a nice usage example for the encoder tool. So, I worked out a little class handling the metronome functionality. You find it in the examples folder of the GitHub repo:repository.

Here a copy of the code. It encapsulates the encoder and the interval timer in a Metronome class. In its begin function you pass it the encoder pins, the encoder limit value and a callback function which will be called every 1/24th of a quarter note. (Hope I got the idea right, I have no clue about all this music stuff :D) In the example I didn't implement your LED matrix but just generate some pulses to check the timing with an LA. You can of course replace this code with your LED matrix code or whatever you need to do every 1/96th of a beat.

Code:
#include "Arduino.h"
#include "EncoderTool.h"

// -------------------------------------------------------------------------------------
// you can move the following class code to some metronome.h file and include it here

class Metronome
{
 public:
    void begin(void (*ppqCallback)(void), unsigned encA, unsigned encB, unsigned maxBPM)
    {
        cb = ppqCallback;                      
        bpmEncoder.begin(encA, encB, [this](int v, int d) { this->EncoderChanged(v, d); });
        bpmEncoder.setLimits(0, maxBPM);
    }

 private:
    void EncoderChanged(int encValue, int d)  // called whenever the encoder value changes
    {
        float bpm = encValue;                 // change this if you want bpm calculated from encoder value in an other way (e.g. bmp = encValue * 0.1f)

        Serial.printf("%.1f bpm\n", bpm);

        if (bpm == 0.0f)                       // switch timer off
        {
            Serial.println("stop");
            ppqTimer.end();
            isRunning = false;
        } 
        else
        {
            constexpr float pFac = 60.0E6f / (4 * 24); // calculate timer period from bpm, assuming  1/24th of a quarter note at the given bpm
            float period = pFac / bpm;                 // microseconds

            if (isRunning)                
                ppqTimer.update(period);               // timer already running -> updating period only
            else                                       // if the timer doesn't run yet we need to start it
            {
                ppqTimer.begin(cb, period);            // call the passed in function to do whatever needs to be done every 1/24 quarter note
                isRunning = true;
            }             
        }
    }

    void (*cb)(void);                                  // storage for the passed in handling function
    bool isRunning = false;                            // we don't have a simple method to check if the timer runs. So, we keep track ourselves

    EncoderTool::Encoder bpmEncoder;                   // encoder to select bpm 
    IntervalTimer ppqTimer;                            // timer to call the  handling function every 1/24th of a quarter note
};




//----------------------------------------------------------------------
// normal sketch code starts here...


// the metronome will call this function every 1/24th of a quarter note.
void ppqCallback()
{
    static int quarters = 0;
    static int beat = 0;

    digitalWriteFast(2, HIGH);      // every 1/24th quarter
    delayMicroseconds(1);
    digitalWriteFast(2, LOW);

    if (quarters == 0)              // every quarter
    {
        digitalWriteFast(3, HIGH);
        delayMicroseconds(1);
        digitalWriteFast(3, LOW);
    }
    if (beat == 0)                  // every beat
    {
        digitalWriteFast(4, HIGH);
        delayMicroseconds(1);
        digitalWriteFast(4, LOW);
    }

    quarters = (quarters+1) % 24;
    beat = (beat + 1) % 96;
}

Metronome metronome;

void setup()
{
    for(int pin: {2,3,4, LED_BUILTIN}){      // we need pin 2,3,4 for the debugging output in ppqCallback
        pinMode(pin, OUTPUT);
    }

    metronome.begin(ppqCallback, 0, 1, 200); // attach an encoder at pins 0,1 to the metronome and limit its range to 200
                                             // tell the metronome to call "ppqCallback" on each 1/24th quarter note
}

void loop()
{
    digitalToggleFast(LED_BUILTIN);
    delay(500);
}

Here the generated output for a 30 BPM setting. One would expect 30 BPM/60 = 0.5Hz beat frequency, 0.5Hz * 4 = 2Hz quarter frequency and 2Hz * 24 = 48Hz for the 1/24th quarters.

Screenshot 2021-03-07 135538.jpg

Hope that's of any use. In any case it gave a nice example for the lib :)
 
Hey luni, thanks for the help yet again! You're correct, I am using your library. Always happy to be able to present the problem to your solution :D

Wow, thanks a ton I will be able to test this shortly and let you know the results. I actually had a question specifically about your library, so I'm double glad you popped in! I'm using the same encoder that adjusts the BPM, to also send MIDI CC messages. How can I set two different ranges for the encoder, which switches depending on the current function? To illustrate what I'm trying to accomplish, when no buttons are pressed on the LED button matrix, turning the encoder adjusts the BPM. When a button is held, turning the encoder sends CC to the respective CC number instead. The button-holding function/sending BPM functions are all set up, the only thing I need help with is how to set two different ranges for the encoder.

So far I've tried defining the same encoder twice, giving two different names with separate ranges (0 - 127 for CC, and 30 - 300 for bpm), but assigning them to the same pins. This made neither encoder work. How can I work around this?
 
Last edited:
So far I've tried defining the same encoder twice, giving two different names with separate ranges (0 - 127 for CC, and 30 - 300 for bpm), but assigning them to the same pins. This made neither encoder work. How can I work around this?

That won't work since it will mess up the interrupt handling. Anyway, I'd simply define a relay function which, depending on the current mode, increments one of two variables. Limiting the values to two ranges is trivial. Here a very quick solution showing the principle.

Code:
#include "EncoderTool.h"

EncoderTool::Encoder enc;

int32_t encVal0;
constexpr int32_t max0 = 20;
constexpr int32_t min0 = 0;

int32_t encVal1;
constexpr int32_t max1 = 10;
constexpr int32_t min1 = -10;

int mode = 0; 

void relay(int value, int delta)
{    
    if (mode == 0) encVal0 = max(min0, min(max0, encVal0 + delta));
    if (mode == 1) encVal1 = max(min1, min(max1, encVal1 + delta));
    // more modes...

    Serial.printf("mode: %d, value_0: %d value_1: %d\n", mode, encVal0 , encVal1);
}

void setup()
{
    enc.begin(0, 1, relay);
}

void loop()
{
    mode = digitalReadFast(2); // switch between the two modes with pin 2 
}
 
Last edited:
Status
Not open for further replies.
Back
Top