Wavesets analyse soundfiles into short fragments for granular synthesis

Inherits from: Object

Wavesets analyses soundfiles into short fragments called wavesets, and keeps these waveset data.

It can support a variety of waveset based synthesis instruments.

By Trevor Wishart's definition, a waveset is a segment of an audio signal between 

one non-positive to positive zero crossing and the next. [ see T. Wishart (1994): Audible Design. ]

Note that this definition only applies to mono signals.

Creation / Class Methods

*from (path, name, toBuffer)

Read a soundfile, analyse the signal, read the file to a buffer

path - the path to the soundfile

name - a name by which to keep the new wavesets in a global dictionary

toBuffer - a boolean whether to load the file to a buffer immediately.

// a first little example

Server.default = s = Server.internal; s.boot;

// make a wavesets from a soundfile

w = Wavesets.from("sounds/a11wlk01.wav");

w.dump; // contains mainly analysis data 

w.plot(200, 1); // plot a single waveset

w.signal.copyRange(w.xings[600], w.xings[601]).plot;

w.plot(600, 1); // a single

w.plot(600, 5); // a group of five contiguous wavesets




// startWs

w.eventFor(startWs: 600, length: 5, repeats: 2).postln.play;

w.eventFor(startWs: 600, length: 2, playRate: 1, repeats: 5).postln.play;

w.eventFor(startWs: 600, length: 2, playRate: 0.5, repeats: 5).postln.play;

w.eventFor(700, 20, 5, 1).play;


