Overview
Here you can find notes on the usage of Buffers for playing back soundfiles and waveshaping.
Buffers
Buffers represent buffers on the sever, which are ordered arrays of floating point numbers. They can be single or multichannel buffers and are the usual way to store data on the server. Any data can be represented (Soundfiles, Waveforms, etc.).
Buffers, like Busses, are numbered, starting at 0. Using the Buffer
class takes care of allocating numbers, and avoids conflicts. Before using a Buffer you need to allocate memory for this, which is an asynchronous process. The size of Buffers is meassured in frames
. A frame is the number of samples in a mono buffer or the number of sample pairs in a stereo buffer.
The number of samples of a buffer is frames * num-of-channels = num-of-samples
.
Since Buffers are server side objects, the server needs to be booted, when you want to allocate memory for a buffer.
Buffers are global, which is to say that they can be accessed by any synth, and by more than one at a time. They can be written to or even changed in size, while they are being read from.
Making a Buffer Object and Allocating Memory
Making a Buffer object and allocating the necessary memory in the server app is quite easy. You can do it all in one step with Buffer’s alloc method:
s.boot;
b = Buffer.alloc(s, 100, 2); // allocate 2 channels, and 100 frames
b.free; // free the memory (when you're finished using it)
The acutal size of the Buffer is the number of channels multiplied with the number of frames.
If you’d like to allocate in terms of seconds, rather than frames, you can do so like this:
b = Buffer.alloc(s, s.sampleRate * 8.0, 2); // an 8 second stereo buffer
b.free;
Buffer’s ‘free’ method frees the memory on the server, and returns the Buffer’s number for reallocation. You should not use a Buffer object after doing this.
Using Buffers with Sound Files
Buffer has another class method called ‘read’, which reads a sound file from disk into memory, and returns a Buffer object. You can manually insert the path of the file or drag-and-drop it to the line, which inserts the absolute path.
When you run the command for loading a file into a buffer, this will not happen immediately. Since it’s an asynchronous process, it needs a little bit of time to get all the information like size, number of channels, and the sampling rate of the file. After a short time, you can access all the information of the soundfile via the appropriate getter methods.
Using the UGen PlayBuf, we can play the file.
// read a soundfile
b = Buffer.read(s, "sndfl.wav");
// now play it
b.play;
// or define a SynthDef for playing a buffer
(
x = SynthDef(\example,{ arg out = 0, bufnum;
Out.ar( out,
PlayBuf.ar(1, bufnum, BufRateScale.kr(bufnum))
)
}).play(s,[\bufnum, b]);
)
x.free; b.free;
You can also overwrite the current values of the buffer with 0
’s with the Buffer.zero
method.
Buffer Management
There are some methods in dealing with bigger number of soundfiles and buffers. Here you can find some examples.
load multiple soundfiles into buffers and save them in an array
(
b = [
Buffer.read(s, "sndfl1"),
Buffer.read(s, "sndfl2"),
Buffer.read(s, "sndfl3"),
];
)
b[0].play;
Buffer.freeAll; // free all buffers
load multiple soundfiles into buffers as an Event
An Event is a subclass from Dictionary. It consists of Key-Value pairs.
b = (); // create an empty Event
b[\thing] = 1.5; // assign a value to a key
b.thing; // return the value of a key
b.removeAt(\thing); // remove a key/value pair
(
b = ();
b.put(\sndfl1 , Buffer.read(s, "sndfl1"));
)
b.sndfl1.play;
use loops to load a directory of soundfiles into buffers
With the help of the PathName
object there are several ways of loading a dir of soundfiles to buffers.
The special keyword thisProcess
can receive the method nowExecutingPath
, which returns the absolute path, where your current .scd file lives. This path can be saved as a PathName
object to a variable, which can return via the parentPath
method, the parent path to this file.
When the PathName
is a directory, you can get the entries of the directory as an array (entries
method), which allows looping on these entries.
// this saves the current file path to a variable
p = PathName(thisProcess.nowExecutingPath);
p.parentPath; // get the current parent path
// this saves the parent path to a variables and appends a directory name to it
p = PathName(PathName(thisProcess.nowExecutingPath).parentPath ++ "sndfls/");
p.entries; // if the PathName is a folder, you can get the entries of the directory as an array, this allows looping on these entries
(
b = PathName(PathName(thisProcess.nowExecutingPath).parentPath ++ "sndfls/").entries.collect({|pn|
Buffer.read(s, pn.fullPath);
});
)
b[0].play;
It is more convenient to save your directory of soundfiles at the same place of you .scd file. This allows this short form for the same process as above.
(
b = PathName("sndfls/".resolveRelative).entries.collect({|pn|
Buffer.read(s, pn.fullPath)
});
)
b[0].play;
PlayBuf
PlayBuf.ar
has some basic options for playing a soundfile from a buffer.
When designing a Synth with PlayBuf
you need a hard coded numChannels
value. This values decides how many channels from a buffer are played. For the compile process of a SynthDef SuperCollider needs to know in before how many channels need to be allocated for the UGen Graph on the server.
To solve flexible playing of buffer of different sizes you can implement multiple SynthDefs with different channel settings.
PlayBuf
is not able to play a soundfile at the right speed per default. When the samplerate of the soundfile you want to play back and the samplerate of the server are not the same, the result will be a transposition of the soundfile, as a result of a ‘wrong’ playback rate. You need to introduce a BufRateScale
object for this.
The manual process would be to scale the playing rate with 1/(serverSampleRate/soundfileSampleRate)
or short version (serverSampleRate/soundfileSampleRate).reciprocal
.
The method midiratio
can help adjusting the playback rate to fixed musical steps.
The trigger
argument allows jumping to a startPos
and is usally a signal (Impulse
can work), while startPos
is a frame value.
The doneAction
is only evaluated, whenn loop = 0
. When loop = 1
the playback is restartet when the last frame is reached. When doneAction = 2
is evaluated, the synth is freed after the complete playback of the synth.
some examples
Basic Playback of a stereo file:
b = Buffer.read(s, "sndfl.wav");
b.numChannels; // it's a stereo file
(
SynthDef(\playbuf, {|buf = 0, rate = 1|
var sig = PlayBuf.ar(
numChannels: 2, // it expects a stereo buffer
bufnum: buf,
rate: BufRateScale.ir(buf) * rate, // allways use 'BufRateScale' to get the right playingrate
trigger: 1,
starPos: 0
);
Out.ar(0, sig);
}).add;
)
Playback with playrate modulation:
(
SynthDef(\playbuf, {|buf = 0, rate = 1, modSteps = 0.01, modSpeed = 1|
var sig = PlayBuf.ar(
numChannels: 2,
bufnum: buf,
rate: BufRateScale.ir(buf) * rate * SinOsc.kr(modSpeed).bipolar(modSteps).midiratio,
);
Out.ar(0, sig);
}).add;
)
Synth(\playbuf, [buf: b, rate: -14.midiratio, modSteps: 5, modSpeed: 30])
Looping with Impulse trigger, random start position and envelope, similiar to a granular synthesizer:
(
SynthDef(\playbuf, {|buf = 0, rate = 1|
var trig = Impulse.ar(MouseX.kr(1, 30, 1));
var env = EnvGen.ar(
Env.perc(0.001, 0.008),
gate: trig
);
var start = TRand.ar(0, BufFrames.ir(buf) - 1, trig);
var sig = PlayBuf.ar(
numChannels: 2, // it expects a stereo buffer
bufnum: buf,
rate: BufRateScale.ir(buf) * rate,
trigger: trig,
startPos: start,
);
sig = sig * env;
Out.ar(0, sig);
}).add;
)
Synth(\playbuf, [buf: b, rate: -12.midiratio])
Multichannel Expansion with different playback rates and a Line
object to itnroduce a doneAction
without cutting sound:
(
SynthDef(\playbuf, {|buf = 0|
var start = 0;
var sig = PlayBuf.ar(
numChannels: 2, // it expects a stereo buffer
bufnum: buf,
rate: BufRateScale.ir(buf) * {Rand(-36.0, 0)}.dup(22).midiratio,
trigger: 1,
startPos: 0,
);
Line.kr(0, 1, BufDur.ir(buf) * 8, doneAction: 2);
sig = Splay.ar(sig);
Out.ar(0, sig);
}).add;
)
Synth(\playbuf, [buf: b]);
BufRd
BufRd
expects a readpointer, that is pointing to a frame in the buffer we want to listen to. This readpointer can be any audio signal, which makes BufRd
way more flexible then PlayBuf
.
A simple readpointer could be a Line.ar
object, which draws a line between two points in a specific time.
(
SynthDef(\bufrd, {|buf|
var phs = Line.ar(0, BufFrames.ir(buf) - 1, BufDur.ir(buf), doneAction: 2);
var sig = BufRd.ar(2, buf, phs);
Out.ar(0, sig);
}).add;
)
Synth(\bufrd, [buf: b]);
But Line
does not allow looping. Using Phasor
makes this more flexible. Phasor is a resetable, linear ramp that has a start, a end and an increment value, by which the value of the Phasor
increments per sample (rate). The end value is a wrap value, this value is never reached.
The increment value is crucial, but easy to manage: if we assume, we want to play the soundfile in the original speed, and the samplerate of the soundfile and the samplerate of the server are the same, we increment the phasor by 1 for each sample. If we have a samplingrate mismatch, we need to calculate the increment. But in both cases, we can just use BufRateScale
.
(
SynthDef(\bufrd, {|buf = 0, rate = 1|
var phs = Phasor.ar(0, BufRateScale.ir(buf) * rate, 0, BufFrames.ir(buf));
var sig = BufRd.ar(2, buf, phs);
Out.ar(0, sig);
}).add;
)
Synth(\bufrd, [buf: b, rate: 0.5]);
Instead of Phasor
you can also use LFSaw
or Sweep
.
But you can also use any non-linear audio signal, like SinOsc.ar
.
(
SynthDef(\bufrd, {|buf = 0, rate = 1|
var phs = SinOsc.ar(
freq: (rate * {Rand(-24.0, 0)}.dup(18).midiratio) / BufDur.ir(buf),
phase: 3pi/2
).range(0, BufFrames.ir(buf) - 1);
var sig = BufRd.ar(2, buf, phs);
sig = Splay.ar(sig);
Out.ar(0, sig);
}).add;
)
Synth(\bufrd, [buf: b, rate: 1.5]);
You can also add offsets to the Phasor
via noise generators.
(
SynthDef(\bufrd, {|buf = 0, rate = 1|
var phs, sig;
phs = Phasor.ar(0, BufRateScale.ir(buf) * rate, 0, BufFrames.ir(buf));
phs = phs + (LFNoise2.ar(1!8).bipolar(0.25) * SampleRate.ir);
sig = BufRd.ar(2, buf, phs);
sig = Splay.ar(sig);
Out.ar(0, sig);
}).add;
)
Synth(\bufrd, [buf: b, rate: 0.5]);
Streaming a File in From Disk
In some cases, for instance when working with very large files, you might not want to load a sound completely into memory. Instead, you can stream it in from disk a bit at a time, using the UGen DiskIn, and Buffer’s ‘cueSoundFile’ method:
(
SynthDef("tutorial-Buffer-cue",{ arg out=0,bufnum;
Out.ar(out,
DiskIn.ar( 1, bufnum )
)
}).add;
)
b = Buffer.cueSoundFile(s,Platform.resourceDir +/+ "sounds/a11wlk01-44_1.aiff", 0, 1);
y = Synth.new("tutorial-Buffer-cue", [\bufnum,b], s);
b.free; y.free;
This is not as flexible as PlayBuf (no rate control), but can save memory.
Getter Methods on Buffer Objects
When you save an object — for example, a Buffer
— to an instance variable, you gain access to several getter and setter methods. In particular, the getter methods for Buffer
instances are very useful, because you often don’t have all information about a sound file available at the time of loading.
// watch the post window
b = Buffer.read(s, "sndfl.wav");
b.plot;
b.query; // plot every information
b.bufnum;
b.numFrames;
b.numChannels;
b.sampleRate;
b.free;
Since there is a latency between loading something into a buffer on the server and being able to use it on the client side, many Buffer
methods accept an action function as an argument. This function is executed once the client receives confirmation from the server that the buffer is ready.
// with an action function
// note that the vars are not immediately up-to-date
(
b = Buffer.read(s, Platform.resourceDir +/+ "sounds/a11wlk01.wav", action: { arg buffer;
("numFrames after update:" + buffer.numFrames).postln;
x = { PlayBuf.ar(1, buffer, BufRateScale.kr(buffer)) }.play;
});
// Note that the next line will execute BEFORE the action function
("numFrames before update:" + b.numFrames).postln;
)
x.free; b.free;
Recording into Buffers
RecordBuf
Recording and Overdubbing a buffer is made very easy with RecordBuf
.
recLevel
is a like a pre gain for the incoming signal. preLevel
is a scalar, that is applied to the previous content of the buffer. This allows mixing new content with the content, that is already in the buffer.
// load one channel of a sndfl into a buffer
b = Buffer.readChannel(s, "sndfl.wav", channels: 1);
// overdub a soundfile with microphone input
(
SynthDef(\recbuf, {
| buf=0, rec = 1, prev = 0|
var mic = SoundIn.ar(0);
RecordBuf.ar(mic, buf, recLevel: rec, preLevel: prev);
}).add;
)
Synth(\recbuf, [buf: b, rec: 1, prev: 1]);
b.play;
BufWr
BufWr
is way more simpler then RecordBuf
. It just needs an input, the bufnum, and an writepointer (phase). Again, everything can be the writepointer, but the standard way would be to use Line
or Phasor
.
~b = Buffer.alloc(s, s.sampleRate * 5, 1);
~b.plot;
(
SynthDef(\bufwr, {
| buf = 0 |
var phs = Line.ar(0, BufFrames.ir(buf) - 1, BufDur.ir(buf), doneAction: 2);
var mic = SoundIn.ar(0);
BufWr.ar(mic, buf, phs)
}).add;
)
Synth(\bufwr, [buf: ~b]);
~b.play;
Here is an example, which results in a delay effect. Here you can see how to write and rad from a buffer at the same time.
~b = Buffer.alloc(s, s.sampleRate * 5, 1);
(
SynthDef(\bufwr, {
| buf = 0 |
var phs, mic, phs_r, sig;
// write buffer
phs = Phasor.ar(0, 1, 0, BufFrames.ir(buf) - 1);
mic = SoundIn.ar(0);
BufWr.ar(mic, buf, phs);
// read buffer
phs_r = phs - (SampleRate.ir * 0.5);
sig = BufRd.ar(1, buf, phs_r);
sig = sig * 0.2 ! 2;
Out.ar(0, sig);
}).add;
)
Synth(\bufwr, [buf: ~b]);
Here is a more complex example, with a delay effect. It makes use of multichannel expansion on the BufRd
.
(
SynthDef(\bufwr, {
| buf = 0 |
var phs, mic, phs_r, sig;
// write buffer
phs = Phasor.ar(0, 1, 0, BufFrames.ir(buf) - 1);
mic = SoundIn.ar(0);
BufWr.ar(mic, buf, phs);
// read buffer
phs_r = phs - (SampleRate.ir * (1..10).linexp(1, 10, 0.08, 3));
phs_r = phs_r + (LFNoise2.ar(1!10).bipolar(0.2) * SampleRate.ir);
sig = BufRd.ar(1, buf, phs_r);
sig = Splay.ar(sig.scramble);
Out.ar(0, sig);
}).add;
)
Synth(\bufwr, [buf: ~b]);
Accessing Buffer Data
The Buffer
class allows easy access to the buffer values for writing or reading. Buffer-set just takes an index and a value to write. Buffer-get takes an index and a action-function. Multichannel Buffer are interleaving their data, so for a two channel buffer index 0 = frame1-chan1, index 1 = frame1-chan2, index 2 = frame2-chan1, and so on.
b = Buffer.alloc(s, 8, 1);
b.set(7, 0.5); // set the value at 7 to 0.5
b.get(7, {|msg| msg.postln}); // get the value at 7 and post it when the reply is received
b.free;
The methods ‘getn’ and ‘setn’ allow you to get and set ranges of adjacent values. ‘setn’ takes a starting index and an array of values to set, ‘getn’ takes a starting index, the number of values to get, and an action function.
b = Buffer.alloc(s,16);
b.setn(0, [1, 2, 3]); // set the first 3 values
b.getn(0, 3, {|msg| msg.postln}); // get them
b.setn(0, Array.fill(b.numFrames, {1.0.rand})); // fill the buffer with random values
b.getn(0, b.numFrames, {|msg| msg.postln}); // get them
b.free;
There is an upper limit on the number of values you can get or set at a time (usually 1633 when using UDP, the default). This is because of a limit on network packet size. To overcome this Buffer has two methods, ‘loadCollection’ and ‘loadToFloatArray’ which allow you to set or get large amounts of data by writing it to disk and then loading to client or server as appropriate.
(
// make some white noise
v = FloatArray.fill(44100, {1.0.rand2});
b = Buffer.alloc(s, 44100);
)
(
// load the FloatArray into b, then play it
b.loadCollection(v, action: {|buf|
x = { PlayBuf.ar(buf.numChannels, buf, BufRateScale.kr(buf), loop: 1)
* 0.2 }.play;
});
)
x.free;
// now get the FloatArray back, and compare it to v; this posts 'true'
// the args 0, -1 mean start from the beginning and load the whole buffer
b.loadToFloatArray(0, -1, {|floatArray| (floatArray == v).postln });
b.free;
A FloatArray is just a subclass of Array which can only contain floats.
Plotting and Playing
Buffer has two useful convenience methods: ‘plot’ and ‘play’.
// see the waveform
b = Buffer.read(s, Platform.resourceDir +/+ "sounds/a11wlk01.wav");
b.plot;
// play the contents
// this takes one arg: loop. If false (the default) the resulting synth is
// freed automatically
b.play; // frees itself
x = b.play(true); // loops so doesn't free
x.free; b.free;
Waveshaping
Buffers can also be used for holding a transferfunction for waveshaping. Here is an example with a chebyshev polynom.
b = Buffer.alloc(s, 512, 1);
b.cheby([1,0,1,1,0,1]);
(
x = play({
Shaper.ar(
b,
SinOsc.ar(300, 0, Line.kr(0,1,6)),
0.5
)
});
)
x.free; b.free;