Hello,
The stuff below is probably tl;dr for some of you so here my 2 questions. Background is below.
- Is it safe to call new MidiMessage() on the audio thread?
- Is it ever a good idea to have locks on the audio thread?
And here the tl;dr stuff :).
I’ve been reading up on best practices for audio applications programming. One element that keeps returning is “never use unbounded (in time) operations on the audio thread.”; examples of unbounded operations being file I/O or memory allocations (malloc() & friends). And another one being “don’t use locks on the audio thread because you might run into priority inversion”. ( see here for an example of an article I enjoyed that dives into these matters; I’m not experienced enough to judge the advice’s soundness but frankly it all makes sense to me ( http://www.rossbencina.com/code/real-time-audio-programming-101-time-waits-for-nothing )
So I have a series of plugins that generate MIDI data that are close to 1.0 release. Neither of them currently misbehaves (glitches, timing errors). I’m too old to be naive enough to believe that because my plugins seem to behave, they will always behave. So reading the above puzzles me for 2 reasons:
1 . Since my plugins generate MIDI, I do have - on the audio thread - plenty of new MidiMessage() calls. Don’t those allocate memory? If new MidiMessage() is a no-no, how do others go about circumventing that? I notice that e.g. in the Juce arpeggiator tutorial the code does something similar:
midi.addEvent (MidiMessage::noteOn (1, lastNoteValue, (uint8) 127), offset);
Is that poor practice and only in the tutorial to simplify things? Should I pre-allocate the memory? We are easily talking several 1000s of midi events per second here - so I’d have to pre-allocate - what? - several 100k midiMessages? Of course I could have that managed on a separate low priority thread, that allocates chunks of say 1000 midiMessages, keeps track of much many pre-allocated midiMessages have been “used” and generate more of these when needed (and plenty early). But that too would be unbounded - still risking an underrun of allocates messages. No?
Is pre-allocating the recommended approach? Or is new MidiMessage() safe? How do others deal with this?
2 My plugins generate realtime (midi) pattterns, and there is a fair amount of randomness inthere. Meaning I cannot actually generate the pattern data in real-time; I have to use some buffering because when processBlock decides to decide to perhaps generate an event, it might conclude that it should have done it earlier.
So I buffer these a bit ahead of time (I generate the outline of the patterns in my own datastructures; I translate them to Midi in real-time). The actual generation of the patterns is farmed off to a lower priority thread, and for a whole bunch of reasons, the timing of (re) generation is not critical. The way I do this now is:
- When the audio thread decides it’s time to generate some more pattern data, it wakes up a low priority worker thread
- The work thread does its job in separate data structures (also allocates memory, but since it’s not time critical we don’t care how long it takes) so there is no overlap with the data model the audio thread needs; and hence locks are not needed.
HOWEVER: I do use locking - on the audio thread. The way I see it this is not at all risky in this particular case:
- Audio thread locks the data model when doing processblock
- worker thread locks the model ONLY to swap the newly generated data into the model (and swap “used” data out of the model). The lock is held by the worker thread ONLY to swap those 2 pointers and that peanuts in terms of processor time.
I fail to see how this could be risky, or how this might cause a problematic priority inversion. A priority inversion might happen of course: when the audio thread wakes up, right after the worker thread snaps the lock to swap those 2 pointers, the audio thread just waits. That’s more than fine - the couple of instructions for swapping those pointers are irrelevant to the amount of work processBlock needs to do - it simply does not make any difference. FWIW I use a spinlock because it’s low overhead and frankly it fits my needs (don’t need the added complexity of critical sections)
For now that seems to work great. But I’m wondering:
- Am I missing something? Is there a real reason why this would be a bad idea/approach in this particular case?
- If I am missing something then what is the way to handle situations like this without using locks?
Any thoughts would be appreciated; feel free to alert me to my own stupidity .