Passing MIDI messages using MIDI File Player - Buffer size dependency

Just an exploratory thread discussing possible improvements and workarounds, in case others are in the same boat as me.

I’m using GP to run backing tracks for my band. I export stems from Logic, load them into dedicated streaming file players, sync them all up to the global playhead, and it works great.

In addition to this, I use the same Logic session to create a MIDI track for sending messages to my pedalboard (for turning effects on and off, expression pedal sweeps, preset changes, and so on). I export this as a midi file and load it into a MIDI player in the same GP rackspace, sync that to the global playhead, and it passes MIDI in time with the track.

My question concerns the MIDI player and its relation to buffer size:

As I was experimenting with this, it looks like the MIDI messages are not actually passed as the playhead passes the message in the MIDI file, but actually at the buffer boundary of the buffer that happens to contain the message (not sure if it’s always at the end of the buffer, always at the start of the buffer or whichever one is closest).

In practise, this means that if I increase the buffer size to reduce possible audio glitches from the audio players, my pedalboard patch changes drift more and more out of time with the music, so I have to find the balance between consistent audio performance and “close enough” MIDI messaging.

My best attempt at the math describing the consequences I’ve experienced:

Buffer “length” at 48kHz sample rate:

  • 128 samples → 128 / 48000 = 2.67 ms
  • 2048 samples → 2048 / 48000 = 42.67 ms

One set of messages that I pass to the board at the start of every song is a set of tap tempo messages, aligned to the grid:

Say the song is 130 BPM. One beat = 60000 / 130 = 461.5 ms between taps. Each tap gets snapped to the buffer grid, so each measured interval can be off by up to ±one buffer period.

At 128 samples, the possible interval range between taps is roughly 458.8 ms to 464.2 ms (461.5 +/- 2.67ms). This gives the pedalboard a received BPM range of 129.3 to 130.8 BPM (60000/464.2 to 60000/458.8)

At 2048 samples, the interval range is now huge: ~418.8 ms to ~504.2 ms, which means the pedalboard could now end up with a tempo setting of anywhere from 119.0 to 143.3 BPM when processing the tap messages. Not great if I need my delays and other effects to stay in time with the music.

The same timing issue hits if I need a patch change to land exactly on a downbeat. At 2048 samples, that means a possible timing offset of 42.67ms, which translates to somewhere around a 32nd note at 130bpm. That means that the attack of that downbeat chord could get swallowed completely if you’re unlucky with your buffer timing.

Disclaimer: I know I can use markers in GP instead of the MIDI player for this, but there are two problems with that approach:

  1. There’s no grid in the streaming file player UI that I can use to align my messages with, so I have to painstakingly find the timestamp for each message and place it manually based on that.
    So a possible improvement there would be to display a grid there and maybe even give us a “snap messages to grid” option. This would of course require the UI to know the bpm of the file, so that would need to be a setting in some way. I use the song properties to set the BPM of each song, and the song part actions to load the correct audio file in the file players, so I’m not sure how this grid could work in practise since the file player UI doesn’t know any of this information. Anyway, just a thought.
  2. Adding things like expression pedal sweeps, where I literally pass dozens or hundreds of CC messages in a row for a few bars, would take hours to do using markers. In Logic, I can just draw them into a MIDI region in seconds, or just perform the sweep manually and record the automation.

I’m sure there are technical reasons why it’s hard to pass a MIDI message right at the correct time (when the playhead actually passes it). You would probably also somehow need to make sure that the MIDI player doesn’t actually start passing messages right away, but waits an entire buffer size before playing (so that it stays in sync with the audio players), which is probably a whole headache in its own right. But I figured it was worth at least making a post for it.

You could use a scriptlet to delay the MIDI Message

On MidiEvent(m : MidiMessage)
 SendLater(m, 500)
End

In this case any MIDI Message is sent out delayed by 500 ms.

Not really, because the offset isn’t a consistent length of time for each message.

A message that happens to be located right at the buffer boundary will get sent to my pedalboard at the correct time, while one that happens to be at the extreme end of a buffer will get offset by 42ms. No way to calculate that for each message in real time, as far as I know.

Another idea:
Did you try using different instances of GP?
The main instance for the backing tracks and the another instance for your MIDI processing?

Yeah, considered that but haven’t tested it. I imagine that keeping the playheads in sync across two instances (both starting them at the exact same time and keeping them in sync) is an even bigger challenge. I would presumably need to set up an OSC channel for passing the song tempo to the MIDI instance before the playhead starts, and then make sure that the start of the audio players line up exactly with the start of the MIDI player across two instances and two different buffer sizes. More of a last resort than anything, I think.

At the moment the system seems to behave pretty well at 128 samples, but I would like to ideally run the tracks players at 512 or 1024 just to minimize the buffer audio glitch potential.

Would love some feedback from the GP devs about my theory for what’s causing this, and if there’s hope for improvements in future versions.

Tempo Sync and Global Playhead Start is easy to achieve in using Ableton Link in Gig Performer
As with OSC you can easily sync Rackspace or Song Part Switches.

True. I researched that and it just felt a little risky, since it’s a full 2-way sync. So if I ever press “Play” in the MIDI instance by accident or because I have the wrong window focused, whatever tempo the last played MIDI file was played at will now sync to the backing tracks instance, it will start playing, and all kinds of hell break loose. It would be user error on my part, but I don’t like the risk.

I might revisit that as a potential plan B though, if I see the 128 sample buffer size being too small for stability.

What audio interface are you using?

For the tracks instance, a 4i4 Gen 3 live and an Apollo Solo at home (when practising or making adjustments)

Did you try using the “old” Audio File Player?

Will that even work for my setup? I have dedicated file players for each stem type (synths, click, sound effects, etc), and then I load stems for each song into each player.

Even if I reduced the number of stems down to 8 (currently 11 I believe), is it actually possible to remove all the currently loaded files and load a whole new set of files into the player when switching songs?

I assume it would need to be done with song part actions or scripting, but I haven’t looked into that player at all. Does it have better glitch stability than the streaming player based on the buffer size?

The “old” Audio File Player uses 8 tracks.
The Streaming audio file player only 1.

Not sure what you mean. The legacy player can play 8 tracks at once, or 8 tracks individually. We have more than 8 songs in our setlist, and each song has a bunch of stems, so I’m not sure what your suggested use case is (I don’t mean to sound snarky, I just need clarification :slight_smile: )

The streaming player can have a list of up to 1000 files, so I can load stems for up to 1000 songs with one set of streaming players (one player per stem category).

You could load the needed audio file players in each rackspaces

Ah I see what you’re saying. I only use one rackspace (with variations for each song, so that each song can store its own mix), so this would be a major restructuring of the entire setup.

If I move to separate rackspaces for each song, I would either have to duplicate the entire mixer widget setup, or move all those widgets into the global rackspace and somehow have them recall their position for every song.

Definitely some welcome suggestions for workarounds here though; things to research and test if I gets to the point where I can’t use the current setup.

But I’m still curious about that MIDI player and how it works in relation to buffer size, and if that’s something that could be addressed at some point.