scsynth 0.1.0

A safe Rust wrapper for an embedded, in-process SuperCollider scsynth engine.
//! Shared test helpers: a minimal SCgf SynthDef encoder, OSC message builders, and a Goertzel
//! tone detector. Used by the offline (NRT) and realtime (pump) synthesis tests.

#![allow(dead_code)]

use std::f32::consts::PI;

use rosc::{OscMessage, OscPacket, OscType};
use scsynth::World;

/// The OSC address of a reply, or `None` if `bytes` is not a decodable OSC message.
pub fn reply_addr(bytes: &[u8]) -> Option<String> {
    match rosc::decoder::decode_udp(bytes) {
        Ok((_, OscPacket::Message(m))) => Some(m.addr),
        _ => None,
    }
}

/// Pump silent blocks (driving the native NRT helper thread) and drain replies until one with OSC
/// address `addr` arrives, returning its raw bytes. Panics if it never arrives within `max_blocks`.
///
/// On wasm the reply is already queued after `send_packet`, so this returns on the first poll
/// without pumping; natively the reply lands after a few blocks once the helper thread has run.
pub fn pump_for_reply(
    world: &mut World,
    addr: &str,
    channels: usize,
    max_blocks: usize,
) -> Vec<u8> {
    let mut scratch = vec![0f32; 64 * channels];
    for _ in 0..max_blocks {
        while let Some(bytes) = world.poll_reply() {
            if reply_addr(&bytes).as_deref() == Some(addr) {
                return bytes;
            }
        }
        world.process(&[], 0, &mut scratch, channels);
        // Native replies land on the NRT helper thread, woken by the pump above; yield briefly so it
        // can run a command stage before the next poll. Wasm dispatches inline, so this never runs.
        #[cfg(not(target_arch = "wasm32"))]
        std::thread::sleep(std::time::Duration::from_millis(1));
    }
    panic!("never received a `{addr}` reply within {max_blocks} blocks");
}

/// Audio calc rate (`calc_FullRate`) in SCgf.
pub const RATE_AUDIO: u8 = 2;

/// Encode `Out.ar(0, [SinOsc.ar(freq, 0); channels])` as SCgf version 2 bytes under the def name
/// `"sine"` - the same sine written to `channels` consecutive output buses from bus 0.
pub fn synthdef_sine(freq: f32, channels: usize) -> Vec<u8> {
    let mut b = Vec::new();
    b.extend_from_slice(b"SCgf");
    b.extend_from_slice(&2i32.to_be_bytes()); // version
    b.extend_from_slice(&1i16.to_be_bytes()); // number of synthdefs

    write_pstring(&mut b, "sine"); // def name

    // Constants: [freq, zero].
    b.extend_from_slice(&2i32.to_be_bytes());
    b.extend_from_slice(&freq.to_be_bytes());
    b.extend_from_slice(&0.0f32.to_be_bytes());

    // Initial control values, then parameter specs (none).
    b.extend_from_slice(&0i32.to_be_bytes());
    b.extend_from_slice(&0i32.to_be_bytes());

    // UGens.
    b.extend_from_slice(&2i32.to_be_bytes());
    // 0: SinOsc.ar(freq = const 0, phase = const 1) -> 1 audio output.
    write_ugen(&mut b, "SinOsc", &[(-1, 0), (-1, 1)], &[RATE_AUDIO]);
    // 1: Out.ar(bus = const 1 (= 0.0), [SinOsc out0; channels]) -> no outputs.
    let mut out_inputs = vec![(-1, 1)];
    out_inputs.extend(std::iter::repeat_n((0, 0), channels));
    write_ugen(&mut b, "Out", &out_inputs, &[]);

    // Variants.
    b.extend_from_slice(&0i16.to_be_bytes());
    b
}

