Adding / removing MidiNotes from a playing MidiSequence in a MidiClip without restarting playback

I also tried adding regular UI buttons to trigger adding of notes / start / stop and the same issue is happening. All my code is running on the message thread, btw. Perhaps there’s a different way to do what I’m trying to do or something else I’m missing?

Do you have some small example code? Like a modification to the Step Sequencer example that uses a MIDI clip rather than a step clip? I think I’m going to have to debug this to see what’s going on.

@dave96 I’ve stripped out everything but the 3 UI buttons (add note, play, stop) and uploaded the project here: minimal-example.zip - Google Drive

It’s even more minimal than the included examples. The add note button will add a note starting at timestamp 0, every 16th note (if I calculated correctly). It should be like a drum roll that loops endlessly except you’ll see if you add a note while others are playing they stop playing till the next measure/loop point (though the metronome doesn’t stop).

I hate to say it but there’s a fair bit wrong with that example, it’s a bit difficult for me to figure out if there is any actual issue with the engine or not…
For a start it won’t close correctly as you’re taking std::shared_ptrs to Tracks that are managed by the Edit.

But the main problem is there’s no audible output. I presume it’s trying to send to a MIDI output device but it’s doing it by name (“Deluge OUT”). I don’t have that device and without some UI to allow me to set a MIDI output device I can’t really debug it.

Perhaps you should start with a soft-synth like the internal Sampler or 4OSC so you can rule out external MIDI devices as a potential problem?

You can use any midi output, just change the name to something available on your system. On a mac you can use the IAC midi driver though I’m not sure what would be the equivalent virtual MIDI on win/linux:

                track->getOutput().setOutputByName("IAC Driver Bus 1");

Anyway, I’ve tried it with multiple synths, both software and hardware as well as the midi monitor so it’s not the specific midi instrument that’s having the issue.

I took your suggestion and rewrote the createMidiOut() function to use a 4xosc instead of MIDI and there is no issue with the audio! This is the new function:

        void createMidiOut()
        {
            track = getOrInsertAudioTrackAt(*edit, 0);
            if (track)
            {
                if (auto synth = dynamic_cast<te::FourOscPlugin*> (edit->getPluginCache().createNewPlugin (te::FourOscPlugin::xmlTypeName, {}).get()))
                {
                    static juce::String organPatch = R"(<PLUGIN type="4osc" windowLocked="1" id="1069" enabled="1" filterType="1" presetDirty="0" presetName="4OSC: Organ" filterFreq="127.00000000000000000000" ampAttack="0.60000002384185791016" ampDecay="10.00000000000000000000" ampSustain="100.00000000000000000000" ampRelease="0.40000000596046447754" waveShape1="4" tune2="-24.00000000000000000000" waveShape2="4"> <MACROPARAMETERS id="1069"/> <MODIFIERASSIGNMENTS/> <MODMATRIX/> </PLUGIN>)";

                    if (auto e = parseXML (organPatch))
                    {
                        auto vt = juce::ValueTree::fromXml (*e);

                        if (vt.isValid())
                            synth->restorePluginStateFromValueTree (vt);
                    }

                    track->pluginList.insertPlugin (*synth, 0, nullptr);
                }

                const te::EditTimeRange editTimeRange(0, edit->tempoSequence.barsBeatsToTime({ 1, 0.0 }));
                if (auto clip = getClip())
                {
                    track->getClips()[0]->removeFromParentTrack();
                }

                track->insertNewClip(te::TrackItem::Type::midi, "MIDI Clip", editTimeRange, nullptr);

                midiClip = getClip();
            }
        }

So it happens only in MIDI, not in audio. I tried to add the midi output at the same time as the audio, but I don’t know if it’s supposed to work like that and could not get both going at the same time to compare. Any ideas on why the MIDI output might work this way but the audio not?

Also, I definitely appreciate your insight on any other issues (I’m sure there are many) you may see like the shared pointer issue as I’m rather new to audio programming and modern C++ so everything helps.

