Finite impulse response (FIR) filtering and convolution are core DSP tools: you shape a signal by summing delayed, weighted copies of it.
miniDSP provides four related APIs so students can start with direct time-domain sums and then compare against the FFT overlap-add fast method.
Time-domain convolution
For input \(x[n]\) (length \(N\)) and kernel \(h[k]\) (length \(M\)), full linear convolution is:
\[
y[n] = \sum_{k=0}^{M-1} h[k]\,x[n-k], \quad n = 0,1,\ldots,N+M-2
\]
Out-of-range samples of \(x[\cdot]\) are treated as zero.
Reading the formula in C:
for (unsigned n = 0; n < out_len; n++) {
double acc = 0.0;
for (unsigned k = 0; k < kernel_len; k++) {
if (k > n) break;
unsigned si = n - k;
if (si >= signal_len) continue;
acc += signal[si] * kernel[k];
}
out[n] = acc;
}
API:
const double *kernel, unsigned kernel_len,
double *out);
unsigned MD_convolution_num_samples(unsigned signal_len, unsigned kernel_len)
Compute the output length of a full linear convolution.
void MD_convolution_time(const double *signal, unsigned signal_len, const double *kernel, unsigned kernel_len, double *out)
Time-domain full linear convolution (direct sum-of-products).
Visuals — response and magnitude spectrum:
Quick example:
Moving-average FIR filter
The moving-average filter is the simplest FIR low-pass:
\[
y[n] = \frac{1}{M}\sum_{k=0}^{M-1} x[n-k]
\]
This implementation is causal and uses zero-padded startup (fixed divisor \(M\) at every sample).
Reading the formula in C:
double sum = 0.0;
for (unsigned n = 0; n < signal_len; n++) {
sum += signal[n];
if (n >= window_len)
sum -= signal[n - window_len];
out[n] = sum / (double)window_len;
}
API:
unsigned window_len, double *out);
void MD_moving_average(const double *signal, unsigned signal_len, unsigned window_len, double *out)
Causal moving-average FIR filter with zero-padded startup.
Visuals — response and magnitude spectrum:
Quick example:
General FIR filter
With arbitrary coefficients \(b[k]\) (length \(M\)), causal FIR filtering is:
\[
y[n] = \sum_{k=0}^{M-1} b[k]\,x[n-k], \quad n = 0,1,\ldots,N-1
\]
This is the direct form used for most textbook FIR designs.
Reading the formula in C:
for (unsigned n = 0; n < signal_len; n++) {
double acc = 0.0;
for (unsigned k = 0; k < num_taps; k++) {
if (k > n) break;
acc += coeffs[k] * signal[n - k];
}
out[n] = acc;
}
API:
const double *coeffs, unsigned num_taps,
double *out);
void MD_fir_filter(const double *signal, unsigned signal_len, const double *coeffs, unsigned num_taps, double *out)
Apply a causal FIR filter with arbitrary coefficients.
Visuals — response and magnitude spectrum:
Quick example:
FFT overlap-add fast convolution
For long filters, direct convolution is \(O(NM)\). Overlap-add computes the same full linear convolution in blocks using FFTs:
\[
Y_b(k) = X_b(k)\,H(k), \quad
y_b[n] = \mathrm{IFFT}(Y_b(k))
\]
Each block's time-domain result is added into the output with overlap between adjacent blocks, after an inverse FFT (IFFT) per block.
Reading the formula in C:
for (unsigned start = 0; start < signal_len; start += block_len) {
for (unsigned n = 0; n < nfft; n++) block_time[n] = 0.0;
for (unsigned n = 0; n < block_len && start + n < signal_len; n++) {
block_time[n] = signal[start + n];
}
for (unsigned k = 0; k < nfft; k++) {
double x_re = 0.0, x_im = 0.0;
for (unsigned n = 0; n < nfft; n++) {
double theta = 2.0 * M_PI * (double)k * (double)n / (double)nfft;
x_re += block_time[n] * cos(theta);
x_im -= block_time[n] * sin(theta);
}
Y_re[k] = x_re * H_re[k] - x_im * H_im[k];
Y_im[k] = x_re * H_im[k] + x_im * H_re[k];
}
for (unsigned n = 0; n < nfft; n++) {
double yb = 0.0;
for (unsigned k = 0; k < nfft; k++) {
double theta = 2.0 * M_PI * (double)k * (double)n / (double)nfft;
yb += Y_re[k] * cos(theta) - Y_im[k] * sin(theta);
}
yb /= (double)nfft;
if (start + n < out_len) out[start + n] += yb;
}
}
API:
const double *kernel, unsigned kernel_len,
double *out);
void MD_convolution_fft_ola(const double *signal, unsigned signal_len, const double *kernel, unsigned kernel_len, double *out)
Full linear convolution using FFT overlap-add (offline).
Visuals — response and magnitude spectrum:
Quick example:
Quick comparison
| Method | Output length | Typical cost | Best use |
| Time-domain convolution | \(N+M-1\) | \(O(NM)\) | Teaching and short kernels |
| Moving average | \(N\) | \(O(N)\) (running sum) | First FIR low-pass example |
| General FIR | \(N\) | \(O(NM)\) | Arbitrary FIR taps |
| FFT overlap-add | \(N+M-1\) | \(O(N\log N)\) blocks | Long kernels / fast offline convolution |
Use direct methods to build intuition, then compare with overlap-add to see why FFT-based convolution matters for longer filters.
Further reading
API reference