From fee323af75c2aa5406295f8ab461dcf162f7f5ef Mon Sep 17 00:00:00 2001 From: Jeff Martin Date: Wed, 27 May 2026 11:02:08 -0500 Subject: [PATCH 01/25] added ImpulseDropout and ImpulseJitter --- CMakeLists.txt | 2 + src/generators/ImpulseDropout.schelp | 50 ++ src/generators/ImpulseJitter.schelp | 48 ++ src/generators/arrayheap.hpp | 87 ++++ src/generators/generators.cpp | 668 ++++++++++++++++++++++++++- src/generators/generators.sc | 26 ++ 6 files changed, 880 insertions(+), 1 deletion(-) create mode 100644 src/generators/ImpulseDropout.schelp create mode 100644 src/generators/ImpulseJitter.schelp create mode 100644 src/generators/arrayheap.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index c6a06f1..313d504 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -124,6 +124,8 @@ install(FILES PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ ) install(FILES + src/generators/ImpulseDropout.schelp + src/generators/ImpulseJitter.schelp src/generators/LoopPhasor.schelp src/pv/PV_CFreeze.schelp src/pv/PV_MagMirror.schelp diff --git a/src/generators/ImpulseDropout.schelp b/src/generators/ImpulseDropout.schelp new file mode 100644 index 0000000..8c16a1d --- /dev/null +++ b/src/generators/ImpulseDropout.schelp @@ -0,0 +1,50 @@ +class:: ImpulseDropout +summary:: Modified Impulse with dropout +related:: Classes/Impulse, Classes/Dust +categories:: Libraries>JeffUGens, UGens>Generators>Stochastic + + +Description:: + +ImpulseDropout is a modified version of Impulse that randomly drops a percentage of impulses, +controlled by the dropFrac argument. For example, if dropFrac = 0.2, 0.2 of the impulses +will be zeroed out per audio block. The impulses that are zeroed out are chosen randomly. + +classmethods:: + +method::ar, kr + +argument::freq +Frequency in Hertz. freq may be negative. + +argument::phase +Phase offset in cycles (0..1). Staying in this range offers a slight efficiency advantage, +though phase offsets outside this range are supported and wrapped internally. + +argument::dropFrac +The percentage of impulses that will be randomly dropped (between 0.0 and 1.0). +Values greater than or equal to 1.0 result in silence (all impulses are dropped), +and values less than or equal to 0.0 produce the same output as the Impulse UGen. + +argument::mul +The output will be multiplied by this value. + +argument::add +This value will be added to the output. + +Examples:: + +code:: +( +SynthDef(\dropout, { + arg freq, dropFrac, amp; + var sig; + sig = ImpulseDropout.ar(freq, 0.0, dropFrac, amp); + sig = LPF.ar(sig, freq); + sig = LeakDC.ar(sig); + Out.ar(0, sig); +}).add; + +Synth(\dropout, [\freq, 440.0, \dropFrac, 0.4, \amp, -12.dbamp]); +) +:: \ No newline at end of file diff --git a/src/generators/ImpulseJitter.schelp b/src/generators/ImpulseJitter.schelp new file mode 100644 index 0000000..8aebdee --- /dev/null +++ b/src/generators/ImpulseJitter.schelp @@ -0,0 +1,48 @@ +class:: ImpulseJitter +summary:: Modified Impulse with jitter +related:: Classes/Impulse, Classes/Dust +categories:: Libraries>JeffUGens, UGens>Generators>Stochastic + + +Description:: + +ImpulseJitter is a modified version of Impulse that adds some jitter to each impulse position, +controlled by the jitterFrac argument. For example, if jitterFrac = 0.2, each impulse can be +randomly shifted in time by up to 0.2 of the block size. + +classmethods:: + +method::ar, kr + +argument::freq +Frequency in Hertz. freq may be negative. + +argument::phase +Phase offset in cycles (0..1). Staying in this range offers a slight efficiency advantage, +though phase offsets outside this range are supported and wrapped internally. + +argument::jitterFrac +The maximum fraction of the block size to allow for jitter. + +argument::mul +The output will be multiplied by this value. + +argument::add +This value will be added to the output. + +Examples:: + +code:: +( +SynthDef(\jitter, { + arg freq, jitterFrac, amp; + var sig; + sig = ImpulseJitter.ar(freq, 0.0, jitterFrac, amp); + sig = LPF.ar(sig, freq); + sig = LeakDC.ar(sig); + Out.ar(0, sig); +}).add; + +Synth(\jitter, [\freq, 440.0, \jitterFrac, 0.1, \amp, -12.dbamp]); +) +:: \ No newline at end of file diff --git a/src/generators/arrayheap.hpp b/src/generators/arrayheap.hpp new file mode 100644 index 0000000..26f9a43 --- /dev/null +++ b/src/generators/arrayheap.hpp @@ -0,0 +1,87 @@ +// A min heap for ints. +typedef struct { + int* heap; + size_t size; + size_t maxSize; +} IntMinHeap; + +// Inserts into the heap +int heapInsert(IntMinHeap* heap, int data) { + if (heap->size == heap->maxSize) { + return 0; + } else { + if (heap->size == 0) { + heap->size++; + } + size_t idx = heap->size; + heap->heap[idx] = data; + heap->size++; + + // Bubble up + while (idx > 1) { + size_t parentIdx = idx; + if (parentIdx % 2 == 1) + parentIdx--; + parentIdx >>= 1; + if (heap->heap[idx] < heap->heap[parentIdx]) { + int swapVal = heap->heap[idx]; + heap->heap[idx] = heap->heap[parentIdx]; + heap->heap[parentIdx] = swapVal; + } + idx = parentIdx; + } + return 1; + } +} + +// Removes from the heap and returns the value popped. Returns 0 if the heap is empty. +int heapPop(IntMinHeap* heap) { + if (heap->size > 1) { + int val = heap->heap[1]; + heap->heap[1] = heap->heap[heap->size-1]; + heap->size--; + size_t idx = 1; + // Bubble down + while (idx < heap->size) { + size_t leftChild = idx * 2; + size_t rightChild = leftChild + 1; + if (rightChild < heap->size) { + if (heap->heap[leftChild] <= heap->heap[rightChild] && heap->heap[idx] > heap->heap[leftChild]) { + int swapVal = heap->heap[leftChild]; + heap->heap[leftChild] = heap->heap[idx]; + heap->heap[idx] = swapVal; + idx = leftChild; + } else if (heap->heap[leftChild] > heap->heap[rightChild] && heap->heap[idx] > heap->heap[rightChild]) { + int swapVal = heap->heap[rightChild]; + heap->heap[rightChild] = heap->heap[idx]; + heap->heap[idx] = swapVal; + idx = rightChild; + } else { + break; + } + } else if (leftChild < heap->size) { + if (heap->heap[idx] > heap->heap[leftChild]) { + int swapVal = heap->heap[leftChild]; + heap->heap[leftChild] = heap->heap[idx]; + heap->heap[idx] = swapVal; + idx = leftChild; + } + break; + } else { + break; + } + } + return val; + } else { + return 0; + } +} + +// Safe peek at the top of the heap +inline int heapPeek(IntMinHeap* heap) { + if (heap->size > 1) { + return heap->heap[1]; + } else { + return -1; + } +} diff --git a/src/generators/generators.cpp b/src/generators/generators.cpp index 9583344..5576c3d 100644 --- a/src/generators/generators.cpp +++ b/src/generators/generators.cpp @@ -23,9 +23,673 @@ along with this program. If not, see . */ #include "SC_PlugIn.h" +#include "arrayheap.hpp" +#define HEAP_MAX_SIZE 1024 static InterfaceTable *ft; +// Represents an ImpulseDropout UGen. +struct ImpulseDropout : public Unit { + double mPhase, mPhaseOffset, mPhaseIncrement; + float mFreqMul; +}; + +void ImpulseDropout_Ctor(ImpulseDropout* unit); +void ImpulseDropout_next_aa(ImpulseDropout* unit, int inNumSamples); +void ImpulseDropout_next_ai(ImpulseDropout* unit, int inNumSamples); +void ImpulseDropout_next_ak(ImpulseDropout* unit, int inNumSamples); +void ImpulseDropout_next_ki(ImpulseDropout* unit, int inNumSamples); +void ImpulseDropout_next_kk(ImpulseDropout* unit, int inNumSamples); + +// This is a copy of the static function from LFUGens.cpp in server/plugins. +// It detects if a phasor is out-of-bounds, triggers, and wraps [0, 1]. +static inline float testWrapPhase(double prev_inc, double& phase) { + if (prev_inc < 0.f) { // negative freqs + if (phase <= 0.f) { + phase += 1.f; + if (phase <= 0.f) { // catch large phase jumps + phase -= sc_ceil(phase); + } + return 1.f; + } else { + return 0.f; + } + } else { // positive freqs + if (phase >= 1.f) { + phase -= 1.f; + if (phase >= 1.f) { + phase -= sc_floor(phase); + } + return 1.f; + } else { + return 0.f; + } + } +} + +void ImpulseDropout_next_aa(ImpulseDropout* unit, int inNumSamples) { + float* out = OUT(0); + float* freqIn = IN(0); + float* offIn = IN(1); + float dropProbIn = IN0(2); + + // Collect UGen state + double phase = unit->mPhase; + double inc = unit->mPhaseIncrement; + float freqMul = unit->mFreqMul; + double prevOff = unit->mPhaseOffset; + + RGET + for (int xxn = 0; xxn < inNumSamples; xxn++) { + float impulseResult = testWrapPhase(inc, phase); + // Drop the impulse if necessary + if (impulseResult > 0.5f && rgen.frand() < dropProbIn) { + impulseResult = 0.f; + } + double off = static_cast(offIn[xxn]); + double offInc = off - prevOff; + phase += offInc; + testWrapPhase(inc, phase); + inc = freqIn[xxn] * freqMul; + out[xxn] = impulseResult; + phase += inc; + prevOff = off; + } + + unit->mPhase = phase; + unit->mPhaseOffset = prevOff; + unit->mPhaseIncrement = inc; +} + +void ImpulseDropout_next_ai(ImpulseDropout* unit, int inNumSamples) { + float* out = OUT(0); + float freqIn = IN0(0); + float dropProbIn = IN0(2); + + // Collect UGen state + double phase = unit->mPhase; + double inc = unit->mPhaseIncrement; + float freqMul = unit->mFreqMul; + + RGET + for (int xxn = 0; xxn < inNumSamples; xxn++) { + float impulseResult = testWrapPhase(inc, phase); + // Drop the impulse if necessary + if (impulseResult > 0.5f && rgen.frand() < dropProbIn) { + impulseResult = 0.f; + } + inc = freqIn * freqMul; + out[xxn] = impulseResult; + phase += inc; + } + + unit->mPhase = phase; + unit->mPhaseIncrement = inc; +} + +void ImpulseDropout_next_ak(ImpulseDropout* unit, int inNumSamples) { + float* out = OUT(0); + float freqIn = IN0(0); + double off = IN0(1); + float dropProbIn = IN0(2); + + // Collect UGen state + double phase = unit->mPhase; + double inc = unit->mPhaseIncrement; + float freqMul = unit->mFreqMul; + double prevOff = unit->mPhaseOffset; + + double offSlope = CALCSLOPE(off, prevOff); + bool offChanged = offSlope != 0.f; + + RGET + for (int xxn = 0; xxn < inNumSamples; xxn++) { + float impulseResult = testWrapPhase(inc, phase); + // Drop the impulse if necessary + if (impulseResult > 0.5f && rgen.frand() < dropProbIn) { + impulseResult = 0.f; + } + if (offChanged) { + phase += offSlope; + testWrapPhase(inc, phase); + } + inc = freqIn * freqMul; + out[xxn] = impulseResult; + phase += inc; + } + + unit->mPhase = phase; + unit->mPhaseOffset = off; + unit->mPhaseIncrement = inc; +} + +void ImpulseDropout_next_ki(ImpulseDropout* unit, int inNumSamples) { + float* out = OUT(0); + double inc = IN0(0) * unit->mFreqMul; + float dropProbIn = IN0(2); + + // Collect UGen state + double phase = unit->mPhase; + double prevInc = unit->mPhaseIncrement; + + double incSlope = CALCSLOPE(inc, prevInc); + + RGET + for (int xxn = 0; xxn < inNumSamples; xxn++) { + float impulseResult = testWrapPhase(prevInc, phase); + // Drop the impulse if necessary + if (impulseResult > 0.5f && rgen.frand() < dropProbIn) { + impulseResult = 0.f; + } + out[xxn] = impulseResult; + prevInc += incSlope; + phase += prevInc; + } + + unit->mPhase = phase; + unit->mPhaseIncrement = inc; +} + +void ImpulseDropout_next_kk(ImpulseDropout* unit, int inNumSamples) { + float* out = OUT(0); + double inc = IN0(0) * unit->mFreqMul; + double off = IN0(1); + float dropProbIn = IN0(2); + + // Collect UGen state + double phase = unit->mPhase; + double prevInc = unit->mPhaseIncrement; + double prevOff = unit->mPhaseOffset; + + double incSlope = CALCSLOPE(inc, prevInc); + double phaseSlope = CALCSLOPE(off, prevOff); + bool phOffChanged = phaseSlope != 0.f; + + RGET + for (int xxn = 0; xxn < inNumSamples; xxn++) { + float impulseResult = testWrapPhase(prevInc, phase); + // Drop the impulse if necessary + if (impulseResult > 0.5f && rgen.frand() < dropProbIn) { + impulseResult = 0.f; + } + if (phOffChanged) { + phase += phaseSlope; + testWrapPhase(prevInc, phase); + } + out[xxn] = impulseResult; + prevInc += incSlope; + phase += prevInc; + } + + unit->mPhase = phase; + unit->mPhaseOffset = off; + unit->mPhaseIncrement = inc; +} + +void ImpulseDropout_next_ii(ImpulseDropout* unit, int inNumSamples) { + float* out = OUT(0); + float dropProbIn = IN0(2); + + // Collect UGen state + double inc = unit->mPhaseIncrement; + double phase = unit->mPhase; + + RGET + for (int xxn = 0; xxn < inNumSamples; xxn++) { + float impulseResult = testWrapPhase(inc, phase); + // Drop the impulse if necessary + if (impulseResult > 0.5f && rgen.frand() < dropProbIn) { + impulseResult = 0.f; + } + out[xxn] = impulseResult; + phase += inc; + } + + unit->mPhase = phase; +} + +void ImpulseDropout_next_ik(ImpulseDropout* unit, int inNumSamples) { + float* out = OUT(0); + double off = IN0(1); + float dropProbIn = IN0(2); + + // Collect UGen state + double phase = unit->mPhase; + double inc = unit->mPhaseIncrement; + double prevOff = unit->mPhaseOffset; + + double phaseSlope = CALCSLOPE(off, prevOff); + bool phOffChanged = phaseSlope != 0.f; + + RGET + for (int xxn = 0; xxn < inNumSamples; xxn++) { + float impulseResult = testWrapPhase(inc, phase); + // Drop the impulse if necessary + if (impulseResult > 0.5f && rgen.frand() < dropProbIn) { + impulseResult = 0.f; + } + if (phOffChanged) { + phase += phaseSlope; + testWrapPhase(inc, phase); + } + out[xxn] = impulseResult; + phase += inc; + } + + unit->mPhase = phase; + unit->mPhaseOffset = off; +} + +// Construct the ImpulseDropout +void ImpulseDropout_Ctor(ImpulseDropout* unit) { + unit->mPhaseIncrement = IN0(0) * unit->mFreqMul; + unit->mPhaseOffset = IN0(1); + unit->mFreqMul = static_cast(unit->mRate->mSampleDur); + + double initOff = unit->mPhaseOffset; + double initInc = unit->mPhaseIncrement; + double initPhase = sc_wrap(initOff, 0.0, 1.0); + + // Initial phase offset of 0 means output of 1 on first sample. + // Set phase to wrap point to trigger impulse on first sample + if (initPhase == 0.0 && initInc >= 0.0) { + initPhase = 1.0; // positive frequency trigger/wrap position + } + unit->mPhase = initPhase; + + UnitCalcFunc func; + switch (INRATE(0)) { + case calc_FullRate: + switch (INRATE(1)) { + case calc_ScalarRate: + func = (UnitCalcFunc)ImpulseDropout_next_ai; + break; + case calc_BufRate: + func = (UnitCalcFunc)ImpulseDropout_next_ak; + break; + case calc_FullRate: + func = (UnitCalcFunc)ImpulseDropout_next_aa; + break; + } + break; + case calc_BufRate: + if (INRATE(1) == calc_ScalarRate) { + func = (UnitCalcFunc)ImpulseDropout_next_ki; + } else { + func = (UnitCalcFunc)ImpulseDropout_next_kk; + } + break; + case calc_ScalarRate: + if (INRATE(1) == calc_ScalarRate) { + func = (UnitCalcFunc)ImpulseDropout_next_ki; + } else { + func = (UnitCalcFunc)ImpulseDropout_next_kk; + } + break; + } + unit->mCalcFunc = func; + func(unit, 1); + + unit->mPhase = initPhase; + unit->mPhaseOffset = initOff; + unit->mPhaseIncrement = initInc; +} + +// Represents an ImpulseJitter UGen. +struct ImpulseJitter : public Unit { + double mPhase, mPhaseOffset, mPhaseIncrement; + float mFreqMul; + IntMinHeap mImpulseHeap; +}; + +void ImpulseJitter_next_aa(ImpulseJitter* unit, int inNumSamples); +void ImpulseJitter_next_ai(ImpulseJitter* unit, int inNumSamples); +void ImpulseJitter_next_ak(ImpulseJitter* unit, int inNumSamples); +void ImpulseJitter_next_ki(ImpulseJitter* unit, int inNumSamples); +void ImpulseJitter_next_kk(ImpulseJitter* unit, int inNumSamples); +void ImpulseJitter_Ctor(ImpulseJitter* unit); +void ImpulseJitter_Dtor(ImpulseJitter* unit); + +void ImpulseJitter_next_aa(ImpulseJitter* unit, int inNumSamples) { + float* out = OUT(0); + float* freq = IN(0); + float* offIn = IN(1); + float jitterFracIn = IN0(2); + + // Collect UGen state + double phase = unit->mPhase; + double inc = unit->mPhaseIncrement; + float freqMul = unit->mFreqMul; + double prevOff = unit->mPhaseOffset; + + // The maximum distance an impulse can be displaced + int jitterWidth = static_cast(jitterFracIn * unit->mImpulseHeap.maxSize); + + // Zero out the output buffer + for (int xxn = 0; xxn < inNumSamples; xxn++) { + out[xxn] = 0.f; + } + + // Update the impulse table indices + for (int xxn = 1; xxn < unit->mImpulseHeap.size; xxn++) { + unit->mImpulseHeap.heap[xxn] -= inNumSamples; + } + + // Retrieve impulses for this block + while (heapPeek(&unit->mImpulseHeap) < inNumSamples && unit->mImpulseHeap.size > 1) { + out[heapPop(&unit->mImpulseHeap)] = 1.f; + } + + RGET + for (int xxn = 0; xxn < inNumSamples; xxn++) { + size_t heapEffectiveSize = static_cast(HEAP_MAX_SIZE / (12 * sc_log2(freq[xxn]))); + float impulseResult = testWrapPhase(inc, phase); + if (impulseResult > 0.5f) { + int idx = rgen.irand(jitterWidth) + xxn; + if (idx < inNumSamples) { + out[idx] = 1.f; + } else if (unit->mImpulseHeap.size < heapEffectiveSize) { + heapInsert(&unit->mImpulseHeap, idx); + } + } + double off = static_cast(offIn[xxn]); + double offInc = off - prevOff; + phase += offInc; + testWrapPhase(inc, phase); + inc = freq[xxn] * freqMul; + phase += inc; + prevOff = off; + } + + unit->mPhase = phase; + unit->mPhaseOffset = prevOff; + unit->mPhaseIncrement = inc; +} + +void ImpulseJitter_next_ai(ImpulseJitter* unit, int inNumSamples) { + float* out = OUT(0); + float freq = IN0(0); + float jitterFracIn = IN0(2); + size_t heapEffectiveSize = static_cast(HEAP_MAX_SIZE / (12 * sc_log2(freq))); + + // Collect UGen state + double phase = unit->mPhase; + double inc = unit->mPhaseIncrement; + float freqMul = unit->mFreqMul; + + // The maximum distance an impulse can be displaced + int jitterWidth = static_cast(jitterFracIn * unit->mImpulseHeap.maxSize); + + // Zero out the output buffer + for (int xxn = 0; xxn < inNumSamples; xxn++) { + out[xxn] = 0.f; + } + + // Update the impulse table indices + for (int xxn = 1; xxn < unit->mImpulseHeap.size; xxn++) { + unit->mImpulseHeap.heap[xxn] -= inNumSamples; + } + + // Retrieve impulses for this block + while (heapPeek(&unit->mImpulseHeap) < inNumSamples && unit->mImpulseHeap.size > 1) { + out[heapPop(&unit->mImpulseHeap)] = 1.f; + } + + RGET + for (int xxn = 0; xxn < inNumSamples; xxn++) { + float impulseResult = testWrapPhase(inc, phase); + if (impulseResult > 0.5f) { + int idx = rgen.irand(jitterWidth) + xxn; + if (idx < inNumSamples) { + out[idx] = 1.f; + } else if (unit->mImpulseHeap.size < heapEffectiveSize) { + heapInsert(&unit->mImpulseHeap, idx); + } + } + inc = freq * freqMul; + phase += inc; + } + + unit->mPhase = phase; + unit->mPhaseIncrement = inc; +} + +void ImpulseJitter_next_ak(ImpulseJitter* unit, int inNumSamples) { + float* out = OUT(0); + float freq = IN0(0); + double off = IN0(1); + float jitterFracIn = IN0(2); + size_t heapEffectiveSize = static_cast(HEAP_MAX_SIZE / (12 * sc_log2(freq))); + + // Collect UGen state + double phase = unit->mPhase; + double inc = unit->mPhaseIncrement; + float freqMul = unit->mFreqMul; + double prevOff = unit->mPhaseOffset; + + double offSlope = CALCSLOPE(off, prevOff); + bool offChanged = offSlope != 0.f; + + // The maximum distance an impulse can be displaced + int jitterWidth = static_cast(jitterFracIn * unit->mImpulseHeap.maxSize); + + // Zero out the output buffer + for (int xxn = 0; xxn < inNumSamples; xxn++) { + out[xxn] = 0.f; + } + + // Update the impulse table indices + for (int xxn = 1; xxn < unit->mImpulseHeap.size; xxn++) { + unit->mImpulseHeap.heap[xxn] -= inNumSamples; + } + + // Retrieve impulses for this block + while (heapPeek(&unit->mImpulseHeap) < inNumSamples && unit->mImpulseHeap.size > 1) { + out[heapPop(&unit->mImpulseHeap)] = 1.f; + } + + RGET + for (int xxn = 0; xxn < inNumSamples; xxn++) { + float impulseResult = testWrapPhase(inc, phase); + if (impulseResult > 0.5f) { + int idx = rgen.irand(jitterWidth) + xxn; + if (idx < inNumSamples) { + out[idx] = 1.f; + } else if (unit->mImpulseHeap.size < heapEffectiveSize) { + heapInsert(&unit->mImpulseHeap, idx); + } + } + if (offChanged) { + phase += offSlope; + testWrapPhase(inc, phase); + } + inc = freq * freqMul; + phase += inc; + } + + unit->mPhase = phase; + unit->mPhaseOffset = off; + unit->mPhaseIncrement = inc; +} + +void ImpulseJitter_next_ki(ImpulseJitter* unit, int inNumSamples) { + float* out = OUT(0); + double freq = IN0(0); + double inc = freq * unit->mFreqMul; + float jitterFracIn = IN0(2); + size_t heapEffectiveSize = static_cast(HEAP_MAX_SIZE / (12 * sc_log2(freq))); + + // Collect UGen state + double phase = unit->mPhase; + double prevInc = unit->mPhaseIncrement; + + double incSlope = CALCSLOPE(inc, prevInc); + + // The maximum distance an impulse can be displaced + int jitterWidth = static_cast(jitterFracIn * unit->mImpulseHeap.maxSize); + + // Zero out the output buffer + for (int xxn = 0; xxn < inNumSamples; xxn++) { + out[xxn] = 0.f; + } + + // Update the impulse table indices + for (int xxn = 1; xxn < unit->mImpulseHeap.size; xxn++) { + unit->mImpulseHeap.heap[xxn] -= inNumSamples; + } + + // Retrieve impulses for this block + while (heapPeek(&unit->mImpulseHeap) < inNumSamples && unit->mImpulseHeap.size > 1) { + out[heapPop(&unit->mImpulseHeap)] = 1.f; + } + + RGET + for (int xxn = 0; xxn < inNumSamples; xxn++) { + float impulseResult = testWrapPhase(prevInc, phase); + if (impulseResult > 0.5f) { + int idx = rgen.irand(jitterWidth) + xxn; + if (idx < inNumSamples) { + out[idx] = 1.f; + } else if (unit->mImpulseHeap.size < heapEffectiveSize) { + heapInsert(&unit->mImpulseHeap, idx); + } + } + prevInc += incSlope; + phase += prevInc; + } + + unit->mPhase = phase; + unit->mPhaseIncrement = inc; +} + +void ImpulseJitter_next_kk(ImpulseJitter* unit, int inNumSamples) { + float* out = OUT(0); + double freq = IN0(0); + double inc = freq * unit->mFreqMul; + double off = IN0(1); + float jitterFracIn = IN0(2); + size_t heapEffectiveSize = static_cast(HEAP_MAX_SIZE / (12 * sc_log2(freq))); + + // Collect UGen state + double phase = unit->mPhase; + double prevInc = unit->mPhaseIncrement; + double prevOff = unit->mPhaseOffset; + + double incSlope = CALCSLOPE(inc, prevInc); + double phaseSlope = CALCSLOPE(off, prevOff); + bool phOffChanged = phaseSlope != 0.f; + + // The maximum distance an impulse can be displaced + int jitterWidth = static_cast(jitterFracIn * unit->mImpulseHeap.maxSize); + + // Zero out the output buffer + for (int xxn = 0; xxn < inNumSamples; xxn++) { + out[xxn] = 0.f; + } + + // Update the impulse table indices + for (int xxn = 1; xxn < unit->mImpulseHeap.size; xxn++) { + unit->mImpulseHeap.heap[xxn] -= inNumSamples; + } + + // Retrieve impulses for this block + while (heapPeek(&unit->mImpulseHeap) < inNumSamples && unit->mImpulseHeap.size > 1) { + out[heapPop(&unit->mImpulseHeap)] = 1.f; + } + + RGET + for (int xxn = 0; xxn < inNumSamples; xxn++) { + float impulseResult = testWrapPhase(prevInc, phase); + if (impulseResult > 0.5f) { + int idx = rgen.irand(jitterWidth) + xxn; + if (idx < inNumSamples) { + out[idx] = 1.f; + } else if (unit->mImpulseHeap.size < heapEffectiveSize) { + heapInsert(&unit->mImpulseHeap, idx); + } + } + if (phOffChanged) { + phase += phaseSlope; + testWrapPhase(prevInc, phase); + } + prevInc += incSlope; + phase += prevInc; + } + + unit->mPhase = phase; + unit->mPhaseOffset = off; + unit->mPhaseIncrement = inc; +} + +// Construct the ImpulseJitter +void ImpulseJitter_Ctor(ImpulseJitter* unit) { + unit->mPhaseIncrement = IN0(0) * unit->mFreqMul; + unit->mPhaseOffset = IN0(1); + unit->mFreqMul = static_cast(unit->mRate->mSampleDur); + unit->mImpulseHeap.maxSize = HEAP_MAX_SIZE; // hard coded for now + unit->mImpulseHeap.size = 1; + unit->mImpulseHeap.heap = (int*)RTAlloc(unit->mWorld, HEAP_MAX_SIZE * sizeof(int)); + + double initOff = unit->mPhaseOffset; + double initInc = unit->mPhaseIncrement; + double initPhase = sc_wrap(initOff, 0.0, 1.0); + + // Initial phase offset of 0 means output of 1 on first sample. + // Set phase to wrap point to trigger impulse on first sample + if (initPhase == 0.0 && initInc >= 0.0) { + initPhase = 1.0; // positive frequency trigger/wrap position + } + unit->mPhase = initPhase; + + UnitCalcFunc func; + switch (INRATE(0)) { + case calc_FullRate: + switch (INRATE(1)) { + case calc_ScalarRate: + func = (UnitCalcFunc)ImpulseJitter_next_ai; + //printf("Calc function set to ai\n"); + break; + case calc_BufRate: + func = (UnitCalcFunc)ImpulseJitter_next_ak; + //printf("Calc function set to ak\n"); + break; + case calc_FullRate: + func = (UnitCalcFunc)ImpulseJitter_next_aa; + //printf("Calc function set to aa\n"); + break; + } + break; + case calc_BufRate: + if (INRATE(1) == calc_ScalarRate) { + func = (UnitCalcFunc)ImpulseJitter_next_ki; + //printf("Calc function set to ki\n"); + } else { + func = (UnitCalcFunc)ImpulseJitter_next_kk; + //printf("Calc function set to kk\n"); + } + break; + case calc_ScalarRate: + if (INRATE(1) == calc_ScalarRate) { + func = (UnitCalcFunc)ImpulseJitter_next_ki; + //printf("Calc function set to ki\n"); + } else { + func = (UnitCalcFunc)ImpulseJitter_next_kk; + //printf("Calc function set to kk\n"); + } + break; + } + unit->mCalcFunc = func; + func(unit, 1); + + unit->mPhase = initPhase; + unit->mPhaseOffset = initOff; + unit->mPhaseIncrement = initInc; +} + +void ImpulseJitter_Dtor(ImpulseJitter* unit) { + RTFree(unit->mWorld, unit->mImpulseHeap.heap); +} + // Represents a LoopPhasor UGen. struct LoopPhasor : public Unit { double m_level; // LoopPhasor output level (position of the phasor between `start` and `end`) @@ -245,7 +909,9 @@ void LoopPhasor_next_aa(LoopPhasor* unit, int inNumSamples) { unit->m_level = level; } -PluginLoad(LoopPhasor) { +PluginLoad(flexplugin_generators) { ft = inTable; + DefineSimpleUnit(ImpulseDropout); + DefineDtorUnit(ImpulseJitter); DefineSimpleUnit(LoopPhasor); } diff --git a/src/generators/generators.sc b/src/generators/generators.sc index 146876a..64694bd 100644 --- a/src/generators/generators.sc +++ b/src/generators/generators.sc @@ -20,6 +20,32 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +// ImpulseDropout is a version of Impulse that randomly drops a percentage of the impulses. +ImpulseDropout : UGen { + *ar { + arg freq = 440.0, phase = 0.0, dropFrac = 0.0, mul = 1.0, add = 0.0; + ^this.multiNew('audio', freq, phase, dropFrac).madd(mul, add); + } + *kr { + arg freq = 440.0, phase = 0.0, dropFrac = 0.0, mul = 1.0, add = 0.0; + ^this.multiNew('control', freq, phase, dropFrac).madd(mul, add); + } + signalRange { ^\unipolar } +} + +// ImpulseJitter is a version of Impulse that allows the addition of jitter to each impulse. +ImpulseJitter : UGen { + *ar { + arg freq = 440.0, phase = 0.0, jitterFrac = 0.0, mul = 1.0, add = 0.0; + ^this.multiNew('audio', freq, phase, jitterFrac).madd(mul, add); + } + *kr { + arg freq = 440.0, phase = 0.0, jitterFrac = 0.0, mul = 1.0, add = 0.0; + ^this.multiNew('control', freq, phase, jitterFrac).madd(mul, add); + } + signalRange { ^\unipolar } +} + // LoopPhasor is a variant of Phasor with the following changes: // 1. It has an embedded loop with start and end position (for playing samples with loop points). // This allows the LoopPhasor to be used for playing a Buffer normally, and then you only loop within a subset of the Buffer. From b45ccb1c1601f1a1f98026c75b6af29320204f4e Mon Sep 17 00:00:00 2001 From: Jeff Martin Date: Sun, 14 Jun 2026 12:35:10 -0500 Subject: [PATCH 02/25] refactored pv code for separate libraries; started on pv stretch --- src/pv/CMakeLists.txt | 5 ++ src/pv/pv.cpp | 205 ++---------------------------------------- src/pv/pv.sc | 8 ++ src/pv/pvCFreeze.cpp | 123 +++++++++++++++++++++++++ src/pv/pvCFreeze.hpp | 42 +++++++++ src/pv/pvOther.cpp | 110 +++++++++++++++++++++++ src/pv/pvOther.hpp | 38 ++++++++ src/pv/pvStretch.cpp | 152 +++++++++++++++++++++++++++++++ src/pv/pvStretch.hpp | 50 +++++++++++ 9 files changed, 533 insertions(+), 200 deletions(-) create mode 100644 src/pv/pvCFreeze.cpp create mode 100644 src/pv/pvCFreeze.hpp create mode 100644 src/pv/pvOther.cpp create mode 100644 src/pv/pvOther.hpp create mode 100644 src/pv/pvStretch.cpp create mode 100644 src/pv/pvStretch.hpp diff --git a/src/pv/CMakeLists.txt b/src/pv/CMakeLists.txt index eebeee9..45c412b 100644 --- a/src/pv/CMakeLists.txt +++ b/src/pv/CMakeLists.txt @@ -1,8 +1,13 @@ # Create the project library add_library(pv MODULE pv.cpp) +add_library(pvCFreeze STATIC pvCFreeze.cpp) +add_library(pvOther STATIC pvOther.cpp) +add_library(pvStretch STATIC pvStretch.cpp) +target_link_libraries(pv PRIVATE pvCFreeze pvOther pvStretch) if(SUPERNOVA) add_library(pv_supernova MODULE pv.cpp) + target_link_libraries(pv_supernova PRIVATE pvCFreeze pvOther pvStretch) set_property(TARGET pv_supernova PROPERTY COMPILE_DEFINITIONS SUPERNOVA) endif() diff --git a/src/pv/pv.cpp b/src/pv/pv.cpp index a7295a7..c2decc0 100644 --- a/src/pv/pv.cpp +++ b/src/pv/pv.cpp @@ -22,213 +22,18 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ -#include "SC_Constants.h" #include "SC_InterfaceTable.h" -#include "FFT_UGens.h" -#include "SC_Unit.h" +#include "pvCFreeze.hpp" +#include "pvOther.hpp" +#include "pvStretch.hpp" InterfaceTable *ft; -struct PV_CFreeze : public Unit { - int mNumBins; // The number of FFT bins - int mNumFrames; // The number of candidate FFT frames to maintain - float *mMags; // The 2D array of FFT mags - float *mDc; // The 1D array of FFT DC values - float *mNyq; // The 1D array of FFT Nyquist values - float *mPhase; // The most recent phase array - float *mPhaseDiffs; // The 2D array of FFT phase differences - size_t mWritePtr; // The write pointer -}; - -struct PV_MagSqueeze : public Unit {}; - -struct PV_MagMirror : public Unit {}; - -struct PV_MagXFade : public Unit {}; - -static void PV_CFreeze_next(PV_CFreeze *unit, int inNumSamples) { - PV_GET_BUF - float freezeState = IN0(1); - // allocate the buffers - if (!unit->mMags) { - // MxN where N is num bins, and M is num frames. Acts as a circular buffer. - unit->mMags = (float*)RTAlloc(unit->mWorld, numbins * sizeof(float) * unit->mNumFrames); - // M (num frames) - unit->mDc = (float*)RTAlloc(unit->mWorld, sizeof(float) * unit->mNumFrames); - // M (num frames) - unit->mNyq = (float*)RTAlloc(unit->mWorld, sizeof(float) * unit->mNumFrames); - // N (num bins) - unit->mPhase = (float*)RTAlloc(unit->mWorld, numbins * sizeof(float)); - // MxN where N is num bins, and M is num frames. - // Acts as a circular buffer corresponding to unit->mMags. - unit->mPhaseDiffs = (float*)RTAlloc(unit->mWorld, numbins * sizeof(float) * unit->mNumFrames); - ClearFFTUnitIfMemFailed(unit->mMags); - ClearFFTUnitIfMemFailed(unit->mDc); - ClearFFTUnitIfMemFailed(unit->mNyq); - ClearFFTUnitIfMemFailed(unit->mPhase); - ClearFFTUnitIfMemFailed(unit->mPhaseDiffs); - unit->mNumBins = numbins; - unit->mWritePtr = 0; - } else if (numbins != unit->mNumBins) { - // Cannot allow the FFT size to change - return; - } - - SCPolarBuf *p = ToPolarApx(buf); - - if (freezeState > 0.f) { - RGET - // Pull random DC and nyquist magnitudes - p->dc = unit->mDc[rgen.irand(unit->mNumFrames)]; - p->nyq = unit->mNyq[rgen.irand(unit->mNumFrames)]; - for (int xxn = 0; xxn < unit->mNumBins; xxn++) { - // For each bin, grab a random magnitude and phase diff pair - int idx = rgen.irand(unit->mNumFrames); - idx = idx * unit->mNumBins + xxn; - p->bin[xxn].mag = unit->mMags[idx]; - unit->mPhase[xxn] = sc_wrap(unit->mPhase[xxn] + unit->mPhaseDiffs[idx], 0.f, static_cast(twopi)); - p->bin[xxn].phase = unit->mPhase[xxn]; - } - } else { - // We're writing to a circular buffer, so pull the current magnitude and phase diff arrays - float *currentMagArr = unit->mMags + (unit->mWritePtr * unit->mNumBins); - float *currentPhaseDiffArr = unit->mPhaseDiffs + (unit->mWritePtr * unit->mNumBins); - for (int xxn = 0; xxn < numbins; xxn++) { - currentMagArr[xxn] = p->bin[xxn].mag; - currentPhaseDiffArr[xxn] = sc_wrap(p->bin[xxn].phase - unit->mPhase[xxn], 0.f, static_cast(twopi)); - unit->mPhase[xxn] = p->bin[xxn].phase; - } - unit->mDc[unit->mWritePtr] = p->dc; - unit->mNyq[unit->mWritePtr] = p->nyq; - unit->mWritePtr++; - unit->mWritePtr %= unit->mNumFrames; - } -} - -static void PV_CFreeze_Ctor(PV_CFreeze *unit) { - SETCALC(PV_CFreeze_next); - OUT0(0) = IN0(0); - unit->mMags = nullptr; - unit->mDc = nullptr; - unit->mNyq = nullptr; - unit->mPhase = nullptr; - unit->mPhaseDiffs = nullptr; - int numFrames = static_cast(IN0(2)); - // prevent the user from doing something nuts - unit->mNumFrames = sc_clip(numFrames, 1, 64); -} - -static void PV_CFreeze_Dtor(PV_CFreeze *unit) { - if (unit->mMags) { - RTFree(unit->mWorld, unit->mMags); - unit->mMags = nullptr; - } - if (unit->mDc) { - RTFree(unit->mWorld, unit->mDc); - unit->mDc = nullptr; - } - if (unit->mNyq) { - RTFree(unit->mWorld, unit->mNyq); - unit->mNyq = nullptr; - } - if (unit->mPhase) { - RTFree(unit->mWorld, unit->mPhase); - unit->mPhase = nullptr; - } - if (unit->mPhaseDiffs) { - RTFree(unit->mWorld, unit->mPhaseDiffs); - unit->mPhaseDiffs = nullptr; - } -} - -static void PV_MagSqueeze_next(PV_MagSqueeze *unit, int inNumSamples) { - PV_GET_BUF - float low = IN0(1); - float high = IN0(2); - SCPolarBuf *p = ToPolarApx(buf); - float min = p->dc; - float max = p->dc; - if (p->nyq < min) - min = p->nyq; - if (p->nyq > max) - max = p->nyq; - for (int i = 0; i < numbins; i++) { - if (p->bin[i].mag < min) - min = p->bin[i].mag; - if (p->bin[i].mag > max) - max = p->bin[i].mag; - } - float range = high - low; - p->dc = (p->dc / max) * range + low; - p->nyq = (p->nyq / max) * range + low; - for (int i = 0; i < numbins; i++) { - p->bin[i].mag = (p->bin[i].mag / max) * range + low; - } -} - -static void PV_MagSqueeze_Ctor(PV_MagSqueeze *unit) { - SETCALC(PV_MagSqueeze_next); - OUT0(0) = IN0(0); -} - -static void PV_MagMirror_next(PV_MagMirror *unit, int inNumSamples) { - PV_GET_BUF - SCPolarBuf *p = ToPolarApx(buf); - float min = p->dc; - float max = p->dc; - if (p->nyq < min) - min = p->nyq; - if (p->nyq > max) - max = p->nyq; - for (int i = 0; i < numbins; i++) { - if (p->bin[i].mag < min) - min = p->bin[i].mag; - if (p->bin[i].mag > max) - max = p->bin[i].mag; - } - p->dc = max - p->dc + min; - p->nyq = max - p->nyq + min; - for (int i = 0; i < numbins; i++) { - p->bin[i].mag = max - p->bin[i].mag + min; - } -} - -static void PV_MagMirror_Ctor(PV_MagMirror *unit) { - SETCALC(PV_MagMirror_next); - OUT0(0) = IN0(0); -} - -static void PV_MagXFade_next(PV_MagXFade *unit, int inNumSamples) { - PV_GET_BUF2 - float crossfade = IN0(2); - crossfade = sc_clip(crossfade, 0.f, 1.f); - SCPolarBuf *p = ToPolarApx(buf1); - SCPolarBuf *q = ToPolarApx(buf2); - // use sqrt crossfade (https://dsp.stackexchange.com/questions/37477/understanding-equal-power-crossfades) - float pCoef = 1.f, qCoef = 0.f; - if (crossfade == 1.f) { - pCoef = 0.f; - qCoef = 1.f; - } else if (crossfade > 0.f) { - pCoef = sc_sqrt(1.f - crossfade); - qCoef = sc_sqrt(crossfade); - } - p->dc = p->dc * pCoef + q->dc * qCoef; - p->nyq = p->nyq * pCoef + q->nyq * qCoef; - for (int i = 0; i < numbins; i++) { - p->bin[i].mag = p->bin[i].mag * pCoef + q->bin[i].mag * qCoef; - } -} - -static void PV_MagXFade_Ctor(PV_MagXFade *unit) { - SETCALC(PV_MagXFade_next); - OUT0(0) = IN0(0); -} - -PluginLoad(PV_Jeff) { +PluginLoad(PV_flexplugins) { ft = inTable; DefineSimpleUnit(PV_MagMirror); DefineSimpleUnit(PV_MagSqueeze); DefineSimpleUnit(PV_MagXFade); + DefineSimpleUnit(PV_PlayBufStretch); DefineDtorUnit(PV_CFreeze); } diff --git a/src/pv/pv.sc b/src/pv/pv.sc index b95376a..542a93c 100644 --- a/src/pv/pv.sc +++ b/src/pv/pv.sc @@ -70,4 +70,12 @@ PV_MagXFade : PV_ChainUGen { arg buffer1, buffer2, fade=0.0; ^this.multiNew('control', buffer1, buffer2, fade); } +} + +// A phase vocoder buffer player +PV_PlayBufStretch : PV_ChainUGen { + *new { + arg buffer, stftBuffer, startPos, rate, loop=0.0, doneAction=0; + ^this.multiNew('control', buffer, stftBuffer, startPos, rate, loop, doneAction); + } } \ No newline at end of file diff --git a/src/pv/pvCFreeze.cpp b/src/pv/pvCFreeze.cpp new file mode 100644 index 0000000..33785f3 --- /dev/null +++ b/src/pv/pvCFreeze.cpp @@ -0,0 +1,123 @@ +/* +File: pvCFreeze.cpp +Author: Jeff Martin + +Description: +Jean-François Charles spectral freeze + +Copyright © 2026 by Jeffrey Martin. All rights reserved. +Website: https://www.jeffreymartincomposer.com + +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 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +#include "pvCFreeze.hpp" +#include "SC_Constants.h" +#include "SC_InterfaceTable.h" +#include "FFT_UGens.h" + +void PV_CFreeze_next(PV_CFreeze *unit, int inNumSamples) { + PV_GET_BUF + float freezeState = IN0(1); + // allocate the buffers + if (!unit->mMags) { + // MxN where N is num bins, and M is num frames. Acts as a circular buffer. + unit->mMags = (float*)RTAlloc(unit->mWorld, numbins * sizeof(float) * unit->mNumFrames); + // M (num frames) + unit->mDc = (float*)RTAlloc(unit->mWorld, sizeof(float) * unit->mNumFrames); + // M (num frames) + unit->mNyq = (float*)RTAlloc(unit->mWorld, sizeof(float) * unit->mNumFrames); + // N (num bins) + unit->mPhase = (float*)RTAlloc(unit->mWorld, numbins * sizeof(float)); + // MxN where N is num bins, and M is num frames. + // Acts as a circular buffer corresponding to unit->mMags. + unit->mPhaseDiffs = (float*)RTAlloc(unit->mWorld, numbins * sizeof(float) * unit->mNumFrames); + ClearFFTUnitIfMemFailed(unit->mMags); + ClearFFTUnitIfMemFailed(unit->mDc); + ClearFFTUnitIfMemFailed(unit->mNyq); + ClearFFTUnitIfMemFailed(unit->mPhase); + ClearFFTUnitIfMemFailed(unit->mPhaseDiffs); + unit->mNumBins = numbins; + unit->mWritePtr = 0; + } else if (numbins != unit->mNumBins) { + // Cannot allow the FFT size to change + return; + } + + SCPolarBuf *p = ToPolarApx(buf); + + if (freezeState > 0.f) { + RGET + // Pull random DC and nyquist magnitudes + p->dc = unit->mDc[rgen.irand(unit->mNumFrames)]; + p->nyq = unit->mNyq[rgen.irand(unit->mNumFrames)]; + for (int xxn = 0; xxn < unit->mNumBins; xxn++) { + // For each bin, grab a random magnitude and phase diff pair + int idx = rgen.irand(unit->mNumFrames); + idx = idx * unit->mNumBins + xxn; + p->bin[xxn].mag = unit->mMags[idx]; + unit->mPhase[xxn] = sc_wrap(unit->mPhase[xxn] + unit->mPhaseDiffs[idx], 0.f, static_cast(twopi)); + p->bin[xxn].phase = unit->mPhase[xxn]; + } + } else { + // We're writing to a circular buffer, so pull the current magnitude and phase diff arrays + float *currentMagArr = unit->mMags + (unit->mWritePtr * unit->mNumBins); + float *currentPhaseDiffArr = unit->mPhaseDiffs + (unit->mWritePtr * unit->mNumBins); + for (int xxn = 0; xxn < numbins; xxn++) { + currentMagArr[xxn] = p->bin[xxn].mag; + currentPhaseDiffArr[xxn] = sc_wrap(p->bin[xxn].phase - unit->mPhase[xxn], 0.f, static_cast(twopi)); + unit->mPhase[xxn] = p->bin[xxn].phase; + } + unit->mDc[unit->mWritePtr] = p->dc; + unit->mNyq[unit->mWritePtr] = p->nyq; + unit->mWritePtr++; + unit->mWritePtr %= unit->mNumFrames; + } +} + +void PV_CFreeze_Ctor(PV_CFreeze *unit) { + SETCALC(PV_CFreeze_next); + OUT0(0) = IN0(0); + unit->mMags = nullptr; + unit->mDc = nullptr; + unit->mNyq = nullptr; + unit->mPhase = nullptr; + unit->mPhaseDiffs = nullptr; + int numFrames = static_cast(IN0(2)); + // prevent the user from doing something nuts + unit->mNumFrames = sc_clip(numFrames, 1, 64); +} + +void PV_CFreeze_Dtor(PV_CFreeze *unit) { + if (unit->mMags) { + RTFree(unit->mWorld, unit->mMags); + unit->mMags = nullptr; + } + if (unit->mDc) { + RTFree(unit->mWorld, unit->mDc); + unit->mDc = nullptr; + } + if (unit->mNyq) { + RTFree(unit->mWorld, unit->mNyq); + unit->mNyq = nullptr; + } + if (unit->mPhase) { + RTFree(unit->mWorld, unit->mPhase); + unit->mPhase = nullptr; + } + if (unit->mPhaseDiffs) { + RTFree(unit->mWorld, unit->mPhaseDiffs); + unit->mPhaseDiffs = nullptr; + } +} diff --git a/src/pv/pvCFreeze.hpp b/src/pv/pvCFreeze.hpp new file mode 100644 index 0000000..d18ccaa --- /dev/null +++ b/src/pv/pvCFreeze.hpp @@ -0,0 +1,42 @@ +/* +File: pvCFreeze.hpp +Author: Jeff Martin + +Description: +Jean-François Charles spectral freeze + +Copyright © 2026 by Jeffrey Martin. All rights reserved. +Website: https://www.jeffreymartincomposer.com + +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 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +#pragma once + +#include "SC_Unit.h" + +struct PV_CFreeze : public Unit { + int mNumBins; // The number of FFT bins + int mNumFrames; // The number of candidate FFT frames to maintain + float *mMags; // The 2D array of FFT mags + float *mDc; // The 1D array of FFT DC values + float *mNyq; // The 1D array of FFT Nyquist values + float *mPhase; // The most recent phase array + float *mPhaseDiffs; // The 2D array of FFT phase differences + size_t mWritePtr; // The write pointer +}; + +void PV_CFreeze_next(PV_CFreeze *unit, int inNumSamples); +void PV_CFreeze_Ctor(PV_CFreeze *unit); +void PV_CFreeze_Dtor(PV_CFreeze *unit); \ No newline at end of file diff --git a/src/pv/pvOther.cpp b/src/pv/pvOther.cpp new file mode 100644 index 0000000..8b43068 --- /dev/null +++ b/src/pv/pvOther.cpp @@ -0,0 +1,110 @@ +/* +File: pvOther.cpp +Author: Jeff Martin + +Description: +A collection of PV UGens for SuperCollider. + +Copyright © 2026 by Jeffrey Martin. All rights reserved. +Website: https://www.jeffreymartincomposer.com + +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 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +#include "pvOther.hpp" +#include "FFT_UGens.h" + +void PV_MagSqueeze_next(PV_MagSqueeze *unit, int inNumSamples) { + PV_GET_BUF + float low = IN0(1); + float high = IN0(2); + SCPolarBuf *p = ToPolarApx(buf); + float min = p->dc; + float max = p->dc; + if (p->nyq < min) + min = p->nyq; + if (p->nyq > max) + max = p->nyq; + for (int i = 0; i < numbins; i++) { + if (p->bin[i].mag < min) + min = p->bin[i].mag; + if (p->bin[i].mag > max) + max = p->bin[i].mag; + } + float range = high - low; + p->dc = (p->dc / max) * range + low; + p->nyq = (p->nyq / max) * range + low; + for (int i = 0; i < numbins; i++) { + p->bin[i].mag = (p->bin[i].mag / max) * range + low; + } +} + +void PV_MagSqueeze_Ctor(PV_MagSqueeze *unit) { + SETCALC(PV_MagSqueeze_next); + OUT0(0) = IN0(0); +} + +void PV_MagMirror_next(PV_MagMirror *unit, int inNumSamples) { + PV_GET_BUF + SCPolarBuf *p = ToPolarApx(buf); + float min = p->dc; + float max = p->dc; + if (p->nyq < min) + min = p->nyq; + if (p->nyq > max) + max = p->nyq; + for (int i = 0; i < numbins; i++) { + if (p->bin[i].mag < min) + min = p->bin[i].mag; + if (p->bin[i].mag > max) + max = p->bin[i].mag; + } + p->dc = max - p->dc + min; + p->nyq = max - p->nyq + min; + for (int i = 0; i < numbins; i++) { + p->bin[i].mag = max - p->bin[i].mag + min; + } +} + +void PV_MagMirror_Ctor(PV_MagMirror *unit) { + SETCALC(PV_MagMirror_next); + OUT0(0) = IN0(0); +} + +void PV_MagXFade_next(PV_MagXFade *unit, int inNumSamples) { + PV_GET_BUF2 + float crossfade = IN0(2); + crossfade = sc_clip(crossfade, 0.f, 1.f); + SCPolarBuf *p = ToPolarApx(buf1); + SCPolarBuf *q = ToPolarApx(buf2); + // use sqrt crossfade (https://dsp.stackexchange.com/questions/37477/understanding-equal-power-crossfades) + float pCoef = 1.f, qCoef = 0.f; + if (crossfade == 1.f) { + pCoef = 0.f; + qCoef = 1.f; + } else if (crossfade > 0.f) { + pCoef = sc_sqrt(1.f - crossfade); + qCoef = sc_sqrt(crossfade); + } + p->dc = p->dc * pCoef + q->dc * qCoef; + p->nyq = p->nyq * pCoef + q->nyq * qCoef; + for (int i = 0; i < numbins; i++) { + p->bin[i].mag = p->bin[i].mag * pCoef + q->bin[i].mag * qCoef; + } +} + +void PV_MagXFade_Ctor(PV_MagXFade *unit) { + SETCALC(PV_MagXFade_next); + OUT0(0) = IN0(0); +} \ No newline at end of file diff --git a/src/pv/pvOther.hpp b/src/pv/pvOther.hpp new file mode 100644 index 0000000..899d6b2 --- /dev/null +++ b/src/pv/pvOther.hpp @@ -0,0 +1,38 @@ +/* +File: pvOther.hpp +Author: Jeff Martin + +Description: +A collection of PV UGens for SuperCollider. + +Copyright © 2026 by Jeffrey Martin. All rights reserved. +Website: https://www.jeffreymartincomposer.com + +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 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +#pragma once +#include "SC_Unit.h" + +struct PV_MagSqueeze : public Unit {}; +void PV_MagSqueeze_next(PV_MagSqueeze *unit, int inNumSamples); +void PV_MagSqueeze_Ctor(PV_MagSqueeze *unit); + +struct PV_MagMirror : public Unit {}; +void PV_MagMirror_next(PV_MagMirror *unit, int inNumSamples); +void PV_MagMirror_Ctor(PV_MagMirror *unit); + +struct PV_MagXFade : public Unit {}; +void PV_MagXFade_next(PV_MagXFade *unit, int inNumSamples); +void PV_MagXFade_Ctor(PV_MagXFade *unit); \ No newline at end of file diff --git a/src/pv/pvStretch.cpp b/src/pv/pvStretch.cpp new file mode 100644 index 0000000..13548c2 --- /dev/null +++ b/src/pv/pvStretch.cpp @@ -0,0 +1,152 @@ +/* +File: pvStretch.cpp +Author: Jeff Martin + +Description: +A phase vocoder time stretcher. + +Copyright © 2026 by Jeffrey Martin. All rights reserved. +Website: https://www.jeffreymartincomposer.com + +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 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +#include "pvStretch.hpp" +#include "SC_Constants.h" +#include "SC_InterfaceTable.h" +#include "FFT_UGens.h" +#include + +void PV_PlayBufStretch_Ctor(PV_PlayBufStretch *unit) { + // Connect to the STFT buffer. For now, we only allow this in the constructor. + float fbufnum = IN0(1); + uint32 bufnum = static_cast(fbufnum); + if (bufnum >= unit->mWorld->mNumSndBufs) bufnum = 0; + unit->m_fbufnum = fbufnum; + unit->m_buf = unit->mWorld->mSndBufs + bufnum; + + // Configure position + float startPos = sc_clip(IN0(2), 0.0, 1.0); + unit->m_pos = 0.f; + unit->m_startPos = startPos; + unit->m_firstFrame = true; + + SETCALC(PV_PlayBufStretch_next); + OUT0(0) = IN0(0); +} + +void PV_PlayBufStretch_next(PV_PlayBufStretch *unit, int inNumSamples) { + PV_GET_BUF + float startPos = sc_clip(IN0(2), 0.0, 1.0); + float rate = IN0(3); + float loop = IN0(4); + + // This section of the code is for acquiring the STFT buffer and information about it. + // It has to be run every time because we cannot be sure the user has not freed the buffer. + // We also have to verify important details about the buffer to make sure we can read from it at all. + const SndBuf *stftBuf = unit->m_buf; + if (!stftBuf) { + OUT0(0) = -1.f; + std::cout << "WARNING: The stftBuf could not be accessed. Aborting.\n"; + return; + } + ACQUIRE_SNDBUF_SHARED(stftBuf); + const float* bufData __attribute__((__unused__)) = stftBuf->data; + const float* stftData __attribute__((__unused__)) = stftBuf->data + 3; + uint32 bufChannels __attribute__((__unused__)) = stftBuf->channels; + uint32 bufSamples __attribute__((__unused__)) = stftBuf->samples; + uint32 bufFrames = stftBuf->frames; + // first 3 frames have analysis parameters + int stftFrames = (static_cast(stftBuf->samples) - 3) / static_cast(buf->samples); + + // If the buffer is improperly configured, we cannot use it. + if (bufChannels != 1) { + OUT0(0) = -1.f; + std::cout << "WARNING: stftBuf is configured with " << bufChannels << + " channels. A properly formatted buffer must only have 1 channel. Aborting.\n"; + RELEASE_SNDBUF_SHARED(stftBuf); + return; + } + else if (stftFrames < 2) { + OUT0(0) = -1.f; + std::cout << "WARNING: stftBuf has " << stftFrames << " frames, which is not enough data to be useful. " << + "Note that a properly formatted stftBuf has three leading elements: the FFT size, the hop size, " << + "and the window type. After that, it contains M analysis STFT frames, each of size N (FFT size). Aborting.\n"; + RELEASE_SNDBUF_SHARED(stftBuf); + return; + } + + // Basic information + size_t stftBufFftSize = bufData[0]; + size_t stftBufHopSize = static_cast(bufData[1] * stftBufFftSize); // in frames, not fraction + int stftBufWinType = static_cast(bufData[2]); // -1, 0, or 1. This information is probably extraneous. + + if (stftBufFftSize != buf->samples) { + OUT0(0) = -1.f; + std::cout << "WARNING: The FFT size of the STFT buffer (" << stftBufFftSize << + ") does not match the FFT size of the PV_PlayBufStretch (" << + buf->samples << "). Aborting.\n"; + RELEASE_SNDBUF_SHARED(stftBuf); + return; + } + + if (startPos != unit->m_startPos) { + unit->m_startPos = startPos; + unit->m_firstFrame = true; + } + + // Now that we've run setup, we're ready to read STFT data and perform phase vocoder stretching. + + // The first frame has to be cloned directly from the STFT buffer with no phase adjustments. + // This is essential to make sure that subsequent phase calculations are correctly aligned. + if (unit->m_firstFrame) { + // Compute the index of the first frame + size_t xxi = static_cast(std::round(startPos * stftFrames)); + if (xxi >= stftFrames) { + if (loop) { + xxi = 0; + } else { + OUT0(0) = -1.f; + RELEASE_SNDBUF_SHARED(stftBuf); + DoneAction(static_cast(IN0(5)), unit); + return; + } + } + + // Copy the FFT data over + SCPolarBuf *p = ToPolarApx(buf); + const float *currentFftFrame = stftData + (xxi * stftBufFftSize); + p->dc = currentFftFrame[0]; + p->nyq = currentFftFrame[1]; + for (size_t xxn = 2, xxk = 0; xxn < stftBufFftSize; xxn+=2, xxk++) { + // For some reason the phase is stored first, then the magnitude. + // This prevents a direct cast to SCPolarBuf, unfortunately. + p->bin[xxk].phase = currentFftFrame[xxn]; + p->bin[xxk].mag = currentFftFrame[xxn+1]; + } + + // We have to advance by one frame because we need to be able to compute frequency for time stretching. + unit->m_pos = static_cast(xxi + 1); + } + + // For frames other than the first frame, we'll need to perform phase computation. + else { + float newPos; + } + + RELEASE_SNDBUF_SHARED(stftBuf); + + // if no loop and we're past the last frame, handle this condition later + // DoneAction(static_cast(IN0(5)), unit); +} \ No newline at end of file diff --git a/src/pv/pvStretch.hpp b/src/pv/pvStretch.hpp new file mode 100644 index 0000000..3b93169 --- /dev/null +++ b/src/pv/pvStretch.hpp @@ -0,0 +1,50 @@ +/* +File: pvStretch.hpp +Author: Jeff Martin + +Description: +A phase vocoder time stretcher. + +Copyright © 2026 by Jeffrey Martin. All rights reserved. +Website: https://www.jeffreymartincomposer.com + +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 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ +#pragma once +#include "SC_Unit.h" + +struct PV_PlayBufStretch : public Unit { + // The index of the buffer with STFT data + float m_fbufnum; + + // The buffer with STFT data + SndBuf *m_buf; + + // Between 0 and 1; represents the start position of playback. + // If it jumps during playback, playback will be restarted + // at the new m_startPos. + float m_startPos; + + // A fractional STFT frame index. Unlike m_startPos, it corresponds to the + // integer index of the current STFT frame (from 0 to M-1). + // Used for interpolating position. + float m_pos; + + // For the first frame, we need to read phase data directly + // instead of computing it. + bool m_firstFrame; +}; + +void PV_PlayBufStretch_Ctor(PV_PlayBufStretch *unit); +void PV_PlayBufStretch_next(PV_PlayBufStretch *unit, int inNumSamples); From 95331e12f4033d82140ab12bc3ead6c74c5bbd2b Mon Sep 17 00:00:00 2001 From: Jeff Martin Date: Sun, 14 Jun 2026 12:40:14 -0500 Subject: [PATCH 03/25] fixed round bug --- src/pv/pvCFreeze.hpp | 1 - src/pv/pvStretch.cpp | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pv/pvCFreeze.hpp b/src/pv/pvCFreeze.hpp index d18ccaa..9d76180 100644 --- a/src/pv/pvCFreeze.hpp +++ b/src/pv/pvCFreeze.hpp @@ -23,7 +23,6 @@ along with this program. If not, see . */ #pragma once - #include "SC_Unit.h" struct PV_CFreeze : public Unit { diff --git a/src/pv/pvStretch.cpp b/src/pv/pvStretch.cpp index 13548c2..6f31bee 100644 --- a/src/pv/pvStretch.cpp +++ b/src/pv/pvStretch.cpp @@ -112,7 +112,7 @@ void PV_PlayBufStretch_next(PV_PlayBufStretch *unit, int inNumSamples) { // This is essential to make sure that subsequent phase calculations are correctly aligned. if (unit->m_firstFrame) { // Compute the index of the first frame - size_t xxi = static_cast(std::round(startPos * stftFrames)); + size_t xxi = static_cast(std::round(startPos * stftFrames)); if (xxi >= stftFrames) { if (loop) { xxi = 0; From 3b857867f3f14737b19e4303d03eb45dbd8f330f Mon Sep 17 00:00:00 2001 From: Jeff Martin Date: Sun, 14 Jun 2026 12:47:15 -0500 Subject: [PATCH 04/25] fixing position independent code bug --- src/pv/CMakeLists.txt | 3 +++ src/rubberband/CMakeLists.txt | 1 - 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pv/CMakeLists.txt b/src/pv/CMakeLists.txt index 45c412b..74de74a 100644 --- a/src/pv/CMakeLists.txt +++ b/src/pv/CMakeLists.txt @@ -3,6 +3,9 @@ add_library(pv MODULE pv.cpp) add_library(pvCFreeze STATIC pvCFreeze.cpp) add_library(pvOther STATIC pvOther.cpp) add_library(pvStretch STATIC pvStretch.cpp) +set_target_properties(pvCFreeze PROPERTIES POSITION_INDEPENDENT_CODE ON) +set_target_properties(pvOther PROPERTIES POSITION_INDEPENDENT_CODE ON) +set_target_properties(pvStretch PROPERTIES POSITION_INDEPENDENT_CODE ON) target_link_libraries(pv PRIVATE pvCFreeze pvOther pvStretch) if(SUPERNOVA) diff --git a/src/rubberband/CMakeLists.txt b/src/rubberband/CMakeLists.txt index 7a4e042..9d6027e 100644 --- a/src/rubberband/CMakeLists.txt +++ b/src/rubberband/CMakeLists.txt @@ -22,7 +22,6 @@ set_target_properties(rubberband_lib PROPERTIES POSITION_INDEPENDENT_CODE ON) if(SUPERNOVA) add_library(rubberband_supernova MODULE rubberband.cpp) target_link_libraries(rubberband_supernova PRIVATE rubberband_lib) - set_target_properties(rubberband_lib PROPERTIES POSITION_INDEPENDENT_CODE ON) set_property(TARGET rubberband_supernova PROPERTY COMPILE_DEFINITIONS SUPERNOVA) endif() From dabb94069d73e32860602e48c4d63a30fdceb6b6 Mon Sep 17 00:00:00 2001 From: Jeff Martin Date: Sun, 14 Jun 2026 17:37:47 -0500 Subject: [PATCH 05/25] works to separate rubberband --- src/rubberband/CMakeLists.txt | 17 +- src/rubberband/rubberBandPS.cpp | 94 +++++++ src/rubberband/rubberBandPS.hpp | 18 ++ src/rubberband/rubberBandStretcher.cpp | 212 ++++++++++++++++ src/rubberband/rubberBandStretcher.hpp | 19 ++ src/rubberband/rubberband.cpp | 331 +------------------------ src/rubberband/rubberband.hpp | 28 +++ 7 files changed, 390 insertions(+), 329 deletions(-) create mode 100644 src/rubberband/rubberBandPS.cpp create mode 100644 src/rubberband/rubberBandPS.hpp create mode 100644 src/rubberband/rubberBandStretcher.cpp create mode 100644 src/rubberband/rubberBandStretcher.hpp create mode 100644 src/rubberband/rubberband.hpp diff --git a/src/rubberband/CMakeLists.txt b/src/rubberband/CMakeLists.txt index 9d6027e..82d4f6e 100644 --- a/src/rubberband/CMakeLists.txt +++ b/src/rubberband/CMakeLists.txt @@ -16,12 +16,25 @@ endif() # Create the project library add_library(rubberband MODULE rubberband.cpp) -target_link_libraries(rubberband PRIVATE rubberband_lib) +add_library(rubberBandPS STATIC rubberBandPS.cpp) +add_library(rubberBandStretcher STATIC rubberBandStretcher.cpp) set_target_properties(rubberband_lib PROPERTIES POSITION_INDEPENDENT_CODE ON) +set_target_properties(rubberBandPS PROPERTIES POSITION_INDEPENDENT_CODE ON) +set_target_properties(rubberBandStretcher PROPERTIES POSITION_INDEPENDENT_CODE ON) +target_link_libraries(rubberband PRIVATE + rubberBandPS + rubberBandStretcher +) +target_link_libraries(rubberBandPS PRIVATE rubberband_lib) +target_link_libraries(rubberBandStretcher PRIVATE rubberband_lib) if(SUPERNOVA) add_library(rubberband_supernova MODULE rubberband.cpp) - target_link_libraries(rubberband_supernova PRIVATE rubberband_lib) + target_link_libraries(rubberband_supernova PRIVATE + rubberband_lib + rubberBandPS + rubberBandStretcher + ) set_property(TARGET rubberband_supernova PROPERTY COMPILE_DEFINITIONS SUPERNOVA) endif() diff --git a/src/rubberband/rubberBandPS.cpp b/src/rubberband/rubberBandPS.cpp new file mode 100644 index 0000000..6e5a684 --- /dev/null +++ b/src/rubberband/rubberBandPS.cpp @@ -0,0 +1,94 @@ +#include "rubberBandPS.hpp" +#include "SC_PlugIn.h" + +extern InterfaceTable *ft; + +void RubberBandPS_Ctor(RubberBandPS *unit) { + float pitchRatio = IN0(1); + + // Initialize the shifter + // 0x01000000 // formant preserving + // 0x00000000 // no formant preservation + unit->m_shifter = (RubberBand::RubberBandLiveShifter*)RTAlloc(unit->mWorld, sizeof(RubberBand::RubberBandLiveShifter)); + new (unit->m_shifter) RubberBand::RubberBandLiveShifter(static_cast(SAMPLERATE), 1, 0x01000000); + unit->m_shifter->setPitchScale(sc_clip(pitchRatio, 1e-2, 64)); + unit->m_blockSize = BUFLENGTH; + unit->m_shifterBlockSize = unit->m_shifter->getBlockSize(); + + // Buffers for holding the samples to feed in and out of the shifter. + // These buffers need to have the block size specified by the RubberBand shifter. + unit->m_shiftBufferIn = (float*)RTAlloc(unit->mWorld, unit->m_shifter->getBlockSize() * sizeof(float)); + unit->m_shiftBufferOut = (float*)RTAlloc(unit->mWorld, unit->m_shifter->getBlockSize() * sizeof(float)); + + // Make the ring buffers + unit->m_sendBuffer = (RingBuffer*)RTAlloc(unit->mWorld, sizeof(RingBuffer)); + new (unit->m_sendBuffer) RingBuffer; + unit->m_sendBuffer->initialize( + (float*)RTAlloc( + unit->mWorld, + (BUFLENGTH + unit->m_shifter->getBlockSize()) * 3 * sizeof(float)), + (BUFLENGTH + unit->m_shifter->getBlockSize()) * 3 + ); + unit->m_receiveBuffer = (RingBuffer*)RTAlloc(unit->mWorld, sizeof(RingBuffer)); + new (unit->m_receiveBuffer) RingBuffer; + unit->m_receiveBuffer->initialize( + (float*)RTAlloc( + unit->mWorld, + (BUFLENGTH + unit->m_shifter->getBlockSize()) * 3 * sizeof(float)), + (BUFLENGTH + unit->m_shifter->getBlockSize()) * 3 + ); + + // Initialize output ring buffer with zeros. If there's trouble, you might need to do this twice. + for (size_t i = 0; i < unit->m_shifter->getBlockSize(); i++) { + unit->m_shiftBufferIn[i] = 0.f; + } + + // Initialize first out sample + OUT0(0) = 0; + + SETCALC(RubberBandPS_next); +} + +void RubberBandPS_Dtor(RubberBandPS *unit) { + RTFree(unit->mWorld, unit->m_sendBuffer->m_buffer); + RTFree(unit->mWorld, unit->m_receiveBuffer->m_buffer); + RTFree(unit->mWorld, unit->m_shifter); + RTFree(unit->mWorld, unit->m_sendBuffer); + RTFree(unit->mWorld, unit->m_receiveBuffer); + RTFree(unit->mWorld, unit->m_shiftBufferIn); + RTFree(unit->mWorld, unit->m_shiftBufferOut); +} + +void RubberBandPS_next(RubberBandPS *unit, int inNumSamples) { + float pitchRatio = IN0(1); + float formantRatio = IN0(2); + float* in = IN(0); + float* out = OUT(0); + + // Prepare the shifter, clipping the pitch and formant ratios for safety + unit->m_shifter->setPitchScale(sc_clip(pitchRatio, 1e-2, 64)); + if (formantRatio) { + unit->m_shifter->setFormantScale(sc_clip(formantRatio, 1e-2, 64)); + } else { + unit->m_shifter->setFormantScale(0.f); + } + + // Feed the input into the shifter + unit->m_sendBuffer->writeBlock(in, inNumSamples); + + if (unit->m_sendBuffer->isReadReady(unit->m_shifterBlockSize)) { + unit->m_sendBuffer->readBlock(unit->m_shiftBufferIn, unit->m_shifterBlockSize); + unit->m_shifter->shift(&(unit->m_shiftBufferIn), &(unit->m_shiftBufferOut)); + unit->m_receiveBuffer->writeBlock(unit->m_shiftBufferOut, unit->m_shifterBlockSize); + } + + // If there is a block of output samples ready, read it. (There should always be a block ready.) + if (unit->m_receiveBuffer->isReadReady(inNumSamples)) { + unit->m_receiveBuffer->readBlock(out, inNumSamples); + } else { + // Zero out the output buffer + for (size_t i = 0; i < inNumSamples; i++) { + out[i] = 0.f; + } + } +} \ No newline at end of file diff --git a/src/rubberband/rubberBandPS.hpp b/src/rubberband/rubberBandPS.hpp new file mode 100644 index 0000000..9fbca09 --- /dev/null +++ b/src/rubberband/rubberBandPS.hpp @@ -0,0 +1,18 @@ +#pragma once +#include "SC_Unit.h" +#include "rubberband/RubberBandLiveShifter.h" +#include "ringbuffer.hpp" + +struct RubberBandPS : public Unit { + RubberBand::RubberBandLiveShifter* m_shifter; + RingBuffer* m_sendBuffer; + RingBuffer* m_receiveBuffer; + float *m_shiftBufferIn; + float *m_shiftBufferOut; + size_t m_blockSize; + size_t m_shifterBlockSize; +}; + +void RubberBandPS_Ctor(RubberBandPS *unit); +void RubberBandPS_Dtor(RubberBandPS *unit); +void RubberBandPS_next(RubberBandPS *unit, int inNumSamples); \ No newline at end of file diff --git a/src/rubberband/rubberBandStretcher.cpp b/src/rubberband/rubberBandStretcher.cpp new file mode 100644 index 0000000..5d18e73 --- /dev/null +++ b/src/rubberband/rubberBandStretcher.cpp @@ -0,0 +1,212 @@ +#include "rubberBandStretcher.hpp" +#include +#include "SC_PlugIn.h" + +extern InterfaceTable *ft; + +void RubberBandStretcher_Ctor(RubberBandStretcher *unit) { + float timeRatio = IN0(1); + float pitchRatio = IN0(2); + float formantRatio = IN0(3); + int transientsMode = static_cast(IN0(4)); + int detector = static_cast(IN0(5)); + int phaseOption = static_cast(IN0(6)); + int pitchQuality = static_cast(IN0(7)); + int windowOption = static_cast(IN0(8)); + int smoothing = static_cast(IN0(9)); + int engine = static_cast(IN0(10)); + + unit->m_timeRatio = timeRatio; + unit->m_pitchRatio = pitchRatio; + unit->m_formantRatio = formantRatio; + unit->m_transientsMode = transientsMode; + unit->m_detectorOption = detector; + unit->m_phaseOption = phaseOption; + unit->m_pitchQuality = pitchQuality; + + // Set up RubberBandStretcher initial options + int options = 0x01000001; // formant-preserving, real-time options set + switch (transientsMode) { + case 1: + options |= 0x00000100; + break; + case 2: + options |= 0x00000200; + break; + } + switch (detector) { + case 1: + options |= 0x00000400; + break; + case 2: + options |= 0x00000800; + break; + } + switch (phaseOption) { + case 1: + options |= 0x00002000; + } + switch (pitchQuality) { + case 1: + options |= 0x02000000; + break; + case 2: + options |= 0x04000000; + break; + } + switch (windowOption) { + case 1: + options |= 0x00100000; + break; + case 2: + options |= 0x00200000; + break; + } + switch (smoothing) { + case 1: + options |= 0x00800000; + break; + } + switch (engine) { + case 1: + options |= 0x20000000; + break; + } + + // Allocate the shifter with the given options + unit->m_stretcher = (RubberBand::RubberBandStretcher*)RTAlloc(unit->mWorld, sizeof(RubberBand::RubberBandStretcher)); + new (unit->m_stretcher) RubberBand::RubberBandStretcher(static_cast(SAMPLERATE), 1, options, timeRatio, pitchRatio); + + // Initialize the shifter + // The shifter accepts a block size (which must be set before the first process() + // call and not after), which avoids the need to use local RingBuffers. + unit->m_stretcher->setMaxProcessSize(BUFLENGTH); + unit->m_stretcher->setTimeRatio(sc_clip(timeRatio, 1.f, std::numeric_limits::infinity())); + unit->m_stretcher->setPitchScale(sc_clip(pitchRatio, 1e-2, 64)); + unit->m_stretcher->setFormantScale(sc_clip(formantRatio, 1e-2, 64)); + + // Feed samples in until the shifter is ready to start producing valid output. + // This is necessary because the shifter isn't ready to produce valid output + // as soon as it is initialized--it requires padded 0s to be fed in for some + // number of samples specified by the shifter. + float *zeroBuf = (float*)RTAlloc(unit->mWorld, BUFLENGTH * sizeof(float)); + for (size_t i = 0; i < BUFLENGTH; i++) { + zeroBuf[i] = 0.f; + } + + // The number of initial zeros required + size_t startPad = unit->m_stretcher->getPreferredStartPad(); + + // The number of samples to discard at the beginning of the stretcher output. + // This is handled in the RubberBandStretcher_next() method. + unit->m_samplesToDiscard = unit->m_stretcher->getStartDelay(); + + // Feed in the start pad samples + while (startPad > 0) { + unit->m_stretcher->process(&zeroBuf, BUFLENGTH, false); + startPad -= BUFLENGTH; + } + RTFree(unit->mWorld, zeroBuf); + + // Initialize first out sample + OUT0(0) = 0; + + SETCALC(RubberBandStretcher_next); +} + +void RubberBandStretcher_Dtor(RubberBandStretcher *unit) { + RTFree(unit->mWorld, unit->m_stretcher); +} + +void RubberBandStretcher_next(RubberBandStretcher *unit, int inNumSamples) { + float *in = IN(0); + float *out = OUT(0); + float timeRatio = IN0(1); + float pitchRatio = IN0(2); + float formantRatio = IN0(3); + int transientsMode = static_cast(IN0(4)); + int detector = static_cast(IN0(5)); + int phaseOption = static_cast(IN0(6)); + int pitchQuality = static_cast(IN0(7)); + + // Update shifter options only if something has changed + if (timeRatio != unit->m_timeRatio) { + unit->m_stretcher->setTimeRatio(sc_clip(timeRatio, 1.f, std::numeric_limits::infinity())); + } + if (pitchRatio != unit->m_pitchRatio) { + unit->m_stretcher->setPitchScale(sc_clip(pitchRatio, 1e-2, 64)); + } + if (formantRatio != unit->m_formantRatio) { + unit->m_stretcher->setFormantScale(sc_clip(formantRatio, 1e-2, 64)); + } + // QUESTION: Will this method of setting options override all existing options, + // or just the option provided? May need to compute all options from scratch. + if (transientsMode != unit->m_transientsMode) { + switch (transientsMode) { + case 1: + unit->m_stretcher->setTransientsOption(0x00000100); + break; + case 2: + unit->m_stretcher->setTransientsOption(0x00000200); + break; + default: + unit->m_stretcher->setTransientsOption(0x00000000); + } + } + if (detector != unit->m_detectorOption) { + switch (detector) { + case 1: + unit->m_stretcher->setDetectorOption(0x00000400); + break; + case 2: + unit->m_stretcher->setDetectorOption(0x00000800); + break; + default: + unit->m_stretcher->setDetectorOption(0x00000000); + } + } + if (phaseOption != unit->m_phaseOption) { + switch (phaseOption) { + case 1: + unit->m_stretcher->setPhaseOption(0x00002000); + break; + default: + unit->m_stretcher->setPhaseOption(0x00000000); + } + } + if (pitchQuality != unit->m_pitchQuality) { + switch (pitchQuality) { + case 1: + unit->m_stretcher->setPitchOption(0x02000000); + break; + case 2: + unit->m_stretcher->setPitchOption(0x04000000); + break; + default: + unit->m_stretcher->setPitchOption(0x00000000); + } + } + + unit->m_stretcher->process(&in, BUFLENGTH, false); + + // If we can retrieve a full block worth of audio + if (unit->m_stretcher->available() >= BUFLENGTH) { + unit->m_stretcher->retrieve(&out, BUFLENGTH); + // Clear initial samples if necessary + if (unit->m_samplesToDiscard > 0) { + size_t i = 0; + while (i < BUFLENGTH && unit->m_samplesToDiscard > 0) { + out[i] = 0.f; + unit->m_samplesToDiscard--; + i++; + } + } + } + + // Output zeros if the shifter has no new samples available + else { + for (size_t i = 0; i < BUFLENGTH; i++) { + out[i] = 0.f; + } + } +} diff --git a/src/rubberband/rubberBandStretcher.hpp b/src/rubberband/rubberBandStretcher.hpp new file mode 100644 index 0000000..c8a765c --- /dev/null +++ b/src/rubberband/rubberBandStretcher.hpp @@ -0,0 +1,19 @@ +#pragma once +#include "SC_Unit.h" +#include "rubberband/RubberBandStretcher.h" + +struct RubberBandStretcher : public Unit { + RubberBand::RubberBandStretcher* m_stretcher; + size_t m_samplesToDiscard; + float m_timeRatio; + float m_pitchRatio; + float m_formantRatio; + int m_transientsMode; + int m_detectorOption; + int m_phaseOption; + int m_pitchQuality; +}; + +void RubberBandStretcher_Ctor(RubberBandStretcher *unit); +void RubberBandStretcher_Dtor(RubberBandStretcher *unit); +void RubberBandStretcher_next(RubberBandStretcher *unit, int inNumSamples); \ No newline at end of file diff --git a/src/rubberband/rubberband.cpp b/src/rubberband/rubberband.cpp index 5f7924d..036dab1 100644 --- a/src/rubberband/rubberband.cpp +++ b/src/rubberband/rubberband.cpp @@ -22,336 +22,13 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ -#include "SC_InterfaceTable.h" -#include "FFT_UGens.h" -#include "SC_Unit.h" -#include "rubberband/RubberBandLiveShifter.h" -#include "rubberband/RubberBandStretcher.h" -#include "ringbuffer.hpp" -#include +#include "SC_PlugIn.h" +#include "rubberBandPS.hpp" +#include "rubberBandStretcher.hpp" InterfaceTable *ft; -struct RubberBandPS : public Unit { - RubberBand::RubberBandLiveShifter* m_shifter; - RingBuffer* m_sendBuffer; - RingBuffer* m_receiveBuffer; - float *m_shiftBufferIn; - float *m_shiftBufferOut; - size_t m_blockSize; - size_t m_shifterBlockSize; -}; - -struct RubberBandStretcher : public Unit { - RubberBand::RubberBandStretcher* m_stretcher; - size_t m_samplesToDiscard; - float m_timeRatio; - float m_pitchRatio; - float m_formantRatio; - int m_transientsMode; - int m_detectorOption; - int m_phaseOption; - int m_pitchQuality; -}; - -static void RubberBandPS_next(RubberBandPS *unit, int inNumSamples) { - float pitchRatio = IN0(1); - float formantRatio = IN0(2); - float* in = IN(0); - float* out = OUT(0); - - // Prepare the shifter, clipping the pitch and formant ratios for safety - unit->m_shifter->setPitchScale(sc_clip(pitchRatio, 1e-2, 64)); - if (formantRatio) { - unit->m_shifter->setFormantScale(sc_clip(formantRatio, 1e-2, 64)); - } else { - unit->m_shifter->setFormantScale(0.f); - } - - // Feed the input into the shifter - unit->m_sendBuffer->writeBlock(in, inNumSamples); - - if (unit->m_sendBuffer->isReadReady(unit->m_shifterBlockSize)) { - unit->m_sendBuffer->readBlock(unit->m_shiftBufferIn, unit->m_shifterBlockSize); - unit->m_shifter->shift(&(unit->m_shiftBufferIn), &(unit->m_shiftBufferOut)); - unit->m_receiveBuffer->writeBlock(unit->m_shiftBufferOut, unit->m_shifterBlockSize); - } - - // If there is a block of output samples ready, read it. (There should always be a block ready.) - if (unit->m_receiveBuffer->isReadReady(inNumSamples)) { - unit->m_receiveBuffer->readBlock(out, inNumSamples); - } else { - // Zero out the output buffer - for (size_t i = 0; i < inNumSamples; i++) { - out[i] = 0.f; - } - } -} - -static void RubberBandPS_Ctor(RubberBandPS *unit) { - float pitchRatio = IN0(1); - - // Initialize the shifter - // 0x01000000 // formant preserving - // 0x00000000 // no formant preservation - unit->m_shifter = (RubberBand::RubberBandLiveShifter*)RTAlloc(unit->mWorld, sizeof(RubberBand::RubberBandLiveShifter)); - new (unit->m_shifter) RubberBand::RubberBandLiveShifter(static_cast(SAMPLERATE), 1, 0x01000000); - unit->m_shifter->setPitchScale(sc_clip(pitchRatio, 1e-2, 64)); - unit->m_blockSize = BUFLENGTH; - unit->m_shifterBlockSize = unit->m_shifter->getBlockSize(); - - // Buffers for holding the samples to feed in and out of the shifter. - // These buffers need to have the block size specified by the RubberBand shifter. - unit->m_shiftBufferIn = (float*)RTAlloc(unit->mWorld, unit->m_shifter->getBlockSize() * sizeof(float)); - unit->m_shiftBufferOut = (float*)RTAlloc(unit->mWorld, unit->m_shifter->getBlockSize() * sizeof(float)); - - // Make the ring buffers - unit->m_sendBuffer = (RingBuffer*)RTAlloc(unit->mWorld, sizeof(RingBuffer)); - new (unit->m_sendBuffer) RingBuffer; - unit->m_sendBuffer->initialize( - (float*)RTAlloc( - unit->mWorld, - (BUFLENGTH + unit->m_shifter->getBlockSize()) * 3 * sizeof(float)), - (BUFLENGTH + unit->m_shifter->getBlockSize()) * 3 - ); - unit->m_receiveBuffer = (RingBuffer*)RTAlloc(unit->mWorld, sizeof(RingBuffer)); - new (unit->m_receiveBuffer) RingBuffer; - unit->m_receiveBuffer->initialize( - (float*)RTAlloc( - unit->mWorld, - (BUFLENGTH + unit->m_shifter->getBlockSize()) * 3 * sizeof(float)), - (BUFLENGTH + unit->m_shifter->getBlockSize()) * 3 - ); - - // Initialize output ring buffer with zeros. If there's trouble, you might need to do this twice. - for (size_t i = 0; i < unit->m_shifter->getBlockSize(); i++) { - unit->m_shiftBufferIn[i] = 0.f; - } - - // Initialize first out sample - OUT0(0) = 0; - - SETCALC(RubberBandPS_next); -} - -static void RubberBandPS_Dtor(RubberBandPS *unit) { - RTFree(unit->mWorld, unit->m_sendBuffer->m_buffer); - RTFree(unit->mWorld, unit->m_receiveBuffer->m_buffer); - RTFree(unit->mWorld, unit->m_shifter); - RTFree(unit->mWorld, unit->m_sendBuffer); - RTFree(unit->mWorld, unit->m_receiveBuffer); - RTFree(unit->mWorld, unit->m_shiftBufferIn); - RTFree(unit->mWorld, unit->m_shiftBufferOut); -} - -static void RubberBandStretcher_next(RubberBandStretcher *unit, int inNumSamples) { - float *in = IN(0); - float *out = OUT(0); - float timeRatio = IN0(1); - float pitchRatio = IN0(2); - float formantRatio = IN0(3); - int transientsMode = static_cast(IN0(4)); - int detector = static_cast(IN0(5)); - int phaseOption = static_cast(IN0(6)); - int pitchQuality = static_cast(IN0(7)); - - // Update shifter options only if something has changed - if (timeRatio != unit->m_timeRatio) { - unit->m_stretcher->setTimeRatio(sc_clip(timeRatio, 1.f, std::numeric_limits::infinity())); - } - if (pitchRatio != unit->m_pitchRatio) { - unit->m_stretcher->setPitchScale(sc_clip(pitchRatio, 1e-2, 64)); - } - if (formantRatio != unit->m_formantRatio) { - unit->m_stretcher->setFormantScale(sc_clip(formantRatio, 1e-2, 64)); - } - // QUESTION: Will this method of setting options override all existing options, - // or just the option provided? May need to compute all options from scratch. - if (transientsMode != unit->m_transientsMode) { - switch (transientsMode) { - case 1: - unit->m_stretcher->setTransientsOption(0x00000100); - break; - case 2: - unit->m_stretcher->setTransientsOption(0x00000200); - break; - default: - unit->m_stretcher->setTransientsOption(0x00000000); - } - } - if (detector != unit->m_detectorOption) { - switch (detector) { - case 1: - unit->m_stretcher->setDetectorOption(0x00000400); - break; - case 2: - unit->m_stretcher->setDetectorOption(0x00000800); - break; - default: - unit->m_stretcher->setDetectorOption(0x00000000); - } - } - if (phaseOption != unit->m_phaseOption) { - switch (phaseOption) { - case 1: - unit->m_stretcher->setPhaseOption(0x00002000); - break; - default: - unit->m_stretcher->setPhaseOption(0x00000000); - } - } - if (pitchQuality != unit->m_pitchQuality) { - switch (pitchQuality) { - case 1: - unit->m_stretcher->setPitchOption(0x02000000); - break; - case 2: - unit->m_stretcher->setPitchOption(0x04000000); - break; - default: - unit->m_stretcher->setPitchOption(0x00000000); - } - } - - unit->m_stretcher->process(&in, BUFLENGTH, false); - - // If we can retrieve a full block worth of audio - if (unit->m_stretcher->available() >= BUFLENGTH) { - unit->m_stretcher->retrieve(&out, BUFLENGTH); - // Clear initial samples if necessary - if (unit->m_samplesToDiscard > 0) { - size_t i = 0; - while (i < BUFLENGTH && unit->m_samplesToDiscard > 0) { - out[i] = 0.f; - unit->m_samplesToDiscard--; - i++; - } - } - } - - // Output zeros if the shifter has no new samples available - else { - for (size_t i = 0; i < BUFLENGTH; i++) { - out[i] = 0.f; - } - } -} - -static void RubberBandStretcher_Ctor(RubberBandStretcher *unit) { - float timeRatio = IN0(1); - float pitchRatio = IN0(2); - float formantRatio = IN0(3); - int transientsMode = static_cast(IN0(4)); - int detector = static_cast(IN0(5)); - int phaseOption = static_cast(IN0(6)); - int pitchQuality = static_cast(IN0(7)); - int windowOption = static_cast(IN0(8)); - int smoothing = static_cast(IN0(9)); - int engine = static_cast(IN0(10)); - - unit->m_timeRatio = timeRatio; - unit->m_pitchRatio = pitchRatio; - unit->m_formantRatio = formantRatio; - unit->m_transientsMode = transientsMode; - unit->m_detectorOption = detector; - unit->m_phaseOption = phaseOption; - unit->m_pitchQuality = pitchQuality; - - // Set up RubberBandStretcher initial options - int options = 0x01000001; // formant-preserving, real-time options set - switch (transientsMode) { - case 1: - options |= 0x00000100; - break; - case 2: - options |= 0x00000200; - break; - } - switch (detector) { - case 1: - options |= 0x00000400; - break; - case 2: - options |= 0x00000800; - break; - } - switch (phaseOption) { - case 1: - options |= 0x00002000; - } - switch (pitchQuality) { - case 1: - options |= 0x02000000; - break; - case 2: - options |= 0x04000000; - break; - } - switch (windowOption) { - case 1: - options |= 0x00100000; - break; - case 2: - options |= 0x00200000; - break; - } - switch (smoothing) { - case 1: - options |= 0x00800000; - break; - } - switch (engine) { - case 1: - options |= 0x20000000; - break; - } - - // Allocate the shifter with the given options - unit->m_stretcher = (RubberBand::RubberBandStretcher*)RTAlloc(unit->mWorld, sizeof(RubberBand::RubberBandStretcher)); - new (unit->m_stretcher) RubberBand::RubberBandStretcher(static_cast(SAMPLERATE), 1, options, timeRatio, pitchRatio); - - // Initialize the shifter - // The shifter accepts a block size (which must be set before the first process() - // call and not after), which avoids the need to use local RingBuffers. - unit->m_stretcher->setMaxProcessSize(BUFLENGTH); - unit->m_stretcher->setTimeRatio(sc_clip(timeRatio, 1.f, std::numeric_limits::infinity())); - unit->m_stretcher->setPitchScale(sc_clip(pitchRatio, 1e-2, 64)); - unit->m_stretcher->setFormantScale(sc_clip(formantRatio, 1e-2, 64)); - - // Feed samples in until the shifter is ready to start producing valid output. - // This is necessary because the shifter isn't ready to produce valid output - // as soon as it is initialized--it requires padded 0s to be fed in for some - // number of samples specified by the shifter. - float *zeroBuf = (float*)RTAlloc(unit->mWorld, BUFLENGTH * sizeof(float)); - for (size_t i = 0; i < BUFLENGTH; i++) { - zeroBuf[i] = 0.f; - } - - // The number of initial zeros required - size_t startPad = unit->m_stretcher->getPreferredStartPad(); - - // The number of samples to discard at the beginning of the stretcher output. - // This is handled in the RubberBandStretcher_next() method. - unit->m_samplesToDiscard = unit->m_stretcher->getStartDelay(); - - // Feed in the start pad samples - while (startPad > 0) { - unit->m_stretcher->process(&zeroBuf, BUFLENGTH, false); - startPad -= BUFLENGTH; - } - RTFree(unit->mWorld, zeroBuf); - - // Initialize first out sample - OUT0(0) = 0; - - SETCALC(RubberBandStretcher_next); -} - -static void RubberBandStretcher_Dtor(RubberBandStretcher *unit) { - RTFree(unit->mWorld, unit->m_stretcher); -} - -PluginLoad(PV_Jeff) { +PluginLoad(RubberBandPlugins) { ft = inTable; DefineDtorUnit(RubberBandPS); DefineDtorUnit(RubberBandStretcher); diff --git a/src/rubberband/rubberband.hpp b/src/rubberband/rubberband.hpp new file mode 100644 index 0000000..88fd995 --- /dev/null +++ b/src/rubberband/rubberband.hpp @@ -0,0 +1,28 @@ +/* +File: rubberband.hpp +Author: Jeff Martin + +Description: +A high quality, formant-preserving live pitch shifter and time stretcher using the RubberBand library. + +Copyright © 2026 by Jeffrey Martin. All rights reserved. +Website: https://www.jeffreymartincomposer.com + +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 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +#pragma once +#include "SC_PlugIn.h" + +extern InterfaceTable *ft; \ No newline at end of file From bbe10bd231b987e0ff61a9eef1e7735cf7c2587d Mon Sep 17 00:00:00 2001 From: Jeff Martin Date: Sun, 14 Jun 2026 18:02:36 -0500 Subject: [PATCH 06/25] refactored generators for separate modules --- src/generators/CMakeLists.txt | 19 + src/generators/arrayheap.cpp | 106 +++ src/generators/arrayheap.hpp | 113 +-- src/generators/generators.cpp | 888 +----------------- src/generators/impulseDropout.cpp | 320 +++++++ .../impulseDropout.hpp} | 20 +- src/generators/impulseJitter.cpp | 393 ++++++++ src/generators/impulseJitter.hpp | 43 + src/generators/loopPhasor.cpp | 232 +++++ src/generators/loopPhasor.hpp | 39 + src/pv/pvCFreeze.cpp | 4 +- src/pv/pvOther.cpp | 2 + src/pv/pvStretch.cpp | 2 + 13 files changed, 1212 insertions(+), 969 deletions(-) create mode 100644 src/generators/arrayheap.cpp create mode 100644 src/generators/impulseDropout.cpp rename src/{rubberband/rubberband.hpp => generators/impulseDropout.hpp} (52%) create mode 100644 src/generators/impulseJitter.cpp create mode 100644 src/generators/impulseJitter.hpp create mode 100644 src/generators/loopPhasor.cpp create mode 100644 src/generators/loopPhasor.hpp diff --git a/src/generators/CMakeLists.txt b/src/generators/CMakeLists.txt index bfab676..81d7c6a 100644 --- a/src/generators/CMakeLists.txt +++ b/src/generators/CMakeLists.txt @@ -1,8 +1,27 @@ # Create the project library add_library(generators MODULE generators.cpp) +add_library(arrayHeap STATIC arrayheap.cpp) +add_library(loopPhasor STATIC loopPhasor.cpp) +add_library(impulseDropout STATIC impulseDropout.cpp) +add_library(impulseJitter STATIC impulseJitter.cpp) +set_target_properties(arrayHeap PROPERTIES POSITION_INDEPENDENT_CODE ON) +set_target_properties(loopPhasor PROPERTIES POSITION_INDEPENDENT_CODE ON) +set_target_properties(impulseDropout PROPERTIES POSITION_INDEPENDENT_CODE ON) +set_target_properties(impulseJitter PROPERTIES POSITION_INDEPENDENT_CODE ON) +target_link_libraries(generators PRIVATE + loopPhasor + impulseDropout + impulseJitter +) +target_link_libraries(impulseJitter PRIVATE arrayHeap) if(SUPERNOVA) add_library(generators_supernova MODULE generators.cpp) set_property(TARGET generators_supernova PROPERTY COMPILE_DEFINITIONS SUPERNOVA) + target_link_libraries(generators_supernova PRIVATE + loopPhasor + impulseDropout + impulseJitter + ) endif() diff --git a/src/generators/arrayheap.cpp b/src/generators/arrayheap.cpp new file mode 100644 index 0000000..4c337e0 --- /dev/null +++ b/src/generators/arrayheap.cpp @@ -0,0 +1,106 @@ +/* +File: arrayheap.cpp +Author: Jeff Martin + +Description: +A simple array-based heap + +Copyright © 2025 by Jeffrey Martin. All rights reserved. +Website: https://www.jeffreymartincomposer.com + +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 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +#include "arrayheap.hpp" + +// Inserts into the heap +int heapInsert(IntMinHeap* heap, int data) { + if (heap->size == heap->maxSize) { + return 0; + } else { + if (heap->size == 0) { + heap->size++; + } + size_t idx = heap->size; + heap->heap[idx] = data; + heap->size++; + + // Bubble up + while (idx > 1) { + size_t parentIdx = idx; + if (parentIdx % 2 == 1) + parentIdx--; + parentIdx >>= 1; + if (heap->heap[idx] < heap->heap[parentIdx]) { + int swapVal = heap->heap[idx]; + heap->heap[idx] = heap->heap[parentIdx]; + heap->heap[parentIdx] = swapVal; + } + idx = parentIdx; + } + return 1; + } +} + +// Removes from the heap and returns the value popped. Returns 0 if the heap is empty. +int heapPop(IntMinHeap* heap) { + if (heap->size > 1) { + int val = heap->heap[1]; + heap->heap[1] = heap->heap[heap->size-1]; + heap->size--; + size_t idx = 1; + // Bubble down + while (idx < heap->size) { + size_t leftChild = idx * 2; + size_t rightChild = leftChild + 1; + if (rightChild < heap->size) { + if (heap->heap[leftChild] <= heap->heap[rightChild] && heap->heap[idx] > heap->heap[leftChild]) { + int swapVal = heap->heap[leftChild]; + heap->heap[leftChild] = heap->heap[idx]; + heap->heap[idx] = swapVal; + idx = leftChild; + } else if (heap->heap[leftChild] > heap->heap[rightChild] && heap->heap[idx] > heap->heap[rightChild]) { + int swapVal = heap->heap[rightChild]; + heap->heap[rightChild] = heap->heap[idx]; + heap->heap[idx] = swapVal; + idx = rightChild; + } else { + break; + } + } else if (leftChild < heap->size) { + if (heap->heap[idx] > heap->heap[leftChild]) { + int swapVal = heap->heap[leftChild]; + heap->heap[leftChild] = heap->heap[idx]; + heap->heap[idx] = swapVal; + idx = leftChild; + } + break; + } else { + break; + } + } + return val; + } else { + return 0; + } +} + +// Safe peek at the top of the heap +int heapPeek(IntMinHeap* heap) { + if (heap->size > 1) { + return heap->heap[1]; + } else { + return -1; + } +} \ No newline at end of file diff --git a/src/generators/arrayheap.hpp b/src/generators/arrayheap.hpp index 26f9a43..c21d879 100644 --- a/src/generators/arrayheap.hpp +++ b/src/generators/arrayheap.hpp @@ -1,87 +1,42 @@ -// A min heap for ints. +/* +File: arrayheap.hpp +Author: Jeff Martin + +Description: +A simple array-based heap + +Copyright © 2025 by Jeffrey Martin. All rights reserved. +Website: https://www.jeffreymartincomposer.com + +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 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +#pragma once +#include + +/// A min heap for ints. typedef struct { int* heap; size_t size; size_t maxSize; } IntMinHeap; -// Inserts into the heap -int heapInsert(IntMinHeap* heap, int data) { - if (heap->size == heap->maxSize) { - return 0; - } else { - if (heap->size == 0) { - heap->size++; - } - size_t idx = heap->size; - heap->heap[idx] = data; - heap->size++; - - // Bubble up - while (idx > 1) { - size_t parentIdx = idx; - if (parentIdx % 2 == 1) - parentIdx--; - parentIdx >>= 1; - if (heap->heap[idx] < heap->heap[parentIdx]) { - int swapVal = heap->heap[idx]; - heap->heap[idx] = heap->heap[parentIdx]; - heap->heap[parentIdx] = swapVal; - } - idx = parentIdx; - } - return 1; - } -} +/// Inserts into the heap +int heapInsert(IntMinHeap* heap, int data); -// Removes from the heap and returns the value popped. Returns 0 if the heap is empty. -int heapPop(IntMinHeap* heap) { - if (heap->size > 1) { - int val = heap->heap[1]; - heap->heap[1] = heap->heap[heap->size-1]; - heap->size--; - size_t idx = 1; - // Bubble down - while (idx < heap->size) { - size_t leftChild = idx * 2; - size_t rightChild = leftChild + 1; - if (rightChild < heap->size) { - if (heap->heap[leftChild] <= heap->heap[rightChild] && heap->heap[idx] > heap->heap[leftChild]) { - int swapVal = heap->heap[leftChild]; - heap->heap[leftChild] = heap->heap[idx]; - heap->heap[idx] = swapVal; - idx = leftChild; - } else if (heap->heap[leftChild] > heap->heap[rightChild] && heap->heap[idx] > heap->heap[rightChild]) { - int swapVal = heap->heap[rightChild]; - heap->heap[rightChild] = heap->heap[idx]; - heap->heap[idx] = swapVal; - idx = rightChild; - } else { - break; - } - } else if (leftChild < heap->size) { - if (heap->heap[idx] > heap->heap[leftChild]) { - int swapVal = heap->heap[leftChild]; - heap->heap[leftChild] = heap->heap[idx]; - heap->heap[idx] = swapVal; - idx = leftChild; - } - break; - } else { - break; - } - } - return val; - } else { - return 0; - } -} +/// Removes from the heap and returns the value popped. Returns 0 if the heap is empty. +int heapPop(IntMinHeap* heap); -// Safe peek at the top of the heap -inline int heapPeek(IntMinHeap* heap) { - if (heap->size > 1) { - return heap->heap[1]; - } else { - return -1; - } -} +/// Safe peek at the top of the heap +int heapPeek(IntMinHeap* heap); \ No newline at end of file diff --git a/src/generators/generators.cpp b/src/generators/generators.cpp index 5576c3d..90cdf2a 100644 --- a/src/generators/generators.cpp +++ b/src/generators/generators.cpp @@ -23,891 +23,11 @@ along with this program. If not, see . */ #include "SC_PlugIn.h" -#include "arrayheap.hpp" -#define HEAP_MAX_SIZE 1024 +#include "loopPhasor.hpp" +#include "impulseDropout.hpp" +#include "impulseJitter.hpp" -static InterfaceTable *ft; - -// Represents an ImpulseDropout UGen. -struct ImpulseDropout : public Unit { - double mPhase, mPhaseOffset, mPhaseIncrement; - float mFreqMul; -}; - -void ImpulseDropout_Ctor(ImpulseDropout* unit); -void ImpulseDropout_next_aa(ImpulseDropout* unit, int inNumSamples); -void ImpulseDropout_next_ai(ImpulseDropout* unit, int inNumSamples); -void ImpulseDropout_next_ak(ImpulseDropout* unit, int inNumSamples); -void ImpulseDropout_next_ki(ImpulseDropout* unit, int inNumSamples); -void ImpulseDropout_next_kk(ImpulseDropout* unit, int inNumSamples); - -// This is a copy of the static function from LFUGens.cpp in server/plugins. -// It detects if a phasor is out-of-bounds, triggers, and wraps [0, 1]. -static inline float testWrapPhase(double prev_inc, double& phase) { - if (prev_inc < 0.f) { // negative freqs - if (phase <= 0.f) { - phase += 1.f; - if (phase <= 0.f) { // catch large phase jumps - phase -= sc_ceil(phase); - } - return 1.f; - } else { - return 0.f; - } - } else { // positive freqs - if (phase >= 1.f) { - phase -= 1.f; - if (phase >= 1.f) { - phase -= sc_floor(phase); - } - return 1.f; - } else { - return 0.f; - } - } -} - -void ImpulseDropout_next_aa(ImpulseDropout* unit, int inNumSamples) { - float* out = OUT(0); - float* freqIn = IN(0); - float* offIn = IN(1); - float dropProbIn = IN0(2); - - // Collect UGen state - double phase = unit->mPhase; - double inc = unit->mPhaseIncrement; - float freqMul = unit->mFreqMul; - double prevOff = unit->mPhaseOffset; - - RGET - for (int xxn = 0; xxn < inNumSamples; xxn++) { - float impulseResult = testWrapPhase(inc, phase); - // Drop the impulse if necessary - if (impulseResult > 0.5f && rgen.frand() < dropProbIn) { - impulseResult = 0.f; - } - double off = static_cast(offIn[xxn]); - double offInc = off - prevOff; - phase += offInc; - testWrapPhase(inc, phase); - inc = freqIn[xxn] * freqMul; - out[xxn] = impulseResult; - phase += inc; - prevOff = off; - } - - unit->mPhase = phase; - unit->mPhaseOffset = prevOff; - unit->mPhaseIncrement = inc; -} - -void ImpulseDropout_next_ai(ImpulseDropout* unit, int inNumSamples) { - float* out = OUT(0); - float freqIn = IN0(0); - float dropProbIn = IN0(2); - - // Collect UGen state - double phase = unit->mPhase; - double inc = unit->mPhaseIncrement; - float freqMul = unit->mFreqMul; - - RGET - for (int xxn = 0; xxn < inNumSamples; xxn++) { - float impulseResult = testWrapPhase(inc, phase); - // Drop the impulse if necessary - if (impulseResult > 0.5f && rgen.frand() < dropProbIn) { - impulseResult = 0.f; - } - inc = freqIn * freqMul; - out[xxn] = impulseResult; - phase += inc; - } - - unit->mPhase = phase; - unit->mPhaseIncrement = inc; -} - -void ImpulseDropout_next_ak(ImpulseDropout* unit, int inNumSamples) { - float* out = OUT(0); - float freqIn = IN0(0); - double off = IN0(1); - float dropProbIn = IN0(2); - - // Collect UGen state - double phase = unit->mPhase; - double inc = unit->mPhaseIncrement; - float freqMul = unit->mFreqMul; - double prevOff = unit->mPhaseOffset; - - double offSlope = CALCSLOPE(off, prevOff); - bool offChanged = offSlope != 0.f; - - RGET - for (int xxn = 0; xxn < inNumSamples; xxn++) { - float impulseResult = testWrapPhase(inc, phase); - // Drop the impulse if necessary - if (impulseResult > 0.5f && rgen.frand() < dropProbIn) { - impulseResult = 0.f; - } - if (offChanged) { - phase += offSlope; - testWrapPhase(inc, phase); - } - inc = freqIn * freqMul; - out[xxn] = impulseResult; - phase += inc; - } - - unit->mPhase = phase; - unit->mPhaseOffset = off; - unit->mPhaseIncrement = inc; -} - -void ImpulseDropout_next_ki(ImpulseDropout* unit, int inNumSamples) { - float* out = OUT(0); - double inc = IN0(0) * unit->mFreqMul; - float dropProbIn = IN0(2); - - // Collect UGen state - double phase = unit->mPhase; - double prevInc = unit->mPhaseIncrement; - - double incSlope = CALCSLOPE(inc, prevInc); - - RGET - for (int xxn = 0; xxn < inNumSamples; xxn++) { - float impulseResult = testWrapPhase(prevInc, phase); - // Drop the impulse if necessary - if (impulseResult > 0.5f && rgen.frand() < dropProbIn) { - impulseResult = 0.f; - } - out[xxn] = impulseResult; - prevInc += incSlope; - phase += prevInc; - } - - unit->mPhase = phase; - unit->mPhaseIncrement = inc; -} - -void ImpulseDropout_next_kk(ImpulseDropout* unit, int inNumSamples) { - float* out = OUT(0); - double inc = IN0(0) * unit->mFreqMul; - double off = IN0(1); - float dropProbIn = IN0(2); - - // Collect UGen state - double phase = unit->mPhase; - double prevInc = unit->mPhaseIncrement; - double prevOff = unit->mPhaseOffset; - - double incSlope = CALCSLOPE(inc, prevInc); - double phaseSlope = CALCSLOPE(off, prevOff); - bool phOffChanged = phaseSlope != 0.f; - - RGET - for (int xxn = 0; xxn < inNumSamples; xxn++) { - float impulseResult = testWrapPhase(prevInc, phase); - // Drop the impulse if necessary - if (impulseResult > 0.5f && rgen.frand() < dropProbIn) { - impulseResult = 0.f; - } - if (phOffChanged) { - phase += phaseSlope; - testWrapPhase(prevInc, phase); - } - out[xxn] = impulseResult; - prevInc += incSlope; - phase += prevInc; - } - - unit->mPhase = phase; - unit->mPhaseOffset = off; - unit->mPhaseIncrement = inc; -} - -void ImpulseDropout_next_ii(ImpulseDropout* unit, int inNumSamples) { - float* out = OUT(0); - float dropProbIn = IN0(2); - - // Collect UGen state - double inc = unit->mPhaseIncrement; - double phase = unit->mPhase; - - RGET - for (int xxn = 0; xxn < inNumSamples; xxn++) { - float impulseResult = testWrapPhase(inc, phase); - // Drop the impulse if necessary - if (impulseResult > 0.5f && rgen.frand() < dropProbIn) { - impulseResult = 0.f; - } - out[xxn] = impulseResult; - phase += inc; - } - - unit->mPhase = phase; -} - -void ImpulseDropout_next_ik(ImpulseDropout* unit, int inNumSamples) { - float* out = OUT(0); - double off = IN0(1); - float dropProbIn = IN0(2); - - // Collect UGen state - double phase = unit->mPhase; - double inc = unit->mPhaseIncrement; - double prevOff = unit->mPhaseOffset; - - double phaseSlope = CALCSLOPE(off, prevOff); - bool phOffChanged = phaseSlope != 0.f; - - RGET - for (int xxn = 0; xxn < inNumSamples; xxn++) { - float impulseResult = testWrapPhase(inc, phase); - // Drop the impulse if necessary - if (impulseResult > 0.5f && rgen.frand() < dropProbIn) { - impulseResult = 0.f; - } - if (phOffChanged) { - phase += phaseSlope; - testWrapPhase(inc, phase); - } - out[xxn] = impulseResult; - phase += inc; - } - - unit->mPhase = phase; - unit->mPhaseOffset = off; -} - -// Construct the ImpulseDropout -void ImpulseDropout_Ctor(ImpulseDropout* unit) { - unit->mPhaseIncrement = IN0(0) * unit->mFreqMul; - unit->mPhaseOffset = IN0(1); - unit->mFreqMul = static_cast(unit->mRate->mSampleDur); - - double initOff = unit->mPhaseOffset; - double initInc = unit->mPhaseIncrement; - double initPhase = sc_wrap(initOff, 0.0, 1.0); - - // Initial phase offset of 0 means output of 1 on first sample. - // Set phase to wrap point to trigger impulse on first sample - if (initPhase == 0.0 && initInc >= 0.0) { - initPhase = 1.0; // positive frequency trigger/wrap position - } - unit->mPhase = initPhase; - - UnitCalcFunc func; - switch (INRATE(0)) { - case calc_FullRate: - switch (INRATE(1)) { - case calc_ScalarRate: - func = (UnitCalcFunc)ImpulseDropout_next_ai; - break; - case calc_BufRate: - func = (UnitCalcFunc)ImpulseDropout_next_ak; - break; - case calc_FullRate: - func = (UnitCalcFunc)ImpulseDropout_next_aa; - break; - } - break; - case calc_BufRate: - if (INRATE(1) == calc_ScalarRate) { - func = (UnitCalcFunc)ImpulseDropout_next_ki; - } else { - func = (UnitCalcFunc)ImpulseDropout_next_kk; - } - break; - case calc_ScalarRate: - if (INRATE(1) == calc_ScalarRate) { - func = (UnitCalcFunc)ImpulseDropout_next_ki; - } else { - func = (UnitCalcFunc)ImpulseDropout_next_kk; - } - break; - } - unit->mCalcFunc = func; - func(unit, 1); - - unit->mPhase = initPhase; - unit->mPhaseOffset = initOff; - unit->mPhaseIncrement = initInc; -} - -// Represents an ImpulseJitter UGen. -struct ImpulseJitter : public Unit { - double mPhase, mPhaseOffset, mPhaseIncrement; - float mFreqMul; - IntMinHeap mImpulseHeap; -}; - -void ImpulseJitter_next_aa(ImpulseJitter* unit, int inNumSamples); -void ImpulseJitter_next_ai(ImpulseJitter* unit, int inNumSamples); -void ImpulseJitter_next_ak(ImpulseJitter* unit, int inNumSamples); -void ImpulseJitter_next_ki(ImpulseJitter* unit, int inNumSamples); -void ImpulseJitter_next_kk(ImpulseJitter* unit, int inNumSamples); -void ImpulseJitter_Ctor(ImpulseJitter* unit); -void ImpulseJitter_Dtor(ImpulseJitter* unit); - -void ImpulseJitter_next_aa(ImpulseJitter* unit, int inNumSamples) { - float* out = OUT(0); - float* freq = IN(0); - float* offIn = IN(1); - float jitterFracIn = IN0(2); - - // Collect UGen state - double phase = unit->mPhase; - double inc = unit->mPhaseIncrement; - float freqMul = unit->mFreqMul; - double prevOff = unit->mPhaseOffset; - - // The maximum distance an impulse can be displaced - int jitterWidth = static_cast(jitterFracIn * unit->mImpulseHeap.maxSize); - - // Zero out the output buffer - for (int xxn = 0; xxn < inNumSamples; xxn++) { - out[xxn] = 0.f; - } - - // Update the impulse table indices - for (int xxn = 1; xxn < unit->mImpulseHeap.size; xxn++) { - unit->mImpulseHeap.heap[xxn] -= inNumSamples; - } - - // Retrieve impulses for this block - while (heapPeek(&unit->mImpulseHeap) < inNumSamples && unit->mImpulseHeap.size > 1) { - out[heapPop(&unit->mImpulseHeap)] = 1.f; - } - - RGET - for (int xxn = 0; xxn < inNumSamples; xxn++) { - size_t heapEffectiveSize = static_cast(HEAP_MAX_SIZE / (12 * sc_log2(freq[xxn]))); - float impulseResult = testWrapPhase(inc, phase); - if (impulseResult > 0.5f) { - int idx = rgen.irand(jitterWidth) + xxn; - if (idx < inNumSamples) { - out[idx] = 1.f; - } else if (unit->mImpulseHeap.size < heapEffectiveSize) { - heapInsert(&unit->mImpulseHeap, idx); - } - } - double off = static_cast(offIn[xxn]); - double offInc = off - prevOff; - phase += offInc; - testWrapPhase(inc, phase); - inc = freq[xxn] * freqMul; - phase += inc; - prevOff = off; - } - - unit->mPhase = phase; - unit->mPhaseOffset = prevOff; - unit->mPhaseIncrement = inc; -} - -void ImpulseJitter_next_ai(ImpulseJitter* unit, int inNumSamples) { - float* out = OUT(0); - float freq = IN0(0); - float jitterFracIn = IN0(2); - size_t heapEffectiveSize = static_cast(HEAP_MAX_SIZE / (12 * sc_log2(freq))); - - // Collect UGen state - double phase = unit->mPhase; - double inc = unit->mPhaseIncrement; - float freqMul = unit->mFreqMul; - - // The maximum distance an impulse can be displaced - int jitterWidth = static_cast(jitterFracIn * unit->mImpulseHeap.maxSize); - - // Zero out the output buffer - for (int xxn = 0; xxn < inNumSamples; xxn++) { - out[xxn] = 0.f; - } - - // Update the impulse table indices - for (int xxn = 1; xxn < unit->mImpulseHeap.size; xxn++) { - unit->mImpulseHeap.heap[xxn] -= inNumSamples; - } - - // Retrieve impulses for this block - while (heapPeek(&unit->mImpulseHeap) < inNumSamples && unit->mImpulseHeap.size > 1) { - out[heapPop(&unit->mImpulseHeap)] = 1.f; - } - - RGET - for (int xxn = 0; xxn < inNumSamples; xxn++) { - float impulseResult = testWrapPhase(inc, phase); - if (impulseResult > 0.5f) { - int idx = rgen.irand(jitterWidth) + xxn; - if (idx < inNumSamples) { - out[idx] = 1.f; - } else if (unit->mImpulseHeap.size < heapEffectiveSize) { - heapInsert(&unit->mImpulseHeap, idx); - } - } - inc = freq * freqMul; - phase += inc; - } - - unit->mPhase = phase; - unit->mPhaseIncrement = inc; -} - -void ImpulseJitter_next_ak(ImpulseJitter* unit, int inNumSamples) { - float* out = OUT(0); - float freq = IN0(0); - double off = IN0(1); - float jitterFracIn = IN0(2); - size_t heapEffectiveSize = static_cast(HEAP_MAX_SIZE / (12 * sc_log2(freq))); - - // Collect UGen state - double phase = unit->mPhase; - double inc = unit->mPhaseIncrement; - float freqMul = unit->mFreqMul; - double prevOff = unit->mPhaseOffset; - - double offSlope = CALCSLOPE(off, prevOff); - bool offChanged = offSlope != 0.f; - - // The maximum distance an impulse can be displaced - int jitterWidth = static_cast(jitterFracIn * unit->mImpulseHeap.maxSize); - - // Zero out the output buffer - for (int xxn = 0; xxn < inNumSamples; xxn++) { - out[xxn] = 0.f; - } - - // Update the impulse table indices - for (int xxn = 1; xxn < unit->mImpulseHeap.size; xxn++) { - unit->mImpulseHeap.heap[xxn] -= inNumSamples; - } - - // Retrieve impulses for this block - while (heapPeek(&unit->mImpulseHeap) < inNumSamples && unit->mImpulseHeap.size > 1) { - out[heapPop(&unit->mImpulseHeap)] = 1.f; - } - - RGET - for (int xxn = 0; xxn < inNumSamples; xxn++) { - float impulseResult = testWrapPhase(inc, phase); - if (impulseResult > 0.5f) { - int idx = rgen.irand(jitterWidth) + xxn; - if (idx < inNumSamples) { - out[idx] = 1.f; - } else if (unit->mImpulseHeap.size < heapEffectiveSize) { - heapInsert(&unit->mImpulseHeap, idx); - } - } - if (offChanged) { - phase += offSlope; - testWrapPhase(inc, phase); - } - inc = freq * freqMul; - phase += inc; - } - - unit->mPhase = phase; - unit->mPhaseOffset = off; - unit->mPhaseIncrement = inc; -} - -void ImpulseJitter_next_ki(ImpulseJitter* unit, int inNumSamples) { - float* out = OUT(0); - double freq = IN0(0); - double inc = freq * unit->mFreqMul; - float jitterFracIn = IN0(2); - size_t heapEffectiveSize = static_cast(HEAP_MAX_SIZE / (12 * sc_log2(freq))); - - // Collect UGen state - double phase = unit->mPhase; - double prevInc = unit->mPhaseIncrement; - - double incSlope = CALCSLOPE(inc, prevInc); - - // The maximum distance an impulse can be displaced - int jitterWidth = static_cast(jitterFracIn * unit->mImpulseHeap.maxSize); - - // Zero out the output buffer - for (int xxn = 0; xxn < inNumSamples; xxn++) { - out[xxn] = 0.f; - } - - // Update the impulse table indices - for (int xxn = 1; xxn < unit->mImpulseHeap.size; xxn++) { - unit->mImpulseHeap.heap[xxn] -= inNumSamples; - } - - // Retrieve impulses for this block - while (heapPeek(&unit->mImpulseHeap) < inNumSamples && unit->mImpulseHeap.size > 1) { - out[heapPop(&unit->mImpulseHeap)] = 1.f; - } - - RGET - for (int xxn = 0; xxn < inNumSamples; xxn++) { - float impulseResult = testWrapPhase(prevInc, phase); - if (impulseResult > 0.5f) { - int idx = rgen.irand(jitterWidth) + xxn; - if (idx < inNumSamples) { - out[idx] = 1.f; - } else if (unit->mImpulseHeap.size < heapEffectiveSize) { - heapInsert(&unit->mImpulseHeap, idx); - } - } - prevInc += incSlope; - phase += prevInc; - } - - unit->mPhase = phase; - unit->mPhaseIncrement = inc; -} - -void ImpulseJitter_next_kk(ImpulseJitter* unit, int inNumSamples) { - float* out = OUT(0); - double freq = IN0(0); - double inc = freq * unit->mFreqMul; - double off = IN0(1); - float jitterFracIn = IN0(2); - size_t heapEffectiveSize = static_cast(HEAP_MAX_SIZE / (12 * sc_log2(freq))); - - // Collect UGen state - double phase = unit->mPhase; - double prevInc = unit->mPhaseIncrement; - double prevOff = unit->mPhaseOffset; - - double incSlope = CALCSLOPE(inc, prevInc); - double phaseSlope = CALCSLOPE(off, prevOff); - bool phOffChanged = phaseSlope != 0.f; - - // The maximum distance an impulse can be displaced - int jitterWidth = static_cast(jitterFracIn * unit->mImpulseHeap.maxSize); - - // Zero out the output buffer - for (int xxn = 0; xxn < inNumSamples; xxn++) { - out[xxn] = 0.f; - } - - // Update the impulse table indices - for (int xxn = 1; xxn < unit->mImpulseHeap.size; xxn++) { - unit->mImpulseHeap.heap[xxn] -= inNumSamples; - } - - // Retrieve impulses for this block - while (heapPeek(&unit->mImpulseHeap) < inNumSamples && unit->mImpulseHeap.size > 1) { - out[heapPop(&unit->mImpulseHeap)] = 1.f; - } - - RGET - for (int xxn = 0; xxn < inNumSamples; xxn++) { - float impulseResult = testWrapPhase(prevInc, phase); - if (impulseResult > 0.5f) { - int idx = rgen.irand(jitterWidth) + xxn; - if (idx < inNumSamples) { - out[idx] = 1.f; - } else if (unit->mImpulseHeap.size < heapEffectiveSize) { - heapInsert(&unit->mImpulseHeap, idx); - } - } - if (phOffChanged) { - phase += phaseSlope; - testWrapPhase(prevInc, phase); - } - prevInc += incSlope; - phase += prevInc; - } - - unit->mPhase = phase; - unit->mPhaseOffset = off; - unit->mPhaseIncrement = inc; -} - -// Construct the ImpulseJitter -void ImpulseJitter_Ctor(ImpulseJitter* unit) { - unit->mPhaseIncrement = IN0(0) * unit->mFreqMul; - unit->mPhaseOffset = IN0(1); - unit->mFreqMul = static_cast(unit->mRate->mSampleDur); - unit->mImpulseHeap.maxSize = HEAP_MAX_SIZE; // hard coded for now - unit->mImpulseHeap.size = 1; - unit->mImpulseHeap.heap = (int*)RTAlloc(unit->mWorld, HEAP_MAX_SIZE * sizeof(int)); - - double initOff = unit->mPhaseOffset; - double initInc = unit->mPhaseIncrement; - double initPhase = sc_wrap(initOff, 0.0, 1.0); - - // Initial phase offset of 0 means output of 1 on first sample. - // Set phase to wrap point to trigger impulse on first sample - if (initPhase == 0.0 && initInc >= 0.0) { - initPhase = 1.0; // positive frequency trigger/wrap position - } - unit->mPhase = initPhase; - - UnitCalcFunc func; - switch (INRATE(0)) { - case calc_FullRate: - switch (INRATE(1)) { - case calc_ScalarRate: - func = (UnitCalcFunc)ImpulseJitter_next_ai; - //printf("Calc function set to ai\n"); - break; - case calc_BufRate: - func = (UnitCalcFunc)ImpulseJitter_next_ak; - //printf("Calc function set to ak\n"); - break; - case calc_FullRate: - func = (UnitCalcFunc)ImpulseJitter_next_aa; - //printf("Calc function set to aa\n"); - break; - } - break; - case calc_BufRate: - if (INRATE(1) == calc_ScalarRate) { - func = (UnitCalcFunc)ImpulseJitter_next_ki; - //printf("Calc function set to ki\n"); - } else { - func = (UnitCalcFunc)ImpulseJitter_next_kk; - //printf("Calc function set to kk\n"); - } - break; - case calc_ScalarRate: - if (INRATE(1) == calc_ScalarRate) { - func = (UnitCalcFunc)ImpulseJitter_next_ki; - //printf("Calc function set to ki\n"); - } else { - func = (UnitCalcFunc)ImpulseJitter_next_kk; - //printf("Calc function set to kk\n"); - } - break; - } - unit->mCalcFunc = func; - func(unit, 1); - - unit->mPhase = initPhase; - unit->mPhaseOffset = initOff; - unit->mPhaseIncrement = initInc; -} - -void ImpulseJitter_Dtor(ImpulseJitter* unit) { - RTFree(unit->mWorld, unit->mImpulseHeap.heap); -} - -// Represents a LoopPhasor UGen. -struct LoopPhasor : public Unit { - double m_level; // LoopPhasor output level (position of the phasor between `start` and `end`) - float m_prevTriggerStart; // previous value of trigger to return to start position - float m_prevTriggerFinish; // previous value of trigger to finish - bool m_triggerFinishState; // current state of finish trigger (true - finish; false - continue looping) -}; - -static void LoopPhasor_next_aa(LoopPhasor* unit, int inNumSamples); -static void LoopPhasor_next_ak(LoopPhasor* unit, int inNumSamples); -static void LoopPhasor_next_kk(LoopPhasor* unit, int inNumSamples); -static void LoopPhasor_Ctor(LoopPhasor* unit); - -// Construct the LoopPhasor -void LoopPhasor_Ctor(LoopPhasor* unit) { - // Set the calculation function - if (unit->mCalcRate == calc_FullRate) { - if (INRATE(0) == calc_FullRate) { - if (INRATE(1) == calc_FullRate) { - SETCALC(LoopPhasor_next_aa); - } else { - SETCALC(LoopPhasor_next_ak); - } - } else { - SETCALC(LoopPhasor_next_kk); - } - } else { - SETCALC(LoopPhasor_next_ak); - } - - // Initialize the triggers - unit->m_prevTriggerStart = IN0(0); - unit->m_prevTriggerFinish = IN0(1); - unit->m_triggerFinishState = false; - - // Initialize the output - unit->m_level = IN0(3); - ZOUT0(0) = static_cast(unit->m_level); -} - -// Calculates samples for a LoopPhasor.kr UGen -void LoopPhasor_next_kk(LoopPhasor* unit, int inNumSamples) { - // Pointer to output array - float* out = OUT(0); - - // Get new parameters of the LoopPhasor - float triggerReturnToStart = IN0(0); - float triggerFinish = IN0(1); - double rate = IN0(2); - double startPosition = IN0(3); - double endPosition = IN0(4); - double loopStart = IN0(5); - double loopEnd = IN0(6); - - // Get current state of the LoopPhasor - float previousTriggerReturnToStart = unit->m_prevTriggerStart; // trigger return to start - float previousTriggerFinish = unit->m_prevTriggerFinish; // trigger finish - double level = unit->m_level; - - // Handle trigger return to start - if (previousTriggerReturnToStart <= 0.f && triggerReturnToStart > 0.f) { - level = startPosition; - } - - // Handle trigger finish. This just flips the finish trigger. - if (previousTriggerFinish <= 0.f && triggerFinish > 0.f) { - unit->m_triggerFinishState = !(unit->m_triggerFinishState); - } - - // Compute output block - for (int xxn = 0; xxn < inNumSamples; xxn++) { - // If we haven't triggered completion - if (!unit->m_triggerFinishState) { - // If we're inside the looping part of the LoopPhasor - if (level >= loopStart && level <= loopEnd) { - level = sc_wrap(level, loopStart, loopEnd); - } else { - level = sc_wrap(level, startPosition, endPosition); - } - } - - // Otherwise we wrap up - else { - level = sc_max(level, startPosition); - level = sc_min(level, endPosition); - } - - out[xxn] = static_cast(level); - level += rate; - } - - // Update the state of the LoopPhasor - unit->m_prevTriggerStart = triggerReturnToStart; - unit->m_prevTriggerFinish = triggerFinish; - unit->m_level = level; -} - -// Calculates samples for a LoopPhasor.ar ugen with .kr parameters -void LoopPhasor_next_ak(LoopPhasor* unit, int inNumSamples) { - // Pointer to output array - float* out = OUT(0); - - // Get new parameters of the LoopPhasor - float *triggerReturnToStart = IN(0); - float *triggerFinish = IN(1); - double rate = IN0(2); - double startPosition = IN0(3); - double endPosition = IN0(4); - double loopStart = IN0(5); - double loopEnd = IN0(6); - - // Get current state of the LoopPhasor - float previousTriggerReturnToStart = unit->m_prevTriggerStart; - float previousTriggerFinish = unit->m_prevTriggerFinish; - double level = unit->m_level; - - // Compute output block - for (int xxn = 0; xxn < inNumSamples; xxn++) { - // If we reset to start - if (previousTriggerReturnToStart <= 0.f && triggerReturnToStart[xxn] > 0.f) { - float frac = 1.f - previousTriggerReturnToStart / (triggerReturnToStart[xxn] - previousTriggerReturnToStart); - level = startPosition + frac * rate; - } - - // Handle trigger finish. This just flips the finish trigger. - if (previousTriggerFinish <= 0.f && triggerFinish[xxn] > 0.f) { - unit->m_triggerFinishState = !(unit->m_triggerFinishState); - } - - // Wrapping: if we haven't triggered completion - if (!unit->m_triggerFinishState) { - // if we're inside the looping part of the LoopPhasor - if (level >= loopStart && level <= loopEnd) { - level = sc_wrap(level, loopStart, loopEnd); - } else { - level = sc_wrap(level, startPosition, endPosition); - } - } - - // Wrapping: if we have triggered completion - else { - level = sc_max(level, startPosition); - level = sc_min(level, endPosition); - } - - out[xxn] = static_cast(level); - level += rate; - previousTriggerReturnToStart = triggerReturnToStart[xxn]; - previousTriggerFinish = triggerFinish[xxn]; - } - - // update the state of the LoopPhasor - unit->m_prevTriggerStart = previousTriggerReturnToStart; - unit->m_prevTriggerFinish = previousTriggerFinish; - unit->m_level = level; -} - -// Calculates samples for a LoopPhasor.ar UGen with .ar parameters -void LoopPhasor_next_aa(LoopPhasor* unit, int inNumSamples) { - float* out = OUT(0); - - // Get new parameters of the LoopPhasor - float *triggerReturnToStart = IN(0); - float *triggerFinish = IN(1); - float *rate = IN(2); - double startPosition = IN0(3); - double endPosition = IN0(4); - double loopStart = IN0(5); - double loopEnd = IN0(6); - - // Get current state of the LoopPhasor - float previousTriggerReturnToStart = unit->m_prevTriggerStart; - float previousTriggerFinish = unit->m_prevTriggerFinish; - double level = unit->m_level; - - float *in = triggerReturnToStart; - float previn = previousTriggerReturnToStart; - - // Compute output block - for (int xxn = 0; xxn < inNumSamples; xxn++) { - // Handle trigger return to start - if (previousTriggerReturnToStart <= 0.f && triggerReturnToStart[xxn] > 0.f) { - float frac = 1.f - previousTriggerReturnToStart / (triggerReturnToStart[xxn] - previousTriggerReturnToStart); - level = startPosition + frac * rate[xxn]; - } - - // Handle trigger finish. This just flips the finish trigger. - if (previousTriggerFinish <= 0.f && triggerFinish[xxn] > 0.f) { - unit->m_triggerFinishState = !(unit->m_triggerFinishState); - } - - // Wrapping: if we haven't triggered completion - if (!unit->m_triggerFinishState) { - // If we're inside the looping part of the LoopPhasor - if (level >= loopStart && level <= loopEnd) { - level = sc_wrap(level, loopStart, loopEnd); - } else { - level = sc_wrap(level, startPosition, endPosition); - } - } - - // Wrapping: if we have triggered completion - else { - level = sc_max(level, startPosition); - level = sc_min(level, endPosition); - } - - out[xxn] = static_cast(level); - level += rate[xxn]; - previousTriggerReturnToStart = triggerReturnToStart[xxn]; - previousTriggerFinish = triggerFinish[xxn]; - } - - // update the state of the LoopPhasor at the end of the calculation block - unit->m_prevTriggerStart = previousTriggerReturnToStart; - unit->m_prevTriggerFinish = previousTriggerFinish; - unit->m_level = level; -} +InterfaceTable *ft; PluginLoad(flexplugin_generators) { ft = inTable; diff --git a/src/generators/impulseDropout.cpp b/src/generators/impulseDropout.cpp new file mode 100644 index 0000000..64d217d --- /dev/null +++ b/src/generators/impulseDropout.cpp @@ -0,0 +1,320 @@ +/* +File: impulseDropout.cpp +Author: Jeff Martin + +Description: +The ImpulseDropout UGen + +Copyright © 2025 by Jeffrey Martin. All rights reserved. +Website: https://www.jeffreymartincomposer.com + +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 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +#include "impulseDropout.hpp" +extern InterfaceTable *ft; + +// This is a copy of the static function from LFUGens.cpp in server/plugins. +// It detects if a phasor is out-of-bounds, triggers, and wraps [0, 1]. +static inline float testWrapPhase(double prev_inc, double& phase) { + if (prev_inc < 0.f) { // negative freqs + if (phase <= 0.f) { + phase += 1.f; + if (phase <= 0.f) { // catch large phase jumps + phase -= sc_ceil(phase); + } + return 1.f; + } else { + return 0.f; + } + } else { // positive freqs + if (phase >= 1.f) { + phase -= 1.f; + if (phase >= 1.f) { + phase -= sc_floor(phase); + } + return 1.f; + } else { + return 0.f; + } + } +} + +void ImpulseDropout_next_aa(ImpulseDropout* unit, int inNumSamples) { + float* out = OUT(0); + float* freqIn = IN(0); + float* offIn = IN(1); + float dropProbIn = IN0(2); + + // Collect UGen state + double phase = unit->mPhase; + double inc = unit->mPhaseIncrement; + float freqMul = unit->mFreqMul; + double prevOff = unit->mPhaseOffset; + + RGET + for (int xxn = 0; xxn < inNumSamples; xxn++) { + float impulseResult = testWrapPhase(inc, phase); + // Drop the impulse if necessary + if (impulseResult > 0.5f && rgen.frand() < dropProbIn) { + impulseResult = 0.f; + } + double off = static_cast(offIn[xxn]); + double offInc = off - prevOff; + phase += offInc; + testWrapPhase(inc, phase); + inc = freqIn[xxn] * freqMul; + out[xxn] = impulseResult; + phase += inc; + prevOff = off; + } + + unit->mPhase = phase; + unit->mPhaseOffset = prevOff; + unit->mPhaseIncrement = inc; +} + +void ImpulseDropout_next_ai(ImpulseDropout* unit, int inNumSamples) { + float* out = OUT(0); + float freqIn = IN0(0); + float dropProbIn = IN0(2); + + // Collect UGen state + double phase = unit->mPhase; + double inc = unit->mPhaseIncrement; + float freqMul = unit->mFreqMul; + + RGET + for (int xxn = 0; xxn < inNumSamples; xxn++) { + float impulseResult = testWrapPhase(inc, phase); + // Drop the impulse if necessary + if (impulseResult > 0.5f && rgen.frand() < dropProbIn) { + impulseResult = 0.f; + } + inc = freqIn * freqMul; + out[xxn] = impulseResult; + phase += inc; + } + + unit->mPhase = phase; + unit->mPhaseIncrement = inc; +} + +void ImpulseDropout_next_ak(ImpulseDropout* unit, int inNumSamples) { + float* out = OUT(0); + float freqIn = IN0(0); + double off = IN0(1); + float dropProbIn = IN0(2); + + // Collect UGen state + double phase = unit->mPhase; + double inc = unit->mPhaseIncrement; + float freqMul = unit->mFreqMul; + double prevOff = unit->mPhaseOffset; + + double offSlope = CALCSLOPE(off, prevOff); + bool offChanged = offSlope != 0.f; + + RGET + for (int xxn = 0; xxn < inNumSamples; xxn++) { + float impulseResult = testWrapPhase(inc, phase); + // Drop the impulse if necessary + if (impulseResult > 0.5f && rgen.frand() < dropProbIn) { + impulseResult = 0.f; + } + if (offChanged) { + phase += offSlope; + testWrapPhase(inc, phase); + } + inc = freqIn * freqMul; + out[xxn] = impulseResult; + phase += inc; + } + + unit->mPhase = phase; + unit->mPhaseOffset = off; + unit->mPhaseIncrement = inc; +} + +void ImpulseDropout_next_ki(ImpulseDropout* unit, int inNumSamples) { + float* out = OUT(0); + double inc = IN0(0) * unit->mFreqMul; + float dropProbIn = IN0(2); + + // Collect UGen state + double phase = unit->mPhase; + double prevInc = unit->mPhaseIncrement; + + double incSlope = CALCSLOPE(inc, prevInc); + + RGET + for (int xxn = 0; xxn < inNumSamples; xxn++) { + float impulseResult = testWrapPhase(prevInc, phase); + // Drop the impulse if necessary + if (impulseResult > 0.5f && rgen.frand() < dropProbIn) { + impulseResult = 0.f; + } + out[xxn] = impulseResult; + prevInc += incSlope; + phase += prevInc; + } + + unit->mPhase = phase; + unit->mPhaseIncrement = inc; +} + +void ImpulseDropout_next_kk(ImpulseDropout* unit, int inNumSamples) { + float* out = OUT(0); + double inc = IN0(0) * unit->mFreqMul; + double off = IN0(1); + float dropProbIn = IN0(2); + + // Collect UGen state + double phase = unit->mPhase; + double prevInc = unit->mPhaseIncrement; + double prevOff = unit->mPhaseOffset; + + double incSlope = CALCSLOPE(inc, prevInc); + double phaseSlope = CALCSLOPE(off, prevOff); + bool phOffChanged = phaseSlope != 0.f; + + RGET + for (int xxn = 0; xxn < inNumSamples; xxn++) { + float impulseResult = testWrapPhase(prevInc, phase); + // Drop the impulse if necessary + if (impulseResult > 0.5f && rgen.frand() < dropProbIn) { + impulseResult = 0.f; + } + if (phOffChanged) { + phase += phaseSlope; + testWrapPhase(prevInc, phase); + } + out[xxn] = impulseResult; + prevInc += incSlope; + phase += prevInc; + } + + unit->mPhase = phase; + unit->mPhaseOffset = off; + unit->mPhaseIncrement = inc; +} + +void ImpulseDropout_next_ii(ImpulseDropout* unit, int inNumSamples) { + float* out = OUT(0); + float dropProbIn = IN0(2); + + // Collect UGen state + double inc = unit->mPhaseIncrement; + double phase = unit->mPhase; + + RGET + for (int xxn = 0; xxn < inNumSamples; xxn++) { + float impulseResult = testWrapPhase(inc, phase); + // Drop the impulse if necessary + if (impulseResult > 0.5f && rgen.frand() < dropProbIn) { + impulseResult = 0.f; + } + out[xxn] = impulseResult; + phase += inc; + } + + unit->mPhase = phase; +} + +void ImpulseDropout_next_ik(ImpulseDropout* unit, int inNumSamples) { + float* out = OUT(0); + double off = IN0(1); + float dropProbIn = IN0(2); + + // Collect UGen state + double phase = unit->mPhase; + double inc = unit->mPhaseIncrement; + double prevOff = unit->mPhaseOffset; + + double phaseSlope = CALCSLOPE(off, prevOff); + bool phOffChanged = phaseSlope != 0.f; + + RGET + for (int xxn = 0; xxn < inNumSamples; xxn++) { + float impulseResult = testWrapPhase(inc, phase); + // Drop the impulse if necessary + if (impulseResult > 0.5f && rgen.frand() < dropProbIn) { + impulseResult = 0.f; + } + if (phOffChanged) { + phase += phaseSlope; + testWrapPhase(inc, phase); + } + out[xxn] = impulseResult; + phase += inc; + } + + unit->mPhase = phase; + unit->mPhaseOffset = off; +} + +// Construct the ImpulseDropout +void ImpulseDropout_Ctor(ImpulseDropout* unit) { + unit->mPhaseIncrement = IN0(0) * unit->mFreqMul; + unit->mPhaseOffset = IN0(1); + unit->mFreqMul = static_cast(unit->mRate->mSampleDur); + + double initOff = unit->mPhaseOffset; + double initInc = unit->mPhaseIncrement; + double initPhase = sc_wrap(initOff, 0.0, 1.0); + + // Initial phase offset of 0 means output of 1 on first sample. + // Set phase to wrap point to trigger impulse on first sample + if (initPhase == 0.0 && initInc >= 0.0) { + initPhase = 1.0; // positive frequency trigger/wrap position + } + unit->mPhase = initPhase; + + UnitCalcFunc func; + switch (INRATE(0)) { + case calc_FullRate: + switch (INRATE(1)) { + case calc_ScalarRate: + func = (UnitCalcFunc)ImpulseDropout_next_ai; + break; + case calc_BufRate: + func = (UnitCalcFunc)ImpulseDropout_next_ak; + break; + case calc_FullRate: + func = (UnitCalcFunc)ImpulseDropout_next_aa; + break; + } + break; + case calc_BufRate: + if (INRATE(1) == calc_ScalarRate) { + func = (UnitCalcFunc)ImpulseDropout_next_ki; + } else { + func = (UnitCalcFunc)ImpulseDropout_next_kk; + } + break; + case calc_ScalarRate: + if (INRATE(1) == calc_ScalarRate) { + func = (UnitCalcFunc)ImpulseDropout_next_ki; + } else { + func = (UnitCalcFunc)ImpulseDropout_next_kk; + } + break; + } + unit->mCalcFunc = func; + func(unit, 1); + + unit->mPhase = initPhase; + unit->mPhaseOffset = initOff; + unit->mPhaseIncrement = initInc; +} \ No newline at end of file diff --git a/src/rubberband/rubberband.hpp b/src/generators/impulseDropout.hpp similarity index 52% rename from src/rubberband/rubberband.hpp rename to src/generators/impulseDropout.hpp index 88fd995..eb79286 100644 --- a/src/rubberband/rubberband.hpp +++ b/src/generators/impulseDropout.hpp @@ -1,11 +1,11 @@ /* -File: rubberband.hpp +File: impulseDropout.hpp Author: Jeff Martin Description: -A high quality, formant-preserving live pitch shifter and time stretcher using the RubberBand library. +The ImpulseDropout UGen -Copyright © 2026 by Jeffrey Martin. All rights reserved. +Copyright © 2025 by Jeffrey Martin. All rights reserved. Website: https://www.jeffreymartincomposer.com This program is free software: you can redistribute it and/or modify @@ -24,5 +24,17 @@ along with this program. If not, see . #pragma once #include "SC_PlugIn.h" +#define HEAP_MAX_SIZE 1024 -extern InterfaceTable *ft; \ No newline at end of file +// Represents an ImpulseDropout UGen. +struct ImpulseDropout : public Unit { + double mPhase, mPhaseOffset, mPhaseIncrement; + float mFreqMul; +}; + +void ImpulseDropout_Ctor(ImpulseDropout* unit); +void ImpulseDropout_next_aa(ImpulseDropout* unit, int inNumSamples); +void ImpulseDropout_next_ai(ImpulseDropout* unit, int inNumSamples); +void ImpulseDropout_next_ak(ImpulseDropout* unit, int inNumSamples); +void ImpulseDropout_next_ki(ImpulseDropout* unit, int inNumSamples); +void ImpulseDropout_next_kk(ImpulseDropout* unit, int inNumSamples); diff --git a/src/generators/impulseJitter.cpp b/src/generators/impulseJitter.cpp new file mode 100644 index 0000000..95ebfc0 --- /dev/null +++ b/src/generators/impulseJitter.cpp @@ -0,0 +1,393 @@ +/* +File: impulseJitter.cpp +Author: Jeff Martin + +Description: +The ImpulseJitter UGen + +Copyright © 2025 by Jeffrey Martin. All rights reserved. +Website: https://www.jeffreymartincomposer.com + +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 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +#include "impulseJitter.hpp" +extern InterfaceTable *ft; + +// This is a copy of the static function from LFUGens.cpp in server/plugins. +// It detects if a phasor is out-of-bounds, triggers, and wraps [0, 1]. +static inline float testWrapPhase(double prev_inc, double& phase) { + if (prev_inc < 0.f) { // negative freqs + if (phase <= 0.f) { + phase += 1.f; + if (phase <= 0.f) { // catch large phase jumps + phase -= sc_ceil(phase); + } + return 1.f; + } else { + return 0.f; + } + } else { // positive freqs + if (phase >= 1.f) { + phase -= 1.f; + if (phase >= 1.f) { + phase -= sc_floor(phase); + } + return 1.f; + } else { + return 0.f; + } + } +} + + +void ImpulseJitter_next_aa(ImpulseJitter* unit, int inNumSamples) { + float* out = OUT(0); + float* freq = IN(0); + float* offIn = IN(1); + float jitterFracIn = IN0(2); + + // Collect UGen state + double phase = unit->mPhase; + double inc = unit->mPhaseIncrement; + float freqMul = unit->mFreqMul; + double prevOff = unit->mPhaseOffset; + + // The maximum distance an impulse can be displaced + int jitterWidth = static_cast(jitterFracIn * unit->mImpulseHeap.maxSize); + + // Zero out the output buffer + for (int xxn = 0; xxn < inNumSamples; xxn++) { + out[xxn] = 0.f; + } + + // Update the impulse table indices + for (int xxn = 1; xxn < unit->mImpulseHeap.size; xxn++) { + unit->mImpulseHeap.heap[xxn] -= inNumSamples; + } + + // Retrieve impulses for this block + while (heapPeek(&unit->mImpulseHeap) < inNumSamples && unit->mImpulseHeap.size > 1) { + out[heapPop(&unit->mImpulseHeap)] = 1.f; + } + + RGET + for (int xxn = 0; xxn < inNumSamples; xxn++) { + size_t heapEffectiveSize = static_cast(HEAP_MAX_SIZE / (12 * sc_log2(freq[xxn]))); + float impulseResult = testWrapPhase(inc, phase); + if (impulseResult > 0.5f) { + int idx = rgen.irand(jitterWidth) + xxn; + if (idx < inNumSamples) { + out[idx] = 1.f; + } else if (unit->mImpulseHeap.size < heapEffectiveSize) { + heapInsert(&unit->mImpulseHeap, idx); + } + } + double off = static_cast(offIn[xxn]); + double offInc = off - prevOff; + phase += offInc; + testWrapPhase(inc, phase); + inc = freq[xxn] * freqMul; + phase += inc; + prevOff = off; + } + + unit->mPhase = phase; + unit->mPhaseOffset = prevOff; + unit->mPhaseIncrement = inc; +} + +void ImpulseJitter_next_ai(ImpulseJitter* unit, int inNumSamples) { + float* out = OUT(0); + float freq = IN0(0); + float jitterFracIn = IN0(2); + size_t heapEffectiveSize = static_cast(HEAP_MAX_SIZE / (12 * sc_log2(freq))); + + // Collect UGen state + double phase = unit->mPhase; + double inc = unit->mPhaseIncrement; + float freqMul = unit->mFreqMul; + + // The maximum distance an impulse can be displaced + int jitterWidth = static_cast(jitterFracIn * unit->mImpulseHeap.maxSize); + + // Zero out the output buffer + for (int xxn = 0; xxn < inNumSamples; xxn++) { + out[xxn] = 0.f; + } + + // Update the impulse table indices + for (int xxn = 1; xxn < unit->mImpulseHeap.size; xxn++) { + unit->mImpulseHeap.heap[xxn] -= inNumSamples; + } + + // Retrieve impulses for this block + while (heapPeek(&unit->mImpulseHeap) < inNumSamples && unit->mImpulseHeap.size > 1) { + out[heapPop(&unit->mImpulseHeap)] = 1.f; + } + + RGET + for (int xxn = 0; xxn < inNumSamples; xxn++) { + float impulseResult = testWrapPhase(inc, phase); + if (impulseResult > 0.5f) { + int idx = rgen.irand(jitterWidth) + xxn; + if (idx < inNumSamples) { + out[idx] = 1.f; + } else if (unit->mImpulseHeap.size < heapEffectiveSize) { + heapInsert(&unit->mImpulseHeap, idx); + } + } + inc = freq * freqMul; + phase += inc; + } + + unit->mPhase = phase; + unit->mPhaseIncrement = inc; +} + +void ImpulseJitter_next_ak(ImpulseJitter* unit, int inNumSamples) { + float* out = OUT(0); + float freq = IN0(0); + double off = IN0(1); + float jitterFracIn = IN0(2); + size_t heapEffectiveSize = static_cast(HEAP_MAX_SIZE / (12 * sc_log2(freq))); + + // Collect UGen state + double phase = unit->mPhase; + double inc = unit->mPhaseIncrement; + float freqMul = unit->mFreqMul; + double prevOff = unit->mPhaseOffset; + + double offSlope = CALCSLOPE(off, prevOff); + bool offChanged = offSlope != 0.f; + + // The maximum distance an impulse can be displaced + int jitterWidth = static_cast(jitterFracIn * unit->mImpulseHeap.maxSize); + + // Zero out the output buffer + for (int xxn = 0; xxn < inNumSamples; xxn++) { + out[xxn] = 0.f; + } + + // Update the impulse table indices + for (int xxn = 1; xxn < unit->mImpulseHeap.size; xxn++) { + unit->mImpulseHeap.heap[xxn] -= inNumSamples; + } + + // Retrieve impulses for this block + while (heapPeek(&unit->mImpulseHeap) < inNumSamples && unit->mImpulseHeap.size > 1) { + out[heapPop(&unit->mImpulseHeap)] = 1.f; + } + + RGET + for (int xxn = 0; xxn < inNumSamples; xxn++) { + float impulseResult = testWrapPhase(inc, phase); + if (impulseResult > 0.5f) { + int idx = rgen.irand(jitterWidth) + xxn; + if (idx < inNumSamples) { + out[idx] = 1.f; + } else if (unit->mImpulseHeap.size < heapEffectiveSize) { + heapInsert(&unit->mImpulseHeap, idx); + } + } + if (offChanged) { + phase += offSlope; + testWrapPhase(inc, phase); + } + inc = freq * freqMul; + phase += inc; + } + + unit->mPhase = phase; + unit->mPhaseOffset = off; + unit->mPhaseIncrement = inc; +} + +void ImpulseJitter_next_ki(ImpulseJitter* unit, int inNumSamples) { + float* out = OUT(0); + double freq = IN0(0); + double inc = freq * unit->mFreqMul; + float jitterFracIn = IN0(2); + size_t heapEffectiveSize = static_cast(HEAP_MAX_SIZE / (12 * sc_log2(freq))); + + // Collect UGen state + double phase = unit->mPhase; + double prevInc = unit->mPhaseIncrement; + + double incSlope = CALCSLOPE(inc, prevInc); + + // The maximum distance an impulse can be displaced + int jitterWidth = static_cast(jitterFracIn * unit->mImpulseHeap.maxSize); + + // Zero out the output buffer + for (int xxn = 0; xxn < inNumSamples; xxn++) { + out[xxn] = 0.f; + } + + // Update the impulse table indices + for (int xxn = 1; xxn < unit->mImpulseHeap.size; xxn++) { + unit->mImpulseHeap.heap[xxn] -= inNumSamples; + } + + // Retrieve impulses for this block + while (heapPeek(&unit->mImpulseHeap) < inNumSamples && unit->mImpulseHeap.size > 1) { + out[heapPop(&unit->mImpulseHeap)] = 1.f; + } + + RGET + for (int xxn = 0; xxn < inNumSamples; xxn++) { + float impulseResult = testWrapPhase(prevInc, phase); + if (impulseResult > 0.5f) { + int idx = rgen.irand(jitterWidth) + xxn; + if (idx < inNumSamples) { + out[idx] = 1.f; + } else if (unit->mImpulseHeap.size < heapEffectiveSize) { + heapInsert(&unit->mImpulseHeap, idx); + } + } + prevInc += incSlope; + phase += prevInc; + } + + unit->mPhase = phase; + unit->mPhaseIncrement = inc; +} + +void ImpulseJitter_next_kk(ImpulseJitter* unit, int inNumSamples) { + float* out = OUT(0); + double freq = IN0(0); + double inc = freq * unit->mFreqMul; + double off = IN0(1); + float jitterFracIn = IN0(2); + size_t heapEffectiveSize = static_cast(HEAP_MAX_SIZE / (12 * sc_log2(freq))); + + // Collect UGen state + double phase = unit->mPhase; + double prevInc = unit->mPhaseIncrement; + double prevOff = unit->mPhaseOffset; + + double incSlope = CALCSLOPE(inc, prevInc); + double phaseSlope = CALCSLOPE(off, prevOff); + bool phOffChanged = phaseSlope != 0.f; + + // The maximum distance an impulse can be displaced + int jitterWidth = static_cast(jitterFracIn * unit->mImpulseHeap.maxSize); + + // Zero out the output buffer + for (int xxn = 0; xxn < inNumSamples; xxn++) { + out[xxn] = 0.f; + } + + // Update the impulse table indices + for (int xxn = 1; xxn < unit->mImpulseHeap.size; xxn++) { + unit->mImpulseHeap.heap[xxn] -= inNumSamples; + } + + // Retrieve impulses for this block + while (heapPeek(&unit->mImpulseHeap) < inNumSamples && unit->mImpulseHeap.size > 1) { + out[heapPop(&unit->mImpulseHeap)] = 1.f; + } + + RGET + for (int xxn = 0; xxn < inNumSamples; xxn++) { + float impulseResult = testWrapPhase(prevInc, phase); + if (impulseResult > 0.5f) { + int idx = rgen.irand(jitterWidth) + xxn; + if (idx < inNumSamples) { + out[idx] = 1.f; + } else if (unit->mImpulseHeap.size < heapEffectiveSize) { + heapInsert(&unit->mImpulseHeap, idx); + } + } + if (phOffChanged) { + phase += phaseSlope; + testWrapPhase(prevInc, phase); + } + prevInc += incSlope; + phase += prevInc; + } + + unit->mPhase = phase; + unit->mPhaseOffset = off; + unit->mPhaseIncrement = inc; +} + +// Construct the ImpulseJitter +void ImpulseJitter_Ctor(ImpulseJitter* unit) { + unit->mPhaseIncrement = IN0(0) * unit->mFreqMul; + unit->mPhaseOffset = IN0(1); + unit->mFreqMul = static_cast(unit->mRate->mSampleDur); + unit->mImpulseHeap.maxSize = HEAP_MAX_SIZE; // hard coded for now + unit->mImpulseHeap.size = 1; + unit->mImpulseHeap.heap = (int*)RTAlloc(unit->mWorld, HEAP_MAX_SIZE * sizeof(int)); + + double initOff = unit->mPhaseOffset; + double initInc = unit->mPhaseIncrement; + double initPhase = sc_wrap(initOff, 0.0, 1.0); + + // Initial phase offset of 0 means output of 1 on first sample. + // Set phase to wrap point to trigger impulse on first sample + if (initPhase == 0.0 && initInc >= 0.0) { + initPhase = 1.0; // positive frequency trigger/wrap position + } + unit->mPhase = initPhase; + + UnitCalcFunc func; + switch (INRATE(0)) { + case calc_FullRate: + switch (INRATE(1)) { + case calc_ScalarRate: + func = (UnitCalcFunc)ImpulseJitter_next_ai; + //printf("Calc function set to ai\n"); + break; + case calc_BufRate: + func = (UnitCalcFunc)ImpulseJitter_next_ak; + //printf("Calc function set to ak\n"); + break; + case calc_FullRate: + func = (UnitCalcFunc)ImpulseJitter_next_aa; + //printf("Calc function set to aa\n"); + break; + } + break; + case calc_BufRate: + if (INRATE(1) == calc_ScalarRate) { + func = (UnitCalcFunc)ImpulseJitter_next_ki; + //printf("Calc function set to ki\n"); + } else { + func = (UnitCalcFunc)ImpulseJitter_next_kk; + //printf("Calc function set to kk\n"); + } + break; + case calc_ScalarRate: + if (INRATE(1) == calc_ScalarRate) { + func = (UnitCalcFunc)ImpulseJitter_next_ki; + //printf("Calc function set to ki\n"); + } else { + func = (UnitCalcFunc)ImpulseJitter_next_kk; + //printf("Calc function set to kk\n"); + } + break; + } + unit->mCalcFunc = func; + func(unit, 1); + + unit->mPhase = initPhase; + unit->mPhaseOffset = initOff; + unit->mPhaseIncrement = initInc; +} + +void ImpulseJitter_Dtor(ImpulseJitter* unit) { + RTFree(unit->mWorld, unit->mImpulseHeap.heap); +} \ No newline at end of file diff --git a/src/generators/impulseJitter.hpp b/src/generators/impulseJitter.hpp new file mode 100644 index 0000000..d1deaf6 --- /dev/null +++ b/src/generators/impulseJitter.hpp @@ -0,0 +1,43 @@ +/* +File: impulseJitter.hpp +Author: Jeff Martin + +Description: +The ImpulseJitter UGen + +Copyright © 2025 by Jeffrey Martin. All rights reserved. +Website: https://www.jeffreymartincomposer.com + +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 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +#pragma once +#include "SC_PlugIn.h" +#include "arrayheap.hpp" + +#define HEAP_MAX_SIZE 1024 +// Represents an ImpulseJitter UGen. +struct ImpulseJitter : public Unit { + double mPhase, mPhaseOffset, mPhaseIncrement; + float mFreqMul; + IntMinHeap mImpulseHeap; +}; + +void ImpulseJitter_Ctor(ImpulseJitter* unit); +void ImpulseJitter_Dtor(ImpulseJitter* unit); +void ImpulseJitter_next_aa(ImpulseJitter* unit, int inNumSamples); +void ImpulseJitter_next_ai(ImpulseJitter* unit, int inNumSamples); +void ImpulseJitter_next_ak(ImpulseJitter* unit, int inNumSamples); +void ImpulseJitter_next_ki(ImpulseJitter* unit, int inNumSamples); +void ImpulseJitter_next_kk(ImpulseJitter* unit, int inNumSamples); diff --git a/src/generators/loopPhasor.cpp b/src/generators/loopPhasor.cpp new file mode 100644 index 0000000..d00107c --- /dev/null +++ b/src/generators/loopPhasor.cpp @@ -0,0 +1,232 @@ +/* +File: loopPhasor.cpp +Author: Jeff Martin + +Description: +The LoopPhasor UGen + +Copyright © 2025 by Jeffrey Martin. All rights reserved. +Website: https://www.jeffreymartincomposer.com + +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 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +#include "loopPhasor.hpp" +extern InterfaceTable *ft; + +// Construct the LoopPhasor +void LoopPhasor_Ctor(LoopPhasor* unit) { + // Set the calculation function + if (unit->mCalcRate == calc_FullRate) { + if (INRATE(0) == calc_FullRate) { + if (INRATE(1) == calc_FullRate) { + SETCALC(LoopPhasor_next_aa); + } else { + SETCALC(LoopPhasor_next_ak); + } + } else { + SETCALC(LoopPhasor_next_kk); + } + } else { + SETCALC(LoopPhasor_next_ak); + } + + // Initialize the triggers + unit->m_prevTriggerStart = IN0(0); + unit->m_prevTriggerFinish = IN0(1); + unit->m_triggerFinishState = false; + + // Initialize the output + unit->m_level = IN0(3); + ZOUT0(0) = static_cast(unit->m_level); +} + +// Calculates samples for a LoopPhasor.kr UGen +void LoopPhasor_next_kk(LoopPhasor* unit, int inNumSamples) { + // Pointer to output array + float* out = OUT(0); + + // Get new parameters of the LoopPhasor + float triggerReturnToStart = IN0(0); + float triggerFinish = IN0(1); + double rate = IN0(2); + double startPosition = IN0(3); + double endPosition = IN0(4); + double loopStart = IN0(5); + double loopEnd = IN0(6); + + // Get current state of the LoopPhasor + float previousTriggerReturnToStart = unit->m_prevTriggerStart; // trigger return to start + float previousTriggerFinish = unit->m_prevTriggerFinish; // trigger finish + double level = unit->m_level; + + // Handle trigger return to start + if (previousTriggerReturnToStart <= 0.f && triggerReturnToStart > 0.f) { + level = startPosition; + } + + // Handle trigger finish. This just flips the finish trigger. + if (previousTriggerFinish <= 0.f && triggerFinish > 0.f) { + unit->m_triggerFinishState = !(unit->m_triggerFinishState); + } + + // Compute output block + for (int xxn = 0; xxn < inNumSamples; xxn++) { + // If we haven't triggered completion + if (!unit->m_triggerFinishState) { + // If we're inside the looping part of the LoopPhasor + if (level >= loopStart && level <= loopEnd) { + level = sc_wrap(level, loopStart, loopEnd); + } else { + level = sc_wrap(level, startPosition, endPosition); + } + } + + // Otherwise we wrap up + else { + level = sc_max(level, startPosition); + level = sc_min(level, endPosition); + } + + out[xxn] = static_cast(level); + level += rate; + } + + // Update the state of the LoopPhasor + unit->m_prevTriggerStart = triggerReturnToStart; + unit->m_prevTriggerFinish = triggerFinish; + unit->m_level = level; +} + +// Calculates samples for a LoopPhasor.ar ugen with .kr parameters +void LoopPhasor_next_ak(LoopPhasor* unit, int inNumSamples) { + // Pointer to output array + float* out = OUT(0); + + // Get new parameters of the LoopPhasor + float *triggerReturnToStart = IN(0); + float *triggerFinish = IN(1); + double rate = IN0(2); + double startPosition = IN0(3); + double endPosition = IN0(4); + double loopStart = IN0(5); + double loopEnd = IN0(6); + + // Get current state of the LoopPhasor + float previousTriggerReturnToStart = unit->m_prevTriggerStart; + float previousTriggerFinish = unit->m_prevTriggerFinish; + double level = unit->m_level; + + // Compute output block + for (int xxn = 0; xxn < inNumSamples; xxn++) { + // If we reset to start + if (previousTriggerReturnToStart <= 0.f && triggerReturnToStart[xxn] > 0.f) { + float frac = 1.f - previousTriggerReturnToStart / (triggerReturnToStart[xxn] - previousTriggerReturnToStart); + level = startPosition + frac * rate; + } + + // Handle trigger finish. This just flips the finish trigger. + if (previousTriggerFinish <= 0.f && triggerFinish[xxn] > 0.f) { + unit->m_triggerFinishState = !(unit->m_triggerFinishState); + } + + // Wrapping: if we haven't triggered completion + if (!unit->m_triggerFinishState) { + // if we're inside the looping part of the LoopPhasor + if (level >= loopStart && level <= loopEnd) { + level = sc_wrap(level, loopStart, loopEnd); + } else { + level = sc_wrap(level, startPosition, endPosition); + } + } + + // Wrapping: if we have triggered completion + else { + level = sc_max(level, startPosition); + level = sc_min(level, endPosition); + } + + out[xxn] = static_cast(level); + level += rate; + previousTriggerReturnToStart = triggerReturnToStart[xxn]; + previousTriggerFinish = triggerFinish[xxn]; + } + + // update the state of the LoopPhasor + unit->m_prevTriggerStart = previousTriggerReturnToStart; + unit->m_prevTriggerFinish = previousTriggerFinish; + unit->m_level = level; +} + +// Calculates samples for a LoopPhasor.ar UGen with .ar parameters +void LoopPhasor_next_aa(LoopPhasor* unit, int inNumSamples) { + float* out = OUT(0); + + // Get new parameters of the LoopPhasor + float *triggerReturnToStart = IN(0); + float *triggerFinish = IN(1); + float *rate = IN(2); + double startPosition = IN0(3); + double endPosition = IN0(4); + double loopStart = IN0(5); + double loopEnd = IN0(6); + + // Get current state of the LoopPhasor + float previousTriggerReturnToStart = unit->m_prevTriggerStart; + float previousTriggerFinish = unit->m_prevTriggerFinish; + double level = unit->m_level; + + float *in = triggerReturnToStart; + float previn = previousTriggerReturnToStart; + + // Compute output block + for (int xxn = 0; xxn < inNumSamples; xxn++) { + // Handle trigger return to start + if (previousTriggerReturnToStart <= 0.f && triggerReturnToStart[xxn] > 0.f) { + float frac = 1.f - previousTriggerReturnToStart / (triggerReturnToStart[xxn] - previousTriggerReturnToStart); + level = startPosition + frac * rate[xxn]; + } + + // Handle trigger finish. This just flips the finish trigger. + if (previousTriggerFinish <= 0.f && triggerFinish[xxn] > 0.f) { + unit->m_triggerFinishState = !(unit->m_triggerFinishState); + } + + // Wrapping: if we haven't triggered completion + if (!unit->m_triggerFinishState) { + // If we're inside the looping part of the LoopPhasor + if (level >= loopStart && level <= loopEnd) { + level = sc_wrap(level, loopStart, loopEnd); + } else { + level = sc_wrap(level, startPosition, endPosition); + } + } + + // Wrapping: if we have triggered completion + else { + level = sc_max(level, startPosition); + level = sc_min(level, endPosition); + } + + out[xxn] = static_cast(level); + level += rate[xxn]; + previousTriggerReturnToStart = triggerReturnToStart[xxn]; + previousTriggerFinish = triggerFinish[xxn]; + } + + // update the state of the LoopPhasor at the end of the calculation block + unit->m_prevTriggerStart = previousTriggerReturnToStart; + unit->m_prevTriggerFinish = previousTriggerFinish; + unit->m_level = level; +} diff --git a/src/generators/loopPhasor.hpp b/src/generators/loopPhasor.hpp new file mode 100644 index 0000000..d710fdd --- /dev/null +++ b/src/generators/loopPhasor.hpp @@ -0,0 +1,39 @@ +/* +File: loopPhasor.hpp +Author: Jeff Martin + +Description: +The LoopPhasor UGen + +Copyright © 2025 by Jeffrey Martin. All rights reserved. +Website: https://www.jeffreymartincomposer.com + +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 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +#pragma once +#include "SC_PlugIn.h" + +// Represents a LoopPhasor UGen. +struct LoopPhasor : public Unit { + double m_level; // LoopPhasor output level (position of the phasor between `start` and `end`) + float m_prevTriggerStart; // previous value of trigger to return to start position + float m_prevTriggerFinish; // previous value of trigger to finish + bool m_triggerFinishState; // current state of finish trigger (true - finish; false - continue looping) +}; + +void LoopPhasor_Ctor(LoopPhasor* unit); +void LoopPhasor_next_aa(LoopPhasor* unit, int inNumSamples); +void LoopPhasor_next_ak(LoopPhasor* unit, int inNumSamples); +void LoopPhasor_next_kk(LoopPhasor* unit, int inNumSamples); diff --git a/src/pv/pvCFreeze.cpp b/src/pv/pvCFreeze.cpp index 33785f3..67700a1 100644 --- a/src/pv/pvCFreeze.cpp +++ b/src/pv/pvCFreeze.cpp @@ -23,10 +23,10 @@ along with this program. If not, see . */ #include "pvCFreeze.hpp" -#include "SC_Constants.h" -#include "SC_InterfaceTable.h" #include "FFT_UGens.h" +extern InterfaceTable *ft; + void PV_CFreeze_next(PV_CFreeze *unit, int inNumSamples) { PV_GET_BUF float freezeState = IN0(1); diff --git a/src/pv/pvOther.cpp b/src/pv/pvOther.cpp index 8b43068..55f1efa 100644 --- a/src/pv/pvOther.cpp +++ b/src/pv/pvOther.cpp @@ -25,6 +25,8 @@ along with this program. If not, see . #include "pvOther.hpp" #include "FFT_UGens.h" +extern InterfaceTable *ft; + void PV_MagSqueeze_next(PV_MagSqueeze *unit, int inNumSamples) { PV_GET_BUF float low = IN0(1); diff --git a/src/pv/pvStretch.cpp b/src/pv/pvStretch.cpp index 6f31bee..514abc5 100644 --- a/src/pv/pvStretch.cpp +++ b/src/pv/pvStretch.cpp @@ -28,6 +28,8 @@ along with this program. If not, see . #include "FFT_UGens.h" #include +extern InterfaceTable *ft; + void PV_PlayBufStretch_Ctor(PV_PlayBufStretch *unit) { // Connect to the STFT buffer. For now, we only allow this in the constructor. float fbufnum = IN0(1); From 805f1cc2cbe29f492a46665b4506b215b895890b Mon Sep 17 00:00:00 2001 From: Jeff Martin Date: Sun, 14 Jun 2026 20:00:33 -0500 Subject: [PATCH 07/25] rough version of playbufstretch completed --- CMakeLists.txt | 1 + src/pv/PV_PlayBufStretch.schelp | 55 ++++++ src/pv/pv.cpp | 2 +- src/pv/pv.sc | 4 +- src/pv/pvStretch.cpp | 293 ++++++++++++++++++++++++++++++-- src/pv/pvStretch.hpp | 32 ++++ 6 files changed, 371 insertions(+), 16 deletions(-) create mode 100644 src/pv/PV_PlayBufStretch.schelp diff --git a/CMakeLists.txt b/CMakeLists.txt index 313d504..bc4d081 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -130,6 +130,7 @@ install(FILES src/pv/PV_CFreeze.schelp src/pv/PV_MagMirror.schelp src/pv/PV_MagSqueeze.schelp + src/pv/PV_PlayBufStretch.schelp src/pv/PV_MagXFade.schelp src/rubberband/RubberBandPS.schelp src/rubberband/RubberBandStretcher.schelp diff --git a/src/pv/PV_PlayBufStretch.schelp b/src/pv/PV_PlayBufStretch.schelp new file mode 100644 index 0000000..b87a5bd --- /dev/null +++ b/src/pv/PV_PlayBufStretch.schelp @@ -0,0 +1,55 @@ +class:: PV_PlayBufStretch +summary:: Phase vocoder buffer player for time stretching +related:: Classes/PV_RecordBuf, Classes/PV_BufRd +categories:: Libraries>FlexUGens, UGens>FFT +related:: Guides/FFT-Overview + +Description:: + +PV_PlayBufStretch is a phase vocoder buffer player for time stretching. + +classmethods:: + +method::new + +argument::buffer +The FFT buffer + +argument::stftBuffer +The buffer with STFT data created by PV_RecordBuf + +argument::startPos +The start position (0.0 to 1.0). Normally you would set this once and let it be. +However, you can change it at any time--if it changes, playback will jump immediately +to the new start position. Keep in mind that the first frame played does not use phase +vocoder computation, so you may or may not like the sound this produces. + +argument::rate +The playback rate. 1.0 is normal; slower rates result in a time stretch, and faster rates result in time shrink. Negative rates result in backwards playback. + +argument::phaseLock +Whether (1.0) or not (0.0) to apply phase locking. Phase locking is used to avoid the "phasy" sound +of a phase vocoder which happens when adjacent bins drift out of sync with each other. +Phase locking produces a cleaner sound. + +argument::loop +Whether or not to loop when the end of the stftBuffer is reached + +argument::doneAction +The action to take after playback is completed + +Examples:: + +code:: +{ + var sig, chain1, chain2; + sig = SoundIn.ar(0); + chain1 = FFT(LocalBuf(2048), sig); + chain1 = PV_MagAbove(chain1, 0.5); + chain2 = PV_MagSqueeze(chain1, 0.5, 1.4); + chain1 = PV_MagXFade(chain1, chain2, 0.5); + sig = IFFT(chain1); + sig = Pan2.ar(sig); + Out.ar(0, sig); +}.play; +:: \ No newline at end of file diff --git a/src/pv/pv.cpp b/src/pv/pv.cpp index c2decc0..ba9adb3 100644 --- a/src/pv/pv.cpp +++ b/src/pv/pv.cpp @@ -34,6 +34,6 @@ PluginLoad(PV_flexplugins) { DefineSimpleUnit(PV_MagMirror); DefineSimpleUnit(PV_MagSqueeze); DefineSimpleUnit(PV_MagXFade); - DefineSimpleUnit(PV_PlayBufStretch); + DefineDtorUnit(PV_PlayBufStretch); DefineDtorUnit(PV_CFreeze); } diff --git a/src/pv/pv.sc b/src/pv/pv.sc index 542a93c..ee8bd66 100644 --- a/src/pv/pv.sc +++ b/src/pv/pv.sc @@ -75,7 +75,7 @@ PV_MagXFade : PV_ChainUGen { // A phase vocoder buffer player PV_PlayBufStretch : PV_ChainUGen { *new { - arg buffer, stftBuffer, startPos, rate, loop=0.0, doneAction=0; - ^this.multiNew('control', buffer, stftBuffer, startPos, rate, loop, doneAction); + arg buffer, stftBuffer, startPos=0.0, rate=1.0, phaseLock=1.0, loop=0.0, doneAction=0; + ^this.multiNew('control', buffer, stftBuffer, startPos, rate, phaseLock, loop, doneAction); } } \ No newline at end of file diff --git a/src/pv/pvStretch.cpp b/src/pv/pvStretch.cpp index 514abc5..580c4e5 100644 --- a/src/pv/pvStretch.cpp +++ b/src/pv/pvStretch.cpp @@ -24,9 +24,9 @@ along with this program. If not, see . #include "pvStretch.hpp" #include "SC_Constants.h" -#include "SC_InterfaceTable.h" -#include "FFT_UGens.h" #include +#include +#define INTERP(a, b, pos) (a + (b-a) * pos) extern InterfaceTable *ft; @@ -37,6 +37,10 @@ void PV_PlayBufStretch_Ctor(PV_PlayBufStretch *unit) { if (bufnum >= unit->mWorld->mNumSndBufs) bufnum = 0; unit->m_fbufnum = fbufnum; unit->m_buf = unit->mWorld->mSndBufs + bufnum; + unit->m_outFramePrev = nullptr; + unit->m_frameNext = nullptr; + unit->m_framePrev1 = nullptr; + unit->m_framePrev2 = nullptr; // Configure position float startPos = sc_clip(IN0(2), 0.0, 1.0); @@ -48,11 +52,30 @@ void PV_PlayBufStretch_Ctor(PV_PlayBufStretch *unit) { OUT0(0) = IN0(0); } +void PV_PlayBufStretch_Dtor(PV_PlayBufStretch *unit) { + if (unit->m_outFramePrev) { + RTFree(unit->mWorld, unit->m_outFramePrev); + } + if (unit->m_frameNext) { + RTFree(unit->mWorld, unit->m_frameNext); + } + if (unit->m_framePrev1) { + RTFree(unit->mWorld, unit->m_framePrev1); + } + if (unit->m_framePrev2) { + RTFree(unit->mWorld, unit->m_framePrev2); + } +} + void PV_PlayBufStretch_next(PV_PlayBufStretch *unit, int inNumSamples) { PV_GET_BUF float startPos = sc_clip(IN0(2), 0.0, 1.0); float rate = IN0(3); float loop = IN0(4); + bool phaseLock = false; + if (IN0(5) != 0.f) { + phaseLock = true; + } // This section of the code is for acquiring the STFT buffer and information about it. // It has to be run every time because we cannot be sure the user has not freed the buffer. @@ -103,6 +126,20 @@ void PV_PlayBufStretch_next(PV_PlayBufStretch *unit, int inNumSamples) { return; } + // The first time through, we need to allocate the SCPolarBuf storage in the UGen. + if (!unit->m_outFramePrev) { + // This is an annoying way to have to allocate memory, + // but it seems to be necessary based on how SCPolarBuf is defined. + float *outFramePrev = (float*)RTAlloc(unit->mWorld, stftBufFftSize * sizeof(float)); + float *frameNext = (float*)RTAlloc(unit->mWorld, stftBufFftSize * sizeof(float)); + float *framePrev1 = (float*)RTAlloc(unit->mWorld, stftBufFftSize * sizeof(float)); + float *framePrev2 = (float*)RTAlloc(unit->mWorld, stftBufFftSize * sizeof(float)); + unit->m_outFramePrev = (SCPolarBuf*)outFramePrev; + unit->m_frameNext = (SCPolarBuf*)frameNext; + unit->m_framePrev1 = (SCPolarBuf*)framePrev1; + unit->m_framePrev2 = (SCPolarBuf*)framePrev2; + } + if (startPos != unit->m_startPos) { unit->m_startPos = startPos; unit->m_firstFrame = true; @@ -110,6 +147,20 @@ void PV_PlayBufStretch_next(PV_PlayBufStretch *unit, int inNumSamples) { // Now that we've run setup, we're ready to read STFT data and perform phase vocoder stretching. + // First we need to figure out where we are, and if that means we need to loop or quit. + float newPos = unit->m_pos + 1/rate; + if (newPos > stftFrames - 1) { + if (loop) { + unit->m_firstFrame = true; + startPos = 0; + } else { + OUT0(0) = -1.f; + RELEASE_SNDBUF_SHARED(stftBuf); + DoneAction(static_cast(IN0(5)), unit); + return; + } + } + // The first frame has to be cloned directly from the STFT buffer with no phase adjustments. // This is essential to make sure that subsequent phase calculations are correctly aligned. if (unit->m_firstFrame) { @@ -129,26 +180,242 @@ void PV_PlayBufStretch_next(PV_PlayBufStretch *unit, int inNumSamples) { // Copy the FFT data over SCPolarBuf *p = ToPolarApx(buf); const float *currentFftFrame = stftData + (xxi * stftBufFftSize); - p->dc = currentFftFrame[0]; - p->nyq = currentFftFrame[1]; - for (size_t xxn = 2, xxk = 0; xxn < stftBufFftSize; xxn+=2, xxk++) { - // For some reason the phase is stored first, then the magnitude. - // This prevents a direct cast to SCPolarBuf, unfortunately. - p->bin[xxk].phase = currentFftFrame[xxn]; - p->bin[xxk].mag = currentFftFrame[xxn+1]; - } + // Fill the output buffer + fillPolarBuf(currentFftFrame, p, stftBufFftSize); + // We also need to log the output to the UGen + fillPolarBuf(currentFftFrame, unit->m_outFramePrev, stftBufFftSize); // We have to advance by one frame because we need to be able to compute frequency for time stretching. unit->m_pos = static_cast(xxi + 1); + unit->m_firstFrame = false; } // For frames other than the first frame, we'll need to perform phase computation. else { - float newPos; + SCPolarBuf *p = ToPolarApx(buf); + if (std::abs(std::round(newPos)-newPos) < 1e-3) { + size_t pos = static_cast(std::round(newPos)); + size_t lpos = pos - 1; + fillPolarBuf(stftData + (pos * stftBufFftSize), unit->m_frameNext, stftBufFftSize); + fillPolarBuf(stftData + (pos * stftBufFftSize), unit->m_framePrev1, stftBufFftSize); + Stretch2( + unit->m_frameNext, + unit->m_framePrev1, + p, + unit->m_outFramePrev, + stftBufFftSize, + stftBufHopSize, + phaseLock + ); + } else { + size_t lo = static_cast(std::floor(newPos)); + size_t hi = static_cast(std::ceil(newPos)); + fillPolarBuf(stftData + (hi * stftBufFftSize), unit->m_frameNext, stftBufFftSize); + fillPolarBuf(stftData + (lo * stftBufFftSize), unit->m_framePrev1, stftBufFftSize); + fillPolarBuf(stftData + ((lo-1) * stftBufFftSize), unit->m_framePrev2, stftBufFftSize); + Stretch3( + unit->m_frameNext, + unit->m_framePrev1, + unit->m_framePrev2, + p, + unit->m_outFramePrev, + newPos, + stftBufFftSize, + stftBufHopSize, + phaseLock + ); + } + copyPolarBuf(p, unit->m_outFramePrev, static_cast(numbins)); + unit->m_pos = newPos; } RELEASE_SNDBUF_SHARED(stftBuf); +} + +/// Computes a single frame of STFT data for time stretching. +/// The assumption is that we are positioned exactly at `frame`, and we therefore +/// just need framePrev to compute the instantaneous frequency. We also do not +/// need to perform any magnitude or frequency interpolation. +/// +/// \param frame The current STFT frame +/// \param framePrev The previous STFT frame +/// \param [out] outFrame The output STFT frame +/// \param outFramePrev The previously computed output STFT frame +/// \param fftSize The FFT size +/// \param hopSize The hop size +/// \param phaseLock Whether or not to apply phase locking +void Stretch2( + const SCPolarBuf *frame, + const SCPolarBuf *framePrev, + SCPolarBuf *outFrame, + const SCPolarBuf *outFramePrev, + size_t fftSize, + size_t hopSize, + bool phaseLock) { + outFrame->dc = frame->dc; + outFrame->nyq = frame->nyq; + for (size_t xxk = 0; xxk < fftSize/2-1; xxk++) { + outFrame->bin[xxk].mag = frame->bin[xxk].mag; + + // Puckette-style phase locking + if (phaseLock) { + // Compute the instantaneous frequency + float omegaK = twopi * (xxk+1) / fftSize; + float phaseInc = frame->bin[xxk].phase - framePrev->bin[xxk].phase - hopSize * omegaK; + phaseInc = std::fmod(phaseInc + pi, twopi) - pi; + float instantaneousFreq = omegaK + phaseInc/hopSize; + + // In Puckette-style phase locking, we make a substitution for the previous phase, + // in order to "lock" phases of adjacent bins together. + float prevPhase = 0.0; + if (xxk == 0) { + std::complex prevBinKMinus1 = std::polar(outFramePrev->dc, 0.f); + std::complex prevBinK = std::polar(outFramePrev->bin[xxk].mag, outFramePrev->bin[xxk].phase); + std::complex prevBinKPlus1 = std::polar(outFramePrev->bin[xxk+1].mag, outFramePrev->bin[xxk+1].phase); + prevPhase = std::arg(prevBinK - prevBinKMinus1 - prevBinKPlus1); + } else if (xxk == fftSize/2-2) { + std::complex prevBinKMinus1 = std::polar(outFramePrev->bin[xxk-1].mag, outFramePrev->bin[xxk-1].phase); + std::complex prevBinK = std::polar(outFramePrev->bin[xxk].mag, outFramePrev->bin[xxk].phase); + std::complex prevBinKPlus1 = std::polar(outFramePrev->nyq, 0.f); + prevPhase = std::arg(prevBinK - prevBinKMinus1 - prevBinKPlus1); + } else { + std::complex prevBinKMinus1 = std::polar(outFramePrev->bin[xxk-1].mag, outFramePrev->bin[xxk-1].phase); + std::complex prevBinK = std::polar(outFramePrev->bin[xxk].mag, outFramePrev->bin[xxk].phase); + std::complex prevBinKPlus1 = std::polar(outFramePrev->bin[xxk+1].mag, outFramePrev->bin[xxk+1].phase); + prevPhase = std::arg(prevBinK - prevBinKMinus1 - prevBinKPlus1); + } + + // Compute the new phase + outFrame->bin[xxk].phase = prevPhase + hopSize * instantaneousFreq; + } else { + // Compute the instantaneous frequency + float omegaK = twopi * (xxk+1) / fftSize; + float phaseInc = frame->bin[xxk].phase - framePrev->bin[xxk].phase - hopSize * omegaK; + phaseInc = std::fmod(phaseInc + pi, twopi) - pi; + float instantaneousFreq = omegaK + phaseInc/hopSize; + + // Compute the new phase + outFrame->bin[xxk].phase = outFramePrev->bin[xxk].phase + hopSize * instantaneousFreq; + } + } +} + +/// Computes a single frame of STFT data for time stretching. +/// The assumption is that we are positioned between framePrev1 and frameNext. +/// This means we will need to interpolate frequency data. So we will need to +/// compute two frequencies for each bin, and that means we need three STFT frames. +/// +/// \param frameNext The next STFT frame +/// \param framePrev1 The previous STFT frame +/// \param framePrev2 The previous STFT frame before that (required for instantaneous frequency interpolation) +/// \param [out] outFrame The output STFT frame +/// \param outFramePrev The previously computed output STFT frame +/// \param pos The position between framePrev1 and frameNext (0 < pos < 1) +/// \param fftSize The FFT size +/// \param hopSize The hop size +/// \param phaseLock Whether or not to apply phase locking +void Stretch3( + const SCPolarBuf *frameNext, + const SCPolarBuf *framePrev1, + const SCPolarBuf *framePrev2, + SCPolarBuf *outFrame, + const SCPolarBuf *outFramePrev, + float pos, + size_t fftSize, + size_t hopSize, + bool phaseLock) { + outFrame->dc = INTERP(framePrev1->dc, frameNext->dc, pos); + outFrame->nyq = INTERP(framePrev1->nyq, frameNext->nyq, pos); + for (size_t xxk = 0; xxk < fftSize/2-1; xxk++) { + outFrame->bin[xxk].mag = INTERP(framePrev1->bin[xxk].mag, frameNext->bin[xxk].mag, pos); + + // Puckette-style phase locking + if (phaseLock) { + float omegaK = twopi * (xxk+1) / fftSize; + + // Compute the next instantaneous frequency + float phaseInc = frameNext->bin[xxk].phase - framePrev1->bin[xxk].phase - hopSize * omegaK; + phaseInc = std::fmod(phaseInc + pi, twopi) - pi; + float instantaneousFreqNext = omegaK + phaseInc/hopSize; + + // Compute the previous instantaneous frequency + phaseInc = framePrev1->bin[xxk].phase - framePrev2->bin[xxk].phase - hopSize * omegaK; + phaseInc = std::fmod(phaseInc + pi, twopi) - pi; + float instantaneousFreqPrev = omegaK + phaseInc/hopSize; + + // Interpolate the instantaneous frequency + float instantaneousFreq = INTERP(instantaneousFreqPrev, instantaneousFreqNext, pos); + + // In Puckette-style phase locking, we make a substitution for the previous phase, + // in order to "lock" phases of adjacent bins together. + float prevPhase = 0.0; + if (xxk == 0) { + std::complex prevBinKMinus1 = std::polar(outFramePrev->dc, 0.f); + std::complex prevBinK = std::polar(outFramePrev->bin[xxk].mag, outFramePrev->bin[xxk].phase); + std::complex prevBinKPlus1 = std::polar(outFramePrev->bin[xxk+1].mag, outFramePrev->bin[xxk+1].phase); + prevPhase = std::arg(prevBinK - prevBinKMinus1 - prevBinKPlus1); + } else if (xxk == fftSize/2-2) { + std::complex prevBinKMinus1 = std::polar(outFramePrev->bin[xxk-1].mag, outFramePrev->bin[xxk-1].phase); + std::complex prevBinK = std::polar(outFramePrev->bin[xxk].mag, outFramePrev->bin[xxk].phase); + std::complex prevBinKPlus1 = std::polar(outFramePrev->nyq, 0.f); + prevPhase = std::arg(prevBinK - prevBinKMinus1 - prevBinKPlus1); + } else { + std::complex prevBinKMinus1 = std::polar(outFramePrev->bin[xxk-1].mag, outFramePrev->bin[xxk-1].phase); + std::complex prevBinK = std::polar(outFramePrev->bin[xxk].mag, outFramePrev->bin[xxk].phase); + std::complex prevBinKPlus1 = std::polar(outFramePrev->bin[xxk+1].mag, outFramePrev->bin[xxk+1].phase); + prevPhase = std::arg(prevBinK - prevBinKMinus1 - prevBinKPlus1); + } + + // Compute the new phase + outFrame->bin[xxk].phase = prevPhase + hopSize * instantaneousFreq; + } else { + float omegaK = twopi * (xxk+1) / fftSize; - // if no loop and we're past the last frame, handle this condition later - // DoneAction(static_cast(IN0(5)), unit); + // Compute the next instantaneous frequency + float phaseInc = frameNext->bin[xxk].phase - framePrev1->bin[xxk].phase - hopSize * omegaK; + phaseInc = std::fmod(phaseInc + pi, twopi) - pi; + float instantaneousFreqNext = omegaK + phaseInc/hopSize; + + // Compute the previous instantaneous frequency + phaseInc = framePrev1->bin[xxk].phase - framePrev2->bin[xxk].phase - hopSize * omegaK; + phaseInc = std::fmod(phaseInc + pi, twopi) - pi; + float instantaneousFreqPrev = omegaK + phaseInc/hopSize; + + // Interpolate the instantaneous frequency + float instantaneousFreq = INTERP(instantaneousFreqPrev, instantaneousFreqNext, pos); + + // Compute the new phase + outFrame->bin[xxk].phase = outFramePrev->bin[xxk].phase + hopSize * instantaneousFreq; + } + } +} + +/// Fills a SCPolarBuf with saved STFT data from a single frame +/// +/// \param fftBuf The FFT frame from the STFT buffer +/// \param [out] polarBuf The SCPolarBuf to copy to +/// \param fftSize The FFT size +void fillPolarBuf(const float *fftBuf, SCPolarBuf *polarBuf, size_t fftSize) { + polarBuf->dc = fftBuf[0]; + polarBuf->nyq = fftBuf[1]; + for (size_t xxn = 2, xxk = 0; xxn < fftSize; xxn+=2, xxk++) { + // For some reason the phase is stored first, then the magnitude. + // This prevents a direct cast to SCPolarBuf, unfortunately. + polarBuf->bin[xxk].phase = fftBuf[xxn]; + polarBuf->bin[xxk].mag = fftBuf[xxn+1]; + } +} + +/// Copies data from one SCPolarBuf to another +/// +/// \param sourceBuf The source buffer +/// \param [out] destBuf The destination buffer +/// \param numbins The number of bins in the SCPolarBuf (fftSize/2-1) +void copyPolarBuf(const SCPolarBuf *sourceBuf, SCPolarBuf *destBuf, size_t numbins) { + destBuf->dc = sourceBuf->dc; + destBuf->nyq = sourceBuf->nyq; + for (size_t xxn = 0; xxn < numbins; xxn++) { + destBuf->bin[xxn].mag = sourceBuf->bin[xxn].mag; + destBuf->bin[xxn].phase = sourceBuf->bin[xxn].phase; + } } \ No newline at end of file diff --git a/src/pv/pvStretch.hpp b/src/pv/pvStretch.hpp index 3b93169..aebfea8 100644 --- a/src/pv/pvStretch.hpp +++ b/src/pv/pvStretch.hpp @@ -23,6 +23,7 @@ along with this program. If not, see . */ #pragma once #include "SC_Unit.h" +#include "FFT_UGens.h" struct PV_PlayBufStretch : public Unit { // The index of the buffer with STFT data @@ -31,6 +32,16 @@ struct PV_PlayBufStretch : public Unit { // The buffer with STFT data SndBuf *m_buf; + // The most recent output STFT frame + SCPolarBuf *m_outFramePrev; + + // The most recent output STFT frame + SCPolarBuf *m_frameNext; + // The most recent output STFT frame + SCPolarBuf *m_framePrev1; + // The most recent output STFT frame + SCPolarBuf *m_framePrev2; + // Between 0 and 1; represents the start position of playback. // If it jumps during playback, playback will be restarted // at the new m_startPos. @@ -47,4 +58,25 @@ struct PV_PlayBufStretch : public Unit { }; void PV_PlayBufStretch_Ctor(PV_PlayBufStretch *unit); +void PV_PlayBufStretch_Dtor(PV_PlayBufStretch *unit); void PV_PlayBufStretch_next(PV_PlayBufStretch *unit, int inNumSamples); +void Stretch2( + const SCPolarBuf *frame, + const SCPolarBuf *framePrev, + SCPolarBuf *outFrame, + const SCPolarBuf *outFramePrev, + size_t fftSize, + size_t hopSize, + bool phaseLock); +void Stretch3( + const SCPolarBuf *frameNext, + const SCPolarBuf *framePrev1, + const SCPolarBuf *framePrev2, + SCPolarBuf *outFrame, + const SCPolarBuf *outFramePrev, + float pos, + size_t fftSize, + size_t hopSize, + bool phaseLock); +void fillPolarBuf(const float *fftBuf, SCPolarBuf *polarBuf, size_t fftSize); +void copyPolarBuf(const SCPolarBuf *sourceBuf, SCPolarBuf *destBuf, size_t numbins); \ No newline at end of file From d17c7b39795958dc50801041876fae589230db73 Mon Sep 17 00:00:00 2001 From: Jeff Martin Date: Wed, 17 Jun 2026 10:28:17 -0500 Subject: [PATCH 08/25] commenting --- .gitignore | 3 ++ src/pv/pvStretch.cpp | 48 ++++++++++++++++++---------- src/pv/pvStretch.hpp | 76 ++++++++++++++++++++++++++++++++++++-------- 3 files changed, 97 insertions(+), 30 deletions(-) diff --git a/.gitignore b/.gitignore index 0ba1c63..19759f7 100644 --- a/.gitignore +++ b/.gitignore @@ -67,3 +67,6 @@ vcpkg_installed/ # test output & cache Testing/ .cache/ + +# don't need the vscode settings folder +.vscode \ No newline at end of file diff --git a/src/pv/pvStretch.cpp b/src/pv/pvStretch.cpp index 580c4e5..3ec5c5f 100644 --- a/src/pv/pvStretch.cpp +++ b/src/pv/pvStretch.cpp @@ -69,14 +69,7 @@ void PV_PlayBufStretch_Dtor(PV_PlayBufStretch *unit) { void PV_PlayBufStretch_next(PV_PlayBufStretch *unit, int inNumSamples) { PV_GET_BUF - float startPos = sc_clip(IN0(2), 0.0, 1.0); - float rate = IN0(3); - float loop = IN0(4); - bool phaseLock = false; - if (IN0(5) != 0.f) { - phaseLock = true; - } - + // This section of the code is for acquiring the STFT buffer and information about it. // It has to be run every time because we cannot be sure the user has not freed the buffer. // We also have to verify important details about the buffer to make sure we can read from it at all. @@ -139,6 +132,10 @@ void PV_PlayBufStretch_next(PV_PlayBufStretch *unit, int inNumSamples) { unit->m_framePrev1 = (SCPolarBuf*)framePrev1; unit->m_framePrev2 = (SCPolarBuf*)framePrev2; } + + float startPos = sc_clip(IN0(2), 0.0, 1.0); + float rate = IN0(3); + float loop = IN0(4); if (startPos != unit->m_startPos) { unit->m_startPos = startPos; @@ -156,7 +153,7 @@ void PV_PlayBufStretch_next(PV_PlayBufStretch *unit, int inNumSamples) { } else { OUT0(0) = -1.f; RELEASE_SNDBUF_SHARED(stftBuf); - DoneAction(static_cast(IN0(5)), unit); + DoneAction(static_cast(IN0(6)), unit); return; } } @@ -172,7 +169,7 @@ void PV_PlayBufStretch_next(PV_PlayBufStretch *unit, int inNumSamples) { } else { OUT0(0) = -1.f; RELEASE_SNDBUF_SHARED(stftBuf); - DoneAction(static_cast(IN0(5)), unit); + DoneAction(static_cast(IN0(6)), unit); return; } } @@ -182,7 +179,10 @@ void PV_PlayBufStretch_next(PV_PlayBufStretch *unit, int inNumSamples) { const float *currentFftFrame = stftData + (xxi * stftBufFftSize); // Fill the output buffer fillPolarBuf(currentFftFrame, p, stftBufFftSize); - // We also need to log the output to the UGen + + // We always need to store the resultant FFT frame in the UGen. + // It is used in the next call to PV_PlayBufStretch_next, for + // phase vocoder calculations. fillPolarBuf(currentFftFrame, unit->m_outFramePrev, stftBufFftSize); // We have to advance by one frame because we need to be able to compute frequency for time stretching. @@ -192,12 +192,20 @@ void PV_PlayBufStretch_next(PV_PlayBufStretch *unit, int inNumSamples) { // For frames other than the first frame, we'll need to perform phase computation. else { + bool phaseLock = false; + if (IN0(5) != 0.f) { + phaseLock = true; + } SCPolarBuf *p = ToPolarApx(buf); - if (std::abs(std::round(newPos)-newPos) < 1e-3) { - size_t pos = static_cast(std::round(newPos)); - size_t lpos = pos - 1; - fillPolarBuf(stftData + (pos * stftBufFftSize), unit->m_frameNext, stftBufFftSize); - fillPolarBuf(stftData + (pos * stftBufFftSize), unit->m_framePrev1, stftBufFftSize); + size_t ipos = static_cast(std::round(newPos)); + if (std::abs(ipos-newPos) < 1e-3) { + // If we're right smack on a specific FFT frame, we don't + // need to do any magnitude or frequency interpolation, so + // we only need the current and previous FFT frames from the buffer. + size_t lpos = ipos - 1; + fillPolarBuf(stftData + (ipos * stftBufFftSize), unit->m_frameNext, stftBufFftSize); + fillPolarBuf(stftData + (ipos * stftBufFftSize), unit->m_framePrev1, stftBufFftSize); + // Render the output FFT frame Stretch2( unit->m_frameNext, unit->m_framePrev1, @@ -208,11 +216,16 @@ void PV_PlayBufStretch_next(PV_PlayBufStretch *unit, int inNumSamples) { phaseLock ); } else { + // Otherwise we're between two FFT frames, and we're going to have to + // interpolate magnitude and frequency data. size_t lo = static_cast(std::floor(newPos)); size_t hi = static_cast(std::ceil(newPos)); + // We are in between these two frames fillPolarBuf(stftData + (hi * stftBufFftSize), unit->m_frameNext, stftBufFftSize); fillPolarBuf(stftData + (lo * stftBufFftSize), unit->m_framePrev1, stftBufFftSize); + // This is the frame right before that. It's needed to compute the previous instantaneous frequencies. fillPolarBuf(stftData + ((lo-1) * stftBufFftSize), unit->m_framePrev2, stftBufFftSize); + // Render the output FFT frame Stretch3( unit->m_frameNext, unit->m_framePrev1, @@ -225,6 +238,9 @@ void PV_PlayBufStretch_next(PV_PlayBufStretch *unit, int inNumSamples) { phaseLock ); } + // We always need to store the resultant FFT frame in the UGen. + // It is used in the next call to PV_PlayBufStretch_next, for + // phase vocoder calculations. copyPolarBuf(p, unit->m_outFramePrev, static_cast(numbins)); unit->m_pos = newPos; } diff --git a/src/pv/pvStretch.hpp b/src/pv/pvStretch.hpp index aebfea8..0c5b232 100644 --- a/src/pv/pvStretch.hpp +++ b/src/pv/pvStretch.hpp @@ -25,41 +25,62 @@ along with this program. If not, see . #include "SC_Unit.h" #include "FFT_UGens.h" +/// Stores the state of a PV_PlayBufStretch UGen instance struct PV_PlayBufStretch : public Unit { - // The index of the buffer with STFT data + /// The index of the buffer with STFT data float m_fbufnum; - // The buffer with STFT data + /// The buffer with STFT data SndBuf *m_buf; - // The most recent output STFT frame + /// The most recent output STFT frame SCPolarBuf *m_outFramePrev; - // The most recent output STFT frame + /// The next STFT frame after the current position SCPolarBuf *m_frameNext; - // The most recent output STFT frame + + /// The STFT frame right before the current position SCPolarBuf *m_framePrev1; - // The most recent output STFT frame + + /// The STFT frame two positions before the current position SCPolarBuf *m_framePrev2; - // Between 0 and 1; represents the start position of playback. - // If it jumps during playback, playback will be restarted - // at the new m_startPos. + /// Between 0 and 1; represents the start position of playback. + /// If it jumps during playback, playback will be restarted + /// at the new m_startPos. float m_startPos; - // A fractional STFT frame index. Unlike m_startPos, it corresponds to the - // integer index of the current STFT frame (from 0 to M-1). - // Used for interpolating position. + /// A fractional STFT frame index. Unlike m_startPos, it corresponds to the + /// integer index of the current STFT frame (from 0 to M-1). + /// Used for interpolating position. float m_pos; - // For the first frame, we need to read phase data directly - // instead of computing it. + /// For the first frame, we need to read phase data directly + /// instead of computing it. bool m_firstFrame; }; +/// Initializes a new PV_PlayBufStretch UGen void PV_PlayBufStretch_Ctor(PV_PlayBufStretch *unit); + +/// Frees memory created by the PV_PlayBufStretch UGen void PV_PlayBufStretch_Dtor(PV_PlayBufStretch *unit); + +/// Computes the next FFT frame requested by the PV_PlayBufStretch UGen void PV_PlayBufStretch_next(PV_PlayBufStretch *unit, int inNumSamples); + +/// Computes a single frame of STFT data for time stretching. +/// The assumption is that we are positioned exactly at `frame`, and we therefore +/// just need framePrev to compute the instantaneous frequency. We also do not +/// need to perform any magnitude or frequency interpolation. +/// +/// \param frame The current STFT frame +/// \param framePrev The previous STFT frame +/// \param [out] outFrame The output STFT frame +/// \param outFramePrev The previously computed output STFT frame +/// \param fftSize The FFT size +/// \param hopSize The hop size +/// \param phaseLock Whether or not to apply phase locking void Stretch2( const SCPolarBuf *frame, const SCPolarBuf *framePrev, @@ -68,6 +89,21 @@ void Stretch2( size_t fftSize, size_t hopSize, bool phaseLock); + +/// Computes a single frame of STFT data for time stretching. +/// The assumption is that we are positioned between framePrev1 and frameNext. +/// This means we will need to interpolate frequency data. So we will need to +/// compute two frequencies for each bin, and that means we need three STFT frames. +/// +/// \param frameNext The next STFT frame +/// \param framePrev1 The previous STFT frame +/// \param framePrev2 The previous STFT frame before that (required for instantaneous frequency interpolation) +/// \param [out] outFrame The output STFT frame +/// \param outFramePrev The previously computed output STFT frame +/// \param pos The position between framePrev1 and frameNext (0 < pos < 1) +/// \param fftSize The FFT size +/// \param hopSize The hop size +/// \param phaseLock Whether or not to apply phase locking void Stretch3( const SCPolarBuf *frameNext, const SCPolarBuf *framePrev1, @@ -78,5 +114,17 @@ void Stretch3( size_t fftSize, size_t hopSize, bool phaseLock); + +/// Fills a SCPolarBuf with saved STFT data from a single frame +/// +/// \param fftBuf The FFT frame from the STFT buffer +/// \param [out] polarBuf The SCPolarBuf to copy to +/// \param fftSize The FFT size void fillPolarBuf(const float *fftBuf, SCPolarBuf *polarBuf, size_t fftSize); + +/// Copies data from one SCPolarBuf to another +/// +/// \param sourceBuf The source buffer +/// \param [out] destBuf The destination buffer +/// \param numbins The number of bins in the SCPolarBuf (fftSize/2-1) void copyPolarBuf(const SCPolarBuf *sourceBuf, SCPolarBuf *destBuf, size_t numbins); \ No newline at end of file From 0bfe377be4143ccc54f70355061077750170b22c Mon Sep 17 00:00:00 2001 From: Jeff Martin Date: Thu, 18 Jun 2026 14:10:06 -0500 Subject: [PATCH 09/25] more work on stretch --- src/pv/pvStretch.cpp | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/pv/pvStretch.cpp b/src/pv/pvStretch.cpp index 3ec5c5f..c1203d0 100644 --- a/src/pv/pvStretch.cpp +++ b/src/pv/pvStretch.cpp @@ -107,8 +107,9 @@ void PV_PlayBufStretch_next(PV_PlayBufStretch *unit, int inNumSamples) { // Basic information size_t stftBufFftSize = bufData[0]; - size_t stftBufHopSize = static_cast(bufData[1] * stftBufFftSize); // in frames, not fraction + size_t stftBufHopSize = static_cast(bufData[1] * stftBufFftSize); // in frames, not fraction int stftBufWinType = static_cast(bufData[2]); // -1, 0, or 1. This information is probably extraneous. + // std::cout << "FFT size: " << stftBufFftSize << " Hop size: " << stftBufHopSize << " Win type: " << stftBufWinType << " STFT frames " << stftFrames << "\n"; if (stftBufFftSize != buf->samples) { OUT0(0) = -1.f; @@ -135,17 +136,19 @@ void PV_PlayBufStretch_next(PV_PlayBufStretch *unit, int inNumSamples) { float startPos = sc_clip(IN0(2), 0.0, 1.0); float rate = IN0(3); - float loop = IN0(4); + float loop = IN0(5); + // std::cout << "Rate: " << rate << " Loop: " << loop << "\n"; if (startPos != unit->m_startPos) { unit->m_startPos = startPos; unit->m_firstFrame = true; + // std::cout << "Start pos changed\n"; } // Now that we've run setup, we're ready to read STFT data and perform phase vocoder stretching. // First we need to figure out where we are, and if that means we need to loop or quit. - float newPos = unit->m_pos + 1/rate; + float newPos = unit->m_pos + rate; if (newPos > stftFrames - 1) { if (loop) { unit->m_firstFrame = true; @@ -193,11 +196,13 @@ void PV_PlayBufStretch_next(PV_PlayBufStretch *unit, int inNumSamples) { // For frames other than the first frame, we'll need to perform phase computation. else { bool phaseLock = false; - if (IN0(5) != 0.f) { + if (IN0(4) != 0.f) { phaseLock = true; + // std::cout << "Phase lock\n"; } SCPolarBuf *p = ToPolarApx(buf); size_t ipos = static_cast(std::round(newPos)); + std::cout << "New pos: " << ipos << "\n"; if (std::abs(ipos-newPos) < 1e-3) { // If we're right smack on a specific FFT frame, we don't // need to do any magnitude or frequency interpolation, so From 1a1f593a873e8fe594a14658238013fd2d437af5 Mon Sep 17 00:00:00 2001 From: Jeff Martin Date: Thu, 18 Jun 2026 14:48:45 -0500 Subject: [PATCH 10/25] fixed pos bug in stretch --- src/pv/pvStretch.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/pv/pvStretch.cpp b/src/pv/pvStretch.cpp index c1203d0..975a76a 100644 --- a/src/pv/pvStretch.cpp +++ b/src/pv/pvStretch.cpp @@ -109,7 +109,7 @@ void PV_PlayBufStretch_next(PV_PlayBufStretch *unit, int inNumSamples) { size_t stftBufFftSize = bufData[0]; size_t stftBufHopSize = static_cast(bufData[1] * stftBufFftSize); // in frames, not fraction int stftBufWinType = static_cast(bufData[2]); // -1, 0, or 1. This information is probably extraneous. - // std::cout << "FFT size: " << stftBufFftSize << " Hop size: " << stftBufHopSize << " Win type: " << stftBufWinType << " STFT frames " << stftFrames << "\n"; + //std::cout << "FFT size: " << stftBufFftSize << " Hop size: " << stftBufHopSize << " Win type: " << stftBufWinType << " STFT frames " << stftFrames << "\n"; if (stftBufFftSize != buf->samples) { OUT0(0) = -1.f; @@ -201,15 +201,15 @@ void PV_PlayBufStretch_next(PV_PlayBufStretch *unit, int inNumSamples) { // std::cout << "Phase lock\n"; } SCPolarBuf *p = ToPolarApx(buf); - size_t ipos = static_cast(std::round(newPos)); - std::cout << "New pos: " << ipos << "\n"; - if (std::abs(ipos-newPos) < 1e-3) { + size_t intPos = static_cast(std::round(newPos)); + //std::cout << "New pos: " << intPos << "\n"; + if (std::abs(intPos-newPos) < 1e-3) { // If we're right smack on a specific FFT frame, we don't // need to do any magnitude or frequency interpolation, so // we only need the current and previous FFT frames from the buffer. - size_t lpos = ipos - 1; - fillPolarBuf(stftData + (ipos * stftBufFftSize), unit->m_frameNext, stftBufFftSize); - fillPolarBuf(stftData + (ipos * stftBufFftSize), unit->m_framePrev1, stftBufFftSize); + size_t lastPos = intPos - 1; + fillPolarBuf(stftData + (intPos * stftBufFftSize), unit->m_frameNext, stftBufFftSize); + fillPolarBuf(stftData + (lastPos * stftBufFftSize), unit->m_framePrev1, stftBufFftSize); // Render the output FFT frame Stretch2( unit->m_frameNext, @@ -237,7 +237,7 @@ void PV_PlayBufStretch_next(PV_PlayBufStretch *unit, int inNumSamples) { unit->m_framePrev2, p, unit->m_outFramePrev, - newPos, + newPos-static_cast(lo), stftBufFftSize, stftBufHopSize, phaseLock From b84326e3dc5fcec96600cc63600b27bd58421daf Mon Sep 17 00:00:00 2001 From: Jeff Martin Date: Sat, 20 Jun 2026 10:18:26 -0500 Subject: [PATCH 11/25] incorporated peak finder; added more const guards --- src/pv/CMakeLists.txt | 3 + src/pv/peakFinder.cpp | 140 ++++++++++++++++++++++++++++++++++++++++++ src/pv/peakFinder.hpp | 80 ++++++++++++++++++++++++ src/pv/pvStretch.cpp | 76 +++++++++++------------ 4 files changed, 261 insertions(+), 38 deletions(-) create mode 100644 src/pv/peakFinder.cpp create mode 100644 src/pv/peakFinder.hpp diff --git a/src/pv/CMakeLists.txt b/src/pv/CMakeLists.txt index 74de74a..fd3fbfb 100644 --- a/src/pv/CMakeLists.txt +++ b/src/pv/CMakeLists.txt @@ -2,10 +2,13 @@ add_library(pv MODULE pv.cpp) add_library(pvCFreeze STATIC pvCFreeze.cpp) add_library(pvOther STATIC pvOther.cpp) +add_library(peakFinder STATIC peakFinder.cpp) add_library(pvStretch STATIC pvStretch.cpp) set_target_properties(pvCFreeze PROPERTIES POSITION_INDEPENDENT_CODE ON) set_target_properties(pvOther PROPERTIES POSITION_INDEPENDENT_CODE ON) +set_target_properties(peakFinder PROPERTIES POSITION_INDEPENDENT_CODE ON) set_target_properties(pvStretch PROPERTIES POSITION_INDEPENDENT_CODE ON) +target_link_libraries(pvStretch PRIVATE peakFinder) target_link_libraries(pv PRIVATE pvCFreeze pvOther pvStretch) if(SUPERNOVA) diff --git a/src/pv/peakFinder.cpp b/src/pv/peakFinder.cpp new file mode 100644 index 0000000..4bc36b6 --- /dev/null +++ b/src/pv/peakFinder.cpp @@ -0,0 +1,140 @@ +/* +File: peakFinder.cpp +Author: Jeff Martin + +Description: +A peak finder for the Laroche/Dolson phase locking algorithm. + +Copyright © 2026 by Jeffrey Martin. All rights reserved. +Website: https://www.jeffreymartincomposer.com + +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 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +#include "peakFinder.hpp" + +Peak::Peak(size_t peak) : peak(peak) {} +Peak::Peak(size_t peak, size_t leftValley, size_t rightValley) : peak(peak), leftValley(leftValley), rightValley(rightValley) {} + +#define SHIFT(arr, idx, val) \ + for (size_t xxi = ) + +PeakFinder::PeakFinder(size_t fftSize, size_t radius) { + m_maxSize = fftSize/2-1; + m_radius = radius; + m_size = 0; + peaks = nullptr; + m_queueL = nullptr; + m_queueR = nullptr; +} + +void PeakFinder::memLoad(void* arr) { + size_t *data = (size_t*)arr; + m_queueL = data; + m_queueR = data + m_radius; + peaks = (Peak*)(data + 2 * m_radius); +} + +size_t PeakFinder::memSize() const { + return m_radius * 2 * sizeof(size_t) + m_maxSize * sizeof(Peak); +} + +void* PeakFinder::memRetrieve() { + return (void*)m_queueL; +} + +void PeakFinder::clear() { + m_size = 0; +} + +size_t PeakFinder::maxSize() const { + return m_maxSize; +} + +size_t PeakFinder::size() const { + return m_size; +} + +void PeakFinder::analyze(const SCPolarBuf *buf) { + // We can only perform the analysis if we have enough bins + if (m_queueL && m_maxSize > m_radius * 2 + 1) { + m_size = 0; // clear any existing data + + size_t xxi = m_radius; + while (xxi < m_maxSize - m_radius) { + bool isMax = true; + for (size_t xxj = xxi - m_radius; xxj < xxi; xxj++) { + if (buf->bin[xxj].mag >= buf->bin[xxi].mag) { + isMax = false; + break; + } + } + for (size_t xxj = xxi + 1; xxj <= xxi + m_radius; xxj++) { + if (buf->bin[xxj].mag >= buf->bin[xxi].mag) { + isMax = false; + break; + } + } + if (isMax) { + peaks[m_size] = Peak(xxi); + m_size++; + xxi += m_radius + 1; + } else { + xxi++; + } + } + + // Find the left valley for the first peak, and the right valley + // for the last peak. + if (m_size > 0) { + float min = buf->bin[0].mag; + size_t argmin = 0; + size_t xxk = 1; + for (; xxk < peaks[0].peak; xxk++) { + if (buf->bin[xxk].mag < min) { + min = buf->bin[xxk].mag; + argmin = xxk; + } + } + peaks[0].leftValley = argmin; + xxk = peaks[m_size-1].peak + 1; + min = buf->bin[xxk].mag; + argmin = xxk; + for (xxk++; xxk < m_maxSize; xxk++) { + if (buf->bin[xxk].mag < min) { + min = buf->bin[xxk].mag; + argmin = xxk; + } + } + peaks[m_size-1].rightValley = argmin; + } + + // Find the remaining left and right valleys + if (m_size > 1) { + for (size_t xxj = 0; xxj < m_size-1; xxj++) { + size_t xxk = peaks[xxj].peak + 1; + float min = buf->bin[xxk].mag; + size_t argmin = xxk; + for (xxk++; xxk < peaks[xxj+1].peak; xxk++) { + if (buf->bin[xxk].mag < min) { + min = buf->bin[xxk].mag; + argmin = xxk; + } + } + peaks[xxj].rightValley = argmin - 1; + peaks[xxj+1].leftValley = argmin; + } + } + } +} diff --git a/src/pv/peakFinder.hpp b/src/pv/peakFinder.hpp new file mode 100644 index 0000000..d6a0e7a --- /dev/null +++ b/src/pv/peakFinder.hpp @@ -0,0 +1,80 @@ +/* +File: peakFinder.hpp +Author: Jeff Martin + +Description: +A peak finder for the Laroche/Dolson phase locking algorithm. + +Copyright © 2026 by Jeffrey Martin. All rights reserved. +Website: https://www.jeffreymartincomposer.com + +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 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +#pragma once +#include "FFT_UGens.h" + +class Peak { +public: + Peak(size_t peak); + Peak(size_t peak, size_t leftValley, size_t rightValley); + size_t peak, leftValley, rightValley; +}; + +class PeakFinder { +public: + /// Constructs the PeakFinder + /// + /// \param fftSize The FFT size + PeakFinder(size_t fftSize, size_t radius); + + /// Finds peaks in the provided SCPolarBuf. Note that this buffer must correspond + /// to the original FFT size provided for the PeakFinder--otherwise memory errors may occur. + /// + /// \param buf The buffer to analyze + void analyze(const SCPolarBuf *buf); + + /// Loads memory from the external SuperCollider allocator. Memory is allocated + /// using RTAlloc() and the size is specified by the memSize() method. + /// + /// \param arr The allocated memory + void memLoad(void *arr); + + /// Gets the memory size required for the PeakFinder + size_t memSize() const; + + /// Gets the max size of the PeakFinder + /// + /// \returns The max size + size_t maxSize() const; + + /// Gets the current size of the PeakFinder (the number of peaks stored) + /// + /// \returns The current size + size_t size() const; + + /// Clears the PeakFinder + void clear(); + + /// Gets a pointer to the memory that was allocated for the PeakFinder + /// so that it can be deallocated + /// + /// \return The memory pointer + void* memRetrieve(); + + Peak *peaks; +private: + size_t m_maxSize, m_size, m_radius; + size_t *m_queueL, *m_queueR; +}; diff --git a/src/pv/pvStretch.cpp b/src/pv/pvStretch.cpp index 975a76a..7b3b32c 100644 --- a/src/pv/pvStretch.cpp +++ b/src/pv/pvStretch.cpp @@ -76,17 +76,17 @@ void PV_PlayBufStretch_next(PV_PlayBufStretch *unit, int inNumSamples) { const SndBuf *stftBuf = unit->m_buf; if (!stftBuf) { OUT0(0) = -1.f; - std::cout << "WARNING: The stftBuf could not be accessed. Aborting.\n"; + std::cout << "WARNING: The stftBuffer could not be accessed. Aborting.\n"; return; } ACQUIRE_SNDBUF_SHARED(stftBuf); const float* bufData __attribute__((__unused__)) = stftBuf->data; const float* stftData __attribute__((__unused__)) = stftBuf->data + 3; - uint32 bufChannels __attribute__((__unused__)) = stftBuf->channels; - uint32 bufSamples __attribute__((__unused__)) = stftBuf->samples; - uint32 bufFrames = stftBuf->frames; + const uint32 bufChannels __attribute__((__unused__)) = stftBuf->channels; + const uint32 bufSamples __attribute__((__unused__)) = stftBuf->samples; + const uint32 bufFrames = stftBuf->frames; // first 3 frames have analysis parameters - int stftFrames = (static_cast(stftBuf->samples) - 3) / static_cast(buf->samples); + const int stftFrames = (static_cast(stftBuf->samples) - 3) / static_cast(buf->samples); // If the buffer is improperly configured, we cannot use it. if (bufChannels != 1) { @@ -106,9 +106,9 @@ void PV_PlayBufStretch_next(PV_PlayBufStretch *unit, int inNumSamples) { } // Basic information - size_t stftBufFftSize = bufData[0]; - size_t stftBufHopSize = static_cast(bufData[1] * stftBufFftSize); // in frames, not fraction - int stftBufWinType = static_cast(bufData[2]); // -1, 0, or 1. This information is probably extraneous. + const size_t stftBufFftSize = static_cast(bufData[0]); + const size_t stftBufHopSize = static_cast(bufData[1] * stftBufFftSize); // in frames, not fraction + const int stftBufWinType = static_cast(bufData[2]); // -1, 0, or 1. This information is probably extraneous. //std::cout << "FFT size: " << stftBufFftSize << " Hop size: " << stftBufHopSize << " Win type: " << stftBufWinType << " STFT frames " << stftFrames << "\n"; if (stftBufFftSize != buf->samples) { @@ -135,8 +135,8 @@ void PV_PlayBufStretch_next(PV_PlayBufStretch *unit, int inNumSamples) { } float startPos = sc_clip(IN0(2), 0.0, 1.0); - float rate = IN0(3); - float loop = IN0(5); + const float rate = IN0(3); + const float loop = IN0(5); // std::cout << "Rate: " << rate << " Loop: " << loop << "\n"; if (startPos != unit->m_startPos) { @@ -148,7 +148,7 @@ void PV_PlayBufStretch_next(PV_PlayBufStretch *unit, int inNumSamples) { // Now that we've run setup, we're ready to read STFT data and perform phase vocoder stretching. // First we need to figure out where we are, and if that means we need to loop or quit. - float newPos = unit->m_pos + rate; + const float newPos = unit->m_pos + rate; if (newPos > stftFrames - 1) { if (loop) { unit->m_firstFrame = true; @@ -165,10 +165,10 @@ void PV_PlayBufStretch_next(PV_PlayBufStretch *unit, int inNumSamples) { // This is essential to make sure that subsequent phase calculations are correctly aligned. if (unit->m_firstFrame) { // Compute the index of the first frame - size_t xxi = static_cast(std::round(startPos * stftFrames)); - if (xxi >= stftFrames) { + size_t firstFrameIdx = static_cast(std::round(startPos * stftFrames)); + if (firstFrameIdx >= stftFrames) { if (loop) { - xxi = 0; + firstFrameIdx = 0; } else { OUT0(0) = -1.f; RELEASE_SNDBUF_SHARED(stftBuf); @@ -179,7 +179,7 @@ void PV_PlayBufStretch_next(PV_PlayBufStretch *unit, int inNumSamples) { // Copy the FFT data over SCPolarBuf *p = ToPolarApx(buf); - const float *currentFftFrame = stftData + (xxi * stftBufFftSize); + const float *currentFftFrame = stftData + (firstFrameIdx * stftBufFftSize); // Fill the output buffer fillPolarBuf(currentFftFrame, p, stftBufFftSize); @@ -189,7 +189,7 @@ void PV_PlayBufStretch_next(PV_PlayBufStretch *unit, int inNumSamples) { fillPolarBuf(currentFftFrame, unit->m_outFramePrev, stftBufFftSize); // We have to advance by one frame because we need to be able to compute frequency for time stretching. - unit->m_pos = static_cast(xxi + 1); + unit->m_pos = static_cast(firstFrameIdx + 1); unit->m_firstFrame = false; } @@ -201,14 +201,14 @@ void PV_PlayBufStretch_next(PV_PlayBufStretch *unit, int inNumSamples) { // std::cout << "Phase lock\n"; } SCPolarBuf *p = ToPolarApx(buf); - size_t intPos = static_cast(std::round(newPos)); + size_t roundedPos = static_cast(std::round(newPos)); //std::cout << "New pos: " << intPos << "\n"; - if (std::abs(intPos-newPos) < 1e-3) { + if (std::abs(roundedPos-newPos) < 1e-3) { // If we're right smack on a specific FFT frame, we don't // need to do any magnitude or frequency interpolation, so // we only need the current and previous FFT frames from the buffer. - size_t lastPos = intPos - 1; - fillPolarBuf(stftData + (intPos * stftBufFftSize), unit->m_frameNext, stftBufFftSize); + size_t lastPos = roundedPos - 1; + fillPolarBuf(stftData + (roundedPos * stftBufFftSize), unit->m_frameNext, stftBufFftSize); fillPolarBuf(stftData + (lastPos * stftBufFftSize), unit->m_framePrev1, stftBufFftSize); // Render the output FFT frame Stretch2( @@ -290,19 +290,19 @@ void Stretch2( // in order to "lock" phases of adjacent bins together. float prevPhase = 0.0; if (xxk == 0) { - std::complex prevBinKMinus1 = std::polar(outFramePrev->dc, 0.f); - std::complex prevBinK = std::polar(outFramePrev->bin[xxk].mag, outFramePrev->bin[xxk].phase); - std::complex prevBinKPlus1 = std::polar(outFramePrev->bin[xxk+1].mag, outFramePrev->bin[xxk+1].phase); + std::complex prevBinKMinus1 = std::polar(outFramePrev->dc, 0.f); + std::complex prevBinK = std::polar(outFramePrev->bin[xxk].mag, outFramePrev->bin[xxk].phase); + std::complex prevBinKPlus1 = std::polar(outFramePrev->bin[xxk+1].mag, outFramePrev->bin[xxk+1].phase); prevPhase = std::arg(prevBinK - prevBinKMinus1 - prevBinKPlus1); } else if (xxk == fftSize/2-2) { - std::complex prevBinKMinus1 = std::polar(outFramePrev->bin[xxk-1].mag, outFramePrev->bin[xxk-1].phase); - std::complex prevBinK = std::polar(outFramePrev->bin[xxk].mag, outFramePrev->bin[xxk].phase); - std::complex prevBinKPlus1 = std::polar(outFramePrev->nyq, 0.f); + std::complex prevBinKMinus1 = std::polar(outFramePrev->bin[xxk-1].mag, outFramePrev->bin[xxk-1].phase); + std::complex prevBinK = std::polar(outFramePrev->bin[xxk].mag, outFramePrev->bin[xxk].phase); + std::complex prevBinKPlus1 = std::polar(outFramePrev->nyq, 0.f); prevPhase = std::arg(prevBinK - prevBinKMinus1 - prevBinKPlus1); } else { - std::complex prevBinKMinus1 = std::polar(outFramePrev->bin[xxk-1].mag, outFramePrev->bin[xxk-1].phase); - std::complex prevBinK = std::polar(outFramePrev->bin[xxk].mag, outFramePrev->bin[xxk].phase); - std::complex prevBinKPlus1 = std::polar(outFramePrev->bin[xxk+1].mag, outFramePrev->bin[xxk+1].phase); + std::complex prevBinKMinus1 = std::polar(outFramePrev->bin[xxk-1].mag, outFramePrev->bin[xxk-1].phase); + std::complex prevBinK = std::polar(outFramePrev->bin[xxk].mag, outFramePrev->bin[xxk].phase); + std::complex prevBinKPlus1 = std::polar(outFramePrev->bin[xxk+1].mag, outFramePrev->bin[xxk+1].phase); prevPhase = std::arg(prevBinK - prevBinKMinus1 - prevBinKPlus1); } @@ -371,19 +371,19 @@ void Stretch3( // in order to "lock" phases of adjacent bins together. float prevPhase = 0.0; if (xxk == 0) { - std::complex prevBinKMinus1 = std::polar(outFramePrev->dc, 0.f); - std::complex prevBinK = std::polar(outFramePrev->bin[xxk].mag, outFramePrev->bin[xxk].phase); - std::complex prevBinKPlus1 = std::polar(outFramePrev->bin[xxk+1].mag, outFramePrev->bin[xxk+1].phase); + std::complex prevBinKMinus1 = std::polar(outFramePrev->dc, 0.f); + std::complex prevBinK = std::polar(outFramePrev->bin[xxk].mag, outFramePrev->bin[xxk].phase); + std::complex prevBinKPlus1 = std::polar(outFramePrev->bin[xxk+1].mag, outFramePrev->bin[xxk+1].phase); prevPhase = std::arg(prevBinK - prevBinKMinus1 - prevBinKPlus1); } else if (xxk == fftSize/2-2) { - std::complex prevBinKMinus1 = std::polar(outFramePrev->bin[xxk-1].mag, outFramePrev->bin[xxk-1].phase); - std::complex prevBinK = std::polar(outFramePrev->bin[xxk].mag, outFramePrev->bin[xxk].phase); - std::complex prevBinKPlus1 = std::polar(outFramePrev->nyq, 0.f); + std::complex prevBinKMinus1 = std::polar(outFramePrev->bin[xxk-1].mag, outFramePrev->bin[xxk-1].phase); + std::complex prevBinK = std::polar(outFramePrev->bin[xxk].mag, outFramePrev->bin[xxk].phase); + std::complex prevBinKPlus1 = std::polar(outFramePrev->nyq, 0.f); prevPhase = std::arg(prevBinK - prevBinKMinus1 - prevBinKPlus1); } else { - std::complex prevBinKMinus1 = std::polar(outFramePrev->bin[xxk-1].mag, outFramePrev->bin[xxk-1].phase); - std::complex prevBinK = std::polar(outFramePrev->bin[xxk].mag, outFramePrev->bin[xxk].phase); - std::complex prevBinKPlus1 = std::polar(outFramePrev->bin[xxk+1].mag, outFramePrev->bin[xxk+1].phase); + std::complex prevBinKMinus1 = std::polar(outFramePrev->bin[xxk-1].mag, outFramePrev->bin[xxk-1].phase); + std::complex prevBinK = std::polar(outFramePrev->bin[xxk].mag, outFramePrev->bin[xxk].phase); + std::complex prevBinKPlus1 = std::polar(outFramePrev->bin[xxk+1].mag, outFramePrev->bin[xxk+1].phase); prevPhase = std::arg(prevBinK - prevBinKMinus1 - prevBinKPlus1); } From 22835b79ea8ce627dc6f181843f537690078d699 Mon Sep 17 00:00:00 2001 From: Jeff Martin Date: Sat, 20 Jun 2026 10:50:40 -0500 Subject: [PATCH 12/25] added in peak finder --- src/pv/pvStretch.cpp | 22 +++++++++++++++++----- src/pv/pvStretch.hpp | 4 ++++ 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/pv/pvStretch.cpp b/src/pv/pvStretch.cpp index 7b3b32c..6b8d44b 100644 --- a/src/pv/pvStretch.cpp +++ b/src/pv/pvStretch.cpp @@ -31,16 +31,21 @@ along with this program. If not, see . extern InterfaceTable *ft; void PV_PlayBufStretch_Ctor(PV_PlayBufStretch *unit) { + PV_GET_BUF + // Connect to the STFT buffer. For now, we only allow this in the constructor. - float fbufnum = IN0(1); - uint32 bufnum = static_cast(fbufnum); - if (bufnum >= unit->mWorld->mNumSndBufs) bufnum = 0; - unit->m_fbufnum = fbufnum; - unit->m_buf = unit->mWorld->mSndBufs + bufnum; + float fstftbufnum = IN0(1); + uint32 stftbufnum = static_cast(fstftbufnum); + if (stftbufnum >= unit->mWorld->mNumSndBufs) stftbufnum = 0; + unit->m_fbufnum = fstftbufnum; + unit->m_buf = unit->mWorld->mSndBufs + stftbufnum; unit->m_outFramePrev = nullptr; unit->m_frameNext = nullptr; unit->m_framePrev1 = nullptr; unit->m_framePrev2 = nullptr; + unit->m_peakFinder = (PeakFinder*)RTAlloc(unit->mWorld, sizeof(PeakFinder)); + new (unit->m_peakFinder) PeakFinder(static_cast(buf->samples), 2); + unit->m_peakFinder->memLoad(RTAlloc(unit->mWorld, unit->m_peakFinder->memSize())); // Configure position float startPos = sc_clip(IN0(2), 0.0, 1.0); @@ -53,6 +58,13 @@ void PV_PlayBufStretch_Ctor(PV_PlayBufStretch *unit) { } void PV_PlayBufStretch_Dtor(PV_PlayBufStretch *unit) { + if (unit->m_peakFinder) { + void *reservedMem = unit->m_peakFinder->memRetrieve(); + if (reservedMem) { + RTFree(unit->mWorld, reservedMem); + } + RTFree(unit->mWorld, unit->m_peakFinder); + } if (unit->m_outFramePrev) { RTFree(unit->mWorld, unit->m_outFramePrev); } diff --git a/src/pv/pvStretch.hpp b/src/pv/pvStretch.hpp index 0c5b232..dbcada4 100644 --- a/src/pv/pvStretch.hpp +++ b/src/pv/pvStretch.hpp @@ -24,6 +24,7 @@ along with this program. If not, see . #pragma once #include "SC_Unit.h" #include "FFT_UGens.h" +#include "peakFinder.hpp" /// Stores the state of a PV_PlayBufStretch UGen instance struct PV_PlayBufStretch : public Unit { @@ -45,6 +46,9 @@ struct PV_PlayBufStretch : public Unit { /// The STFT frame two positions before the current position SCPolarBuf *m_framePrev2; + /// The peak finding utility for the Laroche/Dolson stretching algorithm + PeakFinder *m_peakFinder; + /// Between 0 and 1; represents the start position of playback. /// If it jumps during playback, playback will be restarted /// at the new m_startPos. From 47b69941fc7de0239b8a4ea013b95d4d151bcd1c Mon Sep 17 00:00:00 2001 From: Jeff Martin Date: Sat, 20 Jun 2026 11:22:23 -0500 Subject: [PATCH 13/25] separated phase locking into separate function calls --- src/pv/pvStretch.cpp | 317 ++++++++++++++++++++++++++----------------- src/pv/pvStretch.hpp | 54 +++++++- 2 files changed, 240 insertions(+), 131 deletions(-) diff --git a/src/pv/pvStretch.cpp b/src/pv/pvStretch.cpp index 6b8d44b..8f55a96 100644 --- a/src/pv/pvStretch.cpp +++ b/src/pv/pvStretch.cpp @@ -207,11 +207,7 @@ void PV_PlayBufStretch_next(PV_PlayBufStretch *unit, int inNumSamples) { // For frames other than the first frame, we'll need to perform phase computation. else { - bool phaseLock = false; - if (IN0(4) != 0.f) { - phaseLock = true; - // std::cout << "Phase lock\n"; - } + size_t phaseLock = sc_clip(static_cast(IN0(4)), 0, 2); SCPolarBuf *p = ToPolarApx(buf); size_t roundedPos = static_cast(std::round(newPos)); //std::cout << "New pos: " << intPos << "\n"; @@ -223,15 +219,27 @@ void PV_PlayBufStretch_next(PV_PlayBufStretch *unit, int inNumSamples) { fillPolarBuf(stftData + (roundedPos * stftBufFftSize), unit->m_frameNext, stftBufFftSize); fillPolarBuf(stftData + (lastPos * stftBufFftSize), unit->m_framePrev1, stftBufFftSize); // Render the output FFT frame - Stretch2( - unit->m_frameNext, - unit->m_framePrev1, - p, - unit->m_outFramePrev, - stftBufFftSize, - stftBufHopSize, - phaseLock - ); + switch (phaseLock) { + case 1: + Stretch2Puckette( + unit->m_frameNext, + unit->m_framePrev1, + p, + unit->m_outFramePrev, + stftBufFftSize, + stftBufHopSize + ); + break; + default: + Stretch2( + unit->m_frameNext, + unit->m_framePrev1, + p, + unit->m_outFramePrev, + stftBufFftSize, + stftBufHopSize + ); + } } else { // Otherwise we're between two FFT frames, and we're going to have to // interpolate magnitude and frequency data. @@ -243,17 +251,31 @@ void PV_PlayBufStretch_next(PV_PlayBufStretch *unit, int inNumSamples) { // This is the frame right before that. It's needed to compute the previous instantaneous frequencies. fillPolarBuf(stftData + ((lo-1) * stftBufFftSize), unit->m_framePrev2, stftBufFftSize); // Render the output FFT frame - Stretch3( - unit->m_frameNext, - unit->m_framePrev1, - unit->m_framePrev2, - p, - unit->m_outFramePrev, - newPos-static_cast(lo), - stftBufFftSize, - stftBufHopSize, - phaseLock - ); + switch (phaseLock) { + case 1: + Stretch3Puckette( + unit->m_frameNext, + unit->m_framePrev1, + unit->m_framePrev2, + p, + unit->m_outFramePrev, + newPos-static_cast(lo), + stftBufFftSize, + stftBufHopSize + ); + break; + default: + Stretch3( + unit->m_frameNext, + unit->m_framePrev1, + unit->m_framePrev2, + p, + unit->m_outFramePrev, + newPos-static_cast(lo), + stftBufFftSize, + stftBufHopSize + ); + } } // We always need to store the resultant FFT frame in the UGen. // It is used in the next call to PV_PlayBufStretch_next, for @@ -276,59 +298,78 @@ void PV_PlayBufStretch_next(PV_PlayBufStretch *unit, int inNumSamples) { /// \param outFramePrev The previously computed output STFT frame /// \param fftSize The FFT size /// \param hopSize The hop size -/// \param phaseLock Whether or not to apply phase locking void Stretch2( const SCPolarBuf *frame, const SCPolarBuf *framePrev, SCPolarBuf *outFrame, const SCPolarBuf *outFramePrev, size_t fftSize, - size_t hopSize, - bool phaseLock) { + size_t hopSize) { outFrame->dc = frame->dc; outFrame->nyq = frame->nyq; for (size_t xxk = 0; xxk < fftSize/2-1; xxk++) { outFrame->bin[xxk].mag = frame->bin[xxk].mag; - // Puckette-style phase locking - if (phaseLock) { - // Compute the instantaneous frequency - float omegaK = twopi * (xxk+1) / fftSize; - float phaseInc = frame->bin[xxk].phase - framePrev->bin[xxk].phase - hopSize * omegaK; - phaseInc = std::fmod(phaseInc + pi, twopi) - pi; - float instantaneousFreq = omegaK + phaseInc/hopSize; - - // In Puckette-style phase locking, we make a substitution for the previous phase, - // in order to "lock" phases of adjacent bins together. - float prevPhase = 0.0; - if (xxk == 0) { - std::complex prevBinKMinus1 = std::polar(outFramePrev->dc, 0.f); - std::complex prevBinK = std::polar(outFramePrev->bin[xxk].mag, outFramePrev->bin[xxk].phase); - std::complex prevBinKPlus1 = std::polar(outFramePrev->bin[xxk+1].mag, outFramePrev->bin[xxk+1].phase); - prevPhase = std::arg(prevBinK - prevBinKMinus1 - prevBinKPlus1); - } else if (xxk == fftSize/2-2) { - std::complex prevBinKMinus1 = std::polar(outFramePrev->bin[xxk-1].mag, outFramePrev->bin[xxk-1].phase); - std::complex prevBinK = std::polar(outFramePrev->bin[xxk].mag, outFramePrev->bin[xxk].phase); - std::complex prevBinKPlus1 = std::polar(outFramePrev->nyq, 0.f); - prevPhase = std::arg(prevBinK - prevBinKMinus1 - prevBinKPlus1); - } else { - std::complex prevBinKMinus1 = std::polar(outFramePrev->bin[xxk-1].mag, outFramePrev->bin[xxk-1].phase); - std::complex prevBinK = std::polar(outFramePrev->bin[xxk].mag, outFramePrev->bin[xxk].phase); - std::complex prevBinKPlus1 = std::polar(outFramePrev->bin[xxk+1].mag, outFramePrev->bin[xxk+1].phase); - prevPhase = std::arg(prevBinK - prevBinKMinus1 - prevBinKPlus1); - } - - // Compute the new phase - outFrame->bin[xxk].phase = prevPhase + hopSize * instantaneousFreq; + // Compute the instantaneous frequency + float omegaK = twopi * (xxk+1) / fftSize; + float phaseInc = frame->bin[xxk].phase - framePrev->bin[xxk].phase - hopSize * omegaK; + phaseInc = std::fmod(phaseInc + pi, twopi) - pi; + float instantaneousFreq = omegaK + phaseInc/hopSize; + + // Compute the new phase + outFrame->bin[xxk].phase = outFramePrev->bin[xxk].phase + hopSize * instantaneousFreq; + } +} + +/// Computes a single frame of STFT data for time stretching. +/// The assumption is that we are positioned exactly at `frame`, and we therefore +/// just need framePrev to compute the instantaneous frequency. We also do not +/// need to perform any magnitude or frequency interpolation. +/// +/// This version uses Miller Puckette's phase locking. +/// +/// \param frame The current STFT frame +/// \param framePrev The previous STFT frame +/// \param [out] outFrame The output STFT frame +/// \param outFramePrev The previously computed output STFT frame +/// \param fftSize The FFT size +/// \param hopSize The hop size +void Stretch2Puckette( + const SCPolarBuf *frame, + const SCPolarBuf *framePrev, + SCPolarBuf *outFrame, + const SCPolarBuf *outFramePrev, + size_t fftSize, + size_t hopSize) { + outFrame->dc = frame->dc; + outFrame->nyq = frame->nyq; + for (size_t xxk = 0; xxk < fftSize/2-1; xxk++) { + outFrame->bin[xxk].mag = frame->bin[xxk].mag; + + // Compute the instantaneous frequency + float omegaK = twopi * (xxk+1) / fftSize; + float phaseInc = frame->bin[xxk].phase - framePrev->bin[xxk].phase - hopSize * omegaK; + phaseInc = std::fmod(phaseInc + pi, twopi) - pi; + float instantaneousFreq = omegaK + phaseInc/hopSize; + + // In Puckette-style phase locking, we make a substitution for the previous phase, + // in order to "lock" phases of adjacent bins together. + float prevPhase = 0.0; + if (xxk == 0) { + std::complex prevBinKMinus1 = std::polar(outFramePrev->dc, 0.f); + std::complex prevBinK = std::polar(outFramePrev->bin[xxk].mag, outFramePrev->bin[xxk].phase); + std::complex prevBinKPlus1 = std::polar(outFramePrev->bin[xxk+1].mag, outFramePrev->bin[xxk+1].phase); + prevPhase = std::arg(prevBinK - prevBinKMinus1 - prevBinKPlus1); + } else if (xxk == fftSize/2-2) { + std::complex prevBinKMinus1 = std::polar(outFramePrev->bin[xxk-1].mag, outFramePrev->bin[xxk-1].phase); + std::complex prevBinK = std::polar(outFramePrev->bin[xxk].mag, outFramePrev->bin[xxk].phase); + std::complex prevBinKPlus1 = std::polar(outFramePrev->nyq, 0.f); + prevPhase = std::arg(prevBinK - prevBinKMinus1 - prevBinKPlus1); } else { - // Compute the instantaneous frequency - float omegaK = twopi * (xxk+1) / fftSize; - float phaseInc = frame->bin[xxk].phase - framePrev->bin[xxk].phase - hopSize * omegaK; - phaseInc = std::fmod(phaseInc + pi, twopi) - pi; - float instantaneousFreq = omegaK + phaseInc/hopSize; - - // Compute the new phase - outFrame->bin[xxk].phase = outFramePrev->bin[xxk].phase + hopSize * instantaneousFreq; + std::complex prevBinKMinus1 = std::polar(outFramePrev->bin[xxk-1].mag, outFramePrev->bin[xxk-1].phase); + std::complex prevBinK = std::polar(outFramePrev->bin[xxk].mag, outFramePrev->bin[xxk].phase); + std::complex prevBinKPlus1 = std::polar(outFramePrev->bin[xxk+1].mag, outFramePrev->bin[xxk+1].phase); + prevPhase = std::arg(prevBinK - prevBinKMinus1 - prevBinKPlus1); } } } @@ -346,7 +387,6 @@ void Stretch2( /// \param pos The position between framePrev1 and frameNext (0 < pos < 1) /// \param fftSize The FFT size /// \param hopSize The hop size -/// \param phaseLock Whether or not to apply phase locking void Stretch3( const SCPolarBuf *frameNext, const SCPolarBuf *framePrev1, @@ -355,71 +395,98 @@ void Stretch3( const SCPolarBuf *outFramePrev, float pos, size_t fftSize, - size_t hopSize, - bool phaseLock) { + size_t hopSize) { outFrame->dc = INTERP(framePrev1->dc, frameNext->dc, pos); outFrame->nyq = INTERP(framePrev1->nyq, frameNext->nyq, pos); for (size_t xxk = 0; xxk < fftSize/2-1; xxk++) { outFrame->bin[xxk].mag = INTERP(framePrev1->bin[xxk].mag, frameNext->bin[xxk].mag, pos); - // Puckette-style phase locking - if (phaseLock) { - float omegaK = twopi * (xxk+1) / fftSize; - - // Compute the next instantaneous frequency - float phaseInc = frameNext->bin[xxk].phase - framePrev1->bin[xxk].phase - hopSize * omegaK; - phaseInc = std::fmod(phaseInc + pi, twopi) - pi; - float instantaneousFreqNext = omegaK + phaseInc/hopSize; - - // Compute the previous instantaneous frequency - phaseInc = framePrev1->bin[xxk].phase - framePrev2->bin[xxk].phase - hopSize * omegaK; - phaseInc = std::fmod(phaseInc + pi, twopi) - pi; - float instantaneousFreqPrev = omegaK + phaseInc/hopSize; - - // Interpolate the instantaneous frequency - float instantaneousFreq = INTERP(instantaneousFreqPrev, instantaneousFreqNext, pos); - - // In Puckette-style phase locking, we make a substitution for the previous phase, - // in order to "lock" phases of adjacent bins together. - float prevPhase = 0.0; - if (xxk == 0) { - std::complex prevBinKMinus1 = std::polar(outFramePrev->dc, 0.f); - std::complex prevBinK = std::polar(outFramePrev->bin[xxk].mag, outFramePrev->bin[xxk].phase); - std::complex prevBinKPlus1 = std::polar(outFramePrev->bin[xxk+1].mag, outFramePrev->bin[xxk+1].phase); - prevPhase = std::arg(prevBinK - prevBinKMinus1 - prevBinKPlus1); - } else if (xxk == fftSize/2-2) { - std::complex prevBinKMinus1 = std::polar(outFramePrev->bin[xxk-1].mag, outFramePrev->bin[xxk-1].phase); - std::complex prevBinK = std::polar(outFramePrev->bin[xxk].mag, outFramePrev->bin[xxk].phase); - std::complex prevBinKPlus1 = std::polar(outFramePrev->nyq, 0.f); - prevPhase = std::arg(prevBinK - prevBinKMinus1 - prevBinKPlus1); - } else { - std::complex prevBinKMinus1 = std::polar(outFramePrev->bin[xxk-1].mag, outFramePrev->bin[xxk-1].phase); - std::complex prevBinK = std::polar(outFramePrev->bin[xxk].mag, outFramePrev->bin[xxk].phase); - std::complex prevBinKPlus1 = std::polar(outFramePrev->bin[xxk+1].mag, outFramePrev->bin[xxk+1].phase); - prevPhase = std::arg(prevBinK - prevBinKMinus1 - prevBinKPlus1); - } - - // Compute the new phase - outFrame->bin[xxk].phase = prevPhase + hopSize * instantaneousFreq; + float omegaK = twopi * (xxk+1) / fftSize; + + // Compute the next instantaneous frequency + float phaseInc = frameNext->bin[xxk].phase - framePrev1->bin[xxk].phase - hopSize * omegaK; + phaseInc = std::fmod(phaseInc + pi, twopi) - pi; + float instantaneousFreqNext = omegaK + phaseInc/hopSize; + + // Compute the previous instantaneous frequency + phaseInc = framePrev1->bin[xxk].phase - framePrev2->bin[xxk].phase - hopSize * omegaK; + phaseInc = std::fmod(phaseInc + pi, twopi) - pi; + float instantaneousFreqPrev = omegaK + phaseInc/hopSize; + + // Interpolate the instantaneous frequency + float instantaneousFreq = INTERP(instantaneousFreqPrev, instantaneousFreqNext, pos); + + // Compute the new phase + outFrame->bin[xxk].phase = outFramePrev->bin[xxk].phase + hopSize * instantaneousFreq; + } +} + +/// Computes a single frame of STFT data for time stretching. +/// The assumption is that we are positioned between framePrev1 and frameNext. +/// This means we will need to interpolate frequency data. So we will need to +/// compute two frequencies for each bin, and that means we need three STFT frames. +/// +/// This version uses Miller Puckette's phase locking. +/// +/// \param frameNext The next STFT frame +/// \param framePrev1 The previous STFT frame +/// \param framePrev2 The previous STFT frame before that (required for instantaneous frequency interpolation) +/// \param [out] outFrame The output STFT frame +/// \param outFramePrev The previously computed output STFT frame +/// \param pos The position between framePrev1 and frameNext (0 < pos < 1) +/// \param fftSize The FFT size +/// \param hopSize The hop size +void Stretch3Puckette( + const SCPolarBuf *frameNext, + const SCPolarBuf *framePrev1, + const SCPolarBuf *framePrev2, + SCPolarBuf *outFrame, + const SCPolarBuf *outFramePrev, + float pos, + size_t fftSize, + size_t hopSize) { + outFrame->dc = INTERP(framePrev1->dc, frameNext->dc, pos); + outFrame->nyq = INTERP(framePrev1->nyq, frameNext->nyq, pos); + for (size_t xxk = 0; xxk < fftSize/2-1; xxk++) { + outFrame->bin[xxk].mag = INTERP(framePrev1->bin[xxk].mag, frameNext->bin[xxk].mag, pos); + + float omegaK = twopi * (xxk+1) / fftSize; + + // Compute the next instantaneous frequency + float phaseInc = frameNext->bin[xxk].phase - framePrev1->bin[xxk].phase - hopSize * omegaK; + phaseInc = std::fmod(phaseInc + pi, twopi) - pi; + float instantaneousFreqNext = omegaK + phaseInc/hopSize; + + // Compute the previous instantaneous frequency + phaseInc = framePrev1->bin[xxk].phase - framePrev2->bin[xxk].phase - hopSize * omegaK; + phaseInc = std::fmod(phaseInc + pi, twopi) - pi; + float instantaneousFreqPrev = omegaK + phaseInc/hopSize; + + // Interpolate the instantaneous frequency + float instantaneousFreq = INTERP(instantaneousFreqPrev, instantaneousFreqNext, pos); + + // In Puckette-style phase locking, we make a substitution for the previous phase, + // in order to "lock" phases of adjacent bins together. + float prevPhase = 0.0; + if (xxk == 0) { + std::complex prevBinKMinus1 = std::polar(outFramePrev->dc, 0.f); + std::complex prevBinK = std::polar(outFramePrev->bin[xxk].mag, outFramePrev->bin[xxk].phase); + std::complex prevBinKPlus1 = std::polar(outFramePrev->bin[xxk+1].mag, outFramePrev->bin[xxk+1].phase); + prevPhase = std::arg(prevBinK - prevBinKMinus1 - prevBinKPlus1); + } else if (xxk == fftSize/2-2) { + std::complex prevBinKMinus1 = std::polar(outFramePrev->bin[xxk-1].mag, outFramePrev->bin[xxk-1].phase); + std::complex prevBinK = std::polar(outFramePrev->bin[xxk].mag, outFramePrev->bin[xxk].phase); + std::complex prevBinKPlus1 = std::polar(outFramePrev->nyq, 0.f); + prevPhase = std::arg(prevBinK - prevBinKMinus1 - prevBinKPlus1); } else { - float omegaK = twopi * (xxk+1) / fftSize; - - // Compute the next instantaneous frequency - float phaseInc = frameNext->bin[xxk].phase - framePrev1->bin[xxk].phase - hopSize * omegaK; - phaseInc = std::fmod(phaseInc + pi, twopi) - pi; - float instantaneousFreqNext = omegaK + phaseInc/hopSize; - - // Compute the previous instantaneous frequency - phaseInc = framePrev1->bin[xxk].phase - framePrev2->bin[xxk].phase - hopSize * omegaK; - phaseInc = std::fmod(phaseInc + pi, twopi) - pi; - float instantaneousFreqPrev = omegaK + phaseInc/hopSize; - - // Interpolate the instantaneous frequency - float instantaneousFreq = INTERP(instantaneousFreqPrev, instantaneousFreqNext, pos); - - // Compute the new phase - outFrame->bin[xxk].phase = outFramePrev->bin[xxk].phase + hopSize * instantaneousFreq; + std::complex prevBinKMinus1 = std::polar(outFramePrev->bin[xxk-1].mag, outFramePrev->bin[xxk-1].phase); + std::complex prevBinK = std::polar(outFramePrev->bin[xxk].mag, outFramePrev->bin[xxk].phase); + std::complex prevBinKPlus1 = std::polar(outFramePrev->bin[xxk+1].mag, outFramePrev->bin[xxk+1].phase); + prevPhase = std::arg(prevBinK - prevBinKMinus1 - prevBinKPlus1); } + + // Compute the new phase + outFrame->bin[xxk].phase = prevPhase + hopSize * instantaneousFreq; } } diff --git a/src/pv/pvStretch.hpp b/src/pv/pvStretch.hpp index dbcada4..d52f02c 100644 --- a/src/pv/pvStretch.hpp +++ b/src/pv/pvStretch.hpp @@ -84,15 +84,34 @@ void PV_PlayBufStretch_next(PV_PlayBufStretch *unit, int inNumSamples); /// \param outFramePrev The previously computed output STFT frame /// \param fftSize The FFT size /// \param hopSize The hop size -/// \param phaseLock Whether or not to apply phase locking void Stretch2( const SCPolarBuf *frame, const SCPolarBuf *framePrev, SCPolarBuf *outFrame, const SCPolarBuf *outFramePrev, size_t fftSize, - size_t hopSize, - bool phaseLock); + size_t hopSize); + +/// Computes a single frame of STFT data for time stretching. +/// The assumption is that we are positioned exactly at `frame`, and we therefore +/// just need framePrev to compute the instantaneous frequency. We also do not +/// need to perform any magnitude or frequency interpolation. +/// +/// This version uses Miller Puckette's phase locking. +/// +/// \param frame The current STFT frame +/// \param framePrev The previous STFT frame +/// \param [out] outFrame The output STFT frame +/// \param outFramePrev The previously computed output STFT frame +/// \param fftSize The FFT size +/// \param hopSize The hop size +void Stretch2Puckette( + const SCPolarBuf *frame, + const SCPolarBuf *framePrev, + SCPolarBuf *outFrame, + const SCPolarBuf *outFramePrev, + size_t fftSize, + size_t hopSize); /// Computes a single frame of STFT data for time stretching. /// The assumption is that we are positioned between framePrev1 and frameNext. @@ -107,7 +126,6 @@ void Stretch2( /// \param pos The position between framePrev1 and frameNext (0 < pos < 1) /// \param fftSize The FFT size /// \param hopSize The hop size -/// \param phaseLock Whether or not to apply phase locking void Stretch3( const SCPolarBuf *frameNext, const SCPolarBuf *framePrev1, @@ -116,8 +134,32 @@ void Stretch3( const SCPolarBuf *outFramePrev, float pos, size_t fftSize, - size_t hopSize, - bool phaseLock); + size_t hopSize); + +/// Computes a single frame of STFT data for time stretching. +/// The assumption is that we are positioned between framePrev1 and frameNext. +/// This means we will need to interpolate frequency data. So we will need to +/// compute two frequencies for each bin, and that means we need three STFT frames. +/// +/// This version uses Miller Puckette's phase locking. +/// +/// \param frameNext The next STFT frame +/// \param framePrev1 The previous STFT frame +/// \param framePrev2 The previous STFT frame before that (required for instantaneous frequency interpolation) +/// \param [out] outFrame The output STFT frame +/// \param outFramePrev The previously computed output STFT frame +/// \param pos The position between framePrev1 and frameNext (0 < pos < 1) +/// \param fftSize The FFT size +/// \param hopSize The hop size +void Stretch3Puckette( + const SCPolarBuf *frameNext, + const SCPolarBuf *framePrev1, + const SCPolarBuf *framePrev2, + SCPolarBuf *outFrame, + const SCPolarBuf *outFramePrev, + float pos, + size_t fftSize, + size_t hopSize); /// Fills a SCPolarBuf with saved STFT data from a single frame /// From a9c191eba7a4a0df2b3c9915259bda9a6eb14e51 Mon Sep 17 00:00:00 2001 From: Jeff Martin Date: Sat, 20 Jun 2026 11:40:37 -0500 Subject: [PATCH 14/25] laroche dolson phase locking added --- CMakeLists.txt | 2 +- src/pv/CMakeLists.txt | 3 - src/pv/peakFinder.hpp | 3 +- src/pv/pvStretch.cpp | 390 +++++++++++++++++++++++++++++++++++++++++- src/pv/pvStretch.hpp | 103 ++++++++++- 5 files changed, 494 insertions(+), 7 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index bc4d081..323fa1f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ # This file (and the other CMakeLists.txt files in this repo) # are adapted from the prototype CMakeLists.txt provided with the SuperCollider plugin starter code. -cmake_minimum_required(VERSION 3.5) +cmake_minimum_required(VERSION 3.20) project(flexplugins) # To make a ZIP archive diff --git a/src/pv/CMakeLists.txt b/src/pv/CMakeLists.txt index fd3fbfb..74de74a 100644 --- a/src/pv/CMakeLists.txt +++ b/src/pv/CMakeLists.txt @@ -2,13 +2,10 @@ add_library(pv MODULE pv.cpp) add_library(pvCFreeze STATIC pvCFreeze.cpp) add_library(pvOther STATIC pvOther.cpp) -add_library(peakFinder STATIC peakFinder.cpp) add_library(pvStretch STATIC pvStretch.cpp) set_target_properties(pvCFreeze PROPERTIES POSITION_INDEPENDENT_CODE ON) set_target_properties(pvOther PROPERTIES POSITION_INDEPENDENT_CODE ON) -set_target_properties(peakFinder PROPERTIES POSITION_INDEPENDENT_CODE ON) set_target_properties(pvStretch PROPERTIES POSITION_INDEPENDENT_CODE ON) -target_link_libraries(pvStretch PRIVATE peakFinder) target_link_libraries(pv PRIVATE pvCFreeze pvOther pvStretch) if(SUPERNOVA) diff --git a/src/pv/peakFinder.hpp b/src/pv/peakFinder.hpp index d6a0e7a..6f1d8e6 100644 --- a/src/pv/peakFinder.hpp +++ b/src/pv/peakFinder.hpp @@ -23,7 +23,8 @@ along with this program. If not, see . */ #pragma once -#include "FFT_UGens.h" + +extern class SCPolarBuf; class Peak { public: diff --git a/src/pv/pvStretch.cpp b/src/pv/pvStretch.cpp index 8f55a96..11c419b 100644 --- a/src/pv/pvStretch.cpp +++ b/src/pv/pvStretch.cpp @@ -221,6 +221,7 @@ void PV_PlayBufStretch_next(PV_PlayBufStretch *unit, int inNumSamples) { // Render the output FFT frame switch (phaseLock) { case 1: + // std::cout <<"P stretch\n"; Stretch2Puckette( unit->m_frameNext, unit->m_framePrev1, @@ -230,6 +231,18 @@ void PV_PlayBufStretch_next(PV_PlayBufStretch *unit, int inNumSamples) { stftBufHopSize ); break; + case 2: + // std::cout <<"LD stretch\n"; + Stretch2LarocheDolson( + unit->m_frameNext, + unit->m_framePrev1, + p, + unit->m_outFramePrev, + unit->m_peakFinder, + stftBufFftSize, + stftBufHopSize + ); + break; default: Stretch2( unit->m_frameNext, @@ -253,6 +266,7 @@ void PV_PlayBufStretch_next(PV_PlayBufStretch *unit, int inNumSamples) { // Render the output FFT frame switch (phaseLock) { case 1: + // std::cout <<"P stretch\n"; Stretch3Puckette( unit->m_frameNext, unit->m_framePrev1, @@ -264,6 +278,20 @@ void PV_PlayBufStretch_next(PV_PlayBufStretch *unit, int inNumSamples) { stftBufHopSize ); break; + case 2: + // std::cout <<"LD stretch\n"; + Stretch3LarocheDolson( + unit->m_frameNext, + unit->m_framePrev1, + unit->m_framePrev2, + p, + unit->m_outFramePrev, + unit->m_peakFinder, + newPos-static_cast(lo), + stftBufFftSize, + stftBufHopSize + ); + break; default: Stretch3( unit->m_frameNext, @@ -374,6 +402,104 @@ void Stretch2Puckette( } } +/// Computes a single frame of STFT data for time stretching, +/// using the Laroche/Dolson identity phase locking scheme. +/// The assumption is that we are positioned exactly at `frame`, and we therefore +/// just need framePrev to compute the instantaneous frequency. We also do not +/// need to perform any magnitude or frequency interpolation. +/// +/// \param frame The current STFT frame +/// \param framePrev The previous STFT frame +/// \param [out] outFrame The output STFT frame +/// \param outFramePrev The previously computed output STFT frame +/// \param peakFinder The PeakFinder instance for determining peak locations in the magnitude spectrum +/// \param fftSize The FFT size +/// \param hopSize The hop size +void Stretch2LarocheDolson( + const SCPolarBuf *frame, + const SCPolarBuf *framePrev, + SCPolarBuf *outFrame, + const SCPolarBuf *outFramePrev, + PeakFinder *peakFinder, + size_t fftSize, + size_t hopSize) { + outFrame->dc = frame->dc; + outFrame->nyq = frame->nyq; + + // Acquire peaks + peakFinder->analyze(frame); + + if (peakFinder->size() == 0) { + // No phase locking if no peaks were acquired + for (size_t xxk = 0; xxk < fftSize/2-1; xxk++) { + outFrame->bin[xxk].mag = frame->bin[xxk].mag; + + // Compute the instantaneous frequency + double omegaK = twopi * (xxk+1) / fftSize; + double phaseInc = frame->bin[xxk].phase - framePrev->bin[xxk].phase - hopSize * omegaK; + phaseInc = std::fmod(phaseInc + pi, twopi) - pi; + double instantaneousFreq = omegaK + phaseInc/hopSize; + + // Compute the phase + outFrame->bin[xxk].phase = outFramePrev->bin[xxk].phase + hopSize * instantaneousFreq; + } + } else { + // Update any bins that occur below the lowest peak's region of influence + for (size_t xxk = 0; xxk < peakFinder->peaks[0].leftValley; xxk++) { + outFrame->bin[xxk].mag = frame->bin[xxk].mag; + + // Compute the instantaneous frequency + double omegaK = twopi * (xxk+1) / fftSize; + double phaseInc = frame->bin[xxk].phase - framePrev->bin[xxk].phase - hopSize * omegaK; + phaseInc = std::fmod(phaseInc + pi, twopi) - pi; + double instantaneousFreq = omegaK + phaseInc/hopSize; + + // Compute the phase + outFrame->bin[xxk].phase = outFramePrev->bin[xxk].phase + hopSize * instantaneousFreq; + } + + // Update any bins that occur above the highest peak's region of influence + for (size_t xxk = peakFinder->peaks[peakFinder->size()-1].rightValley + 1; xxk < fftSize / 2 - 1; xxk++) { + outFrame->bin[xxk].mag = frame->bin[xxk].mag; + + // Compute the instantaneous frequency + double omegaK = twopi * (xxk+1) / fftSize; + double phaseInc = frame->bin[xxk].phase - framePrev->bin[xxk].phase - hopSize * omegaK; + phaseInc = std::fmod(phaseInc + pi, twopi) - pi; + double instantaneousFreq = omegaK + phaseInc/hopSize; + + // Compute the phase + outFrame->bin[xxk].phase = outFramePrev->bin[xxk].phase + hopSize * instantaneousFreq; + } + + // Update all other peaks + for (size_t xxn = 0; xxn < peakFinder->size(); xxn++) { + // First we compute the new phase for the peak bin as usual + Peak peak = peakFinder->peaks[xxn]; + outFrame->bin[peak.peak].mag = frame->bin[peak.peak].mag; + + // Compute the instantaneous frequency + double omegaK = twopi * (peak.peak+1) / fftSize; + double phaseInc = frame->bin[peak.peak].phase - framePrev->bin[peak.peak].phase - hopSize * omegaK; + phaseInc = std::fmod(phaseInc + pi, twopi) - pi; + double instantaneousFreq = omegaK + phaseInc/hopSize; + + // Compute the phase + outFrame->bin[peak.peak].phase = outFramePrev->bin[peak.peak].phase + hopSize * instantaneousFreq; + + // Then we update the phases of all other peaks + for (size_t xxo = peak.leftValley; xxo < peak.peak; xxo++) { + outFrame->bin[xxo].mag = frame->bin[xxo].mag; + outFrame->bin[xxo].phase = outFrame->bin[peak.peak].phase + frame->bin[xxo].phase - frame->bin[peak.peak].phase; + } + for (size_t xxo = peak.peak + 1; xxo <= peak.rightValley; xxo++) { + outFrame->bin[xxo].mag = frame->bin[xxo].mag; + outFrame->bin[xxo].phase = outFrame->bin[peak.peak].phase + frame->bin[xxo].phase - frame->bin[peak.peak].phase; + } + } + } +} + /// Computes a single frame of STFT data for time stretching. /// The assumption is that we are positioned between framePrev1 and frameNext. /// This means we will need to interpolate frequency data. So we will need to @@ -490,6 +616,152 @@ void Stretch3Puckette( } } +/// Computes a single frame of STFT data for time stretching, +/// using the Laroche/Dolson identity phase locking scheme. +/// The assumption is that we are positioned exactly at `frame`, and we therefore +/// just need framePrev to compute the instantaneous frequency. We also do not +/// need to perform any magnitude or frequency interpolation. +/// +/// \param frameNext The next STFT frame +/// \param framePrev1 The previous STFT frame +/// \param framePrev2 The previous STFT frame before that (required for instantaneous frequency interpolation) +/// \param [out] outFrame The output STFT frame +/// \param outFramePrev The previously computed output STFT frame +/// \param peakFinder The PeakFinder instance for determining peak locations in the magnitude spectrum +/// \param pos The position between framePrev1 and frameNext (0 < pos < 1) +/// \param fftSize The FFT size +/// \param hopSize The hop size +static void Stretch3LarocheDolson( + const SCPolarBuf *frameNext, + const SCPolarBuf *framePrev1, + const SCPolarBuf *framePrev2, + SCPolarBuf *outFrame, + const SCPolarBuf *outFramePrev, + PeakFinder *peakFinder, + double pos, + size_t fftSize, + size_t hopSize) { + outFrame->dc = INTERP(framePrev1->dc, frameNext->dc, pos); + outFrame->nyq = INTERP(framePrev1->nyq, frameNext->nyq, pos); + + // Get the closest bin for phase relationships + const SCPolarBuf *cframe = nullptr; + if (pos >= 0.5) { + cframe = frameNext; + } else { + cframe = framePrev1; + } + + // Acquire peaks + std::vector peaks; + peakFinder->analyze(cframe); + + if (peaks.size() == 0) { + // No phase locking if no peaks were acquired + for (size_t xxk = 0; xxk < fftSize/2-1; xxk++) { + outFrame->bin[xxk].mag = INTERP(framePrev1->bin[xxk].mag, frameNext->bin[xxk].mag, pos); + + double omegaK = twopi * (xxk+1) / fftSize; + + // Compute the next instantaneous frequency + double phaseInc = frameNext->bin[xxk].phase - framePrev1->bin[xxk].phase - hopSize * omegaK; + phaseInc = std::fmod(phaseInc + pi, twopi) - pi; + double instantaneousFreqNext = omegaK + phaseInc/hopSize; + + // Compute the previous instantaneous frequency + phaseInc = framePrev1->bin[xxk].phase - framePrev2->bin[xxk].phase - hopSize * omegaK; + phaseInc = std::fmod(phaseInc + pi, twopi) - pi; + double instantaneousFreqPrev = omegaK + phaseInc/hopSize; + + // Interpolate the instantaneous frequency + double instantaneousFreq = INTERP(instantaneousFreqPrev, instantaneousFreqNext, pos); + + // Compute the new phase + outFrame->bin[xxk].phase = outFramePrev->bin[xxk].phase + hopSize * instantaneousFreq; + } + } else { + // Update any bins that occur below the lowest peak's region of influence + for (size_t xxk = 0; xxk < peakFinder->peaks[0].leftValley; xxk++) { + outFrame->bin[xxk].mag = INTERP(framePrev1->bin[xxk].mag, frameNext->bin[xxk].mag, pos); + + // Compute the instantaneous frequency + double omegaK = twopi * (xxk+1) / fftSize; + + double phaseInc = frameNext->bin[xxk].phase - framePrev1->bin[xxk].phase - hopSize * omegaK; + phaseInc = std::fmod(phaseInc + pi, twopi) - pi; + double instantaneousFreqNext = omegaK + phaseInc/hopSize; + + // Compute the previous instantaneous frequency + phaseInc = framePrev1->bin[xxk].phase - framePrev2->bin[xxk].phase - hopSize * omegaK; + phaseInc = std::fmod(phaseInc + pi, twopi) - pi; + double instantaneousFreqPrev = omegaK + phaseInc/hopSize; + + // Interpolate the instantaneous frequency + double instantaneousFreq = INTERP(instantaneousFreqPrev, instantaneousFreqNext, pos); + + // Compute the phase + outFrame->bin[xxk].phase = outFramePrev->bin[xxk].phase + hopSize * instantaneousFreq; + } + + // Update any bins that occur above the highest peak's region of influence + for (size_t xxk = peakFinder->peaks[peakFinder->size()-1].rightValley + 1; xxk < fftSize / 2 - 1; xxk++) { + outFrame->bin[xxk].mag = INTERP(framePrev1->bin[xxk].mag, frameNext->bin[xxk].mag, pos); + + // Compute the instantaneous frequency + double omegaK = twopi * (xxk+1) / fftSize; + + double phaseInc = frameNext->bin[xxk].phase - framePrev1->bin[xxk].phase - hopSize * omegaK; + phaseInc = std::fmod(phaseInc + pi, twopi) - pi; + double instantaneousFreqNext = omegaK + phaseInc/hopSize; + + // Compute the previous instantaneous frequency + phaseInc = framePrev1->bin[xxk].phase - framePrev2->bin[xxk].phase - hopSize * omegaK; + phaseInc = std::fmod(phaseInc + pi, twopi) - pi; + double instantaneousFreqPrev = omegaK + phaseInc/hopSize; + + // Interpolate the instantaneous frequency + double instantaneousFreq = INTERP(instantaneousFreqPrev, instantaneousFreqNext, pos); + + // Compute the phase + outFrame->bin[xxk].phase = outFramePrev->bin[xxk].phase + hopSize * instantaneousFreq; + } + + for (size_t xxn = 0; xxn < peaks.size(); xxn++) { + // First we compute the new phase for the peak bin as usual + Peak peak = peakFinder->peaks[xxn]; + outFrame->bin[peak.peak].mag = INTERP(framePrev1->bin[peak.peak].mag, frameNext->bin[peak.peak].mag, pos); + + // Compute the instantaneous frequency + double omegaK = twopi * (peak.peak+1) / fftSize; + + double phaseInc = frameNext->bin[peak.peak].phase - framePrev1->bin[peak.peak].phase - hopSize * omegaK; + phaseInc = std::fmod(phaseInc + pi, twopi) - pi; + double instantaneousFreqNext = omegaK + phaseInc/hopSize; + + // Compute the previous instantaneous frequency + phaseInc = framePrev1->bin[peak.peak].phase - framePrev2->bin[peak.peak].phase - hopSize * omegaK; + phaseInc = std::fmod(phaseInc + pi, twopi) - pi; + double instantaneousFreqPrev = omegaK + phaseInc/hopSize; + + // Interpolate the instantaneous frequency + double instantaneousFreq = INTERP(instantaneousFreqPrev, instantaneousFreqNext, pos); + + // Compute the phase + outFrame->bin[peak.peak].phase = outFramePrev->bin[peak.peak].phase + hopSize * instantaneousFreq; + + // Then we update all bins in the region of influence + for (size_t xxo = peak.leftValley; xxo < peak.peak; xxo++) { + outFrame->bin[xxo].mag = INTERP(framePrev1->bin[xxo].mag, frameNext->bin[xxo].mag, pos); + outFrame->bin[xxo].phase = outFrame->bin[peak.peak].phase + cframe->bin[xxo].phase - cframe->bin[peak.peak].phase; + } + for (size_t xxo = peak.peak + 1; xxo <= peak.rightValley; xxo++) { + outFrame->bin[xxo].mag = INTERP(framePrev1->bin[xxo].mag, frameNext->bin[xxo].mag, pos); + outFrame->bin[xxo].phase = outFrame->bin[peak.peak].phase + cframe->bin[xxo].phase - cframe->bin[peak.peak].phase; + } + } + } +} + /// Fills a SCPolarBuf with saved STFT data from a single frame /// /// \param fftBuf The FFT frame from the STFT buffer @@ -518,4 +790,120 @@ void copyPolarBuf(const SCPolarBuf *sourceBuf, SCPolarBuf *destBuf, size_t numbi destBuf->bin[xxn].mag = sourceBuf->bin[xxn].mag; destBuf->bin[xxn].phase = sourceBuf->bin[xxn].phase; } -} \ No newline at end of file +} + + +Peak::Peak(size_t peak) : peak(peak) {} +Peak::Peak(size_t peak, size_t leftValley, size_t rightValley) : peak(peak), leftValley(leftValley), rightValley(rightValley) {} + +#define SHIFT(arr, idx, val) \ + for (size_t xxi = ) + +PeakFinder::PeakFinder(size_t fftSize, size_t radius) { + m_maxSize = fftSize/2-1; + m_radius = radius; + m_size = 0; + peaks = nullptr; + m_queueL = nullptr; + m_queueR = nullptr; +} + +void PeakFinder::memLoad(void* arr) { + size_t *data = (size_t*)arr; + m_queueL = data; + m_queueR = data + m_radius; + peaks = (Peak*)(data + 2 * m_radius); +} + +size_t PeakFinder::memSize() const { + return m_radius * 2 * sizeof(size_t) + m_maxSize * sizeof(Peak); +} + +void* PeakFinder::memRetrieve() { + return (void*)m_queueL; +} + +void PeakFinder::clear() { + m_size = 0; +} + +size_t PeakFinder::maxSize() const { + return m_maxSize; +} + +size_t PeakFinder::size() const { + return m_size; +} + +void PeakFinder::analyze(const SCPolarBuf *buf) { + // We can only perform the analysis if we have enough bins + if (m_queueL && m_maxSize > m_radius * 2 + 1) { + m_size = 0; // clear any existing data + + size_t xxi = m_radius; + while (xxi < m_maxSize - m_radius) { + bool isMax = true; + for (size_t xxj = xxi - m_radius; xxj < xxi; xxj++) { + if (buf->bin[xxj].mag >= buf->bin[xxi].mag) { + isMax = false; + break; + } + } + for (size_t xxj = xxi + 1; xxj <= xxi + m_radius; xxj++) { + if (buf->bin[xxj].mag >= buf->bin[xxi].mag) { + isMax = false; + break; + } + } + if (isMax) { + peaks[m_size] = Peak(xxi); + m_size++; + xxi += m_radius + 1; + } else { + xxi++; + } + } + + // Find the left valley for the first peak, and the right valley + // for the last peak. + if (m_size > 0) { + float min = buf->bin[0].mag; + size_t argmin = 0; + size_t xxk = 1; + for (; xxk < peaks[0].peak; xxk++) { + if (buf->bin[xxk].mag < min) { + min = buf->bin[xxk].mag; + argmin = xxk; + } + } + peaks[0].leftValley = argmin; + xxk = peaks[m_size-1].peak + 1; + min = buf->bin[xxk].mag; + argmin = xxk; + for (xxk++; xxk < m_maxSize; xxk++) { + if (buf->bin[xxk].mag < min) { + min = buf->bin[xxk].mag; + argmin = xxk; + } + } + peaks[m_size-1].rightValley = argmin; + } + + // Find the remaining left and right valleys + if (m_size > 1) { + for (size_t xxj = 0; xxj < m_size-1; xxj++) { + size_t xxk = peaks[xxj].peak + 1; + float min = buf->bin[xxk].mag; + size_t argmin = xxk; + for (xxk++; xxk < peaks[xxj+1].peak; xxk++) { + if (buf->bin[xxk].mag < min) { + min = buf->bin[xxk].mag; + argmin = xxk; + } + } + peaks[xxj].rightValley = argmin - 1; + peaks[xxj+1].leftValley = argmin; + } + } + } +} diff --git a/src/pv/pvStretch.hpp b/src/pv/pvStretch.hpp index d52f02c..fe70e1a 100644 --- a/src/pv/pvStretch.hpp +++ b/src/pv/pvStretch.hpp @@ -24,7 +24,60 @@ along with this program. If not, see . #pragma once #include "SC_Unit.h" #include "FFT_UGens.h" -#include "peakFinder.hpp" + +class Peak { +public: + Peak(size_t peak); + Peak(size_t peak, size_t leftValley, size_t rightValley); + size_t peak, leftValley, rightValley; +}; + +class PeakFinder { +public: + /// Constructs the PeakFinder + /// + /// \param fftSize The FFT size + PeakFinder(size_t fftSize, size_t radius); + + /// Finds peaks in the provided SCPolarBuf. Note that this buffer must correspond + /// to the original FFT size provided for the PeakFinder--otherwise memory errors may occur. + /// + /// \param buf The buffer to analyze + void analyze(const SCPolarBuf *buf); + + /// Loads memory from the external SuperCollider allocator. Memory is allocated + /// using RTAlloc() and the size is specified by the memSize() method. + /// + /// \param arr The allocated memory + void memLoad(void *arr); + + /// Gets the memory size required for the PeakFinder + size_t memSize() const; + + /// Gets the max size of the PeakFinder + /// + /// \returns The max size + size_t maxSize() const; + + /// Gets the current size of the PeakFinder (the number of peaks stored) + /// + /// \returns The current size + size_t size() const; + + /// Clears the PeakFinder + void clear(); + + /// Gets a pointer to the memory that was allocated for the PeakFinder + /// so that it can be deallocated + /// + /// \return The memory pointer + void* memRetrieve(); + + Peak *peaks; +private: + size_t m_maxSize, m_size, m_radius; + size_t *m_queueL, *m_queueR; +}; /// Stores the state of a PV_PlayBufStretch UGen instance struct PV_PlayBufStretch : public Unit { @@ -113,6 +166,28 @@ void Stretch2Puckette( size_t fftSize, size_t hopSize); +/// Computes a single frame of STFT data for time stretching, +/// using the Laroche/Dolson identity phase locking scheme. +/// The assumption is that we are positioned exactly at `frame`, and we therefore +/// just need framePrev to compute the instantaneous frequency. We also do not +/// need to perform any magnitude or frequency interpolation. +/// +/// \param frame The current STFT frame +/// \param framePrev The previous STFT frame +/// \param [out] outFrame The output STFT frame +/// \param outFramePrev The previously computed output STFT frame +/// \param peakFinder The PeakFinder instance for determining peak locations in the magnitude spectrum +/// \param fftSize The FFT size +/// \param hopSize The hop size +void Stretch2LarocheDolson( + const SCPolarBuf *frame, + const SCPolarBuf *framePrev, + SCPolarBuf *outFrame, + const SCPolarBuf *outFramePrev, + PeakFinder *peakFinder, + size_t fftSize, + size_t hopSize); + /// Computes a single frame of STFT data for time stretching. /// The assumption is that we are positioned between framePrev1 and frameNext. /// This means we will need to interpolate frequency data. So we will need to @@ -161,6 +236,32 @@ void Stretch3Puckette( size_t fftSize, size_t hopSize); +/// Computes a single frame of STFT data for time stretching, +/// using the Laroche/Dolson identity phase locking scheme. +/// The assumption is that we are positioned exactly at `frame`, and we therefore +/// just need framePrev to compute the instantaneous frequency. We also do not +/// need to perform any magnitude or frequency interpolation. +/// +/// \param frameNext The next STFT frame +/// \param framePrev1 The previous STFT frame +/// \param framePrev2 The previous STFT frame before that (required for instantaneous frequency interpolation) +/// \param [out] outFrame The output STFT frame +/// \param outFramePrev The previously computed output STFT frame +/// \param peakFinder The PeakFinder instance for determining peak locations in the magnitude spectrum +/// \param pos The position between framePrev1 and frameNext (0 < pos < 1) +/// \param fftSize The FFT size +/// \param hopSize The hop size +static void Stretch3LarocheDolson( + const SCPolarBuf *frameNext, + const SCPolarBuf *framePrev1, + const SCPolarBuf *framePrev2, + SCPolarBuf *outFrame, + const SCPolarBuf *outFramePrev, + PeakFinder *peakFinder, + double pos, + size_t fftSize, + size_t hopSize); + /// Fills a SCPolarBuf with saved STFT data from a single frame /// /// \param fftBuf The FFT frame from the STFT buffer From 9182d645b1d95599f96c32a1afab9667d5ec3794 Mon Sep 17 00:00:00 2001 From: Jeff Martin Date: Sat, 20 Jun 2026 12:48:57 -0500 Subject: [PATCH 15/25] removed vector bug --- src/pv/pvStretch.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/pv/pvStretch.cpp b/src/pv/pvStretch.cpp index 11c419b..069435a 100644 --- a/src/pv/pvStretch.cpp +++ b/src/pv/pvStretch.cpp @@ -653,10 +653,9 @@ static void Stretch3LarocheDolson( } // Acquire peaks - std::vector peaks; peakFinder->analyze(cframe); - if (peaks.size() == 0) { + if (peakFinder->size() == 0) { // No phase locking if no peaks were acquired for (size_t xxk = 0; xxk < fftSize/2-1; xxk++) { outFrame->bin[xxk].mag = INTERP(framePrev1->bin[xxk].mag, frameNext->bin[xxk].mag, pos); @@ -726,7 +725,7 @@ static void Stretch3LarocheDolson( outFrame->bin[xxk].phase = outFramePrev->bin[xxk].phase + hopSize * instantaneousFreq; } - for (size_t xxn = 0; xxn < peaks.size(); xxn++) { + for (size_t xxn = 0; xxn < peakFinder->size(); xxn++) { // First we compute the new phase for the peak bin as usual Peak peak = peakFinder->peaks[xxn]; outFrame->bin[peak.peak].mag = INTERP(framePrev1->bin[peak.peak].mag, frameNext->bin[peak.peak].mag, pos); From 73d5fb9fcb0ed720c223e72adc8460c4c94d9dce Mon Sep 17 00:00:00 2001 From: Jeff Martin Date: Sat, 20 Jun 2026 14:34:52 -0500 Subject: [PATCH 16/25] updated help file for pv_playbufstretch with example code --- src/pv/PV_PlayBufStretch.schelp | 80 +++++++++++++++++++++++++-------- 1 file changed, 62 insertions(+), 18 deletions(-) diff --git a/src/pv/PV_PlayBufStretch.schelp b/src/pv/PV_PlayBufStretch.schelp index b87a5bd..34c8e27 100644 --- a/src/pv/PV_PlayBufStretch.schelp +++ b/src/pv/PV_PlayBufStretch.schelp @@ -6,7 +6,20 @@ related:: Guides/FFT-Overview Description:: -PV_PlayBufStretch is a phase vocoder buffer player for time stretching. +PV_PlayBufStretch is a phase vocoder buffer player for time stretching. It differs from link::Classes/PV_BufRd:: +in that it has a phase locking feature. Phase locking is a strategy that is used to reduce +"phasiness"--a reverberant sound caused by phase misalignment in adjacent bins. +The idea is that you want the phases of peak bins to dominate the phases of adjacent bins. +This makes sense intuitively because we know that in an FFT, the energy of a partial is spread +across several bins. PV_PlayBufStretch implements two classic phase locking algorithms--an +algorithm by Miller Puckette,footnote::Puckette, Miller. "Phase-locked vocoder." 1995 IEEE Workshop on Applications of Signal Processing to Audio and Acoustics (1995), 222-225.:: +and a later peak-picking algorithm (called "identity phase locking") +by Jean Laroche and Mark Dolson.footnote::Laroche, Jean, and Mark Dolson. "Improved Phase Vocoder Time-Scale Modification of Audio." IEEE Transactions on Speech and Audio Processing, 7, no. 3 (1999), 323-332.:: + +You will want to choose the algorithm based on the sound it produces. In some cases, you may not +wish to use phase locking at all. Traditionally, one would use a hop size of 0.25 or less along with an +appropriate window (like the Hann window) in the STFT. Laroche and Dolson write that for identity +phase locking, a hop size of 0.5 is feasible. classmethods:: @@ -24,16 +37,23 @@ However, you can change it at any time--if it changes, playback will jump immedi to the new start position. Keep in mind that the first frame played does not use phase vocoder computation, so you may or may not like the sound this produces. +This value is clipped internally to the range [0.0, 1.0]. + argument::rate -The playback rate. 1.0 is normal; slower rates result in a time stretch, and faster rates result in time shrink. Negative rates result in backwards playback. +The playback rate. 1.0 is normal; slower rates result in a time stretch, and faster rates +result in time shrink. Negative rates result in backwards playback. + +argument::loop +Whether (1.0) or not (0.0) to loop when the end of the stftBuffer is reached argument::phaseLock -Whether (1.0) or not (0.0) to apply phase locking. Phase locking is used to avoid the "phasy" sound -of a phase vocoder which happens when adjacent bins drift out of sync with each other. -Phase locking produces a cleaner sound. +The phase locking strategy to adopt. 0.0 means no phase locking, 1.0 means Puckette-style +phase locking, and 2.0 means Laroche/Dolson identity phase locking. -argument::loop -Whether or not to loop when the end of the stftBuffer is reached +argument::peakRadius +The radius for the peak-picker in the Laroche/Dolson identity phase locking +algorithm. A bin is a peak if its magnitude is greater than any magnitude of the +strong::peakRadius:: bins to either side. argument::doneAction The action to take after playback is completed @@ -41,15 +61,39 @@ The action to take after playback is completed Examples:: code:: -{ - var sig, chain1, chain2; - sig = SoundIn.ar(0); - chain1 = FFT(LocalBuf(2048), sig); - chain1 = PV_MagAbove(chain1, 0.5); - chain2 = PV_MagSqueeze(chain1, 0.5, 1.4); - chain1 = PV_MagXFade(chain1, chain2, 0.5); - sig = IFFT(chain1); - sig = Pan2.ar(sig); - Out.ar(0, sig); -}.play; +( +var sf; +p = ExampleFiles.child; +sf = SoundFile.new(p); +sf.openRead; +sf.close; +// The size of the buffer is 3 + fftSize * numStftFrames. +y = Buffer.alloc(Server.default, sf.duration.calcPVRecSize(2048, 0.25, s.sampleRate)); +z = Buffer.read(Server.default, p); +) + +( +SynthDef(\rec, { + arg recBuf, soundBuf; + var in, chain; + Line.kr(dur: BufDur.kr(soundBuf), doneAction: Done.freeSelf); + in = PlayBuf.ar(1, soundBuf, BufRateScale.ir(soundBuf)); + chain = FFT(LocalBuf(2048), in, 0.25, 1); + PV_RecordBuf(chain, recBuf, 0, 1, 0, 0.25, 0); +}).add; + +SynthDef(\play2, { + arg recBuf, rate=1, phaseLock=0, loop=0; + var sig, chain; + // Note that you must specify the appropriate hop size in FFTTrigger. + chain = FFTTrigger(LocalBuf(2048), 0.25, 1.0); + chain = PV_PlayBufStretch(chain, recBuf, 0.0, rate, loop, phaseLock, 2.0, Done.freeSelf); + // Use the same window type that was used for analysis. + sig = IFFT(chain, 1); + Out.ar(0, Pan2.ar(sig)); +}).add; +) + +Synth(\rec, [\recBuf, y, \soundBuf, z]); +b = Synth(\play2, [\recBuf, y, \rate, 0.4, \loop, 1.0, \phaseLock, 2.0]); :: \ No newline at end of file From d381433458a61edb1f0537b87e35ce32a9a70855 Mon Sep 17 00:00:00 2001 From: Jeff Martin Date: Sat, 20 Jun 2026 15:22:22 -0500 Subject: [PATCH 17/25] stretcher is more stable now, and accepts negative and 0 playback rates --- src/pv/PV_PlayBufStretch.schelp | 37 ++++++++++++++--- src/pv/pv.sc | 4 +- src/pv/pvStretch.cpp | 73 ++++++++++++++++++++++----------- 3 files changed, 82 insertions(+), 32 deletions(-) diff --git a/src/pv/PV_PlayBufStretch.schelp b/src/pv/PV_PlayBufStretch.schelp index 34c8e27..6a7bb2b 100644 --- a/src/pv/PV_PlayBufStretch.schelp +++ b/src/pv/PV_PlayBufStretch.schelp @@ -35,7 +35,8 @@ argument::startPos The start position (0.0 to 1.0). Normally you would set this once and let it be. However, you can change it at any time--if it changes, playback will jump immediately to the new start position. Keep in mind that the first frame played does not use phase -vocoder computation, so you may or may not like the sound this produces. +vocoder computation, so you may or may not like the sound produced by changing the +start position during playback. This value is clipped internally to the range [0.0, 1.0]. @@ -53,7 +54,9 @@ phase locking, and 2.0 means Laroche/Dolson identity phase locking. argument::peakRadius The radius for the peak-picker in the Laroche/Dolson identity phase locking algorithm. A bin is a peak if its magnitude is greater than any magnitude of the -strong::peakRadius:: bins to either side. +strong::peakRadius:: bins to either side. This parameter can only be set on construction, +and is clipped internally to the range [1, 32]. Laroche and Dolson suggest a radius of 2, +which is the default here. argument::doneAction The action to take after playback is completed @@ -83,11 +86,13 @@ SynthDef(\rec, { }).add; SynthDef(\play2, { - arg recBuf, rate=1, phaseLock=0, loop=0; + arg recBuf, rate=1, phaseLock=0, loop=0, startPos=0; var sig, chain; // Note that you must specify the appropriate hop size in FFTTrigger. + // Also note that the FFT size must be the same as the FFT size of + // the original analysis. chain = FFTTrigger(LocalBuf(2048), 0.25, 1.0); - chain = PV_PlayBufStretch(chain, recBuf, 0.0, rate, loop, phaseLock, 2.0, Done.freeSelf); + chain = PV_PlayBufStretch(chain, recBuf, startPos, rate, loop, phaseLock, 2.0, Done.freeSelf); // Use the same window type that was used for analysis. sig = IFFT(chain, 1); Out.ar(0, Pan2.ar(sig)); @@ -95,5 +100,27 @@ SynthDef(\play2, { ) Synth(\rec, [\recBuf, y, \soundBuf, z]); -b = Synth(\play2, [\recBuf, y, \rate, 0.4, \loop, 1.0, \phaseLock, 2.0]); +b = Synth(\play2, [\recBuf, y, \rate, 0.5, \loop, 1.0, \startPos, 0.0, \phaseLock, 2.0]); + +// A more unusual take. Here we modulate the playback rate, and it can go negative. +( +{ + var sig, chain; + chain = FFTTrigger(LocalBuf(2048), 0.25, 1.0); + chain = PV_PlayBufStretch(chain, y, 0.0, LFTri.kr(0.1), 1.0, 0.0); + sig = IFFT(chain, 1); + sig = Pan2.ar(sig); +}.play; +) + +// Another unusual take, where we abruptly change the playback start point repeatedly. +( +{ + var sig, chain; + chain = FFTTrigger(LocalBuf(2048), 0.25, 1.0); + chain = PV_PlayBufStretch(chain, y, LFPulse.kr(0.25).range(0.2, 0.6), 0.25, 1.0, 0.0); + sig = IFFT(chain, 1); + sig = Pan2.ar(sig); +}.play; +) :: \ No newline at end of file diff --git a/src/pv/pv.sc b/src/pv/pv.sc index ee8bd66..b584a81 100644 --- a/src/pv/pv.sc +++ b/src/pv/pv.sc @@ -75,7 +75,7 @@ PV_MagXFade : PV_ChainUGen { // A phase vocoder buffer player PV_PlayBufStretch : PV_ChainUGen { *new { - arg buffer, stftBuffer, startPos=0.0, rate=1.0, phaseLock=1.0, loop=0.0, doneAction=0; - ^this.multiNew('control', buffer, stftBuffer, startPos, rate, phaseLock, loop, doneAction); + arg buffer, stftBuffer, startPos=0.0, rate=1.0, loop=0.0, phaseLock=0.0, peakRadius=2.0, doneAction=0; + ^this.multiNew('control', buffer, stftBuffer, startPos, rate, loop, phaseLock, peakRadius, doneAction); } } \ No newline at end of file diff --git a/src/pv/pvStretch.cpp b/src/pv/pvStretch.cpp index 069435a..921b10e 100644 --- a/src/pv/pvStretch.cpp +++ b/src/pv/pvStretch.cpp @@ -43,8 +43,9 @@ void PV_PlayBufStretch_Ctor(PV_PlayBufStretch *unit) { unit->m_frameNext = nullptr; unit->m_framePrev1 = nullptr; unit->m_framePrev2 = nullptr; + size_t peakRadius = static_cast(sc_clip(IN0(6), 1.f, 32.f)); unit->m_peakFinder = (PeakFinder*)RTAlloc(unit->mWorld, sizeof(PeakFinder)); - new (unit->m_peakFinder) PeakFinder(static_cast(buf->samples), 2); + new (unit->m_peakFinder) PeakFinder(static_cast(buf->samples), peakRadius); unit->m_peakFinder->memLoad(RTAlloc(unit->mWorld, unit->m_peakFinder->memSize())); // Configure position @@ -148,7 +149,7 @@ void PV_PlayBufStretch_next(PV_PlayBufStretch *unit, int inNumSamples) { float startPos = sc_clip(IN0(2), 0.0, 1.0); const float rate = IN0(3); - const float loop = IN0(5); + const float loop = IN0(4); // std::cout << "Rate: " << rate << " Loop: " << loop << "\n"; if (startPos != unit->m_startPos) { @@ -160,15 +161,32 @@ void PV_PlayBufStretch_next(PV_PlayBufStretch *unit, int inNumSamples) { // Now that we've run setup, we're ready to read STFT data and perform phase vocoder stretching. // First we need to figure out where we are, and if that means we need to loop or quit. - const float newPos = unit->m_pos + rate; + float newPos = unit->m_pos + rate; + float debugNewPos = newPos; if (newPos > stftFrames - 1) { - if (loop) { + if (loop && rate > 0) { unit->m_firstFrame = true; startPos = 0; + } else if (rate <= 0) { + // clip it to the last possible frame if we're working backwards + newPos = stftFrames - 1; } else { OUT0(0) = -1.f; RELEASE_SNDBUF_SHARED(stftBuf); - DoneAction(static_cast(IN0(6)), unit); + DoneAction(static_cast(IN0(7)), unit); + return; + } + } else if (newPos < 0) { + if (loop && rate < 0) { + unit->m_firstFrame = true; + startPos = 1; + } else if (rate >= 0) { + // clip it to the first frame if we're working forwards + newPos = 0; + } else { + OUT0(0) = -1.f; + RELEASE_SNDBUF_SHARED(stftBuf); + DoneAction(static_cast(IN0(7)), unit); return; } } @@ -177,18 +195,8 @@ void PV_PlayBufStretch_next(PV_PlayBufStretch *unit, int inNumSamples) { // This is essential to make sure that subsequent phase calculations are correctly aligned. if (unit->m_firstFrame) { // Compute the index of the first frame - size_t firstFrameIdx = static_cast(std::round(startPos * stftFrames)); - if (firstFrameIdx >= stftFrames) { - if (loop) { - firstFrameIdx = 0; - } else { - OUT0(0) = -1.f; - RELEASE_SNDBUF_SHARED(stftBuf); - DoneAction(static_cast(IN0(6)), unit); - return; - } - } - + size_t firstFrameIdx = static_cast(std::round(startPos * (stftFrames-1))); + // Copy the FFT data over SCPolarBuf *p = ToPolarApx(buf); const float *currentFftFrame = stftData + (firstFrameIdx * stftBufFftSize); @@ -207,15 +215,20 @@ void PV_PlayBufStretch_next(PV_PlayBufStretch *unit, int inNumSamples) { // For frames other than the first frame, we'll need to perform phase computation. else { - size_t phaseLock = sc_clip(static_cast(IN0(4)), 0, 2); + size_t phaseLock = sc_clip(static_cast(IN0(5)), 0, 2); SCPolarBuf *p = ToPolarApx(buf); size_t roundedPos = static_cast(std::round(newPos)); //std::cout << "New pos: " << intPos << "\n"; + // If we're right smack on a specific FFT frame, we don't + // need to do any magnitude or frequency interpolation, so + // we only need the current and previous FFT frames from the buffer. if (std::abs(roundedPos-newPos) < 1e-3) { - // If we're right smack on a specific FFT frame, we don't - // need to do any magnitude or frequency interpolation, so - // we only need the current and previous FFT frames from the buffer. - size_t lastPos = roundedPos - 1; + size_t lastPos = rate >= 0 ? roundedPos - 1 : roundedPos + 1; + if (lastPos < 0) { + lastPos = roundedPos + 1; + } else if (lastPos >= stftFrames) { + lastPos = roundedPos - 1; + } fillPolarBuf(stftData + (roundedPos * stftBufFftSize), unit->m_frameNext, stftBufFftSize); fillPolarBuf(stftData + (lastPos * stftBufFftSize), unit->m_framePrev1, stftBufFftSize); // Render the output FFT frame @@ -256,13 +269,23 @@ void PV_PlayBufStretch_next(PV_PlayBufStretch *unit, int inNumSamples) { } else { // Otherwise we're between two FFT frames, and we're going to have to // interpolate magnitude and frequency data. - size_t lo = static_cast(std::floor(newPos)); - size_t hi = static_cast(std::ceil(newPos)); + size_t lo, hi, loprev; + if (rate >= 0) { + lo = static_cast(std::floor(newPos)); + hi = static_cast(std::ceil(newPos)); + loprev = lo == 0 ? 0 : lo - 1; + } else { + hi = static_cast(std::floor(newPos)); + lo = static_cast(std::ceil(newPos)); + loprev = lo + 1; + if (loprev >= stftFrames) loprev = lo; + } + // We are in between these two frames fillPolarBuf(stftData + (hi * stftBufFftSize), unit->m_frameNext, stftBufFftSize); fillPolarBuf(stftData + (lo * stftBufFftSize), unit->m_framePrev1, stftBufFftSize); // This is the frame right before that. It's needed to compute the previous instantaneous frequencies. - fillPolarBuf(stftData + ((lo-1) * stftBufFftSize), unit->m_framePrev2, stftBufFftSize); + fillPolarBuf(stftData + (loprev * stftBufFftSize), unit->m_framePrev2, stftBufFftSize); // Render the output FFT frame switch (phaseLock) { case 1: From 61135c6586cafc2fb7a0795e32d6406b3b670abc Mon Sep 17 00:00:00 2001 From: Jeff Martin Date: Sat, 20 Jun 2026 15:28:38 -0500 Subject: [PATCH 18/25] updated docs --- README.md | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index fec907c..6c1fd79 100644 --- a/README.md +++ b/README.md @@ -3,13 +3,16 @@ A collection of SuperCollider plugins developed by Jeff Martin (www.jeffreymarti ## List of Plugins -### LoopPhasor -LoopPhasor is an adaptation of Phasor, which allows you to set loop points. It is useful for playing audio samples for which you can define loop points. +### PV_PlayBufStretch +A phase vocoder STFT buffer player for time stretching, for use with PV_RecordBuf in the sc3plugins distribution. It incorporates two different optional phase locking algorithms. ### PV_CFreeze A SuperCollider implementation of Jean-François Charles' Max patch for freezing [(the Max patch is available here)](https://newfloremusic.gumroad.com/). It is a phase vocoder spectral freeze with an innovation that makes the frozen sound less stagnant. +### PV_MagMirror +Mirrors spectral magnitudes, so that high magnitudes become low and low magnitudes become high. + ### PV_MagSqueeze Squeezes spectral magnitudes to fit between (low, high). @@ -22,6 +25,15 @@ A formant-preserving phase vocoder pitch shifter using the [RubberBand library]( ### RubberBandStretcher A phase vocoder pitch shifter and time stretcher using the [RubberBand library](https://breakfastquay.com/rubberband/). +### ImpulseDropout +A modified version of Impulse that randomly drops a percentage of impulses, producing a stuttering effect. + +### ImpulseJitter +A modified version of Impulse that allows the impulses to be shifted randomly in time. It allows a steady move between regular and chaotic impulses, which can be useful for event triggers. + +### LoopPhasor +An adaptation of Phasor, which allows you to set loop start and stop points. It is useful for playing audio samples for which you can define loop points. + ## To Install You can download a compiled version of the plugins from Releases in this repository. Extract the ZIP file and copy its contents to your SuperCollider extensions directory From 9b0ad0a27b26bfd374c50816a72b697ad0d924b6 Mon Sep 17 00:00:00 2001 From: Jeff Martin Date: Sat, 20 Jun 2026 16:26:53 -0500 Subject: [PATCH 19/25] added rubberband buf version --- src/rubberband/CMakeLists.txt | 4 + src/rubberband/rubberBandPS.cpp | 24 ++++ src/rubberband/rubberBandPS.hpp | 24 ++++ src/rubberband/rubberBandStretcher.cpp | 24 ++++ src/rubberband/rubberBandStretcher.hpp | 29 ++++ src/rubberband/rubberBandStretcherBuf.cpp | 155 ++++++++++++++++++++++ src/rubberband/rubberBandStretcherBuf.hpp | 58 ++++++++ src/rubberband/rubberband.sc | 17 +++ 8 files changed, 335 insertions(+) create mode 100644 src/rubberband/rubberBandStretcherBuf.cpp create mode 100644 src/rubberband/rubberBandStretcherBuf.hpp diff --git a/src/rubberband/CMakeLists.txt b/src/rubberband/CMakeLists.txt index 82d4f6e..c881d5c 100644 --- a/src/rubberband/CMakeLists.txt +++ b/src/rubberband/CMakeLists.txt @@ -18,15 +18,19 @@ endif() add_library(rubberband MODULE rubberband.cpp) add_library(rubberBandPS STATIC rubberBandPS.cpp) add_library(rubberBandStretcher STATIC rubberBandStretcher.cpp) +add_library(rubberBandStretcherBuf STATIC rubberBandStretcherBuf.cpp) set_target_properties(rubberband_lib PROPERTIES POSITION_INDEPENDENT_CODE ON) set_target_properties(rubberBandPS PROPERTIES POSITION_INDEPENDENT_CODE ON) set_target_properties(rubberBandStretcher PROPERTIES POSITION_INDEPENDENT_CODE ON) +set_target_properties(rubberBandStretcherBuf PROPERTIES POSITION_INDEPENDENT_CODE ON) target_link_libraries(rubberband PRIVATE rubberBandPS rubberBandStretcher + rubberBandStretcherBuf ) target_link_libraries(rubberBandPS PRIVATE rubberband_lib) target_link_libraries(rubberBandStretcher PRIVATE rubberband_lib) +target_link_libraries(rubberBandStretcherBuf PRIVATE rubberband_lib) if(SUPERNOVA) add_library(rubberband_supernova MODULE rubberband.cpp) diff --git a/src/rubberband/rubberBandPS.cpp b/src/rubberband/rubberBandPS.cpp index 6e5a684..1c9b23b 100644 --- a/src/rubberband/rubberBandPS.cpp +++ b/src/rubberband/rubberBandPS.cpp @@ -1,3 +1,27 @@ +/* +File: rubberBandPS.cpp +Author: Jeff Martin + +Description: +A high-quality formant preserving pitch shifter using the RubberBand library. + +Copyright © 2026 by Jeffrey Martin. All rights reserved. +Website: https://www.jeffreymartincomposer.com + +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 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + #include "rubberBandPS.hpp" #include "SC_PlugIn.h" diff --git a/src/rubberband/rubberBandPS.hpp b/src/rubberband/rubberBandPS.hpp index 9fbca09..82024ac 100644 --- a/src/rubberband/rubberBandPS.hpp +++ b/src/rubberband/rubberBandPS.hpp @@ -1,3 +1,27 @@ +/* +File: rubberBandPS.hpp +Author: Jeff Martin + +Description: +A high-quality formant preserving pitch shifter using the RubberBand library. + +Copyright © 2026 by Jeffrey Martin. All rights reserved. +Website: https://www.jeffreymartincomposer.com + +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 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + #pragma once #include "SC_Unit.h" #include "rubberband/RubberBandLiveShifter.h" diff --git a/src/rubberband/rubberBandStretcher.cpp b/src/rubberband/rubberBandStretcher.cpp index 5d18e73..c9295dd 100644 --- a/src/rubberband/rubberBandStretcher.cpp +++ b/src/rubberband/rubberBandStretcher.cpp @@ -1,3 +1,27 @@ +/* +File: rubberBandStretcher.cpp +Author: Jeff Martin + +Description: +A high-quality formant preserving pitch shifter and time stretcher using the RubberBand library. + +Copyright © 2026 by Jeffrey Martin. All rights reserved. +Website: https://www.jeffreymartincomposer.com + +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 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + #include "rubberBandStretcher.hpp" #include #include "SC_PlugIn.h" diff --git a/src/rubberband/rubberBandStretcher.hpp b/src/rubberband/rubberBandStretcher.hpp index c8a765c..31b3302 100644 --- a/src/rubberband/rubberBandStretcher.hpp +++ b/src/rubberband/rubberBandStretcher.hpp @@ -1,10 +1,39 @@ +/* +File: rubberBandStretcher.hpp +Author: Jeff Martin + +Description: +A high-quality formant preserving pitch shifter and time stretcher using the RubberBand library. + +Copyright © 2026 by Jeffrey Martin. All rights reserved. +Website: https://www.jeffreymartincomposer.com + +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 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + #pragma once #include "SC_Unit.h" #include "rubberband/RubberBandStretcher.h" struct RubberBandStretcher : public Unit { + /// The stretcher RubberBand::RubberBandStretcher* m_stretcher; + + /// The number of initial output samples to discard size_t m_samplesToDiscard; + + // A collection of settings for the RubberBand stretcher float m_timeRatio; float m_pitchRatio; float m_formantRatio; diff --git a/src/rubberband/rubberBandStretcherBuf.cpp b/src/rubberband/rubberBandStretcherBuf.cpp new file mode 100644 index 0000000..60ae884 --- /dev/null +++ b/src/rubberband/rubberBandStretcherBuf.cpp @@ -0,0 +1,155 @@ +/* +File: rubberBandStretcherBuf.cpp +Author: Jeff Martin + +Description: +A high-quality formant preserving pitch shifter and time stretcher using the RubberBand library. + +Copyright © 2026 by Jeffrey Martin. All rights reserved. +Website: https://www.jeffreymartincomposer.com + +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 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +#include "rubberBandStretcherBuf.hpp" +#include +#include "SC_PlugIn.h" + +extern InterfaceTable *ft; + +void RubberBandStretcherBuf_Ctor(RubberBandStretcherBuf *unit) { + float timeRatio = IN0(1); + float pitchRatio = IN0(2); + float formantRatio = IN0(3); + int transientsMode = static_cast(IN0(4)); + int detector = static_cast(IN0(5)); + int phaseOption = static_cast(IN0(6)); + int pitchQuality = static_cast(IN0(7)); + int windowOption = static_cast(IN0(8)); + int smoothing = static_cast(IN0(9)); + int engine = static_cast(IN0(10)); + + unit->m_timeRatio = timeRatio; + unit->m_pitchRatio = pitchRatio; + unit->m_formantRatio = formantRatio; + unit->m_transientsMode = transientsMode; + unit->m_detectorOption = detector; + unit->m_phaseOption = phaseOption; + unit->m_pitchQuality = pitchQuality; + + // Acquire the sound buffer + float fbufnum = IN0(1); + uint32 bufnum = static_cast(fbufnum); + if (bufnum >= unit->mWorld->mNumSndBufs) bufnum = 0; + unit->m_fbufnum = fbufnum; + unit->m_buf = unit->mWorld->mSndBufs + bufnum; + unit->m_writePtr = 0; + + // Set up RubberBandStretcher initial options + int options = 0x01000001; // formant-preserving, real-time options set + switch (transientsMode) { + case 1: + options |= 0x00000100; + break; + case 2: + options |= 0x00000200; + break; + } + switch (detector) { + case 1: + options |= 0x00000400; + break; + case 2: + options |= 0x00000800; + break; + } + switch (phaseOption) { + case 1: + options |= 0x00002000; + } + switch (pitchQuality) { + case 1: + options |= 0x02000000; + break; + case 2: + options |= 0x04000000; + break; + } + switch (windowOption) { + case 1: + options |= 0x00100000; + break; + case 2: + options |= 0x00200000; + break; + } + switch (smoothing) { + case 1: + options |= 0x00800000; + break; + } + switch (engine) { + case 1: + options |= 0x20000000; + break; + } + + // Allocate the shifter with the given options + unit->m_stretcher = (RubberBand::RubberBandStretcher*)RTAlloc(unit->mWorld, sizeof(RubberBand::RubberBandStretcher)); + new (unit->m_stretcher) RubberBand::RubberBandStretcher(static_cast(SAMPLERATE), 1, options, timeRatio, pitchRatio); + + // Initialize the shifter + // The shifter accepts a block size (which must be set before the first process() + // call and not after), which avoids the need to use local RingBuffers. + unit->m_stretcher->setMaxProcessSize(BUFLENGTH); + unit->m_stretcher->setTimeRatio(sc_clip(timeRatio, 1.f, std::numeric_limits::infinity())); + unit->m_stretcher->setPitchScale(sc_clip(pitchRatio, 1e-2, 64)); + unit->m_stretcher->setFormantScale(sc_clip(formantRatio, 1e-2, 64)); + + // Feed samples in until the shifter is ready to start producing valid output. + // This is necessary because the shifter isn't ready to produce valid output + // as soon as it is initialized--it requires padded 0s to be fed in for some + // number of samples specified by the shifter. + float *zeroBuf = (float*)RTAlloc(unit->mWorld, BUFLENGTH * sizeof(float)); + for (size_t i = 0; i < BUFLENGTH; i++) { + zeroBuf[i] = 0.f; + } + + // The number of initial zeros required + size_t startPad = unit->m_stretcher->getPreferredStartPad(); + + // The number of samples to discard at the beginning of the stretcher output. + // This is handled in the RubberBandStretcher_next() method. + unit->m_samplesToDiscard = unit->m_stretcher->getStartDelay(); + + // Feed in the start pad samples + while (startPad > 0) { + unit->m_stretcher->process(&zeroBuf, BUFLENGTH, false); + startPad -= BUFLENGTH; + } + RTFree(unit->mWorld, zeroBuf); + + // Initialize first out sample + OUT0(0) = 0; + + SETCALC(RubberBandStretcherBuf_next); +} + +void RubberBandStretcherBuf_Dtor(RubberBandStretcherBuf *unit) { + RTFree(unit->mWorld, unit->m_stretcher); +} + +void RubberBandStretcherBuf_next(RubberBandStretcherBuf *unit, int inNumSamples) { + +} \ No newline at end of file diff --git a/src/rubberband/rubberBandStretcherBuf.hpp b/src/rubberband/rubberBandStretcherBuf.hpp new file mode 100644 index 0000000..512e2b4 --- /dev/null +++ b/src/rubberband/rubberBandStretcherBuf.hpp @@ -0,0 +1,58 @@ +/* +File: rubberBandStretcherBuf.hpp +Author: Jeff Martin + +Description: +A high-quality formant preserving pitch shifter and time stretcher using the RubberBand library. + +Copyright © 2026 by Jeffrey Martin. All rights reserved. +Website: https://www.jeffreymartincomposer.com + +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 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +#pragma once + +#include "SC_Unit.h" +#include "rubberband/RubberBandStretcher.h" + +struct RubberBandStretcherBuf : public Unit { + /// The stretcher + RubberBand::RubberBandStretcher* m_stretcher; + + /// The number of initial output samples to discard + size_t m_samplesToDiscard; + + // A collection of settings for the RubberBand stretcher + float m_timeRatio; + float m_pitchRatio; + float m_formantRatio; + int m_transientsMode; + int m_detectorOption; + int m_phaseOption; + int m_pitchQuality; + + /// The index of the buffer with STFT data + float m_fbufnum; + + /// The audio buffer to write the stretched audio to + SndBuf *m_buf; + + /// The next sample to write to + size_t m_writePtr; +}; + +void RubberBandStretcherBuf_Ctor(RubberBandStretcherBuf *unit); +void RubberBandStretcherBuf_Dtor(RubberBandStretcherBuf *unit); +void RubberBandStretcherBuf_next(RubberBandStretcherBuf *unit, int inNumSamples); \ No newline at end of file diff --git a/src/rubberband/rubberband.sc b/src/rubberband/rubberband.sc index 13f43eb..a822f53 100644 --- a/src/rubberband/rubberband.sc +++ b/src/rubberband/rubberband.sc @@ -45,3 +45,20 @@ RubberBandStretcher : UGen { .madd(1.0, 0.0); } } + +// RubberBandStretcherBuf is a phase vocoder-based time stretcher/pitch shifter +// using the Rubber Band library. +// This UGen writes the output to a buffer, which allows arbitrary time stretches +// including values less than 1.0. +RubberBandStretcherBuf : UGen { + *ar { + arg in, bufnum=0, offset=0.0, recLevel=1.0, preLevel=0.0, run=1.0, loop=1.0, trigger=1.0, + timeRatio=1.0, pitchRatio=1.0, formantRatio=0.0, transientsMode=0, + detectorMode=0, phaseMode=0, pitchQuality=0, windowOption=0, + smoothing=0, engine=0, doneAction=0; + ^this.multiNew('audio', in, bufnum, offset, recLevel, preLeel, run, loop, trigger, + timeRatio, pitchRatio, formantRatio, transientsMode, + detectorMode, phaseMode, pitchQuality, windowOption, + smoothing, engine, doneAction); + } +} From 9815abad375de47ef835e77b356380f196cf3027 Mon Sep 17 00:00:00 2001 From: Jeff Martin Date: Sat, 20 Jun 2026 20:22:25 -0500 Subject: [PATCH 20/25] fixed bug in rubberband sc library; removed extraneous files --- src/pv/peakFinder.cpp | 140 ----------------------------------- src/pv/peakFinder.hpp | 81 -------------------- src/rubberband/rubberband.sc | 2 +- 3 files changed, 1 insertion(+), 222 deletions(-) delete mode 100644 src/pv/peakFinder.cpp delete mode 100644 src/pv/peakFinder.hpp diff --git a/src/pv/peakFinder.cpp b/src/pv/peakFinder.cpp deleted file mode 100644 index 4bc36b6..0000000 --- a/src/pv/peakFinder.cpp +++ /dev/null @@ -1,140 +0,0 @@ -/* -File: peakFinder.cpp -Author: Jeff Martin - -Description: -A peak finder for the Laroche/Dolson phase locking algorithm. - -Copyright © 2026 by Jeffrey Martin. All rights reserved. -Website: https://www.jeffreymartincomposer.com - -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 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -*/ - -#include "peakFinder.hpp" - -Peak::Peak(size_t peak) : peak(peak) {} -Peak::Peak(size_t peak, size_t leftValley, size_t rightValley) : peak(peak), leftValley(leftValley), rightValley(rightValley) {} - -#define SHIFT(arr, idx, val) \ - for (size_t xxi = ) - -PeakFinder::PeakFinder(size_t fftSize, size_t radius) { - m_maxSize = fftSize/2-1; - m_radius = radius; - m_size = 0; - peaks = nullptr; - m_queueL = nullptr; - m_queueR = nullptr; -} - -void PeakFinder::memLoad(void* arr) { - size_t *data = (size_t*)arr; - m_queueL = data; - m_queueR = data + m_radius; - peaks = (Peak*)(data + 2 * m_radius); -} - -size_t PeakFinder::memSize() const { - return m_radius * 2 * sizeof(size_t) + m_maxSize * sizeof(Peak); -} - -void* PeakFinder::memRetrieve() { - return (void*)m_queueL; -} - -void PeakFinder::clear() { - m_size = 0; -} - -size_t PeakFinder::maxSize() const { - return m_maxSize; -} - -size_t PeakFinder::size() const { - return m_size; -} - -void PeakFinder::analyze(const SCPolarBuf *buf) { - // We can only perform the analysis if we have enough bins - if (m_queueL && m_maxSize > m_radius * 2 + 1) { - m_size = 0; // clear any existing data - - size_t xxi = m_radius; - while (xxi < m_maxSize - m_radius) { - bool isMax = true; - for (size_t xxj = xxi - m_radius; xxj < xxi; xxj++) { - if (buf->bin[xxj].mag >= buf->bin[xxi].mag) { - isMax = false; - break; - } - } - for (size_t xxj = xxi + 1; xxj <= xxi + m_radius; xxj++) { - if (buf->bin[xxj].mag >= buf->bin[xxi].mag) { - isMax = false; - break; - } - } - if (isMax) { - peaks[m_size] = Peak(xxi); - m_size++; - xxi += m_radius + 1; - } else { - xxi++; - } - } - - // Find the left valley for the first peak, and the right valley - // for the last peak. - if (m_size > 0) { - float min = buf->bin[0].mag; - size_t argmin = 0; - size_t xxk = 1; - for (; xxk < peaks[0].peak; xxk++) { - if (buf->bin[xxk].mag < min) { - min = buf->bin[xxk].mag; - argmin = xxk; - } - } - peaks[0].leftValley = argmin; - xxk = peaks[m_size-1].peak + 1; - min = buf->bin[xxk].mag; - argmin = xxk; - for (xxk++; xxk < m_maxSize; xxk++) { - if (buf->bin[xxk].mag < min) { - min = buf->bin[xxk].mag; - argmin = xxk; - } - } - peaks[m_size-1].rightValley = argmin; - } - - // Find the remaining left and right valleys - if (m_size > 1) { - for (size_t xxj = 0; xxj < m_size-1; xxj++) { - size_t xxk = peaks[xxj].peak + 1; - float min = buf->bin[xxk].mag; - size_t argmin = xxk; - for (xxk++; xxk < peaks[xxj+1].peak; xxk++) { - if (buf->bin[xxk].mag < min) { - min = buf->bin[xxk].mag; - argmin = xxk; - } - } - peaks[xxj].rightValley = argmin - 1; - peaks[xxj+1].leftValley = argmin; - } - } - } -} diff --git a/src/pv/peakFinder.hpp b/src/pv/peakFinder.hpp deleted file mode 100644 index 6f1d8e6..0000000 --- a/src/pv/peakFinder.hpp +++ /dev/null @@ -1,81 +0,0 @@ -/* -File: peakFinder.hpp -Author: Jeff Martin - -Description: -A peak finder for the Laroche/Dolson phase locking algorithm. - -Copyright © 2026 by Jeffrey Martin. All rights reserved. -Website: https://www.jeffreymartincomposer.com - -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 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -*/ - -#pragma once - -extern class SCPolarBuf; - -class Peak { -public: - Peak(size_t peak); - Peak(size_t peak, size_t leftValley, size_t rightValley); - size_t peak, leftValley, rightValley; -}; - -class PeakFinder { -public: - /// Constructs the PeakFinder - /// - /// \param fftSize The FFT size - PeakFinder(size_t fftSize, size_t radius); - - /// Finds peaks in the provided SCPolarBuf. Note that this buffer must correspond - /// to the original FFT size provided for the PeakFinder--otherwise memory errors may occur. - /// - /// \param buf The buffer to analyze - void analyze(const SCPolarBuf *buf); - - /// Loads memory from the external SuperCollider allocator. Memory is allocated - /// using RTAlloc() and the size is specified by the memSize() method. - /// - /// \param arr The allocated memory - void memLoad(void *arr); - - /// Gets the memory size required for the PeakFinder - size_t memSize() const; - - /// Gets the max size of the PeakFinder - /// - /// \returns The max size - size_t maxSize() const; - - /// Gets the current size of the PeakFinder (the number of peaks stored) - /// - /// \returns The current size - size_t size() const; - - /// Clears the PeakFinder - void clear(); - - /// Gets a pointer to the memory that was allocated for the PeakFinder - /// so that it can be deallocated - /// - /// \return The memory pointer - void* memRetrieve(); - - Peak *peaks; -private: - size_t m_maxSize, m_size, m_radius; - size_t *m_queueL, *m_queueR; -}; diff --git a/src/rubberband/rubberband.sc b/src/rubberband/rubberband.sc index a822f53..fa43783 100644 --- a/src/rubberband/rubberband.sc +++ b/src/rubberband/rubberband.sc @@ -56,7 +56,7 @@ RubberBandStretcherBuf : UGen { timeRatio=1.0, pitchRatio=1.0, formantRatio=0.0, transientsMode=0, detectorMode=0, phaseMode=0, pitchQuality=0, windowOption=0, smoothing=0, engine=0, doneAction=0; - ^this.multiNew('audio', in, bufnum, offset, recLevel, preLeel, run, loop, trigger, + ^this.multiNew('audio', in, bufnum, offset, recLevel, preLevel, run, loop, trigger, timeRatio, pitchRatio, formantRatio, transientsMode, detectorMode, phaseMode, pitchQuality, windowOption, smoothing, engine, doneAction); From 9f06bfdffe70905221def525b01b2c6b0e918185 Mon Sep 17 00:00:00 2001 From: Jeff Martin Date: Sat, 20 Jun 2026 20:37:39 -0500 Subject: [PATCH 21/25] fixed impulse help files --- src/generators/ImpulseDropout.schelp | 2 +- src/generators/ImpulseJitter.schelp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/generators/ImpulseDropout.schelp b/src/generators/ImpulseDropout.schelp index 8c16a1d..109cb22 100644 --- a/src/generators/ImpulseDropout.schelp +++ b/src/generators/ImpulseDropout.schelp @@ -1,7 +1,7 @@ class:: ImpulseDropout summary:: Modified Impulse with dropout related:: Classes/Impulse, Classes/Dust -categories:: Libraries>JeffUGens, UGens>Generators>Stochastic +categories:: Libraries>FlexUGens, UGens>Generators>Stochastic Description:: diff --git a/src/generators/ImpulseJitter.schelp b/src/generators/ImpulseJitter.schelp index 8aebdee..e1ad49e 100644 --- a/src/generators/ImpulseJitter.schelp +++ b/src/generators/ImpulseJitter.schelp @@ -1,7 +1,7 @@ class:: ImpulseJitter summary:: Modified Impulse with jitter related:: Classes/Impulse, Classes/Dust -categories:: Libraries>JeffUGens, UGens>Generators>Stochastic +categories:: Libraries>FlexUGens, UGens>Generators>Stochastic Description:: From a751a79f89ff339dfa617b6d6426aceecb9638c7 Mon Sep 17 00:00:00 2001 From: Jeff Martin Date: Sun, 21 Jun 2026 09:14:58 -0500 Subject: [PATCH 22/25] cleaned up debug code --- src/pv/pvStretch.cpp | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/src/pv/pvStretch.cpp b/src/pv/pvStretch.cpp index 921b10e..66db7be 100644 --- a/src/pv/pvStretch.cpp +++ b/src/pv/pvStretch.cpp @@ -122,8 +122,7 @@ void PV_PlayBufStretch_next(PV_PlayBufStretch *unit, int inNumSamples) { const size_t stftBufFftSize = static_cast(bufData[0]); const size_t stftBufHopSize = static_cast(bufData[1] * stftBufFftSize); // in frames, not fraction const int stftBufWinType = static_cast(bufData[2]); // -1, 0, or 1. This information is probably extraneous. - //std::cout << "FFT size: " << stftBufFftSize << " Hop size: " << stftBufHopSize << " Win type: " << stftBufWinType << " STFT frames " << stftFrames << "\n"; - + if (stftBufFftSize != buf->samples) { OUT0(0) = -1.f; std::cout << "WARNING: The FFT size of the STFT buffer (" << stftBufFftSize << @@ -150,8 +149,7 @@ void PV_PlayBufStretch_next(PV_PlayBufStretch *unit, int inNumSamples) { float startPos = sc_clip(IN0(2), 0.0, 1.0); const float rate = IN0(3); const float loop = IN0(4); - // std::cout << "Rate: " << rate << " Loop: " << loop << "\n"; - + if (startPos != unit->m_startPos) { unit->m_startPos = startPos; unit->m_firstFrame = true; @@ -162,7 +160,6 @@ void PV_PlayBufStretch_next(PV_PlayBufStretch *unit, int inNumSamples) { // First we need to figure out where we are, and if that means we need to loop or quit. float newPos = unit->m_pos + rate; - float debugNewPos = newPos; if (newPos > stftFrames - 1) { if (loop && rate > 0) { unit->m_firstFrame = true; @@ -218,7 +215,7 @@ void PV_PlayBufStretch_next(PV_PlayBufStretch *unit, int inNumSamples) { size_t phaseLock = sc_clip(static_cast(IN0(5)), 0, 2); SCPolarBuf *p = ToPolarApx(buf); size_t roundedPos = static_cast(std::round(newPos)); - //std::cout << "New pos: " << intPos << "\n"; + // If we're right smack on a specific FFT frame, we don't // need to do any magnitude or frequency interpolation, so // we only need the current and previous FFT frames from the buffer. @@ -234,7 +231,6 @@ void PV_PlayBufStretch_next(PV_PlayBufStretch *unit, int inNumSamples) { // Render the output FFT frame switch (phaseLock) { case 1: - // std::cout <<"P stretch\n"; Stretch2Puckette( unit->m_frameNext, unit->m_framePrev1, @@ -245,7 +241,6 @@ void PV_PlayBufStretch_next(PV_PlayBufStretch *unit, int inNumSamples) { ); break; case 2: - // std::cout <<"LD stretch\n"; Stretch2LarocheDolson( unit->m_frameNext, unit->m_framePrev1, @@ -289,7 +284,6 @@ void PV_PlayBufStretch_next(PV_PlayBufStretch *unit, int inNumSamples) { // Render the output FFT frame switch (phaseLock) { case 1: - // std::cout <<"P stretch\n"; Stretch3Puckette( unit->m_frameNext, unit->m_framePrev1, @@ -302,7 +296,6 @@ void PV_PlayBufStretch_next(PV_PlayBufStretch *unit, int inNumSamples) { ); break; case 2: - // std::cout <<"LD stretch\n"; Stretch3LarocheDolson( unit->m_frameNext, unit->m_framePrev1, @@ -422,6 +415,9 @@ void Stretch2Puckette( std::complex prevBinKPlus1 = std::polar(outFramePrev->bin[xxk+1].mag, outFramePrev->bin[xxk+1].phase); prevPhase = std::arg(prevBinK - prevBinKMinus1 - prevBinKPlus1); } + + // Compute the new phase + outFrame->bin[xxk].phase = prevPhase + hopSize * instantaneousFreq; } } @@ -818,9 +814,6 @@ void copyPolarBuf(const SCPolarBuf *sourceBuf, SCPolarBuf *destBuf, size_t numbi Peak::Peak(size_t peak) : peak(peak) {} Peak::Peak(size_t peak, size_t leftValley, size_t rightValley) : peak(peak), leftValley(leftValley), rightValley(rightValley) {} -#define SHIFT(arr, idx, val) \ - for (size_t xxi = ) - PeakFinder::PeakFinder(size_t fftSize, size_t radius) { m_maxSize = fftSize/2-1; m_radius = radius; From d08092b323536bdbe1c4ebf128a2bac27079f94c Mon Sep 17 00:00:00 2001 From: Jeff Martin Date: Sun, 21 Jun 2026 16:37:01 -0500 Subject: [PATCH 23/25] rubberband stretcher buf implementation works --- CMakeLists.txt | 1 + src/rubberband/RubberBandStretcherBuf.schelp | 219 +++++++++++++++++++ src/rubberband/rubberBandStretcherBuf.cpp | 176 +++++++++++++-- src/rubberband/rubberBandStretcherBuf.hpp | 6 + src/rubberband/rubberband.cpp | 2 + 5 files changed, 387 insertions(+), 17 deletions(-) create mode 100644 src/rubberband/RubberBandStretcherBuf.schelp diff --git a/CMakeLists.txt b/CMakeLists.txt index 323fa1f..4bfc63a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -134,6 +134,7 @@ install(FILES src/pv/PV_MagXFade.schelp src/rubberband/RubberBandPS.schelp src/rubberband/RubberBandStretcher.schelp + src/rubberband/RubberBandStretcherBuf.schelp DESTINATION HelpSource/Classes PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ ) diff --git a/src/rubberband/RubberBandStretcherBuf.schelp b/src/rubberband/RubberBandStretcherBuf.schelp new file mode 100644 index 0000000..d07350e --- /dev/null +++ b/src/rubberband/RubberBandStretcherBuf.schelp @@ -0,0 +1,219 @@ +class:: RubberBandStretcherBuf +summary:: Time stretcher and formant preserving pitch shifter using the RubberBand library +related:: Classes/RecordBuf +categories:: Libraries>FlexUGens, UGens>Filters>Pitch + +Description:: + +RubberBandStretcherBuf is a phase vocoder time stretcher and formant-preserving pitch shifter +using the open-source RubberBand library. This UGen writes the stretched output to a buffer, +which allows time shrinking as well as stretching. If you just want to access the output +directly, check out link::Classes/RubberBandStretcher:: instead. RubberBandStretcherBuf +is probably a better choice, though, because RubberBandStretcher can run into audio +dropout issues due to on-the-fly memory allocation. + +A few of the options here cannot be set after construction. All other options may be +changed on the fly. + +For more information about the RubberBand library, visit link::https://breakfastquay.com/rubberband/::. + +note:: +This UGen uses quite a bit of CPU resources, which could be a problem on a less powerful computer. +You should also consider increasing the strong::memSize:: option for the server from the default 128MB. +:: + +classmethods:: + +method::ar + +argument::in +The input sound + +argument::bufnum +The index of the buffer to write to. This can only be set once, on construction. + +argument::offset +The initial offset into the buffer in samples. This can only be set once, on construction. + +argument::recLevel +The stretched audio will be scaled by this level before writing to the buffer. + +argument::preLevel +The audio already in the buffer will be scaled by this level before being added to the new audio. + +argument::run +If 1, the output of the stretcher will be written to the audio buffer. If 0, the input audio +will still be run through the stretcher, but the stretched output won't be written to the audio buffer. + +argument::loop +If 0, the UGen will stop writing to the buffer when it is full. If > 0, the UGen will loop back to the +beginning of the buffer and keep writing. + +argument::trigger +If the trigger changes from non-positive to positive the record position will jump to the beginning of the buffer. + +argument::timeRatio +The time stretch ratio. For safety, values are clipped between 1e-5 and 1e5. + +argument::pitchRatio +The pitch shift ratio. For safety, values are clipped between 0.01 and 64. + +argument::formantRatio +The formant shift ratio. The default is 0.0, which means that the pitch shifter +will automatically calculate the formant shift for formant preservation. +For safety, unless the strong::formantRatio:: is 0.0, the value will be clipped between 0.01 and 64. + +argument::transientsMode +There are three transients modes, numbered 0, 1, and 2. +Explanations of what these modes do are quoted directly +from the source code comments in the RubberBand library +(see link::https://github.com/breakfastquay/rubberband/blob/default/src/RubberBandStretcher.cpp::): + +0--"Reset component phases at the peak of each transient (the start of a significant note or +percussive event). This, the default setting, usually results in a clear-sounding output; but it is not always +consistent, and may cause interruptions in stable sounds +present at the same time as transient events. The +OptionDetector flags (below) can be used to tune this to some +extent." + +1--"Reset component phases at the peak of each transient, outside a frequency range typical of +musical fundamental frequencies. The results may be more regular for mixed stable and percussive +notes than [option 0], but with a "phasier" sound. The balance may sound very good for certain +types of music and fairly bad for others." + +2--"Do not reset component phases at any point. The results will be smoother and more regular +but may be less clear than with either of the other transients flags." + +argument::detectorMode +There are three transient detection modes, numbered 0, 1, and 2. +Explanations of what these modes do are quoted directly +from the source code comments in the RubberBand library: + +0--"Use a general-purpose transient detector which is likely to be +good for most situations. This is the default." + +1--"Detect percussive transients. Note that this was the default and only option +in Rubber Band versions prior to 1.5." + +2--"Use an onset detector with less of a bias toward percussive transients. +This may give better results with certain material (e.g. relatively monophonic piano music)." + +argument::phaseMode +There are two phase adjustment settings, numbered 0 and 1. +Explanations of what these modes do are quoted directly +from the source code comments in the RubberBand library: + +0--"Adjust phases when stretching in such a way as to try to retain the continuity of phase +relationships between adjacent frequency bins whose phases +are behaving in similar ways. This, the default setting, +should give good results in most situations." + +1--"Adjust the phase in each +frequency bin independently from its neighbours. This +usually results in a slightly softer, phasier sound." + +argument::pitchQuality +There are three pitch quality modes, numbered 0, 1, and 2. +Explanations of what these modes do are quoted directly +from the source code comments in the RubberBand library: + +0--"Favour CPU cost over sound +quality. This is the default. Use this when time-stretching +only, or for fixed pitch shifts where CPU usage is of +concern. Do not use this for arbitrarily time-varying pitch +shifts (see [option 2] below)." + +1--"Favour sound quality over CPU +cost. Use this for fixed pitch shifts where sound quality is +of most concern. Do not use this for arbitrarily time-varying +pitch shifts (see [option 2] below)." + +2--"Use a method that +supports dynamic pitch changes without discontinuities, +including when crossing the 1.0 pitch scale. This may cost +more in CPU than the default, especially when the pitch scale +is exactly 1.0. You should use this option whenever you wish +to support dynamically changing pitch shift during +processing." + +argument::windowOption +This option can only be set once, on construction. There are three +window size options, numbered 0, 1, and 2. +Explanations of what these modes do are quoted directly +from the source code comments in the RubberBand library: + +0--"Use the default window size. +The actual size will vary depending on other parameters. +This option is expected to produce better results than the +other window options in most situations. In the R3 engine +this causes the engine's full multi-resolution processing +scheme to be used." + +1--"Use a shorter window. This has +different effects with R2 and R3 engines. +With the R2 engine it may result in crisper sound for audio +that depends strongly on its timing qualities, but is likely +to sound worse in other ways and will have similar +efficiency. +With the R3 engine, it causes the engine to be restricted to +a single window size, resulting in both dramatically faster +processing and lower delay than OptionWindowStandard, but at +the expense of some sound quality. It may still sound better +for non-percussive material than the R2 engine. +With both engines it reduces the start delay somewhat...." + +2--"Use a longer window. With the R2 +engine this is likely to result in a smoother sound at the +expense of clarity and timing. The R3 engine currently +ignores this option, treating it like [option 0]." + +argument::smoothing +This option can only be set once, on construction. +There are two smoothing modes, numbered 0 and 1. +Explanations of what these modes do are quoted directly +from the source code comments in the RubberBand library: + +0--"Do not use time-domain smoothing. This is the default." + +1--"Use time-domain smoothing. This +will result in a softer sound with some audible artifacts +around sharp transients, but it may be appropriate for longer +stretches of some instruments and can mix well with +[windowOption 1]." + +argument::engine +This option can only be set once, on construction. +There are two stretching engine options, numbered 0 and 1. +Explanations of what these modes do are quoted directly +from the source code comments in the RubberBand library: + +0--"Use the Rubber Band Library R2 (Faster) engine. +This is the engine implemented in Rubber Band Library v1.x and v2.x, +and it remains the default in newer versions. It uses substantially +less CPU than the R3 engine and there are still many situations +in which it is likely to be the more appropriate choice." + +1--"Use the Rubber Band Library R3 +(Finer) engine. This engine was introduced in Rubber Band +Library v3.0. It produces higher-quality results than the R2 +engine for most material, especially complex mixes, vocals +and other sounds that have soft onsets and smooth pitch +changes, and music with substantial bass content. However, it +uses much more CPU power than the R2 engine." + +argument::doneAction +The action to take when the unit is done (if the buffer is full and looping is off) + +Examples:: + +code:: +b = Buffer.read(s, ExampleFiles.child); +c = Buffer.alloc(s, 20 * s.sampleRate); +( +{ + var sig = PlayBuf.ar(1, b, BufRateScale.ir(b)); + RubberBandStretcherBuf.ar(sig, c, loop: 0, timeRatio: 8.0, doneAction: 2); +}.play; +) +{PlayBuf.ar(1, c)}.play; +:: \ No newline at end of file diff --git a/src/rubberband/rubberBandStretcherBuf.cpp b/src/rubberband/rubberBandStretcherBuf.cpp index 60ae884..897c28f 100644 --- a/src/rubberband/rubberBandStretcherBuf.cpp +++ b/src/rubberband/rubberBandStretcherBuf.cpp @@ -23,22 +23,30 @@ along with this program. If not, see . */ #include "rubberBandStretcherBuf.hpp" +#include #include #include "SC_PlugIn.h" extern InterfaceTable *ft; void RubberBandStretcherBuf_Ctor(RubberBandStretcherBuf *unit) { - float timeRatio = IN0(1); - float pitchRatio = IN0(2); - float formantRatio = IN0(3); - int transientsMode = static_cast(IN0(4)); - int detector = static_cast(IN0(5)); - int phaseOption = static_cast(IN0(6)); - int pitchQuality = static_cast(IN0(7)); - int windowOption = static_cast(IN0(8)); - int smoothing = static_cast(IN0(9)); - int engine = static_cast(IN0(10)); + + /* arg in, bufnum=0, offset=0.0, recLevel=1.0, preLevel=0.0, run=1.0, loop=1.0, trigger=1.0, + 8 timeRatio=1.0, pitchRatio=1.0, formantRatio=0.0, transientsMode=0, + 12 detectorMode=0, phaseMode=0, pitchQuality=0, windowOption=0, + 16 smoothing=0, engine=0, doneAction=0; + */ + + float timeRatio = IN0(8); + float pitchRatio = IN0(9); + float formantRatio = IN0(10); + int transientsMode = static_cast(IN0(11)); + int detector = static_cast(IN0(12)); + int phaseOption = static_cast(IN0(13)); + int pitchQuality = static_cast(IN0(14)); + int windowOption = static_cast(IN0(15)); + int smoothing = static_cast(IN0(16)); + int engine = static_cast(IN0(17)); unit->m_timeRatio = timeRatio; unit->m_pitchRatio = pitchRatio; @@ -54,7 +62,8 @@ void RubberBandStretcherBuf_Ctor(RubberBandStretcherBuf *unit) { if (bufnum >= unit->mWorld->mNumSndBufs) bufnum = 0; unit->m_fbufnum = fbufnum; unit->m_buf = unit->mWorld->mSndBufs + bufnum; - unit->m_writePtr = 0; + unit->m_writePtr = static_cast(IN0(2)); // initial offset + unit->m_prevTrigger = 0.f; // Set up RubberBandStretcher initial options int options = 0x01000001; // formant-preserving, real-time options set @@ -113,7 +122,7 @@ void RubberBandStretcherBuf_Ctor(RubberBandStretcherBuf *unit) { // The shifter accepts a block size (which must be set before the first process() // call and not after), which avoids the need to use local RingBuffers. unit->m_stretcher->setMaxProcessSize(BUFLENGTH); - unit->m_stretcher->setTimeRatio(sc_clip(timeRatio, 1.f, std::numeric_limits::infinity())); + unit->m_stretcher->setTimeRatio(sc_clip(timeRatio, 1e-5, 1e5)); unit->m_stretcher->setPitchScale(sc_clip(pitchRatio, 1e-2, 64)); unit->m_stretcher->setFormantScale(sc_clip(formantRatio, 1e-2, 64)); @@ -121,9 +130,9 @@ void RubberBandStretcherBuf_Ctor(RubberBandStretcherBuf *unit) { // This is necessary because the shifter isn't ready to produce valid output // as soon as it is initialized--it requires padded 0s to be fed in for some // number of samples specified by the shifter. - float *zeroBuf = (float*)RTAlloc(unit->mWorld, BUFLENGTH * sizeof(float)); + unit->m_localBuf = (float*)RTAlloc(unit->mWorld, BUFLENGTH * sizeof(float)); for (size_t i = 0; i < BUFLENGTH; i++) { - zeroBuf[i] = 0.f; + unit->m_localBuf[i] = 0.f; } // The number of initial zeros required @@ -135,10 +144,9 @@ void RubberBandStretcherBuf_Ctor(RubberBandStretcherBuf *unit) { // Feed in the start pad samples while (startPad > 0) { - unit->m_stretcher->process(&zeroBuf, BUFLENGTH, false); + unit->m_stretcher->process(&unit->m_localBuf, BUFLENGTH, false); startPad -= BUFLENGTH; } - RTFree(unit->mWorld, zeroBuf); // Initialize first out sample OUT0(0) = 0; @@ -147,9 +155,143 @@ void RubberBandStretcherBuf_Ctor(RubberBandStretcherBuf *unit) { } void RubberBandStretcherBuf_Dtor(RubberBandStretcherBuf *unit) { - RTFree(unit->mWorld, unit->m_stretcher); + if (unit->m_stretcher) RTFree(unit->mWorld, unit->m_stretcher); + if (unit->m_localBuf) RTFree(unit->mWorld, unit->m_localBuf); } void RubberBandStretcherBuf_next(RubberBandStretcherBuf *unit, int inNumSamples) { + /* arg in, bufnum=0, offset=0.0, recLevel=1.0, preLevel=0.0, run=1.0, loop=1.0, trigger=1.0, + 8 timeRatio=1.0, pitchRatio=1.0, formantRatio=0.0, transientsMode=0, + 12 detectorMode=0, phaseMode=0, pitchQuality=0, windowOption=0, + 16 smoothing=0, engine=0, doneAction=0; + */ + + // Step 1: acquire the sound buffer + const SndBuf *writeBuf = unit->m_buf; + if (!writeBuf) { + std::cout << "WARNING: The stftBuffer could not be accessed. Aborting.\n"; + ClearUnitOutputs(unit, inNumSamples); + return; + } + ACQUIRE_SNDBUF_SHARED(writeBuf); + float* bufData __attribute__((__unused__)) = writeBuf->data; + const uint32 bufChannels __attribute__((__unused__)) = writeBuf->channels; + const uint32 bufSamples __attribute__((__unused__)) = writeBuf->samples; + const uint32 bufFrames = writeBuf->frames; + + if (bufChannels != 1) { + std::cout << "WARNING: The buffer has " << bufChannels << " channels, but the " << + "RubberBandStretcherBuf only supports mono buffers. Aborting.\n"; + ClearUnitOutputs(unit, inNumSamples); + RELEASE_SNDBUF_SHARED(writeBuf); + return; + } + + // 2. Update unit parameters if required + float timeRatio = IN0(8); + float pitchRatio = IN0(9); + float formantRatio = IN0(10); + int transientsMode = static_cast(IN0(11)); + int detector = static_cast(IN0(12)); + int phaseOption = static_cast(IN0(13)); + int pitchQuality = static_cast(IN0(14)); + + // Update shifter options only if something has changed + if (timeRatio != unit->m_timeRatio) { + unit->m_stretcher->setTimeRatio(sc_clip(timeRatio, 1e-5, 1e5)); + } + if (pitchRatio != unit->m_pitchRatio) { + unit->m_stretcher->setPitchScale(sc_clip(pitchRatio, 1e-2, 64)); + } + if (formantRatio != unit->m_formantRatio) { + unit->m_stretcher->setFormantScale(sc_clip(formantRatio, 1e-2, 64)); + } + // QUESTION: Will this method of setting options override all existing options, + // or just the option provided? May need to compute all options from scratch. + if (transientsMode != unit->m_transientsMode) { + switch (transientsMode) { + case 1: + unit->m_stretcher->setTransientsOption(0x00000100); + break; + case 2: + unit->m_stretcher->setTransientsOption(0x00000200); + break; + default: + unit->m_stretcher->setTransientsOption(0x00000000); + } + } + if (detector != unit->m_detectorOption) { + switch (detector) { + case 1: + unit->m_stretcher->setDetectorOption(0x00000400); + break; + case 2: + unit->m_stretcher->setDetectorOption(0x00000800); + break; + default: + unit->m_stretcher->setDetectorOption(0x00000000); + } + } + if (phaseOption != unit->m_phaseOption) { + switch (phaseOption) { + case 1: + unit->m_stretcher->setPhaseOption(0x00002000); + break; + default: + unit->m_stretcher->setPhaseOption(0x00000000); + } + } + if (pitchQuality != unit->m_pitchQuality) { + switch (pitchQuality) { + case 1: + unit->m_stretcher->setPitchOption(0x02000000); + break; + case 2: + unit->m_stretcher->setPitchOption(0x04000000); + break; + default: + unit->m_stretcher->setPitchOption(0x00000000); + } + } + + // 3. Handle the trigger functionality + float trigger = IN0(7); + if (trigger > 0.f && unit->m_prevTrigger <= 0.f) { + unit->m_writePtr = 0; + } + unit->m_prevTrigger = trigger; + + // 4. Process input audio. + float *in = IN(0); + float recLevel = IN0(3); + float preLevel = IN0(4); + float loop = IN0(6); + + unit->m_stretcher->process(&in, BUFLENGTH, false); + + while (unit->m_stretcher->available() > 0) { + size_t numRetrieve = static_cast(std::min(BUFLENGTH, unit->m_stretcher->available())); + unit->m_stretcher->retrieve(&unit->m_localBuf, numRetrieve); + // 5. While we run the audio through the stretcher regardless, we only store it to the buffer if "run" is set. + if (IN0(5) > 0.f) { + for (size_t xxi = 0; xxi < numRetrieve; xxi++) { + if (unit->m_writePtr >= bufSamples) { + if (loop > 0.f) { + unit->m_writePtr = 0; + } else { + RELEASE_SNDBUF_SHARED(writeBuf); + ClearUnitOutputs(unit, inNumSamples); + DoneAction(static_cast(IN0(18)), unit); + return; + } + } + + bufData[unit->m_writePtr] = preLevel * bufData[unit->m_writePtr] + recLevel * unit->m_localBuf[xxi]; + unit->m_writePtr++; + } + } + } + ClearUnitOutputs(unit, inNumSamples); + RELEASE_SNDBUF_SHARED(writeBuf); } \ No newline at end of file diff --git a/src/rubberband/rubberBandStretcherBuf.hpp b/src/rubberband/rubberBandStretcherBuf.hpp index 512e2b4..6148b34 100644 --- a/src/rubberband/rubberBandStretcherBuf.hpp +++ b/src/rubberband/rubberBandStretcherBuf.hpp @@ -31,6 +31,9 @@ struct RubberBandStretcherBuf : public Unit { /// The stretcher RubberBand::RubberBandStretcher* m_stretcher; + /// A buffer + float *m_localBuf; + /// The number of initial output samples to discard size_t m_samplesToDiscard; @@ -51,6 +54,9 @@ struct RubberBandStretcherBuf : public Unit { /// The next sample to write to size_t m_writePtr; + + /// The previous trigger + float m_prevTrigger; }; void RubberBandStretcherBuf_Ctor(RubberBandStretcherBuf *unit); diff --git a/src/rubberband/rubberband.cpp b/src/rubberband/rubberband.cpp index 036dab1..51fc551 100644 --- a/src/rubberband/rubberband.cpp +++ b/src/rubberband/rubberband.cpp @@ -25,6 +25,7 @@ along with this program. If not, see . #include "SC_PlugIn.h" #include "rubberBandPS.hpp" #include "rubberBandStretcher.hpp" +#include "rubberBandStretcherBuf.hpp" InterfaceTable *ft; @@ -32,4 +33,5 @@ PluginLoad(RubberBandPlugins) { ft = inTable; DefineDtorUnit(RubberBandPS); DefineDtorUnit(RubberBandStretcher); + DefineDtorUnit(RubberBandStretcherBuf); } From 36d63791d2f469756ed759b3777a2b52878ce73f Mon Sep 17 00:00:00 2001 From: Jeff Martin Date: Sun, 21 Jun 2026 16:41:59 -0500 Subject: [PATCH 24/25] fixed rubberband stretcher buf supernova integration --- README.md | 3 +++ src/rubberband/CMakeLists.txt | 2 +- src/rubberband/rubberBandStretcherBuf.cpp | 1 - 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6c1fd79..45c662c 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,9 @@ A formant-preserving phase vocoder pitch shifter using the [RubberBand library]( ### RubberBandStretcher A phase vocoder pitch shifter and time stretcher using the [RubberBand library](https://breakfastquay.com/rubberband/). +### RubberBandStretcherBuf +A phase vocoder pitch shifter and time stretcher using the [RubberBand library](https://breakfastquay.com/rubberband/). This version writes the stretched audio to a buffer rather than outputting it directly. + ### ImpulseDropout A modified version of Impulse that randomly drops a percentage of impulses, producing a stuttering effect. diff --git a/src/rubberband/CMakeLists.txt b/src/rubberband/CMakeLists.txt index c881d5c..25c0fca 100644 --- a/src/rubberband/CMakeLists.txt +++ b/src/rubberband/CMakeLists.txt @@ -35,9 +35,9 @@ target_link_libraries(rubberBandStretcherBuf PRIVATE rubberband_lib) if(SUPERNOVA) add_library(rubberband_supernova MODULE rubberband.cpp) target_link_libraries(rubberband_supernova PRIVATE - rubberband_lib rubberBandPS rubberBandStretcher + rubberBandStretcherBuf ) set_property(TARGET rubberband_supernova PROPERTY COMPILE_DEFINITIONS SUPERNOVA) diff --git a/src/rubberband/rubberBandStretcherBuf.cpp b/src/rubberband/rubberBandStretcherBuf.cpp index 897c28f..976d10c 100644 --- a/src/rubberband/rubberBandStretcherBuf.cpp +++ b/src/rubberband/rubberBandStretcherBuf.cpp @@ -24,7 +24,6 @@ along with this program. If not, see . #include "rubberBandStretcherBuf.hpp" #include -#include #include "SC_PlugIn.h" extern InterfaceTable *ft; From d9e843e3c8d9b766af9ea13a82f14e5fd8e7c8c6 Mon Sep 17 00:00:00 2001 From: Jeff Martin Date: Mon, 22 Jun 2026 19:25:46 -0500 Subject: [PATCH 25/25] updated puckette phase computation --- src/pv/pvStretch.cpp | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/src/pv/pvStretch.cpp b/src/pv/pvStretch.cpp index 66db7be..cc4b71c 100644 --- a/src/pv/pvStretch.cpp +++ b/src/pv/pvStretch.cpp @@ -286,10 +286,10 @@ void PV_PlayBufStretch_next(PV_PlayBufStretch *unit, int inNumSamples) { case 1: Stretch3Puckette( unit->m_frameNext, - unit->m_framePrev1, + unit->m_framePrev1, unit->m_framePrev2, p, - unit->m_outFramePrev, + unit->m_outFramePrev, newPos-static_cast(lo), stftBufFftSize, stftBufHopSize @@ -400,15 +400,9 @@ void Stretch2Puckette( // in order to "lock" phases of adjacent bins together. float prevPhase = 0.0; if (xxk == 0) { - std::complex prevBinKMinus1 = std::polar(outFramePrev->dc, 0.f); - std::complex prevBinK = std::polar(outFramePrev->bin[xxk].mag, outFramePrev->bin[xxk].phase); - std::complex prevBinKPlus1 = std::polar(outFramePrev->bin[xxk+1].mag, outFramePrev->bin[xxk+1].phase); - prevPhase = std::arg(prevBinK - prevBinKMinus1 - prevBinKPlus1); + prevPhase = outFramePrev->bin[xxk].phase; } else if (xxk == fftSize/2-2) { - std::complex prevBinKMinus1 = std::polar(outFramePrev->bin[xxk-1].mag, outFramePrev->bin[xxk-1].phase); - std::complex prevBinK = std::polar(outFramePrev->bin[xxk].mag, outFramePrev->bin[xxk].phase); - std::complex prevBinKPlus1 = std::polar(outFramePrev->nyq, 0.f); - prevPhase = std::arg(prevBinK - prevBinKMinus1 - prevBinKPlus1); + prevPhase = outFramePrev->bin[xxk].phase; } else { std::complex prevBinKMinus1 = std::polar(outFramePrev->bin[xxk-1].mag, outFramePrev->bin[xxk-1].phase); std::complex prevBinK = std::polar(outFramePrev->bin[xxk].mag, outFramePrev->bin[xxk].phase); @@ -614,15 +608,9 @@ void Stretch3Puckette( // in order to "lock" phases of adjacent bins together. float prevPhase = 0.0; if (xxk == 0) { - std::complex prevBinKMinus1 = std::polar(outFramePrev->dc, 0.f); - std::complex prevBinK = std::polar(outFramePrev->bin[xxk].mag, outFramePrev->bin[xxk].phase); - std::complex prevBinKPlus1 = std::polar(outFramePrev->bin[xxk+1].mag, outFramePrev->bin[xxk+1].phase); - prevPhase = std::arg(prevBinK - prevBinKMinus1 - prevBinKPlus1); + prevPhase = outFramePrev->bin[xxk].phase; } else if (xxk == fftSize/2-2) { - std::complex prevBinKMinus1 = std::polar(outFramePrev->bin[xxk-1].mag, outFramePrev->bin[xxk-1].phase); - std::complex prevBinK = std::polar(outFramePrev->bin[xxk].mag, outFramePrev->bin[xxk].phase); - std::complex prevBinKPlus1 = std::polar(outFramePrev->nyq, 0.f); - prevPhase = std::arg(prevBinK - prevBinKMinus1 - prevBinKPlus1); + prevPhase = outFramePrev->bin[xxk].phase; } else { std::complex prevBinKMinus1 = std::polar(outFramePrev->bin[xxk-1].mag, outFramePrev->bin[xxk-1].phase); std::complex prevBinK = std::polar(outFramePrev->bin[xxk].mag, outFramePrev->bin[xxk].phase);