Midi accuracy


Hi, 
I am new to Juce development, and far from being a C++ expert. I'm experimenting a bit with this library, and so far I've been able to build quite a few funny (and basic) things.
One thing I cannot manage to get working is a small metronome. I know there already are some posts here, but some are old, some are without any answer... so it was no real help for me.
Here is what I want to do: when I click on a button, I launch a very basic metronome that is always producing the same midi sound, with the same interval of time.
Here the code I came with, it is called when my button is clicked.


int tempoInBpm = 150;
MidiOutput* midiOutput = MidiOutput::openDevice(0);
MidiMessageSequence midiMessageSequence;
MidiBuffer midiBuffer;
for(int i=0; i<999; i++){
    MidiMessage midiMessage(153, 56, 100/*, i*1000*/);
    midiBuffer.addEvent(midiMessage, i);
}
midiOutput->startBackgroundThread();
midiOutput->sendBlockOfMessages(midiBuffer, Time::getMillisecondCounter() + 2, tempoInBpm/(double)60); 


This is quite a dummy code (the loop towards 999 is just to test), but it does not really work: the accuracy of the time intervals between two notes is not good. Almost, but not 100% exact.


I saw posts about people trying to send their midi messages in a Timer, and they were answered "do not use Timer, but Audio Thread instead". Actually, as you see, I am not using any Timer.
I also saw a post about the internal behaviour of "MidiOutput::sendBlockOfMessages" that was using a Timer, so being not reliable, but is it really the case? Is it still the case with the last version of Juce (this is the one I am using)?
Moreover, if "MidiOutput::sendBlockOfMessages" is not reliable, what is the purpose of this method?


Is is really mandatory for accuracy to send Midi messages in the Audio Thread? How can I simply achieve this?


Thanks for your help!

 

ps: disclaimer: Sorry for the indentation of the code, I am a native Java and PHP developper, I have always coded this way, and not really wanting to change for the time being

disclaimer2: I am French, so sorry for the uncorrect language...
 

Hi, could anyone please give me an answer?

Or at least, tell me if my post is too noobish, or too complicated?

Thanks.

Your code looks pretty confused about the timestamps of the midi messages it's creating..

Hi, and thanks for your reply!

Sorry for having let an instruction in comments; I do not think I have any confusion about the timestamps as the API doc clearly states in the MidiBuffer class, method "addEvent": "The MidiMessage's timestamp is ignored". That is why I put my timestamp of the MidiMessage in comment.

So my code is (I just removed the comment on line 7):


int tempoInBpm = 150;
MidiOutput* midiOutput = MidiOutput::openDevice(0);
MidiMessageSequence midiMessageSequence;
MidiBuffer midiBuffer;
for(int i=0; i<999; i++){
    MidiMessage midiMessage(153, 56, 100);
    midiBuffer.addEvent(midiMessage, i);
}
midiOutput->startBackgroundThread();
midiOutput->sendBlockOfMessages(midiBuffer, Time::getMillisecondCounter() + 2, tempoInBpm/(double)60); 

 

As you can see, I am just creating a big buffer of midi messages, throw it to the MidiOutput, and tell the MidiOutput to play each midi message with an interval of X. The problem is the interval of X is not 100% respected.

 

I also tried to follow directions from this post: http://www.juce.com/comment/274970#comment-274970. The problem is there is no source code, so even if I understand the therory, I was not able to implement it (I can post some code I tried but I'm not behind my dev computer right now).

 

The only thing I could set to work well is with a "HighResolutionTimer" sending midi messages at a particular rate. It works as expected, but I feel it is not a good way to build a metronome.

Could anyone confirm my thoughts/help me with this?

 

Thanks for your time.

you should set timestamps for individual midi messages. Also your buffer's samplesPerSecondForBuff should depend on the output device sample rate, and it should probably be 60/bpm, not the other way around.

-- p

Thanks for your reply, your suggestion looks like what I tried first (that was in comment in my first post): assign timestamps for each midi message, put the midi messages in a midi buffer, and throw this buffer to the midi output. The only thing I did not test is to set the samplesPerSecondForBuff to 60/bpm.

As far as I remember, it did not work: it produced only one single sound, because as it is stated in the classes API documentation for the MidiBuffer class, method named addEvent: "The MidiMessage's timestamp is ignored". 

I will try and I let you know if this works, thanks for your time!

 

Some hints:

  • When the documentation talks about sampleNumber/samplePosition (in connection with e.g. MidiBuffer) it's in relation to the sample frequency of your audiocard, most common 44100 or 48000 samples/sec. That is, real world sample numbers for consecutive midi events spaced 1 sec apart (think 60bpm) would be 0, 44100, 88200, 132300 etc. and the call of sendBlockOfMessages would rather look like
    sendBlockOfMessages(midiMessages, Time::getMillisecondCounter(), 44100);
     
  • Use MidiMessage::noteOn (10, 53, 0.8f) rather than midiMessage(153, 56, 100), it's clearer.
     
  • A noteOn message should always be followed by a corresponding noteOff message in due time acc to the midi specification. Although this has probably no direct bearing to your timing problems, it'll be a good idea to eventually add them when you've got your metronome up and running.

    Good luck!

