scsynth 0.1.0

A safe Rust wrapper for an embedded, in-process SuperCollider scsynth engine.
//! Device-free proof that a realtime engine can be driven from a host audio loop: create a
//! realtime stereo `World` (host-pumped external driver), load and start a SinOsc SynthDef over
//! OSC, pump it with [`World::process`], and assert both channels carry a 440 Hz tone.
//!
//! This is the headless analogue of driving the engine from a cpal callback (and guards against
//! the "left channel only" / interleaving class of bug).

mod common;

use scsynth::{Options, World};

const SAMPLE_RATE: u32 = 44_100;
const BLOCK: usize = 64;
const CHANNELS: usize = 2;
const FREQ: f32 = 440.0;

#[test]
fn pumps_sine_tone() {
    let mut world = World::new(
        Options::new()
            .real_time(true)
            .input_channels(0)
            .output_channels(CHANNELS as u32)
            .sample_rate(SAMPLE_RATE)
            .block_size(BLOCK as u32)
            .load_synthdefs(false)
            .verbosity(-1),
    )
    .expect("World_New failed");

    let mut scratch = vec![0f32; BLOCK * CHANNELS];

    // `/d_recv` is asynchronous: the NRT helper thread parses the SynthDef across several stages,
    // each advanced by a pump (which signals it). Pump and poll until it replies `/done`, rather
    // than pumping a fixed number of blocks and hoping it finished.
    world.send_packet(&common::encode(common::d_recv(common::synthdef_sine(
        FREQ, CHANNELS,
    ))));
    common::pump_for_reply(&mut world, "/done", CHANNELS, 200);

    world.send_packet(&common::encode(common::s_new("sine", 1000, 0, 0)));
    for _ in 0..10 {
        world.process(&[], 0, &mut scratch, CHANNELS);
    }

    // Capture a chunk of interleaved audio and check every channel independently.
    let mut output = vec![0f32; BLOCK * 64 * CHANNELS];
    world.process(&[], 0, &mut output, CHANNELS);

    let sr = SAMPLE_RATE as f32;
    for ch in 0..CHANNELS {
        let channel: Vec<f32> = output[ch..].iter().step_by(CHANNELS).copied().collect();
        let level = common::rms(&channel);
        let at_440 = common::goertzel(&channel, sr, FREQ);
        let at_1000 = common::goertzel(&channel, sr, 1000.0);
        let at_220 = common::goertzel(&channel, sr, 220.0);
        assert!(level > 0.1, "channel {ch} is ~silent (rms = {level})");
        assert!(
            at_440 > 10.0 * at_1000 && at_440 > 10.0 * at_220,
            "channel {ch}: expected a dominant 440 Hz tone (440={at_440}, 1000={at_1000}, 220={at_220})"
        );
    }
}