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/CMakeLists.txt b/CMakeLists.txt index c6a06f1..4bfc63a 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 @@ -124,13 +124,17 @@ 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 src/pv/PV_MagSqueeze.schelp + src/pv/PV_PlayBufStretch.schelp 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/README.md b/README.md index fec907c..45c662c 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,18 @@ 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. + +### 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 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/ImpulseDropout.schelp b/src/generators/ImpulseDropout.schelp new file mode 100644 index 0000000..109cb22 --- /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>FlexUGens, 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..e1ad49e --- /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>FlexUGens, 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.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 new file mode 100644 index 0000000..c21d879 --- /dev/null +++ b/src/generators/arrayheap.hpp @@ -0,0 +1,42 @@ +/* +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); + +/// 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 +int heapPeek(IntMinHeap* heap); \ No newline at end of file diff --git a/src/generators/generators.cpp b/src/generators/generators.cpp index 9583344..90cdf2a 100644 --- a/src/generators/generators.cpp +++ b/src/generators/generators.cpp @@ -23,229 +23,15 @@ along with this program. If not, see . */ #include "SC_PlugIn.h" +#include "loopPhasor.hpp" +#include "impulseDropout.hpp" +#include "impulseJitter.hpp" -static InterfaceTable *ft; +InterfaceTable *ft; -// 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; -} - -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. 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/generators/impulseDropout.hpp b/src/generators/impulseDropout.hpp new file mode 100644 index 0000000..eb79286 --- /dev/null +++ b/src/generators/impulseDropout.hpp @@ -0,0 +1,40 @@ +/* +File: impulseDropout.hpp +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 . +*/ + +#pragma once +#include "SC_PlugIn.h" +#define HEAP_MAX_SIZE 1024 + +// 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/CMakeLists.txt b/src/pv/CMakeLists.txt index eebeee9..74de74a 100644 --- a/src/pv/CMakeLists.txt +++ b/src/pv/CMakeLists.txt @@ -1,8 +1,16 @@ # 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) +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) 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_PlayBufStretch.schelp b/src/pv/PV_PlayBufStretch.schelp new file mode 100644 index 0000000..6a7bb2b --- /dev/null +++ b/src/pv/PV_PlayBufStretch.schelp @@ -0,0 +1,126 @@ +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. 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:: + +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 produced by changing the +start position during playback. + +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. + +argument::loop +Whether (1.0) or not (0.0) to loop when the end of the stftBuffer is reached + +argument::phaseLock +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::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. 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 + +Examples:: + +code:: +( +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, 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, 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)); +}).add; +) + +Synth(\rec, [\recBuf, y, \soundBuf, z]); +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.cpp b/src/pv/pv.cpp index a7295a7..ba9adb3 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); + DefineDtorUnit(PV_PlayBufStretch); DefineDtorUnit(PV_CFreeze); } diff --git a/src/pv/pv.sc b/src/pv/pv.sc index b95376a..b584a81 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=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/pvCFreeze.cpp b/src/pv/pvCFreeze.cpp new file mode 100644 index 0000000..67700a1 --- /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 "FFT_UGens.h" + +extern InterfaceTable *ft; + +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..9d76180 --- /dev/null +++ b/src/pv/pvCFreeze.hpp @@ -0,0 +1,41 @@ +/* +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..55f1efa --- /dev/null +++ b/src/pv/pvOther.cpp @@ -0,0 +1,112 @@ +/* +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" + +extern InterfaceTable *ft; + +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..cc4b71c --- /dev/null +++ b/src/pv/pvStretch.cpp @@ -0,0 +1,912 @@ +/* +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 +#include +#define INTERP(a, b, pos) (a + (b-a) * pos) + +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 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; + 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), peakRadius); + unit->m_peakFinder->memLoad(RTAlloc(unit->mWorld, unit->m_peakFinder->memSize())); + + // 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_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); + } + 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 + + // 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 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; + 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 + 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) { + 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 + 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. + + 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; + } + + // 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; + } + + float startPos = sc_clip(IN0(2), 0.0, 1.0); + const float rate = IN0(3); + const float loop = IN0(4); + + 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 + rate; + if (newPos > stftFrames - 1) { + 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(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; + } + } + + // 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 firstFrameIdx = static_cast(std::round(startPos * (stftFrames-1))); + + // Copy the FFT data over + SCPolarBuf *p = ToPolarApx(buf); + const float *currentFftFrame = stftData + (firstFrameIdx * stftBufFftSize); + // Fill the output buffer + fillPolarBuf(currentFftFrame, p, stftBufFftSize); + + // 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. + unit->m_pos = static_cast(firstFrameIdx + 1); + unit->m_firstFrame = false; + } + + // For frames other than the first frame, we'll need to perform phase computation. + else { + size_t phaseLock = sc_clip(static_cast(IN0(5)), 0, 2); + SCPolarBuf *p = ToPolarApx(buf); + size_t roundedPos = static_cast(std::round(newPos)); + + // 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) { + 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 + switch (phaseLock) { + case 1: + Stretch2Puckette( + unit->m_frameNext, + unit->m_framePrev1, + p, + unit->m_outFramePrev, + stftBufFftSize, + stftBufHopSize + ); + break; + case 2: + Stretch2LarocheDolson( + unit->m_frameNext, + unit->m_framePrev1, + p, + unit->m_outFramePrev, + unit->m_peakFinder, + 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. + 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 + (loprev * stftBufFftSize), unit->m_framePrev2, stftBufFftSize); + // Render the output FFT frame + 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; + case 2: + 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, + 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 + // phase vocoder calculations. + 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 +void Stretch2( + 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; + + // 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) { + prevPhase = outFramePrev->bin[xxk].phase; + } else if (xxk == fftSize/2-2) { + 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); + 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; + } +} + +/// 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 +/// 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 +void Stretch3( + 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); + + // 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) { + prevPhase = outFramePrev->bin[xxk].phase; + } else if (xxk == fftSize/2-2) { + 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); + 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; + } +} + +/// 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 + peakFinder->analyze(cframe); + + 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); + + 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 < 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); + + // 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 +/// \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; + } +} + + +Peak::Peak(size_t peak) : peak(peak) {} +Peak::Peak(size_t peak, size_t leftValley, size_t rightValley) : peak(peak), leftValley(leftValley), rightValley(rightValley) {} + +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 new file mode 100644 index 0000000..fe70e1a --- /dev/null +++ b/src/pv/pvStretch.hpp @@ -0,0 +1,277 @@ +/* +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" +#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; +}; + +/// Stores the state of a PV_PlayBufStretch UGen instance +struct PV_PlayBufStretch : public Unit { + /// The index of the buffer with STFT data + float m_fbufnum; + + /// The buffer with STFT data + SndBuf *m_buf; + + /// The most recent output STFT frame + SCPolarBuf *m_outFramePrev; + + /// The next STFT frame after the current position + SCPolarBuf *m_frameNext; + + /// The STFT frame right before the current position + SCPolarBuf *m_framePrev1; + + /// 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. + 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; +}; + +/// 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 +void Stretch2( + 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 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, +/// 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 +/// 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 +void Stretch3( + const SCPolarBuf *frameNext, + const SCPolarBuf *framePrev1, + const SCPolarBuf *framePrev2, + SCPolarBuf *outFrame, + const SCPolarBuf *outFramePrev, + float pos, + 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 +/// 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); + +/// 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 +/// \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 diff --git a/src/rubberband/CMakeLists.txt b/src/rubberband/CMakeLists.txt index 7a4e042..25c0fca 100644 --- a/src/rubberband/CMakeLists.txt +++ b/src/rubberband/CMakeLists.txt @@ -16,13 +16,29 @@ 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) +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) - target_link_libraries(rubberband_supernova PRIVATE rubberband_lib) - set_target_properties(rubberband_lib PROPERTIES POSITION_INDEPENDENT_CODE ON) + target_link_libraries(rubberband_supernova PRIVATE + rubberBandPS + rubberBandStretcher + rubberBandStretcherBuf + ) set_property(TARGET rubberband_supernova PROPERTY COMPILE_DEFINITIONS SUPERNOVA) endif() 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/rubberBandPS.cpp b/src/rubberband/rubberBandPS.cpp new file mode 100644 index 0000000..1c9b23b --- /dev/null +++ b/src/rubberband/rubberBandPS.cpp @@ -0,0 +1,118 @@ +/* +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" + +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..82024ac --- /dev/null +++ b/src/rubberband/rubberBandPS.hpp @@ -0,0 +1,42 @@ +/* +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" +#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..c9295dd --- /dev/null +++ b/src/rubberband/rubberBandStretcher.cpp @@ -0,0 +1,236 @@ +/* +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" + +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..31b3302 --- /dev/null +++ b/src/rubberband/rubberBandStretcher.hpp @@ -0,0 +1,48 @@ +/* +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; + 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/rubberBandStretcherBuf.cpp b/src/rubberband/rubberBandStretcherBuf.cpp new file mode 100644 index 0000000..976d10c --- /dev/null +++ b/src/rubberband/rubberBandStretcherBuf.cpp @@ -0,0 +1,296 @@ +/* +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) { + + /* 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; + 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 = 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 + 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, 1e-5, 1e5)); + 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. + unit->m_localBuf = (float*)RTAlloc(unit->mWorld, BUFLENGTH * sizeof(float)); + for (size_t i = 0; i < BUFLENGTH; i++) { + unit->m_localBuf[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(&unit->m_localBuf, BUFLENGTH, false); + startPad -= BUFLENGTH; + } + + // Initialize first out sample + OUT0(0) = 0; + + SETCALC(RubberBandStretcherBuf_next); +} + +void RubberBandStretcherBuf_Dtor(RubberBandStretcherBuf *unit) { + 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 new file mode 100644 index 0000000..6148b34 --- /dev/null +++ b/src/rubberband/rubberBandStretcherBuf.hpp @@ -0,0 +1,64 @@ +/* +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; + + /// A buffer + float *m_localBuf; + + /// 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; + + /// The previous trigger + float m_prevTrigger; +}; + +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.cpp b/src/rubberband/rubberband.cpp index 5f7924d..51fc551 100644 --- a/src/rubberband/rubberband.cpp +++ b/src/rubberband/rubberband.cpp @@ -22,337 +22,16 @@ 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" +#include "rubberBandStretcherBuf.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); + DefineDtorUnit(RubberBandStretcherBuf); } diff --git a/src/rubberband/rubberband.sc b/src/rubberband/rubberband.sc index 13f43eb..fa43783 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, preLevel, run, loop, trigger, + timeRatio, pitchRatio, formantRatio, transientsMode, + detectorMode, phaseMode, pitchQuality, windowOption, + smoothing, engine, doneAction); + } +}