This tutorial introduces the main features of Incudine through trivial examples.

The INCUDINE.SCRATCH package, nicknamed SCRATCH, can be used for the experiments. It inherits the symbols exported from INCUDINE, INCUDINE.VUG, INCUDINE.UTIL and INCUDINE.ANALYSIS packages.

CL-USER> (require :incudine)
CL-USER> (in-package :scratch)

A virtual unit generator (VUG) is a recipe to do something. It is named VUG instead of RECIPE, because it generally describes the ingredients and the steps to realize a unit generator for the sound synthesis. A digital signal processor (DSP) in this context is the "recipe processor", where a recipe is possibly the combination of more recipes and lisp functions.

The syntax of a VUG remembers the definition of a lisp function. For example:

(define-vug phasor (freq init)
  "Produce a normalized moving phase value with frequency FREQ and
initial value INIT (0 by default)."
  (:defaults 1 0)
  (with-samples ((phase init)
                 (inc (* freq *sample-duration*)))
    (prog1 phase
      (incf phase inc)
      (cond ((>= phase 1.0) (decf phase))
            ((minusp phase) (incf phase))))))

the WITH-SAMPLES macro is a shortcut for

(with ((phase init)
       (inc (* freq *sample-duration*)))
  (declare (type sample phase inc))
  ...

The SAMPLE type is DOUBLE-FLOAT by default. WITH is like LET* but it will create the bindings for the function called to initialize a DSP instance.

Generally, an argument in DEFINE-VUG is a list where the first element is the argument name and the second is the argument type. However, we often use arguments of type SAMPLE, so it is the default if we directly use the argument name instead of the list (name type). For example, we can also define the PHASOR VUG in the following way:

(define-vug phasor ((freq sample) (init sample))
  ...

The next example shows a combination of recipes: SINE VUG describes a sinusoidal oscillator defined by following the recipe for moving phase value, and 10-HARM VUG is a sum of ten harmonics obtained by repeating the recipe to prepare the oscillator.

(define-vug sine (freq amp phase)
  "High precision sine wave oscillator with frequency FREQ, amplitude
AMP and PHASE."
  (:defaults 440 1 0)
  (* amp (sin (+ (* +twopi+ (phasor freq 0)) phase))))

(define-vug 10-harm (freq)
  (macrolet ((sine-sum (n)
               `(+ ,@(mapcar (lambda (x)
                               `(sine (* freq ,x) ,(/ .3 n) 0))
                             (loop for i from 1 to n collect i)))))
    (sine-sum 10)))

The definition of a DSP (the "recipe processor") is similar:

(dsp! simple (freq amp)
  (out (sine freq amp 0)))

If you have sense of humor, see the ironic example Spaghetti aglio, olio e peperoncino.

The stereo version of SIMPLE is

(dsp! simple (freq amp)
  (with-samples ((in (sine freq amp 0)))
    (out in in)))

OUT is a VUG-MACRO (it is both a VUG and a macro) to write a signal to the audio hardware (in realtime) or to a soundfile (non-realtime) by using the AUDIO-OUT function. The OUT expansion is

