$nyquist plug-in
$version 4
$type process spectral
$name (_ "Spectral Delete")
$manpage "Spectral_Delete"
$author (_ "Steve Daulton")
$release 3.0.4
$copyright (_ "GNU General Public License v2.0 or later")

;; Released under terms of the GNU General Public License v2.0 or later:
;; 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


(defun sinc (x fc)
  ;; http://www.dspguide.com/ch16/1.htm
  ;; Note that fc is a fraction of the sample rate.
  (if (= x 0)
      (* 2 pi fc)
      (/ (sin (* 2 pi fc x)) x)))

(defun blackman (i M)
  ;; Where: 0 <= i <= M
  (+ 0.42
     (* -0.5 (cos (/ (* 2.0 pi i) M)))
     (* 0.08 (cos (/ (* 4 pi i) M)))))

(defun calc-kernel (size fc)
  ;; Generate windowed sinc kernel impulse
  (when (oddp size)
    (error "Size of sinc filter must be even"))
  (let ((ar (make-array (1+ size)))
        (norm 0)  ;Normalization factor
        val)
    (do ((i 0 (1+ i))
         (j size (1- j))
         (x (- halfk) (1+ x)))
        ((> i j))
      (setf val (* (sinc x fc)(blackman i size)))
      (setf norm (+ norm val))
      (setf (aref ar i) val)
      (setf (aref ar j) val))
    ;; norm is sum of all samples, but don't count middle value twice.
    (setf norm (- (* norm 2)(aref ar halfk)))
    (dotimes (i size ar)
      (setf (aref ar i)(/ (aref ar i) norm)))))

(defun get-kernel (size fc type)
  ;; type: 0 (low pass) or 1 (highpass)
  ;; Returns filter kernel as a sound.
  (let ((kernel (calc-kernel size fc)))
    (when (= type 1)
      ;; Convert kernel to high pass
      ;; https://tomroelandts.com/articles/how-to-create-a-simple-high-pass-filter
      (dotimes (i size kernel)
        (setf (aref kernel i)(* -1 (aref kernel i))))
      (incf (aref kernel halfk)))
    (snd-from-array 0 *sound-srate* kernel)))

(defun sinc-filter (sig start end impulse)
  (extract-abs start end (convolve sig impulse)))

(defmacro validate-low-hz (hz fmin fmax)
  ;; Discard if out of valid range.
  ;; Do NOT coerce into range if too high - if multiple tracks with
  ;; different sample rates, that could cause very unexpected results.
  `(if (or (not ,hz) (< ,hz fmin) (> ,hz fmax))
       (setf ,hz nil)))

(defmacro validate-high-hz (hz fmin fmax)
  ;; Discard if too high. Coerce into range if too low.
  `(if (or (not ,hz) (>= ,hz fmax))
       (setf ,hz nil)
       (setf ,hz (max ,hz fmin))))

(defun dofilter (cf bw type)
  ;; type: 0 (low pass) or 1 (highpass)
  ;; Calculate kernel length (must be even)
  ;; http://www.dspguide.com/ch16/2.htm
  (setf klength (/ 4.0 bw))
  (setf halfk (round (/ klength 2)))
  (setf klength (* 2 halfk))
  (let ((imp (get-kernel klength cf type))
        (start (/ halfk *sound-srate*))
        (dur (get-duration 1)))
    (multichan-expand #'sinc-filter *track* start (+ start dur) imp)))

(defun bandwidth (hz)
  ;; Set bandwidth ratio of each filter as 1% of filter frequency.
  (* hz 0.01))

(defun bw-ratio (hz)
  ;; Bandwidth ratio is required as a fraction of the sampling rate
  (/ (bandwidth hz) *sound-srate*))

(defun filter ()
  (when (< *sound-srate* 100)
    (throw 'err (_ "Error.~%Track sample rate below 100 Hz is not supported.")))
  (let* ((f0 (get '*selection* 'low-hz))
         (f1 (get '*selection* 'high-hz))
         (fc (get '*selection* 'center-hz))
         ; If frequency too low, filter length is too large.
         (fmin (* 0.002 *sound-srate*))
         (fmax (* 0.498 *sound-srate*))
         (tn (truncate len))
         (transition (truncate (* 0.01 *sound-srate*))) ; 10 ms
         (t1 (min transition (/ tn 2)))        ; fade in length (samples)
         (t2 (max (- tn transition) (/ tn 2))) ; length before fade out (samples)
         (breakpoints (list t1 1.0 t2 1.0 tn))
         (env (snd-pwl 0.0 *sound-srate* breakpoints)))
    (validate-low-hz f0 fmin fmax)
    (validate-high-hz f1 fmin fmax)
    ;; Handle very narrow selections.
    ;; This may cause f0 or f1 to 'slightly' exceed fmin fmax.
    (when (and f0 f1 (< (- f1 f0) (* fc 0.02)))
      (setf f0 (* fc 0.99))
      (setf f1 (* fc 1.01)))
    (when f0
      (setf lp-width (bw-ratio f0))
      (setf f0 (/ f0 *sound-srate*)))
    (when f1
      (setf hp-width (bw-ratio f1))
      (setf f1 (/ f1 *sound-srate*)))
    ;(format t "Low: ~a    High: ~a" (if f0 (* f0 *sound-srate*) nil) (if f1 (* f1 *sound-srate*) nil))
    (if (not (or f0 f1))
        ""  ;may occur if multiple tracks with different sample rates
        (sim
          (mult env
              (if f0 (dofilter f0 lp-width 0) 0))
          (mult env
              (if f1 (dofilter f1 hp-width 1) 0))
          (mult (diff 1.0 env) *track*)))))


(catch 'err (filter))
