I think it is possible to state my problem without any reference to parsing and all the other weird things I’m trying to do, which is clearly causing confusion.
Suppose I am making a regular daw, where the user can schedule notes by clicking on a clip. Now suppose that the user cancels a note very close to the line, or in fact after the noteOn
message has been sent (but before the noteOff
has been sent). The note is now stuck on.
Below there is a very crude working example of this problem. In the example, pressing the ‘a’ key schedules a note 1 second in the future, and pressing the ‘b’ key removes it. If you press ‘b’ after the note has started, the note is stuck on.
Of course, you can easily use injectLiveMidiMessage()
to kill the orphaned note, but this isn’t a general solution because you don’t want to kill it if it hasn’t been played yet. So you need to keep track of which notes have actually been played, which is harder than it sounds. (It’s especially hard in my case, since some of the noteOns
have to be injected, since the scheduler will miss them for similar reasons).
static String organPatch = "<PLUGIN type=\"4osc\" windowLocked=\"1\" id=\"1069\" enabled=\"1\" filterType=\"1\" presetDirty=\"0\" presetName=\"4OSC: Organ\" filterFreq=\"127.00000000000000000000\" ampAttack=\"0.06000002384185791016\" ampDecay=\"10.00000000000000000000\" ampSustain=\"40.00000000000000000000\" ampRelease=\"0.40000000596046447754\" waveShape1=\"2\" tune2=\"-24.00000000000000000000\" waveShape2=\"1\"> <MACROPARAMETERS id=\"1069\"/> <MODIFIERASSIGNMENTS/> <MODMATRIX/> </PLUGIN>";
class MyMidiSequencer : public Component
{
public:
MyMidiSequencer() :
engine("MyMidiSequencer"),
edit(engine, tracktion_engine::createEmptyEdit(), tracktion_engine::Edit::EditRole::forEditing, nullptr, 0),
transport(edit.getTransport()),
midiClip(getOrCreateMidiClip())
{
setSize(200, 300);
setWantsKeyboardFocus(true);
const auto newPlugin = edit.getPluginCache().createNewPlugin(tracktion_engine::FourOscPlugin::xmlTypeName, {});
tracktion_engine::getAudioTracks(edit)[0]->pluginList.insertPlugin(newPlugin, 0, nullptr);
XmlDocument doc(organPatch);
if (auto e = doc.getDocumentElement())
{
auto vt = ValueTree::fromXml(*e);
if (vt.isValid())
newPlugin->restorePluginStateFromValueTree(vt);
}
transport.setLoopRange(tracktion_engine::Edit::getMaximumEditTimeRange() - 1);
transport.looping = true;
transport.position = 0.0;
transport.play(true);
}
private:
bool keyPressed(const KeyPress& key) override
{
if (key.getTextCharacter() == 'a')
lastScheduledNote = scheduleNewNote();
else if (key.getTextCharacter() == 'b')
removeLastScheduledNote();
return true;
}
tracktion_engine::MidiNote* scheduleNewNote()
{
const double time = 0.1 + transport.getCurrentPosition();
return midiClip.getSequence().addNote(45, edit.tempoSequence.timeToBeats(time), 1.0, 127, 127, nullptr);
}
void removeLastScheduledNote()
{
if (lastScheduledNote)
{
midiClip.getSequence().removeNote(*lastScheduledNote, nullptr);
}
}
tracktion_engine::MidiClip& getOrCreateMidiClip()
{
edit.ensureNumberOfAudioTracks(1);
auto firstTrack = tracktion_engine::getAudioTracks(edit)[0];
if (dynamic_cast<tracktion_engine::MidiClip*> (firstTrack->getClips()[0]) == nullptr)
firstTrack->insertNewClip(tracktion_engine::TrackItem::Type::midi, {0.0, 10000.0}, nullptr);
return *static_cast<tracktion_engine::MidiClip*> (firstTrack->getClips()[0]);
}
tracktion_engine::Engine engine;
tracktion_engine::Edit edit;
tracktion_engine::TransportControl& transport;
tracktion_engine::MidiClip& midiClip;
tracktion_engine::MidiNote* lastScheduledNote = nullptr;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MyMidiSequencer)
};