;; sliders.lsp -- communicate with NyquistIDE to implement control panels
;; Roger B. Dannenberg
;; April 2015

;;    (stop-on-zero s) -- a sound that returns 1 until s goes to zero, then
;;            the sound terminates. If s comes from a slider and you multiply
;;            a sound by (stop-on-zero s), you can interactively stop it
;;    (make-slider-panel "name" color) -- sets panel name for the following
;;            sliders
;;    (make-slider "param" [initial [low high]]) -- create slider named 
;;            "param" with optional range and initial value. Also returns
;;            a sound.
;;    (make-button "param" normal) -- create a button named "param" with
;;            a starting value of normal (either 0 or 1). While the button
;;            in the panel is pressed, the value changes to 1 or 0.
;;    (get-slider-value "param") -- when called with a string, this looks up
;;            the slider value by name
;;    (slider-panel-close "name") -- close the panel window. Values of any 
;;            existing sliders become undefined.
;;    (slider "panel" "name" [dur]) -- make a signal from slider value
;;    (slider "name" [dur]) -- make a signal from slider in current panel
;;    (get-slider-value "panel" "name") -- get a float value
;;    (get-slider-value "name") -- get a float in current panel

;; *active-slider-panel* is the current panel to which sliders are added
;;
(if (not (boundp '*active-slider-panel*))
    (setf *active-slider-panel* nil))

;; *panels-in-use* is an assoc list of panels, where each panel
;;   is a list of allocated sliders stored as (name number)
;;
(if (not (boundp '*panels-in-use*))
    (setf *panels-in-use* nil))

;; allocate-slider-num -- find an unused slider number
;;   linear search is used to avoid maintaining a parallel structure
;;   for faster searching. We search starting at slider #10, leaving
;;   sliders 0-9 unused; for example, you might want to control them
;;   via open sound control, so this gives you 10 sliders that are
;;   off limits to allocation by the SLIDER function.
;;   
;;   This code takes advantage of the fact that dotimes and dolist
;;   return nil when they end normally, so we signal that we found
;;   or did not find i by explicitly returning. Note that RETURN
;;   returns from the innermost dotimes or dolist -- they do not
;;   return from allocate-slider-num.
;;
(defun allocate-slider-num ()
  (dotimes (n 990)
    (let ((i (+ n 10)))
      (cond ((not (dolist (panel *panels-in-use*)
                    (cond ((dolist (pair (cdr panel))
                             (cond ((eql (second pair) i) (return t))))
                           (return t)))))
              (return i))))))

;; remove panel from list of panels
(defun slider-panel-free (panel)
  (setf *panels-in-use* (remove panel *panels-in-use* :test #'equal)))

(setfn stop-on-zero snd-stoponzero)

(defun make-slider-panel (name &optional (color 0))
  (let ((panel (assoc name *panels-in-use* :test #'equal)))
    ;; first find if panel already exists. If so, free the resources
    (cond (panel
           (slider-panel-free panel)))
    (setf *active-slider-panel* (list name))
    (setf *panels-in-use* (cons *active-slider-panel* *panels-in-use*))
    (format t "slider-panel-create: \"~A\" ~A~%" name color)))

(defun make-slider (name &optional (init 0) (low 0) (high 1))
  (let ((num (allocate-slider-num)))
    (cond ((null num)
           (format t "WARNING: MAKE-SLIDER is out of slider numbers. ~A~%"
                     "No slider created."))
          ((not (and (stringp name) (numberp init) 
                     (numberp low) (numberp high)))
           (display 
            "WARNING: MAKE-SLIDER called with bad arguments. No slider created"
            name init low high)))
    ;; make sure we have an active panel
    (cond ((null *active-slider-panel*)
           (make-slider-panel "Controls")))
    ;; insert new slider into list of sliders in active panel. This
    ;; is aliased with an element in the assoc list *panels-in-use*.
    (rplacd *active-slider-panel* (cons (list name num) 
                                        (cdr *active-slider-panel*)))
    (format t "slider-create: \"~A\" ~A ~A ~A ~A~%" name num init low high)
    num))

(defun make-button (name &optional (normal 0))
  (let ((num (allocate-slider-num)))
    (cond ((null num)
           (format t "WARNING: MAKE-BUTTON is out of slider numbers. ~A~%"
                     "No button created."))
          ((not (and (stringp name) (numberp normal)))
           (display 
            "WARNING: MAKE-BUTTON called with bad arguments. No button created"
            name normal)))
    ;; make sure we have an active panel
    (cond ((null *active-slider-panel*)
           (slider-panel "Controls")))
    ;; insert new button into list of controls in active panel. This
    ;; is aliased with an element in the assoc list *panels-in-use*.
    (rplacd *active-slider-panel* (cons (list name num) 
                                        (cdr *active-slider-panel*)))
    (format t "button-create: \"~A\" ~A ~A~%" name num normal)
    num))

(defun close-slider-panel (name)
  (let ((panel (assoc name *panels-in-use* :test #'equal)))
    (cond ((not (stringp name))
           (display "WARNING: SLIDER-PANEL-CLOSED called with bad argument."
                    name)))
    (cond (panel
           (slider-panel-free panel)
           (format t "slider-panel-close: \"~A\"~%" name))
          (t
           (format t "WARNING: slider panel ~A not found.~%" name)))))

;; SLIDER-LOOKUP - find the slider by name
;;
(defun slider-lookup (name slider)
  (let ((panel (assoc name *panels-in-use* :test #'equal)) s)
    (cond ((null panel)
           (error "Could not find slider panel named" name)))
    (setf s (assoc slider (cdr panel) :test #'equal))
    (cond ((null s)
           (error "Could not find slider named" s)))
    (second s)))


;; SLIDER - creates a signal from real-time slider input
;; 
;; options are:
;;   (SLIDER number [dur])
;;   (SLIDER "name" [dur]) -- look up slider in current slider panel
;;   (SLIDER "panel" "name" [dur]) -- look up panel, then look up slider
;;
(defun slider (id &optional slider-name dur)
    (cond ((and (numberp id) (null slider-name))
           (setf dur 1.0))
          ((and (numberp id) (numberp slider-name) (null dur))
           (setf dur slider-name))
          ((and (stringp id) (null slider-name))
           (setf dur 1.0)
           (setf id (slider-lookup (car *active-slider-panel*) id)))
          ((and (stringp id) (numberp slider-name) (null dur))
           (setf dur slider-name)
           (setf id (slider-lookup (car *active-slider-panel*) id)))
          ((and (stringp id) (stringp slider-name) (null dur))
           (setf dur 1.0)
           (setf id (slider-lookup id slider-name)))
          ((and (stringp id) (stringp slider-name) (numberp dur))
           (setf id (slider-lookup id slider-name)))
          (t
           (error "SLIDER called with invalid arguments")))
    (setf dur (get-duration dur))
    (setf id (round id)) ;; just to make sure it's an integer
    (cond ((or (< id 0) (>= id 1000))
           (error "SLIDER index out of bounds" id)))
    (display "slider" id slider-name dur)
    (snd-slider id *rslt* *sound-srate* dur))


(if (not (boundp '*lpslider-cutoff*))
    (setf *lpslider-cutoff* 20.0))

(defun lpslider (id &optional slider-name dur)
  (lp (slider id slider-name dur) 20.0))

;; save built-in get-slider-value so we can redefine it
(if (not (fboundp 'prim-get-slider-value))
    (setfn prim-get-slider-value get-slider-value))

(defun get-slider-value (id &optional slider-name)
  (cond ((and (numberp id) (null slider-name)) nil)
        ((and (stringp id) (null slider-name))
         (setf id (slider-lookup (car *active-slider-pael*) id)))
        ((and (stringp id) (stringp slider-name))
         (setf id (slider-lookup id slider-name)))
        (t
         (error "GET-SLIDER-VALUE called with invalid arguments")))
  ;; further parameter checking is done in get-slider-value:
  (prim-get-slider-value id))

(autonorm-off)
(snd-set-latency 0.02)
(print "**********sliders.lsp************************")
(print "WARNING: AUTONORM IS NOW TURNED OFF")
(print "WARNING: AUDIO LATENCY SET TO 20MS")
(print "To restore settings, execute (autonorm-on) and")
(print "  (set-audio-latency 0.3)")
(print "*********************************************")
