Ableton / link

Ableton Link
Other
1.09k stars 149 forks source link

How to properly get 24 pulses for each quarter note? #109

Closed DatanoiseTV closed 1 year ago

DatanoiseTV commented 2 years ago

I am working on a device that uses Link (on ESP32). I need to make a midi clock with 24 pulses.

If I do

bool lastState = false;

.....
bool currentState = fmodf(phase, 1.0 / 12) < 0.5/12;
if(lastState != currentState){
    ... send midi clock
lastState = currentState;
}

The FreeRTOS task is highest priorty, but my MIDI clock is jittering +/- 1 bpm and therefore drifts away.

What is the proper way to get 24 pulses? Maybe @mathiasbredholt can help?

mathiasbredholt commented 2 years ago

The code above should work, is it based on the example code? I noticed that the portYIELD() should be removed from the tick task in the example as it will prevent the Link task to run otherwise.

  while (true)
  {
    xSemaphoreTake(handle, portMAX_DELAY);

    const auto state = link.captureAudioSessionState();
    const auto phase = state.phaseAtTime(link.clock().micros(), 1.);
    gpio_set_level(LED, fmodf(phase, 1.) < 0.1);
    portYIELD();  // this should be removed
  }

@DatanoiseTV does this improve your performance in terms of jitter?

DatanoiseTV commented 2 years ago

@mathiasbredholt The gpio_set_level(LED, fmodf(phase, 1.) < 0.1); works okay, but I am not sure how to call midi_clock_send() 24 times in one phase-cycle instead of gpio_set_level()...

My POC looked like this:

while (true)
  {
    xSemaphoreTake(handle, portMAX_DELAY);

    auto state = ctx.link->captureAppSessionState();
    const auto numPeers = ctx.link->numPeers();
    const auto time = ctx.link->clock().micros();
    const auto phase = state.phaseAtTime(time, ctx.cfg.quantum);
    const auto beats = state.beatAtTime(time, ctx.cfg.quantum);

    gpio_set_level(PIN_START_OUT, state.isPlaying());

    gpio_set_level(PIN_LED_HB, fmodf(phase, 1.0) < 0.1);

    bool currentState = fmodf(phase, 1.0 / 12) < 0.004;
    if (lastState != currentState)
    {

      if (firstClock)
      {
        state.forceBeatAtTime(0.0, time, 4.0);
        ctx.link->commitAudioSessionState(state);
        const char data[1] = {0xFA};
        uart_write_bytes(UART_NUM_1, (const char *)data, sizeof(data));
        firstClock = 0;
      }

      if(state.isPlaying()){
        const char data[1] = {0xF8};
        uart_write_bytes(UART_NUM_1, (const char *)data, 1);
        gpio_set_level(PIN_CLK_OUT, fmodf(phase, 0.25) < 0.025);
      }

      //ESP_LOGI(TAG, "%i, Sending MIDI Clock", ctr++);
      lastState = currentState;
    }
  }
}
mathiasbredholt commented 2 years ago

I would probably just do something like

int currentState = state.beatAtTime(time, 1.) * 24;
if (currentState > lastState) {
  // send midi clock
  lastState = currentState;
} 

Then I would set the clk pin independently, as this can be updated continuously, while the midi clock should only be triggered when phase equals zero.

If you wanna stay in floating point world, you could also generate a phase signal which has a period of 1/24 of a bar and then check if the gradient is non positive (only happens once in a cycle, exactly when the phase resets)

auto phase = state.phaseAtTime(time, 1./24.);
if (phase - lastPhase < 0) {
  // send midi clock
}
lastPhase = phase;
DatanoiseTV commented 2 years ago

@mathiasbredholt First one doesn't get triggered somehow. I will investigate a bit more. I'd like to get it as jitter-less as possible. Would you maybe have time tomorrow or in the upcoming days for a chat? I'm working on an open source hardware.

mathiasbredholt commented 2 years ago
image

