audio_samples 2.0.0

A typed audio processing library for Rust that treats audio as a first-class, invariant-preserving object rather than an unstructured numeric buffer.
Documentation
#[cfg(feature = "processing")]
use std::time::Duration;

#[cfg(feature = "processing")]
use audio_samples::{
    AudioProcessing, AudioSampleResult, AudioSamples, AudioStatistics, NormalizationConfig,
    sine_wave,
};

#[cfg(not(feature = "processing"))]
fn main() {
    eprintln!("This example requires the 'processing' feature.");
}

#[cfg(feature = "processing")]
#[inline]
pub fn main() -> AudioSampleResult<()> {
    use audio_samples::{AudioEditing, sample_rate};

    let sample_rate_hz = sample_rate!(44100);

    // Start with a clean 440 Hz sine.
    let audio: AudioSamples<'static, f64> =
        sine_wave(440.0, Duration::from_secs(1), sample_rate_hz, 0.8);
    println!("Input:  peak={:.4}  rms={:.4}", audio.peak(), audio.rms());

    // Demonstrate method chaining with the non-mutating variants (each borrows
    // and returns a new owned copy).
    // Simple gain.
    let audio = audio.scale(0.5);
    println!("Scaled: peak={:.4}  rms={:.4}", audio.peak(), audio.rms());

    // Remove DC offset (no-op for a pure sine, but demonstrates API).
    let audio = audio.remove_dc_offset()?;

    // Normalization using NormalizationConfig.
    let audio = audio.normalize(NormalizationConfig::peak(1.0))?;
    println!(
        "Normalized (Peak): peak={:.4}  rms={:.4}",
        audio.peak(),
        audio.rms()
    );
    assert!(
        (audio.peak() - 1.0).abs() < 1e-3,
        "peak-normalize should make |peak| == 1.0, got {}",
        audio.peak()
    );

    // Clip to a tighter range.
    let audio = audio.clip(-0.7, 0.7)?;
    println!("Clipped: peak={:.4}  rms={:.4}", audio.peak(), audio.rms());
    assert!(
        audio.peak() <= 0.7 + 1e-6,
        "after clip(-0.7, 0.7) peak must not exceed 0.7, got {}",
        audio.peak()
    );

    // Windowing (Hann).

    let num_samples = audio.samples_per_channel().get() as usize;
    let window = spectrograms::hanning_window(core::num::NonZeroUsize::new(num_samples).unwrap());
    let audio = audio.apply_window(&window)?;
    println!("Windowed: peak={:.4}  rms={:.4}", audio.peak(), audio.rms());

    // FIR filtering (moving average).
    let taps = vec![1.0 / 9.0; 9];
    let audio = audio.apply_filter(non_empty_slice::NonEmptySlice::from_slice(&taps).unwrap())?;
    println!(
        "Filtered (moving avg): len={}  peak={:.4}  rms={:.4}",
        audio.samples_per_channel(),
        audio.peak(),
        audio.rms()
    );

    // µ-law round-trip - demonstrating chaining (non-mutating, leaves `audio` intact).
    let ulaw = audio.mu_compress(255.0)?.mu_expand(255.0)?;
    println!(
        "µ-law round-trip: peak={:.4}  rms={:.4}",
        ulaw.peak(),
        ulaw.rms()
    );

    // Simple IIR-ish (internal) filters based on cutoff frequency.
    let lp = sine_wave::<f64>(440.0, Duration::from_secs(1), sample_rate_hz, 0.6);
    // Add a higher tone so the low-pass effect is visible in stats.
    let hi = sine_wave::<f64>(5_000.0, Duration::from_secs(1), sample_rate_hz, 0.2);
    let mixed = AudioSamples::mix(
        non_empty_slice::NonEmptySlice::from_slice(&[lp, hi]).unwrap(),
        None,
    )?;

    println!("Two-tone: peak={:.4}  rms={:.4}", mixed.peak(), mixed.rms());
    let lp = mixed.low_pass_filter(1_000.0)?;
    println!("Low-pass 1kHz: peak={:.4}  rms={:.4}", lp.peak(), lp.rms());
    // The 5 kHz component sits well above the 1 kHz cutoff, so the low-pass
    // must remove energy: filtered RMS should be below the two-tone RMS.
    assert!(
        lp.rms() < mixed.rms(),
        "low-pass should attenuate the two-tone signal: {} !< {}",
        lp.rms(),
        mixed.rms()
    );

    // µ-law round-trip should reconstruct close to the original.
    assert!(
        (ulaw.peak() - audio.peak()).abs() < 0.1,
        "µ-law round-trip peak drifted too far: {} vs {}",
        ulaw.peak(),
        audio.peak()
    );

    Ok(())
}