Closed DatanoiseTV closed 1 year 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?
@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;
}
}
}
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;
@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.
@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?
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();
}
});
}
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.
@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.
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.
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:
These are my results:
This method seems to solve the issue with shootouts.
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.
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!
I am working on a device that uses Link (on ESP32). I need to make a midi clock with 24 pulses.
If I do
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?