diff --git a/src/sfizz/Defaults.h b/src/sfizz/Defaults.h index d44f2fe6..8a7a171b 100644 --- a/src/sfizz/Defaults.h +++ b/src/sfizz/Defaults.h @@ -124,6 +124,41 @@ namespace Default constexpr float rtDecay { 0.0f }; constexpr Range rtDecayRange { 0.0f, 200.0f }; + // Performance parameters: Filters + constexpr float filterCutoff { 0 }; + constexpr float filterResonance { 0 }; + constexpr int filterKeytrack { 0 }; + constexpr uint8_t filterKeycenter { 60 }; + constexpr int filterRandom { 60 }; + constexpr int filterVeltrack { 0 }; + constexpr int filterCutoffCC { 0 }; + constexpr float filterResonanceCC { 0 }; + constexpr Range filterCutoffRange { 0.0f, 96000.0f }; + constexpr Range filterCutoffModRange { -9600, 9600 }; + constexpr Range filterKeytrackRange { 0, 1200 }; + constexpr Range filterRandomRange { 0, 9600 }; + constexpr Range filterVeltrackRange { -9600, 9600 }; + constexpr Range filterResonanceRange { 0, 40 }; + + // Performance parameters: EQ + constexpr float eqBandwidth { 1.0f }; + constexpr float eqBandwidthCC { 0.0f }; + constexpr float eqFrequencyUnset { 0.0f }; + constexpr float eqFrequency1 { 50.0f }; + constexpr float eqFrequency2 { 500.0f }; + constexpr float eqFrequency3 { 5000.0f }; + constexpr float eqFrequencyCC { 0.0f }; + constexpr float eqGain { 0.0f }; + constexpr float eqGainCC { 0.0f }; + constexpr float eqVel2frequency { 0.0f }; + constexpr float eqVel2gain { 0.0f }; + constexpr Range eqBandwidthRange { 0.001f, 4.0f }; + constexpr Range eqBandwidthModRange { -4.0f, 4.0f }; + constexpr Range eqFrequencyRange { 0.0f, 30000.0f }; + constexpr Range eqFrequencyModRange { -30000.0f, 30000.0f }; + constexpr Range eqGainRange { -96.0f, 24.0f }; + constexpr Range eqGainModRange { -96.0f, 24.0f }; + // Performance parameters: pitch constexpr uint8_t pitchKeycenter { 60 }; constexpr int pitchKeytrack { 100 }; diff --git a/src/sfizz/EQDescription.h b/src/sfizz/EQDescription.h new file mode 100644 index 00000000..ec3e4150 --- /dev/null +++ b/src/sfizz/EQDescription.h @@ -0,0 +1,19 @@ +#pragma once +#include "Config.h" +#include "Defaults.h" +#include "CCMap.h" + +namespace sfz +{ +struct EQDescription +{ + float bandwidth { Default::eqBandwidth }; + float frequency { Default::eqFrequencyUnset }; + float gain { Default::eqGain }; + float vel2frequency { Default::eqVel2frequency }; + float vel2gain { Default::eqVel2gain }; + CCMap bandwidthCC { Default::eqBandwidthCC }; + CCMap frequencyCC { Default::eqFrequencyCC }; + CCMap gainCC { Default::eqGainCC }; +}; +} diff --git a/src/sfizz/FilterDescription.h b/src/sfizz/FilterDescription.h new file mode 100644 index 00000000..1bd21a6e --- /dev/null +++ b/src/sfizz/FilterDescription.h @@ -0,0 +1,21 @@ +#pragma once +#include "Config.h" +#include "Defaults.h" +#include "SfzFilter.h" +#include "CCMap.h" + +namespace sfz +{ +struct FilterDescription +{ + float cutoff { Default::filterCutoff }; + float resonance { Default::filterCutoff }; + int keytrack { Default::filterKeytrack }; + uint8_t keycenter { Default::filterKeycenter }; + int veltrack { Default::filterVeltrack }; + int random { Default::filterRandom }; + FilterType type { FilterType::kFilterLpf2p }; + CCMap cutoffCC { Default::filterCutoffCC }; + CCMap resonanceCC { Default::filterResonanceCC }; +}; +} diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index 115ac405..fb585b6a 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -15,6 +15,18 @@ #include "absl/strings/str_cat.h" #include +template +bool extendIfNecessary(std::vector& vec, unsigned size) +{ + if (size == 0) + return false; + + if (vec.size() < size) + vec.resize(size); + + return true; +} + bool sfz::Region::parseOpcode(const Opcode& opcode) { const auto backParameter = opcode.backParameter(); @@ -381,6 +393,226 @@ bool sfz::Region::parseOpcode(const Opcode& opcode) setValueFromOpcode(opcode, rtDecay, Default::rtDecayRange); break; + // Performance parameters: filters + case hash("cutoff"): + { + const auto filterIndex { backParameter.value_or(1) - 1 }; + if (!extendIfNecessary(filters, filterIndex + 1)) + return false; + setValueFromOpcode(opcode, filters[filterIndex].cutoff, Default::filterCutoffRange); + } + break; + case hash("resonance"): + { + const auto filterIndex { backParameter.value_or(1) - 1 }; + if (!extendIfNecessary(filters, filterIndex + 1)) + return false; + setValueFromOpcode(opcode, filters[filterIndex].resonance, Default::filterResonanceRange); + } + break; + case hash("cutoff_cc"): + { + if (!backParameter) + return false; + + const auto filterIndex { opcode.firstParameter().value_or(1) - 1 }; + if (!extendIfNecessary(filters, filterIndex + 1)) + return false; + + setValueFromOpcode( + opcode, + filters[filterIndex].cutoffCC[*backParameter], + Default::filterCutoffModRange + ); + } + break; + case hash("resonance_cc"): + { + if (!backParameter) + return false; + + const auto filterIndex { opcode.firstParameter().value_or(1) - 1 }; + if (!extendIfNecessary(filters, filterIndex + 1)) + return false; + + setValueFromOpcode( + opcode, + filters[filterIndex].resonanceCC[*backParameter], + Default::filterResonanceRange + ); + } + break; + case hash("fil_keytrack"): + { + const auto filterIndex { opcode.firstParameter().value_or(1) - 1 }; + if (!extendIfNecessary(filters, filterIndex + 1)) + return false; + + setValueFromOpcode(opcode, filters[filterIndex].keytrack, Default::filterKeytrackRange); + } + break; + case hash("fil_keycenter"): + { + const auto filterIndex { opcode.firstParameter().value_or(1) - 1 }; + if (!extendIfNecessary(filters, filterIndex + 1)) + return false; + + setValueFromOpcode(opcode, filters[filterIndex].keycenter, Default::keyRange); + } + break; + case hash("fil_veltrack"): + { + const auto filterIndex { opcode.firstParameter().value_or(1) - 1 }; + if (!extendIfNecessary(filters, filterIndex + 1)) + return false; + + setValueFromOpcode(opcode, filters[filterIndex].veltrack, Default::filterVeltrackRange); + } + break; + case hash("fil_random"): + { + const auto filterIndex { opcode.firstParameter().value_or(1) - 1 }; + if (!extendIfNecessary(filters, filterIndex + 1)) + return false; + + setValueFromOpcode(opcode, filters[filterIndex].random, Default::filterRandomRange); + } + break; + case hash("fil_type"): + { + const auto filterIndex { opcode.firstParameter().value_or(1) - 1 }; + if (!extendIfNecessary(filters, filterIndex + 1)) + return false; + + switch (hash(opcode.value)) { + case hash("lpf_1p"): filters[filterIndex].type = FilterType::kFilterLpf1p; break; + case hash("hpf_1p"): filters[filterIndex].type = FilterType::kFilterHpf1p; break; + case hash("lpf_2p"): filters[filterIndex].type = FilterType::kFilterLpf2p; break; + case hash("hpf_2p"): filters[filterIndex].type = FilterType::kFilterHpf2p; break; + case hash("bpf_2p"): filters[filterIndex].type = FilterType::kFilterBpf2p; break; + case hash("brf_2p"): filters[filterIndex].type = FilterType::kFilterBrf2p; break; + case hash("bpf_1p"): filters[filterIndex].type = FilterType::kFilterBpf1p; break; + case hash("brf_1p"): filters[filterIndex].type = FilterType::kFilterBrf1p; break; + case hash("apf_1p"): filters[filterIndex].type = FilterType::kFilterApf1p; break; + case hash("lpf_2p_sv"): filters[filterIndex].type = FilterType::kFilterLpf2pSv; break; + case hash("hpf_2p_sv"): filters[filterIndex].type = FilterType::kFilterHpf2pSv; break; + case hash("bpf_2p_sv"): filters[filterIndex].type = FilterType::kFilterBpf2pSv; break; + case hash("brf_2p_sv"): filters[filterIndex].type = FilterType::kFilterBrf2pSv; break; + case hash("lpf_4p"): filters[filterIndex].type = FilterType::kFilterLpf4p; break; + case hash("hpf_4p"): filters[filterIndex].type = FilterType::kFilterHpf4p; break; + case hash("lpf_6p"): filters[filterIndex].type = FilterType::kFilterLpf6p; break; + case hash("hpf_6p"): filters[filterIndex].type = FilterType::kFilterHpf6p; break; + case hash("pink"): filters[filterIndex].type = FilterType::kFilterPink; break; + case hash("lsh"): filters[filterIndex].type = FilterType::kFilterLsh; break; + case hash("hsh"): filters[filterIndex].type = FilterType::kFilterHsh; break; + case hash("peq"): filters[filterIndex].type = FilterType::kFilterPeq; break; + default: + filters[filterIndex].type = FilterType::kFilterNone; + DBG("Unknown filter type: " << std::string(opcode.value)); + } + } + break; + // Performance parameters: EQ + case hash("eq_bw"): + { + const auto eqNumber = opcode.firstParameter(); + if (!eqNumber || *eqNumber == 0) + return false; + if (!extendIfNecessary(equalizers, *eqNumber)) + return false; + setValueFromOpcode(opcode, equalizers[*eqNumber - 1].bandwidth, Default::eqBandwidthRange); + } + break; + case hash("eq_bw_oncc"): [[fallthrough]]; + case hash("eq_bwcc"): + { + const auto eqNumber = opcode.firstParameter(); + if (!eqNumber || *eqNumber == 0) + return false; + if (!backParameter) + return false; + if (!extendIfNecessary(equalizers, *eqNumber)) + return false; + + setValueFromOpcode(opcode, equalizers[*eqNumber - 1].bandwidthCC[*backParameter], Default::eqBandwidthModRange); + } + break; + case hash("eq_freq"): + { + const auto eqNumber = opcode.firstParameter(); + if (!eqNumber || *eqNumber == 0) + return false; + if (!extendIfNecessary(equalizers, *eqNumber)) + return false; + setValueFromOpcode(opcode, equalizers[*eqNumber - 1].frequency, Default::eqFrequencyRange); + } + break; + case hash("eq_freq_oncc"): [[fallthrough]]; + case hash("eq_freqcc"): + { + const auto eqNumber = opcode.firstParameter(); + if (!eqNumber || *eqNumber == 0) + return false; + if (!backParameter) + return false; + if (!extendIfNecessary(equalizers, *eqNumber)) + return false; + + setValueFromOpcode(opcode, equalizers[*eqNumber - 1].frequencyCC[*backParameter], Default::eqFrequencyModRange); + } + break; + case hash("eq_velfreq"): + { + const auto eqNumber = opcode.firstParameter(); + const auto check2 = opcode.middleParameter(); + if (!eqNumber || *eqNumber == 0) + return false; + if (!check2 || *check2 != 2 || opcode.parameterPositions[1] != 6) + return false; // was eqN_vel3freq or something else than eqN_vel2freq + if (!extendIfNecessary(equalizers, *eqNumber)) + return false; + + setValueFromOpcode(opcode, equalizers[*eqNumber - 1].vel2frequency, Default::eqFrequencyModRange); + } + break; + case hash("eq_gain"): + { + const auto eqNumber = opcode.firstParameter(); + if (!eqNumber || *eqNumber == 0) + return false; + if (!extendIfNecessary(equalizers, *eqNumber)) + return false; + setValueFromOpcode(opcode, equalizers[*eqNumber - 1].gain, Default::eqGainRange); + } + break; + case hash("eq_gain_oncc"): [[fallthrough]]; + case hash("eq_gaincc"): + { + const auto eqNumber = opcode.firstParameter(); + if (!eqNumber || *eqNumber == 0) + return false; + if (!backParameter) + return false; + if (!extendIfNecessary(equalizers, *eqNumber)) + return false; + + setValueFromOpcode(opcode, equalizers[*eqNumber - 1].gainCC[*backParameter], Default::eqGainModRange); + } + break; + case hash("eq_velgain"): + { + const auto eqNumber = opcode.firstParameter(); + const auto check2 = opcode.middleParameter(); + if (!eqNumber || *eqNumber == 0) + return false; + if (!check2 || *check2 != 2 || opcode.parameterPositions[1] != 6) + return false; // was eqN_vel3gain or something else than eqN_vel2gain + if (!extendIfNecessary(equalizers, *eqNumber)) + return false; + + setValueFromOpcode(opcode, equalizers[*eqNumber - 1].vel2gain, Default::eqGainModRange); + } + break; // Performance parameters: pitch case hash("pitch_keycenter"): setValueFromOpcode(opcode, pitchKeycenter, Default::keyRange); diff --git a/src/sfizz/Region.h b/src/sfizz/Region.h index 196a9d15..d398a63e 100644 --- a/src/sfizz/Region.h +++ b/src/sfizz/Region.h @@ -9,6 +9,8 @@ #include "LeakDetector.h" #include "Defaults.h" #include "EGDescription.h" +#include "EQDescription.h" +#include "FilterDescription.h" #include "Opcode.h" #include "AudioBuffer.h" #include "MidiState.h" @@ -274,6 +276,9 @@ struct Region { CCMap> crossfadeCCOutRange { Default::crossfadeCCOutRange }; // xfout_loccN xfout_hiccN float rtDecay { Default::rtDecay }; // rt_decay + // Filters and EQs + std::vector equalizers; + std::vector filters; // Performance parameters: pitch uint8_t pitchKeycenter { Default::pitchKeycenter }; // pitch_keycenter diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index a5776691..9d9fdc53 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -295,6 +295,23 @@ bool sfz::Synth::loadSfzFile(const fs::path& file) region->registerNoteOff(*defaultSwitch, 0, 1.0); } + // Set the default frequencies on equalizers if needed + if (region->equalizers.size() > 0 + && region->equalizers[0].frequency == Default::eqFrequencyUnset) + { + region->equalizers[0].frequency = Default::eqFrequency1; + if (region->equalizers.size() > 1 + && region->equalizers[1].frequency == Default::eqFrequencyUnset) + { + region->equalizers[1].frequency = Default::eqFrequency2; + if (region->equalizers.size() > 2 + && region->equalizers[2].frequency == Default::eqFrequencyUnset) + { + region->equalizers[2].frequency = Default::eqFrequency3; + } + } + } + addEndpointsToVelocityCurve(*region); region->registerPitchWheel(0); region->registerAftertouch(0); diff --git a/tests/RegionT.cpp b/tests/RegionT.cpp index 92f0b61b..35ea0597 100644 --- a/tests/RegionT.cpp +++ b/tests/RegionT.cpp @@ -1029,6 +1029,294 @@ TEST_CASE("[Region] Parsing opcodes") region.parseOpcode({ "sostenuto_sw", "obladi" }); REQUIRE(region.checkSostenuto); } + + SECTION("Filter stacking and cutoffs") + { + REQUIRE(region.filters.empty()); + + region.parseOpcode({ "cutoff", "500" }); + REQUIRE(region.filters.size() == 1); + REQUIRE(region.filters[0].cutoff == 500.0f); + // Check filter defaults + REQUIRE(region.filters[0].keycenter == 60); + REQUIRE(region.filters[0].type == sfz::FilterType::kFilterLpf2p); + REQUIRE(region.filters[0].keytrack == 0); + REQUIRE(region.filters[0].veltrack == 0); + REQUIRE(region.filters[0].resonance == 0.0f); + REQUIRE(region.filters[0].cutoffCC.empty()); + REQUIRE(region.filters[0].resonanceCC.empty()); + + region.parseOpcode({ "cutoff2", "5000" }); + REQUIRE(region.filters.size() == 2); + REQUIRE(region.filters[1].cutoff == 5000.0f); + // Check filter defaults + REQUIRE(region.filters[1].keycenter == 60); + REQUIRE(region.filters[1].type == sfz::FilterType::kFilterLpf2p); + REQUIRE(region.filters[1].keytrack == 0); + REQUIRE(region.filters[1].veltrack == 0); + REQUIRE(region.filters[1].resonance == 0.0f); + REQUIRE(region.filters[1].cutoffCC.empty()); + REQUIRE(region.filters[1].resonanceCC.empty()); + + region.parseOpcode({ "cutoff4", "50" }); + REQUIRE(region.filters.size() == 4); + REQUIRE(region.filters[2].cutoff == 0.0f); + REQUIRE(region.filters[3].cutoff == 50.0f); + // Check filter defaults + REQUIRE(region.filters[2].keycenter == 60); + REQUIRE(region.filters[2].type == sfz::FilterType::kFilterLpf2p); + REQUIRE(region.filters[2].keytrack == 0); + REQUIRE(region.filters[2].veltrack == 0); + REQUIRE(region.filters[2].resonance == 0.0f); + REQUIRE(region.filters[2].cutoffCC.empty()); + REQUIRE(region.filters[2].resonanceCC.empty()); + REQUIRE(region.filters[3].keycenter == 60); + REQUIRE(region.filters[3].type == sfz::FilterType::kFilterLpf2p); + REQUIRE(region.filters[3].keytrack == 0); + REQUIRE(region.filters[3].veltrack == 0); + REQUIRE(region.filters[3].resonance == 0.0f); + REQUIRE(region.filters[3].cutoffCC.empty()); + REQUIRE(region.filters[3].resonanceCC.empty()); + } + + SECTION("Filter parameter dispatch") + { + region.parseOpcode({ "cutoff3", "50" }); + REQUIRE(region.filters.size() == 3); + REQUIRE(region.filters[2].cutoff == 50.0f); + region.parseOpcode({ "resonance2", "3" }); + REQUIRE(region.filters[1].resonance == 3.0f); + region.parseOpcode({ "fil2_veltrack", "-100" }); + REQUIRE(region.filters[1].veltrack == -100); + region.parseOpcode({ "fil3_keytrack", "100" }); + REQUIRE(region.filters[2].keytrack == 100); + REQUIRE(region.filters[0].cutoffCC.empty()); + region.parseOpcode({ "cutoff1_cc15", "210" }); + REQUIRE(region.filters[0].cutoffCC.contains(15)); + REQUIRE(region.filters[0].cutoffCC[15] == 210); + region.parseOpcode({ "resonance3_cc24", "10" }); + REQUIRE(region.filters[2].resonanceCC.contains(24)); + REQUIRE(region.filters[2].resonanceCC[24] == 10); + } + + SECTION("Filter values") + { + REQUIRE(region.filters.empty()); + + region.parseOpcode({ "cutoff", "500" }); + REQUIRE(region.filters.size() == 1); + REQUIRE(region.filters[0].cutoff == 500.0f); + region.parseOpcode({ "cutoff", "-100" }); + REQUIRE(region.filters[0].cutoff == 0.0f); + region.parseOpcode({ "cutoff", "2000000" }); + REQUIRE(region.filters[0].cutoff == 96000.0f); + + REQUIRE(region.filters[0].resonance == 0.0f); + region.parseOpcode({ "resonance", "5" }); + REQUIRE(region.filters[0].resonance == 5.0f); + region.parseOpcode({ "resonance", "-5" }); + REQUIRE(region.filters[0].resonance == 0.0f); + region.parseOpcode({ "resonance", "50" }); + REQUIRE(region.filters[0].resonance == 40.0f); + + REQUIRE(region.filters[0].veltrack == 0); + region.parseOpcode({ "fil_veltrack", "50" }); + REQUIRE(region.filters[0].veltrack == 50); + region.parseOpcode({ "fil_veltrack", "-5" }); + REQUIRE(region.filters[0].veltrack == -5); + region.parseOpcode({ "fil_veltrack", "10000" }); + REQUIRE(region.filters[0].veltrack == 9600); + region.parseOpcode({ "fil_veltrack", "-10000" }); + REQUIRE(region.filters[0].veltrack == -9600); + + REQUIRE(region.filters[0].keycenter == 60); + region.parseOpcode({ "fil_keycenter", "50" }); + REQUIRE(region.filters[0].keycenter == 50); + region.parseOpcode({ "fil_keycenter", "-2" }); + REQUIRE(region.filters[0].keycenter == 0); + region.parseOpcode({ "fil_keycenter", "1000" }); + REQUIRE(region.filters[0].keycenter == 127); + region.parseOpcode({ "fil_keycenter", "c4" }); + REQUIRE(region.filters[0].keycenter == 60); + } + + SECTION("Filter types") + { + REQUIRE(region.filters.empty()); + + region.parseOpcode({ "fil_type", "lpf_1p" }); + REQUIRE(region.filters.size() == 1); + REQUIRE(region.filters[0].type == sfz::FilterType::kFilterLpf1p); + region.parseOpcode({ "fil_type", "lpf_2p" }); + REQUIRE(region.filters[0].type == sfz::FilterType::kFilterLpf2p); + region.parseOpcode({ "fil_type", "hpf_1p" }); + REQUIRE(region.filters[0].type == sfz::FilterType::kFilterHpf1p); + region.parseOpcode({ "fil_type", "hpf_2p" }); + REQUIRE(region.filters[0].type == sfz::FilterType::kFilterHpf2p); + region.parseOpcode({ "fil_type", "bpf_2p" }); + REQUIRE(region.filters[0].type == sfz::FilterType::kFilterBpf2p); + region.parseOpcode({ "fil_type", "brf_2p" }); + REQUIRE(region.filters[0].type == sfz::FilterType::kFilterBrf2p); + region.parseOpcode({ "fil_type", "bpf_1p" }); + REQUIRE(region.filters[0].type == sfz::FilterType::kFilterBpf1p); + region.parseOpcode({ "fil_type", "brf_1p" }); + REQUIRE(region.filters[0].type == sfz::FilterType::kFilterBrf1p); + region.parseOpcode({ "fil_type", "apf_1p" }); + REQUIRE(region.filters[0].type == sfz::FilterType::kFilterApf1p); + region.parseOpcode({ "fil_type", "lpf_2p_sv" }); + REQUIRE(region.filters[0].type == sfz::FilterType::kFilterLpf2pSv); + region.parseOpcode({ "fil_type", "hpf_2p_sv" }); + REQUIRE(region.filters[0].type == sfz::FilterType::kFilterHpf2pSv); + region.parseOpcode({ "fil_type", "bpf_2p_sv" }); + REQUIRE(region.filters[0].type == sfz::FilterType::kFilterBpf2pSv); + region.parseOpcode({ "fil_type", "brf_2p_sv" }); + REQUIRE(region.filters[0].type == sfz::FilterType::kFilterBrf2pSv); + region.parseOpcode({ "fil_type", "lpf_4p" }); + REQUIRE(region.filters[0].type == sfz::FilterType::kFilterLpf4p); + region.parseOpcode({ "fil_type", "hpf_4p" }); + REQUIRE(region.filters[0].type == sfz::FilterType::kFilterHpf4p); + region.parseOpcode({ "fil_type", "lpf_6p" }); + REQUIRE(region.filters[0].type == sfz::FilterType::kFilterLpf6p); + region.parseOpcode({ "fil_type", "hpf_6p" }); + REQUIRE(region.filters[0].type == sfz::FilterType::kFilterHpf6p); + region.parseOpcode({ "fil_type", "pink" }); + REQUIRE(region.filters[0].type == sfz::FilterType::kFilterPink); + region.parseOpcode({ "fil_type", "lsh" }); + REQUIRE(region.filters[0].type == sfz::FilterType::kFilterLsh); + region.parseOpcode({ "fil_type", "hsh" }); + REQUIRE(region.filters[0].type == sfz::FilterType::kFilterHsh); + region.parseOpcode({ "fil_type", "peq" }); + REQUIRE(region.filters[0].type == sfz::FilterType::kFilterPeq); + region.parseOpcode({ "fil_type", "unknown" }); + REQUIRE(region.filters[0].type == sfz::FilterType::kFilterNone); + } + + SECTION("EQ stacking and gains") + { + REQUIRE(region.equalizers.empty()); + + region.parseOpcode({ "eq1_gain", "6" }); + REQUIRE(region.equalizers.size() == 1); + REQUIRE(region.equalizers[0].gain == 6.0f); + // Check defaults + REQUIRE(region.equalizers[0].bandwidth == 1.0f); + REQUIRE(region.equalizers[0].frequency == 0.0f); + REQUIRE(region.equalizers[0].vel2frequency == 0); + REQUIRE(region.equalizers[0].vel2gain == 0); + REQUIRE(region.equalizers[0].frequencyCC.empty()); + REQUIRE(region.equalizers[0].bandwidthCC.empty()); + REQUIRE(region.equalizers[0].gainCC.empty()); + + region.parseOpcode({ "eq2_gain", "-400" }); + REQUIRE(region.equalizers.size() == 2); + REQUIRE(region.equalizers[1].gain == -96.0f); + // Check defaults + REQUIRE(region.equalizers[1].bandwidth == 1.0f); + REQUIRE(region.equalizers[1].frequency == 0.0f); + REQUIRE(region.equalizers[1].vel2frequency == 0); + REQUIRE(region.equalizers[1].vel2gain == 0); + REQUIRE(region.equalizers[1].frequencyCC.empty()); + REQUIRE(region.equalizers[1].bandwidthCC.empty()); + REQUIRE(region.equalizers[1].gainCC.empty()); + + region.parseOpcode({ "eq4_gain", "50" }); + REQUIRE(region.equalizers.size() == 4); + REQUIRE(region.equalizers[2].gain == 0.0f); + REQUIRE(region.equalizers[3].gain == 24.0f); + // Check defaults + REQUIRE(region.equalizers[2].bandwidth == 1.0f); + REQUIRE(region.equalizers[2].frequency == 0.0f); + REQUIRE(region.equalizers[2].vel2frequency == 0); + REQUIRE(region.equalizers[2].vel2gain == 0); + REQUIRE(region.equalizers[2].frequencyCC.empty()); + REQUIRE(region.equalizers[2].bandwidthCC.empty()); + REQUIRE(region.equalizers[2].gainCC.empty()); + REQUIRE(region.equalizers[3].bandwidth == 1.0f); + REQUIRE(region.equalizers[3].frequency == 0.0f); + REQUIRE(region.equalizers[3].vel2frequency == 0); + REQUIRE(region.equalizers[3].vel2gain == 0); + REQUIRE(region.equalizers[3].frequencyCC.empty()); + REQUIRE(region.equalizers[3].bandwidthCC.empty()); + REQUIRE(region.equalizers[3].gainCC.empty()); + } + + SECTION("EQ parameter dispatch") + { + region.parseOpcode({ "eq3_bw", "2" }); + REQUIRE(region.equalizers.size() == 3); + REQUIRE(region.equalizers[2].bandwidth == 2.0f); + region.parseOpcode({ "eq1_gain", "-25" }); + REQUIRE(region.equalizers[0].gain == -25.0f); + region.parseOpcode({ "eq2_freq", "300" }); + REQUIRE(region.equalizers[1].frequency == 300.0f); + region.parseOpcode({ "eq3_vel2gain", "10" }); + REQUIRE(region.equalizers[2].vel2gain == 10.0f); + region.parseOpcode({ "eq1_vel2freq", "100" }); + REQUIRE(region.equalizers[0].vel2frequency == 100.0f); + REQUIRE(region.equalizers[0].bandwidthCC.empty()); + region.parseOpcode({ "eq1_bwcc24", "0.5" }); + REQUIRE(region.equalizers[0].bandwidthCC.contains(24)); + REQUIRE(region.equalizers[0].bandwidthCC[24] == 0.5f); + region.parseOpcode({ "eq1_bw_oncc24", "1.5" }); + REQUIRE(region.equalizers[0].bandwidthCC[24] == 1.5f); + region.parseOpcode({ "eq3_freqcc15", "10" }); + REQUIRE(region.equalizers[2].frequencyCC.contains(15)); + REQUIRE(region.equalizers[2].frequencyCC[15] == 10.0f); + region.parseOpcode({ "eq3_freq_oncc15", "20" }); + REQUIRE(region.equalizers[2].frequencyCC[15] == 20.0f); + region.parseOpcode({ "eq2_gaincc123", "2" }); + REQUIRE(region.equalizers[1].gainCC.contains(123)); + REQUIRE(region.equalizers[1].gainCC[123] == 2.0f); + region.parseOpcode({ "eq2_gain_oncc123", "-2" }); + REQUIRE(region.equalizers[1].gainCC[123] == -2.0f); + } + + SECTION("EQ parameter values") + { + region.parseOpcode({ "eq1_bw", "2" }); + REQUIRE(region.equalizers.size() == 1); + REQUIRE(region.equalizers[0].bandwidth == 2.0f); + region.parseOpcode({ "eq1_bw", "5" }); + REQUIRE(region.equalizers[0].bandwidth == 4.0f); + region.parseOpcode({ "eq1_bw", "0" }); + REQUIRE(region.equalizers[0].bandwidth == 0.001f); + region.parseOpcode({ "eq1_freq", "300" }); + REQUIRE(region.equalizers[0].frequency == 300.0f); + region.parseOpcode({ "eq1_freq", "-300" }); + REQUIRE(region.equalizers[0].frequency == 0.0f); + region.parseOpcode({ "eq1_freq", "35000" }); + REQUIRE(region.equalizers[0].frequency == 30000.0f); + region.parseOpcode({ "eq1_vel2gain", "4" }); + REQUIRE(region.equalizers[0].vel2gain == 4.0f); + region.parseOpcode({ "eq1_vel2gain", "25" }); + REQUIRE(region.equalizers[0].vel2gain == 24.0f); + region.parseOpcode({ "eq1_vel2gain", "-123" }); + REQUIRE(region.equalizers[0].vel2gain == -96.0f); + region.parseOpcode({ "eq1_vel2freq", "40" }); + REQUIRE(region.equalizers[0].vel2frequency == 40.0f); + region.parseOpcode({ "eq1_vel2freq", "35000" }); + REQUIRE(region.equalizers[0].vel2frequency == 30000.0f); + region.parseOpcode({ "eq1_vel2freq", "-35000" }); + REQUIRE(region.equalizers[0].vel2frequency == -30000.0f); + region.parseOpcode({ "eq1_bwcc15", "2" }); + REQUIRE(region.equalizers[0].bandwidthCC[15] == 2.0f); + region.parseOpcode({ "eq1_bwcc15", "-5" }); + REQUIRE(region.equalizers[0].bandwidthCC[15] == -4.0f); + region.parseOpcode({ "eq1_bwcc15", "5" }); + REQUIRE(region.equalizers[0].bandwidthCC[15] == 4.0f); + region.parseOpcode({ "eq1_gaincc15", "2" }); + REQUIRE(region.equalizers[0].gainCC[15] == 2.0f); + region.parseOpcode({ "eq1_gaincc15", "-500" }); + REQUIRE(region.equalizers[0].gainCC[15] == -96.0f); + region.parseOpcode({ "eq1_gaincc15", "50" }); + REQUIRE(region.equalizers[0].gainCC[15] == 24.0f); + region.parseOpcode({ "eq1_freqcc15", "200" }); + REQUIRE(region.equalizers[0].frequencyCC[15] == 200.0f); + region.parseOpcode({ "eq1_freqcc15", "-50000" }); + REQUIRE(region.equalizers[0].frequencyCC[15] == -30000.0f); + region.parseOpcode({ "eq1_freqcc15", "50000" }); + REQUIRE(region.equalizers[0].frequencyCC[15] == 30000.0f); + } } // Specific region bugs