audacia/nyquist/spectral-analysis.lsp

290 lines
9.8 KiB
Common Lisp

;; 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 parameter: ~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))))