Control Waves and Guest Professorship at the Folkwang University, Essen

I’m just coming to the end of my second week of teaching in the Institute for Computer Music and Electronic Media (ICEM) at the Folkwang University of the Arts in Essen, Germany. It’s  a pleasure to be here, working with the dedicated and friendly students and staff.  I’ll be a Guest Professor until the end of 2016 and I’m already looking forward to coming back.

I’m compressing a lot of teaching into the time I spend here—each of these two weeks have involved around 30 hours of class time. A lot of this is one-on-one teaching—always a pleasure—and I’m learning a lot by talking through the generative approaches necessary to realising the students’ works.

control waves

One thing I developed in response to a student’s need turned out to be so generally applicable that it demanded to be incorporated into the slippery chicken project: control waves. Many of us use classic waveforms like sine and sawtooth to control data in our electronic compositions, particularly in real-time. The use of LFOs (low frequency oscillators) to modulate, say, the cutoff frequencies of filters, has been standard practice since the days of analogue synths. I’ve not had cause to add and use these in my own algorithmic composition work to date (though they’re in plenty of my mixes and improvisations), but I’ve now created a set of classes that can create and access such waveforms. You can find the code in the latest online slippery chicken repository and/or view it online.

CLM

The parent control-wave class takes advantage of the speed of Common Lisp Music (CLM) to generate all the control-rate sample points necessary for a complete piece. The idea is that you pass envelope descriptions of frequency and amplitude changes along with data to offset and scale the wave’s values as desired, then generate a waveform over the whole duration of a piece (with a default but user-selectable sample rate of 1000 points per second). You then query the waveform at any time point during the generation of, say, note, amplitude, rhythm, or any other parameter/data needed for your algorithmic piece.

The CLM code below is a rare example of an instrument that returns useful data from its code via run*—in this case an array of curve points. Most CLM instruments are aimed at writing sound files of course. We do that here almost as a side-effect, so that we can look at our lovely curves in a sound editor, for example. Our main interest though is in the array of data we get back from our implicit call to the CLM instrument made during the creation of our new control-wave object.

