VIDEO INTRODUCTION
INTRODUCTION TO ADCs
An Analog-to-Digital Converter (ADC) is the bridge between the continuous physical world and the discrete digital world. A microphone, a temperature sensor, a radio receiver — all produce analog values that vary smoothly over time. A processor or digital logic can only work with numbers. The ADC samples that analog signal at regular intervals and assigns each sample a binary code, turning sound, light of all frequencies, and temperature into bits.
Two properties define what an ADC can represent:
- Sample rate — how many samples per second (Hz). By the Nyquist–Shannon sampling theorem, a signal must be sampled at least twice the average bandwidth to be re-constructed. (E. Candes, M. Wakin , " An Introduction To Compressive Sampling", 2008 https://doi.org/10.1109/MSP.2007.914731)
- Resolution (bits) — how many distinct levels the output can take. An N-bit ADC has 2N levels; each extra bit halves the quantization step and reduces quantization noise by ~6 dB.
Quantization noise is the rounding error introduced every time a continuous value is mapped
to the nearest discrete level. For a full-scale sine wave in an ideal N-bit ADC the
signal-to-noise ratio is approximately 6.02·N + 1.76 dB.
The animated visualiser above lets you watch this tradeoff live: drag the
Bits slider and watch the staircase steps (and the noise strip) change.
NYQUIST ADC vs SIGMA–DELTA
There are two fundamentally different strategies for digitising a signal. The visualiser implements both so you can compare them side by side.
Nyquist-rate ADC
A Nyquist ADC samples the input at one sample per interval. Every sample uses all N bits of resolution. This is what a classic successive-approximation (SAR) or pipeline ADC does. Achieving high resolution requires precise analog components (matched capacitors, accurate references), which become harder to build as process nodes shrink.
Sigma–Delta (ΣΔ) ADC
A sigma–delta ADC takes the opposite approach: sample much faster than strictly necessary (oversampling), but use a very coarse quantizer — even just 1 bit. A feedback loop called the modulator shapes the quantization noise so that it is pushed out of the signal band, into high frequencies where a digital decimation filter can remove it. The result can achieve very high effective resolution using only simple, well-matched digital circuits. See Oversampling and Sigma-Delta ADCs for more information.
The tradeoff: ΣΔ ADCs trade speed for resolution. They are ideal for audio (24-bit at 192 kHz), sensor readout, and instrumentation — but not for wideband RF receivers (base-stations) that need simultaneous high speed and high resolution.
1st-order vs 2nd-order modulator
The order of the sigma–delta modulator sets how aggressively noise is shaped:
| Order | Noise shaping | In-band noise reduction per octave of OSR | Typical use |
|---|---|---|---|
| 1st | (1 − z⁻¹) | 9 dB | Simple, always stable |
| 2nd | (1 − z⁻¹)² | 15 dB | Excellent SNR, needs careful design |
| 3rd (LF) | (1 − z⁻¹)³ + spread zeros | 21 dB | Leapfrog ladder, tuned coefficients |
The decimation filter is a sincM cascade (M = modulator order + 1), matched to the noise shaping to maximise stop-band rejection. A higher oversampling ratio K also helps: each doubling of K reduces in-band noise by an extra 3 dB beyond the shaping benefit.
Leapfrog and control-bounded modes
The LF 3rd button selects a 3rd-order single-loop leapfrog modulator: a chain of three integrators with a resonator cross-coupling that moves a pair of noise-transfer-function zeros off DC into the signal band, flattening the in-band noise floor. It is reconstructed by the usual sinc4 decimation filter.
The LF CB button selects the control-bounded leapfrog: instead of one quantizer in a loop, every integrator carries its own local 1-bit control — which is what guarantees the loop stays bounded — and the input is recovered by a digital estimation filter whose taps come from forward/backward Riccati equations. There is no sinc decimator; the only latency is a half-sample group delay, because each 1-bit control decision acts over the following control period. It needs heavy oversampling, so selecting it automatically lowers the signal frequency. The digital panel draws the three control bit-streams (s₁, s₂, s₃) as thin staircases near the rails — they are the converter's actual digital output — with the white staircase being the estimate computed from all of them.
STATE-SPACE FORMULATION
The maths below follows the unified continuous-time (CT) ADC analysis of Feyling, Malmberg, Wulff & Ytterdal, “A Unified Analysis of Continuous-Time A/D Converters” (TechRxiv, 2024). In that framework every conventional CT ADC is a loop filter \(G(s)\) feeding a back-end quantizer \(Q\) — modelled as a sampler at rate \(f_s\) with additive quantization noise \(q[n]\) — whose output is fed back through one or more DACs. With \(\tilde{H}(z)\) the transfer function from the DAC input to the quantizer output, the noise- and signal-transfer functions are
\[ \mathrm{NTF}(e^{j\omega T}) = \frac{1}{1 - \tilde{H}(e^{j\omega T})}, \qquad \mathrm{STF}(j\omega) = \frac{G(j\omega)}{1 - \tilde{H}(e^{j\omega T})}. \]The \(\mathrm{STF}\) supplies the inherent anti-alias filtering of a CT ADC: for an integrator \(G(s) = 1/s\) it is a sinc with nulls at multiples of \(f_s\). The Leapfrog architecture of the paper builds \(G(s)\) from a chain of \(N\) integrators (forward gain \(\beta\), backward gain \(\alpha\)), each stabilised by a local single-bit digital feedback loop of gain \(\kappa\) — a control-bounded state-space system \(\dot{\mathbf{x}}(t) = \mathbf{A}\,\mathbf{x}(t) + \mathbf{B}\,u(t) + \boldsymbol{\Gamma}\,\mathbf{s}(t)\).
This tool implements the discrete-time, low-order relatives of that picture. Writing the state vector \(\mathbf{x}[n]\), input \(u[n]\) and fed-back quantized output \(y[n-1]\), each modulator advances as
\[ \mathbf{x}[n] = \mathbf{A}\,\mathbf{x}[n-1] + \mathbf{B}\,u[n] + \boldsymbol{\Gamma}\,y[n-1], \qquad y[n] = Q\!\left(\mathbf{c}^{\!\top}\mathbf{x}[n]\right), \]where \(Q\) is a mid-rise quantizer with \(2^{\text{bits}}\) levels spread over \([-v_\text{ref}, v_\text{ref}]\) (a 1-bit quantizer returns only \(\pm v_\text{ref}\)).
Nyquist ADC
No loop filter and no analog state: \(\tilde{H}(z) = 0\), so \(\mathrm{NTF} = 1\) (the quantization noise is white and unshaped) and the converter is memoryless — \(\mathbf{A}\), \(\mathbf{B}\), \(\boldsymbol{\Gamma}\) vanish:
\[ y[n] = Q\big(u[n]\big). \]1st-order ΣΔ
A single delaying integrator accumulates the error between the input and the fed-back output. The state \(x\) is scalar, with \(\mathbf{A}=[\,1\,]\), \(\mathbf{B}=[\,1\,]\), \(\boldsymbol{\Gamma}=[-1]\), \(\mathbf{c}=[\,1\,]\):
\[ x[n] = x[n-1] + u[n] - y[n-1], \qquad y[n] = Q\big(x[n]\big). \]The loop transfer function \(\tilde{H}(z) = z^{-1}\) gives, via the relation above, a first-order shaping \(\mathrm{NTF}(z) = 1 - z^{-1}\).
2nd-order ΣΔ (CIFB)
The standard cascade-of-integrators feedback (CIFB) structure with two integrators and feedback coefficients \(a_1 = 1,\ a_2 = 2\) (and \(b_1 = 1,\ g_1 = 1\)). With the state \(\mathbf{x} = [\,x_1,\ x_2\,]^{\top}\), substituting the first integrator into the second gives the closed-form update:
\[ \begin{bmatrix} x_1[n] \\ x_2[n] \end{bmatrix} = \begin{bmatrix} 1 & 0 \\ 1 & 1 \end{bmatrix} \begin{bmatrix} x_1[n-1] \\ x_2[n-1] \end{bmatrix} + \begin{bmatrix} 1 \\ 1 \end{bmatrix} u[n] + \begin{bmatrix} -1 \\ -3 \end{bmatrix} y[n-1], \qquad y[n] = Q\!\left(\mathbf{c}^{\!\top}\mathbf{x}[n]\right),\ \ \mathbf{c}^{\!\top} = [\,0\ \ 1\,]. \]The unit sub-diagonal of \(\mathbf{A}\) is the chain feed-forward (\(x_1\) into \(x_2\)) — the discrete two-stage analogue of the Leapfrog integrator chain; the feedback column \(\boldsymbol{\Gamma} = [-1,\ -3]^{\top}\) collects \(-a_1\) at the first integrator and \(-(a_1 + a_2)\) at the second. This realises the second-order shaping \(\mathrm{NTF}(z) = (1 - z^{-1})^2\), which pushes far more quantization noise out of band than the first-order loop.
3rd-order leapfrog (single loop)
The LF 3rd mode chains three delaying integrators with the quantized output fed back to every stage (gains \(a_1 = 0.2,\ a_2 = 0.4,\ a_3 = 1.2\)), plus a resonator cross-coupling \(g = 0.004\) that feeds \(x_3\) back into \(x_2\):
\[ \begin{aligned} x_1[n] &= x_1[n-1] + b_1 u[n] - a_1\,y[n-1] \\ x_2[n] &= x_2[n-1] + x_1[n] - a_2\,y[n-1] - g\,x_3[n-1] \\ x_3[n] &= x_3[n-1] + x_2[n] - a_3\,y[n-1], \qquad y[n] = Q\big(x_3[n]\big). \end{aligned} \]Substituting each stage into the next gives the closed-form state-space matrices
\[ \mathbf{A} = \begin{bmatrix} 1 & 0 & 0 \\ 1 & 1 & -g \\ 1 & 1 & 1-g \end{bmatrix}, \quad \mathbf{B} = b_1\!\begin{bmatrix} 1 \\ 1 \\ 1 \end{bmatrix}, \quad \boldsymbol{\Gamma} = \begin{bmatrix} -a_1 \\ -(a_1{+}a_2) \\ -(a_1{+}a_2{+}a_3) \end{bmatrix} = \begin{bmatrix} -0.2 \\ -0.6 \\ -1.8 \end{bmatrix}, \quad \mathbf{c}^{\!\top} = [\,0\ \ 0\ \ 1\,]. \]The resonator moves a pair of NTF zeros off DC to \(\omega \approx \pm\sqrt{g} \approx \pm 0.06\) rad/sample, spreading the in-band notch the way an Nth-order leapfrog ladder does. The input scaling \(b_1 = a_1/2 = 0.1\) keeps the loop stable to a full-scale input; the resulting signal-transfer gain of \(\tfrac{1}{2}\) is measured by the lock-in \(|\mathrm{STF}|\) and divided out by the reconstruction filter.
Control-bounded leapfrog
The LF CB mode abandons the single quantizer entirely. The leapfrog ladder (forward gain \(\beta = 0.4\), backward coupling \(\alpha = 0.004\)) runs with a local 1-bit control on every integrator:
\[ \mathbf{x}[n{+}1] = \mathbf{A}_d\,\mathbf{x}[n] + \mathbf{B}_d\,u[n] + \boldsymbol{\Gamma}\,\mathbf{s}[n], \qquad s_\ell[n] = \mathrm{sign}\big(x_\ell[n]\big), \] \[ \mathbf{A}_d = \begin{bmatrix} 1 & -\alpha & 0 \\ \beta & 1 & -\alpha \\ 0 & \beta & 1 \end{bmatrix}, \qquad \mathbf{B}_d = \begin{bmatrix} \beta \\ 0 \\ 0 \end{bmatrix}, \qquad \boldsymbol{\Gamma} = -\kappa\,\mathbf{I},\ \ \kappa = 0.8 . \]Because every integrator is individually steered back toward zero, the states are bounded by construction — no overload, whatever the input. The price is that no single bit stream is the output: the input must be estimated from all three control sequences. The estimate is a non-causal FIR over a window of \(\pm K\) control samples,
\[ \hat{u}[n] = \sum_{j=0}^{K-1} \mathbf{h}_f[j]^{\!\top}\,\mathbf{s}[n{-}1{-}j] + \sum_{k=0}^{K-1} \mathbf{h}_b[k]^{\!\top}\,\mathbf{s}[n{+}k], \]whose taps come from the steady-state Wiener (smoothing) solution: two algebraic Riccati equations
\[ \mathbf{A}\mathbf{V}_f + \mathbf{V}_f\mathbf{A}^{\!\top} - \tfrac{1}{\eta^2}\mathbf{V}_f\mathbf{V}_f + \mathbf{B}\mathbf{B}^{\!\top} = 0, \qquad -\mathbf{A}\mathbf{V}_b - \mathbf{V}_b\mathbf{A}^{\!\top} - \tfrac{1}{\eta^2}\mathbf{V}_b\mathbf{V}_b + \mathbf{B}\mathbf{B}^{\!\top} = 0, \]give the forward/backward closed-loop matrices \(\mathbf{A}_f = e^{(\mathbf{A} - \mathbf{V}_f/\eta^2)T}\), \(\mathbf{A}_b = e^{-(\mathbf{A} + \mathbf{V}_b/\eta^2)T}\) and \(\mathbf{W} = (\mathbf{V}_f + \mathbf{V}_b)^{-1}\mathbf{B}\), from which \(\mathbf{h}_f[j] = -\mathbf{B}_f^{\!\top}(\mathbf{A}_f^{\!\top})^{j}\,\mathbf{W}\) and \(\mathbf{h}_b[k] = -\mathbf{B}_b^{\!\top}(\mathbf{A}_b^{\!\top})^{k}\,\mathbf{W}\) (this page solves the Riccati equations live, by Kleinman–Newton iteration). The bandwidth parameter \(\eta^2 = |G(e^{j\omega_B})|^2\) sets the estimator band edge; the resulting signal transfer
\[ \mathrm{STF}(\omega) = \frac{|G(\omega)|^2}{|G(\omega)|^2 + \eta^2} \]is real — a zero-phase smoother. The only latency left is half a control period, \(T/2\), because each decision \(s_\ell[n]\) acts over the following hold interval \([nT, (n{+}1)T)\); the visualiser compensates exactly this delay.
HOW THE CODE WORKS
cicadc began as a Python/Manim desktop app for teaching ADC concepts in
lectures. The core simulation lives in src/cicadc/; the browser visualiser in
docs/index.html is a pure HTML5 + JavaScript reimplementation that runs
entirely in the browser with no server.
Signal chain
The simulation mirrors the real analog signal chain shown at the top of the page:
- Analog signal — a sinusoid (
cleanSig) plus optional value-noise (noiseAt), both evaluated analytically so they never flicker between frames. - ADC — either a mid-tread Nyquist quantizer (
nyQ) or a 1st/2nd-order sigma–delta modulator (sd1/sd2). The SD modulators are causal recursive filters with a per-index cache and a 128-sample warm-up, so any frame can jump to any time without recomputing from scratch. - Decimation filter — a sincM FIR built by convolving M
boxcar filters of length K (
getFIR). Applied viafiltOut(k), which evaluates the sliding dot product at every sample index, not just at multiples of K — this gives a smooth continuous staircase rather than chunky decimated blocks. - Gain correction —
filtGain()computes the sincM frequency response at the signal frequency, multiplied by the modulator's signal transfer function magnitude|STF(e^jω)|measured by a Hann-windowed DFT lock-in (modSTFmag()). Dividing by this gain makes the reconstructed amplitude exactly match the analog input regardless of frequency or bit depth. - Group delay compensation —
groupDelay()computes the total latency of the filter path (linear phase delay of the sincM plus the phase delay of the STF). The filtered staircase is time-shifted by this amount so it aligns with the analog signal visually.
Rendering
Everything is drawn each frame onto a single <canvas> element scaled by
devicePixelRatio for sharp rendering on high-DPI screens. Four panels share
the canvas:
- LP (left) — analog signal as a smooth curve with sample dots and the three cars: blue (analog), green ghost (raw ADC), gray (filtered output at its group-delay position).
- RP (right) — digital signal: the coarse ADC/modulator staircase in pale green, plus the filtered reconstruction in white, both delay-compensated.
- NP (bottom-left) — quantization noise strip:
cleanSig(t − D) − filtOut(k), normalised to ±1 LSB, showing the residual error after the decimation filter. - FP (bottom-right) — 512-point Hann-windowed FFT of the digital output on a log-frequency axis, updated every new sample.
The layout is fully responsive: on screens narrower than 700 px the four panels stack
vertically and all fonts and line widths scale with a pxf factor derived from
the panel width.
HISTORY
The project started on 5 June 2026 with a Python/Manim desktop application designed to animate the ADC conversion process for classroom use — inspired by an idea from Domen Visnar. Within a day, a 1st-order sigma–delta modulator and slider controls were added, followed by 2nd-order ΣΔ with sincN decimation, a quantization noise strip, and an FFT display (v0.3.0).
On 6 June 2026 the app gained cross-platform binary builds (macOS, Linux, Windows via PyInstaller), a drag-to-Applications macOS DMG, and MP4 recording — making it easy to distribute and use in lecture recordings.
Also on 6–7 June 2026 a native Swift iOS app was added so the visualiser could run on an iPad or iPhone without any desktop software. The iOS version reproduces the full signal chain — ΣΔ modulators, decimation filters, FFT — in Swift, and introduced the pixel-art car sprites that now animate along the signal curve on all platforms.
On 8 June 2026 the browser version (docs/index.html)
was created — a single self-contained HTML file that runs in any modern browser with no
installation. It brought responsive mobile layout, high-DPI canvas rendering, and a series
of accuracy improvements: the STF lock-in gain correction, proper group-delay compensation,
and a post-filter noise strip that correctly shows the residual after reconstruction.
The browser version is what you see at the top of this page.