How to make up for decibel loss in High/Lowpass filter

I’m sure that this program has already been written so far (and done better), but right now I’m just writing Nyquist code as an attempt to try to learn how to program in Nyquist. Let’s say I have the following piece of code:

;version 1
;type process
;name "HighLowPass..."
;action "Stereo-fying the channels"
;info "Choose a Channel to be Lowpassed as well as a High-pass and Low-pass
 value.nThe left and right channels will be passed accordingly."
;control channel "pick a channel to be lowpassed" choice "Left, Right" 0
;control lowpass "Pick Lowpass Value" int "Hz" 200 100 13000
;control highpass "Pick Highpass Value" int "Hz" 500 0 20000


(if (= channel 0)
  (vector 
    (lp 
      (aref s 0) lowpass) 
    (hp 
      (aref s 1) highpass))
    
     (vector
      (hp 
        (aref s 0) highpass)
      (lp (aref s 1) lowpass)))

The code passes one channel high and another channel low. The intent is to create a type of stereo sound on mono tracks. (Where one channel is more bass friendly, and another channel is more treble friendly). The problem is (as you would imagine) the resulting audio looses quite a few decibels. I know that there’s probably a lot of code out there that already does this, but for the sake of learning the language: How might I go about calculating the decibel loss, then modifying the code so that each channel is multiplied by the number of decibels it looses, so that it may loose certain frequencies, but maintains a constant volume?

Unfortunately there is probably no completely satisfactory answer.

The high/low pass filters in Nyquist are based on biquad filters which behave much like analogue IIR filters and create frequency dependent phase shifts. This means that the peak level after filtering will be dependent on the frequency content and phase of the audio being processed so there is no way to accurately calculate it (as far as I’m aware).

What you can do is to use the peak function to measure the peak amplitude of the processed audio, but you need to be a bit careful using (peak) in Audacity plug-ins as it is calculated in RAM. If you try to calculate the peak amplitude of a very long audio selection and the computer does not have sufficient RAM it can cause Audacity to freeze or crash. To avoid this problem, the maxlen parameter must be set so as prevent excessive RAM usage. 1 million samples is likely to be safe on most computers, but that’s only 22 seconds of audio at 44.1 kHz sample rate, so the measurement will be based on only the first 22 seconds. You could push the maxlen parameter up to 100 million samples (nearly 40 minutes) but with 4 bytes to one 32 bit sample that’s going to use about 400 MB of RAM and if the computer does not have that amount of free RAM it’s a potential crash waiting to happen.

To normalise a mono sound to 0 dB, based on the first 10 million samples you could use something like:

(mult mysound (/ (peak mysound 10000000)))

Ah Okay. That at least basically does what I want it to. I ended up writing this:

;nyquist plug-in
;version 3
;type process
;name "HighLowPassNormalize..."
;action "Stereo-fying the channels"
;info "info stuff here"

;control channel "pick a channel to be lowpassed" choice "Left, Right" 0
;control lowpass "Pick Lowpass Value" int "Hz" 200 0 13000
;control highpass "Pick Highpass Value" int "Hz" 500 0 2000


(if (= channel 0)
  (vector 
    (mult (lp 
      (aref s 0) lowpass) 
          (/ (peak (aref s 0) 10000000))) 
    (mult (hp 
      (aref s 1) highpass)
          (/ (peak (aref s 1) 10000000))))
    
     (vector
      (mult
        (hp 
        (aref s 0) highpass) (/ (peak (aref s 0) 10000000)))
      (mult
        (lp (aref s 1) lowpass)(/ (peak (aref s 1) 10000000)))))

Now my question is, what specifically is this doing in laymen’s terms? Because I only have a vague idea of what it actually is, aside from code that amplifies the channels back up to a decent level.
peak is supposed to represent the highest level that the channel reaches within it’s first 10000000 samples? But that’s about all I really get.

“Peak” computes the maximum absolute value of the sound within the first “maxlen” samples.
“Absolute” values ignores the sign (+ or -) and looks only at the value, so the absolute value of -3 (minus three) is just “3”.

In the case of your code “peak” is calculating the peak value of the input sounds (aref s 0) and (aref s 1).

