;;; various envelope functions ;;; ;;; window-envelope (beg end env) -> portion of env lying between x axis values beg and end ;;; map-envelopes (func env1 env2) maps func over the breakpoints in env1 and env2 returning a new envelope ;;; multiply-envelopes (env1 env2) multiplies break-points of env1 and env2 returning a new envelope ;;; add-envelopes (env1 env2) adds break-points of env1 and env2 returning a new envelope ;;; max-envelope (env) -> max y value in env, min-envelope ;;; integrate-envelope (env) -> area under env ;;; envelope-last-x (env) -> max x axis break point position ;;; stretch-envelope env old-attack new-attack old-decay new-decay -> divseg-like envelope mangler ;;; scale-envelope (env scaler offset) scales y axis values by 'scaler' and optionally adds 'offset' ;;; reverse-envelope (env) reverses the breakpoints in 'env' ;;; concatenate-envelopes (:rest envs) concatenates its arguments into a new envelope ;;; repeat-envelope env repeats (reflected #f) (normalized #f) repeats an envelope ;;; power-env: generator for extended envelopes (each segment has its own base) ;;; envelope-exp: interpolate segments into envelope to give exponential curves ;;; rms-envelope ;;; normalize-envelope ;;; simplify-envelope (provide 'snd-env.scm) ;;; -------- window-envelope (a kinda brute-force translation from the CL version in env.lisp) (define window-envelope (let ((documentation "(window-envelope beg end e) -> portion of e lying between x axis values beg and end: (window-envelope 1.0 3.0 '(0.0 0.0 5.0 1.0)) -> '(1.0 0.2 3.0 0.6)")) (lambda (beg end e) (let ((nenv ()) (lasty (if (pair? e) (cadr e) 0.0)) (len (length e))) (call-with-exit (lambda (return-early) (do ((i 0 (+ i 2))) ((>= i len)) (let ((x (e i)) (y (e (+ i 1)))) (set! lasty y) (cond ((null? nenv) (when (>= x beg) (set! nenv (append nenv (list beg (envelope-interp beg e)))) (if (not (= x beg)) (if (>= x end) (return-early (append nenv (list end (envelope-interp end e)))) (set! nenv (append nenv (list x y))))))) ((<= x end) (set! nenv (append nenv (list x y))) (if (= x end) (return-early nenv))) ((> x end) (return-early (append nenv (list end (envelope-interp end e)))))))) (append nenv (list end lasty)))))))) ;;; -------- map-envelopes like map-across-envelopes in env.lisp (define map-envelopes (let ((documentation "(map-envelopes func env1 env2) maps func over the breakpoints in env1 and env2 returning a new envelope")) (lambda (op e1 e2) (let ((xs ())) (let ((at0 (lambda (e) (let* ((diff (car e)) (len (length e)) (lastx (e (- len 2))) (newe (copy e))) (do ((i 0 (+ i 2))) ((>= i len) newe) (let ((x (/ (- (newe i) diff) lastx))) (set! xs (cons x xs)) (set! (newe i) x)))))) (remove-duplicates (lambda (lst) (let rem-dup ((lst lst) (nlst ())) (cond ((null? lst) nlst) ((member (car lst) nlst) (rem-dup (cdr lst) nlst)) (else (rem-dup (cdr lst) (cons (car lst) nlst)))))))) (if (null? e1) (at0 e2) (if (null? e2) (at0 e1) (let ((ee1 (at0 e1)) (ee2 (at0 e2)) (newe ())) (set! xs (sort! (remove-duplicates xs) <)) (do ((len (length xs)) (i 0 (+ i 1))) ((= i len) newe) (let ((x (xs i))) (set! newe (append newe (list x (op (envelope-interp x ee1) (envelope-interp x ee2))))))))))))))) ;;; -------- multiply-envelopes, add-envelopes (define multiply-envelopes (let ((documentation "(multiply-envelopes env1 env2) multiplies break-points of env1 and env2 returning a new envelope: (multiply-envelopes '(0 0 2 .5) '(0 0 1 2 2 1)) -> '(0 0 0.5 0.5 1.0 0.5)")) (lambda (e1 e2) (map-envelopes * e1 e2)))) (define add-envelopes (let ((documentation "(add-envelopes env1 env2) adds break-points of env1 and env2 returning a new envelope")) (lambda (e1 e2) (map-envelopes + e1 e2)))) ;;; -------- max-envelope (define max-envelope (let ((documentation "(max-envelope env) -> max y value in env")) (lambda (env1) (let max-envelope-1 ((e (cddr env1)) (mx (cadr env1))) (if (null? e) mx (max-envelope-1 (cddr e) (max mx (cadr e)))))))) ;;; -------- min-envelope (define min-envelope (let ((documentation "(min-envelope env) -> min y value in env")) (lambda (env1) (let min-envelope-1 ((e (cddr env1)) (mx (cadr env1))) (if (null? e) mx (min-envelope-1 (cddr e) (min mx (cadr e)))))))) ;;; -------- integrate-envelope (define integrate-envelope (let ((documentation "(integrate-envelope env) -> area under env")) (lambda (env1) (let integrate-envelope-1 ((e env1) (sum 0.0000)) (if (or (null? e) (null? (cddr e))) sum (integrate-envelope-1 (cddr e) (+ sum (* (+ (cadr e) (cadddr e)) 0.5 (- (caddr e) (car e)))))))))) ;;; -------- envelope-last-x (define envelope-last-x (let ((documentation "(envelope-last-x env) -> max x axis break point position")) (lambda (e) (if (null? (cddr e)) (car e) (envelope-last-x (cddr e)))))) ;;; -------- stretch-envelope (define stretch-envelope (let ((documentation "(stretch-envelope env old-attack new-attack old-decay new-decay) takes 'env' and returns a new envelope based on it but with the attack and optionally decay portions stretched or squeezed; 'old-attack' is the original x axis attack end point, 'new-attack' is where that section should end in the new envelope. Similarly for 'old-decay' and 'new-decay'. This mimics divseg in early versions of CLM and its antecedents in Sambox and Mus10 (linen). (stretch-envelope '(0 0 1 1) .1 .2) -> (0 0 0.2 0.1 1.0 1) (stretch-envelope '(0 0 1 1 2 0) .1 .2 1.5 1.6) -> (0 0 0.2 0.1 1.1 1 1.6 0.5 2.0 0)")) (lambda* (fn old-att new-att old-dec new-dec) (cond ((not new-att) (if old-att (error 'wrong-number-of-args "stretch-envelope: ~A, old-attack but no new-attack?" old-att) fn)) ((and old-dec (not new-dec)) (error 'wrong-number-of-args "stretch-envelope:~A ~A ~A, old-decay but no new-decay?" old-att new-att old-dec)) (else (let ((x0 (car fn)) (y0 (cadr fn))) (let ((new-x x0) (last-x (fn (- (length fn) 2))) (new-fn (list y0 x0)) (scl (/ (- new-att x0) (max .0001 (- old-att x0))))) (if (and (number? old-dec) (= old-dec old-att)) (set! old-dec (* 1e-06 last-x))) (reverse (let stretch-envelope-1 ((new-fn new-fn) (old-fn (cddr fn))) (if (null? old-fn) new-fn (let ((x1 (car old-fn)) (y1 (cadr old-fn))) (when (and (< x0 old-att) (>= x1 old-att)) (set! y0 (if (= x1 old-att) y1 (+ y0 (* (- y1 y0) (/ (- old-att x0) (- x1 x0)))))) (set! x0 old-att) (set! new-x new-att) (set! new-fn (cons y0 (cons new-x new-fn))) (set! scl (if old-dec (/ (- new-dec new-att) (- old-dec old-att)) (/ (- last-x new-att) (- last-x old-att))))) (when (and (real? old-dec) (< x0 old-dec) (>= x1 old-dec)) (set! y0 (if (= x1 old-dec) y1 (+ y0 (* (- y1 y0) (/ (- old-dec x0) (- x1 x0)))))) (set! x0 old-dec) (set! new-x new-dec) (set! new-fn (cons y0 (cons new-x new-fn))) (set! scl (/ (- last-x new-dec) (- last-x old-dec)))) (unless (= x0 x1) (set! new-x (+ new-x (* scl (- x1 x0)))) (set! new-fn (cons y1 (cons new-x new-fn))) (set! x0 x1) (set! y0 y1)) (stretch-envelope-1 new-fn (cddr old-fn))))))))))))) ;;; -------- scale-envelope (define scale-envelope (let ((documentation "(scale-envelope env scaler (offset 0)) scales y axis values by 'scaler' and optionally adds 'offset'")) (lambda* (e scl (offset 0)) (if (null? e) () (cons (car e) (cons (+ offset (* scl (cadr e))) (scale-envelope (cddr e) scl offset))))))) ;;; -------- reverse-envelope (define reverse-envelope (let ((documentation "(reverse-envelope env) reverses the breakpoints in 'env'")) (lambda (e) (define (reverse-env-1 e newe xd) (if (null? e) newe (reverse-env-1 (cddr e) (cons (- xd (car e)) (cons (cadr e) newe)) xd))) (let ((len (length e))) (if (memv len '(0 2)) e (reverse-env-1 e () (e (- len 2)))))))) ;;; -------- concatenate-envelopes (define concatenate-envelopes (let ((documentation "(concatenate-envelopes :rest envs) concatenates its arguments into a new envelope")) (lambda envs (define (cat-1 e newe xoff x0) (if (null? e) newe (cat-1 (cddr e) (cons (cadr e) (cons (- (+ (car e) xoff) x0) newe)) xoff x0))) (let ((ne ()) (xoff 0.0)) (for-each (lambda (e) (if (and (pair? ne) (= (car ne) (cadr e))) (begin (set! xoff (- xoff .01)) (set! ne (cat-1 (cddr e) ne xoff (car e)))) (set! ne (cat-1 e ne xoff (car e)))) (set! xoff (+ xoff .01 (cadr ne)))) envs) (reverse ne))))) (define repeat-envelope (let ((documentation "(repeat-envelope env repeats (reflected #f) (normalized #f)) repeats 'env' 'repeats' times. (repeat-envelope '(0 0 100 1) 2) -> (0 0 100 1 101 0 201 1). If the final y value is different from the first y value, a quick ramp is inserted between repeats. 'normalized' causes the new envelope's x axis to have the same extent as the original's. 'reflected' causes every other repetition to be in reverse.")) (lambda* (ur-env repeats reflected normalized) (let ((e (if (not reflected) ur-env (let ((lastx (ur-env (- (length ur-env) 2))) (rev-env (cddr (reverse ur-env))) (new-env (reverse ur-env))) (while (pair? rev-env) (set! new-env (cons (- (+ lastx lastx) (cadr rev-env)) new-env)) (set! new-env (cons (car rev-env) new-env)) (set! rev-env (cddr rev-env))) (reverse new-env))))) (let ((first-y (cadr e)) (x (car e))) (let ((x-max (e (- (length e) 2))) (new-env (list first-y x))) (let ((len (length e)) (times (if reflected (floor (/ repeats 2)) repeats)) (first-y-is-last-y (= first-y (e (- (length e) 1))))) (do ((i 0 (+ i 1))) ((= i times)) (do ((j 2 (+ j 2))) ((>= j len)) (set! x (- (+ x (e j)) (e (- j 2)))) (set! new-env (cons (e (+ j 1)) (cons x new-env)))) (if (and (< i (- times 1)) (not first-y-is-last-y)) (begin (set! x (+ x (/ x-max 100.0))) (set! new-env (cons first-y (cons x new-env))))))) (set! new-env (reverse new-env)) (if normalized (do ((scl (/ x-max x)) (new-len (length new-env)) (i 0 (+ i 2))) ((>= i new-len)) (set! (new-env i) (* scl (new-env i))))) new-env)))))) ;;; -------- power-env ;;; ;;; (this could also be done using multi-expt-env (based on env-any) in generators.scm) (if (provided? 'snd) (require snd-ws.scm) (require sndlib-ws.scm)) ;;; (define pe (make-power-env '(0 0 1 1 2 0) :duration 1.0)) ;;; :(power-env pe) ;;; 0.0 ;;; :(power-env pe) ;;; 4.5352502324316e-05 ;;; :(power-env pe) ;;; 9.0705004648631e-05 ;;; :(power-env pe) ;;; 0.00013605750697295 (defgenerator penv (envs #f) (total-envs 0) (current-env 0) (current-pass 0)) (define (power-env pe) (with-let pe (let ((val (env (vector-ref envs current-env)))) (set! current-pass (- current-pass 1)) (when (and (= current-pass 0) (< current-env (- total-envs 1))) (set! current-env (+ current-env 1)) (set! current-pass (- (length (vector-ref envs current-env)) 1))) val))) (define* (make-power-env envelope (scaler 1.0) (offset 0.0) duration) (let* ((len (- (floor (/ (length envelope) 3)) 1)) (pe (make-penv :envs (make-vector len) :total-envs len :current-env 0 :current-pass 0)) (xext (- (envelope (- (length envelope) 3)) (car envelope)))) (do ((i 0 (+ i 1)) (j 0 (+ j 3))) ((= i len)) (let ((x0 (envelope j)) (x1 (envelope (+ j 3))) (y0 (envelope (+ j 1))) (y1 (envelope (+ j 4))) (base (envelope (+ j 2)))) (vector-set! (pe 'envs) i (make-env (list 0.0 y0 1.0 y1) :base base :scaler scaler :offset offset :duration (* duration (/ (- x1 x0) xext)))))) (set! (pe 'current-pass) (- (length (vector-ref (pe 'envs) 0)) 1)) pe)) (define* (power-env-channel pe (beg 0) snd chn edpos (edname "power-env-channel")) ;; split into successive calls on env-channel (let ((curbeg beg)) ; sample number (as-one-edit (lambda () (do ((i 0 (+ i 1))) ((= i (pe 'total-envs))) (let* ((e (vector-ref (pe 'envs) i)) (len (length e))) (env-channel e curbeg len snd chn edpos) (set! curbeg (+ curbeg len))))) edname))) ;;; here's a simpler version that takes the breakpoint list, rather than the power-env structure: (define powenv-channel (let ((documentation "(powenv-channel envelope (beg 0) dur snd chn edpos) returns an envelope with a separate base for \ each segment: (powenv-channel '(0 0 .325 1 1 32.0 2 0 32.0))")) (lambda* (envelope (beg 0) dur snd chn edpos) (let ((len (length envelope)) (x1 (car envelope))) (let ((curbeg beg) (fulldur (or dur (framples snd chn edpos))) (xrange (- (envelope (- len 3)) x1)) (y1 (cadr envelope)) (base (caddr envelope)) (x0 0.0) (y0 0.0)) (if (= len 3) (scale-channel y1 beg dur snd chn edpos) (as-one-edit (lambda () (do ((i 3 (+ i 3))) ((= i len)) (set! x0 x1) (set! y0 y1) (set! x1 (envelope i)) (set! y1 (envelope (+ i 1))) (let ((curdur (round (* fulldur (/ (- x1 x0) xrange))))) (xramp-channel y0 y1 base curbeg curdur snd chn edpos) (set! curbeg (+ curbeg curdur))) (set! base (envelope (+ i 2)))))))))))) ;;; by Anders Vinjar: ;;; ;;; envelope-exp can be used to create exponential segments to include in ;;; envelopes. Given 2 or more breakpoints, it approximates the ;;; curve between them using 'xgrid linesegments and 'power as the ;;; exponent. ;;; ;;; env is a list of x-y-breakpoint-pairs, ;;; power applies to whole envelope, ;;; xgrid is how fine a solution to sample our new envelope with. (define envelope-exp (let ((documentation "(envelope-exp e (power 1.0) (xgrid 100)) approximates an exponential curve connecting the breakpoints")) (lambda* (e (power 1.0) (xgrid 100)) (let ((mn (min-envelope e))) (let ((largest-diff (* 1.0 (- (max-envelope e) mn))) (x-min (car e)) (x-max (e (- (length e) 2)))) (do ((x-incr (* 1.0 (/ (- x-max x-min) xgrid))) (new-e ()) (x x-min (+ x x-incr))) ((>= x x-max) (reverse new-e)) (let ((y (envelope-interp x e))) (set! new-e (cons (if (= largest-diff 0.0) y (+ mn (* largest-diff (expt (/ (- y mn) largest-diff) power)))) (cons x new-e)))))))))) ;;; rms-envelope (define rms-envelope (let ((documentation "(rms-envelope file (beg 0.0) (dur #f) (rfreq 30.0) (db #f)) returns an envelope of RMS values in 'file'")) (lambda* (file (beg 0.0) dur (rfreq 30.0) db) ;; based on rmsenv.ins by Bret Battey (let* ((fsr (srate file)) (start (round (* beg fsr))) (end (if dur (min (* 1.0 (+ start (round (* fsr dur)))) (mus-sound-framples file)) (mus-sound-framples file)))) (let ((incrsamps (round (/ fsr rfreq))) (len (- (+ end 1) start))) (let ((reader (make-sampler start file)) (rms (make-moving-average incrsamps)) ; this could use make-moving-rms from dsp.scm (e ()) (rms-val 0.0) (jend 0) (data (make-float-vector len))) (do ((i 0 (+ i 1))) ((= i len)) (float-vector-set! data i (next-sample reader))) (float-vector-multiply! data data) (do ((i 0 (+ i incrsamps))) ((>= i end) (reverse e)) (set! jend (min end (+ i incrsamps))) (do ((j i (+ j 1))) ((= j jend)) (moving-average rms (float-vector-ref data j))) (set! e (cons (* 1.0 (/ i fsr)) e)) (set! rms-val (sqrt (* (mus-scaler rms) (mus-increment rms)))) (set! e (cons (if db (if (< rms-val 1e-05) -100.0 (* 20.0 (log rms-val 10.0))) rms-val) e))))))))) (define* (normalize-envelope env1 (new-max 1.0)) (scale-envelope env1 (/ new-max (let abs-max-envelope-1 ((e (cddr env1)) (mx (abs (cadr env1)))) (if (null? e) mx (abs-max-envelope-1 (cddr e) (max mx (abs (cadr e))))))))) ;;; simplify-envelope ;;; ;;; this is not very good... (define* (simplify-envelope env1 (ygrid 10) (xgrid 100)) ;; grid = how fine a fluctuation we will allow. ;; the smaller the grid, the less likely a given bump will get through ;; original x and y values are not changed, just sometimes omitted. (define (point-on-line? px py qx qy tx ty) ;; is point tx ty on line defined by px py and qx qy -- ;; #f if no, :before if on ray from p, :after if on ray from q, :within if between p and q ;; (these are looking at the "line" as a fat vector drawn on a grid) ;; taken from "Graphics Gems" by Glassner, code by A Paeth (if (or (= py qy ty) (= px qx tx)) :within (and (< (abs (- (* (- qy py) (- tx px)) (* (- ty py) (- qx px)))) (max (abs (- qx px)) (abs (- qy py)))) (if (or (< qx px tx) (< qy py ty) (< tx px qx) (< ty py qy)) :before (if (or (< px qx tx) (< py qy ty) (< tx qx px) (< ty qy py)) :after :within))))) (if (not (and env1 (> (length env1) 4))) env1 (let ((new-env (list (cadr env1) (car env1))) (ymax (max-envelope env1)) (ymin (min-envelope env1)) (xmax (env1 (- (length env1) 2))) (xmin (car env1))) (if (= ymin ymax) (list xmin ymin xmax ymax) (do ((y-scl (/ ygrid (- ymax ymin))) (x-scl (/ (or xgrid ygrid) (- xmax xmin))) (px #f) (py #f) (qx #f) (qy #f) (tx #f) (ty #f) (qtx #f) (qty #f) (i 0 (+ i 2))) ((>= i (length env1)) (set! new-env (cons qty (cons qtx new-env))) (reverse new-env)) (let ((ttx (env1 i)) (tty (env1 (+ i 1)))) (set! tx (round (* ttx x-scl))) (set! ty (round (* tty y-scl))) (if px (if (not (point-on-line? px py qx qy tx ty)) (begin (set! new-env (cons qty (cons qtx new-env))) (set! px qx) (set! py qy))) (begin (set! px qx) (set! py qy))) (set! qx tx) (set! qy ty) (set! qtx ttx) (set! qty tty)))))))