So I tried adding one 4xOSC track and one MIDI only track and can confirm that when modifying the sequence by adding a note, only the MIDI track pauses. The 4xOSC continues playing along w/ the metronome. Here’s the updated class that demos this:

#pragma once

#include <utility>

#include "Views/Clip/MainView.h"
#include "Views/BaseView.h"

namespace phenotype
{
    namespace te = tracktion_engine;

    struct Note
    {
        int pitch;
        double beats;
        float length;
        int velocity;
    };

    static bool operator<(const Note& t1, const Note& t2)
    {
        return (t1.beats < t2.beats);
    }

    class Track
    {
    public:
        explicit Track(std::shared_ptr<te::Edit> edit)
                :edit(std::move(edit))
        {
            createAudioOut();
            createMidiOut();
        }

        ~Track() = default;

        static te::AudioTrack* getOrInsertAudioTrackAt(te::Edit& edit, int index)
        {
            edit.ensureNumberOfAudioTracks(index + 1);
            return te::getAudioTracks(edit)[index];
        }

        te::MidiClip::Ptr getClip()
        {
            if (auto clip = dynamic_cast<te::MidiClip*> (track->getClips()[0]))
            {
                return *clip;
            }

            return {};
        }

        te::MidiClip::Ptr getClip2()
        {
            if (auto clip = dynamic_cast<te::MidiClip*> (track2->getClips()[0]))
            {
                return *clip;
            }

            return {};
        }

        void createMidiOut()
        {
            track2 = getOrInsertAudioTrackAt(*edit, 1);
            if (track2)
            {
                // Set MIDI out device here:
                track2->getOutput().setOutputByName("Deluge OUT");
//                track->getOutput().setOutputByName("IAC Driver Bus 1");

                const te::EditTimeRange editTimeRange(0, edit->tempoSequence.barsBeatsToTime({ 1, 0.0 }));
                if (auto clip = getClip2())
                {
                    track2->getClips()[0]->removeFromParentTrack();
                }

                track2->insertNewClip(te::TrackItem::Type::midi, "MIDI Clip 2", editTimeRange, nullptr);

                midiClip2 = getClip2();
            }
        }

        void createAudioOut()
        {
            track = getOrInsertAudioTrackAt(*edit, 0);
            if (track)
            {
                if (auto synth = dynamic_cast<te::FourOscPlugin*> (edit->getPluginCache()
                        .createNewPlugin(te::FourOscPlugin::xmlTypeName, {}).get()))
                {
                    static juce::String organPatch = R"(<PLUGIN type="4osc" windowLocked="1" id="1069" enabled="1" filterType="1" presetDirty="0" presetName="4OSC: Organ" filterFreq="127.00000000000000000000" ampAttack="0.60000002384185791016" ampDecay="10.00000000000000000000" ampSustain="100.00000000000000000000" ampRelease="0.40000000596046447754" waveShape1="4" tune2="-24.00000000000000000000" waveShape2="4"> <MACROPARAMETERS id="1069"/> <MODIFIERASSIGNMENTS/> <MODMATRIX/> </PLUGIN>)";

                    if (auto e = parseXML(organPatch))
                    {
                        auto vt = juce::ValueTree::fromXml(*e);

                        if (vt.isValid())
                            synth->restorePluginStateFromValueTree(vt);
                    }

                    track->pluginList.insertPlugin(*synth, 0, nullptr);
                }

                const te::EditTimeRange editTimeRange(0, edit->tempoSequence.barsBeatsToTime({ 1, 0.0 }));
                if (auto clip = getClip())
                {
                    track->getClips()[0]->removeFromParentTrack();
                }

                track->insertNewClip(te::TrackItem::Type::midi, "MIDI Clip", editTimeRange, nullptr);

                midiClip = getClip();
            }
        }

        void toggleNote(int col, int row)
        {
            double beats = edit->tempoSequence.timeToBeats(col * .125);
            Note note = {
                    75 + row,
                    beats,
                    .125,
                    127
            };

            auto& seq = midiClip->getSequence();
            auto& seq2 = midiClip2->getSequence();

            if (notes.find(note) == notes.end())
            {
                // could just modify the sequence here and get rid of renderMidi()
                seq.addNote(note.pitch, note.beats, note.length, note.velocity, 0, nullptr);
                seq2.addNote(note.pitch, note.beats, note.length, note.velocity, 0, nullptr);
            }
        }

