Music 220b: Winter 2001
Fernando Lopez-Lezcano, instructor
Christopher Burns, teaching assistant
Tamara Smyth, teaching assistant
Week 8: real-time CLM
Common Lisp Music, like its many predecessors (CSound, Music V, etc.), was designed for rendering sounds off-line -- after all, the field was born in an era of slow computers.... Thanks to Moore's Law, however, many synthesis techniques can be run in real-time, and CLM has been adapted to take advantage of the computing power now available to us.
In CLM, real-time instruments are usually comprised of three components: the instrument which renders the samples (similar to more traditional instrument functions), a program which handles user input (either from MIDI or a graphical interface), and a shared memory space which allows the other two components to communicate with one another. Let's look at each in turn, and then see how to run the complete example.
pinstruments: computing samples in parallel
Imagine an typical CLM (with-sound) call which uses several instruments:
(with-sound (:srate 44100 :channels 2 :statistics t) (fm-violin 0 4 400 0.1 :fm-index 2.3) (grani 0 8 3 "/usr/ccrma/web/courses/220b/lectures/2/sounds/small-gong.snd" :grain-duration 0.06 :grain-density 40 :grain-density-spread 5 :grain-start '(0 0.1 1 0.22) :srate -12) (twopole 2 4 0.3 :freq '(0 20 0.7 1000 1.0 40) :r '(0 0.5 1 0.99)))
Ordinarily CLM would render all the fm-violin samples, then all the grani samples, and finally all the twopole samples, mixing them together as a separate step. However, there's a special type of "parallel" instrument which works differently. If we use (with-psound), and call only instruments which have been defined with (defpinstrument), CLM will render and mix all the samples necessary for time 0 before moving on to time 1. The results are immediately streamed to the DAC for output -- as long as your instruments compute in faster than real-time, voila, no waiting!
Any instrument which writes its samples sequentially (as most do, although grani is a notable exception) can be made into a pinstrument just by changing (definstrument) to (defpinstrument). However, if we want real-time control over our instruments, and not just quicker output, we have to be able to pass the control data to the instrument.
The trick is to use the (control) and (fcontrol) functions to bring in the necessary data. After the appropriate (make-control) and (make-fcontrol) invocations, (control variable name) will import the value of a global variable; (fcontrol) does the same thing but filters the incoming data to smooth it (useful for sliders and other types of continuous controllers). Here's a simple example by Bill Schottstaedt:
;;; fm-forever is the instrument that watches the controls and makes simple fm sounds (defpinstrument fm-forever () (let ((carrier (make-oscil 0.0)) (modulator (make-oscil 0.0)) (ampf (make-fcontrol amp)) (freqf (make-fcontrol freq)) (indexf (make-fcontrol index))) (run (loop for i from 0 do #-just-lisp (declare (type integer freq amp fm-ratio index on-off)) (if (= (control on-off) 0.0) (loop-finish)) ;toggle "play" button off => quit (let ((frq (fcontrol freqf))) (outa i (* (fcontrol ampf) (oscil carrier (+ (in-hz frq) (* (fcontrol indexf) (oscil modulator (in-hz (* frq (control fm-ratio))))))))))))))
Note the use of (make-control) and (make-fcontrol) in the initialization phase, and (control) and (fcontrol) in the run loop -- the freqf, ampf, indexf, and fm-ratio parameters can all be updated in real-time by the user. Also, note that there's no end time specified in the loop -- the (loop-finish) function takes care of that, based upon the setting of the (control on-off) toggle switch.
(make-controller): a real-time interface
The second component of our real-time instrument is the interface. While CLM's graphical interface's aren't as sophisticated as those of Max, they're effective for simple tasks. The (make-controller) function does the dirty work:
(make-controller "bess" '(on-off "play" :toggle t) '(freq "carrier freq" :slider 50.0 1000.0) '(amp "amp" :slider 0.0 0.25) '(index "fm index" :slider 0.0 3.14159) '(fm-ratio "c/m ratio" :int-slider 0.0 10.0))
This example (by Bill Schottstaedt) makes a simple GUI called "bess." (Named after the Bessel functions, perhaps?) There are global variables (defined separately) named on-off, freq, amp, index, and fm-ratio; the widgets on the interface are named "play," "carrier freq," etc.; and the type of widget is specified by :toggle, :slider, etc.
shared memory: communicating between components
Now that we've created the parallel instrument and the user interface, we need to tie them together. The communication happens via a shared memory space; to continue our example:
;;; these are our control points (amplitude, frequency, fm c/m ratio as an integer, fm index, on/off switch) (defvar freq (control-allocate)) (defvar amp (control-allocate)) (defvar fm-ratio (control-allocate)) (defvar index (control-allocate)) (defvar on-off (control-allocate))
The (control-allocate) function creates shared memory for a numeric value by default; larger memory sizes can be specified with (control-allocate size). Note that we don't have to distinguish between (control) and (fcontrol) here -- our instrument function takes care of that difference.
making it all work
Here are the steps required to make the example run:
1. Compile and load the file (bess.cl) containing all three components.
CM(2): :cl bess ;;; Compiling file bess.cl ; Writing "/zap/clm_lnxacl_FM-FOREVER.c" ; Autoloading for FOREIGN-FUNCTIONS:CHAR*-TO-STRING: ; Fast loading from bundle code/ffcompat.fasl. ; Compiling "/zap/clm_lnxacl_FM-FOREVER.c" ; Creating shared object file "/zap/clm_lnxacl_FM-FOREVER.so" ;;; Writing fasl file bess.fusl Warning: No IN-PACKAGE form seen in /zap/bess.cl. (Allegro Presto will be ineffective when loading a file having no IN-PACKAGE form.) ;;; Fasl write complete ; Fast loading /zap/bess.fusl ; Foreign loading /zap/clm_lnxacl_FM-FOREVER.so.
2. Copy and paste the (make-controller) function to the lisp interpreter.
CM(3): (make-controller "bess" '(on-off "play" :toggle t) '(freq "carrier freq" :slider 50.0 1000.0) '(amp "amp" :slider 0.0 0.25) '(index "fm index" :slider 0.0 3.14159) '(fm-ratio "c/m ratio" :int-slider 0.0 10.0)) compiling "bess.c" 0
3. From a terminal window (not the lisp interpreter), start the interface program:
[cmn4 cburns] ~> /zap/bess &
4. Finally, issue a (with-psound) to the lisp interpreter:
(with-psound () (fm-forever))
There are a number of examples and resources for realtime CLM: see the relevant section of the manual and the "bess" examples:
As always, there are dozens of other possible applications....