Skip to content

Fix playing notes notes retriggering when changing clip during first loop in Clip Launcher #255

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 77 additions & 31 deletions modules/tracktion_engine/playback/graph/tracktion_LoopingMidiNode.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -711,9 +711,13 @@ struct EventGenerator : public MidiGenerator
bool useMPEChannelMode, MPESourceID midiSourceID,
juce::Array<juce::MidiMessage>& controllerMessagesScratchBuffer) override
{
thread_local MidiMessageArray scratchBuffer, cleanedBufferToMerge;
ActiveNoteList originalState;
activeNoteList.iterate([&](int chan, int note) {
originalState.startNote(chan, note);
});

thread_local MidiMessageArray scratchBuffer;
scratchBuffer.clear();
cleanedBufferToMerge.clear();

MidiHelpers::createMessagesForTime (scratchBuffer,
sequence, noteOffMap,
Expand All @@ -723,32 +727,29 @@ struct EventGenerator : public MidiGenerator
useMPEChannelMode, midiSourceID,
controllerMessagesScratchBuffer);

// This isn't quite right as there could be notes that are turned on in the original buffer after the scratch buffer?
for (const auto& e : scratchBuffer)
{
if (e.isNoteOn())
{
if (! activeNoteList.isNoteActive (e.getChannel(), e.getNoteNumber()))
if (!originalState.isNoteActive(e.getChannel(), e.getNoteNumber()))
{
cleanedBufferToMerge.add (e);
activeNoteList.startNote (e.getChannel(), e.getNoteNumber());
destBuffer.addMidiMessage(e, midiSourceID);
activeNoteList.startNote(e.getChannel(), e.getNoteNumber());
}
}
else if (e.isNoteOff())
{
if (activeNoteList.isNoteActive (e.getChannel(), e.getNoteNumber()))
if (originalState.isNoteActive(e.getChannel(), e.getNoteNumber()))
{
activeNoteList.clearNote (e.getChannel(), e.getNoteNumber());
cleanedBufferToMerge.add (e);
destBuffer.addMidiMessage(e, midiSourceID);
activeNoteList.clearNote(e.getChannel(), e.getNoteNumber());
}
}
else
{
cleanedBufferToMerge.add (e);
destBuffer.addMidiMessage(e, midiSourceID);
}
}

destBuffer.mergeFrom (cleanedBufferToMerge);
}

ActiveNoteList getNotesOnAtTime (SequenceBeatPosition time, juce::Range<int> channelNumbers, LiveClipLevel& clipLevel) override
Expand Down Expand Up @@ -956,11 +957,15 @@ class LoopedMidiEventGenerator : public MidiGenerator
LoopedMidiEventGenerator (std::unique_ptr<MidiGenerator> gen,
std::shared_ptr<ActiveNoteList> anl,
EditBeatRange clipRangeToUse,
ClipBeatRange loopTimesToUse)
ClipBeatRange loopTimesToUse,
juce::Range<int> channels,
LiveClipLevel& level)
: generator (std::move (gen)),
activeNoteList (std::move (anl)),
clipRange (clipRangeToUse),
loopTimes (loopTimesToUse)
loopTimes (loopTimesToUse),
channelNumbers (channels),
clipLevel (level)
{
assert (activeNoteList);
}
Expand Down Expand Up @@ -1025,10 +1030,10 @@ class LoopedMidiEventGenerator : public MidiGenerator
if (exhausted() && ! loopTimes.isEmpty())
{
setLoopIndex (loopIndex + 1);
generator->setTime (0.0);
generator->setTime (loopTimes.getStart());
}

return exhausted();
return !exhausted();
}

bool exhausted() override
Expand All @@ -1044,6 +1049,9 @@ class LoopedMidiEventGenerator : public MidiGenerator
const ClipBeatRange loopTimes;
int loopIndex = 0;

const juce::Range<int> channelNumbers;
LiveClipLevel& clipLevel;

SequenceBeatPosition editBeatPositionToSequenceBeatPosition (EditBeatPosition editBeatPosition) const
{
const ClipBeatPosition clipPos = editBeatPosition - clipRange.getStart();
Expand All @@ -1063,7 +1071,19 @@ class LoopedMidiEventGenerator : public MidiGenerator

loopIndex = newLoopIndex;
const auto sequenceOffset = clipRange.getStart() + (loopIndex * loopTimes.getLength());
generator->cacheSequence (sequenceOffset, loopTimes + sequenceOffset);
generator->cacheSequence(sequenceOffset, loopTimes + sequenceOffset);

// Get notes that would be active at the start of the new loop
ActiveNoteList notesAtLoopStart = generator->getNotesOnAtTime(loopTimes.getStart(),
channelNumbers,
clipLevel);

activeNoteList->reset();

// Track which notes we're preserving vs adding new
notesAtLoopStart.iterate([&](int channel, int note) {
activeNoteList->startNote(channel, note);
});
}
};

