Hi,
it took me a while to get my Midiclock class working perfectly but finally here
is my “ready to use” class to inject Midiclock sync messages into a given
MidiBuffer. The most of the time was used to optimize timing to let the class
deal with position jumps e.g “loops” a perfect way. It was also quite difficult
(for me) to find the best way of injecting events to have different Midiclock
slaves behaving the same way. I’ve tested with Cubase 6.5, Ableton Live,
Reaper and Native Instruments Reaktor. All of them stay perfectly in sync
using my class.
The class is very easy to use, just make it a member of your Audioprocessor class
and feed its generateMidiclock method with some values which you can
extract from LastTimeInfo struct.
To let the class deal with loops I had to hack the juce_VST_Wrapper.cpp as well as
juce_AudioPlayHead.h as shown below. Hopefully Jules can check this in shortly.
To minimize jitter I use a high priority thread to pass the MidiBuffer containing the events
to MidiOutput::sendBlockOfMessages(). There were several discussions about that here
in the forum. MidiOutput::sendBlockOfMessages() allocates memory on the heap
that introduces a global lock and we should never block the audio callback!!!
Using a lockless FIFO might be a better approach…
There are some parameters to tweak the class. One important is “setOffset()”. This
parameter allows you to compensate the latency of the MIdiOuput device…
Feel free to use the class at your own risk! It would be nice to hearing some feedback and
hopefully the class will make its way into the juce framework even if Jules will completely re-write it
This is the juce_VST_Wrapper hack:
bool getCurrentPosition (AudioPlayHead::CurrentPositionInfo& info)
{
const VstTimeInfo* const ti = getTimeInfo (kVstPpqPosValid | kVstTempoValid | kVstBarsValid | kVstCyclePosValid
| kVstTimeSigValid | kVstSmpteValid | kVstClockValid);
if (ti == nullptr || ti->sampleRate <= 0)
return false;
info.bpm = (ti->flags & kVstTempoValid) != 0 ? ti->tempo : 0.0;
if ((ti->flags & kVstTimeSigValid) != 0)
{
info.timeSigNumerator = ti->timeSigNumerator;
info.timeSigDenominator = ti->timeSigDenominator;
}
else
{
info.timeSigNumerator = 4;
info.timeSigDenominator = 4;
}
info.timeInSeconds = ti->samplePos / ti->sampleRate;
info.ppqPosition = (ti->flags & kVstPpqPosValid) != 0 ? ti->ppqPos : 0.0;
info.ppqPositionOfLastBarStart = (ti->flags & kVstBarsValid) != 0 ? ti->barStartPos : 0.0;
if ((ti->flags & kVstSmpteValid) != 0)
{
AudioPlayHead::FrameRateType rate = AudioPlayHead::fpsUnknown;
double fps = 1.0;
switch (ti->smpteFrameRate)
{
case kVstSmpte24fps: rate = AudioPlayHead::fps24; fps = 24.0; break;
case kVstSmpte25fps: rate = AudioPlayHead::fps25; fps = 25.0; break;
case kVstSmpte2997fps: rate = AudioPlayHead::fps2997; fps = 29.97; break;
case kVstSmpte30fps: rate = AudioPlayHead::fps30; fps = 30.0; break;
case kVstSmpte2997dfps: rate = AudioPlayHead::fps2997drop; fps = 29.97; break;
case kVstSmpte30dfps: rate = AudioPlayHead::fps30drop; fps = 30.0; break;
case kVstSmpteFilm16mm:
case kVstSmpteFilm35mm: fps = 24.0; break;
case kVstSmpte239fps: fps = 23.976; break;
case kVstSmpte249fps: fps = 24.976; break;
case kVstSmpte599fps: fps = 59.94; break;
case kVstSmpte60fps: fps = 60; break;
default: jassertfalse; // unknown frame-rate..
}
info.frameRate = rate;
info.editOriginTime = ti->smpteOffset / (80.0 * fps);
}
else
{
info.frameRate = AudioPlayHead::fpsUnknown;
info.editOriginTime = 0;
}
info.isRecording = (ti->flags & kVstTransportRecording) != 0;
info.isPlaying = (ti->flags & kVstTransportPlaying) != 0 || info.isRecording;
if ((ti->flags & kVstCyclePosValid) != 0)
{
info.cycleStartPos = ti->cycleStartPos;
info.cycleEndPos = ti->cycleEndPos;
}
info.isTransportCycleActive = (ti->flags & kVstTransportCycleActive) != 0
return true;
}
and in AudioPlayhead.h…
struct JUCE_API CurrentPositionInfo
{
/** The tempo in BPM */
double bpm;
/** Time signature numerator, e.g. the 3 of a 3/4 time sig */
int timeSigNumerator;
/** Time signature denominator, e.g. the 4 of a 3/4 time sig */
int timeSigDenominator;
/** The current play position, in seconds from the start of the edit. */
double timeInSeconds;
/** For timecode, the position of the start of the edit, in seconds from 00:00:00:00. */
double editOriginTime;
/** The current play position in pulses-per-quarter-note.
This is the number of quarter notes since the edit start.
*/
double ppqPosition;
/** The position of the start of the last bar, in pulses-per-quarter-note.
This is the number of quarter notes from the start of the edit to the
start of the current bar.
Note - this value may be unavailable on some hosts, e.g. Pro-Tools. If
it's not available, the value will be 0.
*/
double ppqPositionOfLastBarStart;
/** The video frame rate, if applicable. */
FrameRateType frameRate;
/** True if the transport is currently playing. */
bool isPlaying;
/** True if the transport is currently recording.
(When isRecording is true, then isPlaying will also be true).
*/
bool isRecording;
/** The current cycle start position in pulses-per-quarter-note. */
double cycleStartPos;
/** The current cycle end position in pulses-per-quarter-note. */
double cycleEndPos;
/** True if the transport cycle is currently active. */
bool isTransportCycleActive;
//==============================================================================
bool operator== (const CurrentPositionInfo& other) const noexcept;
bool operator!= (const CurrentPositionInfo& other) const noexcept;
void resetToDefault();
};
Hope this class is usefull to somebody.
Best,
Joerg