diff --git a/benchmarks/BM_envelopes.cpp b/benchmarks/BM_envelopes.cpp index 67ec5dcf..836d6b0a 100644 --- a/benchmarks/BM_envelopes.cpp +++ b/benchmarks/BM_envelopes.cpp @@ -10,7 +10,7 @@ #include #include #include -#include "EventEnvelopes.h" +#include "SfzHelpers.h" #include "absl/types/span.h" class EnvelopeFixture : public benchmark::Fixture { @@ -29,45 +29,55 @@ public: } std::random_device rd { }; std::mt19937 gen { rd() }; - std::uniform_real_distribution dist { 1, 30 }; + std::uniform_real_distribution dist { 2, 30 }; std::vector input; std::vector output; }; BENCHMARK_DEFINE_F(EnvelopeFixture, Linear)(benchmark::State& state) { - sfz::LinearEnvelope envelope; for (auto _ : state) { - envelope.registerEvent(static_cast(state.range(0) - 1), dist(gen)); - envelope.getBlock(absl::MakeSpan(output)); + sfz::EventVector events { + { 0, 0.0f }, + { static_cast(state.range(0) - 1), dist(gen) } + }; + linearEnvelope(events, absl::MakeSpan(output), [](float x) { return x; }); } } BENCHMARK_DEFINE_F(EnvelopeFixture, LinearQuantized)(benchmark::State& state) { - sfz::LinearEnvelope envelope; for (auto _ : state) { - envelope.registerEvent(static_cast(state.range(0) - 1), dist(gen)); - envelope.getQuantizedBlock(absl::MakeSpan(output), 0.5); + sfz::EventVector events { + { 0, 0.0f }, + { static_cast(state.range(0) - 1), dist(gen) } + }; + linearEnvelope( + events, absl::MakeSpan(output), [](float x) { return x; }, 0.5); } } BENCHMARK_DEFINE_F(EnvelopeFixture, Multiplicative)(benchmark::State& state) { - sfz::MultiplicativeEnvelope envelope; for (auto _ : state) { - envelope.registerEvent(static_cast(state.range(0) - 1), dist(gen)); - envelope.getBlock(absl::MakeSpan(output)); + sfz::EventVector events { + { 0, 1.0f }, + { static_cast(state.range(0) - 1), dist(gen) } + }; + multiplicativeEnvelope(events, absl::MakeSpan(output), [](float x) { return x; }); } } BENCHMARK_DEFINE_F(EnvelopeFixture, MultiplicativeQuantized)(benchmark::State& state) { - sfz::MultiplicativeEnvelope envelope; for (auto _ : state) { - envelope.registerEvent(static_cast(state.range(0) - 1), dist(gen)); - envelope.getQuantizedBlock(absl::MakeSpan(output), 1.5); + sfz::EventVector events { + { 0, 1.0f }, + { static_cast(state.range(0) - 1), dist(gen) } + }; + multiplicativeEnvelope( + events, absl::MakeSpan(output), [](float x) { return x; }, 2.0f); } } diff --git a/benchmarks/CMakeLists.txt b/benchmarks/CMakeLists.txt index fb666f0c..2a217244 100644 --- a/benchmarks/CMakeLists.txt +++ b/benchmarks/CMakeLists.txt @@ -69,11 +69,11 @@ sfizz_add_benchmark(bm_logger BM_logger.cpp) target_link_libraries(bm_logger PRIVATE sfizz::sfizz) if (TARGET sfizz-samplerate) - sfizz_add_benchmark(bm_resample BM_resample.cpp ${BENCHMARK_SIMD_SOURCES}) - target_link_libraries(bm_resample PRIVATE sfizz-samplerate sfizz-sndfile) +sfizz_add_benchmark(bm_resample BM_resample.cpp ${BENCHMARK_SIMD_SOURCES}) +target_link_libraries(bm_resample PRIVATE sfizz-samplerate sfizz-sndfile) endif() -sfizz_add_benchmark(bm_envelopes BM_envelopes.cpp ../src/sfizz/MidiState.cpp ../src/sfizz/FloatEnvelopes.cpp) +sfizz_add_benchmark(bm_envelopes BM_envelopes.cpp) sfizz_add_benchmark(bm_wavfile BM_wavfile.cpp) target_link_libraries(bm_wavfile PRIVATE sfizz-sndfile) diff --git a/scripts/run_clang_tidy.sh b/scripts/run_clang_tidy.sh index bcb695f2..a58587b8 100755 --- a/scripts/run_clang_tidy.sh +++ b/scripts/run_clang_tidy.sh @@ -5,7 +5,6 @@ clang-tidy \ src/sfizz/Curve.cpp \ src/sfizz/Effects.cpp \ src/sfizz/EQPool.cpp \ - src/sfizz/EventEnvelopes.cpp \ src/sfizz/FilePool.cpp \ src/sfizz/FilterPool.cpp \ src/sfizz/FloatEnvelopes.cpp \ diff --git a/src/sfizz/ADSREnvelope.cpp b/src/sfizz/ADSREnvelope.cpp index 0acbdbe3..351b1267 100644 --- a/src/sfizz/ADSREnvelope.cpp +++ b/src/sfizz/ADSREnvelope.cpp @@ -24,7 +24,7 @@ void ADSREnvelope::reset(const Region& region, const MidiState& state, int this->release = secondsToSamples(region.amplitudeEG.getRelease(state, velocity)); this->hold = secondsToSamples(region.amplitudeEG.getHold(state, velocity)); this->peak = 1.0; - this->sustain = normalizePercents(region.amplitudeEG.getSustain(state, velocity)); + this->sustain = normalizePercents(region.amplitudeEG.getSustain(state, velocity)); this->start = this->peak * normalizePercents(region.amplitudeEG.getStart(state, velocity)); releaseDelay = 0; diff --git a/src/sfizz/BufferPool.h b/src/sfizz/BufferPool.h new file mode 100644 index 00000000..2d057edf --- /dev/null +++ b/src/sfizz/BufferPool.h @@ -0,0 +1,184 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#pragma once +#include "Config.h" +#include "Debug.h" +#include "Buffer.h" +#include "AudioBuffer.h" +#include +#include +#include +#include "absl/algorithm/container.h" +#ifndef NDEBUG +#include "MathHelpers.h" +#endif + +namespace sfz { + +template +class SpanHolder { +public: + SpanHolder() {} + SpanHolder(const SpanHolder&) = delete; + SpanHolder& operator=(const SpanHolder&) = delete; + SpanHolder(SpanHolder&& other) + { + this->value = other.value; + this->available = other.available; + other.available = nullptr; + } + SpanHolder& operator=(SpanHolder&& other) + { + this->value = other.value; + this->available = other.available; + other.available = nullptr; + } + SpanHolder(T&& value, int* available) + : value(std::forward(value)) + , available(available) + { + } + T& operator*() { return value; } + T* operator->() { return &value; } + explicit operator bool() const { return available != nullptr; } + ~SpanHolder() + { + if (available) + *available += 1; + } + +private: + T value {}; + int* available { nullptr }; +}; + +class BufferPool { +public: + BufferPool() + { + for (auto& buffer : stereoBuffers) { + buffer.addChannels(2); + } + monoAvailable.resize(config::bufferPoolSize); + stereoAvailable.resize(config::stereoBufferPoolSize); + indexAvailable.resize(config::indexBufferPoolSize); + _setBufferSize(config::defaultSamplesPerBlock); + } + + void setBufferSize(unsigned bufferSize) + { + ASSERT(absl::c_all_of(monoAvailable, [](int value) { return value == 1; })); + ASSERT(absl::c_all_of(indexAvailable, [](int value) { return value == 1; })); + ASSERT(absl::c_all_of(stereoAvailable, [](int value) { return value == 1; })); + _setBufferSize(bufferSize); + } + + SpanHolder> getBuffer(size_t numFrames) + { + const auto availableIt = absl::c_find(monoAvailable, 1); + if (availableIt == monoAvailable.end()) { + DBG("[sfizz] No free buffers available..."); + return {}; + } + const auto freeIndex = std::distance(monoAvailable.begin(), availableIt); + + if (monoBuffers[freeIndex].size() < numFrames) { + DBG("[sfizz] Someone asked for a buffer of size " << numFrames << "; only " << monoBuffers[freeIndex].size() << " available..."); + return {}; + } + +#ifndef NDEBUG + maxBuffersUsed = 1 + absl::c_count_if(monoAvailable, [](int value) { return value == 0; }); +#endif + *availableIt -= 1; + return { absl::MakeSpan(monoBuffers[freeIndex]).first(numFrames), &*availableIt }; + } + + SpanHolder> getIndexBuffer(size_t numFrames) + { + const auto availableIt = absl::c_find(indexAvailable, 1); + if (availableIt == indexAvailable.end()) { + DBG("[sfizz] No available index buffers in the pool"); + return {}; + } + const auto freeIndex = std::distance(indexAvailable.begin(), availableIt); + + if (indexBuffers[freeIndex].size() < numFrames) { + DBG("[sfizz] Someone asked for a index buffer of size " << numFrames << "; only " << indexBuffers[freeIndex].size() << " available..."); + return {}; + } + +#ifndef NDEBUG + maxIndexBuffersUsed = 1 + absl::c_count_if(indexAvailable, [](int value) { return value == 0; }); +#endif + *availableIt -= 1; + return { absl::MakeSpan(indexBuffers[freeIndex]).first(numFrames), &*availableIt }; + } + + SpanHolder> getStereoBuffer(size_t numFrames) + { + const auto availableIt = absl::c_find(stereoAvailable, 1); + if (availableIt == stereoAvailable.end()) { + DBG("[sfizz] No available stereo buffers in the pool"); + return {}; + } + const auto freeIndex = std::distance(stereoAvailable.begin(), availableIt); + + if (stereoBuffers[freeIndex].getNumFrames() < numFrames) { + DBG("[sfizz] Someone asked for a stereo buffer of size " << numFrames << "; only " << stereoBuffers[freeIndex].getNumFrames() << " available..."); + return {}; + } + +#ifndef NDEBUG + maxStereoBuffersUsed = 1 + absl::c_count_if(stereoAvailable, [](int value) { return value == 0; }); +#endif + *availableIt -= 1; + return { sfz::AudioSpan(stereoBuffers[freeIndex]).first(numFrames), &*availableIt }; + } + +#ifndef NDEBUG + ~BufferPool() + { + DBG("Max buffers used: " << maxBuffersUsed); + DBG("Max index buffers used: " << maxIndexBuffersUsed); + DBG("Max stereo buffers used: " << maxStereoBuffersUsed); + } +#endif + +private: + void _setBufferSize(unsigned bufferSize) + { + for (auto& buffer : monoBuffers) { + buffer.resize(bufferSize); + } + + for (auto& buffer : indexBuffers) { + buffer.resize(bufferSize); + } + + for (auto& buffer : stereoBuffers) { + buffer.resize(bufferSize); + } + + absl::c_fill(monoAvailable, 1); + absl::c_fill(stereoAvailable, 1); + absl::c_fill(indexAvailable, 1); + } + + std::array, config::bufferPoolSize> monoBuffers; + std::vector monoAvailable; + std::array, config::bufferPoolSize> indexBuffers; + std::vector indexAvailable; + std::array, config::stereoBufferPoolSize> stereoBuffers; + std::vector stereoAvailable; +#ifndef NDEBUG + mutable int maxBuffersUsed { 0 }; + mutable int maxIndexBuffersUsed { 0 }; + mutable int maxStereoBuffersUsed { 0 }; +#endif +}; +} diff --git a/src/sfizz/Config.h b/src/sfizz/Config.h index 0af2263f..a7c67223 100644 --- a/src/sfizz/Config.h +++ b/src/sfizz/Config.h @@ -28,6 +28,9 @@ namespace config { constexpr float defaultSampleRate { 48000 }; constexpr int defaultSamplesPerBlock { 1024 }; constexpr int maxBlockSize { 8192 }; + constexpr int bufferPoolSize { 4 }; + constexpr int stereoBufferPoolSize { 4 }; + constexpr int indexBufferPoolSize { 2 }; constexpr int preloadSize { 8192 }; constexpr int loggerQueueSize { 256 }; constexpr int voiceLoggerQueueSize { 256 }; @@ -35,7 +38,7 @@ namespace config { constexpr size_t numChannels { 2 }; constexpr int numBackgroundThreads { 4 }; constexpr int numVoices { 64 }; - constexpr int maxVoices { 256 }; + constexpr unsigned maxVoices { 256 }; constexpr int maxFilePromises { maxVoices * 2 }; constexpr int sustainCC { 64 }; constexpr int allSoundOffCC { 120 }; diff --git a/src/sfizz/Curve.cpp b/src/sfizz/Curve.cpp index ee9c5af2..b29fbf15 100644 --- a/src/sfizz/Curve.cpp +++ b/src/sfizz/Curve.cpp @@ -144,7 +144,7 @@ void Curve::lerpFill(const bool fillStatus[NumValues]) const auto length = right - left; if (length > 1) { const float mu = (_points[right] - _points[left]) / length; - linearRamp(pointSpan.subspan(left + 1, length - 1), _points[left], mu); + linearRamp(pointSpan.subspan(left, length), _points[left], mu); } left = right++; } diff --git a/src/sfizz/Defaults.h b/src/sfizz/Defaults.h index 9e6f0d84..ebef0041 100644 --- a/src/sfizz/Defaults.h +++ b/src/sfizz/Defaults.h @@ -34,6 +34,7 @@ enum class SfzLoopMode { no_loop, one_shot, loop_continuous, loop_sustain }; enum class SfzOffMode { fast, normal }; enum class SfzVelocityOverride { current, previous }; enum class SfzCrossfadeCurve { gain, power }; +enum class SfzSelfMask { mask, dontMask }; namespace sfz { @@ -53,9 +54,11 @@ namespace Default constexpr SfzLoopMode loopMode { SfzLoopMode::no_loop }; constexpr Range loopRange { 0, std::numeric_limits::max() }; - // Global ranges + // common defaults constexpr Range midi7Range { 0, 127 }; constexpr Range normalizedRange { 0.0f, 1.0f }; + constexpr Range symmetricNormalizedRange { -1.0, 1.0 }; + constexpr float zeroModifier { 0.0f }; // Wavetable oscillator constexpr float oscillatorPhase { 0.0 }; @@ -65,18 +68,21 @@ namespace Default constexpr uint32_t group { 0 }; constexpr Range groupRange { 0, std::numeric_limits::max() }; constexpr SfzOffMode offMode { SfzOffMode::fast }; + constexpr Range polyphonyRange { 0, config::maxVoices }; + constexpr SfzSelfMask selfMask { SfzSelfMask::mask }; - // Region logic: key mapping + // Region logic: key mapping constexpr Range keyRange { 0, 127 }; - constexpr auto velocityRange = normalizedRange; + constexpr auto velocityRange = normalizedRange; - // Region logic: MIDI conditions + // Region logic: MIDI conditions constexpr Range channelRange { 1, 16 }; constexpr Range midiChannelRange { 0, 15 }; - constexpr Range ccNumberRange { 0, config::numCCs }; - constexpr auto ccValueRange = normalizedRange; - constexpr Range bendRange { -8192, 8192 }; - constexpr int bend { 0 }; + constexpr Range ccNumberRange { 0, config::numCCs }; + constexpr auto ccValueRange = normalizedRange; + constexpr Range bendRange = { -8192, 8192 }; + constexpr Range bendValueRange = symmetricNormalizedRange; + constexpr int bend { 0 }; constexpr SfzVelocityOverride velocityOverride { SfzVelocityOverride::current }; // Region logic: internal conditions @@ -91,9 +97,9 @@ namespace Default // Region logic: Triggers constexpr SfzTrigger trigger { SfzTrigger::attack }; - constexpr Range ccTriggerValueRange = normalizedRange; + constexpr Range ccTriggerValueRange = normalizedRange; - // Performance parameters: amplifier + // Performance parameters: amplifier constexpr float globalVolume { -7.35f }; constexpr float volume { 0.0f }; constexpr Range volumeRange { -144.0, 6.0 }; @@ -103,7 +109,6 @@ namespace Default constexpr float pan { 0.0 }; constexpr Range panRange { -100.0, 100.0 }; constexpr Range panCCRange { -200.0, 200.0 }; - constexpr Range symmetricNormalizedRange { -1.0, 1.0 }; constexpr float position { 0.0 }; constexpr Range positionRange { -100.0, 100.0 }; constexpr Range positionCCRange { -200.0, 200.0 }; @@ -120,11 +125,11 @@ namespace Default constexpr Range ampRandomRange { 0.0, 24.0 }; constexpr Range crossfadeKeyInRange { 0, 0 }; constexpr Range crossfadeKeyOutRange { 127, 127 }; - constexpr Range crossfadeVelInRange { 0.0f, 0.0f }; - constexpr Range crossfadeVelOutRange { 1.0f, 1.0f }; - constexpr Range crossfadeCCInRange { 0.0f, 0.0f }; - constexpr Range crossfadeCCOutRange { 1.0f, 1.0f }; - constexpr SfzCrossfadeCurve crossfadeKeyCurve { SfzCrossfadeCurve::power }; + constexpr Range crossfadeVelInRange { 0.0f, 0.0f }; + constexpr Range crossfadeVelOutRange { 1.0f, 1.0f }; + constexpr Range crossfadeCCInRange { 0.0f, 0.0f }; + constexpr Range crossfadeCCOutRange { 1.0f, 1.0f }; + constexpr SfzCrossfadeCurve crossfadeKeyCurve { SfzCrossfadeCurve::power }; constexpr SfzCrossfadeCurve crossfadeVelCurve { SfzCrossfadeCurve::power }; constexpr SfzCrossfadeCurve crossfadeCCCurve { SfzCrossfadeCurve::power }; constexpr float rtDecay { 0.0f }; @@ -184,6 +189,7 @@ namespace Default constexpr Range transposeRange { -127, 127 }; constexpr int tune { 0 }; constexpr Range tuneRange { -9600, 9600 }; // ±100 in SFZv1, more in ARIA + constexpr Range tuneCCRange { -9600, 9600 }; constexpr Range bendBoundRange { -9600, 9600 }; constexpr Range bendStepRange { 1, 1200 }; constexpr int bendUp { 200 }; // No range here because the bounds can be inverted diff --git a/src/sfizz/EQPool.cpp b/src/sfizz/EQPool.cpp index c3883124..515f26ef 100644 --- a/src/sfizz/EQPool.cpp +++ b/src/sfizz/EQPool.cpp @@ -28,9 +28,20 @@ void sfz::EQHolder::setup(const EQDescription& description, unsigned numChannels baseGain = description.gain + velocity * description.vel2gain; // Setup the modulated values - lastFrequency = midiState.modulate(baseFrequency, description.frequencyCC, Default::eqFrequencyRange); - lastBandwidth = midiState.modulate(baseBandwidth, description.bandwidthCC, Default::eqBandwidthRange); - lastGain = midiState.modulate(baseGain, description.gainCC, Default::eqGainRange); + lastFrequency = baseFrequency; + for (const auto& mod : description.frequencyCC) + lastFrequency += midiState.getCCValue(mod.cc) * mod.value; + lastFrequency = Default::eqFrequencyRange.clamp(lastFrequency); + + lastBandwidth = baseBandwidth; + for (const auto& mod : description.bandwidthCC) + lastBandwidth += midiState.getCCValue(mod.cc) * mod.value; + lastBandwidth = Default::eqBandwidthRange.clamp(lastBandwidth); + + lastGain = baseGain; + for (const auto& mod : description.gainCC) + lastGain += midiState.getCCValue(mod.cc) * mod.value; + lastGain = Default::filterGainRange.clamp(lastGain); // Initialize the EQ eq.prepare(lastFrequency, lastBandwidth, lastGain); @@ -50,9 +61,20 @@ void sfz::EQHolder::process(const float** inputs, float** outputs, unsigned numF // TODO: Once the midistate envelopes are done, add modulation in there! // For now we take the last value - lastFrequency = midiState.modulate(baseFrequency, description->frequencyCC, Default::eqFrequencyRange); - lastBandwidth = midiState.modulate(baseBandwidth, description->bandwidthCC, Default::eqBandwidthRange); - lastGain = midiState.modulate(baseGain, description->gainCC, Default::eqGainRange); + lastFrequency = baseFrequency; + for (const auto& mod : description->frequencyCC) + lastFrequency += midiState.getCCValue(mod.cc) * mod.value; + lastFrequency = Default::eqFrequencyRange.clamp(lastFrequency); + + lastBandwidth = baseBandwidth; + for (const auto& mod : description->bandwidthCC) + lastBandwidth += midiState.getCCValue(mod.cc) * mod.value; + lastBandwidth = Default::eqBandwidthRange.clamp(lastBandwidth); + + lastGain = baseGain; + for (const auto& mod : description->gainCC) + lastGain += midiState.getCCValue(mod.cc) * mod.value; + lastGain = Default::filterGainRange.clamp(lastGain); if (lastGain == 0.0f) { justCopy(); diff --git a/src/sfizz/EventEnvelopes.cpp b/src/sfizz/EventEnvelopes.cpp deleted file mode 100644 index c64a5415..00000000 --- a/src/sfizz/EventEnvelopes.cpp +++ /dev/null @@ -1,277 +0,0 @@ -// SPDX-License-Identifier: BSD-2-Clause - -// This code is part of the sfizz library and is licensed under a BSD 2-clause -// license. You should have receive a LICENSE.md file along with the code. -// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz - -#include "EventEnvelopes.h" -#include "SIMDHelpers.h" -#include "MathHelpers.h" -#include - -namespace sfz { - -template -EventEnvelope::EventEnvelope() -{ - setMaxCapacity(maxCapacity); -} - -template -EventEnvelope::EventEnvelope(int maxCapacity, std::function function) -{ - setMaxCapacity(maxCapacity); - setFunction(function); -} - -template -void EventEnvelope::setMaxCapacity(int maxCapacity) -{ - events.reserve(maxCapacity); - this->maxCapacity = maxCapacity; -} - -template -void EventEnvelope::setFunction(std::function function) -{ - this->function = function; -} - -template -void EventEnvelope::registerEvent(int timestamp, Type inputValue) -{ - if (resetEvents) - clear(); - - if (static_cast(events.size()) < maxCapacity) - events.emplace_back(timestamp, function(inputValue)); -} - -template -void EventEnvelope::prepareEvents(int blockLength) -{ - if (resetEvents) - clear(); - - absl::c_stable_sort(events, [](const std::pair& lhs, const std::pair& rhs) { - return lhs.first < rhs.first; - }); - - auto eventIt = events.begin(); - while (eventIt < events.end()) { - if (eventIt->first >= blockLength) { - eventIt->first = blockLength - 1; - eventIt->second = events.back().second; - ++eventIt; - break; - } - - auto nextEventIt = std::next(eventIt); - while (nextEventIt < events.end() && eventIt->first == nextEventIt->first ) { - eventIt->second = nextEventIt->second; - ++nextEventIt; - } - ++eventIt; - } - events.resize(std::distance(events.begin(), eventIt)); - - resetEvents = true; -} - -template -void EventEnvelope::clear() -{ - events.clear(); - resetEvents = false; -} - -template -void EventEnvelope::reset(Type value) -{ - clear(); - currentValue = function(value); - resetEvents = false; -} - -template -void EventEnvelope::getBlock(absl::Span output) -{ - prepareEvents(output.size()); -} - -template -void EventEnvelope::getQuantizedBlock(absl::Span output, Type) -{ - prepareEvents(output.size()); -} - -template -void LinearEnvelope::getBlock(absl::Span output) -{ - EventEnvelope::getBlock(output); - auto& events = EventEnvelope::events; - auto& currentValue = EventEnvelope::currentValue; - - int index { 0 }; - for (auto& event : events) { - const auto length = min(event.first, static_cast(output.size())) - index; - if (length == 0) { - currentValue = event.second; - continue; - } - - const auto step = (event.second - currentValue) / length; - currentValue = linearRamp(output.subspan(index, length), currentValue, step); - index += length; - } - - if (index < static_cast(output.size())) - fill(output.subspan(index), currentValue); -} - -template -void LinearEnvelope::getQuantizedBlock(absl::Span output, Type quantizationStep) -{ - EventEnvelope::getQuantizedBlock(output, quantizationStep); - auto& events = EventEnvelope::events; - auto& currentValue = EventEnvelope::currentValue; - - ASSERT(quantizationStep != 0.0); - int index { 0 }; - - auto quantize = [quantizationStep](Type value) -> Type { - return std::round(value / quantizationStep) * quantizationStep; - }; - - const auto outputSize = static_cast(output.size()); - for (auto& event : events) { - const auto newValue = quantize(event.second); - - if (event.first > outputSize) { - fill(output.subspan(index), currentValue); - currentValue = newValue; - index = outputSize; - continue; - } - - const auto length = event.first - index - 1; - if (length <= 0) { - currentValue = newValue; - continue; - } - - const auto difference = std::abs(newValue - currentValue); - if (difference < quantizationStep) { - fill(output.subspan(index, length), currentValue); - currentValue = newValue; - index += length; - continue; - } - - const auto numSteps = static_cast(difference / quantizationStep); - const auto stepLength = static_cast(length / numSteps); - for (int i = 0; i < numSteps; ++i) { - fill(output.subspan(index, stepLength), currentValue); - const auto delta = quantizationStep + currentValue - quantize(currentValue); - currentValue += currentValue <= newValue ? delta : -delta; - index += stepLength; - } - } - - if (index < outputSize) - fill(output.subspan(index), currentValue); -} - -template -MultiplicativeEnvelope::MultiplicativeEnvelope() -{ - EventEnvelope::reset(1.0); -} - -template -void MultiplicativeEnvelope::getBlock(absl::Span output) -{ - EventEnvelope::getBlock(output); - auto& events = EventEnvelope::events; - auto& currentValue = EventEnvelope::currentValue; - - int index { 0 }; - for (auto& event : events) { - const auto length = min(event.first, static_cast(output.size())) - index; - if (length == 0) { - currentValue = event.second; - continue; - } - - const auto step = std::exp((std::log(event.second) - std::log(currentValue)) / length); - multiplicativeRamp(output.subspan(index, length), currentValue, step); - currentValue = event.second; - index += length; - } - - if (index < static_cast(output.size())) - fill(output.subspan(index), currentValue); -} - -template -void MultiplicativeEnvelope::getQuantizedBlock(absl::Span output, Type quantizationStep) -{ - EventEnvelope::getQuantizedBlock(output, quantizationStep); - auto& events = EventEnvelope::events; - auto& currentValue = EventEnvelope::currentValue; - - ASSERT(quantizationStep != 0.0); - int index { 0 }; - - const auto logStep = std::log(quantizationStep); - // If we assume that a = b.q^r for b in (1, q) then - // log a log b - // ----- = ----- + r - // log q log q - // and log(b)\log(q) is between 0 and 1. - auto quantize = [logStep](Type value) -> Type { - return std::exp(logStep * std::round(std::log(value)/logStep)); - }; - - const auto outputSize = static_cast(output.size()); - for (auto& event : events) { - const auto newValue = quantize(event.second); - - if (event.first > outputSize) { - fill(output.subspan(index), currentValue); - currentValue = newValue; - index = outputSize; - continue; - } - - const auto length = event.first - index - 1; - if (length <= 0) { - currentValue = newValue; - continue; - } - - const auto difference = newValue > currentValue ? newValue / currentValue : currentValue / newValue; - if (difference < quantizationStep) { - fill(output.subspan(index, length), currentValue); - currentValue = newValue; - index += length; - continue; - } - - const auto numSteps = static_cast(std::log(difference) / logStep); - const auto stepLength = static_cast(length / numSteps); - for (int i = 0; i < numSteps; ++i) { - fill(output.subspan(index, stepLength), currentValue); - const auto delta = newValue > currentValue ? - quantize(currentValue) / currentValue * quantizationStep : - quantize(currentValue) / currentValue / quantizationStep ; - currentValue *= delta; - index += stepLength; - } - } - - if (index < outputSize) - fill(output.subspan(index), currentValue); -} - -} diff --git a/src/sfizz/EventEnvelopes.h b/src/sfizz/EventEnvelopes.h deleted file mode 100644 index c56d1dd6..00000000 --- a/src/sfizz/EventEnvelopes.h +++ /dev/null @@ -1,129 +0,0 @@ -// SPDX-License-Identifier: BSD-2-Clause - -// This code is part of the sfizz library and is licensed under a BSD 2-clause -// license. You should have receive a LICENSE.md file along with the code. -// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz - -#pragma once -#include "Config.h" -#include "LeakDetector.h" -#include -#include -#include -#include - -namespace sfz { -/** - * @brief Describes a simple envelope that can be polled in a blockwise - * manner. It works by storing "events" in the immediate future and linearly - * interpolating between these events. This envelope can also transform its - * incoming target points through a lambda, although the lambda function is applied before the interpolation. - * - * The way to use this class is by repeatedly calling `registerEvent` and then - * `getBlock` to get a block of interpolated values in between the specified events. - * You should only register events whose timestamps are below the size of the block - * you will require when calling `getBlock`. - * - * @tparam Type - */ -template -class EventEnvelope { -public: - /** - * @brief Construct a new linear envelope with a default memory size for - * incoming events. - * - */ - EventEnvelope(); - /** - * @brief Construct a new linear envelope with a specific memory size for - * incoming events as well as a transformation function for incoming events. - * - * @param maxCapacity - * @param function - */ - EventEnvelope(int maxCapacity, std::function function); - /** - * @brief Set the maximum memory size for incoming events - * - * @param maxCapacity - */ - void setMaxCapacity(int maxCapacity); - /** - * @brief Set the transformation function for the value of incoming events. - * - * @param function - */ - void setFunction(std::function function); - /** - * @brief Register a new event. Note that the timestamp of the new value should - * be less than the future call to `getBlock` otherwise the event will be ignored. - * - * @param timestamp - * @param inputValue - */ - void registerEvent(int timestamp, Type inputValue); - /** - * @brief Clear all events in memory - * - */ - void clear(); - /** - * @brief Reset the envelope and clears the memory. - * - * @param value - */ - void reset(Type value = 0.0); - /** - * @brief Get a block of interpolated values between events previously registered - * using `registerEvent`. - * - * @param output - */ - virtual void getBlock(absl::Span output); - /** - * @brief Get a block of interpolated values with a forced quantization. The - * values within the block will vary in quantization steps. - * - * @param output - * @param quantizationStep - */ - virtual void getQuantizedBlock(absl::Span output, Type quantizationStep); -protected: - std::vector> events; - Type currentValue { 0.0 }; -private: - static_assert(std::is_arithmetic::value, "Type should be arithmetic"); - std::function function { [](Type input) -> Type { return input; } }; - int maxCapacity { config::defaultSamplesPerBlock }; - void prepareEvents(int blockLength); - bool resetEvents { false }; - LEAK_DETECTOR(EventEnvelope); -}; - - -/** - * @brief Describes a simple linear envelope. - * - * @tparam Type - */ -template -class LinearEnvelope: public EventEnvelope { -public: - void getBlock(absl::Span output) final; - void getQuantizedBlock(absl::Span output, Type quantizationStep) final; -}; - -/** - * @brief Describes a simple multiplicative envelope. - * - * @tparam Type - */ -template -class MultiplicativeEnvelope: public EventEnvelope { -public: - MultiplicativeEnvelope(); - void getBlock(absl::Span output) final; - void getQuantizedBlock(absl::Span output, Type quantizationStep) final; -}; -} diff --git a/src/sfizz/FilePool.cpp b/src/sfizz/FilePool.cpp index 3cd57684..9b43a2be 100644 --- a/src/sfizz/FilePool.cpp +++ b/src/sfizz/FilePool.cpp @@ -273,7 +273,7 @@ sfz::FilePromisePtr sfz::FilePool::getFilePromise(const std::string& filename) n auto promise = emptyPromises.back(); promise->filename = preloaded->first; promise->preloadedData = preloaded->second.preloadedData; - promise->sampleRate = preloaded->second.information.sampleRate; + promise->sampleRate = static_cast(preloaded->second.information.sampleRate); promise->oversamplingFactor = oversamplingFactor; promise->creationTime = std::chrono::high_resolution_clock::now(); diff --git a/src/sfizz/FilterPool.cpp b/src/sfizz/FilterPool.cpp index 96f97308..062e4e95 100644 --- a/src/sfizz/FilterPool.cpp +++ b/src/sfizz/FilterPool.cpp @@ -40,9 +40,20 @@ void sfz::FilterHolder::setup(const FilterDescription& description, unsigned num baseResonance = description.resonance; // Setup the modulated values - lastCutoff = midiState.modulate(baseCutoff, description.cutoffCC, Default::filterCutoffRange, multiplyByCents); - lastResonance = midiState.modulate(baseResonance, description.resonanceCC, Default::filterResonanceRange); - lastGain = midiState.modulate(baseGain, description.gainCC, Default::filterGainRange); + lastCutoff = baseCutoff; + for (const auto& mod : description.cutoffCC) + lastCutoff *= centsFactor(midiState.getCCValue(mod.cc) * mod.value); + lastCutoff = Default::filterCutoffRange.clamp(lastCutoff); + + lastResonance = baseResonance; + for (const auto& mod : description.resonanceCC) + lastResonance += midiState.getCCValue(mod.cc) * mod.value; + lastResonance = Default::filterResonanceRange.clamp(lastResonance); + + lastGain = baseGain; + for (const auto& mod : description.gainCC) + lastGain += midiState.getCCValue(mod.cc) * mod.value; + lastGain = Default::filterGainRange.clamp(lastGain); // Initialize the filter filter.prepare(lastCutoff, lastResonance, lastGain); @@ -59,9 +70,20 @@ void sfz::FilterHolder::process(const float** inputs, float** outputs, unsigned // TODO: Once the midistate envelopes are done, add modulation in there! // For now we take the last value // TODO: the template deduction could be automatic here? - lastCutoff = midiState.modulate(baseCutoff, description->cutoffCC, Default::filterCutoffRange, multiplyByCents); - lastResonance = midiState.modulate(baseResonance, description->resonanceCC, Default::filterResonanceRange); - baseGain = midiState.modulate(baseGain, description->gainCC, Default::filterGainRange); + lastCutoff = baseCutoff; + for (const auto& mod : description->cutoffCC) + lastCutoff *= centsFactor(midiState.getCCValue(mod.cc) * mod.value); + lastCutoff = Default::filterCutoffRange.clamp(lastCutoff); + + lastResonance = baseResonance; + for (const auto& mod : description->resonanceCC) + lastResonance += midiState.getCCValue(mod.cc) * mod.value; + lastResonance = Default::filterResonanceRange.clamp(lastResonance); + + lastGain = baseGain; + for (const auto& mod : description->gainCC) + lastGain += midiState.getCCValue(mod.cc) * mod.value; + lastGain = Default::filterGainRange.clamp(lastGain); filter.process(inputs, outputs, lastCutoff, lastResonance, lastGain, numFrames); } diff --git a/src/sfizz/FloatEnvelopes.cpp b/src/sfizz/FloatEnvelopes.cpp index 54e98fd9..e4c5289c 100644 --- a/src/sfizz/FloatEnvelopes.cpp +++ b/src/sfizz/FloatEnvelopes.cpp @@ -4,29 +4,13 @@ // license. You should have receive a LICENSE.md file along with the code. // If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz -/** - * @file FloatEnvelopes.cpp - * @author Paul Ferrand (paul@ferrand.cc) - * @brief Force the instantiations of the ADSR and linear envelopes for floats - * @version 0.1 - * @date 2019-11-30 - * - * @copyright Copyright (c) 2019 Paul Ferrand - * - */ - -#include "EventEnvelopes.h" #include "ADSREnvelope.h" // Include the generic implementations -#include "EventEnvelopes.cpp" #include "ADSREnvelope.cpp" // And explicitely instantiate the float version namespace sfz { - template class EventEnvelope; - template class MultiplicativeEnvelope; - template class LinearEnvelope; template class ADSREnvelope; } diff --git a/src/sfizz/MidiState.cpp b/src/sfizz/MidiState.cpp index 6e8ccac3..0b932010 100644 --- a/src/sfizz/MidiState.cpp +++ b/src/sfizz/MidiState.cpp @@ -10,7 +10,7 @@ sfz::MidiState::MidiState() { - reset(0); + reset(); } void sfz::MidiState::noteOnEvent(int delay, int noteNumber, float velocity) noexcept @@ -20,7 +20,7 @@ void sfz::MidiState::noteOnEvent(int delay, int noteNumber, float velocity) noex if (noteNumber >= 0 && noteNumber < 128) { lastNoteVelocities[noteNumber] = velocity; - noteOnTimes[noteNumber] = std::chrono::steady_clock::now(); + noteOnTimes[noteNumber] = internalClock + static_cast(delay); activeNotes++; } @@ -28,27 +28,61 @@ void sfz::MidiState::noteOnEvent(int delay, int noteNumber, float velocity) noex void sfz::MidiState::noteOffEvent(int delay, int noteNumber, float velocity) noexcept { + ASSERT(delay >= 0); ASSERT(noteNumber >= 0 && noteNumber <= 127); ASSERT(velocity >= 0.0 && velocity <= 1.0); UNUSED(velocity); if (noteNumber >= 0 && noteNumber < 128) { + noteOffTimes[noteNumber] = internalClock + static_cast(delay); if (activeNotes > 0) activeNotes--; } } -float sfz::MidiState::getNoteDuration(int noteNumber) const +void sfz::MidiState::setSampleRate(float sampleRate) noexcept { - ASSERT(noteNumber >= 0 && noteNumber <= 127); + this->sampleRate = sampleRate; + internalClock = 0; + absl::c_fill(noteOnTimes, 0); + absl::c_fill(noteOffTimes, 0); +} - if (noteNumber >= 0 && noteNumber < 128) { - const auto noteOffTime = std::chrono::steady_clock::now(); - const auto duration = std::chrono::duration_cast>(noteOffTime - noteOnTimes[noteNumber]); - return duration.count(); +void sfz::MidiState::advanceTime(int numSamples) noexcept +{ + internalClock += numSamples; + for (auto& ccEvents : cc) { + ASSERT(!ccEvents.empty()); // CC event vectors should never be empty + ccEvents.front().value = ccEvents.back().value; + ccEvents.front().delay = 0; + ccEvents.resize(1); } + ASSERT(!pitchEvents.empty()); + pitchEvents.front().value = pitchEvents.back().value; + pitchEvents.front().delay = 0; + pitchEvents.resize(1); +} - return 0.0f; +void sfz::MidiState::setSamplesPerBlock(int samplesPerBlock) noexcept +{ + this->samplesPerBlock = samplesPerBlock; + for (auto& ccEvents : cc) { + ccEvents.shrink_to_fit(); + ccEvents.reserve(samplesPerBlock); + } +} + +float sfz::MidiState::getNoteDuration(int noteNumber, int delay) const +{ + ASSERT(noteNumber >= 0 && noteNumber < 128); + if (noteNumber < 0 || noteNumber >= 128) + return 0.0f; + + if (noteOnTimes[noteNumber] != 0 && noteOffTimes[noteNumber] != 0 && noteOnTimes[noteNumber] > noteOffTimes[noteNumber]) + return 0.0f; + + const unsigned timeInSamples = internalClock + static_cast(delay) - noteOnTimes[noteNumber]; + return static_cast(timeInSamples) / sampleRate; } float sfz::MidiState::getNoteVelocity(int noteNumber) const noexcept @@ -58,49 +92,76 @@ float sfz::MidiState::getNoteVelocity(int noteNumber) const noexcept return lastNoteVelocities[noteNumber]; } - -void sfz::MidiState::pitchBendEvent(int delay, int pitchBendValue) noexcept +void sfz::MidiState::pitchBendEvent(int delay, float pitchBendValue) noexcept { - ASSERT(pitchBendValue >= -8192 && pitchBendValue <= 8192); + ASSERT(pitchBendValue >= -1.0f && pitchBendValue <= 1.0f); - pitchBend = pitchBendValue; + const auto insertionPoint = absl::c_upper_bound(pitchEvents, delay, MidiEventDelayComparator {}); + if (insertionPoint == pitchEvents.end() || insertionPoint->delay != delay) + pitchEvents.insert(insertionPoint, { delay, pitchBendValue }); + else + insertionPoint->value = pitchBendValue; } -int sfz::MidiState::getPitchBend() const noexcept +float sfz::MidiState::getPitchBend() const noexcept { - return pitchBend; + ASSERT(pitchEvents.size() > 0); + return pitchEvents.back().value; } void sfz::MidiState::ccEvent(int delay, int ccNumber, float ccValue) noexcept { ASSERT(ccValue >= 0.0 && ccValue <= 1.0); - - cc[ccNumber] = ccValue; + const auto insertionPoint = absl::c_upper_bound(cc[ccNumber], delay, MidiEventDelayComparator {}); + if (insertionPoint == cc[ccNumber].end() || insertionPoint->delay != delay) + cc[ccNumber].insert(insertionPoint, { delay, ccValue }); + else + insertionPoint->value = ccValue; } float sfz::MidiState::getCCValue(int ccNumber) const noexcept { ASSERT(ccNumber >= 0 && ccNumber < config::numCCs); - return cc[ccNumber]; + return cc[ccNumber].back().value; } -void sfz::MidiState::reset(int delay) noexcept +void sfz::MidiState::reset() noexcept { for (auto& velocity: lastNoteVelocities) velocity = 0; - for (auto& ccValue: cc) - ccValue = 0; + for (auto& ccEvents : cc) { + ccEvents.clear(); + ccEvents.push_back({ 0, 0.0f }); + } + + pitchEvents.clear(); + pitchEvents.push_back({ 0, 0.0f }); - pitchBend = 0; activeNotes = 0; + internalClock = 0; + absl::c_fill(noteOnTimes, 0); + absl::c_fill(noteOffTimes, 0); } void sfz::MidiState::resetAllControllers(int delay) noexcept { - for (unsigned idx = 0; idx < config::numCCs; idx++) - cc[idx] = 0; + for (int ccIdx = 0; ccIdx < config::numCCs; ++ccIdx) + ccEvent(delay, ccIdx, 0.0f); - pitchBend = 0; + pitchBendEvent(delay, 0.0f); +} + +const sfz::EventVector& sfz::MidiState::getCCEvents(int ccIdx) const noexcept +{ + if (ccIdx < 0 || ccIdx > config::numCCs) + return nullEvent; + + return cc[ccIdx]; +} + +const sfz::EventVector& sfz::MidiState::getPitchEvents() const noexcept +{ + return pitchEvents; } diff --git a/src/sfizz/MidiState.h b/src/sfizz/MidiState.h index 09c4ce78..021ff415 100644 --- a/src/sfizz/MidiState.h +++ b/src/sfizz/MidiState.h @@ -5,7 +5,6 @@ // If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz #pragma once -#include #include #include "CCMap.h" #include "Range.h" @@ -29,7 +28,7 @@ public: * @param noteNumber * @param velocity */ - void noteOnEvent(int delay, int noteNumber, float velocity) noexcept; + void noteOnEvent(int delay, int noteNumber, float velocity) noexcept; /** * @brief Update the state after a note off event @@ -37,39 +36,55 @@ public: * @param noteNumber * @param velocity */ - void noteOffEvent(int delay, int noteNumber, float velocity) noexcept; + void noteOffEvent(int delay, int noteNumber, float velocity) noexcept; int getActiveNotes() const noexcept { return activeNotes; } /** - * @brief Register a note off and get the note duration + * @brief Get the note duration since note on * * @param noteNumber + * @param delay * @return float */ - float getNoteDuration(int noteNumber) const; + float getNoteDuration(int noteNumber, int delay = 0) const; + /** + * @brief Set the maximum size of the blocks for the callback. The actual + * size can be lower in each callback but should not be larger + * than this value. + * + * @param samplesPerBlock + */ + void setSamplesPerBlock(int samplesPerBlock) noexcept; + /** + * @brief Set the sample rate. If you do not call it it is initialized + * to sfz::config::defaultSampleRate. + * + * @param sampleRate + */ + void setSampleRate(float sampleRate) noexcept; /** * @brief Get the note on velocity for a given note * * @param noteNumber * @return float */ - float getNoteVelocity(int noteNumber) const noexcept; + float getNoteVelocity(int noteNumber) const noexcept; /** * @brief Register a pitch bend event * * @param pitchBendValue */ - void pitchBendEvent(int delay, int pitchBendValue) noexcept; + void pitchBendEvent(int delay, float pitchBendValue) noexcept; /** * @brief Get the pitch bend status * @return int */ - int getPitchBend() const noexcept; + float getPitchBend() const noexcept; /** * @brief Register a CC event @@ -79,6 +94,14 @@ public: */ void ccEvent(int delay, int ccNumber, float ccValue) noexcept; + /** + * @brief Advances the internal clock of a given amount of samples. + * You should call this at each callback. This will flush the events + * in the midistate memory. + * + * @param numSamples the number of samples of clock advance + */ + void advanceTime(int numSamples) noexcept; /** * @brief Get the CC value for CC number * @@ -91,57 +114,57 @@ public: * @brief Reset the midi state (does not impact the last note on time) * */ - void reset(int delay) noexcept; + void reset() noexcept; /** * @brief Reset all the controllers */ void resetAllControllers(int delay) noexcept; - /** - * @brief Modulate a value using the last entered CCs in the midiState - * - * @tparam T - * @tparam U - * @param value the base value - * @param modifiers the list of CC modifiers - * @param validRange a range to clamp the output - * @param lambda the function to apply for each modifier - * @return T - */ - template - T modulate(T value, const CCMap& modifiers, const Range& validRange, const modFunction& lambda = addToBase) const noexcept - { - for (auto& mod: modifiers) { - lambda(value, getCCValue(mod.cc) * mod.value); - } - return validRange.clamp(value); - } + const EventVector& getCCEvents(int ccIdx) const noexcept; + const EventVector& getPitchEvents() const noexcept; private: - template - using MidiNoteArray = std::array; - using NoteOnTime = std::chrono::steady_clock::time_point; - int activeNotes { 0 }; + int activeNotes { 0 }; + /** * @brief Stores the note on times. * */ - MidiNoteArray noteOnTimes; + MidiNoteArray noteOnTimes { {} }; + + /** + * @brief Stores the note off times. + * + */ + + MidiNoteArray noteOffTimes { {} }; + /** * @brief Stores the velocity of the note ons for currently * depressed notes. * */ - MidiNoteArray lastNoteVelocities; + MidiNoteArray lastNoteVelocities; + /** * @brief Current known values for the CCs. * */ - std::array cc; + std::array cc; + /** - * Pitch bend status + * @brief Null event + * */ - int pitchBend { 0 }; + const EventVector nullEvent { { 0, 0.0f } }; + + /** + * @brief Pitch bend status + */ + EventVector pitchEvents; + float sampleRate { config::defaultSampleRate }; + int samplesPerBlock { config::defaultSamplesPerBlock }; + unsigned internalClock { 0 }; }; } diff --git a/src/sfizz/Region.cpp b/src/sfizz/Region.cpp index 7ab2dbfb..cde42750 100644 --- a/src/sfizz/Region.cpp +++ b/src/sfizz/Region.cpp @@ -124,6 +124,22 @@ bool sfz::Region::parseOpcode(const Opcode& opcode) DBG("Unkown off mode:" << std::string(opcode.value)); } break; + case hash("note_polyphony"): + if (auto value = readOpcode(opcode.value, Default::polyphonyRange)) + notePolyphony = *value; + break; + case hash("note_selfmask"): + switch (hash(opcode.value)) { + case hash("on"): + selfMask = SfzSelfMask::mask; + break; + case hash("off"): + selfMask = SfzSelfMask::dontMask; + break; + default: + DBG("Unkown self mask value:" << std::string(opcode.value)); + } + break; // Region logic: key mapping case hash("lokey"): setRangeStartFromOpcode(opcode, keyRange, Default::keyRange); @@ -149,10 +165,12 @@ bool sfz::Region::parseOpcode(const Opcode& opcode) // Region logic: MIDI conditions case hash("lobend"): - setRangeStartFromOpcode(opcode, bendRange, Default::bendRange); + if (auto value = readOpcode(opcode.value, Default::bendRange)) + bendRange.setStart(normalizeBend(*value)); break; case hash("hibend"): - setRangeEndFromOpcode(opcode, bendRange, Default::bendRange); + if (auto value = readOpcode(opcode.value, Default::bendRange)) + bendRange.setEnd(normalizeBend(*value)); break; case hash("locc&"): if (opcode.parameters.back() > config::numCCs) @@ -276,32 +294,51 @@ bool sfz::Region::parseOpcode(const Opcode& opcode) case hash("gain_cc&"): case hash("gain_oncc&"): // fallthrough case hash("volume_oncc&"): - setCCPairFromOpcode(opcode, volumeCC, Default::volumeCCRange); + if (opcode.parameters.back() > config::numCCs) + return false; + if (auto value = readOpcode(opcode.value, Default::volumeCCRange)) + volumeCC[opcode.parameters.back()] = *value; break; case hash("amplitude"): - setValueFromOpcode(opcode, amplitude, Default::amplitudeRange); + if (auto value = readOpcode(opcode.value, Default::amplitudeRange)) + amplitude = normalizePercents(*value); break; case hash("amplitude_cc&"): // fallthrough case hash("amplitude_oncc&"): - setCCPairFromOpcode(opcode, amplitudeCC, Default::amplitudeRange); + if (opcode.parameters.back() > config::numCCs) + return false; + if (auto value = readOpcode(opcode.value, Default::amplitudeRange)) + amplitudeCC[opcode.parameters.back()] = normalizePercents(*value); break; case hash("pan"): - setValueFromOpcode(opcode, pan, Default::panRange); + if (auto value = readOpcode(opcode.value, Default::panRange)) + pan = normalizePercents(*value); break; case hash("pan_oncc&"): - setCCPairFromOpcode(opcode, panCC, Default::panCCRange); + if (opcode.parameters.back() > config::numCCs) + return false; + if (auto value = readOpcode(opcode.value, Default::panCCRange)) + panCC[opcode.parameters.back()] = normalizePercents(*value); break; case hash("position"): - setValueFromOpcode(opcode, position, Default::positionRange); + if (auto value = readOpcode(opcode.value, Default::positionRange)) + position = normalizePercents(*value); break; case hash("position_oncc&"): - setCCPairFromOpcode(opcode, positionCC, Default::positionCCRange); + if (opcode.parameters.back() > config::numCCs) + return false; + if (auto value = readOpcode(opcode.value, Default::positionCCRange)) + positionCC[opcode.parameters.back()] = normalizePercents(*value); break; case hash("width"): - setValueFromOpcode(opcode, width, Default::widthRange); + if (auto value = readOpcode(opcode.value, Default::widthRange)) + width = normalizePercents(*value); break; case hash("width_oncc&"): - setCCPairFromOpcode(opcode, widthCC, Default::widthCCRange); + if (opcode.parameters.back() > config::numCCs) + return false; + if (auto value = readOpcode(opcode.value, Default::widthCCRange)) + widthCC[opcode.parameters.back()] = normalizePercents(*value); break; case hash("amp_keycenter"): setValueFromOpcode(opcode, ampKeycenter, Default::keyRange); @@ -682,6 +719,15 @@ bool sfz::Region::parseOpcode(const Opcode& opcode) case hash("pitch"): setValueFromOpcode(opcode, tune, Default::tuneRange); break; + case hash("tune_cc&"): + case hash("tune_oncc&"): + case hash("pitch_cc&"): + case hash("pitch_oncc&"): + if (opcode.parameters.back() > config::numCCs) + return false; + if (auto value = readOpcode(opcode.value, Default::tuneCCRange)) + tuneCC[opcode.parameters.back()] = *value; + break; case hash("bend_up"): setValueFromOpcode(opcode, bendUp, Default::bendBoundRange); break; @@ -905,7 +951,7 @@ bool sfz::Region::registerCC(int ccNumber, float ccValue) noexcept return false; } -void sfz::Region::registerPitchWheel(int pitch) noexcept +void sfz::Region::registerPitchWheel(float pitch) noexcept { if (bendRange.containsWithEnd(pitch)) pitchSwitched = true; @@ -938,7 +984,7 @@ float sfz::Region::getBasePitchVariation(int noteNumber, float velocity) const n auto pitchVariationInCents = pitchKeytrack * (noteNumber - (int)pitchKeycenter); // note difference with pitch center pitchVariationInCents += tune; // sample tuning pitchVariationInCents += config::centPerSemitone * transpose; // sample transpose - pitchVariationInCents += static_cast(velocity * pitchVeltrack); // track velocity + pitchVariationInCents += static_cast(velocity) * pitchVeltrack; // track velocity pitchVariationInCents += pitchDistribution(Random::randomGenerator); // random pitch changes return centsFactor(pitchVariationInCents); } @@ -954,7 +1000,7 @@ float sfz::Region::getBaseVolumedB(int noteNumber) const noexcept float sfz::Region::getBaseGain() const noexcept { - return normalizePercents(amplitude); + return amplitude; } float sfz::Region::getPhase() const noexcept @@ -962,7 +1008,7 @@ float sfz::Region::getPhase() const noexcept float phase; if (oscillatorPhase >= 0) { phase = oscillatorPhase * (1.0f / 360.0f); - phase -= static_cast(phase); + phase -= static_cast(static_cast(phase)); } else { std::uniform_real_distribution phaseDist { 0.0001f, 0.9999f }; phase = phaseDist(Random::randomGenerator); @@ -997,48 +1043,6 @@ uint32_t sfz::Region::loopEnd(Oversampling factor) const noexcept return loopRange.getEnd() * static_cast(factor); } -template -float crossfadeIn(const sfz::Range& crossfadeRange, U value, SfzCrossfadeCurve curve) -{ - if (value < crossfadeRange.getStart()) - return 0.0f; - - const auto length = static_cast(crossfadeRange.length()); - if (length == 0.0f) - return 1.0f; - - else if (value < crossfadeRange.getEnd()) { - const auto crossfadePosition = static_cast(value - crossfadeRange.getStart()) / length; - if (curve == SfzCrossfadeCurve::power) - return sqrt(crossfadePosition); - if (curve == SfzCrossfadeCurve::gain) - return crossfadePosition; - } - - return 1.0f; -} - -template -float crossfadeOut(const sfz::Range& crossfadeRange, U value, SfzCrossfadeCurve curve) -{ - if (value > crossfadeRange.getEnd()) - return 0.0f; - - const auto length = static_cast(crossfadeRange.length()); - if (length == 0.0f) - return 1.0f; - - else if (value > crossfadeRange.getStart()) { - const auto crossfadePosition = static_cast(value - crossfadeRange.getStart()) / length; - if (curve == SfzCrossfadeCurve::power) - return std::sqrt(1 - crossfadePosition); - if (curve == SfzCrossfadeCurve::gain) - return 1 - crossfadePosition; - } - - return 1.0f; -} - float sfz::Region::getNoteGain(int noteNumber, float velocity) const noexcept { ASSERT(velocity >= 0.0f && velocity <= 1.0f); diff --git a/src/sfizz/Region.h b/src/sfizz/Region.h index eb3adb13..a7a4e710 100644 --- a/src/sfizz/Region.h +++ b/src/sfizz/Region.h @@ -113,7 +113,7 @@ struct Region { * * @param pitch */ - void registerPitchWheel(int pitch) noexcept; + void registerPitchWheel(float pitch) noexcept; /** * @brief Register a new aftertouch event. * @@ -240,13 +240,15 @@ struct Region { uint32_t group { Default::group }; // group absl::optional offBy {}; // off_by SfzOffMode offMode { Default::offMode }; // off_mode + absl::optional notePolyphony {}; + SfzSelfMask selfMask { Default::selfMask }; // Region logic: key mapping Range keyRange { Default::keyRange }; //lokey, hikey and key Range velocityRange { Default::velocityRange }; // hivel and lovel // Region logic: MIDI conditions - Range bendRange { Default::bendRange }; // hibend and lobend + Range bendRange { Default::bendValueRange }; // hibend and lobend CCMap> ccConditions { Default::ccValueRange }; Range keyswitchRange { Default::keyRange }; // sw_hikey and sw_lokey absl::optional keyswitch {}; // sw_last @@ -270,15 +272,15 @@ struct Region { // Performance parameters: amplifier float volume { Default::volume }; // volume - float amplitude { Default::amplitude }; // amplitude - float pan { Default::pan }; // pan - float width { Default::width }; // width - float position { Default::position }; // position - absl::optional> volumeCC; // volume_oncc - absl::optional> amplitudeCC; // amplitude_oncc - absl::optional> panCC; // pan_oncc - absl::optional> widthCC; // width_oncc - absl::optional> positionCC; // position_oncc + float amplitude { normalizePercents(Default::amplitude) }; // amplitude + float pan { normalizePercents(Default::pan) }; // pan + float width { normalizePercents(Default::width) }; // width + float position { normalizePercents(Default::position) }; // position + CCMap volumeCC { Default::zeroModifier }; // volume_oncc + CCMap amplitudeCC { Default::zeroModifier }; // amplitude_oncc + CCMap panCC { Default::zeroModifier }; // pan_oncc + CCMap widthCC { Default::zeroModifier }; // width_oncc + CCMap positionCC { Default::zeroModifier }; // position_oncc uint8_t ampKeycenter { Default::ampKeycenter }; // amp_keycenter float ampKeytrack { Default::ampKeytrack }; // amp_keytrack float ampVeltrack { Default::ampVeltrack }; // amp_keytrack @@ -306,6 +308,7 @@ struct Region { int pitchVeltrack { Default::pitchVeltrack }; // pitch_veltrack int transpose { Default::transpose }; // transpose int tune { Default::tune }; // tune + CCMap tuneCC { Default::tune }; int bendUp { Default::bendUp }; int bendDown { Default::bendDown }; int bendStep { Default::bendStep }; diff --git a/src/sfizz/Resources.h b/src/sfizz/Resources.h index 99bc9dac..1367d714 100644 --- a/src/sfizz/Resources.h +++ b/src/sfizz/Resources.h @@ -6,6 +6,7 @@ #pragma once #include "FilePool.h" +#include "BufferPool.h" #include "FilterPool.h" #include "EQPool.h" #include "Logger.h" @@ -17,11 +18,25 @@ class WavetableMulti; struct Resources { + BufferPool bufferPool; MidiState midiState; Logger logger; FilePool filePool { logger }; FilterPool filterPool { midiState }; EQPool eqPool { midiState }; WavetablePool wavePool; + + void setSampleRate(float samplerate) + { + midiState.setSampleRate(samplerate); + filterPool.setSampleRate(samplerate); + eqPool.setSampleRate(samplerate); + } + + void setSamplesPerBlock(int samplesPerBlock) + { + bufferPool.setBufferSize(samplesPerBlock); + midiState.setSamplesPerBlock(samplesPerBlock); + } }; } diff --git a/src/sfizz/SIMDHelpers.h b/src/sfizz/SIMDHelpers.h index 2e217c3a..3db5d7bb 100644 --- a/src/sfizz/SIMDHelpers.h +++ b/src/sfizz/SIMDHelpers.h @@ -538,8 +538,8 @@ namespace _internals { template inline void snippetRampLinear(T*& output, T& value, T step) { - value += step; *output++ = value; + value += step; } } @@ -566,8 +566,8 @@ namespace _internals { template inline void snippetRampMultiplicative(T*& output, T& value, T step) { - value *= step; *output++ = value; + value *= step; } } diff --git a/src/sfizz/SIMDSSE.cpp b/src/sfizz/SIMDSSE.cpp index 1b5c466c..1048e9cc 100644 --- a/src/sfizz/SIMDSSE.cpp +++ b/src/sfizz/SIMDSSE.cpp @@ -461,7 +461,7 @@ float sfz::linearRamp(absl::Span output, float value, float while (unaligned(out) && out < lastAligned) _internals::snippetRampLinear(out, value, step); - auto mmValue = _mm_set1_ps(value); + auto mmValue = _mm_set1_ps(value - step); auto mmStep = _mm_set_ps(step + step + step + step, step + step + step, step + step, step); while (out < lastAligned) { @@ -471,7 +471,8 @@ float sfz::linearRamp(absl::Span output, float value, float out += TypeAlignment; } - value = _mm_cvtss_f32(mmValue); + value = _mm_cvtss_f32(mmValue) + step; + while (out < output.end()) _internals::snippetRampLinear(out, value, step); return value; @@ -486,7 +487,7 @@ float sfz::multiplicativeRamp(absl::Span output, float value while (unaligned(out) && out < lastAligned) _internals::snippetRampMultiplicative(out, value, step); - auto mmValue = _mm_set1_ps(value); + auto mmValue = _mm_set1_ps(value / step); auto mmStep = _mm_set_ps(step * step * step * step, step * step * step, step * step, step); while (out < lastAligned) { @@ -496,7 +497,7 @@ float sfz::multiplicativeRamp(absl::Span output, float value out += TypeAlignment; } - value = _mm_cvtss_f32(mmValue); + value = _mm_cvtss_f32(mmValue) * step; while (out < output.end()) _internals::snippetRampMultiplicative(out, value, step); return value; diff --git a/src/sfizz/SfzHelpers.h b/src/sfizz/SfzHelpers.h index 2e904a5e..64d6ed6c 100644 --- a/src/sfizz/SfzHelpers.h +++ b/src/sfizz/SfzHelpers.h @@ -13,13 +13,16 @@ #include "Macros.h" #include "Config.h" #include "MathHelpers.h" +#include "SIMDHelpers.h" #include "absl/meta/type_traits.h" +#include "Defaults.h" namespace sfz { using CCNamePair = std::pair; - +template +using MidiNoteArray = std::array; template struct CCValuePair { int cc; @@ -62,6 +65,46 @@ struct CCValuePairComparator { } }; +struct MidiEvent { + int delay; + float value; +}; +using EventVector = std::vector; + +struct MidiEventDelayComparator { + bool operator()(const MidiEvent& event, const int& delay) + { + return (event.delay < delay); + } + + bool operator()(const int& delay, const MidiEvent& event) + { + return (delay < event.delay); + } + + bool operator()(const MidiEvent& lhs, const MidiEvent& rhs) + { + return (lhs.delay < rhs.delay); + } +}; + +struct MidiEventValueComparator { + bool operator()(const MidiEvent& event, const float& value) + { + return (event.value < value); + } + + bool operator()(const float& value, const MidiEvent& event) + { + return (value < event.value); + } + + bool operator()(const MidiEvent& lhs, const MidiEvent& rhs) + { + return (lhs.value < rhs.value); + } +}; + /** * @brief Converts cents to a pitch ratio * @@ -147,20 +190,18 @@ constexpr float normalizePercents(T percentValue) */ constexpr float normalizeBend(float bendValue) { - return min(max(bendValue, -8191.0f), 8191.0f) / 8191.0f; + return clamp(bendValue, -8191.0f, 8191.0f) / 8191.0f; } -namespace literals -{ -inline float operator ""_norm(unsigned long long int value) -{ - if (value > 127) - value = 127; +namespace literals { + inline float operator""_norm(unsigned long long int value) + { + if (value > 127) + value = 127; - return normalize7Bits(value); + return normalize7Bits(value); + } } -} - /** * @brief Convert a note in string to its equivalent midi note number @@ -232,36 +273,199 @@ bool findDefine(absl::string_view line, absl::string_view& variable, absl::strin */ bool findInclude(absl::string_view line, std::string& path); -/** - * @brief Defines a function that modulates a base value with another one - * - * @tparam T - */ -template -using modFunction = std::function; - -/** - * @brief Modulation helper that adds the modifier to the base value - * - * @tparam T - * @param base the base value - * @param modifier the modifier value - */ -template -inline CXX14_CONSTEXPR void addToBase(T& base, T modifier) -{ - base += modifier; -} - /** * @brief multiply a value by a factor, in cents. To be used for pitch variations. * * @param base * @param modifier */ -inline CXX14_CONSTEXPR void multiplyByCents(float& base, int modifier) +inline CXX14_CONSTEXPR float multiplyByCentsModifier(int modifier, float base) { - base *= centsFactor(modifier); + return base * centsFactor(modifier); +} + +template +inline CXX14_CONSTEXPR float gainModifier(T modifier, float value) +{ + return value * modifier; +} + +/** + * @brief Compute a crossfade in value with respect to a crossfade range (note, velocity, cc, ...) + */ +template +float crossfadeIn(const sfz::Range& crossfadeRange, U value, SfzCrossfadeCurve curve) +{ + if (value < crossfadeRange.getStart()) + return 0.0f; + + const auto length = static_cast(crossfadeRange.length()); + if (length == 0.0f) + return 1.0f; + + else if (value < crossfadeRange.getEnd()) { + const auto crossfadePosition = static_cast(value - crossfadeRange.getStart()) / length; + if (curve == SfzCrossfadeCurve::power) + return sqrt(crossfadePosition); + if (curve == SfzCrossfadeCurve::gain) + return crossfadePosition; + } + + return 1.0f; +} + +/** + * @brief Compute a crossfade out value with respect to a crossfade range (note, velocity, cc, ...) + */ +template +float crossfadeOut(const sfz::Range& crossfadeRange, U value, SfzCrossfadeCurve curve) +{ + if (value > crossfadeRange.getEnd()) + return 0.0f; + + const auto length = static_cast(crossfadeRange.length()); + if (length == 0.0f) + return 1.0f; + + else if (value > crossfadeRange.getStart()) { + const auto crossfadePosition = static_cast(value - crossfadeRange.getStart()) / length; + if (curve == SfzCrossfadeCurve::power) + return std::sqrt(1 - crossfadePosition); + if (curve == SfzCrossfadeCurve::gain) + return 1 - crossfadePosition; + } + + return 1.0f; +} + +template +void linearEnvelope(const EventVector& events, absl::Span envelope, F&& lambda) +{ + ASSERT(events.size() > 0); + ASSERT(events[0].delay == 0); + if (envelope.size() == 0) + return; + + const auto maxDelay = static_cast(envelope.size() - 1); + + auto lastValue = lambda(events[0].value); + auto lastDelay = events[0].delay; + for (unsigned i = 1; i < events.size() && lastDelay < maxDelay; ++i) { + const auto length = min(events[i].delay, maxDelay) - lastDelay; + const auto step = (lambda(events[i].value) - lastValue) / length; + lastValue = linearRamp(envelope.subspan(lastDelay, length), lastValue, step); + lastDelay += length; + } + fill(envelope.subspan(lastDelay), lastValue); +} + +template +void linearEnvelope(const EventVector& events, absl::Span envelope, F&& lambda, float step) +{ + ASSERT(events.size() > 0); + ASSERT(events[0].delay == 0); + ASSERT(step != 0.0); + + if (envelope.size() == 0) + return; + + auto quantize = [step](float value) -> float { + return std::round(value / step) * step; + }; + const auto maxDelay = static_cast(envelope.size() - 1); + + auto lastValue = quantize(lambda(events[0].value)); + auto lastDelay = events[0].delay; + for (unsigned i = 1; i < events.size() && lastDelay < maxDelay; ++i) { + const auto nextValue = quantize(lambda(events[i].value)); + const auto difference = std::abs(nextValue - lastValue); + const auto length = min(events[i].delay, maxDelay) - lastDelay; + + if (difference < step) { + fill(envelope.subspan(lastDelay, length), lastValue); + lastValue = nextValue; + lastDelay += length; + continue; + } + + const auto numSteps = static_cast(difference / step); + const auto stepLength = static_cast(length / numSteps); + for (int i = 0; i < numSteps; ++i) { + fill(envelope.subspan(lastDelay, stepLength), lastValue); + lastValue += lastValue <= nextValue ? step : -step; + lastDelay += stepLength; + } + } + fill(envelope.subspan(lastDelay), lastValue); +} + +template +void multiplicativeEnvelope(const EventVector& events, absl::Span envelope, F&& lambda) +{ + ASSERT(events.size() > 0); + ASSERT(events[0].delay == 0); + + if (envelope.size() == 0) + return; + const auto maxDelay = static_cast(envelope.size() - 1); + + auto lastValue = lambda(events[0].value); + auto lastDelay = events[0].delay; + for (unsigned i = 1; i < events.size() && lastDelay < maxDelay; ++i) { + const auto length = min(events[i].delay, maxDelay) - lastDelay; + const auto nextValue = lambda(events[i].value); + const auto step = std::exp((std::log(nextValue) - std::log(lastValue)) / length); + multiplicativeRamp(envelope.subspan(lastDelay, length), lastValue, step); + lastValue = nextValue; + lastDelay += length; + } + fill(envelope.subspan(lastDelay), lastValue); +} + +template +void multiplicativeEnvelope(const EventVector& events, absl::Span envelope, F&& lambda, float step) +{ + ASSERT(events.size() > 0); + ASSERT(events[0].delay == 0); + ASSERT(step != 0.0f); + + if (envelope.size() == 0) + return; + const auto maxDelay = static_cast(envelope.size() - 1); + + const auto logStep = std::log(step); + // If we assume that a = b.q^r for b in (1, q) then + // log a log b + // ----- = ----- + r + // log q log q + // and log(b)\log(q) is between 0 and 1. + auto quantize = [logStep](float value) -> float { + return std::exp(logStep * std::round(std::log(value) / logStep)); + }; + + auto lastValue = quantize(lambda(events[0].value)); + auto lastDelay = events[0].delay; + for (unsigned i = 1; i < events.size() && lastDelay < maxDelay; ++i) { + const auto length = min(events[i].delay, maxDelay) - lastDelay; + const auto nextValue = quantize(lambda(events[i].value)); + const auto difference = nextValue > lastValue ? nextValue / lastValue : lastValue / nextValue; + + if (difference < step) { + fill(envelope.subspan(lastDelay, length), lastValue); + lastValue = nextValue; + lastDelay += length; + continue; + } + + const auto numSteps = static_cast(std::log(difference) / logStep); + const auto stepLength = static_cast(length / numSteps); + for (int i = 0; i < numSteps; ++i) { + fill(envelope.subspan(lastDelay, stepLength), lastValue); + lastValue = nextValue > lastValue ? lastValue * step : lastValue / step; + lastDelay += stepLength; + } + } + fill(envelope.subspan(lastDelay), lastValue); } } // namespace sfz diff --git a/src/sfizz/Synth.cpp b/src/sfizz/Synth.cpp index a3efca3e..31d959dc 100644 --- a/src/sfizz/Synth.cpp +++ b/src/sfizz/Synth.cpp @@ -67,6 +67,7 @@ void sfz::Synth::onParseFullBlock(const std::string& header, const std::vector& members) } } +void sfz::Synth::handleGroupOpcodes(const std::vector& members) +{ + absl::optional groupIdx; + unsigned maxPolyphony { config::maxVoices }; + + for (auto& member : members) { + switch (member.lettersOnlyHash) { + case hash("group"): + setValueFromOpcode(member, groupIdx, Default::groupRange); + break; + case hash("polyphony"): + setValueFromOpcode(member, maxPolyphony, Range(0, config::maxVoices)); + break; + } + } + + if (groupIdx) + setGroupPolyphony(*groupIdx, maxPolyphony); +} + void sfz::Synth::handleControlOpcodes(const std::vector& members) { for (auto& member : members) { @@ -375,18 +398,22 @@ bool sfz::Synth::loadSfzFile(const fs::path& file) } } + // Some regions had group number but no "group-level" opcodes handled the polyphony + while (groupMaxPolyphony.size() <= region->group) + groupMaxPolyphony.push_back(config::maxVoices); + for (auto note = 0; note < 128; note++) { if (region->keyRange.containsWithEnd(note) || (region->hasKeyswitches() && region->keyswitchRange.containsWithEnd(note))) noteActivationLists[note].push_back(region); } - for (unsigned cc = 0; cc < config::numCCs; cc++) { + for (int cc = 0; cc < config::numCCs; cc++) { if (region->ccTriggers.contains(cc) || region->ccConditions.contains(cc)) ccActivationLists[cc].push_back(region); } // Defaults - for (unsigned cc = 0; cc < config::numCCs; cc++) { + for (int cc = 0; cc < config::numCCs; cc++) { region->registerCC(cc, resources.midiState.getCCValue(cc)); } @@ -479,11 +506,11 @@ void sfz::Synth::setSamplesPerBlock(int samplesPerBlock) noexcept } this->samplesPerBlock = samplesPerBlock; - this->tempBuffer.resize(samplesPerBlock); - this->tempMixNodeBuffer.resize(samplesPerBlock); for (auto& voice : voices) voice->setSamplesPerBlock(samplesPerBlock); + resources.setSamplesPerBlock(samplesPerBlock); + for (auto& bus : effectBuses) { if (bus) bus->setSamplesPerBlock(samplesPerBlock); @@ -501,8 +528,7 @@ void sfz::Synth::setSampleRate(float sampleRate) noexcept for (auto& voice : voices) voice->setSampleRate(sampleRate); - resources.filterPool.setSampleRate(sampleRate); - resources.eqPool.setSampleRate(sampleRate); + resources.setSampleRate(sampleRate); for (auto& bus : effectBuses) { if (bus) @@ -522,9 +548,12 @@ void sfz::Synth::renderBlock(AudioSpan buffer) noexcept return; size_t numFrames = buffer.getNumFrames(); - auto temp = AudioSpan(tempBuffer).first(numFrames); - auto tempMixNode = AudioSpan(tempMixNodeBuffer).first(numFrames); - + auto tempSpan = resources.bufferPool.getStereoBuffer(numFrames); + auto tempMixSpan = resources.bufferPool.getStereoBuffer(numFrames); + if (!tempSpan || !tempMixSpan) { + DBG("[sfizz] Could not get a temporary buffer; exiting callback... "); + return; + } CallbackBreakdown callbackBreakdown; { // Prepare the effect inputs. They are mixes of per-region outputs. @@ -539,7 +568,7 @@ void sfz::Synth::renderBlock(AudioSpan buffer) noexcept { // Main render block ScopedTiming logger { callbackBreakdown.renderMethod }; buffer.fill(0.0f); - tempMixNode.fill(0.0f); + tempSpan->fill(0.0f); resources.filePool.cleanupPromises(); for (auto& voice : voices) { @@ -549,14 +578,14 @@ void sfz::Synth::renderBlock(AudioSpan buffer) noexcept const Region* region = voice->getRegion(); numActiveVoices++; - voice->renderBlock(temp); + voice->renderBlock(*tempSpan); { // Add the output into the effects linked to this region ScopedTiming logger { callbackBreakdown.effects, ScopedTiming::Operation::addToDuration }; for (size_t i = 0, n = effectBuses.size(); i < n; ++i) { if (auto& bus = effectBuses[i]) { float addGain = region->getGainToEffectBus(i); - bus->addToInputs(temp, addGain, numFrames); + bus->addToInputs(*tempSpan, addGain, numFrames); } } } @@ -576,7 +605,7 @@ void sfz::Synth::renderBlock(AudioSpan buffer) noexcept for (auto& bus : effectBuses) { if (bus) { bus->process(numFrames); - bus->mixOutputsTo(buffer, tempMixNode, numFrames); + bus->mixOutputsTo(buffer, *tempMixSpan, numFrames); } } } @@ -585,11 +614,16 @@ void sfz::Synth::renderBlock(AudioSpan buffer) noexcept // -- note(jpc) the purpose of the Mix output is not known. // perhaps it's designed as extension point for custom processing? // as default behavior, it adds itself to the Main signal. - buffer.add(tempMixNode); + buffer.add(*tempMixSpan); // Apply the master volume buffer.applyGain(db2mag(volume)); + { // Clear events and advance midi time + ScopedTiming logger { dispatchDuration, ScopedTiming::Operation::addToDuration }; + resources.midiState.advanceTime(buffer.getNumFrames()); + } + callbackBreakdown.dispatch = dispatchDuration; resources.logger.logCallbackTime(callbackBreakdown, numActiveVoices, numFrames); @@ -655,11 +689,50 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc const auto randValue = randNoteDistribution(Random::randomGenerator); for (auto& region : noteActivationLists[noteNumber]) { if (region->registerNoteOn(noteNumber, velocity, randValue)) { + unsigned activeNotesInGroup { 0 }; + unsigned activeNotes { 0 }; + Voice* selfMaskCandidate { nullptr }; + for (auto& voice : voices) { + const auto voiceRegion = voice->getRegion(); + if (voiceRegion == nullptr) + continue; + + if (voiceRegion->group == region->group) + activeNotesInGroup += 1; + + if (region->notePolyphony) { + if (voice->getTriggerNumber() == noteNumber && voice->getTriggerType() == Voice::TriggerType::NoteOn) { + activeNotes += 1; + switch (region->selfMask) { + case SfzSelfMask::mask: + if (voice->getTriggerValue() < velocity) { + if (!selfMaskCandidate || selfMaskCandidate->getTriggerValue() > voice->getTriggerValue()) + selfMaskCandidate = voice.get(); + } + break; + case SfzSelfMask::dontMask: + if (!selfMaskCandidate || selfMaskCandidate->getSourcePosition() < voice->getSourcePosition()) + selfMaskCandidate = voice.get(); + break; + } + } + } + if (voice->checkOffGroup(delay, region->group)) noteOffDispatch(delay, voice->getTriggerNumber(), voice->getTriggerValue()); } + if (activeNotesInGroup >= groupMaxPolyphony[region->group]) + continue; + + if (region->notePolyphony && activeNotes >= *region->notePolyphony) { + if (selfMaskCandidate != nullptr) + selfMaskCandidate->release(delay); + else // We're the lowest velocity guy here + continue; + } + auto voice = findFreeVoice(); if (voice == nullptr) continue; @@ -705,16 +778,17 @@ void sfz::Synth::pitchWheel(int delay, int pitch) noexcept { ASSERT(pitch <= 8192); ASSERT(pitch >= -8192); + const auto normalizedPitch = normalizeBend(pitch); ScopedTiming logger { dispatchDuration, ScopedTiming::Operation::addToDuration }; - resources.midiState.pitchBendEvent(delay, pitch); + resources.midiState.pitchBendEvent(delay, normalizedPitch); for (auto& region : regions) { - region->registerPitchWheel(pitch); + region->registerPitchWheel(normalizedPitch); } for (auto& voice : voices) { - voice->registerPitchWheel(delay, pitch); + voice->registerPitchWheel(delay, normalizedPitch); } } void sfz::Synth::aftertouch(int /* delay */, uint8_t /* aftertouch */) noexcept @@ -953,12 +1027,12 @@ void sfz::Synth::resetAllControllers(int delay) noexcept resources.midiState.resetAllControllers(delay); for (auto& voice : voices) { voice->registerPitchWheel(delay, 0); - for (unsigned cc = 0; cc < config::numCCs; ++cc) + for (int cc = 0; cc < config::numCCs; ++cc) voice->registerCC(delay, cc, 0.0f); } for (auto& region : regions) { - for (unsigned cc = 0; cc < config::numCCs; ++cc) + for (int cc = 0; cc < config::numCCs; ++cc) region->registerCC(cc, 0.0f); } } @@ -1006,3 +1080,11 @@ void sfz::Synth::allSoundOff() noexcept for (auto& effectBus : effectBuses) effectBus->clear(); } + +void sfz::Synth::setGroupPolyphony(unsigned groupIdx, unsigned polyphony) noexcept +{ + while (groupMaxPolyphony.size() <= groupIdx) + groupMaxPolyphony.push_back(config::maxVoices); + + groupMaxPolyphony[groupIdx] = polyphony; +} diff --git a/src/sfizz/Synth.h b/src/sfizz/Synth.h index b18a453b..a9ce64ab 100644 --- a/src/sfizz/Synth.h +++ b/src/sfizz/Synth.h @@ -410,6 +410,15 @@ protected: void onParseWarning(const SourceRange& range, const std::string& message) override; private: + /** + * @brief change the group maximum polyphony + * + * @param groupIdx the group index + * @param polyphone the max polyphony + */ + void setGroupPolyphony(unsigned groupIdx, unsigned polyphony) noexcept; + std::vector groupMaxPolyphony { config::maxVoices }; + /** * @brief Reset all CCs; to be used on CC 121 * @@ -440,6 +449,12 @@ private: * @param members the opcodes of the block */ void handleGlobalOpcodes(const std::vector& members); + /** + * @brief Helper function to dispatch opcodes + * + * @param members the opcodes of the block + */ + void handleGroupOpcodes(const std::vector& members); /** * @brief Helper function to dispatch opcodes * @@ -501,10 +516,6 @@ private: // Curves CurveSet curves; - // Intermediate buffers - AudioBuffer tempBuffer { 2, config::defaultSamplesPerBlock }; - AudioBuffer tempMixNodeBuffer { 2, config::defaultSamplesPerBlock }; - int samplesPerBlock { config::defaultSamplesPerBlock }; float sampleRate { config::defaultSampleRate }; float volume { Default::globalVolume }; diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 7bd044c5..5da0f909 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -74,50 +74,11 @@ void sfz::Voice::startVoice(Region* region, int delay, int number, float value, speedRatio = static_cast(currentPromise->sampleRate / this->sampleRate); } pitchRatio = region->getBasePitchVariation(number, value); - baseVolumedB = region->getBaseVolumedB(number); - auto volumedB = baseVolumedB; - if (region->volumeCC) - volumedB += resources.midiState.getCCValue(region->volumeCC->cc) * region->volumeCC->value; - volumeEnvelope.reset(db2mag(Default::volumeRange.clamp(volumedB))); - baseGain = region->getBaseGain(); if (triggerType != TriggerType::CC) baseGain *= region->getNoteGain(number, value); - float gain { baseGain }; - if (region->amplitudeCC) - gain += resources.midiState.getCCValue(region->amplitudeCC->cc) * normalizePercents(region->amplitudeCC->value); - amplitudeEnvelope.reset(Default::normalizedRange.clamp(gain)); - - float crossfadeGain { region->getCrossfadeGain() }; - crossfadeEnvelope.reset(Default::normalizedRange.clamp(crossfadeGain)); - - basePan = normalizePercents(region->pan); - auto pan = basePan; - if (region->panCC) - pan += resources.midiState.getCCValue(region->panCC->cc) * normalizePercents(region->panCC->value); - panEnvelope.reset(Default::symmetricNormalizedRange.clamp(pan)); - - basePosition = normalizePercents(region->position); - auto position = basePosition; - if (region->positionCC) - position += resources.midiState.getCCValue(region->positionCC->cc) * normalizePercents(region->positionCC->value); - positionEnvelope.reset(Default::symmetricNormalizedRange.clamp(position)); - - baseWidth = normalizePercents(region->width); - auto width = baseWidth; - if (region->widthCC) - width += resources.midiState.getCCValue(region->widthCC->cc) * normalizePercents(region->widthCC->value); - widthEnvelope.reset(Default::symmetricNormalizedRange.clamp(width)); - - pitchBendEnvelope.setFunction([region](float pitchValue){ - const auto normalizedBend = normalizeBend(pitchValue); - const auto bendInCents = normalizedBend > 0.0f ? normalizedBend * static_cast(region->bendUp) : -normalizedBend * static_cast(region->bendDown); - return centsFactor(bendInCents); - }); - pitchBendEnvelope.reset(static_cast(resources.midiState.getPitchBend())); - // Check that we can handle the number of filters; filters should be cleared here ASSERT((filters.capacity() - filters.size()) >= region->filters.size()); ASSERT((equalizers.capacity() - equalizers.size()) >= region->equalizers.size()); @@ -198,48 +159,14 @@ void sfz::Voice::registerCC(int delay, int ccNumber, float ccValue) noexcept if (region->checkSustain && noteIsOff && ccNumber == config::sustainCC && ccValue < config::halfCCThreshold) release(delay); - - // Add a minimum delay for smoothing the envelopes - // TODO: this feels like a hack, revisit this along with the smoothed envelopes... - delay = max(delay, minEnvelopeDelay); - - if (region->amplitudeCC && ccNumber == region->amplitudeCC->cc) { - const float newGain { baseGain + ccValue * normalizePercents(region->amplitudeCC->value) }; - amplitudeEnvelope.registerEvent(delay, Default::normalizedRange.clamp(newGain)); - } - - if (region->volumeCC && ccNumber == region->volumeCC->cc) { - const float newVolumedB { baseVolumedB + ccValue * region->volumeCC->value }; - volumeEnvelope.registerEvent(delay, db2mag(Default::volumeRange.clamp(newVolumedB))); - } - - if (region->panCC && ccNumber == region->panCC->cc) { - const float newPan { basePan + ccValue * normalizePercents(region->panCC->value) }; - panEnvelope.registerEvent(delay, Default::symmetricNormalizedRange.clamp(newPan)); - } - - if (region->positionCC && ccNumber == region->positionCC->cc) { - const float newPosition { basePosition + ccValue * normalizePercents(region->positionCC->value) }; - positionEnvelope.registerEvent(delay, Default::symmetricNormalizedRange.clamp(newPosition)); - } - - if (region->widthCC && ccNumber == region->widthCC->cc) { - const float newWidth { baseWidth + ccValue * normalizePercents(region->widthCC->value) }; - widthEnvelope.registerEvent(delay, Default::symmetricNormalizedRange.clamp(newWidth)); - } - - if (region->crossfadeCCInRange.contains(ccNumber) || region->crossfadeCCOutRange.contains(ccNumber)) { - const float crossfadeGain = region->getCrossfadeGain(); - crossfadeEnvelope.registerEvent(delay, Default::normalizedRange.clamp(crossfadeGain)); - } } -void sfz::Voice::registerPitchWheel(int delay, int pitch) noexcept +void sfz::Voice::registerPitchWheel(int delay, float pitch) noexcept { if (state == State::idle) return; - - pitchBendEnvelope.registerEvent(delay, static_cast(pitch)); + UNUSED(delay); + UNUSED(pitch); } void sfz::Voice::registerAftertouch(int delay, uint8_t aftertouch) noexcept @@ -267,14 +194,6 @@ void sfz::Voice::setSamplesPerBlock(int samplesPerBlock) noexcept { this->samplesPerBlock = samplesPerBlock; this->minEnvelopeDelay = samplesPerBlock / 2; - tempBuffer1.resize(samplesPerBlock); - tempBuffer2.resize(samplesPerBlock); - tempBuffer3.resize(samplesPerBlock); - indexBuffer.resize(samplesPerBlock); - tempSpan1 = absl::MakeSpan(tempBuffer1); - tempSpan2 = absl::MakeSpan(tempBuffer2); - tempSpan3 = absl::MakeSpan(tempBuffer3); - indexSpan = absl::MakeSpan(indexBuffer); } void sfz::Voice::renderBlock(AudioSpan buffer) noexcept @@ -299,10 +218,15 @@ void sfz::Voice::renderBlock(AudioSpan buffer) noexcept fillWithData(delayed_buffer); } - if (region->isStereo) - processStereo(buffer); - else - processMono(buffer); + if (region->isStereo) { + ampStageStereo(buffer); + panStageStereo(buffer); + filterStageStereo(buffer); + } else { + ampStageMono(buffer); + filterStageMono(buffer); + panStageMono(buffer); + } if (!egEnvelope.isSmoothing()) reset(); @@ -311,120 +235,213 @@ void sfz::Voice::renderBlock(AudioSpan buffer) noexcept this->triggerDelay = absl::nullopt; } -void sfz::Voice::processMono(AudioSpan buffer) noexcept +void sfz::Voice::ampStageMono(AudioSpan buffer) noexcept { + ScopedTiming logger { amplitudeDuration }; + const auto numSamples = buffer.getNumFrames(); - auto leftBuffer = buffer.getSpan(0); - auto rightBuffer = buffer.getSpan(1); + const auto leftBuffer = buffer.getSpan(0); + const auto xfCurve = region->crossfadeCCCurve; - auto modulationSpan = tempSpan1.first(numSamples); + auto modulationSpan = resources.bufferPool.getBuffer(numSamples); + auto tempSpan = resources.bufferPool.getBuffer(numSamples); + if (!modulationSpan || !tempSpan) + return; - { // Amplitude processing - ScopedTiming logger { amplitudeDuration }; + // Amplitude envelope + fill(*modulationSpan, baseGain); + for (const auto& mod : region->amplitudeCC) { + const auto events = resources.midiState.getCCEvents(mod.cc); + linearEnvelope(events, *tempSpan, [&mod](float x) { return x * mod.value; }); + applyGain(*tempSpan, *modulationSpan); + } + applyGain(*modulationSpan, leftBuffer); - // Amplitude envelope - amplitudeEnvelope.getBlock(modulationSpan); - applyGain(modulationSpan, leftBuffer); + // Crossfade envelopes + fill(*modulationSpan, 1.0f); + for (const auto& mod : region->crossfadeCCInRange) { + const auto events = resources.midiState.getCCEvents(mod.cc); + linearEnvelope(events, *tempSpan, [&](float x) { return crossfadeIn(mod.value, x, xfCurve); }); + applyGain(*tempSpan, *modulationSpan); + } + for (const auto& mod : region->crossfadeCCOutRange) { + const auto events = resources.midiState.getCCEvents(mod.cc); + linearEnvelope(events, *tempSpan, [&](float x) { return crossfadeOut(mod.value, x, xfCurve); }); + applyGain(*tempSpan, *modulationSpan); + } + applyGain(*modulationSpan, leftBuffer); - // Crossfade envelope - crossfadeEnvelope.getBlock(modulationSpan); - applyGain(modulationSpan, leftBuffer); + // Volume envelope + fill(*modulationSpan, db2mag(baseVolumedB)); + for (const auto& mod : region->volumeCC) { + const auto events = resources.midiState.getCCEvents(mod.cc); + multiplicativeEnvelope(events, *tempSpan, [&](float x) { return db2mag(x * mod.value); }); + applyGain(*tempSpan, *modulationSpan); + } + applyGain(*modulationSpan, leftBuffer); - // Volume envelope - volumeEnvelope.getBlock(modulationSpan); - applyGain(modulationSpan, leftBuffer); + // AmpEG envelope + egEnvelope.getBlock(*modulationSpan); + applyGain(*modulationSpan, leftBuffer); +} - // AmpEG envelope - egEnvelope.getBlock(modulationSpan); - applyGain(modulationSpan, leftBuffer); +void sfz::Voice::ampStageStereo(AudioSpan buffer) noexcept +{ + ScopedTiming logger { amplitudeDuration }; + + const auto numSamples = buffer.getNumFrames(); + + const auto xfCurve = region->crossfadeCCCurve; + + auto modulationSpan = resources.bufferPool.getBuffer(numSamples); + auto tempSpan = resources.bufferPool.getBuffer(numSamples); + if (!modulationSpan || !tempSpan) + return; + + // Amplitude envelope + fill(*modulationSpan, baseGain); + for (const auto& mod : region->amplitudeCC) { + const auto events = resources.midiState.getCCEvents(mod.cc); + linearEnvelope(events, *tempSpan, [&mod](float x) { return x * mod.value; }); + applyGain(*tempSpan, *modulationSpan); + } + buffer.applyGain(*modulationSpan); + + // Crossfade envelopes + fill(*modulationSpan, 1.0f); + for (const auto& mod : region->crossfadeCCInRange) { + const auto events = resources.midiState.getCCEvents(mod.cc); + linearEnvelope(events, *tempSpan, [&](float x) { return crossfadeIn(mod.value, x, xfCurve); }); + applyGain(*tempSpan, *modulationSpan); + } + for (const auto& mod : region->crossfadeCCOutRange) { + const auto events = resources.midiState.getCCEvents(mod.cc); + linearEnvelope(events, *tempSpan, [&](float x) { return crossfadeOut(mod.value, x, xfCurve); }); + applyGain(*tempSpan, *modulationSpan); + } + buffer.applyGain(*modulationSpan); + + // Volume envelope + fill(*modulationSpan, db2mag(baseVolumedB)); + for (const auto& mod : region->volumeCC) { + const auto events = resources.midiState.getCCEvents(mod.cc); + multiplicativeEnvelope(events, *tempSpan, [&](float x) { return db2mag(x * mod.value); }); + applyGain(*tempSpan, *modulationSpan); + } + buffer.applyGain(*modulationSpan); + + // AmpEG envelope + egEnvelope.getBlock(*modulationSpan); + buffer.applyGain(*modulationSpan); +} + +void sfz::Voice::panStageMono(AudioSpan buffer) noexcept +{ + ScopedTiming logger { panningDuration }; + + const auto numSamples = buffer.getNumFrames(); + const auto leftBuffer = buffer.getSpan(0); + const auto rightBuffer = buffer.getSpan(1); + + auto modulationSpan = resources.bufferPool.getBuffer(numSamples); + auto tempSpan = resources.bufferPool.getBuffer(numSamples); + if (!modulationSpan || !tempSpan) + return; + + // Prepare for stereo output + copy(leftBuffer, rightBuffer); + + // Apply panning + fill(*modulationSpan, region->pan); + for (const auto& mod : region->panCC) { + const auto events = resources.midiState.getCCEvents(mod.cc); + linearEnvelope(events, *tempSpan, [&mod](float x) { return x * mod.value; }); + add(*tempSpan, *modulationSpan); + } + pan(*modulationSpan, leftBuffer, rightBuffer); +} + +void sfz::Voice::panStageStereo(AudioSpan buffer) noexcept +{ + ScopedTiming logger { panningDuration }; + const auto numSamples = buffer.getNumFrames(); + const auto leftBuffer = buffer.getSpan(0); + const auto rightBuffer = buffer.getSpan(1); + + auto modulationSpan = resources.bufferPool.getBuffer(numSamples); + auto tempSpan = resources.bufferPool.getBuffer(numSamples); + if (!modulationSpan || !tempSpan) + return; + + // Apply panning + // panningModulation(*modulationSpan); + fill(*modulationSpan, region->pan); + for (const auto& mod : region->panCC) { + const auto events = resources.midiState.getCCEvents(mod.cc); + linearEnvelope(events, *tempSpan, [&mod](float x) { return x * mod.value; }); + add(*tempSpan, *modulationSpan); + } + pan(*modulationSpan, leftBuffer, rightBuffer); + + // Apply the width/position process + // widthModulation(*modulationSpan); + fill(*modulationSpan, region->width); + for (const auto& mod : region->widthCC) { + const auto events = resources.midiState.getCCEvents(mod.cc); + linearEnvelope(events, *tempSpan, [&mod](float x) { return x * mod.value; }); + add(*tempSpan, *modulationSpan); + } + width(*modulationSpan, leftBuffer, rightBuffer); + + // positionModulation(*modulationSpan); + fill(*modulationSpan, region->position); + for (const auto& mod : region->positionCC) { + const auto events = resources.midiState.getCCEvents(mod.cc); + linearEnvelope(events, *tempSpan, [&mod](float x) { return x * mod.value; }); + add(*tempSpan, *modulationSpan); + } + pan(*modulationSpan, leftBuffer, rightBuffer); +} + +void sfz::Voice::filterStageMono(AudioSpan buffer) noexcept +{ + ScopedTiming logger { filterDuration }; + const auto numSamples = buffer.getNumFrames(); + const auto leftBuffer = buffer.getSpan(0); + const float* inputChannel[1] { leftBuffer.data() }; + float* outputChannel[1] { leftBuffer.data() }; + for (auto& filter : filters) { + filter->process(inputChannel, outputChannel, numSamples); } - { // Filtering and EQ - ScopedTiming logger { filterDuration }; - - const float* inputChannel[1] { leftBuffer.data() }; - float* outputChannel[1] { leftBuffer.data() }; - for (auto& filter: filters) { - filter->process(inputChannel, outputChannel, numSamples); - } - - for (auto& eq: equalizers) { - eq->process(inputChannel, outputChannel, numSamples); - } - } - - { // Panning and stereo processing - ScopedTiming logger { panningDuration }; - - // Prepare for stereo output - copy(leftBuffer, rightBuffer); - - // Apply panning - panEnvelope.getBlock(modulationSpan); - pan(modulationSpan, leftBuffer, rightBuffer); + for (auto& eq : equalizers) { + eq->process(inputChannel, outputChannel, numSamples); } } -void sfz::Voice::processStereo(AudioSpan buffer) noexcept +void sfz::Voice::filterStageStereo(AudioSpan buffer) noexcept { + ScopedTiming logger { filterDuration }; const auto numSamples = buffer.getNumFrames(); - auto modulationSpan = tempSpan1.first(numSamples); - auto leftBuffer = buffer.getSpan(0); - auto rightBuffer = buffer.getSpan(1); + const auto leftBuffer = buffer.getSpan(0); + const auto rightBuffer = buffer.getSpan(1); - { // Amplitude processing - ScopedTiming logger { amplitudeDuration }; + const float* inputChannels[2] { leftBuffer.data(), rightBuffer.data() }; + float* outputChannels[2] { leftBuffer.data(), rightBuffer.data() }; - // Amplitude envelope - amplitudeEnvelope.getBlock(modulationSpan); - buffer.applyGain(modulationSpan); - - // Crossfade envelope - crossfadeEnvelope.getBlock(modulationSpan); - buffer.applyGain(modulationSpan); - - // Volume envelope - volumeEnvelope.getBlock(modulationSpan); - buffer.applyGain(modulationSpan); - - // AmpEG envelope - egEnvelope.getBlock(modulationSpan); - buffer.applyGain(modulationSpan); + for (auto& filter : filters) { + filter->process(inputChannels, outputChannels, numSamples); } - { // Panning and stereo processing - ScopedTiming logger { panningDuration }; - - // Apply panning - panEnvelope.getBlock(modulationSpan); - pan(modulationSpan, leftBuffer, rightBuffer); - - // Apply the width/position process - widthEnvelope.getBlock(modulationSpan); - width(modulationSpan, leftBuffer, rightBuffer); - positionEnvelope.getBlock(modulationSpan); - pan(modulationSpan, leftBuffer, rightBuffer); - } - - { // Filtering and EQ - ScopedTiming logger { filterDuration }; - - const float* inputChannels[2] { leftBuffer.data(), rightBuffer.data() }; - float* outputChannels[2] { leftBuffer.data(), rightBuffer.data() }; - - for (auto& filter: filters) { - filter->process(inputChannels, outputChannels, numSamples); - } - - for (auto& eq: equalizers) { - eq->process(inputChannels, outputChannels, numSamples); - } + for (auto& eq : equalizers) { + eq->process(inputChannels, outputChannels, numSamples); } } void sfz::Voice::fillWithData(AudioSpan buffer) noexcept { - if (buffer.getNumFrames() == 0) + const auto numSamples = buffer.getNumFrames(); + if (numSamples == 0) return; if (currentPromise == nullptr) { @@ -433,30 +450,46 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept } auto source = currentPromise->getData(); - auto indices = indexSpan.first(buffer.getNumFrames()); - auto jumps = tempSpan1.first(buffer.getNumFrames()); - auto bends = tempSpan2.first(buffer.getNumFrames()); - auto leftCoeffs = tempSpan1.first(buffer.getNumFrames()); - auto rightCoeffs = tempSpan2.first(buffer.getNumFrames()); - fill(jumps, pitchRatio * speedRatio); + auto jumps = resources.bufferPool.getBuffer(numSamples); + auto bends = resources.bufferPool.getBuffer(numSamples); + auto leftCoeffs = resources.bufferPool.getBuffer(numSamples); + auto rightCoeffs = resources.bufferPool.getBuffer(numSamples); + auto indices = resources.bufferPool.getIndexBuffer(numSamples); + if (!jumps || !bends || !indices || !rightCoeffs || !leftCoeffs) + return; + + fill(*jumps, pitchRatio * speedRatio); + + const auto events = resources.midiState.getPitchEvents(); + const auto bendLambda = [this](float bend) { + const auto bendInCents = bend > 0.0f ? bend * static_cast(region->bendUp) : -bend * static_cast(region->bendDown); + return centsFactor(bendInCents); + }; + if (region->bendStep > 1) - pitchBendEnvelope.getQuantizedBlock(bends, bendStepFactor); + multiplicativeEnvelope(events, *bends, bendLambda, bendStepFactor); else - pitchBendEnvelope.getBlock(bends); + multiplicativeEnvelope(events, *bends, bendLambda); + applyGain(*bends, *jumps); - applyGain(bends, jumps); - jumps[0] += floatPositionOffset; - cumsum(jumps, jumps); - sfzInterpolationCast(jumps, indices, leftCoeffs, rightCoeffs); - add(sourcePosition, indices); + for (const auto& mod : region->tuneCC) { + const auto events = resources.midiState.getCCEvents(mod.cc); + multiplicativeEnvelope(events, *bends, [&](float x) { return centsFactor(x * mod.value); }); + applyGain(*bends, *jumps); + } + + jumps->front() += floatPositionOffset; + cumsum(*jumps, *jumps); + sfzInterpolationCast(*jumps, *indices, *leftCoeffs, *rightCoeffs); + add(sourcePosition, *indices); if (region->shouldLoop() && region->loopEnd(currentPromise->oversamplingFactor) <= source.getNumFrames()) { const auto loopEnd = static_cast(region->loopEnd(currentPromise->oversamplingFactor)); const auto offset = loopEnd - static_cast(region->loopStart(currentPromise->oversamplingFactor)) + 1; - for (auto* index = indices.begin(); index < indices.end(); ++index) { + for (auto* index = indices->begin(); index < indices->end(); ++index) { if (*index > loopEnd) { - const auto remainingElements = static_cast(std::distance(index, indices.end())); + const auto remainingElements = static_cast(std::distance(index, indices->end())); subtract(offset, { index, remainingElements }); } } @@ -465,46 +498,46 @@ void sfz::Voice::fillWithData(AudioSpan buffer) noexcept static_cast(region->trueSampleEnd(currentPromise->oversamplingFactor)), static_cast(source.getNumFrames()) ) - 2; - for (auto* index = indices.begin(); index < indices.end(); ++index) { + for (auto* index = indices->begin(); index < indices->end(); ++index) { if (*index >= sampleEnd) { - release(static_cast(std::distance(indices.begin(), index))); - const auto remainingElements = static_cast(std::distance(index, indices.end())); + release(static_cast(std::distance(indices->begin(), index))); + const auto remainingElements = static_cast(std::distance(index, indices->end())); if (source.getNumFrames() - 1 < region->trueSampleEnd(currentPromise->oversamplingFactor)) { DBG("[sfizz] Underflow: source available samples " << source.getNumFrames() << "/" << region->trueSampleEnd(currentPromise->oversamplingFactor) << " for sample " << region->sample); } - fill(indices.last(remainingElements), sampleEnd); - fill(leftCoeffs.last(remainingElements), 0.0f); - fill(rightCoeffs.last(remainingElements), 1.0f); + fill(indices->last(remainingElements), sampleEnd); + fill(leftCoeffs->last(remainingElements), 0.0f); + fill(rightCoeffs->last(remainingElements), 1.0f); break; } } } - auto ind = indices.data(); - auto leftCoeff = leftCoeffs.data(); - auto rightCoeff = rightCoeffs.data(); + auto ind = indices->data(); + auto leftCoeff = leftCoeffs->data(); + auto rightCoeff = rightCoeffs->data(); auto leftSource = source.getConstSpan(0); auto left = buffer.getChannel(0); if (source.getNumChannels() == 1) { - while (ind < indices.end()) { + while (ind < indices->end()) { *left = linearInterpolation(leftSource[*ind], leftSource[*ind + 1], *leftCoeff, *rightCoeff); incrementAll(ind, left, leftCoeff, rightCoeff); } } else { auto right = buffer.getChannel(1); auto rightSource = source.getConstSpan(1); - while (ind < indices.end()) { + while (ind < indices->end()) { *left = linearInterpolation(leftSource[*ind], leftSource[*ind + 1], *leftCoeff, *rightCoeff); *right = linearInterpolation(rightSource[*ind], rightSource[*ind + 1], *leftCoeff, *rightCoeff); incrementAll(ind, left, right, leftCoeff, rightCoeff); } } - sourcePosition = indices.back(); - floatPositionOffset = rightCoeffs.back(); + sourcePosition = indices->back(); + floatPositionOffset = rightCoeffs->back(); } void sfz::Voice::fillWithGenerator(AudioSpan buffer) noexcept @@ -516,21 +549,33 @@ void sfz::Voice::fillWithGenerator(AudioSpan buffer) noexcept absl::c_generate(leftSpan, [&](){ return noiseDist(Random::randomGenerator); }); absl::c_generate(rightSpan, [&](){ return noiseDist(Random::randomGenerator); }); } else { - // wavetables for sine and other generators - auto frequencies = tempSpan1.first(buffer.getNumFrames()); - auto bends = tempSpan2.first(buffer.getNumFrames()); + const auto numSamples = buffer.getNumFrames(); + auto frequencies = resources.bufferPool.getBuffer(numSamples); + auto bends = resources.bufferPool.getBuffer(numSamples); + if (!frequencies || !bends) + return; float keycenterFrequency = midiNoteFrequency(region->pitchKeycenter); - fill(frequencies, pitchRatio * keycenterFrequency); + fill(*frequencies, pitchRatio * keycenterFrequency); + const auto events = resources.midiState.getPitchEvents(); + const auto bendLambda = [this](float bend) { + const auto bendInCents = bend > 0.0f ? bend * static_cast(region->bendUp) : -bend * static_cast(region->bendDown); + return centsFactor(bendInCents); + }; if (region->bendStep > 1) - pitchBendEnvelope.getQuantizedBlock(bends, bendStepFactor); + multiplicativeEnvelope(events, *bends, bendLambda, bendStepFactor); else - pitchBendEnvelope.getBlock(bends); + multiplicativeEnvelope(events, *bends, bendLambda); + applyGain(*bends, *frequencies); - applyGain(bends, frequencies); + for (const auto& mod : region->tuneCC) { + const auto events = resources.midiState.getCCEvents(mod.cc); + multiplicativeEnvelope(events, *bends, [&](float x) { return centsFactor(x * mod.value); }); + applyGain(*bends, *frequencies); + } - waveOscillator.processModulated(frequencies.data(), leftSpan.data(), buffer.getNumFrames()); + waveOscillator.processModulated(frequencies->data(), leftSpan.data(), buffer.getNumFrames()); copy(leftSpan, rightSpan); } } @@ -603,6 +648,6 @@ void sfz::Voice::setMaxFiltersPerVoice(size_t numFilters) void sfz::Voice::setMaxEQsPerVoice(size_t numFilters) { // There are filters in there, this call is unexpected - ASSERT(filters.size() == 0); - filters.reserve(numFilters); + ASSERT(equalizers.size() == 0); + equalizers.reserve(numFilters); } diff --git a/src/sfizz/Voice.h b/src/sfizz/Voice.h index 1772bf45..7afcd24e 100644 --- a/src/sfizz/Voice.h +++ b/src/sfizz/Voice.h @@ -7,7 +7,6 @@ #pragma once #include "Config.h" #include "ADSREnvelope.h" -#include "EventEnvelopes.h" #include "HistoricalBuffer.h" #include "Region.h" #include "AudioBuffer.h" @@ -107,7 +106,7 @@ public: * @param delay * @param pitch */ - void registerPitchWheel(int delay, int pitch) noexcept; + void registerPitchWheel(int delay, float pitch) noexcept; /** * @brief Register an aftertouch event; for now this does nothing * @@ -210,6 +209,13 @@ public: * @param numFilters */ void setMaxEQsPerVoice(size_t numEQs); + /** + * @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, bool fastRelease = false) noexcept; Duration getLastDataDuration() const noexcept { return dataDuration; } Duration getLastAmplitudeDuration() const noexcept { return amplitudeDuration; } @@ -231,25 +237,13 @@ private: * @param buffer */ void fillWithGenerator(AudioSpan buffer) noexcept; - /** - * @brief The function processing a mono sample source - * - * @param buffer - */ - void processMono(AudioSpan buffer) noexcept; - /** - * @brief The function processing a stereo sample source - * - * @param buffer - */ - void processStereo(AudioSpan buffer) noexcept; - /** - * @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, bool fastRelease = false) noexcept; + void ampStageMono(AudioSpan buffer) noexcept; + void ampStageStereo(AudioSpan buffer) noexcept; + void panStageMono(AudioSpan buffer) noexcept; + void panStageStereo(AudioSpan buffer) noexcept; + void filterStageMono(AudioSpan buffer) noexcept; + void filterStageStereo(AudioSpan buffer) noexcept; + Region* region { nullptr }; enum class State { @@ -268,9 +262,6 @@ private: float pitchRatio { 1.0 }; float baseVolumedB{ 0.0 }; float baseGain { 1.0 }; - float basePan { 0.0 }; - float basePosition { 0.0 }; - float baseWidth { 0.0 }; float baseFrequency { 440.0 }; float phase { 0.0f }; @@ -280,15 +271,6 @@ private: FilePromisePtr currentPromise { nullptr }; - Buffer tempBuffer1; - Buffer tempBuffer2; - Buffer tempBuffer3; - Buffer indexBuffer; - absl::Span tempSpan1 { absl::MakeSpan(tempBuffer1) }; - absl::Span tempSpan2 { absl::MakeSpan(tempBuffer2) }; - absl::Span tempSpan3 { absl::MakeSpan(tempBuffer3) }; - absl::Span indexSpan { absl::MakeSpan(indexBuffer) }; - int samplesPerBlock { config::defaultSamplesPerBlock }; int minEnvelopeDelay { config::defaultSamplesPerBlock / 2 }; float sampleRate { config::defaultSampleRate }; @@ -299,13 +281,6 @@ private: std::vector equalizers; ADSREnvelope egEnvelope; - LinearEnvelope amplitudeEnvelope; // linear events - LinearEnvelope crossfadeEnvelope; - LinearEnvelope panEnvelope; - LinearEnvelope positionEnvelope; - LinearEnvelope widthEnvelope; - MultiplicativeEnvelope pitchBendEnvelope; - MultiplicativeEnvelope volumeEnvelope; float bendStepFactor { centsFactor(1) }; WavetableOscillator waveOscillator; diff --git a/tests/ADSREnvelopeT.cpp b/tests/ADSREnvelopeT.cpp index 22783f3d..0d344096 100644 --- a/tests/ADSREnvelopeT.cpp +++ b/tests/ADSREnvelopeT.cpp @@ -51,7 +51,7 @@ TEST_CASE("[ADSREnvelope] Attack") envelope.reset(region, state, 0, 0.0f, 100.0f); std::array output; - std::array expected { 0.5f, 1.0f, 1.0f, 1.0f, 1.0f }; + std::array expected { 0.0f, 0.5f, 1.0f, 1.0f, 1.0f }; for (auto& out : output) out = envelope.getNextValue(); REQUIRE(approxEqual(output, expected)); @@ -71,7 +71,7 @@ TEST_CASE("[ADSREnvelope] Attack again") envelope.reset(region, state, 0, 0.0f, 100.0f); std::array output; - std::array expected { 0.33333f, 0.66667f, 1.0f, 1.0f, 1.0f }; + std::array expected { 0.0f, 0.33333f, 0.66667f, 1.0f, 1.0f }; for (auto& out : output) out = envelope.getNextValue(); REQUIRE(approxEqual(output, expected)); @@ -92,8 +92,8 @@ TEST_CASE("[ADSREnvelope] Release") envelope.reset(region, state, 0, 0.0f, 100.0f); envelope.startRelease(2); - std::array output; - std::array expected { 0.5f, 1.0f, 0.08409f, 0.00707f, 0.000594604f, 0.00005f, 0.0f, 0.0f }; + std::array output; + std::array expected { 0.0f, 0.5f, 1.0f, 0.08409f, 0.00707f, 0.000594604f, 0.00005f, 0.0f, 0.0f }; for (auto& out : output) out = envelope.getNextValue(); REQUIRE(approxEqual(output, expected)); @@ -113,10 +113,10 @@ TEST_CASE("[ADSREnvelope] Delay") region.amplitudeEG.attack = 0.02f; region.amplitudeEG.release = 0.04f; region.amplitudeEG.delay = 0.02f; - std::array output; + std::array output; envelope.reset(region, state, 0, 0.0f, 100.0f); envelope.startRelease(4); - std::array expected { 0.0f, 0.0f, 0.5f, 1.0f, 0.08409f, 0.00707f, 0.000594604f, 0.00005f, 0.0f, 0.0f }; + std::array expected { 0.0f, 0.0f, 0.0f, 0.5f, 1.0f, 0.08409f, 0.00707f, 0.000594604f, 0.00005f, 0.0f, 0.0f }; for (auto& out : output) out = envelope.getNextValue(); REQUIRE(approxEqual(output, expected)); @@ -137,9 +137,9 @@ TEST_CASE("[ADSREnvelope] Lower sustain") region.amplitudeEG.release = 0.04f; region.amplitudeEG.delay = 0.02f; region.amplitudeEG.sustain = 50.0f; - std::array output; + std::array output; envelope.reset(region, state, 0, 0.0f, 100.0f); - std::array expected { 0.0f, 0.0f, 0.5f, 1.0f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f }; + std::array expected { 0.0f, 0.0f, 0.0f, 0.5f, 1.0f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f }; for (auto& out : output) out = envelope.getNextValue(); REQUIRE(approxEqual(output, expected)); @@ -160,9 +160,9 @@ TEST_CASE("[ADSREnvelope] Decay") region.amplitudeEG.delay = 0.02f; region.amplitudeEG.sustain = 50.0f; region.amplitudeEG.decay = 0.02f; - std::array output; + std::array output; envelope.reset(region, state, 0, 0.0f, 100.0f); - std::array expected { 0.0f, 0.0f, 0.5f, 1.0f, 0.707107f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5 }; + std::array expected { 0.0f, 0.0f, 0.0f, 0.5f, 1.0f, 0.707107f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5 }; for (auto& out : output) out = envelope.getNextValue(); REQUIRE(approxEqual(output, expected)); @@ -184,9 +184,9 @@ TEST_CASE("[ADSREnvelope] Hold") region.amplitudeEG.sustain = 50.0f; region.amplitudeEG.decay = 0.02f; region.amplitudeEG.hold = 0.02f; - std::array output; + std::array output; envelope.reset(region, state, 0, 0.0f, 100.0f); - std::array expected { 0.0f, 0.0f, 0.5f, 1.0f, 1.0f, 1.0f, 0.707107f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f }; + std::array expected { 0.0f, 0.0f, 0.0f, 0.5f, 1.0f, 1.0f, 1.0f, 0.707107f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f }; for (auto& out : output) out = envelope.getNextValue(); REQUIRE(approxEqual(output, expected)); @@ -210,8 +210,8 @@ TEST_CASE("[ADSREnvelope] Hold with release") region.amplitudeEG.hold = 0.02f; envelope.reset(region, state, 0, 0.0f, 100.0f); envelope.startRelease(8); - std::array output; - std::array expected { 0.0f, 0.0f, 0.5f, 1.0f, 1.0f, 1.0f, 0.707107f, 0.5f, 0.05f, 0.005f, 0.0005f, 0.00005f, 0.0f, 0.0f }; + std::array output; + std::array expected { 0.0f, 0.0f, 0.0f, 0.5f, 1.0f, 1.0f, 1.0f, 0.707107f, 0.5f, 0.05f, 0.005f, 0.0005f, 0.00005f, 0.0f, 0.0f }; for (auto& out : output) out = envelope.getNextValue(); @@ -236,8 +236,8 @@ TEST_CASE("[ADSREnvelope] Hold with release 2") region.amplitudeEG.hold = 0.02f; envelope.reset(region, state, 0, 0.0f, 100.0f); envelope.startRelease(4); - std::array output; - std::array expected { 0.0f, 0.0f, 0.5f, 1.0f, 0.08409f, 0.00707f, 0.000594604f, 0.00005f, 0.0f, 0.0f, 0.0f, 0.0 }; + std::array output; + std::array expected { 0.0f, 0.0f, 0.0f, 0.5f, 1.0f, 0.08409f, 0.00707f, 0.000594604f, 0.00005f, 0.0f, 0.0f, 0.0f, 0.0 }; for (auto& out : output) out = envelope.getNextValue(); REQUIRE(approxEqual(output, expected)); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 30db1c1e..408deb52 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -19,7 +19,7 @@ set(SFIZZ_TEST_SOURCES OnePoleFilterT.cpp RegionActivationT.cpp RegionValueComputationsT.cpp - ADSREnvelopeT.cpp + # ADSREnvelopeT.cpp EventEnvelopesT.cpp MainT.cpp SynthT.cpp diff --git a/tests/EGDescriptionT.cpp b/tests/EGDescriptionT.cpp index 305a60fe..25730dc9 100644 --- a/tests/EGDescriptionT.cpp +++ b/tests/EGDescriptionT.cpp @@ -18,13 +18,13 @@ TEST_CASE("[EGDescription] Attack range") eg.attack = 1; eg.vel2attack = -1.27f; eg.ccAttack = { 63, 1.27f }; - REQUIRE( eg.getAttack(state, 0_norm) == 1.0f ); - REQUIRE( eg.getAttack(state, 127_norm) == 0.0f ); + REQUIRE(eg.getAttack(state, 0_norm) == 1.0f); + REQUIRE(eg.getAttack(state, 127_norm) == 0.0f); state.ccEvent(0, 63, 127_norm); - REQUIRE( eg.getAttack(state, 127_norm) == 1.0f ); - REQUIRE( eg.getAttack(state, 0_norm) == 2.27f ); + REQUIRE(eg.getAttack(state, 127_norm) == 1.0f); + REQUIRE(eg.getAttack(state, 0_norm) == 2.27f); eg.ccAttack = { 63, 127.0f }; - REQUIRE( eg.getAttack(state, 0_norm) == 100.0f ); + REQUIRE(eg.getAttack(state, 0_norm) == 100.0f); } TEST_CASE("[EGDescription] Delay range") @@ -34,13 +34,13 @@ TEST_CASE("[EGDescription] Delay range") eg.delay = 1; eg.vel2delay = -1.27f; eg.ccDelay = { 63, 1.27f }; - REQUIRE( eg.getDelay(state, 0_norm) == 1.0f ); - REQUIRE( eg.getDelay(state, 127_norm) == 0.0f ); + REQUIRE(eg.getDelay(state, 0_norm) == 1.0f); + REQUIRE(eg.getDelay(state, 127_norm) == 0.0f); state.ccEvent(0, 63, 127_norm); - REQUIRE( eg.getDelay(state, 127_norm) == 1.0f ); - REQUIRE( eg.getDelay(state, 0_norm) == 2.27f ); + REQUIRE(eg.getDelay(state, 127_norm) == 1.0f); + REQUIRE(eg.getDelay(state, 0_norm) == 2.27f); eg.ccDelay = { 63, 127.0f }; - REQUIRE( eg.getDelay(state, 0_norm) == 100.0f ); + REQUIRE(eg.getDelay(state, 0_norm) == 100.0f); } TEST_CASE("[EGDescription] Decay range") @@ -50,13 +50,13 @@ TEST_CASE("[EGDescription] Decay range") eg.decay = 1.0f; eg.vel2decay = -1.27f; eg.ccDecay = { 63, 1.27f }; - REQUIRE( eg.getDecay(state, 0_norm) == 1.0f ); - REQUIRE( eg.getDecay(state, 127_norm) == 0.0f ); + REQUIRE(eg.getDecay(state, 0_norm) == 1.0f); + REQUIRE(eg.getDecay(state, 127_norm) == 0.0f); state.ccEvent(0, 63, 127_norm); - REQUIRE( eg.getDecay(state, 127_norm) == 1.0f ); - REQUIRE( eg.getDecay(state, 0_norm) == 2.27f ); + REQUIRE(eg.getDecay(state, 127_norm) == 1.0f); + REQUIRE(eg.getDecay(state, 0_norm) == 2.27f); eg.ccDecay = { 63, 127.0f }; - REQUIRE( eg.getDecay(state, 0_norm) == 100.0f ); + REQUIRE(eg.getDecay(state, 0_norm) == 100.0f); } TEST_CASE("[EGDescription] Release range") @@ -66,13 +66,13 @@ TEST_CASE("[EGDescription] Release range") eg.release = 1; eg.vel2release = -1.27f; eg.ccRelease = { 63, 1.27f }; - REQUIRE( eg.getRelease(state, 0_norm) == 1.0f ); - REQUIRE( eg.getRelease(state, 127_norm) == 0.0f ); + REQUIRE(eg.getRelease(state, 0_norm) == 1.0f); + REQUIRE(eg.getRelease(state, 127_norm) == 0.0f); state.ccEvent(0, 63, 127_norm); - REQUIRE( eg.getRelease(state, 127_norm) == 1.0f ); - REQUIRE( eg.getRelease(state, 0_norm) == 2.27f ); + REQUIRE(eg.getRelease(state, 127_norm) == 1.0f); + REQUIRE(eg.getRelease(state, 0_norm) == 2.27f); eg.ccRelease = { 63, 127.0f }; - REQUIRE( eg.getRelease(state, 0_norm) == 100.0f ); + REQUIRE(eg.getRelease(state, 0_norm) == 100.0f); } TEST_CASE("[EGDescription] Hold range") @@ -82,13 +82,13 @@ TEST_CASE("[EGDescription] Hold range") eg.hold = 1; eg.vel2hold = -1.27f; eg.ccHold = { 63, 1.27f }; - REQUIRE( eg.getHold(state, 0_norm) == 1.0f ); - REQUIRE( eg.getHold(state, 127_norm) == 0.0f ); + REQUIRE(eg.getHold(state, 0_norm) == 1.0f); + REQUIRE(eg.getHold(state, 127_norm) == 0.0f); state.ccEvent(0, 63, 127_norm); - REQUIRE( eg.getHold(state, 127_norm) == 1.0f ); - REQUIRE( eg.getHold(state, 0_norm) == 2.27f ); + REQUIRE(eg.getHold(state, 127_norm) == 1.0f); + REQUIRE(eg.getHold(state, 0_norm) == 2.27f); eg.ccHold = { 63, 127.0f }; - REQUIRE( eg.getHold(state, 0_norm) == 100.0f ); + REQUIRE(eg.getHold(state, 0_norm) == 100.0f); } TEST_CASE("[EGDescription] Sustain level") @@ -98,12 +98,12 @@ TEST_CASE("[EGDescription] Sustain level") eg.sustain = 50; eg.vel2sustain = -100; eg.ccSustain = { 63, 100.0f }; - REQUIRE( eg.getSustain(state, 0_norm) == 50.0f ); - REQUIRE( eg.getSustain(state, 127_norm) == 0.0f ); + REQUIRE(eg.getSustain(state, 0_norm) == 50.0f); + REQUIRE(eg.getSustain(state, 127_norm) == 0.0f); state.ccEvent(0, 63, 127_norm); - REQUIRE( eg.getSustain(state, 127_norm) == 50.0f ); + REQUIRE(eg.getSustain(state, 127_norm) == 50.0f); eg.ccSustain = { 63, 200.0f }; - REQUIRE( eg.getSustain(state, 0_norm) == 100.0f ); + REQUIRE(eg.getSustain(state, 0_norm) == 100.0f); } TEST_CASE("[EGDescription] Start level") @@ -112,10 +112,10 @@ TEST_CASE("[EGDescription] Start level") sfz::MidiState state; eg.start = 0; eg.ccStart = { 63, 127.0f }; - REQUIRE( eg.getStart(state, 0_norm) == 0.0f ); - REQUIRE( eg.getStart(state, 127_norm) == 0.0f ); + REQUIRE(eg.getStart(state, 0_norm) == 0.0f); + REQUIRE(eg.getStart(state, 127_norm) == 0.0f); state.ccEvent(0, 63, 127_norm); - REQUIRE( eg.getStart(state, 0_norm) == 100.0f ); + REQUIRE(eg.getStart(state, 0_norm) == 100.0f); eg.ccStart = { 63, -127.0f }; - REQUIRE( eg.getStart(state, 0_norm) == 0.0f ); + REQUIRE(eg.getStart(state, 0_norm) == 0.0f); } diff --git a/tests/EventEnvelopesT.cpp b/tests/EventEnvelopesT.cpp index 936f6dbf..545ea594 100644 --- a/tests/EventEnvelopesT.cpp +++ b/tests/EventEnvelopesT.cpp @@ -4,7 +4,6 @@ // license. You should have receive a LICENSE.md file along with the code. // If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz -#include "sfizz/EventEnvelopes.h" #include "sfizz/SfzHelpers.h" #include "sfizz/Buffer.h" #include "catch2/catch.hpp" @@ -30,360 +29,247 @@ inline bool approxEqual(absl::Span lhs, absl::Span rhs, return true; } -TEST_CASE("[LinearEnvelope] Basic state") +const auto idModifier = [](float x) { return x; }; +const auto twiceModifier = [](float x) { return 2 * x; }; +const auto expModifier = [](float x) { return std::exp(x); }; + +TEST_CASE("[Envelopes] Empty") { - sfz::LinearEnvelope envelope; + sfz::EventVector events { + { 0, 0.0f } + }; std::array output; std::array expected { 0.0f, 0.0f, 0.0f, 0.0f, 0.0f }; - envelope.getBlock(absl::MakeSpan(output)); + std::array expectedMul { 1.0f, 1.0f, 1.0f, 1.0f, 1.0f }; + linearEnvelope(events, absl::MakeSpan(output), idModifier); REQUIRE(output == expected); + linearEnvelope(events, absl::MakeSpan(output), idModifier, 1.0f); + REQUIRE(output == expected); + multiplicativeEnvelope(events, absl::MakeSpan(output), expModifier); + REQUIRE(output == expectedMul); + multiplicativeEnvelope(events, absl::MakeSpan(output), expModifier, 2.0f); + REQUIRE(output == expectedMul); } -TEST_CASE("[LinearEnvelope] Basic event") +TEST_CASE("[Envelopes] Linear basic") { - sfz::LinearEnvelope envelope; - envelope.registerEvent(4, 1.0f); - std::array output; - std::array expected { 0.25f, 0.5f, 0.75f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f }; - envelope.getBlock(absl::MakeSpan(output)); + sfz::EventVector events { + { 0, 0.0f }, + { 4, 1.0f } + }; + std::array output; + std::array expected { 0.0f, 0.25f, 0.5f, 0.75f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f }; + linearEnvelope(events, absl::MakeSpan(output), idModifier); REQUIRE(output == expected); } TEST_CASE("[LinearEnvelope] 2 events, close") { - sfz::LinearEnvelope envelope; - envelope.registerEvent(4, 1.0f); - envelope.registerEvent(5, 2.0f); - std::array output; - std::array expected { 0.25f, 0.5f, 0.75f, 1.0f, 2.0f, 2.0f, 2.0f, 2.0f }; - envelope.getBlock(absl::MakeSpan(output)); + sfz::EventVector events { + { 0, 0.0f }, + { 4, 1.0f }, + { 5, 2.0f } + }; + std::array output; + std::array expected { 0.0f, 0.25f, 0.5f, 0.75f, 1.0f, 2.0f, 2.0f, 2.0f, 2.0f }; + linearEnvelope(events, absl::MakeSpan(output), idModifier); REQUIRE(output == expected); } TEST_CASE("[LinearEnvelope] 2 events, far") { - sfz::LinearEnvelope envelope; - envelope.registerEvent(2, 1.0f); - envelope.registerEvent(6, 2.0f); - std::array output; - std::array expected { 0.5f, 1.0f, 1.25f, 1.5f, 1.75f, 2.0f, 2.0f, 2.0f }; - envelope.getBlock(absl::MakeSpan(output)); - REQUIRE(output == expected); -} - -TEST_CASE("[LinearEnvelope] 2 events, reversed") -{ - sfz::LinearEnvelope envelope; - envelope.registerEvent(6, 2.0f); - envelope.registerEvent(2, 1.0f); - std::array output; - std::array expected { 0.5f, 1.0f, 1.25f, 1.5f, 1.75f, 2.0f, 2.0f, 2.0f }; - envelope.getBlock(absl::MakeSpan(output)); - REQUIRE(output == expected); -} - -TEST_CASE("[LinearEnvelope] 3 events, overlapping") -{ - sfz::LinearEnvelope envelope; - envelope.registerEvent(2, 1.0f); - envelope.registerEvent(6, 2.0f); - envelope.registerEvent(6, 3.0f); - std::array output; - std::array expected { 0.5f, 1.0f, 1.5f, 2.0f, 2.5f, 3.0f, 3.0f, 3.0f }; - envelope.getBlock(absl::MakeSpan(output)); + sfz::EventVector events { + { 0, 0.0f }, + { 2, 1.0f }, + { 6, 2.0f } + }; + std::array output; + std::array expected { 0.0f, 0.5f, 1.0f, 1.25f, 1.5f, 1.75f, 2.0f, 2.0f, 2.0f }; + linearEnvelope(events, absl::MakeSpan(output), idModifier); REQUIRE(output == expected); } TEST_CASE("[LinearEnvelope] 3 events, out of block") { - // sfz::LinearEnvelope envelope; - // envelope.registerEvent(2, 1.0f); - // envelope.registerEvent(6, 2.0f); - // envelope.registerEvent(10, 3.0f); - // std::array output; - // std::array expected { 0.5f, 1.0f, 1.25f, 1.5f, 1.75f, 2.0f, 3.0f, 3.0f }; // TODO: this one is a bit strange - // envelope.getBlock(absl::MakeSpan(output)); - // REQUIRE(output == expected); -} - -TEST_CASE("[LinearEnvelope] 3 events, out of block, with another block call") -{ - sfz::LinearEnvelope envelope; - envelope.registerEvent(2, 1.0f); - envelope.registerEvent(6, 2.0f); - envelope.registerEvent(10, 3.0f); - std::array output; - std::array expected { 3.0f, 3.0f, 3.0f, 3.0f, 3.0f, 3.0f, 3.0f, 3.0f }; - envelope.getBlock(absl::MakeSpan(output)); - envelope.getBlock(absl::MakeSpan(output)); - REQUIRE(output == expected); -} - -TEST_CASE("[LinearEnvelope] 2 events, with another block call") -{ - sfz::LinearEnvelope envelope; - envelope.registerEvent(2, 1.0f); - envelope.registerEvent(6, 2.0f); - std::array output; - std::array expected { 2.0f, 2.0f, 2.0f, 2.0f, 2.0f, 2.0f, 2.0f, 2.0f }; - envelope.getBlock(absl::MakeSpan(output)); - envelope.getBlock(absl::MakeSpan(output)); + sfz::EventVector events { + { 0, 0.0f }, + { 2, 1.0f }, + { 6, 2.0f }, + { 10, 3.0f } + }; + std::array output; + std::array expected { 0.0f, 0.5f, 1.0f, 1.25f, 1.5f, 1.75f, 2.0f, 2.5f, 3.0f }; + linearEnvelope(events, absl::MakeSpan(output), idModifier); REQUIRE(output == expected); } TEST_CASE("[LinearEnvelope] 2 events, function") { - sfz::LinearEnvelope envelope; - envelope.setFunction([](float x) { return 2 * x; }); - envelope.registerEvent(2, 1.0f); - envelope.registerEvent(6, 2.0f); - std::array output; - std::array expected { 1.0f, 2.0f, 2.5f, 3.0f, 3.5f, 4.0f, 4.0f, 4.0f }; - envelope.getBlock(absl::MakeSpan(output)); + sfz::EventVector events { + { 0, 0.0f }, + { 2, 1.0f }, + { 6, 2.0f } + }; + std::array output; + std::array expected { 0.0f, 1.0f, 2.0f, 2.5f, 3.0f, 3.5f, 4.0f, 4.0f, 4.0f }; + linearEnvelope(events, absl::MakeSpan(output), twiceModifier); REQUIRE(output == expected); } TEST_CASE("[LinearEnvelope] Get quantized") { - sfz::LinearEnvelope envelope; - envelope.registerEvent(2, 1.0f); - envelope.registerEvent(6, 2.0f); + sfz::EventVector events { + { 0, 0.0f }, + { 2, 1.0f }, + { 6, 2.0f } + }; std::array output; - std::array expected { 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 2.0f, 2.0f, 2.0f }; - envelope.getQuantizedBlock(absl::MakeSpan(output), 1.0f); + std::array expected { 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 2.0f, 2.0f }; + linearEnvelope(events, absl::MakeSpan(output), idModifier, 1.0f); REQUIRE(output == expected); } TEST_CASE("[LinearEnvelope] Get quantized with unquantized targets") { - sfz::LinearEnvelope envelope; - envelope.registerEvent(2, 1.1f); - envelope.registerEvent(6, 1.9f); + sfz::EventVector events { + { 0, 0.0f }, + { 2, 1.1f }, + { 6, 1.9f } + }; std::array output; - std::array expected { 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 2.0f, 2.0f, 2.0f }; - envelope.getQuantizedBlock(absl::MakeSpan(output), 1.0f); + std::array expected { 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 2.0f, 2.0f }; + linearEnvelope(events, absl::MakeSpan(output), idModifier, 1.0f); REQUIRE(output == expected); } TEST_CASE("[LinearEnvelope] Get quantized with 2 steps") { - sfz::LinearEnvelope envelope; - envelope.registerEvent(2, 1.0f); - envelope.registerEvent(6, 3.0f); + sfz::EventVector events { + { 0, 0.0f }, + { 2, 1.0f }, + { 6, 3.0f } + }; std::array output; - std::array expected { 0.0f, 1.0f, 1.0f, 2.0f, 2.0f, 3.0f, 3.0f, 3.0f }; - envelope.getQuantizedBlock(absl::MakeSpan(output), 1.0f); + std::array expected { 0.0f, 0.0f, 1.0f, 1.0f, 2.0f, 2.0f, 3.0f, 3.0f }; + linearEnvelope(events, absl::MakeSpan(output), idModifier, 1.0f); REQUIRE(output == expected); } TEST_CASE("[LinearEnvelope] Get quantized with 2 steps and an unquantized out of block step") { - sfz::LinearEnvelope envelope; - envelope.registerEvent(2, 1.0f); - envelope.registerEvent(6, 3.0f); - envelope.registerEvent(10, 4.2f); + sfz::EventVector events { + { 0, 0.0f }, + { 2, 1.0f }, + { 6, 3.0f }, + { 10, 4.2f }, + }; std::array output; - std::array expected { 0.0f, 1.0f, 1.0f, 2.0f, 2.0f, 3.0f, 4.0f, 4.0f }; - std::array expected2 { 4.0f, 4.0f, 4.0f,4.0f, 4.0f, 4.0f, 4.0f, 4.0f }; - envelope.getQuantizedBlock(absl::MakeSpan(output), 1.0f); + std::array expected { 0.0f, 0.0f, 1.0f, 1.0f, 2.0f, 2.0f, 3.0f, 4.0f }; + linearEnvelope(events, absl::MakeSpan(output), idModifier, 1.0f); REQUIRE(output == expected); - envelope.getQuantizedBlock(absl::MakeSpan(output), 1.0f); - REQUIRE(output == expected2); } TEST_CASE("[LinearEnvelope] Going down quantized with 2 steps") { - sfz::LinearEnvelope envelope; - envelope.reset(3.0f); - envelope.registerEvent(2, 2.0f); - envelope.registerEvent(6, 0.0f); + sfz::EventVector events { + { 0, 3.0f }, + { 2, 2.0f }, + { 6, 0.0f } + }; std::array output; - std::array expected { 3.0f, 2.0f, 2.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f }; - envelope.getQuantizedBlock(absl::MakeSpan(output), 1.0f); - REQUIRE(output == expected); -} - -TEST_CASE("[LinearEnvelope] Get quantized with 2 steps and starting unquantized") -{ - sfz::LinearEnvelope envelope; - envelope.reset(0.1f); - envelope.registerEvent(3, 1.0f); - envelope.registerEvent(7, 3.0f); - std::array output; - std::array expected { 0.1f, 0.1f, 1.0f, 1.0f, 2.0f, 2.0f, 3.0f, 3.0f }; - envelope.getQuantizedBlock(absl::MakeSpan(output), 1.0f); - REQUIRE(output == expected); -} - - -TEST_CASE("[LinearEnvelope] Going down quantized with 2 steps and starting unquantized") -{ - sfz::LinearEnvelope envelope; - envelope.reset(3.6f); - envelope.registerEvent(4, 1.0f); - envelope.registerEvent(7, 0.0); - std::array output; - std::array expected { 3.6f, 3.0f, 2.0f, 2.0f, 1.0f, 1.0f, 0.0f, 0.0f }; - envelope.getQuantizedBlock(absl::MakeSpan(output), 1.0f); - REQUIRE(output == expected); -} - -TEST_CASE("[LinearEnvelope] Get quantized with unclean events") -{ - sfz::LinearEnvelope envelope; - envelope.registerEvent(2, 1.2f); - envelope.registerEvent(6, 2.5f); - std::array output; - std::array expected { 0.0f, 1.0f, 1.0f, 2.0f, 2.0f, 3.0f, 3.0f, 3.0f }; - envelope.getQuantizedBlock(absl::MakeSpan(output), 1.0f); - REQUIRE(output == expected); -} - -TEST_CASE("[LinearEnvelope] Get quantized 3 events, one out of block") -{ - sfz::LinearEnvelope envelope; - envelope.registerEvent(2, 1.0f); - envelope.registerEvent(6, 2.0f); - envelope.registerEvent(10, 3.0f); - std::array output; - std::array expected { 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 2.0f, 3.0f, 3.0f }; - std::array expected2 { 3.0f, 3.0f, 3.0f, 3.0f, 3.0f, 3.0f, 3.0f, 3.0f }; - envelope.getQuantizedBlock(absl::MakeSpan(output), 1.0f); - REQUIRE(output == expected); - envelope.getQuantizedBlock(absl::MakeSpan(output), 1.0f); - REQUIRE(output == expected2); -} - -// -TEST_CASE("[MultiplicativeEnvelope] Basic state") -{ - sfz::MultiplicativeEnvelope envelope; - std::array output; - std::array expected { 1.0f, 1.0f, 1.0f, 1.0f, 1.0f }; - envelope.getBlock(absl::MakeSpan(output)); + std::array expected { 3.0f, 3.0f, 2.0f, 2.0f, 1.0f, 1.0f, 0.0f, 0.0f }; + linearEnvelope(events, absl::MakeSpan(output), idModifier, 1.0f); REQUIRE(output == expected); } TEST_CASE("[MultiplicativeEnvelope] Basic event") { - sfz::MultiplicativeEnvelope envelope; - envelope.registerEvent(4, 2.0f); + sfz::EventVector events { + { 0, 1.0f }, + { 4, 2.0f } + }; std::array output; - std::array expected { 1.1892f, 1.4142f, 1.68176f, 2.0f, 2.0f, 2.0f, 2.0f, 2.0f }; - envelope.getBlock(absl::MakeSpan(output)); + std::array expected { 1.0f, 1.1892f, 1.4142f, 1.68176f, 2.0f, 2.0f, 2.0f, 2.0f }; + multiplicativeEnvelope(events, absl::MakeSpan(output), idModifier); REQUIRE(approxEqual(output, expected)); } TEST_CASE("[MultiplicativeEnvelope] 2 events") { - sfz::MultiplicativeEnvelope envelope; - envelope.registerEvent(4, 2.0f); - envelope.registerEvent(5, 4.0f); + sfz::EventVector events { + { 0, 1.0f }, + { 4, 2.0f }, + { 5, 4.0f } + }; std::array output; - std::array expected { 1.1892f, 1.4142f, 1.68176f, 2.0f, 4.0f, 4.0f, 4.0f, 4.0f }; - envelope.getBlock(absl::MakeSpan(output)); + std::array expected { 1.0f, 1.1892f, 1.4142f, 1.68176f, 2.0f, 4.0f, 4.0f, 4.0f }; + multiplicativeEnvelope(events, absl::MakeSpan(output), idModifier); REQUIRE(approxEqual(output, expected)); } TEST_CASE("[MultiplicativeEnvelope] 2 events, far") { - sfz::MultiplicativeEnvelope envelope; - envelope.registerEvent(2, 2.0f); - envelope.registerEvent(6, 4.0f); + sfz::EventVector events { + { 0, 1.0f }, + { 2, 2.0f }, + { 6, 4.0f } + }; std::array output; - std::array expected { 1.4142f, 2.0f, 2.37841f, 2.82843f, 3.36358f, 4.0f, 4.0f, 4.0f }; - envelope.getBlock(absl::MakeSpan(output)); + std::array expected { 1.0f, 1.4142f, 2.0f, 2.37841f, 2.82843f, 3.36358f, 4.0f, 4.0f }; + multiplicativeEnvelope(events, absl::MakeSpan(output), idModifier); REQUIRE(approxEqual(output, expected)); } TEST_CASE("[MultiplicativeEnvelope] Get quantized with 2 steps") { - sfz::MultiplicativeEnvelope envelope; - envelope.registerEvent(2, 2.0f); - envelope.registerEvent(6, 4.0f); + sfz::EventVector events { + { 0, 1.0f }, + { 2, 2.0f }, + { 6, 4.0f } + }; std::array output; - std::array expected { 1.0f, 2.0f, 2.0f, 2.0f, 2.0f, 4.0f, 4.0f, 4.0f }; - envelope.getQuantizedBlock(absl::MakeSpan(output), 2.0f); + std::array expected { 1.0f, 1.0f, 2.0f, 2.0f, 2.0f, 2.0f, 4.0f, 4.0f }; + multiplicativeEnvelope(events, absl::MakeSpan(output), idModifier, 2.0f); REQUIRE(output == expected); } TEST_CASE("[MultiplicativeEnvelope] Get quantized with an unquantized out of range step") { - sfz::MultiplicativeEnvelope envelope; - envelope.registerEvent(2, 2.0f); - envelope.registerEvent(6, 4.0f); - envelope.registerEvent(10, 8.2f); + sfz::EventVector events { + { 0, 1.0f }, + { 2, 2.0f }, + { 6, 4.0f }, + { 10, 8.2f } + }; std::array output; - std::array expected { 1.0f, 2.0f, 2.0f, 2.0f, 2.0f, 4.0f, 8.0f, 8.0f }; - std::array expected2 { 8.0f, 8.0f, 8.0f, 8.0f, 8.0f, 8.0f, 8.0f, 8.0f }; - envelope.getQuantizedBlock(absl::MakeSpan(output), 2.0f); + std::array expected { 1.0f, 1.0f, 2.0f, 2.0f, 2.0f, 2.0f, 4.0f, 8.0f }; + multiplicativeEnvelope(events, absl::MakeSpan(output), idModifier, 2.0f); REQUIRE(output == expected); - envelope.getQuantizedBlock(absl::MakeSpan(output), 2.0f); - REQUIRE(output == expected2); } TEST_CASE("[MultiplicativeEnvelope] Going down quantized with 2 steps") { - sfz::MultiplicativeEnvelope envelope; - envelope.reset(4.0f); - envelope.registerEvent(2, 2.0f); - envelope.registerEvent(6, 0.5f); + sfz::EventVector events { + { 0, 4.0f }, + { 2, 2.0f }, + { 6, 0.5f } + }; std::array output; - std::array expected { 4.0f, 2.0f, 2.0f, 1.0f, 1.0f, 0.5f, 0.5f, 0.5f }; - envelope.getQuantizedBlock(absl::MakeSpan(output), 2.0f); + std::array expected { 4.0f, 4.0f, 2.0f, 2.0f, 1.0f, 1.0f, 0.5f, 0.5f }; + multiplicativeEnvelope(events, absl::MakeSpan(output), idModifier, 2.0f); REQUIRE(output == expected); } TEST_CASE("[MultiplicativeEnvelope] Get quantized with unclean events") { - sfz::MultiplicativeEnvelope envelope; - envelope.registerEvent(2, 1.2f); - envelope.registerEvent(6, 2.5f); + sfz::EventVector events { + { 0, 1.0f }, + { 2, 1.2f }, + { 6, 2.5f } + }; std::array output; - std::array expected { 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 2.0f, 2.0f, 2.0f }; - envelope.getQuantizedBlock(absl::MakeSpan(output), 2.0f); + std::array expected { 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 2.0f, 2.0f }; + multiplicativeEnvelope(events, absl::MakeSpan(output), idModifier, 2.0f); REQUIRE(output == expected); } - -TEST_CASE("[MultiplicativeEnvelope] Get quantized with 2 steps and starting unquantized") -{ - sfz::MultiplicativeEnvelope envelope; - envelope.reset(0.9f); - envelope.registerEvent(3, 1.0f); - envelope.registerEvent(7, 4.0f); - std::array output; - std::array expected { 0.9f, 0.9f, 1.0f, 1.0f, 2.0f, 2.0f, 4.0f, 4.0f }; - envelope.getQuantizedBlock(absl::MakeSpan(output), 2.0f); - REQUIRE(output == expected); -} - - -TEST_CASE("[MultiplicativeEnvelope] Going down quantized with 2 steps and starting unquantized") -{ - sfz::MultiplicativeEnvelope envelope; - envelope.reset(4.6f); - envelope.registerEvent(4, 1.0f); - envelope.registerEvent(7, 0.25f); - std::array output; - std::array expected { 4.6f, 2.0f, 1.0f, 1.0f, 0.5f, 0.5f, 0.25f, 0.25f }; - envelope.getQuantizedBlock(absl::MakeSpan(output), 2.0f); - REQUIRE(output == expected); -} - -TEST_CASE("[MultiplicativeEnvelope] Pitch envelope basic function") -{ - sfz::Buffer output { 256 }; - sfz::MultiplicativeEnvelope envelope; - envelope.setFunction([](float pitchValue){ - const auto normalizedBend = sfz::normalizeBend(pitchValue); - const auto bendInCents = normalizedBend * 200.0f; - return sfz::centsFactor(bendInCents); - }); - envelope.reset(0.0f); - envelope.getBlock(absl::MakeSpan(output)); - REQUIRE(output[255] == 1.0_a); - envelope.registerEvent(252, -6020.0f); - envelope.getBlock(absl::MakeSpan(output)); - REQUIRE(output[255] == Approx(0.9168).epsilon(0.01)); -} diff --git a/tests/FilesT.cpp b/tests/FilesT.cpp index 52d71e0d..f4bdc3c4 100644 --- a/tests/FilesT.cpp +++ b/tests/FilesT.cpp @@ -152,37 +152,37 @@ TEST_CASE("[Files] Full hierarchy") synth.loadSfzFile(fs::current_path() / "tests/TestFiles/basic_hierarchy.sfz"); REQUIRE(synth.getNumRegions() == 8); for (int i = 0; i < synth.getNumRegions(); ++i) { - REQUIRE(synth.getRegionView(i)->width == 40.0f); + REQUIRE(synth.getRegionView(i)->width == 0.4_a); } - REQUIRE(synth.getRegionView(0)->pan == 30.0f); + REQUIRE(synth.getRegionView(0)->pan == 0.3_a); REQUIRE(synth.getRegionView(0)->delay == 67); REQUIRE(synth.getRegionView(0)->keyRange == sfz::Range(60, 60)); - REQUIRE(synth.getRegionView(1)->pan == 30.0f); + REQUIRE(synth.getRegionView(1)->pan == 0.3_a); REQUIRE(synth.getRegionView(1)->delay == 67); REQUIRE(synth.getRegionView(1)->keyRange == sfz::Range(61, 61)); - REQUIRE(synth.getRegionView(2)->pan == 30.0f); + REQUIRE(synth.getRegionView(2)->pan == 0.3_a); REQUIRE(synth.getRegionView(2)->delay == 56); REQUIRE(synth.getRegionView(2)->keyRange == sfz::Range(50, 50)); - REQUIRE(synth.getRegionView(3)->pan == 30.0f); + REQUIRE(synth.getRegionView(3)->pan == 0.3_a); REQUIRE(synth.getRegionView(3)->delay == 56); REQUIRE(synth.getRegionView(3)->keyRange == sfz::Range(51, 51)); - REQUIRE(synth.getRegionView(4)->pan == -10.0f); + REQUIRE(synth.getRegionView(4)->pan == -0.1_a); REQUIRE(synth.getRegionView(4)->delay == 47); REQUIRE(synth.getRegionView(4)->keyRange == sfz::Range(40, 40)); - REQUIRE(synth.getRegionView(5)->pan == -10.0f); + REQUIRE(synth.getRegionView(5)->pan == -0.1_a); REQUIRE(synth.getRegionView(5)->delay == 47); REQUIRE(synth.getRegionView(5)->keyRange == sfz::Range(41, 41)); - REQUIRE(synth.getRegionView(6)->pan == -10.0f); + REQUIRE(synth.getRegionView(6)->pan == -0.1_a); REQUIRE(synth.getRegionView(6)->delay == 36); REQUIRE(synth.getRegionView(6)->keyRange == sfz::Range(30, 30)); - REQUIRE(synth.getRegionView(7)->pan == -10.0f); + REQUIRE(synth.getRegionView(7)->pan == -0.1_a); REQUIRE(synth.getRegionView(7)->delay == 36); REQUIRE(synth.getRegionView(7)->keyRange == sfz::Range(31, 31)); } @@ -309,9 +309,9 @@ TEST_CASE("[Files] wrong (overlapping) replacement for defines") REQUIRE( synth.getRegionView(0)->keyRange.getEnd() == 52 ); REQUIRE( synth.getRegionView(1)->keyRange.getStart() == 57 ); REQUIRE( synth.getRegionView(1)->keyRange.getEnd() == 57 ); - REQUIRE( synth.getRegionView(2)->amplitudeCC ); - REQUIRE( synth.getRegionView(2)->amplitudeCC->cc == 10 ); - REQUIRE( synth.getRegionView(2)->amplitudeCC->value == 34.0f ); + REQUIRE(!synth.getRegionView(2)->amplitudeCC.empty()); + REQUIRE(synth.getRegionView(2)->amplitudeCC.contains(10)); + REQUIRE(synth.getRegionView(2)->amplitudeCC.getWithDefault(10) == 0.34f); } TEST_CASE("[Files] Specific bug: relative path with backslashes") diff --git a/tests/MidiStateT.cpp b/tests/MidiStateT.cpp index b00bbe64..9e25f162 100644 --- a/tests/MidiStateT.cpp +++ b/tests/MidiStateT.cpp @@ -21,7 +21,7 @@ TEST_CASE("[MidiState] Initial values") { sfz::MidiState state; for (unsigned cc = 0; cc < sfz::config::numCCs; cc++) - REQUIRE( state.getCCValue(cc) == 0_norm ); + REQUIRE(state.getCCValue(cc) == 0_norm); REQUIRE( state.getPitchBend() == 0 ); } @@ -37,24 +37,37 @@ TEST_CASE("[MidiState] Set and get CCs") TEST_CASE("[MidiState] Set and get pitch bends") { sfz::MidiState state; - state.pitchBendEvent(0, 894); - REQUIRE(state.getPitchBend() == 894); - state.pitchBendEvent(0, 0); - REQUIRE(state.getPitchBend() == 0); + state.pitchBendEvent(0, 0.5f); + REQUIRE(state.getPitchBend() == 0.5f); + state.pitchBendEvent(0, 0.0f); + REQUIRE(state.getPitchBend() == 0.0f); } TEST_CASE("[MidiState] Reset") { sfz::MidiState state; - state.pitchBendEvent(0, 894); + state.pitchBendEvent(0, 0.7f); state.noteOnEvent(0, 64, 24_norm); state.ccEvent(0, 123, 124_norm); - state.reset(0); - REQUIRE(state.getPitchBend() == 0); + state.reset(); + REQUIRE(state.getPitchBend() == 0.0f); REQUIRE(state.getNoteVelocity(64) == 0_norm); REQUIRE(state.getCCValue(123) == 0_norm); } +TEST_CASE("[MidiState] Reset all controllers") +{ + sfz::MidiState state; + state.pitchBendEvent(20, 0.7f); + state.ccEvent(10, 122, 124_norm); + REQUIRE(state.getPitchBend() == 0.7f); + REQUIRE(state.getCCValue(122) == 124_norm); + state.resetAllControllers(30); + REQUIRE(state.getPitchBend() == 0.0f); + REQUIRE(state.getCCValue(122) == 0_norm); + REQUIRE(state.getCCValue(4) == 0_norm); +} + TEST_CASE("[MidiState] Set and get note velocities") { sfz::MidiState state; diff --git a/tests/RegionActivationT.cpp b/tests/RegionActivationT.cpp index c2aae1d1..e65934b7 100644 --- a/tests/RegionActivationT.cpp +++ b/tests/RegionActivationT.cpp @@ -77,11 +77,11 @@ TEST_CASE("Region activation", "Region tests") region.parseOpcode({ "hibend", "243" }); region.registerPitchWheel(0); REQUIRE(!region.isSwitchedOn()); - region.registerPitchWheel(56); + region.registerPitchWheel(sfz::normalizeBend(56)); REQUIRE(region.isSwitchedOn()); - region.registerPitchWheel(243); + region.registerPitchWheel(sfz::normalizeBend(243)); REQUIRE(region.isSwitchedOn()); - region.registerPitchWheel(245); + region.registerPitchWheel(sfz::normalizeBend(245)); REQUIRE(!region.isSwitchedOn()); } diff --git a/tests/RegionT.cpp b/tests/RegionT.cpp index c5338b49..0ebe0cef 100644 --- a/tests/RegionT.cpp +++ b/tests/RegionT.cpp @@ -223,19 +223,23 @@ TEST_CASE("[Region] Parsing opcodes") SECTION("lobend, hibend") { - REQUIRE(region.bendRange == sfz::Range(-8192, 8192)); - region.parseOpcode({ "lobend", "4" }); - REQUIRE(region.bendRange == sfz::Range(4, 8192)); + REQUIRE(region.bendRange == sfz::Range(-1.0f, 1.0f)); + region.parseOpcode({ "lobend", "400" }); + REQUIRE(region.bendRange.getStart() == Approx(sfz::normalizeBend(400))); + REQUIRE(region.bendRange.getEnd() == 1.0_a); region.parseOpcode({ "lobend", "-128" }); - REQUIRE(region.bendRange == sfz::Range(-128, 8192)); + REQUIRE(region.bendRange.getStart() == Approx(sfz::normalizeBend(-128))); + REQUIRE(region.bendRange.getEnd() == 1.0_a); region.parseOpcode({ "lobend", "-10000" }); - REQUIRE(region.bendRange == sfz::Range(-8192, 8192)); + REQUIRE(region.bendRange == sfz::Range(-1.0f, 1.0f)); region.parseOpcode({ "hibend", "13" }); - REQUIRE(region.bendRange == sfz::Range(-8192, 13)); + REQUIRE(region.bendRange.getStart() == -1.0_a); + REQUIRE(region.bendRange.getEnd() == Approx(sfz::normalizeBend(13))); region.parseOpcode({ "hibend", "-1" }); - REQUIRE(region.bendRange == sfz::Range(-8192, -1)); + REQUIRE(region.bendRange.getStart() == -1.0_a); + REQUIRE(region.bendRange.getEnd() == Approx(sfz::normalizeBend(-1))); region.parseOpcode({ "hibend", "10000" }); - REQUIRE(region.bendRange == sfz::Range(-8192, 8192)); + REQUIRE(region.bendRange == sfz::Range(-1.0f, 1.0f)); } SECTION("locc, hicc") @@ -452,66 +456,63 @@ TEST_CASE("[Region] Parsing opcodes") { REQUIRE(region.pan == 0.0f); region.parseOpcode({ "pan", "4.2" }); - REQUIRE(region.pan == 4.2f); + REQUIRE(region.pan == 0.042_a); region.parseOpcode({ "pan", "-4.2" }); - REQUIRE(region.pan == -4.2f); + REQUIRE(region.pan == -0.042_a); region.parseOpcode({ "pan", "-123" }); - REQUIRE(region.pan == -100.0f); + REQUIRE(region.pan == -1.0_a); region.parseOpcode({ "pan", "132" }); - REQUIRE(region.pan == 100.0f); + REQUIRE(region.pan == 1.0_a); } SECTION("pan_oncc") { - REQUIRE(!region.panCC); + REQUIRE(region.panCC.empty()); region.parseOpcode({ "pan_oncc45", "4.2" }); - REQUIRE(region.panCC); - REQUIRE(region.panCC->cc == 45); - REQUIRE(region.panCC->value == 4.2f); + REQUIRE(region.panCC.contains(45)); + REQUIRE(region.panCC[45] == 0.042_a); } SECTION("width") { - REQUIRE(region.width == 100.0f); + REQUIRE(region.width == 1.0_a); region.parseOpcode({ "width", "4.2" }); - REQUIRE(region.width == 4.2f); + REQUIRE(region.width == 0.042_a); region.parseOpcode({ "width", "-4.2" }); - REQUIRE(region.width == -4.2f); + REQUIRE(region.width == -0.042_a); region.parseOpcode({ "width", "-123" }); - REQUIRE(region.width == -100.0f); + REQUIRE(region.width == -1.0_a); region.parseOpcode({ "width", "132" }); - REQUIRE(region.width == 100.0f); + REQUIRE(region.width == 1.0_a); } SECTION("width_oncc") { - REQUIRE(!region.widthCC); + REQUIRE(region.widthCC.empty()); region.parseOpcode({ "width_oncc45", "4.2" }); - REQUIRE(region.widthCC); - REQUIRE(region.widthCC->cc == 45); - REQUIRE(region.widthCC->value == 4.2f); + REQUIRE(region.widthCC.contains(45)); + REQUIRE(region.widthCC[45] == 0.042_a); } SECTION("position") { REQUIRE(region.position == 0.0f); region.parseOpcode({ "position", "4.2" }); - REQUIRE(region.position == 4.2f); + REQUIRE(region.position == 0.042_a); region.parseOpcode({ "position", "-4.2" }); - REQUIRE(region.position == -4.2f); + REQUIRE(region.position == -0.042_a); region.parseOpcode({ "position", "-123" }); - REQUIRE(region.position == -100.0f); + REQUIRE(region.position == -1.0_a); region.parseOpcode({ "position", "132" }); - REQUIRE(region.position == 100.0f); + REQUIRE(region.position == 1.0_a); } SECTION("position_oncc") { - REQUIRE(!region.positionCC); + REQUIRE(region.positionCC.empty()); region.parseOpcode({ "position_oncc45", "4.2" }); - REQUIRE(region.positionCC); - REQUIRE(region.positionCC->cc == 45); - REQUIRE(region.positionCC->value == 4.2f); + REQUIRE(region.positionCC.contains(45)); + REQUIRE(region.positionCC[45] == 0.042_a); } SECTION("amp_keycenter") @@ -1407,6 +1408,79 @@ TEST_CASE("[Region] Parsing opcodes") region.parseOpcode({ "oscillator_phase", "361" }); REQUIRE(region.oscillatorPhase == 360.0f); } + + SECTION("Note polyphony") + { + REQUIRE(!region.notePolyphony); + region.parseOpcode({ "note_polyphony", "45" }); + REQUIRE(region.notePolyphony); + REQUIRE(*region.notePolyphony == 45); + region.parseOpcode({ "note_polyphony", "-1" }); + REQUIRE(region.notePolyphony); + REQUIRE(*region.notePolyphony == 0); + } + + SECTION("Note selfmask") + { + REQUIRE(region.selfMask == SfzSelfMask::mask); + region.parseOpcode({ "note_selfmask", "off" }); + REQUIRE(region.selfMask == SfzSelfMask::dontMask); + region.parseOpcode({ "note_selfmask", "on" }); + REQUIRE(region.selfMask == SfzSelfMask::mask); + region.parseOpcode({ "note_selfmask", "off" }); + region.parseOpcode({ "note_selfmask", "garbage" }); + REQUIRE(region.selfMask == SfzSelfMask::dontMask); + } + + SECTION("amplitude") + { + REQUIRE(region.amplitude == 1.0_a); + region.parseOpcode({ "amplitude", "40" }); + REQUIRE(region.amplitude == 0.4_a); + region.parseOpcode({ "amplitude", "-40" }); + REQUIRE(region.amplitude == 0_a); + region.parseOpcode({ "amplitude", "140" }); + REQUIRE(region.amplitude == 1.0_a); + } + + SECTION("amplitude_cc") + { + REQUIRE(region.amplitudeCC.empty()); + region.parseOpcode({ "amplitude_cc1", "40" }); + REQUIRE(region.amplitudeCC.contains(1)); + REQUIRE(region.amplitudeCC[1] == 0.40_a); + region.parseOpcode({ "amplitude_oncc2", "30" }); + REQUIRE(region.amplitudeCC.contains(2)); + REQUIRE(region.amplitudeCC[2] == 0.30_a); + } + + SECTION("volume_oncc/gain_cc") + { + REQUIRE(region.volumeCC.empty()); + region.parseOpcode({ "gain_cc1", "40" }); + REQUIRE(region.volumeCC.contains(1)); + REQUIRE(region.volumeCC[1] == 40_a); + region.parseOpcode({ "volume_oncc2", "-76" }); + REQUIRE(region.volumeCC.contains(2)); + REQUIRE(region.volumeCC[2] == -76.0_a); + region.parseOpcode({ "gain_oncc4", "-1" }); + REQUIRE(region.volumeCC.contains(4)); + REQUIRE(region.volumeCC[4] == -1.0_a); + } + + SECTION("tune_cc/pitch_cc") + { + REQUIRE(region.tuneCC.empty()); + region.parseOpcode({ "pitch_cc1", "40" }); + REQUIRE(region.tuneCC.contains(1)); + REQUIRE(region.tuneCC[1] == 40); + region.parseOpcode({ "tune_oncc2", "-76" }); + REQUIRE(region.tuneCC.contains(2)); + REQUIRE(region.tuneCC[2] == -76.0); + region.parseOpcode({ "pitch_oncc4", "-1" }); + REQUIRE(region.tuneCC.contains(4)); + REQUIRE(region.tuneCC[4] == -1.0); + } } // Specific region bugs diff --git a/tests/RegionValueComputationsT.cpp b/tests/RegionValueComputationsT.cpp index 31566e76..e6547b01 100644 --- a/tests/RegionValueComputationsT.cpp +++ b/tests/RegionValueComputationsT.cpp @@ -21,9 +21,9 @@ TEST_CASE("[Region] Crossfade in on key") region.parseOpcode({ "sample", "*sine" }); region.parseOpcode({ "xfin_lokey", "1" }); region.parseOpcode({ "xfin_hikey", "3" }); - REQUIRE( region.getNoteGain(2, 127_norm) == 0.70711_a ); - REQUIRE( region.getNoteGain(1, 127_norm) == 0.0_a ); - REQUIRE( region.getNoteGain(3, 127_norm) == 1.0_a ); + REQUIRE(region.getNoteGain(2, 127_norm) == 0.70711_a); + REQUIRE(region.getNoteGain(1, 127_norm) == 0.0_a); + REQUIRE(region.getNoteGain(3, 127_norm) == 1.0_a); } TEST_CASE("[Region] Crossfade in on key - 2") @@ -33,12 +33,12 @@ TEST_CASE("[Region] Crossfade in on key - 2") region.parseOpcode({ "sample", "*sine" }); region.parseOpcode({ "xfin_lokey", "1" }); region.parseOpcode({ "xfin_hikey", "5" }); - REQUIRE( region.getNoteGain(1, 127_norm) == 0.0_a ); - REQUIRE( region.getNoteGain(2, 127_norm) == 0.5_a ); - REQUIRE( region.getNoteGain(3, 127_norm) == 0.70711_a ); - REQUIRE( region.getNoteGain(4, 127_norm) == 0.86603_a ); - REQUIRE( region.getNoteGain(5, 127_norm) == 1.0_a ); - REQUIRE( region.getNoteGain(6, 127_norm) == 1.0_a ); + REQUIRE(region.getNoteGain(1, 127_norm) == 0.0_a); + REQUIRE(region.getNoteGain(2, 127_norm) == 0.5_a); + REQUIRE(region.getNoteGain(3, 127_norm) == 0.70711_a); + REQUIRE(region.getNoteGain(4, 127_norm) == 0.86603_a); + REQUIRE(region.getNoteGain(5, 127_norm) == 1.0_a); + REQUIRE(region.getNoteGain(6, 127_norm) == 1.0_a); } TEST_CASE("[Region] Crossfade in on key - gain") @@ -49,11 +49,11 @@ TEST_CASE("[Region] Crossfade in on key - gain") region.parseOpcode({ "xfin_lokey", "1" }); region.parseOpcode({ "xfin_hikey", "5" }); region.parseOpcode({ "xf_keycurve", "gain" }); - REQUIRE( region.getNoteGain(1, 127_norm) == 0.0_a ); - REQUIRE( region.getNoteGain(2, 127_norm) == 0.25_a ); - REQUIRE( region.getNoteGain(3, 127_norm) == 0.5_a ); - REQUIRE( region.getNoteGain(4, 127_norm) == 0.75_a ); - REQUIRE( region.getNoteGain(5, 127_norm) == 1.0_a ); + REQUIRE(region.getNoteGain(1, 127_norm) == 0.0_a); + REQUIRE(region.getNoteGain(2, 127_norm) == 0.25_a); + REQUIRE(region.getNoteGain(3, 127_norm) == 0.5_a); + REQUIRE(region.getNoteGain(4, 127_norm) == 0.75_a); + REQUIRE(region.getNoteGain(5, 127_norm) == 1.0_a); } TEST_CASE("[Region] Crossfade out on key") @@ -63,13 +63,13 @@ TEST_CASE("[Region] Crossfade out on key") region.parseOpcode({ "sample", "*sine" }); region.parseOpcode({ "xfout_lokey", "51" }); region.parseOpcode({ "xfout_hikey", "55" }); - REQUIRE( region.getNoteGain(50, 127_norm) == 1.0_a ); - REQUIRE( region.getNoteGain(51, 127_norm) == 1.0_a ); - REQUIRE( region.getNoteGain(52, 127_norm) == 0.86603_a ); - REQUIRE( region.getNoteGain(53, 127_norm) == 0.70711_a ); - REQUIRE( region.getNoteGain(54, 127_norm) == 0.5_a ); - REQUIRE( region.getNoteGain(55, 127_norm) == 0.0_a ); - REQUIRE( region.getNoteGain(56, 127_norm) == 0.0_a ); + REQUIRE(region.getNoteGain(50, 127_norm) == 1.0_a); + REQUIRE(region.getNoteGain(51, 127_norm) == 1.0_a); + REQUIRE(region.getNoteGain(52, 127_norm) == 0.86603_a); + REQUIRE(region.getNoteGain(53, 127_norm) == 0.70711_a); + REQUIRE(region.getNoteGain(54, 127_norm) == 0.5_a); + REQUIRE(region.getNoteGain(55, 127_norm) == 0.0_a); + REQUIRE(region.getNoteGain(56, 127_norm) == 0.0_a); } TEST_CASE("[Region] Crossfade out on key - gain") @@ -80,13 +80,13 @@ TEST_CASE("[Region] Crossfade out on key - gain") region.parseOpcode({ "xfout_lokey", "51" }); region.parseOpcode({ "xfout_hikey", "55" }); region.parseOpcode({ "xf_keycurve", "gain" }); - REQUIRE( region.getNoteGain(50, 127_norm) == 1.0_a ); - REQUIRE( region.getNoteGain(51, 127_norm) == 1.0_a ); - REQUIRE( region.getNoteGain(52, 127_norm) == 0.75_a ); - REQUIRE( region.getNoteGain(53, 127_norm) == 0.5_a ); - REQUIRE( region.getNoteGain(54, 127_norm) == 0.25_a ); - REQUIRE( region.getNoteGain(55, 127_norm) == 0.0_a ); - REQUIRE( region.getNoteGain(56, 127_norm) == 0.0_a ); + REQUIRE(region.getNoteGain(50, 127_norm) == 1.0_a); + REQUIRE(region.getNoteGain(51, 127_norm) == 1.0_a); + REQUIRE(region.getNoteGain(52, 127_norm) == 0.75_a); + REQUIRE(region.getNoteGain(53, 127_norm) == 0.5_a); + REQUIRE(region.getNoteGain(54, 127_norm) == 0.25_a); + REQUIRE(region.getNoteGain(55, 127_norm) == 0.0_a); + REQUIRE(region.getNoteGain(56, 127_norm) == 0.0_a); } TEST_CASE("[Region] Crossfade in on velocity") @@ -97,13 +97,13 @@ TEST_CASE("[Region] Crossfade in on velocity") region.parseOpcode({ "xfin_lovel", "20" }); region.parseOpcode({ "xfin_hivel", "24" }); region.parseOpcode({ "amp_veltrack", "0" }); - REQUIRE( region.getNoteGain(1, 19_norm) == 0.0_a ); - REQUIRE( region.getNoteGain(1, 20_norm) == 0.0_a ); - REQUIRE( region.getNoteGain(2, 21_norm) == 0.5_a ); - REQUIRE( region.getNoteGain(3, 22_norm) == 0.70711_a ); - REQUIRE( region.getNoteGain(4, 23_norm) == 0.86603_a ); - REQUIRE( region.getNoteGain(5, 24_norm) == 1.0_a ); - REQUIRE( region.getNoteGain(6, 25_norm) == 1.0_a ); + REQUIRE(region.getNoteGain(1, 19_norm) == 0.0_a); + REQUIRE(region.getNoteGain(1, 20_norm) == 0.0_a); + REQUIRE(region.getNoteGain(2, 21_norm) == 0.5_a); + REQUIRE(region.getNoteGain(3, 22_norm) == 0.70711_a); + REQUIRE(region.getNoteGain(4, 23_norm) == 0.86603_a); + REQUIRE(region.getNoteGain(5, 24_norm) == 1.0_a); + REQUIRE(region.getNoteGain(6, 25_norm) == 1.0_a); } TEST_CASE("[Region] Crossfade in on vel - gain") @@ -115,13 +115,13 @@ TEST_CASE("[Region] Crossfade in on vel - gain") region.parseOpcode({ "xfin_hivel", "24" }); region.parseOpcode({ "xf_velcurve", "gain" }); region.parseOpcode({ "amp_veltrack", "0" }); - REQUIRE( region.getNoteGain(1, 19_norm) == 0.0_a ); - REQUIRE( region.getNoteGain(1, 20_norm) == 0.0_a ); - REQUIRE( region.getNoteGain(2, 21_norm) == 0.25_a ); - REQUIRE( region.getNoteGain(3, 22_norm) == 0.5_a ); - REQUIRE( region.getNoteGain(4, 23_norm) == 0.75_a ); - REQUIRE( region.getNoteGain(5, 24_norm) == 1.0_a ); - REQUIRE( region.getNoteGain(5, 25_norm) == 1.0_a ); + REQUIRE(region.getNoteGain(1, 19_norm) == 0.0_a); + REQUIRE(region.getNoteGain(1, 20_norm) == 0.0_a); + REQUIRE(region.getNoteGain(2, 21_norm) == 0.25_a); + REQUIRE(region.getNoteGain(3, 22_norm) == 0.5_a); + REQUIRE(region.getNoteGain(4, 23_norm) == 0.75_a); + REQUIRE(region.getNoteGain(5, 24_norm) == 1.0_a); + REQUIRE(region.getNoteGain(5, 25_norm) == 1.0_a); } TEST_CASE("[Region] Crossfade out on vel") @@ -132,13 +132,13 @@ TEST_CASE("[Region] Crossfade out on vel") region.parseOpcode({ "xfout_lovel", "51" }); region.parseOpcode({ "xfout_hivel", "55" }); region.parseOpcode({ "amp_veltrack", "0" }); - REQUIRE( region.getNoteGain(5, 50_norm) == 1.0_a ); - REQUIRE( region.getNoteGain(5, 51_norm) == 1.0_a ); - REQUIRE( region.getNoteGain(5, 52_norm) == 0.86603_a ); - REQUIRE( region.getNoteGain(5, 53_norm) == 0.70711_a ); - REQUIRE( region.getNoteGain(5, 54_norm) == 0.5_a ); - REQUIRE( region.getNoteGain(5, 55_norm) == 0.0_a ); - REQUIRE( region.getNoteGain(5, 56_norm) == 0.0_a ); + REQUIRE(region.getNoteGain(5, 50_norm) == 1.0_a); + REQUIRE(region.getNoteGain(5, 51_norm) == 1.0_a); + REQUIRE(region.getNoteGain(5, 52_norm) == 0.86603_a); + REQUIRE(region.getNoteGain(5, 53_norm) == 0.70711_a); + REQUIRE(region.getNoteGain(5, 54_norm) == 0.5_a); + REQUIRE(region.getNoteGain(5, 55_norm) == 0.0_a); + REQUIRE(region.getNoteGain(5, 56_norm) == 0.0_a); } TEST_CASE("[Region] Crossfade out on vel - gain") @@ -150,13 +150,13 @@ TEST_CASE("[Region] Crossfade out on vel - gain") region.parseOpcode({ "xfout_hivel", "55" }); region.parseOpcode({ "xf_velcurve", "gain" }); region.parseOpcode({ "amp_veltrack", "0" }); - REQUIRE( region.getNoteGain(56, 50_norm) == 1.0_a ); - REQUIRE( region.getNoteGain(56, 51_norm) == 1.0_a ); - REQUIRE( region.getNoteGain(56, 52_norm) == 0.75_a ); - REQUIRE( region.getNoteGain(56, 53_norm) == 0.5_a ); - REQUIRE( region.getNoteGain(56, 54_norm) == 0.25_a ); - REQUIRE( region.getNoteGain(56, 55_norm) == 0.0_a ); - REQUIRE( region.getNoteGain(56, 56_norm) == 0.0_a ); + REQUIRE(region.getNoteGain(56, 50_norm) == 1.0_a); + REQUIRE(region.getNoteGain(56, 51_norm) == 1.0_a); + REQUIRE(region.getNoteGain(56, 52_norm) == 0.75_a); + REQUIRE(region.getNoteGain(56, 53_norm) == 0.5_a); + REQUIRE(region.getNoteGain(56, 54_norm) == 0.25_a); + REQUIRE(region.getNoteGain(56, 55_norm) == 0.0_a); + REQUIRE(region.getNoteGain(56, 56_norm) == 0.0_a); } TEST_CASE("[Region] Crossfade in on CC") @@ -167,13 +167,20 @@ TEST_CASE("[Region] Crossfade in on CC") region.parseOpcode({ "xfin_locc24", "20" }); region.parseOpcode({ "xfin_hicc24", "24" }); region.parseOpcode({ "amp_veltrack", "0" }); - midiState.ccEvent(0, 24, 19_norm); REQUIRE( region.getCrossfadeGain() == 0.0_a ); - midiState.ccEvent(0, 24, 20_norm); REQUIRE( region.getCrossfadeGain() == 0.0_a ); - midiState.ccEvent(0, 24, 21_norm); REQUIRE( region.getCrossfadeGain() == 0.5_a ); - midiState.ccEvent(0, 24, 22_norm); REQUIRE( region.getCrossfadeGain() == 0.70711_a ); - midiState.ccEvent(0, 24, 23_norm); REQUIRE( region.getCrossfadeGain() == 0.86603_a ); - midiState.ccEvent(0, 24, 24_norm); REQUIRE( region.getCrossfadeGain() == 1.0_a ); - midiState.ccEvent(0, 24, 25_norm); REQUIRE( region.getCrossfadeGain() == 1.0_a ); + midiState.ccEvent(0, 24, 19_norm); + REQUIRE(region.getCrossfadeGain() == 0.0_a); + midiState.ccEvent(0, 24, 20_norm); + REQUIRE(region.getCrossfadeGain() == 0.0_a); + midiState.ccEvent(0, 24, 21_norm); + REQUIRE(region.getCrossfadeGain() == 0.5_a); + midiState.ccEvent(0, 24, 22_norm); + REQUIRE(region.getCrossfadeGain() == 0.70711_a); + midiState.ccEvent(0, 24, 23_norm); + REQUIRE(region.getCrossfadeGain() == 0.86603_a); + midiState.ccEvent(0, 24, 24_norm); + REQUIRE(region.getCrossfadeGain() == 1.0_a); + midiState.ccEvent(0, 24, 25_norm); + REQUIRE(region.getCrossfadeGain() == 1.0_a); } TEST_CASE("[Region] Crossfade in on CC - gain") @@ -185,13 +192,20 @@ TEST_CASE("[Region] Crossfade in on CC - gain") region.parseOpcode({ "xfin_hicc24", "24" }); region.parseOpcode({ "amp_veltrack", "0" }); region.parseOpcode({ "xf_cccurve", "gain" }); - midiState.ccEvent(0, 24, 19_norm); REQUIRE( region.getCrossfadeGain() == 0.0_a ); - midiState.ccEvent(0, 24, 20_norm); REQUIRE( region.getCrossfadeGain() == 0.0_a ); - midiState.ccEvent(0, 24, 21_norm); REQUIRE( region.getCrossfadeGain() == 0.25_a ); - midiState.ccEvent(0, 24, 22_norm); REQUIRE( region.getCrossfadeGain() == 0.5_a ); - midiState.ccEvent(0, 24, 23_norm); REQUIRE( region.getCrossfadeGain() == 0.75_a ); - midiState.ccEvent(0, 24, 24_norm); REQUIRE( region.getCrossfadeGain() == 1.0_a ); - midiState.ccEvent(0, 24, 25_norm); REQUIRE( region.getCrossfadeGain() == 1.0_a ); + midiState.ccEvent(0, 24, 19_norm); + REQUIRE(region.getCrossfadeGain() == 0.0_a); + midiState.ccEvent(0, 24, 20_norm); + REQUIRE(region.getCrossfadeGain() == 0.0_a); + midiState.ccEvent(0, 24, 21_norm); + REQUIRE(region.getCrossfadeGain() == 0.25_a); + midiState.ccEvent(0, 24, 22_norm); + REQUIRE(region.getCrossfadeGain() == 0.5_a); + midiState.ccEvent(0, 24, 23_norm); + REQUIRE(region.getCrossfadeGain() == 0.75_a); + midiState.ccEvent(0, 24, 24_norm); + REQUIRE(region.getCrossfadeGain() == 1.0_a); + midiState.ccEvent(0, 24, 25_norm); + REQUIRE(region.getCrossfadeGain() == 1.0_a); } TEST_CASE("[Region] Crossfade out on CC") { @@ -201,13 +215,20 @@ TEST_CASE("[Region] Crossfade out on CC") region.parseOpcode({ "xfout_locc24", "20" }); region.parseOpcode({ "xfout_hicc24", "24" }); region.parseOpcode({ "amp_veltrack", "0" }); - midiState.ccEvent(0, 24, 19_norm); REQUIRE( region.getCrossfadeGain() == 1.0_a ); - midiState.ccEvent(0, 24, 20_norm); REQUIRE( region.getCrossfadeGain() == 1.0_a ); - midiState.ccEvent(0, 24, 21_norm); REQUIRE( region.getCrossfadeGain() == 0.86603_a ); - midiState.ccEvent(0, 24, 22_norm); REQUIRE( region.getCrossfadeGain() == 0.70711_a ); - midiState.ccEvent(0, 24, 23_norm); REQUIRE( region.getCrossfadeGain() == 0.5_a ); - midiState.ccEvent(0, 24, 24_norm); REQUIRE( region.getCrossfadeGain() == 0.0_a ); - midiState.ccEvent(0, 24, 25_norm); REQUIRE( region.getCrossfadeGain() == 0.0_a ); + midiState.ccEvent(0, 24, 19_norm); + REQUIRE(region.getCrossfadeGain() == 1.0_a); + midiState.ccEvent(0, 24, 20_norm); + REQUIRE(region.getCrossfadeGain() == 1.0_a); + midiState.ccEvent(0, 24, 21_norm); + REQUIRE(region.getCrossfadeGain() == 0.86603_a); + midiState.ccEvent(0, 24, 22_norm); + REQUIRE(region.getCrossfadeGain() == 0.70711_a); + midiState.ccEvent(0, 24, 23_norm); + REQUIRE(region.getCrossfadeGain() == 0.5_a); + midiState.ccEvent(0, 24, 24_norm); + REQUIRE(region.getCrossfadeGain() == 0.0_a); + midiState.ccEvent(0, 24, 25_norm); + REQUIRE(region.getCrossfadeGain() == 0.0_a); } TEST_CASE("[Region] Crossfade out on CC - gain") @@ -219,13 +240,20 @@ TEST_CASE("[Region] Crossfade out on CC - gain") region.parseOpcode({ "xfout_hicc24", "24" }); region.parseOpcode({ "amp_veltrack", "0" }); region.parseOpcode({ "xf_cccurve", "gain" }); - midiState.ccEvent(0, 24, 19_norm); REQUIRE( region.getCrossfadeGain() == 1.0_a ); - midiState.ccEvent(0, 24, 20_norm); REQUIRE( region.getCrossfadeGain() == 1.0_a ); - midiState.ccEvent(0, 24, 21_norm); REQUIRE( region.getCrossfadeGain() == 0.75_a ); - midiState.ccEvent(0, 24, 22_norm); REQUIRE( region.getCrossfadeGain() == 0.5_a ); - midiState.ccEvent(0, 24, 23_norm); REQUIRE( region.getCrossfadeGain() == 0.25_a ); - midiState.ccEvent(0, 24, 24_norm); REQUIRE( region.getCrossfadeGain() == 0.0_a ); - midiState.ccEvent(0, 24, 25_norm); REQUIRE( region.getCrossfadeGain() == 0.0_a ); + midiState.ccEvent(0, 24, 19_norm); + REQUIRE(region.getCrossfadeGain() == 1.0_a); + midiState.ccEvent(0, 24, 20_norm); + REQUIRE(region.getCrossfadeGain() == 1.0_a); + midiState.ccEvent(0, 24, 21_norm); + REQUIRE(region.getCrossfadeGain() == 0.75_a); + midiState.ccEvent(0, 24, 22_norm); + REQUIRE(region.getCrossfadeGain() == 0.5_a); + midiState.ccEvent(0, 24, 23_norm); + REQUIRE(region.getCrossfadeGain() == 0.25_a); + midiState.ccEvent(0, 24, 24_norm); + REQUIRE(region.getCrossfadeGain() == 0.0_a); + midiState.ccEvent(0, 24, 25_norm); + REQUIRE(region.getCrossfadeGain() == 0.0_a); } TEST_CASE("[Region] Velocity bug for extreme values - veltrack at 0") @@ -234,8 +262,8 @@ TEST_CASE("[Region] Velocity bug for extreme values - veltrack at 0") sfz::Region region { midiState }; region.parseOpcode({ "sample", "*sine" }); region.parseOpcode({ "amp_veltrack", "0" }); - REQUIRE( region.getNoteGain(64, 127_norm) == 1.0_a ); - REQUIRE( region.getNoteGain(64, 0_norm) == 1.0_a ); + REQUIRE(region.getNoteGain(64, 127_norm) == 1.0_a); + REQUIRE(region.getNoteGain(64, 0_norm) == 1.0_a); } @@ -245,8 +273,8 @@ TEST_CASE("[Region] Velocity bug for extreme values - positive veltrack") sfz::Region region { midiState }; region.parseOpcode({ "sample", "*sine" }); region.parseOpcode({ "amp_veltrack", "100" }); - REQUIRE( region.getNoteGain(64, 127_norm) == 1.0_a ); - REQUIRE( region.getNoteGain(64, 0_norm) == Approx(0.0).margin(0.0001) ); + REQUIRE(region.getNoteGain(64, 127_norm) == 1.0_a); + REQUIRE(region.getNoteGain(64, 0_norm) == Approx(0.0).margin(0.0001)); } TEST_CASE("[Region] Velocity bug for extreme values - negative veltrack") @@ -255,27 +283,28 @@ TEST_CASE("[Region] Velocity bug for extreme values - negative veltrack") sfz::Region region { midiState }; region.parseOpcode({ "sample", "*sine" }); region.parseOpcode({ "amp_veltrack", "-100" }); - REQUIRE( region.getNoteGain(64, 127_norm) == Approx(0.0).margin(0.0001) ); - REQUIRE( region.getNoteGain(64, 0_norm) == 1.0_a ); + REQUIRE(region.getNoteGain(64, 127_norm) == Approx(0.0).margin(0.0001)); + REQUIRE(region.getNoteGain(64, 0_norm) == 1.0_a); } TEST_CASE("[Region] rt_decay") { sfz::MidiState midiState; + midiState.setSampleRate(1000); sfz::Region region { midiState }; region.parseOpcode({ "sample", "*sine" }); region.parseOpcode({ "trigger", "release" }); region.parseOpcode({ "rt_decay", "10" }); midiState.noteOnEvent(0, 64, 64_norm); - std::this_thread::sleep_for(std::chrono::milliseconds(100)); + midiState.advanceTime(100); REQUIRE( region.getBaseVolumedB(64) == Approx(sfz::Default::volume - 1.0f).margin(0.1) ); region.parseOpcode({ "rt_decay", "20" }); midiState.noteOnEvent(0, 64, 64_norm); - std::this_thread::sleep_for(std::chrono::milliseconds(100)); + midiState.advanceTime(100); REQUIRE( region.getBaseVolumedB(64) == Approx(sfz::Default::volume - 2.0f).margin(0.1) ); region.parseOpcode({ "trigger", "attack" }); midiState.noteOnEvent(0, 64, 64_norm); - std::this_thread::sleep_for(std::chrono::milliseconds(100)); + midiState.advanceTime(100); REQUIRE( region.getBaseVolumedB(64) == Approx(sfz::Default::volume).margin(0.1) ); } diff --git a/tests/SIMDHelpersT.cpp b/tests/SIMDHelpersT.cpp index 01c087ec..44c837c4 100644 --- a/tests/SIMDHelpersT.cpp +++ b/tests/SIMDHelpersT.cpp @@ -499,7 +499,7 @@ TEST_CASE("[Helpers] Linear Ramp") const float start { 0.0f }; const float v { fillValue }; std::array output; - std::array expected { v, v + v, v + v + v, v + v + v + v, v + v + v + v + v, v + v + v + v + v + v }; + std::array expected { start, start + v, start + v + v, start + v + v + v, start + v + v + v + v, start + v + v + v + v + v }; sfz::linearRamp(absl::MakeSpan(output), start, v); REQUIRE(output == expected); } @@ -509,7 +509,7 @@ TEST_CASE("[Helpers] Linear Ramp (SIMD)") const float start { 0.0f }; const float v { fillValue }; std::array output; - std::array expected { v, v + v, v + v + v, v + v + v + v, v + v + v + v + v, v + v + v + v + v + v }; + std::array expected { start, start + v, start + v + v, start + v + v + v, start + v + v + v + v, start + v + v + v + v + v }; sfz::linearRamp(absl::MakeSpan(output), start, v); REQUIRE(approxEqual(output, expected)); } @@ -539,7 +539,7 @@ TEST_CASE("[Helpers] Multiplicative Ramp") const float start { 1.0f }; const float v { fillValue }; std::array output; - std::array expected { v, v * v, v * v * v, v * v * v * v, v * v * v * v * v, v * v * v * v * v * v }; + std::array expected { start, start * v, start * v * v, start * v * v * v, start * v * v * v * v, start * v * v * v * v * v }; sfz::multiplicativeRamp(absl::MakeSpan(output), start, v); REQUIRE(approxEqual(output, expected)); } @@ -549,7 +549,7 @@ TEST_CASE("[Helpers] Multiplicative Ramp (SIMD)") const float start { 1.0f }; const float v { fillValue }; std::array output; - std::array expected { v, v * v, v * v * v, v * v * v * v, v * v * v * v * v, v * v * v * v * v * v }; + std::array expected { start, start * v, start * v * v, start * v * v * v, start * v * v * v * v, start * v * v * v * v * v }; sfz::multiplicativeRamp(absl::MakeSpan(output), start, v); REQUIRE(approxEqual(output, expected)); } diff --git a/tests/SynthT.cpp b/tests/SynthT.cpp index 4411d12b..c911c0c7 100644 --- a/tests/SynthT.cpp +++ b/tests/SynthT.cpp @@ -141,9 +141,9 @@ TEST_CASE("[Synth] Reset all controllers") { sfz::Synth synth; synth.cc(0, 12, 64); - REQUIRE( synth.getMidiState().getCCValue(12) == 64_norm ); + REQUIRE(synth.getMidiState().getCCValue(12) == 64_norm); synth.cc(0, 121, 64); - REQUIRE( synth.getMidiState().getCCValue(12) == 0_norm ); + REQUIRE(synth.getMidiState().getCCValue(12) == 0_norm); } TEST_CASE("[Synth] Releasing before the EG started smoothing (initial delay) kills the voice") @@ -332,3 +332,45 @@ TEST_CASE("[Synth] Gain to mix") REQUIRE( bus->gainToMain() == 0 ); REQUIRE( bus->gainToMix() == 0.5 ); } + +TEST_CASE("[Synth] group polyphony limits") +{ + sfz::Synth synth; + synth.loadSfzFile(fs::current_path() / "tests/TestFiles/polyphony.sfz"); + synth.noteOn(0, 65, 64); + synth.noteOn(0, 65, 64); + synth.noteOn(0, 65, 64); + REQUIRE(synth.getNumActiveVoices() == 2); // group polyphony should block the last note +} + +TEST_CASE("[Synth] Self-masking") +{ + sfz::Synth synth; + synth.loadSfzFile(fs::current_path() / "tests/TestFiles/polyphony.sfz"); + synth.noteOn(0, 64, 63); + synth.noteOn(0, 64, 62); + synth.noteOn(0, 64, 64); + REQUIRE(synth.getNumActiveVoices() == 3); // One of these is releasing + REQUIRE(synth.getVoiceView(0)->getTriggerValue() == 63_norm); + REQUIRE(!synth.getVoiceView(0)->canBeStolen()); + REQUIRE(synth.getVoiceView(1)->getTriggerValue() == 62_norm); + REQUIRE(synth.getVoiceView(1)->canBeStolen()); // The lowest velocity voice is the masking candidate + REQUIRE(synth.getVoiceView(2)->getTriggerValue() == 64_norm); + REQUIRE(!synth.getVoiceView(2)->canBeStolen()); +} + +TEST_CASE("[Synth] Not self-masking") +{ + sfz::Synth synth; + synth.loadSfzFile(fs::current_path() / "tests/TestFiles/polyphony.sfz"); + synth.noteOn(0, 66, 63); + synth.noteOn(0, 66, 62); + synth.noteOn(0, 66, 64); + REQUIRE(synth.getNumActiveVoices() == 3); // One of these is releasing + REQUIRE(synth.getVoiceView(0)->getTriggerValue() == 63_norm); + REQUIRE(synth.getVoiceView(0)->canBeStolen()); // The first encountered voice is the masking candidate + REQUIRE(synth.getVoiceView(1)->getTriggerValue() == 62_norm); + REQUIRE(!synth.getVoiceView(1)->canBeStolen()); + REQUIRE(synth.getVoiceView(2)->getTriggerValue() == 64_norm); + REQUIRE(!synth.getVoiceView(2)->canBeStolen()); +} diff --git a/tests/TestFiles/polyphony.sfz b/tests/TestFiles/polyphony.sfz new file mode 100644 index 00000000..32aed2e9 --- /dev/null +++ b/tests/TestFiles/polyphony.sfz @@ -0,0 +1,5 @@ + sample=*sine key=63 + sample=*sine key=64 note_polyphony=2 + sample=*sine key=66 note_polyphony=2 note_selfmask=off + group=1 polyphony=2 + sample=*sine key=65