Octave band filter

This simple process/effect plug-in does band-pass filtering over octave bands
at the standard octave band frequencies: 31, 63, 125, 250, 500, 1k, 2k, 4k, 8kHz,
using the built-in 8 pole high- and low-pass filters. It has roughly unity gain in the
passband, and rapid rolloff in the stopband.

Octave bands are frequently used in technical audio analysis.

This plug-in is meant to be part of a set for measuring room reverberation time
over standard octave bands, including DecayRate.ny, OctaveBand.ny, and RMS.ny

Boffin
OctaveBand.ny (433 Bytes)

That works boffin, and a novel way to select the centre frequency.
No 16 kHz band?

Perhaps something like:

;control fc "Center frequency (Hz)" choice "31 Hz,63 Hz,125 Hz,250 Hz,500,1 k,2 k,4 k,8 k,16 k" 5

(let* ((f0 (* 1000 (expt 2.0 (- fc 5))))
       (s (highpass8 s (/ f0 (sqrt 2.0)))))
  (if (< fc 9) (lowpass8 s (* f0 (sqrt 2.0))) s))

Added more bands, now from 8 Hz to 32 kHz. No longer bombs if frequency extends above Nyquist frequency.

This simple process/effect plug-in does band-pass filtering over octave bands at the standard octave band frequencies: 8, 16, 31, 63, 125, 250, 500, 1k, 2k, 4k, 8k, 16 k, 32 k using the built-in 8 pole high- and low-pass filters. It has roughly unity gain in the passband, and rapid rolloff in the stopband.
OctaveBand.ny (535 Bytes)

Congratulations, that works fine.

Just a few thoughts for you -

