4
$\begingroup$

I inherited responsibility for some audio downsampling code that I don't fully understand:

  // downsampling variables
  var filter = [
      -0.037935, -0.00089024, 0.040173, 0.019989, 0.0047792, -0.058675, -0.056487,
      -0.0040653, 0.14527, 0.26927, 0.33913, 0.26927, 0.14527, -0.0040653, -0.056487,
      -0.058675, 0.0047792, 0.019989, 0.040173, -0.00089024, -0.037935
    ],
    samplingRateRatio = this.options.sourceSampleRate / TARGET_SAMPLE_RATE,
    nOutputSamples = Math.floor((buffer.length - filter.length) / (samplingRateRatio)) + 1,
    outputBuffer = new Float32Array(nOutputSamples);

  for (i = 0; i + filter.length - 1 < buffer.length; i++) {
    offset = Math.round(samplingRateRatio * i);
    var sample = 0;
    for (var j = 0; j < filter.length; ++j) {
      sample += buffer[offset + j] * filter[j];
    }
    outputBuffer[i] = sample;
  }

The audio is always in $32$-bit floats (from the Web Audio API). The source sample rate is typically, but perhaps not always $48\textrm{ kHz}$. The output is always $16\textrm{ kHz}$ at the moment, although I'm interested in providing an $8\textrm{ kHz}$ option which is part of why I want to understand this better.

The things I do understand:

  • Each output sample is basically a weighted average of $21$ input samples
  • Correspondingly, most input samples are partially represented in $21$ output samples
  • Somehow it's filtering out frequencies above $8\textrm{ kHz}$, and that that is necessary to avoid aliasing
  • The weights (filter array) make an interesting graph:

    graph of filter array

Questions I have:

  1. Is this a well-known algorithm that I just don't recognize because I'm a noob? (Perhaps an FIR Filter?)
  2. Which part of the code is the low-pass filter? The fact that it uses $21$ samples when I'd expect $3$-$4$ to suffice? Or is it the "weights" rather than simply averaging the $21$ samples? All of it?
  3. How would I go about choosing the filter values if I didn't already have this list?
  4. Is it significant that the sum of filter array is almost-but-not-quite $1$? ($0.98198732$)
  5. Besides the TARGET_SAMPLE_RATE, what would I need to change for $8\textrm{ kHz}$ output? I assume the low-pass filter would need to go down to a $4\textrm{ kHz}$ max, but I don't know how to do that.

Answers to one or all questions are welcome.

$\endgroup$
  • 3
    $\begingroup$ 1. It is a simple approach to downsampling by lowpass filtering (to prevent aliasing) and decimation. 2. The lowpass filter is represented by its impulse response (looks almost like a sinc, doesn't it?). You can see the frequency response here, which removes components below 8kHz . 3. Design the filter which removes frequencies below 48k/2*target_rate. 4. It is up to you and how do you want to target the overflows (if there are any). Normalizing filter by its length should suffice. 5. Design a linear phase FIR filter with a cutoff of 4kHz. $\endgroup$ – jojek Feb 6 '17 at 16:47
  • $\begingroup$ How would I go about doing you answers to #3 and #5 - any recommended reading? $\endgroup$ – Nathan Friedly Feb 6 '17 at 20:10
  • $\begingroup$ Unless the upper frequency limit of the signal in the "buffer" array is less than the decimated, or post-sampled, Nyquist interval, aliasing will occur. Perhaps the original audio is sampled at a rate of 48 kHz, but the audio is band-limited to frequencies below 8 kHz. In this case no aliasing will occur if the sample rate is decreased to 16 kHz. Maybe the original audio signals are just over sampled at 48 kHz, which is quite common for a lot of audio signals I've encountered on the Internet. $\endgroup$ – Michael_RW Feb 8 '17 at 16:24
  • $\begingroup$ Very good questions. This is what I am looking for. Hopefully, there is a good answer. $\endgroup$ – xiaoke Aug 29 at 18:01
2
$\begingroup$

The vector of coefficients that you gave implement a finite impulse response (FIR) filter with a cutoff of 8 kHz (for an input rate of 48 kHz). As you surmised, these coefficients specify weights that are applied to a sliding window of input samples to generate each output sample; this is the basic operating principle of an FIR filter.

An illustrative way to look at the filter's behavior is via its frequency response. This indicates what frequencies are passed through the filter unchanged and which are attenuated. Generally, if you want to downsample a signal, you first need to bandlimit it so that any frequency content above the post-downsampling Nyquist rate are sufficiently attenuated, to avoid aliasing as much as needed for a particular application.

jojek provided the frequency response in a comment above, which I will shamelessly reproduce here:

enter image description here

Some observations:

  • This shows a lowpass characteristic, with a band of low frequencies passed through the filter with little to no attenuation, followed by a transition region where the frequency response falls off. It then enters a stopband where all frequencies are significantly more attenuated than in the passband. There are no hard-and-fast objective terms that can be used in all cases to identify exactly where these regions start and end, but you can get an idea by looking at the plot.

  • This is not a particularly good filter in terms of stopband attenuation (it only attenuates frequencies above the desired band by ~20 dB at minimum), but better specifications would require more coefficients (and therefore more computations per output sample).

    • Since the filter only attenuates the stopband content by ~20 dB, this data will alias into the signal that you are left with after downsampling. Any content that was in the stopband region in the original signal will be aliased down to some other frequency in the downsampled signal, with its power level decreased by ~20 dB (or more, depending on exactly where in the stopband it is; notice that there are some narrow regions, often called nulls or zeros of the filter, where the filter generates very high attenuation).
  • This is an equiripple filter, in that the ripples in the stopband have an equal peak height (at just under -20 dB). Based on some quick playing around with Matlab's Filter Design and Analysis tool, it looks like this was designed for a passband from 0-7 kHz and a stopband starting at 9 kHz, with equal weights to the passband and stopband approximation error (these are parameters for the Remez exchange algorithm used to design equiripple filters).

If you wanted to enhance or duplicate this design, you would need to use a digital filter design procedure. As far as implementation goes, these range from pretty simple (e.g. the window method) to quite complex (Remez exchange as I described before).

However, there are a lot of tools already out there to do this for you, both in paid tools like MATLAB and in free open-source tools like SciPy. Here's an example of how you could use SciPy to design a filter for the 4 kHz output rate that you mentioned (you'll also need matplotlib installed to plot the result):

