$nyquist plug-in
$version 4
$type tool generate
$name (_ "Sample Data Import")
$manpage "Sample_Data_Import"
$action (_ "Reading and rendering samples...")
$author (_ "Steve Daulton")
$release 2.3.0
$copyright (_ "Released under terms of the GNU General Public License version 2")

$control filename (_ "Select file") file "" "*default*/sample-data.txt" (((_ "Text file") (txt TXT))
                        ((_ "All files") (""))) "open,exists"
$control bad-data (_ "Invalid data handling") choice (("ThrowError" (_ "Throw Error"))
                                                      ("ReadAsZero" (_ "Read as Zero"))) 0


;; 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


;; Check file can be opened
(defun fileopensp (fname)
  (cond
    ((not (setf fstream (open fname)))
        (throw 'err (format nil (_ "Error~%~
                        '~a' could not be opened.~%~
                        Check that file exists.")
                        fname)))
    ; File opened OK, so check for normal ASCII, then close it and return 'true'
    (t  (do ((j 0 (1+ j))(b (read-byte fstream)(read-byte fstream)))
            ((or (> j 100000)(not b)))
          (when (> b 127)
            (throw 'err (format nil (_ "Error:~%~
              The file must contain only plain ASCII text.~%~
              (Invalid byte '~a' at byte number: ~a)") b (1+ j) ))))
        (close fstream)
        t)))

;; ':new' creates a new class 'streamreader'
;; 'filestream' and 'chanel' are its instance variables.
;; (every object of class 'streamreader' has its own
;; copy of these variables)
(setq streamreader
  (send class :new '(filestream chanel)))

;; Initialize class 'streamreader'
(send streamreader :answer :isnew '(stream ch) '(
    (setq filestream stream)
    (setq channel ch)))

;; Create ':next' method.
;; Snd-fromobject calls this method to obtain the
;; next sound sample until it receives 'nil'
(send streamreader :answer :next '() '(
    (case channel
      (0  ;mono
        (read-and-verify filestream))
      (1  ;left channel
        ;Note, we still need to verify data even if skipping it.
        (let ((val (read-and-verify filestream)))
          (read-and-verify filestream) ;skip right channel sample
          val))
      (t  ;right channel
        (read-and-verify filestream) ;skip left channel sample
        (read-and-verify filestream)))))

(defun read-and-verify (stream)
"snd-fromobject requires float values, nil to terminate"
  (let ((val (read stream)))
    (cond
      ((not val) nil) ;end of file
      ((numberp val)  (float val)) ;valid.
      ((= bad-data 0) ;invalid. Throw error and quit
          (throw 'err (format nil (_ "Error~%~
              Data must be numbers in plain ASCII text.~%~
              '~a' is not a numeric value.") val)))
      (t  0.0)))) ;invalid. Replace with zero.

;; Instantiate a new sound object
(defun make-sound-object (stream chan)
  (send streamreader :new stream chan))

(defun sound-from-file (filename)
  ;; Set path. fileopenp should return 'true'
  (if (not (fileopensp filename))
      (throw 'err (format nil (_ "Error.~%Unable to open file"))))
  ; Note: we can't use (arrayp *track*) because
  ; *track* is nil in generate type plug-ins.
  (cond 
    ((= (get '*track*  'channels) 2)
        (let ((left-snd (get-sound filename 1))
              (right-snd (get-sound filename 2)))
          (vector left-snd right-snd)))
    (t  ;; Mono track
        (get-sound filename 0))))

(defun get-sound (fname chan)
  (let* ((stream (open fname :direction :input))
         (left (make-sound-object stream chan)))
    (setf audio-out (snd-fromobject 0 *sound-srate* left))
    (snd-play audio-out) ;force samples to be calculated now.
    (close stream)
    audio-out))

(catch 'err (sound-from-file filename))
