From 8e4a64661930400d7467c6597b20ff238af3bc6f Mon Sep 17 00:00:00 2001 From: olekspickle <22867443+olekspickle@users.noreply.github.com> Date: Wed, 28 Jan 2026 21:41:38 +0100 Subject: [PATCH 1/6] initital node API --- .gitignore | 3 + Cargo.toml | 1 + crates/firewheel-c/Cargo.toml | 35 ++ crates/firewheel-c/build.rs | 17 + crates/firewheel-c/cbindgen.toml | 4 + crates/firewheel-c/include/firewheel-c.h | 266 ++++++++ crates/firewheel-c/src/lib.rs | 743 +++++++++++++++++++++++ crates/firewheel-core/src/node.rs | 12 + crates/firewheel-graph/src/context.rs | 2 +- crates/firewheel-graph/src/processor.rs | 2 +- examples/bindings/cpal.sh | 14 + examples/bindings/main.c | 58 ++ examples/bindings/rtaudio.sh | 14 + 13 files changed, 1169 insertions(+), 2 deletions(-) create mode 100644 crates/firewheel-c/Cargo.toml create mode 100644 crates/firewheel-c/build.rs create mode 100644 crates/firewheel-c/cbindgen.toml create mode 100644 crates/firewheel-c/include/firewheel-c.h create mode 100644 crates/firewheel-c/src/lib.rs create mode 100755 examples/bindings/cpal.sh create mode 100644 examples/bindings/main.c create mode 100755 examples/bindings/rtaudio.sh diff --git a/.gitignore b/.gitignore index efe3eb11..66842212 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,9 @@ debug/ target/ +# C example binaries +examples/bindings/*example + # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html Cargo.lock diff --git a/Cargo.toml b/Cargo.toml index 3f98368b..73320edb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -175,6 +175,7 @@ members = [ "crates/firewheel-pool", "crates/firewheel-rtaudio", "crates/firewheel-symphonium", + "crates/firewheel-c", "examples/beep_test", "examples/cpal_input", "examples/custom_nodes", diff --git a/crates/firewheel-c/Cargo.toml b/crates/firewheel-c/Cargo.toml new file mode 100644 index 00000000..7c0c47f0 --- /dev/null +++ b/crates/firewheel-c/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "firewheel-c" +version = "0.1.0" +edition.workspace = true +license.workspace = true +authors.workspace = true +keywords.workspace = true +categories.workspace = true +exclude.workspace = true +repository.workspace = true + +[dependencies] +firewheel-core = { path = "../firewheel-core" } +firewheel-graph = { path = "../firewheel-graph" } +firewheel-nodes = { path = "../firewheel-nodes", features = ["beep_test", "sampler", "svf"] } +firewheel-cpal = { path = "../firewheel-cpal", optional = true } +firewheel-rtaudio = { path = "../firewheel-rtaudio", optional = true } +libc = "0.2" +lazy_static = "1.4" +symphonium = { version = "0.5", features = ["open-standards", "all-codecs"] } +symphonia-core = "0.5" +tracing.workspace = true +thunderdome.workspace = true + +[build-dependencies] +cbindgen = "0.29" + +[lib] +crate-type = ["staticlib"] + +[features] +default = ["cpal", "musical_transport"] +cpal = ["dep:firewheel-cpal"] +rtaudio = ["dep:firewheel-rtaudio"] +musical_transport = ["firewheel-graph/musical_transport"] diff --git a/crates/firewheel-c/build.rs b/crates/firewheel-c/build.rs new file mode 100644 index 00000000..b3e1e19a --- /dev/null +++ b/crates/firewheel-c/build.rs @@ -0,0 +1,17 @@ +extern crate cbindgen; + +use std::env; +use std::path::PathBuf; + +fn main() { + let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); + + let package_name = env::var("CARGO_PKG_NAME").unwrap(); + let output_file = PathBuf::from(&crate_dir) + .join("include") + .join(format!("{}.h", package_name)); + + cbindgen::generate(&crate_dir) + .unwrap() + .write_to_file(&output_file); +} diff --git a/crates/firewheel-c/cbindgen.toml b/crates/firewheel-c/cbindgen.toml new file mode 100644 index 00000000..72e0c817 --- /dev/null +++ b/crates/firewheel-c/cbindgen.toml @@ -0,0 +1,4 @@ +language = "C" + +[enum] +prefix_with_name = true diff --git a/crates/firewheel-c/include/firewheel-c.h b/crates/firewheel-c/include/firewheel-c.h new file mode 100644 index 00000000..38d6d8f4 --- /dev/null +++ b/crates/firewheel-c/include/firewheel-c.h @@ -0,0 +1,266 @@ +#include +#include +#include +#include + +/** + * An enumeration of the built-in factory nodes. + */ +enum FwFactoryNode { + FwFactoryNode_BeepTest, + FwFactoryNode_Sampler, + FwFactoryNode_SVF, + FwFactoryNode_Volume, + FwFactoryNode_VolumePan, +}; +typedef uint32_t FwFactoryNode; + +/** + * An enumeration of the fields that can be set in a stream configuration. + */ +enum FwStreamConfigField { + FwStreamConfigField_Input, + FwStreamConfigField_Output, + FwStreamConfigField_SampleRate, +}; +typedef uint32_t FwStreamConfigField; + +/** + * Opaque struct for FirewheelConfig. + */ +typedef struct FwConfig { + uint8_t _private[0]; +} FwConfig; + +/** + * Opaque struct for the audio context. + */ +typedef struct FwContext { + uint8_t _private[0]; +} FwContext; + +/** + * Opaque struct for a loaded sample + */ +typedef struct FwSample { + uint8_t _private[0]; +} FwSample; + +/** + * Opaque struct for stream configuration. + */ +typedef struct FwStreamConfig { + uint8_t _private[0]; +} FwStreamConfig; + +/** + * A union of the possible values for a stream configuration field. + */ +typedef union FwStreamConfigValue { + const char *device_name; + uint32_t sample_rate; + uint32_t num_channels; +} FwStreamConfigValue; + +/** + * Opaque struct for a list of audio devices. + */ +typedef struct FwAudioDeviceList { + uint8_t _private[0]; +} FwAudioDeviceList; + +const char *fw_get_last_error(void); + +/** + * Create a new Firewheel config with default values. + */ +struct FwConfig *fw_config_create(void); + +/** + * Free a Firewheel config. + */ +void fw_config_free(struct FwConfig *config); + +/** + * Set the number of graph inputs in a Firewheel config. + */ +void fw_config_set_num_graph_inputs(struct FwConfig *config, uint32_t value); + +/** + * Set the number of graph outputs in a Firewheel config. + */ +void fw_config_set_num_graph_outputs(struct FwConfig *config, uint32_t value); + +/** + * Set whether outputs should be hard clipped in a Firewheel config. + */ +void fw_config_set_hard_clip_outputs(struct FwConfig *config, int value); + +/** + * Set the declick seconds in a Firewheel config. + */ +void fw_config_set_declick_seconds(struct FwConfig *config, float value); + +/** + * Create a new audio context with the given config. + * If `config` is NULL, a default config will be used. + */ +struct FwContext *fw_context_create(struct FwConfig *config); + +/** + * Free an audio context. + */ +void fw_context_free(struct FwContext *ctx); + +/** + * Set the playing state of the context's musical transport. + */ +void fw_context_set_playing(struct FwContext *ctx, bool playing); + +/** + * Get the playing state of the context's musical transport. + * Returns true if playing, false otherwise. + */ +bool fw_context_is_playing(struct FwContext *ctx); + +/** + * Set the static beats per minute (BPM) of the context's musical transport. + * If bpm is 0.0 or negative, the static transport will be unset. + */ +void fw_context_set_static_beats_per_minute(struct FwContext *ctx, double bpm); + +/** + * Get the static beats per minute (BPM) of the context's musical transport. + * Returns 0.0 if no static transport is set or if musical_transport feature is not enabled. + */ +double fw_context_get_beats_per_minute(struct FwContext *ctx); + +/** + * Set the musical playhead of the context's musical transport. + */ +void fw_context_set_playhead(struct FwContext *ctx, double playhead_musical); + +/** + * Get the musical playhead of the context's musical transport. + * Returns 0.0 if musical_transport feature is not enabled. + */ +double fw_context_get_playhead(struct FwContext *ctx); + +/** + * Set the speed multiplier of the context's musical transport. + */ +void fw_context_set_speed_multiplier(struct FwContext *ctx, double multiplier); + +/** + * Remove a node from the Firewheel context. + * Returns 0 on success, -1 on error. + */ +int fw_node_remove(struct FwContext *ctx, uint32_t node_id); + +/** + * Add a new factory node to the Firewheel context. + * Returns the ID of the new node on success, or 0 on error. + */ +uint32_t fw_node_add(struct FwContext *ctx, FwFactoryNode node_type); + +/** + * Connects two nodes in the Firewheel context. + * + * `src_ports` and `dst_ports` are arrays of port indices. + * The length of both arrays must be `num_ports`. + * + * Returns 0 on success, -1 on error. + */ +int fw_node_connect(struct FwContext *ctx, + uint32_t src_node, + uint32_t dst_node, + const uint32_t *src_ports, + const uint32_t *dst_ports, + uintptr_t num_ports); + +/** + * Set an f32 parameter on a node. + * Returns 0 on success, -1 on error. + */ +int fw_node_set_f32_parameter(struct FwContext *ctx, + uint32_t node_id, + const uint32_t *path_indices, + uintptr_t path_len, + float value); + +/** + * Set a u32 parameter on a node. + * Returns 0 on success, -1 on error. + */ +int fw_node_set_u32_parameter(struct FwContext *ctx, + uint32_t node_id, + const uint32_t *path_indices, + uintptr_t path_len, + uint32_t value); + +/** + * Update the Firewheel context, processing any pending events. + */ +void fw_context_update(struct FwContext *ctx); + +/** + * Disconnects two nodes in the Firewheel context. + * + * `src_ports` and `dst_ports` are arrays of port indices. + * The length of both arrays must be `num_ports`. + */ +int fw_node_disconnect(struct FwContext *ctx, + uint32_t src_node, + uint32_t dst_node, + const uint32_t *src_ports, + const uint32_t *dst_ports, + uintptr_t num_ports); + +/** + * Load a sample from a file path. + * Returns a pointer to an FwSample on success, or NULL on error. + * Use fw_get_last_error to retrieve the error message. + */ +struct FwSample *fw_sample_load_from_file(const char *path); + +/** + * Free a loaded sample. + */ +void fw_sample_free(struct FwSample *sample); + +/** + * Create a new stream configuration with default values. + */ +struct FwStreamConfig *fw_stream_config_create(void); + +/** + * Free a stream configuration. + */ +void fw_stream_config_free(struct FwStreamConfig *config); + +/** + * Set a field in a stream configuration. + */ +void fw_stream_config_set(struct FwStreamConfig *config, + FwStreamConfigField field, + union FwStreamConfigValue value); + +/** + * Create a list of available audio devices. + */ +struct FwAudioDeviceList *fw_audio_device_list_create(bool input); + +/** + * Free a list of audio devices. + */ +void fw_audio_device_list_free(struct FwAudioDeviceList *list); + +/** + * Get the number of audio devices in a list. + */ +uintptr_t fw_audio_device_list_get_len(struct FwAudioDeviceList *list); + +/** + * Get the name of an audio device from a list + */ +const char *fw_audio_device_list_get_name(struct FwAudioDeviceList *list, uintptr_t index); diff --git a/crates/firewheel-c/src/lib.rs b/crates/firewheel-c/src/lib.rs new file mode 100644 index 00000000..c7b79508 --- /dev/null +++ b/crates/firewheel-c/src/lib.rs @@ -0,0 +1,743 @@ +use firewheel_core::channel_config::ChannelCount; +use firewheel_core::clock::InstantMusical; +use firewheel_core::collector::ArcGc; +use firewheel_core::diff::ParamPath; +use firewheel_core::event::{NodeEvent, NodeEventType, ParamData}; +use firewheel_core::node::NodeID; +use firewheel_graph::backend::{AudioBackend, SimpleDeviceConfig, SimpleStreamConfig}; +use firewheel_graph::processor::ContextToProcessorMsg; +use firewheel_graph::{FirewheelConfig, FirewheelCtx}; +use firewheel_nodes::{ + beep_test::BeepTestNode, sampler::SamplerNode, svf::SvfStereoNode, volume::VolumeNode, + volume_pan::VolumePanNode, +}; +use lazy_static::lazy_static; +use symphonium::SymphoniumLoader; + +use std::ffi::{c_char, CStr, CString}; +use std::os::raw::c_int; +use std::ptr; +use std::sync::{Arc, Mutex}; + +mod internal { + #[cfg(not(any(feature = "cpal", feature = "rtaudio")))] + pub use super::no_backend::NoBackend as Backend; + #[cfg(feature = "cpal")] + pub use firewheel_cpal::CpalBackend as Backend; + #[cfg(all(feature = "rtaudio", not(feature = "cpal")))] + pub use firewheel_rtaudio::RtAudioBackend as Backend; +} +use internal::Backend; + +lazy_static! { + static ref LAST_ERROR: Mutex> = Mutex::new(None); +} + +fn set_last_error(err: String) { + if let Ok(mut last_error) = LAST_ERROR.lock() { + *last_error = Some(CString::new(err).unwrap_or_default()); + } +} + +fn fail>(msg: T) -> c_int { + set_last_error(msg.into()); + -1 +} + +#[no_mangle] +pub extern "C" fn fw_get_last_error() -> *const c_char { + if let Ok(mut last_error) = LAST_ERROR.lock() { + if let Some(err) = last_error.take() { + return err.into_raw(); + } + } + ptr::null() +} + +/// Opaque struct for a loaded sample +#[repr(C)] +pub struct FwSample { + _private: [u8; 0], +} + +#[cfg(not(any(feature = "cpal", feature = "rtaudio")))] +mod no_backend { + use firewheel_graph::backend::{AudioBackend, DeviceInfoSimple, SimpleStreamConfig}; + use firewheel_graph::processor::FirewheelProcessor; + use std::error::Error; + use std::fmt::{Display, Formatter}; + use std::time::Duration; + + #[derive(Debug)] + pub struct NoBackendError; + + impl Display for NoBackendError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "No Backend") + } + } + + impl Error for NoBackendError {} + + pub struct NoBackend; + + impl AudioBackend for NoBackend { + type Enumerator = Self; + type Config = SimpleStreamConfig; + type StartStreamError = NoBackendError; + type StreamError = NoBackendError; + type Instant = std::time::Instant; + + fn enumerator() -> Self::Enumerator { + Self + } + + fn input_devices_simple(&mut self) -> Vec { + Vec::new() + } + + fn output_devices_simple(&mut self) -> Vec { + Vec::new() + } + + fn convert_simple_config(&mut self, config: &SimpleStreamConfig) -> Self::Config { + config.clone() + } + + fn start_stream( + _config: Self::Config, + ) -> Result<(Self, firewheel_core::StreamInfo), Self::StartStreamError> { + unimplemented!() + } + + fn set_processor(&mut self, _processor: FirewheelProcessor) { + unimplemented!() + } + + fn poll_status(&mut self) -> Result<(), Self::StreamError> { + Ok(()) + } + + fn delay_from_last_process(&self, _process_timestamp: Self::Instant) -> Option { + None + } + } +} + +/// An enumeration of the available audio backends. +#[repr(u32)] +pub enum FwBackend { + FwBackendCpal, + FwBackendRtAudio, + FwBackendNone, +} + +/// Opaque struct for FirewheelConfig. +#[repr(C)] +pub struct FwConfig { + _private: [u8; 0], +} + +/// Create a new Firewheel config with default values. +#[no_mangle] +pub extern "C" fn fw_config_create() -> *mut FwConfig { + let config = Box::new(FirewheelConfig::default()); + Box::into_raw(config) as *mut FwConfig +} + +/// Free a Firewheel config. +#[no_mangle] +pub unsafe extern "C" fn fw_config_free(config: *mut FwConfig) { + if !config.is_null() { + let _ = (*(config as *mut FirewheelConfig)).clone(); + } +} + +/// Set the number of graph inputs in a Firewheel config. +#[no_mangle] +pub unsafe extern "C" fn fw_config_set_num_graph_inputs(config: *mut FwConfig, value: u32) { + if config.is_null() { + return; + } + let config = &mut *(config as *mut FirewheelConfig); + config.num_graph_inputs = ChannelCount::new(value).unwrap_or(ChannelCount::ZERO); +} + +/// Set the number of graph outputs in a Firewheel config. +#[no_mangle] +pub unsafe extern "C" fn fw_config_set_num_graph_outputs(config: *mut FwConfig, value: u32) { + if config.is_null() { + return; + } + let config = &mut *(config as *mut FirewheelConfig); + config.num_graph_outputs = ChannelCount::new(value).unwrap_or(ChannelCount::ZERO); +} + +/// Set whether outputs should be hard clipped in a Firewheel config. +#[no_mangle] +pub unsafe extern "C" fn fw_config_set_hard_clip_outputs(config: *mut FwConfig, value: c_int) { + if config.is_null() { + return; + } + let config = &mut *(config as *mut FirewheelConfig); + config.hard_clip_outputs = value != 0; +} + +/// Set the declick seconds in a Firewheel config. +#[no_mangle] +pub unsafe extern "C" fn fw_config_set_declick_seconds(config: *mut FwConfig, value: f32) { + if config.is_null() { + return; + } + let config = &mut *(config as *mut FirewheelConfig); + config.declick_seconds = value; +} + +/// Opaque struct for the audio context. +#[repr(C)] +pub struct FwContext { + _private: [u8; 0], +} + +/// Create a new audio context with the given config. +/// If `config` is NULL, a default config will be used. +#[no_mangle] +pub extern "C" fn fw_context_create(config: *mut FwConfig) -> *mut FwContext { + let rust_config = if config.is_null() { + FirewheelConfig::default() + } else { + // SAFETY: `config` is checked for null and assumed to be a valid `FwConfig`. + unsafe { (*(config as *mut FirewheelConfig)).clone() } + }; + + let ctx = Box::new(FirewheelCtx::::new(rust_config)); + Box::into_raw(ctx) as *mut FwContext +} + +/// Free an audio context. +#[no_mangle] +pub unsafe extern "C" fn fw_context_free(ctx: *mut FwContext) { + if !ctx.is_null() { + // SAFETY: `ctx` is checked for null and assumed to be a valid `FirewheelCtx`. + // The type `FirewheelCtx` is used here, relying on the same conditional + // compilation as `fw_context_create`. + let _ = Box::from_raw(ctx as *mut FirewheelCtx); + } +} + +/// Set the playing state of the context's musical transport. +#[no_mangle] +pub extern "C" fn fw_context_set_playing(ctx: *mut FwContext, playing: bool) { + if let Some(ctx) = unsafe { (ctx as *mut FirewheelCtx).as_mut() } { + #[cfg(feature = "musical_transport")] + { + let mut ts = ctx.transport_state().clone(); + *ts.playing.as_mut_unsync() = playing; + let _ = ctx.sync_transport(&ts); + } + #[cfg(not(feature = "musical_transport"))] + { + // TODO: Log a warning if musical_transport feature is not enabled + } + } +} + +/// Get the playing state of the context's musical transport. +/// Returns true if playing, false otherwise. +#[no_mangle] +pub extern "C" fn fw_context_is_playing(ctx: *mut FwContext) -> bool { + if let Some(ctx) = unsafe { (ctx as *mut FirewheelCtx).as_ref() } { + #[cfg(feature = "musical_transport")] + { + let playing = ctx.transport_state().playing.as_ref(); + return *playing; + } + } + false +} + +/// Set the static beats per minute (BPM) of the context's musical transport. +/// If bpm is 0.0 or negative, the static transport will be unset. +#[no_mangle] +pub extern "C" fn fw_context_set_static_beats_per_minute(ctx: *mut FwContext, bpm: f64) { + if let Some(ctx) = unsafe { (ctx as *mut FirewheelCtx).as_mut() } { + #[cfg(feature = "musical_transport")] + { + let mut ts = ctx.transport_state().clone(); + if bpm > 0.0 { + ts.set_static_transport(Some(bpm)); + } else { + ts.set_static_transport(None); + } + let _ = ctx.sync_transport(&ts); + } + #[cfg(not(feature = "musical_transport"))] + { + // TODO: Log a warning if musical_transport feature is not enabled + } + } +} + +/// Get the static beats per minute (BPM) of the context's musical transport. +/// Returns 0.0 if no static transport is set or if musical_transport feature is not enabled. +#[no_mangle] +pub extern "C" fn fw_context_get_beats_per_minute(ctx: *mut FwContext) -> f64 { + if let Some(ctx) = unsafe { (ctx as *mut FirewheelCtx).as_ref() } { + #[cfg(feature = "musical_transport")] + { + return ctx.transport_state().beats_per_minute().unwrap_or(0.0); + } + } + 0.0 +} + +/// Set the musical playhead of the context's musical transport. +#[no_mangle] +pub extern "C" fn fw_context_set_playhead(ctx: *mut FwContext, playhead_musical: f64) { + if let Some(ctx) = unsafe { (ctx as *mut FirewheelCtx).as_mut() } { + #[cfg(feature = "musical_transport")] + { + let mut ts = ctx.transport_state().clone(); + *ts.playhead.as_mut_unsync() = InstantMusical(playhead_musical); + let _ = ctx.sync_transport(&ts); + } + #[cfg(not(feature = "musical_transport"))] + { + // TODO: Log a warning if musical_transport feature is not enabled + } + } +} + +/// Get the musical playhead of the context's musical transport. +/// Returns 0.0 if musical_transport feature is not enabled. +#[no_mangle] +pub extern "C" fn fw_context_get_playhead(ctx: *mut FwContext) -> f64 { + if let Some(ctx) = unsafe { (ctx as *mut FirewheelCtx).as_ref() } { + #[cfg(feature = "musical_transport")] + { + return ctx.transport_state().playhead.as_ref().0; + } + } + 0.0 +} + +/// Set the speed multiplier of the context's musical transport. +#[no_mangle] +pub extern "C" fn fw_context_set_speed_multiplier(ctx: *mut FwContext, multiplier: f64) { + if let Some(ctx) = unsafe { (ctx as *mut FirewheelCtx).as_mut() } { + #[cfg(feature = "musical_transport")] + { + let mut ts = ctx.transport_state().clone(); + // TODO: expose change_at + ts.set_speed_multiplier(multiplier, None); + let _ = ctx.sync_transport(&ts); + } + #[cfg(not(feature = "musical_transport"))] + { + // TODO: Log a warning if musical_transport feature is not enabled + } + } +} + +/// An enumeration of the built-in factory nodes. +#[repr(u32)] +pub enum FwFactoryNode { + BeepTest, + Sampler, + SVF, + Volume, + VolumePan, +} + +/// Remove a node from the Firewheel context. +/// Returns 0 on success, -1 on error. +#[no_mangle] +pub extern "C" fn fw_node_remove(ctx: *mut FwContext, node_id: u32) -> c_int { + if let Some(ctx) = unsafe { (ctx as *mut FirewheelCtx).as_mut() } { + // TODO: Handle the GraphError result more gracefully?.. + match ctx.remove_node(NodeID::from_bits(node_id)) { + Ok(_) => 0, + Err(e) => fail(format!("Failed to remove node: {:?}", e)), + } + } else { + fail("Context is null") + } +} + +/// Add a new factory node to the Firewheel context. +/// Returns the ID of the new node on success, or 0 on error. +#[no_mangle] +pub extern "C" fn fw_node_add(ctx: *mut FwContext, node_type: FwFactoryNode) -> u32 { + if let Some(ctx) = unsafe { (ctx as *mut FirewheelCtx).as_mut() } { + // TODO: expose node config + let node_id = match node_type { + FwFactoryNode::BeepTest => ctx.add_node(BeepTestNode::default(), None), + FwFactoryNode::Sampler => ctx.add_node(SamplerNode::default(), None), + FwFactoryNode::SVF => ctx.add_node(SvfStereoNode::default(), None), + FwFactoryNode::Volume => ctx.add_node(VolumeNode::default(), None), + FwFactoryNode::VolumePan => ctx.add_node(VolumePanNode::default(), None), + }; + + return node_id.to_bits(); + } + 0 +} + +/// Connects two nodes in the Firewheel context. +/// +/// `src_ports` and `dst_ports` are arrays of port indices. +/// The length of both arrays must be `num_ports`. +/// +/// Returns 0 on success, -1 on error. +#[no_mangle] +pub extern "C" fn fw_node_connect( + ctx: *mut FwContext, + src_node: u32, + dst_node: u32, + src_ports: *const u32, + dst_ports: *const u32, + num_ports: usize, +) -> c_int { + if let Some(ctx) = unsafe { (ctx as *mut FirewheelCtx).as_mut() } { + // TODO: avoid allocating Vec here?.. + let mut ports_src_dst = Vec::with_capacity(num_ports); + for i in 0..num_ports { + let src_port = unsafe { *src_ports.add(i) }; + let dst_port = unsafe { *dst_ports.add(i) }; + ports_src_dst.push((src_port, dst_port)); + } + + // TODO: The check_for_cycles parameter should probably be exposed to the C API as well. + match ctx.connect( + NodeID::from_bits(src_node), + NodeID::from_bits(dst_node), + &ports_src_dst, + true, + ) { + Ok(_) => 0, + Err(e) => fail(format!("Failed to connect nodes: {:?}", e)), + } + } else { + fail("Context is null") + } +} + +/// Set an f32 parameter on a node. +/// Returns 0 on success, -1 on error. +#[no_mangle] +pub extern "C" fn fw_node_set_f32_parameter( + ctx: *mut FwContext, + node_id: u32, + path_indices: *const u32, + path_len: usize, + value: f32, +) -> c_int { + if let Some(ctx) = unsafe { (ctx as *mut FirewheelCtx).as_mut() } { + let path = if path_len == 1 { + ParamPath::Single(unsafe { *path_indices }) + } else if path_len > 1 { + let slice = unsafe { std::slice::from_raw_parts(path_indices, path_len) }; + ParamPath::Multi(ArcGc::new_unsized(|| Arc::<[u32]>::from(slice))) + } else { + return fail("Empty path is invalid for parameter setting"); + }; + + let node_event = NodeEvent::new( + NodeID::from_bits(node_id), + NodeEventType::Param { + data: ParamData::F32(value), + path, + }, + ); + let msg = ContextToProcessorMsg::EventGroup(vec![node_event]); + + ctx.send_message_to_processor(msg) + .map(|_| 0) + .unwrap_or(fail("Failed to send event to processor")) + } else { + fail("Context is null") + } +} + +/// Set a u32 parameter on a node. +/// Returns 0 on success, -1 on error. +#[no_mangle] +pub extern "C" fn fw_node_set_u32_parameter( + ctx: *mut FwContext, + node_id: u32, + path_indices: *const u32, + path_len: usize, + value: u32, +) -> c_int { + if let Some(ctx) = unsafe { (ctx as *mut FirewheelCtx).as_mut() } { + let path = if path_len == 1 { + ParamPath::Single(unsafe { *path_indices }) + } else if path_len > 1 { + let slice = unsafe { std::slice::from_raw_parts(path_indices, path_len) }; + ParamPath::Multi(ArcGc::new_unsized(|| Arc::<[u32]>::from(slice))) + } else { + return fail("Empty path is invalid for parameter setting"); + }; + + let node_event = NodeEvent::new( + NodeID::from_bits(node_id), + NodeEventType::Param { + data: ParamData::U32(value), + path, + }, + ); + let msg = ContextToProcessorMsg::EventGroup(vec![node_event]); + + // TODO: Handle the error gracefully + if ctx.send_message_to_processor(msg).is_ok() { + 0 + } else { + fail("Failed to send event to processor") + } + } else { + fail("Context is null") + } +} + +/// Update the Firewheel context, processing any pending events. +#[no_mangle] +pub extern "C" fn fw_context_update(ctx: *mut FwContext) { + if let Some(ctx) = unsafe { (ctx as *mut FirewheelCtx).as_mut() } { + let _ = ctx.update(); + } +} + +/// Disconnects two nodes in the Firewheel context. +/// +/// `src_ports` and `dst_ports` are arrays of port indices. +/// The length of both arrays must be `num_ports`. +#[no_mangle] + +pub extern "C" fn fw_node_disconnect( + ctx: *mut FwContext, + src_node: u32, + dst_node: u32, + src_ports: *const u32, + dst_ports: *const u32, + num_ports: usize, +) -> c_int { + if let Some(ctx) = unsafe { (ctx as *mut FirewheelCtx).as_mut() } { + let mut ports_src_dst = Vec::with_capacity(num_ports); + + for i in 0..num_ports { + let src_port = unsafe { *src_ports.add(i) }; + let dst_port = unsafe { *dst_ports.add(i) }; + ports_src_dst.push((src_port, dst_port)); + } + + match ctx.disconnect( + NodeID::from_bits(src_node), + NodeID::from_bits(dst_node), + &ports_src_dst, + ) { + true => 0, + false => fail("Failed to disconnect nodes"), + } + } else { + fail("Context is null") + } +} + +/// Load a sample from a file path. +/// Returns a pointer to an FwSample on success, or NULL on error. +/// Use fw_get_last_error to retrieve the error message. +#[no_mangle] +pub extern "C" fn fw_sample_load_from_file(path: *const c_char) -> *mut FwSample { + let c_str = unsafe { + if path.is_null() { + set_last_error("Invalid path".to_string()); + return ptr::null_mut(); + } + + CStr::from_ptr(path) + }; + + let path_str = match c_str.to_str() { + Ok(s) => s, + Err(_) => { + set_last_error("Invalid UTF-8 in path".to_string()); + return ptr::null_mut(); + } + }; + + match SymphoniumLoader::new().load(path_str, None, Default::default(), None) { + Ok(loader) => { + let sample = Box::new(loader); + Box::into_raw(sample) as *mut FwSample + } + Err(e) => { + set_last_error(e.to_string()); + ptr::null_mut() + } + } +} + +/// Free a loaded sample. +#[no_mangle] +pub unsafe extern "C" fn fw_sample_free(sample: *mut FwSample) { + if !sample.is_null() { + let _ = Box::from_raw(sample as *mut SymphoniumLoader); + } +} + +/// Opaque struct for a list of audio devices. +#[repr(C)] +pub struct FwAudioDeviceList { + _private: [u8; 0], +} + +/// Opaque struct for stream configuration. +#[repr(C)] +pub struct FwStreamConfig { + _private: [u8; 0], +} + +/// An enumeration of the fields that can be set in a stream configuration. +#[repr(u32)] +pub enum FwStreamConfigField { + Input, + Output, + SampleRate, +} + +/// A union of the possible values for a stream configuration field. +#[repr(C)] +pub union FwStreamConfigValue { + pub device_name: *const c_char, + pub sample_rate: u32, + pub num_channels: u32, +} + +/// Create a new stream configuration with default values. +#[no_mangle] +pub extern "C" fn fw_stream_config_create() -> *mut FwStreamConfig { + let config = Box::new(SimpleStreamConfig::default()); + Box::into_raw(config) as *mut FwStreamConfig +} + +/// Free a stream configuration. +#[no_mangle] +pub unsafe extern "C" fn fw_stream_config_free(config: *mut FwStreamConfig) { + if !config.is_null() { + let _ = (*(config as *mut SimpleStreamConfig)).clone(); + } +} + +/// Set a field in a stream configuration. +#[no_mangle] +pub unsafe extern "C" fn fw_stream_config_set( + config: *mut FwStreamConfig, + field: FwStreamConfigField, + value: FwStreamConfigValue, +) { + if config.is_null() { + return; + } + let config = &mut *(config as *mut SimpleStreamConfig); + match field { + FwStreamConfigField::Input => { + config.input = if value.device_name.is_null() { + None + } else { + Some(SimpleDeviceConfig { + device: Some( + CStr::from_ptr(value.device_name) + .to_string_lossy() + .into_owned(), + ), + channels: Some(value.num_channels as usize), + }) + }; + } + FwStreamConfigField::Output => { + config.output = SimpleDeviceConfig { + device: Some( + CStr::from_ptr(value.device_name) + .to_string_lossy() + .into_owned(), + ), + channels: Some(value.num_channels as usize), + }; + } + FwStreamConfigField::SampleRate => { + config.desired_sample_rate = Some(value.sample_rate); + } + } +} + +/// Create a list of available audio devices. +#[no_mangle] +pub extern "C" fn fw_audio_device_list_create(input: bool) -> *mut FwAudioDeviceList { + let enumerator = Backend::enumerator(); + #[cfg(feature = "cpal")] + let devices: Vec = if input { + enumerator + .default_host() + .input_devices() + .into_iter() + .map(|d| d.name.unwrap_or_default()) + .collect() + } else { + enumerator + .default_host() + .output_devices() + .into_iter() + .map(|d| d.name.unwrap_or_default()) + .collect() + }; + #[cfg(feature = "rtaudio")] + let devices: Vec = if input { + enumerator + .default_api() + .iter_input_devices() + .map(|d| d.id.name.clone()) + .collect() + } else { + enumerator + .default_api() + .iter_output_devices() + .map(|d| d.id.name.clone()) + .collect() + }; + let devices: Vec = devices + .into_iter() + .map(|d| CString::new(d).unwrap_or_default()) + .collect(); + let list = Box::new(devices); + Box::into_raw(list) as *mut FwAudioDeviceList +} + +/// Free a list of audio devices. +#[no_mangle] +pub unsafe extern "C" fn fw_audio_device_list_free(list: *mut FwAudioDeviceList) { + if !list.is_null() { + let _ = Box::from_raw(list as *mut Vec); + } +} + +/// Get the number of audio devices in a list. +#[no_mangle] +pub unsafe extern "C" fn fw_audio_device_list_get_len(list: *mut FwAudioDeviceList) -> usize { + (list as *mut Vec) + .as_ref() + .map(|l| l.len()) + .unwrap_or(0) +} + +/// Get the name of an audio device from a list +#[no_mangle] +pub unsafe extern "C" fn fw_audio_device_list_get_name( + list: *mut FwAudioDeviceList, + index: usize, +) -> *const c_char { + (list as *mut Vec) + .as_ref() + .and_then(|l| l.get(index)) + .map(|s| s.as_ptr()) + .unwrap_or(ptr::null()) +} diff --git a/crates/firewheel-core/src/node.rs b/crates/firewheel-core/src/node.rs index f144293c..7214c0e9 100644 --- a/crates/firewheel-core/src/node.rs +++ b/crates/firewheel-core/src/node.rs @@ -38,6 +38,18 @@ pub struct NodeID(pub thunderdome::Index); impl NodeID { pub const DANGLING: Self = Self(thunderdome::Index::DANGLING); + + pub fn to_bits(self) -> u32 { + self.0 + .to_bits() + .try_into() + .unwrap_or_else(|_| panic!("Failed to convert node index to C: {:?}", self.0)) + } + pub fn from_bits(bits: u32) -> Self { + thunderdome::Index::from_bits(bits as u64) + .map(Self) + .unwrap_or_else(|| panic!("Failed to convert node index from C: {bits}")) + } } impl Default for NodeID { diff --git a/crates/firewheel-graph/src/context.rs b/crates/firewheel-graph/src/context.rs index 3d433d06..22987128 100644 --- a/crates/firewheel-graph/src/context.rs +++ b/crates/firewheel-graph/src/context.rs @@ -1018,7 +1018,7 @@ impl FirewheelCtx { }); } - fn send_message_to_processor( + pub fn send_message_to_processor( &mut self, msg: ContextToProcessorMsg, ) -> Result<(), (ContextToProcessorMsg, UpdateError)> { diff --git a/crates/firewheel-graph/src/processor.rs b/crates/firewheel-graph/src/processor.rs index 9077f1fc..3d0d37ee 100644 --- a/crates/firewheel-graph/src/processor.rs +++ b/crates/firewheel-graph/src/processor.rs @@ -172,7 +172,7 @@ pub(crate) struct NodeEntry { event_data: NodeEventSchedulerData, } -pub(crate) enum ContextToProcessorMsg { +pub enum ContextToProcessorMsg { EventGroup(Vec), NewSchedule(Box), HardClipOutputs(bool), diff --git a/examples/bindings/cpal.sh b/examples/bindings/cpal.sh new file mode 100755 index 00000000..e5a099b4 --- /dev/null +++ b/examples/bindings/cpal.sh @@ -0,0 +1,14 @@ +#!/bin/bash +set -e + +# Change to the project root directory +cd "$(dirname "$0")/../.." + +# Build the Rust library +cargo build --package firewheel-c --features cpal + +# Compile the C example +gcc examples/bindings/main.c -Icrates/firewheel-c/include -Ltarget/debug -lfirewheel_c -lasound -ludev -lpthread -lm -o examples/bindings/c_cpal_example + +# Run the example +./examples/bindings/c_cpal_example diff --git a/examples/bindings/main.c b/examples/bindings/main.c new file mode 100644 index 00000000..b126f47f --- /dev/null +++ b/examples/bindings/main.c @@ -0,0 +1,58 @@ +#include +#include +#include +#include "firewheel-c.h" + +int main() { + printf("Creating Firewheel context...\n"); + FwContext* ctx = fw_context_create(NULL); + if (ctx == NULL) { + printf("Failed to create Firewheel context.\n"); + return 1; + } + printf("Firewheel context created successfully.\n"); + + printf("\n--- Available Output Devices ---\n"); + FwAudioDeviceList* device_list = fw_audio_device_list_create(false); + if (device_list != NULL) { + uintptr_t len = fw_audio_device_list_get_len(device_list); + for (uintptr_t i = 0; i < len; ++i) { + const char* name = fw_audio_device_list_get_name(device_list, i); + printf("%lu: %s\n", i, name); + } + fw_audio_device_list_free(device_list); + } + + printf("\n--- Testing Node API ---\n"); + uint32_t beep_node_id = fw_node_add(ctx, FwFactoryNode_BeepTest); + uint32_t volume_node_id = fw_node_add(ctx, FwFactoryNode_Volume); + printf("Added BeepTest node with ID: %u\n", beep_node_id); + printf("Added Volume node with ID: %u\n", volume_node_id); + + // Set volume + uint32_t path_f32[] = {0}; + fw_node_set_f32_parameter(ctx, volume_node_id, path_f32, 1, 0.5f); + + // Connect beep node to volume + uint32_t src_ports[] = {0}; + uint32_t dst_ports[] = {0}; + fw_node_connect(ctx, beep_node_id, volume_node_id, src_ports, dst_ports, 1); + + // Connect to volume node to output + uint32_t graph_output_ports[] = {0, 1}; + fw_node_connect(ctx, volume_node_id, 0, src_ports, graph_output_ports, 1); + + printf("\n--- Playing beep for 1 seconds ---\n"); + uint32_t path_u32[] = {0}; + fw_node_set_u32_parameter(ctx, beep_node_id, path_u32, 1, 1); + fw_context_update(ctx); + sleep(1); + fw_node_set_u32_parameter(ctx, beep_node_id, path_u32, 1, 0); + fw_context_update(ctx); + + fw_context_free(ctx); + printf("Firewheel context freed.\n"); + + return 0; +} + diff --git a/examples/bindings/rtaudio.sh b/examples/bindings/rtaudio.sh new file mode 100755 index 00000000..d5e7f918 --- /dev/null +++ b/examples/bindings/rtaudio.sh @@ -0,0 +1,14 @@ +#!/bin/bash +set -e + +# Change to the project root directory +cd "$(dirname "$0")/../.." + +# Build the Rust library +cargo build --package firewheel-c --features rtaudio + +# Compile the C example +gcc examples/bindings/main.c -Icrates/firewheel-c/include -Ltarget/debug -lfirewheel_c -lasound -lpthread -lm -o examples/bindings/c_rtaudio_example + +# Run the example +./examples/bindings/c_rtaudio_example From c2654ffa2b17fa2bcd91d56987b9963241fcb399 Mon Sep 17 00:00:00 2001 From: olekspickle <22867443+olekspickle@users.noreply.github.com> Date: Wed, 28 Jan 2026 22:05:55 +0100 Subject: [PATCH 2/6] rm unneeded error comment --- crates/firewheel-c/include/firewheel-c.h | 5 ----- crates/firewheel-c/src/lib.rs | 5 ----- 2 files changed, 10 deletions(-) diff --git a/crates/firewheel-c/include/firewheel-c.h b/crates/firewheel-c/include/firewheel-c.h index 38d6d8f4..1ed2d0bd 100644 --- a/crates/firewheel-c/include/firewheel-c.h +++ b/crates/firewheel-c/include/firewheel-c.h @@ -153,7 +153,6 @@ void fw_context_set_speed_multiplier(struct FwContext *ctx, double multiplier); /** * Remove a node from the Firewheel context. - * Returns 0 on success, -1 on error. */ int fw_node_remove(struct FwContext *ctx, uint32_t node_id); @@ -168,8 +167,6 @@ uint32_t fw_node_add(struct FwContext *ctx, FwFactoryNode node_type); * * `src_ports` and `dst_ports` are arrays of port indices. * The length of both arrays must be `num_ports`. - * - * Returns 0 on success, -1 on error. */ int fw_node_connect(struct FwContext *ctx, uint32_t src_node, @@ -180,7 +177,6 @@ int fw_node_connect(struct FwContext *ctx, /** * Set an f32 parameter on a node. - * Returns 0 on success, -1 on error. */ int fw_node_set_f32_parameter(struct FwContext *ctx, uint32_t node_id, @@ -190,7 +186,6 @@ int fw_node_set_f32_parameter(struct FwContext *ctx, /** * Set a u32 parameter on a node. - * Returns 0 on success, -1 on error. */ int fw_node_set_u32_parameter(struct FwContext *ctx, uint32_t node_id, diff --git a/crates/firewheel-c/src/lib.rs b/crates/firewheel-c/src/lib.rs index c7b79508..f3e37407 100644 --- a/crates/firewheel-c/src/lib.rs +++ b/crates/firewheel-c/src/lib.rs @@ -350,7 +350,6 @@ pub enum FwFactoryNode { } /// Remove a node from the Firewheel context. -/// Returns 0 on success, -1 on error. #[no_mangle] pub extern "C" fn fw_node_remove(ctx: *mut FwContext, node_id: u32) -> c_int { if let Some(ctx) = unsafe { (ctx as *mut FirewheelCtx).as_mut() } { @@ -387,8 +386,6 @@ pub extern "C" fn fw_node_add(ctx: *mut FwContext, node_type: FwFactoryNode) -> /// /// `src_ports` and `dst_ports` are arrays of port indices. /// The length of both arrays must be `num_ports`. -/// -/// Returns 0 on success, -1 on error. #[no_mangle] pub extern "C" fn fw_node_connect( ctx: *mut FwContext, @@ -423,7 +420,6 @@ pub extern "C" fn fw_node_connect( } /// Set an f32 parameter on a node. -/// Returns 0 on success, -1 on error. #[no_mangle] pub extern "C" fn fw_node_set_f32_parameter( ctx: *mut FwContext, @@ -460,7 +456,6 @@ pub extern "C" fn fw_node_set_f32_parameter( } /// Set a u32 parameter on a node. -/// Returns 0 on success, -1 on error. #[no_mangle] pub extern "C" fn fw_node_set_u32_parameter( ctx: *mut FwContext, From 95ed9e0367d2e4e1f19c3e471e7c19111d785721 Mon Sep 17 00:00:00 2001 From: olekspickle <22867443+olekspickle@users.noreply.github.com> Date: Wed, 28 Jan 2026 22:13:58 +0100 Subject: [PATCH 3/6] fix docs --- crates/firewheel-graph/src/processor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/firewheel-graph/src/processor.rs b/crates/firewheel-graph/src/processor.rs index 3d0d37ee..2ae27491 100644 --- a/crates/firewheel-graph/src/processor.rs +++ b/crates/firewheel-graph/src/processor.rs @@ -165,7 +165,7 @@ impl FirewheelProcessorInner { } } -pub(crate) struct NodeEntry { +pub struct NodeEntry { pub processor: Box, pub prev_output_was_silent: bool, From 2109e9c6aa68dc3bde629d6aaf3f790e037a2a35 Mon Sep 17 00:00:00 2001 From: olekspickle <22867443+olekspickle@users.noreply.github.com> Date: Wed, 28 Jan 2026 22:24:55 +0100 Subject: [PATCH 4/6] fix visibility --- crates/firewheel-graph/src/processor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/firewheel-graph/src/processor.rs b/crates/firewheel-graph/src/processor.rs index 2ae27491..c46edd18 100644 --- a/crates/firewheel-graph/src/processor.rs +++ b/crates/firewheel-graph/src/processor.rs @@ -192,7 +192,7 @@ pub(crate) enum ProcessorToContextMsg { } #[cfg(feature = "scheduled_events")] -pub(crate) struct ClearScheduledEventsEvent { +pub struct ClearScheduledEventsEvent { /// If `None`, then clear events for all nodes. pub node_id: Option, pub event_type: ClearScheduledEventsType, From 3f74f0e195dcd64d642ca4866cac1ee6084976d4 Mon Sep 17 00:00:00 2001 From: olekspickle <22867443+olekspickle@users.noreply.github.com> Date: Thu, 29 Jan 2026 15:10:32 +0100 Subject: [PATCH 5/6] maybe this will fix tests? generates duplicated function in header tho --- crates/firewheel-c/include/firewheel-c.h | 2 ++ crates/firewheel-c/src/lib.rs | 15 +++++++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/crates/firewheel-c/include/firewheel-c.h b/crates/firewheel-c/include/firewheel-c.h index 1ed2d0bd..f6a06dfd 100644 --- a/crates/firewheel-c/include/firewheel-c.h +++ b/crates/firewheel-c/include/firewheel-c.h @@ -245,6 +245,8 @@ void fw_stream_config_set(struct FwStreamConfig *config, */ struct FwAudioDeviceList *fw_audio_device_list_create(bool input); +struct FwAudioDeviceList *fw_audio_device_list_create(bool input); + /** * Free a list of audio devices. */ diff --git a/crates/firewheel-c/src/lib.rs b/crates/firewheel-c/src/lib.rs index f3e37407..bd0ec6c0 100644 --- a/crates/firewheel-c/src/lib.rs +++ b/crates/firewheel-c/src/lib.rs @@ -666,10 +666,10 @@ pub unsafe extern "C" fn fw_stream_config_set( } /// Create a list of available audio devices. +#[cfg(feature = "cpal")] #[no_mangle] pub extern "C" fn fw_audio_device_list_create(input: bool) -> *mut FwAudioDeviceList { let enumerator = Backend::enumerator(); - #[cfg(feature = "cpal")] let devices: Vec = if input { enumerator .default_host() @@ -685,7 +685,18 @@ pub extern "C" fn fw_audio_device_list_create(input: bool) -> *mut FwAudioDevice .map(|d| d.name.unwrap_or_default()) .collect() }; - #[cfg(feature = "rtaudio")] + let devices: Vec = devices + .into_iter() + .map(|d| CString::new(d).unwrap_or_default()) + .collect(); + let list = Box::new(devices); + Box::into_raw(list) as *mut FwAudioDeviceList +} + +#[cfg(feature = "rtaudio")] +#[no_mangle] +pub extern "C" fn fw_audio_device_list_create(input: bool) -> *mut FwAudioDeviceList { + let enumerator = Backend::enumerator(); let devices: Vec = if input { enumerator .default_api() From 2413372eeec2a565ab3fce9b3a08e7be851503b5 Mon Sep 17 00:00:00 2001 From: olekspickle <22867443+olekspickle@users.noreply.github.com> Date: Thu, 29 Jan 2026 17:35:54 +0100 Subject: [PATCH 6/6] Revert "maybe this will fix tests? generates duplicated function in header tho" This reverts commit 3f74f0e195dcd64d642ca4866cac1ee6084976d4. --- crates/firewheel-c/include/firewheel-c.h | 2 -- crates/firewheel-c/src/lib.rs | 15 ++------------- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/crates/firewheel-c/include/firewheel-c.h b/crates/firewheel-c/include/firewheel-c.h index f6a06dfd..1ed2d0bd 100644 --- a/crates/firewheel-c/include/firewheel-c.h +++ b/crates/firewheel-c/include/firewheel-c.h @@ -245,8 +245,6 @@ void fw_stream_config_set(struct FwStreamConfig *config, */ struct FwAudioDeviceList *fw_audio_device_list_create(bool input); -struct FwAudioDeviceList *fw_audio_device_list_create(bool input); - /** * Free a list of audio devices. */ diff --git a/crates/firewheel-c/src/lib.rs b/crates/firewheel-c/src/lib.rs index bd0ec6c0..f3e37407 100644 --- a/crates/firewheel-c/src/lib.rs +++ b/crates/firewheel-c/src/lib.rs @@ -666,10 +666,10 @@ pub unsafe extern "C" fn fw_stream_config_set( } /// Create a list of available audio devices. -#[cfg(feature = "cpal")] #[no_mangle] pub extern "C" fn fw_audio_device_list_create(input: bool) -> *mut FwAudioDeviceList { let enumerator = Backend::enumerator(); + #[cfg(feature = "cpal")] let devices: Vec = if input { enumerator .default_host() @@ -685,18 +685,7 @@ pub extern "C" fn fw_audio_device_list_create(input: bool) -> *mut FwAudioDevice .map(|d| d.name.unwrap_or_default()) .collect() }; - let devices: Vec = devices - .into_iter() - .map(|d| CString::new(d).unwrap_or_default()) - .collect(); - let list = Box::new(devices); - Box::into_raw(list) as *mut FwAudioDeviceList -} - -#[cfg(feature = "rtaudio")] -#[no_mangle] -pub extern "C" fn fw_audio_device_list_create(input: bool) -> *mut FwAudioDeviceList { - let enumerator = Backend::enumerator(); + #[cfg(feature = "rtaudio")] let devices: Vec = if input { enumerator .default_api()