audacia/nyquist/seq.lsp

337 lines
14 KiB
Common Lisp

;; seq.lsp -- sequence control constructs for Nyquist
;; get-srates -- this either returns the sample rate of a sound or a
;; vector of sample rates of a vector of sounds
;;
(defun get-srates (sounds)
(cond ((arrayp sounds)
(let ((result (make-array (length sounds))))
(dotimes (i (length sounds))
(setf (aref result i) (snd-srate (aref sounds i))))
result))
(t
(snd-srate sounds))))
; These are complex macros that implement sequences of various types.
; The complexity is due to the fact that a behavior within a sequence
; can reference the environment, e.g. (let ((p 60)) (seq (osc p) (osc p)))
; is an example where p must be in the environment of each member of
; the sequence. Since the execution of the sequence elements are delayed,
; the environment must be captured and then used later. In XLISP, the
; EVAL function does not execute in the current environment, so a special
; EVAL, EVALHOOK must be used to evaluate with an environment. Another
; feature of XLISP (see evalenv.lsp) is used to capture the environment
; when the seq is first evaluated, so that the environment can be used
; later. Finally, it is also necessary to save the current transformation
; environment until later.
;
; The SEQ implementation passes an environment through closures that
; are constructed to evaluate expressions. SEQREP is similar, but
; the loop variable must be incremented and tested.
;
; Other considerations are that SEQ can handle multi-channel sounds, but
; we don't know to call the snd_multiseq primitive until the first
; SEQ expression is evaluated. Also, there's no real "NIL" for the end
; of a sequence, so we need serveral special cases: (1) The sequences
; is empty at the top level, so return silence, (2) There is one
; expression, so just evaluate it, (3) there are 2 expressions, so
; return the first followed by the second, (4) there are more than
; 2 expressions, so return the first followed by what is effectively
; a SEQ consisting of the remaining expressions.
;; SEQ-EXPR-EXPAND - helper function, expands expression to push/pop entry
;; on *sal-call-stack* to help debug calls into SAL from lazy evaluation
;; of SAL code by SEQ
(defun seq-expr-expand (expr source)
(if *sal-call-stack*
`(prog2 (sal-trace-enter '(,(strcat "Expression in " source ":") ,expr))
,expr ;; here is where the seq behavior is evaluated
(sal-trace-exit))
expr))
(defun with%environment (env expr)
;; (progv (var1 ...) (val1 ...) expression-list)
`(progv ',*environment-variables* ,env ,expr))
;(trace with%environment seq-expr-expand)
(defmacro eval-seq-behavior (beh source)
;(tracemacro 'eval-seq-behavior (list beh source)
(seq-expr-expand (with%environment 'nyq%environment
`(at-abs t0
(force-srates s%rate ,beh))) source));)
;; Previous implementations grabbed the environment and passed it from
;; closure to closure so that each behavior in the sequence could be
;; evaluated in the saved environment using an evalhook trick. This
;; version precomputes closures, which avoids using evalhook to get or
;; use the environment. It's still tricky, because each behavior has
;; to pass to snd-seq a closure that computes the remaining behavior
;; sequence. To do this, I use a recursive macro to run down the
;; behavior sequence, then as the recursion unwinds, construct nested
;; closures that all capture the current environment. We end up with a
;; closure we can apply to the current time to get a sound to return.
;;
(defmacro seq (&rest behlist)
;; if we have no behaviors, return zero
(cond ((null behlist)
'(snd-zero (local-to-global 0) *sound-srate*))
(t ; we have behaviors. Must evaluate one to see if it is multichan:
`(let* ((first%sound ,(seq-expr-expand (car behlist) "SEQ"))
(s%rate (get-srates first%sound))
(nyq%environment (nyq:the-environment)))
; if there's just one behavior, we have it and we're done:
,(progn (setf behlist (cdr behlist))
(if (null behlist) 'first%sound
; otherwise, start the recursive construction:
`(if (arrayp first%sound)
(seq2-deferred snd-multiseq ,behlist)
(seq2-deferred snd-seq ,behlist))))))))
;; seq2-deferred uses seq2 and seq3 to construct nested closures for
;; snd-seq. It is deferred so that we can first (in seq) determine whether
;; this is a single- or multi-channel sound before recursively constructing
;; the closures, since we only want to do it for either snd-seq or
;; snd-multiseq, but not both. It simply calls seq2 to begin the expansion.
;;
(defmacro seq2-deferred (seq-prim behlist)
(seq2 seq-prim behlist))
#|
;; for debugging, you can replace references to snd-seq with this
(defun snd-seq-trace (asound aclosure)
(princ "Evaluating SND-SEQ-TRACE instead of SND-SEQ...\n")
(format t " Sound argument is ~A\n" asound)
(princ " Closure argument is:\n")
(pprint (get-lambda-expression aclosure))
(princ " Calling SND-SEQ ...\n")
(let ((s (snd-seq asound aclosure)))
(format t " SND-SEQ returned ~A\n" s)
s))
;; also for debugging, you can uncomment some tracemacro wrappers from
;; macro definitions. This function prints what the macro expands to
;; along with name and args (which you add by hand to the call):
(defun tracemacro (name args expr)
(format t "Entered ~A with args:\n" name)
(pprint args)
(format t "Returned from ~A with expression:\n" name)
(pprint expr)
expr)
|#
;; we have at least 2 behaviors so we need the top level call to be
;; a call to snd-multiseq or snd-seq. This macro constructs the call
;; and uses recursion with seq3 to construct the remaining closures.
;;
(defun seq2 (seq-prim behlist)
`(,seq-prim first%sound
(prog1 ,(seq3 seq-prim behlist) ; <- passed to seq-prim
;; we need to remove first%sound from the closure
;; to avoid accumulating samples due to an unnecessary
;; reference:
(setf first%sound nil))))
;; construct a closure that evaluates to a sequence of behaviors.
;; behlist has at least one behavior in it.
;;
(defun seq3 (seq-prim behlist)
`(lambda (t0)
(setf first%sound (eval-seq-behavior ,(car behlist) "SEQ"))
,(progn (setf behlist (cdr behlist))
(if (null behlist) 'first%sound
(seq2 seq-prim behlist)))))
; we have to use the real loop variable name since it could be
; referred to by the sound expression, so we avoid name collisions
; by using % in all the macro variable names
;
(defmacro seqrep (loop-control snd-expr)
;(tracemacro "SEQREP" (list loop-control snd-expr)
`(let ((,(car loop-control) 0)
(loop%count ,(cadr loop-control))
(nyq%environment (nyq:the-environment))
s%rate seqrep%closure)
; note: s%rate will tell whether we want a single or multichannel
; sound, and what the sample rates should be.
(cond ((not (integerp loop%count))
(error "bad argument type" loop%count))
((< loop%count 1)
(snd-zero (local-to-global 0) *sound-srate*))
((= loop%count 1)
,snd-expr)
(t ; more than 1 iterations
(setf loop%count (1- loop%count))
(setf first%sound ,snd-expr)
(setf s%rate (get-srates first%sound))
(setf nyq%environment (nyq:the-environment))
(if (arrayp first%sound)
(seqrep2 snd-multiseq ,loop-control ,snd-expr)
(seqrep2 snd-seq ,loop-control ,snd-expr))))));)
(defmacro seqrep2 (seq-prim loop-control snd-expr)
;(tracemacro "SEQREP2" (list seq-prim loop-control snd-expr)
`(progn (setf seqrep%closure
(lambda (t0) ,(seqrep-iterate seq-prim loop-control snd-expr)))
(,seq-prim (prog1 first%sound (setf first%sound nil))
seqrep%closure)));)
(defun seqrep-iterate (seq-prim loop-control snd-expr)
(setf snd-expr `(eval-seq-behavior ,snd-expr "SEQREP"))
`(progn
(setf ,(car loop-control) (1+ ,(car loop-control))) ; incr. loop counter
(if (>= ,(car loop-control) loop%count) ; last iteration
,snd-expr
(,seq-prim ,snd-expr seqrep%closure))))
;; TRIGGER - sums instances of beh which are launched when input becomes
;; positive (> 0). New in 2021: input is resampled to *sound-srate*.
;; As before, beh sample rates must match, so now they must also be
;; *sound-srate*. This implementation uses eval-seq-behavior to create
;; a more helpful stack trace for SAL.
(defmacro trigger (input beh)
`(let* ((nyq%environment (nyq:the-environment))
(s%rate *sound-srate*))
(snd-trigger (force-srate *sound-srate* ,input)
#'(lambda (t0) (eval-seq-behavior ,beh "TRIGGER")))))
;; EVENT-EXPRESSION -- the sound of the event
;;
(setfn event-expression caddr)
;; EVENT-HAS-ATTR -- test if event has attribute
;;
(defun event-has-attr (note attr)
(expr-has-attr (event-expression note)))
;; EXPR-SET-ATTR -- new expression with attribute = value
;;
(defun expr-set-attr (expr attr value)
(cons (car expr) (list-set-attr-value (cdr expr) attr value)))
(defun list-set-attr-value (lis attr value)
(cond ((null lis) (list attr value))
((eq (car lis) attr)
(cons attr (cons value (cddr lis))))
(t
(cons (car lis)
(cons (cadr lis)
(list-set-attr-value (cddr lis) attr value))))))
;; EXPAND-AND-EVAL-EXPR -- evaluate a note, chord, or rest for timed-seq
;;
(defun expand-and-eval-expr (expr)
(let ((pitch (member :pitch expr)))
(cond ((and pitch (cdr pitch) (listp (cadr pitch)))
(setf pitch (cadr pitch))
(simrep (i (length pitch))
(eval (expr-set-attr expr :pitch (nth i pitch)))))
(t
(eval expr)))))
;; (timed-seq '((time1 stretch1 expr1) (time2 stretch2 expr2) ...))
;; a timed-seq takes a list of events as shown above
;; it sums the behaviors, similar to
;; (sim (at time1 (stretch stretch1 expr1)) ...)
;; but the implementation avoids starting all expressions at once
;;
;; Notes: (1) the times must be in increasing order
;; (2) EVAL is used on each event, so events cannot refer to parameters
;; or local variables
;;
;; If score events are very closely spaced (< 1020 samples), the block
;; overlap can cause a ripple effect where to complete one block of the
;; output, you have to compute part of the next score event, but then
;; it in turn computes part of the next score event, and so on, until
;; the stack overflows (if you have 1000's of events).
;;
;; This is really a fundamental problem in Nyquist because blocks are
;; not aligned. To work around the problem (but not totally solve it)
;; scores are evaluated up to a length of 100. If there are more than
;; 100 score events, we form a balanced tree of adders so that maybe
;; we will end up with a lot of sound in memory, but at least the
;; stack will not overflow. Generally, we should not end up with more
;; than 100 times as many blocks as we would like, but since the
;; normal space required is O(1), we're still using constant space +
;; a small constant * log(score-length).
;;
(setf MAX-LINEAR-SCORE-LEN 100)
(defun timed-seq (score)
(must-be-valid-score "TIMED-SEQ" score)
(let ((len (length score))
pair)
(cond ((< len MAX-LINEAR-SCORE-LEN)
(timed-seq-linear score))
(t ;; split the score -- divide and conquer
(setf pair (score-split score (/ len 2)))
(sum (timed-seq (car pair)) (timed-seq (cdr pair)))))))
;; score-split -- helper function: split score into two, with n elements
;; in the first part; returns a dotted pair
(defun score-split (score n)
;; do the split without recursion to avoid stack overflow
;; algorithm: modify the list destructively to get the first
;; half. Copy it. Reassemble the list.
(let (pair last front back)
(setf last (nthcdr (1- n) score))
(setf back (cdr last))
(rplacd last nil)
(setf front (append score nil)) ; shallow copy
(rplacd last back)
(cons front back)))
;; TIMED-SEQ-LINEAR - check to insure that times are strictly increasing
;; and >= 0 and stretches are >= 0
(defun timed-seq-linear (score)
(let ((start-time 0) error-msg rslt)
(dolist (event score)
(cond ((< (car event) start-time)
(error (format nil
"Out-of-order time in TIMED-SEQ: ~A, consider using SCORE-SORT"
event)))
((< (cadr event) 0)
(error (format nil "Negative stretch factor in TIMED-SEQ: ~A" event)))
(t
(setf start-time (car event)))))
;; remove rests (a rest has a :pitch attribute of nil)
(setf score (score-select score #'(lambda (tim dur evt)
(expr-get-attr evt :pitch t))))
(cond ((and score (car score)
(eq (car (event-expression (car score))) 'score-begin-end))
(setf score (cdr score)))) ; skip score-begin-end data
(cond ((null score) (s-rest 0))
(t
(at (caar score)
(seqrep (i (length score))
(progn
(cond (*sal-call-stack*
(sal-trace-enter (list "Score event:" (car score)) nil nil)
(setf *sal-line* 0)))
(setf rslt
(cond ((cdr score)
(prog1
(set-logical-stop
(stretch (cadar score)
(expand-and-eval-expr (caddar score)))
(- (caadr score) (caar score)))
(setf score (cdr score))))
(t
(stretch (cadar score) (expand-and-eval-expr
(caddar score))))))
(if *sal-call-stack* (sal-trace-exit))
rslt)))))))