Next: Abspielen der Aufnahme , Previous: Generatoren , Up: Automation , Home: Einführung

musikinformatik-wise-24.org

Aufnahme von Live-Interaktion

Mit Hilfe von cl-refs ist die Aufnahme und anschließende Wiedergabe von Live Interaktionen sehr einfach zu bewerkstelligen.

Als Beispiel soll eine Live-Interaktion mit dem ats-player aufgenommen werden. Das Webinterface des ats-players erreicht man über die URL http://localhost:54619/ats-display. Nach dem Start von Clamps sollte diese URL die folgende Grafik darstellen:

ats-display-gui.png
Abbildung 4: Interaktives ATS Display GUI

Wenn man einen Doppelklick irgendwo in der Grafik des Sonogramms macht, erscheint ein senkrechtes Rechteck an der Stelle der Mausposition. Mit dem Mausrad lässt sich das Rechteck vertikal verkleineren, was sich auf die Bandbreite des resynthetisierten Signals auswirkt. Die Resynthese wird mit der Leertaste an- bzw. ausgeschaltet.

Um eine Live Interaktion aufzunehmen, wird eine watch Funktion definiert, die die aufzunehmenden Parameter enthält. Für die Mausposition gibt es die ref-cell atsd.mousepos, die eine Liste mit der normalisierten x und der y Position in der Grafik des Sonogramms enthält. Für die Bandbreite kann man die ref-cell atsd.bw verwenden.