fork { 

666.do { |i| 

var ev = w.eventFor(i * 5, 2, 5, exprand(0.5, 1.0)); 

ev.put(\pan, 1.0.rand2).play;





*new (name, sig, sampleRate)

Make a Wavesets from a signal directly - not typical usage, but may be needed sometimes.

name - a name by which to keep the new wavesets in a global dictionary

sig - the signal to be analysed Explanation of sig. Default value is nil. Other information.

sampleRate - Explanation of sampleRate. Default value is nil. Other information.

all A dictionary where all the wavesets are kept by name. 

Existing wavesets in .all are not re-analysed.


*at (key) Access a Wavesets by its name. 

key - The name to look up

g = Wavesets.at('a11wlk01.wav');

*clear Clear the global dictionary


*minLength set the shortest length (in frames) that will still be treated as a waveset. Shorter 

wavesets are merged with their neighbors.


prepare some Synthdefs to be used with wavesets. 

Accessing Instance and Class Variables

signal the audio signal that was analysed. 

name the wavesets name in the global dictionary

buffer a buffer on the server that was created from the same soundfile

numFrames the number of frames

sampleRate the sample rate of the signal, default is Server.default.sampleRate


The following variables are analysis result lists

xings all integer indices of the zero crossings found

numXings total number of zero crossings found

lengths lengths of all wavesets

amps peak amplitude of every waveset

maxima indices of positive maximum value in every waveset

minima indices of negative minimum value in every waveset

fracXings the calculated fractional zerocrossing points.

this allows for more precise pitch information

and waveset transitions, resulting in smoother sound.

fracLengths fractional lengths - in effect, waveset 'pitch'.

These are overall statistics of the entire wavesets

minSet shorted waveset

maxSet longest waveset

avgLength average length of all wavesets

sqrAvgLength weighted average length - so bigger wavesets have a larger impact

minAmp softest waveset amplitude

maxAmp loudest waveset amplitude

avgAmp average amplitude of the entire waveset

sqrAvgAmp weighted average of (squared) amplitude of the entire waveset

Some utility instance methods:

toBuffer(server) load the analysed soundfile to the buffer.

frameFor(startWs, numWs, useFrac)

calculate startFrame, length in frames, and duration 

for a waveset or group. useFrac = true means use fractional crossings. 

true by default.

ampFor(startWs, length) maximum amp that occurs in a waveset or group.

eventFor(startWs, numWs, repeats, playRate, useFrac)

generate an event for a given combination of start waveset index,

number of wavesets, repeats, playback rate, and use of fractional crossings.

plot (startWs, length) plot a waveset or group for a given waveset index and length.


The simplest usage is to ask the waveset to prepare an event for you.

w = Wavesets.from("sounds/a11wlk01.wav");


fork { 

666.do { |i| 

var ev = w.eventFor((i * 5).postln, 2, 5, exprand(0.5, 1.0)); 

ev.put(\pan, 1.0.rand2).play;





// play a single waveset or waveset group by hand


{ var startFr, endFr, dur; 

startFr = w.xings[800]; 

endFr = w.xings[820];

dur = endFr - startFr / w.buffer.sampleRate;


BufRd.ar(1, w.buffer, Line.ar(startFr, endFr, dur, doneAction: 2)) 



loop buffer segments with a Phasor:


x = { arg start = 0, end = 128, playRate = 1; 

BufRd.ar(1, w.buffer, 

Phasor.ar(0, playRate * BufRateScale.kr(w.buffer), start, end)


}.scope(zoom: 8);


x.set(\start, 0, \end, 1024); // just some random length, may buzz

x.set(\start, w.xings.at(100), \end, w.xings.at(101));

x.set(\start, w.xings.at(101), \end, w.xings.at(102));

x.set(\start, w.xings.at(100), \end, w.xings.at(200));

x.set(\start, w.xings.at(780), \end, w.xings.at(800));

x.set(\playRate, 0.25);

x.set(\playRate, 1);


Doing Some Task (optional)


To play a waveset (or group) for a precise number of repetitions, 

one can use a SynthDef with a hard cutoff envelope, as below.

Note that adding an offset outside the phasor works better; 

Phasor.ar(0, playRate, start, end) is sometimes off by a few samples.

This is likely a 32bit float precision problem.

( // the simplest synthdef as implented in *prepareSynthDefs: 

SynthDef(\wvst0, { arg out = 0, buf = 0, start = 0, length = 441, playRate = 1, sustain = 1, amp=0.2, pan; 

var phasor = Phasor.ar(0, BufRateScale.ir(buf) * playRate, 0, length) + start;

var env = EnvGen.ar(Env([amp, amp, 0], [sustain, 0]), doneAction: 2);

var snd = BufRd.ar(1, buf, phasor) * env;

OffsetOut.ar(out, Pan2.ar(snd, pan));

}, \ir.dup(8)).memStore;


Synth("wvst0", [\bufnum, b, \start, 0, \length, 5000, \sustain, 0.1]);

// do the math by hand to understand it:


var startWs = 100, length = 6, rep = 10, playRate = 0.5;

var startframe, endframe, sustain;

startframe = w.xings[startWs];

endframe = w.xings[startWs + length];

sustain = (endframe - startframe) * rep / playRate / w.sampleRate; 

Synth("wvst0", [

\bufnum, w.buffer, 

\start, startframe, 

\length, endframe - startframe, 

\playRate, playRate,

\sustain, sustain, 

\amp, 1



// the same done with eventFor:

w.eventFor(100, 6, repeats: 10, playRate: 0.5).put(\amp, 1).play;



300.do({ arg i; 

var ev = w.eventFor(100 + i, 2, 10, 1);

ev.putPairs([\pan, [-1, 1].choose, \amp, 0.5]);






x = a11wlk01-44_1.aiff; 

// compare fractional and integer xings:

// especially for high frequency signals, 

// fractional does slightly cleaner looping and finer pitch gradations.

// - better test below with ordering wavesets by length -



[true, false].do { |usefrac| 

(1250..1500).do { arg i; 

var ev = w.eventFor(i, 1, 100, 1, useFrac: usefrac);

ev.putPairs([/*\pan, 0 [-1, 1].choose,*/ \amp, 0.5]);








// some variants waveset timestretch



// segments of 10 wavesets, step by 1 => 10x longer

(300, 301 .. 900).do { arg start; 

var ev = w.eventFor(start, numWs: 10, repeats: 1);

ev.putPairs([\amp, 0.5]);





// 1 waveset at a time, loop 10 times - much more 'pitch beads'-like

(300, 301 .. 900).do { arg start; 

var ev = w.eventFor(start, numWs: 1, repeats: 10);

ev.putPairs([\amp, 0.5]);






// play them sorted by (integer) waveset length: 

w.lengths.plot; // lengths are very irregular

o = w.lengths.order;



var start, end, startFr, endFr, dur, repeats;

var order; 

order = w.lengths.order;

[\false, \true].do { |useFrac| 

if (useFrac) { "fractional crossings - better pitch resolution" } {

"integer waveset borders - pitches quantized to integer sample lengths"


order.do({ arg start; 

var ev = w.eventFor(start, numWs: 1, repeats: 5);

ev.putPairs([\amp, 0.5]);