I'm trying to wrap my head around macros in Common Lisp, and I have this piece of code to create synth-definitions with the cl-collider client for the supercollider sound synthesis language:
(defmacro make-defsynth (name &key args sig (num-channels 2) (env :asr) (pan t))
"Macro for creating defsynths. Defines some common args: out,
pan, gate, as well as setting up the envelope according to the env
argument. Valid envelopes are :asr and :perc. If pan is nil, then
the signal will not be panned, but passed directly through."
(let ((pan-func nil))
(case num-channels
(2 (setf pan-func '(pan2.ar sig pan)))
(4 (setf pan-func '(pan4.ar sig pan)))
(6 (setf pan-func '(pan-az.ar sig pan))))
`(defsynth ,name (,@args (attack 0) (release 0)
(out 0) (pan 0) (amp 1) (gate 1))
(let* ((env (env-gen.kr (asr attack 1 release)
:gate gate :act :free))
,@sig
(sig (* sig env amp))
(sig (eval pan-func)))
(out.ar out sig)))))
I would like to use this macro to create a synthdef like this:
(make-defsynth playbuf-mono
:args ((buf 0)
(rate 1)
(start-pos 0)
(loop 0))
:sig ((sig (play-buf.ar 1 buf rate
:start-pos start-pos
:loop loop))))
I get the error:
The variable PAN-FUNC is unbound.
[Condition of type UNBOUND-VARIABLE]
The question then is: how to get the pan-func to expand correctly depending on num-channels
?
The form (eval pan-func)
will not be evaluated at macro-expansion time and it would not work anyway, since EVAL
can't see local lexical bindings.
You want the code to be inserted
(defmacro make-defsynth (name &key args sig
(num-channels 2) (env :asr) (pan t))
(let ((pan-func nil))
(case num-channels
(2 (setf pan-func '(pan2.ar sig pan)))
(4 (setf pan-func '(pan4.ar sig pan)))
(6 (setf pan-func '(pan-az.ar sig pan))))
`(defsynth ,name (,@args
(attack 0) (release 0) (out 0) (pan 0) (amp 1) (gate 1))
(let* ((env (env-gen.kr (asr attack 1 release)
:gate gate :act :free))
,@sig
(sig (* sig env amp))
(sig ,pan-func))
(out.ar out sig)))))
You would now need to think about where env
and pan
should be used and where the binding should come from. In above code now the env
and pan
arguments of make-defsynth
are unused.
Style: I would not call it make-defsynth
. make-something
implies that something is made at runtime. But this is a definition macro, expanded at macro-expansion/compile-time.
Use macroexpand
, macroexpand-1
and pprint
to debug the code, by checking the code you are creating with the macro:
CL-USER 35 > (defmacro make-defsynth (name &key args sig
(num-channels 2) (env :asr) (pan t))
(let ((pan-func nil))
(case num-channels
(2 (setf pan-func '(pan2.ar sig pan)))
(4 (setf pan-func '(pan4.ar sig pan)))
(6 (setf pan-func '(pan-az.ar sig pan))))
`(defsynth ,name (,@args
(attack 0) (release 0) (out 0) (pan 0) (amp 1) (gate 1))
(let* ((env (env-gen.kr (asr attack 1 release)
:gate gate :act :free))
,@sig
(sig (* sig env amp))
(sig ,pan-func))
(out.ar out sig)))))
MAKE-DEFSYNTH
Then debugging it:
CL-USER 36 > (pprint
(macroexpand-1
'(make-defsynth playbuf-mono
:args ((buf 0)
(rate 1)
(start-pos 0)
(loop 0))
:sig ((sig (play-buf.ar 1 buf rate
:start-pos start-pos
:loop loop))))))
(DEFSYNTH
PLAYBUF-MONO
((BUF 0)
(RATE 1)
(START-POS 0)
(LOOP 0)
(ATTACK 0)
(RELEASE 0)
(OUT 0)
(PAN 0)
(AMP 1)
(GATE 1))
(LET* ((ENV (ENV-GEN.KR (ASR ATTACK 1 RELEASE) :GATE GATE :ACT :FREE))
(SIG (PLAY-BUF.AR 1 BUF RATE :START-POS START-POS :LOOP LOOP))
(SIG (* SIG ENV AMP))
(SIG (PAN2.AR SIG PAN)))
(OUT.AR OUT SIG)))