From a17d8b2331680d1c484375e58285a6bf068d01fb Mon Sep 17 00:00:00 2001 From: Cale Gibbard Date: Fri, 6 Aug 2021 00:49:29 -0400 Subject: [PATCH] Basic support for channel-per-octave mode, while modulation is applied at MIDI input, and with no special treatment for scene masking yet. --- AUTHORS | 1 + src/common/SurgePatch.cpp | 13 +++++- src/common/SurgeStorage.cpp | 1 + src/common/SurgeStorage.h | 4 ++ src/common/SurgeSynthesizer.cpp | 6 ++- src/common/dsp/SurgeVoice.cpp | 20 +++++++-- src/gui/SurgeGUIEditor.cpp | 6 +++ src/headless/UnitTestsTUN.cpp | 78 +++++++++++++++++++++++++++++++++ 8 files changed, 123 insertions(+), 6 deletions(-) diff --git a/AUTHORS b/AUTHORS index d448e95b7e4..2edfc055915 100644 --- a/AUTHORS +++ b/AUTHORS @@ -15,6 +15,7 @@ The Emu (J Riley Hill) Matthias von Faber Jani Frilander Amy Furniss +Cale Gibbard Brian Ginsburg Nathan Kopp Korridor diff --git a/src/common/SurgePatch.cpp b/src/common/SurgePatch.cpp index 5fa309970f1..31bf94f4a13 100644 --- a/src/common/SurgePatch.cpp +++ b/src/common/SurgePatch.cpp @@ -2095,6 +2095,12 @@ void SurgePatch::load_xml(const void *data, int datasize, bool is_preset) } } + p = TINYXML_SAFE_TO_ELEMENT(de->FirstChild("mapChannelToOctave")); + if (p && p->QueryIntAttribute("v", &ival) == TIXML_SUCCESS) + { + dawExtraState.mapChannelToOctave = (ival != 0); + } + p = TINYXML_SAFE_TO_ELEMENT(de->FirstChild("midictrl_map")); if (p) { @@ -2560,6 +2566,11 @@ unsigned int SurgePatch::save_xml(void **data) // allocates mem, must be freed b mpn.SetAttribute("v", dawExtraState.mappingName.c_str()); dawExtraXML.InsertEndChild(mpn); + // Revision 17 adds mapChannelToOctave + TiXmlElement mcto("mapChannelToOctave"); + mcto.SetAttribute("v", (int)(storage->mapChannelToOctave)); + dawExtraXML.InsertEndChild(mcto); + /* ** Add the midi controls */ @@ -2846,4 +2857,4 @@ void SurgePatch::formulaFromXMLElement(FormulaModulatorStorage *fs, TiXmlElement { fs->interpreter = (FormulaModulatorStorage::Interpreter)(interp); } -} \ No newline at end of file +} diff --git a/src/common/SurgeStorage.cpp b/src/common/SurgeStorage.cpp index 0098a1061fb..dfbbfa5bfef 100644 --- a/src/common/SurgeStorage.cpp +++ b/src/common/SurgeStorage.cpp @@ -529,6 +529,7 @@ SurgeStorage::SurgeStorage(std::string suppliedDataPath) : otherscene_clients(0) twelveToneStandardMapping = Tunings::Tuning(Tunings::evenTemperament12NoteScale(), Tunings::KeyboardMapping()); isStandardTuning = true; + mapChannelToOctave = false; isToggledToCache = false; for (int q = 0; q < 3; ++q) togglePriorState[q] = false; diff --git a/src/common/SurgeStorage.h b/src/common/SurgeStorage.h index d3cde09f74d..e7cad6913a6 100644 --- a/src/common/SurgeStorage.h +++ b/src/common/SurgeStorage.h @@ -739,6 +739,8 @@ struct DAWExtraStateStorage std::string mappingContents = ""; std::string mappingName = ""; + bool mapChannelToOctave = false; + std::unordered_map midictrl_map; // param -> midictrl std::unordered_map customcontrol_map; // custom controller number -> midicontrol @@ -1067,6 +1069,8 @@ class alignas(16) SurgeStorage Tunings::KeyboardMapping currentMapping; bool isStandardTuning = true, isStandardScale = true, isStandardMapping = true; + std::atomic mapChannelToOctave; // When other midi modes come along, clean this up. + enum TuningApplicationMode { RETUNE_ALL = 0, // These values are streamed so don't change them if you add diff --git a/src/common/SurgeSynthesizer.cpp b/src/common/SurgeSynthesizer.cpp index 9e2ba5e15f7..f4b1b97dae1 100644 --- a/src/common/SurgeSynthesizer.cpp +++ b/src/common/SurgeSynthesizer.cpp @@ -322,7 +322,8 @@ int SurgeSynthesizer::calculateChannelMask(int channel, int key) break; } } - else if (storage.getPatch().scenemode.val.i == sm_single) + else if (storage.getPatch().scenemode.val.i == sm_single && + !storage.getPatch().dawExtraState.mapChannelToOctave) { if (storage.getPatch().scene_active.val.i == 1) channelmask = 2; @@ -3852,6 +3853,7 @@ void SurgeSynthesizer::populateDawExtraState() storage.getPatch().dawExtraState.mappingContents = ""; storage.getPatch().dawExtraState.mappingName = ""; } + storage.getPatch().dawExtraState.mapChannelToOctave = storage.mapChannelToOctave; int n = n_global_params + n_scene_params; // only store midictrl's for scene A (scene A -> scene // B will be duplicated on load) @@ -3929,6 +3931,8 @@ void SurgeSynthesizer::loadFromDawExtraState() storage.remapToConcertCKeyboard(); } + storage.mapChannelToOctave = storage.getPatch().dawExtraState.mapChannelToOctave; + int n = n_global_params + n_scene_params; // only store midictrl's for scene A (scene A -> scene // B will be duplicated on load) for (int i = 0; i < n; i++) diff --git a/src/common/dsp/SurgeVoice.cpp b/src/common/dsp/SurgeVoice.cpp index 153a9abdd83..60558f2f11e 100644 --- a/src/common/dsp/SurgeVoice.cpp +++ b/src/common/dsp/SurgeVoice.cpp @@ -58,7 +58,7 @@ float SurgeVoiceState::getPitch(SurgeStorage *storage) } auto rkey = keyRetuning; - return res + rkey; + res = res + rkey; } else if (!storage->isStandardTuning && storage->tuningApplicationMode == SurgeStorage::RETUNE_MIDI_ONLY) @@ -68,12 +68,24 @@ float SurgeVoiceState::getPitch(SurgeStorage *storage) float frac = res - idx; // frac is 0 means use idx; frac is 1 means use idx+1 float b0 = storage->currentTuning.logScaledFrequencyForMidiNote(idx) * 12; float b1 = storage->currentTuning.logScaledFrequencyForMidiNote(idx + 1) * 12; - return (1.f - frac) * b0 + frac * b1; + res = (1.f - frac) * b0 + frac * b1; } - else + + if (storage->mapChannelToOctave) { - return res; + float shift; + if (channel > 7) + { + shift = channel - 16; + } + else + { + shift = channel; + } + res += 12 * shift; } + + return res; } SurgeVoice::SurgeVoice() {} diff --git a/src/gui/SurgeGUIEditor.cpp b/src/gui/SurgeGUIEditor.cpp index 1d0aa2ae3aa..a7ffb10b74e 100644 --- a/src/gui/SurgeGUIEditor.cpp +++ b/src/gui/SurgeGUIEditor.cpp @@ -2396,6 +2396,12 @@ juce::PopupMenu SurgeGUIEditor::makeTuningMenu(const juce::Point &where, bo }); }); + tuningSubMenu.addItem(Surge::GUI::toOSCaseForMenu("Use MIDI Channel for Octave Shift"), true, + (synth->storage.mapChannelToOctave), [this]() { + this->synth->storage.mapChannelToOctave = + !(this->synth->storage.mapChannelToOctave); + }); + tuningSubMenu.addSeparator(); tuningSubMenu.addItem( diff --git a/src/headless/UnitTestsTUN.cpp b/src/headless/UnitTestsTUN.cpp index 2afbf098893..fccf9b28f0c 100644 --- a/src/headless/UnitTestsTUN.cpp +++ b/src/headless/UnitTestsTUN.cpp @@ -568,6 +568,84 @@ TEST_CASE("An Octave is an Octave", "[tun]") } } +TEST_CASE("Channel to Octave Mapping") +{ + SECTION("When disabled and no tuning applied, channel 2 is the same as channel 1") + { + auto surge = surgeOnSine(); + float f1, f2; + for (int key = 0; key < 90; key++) + { + f1 = frequencyForNote(surge, key, 2, 0, 0); + f2 = frequencyForNote(surge, key, 2, 0, 1); + REQUIRE(f2 == Approx(f1).margin(0.001)); + } + } + SECTION("When enabled and no tuning applied, channel 2 is one octave higher than channel 1") + { + auto surge = surgeOnSine(); + surge->storage.mapChannelToOctave = true; + surge->storage.setTuningApplicationMode(SurgeStorage::RETUNE_MIDI_ONLY); + float f1, f2; + for (int key = 0; key < 90; key++) + { // Limited range because frequencyForNote starts having trouble measuring high + // frequencies. + INFO("key is " << key); + f1 = frequencyForNote(surge, key, 2, 0, 0); + f2 = frequencyForNote(surge, key, 2, 0, 1); + REQUIRE(f2 == Approx(f1 * 2).margin(0.1)); + } + } + SECTION("When enabled and tuning applied, channel 2 is one octave higher than channel 1") + { + auto surge = surgeOnSine(); + surge->storage.mapChannelToOctave = true; + surge->storage.setTuningApplicationMode(SurgeStorage::RETUNE_MIDI_ONLY); + Tunings::Scale s = Tunings::readSCLFile("resources/test-data/scl/31edo.scl"); + surge->storage.retuneToScale(s); + float f1, f2; + for (int key = 0; key < 90; key++) + { + f1 = frequencyForNote(surge, key, 2, 0, 0); + f2 = frequencyForNote(surge, key, 2, 0, 1); + REQUIRE(f2 == Approx(f1 * 2).margin(0.1)); + } + } + SECTION("When enabled and no tuning applied, note 60 is mapped to the correct octaves in " + "different channels") + { + auto surge = surgeOnSine(); + surge->storage.mapChannelToOctave = true; + surge->storage.setTuningApplicationMode(SurgeStorage::RETUNE_MIDI_ONLY); + float f1, f2; + for (int chanOff = -2; chanOff < 3; chanOff++) + { // Only checking reasonable octaves because frequencyForNote actually examines the + // waveform + INFO("chanOff is " << chanOff); + f1 = frequencyForNote(surge, 60, 2, 0, (chanOff + 16) % 16); + f2 = frequencyForNote(surge, 60, 2, 0, (chanOff + 1 + 16) % 16); + REQUIRE(f2 == Approx(f1 * 2).margin(0.1)); + } + } + SECTION("When enabled and tuning applied, note 60 is mapped to the correct octaves in " + "different channels") + { + auto surge = surgeOnSine(); + surge->storage.mapChannelToOctave = true; + surge->storage.setTuningApplicationMode(SurgeStorage::RETUNE_MIDI_ONLY); + Tunings::Scale s = Tunings::readSCLFile("resources/test-data/scl/31edo.scl"); + surge->storage.retuneToScale(s); + float f1, f2; + for (int chanOff = -2; chanOff < 3; chanOff++) + { + INFO("chanOff is " << chanOff); + f1 = frequencyForNote(surge, 60, 2, 0, (chanOff + 16) % 16); + f2 = frequencyForNote(surge, 60, 2, 0, (chanOff + 1 + 16) % 16); + REQUIRE(f2 == Approx(f1 * 2).margin(0.1)); + } + } +} + TEST_CASE("Non-Monotonic Tunings", "[tun]") { SECTION("SCL Non-monotonicity")