(definstrument ctlwav
    ;; frequency can also be an envelope
    (frequency duration &key
               ;; (make-fun #'make-oscil)
               ;; (gen-fun #'oscil)
               (time 0.0)
               ;; case in the run loop can't handle symbols hence integers here:
               ;; 1=sine, 2=cosine, 3=sawtooth, 4=triangle, 5=square,
               ;; 6=pulse(single sample of 1.0 follwed by zeros)
               (type 1)
               (initial-phase 0.0)      ; in radians
               (amplitude 1.0)
               (rescale nil) ; or '(minimum maximum)
               (amp-env '(0 1 100 1)))
  ;; for ease and at the expense of a little computation force a freq-env
  (unless (listp frequency)
    (setf frequency (list 0 frequency 100 frequency)))
  (let* ((beg (floor (* time *srate*)))
         ;; 1+ so we can access the value at  if necessary...it's
         ;; just one sample extra
         (dur-samps (1+ (ceiling (* duration *srate*))))
         (end (+ beg dur-samps))
         (samples (make-double-float-array dur-samps))
         (samp 0.0)
         (rsamp 0.0)
         (indx 0)
         (prop 0.0)
         (fm 0.0)
         (start-freq (second frequency))
         (fenv (make-env :envelope frequency :duration duration
                         :offset (- start-freq)))
         ;; square and pulse go from 0 to 1, the rest -1 to 1
         (wav-min (if (member type '(5 6)) 0.0 -1.0))
         (wav-range (if (zerop wav-min) 1 2))
         (out-min (when rescale (first rescale)))
         (out-max (when rescale (second rescale)))
         (out-range (when rescale (- out-max out-min)))
         (wav (funcall (case type
                         (1 #'make-oscil)
                         (2 #'make-oscil)
                         (3 #'make-sawtooth-wave)
                         (4 #'make-triangle-wave)
                         (5 #'make-square-wave)
                         (6 #'make-pulse-train)
                         (t (error "ctlwav: unknown wave type: ~a" type)))
                       :frequency start-freq :initial-phase initial-phase))
         (ampf (make-env :envelope amp-env :scaler amplitude
                         :duration duration)))
    (when (= type 2)                     ; cosine
      (setf (mus-phase wav) (mod (+ initial-phase (/ pi 2)) pi)))
    ;; save some computation in the run loop if we're not actually rescaling.
    (when (and (= wav-min out-min)
               (= out-max 1.0))
      (setf rescale nil))
    (run* (samples)
          (loop for i from beg to end do
               (setf fm (hz->radians (env fenv))
                     samp (* (env ampf)
                             ;; can't use funcall in run though apply should
                             ;; work (but doesn't for me here)
                             (case type
                               (1 (oscil wav fm))
                               (2 (oscil wav fm))
                               (3 (sawtooth-wave wav fm))
                               (4 (triangle-wave wav fm))
                               (5 (square-wave wav fm))
                               (6 (pulse-train wav fm))))
                     rsamp samp)
               (when rescale
                 (setf prop (/ (- samp wav-min) wav-range)
                       rsamp (+ out-min (* prop out-range))))
               (setf (aref samples indx) rsamp)
               (incf indx)
               (outa i samp)))
    samples))

The following example shows two control-wave classes in action: a sine wave to generate the sweep of notes through the harmonic series, and a sawtooth wave to control the rhythmic aspects:

(let* ((duration (* 3 60))
       (fundamental 100)
       (harmonics (make-control-sine 
                   :frequency '(0 .1 100 1)
                   :minimum 1 :maximum 21 :duration duration
                   :amp-env '(0 .5 20 .8 30 .4 50 .9 70 .5 100 1)))
       (delta (make-control-sawtooth
               :frequency '(0 .1 30 .2 50 .1 100 4)
               :minimum 0.05 :maximum .3 :duration duration
               :amp-env '(0 1 100 .1)))
       d h e
       (time 0.0)
       (events
        (loop while (<= time duration) do
             (setf d (get-data time delta)
                   h (get-data time harmonics)
                   ;; if we want frequency accuracy we should pass an
                   ;; already-created pitch object to make-event; converting to
                   ;; note symbols just will not do.
                   e (make-event (make-pitch (* (round h) fundamental))
                                 (* d .7) :duration t :start-time time))
             (incf time d)
           collect e)))
  (event-list-to-midi-file events :start-tempo 60))

…which creates this weird little MIDI file, full of just-intoned delights and a seemingly unevolving but constantly varying yet repetitive curve which almost unexpectedly ends up in a quite different place from where it started.

Share Button

← Previous Post

Next Post →

2 Comments

  1. Dear Michael,

    thank you for this implementation, can you please let me know what do the arguments for :frequency and :amp-env are? Or where are these specified? I have followed the source code but I have not been able to understand it.

    :frequency ‘(0 .1 100 1)
    :minimum 1 :maximum 21 :duration duration
    :amp-env ‘(0 .5 20 .8 30 .4 50 .9 70 .5 100 1)))

    • I can see the difficulty. Unlike most slippery-chicken objects, the control-waves and their make-* functions are implemented from the control-wave class, which doesn’t really exist by itself, rather is only meaningful as it emerges during instantiation of one of its subclasses. (I’ll leave that out there before I get us into more trouble with such waffle 😉

      In any case the documentation you need to look at is at the top of control-wave.lsp, specifically for each of the slots of (defclass control-wave …). So the frequency slot, here an envelope, has frequency values in Hertz as y-values and it will be stretched over the given duration (the :duration slot, in seconds); :amp-env is an amplitude envelope applied to the wave also over its whole duration. Minimum and maximum are then the y-value extremes (or sample values) to scale the generated wave to. Those scaled samples, generated over the whole duration given, are what are returned for you to then apply as a control signal for whatever parameters you want.

Leave a Reply

Your email address will not be published. Required fields are marked *