commit 613658446bf6744a64b4436a744bc4c25af56a3c
parent d3b3abd08c42333a95bbaa44214336fe0b7a2437
Author: friedolino78 <[email protected]>
Date: Sun, 16 Feb 2025 00:50:26 +0100
Add reversed delay effect (#321)
Diffstat:
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, µtonal, 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, µtonal, &fft);
+ p = new Part(alloc, *synth, *time_, sync_, compress, interp, µtonal, &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, µtonal, &fft);
+ part = new Part(alloc, *synth, *time, sync, dummy, dummy, µtonal, &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