;nyquist plug-in
;version 3
;type analyze
;name “ACX Check”
;action “Compute statistics about clip and compare to ACX requirements”
;author “Will McCown”
; will@ross-mccown.com
; 2015-07
; release 1.0
; based on code from the plugins:
; measurement.ny from
; endolith@gmail.com
; 2010-08
; and in stats.ny from
; Steve Daulton. http://audacity.easyspacepro.com
; This tool provides a number of useful statisics about the selected
; audio. Where appropriate values are reported both as linear numbers
; and in dBFS (dB relative to full scale). For each of the left & right
; tracks of the selection it will report:
; Peak level - the greatest absolute value in the selection
; RMS level - the root mean square of the total selection
; NoiseFloor - the RMS level of the quietest 500 mS in the selection
; RMS (A) - The RMS level of the clip with an A-weighting filter applied
; NoiseFloor (A) - The noisefloor of the clip with an A-weighting filter applied
; DC offset - The DC (overall average) of the selection, reported as a percentage
; of full scale.
; The length of the selection is reported both in samples and in seconds.
; The sample-rate is also reported.
; The Peak level, RMS level, and Noisefloor results are compared against
; the published ACX requirements:
; https://www.acx.com/help/acx-audio-submission-requirements/201456300
; This tool is intended only as an aid in acheiving ACX acceptance.
; A passing grade from this tool is NO guarantee of ACX acceptance. This
; tool is very limited compared to the testing done by the ACX organization
; on submissions. In particular the noise-floor measurement reported by
; this tool is the quietest 0.5 seconds found in the length of the clip.
; The intentional insertion of silence, or the overuse of noise-reduction
; and compression tools will give you a passing number and yet will likely
; fail ACX acceptance.
; A test clip without a sufficiently long “room tone” section may yield
; a false indication of a high noise floor.
; Also beware that some noise sources are worse than others, and
; noise such as the 1kHz whine that often happens in USB audio interfaces
; may result in an ACX rejection even though it is below the -60 dBFS
; noise floor requirement.
(setq max-len 100000000) ; Empirically determined on my machine, may be hardware dependent.
(setq ACX-max-peak -3.0) ; ACX maximum peak (db)
(setq ACX-max-rms -18.0) ; ACX maximum rms (db)
(setq ACX-min-rms -23.0) ; ACX minimum rms (db)
(setq ACX-max-nf -60.0) ; ACX maximum noise floor (db)
(setq FMT-db “%#.1f”)
(setq FMT-val “%#.6f”)
(defun fmt-pretty (FMT &rest stuff)
(progv ‘(*float-format*) (list FMT)
(apply #’format stuff)))
(fmt-pretty FMT-val nil “~a” 1.2345678)
(defun fmt-peak (value) ;string for peak value including ACX warning
(strcat
(fmt-db value)
(if (> (linear-to-db value) ACX-max-peak)
(format nil “ << Exceeds ACX ~a dB max” ACX-max-peak)
“ .. Passes ACX”)))
(defun fmt-nf (value) ;string for peak value including ACX warning
(strcat
(fmt-db value)
(if (< value 0)
(format nil “ << selection too short”)
(if (> (linear-to-db value) ACX-max-nf)
(format nil “ << Exceeds ACX ~a dB max” ACX-max-nf)
“ .. Passes ACX”))))
(defun fmt-rms (value) ;string for peak value including ACX warning
(strcat
(fmt-db value)
(if (> (linear-to-db value) ACX-max-rms)
(format nil “ << Exceeds ACX ~a dB max” ACX-max-rms)
(if (< (linear-to-db value) ACX-min-rms)
(format nil “ << Less than ACX ~a dB min” ACX-min-rms)
“ .. Passes ACX”))))
(defun fmt-db (value) ;print value and it’s dB equivalent with appropriate formats
(if (< value 0)
(format nil “N/A”)
(strcat
(fmt-pretty FMT-val nil “~a” value)
(if (> value 0)
(fmt-pretty FMT-db nil “ (~a dB)” (linear-to-db value))
(format nil “ (-inf dB)”)))))
;the my-rms and my-avg routines compute the average value by
;two successive calls to snd-avg. This is to get around the
;maximum block-size for the snd-avg function which is quite short
;compared to typical samples to be measured.
(defun my-rms (a len) ; compute the RMS of a sound
(setq chunk 2000000) ;emperically determined maximum blocksize for snd-avg
(setq bsize (truncate (/ len chunk))) ;blocksize to use for the first avg pass
(setq result (mult a a))
(when (> bsize 1)
(setq result (snd-avg result bsize bsize OP-AVERAGE)))
(setq bsize (snd-length result chunk)) ;probably should thow an error if bsize gets chunk here
(setq result (snd-avg result bsize bsize OP-AVERAGE))
(sqrt (snd-fetch result)))
(defun my-avg (a len) ; compute the avg of a sound
(setq chunk 2000000) ;emperically determined maximum blocksize for snd-avg
(setq bsize (truncate (/ len chunk))) ;blocksize to use for the first avg pass
(setq result (snd-copy a))
(when (> bsize 1)
(setq result (snd-avg result bsize bsize OP-AVERAGE)))
(setq bsize (snd-length result chunk)) ;probably should thow an error if bsize gets chunk here
(setq result (snd-avg result bsize bsize OP-AVERAGE))
(snd-fetch result))
(defun my-noisefloor (s-in len) ;find min RMS
(setq noise-win 0.5) ; Noise level window (seconds)
(setq noise-inc 5) ; Noise level increment (fraction of a window will be shifted must be an int)
(setq max-fact 0.0001) ; min-max ratio at which we trust the min finder
(setq min-fact 1e-20) ; give up point…
;set block size to 500 mS
(setq bsize (truncate (* noise-win (snd-srate s-in))))
;set step size to window/5
(setq ssize (truncate (/ bsize noise-inc)))
(if (<= len (* bsize 2)) -1
(progn
(setq s-samp (highpass8 s-in 10))
(setq s-rms (mult s-samp s-samp))
(setq s-rms (snd-avg s-rms bsize ssize OP-AVERAGE))
(setq s-len (snd-length s-rms max-len))
; trick to get max to find the min, find the peak and subract it
;now the min is the max positive
(setq max (peak s-rms s-len))
(setq factor 1)
(sqrt
(loop
(setq sa (sum (- 0 max) s-rms))
(setq min (- max (peak sa (- s-len (+ noise-inc 2)))))
(if (or (> min (* max max-fact)) (< max min-fact))
(return (max 0 min)))
(setq max (* max max-fact))
(setq s-rms (clip s-rms max)))))))
(defun analyze (s-in len) ; code that does the analysis and returns data as a string
; Measure DC
(setq dc-offset (my-avg s-in len))
; Remove DC
(setq s-in (diff s-in dc-offset))
;; Should peak measurement be before or after removing DC?
;; Peak samples should be measured before
; Calculate the maximum and RMS levels
; RMS of full scale square wave is 0 dBFS
(setq lmax (snd-maxsamp s-in))
(setq dbmax (linear-to-db lmax))
(setq lrms (my-rms s-in len))
(setq dbrms (linear-to-db lrms))
;measure noise floor
(setq nfloor (my-noisefloor s-in len))
(setq dbnfloor (linear-to-db nfloor))
(setq acxfail (or (> dbmax ACX-max-peak)
(> dbrms ACX-max-rms)
(< dbrms ACX-min-rms)
(> dbnfloor ACX-max-nf)))
; A-weighted version of sound - by Edgar (thanks!)
(setq sa (lp (lp (hp (hp (hp (hp s-in 20.6) 20.6) 107.7) 737.9) 12200) 12200))
; Calculate the RMS level of the A-weighted signal
; constant is a fudge factor to normalize to 0 dB at 1 kHz
(setq smrate (snd-srate s-in))
(setq fudge
(if (<= smrate 44100)
1.344107
(if (<= smrate 48000)
(+ 1.344107 (* (- 1.336392 1.344107 )(/ (- smrate 44100) 3900)))
(if (<= smrate 96000)
(+ 1.336392 (* (- 1.296891 1.336392) (/ (- smrate 48000) 48000)))
(+ 1.296981 (* (- 1.277714 1.296981) (/ (- smrate 96000) 96000)))))))
(setq larms (* fudge (my-rms sa len)))
;and get the noise floor of the A-weighted version
(setq anfloor (* fudge (my-noisefloor sa len)))
(format nil “~a~%~a”
(format nil
“Peak level: ~a~%RMS level: ~a~%NoiseFloor: ~a~%~
RMS (A): ~a~%NoiseFloor (A): ~a~%DC offset: ~a%~%”
(fmt-peak lmax)
(fmt-rms lrms)
(fmt-nf nfloor)
(fmt-db larms)
(fmt-db anfloor)
(fmt-pretty FMT-val nil “~a” (* dc-offset 100)))
(format nil
(if acxfail
(strcat
“Clip fails to meet ACX requirements~%”
(if (> dbmax ACX-max-peak)
“Peak exceeds ACX specification of -3 dBFS~%” “”)
(if (or (> dbrms ACX-max-rms) (< dbrms ACX-min-rms))
“RMS level is outside the ACX specification of -18 to -23 dBFS~%” “”)
(if (> dbnfloor ACX-max-nf)
“Noise floor exceeds ACX specification of -60 dBFS~%” “”))
“Clip meets ACX requirements~%” ))))
(defun analyze-mono (input len) ; for mono tracks
(format nil “Mono track properties~%~%~a~%”
(analyze input len)))
(defun analyze-stereo (input len) ; for stereo tracks
(format nil “Stereo track properties~%~%Left channel:~%~a~%~%Right channel:~%~a~%”
(analyze (aref input 0) len)
(analyze (aref input 1) len)))
(format nil “~a~%Length of selection: ~a seconds.~%~a samples at ~a Hz.~%”
(if (> len max-len)
(format nil “Selection too long for analysis, please select shorter section~%”)
(if (arrayp s)
(analyze-stereo s len)
(analyze-mono s len)))
(get-duration 1)
(truncate LEN)
(truncate *sound-srate*))