Getting Started with Incudine | ||
---|---|---|
Part 1 | Part 2 |
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:
(flush-pending)
(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))
Home | Part 2 |