DcBlockerBlock

DC offset removal using a first-order high-pass filter.

Overview

DcBlockerBlock removes DC offset (constant voltage) from audio signals, preventing speaker damage and headroom loss. It uses a very low-frequency high-pass filter that passes all audible content while removing the DC component.

Mathematical Foundation

What is DC Offset?

DC offset occurs when a signal's average value is not zero. In audio, signals should oscillate symmetrically around zero:

Centered signal:        Signal with DC offset:
      +                       +
   __/ \__                 __/ \__
--/------\--           --/------\-- ← shifted up
  \_    _/                \_    _/
     --

Why Remove DC?

A constant (DC) component wastes headroom and causes problems:

  1. Reduced headroom: Asymmetric clipping occurs sooner
  2. Speaker damage: DC current heats voice coils
  3. Incorrect metering: Peak meters show false values
  4. Downstream effects: Some effects (like compressors) behave poorly

The High-Pass Filter

A DC blocker is simply a high-pass filter with a very low cutoff frequency (~5 Hz). This removes the DC component (0 Hz) while passing all audible frequencies (20 Hz and above).

First-Order High-Pass

The difference equation for a first-order high-pass filter:

$$ y[n] = x[n] - x[n-1] + R \cdot y[n-1] $$

where:

  • $x[n]$ is the current input sample
  • $y[n]$ is the current output sample
  • $R$ is the filter coefficient (typically ~0.995)

Filter Coefficient

The coefficient $R$ determines the cutoff frequency:

$$ R = 1 - \frac{2\pi f_c}{f_s} $$

where $f_c$ is the cutoff frequency (5 Hz) and $f_s$ is the sample rate.

For 5 Hz cutoff at 44.1 kHz: $$ R = 1 - \frac{2\pi \times 5}{44100} \approx 0.9993 $$

The coefficient is clamped to [0.9, 0.9999] for stability.

Transfer Function

The Z-domain transfer function:

$$ H(z) = \frac{1 - z^{-1}}{1 - R \cdot z^{-1}} $$

Pole-Zero Analysis

  • Zero at $z = 1$ (DC is completely blocked)
  • Pole at $z = R$ (just inside the unit circle)

The zero at $z = 1$ guarantees complete DC rejection. The pole close to $z = 1$ creates a gentle rolloff.

Frequency Response

The magnitude response:

$$ |H(f)| = \frac{\sqrt{2(1 - \cos(2\pi f / f_s))}}{\sqrt{1 + R^2 - 2R\cos(2\pi f / f_s)}} $$

Key points:

  • At DC ($f = 0$): $|H| = 0$ (complete rejection)
  • At cutoff ($f = f_c$): $|H| \approx 0.707$ (-3 dB)
  • At 20 Hz: $|H| > 0.99$ (minimal attenuation)

Creating a DC Blocker

#![allow(unused)]
fn main() {
use bbx_dsp::{blocks::DcBlockerBlock, graph::GraphBuilder};

let mut builder = GraphBuilder::<f32>::new(44100.0, 512, 2);

let dc = builder.add(DcBlockerBlock::new(true));
}

Or with direct construction:

#![allow(unused)]
fn main() {
use bbx_dsp::blocks::DcBlockerBlock;

// Enabled DC blocker
let dc = DcBlockerBlock::<f32>::new(true);

// Disabled (bypass)
let dc = DcBlockerBlock::<f32>::new(false);
}

Port Layout

PortDirectionDescription
0InputAudio with DC
0OutputDC-free audio

Parameters

ParameterTypeDefaultDescription
enabledbooltrueEnable/disable filtering

Usage Examples

After Distortion

Asymmetric distortion often introduces DC offset:

#![allow(unused)]
fn main() {
use bbx_dsp::{blocks::{DcBlockerBlock, OscillatorBlock, OverdriveBlock}, graph::GraphBuilder, waveform::Waveform};

let mut builder = GraphBuilder::<f32>::new(44100.0, 512, 2);

let osc = builder.add(OscillatorBlock::new(440.0, Waveform::Sine, None));
let drive = builder.add(OverdriveBlock::new(5.0, 0.7, 0.5, 44100.0));
let dc = builder.add(DcBlockerBlock::new(true));

builder.connect(osc, 0, drive, 0);
builder.connect(drive, 0, dc, 0);
}

On External Input

Remove DC from external audio sources:

#![allow(unused)]
fn main() {
use bbx_dsp::{blocks::{DcBlockerBlock, FileInputBlock}, graph::GraphBuilder};

let mut builder = GraphBuilder::<f32>::new(44100.0, 512, 2);

let input = builder.add(FileInputBlock::new(Box::new(reader)));
let dc = builder.add(DcBlockerBlock::new(true));

builder.connect(input, 0, dc, 0);
}

Output Stage

Standard practice to include DC blocking on the master output:

#![allow(unused)]
fn main() {
use bbx_dsp::{blocks::{DcBlockerBlock, GainBlock, OscillatorBlock, OutputBlock}, graph::GraphBuilder, waveform::Waveform};

let mut builder = GraphBuilder::<f32>::new(44100.0, 512, 2);

let osc = builder.add(OscillatorBlock::new(440.0, Waveform::Saw, None));
let gain = builder.add(GainBlock::new(-12.0, None));
let dc = builder.add(DcBlockerBlock::new(true));
let output = builder.add(OutputBlock::new(2));

builder.connect(osc, 0, gain, 0);
builder.connect(gain, 0, dc, 0);
builder.connect(dc, 0, output, 0);
}

When to Use

Do use after:

  • Distortion/saturation effects
  • Asymmetric waveshaping
  • Sample rate conversion
  • External audio input
  • Mixing multiple sources

Don't use:

  • In series (one is enough)
  • When sub-bass content is critical (though 5 Hz is inaudible)
  • On already DC-free signals (adds unnecessary processing)

Implementation Notes

  • First-order high-pass filter (~5 Hz cutoff)
  • Coefficient recalculated when sample rate changes
  • Denormals flushed to prevent CPU slowdown
  • Multi-channel with independent filter states
  • Can be disabled for bypass (passthrough mode)
  • Resettable filter state via reset() method

Further Reading

  • Smith, J.O. (2007). Introduction to Digital Filters, Chapter 1. online
  • Orfanidis, S. (2010). Introduction to Signal Processing, Chapter 4. Rutgers.
  • Pirkle, W. (2019). Designing Audio Effect Plugins in C++, Chapter 6: Filters. Focal Press.