zynaddsubfx

ZynAddSubFX open source synthesizer
Log | Files | Refs | Submodules | LICENSE

commit 613658446bf6744a64b4436a744bc4c25af56a3c
parent d3b3abd08c42333a95bbaa44214336fe0b7a2437
Author: friedolino78 <[email protected]>
Date:   Sun, 16 Feb 2025 00:50:26 +0100

Add reversed delay effect (#321)


Diffstat:
Mdoc/effects.txt | 40++++++++++++++++++++++++++++++++++++----
Msrc/DSP/CMakeLists.txt | 1+
Asrc/DSP/Reverter.cpp | 296+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/DSP/Reverter.h | 286+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/Effects/Alienwah.cpp | 2+-
Msrc/Effects/CMakeLists.txt | 1+
Msrc/Effects/Chorus.cpp | 2+-
Msrc/Effects/CombFilterBank.cpp | 6+++---
Msrc/Effects/DynamicFilter.cpp | 2+-
Msrc/Effects/Effect.cpp | 5+++--
Msrc/Effects/Effect.h | 12++++++++++--
Msrc/Effects/EffectLFO.cpp | 7++++---
Msrc/Effects/EffectLFO.h | 77+++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
Msrc/Effects/EffectMgr.cpp | 69+++++++++++++++++++++++++++++++++++++++++----------------------------
Msrc/Effects/EffectMgr.h | 4+++-
Msrc/Effects/Phaser.cpp | 2+-
Asrc/Effects/Reverse.cpp | 305++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/Effects/Reverse.h | 101+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/Misc/Allocator.cpp | 2+-
Msrc/Misc/Master.cpp | 63+++++++++++++++++++++++++++++++++++++++++++++++++--------------
Msrc/Misc/Master.h | 14++++++++++++--
Msrc/Misc/MiddleWare.cpp | 7+++++--
Msrc/Misc/Part.cpp | 5+++--
Msrc/Misc/Part.h | 3++-
Asrc/Misc/Sync.h | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/Misc/Time.h | 21+++++++++++++++++++--
Msrc/Nio/AlsaEngine.cpp | 14+++++++++++---
Msrc/Plugin/ZynAddSubFX/DistrhoPluginInfo.h | 1+
Msrc/Plugin/ZynAddSubFX/ZynAddSubFX.cpp | 34+++++++++++++++++++++++++++-------
Msrc/Tests/AllocatorTest.cpp | 6+++---
Msrc/Tests/CMakeLists.txt | 1+
Msrc/Tests/InstrumentStats.cpp | 5++++-
Msrc/Tests/KitTest.cpp | 5++++-
Msrc/Tests/PortChecker.cpp | 1+
Asrc/Tests/ReverseTest.cpp | 139+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/globals.h | 8++++++++
36 files changed, 1500 insertions(+), 106 deletions(-)

diff --git a/doc/effects.txt b/doc/effects.txt @@ -551,13 +551,45 @@ strings of a piano. Description ^^^^^^^^^^^ -** *Drive* how much input signal id fed to the strings +** *Drive* how much input signal is fed to the strings ** *Level* Output Volume ** *Str* Number of strings to use. 12 is a full Octave. On small systems -be carefull with cpu load -** *Q* How strong is the Reflection -** *Uni* Unisono - how many strings should have the 'same' frequency it' used with the +be careful with cpu load +** *Q* How strong is the reflection +** *Uni* Unisono - how many strings should have the 'same' frequency. It's used with the next paremeter. ** *Detune* Amount of detune between the unisono strings ** *Base* midi note of the lowest string +Reverse +~~~~~~~~ + +Introduction +^^^^^^^^^^^^ + +The reversed effect plays the audio signal backwards. To prevent a time paradoxon +the input signal is chopped in segments that are reversed one by one. +One classic usage is the reversing of a decaying sound for example from reverb. + +Function +^^^^^^^^ +With a melotron it was possible to reverse each key by turning the magentic tape around. +While exactly this is not possible with this effect, because there is no prerecorded +sound when hitting a key, this effect can create sounds that resemble those of the melotron. + + +Description +^^^^^^^^^^^ + +** *Length* Length of the segments being played backwards. +** *Numerator&Denominator* Alternatively the length can be set relative +to the global Tempo (bpm) as note value. For example 1/4 at 120bpm is 0.5s +** *Phase* adjust the time when the reversed segments start relative to a +global time reference. It's useful only in combination with another +timed element like an LFO or delay. +** *Sync* choose how the recording and reverse playing phase is synchronized +* *Auto* the effect switches automatically between recording and reverse playing and vice versa after a segment (length) +* *MIDI* like auto but speed is taken from MIDI timecode +* *Host* like auto but speed is taken from plugin host (when running as plugin mode) +* *NoteOn* recording starts at noteOn. Reverse playing starts after a segment (length) +* *NoteOnOff* recording starts at noteon. Reverse playing starts when the decaying note reaches silence diff --git a/src/DSP/CMakeLists.txt b/src/DSP/CMakeLists.txt @@ -6,6 +6,7 @@ set(zynaddsubfx_dsp_SRCS DSP/SVFilter.cpp DSP/MoogFilter.cpp DSP/CombFilter.cpp + DSP/Reverter.cpp DSP/Unison.cpp DSP/Value_Smoothing_Filter.cpp PARENT_SCOPE diff --git a/src/DSP/Reverter.cpp b/src/DSP/Reverter.cpp @@ -0,0 +1,296 @@ +/* + ZynAddSubFX - a software synthesizer + + Reverter.cpp - Reverse Delay + Copyright (C) 2023-2024 Michael Kirchner + Author: Michael Kirchner + + This program is free software; you can redistribute it and/or + modify it under the teabs_value of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. +*/ + +#include <cassert> +#include <cstdio> +#include <cmath> +#include <cstring> + +#include "../Misc/Allocator.h" +#include "../Misc/Time.h" +#include "Reverter.h" + +namespace zyn { + +Reverter::Reverter(Allocator *alloc, float delay_, unsigned int srate, int bufsize, float tRef_, const AbsTime *time_) + : syncMode(NOTEON), input(nullptr), gain(1.0f), delay(0.0f), phase(0.0f), crossfade(0.16f), + tRef(tRef_), buffer_offset(0), buffer_counter(0), reverse_index(0.0f), phase_offset_old(0.0f), + phase_offset_fade(0.0f), fade_counter(0), mean_abs_value(999.9f), time(time_), memory(*alloc), + samplerate(srate), buffersize(bufsize), max_delay_samples(srate * MAX_REV_DELAY_SECONDS) +{ + setdelay(delay_); + pos_writer = 0; + pos_reader = 0; + pos_start = 0; + reverse_index = 0; + state = PLAYING; +} + +Reverter::~Reverter() { + memory.dealloc(input); +} + +inline float Reverter::sampleLerp(const float *smp,const float pos) { + int poshi = static_cast<int>(pos); + float poslo = pos - static_cast<float>(poshi); + return smp[poshi] + poslo * (smp[poshi + 1] - smp[poshi]); +} + +inline void Reverter::switchBuffers() { + reverse_index = 0; + phase_offset = phase * delay; + // Reset the reverse index to start fresh for the new reverse playback segment. + pos_start = pos_writer; + + float pos_next = fmodf(float(pos_start + mem_size) - (reverse_index + phase_offset), mem_size); + // Determine the position of the next sample (`pos_next`) after switching buffers: + // - Accounts for the current `reverse_index` (reset to 0 above) and phase offset. + // - Uses modulo (`fmodf`) to ensure the position wraps correctly within the circular buffer. + // The formula is the same as for pos_reader in updateReaderPosition() + + delta_crossfade = pos_reader - 1.0f - pos_next; + // Calculate the sample distance between the current read position (`pos_reader`) + // and the newly computed `pos_next`: + // - Subtracting `1.0f` compensates for the fact that `pos_reader` corresponds to a read + // position from the previous tick. + + fade_counter = 0; + // Reset the fade counter to begin a new crossfade for the next segment. +} + +void Reverter::filterout(float *smp) { + writeToRingBuffer(smp); + processBuffer(smp); +} + +void Reverter::writeToRingBuffer(const float *smp) { + int space_to_end = mem_size - pos_writer; + if (buffersize <= space_to_end) { + // No wrap around, copy in one go + memcpy(&input[pos_writer], smp, buffersize * sizeof(float)); + } else { + // Wrap around, split into two copies + memcpy(&input[pos_writer], smp, space_to_end * sizeof(float)); + memcpy(&input[0], smp + space_to_end, (buffersize - space_to_end) * sizeof(float)); + } + // Update pos_writer after copying new buffer + pos_writer = (pos_writer + buffersize) % mem_size; + // calculate mean abs value of new buffer (for silence detection) + float abs_value = 0.0f; + for (int i = 0; i < buffersize; i++) { + abs_value += fabsf(smp[i]); + } + mean_abs_value = abs_value / static_cast<float>(buffersize); +} + +void Reverter::processBuffer(float *smp) { + for (int i = 0; i < buffersize; i++) { + reverse_index++; + checkSync(); + updateReaderPosition(i); + crossfadeSamples(smp, i); + applyGain(smp[i]); + } + + phase_offset_old = phase_offset; + phase_offset_fade = 0.0f; +} + +void Reverter::checkSync() { + float delay_samples = delay * static_cast<float>(samplerate); + switch (syncMode) { + case AUTO: + if (reverse_index >= delay_samples) { + switchBuffers(); + } + break; + case HOST: + if ( (doSync && (float)reverse_index >= syncPos) || // external sync time OR + reverse_index >= max_delay_samples ) { // note is too long buffer ends here + switchBuffers(); + doSync = false; + } + // silence the effect if host transport is stopped + if(!time->playing) + state = IDLE; + else + if (state != PLAYING) + { + state = PLAYING; + reset(); + } + + break; + case NOTEON: + if (reverse_index >= delay_samples && state != IDLE) + handleStateChange(); + break; + case NOTEONOFF: + if ( (reverse_index >= recorded_samples && state == PLAYING) || // finished playing OR + (reverse_index >= max_delay_samples && state == PLAYING) ||// note is too long buffer ends here OR + (mean_abs_value < 0.001f && state == RECORDING) ) // note has decayed + handleStateChange(); + break; + default: + {} + break; + } +} + +void Reverter::handleStateChange() { + if (state == RECORDING) { + recorded_samples = reverse_index; + state = PLAYING; + } else if (state == PLAYING) { + state = IDLE; + } + switchBuffers(); +} + +void Reverter::sync(float pos) { + if (state == IDLE) { + reset(); + state = RECORDING; + reverse_index = 0; + } else { + syncPos = pos + reverse_index; + doSync = true; + } +} + +void Reverter::updateReaderPosition(int i) { + // buffersize + // |<---->| + // ---+------+------+------+------+------+--- + // | | | | | | + // ...| b4 | b5 | b6 | b7 | b8 |... + // ---+------+------+------+------+------+--- + // ^ ^ ^ + // | L pos_start L pos_writer + // L pos_reader + // <--><-------> + // | L reverse_index + // L phase_offset + // ////////////////////////////////////////////////// + + // update reading position + pos_reader = fmodf(float(pos_start + mem_size) - (reverse_index + phase_offset), mem_size); +} + +void Reverter::crossfadeSamples(float *smp, int i) { + if (fade_counter < fading_samples) { + float fadeinFactor = static_cast<float>(fade_counter) / static_cast<float>(fading_samples); + float fadeoutFactor = 1.0f - fadeinFactor; + fade_counter++; + + if (state == IDLE) { + // in IDLE only the fade out part is needed + smp[i] = fadeoutFactor * sampleLerp(input, fmodf(pos_reader + mem_size + delta_crossfade, mem_size)); + } else { + smp[i] = applyFade(fadeinFactor, fadeoutFactor); + } + } else { + smp[i] = (state == PLAYING) ? sampleLerp(input, pos_reader) : 0.0f; + } +} + +float Reverter::applyFade(float fadein, float fadeout) { + if (syncMode == NOTEON || syncMode == NOTEONOFF) { + return fadein * sampleLerp(input, pos_reader); + } else { + return fadein * sampleLerp(input, pos_reader) + fadeout * sampleLerp(input, fmodf(pos_reader + mem_size + delta_crossfade, mem_size)); + } +} + +void Reverter::setdelay(float value) { + + if (value == delay) + return; + else + delay = value; + + // update crossfade since it depends on delay + setcrossfade(crossfade); + // recalculate global offset using new delay + global_offset = fmodf(tRef, delay); + // update mem_size since it depends on delay and crossfade + update_memsize(); +} + +void Reverter::update_memsize() { + // Calculate mem_size for reverse delay effect: + // - 1 times delay for recording + // - 1 times delay for reverse playing + // - 1 times delay for phase changing while playing + // - Add crossfade duration + // - Add 2 extra samples as a safety margin for interpolation and circular buffer wrapping. + // - Add buffersize to prevent a buffer write into reading area + unsigned int mem_size_new; + if (syncMode == NOTEON || syncMode == AUTO) + mem_size_new = static_cast<int>(ceilf(3.0f * delay * static_cast<float>(samplerate))) + fading_samples + 2 + buffersize; + else + mem_size_new = static_cast<int>(ceilf(3.0f * max_delay_samples)) + fading_samples + 2 + buffersize; + + if (mem_size_new == mem_size) + return; + else + mem_size = mem_size_new; + + if (input != nullptr) { + memory.devalloc(input); + } + + input = memory.valloc<float>(mem_size); + reset(); + +} + +void Reverter::setcrossfade(float value) { + crossfade = value; + float delay_samples = delay * static_cast<float>(samplerate); + fading_samples = static_cast<int>(crossfade * static_cast<float>(samplerate)); + if (delay_samples < 1.25f * static_cast<float>(fading_samples)) + fading_samples = static_cast<int>(delay_samples * 0.8f); +} + +void Reverter::setsyncMode(SyncMode value) { + if (value != syncMode) { + syncMode = value; + // initialize state machine appropriate to the new mode + state = (syncMode == NOTEON || syncMode == NOTEONOFF) ? IDLE : PLAYING; + // update mem_size since it depends on syncMode + update_memsize(); + } +} + +void Reverter::reset() { + if (input != nullptr) + memset(input, 0, mem_size * sizeof(float)); + pos_writer = 0; + reverse_index = 0; + syncPos = mem_size; +} + +void Reverter::setphase(float value) { + phase = value; +} + +void Reverter::setgain(float value) { + gain = dB2rap(value); +} + +void Reverter::applyGain(float &sample) { + sample *= gain; +} + +}; diff --git a/src/DSP/Reverter.h b/src/DSP/Reverter.h @@ -0,0 +1,286 @@ +/* + ZynAddSubFX - a software synthesizer + + Reverter.h - Reverse Delay + Copyright (C) 2023-2024 Michael Kirchner + Author: Michael Kirchner + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. +*/ + +#pragma once +#include "../globals.h" + +namespace zyn { + +#define SYNCMODES NOTEON,\ + NOTEONOFF,\ + AUTO,\ + HOST + +enum SyncMode +{ + SYNCMODES +}; + +// State Machine for the modes NOTEON and NOTEONOFF +// Sync-Event leads to a cycle of RECORDING->PLAYING->IDLE +// +// RECORDING - not playing but counting the recording duration +// PLAYING - playing for delay duration (NOTEON) or for the recorded duration (NOTEONOFF) +// IDLE - not playing + +#define STATES RECORDING,\ + PLAYING,\ + IDLE + +enum State +{ + STATES +}; + +/** + * @brief Reverse Delay effect class + * + * Implements a reverse delay effect using a ring buffer. Supports synchronization + * with host, MIDI, or note-on/off events. + */ +class Reverter +{ +public: + /** + * @brief Constructor for the Reverter class. + * + * @param alloc Allocator for memory management. + * @param delay Initial delay time in seconds. + * @param srate Sample rate in Hz. + * @param bufsize Size of the audio buffer. + * @param tRef Optional time reference (default is 0.0f). + * @param time_ Optional pointer to an AbsTime object for timing. + */ + Reverter(Allocator *alloc, float delay, unsigned int srate, int bufsize, float tRef = 0.0f, const AbsTime *time_ = nullptr); + + /// Destructor + ~Reverter(); + + /** + * @brief Process and filter the input audio samples. + * + * @param smp Pointer to the buffer of input samples. + */ + void filterout(float *smp); + + /** + * @brief Set the delay time in seconds. + * + * @param value Delay time. + */ + void setdelay(float value); + + /** + * @brief Set the phase of the delay. + * + * @param value Phase value. + */ + void setphase(float value); + + /** + * @brief Set the crossfade time for the transition between buffers. + * + * @param value Crossfade time. + */ + void setcrossfade(float value); + + /** + * @brief Set the output gain. + * + * @param value Gain value in dB. + */ + void setgain(float value); + + /** + * @brief Set the synchronization mode for the reverter. + * + * @param value Synchronization mode (e.g., AUTO, MIDI, HOST). + */ + void setsyncMode(SyncMode value); + + /// Reset the internal state and buffer. + void reset(); + + /** + * @brief Synchronize the delay based on an external position. + * + * @param pos External position for syncing. + */ + void sync(float pos); + +private: + /** + * @brief Switch between buffers when reverse playback is triggered. + * + * @param offset Offset for buffer switching. + */ + void switchBuffers(); + + /** + * @brief Perform linear interpolation between two samples. + * + * @param smp Pointer to the sample buffer. + * @param pos Position to interpolate. + * @return Interpolated sample value. + */ + float sampleLerp(const float *smp, const float pos); + + /** + * @brief Write new input samples to the ring buffer. + * + * @param smp Pointer to the buffer of input samples. + */ + void writeToRingBuffer(const float *smp); + + /** + * @brief Process the samples and apply the reverse delay effect. + * + * @param smp Pointer to the buffer of input samples. + */ + void processBuffer(float *smp); + + /** + * @brief Handle synchronization modes and state transitions. + * + */ + void checkSync(); + + /** + * @brief Handles state transitions in NOTEON and NOTEONOFF modes. + */ + void handleStateChange(); + + /** + * @brief Update the read position in the ring buffer. + */ + void updateReaderPosition(int i); + + /** + * @brief Apply crossfading between two buffer segments. + * + * @param smp Pointer to the buffer of input samples. + * @param i Index of the current sample in the buffer. + */ + void crossfadeSamples(float *smp, int i); + + /** + * @brief Apply fading for transitions between buffer segments. + * + * @param fadein Fade-in factor. + * @param fadeout Fade-out factor. + * @return Crossfaded sample value. + */ + float applyFade(float fadein, float fadeout); + + /** + * @brief Apply the output gain to a sample. + * + * @param sample Reference to the sample to modify. + */ + void applyGain(float &sample); + + void update_memsize(); + + /// Current synchronization mode + SyncMode syncMode; + + /// Current state of the reverter (RECORDING, PLAYING, IDLE) + State state; + + /// Ring buffer for input samples + float* input; + + /// Output gain value + float gain; + + /// Delay time in samples + float delay; + + /// Phase value + float phase; + + /// Crossfade duration in samples + float crossfade; + + /// Time reference for syncing + float tRef; + + /// Offset in the buffer for reading/writing + int buffer_offset; + + /// Counter for buffer processing + int buffer_counter; + + /// Offset for global time synchronization + float global_offset; + + /// Index for reverse playback + int reverse_index; + + /// Phase offset for delay transitions + float phase_offset_old; + float phase_offset_fade; + + /// Number of samples for crossfading + int fading_samples; + + /// Counter for crossfade processing + int fade_counter; + + /// Root mean square (RMS) history for input volume analysis + float mean_abs_value; + + /// Pointer to timing object for synchronization + const AbsTime *time; + + /// Memory allocator + Allocator &memory; + + /// Total size of the memory buffer in samples + unsigned int mem_size; + + /// Sample rate of the audio engine + const int samplerate; + + /// Size of the audio buffer in samples + const int buffersize; + + /// Maximum allowable delay in samples + const float max_delay_samples; + + /// Start position for the read head in the ring buffer + unsigned int pos_start; + + /// Offset for phase correction + float phase_offset; + + /// Number of samples recorded for playback + int recorded_samples; + + /// Position to synchronize playback + float syncPos; + + /// Current position of the read head in the ring buffer + float pos_reader; + + /// Delta between crossfaded buffer positions + float delta_crossfade; + + /// Flag indicating whether synchronization is active + bool doSync; + + /// Write head position in the ring buffer + unsigned int pos_writer; +}; + +} diff --git a/src/Effects/Alienwah.cpp b/src/Effects/Alienwah.cpp @@ -68,7 +68,7 @@ rtosc::Ports Alienwah::ports = { Alienwah::Alienwah(EffectParams pars) :Effect(pars), - lfo(pars.srate, pars.bufsize), + lfo(pars.srate, pars.bufsize, pars.time), oldl(NULL), oldr(NULL) { diff --git a/src/Effects/CMakeLists.txt b/src/Effects/CMakeLists.txt @@ -12,5 +12,6 @@ set(zynaddsubfx_effect_SRCS Effects/Phaser.cpp Effects/Reverb.cpp Effects/Sympathetic.cpp + Effects/Reverse.cpp PARENT_SCOPE ) diff --git a/src/Effects/Chorus.cpp b/src/Effects/Chorus.cpp @@ -75,7 +75,7 @@ rtosc::Ports Chorus::ports = { Chorus::Chorus(EffectParams pars) :Effect(pars), - lfo(pars.srate, pars.bufsize), + lfo(pars.srate, pars.bufsize, pars.time), maxdelay((int)(MAX_CHORUS_DELAY / 1000.0f * samplerate_f)), delaySample(memory.valloc<float>(maxdelay), memory.valloc<float>(maxdelay)) { diff --git a/src/Effects/CombFilterBank.cpp b/src/Effects/CombFilterBank.cpp @@ -121,9 +121,9 @@ namespace zyn { unsigned int nrOfActualStrings = 0; for (unsigned int j = 0; j < nrOfStrings; ++j) if (delays[j] != 0.0f) { - smp[i] += string_smps[j][pos_writer]; - nrOfActualStrings++; - } + smp[i] += string_smps[j][pos_writer]; + nrOfActualStrings++; + } // apply output gain to sum of strings and // divide by nrOfStrings to get mean value diff --git a/src/Effects/DynamicFilter.cpp b/src/Effects/DynamicFilter.cpp @@ -65,7 +65,7 @@ rtosc::Ports DynamicFilter::ports = { DynamicFilter::DynamicFilter(EffectParams pars) :Effect(pars), - lfo(pars.srate, pars.bufsize), + lfo(pars.srate, pars.bufsize, pars.time), Pvolume(110), Pdepth(0), Pampsns(90), diff --git a/src/Effects/Effect.cpp b/src/Effects/Effect.cpp @@ -21,10 +21,10 @@ namespace zyn { EffectParams::EffectParams(Allocator &alloc_, bool insertion_, float *efxoutl_, float *efxoutr_, unsigned char Ppreset_, unsigned int srate_, int bufsize_, FilterParams *filterpars_, - bool filterprotect_) + bool filterprotect_, const AbsTime *time_) :alloc(alloc_), insertion(insertion_), efxoutl(efxoutl_), efxoutr(efxoutr_), Ppreset(Ppreset_), srate(srate_), bufsize(bufsize_), filterpars(filterpars_), - filterprotect(filterprotect_) + filterprotect(filterprotect_), time(time_) {} Effect::Effect(EffectParams pars) :Ppreset(pars.Ppreset), @@ -33,6 +33,7 @@ Effect::Effect(EffectParams pars) filterpars(pars.filterpars), insertion(pars.insertion), memory(pars.alloc), + time(pars.time), samplerate(pars.srate), buffersize(pars.bufsize) { diff --git a/src/Effects/Effect.h b/src/Effects/Effect.h @@ -18,6 +18,7 @@ #include "../globals.h" #include "../Params/FilterParams.h" #include "../Misc/Stereo.h" +#include "../Misc/Sync.h" // effect parameters differing between single effects #ifndef rEffPar @@ -97,7 +98,7 @@ struct EffectParams * @return Initialized Effect Parameter object*/ EffectParams(Allocator &alloc_, bool insertion_, float *efxoutl_, float *efxoutr_, unsigned char Ppreset_, unsigned int srate, int bufsize, FilterParams *filterpars_, - bool filterprotect=false); + bool filterprotect=false, const AbsTime *time_ = nullptr); Allocator &alloc; @@ -109,14 +110,18 @@ struct EffectParams int bufsize; FilterParams *filterpars; bool filterprotect; + const AbsTime *time; }; /**this class is inherited by the all effects(Reverb, Echo, ..)*/ -class Effect +class Effect: public Observer { public: Effect(EffectParams pars); virtual ~Effect() {} + + void update() override {} + /** * Get default preset parameter value * @param npreset chosen preset @@ -162,6 +167,7 @@ class Effect * Master Output only.*/ float volume; + float speedfactor; /**relative factor for using the global tempo (BPM)*/ FilterParams *filterpars; /**<Parameters for filters used by Effect*/ @@ -183,6 +189,8 @@ class Effect //Allocator Allocator &memory; + const AbsTime *time; + // current setup unsigned int samplerate; int buffersize; diff --git a/src/Effects/EffectLFO.cpp b/src/Effects/EffectLFO.cpp @@ -15,11 +15,11 @@ #include "../Misc/Util.h" #include <cmath> -#include "globals.h" +#include "../globals.h" namespace zyn { -EffectLFO::EffectLFO(float srate_f, float bufsize_f) +EffectLFO::EffectLFO(float srate_f, float bufsize_f, const AbsTime *time_) :Pfreq(40), Prandomness(0), PLFOtype(0), @@ -32,7 +32,8 @@ EffectLFO::EffectLFO(float srate_f, float bufsize_f) ampr2(RND), lfornd(0.0f), samplerate_f(srate_f), - buffersize_f(bufsize_f) + buffersize_f(bufsize_f), + time(time_) { updateparams(); } diff --git a/src/Effects/EffectLFO.h b/src/Effects/EffectLFO.h @@ -14,6 +14,8 @@ #ifndef EFFECT_LFO_H #define EFFECT_LFO_H +#include "../globals.h" + namespace zyn { /**LFO for some of the Effect objects @@ -21,26 +23,61 @@ namespace zyn { class EffectLFO { public: - EffectLFO(float srate_f, float bufsize_f); - ~EffectLFO(); - void effectlfoout(float *outl, float *outr); - void updateparams(void); - unsigned char Pfreq; - unsigned char Prandomness; - unsigned char PLFOtype; - unsigned char Pstereo; // 64 is centered - private: - float getlfoshape(float x); - - float xl, xr; - float incx; - float ampl1, ampl2, ampr1, ampr2; //necessary for "randomness" - float lfornd; - char lfotype; - - // current setup - float samplerate_f; - float buffersize_f; + /** + * Constructs an EffectLFO object. + * + * @param srate_f Sample rate. + * @param bufsize_f Buffer size. + * @param time Pointer to an AbsTime object (optional). + */ + EffectLFO(float srate_f, float bufsize_f, const AbsTime *time = nullptr); + + /** + * Destructs the EffectLFO object. + */ + ~EffectLFO(); + + /** + * Processes the LFO and outputs the result to the provided pointers. + * + * @param outl Pointer to the left output channel. + * @param outr Pointer to the right output channel. + */ + void effectlfoout(float *outl, float *outr); + + /** + * Updates the LFO parameters based on the current settings. + */ + void updateparams(void); + + unsigned char Pfreq; //!< Frequency parameter (0-127) + unsigned char Prandomness; //!< Randomness parameter (0-127) + unsigned char PLFOtype; //!< LFO type parameter (0-2) + unsigned char Pstereo; //!< Stereo parameter (-64 to 64) + +private: + /** + * Calculates the LFO shape based on the current phase and type. + * + * @param x Phase value (0-1). + * @return The calculated LFO shape value. + */ + float getlfoshape(float x); + + float xl, xr; //!< Phase accumulators for left and right channels + float incx; //!< Increment for phase accumulators + float ampl1, ampl2, ampr1, ampr2; //!< Amplitude modulation parameters + float lfornd; //!< Randomness factor + char lfotype; //!< Current LFO type + + // Current setup + float samplerate_f; //!< Sample rate + float buffersize_f; //!< Buffer size + + const AbsTime *time; //!< Pointer to an AbsTime object + unsigned int numerator; //!< The numerator of the time signature to calculate the frequency from system tempo(bpm) + //!< If it is 0 Pfreq is used. + unsigned int denominator; //!< The denominator of the time signature to calculate the frequency from system tempo(bpm) }; } diff --git a/src/Effects/EffectMgr.cpp b/src/Effects/EffectMgr.cpp @@ -27,11 +27,13 @@ #include "DynamicFilter.h" #include "Phaser.h" #include "Sympathetic.h" +#include "../Effects/Reverse.h" #include "../Misc/XMLwrapper.h" #include "../Misc/Util.h" -#include "../Misc/Time.h" + #include "../Params/FilterParams.h" #include "../Misc/Allocator.h" +#include "../Misc/Time.h" namespace zyn { @@ -130,29 +132,30 @@ static const rtosc::Ports local_ports = { if (val>=0) { eff->numerator = val; int Pdelay, Pfreq; - float freq; - if(eff->denominator) { + float freq, delay; + if (eff->numerator&&eff->denominator) { + eff->efx->speedfactor = (float)eff->denominator / (4.0f *(float)eff->numerator); switch(eff->nefx) { case 2: // Echo + case 10: // Reverse // invert: - // delay = (Pdelay / 127.0f * 1.5f); //0 .. 1.5 sec - Pdelay = (int)roundf((20320.0f / (float)eff->time->tempo) * - ((float)eff->numerator / (float)eff->denominator)); - if (eff->numerator&&eff->denominator) - eff->seteffectparrt(2, Pdelay); + // delay = ((Pdelay+1)/128.0f*MAX_REV_DELAY_SECONDS); //0 .. x sec + // Pdelay = (delay * 128.0f / MAX_REV_DELAY_SECONDS) -1 + // delay = 60 / tempo * 4 * numerator / denominator + assert(eff->time->tempo > 0); + delay = 60.0f / ((float)eff->time->tempo * eff->efx->speedfactor); + Pdelay = (unsigned char)(delay * 128.0f / MAX_REV_DELAY_SECONDS)-1; + eff->seteffectparrt(2, Pdelay); break; case 3: // Chorus case 4: // Phaser case 5: // Alienwah case 8: // DynamicFilter - freq = ((float)eff->time->tempo * - (float)eff->denominator / - (240.0f * (float)eff->numerator)); + freq = (float)eff->time->tempo * 60.0 * eff->efx->speedfactor; // invert: // (powf(2.0f, Pfreq / 127.0f * 10.0f) - 1.0f) * 0.03f Pfreq = (int)roundf(logf((freq/0.03f)+1.0f)/LOG_2 * 12.7f); - if (eff->numerator&&eff->denominator) - eff->seteffectparrt(2, Pfreq); + eff->seteffectparrt(2, Pfreq); break; case 1: // Reverb case 6: // Distortion @@ -161,6 +164,8 @@ static const rtosc::Ports local_ports = { break; } } + else + eff->efx->speedfactor = 0.0f; } d.broadcast(d.loc, "i", val); } else { @@ -177,29 +182,26 @@ static const rtosc::Ports local_ports = { if (val > 0) { eff->denominator = val; int Pdelay, Pfreq; - float freq; - if(eff->numerator) { + float freq, delay; + if (eff->numerator&&eff->denominator) { + eff->efx->speedfactor = (float)eff->denominator / (4.0f *(float)eff->numerator); switch(eff->nefx) { case 2: // Echo - // invert: - // delay = (Pdelay / 127.0f * 1.5f); //0 .. 1.5 sec - Pdelay = (int)roundf((20320.0f / (float)eff->time->tempo) * - ((float)eff->numerator / (float)eff->denominator)); - if (eff->numerator&&eff->denominator) - eff->seteffectparrt(2, Pdelay); + case 10: // Reverse + assert(eff->time->tempo > 0); + delay = 60.0f / ((float)eff->time->tempo * eff->efx->speedfactor); + Pdelay = (unsigned char)(delay * 128.0f / MAX_REV_DELAY_SECONDS)-1; + eff->seteffectparrt(2, Pdelay); break; case 3: // Chorus case 4: // Phaser case 5: // Alienwah case 8: // DynamicFilter - freq = ((float)eff->time->tempo * - (float)eff->denominator / - (240.0f * (float)eff->numerator)); + freq = (float)eff->time->tempo * 60.0 * eff->efx->speedfactor; // invert: // (powf(2.0f, Pfreq / 127.0f * 10.0f) - 1.0f) * 0.03f Pfreq = (int)roundf(logf((freq/0.03f)+1.0f)/LOG_2 * 12.7f); - if (eff->numerator&&eff->denominator) - eff->seteffectparrt(2, Pfreq); + eff->seteffectparrt(2, Pfreq); break; case 1: // Reverb case 6: // Distortion @@ -208,6 +210,8 @@ static const rtosc::Ports local_ports = { break; } } + else + eff->efx->speedfactor = 0.0f; } d.broadcast(d.loc, "i", val); } else { @@ -229,7 +233,7 @@ static const rtosc::Ports local_ports = { d.reply(d.loc, "bb", sizeof(a), a, sizeof(b), b); }}, {"efftype::i:c:S", rOptions(Disabled, Reverb, Echo, Chorus, - Phaser, Alienwah, Distortion, EQ, DynFilter, Sympathetic) rDefault(Disabled) + Phaser, Alienwah, Distortion, EQ, DynFilter, Sympathetic, Reverse) rDefault(Disabled) rProp(parameter) rDoc("Get Effect Type"), NULL, rCOptionCb(obj->nefx, obj->changeeffectrt(var))}, {"efftype:b", rProp(internal) rDoc("Pointer swap EffectMgr"), NULL, @@ -258,12 +262,13 @@ static const rtosc::Ports local_ports = { rSubtype(Phaser), rSubtype(Reverb), rSubtype(Sympathetic), + rSubtype(Reverse), }; const rtosc::Ports &EffectMgr::ports = local_ports; EffectMgr::EffectMgr(Allocator &alloc, const SYNTH_T &synth_, - const bool insertion_, const AbsTime *time_) + const bool insertion_, const AbsTime *time_, Sync *sync_) :insertion(insertion_), efxoutl(new float[synth_.buffersize]), efxoutr(new float[synth_.buffersize]), @@ -271,6 +276,7 @@ EffectMgr::EffectMgr(Allocator &alloc, const SYNTH_T &synth_, nefx(0), efx(NULL), time(time_), + sync(sync_), numerator(0), denominator(4), dryonly(false), @@ -287,6 +293,7 @@ EffectMgr::EffectMgr(Allocator &alloc, const SYNTH_T &synth_, EffectMgr::~EffectMgr() { + if(sync) sync->detach(efx); memory.dealloc(efx); delete filterpars; delete [] efxoutl; @@ -346,6 +353,10 @@ void EffectMgr::changeeffectrt(int _nefx, bool avoidSmash) case 9: efx = memory.alloc<Sympathetic>(pars); break; + case 10: + efx = memory.alloc<Reverse>(pars, time); + if(sync) sync->attach(efx); + break; //put more effect here default: efx = NULL; @@ -358,6 +369,7 @@ void EffectMgr::changeeffectrt(int _nefx, bool avoidSmash) if (numerator>0) { switch(nefx) { case 2: // Echo + case 10:// Reverse // invert: // delay = (Pdelay / 127.0f * 1.5f); //0 .. 1.5 sec Pdelay = (int)roundf((20320.0f / (float)time->tempo) * @@ -437,6 +449,7 @@ void EffectMgr::init(void) void EffectMgr::kill(void) { //printf("Killing Effect(%d)\n", nefx); + if(sync) sync->detach(efx); memory.dealloc(efx); } diff --git a/src/Effects/EffectMgr.h b/src/Effects/EffectMgr.h @@ -18,6 +18,7 @@ #include "../Params/FilterParams.h" #include "../Params/Presets.h" +#include "../globals.h" namespace zyn { @@ -31,7 +32,7 @@ class EffectMgr:public Presets { public: EffectMgr(Allocator &alloc, const SYNTH_T &synth, const bool insertion_, - const AbsTime *time_ = nullptr); + const AbsTime *time_ = nullptr, Sync *sync_ = nullptr); ~EffectMgr() override; void paste(EffectMgr &e); @@ -73,6 +74,7 @@ class EffectMgr:public Presets int nefx; Effect *efx; const AbsTime *time; + Sync *sync; int numerator; int denominator; diff --git a/src/Effects/Phaser.cpp b/src/Effects/Phaser.cpp @@ -113,7 +113,7 @@ rtosc::Ports Phaser::ports = { #define ZERO_ 0.00001f // Same idea as above. Phaser::Phaser(EffectParams pars) - :Effect(pars), lfo(pars.srate, pars.bufsize), old(NULL), xn1(NULL), + :Effect(pars), lfo(pars.srate, pars.bufsize, pars.time), old(NULL), xn1(NULL), yn1(NULL), diff(0.0f), oldgain(0.0f), fb(0.0f) { analog_setup(); diff --git a/src/Effects/Reverse.cpp b/src/Effects/Reverse.cpp @@ -0,0 +1,305 @@ +/* + ZynAddSubFX - a software synthesizer + + Reverse.cpp - Reverse Delay Effect + Copyright (C) 2023-2024 Michael Kirchner + Author: Michael Kirchner + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. +*/ + +#include <cmath> +#include <rtosc/ports.h> +#include <rtosc/port-sugar.h> +#include "../DSP/Reverter.h" +#include "../Misc/Allocator.h" +#include "../Misc/Util.h" +#include "../Misc/Time.h" +#include "Reverse.h" + +namespace zyn { +#define HZ2BPM 60.0f // frequency (Hz) * HZ2BPM = frequency (BPM) + +#define rObject Reverse +#define rBegin [](const char *msg, rtosc::RtData &d) { +#define rEnd } + +rtosc::Ports Reverse::ports = { + {"preset::i", rProp(parameter) + rOptions(noteon, noteonoff, auto) + rProp(alias) + rShort("preset") + rDefault(0) + rDoc("Instrument Presets"), 0, + rBegin; + rObject *o = (rObject*)d.obj; + if(rtosc_narguments(msg)) + o->setpreset(rtosc_argument(msg, 0).i); + else + d.reply(d.loc, "i", o->Ppreset); + rEnd}, + rPresetForVolume, + rEffParVol(), + rEffParPan(), + rEffPar(Pdelay, 2, rShort("delay"), rDefault(31), + "Length of Reversed Segment"), + rEffParTF(Pstereo,3, rShort("stereo"), + "Enable Stereo Processing"), + rEffPar(Pphase, 4, rShort("phase"), rDefault(64), + "Phase offset for Reversed Segment"), + rEffPar(Pcrossfade, 5, rShort("fade"), rDefault(64), rUnit(1\/100 s), + "Cross Fade Time between Reversed Segments"), + rEffParOpt(Psyncmode, 6, rShort("mode"), rDefault(NOTEON), + rOptions(SYNCMODES), + "Sync Mode"), +}; +#undef rBegin +#undef rEnd +#undef rObject + +Reverse::Reverse(EffectParams pars, const AbsTime *time_) + :Effect(pars),Pvolume(50),Pdelay(31),Pphase(64), Pcrossfade(64), PsyncMode(NOTEON), Pstereo(0),time(time_), tick_hist(0) +{ + float tRef = float(time->time()); + reverterL = memory.alloc<Reverter>(&memory, float(Pdelay+1)/128.0f*MAX_REV_DELAY_SECONDS, samplerate, buffersize, tRef, time); + reverterR = memory.alloc<Reverter>(&memory, float(Pdelay+1)/128.0f*MAX_REV_DELAY_SECONDS, samplerate, buffersize, tRef, time); + setpanning(64); + setvolume(Pvolume); + tick_at_host_buffer_start = (time->beat - 1) * time->ppq + time->tick; + playing_hist = false; +} + +Reverse::~Reverse() +{ + memory.dealloc(reverterL); + memory.dealloc(reverterR); +} + +//Cleanup the effect +void Reverse::cleanup(void) +{ + reverterR->reset(); + reverterL->reset(); +} + +//Effect output +void Reverse::out(const Stereo<float *> &input) +{ + // prepare the processing buffers + if(Pstereo) //Stereo + for(int i = 0; i < buffersize; ++i) { + efxoutl[i] = input.l[i] * pangainL; + efxoutr[i] = input.r[i] * pangainR; + } + else //Mono + for(int i = 0; i < buffersize; ++i) + efxoutl[i] = (input.l[i] * pangainL + input.r[i] * pangainR); + + // process external timecode for syncing to host beat + // but only if we have timing info, speedfactor is set and we are in host mode. + if (time->tempo && speedfactor && (PsyncMode == HOST)) { + // in host mode we want to find out if (condition) and when (position) a beat happens inside the buffer + // and call sync at that position + // condition: at the end of the buffer: ticks % ticks_per_beat < ticks_per buffer + // position: ticks % ticks_per_beat + // + // to complicate things: + // when running as plugin, the host may have a larger buffer than buffersize. + // in this case the processing function is called multiple times for one host buffer + // and we have to interpolate the ticks for each internal buffer + // + // 1:01:00 (start of "song") 1:02:00 internal buffer end + // | | + // |-------------------------------|-------------------------------| + // 1 beat + // + // + // |-----------------------------------------------------------| + // "tick" + // |--"ticks_since_last_beat"--| + // + // |---"internal_buffer_ticks"------------------| if beat change inside internal buffer + // |-"internal_buffer_ticks"-| if beat change not inside internal buffer + + + // number of ticks during the length of one internal buffer + const float internal_buffer_ticks = ((float)buffersize / (float)samplerate) * // seconds per host buffer * + ((float)time->tempo / HZ2BPM) * // beats per second * + (float)time->ppq; // ticks per beat = ticks per buffer + + // check if there is new timing information + // that indicates a new host buffer + // therefore we reset the current subbuffer index. + // and calculate the new tick at time of host buffer start + if(time->tick != tick_hist) { + currentSubbufferIndex = 0; + tick_hist = time->tick; + tick_at_host_buffer_start = (((time->bar - 1) * time->beatsPerBar) + (time->beat - 1)) * time->ppq + time->tick; + if(time->playing != playing_hist) { + reverterL->sync(0); + if (Pstereo) reverterR->sync(0); + } + playing_hist = time->playing; + + } + else + currentSubbufferIndex++; + + { + // tick offset from the host buffer to the current internal buffer + const float tick_offset = internal_buffer_ticks * (float)currentSubbufferIndex; + // tick at time of internal buffer start + const float tick_at_buffer_start = tick_at_host_buffer_start + tick_offset; + // now calculate the tick at time of internal buffer end + // this is needed to determine if a beat change will happen during this internal buffer + + tick = tick_at_buffer_start + internal_buffer_ticks; + } + + + const float ticks_per_beat = (float)time->ppq / speedfactor; + const float ticks_since_last_beat = fmodf(tick, ticks_per_beat); + if(ticks_since_last_beat < internal_buffer_ticks) { // Ensure beat is inside the buffer + const float syncPos = ( (internal_buffer_ticks - ticks_since_last_beat) / (float)internal_buffer_ticks) * (float)buffersize; + reverterL->sync(syncPos); + if (Pstereo) reverterR->sync(syncPos); + } + } + + // do the actual processing + reverterL->filterout(efxoutl); + if(Pstereo) reverterR->filterout(efxoutr); + else memcpy(efxoutr, efxoutl, bufferbytes); +} + +void Reverse::update() +{ + // process noteon trigger + if( (PsyncMode == NOTEON || PsyncMode == NOTEONOFF) ) { + reverterL->sync(0.0f); + if(Pstereo) reverterR->sync(0.0f); + } +} +//Parameter control +void Reverse::setvolume(unsigned char _Pvolume) +{ + Pvolume = _Pvolume; + + if(insertion == 0) { + if (Pvolume == 0) { + outvolume = 0.0f; + } else { + outvolume = powf(0.01f, (1.0f - Pvolume / 127.0f)) * 4.0f; + } + volume = 1.0f; + } + else + volume = outvolume = Pvolume / 127.0f; + if(Pvolume == 0) + cleanup(); +} + +void Reverse::setdelay(unsigned char value) +{ + Pdelay = limit(value,static_cast<unsigned char>(0),static_cast<unsigned char>(127)); + reverterL->setdelay(float(Pdelay+1)/128.0f*MAX_REV_DELAY_SECONDS); + reverterR->setdelay(float(Pdelay+1)/128.0f*MAX_REV_DELAY_SECONDS); +} + +void Reverse::setphase(unsigned char value) +{ + Pphase = value; + reverterL->setphase(float(Pphase)/127.0f); + reverterR->setphase(float(Pphase)/127.0f); +} + +void Reverse::setcrossfade(unsigned char value) +{ + Pcrossfade = value; + reverterL->setcrossfade(float(value+1)/100.0f); + reverterR->setcrossfade(float(value+1)/100.0f); +} + +void Reverse::setsyncMode(unsigned char value) +{ + PsyncMode = value; + reverterL->setsyncMode((SyncMode)value); + reverterR->setsyncMode((SyncMode)value); +} + +unsigned char Reverse::getpresetpar(unsigned char npreset, unsigned int npar) +{ +#define PRESET_SIZE 7 +#define NUM_PRESETS 3 + static const unsigned char presets[NUM_PRESETS][PRESET_SIZE] = { + //NOTEON + {64, 64, 25, 0, 64, 32, NOTEON}, + //NOTEONOFF + {64, 64, 25, 0, 64, 16, NOTEONOFF}, + //AUTO + {64, 64, 25, 0, 64, 50, AUTO} + }; + if(npreset < NUM_PRESETS && npar < PRESET_SIZE) { + if (npar == 0 && insertion == 0) { + /* lower the volume if this is system effect */ + return presets[npreset][npar] / 2; + } + return presets[npreset][npar]; + } + return 0; +} + +void Reverse::setpreset(unsigned char npreset) +{ + if(npreset >= NUM_PRESETS) + npreset = NUM_PRESETS - 1; + for(int n = 0; n != 128; n++) + changepar(n, getpresetpar(npreset, n)); + Ppreset = npreset; +} + +void Reverse::changepar(int npar, unsigned char value) +{ + switch(npar) { + case 0: + setvolume(value); + break; + case 1: + setpanning(value); + break; + case 2: + setdelay(value); + break; + case 3: + Pstereo = (value > 1) ? 1 : value; + break; + case 4: + setphase(value); + break; + case 5: + setcrossfade(value); + break; + case 6: + setsyncMode(value); + break; + } +} + +unsigned char Reverse::getpar(int npar) const +{ + switch(npar) { + case 0: return Pvolume; + case 1: return Ppanning; + case 2: return Pdelay; + case 3: return Pstereo; + case 4: return Pphase; + case 5: return Pcrossfade; + case 6: return PsyncMode; + default: return 0; // in case of bogus parameter number + } +} + +} diff --git a/src/Effects/Reverse.h b/src/Effects/Reverse.h @@ -0,0 +1,101 @@ +/* + ZynAddSubFX - a software synthesizer + + Reverse.h - Reverse Delay Effect + Copyright (C) 2023-2024 Michael Kirchner + Author: Michael Kirchner + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. +*/ + +#ifndef REVERSE_H +#define REVERSE_H + +#include "Effect.h" + +namespace zyn { + +class Reverter; + +/**Reverse Effect*/ +class Reverse final:public Effect +{ + public: + Reverse(EffectParams pars, const AbsTime *time_); + ~Reverse(); + void update(); + void out(const Stereo<float *> &input); + unsigned char getpresetpar(unsigned char npreset, unsigned int npar); + void setpreset(unsigned char npreset); + /** + * Sets the value of the chosen variable + * + * The possible parameters are: + * -# Volume + * -# Panning (member of parent class) + * -# Delay + * -# Stereo + * -# Phase + * -# Crossfade + * -# Sync Mode + + * @param npar number of chosen parameter + * @param value the new value + */ + void changepar(int npar, unsigned char value); + + /** + * Gets the specified parameter + * + * The possible parameters are: + * -# Volume + * -# Panning (member of parent class) + * -# Delay + * -# Stereo + * -# Phase + * -# Crossfade + * -# Sync Mode + + + * @param npar number of chosen parameter + * @return value of parameter + */ + unsigned char getpar(int npar) const; + int getnumparams(void); + void cleanup(void); + + static rtosc::Ports ports; + private: + //Parameters + unsigned char Pvolume; /**< #0 Volume or Dry/Wetness */ + unsigned char Pdelay; /**< #2 Length of reversed segment, 127 = 1.5s */ + unsigned char Pphase; /**< #3 Phase offset for delay effect */ + unsigned char Pcrossfade; /**< #4 Crossfade duration between segments */ + unsigned char PsyncMode; /**< #5 Synchronization mode setting */ + unsigned char Pstereo; /**< #6 Stereo mode flag */ + + void setvolume(unsigned char _Pvolume); + void setdelay(unsigned char _Pdelay); + void setphase(unsigned char _Pphase); + void setcrossfade(unsigned char value); + void setsyncMode(unsigned char value); + + const AbsTime *time; + + Reverter* reverterL; + Reverter* reverterR; + + float tick; // number of ticks passed in the current measure + // - used to predict the position of the next beat + unsigned int currentSubbufferIndex = 0; + float tick_hist; + float tick_at_host_buffer_start; + bool playing_hist; +}; + +} + +#endif diff --git a/src/Misc/Allocator.cpp b/src/Misc/Allocator.cpp @@ -49,7 +49,7 @@ struct AllocatorImpl Allocator::Allocator(void) : transaction_active() { impl = new AllocatorImpl; - size_t default_size = 10*1024*1024; + size_t default_size = 16*1024*1024; impl->pools = (next_t*)malloc(default_size); impl->pools->next = 0x0; impl->pools->pool_size = default_size; diff --git a/src/Misc/Master.cpp b/src/Misc/Master.cpp @@ -19,6 +19,7 @@ #include "zyn-version.h" #include "../Misc/Stereo.h" #include "../Misc/Util.h" +#include "../Misc/Sync.h" #include "../Params/LFOParams.h" #include "../Effects/EffectMgr.h" #include "../DSP/FFTwrapper.h" @@ -400,13 +401,13 @@ static const Ports master_ports = { Part7, Part8, Part9, Part10, Part11, Part12, Part13, Part14, Part15, Part16) rDefault([Off ...]), "Part to insert part onto"), - {"Pkeyshift::i", rShort("key shift") rProp(parameter) rLinear(-64,63) rUnit(semitones) - rDefault(0) rDoc("Global Key Shift"), 0, [](const char *m, RtData&d) { + {"Pkeyshift::i", rShort("key shift") rProp(parameter) rLinear(0,127) + rDefault(64) rDoc("Global Key Shift"), 0, [](const char *m, RtData&d) { if(rtosc_narguments(m)==0) { - d.reply(d.loc, "i", ((Master*)d.obj)->Pkeyshift-64); + d.reply(d.loc, "i", ((Master*)d.obj)->Pkeyshift); } else if(rtosc_narguments(m)==1 && rtosc_type(m,0)=='i') { - ((Master*)d.obj)->setPkeyshift(limit<char>(rtosc_argument(m,0).i+64,0,127)); - d.broadcast(d.loc, "i", ((Master*)d.obj)->Pkeyshift-64);}}}, + ((Master*)d.obj)->setPkeyshift(limit<char>(rtosc_argument(m,0).i,0,127)); + d.broadcast(d.loc, "i", ((Master*)d.obj)->Pkeyshift);}}}, {"echo", rDoc("Hidden port to echo messages"), 0, [](const char *m, RtData&d) { d.reply(m-1);}}, {"get-vu:", rDoc("Grab VU Data"), 0, [](const char *, RtData &d) { @@ -766,7 +767,7 @@ void Master::loadAutomation(XMLwrapper &xml, rtosc::AutomationMgr &midi) } Master::Master(const SYNTH_T &synth_, Config* config) - :HDDRecorder(synth_), time(synth_), ctl(synth_, &time), + :HDDRecorder(synth_), time(synth_), sync(), ctl(synth_, &time), microtonal(config->cfg.GzipCompression), bank(config), automate(16,4,8), frozenState(false), pendingMemory(false), @@ -776,8 +777,14 @@ Master::Master(const SYNTH_T &synth_, Config* config) bToU = NULL; uToB = NULL; + sync = new Sync(); + // set default tempo time.tempo = 120; + time.bar = 0; + time.beat = 0; + time.tick = 0.0f; + time.bpm = 0.0f; //Setup MIDI Learn automate.set_ports(master_ports); @@ -807,7 +814,7 @@ Master::Master(const SYNTH_T &synth_, Config* config) ScratchString ss; for(int npart = 0; npart < NUM_MIDI_PARTS; ++npart) { - part[npart] = new Part(*memory, synth, time, config->cfg.GzipCompression, + part[npart] = new Part(*memory, synth, time, sync, config->cfg.GzipCompression, config->cfg.Interpolation, &microtonal, fft, &watcher, (ss+"/part"+npart+"/").c_str); smoothing_part_l[npart].sample_rate( synth.samplerate ); @@ -821,11 +828,11 @@ Master::Master(const SYNTH_T &synth_, Config* config) //Insertion Effects init for(int nefx = 0; nefx < NUM_INS_EFX; ++nefx) - insefx[nefx] = new EffectMgr(*memory, synth, 1, &time); + insefx[nefx] = new EffectMgr(*memory, synth, 1, &time, sync); //System Effects init for(int nefx = 0; nefx < NUM_SYS_EFX; ++nefx) - sysefx[nefx] = new EffectMgr(*memory, synth, 0, &time); + sysefx[nefx] = new EffectMgr(*memory, synth, 0, &time, sync); //Note Visualization memset(activeNotes, 0, sizeof(activeNotes)); @@ -966,6 +973,7 @@ void Master::defaults() void Master::noteOn(char chan, note_t note, char velocity, float note_log2_freq) { if(velocity) { + sync->notify(); for(int npart = 0; npart < NUM_MIDI_PARTS; ++npart) { if(chan == part[npart]->Prcvchn) { fakepeakpart[npart] = velocity * 2; @@ -1255,11 +1263,12 @@ bool Master::runOSC(float *outl, float *outr, bool offline, */ bool Master::AudioOut(float *outl, float *outr) { + //Danger Limits if(memory->lowMemory(2,1024*1024)) printf("QUITE LOW MEMORY IN THE RT POOL BE PREPARED FOR WEIRD BEHAVIOR!!\n"); //Normal Limits - if(!pendingMemory && memory->lowMemory(4,1024*1024)) { + if(!pendingMemory && memory->lowMemory(6,1024*1024)) { printf("Requesting more memory\n"); bToU->write("/request-memory", ""); pendingMemory = true; @@ -1269,13 +1278,11 @@ bool Master::AudioOut(float *outl, float *outr) if(!runOSC(outl, outr, false)) return false; - //Handle watch points if(bToU) watcher.write_back = bToU; watcher.tick(); - //Swaps the Left channel with Right Channel if(swaplr) swap(outl, outr); @@ -1299,7 +1306,6 @@ bool Master::AudioOut(float *outl, float *outr) part[efxpart]->partoutr); } - float gainbuf[synth.buffersize]; //Apply the part volumes and pannings (after insertion effects) @@ -1461,11 +1467,40 @@ bool Master::AudioOut(float *outl, float *outr) //TODO review the respective code from yoshimi for this //If memory serves correctly, libsamplerate was used +// +// beatType is not being used yet. +// but beatsPerBar/beatType could be used to +// match numerator/denominator along with bpm to plugin host + void Master::GetAudioOutSamples(size_t nsamples, unsigned samplerate, float *outl, - float *outr) + float *outr, + int bar, + int beat, + float tick, + float beatsPerBar, + float beatType, + float bpm, + float PPQ, + bool playing, + size_t frames) { + + if(bpm) { + time.hostSamples = frames; + time.bar = bar; + time.beat = beat; + time.tick = tick; + time.beatsPerBar = beatsPerBar; + time.tempo = bpm; + time.bpm = bpm; + time.ppq = PPQ; + time.playing = playing; + } + else + time.bpm = 0; + off_t out_off = 0; //Fail when resampling rather than doing a poor job diff --git a/src/Misc/Master.h b/src/Misc/Master.h @@ -137,7 +137,16 @@ class Master void GetAudioOutSamples(size_t nsamples, unsigned samplerate, float *outl, - float *outr) REALTIME; + float *outr, + int bar=0, + int beat=0, + float beatsPerBar=0.0f, + float beatType=0.0f, + float tick=0.0f, + float bpm=0.0f, + float PPQ=0.0f, + bool playing=false, + size_t frames=0) REALTIME; void partonoff(int npart, int what); @@ -185,7 +194,8 @@ class Master float vuoutpeakpartr[NUM_MIDI_PARTS]; unsigned char fakepeakpart[NUM_MIDI_PARTS]; //this is used to compute the "peak" when the part is disabled - AbsTime time; + AbsTime time; + Sync* sync; Controller ctl; bool swaplr; //if L and R are swapped diff --git a/src/Misc/MiddleWare.cpp b/src/Misc/MiddleWare.cpp @@ -590,6 +590,7 @@ public: [master,filename,this,npart](){ Part *p = new Part(*master->memory, synth, master->time, + master->sync, config->cfg.GzipCompression, config->cfg.Interpolation, &master->microtonal, master->fft, &master->watcher, @@ -649,6 +650,7 @@ public: Part *p = new Part(*master->memory, synth, master->time, + master->sync, config->cfg.GzipCompression, config->cfg.Interpolation, &master->microtonal, master->fft); @@ -1890,8 +1892,8 @@ static rtosc::Ports middlewareReplyPorts = { {"request-memory:", 0, 0, rBegin; //Generate out more memory for the RT memory pool - //5MBi chunk - size_t N = 5*1024*1024; + //8MBi chunk + size_t N = 8*1024*1024; void *mem = malloc(N); impl.uToB->write("/add-rt-memory", "bi", sizeof(void*), &mem, N); rEnd}, @@ -2627,6 +2629,7 @@ void MiddleWare::messageAnywhere(const char *path, const char *args, ...) fprintf(stderr, "Middleware::messageAnywhere message too big...\n"); impl->multi_thread_source.free(mem); } + va_end(va); } diff --git a/src/Misc/Part.cpp b/src/Misc/Part.cpp @@ -286,7 +286,7 @@ static const Ports kitPorts = { const Ports &Part::Kit::ports = kitPorts; const Ports &Part::ports = partPorts; -Part::Part(Allocator &alloc, const SYNTH_T &synth_, const AbsTime &time_, +Part::Part(Allocator &alloc, const SYNTH_T &synth_, const AbsTime &time_, Sync* sync_, const int &gzip_compression, const int &interpolation, Microtonal *microtonal_, FFTwrapper *fft_, WatchManager *wm_, const char *prefix_) :Pdrummode(false), @@ -302,6 +302,7 @@ Part::Part(Allocator &alloc, const SYNTH_T &synth_, const AbsTime &time_, memory(alloc), synth(synth_), time(time_), + sync(sync_), gzip_compression(gzip_compression), interpolation(interpolation) { @@ -326,7 +327,7 @@ Part::Part(Allocator &alloc, const SYNTH_T &synth_, const AbsTime &time_, //Part's Insertion Effects init for(int nefx = 0; nefx < NUM_PART_EFX; ++nefx) { - partefx[nefx] = new EffectMgr(memory, synth, 1, &time); + partefx[nefx] = new EffectMgr(memory, synth, 1, &time, sync); Pefxbypass[nefx] = false; } assert(partefx[0]); diff --git a/src/Misc/Part.h b/src/Misc/Part.h @@ -32,7 +32,7 @@ class Part /**Constructor * @param microtonal_ Pointer to the microtonal object * @param fft_ Pointer to the FFTwrapper*/ - Part(Allocator &alloc, const SYNTH_T &synth, const AbsTime &time, + Part(Allocator &alloc, const SYNTH_T &synth, const AbsTime &time, Sync* sync, const int& gzip_compression, const int& interpolation, Microtonal *microtonal_, FFTwrapper *fft_, WatchManager *wm=0, const char *prefix=0); /**Destructor*/ @@ -231,6 +231,7 @@ class Part Allocator &memory; const SYNTH_T &synth; const AbsTime &time; + Sync* sync; const int &gzip_compression, &interpolation; }; diff --git a/src/Misc/Sync.h b/src/Misc/Sync.h @@ -0,0 +1,59 @@ +/* + ZynAddSubFX - a software synthesizer + + Sync.h - allow sync callback using observer pattern + Copyright (C) 2024 Michael Kirchner + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. +*/ +#pragma once +#include <algorithm> // for std::find + +namespace zyn { + +#define MAX_OBSERVERS 8 + +class Observer { +public: + virtual void update() = 0; +}; + + +class Sync { +public: + Sync() {observerCount = 0;} + void attach(Observer* observer) { + if (observerCount >= MAX_OBSERVERS) { + return; // No space left to attach a new observer + } + // Check if already attached + if (std::find(observers, observers + observerCount, observer) != observers + observerCount) { + return; // Observer already attached + } + observers[observerCount++] = observer; + } + void detach(const Observer* observer) { + // Find the observer + auto it = std::find(observers, observers + observerCount, observer); + if (it == observers + observerCount) { + return; // Observer not found + } + // Remove the observer by shifting the rest + std::move(it + 1, observers + observerCount, it); + --observerCount; + } + void notify() { + for (int i = 0; i < observerCount; ++i) { + observers[i]->update(); + } + } + +private: + Observer* observers[MAX_OBSERVERS]; + int observerCount; // Current number of observers +}; + +} diff --git a/src/Misc/Time.h b/src/Misc/Time.h @@ -19,15 +19,32 @@ class AbsTime { public: AbsTime(const SYNTH_T &synth) - :frames(0), - s(synth){}; + :tempo(120), + hostSamples(0), + bar(0), + beat(0), + tick(0.0f), + bpm(120.0f), + ppq(1920.0f), + frames(0), + s(synth) {}; void operator++(){++frames;}; void operator++(int){frames++;}; int64_t time() const {return frames;}; unsigned int tempo; + int hostSamples; + int bar; + int beat; + float tick; + float beatsPerBar; + float beatType; + float bpm; + float ppq; + bool playing; float dt() const { return s.dt(); } float framesPerSec() const { return 1/s.dt();} int samplesPerFrame() const {return s.buffersize;} + int samplerate() const {return s.buffersize / s.dt();} private: int64_t frames; const SYNTH_T &s; diff --git a/src/Nio/AlsaEngine.cpp b/src/Nio/AlsaEngine.cpp @@ -221,9 +221,17 @@ void *AlsaEngine::MidiThread(void) break; default: - if(true) - cout << "Info, other non-handled midi event, type: " - << (int)event->type << endl; + for (unsigned int x = 0; x < event->data.ext.len; x += 3) { + uint8_t buf[3]; + int y = event->data.ext.len - x; + if (y >= 3) { + memcpy(buf, (uint8_t *)event->data.ext.ptr + x, 3); + } else { + memset(buf, 0, sizeof(buf)); + memcpy(buf, (uint8_t *)event->data.ext.ptr + x, y); + } + midiProcess(buf[0], buf[1], buf[2]); + } break; } snd_seq_free_event(event); diff --git a/src/Plugin/ZynAddSubFX/DistrhoPluginInfo.h b/src/Plugin/ZynAddSubFX/DistrhoPluginInfo.h @@ -42,6 +42,7 @@ #define DISTRHO_PLUGIN_WANT_PROGRAMS 1 #define DISTRHO_PLUGIN_WANT_STATE 1 #define DISTRHO_PLUGIN_WANT_FULL_STATE 1 +#define DISTRHO_PLUGIN_WANT_TIMEPOS 1 enum Parameters { kParamSlot1, diff --git a/src/Plugin/ZynAddSubFX/ZynAddSubFX.cpp b/src/Plugin/ZynAddSubFX/ZynAddSubFX.cpp @@ -141,7 +141,6 @@ public: protected: /* -------------------------------------------------------------------------------------------------------- * Information */ - /** Get the plugin label. This label is a short restricted name consisting of only _, a-z, A-Z and 0-9 characters. @@ -343,6 +342,11 @@ protected: */ void run(const float**, float** outputs, uint32_t frames, const MidiEvent* midiEvents, uint32_t midiEventCount) override { + + // Zeitposition vom Host abfragen + const TimePosition& timePosition = getTimePosition(); + + if (! mutex.tryLock()) { //if (! isOffline()) @@ -351,7 +355,6 @@ protected: std::memset(outputs[1], 0, sizeof(float)*frames); return; } - mutex.lock(); } @@ -370,8 +373,10 @@ protected: if (midiEvent.frame > framesOffset) { - master->GetAudioOutSamples(midiEvent.frame-framesOffset, synth.samplerate, outputs[0]+framesOffset, - outputs[1]+framesOffset); + master->GetAudioOutSamples(midiEvent.frame-framesOffset, synth.samplerate, + outputs[0]+framesOffset, + outputs[1]+framesOffset); + framesOffset = midiEvent.frame; } @@ -444,9 +449,24 @@ protected: } } - if (frames > framesOffset) - master->GetAudioOutSamples(frames-framesOffset, synth.samplerate, outputs[0]+framesOffset, - outputs[1]+framesOffset); + if (timePosition.bbt.valid) + master->GetAudioOutSamples(frames-framesOffset, synth.samplerate, + outputs[0]+framesOffset, + outputs[1]+framesOffset, + timePosition.bbt.bar, + timePosition.bbt.beat, + timePosition.bbt.tick, + timePosition.bbt.beatsPerBar, + timePosition.bbt.beatType, + timePosition.bbt.beatsPerMinute, + timePosition.bbt.ticksPerBeat, + timePosition.playing, + frames); + + else + master->GetAudioOutSamples(frames-framesOffset, synth.samplerate, + outputs[0]+framesOffset, + outputs[1]+framesOffset); mutex.unlock(); } diff --git a/src/Tests/AllocatorTest.cpp b/src/Tests/AllocatorTest.cpp @@ -82,13 +82,13 @@ class AllocatorTest //We should be able to see that a chunk enters and exits the free //state - char *mem2 = (char*)memory.alloc_mem(10*1024*1024); + char *mem2 = (char*)memory.alloc_mem(16*1024*1024); TS_NON_NULL(mem2); TS_ASSERT(!memory.memFree(bufA)); memory.dealloc_mem(mem2); TS_ASSERT(memory.memFree(bufA)); - mem2 = (char*)memory.alloc_mem(10*1024*1024); - char *mem3 = (char*)memory.alloc_mem(10*1024*1024); + mem2 = (char*)memory.alloc_mem(16*1024*1024); + char *mem3 = (char*)memory.alloc_mem(16*1024*1024); TS_NON_NULL(mem3); memory.dealloc_mem(mem2); TS_ASSERT(!memory.memFree(bufA)); diff --git a/src/Tests/CMakeLists.txt b/src/Tests/CMakeLists.txt @@ -47,6 +47,7 @@ quick_test(TriggerTest ${test_lib}) quick_test(UnisonTest ${test_lib}) quick_test(WatchTest ${test_lib}) quick_test(XMLwrapperTest ${test_lib}) +quick_test(ReverseTest ${test_lib}) quick_test(PluginTest zynaddsubfx_core zynaddsubfx_nio zynaddsubfx_gui_bridge diff --git a/src/Tests/InstrumentStats.cpp b/src/Tests/InstrumentStats.cpp @@ -15,6 +15,7 @@ #include <fstream> #include <string> #include "../Misc/Time.h" +#include "../Misc/Sync.h" #include "../Misc/MiddleWare.h" #include "../Misc/Part.h" #include "../Misc/Util.h" @@ -34,6 +35,7 @@ using namespace zyn; SYNTH_T *synth; AbsTime *time_; +Sync *sync_; char *instance_name=(char*)""; MiddleWare *middleware; @@ -74,6 +76,7 @@ void setup() { synth->samplerate = 48000; synth->alias(); time_ = new AbsTime(*synth); + sync_ = new Sync(); //for those patches that are just really big alloc.addMemory(malloc(1024*1024),1024*1024); @@ -84,7 +87,7 @@ void setup() { for(int i = 0; i < synth->buffersize; ++i) outR[i] = 0.0f; - p = new Part(alloc, *synth, *time_, compress, interp, &microtonal, &fft); + p = new Part(alloc, *synth, *time_, sync_, compress, interp, &microtonal, &fft); } void xml(string s) diff --git a/src/Tests/KitTest.cpp b/src/Tests/KitTest.cpp @@ -15,6 +15,7 @@ #include <cstdlib> #include <iostream> #include "../Misc/Time.h" +#include "../Misc/Sync.h" #include "../Misc/Allocator.h" #include "../DSP/FFTwrapper.h" #include "../Misc/Microtonal.h" @@ -50,6 +51,7 @@ class KitTest Microtonal microtonal; Part *part; AbsTime *time; + Sync *sync; float *outL, *outR; static int getSynthTDefaultOscilSize() { @@ -64,12 +66,13 @@ class KitTest void setUp() { synth = new SYNTH_T; time = new AbsTime(*synth); + sync = new Sync(); outL = new float[synth->buffersize]; outR = new float[synth->buffersize]; memset(outL, 0, synth->bufferbytes); memset(outR, 0, synth->bufferbytes); - part = new Part(alloc, *synth, *time, dummy, dummy, &microtonal, &fft); + part = new Part(alloc, *synth, *time, sync, dummy, dummy, &microtonal, &fft); } //Standard poly mode with sustain diff --git a/src/Tests/PortChecker.cpp b/src/Tests/PortChecker.cpp @@ -148,6 +148,7 @@ class PortChecker synth = new zyn::SYNTH_T; synth->buffersize = 256; synth->samplerate = 48000; + synth->oscilsize = 256; synth->alias(); mw = new zyn::MiddleWare(std::move(*synth), &config); diff --git a/src/Tests/ReverseTest.cpp b/src/Tests/ReverseTest.cpp @@ -0,0 +1,139 @@ +/* + ZynAddSubFX - a software synthesizer + + ReverseTest.h - CxxTest for Effect/Reverse + Copyright (C) 2024 Mark McCurry + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. +*/ +#include "test-suite.h" +#include <cmath> +#include <cstdlib> +#include <iostream> +#include "../Effects/Reverse.h" +#include "../Misc/Allocator.h" +#include "../Misc/Time.h" +#include "../globals.h" + +using namespace std; +using namespace zyn; + +SYNTH_T *synth; + +class ReverseTest +{ + public: + void setUp() { + synth = new SYNTH_T; + time = new AbsTime(*synth); + outL = new float[synth->buffersize]; + for(int i = 0; i < synth->buffersize; ++i) + outL[i] = 0.0f; + outR = new float[synth->buffersize]; + for(int i = 0; i < synth->buffersize; ++i) + outR[i] = 0.0f; + input = new Stereo<float *>(new float[synth->buffersize], + new float[synth->buffersize]); + for(int i = 0; i < synth->buffersize; ++i) + input->l[i] = input->r[i] = 0.0f; + EffectParams pars{alloc,true, outL, outR, 0, 44100, 256, nullptr}; + testFX = new Reverse(pars,time); + } + + void tearDown() { + delete[] input->r; + delete[] input->l; + delete input; + delete[] outL; + delete[] outR; + delete testFX; + delete time; + delete synth; + } + + + void testInit() { + //Make sure that the output will be zero at start + //(given a zero input) + testFX->out(*input); + for(int i = 0; i < synth->buffersize; ++i) { + TS_ASSERT_DELTA(outL[i], 0.0f, 0.0001f); + TS_ASSERT_DELTA(outR[i], 0.0f, 0.0001f); + } + } + + void testClear() { + char DELAY = 2; + char MODE = 6; + testFX->changepar(DELAY, 127); + testFX->changepar(MODE, 2); + + //flood with high input + for(int i = 0; i < synth->buffersize; ++i) + input->r[i] = input->l[i] = 1.0f; + + for(int i = 0; i < 5000; ++i) + testFX->out(*input); + for(int i = 0; i < synth->buffersize; ++i) { + TS_ASSERT(outL[i] != 0.0f); + TS_ASSERT(outR[i] != 0.0f); + } + //After making sure the internal buffer has a nonzero value + //cleanup + //Then get the next output, which should be zereoed out if DELAY + //is large enough + testFX->cleanup(); + testFX->out(*input); + for(int i = 0; i < synth->buffersize; ++i) { + TS_ASSERT_DELTA(outL[i], 0.0f, 0.0001f); + TS_ASSERT_DELTA(outR[i], 0.0f, 0.0001f); + } + } + //Insures that the proper decay occurs with high feedback + void testRandom() { + const int steps = 100000; + for(int i = 0; i < steps; ++i) { + + //input is [-0.5..0.5] + for(int j = 0; j < synth->buffersize; ++j) + input->r[j] = input->l[j] = RND-0.5; + + for(int j = 0; j < 6; ++j) { + if(RND < 0.01) {//1% chance a paramter change occurs + int value = prng()%128; + if(j == 6) + value = prng()%8; + testFX->changepar(j, value); + } + } + + testFX->out(*input); + + for(int i = 0; i < synth->buffersize; ++i) { + TS_ASSERT(fabsf(outL[i]) < 0.75); + TS_ASSERT(fabsf(outR[i]) < 0.75); + } + } + } + + + private: + Stereo<float *> *input; + float *outR, *outL; + AbsTime *time; + Reverse *testFX; + Alloc alloc; +}; + +int main() +{ + tap_quiet = 1; + ReverseTest test; + RUN_TEST(testInit); + RUN_TEST(testClear); + RUN_TEST(testRandom); + return test_summary(); +} diff --git a/src/globals.h b/src/globals.h @@ -73,6 +73,9 @@ class SVFilter; class FormantFilter; class ModFilter; +class Sync; + + typedef float fftwf_real; typedef std::complex<fftwf_real> fft_t; @@ -180,6 +183,11 @@ typedef std::complex<fftwf_real> fft_t; #define FF_MAX_FORMANTS 12 #define FF_MAX_SEQUENCE 8 +/* + * Maximum length of the reverse delay effect + */ +#define MAX_REV_DELAY_SECONDS 4.0f + #define MAX_PRESETTYPE_SIZE 30 #define LOG_2 0.693147181f