Looking at a simple example, let’s say that we have a mono track with a peak amplitude of 0.8 (linear scale).
The audio from the track will be passed to Nyquist in the global variable “s”.
(peak s 1000000) will compute the maximum absolute value and return the number “0.8” assuming that the peak is within the first 1000000 samples.

(/ val) is the same as (/ 1 val)

so (/ (peak s 1000000)) equates to 1/0.8
If we then multiply the sound “s” by (/ (peak s 1000000) the resulting peak value =
original peak value” x 1/“original peak value” = 0.8/0.8 = 1

In other words we can normalize the peak amplitude to 1.0 linear scale (= 0 dB) by multiplying the sound by 1/“peak amplitude of the sound”.

ny:all is a built-in value, which in Audacity Nyquist is equal to 1 billion (1000000000)

(mult s (/ (peak s ny:all)))

This code will normalize a mono track to 0 dB (provided that the computer has enough free RAM for the track audio).

\

Ok so far?


With a small modification you your code you could calculate the filtered audio first and then normalize to 0 dB:

;control channel "pick a channel to be lowpassed" choice "Left, Right" 0
;control lowpass "Pick Lowpass Frequency" real "Hz" 200 0 13000
;control highpass "Pick Highpass Frequency" real "Hz" 500 0 2000

;; filter audio
(case channel
  (0 (setf (aref s 0)(lp (aref s 0) lowpass))
     (setf (aref s 1)(hp (aref s 1) highpass)))
  (1 (setf (aref s 0)(hp (aref s 0) highpass))
     (setf (aref s 1)(lp (aref s 1) lowpass))))

(setq bignum 10000000)

;; normalize channels
(vector
  (mult 
    (/ (peak (aref s 0) bignum))
    (aref s 0))
  (mult 
    (/ (peak (aref s 1) bignum))
    (aref s 1)))

If you prefer to normalize to some value other than 1.0, replace (/ (peak (aref s 1) bignum)) with (/ NewAmp (peak (aref s 1) bignum)) where NewAmp is the target amplitude. For example, to normalize to -3 dB

(mult <sound> (/ (db-to-linear -3.0) (peak <sound> bignum))

If you want the output audio to have the same amplitude as the input audio, you would need to compute the peak level of the audio before filtering and the peak level of the audio after filtering, so for the left channel:

(setq bignum 10000000)
(setq OldAmp (peak (aref s 0) bignum)
; ....
; filter (aref s 0)
; ....
(setq NewAmp (peak (aref s 0) bignum)
; ....
(mult (/ OldAmp NewAmp)(aref s 0))

Thank you so much.

I realise this post is a bit old now, but it caught my attention.

Firstly probably the best way to “easily” achieve pseudo-stereo is with an all-pass filter to introduce phase differences between the channels. Then it won’t colour your output by changing the frequency content.

None-the less, to work out how much the gain of each channel has changed, the above methods proposed are based on maintaining the “peak” level, however, your perceived level may actually be controlled by the energy in the sound. To check this you could determine a parameter which is commonly used in acoustics, called the Equivalent Continuous Noise Level (Leq) of the sound and compare that to before and after the filter and then apply the dB difference. The Leq is fairly straight forward to calculate. This has an advantage because the peak may well have been filtered out by your process even if the overall level didn’t change all that much, which may result in a predicted large change in level.

Just to add to this confusion, is that your perceived level probably also depends on what frequencies have been removed, since, for example you are more likely to notice missing mid-high frequencies than low (in terms of level difference).

Another way would be to analyse the maximum exponentially time weighted rms average of the sound but that is a bit trickier.

The code to calculate the Leq of your sound in dB is below.

(defun log10 (x)
	(/ (log (float x)) (log 10.0))
)

(defun dB (x)
	(* 10.0 (log10 (float x)))
)

(defun snd-Leq (s1)
	(setf time (+ (/ (- len 1) *sound-srate*) (snd-t0 s)))
	(dB (/ (sref (sound (integrate (mult s1 s1))) time) time))
)

(snd-Leq s)

Interesting post kai.fisher.

In the code example, why (- len 1) ?

For all audio selections, (snd-t0 s) is zero (and the logical duration of the selection is always 1.0)
so shouldn’t time just be (/ len sound-srate) in (/ time)?

But then the final sample is “1” sample less than that, so sref we would need to be (sref sound (/ (- len 1) sound-srate))

I presume that for a constant amplitude sine wave, the Leq should be the same for any whole number of cycles?
Testing these two alternative functions, the second appears to give more consistent results when testing a small number of complete cycles (particularly if you use a frequency that has an exact number of samples per cycle):

(defun snd-Leq (s1)
   (setf time (+ (/ (- len 1) *sound-srate*) (snd-t0 s)))
   (dB (/ (sref (sound (integrate (mult s1 s1))) time) time)))

(defun snd-Leq2 (s1)
   (setf time (/ len *sound-srate*))
   (setf lastsample (/ (- len 1) *sound-srate*))
   (dB (/ (sref (sound (integrate (mult s1 s1))) lastsample) time)))

If that second function is correct, then a more memory efficient way to calculate would be:

(linear-to-db 
  (sqrt (/ (peak (integrate (mult s s)) ny:all)(get-duration 1))))

Ideally this would not be the case. It should be just len as far as I know, but on many examples I tried (eg generate a 10s random noise signal) I got an error saying that I was past the end of the audio, so this was my fix. In reality being 1 sample out will not change the Leq significantly at all, but it would be nice to fix it.

OK, I didn’t realise that (snd-t0 s) was always 0 for a selection. In this case, you are right its not needed. Basically I want to divide the final result of the integral by the time duration. The Leq is essentially an average of the squared noise level.

Yes I think this would work well. I didn’t think to use peak - but since the integral is always getting larger then the last sample should always be the peak, in this case this function would work. Only that - if you use my “dB” function - you don’t need the additional sqrt.
ie

    (dB (/ (peak (integrate (mult s s)) ny:all)(get-duration 1)))

Although admitedly the implementation of my dB function may not be faster than doing a sqrt… who knows - its only one operation anyway.

Yes. A sine wave of exactly an integer number of wavelengths will always have an Leq of -3dB (relative to full scale). The longer your sine wave sample the less influence being a non-integer number of waves will be. Try for example a sine wave of 30seconds. Then increase and decrease it by some arbitrary (but small amount) and you won’t notice the Leq change. For most things in acoustics you take a measurement for at least 1minute, but preferably more like 15 minutes depending on what exactly you are measuring and how much it is fluctuating. Basically the more fluctuation you have in noise level the longer you have to measure to get a representative Leq. This is also why my (- len 1) “error” is negligible.

I guessed that was the case, but with long selections memory usage can be important, hence my comment much earlier in this thread about “no completely satisfactory answer”.

There was a long discussion a couple of years ago on the Nyquist mailing list regarding the inability of Audacity Nyquist to normalize a track without loading the entire selection into RAM. Roger Dannenberg (the creator of Nyquist) came up with a way to normalize long tracks, but it required running two effects (or running one plug-in twice). On the first pass it would measure the peak amplitude without retaining “s”, then on the second pass it would apply the normalization determined by the first pass. In practice it is easier to just use Audacity’s built in Normalization effect, but if I can find that topic on the mailing list I’ll post a link.

Found it: http://www.cs.cmu.edu/~music/nyquist/debug-plugin.html

Yeah, once you lower frequency levels the whole dynamic of the audio is changed, so
who could really say what the peak value would be?

For instance, your whole audio could lower to (say) 0.5, but there could be one spike
that reaches up to 0.9, thus making using PEAK pretty useless.

A good approach (IMO), at least for end users, is to add a volume adjustment slider.
That way, the user can (more easily) boost their audio to their desired, uh, levels.

For me, it’d be nice if there were some function that could handle the task in Nyquist,
such as polling before and after or something. I wouldn’t mind if that 0.9 peak got
chopped (for instance).

Still, such an “auto” effect could produce disasterous consequences to end-user audio
without some control, perhaps the reasoning for Q levels or something? :astonished:

For my new plug-in (due out soon!) I’m just writing a note in the read-me suggesting
that users handle such a thing themselves. Too many sliders can sometimes distract
from the intention and theme of your utility…