    protected:
        std::shared_ptr<te::Edit> edit;
        std::map<Note, Note> notes;
        //std::map<Note, te::MidiNote*> notes; // alt structure for direct manipulation
        te::MidiClip::Ptr midiClip;
        te::MidiClip::Ptr midiClip2;
        te::AudioTrack* track{};
        te::AudioTrack* track2{};
    };
}

What is playing your MIDI out though? It sounds like whatever synth you have plugged in there is just pausing for some reason?

Have you monitored the actual MIDI in something like “MIDI Monitor”?

I’ve tried both hardware and software synths and a midi monitor. It’s clear from the midi monitor that the all notes off / reset all controllers fires and then there are no more messages. If I keep adding notes to the sequence, the midi does not resume until after I stop pressing the button and the sequence is no longer being modified.

Ok, well that narrows it down a bit, it sounds like it’s somewhere in the MIDI output then.

Can you log the messages that get sent in MidiOutputDevice::sendMessageNow, does that send all the note on/offs or does that pause between bars too?

If that’s not sending the correct messages, are they getting passed to MidiOutputDeviceInstanceInjectingNode::process correctly (as the sourceBuffers.midi)?

MidiOutputDeviceInstanceInjectingNode::process has all the correct messages even during the pause.
MidiOutputDevice::sendMessageNow doesn’t have any messages during the pause.

When adding new notes to the sequence with the button, somewhere in between there, they seem to be getting lost.

They also don’t seem to make it to MidiNoteDispatcher::hiResTimerCallback

Ok, does adding this line
midiDispatcher.masterTimeUpdate (editTime); in EditPlaybackContext::fillNextNodeBlock:
here:

    const double editTime = tracktion_graph::sampleToTime (nodePlaybackContext->playHead.getPosition(), nodePlaybackContext->getSampleRate());
    edit.updateModifierTimers (editTime, numSamples);
    midiDispatcher.masterTimeUpdate (editTime);    //<<< Add this line

Fix your problem?
If so, I’ll push that to develop

Yes, this fixes the pause and almost the entire problem! Can you explain what the bug is a little bit so I can try to understand? This is really amazing and exciting!

The only minor issue that remains after adding this line is that the “all notes off”/“reset all controllers” still happens when modifying the sequence, so a part of a playing note might be cut off while it’s playing, especially if adding many notes quickly in succession. I wonder if that has a cause that can be fixed also.

It was an issue that the current time wasn’t getting updated so messages were being queued up forever.

I’ll have to have another look at why the all-notes-off message is being sent. I have a feeling its because the playhead is registering as having jumped when it doesn’t.

I see. That makes sense. Any particular functions, classes, or areas of the code I could look into for the playhead issue to help debug it? Would love to have a fix for that so I can work more on other parts of the app.

I presume they’re getting added by MidiOutputDevice::sendNoteOffMessages()?

If so, what is the callstack if you put a breakpoint in there? If it’s in MidiNoteDispatcher::hiResTimerCallback, in the if (buffer.isAllNotesOff), we’ll have to see where it’s getting set to true. I thought the only place that happened was in PluginNode though.
Does it still happen if you remove all the plugins? (including vol/pan and level meter?)

It seems to be coming from the Edit::timerCallback() function. The MIDI device is stopped and restarted, I think from looking through the calls. If I comment out the stop() call on line 647 of MidiOutputDeviceInstance::prepareToPlay, then it no longer happens, but I’m not sure of the repercussions of doing that overall as it seems to sometimes lead to hung notes and such though the playing notes aren’t getting cut off anymore. Not sure if that is a proper fix or what is. But I have tried replacing that stop() call with an identical function that calls a similar function to sendNoteOffMessages() that does everything sendNoteOffMessages does except send the all notes off/all controllers off but that didn’t help much. Finally, I replaced it with a function that simply does the below and it seems ok but with some sticky notes:

    if (playing)
    {
        playing = false;
    }

