From 3f930c19e9d01f53714a15a03983e8629fab6cbc Mon Sep 17 00:00:00 2001 From: Paul Date: Mon, 4 Nov 2024 18:31:18 -0500 Subject: [PATCH] An initial pass at mono-non-legato mode (#12) This implements mono non-legato mode. Lots I still want to do and I would say this is still not a stable itnegration point but it provides per-group mono with a moderately robust suite of tests. --- include/sst/voicemanager/voicemanager.h | 77 ++- .../voicemanager/voicemanager_constraints.h | 4 +- include/sst/voicemanager/voicemanager_impl.h | 478 +++++++++++++++-- tests/mono_playback.cpp | 480 +++++++++++++++++- tests/mpe_tests.cpp | 4 +- tests/piano_mode.cpp | 8 +- tests/routing_params.cpp | 2 +- tests/stealing_priorities.cpp | 11 +- tests/test_player.h | 211 +++++--- 9 files changed, 1143 insertions(+), 132 deletions(-) diff --git a/include/sst/voicemanager/voicemanager.h b/include/sst/voicemanager/voicemanager.h index 5ce10de..842a65e 100644 --- a/include/sst/voicemanager/voicemanager.h +++ b/include/sst/voicemanager/voicemanager.h @@ -42,7 +42,7 @@ namespace sst::voicemanager */ template struct VoiceInitBufferEntry { - typename Cfg::voice_t *voice; ///< A pointer to the voice, owned by the responder + typename Cfg::voice_t *voice{nullptr}; ///< A pointer to the voice, owned by the responder /** * buffer_t is the typedef for the working group array you will receive @@ -51,6 +51,23 @@ template struct VoiceInitBufferEntry using buffer_t = std::array, Cfg::maxVoiceCount>; }; +/** + * VoiceInitInstructionsEntry is how the voice manager gives instructions to the synth + * to start or restart a voice. + */ +template struct VoiceInitInstructionsEntry +{ + enum struct Instruction + { + START, ///< Start a new voice at this entry + SKIP, ///< Skip this voice altogether. The voice manager has discarded it + MOVE_FROM ///< The voice at thius index is a move of the voice pointer + } instruction{Instruction::START}; + + typename Cfg::voice_t *moveFromVoice{nullptr}; + using buffer_t = std::array, Cfg::maxVoiceCount>; +}; + /** * VoiceBeginBufferEntry is the object which the responder needs to populate * in the voice begin creation lifecycle. @@ -85,30 +102,62 @@ template struct VoiceBeginBufferEntry */ template struct VoiceManager { - enum MIDI1Dialect + typedef VoiceInitInstructionsEntry initInstruction_t; + + enum struct MIDI1Dialect { MIDI1, MIDI1_MPE - } dialect{MIDI1}; + } dialect{MIDI1Dialect::MIDI1}; - /* + /** * If a key is struck twice while still gated or sustained, do we start a new voice - * or do we re-use the voice (and move the note id) + * or do we re-use the voice (and move the note id) or do we trigger a second voice */ - enum RepeatedKeyMode + enum struct RepeatedKeyMode { MULTI_VOICE, PIANO - } repeatedKeyMode{MULTI_VOICE}; + } repeatedKeyMode{RepeatedKeyMode::MULTI_VOICE}; + + /** + * The voice manager can either run in a mode where it manages to voice limits in a polyphonic + * mode (but multi-voice notes still steal together) or can manage to a single note with + * essentially unlimited voices. This is controlled by the group PlayMode. + */ + enum struct PlayMode + { + POLY_VOICES, ///< The voice manager manages voice counts from any number of keys in piano + ///< mode + MONO_NOTES ///< The voice manager makes sure the consequence of only one key is playing at a + ///< time, independent of voices + }; - enum PlayMode + /** + * Mono Playmode is somewhat ambiguous, meaning many things. So lets enumerate the features. + * Hope we have fewer than 64. Even though these are using the bits as features they are not all + * possible to be active simultaneusly. As well as distinct bit values, we provide a few + * preset | combinations of flags for common use cases. + */ + enum struct MonoPlayModeFeatures : uint64_t { - POLY, - MONO, - MONO_LEGATO + NONE = 0, + MONO_RETRIGGER = 1 << 0, ///< A new keypress triggers a new voice + MONO_LEGATO = 1 << 1, ///< A new keypress moves the playing voice + + ON_RELEASE_TO_LATEST = 1 << 2, ///< mono release return to latest + ON_RELEASE_TO_HIGHEST = 1 << 3, ///< release to highest + ON_RELEASE_TO_LOWEST = 1 << 4, ///< release to lowest + + NATURAL_MONO = MONO_RETRIGGER | ON_RELEASE_TO_LATEST, ///< What a 'mono' button would do + NATURAL_LEGATO = MONO_LEGATO | ON_RELEASE_TO_LATEST, ///< What a 'legato' button would do }; - enum StealingPriorityMode + /** + * StealingPriorityMode deteremines how to pick a voice to steal when an appropriate voice or + * note limit is met. HIGHEST and LOWEST are in midi-key space. + */ + enum struct StealingPriorityMode { OLDEST, HIGHEST, @@ -150,8 +199,10 @@ template struct Voice void allNotesOff(); void allSoundsOff(); + void guaranteeGroup(uint64_t groupId); void setPolyphonyGroupVoiceLimit(uint64_t groupId, int32_t limit); - void setPlaymode(uint64_t groupId, PlayMode pm); + void setPlaymode(uint64_t groupId, PlayMode pm, + uint64_t features = (uint64_t)MonoPlayModeFeatures::NONE); void setStealingPriorityMode(uint64_t groupId, StealingPriorityMode pm); private: diff --git a/include/sst/voicemanager/voicemanager_constraints.h b/include/sst/voicemanager/voicemanager_constraints.h index 0d55fa2..b2ce200 100644 --- a/include/sst/voicemanager/voicemanager_constraints.h +++ b/include/sst/voicemanager/voicemanager_constraints.h @@ -59,7 +59,9 @@ template struct Const void (Responder::*)(uint16_t, uint16_t, uint16_t, int32_t, float)) HASMEM(terminateVoice, Responder, void (Responder::*)(typename Cfg::voice_t *)) HASMEM(initializeMultipleVoices, Responder, - int32_t (Responder::*)(typename VoiceInitBufferEntry::buffer_t &, uint16_t, + int32_t (Responder::*)(int32_t, + const typename VoiceInitInstructionsEntry::buffer_t &, + typename VoiceInitBufferEntry::buffer_t &, uint16_t, uint16_t, uint16_t, int32_t, float, float)) HASMEM(releaseVoice, Responder, void (Responder::*)(typename Cfg::voice_t *, float)) HASMEM(setNoteExpression, Responder, diff --git a/include/sst/voicemanager/voicemanager_impl.h b/include/sst/voicemanager/voicemanager_impl.h index d35abfc..7bb7f62 100644 --- a/include/sst/voicemanager/voicemanager_impl.h +++ b/include/sst/voicemanager/voicemanager_impl.h @@ -22,6 +22,7 @@ #include #include +#include namespace sst::voicemanager { @@ -43,12 +44,12 @@ struct VoiceManager::Details Details(VoiceManager &in) : vm(in) { std::fill(lastPBByChannel.begin(), lastPBByChannel.end(), 0); - usedVoices[0] = 0; - polyLimits[0] = Cfg::maxVoiceCount; - stealingPriorityMode[0] = OLDEST; + + keyStateByPort[0] = {}; + guaranteeGroup(0); } - int64_t mostRecentNoteCounter{1}; + int64_t mostRecentVoiceCounter{1}; int64_t mostRecentTransactionID{1}; struct VoiceInfo @@ -56,7 +57,7 @@ struct VoiceManager::Details int16_t port{0}, channel{0}, key{0}; int32_t noteId{-1}; - int64_t noteCounter{0}, transactionId{0}; + int64_t voiceCounter{0}, transactionId{0}; bool gated{false}; bool gatedDueToSustain{false}; @@ -79,10 +80,36 @@ struct VoiceManager::Details std::unordered_map polyLimits; std::unordered_map usedVoices; std::unordered_map stealingPriorityMode; + std::unordered_map playMode; + std::unordered_map playModeFeatures; int32_t totalUsedVoices{0}; + struct IndividualKeyState + { + int64_t transaction{0}; + float inceptionVelocity{0.f}; + bool heldBySustain{false}; + }; + using keyState_t = std::array, 128>, 16>; + std::map keyStateByPort{}; + + void guaranteeGroup(int groupId) + { + if (polyLimits.find(groupId) == polyLimits.end()) + polyLimits[groupId] = Cfg::maxVoiceCount; + if (usedVoices.find(groupId) == usedVoices.end()) + usedVoices[groupId] = 0; + if (stealingPriorityMode.find(groupId) == stealingPriorityMode.end()) + stealingPriorityMode[groupId] = StealingPriorityMode::OLDEST; + if (playMode.find(groupId) == playMode.end()) + playMode[groupId] = PlayMode::POLY_VOICES; + if (playModeFeatures.find(groupId) == playModeFeatures.end()) + playModeFeatures[groupId] = (uint64_t)MonoPlayModeFeatures::NONE; + } + typename VoiceBeginBufferEntry::buffer_t voiceBeginWorkingBuffer; typename VoiceInitBufferEntry::buffer_t voiceInitWorkingBuffer; + typename VoiceInitInstructionsEntry::buffer_t voiceInitInstructionsBuffer; std::array, 16> midiCCCache{}; bool sustainOn{false}; std::array lastPBByChannel{}; @@ -129,12 +156,12 @@ struct VoiceManager::Details { if (vi.activeVoiceCookie == v) { - vi.activeVoiceCookie = nullptr; --usedVoices.at(vi.polyGroup); --totalUsedVoices; - VML("Ending voice " << vi.activeVoiceCookie << " pg=" << vi.polyGroup - << " used now is " << usedVoices.at(vi.polyGroup) << " (" - << totalUsedVoices << ")"); + VML(" - Ending voice " << vi.activeVoiceCookie << " pg=" << vi.polyGroup + << " used now is " << usedVoices.at(vi.polyGroup) << " (" + << totalUsedVoices << ")"); + vi.activeVoiceCookie = nullptr; } } } @@ -144,7 +171,7 @@ struct VoiceManager::Details { int32_t oldestGated{-1}, oldestNonGated{-1}; int64_t gi{std::numeric_limits::max()}, ngi{gi}; - if (pm == HIGHEST) + if (pm == StealingPriorityMode::HIGHEST) { gi = std::numeric_limits::min(); ngi = gi; @@ -166,22 +193,22 @@ struct VoiceManager::Details continue; } - VML(" - Considering " << vi << " " << v.key << " " << gi << " " << v.noteCounter); + VML(" - Considering " << vi << " " << v.key << " " << gi << " " << v.voiceCounter); if (v.gated || v.gatedDueToSustain) { switch (pm) { - case OLDEST: + case StealingPriorityMode::OLDEST: { - if (v.noteCounter < gi) + if (v.voiceCounter < gi) { oldestGated = vi; - gi = v.noteCounter; + gi = v.voiceCounter; } } break; - case HIGHEST: + case StealingPriorityMode::HIGHEST: { if (v.key > gi) { @@ -191,7 +218,7 @@ struct VoiceManager::Details } break; - case LOWEST: + case StealingPriorityMode::LOWEST: { if (v.key < gi) { @@ -206,17 +233,17 @@ struct VoiceManager::Details { switch (pm) { - case OLDEST: + case StealingPriorityMode::OLDEST: { - if (v.noteCounter < ngi) + if (v.voiceCounter < ngi) { oldestNonGated = vi; - ngi = v.noteCounter; + ngi = v.voiceCounter; } } break; - case HIGHEST: + case StealingPriorityMode::HIGHEST: { if (v.key > ngi) { @@ -226,7 +253,7 @@ struct VoiceManager::Details } break; - case LOWEST: + case StealingPriorityMode::LOWEST: { if (v.key < ngi) { @@ -249,9 +276,215 @@ struct VoiceManager::Details } return -1; } -}; -// ToDo: API Static Asserts + void doMonoRetrigger(int16_t port, uint64_t polyGroup) + { + VML("=== MONO mode voice retrigger for " << polyGroup); + auto &ks = keyStateByPort[port]; + auto ft = playModeFeatures.at(polyGroup); + int dch{-1}, dk{-1}; + float dvel{0.f}; + + auto findBestKey = [&](bool ignoreSustain) + { + VML("- find best key " << ignoreSustain); + if (ft & (uint64_t)MonoPlayModeFeatures::ON_RELEASE_TO_LATEST) + { + int64_t mtx = 0; + for (int ch = 0; ch < 16; ++ch) + { + for (int k = 0; k < 128; ++k) + { + auto ksp = ks[ch][k].find(polyGroup); + if (ksp != ks[ch][k].end()) + { + const auto [tx, vel, hbs] = ksp->second; + if (hbs != ignoreSustain) + continue; + VML("- Found note " << ch << " " << k << " " << tx << " " << vel << " " + << hbs << " with ignore " << ignoreSustain); + if (mtx < tx) + { + mtx = tx; + dch = ch; + dk = k; + dvel = vel; + } + } + } + } + } + else if (ft & (uint64_t)MonoPlayModeFeatures::ON_RELEASE_TO_HIGHEST) + { + int64_t mk = 0; + for (int ch = 0; ch < 16; ++ch) + { + for (int k = 0; k < 128; ++k) + { + auto ksp = ks[ch][k].find(polyGroup); + if (ksp != ks[ch][k].end()) + { + const auto [tx, vel, hbs] = ksp->second; + if (hbs != ignoreSustain) + continue; + if (tx != 0 && k > mk) + { + mk = k; + dch = ch; + dk = k; + dvel = vel; + } + } + } + } + } + else if (ft & (uint64_t)MonoPlayModeFeatures::ON_RELEASE_TO_LOWEST) + { + int64_t mk = 1024; + for (int ch = 0; ch < 16; ++ch) + { + for (int k = 0; k < 128; ++k) + { + auto ksp = ks[ch][k].find(polyGroup); + if (ksp != ks[ch][k].end()) + { + const auto [tx, vel, hbs] = ksp->second; + if (hbs != ignoreSustain) + continue; + if (tx != 0 && k < mk) + { + mk = k; + dch = ch; + dk = k; + dvel = vel; + } + } + } + } + } + VML("- FindBestKey Result is " << dch << "/" << dk); + }; + + findBestKey(false); + if (dch < 0) + findBestKey(true); + + if (dch >= 0 && dk >= 0) + { + // Need to know the velocity and the port + VML("- retrigger Note " << dch << " " << dk << " " << dvel); + + // FIXME + auto dnid = -1; + + // So now begin end voice transaction + auto voicesToBeLaunched = vm.responder.beginVoiceCreationTransaction( + voiceBeginWorkingBuffer, port, dch, dk, dnid, dvel); + for (int i = 0; i < voicesToBeLaunched; ++i) + { + voiceInitInstructionsBuffer[i] = {}; + voiceInitWorkingBuffer[i] = {}; + if (voiceBeginWorkingBuffer[i].polyphonyGroup != polyGroup) + { + voiceInitInstructionsBuffer[i].instruction = + VoiceInitInstructionsEntry::Instruction::SKIP; + } + } + auto voicesLeft = vm.responder.initializeMultipleVoices( + voicesToBeLaunched, voiceInitInstructionsBuffer, voiceInitWorkingBuffer, port, dch, + dk, dnid, dvel, 0.f); + auto idx = 0; + while (!voiceInitWorkingBuffer[idx].voice) + idx++; + + for (auto &vi : voiceInfo) + { + if (!vi.activeVoiceCookie) + { + vi.voiceCounter = mostRecentVoiceCounter++; + vi.transactionId = mostRecentTransactionID; + vi.port = port; + vi.channel = dch; + vi.key = dk; + vi.noteId = dnid; + + vi.gated = true; + vi.gatedDueToSustain = false; + vi.activeVoiceCookie = voiceInitWorkingBuffer[idx].voice; + vi.polyGroup = voiceBeginWorkingBuffer[idx].polyphonyGroup; + + keyStateByPort[vi.port][vi.channel][vi.key][vi.polyGroup] = {vi.transactionId, + dvel}; + + VML("- New Voice assigned with " + << mostRecentVoiceCounter << " at pckn=" << port << "/" << dch << "/" << dk + << "/" << dnid << " pg=" << vi.polyGroup); + + ++usedVoices.at(vi.polyGroup); + ++totalUsedVoices; + + voicesLeft--; + if (voicesLeft == 0) + { + break; + } + idx++; + while (!voiceInitWorkingBuffer[idx].voice) + idx++; + } + } + + vm.responder.endVoiceCreationTransaction(port, dch, dk, dnid, dvel); + } + } + + bool anyKeyHeldFor(int16_t port, uint64_t polyGroup, int exceptChannel, int exceptKey, + bool includeHeldBySustain = false) + { + auto &ks = keyStateByPort[port]; + for (int ch = 0; ch < 16; ++ch) + { + for (int k = 0; k < 128; ++k) + { + auto ksp = ks[ch][k].find(polyGroup); + if (ksp != ks[ch][k].end() && (includeHeldBySustain || !ksp->second.heldBySustain)) + { + if (!(ch == exceptChannel && k == exceptKey)) + { + return true; + } + } + } + } + return false; + } + + void debugDumpKeyState(int port) const + { + if constexpr (vmLog) + { + VML(">>>> Dump Key State"); + auto &ks = keyStateByPort.at(port); + for (int ch = 0; ch < 16; ++ch) + { + for (int k = 0; k < 128; ++k) + { + if (!ks[ch][k].empty()) + { + VML("- State at " << ch << "/" << k); + auto &vmap = ks[ch][k]; + for (const auto &[pg, it] : vmap) + { + VML(" - PG=" << pg); + VML(" " << it.transaction << "/" << it.inceptionVelocity << "/" + << it.heldBySustain); + } + } + } + } + } + } +}; template VoiceManager::VoiceManager(Responder &r, MonoResponder &m) @@ -272,7 +505,7 @@ bool VoiceManager::processNoteOnEvent(int16_t por int16_t key, int32_t noteid, float velocity, float retune) { - if (repeatedKeyMode == PIANO) + if (repeatedKeyMode == RepeatedKeyMode::PIANO) { bool didAnyRetrigger{false}; ++details.mostRecentTransactionID; @@ -282,7 +515,7 @@ bool VoiceManager::processNoteOnEvent(int16_t por { responder.retriggerVoiceWithNewNoteID(vi.activeVoiceCookie, noteid, velocity); vi.gated = true; - vi.noteCounter = ++details.mostRecentNoteCounter; + vi.voiceCounter = ++details.mostRecentVoiceCounter; vi.transactionId = details.mostRecentTransactionID; didAnyRetrigger = true; } @@ -304,20 +537,36 @@ bool VoiceManager::processNoteOnEvent(int16_t por } std::unordered_map createdByPolyGroup; + std::unordered_set monoGroups; for (int i = 0; i < voicesToBeLaunched; ++i) { + assert(details.playMode.find(details.voiceBeginWorkingBuffer[i].polyphonyGroup) != + details.playMode.end()); + ++createdByPolyGroup[details.voiceBeginWorkingBuffer[i].polyphonyGroup]; + if (details.playMode[details.voiceBeginWorkingBuffer[i].polyphonyGroup] == + PlayMode::MONO_NOTES) + { + monoGroups.insert(details.voiceBeginWorkingBuffer[i].polyphonyGroup); + } } VML("======== LAUNCHING " << voicesToBeLaunched << " @ " << port << "/" << channel << "/" << key << "/" << noteid << " ============"); + for (int i = 0; i < voicesToBeLaunched; ++i) { const auto &vbb = details.voiceBeginWorkingBuffer[i]; auto polyGroup = vbb.polyphonyGroup; assert(details.polyLimits.find(polyGroup) != details.polyLimits.end()); + assert(details.playMode.find(polyGroup) != details.playMode.end()); - VML("Stealing:"); + auto pm = details.playMode.at(polyGroup); + if (pm == PlayMode::MONO_NOTES) + continue; + + VML("Poly Stealing:"); + VML("- Voice " << i << " group=" << polyGroup << " mode=" << (int)pm); VML("- Checking polygroup " << polyGroup); int32_t voiceLimit{details.polyLimits.at(polyGroup)}; int32_t voicesUsed{details.usedVoices.at(polyGroup)}; @@ -368,6 +617,22 @@ bool VoiceManager::processNoteOnEvent(int16_t por } } + // Mono Stealing + if (!monoGroups.empty()) + VML("Mono Stealing:"); + for (const auto &mpg : monoGroups) + { + VML("- Would steal all voices in " << mpg << " (TODO: This is *WRONG* for Legato)"); + for (const auto &v : details.voiceInfo) + { + if (v.activeVoiceCookie && v.polyGroup == mpg) + { + VML("- Stealing voice " << v.key); + responder.terminateVoice(v.activeVoiceCookie); + } + } + } + if (details.lastPBByChannel[channel] != 0) { monoResponder.setMIDIPitchBend(channel, details.lastPBByChannel[channel] + 8192); @@ -384,9 +649,10 @@ bool VoiceManager::processNoteOnEvent(int16_t por } auto voicesLaunched = responder.initializeMultipleVoices( - details.voiceInitWorkingBuffer, port, channel, key, noteid, velocity, retune); + voicesToBeLaunched, details.voiceInitInstructionsBuffer, details.voiceInitWorkingBuffer, + port, channel, key, noteid, velocity, retune); - VML("Voices created " << voicesLaunched); + VML("- Voices created " << voicesLaunched); if (voicesLaunched != voicesToBeLaunched) { @@ -407,7 +673,7 @@ bool VoiceManager::processNoteOnEvent(int16_t por { if (!vi.activeVoiceCookie) { - vi.noteCounter = details.mostRecentNoteCounter++; + vi.voiceCounter = details.mostRecentVoiceCounter++; vi.transactionId = details.mostRecentTransactionID; vi.port = port; vi.channel = channel; @@ -419,9 +685,12 @@ bool VoiceManager::processNoteOnEvent(int16_t por vi.activeVoiceCookie = details.voiceInitWorkingBuffer[voicesLeft - 1].voice; vi.polyGroup = details.voiceBeginWorkingBuffer[voicesLeft - 1].polyphonyGroup; - VML("New Voice assigned with " << details.mostRecentNoteCounter << " at pckn=" << port - << "/" << channel << "/" << key << "/" << noteid - << " pg=" << vi.polyGroup); + details.keyStateByPort[vi.port][vi.channel][vi.key][vi.polyGroup] = {vi.transactionId, + velocity}; + + VML("- New Voice assigned with " << details.mostRecentVoiceCounter + << " at pckn=" << port << "/" << channel << "/" << key + << "/" << noteid << " pg=" << vi.polyGroup); ++details.usedVoices.at(vi.polyGroup); ++details.totalUsedVoices; @@ -431,6 +700,7 @@ bool VoiceManager::processNoteOnEvent(int16_t por { responder.endVoiceCreationTransaction(port, channel, key, noteid, velocity); + details.debugDumpKeyState(port); return true; } } @@ -446,24 +716,95 @@ void VoiceManager::processNoteOffEvent(int16_t po int16_t key, int32_t noteid, float velocity) { + std::unordered_set retriggerGroups; + + VML("==== PROCESS NOTE OFF " << port << "/" << channel << "/" << key << "/" << noteid << " @ " + << velocity); for (auto &vi : details.voiceInfo) { if (vi.matches(port, channel, key, noteid)) { - if (details.sustainOn) + VML("- Found matching release note at " << vi.polyGroup << " " << vi.key); + if (details.playMode[vi.polyGroup] == PlayMode::MONO_NOTES) { - vi.gatedDueToSustain = true; + if (details.sustainOn) + { + VML("- Release with sustain on. Checking to see if there are gated voices " + "away"); + + details.debugDumpKeyState(port); + bool anyOtherOption = details.anyKeyHeldFor(port, vi.polyGroup, channel, key); + if (anyOtherOption) + { + VML("- There's a gated key away so untrigger this"); + retriggerGroups.insert(vi.polyGroup); + responder.terminateVoice(vi.activeVoiceCookie); + vi.gated = false; + } + else + { + vi.gatedDueToSustain = true; + } + } + else + { + if (vi.gated) + { + VML("- Terminating voice at " << vi.polyGroup << " " + << vi.activeVoiceCookie); + bool anyOtherOption = + details.anyKeyHeldFor(port, vi.polyGroup, channel, key); + if (anyOtherOption) + { + responder.terminateVoice(vi.activeVoiceCookie); + retriggerGroups.insert(vi.polyGroup); + } + else + { + responder.releaseVoice(vi.activeVoiceCookie, velocity); + } + vi.gated = false; + } + } } else { - if (vi.gated) + if (details.sustainOn) { - responder.releaseVoice(vi.activeVoiceCookie, velocity); - vi.gated = false; + vi.gatedDueToSustain = true; + } + else + { + if (vi.gated) + { + responder.releaseVoice(vi.activeVoiceCookie, velocity); + vi.gated = false; + } } } } } + + if (details.sustainOn) + { + VML("- Updating just-by-sustain at " << port << " " << channel << " " << key); + for (auto &inf : details.keyStateByPort[port][channel][key]) + { + inf.second.heldBySustain = true; + } + } + else + { + VML("- Clearing keyStateByPort at " << port << " " << channel << " " << key); + details.keyStateByPort[port][channel][key] = {}; + } + + details.debugDumpKeyState(port); + + for (const auto &rtg : retriggerGroups) + { + details.doMonoRetrigger(port, rtg); + } } template @@ -476,16 +817,50 @@ void VoiceManager::updateSustainPedal(int16_t por { if (!details.sustainOn) { + VML("Sustain Release"); + std::unordered_set retriggerGroups; // release all voices with sustain gates for (auto &vi : details.voiceInfo) { + if (!vi.activeVoiceCookie) + continue; + + VML("- Checking " << vi.gated << " " << vi.gatedDueToSustain << " " << vi.key); if (vi.gatedDueToSustain && vi.matches(port, channel, -1, -1)) { - responder.releaseVoice(vi.activeVoiceCookie, 0); + if (details.playMode[vi.polyGroup] == PlayMode::MONO_NOTES) + { + retriggerGroups.insert(vi.polyGroup); + responder.terminateVoice(vi.activeVoiceCookie); + } + else + { + responder.releaseVoice(vi.activeVoiceCookie, 0); + } + + details.keyStateByPort[vi.port][vi.channel][vi.key] = {}; + vi.gated = false; vi.gatedDueToSustain = false; } } + for (const auto &rtg : retriggerGroups) + { + auto &ks = details.keyStateByPort[port]; + for (int ch = 0; ch < 16; ++ch) + { + for (int k = 0; k < 128; ++k) + { + auto ksp = ks[ch][k].find(rtg); + if (ksp != ks[ch][k].end() && ksp->second.heldBySustain) + { + ks[ch][k].erase(ksp); + } + } + } + + details.doMonoRetrigger(port, rtg); + } } } } @@ -494,11 +869,11 @@ template void VoiceManager::routeMIDIPitchBend(int16_t port, int16_t channel, int16_t pb14bit) { - if (dialect == MIDI1) + if (dialect == MIDI1Dialect::MIDI1) { details.doMonoPitchBend(port, channel, pb14bit); } - else if (dialect == MIDI1_MPE) + else if (dialect == MIDI1Dialect::MIDI1_MPE) { if (channel == mpeGlobalChannel) { @@ -586,11 +961,11 @@ template void VoiceManager::routeChannelPressure(int16_t port, int16_t channel, int8_t pat) { - if (dialect == MIDI1) + if (dialect == MIDI1Dialect::MIDI1) { details.doMonoChannelPressure(port, channel, pat); } - else if (dialect == MIDI1_MPE) + else if (dialect == MIDI1Dialect::MIDI1_MPE) { if (channel == mpeGlobalChannel) { @@ -607,7 +982,7 @@ template void VoiceManager::routeMIDI1CC(int16_t port, int16_t channel, int8_t cc, int8_t val) { - if (dialect == MIDI1_MPE && channel != mpeGlobalChannel && cc == mpeTimbreCC) + if (dialect == MIDI1Dialect::MIDI1_MPE && channel != mpeGlobalChannel && cc == mpeTimbreCC) { for (auto &vi : details.voiceInfo) { @@ -653,24 +1028,31 @@ template void VoiceManager::setPolyphonyGroupVoiceLimit(uint64_t groupId, int32_t limit) { + details.guaranteeGroup(groupId); details.polyLimits[groupId] = limit; - if (details.usedVoices.find(groupId) == details.usedVoices.end()) - details.usedVoices[groupId] = 0; - if (details.stealingPriorityMode.find(groupId) == details.stealingPriorityMode.end()) - details.stealingPriorityMode[groupId] = OLDEST; } template -void VoiceManager::setPlaymode(uint64_t groupId, PlayMode pm) +void VoiceManager::setPlaymode(uint64_t groupId, PlayMode pm, + uint64_t features) { + details.guaranteeGroup(groupId); + details.playMode[groupId] = pm; + details.playModeFeatures[groupId] = features; } template void VoiceManager::setStealingPriorityMode(uint64_t groupId, StealingPriorityMode pm) { + details.guaranteeGroup(groupId); details.stealingPriorityMode[groupId] = pm; } +template +void VoiceManager::guaranteeGroup(uint64_t groupId) +{ + details.guaranteeGroup(groupId); +} } // namespace sst::voicemanager #endif // VOICEMANAGER_IMPL_H diff --git a/tests/mono_playback.cpp b/tests/mono_playback.cpp index 1a653ee..7d46206 100644 --- a/tests/mono_playback.cpp +++ b/tests/mono_playback.cpp @@ -18,6 +18,482 @@ #include "sst/voicemanager/voicemanager.h" #include "test_player.h" -TEST_CASE("Mono Mode - Voices") { REQUIRE_INCOMPLETE_TEST; } +TEST_CASE("Mono Mode - Single key releases not terminates") +{ + auto tp = TestPlayer<32, false>(); + typedef TestPlayer<32, false>::voiceManager_t vm_t; + auto &vm = tp.voiceManager; -TEST_CASE("Mono Mode - Notes") { REQUIRE_INCOMPLETE_TEST; } \ No newline at end of file + vm.setPlaymode(0, vm_t::PlayMode::MONO_NOTES, + (uint64_t)vm_t::MonoPlayModeFeatures::NATURAL_MONO); + + REQUIRE_NO_VOICES; + + vm.processNoteOnEvent(0, 0, 60, -1, 0.8, 0); + REQUIRE_VOICE_COUNTS(1, 1); + vm.processNoteOffEvent(0, 0, 60, -1, 0.8); + REQUIRE_VOICE_COUNTS(1, 0); + tp.processFor(10); + REQUIRE_VOICE_COUNTS(0, 0); +} +TEST_CASE("Mono Mode - Single Layer") +{ + auto tp = TestPlayer<32, false>(); + typedef TestPlayer<32, false>::voiceManager_t vm_t; + auto &vm = tp.voiceManager; + + REQUIRE_NO_VOICES; + vm.setPlaymode(0, vm_t::PlayMode::MONO_NOTES, + (uint64_t)vm_t::MonoPlayModeFeatures::NATURAL_MONO); + + vm.processNoteOnEvent(0, 0, 60, -1, 0.8, 0.0); + + REQUIRE_VOICE_COUNTS(1, 1); + REQUIRE_VOICE_MATCH(1, v.key() == 60); + REQUIRE_VOICE_MATCH(0, v.key() != 60); + + vm.processNoteOnEvent(0, 0, 62, -1, 0.8, 0.0); + REQUIRE_VOICE_COUNTS(1, 1); + REQUIRE_VOICE_MATCH(1, v.key() == 62); + REQUIRE_VOICE_MATCH(0, v.key() != 62); + + vm.processNoteOffEvent(0, 0, 62, -1, 0.0); + REQUIRE_VOICE_COUNTS(1, 1); + REQUIRE_VOICE_MATCH(1, v.key() == 60); + REQUIRE_VOICE_MATCH(0, v.key() != 60); + + vm.processNoteOffEvent(0, 0, 60, -1, 0.0); + REQUIRE_VOICE_COUNTS(1, 0); + tp.processFor(10); + REQUIRE_NO_VOICES; +} + +TEST_CASE("Mono Mode - Release Non Playing") +{ + auto tp = TestPlayer<32, false>(); + typedef TestPlayer<32, false>::voiceManager_t vm_t; + auto &vm = tp.voiceManager; + + REQUIRE_NO_VOICES; + vm.setPlaymode(0, vm_t::PlayMode::MONO_NOTES, + (uint64_t)vm_t::MonoPlayModeFeatures::NATURAL_MONO); + + for (int k = 60; k <= 64; ++k) + { + INFO("Launching note " << k); + vm.processNoteOnEvent(0, 0, k, -1, 0.8, 0.0); + REQUIRE_VOICE_COUNTS(1, 1); + REQUIRE_VOICE_MATCH_FN(1, [k](auto &v) { return v.key() == k; }); + } + + for (int k = 60; k < 64; ++k) + { + INFO("Launching note " << k); + vm.processNoteOffEvent(0, 0, k, -1, 0.8); + REQUIRE_VOICE_COUNTS(1, 1); + REQUIRE_VOICE_MATCH_FN(1, [](auto &v) { return v.key() == 64; }); + } + + vm.processNoteOffEvent(0, 0, 64, -1, 0.8); + REQUIRE_VOICE_COUNTS(1, 0); + tp.processFor(10); + REQUIRE_NO_VOICES; +} + +TEST_CASE("Mono Mode - Three Notes, Most Recent (default)") +{ + auto tp = TestPlayer<32, false>(); + typedef TestPlayer<32, false>::voiceManager_t vm_t; + auto &vm = tp.voiceManager; + + REQUIRE_NO_VOICES; + vm.setPlaymode(0, vm_t::PlayMode::MONO_NOTES, + (uint64_t)vm_t::MonoPlayModeFeatures::NATURAL_MONO); + + INFO("Play notes in order, 60, 58, 62"); + vm.processNoteOnEvent(0, 0, 60, -1, 0.8, 0.0); + REQUIRE_VOICE_COUNTS(1, 1); + REQUIRE_VOICE_MATCH(1, v.key() == 60); + REQUIRE_VOICE_MATCH(0, v.key() != 60); + + vm.processNoteOnEvent(0, 0, 58, -1, 0.8, 0.0); + REQUIRE_VOICE_COUNTS(1, 1); + REQUIRE_VOICE_MATCH(1, v.key() == 58); + REQUIRE_VOICE_MATCH(0, v.key() != 58); + + vm.processNoteOnEvent(0, 0, 62, -1, 0.8, 0.0); + REQUIRE_VOICE_COUNTS(1, 1); + REQUIRE_VOICE_MATCH(1, v.key() == 62); + REQUIRE_VOICE_MATCH(0, v.key() != 62); + + INFO("With 62 sounding, releasing it returns to newest, which is 58"); + vm.processNoteOffEvent(0, 0, 62, -1, 0.0); + REQUIRE_VOICE_COUNTS(1, 1); + REQUIRE_VOICE_MATCH(1, v.key() == 58); + REQUIRE_VOICE_MATCH(0, v.key() != 58); + + INFO("And releasing 68 returns to 60"); + vm.processNoteOffEvent(0, 0, 58, -1, 0.0); + REQUIRE_VOICE_COUNTS(1, 1); + REQUIRE_VOICE_MATCH(1, v.key() == 60); + REQUIRE_VOICE_MATCH(0, v.key() != 60); + + INFO("The release of which returns to empty"); + vm.processNoteOffEvent(0, 0, 60, -1, 0.0); + REQUIRE_VOICE_COUNTS(1, 0); + tp.processFor(10); + REQUIRE_NO_VOICES; +} + +TEST_CASE("Mono Mode - Highest Prio") +{ + auto tp = TestPlayer<32, false>(); + typedef TestPlayer<32, false>::voiceManager_t vm_t; + auto &vm = tp.voiceManager; + + REQUIRE_NO_VOICES; + vm.setPlaymode(0, vm_t::PlayMode::MONO_NOTES, + (uint64_t)vm_t::MonoPlayModeFeatures::MONO_RETRIGGER | + (uint64_t)vm_t::MonoPlayModeFeatures::ON_RELEASE_TO_HIGHEST); + + INFO("Play notes in order, 60, 58, 62"); + vm.processNoteOnEvent(0, 0, 60, -1, 0.8, 0.0); + REQUIRE_VOICE_COUNTS(1, 1); + REQUIRE_VOICE_MATCH(1, v.key() == 60); + REQUIRE_VOICE_MATCH(0, v.key() != 60); + + vm.processNoteOnEvent(0, 0, 58, -1, 0.8, 0.0); + REQUIRE_VOICE_COUNTS(1, 1); + REQUIRE_VOICE_MATCH(1, v.key() == 58); + REQUIRE_VOICE_MATCH(0, v.key() != 58); + + vm.processNoteOnEvent(0, 0, 62, -1, 0.8, 0.0); + REQUIRE_VOICE_COUNTS(1, 1); + REQUIRE_VOICE_MATCH(1, v.key() == 62); + REQUIRE_VOICE_MATCH(0, v.key() != 62); + + INFO("With 62 sounding, releasing it returns to highest, which is 60"); + vm.processNoteOffEvent(0, 0, 62, -1, 0.0); + REQUIRE_VOICE_COUNTS(1, 1); + REQUIRE_VOICE_MATCH(1, v.key() == 60); + REQUIRE_VOICE_MATCH(0, v.key() != 60); + + INFO("And releasing 60 returns to 58"); + vm.processNoteOffEvent(0, 0, 60, -1, 0.0); + REQUIRE_VOICE_COUNTS(1, 1); + REQUIRE_VOICE_MATCH(1, v.key() == 58); + REQUIRE_VOICE_MATCH(0, v.key() != 58); + + INFO("The release of which returns to empty"); + vm.processNoteOffEvent(0, 0, 58, -1, 0.0); + REQUIRE_VOICE_COUNTS(1, 0); + tp.processFor(10); + REQUIRE_NO_VOICES; +} +TEST_CASE("Mono Mode - Lowest Prio") +{ + auto tp = TestPlayer<32, false>(); + typedef TestPlayer<32, false>::voiceManager_t vm_t; + auto &vm = tp.voiceManager; + + REQUIRE_NO_VOICES; + vm.setPlaymode(0, vm_t::PlayMode::MONO_NOTES, + (uint64_t)vm_t::MonoPlayModeFeatures::MONO_RETRIGGER | + (uint64_t)vm_t::MonoPlayModeFeatures::ON_RELEASE_TO_LOWEST); + + INFO("Play notes in order, 60, 58, 62"); + + vm.processNoteOnEvent(0, 0, 58, -1, 0.8, 0.0); + REQUIRE_VOICE_COUNTS(1, 1); + REQUIRE_VOICE_MATCH(1, v.key() == 58); + REQUIRE_VOICE_MATCH(0, v.key() != 58); + + vm.processNoteOnEvent(0, 0, 60, -1, 0.8, 0.0); + REQUIRE_VOICE_COUNTS(1, 1); + REQUIRE_VOICE_MATCH(1, v.key() == 60); + REQUIRE_VOICE_MATCH(0, v.key() != 60); + + vm.processNoteOnEvent(0, 0, 62, -1, 0.8, 0.0); + REQUIRE_VOICE_COUNTS(1, 1); + REQUIRE_VOICE_MATCH(1, v.key() == 62); + REQUIRE_VOICE_MATCH(0, v.key() != 62); + + INFO("With 62 sounding, releasing it returns to lowest, which is 58"); + vm.processNoteOffEvent(0, 0, 62, -1, 0.0); + REQUIRE_VOICE_COUNTS(1, 1); + REQUIRE_VOICE_MATCH(1, v.key() == 58); + REQUIRE_VOICE_MATCH(0, v.key() != 58); + + INFO("And releasing 58 returns to 60"); + vm.processNoteOffEvent(0, 0, 58, -1, 0.0); + REQUIRE_VOICE_COUNTS(1, 1); + REQUIRE_VOICE_MATCH(1, v.key() == 60); + REQUIRE_VOICE_MATCH(0, v.key() != 60); + + INFO("The release of which returns to empty"); + vm.processNoteOffEvent(0, 0, 60, -1, 0.0); + REQUIRE_VOICE_COUNTS(1, 0); + tp.processFor(10); + REQUIRE_NO_VOICES; +} + +TEST_CASE("Mono Mode - Two Layers (Duophonic)") +{ + auto tp = TestPlayer<32, false>(); + typedef TestPlayer<32, false>::voiceManager_t vm_t; + auto &vm = tp.voiceManager; + + INFO("Put EVEN and ODD keys in different monophonic groups"); + tp.polyGroupForKey = [](auto k) { return (k % 2 == 0 ? 1477 : 1832); }; + vm.setPlaymode(1477, vm_t::PlayMode::MONO_NOTES, + (uint64_t)vm_t::MonoPlayModeFeatures::NATURAL_MONO); + vm.setPlaymode(1832, vm_t::PlayMode::MONO_NOTES, + (uint64_t)vm_t::MonoPlayModeFeatures::NATURAL_MONO); + + REQUIRE_NO_VOICES; + vm.processNoteOnEvent(0, 0, 60, -1, 0.8, 0.0); + REQUIRE_VOICE_COUNTS(1, 1); + REQUIRE_VOICE_MATCH(1, v.key() == 60); + REQUIRE_VOICE_MATCH(0, v.key() != 60); + + vm.processNoteOnEvent(0, 0, 61, -1, 0.8, 0.0); + REQUIRE_VOICE_COUNTS(2, 2); + REQUIRE_VOICE_MATCH(1, v.key() == 60); + REQUIRE_VOICE_MATCH(1, v.key() == 61); + + vm.processNoteOnEvent(0, 0, 64, -1, 0.8, 0.0); + REQUIRE_VOICE_COUNTS(2, 2); + REQUIRE_VOICE_MATCH(1, v.key() == 64); + REQUIRE_VOICE_MATCH(1, v.key() == 61); + + vm.processNoteOnEvent(0, 0, 63, -1, 0.8, 0.0); + REQUIRE_VOICE_COUNTS(2, 2); + REQUIRE_VOICE_MATCH(1, v.key() == 64); + REQUIRE_VOICE_MATCH(1, v.key() == 63); + + vm.processNoteOffEvent(0, 0, 64, -1, 0.9); + REQUIRE_VOICE_COUNTS(2, 2); + REQUIRE_VOICE_MATCH(1, v.key() == 60); + REQUIRE_VOICE_MATCH(1, v.key() == 63); + + INFO("The 60 release should put one of the groups in release state"); + vm.processNoteOffEvent(0, 0, 60, -1, 0.9); + REQUIRE_VOICE_COUNTS(2, 1); + REQUIRE_VOICE_MATCH(1, v.key() == 63); + REQUIRE_VOICE_MATCH(1, v.key() == 60); + tp.processFor(10); + REQUIRE_VOICE_COUNTS(1, 1); + REQUIRE_VOICE_MATCH(1, v.key() == 63); + + vm.processNoteOffEvent(0, 0, 63, -1, 0.9); + REQUIRE_VOICE_COUNTS(1, 1); + REQUIRE_VOICE_MATCH(1, v.key() == 61); + + vm.processNoteOffEvent(0, 0, 61, -1, 0.9); + REQUIRE_VOICE_COUNTS(1, 0); + tp.processFor(10); + REQUIRE_NO_VOICES; +} + +TEST_CASE("Mono Mode - Sustain Pedal") +{ + SECTION("Release with Gated When Releasing") + { + auto tp = TestPlayer<32, false>(); + typedef TestPlayer<32, false>::voiceManager_t vm_t; + auto &vm = tp.voiceManager; + + vm.setPlaymode(0, vm_t::PlayMode::MONO_NOTES, + (uint64_t)vm_t::MonoPlayModeFeatures::NATURAL_MONO); + REQUIRE_NO_VOICES; + + vm.updateSustainPedal(0, 0, 127); + REQUIRE_NO_VOICES; + INFO("PLay 60 launches 60"); + vm.processNoteOnEvent(0, 0, 60, -1, 0.8, 0); + REQUIRE_VOICE_MATCH(1, v.key() == 60); + + INFO("Mono move to 62 gets us there"); + vm.processNoteOnEvent(0, 0, 62, -1, 0.8, 0); + REQUIRE_VOICE_MATCH(1, v.key() == 62); + + INFO("The release of 62 jumps back to 60 because it is still held as a gated key"); + vm.processNoteOffEvent(0, 0, 62, -1, 0.8); + REQUIRE_VOICE_MATCH(1, v.key() == 60); + + INFO("THe release of 60 keeps it playing due to pedal"); + vm.processNoteOffEvent(0, 0, 60, -1, 0.8); + REQUIRE_VOICE_MATCH(1, v.key() == 60); + + INFO("Release the pedal, kill the voices"); + vm.updateSustainPedal(0, 0, 0); + REQUIRE_NO_VOICES + } + + SECTION("Release when non-gated but pedal held (surge 6620)") + { + auto tp = TestPlayer<32, false>(); + typedef TestPlayer<32, false>::voiceManager_t vm_t; + auto &vm = tp.voiceManager; + + vm.setPlaymode(0, vm_t::PlayMode::MONO_NOTES, + (uint64_t)vm_t::MonoPlayModeFeatures::NATURAL_MONO); + REQUIRE_NO_VOICES; + + vm.updateSustainPedal(0, 0, 127); + REQUIRE_NO_VOICES; + INFO("Note 60 gives us note 60"); + vm.processNoteOnEvent(0, 0, 60, -1, 0.8, 0); + REQUIRE_VOICE_MATCH(1, v.key() == 60); + + INFO("The release of 60 doesn't do anything musically"); + vm.processNoteOffEvent(0, 0, 60, -1, 0.8); + REQUIRE_VOICE_MATCH(1, v.key() == 60); + + INFO("The press of 62 moves you to 62"); + vm.processNoteOnEvent(0, 0, 62, -1, 0.8, 0.f); + REQUIRE_VOICE_COUNTS(1, 1); + REQUIRE_VOICE_MATCH(1, v.key() == 62); + + INFO("The release of 62 keeys you on 62 since nothing is gated"); + vm.processNoteOffEvent(0, 0, 62, -1, 0.8); + REQUIRE_VOICE_MATCH(1, v.key() == 62); + + INFO("The release of the pdeal silences us"); + vm.updateSustainPedal(0, 0, 0); + REQUIRE_NO_VOICES + } +} + +TEST_CASE("Mono Mode - Two Layers, One Poly") +{ + auto tp = TestPlayer<32, true>(); + typedef TestPlayer<32, true>::voiceManager_t vm_t; + auto &vm = tp.voiceManager; + + INFO("Put EVEN and ODD keys in different groups, even poly odd mono"); + tp.polyGroupForKey = [](auto k) { return (k % 2 == 0 ? 19884 : 8675309); }; + vm.setPlaymode(19884, vm_t::PlayMode::POLY_VOICES); + vm.setPlaymode(8675309, vm_t::PlayMode::MONO_NOTES, + (uint64_t)vm_t::MonoPlayModeFeatures::NATURAL_MONO); + + INFO("Play three poly voices"); + for (int k = 60; k < 65; k += 2) + { + vm.processNoteOnEvent(0, 0, k, -1, 0.8, 0.0); + } + REQUIRE_VOICE_COUNTS(3, 3); + + INFO("Play note 61 for an extra voice"); + vm.processNoteOnEvent(0, 0, 61, -1, 0.8, 0.0); + REQUIRE_VOICE_COUNTS(4, 4); + REQUIRE_VOICE_MATCH(1, v.key() == 61); + + INFO("Mono move that to 63"); + vm.processNoteOnEvent(0, 0, 63, -1, 0.8, 0.0); + REQUIRE_VOICE_COUNTS(4, 4); + REQUIRE_VOICE_MATCH(1, v.key() == 63); + + INFO("Release a poly voice"); + vm.processNoteOffEvent(0, 0, 62, -1, 0.8); + REQUIRE_VOICE_COUNTS(4, 3); // remember that poly voice is in release mode + + INFO("Release the 63 to go back to 61"); + vm.processNoteOffEvent(0, 0, 63, -1, 0.8); + REQUIRE_VOICE_COUNTS(4, 3); // remember that poly voice is in release mode + tp.processFor(10); + REQUIRE_VOICE_COUNTS(3, 3); // and that poly voice is no longer released + + INFO("Release the 61 and you get a release mono voice"); + vm.processNoteOffEvent(0, 0, 61, -1, 0.8); + REQUIRE_VOICE_COUNTS(3, 2); + REQUIRE_VOICE_MATCH(1, v.key() == 61); + tp.processFor(10); + REQUIRE_VOICE_COUNTS(2, 2); + REQUIRE_VOICE_MATCH(0, v.key() == 61); +} + +TEST_CASE("Mono terminates a non-gated release voice") +{ + SECTION("Case one - different key") + { + auto tp = TestPlayer<32, false>(); + typedef TestPlayer<32, false>::voiceManager_t vm_t; + auto &vm = tp.voiceManager; + + vm.setPlaymode(0, vm_t::PlayMode::MONO_NOTES, + (uint64_t)vm_t::MonoPlayModeFeatures::NATURAL_MONO); + + REQUIRE_NO_VOICES; + + vm.processNoteOnEvent(0, 0, 60, -1, 0.8, 0); + REQUIRE_VOICE_COUNTS(1, 1); + REQUIRE_VOICE_MATCH(1, v.key() == 60); + tp.processFor(4); + vm.processNoteOffEvent(0, 0, 60, -1, 0.8); + REQUIRE_VOICE_COUNTS(1, 0); + REQUIRE_VOICE_MATCH(1, v.key() == 60); + tp.processFor(2); + REQUIRE_VOICE_COUNTS(1, 0); + REQUIRE_VOICE_MATCH(1, v.key() == 60); + vm.processNoteOnEvent(0, 0, 64, -1, 0.8, 0); + REQUIRE_VOICE_COUNTS(1, 1); + REQUIRE_VOICE_MATCH(0, v.key() == 60); + REQUIRE_VOICE_MATCH(1, v.key() == 64); + } + + SECTION("Case one - same key") + { + auto tp = TestPlayer<32, false>(); + typedef TestPlayer<32, false>::voiceManager_t vm_t; + auto &vm = tp.voiceManager; + + vm.setPlaymode(0, vm_t::PlayMode::MONO_NOTES, + (uint64_t)vm_t::MonoPlayModeFeatures::NATURAL_MONO); + + REQUIRE_NO_VOICES; + + vm.processNoteOnEvent(0, 0, 60, -1, 0.8, 0); + REQUIRE_VOICE_COUNTS(1, 1); + REQUIRE_VOICE_MATCH(1, v.key() == 60); + tp.processFor(4); + vm.processNoteOffEvent(0, 0, 60, -1, 0.8); + REQUIRE_VOICE_COUNTS(1, 0); + REQUIRE_VOICE_MATCH(1, v.key() == 60); + tp.processFor(2); + REQUIRE_VOICE_COUNTS(1, 0); + REQUIRE_VOICE_MATCH(1, v.key() == 60); + vm.processNoteOnEvent(0, 0, 60, -1, 0.8, 0); + REQUIRE_VOICE_COUNTS(1, 1); + REQUIRE_VOICE_MATCH(1, v.key() == 60); + } +} + +TEST_CASE("Mono Mode - Poly and Mono on same key with multi-voice start") +{ + // A special player which for every key makes 2 voices, one in + // group 2112, one in group 90125 + auto tp = TwoGroupsEveryKey<32, true>(); + typedef TwoGroupsEveryKey<32, true>::voiceManager_t vm_t; + auto &vm = tp.voiceManager; + + vm.setPlaymode(2112, vm_t::PlayMode::MONO_NOTES, + (uint64_t)vm_t::MonoPlayModeFeatures::NATURAL_MONO); + + REQUIRE_NO_VOICES; + vm.processNoteOnEvent(0, 0, 60, -1, 0.8, 0.); + REQUIRE_VOICE_COUNTS(2, 2); + REQUIRE_VOICE_MATCH(2, v.key() == 60); + tp.processFor(1); + + vm.processNoteOnEvent(0, 0, 62, -1, 0.8, 0.); + REQUIRE_VOICE_COUNTS(3, 3); + REQUIRE_VOICE_MATCH(1, v.key() == 60); + REQUIRE_VOICE_MATCH(2, v.key() == 62); + tp.processFor(1); + + vm.processNoteOffEvent(0, 0, 62, -1, 0.0); + REQUIRE_VOICE_COUNTS(3, 2); + REQUIRE_VOICE_MATCH(2, v.key() == 60); + REQUIRE_VOICE_MATCH(1, v.key() == 62); +} diff --git a/tests/mpe_tests.cpp b/tests/mpe_tests.cpp index 630a4db..db7363d 100644 --- a/tests/mpe_tests.cpp +++ b/tests/mpe_tests.cpp @@ -22,7 +22,7 @@ TEST_CASE("MPE Basic") { auto tp = TestPlayer<32, false>(); auto &vm = tp.voiceManager; - vm.dialect = TestPlayer<32, false>::voiceManager_t::MIDI1_MPE; + vm.dialect = TestPlayer<32, false>::voiceManager_t::MIDI1Dialect::MIDI1_MPE; REQUIRE_NO_VOICES; @@ -64,7 +64,7 @@ TEST_CASE("MPE After Release") auto tp = TestPlayer<32, false>(); auto &vm = tp.voiceManager; - vm.dialect = TestPlayer<32, false>::voiceManager_t::MIDI1_MPE; + vm.dialect = TestPlayer<32, false>::voiceManager_t::MIDI1Dialect::MIDI1_MPE; REQUIRE_NO_VOICES; diff --git a/tests/piano_mode.cpp b/tests/piano_mode.cpp index 97753a8..a3f11f7 100644 --- a/tests/piano_mode.cpp +++ b/tests/piano_mode.cpp @@ -24,7 +24,7 @@ TEST_CASE("Poly Multi Key Piano Mode") { auto tp = TestPlayer<32>(); auto &vm = tp.voiceManager; - vm.repeatedKeyMode = TestPlayer<32>::voiceManager_t::PIANO; + vm.repeatedKeyMode = TestPlayer<32>::voiceManager_t::RepeatedKeyMode::PIANO; REQUIRE_NO_VOICES; @@ -43,7 +43,7 @@ TEST_CASE("Poly Multi Key Piano Mode") { auto tp = TestPlayer<32>(); auto &vm = tp.voiceManager; - vm.repeatedKeyMode = TestPlayer<32>::voiceManager_t::PIANO; + vm.repeatedKeyMode = TestPlayer<32>::voiceManager_t::RepeatedKeyMode::PIANO; REQUIRE_NO_VOICES; @@ -65,7 +65,7 @@ TEST_CASE("Poly Multi Key Non Piano Mode") { auto tp = TestPlayer<32>(); auto &vm = tp.voiceManager; - REQUIRE(vm.repeatedKeyMode == TestPlayer<32>::voiceManager_t::MULTI_VOICE); + REQUIRE(vm.repeatedKeyMode == TestPlayer<32>::voiceManager_t::RepeatedKeyMode::MULTI_VOICE); REQUIRE_NO_VOICES; @@ -90,7 +90,7 @@ TEST_CASE("Poly Multi Key Non Piano Mode") { auto tp = TestPlayer<32>(); auto &vm = tp.voiceManager; - REQUIRE(vm.repeatedKeyMode == TestPlayer<32>::voiceManager_t::MULTI_VOICE); + REQUIRE(vm.repeatedKeyMode == TestPlayer<32>::voiceManager_t::RepeatedKeyMode::MULTI_VOICE); REQUIRE_NO_VOICES; diff --git a/tests/routing_params.cpp b/tests/routing_params.cpp index b032144..8bddbc6 100644 --- a/tests/routing_params.cpp +++ b/tests/routing_params.cpp @@ -24,7 +24,7 @@ TEST_CASE("Routing Midi CC") auto &vm = tp.voiceManager; REQUIRE_NO_VOICES; - REQUIRE(vm.dialect == TestPlayer<32>::voiceManager_t::MIDI1); + REQUIRE(vm.dialect == TestPlayer<32>::voiceManager_t::MIDI1Dialect::MIDI1); // Test one: Does routing midi cc on cc 0 and 6 stay independent vm.routeMIDI1CC(0, 0, 0, 17); diff --git a/tests/stealing_priorities.cpp b/tests/stealing_priorities.cpp index 3bb4a83..a82894e 100644 --- a/tests/stealing_priorities.cpp +++ b/tests/stealing_priorities.cpp @@ -65,7 +65,8 @@ TEST_CASE("Stealing Priority - Highest") auto &vm = tp.voiceManager; vm.setPolyphonyGroupVoiceLimit(0, 4); - vm.setStealingPriorityMode(0, TestPlayer<32>::voiceManager_t::HIGHEST); + vm.setStealingPriorityMode(0, + TestPlayer<32>::voiceManager_t::StealingPriorityMode::HIGHEST); for (int i = 0; i < 4; ++i) { vm.processNoteOnEvent(0, 0, 60 + i, -1, 0.8, 0.0); @@ -84,7 +85,8 @@ TEST_CASE("Stealing Priority - Highest") auto &vm = tp.voiceManager; vm.setPolyphonyGroupVoiceLimit(0, lim); - vm.setStealingPriorityMode(0, TestPlayer<32>::voiceManager_t::HIGHEST); + vm.setStealingPriorityMode( + 0, TestPlayer<32>::voiceManager_t::StealingPriorityMode::HIGHEST); for (int i = 0; i < 4; ++i) { @@ -107,7 +109,7 @@ TEST_CASE("Stealing Priority - Lowest") auto &vm = tp.voiceManager; vm.setPolyphonyGroupVoiceLimit(0, 4); - vm.setStealingPriorityMode(0, TestPlayer<32>::voiceManager_t::LOWEST); + vm.setStealingPriorityMode(0, TestPlayer<32>::voiceManager_t::StealingPriorityMode::LOWEST); for (int i = 0; i < 4; ++i) { vm.processNoteOnEvent(0, 0, 60 - i, -1, 0.8, 0.0); @@ -126,7 +128,8 @@ TEST_CASE("Stealing Priority - Lowest") auto &vm = tp.voiceManager; vm.setPolyphonyGroupVoiceLimit(0, lim); - vm.setStealingPriorityMode(0, TestPlayer<32>::voiceManager_t::LOWEST); + vm.setStealingPriorityMode( + 0, TestPlayer<32>::voiceManager_t::StealingPriorityMode::LOWEST); for (int i = 0; i < 4; ++i) { diff --git a/tests/test_player.h b/tests/test_player.h index 2949c05..c7aaf5c 100644 --- a/tests/test_player.h +++ b/tests/test_player.h @@ -99,49 +99,17 @@ template struct TestPlayer testPlayer.voiceEndCallback = f; } - int32_t - initializeMultipleVoices(typename sst::voicemanager::VoiceInitBufferEntry::buffer_t - &voiceInitWorkingBuffer, - uint16_t port, uint16_t channel, uint16_t key, int32_t noteId, - float velocity, float retune) + int32_t initializeMultipleVoices( + int32_t voices, + const typename sst::voicemanager::VoiceInitInstructionsEntry::buffer_t + &voiceInitInstructionBuffer, + typename sst::voicemanager::VoiceInitBufferEntry::buffer_t + &voiceInitWorkingBuffer, + uint16_t port, uint16_t channel, uint16_t key, int32_t noteId, float velocity, + float retune) { - TPF; - Voice *nv[3]{nullptr, nullptr, nullptr}; - int idx{0}; - int midx{key <= 72 ? 1 : 3}; - - for (auto &v : testPlayer.voiceStorage) - { - if (v.state != Voice::ACTIVE) - { - nv[idx] = &v; - TPT("Assigning voice at " << idx << " from " << &v); - ++idx; - if (idx == midx) - break; - } - } - - if (idx != midx) - return 0; - - for (int i = 0; i < midx; ++i) - { - auto v = nv[i]; - if (!v) - continue; - v->state = Voice::ACTIVE; - v->runtime = 0; - v->isGated = true; - v->pckn = {port, channel, key, noteId}; - v->velocity = velocity; - - voiceInitWorkingBuffer[i].voice = v; - TPT(" Set voice at " << i << " voices " << testPlayer.pcknToString(v->pckn)); - } - - TPT("Created " << midx << " voices "); - return midx; + return testPlayer.initFn(voices, voiceInitInstructionBuffer, voiceInitWorkingBuffer, + port, channel, key, noteId, velocity, retune); } void terminateVoice(Voice *v) @@ -170,21 +138,7 @@ template struct TestPlayer uint16_t channel, uint16_t key, int32_t noteid, float velocity) { TPF; - auto res{1}; - if (key > 72) - res = 3; - - for (int i = 0; i < res; ++i) - { - if (testPlayer.polyGroupForKey) - { - buf[i].polyphonyGroup = testPlayer.polyGroupForKey(key); - TPT(TPD(i) << TPD(buf[i].polyphonyGroup)); - } - else - buf[i].polyphonyGroup = 0; - } - return res; + return testPlayer.beginFn(buf, port, channel, key, noteid, velocity); } void endVoiceCreationTransaction(uint16_t port, uint16_t channel, uint16_t key, int32_t noteid, float velocity) @@ -225,6 +179,78 @@ template struct TestPlayer } responder; + /* + * Make these virtual for test purposes. You probably wouldn't do this in the wild + * and virutal and templates together suck and stuff etc + */ + virtual int beginFn(typename sst::voicemanager::VoiceBeginBufferEntry::buffer_t &buf, + uint16_t port, uint16_t channel, uint16_t key, int32_t noteid, + float velocity) + { + auto res{1}; + if (key > 72) + res = 3; + + for (int i = 0; i < res; ++i) + { + if (polyGroupForKey) + { + buf[i].polyphonyGroup = polyGroupForKey(key); + TPT(TPD(i) << TPD(buf[i].polyphonyGroup)); + } + else + buf[i].polyphonyGroup = 0; + } + return res; + } + + virtual int32_t initFn( + int32_t voices, + const typename sst::voicemanager::VoiceInitInstructionsEntry::buffer_t + &voiceInitInstructionBuffer, + typename sst::voicemanager::VoiceInitBufferEntry::buffer_t &voiceInitWorkingBuffer, + uint16_t port, uint16_t channel, uint16_t key, int32_t noteId, float velocity, float retune) + { + TPF; + Voice *nv[3]{nullptr, nullptr, nullptr}; + int idx{0}; + int midx{key <= 72 ? 1 : 3}; + assert(voices == midx); + + for (auto &v : voiceStorage) + { + if (v.state != Voice::ACTIVE) + { + nv[idx] = &v; + TPT("Assigning voice at " << idx << " from " << &v); + ++idx; + if (idx == midx) + break; + } + } + + if (idx != midx) + return 0; + + for (int i = 0; i < midx; ++i) + { + auto v = nv[i]; + if (!v) + continue; + v->state = Voice::ACTIVE; + v->runtime = 0; + v->isGated = true; + v->pckn = {port, channel, key, noteId}; + v->velocity = velocity; + + voiceInitWorkingBuffer[i].voice = v; + TPT(" Set voice at " << i << " voices " << pcknToString(v->pckn)); + } + + TPT("Created " << midx << " voices "); + return midx; + } + struct MonoResponder { TestPlayer &testPlayer; @@ -392,6 +418,72 @@ template struct TestPlayer } }; +template +struct TwoGroupsEveryKey : TestPlayer +{ + TwoGroupsEveryKey() + { + this->voiceManager.guaranteeGroup(2112); + this->voiceManager.guaranteeGroup(90125); + } + int beginFn(typename sst::voicemanager::VoiceBeginBufferEntry< + typename TestPlayer::Config>::buffer_t &buf, + uint16_t port, uint16_t channel, uint16_t key, int32_t noteid, + float velocity) override + { + buf[0].polyphonyGroup = 2112; + buf[1].polyphonyGroup = 90125; + return 2; + } + int32_t initFn( + int voices, + const typename sst::voicemanager::VoiceInitInstructionsEntry< + typename TestPlayer::Config>::buffer_t &voiceInitInstructionBuffer, + typename sst::voicemanager::VoiceInitBufferEntry< + typename TestPlayer::Config>::buffer_t &voiceInitWorkingBuffer, + uint16_t port, uint16_t channel, uint16_t key, int32_t noteId, float velocity, + float retune) override + { + assert(voices == 2); + typename TestPlayer::Voice *nv[2]{nullptr, nullptr}; + + int idx = 0; + for (auto &v : this->voiceStorage) + { + if (v.state != TestPlayer::Voice::ACTIVE) + { + nv[idx] = &v; + TPT("Assigning voice at " << idx << " from " << &v); + ++idx; + if (idx == 2) + break; + } + } + + idx = 0; + for (int i = 0; i < voices; ++i) + { + if (voiceInitInstructionBuffer[i].instruction == + TestPlayer::voiceManager_t::initInstruction_t::Instruction::SKIP) + continue; + auto v = nv[idx]; + idx++; + if (!v) + continue; + v->state = TestPlayer::Voice::ACTIVE; + v->runtime = 0; + v->isGated = true; + v->pckn = {port, channel, key, noteId}; + v->velocity = velocity; + + voiceInitWorkingBuffer[i].voice = v; + TPT(" Set voice at " << i << " voices " << this->pcknToString(v->pckn)); + } + + return idx; + } +}; + #undef TPT #undef TPF #undef TPD @@ -410,6 +502,11 @@ template struct TestPlayer REQUIRE(tp.getActiveVoicePCKNS().empty()); \ REQUIRE(tp.getGatedVoicePCKNS().empty()); +#define REQUIRE_VOICE_MATCH(ct, ...) \ + REQUIRE(tp.activeVoicesMatching([](auto &v) { return __VA_ARGS__; }) == ct) + +#define REQUIRE_VOICE_MATCH_FN(ct, ...) REQUIRE(tp.activeVoicesMatching(__VA_ARGS__) == ct) + // #define REQUIRE_INCOMPLETE_TEST REQUIRE(false) #define REQUIRE_INCOMPLETE_TEST INFO("This test is currently incomplete");