Denormal Handling

Utilities for flushing denormal (subnormal) floating-point values to zero.

Why Denormals Matter

Denormal numbers are very small floating-point values near zero. Processing them can cause significant CPU slowdowns (10-100x slower) on many processors.

In audio processing, denormals commonly occur:

  • In filter feedback paths as signals decay
  • After gain reduction to very low levels
  • In reverb/delay tail-off

API

flush_denormal_f32

Flush a 32-bit float to zero if denormal:

#![allow(unused)]
fn main() {
use bbx_core::flush_denormal_f32;

let small_value = 1.0e-40_f32;  // Denormal
let result = flush_denormal_f32(small_value);
assert_eq!(result, 0.0);

let normal_value = 0.5_f32;
let result = flush_denormal_f32(normal_value);
assert_eq!(result, 0.5);
}

flush_denormal_f64

Flush a 64-bit float to zero if denormal:

#![allow(unused)]
fn main() {
use bbx_core::flush_denormal_f64;

let small_value = 1.0e-310_f64;  // Denormal
let result = flush_denormal_f64(small_value);
assert_eq!(result, 0.0);
}

Usage Patterns

In Feedback Loops

#![allow(unused)]
fn main() {
use bbx_core::flush_denormal_f32;

fn process_filter(input: f32, state: &mut f32, coefficient: f32) -> f32 {
    let output = input + *state * coefficient;
    *state = flush_denormal_f32(output);  // Prevent denormal accumulation
    output
}
}

Block Processing

#![allow(unused)]
fn main() {
use bbx_core::flush_denormal_f32;

fn process_block(samples: &mut [f32]) {
    for sample in samples {
        *sample = flush_denormal_f32(*sample);
    }
}
}

Performance

The flush functions use bit manipulation, not branching:

#![allow(unused)]
fn main() {
// Conceptually (actual implementation may differ):
fn flush_denormal_f32(x: f32) -> f32 {
    let bits = x.to_bits();
    let exponent = (bits >> 23) & 0xFF;
    if exponent == 0 && (bits & 0x007FFFFF) != 0 {
        0.0  // Denormal
    } else {
        x    // Normal
    }
}
}

This is typically faster than relying on FPU denormal handling.

Hardware FTZ/DAZ Mode

For maximum performance, bbx_core provides the ftz-daz Cargo feature that enables hardware-level denormal handling on x86/x86_64 processors.

Enabling the Feature

[dependencies]
bbx_core = { version = "...", features = ["ftz-daz"] }

enable_ftz_daz

When the feature is enabled, call enable_ftz_daz() once at the start of each audio processing thread:

#![allow(unused)]
fn main() {
use bbx_core::denormal::enable_ftz_daz;

fn audio_thread_init() {
    enable_ftz_daz();
    // All subsequent float operations on this thread will auto-flush denormals
}
}

This sets two CPU flags:

  • FTZ (Flush-To-Zero): Denormal results are flushed to zero
  • DAZ (Denormals-Are-Zero): Denormal inputs are treated as zero

Platform Support

PlatformBehavior
x86/x86_64Full FTZ + DAZ via MXCSR register
AArch64 (ARM64/Apple Silicon)FTZ only via FPCR register
OtherNo-op (use software flush functions)

bbx_plugin Integration

When using bbx_plugin with the ftz-daz feature enabled, enable_ftz_daz() is called automatically during prepare():

[dependencies]
bbx_plugin = { version = "...", features = ["ftz-daz"] }

This is the recommended approach for audio plugins.

Software vs Hardware Approach

ApproachUse Case
flush_denormal_* functionsCross-platform, targeted flushing in specific code paths
enable_ftz_daz()Maximum performance on x86/x86_64, affects all operations

For production audio plugins on desktop platforms, enabling the ftz-daz feature is recommended. The software flush functions remain useful for cross-platform code or when you need fine-grained control over which values are flushed.