Real-time hearing aid DSP on mobile devices demands consistent sub-5ms latency across hardware spanning three orders of magnitude in compute power. Floating-point arithmetic—the default choice in desktop audio—introduces unpredictable performance characteristics that make this impossible. Fixed-point integer math, despite its dated reputation, remains the only viable path to production-grade assistive audio on constrained devices.
The Latency Budget Problem
A 48kHz sample rate yields 20.8μs per sample. A typical prescription hearing aid applies frequency-dependent gain across 8-12 bands, each requiring biquad filtering, compression, and makeup gain. Processing 128-sample buffers (2.67ms) leaves roughly 2ms for the entire DSP chain before perceptual latency becomes objectionable. On an iPhone 11's A13 chip, a naive floating-point implementation averages 3.2ms—acceptable. The same code on a budget Android device with a Snapdragon 439 clocks 8.7ms, causing audible echo between direct sound and processed output.
The variance matters more than the mean. Floating-point denormal handling triggers microcode fallbacks on ARM Cortex-A53 cores, injecting 40-120μs stalls when filter states decay near zero—exactly the scenario in silence detection and adaptive noise gates. Fixed-point arithmetic eliminates denormals entirely; zero is zero in Q15 representation.
Q15 Format: 16-Bit Signed Fractional Math
Q15 represents values in [-1.0, 0.999969] using a 16-bit signed integer where the decimal point sits after the sign bit. The value 0x4000 equals 0.5, 0x7FFF equals 0.999969, and 0x8000 represents -1.0. Multiplication of two Q15 values yields a Q30 result; right-shifting by 15 bits recovers Q15 format with one guard bit for rounding.
A biquad direct-form-I filter in floating-point:
y[n] = b0*x[n] + b1*x[n-1] + b2*x[n-2] - a1*y[n-1] - a2*y[n-2]
Translates to fixed-point with explicit shift operations:
int32_t acc = ((int32_t)b0_q15 * x[n]) >> 15; acc += ((int32_t)b1_q15 * x_1) >> 15; acc += ((int32_t)b2_q15 * x_2) >> 15; acc -= ((int32_t)a1_q15 * y_1) >> 15; acc -= ((int32_t)a2_q15 * y_2) >> 15; y[n] = (int16_t)__SSAT(acc, 16);
The __SSAT intrinsic provides saturating arithmetic—values exceeding ±1.0 clamp rather than wrapping. This prevents catastrophic filter blow-up from coefficient quantization errors, a real concern when converting analog filter designs to discrete implementations.
Coefficient Quantization Strategy
A second-order peaking filter at 2kHz with 12dB gain and Q=1.4 yields floating-point coefficients like b0=1.0847. Direct Q15 conversion would overflow. The solution: scale all coefficients by the largest absolute value, store the scale factor separately, and apply it as a final bit-shift.
For the 2kHz peaking filter, the max coefficient magnitude is 1.0847. Scale by 0.9, yielding b0=0.9762 (Q15: 0x7CFA), b1=-1.7891 (0x8C3A after scaling), and so forth. After filtering, left-shift the output by 1 bit to compensate. This trades 1 bit of dynamic range for numerical stability—acceptable when input signals rarely exceed -6dBFS in practice.
Bilinear Transform Warping in Fixed-Point
Analog filter designs converted via bilinear transform exhibit frequency warping: a 2kHz digital filter doesn't land exactly at 2kHz. Pre-warping the analog frequency by Ω = tan(πf/fs) corrects this, but tan() in fixed-point requires lookup tables or CORDIC approximation. For hearing aid applications spanning 125Hz-8kHz, a 256-entry LUT covers the range with