DH_AllpassFilter: Design Guide and Practical Applications

Implementing DH_AllpassFilter in DSP: Code Examples and PerformanceAn all-pass filter passes all input frequencies with equal gain while altering the phase response. A delay–half (DH) all-pass filter (DH_AllpassFilter) is a common building block in digital signal processing (DSP) used for phase correction, artificial reverberation, phase-based interpolation, and fractional delay implementations. This article explains the theory, presents practical C++ and Python implementations, shows how to use the filter in common DSP tasks, and analyzes performance implications and optimization strategies.


What is a DH All-pass Filter?

An all-pass filter has a transfer function H(z) satisfying |H(e^{jω})| = 1 for all ω. The DH structure usually refers to a particular implementation topology where a unit delay and feedforward/feedback coefficients are arranged to achieve the all-pass property while providing an adjustable phase shift and fractional delay.

A common first-order all-pass filter in z-domain: H(z) = (a + z^{-1}) / (1 + a z^{-1}), where |a| < 1 ensures stability.

The DH_AllpassFilter extends this idea to provide a delay-plus-allpass behaviour, often used to approximate fractional delays: H(z) = z^{-N} * (b0 + b1 z^{-1} + … ) / (1 + a1 z^{-1} + …)

For practical DSP, two key properties are exploited:

  • Unity magnitude: avoids changing spectral magnitude while reshaping phase.
  • Adjustable group delay: useful for aligning channels, implementing fractional delays, and creating dense echo networks (reverb).

Design Variants and Use Cases

  • First-order DH all-pass: simple, low-cost phase shifter (a single coefficient).
  • Higher-order cascade of first-order sections: achieves larger and more flexible phase responses while maintaining numerical stability.
  • Lattice or Schur-form implementations: provide robust numerical stability for higher-order sections.
  • Fractional delay approximations: combining an integer delay with an all-pass section to approximate sub-sample delays.
  • Reverb/diffusion networks: many DH all-pass filters in series produce dense phase scattering.

Typical use cases:

  • Stereo phase alignment and channel matching.
  • Fractional delay for sample-rate conversion or delay-line tuning.
  • Building blocks in feedback delay networks (FDNs) for artificial reverbs.
  • Phase-correcting filters in measurement systems.

Basic Theory (Concise)

First-order all-pass (canonical): H(z) = (a + z^{-1}) / (1 + a z^{-1})

Group delay τ(ω) = -d arg(H(e^{jω}))/dω. By selecting a appropriately, the filter produces desired phase slope around frequency bands.

Cascading N first-order sections multiplies their phase responses, allowing more complex delay/phase shaping while preserving unity magnitude.


Reference Implementations

Below are practical implementations: a straightforward, readable C++ real-time-friendly implementation, and a Python reference for experimentation. Both implement a cascade of first-order all-pass sections with optional integer delay (DH behavior: integer delay + all-pass section(s)).

C++ (real-time, single-sample process)

  • Single-precision floating point (float) for speed.
  • Circular buffer for integer delay.
  • Per-section state (past sample).
  • No dynamic allocations in process path.
// DH_AllpassFilter.h #pragma once #include <vector> #include <cmath> class DH_AllpassFilter { public:     // aCoeffs: vector of "a" coefficients for each first-order all-pass section     // delaySamples: integer delay in samples (>= 0)     DH_AllpassFilter(const std::vector<float>& aCoeffs, int delaySamples = 0);     // Process one sample (mono)     float processSample(float input);     // Reset internal state     void reset(); private:     std::vector<float> a;        // all-pass coefficients     std::vector<float> state;    // per-section previous output (y[n-1])     std::vector<float> delayBuf; // circular buffer for integer delay     int delayWriteIdx;     int delaySize; }; 
// DH_AllpassFilter.cpp #include "DH_AllpassFilter.h" DH_AllpassFilter::DH_AllpassFilter(const std::vector<float>& aCoeffs, int delaySamples) : a(aCoeffs), state(aCoeffs.size(), 0.0f),   delaySize(std::max(0, delaySamples)),   delayBuf(delaySize > 0 ? std::vector<float>(delaySize, 0.0f) : std::vector<float>()),   delayWriteIdx(0) {} void DH_AllpassFilter::reset() {     std::fill(state.begin(), state.end(), 0.0f);     if (delaySize > 0) std::fill(delayBuf.begin(), delayBuf.end(), 0.0f);     delayWriteIdx = 0; } // Single first-order all-pass section processing (in-place) static inline float allpass1_process(float x, float a, float& s) {     // y[n] = -a*x + s     // s gets updated to x + a*y[n]     float y = -a * x + s;     s = x + a * y;     return y; } float DH_AllpassFilter::processSample(float input) {     float x = input;     // Optional integer delay (DH part)     if (delaySize > 0) {         float delayed = delayBuf[delayWriteIdx];         delayBuf[delayWriteIdx] = x;         delayWriteIdx = (delayWriteIdx + 1) % delaySize;         x = delayed;     }     // Cascade of first-order all-pass sections     for (size_t i = 0; i < a.size(); ++i) {         x = allpass1_process(x, a[i], state[i]);     }     return x; } 

Notes:

  • The allpass1_process uses the direct form that keeps only one state variable per section and is efficient and numerically stable for |a| < 1.
  • For stereo, instantiate two independent instances or process per-channel state.

Python (reference, not real-time)

This implementation is convenient for testing, plotting phase/group-delay, and exploring coefficient effects.

”`python

dh_allpass.py

import numpy as np

class DH_AllpassFilter:

def __init__(self, a_coeffs, delay_samples=0):     self.a = np.array(a_coeffs, dtype=float)     self.states = np.zeros_like(self.a)     self.delay_samples = int(max(0, delay_samples))     if self.delay_samples > 0:         self.delay_buf = np.zeros(self.delay_samples)         self.write_idx = 0 def reset(self):     self.states.fill(0.0)     if self.delay_samples > 0:         self.delay_buf.fill(0.0)         self.write_idx = 0 def process_sample(self, x):     # integer delay     if self.delay_samples > 0:         delayed = self.delay_buf[self.write_idx]         self.delay_buf[self.write_idx] = x         self.write_idx = (self.write_idx + 1) % self.delay_samples         x = delayed     # cascade of first-order all-pass sections     for i, a in enumerate(self.a):         y = -a * x + self.states[i]         self.states[i] = x + a * y         x = y 

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *