micstudio 0.1.0

High-performance native Rust audio studio, voice preset manager, and real-time DSP pipeline for PipeWire
use anyhow::{Context, Result};
use libspa::sys as spa_sys;
use pipewire as pw;
use pw::properties::properties;
use std::sync::Arc;
use std::sync::atomic::Ordering;
use std::thread;
use std::time::Duration;

#[test]
fn test_pipewire_capture_stream() -> Result<()> {
    pw::init();

    let mainloop =
        pw::main_loop::MainLoopRc::new(None).context("Failed to create PipeWire mainloop")?;
    let context = pw::context::ContextRc::new(&mainloop, None)
        .context("Failed to create PipeWire context")?;
    let core = context
        .connect_rc(None)
        .context("Failed to connect to PipeWire core")?;

    let capture_props = properties! {
        *pw::keys::MEDIA_TYPE => "Audio",
        *pw::keys::MEDIA_CATEGORY => "Capture",
        *pw::keys::MEDIA_ROLE => "Communication",
        *pw::keys::NODE_NAME => "test-stream-capture",
        *pw::keys::NODE_DESCRIPTION => "Test Stream",
        *pw::keys::AUDIO_CHANNELS => "1",
        "audio.rate" => "48000",
        *pw::keys::NODE_AUTOCONNECT => "true",
    };

    let stream = pw::stream::StreamBox::new(&core, "test-stream", capture_props)
        .context("Failed to create PipeWire stream")?;

    let received_samples = Arc::new(std::sync::atomic::AtomicUsize::new(0));
    let rec_clone = received_samples.clone();

    let _listener = stream
        .add_local_listener::<()>()
        .process(move |stream, _| {
            if let Some(mut buffer) = stream.dequeue_buffer() {
                let datas = buffer.datas_mut();
                if !datas.is_empty() {
                    if let Some(slice) = datas[0].data() {
                        let n_frames = slice.len() / 4;
                        rec_clone.fetch_add(n_frames, Ordering::SeqCst);
                    }
                }
            }
        })
        .register()?;

    let mut audio_info = libspa::param::audio::AudioInfoRaw::new();
    audio_info.set_format(libspa::param::audio::AudioFormat::F32LE);
    audio_info.set_rate(48000);
    audio_info.set_channels(1);
    let mut position = [0; libspa::param::audio::MAX_CHANNELS];
    position[0] = spa_sys::SPA_AUDIO_CHANNEL_MONO;
    audio_info.set_position(position);

    let format_pod: Vec<u8> = pw::spa::pod::serialize::PodSerializer::serialize(
        std::io::Cursor::new(Vec::new()),
        &pw::spa::pod::Value::Object(pw::spa::pod::Object {
            type_: spa_sys::SPA_TYPE_OBJECT_Format,
            id: spa_sys::SPA_PARAM_EnumFormat,
            properties: audio_info.into(),
        }),
    )?
    .0
    .into_inner();

    let pod = pw::spa::pod::Pod::from_bytes(&format_pod).context("parse pod")?;

    let (pw_sender, pw_receiver) = pw::channel::channel();
    let _pw_listener = pw_receiver.attach(mainloop.loop_(), {
        let mainloop = mainloop.clone();
        move |_| mainloop.quit()
    });

    thread::spawn(move || {
        thread::sleep(Duration::from_millis(1000));
        let _ = pw_sender.send(());
    });

    let mut params = [pod];
    stream.connect(
        pw::spa::utils::Direction::Input,
        None,
        pw::stream::StreamFlags::AUTOCONNECT
            | pw::stream::StreamFlags::MAP_BUFFERS
            | pw::stream::StreamFlags::RT_PROCESS,
        &mut params,
    )?;

    mainloop.run();

    let total = received_samples.load(Ordering::SeqCst);
    println!("Total frames captured in 1 second: {}", total);
    assert!(total > 0, "Must capture audio frames!");
    Ok(())
}

#[test]
fn test_mic_monitor_spectrum() -> Result<()> {
    use micstudio::monitor::MicMonitor;

    let mut monitor = MicMonitor::new(true);
    thread::sleep(Duration::from_millis(600));

    let spec = monitor.get_spectrum();
    println!(
        "MicMonitor captured RMS: {:.1} dBFS, Peak: {:.4}",
        spec.rms_db, spec.peak_level
    );
    assert_eq!(spec.bands.len(), 32);

    monitor.set_enabled(false);
    Ok(())
}