Observations

Keeping it safe with macros

Feb 02, 2021


#racket #macro

Keeping it safe with macros

Playing around with the excellent pure data, embedding it in a racket framework to get the extra super freedom of expression. How to abstract away all the C level details? Syntax parameters!

What is pure data?

Visual programming environment for multimedia, mostly sound. Really great because of very fast feedback - hear changes live. Also it's easier to see sound transformations wired on a patchboard.

There are some limitations, and some operations are not easily handled in pure data. Possible to interface from pd outside, and from outside to pd - libpd.

Embedding pure data patches

How better to complement a fast prototyping framework than with the best LISP? Enter racket.

To send some data and export a sound from pd instance, a naive implementation yields the following:

(let ([pd (make-pd-instance)])
 (begin
  (pd-instance-open-project-patch pd "kick.pd")
  (pd-instance-send-float pd "kick_transient" 0.035)
  (pd-instance-send-float pd "kick_max_pitch" 292.2)
  (pd-instance-send-float pd "kick_fall_time_1" 17.59)
  (pd-instance-send-float pd "kick_break_pitch" 83.89)
  (pd-instance-send-float pd "kick_fall_time_2" 1042.0)
  (pd-instance-send-float pd "kick_amp_decay" 1120.0)
  (pd-instance-tick pd)
  (pd-instance-send-bang pd "kick_bang")
  (begin0
    (pd-instance-export pd 5)
    #| ... |#
    #| close and free everything ... |#
    #| ... |#
    )))

Gritty, chaotic, noisy and annoying. Very C. A lot is left to be desired here, especially since we have all these awesome metaprogramming features available in racket.

What

What can be improved?

How

Now to implement it.

Memory management

(define executor (make-will-executor))
(void (thread (lambda ()
                (let loop ()
                  (will-execute executor)
                  (loop)))))

(define (pd-instance-free pd)
  (libpd_free_instance pd))

#| after creating a pd-instance: |#
(will-register executor instance pd-instance-free)

That should be enough to automatically the free once the pd-instance goes out of scope.

Isolated context

To create the pd-instance, pass it where necessary and keep the pd specific functions only accesible in this context, now this is asking for quite a lot. Better bring out the big guns! Syntax parameters will let us dynamically re-bind syntax to different expressions depending on context.

At first we define empty syntax parameters (these will result in an exception if evaluated):

(require
  racket/stxparam
  racket/splicing)

(define-syntax-parameter open-project-patch (syntax-rules ()))
(define-syntax-parameter send-float (syntax-rules ()))
(define-syntax-parameter send-bang (syntax-rules ()))
(define-syntax-parameter tick (syntax-rules ()))
(define-syntax-parameter export-rsound (syntax-rules ()))

Ideally these would throw custom exceptions, or result in a noop, or a sane default behavior.

And the context macro to substitute the parameters:

(define-syntax with-pd-instance
  (syntax-rules ()
    [(_ (action args ...) ...)
     (let ([pd* (make-pd-instance)])
       (splicing-syntax-parameterize
         ([open-project-patch (lambda (stx) #'(curry pd-instance-open-project-patch pd*))]
          [send-float (lambda (stx) #'(curry pd-instance-send-float pd*))]
          [send-bang (lambda (stx) #'(curry pd-instance-send-bang pd*))]
          [tick (lambda (stx) #'(lambda () (curry pd-instance-tick pd*)))]
          [export-rsound (lambda (stx) #'(curry pd-instance-export-rsound pd*))])
         (begin
           ((action) args ...) ...)))]))

When expanded, this will create a new pd-instance, set the syntax parameters to pass the instance to the underlying function, and unwrap the body of the macro otherwise unchanged. (The freeing of the instance is already handled by the will-executor.)

Using the syntax parameters also guarantees that our macros are hygienic.

The final form

Now this:

(let ([pd (make-pd-instance)])
 (begin
  (pd-instance-open-project-patch pd "kick.pd")
  (pd-instance-send-float pd "kick_transient" 0.035)
  (pd-instance-send-float pd "kick_max_pitch" 292.2)
  (pd-instance-send-float pd "kick_fall_time_1" 17.59)
  (pd-instance-send-float pd "kick_break_pitch" 83.89)
  (pd-instance-send-float pd "kick_fall_time_2" 1042.0)
  (pd-instance-send-float pd "kick_amp_decay" 1120.0)
  (pd-instance-tick pd)
  (pd-instance-send-bang pd "kick_bang")
  (begin0
    (pd-instance-export pd 5)
    #| ... |#
    #| close and free everything ... |#
    #| ... |#
    )))

becomes:

(with-pd-instance
  (open-project-patch "kick.pd")
  (send-float "kick_transient" 0.035)
  (send-float "kick_max_pitch" 292.2)
  (send-float "kick_fall_time_1" 17.59)
  (send-float "kick_break_pitch" 83.89)
  (send-float "kick_fall_time_2" 1042.0)
  (send-float "kick_amp_decay" 1120.0)
  (tick)
  (send-bang "kick_bang")
  (export 5))