[chuck-users] Reusable Thread, Resource Pools, Polyphony: all in one....

Robert Poor rdpoor at gmail.com
Wed Apr 22 02:59:58 EDT 2009


After seeing several people struggle with "how do I do polyphony", I  
thought I'd pass along the ChucK code for how I do it.  The example  
may seem like overkill, but it's clean and serviceable.

It's really three examples in one:

* How to do Resource Pools (useful in a ChucK world without a full  
Garbage Collector)
* How to not leak threads, er, I mean shreds (ditto)
* How do to polyphony (with dynamic patching and unpatching)

I ran this example for a long time and verified that it doesn't  
continually grow.

Here's the deal: it's a LONG code example (though most of it is  
comments).  If someone else wants to put in in a safe, friendly place  
for the ChucK community, I will thank you for doing so.  If you have  
questions about the code, I'm an easy mark and will do my best to  
answer (schedule permitting).  And finally, if you know how to make  
the code better, by all means, create a revision.

It should be obvious from the layout of the code that a "production"  
version would split this into three or four independent files, but it  
was easier to package everything in a single file for demoware.

Oh, a polyphonic MIDI version will follow within the next day or two.   
Share and enjoy...

- Rob

// File: ShredPoolExample1.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 ShredPoolExample1:
//
// ShredPoolExample1.ck is a standalone example for the ChucK audio
// programming and processing environment.  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).
//
// ================================================================
// USAGE:
//	chuck ShredPoolExample1.ck
//
// ================================================================
// Revision History
// 1.00	21-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 PlinkerPool 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, once 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 yield,
         // 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: Plinker
//
// Plinker is a subclass of a ReusableShred.  It defines the "big
// three" functions of ReusableShred: process_will_start(), process(),
// and process_did_stop().
//
// In this case, process_will_start() sets up parameters to play some
// repeated notes and patches a Mandolin into the signal chain.  The
// process() function plays a note (repeatedly), and calls stop() when
// the repetition is to end.  Finally, process_did_stop() unpatches
// the Mandolin from the signal chain.
//
// I'd like to think that David Jaffe would enjoy the results.

NRev _ug_rev => dac;
.1 => _ug_rev.mix;

class Plinker extends ReusableShred {

     // pick a note, any note.  but note, these dice are loaded.
     [0, 0, 0, 2, 4, 6, 7, 7, 9, 11 ] @=> int _scale[];

     Mandolin _mandolin;
     time _end_time;
     dur _plink_duration;
     float _freq;

     // This gets called before we enter the shred processing loop.
     fun void process_will_start() {
         now + Std.rand2f(3.0, 12.0)::second => _end_time;
         Std.rand2f(0.5, 0.05)::second => _plink_duration;
         Std.mtof(21 + Std.rand2(2, 5) * 12 +
                  _scale[Std.rand2(0, _scale.size()-1)]) => _freq;

         // dynamically patch UG into output
         _mandolin => _ug_rev;
     }

     // This gets called once every time through the loop.  Note
     // that it's perfectly safe to call stop() from within the
     // shred.
     fun void process() {
         if (now > _end_time) {
             // Stop the repeating note by calling stop(),
             // defined in the ReusableShred super-class
             stop();
         } else {
             _freq => _mandolin.freq;
             Std.rand2f(0.2, 0.9) => _mandolin.pluckPos;
             0.5 => _mandolin.pluck;
             _plink_duration => now;
         }
     }

     // This gets called just after stop() is called (but still
     // in the processing shred().  Do whatever teardown needed.
     fun void process_did_stop() {
         // dynamically unpatch the Mandolin
         _mandolin =< _ug_rev;
         // Since processing has finished, this is the best place to
         // release this Plinker back to the resource pool.  (Note that
         // get_resource_pool() is defined in the Element superclass.)
         get_resource_pool().release_element(this);
     }

}

// ================================================================
// ================================================================
// Class: PlinkerPool
//
// The PlinkerPool subclass of ResourcePool exists soley to define the
// create_element() method, which just returns a new instance of a
// Plinker.
//
class PlinkerPool extends ResourcePool {
     fun Element create_element() {
         return new Plinker;
     }
}

// ================================================================
// ================================================================
// ================================================================
// Top-level code starts here

PlinkerPool resource_pool;

while (true) {

     // Note: We allocate a Plinker from the pool here.  We release the
     // Plinker to the pool in the Plinker::process_did_stop() method
     // (see above).
     resource_pool.allocate_element() $ Plinker @=> Plinker @ plinker;

     // Start playing a note.  All the performance parameters are set
     // in Plinker::process_will_start() (see above).
     plinker.start();

     // Start a new Plinker every two seconds.
     2::second => now;
}

// EOF



More information about the chuck-users mailing list