So probably there is a better solution for this. Here is the sendNoteOffMessages stacktrace:

tracktion_engine::MidiOutputDevice::sendNoteOffMessages() tracktion_MidiOutputDevice.cpp:502
tracktion_engine::MidiOutputDeviceInstance::stop() tracktion_MidiOutputDevice.cpp:676
tracktion_engine::MidiOutputDeviceInstance::prepareToPlay(double, bool) tracktion_MidiOutputDevice.cpp:647
tracktion_engine::EditPlaybackContext::prepareOutputDevices(double) tracktion_EditPlaybackContext.cpp:584
tracktion_engine::EditPlaybackContext::startPlaying(double) tracktion_EditPlaybackContext.cpp:518
tracktion_engine::EditPlaybackContext::createPlayAudioNodes(double) tracktion_EditPlaybackContext.cpp:502
tracktion_engine::TransportControl::ensureContextAllocated(bool) tracktion_TransportControl.cpp:731
tracktion_engine::TransportControl::editHasChanged() tracktion_TransportControl.cpp:684
tracktion_engine::Edit::timerCallback() tracktion_Edit.cpp:1604
juce::Timer::TimerThread::callTimers() juce_Timer.cpp:114
juce::Timer::TimerThread::CallTimersMessage::messageCallback() juce_Timer.cpp:180
juce::MessageQueue::deliverNextMessage() juce_osx_MessageQueue.h:81
juce::MessageQueue::runLoopCallback() juce_osx_MessageQueue.h:92
juce::MessageQueue::runLoopSourceCallback(void*) juce_osx_MessageQueue.h:100
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ 0x00007fff2064b37c
__CFRunLoopDoSource0 0x00007fff2064b2e4
__CFRunLoopDoSources0 0x00007fff2064b064
__CFRunLoopRun 0x00007fff20649a8c
CFRunLoopRunSpecific 0x00007fff2064904c
RunCurrentEventLoopInMode 0x00007fff28891a83
ReceiveNextEventCommon 0x00007fff288917e5
_BlockUntilNextEventMatchingListInModeWithFilter 0x00007fff28891583
_DPSNextEvent 0x00007fff22e51d72
-[NSApplication(NSEvent) _nextEventMatchingEventMask:untilDate:inMode:dequeue:] 0x00007fff22e50545
-[NSApplication run] 0x00007fff22e42869
juce::MessageManager::runDispatchLoop() juce_mac_MessageManager.mm:359
juce::JUCEApplicationBase::main() juce_ApplicationBase.cpp:262
juce::JUCEApplicationBase::main(int, char const**) juce_ApplicationBase.cpp:240
main Main.cpp:109
start 0x00007fff2056ef3d

It seems that modifying the MIDI clip is what triggers the stop/restart and the all notes off/reset all controllers. It seems that Edit::restartPlayback() is being called which sets shouldRestartPlayback = true. The callstack for Edit::restartPlayback() seems to lead back to MidiList::copyFrom:

tracktion_engine::Edit::restartPlayback() tracktion_Edit.cpp:1055
tracktion_engine::Edit::TreeWatcher::restart() tracktion_Edit.cpp:353
tracktion_engine::Edit::TreeWatcher::childAddedOrRemoved(juce::ValueTree&, juce::ValueTree&) tracktion_Edit.cpp:305
tracktion_engine::Edit::TreeWatcher::valueTreeChildRemoved(juce::ValueTree&, juce::ValueTree&, int) tracktion_Edit.cpp:278
juce::ValueTree::SharedObject::sendChildRemovedMessage(juce::ValueTree, int)::'lambda'(juce::ValueTree::Listener&)::operator()(juce::ValueTree::Listener&) const juce_ValueTree.cpp:112
void juce::ListenerList<juce::ValueTree::Listener, juce::Array<juce::ValueTree::Listener*, juce::DummyCriticalSection, 0> >::callExcluding<juce::ValueTree::SharedObject::sendChildRemovedMessage(juce::ValueTree, int)::'lambda'(juce::ValueTree::Listener&)&>(juce::ValueTree::Listener*, juce::ValueTree::SharedObject::sendChildRemovedMessage(juce::ValueTree, int)::'lambda'(juce::ValueTree::Listener&)&) juce_ListenerList.h:140
void juce::ValueTree::SharedObject::callListeners<juce::ValueTree::SharedObject::sendChildRemovedMessage(juce::ValueTree, int)::'lambda'(juce::ValueTree::Listener&)>(juce::ValueTree::Listener*, juce::ValueTree::SharedObject::sendChildRemovedMessage(juce::ValueTree, int)::'lambda'(juce::ValueTree::Listener&)) const juce_ValueTree.cpp:85
void juce::ValueTree::SharedObject::callListenersForAllParents<juce::ValueTree::SharedObject::sendChildRemovedMessage(juce::ValueTree, int)::'lambda'(juce::ValueTree::Listener&)>(juce::ValueTree::Listener*, juce::ValueTree::SharedObject::sendChildRemovedMessage(juce::ValueTree, int)::'lambda'(juce::ValueTree::Listener&)) const juce_ValueTree.cpp:94
juce::ValueTree::SharedObject::sendChildRemovedMessage(juce::ValueTree, int) juce_ValueTree.cpp:112
juce::ValueTree::SharedObject::removeChild(int, juce::UndoManager*) juce_ValueTree.cpp:296
juce::ValueTree::SharedObject::removeAllChildren(juce::UndoManager*) juce_ValueTree.cpp:309
juce::ValueTree::removeAllChildren(juce::UndoManager*) juce_ValueTree.cpp:938
tracktion_engine::MidiList::clear(juce::UndoManager*) tracktion_MidiList.cpp:1090
tracktion_engine::MidiList::copyFrom(tracktion_engine::MidiList const&, juce::UndoManager*) tracktion_MidiList.cpp:1098
phenotype::Clip::renderMidi() Clip.h:110
phenotype::MainView::onPadReleased(int) MainView.h:64
phenotype::MultiAkaiFire::onPadReleased(int)::$_6::operator()(phenotype::MidiControllerListener&) const MultiAkaiFire.cpp:111
void juce::ListenerList<phenotype::MidiControllerListener, juce::Array<phenotype::MidiControllerListener*, juce::DummyCriticalSection, 0> >::call<phenotype::MultiAkaiFire::onPadReleased(int)::$_6>(phenotype::MultiAkaiFire::onPadReleased(int)::$_6&&) juce_ListenerList.h:124
phenotype::MultiAkaiFire::onPadReleased(int) MultiAkaiFire.cpp:109
phenotype::MultiAkaiFire::processControllerMessage(std::__1::shared_ptr<phenotype::ControllerMessage>&, int) MultiAkaiFire.cpp:155
phenotype::MultiAkaiFire::processMessages() MultiAkaiFire.cpp:190
phenotype::MultiAkaiFire::timerCallback()::$_7::operator()() const MultiAkaiFire.cpp:201
decltype(std::__1::forward<phenotype::MultiAkaiFire::timerCallback()::$_7&>(fp)()) std::__1::__invoke<phenotype::MultiAkaiFire::timerCallback()::$_7&>(phenotype::MultiAkaiFire::timerCallback()::$_7&) type_traits:3747
void std::__1::__invoke_void_return_wrapper<void>::__call<phenotype::MultiAkaiFire::timerCallback()::$_7&>(phenotype::MultiAkaiFire::timerCallback()::$_7&) __functional_base:348
std::__1::__function::__alloc_func<phenotype::MultiAkaiFire::timerCallback()::$_7, std::__1::allocator<phenotype::MultiAkaiFire::timerCallback()::$_7>, void ()>::operator()() functional:1553
std::__1::__function::__func<phenotype::MultiAkaiFire::timerCallback()::$_7, std::__1::allocator<phenotype::MultiAkaiFire::timerCallback()::$_7>, void ()>::operator()() functional:1727
std::__1::__function::__value_func<void ()>::operator()() const functional:1880
std::__1::function<void ()>::operator()() const functional:2555
juce::MessageManager::callAsync(std::__1::function<void ()>)::AsyncCallInvoker::messageCallback() juce_MessageManager.cpp:195
juce::MessageQueue::deliverNextMessage() juce_osx_MessageQueue.h:81
juce::MessageQueue::runLoopCallback() juce_osx_MessageQueue.h:92
juce::MessageQueue::runLoopSourceCallback(void*) juce_osx_MessageQueue.h:100
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ 0x00007fff2064b37c
__CFRunLoopDoSource0 0x00007fff2064b2e4
__CFRunLoopDoSources0 0x00007fff2064b064
__CFRunLoopRun 0x00007fff20649a8c
CFRunLoopRunSpecific 0x00007fff2064904c
RunCurrentEventLoopInMode 0x00007fff28891a83
ReceiveNextEventCommon 0x00007fff288917e5
_BlockUntilNextEventMatchingListInModeWithFilter 0x00007fff28891583
_DPSNextEvent 0x00007fff22e51d72
-[NSApplication(NSEvent) _nextEventMatchingEventMask:untilDate:inMode:dequeue:] 0x00007fff22e50545
-[NSApplication run] 0x00007fff22e42869
juce::MessageManager::runDispatchLoop() juce_mac_MessageManager.mm:359
juce::JUCEApplicationBase::main() juce_ApplicationBase.cpp:262
juce::JUCEApplicationBase::main(int, char const**) juce_ApplicationBase.cpp:240
main Main.cpp:109
start 0x00007fff2056ef3d

