• Welcome to ASR. There are many reviews of audio hardware and expert members to help answer your questions. Click here to have your audio equipment measured for free!

Dynamic loudness compensation with miniDSP

kifeep

Member
Joined
Dec 16, 2023
Messages
95
Likes
91
A while back, I ditched my AVR for a miniDSP Flex HT. I really liked having more direct control over room correction (I use a combination of MSO and REW), but the one thing I missed was dynamic loudness compensation (or Dynamic EQ in Denon-speak).

I've recently completed a DIY project to replicate this feature in my system. I thought I'd share it here to help anyone who'd like to do the same thing, and also to get feedback if I've made any glaring errors.

One other note before diving in: I am not a programmer and have had no training. I've included the python script below, but I'm certain the same thing could be accomplished more elegantly and efficiently. The script comes with absolutely no warranties, as you'd expect with something posted to a forum, but maybe even more so since I don't actually know what I'm doing.

Ok, enough preamble.

1. Controlling the miniDSP​

Controlling the miniDSP unit is accomplished with the open source minidsp-rs package:

Repository: https://github.com/mrene/minidsp-rs
Documentation: https://minidsp-rs.pages.dev

I installed the package on a Raspberry Pi Zero 2W using the available .deb package files.

When I purchased my miniDSP, I also got a WI-DG wireless bridge. This allowed me to connect the Device Console configuration software to the unit over my local network. The wireless bridge also provides an API over TCP that can be addressed by minidsp-rs. So, for example, a status request can be made like this:

$ minidsp --tcp 192.168.0.10 status

Unfortunately, my Flex HD is not yet supported by minidsp-rs, and the above status request returned information on volume, preset, and mute, but the input and output levels were blank. The Flex HTx is partially supported, however, and I was able to get input and output levels using:

$ minidsp --force-kind flexhtx --tcp 192.168.0.10 status

Additionally, minidsp-rs can be told to output to JSON for easier manipulation:

$ minidsp --output json --force-kind flexhtx --tcp 192.168.0.10 status

There's no push functionality in minidsp-rs, so my first script just polled the unit every few seconds and printed the results. This worked great when the unit was on. When the unit was turned off, the command returned an error which was easy to trap, but often took 10-15 seconds to timeout, which was not ideal.

So I connected the pi to the minidsp directly via USB and confirmed that the status request still threw an error when the unit was off, but only took a fraction of a second to do so. But now I needed a way to connect the Device Console software to the pi.

Fortunately, minidsp-rs comes bundled with a daemon and system service file for just this purpose. Follow the instructions to configure the daemon and set up the system service.

But now I had a new problem. If you make a status request to the WI-DG when Device Console is connected, the request is ignored and eventually times out. But if you make a status request to the unit over USB when Device Console is connected to the minidsp-rs daemon, it interrupts the service and throws javascript errors in Device Console. So I needed to stop polling the unit when Device Console was connected. I ended up using the ss command to check for an active connection:

$ ss -l state established | grep -w 5333

Where 5333 is the standard port for Device Console.

At this point I had a working script that polled for the current miniDSP volume level, and paused polling when Device Console was connected. Time to actually apply some loudness compensation.

2. CamillaDSP-style filters​

CamillaDSP has a well-documented loudness compensation feature, which seemed like a reasonable place to start:


This essentially amounts to a low shelf filter at 70 Hz and a high shelf filter at 3500 Hz, with a Q of 0.707 and a (default) gain of 0.5 times the difference between the current volume and some defined reference volume.

Reference volume is a bit of a vague target. I've often seen it stated that movies are mixed at 85 dB RMS, and it's been mentioned that music is usually mixed lower. Since this is all adjustable after the fact, I decided a reasonable starting place would be 82.5 dB, which corresponds on my system to about -5 on the miniDSP.

Once a reference level is chosen, it's easy to calculate the gain of the shelf filters (and the ratio can also be adjusted later). So now I just needed to get the filters dynamically loaded on the miniDSP.

The minidsp-rs documentation explains how to load filters from biquads in a file, but digging through the GitHub discussions, I found that biquads can also be loaded directly. The form of the command is:

$ minidsp output N peq M set -- b0 b1 b2 a1 a2

N and M are the zero-indexed channel and filter numbers, and b0, b1, b2, a1, and a2 are biquad coefficients. Setting input filters appears to be supported on some units, but didn't work on the Flex HT(x).

