Tom Says: Code something crazy every day you feel like it!

ChucK is the audio software powering most of PLOrk (the PRinceton Laptop Orchestra), and pretty much anything I've made recently requiring real-time audio. It's a flaky scripting language that's strongly-timed, meaning that it makes guarantees about where samples you create will appear in the output stream (even if the stream doesn't make it to the speaker on time because your script is slow).
But if you don't already know ChucK, I'll have to point you toward his wonderful documentation before you read much further.
In creating the various ChucK instruments I have over the semester, I've often needed a way to combine many sounds with interleaved start and end times. There are two major stumbling blocks to doing this well:
After many, many trials, I believe I'm finally getting close to something usable, even though I have reservations. Take a look:
Gain mix => dac;
9 => int numVoices;
Envelope outs[numVoices];
Shred managers[numVoices];
for(0 => int i; i < numVoices; i++) {
Envelope e @=> outs[i];
null => managers[i];
}
fun void open(int voice) { outs[voice] => mix; }
fun void close(int voice) { outs[voice] =< mix; }
fun void rampTo(int voice, float gain, dur len) {
gain => outs[voice].target;
len => outs[voice].duration;
outs[voice].keyOn();
len => now;
}
// spork this
fun void on(int voice, float gain) {
if(managers[voice] != null) Machine.remove(managers[voice].id());
me @=> managers[voice];
open(voice);
rampTo(voice, gain * 2.0, 50::ms); // attack
rampTo(voice, gain, 50::ms); // sustain
null => managers[voice];
}
// spork this
fun void off(int voice) {
if(managers[voice] != null) Machine.remove(managers[voice].id());
me @=> managers[voice];
rampTo(voice, 0.0, 500::ms);
close(voice);
null => managers[voice];
}
Each voice is an Envelope you chuck audio to. Think of it as an organ; you chuck sounds to the different pipes, which you on() and off() to open or close.
Each voice also has a reference to the "shred" which is currently managing its envelope. If you should on() a voice while it is still fading out, the shred doing the fading is killed and the new, fade-in shred takes over.
That the shreds go around killing each other really bothers me. What happens when a voice is told to on and off at the same ChucK time? Who knows? However, I cannot argue with the results: this is my first implementation that works well enough that I don't feel like my system for handling voices is getting in the way when I banging out notes. It's hard to argue with results, even when race conditions lurk behind every corner.
The code I'm using this for requires a special MIDI device that I've been working with recently, but here is some pseudo-code that should work just as well as a demonstration.
for(0 => int i; i < numVoices; i++) {
SinOsc s => outs[i];
0.4 => s.gain; Std.mtof(60 + i) => s.freq;
}
spork ~ on(0, 1.0);
500::ms => now;
spork ~ on(1, 1.0);
300::ms => now;
spork ~ off(0);
spork ~ on(2, 1.0);
500::ms => now;
spork ~ off(1);
spork ~ off(2);
1::second => now;
'Course if there are better solutions out there, I'd love to hear them.
Posted May 12, 2008, in the early morning. Updated updated May 14, 2008, in the morning: Tried to better describe what the code was doing.