NOISE SHAPING

← all examples · interactive version of ex/sd_1st.py · data converters
u[n]
Σ
INTEGRATOR x[n]
QUANTISE 1b
y[n]
FEEDBACK
MODULATOR
One integrator, one quantizer, one feedback path. Y = U + E·(1 − z-1): the signal passes, the quantization error gets differentiated.
SIGNAL
VIEW
SNR SHAPED
SNR PLAIN
ENOB
|x| MAX
OUTPUT
Input u[n]
Plain quantiser
Noise-shaped y_sd
|NTF| = |1 − z-1|
Band edge

WHAT YOU ARE LOOKING AT

Noise-shaping modulators have a reputation for being mysterious. They are six lines of Python. For each input sample, work out the quantizer input x — the previous quantizer input plus the difference between the current input and the previous output — then quantize it, optionally with a bit of dither added.

Write the loop out in the z-domain with the quantization error called E and the algebra collapses to Y = U + E·(1 − z-1). The signal transfer function is 1: the input passes straight through. The noise transfer function is a differentiator: it is zero at DC and rises at 20 dB/decade, so the error gets pushed out of the low frequencies and piled up near fs/2. If you only care about a small band near DC, the error you kept is much smaller than the error a plain quantizer would have left there.

The top row is the loop: input and modulator output, then the integrator state, then the output decoded back — a sinc2 average of the same coarse samples, which lands on the input again. The bottom row is why it works: the two spectra, the shaping law, and what it buys per octave of oversampling.

One caveat about the word “one-bit”. A quantizer that rounds to a grid but never saturates (np.round(x*2**bits)/2**bits, which is what ex/sd_1st.py did before it was fixed) emits seven levels at bits = 1 rather than two, and its output is not a bitstream at all. This page and the script both use a proper B-bit quantizer whose 2B levels reach ±1 — at B = 1 that is sign(x), a genuine comparator. The output readout counts the levels that actually came out, and ticking use the unclamped quantiser reproduces the old behaviour.

THINGS WORTH TRYING

THE PYTHON

dither = 1
M = len(u)
y_sd = np.zeros(M)
x = np.zeros(M)
for n in range(1,M):
    x[n] = x[n-1] + (u[n]-y_sd[n-1])
    y_sd[n] = np.round(x[n]*2**bits + dither*np.random.randn()/4)/2**bits

Source: ex/sd_1st.py. The script trims the first two samples to skip the settling; this page keeps them, because the Hann window is close to zero there anyway.