(macroexpand-1 '(out one two three four))
(PROGN
 (INCF (AUDIO-OUT 0) (SAMPLE ONE))
 (INCF (AUDIO-OUT 1) (SAMPLE TWO))
 (INCF (AUDIO-OUT 2) (SAMPLE THREE))
 (INCF (AUDIO-OUT 3) (SAMPLE FOUR))
 (VALUES))

The utility FOREACH-CHANNEL iterates over the number of output channels with CURRENT-CHANNEL bound to each number of channel. We can rebind CURRENT-CHANNEL, for example:

(dsp! swap-channels ((buf buffer) rate start-pos (loop-p boolean))
  (foreach-channel
    (cout (let ((current-channel (if (zerop current-channel) 1 0)))
            (buffer-play buf rate start-pos loop-p #'stop)))))

The BUFFER-PLAY VUG depends on CURRENT-CHANNEL to play the content of a buffer.

The BUFFER-FRAME VUG-MACRO, defined in incudine/src/vug/buffer.lisp, is another useful example:

(define-vug-macro buffer-frame (buffer frame &key wrap-p interpolation)
  "Return the BUFFER FRAME.

If WRAP-P is T, wrap around if necessary.

INTERPOLATION is one of :LINEAR, :CUBIC or NIL (default)."
  (with-gensyms (bframe)
    `(vuglet ((,bframe ((buf buffer) frame (wrap-p boolean))
                (with ((channels (buffer-channels buf))
                       (frame (make-frame channels)))
                  (dochannels (current-channel channels)
                    (setf (frame-ref frame current-channel)
                          (buffer-read buf frame
                                       :wrap-p wrap-p
                                       :interpolation ,interpolation)))
                  frame)))
       (,bframe ,buffer ,frame ,wrap-p))))

COUT is similar to OUT but it is generally used in combination with FOREACH-CHANNEL. With only one argument, there aren't differences between OUT and COUT. However, when the arguments of COUT are more than one, the expansion of

(cout arg1 arg2 arg3 ...)

is

(incf (audio-out current-channel)
      (cond ((= current-channel 0) arg1)
            ((= current-channel 1) arg2)
            ((= current-channel 2) arg3)
            ...
            (t +sample-zero+)))

Example for stereo output:

(define-vug stereo (input)
  "Mix the INPUT into the first two output busses."
  (out input input))

and the alternative definition of SIMPLE is

(dsp! simple (freq amp)
  (stereo (sine freq amp 0)))

AUDIO-IN is the utility to read the input samples from the audio hardware (in realtime) or from a sound file (non-realtime).

Ok, create and play an instance of SIMPLE on the node 1

SCRATCH> (rt-start)
:STARTED
SCRATCH> (simple 440 .3 :id 1)
; No value

We can get and set the DSP parameter controls with CONTROL-VALUE

SCRATCH> (control-value 1 'freq)
440.0d0
SCRATCH> (setf (control-value 1 'freq) 880)
880
SCRATCH> (setf (control-value 1 'amp) .2)
0.2
SCRATCH> (control-value 1 'freq)
880.0d0
SCRATCH> (control-value 1 'amp)
0.2d0
SCRATCH> (control-list 1)
(880.0d0 0.2d0)

SET-CONTROL and SET-CONTROLS are another way to set the DSP parameter controls.

SCRATCH> (set-control 1 :freq 2500)
2500
SCRATCH> (control-value 1 'freq)
2500.0d0
SCRATCH> (set-controls 1 :freq 4000 :amp .1)
NIL
SCRATCH> (control-list 1)
(4000.0d0 0.1d0)
SCRATCH> (set-controls 1 :freq 100 :amp .35)
NIL
SCRATCH> (control-list 1)
(100.0d0 0.35d0)
SCRATCH> (control-names 1)
(FREQ AMP)

Here is a "classic" recursive function to control a DSP instance:

(defun simple-test (time)
  (set-controls 1 :freq (+ 100 (random 1000)) :amp (+ .1 (random .3)))
  (let ((next (+ time #[1 beat])))
    (at next #'simple-test next)))

SCRATCH> (simple-test (now))

The anaphoric macro AAT simplifies this type of functions:

(defun simple-test (time)
  (set-controls 1 :freq (+ 100 (random 1000)) :amp (+ .1 (random .3)))
  (aat (+ time #[1 beat]) #'simple-test it))

where IT is bound to the time, the first argument of AAT.

The first argument of AT is the time in samples from the start of realtime (or non realtime in BOUNCE-TO-DISK). The current time is

SCRATCH> (now)
4.2066944d7

The type of the time is SAMPLE and NOW also works inside the definition of a VUG, UGEN or DSP:

(dsp! sin-test (freq amp)
  (with-samples ((k (* +twopi+ freq *sample-duration*)))
    (out (* amp (sin (* k (now)))))))

SCRATCH> (sin-test 1234 .25 :id 2)
SCRATCH> (free 2)

The second argument of AT is a function and the rest of the arguments are passed to that function.

The utility TEMPO-SYNC returns the time in samples synchronized to a time period. It is equivalent to

(- (+ (now) period) (mod (now) period))

For example:

SCRATCH> (rt-eval (:return-value-p t) (list (now) (tempo-sync #[1 sec])))
(4.8462848d7 4.848d7)
SCRATCH> (rt-eval (:return-value-p t) (list (now) (tempo-sync #[1 sec])))
(4.8859136d7 4.8864d7)

RT-EVAL is a facility to evaluate a form in the realtime thread and to return the result if :RETURN-VALUE-P is T.

The reader syntax #[...] allows to enter the time in samples by using different units. The possible units are:

unit the symbol starts with examples
sample sa sa, samp, samps, samples
millisecond ms ms, msec
second s s, sec, seconds
minute mi mi, min, minutes
hour h h, hours
day d d, days
week w w, weeks
beat b b, beats
meter m m, meters

The number of beats depends on a TEMPO structure (the default is *TEMPO*).

SCRATCH> *sample-rate*
48000.0d0
SCRATCH> #[1 beat]
48000.0d0
SCRATCH> *tempo*
#<TEMPO 60.00>
SCRATCH> (bpm *tempo*)
60.0d0
SCRATCH> (setf (bpm *tempo*) 135)
135
SCRATCH> (bpm *tempo*)
135.0d0
SCRATCH> (bps *tempo*)
2.25d0
SCRATCH> (spb *tempo*)
0.4444444444444444d0
SCRATCH> #[1 beat]
21333.333333333332d0
SCRATCH> #[7/4 beat]
37333.33333333333d0
SCRATCH> (defvar my-tempo (make-tempo 180))
MY-TEMPO
SCRATCH> my-tempo
#<TEMPO 180.00>
SCRATCH> #[1 beat my-tempo]
16000.0d0
SCRATCH> #[1 beat]
21333.333333333332d0

The number of meters depends on the velocity of the sound in m/s at 22°C, 1 atmosfera (the default is *SOUND-VELOCITY*)

SCRATCH> *sound-velocity*
345.0d0
SCRATCH> #[1 meter]
139.1304347826087d0
SCRATCH> #[1 meter 340]
141.1764705882353d0

We can redefine the SIMPLE-TEST function and stop it in at least two possible ways:

  1. (flush-pending)
  2. (defun simple-test (time) time)

FLUSH-PENDING cancels all the scheduled events; if the optional argument TIME-STEP is a number, the evaluation of a pending event is forced every TIME-STEP samples.

PEAK-INFO takes a number of the channel (zero based) and returns two values, the peak and the number of samples out of range.

SCRATCH> (flush-pending)
SCRATCH> (peak-info 0)
0.6336748916078843d0
0
SCRATCH> (peak-info 1)
0.39901842299614415d0
0

Free the node 1

SCRATCH> (free 1)
SCRATCH> (reset-peak-meters)

There is a simple UGen in SuperCollider named Crackle, a noise generator based on a chaotic function. A possible VUG definition is

(define-vug crackle (param amp)
  (with-samples (y0 (y1 0.3d0) y2)
    (setf y0 (abs (- (* y1 param) y2 0.05d0))
          y2 y1 y1 y0)
    (* amp y0)))

We can filter that noise with a one-pole filter defined as

(define-vug pole (in coef)
  (with-samples (y1)
    (setf y1 (+ in (* coef y1)))))

Often the anaphoric ~ VUG-MACRO simplifies the code with feedback parameters (the symbol ~ is inspired by FAUST programming language). An alternative definition of the filter is

(define-vug pole (in coef)
  "One pole filter."
  (~ (+ in (* coef it))))

and another definition for CRACKLE is

(define-vug crackle (param amp init-value)
  "Noise generator based on a chaotic function with scale
factor AMP (1 by default).

The formula is

    y[n] = | param * y[n-1] - y[n-2] - 0.05 |

INIT-VALUE is the initial value of y and defaults to 0.3."
  (:defaults (incudine:incudine-missing-arg "PARAM") 1 .3)
  (* amp (~ (abs (- (* it param) (delay1 it) 0.05)) :initial-value init-value)))

Here is a DSP to play:

(dsp! crackle-test (f1 f2 amp param-fmod coef)
  (with-samples ((thresh (* amp .1)))
    (stereo (pole
              (sine (if (> (crackle (+ 1.9 (sine param-fmod .07 0)) amp)
                           thresh)
                        f1
                        f2)
                    amp 0)
              coef))))

SCRATCH> (crackle-test 440 880 .02 .5 .96 :id 1)

Is there consing during the synthesis? GET-BYTES-CONSED-IN is a rough estimate of the bytes consed in a period of x seconds. There is consing if GET-BYTES-CONSED-IN returns a value greater than zero. For example, the next 5 seconds

SCRATCH> (get-bytes-consed-in 5)
0

Good.

SCRATCH> (set-controls 1 :f1 800 :f2 80 :param-fmod .2)
SCRATCH> (set-controls 1 :f1 1234 :f2 3000 :param-fmod .1)
SCRATCH> (free 1)

The next example includes a VUG for stereo panning, a VUG for a stereo sinusoidal oscillator through wavetable lookup, and a DSP for the test:

(define-vug pan2 (in pos)
  "Stereo equal power panpot with position POS between 0 (left)
and 1 (right)."
  (with-samples ((alpha (* +half-pi+ pos))
                 (left (cos alpha))
                 (right (sin alpha)))
    (cond ((= current-channel 0) (* left in))
          ((= current-channel 1) (* right in))
          (t +sample-zero+))))

(defvar *sintab* (make-buffer 8192 :fill-function (gen:partials '(1))))

(define-vug sinosc (freq amp position)
  (pan2 (osc *sintab* freq amp 0 :linear) position))

(dsp! simple2 (freq amp pos)
  (foreach-frame (foreach-channel (cout (sinosc freq amp pos)))))

FOREACH-FRAME in SIMPLE2 VUG allows block by block processing. Now we restart the real-time thread with block size 64, play 8 DSPs and divide them between two groups:

SCRATCH> (block-size)
1
SCRATCH> (set-rt-block-size 64)
64
SCRATCH> (rt-status)
:STOPPED
SCRATCH> (block-size)
64
SCRATCH> (rt-start)
:STARTED
SCRATCH> (loop repeat 8 do (simple2 (+ 100 (random 3000)) .01 (random 1.0)))
SCRATCH> (dump (node 0))
group 0
    node 8
      SIMPLE2 1864.0d0 0.01d0 0.929777d0
    node 7
      SIMPLE2 2164.0d0 0.01d0 0.91501355d0
    node 6
      SIMPLE2 1673.0d0 0.01d0 0.55699635d0
    node 5
      SIMPLE2 2447.0d0 0.01d0 0.19508076d0
    node 4
      SIMPLE2 105.0d0 0.01d0 0.44206798d0
    node 3
      SIMPLE2 1424.0d0 0.01d0 0.93773544d0
    node 2
      SIMPLE2 2898.0d0 0.01d0 0.6700171d0
    node 1
      SIMPLE2 3008.0d0 0.01d0 0.2709539d0
NIL
SCRATCH> (make-group 100)
NIL
SCRATCH> (dograph (n)
           (unless (group-p n)
             (move n :head 100)))
NIL
SCRATCH> (make-group 200 :after 100)
NIL
SCRATCH> (dogroup (n (node 100))
           (when (> (node-id n) 4)
             (move n :tail 200)))
NIL
SCRATCH> (dump (node 0))
group 0
    group 100
        node 1
          SIMPLE2 3008.0d0 0.01d0 0.2709539d0
        node 2
          SIMPLE2 2898.0d0 0.01d0 0.6700171d0
        node 3
          SIMPLE2 1424.0d0 0.01d0 0.93773544d0
        node 4
          SIMPLE2 105.0d0 0.01d0 0.44206798d0
    group 200
        node 5
          SIMPLE2 2447.0d0 0.01d0 0.19508076d0
        node 6
          SIMPLE2 1673.0d0 0.01d0 0.55699635d0
        node 7
          SIMPLE2 2164.0d0 0.01d0 0.91501355d0
        node 8
          SIMPLE2 1864.0d0 0.01d0 0.929777d0
NIL
SCRATCH> (move 200 :before 100)
NIL
SCRATCH> (dump (node 0))
group 0
    group 200
        node 5
          SIMPLE2 2447.0d0 0.01d0 0.19508076d0
        node 6
          SIMPLE2 1673.0d0 0.01d0 0.55699635d0
        node 7
          SIMPLE2 2164.0d0 0.01d0 0.91501355d0
        node 8
          SIMPLE2 1864.0d0 0.01d0 0.929777d0
    group 100
        node 1
          SIMPLE2 3008.0d0 0.01d0 0.2709539d0
        node 2
          SIMPLE2 2898.0d0 0.01d0 0.6700171d0
        node 3
          SIMPLE2 1424.0d0 0.01d0 0.93773544d0
        node 4
          SIMPLE2 105.0d0 0.01d0 0.44206798d0
NIL
SCRATCH> (pause 100)
#<NODE :ID 100>
SCRATCH> (unpause 100)
#<NODE :ID 100>
SCRATCH> (stop 100)
; No value
SCRATCH> (stop 200)
; No value
SCRATCH> (dump (node 0))
group 0
    group 200
    group 100
NIL
SCRATCH> (free 200)
SCRATCH> (dump (node 0))
group 0
    group 100
NIL
SCRATCH> (free 0)
SCRATCH> (dump (node 0))
group 0
NIL
SCRATCH> (set-rt-block-size 1)
SCRATCH> (rt-start)

ALL-VUG-NAMES returns the list of the defined VUG's.

The definitions of the built-in virtual unit generators are in incudine/src/vug/ directory. Some of them are flexible but complicated VUG-MACROs. For example, OSC is an unique VUG for a wavetable lookup oscillator with modulable amplitude, frequency and/or phase, and selectionable interpolation (:LINEAR, :CUBIC or NIL).

(dsp! osc-cubic-test (freq amp pos)
  (foreach-channel
    (cout (pan2 (osc *sine-table* freq amp 0 :cubic) pos))))

BUZZ and GBUZZ can use wavetable lookup oscillators with selectionable interpolation or sin/cos functions, and it is possible to change the lag-time for the crossfade when the number of the harmonics changes.

(dsp! buzz-test-1 (freq amp (nh fixnum))
  (stereo (buzz freq amp nh)))

(dsp! buzz-test-2 (freq amp (nh fixnum))
  (stereo (buzz freq amp nh :interpolation :cubic)))

(dsp! buzz-test-3 (freq amp (nh fixnum))
  (stereo (buzz freq amp nh :table-lookup-p nil)))

(dsp! gbuzz-test-1 (freq amp (nh fixnum) (lh fixnum) mul)
  (stereo (gbuzz freq amp nh lh mul)))

(dsp! gbuzz-test-2 (freq amp mul)
  (stereo (gbuzz freq amp (sample->fixnum (lag (lin-mouse-x 1 50) .02))
                 (sample->fixnum (lin-mouse-y 1 20))
                 mul :table-lookup-p nil :harm-change-lag .05)))

(dsp! gbuzz-test-3 (freq amp (nh fixnum) (lh fixnum) fmod amod)
  (stereo (gbuzz freq amp nh lh (sine fmod amod 0)
                 :interpolation :cubic)))

The RAND VUG-MACRO is the recipe for a noise generator with a random number distribution provided by GNU Scientific Library (GSL). The utility ALL-RANDOM-DISTRIBUTIONS returns the list of the available random number distributions (the keyword names of a sublist are aliases for the same distribution).

SCRATCH> (all-random-distributions)
((:LINEAR :LOW :LOW-PASS :LP) (:HIGH :HIGH-PASS :HP)
 (:TRIANGULAR :TRI :TRIANG :MEAN) (:GAUSS :GAUSSIAN)
 (:GAUSS-TAIL :GAUSSIAN-TAIL) (:EXP :EXPON :EXPONENTIAL)
 (:LAPLACE :BIEXP :BIEXPON :BIEXPONENTIAL) (:EXPPOW :EXPONENTIAL-POWER)
 (:CAUCHY) (:RAYLEIGH) (:RAYLEIGH-TAIL) (:LANDAU) (:LEVY) (:LEVY-SKEW) (:GAMMA)
 (:UNI :UNIFORM :FLAT) (:LOGNORMAL) (:CHISQ :CHI-SQUARED) (:F :FDIST)
 (:T :TDIST) (:BETA) (:LOGISTIC) (:PARETO) (:WEIBULL) (:GUMBEL1) (:GUMBEL2)
 (:POISSON) (:BERNOULLI) (:BINOMIAL) (:NEGATIVE-BINOMIAL) (:PASCAL)
 (:GEOM :GEOMETRIC) (:HYPERGEOM :HYPERGEOMETRIC) (:LOG :LOGARITHMIC))

Examples:

(dsp! gauss-test (sigma)
  (stereo (rand :gauss :sigma sigma)))

(dsp! cauchy-test (freq amp pos)
  (foreach-channel
    (cout (pan2 (sine (+ freq (rand :cauchy :a (lag (lin-mouse-x 1 50) .02)))
                      amp 0)
                pos))))

SCRATCH> (gauss-test .15 :id 1)
SCRATCH> (cauchy-test 300 .5 .5 :replace 1)
SCRATCH> (free 1)

It is really simple to define the interpolated version of any generator:

;;; Csound opcode in just two lines.
(define-vug randi (amp freq)
  (* amp (interpolate (white-noise 1) freq)))

;;; SC3 ugen in two lines.
(define-vug lfdnoise3 (amp freq)
  (* amp (interpolate (white-noise 1) freq :cubic)))

;;; Gaussian distribution random generator with cubic interpolation.
(define-vug gauss-cubic (sigma freq)
  (interpolate (rand :gauss :sigma sigma) freq :cubic))

;;; Chaotic ugen with selectionable interpolation.
(define-vug henon-cubic (freq a b x0 x1)
  (henon freq a b x0 x1 :cubic))

Sourceforge project page