Dynamic Waveshaping in SuperCollider

Here is a short tutorial covering a technique for dynamic waveshaping in SuperCollider.

Dynamic Waveshaping

Waveshaping is a way of distorting a signal in a non-linear way. It involves the use of a transfer function in order to calculate the result. A transfer function is just a way of mapping a certain input value to a certain output value.

In this transfer function, any input value will be mapped to the same output value – this is a linear function.

In this next example, the transfer function is a sine wave, so the output value at any point is going to be different from the input value.

Waveshaping involves using a signal as an input. In the diagram below, a sine wave passes through a transfer function that is linear, which means there will be no difference in sound at all.

In the following diagram, the transfer function is the result of a few harmonics added together, making a more complex shape. Now it can be seen that the sine wave has been distorted by this function.

Waveshaping in SuperCollider

Waveshaping in SC traditionally involves the use of wavetables – a special way of formatting an array of numbers for use in oscillators which work in that way – i.e. reading through a table of values. Here is a small example:

s = Server.internal.boot;

(

// in order to be turned into a wavetable,

// the signal must be half buffer size plus one

x = Signal.sineFill(513, [0.5, 0.2, 0.3, 0.0, 0.2]);

x.plot;

b = Buffer.alloc(s, 1024, 1);

)

b.sendCollection(x.asWavetableNoWrap);

(

{

var input, output;

input = SinOsc.ar(MouseX.kr(80, 800, \exponential), 0, 0.7);

// Shaper is the waveshaping Ugen

output = Shaper.ar(b.bufnum, input);

output ! 2

}.scope;

)

However, there is another way, which allows an arbitrary buffer size to be used, as well as a little dynamism to be added to the result. BufRd is a UGen which indexes into a buffer. If the input signal (-1.0 to 1.0) is mapped to the index of a buffer containing a transfer function, the result will be waveshaping.

There are a few ways to involve some modulation:

• change the range that the input signal is mapped to

• modulate the offset of the input signal

• as smoothly as possible, change the contents of the buffer containing the function.

fill the buffer with the transfer function – no need for special encoding or special buffer sizes

(

s = Server.internal;

a = Signal.sineFill(1000, [1, 0.2, 0.7]);

// or..

//a = Signal.sineFill(1000, [0, 0.2, 0.8, 0.1, 0.5]);

// the straight version…

// a = Array.interpolation(1000, 0.0, 1.0);

// check out the transfer function

a.plot;

)

// then…

(

b = Buffer.sendCollection(s, a, 1);

)

use audio input as an index into the buffer, using LinLin to map one to the other.

perhaps modulate the range of the index, for a more dynamic sound.

(

{

var sinFreq, soundIn, playHead, output;

var thisIndex;

sinFreq = MouseX.kr(20, 1000, \exponential).poll;

soundIn = SinOsc.ar(sinFreq, 0, 0.8);

thisIndex = LinLin.ar(soundIn, -1.0, 1.0, 0.0, BufFrames.kr(b.bufnum));

// some gentle dynamic waveshaping -

// modulate the range of the indexing

// thisIndex = LinLin.ar(soundIn, -1.0, 1.0, 0.0, BufFrames.kr(b.bufnum) * SinOsc.kr(0.6).range(0.15, 1.0));

// some over-aggressive modulation!

// thisIndex = LinLin.ar(soundIn, -1.0, 1.0, 0.0, BufFrames.kr(b.bufnum) * SinOsc.ar(sinFreq * 0.25).range(0.15, 1.0));

playHead = BufRd.ar(1, b.bufnum, thisIndex, 0, 4);

// remove any DC weirdness

output = LeakDC.ar(playHead);

output ! 2;

}.scope;

)

…or offset the input before “waveshaping.” then use LeakDC to correct the output.

(

{

var sinFreq, soundIn, playHead, output;

var thisIndex;

var sinMult, sinOffsetRange;

sinFreq = MouseX.kr(20, 2000, \exponential);

// now move the base position of the sine around, so that different areas of the transfer function get used

// resulting in a nice shifting around of the phase

sinMult = 0.32; sinOffsetRange = 0.64;

soundIn = SinOsc.ar(sinFreq, 0, sinMult, SinOsc.kr(0.17).range(0.0 – sinOffsetRange, sinOffsetRange));

thisIndex = LinLin.ar(soundIn, -1.0, 1.0, 0.0, BufFrames.kr(b.bufnum));

playHead = BufRd.ar(1, b.bufnum, thisIndex, 0, 4);

// remove any DC weirdness

output = LeakDC.ar(playHead);

output ! 2;

}.scope;

)

