Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

WIP: Basic support for octave-per-channel mode #4805

Merged
merged 1 commit into from
Aug 8, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -2396,6 +2396,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