micdah / RtMidi.Core

RtMidi for .Net Standard
https://rtmidicore.micdah.dk/
Other
36 stars 12 forks source link

How to get more time resolution #13

Closed jllodra closed 3 years ago

jllodra commented 6 years ago

Hi,

First of all, thanks for writing and sharing this software.

Today I forked it and tried to add MIDI Clock support (which I need to calculate BPM). But what I found was a lack of precision for the timestamp (in private void HandleRtMidiCallback(double timestamp, IntPtr messagePtr, UIntPtr messageSize, IntPtr userData).

image

With values like 0,021 and 0,02 I can't get the original 120 bpm value, what I get is 119 and 121.

Do you know if that is the max resolution rtmidi can provide?

Thank you for reading this!

micdah commented 6 years ago

Hi @jllodra,

Great to hear that you are tinkering with adding new features, do make a pull request if you end up with something awesome.

Looking into the rtmidi code, this is what I found with regards to how the timestamp is calculated.

// Calculate time stamp.
  if ( data->firstMessage == true ) {
    apiData->message.timeStamp = 0.0;
    data->firstMessage = false;
  }
  else apiData->message.timeStamp = (double) ( timestamp - apiData->lastTime ) * 0.001;

But it depends on the specific platform, as rtmidi uses different underlying MIDI providers depending on the available API's.

The timestamp used in the above, is provided to the platform-specific event handler.

So I don't know if it might be some inaccuracy in the timestampprovided to the rtmidi event handler - as far as I can trace, the callback method is handed off to Windows MM midiInOpen which invokes it on incoming MIDI messages, as described here: https://msdn.microsoft.com/en-us/library/Dd798458(v=VS.85).aspx

Another route to go, could be that we calculate our own timestamp internally in RtMidi.Core and not rely on the underlying MIDI library in use?

The callbcak function is invoked shortly after the code which determines the timestamp, meaning our .net handled callback function ought to be invoked shortly after, so we might be able to produce a precise enough timestamp just when we receive it.

Could do a quick experiment using a System.Diagnostics.Stopwatch?

micdah commented 6 years ago

Here's the specific data structure as defined for Windows MM: https://docs.microsoft.com/da-dk/windows/desktop/Multimedia/mim-data

dwTimestamp Time that the message was received by the input device driver. The time stamp is specified in milliseconds, beginning at zero when the midiInStart function was called.

micdah commented 6 years ago

But I guess if the time resolution available is only down to the millisecond, then we cannot rely on that for high enough precision for this kind of time-keeping?

With 120BPM we would receive a clock every 20833 microsecond, or 20.833 millisecond, so rounding that to nearest millisecond would become 20 or 21 milliseconds, depending on rounding strategy, and thus a quite significant loss of precision, no?

Seems we need to keep track of time ourselves using something with higher than millisecond precision.

micdah commented 6 years ago

I guess something along these lines in MidiInputDevice might do?

private readonly Stopwatch _stopwatch = Stopwatch.StartNew();
private long _lastTicks = 0;

...

private void RtMidiInputDevice_Message(object sender, byte[] message)
{
     // Calculate timestamp
    var ticks = _stopwatch.ElapsedTicks;
    var timestamp = ((double) (ticks - _lastTicks)) / Stopwatch.Frequency;
   _lastTicks = ticks;
   ...
}
jllodra commented 6 years ago

Hi @micdah,

Thank you for your follow-up.

Indeed, using a Stopwatch (QueryPerformanceCounter) should work and we should be able to get steady times, I haven't tested it myself yet but I will.

To give you some background about the topic, I am working on a Unity application that needs to read MIDI events (note & CC messages that RtMidi.Core provide plus MIDI Clock). Unity uses mono as scripting backend, I liked how RtMidi.Core is written so I decided to try it, and I got to integrate RtMidi.Core into my Unity project in an afternoon. The only issue I had to deal with was that RtMidi.Core seems to be written in C#7, and the mono runtime in Unity supports up to C#6 (and they don't have plans to upgrade to 7, they are moving to Roslyn but not at the moment).

So, I had to "downgrade" it to C#6. I did it but I am unaware of the consequences or performance penalties.

Drachenkaetzchen commented 5 years ago

I also have the need for MIDI Clock, and did a test using Stopwatch. The result jitters about the same, if not more than the MIDI timestamp itself. Here's an excerpt of a 120bpm MIDI clock via loopMIDI running on the same host:

0,02 clock bpm 125 stopwatch 0,0199037 stopwatch bpm 125,604787049644
0,021 clock bpm 119,047619047619 stopwatch 0,0209928 stopwatch bpm 119,088449373118
0,021 clock bpm 119,047619047619 stopwatch 0,021519 stopwatch bpm 116,176402249175
0,021 clock bpm 119,047619047619 stopwatch 0,0205244 stopwatch bpm 121,806240377307
0,021 clock bpm 119,047619047619 stopwatch 0,020931 stopwatch bpm 119,440064975395
0,02 clock bpm 125 stopwatch 0,0197399 stopwatch bpm 126,647044817856
0,022 clock bpm 113,636363636364 stopwatch 0,0219713 stopwatch bpm 113,784801081411
0,021 clock bpm 119,047619047619 stopwatch 0,0209088 stopwatch bpm 119,566880930517
0,021 clock bpm 119,047619047619 stopwatch 0,0209698 stopwatch bpm 119,219067420767
0,021 clock bpm 119,047619047619 stopwatch 0,0209424 stopwatch bpm 119,375047750019
0,02 clock bpm 125 stopwatch 0,0203236 stopwatch bpm 123,009703005373
0,022 clock bpm 113,636363636364 stopwatch 0,0215804 stopwatch bpm 115,845860132342
0,019 clock bpm 131,578947368421 stopwatch 0,0199187 stopwatch bpm 125,510198958767
0,021 clock bpm 119,047619047619 stopwatch 0,0209741 stopwatch bpm 119,194625752714
0,021 clock bpm 119,047619047619 stopwatch 0,0210208 stopwatch bpm 118,929821890699
0,021 clock bpm 119,047619047619 stopwatch 0,021003 stopwatch bpm 119,030614674094

However, I have good news. By utilizing a ring buffer, I was able to smoothen out things.

24 clocks (= one beat)

0,02075 average bpm for buffer size 24: 120,48, 0,02000 clock bpm 125,00 stopwatch 0,01998 stopwatch bpm 125,10
0,02088 average bpm for buffer size 24: 119,76, 0,02200 clock bpm 113,64 stopwatch 0,02145 stopwatch bpm 116,55
0,02083 average bpm for buffer size 24: 120,00, 0,02000 clock bpm 125,00 stopwatch 0,02015 stopwatch bpm 124,04
0,02079 average bpm for buffer size 24: 120,24, 0,02000 clock bpm 125,00 stopwatch 0,02024 stopwatch bpm 123,50
0,02088 average bpm for buffer size 24: 119,76, 0,02300 clock bpm 108,70 stopwatch 0,02270 stopwatch bpm 110,15
0,02083 average bpm for buffer size 24: 120,00, 0,01900 clock bpm 131,58 stopwatch 0,01948 stopwatch bpm 128,36
0,02083 average bpm for buffer size 24: 120,00, 0,02100 clock bpm 119,05 stopwatch 0,02108 stopwatch bpm 118,62
0,02088 average bpm for buffer size 24: 119,76, 0,02200 clock bpm 113,64 stopwatch 0,02182 stopwatch bpm 114,58
0,02083 average bpm for buffer size 24: 120,00, 0,02100 clock bpm 119,05 stopwatch 0,02097 stopwatch bpm 119,24
0,02075 average bpm for buffer size 24: 120,48, 0,01900 clock bpm 131,58 stopwatch 0,01928 stopwatch bpm 129,67
0,02075 average bpm for buffer size 24: 120,48, 0,02100 clock bpm 119,05 stopwatch 0,02091 stopwatch bpm 119,55
0,02079 average bpm for buffer size 24: 120,24, 0,02100 clock bpm 119,05 stopwatch 0,02106 stopwatch bpm 118,73
0,02079 average bpm for buffer size 24: 120,24, 0,02200 clock bpm 113,64 stopwatch 0,02153 stopwatch bpm 116,10

96 clocks (= one bar)

0,02083 average bpm for buffer size 96: 120,00, 0,02100 clock bpm 119,05 stopwatch 0,02031 stopwatch bpm 123,07
0,02083 average bpm for buffer size 96: 120,00, 0,02100 clock bpm 119,05 stopwatch 0,02128 stopwatch bpm 117,51
0,02084 average bpm for buffer size 96: 119,94, 0,02200 clock bpm 113,64 stopwatch 0,02163 stopwatch bpm 115,56
0,02082 average bpm for buffer size 96: 120,06, 0,02000 clock bpm 125,00 stopwatch 0,01987 stopwatch bpm 125,80
0,02082 average bpm for buffer size 96: 120,06, 0,02000 clock bpm 125,00 stopwatch 0,02165 stopwatch bpm 115,49

384 clocks (= 4 bars)

0,02084 average bpm for buffer size 384: 119,99, 0,01900 clock bpm 131,58 stopwatch 0,01980 stopwatch bpm 126,24
0,02084 average bpm for buffer size 384: 119,99, 0,02100 clock bpm 119,05 stopwatch 0,02054 stopwatch bpm 121,68
0,02083 average bpm for buffer size 384: 120,00, 0,02000 clock bpm 125,00 stopwatch 0,02039 stopwatch bpm 122,62
0,02083 average bpm for buffer size 384: 120,02, 0,02100 clock bpm 119,05 stopwatch 0,02075 stopwatch bpm 120,46
0,02083 average bpm for buffer size 384: 120,02, 0,02200 clock bpm 113,64 stopwatch 0,02173 stopwatch bpm 115,05

In my (brief) tests, I've found 96 clocks to be the best compromise between accuracy and (potential) tempo change delays.

I'll put up a PR very soon, including a (potential) deal breaker since I've added timestamp support for each callback.

Drachenkaetzchen commented 5 years ago

Oh another info: I just tried to sync NI Maschine with NI Traktor.

Guess what - they are also interpolating. Even worse - the BPM cycles between 119 and 121 BPM over a period of a few seconds. No wonder they implemented Ableton Link ;)

micdah commented 4 years ago

@Drachenkaetzchen You still have this issue and related PR, I guess you have stopped developing on the PR.

Can I close this issue and related PR?

Drachenkaetzchen commented 4 years ago

I closed the PR but I cannot close this issue.