;; spectral-analysis.lsp -- functions to simplify computing
;;   spectrogram data
;;
;; Roger B. Dannenberg and Gus Xia
;; Jan 2013, modified Oct 2017

;; API:
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; set sa-obj = sa-init(resolution: <nil or Hz>,
;;                      fft-dur: <nil or seconds>,
;;                      skip-period: <seconds>,
;;                      window: <window type>, 
;;                      input: <filename or sound>)
;; 
;; sa-init() creates a spectral-analysis object that can be used
;; to obtain spectral data from a sound.
;;
;; resolution is the width of each spectral bin in Hz. If nil of
;;     not specified, the resolution is computed from fft-dur. 
;;     The actual resolution will be finer than the specified 
;;     resolution because fft sizes are rounded to a power of 2.
;; fft-dur is the width of the FFT window in seconds. The actual
;;     FFT size will be rounded up to the nearest power of two
;;     in samples. If nil, fft-dur will be calculated from 
;;     resolution. If both fft-size and resolution are nil
;;     or not specified, the default value of 1024 samples,
;;     corresponding to a duration of 1024 / signal-sample-rate,
;;     will be used. If both resolution and fft-dur are
;;     specified, the resolution parameter will be ignored.
;;     Note that fft-dur and resolution are reciprocals.
;; skip-period specifies the time interval in seconds between 
;;     successive spectra (FFT windows). Overlapping FFTs are
;;     possible. The default value overlaps windows by 50%. 
;;     Non-overlapped and widely spaced windows that ignore 
;;     samples by skipping over them entirely are also acceptable.
;; window specifies the type of window. The default is raised
;;     cosine (Hann or "Hanning") window. Options include
;;     :hann, :hanning, :hamming, :none, nil, where :none and
;;     nil mean a rectangular window.
;; input can be a string (which specifies a sound file to read)
;;     or a Nyquist SOUND to be analyzed.
;; Return value is an XLISP object that can be called to obtain
;;     parameters as well as a sequence of spectral frames.
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; set sa-frame = sa-next(sa-obj)
;;
;; sa-next() fetches the next spectrum from sa-obj.
;;
;; sa-obj is a spectral-analysis object returned by sa-init().
;; Return value is an array of FLONUMS representing the discrete
;;     spectrum.
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; exec sa-info(sa-obj)
;;
;; sa-info prints information about the spectral computation.
;;
;; sa-obj is a spectral-analysis object returned by sa-init().
;; Return value is nil, but information is printed.
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; set mag = sa-magnitude(frame)
;;
;; sa-magnitude computes the magnitude (amplitude) spectrum
;; from a frame returned by sa-frame.
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; exec sa-plot(sa-obj, sa-frame)
;;
;; sa-plot plots the amplitude (magnitude) spectrum of sa-frame.
;;
;; sa-obj is used to determine the bin width of data in sa-frame.
;;
;; sa-frame is a spectral frame (array) returned by sa-next()
;;
;; Return value is nil, but a plot is generated and displayed.
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; set hz = sa-get-bin-width(sa-obj)
;; set n = sa-get-fft-size(sa-obj)
;; set secs = sa-get-fft-dur(sa-obj)
;; set window = sa-get-fft-window(sa-obj)
;; set skip-period = sa-get-skip-period(sa-obj)
;; set m = sa-get-fft-skip-size(sa-obj)
;; set sr = sa-get-sample-rate(sa-obj)
;;
;; These functions retrieve data from the sa-obj created by 
;; sa-init. The return values are:
;;   hz - the width of a frequency bin (also the separation
;;       of bin center frequencies). The center frequency of
;;       the i'th bin is i * hz.
;;   n - the size of the FFT, an integer, a power of two. The
;;       size of a spectral frame (an array returned by sa-next)
;;       is (n / 2) + 1.
;;   secs - the duration of an FFT window.
;;   window - the type of window used (:hann, :hamming, :none)
;;   skip-period - the time in seconds of the skip (the time
;;       difference between successive frames
;;   m - the size of the skip in samples.
;;   sr - the sample rate of the sound being analyzed (in Hz, a flonum)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;


;; define the class of spectral analysis objects
(setf sa-class (send class :new '(sound length skip window window-type)))

(send sa-class :answer :next '() '(
    (snd-fft sound length skip window)))

(defun sa-raised-cosine (alpha beta)
  (sum (const alpha)
       (scale beta (lfo 1.0 1.0 *sine-table* 270))))

(defun sa-fft-window (frame-size alpha beta)
  (abs-env (control-srate-abs frame-size                
               (sa-raised-cosine alpha beta))))

(defun hann-window (frame-size) (sa-fft-window frame-size 0.5 0.5))
(defun hamming-window (frame-size) (sa-fft-window frame-size 0.54 0.46))

(defun sa-get-window-type (win-type)
  (case win-type
    ((:hann :hanning)    :hann)
    ((nil :none)         :none)
    (:hamming            :hamming)
    (t (print "Warning: invalid window-type parameter: ~A~%" win-type)
       (print "    Using :HAMMING instead.~%")
       :hamming)))


(defun sa-compute-window (len win-type)
  (case win-type
    (:hann        (hann-window len))
    (:none        nil)
    (:hamming     (hamming-window len))
    (t (print "Warning: invalid window-type paramter: ~A~%" win-type)
       (print "    Using :HAMMING instead.~%")
       (hamming-window len))))
  

(send sa-class :answer :isnew '(snd len skp win-type) '(
    (setf sound snd)
    (setf length len)
    (setf skip skp)
    (setf window-type (sa-get-window-type win-type))
    (setf window (sa-compute-window length window-type))))


;; sa-to-mono -- sum up the channels in an array
;;
(defun sa-to-mono (s)
  (let ((mono (aref s 0)))
    (dotimes (i (1- (length s)))
      (setf mono (sum mono (aref s (1+ i)))))
    mono))


(defun sa-init (&key resolution fft-dur skip-period window input)
  (let (len sr n skip)
    (cond ((stringp input)
           (setf input (s-read input))))
    (cond ((arrayp input)
           (format t "Warning: sa-init is converting stereo sound to mono~%")
           (setf input (sa-to-mono input)))
          ((soundp input) ;; so that variables are not "consumed" by snd-fft
           (setf input (snd-copy input))))
    (cond ((not (soundp input))
           (error
            (format nil
             "Error: sa-init did not get a valid :input parameter~%"))))
    (setf sr (snd-srate input))
    (setf len 1024)
    (cond (fft-dur
           (setf len (* fft-dur sr)))
          (resolution
           (setf len (/ sr resolution))))
    ;; limit fft size to between 4 and 2^16
    (cond ((> len 65536)
           (format t "Warning: fft-size reduced from ~A to 65536~%" len)
           (setf len 65536))
          ((< len 4)
           (format t "Warning: fft-size increased from ~A to 4~%" len)
           (setf len 4)))
    ;; round up len to a power of two
    (setf n 4)
    (while (< n len)
      (setf n (* n 2)))
    (setf length n) ;; len is now an integer power of 2
    ;(display "sa-init" length)
    ;; compute skip length - default is len/2
    (setf skip (if skip-period (round (* skip-period sr))
                               (/ length 2)))
    (send sa-class :new input length skip window)))


(defun sa-next (sa-obj)
  (send sa-obj :next))

(defun sa-info (sa-obj)
  (send sa-obj :info))

(send sa-class :answer :info '() '(
  (format t "Spectral Analysis object (instance of sa-class):~%")
  (format t "  resolution (bin width): ~A Hz~%" (/ (snd-srate sound) length))
  (format t "  fft-dur: ~A s (~A samples)~%" (/ length (snd-srate sound)) length)
  (format t "  skip-period: ~A s (~A samples)~%" (/ skip (snd-srate sound)) skip)
  (format t "  window: ~A~%" window-type)
  nil))


(defun sa-plot (sa-obj frame)
  (send sa-obj :plot frame))

(defun sa-magnitude(frame)
  (let* ((flen (length frame))
         (n (/ (length frame) 2)) ; size of amplitude spectrum - 1
         (as (make-array (1+ n))))  ; amplitude spectrum
    ;; first compute an amplitude spectrum
    (setf (aref as 0) (abs (aref frame 0))) ;; DC
    ;; half_n is actually length/2 - 1, the number of complex pairs
    ;;    in addition there is the DC and Nyquist terms, which are
    ;;    real and in the first and last slots of frame
    (setf half_n (1- n))
    (dotimes (i half_n)
      (let* ((i2 (+ i i 2))  ; index of the imag part
             (i2m1 (1- i2)) ; index of the real part
             (amp (sqrt (+ (* (aref frame i2m1) (aref frame i2m1))
                           (* (aref frame i2)   (aref frame i2))))))
        (setf (aref as (1+ i)) amp)))
    (setf (aref as n) (aref frame (1- flen)))
    as)) ;; return the amplitude spectrum
  

(send sa-class :answer :plot '(frame) '(
  (let* ((as (sa-magnitude frame))
         (sr (snd-srate sound)))
    (s-plot (snd-from-array 0 (/ length sr) as)
            sr (length as)))))

(defun sa-get-bin-width (sa-obj)
  (send sa-obj :get-bin-width))

(send sa-class :answer :get-bin-width '()
      '((/ (snd-srate sound) length)))

(defun sa-get-fft-size (sa-obj)
  (send sa-obj :get-fft-size))

(send sa-class :answer :get-fft-size '() '(length))

(defun sa-get-fft-dur (sa-obj)
  (send sa-obj :get-fft-dur))

(send sa-class :answer :get-fft-dur '() '(/ length (snd-srate sound)))

(defun sa-get-fft-window (sa-obj)
  (send sa-obj :get-fft-window))

(send sa-class :answer :get-fft-window '() '(window-type))

(defun sa-get-fft-skip-period (sa-obj)
  (send sa-obj :get-skip-period))

(send sa-class :answer :get-skip-period '() '((/ skip (snd-srate sound))))

(defun sa-get-fft-skip-size (sa-obj)
  (send sa-obj :get-skip-size))

(send sa-class :answer :get-fft-skip-size '() '(skip))

(defun sa-get-sample-rate (sa-obj)
  (send sa-obj :get-sample-rate))

(send sa-class :answer :get-sample-rate '() '((snd-srate sound)))


;;;;;;; TESTS ;;;;;;;;;;


(defun plot-test ()
  (let (frame)
    (setf sa (sa-init :input "./rpd-cello.wav"))
    (while t
      (setf frame (sa-next sa))
      (if (null sa) (return nil))
      (sa-plot sa frame))))