If I use MidiList::addNode() instead of copyFrom(), the issue is the same:

tracktion_engine::Edit::restartPlayback() tracktion_Edit.cpp:1055
tracktion_engine::Edit::TreeWatcher::restart() tracktion_Edit.cpp:353
tracktion_engine::Edit::TreeWatcher::childAddedOrRemoved(juce::ValueTree&, juce::ValueTree&) tracktion_Edit.cpp:305
tracktion_engine::Edit::TreeWatcher::valueTreeChildAdded(juce::ValueTree&, juce::ValueTree&) tracktion_Edit.cpp:273
juce::ValueTree::SharedObject::sendChildAddedMessage(juce::ValueTree)::'lambda'(juce::ValueTree::Listener&)::operator()(juce::ValueTree::Listener&) const juce_ValueTree.cpp:106
void juce::ListenerList<juce::ValueTree::Listener, juce::Array<juce::ValueTree::Listener*, juce::DummyCriticalSection, 0> >::callExcluding<juce::ValueTree::SharedObject::sendChildAddedMessage(juce::ValueTree)::'lambda'(juce::ValueTree::Listener&)&>(juce::ValueTree::Listener*, juce::ValueTree::SharedObject::sendChildAddedMessage(juce::ValueTree)::'lambda'(juce::ValueTree::Listener&)&) juce_ListenerList.h:140
void juce::ValueTree::SharedObject::callListeners<juce::ValueTree::SharedObject::sendChildAddedMessage(juce::ValueTree)::'lambda'(juce::ValueTree::Listener&)>(juce::ValueTree::Listener*, juce::ValueTree::SharedObject::sendChildAddedMessage(juce::ValueTree)::'lambda'(juce::ValueTree::Listener&)) const juce_ValueTree.cpp:85
void juce::ValueTree::SharedObject::callListenersForAllParents<juce::ValueTree::SharedObject::sendChildAddedMessage(juce::ValueTree)::'lambda'(juce::ValueTree::Listener&)>(juce::ValueTree::Listener*, juce::ValueTree::SharedObject::sendChildAddedMessage(juce::ValueTree)::'lambda'(juce::ValueTree::Listener&)) const juce_ValueTree.cpp:94
juce::ValueTree::SharedObject::sendChildAddedMessage(juce::ValueTree) juce_ValueTree.cpp:106
juce::ValueTree::SharedObject::addChild(juce::ValueTree::SharedObject*, int, juce::UndoManager*) juce_ValueTree.cpp:268
juce::ValueTree::addChild(juce::ValueTree const&, int, juce::UndoManager*) juce_ValueTree.cpp:915
tracktion_engine::MidiList::addNote(int, double, double, int, int, juce::UndoManager*) tracktion_MidiList.cpp:1275
phenotype::Clip::renderMidi() Clip.h:106
phenotype::MainView::onPadReleased(int) MainView.h:64
phenotype::MultiAkaiFire::onPadReleased(int)::$_6::operator()(phenotype::MidiControllerListener&) const MultiAkaiFire.cpp:111
void juce::ListenerList<phenotype::MidiControllerListener, juce::Array<phenotype::MidiControllerListener*, juce::DummyCriticalSection, 0> >::call<phenotype::MultiAkaiFire::onPadReleased(int)::$_6>(phenotype::MultiAkaiFire::onPadReleased(int)::$_6&&) juce_ListenerList.h:124
phenotype::MultiAkaiFire::onPadReleased(int) MultiAkaiFire.cpp:109
phenotype::MultiAkaiFire::processControllerMessage(std::__1::shared_ptr<phenotype::ControllerMessage>&, int) MultiAkaiFire.cpp:155
phenotype::MultiAkaiFire::processMessages() MultiAkaiFire.cpp:190
phenotype::MultiAkaiFire::timerCallback()::$_7::operator()() const MultiAkaiFire.cpp:201
decltype(std::__1::forward<phenotype::MultiAkaiFire::timerCallback()::$_7&>(fp)()) std::__1::__invoke<phenotype::MultiAkaiFire::timerCallback()::$_7&>(phenotype::MultiAkaiFire::timerCallback()::$_7&) type_traits:3747
void std::__1::__invoke_void_return_wrapper<void>::__call<phenotype::MultiAkaiFire::timerCallback()::$_7&>(phenotype::MultiAkaiFire::timerCallback()::$_7&) __functional_base:348
std::__1::__function::__alloc_func<phenotype::MultiAkaiFire::timerCallback()::$_7, std::__1::allocator<phenotype::MultiAkaiFire::timerCallback()::$_7>, void ()>::operator()() functional:1553
std::__1::__function::__func<phenotype::MultiAkaiFire::timerCallback()::$_7, std::__1::allocator<phenotype::MultiAkaiFire::timerCallback()::$_7>, void ()>::operator()() functional:1727
std::__1::__function::__value_func<void ()>::operator()() const functional:1880
std::__1::function<void ()>::operator()() const functional:2555
juce::MessageManager::callAsync(std::__1::function<void ()>)::AsyncCallInvoker::messageCallback() juce_MessageManager.cpp:195
juce::MessageQueue::deliverNextMessage() juce_osx_MessageQueue.h:81
juce::MessageQueue::runLoopCallback() juce_osx_MessageQueue.h:92
juce::MessageQueue::runLoopSourceCallback(void*) juce_osx_MessageQueue.h:100
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ 0x00007fff2064b37c
__CFRunLoopDoSource0 0x00007fff2064b2e4
__CFRunLoopDoSources0 0x00007fff2064b064
__CFRunLoopRun 0x00007fff20649a8c
CFRunLoopRunSpecific 0x00007fff2064904c
RunCurrentEventLoopInMode 0x00007fff28891a83
ReceiveNextEventCommon 0x00007fff288917e5
_BlockUntilNextEventMatchingListInModeWithFilter 0x00007fff28891583
_DPSNextEvent 0x00007fff22e51d72
-[NSApplication(NSEvent) _nextEventMatchingEventMask:untilDate:inMode:dequeue:] 0x00007fff22e50545
-[NSApplication run] 0x00007fff22e42869
juce::MessageManager::runDispatchLoop() juce_mac_MessageManager.mm:359
juce::JUCEApplicationBase::main() juce_ApplicationBase.cpp:262
juce::JUCEApplicationBase::main(int, char const**) juce_ApplicationBase.cpp:240
main Main.cpp:109
start 0x00007fff2056ef3d