from scipy import signal
import numpy as np

# Sample rate of the input signal (Hz).
fs = 48e3
# Desired number of coefficients in the lowpass filter.
N = 21
# Passband edge (Hz).
fpass = 3e3
# Stopband edge (Hz).
fstop = 5e3
# Generate the desired filter coefficients. The arguments can be interpreted as follows:
#   - the desired length of the filter
#   - list of band edges; this describes a passband from 0 Hz to `fpass` Hz
#     and a stopband from `fstop` Hz to `fs/2` Hz
#   - a response of 1 in the passband is desired (0 dB attenuation), and
#     a response of 0 in the stopband is desired
#   - since we passed frequencies in Hz to the filter design routine, it needs
#     to know the value of the sample rate
h = signal.remez(N, [0, fpass, fstop, fs/2], [1, 0], Hz = fs)

# Normalize the filter to 0 dB gain at zero frequency (which is just equal to
# the sum of the filter coefficients).
h = h / np.sum(h)

# Calculate the frequency response of the filter, then calculate its magnitude
# response, measured in dB.
freq, response = signal.freqz(h)
ampl = 20*np.log10(np.abs(response))

import matplotlib.pyplot as plt
fig = plt.figure()
ax1 = fig.add_subplot(111)
ax1.plot(freq/(2*np.pi)*fs, ampl, 'b-')
plt.title('Magnitude response of designed filter')
plt.xlabel('Frequency (Hz)')
plt.ylabel('Magnitude (dB)')
plt.grid()
plt.show()

Note that digital filters don't have any inherent knowledge of their sample rate. Instead, their behavior is normalized to whatever sample rate you push through the filter. For example, if you took your original filter and passed data into it at 24 kHz instead of 48 kHz, it would have a cutoff commensurate with a 4 kHz output rate instead of the 8 kHz of the original.

With that said, I'm not saying that this is a great filter design. There is noticeable ripple in the passband and the stopband attenuation is relatively poor. There are a lot of parameters that you can adjust with this method, and there are lots of other filter design techniques as well; this should get you a start. As far as a text goes, I would recommend Understanding DSP by Lyons; it's a good introductory book.

$\endgroup$
  • 1
    $\begingroup$ No shame in stealing the image ;) I am glad that someone answered that question - I couldn't find a time to do so. $\endgroup$ – jojek Feb 8 '17 at 14:06
0
$\begingroup$

A filter design tool that I came across a few days ago is called MicroModeler DSP. It is free, interactive, and visual so its likely useful for your immediate needs.

The link is http://www.micromodeler.com/dsp/#.

Michael.

$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.