;; ;; starlet/open-sound-control/utils.scm ;; ;; Copyright © 2020-2023 Thomas White ;; ;; This file is part of Starlet. ;; ;; Starlet is free software: you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or ;; (at your option) any later version. ;; ;; This program is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; ;; You should have received a copy of the GNU General Public License ;; along with this program. If not, see . ;; (define-module (starlet open-sound-control utils) #:use-module (starlet attributes) #:use-module (starlet playback) #:use-module (starlet selection) #:use-module (starlet fixture) #:use-module (starlet engine) #:use-module (starlet state) #:use-module (starlet utils) #:use-module (starlet colours) #:use-module (open-sound-control client) #:use-module (open-sound-control server-thread) #:use-module (srfi srfi-1) #:use-module (srfi srfi-9) #:use-module (srfi srfi-26) #:use-module (ice-9 receive) #:export (osc-playback-controls osc-select-button osc-parameter-encoder osc-smart-potentiometer osc-cmy-potentiometer osc-state-fader send-selection-updates-to)) (define* (osc-playback-controls pb server addr go-button stop-button back-button #:key (min-time-between-presses 0.2)) (let ((time-last-press 0)) (add-osc-method server (string-append go-button "/press") "" (lambda () (let ((time-this-press (hirestime))) (if (> time-this-press (+ time-last-press min-time-between-presses)) (go! pb) (display "Too soon after last press!\n")) (set! time-last-press time-this-press))))) (add-osc-method server (string-append stop-button "/press") "" (lambda () (stop! pb))) (add-osc-method server (string-append back-button "/press") "" (lambda () (back! pb))) ;; LEDs (osc-send addr (string-append back-button "/set-led") 'green) (add-and-run-hook! (state-change-hook pb) (lambda (new-state) (if (eq? new-state 'running) (osc-send addr (string-append stop-button "/set-led") 'green) (osc-send addr (string-append stop-button "/set-led") 'off)) (cond ((eq? new-state 'pause) (osc-send addr (string-append go-button "/set-led") 'orange)) ((eq? new-state 'ready) (osc-send addr (string-append go-button "/set-led") 'green)) ((eq? new-state 'running) (osc-send addr (string-append go-button "/set-led") 'green)) (else (osc-send addr (string-append go-button "/set-led") 'off)))) (playback-state pb))) (define (osc-select-button fix server addr button) (add-osc-method server (string-append button "/press") "" (lambda () (toggle-sel fix))) (add-and-run-hook! selection-hook (lambda (sel) (if (selected? fix) (osc-send addr (string-append button "/set-led") 'orange) (osc-send addr (string-append button "/set-led") 'red))) (get-selection))) (define (encoder-inc attr-id n) (for-each (lambda (fix) (let ((attr (find-attr fix attr-id)) (cval (current-value fix attr-id))) (cond ((eq? 'continuous (get-attr-type attr)) (at fix attr-id (+ cval n))) ((eq? 'list (get-attr-type attr)) (if (> n 0) (at fix attr-id (next-attr-item attr cval)) (at fix attr-id (prev-attr-item attr cval))))))) (get-selection))) (define (osc-parameter-encoder attr server addr encoder) (add-osc-method server (string-append encoder "/inc") "" (lambda () (encoder-inc attr 3))) (add-osc-method server (string-append encoder "/dec") "" (lambda () (encoder-inc attr -3))) (add-osc-method server (string-append encoder "/inc-fine") "" (lambda () (encoder-inc attr 1))) (add-osc-method server (string-append encoder "/dec-fine") "" (lambda () (encoder-inc attr -1))) (add-and-run-hook! selection-hook (lambda (sel) (if (any (lambda (fix) (fixture-has-attr? fix attr)) (get-selection)) (osc-send addr (string-append encoder "/set-led") 'green) (osc-send addr (string-append encoder "/set-led") 'off))) (get-selection))) (define (ccval->percent n) (/ (* n 100) 127)) (define (osc-state-fader server addr fader state) (let ((fader-val 0)) (register-state! (lighting-state (state-for-each (lambda (fix attr val) (at fix attr (lambda () (if (intensity? attr) ;; Intensity parameters get scaled according to the fader (* 0.01 val (ccval->percent fader-val)) ;; Non-intensity parameters just get set in our new state, ;; but only if the fader is up! (if (> fader-val 0) val 'no-value))))) state))) (osc-send addr (string-append fader "/enable")) (osc-send addr (string-append fader "/set-pickup") 0) (add-osc-method server (string-append fader "/value-change") "i" (lambda (v) (set! fader-val v))))) (define (send-selection-updates-to addr) (add-hook! selection-hook (lambda (sel) (osc-send addr "/starlet/selection/update" (get-selection-as-string))))) (define (fader-up-gradients initial-vals max-vals congruent-val) (map (lambda (initial-val attr-max) (if (< congruent-val 127) (/ (- attr-max initial-val) (- 127 congruent-val)) 0)) initial-vals max-vals)) (define (fader-down-gradients initial-vals min-vals congruent-val) (map (lambda (initial-val attr-min) (if (> congruent-val 0) (/ (- initial-val attr-min) congruent-val) 0)) initial-vals min-vals)) (define (fixtures-with-attr fixture-list attr-name) (let ((fix-attrs (map (lambda (fix) (let ((attr (find-attr fix attr-name))) (if attr (cons fix attr) (cons #f #f)))) fixture-list))) (values (filter (lambda (x) x) (map car fix-attrs)) (filter (lambda (x) x) (map cdr fix-attrs))))) (define (current-values fixture-list attr-name) (map (lambda (fix) (current-value fix attr-name)) fixture-list)) (define-record-type (smart-pot-record addr pot-method initial-vals min-vals max-vals congruent-val up-gradients dn-gradients) smart-pot? (addr get-target-addr) (pot-method get-method) (initial-vals get-initial-vals set-initial-vals) (min-vals get-min-vals set-min-vals) (max-vals get-max-vals set-max-vals) (congruent-val get-congruent-val set-congruent-val) (up-gradients get-up-gradients set-up-gradients) (dn-gradients get-dn-gradients set-dn-gradients)) (define (make-smart-potentiometer server addr pot-method callback) (let ((sp (smart-pot-record addr pot-method '() '() '() 0 '() '()))) (add-osc-method server (string-append pot-method "/value-change") "i" (lambda (new-cc-value) (callback (map (lambda (initial-val gradient) (+ initial-val (* gradient (- new-cc-value (get-congruent-val sp))))) (get-initial-vals sp) (if (> new-cc-value (get-congruent-val sp)) (get-up-gradients sp) (get-dn-gradients sp)))))) sp)) (define (reset-gradients sp) (unless (nil? (get-initial-vals sp)) (set-congruent-val sp (mean (map (lambda (val min-val max-val) (scale-to-range val (list min-val max-val) '(0 127))) (get-initial-vals sp) (get-min-vals sp) (get-max-vals sp)))) (set-up-gradients sp (fader-up-gradients (get-initial-vals sp) (get-max-vals sp) (get-congruent-val sp))) (set-dn-gradients sp (fader-up-gradients (get-initial-vals sp) (get-min-vals sp) (get-congruent-val sp))) (osc-send (get-target-addr sp) (string-append (get-method sp) "/set-pickup") (get-congruent-val sp)))) (define (osc-smart-potentiometer attr-name server addr potentiometer) (let ((fixtures '())) ;; First, create a smart potentiometer object and tell it to ;; set the attribute values in the programmer state (let ((smart-pot (make-smart-potentiometer server addr potentiometer (lambda (new-vals) (for-each (lambda (fix new-val) (set-in-state! programmer-state fix attr-name new-val potentiometer)) fixtures new-vals))))) ;; Next, set up a selection hook callback to update the list of ;; fixtures we are controlling (add-and-run-hook! selection-hook (lambda (selection) (receive (new-fixtures attrs) (fixtures-with-attr selection attr-name) (if (nil? new-fixtures) (osc-send addr (string-append potentiometer "/disable")) (begin (set! fixtures new-fixtures) (let ((ranges (map get-attr-range attrs))) (set-min-vals smart-pot (map first ranges)) (set-max-vals smart-pot (map second ranges))) (set-initial-vals smart-pot (current-values fixtures attr-name)) (reset-gradients smart-pot) (osc-send addr (string-append potentiometer "/enable")))))) (get-selection)) ;; Finally, arrange for the smart potentiometer object to be notified ;; if the values change externally (add-update-hook! programmer-state (lambda (source) (unless (eq? source potentiometer) (set-initial-vals smart-pot (current-values fixtures attr-name)) (reset-gradients smart-pot))))))) (define (osc-cmy-potentiometer attr-name server addr c-pot-method m-pot-method y-pot-method) (let ((fixtures '()) (colours '())) (let ((c-pot (make-smart-potentiometer server addr c-pot-method (lambda (new-vals) (set! colours (map (lambda (old-colour new-c) (cmy new-c (magenta old-colour) (yellow old-colour))) (map colour-as-cmy colours) new-vals)) (for-each (lambda (fix colour) (set-in-state! programmer-state fix attr-name colour c-pot-method)) fixtures colours)))) (m-pot (make-smart-potentiometer server addr m-pot-method (lambda (new-vals) (set! colours (map (lambda (old-colour new-m) (cmy (cyan old-colour) new-m (yellow old-colour))) (map colour-as-cmy colours) new-vals)) (for-each (lambda (fix colour) (set-in-state! programmer-state fix attr-name colour m-pot-method)) fixtures colours)))) (y-pot (make-smart-potentiometer server addr y-pot-method (lambda (new-vals) (set! colours (map (lambda (old-colour new-y) (cmy (cyan old-colour) (magenta old-colour) new-y)) (map colour-as-cmy colours) new-vals)) (for-each (lambda (fix colour) (set-in-state! programmer-state fix attr-name colour y-pot-method)) fixtures colours))))) (add-and-run-hook! selection-hook (lambda (selection) (receive (new-fixtures attrs) (fixtures-with-attr selection attr-name) (if (nil? new-fixtures) (begin (osc-send addr (string-append c-pot-method "/disable")) (osc-send addr (string-append m-pot-method "/disable")) (osc-send addr (string-append y-pot-method "/disable"))) (begin (set! fixtures new-fixtures) (set-min-vals c-pot (map (lambda (x) 0) fixtures)) (set-min-vals m-pot (map (lambda (x) 0) fixtures)) (set-min-vals y-pot (map (lambda (x) 0) fixtures)) (set-max-vals c-pot (map (lambda (x) 100) fixtures)) (set-max-vals m-pot (map (lambda (x) 100) fixtures)) (set-max-vals y-pot (map (lambda (x) 100) fixtures)) (set! colours (current-values fixtures attr-name)) (set-initial-vals c-pot (map cyan (map colour-as-cmy colours))) (set-initial-vals m-pot (map magenta (map colour-as-cmy colours))) (set-initial-vals y-pot (map yellow (map colour-as-cmy colours))) (reset-gradients c-pot) (reset-gradients m-pot) (reset-gradients y-pot) (osc-send addr (string-append c-pot-method "/enable")) (osc-send addr (string-append m-pot-method "/enable")) (osc-send addr (string-append y-pot-method "/enable")))))) (get-selection)) (add-update-hook! programmer-state (lambda (source) (unless (or (eq? source c-pot-method) (eq? source m-pot-method) (eq? source y-pot-method)) (set! colours (current-values fixtures attr-name)) (set-initial-vals c-pot (map cyan (map colour-as-cmy colours))) (set-initial-vals m-pot (map magenta (map colour-as-cmy colours))) (set-initial-vals y-pot (map yellow (map colour-as-cmy colours))) (reset-gradients c-pot) (reset-gradients m-pot) (reset-gradients y-pot)))))))