I also noticed that there are a lot of MIDI messages with channel 0 that fail the asserts in MidiMessage::isForChannel(), MidiMessage::noteOff / MidiMessage::noteOn, and the MidiNode::MidiNode constructor but only when I use copyFrom instead of just addNote.

I think that MidiOutputDeviceInstance::stop/start is there in case there are playing notes which have been removed. What should probably happen is that an ActiveNoteList is used to send note-offs if the get removed.

I’m fully slammed with some other things right now though so won’t have a chance to do this myself for a while. You could have a go by looking at how it’s used in ExternalPlugin if you want to try it yourself?
I think when the topology changes and restartPlayback is called, MidiNode::createMessagesForTime will be called sending note-on messages for notes that should be on in the audio callback.
If you compare those with the an ActiveNoteList that’s updated in MidiOutputDeviceInstance::mergeInMidiMessages, you should be able to tell what note-offs should be sent for notes that are no longer active.

I think that should work…

Sorry it’s taken me a long time to get back to this, but I have discovered something interesting. When a note ends on the loop boundary, the MIDI note off event is not always sent when looping. Thus multiple MIDI note on events get sent and can lead to stuck notes if the corresponding MIDI note off messages are not sent or all notes off message is not sent. If I modify the length of such notes so that the MIDI note off event occurs just slightly ahead of the loop boundry, the MIDI note on / off events match up and as far as I can tell, there are no stuck notes. So MidiOutputDeviceInstance::prepareToPlay() doesn’t need to call stop() which then calls getMidiOutput().sendNoteOffMessages(). It may be enough for it do do if (playing) playing = false;. If this is the case perhaps there is a bug with sending MIDI note off messages that are right on the loop boundary, or perhaps this is intended as I suppose one could consider them past the end of the loop? I suspect maybe a bug because it does sometimes send MIDI note off messages for notes ending on the loop boundary and sometimes not which is strange as it’s just playing the same loop over and over. It could also just be a bug in my code which I just recently fixed and this may be the intended behavior.

