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);
+ }
+}