midilab / uClock

A tight BPM clock generator for Arduino and PlatformIO using hardware timer interruption. AVR, Teensy, STM32xx, ESP32 and RP2040 support
https://midilab.co/umodular
MIT License
153 stars 19 forks source link

External clock and send midi notes not in sync #20

Closed arnoson closed 1 year ago

arnoson commented 1 year ago

I'm testing a simple setup on a teensy 4.1:

#include <SPI.h>
#include <uClock.h>

void startClock() { usbMIDI.sendRealTime(usbMIDI.Start); }

void stopClock() { usbMIDI.sendRealTime(usbMIDI.Stop); }

void sendClock(uint32_t *tick) {
  usbMIDI.sendRealTime(usbMIDI.Clock);

  if ((*tick) % 96 == 0) {
    usbMIDI.sendNoteOn(62, 127, 1);
  };
}

void setup() {
  usbMIDI.begin();
  uClock.init();
  uClock.setClock96PPQNOutput(sendClock);
  uClock.setOnClockStartOutput(startClock);
  uClock.setOnClockStopOutput(startClock);
  uClock.setTempo(120);

  delay(5000);
  uClock.start();
}

void loop() {}

The teensy is connected to (and controlling the tempo of) Ableton Live 11 via USB. The tempo works perfectly, but the note I send is relly off. It is ~118ms too late. A few ms of latency I expected because the clock signal has a higher priority than a normal note I guess. Can you think of any reason this is happening?

Another issue I have (which could also just be me misunterstanding the MIDI clock standard an the PPQN) is that in the setup above, I would expect to hear one note per quarter note (so if I turn on a metronome I would hear one note per metronome tick). But it seems to be only one note per bar.

midilab commented 1 year ago

Looks like you're getting the tick by reference, when you should get by value.

the correct signature of setClock96PPQNOutput callback is to receive a uint32_t tick, instead of uint32_t *tick.

please take a look at this example on example folder for the corect way of using callbacks: https://github.com/midilab/uClock/blob/main/examples/TeensyUsbMasterMidiClock/TeensyUsbMasterMidiClock.ino

So at your example you should just take off the unary operator "*" from your code and you should be fine.

What is the version of uClock you're using rigth now?

arnoson commented 1 year ago

Thanks for the quick reply! I'm using version 0.10.6 which I now see is outdated, it is the latest in the platform io registry . I updated the library manually to the latest version. You're right, now I don't need the reference anymore (in the old version uint32_t *tick was the signature of the callback). I changed the callback accordingly but still get the same issues (notes are delayed by ~120ms and using module 96 results in full-bar notes, not quarter notes)

midilab commented 1 year ago

i didn't even know that uClock was on platform io registry! o_O

but all the official releases you will find it here.

The delay problem is related to USB stack, your operational system(windows or mac, not made for realtime tasks) and ableton audio buffers(the more processing you have on ableton the more delay you'll get).

For that matter ableton guys let you compensate those delay at midi configuration level, you need to compensate it manually until you ge the perfect sync(just like a back to back Vinyl mix).

You can check they official response for that audio buffer problem at step 4

So keep in mind that any hardware sequencer using usb as midi will have the same problem, each project you open at ableton probrably will need different delay setup.

Step 4: Adjust the MIDI Clock Sync delay Because of a number of factors, including audio buffer sizes both in Live and in the connected device, the timing of the two will probably be slightly offset. You can correct the timing by adjusting the MIDI Clock Sync Delay.

midilab commented 1 year ago

For the bar length, 96ppqn means a full bar actually. 96 pulses per quarter note, so 4 step notes in 96 pulses, and each pulse is used as sync, so each 24 pulses means a note.

if you want a per step you should better use the 16ppqn callback

// Set the callback function for the step sequencer on 16ppqn uClock.setClock16PPQNOutput(ClockOut16PPQN);

Or divide you timming per 24 instead of 96. but the best way to write a step sequencer would be the 16ppqn callback as start point. so each call of that callback will means one step.

arnoson commented 1 year ago

Wow thanks for all the explanations :) I've used the ableton clock sync delay but wasn't sure that such a high delay was normal with USB. Dividing by 24 works, thanks! But maybe the API naming can be a bit confusing as there are higher resolution midi clocks that work with 96 ppqn (not per bar) instead of the default 24ppqn.