Expand Down Expand Up @@ -1150,14 +1170,18 @@ class GeneratorAndNoteList
BeatDuration offsetToUse,
const QuantisationType& quantisation_,
const GrooveTemplate* groove_,
float grooveStrength_)
float grooveStrength_,
juce::Range<int> channelNumbers_,
LiveClipLevel& clipLevelToUse)
: sequences (std::move (sequencesToUse)),
editRange (editRangeToUse),
loopRange (loopRangeToUse),
offset (offsetToUse),
quantisation (quantisation_),
groove (groove_ != nullptr ? *groove_ : GrooveTemplate()),
grooveStrength (grooveStrength_)
grooveStrength (grooveStrength_),
channelNumbers (channelNumbers_),
clipLevel (clipLevelToUse)
{
assert (sequences.size() > 0);
}
Expand All @@ -1170,10 +1194,13 @@ class GeneratorAndNoteList
return;

assert (sequences.size() > 0);
dynamicOffsetBeats = std::move (dynamicOffsetBeatsToUse);
shouldCreateMessagesForTime = clipPropertiesHaveChanged || noteListToUse == nullptr;
dynamicOffsetBeats = !dynamicOffsetBeatsToUse ? std::move (dynamicOffsetBeatsToUse)
: std::make_shared<BeatDuration>();
activeNoteList = noteListToUse ? std::move (noteListToUse)
: std::make_shared<ActiveNoteList>();

// If the clip properties have changed, we need to recreate messages
shouldCreateMessagesForTime = clipPropertiesHaveChanged || activeNoteList->areAnyNotesActive() == false;

const EditBeatRange clipRangeRaw { editRange.getStart().inBeats(), editRange.getEnd().inBeats() };
const ClipBeatRange loopRangeRaw { loopRange.getStart().inBeats(), loopRange.getEnd().inBeats() };
Expand All @@ -1183,13 +1210,14 @@ class GeneratorAndNoteList

sequencesHash = std::hash<std::vector<juce::MidiMessageSequence>>{} (sequences);

if (sequencesHash != lastSequencesHash || clipPropertiesHaveChanged)
shouldSendNoteOffsForNotesNoLongerPlaying = true;
// Force note-offs for all active notes if the sequence has changed
shouldSendNoteOffsForNotesNoLongerPlaying = (sequencesHash != lastSequencesHash) || clipPropertiesHaveChanged;

auto cachingGenerator = std::make_unique<CachingMidiEventGenerator> (std::move (sequences),
std::move (quantisation), std::move (groove), grooveStrength);
auto loopedGenerator = std::make_unique<LoopedMidiEventGenerator> (std::move (cachingGenerator),
activeNoteList, clipRangeRaw, loopRangeRaw);
activeNoteList, clipRangeRaw, loopRangeRaw,
channelNumbers, clipLevel);
generator = std::make_unique<OffsetMidiEventGenerator> (std::move (loopedGenerator),
offset.inBeats(), dynamicOffsetBeats);

Expand Down Expand Up @@ -1262,12 +1290,25 @@ class GeneratorAndNoteList
if (! isContiguousWithPreviousBlock
|| blockStartBeatRelativeToClip <= 0.00001_bd)
{
MidiNodeHelpers::createNoteOffs (*activeNoteList,
destBuffer,
midiSourceID,
0.0,
isPlaying);
shouldCreateMessagesForTime = true;
// Only send note-offs when actually recreating the node - avoid turning off notes unnecessarily
if (! isContiguousWithPreviousBlock && activeNoteList->areAnyNotesActive())
{
// Get the notes that would be active at this clip intersection
auto notesAtCurrentPosition = generator->getNotesOnAtTime(clipIntersection.getStart().inBeats(),
channelNumbers,
clipLevel);

// Turn off any notes that don't appear in the current position
activeNoteList->iterate([&](int chan, int note) {
if (!notesAtCurrentPosition.isNoteActive(chan, note)) {
destBuffer.addMidiMessage(juce::MidiMessage::noteOff(chan, note), 0.0, midiSourceID);
activeNoteList->clearNote(chan, note);
}
});
}

// Only create messages at clip start, otherwise use the existing note state
shouldCreateMessagesForTime = blockStartBeatRelativeToClip <= 0.00001_bd;
}

if (shouldCreateMessagesForTime)
Expand Down Expand Up @@ -1383,6 +1424,9 @@ class GeneratorAndNoteList

bool shouldCreateMessagesForTime = false, shouldSendNoteOffsForNotesNoLongerPlaying = false;
juce::Array<juce::MidiMessage> controllerMessagesScratchBuffer;

const juce::Range<int> channelNumbers;
LiveClipLevel& clipLevel;
};


Expand Down Expand Up @@ -1422,7 +1466,9 @@ LoopingMidiNode::LoopingMidiNode (std::vector<juce::MidiMessageSequence> sequenc
sequenceOffset,
quantisation,
groove,
grooveStrength);
grooveStrength,
channelNumbers,
clipLevel);
}

const std::shared_ptr<ActiveNoteList>& LoopingMidiNode::getActiveNoteList() const
Expand Down