SPL Measurements, very simple

I do some product testing where it’s very convenient to record noise and then choose certain sections for analysis. I was using the Measure RMS plug-in but decided to pare it down and add what I needed to get a direct calibrated SPL answer. I did a web page on this for an intro- https://www.conradhoffman.com//Audacity%20SPL.htm It’s still a work in progress and I need to add the plug-in. Looking for feedback on whether I did everything right in the plug-in as I’m a Nyquist newbie at best.

;nyquist plug-in
;version 4
;type analyze
;name "Measure SPL"
;action "Measuring SPL level..."
;author "C. Hoffman based on Steve Daulton's Measure RMS plug-in"

;; Translations were removed and constants added

;; Plugin adds a constant to the dB measurement to convert it to SPL
;; This constant is about 105 for the iSV-1611 left (high gain) channel and about 145 for the right (low gain) channel
;; You will have to determine the constants for different mics and change them below
;; Note that USB mics will usually have constant gain but analog mics will be dependent on Windows/Audacity gain settings
;; Keep careful setup notes for analog mics so you can repeat the measurements

;; linear-to-db is a nyquist conversion
;; *SELECTION* : A variable with a list of properties relating to the current selection.

(set 'leftoffset 104.93)      ; Constant to adjust left mic channel to give dB SPL directly- put your number here
(set 'rightoffset 144.8049)   ; Constant to adjust right mic channel to give dB SPL directly- put your number here
(set 'monooffset 0)           ; Constant to adjust mono mic channel to give dB SPL directly- put your number here
                              ; Remove 'missing constant' note below if you use this

(setf *float-format* "%.2f")  ;2 decimal places, which is one more than you really need

(let ((rms (get '*selection* 'rms)))
  (if (arrayp rms)
      (format nil "~a: \t~a ~a~%~
	              ~a: \t~a ~a"
                  (_ "Left") (+ leftoffset (linear-to-db (aref rms 0))) (_ "dB SPL")
                  (_ "Right") (+ rightoffset (linear-to-db (aref rms 1))) (_ "dB SPL"))
      (format nil "~a: \t~a ~a" (_ "Mono") (* monooffset (linear-to-db rms))(_ "dB SPL missing constant"))))

Sound Pressure Levels aren’t simple. For one thing, they come in three weight flavors, A, C, and Z.

https://www.noisemeters.com/help/faq/frequency-weighting/

Then there’s Slow Response and Fast Response.

Safety and health measurements are mostly in dBSPL (A). They don’t care about really low pitch and really high pitch sounds. A is mostly concerned with the middle tones around 3000Hz. We know that as “baby screaming on a jet.”

These are absolute air movement measurements. Note the meter has a calibration click dial, but not a volume control. 90dBSPL is 90dBSPL. If you come back next week, it’s still going to be 90dBSPL. If you arrive with a different meter, it’s still going to be 90dBSPL.

If you don’t go through all those gymnastics, then you’re not reporting Calibrated SPL.

It’s not unusual for a forum posting to want the conversion from (for example) -20dB tone to Sound Pressure Level. Isn’t one that I know of.

Koz

Yes,the numbers are plus and minus. SPL starts zero as the lower limit of human hearing (give or take) and goes up in positive numbers. +130dBSPL is threshold of pain.

dB inside Audacity starts zero at the digital upper limit (clipping and overload damage) and works down in negative numbers. -20dB is the average loudness of an audiobook. -60dB is the loudest that background noise is allowed to get. -96dB is the quiet limit for audio CDs.

Koz

Title means the plug-in is simple, not SPL measurements, though they’re not rocket surgery unless you get into long duration industrial exposure measurements, which is not the point of this. Either you didn’t read my page or I did a lousy job of explaining the purpose and how to calibrate. No, the plug-in doesn’t just “work” without establishing calibration. One can certainly apply a-weighting in Audacity and I do this quite frequently. Works fine if it’s normalized correctly. I know this because I have a reciprocity calibrator for my mics and a $5k SLM to compare with, not an old Radio Shack meter. Well, OK, I have one of those too. Truth is, Audacity is way more convenient for the things I measure. What I want to know is if my plug-in code is OK, that I haven’t messed up variables or something that isn’t obvious to somebody that’s not used to looking at LISP.

About the code:
There’s nothing drastically wrong, and it looks like it is working as you intended (congratulations :ugeek: ), so the points below are mostly about Lisp style conventions.

(set 'leftoffset 104.93)

“SET QUOTE” is more usually written as “SETQ”. By convention, this (and similar lines) would be written as:

(setq leftoffset 104.93)

“SETQ” is an abbreviation of “SET QUOTED”.

“SETF” can also be used anywhere that “SETQ” is used, so many LISP programmers use SETF rather than SETQ.

(setf leftoffset 104.93)

“SETF” is an abbreviation of “SET field”. It is a more advanced version of the SETQ command that supports additional features including setting the value of elements of lists and arrays.


(_ "dB SPL")

Here you are calling the function “UNDERLINE”. This is an Audacity extension of Nyquist that attempts to call the localised translation of the string argument. In this case the function will fail because there are no translations for this plug-in. Fortunately the UNDERLINE function fails gracefully and simply does nothing when translations don’t exist. Better to use the string literal (unless you intend to add translations).


(format nil "~a: \t~a ~a~%~
	              ~a: \t~a ~a"

Not wrong, but this would be better on one line:

(format nil "~a: \t~a ~a~%~a: \t~a ~a"

(_ "Left") (+ leftoffset (linear-to-db (aref rms 0))) (_ "dB SPL")
(_ "Right") (+ rightoffset (linear-to-db (aref rms 1))) (_ "dB SPL"))

This requires careful reading to see that it is 6 separate values. It’s just a style issue, but the second and fifth values in particular would be easier to read if the arguments were written as a list:

(format nil "~a: \t~a ~a~%~a: \t~a ~a"
        "Left"
        (+ leftoffset (linear-to-db (aref rms 0)))
        "dB SPL"
        "Right"
        (+ rightoffset (linear-to-db (aref rms 1)))
        "dB SPL")

"dB SPL missing constant"

I tried using the plug-in before reading the code, and I was initially confused to see this in the output.

(format nil "~a: \t~a ~a" (_ "Mono") (* monooffset (linear-to-db rms))(_ "dB SPL missing constant"))

Given that “mono” and “dB SPL missing constant” are literal strings (rather than a variables), substitution into the formatted string is unnecessary. It would be better written as:

(format nil "Mono: \t~a dB SPL missing constant"
        (* monooffset (linear-to-db rms)))

Is “missing constant” important / helpful? Personally I think that could be left out so that the output is in the form:

Mono: -0.00 dB SPL

In which case the format command would simply be:

(format nil "Mono: ~a dB SPL" (* monooffset (linear-to-db rms)))

(setq monooffset 0)

Probably better to set the default value to 1 rather than 0.


Putting it all together:

;nyquist plug-in
;version 4
;type analyze
;name "Measure SPL"
;action "Measuring SPL level..."
;author "C. Hoffman based on Steve Daulton's Measure RMS plug-in"


;; Plugin adds a constant to the dB measurement to convert it to SPL
;; This constant is about 105 for the iSV-1611 left (high gain) channel and
;; about 145 for the right (low gain) channel.
;;
;; You will have to determine the constants for different mics and change
;; them below.
;; Note that USB mics will usually have constant gain but analog mics will be
;; dependent on the OS/Audacity gain settings.
;; Keep careful setup notes for analog mics so you can repeat the measurements.


(setq leftoffset 104.93)      ; Constant to adjust left mic channel to give dB SPL directly- put your number here
(setq rightoffset 144.8049)   ; Constant to adjust right mic channel to give dB SPL directly- put your number here
(setq monooffset 1)           ; Constant to adjust mono mic channel to give dB SPL directly- put your number here
                              ; Remove 'missing constant' note below if you use this

(setf *float-format* "%.2f")  ;2 decimal places, which is one more than you really need

(let ((rms (get '*selection* 'rms)))
  (if (arrayp rms)
      (format nil "Left: \t~a dB SPL~%Right: \t~a dB SPL"
              (+ leftoffset (linear-to-db (aref rms 0)))
              (+ rightoffset (linear-to-db (aref rms 1))))
      (format nil "Mono: ~a dB SPL" (* monooffset (linear-to-db rms)))))

[UPDATE: See also: https://forum.audacityteam.org/viewtopic.php?p=449674#p449674]

Nice :slight_smile:

It would be nice to have a clear example of how to calibrate the plug-in. In particular, it is important to note that the measurement becomes meaningless if the recording level is changed (either in Audacity’s Mixer Toolbar, or the computer’s sound settings).

Callibration is essential for proper use of your plug-in, so I would definitely a help file to the plug-in for easy reference. The simplest way to do that is to use the “;manpage” header. See: Missing features - Audacity Support
A couple of caveats regarding “;manpage”:

  1. The help button can only be displayed if the plug-in has a GUI.
  2. By default, Audacity looks for a locally installed manual, and of course most users won’t have your web page installed locally, so they will be prompted to “download or view online”. Selecting “view online” will bring up your web page.

Personally, I’d make “leftoffset, rightoffset, and monooffset” into ;controls, so that their values can be set without having to manually edit the plug-in file. See: Missing features - Audacity Support
Audacity remembers the last used settings.
Example:

;control leftoffset "Left channel calibration" float-text "dB" 104.93 0 nil

An alternative to “;manpage” which avoids Audacity’s prompt to download / view online is to use the “;helpfile” header.
See: Missing features - Audacity Support

I missed this first time round:

(format nil "~a: \t~a ~a" (_ "Mono") (* monooffset (linear-to-db rms))(_ "dB SPL missing constant"))

Shouldn’t that be “+” rather than “*”?
In which case “0” is a good default value for “monooffset”.

Steve, most excellent comments that I’ll take advantage of. I had pulled the translations out of the Measure RMS plug-in that I started with, but didn’t understand how to completely undo it, thus the remaining underscore stuff. We’ve been doing various SPL measurements for decades, but over the last 5 years we’ve used Audacity more and more. It’s just the easiest way to work with our data and having the plug-in means anybody will be able to do it without a lot of instruction.

Something I need to add to my page, but a useful general practice for anybody, is to pop a calibrator on the mic before each session and record a few seconds of standard calibrated tone. With that, the plug-in values and gain can be checked for every run, increasing confidence. The calibration tone becomes a permanent part of the record so numbers can be crosschecked in the future, should the need arise.

There’s another post here about Windows level controls not working with USB mics. For measurement purposes that’s actually a very valuable thing, as unknown gain is such a problem otherwise. I know my iSV-1611 USB mic is unaffected by Windows (10) controls and the manufacturer mentions it in a somewhat vague way as a feature.

Thanks & best regards,
Conrad

OK, so here’s where it is today. Probably too much text but I think it’s necessary. I may do a small text summary of my web page that could go with it. I provide the option to go either way with the controls because the first window can be annoying when using this a lot.

;nyquist plug-in
;version 4
;type analyze
;name "Measure SPL"
;action "Measuring SPL level..."
;manpage "https://www.conradhoffman.com//Audacity%20SPL.htm"
;author "C. Hoffman based on Steve Daulton's Measure RMS plug-in"

;; Plugin adds a constant to the dB measurement to convert it to SPL (sound pressure level).
;; With an iSV-1611 mic, the left (high gain) channel constant is about 105 and
;; the right (low gain) channel is about 145.

;; You will have to determine the constants for different mics and enter them when you run
;; the plug-in. They will be remembered until you change them again. You may decide to hard
;; code them below and comment out the control lines for more streamlined operation, i.e.
;; avoiding the first window.

;; For mics with one output you might want to set all values the same so the plug-in works
;; regardless of what channel you record or delete.

;; Note that USB mics will usually have constant gain but analog mics will be
;; dependent on the OS/Audacity gain settings.

;; Keep careful setup notes for analog mics so you can repeat the measurements.

;; Uncomment the next three lines to hard code the values
;; (setq leftoffset 104.93)
;; (setq rightoffset 144.8049)
;; (setq monooffset 104.93)

;; Comment out next three lines if hardcoding values above
;control leftoffset "Left channel calibration constant" float-text "dB" 104.93 0 nil
;control rightoffset "Right channel calibration constant" float-text "dB" 144.8049 0 nil
;control monooffset "Mono calibration constant" float-text "dB" 104.93 0 nil

(setf *float-format* "%.2f")  ;2 decimal places, which is one more than you really need

(let ((rms (get '*selection* 'rms)))
  (if (arrayp rms)
      (format nil "Left: \t~a dB SPL~%Right: \t~a dB SPL"
              (+ leftoffset (linear-to-db (aref rms 0)))
              (+ rightoffset (linear-to-db (aref rms 1))))
      (format nil "Mono: ~a dB SPL" (+ monooffset (linear-to-db rms)))))

Here’s a little “trick” that you might like:

;; Comment out next three lines if hardcoding values:

;control leftoffset "Left channel calibration constant" float-text "dB" 104.93 0 nil
;control rightoffset "Right channel calibration constant" float-text "dB" 144.8049 0 nil
;control monooffset "Mono calibration constant" float-text "dB" 104.93 0 nil


;;; Substitute hardcoded values if controls are commented out

(unless (boundp 'leftoffset)
  (setf leftoffset 104.93))

(unless (boundp 'rightoffset)
  (setf rightoffset 144.8049))

(unless (boundp 'monooffset)
  (setf monooffset 104.93))

or a less repetitive (but slightly “magical”) version:

;; Comment out or delete the next three lines if hardcoding values:

;control leftoffset "Left channel calibration constant" float-text "dB" 104.93 0 nil
;control rightoffset "Right channel calibration constant" float-text "dB" 144.8049 0 nil
;control monooffset "Mono calibration constant" float-text "dB" 104.93 0 nil


(defmacro set-default (symb val)
  ;;; Set symbol to val if symbol not bound.
  `(unless (boundp ',symb)
    (setf ,symb ,val)))

;;; Hardcoded values used when ;control values not used.
(set-default leftoffset 104.93)
(set-default rightoffset 144.8049)
(set-default monooffset 104.93)

Slightly off topic…

I also have an old Radio Shack SPL meter. A couple of months ago I was doing some “experiments” and I’ve read that electret condensers can loose their sensitivity (or loose their “permanent” charge). So I bought a calibrator (about $100 so about the same price as an inexpensive SPL meter). It puts-out 94dB at 1kHz and amazingly the Radio Shack meter read 94dB, just about “perfectly” on the analog meter!!!

Another way to calibrate is to use a calibrated USB measurement microphone. These can be inexpensive and since they have fixed gain and digital output the calibration between dB SPL and dBFS is fixed and known. But of course the A-weighting still has to be applied.

If required, which depends on need. There’s also “z-weighting” (zero weighting).

Cool “trick” above, I’ll give that a try.

Not surprised that the SLM is still in cal. I have an even older version of the Radio Shack unit and it’s been stable for decades. I think it uses a dynamic mic, but AFAIK electrets maintain their charge very well over time.

A-weighting kills the very strong HVAC stuff at work of 13 and 29 Hz, which can swamp out everything else. A-weighting is a highly imperfect thing, but we’re stuck with it because it’s been written into so many laws and specifications. See https://www.acoustics.asn.au/conference_proceedings/AAS2013/papers/p39.pdf for some interesting reading.

Trivia- there’s an almost completely unknown weighting called U-weighting that kills off ultrasonics. It can be combined with A to give you AU-weighting. I’ve modded the A plug-in for AU but I might be the only person on the planet that needs it.

Best,
Conrad

I think this is about ready for prime time. I’ve added a block that switches between ‘dB’ and ‘dB SPL’ because when the calibration numbers are zero, the result isn’t SPL and returns the plain RMS value just like the original Measure RMS plug-in. No doubt my method is a bit clunky, but it seems to work OK.

Please give it a once-over. I’m rewriting my web page to eliminate some of the verbal diarrhea I’m known for and better describe the plug-in and its use.

Thanks,
Conrad

;nyquist plug-in
;version 4
;type analyze
;name "Measure SPL"
;action "Measuring SPL level..."
;manpage https://www.conradhoffman.com//Audacity%20SPL.htm
;author "Conrad Hoffman and Steve Daulton based on Measure RMS"
;copyright "Released under terms of the GNU General Public License version 2"
;release 1
;debugbutton false

;; Plugin adds a constant to the dB measurement to convert it to SPL (sound pressure level).

;; You will have to determine constants for your mics and enter them when you run
;; the plug-in. They will be remembered until you change them again. See manpage for
;; complete details.

;; Comment out the next three lines with an extra semicolon if hardcoding values:

;control leftoffset "Left channel calibration constant" float-text "dB" 104.93 0 nil
;control rightoffset "Right channel calibration constant" float-text "dB" 144.8049 0 nil
;control monooffset "Mono calibration constant" float-text "dB" 104.93 0 nil

(defmacro set-default (symb val)
  ;;; Set symbol to val if symbol not bound.
  `(unless (boundp ',symb)
    (setf ,symb ,val)))

;;; Hardcoded values used when ;control values not used.
(set-default leftoffset 104.93)
(set-default rightoffset 144.8049)
(set-default monooffset 104.93)

(setf *float-format* "%.2f")  ;2 decimal places, which is one more than you really need

;;; Change suffix because if calibration is zero, result is original RMS value, not SPL
(if (= leftoffset 0) (setq leftsuffix "dB") (setq leftsuffix "dB SPL"))
(if (= rightoffset 0) (setq rightsuffix "dB") (setq rightsuffix "dB SPL"))
(if (= monooffset 0) (setq monosuffix "dB") (setq monosuffix "dB SPL"))


(let ((rms (get '*selection* 'rms)))
  (if (arrayp rms)
      (format nil "Left: \t~a ~a~%Right: \t~a ~a"
              (+ leftoffset (linear-to-db (aref rms 0))) leftsuffix
              (+ rightoffset (linear-to-db (aref rms 1))) rightsuffix)
      (format nil "Mono: ~a ~a" (+ monooffset (linear-to-db rms)) monosuffix)))
(if (= leftoffset 0) (setq leftsuffix "dB") (setq leftsuffix "dB SPL"))

Notice that the entire line has to be read to see that this line is:
IF … THEN … ELSE …
and not just:
IF … THEN …

The above example is probably a borderline case, but as a general rule, unless the line is very short, it’s easier to read an IF/THEN/ELSE clause when formatted across three lines:

(if (= leftoffset 0)
    (setq leftsuffix "dB")
    (setq leftsuffix "dB SPL"))

Notice that it is now obvious at a glance what the “THEN” and “ELSE” statements are.

However, notice too that this is quite repetitive:

(if (= leftoffset 0) (setq leftsuffix "dB") (setq leftsuffix "dB SPL"))
(if (= rightoffset 0) (setq rightsuffix "dB") (setq rightsuffix "dB SPL"))
(if (= monooffset 0) (setq monosuffix "dB") (setq monosuffix "dB SPL"))

You are saying, three times, that if the value is zero, then use a different suffix.

A recommended pattern in programming (any language) is “Don’t repeat yourself”.

Looking at where these values are used:

(let ((rms (get '*selection* 'rms)))
  (if (arrayp rms)
      (format nil "Left: \t~a ~a~%Right: \t~a ~a"
              (+ leftoffset (linear-to-db (aref rms 0)))
              leftsuffix   ; *** HERE ***
              (+ rightoffset (linear-to-db (aref rms 1)))
              rightsuffix)   ; *** HERE ***
      (format nil "Mono: ~a ~a"
              (+ monooffset (linear-to-db rms))
              monosuffix)))   ; *** HERE ***

We could do something like:

(let ((rms (get '*selection* 'rms)))
  (if (arrayp rms)
      (format nil "Left: \t~a ~a~%Right: \t~a ~a"
              (+ leftoffset (linear-to-db (aref rms 0)))
              (get-suffix leftoffset)
              (+ rightoffset (linear-to-db (aref rms 1)))
              (get-suffix rightoffset))
      (format nil "Mono: ~a ~a"
              (+ monooffset (linear-to-db rms))
              (get-suffix monooffset))))

Then to get the suffix, a little function like this:

(defun get-suffix (offset)
  (if (= offset 0) "dB" "dB SPL"))

Just down to personal preference, but personally I’d use shorter names for the offset values. The important thing is that it should be easy for future developers (or yourself in months or years hence) to understand the intent. Personally I think this would be sufficient:

;;; Hardcoded offset values used when ;control values not used.
(set-default left 104.93)
(set-default right 144.8049)
(set-default mono 104.93)

which makes the whole code look like this:

;; Comment out the next three lines with an extra semicolon if hardcoding values:

;control left "Left channel calibration constant" float-text "dB" 104.93 0 nil
;control right "Right channel calibration constant" float-text "dB" 144.8049 0 nil
;control mono "Mono calibration constant" float-text "dB" 104.93 0 nil

(defmacro set-default (symb val)
  ;;; Set symbol to val if symbol not bound.
  `(unless (boundp ',symb)
    (setf ,symb ,val)))

;;; Hardcoded offset values used when ;control values not used.
(set-default left 104.93)
(set-default right 144.8049)
(set-default mono 104.93)


(defun get-suffix (offset)
  (if (= offset 0) "dB" "dB SPL"))


(setf *float-format* "%.2f")

(let ((rms (get '*selection* 'rms)))
  (if (arrayp rms)
      (format nil "Left: \t~a ~a~%Right: \t~a ~a"
              (+ left (linear-to-db (aref rms 0)))
              (get-suffix left)
              (+ right (linear-to-db (aref rms 1)))
              (get-suffix right))
      (format nil "Mono: ~a ~a"
              (+ mono (linear-to-db rms))
              (get-suffix mono))))
;manpage https://www.conradhoffman.com//Audacity%20SPL.htm

Is this supposed to have “//” before the name of the page?

(Personally I also like to avoid spaces in URLs, and I prefer the full “html” file extension. So I would rename the page: “Audacity_SPL.html”, though I think this is purely a matter of personal preference :wink:)

Thanks Steve, all good stuff. Some of my repetition is to make certain changes easier in the future. I don’t know what I don’t know, but if I combine suffixes it’s almost certain someday I’ll have to separate them again. Structure-wise, your way is much better. Not a clue about the double backslash, as I just cut and pasted the URL from Chrome.

It’s my primitive pea-brain, but I need long and descriptive variable names to have any hope of remembering what I did in the past.

I’ve got the new description page nearly done, but won’t have it up for a couple days, nor a direct link from any of my pages, until I think most of the errors are corrected and the descriptions are clear. Hard to believe adding two numbers together could require this much messing about!

Best regards,
Conrad

:smiley:
but don’t talk yourself down. You’ve done extremely well writing your first ever Nyquist plug-in. Although I’ve written a lot in this forum thread, it has almost all been about style / conventions. Everything else you’ve worked out for yourself.


but much less than if you’d been writing in C++ rather than Nyquist.

I recall a brilliant C++ developer being amazed by a Nyquist version of Audacity’s “Tone” generator. These two files are the C++ code:

whereas the Nyquist version was just the plug-in header plus half a dozen lines of code.

This is an endless discussion better had over a beer or three, but it’s an interesting topic to me. I work mostly in test and measurement, electronic, mechanical and anything else. I’m not a programmer but have written thousands of lines of code to support various measurements and equipment over the decades. Sensors, encoders and GPIB test equipment, mostly in Power Basic, Visual Basic, VBA or similar. RIP Bob Zale.

I have a good sense of aesthetics and love tight elegant code, yet it’s often proved to be a hinderance in the long term. Programs are often repurposed for some unanticipated task, sometimes years in the future. The biggest thing I ever wrote was back in 2006 (3700 lines) and it’s still in use every day. Of course it’s been through dozens and dozens of revisions. Only because of excessively descriptive function and variable names plus open and simple-minded structures, not to mention commenting even obvious stuff, am I (and others) able to pick it up and modify it without a lot of study. Well yes, we still avoid a couple areas with minefields of pointers and assembly language, where maximum speed was a necessity.

Anyway, my quandary; should the reaction be, “Wow, all that stuff was accomplished in three lines of code!” or “So what? That’s just obvious to anybody with a pulse.” Should I be an artist or an auto mechanic? Fortunately my hobbies and my work are indistinguishable from each other, so I get to do both!

I found diving into Nyquist/LISP, even with some background, was harder than I thought it would be. As I said earlier, I started and gave up several times. The answer is to take something simple (like this “simple” mod to your existing plug-in) and just hammer at it until it starts to work, if only a little. Once over that hump it gets easier.

Enough of a ramble! Thanks again for taking so much time with this. I’ll post the final link when I finish the description page.

Best,
Conrad