diff --git a/nyquist/aud-do-support.lsp b/nyquist/aud-do-support.lsp index be3f7a306..eca183fe9 100644 --- a/nyquist/aud-do-support.lsp +++ b/nyquist/aud-do-support.lsp @@ -15,120 +15,209 @@ (if (char/= (char str i) ch) (setf out (format nil "~a~a" out (char str i)))))) +(defun number-string-p (str) + ;;; like digit-char-p for strings + (unless (stringp str) + (return-from number-string-p nil)) + (let ((num (string-to-number str))) + (if (numberp num) + num + nil))) (defmacro string-append (str &rest strs) ;;; Append one or more strings to 'str' `(setf ,str (strcat ,str ,@strs))) - -(defun aud-get-command (id) - ;;; Return command signature from id string or NIL. - (let* ((helpstr (format nil "Help: Command=~s Format=LISP" id)) - (cmd-sig (aud-do helpstr))) +(defun aud-print-command (cmd) + ;;; Print a quick reference for command arguments. + (let ((help-data (first (aud-do-command "Help" :command cmd :format "LISP"))) + (out (format nil "(aud-do-command ~s [:key val ...])~%" (string-downcase cmd)))) (cond - ((not (listp cmd-sig)) (error "Unknown error in aud-do" cmd-sig)) - ((string-equal (first cmd-sig) "Command not found") nil) - (t (setf cmd-sig (first cmd-sig)) - (eval-string (quote-string cmd-sig)))))) + ((string-equal help-data "Command not found") + ;Debug out can be copied on all platforms. + (format t "~a~a." out help-data) + (format nil "~a~a." out help-data)) + (t (setf help-data (eval-string (quote-string help-data))) + (let ((params (second (assoc 'params help-data)))) + (dolist (p params) + (setf out (format nil "~a :~a (~a) default: ~s~%" + out + (string-downcase (second (assoc 'key p))) + (second (assoc 'type p)) + (second (assoc 'default p)))) + (let ((enums (assoc 'enum p))) + (when enums + (setf out (format nil "~a [" out)) + (dolist (e (second enums)) + (setf out (format nil "~a~s " out e))) + (setf out (format nil "~a]~%" (string-right-trim " " out))))))) + (format t "~a" out) + out)))) -(defun aud-import-command (cmd &optional func-name) - ;;; Generate a LISP function from Audacity command ID or signature. - ;;; If supplied, the generated function name will be 'func-name', otherwise - ;;; it will be the command id, preceeded by 'aud-'. - (when (stringp cmd) - ;; cmd is the id, so get the command signature - (let ((id cmd)) - (setf cmd (aud-get-command id)) - (if cmd - (aud-import-command cmd func-name) - (error "in aud-import-command, invalid argument" id)))) - (let ((id (second (assoc 'id cmd))) - (params (second (assoc 'params cmd))) - (func-def "(defun aud-") - (func-kwargs "(&key ") - (func-body "")) - (if func-name - (setf func-def (format nil "(defun ~a " func-name)) - (string-append func-def id " ")) +(defun aud-do-command (id &rest params) + ;; Translate aud-do-command, to (aud-do "command"). + ;; To avoid unnecessary overhead, only validate when debugging enabled + (when (and (= (length params) 1) + (listp (first params))) + ;Unpack params from "aud-" stubs + (setf params (first params))) + (when *tracenable* + (aud-check-debug-cache) + (let (val-allowed type enums pstr + (id-valid (aud-verify-command-id id)) + (valid-params (aud-get-command-params id)) + (keystr "")) + (if (not id-valid) + (format t "Debug data unavailable: ~s.~%" id) + (dolist (p params) + (setf pstr (format nil "~a" p)) + (cond + ((char= (char pstr 0) #\:) ;keyword + (setf keystr (subseq pstr 1)) + (let ((kf (dolist (vp valid-params nil) + (when (string-equal (second (assoc 'key vp)) keystr) + (return vp))))) + (cond + (kf ;keyword found + (setf type (second (assoc 'type kf))) + (setf enums (second (assoc 'enum kf))) + (cond + ((member type '("int" "float" "double") :test 'string-equal) + (setf val-allowed "number")) + ((string-equal type "enum") + (setf val-allowed enums)) ;a list + (t (setf val-allowed type)))) ;"string" "bool" or NIL + (t (format t "Invalid key in ~s :~a~%" id keystr))))) + (t ;key value + (cond + ((not val-allowed) + (format t "Too many arguments: ~s :~a~%" id keystr)) + ((listp val-allowed) + (unless (member pstr enums :test 'string=) ;case sensitive + (format t "Invalid enum in ~s :~a - ~s~%" id keystr p))) + ((string= val-allowed "bool") + (unless (or (string= pstr "0") (string= pstr "1")) + (format t "~s :~a value must be 0 or 1~%" id keystr))) + ((string= val-allowed "number") + (unless (or (numberp p) (number-string-p p)) + (format t "~s :~a value must be a number: ~s~%" id keystr p))) + ((string= val-allowed "string") + (unless (stringp p) + (format t "~s :~a value must be a string: ~a~%" id keystr p)))) + (psetq val-allowed nil + type nil + enums nil))))))) + ;; Send the command + (let ((cmd (format nil "~a:" id))) (dolist (p params) - (let* ((key (second (assoc 'key p))) - (type (second (assoc 'type p))) - (enums (second (assoc 'enum p))) - ; The kwarg value must be a valid Lisp variable name (no spaces). - (val (char-remove #\Space key))) - (string-append func-kwargs val " ") - ;; Convert list of 'enums' to a string with quoted enums so we can string compare. - (if enums - (let ((str-enum "(list ")) - (dolist (e enums) - (string-append str-enum "\"" (string e) "\" ")) - (setf enums (string-append str-enum ")"))) - (setf enums "")) - ;; Add validators for each parameter to function body. - (string-append func-body -" (when " val " - (unless (validate " val " \"" type "\" " enums ")(error \"bad argument type\" " val ")) - (push (format nil \"\\\"" key "\\\"=~s \" " val ") params))\n"))) - ;; concatenate strings to build the complete function. - (string-append func-def func-kwargs "&aux (params ()))\n" -" ;; Push validated 'val's onto 'params' list - (defun validate (val type &optional enums) - (cond - ((string-equal type \"bool\") - (or (= val 0)(= val 1))) - ((string-equal type \"string\") - (stringp val)) - ((string-equal type \"enum\") - (member val enums :test 'string=)) - ((string-equal type \"int\") - (integerp val)) - ((string-equal type \"float\") - (numberp val)) - ((string-equal type \"double\") - (numberp val))))\n" - func-body -" - (setf command \"" id ": \") - (dolist (p params) - (setf command (strcat command p))) - (aud-do command))") - (eval-string func-def))) + (setf p (format nil "~a" p)) + (string-append cmd + (cond + ((char= (char p 0) #\:) ;keyword + (format nil " ~a=" (subseq p 1))) + (t ;key value + (format nil "~s" p))))) + (aud-do cmd))) -(defun aud-generate-command-stubs (cmd-list) - ;; Generate one stub for each function. - ;; Stubs check that command is actually available before - ;; generating the Lisp function. - ;; This function is for internal use only. - (dolist (cmd-id cmd-list) - (let ((func-def (format nil -"(defun aud-~a (&rest args) -(if (string-equal (first (aud-do \"Help: Command=~a\")) \"Command not found\") -(error \"Command unavailable\" ~s)) -(aud-import-command ~s) -(let ((arg-string \"\") (cmd-string \"(aud-~a \")) -(dolist (arg args) -(setf arg-string (format nil \"~a ~a\" arg-string arg))) -(setf cmd-string (format nil \"~a~a)\" cmd-string arg-string)) -(eval-string cmd-string)))" -cmd-id cmd-id cmd-id cmd-id cmd-id "~a" "~s" "~a" "~a"))) - (eval-string func-def)))) +(defun aud-import-effects (&aux cmd) + ;; Generate function stubs in the form (aud- [&key arg ...]) + ;; Call once to make "aud-"s available. + ;; Unfortunatly we can't call this on load, as the cache may + ;; not exist yet, and we don't want to delay loading for regular users. + (unless (fboundp 'aud-do-version) + (aud-check-debug-cache)) + (dolist (cmd (aud-get-command)) + (setf cmd (second (assoc 'id cmd))) + (let ((symb (intern (string-upcase (format nil "aud-~a" cmd))))) + (eval `(defun ,symb (&rest args) + (aud-do-command ,cmd args)))))) -;; Hard coded list because "GetInfo:" is slow and we can't yet exclude -;; Nyquist plug-ins (Nyquist plug-ins can't run from Nyquist Macros). -;; TODO: Create a fast scripting command to return this list instead of relying on hard coded. -(aud-generate-command-stubs - (list "Amplify" "AutoDuck" "BassAndTreble" "ChangePitch" "ChangeSpeed" - "ChangeTempo" "Chirp" "ClickRemoval" "Compressor" "DtmfTones" - "Distortion" "Echo" "FadeIn" "FadeOut" "FilterCurve" "FindClipping" - "GraphicEq" "Invert" "LoudnessNormalization" "Noise" "Normalize" - "Paulstretch" "Phaser" "Repeat" "Repair" "Reverb" "Reverse" - "Silence" "SlidingStretch" "Tone" "TruncateSilence" "Wahwah" - ;; Scriptable Commands - "CompareAudio" "Demo" "Export2" "GetInfo" "GetPreference" "Help" - "Import2" "Message" "OpenProject2" "SaveProject2" "Screenshot" - "SelectFrequencies" "SelectTime" "SelectTracks" "Select" "SetClip" - "SetEnvelope" "SetLabel" "SetPreference" "SetProject" "SetTrackAudio" - "SetTrackStatus" "SetTrackVisuals" "SetTrack")) +(defun aud-check-debug-cache () + ;;; Load aud-do-debug-data-cache, updating if necessary. + (let ((fqname (format nil "~a~a~a" + (string-right-trim (string *file-separator*) (get-temp-path)) + *file-separator* + "aud-do-debug-data-cache.lsp"))) + (cond ;Update if necessary + ((fboundp 'aud-do-version) ;cache is loaded + ;is cache the current version? Reload aud-do-version if loaded version old. + (when (and (string/= (format nil "~a" (aud-do-version)) + (format nil "~a" (get '*audacity* 'version))) + (string/= (format nil "~a" (autoload-helper fqname 'aud-do-version nil)) + (format nil "~a" (get '*audacity* 'version)))) + ;wrong version, so refresh cache. + (aud-refresh-debug-data-cache))) + (t ;cache not loaded, so try loading and refresh if we can't. + (unless (load fqname :verbose t) + (aud-refresh-debug-data-cache)))))) + + +(defun aud-refresh-debug-data-cache () + ;; Cache the list of command profiles as function "aud-get-command", and load it. + (labels ((disable-plugins (typestring &aux oldval) + (let ((getcmd (format nil "GetPreference: Name=\"~a/Enable\"" typestring))) + (setf oldval (first (aud-do getcmd))) + (do-set-val typestring oldval 0) ;Disable all plug-ins + oldval)) ;may be 0, 1 or "" + (do-set-val (typestring oldval newval) + (let ((setcmd (format nil "SetPreference: Name=\"/~a/Enable\" Value=" typestring))) + (when (and oldval (or (string= oldval "")(string= oldval "1"))) + (aud-do (format nil "~a~s" setcmd (if (= newval 0) 0 oldval)))))) + (get-usable-commands () + (let ((cmds '(("Nyquist" ny)("LADSPA" la)("LV2" lv)("VST" vs)("AudioUnit" au)("Vamp" va))) + info) + (dolist (cmd cmds) + (setf (nth 1 cmd) (disable-plugins (nth 0 cmd)))) + (setf info (first (aud-do "getinfo: type=Commands format=LISP"))) ;Get scriptables and built-in effects + (dolist (cmd cmds) + (do-set-val (nth 0 cmd) (nth 1 cmd) 1)) ;Re-enable plug-ins + info))) + (let ((path (get-temp-path)) fp) + (unless path + (return-from aud-refresh-debug-data-cache + (format t "Error: Audacity data directory not found."))) + ; file-separator at end of 'get-temp-path' is platform dependent. + (let* ((fqname (format nil "~a~a~a" + (string-right-trim (string *file-separator*) (get-temp-path)) + *file-separator* + "aud-do-debug-data-cache.lsp")) + (fp (open fqname :direction :output))) + (cond + (fp (format fp +";; Intended for internal use by aud-do-command.~% +(defun aud-do-version () + '~a)~% +(defun aud-verify-command-id (id) + (second (assoc 'id (aud-get-command id))))~% +(defun aud-get-command-params (id) + (second (assoc 'params (aud-get-command id))))~% +(defun aud-get-command (&optional id &aux cmds) + ;; If id supplied, return command profile or nil. + ;; Else, return full list. + (setf cmds + '~a) + ;; Return all commands, or one command or nil. + (if id + (dolist (cmd cmds nil) + (when (string-equal id (second (assoc 'id cmd))) + (return cmd))) + cmds))" + (get '*audacity* 'version) + (get-usable-commands)) + (format t "Debug data cache refreshed.~%") + (close fp) + (unless (load fqname :verbose t) ;load the file + (error "Unable to load" fqname))) ;assert + (t (format t "Error: ~a cannot be written." fqname))))))) + + +;; Try to load AUD- command cache. +(when (get-temp-path) + (let ((fqname (format nil "~a~a~a" + (string-right-trim (string *file-separator*) (get-temp-path)) + *file-separator* + "aud-do-debug-data-cache.lsp"))) + (load fqname :verbose t))) diff --git a/nyquist/init.lsp b/nyquist/init.lsp index 038bf0bd7..a90ab250b 100644 --- a/nyquist/init.lsp +++ b/nyquist/init.lsp @@ -86,4 +86,4 @@ ;;; Load wrapper functions for aud-do commands. ;;; If commented out, "aud-do-support.lsp" may be loaded by a plug-in. ;;; Example: (lisp-loader (strcat *runtime-path* "aud-do-support.lsp")) -(load "aud-do-support.lsp") +(load "aud-do-support.lsp" :verbose nil)