Hi,

  • really interesting point on the sampleNumber, I will look at it! So if I understood you correctly, the "sendBlockOfMessages" is only used to send multiple midi messages at the exact same time (for instance, a drum kick and crash at the beginning of a bar).
  • ok for the noteOn, I did not know it was clearer
  • yes (one thing I knew, at least :)) I knew for the noteOff, but my code as it is here is just a test, and it is working without the notes off. I know it is quite ugly, and will put the notes off when I get the "notes on" 100% accurate.

Thanks for your reply!

 

Hi, I did try what you suggested and it did not work. As I told you, setting the timestamps for individual midi messages is useless, as it is clearly stated in the docs.

example: if in my code, I write "MidiMessage midiMessage(153, 56, 100, i*10000);", it clearly plays the same as "MidiMessage midiMessage(153, 56, 100, i*10);".

Moreover, you got your calculation wrong, 60/bpm is false, the good calculation is (as written in my first post) bmp/60.

example: If my bpm is 120, then bpm/60 = 2 (twice as fast): good, 60/bpm = 0.5: bad

Sorry if I did not understand what you suggested, but I think your solution does not work. Anyway, thanks for your time.

EDIT: Ooops, of course I did not understand was you meant because the user "4321" explained your suggestion with a code example, which works, sorry. Your calculation is RIGHT. But mine is also right, it depends on how you insert messages in the buffer. Anyway the ticks are still not accurate.

Hi, I tried what you suggested, implemented this way:

MidiBuffer midiBuffer;
midiOutput->startBackgroundThread();
MidiMessage midiMessage(153, 56, 100);
midiBuffer.addEvent(midiMessage, 0);
for(int i=0; i<999; i++){
    midiOutput->sendBlockOfMessages(midiBuffer, Time::getMillisecondCounter(), i*44100);
}

It's by far worse than what I did first. Really not accurate, it throws cracks and pops in my headphones, and I have to kill the app myself through the taskmanager.

I also tried this:

MidiBuffer midiBuffer;
midiOutput->startBackgroundThread();
MidiMessage midiMessage(153, 56, 100);
midiBuffer.addEvent(midiMessage, 0);
for(int i=0; i<999; i++){
     midiOutput->sendBlockOfMessages(midiBuffer, Time::getMillisecondCounter() + i, 60);
 }

It may be really stupid, but I try what I can. Of course it did not work.

I thought this was a simple issue, but in almost one month, nobody could answer me the right thing, so maybe it is complicated? Or maybe Juce is not done for this? Can anyone share his thoughts on this, please?

 

Thanks for your time!

 

 

I never use MIDI features of JUCE such consider my suggestion with caution.

const double unit = 44100.0;

for (int i = 0; i < 1000; ++i) {
    midiBuffer.addEvent (midiMessage, i * unit * aBpmRatio);  
}

double delay = Time::getMillisecondCounter() + something;

midiOutput->sendBlockOfMessages (midiBuffer, delay, unit);

Please correct me if i’m wrong.

Hi, and thanks for your reply!

You are (almost) right: you code works, but it gives me the exact same result as my first code: still not 100% accurate. The first ticks are ok, then it begins to tick ahead or behind the real time. The faster the bpm, the clearer it can be heard.

But thanks to you I was able to understand one post from the user "petah", I told him his suggestion was wrong, it was the same as you (but without any code): I was wrong!

Here is the code I tested, thanks to you:

MidiBuffer midiBuffer;
MidiMessage midiMessage(153, 56, 100);
double aBpmRatio = (double)60/180; //180 is the bpm here
const double unit = 44100.0;
for (int i = 0; i < 1000; ++i) {
    midiBuffer.addEvent (midiMessage, i * unit * aBpmRatio);  
 }
double delay = Time::getMillisecondCounter() + 2;
midiOutput->startBackgroundThread();
midiOutput->sendBlockOfMessages (midiBuffer, delay, unit);

As I wrote in a previous answer, I could set up a more accurate metronome with a "HighResolutionTimer", maybe there is no other solution?

Thanks!

To understand what's happen the best way is to investigate through the JUCE sources. Currently i have no idea how the process is managed there so i can not say if such precision is possible.

But what's the precision you are expecting? I would be surprised if more or less 5 ms is not doable with MIDI classes. Of course if you want (sub)millisecond schedulers it's another problem.

When i started to programming i was astonished by how hard it is to get a precise clock. More when you include preemption into the scope ( https://en.wikipedia.org/wiki/Preemption_%28computing%29 ). Using C language at that time i made my own HighResolutionTimer with synchronisation timestamps. Sometimes the hard way is the one that fit.