Merge pull request #142 from paulfd/midi-state-block-processing

The midi state now tracks time through block duration; you need to call advanceTime(numSamples) at each callback.
The midi state now stores all CC and pitch events that happened during the block.
Added group polyphony and note_polyphony support, as well as note_selfmask support.
Multiple CCs can modulate pan, amplitude, width, position rather than one.
The voices do not store CC history for the modulation targets and rely on the midi state to provide the events that happened during the last block.
Removed the EventEnvelopes; they're replaced by generic free functions that take as input an EventVector from the midi state; old tests migrated.
Added support for tune_cc and pitch_cc related opcodes.

In all this process, I also removed most of the per-voice temporary buffers and preallocation to replace everything by a BufferPool that distributes buffers around, and can track the maximum buffer usage in debug mode. The number of concurrent buffers are set at compile-time, so it must be adapted to the current processing needs.
This commit is contained in:
Paul Ferrand 2020-03-31 23:22:25 +02:00 committed by GitHub
commit 2f2de8c677
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 1680 additions and 1383 deletions

View file

@ -10,7 +10,7 @@
#include <vector> #include <vector>
#include <cmath> #include <cmath>
#include <iostream> #include <iostream>
#include "EventEnvelopes.h" #include "SfzHelpers.h"
#include "absl/types/span.h" #include "absl/types/span.h"
class EnvelopeFixture : public benchmark::Fixture { class EnvelopeFixture : public benchmark::Fixture {
@ -29,45 +29,55 @@ public:
} }
std::random_device rd { }; std::random_device rd { };
std::mt19937 gen { rd() }; std::mt19937 gen { rd() };
std::uniform_real_distribution<float> dist { 1, 30 }; std::uniform_real_distribution<float> dist { 2, 30 };
std::vector<float> input; std::vector<float> input;
std::vector<float> output; std::vector<float> output;
}; };
BENCHMARK_DEFINE_F(EnvelopeFixture, Linear)(benchmark::State& state) { BENCHMARK_DEFINE_F(EnvelopeFixture, Linear)(benchmark::State& state) {
sfz::LinearEnvelope<float> envelope;
for (auto _ : state) for (auto _ : state)
{ {
envelope.registerEvent(static_cast<int>(state.range(0) - 1), dist(gen)); sfz::EventVector events {
envelope.getBlock(absl::MakeSpan(output)); { 0, 0.0f },
{ static_cast<int>(state.range(0) - 1), dist(gen) }
};
linearEnvelope(events, absl::MakeSpan(output), [](float x) { return x; });
} }
} }
BENCHMARK_DEFINE_F(EnvelopeFixture, LinearQuantized)(benchmark::State& state) { BENCHMARK_DEFINE_F(EnvelopeFixture, LinearQuantized)(benchmark::State& state) {
sfz::LinearEnvelope<float> envelope;
for (auto _ : state) for (auto _ : state)
{ {
envelope.registerEvent(static_cast<int>(state.range(0) - 1), dist(gen)); sfz::EventVector events {
envelope.getQuantizedBlock(absl::MakeSpan(output), 0.5); { 0, 0.0f },
{ static_cast<int>(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) { BENCHMARK_DEFINE_F(EnvelopeFixture, Multiplicative)(benchmark::State& state) {
sfz::MultiplicativeEnvelope<float> envelope;
for (auto _ : state) for (auto _ : state)
{ {
envelope.registerEvent(static_cast<int>(state.range(0) - 1), dist(gen)); sfz::EventVector events {
envelope.getBlock(absl::MakeSpan(output)); { 0, 1.0f },
{ static_cast<int>(state.range(0) - 1), dist(gen) }
};
multiplicativeEnvelope(events, absl::MakeSpan(output), [](float x) { return x; });
} }
} }
BENCHMARK_DEFINE_F(EnvelopeFixture, MultiplicativeQuantized)(benchmark::State& state) { BENCHMARK_DEFINE_F(EnvelopeFixture, MultiplicativeQuantized)(benchmark::State& state) {
sfz::MultiplicativeEnvelope<float> envelope;
for (auto _ : state) for (auto _ : state)
{ {
envelope.registerEvent(static_cast<int>(state.range(0) - 1), dist(gen)); sfz::EventVector events {
envelope.getQuantizedBlock(absl::MakeSpan(output), 1.5); { 0, 1.0f },
{ static_cast<int>(state.range(0) - 1), dist(gen) }
};
multiplicativeEnvelope(
events, absl::MakeSpan(output), [](float x) { return x; }, 2.0f);
} }
} }

View file

@ -69,11 +69,11 @@ sfizz_add_benchmark(bm_logger BM_logger.cpp)
target_link_libraries(bm_logger PRIVATE sfizz::sfizz) target_link_libraries(bm_logger PRIVATE sfizz::sfizz)
if (TARGET sfizz-samplerate) if (TARGET sfizz-samplerate)
sfizz_add_benchmark(bm_resample BM_resample.cpp ${BENCHMARK_SIMD_SOURCES}) sfizz_add_benchmark(bm_resample BM_resample.cpp ${BENCHMARK_SIMD_SOURCES})
target_link_libraries(bm_resample PRIVATE sfizz-samplerate sfizz-sndfile) target_link_libraries(bm_resample PRIVATE sfizz-samplerate sfizz-sndfile)
endif() 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) sfizz_add_benchmark(bm_wavfile BM_wavfile.cpp)
target_link_libraries(bm_wavfile PRIVATE sfizz-sndfile) target_link_libraries(bm_wavfile PRIVATE sfizz-sndfile)

View file

@ -5,7 +5,6 @@ clang-tidy \
src/sfizz/Curve.cpp \ src/sfizz/Curve.cpp \
src/sfizz/Effects.cpp \ src/sfizz/Effects.cpp \
src/sfizz/EQPool.cpp \ src/sfizz/EQPool.cpp \
src/sfizz/EventEnvelopes.cpp \
src/sfizz/FilePool.cpp \ src/sfizz/FilePool.cpp \
src/sfizz/FilterPool.cpp \ src/sfizz/FilterPool.cpp \
src/sfizz/FloatEnvelopes.cpp \ src/sfizz/FloatEnvelopes.cpp \

View file

@ -24,7 +24,7 @@ void ADSREnvelope<Type>::reset(const Region& region, const MidiState& state, int
this->release = secondsToSamples(region.amplitudeEG.getRelease(state, velocity)); this->release = secondsToSamples(region.amplitudeEG.getRelease(state, velocity));
this->hold = secondsToSamples(region.amplitudeEG.getHold(state, velocity)); this->hold = secondsToSamples(region.amplitudeEG.getHold(state, velocity));
this->peak = 1.0; 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)); this->start = this->peak * normalizePercents(region.amplitudeEG.getStart(state, velocity));
releaseDelay = 0; releaseDelay = 0;

184
src/sfizz/BufferPool.h Normal file
View file

@ -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 <array>
#include <memory>
#include <functional>
#include "absl/algorithm/container.h"
#ifndef NDEBUG
#include "MathHelpers.h"
#endif
namespace sfz {
template <class T>
class SpanHolder {
public:
SpanHolder() {}
SpanHolder(const SpanHolder<T>&) = delete;
SpanHolder<T>& operator=(const SpanHolder<T>&) = delete;
SpanHolder(SpanHolder<T>&& other)
{
this->value = other.value;
this->available = other.available;
other.available = nullptr;
}
SpanHolder<T>& operator=(SpanHolder<T>&& other)
{
this->value = other.value;
this->available = other.available;
other.available = nullptr;
}
SpanHolder(T&& value, int* available)
: value(std::forward<T>(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<absl::Span<float>> 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<absl::Span<int>> 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<AudioSpan<float>> 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<float>(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<sfz::Buffer<float>, config::bufferPoolSize> monoBuffers;
std::vector<int> monoAvailable;
std::array<sfz::Buffer<int>, config::bufferPoolSize> indexBuffers;
std::vector<int> indexAvailable;
std::array<sfz::AudioBuffer<float>, config::stereoBufferPoolSize> stereoBuffers;
std::vector<int> stereoAvailable;
#ifndef NDEBUG
mutable int maxBuffersUsed { 0 };
mutable int maxIndexBuffersUsed { 0 };
mutable int maxStereoBuffersUsed { 0 };
#endif
};
}

View file

@ -28,6 +28,9 @@ namespace config {
constexpr float defaultSampleRate { 48000 }; constexpr float defaultSampleRate { 48000 };
constexpr int defaultSamplesPerBlock { 1024 }; constexpr int defaultSamplesPerBlock { 1024 };
constexpr int maxBlockSize { 8192 }; constexpr int maxBlockSize { 8192 };
constexpr int bufferPoolSize { 4 };
constexpr int stereoBufferPoolSize { 4 };
constexpr int indexBufferPoolSize { 2 };
constexpr int preloadSize { 8192 }; constexpr int preloadSize { 8192 };
constexpr int loggerQueueSize { 256 }; constexpr int loggerQueueSize { 256 };
constexpr int voiceLoggerQueueSize { 256 }; constexpr int voiceLoggerQueueSize { 256 };
@ -35,7 +38,7 @@ namespace config {
constexpr size_t numChannels { 2 }; constexpr size_t numChannels { 2 };
constexpr int numBackgroundThreads { 4 }; constexpr int numBackgroundThreads { 4 };
constexpr int numVoices { 64 }; constexpr int numVoices { 64 };
constexpr int maxVoices { 256 }; constexpr unsigned maxVoices { 256 };
constexpr int maxFilePromises { maxVoices * 2 }; constexpr int maxFilePromises { maxVoices * 2 };
constexpr int sustainCC { 64 }; constexpr int sustainCC { 64 };
constexpr int allSoundOffCC { 120 }; constexpr int allSoundOffCC { 120 };

View file

@ -144,7 +144,7 @@ void Curve::lerpFill(const bool fillStatus[NumValues])
const auto length = right - left; const auto length = right - left;
if (length > 1) { if (length > 1) {
const float mu = (_points[right] - _points[left]) / length; const float mu = (_points[right] - _points[left]) / length;
linearRamp<float>(pointSpan.subspan(left + 1, length - 1), _points[left], mu); linearRamp<float>(pointSpan.subspan(left, length), _points[left], mu);
} }
left = right++; left = right++;
} }

View file

@ -34,6 +34,7 @@ enum class SfzLoopMode { no_loop, one_shot, loop_continuous, loop_sustain };
enum class SfzOffMode { fast, normal }; enum class SfzOffMode { fast, normal };
enum class SfzVelocityOverride { current, previous }; enum class SfzVelocityOverride { current, previous };
enum class SfzCrossfadeCurve { gain, power }; enum class SfzCrossfadeCurve { gain, power };
enum class SfzSelfMask { mask, dontMask };
namespace sfz namespace sfz
{ {
@ -53,9 +54,11 @@ namespace Default
constexpr SfzLoopMode loopMode { SfzLoopMode::no_loop }; constexpr SfzLoopMode loopMode { SfzLoopMode::no_loop };
constexpr Range<uint32_t> loopRange { 0, std::numeric_limits<uint32_t>::max() }; constexpr Range<uint32_t> loopRange { 0, std::numeric_limits<uint32_t>::max() };
// Global ranges // common defaults
constexpr Range<uint8_t> midi7Range { 0, 127 }; constexpr Range<uint8_t> midi7Range { 0, 127 };
constexpr Range<float> normalizedRange { 0.0f, 1.0f }; constexpr Range<float> normalizedRange { 0.0f, 1.0f };
constexpr Range<float> symmetricNormalizedRange { -1.0, 1.0 };
constexpr float zeroModifier { 0.0f };
// Wavetable oscillator // Wavetable oscillator
constexpr float oscillatorPhase { 0.0 }; constexpr float oscillatorPhase { 0.0 };
@ -65,18 +68,21 @@ namespace Default
constexpr uint32_t group { 0 }; constexpr uint32_t group { 0 };
constexpr Range<uint32_t> groupRange { 0, std::numeric_limits<uint32_t>::max() }; constexpr Range<uint32_t> groupRange { 0, std::numeric_limits<uint32_t>::max() };
constexpr SfzOffMode offMode { SfzOffMode::fast }; constexpr SfzOffMode offMode { SfzOffMode::fast };
constexpr Range<uint32_t> polyphonyRange { 0, config::maxVoices };
constexpr SfzSelfMask selfMask { SfzSelfMask::mask };
// Region logic: key mapping // Region logic: key mapping
constexpr Range<uint8_t> keyRange { 0, 127 }; constexpr Range<uint8_t> keyRange { 0, 127 };
constexpr auto velocityRange = normalizedRange; constexpr auto velocityRange = normalizedRange;
// Region logic: MIDI conditions // Region logic: MIDI conditions
constexpr Range<uint8_t> channelRange { 1, 16 }; constexpr Range<uint8_t> channelRange { 1, 16 };
constexpr Range<uint8_t> midiChannelRange { 0, 15 }; constexpr Range<uint8_t> midiChannelRange { 0, 15 };
constexpr Range<uint16_t> ccNumberRange { 0, config::numCCs }; constexpr Range<uint16_t> ccNumberRange { 0, config::numCCs };
constexpr auto ccValueRange = normalizedRange; constexpr auto ccValueRange = normalizedRange;
constexpr Range<int> bendRange { -8192, 8192 }; constexpr Range<int> bendRange = { -8192, 8192 };
constexpr int bend { 0 }; constexpr Range<float> bendValueRange = symmetricNormalizedRange;
constexpr int bend { 0 };
constexpr SfzVelocityOverride velocityOverride { SfzVelocityOverride::current }; constexpr SfzVelocityOverride velocityOverride { SfzVelocityOverride::current };
// Region logic: internal conditions // Region logic: internal conditions
@ -91,9 +97,9 @@ namespace Default
// Region logic: Triggers // Region logic: Triggers
constexpr SfzTrigger trigger { SfzTrigger::attack }; constexpr SfzTrigger trigger { SfzTrigger::attack };
constexpr Range<float> ccTriggerValueRange = normalizedRange; constexpr Range<float> ccTriggerValueRange = normalizedRange;
// Performance parameters: amplifier // Performance parameters: amplifier
constexpr float globalVolume { -7.35f }; constexpr float globalVolume { -7.35f };
constexpr float volume { 0.0f }; constexpr float volume { 0.0f };
constexpr Range<float> volumeRange { -144.0, 6.0 }; constexpr Range<float> volumeRange { -144.0, 6.0 };
@ -103,7 +109,6 @@ namespace Default
constexpr float pan { 0.0 }; constexpr float pan { 0.0 };
constexpr Range<float> panRange { -100.0, 100.0 }; constexpr Range<float> panRange { -100.0, 100.0 };
constexpr Range<float> panCCRange { -200.0, 200.0 }; constexpr Range<float> panCCRange { -200.0, 200.0 };
constexpr Range<float> symmetricNormalizedRange { -1.0, 1.0 };
constexpr float position { 0.0 }; constexpr float position { 0.0 };
constexpr Range<float> positionRange { -100.0, 100.0 }; constexpr Range<float> positionRange { -100.0, 100.0 };
constexpr Range<float> positionCCRange { -200.0, 200.0 }; constexpr Range<float> positionCCRange { -200.0, 200.0 };
@ -120,11 +125,11 @@ namespace Default
constexpr Range<float> ampRandomRange { 0.0, 24.0 }; constexpr Range<float> ampRandomRange { 0.0, 24.0 };
constexpr Range<uint8_t> crossfadeKeyInRange { 0, 0 }; constexpr Range<uint8_t> crossfadeKeyInRange { 0, 0 };
constexpr Range<uint8_t> crossfadeKeyOutRange { 127, 127 }; constexpr Range<uint8_t> crossfadeKeyOutRange { 127, 127 };
constexpr Range<float> crossfadeVelInRange { 0.0f, 0.0f }; constexpr Range<float> crossfadeVelInRange { 0.0f, 0.0f };
constexpr Range<float> crossfadeVelOutRange { 1.0f, 1.0f }; constexpr Range<float> crossfadeVelOutRange { 1.0f, 1.0f };
constexpr Range<float> crossfadeCCInRange { 0.0f, 0.0f }; constexpr Range<float> crossfadeCCInRange { 0.0f, 0.0f };
constexpr Range<float> crossfadeCCOutRange { 1.0f, 1.0f }; constexpr Range<float> crossfadeCCOutRange { 1.0f, 1.0f };
constexpr SfzCrossfadeCurve crossfadeKeyCurve { SfzCrossfadeCurve::power }; constexpr SfzCrossfadeCurve crossfadeKeyCurve { SfzCrossfadeCurve::power };
constexpr SfzCrossfadeCurve crossfadeVelCurve { SfzCrossfadeCurve::power }; constexpr SfzCrossfadeCurve crossfadeVelCurve { SfzCrossfadeCurve::power };
constexpr SfzCrossfadeCurve crossfadeCCCurve { SfzCrossfadeCurve::power }; constexpr SfzCrossfadeCurve crossfadeCCCurve { SfzCrossfadeCurve::power };
constexpr float rtDecay { 0.0f }; constexpr float rtDecay { 0.0f };
@ -184,6 +189,7 @@ namespace Default
constexpr Range<int> transposeRange { -127, 127 }; constexpr Range<int> transposeRange { -127, 127 };
constexpr int tune { 0 }; constexpr int tune { 0 };
constexpr Range<int> tuneRange { -9600, 9600 }; // ±100 in SFZv1, more in ARIA constexpr Range<int> tuneRange { -9600, 9600 }; // ±100 in SFZv1, more in ARIA
constexpr Range<int> tuneCCRange { -9600, 9600 };
constexpr Range<int> bendBoundRange { -9600, 9600 }; constexpr Range<int> bendBoundRange { -9600, 9600 };
constexpr Range<int> bendStepRange { 1, 1200 }; constexpr Range<int> bendStepRange { 1, 1200 };
constexpr int bendUp { 200 }; // No range here because the bounds can be inverted constexpr int bendUp { 200 }; // No range here because the bounds can be inverted

View file

@ -28,9 +28,20 @@ void sfz::EQHolder::setup(const EQDescription& description, unsigned numChannels
baseGain = description.gain + velocity * description.vel2gain; baseGain = description.gain + velocity * description.vel2gain;
// Setup the modulated values // Setup the modulated values
lastFrequency = midiState.modulate(baseFrequency, description.frequencyCC, Default::eqFrequencyRange); lastFrequency = baseFrequency;
lastBandwidth = midiState.modulate(baseBandwidth, description.bandwidthCC, Default::eqBandwidthRange); for (const auto& mod : description.frequencyCC)
lastGain = midiState.modulate(baseGain, description.gainCC, Default::eqGainRange); 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 // Initialize the EQ
eq.prepare(lastFrequency, lastBandwidth, lastGain); 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! // TODO: Once the midistate envelopes are done, add modulation in there!
// For now we take the last value // For now we take the last value
lastFrequency = midiState.modulate(baseFrequency, description->frequencyCC, Default::eqFrequencyRange); lastFrequency = baseFrequency;
lastBandwidth = midiState.modulate(baseBandwidth, description->bandwidthCC, Default::eqBandwidthRange); for (const auto& mod : description->frequencyCC)
lastGain = midiState.modulate(baseGain, description->gainCC, Default::eqGainRange); 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) { if (lastGain == 0.0f) {
justCopy(); justCopy();

View file

@ -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 <absl/algorithm/container.h>
namespace sfz {
template <class Type>
EventEnvelope<Type>::EventEnvelope()
{
setMaxCapacity(maxCapacity);
}
template <class Type>
EventEnvelope<Type>::EventEnvelope(int maxCapacity, std::function<Type(Type)> function)
{
setMaxCapacity(maxCapacity);
setFunction(function);
}
template <class Type>
void EventEnvelope<Type>::setMaxCapacity(int maxCapacity)
{
events.reserve(maxCapacity);
this->maxCapacity = maxCapacity;
}
template <class Type>
void EventEnvelope<Type>::setFunction(std::function<Type(Type)> function)
{
this->function = function;
}
template <class Type>
void EventEnvelope<Type>::registerEvent(int timestamp, Type inputValue)
{
if (resetEvents)
clear();
if (static_cast<int>(events.size()) < maxCapacity)
events.emplace_back(timestamp, function(inputValue));
}
template <class Type>
void EventEnvelope<Type>::prepareEvents(int blockLength)
{
if (resetEvents)
clear();
absl::c_stable_sort(events, [](const std::pair<int, Type>& lhs, const std::pair<int, Type>& 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 <class Type>
void EventEnvelope<Type>::clear()
{
events.clear();
resetEvents = false;
}
template <class Type>
void EventEnvelope<Type>::reset(Type value)
{
clear();
currentValue = function(value);
resetEvents = false;
}
template <class Type>
void EventEnvelope<Type>::getBlock(absl::Span<Type> output)
{
prepareEvents(output.size());
}
template <class Type>
void EventEnvelope<Type>::getQuantizedBlock(absl::Span<Type> output, Type)
{
prepareEvents(output.size());
}
template <class Type>
void LinearEnvelope<Type>::getBlock(absl::Span<Type> output)
{
EventEnvelope<Type>::getBlock(output);
auto& events = EventEnvelope<Type>::events;
auto& currentValue = EventEnvelope<Type>::currentValue;
int index { 0 };
for (auto& event : events) {
const auto length = min(event.first, static_cast<int>(output.size())) - index;
if (length == 0) {
currentValue = event.second;
continue;
}
const auto step = (event.second - currentValue) / length;
currentValue = linearRamp<Type>(output.subspan(index, length), currentValue, step);
index += length;
}
if (index < static_cast<int>(output.size()))
fill<Type>(output.subspan(index), currentValue);
}
template <class Type>
void LinearEnvelope<Type>::getQuantizedBlock(absl::Span<Type> output, Type quantizationStep)
{
EventEnvelope<Type>::getQuantizedBlock(output, quantizationStep);
auto& events = EventEnvelope<Type>::events;
auto& currentValue = EventEnvelope<Type>::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<int>(output.size());
for (auto& event : events) {
const auto newValue = quantize(event.second);
if (event.first > outputSize) {
fill<Type>(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<Type>(output.subspan(index, length), currentValue);
currentValue = newValue;
index += length;
continue;
}
const auto numSteps = static_cast<int>(difference / quantizationStep);
const auto stepLength = static_cast<int>(length / numSteps);
for (int i = 0; i < numSteps; ++i) {
fill<Type>(output.subspan(index, stepLength), currentValue);
const auto delta = quantizationStep + currentValue - quantize(currentValue);
currentValue += currentValue <= newValue ? delta : -delta;
index += stepLength;
}
}
if (index < outputSize)
fill<Type>(output.subspan(index), currentValue);
}
template <class Type>
MultiplicativeEnvelope<Type>::MultiplicativeEnvelope()
{
EventEnvelope<Type>::reset(1.0);
}
template <class Type>
void MultiplicativeEnvelope<Type>::getBlock(absl::Span<Type> output)
{
EventEnvelope<Type>::getBlock(output);
auto& events = EventEnvelope<Type>::events;
auto& currentValue = EventEnvelope<Type>::currentValue;
int index { 0 };
for (auto& event : events) {
const auto length = min(event.first, static_cast<int>(output.size())) - index;
if (length == 0) {
currentValue = event.second;
continue;
}
const auto step = std::exp((std::log(event.second) - std::log(currentValue)) / length);
multiplicativeRamp<Type>(output.subspan(index, length), currentValue, step);
currentValue = event.second;
index += length;
}
if (index < static_cast<int>(output.size()))
fill<Type>(output.subspan(index), currentValue);
}
template <class Type>
void MultiplicativeEnvelope<Type>::getQuantizedBlock(absl::Span<Type> output, Type quantizationStep)
{
EventEnvelope<Type>::getQuantizedBlock(output, quantizationStep);
auto& events = EventEnvelope<Type>::events;
auto& currentValue = EventEnvelope<Type>::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<int>(output.size());
for (auto& event : events) {
const auto newValue = quantize(event.second);
if (event.first > outputSize) {
fill<Type>(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<Type>(output.subspan(index, length), currentValue);
currentValue = newValue;
index += length;
continue;
}
const auto numSteps = static_cast<int>(std::log(difference) / logStep);
const auto stepLength = static_cast<int>(length / numSteps);
for (int i = 0; i < numSteps; ++i) {
fill<Type>(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<Type>(output.subspan(index), currentValue);
}
}

View file

@ -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 <absl/types/span.h>
#include <functional>
#include <type_traits>
#include <vector>
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 Type>
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<Type(Type)> 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<Type(Type)> 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<Type> 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<Type> output, Type quantizationStep);
protected:
std::vector<std::pair<int, Type>> events;
Type currentValue { 0.0 };
private:
static_assert(std::is_arithmetic<Type>::value, "Type should be arithmetic");
std::function<Type(Type)> 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 Type>
class LinearEnvelope: public EventEnvelope<Type> {
public:
void getBlock(absl::Span<Type> output) final;
void getQuantizedBlock(absl::Span<Type> output, Type quantizationStep) final;
};
/**
* @brief Describes a simple multiplicative envelope.
*
* @tparam Type
*/
template <class Type>
class MultiplicativeEnvelope: public EventEnvelope<Type> {
public:
MultiplicativeEnvelope();
void getBlock(absl::Span<Type> output) final;
void getQuantizedBlock(absl::Span<Type> output, Type quantizationStep) final;
};
}

View file

@ -273,7 +273,7 @@ sfz::FilePromisePtr sfz::FilePool::getFilePromise(const std::string& filename) n
auto promise = emptyPromises.back(); auto promise = emptyPromises.back();
promise->filename = preloaded->first; promise->filename = preloaded->first;
promise->preloadedData = preloaded->second.preloadedData; promise->preloadedData = preloaded->second.preloadedData;
promise->sampleRate = preloaded->second.information.sampleRate; promise->sampleRate = static_cast<float>(preloaded->second.information.sampleRate);
promise->oversamplingFactor = oversamplingFactor; promise->oversamplingFactor = oversamplingFactor;
promise->creationTime = std::chrono::high_resolution_clock::now(); promise->creationTime = std::chrono::high_resolution_clock::now();

View file

@ -40,9 +40,20 @@ void sfz::FilterHolder::setup(const FilterDescription& description, unsigned num
baseResonance = description.resonance; baseResonance = description.resonance;
// Setup the modulated values // Setup the modulated values
lastCutoff = midiState.modulate<float, int>(baseCutoff, description.cutoffCC, Default::filterCutoffRange, multiplyByCents); lastCutoff = baseCutoff;
lastResonance = midiState.modulate(baseResonance, description.resonanceCC, Default::filterResonanceRange); for (const auto& mod : description.cutoffCC)
lastGain = midiState.modulate(baseGain, description.gainCC, Default::filterGainRange); 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 // Initialize the filter
filter.prepare(lastCutoff, lastResonance, lastGain); 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! // TODO: Once the midistate envelopes are done, add modulation in there!
// For now we take the last value // For now we take the last value
// TODO: the template deduction could be automatic here? // TODO: the template deduction could be automatic here?
lastCutoff = midiState.modulate<float, int>(baseCutoff, description->cutoffCC, Default::filterCutoffRange, multiplyByCents); lastCutoff = baseCutoff;
lastResonance = midiState.modulate(baseResonance, description->resonanceCC, Default::filterResonanceRange); for (const auto& mod : description->cutoffCC)
baseGain = midiState.modulate(baseGain, description->gainCC, Default::filterGainRange); 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); filter.process(inputs, outputs, lastCutoff, lastResonance, lastGain, numFrames);
} }

View file

@ -4,29 +4,13 @@
// license. You should have receive a LICENSE.md file along with the code. // 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 // 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 "ADSREnvelope.h"
// Include the generic implementations // Include the generic implementations
#include "EventEnvelopes.cpp"
#include "ADSREnvelope.cpp" #include "ADSREnvelope.cpp"
// And explicitely instantiate the float version // And explicitely instantiate the float version
namespace sfz namespace sfz
{ {
template class EventEnvelope<float>;
template class MultiplicativeEnvelope<float>;
template class LinearEnvelope<float>;
template class ADSREnvelope<float>; template class ADSREnvelope<float>;
} }

View file

@ -10,7 +10,7 @@
sfz::MidiState::MidiState() sfz::MidiState::MidiState()
{ {
reset(0); reset();
} }
void sfz::MidiState::noteOnEvent(int delay, int noteNumber, float velocity) noexcept 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) { if (noteNumber >= 0 && noteNumber < 128) {
lastNoteVelocities[noteNumber] = velocity; lastNoteVelocities[noteNumber] = velocity;
noteOnTimes[noteNumber] = std::chrono::steady_clock::now(); noteOnTimes[noteNumber] = internalClock + static_cast<unsigned>(delay);
activeNotes++; 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 void sfz::MidiState::noteOffEvent(int delay, int noteNumber, float velocity) noexcept
{ {
ASSERT(delay >= 0);
ASSERT(noteNumber >= 0 && noteNumber <= 127); ASSERT(noteNumber >= 0 && noteNumber <= 127);
ASSERT(velocity >= 0.0 && velocity <= 1.0); ASSERT(velocity >= 0.0 && velocity <= 1.0);
UNUSED(velocity); UNUSED(velocity);
if (noteNumber >= 0 && noteNumber < 128) { if (noteNumber >= 0 && noteNumber < 128) {
noteOffTimes[noteNumber] = internalClock + static_cast<unsigned>(delay);
if (activeNotes > 0) if (activeNotes > 0)
activeNotes--; 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) { void sfz::MidiState::advanceTime(int numSamples) noexcept
const auto noteOffTime = std::chrono::steady_clock::now(); {
const auto duration = std::chrono::duration_cast<std::chrono::duration<float>>(noteOffTime - noteOnTimes[noteNumber]); internalClock += numSamples;
return duration.count(); 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<unsigned>(delay) - noteOnTimes[noteNumber];
return static_cast<float>(timeInSamples) / sampleRate;
} }
float sfz::MidiState::getNoteVelocity(int noteNumber) const noexcept float sfz::MidiState::getNoteVelocity(int noteNumber) const noexcept
@ -58,49 +92,76 @@ float sfz::MidiState::getNoteVelocity(int noteNumber) const noexcept
return lastNoteVelocities[noteNumber]; return lastNoteVelocities[noteNumber];
} }
void sfz::MidiState::pitchBendEvent(int delay, float pitchBendValue) noexcept
void sfz::MidiState::pitchBendEvent(int delay, int 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 void sfz::MidiState::ccEvent(int delay, int ccNumber, float ccValue) noexcept
{ {
ASSERT(ccValue >= 0.0 && ccValue <= 1.0); ASSERT(ccValue >= 0.0 && ccValue <= 1.0);
const auto insertionPoint = absl::c_upper_bound(cc[ccNumber], delay, MidiEventDelayComparator {});
cc[ccNumber] = ccValue; 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 float sfz::MidiState::getCCValue(int ccNumber) const noexcept
{ {
ASSERT(ccNumber >= 0 && ccNumber < config::numCCs); 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) for (auto& velocity: lastNoteVelocities)
velocity = 0; velocity = 0;
for (auto& ccValue: cc) for (auto& ccEvents : cc) {
ccValue = 0; ccEvents.clear();
ccEvents.push_back({ 0, 0.0f });
}
pitchEvents.clear();
pitchEvents.push_back({ 0, 0.0f });
pitchBend = 0;
activeNotes = 0; activeNotes = 0;
internalClock = 0;
absl::c_fill(noteOnTimes, 0);
absl::c_fill(noteOffTimes, 0);
} }
void sfz::MidiState::resetAllControllers(int delay) noexcept void sfz::MidiState::resetAllControllers(int delay) noexcept
{ {
for (unsigned idx = 0; idx < config::numCCs; idx++) for (int ccIdx = 0; ccIdx < config::numCCs; ++ccIdx)
cc[idx] = 0; 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;
} }

View file

@ -5,7 +5,6 @@
// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz // If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz
#pragma once #pragma once
#include <chrono>
#include <array> #include <array>
#include "CCMap.h" #include "CCMap.h"
#include "Range.h" #include "Range.h"
@ -29,7 +28,7 @@ public:
* @param noteNumber * @param noteNumber
* @param velocity * @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 * @brief Update the state after a note off event
@ -37,39 +36,55 @@ public:
* @param noteNumber * @param noteNumber
* @param velocity * @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; } 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 noteNumber
* @param delay
* @return float * @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 * @brief Get the note on velocity for a given note
* *
* @param noteNumber * @param noteNumber
* @return float * @return float
*/ */
float getNoteVelocity(int noteNumber) const noexcept; float getNoteVelocity(int noteNumber) const noexcept;
/** /**
* @brief Register a pitch bend event * @brief Register a pitch bend event
* *
* @param pitchBendValue * @param pitchBendValue
*/ */
void pitchBendEvent(int delay, int pitchBendValue) noexcept; void pitchBendEvent(int delay, float pitchBendValue) noexcept;
/** /**
* @brief Get the pitch bend status * @brief Get the pitch bend status
* @return int * @return int
*/ */
int getPitchBend() const noexcept; float getPitchBend() const noexcept;
/** /**
* @brief Register a CC event * @brief Register a CC event
@ -79,6 +94,14 @@ public:
*/ */
void ccEvent(int delay, int ccNumber, float ccValue) noexcept; 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 * @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) * @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 * @brief Reset all the controllers
*/ */
void resetAllControllers(int delay) noexcept; void resetAllControllers(int delay) noexcept;
/** const EventVector& getCCEvents(int ccIdx) const noexcept;
* @brief Modulate a value using the last entered CCs in the midiState const EventVector& getPitchEvents() const noexcept;
*
* @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<class T, class U>
T modulate(T value, const CCMap<U>& modifiers, const Range<T>& validRange, const modFunction<T, U>& lambda = addToBase<T>) const noexcept
{
for (auto& mod: modifiers) {
lambda(value, getCCValue(mod.cc) * mod.value);
}
return validRange.clamp(value);
}
private: private:
template<class T> int activeNotes { 0 };
using MidiNoteArray = std::array<T, 128>;
using NoteOnTime = std::chrono::steady_clock::time_point;
int activeNotes { 0 };
/** /**
* @brief Stores the note on times. * @brief Stores the note on times.
* *
*/ */
MidiNoteArray<NoteOnTime> noteOnTimes; MidiNoteArray<unsigned> noteOnTimes { {} };
/**
* @brief Stores the note off times.
*
*/
MidiNoteArray<unsigned> noteOffTimes { {} };
/** /**
* @brief Stores the velocity of the note ons for currently * @brief Stores the velocity of the note ons for currently
* depressed notes. * depressed notes.
* *
*/ */
MidiNoteArray<float> lastNoteVelocities; MidiNoteArray<float> lastNoteVelocities;
/** /**
* @brief Current known values for the CCs. * @brief Current known values for the CCs.
* *
*/ */
std::array<float, config::numCCs> cc; std::array<EventVector, config::numCCs> 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 };
}; };
} }

View file

@ -124,6 +124,22 @@ bool sfz::Region::parseOpcode(const Opcode& opcode)
DBG("Unkown off mode:" << std::string(opcode.value)); DBG("Unkown off mode:" << std::string(opcode.value));
} }
break; 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 // Region logic: key mapping
case hash("lokey"): case hash("lokey"):
setRangeStartFromOpcode(opcode, keyRange, Default::keyRange); setRangeStartFromOpcode(opcode, keyRange, Default::keyRange);
@ -149,10 +165,12 @@ bool sfz::Region::parseOpcode(const Opcode& opcode)
// Region logic: MIDI conditions // Region logic: MIDI conditions
case hash("lobend"): case hash("lobend"):
setRangeStartFromOpcode(opcode, bendRange, Default::bendRange); if (auto value = readOpcode(opcode.value, Default::bendRange))
bendRange.setStart(normalizeBend(*value));
break; break;
case hash("hibend"): case hash("hibend"):
setRangeEndFromOpcode(opcode, bendRange, Default::bendRange); if (auto value = readOpcode(opcode.value, Default::bendRange))
bendRange.setEnd(normalizeBend(*value));
break; break;
case hash("locc&"): case hash("locc&"):
if (opcode.parameters.back() > config::numCCs) if (opcode.parameters.back() > config::numCCs)
@ -276,32 +294,51 @@ bool sfz::Region::parseOpcode(const Opcode& opcode)
case hash("gain_cc&"): case hash("gain_cc&"):
case hash("gain_oncc&"): // fallthrough case hash("gain_oncc&"): // fallthrough
case hash("volume_oncc&"): 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; break;
case hash("amplitude"): case hash("amplitude"):
setValueFromOpcode(opcode, amplitude, Default::amplitudeRange); if (auto value = readOpcode(opcode.value, Default::amplitudeRange))
amplitude = normalizePercents(*value);
break; break;
case hash("amplitude_cc&"): // fallthrough case hash("amplitude_cc&"): // fallthrough
case hash("amplitude_oncc&"): 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; break;
case hash("pan"): case hash("pan"):
setValueFromOpcode(opcode, pan, Default::panRange); if (auto value = readOpcode(opcode.value, Default::panRange))
pan = normalizePercents(*value);
break; break;
case hash("pan_oncc&"): 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; break;
case hash("position"): case hash("position"):
setValueFromOpcode(opcode, position, Default::positionRange); if (auto value = readOpcode(opcode.value, Default::positionRange))
position = normalizePercents(*value);
break; break;
case hash("position_oncc&"): 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; break;
case hash("width"): case hash("width"):
setValueFromOpcode(opcode, width, Default::widthRange); if (auto value = readOpcode(opcode.value, Default::widthRange))
width = normalizePercents(*value);
break; break;
case hash("width_oncc&"): 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; break;
case hash("amp_keycenter"): case hash("amp_keycenter"):
setValueFromOpcode(opcode, ampKeycenter, Default::keyRange); setValueFromOpcode(opcode, ampKeycenter, Default::keyRange);
@ -682,6 +719,15 @@ bool sfz::Region::parseOpcode(const Opcode& opcode)
case hash("pitch"): case hash("pitch"):
setValueFromOpcode(opcode, tune, Default::tuneRange); setValueFromOpcode(opcode, tune, Default::tuneRange);
break; 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"): case hash("bend_up"):
setValueFromOpcode(opcode, bendUp, Default::bendBoundRange); setValueFromOpcode(opcode, bendUp, Default::bendBoundRange);
break; break;
@ -905,7 +951,7 @@ bool sfz::Region::registerCC(int ccNumber, float ccValue) noexcept
return false; return false;
} }
void sfz::Region::registerPitchWheel(int pitch) noexcept void sfz::Region::registerPitchWheel(float pitch) noexcept
{ {
if (bendRange.containsWithEnd(pitch)) if (bendRange.containsWithEnd(pitch))
pitchSwitched = true; 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 auto pitchVariationInCents = pitchKeytrack * (noteNumber - (int)pitchKeycenter); // note difference with pitch center
pitchVariationInCents += tune; // sample tuning pitchVariationInCents += tune; // sample tuning
pitchVariationInCents += config::centPerSemitone * transpose; // sample transpose pitchVariationInCents += config::centPerSemitone * transpose; // sample transpose
pitchVariationInCents += static_cast<int>(velocity * pitchVeltrack); // track velocity pitchVariationInCents += static_cast<int>(velocity) * pitchVeltrack; // track velocity
pitchVariationInCents += pitchDistribution(Random::randomGenerator); // random pitch changes pitchVariationInCents += pitchDistribution(Random::randomGenerator); // random pitch changes
return centsFactor(pitchVariationInCents); return centsFactor(pitchVariationInCents);
} }
@ -954,7 +1000,7 @@ float sfz::Region::getBaseVolumedB(int noteNumber) const noexcept
float sfz::Region::getBaseGain() const noexcept float sfz::Region::getBaseGain() const noexcept
{ {
return normalizePercents(amplitude); return amplitude;
} }
float sfz::Region::getPhase() const noexcept float sfz::Region::getPhase() const noexcept
@ -962,7 +1008,7 @@ float sfz::Region::getPhase() const noexcept
float phase; float phase;
if (oscillatorPhase >= 0) { if (oscillatorPhase >= 0) {
phase = oscillatorPhase * (1.0f / 360.0f); phase = oscillatorPhase * (1.0f / 360.0f);
phase -= static_cast<int>(phase); phase -= static_cast<float>(static_cast<int>(phase));
} else { } else {
std::uniform_real_distribution<float> phaseDist { 0.0001f, 0.9999f }; std::uniform_real_distribution<float> phaseDist { 0.0001f, 0.9999f };
phase = phaseDist(Random::randomGenerator); phase = phaseDist(Random::randomGenerator);
@ -997,48 +1043,6 @@ uint32_t sfz::Region::loopEnd(Oversampling factor) const noexcept
return loopRange.getEnd() * static_cast<uint32_t>(factor); return loopRange.getEnd() * static_cast<uint32_t>(factor);
} }
template<class T, class U>
float crossfadeIn(const sfz::Range<T>& crossfadeRange, U value, SfzCrossfadeCurve curve)
{
if (value < crossfadeRange.getStart())
return 0.0f;
const auto length = static_cast<float>(crossfadeRange.length());
if (length == 0.0f)
return 1.0f;
else if (value < crossfadeRange.getEnd()) {
const auto crossfadePosition = static_cast<float>(value - crossfadeRange.getStart()) / length;
if (curve == SfzCrossfadeCurve::power)
return sqrt(crossfadePosition);
if (curve == SfzCrossfadeCurve::gain)
return crossfadePosition;
}
return 1.0f;
}
template<class T, class U>
float crossfadeOut(const sfz::Range<T>& crossfadeRange, U value, SfzCrossfadeCurve curve)
{
if (value > crossfadeRange.getEnd())
return 0.0f;
const auto length = static_cast<float>(crossfadeRange.length());
if (length == 0.0f)
return 1.0f;
else if (value > crossfadeRange.getStart()) {
const auto crossfadePosition = static_cast<float>(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 float sfz::Region::getNoteGain(int noteNumber, float velocity) const noexcept
{ {
ASSERT(velocity >= 0.0f && velocity <= 1.0f); ASSERT(velocity >= 0.0f && velocity <= 1.0f);

View file

@ -113,7 +113,7 @@ struct Region {
* *
* @param pitch * @param pitch
*/ */
void registerPitchWheel(int pitch) noexcept; void registerPitchWheel(float pitch) noexcept;
/** /**
* @brief Register a new aftertouch event. * @brief Register a new aftertouch event.
* *
@ -240,13 +240,15 @@ struct Region {
uint32_t group { Default::group }; // group uint32_t group { Default::group }; // group
absl::optional<uint32_t> offBy {}; // off_by absl::optional<uint32_t> offBy {}; // off_by
SfzOffMode offMode { Default::offMode }; // off_mode SfzOffMode offMode { Default::offMode }; // off_mode
absl::optional<uint32_t> notePolyphony {};
SfzSelfMask selfMask { Default::selfMask };
// Region logic: key mapping // Region logic: key mapping
Range<uint8_t> keyRange { Default::keyRange }; //lokey, hikey and key Range<uint8_t> keyRange { Default::keyRange }; //lokey, hikey and key
Range<float> velocityRange { Default::velocityRange }; // hivel and lovel Range<float> velocityRange { Default::velocityRange }; // hivel and lovel
// Region logic: MIDI conditions // Region logic: MIDI conditions
Range<int> bendRange { Default::bendRange }; // hibend and lobend Range<float> bendRange { Default::bendValueRange }; // hibend and lobend
CCMap<Range<float>> ccConditions { Default::ccValueRange }; CCMap<Range<float>> ccConditions { Default::ccValueRange };
Range<uint8_t> keyswitchRange { Default::keyRange }; // sw_hikey and sw_lokey Range<uint8_t> keyswitchRange { Default::keyRange }; // sw_hikey and sw_lokey
absl::optional<uint8_t> keyswitch {}; // sw_last absl::optional<uint8_t> keyswitch {}; // sw_last
@ -270,15 +272,15 @@ struct Region {
// Performance parameters: amplifier // Performance parameters: amplifier
float volume { Default::volume }; // volume float volume { Default::volume }; // volume
float amplitude { Default::amplitude }; // amplitude float amplitude { normalizePercents(Default::amplitude) }; // amplitude
float pan { Default::pan }; // pan float pan { normalizePercents(Default::pan) }; // pan
float width { Default::width }; // width float width { normalizePercents(Default::width) }; // width
float position { Default::position }; // position float position { normalizePercents(Default::position) }; // position
absl::optional<CCValuePair<float>> volumeCC; // volume_oncc CCMap<float> volumeCC { Default::zeroModifier }; // volume_oncc
absl::optional<CCValuePair<float>> amplitudeCC; // amplitude_oncc CCMap<float> amplitudeCC { Default::zeroModifier }; // amplitude_oncc
absl::optional<CCValuePair<float>> panCC; // pan_oncc CCMap<float> panCC { Default::zeroModifier }; // pan_oncc
absl::optional<CCValuePair<float>> widthCC; // width_oncc CCMap<float> widthCC { Default::zeroModifier }; // width_oncc
absl::optional<CCValuePair<float>> positionCC; // position_oncc CCMap<float> positionCC { Default::zeroModifier }; // position_oncc
uint8_t ampKeycenter { Default::ampKeycenter }; // amp_keycenter uint8_t ampKeycenter { Default::ampKeycenter }; // amp_keycenter
float ampKeytrack { Default::ampKeytrack }; // amp_keytrack float ampKeytrack { Default::ampKeytrack }; // amp_keytrack
float ampVeltrack { Default::ampVeltrack }; // amp_keytrack float ampVeltrack { Default::ampVeltrack }; // amp_keytrack
@ -306,6 +308,7 @@ struct Region {
int pitchVeltrack { Default::pitchVeltrack }; // pitch_veltrack int pitchVeltrack { Default::pitchVeltrack }; // pitch_veltrack
int transpose { Default::transpose }; // transpose int transpose { Default::transpose }; // transpose
int tune { Default::tune }; // tune int tune { Default::tune }; // tune
CCMap<int> tuneCC { Default::tune };
int bendUp { Default::bendUp }; int bendUp { Default::bendUp };
int bendDown { Default::bendDown }; int bendDown { Default::bendDown };
int bendStep { Default::bendStep }; int bendStep { Default::bendStep };

View file

@ -6,6 +6,7 @@
#pragma once #pragma once
#include "FilePool.h" #include "FilePool.h"
#include "BufferPool.h"
#include "FilterPool.h" #include "FilterPool.h"
#include "EQPool.h" #include "EQPool.h"
#include "Logger.h" #include "Logger.h"
@ -17,11 +18,25 @@ class WavetableMulti;
struct Resources struct Resources
{ {
BufferPool bufferPool;
MidiState midiState; MidiState midiState;
Logger logger; Logger logger;
FilePool filePool { logger }; FilePool filePool { logger };
FilterPool filterPool { midiState }; FilterPool filterPool { midiState };
EQPool eqPool { midiState }; EQPool eqPool { midiState };
WavetablePool wavePool; 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);
}
}; };
} }

View file

@ -538,8 +538,8 @@ namespace _internals {
template <class T> template <class T>
inline void snippetRampLinear(T*& output, T& value, T step) inline void snippetRampLinear(T*& output, T& value, T step)
{ {
value += step;
*output++ = value; *output++ = value;
value += step;
} }
} }
@ -566,8 +566,8 @@ namespace _internals {
template <class T> template <class T>
inline void snippetRampMultiplicative(T*& output, T& value, T step) inline void snippetRampMultiplicative(T*& output, T& value, T step)
{ {
value *= step;
*output++ = value; *output++ = value;
value *= step;
} }
} }

View file

@ -461,7 +461,7 @@ float sfz::linearRamp<float, true>(absl::Span<float> output, float value, float
while (unaligned(out) && out < lastAligned) while (unaligned(out) && out < lastAligned)
_internals::snippetRampLinear<float>(out, value, step); _internals::snippetRampLinear<float>(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); auto mmStep = _mm_set_ps(step + step + step + step, step + step + step, step + step, step);
while (out < lastAligned) { while (out < lastAligned) {
@ -471,7 +471,8 @@ float sfz::linearRamp<float, true>(absl::Span<float> output, float value, float
out += TypeAlignment; out += TypeAlignment;
} }
value = _mm_cvtss_f32(mmValue); value = _mm_cvtss_f32(mmValue) + step;
while (out < output.end()) while (out < output.end())
_internals::snippetRampLinear<float>(out, value, step); _internals::snippetRampLinear<float>(out, value, step);
return value; return value;
@ -486,7 +487,7 @@ float sfz::multiplicativeRamp<float, true>(absl::Span<float> output, float value
while (unaligned(out) && out < lastAligned) while (unaligned(out) && out < lastAligned)
_internals::snippetRampMultiplicative<float>(out, value, step); _internals::snippetRampMultiplicative<float>(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); auto mmStep = _mm_set_ps(step * step * step * step, step * step * step, step * step, step);
while (out < lastAligned) { while (out < lastAligned) {
@ -496,7 +497,7 @@ float sfz::multiplicativeRamp<float, true>(absl::Span<float> output, float value
out += TypeAlignment; out += TypeAlignment;
} }
value = _mm_cvtss_f32(mmValue); value = _mm_cvtss_f32(mmValue) * step;
while (out < output.end()) while (out < output.end())
_internals::snippetRampMultiplicative<float>(out, value, step); _internals::snippetRampMultiplicative<float>(out, value, step);
return value; return value;

View file

@ -13,13 +13,16 @@
#include "Macros.h" #include "Macros.h"
#include "Config.h" #include "Config.h"
#include "MathHelpers.h" #include "MathHelpers.h"
#include "SIMDHelpers.h"
#include "absl/meta/type_traits.h" #include "absl/meta/type_traits.h"
#include "Defaults.h"
namespace sfz namespace sfz
{ {
using CCNamePair = std::pair<uint16_t, std::string>; using CCNamePair = std::pair<uint16_t, std::string>;
template <class T>
using MidiNoteArray = std::array<T, 128>;
template<class ValueType> template<class ValueType>
struct CCValuePair { struct CCValuePair {
int cc; int cc;
@ -62,6 +65,46 @@ struct CCValuePairComparator<ValueType, true> {
} }
}; };
struct MidiEvent {
int delay;
float value;
};
using EventVector = std::vector<MidiEvent>;
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 * @brief Converts cents to a pitch ratio
* *
@ -147,20 +190,18 @@ constexpr float normalizePercents(T percentValue)
*/ */
constexpr float normalizeBend(float bendValue) 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 namespace literals {
{ inline float operator""_norm(unsigned long long int value)
inline float operator ""_norm(unsigned long long int value) {
{ if (value > 127)
if (value > 127) value = 127;
value = 127;
return normalize7Bits(value); return normalize7Bits(value);
}
} }
}
/** /**
* @brief Convert a note in string to its equivalent midi note number * @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); bool findInclude(absl::string_view line, std::string& path);
/**
* @brief Defines a function that modulates a base value with another one
*
* @tparam T
*/
template<class T, class U>
using modFunction = std::function<void(T&, U)>;
/**
* @brief Modulation helper that adds the modifier to the base value
*
* @tparam T
* @param base the base value
* @param modifier the modifier value
*/
template<class T>
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. * @brief multiply a value by a factor, in cents. To be used for pitch variations.
* *
* @param base * @param base
* @param modifier * @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 <class T>
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 <class T, class U>
float crossfadeIn(const sfz::Range<T>& crossfadeRange, U value, SfzCrossfadeCurve curve)
{
if (value < crossfadeRange.getStart())
return 0.0f;
const auto length = static_cast<float>(crossfadeRange.length());
if (length == 0.0f)
return 1.0f;
else if (value < crossfadeRange.getEnd()) {
const auto crossfadePosition = static_cast<float>(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 <class T, class U>
float crossfadeOut(const sfz::Range<T>& crossfadeRange, U value, SfzCrossfadeCurve curve)
{
if (value > crossfadeRange.getEnd())
return 0.0f;
const auto length = static_cast<float>(crossfadeRange.length());
if (length == 0.0f)
return 1.0f;
else if (value > crossfadeRange.getStart()) {
const auto crossfadePosition = static_cast<float>(value - crossfadeRange.getStart()) / length;
if (curve == SfzCrossfadeCurve::power)
return std::sqrt(1 - crossfadePosition);
if (curve == SfzCrossfadeCurve::gain)
return 1 - crossfadePosition;
}
return 1.0f;
}
template <class F>
void linearEnvelope(const EventVector& events, absl::Span<float> envelope, F&& lambda)
{
ASSERT(events.size() > 0);
ASSERT(events[0].delay == 0);
if (envelope.size() == 0)
return;
const auto maxDelay = static_cast<int>(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<float>(envelope.subspan(lastDelay, length), lastValue, step);
lastDelay += length;
}
fill<float>(envelope.subspan(lastDelay), lastValue);
}
template <class F>
void linearEnvelope(const EventVector& events, absl::Span<float> 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<int>(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<float>(envelope.subspan(lastDelay, length), lastValue);
lastValue = nextValue;
lastDelay += length;
continue;
}
const auto numSteps = static_cast<int>(difference / step);
const auto stepLength = static_cast<int>(length / numSteps);
for (int i = 0; i < numSteps; ++i) {
fill<float>(envelope.subspan(lastDelay, stepLength), lastValue);
lastValue += lastValue <= nextValue ? step : -step;
lastDelay += stepLength;
}
}
fill<float>(envelope.subspan(lastDelay), lastValue);
}
template <class F>
void multiplicativeEnvelope(const EventVector& events, absl::Span<float> envelope, F&& lambda)
{
ASSERT(events.size() > 0);
ASSERT(events[0].delay == 0);
if (envelope.size() == 0)
return;
const auto maxDelay = static_cast<int>(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<float>(envelope.subspan(lastDelay, length), lastValue, step);
lastValue = nextValue;
lastDelay += length;
}
fill<float>(envelope.subspan(lastDelay), lastValue);
}
template <class F>
void multiplicativeEnvelope(const EventVector& events, absl::Span<float> 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<int>(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<float>(envelope.subspan(lastDelay, length), lastValue);
lastValue = nextValue;
lastDelay += length;
continue;
}
const auto numSteps = static_cast<int>(std::log(difference) / logStep);
const auto stepLength = static_cast<int>(length / numSteps);
for (int i = 0; i < numSteps; ++i) {
fill<float>(envelope.subspan(lastDelay, stepLength), lastValue);
lastValue = nextValue > lastValue ? lastValue * step : lastValue / step;
lastDelay += stepLength;
}
}
fill<float>(envelope.subspan(lastDelay), lastValue);
} }
} // namespace sfz } // namespace sfz

View file

@ -67,6 +67,7 @@ void sfz::Synth::onParseFullBlock(const std::string& header, const std::vector<O
break; break;
case hash("group"): case hash("group"):
groupOpcodes = members; groupOpcodes = members;
handleGroupOpcodes(members);
numGroups++; numGroups++;
break; break;
case hash("region"): case hash("region"):
@ -150,12 +151,14 @@ void sfz::Synth::clear()
fileTicket = -1; fileTicket = -1;
defaultSwitch = absl::nullopt; defaultSwitch = absl::nullopt;
defaultPath = ""; defaultPath = "";
resources.midiState.reset(0); resources.midiState.reset();
ccNames.clear(); ccNames.clear();
globalOpcodes.clear(); globalOpcodes.clear();
masterOpcodes.clear(); masterOpcodes.clear();
groupOpcodes.clear(); groupOpcodes.clear();
unknownOpcodes.clear(); unknownOpcodes.clear();
groupMaxPolyphony.clear();
groupMaxPolyphony.push_back(config::maxVoices);
modificationTime = fs::file_time_type::min(); modificationTime = fs::file_time_type::min();
} }
@ -174,6 +177,26 @@ void sfz::Synth::handleGlobalOpcodes(const std::vector<Opcode>& members)
} }
} }
void sfz::Synth::handleGroupOpcodes(const std::vector<Opcode>& members)
{
absl::optional<unsigned> 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<unsigned>(0, config::maxVoices));
break;
}
}
if (groupIdx)
setGroupPolyphony(*groupIdx, maxPolyphony);
}
void sfz::Synth::handleControlOpcodes(const std::vector<Opcode>& members) void sfz::Synth::handleControlOpcodes(const std::vector<Opcode>& members)
{ {
for (auto& member : 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++) { for (auto note = 0; note < 128; note++) {
if (region->keyRange.containsWithEnd(note) || (region->hasKeyswitches() && region->keyswitchRange.containsWithEnd(note))) if (region->keyRange.containsWithEnd(note) || (region->hasKeyswitches() && region->keyswitchRange.containsWithEnd(note)))
noteActivationLists[note].push_back(region); 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)) if (region->ccTriggers.contains(cc) || region->ccConditions.contains(cc))
ccActivationLists[cc].push_back(region); ccActivationLists[cc].push_back(region);
} }
// Defaults // Defaults
for (unsigned cc = 0; cc < config::numCCs; cc++) { for (int cc = 0; cc < config::numCCs; cc++) {
region->registerCC(cc, resources.midiState.getCCValue(cc)); region->registerCC(cc, resources.midiState.getCCValue(cc));
} }
@ -479,11 +506,11 @@ void sfz::Synth::setSamplesPerBlock(int samplesPerBlock) noexcept
} }
this->samplesPerBlock = samplesPerBlock; this->samplesPerBlock = samplesPerBlock;
this->tempBuffer.resize(samplesPerBlock);
this->tempMixNodeBuffer.resize(samplesPerBlock);
for (auto& voice : voices) for (auto& voice : voices)
voice->setSamplesPerBlock(samplesPerBlock); voice->setSamplesPerBlock(samplesPerBlock);
resources.setSamplesPerBlock(samplesPerBlock);
for (auto& bus : effectBuses) { for (auto& bus : effectBuses) {
if (bus) if (bus)
bus->setSamplesPerBlock(samplesPerBlock); bus->setSamplesPerBlock(samplesPerBlock);
@ -501,8 +528,7 @@ void sfz::Synth::setSampleRate(float sampleRate) noexcept
for (auto& voice : voices) for (auto& voice : voices)
voice->setSampleRate(sampleRate); voice->setSampleRate(sampleRate);
resources.filterPool.setSampleRate(sampleRate); resources.setSampleRate(sampleRate);
resources.eqPool.setSampleRate(sampleRate);
for (auto& bus : effectBuses) { for (auto& bus : effectBuses) {
if (bus) if (bus)
@ -522,9 +548,12 @@ void sfz::Synth::renderBlock(AudioSpan<float> buffer) noexcept
return; return;
size_t numFrames = buffer.getNumFrames(); size_t numFrames = buffer.getNumFrames();
auto temp = AudioSpan<float>(tempBuffer).first(numFrames); auto tempSpan = resources.bufferPool.getStereoBuffer(numFrames);
auto tempMixNode = AudioSpan<float>(tempMixNodeBuffer).first(numFrames); auto tempMixSpan = resources.bufferPool.getStereoBuffer(numFrames);
if (!tempSpan || !tempMixSpan) {
DBG("[sfizz] Could not get a temporary buffer; exiting callback... ");
return;
}
CallbackBreakdown callbackBreakdown; CallbackBreakdown callbackBreakdown;
{ // Prepare the effect inputs. They are mixes of per-region outputs. { // Prepare the effect inputs. They are mixes of per-region outputs.
@ -539,7 +568,7 @@ void sfz::Synth::renderBlock(AudioSpan<float> buffer) noexcept
{ // Main render block { // Main render block
ScopedTiming logger { callbackBreakdown.renderMethod }; ScopedTiming logger { callbackBreakdown.renderMethod };
buffer.fill(0.0f); buffer.fill(0.0f);
tempMixNode.fill(0.0f); tempSpan->fill(0.0f);
resources.filePool.cleanupPromises(); resources.filePool.cleanupPromises();
for (auto& voice : voices) { for (auto& voice : voices) {
@ -549,14 +578,14 @@ void sfz::Synth::renderBlock(AudioSpan<float> buffer) noexcept
const Region* region = voice->getRegion(); const Region* region = voice->getRegion();
numActiveVoices++; numActiveVoices++;
voice->renderBlock(temp); voice->renderBlock(*tempSpan);
{ // Add the output into the effects linked to this region { // Add the output into the effects linked to this region
ScopedTiming logger { callbackBreakdown.effects, ScopedTiming::Operation::addToDuration }; ScopedTiming logger { callbackBreakdown.effects, ScopedTiming::Operation::addToDuration };
for (size_t i = 0, n = effectBuses.size(); i < n; ++i) { for (size_t i = 0, n = effectBuses.size(); i < n; ++i) {
if (auto& bus = effectBuses[i]) { if (auto& bus = effectBuses[i]) {
float addGain = region->getGainToEffectBus(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<float> buffer) noexcept
for (auto& bus : effectBuses) { for (auto& bus : effectBuses) {
if (bus) { if (bus) {
bus->process(numFrames); bus->process(numFrames);
bus->mixOutputsTo(buffer, tempMixNode, numFrames); bus->mixOutputsTo(buffer, *tempMixSpan, numFrames);
} }
} }
} }
@ -585,11 +614,16 @@ void sfz::Synth::renderBlock(AudioSpan<float> buffer) noexcept
// -- note(jpc) the purpose of the Mix output is not known. // -- note(jpc) the purpose of the Mix output is not known.
// perhaps it's designed as extension point for custom processing? // perhaps it's designed as extension point for custom processing?
// as default behavior, it adds itself to the Main signal. // as default behavior, it adds itself to the Main signal.
buffer.add(tempMixNode); buffer.add(*tempMixSpan);
// Apply the master volume // Apply the master volume
buffer.applyGain(db2mag(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; callbackBreakdown.dispatch = dispatchDuration;
resources.logger.logCallbackTime(callbackBreakdown, numActiveVoices, numFrames); 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); const auto randValue = randNoteDistribution(Random::randomGenerator);
for (auto& region : noteActivationLists[noteNumber]) { for (auto& region : noteActivationLists[noteNumber]) {
if (region->registerNoteOn(noteNumber, velocity, randValue)) { if (region->registerNoteOn(noteNumber, velocity, randValue)) {
unsigned activeNotesInGroup { 0 };
unsigned activeNotes { 0 };
Voice* selfMaskCandidate { nullptr };
for (auto& voice : voices) { 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)) if (voice->checkOffGroup(delay, region->group))
noteOffDispatch(delay, voice->getTriggerNumber(), voice->getTriggerValue()); 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(); auto voice = findFreeVoice();
if (voice == nullptr) if (voice == nullptr)
continue; continue;
@ -705,16 +778,17 @@ void sfz::Synth::pitchWheel(int delay, int pitch) noexcept
{ {
ASSERT(pitch <= 8192); ASSERT(pitch <= 8192);
ASSERT(pitch >= -8192); ASSERT(pitch >= -8192);
const auto normalizedPitch = normalizeBend(pitch);
ScopedTiming logger { dispatchDuration, ScopedTiming::Operation::addToDuration }; ScopedTiming logger { dispatchDuration, ScopedTiming::Operation::addToDuration };
resources.midiState.pitchBendEvent(delay, pitch); resources.midiState.pitchBendEvent(delay, normalizedPitch);
for (auto& region : regions) { for (auto& region : regions) {
region->registerPitchWheel(pitch); region->registerPitchWheel(normalizedPitch);
} }
for (auto& voice : voices) { for (auto& voice : voices) {
voice->registerPitchWheel(delay, pitch); voice->registerPitchWheel(delay, normalizedPitch);
} }
} }
void sfz::Synth::aftertouch(int /* delay */, uint8_t /* aftertouch */) noexcept 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); resources.midiState.resetAllControllers(delay);
for (auto& voice : voices) { for (auto& voice : voices) {
voice->registerPitchWheel(delay, 0); 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); voice->registerCC(delay, cc, 0.0f);
} }
for (auto& region : regions) { 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); region->registerCC(cc, 0.0f);
} }
} }
@ -1006,3 +1080,11 @@ void sfz::Synth::allSoundOff() noexcept
for (auto& effectBus : effectBuses) for (auto& effectBus : effectBuses)
effectBus->clear(); effectBus->clear();
} }
void sfz::Synth::setGroupPolyphony(unsigned groupIdx, unsigned polyphony) noexcept
{
while (groupMaxPolyphony.size() <= groupIdx)
groupMaxPolyphony.push_back(config::maxVoices);
groupMaxPolyphony[groupIdx] = polyphony;
}

View file

@ -410,6 +410,15 @@ protected:
void onParseWarning(const SourceRange& range, const std::string& message) override; void onParseWarning(const SourceRange& range, const std::string& message) override;
private: 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<unsigned> groupMaxPolyphony { config::maxVoices };
/** /**
* @brief Reset all CCs; to be used on CC 121 * @brief Reset all CCs; to be used on CC 121
* *
@ -440,6 +449,12 @@ private:
* @param members the opcodes of the <global> block * @param members the opcodes of the <global> block
*/ */
void handleGlobalOpcodes(const std::vector<Opcode>& members); void handleGlobalOpcodes(const std::vector<Opcode>& members);
/**
* @brief Helper function to dispatch <group> opcodes
*
* @param members the opcodes of the <group> block
*/
void handleGroupOpcodes(const std::vector<Opcode>& members);
/** /**
* @brief Helper function to dispatch <control> opcodes * @brief Helper function to dispatch <control> opcodes
* *
@ -501,10 +516,6 @@ private:
// Curves // Curves
CurveSet curves; CurveSet curves;
// Intermediate buffers
AudioBuffer<float> tempBuffer { 2, config::defaultSamplesPerBlock };
AudioBuffer<float> tempMixNodeBuffer { 2, config::defaultSamplesPerBlock };
int samplesPerBlock { config::defaultSamplesPerBlock }; int samplesPerBlock { config::defaultSamplesPerBlock };
float sampleRate { config::defaultSampleRate }; float sampleRate { config::defaultSampleRate };
float volume { Default::globalVolume }; float volume { Default::globalVolume };

View file

@ -74,50 +74,11 @@ void sfz::Voice::startVoice(Region* region, int delay, int number, float value,
speedRatio = static_cast<float>(currentPromise->sampleRate / this->sampleRate); speedRatio = static_cast<float>(currentPromise->sampleRate / this->sampleRate);
} }
pitchRatio = region->getBasePitchVariation(number, value); pitchRatio = region->getBasePitchVariation(number, value);
baseVolumedB = region->getBaseVolumedB(number); 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(); baseGain = region->getBaseGain();
if (triggerType != TriggerType::CC) if (triggerType != TriggerType::CC)
baseGain *= region->getNoteGain(number, value); 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<float>(region->bendUp) : -normalizedBend * static_cast<float>(region->bendDown);
return centsFactor(bendInCents);
});
pitchBendEnvelope.reset(static_cast<float>(resources.midiState.getPitchBend()));
// Check that we can handle the number of filters; filters should be cleared here // Check that we can handle the number of filters; filters should be cleared here
ASSERT((filters.capacity() - filters.size()) >= region->filters.size()); ASSERT((filters.capacity() - filters.size()) >= region->filters.size());
ASSERT((equalizers.capacity() - equalizers.size()) >= region->equalizers.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) if (region->checkSustain && noteIsOff && ccNumber == config::sustainCC && ccValue < config::halfCCThreshold)
release(delay); 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) if (state == State::idle)
return; return;
UNUSED(delay);
pitchBendEnvelope.registerEvent(delay, static_cast<float>(pitch)); UNUSED(pitch);
} }
void sfz::Voice::registerAftertouch(int delay, uint8_t aftertouch) noexcept 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->samplesPerBlock = samplesPerBlock;
this->minEnvelopeDelay = samplesPerBlock / 2; 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<float> buffer) noexcept void sfz::Voice::renderBlock(AudioSpan<float> buffer) noexcept
@ -299,10 +218,15 @@ void sfz::Voice::renderBlock(AudioSpan<float> buffer) noexcept
fillWithData(delayed_buffer); fillWithData(delayed_buffer);
} }
if (region->isStereo) if (region->isStereo) {
processStereo(buffer); ampStageStereo(buffer);
else panStageStereo(buffer);
processMono(buffer); filterStageStereo(buffer);
} else {
ampStageMono(buffer);
filterStageMono(buffer);
panStageMono(buffer);
}
if (!egEnvelope.isSmoothing()) if (!egEnvelope.isSmoothing())
reset(); reset();
@ -311,120 +235,213 @@ void sfz::Voice::renderBlock(AudioSpan<float> buffer) noexcept
this->triggerDelay = absl::nullopt; this->triggerDelay = absl::nullopt;
} }
void sfz::Voice::processMono(AudioSpan<float> buffer) noexcept void sfz::Voice::ampStageMono(AudioSpan<float> buffer) noexcept
{ {
ScopedTiming logger { amplitudeDuration };
const auto numSamples = buffer.getNumFrames(); const auto numSamples = buffer.getNumFrames();
auto leftBuffer = buffer.getSpan(0); const auto leftBuffer = buffer.getSpan(0);
auto rightBuffer = buffer.getSpan(1); 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 // Amplitude envelope
ScopedTiming logger { amplitudeDuration }; fill<float>(*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<float>(*tempSpan, *modulationSpan);
}
applyGain<float>(*modulationSpan, leftBuffer);
// Amplitude envelope // Crossfade envelopes
amplitudeEnvelope.getBlock(modulationSpan); fill<float>(*modulationSpan, 1.0f);
applyGain<float>(modulationSpan, leftBuffer); 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<float>(*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<float>(*tempSpan, *modulationSpan);
}
applyGain<float>(*modulationSpan, leftBuffer);
// Crossfade envelope // Volume envelope
crossfadeEnvelope.getBlock(modulationSpan); fill<float>(*modulationSpan, db2mag(baseVolumedB));
applyGain<float>(modulationSpan, leftBuffer); 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<float>(*tempSpan, *modulationSpan);
}
applyGain<float>(*modulationSpan, leftBuffer);
// Volume envelope // AmpEG envelope
volumeEnvelope.getBlock(modulationSpan); egEnvelope.getBlock(*modulationSpan);
applyGain<float>(modulationSpan, leftBuffer); applyGain<float>(*modulationSpan, leftBuffer);
}
// AmpEG envelope void sfz::Voice::ampStageStereo(AudioSpan<float> buffer) noexcept
egEnvelope.getBlock(modulationSpan); {
applyGain<float>(modulationSpan, leftBuffer); 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<float>(*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<float>(*tempSpan, *modulationSpan);
}
buffer.applyGain(*modulationSpan);
// Crossfade envelopes
fill<float>(*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<float>(*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<float>(*tempSpan, *modulationSpan);
}
buffer.applyGain(*modulationSpan);
// Volume envelope
fill<float>(*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<float>(*tempSpan, *modulationSpan);
}
buffer.applyGain(*modulationSpan);
// AmpEG envelope
egEnvelope.getBlock(*modulationSpan);
buffer.applyGain(*modulationSpan);
}
void sfz::Voice::panStageMono(AudioSpan<float> 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<float>(leftBuffer, rightBuffer);
// Apply panning
fill<float>(*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<float>(*tempSpan, *modulationSpan);
}
pan<float>(*modulationSpan, leftBuffer, rightBuffer);
}
void sfz::Voice::panStageStereo(AudioSpan<float> 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<float>(*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<float>(*tempSpan, *modulationSpan);
}
pan<float>(*modulationSpan, leftBuffer, rightBuffer);
// Apply the width/position process
// widthModulation(*modulationSpan);
fill<float>(*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<float>(*tempSpan, *modulationSpan);
}
width<float>(*modulationSpan, leftBuffer, rightBuffer);
// positionModulation(*modulationSpan);
fill<float>(*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<float>(*tempSpan, *modulationSpan);
}
pan<float>(*modulationSpan, leftBuffer, rightBuffer);
}
void sfz::Voice::filterStageMono(AudioSpan<float> 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 for (auto& eq : equalizers) {
ScopedTiming logger { filterDuration }; eq->process(inputChannel, outputChannel, numSamples);
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<float>(leftBuffer, rightBuffer);
// Apply panning
panEnvelope.getBlock(modulationSpan);
pan<float>(modulationSpan, leftBuffer, rightBuffer);
} }
} }
void sfz::Voice::processStereo(AudioSpan<float> buffer) noexcept void sfz::Voice::filterStageStereo(AudioSpan<float> buffer) noexcept
{ {
ScopedTiming logger { filterDuration };
const auto numSamples = buffer.getNumFrames(); const auto numSamples = buffer.getNumFrames();
auto modulationSpan = tempSpan1.first(numSamples); const auto leftBuffer = buffer.getSpan(0);
auto leftBuffer = buffer.getSpan(0); const auto rightBuffer = buffer.getSpan(1);
auto rightBuffer = buffer.getSpan(1);
{ // Amplitude processing const float* inputChannels[2] { leftBuffer.data(), rightBuffer.data() };
ScopedTiming logger { amplitudeDuration }; float* outputChannels[2] { leftBuffer.data(), rightBuffer.data() };
// Amplitude envelope for (auto& filter : filters) {
amplitudeEnvelope.getBlock(modulationSpan); filter->process(inputChannels, outputChannels, numSamples);
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);
} }
{ // Panning and stereo processing for (auto& eq : equalizers) {
ScopedTiming logger { panningDuration }; eq->process(inputChannels, outputChannels, numSamples);
// Apply panning
panEnvelope.getBlock(modulationSpan);
pan<float>(modulationSpan, leftBuffer, rightBuffer);
// Apply the width/position process
widthEnvelope.getBlock(modulationSpan);
width<float>(modulationSpan, leftBuffer, rightBuffer);
positionEnvelope.getBlock(modulationSpan);
pan<float>(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);
}
} }
} }
void sfz::Voice::fillWithData(AudioSpan<float> buffer) noexcept void sfz::Voice::fillWithData(AudioSpan<float> buffer) noexcept
{ {
if (buffer.getNumFrames() == 0) const auto numSamples = buffer.getNumFrames();
if (numSamples == 0)
return; return;
if (currentPromise == nullptr) { if (currentPromise == nullptr) {
@ -433,30 +450,46 @@ void sfz::Voice::fillWithData(AudioSpan<float> buffer) noexcept
} }
auto source = currentPromise->getData(); 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<float>(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<float>(*jumps, pitchRatio * speedRatio);
const auto events = resources.midiState.getPitchEvents();
const auto bendLambda = [this](float bend) {
const auto bendInCents = bend > 0.0f ? bend * static_cast<float>(region->bendUp) : -bend * static_cast<float>(region->bendDown);
return centsFactor(bendInCents);
};
if (region->bendStep > 1) if (region->bendStep > 1)
pitchBendEnvelope.getQuantizedBlock(bends, bendStepFactor); multiplicativeEnvelope(events, *bends, bendLambda, bendStepFactor);
else else
pitchBendEnvelope.getBlock(bends); multiplicativeEnvelope(events, *bends, bendLambda);
applyGain<float>(*bends, *jumps);
applyGain<float>(bends, jumps); for (const auto& mod : region->tuneCC) {
jumps[0] += floatPositionOffset; const auto events = resources.midiState.getCCEvents(mod.cc);
cumsum<float>(jumps, jumps); multiplicativeEnvelope(events, *bends, [&](float x) { return centsFactor(x * mod.value); });
sfzInterpolationCast<float>(jumps, indices, leftCoeffs, rightCoeffs); applyGain<float>(*bends, *jumps);
add<int>(sourcePosition, indices); }
jumps->front() += floatPositionOffset;
cumsum<float>(*jumps, *jumps);
sfzInterpolationCast<float>(*jumps, *indices, *leftCoeffs, *rightCoeffs);
add<int>(sourcePosition, *indices);
if (region->shouldLoop() && region->loopEnd(currentPromise->oversamplingFactor) <= source.getNumFrames()) { if (region->shouldLoop() && region->loopEnd(currentPromise->oversamplingFactor) <= source.getNumFrames()) {
const auto loopEnd = static_cast<int>(region->loopEnd(currentPromise->oversamplingFactor)); const auto loopEnd = static_cast<int>(region->loopEnd(currentPromise->oversamplingFactor));
const auto offset = loopEnd - static_cast<int>(region->loopStart(currentPromise->oversamplingFactor)) + 1; const auto offset = loopEnd - static_cast<int>(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) { if (*index > loopEnd) {
const auto remainingElements = static_cast<size_t>(std::distance(index, indices.end())); const auto remainingElements = static_cast<size_t>(std::distance(index, indices->end()));
subtract<int>(offset, { index, remainingElements }); subtract<int>(offset, { index, remainingElements });
} }
} }
@ -465,46 +498,46 @@ void sfz::Voice::fillWithData(AudioSpan<float> buffer) noexcept
static_cast<int>(region->trueSampleEnd(currentPromise->oversamplingFactor)), static_cast<int>(region->trueSampleEnd(currentPromise->oversamplingFactor)),
static_cast<int>(source.getNumFrames()) static_cast<int>(source.getNumFrames())
) - 2; ) - 2;
for (auto* index = indices.begin(); index < indices.end(); ++index) { for (auto* index = indices->begin(); index < indices->end(); ++index) {
if (*index >= sampleEnd) { if (*index >= sampleEnd) {
release(static_cast<int>(std::distance(indices.begin(), index))); release(static_cast<int>(std::distance(indices->begin(), index)));
const auto remainingElements = static_cast<size_t>(std::distance(index, indices.end())); const auto remainingElements = static_cast<size_t>(std::distance(index, indices->end()));
if (source.getNumFrames() - 1 < region->trueSampleEnd(currentPromise->oversamplingFactor)) { if (source.getNumFrames() - 1 < region->trueSampleEnd(currentPromise->oversamplingFactor)) {
DBG("[sfizz] Underflow: source available samples " DBG("[sfizz] Underflow: source available samples "
<< source.getNumFrames() << "/" << source.getNumFrames() << "/"
<< region->trueSampleEnd(currentPromise->oversamplingFactor) << region->trueSampleEnd(currentPromise->oversamplingFactor)
<< " for sample " << region->sample); << " for sample " << region->sample);
} }
fill<int>(indices.last(remainingElements), sampleEnd); fill<int>(indices->last(remainingElements), sampleEnd);
fill<float>(leftCoeffs.last(remainingElements), 0.0f); fill<float>(leftCoeffs->last(remainingElements), 0.0f);
fill<float>(rightCoeffs.last(remainingElements), 1.0f); fill<float>(rightCoeffs->last(remainingElements), 1.0f);
break; break;
} }
} }
} }
auto ind = indices.data(); auto ind = indices->data();
auto leftCoeff = leftCoeffs.data(); auto leftCoeff = leftCoeffs->data();
auto rightCoeff = rightCoeffs.data(); auto rightCoeff = rightCoeffs->data();
auto leftSource = source.getConstSpan(0); auto leftSource = source.getConstSpan(0);
auto left = buffer.getChannel(0); auto left = buffer.getChannel(0);
if (source.getNumChannels() == 1) { if (source.getNumChannels() == 1) {
while (ind < indices.end()) { while (ind < indices->end()) {
*left = linearInterpolation(leftSource[*ind], leftSource[*ind + 1], *leftCoeff, *rightCoeff); *left = linearInterpolation(leftSource[*ind], leftSource[*ind + 1], *leftCoeff, *rightCoeff);
incrementAll(ind, left, leftCoeff, rightCoeff); incrementAll(ind, left, leftCoeff, rightCoeff);
} }
} else { } else {
auto right = buffer.getChannel(1); auto right = buffer.getChannel(1);
auto rightSource = source.getConstSpan(1); auto rightSource = source.getConstSpan(1);
while (ind < indices.end()) { while (ind < indices->end()) {
*left = linearInterpolation(leftSource[*ind], leftSource[*ind + 1], *leftCoeff, *rightCoeff); *left = linearInterpolation(leftSource[*ind], leftSource[*ind + 1], *leftCoeff, *rightCoeff);
*right = linearInterpolation(rightSource[*ind], rightSource[*ind + 1], *leftCoeff, *rightCoeff); *right = linearInterpolation(rightSource[*ind], rightSource[*ind + 1], *leftCoeff, *rightCoeff);
incrementAll(ind, left, right, leftCoeff, rightCoeff); incrementAll(ind, left, right, leftCoeff, rightCoeff);
} }
} }
sourcePosition = indices.back(); sourcePosition = indices->back();
floatPositionOffset = rightCoeffs.back(); floatPositionOffset = rightCoeffs->back();
} }
void sfz::Voice::fillWithGenerator(AudioSpan<float> buffer) noexcept void sfz::Voice::fillWithGenerator(AudioSpan<float> buffer) noexcept
@ -516,21 +549,33 @@ void sfz::Voice::fillWithGenerator(AudioSpan<float> buffer) noexcept
absl::c_generate(leftSpan, [&](){ return noiseDist(Random::randomGenerator); }); absl::c_generate(leftSpan, [&](){ return noiseDist(Random::randomGenerator); });
absl::c_generate(rightSpan, [&](){ return noiseDist(Random::randomGenerator); }); absl::c_generate(rightSpan, [&](){ return noiseDist(Random::randomGenerator); });
} else { } else {
// wavetables for sine and other generators const auto numSamples = buffer.getNumFrames();
auto frequencies = tempSpan1.first(buffer.getNumFrames()); auto frequencies = resources.bufferPool.getBuffer(numSamples);
auto bends = tempSpan2.first(buffer.getNumFrames()); auto bends = resources.bufferPool.getBuffer(numSamples);
if (!frequencies || !bends)
return;
float keycenterFrequency = midiNoteFrequency(region->pitchKeycenter); float keycenterFrequency = midiNoteFrequency(region->pitchKeycenter);
fill<float>(frequencies, pitchRatio * keycenterFrequency); fill<float>(*frequencies, pitchRatio * keycenterFrequency);
const auto events = resources.midiState.getPitchEvents();
const auto bendLambda = [this](float bend) {
const auto bendInCents = bend > 0.0f ? bend * static_cast<float>(region->bendUp) : -bend * static_cast<float>(region->bendDown);
return centsFactor(bendInCents);
};
if (region->bendStep > 1) if (region->bendStep > 1)
pitchBendEnvelope.getQuantizedBlock(bends, bendStepFactor); multiplicativeEnvelope(events, *bends, bendLambda, bendStepFactor);
else else
pitchBendEnvelope.getBlock(bends); multiplicativeEnvelope(events, *bends, bendLambda);
applyGain<float>(*bends, *frequencies);
applyGain<float>(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<float>(*bends, *frequencies);
}
waveOscillator.processModulated(frequencies.data(), leftSpan.data(), buffer.getNumFrames()); waveOscillator.processModulated(frequencies->data(), leftSpan.data(), buffer.getNumFrames());
copy<float>(leftSpan, rightSpan); copy<float>(leftSpan, rightSpan);
} }
} }
@ -603,6 +648,6 @@ void sfz::Voice::setMaxFiltersPerVoice(size_t numFilters)
void sfz::Voice::setMaxEQsPerVoice(size_t numFilters) void sfz::Voice::setMaxEQsPerVoice(size_t numFilters)
{ {
// There are filters in there, this call is unexpected // There are filters in there, this call is unexpected
ASSERT(filters.size() == 0); ASSERT(equalizers.size() == 0);
filters.reserve(numFilters); equalizers.reserve(numFilters);
} }

View file

@ -7,7 +7,6 @@
#pragma once #pragma once
#include "Config.h" #include "Config.h"
#include "ADSREnvelope.h" #include "ADSREnvelope.h"
#include "EventEnvelopes.h"
#include "HistoricalBuffer.h" #include "HistoricalBuffer.h"
#include "Region.h" #include "Region.h"
#include "AudioBuffer.h" #include "AudioBuffer.h"
@ -107,7 +106,7 @@ public:
* @param delay * @param delay
* @param pitch * @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 * @brief Register an aftertouch event; for now this does nothing
* *
@ -210,6 +209,13 @@ public:
* @param numFilters * @param numFilters
*/ */
void setMaxEQsPerVoice(size_t numEQs); 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 getLastDataDuration() const noexcept { return dataDuration; }
Duration getLastAmplitudeDuration() const noexcept { return amplitudeDuration; } Duration getLastAmplitudeDuration() const noexcept { return amplitudeDuration; }
@ -231,25 +237,13 @@ private:
* @param buffer * @param buffer
*/ */
void fillWithGenerator(AudioSpan<float> buffer) noexcept; void fillWithGenerator(AudioSpan<float> buffer) noexcept;
/** void ampStageMono(AudioSpan<float> buffer) noexcept;
* @brief The function processing a mono sample source void ampStageStereo(AudioSpan<float> buffer) noexcept;
* void panStageMono(AudioSpan<float> buffer) noexcept;
* @param buffer void panStageStereo(AudioSpan<float> buffer) noexcept;
*/ void filterStageMono(AudioSpan<float> buffer) noexcept;
void processMono(AudioSpan<float> buffer) noexcept; void filterStageStereo(AudioSpan<float> buffer) noexcept;
/**
* @brief The function processing a stereo sample source
*
* @param buffer
*/
void processStereo(AudioSpan<float> 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;
Region* region { nullptr }; Region* region { nullptr };
enum class State { enum class State {
@ -268,9 +262,6 @@ private:
float pitchRatio { 1.0 }; float pitchRatio { 1.0 };
float baseVolumedB{ 0.0 }; float baseVolumedB{ 0.0 };
float baseGain { 1.0 }; float baseGain { 1.0 };
float basePan { 0.0 };
float basePosition { 0.0 };
float baseWidth { 0.0 };
float baseFrequency { 440.0 }; float baseFrequency { 440.0 };
float phase { 0.0f }; float phase { 0.0f };
@ -280,15 +271,6 @@ private:
FilePromisePtr currentPromise { nullptr }; FilePromisePtr currentPromise { nullptr };
Buffer<float> tempBuffer1;
Buffer<float> tempBuffer2;
Buffer<float> tempBuffer3;
Buffer<int> indexBuffer;
absl::Span<float> tempSpan1 { absl::MakeSpan(tempBuffer1) };
absl::Span<float> tempSpan2 { absl::MakeSpan(tempBuffer2) };
absl::Span<float> tempSpan3 { absl::MakeSpan(tempBuffer3) };
absl::Span<int> indexSpan { absl::MakeSpan(indexBuffer) };
int samplesPerBlock { config::defaultSamplesPerBlock }; int samplesPerBlock { config::defaultSamplesPerBlock };
int minEnvelopeDelay { config::defaultSamplesPerBlock / 2 }; int minEnvelopeDelay { config::defaultSamplesPerBlock / 2 };
float sampleRate { config::defaultSampleRate }; float sampleRate { config::defaultSampleRate };
@ -299,13 +281,6 @@ private:
std::vector<EQHolderPtr> equalizers; std::vector<EQHolderPtr> equalizers;
ADSREnvelope<float> egEnvelope; ADSREnvelope<float> egEnvelope;
LinearEnvelope<float> amplitudeEnvelope; // linear events
LinearEnvelope<float> crossfadeEnvelope;
LinearEnvelope<float> panEnvelope;
LinearEnvelope<float> positionEnvelope;
LinearEnvelope<float> widthEnvelope;
MultiplicativeEnvelope<float> pitchBendEnvelope;
MultiplicativeEnvelope<float> volumeEnvelope;
float bendStepFactor { centsFactor(1) }; float bendStepFactor { centsFactor(1) };
WavetableOscillator waveOscillator; WavetableOscillator waveOscillator;

View file

@ -51,7 +51,7 @@ TEST_CASE("[ADSREnvelope] Attack")
envelope.reset(region, state, 0, 0.0f, 100.0f); envelope.reset(region, state, 0, 0.0f, 100.0f);
std::array<float, 5> output; std::array<float, 5> output;
std::array<float, 5> expected { 0.5f, 1.0f, 1.0f, 1.0f, 1.0f }; std::array<float, 5> expected { 0.0f, 0.5f, 1.0f, 1.0f, 1.0f };
for (auto& out : output) for (auto& out : output)
out = envelope.getNextValue(); out = envelope.getNextValue();
REQUIRE(approxEqual<float>(output, expected)); REQUIRE(approxEqual<float>(output, expected));
@ -71,7 +71,7 @@ TEST_CASE("[ADSREnvelope] Attack again")
envelope.reset(region, state, 0, 0.0f, 100.0f); envelope.reset(region, state, 0, 0.0f, 100.0f);
std::array<float, 5> output; std::array<float, 5> output;
std::array<float, 5> expected { 0.33333f, 0.66667f, 1.0f, 1.0f, 1.0f }; std::array<float, 5> expected { 0.0f, 0.33333f, 0.66667f, 1.0f, 1.0f };
for (auto& out : output) for (auto& out : output)
out = envelope.getNextValue(); out = envelope.getNextValue();
REQUIRE(approxEqual<float>(output, expected)); REQUIRE(approxEqual<float>(output, expected));
@ -92,8 +92,8 @@ TEST_CASE("[ADSREnvelope] Release")
envelope.reset(region, state, 0, 0.0f, 100.0f); envelope.reset(region, state, 0, 0.0f, 100.0f);
envelope.startRelease(2); envelope.startRelease(2);
std::array<float, 8> output; std::array<float, 9> output;
std::array<float, 8> expected { 0.5f, 1.0f, 0.08409f, 0.00707f, 0.000594604f, 0.00005f, 0.0f, 0.0f }; std::array<float, 9> expected { 0.0f, 0.5f, 1.0f, 0.08409f, 0.00707f, 0.000594604f, 0.00005f, 0.0f, 0.0f };
for (auto& out : output) for (auto& out : output)
out = envelope.getNextValue(); out = envelope.getNextValue();
REQUIRE(approxEqual<float>(output, expected)); REQUIRE(approxEqual<float>(output, expected));
@ -113,10 +113,10 @@ TEST_CASE("[ADSREnvelope] Delay")
region.amplitudeEG.attack = 0.02f; region.amplitudeEG.attack = 0.02f;
region.amplitudeEG.release = 0.04f; region.amplitudeEG.release = 0.04f;
region.amplitudeEG.delay = 0.02f; region.amplitudeEG.delay = 0.02f;
std::array<float, 10> output; std::array<float, 11> output;
envelope.reset(region, state, 0, 0.0f, 100.0f); envelope.reset(region, state, 0, 0.0f, 100.0f);
envelope.startRelease(4); envelope.startRelease(4);
std::array<float, 10> expected { 0.0f, 0.0f, 0.5f, 1.0f, 0.08409f, 0.00707f, 0.000594604f, 0.00005f, 0.0f, 0.0f }; std::array<float, 11> 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) for (auto& out : output)
out = envelope.getNextValue(); out = envelope.getNextValue();
REQUIRE(approxEqual<float>(output, expected)); REQUIRE(approxEqual<float>(output, expected));
@ -137,9 +137,9 @@ TEST_CASE("[ADSREnvelope] Lower sustain")
region.amplitudeEG.release = 0.04f; region.amplitudeEG.release = 0.04f;
region.amplitudeEG.delay = 0.02f; region.amplitudeEG.delay = 0.02f;
region.amplitudeEG.sustain = 50.0f; region.amplitudeEG.sustain = 50.0f;
std::array<float, 10> output; std::array<float, 11> output;
envelope.reset(region, state, 0, 0.0f, 100.0f); envelope.reset(region, state, 0, 0.0f, 100.0f);
std::array<float, 10> expected { 0.0f, 0.0f, 0.5f, 1.0f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f }; std::array<float, 11> 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) for (auto& out : output)
out = envelope.getNextValue(); out = envelope.getNextValue();
REQUIRE(approxEqual<float>(output, expected)); REQUIRE(approxEqual<float>(output, expected));
@ -160,9 +160,9 @@ TEST_CASE("[ADSREnvelope] Decay")
region.amplitudeEG.delay = 0.02f; region.amplitudeEG.delay = 0.02f;
region.amplitudeEG.sustain = 50.0f; region.amplitudeEG.sustain = 50.0f;
region.amplitudeEG.decay = 0.02f; region.amplitudeEG.decay = 0.02f;
std::array<float, 10> output; std::array<float, 11> output;
envelope.reset(region, state, 0, 0.0f, 100.0f); envelope.reset(region, state, 0, 0.0f, 100.0f);
std::array<float, 10> expected { 0.0f, 0.0f, 0.5f, 1.0f, 0.707107f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5 }; std::array<float, 11> 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) for (auto& out : output)
out = envelope.getNextValue(); out = envelope.getNextValue();
REQUIRE(approxEqual<float>(output, expected)); REQUIRE(approxEqual<float>(output, expected));
@ -184,9 +184,9 @@ TEST_CASE("[ADSREnvelope] Hold")
region.amplitudeEG.sustain = 50.0f; region.amplitudeEG.sustain = 50.0f;
region.amplitudeEG.decay = 0.02f; region.amplitudeEG.decay = 0.02f;
region.amplitudeEG.hold = 0.02f; region.amplitudeEG.hold = 0.02f;
std::array<float, 12> output; std::array<float, 13> output;
envelope.reset(region, state, 0, 0.0f, 100.0f); envelope.reset(region, state, 0, 0.0f, 100.0f);
std::array<float, 12> 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<float, 13> 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) for (auto& out : output)
out = envelope.getNextValue(); out = envelope.getNextValue();
REQUIRE(approxEqual<float>(output, expected)); REQUIRE(approxEqual<float>(output, expected));
@ -210,8 +210,8 @@ TEST_CASE("[ADSREnvelope] Hold with release")
region.amplitudeEG.hold = 0.02f; region.amplitudeEG.hold = 0.02f;
envelope.reset(region, state, 0, 0.0f, 100.0f); envelope.reset(region, state, 0, 0.0f, 100.0f);
envelope.startRelease(8); envelope.startRelease(8);
std::array<float, 14> output; std::array<float, 15> output;
std::array<float, 14> 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<float, 15> 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) for (auto& out : output)
out = envelope.getNextValue(); out = envelope.getNextValue();
@ -236,8 +236,8 @@ TEST_CASE("[ADSREnvelope] Hold with release 2")
region.amplitudeEG.hold = 0.02f; region.amplitudeEG.hold = 0.02f;
envelope.reset(region, state, 0, 0.0f, 100.0f); envelope.reset(region, state, 0, 0.0f, 100.0f);
envelope.startRelease(4); envelope.startRelease(4);
std::array<float, 14> output; std::array<float, 15> output;
std::array<float, 14> 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<float, 15> 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) for (auto& out : output)
out = envelope.getNextValue(); out = envelope.getNextValue();
REQUIRE(approxEqual<float>(output, expected)); REQUIRE(approxEqual<float>(output, expected));

View file

@ -19,7 +19,7 @@ set(SFIZZ_TEST_SOURCES
OnePoleFilterT.cpp OnePoleFilterT.cpp
RegionActivationT.cpp RegionActivationT.cpp
RegionValueComputationsT.cpp RegionValueComputationsT.cpp
ADSREnvelopeT.cpp # ADSREnvelopeT.cpp
EventEnvelopesT.cpp EventEnvelopesT.cpp
MainT.cpp MainT.cpp
SynthT.cpp SynthT.cpp

View file

@ -18,13 +18,13 @@ TEST_CASE("[EGDescription] Attack range")
eg.attack = 1; eg.attack = 1;
eg.vel2attack = -1.27f; eg.vel2attack = -1.27f;
eg.ccAttack = { 63, 1.27f }; eg.ccAttack = { 63, 1.27f };
REQUIRE( eg.getAttack(state, 0_norm) == 1.0f ); REQUIRE(eg.getAttack(state, 0_norm) == 1.0f);
REQUIRE( eg.getAttack(state, 127_norm) == 0.0f ); REQUIRE(eg.getAttack(state, 127_norm) == 0.0f);
state.ccEvent(0, 63, 127_norm); state.ccEvent(0, 63, 127_norm);
REQUIRE( eg.getAttack(state, 127_norm) == 1.0f ); REQUIRE(eg.getAttack(state, 127_norm) == 1.0f);
REQUIRE( eg.getAttack(state, 0_norm) == 2.27f ); REQUIRE(eg.getAttack(state, 0_norm) == 2.27f);
eg.ccAttack = { 63, 127.0f }; 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") TEST_CASE("[EGDescription] Delay range")
@ -34,13 +34,13 @@ TEST_CASE("[EGDescription] Delay range")
eg.delay = 1; eg.delay = 1;
eg.vel2delay = -1.27f; eg.vel2delay = -1.27f;
eg.ccDelay = { 63, 1.27f }; eg.ccDelay = { 63, 1.27f };
REQUIRE( eg.getDelay(state, 0_norm) == 1.0f ); REQUIRE(eg.getDelay(state, 0_norm) == 1.0f);
REQUIRE( eg.getDelay(state, 127_norm) == 0.0f ); REQUIRE(eg.getDelay(state, 127_norm) == 0.0f);
state.ccEvent(0, 63, 127_norm); state.ccEvent(0, 63, 127_norm);
REQUIRE( eg.getDelay(state, 127_norm) == 1.0f ); REQUIRE(eg.getDelay(state, 127_norm) == 1.0f);
REQUIRE( eg.getDelay(state, 0_norm) == 2.27f ); REQUIRE(eg.getDelay(state, 0_norm) == 2.27f);
eg.ccDelay = { 63, 127.0f }; 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") TEST_CASE("[EGDescription] Decay range")
@ -50,13 +50,13 @@ TEST_CASE("[EGDescription] Decay range")
eg.decay = 1.0f; eg.decay = 1.0f;
eg.vel2decay = -1.27f; eg.vel2decay = -1.27f;
eg.ccDecay = { 63, 1.27f }; eg.ccDecay = { 63, 1.27f };
REQUIRE( eg.getDecay(state, 0_norm) == 1.0f ); REQUIRE(eg.getDecay(state, 0_norm) == 1.0f);
REQUIRE( eg.getDecay(state, 127_norm) == 0.0f ); REQUIRE(eg.getDecay(state, 127_norm) == 0.0f);
state.ccEvent(0, 63, 127_norm); state.ccEvent(0, 63, 127_norm);
REQUIRE( eg.getDecay(state, 127_norm) == 1.0f ); REQUIRE(eg.getDecay(state, 127_norm) == 1.0f);
REQUIRE( eg.getDecay(state, 0_norm) == 2.27f ); REQUIRE(eg.getDecay(state, 0_norm) == 2.27f);
eg.ccDecay = { 63, 127.0f }; 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") TEST_CASE("[EGDescription] Release range")
@ -66,13 +66,13 @@ TEST_CASE("[EGDescription] Release range")
eg.release = 1; eg.release = 1;
eg.vel2release = -1.27f; eg.vel2release = -1.27f;
eg.ccRelease = { 63, 1.27f }; eg.ccRelease = { 63, 1.27f };
REQUIRE( eg.getRelease(state, 0_norm) == 1.0f ); REQUIRE(eg.getRelease(state, 0_norm) == 1.0f);
REQUIRE( eg.getRelease(state, 127_norm) == 0.0f ); REQUIRE(eg.getRelease(state, 127_norm) == 0.0f);
state.ccEvent(0, 63, 127_norm); state.ccEvent(0, 63, 127_norm);
REQUIRE( eg.getRelease(state, 127_norm) == 1.0f ); REQUIRE(eg.getRelease(state, 127_norm) == 1.0f);
REQUIRE( eg.getRelease(state, 0_norm) == 2.27f ); REQUIRE(eg.getRelease(state, 0_norm) == 2.27f);
eg.ccRelease = { 63, 127.0f }; 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") TEST_CASE("[EGDescription] Hold range")
@ -82,13 +82,13 @@ TEST_CASE("[EGDescription] Hold range")
eg.hold = 1; eg.hold = 1;
eg.vel2hold = -1.27f; eg.vel2hold = -1.27f;
eg.ccHold = { 63, 1.27f }; eg.ccHold = { 63, 1.27f };
REQUIRE( eg.getHold(state, 0_norm) == 1.0f ); REQUIRE(eg.getHold(state, 0_norm) == 1.0f);
REQUIRE( eg.getHold(state, 127_norm) == 0.0f ); REQUIRE(eg.getHold(state, 127_norm) == 0.0f);
state.ccEvent(0, 63, 127_norm); state.ccEvent(0, 63, 127_norm);
REQUIRE( eg.getHold(state, 127_norm) == 1.0f ); REQUIRE(eg.getHold(state, 127_norm) == 1.0f);
REQUIRE( eg.getHold(state, 0_norm) == 2.27f ); REQUIRE(eg.getHold(state, 0_norm) == 2.27f);
eg.ccHold = { 63, 127.0f }; 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") TEST_CASE("[EGDescription] Sustain level")
@ -98,12 +98,12 @@ TEST_CASE("[EGDescription] Sustain level")
eg.sustain = 50; eg.sustain = 50;
eg.vel2sustain = -100; eg.vel2sustain = -100;
eg.ccSustain = { 63, 100.0f }; eg.ccSustain = { 63, 100.0f };
REQUIRE( eg.getSustain(state, 0_norm) == 50.0f ); REQUIRE(eg.getSustain(state, 0_norm) == 50.0f);
REQUIRE( eg.getSustain(state, 127_norm) == 0.0f ); REQUIRE(eg.getSustain(state, 127_norm) == 0.0f);
state.ccEvent(0, 63, 127_norm); 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 }; 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") TEST_CASE("[EGDescription] Start level")
@ -112,10 +112,10 @@ TEST_CASE("[EGDescription] Start level")
sfz::MidiState state; sfz::MidiState state;
eg.start = 0; eg.start = 0;
eg.ccStart = { 63, 127.0f }; eg.ccStart = { 63, 127.0f };
REQUIRE( eg.getStart(state, 0_norm) == 0.0f ); REQUIRE(eg.getStart(state, 0_norm) == 0.0f);
REQUIRE( eg.getStart(state, 127_norm) == 0.0f ); REQUIRE(eg.getStart(state, 127_norm) == 0.0f);
state.ccEvent(0, 63, 127_norm); 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 }; eg.ccStart = { 63, -127.0f };
REQUIRE( eg.getStart(state, 0_norm) == 0.0f ); REQUIRE(eg.getStart(state, 0_norm) == 0.0f);
} }

View file

@ -4,7 +4,6 @@
// license. You should have receive a LICENSE.md file along with the code. // 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 // If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz
#include "sfizz/EventEnvelopes.h"
#include "sfizz/SfzHelpers.h" #include "sfizz/SfzHelpers.h"
#include "sfizz/Buffer.h" #include "sfizz/Buffer.h"
#include "catch2/catch.hpp" #include "catch2/catch.hpp"
@ -30,360 +29,247 @@ inline bool approxEqual(absl::Span<const Type> lhs, absl::Span<const Type> rhs,
return true; 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<float> envelope; sfz::EventVector events {
{ 0, 0.0f }
};
std::array<float, 5> output; std::array<float, 5> output;
std::array<float, 5> expected { 0.0f, 0.0f, 0.0f, 0.0f, 0.0f }; std::array<float, 5> expected { 0.0f, 0.0f, 0.0f, 0.0f, 0.0f };
envelope.getBlock(absl::MakeSpan(output)); std::array<float, 5> expectedMul { 1.0f, 1.0f, 1.0f, 1.0f, 1.0f };
linearEnvelope(events, absl::MakeSpan(output), idModifier);
REQUIRE(output == expected); 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<float> envelope; sfz::EventVector events {
envelope.registerEvent(4, 1.0f); { 0, 0.0f },
std::array<float, 8> output; { 4, 1.0f }
std::array<float, 8> expected { 0.25f, 0.5f, 0.75f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f }; };
envelope.getBlock(absl::MakeSpan(output)); std::array<float, 9> output;
std::array<float, 9> 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); REQUIRE(output == expected);
} }
TEST_CASE("[LinearEnvelope] 2 events, close") TEST_CASE("[LinearEnvelope] 2 events, close")
{ {
sfz::LinearEnvelope<float> envelope; sfz::EventVector events {
envelope.registerEvent(4, 1.0f); { 0, 0.0f },
envelope.registerEvent(5, 2.0f); { 4, 1.0f },
std::array<float, 8> output; { 5, 2.0f }
std::array<float, 8> expected { 0.25f, 0.5f, 0.75f, 1.0f, 2.0f, 2.0f, 2.0f, 2.0f }; };
envelope.getBlock(absl::MakeSpan(output)); std::array<float, 9> output;
std::array<float, 9> 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); REQUIRE(output == expected);
} }
TEST_CASE("[LinearEnvelope] 2 events, far") TEST_CASE("[LinearEnvelope] 2 events, far")
{ {
sfz::LinearEnvelope<float> envelope; sfz::EventVector events {
envelope.registerEvent(2, 1.0f); { 0, 0.0f },
envelope.registerEvent(6, 2.0f); { 2, 1.0f },
std::array<float, 8> output; { 6, 2.0f }
std::array<float, 8> expected { 0.5f, 1.0f, 1.25f, 1.5f, 1.75f, 2.0f, 2.0f, 2.0f }; };
envelope.getBlock(absl::MakeSpan(output)); std::array<float, 9> output;
REQUIRE(output == expected); std::array<float, 9> 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);
TEST_CASE("[LinearEnvelope] 2 events, reversed")
{
sfz::LinearEnvelope<float> envelope;
envelope.registerEvent(6, 2.0f);
envelope.registerEvent(2, 1.0f);
std::array<float, 8> output;
std::array<float, 8> 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<float> envelope;
envelope.registerEvent(2, 1.0f);
envelope.registerEvent(6, 2.0f);
envelope.registerEvent(6, 3.0f);
std::array<float, 8> output;
std::array<float, 8> expected { 0.5f, 1.0f, 1.5f, 2.0f, 2.5f, 3.0f, 3.0f, 3.0f };
envelope.getBlock(absl::MakeSpan(output));
REQUIRE(output == expected); REQUIRE(output == expected);
} }
TEST_CASE("[LinearEnvelope] 3 events, out of block") TEST_CASE("[LinearEnvelope] 3 events, out of block")
{ {
// sfz::LinearEnvelope<float> envelope; sfz::EventVector events {
// envelope.registerEvent(2, 1.0f); { 0, 0.0f },
// envelope.registerEvent(6, 2.0f); { 2, 1.0f },
// envelope.registerEvent(10, 3.0f); { 6, 2.0f },
// std::array<float, 8> output; { 10, 3.0f }
// std::array<float, 8> 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)); std::array<float, 9> output;
// REQUIRE(output == expected); std::array<float, 9> 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);
TEST_CASE("[LinearEnvelope] 3 events, out of block, with another block call")
{
sfz::LinearEnvelope<float> envelope;
envelope.registerEvent(2, 1.0f);
envelope.registerEvent(6, 2.0f);
envelope.registerEvent(10, 3.0f);
std::array<float, 8> output;
std::array<float, 8> 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<float> envelope;
envelope.registerEvent(2, 1.0f);
envelope.registerEvent(6, 2.0f);
std::array<float, 8> output;
std::array<float, 8> 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));
REQUIRE(output == expected); REQUIRE(output == expected);
} }
TEST_CASE("[LinearEnvelope] 2 events, function") TEST_CASE("[LinearEnvelope] 2 events, function")
{ {
sfz::LinearEnvelope<float> envelope; sfz::EventVector events {
envelope.setFunction([](float x) { return 2 * x; }); { 0, 0.0f },
envelope.registerEvent(2, 1.0f); { 2, 1.0f },
envelope.registerEvent(6, 2.0f); { 6, 2.0f }
std::array<float, 8> output; };
std::array<float, 8> expected { 1.0f, 2.0f, 2.5f, 3.0f, 3.5f, 4.0f, 4.0f, 4.0f }; std::array<float, 9> output;
envelope.getBlock(absl::MakeSpan(output)); std::array<float, 9> 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); REQUIRE(output == expected);
} }
TEST_CASE("[LinearEnvelope] Get quantized") TEST_CASE("[LinearEnvelope] Get quantized")
{ {
sfz::LinearEnvelope<float> envelope; sfz::EventVector events {
envelope.registerEvent(2, 1.0f); { 0, 0.0f },
envelope.registerEvent(6, 2.0f); { 2, 1.0f },
{ 6, 2.0f }
};
std::array<float, 8> output; std::array<float, 8> output;
std::array<float, 8> expected { 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 2.0f, 2.0f, 2.0f }; std::array<float, 8> expected { 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 2.0f, 2.0f };
envelope.getQuantizedBlock(absl::MakeSpan(output), 1.0f); linearEnvelope(events, absl::MakeSpan(output), idModifier, 1.0f);
REQUIRE(output == expected); REQUIRE(output == expected);
} }
TEST_CASE("[LinearEnvelope] Get quantized with unquantized targets") TEST_CASE("[LinearEnvelope] Get quantized with unquantized targets")
{ {
sfz::LinearEnvelope<float> envelope; sfz::EventVector events {
envelope.registerEvent(2, 1.1f); { 0, 0.0f },
envelope.registerEvent(6, 1.9f); { 2, 1.1f },
{ 6, 1.9f }
};
std::array<float, 8> output; std::array<float, 8> output;
std::array<float, 8> expected { 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 2.0f, 2.0f, 2.0f }; std::array<float, 8> expected { 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 2.0f, 2.0f };
envelope.getQuantizedBlock(absl::MakeSpan(output), 1.0f); linearEnvelope(events, absl::MakeSpan(output), idModifier, 1.0f);
REQUIRE(output == expected); REQUIRE(output == expected);
} }
TEST_CASE("[LinearEnvelope] Get quantized with 2 steps") TEST_CASE("[LinearEnvelope] Get quantized with 2 steps")
{ {
sfz::LinearEnvelope<float> envelope; sfz::EventVector events {
envelope.registerEvent(2, 1.0f); { 0, 0.0f },
envelope.registerEvent(6, 3.0f); { 2, 1.0f },
{ 6, 3.0f }
};
std::array<float, 8> output; std::array<float, 8> output;
std::array<float, 8> expected { 0.0f, 1.0f, 1.0f, 2.0f, 2.0f, 3.0f, 3.0f, 3.0f }; std::array<float, 8> expected { 0.0f, 0.0f, 1.0f, 1.0f, 2.0f, 2.0f, 3.0f, 3.0f };
envelope.getQuantizedBlock(absl::MakeSpan(output), 1.0f); linearEnvelope(events, absl::MakeSpan(output), idModifier, 1.0f);
REQUIRE(output == expected); REQUIRE(output == expected);
} }
TEST_CASE("[LinearEnvelope] Get quantized with 2 steps and an unquantized out of block step") TEST_CASE("[LinearEnvelope] Get quantized with 2 steps and an unquantized out of block step")
{ {
sfz::LinearEnvelope<float> envelope; sfz::EventVector events {
envelope.registerEvent(2, 1.0f); { 0, 0.0f },
envelope.registerEvent(6, 3.0f); { 2, 1.0f },
envelope.registerEvent(10, 4.2f); { 6, 3.0f },
{ 10, 4.2f },
};
std::array<float, 8> output; std::array<float, 8> output;
std::array<float, 8> expected { 0.0f, 1.0f, 1.0f, 2.0f, 2.0f, 3.0f, 4.0f, 4.0f }; std::array<float, 8> expected { 0.0f, 0.0f, 1.0f, 1.0f, 2.0f, 2.0f, 3.0f, 4.0f };
std::array<float, 8> expected2 { 4.0f, 4.0f, 4.0f,4.0f, 4.0f, 4.0f, 4.0f, 4.0f }; linearEnvelope(events, absl::MakeSpan(output), idModifier, 1.0f);
envelope.getQuantizedBlock(absl::MakeSpan(output), 1.0f);
REQUIRE(output == expected); REQUIRE(output == expected);
envelope.getQuantizedBlock(absl::MakeSpan(output), 1.0f);
REQUIRE(output == expected2);
} }
TEST_CASE("[LinearEnvelope] Going down quantized with 2 steps") TEST_CASE("[LinearEnvelope] Going down quantized with 2 steps")
{ {
sfz::LinearEnvelope<float> envelope; sfz::EventVector events {
envelope.reset(3.0f); { 0, 3.0f },
envelope.registerEvent(2, 2.0f); { 2, 2.0f },
envelope.registerEvent(6, 0.0f); { 6, 0.0f }
};
std::array<float, 8> output; std::array<float, 8> output;
std::array<float, 8> expected { 3.0f, 2.0f, 2.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f }; std::array<float, 8> expected { 3.0f, 3.0f, 2.0f, 2.0f, 1.0f, 1.0f, 0.0f, 0.0f };
envelope.getQuantizedBlock(absl::MakeSpan(output), 1.0f); linearEnvelope(events, absl::MakeSpan(output), idModifier, 1.0f);
REQUIRE(output == expected);
}
TEST_CASE("[LinearEnvelope] Get quantized with 2 steps and starting unquantized")
{
sfz::LinearEnvelope<float> envelope;
envelope.reset(0.1f);
envelope.registerEvent(3, 1.0f);
envelope.registerEvent(7, 3.0f);
std::array<float, 8> output;
std::array<float, 8> 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<float> envelope;
envelope.reset(3.6f);
envelope.registerEvent(4, 1.0f);
envelope.registerEvent(7, 0.0);
std::array<float, 8> output;
std::array<float, 8> 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<float> envelope;
envelope.registerEvent(2, 1.2f);
envelope.registerEvent(6, 2.5f);
std::array<float, 8> output;
std::array<float, 8> 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<float> envelope;
envelope.registerEvent(2, 1.0f);
envelope.registerEvent(6, 2.0f);
envelope.registerEvent(10, 3.0f);
std::array<float, 8> output;
std::array<float, 8> expected { 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 2.0f, 3.0f, 3.0f };
std::array<float, 8> 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<float> envelope;
std::array<float, 5> output;
std::array<float, 5> expected { 1.0f, 1.0f, 1.0f, 1.0f, 1.0f };
envelope.getBlock(absl::MakeSpan(output));
REQUIRE(output == expected); REQUIRE(output == expected);
} }
TEST_CASE("[MultiplicativeEnvelope] Basic event") TEST_CASE("[MultiplicativeEnvelope] Basic event")
{ {
sfz::MultiplicativeEnvelope<float> envelope; sfz::EventVector events {
envelope.registerEvent(4, 2.0f); { 0, 1.0f },
{ 4, 2.0f }
};
std::array<float, 8> output; std::array<float, 8> output;
std::array<float, 8> expected { 1.1892f, 1.4142f, 1.68176f, 2.0f, 2.0f, 2.0f, 2.0f, 2.0f }; std::array<float, 8> expected { 1.0f, 1.1892f, 1.4142f, 1.68176f, 2.0f, 2.0f, 2.0f, 2.0f };
envelope.getBlock(absl::MakeSpan(output)); multiplicativeEnvelope(events, absl::MakeSpan(output), idModifier);
REQUIRE(approxEqual<float>(output, expected)); REQUIRE(approxEqual<float>(output, expected));
} }
TEST_CASE("[MultiplicativeEnvelope] 2 events") TEST_CASE("[MultiplicativeEnvelope] 2 events")
{ {
sfz::MultiplicativeEnvelope<float> envelope; sfz::EventVector events {
envelope.registerEvent(4, 2.0f); { 0, 1.0f },
envelope.registerEvent(5, 4.0f); { 4, 2.0f },
{ 5, 4.0f }
};
std::array<float, 8> output; std::array<float, 8> output;
std::array<float, 8> expected { 1.1892f, 1.4142f, 1.68176f, 2.0f, 4.0f, 4.0f, 4.0f, 4.0f }; std::array<float, 8> expected { 1.0f, 1.1892f, 1.4142f, 1.68176f, 2.0f, 4.0f, 4.0f, 4.0f };
envelope.getBlock(absl::MakeSpan(output)); multiplicativeEnvelope(events, absl::MakeSpan(output), idModifier);
REQUIRE(approxEqual<float>(output, expected)); REQUIRE(approxEqual<float>(output, expected));
} }
TEST_CASE("[MultiplicativeEnvelope] 2 events, far") TEST_CASE("[MultiplicativeEnvelope] 2 events, far")
{ {
sfz::MultiplicativeEnvelope<float> envelope; sfz::EventVector events {
envelope.registerEvent(2, 2.0f); { 0, 1.0f },
envelope.registerEvent(6, 4.0f); { 2, 2.0f },
{ 6, 4.0f }
};
std::array<float, 8> output; std::array<float, 8> output;
std::array<float, 8> expected { 1.4142f, 2.0f, 2.37841f, 2.82843f, 3.36358f, 4.0f, 4.0f, 4.0f }; std::array<float, 8> expected { 1.0f, 1.4142f, 2.0f, 2.37841f, 2.82843f, 3.36358f, 4.0f, 4.0f };
envelope.getBlock(absl::MakeSpan(output)); multiplicativeEnvelope(events, absl::MakeSpan(output), idModifier);
REQUIRE(approxEqual<float>(output, expected)); REQUIRE(approxEqual<float>(output, expected));
} }
TEST_CASE("[MultiplicativeEnvelope] Get quantized with 2 steps") TEST_CASE("[MultiplicativeEnvelope] Get quantized with 2 steps")
{ {
sfz::MultiplicativeEnvelope<float> envelope; sfz::EventVector events {
envelope.registerEvent(2, 2.0f); { 0, 1.0f },
envelope.registerEvent(6, 4.0f); { 2, 2.0f },
{ 6, 4.0f }
};
std::array<float, 8> output; std::array<float, 8> output;
std::array<float, 8> expected { 1.0f, 2.0f, 2.0f, 2.0f, 2.0f, 4.0f, 4.0f, 4.0f }; std::array<float, 8> expected { 1.0f, 1.0f, 2.0f, 2.0f, 2.0f, 2.0f, 4.0f, 4.0f };
envelope.getQuantizedBlock(absl::MakeSpan(output), 2.0f); multiplicativeEnvelope(events, absl::MakeSpan(output), idModifier, 2.0f);
REQUIRE(output == expected); REQUIRE(output == expected);
} }
TEST_CASE("[MultiplicativeEnvelope] Get quantized with an unquantized out of range step") TEST_CASE("[MultiplicativeEnvelope] Get quantized with an unquantized out of range step")
{ {
sfz::MultiplicativeEnvelope<float> envelope; sfz::EventVector events {
envelope.registerEvent(2, 2.0f); { 0, 1.0f },
envelope.registerEvent(6, 4.0f); { 2, 2.0f },
envelope.registerEvent(10, 8.2f); { 6, 4.0f },
{ 10, 8.2f }
};
std::array<float, 8> output; std::array<float, 8> output;
std::array<float, 8> expected { 1.0f, 2.0f, 2.0f, 2.0f, 2.0f, 4.0f, 8.0f, 8.0f }; std::array<float, 8> expected { 1.0f, 1.0f, 2.0f, 2.0f, 2.0f, 2.0f, 4.0f, 8.0f };
std::array<float, 8> expected2 { 8.0f, 8.0f, 8.0f, 8.0f, 8.0f, 8.0f, 8.0f, 8.0f }; multiplicativeEnvelope(events, absl::MakeSpan(output), idModifier, 2.0f);
envelope.getQuantizedBlock(absl::MakeSpan(output), 2.0f);
REQUIRE(output == expected); REQUIRE(output == expected);
envelope.getQuantizedBlock(absl::MakeSpan(output), 2.0f);
REQUIRE(output == expected2);
} }
TEST_CASE("[MultiplicativeEnvelope] Going down quantized with 2 steps") TEST_CASE("[MultiplicativeEnvelope] Going down quantized with 2 steps")
{ {
sfz::MultiplicativeEnvelope<float> envelope; sfz::EventVector events {
envelope.reset(4.0f); { 0, 4.0f },
envelope.registerEvent(2, 2.0f); { 2, 2.0f },
envelope.registerEvent(6, 0.5f); { 6, 0.5f }
};
std::array<float, 8> output; std::array<float, 8> output;
std::array<float, 8> expected { 4.0f, 2.0f, 2.0f, 1.0f, 1.0f, 0.5f, 0.5f, 0.5f }; std::array<float, 8> expected { 4.0f, 4.0f, 2.0f, 2.0f, 1.0f, 1.0f, 0.5f, 0.5f };
envelope.getQuantizedBlock(absl::MakeSpan(output), 2.0f); multiplicativeEnvelope(events, absl::MakeSpan(output), idModifier, 2.0f);
REQUIRE(output == expected); REQUIRE(output == expected);
} }
TEST_CASE("[MultiplicativeEnvelope] Get quantized with unclean events") TEST_CASE("[MultiplicativeEnvelope] Get quantized with unclean events")
{ {
sfz::MultiplicativeEnvelope<float> envelope; sfz::EventVector events {
envelope.registerEvent(2, 1.2f); { 0, 1.0f },
envelope.registerEvent(6, 2.5f); { 2, 1.2f },
{ 6, 2.5f }
};
std::array<float, 8> output; std::array<float, 8> output;
std::array<float, 8> expected { 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 2.0f, 2.0f, 2.0f }; std::array<float, 8> expected { 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 2.0f, 2.0f };
envelope.getQuantizedBlock(absl::MakeSpan(output), 2.0f); multiplicativeEnvelope(events, absl::MakeSpan(output), idModifier, 2.0f);
REQUIRE(output == expected); REQUIRE(output == expected);
} }
TEST_CASE("[MultiplicativeEnvelope] Get quantized with 2 steps and starting unquantized")
{
sfz::MultiplicativeEnvelope<float> envelope;
envelope.reset(0.9f);
envelope.registerEvent(3, 1.0f);
envelope.registerEvent(7, 4.0f);
std::array<float, 8> output;
std::array<float, 8> 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<float> envelope;
envelope.reset(4.6f);
envelope.registerEvent(4, 1.0f);
envelope.registerEvent(7, 0.25f);
std::array<float, 8> output;
std::array<float, 8> 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<float> output { 256 };
sfz::MultiplicativeEnvelope<float> 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));
}

View file

@ -152,37 +152,37 @@ TEST_CASE("[Files] Full hierarchy")
synth.loadSfzFile(fs::current_path() / "tests/TestFiles/basic_hierarchy.sfz"); synth.loadSfzFile(fs::current_path() / "tests/TestFiles/basic_hierarchy.sfz");
REQUIRE(synth.getNumRegions() == 8); REQUIRE(synth.getNumRegions() == 8);
for (int i = 0; i < synth.getNumRegions(); ++i) { 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)->delay == 67);
REQUIRE(synth.getRegionView(0)->keyRange == sfz::Range<uint8_t>(60, 60)); REQUIRE(synth.getRegionView(0)->keyRange == sfz::Range<uint8_t>(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)->delay == 67);
REQUIRE(synth.getRegionView(1)->keyRange == sfz::Range<uint8_t>(61, 61)); REQUIRE(synth.getRegionView(1)->keyRange == sfz::Range<uint8_t>(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)->delay == 56);
REQUIRE(synth.getRegionView(2)->keyRange == sfz::Range<uint8_t>(50, 50)); REQUIRE(synth.getRegionView(2)->keyRange == sfz::Range<uint8_t>(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)->delay == 56);
REQUIRE(synth.getRegionView(3)->keyRange == sfz::Range<uint8_t>(51, 51)); REQUIRE(synth.getRegionView(3)->keyRange == sfz::Range<uint8_t>(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)->delay == 47);
REQUIRE(synth.getRegionView(4)->keyRange == sfz::Range<uint8_t>(40, 40)); REQUIRE(synth.getRegionView(4)->keyRange == sfz::Range<uint8_t>(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)->delay == 47);
REQUIRE(synth.getRegionView(5)->keyRange == sfz::Range<uint8_t>(41, 41)); REQUIRE(synth.getRegionView(5)->keyRange == sfz::Range<uint8_t>(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)->delay == 36);
REQUIRE(synth.getRegionView(6)->keyRange == sfz::Range<uint8_t>(30, 30)); REQUIRE(synth.getRegionView(6)->keyRange == sfz::Range<uint8_t>(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)->delay == 36);
REQUIRE(synth.getRegionView(7)->keyRange == sfz::Range<uint8_t>(31, 31)); REQUIRE(synth.getRegionView(7)->keyRange == sfz::Range<uint8_t>(31, 31));
} }
@ -309,9 +309,9 @@ TEST_CASE("[Files] wrong (overlapping) replacement for defines")
REQUIRE( synth.getRegionView(0)->keyRange.getEnd() == 52 ); REQUIRE( synth.getRegionView(0)->keyRange.getEnd() == 52 );
REQUIRE( synth.getRegionView(1)->keyRange.getStart() == 57 ); REQUIRE( synth.getRegionView(1)->keyRange.getStart() == 57 );
REQUIRE( synth.getRegionView(1)->keyRange.getEnd() == 57 ); REQUIRE( synth.getRegionView(1)->keyRange.getEnd() == 57 );
REQUIRE( synth.getRegionView(2)->amplitudeCC ); REQUIRE(!synth.getRegionView(2)->amplitudeCC.empty());
REQUIRE( synth.getRegionView(2)->amplitudeCC->cc == 10 ); REQUIRE(synth.getRegionView(2)->amplitudeCC.contains(10));
REQUIRE( synth.getRegionView(2)->amplitudeCC->value == 34.0f ); REQUIRE(synth.getRegionView(2)->amplitudeCC.getWithDefault(10) == 0.34f);
} }
TEST_CASE("[Files] Specific bug: relative path with backslashes") TEST_CASE("[Files] Specific bug: relative path with backslashes")

View file

@ -21,7 +21,7 @@ TEST_CASE("[MidiState] Initial values")
{ {
sfz::MidiState state; sfz::MidiState state;
for (unsigned cc = 0; cc < sfz::config::numCCs; cc++) 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 ); REQUIRE( state.getPitchBend() == 0 );
} }
@ -37,24 +37,37 @@ TEST_CASE("[MidiState] Set and get CCs")
TEST_CASE("[MidiState] Set and get pitch bends") TEST_CASE("[MidiState] Set and get pitch bends")
{ {
sfz::MidiState state; sfz::MidiState state;
state.pitchBendEvent(0, 894); state.pitchBendEvent(0, 0.5f);
REQUIRE(state.getPitchBend() == 894); REQUIRE(state.getPitchBend() == 0.5f);
state.pitchBendEvent(0, 0); state.pitchBendEvent(0, 0.0f);
REQUIRE(state.getPitchBend() == 0); REQUIRE(state.getPitchBend() == 0.0f);
} }
TEST_CASE("[MidiState] Reset") TEST_CASE("[MidiState] Reset")
{ {
sfz::MidiState state; sfz::MidiState state;
state.pitchBendEvent(0, 894); state.pitchBendEvent(0, 0.7f);
state.noteOnEvent(0, 64, 24_norm); state.noteOnEvent(0, 64, 24_norm);
state.ccEvent(0, 123, 124_norm); state.ccEvent(0, 123, 124_norm);
state.reset(0); state.reset();
REQUIRE(state.getPitchBend() == 0); REQUIRE(state.getPitchBend() == 0.0f);
REQUIRE(state.getNoteVelocity(64) == 0_norm); REQUIRE(state.getNoteVelocity(64) == 0_norm);
REQUIRE(state.getCCValue(123) == 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") TEST_CASE("[MidiState] Set and get note velocities")
{ {
sfz::MidiState state; sfz::MidiState state;

View file

@ -77,11 +77,11 @@ TEST_CASE("Region activation", "Region tests")
region.parseOpcode({ "hibend", "243" }); region.parseOpcode({ "hibend", "243" });
region.registerPitchWheel(0); region.registerPitchWheel(0);
REQUIRE(!region.isSwitchedOn()); REQUIRE(!region.isSwitchedOn());
region.registerPitchWheel(56); region.registerPitchWheel(sfz::normalizeBend(56));
REQUIRE(region.isSwitchedOn()); REQUIRE(region.isSwitchedOn());
region.registerPitchWheel(243); region.registerPitchWheel(sfz::normalizeBend(243));
REQUIRE(region.isSwitchedOn()); REQUIRE(region.isSwitchedOn());
region.registerPitchWheel(245); region.registerPitchWheel(sfz::normalizeBend(245));
REQUIRE(!region.isSwitchedOn()); REQUIRE(!region.isSwitchedOn());
} }

View file

@ -223,19 +223,23 @@ TEST_CASE("[Region] Parsing opcodes")
SECTION("lobend, hibend") SECTION("lobend, hibend")
{ {
REQUIRE(region.bendRange == sfz::Range<int>(-8192, 8192)); REQUIRE(region.bendRange == sfz::Range<float>(-1.0f, 1.0f));
region.parseOpcode({ "lobend", "4" }); region.parseOpcode({ "lobend", "400" });
REQUIRE(region.bendRange == sfz::Range<int>(4, 8192)); REQUIRE(region.bendRange.getStart() == Approx(sfz::normalizeBend(400)));
REQUIRE(region.bendRange.getEnd() == 1.0_a);
region.parseOpcode({ "lobend", "-128" }); region.parseOpcode({ "lobend", "-128" });
REQUIRE(region.bendRange == sfz::Range<int>(-128, 8192)); REQUIRE(region.bendRange.getStart() == Approx(sfz::normalizeBend(-128)));
REQUIRE(region.bendRange.getEnd() == 1.0_a);
region.parseOpcode({ "lobend", "-10000" }); region.parseOpcode({ "lobend", "-10000" });
REQUIRE(region.bendRange == sfz::Range<int>(-8192, 8192)); REQUIRE(region.bendRange == sfz::Range<float>(-1.0f, 1.0f));
region.parseOpcode({ "hibend", "13" }); region.parseOpcode({ "hibend", "13" });
REQUIRE(region.bendRange == sfz::Range<int>(-8192, 13)); REQUIRE(region.bendRange.getStart() == -1.0_a);
REQUIRE(region.bendRange.getEnd() == Approx(sfz::normalizeBend(13)));
region.parseOpcode({ "hibend", "-1" }); region.parseOpcode({ "hibend", "-1" });
REQUIRE(region.bendRange == sfz::Range<int>(-8192, -1)); REQUIRE(region.bendRange.getStart() == -1.0_a);
REQUIRE(region.bendRange.getEnd() == Approx(sfz::normalizeBend(-1)));
region.parseOpcode({ "hibend", "10000" }); region.parseOpcode({ "hibend", "10000" });
REQUIRE(region.bendRange == sfz::Range<int>(-8192, 8192)); REQUIRE(region.bendRange == sfz::Range<float>(-1.0f, 1.0f));
} }
SECTION("locc, hicc") SECTION("locc, hicc")
@ -452,66 +456,63 @@ TEST_CASE("[Region] Parsing opcodes")
{ {
REQUIRE(region.pan == 0.0f); REQUIRE(region.pan == 0.0f);
region.parseOpcode({ "pan", "4.2" }); region.parseOpcode({ "pan", "4.2" });
REQUIRE(region.pan == 4.2f); REQUIRE(region.pan == 0.042_a);
region.parseOpcode({ "pan", "-4.2" }); region.parseOpcode({ "pan", "-4.2" });
REQUIRE(region.pan == -4.2f); REQUIRE(region.pan == -0.042_a);
region.parseOpcode({ "pan", "-123" }); region.parseOpcode({ "pan", "-123" });
REQUIRE(region.pan == -100.0f); REQUIRE(region.pan == -1.0_a);
region.parseOpcode({ "pan", "132" }); region.parseOpcode({ "pan", "132" });
REQUIRE(region.pan == 100.0f); REQUIRE(region.pan == 1.0_a);
} }
SECTION("pan_oncc") SECTION("pan_oncc")
{ {
REQUIRE(!region.panCC); REQUIRE(region.panCC.empty());
region.parseOpcode({ "pan_oncc45", "4.2" }); region.parseOpcode({ "pan_oncc45", "4.2" });
REQUIRE(region.panCC); REQUIRE(region.panCC.contains(45));
REQUIRE(region.panCC->cc == 45); REQUIRE(region.panCC[45] == 0.042_a);
REQUIRE(region.panCC->value == 4.2f);
} }
SECTION("width") SECTION("width")
{ {
REQUIRE(region.width == 100.0f); REQUIRE(region.width == 1.0_a);
region.parseOpcode({ "width", "4.2" }); region.parseOpcode({ "width", "4.2" });
REQUIRE(region.width == 4.2f); REQUIRE(region.width == 0.042_a);
region.parseOpcode({ "width", "-4.2" }); region.parseOpcode({ "width", "-4.2" });
REQUIRE(region.width == -4.2f); REQUIRE(region.width == -0.042_a);
region.parseOpcode({ "width", "-123" }); region.parseOpcode({ "width", "-123" });
REQUIRE(region.width == -100.0f); REQUIRE(region.width == -1.0_a);
region.parseOpcode({ "width", "132" }); region.parseOpcode({ "width", "132" });
REQUIRE(region.width == 100.0f); REQUIRE(region.width == 1.0_a);
} }
SECTION("width_oncc") SECTION("width_oncc")
{ {
REQUIRE(!region.widthCC); REQUIRE(region.widthCC.empty());
region.parseOpcode({ "width_oncc45", "4.2" }); region.parseOpcode({ "width_oncc45", "4.2" });
REQUIRE(region.widthCC); REQUIRE(region.widthCC.contains(45));
REQUIRE(region.widthCC->cc == 45); REQUIRE(region.widthCC[45] == 0.042_a);
REQUIRE(region.widthCC->value == 4.2f);
} }
SECTION("position") SECTION("position")
{ {
REQUIRE(region.position == 0.0f); REQUIRE(region.position == 0.0f);
region.parseOpcode({ "position", "4.2" }); region.parseOpcode({ "position", "4.2" });
REQUIRE(region.position == 4.2f); REQUIRE(region.position == 0.042_a);
region.parseOpcode({ "position", "-4.2" }); region.parseOpcode({ "position", "-4.2" });
REQUIRE(region.position == -4.2f); REQUIRE(region.position == -0.042_a);
region.parseOpcode({ "position", "-123" }); region.parseOpcode({ "position", "-123" });
REQUIRE(region.position == -100.0f); REQUIRE(region.position == -1.0_a);
region.parseOpcode({ "position", "132" }); region.parseOpcode({ "position", "132" });
REQUIRE(region.position == 100.0f); REQUIRE(region.position == 1.0_a);
} }
SECTION("position_oncc") SECTION("position_oncc")
{ {
REQUIRE(!region.positionCC); REQUIRE(region.positionCC.empty());
region.parseOpcode({ "position_oncc45", "4.2" }); region.parseOpcode({ "position_oncc45", "4.2" });
REQUIRE(region.positionCC); REQUIRE(region.positionCC.contains(45));
REQUIRE(region.positionCC->cc == 45); REQUIRE(region.positionCC[45] == 0.042_a);
REQUIRE(region.positionCC->value == 4.2f);
} }
SECTION("amp_keycenter") SECTION("amp_keycenter")
@ -1407,6 +1408,79 @@ TEST_CASE("[Region] Parsing opcodes")
region.parseOpcode({ "oscillator_phase", "361" }); region.parseOpcode({ "oscillator_phase", "361" });
REQUIRE(region.oscillatorPhase == 360.0f); 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 // Specific region bugs

View file

@ -21,9 +21,9 @@ TEST_CASE("[Region] Crossfade in on key")
region.parseOpcode({ "sample", "*sine" }); region.parseOpcode({ "sample", "*sine" });
region.parseOpcode({ "xfin_lokey", "1" }); region.parseOpcode({ "xfin_lokey", "1" });
region.parseOpcode({ "xfin_hikey", "3" }); region.parseOpcode({ "xfin_hikey", "3" });
REQUIRE( region.getNoteGain(2, 127_norm) == 0.70711_a ); REQUIRE(region.getNoteGain(2, 127_norm) == 0.70711_a);
REQUIRE( region.getNoteGain(1, 127_norm) == 0.0_a ); REQUIRE(region.getNoteGain(1, 127_norm) == 0.0_a);
REQUIRE( region.getNoteGain(3, 127_norm) == 1.0_a ); REQUIRE(region.getNoteGain(3, 127_norm) == 1.0_a);
} }
TEST_CASE("[Region] Crossfade in on key - 2") 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({ "sample", "*sine" });
region.parseOpcode({ "xfin_lokey", "1" }); region.parseOpcode({ "xfin_lokey", "1" });
region.parseOpcode({ "xfin_hikey", "5" }); region.parseOpcode({ "xfin_hikey", "5" });
REQUIRE( region.getNoteGain(1, 127_norm) == 0.0_a ); REQUIRE(region.getNoteGain(1, 127_norm) == 0.0_a);
REQUIRE( region.getNoteGain(2, 127_norm) == 0.5_a ); REQUIRE(region.getNoteGain(2, 127_norm) == 0.5_a);
REQUIRE( region.getNoteGain(3, 127_norm) == 0.70711_a ); REQUIRE(region.getNoteGain(3, 127_norm) == 0.70711_a);
REQUIRE( region.getNoteGain(4, 127_norm) == 0.86603_a ); REQUIRE(region.getNoteGain(4, 127_norm) == 0.86603_a);
REQUIRE( region.getNoteGain(5, 127_norm) == 1.0_a ); REQUIRE(region.getNoteGain(5, 127_norm) == 1.0_a);
REQUIRE( region.getNoteGain(6, 127_norm) == 1.0_a ); REQUIRE(region.getNoteGain(6, 127_norm) == 1.0_a);
} }
TEST_CASE("[Region] Crossfade in on key - gain") 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_lokey", "1" });
region.parseOpcode({ "xfin_hikey", "5" }); region.parseOpcode({ "xfin_hikey", "5" });
region.parseOpcode({ "xf_keycurve", "gain" }); region.parseOpcode({ "xf_keycurve", "gain" });
REQUIRE( region.getNoteGain(1, 127_norm) == 0.0_a ); REQUIRE(region.getNoteGain(1, 127_norm) == 0.0_a);
REQUIRE( region.getNoteGain(2, 127_norm) == 0.25_a ); REQUIRE(region.getNoteGain(2, 127_norm) == 0.25_a);
REQUIRE( region.getNoteGain(3, 127_norm) == 0.5_a ); REQUIRE(region.getNoteGain(3, 127_norm) == 0.5_a);
REQUIRE( region.getNoteGain(4, 127_norm) == 0.75_a ); REQUIRE(region.getNoteGain(4, 127_norm) == 0.75_a);
REQUIRE( region.getNoteGain(5, 127_norm) == 1.0_a ); REQUIRE(region.getNoteGain(5, 127_norm) == 1.0_a);
} }
TEST_CASE("[Region] Crossfade out on key") TEST_CASE("[Region] Crossfade out on key")
@ -63,13 +63,13 @@ TEST_CASE("[Region] Crossfade out on key")
region.parseOpcode({ "sample", "*sine" }); region.parseOpcode({ "sample", "*sine" });
region.parseOpcode({ "xfout_lokey", "51" }); region.parseOpcode({ "xfout_lokey", "51" });
region.parseOpcode({ "xfout_hikey", "55" }); region.parseOpcode({ "xfout_hikey", "55" });
REQUIRE( region.getNoteGain(50, 127_norm) == 1.0_a ); REQUIRE(region.getNoteGain(50, 127_norm) == 1.0_a);
REQUIRE( region.getNoteGain(51, 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(52, 127_norm) == 0.86603_a);
REQUIRE( region.getNoteGain(53, 127_norm) == 0.70711_a ); REQUIRE(region.getNoteGain(53, 127_norm) == 0.70711_a);
REQUIRE( region.getNoteGain(54, 127_norm) == 0.5_a ); REQUIRE(region.getNoteGain(54, 127_norm) == 0.5_a);
REQUIRE( region.getNoteGain(55, 127_norm) == 0.0_a ); REQUIRE(region.getNoteGain(55, 127_norm) == 0.0_a);
REQUIRE( region.getNoteGain(56, 127_norm) == 0.0_a ); REQUIRE(region.getNoteGain(56, 127_norm) == 0.0_a);
} }
TEST_CASE("[Region] Crossfade out on key - gain") 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_lokey", "51" });
region.parseOpcode({ "xfout_hikey", "55" }); region.parseOpcode({ "xfout_hikey", "55" });
region.parseOpcode({ "xf_keycurve", "gain" }); region.parseOpcode({ "xf_keycurve", "gain" });
REQUIRE( region.getNoteGain(50, 127_norm) == 1.0_a ); REQUIRE(region.getNoteGain(50, 127_norm) == 1.0_a);
REQUIRE( region.getNoteGain(51, 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(52, 127_norm) == 0.75_a);
REQUIRE( region.getNoteGain(53, 127_norm) == 0.5_a ); REQUIRE(region.getNoteGain(53, 127_norm) == 0.5_a);
REQUIRE( region.getNoteGain(54, 127_norm) == 0.25_a ); REQUIRE(region.getNoteGain(54, 127_norm) == 0.25_a);
REQUIRE( region.getNoteGain(55, 127_norm) == 0.0_a ); REQUIRE(region.getNoteGain(55, 127_norm) == 0.0_a);
REQUIRE( region.getNoteGain(56, 127_norm) == 0.0_a ); REQUIRE(region.getNoteGain(56, 127_norm) == 0.0_a);
} }
TEST_CASE("[Region] Crossfade in on velocity") 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_lovel", "20" });
region.parseOpcode({ "xfin_hivel", "24" }); region.parseOpcode({ "xfin_hivel", "24" });
region.parseOpcode({ "amp_veltrack", "0" }); region.parseOpcode({ "amp_veltrack", "0" });
REQUIRE( region.getNoteGain(1, 19_norm) == 0.0_a ); REQUIRE(region.getNoteGain(1, 19_norm) == 0.0_a);
REQUIRE( region.getNoteGain(1, 20_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(2, 21_norm) == 0.5_a);
REQUIRE( region.getNoteGain(3, 22_norm) == 0.70711_a ); REQUIRE(region.getNoteGain(3, 22_norm) == 0.70711_a);
REQUIRE( region.getNoteGain(4, 23_norm) == 0.86603_a ); REQUIRE(region.getNoteGain(4, 23_norm) == 0.86603_a);
REQUIRE( region.getNoteGain(5, 24_norm) == 1.0_a ); REQUIRE(region.getNoteGain(5, 24_norm) == 1.0_a);
REQUIRE( region.getNoteGain(6, 25_norm) == 1.0_a ); REQUIRE(region.getNoteGain(6, 25_norm) == 1.0_a);
} }
TEST_CASE("[Region] Crossfade in on vel - gain") 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({ "xfin_hivel", "24" });
region.parseOpcode({ "xf_velcurve", "gain" }); region.parseOpcode({ "xf_velcurve", "gain" });
region.parseOpcode({ "amp_veltrack", "0" }); region.parseOpcode({ "amp_veltrack", "0" });
REQUIRE( region.getNoteGain(1, 19_norm) == 0.0_a ); REQUIRE(region.getNoteGain(1, 19_norm) == 0.0_a);
REQUIRE( region.getNoteGain(1, 20_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(2, 21_norm) == 0.25_a);
REQUIRE( region.getNoteGain(3, 22_norm) == 0.5_a ); REQUIRE(region.getNoteGain(3, 22_norm) == 0.5_a);
REQUIRE( region.getNoteGain(4, 23_norm) == 0.75_a ); REQUIRE(region.getNoteGain(4, 23_norm) == 0.75_a);
REQUIRE( region.getNoteGain(5, 24_norm) == 1.0_a ); REQUIRE(region.getNoteGain(5, 24_norm) == 1.0_a);
REQUIRE( region.getNoteGain(5, 25_norm) == 1.0_a ); REQUIRE(region.getNoteGain(5, 25_norm) == 1.0_a);
} }
TEST_CASE("[Region] Crossfade out on vel") 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_lovel", "51" });
region.parseOpcode({ "xfout_hivel", "55" }); region.parseOpcode({ "xfout_hivel", "55" });
region.parseOpcode({ "amp_veltrack", "0" }); region.parseOpcode({ "amp_veltrack", "0" });
REQUIRE( region.getNoteGain(5, 50_norm) == 1.0_a ); REQUIRE(region.getNoteGain(5, 50_norm) == 1.0_a);
REQUIRE( region.getNoteGain(5, 51_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, 52_norm) == 0.86603_a);
REQUIRE( region.getNoteGain(5, 53_norm) == 0.70711_a ); REQUIRE(region.getNoteGain(5, 53_norm) == 0.70711_a);
REQUIRE( region.getNoteGain(5, 54_norm) == 0.5_a ); REQUIRE(region.getNoteGain(5, 54_norm) == 0.5_a);
REQUIRE( region.getNoteGain(5, 55_norm) == 0.0_a ); REQUIRE(region.getNoteGain(5, 55_norm) == 0.0_a);
REQUIRE( region.getNoteGain(5, 56_norm) == 0.0_a ); REQUIRE(region.getNoteGain(5, 56_norm) == 0.0_a);
} }
TEST_CASE("[Region] Crossfade out on vel - gain") 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({ "xfout_hivel", "55" });
region.parseOpcode({ "xf_velcurve", "gain" }); region.parseOpcode({ "xf_velcurve", "gain" });
region.parseOpcode({ "amp_veltrack", "0" }); region.parseOpcode({ "amp_veltrack", "0" });
REQUIRE( region.getNoteGain(56, 50_norm) == 1.0_a ); REQUIRE(region.getNoteGain(56, 50_norm) == 1.0_a);
REQUIRE( region.getNoteGain(56, 51_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, 52_norm) == 0.75_a);
REQUIRE( region.getNoteGain(56, 53_norm) == 0.5_a ); REQUIRE(region.getNoteGain(56, 53_norm) == 0.5_a);
REQUIRE( region.getNoteGain(56, 54_norm) == 0.25_a ); REQUIRE(region.getNoteGain(56, 54_norm) == 0.25_a);
REQUIRE( region.getNoteGain(56, 55_norm) == 0.0_a ); REQUIRE(region.getNoteGain(56, 55_norm) == 0.0_a);
REQUIRE( region.getNoteGain(56, 56_norm) == 0.0_a ); REQUIRE(region.getNoteGain(56, 56_norm) == 0.0_a);
} }
TEST_CASE("[Region] Crossfade in on CC") 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_locc24", "20" });
region.parseOpcode({ "xfin_hicc24", "24" }); region.parseOpcode({ "xfin_hicc24", "24" });
region.parseOpcode({ "amp_veltrack", "0" }); region.parseOpcode({ "amp_veltrack", "0" });
midiState.ccEvent(0, 24, 19_norm); REQUIRE( region.getCrossfadeGain() == 0.0_a ); midiState.ccEvent(0, 24, 19_norm);
midiState.ccEvent(0, 24, 20_norm); REQUIRE( region.getCrossfadeGain() == 0.0_a ); REQUIRE(region.getCrossfadeGain() == 0.0_a);
midiState.ccEvent(0, 24, 21_norm); REQUIRE( region.getCrossfadeGain() == 0.5_a ); midiState.ccEvent(0, 24, 20_norm);
midiState.ccEvent(0, 24, 22_norm); REQUIRE( region.getCrossfadeGain() == 0.70711_a ); REQUIRE(region.getCrossfadeGain() == 0.0_a);
midiState.ccEvent(0, 24, 23_norm); REQUIRE( region.getCrossfadeGain() == 0.86603_a ); midiState.ccEvent(0, 24, 21_norm);
midiState.ccEvent(0, 24, 24_norm); REQUIRE( region.getCrossfadeGain() == 1.0_a ); REQUIRE(region.getCrossfadeGain() == 0.5_a);
midiState.ccEvent(0, 24, 25_norm); REQUIRE( region.getCrossfadeGain() == 1.0_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") 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({ "xfin_hicc24", "24" });
region.parseOpcode({ "amp_veltrack", "0" }); region.parseOpcode({ "amp_veltrack", "0" });
region.parseOpcode({ "xf_cccurve", "gain" }); region.parseOpcode({ "xf_cccurve", "gain" });
midiState.ccEvent(0, 24, 19_norm); REQUIRE( region.getCrossfadeGain() == 0.0_a ); midiState.ccEvent(0, 24, 19_norm);
midiState.ccEvent(0, 24, 20_norm); REQUIRE( region.getCrossfadeGain() == 0.0_a ); REQUIRE(region.getCrossfadeGain() == 0.0_a);
midiState.ccEvent(0, 24, 21_norm); REQUIRE( region.getCrossfadeGain() == 0.25_a ); midiState.ccEvent(0, 24, 20_norm);
midiState.ccEvent(0, 24, 22_norm); REQUIRE( region.getCrossfadeGain() == 0.5_a ); REQUIRE(region.getCrossfadeGain() == 0.0_a);
midiState.ccEvent(0, 24, 23_norm); REQUIRE( region.getCrossfadeGain() == 0.75_a ); midiState.ccEvent(0, 24, 21_norm);
midiState.ccEvent(0, 24, 24_norm); REQUIRE( region.getCrossfadeGain() == 1.0_a ); REQUIRE(region.getCrossfadeGain() == 0.25_a);
midiState.ccEvent(0, 24, 25_norm); REQUIRE( region.getCrossfadeGain() == 1.0_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") 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_locc24", "20" });
region.parseOpcode({ "xfout_hicc24", "24" }); region.parseOpcode({ "xfout_hicc24", "24" });
region.parseOpcode({ "amp_veltrack", "0" }); region.parseOpcode({ "amp_veltrack", "0" });
midiState.ccEvent(0, 24, 19_norm); REQUIRE( region.getCrossfadeGain() == 1.0_a ); midiState.ccEvent(0, 24, 19_norm);
midiState.ccEvent(0, 24, 20_norm); REQUIRE( region.getCrossfadeGain() == 1.0_a ); REQUIRE(region.getCrossfadeGain() == 1.0_a);
midiState.ccEvent(0, 24, 21_norm); REQUIRE( region.getCrossfadeGain() == 0.86603_a ); midiState.ccEvent(0, 24, 20_norm);
midiState.ccEvent(0, 24, 22_norm); REQUIRE( region.getCrossfadeGain() == 0.70711_a ); REQUIRE(region.getCrossfadeGain() == 1.0_a);
midiState.ccEvent(0, 24, 23_norm); REQUIRE( region.getCrossfadeGain() == 0.5_a ); midiState.ccEvent(0, 24, 21_norm);
midiState.ccEvent(0, 24, 24_norm); REQUIRE( region.getCrossfadeGain() == 0.0_a ); REQUIRE(region.getCrossfadeGain() == 0.86603_a);
midiState.ccEvent(0, 24, 25_norm); REQUIRE( region.getCrossfadeGain() == 0.0_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") 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({ "xfout_hicc24", "24" });
region.parseOpcode({ "amp_veltrack", "0" }); region.parseOpcode({ "amp_veltrack", "0" });
region.parseOpcode({ "xf_cccurve", "gain" }); region.parseOpcode({ "xf_cccurve", "gain" });
midiState.ccEvent(0, 24, 19_norm); REQUIRE( region.getCrossfadeGain() == 1.0_a ); midiState.ccEvent(0, 24, 19_norm);
midiState.ccEvent(0, 24, 20_norm); REQUIRE( region.getCrossfadeGain() == 1.0_a ); REQUIRE(region.getCrossfadeGain() == 1.0_a);
midiState.ccEvent(0, 24, 21_norm); REQUIRE( region.getCrossfadeGain() == 0.75_a ); midiState.ccEvent(0, 24, 20_norm);
midiState.ccEvent(0, 24, 22_norm); REQUIRE( region.getCrossfadeGain() == 0.5_a ); REQUIRE(region.getCrossfadeGain() == 1.0_a);
midiState.ccEvent(0, 24, 23_norm); REQUIRE( region.getCrossfadeGain() == 0.25_a ); midiState.ccEvent(0, 24, 21_norm);
midiState.ccEvent(0, 24, 24_norm); REQUIRE( region.getCrossfadeGain() == 0.0_a ); REQUIRE(region.getCrossfadeGain() == 0.75_a);
midiState.ccEvent(0, 24, 25_norm); REQUIRE( region.getCrossfadeGain() == 0.0_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") 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 }; sfz::Region region { midiState };
region.parseOpcode({ "sample", "*sine" }); region.parseOpcode({ "sample", "*sine" });
region.parseOpcode({ "amp_veltrack", "0" }); region.parseOpcode({ "amp_veltrack", "0" });
REQUIRE( region.getNoteGain(64, 127_norm) == 1.0_a ); REQUIRE(region.getNoteGain(64, 127_norm) == 1.0_a);
REQUIRE( region.getNoteGain(64, 0_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 }; sfz::Region region { midiState };
region.parseOpcode({ "sample", "*sine" }); region.parseOpcode({ "sample", "*sine" });
region.parseOpcode({ "amp_veltrack", "100" }); region.parseOpcode({ "amp_veltrack", "100" });
REQUIRE( region.getNoteGain(64, 127_norm) == 1.0_a ); 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, 0_norm) == Approx(0.0).margin(0.0001));
} }
TEST_CASE("[Region] Velocity bug for extreme values - negative veltrack") 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 }; sfz::Region region { midiState };
region.parseOpcode({ "sample", "*sine" }); region.parseOpcode({ "sample", "*sine" });
region.parseOpcode({ "amp_veltrack", "-100" }); region.parseOpcode({ "amp_veltrack", "-100" });
REQUIRE( region.getNoteGain(64, 127_norm) == Approx(0.0).margin(0.0001) ); 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, 0_norm) == 1.0_a);
} }
TEST_CASE("[Region] rt_decay") TEST_CASE("[Region] rt_decay")
{ {
sfz::MidiState midiState; sfz::MidiState midiState;
midiState.setSampleRate(1000);
sfz::Region region { midiState }; sfz::Region region { midiState };
region.parseOpcode({ "sample", "*sine" }); region.parseOpcode({ "sample", "*sine" });
region.parseOpcode({ "trigger", "release" }); region.parseOpcode({ "trigger", "release" });
region.parseOpcode({ "rt_decay", "10" }); region.parseOpcode({ "rt_decay", "10" });
midiState.noteOnEvent(0, 64, 64_norm); 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) ); REQUIRE( region.getBaseVolumedB(64) == Approx(sfz::Default::volume - 1.0f).margin(0.1) );
region.parseOpcode({ "rt_decay", "20" }); region.parseOpcode({ "rt_decay", "20" });
midiState.noteOnEvent(0, 64, 64_norm); 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) ); REQUIRE( region.getBaseVolumedB(64) == Approx(sfz::Default::volume - 2.0f).margin(0.1) );
region.parseOpcode({ "trigger", "attack" }); region.parseOpcode({ "trigger", "attack" });
midiState.noteOnEvent(0, 64, 64_norm); 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) ); REQUIRE( region.getBaseVolumedB(64) == Approx(sfz::Default::volume).margin(0.1) );
} }

View file

@ -499,7 +499,7 @@ TEST_CASE("[Helpers] Linear Ramp")
const float start { 0.0f }; const float start { 0.0f };
const float v { fillValue }; const float v { fillValue };
std::array<float, 6> output; std::array<float, 6> output;
std::array<float, 6> expected { v, v + v, v + v + v, v + v + v + v, v + v + v + v + v, v + v + v + v + v + v }; std::array<float, 6> expected { start, start + v, start + v + v, start + v + v + v, start + v + v + v + v, start + v + v + v + v + v };
sfz::linearRamp<float, false>(absl::MakeSpan(output), start, v); sfz::linearRamp<float, false>(absl::MakeSpan(output), start, v);
REQUIRE(output == expected); REQUIRE(output == expected);
} }
@ -509,7 +509,7 @@ TEST_CASE("[Helpers] Linear Ramp (SIMD)")
const float start { 0.0f }; const float start { 0.0f };
const float v { fillValue }; const float v { fillValue };
std::array<float, 6> output; std::array<float, 6> output;
std::array<float, 6> expected { v, v + v, v + v + v, v + v + v + v, v + v + v + v + v, v + v + v + v + v + v }; std::array<float, 6> expected { start, start + v, start + v + v, start + v + v + v, start + v + v + v + v, start + v + v + v + v + v };
sfz::linearRamp<float, true>(absl::MakeSpan(output), start, v); sfz::linearRamp<float, true>(absl::MakeSpan(output), start, v);
REQUIRE(approxEqual<float>(output, expected)); REQUIRE(approxEqual<float>(output, expected));
} }
@ -539,7 +539,7 @@ TEST_CASE("[Helpers] Multiplicative Ramp")
const float start { 1.0f }; const float start { 1.0f };
const float v { fillValue }; const float v { fillValue };
std::array<float, 6> output; std::array<float, 6> output;
std::array<float, 6> expected { v, v * v, v * v * v, v * v * v * v, v * v * v * v * v, v * v * v * v * v * v }; std::array<float, 6> expected { start, start * v, start * v * v, start * v * v * v, start * v * v * v * v, start * v * v * v * v * v };
sfz::multiplicativeRamp<float, false>(absl::MakeSpan(output), start, v); sfz::multiplicativeRamp<float, false>(absl::MakeSpan(output), start, v);
REQUIRE(approxEqual<float>(output, expected)); REQUIRE(approxEqual<float>(output, expected));
} }
@ -549,7 +549,7 @@ TEST_CASE("[Helpers] Multiplicative Ramp (SIMD)")
const float start { 1.0f }; const float start { 1.0f };
const float v { fillValue }; const float v { fillValue };
std::array<float, 6> output; std::array<float, 6> output;
std::array<float, 6> expected { v, v * v, v * v * v, v * v * v * v, v * v * v * v * v, v * v * v * v * v * v }; std::array<float, 6> expected { start, start * v, start * v * v, start * v * v * v, start * v * v * v * v, start * v * v * v * v * v };
sfz::multiplicativeRamp<float, true>(absl::MakeSpan(output), start, v); sfz::multiplicativeRamp<float, true>(absl::MakeSpan(output), start, v);
REQUIRE(approxEqual<float>(output, expected)); REQUIRE(approxEqual<float>(output, expected));
} }

View file

@ -141,9 +141,9 @@ TEST_CASE("[Synth] Reset all controllers")
{ {
sfz::Synth synth; sfz::Synth synth;
synth.cc(0, 12, 64); synth.cc(0, 12, 64);
REQUIRE( synth.getMidiState().getCCValue(12) == 64_norm ); REQUIRE(synth.getMidiState().getCCValue(12) == 64_norm);
synth.cc(0, 121, 64); 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") 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->gainToMain() == 0 );
REQUIRE( bus->gainToMix() == 0.5 ); 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());
}

View file

@ -0,0 +1,5 @@
<region> sample=*sine key=63
<region> sample=*sine key=64 note_polyphony=2
<region> sample=*sine key=66 note_polyphony=2 note_selfmask=off
<group> group=1 polyphony=2
<region> sample=*sine key=65