Lab 4 — The fade that never reaches silence

A one-pole smoother — the workhorse behind every envelope follower and gain ramp — ported with / 32 turned into >> 5, like every textbook says. No sample is ever wrong by more than a hair, which is exactly why this bug family survives code review. The damage is statistical: a standing DC bias, and a fade-out that parks below zero forever. 15–20 minutes.

What you'll need

The same toolchain as Lab 1 — VS Code, the C/C++ extension, a g++ on your PATH, and (optionally) the Mantissa Bridge extension for live variable binding. First lab? Do the Lab 1 setup once; everything here reuses it.

Get the lab project

Two builds (float reference + buggy fixed-point port), VS Code config pre-wired so F5 just works. Grab the zip and extract it anywhere you like:

Inside the Mantissa app this download step is skipped — the lab ships bundled with the install and the IDE button opens it directly.

Open the project

Point VS Code at the folder you extracted the zip into. Inside the Mantissa app the lab is bundled, so the IDE button just works without a path.

1. Run both builds

  1. Build and run the float target (F5). It writes out_float.wav — a quiet 200 Hz tone (−34 dBFS, on purpose) fading to silence over two seconds.
  2. Build and run the fixed target for out_fixed.wav.
  3. These two sound identical. That's the point — your ears won't find this one. The tools will.

2. See it in Wave Compare

  1. Load both files, press Compare, and look at the error trace: instead of hugging zero it sits on a flat offset of about 16 LSB — a bias, not noise. On a −34 dBFS signal that's several percent of the peak.
  2. Zoom into the tail, after the tone has faded. The float build decays to a true zero. The fixed build parks at −16 LSB and stays there — a permanent DC offset on "silence".
  3. The spectrogram shows the giveaway: a line at 0 Hz that the float build doesn't have.

3. Watch the bias build in the Debugger

  1. Set a breakpoint inside SmootherFixed::process in fixed/smoother_fx.h and its float twin in float/smoother.h; F5 both builds.
  2. Bind diffdiff_fx and yy_fx (Q15 — Auto-Q agrees).
  3. Step a few samples: every individual value looks plausibly close. Now let both builds free-run a few thousand samples and look at the y pair's error — it has settled onto a constant offset. No single step was wrong; every step was wrong by a little, in the same direction.
  4. At the very end, read y_fx in the Locals panel: stuck below zero while the float side reads 0.0.

4. Fix it and prove the fix

The repair is one line, documented next to the bug in fixed/smoother_fx.h: round to nearest before the shift — (diff + 16) >> 5. Rebuild and Compare again: the standing bias drops to about half an LSB and the tail decays to a true zero.

What this lab teaches

Next

That's the tour: Lab 1 wraps, Lab 3 saturates, Lab 4 drifts — the three ways fixed-point ports actually go wrong, and the Mantissa workflow that catches each one. Head back to all tutorials, or revisit Lab 2 to go deeper on the analysis side.

Finished this step?

Mark it complete to track your progress through the tutorial.


Re-download lab zip · ← All tutorials