Code to Select Audio

Have you tried testing on tracks where you easily check what the correct values should be?

The “Contrast” tool will give you the RMS level of a selection, so if a track contains a section that is a continuous tone, and that part has the highest RMS, and that section is at least double the window size in the new code, then you can know the expected “highest RMS”.

The easiest way to find the highest peak, is to open the Amplify effect. This effect defaults to the necessary amount of gain to bring the peak to 0 dB.

“sig” is a local variable (it is local to the function “peaks2”. It is also called a “positional argument” of the function. XLISP defun

&key indicates “keyword arguments”. See: XLISP &key


Yes. I would probably have been simpler to only handle mono and stereo, but at some point in the future, Audacity may support tracks with more than 2 channels, so I think it’s a worthwhile exercise to consider how to handle an arbitrary number of channels.

Let’s strip the code down to essentials, and assume that “sig” is always an array of sounds (I’ve ripped out the “IF” so that we can look specifically at what happens in the case of an array of sounds):

(defun peak3 (sig &key (maxlen ny:all) (high 0))
  (dotimes (i (length sig) high)
    (setf high (max high (peak (aref sig i) maxlen)))))

SIG is a positional argument. It is the first argument after the function name, so when we call the function, the first value that we pass will be the value of SIG. Example: if we call the function with:

(peak3 *track*)

Then the value of SIG is TRACK.

As explained in the links above, MAXLEN and HIGH are optional “keyword” arguments. If we call the function without specifying values for these keyword arguments, then they take default values:
maxlen = ny:all (a very big number)
high = 0

Then we have a DO loop, that sets the initial value of i to 0, and increments it on each loop, up to the value (length sig), which is the length of the array (the number of elements in the array, or in other words, then number of channels.

On each loop, we set HIGH to the maximum of HIGH and the peak level of the channel being processed in this loop.

Note that for the first track processed by the effect, HIGH starts off at its default value of zero.
For each subsequent track, we pass the current highest value of previous tracks as the value of HIGH, so within the DO loop, we are looking for the maximum of “the current channel” and "any previous channel in any previous track.

NY:ALL is a variable that has a preset value within Nyquist. I believe it was added to Nyquist as a convenience when applying functions that need a “maximum number of samples” to be specified, so it’s a big number, but hopefully no so big as to cause memory to run out.
You can see the actual value of NY:ALL by printing it:

(print ny:all)

If I recall correctly, it has a default value of a billion.

Nyquist’s RMS function is actually written in Nyquist (LISP) code. The code can be found in Audacity’s source code:

;; RMS -- compute the RMS of a sound
;;
(defun rms (s &optional (rate 100.0) window-size)
  (let (rslt step-size)
    (cond ((not (eq (type-of s) 'SOUND))
	   (break "in RMS, first parameter must be a monophonic SOUND")))
    (setf step-size (round (/ (snd-srate s) rate)))
    (cond ((null window-size)
               (setf window-size step-size)))
    (setf s (prod s s))
    (setf result (snd-avg s window-size step-size OP-AVERAGE))
        ;; compute square root of average
        (s-exp (scale 0.5 (s-log result)))))

You may notice that my mean-sq function is very much like Nyquist’s RMS function - the major difference being that I don’t calculate the square root at the end of the function.

COND is a very useful “conditional” form.
It is essentially the same as (in pseudo code):

IF (condition A)
   THEN DO
      (some code)
ELSE IF (condition B)
   THEN DO
      (some other code)
...
ELSE IF (condition X)
   THEN DO
      (some other code)
... and optionally ...
ELSE
   THEN DO
      (default code if no matches)

It is also handy even when there are only one true / false condition, because unlike “IF” each option can take multiple expressions, whereas “IF” can only do one expression when the condition is true, else one other expression when the condition is false.

See: XLISP cond

Most of your explanation has become clearer to over the past few hours (after and while writing these posts).

I’ll keep bugging you for more answers in the future! :laughing:

By the way, could that multi-channel version of RMS work? For now I’ve kept the original RMS function instead of your mean-sq function but I might change that in the future, once I really got the hang of it.

(defun mrms (sig &optional rate window-size (high2 0))
  ;; A multi-channel version of RMS - MRMS.
  (cond
    ((soundp sig)
      (if window-size
          (rms sig rate window-size)
          (rms sig rate)))
    ((arrayp sig)
      (dotimes (i (length sig) high2)
        (setf rms-ch (if window-size
          (rms (aref sig i) rate window-size)
          (rms (aref sig i) rate)))
        (setf high2 (sum high2 (mult rms-ch rms-ch))))
        (setf high2 (mult (/ 1 i) high2))
      (s-sqrt high2))
    (t (error "Type error in mrms" sig))))

I admittedly haven’t tested it yet, alas, it’s late and I really need to get some sleep. Four hours sleep may be to little! :mrgreen:

I myself see at least two problems: the i from dotimes will not be accessible to the i in (setf high2 (mult (/ 1 i) high2)) - I probably need a more global variable. And maybe I need some sort of binging for the setf rms-ch part.

This is a good example of how good indentation can be a big help.
In the example below, it is obvious that line 15 is outside of the scope of the “dotimes” block:

code.png

I use Notepad++ which is not useful as far as indentation (for lisp) is concerned. So it jumps up and down, no matter how much I try. What do you use?

But I finally found the code:

(defun rmsm (sig &optional rate window-size (high2 0))
  ;; A multi-channel version of RMS.
  (cond
    ((soundp sig)
      (if window-size
          (rms sig rate window-size)
          (rms sig rate)))
    ((arrayp sig)
      (dotimes (i (length sig) high2)
        (setf rms-ch (if window-size
          (mean-sq (aref sig i) rate window-size)
          (mean-sq (aref sig i) rate)))
        (setf high2 (sum high2 rms-ch)))
      (setf mean (mult (/ 1.0 (length sig)) high2))
      (s-sqrt mean))
    (t (error "Type error in rmsm" sig))))

The only difference are the lines with the actual mean-sq/rms code (see, I finally use your function - I just hoped that I’d need to define only one function to replace the official RMS function).

You basically write:

(ms-i (mean-sq (aref sig i)))

While mine is:

(rms-ch (mean-sq (aref sig i) rate))

And its result is much closer to what your RMS plugin (rms.ny, v2.3.1) displays.

Here’s my (our) almost final code:

;nyquist plug-in
;version 4
;type analyze
;name "Measure Highest RMS"
;action "Measuring RMS level of the loudest section"
;release 1.0.2
;copyright "Released under terms of the GNU General Public License version 2"

;; Released under terms of the GNU General Public License version 2:
;; http://www.gnu.org/licenses/old-licenses/gpl-2.0.html


(defun number-to-string (number digits)
  ;; Returns the string representation of
  ;; a number with specified decimal places.
  (setf *float-format* (format nil "%.~af" digits))
  (format nil "~a" number))

(defun peakm (sig &key (maxlen ny:all) (high 0))
  ;; A multi-channel version of PEAK.
  ;; Returns the maximum of "high" (keyword) and peak per channel.
  (cond
    ((soundp sig)
	  (if maxlen
	    (peak sig maxlen)
		(peak sig)))
	((arrayp sig)
      (dotimes (i (length sig) high)
        (setf high (max high (peak (aref sig i) maxlen)))))
    (t (error "Type error in peakm" sig))))


(defun mean-sq (sig &optional (rate 100.0) window-size)
  (let (rslt step-size)
    (when (not (soundp sig))
      (error "Type  error in mean-sq" sig))
    (setf step-size (round (/ (snd-srate sig) rate)))
    (when (null window-size)
      (setf window-size step-size))
    (snd-avg (mult sig sig) window-size step-size OP-AVERAGE)))

(defun rmsm (sig &optional rate window-size (high2 0))
  ;; A multi-channel version of RMS.
  (cond
    ((soundp sig)
      (if window-size
          (rms sig rate window-size)
          (rms sig rate)))
    ((arrayp sig)
      (dotimes (i (length sig) high2)
        (setf rms-ch (if window-size
          (mean-sq (aref sig i) rate window-size)
          (mean-sq (aref sig i) rate)))
        (setf high2 (sum high2 rms-ch)))
      (setf mean (mult (/ 1.0 (length sig)) high2))
      (s-sqrt mean))
    (t (error "Type error in rmsm" sig))))

(defun peak-rms (sig rate val)
  ;Return the max of 'val' and the peak rms of 'sig'
  (let ((peak-rms (peak (rmsm sig rate) ny:all)))
    (max val peak-rms)))

(defun process-first-track (sig rate)
  (putprop '*scratch* (peakm sig) 'peak)
  (putprop '*scratch* (peak-rms sig rate 0) 'rms))

(defun process-others (sig rate)
  (let ((old-peak (get '*scratch* 'peak))
        (old-rms (get '*scratch* 'rms)))
    (putprop '*scratch* (peakm sig :high old-peak) 'peak)
    (putprop '*scratch* (peak-rms sig rate old-rms) 'rms)))

(defun output ()
  (let ((peak (get '*scratch* 'peak))
        (peak-db (linear-to-db (get '*scratch* 'peak)))
        (rms (get '*scratch* 'rms))
        (rms-db (linear-to-db (get '*scratch* 'rms))))
    (remprop '*scratch* 'peak)
    (remprop '*scratch* 'rms)
    (format nil
            "Hightest RMS is ~a (~a dB).~%~
            Highest peak is ~a (~a dB).~%~
            Crest factor based on highest values: ~a (~a dB)."
            (number-to-string rms 3)
            (number-to-string rms-db 1)
            (number-to-string peak 3)
            (number-to-string peak-db 1)
            (number-to-string (/ peak rms) 2)
            (number-to-string (linear-to-db (/ peak rms)) 1))))


;control rms-window "RMS window size" float "s" 10 0.01 30

(setf rms-rate (/ 1.0 rms-window))

(let ((selected (get '*selection* 'tracks))
      (id (get '*track* 'index)))
  (if (= id 1)
      (process-first-track *track* rms-rate)
      (process-others *track* rms-rate))
  (if (= id (length selected))
      (output)
      ""))

Since I want to provide for quite long time windows (up to 30 seconds), I still need to write a n error message for the cases when someone makes a sound selection shorter then the chosen time window. Ideally the control will allow only for a maximum that is at most equal to the audio selection length but ;control does seem to take a symbol as value. But that’s for tomorrow.

Then I’ll do some testing… :stuck_out_tongue:

I use Scite on Linux. Both Scite and NotePad++ are based on the Scintilla library.

I’ve not used NotePad++ for quite a while, but like Scite, it has options to set indentation as 2 spaces, and LISP syntax highlighting.

You didn’t protest against this change so I’ll keep it :wink:

Here’s now a functioning version, as far as I am concerned for anyone to use who should find some use for it.

Here’s the comment section of the plugin:

This plugin is intended to allow for real loudness comparison between single tracks
or selections by concentrating on the loudest section of each track while leaving
quieter sections out of the loudness calculation. This is called the “Highest RMS”
in this plugin.

For better use the highest peak of the selection is provided.

Additonally the (stricly mathematical) crest factor between the highest peak and
the highest RMS. It is provided purely as additional information hinting at the
level of dynamic range. It is not the same as TT DR’s dynamic range calculation.

I hope you’ll enjoy this one! I had fun learning the basics of Nyquist plugin programming with Steve.
max-rms.ny (4.63 KB)

Here’s the error code:

(let ((selected (get '*selection* 'tracks))
      (id (get '*track* 'index))
      (selstart (get '*selection* 'start))
      (selend (get '*selection* 'end)))
  (cond
    ((> rms-window (- selend selstart))
      (format nil "Your selection is shorter than the chosen windows size.~%~
    Either chose a longer selection or a shorter windows size."))
    ((if (= id 1)
      (process-first-track *track* rms-rate)
      (process-others *track* rms-rate))
  (if (= id (length selected))
      (output)
      ""))))



I guess the widget code does not allow for a variable to be used or I couldn’t find the way, (I did not exhaust the search option as I didn’t feel the need for this. It’s okay like that.)

For process. type effects, the length of the selection is conveniently available in the variable LEN (in samples). The length in seconds can be calculated

(/ len *sound-srate*)