audio-opus-bsd 0.1.0

Opus codec (RFC 6716) encoder/decoder for the sonicbrew audio-toolkit — libopus binding, worker-thread encode/decode emitting planar audio-core-bsd AudioFrames
//! RT-safe Opus decode example.
//!
//! Encodes a short mono sine clip into Opus packets, then decodes them through
//! [`OpusDecodeWorker`] — the worker-thread / RT-safety boundary. The worker
//! owns the libopus decoder; the "real-time" loop on the main thread only
//! performs wait-free `rtrb::Consumer::pop` to receive planar `AudioFrame`s.
//!
//! Run with:
//! ```sh
//! PKG_CONFIG_PATH=$HOME/.local/lib/pkgconfig \
//! LD_LIBRARY_PATH=$HOME/.local/lib \
//! cargo run --example decode_worker
//! ```

use std::thread;
use std::time::Duration;

use audio_opus_bsd::{AudioEncoder, OpusDecodeWorker, OpusEncoder};

const SAMPLE_RATE: u32 = 48_000;
const CHANNELS: u16 = 1;
/// 20 ms @ 48 kHz — the canonical Opus block.
const BLOCK: usize = 960;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // ---- Encode a 1-second 440 Hz clip into 50 x 20 ms Opus packets. ----
    let mut enc = OpusEncoder::new(SAMPLE_RATE, CHANNELS, opus::Application::Audio)?;
    enc.set_bitrate(64_000);

    let packets: Vec<Vec<u8>> = (0..50)
        .map(|b| {
            // Mono sine for block `b` (mono => interleaved == planar).
            let pcm: Vec<f32> = (0..BLOCK)
                .map(|i| {
                    let idx = b * BLOCK + i;
                    0.5 * (2.0 * std::f32::consts::PI * 440.0 * idx as f32 / SAMPLE_RATE as f32)
                        .sin()
                })
                .collect();
            enc.encode(&pcm).expect("encode")
        })
        .collect();
    let total_bytes: usize = packets.iter().map(Vec::len).sum();
    eprintln!(
        "encoded {} packets ({} bytes total)",
        packets.len(),
        total_bytes
    );

    // ---- Spawn the decode worker. The RT side gets a wait-free Consumer. ----
    let (mut worker, mut rt_consumer) = OpusDecodeWorker::new(SAMPLE_RATE, CHANNELS, 64)?;

    // Submit all packets; the worker decodes them off-thread. The input ring
    // (capacity 64) holds more than the 50 packets, so every submit succeeds.
    for pkt in &packets {
        assert!(
            worker.submit_packet(pkt.clone()),
            "input ring unexpectedly full"
        );
    }

    // ---- "Real-time" loop: pop pre-decoded frames, never touching libopus. ----
    let want = packets.len();
    let mut received = 0usize;
    let mut poll = 0u32;
    while received < want && poll < 50_000 {
        while let Ok(frame) = rt_consumer.pop() {
            received += 1;
            assert_eq!(frame.channels, CHANNELS);
            assert_eq!(frame.num_frames(), BLOCK);
            if received >= want {
                break;
            }
        }
        if received < want {
            thread::sleep(Duration::from_micros(200));
            poll += 1;
        }
    }

    // Clean shutdown: dropping the input producer lets the worker exit, then
    // join completes. (The consumer is drained above; we let it drop here.)
    drop(rt_consumer);
    worker.join().expect("worker join");

    eprintln!("RT loop received {received}/{want} AudioFrames via wait-free pop");
    assert_eq!(received, want, "underrun: not all frames decoded in time");
    eprintln!("OK — RT-safe Opus decode pipeline verified.");
    Ok(())
}