/// Write one UGen spec: name, audio calc rate, inputs `(from_unit, from_output)` (a `from_unit`
/// of -1 means a constant indexed by `from_output`), and per-output calc rates.
pub fn write_ugen(b: &mut Vec<u8>, name: &str, inputs: &[(i32, i32)], output_rates: &[u8]) {
    write_pstring(b, name);
    b.push(RATE_AUDIO);
    b.extend_from_slice(&(inputs.len() as i32).to_be_bytes());
    b.extend_from_slice(&(output_rates.len() as i32).to_be_bytes());
    b.extend_from_slice(&0i16.to_be_bytes()); // special index
    for &(from_unit, from_output) in inputs {
        b.extend_from_slice(&from_unit.to_be_bytes());
        b.extend_from_slice(&from_output.to_be_bytes());
    }
    b.extend_from_slice(output_rates);
}

/// Write a Pascal string: a `u8` length followed by the bytes (no terminator).
pub fn write_pstring(b: &mut Vec<u8>, s: &str) {
    b.push(u8::try_from(s.len()).expect("name too long"));
    b.extend_from_slice(s.as_bytes());
}

/// A `/d_recv` message carrying the given SynthDef bytes.
pub fn d_recv(synthdef: Vec<u8>) -> OscMessage {
    OscMessage {
        addr: "/d_recv".into(),
        args: vec![OscType::Blob(synthdef)],
    }
}

/// An `/s_new` message: `defname`, node id, add action (0 = head), target group.
pub fn s_new(name: &str, node_id: i32, add_action: i32, target: i32) -> OscMessage {
    OscMessage {
        addr: "/s_new".into(),
        args: vec![
            OscType::String(name.into()),
            OscType::Int(node_id),
            OscType::Int(add_action),
            OscType::Int(target),
        ],
    }
}

/// An `/n_free` message for the given node id.
pub fn n_free(node_id: i32) -> OscMessage {
    OscMessage {
        addr: "/n_free".into(),
        args: vec![OscType::Int(node_id)],
    }
}

/// A `/notify` message: register (`on = true`) or unregister this connection's reply address to
/// receive node notifications (`/n_go`, `/n_end`, ...). The server replies `/done`.
pub fn notify(on: bool) -> OscMessage {
    OscMessage {
        addr: "/notify".into(),
        args: vec![OscType::Int(i32::from(on))],
    }
}

/// A `/b_alloc` message: allocate buffer `bufnum` with `frames` frames and `channels` channels.
/// Sequenced - the server replies `/done` once done.
pub fn b_alloc(bufnum: i32, frames: i32, channels: i32) -> OscMessage {
    OscMessage {
        addr: "/b_alloc".into(),
        args: vec![
            OscType::Int(bufnum),
            OscType::Int(frames),
            OscType::Int(channels),
        ],
    }
}

/// A `/b_setn` message: write `values` into buffer `bufnum` starting at sample index `start`.
pub fn b_setn(bufnum: i32, start: i32, values: &[f32]) -> OscMessage {
    let mut args = vec![
        OscType::Int(bufnum),
        OscType::Int(start),
        OscType::Int(values.len() as i32),
    ];
    args.extend(values.iter().map(|&v| OscType::Float(v)));
    OscMessage {
        addr: "/b_setn".into(),
        args,
    }
}

/// Encode a single OSC message to bytes (for `World::send_packet`).
pub fn encode(message: OscMessage) -> Vec<u8> {
    rosc::encoder::encode(&OscPacket::Message(message)).expect("failed to encode OSC message")
}

/// Goertzel magnitude of `samples` at `freq`.
pub fn goertzel(samples: &[f32], sample_rate: f32, freq: f32) -> f32 {
    let w = 2.0 * PI * freq / sample_rate;
    let coeff = 2.0 * w.cos();
    let (mut s1, mut s2) = (0.0f32, 0.0f32);
    for &x in samples {
        let s0 = x + coeff * s1 - s2;
        s2 = s1;
        s1 = s0;
    }
    (s1 * s1 + s2 * s2 - coeff * s1 * s2).sqrt()
}

/// Root-mean-square level of `samples`.
pub fn rms(samples: &[f32]) -> f32 {
    if samples.is_empty() {
        return 0.0;
    }
    (samples.iter().map(|s| s * s).sum::<f32>() / samples.len() as f32).sqrt()
}