To calculate the biquad coefficients, I used the formulae found here:


Once I coded the calculations, I did a reality check by comparing the output to equivalent filters generated in REW. However the calculated values for a1 and a2 were always the opposite sign as those in REW, which disturbed me at first. Eventually, I found that this is a documented (almost in passing) feature on miniDSP:


With all the pieces now in place, I updated the script and prepared to enjoy automated dynamic compensation on my miniDSP!

Well, almost. The script worked great, and it sounded much better than no compensation, but it wasn't quite right. I played around with different reference levels and filter gain ratios, but was never entirely satisfied. So I figured I could do better.


3. ISO 226 based filters​

I decided to try to base my loudness compensation on the ISO 226 equal-loudness curves we have probably all seen. Graphs of the curves are everywhere, but it took me a little searching to find tabular data for them. There are probably other calculators out there, but this is the only one I found:


The calculator only produces 29 data points, but this is still enough to copy into a spreadsheet, export as a csv file, and import into REW. REW extrapolates values between the points.

Since I'd already selected 82.5 dB as my reference level, I created curves in REW for 82.5, 77.5, 72.5, 67.5, and 62.5 phon. Here they are:

curves.png


I'm actually not interested the curves so much as the difference between the curves, so first I normalized the SPL:

normalized.png


And then used trace division to plot the -5, -10, -15 and -20 curves relative to the reference level. These became my target curves for the compensation filters (note the zoomed-in scale):

targets.png


The next step was mostly trial and error. I needed to apply the shelf filters linearly (meaning the PEQ for -20 would be the same as -10, except with twice the gain). So I hand-crafted filters for -10, applied scaled versions to the other levels, and then iterated until I was satisfied. In the end, this is what I settled on:

Low Shelf:
Fc: 180 Hz
Q: 0.4
Gain: 0.5 * (reference_volume - current_volume)

High Shelf:
Fc: 12,000 Hz
Q: 0.66
Gain: 0.35 * (reference_volume - current_volume)

And here's how these filters compare to the targets:

predicted.png


As you can see, the low compensation starts diverging from the target at around 50 Hz, but this was preferable to deviations at higher frequencies, which I expected to be more audible.

The only changes I needed to make to my script were the values used to generate the biquads, so it was trivial to get the new compensation filters working. Sounded great. After a week or so, I decided that the compensation effect was just a little strong for my taste, so I adjusted my reference level down 2.5 dB and have been happy ever since.

4. Files​

I've attached the python script I wrote. Again: use at your own risk, this could break things! You'll want to set it up as a user service (should be easy to find instructions online).

I've also attached the .mdat file with the ISO 226 curves, targets, and filters:


I hope this is useful to someone.


Edit: minor changes for clarity and grammar
 

Attachments

Last edited:
Amazing work! How does it sound? I’m particularly curious about the high shelf, where I’d have assumed most listeners would have trouble discriminating a 4dB difference at 12kHz.
 
A while back, I ditched my AVR for a miniDSP Flex HT. I really liked having more direct control over room correction (I use a combination of MSO and REW), but the one thing I missed was dynamic loudness compensation (or Dynamic EQ in Denon-speak).

I've recently completed a DIY project to replicate this feature in my system. I thought I'd share it here to help anyone who'd like to do the same thing, and also to get feedback if I've made any glaring errors.

One other note before diving in: I am not a programmer and have had no training. I've included the python script below, but I'm certain the same thing could be accomplished more elegantly and efficiently. The script comes with absolutely no warranties, as you'd expect with something posted to a forum, but maybe even more so since I don't actually know what I'm doing.

Ok, enough preamble.

1. Controlling the miniDSP​

Controlling the miniDSP unit is accomplished with the open source minidsp-rs package:

Repository: https://github.com/mrene/minidsp-rs
Documentation: https://minidsp-rs.pages.dev

I installed the package on a Raspberry Pi Zero 2W using the available .deb package files.

When I purchased my miniDSP, I also got a WI-DG wireless bridge. This allowed me to connect the Device Console configuration software to the unit over my local network. The wireless bridge also provides an API over TCP that can be addressed by minidsp-rs. So, for example, a status request can be made like this:

$ minidsp --tcp 192.168.0.10 status

Unfortunately, my Flex HD is not yet supported by minidsp-rs, and the above status request returned information on volume, preset, and mute, but the input and output levels were blank. The Flex HTx is partially supported, however, and I was able to get input and output levels using:

$ minidsp --force-kind flexhtx --tcp 192.168.0.10 status

Additionally, minidsp-rs can be told to output to JSON for easier manipulation:

$ minidsp --output json --force-kind flexhtx --tcp 192.168.0.10 status

There's no push functionality in minidsp-rs, so my first script just polled the unit every few seconds and printed the results. This worked great when the unit was on. When the unit was turned off, the command returned an error (which was easy to trap) but often took 10-15 seconds to timeout, which was not ideal.

So I connected the pi to the minidsp directly via USB and confirmed that the status request still threw an error when the unit was off, but only took a fraction of a second to do so. But now I needed a way to connect the Device Console software to the pi.

Fortunately, minidsp-rs comes bundled with a daemon and system service file for just this purpose. Follow the instructions to configure the daemon and set up the system service.

But now I had a new problem. If you make a status request to the WI-DG when Device Console is connected, the request is ignored and eventually times out. But if you make a status request to the unit over USB when Device Console is connected to the minidsp-rs daemon, it interrupts the service and throws javascript errors in Device Console. So I needed to stop polling the unit when Device Console was connected. I ended up using the ss command to check for an active connection:

$ ss -l state established | grep -w 5333

Where 5333 is the standard port for Device Console.

At this point I had a working script that polled for the current miniDSP volume level, and paused polling when Device Console was connected. Time to actually apply some loudness compensation.

2. CamillaDSP-style filters​

CamillaDSP has a well-documented loudness compensation feature, which seemed like a reasonable place to start:


This essentially amounts to a low shelf filter at 70 Hz and a high shelf filter at 3500 Hz, with a Q of 0.707 and a (default) gain of 0.5 times the difference between the current volume and some defined reference volume.

Reference volume is a bit of a vague target. I've often seen it stated that movies are mixed at 85 dB RMS, and it's been mentioned that music is usually mixed lower. Since this is all adjustable after the fact, I decided a reasonable starting place would be 82.5 dB, which corresponds on my system to about -5 on the miniDSP.

Once a reference level is chosen, it's easy to calculate the gain of the shelf filters (and the ratio can also be adjusted later). So now I just needed to get the filters dynamically loaded on the miniDSP.

The minidsp-rs documentation explains how to load filters from biquads in a file, but digging through the GitHub discussions, I found that biquads can also be loaded directly. The form of the command is:

$ minidsp output <N> peq <M> set -- <b0> <b1> <b2> <a1> <a2>

Setting input filters appears to be supported on some units, but didn't work on the Flex HT(x). N and M are the zero-indexed channel and filter numbers, and b0, b1, b2, a1, and a2 are biquad coefficients.

To calculate the biquad coefficients, I used the formulae found here:


Once I coded the calculations, I did a reality check by comparing the output to equivalent filters generated in REW. However the calculated values for a1 and a2 were always the opposite sign as those in REW, which disturbed me at first. Eventually, I found that this is a documented (almost in passing) feature on miniDSP:


With all the pieces now in place, I updated the script and prepared to enjoy automated dynamic compensation on my miniDSP!

Well, almost. The script worked great, and it sounded much better than no compensation, but it wasn't quite right. I played around with different reference levels and filter gain ratios, but was never entirely satisfied. So I figured I could do better.


3. ISO 226 based filters​

I decided to try to base my loudness compensation on the ISO 226 equal-loudness curves we have probably all seen. Graphs of the curves are everywhere, but it took me a little searching to find tabular data for them. There are probably other calculators out there, but this is the only one I found:


The calculator only produces 29 data points, but this is still enough to copy into a spreadsheet, export as a csv file, and import into REW. REW extrapolates values between the points.

Since I'd already selected 82.5 dB as my reference level, I created curves in REW for 82.5, 77.5, 72.5, 67.5, and 62.5 phon. Here they are:

View attachment 484137

I'm actually not interested the curves so much as the difference between the curves, so first I normalized the SPL:

View attachment 484138

And then used trace arithmetic to plot the -5, -10, -15 and -20 curves relative to the reference level. These became my target curves for the compensation filters (note the zoomed-in scale):

View attachment 484140

The next step was mostly trial and error. I needed to apply the shelf filters linearly (meaning the PEQ for -20 would be the same as -10, except with twice the gain). So I hand-crafted filters for -10, applied scaled versions to the other levels, and then iterated until I was satisfied. In the end, this is what I settled on:

