diff --git a/src/sfizz/Defaults.cpp b/src/sfizz/Defaults.cpp index 2d2886f0..cb0f98d1 100644 --- a/src/sfizz/Defaults.cpp +++ b/src/sfizz/Defaults.cpp @@ -23,10 +23,12 @@ FloatSpec delayMod { 0.0f, {0.0f, 100.0f}, kPermissiveBounds }; Int64Spec offset { 0, {0, uint32_t_max}, kPermissiveBounds }; Int64Spec offsetMod { 0, {0, uint32_t_max}, kPermissiveBounds }; Int64Spec offsetRandom { 0, {0, uint32_t_max}, kPermissiveBounds }; -UInt32Spec sampleEnd { uint32_t_max, {0, uint32_t_max}, kEnforceBounds }; +Int64Spec sampleEnd { uint32_t_max, {0, uint32_t_max}, kEnforceBounds }; +Int64Spec sampleEndMod { 0, {-uint32_t_max, uint32_t_max}, kPermissiveBounds }; UInt32Spec sampleCount { 0, {0, uint32_t_max}, kEnforceUpperBound }; -UInt32Spec loopStart { 0, {0, uint32_t_max}, kEnforceUpperBound }; -UInt32Spec loopEnd { uint32_t_max, {0, uint32_t_max}, kEnforceUpperBound }; +Int64Spec loopStart { 0, {0, uint32_t_max}, kEnforceUpperBound }; +Int64Spec loopEnd { uint32_t_max, {0, uint32_t_max}, kEnforceUpperBound }; +Int64Spec loopMod { 0, {-uint32_t_max, uint32_t_max}, kPermissiveBounds }; UInt32Spec loopCount { 0, {0, uint32_t_max}, kEnforceUpperBound }; FloatSpec loopCrossfade { 1e-3, {1e-3, 1.0f}, kEnforceLowerBound|kPermissiveUpperBound }; ESpec oscillator { OscillatorEnabled::Auto, {OscillatorEnabled::Auto, OscillatorEnabled::On}, 0 }; diff --git a/src/sfizz/Defaults.h b/src/sfizz/Defaults.h index 70735abf..0fca0173 100644 --- a/src/sfizz/Defaults.h +++ b/src/sfizz/Defaults.h @@ -123,10 +123,12 @@ namespace Default extern const OpcodeSpec offset; extern const OpcodeSpec offsetMod; extern const OpcodeSpec offsetRandom; - extern const OpcodeSpec sampleEnd; + extern const OpcodeSpec sampleEnd; + extern const OpcodeSpec sampleEndMod; extern const OpcodeSpec sampleCount; - extern const OpcodeSpec loopStart; - extern const OpcodeSpec loopEnd; + extern const OpcodeSpec loopStart; + extern const OpcodeSpec loopEnd; + extern const OpcodeSpec loopMod; extern const OpcodeSpec loopCount; extern const OpcodeSpec loopCrossfade; extern const OpcodeSpec oscillatorPhase; diff --git a/src/sfizz/FilePool.cpp b/src/sfizz/FilePool.cpp index db5688c0..9689c554 100644 --- a/src/sfizz/FilePool.cpp +++ b/src/sfizz/FilePool.cpp @@ -256,7 +256,8 @@ absl::optional sfz::FilePool::getFileInformation(const Fil if (haveInstrumentInfo && instrumentInfo.loop_count > 0) { returnedValue.hasLoop = true; returnedValue.loopStart = instrumentInfo.loops[0].start; - returnedValue.loopEnd = min(returnedValue.end, instrumentInfo.loops[0].end - 1); + returnedValue.loopEnd = + min(returnedValue.end, static_cast(instrumentInfo.loops[0].end - 1)); } } else { // TODO loops ignored when reversed diff --git a/src/sfizz/FilePool.h b/src/sfizz/FilePool.h index 938061bb..7f61e2c8 100644 --- a/src/sfizz/FilePool.h +++ b/src/sfizz/FilePool.h @@ -51,10 +51,10 @@ using FileAudioBuffer = AudioBuffer; struct FileInformation { - uint32_t end { Default::sampleEnd }; - uint32_t maxOffset { 0 }; - uint32_t loopStart { Default::loopStart }; - uint32_t loopEnd { Default::loopEnd }; + int64_t end { Default::sampleEnd }; + int64_t maxOffset { 0 }; + int64_t loopStart { Default::loopStart }; + int64_t loopEnd { Default::loopEnd }; bool hasLoop { false }; double sampleRate { config::defaultSampleRate }; int numChannels { 0 }; diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index cfc84355..7493e205 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -108,6 +108,12 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) case hash("end"): sampleEnd = opcode.read(Default::sampleEnd); break; + case hash("end_oncc&"): // also end_cc& + if (opcode.parameters.back() > config::numCCs) + return false; + + endCC[opcode.parameters.back()] = opcode.read(Default::sampleEndMod); + break; case hash("count"): sampleCount = opcode.readOptional(Default::sampleCount); loopMode = LoopMode::one_shot; @@ -124,6 +130,18 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) case hash("loop_start"): // also loopstart loopRange.setStart(opcode.read(Default::loopStart)); break; + case hash("loop_start_oncc&"): // also loop_start_cc& + if (opcode.parameters.back() > config::numCCs) + return false; + + loopStartCC[opcode.parameters.back()] = opcode.read(Default::loopMod); + break; + case hash("loop_end_oncc&"): // also loop_end_cc& + if (opcode.parameters.back() > config::numCCs) + return false; + + loopEndCC[opcode.parameters.back()] = opcode.read(Default::loopMod); + break; case hash("loop_crossfade"): loopCrossfade = opcode.read(Default::loopCrossfade); break; @@ -1552,6 +1570,7 @@ uint64_t sfz::Region::getOffset(const MidiState& midiState, Oversampling factor) uint64_t finalOffset = offset + offsetDistribution(Random::randomGenerator); for (const auto& mod: offsetCC) finalOffset += static_cast(mod.data * midiState.getCCValue(mod.cc)); + return Default::offset.bounds.clamp(finalOffset) * static_cast(factor); } @@ -1562,25 +1581,38 @@ float sfz::Region::getDelay(const MidiState& midiState) const noexcept finalDelay += delayDistribution(Random::randomGenerator); for (const auto& mod: delayCC) finalDelay += mod.data * midiState.getCCValue(mod.cc); + return Default::delay.bounds.clamp(finalDelay); } -uint32_t sfz::Region::trueSampleEnd(Oversampling factor) const noexcept +uint32_t sfz::Region::getSampleEnd(MidiState& midiState, Oversampling factor) const noexcept { - if (sampleEnd <= 0) - return 0; + int64_t end = sampleEnd; + for (const auto& mod: endCC) + end += static_cast(mod.data * midiState.getCCValue(mod.cc)); - return static_cast(sampleEnd) * static_cast(factor); + end = clamp(end, int64_t { 0 }, sampleEnd); + return static_cast(end) * static_cast(factor); } -uint32_t sfz::Region::loopStart(Oversampling factor) const noexcept +uint32_t sfz::Region::loopStart(MidiState& midiState, Oversampling factor) const noexcept { - return loopRange.getStart() * static_cast(factor); + auto start = loopRange.getStart(); + for (const auto& mod: loopStartCC) + start += static_cast(mod.data * midiState.getCCValue(mod.cc)); + + start = clamp(start, int64_t { 0 }, sampleEnd); + return static_cast(start) * static_cast(factor); } -uint32_t sfz::Region::loopEnd(Oversampling factor) const noexcept +uint32_t sfz::Region::loopEnd(MidiState& midiState, Oversampling factor) const noexcept { - return loopRange.getEnd() * static_cast(factor); + auto end = loopRange.getEnd(); + for (const auto& mod: loopEndCC) + end += static_cast(mod.data * midiState.getCCValue(mod.cc)); + + end = clamp(end, int64_t { 0 }, sampleEnd); + return static_cast(end) * static_cast(factor); } float sfz::Region::getNoteGain(int noteNumber, float velocity) const noexcept diff --git a/src/sfizz/Region.h b/src/sfizz/Region.h index 8c77d145..0cfdb921 100644 --- a/src/sfizz/Region.h +++ b/src/sfizz/Region.h @@ -180,7 +180,7 @@ struct Region { * * @return uint32_t */ - uint32_t trueSampleEnd(Oversampling factor = Oversampling::x1) const noexcept; + uint32_t getSampleEnd(MidiState& midiState, Oversampling factor = Oversampling::x1) const noexcept; /** * @brief Parse a new opcode into the region to fill in the proper parameters. * This must be called multiple times for each opcode applying to this region. @@ -259,8 +259,8 @@ struct Region { void offsetAllKeys(int offset) noexcept; - uint32_t loopStart(Oversampling factor = Oversampling::x1) const noexcept; - uint32_t loopEnd(Oversampling factor = Oversampling::x1) const noexcept; + uint32_t loopStart(MidiState& midiState, Oversampling factor = Oversampling::x1) const noexcept; + uint32_t loopEnd(MidiState& midiState, Oversampling factor = Oversampling::x1) const noexcept; /** * @brief Get the gain this region contributes into the input of the Nth @@ -305,10 +305,13 @@ struct Region { int64_t offset { Default::offset }; // offset int64_t offsetRandom { Default::offsetRandom }; // offset_random CCMap offsetCC { Default::offsetMod }; - uint32_t sampleEnd { Default::sampleEnd }; // end + int64_t sampleEnd { Default::sampleEnd }; // end + CCMap endCC { Default::sampleEndMod }; absl::optional sampleCount {}; // count absl::optional loopMode {}; // loopmode - UncheckedRange loopRange { Default::loopStart, Default::loopEnd }; //loopstart and loopend + UncheckedRange loopRange { Default::loopStart, Default::loopEnd }; //loopstart and loopend + CCMap loopStartCC { Default::sampleEndMod }; + CCMap loopEndCC { Default::sampleEndMod }; absl::optional loopCount {}; // count float loopCrossfade { Default::loopCrossfade }; // loop_crossfade diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index ee9fbd8d..b96a6d61 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -590,7 +590,7 @@ void Synth::Impl::finalizeSfzLoad() } if (!region.isOscillator()) { - region.sampleEnd = std::min(region.sampleEnd, fileInformation->end); + region.sampleEnd = min(region.sampleEnd, fileInformation->end); if (fileInformation->hasLoop) { if (region.loopRange.getStart() == Default::loopStart) diff --git a/src/sfizz/SynthMessaging.cpp b/src/sfizz/SynthMessaging.cpp index 24de4b96..238fb265 100644 --- a/src/sfizz/SynthMessaging.cpp +++ b/src/sfizz/SynthMessaging.cpp @@ -165,6 +165,11 @@ void sfz::Synth::dispatchMessage(Client& client, int delay, const char* path, co client.receive<'h'>(delay, path, region.sampleEnd); } break; + MATCH("/region&/end_cc&", "") { + GET_REGION_OR_BREAK(indices[0]) + client.receive<'h'>(delay, path, region.endCC.getWithDefault(indices[1])); + } break; + MATCH("/region&/enabled", "") { GET_REGION_OR_BREAK(indices[0]) if (region.disabled()) { @@ -208,6 +213,16 @@ void sfz::Synth::dispatchMessage(Client& client, int delay, const char* path, co client.receive(delay, path, "hh", args); } break; + MATCH("/region&/loop_start_cc&", "") { + GET_REGION_OR_BREAK(indices[0]) + client.receive<'h'>(delay, path, region.loopStartCC.getWithDefault(indices[1])); + } break; + + MATCH("/region&/loop_end_cc&", "") { + GET_REGION_OR_BREAK(indices[0]) + client.receive<'h'>(delay, path, region.loopEndCC.getWithDefault(indices[1])); + } break; + MATCH("/region&/loop_mode", "") { GET_REGION_OR_BREAK(indices[0]) if (!region.loopMode) { diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index cde6047f..64c7efa5 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -459,7 +459,7 @@ bool Voice::startVoice(Layer* layer, int delay, const TriggerEvent& event) noexc impl.triggerDelay_ = delay; impl.initialDelay_ = delay + static_cast(region.getDelay(resources.midiState) * impl.sampleRate_); impl.baseFrequency_ = resources.tuning.getFrequencyOfKey(impl.triggerEvent_.number); - impl.sampleSize_ = region.trueSampleEnd(resources.filePool.getOversamplingFactor()) - impl.sourcePosition_ - 1; + impl.sampleSize_ = region.getSampleEnd(resources.midiState, resources.filePool.getOversamplingFactor()) - impl.sourcePosition_ - 1; impl.bendStepFactor_ = centsFactor(region.bendStep); impl.bendSmoother_.setSmoothing(region.bendSmooth, impl.sampleRate_); impl.bendSmoother_.reset(centsFactor(region.getBendInCents(resources.midiState.getPitchBend()))); @@ -578,7 +578,6 @@ void Voice::registerNoteOff(int delay, int noteNumber, float velocity) noexcept void Voice::registerCC(int delay, int ccNumber, float ccValue) noexcept { - ASSERT(ccValue >= 0.0 && ccValue <= 1.0); Impl& impl = *impl_; if (impl.region_ == nullptr) return; @@ -986,7 +985,8 @@ void Voice::Impl::fillWithData(AudioSpan buffer) noexcept add1(sourcePosition_, *indices); } - // calculate loop characteristics + // Update loop characteristics with the current CC state + updateLoopInformation(); const auto loop = this->loop_; // Looping logic @@ -1035,7 +1035,7 @@ void Voice::Impl::fillWithData(AudioSpan buffer) noexcept } const auto sampleEnd = min( - int(region_->trueSampleEnd(resources_.filePool.getOversamplingFactor())), + int(sampleSize_), int(currentPromise_->information.end), int(source.getNumFrames())) - 1; @@ -1591,12 +1591,14 @@ void Voice::Impl::updateLoopInformation() noexcept if (!region_->shouldLoop()) return; - const auto& info = currentPromise_->information; - const auto factor = resources_.filePool.getOversamplingFactor(); - const auto rate = info.sampleRate; - loop_.end = static_cast(region_->loopEnd(factor)); - loop_.start = static_cast(region_->loopStart(factor)); + Resources& resources = resources_; + const FileInformation& info = currentPromise_->information; + const Oversampling factor = resources_.filePool.getOversamplingFactor(); + const double rate = info.sampleRate; + + loop_.start = static_cast(region_->loopStart(resources.midiState, factor)); + loop_.end = max(static_cast(region_->loopEnd(resources.midiState, factor)), loop_.start); loop_.size = loop_.end + 1 - loop_.start; loop_.xfSize = static_cast(lroundPositive(region_->loopCrossfade * rate)); // Clamp the crossfade to the part available before the loop starts diff --git a/tests/FilesT.cpp b/tests/FilesT.cpp index 6fc1e773..fa48d801 100644 --- a/tests/FilesT.cpp +++ b/tests/FilesT.cpp @@ -535,9 +535,9 @@ TEST_CASE("[Files] Looped regions taken from files and possibly overriden") REQUIRE( synth.getRegionView(1)->loopMode == LoopMode::no_loop ); REQUIRE( synth.getRegionView(2)->loopMode == LoopMode::loop_continuous ); - REQUIRE(synth.getRegionView(0)->loopRange == Range { 77554, 186581 }); - REQUIRE(synth.getRegionView(1)->loopRange == Range { 77554, 186581 }); - REQUIRE(synth.getRegionView(2)->loopRange == Range { 4, 124 }); + REQUIRE(synth.getRegionView(0)->loopRange == Range { 77554, 186581 }); + REQUIRE(synth.getRegionView(1)->loopRange == Range { 77554, 186581 }); + REQUIRE(synth.getRegionView(2)->loopRange == Range { 4, 124 }); } TEST_CASE("[Files] Looped regions can start at 0") @@ -548,7 +548,7 @@ TEST_CASE("[Files] Looped regions can start at 0") )"); REQUIRE( synth.getNumRegions() == 1 ); REQUIRE( synth.getRegionView(0)->loopMode == LoopMode::loop_continuous ); - REQUIRE( synth.getRegionView(0)->loopRange == Range { 0, synth.getRegionView(0)->sampleEnd } ); + REQUIRE( synth.getRegionView(0)->loopRange == Range { 0, synth.getRegionView(0)->sampleEnd } ); } TEST_CASE("[Synth] Release triggers automatically sets the loop mode") diff --git a/tests/RegionValuesT.cpp b/tests/RegionValuesT.cpp index 1feb9cf1..3f069900 100644 --- a/tests/RegionValuesT.cpp +++ b/tests/RegionValuesT.cpp @@ -189,30 +189,55 @@ TEST_CASE("[Values] End") std::vector messageList; Client client(&messageList); client.setReceiveCallback(&simpleMessageReceiver); - synth.loadSfzString(fs::current_path() / "tests/TestFiles/value_tests.sfz", R"( - sample=kick.wav end=194 - sample=kick.wav end=-1 - sample=kick.wav end=0 - sample=kick.wav end=194 end=-1 - sample=kick.wav end=0 end=194 - )"); - synth.dispatchMessage(client, 0, "/region0/end", "", nullptr); - synth.dispatchMessage(client, 0, "/region0/enabled", "", nullptr); - synth.dispatchMessage(client, 0, "/region1/enabled", "", nullptr); - synth.dispatchMessage(client, 0, "/region2/enabled", "", nullptr); - synth.dispatchMessage(client, 0, "/region3/enabled", "", nullptr); - synth.dispatchMessage(client, 0, "/region4/enabled", "", nullptr); - synth.dispatchMessage(client, 0, "/region4/end", "", nullptr); - std::vector expected { - "/region0/end,h : { 194 }", - "/region0/enabled,T : { }", - "/region1/enabled,F : { }", - "/region2/enabled,F : { }", - "/region3/enabled,F : { }", - "/region4/enabled,T : { }", - "/region4/end,h : { 194 }", - }; - REQUIRE(messageList == expected); + SECTION("Basic") + { + synth.loadSfzString(fs::current_path() / "tests/TestFiles/value_tests.sfz", R"( + sample=kick.wav end=194 + sample=kick.wav end=-1 + sample=kick.wav end=0 + sample=kick.wav end=194 end=-1 + sample=kick.wav end=0 end=194 + )"); + synth.dispatchMessage(client, 0, "/region0/end", "", nullptr); + synth.dispatchMessage(client, 0, "/region0/enabled", "", nullptr); + synth.dispatchMessage(client, 0, "/region1/enabled", "", nullptr); + synth.dispatchMessage(client, 0, "/region2/enabled", "", nullptr); + synth.dispatchMessage(client, 0, "/region3/enabled", "", nullptr); + synth.dispatchMessage(client, 0, "/region4/enabled", "", nullptr); + synth.dispatchMessage(client, 0, "/region4/end", "", nullptr); + std::vector expected { + "/region0/end,h : { 194 }", + "/region0/enabled,T : { }", + "/region1/enabled,F : { }", + "/region2/enabled,F : { }", + "/region3/enabled,F : { }", + "/region4/enabled,T : { }", + "/region4/end,h : { 194 }", + }; + REQUIRE(messageList == expected); + } + SECTION("CC") + { + synth.loadSfzString(fs::current_path() / "tests/TestFiles/value_tests.sfz", R"( + sample=kick.wav + sample=kick.wav end_cc12=12 + sample=kick.wav end_oncc12=-12 + sample=kick.wav end_cc14=14 end_cc12=12 end_oncc12=-12 + )"); + synth.dispatchMessage(client, 0, "/region0/end_cc12", "", nullptr); + synth.dispatchMessage(client, 0, "/region1/end_cc12", "", nullptr); + synth.dispatchMessage(client, 0, "/region2/end_cc12", "", nullptr); + synth.dispatchMessage(client, 0, "/region3/end_cc14", "", nullptr); + synth.dispatchMessage(client, 0, "/region3/end_cc12", "", nullptr); + std::vector expected { + "/region0/end_cc12,h : { 0 }", + "/region1/end_cc12,h : { 12 }", + "/region2/end_cc12,h : { -12 }", + "/region3/end_cc14,h : { 14 }", + "/region3/end_cc12,h : { -12 }", + }; + REQUIRE(messageList == expected); + } } TEST_CASE("[Values] Count") @@ -305,23 +330,49 @@ TEST_CASE("[Values] Loop range") std::vector messageList; Client client(&messageList); client.setReceiveCallback(&simpleMessageReceiver); - synth.loadSfzString(fs::current_path() / "tests/TestFiles/value_tests.sfz", R"( - sample=kick.wav - sample=kick.wav loop_start=10 loop_end=100 - sample=kick.wav loopstart=10 loopend=100 - sample=kick.wav loop_start=-1 loopend=-100 - )"); - synth.dispatchMessage(client, 0, "/region0/loop_range", "", nullptr); - synth.dispatchMessage(client, 0, "/region1/loop_range", "", nullptr); - synth.dispatchMessage(client, 0, "/region2/loop_range", "", nullptr); - synth.dispatchMessage(client, 0, "/region3/loop_range", "", nullptr); - std::vector expected { - "/region0/loop_range,hh : { 0, 44011 }", // Default loop points in the file - "/region1/loop_range,hh : { 10, 100 }", - "/region2/loop_range,hh : { 10, 100 }", - "/region3/loop_range,hh : { 0, 44011 }", - }; - REQUIRE(messageList == expected); + SECTION("Basic") + { + synth.loadSfzString(fs::current_path() / "tests/TestFiles/value_tests.sfz", R"( + sample=kick.wav + sample=kick.wav loop_start=10 loop_end=100 + sample=kick.wav loopstart=10 loopend=100 + sample=kick.wav loop_start=-1 loopend=-100 + )"); + synth.dispatchMessage(client, 0, "/region0/loop_range", "", nullptr); + synth.dispatchMessage(client, 0, "/region1/loop_range", "", nullptr); + synth.dispatchMessage(client, 0, "/region2/loop_range", "", nullptr); + synth.dispatchMessage(client, 0, "/region3/loop_range", "", nullptr); + std::vector expected { + "/region0/loop_range,hh : { 0, 44011 }", // Default loop points in the file + "/region1/loop_range,hh : { 10, 100 }", + "/region2/loop_range,hh : { 10, 100 }", + "/region3/loop_range,hh : { 0, 44011 }", + }; + REQUIRE(messageList == expected); + } + SECTION("CC") + { + synth.loadSfzString(fs::current_path() / "tests/TestFiles/value_tests.sfz", R"( + sample=kick.wav + sample=kick.wav loop_start_cc12=10 loop_end_cc14=-100 + sample=kick.wav loop_start_oncc12=-10 loop_end_oncc14=100 + )"); + synth.dispatchMessage(client, 0, "/region0/loop_start_cc12", "", nullptr); + synth.dispatchMessage(client, 0, "/region0/loop_end_cc14", "", nullptr); + synth.dispatchMessage(client, 0, "/region1/loop_start_cc12", "", nullptr); + synth.dispatchMessage(client, 0, "/region1/loop_end_cc14", "", nullptr); + synth.dispatchMessage(client, 0, "/region2/loop_start_cc12", "", nullptr); + synth.dispatchMessage(client, 0, "/region2/loop_end_cc14", "", nullptr); + std::vector expected { + "/region0/loop_start_cc12,h : { 0 }", + "/region0/loop_end_cc14,h : { 0 }", + "/region1/loop_start_cc12,h : { 10 }", + "/region1/loop_end_cc14,h : { -100 }", + "/region2/loop_start_cc12,h : { -10 }", + "/region2/loop_end_cc14,h : { 100 }", + }; + REQUIRE(messageList == expected); + } } TEST_CASE("[Values] Loop crossfade")