@DatanoiseTV and I have been trying to generate an accurate MIDI clock from Link on ESP32, but we're experiencing issues with shootouts in the jitter of the clock. On this plot, the shootouts seem to be periodic with ~30 sec, I wonder if there's any timeouts in Link with this period @fgo-ableton?

mathiasbredholt commented 2 years ago

Ok possibly this one in Sessions.hpp?

  void scheduleRemeasurement()
  {
    // set a timer to re-measure the active session after a period
    mTimer.expires_from_now(std::chrono::microseconds{30000000});
    mTimer.async_wait([this](const typename Timer::ErrorCode e) {
      if (!e)
      {
        launchSessionMeasurement(mCurrent);
        scheduleRemeasurement();
      }
    });
  }
DatanoiseTV commented 2 years ago

Ok possibly this one in Sessions.hpp?

  void scheduleRemeasurement()
  {
    // set a timer to re-measure the active session after a period
    mTimer.expires_from_now(std::chrono::microseconds{30000000});
    mTimer.async_wait([this](const typename Timer::ErrorCode e) {
      if (!e)
      {
        launchSessionMeasurement(mCurrent);
        scheduleRemeasurement();
      }
    });
  }

I tried removing it and changing the interval. It just delays the problem or makes it appear faster.

fgo-ableton commented 2 years ago

@DatanoiseTV and I have been trying to generate an accurate MIDI clock from Link on ESP32, but we're experiencing issues with shootouts in the jitter of the clock. On this plot, the shootouts seem to be periodic with ~30 sec, I wonder if there's any timeouts in Link with this period @fgo-ableton?

Yes. Link periodically re-measures the timing offset with other peers. As this will update the host time / beat time relation it can lead to jumps in the timeline. This would be expected behaviour. The time difference depends on the amount of clock drift between the different devices. In this case you should let the midi clock catch up smoothly with Link. (Some kind of low pass filter like behaviour). Is the behaviour the same for the device that enabled Link first and a device that joined a session? If the issue only exists on devices that join an existing session this is likely what happens.

Another issue causing this could be that the measurement blocks the loop that generates the MIDI ticks. In this case pinning Link to a different core than the one generating MIDI could help.

mathiasbredholt commented 2 years ago

Yes. Link periodically re-measures the timing offset with other peers. As this will update the host time / beat time relation it can lead to jumps in the timeline. This would be expected behaviour.

I understand, this would explain the shootouts.

The time difference depends on the amount of clock drift between the different devices. In this case you should let the midi clock catch up smoothly with Link. (Some kind of low pass filter like behaviour).

I had expected that the filtering of the measurements within Link would smooth out the jumps, but a filter solution would work :)

Is the behaviour the same for the device that enabled Link first and a device that joined a session? If the issue only exists on devices that join an existing session this is likely what happens.

Haven't checked this yet, but will do!

Another issue causing this could be that the measurement blocks the loop that generates the MIDI ticks. In this case pinning Link to a different core than the one generating MIDI could help.

We have tried a lot of switching around priorities and tasks/pinning to cores and it doesn't seem to resolve the issue.

mathiasbredholt commented 2 years ago

I came up with a filter solution that works pretty well for my tests. The idea is to generate a phase signal from the BPM and then calculate the error between the BPM and Link generated phases. The error is used to lock the BPM generated phase signal to the Link timeline with an adjustable lag. The lag can be adjusted by changing the gain of the error signal.

I implemented it in a PD patch below:

image

These are my results:

image

This method seems to solve the issue with shootouts.

fgo-ableton commented 2 years ago

The result looks nice! I actually thought more in the direction of regularly scheduling upcoming ticks for some small time interval (100ms or so). But this makes sense too.

ndonald2 commented 2 years ago

Confirming that a well-tuned PLL tracking the link timeline is highly effective for regulating and smoothing tempo changes in a pulse-clock output. I was able to engineer a solution using the general concept illustrated by @mathiasbredholt in the Pd patch screenshot. Thanks for sharing, this was a very helpful thread!