Measure Loudness in LUFS

UPDATE: To see the latest version see this thread: https://forum.audacityteam.org/viewtopic.php?f=69&t=126791

Hello, let me post here a plugin Loudness Measurement that measure the loudness in LUFS. This was build in Audacity 2.4.2.
LoudnessMeasurement.ny (1.74 KB)
Loudness Measurement.txt (172 Bytes)
It is necessary to have a macro that runs this plugin.

Enable plugin. Assign a shortcut to the macro. Then select audio and then hit the shortcut. Macro triggers the plugin and runs only first part of it, then it triggers the same plugin again, but now it runs the second part of the plugin. At the end the macro set the main parameter back to zero, so it is in a “safe” mode = for the case that a user would run the plugin itself.

Please let me know if you have any suggestions how to improve this. Of course, I would like to use just a plugin without macro, but I have no idea if that would be possible.

Thank you!

Macros can’t make decisions (no IF-THEN) and as far as I know, they can’t call each other.

Koz

That’s true. It is enough to make a plug-in with input parameter that defines which procedure inside plug-in will run. Then you call by macro the plug-in as many time as you want, but you specify every time the parameter.

This bit:

	(setf pkA1ini (get '*selection* 'peak))
	(if (arrayp pkA1ini)
		(if (> (aref pkA1ini 0) (aref pkA1ini 1))
			(setf pkA1 (aref pkA1ini 0))
			(setf pkA1 (aref pkA1ini 1)))
		(setf pkA1 pkA1ini))

can be simplified as:

  (setf pkA (get '*selection* 'peak))
  (when (arrayp pkA)
    (setf pkA (max (aref pkA 0) (aref pkA 1))))

I don’t understand the purpose of this:

	(cond ((< (linear-to-db pkA1) -30)
		(AUD-DO "Amplify:Ratio=31.622776")))

Immediately after that you apply Loudness Normalization, so doesn’t that make the Amplify process irrelevant?

I think a comment in the “loudness-B” function would be helpful.
If I’m understanding correctly, the meaning of this line:

	(setf lufs (- (- pkB1 pkB2) 35))

is:

  ;; -35 LUFS normalization reduced the peak level from
  ;; 'original-peak' to 'new-peak', so the original LUFS was
  ;; -35 + (original-peak - new-peak).

(I’ve used the name ‘original-peak’ in place of ‘pkB1’ and ‘new-peak’ in place of ‘pkB2’)

How about using just a macro?
You can embed Nyquist code in a macro by using the Nyquist Prompt in a macro.

Example
The first script to run:
(I’ve simplified a little by normalizing to 0 LUFS.)

