The loop starts with FOREACH-FRAME and all the nested loops (also within VUGs but not within UGENs) are merged in a single loop (the first). The indexes of the loop are CURRENT-FRAME and CURRENT-SAMPLE and the sample-counter is updated when we use NOW inside a FOREACH-FRAME loop.

Some simple examples:

(in-package :scratch)

(dsp! sin-test (freq amp)
  (with-samples ((k (* +twopi+ freq *sample-duration*)))
      (out (* amp (sin (* k (now))))))))

SCRATCH> (block-size)
SCRATCH> (set-rt-block-size 64)
SCRATCH> (block-size)
SCRATCH> (rt-start)

SCRATCH> (sin-test 440 .3)

SCRATCH> (free 0)

(define-vug filter-in ((ch fixnum) fcut bp amp)
    (* amp (butter-bp (audio-in ch) fcut bp))))

(dsp! in-out-test ()
    ;; There is only one loop because the nested FOREACH-FRAME loops
    ;; are merged with the first.
    (out (filter-in 0 2500 100 20)
         (filter-in 1 2500 100 20))))

SCRATCH> (in-out-test)

SCRATCH> (free 0)

(defvar *sintab* (make-buffer 8192 :fill-function (gen:partials '(1))))

(dsp! sintonia (freq amp pos)
  (with-samples ((x (osc *sintab* freq amp 0 :linear))
                 (alpha (* +half-pi+ pos))
                 (left (cos alpha))
                 (right (sin alpha)))
      (out (* left x) (* right x)))))

(defun new-frequencies (max)
  (rt-eval () (dograph (n) (set-control n :freq (+ 100 (random max))))))

SCRATCH> (loop repeat 100 do (sintonia (+ 100 (random 3000)) .01 (random 1.0)))

SCRATCH> (new-frequencies 5000)

SCRATCH> (free 0)

A UGEN is a compiled VUG and it's impossible to merge FOREACH-FRAME with another loop compiled within a UGEN. A efficient technique is to compute an array of samples within the UGEN. For example:

(define-ugen envelope* frame ((env envelope) gate time-scale
                              (done-action function))
  (with ((frm (make-frame (block-size))))
      (setf (frame-ref frm current-frame)
            (envelope env gate time-scale done-action)))

(dsp! bbb-ugen-test ((env envelope) freq amp gate)
  (with ((frm (envelope* env gate 1 #'free)))
    (declare (type frame frm))
    ;; Set the envelope before the next loop.
    (maybe-expand frm)
      (stereo (* (frame-ref frm current-frame)
                 (gbuzz freq amp 10 1 .5))))))

(defvar *env-test* (make-adsr 1.2 .09 .9 .5))

SCRATCH> (bbb-ugen-test *env-test* 440 .3 1 :id #xee)

SCRATCH> (set-control #xee :gate 0)

SCRATCH> (free #xee)

SCRATCH> (set-rt-block-size 1)

The index of the array is CURRENT-FRAME because the output of ENVELOPE is mono. With a multi-channel output, the index is

(+ CURRENT-SAMPLE <channel>)

where <channel> is the number of the channel starting from zero.

Note: a FRAME is an array of samples, so SMP-REF and FRAME-REF are the same.

Sourceforge project page