[chuck-users] polyphony (what happens after note off)? was: Killing thread from without

Robert Poor rdpoor at gmail.com
Thu Apr 23 12:53:45 EDT 2009


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 <rdpoor at gmail.com>
//	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



More information about the chuck-users mailing list