Isn't there any way to add MIDI messages to a MIDIBuffer without allocating?

Here’s a simple plugin which can be quite useful: broadcasting incoming MIDI events to all channels. This means the output buffer is 16x the size of the input buffer. We’d like to do this without allocating.

Here is something that doesn’t work at all, which I’ve seen suggested maybe dozens of times on here: having a MidiBuffer as a member variable of your PluginProcessor and calling MidiBuffer::ensureSize() on it somewhere else:

// somewhere else, not on the audio thread...
juce::MidiBuffer processedMidi;
processedMidi.ensureSize(SOME_LARGE_NUMBER); // <-- SWAPPED AWAY BELOW

void MyAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages) {
    processedMidi.clear();
 
    for (const auto metadata : midiMessages) {
        for (int channel = 1; channel <= 16; ++channel) {
            juce::MidiMessage message = metadata.getMessage();
            message.setChannel (channel);
            processedMidi.addEvent (message, metadata.samplePosition);
        }
    }
 
    midiMessages.swapWith (processedMidi);  // <-- GONE
}

We’ve basically safely preallocated our nice large buffer… and then swapped it out. After this, processMidi has the smaller default buffer size of 2048 again, which JUCE has set as a magic number to preallocate all midiBuffers to: see “MidiBuffer memory allocation

For VSTs, what happens is that on the next iteration of processBlock, the original large buffer will be swapped back, so it’ll keep ping-ponging between the two buffer sizes. For AU’s, this instead happens every three iterations. I’m not sure about other plugin types.

So, if you ever get an input buffer with 1024/16 = 128 MIDI messages in it, and it’s on an odd-numbered sample (or a sample divisible by 3 for AUs), this will allocate.

Is it possible to do this kind of thing in JUCE without allocating?

Could we perhaps make that 2048 something the user can change without patching the library?

One thought: if it is guaranteed that we’ll always just be rotating between reused buffers when we call swapWith() - like we saw above with VST and AU - then we can simply call ensureSize within processBlock.

This doesn’t do anything if the midiBuffer is already the size we’re ensuring, so we’ll get an allocation only the first few times processBlock is called (i.e. on plugin initialization). Then after that, nothing will happen.

So I guess the question is: are we certain that JUCE, internally, is always going to be sending us one of a small number of preallocated midiBuffer’s that we’ve seen before, for all possible plugin types (AU, VST, etc), so that after some initial segment of processBlock() calls, nothing happens?

Due to the 80:s limited serial protocol speeds, the max number of ordinary midi messages (such as not-on and note-offs etc) is about 1000 per second.

The audio buffer of an audio interface is typically set to 10ms. Which means it will contain no more than about 10 midi messages. Multiply that by 16 and you would still have ample of space left in the midibuffer.

Yes, you can create a midifile with an outrageous amount of midi messages destined to be played at exactly the same time, but this will certainly clog up the midi transport layer or any daw/plugins before they arrive at their destination. And they certainly won’t arrive in time…

Perhaps I don’t really understand your use case, is it just theoretical or do you have a real world example where this amount of midi throughput is meaningful?

1 Like

Yes: see the very first sentence of my original post. I’m not sure where you’re getting this idea from that one will never see more than 10 messages in one midibuffer, but it certainly isn’t true on my system.

Calling ensureSize in processBlock seems to work for the platforms that I’ve tested it on, which so far is AU/VST on M1. It allocates the first two or three calls to processBlock, and then never seems to allocate again, as expected from the above. It would be good to know if this is the correct way to do it, though. Perhaps @fr810 would have some clarity on this?

That’s not what I’m saying. I’m saying a typical audio interface buffer is of a duration of 10ms, and this buffer will contain at most 10 typical midimessages when using a standard midi transmission line. And that a midi file can contain in principle an unlimited amount of midi messages destined to be played at the same time, and hence is likely to be able to fill a buffer of any practical size. But that is quite unlikely, while it would be hard to find a midi player that could cope with an unlimited amount of simultaneous midi events.

It’s hard to understand what you’re up to, “brodacsting incoming MIDI events to all channels”, what does this even mean? Where do the messages come from for a start? From a keyboard being played in real time, from a pre-recorded midi file or from something else? And what do you mean by broadcasting? Where are those midi messages heading? A midi channel is not a destination, it’s merely a label used for sorting.

If you’re a bit more specific about your goal and your actual setup, you’d likely get more useful answers.

This is not a satisfying answer for a few reasons:

  1. plug-ins aren’t connected over a 5-pin DIN connection; there’s no reason to impose a transport-based limitation that doesn’t exist (and most physical MIDI gear sold in the last few decades has included a USB MIDI connection, more frequently as its only MIDI connection. USB MIDI isn’t throttled in the same way)
  2. In the MIDI 2.0 world we’re entering, the amount of MIDI data that will be in flight at any given moment is going to need to be able to be orders of magnitude higher than what we’re used to now.
3 Likes

Well the problem, as I see it, is that OP wants to receive the max number of messages possible over a transmission line capable of max n messeges/s (n being 1000, 1 000 000 or whatever you like it to be), multiply this amount by 16 and broadcast it over (I assume) the same kind of transmission line. That aint going to work for obvious reasons.

If this is not their use case, maybe they could define it a bit more thoroughly, e.g. by specifying the kind of midi source, the max number of messages expected and whereto and how these messages are going to be broadcasted.

I think you all are totally off topic! There is a clear question (it is even in the title) and it is throwing light on a problematic bit of the JUCE library, where it does not offer us to bypass system calls effectively, even though a solution was outlined by the OP. The question was, is there another way — which apparently there isnt as the behavior isn’t even consistent between different plugin formats which, IMHO, is the main pain point here.

Maybe someone from the JUCE team can take position to the actual question and we stop with this midi limit transmission nonsense now :slight_smile:

4 Likes

So, I just checked the source code of JUCE, and this 2048 seems to be hardcoded separately into each processor wrapper (I think this should be changed at the very least).

And I think calling ensureSize on all MidiBuffers coming is probably the best you can do right now. But having a separate class member processedMidi does not seem to do anything good for you, it just increases the RAM usage (even if marginal). I’d rather suggest you implement a counter + assert, tracking that per prepareBlock/releaseResources call, you are only enlarging the MidiBuffer coming in 5 times (?) max, so that if the behaviour on JUCE end changes (for example by preallocating 2048 events on the stack or whatever) you’ll get an assert that you path is no longer a viable solution.

Yes, it would be great to have some control over the preallocated size of that buffer.

P.S. the need for buffer.swapWith is not to avoid allocation but for logical reasons - if you intend to modify the messages in the buffer that’s the only logical way to do it correctly in a plugin in JUCE (that I know of).

That is because the buffer isn’t giving you a reference to the hosted MIDI messages, and is instead presenting the data to you in a read-only way.

3 Likes

My bad, you are absolutely correct. But that only resulted out of this specific usecase. Seeing just the title question, an additional midi buffer is not necessary

I second being able to control the pre-allocated size of the process block midi buffer.
For example I need to inject 400+ midi messages into a buffer (for …reasons) which causes a spike. Would be nice to resolve that by making the incoming buffer bigger.

Edit: Nevermind, I tried changing the hardcoded size, turns out to not be that. But the issue still does stem from addEvent since commenting that out gets rid of the spike.

1 Like

Solved my problem above by checking if each midi message is redundant before adding them. This prevents most midi messages from being added to the buffer since almost always 90%+ of them are gonna be redundant. This caused my CPU meter to go from spiking to 30-50% (at a buffer size of 32) to not spiking at all.