Yesterday, I posted some links to a supercollider class, BufferTool and it’s helpfile. I thought maybe I should also post an example of using the class.
I wrote one piece, “Rush to Excuse,” in 2004 that uses most of the features of the class. Program notes are posted at my podcast. And, The code is below. It requires two audio files, geneva-rush.wav and limbaugh-dog.aiff You will need to modify the source code to point at your local copy of those files.
The piece chops up the dog file into evenly sized pieces and finds the average pitch for each of them. It also finds phrases in Rush Limbaugh’s speech and intersperses his phrases with the shorter grains. This piece is several years old, but I think it’s a good example to post because the code is all cleaned up to be in my MA thesis. Also, listening to this for the first time in a few years makes me feel really happy. Americans finally seem to agree that torture is bad! Yay! (Oh alas, that it was ever a conversation.)
( // first run this section var sdef, buf; // a callback function for loading pitches c = { var callback, array, count; array = g.grains; count = g.grains.size - 1; callback = { var next, failed; failed = true; {failed == true}. while ({ (count > 0 ). if ({ count = count -1; next = array.at(count); (next.notNil).if ({ next.findPitch(action: callback); failed = false; }, { // this is bad. "failed".postln; failed = true; }); }, { // we've run out of grains, so we must have succeeded failed = false; "pitch finding finished".postln; }); }); }; }; // buffers can take a callback function for when they finish loading // so when the buffer loads, we create the BufferTools and then // analyze them buf = Buffer.read(s, "sounds/pundits/limbaugh-dog.aiff", action: { g = BufferTool.grain(s, buf); h = BufferTool.grain(s, buf); "buffers read!".postln; g.calc_grains_num(600, g.dur); g.grains.last.findPitch(action: c.value); h.prepareWords(0.35, 8000, true, 4000); }); i = BufferTool.open(s, "sounds/pundits/geneva-rush.wav"); sdef = SynthDef(marimba, {arg out=0, freq, dur, amp = 1, pan = 0; var ring, noiseEnv, noise, panner, totalEnv; noise = WhiteNoise.ar(1); noiseEnv = EnvGen.kr(Env.triangle(0.001, 1)); ring = Ringz.ar(noise * noiseEnv, freq, dur*5, amp); totalEnv = EnvGen.kr(Env.linen(0.01, dur*5, 2, 1), doneAction:2); panner = Pan2.ar(ring * totalEnv * amp, pan, 1); Out.ar(out, panner); }).writeDefFile; sdef.load(s); sdef.send(s); SynthDescLib.global.read; // pbinds and buffers act strangely if this line is omitted ) // wait for: pitch finding finished ( // this section runs the piece var end_grains, doOwnCopy; end_grains = g.grains.copyRange(g.grains.size - 20, g.grains.size); // for some reason, Array.copyRange blows up // this is better anyway because it creates copies of // the array elements // also: why not stress test the garbage collector? doOwnCopy = { arg arr, start = 0, end = 10, inc = 1; var new_arr, index; new_arr = []; index = start.ceil; end = end.floor; {(index < end) && (index < arr.size)}. while ({ new_arr = new_arr.add(arr.at(index).copy); index = index + inc; }); new_arr; }; Pseq( [ // The introduction just plays the pitches of the last 20 grains Pbind( instrument, marimba, amp, 0.4, pan, 0, grain, Pseq(end_grains, 1), [freq, dur], Pfunc({ arg event; var grain; grain = event.at(grain); [ grain.pitch, grain.dur]; }) ), Pbind( grain, Prout({ var length, loop, num_words, loop_size, max, grains, filler, size, grain; length = 600; loop = 5; num_words = h.grains.size; loop_size = num_words / loop; filler = 0.6; size = (g.grains.size * filler).floor; grains = g.grains.reverse.copy; // then play it straight through with buffer and pitches {grains.size > 0} . while ({ //"pop".postln; grain = grains.pop; (grain.notNil).if({ grain.yield; }); }); loop.do ({ arg index; "looping".postln; // mix up some pitched even sizes grains with phrases max = ((index +2) * loop_size).floor; (max > num_words). if ({ max = num_words}); grains = //g.grains.scramble.copyRange(0, size) ++ doOwnCopy.value(g.grains.scramble, 0, size) ++ //h.grains.copyRange((index * loop_size).ceil, max); doOwnCopy.value(h.grains, (index * loop_size).ceil, max); // start calculating for the next pass through the loop length = (length / 1.5).floor; g.calc_grains_num(length, g.dur); g.grains.last.findPitch(action: c.value); grains = grains.scramble; // ok, play them {grains.size > 0} . while ({ //"pop".postln; grain = grains.pop; (grain.notNil).if({ grain.yield; }); }); }); i.yield; "end".postln; }), [bufnum, dur, grainDur, startFrame, freq, instrument], Pfunc({arg event; // oddly, i find it easier to figure out the grain in one step // and extract data from it in another step // this gets all the data you might need var grain, dur, pitch; grain = event.at(grain); dur = grain.dur - 0.002; pitch = grain.pitch; (pitch == nil).if ({ pitch = 0; }); [ grain.bufnum, dur, grain.dur, grain.startFrame, pitch, grain.synthDefName ]; }), amp, 0.6, pan, 0, xPan, 0, yPan, 0, rate, 1, twoinsts, Pfunc({ arg event; // so how DO you play two different synths in a Pbind // step 1: figure out all the data you need for both // step 2: give that a synthDef that will get invoked no matter what // step 3: duplicate the event generated by the Pbind and tell it to play var evt, pitch; pitch = event.at(freq); (pitch.notNil). if ({ // the pitches below 20 Hz do cool things to the // speakers, but they're not really pitches, // so screw 'em (pitch > 20). if ({ evt = event.copy; evt.put(instrument, marimba); evt.put(amp, 0.4); evt.play; true; }, { false; }) }, { // don't let a nil pitch cause the Pbind to halt event.put(freq, rest); false; }); }) ) ], 1).play )
This code is under a Creative Commons Share Music License
Podcast: Play in new window | Download