$nyquist plug-in
$version 4
$type tool
$name (_ "EQ XML to TXT Converter")
$manpage "EQ_XML_to_TXT_Converter"
$debugbutton false
$preview disabled
$author (_ "Steve Daulton")
$release 3.0.0
$copyright (_ "Released under terms of the GNU General Public License version 2")


;; Released under terms of the GNU General Public License version 2:
;; http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
;;
;; For information about writing and modifying Nyquist plug-ins:
;; https://wiki.audacityteam.org/wiki/Nyquist_Plug-ins_Reference


$control fxname (_ "Select target EQ effect") choice (("Graphic" (_ "Graphic EQ"))
                                                      ("FilterCurve" (_ "Filter Curve EQ"))) 0

$control infile (_ "Equalization XML file") file "" "*default*/EQCurves.xml" (((_ "XML file") (xml XML))
                        ((_ "All files") ("")))  "open,exists"

$control overwrite (_ "If output text file exists") choice (("Append" (_ "Append number"))
                                                            ("Overwrite" (_ "Overwrite"))
                                                            ("Error" (_ "Error"))) 0

; output message
(setf success
  (format nil "Input:~%~a~%~%Output:~%" infile))


(defun process-xml (filename)
  (let ((fp-in (open filename :direction :input))
        curve-name
        iscurve
        gotcurve
        (frequencies ())
        (values ()))
    (unless fp-in
      (throw 'err
          (format nil (_ "Error.~%Unable to open file~%~s") filename)))
    ;; Read each line and get the (frequency value) pair, and the curve name.
    (do ((line (read-line fp-in) (read-line fp-in)))
        ((not line))
      (let ((fxname (get-name line)))
        (when fxname
          ;; trim in two steps so that we don't remove characters from the curve name.
          (setf curve-name (string-trim "\t name=" fxname))
          (setf curve-name (string-trim "\t \"" curve-name))
          (setf iscurve t)))
      (when iscurve
        (let ((f-v-pair (get-f-v-pair (string-trim " <>/point\t\r" line))))
          (when f-v-pair
            (push (first f-v-pair) frequencies)
            (push (second f-v-pair) values)))
        (when (get-end-of-curve line)
          (setf gotcurve t)
          (setf iscurve nil)))
      (when gotcurve
        (write-curve-txt curve-name frequencies values)
        (psetq gotcurve nil
               curve-name nil
               frequencies ()
               values ())))
    (close fp-in)
    (format nil success)))

(defun get-f-v-pair (line &aux (f "")(v ""))
  ;;Return (list frequency value) or nil
  (cond
    ;; Get frequency
    ((and (> (length line) 3)(string-equal (subseq line 0 3) "f=\""))
      (setf line (subseq line 3))
      (setf idx ;number of characters to remove from string
        (dotimes (i (length line))
          (setf ch (char line i))
          (cond
            ((char= ch #\-) (setf f (format nil "~a~a" f #\-)))
            ((digit-char-p ch) (setf f (format nil "~a~a" f ch)))
            ((char= ch #\.) (setf f (format nil "~a~a" f #\.)))
            (t  (return (1+ i))))))
      ;; We now have the frequency., and the number of characters.
      (setf v (string-trim " \td=\"" (subseq line idx)))
      (list f v))))

(defun get-name (line)
  ;; If line is in the form: "  <curve name=\"Name of curve\">  "
  ;; Return "Name of curve", else NIL.
  (let ((line (string-trim " <>\t\r" line)))
    (cond
      ((< (length line) 6) nil)
      ((string-equal (subseq line 0 6) "curve ")
        (subseq line 5))
      (t nil))))

(defun get-end-of-curve (line)
  ;; If the line is "   </curve>  "
  ;; Return T else NIL.
  (let ((line (string-trim " \t\r" line)))
    (if (string-equal line "</curve>") t nil)))

(defun write-curve-txt (curvename frequencies values)
  ;; Write new style curve file(s), ensuring no overwrites unless explicitly allowed.
  (let ((outfile (get-out-file-name (get-path infile) curvename))
        fp-out)
    (if outfile
        (setf fp-out (open outfile :direction :output))
        (throw 'err
            (format nil
                    ;i18n-hint: Do not translate "~a".txt
                    (_ "Error.~%File overwrite disallowed:~%\"~a.txt\"")
                    curvename)))
    (unless fp-out
      (throw 'err (format nil
                          (_ "Error.~%File cannot be written:~%\"~a.txt\"")
                          curvename)))
    (format fp-out "~a" (if (= fxname 0) "GraphicEq:" "FilterCurve:"))
    (dotimes (i (length frequencies))
      (format fp-out "f~a=~s " i (nth i frequencies)))
    (format fp-out
            "filterLength=\"8191\" InterpolateLin=\"0\" ~
            InterpolationMethod=\"B-spline\"")
    (dotimes (i (length frequencies))
      (format fp-out " v~a=~s" i (nth i values)))
    (close fp-out)
    (string-append success outfile "\n")))

(defun get-path (fqname)
  ;; Return file path from fully qualified file name.
  (do ((i (1- (length fqname)) (1- i)))
      ((= i 0) nil)
    (when (char= (char fqname i) *file-separator*)
      (return-from get-path (subseq fqname 0 (1+ i))))))

(defun get-out-file-name (path fname)
  ; Return a fully qualified name or nil.
  (case overwrite
    (1  ;Overwrite
      (format nil "~a~a.txt" path fname))
    (2  ;Error
      (let* ((fqname (format nil "~a~a.txt" path fname))
             (fp (open fqname)))
        (cond
          (fp (close fp)
              nil)
          (t fqname))))
    (t  ; If file already exists, append a number to the name (up to 99).
      (dotimes (i 99 nil)
        (let* ((fqname  (format nil "~a~a~a.txt"
                                path fname (if (> i 0) i "")))
               (fp (open fqname)))
          (if fp
              (close fp)
              (return-from get-out-file-name fqname)))))))

(catch 'err (process-xml infile))
