scsynth 0.1.0

A safe Rust wrapper for an embedded, in-process SuperCollider scsynth engine.
//! Proves log capture: scsynth's diagnostic output (here, the error printed when `/s_new` names a
//! SynthDef that was never installed) is redirected into the [`scsynth::log`] buffer instead of
//! stdout.
//!
//! This is the native verification of the `SetPrintFunc` redirection that also serves the wasm build
//! (where stdout goes nowhere, so capture is the only way to see engine diagnostics).

mod common;

use scsynth::{Options, World};

const SAMPLE_RATE: u32 = 48_000;
const BLOCK: usize = 64;
const CHANNELS: usize = 1;

#[test]
fn captures_engine_log_output() {
    scsynth::log::capture();

    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");

    // `/s_new` for a SynthDef that was never installed makes the engine print an error line
    // ("*** ERROR: SynthDef <name> not found"). On native the command is performed during a pump.
    world.send_packet(&common::encode(common::s_new(
        "missing_def_xyz",
        1000,
        0,
        0,
    )));
    let mut scratch = vec![0f32; BLOCK * CHANNELS];

    let mut captured = Vec::new();
    for _ in 0..200 {
        world.process(&[], 0, &mut scratch, CHANNELS);
        while let Some(line) = scsynth::log::poll() {
            captured.push(line);
        }
        // The engine prints the specific error first ("*** ERROR: SynthDef <name> not found"), then
        // the generic command-failure line; stop once we have the specific (named) one.
        if captured.iter().any(|l| l.contains("missing_def_xyz")) {
            break;
        }
        #[cfg(not(target_arch = "wasm32"))]
        std::thread::sleep(std::time::Duration::from_millis(1));
    }

    scsynth::log::release();

    // Capture works at all: the engine's "not found" diagnostic reached our sink, not stdout.
    assert!(
        captured.iter().any(|l| l.contains("not found")),
        "no engine diagnostic captured; got: {captured:?}"
    );
    // Message formatting (the `%s` argument) survives the capture path: the offending def name shows.
    assert!(
        captured.iter().any(|l| l.contains("missing_def_xyz")),
        "expected the missing def name in the captured logs; got: {captured:?}"
    );
}