;type analyze
(setf key 'loudness-measurement-key)

;; Write peak level to *scratch* and normalize to -35 LUFS.
(let ((peak (get '*selection* 'peak)))
  (when (arrayp peak)
    (setf peak (max (aref peak 0) (aref peak 1))))
  (setf peak (linear-to-db peak))
  ; Normalize to 0 LUFS
  (AUD-DO "LoudnessNormalization:DualMono=0 LUFSLevel=0 NormalizeTo=0 RMSLevel=-23 StereoIndependent=0")
  (putprop '*scratch* peak key))
""

The second script to run:

;type analyze
(setf key 'loudness-measurement-key)

(let ((original-peak (get '*scratch* key))
      (new-peak (get '*selection* 'peak)))
  (when (arrayp new-peak)
    (setf new-peak (max (aref new-peak 0) (aref new-peak 1))))
  (setf new-peak (linear-to-db new-peak))
  ;; 0 LUFS normalization changed the peak level from
  ;; 'original-peak' to 'new-peak', so the original LUFS was
  ;; original-peak - new-peak.
  (setf lufs (- original-peak new-peak 35))
  ;; Clean up
  (remprop '*scratch* key)
  (format nil "Loudness = ~a LUFS~%~
          Peak = ~a dB" lufs original-peak))

The macro, with comments and empty lines stripped out:

NyquistPrompt:Command=";type analyze\n(setf key 'loudness-measurement-key)\n(let ((peak (get '*selection* 'peak)))\n  (when (arrayp peak)\n    (setf peak (max (aref peak 0) (aref peak 1))))\n  (setf peak (linear-to-db peak))\n  (AUD-DO \"LoudnessNormalization:DualMono=0 LUFSLevel=0 NormalizeTo=0 RMSLevel=-23 StereoIndependent=0\")\n  (putprop '*scratch* peak key))\n\"\"" Parameters=""
NyquistPrompt:Command=";type analyze\n(setf key 'loudness-measurement-key)\n(let ((original-peak (get '*scratch* key))\n      (new-peak (get '*selection* 'peak)))\n  (when (arrayp new-peak)\n    (setf new-peak (max (aref new-peak 0) (aref new-peak 1))))\n  (setf new-peak (linear-to-db new-peak))\n  (setf lufs (- original-peak new-peak))\n  (remprop '*scratch* key)\n  (format nil \"Loudness = ~a LUFS~%~\n          Peak = ~a dB\" lufs original-peak))" Parameters=""
Undo:

but there’s a bug somewhere (and with your original version) in that the reported LUFS is incorrect. :confused:
I’ll try to find it …

Hmm… the bug might be in my version of Audacity (3.2.0 alpha).
Update: Logged here: Incorrect peak level detection · Issue #3372 · audacity/audacity · GitHub


I’ll try 2.4.2…

It works for me in Audacity 2.4.2, so it seems there is a bug in the current development version.

Having said that, it would be better for Loudness Normalization to use DualMono=1
and the handling of stereo tracks could be simplified further as:

(when (arrayp new-peak)
  (setf new-peak (aref new-peak 0)))

in both of the Nyquist scripts.

Wow! I am surprised that all this is possible to pack into one single macro! So let me update it based on your comments and post this on Macros and Scripting forum. BTW I see now that my variables names are horrible :smiley: … I will try to make them more clear.

I don’t understand the purpose of this:

(cond ((< (linear-to-db pkA1) -30)
	(AUD-DO "Amplify:Ratio=31.622776")))
>

Good question  :slight_smile:  It is a small trick. It allows to measure LUFS below -60 LUFS. If you remove that piece of code, you will not be able to measure anything that has value below -60 LUFS. This limitation is part of build-in plugin Loudness Normalization. Of course, this workaround really does not have a huge value in practical sense.

> it would be better for Loudness Normalization to use DualMono=1

I tried to imitate the logic in Izotope Insight. When I compare it with that tool, I have exactly the same measured values. I do not know what is correct, just explaining the reasoning behind...

Thank you!

Here is the post on the forum for macros: https://forum.audacityteam.org/t/measure-loudness-in-lufs/65480/1
There you can find the latest version of this tool.

Hello,

let me share the latest version of the Nyquist plugin with you:
Loudness Measurement.ny (6.8 KB)

The implemented loudness measurement is fully compliant with ITU-R BS.1770-4.
Reference: Recommendation ITU-R BS.1770-4 (10/2015)

I am reaching out for assistance with optimization. In another plugin, Dereverb, it was possible to minimize the memory consumption - the size of the used RAM is almost equivalent to the input audio. However, I am having difficulty achieving the same with the Loudness Measurement plugin.

Currently, the consumed RAM space is three times the volume of the audio signal. Is there a way we can improve this?

Thank you.

PS: How can I edit an existing post? I would like to include this version in the first post of this thread.

I’d write it as a macro and leverage Audacity’s built-in processing:
LUFS.txt (524 Bytes)

Thanks, Steve. I hoped that you would respond :slight_smile:

Macro - Despite preferring to keep discussions on macro related topics within the appropriate forum sections, let me answer here:

  1. Your macro LUFS.txt accurately measures integrated loudness, but it doesn’t align with the readings of most popular tools. Conversely, my macro provides measurements that match those tools, but they’re technically “incorrect.”
    Loudness Measurement.txt (1.2 KB)
    Although I understand the problem with incorrect measurement, I opted for this approach to avoid confusion among users of multiple tools.
  2. Your macro works only for 32-bit tracks. Mine works also for 16 and 24-bit tracks. Further details are explained in the Macros and Scripting section.
  3. I recommend storing values in the unique properties of scratch as a safer method with any drawbacks.

Nyquist Plugin - I posted a new plugin yesterday. While I recommend the macro, the plugin provides added functionality in complex scenarios. For instance, I or anybody else can include its code into another plugin, which enables to measure integrated loudness and execute additional processing based on that. Now, instead of just RMS, I can also choose LUFS internal measurement in custom Nyquist plugins.

Despite multiple attempts, I’m still struggling with optimal “top-level” efficiency - minimal RAM use and quickest processing time. Any guidance would be much appreciated.

Thank you in advance for reviewing :pray:

I’m not convinced that it does “avoid confusion”.
Many “Loudness” meters handle mono audio incorrectly. Personally I think that creating yet another tool that handles mono incorrectly will add to the problem rather than avoid confusion.

I’m sure that you do understand the issue (we’ve discussed it previously), but for the benefit of other readers:

If you have a mono recording as a single channel audio file, and the same mono recording as a 2 channel audio file (both channels containing the exact same audio), then both files will sound identical. The problem is that many Loudness meters will say that the single channel version is 3 LU quieter than the 2 channel version, which is clearly nonsense as they sound identical.

The macro that I posted was only intended as a demonstration. It could easily be modified to support integer format track by simply changing the Loudness Normalization step to normalize to a lower level, then modify the Nyquist code to compensate, but that makes the code less easy to understand.

(I did however notice that there is a bug in Audacity’s “Loudness Normalization” effect. If the audio level is > 0 LUFS, the effect silently fails. I’ll log this on the Audacity bug tracker if it’s not already logged.)

I would also strongly urge all Audacity users to always use 32-bit float format in all of their projects.

There is nothing “unsafe” about using the value of *scratch* directly provided that the code does not make assumptions about the value of *scratch*.

In the case of LUFS.txt, the macro sets the value of *scratch*, then reads the value back, then clears the value (leaving *scratch* unbound at the end). There is no need in this case to complicate the code with property lists rather than simply binding the symbol to a value.

This is very tricky. From a quick look I suspect that the problem lies with using the PEAK function. Setting *track* to NIL does not help in this case because RAM can’t be released while there a reference to the data exists. The solution that comes to mind would be to write your own DSP function using OOP so that it consumes the samples as it searches for the peak. Unfortunately that would be both difficult and much slower than using PEAK.

(Also a similar issue with SND-LENGTH).

An alternative “solution” is to enforce a maximum length (possibly by checking the max length as well as the min length in the ERROR-CHECK function).

There’s an associated problem with the .ny code, which is that it doesn’t restore SND-SET-MAX-AUDIO-MEM back to the original value. This will affect the global environment until Audacity is restarted.
A minimal example of how to restore SND-SET-MAX-AUDIO-MEM:

(setf old (SND-SET-MAX-AUDIO-MEM 1000))
(SND-SET-MAX-AUDIO-MEM old)
(print old)

You will also need to take care that SND-SET-MAX-AUDIO-MEM is not left in a non-default state if the ERROR-CHECK function throws.