hey, here is a collection of unit shapers (they are still work in progress, i will add some other functions in the upcoming days). All the params used for these functions are nicely normalized between 0 and 1 and can be modulated at audio rate. To use them you need a ramp signal between 0 and 1 to drive them and the ProtoDef class by @elgiano GitHub - elgiano/ProtoDef: Prototyping classes for SuperCollider.
Maybe there could be a better way to organize those in a dictionary, but im mainly using this ProtoDef class for event prototyping and im focused on dsp rather then on these programming aspects, so if somebody knows a way to organize them which will be more accesible for others without that class let me know.
Some of these functions are straight forward and you can directly see their use case while others might not be as accessible on first sight. But I willl add more examples in the upcoming days, where you can see a possible use case.
update: added cubic seat and elliptic seat to linear morph and adjusted a few other things
update 2: implemented a general solution for the easing functions (also renamed some functions easeInOut is sigmoid while easeOutIn is seat). Now you could add additional easing cores. I have dervied the tukey window from the trapezoidal window with an additional duty param now (which shows that you can use any shaping function for the taper)
update 3: added a height param for easeInOut
update 4: implemented the linear interpolation of easing functions into the helping functions
update 5: added pseudoExponential easing (similiar to octic), adjusted a few variable names and descriptions
update 6: added OnePole filters
update 7: updated the easeInOut
/ easeOutIn
functions with adjustable offset
/ height
and included step
, smoothStep
and smootherStep
for interpolation
(
ProtoDef(\unitShapers) {
~init = { |self|
self.helperFunctions = IdentityDictionary.new();
self.unitShapers = IdentityDictionary.new();
self.onePoleFilters = IdentityDictionary.new();
self.waveShapers = IdentityDictionary.new();
self.easingFunctions = IdentityDictionary.new();
self.interpFunctions = IdentityDictionary.new();
self.windowFunctions = IdentityDictionary.new();
self.getHelperFunctions;
self.getUnitShapers;
self.getOnePoleFilters;
self.getWaveShapers;
self.getEasingFunctions;
self.getInterpFunctions;
self.getWindowFunctions;
};
~getHelperFunctions = { |self|
// transfer functions
var triangle = { |phase, skew|
var warpedPhase = Select.ar(BinaryOpUGen('>', phase, skew), [
phase / skew,
1 - ((phase - skew) / (1 - skew))
]);
Select.ar(BinaryOpUGen('==', skew, 0), [warpedPhase, 1 - phase]);
};
var kink = { |phase, skew|
var warpedPhase = Select.ar(BinaryOpUGen('>', phase, skew), [
0.5 * (phase / skew),
0.5 * (1 + ((phase - skew) / (1 - skew)))
]);
Select.ar(BinaryOpUGen('==', skew, 0), [warpedPhase, 0.5 * (1 + phase)]);
};
// interpolation of easing functions
var interpEasing = { |x, shape, easingFuncA, easingFuncB, interp = \step|
var interpFunc = case
{ interp == \step } {
var step = { |x|
x;
};
step;
}
{ interp == \smoothStep } {
var smoothStep = { |x|
x * x * (3 - (2 * x));
};
smoothStep;
}
{ interp == \smootherStep } {
var smootherStep = { |x|
x * x * x * (x * (6 * x - 15) + 10);
};
smootherStep;
};
var easingToLinear = { |x, shape, easingFunc, interpFunc|
var mix = shape * 2;
var mixInterp = interpFunc.(mix);
easingFunc * (1 - mixInterp) + (x * mixInterp);
};
var linearToEasing = { |x, shape, easingFunc, interpFunc|
var mix = (shape - 0.5) * 2;
var mixInterp = interpFunc.(mix);
x * (1 - mixInterp) + (easingFunc * mixInterp);
};
Select.ar(BinaryOpUGen('>', shape, 0.5), [
easingToLinear.(x, shape, easingFuncA, interpFunc),
linearToEasing.(x, shape, easingFuncB, interpFunc)
]);
};
// scale modulation depth of modulators between 0 and 1
var modScale = { |modulator, value, amount, mode = \bipolar, direction = \full|
// Convert bipolar to unipolar if needed
var mod = if(mode == \bipolar) { (modulator + 1) * 0.5 } { modulator };
case
// Full range modulation
{ direction == \full } {
value * (1 - amount) + (mod * amount);
}
// Upward only modulation
{ direction == \up } {
value + (mod * (1 - value) * amount);
}
// Downward only modulation
{ direction == \down } {
value - (mod * value * amount);
};
};
var modScaleBipolar = { |modulator, value, amount, direction = \center|
modScale.(modulator, value, amount, \bipolar, direction);
};
var modScaleUnipolar = { |modulator, value, amount, direction = \center|
modScale.(modulator, value, amount, \unipolar, direction);
};
self.helperFunctions.put(\triangle, triangle);
self.helperFunctions.put(\kink, kink);
self.helperFunctions.put(\interpEasing, interpEasing);
self.helperFunctions.put(\modScaleBipolar, modScaleBipolar);
self.helperFunctions.put(\modScaleUnipolar, modScaleUnipolar);
};
~getUnitShapers = { |self|
var unitHanning = { |phase|
1 - cos(phase * pi) * 0.5;
};
var unitCircular = { |phase|
sqrt(phase * (2 - phase));
};
var unitWelch = { |phase|
1 - squared(phase - 1);
};
var unitRaisedCos = { |phase, index|
var cosine = cos(phase * pi);
exp(index.abs * (cosine.neg - 1));
};
var unitGaussian = { |phase, index|
var cosine = cos(phase * 0.5pi) * index;
exp(cosine * cosine.neg);
};
var unitTrapezoid = { |phase, width, duty = 1|
var sustain = 1 - width;
var offset = phase - (1 - duty);
var trapezoid = (offset / sustain + (1 - duty)).clip(0, 1);
var pulse = offset > 0;
Select.ar(BinaryOpUGen('==', width, 1), [trapezoid, pulse]);
};
var unitTukey = { |phase, width, duty = 1|
var trapezoid = unitTrapezoid.(phase, width, duty);
unitHanning.(trapezoid);
};
self.unitShapers.put(\hanning, unitHanning);
self.unitShapers.put(\circular, unitCircular);
self.unitShapers.put(\welch, unitWelch);
self.unitShapers.put(\raisedCos, unitRaisedCos);
self.unitShapers.put(\gaussian, unitGaussian);
self.unitShapers.put(\trapezoid, unitTrapezoid);
self.unitShapers.put(\tukey, unitTukey);
};
~getOnePoleFilters = { |self|
var lowpass = { |sig, slope|
var safeSlope = slope.clip(-0.5, 0.5);
OnePole.ar(sig, exp(-2pi * safeSlope.abs));
};
var highpass = { |sig, slope|
sig - lowpass.(sig, slope);
};
self.onePoleFilters.put(\lpf, lowpass);
self.onePoleFilters.put(\hpf, highpass);
};
~getWaveShapers = { |self|
var sigmoid = { |x, curve|
var safeDenom = max(1 - curve, 0.0001);
var k = 2 * curve / safeDenom;
(1 + k) * x / (1 + (k * x.abs));
};
self.waveShapers.put(\sigmoid, sigmoid);
};
~getEasingFunctions = { |self|
var easingCores = [
\cubic,
\quintic,
\sine,
\circular,
\pseudoExp,
\pseudoLog2
];
easingCores.do{ |key|
var easeIn = case
{ key == \cubic } {
var cubicIn = { |x|
x * x * x;
};
cubicIn;
}
{ key == \quintic } {
var quinticIn = { |x|
x * x * x * x * x;
};
quinticIn;
}
{ key == \sine } {
var sineIn = { |x|
1 - cos(x * 0.5pi);
};
sineIn;
}
{ key == \circular } {
var circularIn = { |x|
1 - sqrt(1 - (x * x));
};
circularIn;
}
{ key == \pseudoExp } {
var pseudoExpIn = { |x, coef = 13|
(2 ** (coef * x) - 1) / (2 ** coef - 1)
};
pseudoExpIn;
}
{ key == \pseudoLog2 } {
var pseudoLog2In = { |x, coef = 12.5|
1 - (log2((1 - x) * (2 ** coef - 1) + 1) / coef);
};
pseudoLog2In;
};
var easeOut = { |x|
1 - easeIn.(1 - x);
};
// sigmoid with variable offset
var easeInOut = { |x, offset = 0.5|
Select.ar(x > offset, [
offset * easeIn.(x / offset),
offset + ((1 - offset) * (1 - easeIn.((1 - x) / (1 - offset))))
]);
};
// seat with variable height
var easeOutIn = { |x, height = 0.5|
Select.ar(x > height, [
height * (1 - easeIn.((height - x) / height)),
height + ((1 - height) * easeIn.((x - height) / (1 - height)))
]);
};
self.easingFunctions.put("%In".format(key).asSymbol, easeIn);
self.easingFunctions.put("%Out".format(key).asSymbol, easeOut);
self.easingFunctions.put("%Sigmoid".format(key).asSymbol, easeInOut);
self.easingFunctions.put("%Seat".format(key).asSymbol, easeOutIn);
};
};
~getInterpFunctions = { |self|
// linear interpolation of quintic in and quintic out
var quinticInterp = { |x, shape, interp = \step|
var easeOut = self.easingFunctions[\quinticOut].(x);
var easeIn = self.easingFunctions[\quinticIn].(x);
self.helperFunctions[\interpEasing].(x, shape, easeOut, easeIn, interp);
};
// linear interpolation of exponential in and exponential out
var pseudoExpInterp = { |x, shape, interp = \step|
var easeOut = self.easingFunctions[\pseudoExpOut].(x);
var easeIn = self.easingFunctions[\pseudoExpIn].(x);
self.helperFunctions[\interpEasing].(x, shape, easeOut, easeIn, interp);
};
// linear interpolation of quintic sigmoid to quintic seat
var sigmoidToSeatInterp = { |x, shape, inflection = 0.5, interp = \step|
var easeOut = self.easingFunctions[\quinticSigmoid].(x, inflection);
var easeIn = self.easingFunctions[\quinticSeat].(x, inflection);
self.helperFunctions[\interpEasing].(x, shape, easeOut, easeIn, interp);
};
self.interpFunctions.put(\quintic, quinticInterp);
self.interpFunctions.put(\exponential, pseudoExpInterp);
self.interpFunctions.put(\sigmoidToSeat, sigmoidToSeatInterp);
};
~getWindowFunctions = { |self|
var hanningWindow = { |phase, skew|
var warpedPhase = self.helperFunctions[\triangle].(phase, skew);
self.unitShapers[\hanning].(warpedPhase);
};
var circularWindow = { |phase, skew|
var warpedPhase = self.helperFunctions[\triangle].(phase, skew);
self.unitShapers[\circular].(warpedPhase);
};
var welchWindow = { |phase, skew|
var warpedPhase = self.helperFunctions[\triangle].(phase, skew);
self.unitShapers[\welch].(warpedPhase);
};
var raisedCosWindow = { |phase, skew, index|
var warpedPhase = self.helperFunctions[\triangle].(phase, skew);
var raisedCos = self.unitShapers[\raisedCos].(warpedPhase, index);
var hanning = self.unitShapers[\hanning].(warpedPhase);
raisedCos * hanning;
};
var gaussianWindow = { |phase, skew, index|
var warpedPhase = self.helperFunctions[\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.helperFunctions[\triangle].(phase, skew);
self.unitShapers[\trapezoid].(warpedPhase, width, duty);
};
var tukeyWindow = { |phase, skew, width, duty = 1|
var warpedPhase = self.helperFunctions[\triangle].(phase, skew);
self.unitShapers[\tukey].(warpedPhase, width, duty);
};
var exponentialWindow = { |phase, skew, shape, interp = \step|
var warpedPhase = self.helperFunctions[\triangle].(phase, skew);
self.interpFunctions[\exponential].(warpedPhase, 1 - shape, interp);
};
self.windowFunctions.put(\hanning, hanningWindow);
self.windowFunctions.put(\circular, circularWindow);
self.windowFunctions.put(\welch, welchWindow);
self.windowFunctions.put(\raisedCos, raisedCosWindow);
self.windowFunctions.put(\gaussian, gaussianWindow);
self.windowFunctions.put(\tukey, tukeyWindow);
self.windowFunctions.put(\trapezoid, trapezoidalWindow);
self.windowFunctions.put(\exponential, exponentialWindow);
};
};
)
~unitShapers = Prototype(\unitShapers);
// warped triangle
(
{
var phase = Phasor.ar(0, 50 * SampleDur.ir);
~unitShapers.helperFunctions[\triangle].(phase, \skew.kr(0.5));
}.plot(0.02);
)
// warped hanning window
(
{
var phase = Phasor.ar(0, 50 * SampleDur.ir);
~unitShapers.windowFunctions[\hanning].(phase, \skew.kr(0.5));
}.plot(0.02);
)
// warped welch window
(
{
var phase = Phasor.ar(0, 50 * SampleDur.ir);
~unitShapers.windowFunctions[\welch].(phase, \skew.kr(0.5));
}.plot(0.02);
)
// warped circular window
(
{
var phase = Phasor.ar(0, 50 * SampleDur.ir);
~unitShapers.windowFunctions[\circular].(phase, \skew.kr(0.5));
}.plot(0.02);
)
// warped raised cosine window
(
{
var phase = Phasor.ar(0, 50 * SampleDur.ir);
~unitShapers.windowFunctions[\raisedCos].(phase, \skew.kr(0.5), \index.kr(5));
}.plot(0.02);
)
// warped gaussian window
(
{
var phase = Phasor.ar(0, 50 * SampleDur.ir);
~unitShapers.windowFunctions[\gaussian].(phase, \skew.kr(0.5), \index.kr(5));
}.plot(0.02);
)
// warped trapezoidal window
(
{
var phase = Phasor.ar(0, 50 * SampleDur.ir);
~unitShapers.windowFunctions[\trapezoid].(phase, \skew.kr(0.5), \width.kr(0.5), \duty.kr(0.25));
}.plot(0.02);
)
// warped tukey window
(
{
var phase = Phasor.ar(0, 50 * SampleDur.ir);
~unitShapers.windowFunctions[\tukey].(phase, \skew.kr(0.5), \width.kr(0.5), \duty.kr(1));
}.plot(0.02);
)
// warped exponential window
(
{
var phase = Phasor.ar(0, 50 * SampleDur.ir);
var sigA = ~unitShapers.windowFunctions[\exponential].(phase, \skew.kr(0.5), \shape.kr(0.4), \step);
var sigB = ~unitShapers.windowFunctions[\exponential].(phase, \skew.kr(0.5), \shape.kr(0.4), \smoothStep);
var sigC = ~unitShapers.windowFunctions[\exponential].(phase, \skew.kr(0.5), \shape.kr(0.4), \smootherStep);
[sigA, sigB, sigC];
}.plot(0.02).superpose_(true).plotColor_([Color.red, Color.blue, Color.magenta]);
)
///////////////////////////////////////////////////////////////////////////////
// easing functions:
// test interpolation
(
{
var phase = Phasor.ar(0, 50 * SampleDur.ir);
var sigA = ~unitShapers.interpFunctions[\exponential].(phase, \shapeA.kr(0.7), \step);
var sigB = ~unitShapers.interpFunctions[\exponential].(phase, \shapeB.kr(0.7), \smoothStep);
var sigC = ~unitShapers.interpFunctions[\exponential].(phase, \shapeC.kr(0.7), \smootherStep);
[sigA, sigB, sigC];
}.plot(0.02).superpose_(true).plotColor_([Color.red, Color.blue, Color.magenta]);
)
// linear interpolation of exponential in and out
(
{
var phase = Phasor.ar(0, 50 * SampleDur.ir);
var sigA = ~unitShapers.interpFunctions[\exponential].(phase, \shapeA.kr(0));
var sigB = ~unitShapers.interpFunctions[\exponential].(phase, \shapeB.kr(0.5));
var sigC = ~unitShapers.interpFunctions[\exponential].(phase, \shapeC.kr(1));
[sigA, sigB, sigC];
}.plot(0.02).superpose_(true).plotColor_([Color.red, Color.blue, Color.magenta]);
)
///////////////////////////////////////////////////////////////////////////////
// easing functions:
// linear interpolation of quintic sigmoid to quintic seat (variable shape)
(
{
var phase = Phasor.ar(0, 50 * SampleDur.ir);
var sigA = ~unitShapers.interpFunctions[\sigmoidToSeat].(phase, \shapeA.kr(0), \inflection.kr(0.5));
var sigB = ~unitShapers.interpFunctions[\sigmoidToSeat].(phase, \shapeB.kr(0.5), \inflection.kr(0.5));
var sigC = ~unitShapers.interpFunctions[\sigmoidToSeat].(phase, \shapeC.kr(1), \inflection.kr(0.5));
[sigA, sigB, sigC];
}.plot(0.02).superpose_(true).plotColor_([Color.red, Color.blue, Color.magenta]);
)
// linear interpolation of quintic sigmoid to quintic seat (variable inflection)
(
{
var phase = Phasor.ar(0, 50 * SampleDur.ir);
var sigA = ~unitShapers.interpFunctions[\sigmoidToSeat].(phase, \shapeA.kr(0), \inflectionA.kr(0.25));
var sigB = ~unitShapers.interpFunctions[\sigmoidToSeat].(phase, \shapeB.kr(0.5), \inflectionB.kr(0.50));
var sigC = ~unitShapers.interpFunctions[\sigmoidToSeat].(phase, \shapeC.kr(1), \inflectionC.kr(0.75));
[sigA, sigB, sigC];
}.plot(0.02).superpose_(true).plotColor_([Color.red, Color.blue, Color.magenta]);
)