common-lisplisp-macros

How to pass function to macro in common-lisp


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?


Solution

  • 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)))