Hans, Eduard, community at large,
I think what Hans is asking is "If you want a note to continue after a
note-off event, what's a good way to program this in ChucK?"
Attached is how I do it.
The philosophy is simple: *Every* note-on event spawns a thread. That
thread patches in the UGs and plays sound until there's a note-off
event, at which it *begins* to shut down the note. Only after the
note actually ends does the the thread unpatch the UGs and kill itself.
That's the *philosophy*, but the implementation shown here deviates in
an important way: A thread (and related UGs) are created on demand,
but it is never killed: it blocks rather than terminates. And the
object associated with the thread is never discarded: it is put into a
resource pool and reused for the next note.
If you hit C4 twice in a row, it will actually spawn two threads --
this is exactly what you want for something with a long decay.
Don't let the length of the code scare you - most of it is comments.
- Rob
// File: ShredPoolExample2.ck
// ================================================================
//
// This program is free software: you can redistribute it and/or
modify it under
// the terms of the GNU General Public License as published by the
Free Software
// Foundation, either version 3 of the License, or (at your option)
any later
// version.
//
// This program is distributed in the hope that it will be useful, but
WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
or FITNESS
// FOR A PARTICULAR PURPOSE. See the GNU General Public License for
more
// details.
//
// You should have received a copy of the GNU General Public License
along with
// this program. If not, see http://www.gnu.org/licenses/.
//
// ================================================================
// About ShredPoolExample2:
//
// ShredPoolExample2.ck is a example for the ChucK audio programming
// and processing environment. It requires a MIDI keyboard or
// equivalent source of midi note on/note off events. It demonstrates
// three separate idioms:
//
// * How to implement a general purpose "resource pool". The basic
// idea is that if you need an object, and there's one in the pool,
// then you remove it from the pool and use it. If there isn't one
// in the pool, only then do you allocate one from memory. When you
// are done with the object, you return it to the pool. This way,
// you will initially allocate some objects from memory, but
// (hopefully) most of the time you'll be reusing ones you already
// allocated. This techique is useful in ChucK since it lacks a
// full garbage collection system.
//
// * How to create a Shred (thread) that can be started and stopped
// for reuse. This is useful because ChucK does not garbage collect
// shreads after they terminate, and memory will silt up.
//
// * One way to implement polyphony (including dynamic patching) and how
// to handle "overhang" after a midi note ends.
//
// ================================================================
// USAGE:
// chuck ShredPoolExample2.ck
//
// ================================================================
// Revision History
// 1.00 22-April-2009: Robert Poor
// Original release.
//
// ================================================================
// ================================================================
// Class: Element
//
// Element defines any object that may be stored in a ResourcePool.
// Any class that wants to be used in a resource pool must inherit
// from Element.
//
class Element {
ResourcePool @ _resource_pool;
fun ResourcePool get_resource_pool() {
return _resource_pool;
}
fun void set_resource_pool(ResourcePool @ resource_pool) {
resource_pool @=> _resource_pool;
}
}
// ================================================================
// ================================================================
// Class: ResourcePool
//
// ResourcePool maintains a "pool" of available elements. When you call
// allocate_element(), it will pull an element from the pool if any are
// available. If none are available, it calls create_element() to
// create one and returns it. When you are finished with the element,
// you MUST call release_element() to return it to the pool.
//
// Since you want create_element() to return your custom sub-class of
// Element, you must also create a sub-class of ResourcePool with a
// create_element() method to return one. (This is simpler than it
// sounds -- see the MistralPool sub-class later in this file.)
//
class ResourcePool {
Element @ _pool[0]; // the pool of available elements
// Remove and return an element from the pool, if available.
// otherwise call create_element() to create one from scratch
// and return it.
fun Element allocate_element() {
null @=> Element @ element;
if ((_pool.size() => int size) > 0) {
_pool[size-1] @=> element;
size-1 => _pool.size;
} else {
create_element() @=> element;
<<< "Creating new element", element, "for pool", this >>>;
element.set_resource_pool(this);
}
return element;
}
// Release an element to the pool
fun void release_element(Element element) {
_pool << element;
}
// This implementation of create_element() is a placeholder -- you
// must create a subclass of ResourcePool whose create_element()
// wil return an instance of your subclass of Element.
fun Element create_element() {
return new Element;
}
}
// ================================================================
// ================================================================
// Class: ReusableShred
//
// In ChucK, one a shred (thread) exits, there's no way to restart it
// or to reclaim its memory. Consequently, available memory will
// eventually fill up after you've created many shreds.
//
// ReusableShred avoids this by creating shreds that never exit. A
// ReusableShred has start() and stop() methods to make it start and
// stop. Most of the functionality, though, happens in three methods
// that you implement in a subclass:
//
// process_will_start()
// called (in the processing thread) after the start() function
// has been called, and just before the main processing loop starts.
// process()
// called (in the processing thread) once through each loop.
// Looping continues until some agency calls the stop() function.
// process_did_stop()
// called (in the processing thread) after the stop() function
// has been called, and just after the main processingn loop ends.
//
// Implementation note: The Shred is spork'd when it is created, and
// it sits waiting for a trigger event (provided by the start()
// function). After the stop() function is called, the thread reverts
// to waiting for the next call to start().
//
class ReusableShred extends Element {
Event _trigger;
int _runnable;
// _initialize() is called once when the ReusableShred is created.
fun void _initialize() {
spork ~ _shred_process();
// NB: This call to yield() is cruicial and subtle: we need to
// give the _shred_process() time enough to start and to block
// on the call to _trigger => now. Without the call to yeild,
// it is possible (and likely) that the user will call start()
// before the _shred_process() blocks, meaning it will miss
// the _trigger.broadcast() in start(), and so start() will
// fail (until you call it a second time). [This was the
// source of a bug until I figured out what was going wrong.]
me.yield();
}
_initialize();
// Start the processing thread.
fun void start() {
true => _runnable;
_trigger.broadcast();
}
// Stop the processing thread.
fun void stop() {
false => _runnable;
}
// Here is the entire processing thread. Notice that most of the
// work will happen in the subclassed definitions of
// process_will_start(), process() and process_did_stop().
//
fun void _shred_process() {
while (true) {
_trigger => now; // block here until start() is called
process_will_start();
while (_runnable) {
process();
}
process_did_stop();
}
}
// The following three functions may be subclassed (and at the very
// least, you are expected to subclass the process() function).
All
// three of these functions are called from the same thread as the
// _shred_process() loop.
// process_will_start() gets called just before the shred's process
// loop starts.
fun void process_will_start() { }
// process() is called each time in the processing loop.
fun void process() { 1::second => now; }
// process_did_stop() is called just after the processing loop has
// ended.
fun void process_did_stop() { }
}
// ================================================================
// ================================================================
// ================================================================
// Example code starts below
// ================================================================
// ================================================================
// Class: Mistral
//
// Mistral is an subclass of a ReusableShred. It shadows much of the
// functionality in the original class, though.
//
// Calling note_on() causes the shred process to call
process_will_start()
// and then to start waiting for call to note_off(). When note_off() is
// called, the shred process calls process_did_stop() which *initiates*
// the process of stopping the note -- but it doesn't actually stop it
// yet. Only after the note ends does the shred process unpatch the UGs
// and return the Mistral object to the pool of available objects for
// re-use.
NRev _ug_rev => dac;
.1 => _ug_rev.mix;
class Mistral extends ReusableShred {
Event _release;
Noise _ug_noise;
ResonZ _ug_resonz;
ADSR _ug_adsr;
1.0::second => dur RELEASE_TIME;
_ug_adsr.attackTime(0.025::second);
_ug_adsr.decayTime(0.05::second);
_ug_adsr.sustainLevel(0.3);
_ug_adsr.releaseTime(RELEASE_TIME);
// Set up most of the patch, but don't connect it to the DAC yet.
// We will complete the patch in process_will_start()
_ug_noise => _ug_resonz => _ug_adsr;
fun void note_on(int midi_key, int midi_velocity) {
_ug_resonz.freq(Std.mtof(midi_key));
_ug_noise.gain(midi_velocity/127.0);
_ug_resonz.Q(50.0);
_trigger.broadcast(); // start the shred process running
}
fun void note_off() {
_release.broadcast(); // "start stopping" the shred process
}
// We've shadowed the superclass definition of _shred_process with
// our own primarily because there's no processing to be done
// between a note on event and a note off event.
//
fun void _shred_process() {
while (true) {
_trigger => now; // block here until note_on() is called
process_will_start();
_release => now; // block here until note_off() is
called
process_did_stop();
}
}
// This gets called before we enter the shred processing loop.
fun void process_will_start() {
_ug_adsr => _ug_rev;
_ug_adsr.keyOn();
}
// This gets called just after stop() is called (but still
// in the processing shred(). Do whatever teardown needed.
fun void process_did_stop() {
// initiate the ramp down in the ADSR
_ug_adsr.keyOff();
// hang until the ADSR completes
RELEASE_TIME => now;
// dynamically unpatch the UGs
_ug_adsr =< _ug_rev;
// Since processing has finished, this is the best place to
// release this Mistral back to the resource pool. (Note that
// get_resource_pool() is defined in the Element superclass.)
get_resource_pool().release_element(this);
}
}
// ================================================================
// ================================================================
// Class: MistralPool
//
// The MistralPool subclass of ResourcePool exists soley to define the
// create_element() method, which just returns a new instance of a
// Mistral.
//
class MistralPool extends ResourcePool {
fun Element create_element() {
return new Mistral;
}
}
// ================================================================
// ================================================================
// ================================================================
// Top-level code starts here
MidiIn midi_in;
0 => int midi_device;
MidiMsg msg;
MistralPool resource_pool;
Mistral @ _active_notes[128]; // keep handles on active notes
// ================
// open the MIDI port
if (me.args() > 0) {
Std.atoi(me.arg(0)) => midi_device;
}
if (!midi_in.open(midi_device)) {
<<< "Can't open midi device #", midi_device >>>;
me.exit();
} else {
<<< "Opened midi device #", midi_device, "(", midi_in.name(), ")"
;
}
// ================
// "forever" loop starts here
while (true) {
midi_in => now; // block here until a midi event
arrives
while (midi_in.recv(msg)) {
// ignore everything but key press messages
if (isKeyPress(msg)) {
// extract key number and velocity from the midi message
msg.data2 => int midi_key;
msg.data3 => int midi_velocity;
// Theoretically, we can't get two note on events without
// seeing an intermediate note off event. But sometimes
// midi events are dropped (notably note off events). If
// two note on events happen in a row for the same key, we
// need to turn off the previous note, so we conditionally
// do it here:
note_off(midi_key);
if (midi_velocity > 0) {
// Here with a note-on event. We allocate a Mistral
// from the pool, start it playing, and save a handle
// to it in the _active_notes[] array so we can find
// it when it's time to call note_off()
note_on(midi_key, midi_velocity);
} else {
// Here with a note-off event. But we've already
// called note_off() above. So nothing to do here.
}
}
}
}
fun int isKeyPress(MidiMsg msg) {
return (msg.data1 == 144);
}
// Allocate a Mistral from the pool, send it a note_on message and
// stash a pointer to it in the _active_notes[] list so we know how
// to send it a note_off message when the midi event arrives.
fun void note_on(int midi_key, int midi_velocity) {
resource_pool.allocate_element() $ Mistral @=> Mistral @ mistral;
mistral.note_on(midi_key, midi_velocity);
mistral @=> _active_notes[midi_key];
}
// If there is an active note on this key, ask it do to a note_off()
// and remove it from the list of active notes. The mistral will take
// care of stopping its own processing and returning itself to the
// resource pool when the note actually completes.
//
fun void note_off(int midi_key) {
if (_active_notes[midi_key] != null) {
_active_notes[midi_key].note_off();
null @=> _active_notes[midi_key];
}
}
// EOF