Zum Testen schreiben wir zunächst eine watch Funktion, die die Werte für x, y und bw in der REPL ausdruckt, wenn sich einer der Werte ändert. Auch hier sollten wir darauf achten, die Funktion zu speichern, mit der man die watch Funktion aufheben kann (also das Resultat der Auswertung von #'watch):

(defparameter *unwatch* nil)

(progn
  (dolist (fn *unwatch*) ;;; vorherige watch Bindungen aufheben
    (funcall fn))
  (setf *unwatch* nil)
  ;;; neue Bindung aufstellen.
  (push (watch (lambda () (destructuring-bind (x y) (get-val atsd.mousepos)
                       (let ((bw (get-val atsd.bw)))
                         (msg :warn "x: ~,2f y: ~,2f bw: ~,2f"
                              x y bw)))))
        *unwatch*))

Nachdem man den obenstehenden Ausdrücke ausgewertet hat, sollte eine Bewegung mit dem Mauszeiger auf der Sonogramm Grafik bzw. eine Bewegung am Mausrad zu zeilenweiser Ausgabe der aktuellen x und y-Position und der Bandbreite in der REPL führen.

Mit einem erneuten Doppelklick in der Sonogramm Grafik kann man die Grafik mit der Maus verlassen, ohne, dass die Mausposition weiter getrackt wird.

Im obenstehenden Code ist die Form destructuring-bind evtl. unbekannt. Bei destructuring-bind handelt es sich um eine lexikalische Bindung, die ganz ähnlich zu let in dem zugehörigen body Symbole an Werte bindet. Anders als bei let werden die Werte aber nicht direkt gebunden, sondern an eine analoge Listenstruktur gebunden: Die Symbole der Liste des ersten Arguments von destructuring-bind werden an die Werte an der entsprechenden Stelle der Liste des zweiten Arguments von destructuring-bind gebunden. Das bedeutet, die Struktur der Liste mit den Symbolen muss genau der Struktur der Liste mit den Werten entsprechen, an die die Symbole gebunden werden sollen.

In dem Code oben werden durch das destructuring-bind die Symbole x und y an das erste bzw. zweite Element der Liste des Wertes der ref-cell atsd.mousepos gebunden, d.h. im body des destructuring-bind ist x an die x-Koordinate und y an die y-Koordinate dieser ref-cell gebunden.

Wie bei den Argumenten einer Funktionsdefinition dürfen auch die speziellen Symbole &optional, &key, &args, &whole und &rest verwendet werden1.

Hier ein paar Beispiele zur Verdeutlichung:

(destructuring-bind (a b c d) '(1 2 3 4)
  (list a b c d))
;; => (1 2 3 4)

(destructuring-bind (a (b (c) d)) '(1 (2 (3) 4))
  (list a b c d))
;; => (1 2 3 4)

(destructuring-bind (a &rest rest) '(1 2 3 4)
  (list a rest)) ; => (1 (2 3 4))

;; Das Gleiche, wie zuvor, aber mit "dotted Notation":

(destructuring-bind (a . rest) '(1 2 3 4)
  (list a rest)) ; => (1 (2 3 4))


  • Zeitinformation hinzufügen

    Die zeitliche Information kann man einfach mit der Funktion #'now hinzufügen:

    (progn
      (dolist (fn *unwatch*) ;;; vorherige watch Bindungen aufheben
        (funcall fn))
      (setf *unwatch* nil)
      ;;; neue Bindung aufstellen.
      (push (watch (lambda () (destructuring-bind (x y) (get-val atsd.mousepos)
                           (let ((bw (get-val atsd.bw)))
                             (msg :warn "time: ~,2f x: ~,2f y: ~,2f bw: ~,2f"
                                  (now) x y bw)))))
            *unwatch*))
    

    Um die Interaktion "aufzunehmen", werden diese Informationen in einer Liste zusammengefasst und in eine globale Variable *​recording​* mit dem Befehl #'push geschoben. Die dadurch entstandene Liste *​recording​* enthält die Animation der Interaktion in umgekehrt chronologischer Reihenfolge:

    (defparameter *recording* nil)
    
    (progn
      (dolist (fn *unwatch*) ;;; vorherige watch Bindungen aufheben
        (funcall fn))
      (setf *unwatch* nil)
      ;;; neue Bindung aufstellen.
      (push (watch (lambda () (destructuring-bind (x y) (get-val atsd.mousepos)
                           (let ((bw (get-val atsd.bw)))
                             (push (list :time (now) :x x :y y :bw bw)
                                   *recording*)))))
            *unwatch*))
    

    Nachdem der obenstehende Ausdruck evaluiert wurde und anschließend das Rechteck in der Grafik des Sonogramms bewegt wurde, sollte die Variable *​recording​* Einträge enthalten:

    clamps> *recording*
     (:time 2074.1340589569163d0 :x 0.3572216175158119d0 :y 0.7066976963700129d0
            :bw 0.10399999999999937d0)
     (:time 2074.1108390022678d0 :x 0.36617972145559097d0 :y 0.6838429714118321d0
            :bw 0.10399999999999937d0)
     (:time 2074.0992290249433d0 :x 0.3730705706400364d0 :y 0.666067074222136d0 :bw
            0.10399999999999937d0)
     (:time 2074.0876190476192d0 :x 0.37651599523225915d0 :y 0.650830590916682d0
            :bw 0.10399999999999937d0)
     (:time 2074.0643990929707d0 :x 0.3792723349060373d0 :y 0.6394032284375917d0
            :bw 0.10399999999999937d0)
     (:time 2074.052789115646d0 :x 0.3820286745798155d0 :y 0.6317849867848647d0 :bw
            0.10399999999999937d0)
     (:time 2074.041179138322d0 :x 0.3820286745798155d0 :y 0.6279758659585013d0 :bw
            0.10399999999999937d0)
     (:time 2073.9715192743765d0 :x 0.3820286745798155d0 :y 0.62670615901638d0 :bw
            0.10399999999999937d0)
     (:time 2072.1951927437644d0 :x 0.502618535307611d0 :y 0.36895564976578543d0
            :bw 0.10399999999999937d0))
    clamps> 
    

    Um diese Aufnahme abspielen zu können, sollten die Zeitwerte chronologisch sortiert und die Absolutzeiten in Deltazeiten ("Einsatzabstände") umgewandelt werden. Für das Ermitteln der Einsatzabstände kann die Funktion #'differentiate aus dem Paket orm-utils, das in Clamps integriert ist, verwendet werden. Hierzu zunächst einige Beispiele über die Funktionsweise von #'differentiate und seiner Umkehrfunktion #'integrate:

    ;; differentiate kopiert das erste Element einer Liste und bildet in
    ;; der Folge die Differenzen zwischen aufeinanderfolgenden Elementen
    ;; dieser Liste.
    
    (differentiate '(5 4 6 1 9)) ; => (5 -1 2 -5 8)
    
    ;; integrate summiert die Elemente einer Liste:
    
    (integrate '(1 2 3 4 5))  ; => (1 3 6 10 15)
    
    ;; integrate und differentiate sind Umkehrfunktionen:
    
    (integrate '(5 -1 2 -5 8))  ; => (5 4 6 1 9)
    
    (integrate (differentiate '(10 2 5 1 9)))  ; => (10 2 5 1 9)
    

    Auf diese Weise lassen sich Listen mit Absolutzeiten einfach in Listen mit Einsatzabständen (und umgekehrt) umwandeln. Im Falle der *​recording​* Liste lässt sich eine Liste mit den Einsatzabständen erzeugen, indem man das jeweils zweite Element der Unterlisten von *​recording​*, das den Absolutwert der Zeit enthält, mit Hilfe von #'mapcar aus der Liste extrahiert und das Ergebnis mit #'differentiate in Einsatzabstände umwandelt. Da *​recording​* die Animation in umgekehrter Reihenfolge enthält, wird die Liste zuvor mit der Funktion #'reverse umgekehrt.

    Hier die Schritte im Einzelnen:

    ;;; Die Zeitwerte von *recording* in einer Liste extrahieren:
    
    ;;; Die Absolutzeiten extrahieren:
    
    (mapcar #'second (reverse *recording*))
    
    ;;  => (2072.1951927437644d0 2073.9715192743765d0 2074.041179138322d0
    ;; 2074.052789115646d0 2074.0643990929707d0 2074.0876190476192d0
    ;; 2074.0992290249433d0 2074.1108390022678d0 2074.1340589569163d0)
    
    ;;; Absolutzeiten in Einsatzabstände umwandeln:
    
    (differentiate (mapcar #'second (reverse *recording*)))
    
    ;; => (2072.1951927437644d0 1.7763265306120957d0 0.06965986394561696d0
    ;; 0.01160997732404212d0 0.011609977324496867d0 0.023219954648538987d0
    ;; 0.01160997732404212d0 0.011609977324496867d0 0.023219954648538987d0)
    
    ;; Den Einsatzabstand des ersten Ereignisses auf 0 setzen:
    
    (cons 0 (rest (differentiate (mapcar #'second (reverse *recording*)))))
    
    ;; => (0 1.7763265306120957d0 0.06965986394561696d0 0.01160997732404212d0
    ;; 0.011609977324496867d0 0.023219954648538987d0 0.01160997732404212d0
    ;; 0.011609977324496867d0 0.023219954648538987d0)
    

    Die gesamte Liste *​recording​* mit Einsatzabständen an Stelle der Absolutzeiten kann man dann auf folgende Weise erzeugen:

    (let ((seq (reverse *recording*)))
      (mapcar (lambda (x y) (list* :time x (nthcdr 2 y)))
              (cons 0 (rest (differentiate (mapcar #'second seq))))
              seq))
    
    ;; => ((:time 0 :x 0.502618535307611d0 :y 0.36895564976578543d0 :bw
    ;;        0.10399999999999937d0)
    ;; (:time 1.7763265306120957d0 :x 0.3820286745798155d0 :y 0.62670615901638d0 :bw
    ;;        0.10399999999999937d0)
    ;; (:time 0.06965986394561696d0 :x 0.3820286745798155d0 :y 0.6279758659585013d0
    ;;        :bw 0.10399999999999937d0)
    ;; (:time 0.01160997732404212d0 :x 0.3820286745798155d0 :y 0.6317849867848647d0
    ;;        :bw 0.10399999999999937d0)
    ;; (:time 0.011609977324496867d0 :x 0.3792723349060373d0 :y 0.6394032284375917d0
    ;;        :bw 0.10399999999999937d0)
    ;; (:time 0.023219954648538987d0 :x 0.37651599523225915d0 :y 0.650830590916682d0
    ;;        :bw 0.10399999999999937d0)
    ;; (:time 0.01160997732404212d0 :x 0.3730705706400364d0 :y 0.666067074222136d0
    ;;        :bw 0.10399999999999937d0)
    ;; (:time 0.011609977324496867d0 :x 0.36617972145559097d0 :y 0.6838429714118321d0
    ;;        :bw 0.10399999999999937d0)
    ;; (:time 0.023219954648538987d0 :x 0.3572216175158119d0 :y 0.7066976963700129d0
    ;;        :bw 0.10399999999999937d0))
    

    Da man diesen Vorgang wiederholt verwenden wird, empfiehlt sich dafür die Definition einer Funktion:

    (defun get-dtime-seq (recording)
      (let ((seq (reverse recording)))
        (mapcar (lambda (x y) (list* :time x (nthcdr 2 y)))
                (cons 0 (rest (differentiate (mapcar #'second seq))))
                seq)))
    
    (get-dtime-seq *recording*)
    
    ;;  => ((:time 0 :x 0.502618535307611d0 :y 0.36895564976578543d0 :bw
    ;;        0.10399999999999937d0)
    ;;      (:time 1.7763265306120957d0 :x 0.3820286745798155d0 :y 0.62670615901638d0 :bw
    ;;        0.10399999999999937d0)
    ;;      (:time 0.06965986394561696d0 :x 0.3820286745798155d0 :y 0.6279758659585013d0
    ;;        :bw 0.10399999999999937d0)
    ;;      (:time 0.01160997732404212d0 :x 0.3820286745798155d0 :y 0.6317849867848647d0
    ;;        :bw 0.10399999999999937d0)
    ;;      (:time 0.011609977324496867d0 :x 0.3792723349060373d0 :y 0.6394032284375917d0
    ;;        :bw 0.10399999999999937d0)
    ;;      (:time 0.023219954648538987d0 :x 0.37651599523225915d0 :y 0.650830590916682d0
    ;;        :bw 0.10399999999999937d0)
    ;;      (:time 0.01160997732404212d0 :x 0.3730705706400364d0 :y 0.666067074222136d0
    ;;        :bw 0.10399999999999937d0)
    ;;      (:time 0.011609977324496867d0 :x 0.36617972145559097d0 :y 0.6838429714118321d0
    ;;        :bw 0.10399999999999937d0)
    ;;      (:time 0.023219954648538987d0 :x 0.3572216175158119d0 :y 0.7066976963700129d0
    ;;        :bw 0.10399999999999937d0))
    
  • Alternative Implementation (freiwilig)

    Das Verfahren ist bei großen Listen etwas ineffizient, da es zwei verschachtelte Aufrufe zu #'mapcar enthält und daher zweimal über die gesamte Liste iteriert und auch zweimal eine komplett neue Liste mit der Anzahl der Listenelemente erzeugt. Das ist im Normalfall kein Problem, aber etwas effizienter wäre es, die Liste nur einmal anzulegen und sich den Tatbestand zunutze zu machen, dass die Liste in umgekehrter Reihenfolge vorliegt: Wenn man die Liste in der vorliegenden umgekehrten Reihenfolge abarbeitet und die Ergebnisse in eine neue Liste pusht, ist diese Liste am Ende in der richtigen Reihenfolge:

    (defun get-dtime-seq (recording)
      (loop
        with result = nil
        for ((_time1 time1 . rest1) (_time2 time2 . rest2)) on recording 
        do (push (list* :time (if time2 (- time1 time2) 0) rest1) result)
        finally (return result)))
    
    (get-dtime-seq *recording*)
    ;;  => ((:time 0 :x 0.502618535307611d0 :y 0.36895564976578543d0 :bw
    ;;       0.10399999999999937d0)
    ;;      (:time 1.7763265306120957d0 :x 0.3820286745798155d0 :y 0.62670615901638d0 :bw
    ;;       0.10399999999999937d0)
    ;;      (:time 0.06965986394561696d0 :x 0.3820286745798155d0 :y 0.6279758659585013d0
    ;;       :bw 0.10399999999999937d0)
    ;;      (:time 0.01160997732404212d0 :x 0.3820286745798155d0 :y 0.6317849867848647d0
    ;;       :bw 0.10399999999999937d0)
    ;;      (:time 0.011609977324496867d0 :x 0.3792723349060373d0 :y 0.6394032284375917d0
    ;;       :bw 0.10399999999999937d0)
    ;;      (:time 0.023219954648538987d0 :x 0.37651599523225915d0 :y 0.650830590916682d0
    ;;       :bw 0.10399999999999937d0)
    ;;      (:time 0.01160997732404212d0 :x 0.3730705706400364d0 :y 0.666067074222136d0
    ;;       :bw 0.10399999999999937d0)
    ;;      (:time 0.011609977324496867d0 :x 0.36617972145559097d0 :y 0.6838429714118321d0
    ;;       :bw 0.10399999999999937d0)
    ;;      (:time 0.023219954648538987d0 :x 0.3572216175158119d0 :y 0.7066976963700129d0
    ;;       :bw 0.10399999999999937d0))
    

    Die Lösung oben macht sich die Möglichkeit der Argumentdestrukturiering des loop Makros zu Nutze: Die Symbole _time1 time1 und rest1 iterieren über die *​recording​* Liste, indem sie an die entsprechenden Werte eines Eintrags gebunden werden, d.h. _time1 ist immer an das Keyword :time gebunden, _time1 dann an die Absolutzeit eines Elements und rest1 an die restlichen Elemente eines Eintrags (der Punkt zwischen time1 und rest1 führt dazu, dass der gesamte Rest verwendet wird). _time2, time2 und rest2 werden dann an die entsprechenden Werte des jeweils nachfolgenden Elements gebunden. In der letzten Iteration sind _time2, time2 und rest2 alle nil. Die Funktion setzt für die Differenzzeit an diese Stelle den Wert 0, da dies der Einsatzabstand des ersten Elements des Resultats sein soll.

Fußnoten:

1

Für eine vertiefende Darstellung siehe die folgende Seite (zur Überschrift "Destructuring-Bind" weiter unten auf der Seite scrollen): https://gigamonkeys.com/book/beyond-lists-other-uses-for-cons-cells

Created: 2025-02-12 Mi 20:35

Validate