diff --git a/src/common/SurgePatch.cpp b/src/common/SurgePatch.cpp index 9a88e5da5d5..bd2e96633a3 100644 --- a/src/common/SurgePatch.cpp +++ b/src/common/SurgePatch.cpp @@ -1326,6 +1326,26 @@ void SurgePatch::load_xml(const void* data, int datasize, bool is_preset) } } + TiXmlElement* nonparamconfig = TINYXML_SAFE_TO_ELEMENT(patch->FirstChild("nonparamconfig")); + if( nonparamconfig ) + { + for( int sc=0; sc < n_scenes; ++sc ) + { + std::string mvname = "monoVoicePrority_" + std::to_string(sc); + auto *mv1 = TINYXML_SAFE_TO_ELEMENT(nonparamconfig->FirstChild( mvname.c_str() )); + storage->getPatch().scene[sc].monoVoicePriorityMode = ALWAYS_LATEST; + if( mv1 ) + { + // Get value + int mvv; + if( mv1->QueryIntAttribute("v", &mvv ) == TIXML_SUCCESS ) + { + storage->getPatch().scene[sc].monoVoicePriorityMode = (MonoVoicePriorityMode)mvv; + } + } + } + } + if (revision < 1) { for (int sc = 0; sc < n_scenes; sc++) @@ -1463,6 +1483,7 @@ void SurgePatch::load_xml(const void* data, int datasize, bool is_preset) // The Great Filter Remap of issue #3006 for (auto& sc : scene) { + sc.monoVoicePriorityMode = NOTE_ON_LATEST_RETRIGGER_HIGHEST; // Older patches use Legacy mode for (int u = 0; u < n_filterunits_per_scene; u++) { auto* fu = &(sc.filterunit[u]); @@ -2005,6 +2026,17 @@ unsigned int SurgePatch::save_xml(void** data) // allocates mem, must be freed b } patch.InsertEndChild(parameters); + // TODO: Stream that priority mode here + TiXmlElement nonparamconfig( "nonparamconfig" ); + for( int sc=0; sc < n_scenes; ++sc ) + { + std::string mvname = "monoVoicePrority_" + std::to_string(sc); + TiXmlElement mvv(mvname.c_str()); + mvv.SetAttribute("v", storage->getPatch().scene[sc].monoVoicePriorityMode); + nonparamconfig.InsertEndChild(mvv); + } + patch.InsertEndChild(nonparamconfig); + TiXmlElement eod( "extraoscdata" ); for (int sc = 0; sc < n_scenes; ++sc) { diff --git a/src/common/SurgeStorage.h b/src/common/SurgeStorage.h index 9373f9c4e1e..4e4365844d9 100644 --- a/src/common/SurgeStorage.h +++ b/src/common/SurgeStorage.h @@ -440,10 +440,35 @@ const char em_names[n_env_modes][16] = "Analog", }; + +/* + * How does the sustain pedal work in mono mode? Current modes for this are + * + * HOLD_ALL_NOTES (the default). If you release a note with the pedal down + * it does not release + * + * RELEASE_IF_OTHERS_HELD. If you release a note, and no other notes are down, + * do not release. But if you release and another note is down, return to that + * note (basically allow sustain pedal trills). + */ +enum MonoPedalMode { + HOLD_ALL_NOTES, + RELEASE_IF_OTHERS_HELD +}; + +enum MonoVoicePriorityMode { + NOTE_ON_LATEST_RETRIGGER_HIGHEST, // The legacy mode for 1.7.1 and earlier + ALWAYS_LATEST, // Could also be called "NOTE_ON_LATEST_RETRIGGER_LATEST" + ALWAYS_HIGHEST, + ALWAYS_LOWEST, +}; + + struct MidiKeyState { int keystate; char lastdetune; + int64_t voiceOrder; }; struct MidiChannelState @@ -548,6 +573,8 @@ struct SurgeSceneStorage std::vector modsources; bool modsource_doprocess[n_modsources]; + + MonoVoicePriorityMode monoVoicePriorityMode = ALWAYS_LATEST; }; const int n_stepseqsteps = 16; @@ -627,6 +654,8 @@ struct MSEGStorage { struct FormulaModulatorStorage { // Currently an unused placeholder }; + + /* ** There are a collection of things we want your DAW to save about your particular instance ** but don't want saved in your patch. So have this extra structure in the patch which we @@ -793,21 +822,6 @@ enum surge_copysource n_copysources, }; -/* - * How does the sustain pedal work in mono mode? Current modes for this are - * - * HOLD_ALL_NOTES (the default). If you release a note with the pedal down - * it does not release - * - * RELEASE_IF_OTHERS_HELD. If you release a note, and no other notes are down, - * do not release. But if you release and another note is down, return to that - * note (basically allow sustain pedal trills). - */ -enum MonoPedalMode { - HOLD_ALL_NOTES, - RELEASE_IF_OTHERS_HELD -}; - /* STORAGE layer */ diff --git a/src/common/SurgeSynthesizer.cpp b/src/common/SurgeSynthesizer.cpp index 05bbea0b8f4..a4f907d5ee2 100644 --- a/src/common/SurgeSynthesizer.cpp +++ b/src/common/SurgeSynthesizer.cpp @@ -570,7 +570,8 @@ void SurgeSynthesizer::playVoice(int scene, char channel, char key, char velocit new (nvoice) SurgeVoice(&storage, &storage.getPatch().scene[scene], storage.getPatch().scenedata[scene], key, velocity, channel, scene, detune, &channelState[channel].keyState[key], - &channelState[mpeMainChannel], &channelState[channel], mpeEnabled); + &channelState[mpeMainChannel], &channelState[channel], + mpeEnabled, voiceCounter++ ); } break; } @@ -581,30 +582,76 @@ void SurgeSynthesizer::playVoice(int scene, char channel, char key, char velocit list::const_iterator iter; bool glide = false; - for (iter = voices[scene].begin(); iter != voices[scene].end(); iter++) + int primode = storage.getPatch().scene[scene].monoVoicePriorityMode; + bool createVoice = true; + if( primode == ALWAYS_HIGHEST || primode == ALWAYS_LOWEST ) { - SurgeVoice* v = *iter; - if (v->state.scene_id == scene) + /* + * There is a chance we don't want to make a voice + */ + if( mpeEnabled ) { - if (v->state.gate) + for( int k=0; k<128; ++k ) { - glide = true; + for( int mpeChan=0; mpeChan < 16; ++mpeChan ) + { + if (channelState[mpeChan].keyState[k].keystate) + { + if (primode == ALWAYS_HIGHEST && k > key) + createVoice = false; + if (primode == ALWAYS_LOWEST && k < key) + createVoice = false; + } + } + } + } + else + { + for( int k=0; k<128; ++k ) + { + if( channelState[channel].keyState[k].keystate ) + { + if( primode == ALWAYS_HIGHEST && k > key ) createVoice = false; + if( primode == ALWAYS_LOWEST && k < key ) createVoice = false; + } } - v->uber_release(); } } - SurgeVoice* nvoice = getUnusedVoice(scene); - if (nvoice) + + if( createVoice ) { - int mpeMainChannel = getMpeMainChannel(channel, key); + for (iter = voices[scene].begin(); iter != voices[scene].end(); iter++) + { + SurgeVoice* v = *iter; + if (v->state.scene_id == scene) + { + if (v->state.gate) + { + glide = true; + } + v->uber_release(); + } + } + SurgeVoice* nvoice = getUnusedVoice(scene); + if (nvoice) + { + int mpeMainChannel = getMpeMainChannel(channel, key); - voices[scene].push_back(nvoice); - if ((storage.getPatch().scene[scene].polymode.val.i == pm_mono_fp) && !glide) - storage.last_key[scene] = key; - new (nvoice) SurgeVoice(&storage, &storage.getPatch().scene[scene], - storage.getPatch().scenedata[scene], key, velocity, channel, scene, - detune, &channelState[channel].keyState[key], - &channelState[mpeMainChannel], &channelState[channel], mpeEnabled); + voices[scene].push_back(nvoice); + if ((storage.getPatch().scene[scene].polymode.val.i == pm_mono_fp) && !glide) + storage.last_key[scene] = key; + new (nvoice) SurgeVoice( + &storage, &storage.getPatch().scene[scene], storage.getPatch().scenedata[scene], + key, velocity, channel, scene, detune, &channelState[channel].keyState[key], + &channelState[mpeMainChannel], &channelState[channel], mpeEnabled, voiceCounter++); + } + } + else + { + /* + * We still need to indicate that this is an ordered voice even though we don't create it + */ + channelState[channel].keyState[key].voiceOrder = voiceCounter++; } } break; @@ -612,46 +659,105 @@ void SurgeSynthesizer::playVoice(int scene, char channel, char key, char velocit case pm_mono_st_fp: { bool found_one = false; - list::const_iterator iter; - for (iter = voices[scene].begin(); iter != voices[scene].end(); iter++) + int primode = storage.getPatch().scene[scene].monoVoicePriorityMode; + bool createVoice = true; + if( primode == ALWAYS_HIGHEST || primode == ALWAYS_LOWEST ) { - SurgeVoice* v = *iter; - if ((v->state.scene_id == scene) && (v->state.gate)) + /* + * There is a chance we don't want to make a voice + */ + if( mpeEnabled ) { - v->legato(key, velocity, detune); - found_one = true; - if (mpeEnabled) + for( int k=0; k<128; ++k ) { - /* - ** This voice was created on a channel but is being legato held to another channel - ** so it needs to borrow the channel and channelState. Obviously this can only - ** happen in MPE mode. - */ - v->state.channel = channel; - v->state.voiceChannelState = &channelState[channel]; + for( int mpeChan=0; mpeChan < 16; ++mpeChan ) + { + if (channelState[mpeChan].keyState[k].keystate) + { + if (primode == ALWAYS_HIGHEST && k > key) + createVoice = false; + if (primode == ALWAYS_LOWEST && k < key) + createVoice = false; + } + } } - break; } else { - if (v->state.scene_id == scene) - v->uber_release(); // make this optional for poly legato + for( int k=0; k<128; ++k ) + { + if( channelState[channel].keyState[k].keystate ) + { + if( primode == ALWAYS_HIGHEST && k > key ) createVoice = false; + if( primode == ALWAYS_LOWEST && k < key ) createVoice = false; + } + } } } - if (!found_one) + + if( createVoice ) { - int mpeMainChannel = getMpeMainChannel(channel, key); + list::const_iterator iter; + for (iter = voices[scene].begin(); iter != voices[scene].end(); iter++) + { + SurgeVoice* v = *iter; + if ((v->state.scene_id == scene) && (v->state.gate)) + { + v->legato(key, velocity, detune); + found_one = true; + if (mpeEnabled) + { + /* + ** This voice was created on a channel but is being legato held to another channel + ** so it needs to borrow the channel and channelState. Obviously this can only + ** happen in MPE mode. + */ + v->state.channel = channel; + v->state.voiceChannelState = &channelState[channel]; + } + break; + } + else + { + if (v->state.scene_id == scene) + v->uber_release(); // make this optional for poly legato + } + } + if (!found_one) + { + int mpeMainChannel = getMpeMainChannel(channel, key); - SurgeVoice* nvoice = getUnusedVoice(scene); - if (nvoice) + SurgeVoice* nvoice = getUnusedVoice(scene); + if (nvoice) + { + voices[scene].push_back(nvoice); + new (nvoice) SurgeVoice(&storage, &storage.getPatch().scene[scene], + storage.getPatch().scenedata[scene], key, velocity, channel, + scene, detune, &channelState[channel].keyState[key], + &channelState[mpeMainChannel], &channelState[channel], + mpeEnabled, voiceCounter++); + } + } + else { - voices[scene].push_back(nvoice); - new (nvoice) SurgeVoice(&storage, &storage.getPatch().scene[scene], - storage.getPatch().scenedata[scene], key, velocity, channel, - scene, detune, &channelState[channel].keyState[key], - &channelState[mpeMainChannel], &channelState[channel], mpeEnabled); + /* + * Here we are legato sliding a voice which is fine but it means we + * don't create a new voice. That will screw up the voice counters + * - basically all voices will have the same count - so a concept of + * latest doesn't work. Thus update the channel state making this key + * 'newer' than the prior voice (as is normally done in the SurgeVoice + * constructor call). + */ + channelState[channel].keyState[key].voiceOrder = voiceCounter++; } } + else + { + /* + * Still need to update the counter if we don't create in hilow mode + */ + channelState[channel].keyState[key].voiceOrder = voiceCounter++; + } } break; } @@ -753,40 +859,114 @@ void SurgeSynthesizer::releaseNotePostHoldCheck(int scene, char channel, char ke ** In MPE mode, where each note is per channel, that means ** scanning all non-main channels rather than ourself for the ** highest note + * + * But with the introduction of voice modes finding the next one + * is trickier so add these two little lambdas */ + if ((v->state.key == key) && (v->state.channel == channel)) { int activateVoiceKey = 60, activateVoiceChannel = 0; // these will be overriden + auto priorityMode = storage.getPatch().scene[v->state.scene_id].monoVoicePriorityMode; // v->release(); if (!mpeEnabled) { + int highest=-1, lowest=128, latest=-1; + int64_t lt = 0; for (k = hikey; k >= lowkey && !do_switch; k--) // search downwards { - if (channelState[channel].keyState[k].keystate) + if( channelState[channel].keyState[k].keystate) { - do_switch = true; - activateVoiceKey = k; - activateVoiceChannel = channel; - break; + if( k >= highest ) highest = k; + if( k <= lowest ) lowest = k; + if( channelState[channel].keyState[k].voiceOrder >= lt ) + { + latest = k; + lt = channelState[channel].keyState[k].voiceOrder; + } } } + + switch( storage.getPatch().scene[v->state.scene_id].monoVoicePriorityMode) + { + case ALWAYS_HIGHEST: + case NOTE_ON_LATEST_RETRIGGER_HIGHEST: + k = highest >= 0 ? highest : -1; + break; + case ALWAYS_LATEST: + k = latest >= 0 ? latest : -1; + break; + case ALWAYS_LOWEST: + k = lowest <= 127 ? lowest : -1; + break; + } + + if( k >= 0 ) + { + do_switch = true; + activateVoiceKey = k; + activateVoiceChannel = channel; + } } else { + int highest=-1, lowest=128, latest=-1; + int hichan, lowchan, latechan; + int64_t lt = 0; + for (k = hikey; k >= lowkey && !do_switch; k--) { for (int mpeChan = 1; mpeChan < 16; ++mpeChan) { if (mpeChan != channel && channelState[mpeChan].keyState[k].keystate) { - do_switch = true; - activateVoiceChannel = mpeChan; - activateVoiceKey = k; - break; + if (k >= highest) + { + highest = k; + hichan = mpeChan; + } + if (k <= lowest) + { + lowest = k; + lowchan = mpeChan; + } + if (channelState[mpeChan].keyState[k].voiceOrder >= lt) + { + lt = channelState[mpeChan].keyState[k].voiceOrder; + latest = k; + latechan = mpeChan; + } } } } + k = -1; + int kchan; + + switch( storage.getPatch().scene[v->state.scene_id].monoVoicePriorityMode) + { + case ALWAYS_HIGHEST: + case NOTE_ON_LATEST_RETRIGGER_HIGHEST: + k = highest >= 0 ? highest : -1; + kchan = hichan; + break; + case ALWAYS_LATEST: + k = latest >= 0 ? latest : -1; + kchan = latechan; + break; + case ALWAYS_LOWEST: + k = lowest <= 127 ? lowest : -1; + kchan = lowchan; + break; + } + + if( k >= 0 ) + { + do_switch = true; + activateVoiceChannel = kchan; + activateVoiceKey = k; + } + } if (!do_switch) { @@ -824,33 +1004,101 @@ void SurgeSynthesizer::releaseNotePostHoldCheck(int scene, char channel, char ke */ if (!mpeEnabled) { - for (k = hikey; k >= lowkey && do_release; k--) // search downwards + int highest=-1, lowest=128, latest=-1; + int64_t lt = 0; + for (k = hikey; k >= lowkey; k--) // search downwards { - if (channelState[channel].keyState[k].keystate) + if( k != key && channelState[channel].keyState[k].keystate) { - v->legato(k, velocity, channelState[channel].keyState[k].lastdetune); - do_release = false; - break; + if( k >= highest ) highest = k; + if( k <= lowest ) lowest = k; + if( channelState[channel].keyState[k].voiceOrder >= lt ) + { + latest = k; + lt = channelState[channel].keyState[k].voiceOrder; + } } } + + switch( storage.getPatch().scene[v->state.scene_id].monoVoicePriorityMode) + { + case ALWAYS_HIGHEST: + case NOTE_ON_LATEST_RETRIGGER_HIGHEST: + k = highest >= 0 ? highest : -1; + break; + case ALWAYS_LATEST: + k = latest >= 0 ? latest : -1; + break; + case ALWAYS_LOWEST: + k = lowest <= 127 ? lowest : -1; + break; + } + + if( k >= 0 ) + { + v->legato(k, velocity, channelState[channel].keyState[k].lastdetune); + do_release = false; + } } else { - for (k = hikey; k >= lowkey && do_release; k--) // search downwards + int highest=-1, lowest=128, latest=-1; + int hichan, lowchan, latechan; + int64_t lt = 0; + + for (k = hikey; k >= lowkey && !do_switch; k--) { for (int mpeChan = 1; mpeChan < 16; ++mpeChan) { if (mpeChan != channel && channelState[mpeChan].keyState[k].keystate) { - v->legato(k, velocity, channelState[mpeChan].keyState[k].lastdetune); - do_release = false; - // See the comment above at the other _st legato spot - v->state.channel = mpeChan; - v->state.voiceChannelState = &channelState[mpeChan]; - break; + if (k >= highest) + { + highest = k; + hichan = mpeChan; + } + if (k <= lowest) + { + lowest = k; + lowchan = mpeChan; + } + if (channelState[mpeChan].keyState[k].voiceOrder >= lt) + { + lt = channelState[mpeChan].keyState[k].voiceOrder; + latest = k; + latechan = mpeChan; + } } } } + k = -1; + int kchan; + + switch( storage.getPatch().scene[v->state.scene_id].monoVoicePriorityMode) + { + case ALWAYS_HIGHEST: + case NOTE_ON_LATEST_RETRIGGER_HIGHEST: + k = highest >= 0 ? highest : -1; + kchan = hichan; + break; + case ALWAYS_LATEST: + k = latest >= 0 ? latest : -1; + kchan = latechan; + break; + case ALWAYS_LOWEST: + k = lowest <= 127 ? lowest : -1; + kchan = lowchan; + break; + } + + if( k >= 0 ) + { + v->legato(k, velocity, channelState[kchan].keyState[k].lastdetune); + do_release = false; + // See the comment above at the other _st legato spot + v->state.channel = kchan; + v->state.voiceChannelState = &channelState[kchan]; + } } if (do_release) @@ -3323,7 +3571,7 @@ void SurgeSynthesizer::reorderFx(int source, int target, FXReorderMode m ) else if(m == FXReorderMode::SWAP) { fxsync[source].type.val.i = to.type.val.i; - Effect* t_fx = spawn_effect(fxsync[source].type.val.i, &storage, &fxsync[source], 0); + t_fx = spawn_effect(fxsync[source].type.val.i, &storage, &fxsync[source], 0); if (t_fx) { t_fx->init_ctrltypes(); diff --git a/src/common/SurgeSynthesizer.h b/src/common/SurgeSynthesizer.h index 5f211aa1d09..7db376fa156 100644 --- a/src/common/SurgeSynthesizer.h +++ b/src/common/SurgeSynthesizer.h @@ -140,6 +140,7 @@ class alignas(16) SurgeSynthesizer void freeVoice(SurgeVoice*); std::array, 2> voices_array; unsigned int voices_usedby[2][MAX_VOICES]; // 0 indicates no user, 1 is scene A & 2 is scene B // TODO: FIX SCENE ASSUMPTION! + int64_t voiceCounter = 1L; public: diff --git a/src/common/dsp/SurgeVoice.cpp b/src/common/dsp/SurgeVoice.cpp index 7eff86e37a6..306acf97bf5 100644 --- a/src/common/dsp/SurgeVoice.cpp +++ b/src/common/dsp/SurgeVoice.cpp @@ -70,7 +70,8 @@ SurgeVoice::SurgeVoice(SurgeStorage* storage, MidiKeyState* keyState, MidiChannelState* mainChannelState, MidiChannelState* voiceChannelState, - bool mpeEnabled + bool mpeEnabled, + int64_t voiceOrder ) //: fb(storage,oscene) { @@ -84,6 +85,9 @@ SurgeVoice::SurgeVoice(SurgeStorage* storage, memcpy(localcopy, paramptr, sizeof(localcopy)); + // We want this on the keystate so it survives the voice for mono mode + keyState->voiceOrder = voiceOrder; + age = 0; age_release = 0; state.key = key; @@ -112,7 +116,7 @@ SurgeVoice::SurgeVoice(SurgeStorage* storage, else state.portasrc_key = storage->last_key[scene_id]; state.priorpkey = state.portasrc_key; - + storage->last_key[scene_id] = key; state.portaphase = 0; noisegenL[0] = 0.f; diff --git a/src/common/dsp/SurgeVoice.h b/src/common/dsp/SurgeVoice.h index c593cbf750a..fa71e46b146 100644 --- a/src/common/dsp/SurgeVoice.h +++ b/src/common/dsp/SurgeVoice.h @@ -48,7 +48,8 @@ class alignas(16) SurgeVoice MidiKeyState* keyState, MidiChannelState* mainChannelState, MidiChannelState* voiceChannelState, - bool mpeEnabled + bool mpeEnabled, + int64_t voiceOrder ); ~SurgeVoice(); diff --git a/src/common/gui/SurgeGUIEditor.cpp b/src/common/gui/SurgeGUIEditor.cpp index 473c00e0bdc..ae0cece2ed4 100644 --- a/src/common/gui/SurgeGUIEditor.cpp +++ b/src/common/gui/SurgeGUIEditor.cpp @@ -2913,6 +2913,22 @@ int32_t SurgeGUIEditor::controlModifierClicked(CControl* control, CButtonState b p->val.i == pm_mono_fp || p->val.i == pm_mono_st_fp )) { + std::vector labels = { "Latest", "Highest", "Lowest", "Latest On, Highest Off (Legacy)" }; + std::vector vals = { ALWAYS_LATEST, ALWAYS_HIGHEST, ALWAYS_LOWEST, NOTE_ON_LATEST_RETRIGGER_HIGHEST }; + contextMenu->addSeparator(); + for( int i=0; i<4; ++i ) + { + auto m = addCallbackMenu(contextMenu, + Surge::UI::toOSCaseForMenu("Priority: " + labels[i]), + [this,vals,i] () + { + synth->storage.getPatch().scene[current_scene].monoVoicePriorityMode = + vals[i]; + } + ); + if( vals[i] == synth->storage.getPatch().scene[current_scene].monoVoicePriorityMode ) + m->setChecked( true ); + } contextMenu->addSeparator(); contextMenu->addEntry(makeMonoModeOptionsMenu(menuRect, false ), Surge::UI::toOSCaseForMenu("Mono Mode Options (Current Instance)")); diff --git a/src/headless/UnitTestsIO.cpp b/src/headless/UnitTestsIO.cpp index 85e7c769dd4..4a4580fcae1 100644 --- a/src/headless/UnitTestsIO.cpp +++ b/src/headless/UnitTestsIO.cpp @@ -726,4 +726,40 @@ TEST_CASE( "Patch Version Builder", "[io]") REQUIRE(entfn == cand_fn.str()); } } +} + +TEST_CASE( "MonoVoicePriority Streams", "[io]" ) +{ + auto fromto = [](std::shared_ptr src, + std::shared_ptr dest) + { + void *d = nullptr; + auto sz = src->saveRaw( &d ); + + dest->loadRaw( d, sz, false ); + }; + SECTION( "MVP Streams Properly" ) + { + int mvp = ALWAYS_LOWEST; + for( int i=0; i<20; ++i ) + { + int r1 = rand() % (mvp + 1); + int r2 = rand() % (mvp + 1); + INFO( "Checking type " << r1 << " " << r2 ); + auto ssrc = Surge::Headless::createSurge(44100); + ssrc->storage.getPatch().scene[0].monoVoicePriorityMode = (MonoVoicePriorityMode)r1; + ssrc->storage.getPatch().scene[1].monoVoicePriorityMode = (MonoVoicePriorityMode)r2; + auto sdst = Surge::Headless::createSurge(44100); + + REQUIRE( sdst->storage.getPatch().scene[0].monoVoicePriorityMode == ALWAYS_LATEST ); + REQUIRE( sdst->storage.getPatch().scene[1].monoVoicePriorityMode == ALWAYS_LATEST ); + + fromto( ssrc, sdst ); + + REQUIRE( sdst->storage.getPatch().scene[0].monoVoicePriorityMode == (MonoVoicePriorityMode)r1); + REQUIRE( sdst->storage.getPatch().scene[1].monoVoicePriorityMode == (MonoVoicePriorityMode)r2); + + } + } + } \ No newline at end of file diff --git a/src/headless/UnitTestsMIDI.cpp b/src/headless/UnitTestsMIDI.cpp index 404590f1e41..8fdd4febc9e 100644 --- a/src/headless/UnitTestsMIDI.cpp +++ b/src/headless/UnitTestsMIDI.cpp @@ -211,6 +211,7 @@ TEST_CASE( "Sustain Pedal and Mono", "[midi]" ) // #1459 auto surge = Surge::Headless::createSurge(44100); REQUIRE( surge ); surge->storage.getPatch().scene[0].polymode.val.i = pm_mono; + surge->storage.getPatch().scene[0].monoVoicePriorityMode = NOTE_ON_LATEST_RETRIGGER_HIGHEST; // Legacy mode auto step = [surge]() { for( int i=0; i<25; ++i ) surge->process(); }; step(); @@ -403,8 +404,389 @@ TEST_CASE( "Sustain Pedal and Mono", "[midi]" ) // #1459 surge->channelController( 0, 64, 0 ); step( surge ); REQUIRE( playingNoteCount( surge ) == 0 ); }); + } +} + +TEST_CASE( "Mono Voice Priority Modes", "[midi]" ) +{ + struct TestCase + { + TestCase(std::string lab, int pm, bool mp, MonoVoicePriorityMode m) + : name(lab), polymode(pm), mpe(mp), mode(m) + {} + std::vector> onoffs; + int polymode = pm_mono; + bool mpe = false; + std::string name = ""; + MonoVoicePriorityMode mode = NOTE_ON_LATEST_RETRIGGER_HIGHEST; + void on(int n) + { + onoffs.push_back(std::make_pair(n, true)); + } + void off(int n) + { + onoffs.push_back(std::make_pair(n, false)); + } + }; + + auto step = [](auto surge) { + for (int i = 0; i < 25; ++i) + surge->process(); + }; + auto testInSurge = [](TestCase& c) { + auto surge = Surge::Headless::createSurge(44100); + REQUIRE(surge); + surge->storage.getPatch().scene[0].monoVoicePriorityMode = c.mode; + surge->mpeEnabled = c.mpe; + + std::map channelAssignments; + int nxtch = 1; + surge->storage.getPatch().scene[0].polymode.val.i = c.polymode; + for (auto n : c.onoffs) + { + int channel = 0; + // Implement a simple channel increase strategy + if (c.mpe) + { + if (n.second) + { + channelAssignments[n.first] = nxtch++; + } + channel = channelAssignments[n.first]; + if (nxtch > 16) + nxtch = 1; + if (!n.second) + { + channelAssignments[n.first] = -1; + } + } + if (n.second) + { + surge->playNote(channel, n.first, 127, 0); + } + else + { + surge->releaseNote(channel, n.first, 0); + } + } + return surge; + }; + + auto playingNoteCount = [](std::shared_ptr surge) { + int ct = 0; + for (auto v : surge->voices[0]) + { + if (v->state.gate) + ct++; + } + return ct; + }; + + auto solePlayingNote = [](std::shared_ptr surge) { + int ct = 0; + int res = -1; + for (auto v : surge->voices[0]) + { + if (v->state.gate) + { + ct++; + res = v->state.key; + } + } + REQUIRE(ct == 1); + return res; + }; + + // With no second argument, confirms no note; with a second argument confirms that note + auto confirmResult = [&](TestCase& c, int which = -1) { + INFO("Running " << c.name); + auto s = testInSurge(c); + REQUIRE(playingNoteCount(s) == (which >= 0 ? 1 : 0)); + if (which >= 0) + REQUIRE(solePlayingNote(s) == which); + }; + + auto modes = {pm_mono, pm_mono_st, pm_mono_fp, pm_mono_st_fp}; + auto mpe = {false, true}; + for (auto mp : mpe) + { + for (auto m : modes) + { + DYNAMIC_SECTION("Legacy in PlayMode " << m << " " << (mp ? "mpe" : "non-mpe")) + { + { + TestCase c("No Notes", m, mp, NOTE_ON_LATEST_RETRIGGER_HIGHEST); + auto s = testInSurge(c); + confirmResult(c); + } + + { + TestCase c("One Note", m, mp, NOTE_ON_LATEST_RETRIGGER_HIGHEST); + c.on(60); + confirmResult(c, 60); + } + + { + TestCase c("On Off", m, mp, NOTE_ON_LATEST_RETRIGGER_HIGHEST); + c.on(60); + c.off(60); + confirmResult(c); + } + + { + TestCase c("Three Notes", m, mp, NOTE_ON_LATEST_RETRIGGER_HIGHEST); + c.on(60); + c.on(61); + c.on(59); + confirmResult(c, 59); + } + + { + TestCase c("Three On and an Off", m, mp, NOTE_ON_LATEST_RETRIGGER_HIGHEST); + c.on(60); + c.on(61); + c.on(59); + c.off(59); + confirmResult(c, 61); + } + + { + TestCase c("Second Three On", m, mp, NOTE_ON_LATEST_RETRIGGER_HIGHEST); + c.on(61); + c.on(60); + c.on(59); + c.off(59); + confirmResult(c, 61); + } + + { + TestCase c("Third Three On", m, mp, NOTE_ON_LATEST_RETRIGGER_HIGHEST); + c.on(61); + c.on(60); + c.on(59); + c.off(60); + confirmResult(c, 59); + } + + { + TestCase c("All Backs Off", m, mp, NOTE_ON_LATEST_RETRIGGER_HIGHEST); + c.on(61); + c.on(60); + c.on(59); + c.off(61); + c.off(60); + confirmResult(c, 59); + } + + { + TestCase c("Toggle 59", m, mp, NOTE_ON_LATEST_RETRIGGER_HIGHEST); + c.on(61); + c.on(60); + c.on(59); + c.off(59); + c.on(59); + confirmResult(c, 59); + } + } + + DYNAMIC_SECTION("Latest in PlayMode " << m << " " << (mp ? "mpe" : "non-mpe")) + { + { + TestCase c("No Notes", m, mp, ALWAYS_LATEST); + confirmResult(c); + } + + { + TestCase c("One One", m, mp, ALWAYS_LATEST); + c.on(60); + confirmResult(c, 60); + } + + { + TestCase c("On Off", m, mp, ALWAYS_LATEST); + c.on(60); + c.off(60); + confirmResult(c); + } + + { + TestCase c("Three Ons", m, mp, ALWAYS_LATEST); + c.on(60); + c.on(61); + c.on(59); + confirmResult(c, 59); + } + + { + TestCase c("Three On and an Off AW", m, mp, ALWAYS_LATEST); + c.on(60); + c.on(61); + c.on(59); + c.off(59); + confirmResult(c, 61); + } + + { + TestCase c("Three On and an Off Two", m, mp, ALWAYS_LATEST); + c.on(61); + c.on(60); + c.on(59); + c.off(59); + confirmResult(c, 60); // First difference + } + + { + TestCase c("Three On and Back Off", m, mp, ALWAYS_LATEST); + c.on(61); + c.on(60); + c.on(59); + c.off(60); + confirmResult(c, 59); + } + + { + TestCase c("Three On and Under Off", m, mp, ALWAYS_LATEST); + c.on(61); + c.on(60); + c.on(59); + c.off(61); + c.off(60); + confirmResult(c, 59); + } + + { + TestCase c("Three On and Trill", m, mp, ALWAYS_LATEST); + c.on(61); + c.on(60); + c.on(59); + c.off(59); + c.on(59); + confirmResult(c, 59); + } + } + + DYNAMIC_SECTION("Highest in PlayMode " << m << " " << (mp ? "mpe" : "non-mpe")) + { + { + TestCase c("No Notes", m, mp, ALWAYS_HIGHEST); + confirmResult(c); + } + + { + TestCase c("One One", m, mp, ALWAYS_HIGHEST); + c.on(60); + confirmResult(c, 60); + } + + { + TestCase c("On Off", m, mp, ALWAYS_HIGHEST); + c.on(60); + c.off(60); + confirmResult(c); + } + + { + TestCase c("Three Ons", m, mp, ALWAYS_HIGHEST); + c.on(60); + c.on(61); + c.on(59); + confirmResult(c, 61); + } + + { + TestCase c("Release to Highest", m, mp, ALWAYS_HIGHEST); + c.on(60); + c.on(61); + c.on(62); + c.off(62); + confirmResult(c, 61); + } + + + { + TestCase c("Release to Highest Two", m, mp, ALWAYS_HIGHEST); + c.on(61); + c.on(60); + c.on(62); + c.off(62); + confirmResult(c, 61); + } + + { + TestCase c("Release Underneath Me", m, mp, ALWAYS_HIGHEST); + c.on(61); + c.on(60); + c.on(62); + c.off(61); + c.off(62); + confirmResult(c, 60); + } + } + DYNAMIC_SECTION("Lowest in PlayMode " << m << " " << (mp ? "mpe" : "non-mpe")) + { + { + TestCase c("No Notes", m, mp, ALWAYS_LOWEST); + confirmResult(c); + } + + { + TestCase c("One One", m, mp, ALWAYS_LOWEST); + c.on(60); + confirmResult(c, 60); + } + + { + TestCase c("On Off", m, mp, ALWAYS_LOWEST); + c.on(60); + c.off(60); + confirmResult(c); + } + + { + TestCase c("Three Ons", m, mp, ALWAYS_LOWEST); + c.on(60); + c.on(59); + c.on(61); + confirmResult(c, 59); + } + + { + TestCase c("Release to Lowest", m, mp, ALWAYS_LOWEST); + c.on(60); + c.on(59); + c.on(58); + c.off(58); + confirmResult(c, 59); + } + + + { + TestCase c("Release to Lowest Two", m, mp, ALWAYS_LOWEST); + c.on(60); + c.on(61); + c.on(59); + c.off(59); + confirmResult(c, 60); + } + + { + TestCase c("Release Above Me", m, mp, ALWAYS_HIGHEST); + c.on(61); + c.on(62); + c.on(60); + c.off(62); + c.off(61); + confirmResult(c, 60); + } + } + } } - // add it to user prefs menu - // add it for patch to play meny rmb + /* + * TODO: + * - Stream the Scene variable + * - Implement + * - Add UI + * - Test streaming + */ } \ No newline at end of file