Rack: Add timestamp to midi::Message to improve MIDI interface timing

Created on 5 Dec 2019  路  2Comments  路  Source: VCVRack/Rack

Following the discussion on the communityboard:
https://community.vcvrack.com/t/dedicated-midi-clock-sync-module-idea-programmer-needed/6856/25?u=alasdairmoons

Would it be possible to include or set a TIMESTAMP to the midi::message for the special case of 0xF messages to improve/stabilize the Midi-Clock/BPM calculation in Rack v2.x.x.?

Most helpful comment

Problem

The cause of this issue is that MIDI messages are processed as soon as the engine runs VCV MIDI-CV's process() method after the message is received by the RtMidi driver.

  • If a MIDI message is received immediately after an audio buffer is pulled by VCV Audio but before the engine steps any frames, the MIDI-to-audio latency is blockSize / sampleRate.
  • If a message is received immediately before the audio buffer is pushed by VCV Audio but after the engine steps all frames in the buffer, the latency can be between blockSize / sampleRate (if we're near the audio buffer deadline) to 2 * blockSize / sampleRate (if we're well before the audio buffer deadline).
  • If a message is received in time for the last engine frame in the audio buffer to process, the latency can be between 0 (if near the deadline) to blockSize / sampleRate (if well before the deadline).

As you can see, the maximum jitter is 2 * blockSize / sampleRate, but of course is usually blockSize / sampleRate since it's rare for the engine to be near the audio buffer deadline for one buffer and finish stepping well before the deadline for another buffer.

Approach at a solution

Right now a module (such as VCV MIDI-CV) has no knowledge of the block size or when the block was requested, so even if we have MIDI message timestamps, we couldn't do anything with the information. I propose adding double Engine::getStepTimestamp() and int Engine::getStepFrame() which returns the timestamp and engine frame number the last time Engine::step(int frames) was called. (This is a new Rack v2 method.)

VCV MIDI-CV can then compute message timestamp - engine timestamp to see how long we need to wait before consuming the MIDI message. When message timestamp - engine timestamp < (current frame - last step frame) / sampleRate, then we're ready to process.

The only problem is that RtMidi annoyingly gives the delta-timestamp instead of the timestamp. I suggest ignoring this value and computing the timestamp based on when the RtMidiCallback is called. Might not be perfect, but it has fewer problems than trying to accumulate delta-timestamp values.

Reproducing

Before I finalize the above solution proposal, I need to quantify the amount of jitter before and after implementing a possible fix. I'll use a stable MIDI hardware clock.

All 2 comments

RtMidi provides a timestamp relative to the last MIDI message received. I think this is fine. But how do we then associate each message with an engine frame? Using floor(absoluteTime * sampleRate) has the problem of accumulating error and slewing towards the past or future.

Problem

The cause of this issue is that MIDI messages are processed as soon as the engine runs VCV MIDI-CV's process() method after the message is received by the RtMidi driver.

  • If a MIDI message is received immediately after an audio buffer is pulled by VCV Audio but before the engine steps any frames, the MIDI-to-audio latency is blockSize / sampleRate.
  • If a message is received immediately before the audio buffer is pushed by VCV Audio but after the engine steps all frames in the buffer, the latency can be between blockSize / sampleRate (if we're near the audio buffer deadline) to 2 * blockSize / sampleRate (if we're well before the audio buffer deadline).
  • If a message is received in time for the last engine frame in the audio buffer to process, the latency can be between 0 (if near the deadline) to blockSize / sampleRate (if well before the deadline).

As you can see, the maximum jitter is 2 * blockSize / sampleRate, but of course is usually blockSize / sampleRate since it's rare for the engine to be near the audio buffer deadline for one buffer and finish stepping well before the deadline for another buffer.

Approach at a solution

Right now a module (such as VCV MIDI-CV) has no knowledge of the block size or when the block was requested, so even if we have MIDI message timestamps, we couldn't do anything with the information. I propose adding double Engine::getStepTimestamp() and int Engine::getStepFrame() which returns the timestamp and engine frame number the last time Engine::step(int frames) was called. (This is a new Rack v2 method.)

VCV MIDI-CV can then compute message timestamp - engine timestamp to see how long we need to wait before consuming the MIDI message. When message timestamp - engine timestamp < (current frame - last step frame) / sampleRate, then we're ready to process.

The only problem is that RtMidi annoyingly gives the delta-timestamp instead of the timestamp. I suggest ignoring this value and computing the timestamp based on when the RtMidiCallback is called. Might not be perfect, but it has fewer problems than trying to accumulate delta-timestamp values.

Reproducing

Before I finalize the above solution proposal, I need to quantify the amount of jitter before and after implementing a possible fix. I'll use a stable MIDI hardware clock.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

oblivionratula picture oblivionratula  路  7Comments

ryan-allen picture ryan-allen  路  5Comments

gogobanziibaby picture gogobanziibaby  路  4Comments

Coirt picture Coirt  路  7Comments

AndrewBelt picture AndrewBelt  路  5Comments