diff --git a/src/sfizz/Defaults.cpp b/src/sfizz/Defaults.cpp index 792983ff..b30272dc 100644 --- a/src/sfizz/Defaults.cpp +++ b/src/sfizz/Defaults.cpp @@ -12,9 +12,10 @@ extern const OpcodeSpec offset { 0, Range(0, uint32_t_max), 0 extern const OpcodeSpec offsetMod { 0, Range(0, uint32_t_max), 0 }; extern const OpcodeSpec offsetRandom { 0, Range(0, uint32_t_max), 0 }; extern const OpcodeSpec sampleEnd { uint32_t_max, Range(0, uint32_t_max), kEnforceLowerBound }; -extern const OpcodeSpec sampleCount { 1, Range(1, uint32_t_max), 0 }; +extern const OpcodeSpec sampleCount { 0, Range(0, uint32_t_max), 0 }; extern const OpcodeSpec loopStart { 0, Range(0, uint32_t_max), 0 }; extern const OpcodeSpec loopEnd { uint32_t_max, Range(0, uint32_t_max), 0 }; +extern const OpcodeSpec loopCount { 0, Range(0, uint32_t_max), 0 }; extern const OpcodeSpec loopCrossfade { 1e-3, Range(1e-3, 1.0f), 0 }; extern const OpcodeSpec oscillator { OscillatorEnabled::Auto, Range(OscillatorEnabled::Auto, OscillatorEnabled::On), 0 }; extern const OpcodeSpec oscillatorPhase { 0.0f, Range(-1000.0f, 1000.0f), 0 }; diff --git a/src/sfizz/Defaults.h b/src/sfizz/Defaults.h index f778e09e..2290052b 100644 --- a/src/sfizz/Defaults.h +++ b/src/sfizz/Defaults.h @@ -134,6 +134,7 @@ namespace Default extern const OpcodeSpec sampleCount; extern const OpcodeSpec loopStart; extern const OpcodeSpec loopEnd; + extern const OpcodeSpec loopCount; extern const OpcodeSpec loopCrossfade; extern const OpcodeSpec oscillatorPhase; extern const OpcodeSpec oscillator; diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index 4fed8896..6283cbf4 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -122,7 +122,8 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) sampleEnd = opcode.read(Default::sampleEnd); break; case hash("count"): - sampleCount = opcode.read(Default::sampleCount); + sampleCount = opcode.readOptional(Default::sampleCount); + loopMode = LoopMode::one_shot; break; case hash("loop_mode"): // also loopmode loopMode = opcode.readOptional(Default::loopMode); @@ -130,6 +131,9 @@ bool sfz::Region::parseOpcode(const Opcode& rawOpcode) case hash("loop_end"): // also loopend loopRange.setEnd(opcode.read(Default::loopEnd)); break; + case hash("loop_count"): + loopCount = opcode.readOptional(Default::loopCount); + break; case hash("loop_start"): // also loopstart loopRange.setStart(opcode.read(Default::loopStart)); break; @@ -1657,7 +1661,7 @@ uint32_t sfz::Region::trueSampleEnd(Oversampling factor) const noexcept if (sampleEnd <= 0) return 0; - return min(static_cast(sampleEnd), loopRange.getEnd()) * static_cast(factor); + return static_cast(sampleEnd) * static_cast(factor); } uint32_t sfz::Region::loopStart(Oversampling factor) const noexcept diff --git a/src/sfizz/Region.h b/src/sfizz/Region.h index 98dda576..bb72cfd4 100644 --- a/src/sfizz/Region.h +++ b/src/sfizz/Region.h @@ -326,9 +326,10 @@ struct Region { int64_t offsetRandom { Default::offsetRandom }; // offset_random CCMap offsetCC { Default::offsetMod }; uint32_t sampleEnd { Default::sampleEnd }; // end - uint32_t sampleCount { Default::sampleCount }; // count + absl::optional sampleCount {}; // count absl::optional loopMode {}; // loopmode Range loopRange { Default::loopStart, Default::loopEnd }; //loopstart and loopend + absl::optional loopCount {}; // count float loopCrossfade { Default::loopCrossfade }; // loop_crossfade // Wavetable oscillator diff --git a/src/sfizz/SynthMessaging.cpp b/src/sfizz/SynthMessaging.cpp index 67407e3c..3b4b18bf 100644 --- a/src/sfizz/SynthMessaging.cpp +++ b/src/sfizz/SynthMessaging.cpp @@ -186,7 +186,10 @@ void sfz::Synth::dispatchMessage(Client& client, int delay, const char* path, co MATCH("/region&/count", "") { GET_REGION_OR_BREAK(indices[0]) - client.receive<'h'>(delay, path, region.sampleCount); + if (region.sampleCount) + client.receive<'h'>(delay, path, *region.sampleCount); + else + client.receive<'N'>(delay, path, {}); } break; MATCH("/region&/loop_range", "") { @@ -225,6 +228,14 @@ void sfz::Synth::dispatchMessage(Client& client, int delay, const char* path, co client.receive<'f'>(delay, path, region.loopCrossfade); } break; + MATCH("/region&/loop_count", "") { + GET_REGION_OR_BREAK(indices[0]) + if (region.loopCount) + client.receive<'h'>(delay, path, *region.loopCount); + else + client.receive<'N'>(delay, path, {}); + } break; + MATCH("/region&/group", "") { GET_REGION_OR_BREAK(indices[0]) client.receive<'h'>(delay, path, region.group); diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 33d2d5eb..e6a67170 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -171,6 +171,30 @@ struct Voice::Impl */ void updateLoopInformation() noexcept; + /** + * @brief Check whether the voice is released + * + * @return true + * @return false + */ + bool released() const noexcept; + + /** + * @brief Release the voice after a given delay + * + * @param delay + */ + void release(int delay) noexcept; + + /** + * @brief Off the voice (steal). This will respect the off mode of the region + * and set the envelopes if necessary. + * + * @param delay + * @param fast whether to apply a fast release regardless of the off mode + */ + void off(int delay, bool fast = false) noexcept; + const NumericId id_; StateListener* stateListener_ = nullptr; @@ -192,6 +216,9 @@ struct Voice::Impl int sourcePosition_ { 0 }; int initialDelay_ { 0 }; int age_ { 0 }; + uint32_t count_ { 1 }; + int sampleSize_ { 0 }; + struct { int start { 0 }; int end { 0 }; @@ -199,6 +226,7 @@ struct Voice::Impl int xfSize { 0 }; int xfOutStart { 0 }; int xfInStart { 0 }; + uint32_t restarts { 0 }; } loop_; FileDataHolder currentPromise_; @@ -416,6 +444,7 @@ void Voice::startVoice(Region* region, int delay, const TriggerEvent& event) noe impl.triggerDelay_ = delay; impl.initialDelay_ = delay + static_cast(region->getDelay() * impl.sampleRate_); impl.baseFrequency_ = impl.resources_.tuning.getFrequencyOfKey(impl.triggerEvent_.number); + impl.sampleSize_ = region->trueSampleEnd(impl.resources_.filePool.getOversamplingFactor()) - impl.sourcePosition_ - 1; impl.bendStepFactor_ = centsFactor(region->bendStep); impl.bendSmoother_.setSmoothing(region->bendSmooth, impl.sampleRate_); impl.bendSmoother_.reset(centsFactor(region->getBendInCents(impl.resources_.midiState.getPitchBend()))); @@ -445,29 +474,39 @@ bool Voice::isFree() const noexcept void Voice::release(int delay) noexcept { Impl& impl = *impl_; - if (impl.state_ != State::playing) + impl.release(delay); +} + +void Voice::Impl::release(int delay) noexcept +{ + if (state_ != State::playing) return; - if (!impl.region_->flexAmpEG) { - if (impl.egAmplitude_.getRemainingDelay() > delay) - impl.switchState(State::cleanMeUp); + if (!region_->flexAmpEG) { + if (egAmplitude_.getRemainingDelay() > delay) + switchState(State::cleanMeUp); } else { - if (impl.flexEGs_[*impl.region_->flexAmpEG]->getRemainingDelay() > static_cast(delay)) - impl.switchState(State::cleanMeUp); + if (flexEGs_[*region_->flexAmpEG]->getRemainingDelay() > static_cast(delay)) + switchState(State::cleanMeUp); } - impl.resources_.modMatrix.releaseVoice(impl.id_, impl.region_->getId(), delay); + resources_.modMatrix.releaseVoice(id_, region_->getId(), delay); } void Voice::off(int delay, bool fast) noexcept { Impl& impl = *impl_; - if (!impl.region_->flexAmpEG) { - if (impl.region_->offMode == OffMode::fast || fast) { - impl.egAmplitude_.setReleaseTime(Default::offTime); - } else if (impl.region_->offMode == OffMode::time) { - impl.egAmplitude_.setReleaseTime(impl.region_->offTime); + impl.off(delay, fast); +} + +void Voice::Impl::off(int delay, bool fast) noexcept +{ + if (!region_->flexAmpEG) { + if (region_->offMode == OffMode::fast || fast) { + egAmplitude_.setReleaseTime(Default::offTime); + } else if (region_->offMode == OffMode::time) { + egAmplitude_.setReleaseTime(region_->offTime); } } else { @@ -882,8 +921,13 @@ void Voice::Impl::fillWithData(AudioSpan buffer) noexcept // calculate loop characteristics const auto loop = this->loop_; - const bool isLooping = region_->shouldLoop() - && (static_cast(loop.end) < source.getNumFrames()); + + // Looping logic + const bool hasLoopSamples = static_cast(loop.end) < source.getNumFrames(); + const bool loopCountReached = region_->loopCount && loop_.restarts >= *region_->loopCount; + const bool loopContinuous = (region_->loopMode == LoopMode::loop_continuous); + const bool loopSustain = (region_->loopMode == LoopMode::loop_sustain) && !released(); + const bool shouldLoop = hasLoopSamples && (loopSustain || loopContinuous) && !loopCountReached; /* loop start loop end @@ -905,13 +949,7 @@ void Voice::Impl::fillWithData(AudioSpan buffer) noexcept enum PartitionType { kPartitionNormal, kPartitionLoopXfade }; SpanHolder> partitionBuffers[2]; - if (!isLooping) { - static const int starts[1] = { 0 }; - static const int types[1] = { kPartitionNormal }; - partitionStarts = absl::MakeSpan(const_cast(starts), 1); - partitionTypes = absl::MakeSpan(const_cast(types), 1); - numPartitions = 1; - } else { + if (shouldLoop) { for (auto& buf : partitionBuffers) { buf = resources_.bufferPool.getIndexBuffer(numSamples); if (!buf) @@ -921,63 +959,85 @@ void Voice::Impl::fillWithData(AudioSpan buffer) noexcept partitionTypes = *partitionBuffers[1]; // Note: partitions will be alternance of Normal/Xfade // computed along with index processing below + } else { + static const int starts[1] = { 0 }; + static const int types[1] = { kPartitionNormal }; + partitionStarts = absl::MakeSpan(const_cast(starts), 1); + partitionTypes = absl::MakeSpan(const_cast(types), 1); + numPartitions = 1; } - // index preprocessing for loops - if (isLooping) { - int oldIndex {}; - int oldPartitionType {}; - for (unsigned i = 0; i < numSamples; ++i) { - int index = (*indices)[i]; - - // wrap indices post loop-entry around the loop segment - int wrappedIndex = (index <= loop.end) ? index : - (loop.start + (index - loop.start) % loop.size); - (*indices)[i] = wrappedIndex; - - // identify the partition this index is in - bool xfading = wrappedIndex >= loop.start && wrappedIndex >= loop.xfOutStart; - int partitionType = xfading ? kPartitionLoopXfade : kPartitionNormal; - // if looping or entering a different type, start a new partition - bool start = i == 0 || wrappedIndex < oldIndex || partitionType != oldPartitionType; - if (start) { - partitionStarts[numPartitions] = i; - partitionTypes[numPartitions] = partitionType; - ++numPartitions; - } - - oldIndex = wrappedIndex; - oldPartitionType = partitionType; - } - } - // index preprocessing for one-shots - else { - // cut short the voice at the instant of reaching end of sample - const auto sampleEnd = min( + const auto sampleEnd = min( int(region_->trueSampleEnd(resources_.filePool.getOversamplingFactor())), int(currentPromise_->information.end), int(source.getNumFrames())) - 1; - for (unsigned i = 0; i < numSamples; ++i) { + int blockRestarts { 0 }; + int oldIndex {}; + int oldPartitionType {}; + + const auto addPartitionIfNecessary = [&] (unsigned blockIndex, int wrappedIndex, bool wrapped) { + const bool xfading = wrappedIndex >= loop.start && wrappedIndex >= loop.xfOutStart; + const int partitionType = xfading ? kPartitionLoopXfade : kPartitionNormal; + + // if looping or entering a different type, start a new partition + bool start = blockIndex == 0 || wrapped || partitionType != oldPartitionType; + if (start) { + partitionStarts[numPartitions] = blockIndex; + partitionTypes[numPartitions] = partitionType; + ++numPartitions; + } + + oldIndex = wrappedIndex; + oldPartitionType = partitionType; + }; + + if (shouldLoop) { + unsigned i = 0; + while (i < numSamples) { + int wrappedIndex = (*indices)[i] - loop.size * blockRestarts; + if (wrappedIndex > loop.end) { + wrappedIndex -= loop.size; + blockRestarts += 1; + loop_.restarts += 1; + } + (*indices)[i] = wrappedIndex; + const bool wrapped = wrappedIndex < oldIndex; + + // identify the partition this index is in + addPartitionIfNecessary(i, wrappedIndex, wrapped); + i++; + + // Break if we reached the loop count + if (wrapped && region_->loopCount && loop_.restarts >= *region_->loopCount) + break; + } + + while (i < numSamples) { // In case we released within the block, continue as if it were a one-shot + (*indices)[i] -= loop.size * blockRestarts; if ((*indices)[i] >= sampleEnd) { -#ifndef NDEBUG - // Check for underflow - if (source.getNumFrames() - 1 < currentPromise_->information.end) { - DBG("[sfizz] Underflow: source available samples " - << source.getNumFrames() << "/" - << currentPromise_->information.end - << " for sample " << *region_->sampleId); - } -#endif - if (!region_->flexAmpEG) { - egAmplitude_.setReleaseTime(0.0f); - egAmplitude_.startRelease(i); - } - else { - // TODO(jpc): Flex AmpEG - flexEGs_[*region_->flexAmpEG]->release(i); + fill(indices->subspan(i), sampleEnd); + fill(coeffs->subspan(i), 0x1.fffffep-1); + break; + } + i++; + } + } else { // One shots and loops that have released (for loop sustain) or ended (with loop counts) + for (unsigned i = 0; i < numSamples; ++i) { + (*indices)[i] -= sampleSize_ * blockRestarts; + + if ((*indices)[i] >= sampleEnd) { + if (region_->sampleCount && count_ < *region_->sampleCount && !region_->shouldLoop()) { + (*indices)[i] -= sampleSize_; + blockRestarts += 1; + count_ += 1; + continue; } + + if (!released()) + off(i, true); + fill(indices->subspan(i), sampleEnd); fill(coeffs->subspan(i), 1.0f); break; @@ -1395,6 +1455,14 @@ void Voice::Impl::fillWithGenerator(AudioSpan buffer) noexcept #endif } +bool Voice::Impl::released() const noexcept +{ + if (!region_->flexAmpEG) + return egAmplitude_.isReleased(); + else + return flexEGs_[*region_->flexAmpEG]->isReleased(); +} + bool Voice::checkOffGroup(const Region* other, int delay, int noteNumber) noexcept { Impl& impl = *impl_; @@ -1419,6 +1487,7 @@ void Voice::reset() noexcept impl.currentPromise_.reset(); impl.sourcePosition_ = 0; impl.age_ = 0; + impl.count_ = 1; impl.floatPositionOffset_ = 0.0f; impl.noteIsOff_ = false; @@ -1443,6 +1512,7 @@ void Voice::Impl::resetLoopInformation() noexcept loop_.xfSize = 0; loop_.xfOutStart = 0; loop_.xfInStart = 0; + loop_.restarts = 0; } void Voice::Impl::updateLoopInformation() noexcept @@ -1500,10 +1570,8 @@ bool Voice::releasedOrFree() const noexcept Impl& impl = *impl_; if (impl.state_ != State::playing) return true; - if (!impl.region_->flexAmpEG) - return impl.egAmplitude_.isReleased(); - else - return impl.flexEGs_[*impl.region_->flexAmpEG]->isReleased(); + + return impl.released(); } void Voice::setMaxFiltersPerVoice(size_t numFilters) diff --git a/src/sfizz/Voice.h b/src/sfizz/Voice.h index b6a07d38..34885cbb 100644 --- a/src/sfizz/Voice.h +++ b/src/sfizz/Voice.h @@ -309,7 +309,6 @@ public: * @brief Release the voice after a given delay * * @param delay - * @param fastRelease whether to do a normal release or cut the voice abruptly */ void release(int delay) noexcept; diff --git a/tests/RegionValuesT.cpp b/tests/RegionValuesT.cpp index 24467ea3..91df9ba0 100644 --- a/tests/RegionValuesT.cpp +++ b/tests/RegionValuesT.cpp @@ -237,9 +237,9 @@ TEST_CASE("[Values] Count") synth.dispatchMessage(client, 0, "/region1/count", "", nullptr); synth.dispatchMessage(client, 0, "/region2/count", "", nullptr); std::vector expected { - "/region0/count,h : { 1 }", + "/region0/count,N : { }", "/region1/count,h : { 2 }", - "/region2/count,h : { 1 }", + "/region2/count,N : { }", }; REQUIRE(messageList == expected); } @@ -350,6 +350,29 @@ TEST_CASE("[Values] Loop crossfade") REQUIRE(messageList == expected); } +TEST_CASE("[Values] Loop count") +{ + Synth synth; + 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_count=2 + sample=kick.wav loop_count=-1 + )"); + synth.dispatchMessage(client, 0, "/region0/loop_count", "", nullptr); + synth.dispatchMessage(client, 0, "/region1/loop_count", "", nullptr); + synth.dispatchMessage(client, 0, "/region2/loop_count", "", nullptr); + std::vector expected { + "/region0/loop_count,N : { }", + "/region1/loop_count,h : { 2 }", + "/region2/loop_count,N : { }", + }; + REQUIRE(messageList == expected); +} + + TEST_CASE("[Values] Group") { Synth synth;