This is awesome. How did you do the anti-derivative of the 259 function?
Sam
This is awesome. How did you do the anti-derivative of the 259 function?
Sam
There is a base ADAA Waveshaper class and implementations for some memoryless waveshapers in jatin chowdhurys library which i have rewritten
Thats the paper
The struct for the 1st order which is already working fine currently looks like this, adding 2x oversampling is already sufficient for really good results.
#pragma once
#include "SC_PlugIn.hpp"
#include "Utils.hpp"
#include <array>
namespace DistortionUtils {
struct BuchlaFoldADAA {
struct Cell {
double gain, bias, thresh, mix;
double Bp;
Cell(double g, double b, double t, double m)
: gain(g), bias(b), thresh(t), mix(m),
Bp(0.5 * g * t * t - b * t)
{}
inline double func(double x) const noexcept {
if (std::abs(x) > thresh) {
return gain * x - bias * sc_sign(x);
}
return 0.0;
}
inline double AD1(double x) const noexcept {
if (std::abs(x) > thresh) {
return 0.5 * gain * x * x - bias * x * sc_sign(x) - Bp;
}
return 0.0;
}
};
inline static const std::array<Cell, 5> CELLS{{
{0.8333, 0.5, 0.6, -12.0},
{0.3768, 1.1281, 2.994, -27.777},
{0.2829, 1.5446, 5.46, -21.428},
{0.5743, 1.0338, 1.8, 17.647},
{0.2673, 1.0907, 4.08, 36.363}
}};
double m_x1{0.0};
double m_ad1_x1{0.0};
static constexpr double TOL = 1e-2;
static constexpr double X_MIX = 5.0;
static constexpr double IN_GAIN = 0.6;
static constexpr double OUT_GAIN = 1.6666666666666667;
BuchlaFoldADAA() = default;
void reset() {
m_x1 = 0.0;
m_ad1_x1 = 0.0;
}
inline float process(float input, float drive) noexcept {
double x = input * (drive + 1.0) * IN_GAIN;
double y = processADAA(x);
return static_cast<float>((y / X_MIX) * OUT_GAIN);
}
private:
// F(x) = 5x + cells
inline double nlFunc(double x) const noexcept {
double y = X_MIX * x;
for (const auto& cell : CELLS) {
y += cell.mix * cell.func(x);
}
return y;
}
// First antiderivative: 2.5x² + cells_AD1
inline double nlFunc_AD1(double x) const noexcept {
double y = 0.5 * X_MIX * x * x;
for (const auto& cell : CELLS) {
y += cell.mix * cell.AD1(x);
}
return y;
}
inline double processADAA(double x) noexcept {
double delta = x - m_x1;
bool illCondition = std::abs(delta) < TOL;
double y;
if (illCondition) {
y = nlFunc(0.5 * (x + m_x1));
} else {
double ad1_x = nlFunc_AD1(x);
y = (ad1_x - m_ad1_x1) / delta;
}
// Update state
m_x1 = x;
m_ad1_x1 = nlFunc_AD1(x);
return y;
}
};
} // namespace DistortionUtils
Will add that to the library mostly to implement it in a phasor driven feedback osc design, but wanted to spent some more time with the papers first and implement the lookup tables to be more efficient. There is also a really recent DAFx 2025 paper https://www.dafx.de/paper-archive/2025/DAFx25_paper_30.pdf
wow, it’s hard to keep up with all these new additions… btw I’m curious what kind of music/noises you (or others) are actually making with this
I’ve got some sounds:
In this one I was experimenting with glissons in different scales. There’s three streams with the phasors driving wavetables, hence the harmonic sweep on the “droney” stream
And in this one I was experimenting with the different masking options available and thought I could make some cicadas and other chirps.
some experiments with wavefolders:
Here i have placed the BuchlaFoldADAA in a feedback path of a cross-coupled harmonic oscillator pair with XPM and 90° phase shift:
While investigating the K-Accumulator, i have figured out another way of wavefolding which is putting your input through a quantizer (project and shift in k-space and round) and subtracting the quantized signal from the input. Here with an harmonic oscillator pair with 90° phase shift.
Ive added BuchlaFoldADAA and RampDivider, merry xmas ![]()
RampDivider is implementing the rampdiv abstraction from the go book, similiar to the rate~ object.
RampDivider tracks the slope of an input ramp signal between 0 and 1, scales the derived slope by a ratio and integrates the scaled slope to output a subdivided ramp signal between 0 and 1.
When autosync is enabled, the derived ramp will automatically sync to the main ramp whenever the ratio changes significantly and the main ramp begins a new cycle. The sync operation snaps to the nearest valid grid point to minimize phase discontinuity.
Thanks so much for these!
Really convincing and expensive sounding in lack of a better word.
Thanks, I hope I can come up with some interesting phasor driven complex oscillator Designs which will fit into the Library in the Next year ![]()
&happy new year! thanks!
I have made a new update to the library
I have:
UnitUrn with a Demand version called Durn, which is better aligned with its indexing paradigm instead of outputting normalized values between 0 and 1SingleOscOs and DualOscOS) and made some further performance improvementsHere are some random presets from DualOscOS :
It seems to me that currently the libraries are not a good match. Took me some time to figure out why i got unexpected results haha. GrainUtils is all audio rate while BufFFT is all control rate.
good point - I still think it’s musically useful. Are there any FFT libs that are triggerable at audio rate?
Not that im aware of. You could use T2K to convert the audio rate triggers into control rate. But thats somehow missing the point of the library.
(
~maxGrains = 8;
~fftSize = 4096;
d = Buffer.read(s, Platform.resourceDir +/+ "sounds/a11wlk01.wav");
e = Array.fill(~maxGrains, {Buffer.alloc(s, ~fftSize)});
)
(
var getSubDivs = { |rate, randomness|
var subDiv = Ddup(2, (2 ** (Dwhite(-1.0, 1.0) * randomness))) / rate;
Duty.ar(subDiv, DC.ar(0), 1 / subDiv);
};
e.do{ |item| item.zero };
{
var numChannels = 8;
var subDiv, events, voices, grainWindows;
var triggers, pos, chain, sig;
subDiv = getSubDivs.(\tFreq.kr(40), \randomness.kr(1));
events = SchedulerCycle.ar(subDiv);
voices = VoiceAllocator.ar(
numChannels: numChannels,
trig: events[\trigger],
rate: events[\rate] / \overlap.kr(4),
subSampleOffset: events[\subSampleOffset],
);
grainWindows = HanningWindow.ar(
phase: voices[\phases],
skew: \windowSkew.kr(0.5)
);
triggers = T2K.kr(voices[\triggers]);
pos = TRand.kr(0, 1, triggers) * BufFrames.kr(d);
chain = BufFFTTrigger2(e, triggers);
chain = BufFFT_BufCopy(chain, d, pos, BufRateScale.kr(d));
chain = BufFFT(chain, -1);
chain = PV_BrickWall(chain, TExpRand.kr(0.0075,0.05, triggers));
chain = PV_BrickWall(chain, TExpRand.kr(-0.0075,-0.05, triggers));
sig = BufIFFT(chain, 1);
sig = sig * grainWindows;
Mix(Pan2.ar(sig, TRand.kr(-1, 1, triggers)));
}.play
)
I see you’re using 4096-sample windows. For audio rate triggering to be meaningful, you would need an inter-onset interval between FFT windows (grains) less than the control block size but still a power of two. Assuming a 64-sample control block, the next lower power of two is 32. So the overlap would have to be at least 128 – or the hop size, which is normally 0.5 or 0.25, would have to be 0.0078125.
… which I imagine is likely to be a pretty hefty drain on CPU.
That is, perhaps one reason why SC doesn’t trigger FFTs at audio rate is because it may be impractical. 44100/32 is over a thousand FFT frames per second. If they’re large windows, it’s a lot of calculation; if they’re short, then you lose frequency resolution.
Perhaps there’s an unconventional use lurking here, but before looking seriously into audio rate triggering, try a hop size such that it’s calculating every control block (e.g. 64/4096 = 1/64) and find out where the practical limits are. If it can’t handle one frame every 64 samples (670-750 FFT frames per second) then audio rate triggering would not be meaningful.
hjh
Im currently experimenting with sequencing single cycles from a wavetable for band-limited grain envelopes using SingleOscOS for pulsar synthesis, for example with Durn.
This might sound like:
You can create a wavetable of single cycles with a baked in trajectory using unit shaping:
(
ProtoDef(\UnitShapers) {
~init = { |self|
self.unitShapers = IdentityDictionary.new();
self.easingCores = IdentityDictionary.new();
self.easingTypes = IdentityDictionary.new();
self.interpFuncs = IdentityDictionary.new();
self.windowFuncs = IdentityDictionary.new();
self.getUnitShapers;
self.getEasingCores;
self.getEasingTypes;
self.getInterpFuncs;
self.getWindowFuncs;
};
~makeTable = { |self, func, paramsList, numSamples = 2048|
var sigs = paramsList.collect({ |params|
var sig = Signal.newClear(numSamples);
sig.waveFill({ |x|
func.(x, *params);
});
});
sigs.inject(Signal.newClear(0), { |acc, sig| acc.addAll(sig) });
};
~getUnitShapers = { |self|
var unitTriangle = { |x, skew|
var safeDenom = 1e-4;
var safeInvSkew = max(1.0 - skew, safeDenom);
if(x <= skew) {
x / max(skew, safeDenom);
} {
1.0 - ((x - skew) / safeInvSkew);
};
};
var unitKink = { |x, skew|
var safeDenom = 1e-4;
var safeInvSkew = max(1.0 - skew, safeDenom);
if(x <= skew) {
0.5 * (x / max(skew, safeDenom));
} {
0.5 * (1.0 + ((x - skew) / safeInvSkew));
};
};
var unitHanning = { |x|
1 - cos(x * pi) * 0.5;
};
var unitGaussian = { |x, index|
var cosine = cos(x * 0.5pi) * index;
exp(cosine * cosine.neg);
};
var unitTrapezoid = { |x, width, duty|
var safeDenom = 1e-4;
var sustain = 1.0 - width;
if(sustain < safeDenom) {
var offset = x - (1.0 - duty);
if(offset > 0.0) { 1.0 } { 0.0 };
} {
var offset = x - (1.0 - duty);
((offset / sustain) + (1.0 - duty)).clip(0.0, 1.0);
};
};
var unitTukey = { |x, width, duty = 1|
var trapezoid = unitTrapezoid.(x, width, duty);
unitHanning.(trapezoid);
};
self.unitShapers.put(\triangle, unitTriangle);
self.unitShapers.put(\kink, unitKink);
self.unitShapers.put(\hanning, unitHanning);
self.unitShapers.put(\gaussian, unitGaussian);
self.unitShapers.put(\trapezoid, unitTrapezoid);
self.unitShapers.put(\tukey, unitTukey);
};
~getEasingCores = { |self|
var easingCores = [
\cubic,
\quintic,
\sine,
\circular,
\pseudoExp
];
easingCores.do{ |key|
var easingCore = case
{ key == \cubic } {
{ |x| x * x * x };
}
{ key == \quintic } {
{ |x| x * x * x * x * x };
}
{ key == \sine } {
{ |x| 1 - cos(x * 0.5pi) };
}
{ key == \circular } {
{ |x| 1 - sqrt(1 - (x * x)) };
}
{ key == \pseudoExp } {
{ |x, coef = 13|
(2 ** (coef * x) - 1) / (2 ** coef - 1)
};
};
self.easingCores.put(key, easingCore);
};
};
~getEasingTypes = { |self|
var easeIn = { |x, core|
core.(x);
};
var easeOut = { |x, core|
1 - core.(1 - x);
};
// Sigmoid with variable offset
var easeInOut = { |x, inflection, core|
var safeDenom = 1e-4;
var safeOffset = max(inflection, safeDenom);
var safeInvOffset = max(1 - inflection, safeDenom);
if(x <= inflection) {
inflection * core.(x / safeOffset)
} {
inflection + ((1 - inflection) * (1 - core.((1 - x) / safeInvOffset)))
};
};
// Seat with variable height
var easeOutIn = { |x, height, core|
var safeDenom = 1e-4;
var safeHeight = max(height, safeDenom);
var safeInvHeight = max(1 - height, safeDenom);
if(x <= height) {
height * (1 - core.((height - x) / safeHeight))
} {
height + ((1 - height) * core.((x - height) / safeInvHeight))
};
};
self.easingTypes.put(\easeIn, easeIn);
self.easingTypes.put(\easeOut, easeOut);
self.easingTypes.put(\easeInOut, easeInOut);
self.easingTypes.put(\easeOutIn, easeOutIn);
};
~getInterpFuncs = { |self|
var interpFunc = { |x, interp|
case
{ interp == \step } { x }
{ interp == \smoothStep } { x * x * (3 - (2 * x)) }
{ interp == \smootherStep } { x * x * x * (x * (6 * x - 15) + 10) };
};
var jCurve = { |x, shape, core, interp = \smootherStep|
var coreFunc = self.easingCores[core];
if(shape <= 0.5) {
var mix = interpFunc.(shape * 2, interp);
blend(self.easingTypes[\easeOut].(x, coreFunc), x, mix);
} {
var mix = interpFunc.((shape - 0.5) * 2, interp);
blend(x, self.easingTypes[\easeIn].(x, coreFunc), mix);
};
};
var sCurve = { |x, shape, inflection, core, interp = \smootherStep|
var coreFunc = self.easingCores[core];
if(shape <= 0.5) {
var mix = interpFunc.(shape * 2, interp);
blend(self.easingTypes[\easeInOut].(x, inflection, coreFunc), x, mix);
} {
var mix = interpFunc.((shape - 0.5) * 2, interp);
blend(x, self.easingTypes[\easeOutIn].(x, inflection, coreFunc), mix);
};
};
self.interpFuncs.put(\jCurve, jCurve);
self.interpFuncs.put(\sCurve, sCurve);
};
~getWindowFuncs = { |self|
var hanningWindow = { |phase, skew|
var warpedPhase = self.unitShapers[\triangle].(phase, skew);
self.unitShapers[\hanning].(warpedPhase);
};
var gaussianWindow = { |phase, skew, index|
var warpedPhase = self.unitShapers[\triangle].(phase, skew);
var gaussian = self.unitShapers[\gaussian].(warpedPhase, index);
var hanning = self.unitShapers[\hanning].(warpedPhase);
gaussian * hanning;
};
var trapezoidalWindow = { |phase, skew, width, duty = 1|
var warpedPhase = self.unitShapers[\triangle].(phase, skew);
self.unitShapers[\trapezoid].(warpedPhase, width, duty);
};
var tukeyWindow = { |phase, skew, width, duty = 1|
var warpedPhase = self.unitShapers[\triangle].(phase, skew);
self.unitShapers[\tukey].(warpedPhase, width, duty);
};
var exponentialWindow = { |phase, skew, shape|
var warpedPhase = self.unitShapers[\triangle].(phase, skew);
self.interpFuncs[\jCurve].(warpedPhase, 1 - shape, \pseudoExp);
};
self.windowFuncs.put(\hanning, hanningWindow);
self.windowFuncs.put(\gaussian, gaussianWindow);
self.windowFuncs.put(\tukey, tukeyWindow);
self.windowFuncs.put(\trapezoid, trapezoidalWindow);
self.windowFuncs.put(\exponential, exponentialWindow);
};
};
)
If you have evaluated the ProtoDef, you can do something like this:
~unitShapers = Prototype(\UnitShapers);
(
var numCycles = 8;
var params = numCycles.collect({ |i|
[
i.linlin(0, numCycles - 1, 0.01, 0.99),
i.linlin(0, numCycles - 1, 0.00, 0.50)
]
});
~table = ~unitShapers.makeTable(~unitShapers.windowFuncs[\exponential], params);
)
~buffer = Buffer.loadCollection(s, ~table);
~buffer.plot;
or like this:
(
var numCycles = 5;
var params = numCycles.collect({ |i|
[
i.linlin(0, numCycles - 1, 0.1, 0.9),
0.0,
0.5
]
});
~table = ~unitShapers.makeTable(~unitShapers.windowFuncs[\tukey], params);
)
~buffer = Buffer.loadCollection(s, ~table);
~buffer.plot;
and then index into the tables with Durn like this:
(
var multiChannelDurn = { |triggers, reset, chance, size, repeatItem|
var demand = Ddup(repeatItem, Durn(chance, size, inf));
triggers.collect{ |localTrig|
Demand.ar(localTrig + reset, reset, demand)
};
};
{
var numChannels = 5;
var reset, tFreq;
var events, voices;
var tableIndex, sizeOfTable;
reset = Trig1.ar(\reset.tr(0), SampleDur.ir);
tFreq = \tFreq.kr(400);
events = SchedulerCycle.ar(tFreq, reset);
voices = VoiceAllocator.ar(
numChannels: numChannels,
trig: events[\trigger],
rate: events[\rate] / \overlap.kr(4),
subSampleOffset: events[\subSampleOffset],
);
// Create wavetable position modulation
tableIndex = multiChannelDurn.(
voices[\triggers],
reset,
\chance.kr(0),
\size.kr(8),
\repeat.kr(1)
);
// Create grain windows
sizeOfTable = BufFrames.kr(~buffer) / 2048;
SingleOscOS.ar(
bufnum: ~buffer,
phase: voices[\phases],
numCycles: sizeOfTable,
cyclePos: tableIndex / (sizeOfTable - 1),
oversample: 0
);
}.plot(0.041);
)
You can trigger FFT windowed calculations in audio-rate with the new version of DynGen, stay tuned for that ![]()