Thanks @cpr2323.
After some investigation I have identified a bug in JUCE with respect to sending blocks of messages.
I first re-produced the issue in a fresh test project (see attached file), for which the relevant code is below…
MainComponent.h:
//==============================================================================
class MainComponent : public juce::Component
{
public:
//==============================================================================
MainComponent();
~MainComponent() override;
//==============================================================================
void resized() override;
private:
//==============================================================================
void sendMidiMessages();
TextButton midiButton;
std::unique_ptr<MidiOutput> myMidiOutput;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainComponent)
};
MainComponent.cpp:
#include "MainComponent.h"
//==============================================================================
MainComponent::MainComponent()
{
myMidiOutput = MidiOutput::createNewDevice ("My MIDI Out");
if (myMidiOutput)
myMidiOutput->startBackgroundThread();
midiButton.setButtonText ("Send Messages");
addAndMakeVisible (midiButton);
midiButton.onClick = [this]()
{
sendMidiMessages();
};
setSize (600, 400);
}
//==============================================================================
MainComponent::~MainComponent()
{
if (myMidiOutput != nullptr)
{
myMidiOutput->stopBackgroundThread();
myMidiOutput.reset();
}
}
//==============================================================================
void MainComponent::sendMidiMessages()
{
constexpr int channel = 1;
constexpr int noteNumber = 60;
constexpr int velocity = 100;
constexpr int lengthInMs = 1000;
MidiBuffer m;
m.addEvent (MidiMessage::noteOn (channel, noteNumber, velocity / 127.f), 0);
m.addEvent (MidiMessage::noteOff (channel, noteNumber), lengthInMs);
myMidiOutput->sendBlockOfMessages (m, Time::getMillisecondCounter(), 1000.0);
}
//==============================================================================
void MainComponent::resized()
{
Rectangle<int> r (getLocalBounds());
midiButton.setBounds (r.withSizeKeepingCentre (150, 20));
}
Bug Details and Suggested Fix
As mentioned in the first post - sending messages with time offsets means only the first message gets sent (use Protokol or MIDI Monitor to verify this).
In the class ScheduledEventThread in JUCE there is a loop dispatching events in the run() function.
The relevant code is below:
const auto now = Time::getMillisecondCounter();
const auto event = *pendingMessages.begin();
pendingMessages.erase (pendingMessages.begin());
const auto timestamp = event.getTimeStamp();
if (timestamp > now + 20)
{
const auto millis = static_cast<int64_t> (timestamp - (now + 20));
condvar.wait_for (lock, std::chrono::milliseconds (millis));
continue;
}
if (timestamp > now)
Time::waitForMillisecondCounter ((uint32) timestamp);
if (timestamp > now - 200)
outputCallback (event);
Note in the above that the statement if (timestamp > now + 20) causes the code to continue if the timestamp is more than 20ms in the future.
However - the line pendingMessages.erase (pendingMessages.begin()); has already happened so messages later than 20ms are discarded.
I think that the fix is just to move the line:
pendingMessages.erase (pendingMessages.begin());
to after the if (timestamp > now + 20) statement has finished, that way we are only erasing elements we consume (if i understand the code correctly). This fixed the issue for me in my test project.
If it was possible to get a fix for this soon that would be great as it is an issue in a live product,
MidiMessageIssue.zip (5.6 KB)
Thanks,
Adam