I got MIDI Thru working! No MIDI sequencer is complete without it.
The input side was easy, I pretty much ripped it straight out of
ChordEase.
The output side was tricky however, because you can't write events
directly to a MIDI device that's been opened for synchronized streaming
(as opposed to immediate output, as in ChordEase). Instead your callback
function (which is being called at regular intervals by Windows) has to
add the events to its next buffer, which it subsequently queues to the
output device. My solution was to add a special input queue for "live"
events. At the start of each callback, I check the live input queue, and
if there are events in it, I dequeue them and add them to the start of
my next MIDI buffer, before any events that may get added from the
song's tracks. This is quite safe (provided a thread-safe queue is used)
and it works pretty well. It does have some limitations though.
-
MIDI thru is only operational while the sequencer is playing, not
while it's stopped or paused. This is potentially annoying, and the only
solution is to keep the MIDI output device open all the time, not only
during playback. Doing so would have the additional benefit of reducing
the lag between pressing "Play" and playback actually starting. Most of
that lag is caused by opening the device, though some devices are worse
than others. It's a cool idea and it's on the list, but I'm not going to
deal with it right away.
-
The delay can be longer than one might like, depending on how the
sequencer's latency is set. At the default latency of 10ms, it's
probably fine for controlling parameters, but Chopin is out of the
question. The sequencer's latency can be set lower, all the way down to
1ms, but the lower it is, the greater the risk of timing glitches in
song playback, due to the callback taking too long and not being able to
keep up. This also depends on other factors such as tempo and the
density of the sequence.
-
The delay isn't constant. This is because I grab all the live input
events that have occurred since the last callback and write them all to
the start of the buffer. They probably didn't all happen at the same
time, but they're all output at the same time. In other words, the live
input can get "bunched up" into discrete packets.
Whether "bunching" is a problem depends on how fast and dense the
input is. If the input is sparse, there might only be one input event
per callback. But if there are multiple events per callback, the
callback could try to approximate their original timing. The events have
timestamps, which can presumably be used to space out the events in
time. Note however that doing so makes the events even LATER than they
already are. A nasty trade-off! This is a hard problem and I intend to
ignore it for now. Possible bunching aside, the current scheme is
straightforward and minimizes delay.
The live input queue also makes it possible to do live patch changes,
along with volume and panning. This is already implemented in the
Channels bar. The addition of a MIDI input device is a prerequisite to
other useful things, such as MIDI mapping of all parameters, which is
also on the list.