Skip to content

Commit

Permalink
Basic support for channel-per-octave mode, while modulation is applie…
Browse files Browse the repository at this point in the history
…d at MIDI input, and with no special treatment for scene masking yet. (#4805)

Implement a 12 key shift per midi channel
Closes #4151
  • Loading branch information
cgibbard authored Aug 8, 2021
1 parent 11b00cd commit 05e954a
Show file tree
Hide file tree
Showing 8 changed files with 123 additions and 6 deletions.
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ The Emu (J Riley Hill) <http://jrileyhill.com>
Matthias von Faber <http://github.com/mvf>
Jani Frilander <http://github.com/janifr>
Amy Furniss <http://github.com/amyfurniss>
Cale Gibbard <[email protected]>
Brian Ginsburg <https://github.com/bgins>
Nathan Kopp <[email protected]>
Korridor <[email protected]>
Expand Down
13 changes: 12 additions & 1 deletion src/common/SurgePatch.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -2846,4 +2857,4 @@ void SurgePatch::formulaFromXMLElement(FormulaModulatorStorage *fs, TiXmlElement
{
fs->interpreter = (FormulaModulatorStorage::Interpreter)(interp);
}
}
}
1 change: 1 addition & 0 deletions src/common/SurgeStorage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 4 additions & 0 deletions src/common/SurgeStorage.h
Original file line number Diff line number Diff line change
Expand Up @@ -739,6 +739,8 @@ struct DAWExtraStateStorage
std::string mappingContents = "";
std::string mappingName = "";

bool mapChannelToOctave = false;

std::unordered_map<int, int> midictrl_map; // param -> midictrl
std::unordered_map<int, int> customcontrol_map; // custom controller number -> midicontrol

Expand Down Expand Up @@ -1067,6 +1069,8 @@ class alignas(16) SurgeStorage
Tunings::KeyboardMapping currentMapping;
bool isStandardTuning = true, isStandardScale = true, isStandardMapping = true;

std::atomic<bool> 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
Expand Down
6 changes: 5 additions & 1 deletion src/common/SurgeSynthesizer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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++)
Expand Down
20 changes: 16 additions & 4 deletions src/common/dsp/SurgeVoice.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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() {}
Expand Down
6 changes: 6 additions & 0 deletions src/gui/SurgeGUIEditor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2403,6 +2403,12 @@ juce::PopupMenu SurgeGUIEditor::makeTuningMenu(const juce::Point<int> &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(
Expand Down
78 changes: 78 additions & 0 deletions src/headless/UnitTestsTUN.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down

0 comments on commit 05e954a

Please sign in to comment.