use std::collections::VecDeque;
use std::ffi::CString;
use std::os::raw::{c_char, c_int, c_void};
use std::path::Path;
#[cfg(not(target_arch = "wasm32"))]
use std::ptr;
use std::sync::Mutex;
use scsynth_sys as sys;
pub use scsynth_sys;
pub mod log {
use std::collections::VecDeque;
use std::os::raw::{c_char, c_int, c_void};
use std::sync::{LazyLock, Mutex};
use super::sys;
static SINK: LazyLock<Mutex<VecDeque<String>>> = LazyLock::new(|| Mutex::new(VecDeque::new()));
pub fn capture() {
unsafe { sys::scsynth_set_log_func(Some(push), std::ptr::null_mut()) };
}
pub fn release() {
unsafe { sys::scsynth_set_log_func(None, std::ptr::null_mut()) };
}
pub fn poll() -> Option<String> {
SINK.lock().unwrap().pop_front()
}
extern "C" fn push(_ctx: *mut c_void, text: *const c_char, len: c_int) {
if text.is_null() || len <= 0 {
return;
}
let bytes = unsafe { std::slice::from_raw_parts(text as *const u8, len as usize) };
let msg = String::from_utf8_lossy(bytes);
SINK.lock()
.unwrap()
.push_back(msg.trim_end_matches('\n').to_string());
}
}
type ReplySink = Mutex<VecDeque<Vec<u8>>>;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("World_New returned null - the engine could not be created")]
WorldNew,
#[error("path contained an interior NUL byte")]
Nul(#[from] std::ffi::NulError),
#[error("soundbuffer index {0} is out of range")]
BufferIndexOutOfRange(u32),
}
pub struct Buffer {
pub data: Vec<f32>,
pub channels: usize,
pub frames: usize,
}
pub struct Options {
raw: sys::WorldOptions,
cmd_filename: Option<CString>,
input_filename: Option<CString>,
output_filename: Option<CString>,
output_header_format: Option<CString>,
output_sample_format: Option<CString>,
}
pub struct World {
inner: *mut sys::World,
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
options: Options,
replies: Box<ReplySink>,
}
impl Default for Options {
fn default() -> Self {
let mut raw = std::mem::MaybeUninit::<sys::WorldOptions>::uninit();
let raw = unsafe {
sys::scsynth_default_world_options(raw.as_mut_ptr());
raw.assume_init()
};
Options {
raw,
cmd_filename: None,
input_filename: None,
output_filename: None,
output_header_format: None,
output_sample_format: None,
}
}
}
impl Options {
pub fn new() -> Self {
Self::default()
}
pub fn real_time(mut self, real_time: bool) -> Self {
self.raw.mRealTime = real_time;
self
}
pub fn output_channels(mut self, channels: u32) -> Self {
self.raw.mNumOutputBusChannels = channels;
self
}
pub fn input_channels(mut self, channels: u32) -> Self {
self.raw.mNumInputBusChannels = channels;
self
}
pub fn sample_rate(mut self, sample_rate: u32) -> Self {
self.raw.mPreferredSampleRate = sample_rate;
self
}
pub fn block_size(mut self, frames: u32) -> Self {
self.raw.mBufLength = frames;
self
}
pub fn verbosity(mut self, verbosity: i32) -> Self {
self.raw.mVerbosity = verbosity;
self
}
pub fn load_synthdefs(mut self, load: bool) -> Self {
self.raw.mLoadGraphDefs = u32::from(load);
self
}
pub fn nrt_command_file(mut self, path: impl AsRef<Path>) -> Result<Self, Error> {
let s = path_to_cstring(path)?;
self.raw.mNonRealTimeCmdFilename = s.as_ptr();
self.cmd_filename = Some(s);
Ok(self)
}
pub fn nrt_input_file(mut self, path: impl AsRef<Path>) -> Result<Self, Error> {
let s = path_to_cstring(path)?;
self.raw.mNonRealTimeInputFilename = s.as_ptr();
self.input_filename = Some(s);
Ok(self)
}
pub fn nrt_output_file(
mut self,
path: impl AsRef<Path>,
header_format: &str,
sample_format: &str,
) -> Result<Self, Error> {
let path = path_to_cstring(path)?;
let header = CString::new(header_format)?;
let sample = CString::new(sample_format)?;
self.raw.mNonRealTimeOutputFilename = path.as_ptr();
self.raw.mNonRealTimeOutputHeaderFormat = header.as_ptr();
self.raw.mNonRealTimeOutputSampleFormat = sample.as_ptr();
self.output_filename = Some(path);
self.output_header_format = Some(header);
self.output_sample_format = Some(sample);
Ok(self)
}
}
impl World {
pub fn new(mut options: Options) -> Result<Self, Error> {
#[cfg(not(target_arch = "wasm32"))]
let inner = unsafe { sys::World_New(&mut options.raw) };
#[cfg(target_arch = "wasm32")]
let inner = unsafe { sys::scsynth_wasm_new(&mut options.raw) };
if inner.is_null() {
return Err(Error::WorldNew);
}
let replies = Box::new(ReplySink::default());
Ok(World {
inner,
options,
replies,
})
}
pub fn send_packet(&self, packet: &[u8]) {
let len = c_int::try_from(packet.len()).expect("packet too large");
let ctx = &*self.replies as *const ReplySink as *mut c_void;
#[cfg(not(target_arch = "wasm32"))]
{
let mut buf = packet.to_vec();
unsafe {
sys::World_SendPacketWithContext(
self.inner,
len,
buf.as_mut_ptr() as *mut c_char,
Some(reply_to_sink),
ctx,
);
}
}
#[cfg(target_arch = "wasm32")]
unsafe {
sys::scsynth_wasm_perform(self.inner, packet.as_ptr(), len, Some(reply_to_sink), ctx);
}
}
pub fn poll_reply(&self) -> Option<Vec<u8>> {
self.replies.lock().unwrap().pop_front()
}
pub fn process(
&mut self,
input: &[f32],
input_channels: usize,
output: &mut [f32],
output_channels: usize,
) {
let num_frames = output.len().checked_div(output_channels).unwrap_or(0);
#[cfg(not(target_arch = "wasm32"))]
unsafe {
sys::scsynth_pump(
self.inner,
input.as_ptr(),
input_channels as c_int,
output.as_mut_ptr(),
output_channels as c_int,
num_frames as c_int,
);
}
#[cfg(target_arch = "wasm32")]
unsafe {
sys::scsynth_wasm_pump(
self.inner,
input.as_ptr(),
input_channels as c_int,
output.as_mut_ptr(),
output_channels as c_int,
num_frames as c_int,
);
}
}
pub fn copy_buffer(&self, index: u32) -> Result<Buffer, Error> {
let mut out: Option<Buffer> = None;
let ctx = &mut out as *mut Option<Buffer> as *mut c_void;
let err = unsafe { sys::scsynth_copy_buffer(self.inner, index, Some(copy_buffer_to), ctx) };
if err != 0 {
return Err(Error::BufferIndexOutOfRange(index));
}
Ok(out.unwrap_or(Buffer {
data: Vec::new(),
channels: 0,
frames: 0,
}))
}
#[cfg(not(target_arch = "wasm32"))]
pub fn non_realtime_render(mut self) {
unsafe { sys::World_NonRealTimeSynthesis(self.inner, &mut self.options.raw) };
self.inner = ptr::null_mut();
}
}
impl Drop for World {
fn drop(&mut self) {
if self.inner.is_null() {
return;
}
unsafe { sys::World_Cleanup(self.inner, false) };
}
}
extern "C" fn reply_to_sink(addr: *mut sys::ReplyAddress, buf: *mut c_char, size: c_int) {
let ctx = unsafe { sys::scsynth_reply_context(addr) };
if ctx.is_null() || buf.is_null() || size <= 0 {
return;
}
let sink = unsafe { &*(ctx as *const ReplySink) };
let bytes = unsafe { std::slice::from_raw_parts(buf as *const u8, size as usize) }.to_vec();
sink.lock().unwrap().push_back(bytes);
}
extern "C" fn copy_buffer_to(
ctx: *mut c_void,
data: *const f32,
num_samples: c_int,
num_channels: c_int,
num_frames: c_int,
) {
if ctx.is_null() {
return;
}
let out = unsafe { &mut *(ctx as *mut Option<Buffer>) };
let samples = num_samples.max(0) as usize;
let data = if data.is_null() || samples == 0 {
Vec::new()
} else {
unsafe { std::slice::from_raw_parts(data, samples) }.to_vec()
};
*out = Some(Buffer {
data,
channels: num_channels.max(0) as usize,
frames: num_frames.max(0) as usize,
});
}
fn path_to_cstring(path: impl AsRef<Path>) -> Result<CString, Error> {
Ok(CString::new(path.as_ref().to_string_lossy().into_owned())?)
}