change the contents of the transfer function buffer using “harmonics” fading in and out

(

{

var sinFreq, soundIn, playHead, output;

var thisIndex;

var bufInput, bufInputFreq;

sinFreq = MouseX.kr(20, 1000, \exponential).poll;

soundIn = SinOsc.ar(sinFreq, 0, 0.8);

thisIndex = LinLin.ar(soundIn, -1.0, 1.0, 0.0, BufFrames.kr(b.bufnum));

// fill one cycle of the buffer without glitches -

// the frequency should be a period that fits into the size of the buffer,

// according to the sample rate

bufInputFreq = SampleRate.ir / BufFrames.kr(b.bufnum);

// mix the first 6 tones of the harmonic series – let them fade in and out randomly

bufInput = Mix(

SinOsc.ar(bufInputFreq * (1 .. 6), Array.linrand(6, 0.0, 6.28), SinOsc.kr(Array.exprand(6, 0.1, 0.3), 0, 0.2))

);

// write this into the buffer that is being used as the transfer function

BufWr.ar(bufInput, b.bufnum, Phasor.ar(0, BufRateScale.kr(b.bufnum), 0, BufFrames.kr(b.bufnum)));

playHead = BufRd.ar(1, b.bufnum, thisIndex, 0, 4);

// remove any DC weirdness

output = LeakDC.ar(playHead);

// uncomment to check out the transfer function

// output = BufRd.ar(1, b.bufnum, Phasor.ar(0, BufRateScale.kr(b.bufnum), 0, BufFrames.kr(b.bufnum)), 0, 4);

output ! 2;

}.scope;

)

use an envelope with shifting points in order to dynamically change the transfer function

(

{

var sinFreq, soundIn, playHead, output;

var thisIndex;

var bufInput, bufInputFreq;

var modPoints, env, envgen;

sinFreq = MouseX.kr(20, 1000, \exponential).poll;

soundIn = SinOsc.ar(sinFreq, 0, 0.8);

thisIndex = LinLin.ar(soundIn, -1.0, 1.0, 0.0, BufFrames.kr(b.bufnum));

bufInputFreq = SampleRate.ir / BufFrames.kr(b.bufnum);

// use these points for the env levels – one extra last one (release node) so that the env loops properly

modPoints = [-1.0, -0.2, -0.5, 0.9, 1.0, 1.0];

// uncomment to see the points

// modPoints.plot;

// uncomment to replace the points with modulating values

// 5.do { |i|

// modPoints[i] = SinOsc.kr(ExpRand(0.1, 0.8), Rand(0.0, 6.28)).range(-0.9, 0.9);

// };

// make an envelope which will result in a warped transfer function

env = Env(modPoints, [0.25, 0.25, 0.25, 0.25, 0.0], [2, -4, 7, -5, 0], 4, 0);

envgen = EnvGen.ar(env, timeScale: bufInputFreq.reciprocal);

bufInput = envgen;

// write this into the buffer that is being used as the transfer function

BufWr.ar(bufInput, b.bufnum, Phasor.ar(0, BufRateScale.kr(b.bufnum), 0, BufFrames.kr(b.bufnum)));

playHead = BufRd.ar(1, b.bufnum, thisIndex, 0, 4);

// remove any DC weirdness

output = LeakDC.ar(playHead);

// uncomment to check out the transfer function

// output = BufRd.ar(1, b.bufnum, Phasor.ar(0, BufRateScale.kr(b.bufnum), 0, BufFrames.kr(b.bufnum)), 0, 4);

output ! 2;

}.scope;

)

Conclusion

“Waveshaping” seems like an exotic concept, particularly if you use SC’s suggested method. However, it is nothing more than mapping one value to another using a “transfer function”. The input can be a signal, and the transfer function (basically an array of values) can be a buffer. The buffer can be changed as it’s being read, perhaps by recording the output of another UGen. This technique can create waveforms which shift around in a rich and interesting way.

This entry was posted in code, music and tagged , , , . Bookmark the permalink.

Leave a Reply