While not really an issue with a little program, and more a matter of programming style, it’s generally good to avoid unnecessary global variable. In large programs that cab be important because of the increased risk of using the same symbol to represent (what should be) different variables (so it may become more important as you develop your set of measuring tools.

To avoid unnecessary global variables you can use LET (http://www.audacity-forum.de/download/edgar/nyquist/nyquist-doc/xlisp/xlisp-ref/xlisp-ref-148.htm) or LET* (http://www.audacity-forum.de/download/edgar/nyquist/nyquist-doc/xlisp/xlisp-ref/xlisp-ref-149.htm) to declare variables as local. For example, your main code block, with the frequencies defined as local to that block of code, could be something like:

(let* ((fc (* 1000.0 (expt 2.0 (- fchoice 7))))
       (nyq (/ *sound-srate* 2))
       (f0 (min nyq (* fc (sqrt 0.5))))
       (f1 (min nyq (* fc (sqrt 2.0)))))
  (highpass8 (lowpass8 s f1) f0))

Also, there is probably not much point in running the high pass filter at the Nyquist frequency because it’s virtually a brick wall. Similarly a low pass filter at the Nyquist frequency is virtually an open door. For efficiency these could be made conditional (though in reality it will have very little impact on processing speed):

(let* ((fc (* 1000.0 (expt 2.0 (- fchoice 7))))
       (nyq (/ *sound-srate* 2))
       (f0 (* fc (sqrt 0.5)))
       (f1 (* fc (sqrt 2.0))))
  (cond
    ((>= f0 nyq)(s-rest 1))
    ((>= f1 nyq)(highpass8 s f0))
    (T (highpass8 (lowpass8 s f1) f0))))

Another option could be to handle the case of (> f0 (/ sound-srate 2)) as an error (generally better than waiting for a long selection to return nothing)

    ((>= f0 nyq) (format nil "For track sample rates of ~a Hz or less~%~
                    the centre frequency cannot be greater than ~a Hz."
                    *sound-srate*
                    (truncate (/ nyq (sqrt 0.5)))))

Thank you, Steve. That’s a good lesson on how to use local variables, and now I know about the difference between let and let*. I appreciate that you’d take the trouble to show me how to do it right.

I have a couple of questions, if you or someone else could help me out.

First, I notice that the (s-rest 1) expression does not return exactly zeros. On my machine, Audacity on Win32, on a 16 bit track, it returns high-frequency noise of a few counts. Perhaps this is some sort of rounding error, though it’s hard to see how zero would round to something else.

Secondly, if I use (const 0.0 1), it returns a track of constant values as expected, but with a duration of 1/20 of the original. What’s up with that? This seems to contradict this page: http://www.audacity-forum.de/download/edgar/nyquist/nyquist-doc/manual/part6.html#49
“See also s-rest, which is equivalent to calling const with zero…”

Bob

Nyquist, like other effects in Audacity, processes in 32 bit float. It is highly recommended that you work in 32 bit float format in Audacity (unless you have a really good overriding reason not to :wink:) 32 bit float format provides extreme accuracy when generating and processing sound. I don’t know how deeply you want to go into the explanation here, but as you’re working with Nyquist I think that you may be interested in the technical details…

Let’s take an example (these snippets can be run in the Nyquist Prompt effect).
For the first part, ensure that your track is 32 bit float format.

Let’s say that we want to generate a short “plink” sound that rises rapidly to 0.25 (-12 dB) and then decays exponentially over about 2 seconds.

We can generate a tone with osc (which has a default duration of 1).

Because we are using the Nyquist Prompt, which is a “process” effect, warp is such that by default 1 second of “local time” is stretched to the duration of the selection - but we don’t want that behaviour here - we want “1” to be “1 second” of actual track time and “2” to be “2 seconds” of actual track time, so we will wrap out code in abs-env so that it is evaluated with “absolute” (global / real) time values. So this will generate a 2 second tone (and it does not matter how long the track selection is)

(abs-env
  (osc 72 2))

Now we want to shape the tone with an “envelope”.
Nyquist provides a number of “piece-wise approximation” functions that are really useful for creating envelopes Nyquist Functions

Because we want an exponential decay, we can use one of the exponential variants, for example pwev. Now we need to be a bit careful when using these exponential functions because if we set a break-point to have an amplitude of zero, the function will end up trying to calculate “log 0” which is undefined/infinite. To avoid this, we will define a very small number:

(setq zero (/ (power 2 16)))

So now we can create an envelope that rises in, say 2 milliseconds, from “zero” (our very small number) up to 0.25, then decays exponentially over the rest of the 2 seconds duration to “zero”

(abs-env
  (pwev zero 0.002 0.25 2 zero))

In order to apply our envelope to the tone, all we need to do is to “multiply” the tone by the envelope. Thus, each sample value of the sound (the tone) is multiplied by the corresponding sample value of the envelope:

(setq zero (/ (power 2 16)))
(abs-env
  (mult (pwev zero 0.002 0.25 2 zero)
  (osc 72 2))))

And there is our “plink” :stuck_out_tongue:

Now, let’s see close up what is happening at the very end of the “plink”.
We will now use a second piece of code to amplify the Plink with an envelope that expands rapidly from an amplitude of 1 to a very large value.
We don’t need to use abs-env this time because we can just select the entire tone (which is 2 second duration in “real” time, but is stretched to “1” in “local” time because we are applying a process type effect).
For the “reverse envelope” we will use:

(pwev 1 1 (power 2 14))

This envelope rises exponentially from 1 (which will give unity gain when we multiply the sound by "1) up to 2^14 (which, if my arithmetic is correct should give us an end amplitude of 0.25)

(mult s (pwev 1 1 (power 2 14)))

Assuming that you are using a 32 bit float format track, you should now have a fairly constant tone, amplitude 0.25 and duration 2 seconds (still has the fast fade-in at the start).

Now try the same steps with a 16 bit track :open_mouth:
The problem here is that our original plink went so quiet that the 16 bit format “ran out of bits” - Audacity did it’s best to “round” the sample values, but when we amplified the end of the plink to 16384 times it’s original level we run head on into the “dither noise

The noise that is produced by ‘(s-rest 1)’ is due to the enabled dither setting in the quality tab of the preferences dialog. Nyquist works always with 32 bit and the produced sound is resampled before it goes back to Audacity. Hence, the nyquist output is handled like an ordinary import or conversion to 16 bit. It is really the best idea to use exclusively 32 bit float.
2. The const function works at a sample rate of 2205 (so called control-srate). Audacity does not respect the sample rate of the returned sounds, it automatically assumes the track’s sample-rate. Thus the 44100 samples of a 20 s sound at 2205 Hz sr will be squeezed into a single second.
You can either use ‘(control-srate-abs 44100 (const 1 1))’ or the low level function ‘(snd-const 1 0 44100 1)’. The latter has the disadvantage that the duration (the last ‘1’ value) hasn’t the length of the selection. You can achieve this by replacing the 1 with ‘(get-duration 1)’.

I know this is a bit late in the game for a thread over ten years old. Can anyone help me adapt this to make 1/3 octave bands from 1t least 125 Hz to 8 kHz?