Low Shelf:
Fc: 180 Hz
Q: 0.4
Gain: 0.5 * (reference_volume - current_volume)

High Shelf:
Fc: 12,000 Hz
Q: 0.66
Gain: 0.35 * (reference_volume - current_volume)

And here's how these filters compare to the targets:

View attachment 484139

As you can see, the low compensation starts diverging from the target at around 50 Hz, but this was preferable to deviations at higher frequencies, which I expected to be more audible.

The only changes I needed to make to my script were the values used to generate the biquads, so it was trivial to get the new compensations filters working. Sounded great. After a week or so, I decided that the compensation effect was just a little strong for my taste, so I adjusted my reference level down 2.5 dB and have been happy ever since.

4. Files​

I've attached the python script I wrote. Again: use at your own risk, this could break things! You'll want to set it up as a user service (should be easy to find instructions online).

I've also attached the .mdat file with the ISO 226 curves, targets, and filters:



I hope this is useful to someone.
Fascinating. That's a lot of work and very interesting. Thank you
 
Great work, especially since this feature is frequently requested on the minidsp forum. Have you shared your work with the minidsp developers, too? It would be great to see adaptive loudness implemented in the standard user interface.
 
How does it sound?
Well, it's only meant to maintain tonal balance when turning down the volume. If I compare low-level listening with and without, then sure, the compensation is obvious (and great). But if I compare high-level listening to low-level listening, it doesn't sound like anything at all, which is the goal.

I’m particularly curious about the high shelf, where I’d have assumed most listeners would have trouble discriminating a 4dB difference at 12kHz.
I understand your intuition, but equal-loudness curves are based on what people can hear, so...

If you're curious, it should be fairly easy for you to adapt the script to turn the high shelf filter on and off at will (or randomly) to see how much difference it makes. It's not really a priority for me since I've already done the work and don't get anything back by turning the high shelf off.
 
Last edited:
Have you shared your work with the minidsp developers, too? It would be great to see adaptive loudness implemented in the standard user interface.
I've only posted here, so far. This solution requires an outboard processor, so I doubt it could be added to the standard user interface without changing the internal hardware. Possibly some future model with more spare processing capacity?
 
TL;DR: What exactly handles the dynamic part here? How do you control the volume and does it load a different filter every time you change the volume?
 
A very underrated feature and one of the reasons why I love my RME ADI-2 Pro. Very cool that you managed to implement it for a miniDSP device!
 
I think this is great.

Do you mind if I document it using AI?

I will share it here if anyone is interested.
 
TL;DR: What exactly handles the dynamic part here? How do you control the volume and does it load a different filter every time you change the volume?
I control the volume by handheld remote. The script monitors the volume setting on the miniDSP and loads 2 filters on each channel every time the volume (or preset) changes. Over USB, this takes a fraction of a second.
 
Amazing, that is the one feature I am missing the most on my Flex HT as well. I definitely need to do this as well. Thank you for sharing your great work!
 
Very-very impressive!
Kifip, you mention that loading corrections into input channels did not work. I suppose, this means that they should be pushed to all output channels?
What are the units for which loading input channels works?
And thank you again for sharing.
 
loading corrections into input channels did not work. I suppose, this means that they should be pushed to all output channels?
In my case, yes.

What are the units for which loading input channels works?
I don't have a definitive answer, since I only have one unit to test.

I suspect that the problem also exists on the Flex HTx, which I am emulating. Support for this model was added to minidsp-rs relatively recently, and does not appear to be complete. I also suspect that the devices listed as 'full support' in the minidsp-rs documentation (https://minidsp-rs.pages.dev/devices) would work fine. Anything else, who knows?

You'll probably get a better answer asking on the repository's GitHub discussion or Discord.
 
Updated script. Should be more stable.

Changes:
  • Select which presets to apply loudness correction to
  • Use async subprocess for shell commands
  • Properly queue miniDSP commands for serial execution
  • Add global async exception handler
  • More graceful shutdown at interrupt (including async task cleanup)
  • Execute loudness correction at Device Console disconnect (write-only problem)
  • Only send unbypass commands when needed (preset change, console disconnect)
  • Remove unused biquad calculations (reduce file size)
  • Create logs directory if it does not exist
  • Bundle with .service file
  • Rename with underscore (more pythonic)
 

Attachments

Back
Top Bottom