Could replacing the stop() call with if (playing) playing = false; be a solution then if I can fix the MIDI note off messages not sending when they are on the loop boundary (or not send them that way if the error is mine)?

If not, perhaps I can try to implement the solution you detail, though I’m not exactly sure how to. In MidiOutputDeviceInstance::mergeInMidiMessages() and in the MidiOutputDeviceInstance class there is a MidiMessageArray midiMessages. Are these the messages that need to be on? If so, where is the structure of all the notes that I should compare this to, to do a diff and send note off messages for notes that are no longer on? I looked in MidiOutputDevice and it also has a MidiMessageArray but neither really seem to every have any messages, except very, very rarely in MidiOutputDeviceInstance. I think I understand what you’re trying to say as compare the notes that should be playing with the notes that were playing and send note off messages for the difference, but I’m not sure which structure contains the notes that should be playing vs. notes that were playing before restartPlayback was called. I can see how you are iterating through ActiveNoteList in ExternalPlugin but I’m not sure which structures from where to use for a similar effect.

Also, it seems that MidiOutputDevice::sendNoteOffMessages() is actually keeping track of channels and notes to send note offs to, though only with simple bools which may not work for multiple MIDI note on messages that each need their corresponding MIDI note off messages sent.

@dave96

Can I ask if you’re using the latest tip of develop? There has been lots of changes to MIDI handling in the past few years.

Yes, I’m on the latest develop. The issue hasn’t really gone away, but after the first fix above it’s been fairly minor so I was working on other things. I’m looking at MidiOutputDevice::sendNoteOffMessages() and really the only thing that is causing the issue is the call to send the ‘all notes off’ message not any of the individual note offs: sendMessageNow (juce::MidiMessage::allNotesOff (channel)); so I’m wondering if that can be made optional too like the ‘all controllers off’ message below it. If not, I’m willing to give a proper implementation a try but I’m just not sure I understand the data sources to compare to send the proper note off messages.