commit
53a4cf0ead
23 changed files with 1149 additions and 52 deletions
|
|
@ -14,58 +14,64 @@
|
||||||
|
|
||||||
class MultiplyAddFixedGain : public benchmark::Fixture {
|
class MultiplyAddFixedGain : public benchmark::Fixture {
|
||||||
public:
|
public:
|
||||||
void SetUp(const ::benchmark::State& state) {
|
void SetUp(const ::benchmark::State& state)
|
||||||
std::random_device rd { };
|
{
|
||||||
std::mt19937 gen { rd() };
|
std::random_device rd {};
|
||||||
std::uniform_real_distribution<float> dist { 0, 1 };
|
std::mt19937 gen { rd() };
|
||||||
input = std::vector<float>(state.range(0));
|
std::uniform_real_distribution<float> dist { 0, 1 };
|
||||||
output = std::vector<float>(state.range(0));
|
input = std::vector<float>(state.range(0));
|
||||||
gain = dist(gen);
|
output = std::vector<float>(state.range(0));
|
||||||
std::fill(output.begin(), output.end(), 1.0f );
|
gain = dist(gen);
|
||||||
std::generate(input.begin(), input.end(), [&]() { return dist(gen); });
|
std::fill(output.begin(), output.end(), 1.0f);
|
||||||
}
|
std::generate(input.begin(), input.end(), [&]() { return dist(gen); });
|
||||||
|
}
|
||||||
|
|
||||||
void TearDown(const ::benchmark::State& state [[maybe_unused]]) {
|
void TearDown(const ::benchmark::State& state [[maybe_unused]])
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
}
|
float gain = {};
|
||||||
|
std::vector<float> input;
|
||||||
float gain = {};
|
std::vector<float> output;
|
||||||
std::vector<float> input;
|
|
||||||
std::vector<float> output;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
BENCHMARK_DEFINE_F(MultiplyAddFixedGain, Straight)(benchmark::State& state) {
|
BENCHMARK_DEFINE_F(MultiplyAddFixedGain, Straight)
|
||||||
for (auto _ : state)
|
(benchmark::State& state)
|
||||||
{
|
{
|
||||||
|
for (auto _ : state) {
|
||||||
for (int i = 0; i < state.range(0); ++i)
|
for (int i = 0; i < state.range(0); ++i)
|
||||||
output[i] += gain * input[i];
|
output[i] += gain * input[i];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
BENCHMARK_DEFINE_F(MultiplyAddFixedGain, Scalar)(benchmark::State& state) {
|
BENCHMARK_DEFINE_F(MultiplyAddFixedGain, Scalar)
|
||||||
for (auto _ : state)
|
(benchmark::State& state)
|
||||||
{
|
{
|
||||||
|
for (auto _ : state) {
|
||||||
sfz::multiplyAdd<float, false>(gain, input, absl::MakeSpan(output));
|
sfz::multiplyAdd<float, false>(gain, input, absl::MakeSpan(output));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
BENCHMARK_DEFINE_F(MultiplyAddFixedGain, SIMD)(benchmark::State& state) {
|
BENCHMARK_DEFINE_F(MultiplyAddFixedGain, SIMD)
|
||||||
for (auto _ : state)
|
(benchmark::State& state)
|
||||||
{
|
{
|
||||||
|
for (auto _ : state) {
|
||||||
sfz::multiplyAdd<float, true>(gain, input, absl::MakeSpan(output));
|
sfz::multiplyAdd<float, true>(gain, input, absl::MakeSpan(output));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
BENCHMARK_DEFINE_F(MultiplyAddFixedGain, Scalar_Unaligned)(benchmark::State& state) {
|
BENCHMARK_DEFINE_F(MultiplyAddFixedGain, Scalar_Unaligned)
|
||||||
for (auto _ : state)
|
(benchmark::State& state)
|
||||||
{
|
{
|
||||||
|
for (auto _ : state) {
|
||||||
sfz::multiplyAdd<float, false>(gain, absl::MakeSpan(input).subspan(1), absl::MakeSpan(output).subspan(1));
|
sfz::multiplyAdd<float, false>(gain, absl::MakeSpan(input).subspan(1), absl::MakeSpan(output).subspan(1));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
BENCHMARK_DEFINE_F(MultiplyAddFixedGain, SIMD_Unaligned)(benchmark::State& state) {
|
BENCHMARK_DEFINE_F(MultiplyAddFixedGain, SIMD_Unaligned)
|
||||||
for (auto _ : state)
|
(benchmark::State& state)
|
||||||
{
|
{
|
||||||
|
for (auto _ : state) {
|
||||||
sfz::multiplyAdd<float, true>(gain, absl::MakeSpan(input).subspan(1), absl::MakeSpan(output).subspan(1));
|
sfz::multiplyAdd<float, true>(gain, absl::MakeSpan(input).subspan(1), absl::MakeSpan(output).subspan(1));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,9 @@ set (SFIZZ_SOURCES
|
||||||
sfizz/FloatEnvelopes.cpp
|
sfizz/FloatEnvelopes.cpp
|
||||||
sfizz/Logger.cpp
|
sfizz/Logger.cpp
|
||||||
sfizz/SfzFilter.cpp
|
sfizz/SfzFilter.cpp
|
||||||
|
sfizz/Effects.cpp
|
||||||
|
sfizz/effects/Nothing.cpp
|
||||||
|
sfizz/effects/Lofi.cpp
|
||||||
)
|
)
|
||||||
include (SfizzSIMDSourceFiles)
|
include (SfizzSIMDSourceFiles)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -209,6 +209,22 @@ public:
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Convert implicitly to a pointer of channels
|
||||||
|
*/
|
||||||
|
operator const float* const*() const noexcept
|
||||||
|
{
|
||||||
|
return spans.data();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Convert implicitly to a pointer of channels
|
||||||
|
*/
|
||||||
|
operator float* const*() noexcept
|
||||||
|
{
|
||||||
|
return spans.data();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Get a Span<Type> corresponding to a specific channel
|
* @brief Get a Span<Type> corresponding to a specific channel
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,10 @@ namespace config {
|
||||||
*/
|
*/
|
||||||
const absl::string_view midnamManufacturer { "The Sfizz authors" };
|
const absl::string_view midnamManufacturer { "The Sfizz authors" };
|
||||||
const absl::string_view midnamModel { "Sfizz" };
|
const absl::string_view midnamModel { "Sfizz" };
|
||||||
|
/**
|
||||||
|
Limit of how many "fxN" buses are accepted (in SFZv2, maximum is 4)
|
||||||
|
*/
|
||||||
|
constexpr int maxEffectBuses { 256 };
|
||||||
} // namespace config
|
} // namespace config
|
||||||
|
|
||||||
// Enable or disable SIMD accelerators by default
|
// Enable or disable SIMD accelerators by default
|
||||||
|
|
|
||||||
146
src/sfizz/Effects.cpp
Normal file
146
src/sfizz/Effects.cpp
Normal file
|
|
@ -0,0 +1,146 @@
|
||||||
|
// 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 "Effects.h"
|
||||||
|
#include "AudioSpan.h"
|
||||||
|
#include "Opcode.h"
|
||||||
|
#include "SIMDHelpers.h"
|
||||||
|
#include "Config.h"
|
||||||
|
#include "effects/Nothing.h"
|
||||||
|
#include "effects/Lofi.h"
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
namespace sfz {
|
||||||
|
|
||||||
|
void EffectFactory::registerStandardEffectTypes()
|
||||||
|
{
|
||||||
|
// TODO
|
||||||
|
registerEffectType("lofi", fx::Lofi::makeInstance);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EffectFactory::registerEffectType(absl::string_view name, Effect::MakeInstance& make)
|
||||||
|
{
|
||||||
|
FactoryEntry ent;
|
||||||
|
ent.name = std::string(name);
|
||||||
|
ent.make = &make;
|
||||||
|
_entries.push_back(std::move(ent));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<Effect> EffectFactory::makeEffect(absl::Span<const Opcode> members)
|
||||||
|
{
|
||||||
|
const Opcode* opcode = nullptr;
|
||||||
|
|
||||||
|
for (auto it = members.rbegin(); it != members.rend() && !opcode; ++it) {
|
||||||
|
if (it->lettersOnlyHash == hash("type"))
|
||||||
|
opcode = &*it;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!opcode) {
|
||||||
|
DBG("The effect does not specify a type");
|
||||||
|
return std::make_unique<sfz::fx::Nothing>();
|
||||||
|
}
|
||||||
|
|
||||||
|
const absl::string_view type = opcode->value;
|
||||||
|
|
||||||
|
const auto it = absl::c_find_if(_entries, [&](auto&& entry) { return entry.name == type; });
|
||||||
|
if (it == _entries.end()) {
|
||||||
|
DBG("Unsupported effect type: " << type);
|
||||||
|
return std::make_unique<sfz::fx::Nothing>();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto fx = it->make(members);
|
||||||
|
if (!fx) {
|
||||||
|
DBG("Could not instantiate effect of type: " << type);
|
||||||
|
return std::make_unique<sfz::fx::Nothing>();
|
||||||
|
}
|
||||||
|
|
||||||
|
return fx;
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
EffectBus::EffectBus()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
EffectBus::~EffectBus()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void EffectBus::addEffect(std::unique_ptr<Effect> fx)
|
||||||
|
{
|
||||||
|
_effects.emplace_back(std::move(fx));
|
||||||
|
}
|
||||||
|
|
||||||
|
void EffectBus::clearInputs(unsigned nframes)
|
||||||
|
{
|
||||||
|
AudioSpan<float>(_inputs).first(nframes).fill(0.0f);
|
||||||
|
AudioSpan<float>(_outputs).first(nframes).fill(0.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EffectBus::addToInputs(const float* const addInput[], float addGain, unsigned nframes)
|
||||||
|
{
|
||||||
|
if (addGain == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
for (unsigned c = 0; c < EffectChannels; ++c) {
|
||||||
|
absl::Span<const float> addIn { addInput[c], nframes };
|
||||||
|
sfz::multiplyAdd(addGain, addIn, _inputs.getSpan(c));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void EffectBus::setSampleRate(double sampleRate)
|
||||||
|
{
|
||||||
|
for (const auto& effectPtr : _effects)
|
||||||
|
effectPtr->setSampleRate(sampleRate);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EffectBus::clear()
|
||||||
|
{
|
||||||
|
for (const auto& effectPtr : _effects)
|
||||||
|
effectPtr->clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void EffectBus::process(unsigned nframes)
|
||||||
|
{
|
||||||
|
size_t numEffects = _effects.size();
|
||||||
|
|
||||||
|
if (numEffects > 0 && hasNonZeroOutput()) {
|
||||||
|
_effects[0]->process(
|
||||||
|
AudioSpan<float>(_inputs), AudioSpan<float>(_outputs), nframes);
|
||||||
|
for (size_t i = 1; i < numEffects; ++i)
|
||||||
|
_effects[i]->process(
|
||||||
|
AudioSpan<float>(_outputs), AudioSpan<float>(_outputs), nframes);
|
||||||
|
} else
|
||||||
|
fx::Nothing().process(
|
||||||
|
AudioSpan<float>(_inputs), AudioSpan<float>(_outputs), nframes);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EffectBus::mixOutputsTo(float* const mainOutput[], float* const mixOutput[], unsigned nframes)
|
||||||
|
{
|
||||||
|
const float gainToMain = _gainToMain;
|
||||||
|
const float gainToMix = _gainToMix;
|
||||||
|
|
||||||
|
for (unsigned c = 0; c < EffectChannels; ++c) {
|
||||||
|
auto fxOut = _outputs.getConstSpan(c);
|
||||||
|
sfz::multiplyAdd(gainToMain, fxOut, absl::Span<float>(mainOutput[c], nframes));
|
||||||
|
sfz::multiplyAdd(gainToMix, fxOut, absl::Span<float>(mixOutput[c], nframes));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t EffectBus::numEffects() const noexcept
|
||||||
|
{
|
||||||
|
return _effects.size();
|
||||||
|
}
|
||||||
|
void EffectBus::setSamplesPerBlock(int samplesPerBlock) noexcept
|
||||||
|
{
|
||||||
|
_inputs.resize(samplesPerBlock);
|
||||||
|
_outputs.resize(samplesPerBlock);
|
||||||
|
|
||||||
|
for (const auto& effectPtr : _effects)
|
||||||
|
effectPtr->setSamplesPerBlock(samplesPerBlock);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace sfz
|
||||||
180
src/sfizz/Effects.h
Normal file
180
src/sfizz/Effects.h
Normal file
|
|
@ -0,0 +1,180 @@
|
||||||
|
// 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 "AudioBuffer.h"
|
||||||
|
#include "absl/strings/string_view.h"
|
||||||
|
#include "absl/types/span.h"
|
||||||
|
#include <array>
|
||||||
|
#include <vector>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
namespace sfz {
|
||||||
|
struct Opcode;
|
||||||
|
|
||||||
|
enum {
|
||||||
|
// Number of channels processed by effects
|
||||||
|
EffectChannels = 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief Abstract base of SFZ effects
|
||||||
|
*/
|
||||||
|
class Effect {
|
||||||
|
public:
|
||||||
|
virtual ~Effect() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief Initializes with the given sample rate.
|
||||||
|
*/
|
||||||
|
virtual void setSampleRate(double sampleRate) = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Sets the maximum number of frames to render at a time. The actual
|
||||||
|
* value can be lower but should never be higher.
|
||||||
|
*/
|
||||||
|
virtual void setSamplesPerBlock(int samplesPerBlock) = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief Reset the state to initial.
|
||||||
|
*/
|
||||||
|
virtual void clear() = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief Computes a cycle of the effect in stereo.
|
||||||
|
*/
|
||||||
|
virtual void process(const float* const inputs[], float* const outputs[], unsigned nframes) = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief Type of the factory function used to instantiate an effect given
|
||||||
|
the contents of the <effect> block
|
||||||
|
*/
|
||||||
|
typedef std::unique_ptr<Effect>(MakeInstance)(absl::Span<const Opcode> members);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief SFZ effects factory
|
||||||
|
*/
|
||||||
|
class EffectFactory {
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
@brief Registers all available standard effects into the factory.
|
||||||
|
*/
|
||||||
|
void registerStandardEffectTypes();
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief Registers a user-defined effect into the factory.
|
||||||
|
*/
|
||||||
|
void registerEffectType(absl::string_view name, Effect::MakeInstance& make);
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief Instantiates an effect given the contents of the <effect> block.
|
||||||
|
*/
|
||||||
|
std::unique_ptr<Effect> makeEffect(absl::Span<const Opcode> members);
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct FactoryEntry {
|
||||||
|
std::string name;
|
||||||
|
Effect::MakeInstance* make;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<FactoryEntry> _entries;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief Sequence of effects processed in series
|
||||||
|
*/
|
||||||
|
class EffectBus {
|
||||||
|
public:
|
||||||
|
EffectBus();
|
||||||
|
~EffectBus();
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief Adds an effect at the end of the bus.
|
||||||
|
*/
|
||||||
|
void addEffect(std::unique_ptr<Effect> fx);
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief Checks whether this bus can produce output.
|
||||||
|
*/
|
||||||
|
bool hasNonZeroOutput() const { return _gainToMain != 0 || _gainToMix != 0; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief Sets the amount of effect output going to the main.
|
||||||
|
*/
|
||||||
|
void setGainToMain(float gain) { _gainToMain = gain; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief Sets the amount of effect output going to the mix.
|
||||||
|
*/
|
||||||
|
void setGainToMix(float gain) { _gainToMix = gain; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Returns the gain for the main out
|
||||||
|
*
|
||||||
|
* @return float
|
||||||
|
*/
|
||||||
|
float gainToMain() const { return _gainToMain; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Returns the gain for the mix out
|
||||||
|
*
|
||||||
|
* @return float
|
||||||
|
*/
|
||||||
|
float gainToMix() const { return _gainToMix; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief Resets the input buffers to zero.
|
||||||
|
*/
|
||||||
|
void clearInputs(unsigned nframes);
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief Adds some audio into the input buffer.
|
||||||
|
*/
|
||||||
|
void addToInputs(const float* const addInput[], float addGain, unsigned nframes);
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief Initializes all effects in the bus with the given sample rate.
|
||||||
|
*/
|
||||||
|
void setSampleRate(double sampleRate);
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief Resets the state of all effects in the bus.
|
||||||
|
*/
|
||||||
|
void clear();
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief Computes a cycle of the effect bus.
|
||||||
|
*/
|
||||||
|
void process(unsigned nframes);
|
||||||
|
|
||||||
|
/**
|
||||||
|
@brief Mixes the outputs into a pair of stereo signals: Main and Mix.
|
||||||
|
*/
|
||||||
|
void mixOutputsTo(float* const mainOutput[], float* const mixOutput[], unsigned nframes);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Sets the maximum number of frames to render at a time. The actual value can be lower
|
||||||
|
* but should never be higher.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
void setSamplesPerBlock(int samplesPerBlock) noexcept;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Return the number of effects in the bus
|
||||||
|
*
|
||||||
|
* @return size_t
|
||||||
|
*/
|
||||||
|
size_t numEffects() const noexcept;
|
||||||
|
private:
|
||||||
|
std::vector<std::unique_ptr<Effect>> _effects;
|
||||||
|
AudioBuffer<float> _inputs { EffectChannels, config::defaultSamplesPerBlock };
|
||||||
|
AudioBuffer<float> _outputs { EffectChannels, config::defaultSamplesPerBlock };
|
||||||
|
float _gainToMain = 0.0;
|
||||||
|
float _gainToMix = 0.0;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace sfz
|
||||||
|
|
@ -83,7 +83,7 @@ sfz::Logger::~Logger()
|
||||||
fs::path callbackLogPath{ fs::current_path() / callbackLogFilename.str() };
|
fs::path callbackLogPath{ fs::current_path() / callbackLogFilename.str() };
|
||||||
std::cout << "Logging " << callbackTimes.size() << " callback times to " << callbackLogPath.filename() << '\n';
|
std::cout << "Logging " << callbackTimes.size() << " callback times to " << callbackLogPath.filename() << '\n';
|
||||||
std::ofstream callbackLogFile { callbackLogPath.string() };
|
std::ofstream callbackLogFile { callbackLogPath.string() };
|
||||||
callbackLogFile << "Dispatch,RenderMethod,Data,Amplitude,Filters,Panning,NumVoices,NumSamples" << '\n';
|
callbackLogFile << "Dispatch,RenderMethod,Data,Amplitude,Filters,Panning,Effects,NumVoices,NumSamples" << '\n';
|
||||||
for (auto& time: callbackTimes)
|
for (auto& time: callbackTimes)
|
||||||
callbackLogFile << time.breakdown.dispatch.count() << ','
|
callbackLogFile << time.breakdown.dispatch.count() << ','
|
||||||
<< time.breakdown.renderMethod.count() << ','
|
<< time.breakdown.renderMethod.count() << ','
|
||||||
|
|
@ -91,6 +91,7 @@ sfz::Logger::~Logger()
|
||||||
<< time.breakdown.amplitude.count() << ','
|
<< time.breakdown.amplitude.count() << ','
|
||||||
<< time.breakdown.filters.count() << ','
|
<< time.breakdown.filters.count() << ','
|
||||||
<< time.breakdown.panning.count() << ','
|
<< time.breakdown.panning.count() << ','
|
||||||
|
<< time.breakdown.effects.count() << ','
|
||||||
<< time.numVoices << ','
|
<< time.numVoices << ','
|
||||||
<< time.numSamples << '\n';
|
<< time.numSamples << '\n';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,7 @@ struct CallbackBreakdown
|
||||||
Duration amplitude { 0 };
|
Duration amplitude { 0 };
|
||||||
Duration filters { 0 };
|
Duration filters { 0 };
|
||||||
Duration panning { 0 };
|
Duration panning { 0 };
|
||||||
|
Duration effects { 0 };
|
||||||
};
|
};
|
||||||
|
|
||||||
struct CallbackTime
|
struct CallbackTime
|
||||||
|
|
|
||||||
|
|
@ -733,6 +733,20 @@ bool sfz::Region::parseOpcode(const Opcode& opcode)
|
||||||
setCCPairFromOpcode(opcode, amplitudeEG.ccSustain, Default::egOnCCPercentRange);
|
setCCPairFromOpcode(opcode, amplitudeEG.ccSustain, Default::egOnCCPercentRange);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case hash("effect&"):
|
||||||
|
{
|
||||||
|
const auto effectNumber = opcode.parameters.back();
|
||||||
|
if (!effectNumber || effectNumber < 1 || effectNumber > config::maxEffectBuses)
|
||||||
|
break;
|
||||||
|
auto value = readOpcode<float>(opcode.value, { 0, 100 });
|
||||||
|
if (!value)
|
||||||
|
break;
|
||||||
|
if (static_cast<size_t>(effectNumber + 1) > gainToEffect.size())
|
||||||
|
gainToEffect.resize(effectNumber + 1);
|
||||||
|
gainToEffect[effectNumber] = *value / 100;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
// Ignored opcodes
|
// Ignored opcodes
|
||||||
case hash("hichan"):
|
case hash("hichan"):
|
||||||
case hash("lochan"):
|
case hash("lochan"):
|
||||||
|
|
@ -1073,3 +1087,11 @@ void sfz::Region::offsetAllKeys(int offset) noexcept
|
||||||
crossfadeKeyOutRange.setEnd(offsetAndClamp(end, offset, Default::keyRange));
|
crossfadeKeyOutRange.setEnd(offsetAndClamp(end, offset, Default::keyRange));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
float sfz::Region::getGainToEffectBus(unsigned number) const noexcept
|
||||||
|
{
|
||||||
|
if (number >= gainToEffect.size())
|
||||||
|
return 0.0;
|
||||||
|
|
||||||
|
return gainToEffect[number];
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,9 @@ struct Region {
|
||||||
: midiState(midiState), defaultPath(std::move(defaultPath))
|
: midiState(midiState), defaultPath(std::move(defaultPath))
|
||||||
{
|
{
|
||||||
ccSwitched.set();
|
ccSwitched.set();
|
||||||
|
|
||||||
|
gainToEffect.reserve(5); // sufficient room for main and fx1-4
|
||||||
|
gainToEffect.push_back(1.0); // contribute 100% into the main bus
|
||||||
}
|
}
|
||||||
Region(const Region&) = default;
|
Region(const Region&) = default;
|
||||||
~Region() = default;
|
~Region() = default;
|
||||||
|
|
@ -206,6 +209,12 @@ struct Region {
|
||||||
|
|
||||||
bool hasKeyswitches() const noexcept { return keyswitchDown || keyswitchUp || keyswitch || previousNote; }
|
bool hasKeyswitches() const noexcept { return keyswitchDown || keyswitchUp || keyswitch || previousNote; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get the gain this region contributes into the input of the Nth
|
||||||
|
* effect bus
|
||||||
|
*/
|
||||||
|
float getGainToEffectBus(unsigned number) const noexcept;
|
||||||
|
|
||||||
// Sound source: sample playback
|
// Sound source: sample playback
|
||||||
std::string sample {}; // Sample
|
std::string sample {}; // Sample
|
||||||
float delay { Default::delay }; // delay
|
float delay { Default::delay }; // delay
|
||||||
|
|
@ -297,6 +306,10 @@ struct Region {
|
||||||
EGDescription filterEG;
|
EGDescription filterEG;
|
||||||
|
|
||||||
bool isStereo { false };
|
bool isStereo { false };
|
||||||
|
|
||||||
|
// Effects
|
||||||
|
std::vector<float> gainToEffect;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
const MidiState& midiState;
|
const MidiState& midiState;
|
||||||
bool keySwitched { true };
|
bool keySwitched { true };
|
||||||
|
|
|
||||||
|
|
@ -21,12 +21,16 @@
|
||||||
using namespace std::literals;
|
using namespace std::literals;
|
||||||
|
|
||||||
sfz::Synth::Synth()
|
sfz::Synth::Synth()
|
||||||
|
: Synth(config::numVoices)
|
||||||
{
|
{
|
||||||
resetVoices(this->numVoices);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sfz::Synth::Synth(int numVoices)
|
sfz::Synth::Synth(int numVoices)
|
||||||
{
|
{
|
||||||
|
effectFactory.registerStandardEffectTypes();
|
||||||
|
|
||||||
|
effectBuses.reserve(5); // sufficient room for main and fx1-4
|
||||||
|
|
||||||
resetVoices(numVoices);
|
resetVoices(numVoices);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -70,7 +74,7 @@ void sfz::Synth::callback(absl::string_view header, const std::vector<Opcode>& m
|
||||||
numCurves++;
|
numCurves++;
|
||||||
break;
|
break;
|
||||||
case hash("effect"):
|
case hash("effect"):
|
||||||
// TODO: implement effects
|
handleEffectOpcodes(members);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
std::cerr << "Unknown header: " << header << '\n';
|
std::cerr << "Unknown header: " << header << '\n';
|
||||||
|
|
@ -118,6 +122,11 @@ void sfz::Synth::clear()
|
||||||
for (auto& list: ccActivationLists)
|
for (auto& list: ccActivationLists)
|
||||||
list.clear();
|
list.clear();
|
||||||
regions.clear();
|
regions.clear();
|
||||||
|
effectBuses.clear();
|
||||||
|
effectBuses.emplace_back(new EffectBus);
|
||||||
|
effectBuses[0]->setGainToMain(1.0);
|
||||||
|
effectBuses[0]->setSamplesPerBlock(samplesPerBlock);
|
||||||
|
effectBuses[0]->setSampleRate(sampleRate);
|
||||||
resources.filePool.clear();
|
resources.filePool.clear();
|
||||||
resources.logger.clear();
|
resources.logger.clear();
|
||||||
numGroups = 0;
|
numGroups = 0;
|
||||||
|
|
@ -185,6 +194,68 @@ void sfz::Synth::handleControlOpcodes(const std::vector<Opcode>& members)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void sfz::Synth::handleEffectOpcodes(const std::vector<Opcode>& members)
|
||||||
|
{
|
||||||
|
absl::string_view busName = "main";
|
||||||
|
|
||||||
|
auto getOrCreateBus = [this](unsigned index) -> EffectBus& {
|
||||||
|
if (index + 1 > effectBuses.size())
|
||||||
|
effectBuses.resize(index + 1);
|
||||||
|
EffectBusPtr& bus = effectBuses[index];
|
||||||
|
if (!bus) {
|
||||||
|
bus.reset(new EffectBus);
|
||||||
|
bus->setSampleRate(sampleRate);
|
||||||
|
bus->setSamplesPerBlock(samplesPerBlock);
|
||||||
|
}
|
||||||
|
return *bus;
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const Opcode& opcode : members) {
|
||||||
|
switch (opcode.lettersOnlyHash) {
|
||||||
|
case hash("bus"):
|
||||||
|
busName = opcode.value;
|
||||||
|
break;
|
||||||
|
|
||||||
|
// note(jpc): gain opcodes are linear volumes in % units
|
||||||
|
|
||||||
|
case hash("directtomain"):
|
||||||
|
if (auto valueOpt = readOpcode<float>(opcode.value, { 0, 100 }))
|
||||||
|
getOrCreateBus(0).setGainToMain(*valueOpt / 100);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case hash("fx&tomain"): // fx&tomain
|
||||||
|
if (opcode.parameters.front() < 1 || opcode.parameters.front() > config::maxEffectBuses)
|
||||||
|
break;
|
||||||
|
if (auto valueOpt = readOpcode<float>(opcode.value, { 0, 100 }))
|
||||||
|
getOrCreateBus(opcode.parameters.front()).setGainToMain(*valueOpt / 100);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case hash("fx&tomix"): // fx&tomix
|
||||||
|
if (opcode.parameters.front() < 1 || opcode.parameters.front() > config::maxEffectBuses)
|
||||||
|
break;
|
||||||
|
if (auto valueOpt = readOpcode<float>(opcode.value, { 0, 100 }))
|
||||||
|
getOrCreateBus(opcode.parameters.front()).setGainToMix(*valueOpt / 100);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned busIndex;
|
||||||
|
if (busName.empty() || busName == "main")
|
||||||
|
busIndex = 0;
|
||||||
|
else if (busName.size() > 2 && busName.substr(0, 2) == "fx" && absl::SimpleAtoi(busName.substr(2), &busIndex) && busIndex >= 1 && busIndex <= config::maxEffectBuses) {
|
||||||
|
// an effect bus fxN, with N usually in [1,4]
|
||||||
|
} else {
|
||||||
|
DBG("Unsupported effect bus: " << busName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// create the effect and add it
|
||||||
|
EffectBus& bus = getOrCreateBus(busIndex);
|
||||||
|
auto fx = effectFactory.makeEffect(members);
|
||||||
|
fx->setSampleRate(sampleRate);
|
||||||
|
bus.addEffect(std::move(fx));
|
||||||
|
}
|
||||||
|
|
||||||
void addEndpointsToVelocityCurve(sfz::Region& region)
|
void addEndpointsToVelocityCurve(sfz::Region& region)
|
||||||
{
|
{
|
||||||
if (region.velocityPoints.size() > 0) {
|
if (region.velocityPoints.size() > 0) {
|
||||||
|
|
@ -385,8 +456,14 @@ void sfz::Synth::setSamplesPerBlock(int samplesPerBlock) noexcept
|
||||||
|
|
||||||
this->samplesPerBlock = samplesPerBlock;
|
this->samplesPerBlock = samplesPerBlock;
|
||||||
this->tempBuffer.resize(samplesPerBlock);
|
this->tempBuffer.resize(samplesPerBlock);
|
||||||
|
this->tempMixNodeBuffer.resize(samplesPerBlock);
|
||||||
for (auto& voice : voices)
|
for (auto& voice : voices)
|
||||||
voice->setSamplesPerBlock(samplesPerBlock);
|
voice->setSamplesPerBlock(samplesPerBlock);
|
||||||
|
|
||||||
|
for (auto& bus: effectBuses) {
|
||||||
|
if (bus)
|
||||||
|
bus->setSamplesPerBlock(samplesPerBlock);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void sfz::Synth::setSampleRate(float sampleRate) noexcept
|
void sfz::Synth::setSampleRate(float sampleRate) noexcept
|
||||||
|
|
@ -402,13 +479,17 @@ void sfz::Synth::setSampleRate(float sampleRate) noexcept
|
||||||
|
|
||||||
resources.filterPool.setSampleRate(sampleRate);
|
resources.filterPool.setSampleRate(sampleRate);
|
||||||
resources.eqPool.setSampleRate(sampleRate);
|
resources.eqPool.setSampleRate(sampleRate);
|
||||||
|
|
||||||
|
for (auto& bus: effectBuses) {
|
||||||
|
if (bus)
|
||||||
|
bus->setSampleRate(sampleRate);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void sfz::Synth::renderBlock(AudioSpan<float> buffer) noexcept
|
void sfz::Synth::renderBlock(AudioSpan<float> buffer) noexcept
|
||||||
{
|
{
|
||||||
ScopedFTZ ftz;
|
ScopedFTZ ftz;
|
||||||
|
|
||||||
|
|
||||||
if (freeWheeling)
|
if (freeWheeling)
|
||||||
resources.filePool.waitForBackgroundLoading();
|
resources.filePool.waitForBackgroundLoading();
|
||||||
|
|
||||||
|
|
@ -416,32 +497,77 @@ void sfz::Synth::renderBlock(AudioSpan<float> buffer) noexcept
|
||||||
if (!canEnterCallback)
|
if (!canEnterCallback)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
size_t numFrames = buffer.getNumFrames();
|
||||||
|
auto temp = AudioSpan<float>(tempBuffer).first(numFrames);
|
||||||
|
auto tempMixNode = AudioSpan<float>(tempMixNodeBuffer).first(numFrames);
|
||||||
|
|
||||||
CallbackBreakdown callbackBreakdown;
|
CallbackBreakdown callbackBreakdown;
|
||||||
|
|
||||||
|
{ // Prepare the effect inputs. They are mixes of per-region outputs.
|
||||||
|
ScopedTiming logger { callbackBreakdown.effects };
|
||||||
|
for (auto& bus: effectBuses) {
|
||||||
|
if (bus)
|
||||||
|
bus->clearInputs(numFrames);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
int numActiveVoices { 0 };
|
int numActiveVoices { 0 };
|
||||||
{ // Main render block
|
{ // Main render block
|
||||||
ScopedTiming logger { callbackBreakdown.renderMethod };
|
ScopedTiming logger { callbackBreakdown.renderMethod };
|
||||||
buffer.fill(0.0f);
|
buffer.fill(0.0f);
|
||||||
|
tempMixNode.fill(0.0f);
|
||||||
resources.filePool.cleanupPromises();
|
resources.filePool.cleanupPromises();
|
||||||
|
|
||||||
|
|
||||||
auto tempSpan = AudioSpan<float>(tempBuffer).first(buffer.getNumFrames());
|
|
||||||
for (auto& voice : voices) {
|
for (auto& voice : voices) {
|
||||||
if (!voice->isFree()) {
|
if (voice->isFree())
|
||||||
numActiveVoices++;
|
continue;
|
||||||
voice->renderBlock(tempSpan);
|
|
||||||
buffer.add(tempSpan);
|
|
||||||
callbackBreakdown.data += voice->getLastDataDuration();
|
|
||||||
callbackBreakdown.amplitude += voice->getLastAmplitudeDuration();
|
|
||||||
callbackBreakdown.filters += voice->getLastFilterDuration();
|
|
||||||
callbackBreakdown.panning += voice->getLastPanningDuration();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buffer.applyGain(db2mag(volume));
|
const Region* region = voice->getRegion();
|
||||||
|
|
||||||
|
numActiveVoices++;
|
||||||
|
voice->renderBlock(temp);
|
||||||
|
|
||||||
|
{ // 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
callbackBreakdown.data += voice->getLastDataDuration();
|
||||||
|
callbackBreakdown.amplitude += voice->getLastAmplitudeDuration();
|
||||||
|
callbackBreakdown.filters += voice->getLastFilterDuration();
|
||||||
|
callbackBreakdown.panning += voice->getLastPanningDuration();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{ // Apply effect buses
|
||||||
|
// -- note(jpc) there is always a "main" bus which is initially empty.
|
||||||
|
// without any <effect>, the signal is just going to flow through it.
|
||||||
|
ScopedTiming logger { callbackBreakdown.effects, ScopedTiming::Operation::addToDuration };
|
||||||
|
|
||||||
|
for (auto& bus: effectBuses) {
|
||||||
|
if (bus) {
|
||||||
|
bus->process(numFrames);
|
||||||
|
bus->mixOutputsTo(buffer, tempMixNode, numFrames);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the Mix output (fxNtomix opcodes)
|
||||||
|
// -- 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);
|
||||||
|
|
||||||
|
// Apply the master volume
|
||||||
|
buffer.applyGain(db2mag(volume));
|
||||||
|
|
||||||
callbackBreakdown.dispatch = dispatchDuration;
|
callbackBreakdown.dispatch = dispatchDuration;
|
||||||
resources.logger.logCallbackTime(std::move(callbackBreakdown), numActiveVoices, buffer.getNumFrames());
|
resources.logger.logCallbackTime(std::move(callbackBreakdown), numActiveVoices, numFrames);
|
||||||
|
|
||||||
// Reset the dispatch counter
|
// Reset the dispatch counter
|
||||||
dispatchDuration = Duration(0);
|
dispatchDuration = Duration(0);
|
||||||
|
|
@ -683,6 +809,11 @@ const sfz::Region* sfz::Synth::getRegionView(int idx) const noexcept
|
||||||
return (size_t)idx < regions.size() ? regions[idx].get() : nullptr;
|
return (size_t)idx < regions.size() ? regions[idx].get() : nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sfz::EffectBus* sfz::Synth::getEffectBusView(int idx) const noexcept
|
||||||
|
{
|
||||||
|
return (size_t)idx < effectBuses.size() ? effectBuses[idx].get() : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
const sfz::Voice* sfz::Synth::getVoiceView(int idx) const noexcept
|
const sfz::Voice* sfz::Synth::getVoiceView(int idx) const noexcept
|
||||||
{
|
{
|
||||||
return (size_t)idx < voices.size() ? voices[idx].get() : nullptr;
|
return (size_t)idx < voices.size() ? voices[idx].get() : nullptr;
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
#include "Parser.h"
|
#include "Parser.h"
|
||||||
#include "Voice.h"
|
#include "Voice.h"
|
||||||
#include "Region.h"
|
#include "Region.h"
|
||||||
|
#include "Effects.h"
|
||||||
#include "LeakDetector.h"
|
#include "LeakDetector.h"
|
||||||
#include "MidiState.h"
|
#include "MidiState.h"
|
||||||
#include "AudioSpan.h"
|
#include "AudioSpan.h"
|
||||||
|
|
@ -131,6 +132,14 @@ public:
|
||||||
* @return const Region*
|
* @return const Region*
|
||||||
*/
|
*/
|
||||||
const Voice* getVoiceView(int idx) const noexcept;
|
const Voice* getVoiceView(int idx) const noexcept;
|
||||||
|
/**
|
||||||
|
* @brief Get a raw view into a specific voice. This is mostly used
|
||||||
|
* for testing.
|
||||||
|
*
|
||||||
|
* @param idx
|
||||||
|
* @return const Region*
|
||||||
|
*/
|
||||||
|
const EffectBus* getEffectBusView(int idx) const noexcept;
|
||||||
/**
|
/**
|
||||||
* @brief Get a list of unknown opcodes. The lifetime of the
|
* @brief Get a list of unknown opcodes. The lifetime of the
|
||||||
* string views in the code are linked to the currently loaded
|
* string views in the code are linked to the currently loaded
|
||||||
|
|
@ -400,6 +409,12 @@ private:
|
||||||
* @param members the opcodes of the <control> block
|
* @param members the opcodes of the <control> block
|
||||||
*/
|
*/
|
||||||
void handleControlOpcodes(const std::vector<Opcode>& members);
|
void handleControlOpcodes(const std::vector<Opcode>& members);
|
||||||
|
/**
|
||||||
|
* @brief Helper function to dispatch <effect> opcodes
|
||||||
|
*
|
||||||
|
* @param members the opcodes of the <effect> block
|
||||||
|
*/
|
||||||
|
void handleEffectOpcodes(const std::vector<Opcode>& members);
|
||||||
/**
|
/**
|
||||||
* @brief Helper function to merge all the currently active opcodes
|
* @brief Helper function to merge all the currently active opcodes
|
||||||
* as set by the successive callbacks and create a new region to store
|
* as set by the successive callbacks and create a new region to store
|
||||||
|
|
@ -441,8 +456,14 @@ private:
|
||||||
std::array<RegionPtrVector, 128> noteActivationLists;
|
std::array<RegionPtrVector, 128> noteActivationLists;
|
||||||
std::array<RegionPtrVector, config::numCCs> ccActivationLists;
|
std::array<RegionPtrVector, config::numCCs> ccActivationLists;
|
||||||
|
|
||||||
// Internal temporary buffer
|
// Effect factory and buses
|
||||||
|
EffectFactory effectFactory;
|
||||||
|
typedef std::unique_ptr<EffectBus> EffectBusPtr;
|
||||||
|
std::vector<EffectBusPtr> effectBuses; // 0 is "main", 1-N are "fx1"-"fxN"
|
||||||
|
|
||||||
|
// Intermediate buffers
|
||||||
AudioBuffer<float> tempBuffer { 2, config::defaultSamplesPerBlock };
|
AudioBuffer<float> tempBuffer { 2, config::defaultSamplesPerBlock };
|
||||||
|
AudioBuffer<float> tempMixNodeBuffer { 2, config::defaultSamplesPerBlock };
|
||||||
|
|
||||||
int samplesPerBlock { config::defaultSamplesPerBlock };
|
int samplesPerBlock { config::defaultSamplesPerBlock };
|
||||||
float sampleRate { config::defaultSampleRate };
|
float sampleRate { config::defaultSampleRate };
|
||||||
|
|
|
||||||
212
src/sfizz/effects/Lofi.cpp
Normal file
212
src/sfizz/effects/Lofi.cpp
Normal file
|
|
@ -0,0 +1,212 @@
|
||||||
|
// 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
|
||||||
|
|
||||||
|
/**
|
||||||
|
Note(jpc): implementation status
|
||||||
|
|
||||||
|
- [x] bitred
|
||||||
|
- [ ] bitred_oncc
|
||||||
|
- [ ] bitred_smoothcc
|
||||||
|
- [ ] bitred_stepcc
|
||||||
|
- [ ] bitred_curvecc
|
||||||
|
|
||||||
|
- [x] decim
|
||||||
|
- [ ] decim_oncc
|
||||||
|
- [ ] decim_smoothcc
|
||||||
|
- [ ] decim_stepcc
|
||||||
|
- [ ] decim_curvecc
|
||||||
|
|
||||||
|
- [ ] egN_bitred
|
||||||
|
- [ ] egN_bitred_oncc
|
||||||
|
- [ ] lfoN_bitred
|
||||||
|
- [ ] lfoN_bitred_oncc
|
||||||
|
- [ ] lfoN_bitred_smoothcc
|
||||||
|
- [ ] lfoN_bitred_stepcc
|
||||||
|
|
||||||
|
- [ ] egN_decim
|
||||||
|
- [ ] egN_decim_oncc
|
||||||
|
- [ ] lfoN_decim
|
||||||
|
- [ ] lfoN_decim_oncc
|
||||||
|
- [ ] lfoN_decim_smoothcc
|
||||||
|
- [ ] lfoN_decim_stepcc
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "Lofi.h"
|
||||||
|
#include "Opcode.h"
|
||||||
|
#include <memory>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstring>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
namespace sfz {
|
||||||
|
namespace fx {
|
||||||
|
|
||||||
|
void Lofi::setSampleRate(double sampleRate)
|
||||||
|
{
|
||||||
|
for (unsigned c = 0; c < EffectChannels; ++c) {
|
||||||
|
_bitred[c].init(sampleRate);
|
||||||
|
_decim[c].init(sampleRate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Lofi::setSamplesPerBlock(int samplesPerBlock)
|
||||||
|
{
|
||||||
|
(void)samplesPerBlock;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Lofi::clear()
|
||||||
|
{
|
||||||
|
for (unsigned c = 0; c < EffectChannels; ++c) {
|
||||||
|
_bitred[c].clear();
|
||||||
|
_decim[c].clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Lofi::process(const float* const inputs[2], float* const outputs[2], unsigned nframes)
|
||||||
|
{
|
||||||
|
for (unsigned c = 0; c < EffectChannels; ++c) {
|
||||||
|
_bitred[c].setDepth(_bitred_depth);
|
||||||
|
_bitred[c].process(inputs[c], outputs[c], nframes);
|
||||||
|
|
||||||
|
_decim[c].setDepth(_decim_depth);
|
||||||
|
_decim[c].process(outputs[c], outputs[c], nframes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<Effect> Lofi::makeInstance(absl::Span<const Opcode> members)
|
||||||
|
{
|
||||||
|
auto fx = std::make_unique<Lofi>();
|
||||||
|
|
||||||
|
for (const Opcode& opcode : members) {
|
||||||
|
switch (opcode.lettersOnlyHash) {
|
||||||
|
case hash("bitred"):
|
||||||
|
setValueFromOpcode(opcode, fx->_bitred_depth, { 0.0, 100.0 });
|
||||||
|
break;
|
||||||
|
case hash("decim"):
|
||||||
|
setValueFromOpcode(opcode, fx->_decim_depth, { 0.0, 100.0 });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fx;
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
void Lofi::Bitred::init(double sampleRate)
|
||||||
|
{
|
||||||
|
(void)sampleRate;
|
||||||
|
|
||||||
|
static constexpr double coefs2x[12] = { 0.036681502163648017, 0.13654762463195794, 0.27463175937945444, 0.42313861743656711, 0.56109869787919531, 0.67754004997416184, 0.76974183386322703, 0.83988962484963892, 0.89226081800387902, 0.9315419599631839, 0.96209454837808417, 0.98781637073289585 };
|
||||||
|
fDownsampler2x.set_coefs(coefs2x);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Lofi::Bitred::clear()
|
||||||
|
{
|
||||||
|
fLastValue = 0.0;
|
||||||
|
fDownsampler2x.clear_buffers();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Lofi::Bitred::setDepth(float depth)
|
||||||
|
{
|
||||||
|
fDepth = clamp(depth, 0.0f, 100.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Lofi::Bitred::process(const float* in, float* out, uint32_t nframes)
|
||||||
|
{
|
||||||
|
if (fDepth == 0) {
|
||||||
|
if (in != out)
|
||||||
|
std::memcpy(out, in, nframes * sizeof(float));
|
||||||
|
clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
float lastValue = fLastValue;
|
||||||
|
|
||||||
|
const float steps = (1.0f + (100.0f - fDepth)) * 0.75f;
|
||||||
|
const float invSteps = 1.0f / steps;
|
||||||
|
|
||||||
|
for (uint32_t i = 0; i < nframes; ++i) {
|
||||||
|
float x = in[i];
|
||||||
|
|
||||||
|
float y = std::copysign((int)(0.5f + std::fabs(x * steps)), x) * invSteps;
|
||||||
|
|
||||||
|
float y2x[2];
|
||||||
|
y2x[0] = (y != lastValue) ? (0.5f * (y + lastValue)) : y;
|
||||||
|
y2x[1] = y;
|
||||||
|
|
||||||
|
lastValue = y;
|
||||||
|
|
||||||
|
y = fDownsampler2x.process_sample(y2x);
|
||||||
|
out[i] = y;
|
||||||
|
}
|
||||||
|
|
||||||
|
fLastValue = lastValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
void Lofi::Decim::init(double sampleRate)
|
||||||
|
{
|
||||||
|
fSampleTime = 1.0 / sampleRate;
|
||||||
|
|
||||||
|
static constexpr double coefs2x[12] = { 0.036681502163648017, 0.13654762463195794, 0.27463175937945444, 0.42313861743656711, 0.56109869787919531, 0.67754004997416184, 0.76974183386322703, 0.83988962484963892, 0.89226081800387902, 0.9315419599631839, 0.96209454837808417, 0.98781637073289585 };
|
||||||
|
fDownsampler2x.set_coefs(coefs2x);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Lofi::Decim::clear()
|
||||||
|
{
|
||||||
|
fPhase = 0.0;
|
||||||
|
fLastValue = 0.0;
|
||||||
|
fDownsampler2x.clear_buffers();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Lofi::Decim::setDepth(float depth)
|
||||||
|
{
|
||||||
|
fDepth = clamp(depth, 0.0f, 100.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Lofi::Decim::process(const float* in, float* out, uint32_t nframes)
|
||||||
|
{
|
||||||
|
if (fDepth == 0) {
|
||||||
|
if (in != out)
|
||||||
|
std::memcpy(out, in, nframes * sizeof(float));
|
||||||
|
clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float dt = [this]() {
|
||||||
|
// exponential curve fit
|
||||||
|
const float a = 1.289079e+00, b = 1.384141e-01, c = 1.313298e-04;
|
||||||
|
const float denom = std::pow(a, b * fDepth) * c - c;
|
||||||
|
return fSampleTime / denom;
|
||||||
|
}();
|
||||||
|
|
||||||
|
float phase = fPhase;
|
||||||
|
float lastValue = fLastValue;
|
||||||
|
|
||||||
|
for (uint32_t i = 0; i < nframes; ++i) {
|
||||||
|
float x = in[i];
|
||||||
|
|
||||||
|
phase += dt;
|
||||||
|
float y = (phase > 1.0f) ? x : lastValue;
|
||||||
|
phase -= static_cast<int>(phase);
|
||||||
|
|
||||||
|
float y2x[2];
|
||||||
|
y2x[0] = (y != lastValue) ? (0.5f * (y + lastValue)) : y;
|
||||||
|
y2x[1] = y;
|
||||||
|
|
||||||
|
lastValue = y;
|
||||||
|
|
||||||
|
y = fDownsampler2x.process_sample(y2x);
|
||||||
|
out[i] = y;
|
||||||
|
}
|
||||||
|
|
||||||
|
fPhase = phase;
|
||||||
|
fLastValue = lastValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace fx
|
||||||
|
} // namespace sfz
|
||||||
85
src/sfizz/effects/Lofi.h
Normal file
85
src/sfizz/effects/Lofi.h
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
// 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 "Effects.h"
|
||||||
|
#include "hiir/Downsampler2xFpu.h"
|
||||||
|
|
||||||
|
namespace sfz {
|
||||||
|
namespace fx {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Bit crushing effect
|
||||||
|
*/
|
||||||
|
class Lofi : public Effect {
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* @brief Initializes with the given sample rate.
|
||||||
|
*/
|
||||||
|
void setSampleRate(double sampleRate) override;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Sets the maximum number of frames to render at a time. The actual
|
||||||
|
* value can be lower but should never be higher.
|
||||||
|
*/
|
||||||
|
void setSamplesPerBlock(int samplesPerBlock) override;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Reset the state to initial.
|
||||||
|
*/
|
||||||
|
void clear() override;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Computes a cycle of the effect in stereo.
|
||||||
|
*/
|
||||||
|
void process(const float* const inputs[], float* const outputs[], unsigned nframes) override;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Instantiates given the contents of the <effect> block.
|
||||||
|
*/
|
||||||
|
static std::unique_ptr<Effect> makeInstance(absl::Span<const Opcode> members);
|
||||||
|
|
||||||
|
private:
|
||||||
|
float _bitred_depth = 0;
|
||||||
|
float _decim_depth = 0;
|
||||||
|
|
||||||
|
///
|
||||||
|
class Bitred {
|
||||||
|
public:
|
||||||
|
void init(double sampleRate);
|
||||||
|
void clear();
|
||||||
|
void setDepth(float depth);
|
||||||
|
void process(const float* in, float* out, uint32_t nframes);
|
||||||
|
|
||||||
|
private:
|
||||||
|
float fDepth = 0.0;
|
||||||
|
float fLastValue = 0.0;
|
||||||
|
hiir::Downsampler2xFpu<12> fDownsampler2x;
|
||||||
|
};
|
||||||
|
|
||||||
|
///
|
||||||
|
class Decim {
|
||||||
|
public:
|
||||||
|
void init(double sampleRate);
|
||||||
|
void clear();
|
||||||
|
void setDepth(float depth);
|
||||||
|
void process(const float* in, float* out, uint32_t nframes);
|
||||||
|
|
||||||
|
private:
|
||||||
|
float fSampleTime = 0.0;
|
||||||
|
float fDepth = 0.0;
|
||||||
|
float fPhase = 0.0;
|
||||||
|
float fLastValue = 0.0;
|
||||||
|
hiir::Downsampler2xFpu<12> fDownsampler2x;
|
||||||
|
};
|
||||||
|
|
||||||
|
///
|
||||||
|
Bitred _bitred[EffectChannels];
|
||||||
|
Decim _decim[EffectChannels];
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace fx
|
||||||
|
} // namespace sfz
|
||||||
36
src/sfizz/effects/Nothing.cpp
Normal file
36
src/sfizz/effects/Nothing.cpp
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
// 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 "Nothing.h"
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
namespace sfz {
|
||||||
|
namespace fx {
|
||||||
|
|
||||||
|
void Nothing::setSampleRate(double sampleRate)
|
||||||
|
{
|
||||||
|
(void)sampleRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Nothing::setSamplesPerBlock(int samplesPerBlock)
|
||||||
|
{
|
||||||
|
(void)samplesPerBlock;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Nothing::clear()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void Nothing::process(const float* const inputs[], float* const outputs[], unsigned nframes)
|
||||||
|
{
|
||||||
|
for (unsigned c = 0; c < EffectChannels; ++c) {
|
||||||
|
if (inputs[c] != outputs[c])
|
||||||
|
std::memcpy(outputs[c], inputs[c], nframes * sizeof(float));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace fx
|
||||||
|
} // namespace sfz
|
||||||
41
src/sfizz/effects/Nothing.h
Normal file
41
src/sfizz/effects/Nothing.h
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
// 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 "Effects.h"
|
||||||
|
|
||||||
|
namespace sfz {
|
||||||
|
namespace fx {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Effect which does nothing
|
||||||
|
*/
|
||||||
|
class Nothing : public Effect {
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* @brief Initializes with the given sample rate.
|
||||||
|
*/
|
||||||
|
void setSampleRate(double sampleRate) override;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Sets the maximum number of frames to render at a time. The actual
|
||||||
|
* value can be lower but should never be higher.
|
||||||
|
*/
|
||||||
|
void setSamplesPerBlock(int samplesPerBlock) override;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Reset the state to initial.
|
||||||
|
*/
|
||||||
|
void clear() override;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Copy the input signal to the output
|
||||||
|
*/
|
||||||
|
void process(const float* const inputs[], float* const outputs[], unsigned nframes) override;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace fx
|
||||||
|
} // namespace sfz
|
||||||
|
|
@ -1350,6 +1350,23 @@ TEST_CASE("[Region] Parsing opcodes")
|
||||||
region.parseOpcode({ "eq1_freqcc15", "50000" });
|
region.parseOpcode({ "eq1_freqcc15", "50000" });
|
||||||
REQUIRE(region.equalizers[0].frequencyCC[15] == 30000.0f);
|
REQUIRE(region.equalizers[0].frequencyCC[15] == 30000.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SECTION("Effects send")
|
||||||
|
{
|
||||||
|
REQUIRE(region.gainToEffect.size() == 1);
|
||||||
|
REQUIRE(region.gainToEffect[0] == 1.0f);
|
||||||
|
region.parseOpcode({ "effect1", "50.4" });
|
||||||
|
REQUIRE(region.gainToEffect.size() == 2);
|
||||||
|
REQUIRE(region.gainToEffect[1] == 0.504f);
|
||||||
|
region.parseOpcode({ "effect3", "100" });
|
||||||
|
REQUIRE(region.gainToEffect.size() == 4);
|
||||||
|
REQUIRE(region.gainToEffect[2] == 0.0f);
|
||||||
|
REQUIRE(region.gainToEffect[3] == 1.0f);
|
||||||
|
region.parseOpcode({ "effect3", "150.1" });
|
||||||
|
REQUIRE(region.gainToEffect[3] == 1.0f);
|
||||||
|
region.parseOpcode({ "effect3", "-50.65" });
|
||||||
|
REQUIRE(region.gainToEffect[3] == 0.0f);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Specific region bugs
|
// Specific region bugs
|
||||||
|
|
|
||||||
115
tests/SynthT.cpp
115
tests/SynthT.cpp
|
|
@ -13,7 +13,7 @@ constexpr int blockSize { 256 };
|
||||||
TEST_CASE("[Synth] Play and check active voices")
|
TEST_CASE("[Synth] Play and check active voices")
|
||||||
{
|
{
|
||||||
sfz::Synth synth;
|
sfz::Synth synth;
|
||||||
synth.setSamplesPerBlock(256);
|
synth.setSamplesPerBlock(blockSize);
|
||||||
sfz::AudioBuffer<float> buffer { 2, blockSize };
|
sfz::AudioBuffer<float> buffer { 2, blockSize };
|
||||||
synth.loadSfzFile(fs::current_path() / "tests/TestFiles/groups_avl.sfz");
|
synth.loadSfzFile(fs::current_path() / "tests/TestFiles/groups_avl.sfz");
|
||||||
|
|
||||||
|
|
@ -29,7 +29,7 @@ TEST_CASE("[Synth] Play and check active voices")
|
||||||
TEST_CASE("[Synth] Change the number of voice while playing")
|
TEST_CASE("[Synth] Change the number of voice while playing")
|
||||||
{
|
{
|
||||||
sfz::Synth synth;
|
sfz::Synth synth;
|
||||||
synth.setSamplesPerBlock(256);
|
synth.setSamplesPerBlock(blockSize);
|
||||||
sfz::AudioBuffer<float> buffer { 2, blockSize };
|
sfz::AudioBuffer<float> buffer { 2, blockSize };
|
||||||
synth.loadSfzFile(fs::current_path() / "tests/TestFiles/groups_avl.sfz");
|
synth.loadSfzFile(fs::current_path() / "tests/TestFiles/groups_avl.sfz");
|
||||||
|
|
||||||
|
|
@ -77,6 +77,7 @@ TEST_CASE("[Synth] Check that we can change the size of the preload before and a
|
||||||
{
|
{
|
||||||
sfz::Synth synth;
|
sfz::Synth synth;
|
||||||
synth.setPreloadSize(512);
|
synth.setPreloadSize(512);
|
||||||
|
synth.setSamplesPerBlock(blockSize);
|
||||||
sfz::AudioBuffer<float> buffer { 2, blockSize };
|
sfz::AudioBuffer<float> buffer { 2, blockSize };
|
||||||
synth.loadSfzFile(fs::current_path() / "tests/TestFiles/groups_avl.sfz");
|
synth.loadSfzFile(fs::current_path() / "tests/TestFiles/groups_avl.sfz");
|
||||||
synth.setPreloadSize(1024);
|
synth.setPreloadSize(1024);
|
||||||
|
|
@ -92,6 +93,7 @@ TEST_CASE("[Synth] Check that we can change the oversampling factor before and a
|
||||||
{
|
{
|
||||||
sfz::Synth synth;
|
sfz::Synth synth;
|
||||||
synth.setOversamplingFactor(sfz::Oversampling::x2);
|
synth.setOversamplingFactor(sfz::Oversampling::x2);
|
||||||
|
synth.setSamplesPerBlock(blockSize);
|
||||||
sfz::AudioBuffer<float> buffer { 2, blockSize };
|
sfz::AudioBuffer<float> buffer { 2, blockSize };
|
||||||
synth.loadSfzFile(fs::current_path() / "tests/TestFiles/groups_avl.sfz");
|
synth.loadSfzFile(fs::current_path() / "tests/TestFiles/groups_avl.sfz");
|
||||||
synth.setOversamplingFactor(sfz::Oversampling::x4);
|
synth.setOversamplingFactor(sfz::Oversampling::x4);
|
||||||
|
|
@ -208,3 +210,112 @@ TEST_CASE("[Synth] Trigger=release_key and an envelope properly kills the voice
|
||||||
synth.renderBlock(buffer);
|
synth.renderBlock(buffer);
|
||||||
REQUIRE( synth.getVoiceView(0)->isFree() );
|
REQUIRE( synth.getVoiceView(0)->isFree() );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("[Synth] Number of effect buses and resetting behavior")
|
||||||
|
{
|
||||||
|
sfz::Synth synth;
|
||||||
|
synth.setSamplesPerBlock(blockSize);
|
||||||
|
sfz::AudioBuffer<float> buffer { 2, blockSize };
|
||||||
|
|
||||||
|
REQUIRE( synth.getEffectBusView(0) == nullptr); // No effects at first
|
||||||
|
synth.loadSfzFile(fs::current_path() / "tests/TestFiles/Effects/base.sfz");
|
||||||
|
REQUIRE( synth.getEffectBusView(0) != nullptr); // We have a main bus
|
||||||
|
// Check that we can render blocks
|
||||||
|
for (int i = 0; i < 100; ++i)
|
||||||
|
synth.renderBlock(buffer);
|
||||||
|
|
||||||
|
synth.loadSfzFile(fs::current_path() / "tests/TestFiles/Effects/bitcrusher_2.sfz");
|
||||||
|
REQUIRE( synth.getEffectBusView(0) != nullptr); // We have a main bus
|
||||||
|
REQUIRE( synth.getEffectBusView(1) != nullptr); // and an FX bus
|
||||||
|
// Check that we can render blocks
|
||||||
|
for (int i = 0; i < 100; ++i)
|
||||||
|
synth.renderBlock(buffer);
|
||||||
|
|
||||||
|
synth.loadSfzFile(fs::current_path() / "tests/TestFiles/Effects/base.sfz");
|
||||||
|
REQUIRE( synth.getEffectBusView(0) != nullptr); // We have a main bus
|
||||||
|
REQUIRE( synth.getEffectBusView(1) == nullptr); // and no FX bus
|
||||||
|
// Check that we can render blocks
|
||||||
|
for (int i = 0; i < 100; ++i)
|
||||||
|
synth.renderBlock(buffer);
|
||||||
|
|
||||||
|
synth.loadSfzFile(fs::current_path() / "tests/TestFiles/Effects/bitcrusher_3.sfz");
|
||||||
|
REQUIRE( synth.getEffectBusView(0) != nullptr); // We have a main bus
|
||||||
|
REQUIRE( synth.getEffectBusView(1) == nullptr); // empty/uninitialized fx bus
|
||||||
|
REQUIRE( synth.getEffectBusView(2) == nullptr); // empty/uninitialized fx bus
|
||||||
|
REQUIRE( synth.getEffectBusView(3) != nullptr); // and an FX bus (because we built up to fx3)
|
||||||
|
REQUIRE( synth.getEffectBusView(3)->numEffects() == 1);
|
||||||
|
// Check that we can render blocks
|
||||||
|
for (int i = 0; i < 100; ++i)
|
||||||
|
synth.renderBlock(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("[Synth] No effect in the main bus")
|
||||||
|
{
|
||||||
|
sfz::Synth synth;
|
||||||
|
synth.loadSfzFile(fs::current_path() / "tests/TestFiles/Effects/base.sfz");
|
||||||
|
auto bus = synth.getEffectBusView(0);
|
||||||
|
REQUIRE( bus != nullptr); // We have a main bus
|
||||||
|
REQUIRE( bus->numEffects() == 0 );
|
||||||
|
REQUIRE( bus->gainToMain() == 1 );
|
||||||
|
REQUIRE( bus->gainToMix() == 0 );
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("[Synth] One effect")
|
||||||
|
{
|
||||||
|
sfz::Synth synth;
|
||||||
|
synth.loadSfzFile(fs::current_path() / "tests/TestFiles/Effects/bitcrusher_1.sfz");
|
||||||
|
auto bus = synth.getEffectBusView(0);
|
||||||
|
REQUIRE( bus != nullptr); // We have a main bus
|
||||||
|
REQUIRE( bus->numEffects() == 1 );
|
||||||
|
REQUIRE( bus->gainToMain() == 1 );
|
||||||
|
REQUIRE( bus->gainToMix() == 0 );
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("[Synth] Effect on a second bus")
|
||||||
|
{
|
||||||
|
sfz::Synth synth;
|
||||||
|
synth.loadSfzFile(fs::current_path() / "tests/TestFiles/Effects/bitcrusher_2.sfz");
|
||||||
|
auto bus = synth.getEffectBusView(0);
|
||||||
|
REQUIRE( bus != nullptr); // We have a main bus
|
||||||
|
REQUIRE( bus->numEffects() == 0 );
|
||||||
|
REQUIRE( bus->gainToMain() == 0.5 );
|
||||||
|
REQUIRE( bus->gainToMix() == 0 );
|
||||||
|
bus = synth.getEffectBusView(1);
|
||||||
|
REQUIRE( bus != nullptr);
|
||||||
|
REQUIRE( bus->numEffects() == 1 );
|
||||||
|
REQUIRE( bus->gainToMain() == 0.5 );
|
||||||
|
REQUIRE( bus->gainToMix() == 0 );
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TEST_CASE("[Synth] Effect on a third bus")
|
||||||
|
{
|
||||||
|
sfz::Synth synth;
|
||||||
|
synth.loadSfzFile(fs::current_path() / "tests/TestFiles/Effects/bitcrusher_3.sfz");
|
||||||
|
auto bus = synth.getEffectBusView(0);
|
||||||
|
REQUIRE( bus != nullptr); // We have a main bus
|
||||||
|
REQUIRE( bus->numEffects() == 0 );
|
||||||
|
REQUIRE( bus->gainToMain() == 0.5 );
|
||||||
|
REQUIRE( bus->gainToMix() == 0 );
|
||||||
|
bus = synth.getEffectBusView(3);
|
||||||
|
REQUIRE( bus != nullptr);
|
||||||
|
REQUIRE( bus->numEffects() == 1 );
|
||||||
|
REQUIRE( bus->gainToMain() == 0.5 );
|
||||||
|
REQUIRE( bus->gainToMix() == 0 );
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("[Synth] Gain to mix")
|
||||||
|
{
|
||||||
|
sfz::Synth synth;
|
||||||
|
synth.loadSfzFile(fs::current_path() / "tests/TestFiles/Effects/to_mix.sfz");
|
||||||
|
auto bus = synth.getEffectBusView(0);
|
||||||
|
REQUIRE( bus != nullptr); // We have a main bus
|
||||||
|
REQUIRE( bus->numEffects() == 0 );
|
||||||
|
REQUIRE( bus->gainToMain() == 1 );
|
||||||
|
REQUIRE( bus->gainToMix() == 0 );
|
||||||
|
bus = synth.getEffectBusView(1);
|
||||||
|
REQUIRE( bus != nullptr);
|
||||||
|
REQUIRE( bus->numEffects() == 1 );
|
||||||
|
REQUIRE( bus->gainToMain() == 0 );
|
||||||
|
REQUIRE( bus->gainToMix() == 0.5 );
|
||||||
|
}
|
||||||
|
|
|
||||||
4
tests/TestFiles/Effects/base.sfz
Normal file
4
tests/TestFiles/Effects/base.sfz
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
<region>
|
||||||
|
lokey=0
|
||||||
|
hikey=127
|
||||||
|
sample=*sine
|
||||||
9
tests/TestFiles/Effects/bitcrusher_1.sfz
Normal file
9
tests/TestFiles/Effects/bitcrusher_1.sfz
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
<region>
|
||||||
|
lokey=0
|
||||||
|
hikey=127
|
||||||
|
sample=*sine
|
||||||
|
|
||||||
|
<effect>
|
||||||
|
type=lofi
|
||||||
|
bitred=90
|
||||||
|
decim=10
|
||||||
13
tests/TestFiles/Effects/bitcrusher_2.sfz
Normal file
13
tests/TestFiles/Effects/bitcrusher_2.sfz
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
<region>
|
||||||
|
lokey=0
|
||||||
|
hikey=127
|
||||||
|
sample=*sine
|
||||||
|
effect1=100
|
||||||
|
|
||||||
|
<effect>
|
||||||
|
directtomain=50
|
||||||
|
fx1tomain=50
|
||||||
|
type=lofi
|
||||||
|
bus=fx1
|
||||||
|
bitred=90
|
||||||
|
decim=10
|
||||||
13
tests/TestFiles/Effects/bitcrusher_3.sfz
Normal file
13
tests/TestFiles/Effects/bitcrusher_3.sfz
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
<region>
|
||||||
|
lokey=0
|
||||||
|
hikey=127
|
||||||
|
sample=*sine
|
||||||
|
effect1=100
|
||||||
|
|
||||||
|
<effect>
|
||||||
|
directtomain=50
|
||||||
|
fx3tomain=50
|
||||||
|
type=lofi
|
||||||
|
bus=fx3
|
||||||
|
bitred=90
|
||||||
|
decim=10
|
||||||
12
tests/TestFiles/Effects/to_mix.sfz
Normal file
12
tests/TestFiles/Effects/to_mix.sfz
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
<region>
|
||||||
|
lokey=0
|
||||||
|
hikey=127
|
||||||
|
sample=*sine
|
||||||
|
effect1=100
|
||||||
|
|
||||||
|
<effect>
|
||||||
|
fx1tomix=50
|
||||||
|
bus=fx1
|
||||||
|
type=lofi
|
||||||
|
bitred=90
|
||||||
|
decim=10
|
||||||
Loading…
Add table
Reference in a new issue