In quest for ´perfect´ MIDI timing, I came with a solution which works for me. I’ll let you know and criticize it, since I’m still struggling with the subtleties of both C++ and JUCE. I wrote and tested a few months ago and did not review later.
I created a MIDI_Interface class, initially to emulate Kontakt’s callback way of handling MIDI and ended up with a scheduler which is sample accurate.
´MIDI_Interface.h´
[CODE] #pragma once
class MIDI_Interface
{
public:
MIDI_Interface();
virtual ~MIDI_Interface();
/// Multimap of MidiMessages to be scheduled
typedef multimap<uint64 , MidiMessage> schMapType;
schMapType schMidiMsg;
/// Indicates the type of Message: 0 - input (DAWI_INPUT) event, 1 - output (PLAY_MIDI_OUTPUT) event
enum eMESSAGE_TYPE
{ DAW_INPUT , PLAY_NOTE_OUTPUT , SAMPLE_LOOP };
/// Number of samples played since playback start (prepareToPlay() )
/// no need to reset later, 8 bytes unsigned long long reaches 18,446,744,073,709,551,615 (enough play time!)
uint64 sampleCounter;
/// Reset in prepareToPlay, incs every new processblock()
uint64 bufferCounter;
/// Used to count MidiMessags, context dependent
int midiMsgCounter;
/// Passed from processorBlock()
AudioPlayHead::CurrentPositionInfo PB_CurrentPosInfo;
/// This is the MidiBuffer for manipulation. Every edition is made within this block,
/// then exchanged with original ´midiMessages´ before sending the buffer to audio device (end of processorBlock() method )
MidiBuffer MI_MidiBuffer;
/// Time stamp for MidiMessages described in MI_MidiMessage (in samples within processorBlock)
int MI_BufferPos; // MidiMessage timestamp is ´int´
/// MidiMessages handled from MI_MidiBuffer. Not necessarily modified. Optional: contextual use.
MidiMessage MI_MidiMessage;
/// This is the integrator of the AudioProcessor processorBlock() to our MIDI_Interface
void MI_ProcessBlock(const MidiBuffer& midiMessages, const AudioPlayHead::CurrentPositionInfo& CurrentPosInto, const int& blockSize);
/// This will play the note (offset in samples, have to build the offset handler)
/// offset defaults to 0, msgType defaults to DAW_INPUT
/// For the sake of simplicity, it'll fill schMidiMsg (since it's checked for every sample)
void playMIDI(const MidiMessage&, const int& timestamp, const int& offset = 0, const int& msgType = DAW_INPUT);
/// Callback emulation. When a MidiMessage is iterated in the original MidiBuffer, it is sent to this
void ON_MIDI(MidiMessage&, const int& timestamp, const int& msgType);
private:
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR( MIDI_Interface )
};
[/CODE]
´MIDI_interface.cpp´
[CODE] /*
Every MIDI Mesasge will be sent to the scheduler (schMidiMsg).
Notes passing through (unprocessed) will be scheduled with 0 offset
The notes are gonna be added to Midi Buffer in the ´Sample Loop´
*/
#include "MIDI_Interface.h"
MIDI_Interface::MIDI_Interface() {}
MIDI_Interface::~MIDI_Interface() {}
void MIDI_Interface::MI_ProcessBlock(const MidiBuffer& PB_MidiBuffer, const AudioPlayHead::CurrentPositionInfo& MI_CurrentPosInto, const int& blockSize)
{
MI_MidiBuffer.clear(); // creates an empty MidiBuffer that will be filled by
if (PB_MidiBuffer.isEmpty() == false) // Any midi messages for this block?
{
/* --- MIDI BUFFER LOOP --- */
for (MidiBuffer::Iterator mbIterator(PB_MidiBuffer); mbIterator.getNextEvent(MI_MidiMessage, MI_BufferPos);)
{
// Callback emulation, every MIDI message will be sent to ON_MIDI (callback emulation)
ON_MIDI(MI_MidiMessage, MI_BufferPos, DAW_INPUT);
}
/* --- End of MIDI BUFFER LOOP --- */
}
/* --- SAMPLE LOOP --- */
for (int bufferPos = 0; bufferPos < blockSize; ++bufferPos)
{
if (schMidiMsg.find(sampleCounter) != schMidiMsg.end()) // If there's some MidiMessage scheduled to actual sampleCounter
{
schMapType::const_iterator itLower = schMidiMsg.lower_bound(sampleCounter);
schMapType::const_iterator itUpper = schMidiMsg.upper_bound(sampleCounter);
for (auto& it = itLower; it != itUpper; ++it)
{
MI_MidiBuffer.addEvent(it->second, bufferPos);
}
schMidiMsg.erase(sampleCounter); // all events for this sample are removed
}
++sampleCounter; // now increment Samples Counter
};
/* --- End of SAMPLE ACCURATE LOOP --- */
}
void MIDI_Interface::ON_MIDI( MidiMessage& midiMessage , const int& timestamp , const int& msgType )
{
playMIDI( midiMessage , timestamp , 0 );
}
void MIDI_Interface::playMIDI(const MidiMessage& midiMessage, const int& timeStamp, const int& offset, const int& msgType)
{
// ´offset´ default = 0. If offset < 0 (illegal), it's ignored
if (offset >= 0)
{
schMidiMsg.emplace(sampleCounter + offset, midiMessage);
}
}
[/CODE]
Then I inherit my AudioProcessor from MIDI_Interface:
and add this code:
void MIDI_Int_AudioProcessor::prepareToPlay(double sampleRate, int samplesPerBlock)
{
// Initializes counters and schMidiMsg (MidiMessage Scheduler Multimap)
bufferCounter = 0;
sampleCounter = 0;
midiMsgCounter = 0;
schMidiMsg.clear();
}
void MIDI_Int_AudioProcessor::releaseResources()
{
midiMsgCounter = 0;
schMidiMsg.clear(); // clear all scheduled events
}
void MIDI_Int_AudioProcessor::processBlock(AudioSampleBuffer& PB_AudioBuffer, MidiBuffer& PB_MidiBuffer)
{
PB_AudioBuffer.clear();
++bufferCounter;
getPlayHead()->getCurrentPosition(PB_CurrentPosInfo);
MI_ProcessBlock(PB_MidiBuffer, PB_CurrentPosInfo, PB_AudioBuffer.getNumSamples());
// Replaces received MidiBuffer with edited MI_MidiBuffer
PB_MidiBuffer.swapWith(MI_MidiBuffer);
}
Since I don’t process audio at all, this method works with zero overhead for me and you can use playMIDI with any uint64 offset, it’ll be sample accurate, no threads and timers involved for maximum accuracy. I had to keep my eyes a few months away from programming, I don’t know I would do it like this right now, but since it worked well for my initial tests, I’m restarting from here. This is probably not the most elegant solution, and I can’t predict how it would work combined with audio processing.