Forum Rule: Always post complete source code & details to reproduce any issue!
Results 1 to 7 of 7

Thread: Attempting to make an internal bpm/clock

  1. #1

    Attempting to make an internal bpm/clock

    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.

  2. #2
    Senior Member
    Join Date
    Aug 2013
    Location
    Gothenburg, Sweden
    Posts
    402
    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.

  3. #3
    Senior Member
    Join Date
    Jul 2020
    Posts
    927
    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.

  4. #4
    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!

  5. #5
    Senior Member
    Join Date
    Apr 2014
    Location
    Germany
    Posts
    1,433
    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 ) 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.

    Click image for larger version. 

Name:	Screenshot 2021-03-07 135538.jpg 
Views:	11 
Size:	60.5 KB 
ID:	23952

    Hope that's of any use. In any case it gave a nice example for the lib :-)

  6. #6
    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

    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 by Hitachii; 03-07-2021 at 09:05 PM.

  7. #7
    Senior Member
    Join Date
    Apr 2014
    Location
    Germany
    Posts
    1,433
    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 by luni; 03-08-2021 at 08:04 AM.

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •