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:
commit
2f2de8c677
38 changed files with 1680 additions and 1383 deletions
|
|
@ -10,7 +10,7 @@
|
|||
#include <vector>
|
||||
#include <cmath>
|
||||
#include <iostream>
|
||||
#include "EventEnvelopes.h"
|
||||
#include "SfzHelpers.h"
|
||||
#include "absl/types/span.h"
|
||||
|
||||
class EnvelopeFixture : public benchmark::Fixture {
|
||||
|
|
@ -29,45 +29,55 @@ public:
|
|||
}
|
||||
std::random_device rd { };
|
||||
std::mt19937 gen { rd() };
|
||||
std::uniform_real_distribution<float> dist { 1, 30 };
|
||||
std::uniform_real_distribution<float> dist { 2, 30 };
|
||||
std::vector<float> input;
|
||||
std::vector<float> output;
|
||||
};
|
||||
|
||||
|
||||
BENCHMARK_DEFINE_F(EnvelopeFixture, Linear)(benchmark::State& state) {
|
||||
sfz::LinearEnvelope<float> envelope;
|
||||
for (auto _ : state)
|
||||
{
|
||||
envelope.registerEvent(static_cast<int>(state.range(0) - 1), dist(gen));
|
||||
envelope.getBlock(absl::MakeSpan(output));
|
||||
sfz::EventVector events {
|
||||
{ 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) {
|
||||
sfz::LinearEnvelope<float> envelope;
|
||||
for (auto _ : state)
|
||||
{
|
||||
envelope.registerEvent(static_cast<int>(state.range(0) - 1), dist(gen));
|
||||
envelope.getQuantizedBlock(absl::MakeSpan(output), 0.5);
|
||||
sfz::EventVector events {
|
||||
{ 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) {
|
||||
sfz::MultiplicativeEnvelope<float> envelope;
|
||||
for (auto _ : state)
|
||||
{
|
||||
envelope.registerEvent(static_cast<int>(state.range(0) - 1), dist(gen));
|
||||
envelope.getBlock(absl::MakeSpan(output));
|
||||
sfz::EventVector events {
|
||||
{ 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) {
|
||||
sfz::MultiplicativeEnvelope<float> envelope;
|
||||
for (auto _ : state)
|
||||
{
|
||||
envelope.registerEvent(static_cast<int>(state.range(0) - 1), dist(gen));
|
||||
envelope.getQuantizedBlock(absl::MakeSpan(output), 1.5);
|
||||
sfz::EventVector events {
|
||||
{ 0, 1.0f },
|
||||
{ static_cast<int>(state.range(0) - 1), dist(gen) }
|
||||
};
|
||||
multiplicativeEnvelope(
|
||||
events, absl::MakeSpan(output), [](float x) { return x; }, 2.0f);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -69,11 +69,11 @@ sfizz_add_benchmark(bm_logger BM_logger.cpp)
|
|||
target_link_libraries(bm_logger PRIVATE sfizz::sfizz)
|
||||
|
||||
if (TARGET sfizz-samplerate)
|
||||
sfizz_add_benchmark(bm_resample BM_resample.cpp ${BENCHMARK_SIMD_SOURCES})
|
||||
target_link_libraries(bm_resample PRIVATE sfizz-samplerate sfizz-sndfile)
|
||||
sfizz_add_benchmark(bm_resample BM_resample.cpp ${BENCHMARK_SIMD_SOURCES})
|
||||
target_link_libraries(bm_resample PRIVATE sfizz-samplerate sfizz-sndfile)
|
||||
endif()
|
||||
|
||||
sfizz_add_benchmark(bm_envelopes BM_envelopes.cpp ../src/sfizz/MidiState.cpp ../src/sfizz/FloatEnvelopes.cpp)
|
||||
sfizz_add_benchmark(bm_envelopes BM_envelopes.cpp)
|
||||
|
||||
sfizz_add_benchmark(bm_wavfile BM_wavfile.cpp)
|
||||
target_link_libraries(bm_wavfile PRIVATE sfizz-sndfile)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ clang-tidy \
|
|||
src/sfizz/Curve.cpp \
|
||||
src/sfizz/Effects.cpp \
|
||||
src/sfizz/EQPool.cpp \
|
||||
src/sfizz/EventEnvelopes.cpp \
|
||||
src/sfizz/FilePool.cpp \
|
||||
src/sfizz/FilterPool.cpp \
|
||||
src/sfizz/FloatEnvelopes.cpp \
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ void ADSREnvelope<Type>::reset(const Region& region, const MidiState& state, int
|
|||
this->release = secondsToSamples(region.amplitudeEG.getRelease(state, velocity));
|
||||
this->hold = secondsToSamples(region.amplitudeEG.getHold(state, velocity));
|
||||
this->peak = 1.0;
|
||||
this->sustain = normalizePercents(region.amplitudeEG.getSustain(state, velocity));
|
||||
this->sustain = normalizePercents(region.amplitudeEG.getSustain(state, velocity));
|
||||
this->start = this->peak * normalizePercents(region.amplitudeEG.getStart(state, velocity));
|
||||
|
||||
releaseDelay = 0;
|
||||
|
|
|
|||
184
src/sfizz/BufferPool.h
Normal file
184
src/sfizz/BufferPool.h
Normal 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
|
||||
};
|
||||
}
|
||||
|
|
@ -28,6 +28,9 @@ namespace config {
|
|||
constexpr float defaultSampleRate { 48000 };
|
||||
constexpr int defaultSamplesPerBlock { 1024 };
|
||||
constexpr int maxBlockSize { 8192 };
|
||||
constexpr int bufferPoolSize { 4 };
|
||||
constexpr int stereoBufferPoolSize { 4 };
|
||||
constexpr int indexBufferPoolSize { 2 };
|
||||
constexpr int preloadSize { 8192 };
|
||||
constexpr int loggerQueueSize { 256 };
|
||||
constexpr int voiceLoggerQueueSize { 256 };
|
||||
|
|
@ -35,7 +38,7 @@ namespace config {
|
|||
constexpr size_t numChannels { 2 };
|
||||
constexpr int numBackgroundThreads { 4 };
|
||||
constexpr int numVoices { 64 };
|
||||
constexpr int maxVoices { 256 };
|
||||
constexpr unsigned maxVoices { 256 };
|
||||
constexpr int maxFilePromises { maxVoices * 2 };
|
||||
constexpr int sustainCC { 64 };
|
||||
constexpr int allSoundOffCC { 120 };
|
||||
|
|
|
|||
|
|
@ -144,7 +144,7 @@ void Curve::lerpFill(const bool fillStatus[NumValues])
|
|||
const auto length = right - left;
|
||||
if (length > 1) {
|
||||
const float mu = (_points[right] - _points[left]) / length;
|
||||
linearRamp<float>(pointSpan.subspan(left + 1, length - 1), _points[left], mu);
|
||||
linearRamp<float>(pointSpan.subspan(left, length), _points[left], mu);
|
||||
}
|
||||
left = right++;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ enum class SfzLoopMode { no_loop, one_shot, loop_continuous, loop_sustain };
|
|||
enum class SfzOffMode { fast, normal };
|
||||
enum class SfzVelocityOverride { current, previous };
|
||||
enum class SfzCrossfadeCurve { gain, power };
|
||||
enum class SfzSelfMask { mask, dontMask };
|
||||
|
||||
namespace sfz
|
||||
{
|
||||
|
|
@ -53,9 +54,11 @@ namespace Default
|
|||
constexpr SfzLoopMode loopMode { SfzLoopMode::no_loop };
|
||||
constexpr Range<uint32_t> loopRange { 0, std::numeric_limits<uint32_t>::max() };
|
||||
|
||||
// Global ranges
|
||||
// common defaults
|
||||
constexpr Range<uint8_t> midi7Range { 0, 127 };
|
||||
constexpr Range<float> normalizedRange { 0.0f, 1.0f };
|
||||
constexpr Range<float> symmetricNormalizedRange { -1.0, 1.0 };
|
||||
constexpr float zeroModifier { 0.0f };
|
||||
|
||||
// Wavetable oscillator
|
||||
constexpr float oscillatorPhase { 0.0 };
|
||||
|
|
@ -65,18 +68,21 @@ namespace Default
|
|||
constexpr uint32_t group { 0 };
|
||||
constexpr Range<uint32_t> groupRange { 0, std::numeric_limits<uint32_t>::max() };
|
||||
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 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> midiChannelRange { 0, 15 };
|
||||
constexpr Range<uint16_t> ccNumberRange { 0, config::numCCs };
|
||||
constexpr auto ccValueRange = normalizedRange;
|
||||
constexpr Range<int> bendRange { -8192, 8192 };
|
||||
constexpr int bend { 0 };
|
||||
constexpr Range<uint16_t> ccNumberRange { 0, config::numCCs };
|
||||
constexpr auto ccValueRange = normalizedRange;
|
||||
constexpr Range<int> bendRange = { -8192, 8192 };
|
||||
constexpr Range<float> bendValueRange = symmetricNormalizedRange;
|
||||
constexpr int bend { 0 };
|
||||
constexpr SfzVelocityOverride velocityOverride { SfzVelocityOverride::current };
|
||||
|
||||
// Region logic: internal conditions
|
||||
|
|
@ -91,9 +97,9 @@ namespace Default
|
|||
|
||||
// Region logic: Triggers
|
||||
constexpr SfzTrigger trigger { SfzTrigger::attack };
|
||||
constexpr Range<float> ccTriggerValueRange = normalizedRange;
|
||||
constexpr Range<float> ccTriggerValueRange = normalizedRange;
|
||||
|
||||
// Performance parameters: amplifier
|
||||
// Performance parameters: amplifier
|
||||
constexpr float globalVolume { -7.35f };
|
||||
constexpr float volume { 0.0f };
|
||||
constexpr Range<float> volumeRange { -144.0, 6.0 };
|
||||
|
|
@ -103,7 +109,6 @@ namespace Default
|
|||
constexpr float pan { 0.0 };
|
||||
constexpr Range<float> panRange { -100.0, 100.0 };
|
||||
constexpr Range<float> panCCRange { -200.0, 200.0 };
|
||||
constexpr Range<float> symmetricNormalizedRange { -1.0, 1.0 };
|
||||
constexpr float position { 0.0 };
|
||||
constexpr Range<float> positionRange { -100.0, 100.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<uint8_t> crossfadeKeyInRange { 0, 0 };
|
||||
constexpr Range<uint8_t> crossfadeKeyOutRange { 127, 127 };
|
||||
constexpr Range<float> crossfadeVelInRange { 0.0f, 0.0f };
|
||||
constexpr Range<float> crossfadeVelOutRange { 1.0f, 1.0f };
|
||||
constexpr Range<float> crossfadeCCInRange { 0.0f, 0.0f };
|
||||
constexpr Range<float> crossfadeCCOutRange { 1.0f, 1.0f };
|
||||
constexpr SfzCrossfadeCurve crossfadeKeyCurve { SfzCrossfadeCurve::power };
|
||||
constexpr Range<float> crossfadeVelInRange { 0.0f, 0.0f };
|
||||
constexpr Range<float> crossfadeVelOutRange { 1.0f, 1.0f };
|
||||
constexpr Range<float> crossfadeCCInRange { 0.0f, 0.0f };
|
||||
constexpr Range<float> crossfadeCCOutRange { 1.0f, 1.0f };
|
||||
constexpr SfzCrossfadeCurve crossfadeKeyCurve { SfzCrossfadeCurve::power };
|
||||
constexpr SfzCrossfadeCurve crossfadeVelCurve { SfzCrossfadeCurve::power };
|
||||
constexpr SfzCrossfadeCurve crossfadeCCCurve { SfzCrossfadeCurve::power };
|
||||
constexpr float rtDecay { 0.0f };
|
||||
|
|
@ -184,6 +189,7 @@ namespace Default
|
|||
constexpr Range<int> transposeRange { -127, 127 };
|
||||
constexpr int tune { 0 };
|
||||
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> bendStepRange { 1, 1200 };
|
||||
constexpr int bendUp { 200 }; // No range here because the bounds can be inverted
|
||||
|
|
|
|||
|
|
@ -28,9 +28,20 @@ void sfz::EQHolder::setup(const EQDescription& description, unsigned numChannels
|
|||
baseGain = description.gain + velocity * description.vel2gain;
|
||||
|
||||
// Setup the modulated values
|
||||
lastFrequency = midiState.modulate(baseFrequency, description.frequencyCC, Default::eqFrequencyRange);
|
||||
lastBandwidth = midiState.modulate(baseBandwidth, description.bandwidthCC, Default::eqBandwidthRange);
|
||||
lastGain = midiState.modulate(baseGain, description.gainCC, Default::eqGainRange);
|
||||
lastFrequency = baseFrequency;
|
||||
for (const auto& mod : description.frequencyCC)
|
||||
lastFrequency += midiState.getCCValue(mod.cc) * mod.value;
|
||||
lastFrequency = Default::eqFrequencyRange.clamp(lastFrequency);
|
||||
|
||||
lastBandwidth = baseBandwidth;
|
||||
for (const auto& mod : description.bandwidthCC)
|
||||
lastBandwidth += midiState.getCCValue(mod.cc) * mod.value;
|
||||
lastBandwidth = Default::eqBandwidthRange.clamp(lastBandwidth);
|
||||
|
||||
lastGain = baseGain;
|
||||
for (const auto& mod : description.gainCC)
|
||||
lastGain += midiState.getCCValue(mod.cc) * mod.value;
|
||||
lastGain = Default::filterGainRange.clamp(lastGain);
|
||||
|
||||
// Initialize the EQ
|
||||
eq.prepare(lastFrequency, lastBandwidth, lastGain);
|
||||
|
|
@ -50,9 +61,20 @@ void sfz::EQHolder::process(const float** inputs, float** outputs, unsigned numF
|
|||
|
||||
// TODO: Once the midistate envelopes are done, add modulation in there!
|
||||
// For now we take the last value
|
||||
lastFrequency = midiState.modulate(baseFrequency, description->frequencyCC, Default::eqFrequencyRange);
|
||||
lastBandwidth = midiState.modulate(baseBandwidth, description->bandwidthCC, Default::eqBandwidthRange);
|
||||
lastGain = midiState.modulate(baseGain, description->gainCC, Default::eqGainRange);
|
||||
lastFrequency = baseFrequency;
|
||||
for (const auto& mod : description->frequencyCC)
|
||||
lastFrequency += midiState.getCCValue(mod.cc) * mod.value;
|
||||
lastFrequency = Default::eqFrequencyRange.clamp(lastFrequency);
|
||||
|
||||
lastBandwidth = baseBandwidth;
|
||||
for (const auto& mod : description->bandwidthCC)
|
||||
lastBandwidth += midiState.getCCValue(mod.cc) * mod.value;
|
||||
lastBandwidth = Default::eqBandwidthRange.clamp(lastBandwidth);
|
||||
|
||||
lastGain = baseGain;
|
||||
for (const auto& mod : description->gainCC)
|
||||
lastGain += midiState.getCCValue(mod.cc) * mod.value;
|
||||
lastGain = Default::filterGainRange.clamp(lastGain);
|
||||
|
||||
if (lastGain == 0.0f) {
|
||||
justCopy();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
|
@ -273,7 +273,7 @@ sfz::FilePromisePtr sfz::FilePool::getFilePromise(const std::string& filename) n
|
|||
auto promise = emptyPromises.back();
|
||||
promise->filename = preloaded->first;
|
||||
promise->preloadedData = preloaded->second.preloadedData;
|
||||
promise->sampleRate = preloaded->second.information.sampleRate;
|
||||
promise->sampleRate = static_cast<float>(preloaded->second.information.sampleRate);
|
||||
promise->oversamplingFactor = oversamplingFactor;
|
||||
promise->creationTime = std::chrono::high_resolution_clock::now();
|
||||
|
||||
|
|
|
|||
|
|
@ -40,9 +40,20 @@ void sfz::FilterHolder::setup(const FilterDescription& description, unsigned num
|
|||
baseResonance = description.resonance;
|
||||
|
||||
// Setup the modulated values
|
||||
lastCutoff = midiState.modulate<float, int>(baseCutoff, description.cutoffCC, Default::filterCutoffRange, multiplyByCents);
|
||||
lastResonance = midiState.modulate(baseResonance, description.resonanceCC, Default::filterResonanceRange);
|
||||
lastGain = midiState.modulate(baseGain, description.gainCC, Default::filterGainRange);
|
||||
lastCutoff = baseCutoff;
|
||||
for (const auto& mod : description.cutoffCC)
|
||||
lastCutoff *= centsFactor(midiState.getCCValue(mod.cc) * mod.value);
|
||||
lastCutoff = Default::filterCutoffRange.clamp(lastCutoff);
|
||||
|
||||
lastResonance = baseResonance;
|
||||
for (const auto& mod : description.resonanceCC)
|
||||
lastResonance += midiState.getCCValue(mod.cc) * mod.value;
|
||||
lastResonance = Default::filterResonanceRange.clamp(lastResonance);
|
||||
|
||||
lastGain = baseGain;
|
||||
for (const auto& mod : description.gainCC)
|
||||
lastGain += midiState.getCCValue(mod.cc) * mod.value;
|
||||
lastGain = Default::filterGainRange.clamp(lastGain);
|
||||
|
||||
// Initialize the filter
|
||||
filter.prepare(lastCutoff, lastResonance, lastGain);
|
||||
|
|
@ -59,9 +70,20 @@ void sfz::FilterHolder::process(const float** inputs, float** outputs, unsigned
|
|||
// TODO: Once the midistate envelopes are done, add modulation in there!
|
||||
// For now we take the last value
|
||||
// TODO: the template deduction could be automatic here?
|
||||
lastCutoff = midiState.modulate<float, int>(baseCutoff, description->cutoffCC, Default::filterCutoffRange, multiplyByCents);
|
||||
lastResonance = midiState.modulate(baseResonance, description->resonanceCC, Default::filterResonanceRange);
|
||||
baseGain = midiState.modulate(baseGain, description->gainCC, Default::filterGainRange);
|
||||
lastCutoff = baseCutoff;
|
||||
for (const auto& mod : description->cutoffCC)
|
||||
lastCutoff *= centsFactor(midiState.getCCValue(mod.cc) * mod.value);
|
||||
lastCutoff = Default::filterCutoffRange.clamp(lastCutoff);
|
||||
|
||||
lastResonance = baseResonance;
|
||||
for (const auto& mod : description->resonanceCC)
|
||||
lastResonance += midiState.getCCValue(mod.cc) * mod.value;
|
||||
lastResonance = Default::filterResonanceRange.clamp(lastResonance);
|
||||
|
||||
lastGain = baseGain;
|
||||
for (const auto& mod : description->gainCC)
|
||||
lastGain += midiState.getCCValue(mod.cc) * mod.value;
|
||||
lastGain = Default::filterGainRange.clamp(lastGain);
|
||||
|
||||
filter.process(inputs, outputs, lastCutoff, lastResonance, lastGain, numFrames);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,29 +4,13 @@
|
|||
// license. You should have receive a LICENSE.md file along with the code.
|
||||
// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz
|
||||
|
||||
/**
|
||||
* @file FloatEnvelopes.cpp
|
||||
* @author Paul Ferrand (paul@ferrand.cc)
|
||||
* @brief Force the instantiations of the ADSR and linear envelopes for floats
|
||||
* @version 0.1
|
||||
* @date 2019-11-30
|
||||
*
|
||||
* @copyright Copyright (c) 2019 Paul Ferrand
|
||||
*
|
||||
*/
|
||||
|
||||
#include "EventEnvelopes.h"
|
||||
#include "ADSREnvelope.h"
|
||||
|
||||
// Include the generic implementations
|
||||
#include "EventEnvelopes.cpp"
|
||||
#include "ADSREnvelope.cpp"
|
||||
|
||||
// And explicitely instantiate the float version
|
||||
namespace sfz
|
||||
{
|
||||
template class EventEnvelope<float>;
|
||||
template class MultiplicativeEnvelope<float>;
|
||||
template class LinearEnvelope<float>;
|
||||
template class ADSREnvelope<float>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
|
||||
sfz::MidiState::MidiState()
|
||||
{
|
||||
reset(0);
|
||||
reset();
|
||||
}
|
||||
|
||||
void sfz::MidiState::noteOnEvent(int delay, int noteNumber, float velocity) noexcept
|
||||
|
|
@ -20,7 +20,7 @@ void sfz::MidiState::noteOnEvent(int delay, int noteNumber, float velocity) noex
|
|||
|
||||
if (noteNumber >= 0 && noteNumber < 128) {
|
||||
lastNoteVelocities[noteNumber] = velocity;
|
||||
noteOnTimes[noteNumber] = std::chrono::steady_clock::now();
|
||||
noteOnTimes[noteNumber] = internalClock + static_cast<unsigned>(delay);
|
||||
activeNotes++;
|
||||
}
|
||||
|
||||
|
|
@ -28,27 +28,61 @@ void sfz::MidiState::noteOnEvent(int delay, int noteNumber, float velocity) noex
|
|||
|
||||
void sfz::MidiState::noteOffEvent(int delay, int noteNumber, float velocity) noexcept
|
||||
{
|
||||
ASSERT(delay >= 0);
|
||||
ASSERT(noteNumber >= 0 && noteNumber <= 127);
|
||||
ASSERT(velocity >= 0.0 && velocity <= 1.0);
|
||||
UNUSED(velocity);
|
||||
if (noteNumber >= 0 && noteNumber < 128) {
|
||||
noteOffTimes[noteNumber] = internalClock + static_cast<unsigned>(delay);
|
||||
if (activeNotes > 0)
|
||||
activeNotes--;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
float sfz::MidiState::getNoteDuration(int noteNumber) const
|
||||
void sfz::MidiState::setSampleRate(float sampleRate) noexcept
|
||||
{
|
||||
ASSERT(noteNumber >= 0 && noteNumber <= 127);
|
||||
this->sampleRate = sampleRate;
|
||||
internalClock = 0;
|
||||
absl::c_fill(noteOnTimes, 0);
|
||||
absl::c_fill(noteOffTimes, 0);
|
||||
}
|
||||
|
||||
if (noteNumber >= 0 && noteNumber < 128) {
|
||||
const auto noteOffTime = std::chrono::steady_clock::now();
|
||||
const auto duration = std::chrono::duration_cast<std::chrono::duration<float>>(noteOffTime - noteOnTimes[noteNumber]);
|
||||
return duration.count();
|
||||
void sfz::MidiState::advanceTime(int numSamples) noexcept
|
||||
{
|
||||
internalClock += numSamples;
|
||||
for (auto& ccEvents : cc) {
|
||||
ASSERT(!ccEvents.empty()); // CC event vectors should never be empty
|
||||
ccEvents.front().value = ccEvents.back().value;
|
||||
ccEvents.front().delay = 0;
|
||||
ccEvents.resize(1);
|
||||
}
|
||||
ASSERT(!pitchEvents.empty());
|
||||
pitchEvents.front().value = pitchEvents.back().value;
|
||||
pitchEvents.front().delay = 0;
|
||||
pitchEvents.resize(1);
|
||||
}
|
||||
|
||||
return 0.0f;
|
||||
void sfz::MidiState::setSamplesPerBlock(int samplesPerBlock) noexcept
|
||||
{
|
||||
this->samplesPerBlock = samplesPerBlock;
|
||||
for (auto& ccEvents : cc) {
|
||||
ccEvents.shrink_to_fit();
|
||||
ccEvents.reserve(samplesPerBlock);
|
||||
}
|
||||
}
|
||||
|
||||
float sfz::MidiState::getNoteDuration(int noteNumber, int delay) const
|
||||
{
|
||||
ASSERT(noteNumber >= 0 && noteNumber < 128);
|
||||
if (noteNumber < 0 || noteNumber >= 128)
|
||||
return 0.0f;
|
||||
|
||||
if (noteOnTimes[noteNumber] != 0 && noteOffTimes[noteNumber] != 0 && noteOnTimes[noteNumber] > noteOffTimes[noteNumber])
|
||||
return 0.0f;
|
||||
|
||||
const unsigned timeInSamples = internalClock + static_cast<unsigned>(delay) - noteOnTimes[noteNumber];
|
||||
return static_cast<float>(timeInSamples) / sampleRate;
|
||||
}
|
||||
|
||||
float sfz::MidiState::getNoteVelocity(int noteNumber) const noexcept
|
||||
|
|
@ -58,49 +92,76 @@ float sfz::MidiState::getNoteVelocity(int noteNumber) const noexcept
|
|||
return lastNoteVelocities[noteNumber];
|
||||
}
|
||||
|
||||
|
||||
void sfz::MidiState::pitchBendEvent(int delay, int pitchBendValue) noexcept
|
||||
void sfz::MidiState::pitchBendEvent(int delay, float pitchBendValue) noexcept
|
||||
{
|
||||
ASSERT(pitchBendValue >= -8192 && pitchBendValue <= 8192);
|
||||
ASSERT(pitchBendValue >= -1.0f && pitchBendValue <= 1.0f);
|
||||
|
||||
pitchBend = pitchBendValue;
|
||||
const auto insertionPoint = absl::c_upper_bound(pitchEvents, delay, MidiEventDelayComparator {});
|
||||
if (insertionPoint == pitchEvents.end() || insertionPoint->delay != delay)
|
||||
pitchEvents.insert(insertionPoint, { delay, pitchBendValue });
|
||||
else
|
||||
insertionPoint->value = pitchBendValue;
|
||||
}
|
||||
|
||||
int sfz::MidiState::getPitchBend() const noexcept
|
||||
float sfz::MidiState::getPitchBend() const noexcept
|
||||
{
|
||||
return pitchBend;
|
||||
ASSERT(pitchEvents.size() > 0);
|
||||
return pitchEvents.back().value;
|
||||
}
|
||||
|
||||
void sfz::MidiState::ccEvent(int delay, int ccNumber, float ccValue) noexcept
|
||||
{
|
||||
ASSERT(ccValue >= 0.0 && ccValue <= 1.0);
|
||||
|
||||
cc[ccNumber] = ccValue;
|
||||
const auto insertionPoint = absl::c_upper_bound(cc[ccNumber], delay, MidiEventDelayComparator {});
|
||||
if (insertionPoint == cc[ccNumber].end() || insertionPoint->delay != delay)
|
||||
cc[ccNumber].insert(insertionPoint, { delay, ccValue });
|
||||
else
|
||||
insertionPoint->value = ccValue;
|
||||
}
|
||||
|
||||
float sfz::MidiState::getCCValue(int ccNumber) const noexcept
|
||||
{
|
||||
ASSERT(ccNumber >= 0 && ccNumber < config::numCCs);
|
||||
|
||||
return cc[ccNumber];
|
||||
return cc[ccNumber].back().value;
|
||||
}
|
||||
|
||||
void sfz::MidiState::reset(int delay) noexcept
|
||||
void sfz::MidiState::reset() noexcept
|
||||
{
|
||||
for (auto& velocity: lastNoteVelocities)
|
||||
velocity = 0;
|
||||
|
||||
for (auto& ccValue: cc)
|
||||
ccValue = 0;
|
||||
for (auto& ccEvents : cc) {
|
||||
ccEvents.clear();
|
||||
ccEvents.push_back({ 0, 0.0f });
|
||||
}
|
||||
|
||||
pitchEvents.clear();
|
||||
pitchEvents.push_back({ 0, 0.0f });
|
||||
|
||||
pitchBend = 0;
|
||||
activeNotes = 0;
|
||||
internalClock = 0;
|
||||
absl::c_fill(noteOnTimes, 0);
|
||||
absl::c_fill(noteOffTimes, 0);
|
||||
}
|
||||
|
||||
void sfz::MidiState::resetAllControllers(int delay) noexcept
|
||||
{
|
||||
for (unsigned idx = 0; idx < config::numCCs; idx++)
|
||||
cc[idx] = 0;
|
||||
for (int ccIdx = 0; ccIdx < config::numCCs; ++ccIdx)
|
||||
ccEvent(delay, ccIdx, 0.0f);
|
||||
|
||||
pitchBend = 0;
|
||||
pitchBendEvent(delay, 0.0f);
|
||||
}
|
||||
|
||||
const sfz::EventVector& sfz::MidiState::getCCEvents(int ccIdx) const noexcept
|
||||
{
|
||||
if (ccIdx < 0 || ccIdx > config::numCCs)
|
||||
return nullEvent;
|
||||
|
||||
return cc[ccIdx];
|
||||
}
|
||||
|
||||
const sfz::EventVector& sfz::MidiState::getPitchEvents() const noexcept
|
||||
{
|
||||
return pitchEvents;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@
|
|||
// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz
|
||||
|
||||
#pragma once
|
||||
#include <chrono>
|
||||
#include <array>
|
||||
#include "CCMap.h"
|
||||
#include "Range.h"
|
||||
|
|
@ -29,7 +28,7 @@ public:
|
|||
* @param noteNumber
|
||||
* @param velocity
|
||||
*/
|
||||
void noteOnEvent(int delay, int noteNumber, float velocity) noexcept;
|
||||
void noteOnEvent(int delay, int noteNumber, float velocity) noexcept;
|
||||
|
||||
/**
|
||||
* @brief Update the state after a note off event
|
||||
|
|
@ -37,39 +36,55 @@ public:
|
|||
* @param noteNumber
|
||||
* @param velocity
|
||||
*/
|
||||
void noteOffEvent(int delay, int noteNumber, float velocity) noexcept;
|
||||
void noteOffEvent(int delay, int noteNumber, float velocity) noexcept;
|
||||
|
||||
int getActiveNotes() const noexcept { return activeNotes; }
|
||||
|
||||
/**
|
||||
* @brief Register a note off and get the note duration
|
||||
* @brief Get the note duration since note on
|
||||
*
|
||||
* @param noteNumber
|
||||
* @param delay
|
||||
* @return float
|
||||
*/
|
||||
float getNoteDuration(int noteNumber) const;
|
||||
float getNoteDuration(int noteNumber, int delay = 0) const;
|
||||
|
||||
/**
|
||||
* @brief Set the maximum size of the blocks for the callback. The actual
|
||||
* size can be lower in each callback but should not be larger
|
||||
* than this value.
|
||||
*
|
||||
* @param samplesPerBlock
|
||||
*/
|
||||
void setSamplesPerBlock(int samplesPerBlock) noexcept;
|
||||
/**
|
||||
* @brief Set the sample rate. If you do not call it it is initialized
|
||||
* to sfz::config::defaultSampleRate.
|
||||
*
|
||||
* @param sampleRate
|
||||
*/
|
||||
void setSampleRate(float sampleRate) noexcept;
|
||||
/**
|
||||
* @brief Get the note on velocity for a given note
|
||||
*
|
||||
* @param noteNumber
|
||||
* @return float
|
||||
*/
|
||||
float getNoteVelocity(int noteNumber) const noexcept;
|
||||
float getNoteVelocity(int noteNumber) const noexcept;
|
||||
|
||||
/**
|
||||
* @brief Register a pitch bend event
|
||||
*
|
||||
* @param pitchBendValue
|
||||
*/
|
||||
void pitchBendEvent(int delay, int pitchBendValue) noexcept;
|
||||
void pitchBendEvent(int delay, float pitchBendValue) noexcept;
|
||||
|
||||
/**
|
||||
* @brief Get the pitch bend status
|
||||
|
||||
* @return int
|
||||
*/
|
||||
int getPitchBend() const noexcept;
|
||||
float getPitchBend() const noexcept;
|
||||
|
||||
/**
|
||||
* @brief Register a CC event
|
||||
|
|
@ -79,6 +94,14 @@ public:
|
|||
*/
|
||||
void ccEvent(int delay, int ccNumber, float ccValue) noexcept;
|
||||
|
||||
/**
|
||||
* @brief Advances the internal clock of a given amount of samples.
|
||||
* You should call this at each callback. This will flush the events
|
||||
* in the midistate memory.
|
||||
*
|
||||
* @param numSamples the number of samples of clock advance
|
||||
*/
|
||||
void advanceTime(int numSamples) noexcept;
|
||||
/**
|
||||
* @brief Get the CC value for CC number
|
||||
*
|
||||
|
|
@ -91,57 +114,57 @@ public:
|
|||
* @brief Reset the midi state (does not impact the last note on time)
|
||||
*
|
||||
*/
|
||||
void reset(int delay) noexcept;
|
||||
void reset() noexcept;
|
||||
|
||||
/**
|
||||
* @brief Reset all the controllers
|
||||
*/
|
||||
void resetAllControllers(int delay) noexcept;
|
||||
|
||||
/**
|
||||
* @brief Modulate a value using the last entered CCs in the midiState
|
||||
*
|
||||
* @tparam T
|
||||
* @tparam U
|
||||
* @param value the base value
|
||||
* @param modifiers the list of CC modifiers
|
||||
* @param validRange a range to clamp the output
|
||||
* @param lambda the function to apply for each modifier
|
||||
* @return T
|
||||
*/
|
||||
template<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);
|
||||
}
|
||||
const EventVector& getCCEvents(int ccIdx) const noexcept;
|
||||
const EventVector& getPitchEvents() const noexcept;
|
||||
|
||||
private:
|
||||
template<class T>
|
||||
using MidiNoteArray = std::array<T, 128>;
|
||||
using NoteOnTime = std::chrono::steady_clock::time_point;
|
||||
int activeNotes { 0 };
|
||||
int activeNotes { 0 };
|
||||
|
||||
/**
|
||||
* @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
|
||||
* depressed notes.
|
||||
*
|
||||
*/
|
||||
MidiNoteArray<float> lastNoteVelocities;
|
||||
MidiNoteArray<float> lastNoteVelocities;
|
||||
|
||||
/**
|
||||
* @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 };
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -124,6 +124,22 @@ bool sfz::Region::parseOpcode(const Opcode& opcode)
|
|||
DBG("Unkown off mode:" << std::string(opcode.value));
|
||||
}
|
||||
break;
|
||||
case hash("note_polyphony"):
|
||||
if (auto value = readOpcode(opcode.value, Default::polyphonyRange))
|
||||
notePolyphony = *value;
|
||||
break;
|
||||
case hash("note_selfmask"):
|
||||
switch (hash(opcode.value)) {
|
||||
case hash("on"):
|
||||
selfMask = SfzSelfMask::mask;
|
||||
break;
|
||||
case hash("off"):
|
||||
selfMask = SfzSelfMask::dontMask;
|
||||
break;
|
||||
default:
|
||||
DBG("Unkown self mask value:" << std::string(opcode.value));
|
||||
}
|
||||
break;
|
||||
// Region logic: key mapping
|
||||
case hash("lokey"):
|
||||
setRangeStartFromOpcode(opcode, keyRange, Default::keyRange);
|
||||
|
|
@ -149,10 +165,12 @@ bool sfz::Region::parseOpcode(const Opcode& opcode)
|
|||
|
||||
// Region logic: MIDI conditions
|
||||
case hash("lobend"):
|
||||
setRangeStartFromOpcode(opcode, bendRange, Default::bendRange);
|
||||
if (auto value = readOpcode(opcode.value, Default::bendRange))
|
||||
bendRange.setStart(normalizeBend(*value));
|
||||
break;
|
||||
case hash("hibend"):
|
||||
setRangeEndFromOpcode(opcode, bendRange, Default::bendRange);
|
||||
if (auto value = readOpcode(opcode.value, Default::bendRange))
|
||||
bendRange.setEnd(normalizeBend(*value));
|
||||
break;
|
||||
case hash("locc&"):
|
||||
if (opcode.parameters.back() > config::numCCs)
|
||||
|
|
@ -276,32 +294,51 @@ bool sfz::Region::parseOpcode(const Opcode& opcode)
|
|||
case hash("gain_cc&"):
|
||||
case hash("gain_oncc&"): // fallthrough
|
||||
case hash("volume_oncc&"):
|
||||
setCCPairFromOpcode(opcode, volumeCC, Default::volumeCCRange);
|
||||
if (opcode.parameters.back() > config::numCCs)
|
||||
return false;
|
||||
if (auto value = readOpcode(opcode.value, Default::volumeCCRange))
|
||||
volumeCC[opcode.parameters.back()] = *value;
|
||||
break;
|
||||
case hash("amplitude"):
|
||||
setValueFromOpcode(opcode, amplitude, Default::amplitudeRange);
|
||||
if (auto value = readOpcode(opcode.value, Default::amplitudeRange))
|
||||
amplitude = normalizePercents(*value);
|
||||
break;
|
||||
case hash("amplitude_cc&"): // fallthrough
|
||||
case hash("amplitude_oncc&"):
|
||||
setCCPairFromOpcode(opcode, amplitudeCC, Default::amplitudeRange);
|
||||
if (opcode.parameters.back() > config::numCCs)
|
||||
return false;
|
||||
if (auto value = readOpcode(opcode.value, Default::amplitudeRange))
|
||||
amplitudeCC[opcode.parameters.back()] = normalizePercents(*value);
|
||||
break;
|
||||
case hash("pan"):
|
||||
setValueFromOpcode(opcode, pan, Default::panRange);
|
||||
if (auto value = readOpcode(opcode.value, Default::panRange))
|
||||
pan = normalizePercents(*value);
|
||||
break;
|
||||
case hash("pan_oncc&"):
|
||||
setCCPairFromOpcode(opcode, panCC, Default::panCCRange);
|
||||
if (opcode.parameters.back() > config::numCCs)
|
||||
return false;
|
||||
if (auto value = readOpcode(opcode.value, Default::panCCRange))
|
||||
panCC[opcode.parameters.back()] = normalizePercents(*value);
|
||||
break;
|
||||
case hash("position"):
|
||||
setValueFromOpcode(opcode, position, Default::positionRange);
|
||||
if (auto value = readOpcode(opcode.value, Default::positionRange))
|
||||
position = normalizePercents(*value);
|
||||
break;
|
||||
case hash("position_oncc&"):
|
||||
setCCPairFromOpcode(opcode, positionCC, Default::positionCCRange);
|
||||
if (opcode.parameters.back() > config::numCCs)
|
||||
return false;
|
||||
if (auto value = readOpcode(opcode.value, Default::positionCCRange))
|
||||
positionCC[opcode.parameters.back()] = normalizePercents(*value);
|
||||
break;
|
||||
case hash("width"):
|
||||
setValueFromOpcode(opcode, width, Default::widthRange);
|
||||
if (auto value = readOpcode(opcode.value, Default::widthRange))
|
||||
width = normalizePercents(*value);
|
||||
break;
|
||||
case hash("width_oncc&"):
|
||||
setCCPairFromOpcode(opcode, widthCC, Default::widthCCRange);
|
||||
if (opcode.parameters.back() > config::numCCs)
|
||||
return false;
|
||||
if (auto value = readOpcode(opcode.value, Default::widthCCRange))
|
||||
widthCC[opcode.parameters.back()] = normalizePercents(*value);
|
||||
break;
|
||||
case hash("amp_keycenter"):
|
||||
setValueFromOpcode(opcode, ampKeycenter, Default::keyRange);
|
||||
|
|
@ -682,6 +719,15 @@ bool sfz::Region::parseOpcode(const Opcode& opcode)
|
|||
case hash("pitch"):
|
||||
setValueFromOpcode(opcode, tune, Default::tuneRange);
|
||||
break;
|
||||
case hash("tune_cc&"):
|
||||
case hash("tune_oncc&"):
|
||||
case hash("pitch_cc&"):
|
||||
case hash("pitch_oncc&"):
|
||||
if (opcode.parameters.back() > config::numCCs)
|
||||
return false;
|
||||
if (auto value = readOpcode(opcode.value, Default::tuneCCRange))
|
||||
tuneCC[opcode.parameters.back()] = *value;
|
||||
break;
|
||||
case hash("bend_up"):
|
||||
setValueFromOpcode(opcode, bendUp, Default::bendBoundRange);
|
||||
break;
|
||||
|
|
@ -905,7 +951,7 @@ bool sfz::Region::registerCC(int ccNumber, float ccValue) noexcept
|
|||
return false;
|
||||
}
|
||||
|
||||
void sfz::Region::registerPitchWheel(int pitch) noexcept
|
||||
void sfz::Region::registerPitchWheel(float pitch) noexcept
|
||||
{
|
||||
if (bendRange.containsWithEnd(pitch))
|
||||
pitchSwitched = true;
|
||||
|
|
@ -938,7 +984,7 @@ float sfz::Region::getBasePitchVariation(int noteNumber, float velocity) const n
|
|||
auto pitchVariationInCents = pitchKeytrack * (noteNumber - (int)pitchKeycenter); // note difference with pitch center
|
||||
pitchVariationInCents += tune; // sample tuning
|
||||
pitchVariationInCents += config::centPerSemitone * transpose; // sample transpose
|
||||
pitchVariationInCents += static_cast<int>(velocity * pitchVeltrack); // track velocity
|
||||
pitchVariationInCents += static_cast<int>(velocity) * pitchVeltrack; // track velocity
|
||||
pitchVariationInCents += pitchDistribution(Random::randomGenerator); // random pitch changes
|
||||
return centsFactor(pitchVariationInCents);
|
||||
}
|
||||
|
|
@ -954,7 +1000,7 @@ float sfz::Region::getBaseVolumedB(int noteNumber) const noexcept
|
|||
|
||||
float sfz::Region::getBaseGain() const noexcept
|
||||
{
|
||||
return normalizePercents(amplitude);
|
||||
return amplitude;
|
||||
}
|
||||
|
||||
float sfz::Region::getPhase() const noexcept
|
||||
|
|
@ -962,7 +1008,7 @@ float sfz::Region::getPhase() const noexcept
|
|||
float phase;
|
||||
if (oscillatorPhase >= 0) {
|
||||
phase = oscillatorPhase * (1.0f / 360.0f);
|
||||
phase -= static_cast<int>(phase);
|
||||
phase -= static_cast<float>(static_cast<int>(phase));
|
||||
} else {
|
||||
std::uniform_real_distribution<float> phaseDist { 0.0001f, 0.9999f };
|
||||
phase = phaseDist(Random::randomGenerator);
|
||||
|
|
@ -997,48 +1043,6 @@ uint32_t sfz::Region::loopEnd(Oversampling factor) const noexcept
|
|||
return loopRange.getEnd() * static_cast<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
|
||||
{
|
||||
ASSERT(velocity >= 0.0f && velocity <= 1.0f);
|
||||
|
|
|
|||
|
|
@ -113,7 +113,7 @@ struct Region {
|
|||
*
|
||||
* @param pitch
|
||||
*/
|
||||
void registerPitchWheel(int pitch) noexcept;
|
||||
void registerPitchWheel(float pitch) noexcept;
|
||||
/**
|
||||
* @brief Register a new aftertouch event.
|
||||
*
|
||||
|
|
@ -240,13 +240,15 @@ struct Region {
|
|||
uint32_t group { Default::group }; // group
|
||||
absl::optional<uint32_t> offBy {}; // off_by
|
||||
SfzOffMode offMode { Default::offMode }; // off_mode
|
||||
absl::optional<uint32_t> notePolyphony {};
|
||||
SfzSelfMask selfMask { Default::selfMask };
|
||||
|
||||
// Region logic: key mapping
|
||||
Range<uint8_t> keyRange { Default::keyRange }; //lokey, hikey and key
|
||||
Range<float> velocityRange { Default::velocityRange }; // hivel and lovel
|
||||
|
||||
// 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 };
|
||||
Range<uint8_t> keyswitchRange { Default::keyRange }; // sw_hikey and sw_lokey
|
||||
absl::optional<uint8_t> keyswitch {}; // sw_last
|
||||
|
|
@ -270,15 +272,15 @@ struct Region {
|
|||
|
||||
// Performance parameters: amplifier
|
||||
float volume { Default::volume }; // volume
|
||||
float amplitude { Default::amplitude }; // amplitude
|
||||
float pan { Default::pan }; // pan
|
||||
float width { Default::width }; // width
|
||||
float position { Default::position }; // position
|
||||
absl::optional<CCValuePair<float>> volumeCC; // volume_oncc
|
||||
absl::optional<CCValuePair<float>> amplitudeCC; // amplitude_oncc
|
||||
absl::optional<CCValuePair<float>> panCC; // pan_oncc
|
||||
absl::optional<CCValuePair<float>> widthCC; // width_oncc
|
||||
absl::optional<CCValuePair<float>> positionCC; // position_oncc
|
||||
float amplitude { normalizePercents(Default::amplitude) }; // amplitude
|
||||
float pan { normalizePercents(Default::pan) }; // pan
|
||||
float width { normalizePercents(Default::width) }; // width
|
||||
float position { normalizePercents(Default::position) }; // position
|
||||
CCMap<float> volumeCC { Default::zeroModifier }; // volume_oncc
|
||||
CCMap<float> amplitudeCC { Default::zeroModifier }; // amplitude_oncc
|
||||
CCMap<float> panCC { Default::zeroModifier }; // pan_oncc
|
||||
CCMap<float> widthCC { Default::zeroModifier }; // width_oncc
|
||||
CCMap<float> positionCC { Default::zeroModifier }; // position_oncc
|
||||
uint8_t ampKeycenter { Default::ampKeycenter }; // amp_keycenter
|
||||
float ampKeytrack { Default::ampKeytrack }; // amp_keytrack
|
||||
float ampVeltrack { Default::ampVeltrack }; // amp_keytrack
|
||||
|
|
@ -306,6 +308,7 @@ struct Region {
|
|||
int pitchVeltrack { Default::pitchVeltrack }; // pitch_veltrack
|
||||
int transpose { Default::transpose }; // transpose
|
||||
int tune { Default::tune }; // tune
|
||||
CCMap<int> tuneCC { Default::tune };
|
||||
int bendUp { Default::bendUp };
|
||||
int bendDown { Default::bendDown };
|
||||
int bendStep { Default::bendStep };
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
#pragma once
|
||||
#include "FilePool.h"
|
||||
#include "BufferPool.h"
|
||||
#include "FilterPool.h"
|
||||
#include "EQPool.h"
|
||||
#include "Logger.h"
|
||||
|
|
@ -17,11 +18,25 @@ class WavetableMulti;
|
|||
|
||||
struct Resources
|
||||
{
|
||||
BufferPool bufferPool;
|
||||
MidiState midiState;
|
||||
Logger logger;
|
||||
FilePool filePool { logger };
|
||||
FilterPool filterPool { midiState };
|
||||
EQPool eqPool { midiState };
|
||||
WavetablePool wavePool;
|
||||
|
||||
void setSampleRate(float samplerate)
|
||||
{
|
||||
midiState.setSampleRate(samplerate);
|
||||
filterPool.setSampleRate(samplerate);
|
||||
eqPool.setSampleRate(samplerate);
|
||||
}
|
||||
|
||||
void setSamplesPerBlock(int samplesPerBlock)
|
||||
{
|
||||
bufferPool.setBufferSize(samplesPerBlock);
|
||||
midiState.setSamplesPerBlock(samplesPerBlock);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -538,8 +538,8 @@ namespace _internals {
|
|||
template <class T>
|
||||
inline void snippetRampLinear(T*& output, T& value, T step)
|
||||
{
|
||||
value += step;
|
||||
*output++ = value;
|
||||
value += step;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -566,8 +566,8 @@ namespace _internals {
|
|||
template <class T>
|
||||
inline void snippetRampMultiplicative(T*& output, T& value, T step)
|
||||
{
|
||||
value *= step;
|
||||
*output++ = value;
|
||||
value *= step;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -461,7 +461,7 @@ float sfz::linearRamp<float, true>(absl::Span<float> output, float value, float
|
|||
while (unaligned(out) && out < lastAligned)
|
||||
_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);
|
||||
|
||||
while (out < lastAligned) {
|
||||
|
|
@ -471,7 +471,8 @@ float sfz::linearRamp<float, true>(absl::Span<float> output, float value, float
|
|||
out += TypeAlignment;
|
||||
}
|
||||
|
||||
value = _mm_cvtss_f32(mmValue);
|
||||
value = _mm_cvtss_f32(mmValue) + step;
|
||||
|
||||
while (out < output.end())
|
||||
_internals::snippetRampLinear<float>(out, value, step);
|
||||
return value;
|
||||
|
|
@ -486,7 +487,7 @@ float sfz::multiplicativeRamp<float, true>(absl::Span<float> output, float value
|
|||
while (unaligned(out) && out < lastAligned)
|
||||
_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);
|
||||
|
||||
while (out < lastAligned) {
|
||||
|
|
@ -496,7 +497,7 @@ float sfz::multiplicativeRamp<float, true>(absl::Span<float> output, float value
|
|||
out += TypeAlignment;
|
||||
}
|
||||
|
||||
value = _mm_cvtss_f32(mmValue);
|
||||
value = _mm_cvtss_f32(mmValue) * step;
|
||||
while (out < output.end())
|
||||
_internals::snippetRampMultiplicative<float>(out, value, step);
|
||||
return value;
|
||||
|
|
|
|||
|
|
@ -13,13 +13,16 @@
|
|||
#include "Macros.h"
|
||||
#include "Config.h"
|
||||
#include "MathHelpers.h"
|
||||
#include "SIMDHelpers.h"
|
||||
#include "absl/meta/type_traits.h"
|
||||
#include "Defaults.h"
|
||||
|
||||
namespace sfz
|
||||
{
|
||||
|
||||
using CCNamePair = std::pair<uint16_t, std::string>;
|
||||
|
||||
template <class T>
|
||||
using MidiNoteArray = std::array<T, 128>;
|
||||
template<class ValueType>
|
||||
struct CCValuePair {
|
||||
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
|
||||
*
|
||||
|
|
@ -147,20 +190,18 @@ constexpr float normalizePercents(T percentValue)
|
|||
*/
|
||||
constexpr float normalizeBend(float bendValue)
|
||||
{
|
||||
return min(max(bendValue, -8191.0f), 8191.0f) / 8191.0f;
|
||||
return clamp(bendValue, -8191.0f, 8191.0f) / 8191.0f;
|
||||
}
|
||||
|
||||
namespace literals
|
||||
{
|
||||
inline float operator ""_norm(unsigned long long int value)
|
||||
{
|
||||
if (value > 127)
|
||||
value = 127;
|
||||
namespace literals {
|
||||
inline float operator""_norm(unsigned long long int value)
|
||||
{
|
||||
if (value > 127)
|
||||
value = 127;
|
||||
|
||||
return normalize7Bits(value);
|
||||
return normalize7Bits(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @brief Convert a note in string to its equivalent midi note number
|
||||
|
|
@ -232,36 +273,199 @@ bool findDefine(absl::string_view line, absl::string_view& variable, absl::strin
|
|||
*/
|
||||
bool findInclude(absl::string_view line, std::string& path);
|
||||
|
||||
/**
|
||||
* @brief Defines a function that modulates a base value with another one
|
||||
*
|
||||
* @tparam T
|
||||
*/
|
||||
template<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.
|
||||
*
|
||||
* @param base
|
||||
* @param modifier
|
||||
*/
|
||||
inline CXX14_CONSTEXPR void multiplyByCents(float& base, int modifier)
|
||||
inline CXX14_CONSTEXPR float multiplyByCentsModifier(int modifier, float base)
|
||||
{
|
||||
base *= centsFactor(modifier);
|
||||
return base * centsFactor(modifier);
|
||||
}
|
||||
|
||||
template <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
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ void sfz::Synth::onParseFullBlock(const std::string& header, const std::vector<O
|
|||
break;
|
||||
case hash("group"):
|
||||
groupOpcodes = members;
|
||||
handleGroupOpcodes(members);
|
||||
numGroups++;
|
||||
break;
|
||||
case hash("region"):
|
||||
|
|
@ -150,12 +151,14 @@ void sfz::Synth::clear()
|
|||
fileTicket = -1;
|
||||
defaultSwitch = absl::nullopt;
|
||||
defaultPath = "";
|
||||
resources.midiState.reset(0);
|
||||
resources.midiState.reset();
|
||||
ccNames.clear();
|
||||
globalOpcodes.clear();
|
||||
masterOpcodes.clear();
|
||||
groupOpcodes.clear();
|
||||
unknownOpcodes.clear();
|
||||
groupMaxPolyphony.clear();
|
||||
groupMaxPolyphony.push_back(config::maxVoices);
|
||||
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)
|
||||
{
|
||||
for (auto& member : members) {
|
||||
|
|
@ -375,18 +398,22 @@ bool sfz::Synth::loadSfzFile(const fs::path& file)
|
|||
}
|
||||
}
|
||||
|
||||
// Some regions had group number but no "group-level" opcodes handled the polyphony
|
||||
while (groupMaxPolyphony.size() <= region->group)
|
||||
groupMaxPolyphony.push_back(config::maxVoices);
|
||||
|
||||
for (auto note = 0; note < 128; note++) {
|
||||
if (region->keyRange.containsWithEnd(note) || (region->hasKeyswitches() && region->keyswitchRange.containsWithEnd(note)))
|
||||
noteActivationLists[note].push_back(region);
|
||||
}
|
||||
|
||||
for (unsigned cc = 0; cc < config::numCCs; cc++) {
|
||||
for (int cc = 0; cc < config::numCCs; cc++) {
|
||||
if (region->ccTriggers.contains(cc) || region->ccConditions.contains(cc))
|
||||
ccActivationLists[cc].push_back(region);
|
||||
}
|
||||
|
||||
// Defaults
|
||||
for (unsigned cc = 0; cc < config::numCCs; cc++) {
|
||||
for (int cc = 0; cc < config::numCCs; cc++) {
|
||||
region->registerCC(cc, resources.midiState.getCCValue(cc));
|
||||
}
|
||||
|
||||
|
|
@ -479,11 +506,11 @@ void sfz::Synth::setSamplesPerBlock(int samplesPerBlock) noexcept
|
|||
}
|
||||
|
||||
this->samplesPerBlock = samplesPerBlock;
|
||||
this->tempBuffer.resize(samplesPerBlock);
|
||||
this->tempMixNodeBuffer.resize(samplesPerBlock);
|
||||
for (auto& voice : voices)
|
||||
voice->setSamplesPerBlock(samplesPerBlock);
|
||||
|
||||
resources.setSamplesPerBlock(samplesPerBlock);
|
||||
|
||||
for (auto& bus : effectBuses) {
|
||||
if (bus)
|
||||
bus->setSamplesPerBlock(samplesPerBlock);
|
||||
|
|
@ -501,8 +528,7 @@ void sfz::Synth::setSampleRate(float sampleRate) noexcept
|
|||
for (auto& voice : voices)
|
||||
voice->setSampleRate(sampleRate);
|
||||
|
||||
resources.filterPool.setSampleRate(sampleRate);
|
||||
resources.eqPool.setSampleRate(sampleRate);
|
||||
resources.setSampleRate(sampleRate);
|
||||
|
||||
for (auto& bus : effectBuses) {
|
||||
if (bus)
|
||||
|
|
@ -522,9 +548,12 @@ void sfz::Synth::renderBlock(AudioSpan<float> buffer) noexcept
|
|||
return;
|
||||
|
||||
size_t numFrames = buffer.getNumFrames();
|
||||
auto temp = AudioSpan<float>(tempBuffer).first(numFrames);
|
||||
auto tempMixNode = AudioSpan<float>(tempMixNodeBuffer).first(numFrames);
|
||||
|
||||
auto tempSpan = resources.bufferPool.getStereoBuffer(numFrames);
|
||||
auto tempMixSpan = resources.bufferPool.getStereoBuffer(numFrames);
|
||||
if (!tempSpan || !tempMixSpan) {
|
||||
DBG("[sfizz] Could not get a temporary buffer; exiting callback... ");
|
||||
return;
|
||||
}
|
||||
CallbackBreakdown callbackBreakdown;
|
||||
|
||||
{ // Prepare the effect inputs. They are mixes of per-region outputs.
|
||||
|
|
@ -539,7 +568,7 @@ void sfz::Synth::renderBlock(AudioSpan<float> buffer) noexcept
|
|||
{ // Main render block
|
||||
ScopedTiming logger { callbackBreakdown.renderMethod };
|
||||
buffer.fill(0.0f);
|
||||
tempMixNode.fill(0.0f);
|
||||
tempSpan->fill(0.0f);
|
||||
resources.filePool.cleanupPromises();
|
||||
|
||||
for (auto& voice : voices) {
|
||||
|
|
@ -549,14 +578,14 @@ void sfz::Synth::renderBlock(AudioSpan<float> buffer) noexcept
|
|||
const Region* region = voice->getRegion();
|
||||
|
||||
numActiveVoices++;
|
||||
voice->renderBlock(temp);
|
||||
voice->renderBlock(*tempSpan);
|
||||
|
||||
{ // Add the output into the effects linked to this region
|
||||
ScopedTiming logger { callbackBreakdown.effects, ScopedTiming::Operation::addToDuration };
|
||||
for (size_t i = 0, n = effectBuses.size(); i < n; ++i) {
|
||||
if (auto& bus = effectBuses[i]) {
|
||||
float addGain = region->getGainToEffectBus(i);
|
||||
bus->addToInputs(temp, addGain, numFrames);
|
||||
bus->addToInputs(*tempSpan, addGain, numFrames);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -576,7 +605,7 @@ void sfz::Synth::renderBlock(AudioSpan<float> buffer) noexcept
|
|||
for (auto& bus : effectBuses) {
|
||||
if (bus) {
|
||||
bus->process(numFrames);
|
||||
bus->mixOutputsTo(buffer, tempMixNode, numFrames);
|
||||
bus->mixOutputsTo(buffer, *tempMixSpan, numFrames);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -585,11 +614,16 @@ void sfz::Synth::renderBlock(AudioSpan<float> buffer) noexcept
|
|||
// -- note(jpc) the purpose of the Mix output is not known.
|
||||
// perhaps it's designed as extension point for custom processing?
|
||||
// as default behavior, it adds itself to the Main signal.
|
||||
buffer.add(tempMixNode);
|
||||
buffer.add(*tempMixSpan);
|
||||
|
||||
// Apply the master volume
|
||||
buffer.applyGain(db2mag(volume));
|
||||
|
||||
{ // Clear events and advance midi time
|
||||
ScopedTiming logger { dispatchDuration, ScopedTiming::Operation::addToDuration };
|
||||
resources.midiState.advanceTime(buffer.getNumFrames());
|
||||
}
|
||||
|
||||
callbackBreakdown.dispatch = dispatchDuration;
|
||||
resources.logger.logCallbackTime(callbackBreakdown, numActiveVoices, numFrames);
|
||||
|
||||
|
|
@ -655,11 +689,50 @@ void sfz::Synth::noteOnDispatch(int delay, int noteNumber, float velocity) noexc
|
|||
const auto randValue = randNoteDistribution(Random::randomGenerator);
|
||||
for (auto& region : noteActivationLists[noteNumber]) {
|
||||
if (region->registerNoteOn(noteNumber, velocity, randValue)) {
|
||||
unsigned activeNotesInGroup { 0 };
|
||||
unsigned activeNotes { 0 };
|
||||
Voice* selfMaskCandidate { nullptr };
|
||||
|
||||
for (auto& voice : voices) {
|
||||
const auto voiceRegion = voice->getRegion();
|
||||
if (voiceRegion == nullptr)
|
||||
continue;
|
||||
|
||||
if (voiceRegion->group == region->group)
|
||||
activeNotesInGroup += 1;
|
||||
|
||||
if (region->notePolyphony) {
|
||||
if (voice->getTriggerNumber() == noteNumber && voice->getTriggerType() == Voice::TriggerType::NoteOn) {
|
||||
activeNotes += 1;
|
||||
switch (region->selfMask) {
|
||||
case SfzSelfMask::mask:
|
||||
if (voice->getTriggerValue() < velocity) {
|
||||
if (!selfMaskCandidate || selfMaskCandidate->getTriggerValue() > voice->getTriggerValue())
|
||||
selfMaskCandidate = voice.get();
|
||||
}
|
||||
break;
|
||||
case SfzSelfMask::dontMask:
|
||||
if (!selfMaskCandidate || selfMaskCandidate->getSourcePosition() < voice->getSourcePosition())
|
||||
selfMaskCandidate = voice.get();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (voice->checkOffGroup(delay, region->group))
|
||||
noteOffDispatch(delay, voice->getTriggerNumber(), voice->getTriggerValue());
|
||||
}
|
||||
|
||||
if (activeNotesInGroup >= groupMaxPolyphony[region->group])
|
||||
continue;
|
||||
|
||||
if (region->notePolyphony && activeNotes >= *region->notePolyphony) {
|
||||
if (selfMaskCandidate != nullptr)
|
||||
selfMaskCandidate->release(delay);
|
||||
else // We're the lowest velocity guy here
|
||||
continue;
|
||||
}
|
||||
|
||||
auto voice = findFreeVoice();
|
||||
if (voice == nullptr)
|
||||
continue;
|
||||
|
|
@ -705,16 +778,17 @@ void sfz::Synth::pitchWheel(int delay, int pitch) noexcept
|
|||
{
|
||||
ASSERT(pitch <= 8192);
|
||||
ASSERT(pitch >= -8192);
|
||||
const auto normalizedPitch = normalizeBend(pitch);
|
||||
|
||||
ScopedTiming logger { dispatchDuration, ScopedTiming::Operation::addToDuration };
|
||||
resources.midiState.pitchBendEvent(delay, pitch);
|
||||
resources.midiState.pitchBendEvent(delay, normalizedPitch);
|
||||
|
||||
for (auto& region : regions) {
|
||||
region->registerPitchWheel(pitch);
|
||||
region->registerPitchWheel(normalizedPitch);
|
||||
}
|
||||
|
||||
for (auto& voice : voices) {
|
||||
voice->registerPitchWheel(delay, pitch);
|
||||
voice->registerPitchWheel(delay, normalizedPitch);
|
||||
}
|
||||
}
|
||||
void sfz::Synth::aftertouch(int /* delay */, uint8_t /* aftertouch */) noexcept
|
||||
|
|
@ -953,12 +1027,12 @@ void sfz::Synth::resetAllControllers(int delay) noexcept
|
|||
resources.midiState.resetAllControllers(delay);
|
||||
for (auto& voice : voices) {
|
||||
voice->registerPitchWheel(delay, 0);
|
||||
for (unsigned cc = 0; cc < config::numCCs; ++cc)
|
||||
for (int cc = 0; cc < config::numCCs; ++cc)
|
||||
voice->registerCC(delay, cc, 0.0f);
|
||||
}
|
||||
|
||||
for (auto& region : regions) {
|
||||
for (unsigned cc = 0; cc < config::numCCs; ++cc)
|
||||
for (int cc = 0; cc < config::numCCs; ++cc)
|
||||
region->registerCC(cc, 0.0f);
|
||||
}
|
||||
}
|
||||
|
|
@ -1006,3 +1080,11 @@ void sfz::Synth::allSoundOff() noexcept
|
|||
for (auto& effectBus : effectBuses)
|
||||
effectBus->clear();
|
||||
}
|
||||
|
||||
void sfz::Synth::setGroupPolyphony(unsigned groupIdx, unsigned polyphony) noexcept
|
||||
{
|
||||
while (groupMaxPolyphony.size() <= groupIdx)
|
||||
groupMaxPolyphony.push_back(config::maxVoices);
|
||||
|
||||
groupMaxPolyphony[groupIdx] = polyphony;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -410,6 +410,15 @@ protected:
|
|||
void onParseWarning(const SourceRange& range, const std::string& message) override;
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief change the group maximum polyphony
|
||||
*
|
||||
* @param groupIdx the group index
|
||||
* @param polyphone the max polyphony
|
||||
*/
|
||||
void setGroupPolyphony(unsigned groupIdx, unsigned polyphony) noexcept;
|
||||
std::vector<unsigned> groupMaxPolyphony { config::maxVoices };
|
||||
|
||||
/**
|
||||
* @brief Reset all CCs; to be used on CC 121
|
||||
*
|
||||
|
|
@ -440,6 +449,12 @@ private:
|
|||
* @param members the opcodes of the <global> block
|
||||
*/
|
||||
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
|
||||
*
|
||||
|
|
@ -501,10 +516,6 @@ private:
|
|||
// Curves
|
||||
CurveSet curves;
|
||||
|
||||
// Intermediate buffers
|
||||
AudioBuffer<float> tempBuffer { 2, config::defaultSamplesPerBlock };
|
||||
AudioBuffer<float> tempMixNodeBuffer { 2, config::defaultSamplesPerBlock };
|
||||
|
||||
int samplesPerBlock { config::defaultSamplesPerBlock };
|
||||
float sampleRate { config::defaultSampleRate };
|
||||
float volume { Default::globalVolume };
|
||||
|
|
|
|||
|
|
@ -74,50 +74,11 @@ void sfz::Voice::startVoice(Region* region, int delay, int number, float value,
|
|||
speedRatio = static_cast<float>(currentPromise->sampleRate / this->sampleRate);
|
||||
}
|
||||
pitchRatio = region->getBasePitchVariation(number, value);
|
||||
|
||||
baseVolumedB = region->getBaseVolumedB(number);
|
||||
auto volumedB = baseVolumedB;
|
||||
if (region->volumeCC)
|
||||
volumedB += resources.midiState.getCCValue(region->volumeCC->cc) * region->volumeCC->value;
|
||||
volumeEnvelope.reset(db2mag(Default::volumeRange.clamp(volumedB)));
|
||||
|
||||
baseGain = region->getBaseGain();
|
||||
if (triggerType != TriggerType::CC)
|
||||
baseGain *= region->getNoteGain(number, value);
|
||||
|
||||
float gain { baseGain };
|
||||
if (region->amplitudeCC)
|
||||
gain += resources.midiState.getCCValue(region->amplitudeCC->cc) * normalizePercents(region->amplitudeCC->value);
|
||||
amplitudeEnvelope.reset(Default::normalizedRange.clamp(gain));
|
||||
|
||||
float crossfadeGain { region->getCrossfadeGain() };
|
||||
crossfadeEnvelope.reset(Default::normalizedRange.clamp(crossfadeGain));
|
||||
|
||||
basePan = normalizePercents(region->pan);
|
||||
auto pan = basePan;
|
||||
if (region->panCC)
|
||||
pan += resources.midiState.getCCValue(region->panCC->cc) * normalizePercents(region->panCC->value);
|
||||
panEnvelope.reset(Default::symmetricNormalizedRange.clamp(pan));
|
||||
|
||||
basePosition = normalizePercents(region->position);
|
||||
auto position = basePosition;
|
||||
if (region->positionCC)
|
||||
position += resources.midiState.getCCValue(region->positionCC->cc) * normalizePercents(region->positionCC->value);
|
||||
positionEnvelope.reset(Default::symmetricNormalizedRange.clamp(position));
|
||||
|
||||
baseWidth = normalizePercents(region->width);
|
||||
auto width = baseWidth;
|
||||
if (region->widthCC)
|
||||
width += resources.midiState.getCCValue(region->widthCC->cc) * normalizePercents(region->widthCC->value);
|
||||
widthEnvelope.reset(Default::symmetricNormalizedRange.clamp(width));
|
||||
|
||||
pitchBendEnvelope.setFunction([region](float pitchValue){
|
||||
const auto normalizedBend = normalizeBend(pitchValue);
|
||||
const auto bendInCents = normalizedBend > 0.0f ? normalizedBend * static_cast<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
|
||||
ASSERT((filters.capacity() - filters.size()) >= region->filters.size());
|
||||
ASSERT((equalizers.capacity() - equalizers.size()) >= region->equalizers.size());
|
||||
|
|
@ -198,48 +159,14 @@ void sfz::Voice::registerCC(int delay, int ccNumber, float ccValue) noexcept
|
|||
|
||||
if (region->checkSustain && noteIsOff && ccNumber == config::sustainCC && ccValue < config::halfCCThreshold)
|
||||
release(delay);
|
||||
|
||||
// Add a minimum delay for smoothing the envelopes
|
||||
// TODO: this feels like a hack, revisit this along with the smoothed envelopes...
|
||||
delay = max(delay, minEnvelopeDelay);
|
||||
|
||||
if (region->amplitudeCC && ccNumber == region->amplitudeCC->cc) {
|
||||
const float newGain { baseGain + ccValue * normalizePercents(region->amplitudeCC->value) };
|
||||
amplitudeEnvelope.registerEvent(delay, Default::normalizedRange.clamp(newGain));
|
||||
}
|
||||
|
||||
if (region->volumeCC && ccNumber == region->volumeCC->cc) {
|
||||
const float newVolumedB { baseVolumedB + ccValue * region->volumeCC->value };
|
||||
volumeEnvelope.registerEvent(delay, db2mag(Default::volumeRange.clamp(newVolumedB)));
|
||||
}
|
||||
|
||||
if (region->panCC && ccNumber == region->panCC->cc) {
|
||||
const float newPan { basePan + ccValue * normalizePercents(region->panCC->value) };
|
||||
panEnvelope.registerEvent(delay, Default::symmetricNormalizedRange.clamp(newPan));
|
||||
}
|
||||
|
||||
if (region->positionCC && ccNumber == region->positionCC->cc) {
|
||||
const float newPosition { basePosition + ccValue * normalizePercents(region->positionCC->value) };
|
||||
positionEnvelope.registerEvent(delay, Default::symmetricNormalizedRange.clamp(newPosition));
|
||||
}
|
||||
|
||||
if (region->widthCC && ccNumber == region->widthCC->cc) {
|
||||
const float newWidth { baseWidth + ccValue * normalizePercents(region->widthCC->value) };
|
||||
widthEnvelope.registerEvent(delay, Default::symmetricNormalizedRange.clamp(newWidth));
|
||||
}
|
||||
|
||||
if (region->crossfadeCCInRange.contains(ccNumber) || region->crossfadeCCOutRange.contains(ccNumber)) {
|
||||
const float crossfadeGain = region->getCrossfadeGain();
|
||||
crossfadeEnvelope.registerEvent(delay, Default::normalizedRange.clamp(crossfadeGain));
|
||||
}
|
||||
}
|
||||
|
||||
void sfz::Voice::registerPitchWheel(int delay, int pitch) noexcept
|
||||
void sfz::Voice::registerPitchWheel(int delay, float pitch) noexcept
|
||||
{
|
||||
if (state == State::idle)
|
||||
return;
|
||||
|
||||
pitchBendEnvelope.registerEvent(delay, static_cast<float>(pitch));
|
||||
UNUSED(delay);
|
||||
UNUSED(pitch);
|
||||
}
|
||||
|
||||
void sfz::Voice::registerAftertouch(int delay, uint8_t aftertouch) noexcept
|
||||
|
|
@ -267,14 +194,6 @@ void sfz::Voice::setSamplesPerBlock(int samplesPerBlock) noexcept
|
|||
{
|
||||
this->samplesPerBlock = samplesPerBlock;
|
||||
this->minEnvelopeDelay = samplesPerBlock / 2;
|
||||
tempBuffer1.resize(samplesPerBlock);
|
||||
tempBuffer2.resize(samplesPerBlock);
|
||||
tempBuffer3.resize(samplesPerBlock);
|
||||
indexBuffer.resize(samplesPerBlock);
|
||||
tempSpan1 = absl::MakeSpan(tempBuffer1);
|
||||
tempSpan2 = absl::MakeSpan(tempBuffer2);
|
||||
tempSpan3 = absl::MakeSpan(tempBuffer3);
|
||||
indexSpan = absl::MakeSpan(indexBuffer);
|
||||
}
|
||||
|
||||
void sfz::Voice::renderBlock(AudioSpan<float> buffer) noexcept
|
||||
|
|
@ -299,10 +218,15 @@ void sfz::Voice::renderBlock(AudioSpan<float> buffer) noexcept
|
|||
fillWithData(delayed_buffer);
|
||||
}
|
||||
|
||||
if (region->isStereo)
|
||||
processStereo(buffer);
|
||||
else
|
||||
processMono(buffer);
|
||||
if (region->isStereo) {
|
||||
ampStageStereo(buffer);
|
||||
panStageStereo(buffer);
|
||||
filterStageStereo(buffer);
|
||||
} else {
|
||||
ampStageMono(buffer);
|
||||
filterStageMono(buffer);
|
||||
panStageMono(buffer);
|
||||
}
|
||||
|
||||
if (!egEnvelope.isSmoothing())
|
||||
reset();
|
||||
|
|
@ -311,120 +235,213 @@ void sfz::Voice::renderBlock(AudioSpan<float> buffer) noexcept
|
|||
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();
|
||||
auto leftBuffer = buffer.getSpan(0);
|
||||
auto rightBuffer = buffer.getSpan(1);
|
||||
const auto leftBuffer = buffer.getSpan(0);
|
||||
const auto xfCurve = region->crossfadeCCCurve;
|
||||
|
||||
auto modulationSpan = tempSpan1.first(numSamples);
|
||||
auto modulationSpan = resources.bufferPool.getBuffer(numSamples);
|
||||
auto tempSpan = resources.bufferPool.getBuffer(numSamples);
|
||||
if (!modulationSpan || !tempSpan)
|
||||
return;
|
||||
|
||||
{ // Amplitude processing
|
||||
ScopedTiming logger { amplitudeDuration };
|
||||
// Amplitude envelope
|
||||
fill<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
|
||||
amplitudeEnvelope.getBlock(modulationSpan);
|
||||
applyGain<float>(modulationSpan, leftBuffer);
|
||||
// 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);
|
||||
}
|
||||
applyGain<float>(*modulationSpan, leftBuffer);
|
||||
|
||||
// Crossfade envelope
|
||||
crossfadeEnvelope.getBlock(modulationSpan);
|
||||
applyGain<float>(modulationSpan, leftBuffer);
|
||||
// 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);
|
||||
}
|
||||
applyGain<float>(*modulationSpan, leftBuffer);
|
||||
|
||||
// Volume envelope
|
||||
volumeEnvelope.getBlock(modulationSpan);
|
||||
applyGain<float>(modulationSpan, leftBuffer);
|
||||
// AmpEG envelope
|
||||
egEnvelope.getBlock(*modulationSpan);
|
||||
applyGain<float>(*modulationSpan, leftBuffer);
|
||||
}
|
||||
|
||||
// AmpEG envelope
|
||||
egEnvelope.getBlock(modulationSpan);
|
||||
applyGain<float>(modulationSpan, leftBuffer);
|
||||
void sfz::Voice::ampStageStereo(AudioSpan<float> buffer) noexcept
|
||||
{
|
||||
ScopedTiming logger { amplitudeDuration };
|
||||
|
||||
const auto numSamples = buffer.getNumFrames();
|
||||
|
||||
const auto xfCurve = region->crossfadeCCCurve;
|
||||
|
||||
auto modulationSpan = resources.bufferPool.getBuffer(numSamples);
|
||||
auto tempSpan = resources.bufferPool.getBuffer(numSamples);
|
||||
if (!modulationSpan || !tempSpan)
|
||||
return;
|
||||
|
||||
// Amplitude envelope
|
||||
fill<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
|
||||
ScopedTiming logger { filterDuration };
|
||||
|
||||
const float* inputChannel[1] { leftBuffer.data() };
|
||||
float* outputChannel[1] { leftBuffer.data() };
|
||||
for (auto& filter: filters) {
|
||||
filter->process(inputChannel, outputChannel, numSamples);
|
||||
}
|
||||
|
||||
for (auto& eq: equalizers) {
|
||||
eq->process(inputChannel, outputChannel, numSamples);
|
||||
}
|
||||
}
|
||||
|
||||
{ // Panning and stereo processing
|
||||
ScopedTiming logger { panningDuration };
|
||||
|
||||
// Prepare for stereo output
|
||||
copy<float>(leftBuffer, rightBuffer);
|
||||
|
||||
// Apply panning
|
||||
panEnvelope.getBlock(modulationSpan);
|
||||
pan<float>(modulationSpan, leftBuffer, rightBuffer);
|
||||
for (auto& eq : equalizers) {
|
||||
eq->process(inputChannel, outputChannel, numSamples);
|
||||
}
|
||||
}
|
||||
|
||||
void sfz::Voice::processStereo(AudioSpan<float> buffer) noexcept
|
||||
void sfz::Voice::filterStageStereo(AudioSpan<float> buffer) noexcept
|
||||
{
|
||||
ScopedTiming logger { filterDuration };
|
||||
const auto numSamples = buffer.getNumFrames();
|
||||
auto modulationSpan = tempSpan1.first(numSamples);
|
||||
auto leftBuffer = buffer.getSpan(0);
|
||||
auto rightBuffer = buffer.getSpan(1);
|
||||
const auto leftBuffer = buffer.getSpan(0);
|
||||
const auto rightBuffer = buffer.getSpan(1);
|
||||
|
||||
{ // Amplitude processing
|
||||
ScopedTiming logger { amplitudeDuration };
|
||||
const float* inputChannels[2] { leftBuffer.data(), rightBuffer.data() };
|
||||
float* outputChannels[2] { leftBuffer.data(), rightBuffer.data() };
|
||||
|
||||
// Amplitude envelope
|
||||
amplitudeEnvelope.getBlock(modulationSpan);
|
||||
buffer.applyGain(modulationSpan);
|
||||
|
||||
// Crossfade envelope
|
||||
crossfadeEnvelope.getBlock(modulationSpan);
|
||||
buffer.applyGain(modulationSpan);
|
||||
|
||||
// Volume envelope
|
||||
volumeEnvelope.getBlock(modulationSpan);
|
||||
buffer.applyGain(modulationSpan);
|
||||
|
||||
// AmpEG envelope
|
||||
egEnvelope.getBlock(modulationSpan);
|
||||
buffer.applyGain(modulationSpan);
|
||||
for (auto& filter : filters) {
|
||||
filter->process(inputChannels, outputChannels, numSamples);
|
||||
}
|
||||
|
||||
{ // Panning and stereo processing
|
||||
ScopedTiming logger { panningDuration };
|
||||
|
||||
// Apply panning
|
||||
panEnvelope.getBlock(modulationSpan);
|
||||
pan<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);
|
||||
}
|
||||
for (auto& eq : equalizers) {
|
||||
eq->process(inputChannels, outputChannels, numSamples);
|
||||
}
|
||||
}
|
||||
|
||||
void sfz::Voice::fillWithData(AudioSpan<float> buffer) noexcept
|
||||
{
|
||||
if (buffer.getNumFrames() == 0)
|
||||
const auto numSamples = buffer.getNumFrames();
|
||||
if (numSamples == 0)
|
||||
return;
|
||||
|
||||
if (currentPromise == nullptr) {
|
||||
|
|
@ -433,30 +450,46 @@ void sfz::Voice::fillWithData(AudioSpan<float> buffer) noexcept
|
|||
}
|
||||
|
||||
auto source = currentPromise->getData();
|
||||
auto indices = indexSpan.first(buffer.getNumFrames());
|
||||
auto jumps = tempSpan1.first(buffer.getNumFrames());
|
||||
auto bends = tempSpan2.first(buffer.getNumFrames());
|
||||
auto leftCoeffs = tempSpan1.first(buffer.getNumFrames());
|
||||
auto rightCoeffs = tempSpan2.first(buffer.getNumFrames());
|
||||
|
||||
fill<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)
|
||||
pitchBendEnvelope.getQuantizedBlock(bends, bendStepFactor);
|
||||
multiplicativeEnvelope(events, *bends, bendLambda, bendStepFactor);
|
||||
else
|
||||
pitchBendEnvelope.getBlock(bends);
|
||||
multiplicativeEnvelope(events, *bends, bendLambda);
|
||||
applyGain<float>(*bends, *jumps);
|
||||
|
||||
applyGain<float>(bends, jumps);
|
||||
jumps[0] += floatPositionOffset;
|
||||
cumsum<float>(jumps, jumps);
|
||||
sfzInterpolationCast<float>(jumps, indices, leftCoeffs, rightCoeffs);
|
||||
add<int>(sourcePosition, indices);
|
||||
for (const auto& mod : region->tuneCC) {
|
||||
const auto events = resources.midiState.getCCEvents(mod.cc);
|
||||
multiplicativeEnvelope(events, *bends, [&](float x) { return centsFactor(x * mod.value); });
|
||||
applyGain<float>(*bends, *jumps);
|
||||
}
|
||||
|
||||
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()) {
|
||||
const auto loopEnd = static_cast<int>(region->loopEnd(currentPromise->oversamplingFactor));
|
||||
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) {
|
||||
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 });
|
||||
}
|
||||
}
|
||||
|
|
@ -465,46 +498,46 @@ void sfz::Voice::fillWithData(AudioSpan<float> buffer) noexcept
|
|||
static_cast<int>(region->trueSampleEnd(currentPromise->oversamplingFactor)),
|
||||
static_cast<int>(source.getNumFrames())
|
||||
) - 2;
|
||||
for (auto* index = indices.begin(); index < indices.end(); ++index) {
|
||||
for (auto* index = indices->begin(); index < indices->end(); ++index) {
|
||||
if (*index >= sampleEnd) {
|
||||
release(static_cast<int>(std::distance(indices.begin(), index)));
|
||||
const auto remainingElements = static_cast<size_t>(std::distance(index, indices.end()));
|
||||
release(static_cast<int>(std::distance(indices->begin(), index)));
|
||||
const auto remainingElements = static_cast<size_t>(std::distance(index, indices->end()));
|
||||
if (source.getNumFrames() - 1 < region->trueSampleEnd(currentPromise->oversamplingFactor)) {
|
||||
DBG("[sfizz] Underflow: source available samples "
|
||||
<< source.getNumFrames() << "/"
|
||||
<< region->trueSampleEnd(currentPromise->oversamplingFactor)
|
||||
<< " for sample " << region->sample);
|
||||
}
|
||||
fill<int>(indices.last(remainingElements), sampleEnd);
|
||||
fill<float>(leftCoeffs.last(remainingElements), 0.0f);
|
||||
fill<float>(rightCoeffs.last(remainingElements), 1.0f);
|
||||
fill<int>(indices->last(remainingElements), sampleEnd);
|
||||
fill<float>(leftCoeffs->last(remainingElements), 0.0f);
|
||||
fill<float>(rightCoeffs->last(remainingElements), 1.0f);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto ind = indices.data();
|
||||
auto leftCoeff = leftCoeffs.data();
|
||||
auto rightCoeff = rightCoeffs.data();
|
||||
auto ind = indices->data();
|
||||
auto leftCoeff = leftCoeffs->data();
|
||||
auto rightCoeff = rightCoeffs->data();
|
||||
auto leftSource = source.getConstSpan(0);
|
||||
auto left = buffer.getChannel(0);
|
||||
if (source.getNumChannels() == 1) {
|
||||
while (ind < indices.end()) {
|
||||
while (ind < indices->end()) {
|
||||
*left = linearInterpolation(leftSource[*ind], leftSource[*ind + 1], *leftCoeff, *rightCoeff);
|
||||
incrementAll(ind, left, leftCoeff, rightCoeff);
|
||||
}
|
||||
} else {
|
||||
auto right = buffer.getChannel(1);
|
||||
auto rightSource = source.getConstSpan(1);
|
||||
while (ind < indices.end()) {
|
||||
while (ind < indices->end()) {
|
||||
*left = linearInterpolation(leftSource[*ind], leftSource[*ind + 1], *leftCoeff, *rightCoeff);
|
||||
*right = linearInterpolation(rightSource[*ind], rightSource[*ind + 1], *leftCoeff, *rightCoeff);
|
||||
incrementAll(ind, left, right, leftCoeff, rightCoeff);
|
||||
}
|
||||
}
|
||||
|
||||
sourcePosition = indices.back();
|
||||
floatPositionOffset = rightCoeffs.back();
|
||||
sourcePosition = indices->back();
|
||||
floatPositionOffset = rightCoeffs->back();
|
||||
}
|
||||
|
||||
void sfz::Voice::fillWithGenerator(AudioSpan<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(rightSpan, [&](){ return noiseDist(Random::randomGenerator); });
|
||||
} else {
|
||||
// wavetables for sine and other generators
|
||||
auto frequencies = tempSpan1.first(buffer.getNumFrames());
|
||||
auto bends = tempSpan2.first(buffer.getNumFrames());
|
||||
const auto numSamples = buffer.getNumFrames();
|
||||
auto frequencies = resources.bufferPool.getBuffer(numSamples);
|
||||
auto bends = resources.bufferPool.getBuffer(numSamples);
|
||||
if (!frequencies || !bends)
|
||||
return;
|
||||
|
||||
float keycenterFrequency = midiNoteFrequency(region->pitchKeycenter);
|
||||
fill<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)
|
||||
pitchBendEnvelope.getQuantizedBlock(bends, bendStepFactor);
|
||||
multiplicativeEnvelope(events, *bends, bendLambda, bendStepFactor);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -603,6 +648,6 @@ void sfz::Voice::setMaxFiltersPerVoice(size_t numFilters)
|
|||
void sfz::Voice::setMaxEQsPerVoice(size_t numFilters)
|
||||
{
|
||||
// There are filters in there, this call is unexpected
|
||||
ASSERT(filters.size() == 0);
|
||||
filters.reserve(numFilters);
|
||||
ASSERT(equalizers.size() == 0);
|
||||
equalizers.reserve(numFilters);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@
|
|||
#pragma once
|
||||
#include "Config.h"
|
||||
#include "ADSREnvelope.h"
|
||||
#include "EventEnvelopes.h"
|
||||
#include "HistoricalBuffer.h"
|
||||
#include "Region.h"
|
||||
#include "AudioBuffer.h"
|
||||
|
|
@ -107,7 +106,7 @@ public:
|
|||
* @param delay
|
||||
* @param pitch
|
||||
*/
|
||||
void registerPitchWheel(int delay, int pitch) noexcept;
|
||||
void registerPitchWheel(int delay, float pitch) noexcept;
|
||||
/**
|
||||
* @brief Register an aftertouch event; for now this does nothing
|
||||
*
|
||||
|
|
@ -210,6 +209,13 @@ public:
|
|||
* @param numFilters
|
||||
*/
|
||||
void setMaxEQsPerVoice(size_t numEQs);
|
||||
/**
|
||||
* @brief Release the voice after a given delay
|
||||
*
|
||||
* @param delay
|
||||
* @param fastRelease whether to do a normal release or cut the voice abruptly
|
||||
*/
|
||||
void release(int delay, bool fastRelease = false) noexcept;
|
||||
|
||||
Duration getLastDataDuration() const noexcept { return dataDuration; }
|
||||
Duration getLastAmplitudeDuration() const noexcept { return amplitudeDuration; }
|
||||
|
|
@ -231,25 +237,13 @@ private:
|
|||
* @param buffer
|
||||
*/
|
||||
void fillWithGenerator(AudioSpan<float> buffer) noexcept;
|
||||
/**
|
||||
* @brief The function processing a mono sample source
|
||||
*
|
||||
* @param buffer
|
||||
*/
|
||||
void processMono(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;
|
||||
void ampStageMono(AudioSpan<float> buffer) noexcept;
|
||||
void ampStageStereo(AudioSpan<float> buffer) noexcept;
|
||||
void panStageMono(AudioSpan<float> buffer) noexcept;
|
||||
void panStageStereo(AudioSpan<float> buffer) noexcept;
|
||||
void filterStageMono(AudioSpan<float> buffer) noexcept;
|
||||
void filterStageStereo(AudioSpan<float> buffer) noexcept;
|
||||
|
||||
Region* region { nullptr };
|
||||
|
||||
enum class State {
|
||||
|
|
@ -268,9 +262,6 @@ private:
|
|||
float pitchRatio { 1.0 };
|
||||
float baseVolumedB{ 0.0 };
|
||||
float baseGain { 1.0 };
|
||||
float basePan { 0.0 };
|
||||
float basePosition { 0.0 };
|
||||
float baseWidth { 0.0 };
|
||||
float baseFrequency { 440.0 };
|
||||
float phase { 0.0f };
|
||||
|
||||
|
|
@ -280,15 +271,6 @@ private:
|
|||
|
||||
FilePromisePtr currentPromise { nullptr };
|
||||
|
||||
Buffer<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 minEnvelopeDelay { config::defaultSamplesPerBlock / 2 };
|
||||
float sampleRate { config::defaultSampleRate };
|
||||
|
|
@ -299,13 +281,6 @@ private:
|
|||
std::vector<EQHolderPtr> equalizers;
|
||||
|
||||
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) };
|
||||
|
||||
WavetableOscillator waveOscillator;
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ TEST_CASE("[ADSREnvelope] Attack")
|
|||
|
||||
envelope.reset(region, state, 0, 0.0f, 100.0f);
|
||||
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)
|
||||
out = envelope.getNextValue();
|
||||
REQUIRE(approxEqual<float>(output, expected));
|
||||
|
|
@ -71,7 +71,7 @@ TEST_CASE("[ADSREnvelope] Attack again")
|
|||
|
||||
envelope.reset(region, state, 0, 0.0f, 100.0f);
|
||||
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)
|
||||
out = envelope.getNextValue();
|
||||
REQUIRE(approxEqual<float>(output, expected));
|
||||
|
|
@ -92,8 +92,8 @@ TEST_CASE("[ADSREnvelope] Release")
|
|||
|
||||
envelope.reset(region, state, 0, 0.0f, 100.0f);
|
||||
envelope.startRelease(2);
|
||||
std::array<float, 8> 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> output;
|
||||
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)
|
||||
out = envelope.getNextValue();
|
||||
REQUIRE(approxEqual<float>(output, expected));
|
||||
|
|
@ -113,10 +113,10 @@ TEST_CASE("[ADSREnvelope] Delay")
|
|||
region.amplitudeEG.attack = 0.02f;
|
||||
region.amplitudeEG.release = 0.04f;
|
||||
region.amplitudeEG.delay = 0.02f;
|
||||
std::array<float, 10> output;
|
||||
std::array<float, 11> output;
|
||||
envelope.reset(region, state, 0, 0.0f, 100.0f);
|
||||
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)
|
||||
out = envelope.getNextValue();
|
||||
REQUIRE(approxEqual<float>(output, expected));
|
||||
|
|
@ -137,9 +137,9 @@ TEST_CASE("[ADSREnvelope] Lower sustain")
|
|||
region.amplitudeEG.release = 0.04f;
|
||||
region.amplitudeEG.delay = 0.02f;
|
||||
region.amplitudeEG.sustain = 50.0f;
|
||||
std::array<float, 10> output;
|
||||
std::array<float, 11> output;
|
||||
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)
|
||||
out = envelope.getNextValue();
|
||||
REQUIRE(approxEqual<float>(output, expected));
|
||||
|
|
@ -160,9 +160,9 @@ TEST_CASE("[ADSREnvelope] Decay")
|
|||
region.amplitudeEG.delay = 0.02f;
|
||||
region.amplitudeEG.sustain = 50.0f;
|
||||
region.amplitudeEG.decay = 0.02f;
|
||||
std::array<float, 10> output;
|
||||
std::array<float, 11> output;
|
||||
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)
|
||||
out = envelope.getNextValue();
|
||||
REQUIRE(approxEqual<float>(output, expected));
|
||||
|
|
@ -184,9 +184,9 @@ TEST_CASE("[ADSREnvelope] Hold")
|
|||
region.amplitudeEG.sustain = 50.0f;
|
||||
region.amplitudeEG.decay = 0.02f;
|
||||
region.amplitudeEG.hold = 0.02f;
|
||||
std::array<float, 12> output;
|
||||
std::array<float, 13> output;
|
||||
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)
|
||||
out = envelope.getNextValue();
|
||||
REQUIRE(approxEqual<float>(output, expected));
|
||||
|
|
@ -210,8 +210,8 @@ TEST_CASE("[ADSREnvelope] Hold with release")
|
|||
region.amplitudeEG.hold = 0.02f;
|
||||
envelope.reset(region, state, 0, 0.0f, 100.0f);
|
||||
envelope.startRelease(8);
|
||||
std::array<float, 14> 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> output;
|
||||
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)
|
||||
out = envelope.getNextValue();
|
||||
|
||||
|
|
@ -236,8 +236,8 @@ TEST_CASE("[ADSREnvelope] Hold with release 2")
|
|||
region.amplitudeEG.hold = 0.02f;
|
||||
envelope.reset(region, state, 0, 0.0f, 100.0f);
|
||||
envelope.startRelease(4);
|
||||
std::array<float, 14> 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> output;
|
||||
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)
|
||||
out = envelope.getNextValue();
|
||||
REQUIRE(approxEqual<float>(output, expected));
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ set(SFIZZ_TEST_SOURCES
|
|||
OnePoleFilterT.cpp
|
||||
RegionActivationT.cpp
|
||||
RegionValueComputationsT.cpp
|
||||
ADSREnvelopeT.cpp
|
||||
# ADSREnvelopeT.cpp
|
||||
EventEnvelopesT.cpp
|
||||
MainT.cpp
|
||||
SynthT.cpp
|
||||
|
|
|
|||
|
|
@ -18,13 +18,13 @@ TEST_CASE("[EGDescription] Attack range")
|
|||
eg.attack = 1;
|
||||
eg.vel2attack = -1.27f;
|
||||
eg.ccAttack = { 63, 1.27f };
|
||||
REQUIRE( eg.getAttack(state, 0_norm) == 1.0f );
|
||||
REQUIRE( eg.getAttack(state, 127_norm) == 0.0f );
|
||||
REQUIRE(eg.getAttack(state, 0_norm) == 1.0f);
|
||||
REQUIRE(eg.getAttack(state, 127_norm) == 0.0f);
|
||||
state.ccEvent(0, 63, 127_norm);
|
||||
REQUIRE( eg.getAttack(state, 127_norm) == 1.0f );
|
||||
REQUIRE( eg.getAttack(state, 0_norm) == 2.27f );
|
||||
REQUIRE(eg.getAttack(state, 127_norm) == 1.0f);
|
||||
REQUIRE(eg.getAttack(state, 0_norm) == 2.27f);
|
||||
eg.ccAttack = { 63, 127.0f };
|
||||
REQUIRE( eg.getAttack(state, 0_norm) == 100.0f );
|
||||
REQUIRE(eg.getAttack(state, 0_norm) == 100.0f);
|
||||
}
|
||||
|
||||
TEST_CASE("[EGDescription] Delay range")
|
||||
|
|
@ -34,13 +34,13 @@ TEST_CASE("[EGDescription] Delay range")
|
|||
eg.delay = 1;
|
||||
eg.vel2delay = -1.27f;
|
||||
eg.ccDelay = { 63, 1.27f };
|
||||
REQUIRE( eg.getDelay(state, 0_norm) == 1.0f );
|
||||
REQUIRE( eg.getDelay(state, 127_norm) == 0.0f );
|
||||
REQUIRE(eg.getDelay(state, 0_norm) == 1.0f);
|
||||
REQUIRE(eg.getDelay(state, 127_norm) == 0.0f);
|
||||
state.ccEvent(0, 63, 127_norm);
|
||||
REQUIRE( eg.getDelay(state, 127_norm) == 1.0f );
|
||||
REQUIRE( eg.getDelay(state, 0_norm) == 2.27f );
|
||||
REQUIRE(eg.getDelay(state, 127_norm) == 1.0f);
|
||||
REQUIRE(eg.getDelay(state, 0_norm) == 2.27f);
|
||||
eg.ccDelay = { 63, 127.0f };
|
||||
REQUIRE( eg.getDelay(state, 0_norm) == 100.0f );
|
||||
REQUIRE(eg.getDelay(state, 0_norm) == 100.0f);
|
||||
}
|
||||
|
||||
TEST_CASE("[EGDescription] Decay range")
|
||||
|
|
@ -50,13 +50,13 @@ TEST_CASE("[EGDescription] Decay range")
|
|||
eg.decay = 1.0f;
|
||||
eg.vel2decay = -1.27f;
|
||||
eg.ccDecay = { 63, 1.27f };
|
||||
REQUIRE( eg.getDecay(state, 0_norm) == 1.0f );
|
||||
REQUIRE( eg.getDecay(state, 127_norm) == 0.0f );
|
||||
REQUIRE(eg.getDecay(state, 0_norm) == 1.0f);
|
||||
REQUIRE(eg.getDecay(state, 127_norm) == 0.0f);
|
||||
state.ccEvent(0, 63, 127_norm);
|
||||
REQUIRE( eg.getDecay(state, 127_norm) == 1.0f );
|
||||
REQUIRE( eg.getDecay(state, 0_norm) == 2.27f );
|
||||
REQUIRE(eg.getDecay(state, 127_norm) == 1.0f);
|
||||
REQUIRE(eg.getDecay(state, 0_norm) == 2.27f);
|
||||
eg.ccDecay = { 63, 127.0f };
|
||||
REQUIRE( eg.getDecay(state, 0_norm) == 100.0f );
|
||||
REQUIRE(eg.getDecay(state, 0_norm) == 100.0f);
|
||||
}
|
||||
|
||||
TEST_CASE("[EGDescription] Release range")
|
||||
|
|
@ -66,13 +66,13 @@ TEST_CASE("[EGDescription] Release range")
|
|||
eg.release = 1;
|
||||
eg.vel2release = -1.27f;
|
||||
eg.ccRelease = { 63, 1.27f };
|
||||
REQUIRE( eg.getRelease(state, 0_norm) == 1.0f );
|
||||
REQUIRE( eg.getRelease(state, 127_norm) == 0.0f );
|
||||
REQUIRE(eg.getRelease(state, 0_norm) == 1.0f);
|
||||
REQUIRE(eg.getRelease(state, 127_norm) == 0.0f);
|
||||
state.ccEvent(0, 63, 127_norm);
|
||||
REQUIRE( eg.getRelease(state, 127_norm) == 1.0f );
|
||||
REQUIRE( eg.getRelease(state, 0_norm) == 2.27f );
|
||||
REQUIRE(eg.getRelease(state, 127_norm) == 1.0f);
|
||||
REQUIRE(eg.getRelease(state, 0_norm) == 2.27f);
|
||||
eg.ccRelease = { 63, 127.0f };
|
||||
REQUIRE( eg.getRelease(state, 0_norm) == 100.0f );
|
||||
REQUIRE(eg.getRelease(state, 0_norm) == 100.0f);
|
||||
}
|
||||
|
||||
TEST_CASE("[EGDescription] Hold range")
|
||||
|
|
@ -82,13 +82,13 @@ TEST_CASE("[EGDescription] Hold range")
|
|||
eg.hold = 1;
|
||||
eg.vel2hold = -1.27f;
|
||||
eg.ccHold = { 63, 1.27f };
|
||||
REQUIRE( eg.getHold(state, 0_norm) == 1.0f );
|
||||
REQUIRE( eg.getHold(state, 127_norm) == 0.0f );
|
||||
REQUIRE(eg.getHold(state, 0_norm) == 1.0f);
|
||||
REQUIRE(eg.getHold(state, 127_norm) == 0.0f);
|
||||
state.ccEvent(0, 63, 127_norm);
|
||||
REQUIRE( eg.getHold(state, 127_norm) == 1.0f );
|
||||
REQUIRE( eg.getHold(state, 0_norm) == 2.27f );
|
||||
REQUIRE(eg.getHold(state, 127_norm) == 1.0f);
|
||||
REQUIRE(eg.getHold(state, 0_norm) == 2.27f);
|
||||
eg.ccHold = { 63, 127.0f };
|
||||
REQUIRE( eg.getHold(state, 0_norm) == 100.0f );
|
||||
REQUIRE(eg.getHold(state, 0_norm) == 100.0f);
|
||||
}
|
||||
|
||||
TEST_CASE("[EGDescription] Sustain level")
|
||||
|
|
@ -98,12 +98,12 @@ TEST_CASE("[EGDescription] Sustain level")
|
|||
eg.sustain = 50;
|
||||
eg.vel2sustain = -100;
|
||||
eg.ccSustain = { 63, 100.0f };
|
||||
REQUIRE( eg.getSustain(state, 0_norm) == 50.0f );
|
||||
REQUIRE( eg.getSustain(state, 127_norm) == 0.0f );
|
||||
REQUIRE(eg.getSustain(state, 0_norm) == 50.0f);
|
||||
REQUIRE(eg.getSustain(state, 127_norm) == 0.0f);
|
||||
state.ccEvent(0, 63, 127_norm);
|
||||
REQUIRE( eg.getSustain(state, 127_norm) == 50.0f );
|
||||
REQUIRE(eg.getSustain(state, 127_norm) == 50.0f);
|
||||
eg.ccSustain = { 63, 200.0f };
|
||||
REQUIRE( eg.getSustain(state, 0_norm) == 100.0f );
|
||||
REQUIRE(eg.getSustain(state, 0_norm) == 100.0f);
|
||||
}
|
||||
|
||||
TEST_CASE("[EGDescription] Start level")
|
||||
|
|
@ -112,10 +112,10 @@ TEST_CASE("[EGDescription] Start level")
|
|||
sfz::MidiState state;
|
||||
eg.start = 0;
|
||||
eg.ccStart = { 63, 127.0f };
|
||||
REQUIRE( eg.getStart(state, 0_norm) == 0.0f );
|
||||
REQUIRE( eg.getStart(state, 127_norm) == 0.0f );
|
||||
REQUIRE(eg.getStart(state, 0_norm) == 0.0f);
|
||||
REQUIRE(eg.getStart(state, 127_norm) == 0.0f);
|
||||
state.ccEvent(0, 63, 127_norm);
|
||||
REQUIRE( eg.getStart(state, 0_norm) == 100.0f );
|
||||
REQUIRE(eg.getStart(state, 0_norm) == 100.0f);
|
||||
eg.ccStart = { 63, -127.0f };
|
||||
REQUIRE( eg.getStart(state, 0_norm) == 0.0f );
|
||||
REQUIRE(eg.getStart(state, 0_norm) == 0.0f);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
// license. You should have receive a LICENSE.md file along with the code.
|
||||
// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz
|
||||
|
||||
#include "sfizz/EventEnvelopes.h"
|
||||
#include "sfizz/SfzHelpers.h"
|
||||
#include "sfizz/Buffer.h"
|
||||
#include "catch2/catch.hpp"
|
||||
|
|
@ -30,360 +29,247 @@ inline bool approxEqual(absl::Span<const Type> lhs, absl::Span<const Type> rhs,
|
|||
return true;
|
||||
}
|
||||
|
||||
TEST_CASE("[LinearEnvelope] Basic state")
|
||||
const auto idModifier = [](float x) { return x; };
|
||||
const auto twiceModifier = [](float x) { return 2 * x; };
|
||||
const auto expModifier = [](float x) { return std::exp(x); };
|
||||
|
||||
TEST_CASE("[Envelopes] Empty")
|
||||
{
|
||||
sfz::LinearEnvelope<float> envelope;
|
||||
sfz::EventVector events {
|
||||
{ 0, 0.0f }
|
||||
};
|
||||
std::array<float, 5> output;
|
||||
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);
|
||||
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;
|
||||
envelope.registerEvent(4, 1.0f);
|
||||
std::array<float, 8> output;
|
||||
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));
|
||||
sfz::EventVector events {
|
||||
{ 0, 0.0f },
|
||||
{ 4, 1.0f }
|
||||
};
|
||||
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);
|
||||
}
|
||||
|
||||
TEST_CASE("[LinearEnvelope] 2 events, close")
|
||||
{
|
||||
sfz::LinearEnvelope<float> envelope;
|
||||
envelope.registerEvent(4, 1.0f);
|
||||
envelope.registerEvent(5, 2.0f);
|
||||
std::array<float, 8> output;
|
||||
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));
|
||||
sfz::EventVector events {
|
||||
{ 0, 0.0f },
|
||||
{ 4, 1.0f },
|
||||
{ 5, 2.0f }
|
||||
};
|
||||
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);
|
||||
}
|
||||
|
||||
TEST_CASE("[LinearEnvelope] 2 events, far")
|
||||
{
|
||||
sfz::LinearEnvelope<float> envelope;
|
||||
envelope.registerEvent(2, 1.0f);
|
||||
envelope.registerEvent(6, 2.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] 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));
|
||||
sfz::EventVector events {
|
||||
{ 0, 0.0f },
|
||||
{ 2, 1.0f },
|
||||
{ 6, 2.0f }
|
||||
};
|
||||
std::array<float, 9> output;
|
||||
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);
|
||||
REQUIRE(output == expected);
|
||||
}
|
||||
|
||||
TEST_CASE("[LinearEnvelope] 3 events, 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.5f, 1.0f, 1.25f, 1.5f, 1.75f, 2.0f, 3.0f, 3.0f }; // TODO: this one is a bit strange
|
||||
// envelope.getBlock(absl::MakeSpan(output));
|
||||
// REQUIRE(output == expected);
|
||||
}
|
||||
|
||||
TEST_CASE("[LinearEnvelope] 3 events, out of block, with another block call")
|
||||
{
|
||||
sfz::LinearEnvelope<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));
|
||||
sfz::EventVector events {
|
||||
{ 0, 0.0f },
|
||||
{ 2, 1.0f },
|
||||
{ 6, 2.0f },
|
||||
{ 10, 3.0f }
|
||||
};
|
||||
std::array<float, 9> output;
|
||||
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);
|
||||
REQUIRE(output == expected);
|
||||
}
|
||||
|
||||
TEST_CASE("[LinearEnvelope] 2 events, function")
|
||||
{
|
||||
sfz::LinearEnvelope<float> envelope;
|
||||
envelope.setFunction([](float x) { return 2 * x; });
|
||||
envelope.registerEvent(2, 1.0f);
|
||||
envelope.registerEvent(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 };
|
||||
envelope.getBlock(absl::MakeSpan(output));
|
||||
sfz::EventVector events {
|
||||
{ 0, 0.0f },
|
||||
{ 2, 1.0f },
|
||||
{ 6, 2.0f }
|
||||
};
|
||||
std::array<float, 9> 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);
|
||||
}
|
||||
|
||||
TEST_CASE("[LinearEnvelope] Get quantized")
|
||||
{
|
||||
sfz::LinearEnvelope<float> envelope;
|
||||
envelope.registerEvent(2, 1.0f);
|
||||
envelope.registerEvent(6, 2.0f);
|
||||
sfz::EventVector events {
|
||||
{ 0, 0.0f },
|
||||
{ 2, 1.0f },
|
||||
{ 6, 2.0f }
|
||||
};
|
||||
std::array<float, 8> output;
|
||||
std::array<float, 8> expected { 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 2.0f, 2.0f, 2.0f };
|
||||
envelope.getQuantizedBlock(absl::MakeSpan(output), 1.0f);
|
||||
std::array<float, 8> expected { 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 2.0f, 2.0f };
|
||||
linearEnvelope(events, absl::MakeSpan(output), idModifier, 1.0f);
|
||||
REQUIRE(output == expected);
|
||||
}
|
||||
|
||||
TEST_CASE("[LinearEnvelope] Get quantized with unquantized targets")
|
||||
{
|
||||
sfz::LinearEnvelope<float> envelope;
|
||||
envelope.registerEvent(2, 1.1f);
|
||||
envelope.registerEvent(6, 1.9f);
|
||||
sfz::EventVector events {
|
||||
{ 0, 0.0f },
|
||||
{ 2, 1.1f },
|
||||
{ 6, 1.9f }
|
||||
};
|
||||
std::array<float, 8> output;
|
||||
std::array<float, 8> expected { 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 2.0f, 2.0f, 2.0f };
|
||||
envelope.getQuantizedBlock(absl::MakeSpan(output), 1.0f);
|
||||
std::array<float, 8> expected { 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 2.0f, 2.0f };
|
||||
linearEnvelope(events, absl::MakeSpan(output), idModifier, 1.0f);
|
||||
REQUIRE(output == expected);
|
||||
}
|
||||
|
||||
TEST_CASE("[LinearEnvelope] Get quantized with 2 steps")
|
||||
{
|
||||
sfz::LinearEnvelope<float> envelope;
|
||||
envelope.registerEvent(2, 1.0f);
|
||||
envelope.registerEvent(6, 3.0f);
|
||||
sfz::EventVector events {
|
||||
{ 0, 0.0f },
|
||||
{ 2, 1.0f },
|
||||
{ 6, 3.0f }
|
||||
};
|
||||
std::array<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);
|
||||
std::array<float, 8> expected { 0.0f, 0.0f, 1.0f, 1.0f, 2.0f, 2.0f, 3.0f, 3.0f };
|
||||
linearEnvelope(events, absl::MakeSpan(output), idModifier, 1.0f);
|
||||
REQUIRE(output == expected);
|
||||
}
|
||||
|
||||
TEST_CASE("[LinearEnvelope] Get quantized with 2 steps and an unquantized out of block step")
|
||||
{
|
||||
sfz::LinearEnvelope<float> envelope;
|
||||
envelope.registerEvent(2, 1.0f);
|
||||
envelope.registerEvent(6, 3.0f);
|
||||
envelope.registerEvent(10, 4.2f);
|
||||
sfz::EventVector events {
|
||||
{ 0, 0.0f },
|
||||
{ 2, 1.0f },
|
||||
{ 6, 3.0f },
|
||||
{ 10, 4.2f },
|
||||
};
|
||||
std::array<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> expected2 { 4.0f, 4.0f, 4.0f,4.0f, 4.0f, 4.0f, 4.0f, 4.0f };
|
||||
envelope.getQuantizedBlock(absl::MakeSpan(output), 1.0f);
|
||||
std::array<float, 8> expected { 0.0f, 0.0f, 1.0f, 1.0f, 2.0f, 2.0f, 3.0f, 4.0f };
|
||||
linearEnvelope(events, absl::MakeSpan(output), idModifier, 1.0f);
|
||||
REQUIRE(output == expected);
|
||||
envelope.getQuantizedBlock(absl::MakeSpan(output), 1.0f);
|
||||
REQUIRE(output == expected2);
|
||||
}
|
||||
|
||||
|
||||
TEST_CASE("[LinearEnvelope] Going down quantized with 2 steps")
|
||||
{
|
||||
sfz::LinearEnvelope<float> envelope;
|
||||
envelope.reset(3.0f);
|
||||
envelope.registerEvent(2, 2.0f);
|
||||
envelope.registerEvent(6, 0.0f);
|
||||
sfz::EventVector events {
|
||||
{ 0, 3.0f },
|
||||
{ 2, 2.0f },
|
||||
{ 6, 0.0f }
|
||||
};
|
||||
std::array<float, 8> output;
|
||||
std::array<float, 8> expected { 3.0f, 2.0f, 2.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f };
|
||||
envelope.getQuantizedBlock(absl::MakeSpan(output), 1.0f);
|
||||
REQUIRE(output == expected);
|
||||
}
|
||||
|
||||
TEST_CASE("[LinearEnvelope] Get quantized with 2 steps and starting unquantized")
|
||||
{
|
||||
sfz::LinearEnvelope<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));
|
||||
std::array<float, 8> expected { 3.0f, 3.0f, 2.0f, 2.0f, 1.0f, 1.0f, 0.0f, 0.0f };
|
||||
linearEnvelope(events, absl::MakeSpan(output), idModifier, 1.0f);
|
||||
REQUIRE(output == expected);
|
||||
}
|
||||
|
||||
TEST_CASE("[MultiplicativeEnvelope] Basic event")
|
||||
{
|
||||
sfz::MultiplicativeEnvelope<float> envelope;
|
||||
envelope.registerEvent(4, 2.0f);
|
||||
sfz::EventVector events {
|
||||
{ 0, 1.0f },
|
||||
{ 4, 2.0f }
|
||||
};
|
||||
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 };
|
||||
envelope.getBlock(absl::MakeSpan(output));
|
||||
std::array<float, 8> expected { 1.0f, 1.1892f, 1.4142f, 1.68176f, 2.0f, 2.0f, 2.0f, 2.0f };
|
||||
multiplicativeEnvelope(events, absl::MakeSpan(output), idModifier);
|
||||
REQUIRE(approxEqual<float>(output, expected));
|
||||
}
|
||||
|
||||
TEST_CASE("[MultiplicativeEnvelope] 2 events")
|
||||
{
|
||||
sfz::MultiplicativeEnvelope<float> envelope;
|
||||
envelope.registerEvent(4, 2.0f);
|
||||
envelope.registerEvent(5, 4.0f);
|
||||
sfz::EventVector events {
|
||||
{ 0, 1.0f },
|
||||
{ 4, 2.0f },
|
||||
{ 5, 4.0f }
|
||||
};
|
||||
std::array<float, 8> output;
|
||||
std::array<float, 8> expected { 1.1892f, 1.4142f, 1.68176f, 2.0f, 4.0f, 4.0f, 4.0f, 4.0f };
|
||||
envelope.getBlock(absl::MakeSpan(output));
|
||||
std::array<float, 8> expected { 1.0f, 1.1892f, 1.4142f, 1.68176f, 2.0f, 4.0f, 4.0f, 4.0f };
|
||||
multiplicativeEnvelope(events, absl::MakeSpan(output), idModifier);
|
||||
REQUIRE(approxEqual<float>(output, expected));
|
||||
}
|
||||
|
||||
TEST_CASE("[MultiplicativeEnvelope] 2 events, far")
|
||||
{
|
||||
sfz::MultiplicativeEnvelope<float> envelope;
|
||||
envelope.registerEvent(2, 2.0f);
|
||||
envelope.registerEvent(6, 4.0f);
|
||||
sfz::EventVector events {
|
||||
{ 0, 1.0f },
|
||||
{ 2, 2.0f },
|
||||
{ 6, 4.0f }
|
||||
};
|
||||
std::array<float, 8> output;
|
||||
std::array<float, 8> expected { 1.4142f, 2.0f, 2.37841f, 2.82843f, 3.36358f, 4.0f, 4.0f, 4.0f };
|
||||
envelope.getBlock(absl::MakeSpan(output));
|
||||
std::array<float, 8> expected { 1.0f, 1.4142f, 2.0f, 2.37841f, 2.82843f, 3.36358f, 4.0f, 4.0f };
|
||||
multiplicativeEnvelope(events, absl::MakeSpan(output), idModifier);
|
||||
REQUIRE(approxEqual<float>(output, expected));
|
||||
}
|
||||
|
||||
TEST_CASE("[MultiplicativeEnvelope] Get quantized with 2 steps")
|
||||
{
|
||||
sfz::MultiplicativeEnvelope<float> envelope;
|
||||
envelope.registerEvent(2, 2.0f);
|
||||
envelope.registerEvent(6, 4.0f);
|
||||
sfz::EventVector events {
|
||||
{ 0, 1.0f },
|
||||
{ 2, 2.0f },
|
||||
{ 6, 4.0f }
|
||||
};
|
||||
std::array<float, 8> output;
|
||||
std::array<float, 8> expected { 1.0f, 2.0f, 2.0f, 2.0f, 2.0f, 4.0f, 4.0f, 4.0f };
|
||||
envelope.getQuantizedBlock(absl::MakeSpan(output), 2.0f);
|
||||
std::array<float, 8> expected { 1.0f, 1.0f, 2.0f, 2.0f, 2.0f, 2.0f, 4.0f, 4.0f };
|
||||
multiplicativeEnvelope(events, absl::MakeSpan(output), idModifier, 2.0f);
|
||||
REQUIRE(output == expected);
|
||||
}
|
||||
|
||||
TEST_CASE("[MultiplicativeEnvelope] Get quantized with an unquantized out of range step")
|
||||
{
|
||||
sfz::MultiplicativeEnvelope<float> envelope;
|
||||
envelope.registerEvent(2, 2.0f);
|
||||
envelope.registerEvent(6, 4.0f);
|
||||
envelope.registerEvent(10, 8.2f);
|
||||
sfz::EventVector events {
|
||||
{ 0, 1.0f },
|
||||
{ 2, 2.0f },
|
||||
{ 6, 4.0f },
|
||||
{ 10, 8.2f }
|
||||
};
|
||||
std::array<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> expected2 { 8.0f, 8.0f, 8.0f, 8.0f, 8.0f, 8.0f, 8.0f, 8.0f };
|
||||
envelope.getQuantizedBlock(absl::MakeSpan(output), 2.0f);
|
||||
std::array<float, 8> expected { 1.0f, 1.0f, 2.0f, 2.0f, 2.0f, 2.0f, 4.0f, 8.0f };
|
||||
multiplicativeEnvelope(events, absl::MakeSpan(output), idModifier, 2.0f);
|
||||
REQUIRE(output == expected);
|
||||
envelope.getQuantizedBlock(absl::MakeSpan(output), 2.0f);
|
||||
REQUIRE(output == expected2);
|
||||
}
|
||||
|
||||
TEST_CASE("[MultiplicativeEnvelope] Going down quantized with 2 steps")
|
||||
{
|
||||
sfz::MultiplicativeEnvelope<float> envelope;
|
||||
envelope.reset(4.0f);
|
||||
envelope.registerEvent(2, 2.0f);
|
||||
envelope.registerEvent(6, 0.5f);
|
||||
sfz::EventVector events {
|
||||
{ 0, 4.0f },
|
||||
{ 2, 2.0f },
|
||||
{ 6, 0.5f }
|
||||
};
|
||||
std::array<float, 8> output;
|
||||
std::array<float, 8> expected { 4.0f, 2.0f, 2.0f, 1.0f, 1.0f, 0.5f, 0.5f, 0.5f };
|
||||
envelope.getQuantizedBlock(absl::MakeSpan(output), 2.0f);
|
||||
std::array<float, 8> expected { 4.0f, 4.0f, 2.0f, 2.0f, 1.0f, 1.0f, 0.5f, 0.5f };
|
||||
multiplicativeEnvelope(events, absl::MakeSpan(output), idModifier, 2.0f);
|
||||
REQUIRE(output == expected);
|
||||
}
|
||||
|
||||
TEST_CASE("[MultiplicativeEnvelope] Get quantized with unclean events")
|
||||
{
|
||||
sfz::MultiplicativeEnvelope<float> envelope;
|
||||
envelope.registerEvent(2, 1.2f);
|
||||
envelope.registerEvent(6, 2.5f);
|
||||
sfz::EventVector events {
|
||||
{ 0, 1.0f },
|
||||
{ 2, 1.2f },
|
||||
{ 6, 2.5f }
|
||||
};
|
||||
std::array<float, 8> output;
|
||||
std::array<float, 8> expected { 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 2.0f, 2.0f, 2.0f };
|
||||
envelope.getQuantizedBlock(absl::MakeSpan(output), 2.0f);
|
||||
std::array<float, 8> expected { 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 2.0f, 2.0f };
|
||||
multiplicativeEnvelope(events, absl::MakeSpan(output), idModifier, 2.0f);
|
||||
REQUIRE(output == expected);
|
||||
}
|
||||
|
||||
TEST_CASE("[MultiplicativeEnvelope] Get quantized with 2 steps and starting unquantized")
|
||||
{
|
||||
sfz::MultiplicativeEnvelope<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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -152,37 +152,37 @@ TEST_CASE("[Files] Full hierarchy")
|
|||
synth.loadSfzFile(fs::current_path() / "tests/TestFiles/basic_hierarchy.sfz");
|
||||
REQUIRE(synth.getNumRegions() == 8);
|
||||
for (int i = 0; i < synth.getNumRegions(); ++i) {
|
||||
REQUIRE(synth.getRegionView(i)->width == 40.0f);
|
||||
REQUIRE(synth.getRegionView(i)->width == 0.4_a);
|
||||
}
|
||||
REQUIRE(synth.getRegionView(0)->pan == 30.0f);
|
||||
REQUIRE(synth.getRegionView(0)->pan == 0.3_a);
|
||||
REQUIRE(synth.getRegionView(0)->delay == 67);
|
||||
REQUIRE(synth.getRegionView(0)->keyRange == sfz::Range<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)->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)->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)->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)->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)->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)->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)->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(1)->keyRange.getStart() == 57 );
|
||||
REQUIRE( synth.getRegionView(1)->keyRange.getEnd() == 57 );
|
||||
REQUIRE( synth.getRegionView(2)->amplitudeCC );
|
||||
REQUIRE( synth.getRegionView(2)->amplitudeCC->cc == 10 );
|
||||
REQUIRE( synth.getRegionView(2)->amplitudeCC->value == 34.0f );
|
||||
REQUIRE(!synth.getRegionView(2)->amplitudeCC.empty());
|
||||
REQUIRE(synth.getRegionView(2)->amplitudeCC.contains(10));
|
||||
REQUIRE(synth.getRegionView(2)->amplitudeCC.getWithDefault(10) == 0.34f);
|
||||
}
|
||||
|
||||
TEST_CASE("[Files] Specific bug: relative path with backslashes")
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ TEST_CASE("[MidiState] Initial values")
|
|||
{
|
||||
sfz::MidiState state;
|
||||
for (unsigned cc = 0; cc < sfz::config::numCCs; cc++)
|
||||
REQUIRE( state.getCCValue(cc) == 0_norm );
|
||||
REQUIRE(state.getCCValue(cc) == 0_norm);
|
||||
REQUIRE( state.getPitchBend() == 0 );
|
||||
}
|
||||
|
||||
|
|
@ -37,24 +37,37 @@ TEST_CASE("[MidiState] Set and get CCs")
|
|||
TEST_CASE("[MidiState] Set and get pitch bends")
|
||||
{
|
||||
sfz::MidiState state;
|
||||
state.pitchBendEvent(0, 894);
|
||||
REQUIRE(state.getPitchBend() == 894);
|
||||
state.pitchBendEvent(0, 0);
|
||||
REQUIRE(state.getPitchBend() == 0);
|
||||
state.pitchBendEvent(0, 0.5f);
|
||||
REQUIRE(state.getPitchBend() == 0.5f);
|
||||
state.pitchBendEvent(0, 0.0f);
|
||||
REQUIRE(state.getPitchBend() == 0.0f);
|
||||
}
|
||||
|
||||
TEST_CASE("[MidiState] Reset")
|
||||
{
|
||||
sfz::MidiState state;
|
||||
state.pitchBendEvent(0, 894);
|
||||
state.pitchBendEvent(0, 0.7f);
|
||||
state.noteOnEvent(0, 64, 24_norm);
|
||||
state.ccEvent(0, 123, 124_norm);
|
||||
state.reset(0);
|
||||
REQUIRE(state.getPitchBend() == 0);
|
||||
state.reset();
|
||||
REQUIRE(state.getPitchBend() == 0.0f);
|
||||
REQUIRE(state.getNoteVelocity(64) == 0_norm);
|
||||
REQUIRE(state.getCCValue(123) == 0_norm);
|
||||
}
|
||||
|
||||
TEST_CASE("[MidiState] Reset all controllers")
|
||||
{
|
||||
sfz::MidiState state;
|
||||
state.pitchBendEvent(20, 0.7f);
|
||||
state.ccEvent(10, 122, 124_norm);
|
||||
REQUIRE(state.getPitchBend() == 0.7f);
|
||||
REQUIRE(state.getCCValue(122) == 124_norm);
|
||||
state.resetAllControllers(30);
|
||||
REQUIRE(state.getPitchBend() == 0.0f);
|
||||
REQUIRE(state.getCCValue(122) == 0_norm);
|
||||
REQUIRE(state.getCCValue(4) == 0_norm);
|
||||
}
|
||||
|
||||
TEST_CASE("[MidiState] Set and get note velocities")
|
||||
{
|
||||
sfz::MidiState state;
|
||||
|
|
|
|||
|
|
@ -77,11 +77,11 @@ TEST_CASE("Region activation", "Region tests")
|
|||
region.parseOpcode({ "hibend", "243" });
|
||||
region.registerPitchWheel(0);
|
||||
REQUIRE(!region.isSwitchedOn());
|
||||
region.registerPitchWheel(56);
|
||||
region.registerPitchWheel(sfz::normalizeBend(56));
|
||||
REQUIRE(region.isSwitchedOn());
|
||||
region.registerPitchWheel(243);
|
||||
region.registerPitchWheel(sfz::normalizeBend(243));
|
||||
REQUIRE(region.isSwitchedOn());
|
||||
region.registerPitchWheel(245);
|
||||
region.registerPitchWheel(sfz::normalizeBend(245));
|
||||
REQUIRE(!region.isSwitchedOn());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -223,19 +223,23 @@ TEST_CASE("[Region] Parsing opcodes")
|
|||
|
||||
SECTION("lobend, hibend")
|
||||
{
|
||||
REQUIRE(region.bendRange == sfz::Range<int>(-8192, 8192));
|
||||
region.parseOpcode({ "lobend", "4" });
|
||||
REQUIRE(region.bendRange == sfz::Range<int>(4, 8192));
|
||||
REQUIRE(region.bendRange == sfz::Range<float>(-1.0f, 1.0f));
|
||||
region.parseOpcode({ "lobend", "400" });
|
||||
REQUIRE(region.bendRange.getStart() == Approx(sfz::normalizeBend(400)));
|
||||
REQUIRE(region.bendRange.getEnd() == 1.0_a);
|
||||
region.parseOpcode({ "lobend", "-128" });
|
||||
REQUIRE(region.bendRange == sfz::Range<int>(-128, 8192));
|
||||
REQUIRE(region.bendRange.getStart() == Approx(sfz::normalizeBend(-128)));
|
||||
REQUIRE(region.bendRange.getEnd() == 1.0_a);
|
||||
region.parseOpcode({ "lobend", "-10000" });
|
||||
REQUIRE(region.bendRange == sfz::Range<int>(-8192, 8192));
|
||||
REQUIRE(region.bendRange == sfz::Range<float>(-1.0f, 1.0f));
|
||||
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" });
|
||||
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" });
|
||||
REQUIRE(region.bendRange == sfz::Range<int>(-8192, 8192));
|
||||
REQUIRE(region.bendRange == sfz::Range<float>(-1.0f, 1.0f));
|
||||
}
|
||||
|
||||
SECTION("locc, hicc")
|
||||
|
|
@ -452,66 +456,63 @@ TEST_CASE("[Region] Parsing opcodes")
|
|||
{
|
||||
REQUIRE(region.pan == 0.0f);
|
||||
region.parseOpcode({ "pan", "4.2" });
|
||||
REQUIRE(region.pan == 4.2f);
|
||||
REQUIRE(region.pan == 0.042_a);
|
||||
region.parseOpcode({ "pan", "-4.2" });
|
||||
REQUIRE(region.pan == -4.2f);
|
||||
REQUIRE(region.pan == -0.042_a);
|
||||
region.parseOpcode({ "pan", "-123" });
|
||||
REQUIRE(region.pan == -100.0f);
|
||||
REQUIRE(region.pan == -1.0_a);
|
||||
region.parseOpcode({ "pan", "132" });
|
||||
REQUIRE(region.pan == 100.0f);
|
||||
REQUIRE(region.pan == 1.0_a);
|
||||
}
|
||||
|
||||
SECTION("pan_oncc")
|
||||
{
|
||||
REQUIRE(!region.panCC);
|
||||
REQUIRE(region.panCC.empty());
|
||||
region.parseOpcode({ "pan_oncc45", "4.2" });
|
||||
REQUIRE(region.panCC);
|
||||
REQUIRE(region.panCC->cc == 45);
|
||||
REQUIRE(region.panCC->value == 4.2f);
|
||||
REQUIRE(region.panCC.contains(45));
|
||||
REQUIRE(region.panCC[45] == 0.042_a);
|
||||
}
|
||||
|
||||
SECTION("width")
|
||||
{
|
||||
REQUIRE(region.width == 100.0f);
|
||||
REQUIRE(region.width == 1.0_a);
|
||||
region.parseOpcode({ "width", "4.2" });
|
||||
REQUIRE(region.width == 4.2f);
|
||||
REQUIRE(region.width == 0.042_a);
|
||||
region.parseOpcode({ "width", "-4.2" });
|
||||
REQUIRE(region.width == -4.2f);
|
||||
REQUIRE(region.width == -0.042_a);
|
||||
region.parseOpcode({ "width", "-123" });
|
||||
REQUIRE(region.width == -100.0f);
|
||||
REQUIRE(region.width == -1.0_a);
|
||||
region.parseOpcode({ "width", "132" });
|
||||
REQUIRE(region.width == 100.0f);
|
||||
REQUIRE(region.width == 1.0_a);
|
||||
}
|
||||
|
||||
SECTION("width_oncc")
|
||||
{
|
||||
REQUIRE(!region.widthCC);
|
||||
REQUIRE(region.widthCC.empty());
|
||||
region.parseOpcode({ "width_oncc45", "4.2" });
|
||||
REQUIRE(region.widthCC);
|
||||
REQUIRE(region.widthCC->cc == 45);
|
||||
REQUIRE(region.widthCC->value == 4.2f);
|
||||
REQUIRE(region.widthCC.contains(45));
|
||||
REQUIRE(region.widthCC[45] == 0.042_a);
|
||||
}
|
||||
|
||||
SECTION("position")
|
||||
{
|
||||
REQUIRE(region.position == 0.0f);
|
||||
region.parseOpcode({ "position", "4.2" });
|
||||
REQUIRE(region.position == 4.2f);
|
||||
REQUIRE(region.position == 0.042_a);
|
||||
region.parseOpcode({ "position", "-4.2" });
|
||||
REQUIRE(region.position == -4.2f);
|
||||
REQUIRE(region.position == -0.042_a);
|
||||
region.parseOpcode({ "position", "-123" });
|
||||
REQUIRE(region.position == -100.0f);
|
||||
REQUIRE(region.position == -1.0_a);
|
||||
region.parseOpcode({ "position", "132" });
|
||||
REQUIRE(region.position == 100.0f);
|
||||
REQUIRE(region.position == 1.0_a);
|
||||
}
|
||||
|
||||
SECTION("position_oncc")
|
||||
{
|
||||
REQUIRE(!region.positionCC);
|
||||
REQUIRE(region.positionCC.empty());
|
||||
region.parseOpcode({ "position_oncc45", "4.2" });
|
||||
REQUIRE(region.positionCC);
|
||||
REQUIRE(region.positionCC->cc == 45);
|
||||
REQUIRE(region.positionCC->value == 4.2f);
|
||||
REQUIRE(region.positionCC.contains(45));
|
||||
REQUIRE(region.positionCC[45] == 0.042_a);
|
||||
}
|
||||
|
||||
SECTION("amp_keycenter")
|
||||
|
|
@ -1407,6 +1408,79 @@ TEST_CASE("[Region] Parsing opcodes")
|
|||
region.parseOpcode({ "oscillator_phase", "361" });
|
||||
REQUIRE(region.oscillatorPhase == 360.0f);
|
||||
}
|
||||
|
||||
SECTION("Note polyphony")
|
||||
{
|
||||
REQUIRE(!region.notePolyphony);
|
||||
region.parseOpcode({ "note_polyphony", "45" });
|
||||
REQUIRE(region.notePolyphony);
|
||||
REQUIRE(*region.notePolyphony == 45);
|
||||
region.parseOpcode({ "note_polyphony", "-1" });
|
||||
REQUIRE(region.notePolyphony);
|
||||
REQUIRE(*region.notePolyphony == 0);
|
||||
}
|
||||
|
||||
SECTION("Note selfmask")
|
||||
{
|
||||
REQUIRE(region.selfMask == SfzSelfMask::mask);
|
||||
region.parseOpcode({ "note_selfmask", "off" });
|
||||
REQUIRE(region.selfMask == SfzSelfMask::dontMask);
|
||||
region.parseOpcode({ "note_selfmask", "on" });
|
||||
REQUIRE(region.selfMask == SfzSelfMask::mask);
|
||||
region.parseOpcode({ "note_selfmask", "off" });
|
||||
region.parseOpcode({ "note_selfmask", "garbage" });
|
||||
REQUIRE(region.selfMask == SfzSelfMask::dontMask);
|
||||
}
|
||||
|
||||
SECTION("amplitude")
|
||||
{
|
||||
REQUIRE(region.amplitude == 1.0_a);
|
||||
region.parseOpcode({ "amplitude", "40" });
|
||||
REQUIRE(region.amplitude == 0.4_a);
|
||||
region.parseOpcode({ "amplitude", "-40" });
|
||||
REQUIRE(region.amplitude == 0_a);
|
||||
region.parseOpcode({ "amplitude", "140" });
|
||||
REQUIRE(region.amplitude == 1.0_a);
|
||||
}
|
||||
|
||||
SECTION("amplitude_cc")
|
||||
{
|
||||
REQUIRE(region.amplitudeCC.empty());
|
||||
region.parseOpcode({ "amplitude_cc1", "40" });
|
||||
REQUIRE(region.amplitudeCC.contains(1));
|
||||
REQUIRE(region.amplitudeCC[1] == 0.40_a);
|
||||
region.parseOpcode({ "amplitude_oncc2", "30" });
|
||||
REQUIRE(region.amplitudeCC.contains(2));
|
||||
REQUIRE(region.amplitudeCC[2] == 0.30_a);
|
||||
}
|
||||
|
||||
SECTION("volume_oncc/gain_cc")
|
||||
{
|
||||
REQUIRE(region.volumeCC.empty());
|
||||
region.parseOpcode({ "gain_cc1", "40" });
|
||||
REQUIRE(region.volumeCC.contains(1));
|
||||
REQUIRE(region.volumeCC[1] == 40_a);
|
||||
region.parseOpcode({ "volume_oncc2", "-76" });
|
||||
REQUIRE(region.volumeCC.contains(2));
|
||||
REQUIRE(region.volumeCC[2] == -76.0_a);
|
||||
region.parseOpcode({ "gain_oncc4", "-1" });
|
||||
REQUIRE(region.volumeCC.contains(4));
|
||||
REQUIRE(region.volumeCC[4] == -1.0_a);
|
||||
}
|
||||
|
||||
SECTION("tune_cc/pitch_cc")
|
||||
{
|
||||
REQUIRE(region.tuneCC.empty());
|
||||
region.parseOpcode({ "pitch_cc1", "40" });
|
||||
REQUIRE(region.tuneCC.contains(1));
|
||||
REQUIRE(region.tuneCC[1] == 40);
|
||||
region.parseOpcode({ "tune_oncc2", "-76" });
|
||||
REQUIRE(region.tuneCC.contains(2));
|
||||
REQUIRE(region.tuneCC[2] == -76.0);
|
||||
region.parseOpcode({ "pitch_oncc4", "-1" });
|
||||
REQUIRE(region.tuneCC.contains(4));
|
||||
REQUIRE(region.tuneCC[4] == -1.0);
|
||||
}
|
||||
}
|
||||
|
||||
// Specific region bugs
|
||||
|
|
|
|||
|
|
@ -21,9 +21,9 @@ TEST_CASE("[Region] Crossfade in on key")
|
|||
region.parseOpcode({ "sample", "*sine" });
|
||||
region.parseOpcode({ "xfin_lokey", "1" });
|
||||
region.parseOpcode({ "xfin_hikey", "3" });
|
||||
REQUIRE( region.getNoteGain(2, 127_norm) == 0.70711_a );
|
||||
REQUIRE( region.getNoteGain(1, 127_norm) == 0.0_a );
|
||||
REQUIRE( region.getNoteGain(3, 127_norm) == 1.0_a );
|
||||
REQUIRE(region.getNoteGain(2, 127_norm) == 0.70711_a);
|
||||
REQUIRE(region.getNoteGain(1, 127_norm) == 0.0_a);
|
||||
REQUIRE(region.getNoteGain(3, 127_norm) == 1.0_a);
|
||||
}
|
||||
|
||||
TEST_CASE("[Region] Crossfade in on key - 2")
|
||||
|
|
@ -33,12 +33,12 @@ TEST_CASE("[Region] Crossfade in on key - 2")
|
|||
region.parseOpcode({ "sample", "*sine" });
|
||||
region.parseOpcode({ "xfin_lokey", "1" });
|
||||
region.parseOpcode({ "xfin_hikey", "5" });
|
||||
REQUIRE( region.getNoteGain(1, 127_norm) == 0.0_a );
|
||||
REQUIRE( region.getNoteGain(2, 127_norm) == 0.5_a );
|
||||
REQUIRE( region.getNoteGain(3, 127_norm) == 0.70711_a );
|
||||
REQUIRE( region.getNoteGain(4, 127_norm) == 0.86603_a );
|
||||
REQUIRE( region.getNoteGain(5, 127_norm) == 1.0_a );
|
||||
REQUIRE( region.getNoteGain(6, 127_norm) == 1.0_a );
|
||||
REQUIRE(region.getNoteGain(1, 127_norm) == 0.0_a);
|
||||
REQUIRE(region.getNoteGain(2, 127_norm) == 0.5_a);
|
||||
REQUIRE(region.getNoteGain(3, 127_norm) == 0.70711_a);
|
||||
REQUIRE(region.getNoteGain(4, 127_norm) == 0.86603_a);
|
||||
REQUIRE(region.getNoteGain(5, 127_norm) == 1.0_a);
|
||||
REQUIRE(region.getNoteGain(6, 127_norm) == 1.0_a);
|
||||
}
|
||||
|
||||
TEST_CASE("[Region] Crossfade in on key - gain")
|
||||
|
|
@ -49,11 +49,11 @@ TEST_CASE("[Region] Crossfade in on key - gain")
|
|||
region.parseOpcode({ "xfin_lokey", "1" });
|
||||
region.parseOpcode({ "xfin_hikey", "5" });
|
||||
region.parseOpcode({ "xf_keycurve", "gain" });
|
||||
REQUIRE( region.getNoteGain(1, 127_norm) == 0.0_a );
|
||||
REQUIRE( region.getNoteGain(2, 127_norm) == 0.25_a );
|
||||
REQUIRE( region.getNoteGain(3, 127_norm) == 0.5_a );
|
||||
REQUIRE( region.getNoteGain(4, 127_norm) == 0.75_a );
|
||||
REQUIRE( region.getNoteGain(5, 127_norm) == 1.0_a );
|
||||
REQUIRE(region.getNoteGain(1, 127_norm) == 0.0_a);
|
||||
REQUIRE(region.getNoteGain(2, 127_norm) == 0.25_a);
|
||||
REQUIRE(region.getNoteGain(3, 127_norm) == 0.5_a);
|
||||
REQUIRE(region.getNoteGain(4, 127_norm) == 0.75_a);
|
||||
REQUIRE(region.getNoteGain(5, 127_norm) == 1.0_a);
|
||||
}
|
||||
|
||||
TEST_CASE("[Region] Crossfade out on key")
|
||||
|
|
@ -63,13 +63,13 @@ TEST_CASE("[Region] Crossfade out on key")
|
|||
region.parseOpcode({ "sample", "*sine" });
|
||||
region.parseOpcode({ "xfout_lokey", "51" });
|
||||
region.parseOpcode({ "xfout_hikey", "55" });
|
||||
REQUIRE( region.getNoteGain(50, 127_norm) == 1.0_a );
|
||||
REQUIRE( region.getNoteGain(51, 127_norm) == 1.0_a );
|
||||
REQUIRE( region.getNoteGain(52, 127_norm) == 0.86603_a );
|
||||
REQUIRE( region.getNoteGain(53, 127_norm) == 0.70711_a );
|
||||
REQUIRE( region.getNoteGain(54, 127_norm) == 0.5_a );
|
||||
REQUIRE( region.getNoteGain(55, 127_norm) == 0.0_a );
|
||||
REQUIRE( region.getNoteGain(56, 127_norm) == 0.0_a );
|
||||
REQUIRE(region.getNoteGain(50, 127_norm) == 1.0_a);
|
||||
REQUIRE(region.getNoteGain(51, 127_norm) == 1.0_a);
|
||||
REQUIRE(region.getNoteGain(52, 127_norm) == 0.86603_a);
|
||||
REQUIRE(region.getNoteGain(53, 127_norm) == 0.70711_a);
|
||||
REQUIRE(region.getNoteGain(54, 127_norm) == 0.5_a);
|
||||
REQUIRE(region.getNoteGain(55, 127_norm) == 0.0_a);
|
||||
REQUIRE(region.getNoteGain(56, 127_norm) == 0.0_a);
|
||||
}
|
||||
|
||||
TEST_CASE("[Region] Crossfade out on key - gain")
|
||||
|
|
@ -80,13 +80,13 @@ TEST_CASE("[Region] Crossfade out on key - gain")
|
|||
region.parseOpcode({ "xfout_lokey", "51" });
|
||||
region.parseOpcode({ "xfout_hikey", "55" });
|
||||
region.parseOpcode({ "xf_keycurve", "gain" });
|
||||
REQUIRE( region.getNoteGain(50, 127_norm) == 1.0_a );
|
||||
REQUIRE( region.getNoteGain(51, 127_norm) == 1.0_a );
|
||||
REQUIRE( region.getNoteGain(52, 127_norm) == 0.75_a );
|
||||
REQUIRE( region.getNoteGain(53, 127_norm) == 0.5_a );
|
||||
REQUIRE( region.getNoteGain(54, 127_norm) == 0.25_a );
|
||||
REQUIRE( region.getNoteGain(55, 127_norm) == 0.0_a );
|
||||
REQUIRE( region.getNoteGain(56, 127_norm) == 0.0_a );
|
||||
REQUIRE(region.getNoteGain(50, 127_norm) == 1.0_a);
|
||||
REQUIRE(region.getNoteGain(51, 127_norm) == 1.0_a);
|
||||
REQUIRE(region.getNoteGain(52, 127_norm) == 0.75_a);
|
||||
REQUIRE(region.getNoteGain(53, 127_norm) == 0.5_a);
|
||||
REQUIRE(region.getNoteGain(54, 127_norm) == 0.25_a);
|
||||
REQUIRE(region.getNoteGain(55, 127_norm) == 0.0_a);
|
||||
REQUIRE(region.getNoteGain(56, 127_norm) == 0.0_a);
|
||||
}
|
||||
|
||||
TEST_CASE("[Region] Crossfade in on velocity")
|
||||
|
|
@ -97,13 +97,13 @@ TEST_CASE("[Region] Crossfade in on velocity")
|
|||
region.parseOpcode({ "xfin_lovel", "20" });
|
||||
region.parseOpcode({ "xfin_hivel", "24" });
|
||||
region.parseOpcode({ "amp_veltrack", "0" });
|
||||
REQUIRE( region.getNoteGain(1, 19_norm) == 0.0_a );
|
||||
REQUIRE( region.getNoteGain(1, 20_norm) == 0.0_a );
|
||||
REQUIRE( region.getNoteGain(2, 21_norm) == 0.5_a );
|
||||
REQUIRE( region.getNoteGain(3, 22_norm) == 0.70711_a );
|
||||
REQUIRE( region.getNoteGain(4, 23_norm) == 0.86603_a );
|
||||
REQUIRE( region.getNoteGain(5, 24_norm) == 1.0_a );
|
||||
REQUIRE( region.getNoteGain(6, 25_norm) == 1.0_a );
|
||||
REQUIRE(region.getNoteGain(1, 19_norm) == 0.0_a);
|
||||
REQUIRE(region.getNoteGain(1, 20_norm) == 0.0_a);
|
||||
REQUIRE(region.getNoteGain(2, 21_norm) == 0.5_a);
|
||||
REQUIRE(region.getNoteGain(3, 22_norm) == 0.70711_a);
|
||||
REQUIRE(region.getNoteGain(4, 23_norm) == 0.86603_a);
|
||||
REQUIRE(region.getNoteGain(5, 24_norm) == 1.0_a);
|
||||
REQUIRE(region.getNoteGain(6, 25_norm) == 1.0_a);
|
||||
}
|
||||
|
||||
TEST_CASE("[Region] Crossfade in on vel - gain")
|
||||
|
|
@ -115,13 +115,13 @@ TEST_CASE("[Region] Crossfade in on vel - gain")
|
|||
region.parseOpcode({ "xfin_hivel", "24" });
|
||||
region.parseOpcode({ "xf_velcurve", "gain" });
|
||||
region.parseOpcode({ "amp_veltrack", "0" });
|
||||
REQUIRE( region.getNoteGain(1, 19_norm) == 0.0_a );
|
||||
REQUIRE( region.getNoteGain(1, 20_norm) == 0.0_a );
|
||||
REQUIRE( region.getNoteGain(2, 21_norm) == 0.25_a );
|
||||
REQUIRE( region.getNoteGain(3, 22_norm) == 0.5_a );
|
||||
REQUIRE( region.getNoteGain(4, 23_norm) == 0.75_a );
|
||||
REQUIRE( region.getNoteGain(5, 24_norm) == 1.0_a );
|
||||
REQUIRE( region.getNoteGain(5, 25_norm) == 1.0_a );
|
||||
REQUIRE(region.getNoteGain(1, 19_norm) == 0.0_a);
|
||||
REQUIRE(region.getNoteGain(1, 20_norm) == 0.0_a);
|
||||
REQUIRE(region.getNoteGain(2, 21_norm) == 0.25_a);
|
||||
REQUIRE(region.getNoteGain(3, 22_norm) == 0.5_a);
|
||||
REQUIRE(region.getNoteGain(4, 23_norm) == 0.75_a);
|
||||
REQUIRE(region.getNoteGain(5, 24_norm) == 1.0_a);
|
||||
REQUIRE(region.getNoteGain(5, 25_norm) == 1.0_a);
|
||||
}
|
||||
|
||||
TEST_CASE("[Region] Crossfade out on vel")
|
||||
|
|
@ -132,13 +132,13 @@ TEST_CASE("[Region] Crossfade out on vel")
|
|||
region.parseOpcode({ "xfout_lovel", "51" });
|
||||
region.parseOpcode({ "xfout_hivel", "55" });
|
||||
region.parseOpcode({ "amp_veltrack", "0" });
|
||||
REQUIRE( region.getNoteGain(5, 50_norm) == 1.0_a );
|
||||
REQUIRE( region.getNoteGain(5, 51_norm) == 1.0_a );
|
||||
REQUIRE( region.getNoteGain(5, 52_norm) == 0.86603_a );
|
||||
REQUIRE( region.getNoteGain(5, 53_norm) == 0.70711_a );
|
||||
REQUIRE( region.getNoteGain(5, 54_norm) == 0.5_a );
|
||||
REQUIRE( region.getNoteGain(5, 55_norm) == 0.0_a );
|
||||
REQUIRE( region.getNoteGain(5, 56_norm) == 0.0_a );
|
||||
REQUIRE(region.getNoteGain(5, 50_norm) == 1.0_a);
|
||||
REQUIRE(region.getNoteGain(5, 51_norm) == 1.0_a);
|
||||
REQUIRE(region.getNoteGain(5, 52_norm) == 0.86603_a);
|
||||
REQUIRE(region.getNoteGain(5, 53_norm) == 0.70711_a);
|
||||
REQUIRE(region.getNoteGain(5, 54_norm) == 0.5_a);
|
||||
REQUIRE(region.getNoteGain(5, 55_norm) == 0.0_a);
|
||||
REQUIRE(region.getNoteGain(5, 56_norm) == 0.0_a);
|
||||
}
|
||||
|
||||
TEST_CASE("[Region] Crossfade out on vel - gain")
|
||||
|
|
@ -150,13 +150,13 @@ TEST_CASE("[Region] Crossfade out on vel - gain")
|
|||
region.parseOpcode({ "xfout_hivel", "55" });
|
||||
region.parseOpcode({ "xf_velcurve", "gain" });
|
||||
region.parseOpcode({ "amp_veltrack", "0" });
|
||||
REQUIRE( region.getNoteGain(56, 50_norm) == 1.0_a );
|
||||
REQUIRE( region.getNoteGain(56, 51_norm) == 1.0_a );
|
||||
REQUIRE( region.getNoteGain(56, 52_norm) == 0.75_a );
|
||||
REQUIRE( region.getNoteGain(56, 53_norm) == 0.5_a );
|
||||
REQUIRE( region.getNoteGain(56, 54_norm) == 0.25_a );
|
||||
REQUIRE( region.getNoteGain(56, 55_norm) == 0.0_a );
|
||||
REQUIRE( region.getNoteGain(56, 56_norm) == 0.0_a );
|
||||
REQUIRE(region.getNoteGain(56, 50_norm) == 1.0_a);
|
||||
REQUIRE(region.getNoteGain(56, 51_norm) == 1.0_a);
|
||||
REQUIRE(region.getNoteGain(56, 52_norm) == 0.75_a);
|
||||
REQUIRE(region.getNoteGain(56, 53_norm) == 0.5_a);
|
||||
REQUIRE(region.getNoteGain(56, 54_norm) == 0.25_a);
|
||||
REQUIRE(region.getNoteGain(56, 55_norm) == 0.0_a);
|
||||
REQUIRE(region.getNoteGain(56, 56_norm) == 0.0_a);
|
||||
}
|
||||
|
||||
TEST_CASE("[Region] Crossfade in on CC")
|
||||
|
|
@ -167,13 +167,20 @@ TEST_CASE("[Region] Crossfade in on CC")
|
|||
region.parseOpcode({ "xfin_locc24", "20" });
|
||||
region.parseOpcode({ "xfin_hicc24", "24" });
|
||||
region.parseOpcode({ "amp_veltrack", "0" });
|
||||
midiState.ccEvent(0, 24, 19_norm); REQUIRE( region.getCrossfadeGain() == 0.0_a );
|
||||
midiState.ccEvent(0, 24, 20_norm); REQUIRE( region.getCrossfadeGain() == 0.0_a );
|
||||
midiState.ccEvent(0, 24, 21_norm); REQUIRE( region.getCrossfadeGain() == 0.5_a );
|
||||
midiState.ccEvent(0, 24, 22_norm); REQUIRE( region.getCrossfadeGain() == 0.70711_a );
|
||||
midiState.ccEvent(0, 24, 23_norm); REQUIRE( region.getCrossfadeGain() == 0.86603_a );
|
||||
midiState.ccEvent(0, 24, 24_norm); REQUIRE( region.getCrossfadeGain() == 1.0_a );
|
||||
midiState.ccEvent(0, 24, 25_norm); REQUIRE( region.getCrossfadeGain() == 1.0_a );
|
||||
midiState.ccEvent(0, 24, 19_norm);
|
||||
REQUIRE(region.getCrossfadeGain() == 0.0_a);
|
||||
midiState.ccEvent(0, 24, 20_norm);
|
||||
REQUIRE(region.getCrossfadeGain() == 0.0_a);
|
||||
midiState.ccEvent(0, 24, 21_norm);
|
||||
REQUIRE(region.getCrossfadeGain() == 0.5_a);
|
||||
midiState.ccEvent(0, 24, 22_norm);
|
||||
REQUIRE(region.getCrossfadeGain() == 0.70711_a);
|
||||
midiState.ccEvent(0, 24, 23_norm);
|
||||
REQUIRE(region.getCrossfadeGain() == 0.86603_a);
|
||||
midiState.ccEvent(0, 24, 24_norm);
|
||||
REQUIRE(region.getCrossfadeGain() == 1.0_a);
|
||||
midiState.ccEvent(0, 24, 25_norm);
|
||||
REQUIRE(region.getCrossfadeGain() == 1.0_a);
|
||||
}
|
||||
|
||||
TEST_CASE("[Region] Crossfade in on CC - gain")
|
||||
|
|
@ -185,13 +192,20 @@ TEST_CASE("[Region] Crossfade in on CC - gain")
|
|||
region.parseOpcode({ "xfin_hicc24", "24" });
|
||||
region.parseOpcode({ "amp_veltrack", "0" });
|
||||
region.parseOpcode({ "xf_cccurve", "gain" });
|
||||
midiState.ccEvent(0, 24, 19_norm); REQUIRE( region.getCrossfadeGain() == 0.0_a );
|
||||
midiState.ccEvent(0, 24, 20_norm); REQUIRE( region.getCrossfadeGain() == 0.0_a );
|
||||
midiState.ccEvent(0, 24, 21_norm); REQUIRE( region.getCrossfadeGain() == 0.25_a );
|
||||
midiState.ccEvent(0, 24, 22_norm); REQUIRE( region.getCrossfadeGain() == 0.5_a );
|
||||
midiState.ccEvent(0, 24, 23_norm); REQUIRE( region.getCrossfadeGain() == 0.75_a );
|
||||
midiState.ccEvent(0, 24, 24_norm); REQUIRE( region.getCrossfadeGain() == 1.0_a );
|
||||
midiState.ccEvent(0, 24, 25_norm); REQUIRE( region.getCrossfadeGain() == 1.0_a );
|
||||
midiState.ccEvent(0, 24, 19_norm);
|
||||
REQUIRE(region.getCrossfadeGain() == 0.0_a);
|
||||
midiState.ccEvent(0, 24, 20_norm);
|
||||
REQUIRE(region.getCrossfadeGain() == 0.0_a);
|
||||
midiState.ccEvent(0, 24, 21_norm);
|
||||
REQUIRE(region.getCrossfadeGain() == 0.25_a);
|
||||
midiState.ccEvent(0, 24, 22_norm);
|
||||
REQUIRE(region.getCrossfadeGain() == 0.5_a);
|
||||
midiState.ccEvent(0, 24, 23_norm);
|
||||
REQUIRE(region.getCrossfadeGain() == 0.75_a);
|
||||
midiState.ccEvent(0, 24, 24_norm);
|
||||
REQUIRE(region.getCrossfadeGain() == 1.0_a);
|
||||
midiState.ccEvent(0, 24, 25_norm);
|
||||
REQUIRE(region.getCrossfadeGain() == 1.0_a);
|
||||
}
|
||||
TEST_CASE("[Region] Crossfade out on CC")
|
||||
{
|
||||
|
|
@ -201,13 +215,20 @@ TEST_CASE("[Region] Crossfade out on CC")
|
|||
region.parseOpcode({ "xfout_locc24", "20" });
|
||||
region.parseOpcode({ "xfout_hicc24", "24" });
|
||||
region.parseOpcode({ "amp_veltrack", "0" });
|
||||
midiState.ccEvent(0, 24, 19_norm); REQUIRE( region.getCrossfadeGain() == 1.0_a );
|
||||
midiState.ccEvent(0, 24, 20_norm); REQUIRE( region.getCrossfadeGain() == 1.0_a );
|
||||
midiState.ccEvent(0, 24, 21_norm); REQUIRE( region.getCrossfadeGain() == 0.86603_a );
|
||||
midiState.ccEvent(0, 24, 22_norm); REQUIRE( region.getCrossfadeGain() == 0.70711_a );
|
||||
midiState.ccEvent(0, 24, 23_norm); REQUIRE( region.getCrossfadeGain() == 0.5_a );
|
||||
midiState.ccEvent(0, 24, 24_norm); REQUIRE( region.getCrossfadeGain() == 0.0_a );
|
||||
midiState.ccEvent(0, 24, 25_norm); REQUIRE( region.getCrossfadeGain() == 0.0_a );
|
||||
midiState.ccEvent(0, 24, 19_norm);
|
||||
REQUIRE(region.getCrossfadeGain() == 1.0_a);
|
||||
midiState.ccEvent(0, 24, 20_norm);
|
||||
REQUIRE(region.getCrossfadeGain() == 1.0_a);
|
||||
midiState.ccEvent(0, 24, 21_norm);
|
||||
REQUIRE(region.getCrossfadeGain() == 0.86603_a);
|
||||
midiState.ccEvent(0, 24, 22_norm);
|
||||
REQUIRE(region.getCrossfadeGain() == 0.70711_a);
|
||||
midiState.ccEvent(0, 24, 23_norm);
|
||||
REQUIRE(region.getCrossfadeGain() == 0.5_a);
|
||||
midiState.ccEvent(0, 24, 24_norm);
|
||||
REQUIRE(region.getCrossfadeGain() == 0.0_a);
|
||||
midiState.ccEvent(0, 24, 25_norm);
|
||||
REQUIRE(region.getCrossfadeGain() == 0.0_a);
|
||||
}
|
||||
|
||||
TEST_CASE("[Region] Crossfade out on CC - gain")
|
||||
|
|
@ -219,13 +240,20 @@ TEST_CASE("[Region] Crossfade out on CC - gain")
|
|||
region.parseOpcode({ "xfout_hicc24", "24" });
|
||||
region.parseOpcode({ "amp_veltrack", "0" });
|
||||
region.parseOpcode({ "xf_cccurve", "gain" });
|
||||
midiState.ccEvent(0, 24, 19_norm); REQUIRE( region.getCrossfadeGain() == 1.0_a );
|
||||
midiState.ccEvent(0, 24, 20_norm); REQUIRE( region.getCrossfadeGain() == 1.0_a );
|
||||
midiState.ccEvent(0, 24, 21_norm); REQUIRE( region.getCrossfadeGain() == 0.75_a );
|
||||
midiState.ccEvent(0, 24, 22_norm); REQUIRE( region.getCrossfadeGain() == 0.5_a );
|
||||
midiState.ccEvent(0, 24, 23_norm); REQUIRE( region.getCrossfadeGain() == 0.25_a );
|
||||
midiState.ccEvent(0, 24, 24_norm); REQUIRE( region.getCrossfadeGain() == 0.0_a );
|
||||
midiState.ccEvent(0, 24, 25_norm); REQUIRE( region.getCrossfadeGain() == 0.0_a );
|
||||
midiState.ccEvent(0, 24, 19_norm);
|
||||
REQUIRE(region.getCrossfadeGain() == 1.0_a);
|
||||
midiState.ccEvent(0, 24, 20_norm);
|
||||
REQUIRE(region.getCrossfadeGain() == 1.0_a);
|
||||
midiState.ccEvent(0, 24, 21_norm);
|
||||
REQUIRE(region.getCrossfadeGain() == 0.75_a);
|
||||
midiState.ccEvent(0, 24, 22_norm);
|
||||
REQUIRE(region.getCrossfadeGain() == 0.5_a);
|
||||
midiState.ccEvent(0, 24, 23_norm);
|
||||
REQUIRE(region.getCrossfadeGain() == 0.25_a);
|
||||
midiState.ccEvent(0, 24, 24_norm);
|
||||
REQUIRE(region.getCrossfadeGain() == 0.0_a);
|
||||
midiState.ccEvent(0, 24, 25_norm);
|
||||
REQUIRE(region.getCrossfadeGain() == 0.0_a);
|
||||
}
|
||||
|
||||
TEST_CASE("[Region] Velocity bug for extreme values - veltrack at 0")
|
||||
|
|
@ -234,8 +262,8 @@ TEST_CASE("[Region] Velocity bug for extreme values - veltrack at 0")
|
|||
sfz::Region region { midiState };
|
||||
region.parseOpcode({ "sample", "*sine" });
|
||||
region.parseOpcode({ "amp_veltrack", "0" });
|
||||
REQUIRE( region.getNoteGain(64, 127_norm) == 1.0_a );
|
||||
REQUIRE( region.getNoteGain(64, 0_norm) == 1.0_a );
|
||||
REQUIRE(region.getNoteGain(64, 127_norm) == 1.0_a);
|
||||
REQUIRE(region.getNoteGain(64, 0_norm) == 1.0_a);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -245,8 +273,8 @@ TEST_CASE("[Region] Velocity bug for extreme values - positive veltrack")
|
|||
sfz::Region region { midiState };
|
||||
region.parseOpcode({ "sample", "*sine" });
|
||||
region.parseOpcode({ "amp_veltrack", "100" });
|
||||
REQUIRE( region.getNoteGain(64, 127_norm) == 1.0_a );
|
||||
REQUIRE( region.getNoteGain(64, 0_norm) == Approx(0.0).margin(0.0001) );
|
||||
REQUIRE(region.getNoteGain(64, 127_norm) == 1.0_a);
|
||||
REQUIRE(region.getNoteGain(64, 0_norm) == Approx(0.0).margin(0.0001));
|
||||
}
|
||||
|
||||
TEST_CASE("[Region] Velocity bug for extreme values - negative veltrack")
|
||||
|
|
@ -255,27 +283,28 @@ TEST_CASE("[Region] Velocity bug for extreme values - negative veltrack")
|
|||
sfz::Region region { midiState };
|
||||
region.parseOpcode({ "sample", "*sine" });
|
||||
region.parseOpcode({ "amp_veltrack", "-100" });
|
||||
REQUIRE( region.getNoteGain(64, 127_norm) == Approx(0.0).margin(0.0001) );
|
||||
REQUIRE( region.getNoteGain(64, 0_norm) == 1.0_a );
|
||||
REQUIRE(region.getNoteGain(64, 127_norm) == Approx(0.0).margin(0.0001));
|
||||
REQUIRE(region.getNoteGain(64, 0_norm) == 1.0_a);
|
||||
}
|
||||
|
||||
TEST_CASE("[Region] rt_decay")
|
||||
{
|
||||
sfz::MidiState midiState;
|
||||
midiState.setSampleRate(1000);
|
||||
sfz::Region region { midiState };
|
||||
region.parseOpcode({ "sample", "*sine" });
|
||||
region.parseOpcode({ "trigger", "release" });
|
||||
region.parseOpcode({ "rt_decay", "10" });
|
||||
midiState.noteOnEvent(0, 64, 64_norm);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
midiState.advanceTime(100);
|
||||
REQUIRE( region.getBaseVolumedB(64) == Approx(sfz::Default::volume - 1.0f).margin(0.1) );
|
||||
region.parseOpcode({ "rt_decay", "20" });
|
||||
midiState.noteOnEvent(0, 64, 64_norm);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
midiState.advanceTime(100);
|
||||
REQUIRE( region.getBaseVolumedB(64) == Approx(sfz::Default::volume - 2.0f).margin(0.1) );
|
||||
region.parseOpcode({ "trigger", "attack" });
|
||||
midiState.noteOnEvent(0, 64, 64_norm);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
midiState.advanceTime(100);
|
||||
REQUIRE( region.getBaseVolumedB(64) == Approx(sfz::Default::volume).margin(0.1) );
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -499,7 +499,7 @@ TEST_CASE("[Helpers] Linear Ramp")
|
|||
const float start { 0.0f };
|
||||
const float v { fillValue };
|
||||
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);
|
||||
REQUIRE(output == expected);
|
||||
}
|
||||
|
|
@ -509,7 +509,7 @@ TEST_CASE("[Helpers] Linear Ramp (SIMD)")
|
|||
const float start { 0.0f };
|
||||
const float v { fillValue };
|
||||
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);
|
||||
REQUIRE(approxEqual<float>(output, expected));
|
||||
}
|
||||
|
|
@ -539,7 +539,7 @@ TEST_CASE("[Helpers] Multiplicative Ramp")
|
|||
const float start { 1.0f };
|
||||
const float v { fillValue };
|
||||
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);
|
||||
REQUIRE(approxEqual<float>(output, expected));
|
||||
}
|
||||
|
|
@ -549,7 +549,7 @@ TEST_CASE("[Helpers] Multiplicative Ramp (SIMD)")
|
|||
const float start { 1.0f };
|
||||
const float v { fillValue };
|
||||
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);
|
||||
REQUIRE(approxEqual<float>(output, expected));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -141,9 +141,9 @@ TEST_CASE("[Synth] Reset all controllers")
|
|||
{
|
||||
sfz::Synth synth;
|
||||
synth.cc(0, 12, 64);
|
||||
REQUIRE( synth.getMidiState().getCCValue(12) == 64_norm );
|
||||
REQUIRE(synth.getMidiState().getCCValue(12) == 64_norm);
|
||||
synth.cc(0, 121, 64);
|
||||
REQUIRE( synth.getMidiState().getCCValue(12) == 0_norm );
|
||||
REQUIRE(synth.getMidiState().getCCValue(12) == 0_norm);
|
||||
}
|
||||
|
||||
TEST_CASE("[Synth] Releasing before the EG started smoothing (initial delay) kills the voice")
|
||||
|
|
@ -332,3 +332,45 @@ TEST_CASE("[Synth] Gain to mix")
|
|||
REQUIRE( bus->gainToMain() == 0 );
|
||||
REQUIRE( bus->gainToMix() == 0.5 );
|
||||
}
|
||||
|
||||
TEST_CASE("[Synth] group polyphony limits")
|
||||
{
|
||||
sfz::Synth synth;
|
||||
synth.loadSfzFile(fs::current_path() / "tests/TestFiles/polyphony.sfz");
|
||||
synth.noteOn(0, 65, 64);
|
||||
synth.noteOn(0, 65, 64);
|
||||
synth.noteOn(0, 65, 64);
|
||||
REQUIRE(synth.getNumActiveVoices() == 2); // group polyphony should block the last note
|
||||
}
|
||||
|
||||
TEST_CASE("[Synth] Self-masking")
|
||||
{
|
||||
sfz::Synth synth;
|
||||
synth.loadSfzFile(fs::current_path() / "tests/TestFiles/polyphony.sfz");
|
||||
synth.noteOn(0, 64, 63);
|
||||
synth.noteOn(0, 64, 62);
|
||||
synth.noteOn(0, 64, 64);
|
||||
REQUIRE(synth.getNumActiveVoices() == 3); // One of these is releasing
|
||||
REQUIRE(synth.getVoiceView(0)->getTriggerValue() == 63_norm);
|
||||
REQUIRE(!synth.getVoiceView(0)->canBeStolen());
|
||||
REQUIRE(synth.getVoiceView(1)->getTriggerValue() == 62_norm);
|
||||
REQUIRE(synth.getVoiceView(1)->canBeStolen()); // The lowest velocity voice is the masking candidate
|
||||
REQUIRE(synth.getVoiceView(2)->getTriggerValue() == 64_norm);
|
||||
REQUIRE(!synth.getVoiceView(2)->canBeStolen());
|
||||
}
|
||||
|
||||
TEST_CASE("[Synth] Not self-masking")
|
||||
{
|
||||
sfz::Synth synth;
|
||||
synth.loadSfzFile(fs::current_path() / "tests/TestFiles/polyphony.sfz");
|
||||
synth.noteOn(0, 66, 63);
|
||||
synth.noteOn(0, 66, 62);
|
||||
synth.noteOn(0, 66, 64);
|
||||
REQUIRE(synth.getNumActiveVoices() == 3); // One of these is releasing
|
||||
REQUIRE(synth.getVoiceView(0)->getTriggerValue() == 63_norm);
|
||||
REQUIRE(synth.getVoiceView(0)->canBeStolen()); // The first encountered voice is the masking candidate
|
||||
REQUIRE(synth.getVoiceView(1)->getTriggerValue() == 62_norm);
|
||||
REQUIRE(!synth.getVoiceView(1)->canBeStolen());
|
||||
REQUIRE(synth.getVoiceView(2)->getTriggerValue() == 64_norm);
|
||||
REQUIRE(!synth.getVoiceView(2)->canBeStolen());
|
||||
}
|
||||
|
|
|
|||
5
tests/TestFiles/polyphony.sfz
Normal file
5
tests/TestFiles/polyphony.sfz
Normal 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
|
||||
Loading…
Add table
Reference in a new issue