Tap Tempo Metronome drifts (ESP32 C3 supermini)

Hi everyone, I have a question: I have created this project (GitHub - campidelli/tap-tempo-metronome: An Arduino/ESP32 tap tempo metronome.) to create a tap tempo metronome pedal (there is a demo video in the GitHub repository).

It works, sort of. The problem is that the tempo drifts a little bit after a while. For example, if I put another tap tempo metronome side by side, they start syncing but after some seconds I start hearing the difference. What could it be?

This is the code, in case you didn't check the Github repo:

#include "Arduino.h"
#include "ArduinoTapTempo.h"
#include "ESP32_C3_TimerInterrupt.h"

#define LED_PIN              8
#define TAP_TEMPO_SWITCH_PIN 5
#define TIMER1_INTERVAL_MS   20
#define PWM_AUDIO_OUTPUT_PIN 4
#define PWM_CHANNEL          0
#define PWM_FREQ             4000 // Frequency in Hz
#define PWM_RESOLUTION       8    // 8-bit resolution (0-255)

ESP32Timer ITimer1(1);    // Timer to check the button state every TIMER1_INTERVAL_MS
ArduinoTapTempo tapTempo; // Used to calculate the BPM

unsigned long lastClickTime = 0;     // Last time a click sound was played
volatile bool buttonPressed = false; // Flag to indicate if the button is pressed

bool IRAM_ATTR TimerHandler1(void * timerNo) {
  if (digitalRead(TAP_TEMPO_SWITCH_PIN) == LOW && !buttonPressed) {
    buttonPressed = true;
  }
  if (digitalRead(TAP_TEMPO_SWITCH_PIN) == HIGH) {
    buttonPressed = false;
  }
  tapTempo.update(buttonPressed);
  return true;
}

void setup() {
  Serial.begin(9600);

  pinMode(TAP_TEMPO_SWITCH_PIN, INPUT_PULLUP);
  pinMode(LED_PIN, OUTPUT);

  // Set up PWM on the audio output pin
  ledcSetup(PWM_CHANNEL, PWM_FREQ, PWM_RESOLUTION);
  ledcAttachPin(PWM_AUDIO_OUTPUT_PIN, PWM_CHANNEL);

  // Attach timer interrupt to check the button state every TIMER1_INTERVAL_MS
  if (ITimer1.attachInterruptInterval(TIMER1_INTERVAL_MS * 1000, TimerHandler1)) {
    Serial.println("Timer interrupt started.");
  } else {
    Serial.println("Failed to start timer interrupt.");
  }
}

void loop() {
  float bpm = tapTempo.getBPM();
  unsigned long interval = 60000 / bpm; // Interval in milliseconds for each beat

  // Click sound and LED blink if it's time for a beat
  if (bpm > 0 && millis() - lastClickTime >= interval) {
    Serial.print(F("Current BPM: "));
    Serial.println(bpm);

    digitalWrite(LED_PIN, LOW);

    ledcWrite(PWM_CHANNEL, 200); // Set a moderate duty cycle
    delay(20);
    ledcWrite(PWM_CHANNEL, 0);   // Stop sound

    digitalWrite(LED_PIN, HIGH);

    lastClickTime = millis(); // Update the last click time
  }
}

Cheers!

That is the real world, my friend. The internal clocks of the two ESP board are not IDENTICAL and their frequency drifting with temperature changes is not identical.

Hi Paul, it is not necessarily bad that they differ a little bit. But I would like to have consistency on my metronome, I want to be sure that the tempo the music started is the same as the music ended. ANy suggestions?

Use a RTC (Real Time Clock) module instead

I found out the issue... To generate the click sound, there was a 20ms delay and I was using delay(). Now I am compensating for that delay and using millis() instead, that fixed the issue!

void loop() {
  static unsigned long clickEndTime = 0;
  float bpm = 150.0f;
  unsigned long interval = 60000 / bpm;

  if (bpm > 0 && millis() - lastClickTime >= interval) {
    lastClickTime += interval; // Keep timing accurate

    digitalWrite(LED_PIN, LOW);
    ledcWrite(PWM_CHANNEL, 200); // Start sound

    clickEndTime = millis() + 20; // Schedule when to stop the sound

    Serial.print(millis());
    Serial.print(F(" ms - Current BPM: "));
    Serial.println(bpm);
  }

  // Stop sound after 20ms without blocking
  if (millis() >= clickEndTime) {
    ledcWrite(PWM_CHANNEL, 0);
    digitalWrite(LED_PIN, HIGH);
  }
}