From d22e929d179f8f0c3d34e7450a1c74ad3cc20d11 Mon Sep 17 00:00:00 2001 From: Ali Somay Date: Wed, 27 Nov 2024 20:29:17 +0100 Subject: [PATCH] Re-design the library with multi instance support and pass all tests. --- Cargo.toml | 32 +- README.md | 2 +- examples/simple.rs | 9 +- examples/with_nannou/bubble.rs | 3 +- examples/with_nannou/main.rs | 19 +- src/convenience.rs | 539 -------- src/error.rs | 125 +- src/functions.rs | 768 ++++++++++++ src/{ => functions}/array.rs | 46 +- src/{ => functions}/gui.rs | 13 +- src/{ => functions}/process.rs | 44 +- src/{ => functions}/receive.rs | 210 ++-- src/{ => functions}/send.rs | 150 ++- src/helpers.rs | 6 +- src/instance.rs | 270 ++++ src/lib.rs | 1299 +++++++++----------- src/types.rs | 43 +- tests/arrays.rs | 7 +- tests/general.rs | 8 +- tests/gui.rs | 4 +- tests/listening.rs | 13 +- tests/message_building.rs | 7 +- tests/pd_global_dollar_zero.rs | 4 +- tests/pd_global_search_paths.rs | 4 +- tests/pd_global_state.rs | 4 +- tests/pd_open_close_patch.rs | 9 +- tests/pd_subscribe_unsubscribe.rs | 20 +- tests/process.rs | 4 +- tests/send_and_receive_after_touch.rs | 20 +- tests/send_and_receive_bang.rs | 22 +- tests/send_and_receive_control_change.rs | 20 +- tests/send_and_receive_double.rs | 22 +- tests/send_and_receive_float.rs | 22 +- tests/send_and_receive_list.rs | 33 +- tests/send_and_receive_midi_byte.rs | 20 +- tests/send_and_receive_note_on.rs | 20 +- tests/send_and_receive_pitch_bend.rs | 20 +- tests/send_and_receive_poly_after_touch.rs | 20 +- tests/send_and_receive_program_change.rs | 20 +- tests/send_and_receive_sys_realtime.rs | 20 +- tests/send_and_receive_sysex.rs | 19 +- tests/send_and_receive_typed_message.rs | 25 +- 42 files changed, 2300 insertions(+), 1665 deletions(-) delete mode 100644 src/convenience.rs create mode 100644 src/functions.rs rename src/{ => functions}/array.rs (85%) rename src/{ => functions}/gui.rs (85%) rename src/{ => functions}/process.rs (86%) rename src/{ => functions}/receive.rs (80%) rename src/{ => functions}/send.rs (78%) create mode 100644 src/instance.rs diff --git a/Cargo.toml b/Cargo.toml index 13bcdb6..e921201 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "libpd-rs" -version = "0.2.0" +version = "0.3.0" authors = ["alisomay "] edition = "2021" license = "BSD-3-Clause" @@ -17,10 +17,19 @@ exclude = [ "assets/logo_*" ] +[lib] +name = "libpd_rs" +path = "src/lib.rs" +test = true +doctest = true +bench = false +doc = true +edition = "2021" +crate-type = ["lib"] + [dependencies] -# libpd-sys = "0.2" -libpd-sys = { path = "../libpd-sys" } -thiserror = "1.0.30" +libpd-sys = "0.3" +thiserror = "2" libffi = "3.0.0" tempfile = "3.3.0" embed-doc-image = "0.1.4" @@ -28,8 +37,8 @@ embed-doc-image = "0.1.4" [dev-dependencies] cpal = "0.15.2" sys-info = "0.9.1" -nannou = "0.18" -nannou_audio = "0.18" +nannou = "0.19" +nannou_audio = "0.19" rand = "0.8.5" # For local development, @@ -40,15 +49,6 @@ rand = "0.8.5" # [patch."https://github.com/alisomay/libpd-sys"] # libpd-sys = { path = "../libpd-sys" } -[lib] -name = "libpd_rs" # The name of the target. -path = "src/lib.rs" # The source file of the target. -test = true # Is tested by default. -doctest = true # Documentation examples are tested by default. -bench = false # Is benchmarked by default. -doc = true # Is documented by default. -proc-macro = false # Set to `true` for a proc-macro library. -edition = "2021" # The edition of the target. -crate-type = ["lib"] # The crate types to generate. + diff --git a/README.md b/README.md index cd1cd18..3e3bfce 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,7 @@ fn main() -> Result<(), Box> { // Here if we had an input buffer we could have modified it to do pre-processing. // Process audio, advance internal scheduler. - libpd_rs::process::process_float(ticks, &[], data); + libpd_rs::functions::process::process_float(ticks, &[], data); // Here we could have done post processing after pd processed our output buffer in place. }, diff --git a/examples/simple.rs b/examples/simple.rs index 2980512..0e1af11 100644 --- a/examples/simple.rs +++ b/examples/simple.rs @@ -1,5 +1,5 @@ use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; -use libpd_rs::convenience::PdGlobal; +use libpd_rs::Pd; fn main() -> Result<(), Box> { // Initialize cpal @@ -19,7 +19,7 @@ fn main() -> Result<(), Box> { // Initialize libpd with that configuration, // with no input channels since we're not going to use them. - let mut pd = PdGlobal::init_and_configure(0, output_channels, sample_rate)?; + let mut pd = Pd::init_and_configure(0, output_channels, sample_rate)?; // Let's evaluate a pd patch. // We could have opened a `.pd` file also. @@ -43,12 +43,13 @@ fn main() -> Result<(), Box> { &config.into(), move |data: &mut [f32], _: &cpal::OutputCallbackInfo| { // Provide the ticks to advance per iteration for the internal scheduler. - let ticks = libpd_rs::convenience::calculate_ticks(output_channels, data.len() as i32); + let ticks = + libpd_rs::functions::util::calculate_ticks(output_channels, data.len() as i32); // Here if we had an input buffer we could have modified it to do pre-processing. // Process audio, advance internal scheduler. - libpd_rs::process::process_float(ticks, &[], data); + libpd_rs::functions::process::process_float(ticks, &[], data); // Here we could have done post processing after pd processed our output buffer in place. }, diff --git a/examples/with_nannou/bubble.rs b/examples/with_nannou/bubble.rs index 0ee6762..4aa61cf 100644 --- a/examples/with_nannou/bubble.rs +++ b/examples/with_nannou/bubble.rs @@ -213,7 +213,8 @@ impl Bubble { // Collision with the floor! if distance_to_floor < self.properties.r * 2.0 { // On collision we tell the right voice to play with the right parameters in pd. - libpd_rs::send::send_list_to("bubble_collision", &self.pack_message()).unwrap(); + libpd_rs::functions::send::send_list_to("bubble_collision", &self.pack_message()) + .unwrap(); // Physics self.properties.dy = -self.properties.dy; diff --git a/examples/with_nannou/main.rs b/examples/with_nannou/main.rs index 1fb1041..f4f333a 100644 --- a/examples/with_nannou/main.rs +++ b/examples/with_nannou/main.rs @@ -14,7 +14,7 @@ fn main() { // This data structure will be shared across nannou functions. pub struct Model { - pd: libpd_rs::convenience::PdGlobal, + pd: libpd_rs::Pd, output_stream: audio::Stream<()>, gravity: f32, bubbles: RefCell>, @@ -62,19 +62,14 @@ fn model(app: &App) -> Model { .unwrap(); // Listen for console messages from pd - libpd_rs::receive::on_print(|val| { + libpd_rs::functions::receive::on_print(|val| { println!("{}", val); }); // This data structure will be shared across nannou functions. let mut model = Model { // Initialize pd - pd: libpd_rs::convenience::PdGlobal::init_and_configure( - 0, - channels as i32, - sample_rate as i32, - ) - .unwrap(), + pd: libpd_rs::Pd::init_and_configure(0, channels as i32, sample_rate as i32).unwrap(), output_stream, gravity: 0.8, bubbles: RefCell::new(vec![]), @@ -95,7 +90,7 @@ fn model(app: &App) -> Model { // Initially pd needs to know how many bubbles we have. // Because it will create adequate amount of voices for them. - libpd_rs::send::send_float_to("bubble_count", model.bubble_count as f32).unwrap(); + libpd_rs::functions::send::send_float_to("bubble_count", model.bubble_count as f32).unwrap(); // Run pd! model.pd.activate_audio(true).unwrap(); @@ -145,14 +140,14 @@ impl Model { // We hand over all tasks to our pd patch! fn audio_callback(_: &mut (), buffer: &mut Buffer) { let ticks = - libpd_rs::convenience::calculate_ticks(buffer.channels() as i32, buffer.len() as i32); - libpd_rs::process::process_float(ticks, &[], buffer); + libpd_rs::functions::util::calculate_ticks(buffer.channels() as i32, buffer.len() as i32); + libpd_rs::functions::process::process_float(ticks, &[], buffer); } // This is where we draw repeatedly! fn view(app: &App, model: &Model, frame: Frame) { // Let's poll pd messages here, for every frame. - libpd_rs::receive::receive_messages_from_pd(); + libpd_rs::functions::receive::receive_messages_from_pd(); let background_color = nannou::color::srgb8(238, 108, 77); diff --git a/src/convenience.rs b/src/convenience.rs deleted file mode 100644 index 03a5f06..0000000 --- a/src/convenience.rs +++ /dev/null @@ -1,539 +0,0 @@ -#![allow(dead_code)] - -use std::collections::HashMap; -use std::path::{Path, PathBuf}; -use tempfile::NamedTempFile; - -use crate::{ - error::{InitializationError, PatchLifeCycleError}, - types::{PatchFileHandle, ReceiverHandle}, -}; - -/// Activates audio in pd. -/// -/// # Errors -/// -/// A list of errors that can occur: -/// - [`SendError`](crate::error::SendError) -/// - [`MissingDestination`](crate::error::SendError::MissingDestination) -/// - [`SizeError`](crate::error::SizeError) -/// - [`TooLarge`](crate::error::SizeError::TooLarge) -/// -/// To match over these errors, you would need to downcast the returned error. -pub fn dsp_on() -> Result<(), Box> { - crate::send::start_message(1)?; - crate::send::add_float_to_started_message(1.0); - crate::send::finish_message_as_typed_message_and_send_to("pd", "dsp")?; - Ok(()) -} - -/// De-activates audio in pd. -/// -/// # Errors -/// -/// A list of errors that can occur: -/// - [`SendError`](crate::error::SendError) -/// - [`MissingDestination`](crate::error::SendError::MissingDestination) -/// - [`SizeError`](crate::error::SizeError) -/// - [`TooLarge`](crate::error::SizeError::TooLarge) -/// -/// To match over these errors, you would need to downcast the returned error. -pub fn dsp_off() -> Result<(), Box> { - crate::send::start_message(1)?; - crate::send::add_float_to_started_message(0.0); - crate::send::finish_message_as_typed_message_and_send_to("pd", "dsp")?; - Ok(()) -} - -/// Find the number of pd ticks according to the case. -/// -/// The calculation is `buffer_size / (block_size * channels)` -#[must_use] -#[allow(clippy::integer_division)] -pub fn calculate_ticks(channels: i32, buffer_size: i32) -> i32 { - let block_size = crate::block_size(); - buffer_size / (block_size * channels) -} - -/// An abstraction provided for convenience to track the state of pd and execute some common functions. -/// -/// Pd initializes globally. -/// -/// This is one of the reasons that there are more bare functions in this crate than data structures and abstractions. -/// This has some advantages and disadvantages. In the case of [`PdGlobal`] we can not fully trust the state we track here. -/// To trust it we need to not mix the bare functions which [`PdGlobal`] wraps and member functions of [`PdGlobal`] together. -/// -/// # Example of an unwanted mix -/// -/// ```rust -/// use libpd_rs::convenience::PdGlobal; -/// use libpd_rs::convenience::dsp_off; -/// -/// let mut pd = PdGlobal::init_and_configure(1, 2, 44100).unwrap(); -/// -/// // We call the member function of [`PdGlobal`] to activate audio -/// // which calls [`dsp_on`] internally which then sends a message -/// // to globally initialized pd to activate dsp. -/// pd.activate_audio(true).unwrap(); -/// -/// // So far so good. -/// assert_eq!(pd.audio_active(), true); -/// -/// // But we can send messages to globally initialized pd many ways -/// // and here is one of the ways we can do it. -/// dsp_off().unwrap(); -/// -/// // But now [`PdGlobal`] is not aware of the state -/// // of the globally initialized pd in the background. -/// // The information it holds is outdated and not true anymore. -/// assert_eq!(pd.audio_active(), true); -/// ``` -/// -/// To avoid this situation if you use [`PdGlobal`] check its member functions and only use them and **not** their bare counterparts. -/// -/// There are many bare functions in this crate which is not wrapped by [`PdGlobal`] and those are safe to use while using [`PdGlobal`] related functions. -pub struct PdGlobal { - audio_active: bool, - input_channels: i32, - output_channels: i32, - sample_rate: i32, - running_patch: Option, - temporary_evaluated_patch: Option, - /// A store to keep track of subscriptions which are made to senders in pd through the app lifecycle. - pub subscriptions: HashMap, - /// A store to keep track of paths which are added to pd search paths through the app lifecycle. - pub search_paths: Vec, -} - -impl PdGlobal { - /// Initializes pd globally. - /// - /// It calls [`init`](crate::init) and [`initialize_audio`](crate::initialize_audio) with the provided arguments and returns an instance of [`PdGlobal`] where a user can keep simple state and call some convenience methods. - /// It would be wise to call this function before anything pd related. - /// - /// Please use only one instance of this struct, because of how [`libpd`](https://github.com/libpd/libpd) is designed the underlying initialization is scoped globally. - /// - /// # Examples - /// ```rust - /// use libpd_rs::convenience::PdGlobal; - /// - /// let mut pd = PdGlobal::init_and_configure(1, 2, 44100).unwrap(); - /// ``` - /// - /// # Errors - /// - /// A list of errors that can occur: - /// - [`InitializationError`](crate::error::InitializationError) - /// - [`RingBufferInitializationError`](crate::error::InitializationError::RingBufferInitializationError) - /// - [`InitializationFailed`](crate::error::InitializationError::InitializationFailed) - /// - [`AudioInitializationError`](crate::error::AudioInitializationError) - /// - [`InitializationFailed`](crate::error::AudioInitializationError::InitializationFailed) - /// - /// To match over these errors, you would need to downcast the returned error. - pub fn init_and_configure( - input_channels: i32, - output_channels: i32, - sample_rate: i32, - ) -> Result> { - match crate::init() { - Ok(_) => (), - Err(err) => match err { - // Ignore re-initialization errors. - InitializationError::AlreadyInitialized => (), - err => return Err(err.into()), - }, - } - crate::initialize_audio(input_channels, output_channels, sample_rate)?; - Ok(Self { - audio_active: false, - input_channels, - output_channels, - sample_rate, - running_patch: None, - temporary_evaluated_patch: None, - subscriptions: HashMap::default(), - search_paths: vec![], - }) - } - - /// Adds a path to the list of paths where libpd searches in. - /// - /// Relative paths are relative to the current working directory. - /// Unlike the desktop pd application, **no** search paths are set by default. - /// - /// # Errors - /// - /// A list of errors that can occur: - /// - [`IoError`](crate::error::IoError) - /// - [`PathDoesNotExist`](crate::error::IoError::PathDoesNotExist) - /// - /// To match over these errors, you would need to downcast the returned error. - pub fn add_path_to_search_paths>( - &mut self, - path: T, - ) -> Result<(), Box> { - let path = path.as_ref().to_path_buf(); - if !self.search_paths.contains(&path) { - crate::add_to_search_paths(path.clone())?; - self.search_paths.push(path); - } - Ok(()) - } - - /// Adds many paths to the list of paths where libpd searches in. - /// - /// Relative paths are relative to the current working directory. - /// Unlike the desktop pd application, **no** search paths are set by default. - /// - /// # Errors - /// - /// A list of errors that can occur: - /// - [`IoError`](crate::error::IoError) - /// - [`PathDoesNotExist`](crate::error::IoError::PathDoesNotExist) - /// - /// To match over these errors, you would need to downcast the returned error. - pub fn add_paths_to_search_paths>( - &mut self, - paths: &[T], - ) -> Result<(), Box> { - for path in paths { - if !self.search_paths.contains(&path.as_ref().to_path_buf()) { - crate::add_to_search_paths(path)?; - self.search_paths.push(path.as_ref().to_path_buf()); - } - } - Ok(()) - } - - /// Clears all the paths where libpd searches for patches and assets. - pub fn clear_all_search_paths(&mut self) { - crate::clear_search_paths(); - self.search_paths.clear(); - } - - /// Closes a pd patch. - /// - /// # Errors - /// - /// A list of errors that can occur: - /// - [`PatchLifeCycleError`](crate::error::PatchLifeCycleError) - /// - [`FailedToClosePatch`](crate::error::PatchLifeCycleError::FailedToClosePatch) - /// - /// To match over these errors, you would need to downcast the returned error. - pub fn close_patch(&mut self) -> Result<(), Box> { - if let Some(handle) = self.running_patch.take() { - crate::close_patch(handle)?; - } - self.temporary_evaluated_patch.take(); - Ok(()) - } - - /// Opens a pd patch. - /// - /// The argument should be an absolute path to the patch file. - /// Absolute and relative paths are supported. - /// Relative paths and single file names are tried in executable directory and manifest directory. - /// - /// Tha function **first** checks the executable directory and **then** the manifest directory. - /// - /// # Examples - /// ```no_run - /// use libpd_rs::convenience::PdGlobal; - /// - /// let mut pd = PdGlobal::init_and_configure(1, 2, 44100).unwrap(); - /// assert!(pd.open_patch("tests/patches/sine.pd").is_ok()); - /// ``` - /// - /// # Errors - /// - /// A list of errors that can occur: - /// - [`PatchLifeCycleError`](crate::error::PatchLifeCycleError) - /// - [`FailedToClosePatch`](crate::error::PatchLifeCycleError::FailedToClosePatch) - /// - [`FailedToOpenPatch`](crate::error::PatchLifeCycleError::FailedToOpenPatch) - /// - [`PathDoesNotExist`](crate::error::PatchLifeCycleError::PathDoesNotExist) - /// - /// To match over these errors, you would need to downcast the returned error. - pub fn open_patch>( - &mut self, - path: T, - ) -> Result<(), Box> { - if self.running_patch.is_some() { - self.close_patch()?; - } - self.running_patch = Some(crate::open_patch(path)?); - Ok(()) - } - - /// Evaluate a string as a pd patch. - /// - /// This function creates a temporary file with the contents passed behind the scenes. - /// and saves it into the [`PdGlobal`] struct holding onto it until the patch is closed or the instantiated [`PdGlobal`] is dropped. - /// - /// Note: The patch opened after this evaluation could be closed safely with [`close_patch`](PdGlobal::close_patch). - /// - /// # Examples - /// ```rust - /// use libpd_rs::convenience::PdGlobal; - /// - /// let mut pd = PdGlobal::init_and_configure(1, 2, 44100).unwrap(); - /// - /// assert!(pd.eval_patch( - /// r#" - /// #N canvas 577 549 158 168 12; - /// #X obj 23 116 dac~; - /// #X obj 23 17 osc~ 440; - /// #X obj 23 66 *~ 0.1; - /// #X obj 81 67 *~ 0.1; - /// #X connect 1 0 2 0; - /// #X connect 1 0 3 0; - /// #X connect 2 0 0 0; - /// #X connect 3 0 0 1; - /// "# - /// ,).is_ok()); - /// ``` - /// - /// # Errors - /// - /// A list of errors that can occur: - /// - [`PatchLifeCycleError`](crate::error::PatchLifeCycleError) - /// - [`FailedToEvaluateAsPatch`](crate::error::PatchLifeCycleError::FailedToEvaluateAsPatch) - /// - [`FailedToClosePatch`](crate::error::PatchLifeCycleError::FailedToClosePatch) - /// - [`FailedToOpenPatch`](crate::error::PatchLifeCycleError::FailedToOpenPatch) - /// - [`PathDoesNotExist`](crate::error::PatchLifeCycleError::PathDoesNotExist) - /// - /// To match over these errors, you would need to downcast the returned error. - pub fn eval_patch>( - &mut self, - contents: T, - ) -> Result<(), Box> { - if self.running_patch.is_some() { - self.close_patch()?; - } - let temp_file = - NamedTempFile::new().map_err(|err| PatchLifeCycleError::FailedToEvaluateAsPatch { - content: contents.as_ref().to_owned(), - msg: err.to_string(), - })?; - std::fs::write(temp_file.path(), contents.as_ref()).map_err(|err| { - PatchLifeCycleError::FailedToEvaluateAsPatch { - content: contents.as_ref().to_owned(), - msg: err.to_string(), - } - })?; - self.running_patch = Some(crate::open_patch(temp_file.path())?); - self.temporary_evaluated_patch = Some(temp_file); - Ok(()) - } - - /// Starts listening messages from a source. - /// - /// If the source is already being listened to, this function will early return not doing anything without an error. - /// - /// # Examples - /// ```no_run - /// use libpd_rs::convenience::PdGlobal; - /// - /// let mut pd = PdGlobal::init_and_configure(1, 2, 44100).unwrap(); - /// pd.open_patch("tests/patches/sine.pd").unwrap(); - /// pd.subscribe_to("sender").unwrap(); - /// ``` - /// - /// # Errors - /// - /// A list of errors that can occur: - /// - [`SubscriptionError`](crate::error::SubscriptionError) - /// - [`FailedToSubscribeToSender`](crate::error::SubscriptionError::FailedToSubscribeToSender) - /// - /// To match over these errors, you would need to downcast the returned error. - pub fn subscribe_to>( - &mut self, - source: T, - ) -> Result<(), Box> { - if self.subscriptions.contains_key(source.as_ref()) { - return Ok(()); - } - self.subscriptions.insert( - source.as_ref().to_owned(), - crate::receive::start_listening_from(source.as_ref())?, - ); - Ok(()) - } - - /// Starts listening messages from many source. - /// - /// If the any source is already being listened to, this function will will ignore them. - /// - /// # Examples - /// ```no_run - /// use libpd_rs::convenience::PdGlobal; - /// - /// let mut pd = PdGlobal::init_and_configure(1, 2, 44100).unwrap(); - /// pd.open_patch("tests/patches/sine.pd").unwrap(); - /// pd.subscribe_to_many(&["sender", "other_sender"]).unwrap(); - /// ``` - /// - /// # Errors - /// - /// A list of errors that can occur: - /// - [`SubscriptionError`](crate::error::SubscriptionError) - /// - [`FailedToSubscribeToSender`](crate::error::SubscriptionError::FailedToSubscribeToSender) - /// - /// To match over these errors, you would need to downcast the returned error. - pub fn subscribe_to_many>( - &mut self, - sources: &[T], - ) -> Result<(), Box> { - for source in sources { - if self.subscriptions.contains_key(source.as_ref()) { - continue; - } - self.subscriptions.insert( - source.as_ref().to_owned(), - crate::receive::start_listening_from(source.as_ref())?, - ); - } - Ok(()) - } - - /// Stops listening messages from a source. - /// - /// # Examples - /// ```no_run - /// use libpd_rs::convenience::PdGlobal; - /// - /// let mut pd = PdGlobal::init_and_configure(1, 2, 44100).unwrap(); - /// pd.open_patch("tests/patches/sine.pd").unwrap(); - /// pd.subscribe_to("sender").unwrap(); - /// pd.unsubscribe_from("sender"); - /// ``` - pub fn unsubscribe_from>(&mut self, source: T) { - if let Some(handle) = self.subscriptions.remove(source.as_ref()) { - crate::receive::stop_listening_from(handle); - } - } - - /// Stops listening messages from many sources. - /// - /// # Examples - /// ```no_run - /// use libpd_rs::convenience::PdGlobal; - /// - /// let mut pd = PdGlobal::init_and_configure(1, 2, 44100).unwrap(); - /// pd.open_patch("tests/patches/sine.pd").unwrap(); - /// pd.subscribe_to_many(&["sender", "other_sender"]).unwrap(); - /// - /// pd.unsubscribe_from_many(&["sender", "other_sender"]); - /// ``` - pub fn unsubscribe_from_many>(&mut self, sources: &[T]) { - for source in sources { - if let Some(handle) = self.subscriptions.remove(source.as_ref()) { - crate::receive::stop_listening_from(handle); - } - } - } - - /// Stops listening from all sources. - /// - /// # Examples - /// ```no_run - /// use libpd_rs::convenience::PdGlobal; - /// - /// let mut pd = PdGlobal::init_and_configure(1, 2, 44100).unwrap(); - /// pd.open_patch("tests/patches/sine.pd").unwrap(); - /// pd.subscribe_to_many(&["sender", "other_sender"]).unwrap(); - /// - /// pd.unsubscribe_from_all(); - /// ``` - pub fn unsubscribe_from_all(&mut self) { - let sources: Vec = self.subscriptions.keys().cloned().collect(); - for source in &sources { - if let Some(handle) = self.subscriptions.remove(source) { - crate::receive::stop_listening_from(handle); - } - } - } - - /// Gets the `$0` of the running patch. - /// - /// `$0` id in pd could be thought as a auto generated unique identifier for the patch. - /// - /// # Errors - /// - /// A list of errors that can occur: - /// - [`PatchLifeCycleError`](crate::error::PatchLifeCycleError) - /// - [`PatchIsNotOpen`](crate::error::PatchLifeCycleError::PatchIsNotOpen) - /// - /// To match over these errors, you would need to downcast the returned error. - pub fn dollar_zero(&self) -> Result> { - if let Some(ref patch) = self.running_patch { - let dollar_zero = crate::get_dollar_zero(patch)?; - return Ok(dollar_zero); - } - Err(Box::new(PatchLifeCycleError::PatchIsNotOpen)) - } - - /// Checks if the audio is active. - /// - /// The state is tracked by [`PdGlobal`]. - /// - /// If messages sent to pd previously to activate or de-activate audio not using methods provided in this struct. - /// This state might be false. - #[must_use] - pub const fn audio_active(&self) -> bool { - self.audio_active - } - - /// Activates or deactivates audio in pd. - /// - /// # Errors - /// - /// A list of errors that can occur: - /// - [`SendError`](crate::error::SendError) - /// - [`MissingDestination`](crate::error::SendError::MissingDestination) - /// - [`SizeError`](crate::error::SizeError) - /// - [`TooLarge`](crate::error::SizeError::TooLarge) - /// - /// To match over these errors, you would need to downcast the returned error. - pub fn activate_audio(&mut self, on: bool) -> Result<(), Box> { - if on && !self.audio_active { - dsp_on()?; - self.audio_active = true; - } else if !on && self.audio_active { - dsp_off()?; - self.audio_active = false; - } else { - return Ok(()); - } - Ok(()) - } - - /// Gets the sample rate which pd is configured with. - /// - /// The state is tracked by [`PdGlobal`]. - /// - /// If anything else changes the internal state of pd it will not be reflected in this struct. - #[must_use] - pub const fn sample_rate(&self) -> i32 { - self.sample_rate - } - - /// Gets the number of input channels which pd is configured with. - /// - /// The state is tracked by [`PdGlobal`]. - /// - /// If anything else changes the internal state of pd it will not be reflected in this struct. - #[must_use] - pub const fn input_channels(&self) -> i32 { - self.input_channels - } - - /// Gets the number of output channels which pd is configured with. - /// - /// The state is tracked by [`PdGlobal`]. - /// - /// If anything else changes the internal state of pd it will not be reflected in this struct. - #[must_use] - pub const fn output_channels(&self) -> i32 { - self.output_channels - } -} diff --git a/src/error.rs b/src/error.rs index 6d78949..73536d1 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,12 +1,62 @@ +use std::ffi::NulError; + use thiserror::Error; +#[expect(dead_code, reason = "We might use this in the future.")] +pub(crate) const C_STRING_FAILURE: &str = + "Provided an invalid CString, check if your string contains null bytes in the middle."; +pub(crate) const C_STR_FAILURE: &str = "Converting a CStr to an &str is failed."; + +/// The umbrella error type for libpd-rs. +#[non_exhaustive] +#[derive(Error, Debug)] +pub enum PdError { + /// An error occurred during initialization. + #[error(transparent)] + InitializationError(#[from] InitializationError), + /// An error occurred during audio initialization. + #[error(transparent)] + AudioInitializationError(#[from] AudioInitializationError), + /// An error occurred during patch lifecycle. + #[error(transparent)] + PatchLifeCycleError(#[from] PatchLifeCycleError), + /// An error occurred during gui lifecycle. + #[error(transparent)] + GuiLifeCycleError(#[from] GuiLifeCycleError), + /// An error occurred during general filesystem access. + #[error(transparent)] + IoError(#[from] IoError), + /// An error occurred during sending messages to a pd patch. + #[error(transparent)] + SendError(#[from] SendError), + /// An error occurred during subscription to senders in a pd patch. + #[error(transparent)] + SubscriptionError(#[from] SubscriptionError), + /// An error occurred related to sizes of entities. + #[error(transparent)] + SizeError(#[from] SizeError), + /// An error occurred related to pd arrays. + #[error(transparent)] + ArrayError(#[from] ArrayError), + /// An error occurred related to pd instances. + #[error(transparent)] + InstanceError(#[from] InstanceError), + /// An error occurred related to string conversion. + /// + /// `CString` or `CStr` conversion error. + #[error(transparent)] + StringConversionError(#[from] StringConversionError), +} + /// Errors related to initialization. #[non_exhaustive] #[derive(Error, Debug)] pub enum InitializationError { + // We are deprecating this error. /// pd could not be initialized two times. - #[error("Pure Data is already initialized.")] - AlreadyInitialized, + // #[error("Pure Data is already initialized.")] + // AlreadyInitialized, + /// Error in initializing ring buffers. #[error("Failed to initialize ring buffers which are needed for the message queue.")] RingBufferInitializationError, @@ -43,6 +93,11 @@ pub enum PatchLifeCycleError { /// The path to the patch which are being tried to open is invalid. #[error("The path you have provided does not exist in the file system. Path: {0}")] PathDoesNotExist(String), + /// An error occurred related to string conversion. + /// + /// `CString` or `CStr` conversion error. + #[error(transparent)] + StringConversion(#[from] StringConversionError), } /// Errors related to a lifecycle of a pd gui. @@ -52,6 +107,11 @@ pub enum GuiLifeCycleError { /// Failed to open gui, most probably because the path is invalid to the pd binary. #[error("Failed to open gui, please provide a valid path to the pd binary.")] FailedToOpenGui, + /// An error occurred related to string conversion. + /// + /// `CString` or `CStr` conversion error. + #[error(transparent)] + StringConversion(#[from] StringConversionError), } /// Errors related to general filesystem access. @@ -61,6 +121,11 @@ pub enum IoError { /// The path to the patch which are being tried to open is invalid. #[error("The path you have provided does not exist in the file system. Path: {0}")] PathDoesNotExist(String), + /// An error occurred related to string conversion. + /// + /// `CString` or `CStr` conversion error. + #[error(transparent)] + StringConversion(#[from] StringConversionError), } /// Errors related to sending messages to a pd patch. @@ -73,6 +138,11 @@ pub enum SendError { /// A general error when the values which you are sending to the receiver are out of range. #[error("Values which are being sent are out of range.")] OutOfRange, + /// An error occurred related to string conversion. + /// + /// `CString` or `CStr` conversion error. + #[error(transparent)] + StringConversion(#[from] StringConversionError), } /// Errors related to subscription to senders in a pd patch. @@ -82,6 +152,11 @@ pub enum SubscriptionError { /// A failure of subscription to a sender with an unknown reason. #[error("Failed to subscribe to sender: `{0}` in loaded pd patch.")] FailedToSubscribeToSender(String), + /// An error occurred related to string conversion. + /// + /// `CString` or `CStr` conversion error. + #[error(transparent)] + StringConversion(#[from] StringConversionError), } /// Errors related to sizes of entities. @@ -94,6 +169,11 @@ pub enum SizeError { /// Could not determine the size of the entity. #[error("Could not determine the size.")] CouldNotDetermine, + /// An error occurred related to string conversion. + /// + /// `CString` or `CStr` conversion error. + #[error(transparent)] + StringConversion(#[from] StringConversionError), } /// Errors related to pd arrays. @@ -106,4 +186,45 @@ pub enum ArrayError { /// The position in the array which is tried to be written is out of bounds. #[error("The position in array which you're trying to write is out of bounds.")] OutOfBounds, + /// An error occurred related to string conversion. + /// + /// `CString` or `CStr` conversion error. + #[error(transparent)] + StringConversion(#[from] StringConversionError), +} + +/// Errors related to pd instances. +#[non_exhaustive] +#[derive(Error, Debug)] +pub enum InstanceError { + /// The instance which is being tried to be created already exists. + #[error("The instance {0} already created and exists.")] + InstanceAlreadyExists(String), + /// The instance which is being tried to be accessed does not exist. + #[error("The instance {0} does not exist.")] + InstanceDoesNotExist(String), + /// The instance which is being tried to be accessed does not exist. + #[error("The instance failed to create. Error: {0}")] + InstanceFailedToCreate(String), +} + +/// Errors related to string conversion. +/// +/// `CString` or `CStr` conversion error. +#[non_exhaustive] +#[derive(Error, Debug)] +pub enum StringConversionError { + /// An interior null byte was found in the middle while converting to `CString` or `CStr`. + #[error("An interior null byte was found at position {pos} while converting to CString. Original string: '{string}' (bytes: {bytes:?})", + pos = .0, + string = String::from_utf8_lossy(.1), + bytes = .1 + )] + NullTerminatedString(usize, Vec), +} + +impl From for StringConversionError { + fn from(err: NulError) -> Self { + Self::NullTerminatedString(err.nul_position(), err.into_vec()) + } } diff --git a/src/functions.rs b/src/functions.rs new file mode 100644 index 0000000..ff4d473 --- /dev/null +++ b/src/functions.rs @@ -0,0 +1,768 @@ +/// Audio processing +/// +/// Process functions which you call in your audio callback are collected here. +/// +/// These functions also run the scheduler of pd. The chosen function needs to be called in a loop to keep pd "running". +/// +/// # Examples +/// +/// ```rust +/// use libpd_rs::{ +/// functions::{ +/// close_patch, +/// initialize_audio, open_patch, +/// process::process_float, +/// util::{dsp_on, calculate_ticks}, +/// receive::receive_messages_from_pd, +/// }, +/// instance::PdInstance, +/// }; +/// +/// fn main() -> Result<(), Box> { +/// // Automatically initializes the pd instance and sets it as the main instance for the thread. +/// let mut main_instance = PdInstance::new()?; +/// +/// initialize_audio(1, 2, 44100)?; +/// +/// // Place your own patch here. +/// let patch_handle = open_patch("tests/patches/sine.pd")?; +/// +/// // Turn the dsp on +/// dsp_on()?; +/// +/// // Process the audio (imagine that this is your audio callback) +/// // We're going to treat this separate thread as the +/// // high priority audio callback from the OS for this example. +/// +/// // Open some channels to communicate with it. +/// let (tx, rx) = std::sync::mpsc::channel::<()>(); +/// +/// let mut instance_in_callback = main_instance.clone(); +/// +/// let handle = std::thread::spawn(move || { +/// // Mimic audio callback buffers. +/// let input_buffer = [0.0f32; 512]; +/// let mut output_buffer = [0.0f32; 1024]; +/// +/// let output_channels = 2; +/// let sample_rate = 44100; +/// +/// // Pd instances are tracked in a thread local way so we need to set it as the current instance for this thread. +/// // Handle the error properly in real life. +/// instance_in_callback.set_as_current(); +/// +/// // Run pd +/// loop { +/// // Mimic the call frequency of the audio callback. +/// let approximate_buffer_duration = +/// (output_buffer.len() as f32 / sample_rate as f32) * 1000.0; +/// std::thread::sleep(std::time::Duration::from_millis( +/// approximate_buffer_duration as u64, +/// )); +/// +/// // We may call this to also receive from internal ring buffers in this loop. +/// // So our registered listeners receive messages. +/// receive_messages_from_pd(); +/// +/// // Calculate ticks for the internal scheduler +/// let ticks = calculate_ticks(output_buffer.len() as i32, output_channels); +/// +/// // Process the audio and advance the internal scheduler by the number of ticks. +/// process_float(ticks, &input_buffer, &mut output_buffer); +/// +/// // This is just meaningful for this example, +/// // so we can break from this loop. +/// match rx.try_recv() { +/// Ok(_) => break, +/// _ => continue, +/// } +/// } +/// }); +/// +/// // When processing starts pd becomes alive because the scheduler is running. +/// +/// // After some time +/// std::thread::sleep(std::time::Duration::from_millis(1000)); +/// +/// // We may join the thread. +/// tx.send(()).unwrap(); +/// handle.join().unwrap(); +/// +/// // And close the patch +/// close_patch(patch_handle)?; +/// +/// Ok(()) +/// } +/// ``` +pub mod process; + +/// Send messages to pd +/// +/// Collection of functions where messages may be sent to pd +/// +/// # Examples +/// ```rust +/// use libpd_rs::{ +/// functions::{ +/// close_patch, +/// init, initialize_audio, open_patch, +/// process::process_float, +/// util::{dsp_on, calculate_ticks}, +/// receive::{receive_messages_from_pd, on_print, on_float, start_listening_from}, +/// send::{send_float_to}, +/// }, +/// instance::PdInstance, +/// }; +/// +/// fn main() -> Result<(), Box> { +/// // Automatically initializes the pd instance and sets it as the main instance for the thread. +/// let mut main_instance = PdInstance::new()?; +/// +/// initialize_audio(1, 2, 44100)?; +/// +/// // Place your own patch here. +/// let patch_handle = open_patch("tests/patches/echo.pd")?; +/// +/// // Turn the dsp on +/// dsp_on()?; +/// +/// // Register some listeners +/// // Print is a special one which is always listened from. +/// on_print(|value| { +/// println!("{value}"); +/// }); +/// +/// on_float(|source, value| { +/// assert_eq!(source, "float_from_pd"); +/// assert_eq!(value, 42.0); +/// println!("{value} received from {source}"); +/// }); +/// +/// // For others we need to register them. +/// start_listening_from("float_from_pd")?; +/// +/// // We're going to treat this separate thread as the +/// // high priority audio callback from the OS for this example. +/// +/// // Open some channels to communicate with it. +/// let (tx, rx) = std::sync::mpsc::channel::<()>(); +/// +/// let mut instance_in_callback = main_instance.clone(); +/// +/// let handle = std::thread::spawn(move || { +/// // Mimic audio callback buffers. +/// let input_buffer = [0.0f32; 512]; +/// let mut output_buffer = [0.0f32; 1024]; +/// +/// let output_channels = 2; +/// let sample_rate = 44100; +/// +/// // Pd instances are tracked in a thread local way so we need to set it as the current instance for this thread. +/// // Handle the error properly in real life. +/// instance_in_callback.set_as_current(); +/// +/// // Run pd +/// loop { +/// // Mimic the call frequency of the audio callback. +/// let approximate_buffer_duration = +/// (output_buffer.len() as f32 / sample_rate as f32) * 1000.0; +/// std::thread::sleep(std::time::Duration::from_millis( +/// approximate_buffer_duration as u64, +/// )); +/// +/// // Receive messages from pd. +/// receive_messages_from_pd(); +/// +/// // Calculate ticks for the internal scheduler +/// let ticks = calculate_ticks(output_buffer.len() as i32, output_channels); +/// +/// // Process the audio and advance the internal scheduler by the number of ticks. +/// process_float(ticks, &input_buffer, &mut output_buffer); +/// +/// // This is just meaningful for this example, +/// // so we can break from this loop. +/// match rx.try_recv() { +/// Ok(_) => break, +/// _ => continue, +/// } +/// } +/// }); +/// +/// // When processing starts pd becomes alive because the scheduler is running. +/// +/// // Let's send a float to pd, it will be caught by the float listener, +/// // because our patch which we've loaded, echoes it back. +/// send_float_to("float_from_rust", 42.0)?; +/// +/// // After some time +/// std::thread::sleep(std::time::Duration::from_millis(1000)); +/// +/// // We may join the thread. +/// tx.send(()).unwrap(); +/// handle.join().unwrap(); +/// +/// // And close the patch +/// close_patch(patch_handle)?; +/// +/// Ok(()) +/// } +/// ``` +pub mod send; + +/// Receive messages from pd +/// +/// Collection of endpoints where listeners (hooks) may be registered to receive messages from pd. +/// +/// # Examples +/// ```rust +/// use libpd_rs::{ +/// functions::{ +/// close_patch, +/// initialize_audio, open_patch, +/// process::process_float, +/// util::{dsp_on, calculate_ticks}, +/// receive::{receive_messages_from_pd, on_print, on_float, start_listening_from}, +/// }, +/// instance::PdInstance, +/// }; +/// +/// fn main() -> Result<(), Box> { +/// // Automatically initializes the pd instance and sets it as the main instance for the thread. +/// let mut main_instance = PdInstance::new()?; +/// +/// initialize_audio(1, 2, 44100)?; +/// +/// // Place your own patch here. +/// let patch_handle = open_patch("tests/patches/echo.pd")?; +/// +/// // Turn the dsp on +/// dsp_on()?; +/// +/// // Register some listeners +/// // Print is a special one which is always listened from. +/// on_print(|value| { +/// println!("{value}"); +/// }); +/// +/// on_float(|source, value| { +/// println!("{value} received from {source}"); +/// }); +/// +/// // For others we need to register them. +/// start_listening_from("float_from_pd")?; +/// +/// // We're going to treat this separate thread as the +/// // high priority audio callback from the OS for this example. +/// +/// // Open some channels to communicate with it. +/// let (tx, rx) = std::sync::mpsc::channel::<()>(); +/// +/// let mut instance_in_callback = main_instance.clone(); +/// +/// let handle = std::thread::spawn(move || { +/// // Mimic audio callback buffers. +/// let input_buffer = [0.0f32; 512]; +/// let mut output_buffer = [0.0f32; 1024]; +/// +/// let output_channels = 2; +/// let sample_rate = 44100; +/// +/// // Pd instances are tracked in a thread local way so we need to set it as the current instance for this thread. +/// // Handle the error properly in real life. +/// instance_in_callback.set_as_current(); +/// +/// // Run pd +/// loop { +/// // Mimic the call frequency of the audio callback. +/// let approximate_buffer_duration = +/// (output_buffer.len() as f32 / sample_rate as f32) * 1000.0; +/// std::thread::sleep(std::time::Duration::from_millis( +/// approximate_buffer_duration as u64, +/// )); +/// +/// // Receive messages from pd. +/// receive_messages_from_pd(); +/// +/// // Calculate ticks for the internal scheduler +/// let ticks = calculate_ticks(output_buffer.len() as i32, output_channels); +/// +/// // Process the audio and advance the internal scheduler by the number of ticks. +/// process_float(ticks, &input_buffer, &mut output_buffer); +/// +/// // This is just meaningful for this example, +/// // so we can break from this loop. +/// match rx.try_recv() { +/// Ok(_) => break, +/// _ => continue, +/// } +/// } +/// }); +/// +/// // When processing starts pd becomes alive because the scheduler is running. +/// +/// // After some time +/// std::thread::sleep(std::time::Duration::from_millis(1000)); +/// +/// // We may join the thread. +/// tx.send(()).unwrap(); +/// handle.join().unwrap(); +/// +/// // And close the patch +/// close_patch(patch_handle)?; +/// +/// Ok(()) +/// } +/// ``` +pub mod receive; + +/// Work with pd arrays +/// +/// This module provides all tools to work with pd named arrays which are exposed by libpd with some extra safety such as bounds checking. +/// +/// Corresponding libpd functions in [libpd repository](https://github.com/libpd/libpd) could be explored [here](https://github.com/libpd/libpd/blob/master/libpd_wrapper/z_libpd.h#L115). +/// +/// # Examples +/// +/// ```rust +/// use libpd_rs::{ +/// functions::{ +/// array::{array_size, read_float_array_from, resize_array, write_float_array_to}, +/// close_patch, +/// initialize_audio, open_patch, +/// }, +/// instance::PdInstance, +/// }; +/// +/// fn main() -> Result<(), Box> { +/// // Automatically initializes the pd instance and sets it as the main instance for the thread. +/// let mut main_instance = PdInstance::new()?; +/// +/// initialize_audio(1, 2, 44100)?; +/// +/// // Place your own patch here. +/// let handle = open_patch("tests/patches/array_sketch_pad.pd")?; +/// +/// let size = array_size("sketch_pad")?; +/// +/// // Arrays are sized to 100 by default. +/// assert_eq!(size, 100); +/// +/// // We can resize this array to 8. +/// resize_array("sketch_pad", 8)?; +/// +/// let size = array_size("sketch_pad")?; +/// assert_eq!(size, 8); +/// +/// // Let's write some stuff to our array. +/// write_float_array_to("sketch_pad", 0, &[1.0, 2.0, 3.0, 4.0], 4)?; +/// +/// // Let's overwrite the second part of the array with the first part of our slice. +/// write_float_array_to("sketch_pad", 2, &[500.0, 600.0, 3.0, 4.0], 2)?; +/// +/// // Now we can read the array to the second half of our slice. +/// let mut read_to: Vec = vec![0.0; 4]; +/// +/// // Read it all back. +/// read_float_array_from("sketch_pad", 0, 4, &mut read_to)?; +/// assert_eq!(read_to, [1.0, 2.0, 500.0, 600.0]); +/// +/// // Now we can read the second half of our array to our first half of our slice. +/// read_float_array_from("sketch_pad", 2, 2, &mut read_to)?; +/// assert_eq!(read_to, [500.0, 600.0, 500.0, 600.0]); +/// +/// close_patch(handle)?; +/// +/// Ok(()) +/// } +/// ``` +pub mod array; + +/// Start, stop, poll pd gui +/// +/// This module provides functions to start, stop and poll pd gui. +/// +/// If the pd desktop application is installed in your computer. +/// You may use these functions to launch or quit it. +pub mod gui; + +use crate::error::{AudioInitializationError, PatchLifeCycleError, StringConversionError}; + +use crate::{ + error::{InitializationError, IoError}, + types::PatchFileHandle, +}; + +use std::ffi::{self, CString}; +use std::path::{Path, PathBuf}; +use std::{env, path}; + +/// Initializes libpd. +/// +/// Initializes the currently set pd instance for the thread also initializes ring buffers for internal message passing. +/// Call this function as early as possible after either creating or setting the pd instance. +/// +/// After setting internal hooks, it initializes `libpd` by calling the underlying +/// C function which is `libpd_queued_init` which could be found in `z_queued.c` in the libpd repository. +/// +/// **Notes**: +/// +/// - It is safe to call this function more than once. +/// - You would need to call this function once for each instance you create. +/// Using `PdInstance::new()` is recommended since it automatically initializes the pd instance. +/// - `libpd-rs` is compiled with multi instance support. So this function only effects the current set instance for the thread. +/// +/// # Example +/// ```rust +/// use libpd_rs::functions::init; +/// +/// assert_eq!(init().is_ok(), true); +/// assert_eq!(init().is_ok(), true); +/// ``` +/// +/// # Errors +/// +/// A list of errors that can occur: +/// - [`RingBufferInitializationError`](crate::error::InitializationError::RingBufferInitializationError) +/// - [`InitializationFailed`](crate::error::InitializationError::InitializationFailed) +pub fn init() -> Result<(), InitializationError> { + unsafe { + match libpd_sys::libpd_queued_init() { + // Returns -1 if it is already initialized which we ignore. + 0 | -1 => Ok(()), + -2 => Err(InitializationError::RingBufferInitializationError), + _ => Err(InitializationError::InitializationFailed), + } + } +} + +/// Frees the internal queued ring buffers. +/// +/// This function needs to be called after you're done with the pd instance before freeing it. +/// +/// Using [`PdInstance`](crate::instance::PdInstance) is recommended since it automatically frees the pd instance +/// and calls this function in its `Drop` implementation. +/// +/// From libpd source code: +/// +/// ```c +/// /// free the queued ringbuffers +/// /// with multiple instances, call before freeing each instance: +/// /// libpd_set_instance(pd1); +/// /// libpd_queued_release(); +/// /// libpd_free_instance(pd1); +/// ``` +pub fn release_internal_queues() { + unsafe { + libpd_sys::libpd_queued_release(); + }; +} + +/// Clears all the paths where libpd searches for patches and assets. +/// +/// Initializing an instance also clears the search paths. +pub fn clear_search_paths() { + unsafe { + libpd_sys::libpd_clear_search_path(); + } +} + +/// Adds a path to the list of paths where pd searches in. +/// +/// Relative paths are relative to the current working directory. +/// +/// Unlike the desktop pd application, **no** search paths are set by default. +/// +/// # Errors +/// +/// A list of errors that can occur: +/// - [`PathDoesNotExist`](crate::error::IoError::PathDoesNotExist) +/// - [`StringConversion`](crate::error::IoError::StringConversion) +pub fn add_to_search_paths>(path: T) -> Result<(), IoError> { + if !path.as_ref().exists() { + return Err(IoError::PathDoesNotExist( + path.as_ref().to_string_lossy().to_string(), + )); + } + unsafe { + let c_path = + CString::new(&*path.as_ref().to_string_lossy()).map_err(StringConversionError::from)?; + libpd_sys::libpd_add_to_search_path(c_path.as_ptr()); + Ok(()) + } +} + +/// Opens a pd patch. +/// +/// The argument should be an absolute path to the patch file. +/// It would be useful to keep the return value of this function. +/// It can be used later to close it. +/// Absolute and relative paths are supported. +/// Relative paths and single file names are tried in executable directory and manifest directory. +/// +/// Tha function **first** checks the executable directory and **then** the manifest directory. +/// +/// # Examples +/// ```no_run +/// use libpd_rs::functions::open_patch; +/// use std::path::PathBuf; +/// +/// let absolute_path = PathBuf::from("/home/user/my_patch.pd"); +/// let relative_path = PathBuf::from("../my_patch.pd"); +/// let patch_name = PathBuf::from("my_patch.pd"); +/// +/// let patch_handle = open_patch(&patch_name).unwrap(); +/// // or others.. +/// ``` +/// +/// # Errors +/// +/// A list of errors that can occur: +/// - [`FailedToOpenPatch`](crate::error::PatchLifeCycleError::FailedToOpenPatch) +/// - [`PathDoesNotExist`](crate::error::PatchLifeCycleError::PathDoesNotExist) +/// - [`StringConversion`](crate::error::PatchLifeCycleError::StringConversion) +pub fn open_patch>( + path_to_patch: T, +) -> Result { + let file_name = path_to_patch + .as_ref() + .file_name() + .ok_or(PatchLifeCycleError::FailedToOpenPatch)?; + let file_name = file_name.to_string_lossy(); + let file_name = file_name.as_ref(); + let parent_path = path_to_patch + .as_ref() + .parent() + .unwrap_or_else(|| path::Path::new("/")); + let parent_path_string: String = parent_path.to_string_lossy().into(); + + // Assume absolute path. + let mut directory: String = parent_path_string.clone(); + + if !parent_path.is_absolute() { + // "../some.pd" --> prepend working directory + if parent_path.is_relative() { + let mut app_dir = env::current_exe() + .map_err(|_| -> PatchLifeCycleError { PatchLifeCycleError::FailedToOpenPatch })?; + app_dir.pop(); + app_dir.push(parent_path); + let parent_path_str = app_dir.to_string_lossy(); + + if app_dir.exists() { + directory = parent_path_str.into(); + } else { + let manifest_dir = + PathBuf::from(&std::env!("CARGO_MANIFEST_DIR")).join(parent_path); + // Try manifest dir. + let manifest_dir_str = manifest_dir.to_string_lossy(); + directory = manifest_dir_str.into(); + } + } + // "some.pd" --> prepend working directory + if parent_path_string.is_empty() { + let mut app_dir = env::current_exe() + .map_err(|_| -> PatchLifeCycleError { PatchLifeCycleError::FailedToOpenPatch })?; + app_dir.pop(); + app_dir.push(file_name); + let parent_path_str = app_dir.to_string_lossy(); + + if app_dir.exists() { + directory = parent_path_str.into(); + } else { + // Try manifest dir. + directory = std::env!("CARGO_MANIFEST_DIR").into(); + } + } + } + + // Invalid path. + let calculated_patch_path = PathBuf::from(&directory).join(file_name); + if !calculated_patch_path.exists() { + return Err(PatchLifeCycleError::PathDoesNotExist( + calculated_patch_path.to_string_lossy().to_string(), + )); + } + + // All good. + unsafe { + let name = CString::new(file_name).map_err(StringConversionError::from)?; + let directory = CString::new(directory).map_err(StringConversionError::from)?; + + let file_handle = + libpd_sys::libpd_openfile(name.as_ptr(), directory.as_ptr()).cast::(); + + if file_handle.is_null() { + return Err(PatchLifeCycleError::FailedToOpenPatch); + } + Ok(file_handle.into()) + } +} + +/// Closes a pd patch which has opened before. +/// +/// Handle needs to point to a valid opened patch file. +/// +/// # Examples +/// ```no_run +/// use std::path::PathBuf; +/// use libpd_rs::functions::{open_patch, close_patch}; +/// +/// let patch = PathBuf::from("my_patch.pd"); +/// let patch_handle = open_patch(&patch).unwrap(); +/// +/// assert!(close_patch(patch_handle).is_ok()); +/// ``` +/// # Errors +/// +/// A list of errors that can occur: +/// - [`FailedToClosePatch`](crate::error::PatchLifeCycleError::FailedToClosePatch) +pub fn close_patch(handle: PatchFileHandle) -> Result<(), PatchLifeCycleError> { + unsafe { + let ptr = handle.into_inner(); + if ptr.is_null() { + Err(PatchLifeCycleError::FailedToClosePatch) + } else { + libpd_sys::libpd_closefile(ptr); + Ok(()) + } + } +} + +/// Gets the `$0` of the running patch. +/// +/// `$0` id in pd could be thought as a auto generated unique identifier number for the patch. +/// +/// # Errors +/// +/// A list of errors that can occur: +/// - [`PatchIsNotOpen`](crate::error::PatchLifeCycleError::PatchIsNotOpen) +pub fn get_dollar_zero(handle: &PatchFileHandle) -> Result { + unsafe { + match libpd_sys::libpd_getdollarzero(handle.as_mut_ptr()) { + 0 => Err(PatchLifeCycleError::PatchIsNotOpen), + other => Ok(other), + } + } +} + +/// Returns pd's fixed block size which is `64` by default. +/// +/// The number of frames per 1 pd tick. +/// +/// For every pd tick, pd will process frames by the amount of block size. +/// e.g. this would make 128 samples if we have a stereo output and the default block size. +/// +/// It will first process the input buffers and then will continue with the output buffers. +/// Check the [`PROCESS`](https://github.com/libpd/libpd/blob/master/libpd_wrapper/z_libpd.c#L177) macro in `libpd` [source](https://github.com/libpd/libpd/blob/master/libpd_wrapper) for more information. +/// +/// # Examples +/// +/// ```rust +/// use libpd_rs::functions::block_size; +/// +/// let block_size = block_size(); +/// let output_channels = 2; +/// let buffer_size = 1024; +/// +/// // Calculate pd ticks according to the upper information +/// let pd_ticks = buffer_size / (block_size * output_channels); +/// +/// // If we know about pd_ticks, then we can also calculate the buffer size, +/// assert_eq!(buffer_size, pd_ticks * (block_size * output_channels)); +/// ``` +#[must_use] +pub fn block_size() -> i32 { + unsafe { libpd_sys::libpd_blocksize() } +} + +/// Initializes audio rendering +/// +/// This doesn't mean that the audio is actually playing. +/// To start audio processing please call [`dsp_on`](crate::util::dsp_on) function after the initialization. +/// +/// # Errors +/// +/// A list of errors that can occur: +/// - [`InitializationFailed`](crate::error::AudioInitializationError::InitializationFailed) +pub fn initialize_audio( + input_channels: i32, + output_channels: i32, + sample_rate: i32, +) -> Result<(), AudioInitializationError> { + unsafe { + match libpd_sys::libpd_init_audio(input_channels, output_channels, sample_rate) { + 0 => Ok(()), + _ => Err(AudioInitializationError::InitializationFailed), + } + } +} + +/// Sets the flag for the functionality of verbose printing to the pd console +pub fn verbose_print_state(active: bool) { + if active { + unsafe { libpd_sys::libpd_set_verbose(1) } + } else { + unsafe { libpd_sys::libpd_set_verbose(0) } + } +} + +/// Checks if verbose printing functionality to the pd console is active +#[must_use] +pub fn verbose_print_state_active() -> bool { + unsafe { libpd_sys::libpd_get_verbose() == 1 } +} + +/// Useful utilities for working with pd abstracting common operations. +pub mod util { + use crate::error::PdError; + + use super::block_size; + use super::send; + + /// Activates audio in pd. + /// + /// # Errors + /// + /// A list of errors that can occur: + /// - [`SendError`](crate::error::SendError) + /// - [`MissingDestination`](crate::error::SendError::MissingDestination) + /// - [`SizeError`](crate::error::SizeError) + /// - [`TooLarge`](crate::error::SizeError::TooLarge) + /// + /// To match over these errors, you would need to downcast the returned error. + pub fn dsp_on() -> Result<(), PdError> { + send::start_message(1)?; + send::add_float_to_started_message(1.0); + send::finish_message_as_typed_message_and_send_to("pd", "dsp")?; + Ok(()) + } + + /// De-activates audio in pd. + /// + /// # Errors + /// + /// A list of errors that can occur: + /// - [`SendError`](crate::error::SendError) + /// - [`MissingDestination`](crate::error::SendError::MissingDestination) + /// - [`SizeError`](crate::error::SizeError) + /// - [`TooLarge`](crate::error::SizeError::TooLarge) + /// + /// To match over these errors, you would need to downcast the returned error. + pub fn dsp_off() -> Result<(), PdError> { + send::start_message(1)?; + send::add_float_to_started_message(0.0); + send::finish_message_as_typed_message_and_send_to("pd", "dsp")?; + Ok(()) + } + + /// Find the number of pd ticks according to the case. + /// + /// The calculation is `buffer_size / (block_size * channels)` + #[expect( + clippy::integer_division, + reason = "This function is used to calculate the number of ticks. We don't need floating point precision here." + )] + pub fn calculate_ticks(channels: i32, buffer_size: i32) -> i32 { + let block_size = block_size(); + buffer_size / (block_size * channels) + } +} diff --git a/src/array.rs b/src/functions/array.rs similarity index 85% rename from src/array.rs rename to src/functions/array.rs index 97319f0..8e95da7 100644 --- a/src/array.rs +++ b/src/functions/array.rs @@ -1,7 +1,4 @@ -use crate::{ - error::{ArrayError, SizeError}, - C_STRING_FAILURE, -}; +use crate::error::{ArrayError, SizeError, StringConversionError}; use std::ffi::CString; @@ -9,7 +6,7 @@ use std::ffi::CString; /// /// # Example /// ```no_run -/// use libpd_rs::array::array_size; +/// use libpd_rs::functions::array::array_size; /// /// let size = array_size("my_array").unwrap(); /// ``` @@ -18,9 +15,10 @@ use std::ffi::CString; /// /// A list of errors that can occur: /// - [`CouldNotDetermine`](crate::error::SizeError::CouldNotDetermine) +/// - [`StringConversion`](crate::error::SizeError::StringConversion) pub fn array_size>(name: T) -> Result { unsafe { - let name = CString::new(name.as_ref()).expect(C_STRING_FAILURE); + let name = CString::new(name.as_ref()).map_err(StringConversionError::from)?; // Returns size or negative error code if non-existent let result = libpd_sys::libpd_arraysize(name.as_ptr()); if result >= 0 { @@ -36,7 +34,7 @@ pub fn array_size>(name: T) -> Result { /// /// # Example /// ```no_run -/// use libpd_rs::array::{array_size, resize_array}; +/// use libpd_rs::functions::array::{array_size, resize_array}; /// /// resize_array("my_array", 1024).unwrap(); /// let size = array_size("my_array").unwrap(); @@ -51,6 +49,7 @@ pub fn array_size>(name: T) -> Result { /// /// A list of errors that can occur: /// - [`CouldNotDetermine`](crate::error::SizeError::CouldNotDetermine) +/// - [`StringConversion`](crate::error::SizeError::StringConversion) pub fn resize_array>(name: T, size: i32) -> Result<(), SizeError> { // The size argument is a `long` but bindgen interprets it as i64 // @@ -73,7 +72,7 @@ pub fn resize_array>(name: T, size: i32) -> Result<(), SizeError> // // TODO: Find the right approach here. Low-priority unsafe { - let name = CString::new(name.as_ref()).expect(C_STRING_FAILURE); + let name = CString::new(name.as_ref()).map_err(StringConversionError::from)?; // returns 0 on success or negative error code if non-existent #[cfg(target_os = "macos")] match libpd_sys::libpd_resize_array(name.as_ptr(), i64::from(size)) { @@ -100,7 +99,7 @@ pub fn resize_array>(name: T, size: i32) -> Result<(), SizeError> /// /// # Example /// ```no_run -/// use libpd_rs::array::read_float_array_from; +/// use libpd_rs::functions::array::read_float_array_from; /// /// let mut destination = [0.0_f32; 64]; /// read_float_array_from("my_array", 32, 32, &mut destination).unwrap(); @@ -114,6 +113,7 @@ pub fn resize_array>(name: T, size: i32) -> Result<(), SizeError> /// A list of errors that can occur: /// - [`OutOfBounds`](crate::error::ArrayError::OutOfBounds) /// - [`FailedToFindArray`](crate::error::ArrayError::FailedToFindArray) +/// - [`StringConversion`](crate::error::ArrayError::StringConversion) pub fn read_float_array_from>( source_name: T, source_read_offset: i32, @@ -121,7 +121,7 @@ pub fn read_float_array_from>( destination: &mut [f32], ) -> Result<(), ArrayError> { unsafe { - let name = CString::new(source_name.as_ref()).expect(C_STRING_FAILURE); + let name = CString::new(source_name.as_ref()).map_err(StringConversionError::from)?; // Returns 0 on success or a negative error code if the array is non-existent // or offset + n exceeds range of array @@ -151,7 +151,7 @@ pub fn read_float_array_from>( /// /// # Example /// ```no_run -/// use libpd_rs::array::write_float_array_to; +/// use libpd_rs::functions::array::write_float_array_to; /// /// let mut source = [1.0_f32; 64]; /// write_float_array_to("my_array", 32, &source, 32).unwrap(); @@ -165,6 +165,7 @@ pub fn read_float_array_from>( /// A list of errors that can occur: /// - [`OutOfBounds`](crate::error::ArrayError::OutOfBounds) /// - [`FailedToFindArray`](crate::error::ArrayError::FailedToFindArray) +/// - [`StringConversion`](crate::error::ArrayError::StringConversion) pub fn write_float_array_to>( destination_name: T, destination_write_offset: i32, @@ -172,12 +173,14 @@ pub fn write_float_array_to>( source_read_amount: i32, ) -> Result<(), ArrayError> { unsafe { - let name = CString::new(destination_name.as_ref()).expect(C_STRING_FAILURE); + let name = CString::new(destination_name.as_ref()).map_err(StringConversionError::from)?; // Returns 0 on success or a negative error code if the array is non-existent // or offset + n exceeds range of array - // We check this manually in the predicate. - #[allow(clippy::cast_sign_loss)] + #[expect( + clippy::cast_sign_loss, + reason = "We check this manually in the predicate." + )] if destination_write_offset + source_read_amount > array_size(destination_name.as_ref()).map_err(|_| ArrayError::FailedToFindArray)? || source_read_amount < 0 @@ -205,7 +208,7 @@ pub fn write_float_array_to>( /// /// # Example /// ```no_run -/// use libpd_rs::array::read_double_array_from; +/// use libpd_rs::functions::array::read_double_array_from; /// /// let mut destination = [0.0_f64; 64]; /// read_double_array_from("my_array", 32, 32, &mut destination).unwrap(); @@ -219,6 +222,7 @@ pub fn write_float_array_to>( /// A list of errors that can occur: /// - [`OutOfBounds`](crate::error::ArrayError::OutOfBounds) /// - [`FailedToFindArray`](crate::error::ArrayError::FailedToFindArray) +/// - [`StringConversion`](crate::error::ArrayError::StringConversion) pub fn read_double_array_from>( source_name: T, source_read_offset: i32, @@ -226,7 +230,7 @@ pub fn read_double_array_from>( destination: &mut [f64], ) -> Result<(), ArrayError> { unsafe { - let name = CString::new(source_name.as_ref()).expect(C_STRING_FAILURE); + let name = CString::new(source_name.as_ref()).map_err(StringConversionError::from)?; // Returns 0 on success or a negative error code if the array is non-existent // or offset + n exceeds range of array @@ -256,7 +260,7 @@ pub fn read_double_array_from>( /// /// # Example /// ```no_run -/// use libpd_rs::array::write_double_array_to; +/// use libpd_rs::functions::array::write_double_array_to; /// /// let source = [1.0_f64; 64]; /// write_double_array_to("my_array", 32, &source, 32).unwrap(); @@ -270,6 +274,7 @@ pub fn read_double_array_from>( /// A list of errors that can occur: /// - [`OutOfBounds`](crate::error::ArrayError::OutOfBounds) /// - [`FailedToFindArray`](crate::error::ArrayError::FailedToFindArray) +/// - [`StringConversion`](crate::error::ArrayError::StringConversion) pub fn write_double_array_to>( destination_name: T, destination_write_offset: i32, @@ -277,12 +282,15 @@ pub fn write_double_array_to>( source_read_amount: i32, ) -> Result<(), ArrayError> { unsafe { - let name = CString::new(destination_name.as_ref()).expect(C_STRING_FAILURE); + let name = CString::new(destination_name.as_ref()).map_err(StringConversionError::from)?; // Returns 0 on success or a negative error code if the array is non-existent // or offset + n exceeds range of array // We check this manually in the predicate. - #[allow(clippy::cast_sign_loss)] + #[expect( + clippy::cast_sign_loss, + reason = "We check this manually in the predicate." + )] if destination_write_offset + source_read_amount > array_size(destination_name.as_ref()).map_err(|_| ArrayError::FailedToFindArray)? || source_read_amount < 0 diff --git a/src/gui.rs b/src/functions/gui.rs similarity index 85% rename from src/gui.rs rename to src/functions/gui.rs index f003b08..ce54466 100644 --- a/src/gui.rs +++ b/src/functions/gui.rs @@ -1,4 +1,5 @@ -use crate::{error::GuiLifeCycleError, C_STRING_FAILURE}; +use crate::error::GuiLifeCycleError; +use crate::error::StringConversionError; use std::ffi::CString; use std::path::Path; @@ -11,7 +12,7 @@ use std::path::Path; /// /// # Examples /// ```no_run -/// use libpd_rs::gui::start_gui; +/// use libpd_rs::functions::gui::start_gui; /// use std::path::PathBuf; /// /// // In mac os probably it would look something like this, @@ -24,10 +25,11 @@ use std::path::Path; /// /// A list of errors that can occur: /// - [`FailedToOpenGui`](crate::error::GuiLifeCycleError::FailedToOpenGui) +/// - [`StringConversion`](crate::error::GuiLifeCycleError::StringConversion) pub fn start_gui>(path_to_pd: T) -> Result<(), GuiLifeCycleError> { if path_to_pd.as_ref().exists() { let path_to_pd = path_to_pd.as_ref().to_string_lossy(); - let path_to_pd = CString::new(path_to_pd.as_ref()).expect(C_STRING_FAILURE); + let path_to_pd = CString::new(path_to_pd.as_ref()).map_err(StringConversionError::from)?; unsafe { match libpd_sys::libpd_start_gui(path_to_pd.as_ptr()) { 0 => return Ok(()), @@ -55,9 +57,9 @@ pub fn stop_gui() { /// /// # Examples /// ```no_run -/// use libpd_rs::gui::poll_gui; +/// use libpd_rs::functions::gui::poll_gui; /// -/// libpd_rs::init(); +/// libpd_rs::functions::init(); /// /// loop { /// while let Some(_) = poll_gui() { @@ -65,7 +67,6 @@ pub fn stop_gui() { /// } /// } /// ``` -#[must_use] pub fn poll_gui() -> Option<()> { unsafe { match libpd_sys::libpd_poll_gui() { diff --git a/src/process.rs b/src/functions/process.rs similarity index 86% rename from src/process.rs rename to src/functions/process.rs index f3f95eb..10705b4 100644 --- a/src/process.rs +++ b/src/functions/process.rs @@ -6,8 +6,8 @@ /// /// # Examples /// ```no_run -/// use libpd_rs::process::process_float; -/// use libpd_rs::block_size; +/// use libpd_rs::functions::process::process_float; +/// use libpd_rs::functions::block_size; /// /// let output_channels = 2; /// // ... @@ -30,6 +30,10 @@ /// This function may panic for multiple reasons, /// first of all there is a mutex lock used internally and also it processes buffers in place so there are possibilities of segfaults. /// Use with care. +/// +/// To name a few +/// - If the pd instance is not initialized or set for the thread. +/// - If input and output buffer sizes are wrong. pub fn process_float(ticks: i32, input_buffer: &[f32], output_buffer: &mut [f32]) { unsafe { libpd_sys::libpd_process_float(ticks, input_buffer.as_ptr(), output_buffer.as_mut_ptr()); @@ -49,8 +53,8 @@ pub fn process_float(ticks: i32, input_buffer: &[f32], output_buffer: &mut [f32] /// /// # Examples /// ```no_run -/// use libpd_rs::process::process_short; -/// use libpd_rs::block_size; +/// use libpd_rs::functions::process::process_short; +/// use libpd_rs::functions::block_size; /// /// let output_channels = 2; /// // ... @@ -73,7 +77,10 @@ pub fn process_float(ticks: i32, input_buffer: &[f32], output_buffer: &mut [f32] /// This function may panic for multiple reasons, /// first of all there is a mutex lock used internally and also it processes buffers in place so there are possibilities of segfaults. /// Use with care. - +/// +/// To name a few +/// - If the pd instance is not initialized or set for the thread. +/// - If input and output buffer sizes are wrong. pub fn process_short(ticks: i32, input_buffer: &[i16], output_buffer: &mut [i16]) { unsafe { libpd_sys::libpd_process_short(ticks, input_buffer.as_ptr(), output_buffer.as_mut_ptr()); @@ -88,8 +95,8 @@ pub fn process_short(ticks: i32, input_buffer: &[i16], output_buffer: &mut [i16] /// /// # Examples /// ```no_run -/// use libpd_rs::process::process_double; -/// use libpd_rs::block_size; +/// use libpd_rs::functions::process::process_double; +/// use libpd_rs::functions::block_size; /// /// let output_channels = 2; /// // ... @@ -112,6 +119,10 @@ pub fn process_short(ticks: i32, input_buffer: &[i16], output_buffer: &mut [i16] /// This function may panic for multiple reasons, /// first of all there is a mutex lock used internally and also it processes buffers in place so there are possibilities of segfaults. /// Use with care. +/// +/// To name a few +/// - If the pd instance is not initialized or set for the thread. +/// - If input and output buffer sizes are wrong. pub fn process_double(ticks: i32, input_buffer: &[f64], output_buffer: &mut [f64]) { unsafe { libpd_sys::libpd_process_double(ticks, input_buffer.as_ptr(), output_buffer.as_mut_ptr()); @@ -128,7 +139,7 @@ pub fn process_double(ticks: i32, input_buffer: &[f64], output_buffer: &mut [f64 /// /// # Examples /// ```no_run -/// use libpd_rs::process::process_raw; +/// use libpd_rs::functions::process::process_raw; /// /// // After initializing audio and opening a patch file then in the audio callback.. /// @@ -146,6 +157,10 @@ pub fn process_double(ticks: i32, input_buffer: &[f64], output_buffer: &mut [f64 /// This function may panic for multiple reasons, /// first of all there is a mutex lock used internally and also it processes buffers in place so there are possibilities of segfaults. /// Use with care. +/// +/// To name a few +/// - If the pd instance is not initialized or set for the thread. +/// - If input and output buffer sizes are wrong. pub fn process_raw(input_buffer: &[f32], output_buffer: &mut [f32]) { unsafe { libpd_sys::libpd_process_raw(input_buffer.as_ptr(), output_buffer.as_mut_ptr()); @@ -167,7 +182,7 @@ pub fn process_raw(input_buffer: &[f32], output_buffer: &mut [f32]) { /// /// # Examples /// ```no_run -/// use libpd_rs::process::process_raw_short; +/// use libpd_rs::functions::process::process_raw_short; /// /// // After initializing audio and opening a patch file then in the audio callback.. /// @@ -185,7 +200,10 @@ pub fn process_raw(input_buffer: &[f32], output_buffer: &mut [f32]) { /// This function may panic for multiple reasons, /// first of all there is a mutex lock used internally and also it processes buffers in place so there are possibilities of segfaults. /// Use with care. - +/// +/// To name a few +/// - If the pd instance is not initialized or set for the thread. +/// - If input and output buffer sizes are wrong. pub fn process_raw_short(input_buffer: &[i16], output_buffer: &mut [i16]) { unsafe { libpd_sys::libpd_process_raw_short(input_buffer.as_ptr(), output_buffer.as_mut_ptr()); @@ -202,7 +220,7 @@ pub fn process_raw_short(input_buffer: &[i16], output_buffer: &mut [i16]) { /// /// # Examples /// ```no_run -/// use libpd_rs::process::process_raw_double; +/// use libpd_rs::functions::process::process_raw_double; /// /// // After initializing audio and opening a patch file then in the audio callback.. /// @@ -220,6 +238,10 @@ pub fn process_raw_short(input_buffer: &[i16], output_buffer: &mut [i16]) { /// This function may panic for multiple reasons, /// first of all there is a mutex lock used internally and also it processes buffers in place so there are possibilities of segfaults. /// Use with care. +/// +/// To name a few +/// - If the pd instance is not initialized or set for the thread. +/// - If input and output buffer sizes are wrong. pub fn process_raw_double(input_buffer: &[f64], output_buffer: &mut [f64]) { unsafe { libpd_sys::libpd_process_raw_double(input_buffer.as_ptr(), output_buffer.as_mut_ptr()); diff --git a/src/receive.rs b/src/functions/receive.rs similarity index 80% rename from src/receive.rs rename to src/functions/receive.rs index 5a6fc54..cbb23fd 100644 --- a/src/receive.rs +++ b/src/functions/receive.rs @@ -1,8 +1,14 @@ +#![expect( + clippy::expect_used, + clippy::missing_panics_doc, + reason = "In closure implementations, there are CStr conversions. + The use of expect there is ok because those strings are coming from Pd and pd uses C strings." +)] + use crate::{ - error::SubscriptionError, + error::{StringConversionError, SubscriptionError, C_STR_FAILURE}, helpers::make_atom_list_from_t_atom_list, types::{Atom, ReceiverHandle}, - C_STRING_FAILURE, C_STR_FAILURE, }; use libffi::high::{ @@ -14,7 +20,10 @@ use libpd_sys::{ t_libpd_noteonhook, t_libpd_pitchbendhook, t_libpd_polyaftertouchhook, t_libpd_printhook, t_libpd_programchangehook, t_libpd_symbolhook, }; -use std::ffi::{CStr, CString}; +use std::{ + ffi::{CStr, CString}, + mem, os, slice, +}; type PrintHookCodePtr = *const FnPtr1<'static, *const i8, ()>; type BangHookCodePtr = *const FnPtr1<'static, *const i8, ()>; @@ -41,10 +50,11 @@ type MidiByteCodePtr = *const FnPtr2<'static, i32, i32, ()>; /// # Example /// ```rust /// use std::collections::HashMap; -/// use libpd_rs::receive::{start_listening_from}; +/// use libpd_rs::functions::receive::{start_listening_from}; /// use libpd_rs::types::ReceiverHandle; +/// use libpd_rs::instance::PdInstance; /// -/// libpd_rs::init(); +/// let _main_instance = PdInstance::new().unwrap(); /// /// let sources = vec!["foo", "bar"]; /// // Maybe you would like to use the receiver handles later so you may store them.. @@ -64,8 +74,9 @@ type MidiByteCodePtr = *const FnPtr2<'static, i32, i32, ()>; /// /// A list of errors that can occur: /// - [`FailedToSubscribeToSender`](crate::error::SubscriptionError::FailedToSubscribeToSender) +/// - [`StringConversion`](crate::error::SubscriptionError::StringConversion) pub fn start_listening_from>(sender: T) -> Result { - let send = CString::new(sender.as_ref()).expect(C_STRING_FAILURE); + let send = CString::new(sender.as_ref()).map_err(StringConversionError::from)?; unsafe { let handle = libpd_sys::libpd_bind(send.as_ptr()); @@ -87,15 +98,16 @@ pub fn start_listening_from>(sender: T) -> Result>(sender: T) -> bool { - let send = CString::new(sender.as_ref()).expect(C_STRING_FAILURE); - unsafe { matches!(libpd_sys::libpd_exists(send.as_ptr()), 1) } +/// # Errors +/// +/// A list of errors that can occur: +/// - [`StringConversion`](crate::error::SubscriptionError::StringConversion) +pub fn source_to_listen_from_exists>(sender: T) -> Result { + let send = CString::new(sender.as_ref()).map_err(StringConversionError::from)?; + unsafe { Ok(matches!(libpd_sys::libpd_exists(send.as_ptr()), 1)) } } /// Sets a closure to be called when a message is written to the pd console. @@ -131,9 +148,10 @@ pub fn source_to_listen_from_exists>(sender: T) -> bool { /// /// # Example /// ```rust -/// use libpd_rs::receive::{on_print}; +/// use libpd_rs::functions::receive::{on_print}; +/// use libpd_rs::instance::PdInstance; /// -/// libpd_rs::init(); +/// let _main_instance = PdInstance::new().unwrap(); /// /// on_print(|msg: &str| { /// println!("pd is printing: {msg}"); @@ -141,14 +159,14 @@ pub fn source_to_listen_from_exists>(sender: T) -> bool { /// /// ``` pub fn on_print(mut user_provided_closure: F) { - let closure: &'static mut _ = Box::leak(Box::new(move |out: *const std::os::raw::c_char| { + let closure: &'static mut _ = Box::leak(Box::new(move |out: *const os::raw::c_char| { let out = unsafe { CStr::from_ptr(out).to_str().expect(C_STR_FAILURE) }; user_provided_closure(out); })); let callback = ClosureMut1::new(closure); let code = callback.code_ptr() as PrintHookCodePtr; let ptr = unsafe { *code.cast::() }; - std::mem::forget(callback); + mem::forget(callback); unsafe { libpd_sys::libpd_set_queued_printhook(Some(libpd_sys::libpd_print_concatenator)); @@ -166,9 +184,10 @@ pub fn on_print(mut user_provided_closur /// /// # Example /// ```rust -/// use libpd_rs::receive::{on_bang, start_listening_from}; +/// use libpd_rs::functions::receive::{on_bang, start_listening_from}; +/// use libpd_rs::instance::PdInstance; /// -/// libpd_rs::init(); +/// let _main_instance = PdInstance::new().unwrap(); /// /// on_bang(|source: &str| { /// match source { @@ -183,15 +202,14 @@ pub fn on_print(mut user_provided_closur /// let bar_receiver_handle = start_listening_from("bar").unwrap(); /// ``` pub fn on_bang(mut user_provided_closure: F) { - let closure: &'static mut _ = - Box::leak(Box::new(move |source: *const std::os::raw::c_char| { - let source = unsafe { CStr::from_ptr(source).to_str().expect(C_STR_FAILURE) }; - user_provided_closure(source); - })); + let closure: &'static mut _ = Box::leak(Box::new(move |source: *const os::raw::c_char| { + let source = unsafe { CStr::from_ptr(source).to_str().expect(C_STR_FAILURE) }; + user_provided_closure(source); + })); let callback = ClosureMut1::new(closure); let code = callback.code_ptr() as BangHookCodePtr; let ptr = unsafe { *code.cast::() }; - std::mem::forget(callback); + mem::forget(callback); unsafe { libpd_sys::libpd_set_queued_banghook(ptr); @@ -208,9 +226,10 @@ pub fn on_bang(mut user_provided_closure /// /// # Example /// ```rust -/// use libpd_rs::receive::{on_float, start_listening_from}; +/// use libpd_rs::functions::receive::{on_float, start_listening_from}; +/// use libpd_rs::instance::PdInstance; /// -/// libpd_rs::init(); +/// let _main_instance = PdInstance::new().unwrap(); /// /// on_float(|source: &str, value: f32| { /// match source { @@ -226,7 +245,7 @@ pub fn on_bang(mut user_provided_closure /// ``` pub fn on_float(mut user_provided_closure: F) { let closure: &'static mut _ = Box::leak(Box::new( - move |source: *const std::os::raw::c_char, float: f32| { + move |source: *const os::raw::c_char, float: f32| { let source = unsafe { CStr::from_ptr(source).to_str().expect(C_STR_FAILURE) }; user_provided_closure(source, float); }, @@ -234,7 +253,7 @@ pub fn on_float(mut user_provided_c let callback = ClosureMut2::new(closure); let code = callback.code_ptr() as FloatHookCodePtr; let ptr = unsafe { *code.cast::() }; - std::mem::forget(callback); + mem::forget(callback); unsafe { libpd_sys::libpd_set_queued_floathook(ptr); @@ -251,9 +270,10 @@ pub fn on_float(mut user_provided_c /// /// # Example /// ```rust -/// use libpd_rs::receive::{on_double, start_listening_from}; +/// use libpd_rs::functions::receive::{on_double, start_listening_from}; +/// use libpd_rs::instance::PdInstance; /// -/// libpd_rs::init(); +/// let _main_instance = PdInstance::new().unwrap(); /// /// on_double(|source: &str, value: f64| { /// match source { @@ -269,7 +289,7 @@ pub fn on_float(mut user_provided_c /// ``` pub fn on_double(mut user_provided_closure: F) { let closure: &'static mut _ = Box::leak(Box::new( - move |source: *const std::os::raw::c_char, double: f64| { + move |source: *const os::raw::c_char, double: f64| { let source = unsafe { CStr::from_ptr(source).to_str().expect(C_STR_FAILURE) }; user_provided_closure(source, double); }, @@ -277,7 +297,7 @@ pub fn on_double(mut user_provided_ let callback = ClosureMut2::new(closure); let code = callback.code_ptr() as DoubleHookCodePtr; let ptr = unsafe { *code.cast::() }; - std::mem::forget(callback); + mem::forget(callback); unsafe { libpd_sys::libpd_set_queued_doublehook(ptr); @@ -290,9 +310,10 @@ pub fn on_double(mut user_provided_ /// /// # Example /// ```rust -/// use libpd_rs::receive::{on_symbol, start_listening_from}; +/// use libpd_rs::functions::receive::{on_symbol, start_listening_from}; +/// use libpd_rs::instance::PdInstance; /// -/// libpd_rs::init(); +/// let _main_instance = PdInstance::new().unwrap(); /// /// on_symbol(|source: &str, symbol: &str| { /// match source { @@ -308,7 +329,7 @@ pub fn on_double(mut user_provided_ /// ``` pub fn on_symbol(mut user_provided_closure: F) { let closure: &'static mut _ = Box::leak(Box::new( - move |source: *const std::os::raw::c_char, symbol: *const std::os::raw::c_char| { + move |source: *const os::raw::c_char, symbol: *const os::raw::c_char| { let source = unsafe { CStr::from_ptr(source).to_str().expect(C_STR_FAILURE) }; let symbol = unsafe { CStr::from_ptr(symbol).to_str().expect(C_STR_FAILURE) }; user_provided_closure(source, symbol); @@ -318,7 +339,7 @@ pub fn on_symbol(mut user_provided let code = callback.code_ptr() as SymbolHookCodePtr; let ptr = unsafe { *code.cast::() }; - std::mem::forget(callback); + mem::forget(callback); unsafe { libpd_sys::libpd_set_queued_symbolhook(ptr); }; @@ -330,10 +351,11 @@ pub fn on_symbol(mut user_provided /// /// # Example /// ```rust -/// use libpd_rs::receive::{on_list, start_listening_from}; +/// use libpd_rs::functions::receive::{on_list, start_listening_from}; /// use libpd_rs::types::Atom; +/// use libpd_rs::instance::PdInstance; /// -/// libpd_rs::init(); +/// let _main_instance = PdInstance::new().unwrap(); /// /// on_list(|source: &str, list: &[Atom]| match source { /// "foo" => { @@ -371,13 +393,16 @@ pub fn on_symbol(mut user_provided /// ``` pub fn on_list(mut user_provided_closure: F) { let closure: &'static mut _ = Box::leak(Box::new( - move |source: *const std::os::raw::c_char, + move |source: *const os::raw::c_char, list_length: i32, atom_list: *mut libpd_sys::t_atom| { let source = unsafe { CStr::from_ptr(source).to_str().expect(C_STR_FAILURE) }; - // It is practically impossible that this list will have a negative size or a size of millions so this is safe. - #[allow(clippy::cast_sign_loss)] - let atom_list = unsafe { std::slice::from_raw_parts(atom_list, list_length as usize) }; + + #[expect( + clippy::cast_sign_loss, + reason = "We're trusting Pd to not send a negative list length. I think this is sane enough." + )] + let atom_list = unsafe { slice::from_raw_parts(atom_list, list_length as usize) }; let atoms = make_atom_list_from_t_atom_list!(atom_list); user_provided_closure(source, &atoms); }, @@ -385,7 +410,7 @@ pub fn on_list(mut user_provide let callback = ClosureMut3::new(closure); let code = callback.code_ptr() as ListHookCodePtr; let ptr = unsafe { *code.cast::() }; - std::mem::forget(callback); + mem::forget(callback); unsafe { libpd_sys::libpd_set_queued_listhook(ptr); @@ -410,10 +435,11 @@ pub fn on_list(mut user_provide /// /// # Example /// ```rust -/// use libpd_rs::receive::{on_message, start_listening_from}; +/// use libpd_rs::functions::receive::{on_message, start_listening_from}; /// use libpd_rs::types::Atom; +/// use libpd_rs::instance::PdInstance; /// -/// libpd_rs::init(); +/// let _main_instance = PdInstance::new().unwrap(); /// /// on_message(|source: &str, message: &str, values: &[Atom]| match source { /// "foo" => { @@ -440,15 +466,18 @@ pub fn on_message( mut user_provided_closure: F, ) { let closure: &'static mut _ = Box::leak(Box::new( - move |source: *const std::os::raw::c_char, - message: *const std::os::raw::c_char, + move |source: *const os::raw::c_char, + message: *const os::raw::c_char, list_length: i32, atom_list: *mut libpd_sys::t_atom| { let source = unsafe { CStr::from_ptr(source).to_str().expect(C_STR_FAILURE) }; let message = unsafe { CStr::from_ptr(message).to_str().expect(C_STR_FAILURE) }; - // It is practically impossible that this list will have a negative size or a size of millions so this is safe. - #[allow(clippy::cast_sign_loss)] - let atom_list = unsafe { std::slice::from_raw_parts(atom_list, list_length as usize) }; + + #[expect( + clippy::cast_sign_loss, + reason = "We're trusting Pd to not send a negative list length. I think this is sane enough." + )] + let atom_list = unsafe { slice::from_raw_parts(atom_list, list_length as usize) }; let atoms = make_atom_list_from_t_atom_list!(atom_list); user_provided_closure(source, message, &atoms); }, @@ -456,7 +485,7 @@ pub fn on_message( let callback = ClosureMut4::new(closure); let code = callback.code_ptr() as MessageHookCodePtr; let ptr = unsafe { *code.cast::() }; - std::mem::forget(callback); + mem::forget(callback); unsafe { libpd_sys::libpd_set_queued_messagehook(ptr); @@ -469,9 +498,10 @@ pub fn on_message( /// /// # Example /// ```no_run -/// use libpd_rs::receive::{start_listening_from, on_symbol, receive_messages_from_pd}; +/// use libpd_rs::functions::receive::{start_listening_from, on_symbol, receive_messages_from_pd}; +/// use libpd_rs::instance::PdInstance; /// -/// libpd_rs::init(); +/// let _main_instance = PdInstance::new().unwrap(); /// /// on_symbol(|source: &str, value: &str| { /// match source { @@ -508,11 +538,12 @@ pub fn receive_messages_from_pd() { /// /// # Example /// ```rust -/// use libpd_rs::receive::{on_midi_note_on}; +/// use libpd_rs::functions::receive::{on_midi_note_on}; +/// use libpd_rs::instance::PdInstance; /// -/// libpd_rs::init(); +/// let _main_instance = PdInstance::new().unwrap(); /// -/// libpd_rs::init(); +/// libpd_rs::functions::init(); /// /// on_midi_note_on(|channel: i32, pitch: i32, velocity: i32| { /// println!("Note On: channel {channel}, pitch {pitch}, velocity {velocity}"); @@ -528,7 +559,7 @@ pub fn on_midi_note_on( let callback = ClosureMut3::new(closure); let code = callback.code_ptr() as MidiNoteOnCodePtr; let ptr = unsafe { *code.cast::() }; - std::mem::forget(callback); + mem::forget(callback); unsafe { libpd_sys::libpd_set_queued_noteonhook(ptr); @@ -547,11 +578,10 @@ pub fn on_midi_note_on( /// /// # Example /// ```rust -/// use libpd_rs::receive::{on_midi_control_change}; +/// use libpd_rs::functions::receive::{on_midi_control_change}; +/// use libpd_rs::instance::PdInstance; /// -/// libpd_rs::init(); -/// -/// libpd_rs::init(); +/// let _main_instance = PdInstance::new().unwrap(); /// /// on_midi_control_change(|channel: i32, controller: i32, value: i32| { /// println!("Control Change: channel {channel}, controller number {controller}, value {value}"); @@ -568,7 +598,7 @@ pub fn on_midi_control_change( let callback = ClosureMut3::new(closure); let code = callback.code_ptr() as MidiControlChangeCodePtr; let ptr = unsafe { *code.cast::() }; - std::mem::forget(callback); + mem::forget(callback); unsafe { libpd_sys::libpd_set_queued_controlchangehook(ptr); @@ -587,11 +617,10 @@ pub fn on_midi_control_change( /// /// # Example /// ```rust -/// use libpd_rs::receive::{on_midi_program_change}; -/// -/// libpd_rs::init(); +/// use libpd_rs::functions::receive::{on_midi_program_change}; +/// use libpd_rs::instance::PdInstance; /// -/// libpd_rs::init(); +/// let _main_instance = PdInstance::new().unwrap(); /// /// on_midi_program_change(|channel: i32, value: i32| { /// println!("Program Change: channel {channel}, program number {value}"); @@ -606,7 +635,7 @@ pub fn on_midi_program_change( let callback = ClosureMut2::new(closure); let code = callback.code_ptr() as MidiProgramChangeCodePtr; let ptr = unsafe { *code.cast::() }; - std::mem::forget(callback); + mem::forget(callback); unsafe { libpd_sys::libpd_set_queued_programchangehook(ptr); @@ -627,11 +656,10 @@ pub fn on_midi_program_change( /// /// # Example /// ```rust -/// use libpd_rs::receive::{on_midi_pitch_bend}; +/// use libpd_rs::functions::receive::{on_midi_pitch_bend}; +/// use libpd_rs::instance::PdInstance; /// -/// libpd_rs::init(); -/// -/// libpd_rs::init(); +/// let _main_instance = PdInstance::new().unwrap(); /// /// on_midi_pitch_bend(|channel: i32, value: i32| { /// println!("Pitch Bend: channel {channel}, bend amount {value}"); @@ -646,7 +674,7 @@ pub fn on_midi_pitch_bend( let callback = ClosureMut2::new(closure); let code = callback.code_ptr() as MidiPitchBendCodePtr; let ptr = unsafe { *code.cast::() }; - std::mem::forget(callback); + mem::forget(callback); unsafe { libpd_sys::libpd_set_queued_pitchbendhook(ptr); @@ -665,11 +693,10 @@ pub fn on_midi_pitch_bend( /// /// # Example /// ```rust -/// use libpd_rs::receive::{on_midi_after_touch}; -/// -/// libpd_rs::init(); +/// use libpd_rs::functions::receive::{on_midi_after_touch}; +/// use libpd_rs::instance::PdInstance; /// -/// libpd_rs::init(); +/// let _main_instance = PdInstance::new().unwrap(); /// /// on_midi_after_touch(|channel: i32, value: i32| { /// println!("After Touch: channel {channel}, after touch amount {value}"); @@ -684,7 +711,7 @@ pub fn on_midi_after_touch( let callback = ClosureMut2::new(closure); let code = callback.code_ptr() as MidiAfterTouchCodePtr; let ptr = unsafe { *code.cast::() }; - std::mem::forget(callback); + mem::forget(callback); unsafe { libpd_sys::libpd_set_queued_aftertouchhook(ptr); @@ -703,11 +730,10 @@ pub fn on_midi_after_touch( /// /// # Example /// ```rust -/// use libpd_rs::receive::{on_midi_poly_after_touch}; +/// use libpd_rs::functions::receive::{on_midi_poly_after_touch}; +/// use libpd_rs::instance::PdInstance; /// -/// libpd_rs::init(); -/// -/// libpd_rs::init(); +/// let _main_instance = PdInstance::new().unwrap(); /// /// on_midi_poly_after_touch(|channel: i32, pitch: i32, value: i32| { /// println!("Poly After Touch: channel {channel}, pitch {pitch}, after touch amount {value}"); @@ -723,7 +749,7 @@ pub fn on_midi_poly_after_touch let callback = ClosureMut3::new(closure); let code = callback.code_ptr() as MidiPolyAfterTouchCodePtr; let ptr = unsafe { *code.cast::() }; - std::mem::forget(callback); + mem::forget(callback); unsafe { libpd_sys::libpd_set_queued_polyaftertouchhook(ptr); @@ -740,11 +766,10 @@ pub fn on_midi_poly_after_touch /// /// # Example /// ```rust -/// use libpd_rs::receive::{on_midi_byte}; -/// -/// libpd_rs::init(); +/// use libpd_rs::functions::receive::{on_midi_byte}; +/// use libpd_rs::instance::PdInstance; /// -/// libpd_rs::init(); +/// let _main_instance = PdInstance::new().unwrap(); /// /// on_midi_byte(|port: i32, byte: i32| { /// println!("Raw MIDI Byte: port {port}, byte {byte}"); @@ -757,7 +782,7 @@ pub fn on_midi_byte(mut user_provide let callback = ClosureMut2::new(closure); let code = callback.code_ptr() as MidiByteCodePtr; let ptr = unsafe { *code.cast::() }; - std::mem::forget(callback); + mem::forget(callback); unsafe { libpd_sys::libpd_set_queued_midibytehook(ptr); @@ -770,9 +795,10 @@ pub fn on_midi_byte(mut user_provide /// /// # Example /// ```no_run -/// use libpd_rs::receive::{on_midi_byte, receive_midi_messages_from_pd}; +/// use libpd_rs::functions::receive::{on_midi_byte, receive_midi_messages_from_pd}; +/// use libpd_rs::instance::PdInstance; /// -/// libpd_rs::init(); +/// let _main_instance = PdInstance::new().unwrap(); /// /// on_midi_byte(|port: i32, byte: i32| { /// println!("{port}, {byte}"); diff --git a/src/send.rs b/src/functions/send.rs similarity index 78% rename from src/send.rs rename to src/functions/send.rs index 8ddbad2..d2da7ff 100644 --- a/src/send.rs +++ b/src/functions/send.rs @@ -1,8 +1,7 @@ use crate::{ - error::{SendError, SizeError}, + error::{SendError, SizeError, StringConversionError}, helpers::make_t_atom_list_from_atom_list, types::Atom, - C_STRING_FAILURE, }; use std::ffi::CString; @@ -15,7 +14,7 @@ use std::ffi::CString; /// /// # Example /// ```no_run -/// use libpd_rs::send::send_bang_to; +/// use libpd_rs::functions::send::send_bang_to; /// /// // Handle the error if the receiver object is not found /// send_bang_to("foo").unwrap_or_else(|err| { @@ -29,8 +28,9 @@ use std::ffi::CString; /// /// A list of errors that can occur: /// - [`MissingDestination`](crate::error::SendError::MissingDestination) +/// - [`StringConversion`](crate::error::SendError::StringConversion) pub fn send_bang_to>(receiver: T) -> Result<(), SendError> { - let recv = CString::new(receiver.as_ref()).expect(C_STRING_FAILURE); + let recv = CString::new(receiver.as_ref()).map_err(StringConversionError::from)?; unsafe { match libpd_sys::libpd_bang(recv.as_ptr()) { 0 => Ok(()), @@ -47,7 +47,7 @@ pub fn send_bang_to>(receiver: T) -> Result<(), SendError> { /// /// # Example /// ```no_run -/// use libpd_rs::send::send_float_to; +/// use libpd_rs::functions::send::send_float_to; /// /// // Handle the error if the receiver object is not found /// send_float_to("foo", 1.0).unwrap_or_else(|err| { @@ -61,8 +61,9 @@ pub fn send_bang_to>(receiver: T) -> Result<(), SendError> { /// /// A list of errors that can occur: /// - [`MissingDestination`](crate::error::SendError::MissingDestination) +/// - [`StringConversion`](crate::error::SendError::StringConversion) pub fn send_float_to>(receiver: T, value: f32) -> Result<(), SendError> { - let recv = CString::new(receiver.as_ref()).expect(C_STRING_FAILURE); + let recv = CString::new(receiver.as_ref()).map_err(StringConversionError::from)?; unsafe { match libpd_sys::libpd_float(recv.as_ptr(), value) { 0 => Ok(()), @@ -79,7 +80,7 @@ pub fn send_float_to>(receiver: T, value: f32) -> Result<(), SendE /// /// # Example /// ```no_run -/// use libpd_rs::send::send_double_to; +/// use libpd_rs::functions::send::send_double_to; /// /// // Handle the error if the receiver object is not found /// send_double_to("foo", 1.0).unwrap_or_else(|err| { @@ -93,8 +94,9 @@ pub fn send_float_to>(receiver: T, value: f32) -> Result<(), SendE /// /// A list of errors that can occur: /// - [`MissingDestination`](crate::error::SendError::MissingDestination) +/// - [`StringConversion`](crate::error::SendError::StringConversion) pub fn send_double_to>(receiver: T, value: f64) -> Result<(), SendError> { - let recv = CString::new(receiver.as_ref()).expect(C_STRING_FAILURE); + let recv = CString::new(receiver.as_ref()).map_err(StringConversionError::from)?; unsafe { match libpd_sys::libpd_double(recv.as_ptr(), value) { 0 => Ok(()), @@ -111,7 +113,7 @@ pub fn send_double_to>(receiver: T, value: f64) -> Result<(), Send /// /// # Example /// ```no_run -/// use libpd_rs::send::send_symbol_to; +/// use libpd_rs::functions::send::send_symbol_to; /// /// // Handle the error if the receiver object is not found /// send_symbol_to("foo", "bar").unwrap_or_else(|err| { @@ -125,12 +127,13 @@ pub fn send_double_to>(receiver: T, value: f64) -> Result<(), Send /// /// A list of errors that can occur: /// - [`MissingDestination`](crate::error::SendError::MissingDestination) +/// - [`StringConversion`](crate::error::SendError::StringConversion) pub fn send_symbol_to, S: AsRef>( receiver: T, value: S, ) -> Result<(), SendError> { - let recv = CString::new(receiver.as_ref()).expect(C_STRING_FAILURE); - let sym = CString::new(value.as_ref()).expect(C_STRING_FAILURE); + let recv = CString::new(receiver.as_ref()).map_err(StringConversionError::from)?; + let sym = CString::new(value.as_ref()).map_err(StringConversionError::from)?; unsafe { match libpd_sys::libpd_symbol(recv.as_ptr(), sym.as_ptr()) { 0 => Ok(()), @@ -147,9 +150,10 @@ pub fn send_symbol_to, S: AsRef>( /// /// # Example /// ```rust -/// use libpd_rs::send::{start_message}; +/// use libpd_rs::functions::send::{start_message}; +/// use libpd_rs::instance::PdInstance; /// -/// libpd_rs::init(); +/// let _main_instance = PdInstance::new().unwrap(); /// /// // Arbitrary length /// let message_length = 4; @@ -175,9 +179,10 @@ pub fn start_message(length: i32) -> Result<(), SizeError> { /// /// # Example /// ```rust -/// use libpd_rs::send::{start_message, add_float_to_started_message}; +/// use libpd_rs::functions::send::{start_message, add_float_to_started_message}; +/// use libpd_rs::instance::PdInstance; /// -/// libpd_rs::init(); +/// let _main_instance = PdInstance::new().unwrap(); /// /// // Arbitrary length /// let message_length = 4; @@ -200,9 +205,10 @@ pub fn add_float_to_started_message(value: f32) { /// /// # Example /// ```rust -/// use libpd_rs::send::{start_message, add_double_to_started_message}; +/// use libpd_rs::functions::send::{start_message, add_double_to_started_message}; +/// use libpd_rs::instance::PdInstance; /// -/// libpd_rs::init(); +/// let _main_instance = PdInstance::new().unwrap(); /// /// // Arbitrary length /// let message_length = 4; @@ -225,9 +231,10 @@ pub fn add_double_to_started_message(value: f64) { /// /// # Example /// ```rust -/// use libpd_rs::send::{start_message, add_symbol_to_started_message}; +/// use libpd_rs::functions::send::{start_message, add_symbol_to_started_message}; +/// use libpd_rs::instance::PdInstance; /// -/// libpd_rs::init(); +/// let _main_instance = PdInstance::new().unwrap(); /// /// // Arbitrary length /// let message_length = 4; @@ -235,16 +242,21 @@ pub fn add_double_to_started_message(value: f64) { /// add_symbol_to_started_message("foo"); /// } /// ``` +/// # Errors +/// +/// A list of errors that can occur: +/// - [`StringConversion`](crate::error::SendError::StringConversion) /// /// # Panics /// To be honest I'd expect this to panic if you overflow a message buffer. /// /// Although I didn't check that, please create an [issue](https://github.com/alisomay/libpd-rs/issues). -pub fn add_symbol_to_started_message>(value: T) { - let sym = CString::new(value.as_ref()).expect(C_STRING_FAILURE); +pub fn add_symbol_to_started_message>(value: T) -> Result<(), SendError> { + let sym = CString::new(value.as_ref()).map_err(StringConversionError::from)?; unsafe { libpd_sys::libpd_add_symbol(sym.as_ptr()); } + Ok(()) } /// Finishes the current message and send as a list to a receiver in the loaded pd patch @@ -255,9 +267,10 @@ pub fn add_symbol_to_started_message>(value: T) { /// /// # Example /// ```rust -/// use libpd_rs::send::{start_message, add_symbol_to_started_message, add_float_to_started_message, finish_message_as_list_and_send_to}; +/// use libpd_rs::functions::send::{start_message, add_symbol_to_started_message, add_float_to_started_message, finish_message_as_list_and_send_to}; +/// use libpd_rs::instance::PdInstance; /// -/// libpd_rs::init(); +/// let _main_instance = PdInstance::new().unwrap(); /// /// // Arbitrary length /// let message_length = 2; @@ -274,8 +287,9 @@ pub fn add_symbol_to_started_message>(value: T) { /// /// A list of errors that can occur: /// - [`MissingDestination`](crate::error::SendError::MissingDestination) +/// - [`StringConversion`](crate::error::SendError::StringConversion) pub fn finish_message_as_list_and_send_to>(receiver: T) -> Result<(), SendError> { - let recv = CString::new(receiver.as_ref()).expect(C_STRING_FAILURE); + let recv = CString::new(receiver.as_ref()).map_err(StringConversionError::from)?; unsafe { match libpd_sys::libpd_finish_list(recv.as_ptr()) { 0 => Ok(()), @@ -294,9 +308,10 @@ pub fn finish_message_as_list_and_send_to>(receiver: T) -> Result< /// /// # Example /// ```rust -/// use libpd_rs::send::{start_message, add_float_to_started_message, finish_message_as_typed_message_and_send_to}; +/// use libpd_rs::functions::send::{start_message, add_float_to_started_message, finish_message_as_typed_message_and_send_to}; +/// use libpd_rs::instance::PdInstance; /// -/// libpd_rs::init(); +/// let _main_instance = PdInstance::new().unwrap(); /// /// // Arbitrary length /// let message_length = 1; @@ -312,12 +327,13 @@ pub fn finish_message_as_list_and_send_to>(receiver: T) -> Result< /// /// A list of errors that can occur: /// - [`MissingDestination`](crate::error::SendError::MissingDestination) +/// - [`StringConversion`](crate::error::SendError::StringConversion) pub fn finish_message_as_typed_message_and_send_to, S: AsRef>( receiver: T, message_header: S, ) -> Result<(), SendError> { - let recv = CString::new(receiver.as_ref()).expect(C_STRING_FAILURE); - let msg = CString::new(message_header.as_ref()).expect(C_STRING_FAILURE); + let recv = CString::new(receiver.as_ref()).map_err(StringConversionError::from)?; + let msg = CString::new(message_header.as_ref()).map_err(StringConversionError::from)?; unsafe { match libpd_sys::libpd_finish_message(recv.as_ptr(), msg.as_ptr()) { 0 => Ok(()), @@ -334,10 +350,11 @@ pub fn finish_message_as_typed_message_and_send_to, S: AsRef> /// /// # Example /// ```rust -/// use libpd_rs::send::{send_list_to}; +/// use libpd_rs::functions::send::{send_list_to}; /// use libpd_rs::types::Atom; +/// use libpd_rs::instance::PdInstance; /// -/// libpd_rs::init(); +/// let _main_instance = PdInstance::new().unwrap(); /// /// let list = vec![Atom::from(42.0), Atom::from("bar")]; /// // Handle the error if the receiver object is not found @@ -352,18 +369,21 @@ pub fn finish_message_as_typed_message_and_send_to, S: AsRef> /// /// A list of errors that can occur: /// - [`MissingDestination`](crate::error::SendError::MissingDestination) +/// - [`StringConversion`](crate::error::SendError::StringConversion) pub fn send_list_to>(receiver: T, list: &[Atom]) -> Result<(), SendError> { - let recv = CString::new(receiver.as_ref()).expect(C_STRING_FAILURE); + let recv = CString::new(receiver.as_ref()).map_err(StringConversionError::from)?; let mut atom_list: Vec = make_t_atom_list_from_atom_list!(list); let atom_list_slice = atom_list.as_mut_slice(); unsafe { - #[allow(clippy::cast_possible_wrap)] - #[allow(clippy::cast_possible_truncation)] + #[expect( + clippy::cast_possible_wrap, + clippy::cast_possible_truncation, + reason = "This is what the function wants (i32). The value is never going to be negative or huge." + )] match libpd_sys::libpd_list( recv.as_ptr(), - // This is fine since a list will not be millions of elements long and not negative for sure. list.len() as i32, atom_list_slice.as_mut_ptr(), ) { @@ -381,10 +401,11 @@ pub fn send_list_to>(receiver: T, list: &[Atom]) -> Result<(), Sen /// /// # Example /// ```rust -/// use libpd_rs::send::{send_message_to}; +/// use libpd_rs::functions::send::{send_message_to}; /// use libpd_rs::types::Atom; +/// use libpd_rs::instance::PdInstance; /// -/// libpd_rs::init(); +/// let _main_instance = PdInstance::new().unwrap(); /// /// let values = vec![Atom::from(1.0)]; /// // Handle the error if the receiver object is not found @@ -399,20 +420,24 @@ pub fn send_list_to>(receiver: T, list: &[Atom]) -> Result<(), Sen /// /// A list of errors that can occur: /// - [`MissingDestination`](crate::error::SendError::MissingDestination) +/// - [`StringConversion`](crate::error::SendError::StringConversion) pub fn send_message_to>( receiver: T, message: T, list: &[Atom], ) -> Result<(), SendError> { - let recv = CString::new(receiver.as_ref()).expect(C_STRING_FAILURE); - let msg = CString::new(message.as_ref()).expect(C_STRING_FAILURE); + let recv = CString::new(receiver.as_ref()).map_err(StringConversionError::from)?; + let msg = CString::new(message.as_ref()).map_err(StringConversionError::from)?; let mut atom_list: Vec = make_t_atom_list_from_atom_list!(list); let atom_list_slice = atom_list.as_mut_slice(); unsafe { - #[allow(clippy::cast_possible_wrap)] - #[allow(clippy::cast_possible_truncation)] + #[expect( + clippy::cast_possible_wrap, + clippy::cast_possible_truncation, + reason = "This is what the function wants (i32). The value is never going to be negative or huge." + )] match libpd_sys::libpd_message( recv.as_ptr(), msg.as_ptr(), @@ -436,9 +461,10 @@ pub fn send_message_to>( /// /// # Example /// ```rust -/// use libpd_rs::send::{send_note_on}; +/// use libpd_rs::functions::send::{send_note_on}; +/// use libpd_rs::instance::PdInstance; /// -/// libpd_rs::init(); +/// let _main_instance = PdInstance::new().unwrap(); /// /// // Handle the error if the receiver object is not found /// send_note_on(0, 48, 64).unwrap_or_else(|err| { @@ -470,9 +496,10 @@ pub fn send_note_on(channel: i32, pitch: i32, velocity: i32) -> Result<(), SendE /// /// # Example /// ```rust -/// use libpd_rs::send::{send_control_change}; +/// use libpd_rs::functions::send::{send_control_change}; +/// use libpd_rs::instance::PdInstance; /// -/// libpd_rs::init(); +/// let _main_instance = PdInstance::new().unwrap(); /// /// // Handle the error if the receiver object is not found /// send_control_change(0, 0, 64).unwrap_or_else(|err| { @@ -504,9 +531,10 @@ pub fn send_control_change(channel: i32, controller: i32, value: i32) -> Result< /// /// # Example /// ```rust -/// use libpd_rs::send::{send_program_change}; +/// use libpd_rs::functions::send::{send_program_change}; +/// use libpd_rs::instance::PdInstance; /// -/// libpd_rs::init(); +/// let _main_instance = PdInstance::new().unwrap(); /// /// // Handle the error if the receiver object is not found /// send_program_change(0, 42).unwrap_or_else(|err| { @@ -540,9 +568,10 @@ pub fn send_program_change(channel: i32, value: i32) -> Result<(), SendError> { /// /// # Example /// ```rust -/// use libpd_rs::send::{send_pitch_bend}; +/// use libpd_rs::functions::send::{send_pitch_bend}; +/// use libpd_rs::instance::PdInstance; /// -/// libpd_rs::init(); +/// let _main_instance = PdInstance::new().unwrap(); /// /// // Handle the error if the receiver object is not found /// send_pitch_bend(0, 8192).unwrap_or_else(|err| { @@ -574,9 +603,10 @@ pub fn send_pitch_bend(channel: i32, value: i32) -> Result<(), SendError> { /// /// # Example /// ```rust -/// use libpd_rs::send::{send_after_touch}; +/// use libpd_rs::functions::send::{send_after_touch}; +/// use libpd_rs::instance::PdInstance; /// -/// libpd_rs::init(); +/// let _main_instance = PdInstance::new().unwrap(); /// /// // Handle the error if the receiver object is not found /// send_after_touch(0, 42).unwrap_or_else(|err| { @@ -608,9 +638,10 @@ pub fn send_after_touch(channel: i32, value: i32) -> Result<(), SendError> { /// /// # Example /// ```rust -/// use libpd_rs::send::{send_poly_after_touch}; +/// use libpd_rs::functions::send::{send_poly_after_touch}; +/// use libpd_rs::instance::PdInstance; /// -/// libpd_rs::init(); +/// let _main_instance = PdInstance::new().unwrap(); /// /// // Handle the error if the receiver object is not found /// send_poly_after_touch(0, 48, 64).unwrap_or_else(|err| { @@ -640,9 +671,10 @@ pub fn send_poly_after_touch(channel: i32, pitch: i32, value: i32) -> Result<(), /// /// # Example /// ```rust -/// use libpd_rs::send::{send_midi_byte}; +/// use libpd_rs::functions::send::{send_midi_byte}; +/// use libpd_rs::instance::PdInstance; /// -/// libpd_rs::init(); +/// let _main_instance = PdInstance::new().unwrap(); /// /// // Handle the error if the receiver object is not found /// send_midi_byte(0, 0xFF).unwrap_or_else(|err| { @@ -672,9 +704,10 @@ pub fn send_midi_byte(port: i32, byte: i32) -> Result<(), SendError> { /// /// # Example /// ```rust -/// use libpd_rs::send::{send_sysex}; +/// use libpd_rs::functions::send::{send_sysex}; +/// use libpd_rs::instance::PdInstance; /// -/// libpd_rs::init(); +/// let _main_instance = PdInstance::new().unwrap(); /// /// // Handle the error if the receiver object is not found /// send_sysex(0, 0x7F).unwrap_or_else(|err| { @@ -704,9 +737,10 @@ pub fn send_sysex(port: i32, byte: i32) -> Result<(), SendError> { /// /// # Example /// ```rust -/// use libpd_rs::send::{send_sys_realtime}; +/// use libpd_rs::functions::send::{send_sys_realtime}; +/// use libpd_rs::instance::PdInstance; /// -/// libpd_rs::init(); +/// let _main_instance = PdInstance::new().unwrap(); /// /// // Handle the error if the receiver object is not found /// send_sys_realtime(0, 0x7F).unwrap_or_else(|err| { diff --git a/src/helpers.rs b/src/helpers.rs index d98960a..77fbcfc 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -11,7 +11,7 @@ macro_rules! make_t_atom_list_from_atom_list { a_type: libpd_sys::t_atomtype_A_FLOAT, a_w: libpd_sys::word { w_float: *value }, }; - let p = &t_atom as *const libpd_sys::t_atom as *mut libpd_sys::t_atom; + let p = std::ptr::from_ref::(&t_atom).cast_mut(); // Using a setter us crucial or else float values become 0s when sending a list. unsafe { libpd_sys::libpd_set_double(p, *value); @@ -44,13 +44,13 @@ macro_rules! make_atom_list_from_t_atom_list { .map(|atom_type| match atom_type.a_type { libpd_sys::t_atomtype_A_FLOAT => { let ptr_to_inner = - atom_type as *const libpd_sys::t_atom as *mut libpd_sys::t_atom; + std::ptr::from_ref::(atom_type).cast_mut(); let f: f64 = unsafe { libpd_sys::libpd_get_double(ptr_to_inner) }; Atom::Float(f) } libpd_sys::t_atomtype_A_SYMBOL => { let ptr_to_inner = - atom_type as *const libpd_sys::t_atom as *mut libpd_sys::t_atom; + std::ptr::from_ref::(atom_type).cast_mut(); let sym: *const std::os::raw::c_char = unsafe { libpd_sys::libpd_get_symbol(ptr_to_inner) }; let result = unsafe { CStr::from_ptr(sym) }; diff --git a/src/instance.rs b/src/instance.rs new file mode 100644 index 0000000..9b704dc --- /dev/null +++ b/src/instance.rs @@ -0,0 +1,270 @@ +use libffi::high::{ClosureMut1, FnPtr1}; + +//`libpdimp_free` and `libpdimp_new` are the only functions that are not exposed. We don't want to forget them for the future. +// use libpd_sys::{libpdimp_free, libpdimp_new}; +use libpd_sys::{ + _pdinstance, libpd_free_instance, libpd_get_instancedata, libpd_main_instance, + libpd_new_instance, libpd_num_instances, libpd_set_instance, libpd_set_instancedata, + libpd_this_instance, t_libpd_freehook, +}; +use std::{any::TypeId, ffi::c_void, mem}; + +use crate::{error::InstanceError, functions}; + +type FreeHookCodePtr = *const FnPtr1<'static, *mut c_void, ()>; + +/// A Pure Data instance that can be used to process audio and handle Pd patches. +/// +/// # Thread Safety +/// +/// This type is both `Send` and `Sync` because: +/// - The underlying Pd instance maintains its own internal thread synchronization +/// - All mutations of the instance state are done through libpd's thread-safe API +/// - The internal pointer is only modified through synchronized libpd functions +/// - Instance data access is protected by type checking +/// +/// While the type is thread-safe, users should note that: +/// - Only one thread should process audio at a time for a given instance +/// - The instance should be set as current before processing audio or sending messages +/// - The instance remains valid as long as this struct exists +#[derive(Debug, Clone, Eq)] +pub struct PdInstance { + inner: *mut _pdinstance, + number: i32, + // Add a field to track the type + stored_type: Option, +} + +impl PartialEq for PdInstance { + fn eq(&self, other: &Self) -> bool { + self.number == other.number + } +} + +// Safe because libpd handles internal synchronization and we maintain +// exclusive access to the pointer through the public API +unsafe impl Send for PdInstance {} + +// Safe because all methods that mutate state use internal libpd locks +// and our public API ensures thread-safe access to the instance pointer +unsafe impl Sync for PdInstance {} + +impl PdInstance { + /// Create a new instance of Pd. + /// + /// If this is the first instance created, it will be the main instance. + /// The main instance is always valid. + /// + /// If this is not the first instance created, it will be a new instance. + /// + /// All instances created come initialized. + /// + /// Dropping an instance will free its resources properly. + /// + /// # Errors + /// + /// A list of errors that can occur: + /// - [`InstanceFailedToCreate`](crate::error::InstanceError::InstanceFailedToCreate) + pub fn new() -> Result { + // First instance as the main instance. + if instance_count() == 0 { + functions::init() + .map_err(|err| InstanceError::InstanceFailedToCreate(err.to_string()))?; + let main_instance_ptr = unsafe { libpd_main_instance() }; + // Set the current instance to the main instance. + unsafe { + libpd_set_instance(main_instance_ptr); + } + + return Ok(Self { + inner: main_instance_ptr, + // Since we've just successfully created the main instance, it's safe to dereference here. + number: unsafe { (*main_instance_ptr).pd_instanceno }, + stored_type: None, + }); + } + + let currently_set_instance_ptr = unsafe { libpd_this_instance() }; + let new_instance_ptr = unsafe { libpd_new_instance() }; + + if currently_set_instance_ptr.is_null() || new_instance_ptr.is_null() { + return Err(InstanceError::InstanceFailedToCreate( + "Returned instance pointer is null.".to_owned(), + )); + } + + // Set the current instance to the new instance to initialize it. + unsafe { + libpd_set_instance(new_instance_ptr); + } + + // TODO: Learn why it is required to be called after each instance creation and returns like the global init. (low priority) + functions::init().map_err(|err| InstanceError::InstanceFailedToCreate(err.to_string()))?; + + // Set the current instance back to the previous instance. + unsafe { + libpd_set_instance(currently_set_instance_ptr); + } + + Ok(Self { + inner: new_instance_ptr, + // Since we've just successfully created the main instance, it's safe to dereference here. + number: unsafe { (*new_instance_ptr).pd_instanceno }, + stored_type: None, + }) + } + + /// Get the raw pointer to the internal pd instance. + /// + /// From this point on you're responsible for managing the instance's lifetime. + /// + /// If you are not sure about what you're doing do not use this method. + /// + /// # Important + /// The caller must ensure they don't violate pd's threading and ownership rules + /// when using this pointer. + pub const fn as_ptr(&self) -> *mut _pdinstance { + self.inner + } + + /// Makes this instance the current instance. + /// + /// So that all subsequent calls to libpd functions will be made on this instance. + pub fn set_as_current(&mut self) { + unsafe { libpd_set_instance(self.inner) } + } + + /// Gets the instance number of this instance. + /// + /// Returns `pd_instanceno`. + pub const fn number(&self) -> i32 { + self.number + } + + /// Gets the system time of this instance. + /// + /// Returns `pd_systime`. + pub fn system_time(&self) -> f64 { + unsafe { (*self.inner).pd_systime } + } + + /// Gets if this instance is locked. + /// + /// Returns `pd_islocked`. + pub fn is_locked(&self) -> bool { + unsafe { (*self.inner).pd_islocked != 0 } + } + + /// Checks if this instance is the main instance. + /// + /// The main instance is the first instance created. + /// + /// The main instance is always valid. + pub fn is_main_instance(&self) -> bool { + let main_instance = unsafe { libpd_main_instance() }; + // # Safety + // Main instance is always valid, it is safe to dereference here. + let main_instance = unsafe { &mut *main_instance }; + main_instance.pd_instanceno == self.number + } + + /// Checks if this instance is set as the current instance. + pub fn is_current_instance(&self) -> bool { + unsafe { + if libpd_this_instance().is_null() { + return false; + } + } + + let current_instance = unsafe { libpd_this_instance() }; + // # Safety + // We've done a null check above, it is safe to dereference here. + let current_instance = unsafe { &mut *current_instance }; + current_instance.pd_instanceno == self.number + } + + /// Set custom instance data with an optional free hook + /// + /// We expose this since it is a library function but I'm not sure if it is useful. + pub fn set_instance_data(&mut self, data: T, free_hook: Option) + where + T: 'static + Send + Sync, + F: FnMut(&mut T) + Send + Sync + 'static, + { + let boxed = Box::new(data); + + // Handle the free hook if provided + let hook_ptr = free_hook.and_then(|mut free_hook| { + let closure: &'static mut _ = Box::leak(Box::new(move |data: *mut c_void| { + // Convert back to the original type for the user's closure + let data = unsafe { &mut *data.cast::() }; + free_hook(data); + })); + let callback = ClosureMut1::new(closure); + let code = callback.code_ptr() as FreeHookCodePtr; + let ptr = unsafe { *code.cast::() }; + mem::forget(callback); + ptr + }); + + unsafe { + libpd_set_instancedata(Box::into_raw(boxed).cast(), hook_ptr); + } + + self.stored_type = Some(TypeId::of::()); + } + + /// Get custom instance data + /// + /// Returns `None` + /// - If the stored type does not match the requested type. + /// - If no data is stored. + pub fn get_instance_data(&self) -> Option<&T> + where + T: 'static + Send + Sync, + { + // Check if the requested type matches what was stored + match self.stored_type { + Some(stored) if stored == TypeId::of::() => { + let ptr = unsafe { libpd_get_instancedata() }; + if ptr.is_null() { + None + } else { + // # Safety + // We've checked the type above, it is safe to dereference here. + Some(unsafe { &*(ptr as *const T) }) + } + } + _ => None, + } + } +} + +impl Drop for PdInstance { + fn drop(&mut self) { + if self.inner.is_null() { + return; + } + + // The advice below can be found in z_queued.h: + + // free the queued ringbuffers + // with multiple instances, call before freeing each instance: + // libpd_set_instance(pd1); + // libpd_queued_release(); + // libpd_free_instance(pd1); + + self.set_as_current(); + functions::release_internal_queues(); + unsafe { libpd_free_instance(self.inner) } + } +} + +#[expect( + clippy::cast_sign_loss, + reason = "The instance count can not be negative." +)] +/// Gets the number of instances of Pd registered. +pub fn instance_count() -> usize { + unsafe { libpd_num_instances() as usize } +} diff --git a/src/lib.rs b/src/lib.rs index 594b584..1e77dc7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,6 +7,7 @@ )] #![allow( // Group of too restrictive lints + clippy::allow_attributes_without_reason, clippy::undocumented_unsafe_blocks, clippy::as_conversions, clippy::arithmetic_side_effects, @@ -19,7 +20,7 @@ clippy::shadow_reuse, clippy::shadow_same, clippy::shadow_unrelated, - // clippy::must_use_candidate, + clippy::must_use_candidate, clippy::clone_on_ref_ptr, clippy::multiple_crate_versions, clippy::default_numeric_fallback, @@ -29,9 +30,18 @@ clippy::std_instead_of_core, clippy::partial_pub_fields, clippy::ref_patterns, + clippy::semicolon_inside_block, + clippy::semicolon_outside_block, + clippy::pub_with_shorthand, + clippy::self_named_module_files, + clippy::integer_division_remainder_used, + clippy::min_ident_chars, + clippy::missing_trait_methods, + clippy::mem_forget, + clippy::pub_use, // Expect is fine in relevant cases - clippy::expect_used, + // clippy::expect_used, // Too restrictive for the current style clippy::missing_inline_in_public_items, @@ -91,8 +101,8 @@ doc = ::embed_doc_image::embed_image!("phasor_patch", "assets/phasor_patch.png") //! Add the latest version of [libpd-rs](https://github.com/alisomay/libpd-rs) to your `Cargo.toml`: //! ```toml //! [dependencies] -//! libpd-rs = "0.1.9" -//! cpal = "0.13" +//! libpd-rs = "0.3" +//! cpal = "0.15" //! ``` //! We also add [cpal](https://github.com/RustAudio/cpal) to our dependencies //! to get access to the high priority audio callback from the OS. @@ -107,9 +117,9 @@ doc = ::embed_doc_image::embed_image!("phasor_patch", "assets/phasor_patch.png") //! //! Pd patches are not binary files, they are simple files full of pd commands as text. //! [libpd-rs](https://github.com/alisomay/libpd-rs) provides an additional way to -//! [evaluate](crate::convenience::PdGlobal::eval_patch) strings as pd patches. +//! [evaluate](crate::Pd::eval_patch) strings as pd patches. //! -//! This is the [method](crate::convenience::PdGlobal::eval_patch) we'll use in the following examples. +//! This is the [method](crate::Pd::eval_patch) we'll use in the following examples. //! //! ### Initialize, open patch, run //! @@ -119,10 +129,9 @@ doc = ::embed_doc_image::embed_image!("phasor_patch", "assets/phasor_patch.png") //! //! ```no_run //! use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; -//! use libpd_rs::convenience::{PdGlobal, calculate_ticks}; +//! use libpd_rs::{Pd, functions::util::calculate_ticks}; //! //! fn main() -> Result<(), Box> { -//! //! // Initialize cpal //! // This could have been another cross platform audio library //! // basically anything which gets you the audio callback of the os. @@ -140,7 +149,7 @@ doc = ::embed_doc_image::embed_image!("phasor_patch", "assets/phasor_patch.png") //! //! // Initialize libpd with that configuration, //! // with no input channels since we're not going to use them. -//! let mut pd = PdGlobal::init_and_configure(0, output_channels, sample_rate)?; +//! let mut pd = Pd::init_and_configure(0, output_channels, sample_rate)?; //! //! // Let's evaluate a pd patch. //! // We could have opened a `.pd` file also. @@ -170,7 +179,7 @@ doc = ::embed_doc_image::embed_image!("phasor_patch", "assets/phasor_patch.png") //! // we could have modified it to do pre-processing. //! //! // Process audio, advance internal scheduler. -//! libpd_rs::process::process_float(ticks, &[], data); +//! libpd_rs::functions::process::process_float(ticks, &[], data); //! //! // Here we could have done post processing //! // after pd processed our output buffer in place. @@ -219,8 +228,8 @@ doc = ::embed_doc_image::embed_image!("phasor_patch", "assets/phasor_patch.png") //! //! ```toml //! [dependencies] -//! libpd-rs = "0.1.9" -//! cpal = "0.13" +//! libpd-rs = "0.3" +//! cpal = "0.15" //! sys-info = "0.9.1" //! ``` //! Paste the code into your `main.rs`: @@ -230,7 +239,7 @@ doc = ::embed_doc_image::embed_image!("phasor_patch", "assets/phasor_patch.png") //! ```no_run //! use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; //! use libpd_rs::{ -//! convenience::{PdGlobal, calculate_ticks}, receive::on_float, receive::receive_messages_from_pd, send::send_list_to, +//! Pd, functions::{util::calculate_ticks, receive::on_float, receive::receive_messages_from_pd, send::send_list_to} //! }; //! use sys_info::loadavg; //! @@ -252,7 +261,7 @@ doc = ::embed_doc_image::embed_image!("phasor_patch", "assets/phasor_patch.png") //! //! // Initialize libpd with that configuration, //! // with no input channels since we're not going to use them. -//! let mut pd = PdGlobal::init_and_configure(0, output_channels, sample_rate)?; +//! let mut pd = Pd::init_and_configure(0, output_channels, sample_rate)?; //! //! // Let's evaluate another pd patch. //! // We could have opened a `.pd` file also. @@ -334,7 +343,7 @@ doc = ::embed_doc_image::embed_image!("phasor_patch", "assets/phasor_patch.png") //! receive_messages_from_pd(); //! //! // Process audio, advance internal scheduler. -//! libpd_rs::process::process_float(ticks, &[], data); +//! libpd_rs::functions::process::process_float(ticks, &[], data); //! //! // Here we could have done post processing after //! // pd processed our output buffer in place. @@ -414,28 +423,44 @@ doc = ::embed_doc_image::embed_image!("phasor_patch", "assets/phasor_patch.png") //! //! Enjoy! //! -//! ## Things to note +//! ## Create Layers +//! +//! This crate is thought as 3 layers. +//! +//! ### Low Level +//! These are the sys bindings. You can use them directly if you'd like to through the re-export of [libpd-sys](https://github.com/alisomay/libpd-sys) from this crate. +//! +//! Since [libpd-rs](https://github.com/alisomay/libpd-rs) exhaustively wraps the sys layer, you don't need to use this layer directly. +//! +//! But if you wish, you can do so and create your own abstractions on top of it. +//! +//! ### Mid Level +//! These are the safe wrappers directly around the sys bindings and sometimes with slight extra functionality. +//! +//! You can reach this layer through the [functions](crate::functions) module. +//! +//! While a little bit more high level, here you would still need an understanding of [libpd](https://github.com/libpd) to use it effectively. //! -//! [libpd](https://github.com/libpd/libpd) is a C library and the implementation allocates libpd **globally**. -//! This means that you can only have one instance of libpd running at a time. -//! There is support for [multi instances](https://github.com/libpd/libpd/blob/master/libpd_wrapper/z_libpd.h#L529) -//! in [libpd](https://github.com/libpd/libpd) but those -//! will be implemented in [libpd-rs](https://github.com/alisomay/libpd-rs) in the future. +//! This module is very heavily documented and tested so to learn further you can continue from here [functions](crate::functions). //! -//! This is not very Rust like and on top of that we do not manage the memory of that instantiation. -//! Using it from different threads is fine because [libpd](https://github.com/libpd/libpd) has -//! locking implemented inside it. +//! ### High Level +//! This layer consists of the [Pd](crate::Pd) struct, its methods and the types around it. //! -//! On the other hand, the programming style and state tracking might be a little different that how we do it in Rust. +//! Here we map one pure data instance to one [Pd](crate::Pd) instance. //! -//! There are many functions and less data structures in this crate. -//! Because all, of those functions act as methods for a globally -//! initialized singleton libpd instance. +//! There are convenient instance management, drop implementations and other high level functionality here. //! -//! There are limited ways of retrieving state from the running libpd instance but libpd-rs provides -//! [`PdGlobal`](crate::convenience::PdGlobal) to track some of the -//! state manually on the other hand this operation needs to be handled with care. -//! See [`PdGlobal`](crate::convenience::PdGlobal) for more details. +//! This is the most advised way to use this library. +//! +//! ### Mixing Layers +//! +//! Low level and mid level layers can be used together since one is just a safe wrapper over the other. +//! +//! Mixed usage of mid and the high level layer requires significant understanding of the library and [libpd](https://github.com/libpd). +//! +//! So if you don't know what you're doing, it's advised to stick with the high level layer and not mix it with the others. +//! +//! The library tries to make this as convenient as possible. //! //! ## Plans and Support //! @@ -465,732 +490,624 @@ doc = ::embed_doc_image::embed_image!("phasor_patch", "assets/phasor_patch.png") //! [BSD-3-Clause](https://opensource.org/licenses/BSD-3-Clause). //! See [LICENSE](https://raw.githubusercontent.com/alisomay/libpd-rs/main/LICENCE) file. -/// Work with pd arrays -/// -/// This module provides all tools to work with pd named arrays which are exposed by libpd with some extra safety such as bounds checking. -/// -/// Corresponding libpd functions in [libpd repository](https://github.com/libpd/libpd) could be explored [here](https://github.com/libpd/libpd/blob/master/libpd_wrapper/z_libpd.h#L115). -/// -/// # Examples -/// -/// ```rust -/// use libpd_rs::{ -/// array::{array_size, read_float_array_from, resize_array, write_float_array_to}, -/// close_patch, -/// init, initialize_audio, open_patch, -/// }; -/// -/// fn main() -> Result<(), Box> { -/// init()?; -/// initialize_audio(1, 2, 44100)?; -/// -/// // Place your own patch here. -/// let handle = open_patch("tests/patches/array_sketch_pad.pd")?; -/// -/// let size = array_size("sketch_pad")?; -/// -/// // Arrays are sized to 100 by default. -/// assert_eq!(size, 100); -/// -/// // We can resize this array to 8. -/// resize_array("sketch_pad", 8)?; -/// -/// let size = array_size("sketch_pad")?; -/// assert_eq!(size, 8); -/// -/// // Let's write some stuff to our array. -/// write_float_array_to("sketch_pad", 0, &[1.0, 2.0, 3.0, 4.0], 4)?; -/// -/// // Let's overwrite the second part of the array with the first part of our slice. -/// write_float_array_to("sketch_pad", 2, &[500.0, 600.0, 3.0, 4.0], 2)?; -/// -/// // Now we can read the array to the second half of our slice. -/// let mut read_to: Vec = vec![0.0; 4]; -/// -/// // Read it all back. -/// read_float_array_from("sketch_pad", 0, 4, &mut read_to)?; -/// assert_eq!(read_to, [1.0, 2.0, 500.0, 600.0]); -/// -/// // Now we can read the second half of our array to our first half of our slice. -/// read_float_array_from("sketch_pad", 2, 2, &mut read_to)?; -/// assert_eq!(read_to, [500.0, 600.0, 500.0, 600.0]); -/// -/// close_patch(handle)?; -/// -/// Ok(()) -/// } -/// ``` -pub mod array; -/// Convenience functions and types which encapsulate common actions when communicating with pd -/// -/// `libpd-rs` is a safe wrapper around [`libpd`](https://github.com/libpd/libpd) which provides a convenient interface to pd for Rust ecosystem. -/// -/// [`libpd`](https://github.com/libpd/libpd) internally initializes pd and provides a way to communicate with it but it does it in global scope. -/// On the Rust side we do not have access to that piece of memory and we need to use what we have from exposed APIs. -/// -/// [`libpd`](https://github.com/libpd/libpd) does not provide a very detailed way to track the state of pd yet. We need our own state tracking to track which -/// senders we have subscribed, which paths we have registered to the list of pd search paths or if the dsp is running or not. -/// -/// This module provides some types for this and some wrapper functions for easier communication with pd. -/// -/// There is one thing to note though. The state tracked in the Rust side is **decoupled** from libpd's internal state. -/// -/// User needs to be careful to design the application in a way that the state on the Rust side **always** reflects the state on the C side. -/// Check the struct [`PdGlobal`](crate::convenience::PdGlobal) for more details on this matter. -/// -/// # Examples -/// ```rust -/// use libpd_rs::convenience::PdGlobal; -/// -/// fn main() -> Result<(), Box> { -/// let mut pd = PdGlobal::init_and_configure(1, 2, 44100)?; -/// -/// // pd will keep track of the open patch. -/// pd.open_patch("tests/patches/sine.pd")?; -/// -/// // We may close the open patch safely any time. -/// pd.close_patch()?; -/// -/// pd.open_patch("tests/patches/sine.pd")?; -/// -/// // We may subscribe to senders -/// pd.subscribe_to_many(&["some_sender", "some_other_sender"])?; -/// -/// // Unsubscribe from one or many -/// pd.unsubscribe_from("some_sender"); -/// -/// // Or all -/// pd.unsubscribe_from_all(); -/// -/// // Activate or deactivate audio -/// pd.activate_audio(true)?; +/// TODO: +pub mod instance; + +/// TODO: +pub mod functions; + +/// Types for working with pd /// -/// // Check if it is active -/// assert!(pd.audio_active()); +/// Pd wraps primitive types such as a float or a string in a type called atom. +/// This enables pd to have heterogenous lists. /// -/// // And more.. -/// // Check `PdGlobal` type for all the things you may do with it, -/// // which includes examples and documentation. +/// This module exposes the representation of that type as a Rust enum, [`Atom`](crate::types::Atom). /// -/// Ok(()) -/// } -/// ``` -pub mod convenience; +/// It also exposes some others to hold file or receiver handles returned from libpd functions. +pub mod types; + /// All errors /// /// This module contains all the errors which can be returned by the library. pub mod error; -/// Start, stop, poll pd gui -/// -/// This module provides functions to start, stop and poll pd gui. -/// -/// If the pd desktop application is installed in your computer. -/// You may use these functions to launch or quit it. -pub mod gui; -/// Audio processing -/// -/// Process functions which you call in your audio callback are collected here. -/// -/// These functions also run the scheduler of pd. The chosen function needs to be called in a loop to keep pd "running". -/// -/// # Examples -/// -/// ```rust -/// use libpd_rs::{ -/// close_patch, -/// init, initialize_audio, open_patch, -/// process::process_float, -/// convenience::{dsp_on, calculate_ticks}, -/// receive::receive_messages_from_pd -/// }; -/// -/// fn main() -> Result<(), Box> { -/// init()?; -/// initialize_audio(1, 2, 44100)?; -/// -/// // Place your own patch here. -/// let patch_handle = open_patch("tests/patches/sine.pd")?; -/// -/// // Turn the dsp on -/// dsp_on()?; +pub(crate) mod helpers; + +use error::PdError; +use libpd_sys::_pdinstance; +use std::collections::HashMap; +use std::path::{Path, PathBuf}; +use std::{fs, ptr}; +use tempfile::NamedTempFile; + +use crate::instance::PdInstance; +use crate::{ + error::PatchLifeCycleError, + types::{PatchFileHandle, ReceiverHandle}, +}; + +/// Re-exports of the libpd-sys crate. +pub use libpd_sys; + +/// An abstraction provided for convenience to express a pure data instance, track the state and execute some common functions. /// -/// // Process the audio (imagine that this is your audio callback) -/// // We're going to treat this separate thread as the -/// // high priority audio callback from the OS for this example. +/// This struct represents a single instance of pd. /// -/// // Open some channels to communicate with it. -/// let (tx, rx) = std::sync::mpsc::channel::<()>(); +/// After created and registered internally the instance lives in libpd's memory. +/// Dropping this struct will free the resources of the instance. /// -/// let handle = std::thread::spawn(move || { -/// // Mimic audio callback buffers. -/// let input_buffer = [0.0f32; 512]; -/// let mut output_buffer = [0.0f32; 1024]; -/// -/// let output_channels = 2; -/// let sample_rate = 44100; -/// -/// // Run pd -/// loop { -/// // Mimic the call frequency of the audio callback. -/// let approximate_buffer_duration = -/// (output_buffer.len() as f32 / sample_rate as f32) * 1000.0; -/// std::thread::sleep(std::time::Duration::from_millis( -/// approximate_buffer_duration as u64, -/// )); -/// -/// // We may call this to also receive from internal ring buffers in this loop. -/// // So our registered listeners receive messages. -/// receive_messages_from_pd(); -/// -/// // Calculate ticks for the internal scheduler -/// let ticks = calculate_ticks(output_buffer.len() as i32, output_channels); -/// -/// // Process the audio and advance the internal scheduler by the number of ticks. -/// process_float(ticks, &input_buffer, &mut output_buffer); -/// -/// // This is just meaningful for this example, -/// // so we can break from this loop. -/// match rx.try_recv() { -/// Ok(_) => break, -/// _ => continue, -/// } -/// } -/// }); +/// It is also important to note that the mid-level layer of this library [`functions`](crate::functions) also can modify the state of this instance. /// -/// // When processing starts pd becomes alive because the scheduler is running. -/// -/// // After some time -/// std::thread::sleep(std::time::Duration::from_millis(1000)); -/// -/// // We may join the thread. -/// tx.send(()).unwrap(); -/// handle.join().unwrap(); +/// To learn more about how instances are created and managed, please see the [`instance`](crate::instance) module level documentation. /// -/// // And close the patch -/// close_patch(patch_handle)?; +/// This is why that if you're not very familiar with this library's and libpd's source code you shouldn't mix the high level layer with the mid level layer. /// -/// Ok(()) -/// } -/// ``` -pub mod process; -/// Receive messages from pd +/// # Example of an unwanted mix /// -/// Collection of endpoints where listeners (hooks) may be registered to receive messages from pd. -/// -/// # Examples /// ```rust -/// use libpd_rs::{ -/// close_patch, -/// init, initialize_audio, open_patch, -/// process::process_float, -/// convenience::{dsp_on, calculate_ticks}, -/// receive::{receive_messages_from_pd, on_print, on_float, start_listening_from} -/// }; -/// -/// fn main() -> Result<(), Box> { -/// init()?; -/// initialize_audio(1, 2, 44100)?; -/// -/// // Place your own patch here. -/// let patch_handle = open_patch("tests/patches/echo.pd")?; -/// -/// // Turn the dsp on -/// dsp_on()?; -/// -/// // Register some listeners -/// // Print is a special one which is always listened from. -/// on_print(|value| { -/// println!("{value}"); -/// }); -/// -/// on_float(|source, value| { -/// println!("{value} received from {source}"); -/// }); +/// use libpd_rs::Pd; +/// use libpd_rs::functions::util::dsp_off; /// -/// // For others we need to register them. -/// start_listening_from("float_from_pd")?; -/// -/// // We're going to treat this separate thread as the -/// // high priority audio callback from the OS for this example. +/// let mut pd = Pd::init_and_configure(1, 2, 44100).unwrap(); /// -/// // Open some channels to communicate with it. -/// let (tx, rx) = std::sync::mpsc::channel::<()>(); +/// // We call the method of [`Pd`] to activate audio +/// // which calls [`dsp_on`] internally which then sends a message +/// // to globally initialized pd to activate dsp. +/// pd.activate_audio(true).unwrap(); /// -/// let handle = std::thread::spawn(move || { -/// // Mimic audio callback buffers. -/// let input_buffer = [0.0f32; 512]; -/// let mut output_buffer = [0.0f32; 1024]; -/// -/// let output_channels = 2; -/// let sample_rate = 44100; -/// -/// // Run pd -/// loop { -/// // Mimic the call frequency of the audio callback. -/// let approximate_buffer_duration = -/// (output_buffer.len() as f32 / sample_rate as f32) * 1000.0; -/// std::thread::sleep(std::time::Duration::from_millis( -/// approximate_buffer_duration as u64, -/// )); -/// -/// // Receive messages from pd. -/// receive_messages_from_pd(); -/// -/// // Calculate ticks for the internal scheduler -/// let ticks = calculate_ticks(output_buffer.len() as i32, output_channels); -/// -/// // Process the audio and advance the internal scheduler by the number of ticks. -/// process_float(ticks, &input_buffer, &mut output_buffer); -/// -/// // This is just meaningful for this example, -/// // so we can break from this loop. -/// match rx.try_recv() { -/// Ok(_) => break, -/// _ => continue, -/// } -/// } -/// }); +/// // So far so good. +/// assert_eq!(pd.audio_active(), true); /// -/// // When processing starts pd becomes alive because the scheduler is running. -/// -/// // After some time -/// std::thread::sleep(std::time::Duration::from_millis(1000)); -/// -/// // We may join the thread. -/// tx.send(()).unwrap(); -/// handle.join().unwrap(); +/// // But we can send messages to globally initialized pd many ways +/// // and here is one of the ways we can do it. +/// dsp_off().unwrap(); /// -/// // And close the patch -/// close_patch(patch_handle)?; -/// -/// Ok(()) -/// } +/// // But now [`Pd`] is not aware of the mutated state +/// // of the instance in the background. +/// // The information it holds is outdated and not true anymore. +/// assert_eq!(pd.audio_active(), true); /// ``` -pub mod receive; -/// Send messages to pd -/// -/// Collection of functions where messages may be sent to pd -/// -/// # Examples -/// ```rust -/// use libpd_rs::{ -/// close_patch, -/// init, initialize_audio, open_patch, -/// process::process_float, -/// convenience::{dsp_on, calculate_ticks}, -/// receive::{receive_messages_from_pd, on_print, on_float, start_listening_from}, -/// send::{send_float_to} -/// }; -/// -/// fn main() -> Result<(), Box> { -/// init()?; -/// initialize_audio(1, 2, 44100)?; -/// -/// // Place your own patch here. -/// let patch_handle = open_patch("tests/patches/echo.pd")?; -/// -/// // Turn the dsp on -/// dsp_on()?; -/// -/// // Register some listeners -/// // Print is a special one which is always listened from. -/// on_print(|value| { -/// println!("{value}"); -/// }); -/// -/// on_float(|source, value| { -/// assert_eq!(source, "float_from_pd"); -/// assert_eq!(value, 42.0); -/// println!("{value} received from {source}"); -/// }); -/// -/// // For others we need to register them. -/// start_listening_from("float_from_pd")?; -/// -/// // We're going to treat this separate thread as the -/// // high priority audio callback from the OS for this example. -/// -/// // Open some channels to communicate with it. -/// let (tx, rx) = std::sync::mpsc::channel::<()>(); -/// -/// let handle = std::thread::spawn(move || { -/// // Mimic audio callback buffers. -/// let input_buffer = [0.0f32; 512]; -/// let mut output_buffer = [0.0f32; 1024]; -/// -/// let output_channels = 2; -/// let sample_rate = 44100; -/// -/// // Run pd -/// loop { -/// // Mimic the call frequency of the audio callback. -/// let approximate_buffer_duration = -/// (output_buffer.len() as f32 / sample_rate as f32) * 1000.0; -/// std::thread::sleep(std::time::Duration::from_millis( -/// approximate_buffer_duration as u64, -/// )); -/// -/// // Receive messages from pd. -/// receive_messages_from_pd(); -/// -/// // Calculate ticks for the internal scheduler -/// let ticks = calculate_ticks(output_buffer.len() as i32, output_channels); -/// -/// // Process the audio and advance the internal scheduler by the number of ticks. -/// process_float(ticks, &input_buffer, &mut output_buffer); -/// -/// // This is just meaningful for this example, -/// // so we can break from this loop. -/// match rx.try_recv() { -/// Ok(_) => break, -/// _ => continue, -/// } -/// } -/// }); /// -/// // When processing starts pd becomes alive because the scheduler is running. +/// To avoid this situation if you use [`Pd`] check its methods, only use them and **not** their function counterparts. /// -/// // Let's send a float to pd, it will be caught by the float listener, -/// // because our patch which we've loaded, echoes it back. -/// send_float_to("float_from_rust", 42.0)?; -/// -/// // After some time -/// std::thread::sleep(std::time::Duration::from_millis(1000)); -/// -/// // We may join the thread. -/// tx.send(()).unwrap(); -/// handle.join().unwrap(); -/// -/// // And close the patch -/// close_patch(patch_handle)?; -/// -/// Ok(()) -/// } -/// ``` -pub mod send; -/// Types for working with pd -/// -/// Pd wraps primitive types such as a float or a string in a type called atom. -/// This enables pd to have heterogenous lists. -/// -/// This module exposes the representation of that type as a Rust enum, [`Atom`](crate::types::Atom). -/// -/// It also exposes some others to hold file or receiver handles returned from libpd functions. -pub mod types; +/// If you really need to mix the layers, you should read the source of the relevant part before doing so. +pub struct Pd { + inner: PdInstance, + audio_active: bool, + input_channels: i32, + output_channels: i32, + sample_rate: i32, + running_patch: Option, + temporary_evaluated_patch: Option, + /// A store to keep track of subscriptions which are made to senders in pd through the app lifecycle. + pub subscriptions: HashMap, + /// A store to keep track of paths which are added to pd search paths through the app lifecycle. + pub search_paths: Vec, +} -pub(crate) mod helpers; -use error::{AudioInitializationError, PatchLifeCycleError}; +impl Pd { + // TODO: Document these functions maybe even add other impls exposing other functions through the instance. -use crate::{ - error::{InitializationError, IoError}, - types::PatchFileHandle, -}; + pub const fn inner(&self) -> &PdInstance { + &self.inner + } -use std::ffi::CString; -use std::path::{Path, PathBuf}; + pub fn inner_mut(&mut self) -> &mut PdInstance { + &mut self.inner + } -// TODO: Currently panicing is enough since this is a rare case, but may be improved later with a dedicated error. -pub(crate) const C_STRING_FAILURE: &str = - "Provided an invalid CString, check if your string contains null bytes in the middle."; -pub(crate) const C_STR_FAILURE: &str = "Converting a CStr to an &str is failed."; + pub fn audio_context(&self) -> PdAudioContext { + PdAudioContext { + instance: self.inner.clone(), + } + } -/// Initializes libpd. -/// -/// This function should be called **before** any other in this crate. -/// It initializes libpd **globally** and also initializes ring buffers for internal message passing. -/// -/// After setting internal hooks, it initializes `libpd` by calling the underlying -/// C function which is [`libpd_init`](https://github.com/libpd/libpd/blob/master/libpd_wrapper/z_libpd.c#L68). -/// See [`libpd_queued_init`](https://github.com/libpd/libpd/blob/master/libpd_wrapper/util/z_queued.c#L308) to -/// explore what it is doing. -/// -/// **Note**: *Support for multi instances of pd is not implemented yet.* -/// -/// # Example -/// ```rust -/// use libpd_rs::init; -/// -/// assert_eq!(init().is_ok(), true); -/// assert_eq!(init().is_err(), true); -/// ``` -/// -/// # Errors -/// -/// A second call to this function will return an error. -/// -/// A list of errors that can occur: -/// - [`AlreadyInitialized`](crate::error::InitializationError::AlreadyInitialized) -/// - [`RingBufferInitializationError`](crate::error::InitializationError::RingBufferInitializationError) -/// - [`InitializationFailed`](crate::error::InitializationError::InitializationFailed) -pub fn init() -> Result<(), InitializationError> { - unsafe { - match libpd_sys::libpd_queued_init() { - 0 => Ok(()), - -1 => Err(InitializationError::AlreadyInitialized), - -2 => Err(InitializationError::RingBufferInitializationError), - _ => Err(InitializationError::InitializationFailed), + pub fn set_as_current(&mut self) { + self.inner.set_as_current(); + } + + pub const fn instance_number(&self) -> i32 { + self.inner.number() + } + + pub fn is_main_instance(&self) -> bool { + self.inner.is_main_instance() + } + + pub fn is_current_instance(&self) -> bool { + self.inner.is_current_instance() + } + + pub(crate) fn set_as_active_instance(&mut self) -> ActiveInstanceGuard { + if self.inner.is_current_instance() { + // This would render the guard useless and as a no-op on drop which is what we want. + return ActiveInstanceGuard::wrap(ptr::null_mut::<_pdinstance>()); } + let previous_instance = unsafe { libpd_sys::libpd_this_instance() }; + self.inner.set_as_current(); + ActiveInstanceGuard::wrap(previous_instance) } -} -// TODO: This needs to be an internal function which frees the ring buffers with multiple instances support. -/// Frees the internal queued ring buffers. -/// -/// Currently I don't see a necessity to call this function in any case. -/// If you find a valid use case, please open an [issue](https://github.com/alisomay/libpd-rs/issues). -pub fn release_internal_queues() { - unsafe { - libpd_sys::libpd_queued_release(); - }; -} + /// Initializes a pd instance. + /// + /// It calls [`PdInstance::new`](crate::instance::PdInstance::new) and [`initialize_audio`](crate::initialize_audio) with the provided arguments and returns an instance where a user can keep simple state and call some convenience methods. + /// + /// You may crate any number of instances of [`Pd`] but only one of them can be active at a time. + /// Many of the methods in this struct would set it as the active instance before operating and reset it to the last set active after. + /// + /// It is your duty to keep this struct alive as long as you need to use it. + /// When it goes out of scope, the instance will be dropped and the pd instance it wraps will be destroyed. + /// + /// # Examples + /// ```rust + /// use libpd_rs::Pd; + /// + /// let mut pd = Pd::init_and_configure(1, 2, 44100).unwrap(); + /// ``` + /// + /// # Errors + /// + /// A list of errors that can occur: + /// - [`InitializationError`](crate::error::InitializationError) + /// - [`RingBufferInitializationError`](crate::error::InitializationError::RingBufferInitializationError) + /// - [`InitializationFailed`](crate::error::InitializationError::InitializationFailed) + /// - [`AudioInitializationError`](crate::error::AudioInitializationError) + /// - [`InitializationFailed`](crate::error::AudioInitializationError::InitializationFailed) + pub fn init_and_configure( + input_channels: i32, + output_channels: i32, + sample_rate: i32, + ) -> Result { + let inner = PdInstance::new()?; + functions::initialize_audio(input_channels, output_channels, sample_rate)?; + Ok(Self { + inner, + audio_active: false, + input_channels, + output_channels, + sample_rate, + running_patch: None, + temporary_evaluated_patch: None, + subscriptions: HashMap::default(), + search_paths: vec![], + }) + } -/// Clears all the paths where libpd searches for patches and assets. -/// -/// This function is also called by [`init`]. -pub fn clear_search_paths() { - unsafe { - libpd_sys::libpd_clear_search_path(); + /// Adds a path to the list of paths where this instance searches in. + /// + /// Relative paths are relative to the current working directory. + /// Unlike the desktop pd application, **no** search paths are set by default. + /// + /// # Errors + /// + /// A list of errors that can occur: + /// - [`IoError`](crate::error::IoError) + /// - [`PathDoesNotExist`](crate::error::IoError::PathDoesNotExist) + pub fn add_path_to_search_paths>(&mut self, path: T) -> Result<(), PdError> { + let _guard = self.set_as_active_instance(); + let path = path.as_ref().to_path_buf(); + if !self.search_paths.contains(&path) { + functions::add_to_search_paths(path.clone())?; + self.search_paths.push(path); + } + Ok(()) } -} -/// Adds a path to the list of paths where pd searches in. -/// -/// Relative paths are relative to the current working directory. -/// -/// Unlike the desktop pd application, **no** search paths are set by default. -/// -/// # Errors -/// -/// A list of errors that can occur: -/// - [`PathDoesNotExist`](crate::error::IoError::PathDoesNotExist) -pub fn add_to_search_paths>(path: T) -> Result<(), IoError> { - if !path.as_ref().exists() { - return Err(IoError::PathDoesNotExist( - path.as_ref().to_string_lossy().to_string(), - )); + /// Adds many paths to the list of paths where this instance searches in. + /// + /// Relative paths are relative to the current working directory. + /// Unlike the desktop pd application, **no** search paths are set by default. + /// + /// # Errors + /// + /// A list of errors that can occur: + /// - [`IoError`](crate::error::IoError) + /// - [`PathDoesNotExist`](crate::error::IoError::PathDoesNotExist) + pub fn add_paths_to_search_paths>( + &mut self, + paths: &[T], + ) -> Result<(), PdError> { + let _guard = self.set_as_active_instance(); + for path in paths { + if !self.search_paths.contains(&path.as_ref().to_path_buf()) { + functions::add_to_search_paths(path)?; + self.search_paths.push(path.as_ref().to_path_buf()); + } + } + Ok(()) } - unsafe { - let c_path = CString::new(&*path.as_ref().to_string_lossy()).expect(C_STRING_FAILURE); - libpd_sys::libpd_add_to_search_path(c_path.as_ptr()); + + /// Clears all the paths where this instance searches for patches and assets. + pub fn clear_all_search_paths(&mut self) { + let _guard = self.set_as_active_instance(); + functions::clear_search_paths(); + self.search_paths.clear(); + } + + /// Closes a pd patch for this instance. + /// + /// # Errors + /// + /// A list of errors that can occur: + /// - [`PatchLifeCycleError`](crate::error::PatchLifeCycleError) + /// - [`FailedToClosePatch`](crate::error::PatchLifeCycleError::FailedToClosePatch) + pub fn close_patch(&mut self) -> Result<(), PdError> { + let _guard = self.set_as_active_instance(); + if let Some(handle) = self.running_patch.take() { + functions::close_patch(handle)?; + } + self.temporary_evaluated_patch.take(); Ok(()) } -} -/// Opens a pd patch. -/// -/// The argument should be an absolute path to the patch file. -/// It would be useful to keep the return value of this function. -/// It can be used later to close it. -/// Absolute and relative paths are supported. -/// Relative paths and single file names are tried in executable directory and manifest directory. -/// -/// Tha function **first** checks the executable directory and **then** the manifest directory. -/// -/// # Examples -/// ```no_run -/// use libpd_rs::open_patch; -/// use std::path::PathBuf; -/// -/// let absolute_path = PathBuf::from("/home/user/my_patch.pd"); -/// let relative_path = PathBuf::from("../my_patch.pd"); -/// let patch_name = PathBuf::from("my_patch.pd"); -/// -/// let patch_handle = open_patch(&patch_name).unwrap(); -/// // or others.. -/// ``` -/// -/// # Errors -/// -/// A list of errors that can occur: -/// - [`FailedToOpenPatch`](crate::error::PatchLifeCycleError::FailedToOpenPatch) -/// - [`PathDoesNotExist`](crate::error::PatchLifeCycleError::PathDoesNotExist) -pub fn open_patch>( - path_to_patch: T, -) -> Result { - let file_name = path_to_patch - .as_ref() - .file_name() - .ok_or(PatchLifeCycleError::FailedToOpenPatch)?; - let file_name = file_name.to_string_lossy(); - let file_name = file_name.as_ref(); - let parent_path = path_to_patch - .as_ref() - .parent() - .unwrap_or_else(|| std::path::Path::new("/")); - let parent_path_string: String = parent_path.to_string_lossy().into(); + /// Opens a pd patch for this instance. + /// + /// The argument should be an absolute path to the patch file. + /// Absolute and relative paths are supported. + /// Relative paths and single file names are tried in executable directory and manifest directory. + /// + /// Tha function **first** checks the executable directory and **then** the manifest directory. + /// + /// # Examples + /// ```no_run + /// use libpd_rs::Pd; + /// + /// let mut pd = Pd::init_and_configure(1, 2, 44100).unwrap(); + /// assert!(pd.open_patch("tests/patches/sine.pd").is_ok()); + /// ``` + /// + /// # Errors + /// + /// A list of errors that can occur: + /// - [`PatchLifeCycleError`](crate::error::PatchLifeCycleError) + /// - [`FailedToClosePatch`](crate::error::PatchLifeCycleError::FailedToClosePatch) + /// - [`FailedToOpenPatch`](crate::error::PatchLifeCycleError::FailedToOpenPatch) + /// - [`PathDoesNotExist`](crate::error::PatchLifeCycleError::PathDoesNotExist) + pub fn open_patch>(&mut self, path: T) -> Result<(), PdError> { + let _guard = self.set_as_active_instance(); + if self.running_patch.is_some() { + self.close_patch()?; + } + self.running_patch = Some(functions::open_patch(path)?); + Ok(()) + } - // Assume absolute path. - let mut directory: String = parent_path_string.clone(); + /// Evaluate a string as a pd patch for this instance. + /// + /// This function creates a temporary file with the contents passed behind the scenes. + /// and saves it into the [`Pd`] struct holding onto it until the patch is closed or the instantiated [`Pd`] is dropped. + /// + /// Note: The patch opened after this evaluation could be closed safely with [`close_patch`](Pd::close_patch). + /// + /// # Examples + /// ```rust + /// use libpd_rs::Pd; + /// + /// let mut pd = Pd::init_and_configure(1, 2, 44100).unwrap(); + /// + /// assert!(pd.eval_patch( + /// r#" + /// #N canvas 577 549 158 168 12; + /// #X obj 23 116 dac~; + /// #X obj 23 17 osc~ 440; + /// #X obj 23 66 *~ 0.1; + /// #X obj 81 67 *~ 0.1; + /// #X connect 1 0 2 0; + /// #X connect 1 0 3 0; + /// #X connect 2 0 0 0; + /// #X connect 3 0 0 1; + /// "# + /// ,).is_ok()); + /// ``` + /// + /// # Errors + /// + /// A list of errors that can occur: + /// - [`PatchLifeCycleError`](crate::error::PatchLifeCycleError) + /// - [`FailedToEvaluateAsPatch`](crate::error::PatchLifeCycleError::FailedToEvaluateAsPatch) + /// - [`FailedToClosePatch`](crate::error::PatchLifeCycleError::FailedToClosePatch) + /// - [`FailedToOpenPatch`](crate::error::PatchLifeCycleError::FailedToOpenPatch) + /// - [`PathDoesNotExist`](crate::error::PatchLifeCycleError::PathDoesNotExist) + pub fn eval_patch>(&mut self, contents: T) -> Result<(), PdError> { + let _guard = self.set_as_active_instance(); + if self.running_patch.is_some() { + self.close_patch()?; + } + let temp_file = + NamedTempFile::new().map_err(|err| PatchLifeCycleError::FailedToEvaluateAsPatch { + content: contents.as_ref().to_owned(), + msg: err.to_string(), + })?; + fs::write(temp_file.path(), contents.as_ref()).map_err(|err| { + PatchLifeCycleError::FailedToEvaluateAsPatch { + content: contents.as_ref().to_owned(), + msg: err.to_string(), + } + })?; + self.running_patch = Some(functions::open_patch(temp_file.path())?); + self.temporary_evaluated_patch = Some(temp_file); + Ok(()) + } - if !parent_path.is_absolute() { - // "../some.pd" --> prepend working directory - if parent_path.is_relative() { - let mut app_dir = std::env::current_exe() - .map_err(|_| -> PatchLifeCycleError { PatchLifeCycleError::FailedToOpenPatch })?; - app_dir.pop(); - app_dir.push(parent_path); - let parent_path_str = app_dir.to_string_lossy(); + /// Starts listening messages from a source. + /// + /// If the source is already being listened to, this function will early return not doing anything without an error. + /// + /// # Examples + /// ```no_run + /// use libpd_rs::Pd; + /// + /// let mut pd = Pd::init_and_configure(1, 2, 44100).unwrap(); + /// pd.open_patch("tests/patches/sine.pd").unwrap(); + /// pd.subscribe_to("sender").unwrap(); + /// ``` + /// + /// # Errors + /// + /// A list of errors that can occur: + /// - [`SubscriptionError`](crate::error::SubscriptionError) + /// - [`FailedToSubscribeToSender`](crate::error::SubscriptionError::FailedToSubscribeToSender) + pub fn subscribe_to>(&mut self, source: T) -> Result<(), PdError> { + let _guard = self.set_as_active_instance(); + if self.subscriptions.contains_key(source.as_ref()) { + return Ok(()); + } + self.subscriptions.insert( + source.as_ref().to_owned(), + functions::receive::start_listening_from(source.as_ref())?, + ); + Ok(()) + } - if app_dir.exists() { - directory = parent_path_str.into(); - } else { - let manifest_dir = - PathBuf::from(&std::env!("CARGO_MANIFEST_DIR")).join(parent_path); - // Try manifest dir. - let manifest_dir_str = manifest_dir.to_string_lossy(); - directory = manifest_dir_str.into(); + /// Starts listening messages from many sources. + /// + /// If the any source is already being listened to, this function will will ignore them. + /// + /// # Examples + /// ```no_run + /// use libpd_rs::Pd; + /// + /// let mut pd = Pd::init_and_configure(1, 2, 44100).unwrap(); + /// pd.open_patch("tests/patches/sine.pd").unwrap(); + /// pd.subscribe_to_many(&["sender", "other_sender"]).unwrap(); + /// ``` + /// + /// # Errors + /// + /// A list of errors that can occur: + /// - [`SubscriptionError`](crate::error::SubscriptionError) + /// - [`FailedToSubscribeToSender`](crate::error::SubscriptionError::FailedToSubscribeToSender) + pub fn subscribe_to_many>(&mut self, sources: &[T]) -> Result<(), PdError> { + let _guard = self.set_as_active_instance(); + for source in sources { + if self.subscriptions.contains_key(source.as_ref()) { + continue; } + self.subscriptions.insert( + source.as_ref().to_owned(), + functions::receive::start_listening_from(source.as_ref())?, + ); } - // "some.pd" --> prepend working directory - if parent_path_string.is_empty() { - let mut app_dir = std::env::current_exe() - .map_err(|_| -> PatchLifeCycleError { PatchLifeCycleError::FailedToOpenPatch })?; - app_dir.pop(); - app_dir.push(file_name); - let parent_path_str = app_dir.to_string_lossy(); + Ok(()) + } + + /// Stops listening messages from a source. + /// + /// # Examples + /// ```no_run + /// use libpd_rs::Pd; + /// + /// let mut pd = Pd::init_and_configure(1, 2, 44100).unwrap(); + /// pd.open_patch("tests/patches/sine.pd").unwrap(); + /// pd.subscribe_to("sender").unwrap(); + /// pd.unsubscribe_from("sender"); + /// ``` + pub fn unsubscribe_from>(&mut self, source: T) { + let _guard = self.set_as_active_instance(); + if let Some(handle) = self.subscriptions.remove(source.as_ref()) { + functions::receive::stop_listening_from(handle); + } + } - if app_dir.exists() { - directory = parent_path_str.into(); - } else { - // Try manifest dir. - directory = std::env!("CARGO_MANIFEST_DIR").into(); + /// Stops listening messages from many sources. + /// + /// # Examples + /// ```no_run + /// use libpd_rs::Pd; + /// + /// let mut pd = Pd::init_and_configure(1, 2, 44100).unwrap(); + /// pd.open_patch("tests/patches/sine.pd").unwrap(); + /// pd.subscribe_to_many(&["sender", "other_sender"]).unwrap(); + /// + /// pd.unsubscribe_from_many(&["sender", "other_sender"]); + /// ``` + pub fn unsubscribe_from_many>(&mut self, sources: &[T]) { + let _guard = self.set_as_active_instance(); + for source in sources { + if let Some(handle) = self.subscriptions.remove(source.as_ref()) { + functions::receive::stop_listening_from(handle); } } } - // Invalid path. - let calculated_patch_path = PathBuf::from(&directory).join(file_name); - if !calculated_patch_path.exists() { - return Err(PatchLifeCycleError::PathDoesNotExist( - calculated_patch_path.to_string_lossy().to_string(), - )); + /// Stops listening from all sources. + /// + /// # Examples + /// ```no_run + /// use libpd_rs::Pd; + /// + /// let mut pd = Pd::init_and_configure(1, 2, 44100).unwrap(); + /// pd.open_patch("tests/patches/sine.pd").unwrap(); + /// pd.subscribe_to_many(&["sender", "other_sender"]).unwrap(); + /// + /// pd.unsubscribe_from_all(); + /// ``` + pub fn unsubscribe_from_all(&mut self) { + let _guard = self.set_as_active_instance(); + let sources: Vec = self.subscriptions.keys().cloned().collect(); + for source in &sources { + if let Some(handle) = self.subscriptions.remove(source) { + functions::receive::stop_listening_from(handle); + } + } } - // All good. - unsafe { - let name = CString::new(file_name).expect(C_STRING_FAILURE); - let directory = CString::new(directory).expect(C_STRING_FAILURE); - dbg!(&name, &directory); - let file_handle = - libpd_sys::libpd_openfile(name.as_ptr(), directory.as_ptr()).cast::(); - if file_handle.is_null() { - return Err(PatchLifeCycleError::FailedToOpenPatch); + /// Gets the `$0` of the running patch. + /// + /// `$0` id in pd could be thought as a auto generated unique identifier for the patch. + /// + /// # Errors + /// + /// A list of errors that can occur: + /// - [`PatchLifeCycleError`](crate::error::PatchLifeCycleError) + /// - [`PatchIsNotOpen`](crate::error::PatchLifeCycleError::PatchIsNotOpen) + pub fn dollar_zero(&mut self) -> Result { + let _guard = self.set_as_active_instance(); + if let Some(ref patch) = self.running_patch { + let dollar_zero = functions::get_dollar_zero(patch)?; + return Ok(dollar_zero); } - Ok(file_handle.into()) + Err(PatchLifeCycleError::PatchIsNotOpen.into()) } -} -/// Closes a pd patch which has opened before. -/// -/// Handle needs to point to a valid opened patch file. -/// -/// # Examples -/// ```no_run -/// use std::path::PathBuf; -/// use libpd_rs::{open_patch, close_patch}; -/// -/// let patch = PathBuf::from("my_patch.pd"); -/// let patch_handle = open_patch(&patch).unwrap(); -/// -/// assert!(close_patch(patch_handle).is_ok()); -/// ``` -/// # Errors -/// -/// A list of errors that can occur: -/// - [`FailedToClosePatch`](crate::error::PatchLifeCycleError::FailedToClosePatch) -pub fn close_patch(handle: PatchFileHandle) -> Result<(), PatchLifeCycleError> { - unsafe { - let ptr: *mut std::ffi::c_void = handle.into(); - if ptr.is_null() { - Err(PatchLifeCycleError::FailedToClosePatch) + /// Checks if the audio is active. + /// + /// # Important + /// + /// The state is tracked by libpd internally for the instance and we expose other functions to modify this state. + /// + /// If messages sent to pd previously used another way to modify this information, this state might not reflect reality. + pub const fn audio_active(&self) -> bool { + self.audio_active + } + + /// Activates or deactivates audio in pd. + /// + /// # Errors + /// + /// A list of errors that can occur: + /// - [`SendError`](crate::error::SendError) + /// - [`MissingDestination`](crate::error::SendError::MissingDestination) + /// - [`SizeError`](crate::error::SizeError) + /// - [`TooLarge`](crate::error::SizeError::TooLarge) + pub fn activate_audio(&mut self, on: bool) -> Result<(), PdError> { + let _guard = self.set_as_active_instance(); + if on && !self.audio_active { + functions::util::dsp_on()?; + self.audio_active = true; + } else if !on && self.audio_active { + functions::util::dsp_off()?; + self.audio_active = false; } else { - libpd_sys::libpd_closefile(ptr); - Ok(()) + return Ok(()); } + Ok(()) } -} -/// Gets the `$0` of the running patch. -/// -/// `$0` id in pd could be thought as a auto generated unique identifier number for the patch. -/// -/// # Errors -/// -/// A list of errors that can occur: -/// - [`PatchIsNotOpen`](crate::error::PatchLifeCycleError::PatchIsNotOpen) -pub fn get_dollar_zero(handle: &PatchFileHandle) -> Result { - unsafe { - match libpd_sys::libpd_getdollarzero(handle.as_mut_ptr()) { - 0 => Err(PatchLifeCycleError::PatchIsNotOpen), - other => Ok(other), - } + /// Gets the sample rate which pd is configured with. + /// + /// # Important + /// + /// The state is tracked by libpd internally for the instance and we expose other functions to modify this state. + /// + /// If messages sent to pd previously used another way to modify this information, this state might not reflect reality. + #[must_use] + pub const fn sample_rate(&self) -> i32 { + self.sample_rate + } + + /// Gets the number of input channels which pd is configured with. + /// + /// # Important + /// + /// The state is tracked by libpd internally for the instance and we expose other functions to modify this state. + /// + /// If messages sent to pd previously used another way to modify this information, this state might not reflect reality. + #[must_use] + pub const fn input_channels(&self) -> i32 { + self.input_channels + } + + /// Gets the number of output channels which pd is configured with. + /// + /// # Important + /// + /// The state is tracked by libpd internally for the instance and we expose other functions to modify this state. + /// + /// If messages sent to pd previously used another way to modify this information, this state might not reflect reality. + #[must_use] + pub const fn output_channels(&self) -> i32 { + self.output_channels } } -/// Returns pd's fixed block size which is `64` by default. -/// -/// The number of frames per 1 pd tick. -/// -/// For every pd tick, pd will process frames by the amount of block size. -/// e.g. this would make 128 samples if we have a stereo output and the default block size. -/// -/// It will first process the input buffers and then will continue with the output buffers. -/// Check the [`PROCESS`](https://github.com/libpd/libpd/blob/master/libpd_wrapper/z_libpd.c#L177) macro in `libpd` [source](https://github.com/libpd/libpd/blob/master/libpd_wrapper) for more information. -/// -/// # Examples -/// -/// ```rust -/// use libpd_rs::block_size; -/// -/// let block_size = block_size(); -/// let output_channels = 2; -/// let buffer_size = 1024; -/// -/// // Calculate pd ticks according to the upper information -/// let pd_ticks = buffer_size / (block_size * output_channels); -/// -/// // If we know about pd_ticks, then we can also calculate the buffer size, -/// assert_eq!(buffer_size, pd_ticks * (block_size * output_channels)); -/// ``` -#[must_use] -pub fn block_size() -> i32 { - unsafe { libpd_sys::libpd_blocksize() } +/// TODO: Document +#[derive(Debug, Clone)] +pub struct PdAudioContext { + instance: PdInstance, } -/// Initializes audio rendering -/// -/// This doesn't mean that the audio is actually playing. -/// To start audio processing please call [`dsp_on`](crate::convenience::dsp_on) function after the initialization. -/// -/// # Errors -/// -/// A list of errors that can occur: -/// - [`InitializationFailed`](crate::error::AudioInitializationError::InitializationFailed) -pub fn initialize_audio( - input_channels: i32, - output_channels: i32, - sample_rate: i32, -) -> Result<(), AudioInitializationError> { - unsafe { - match libpd_sys::libpd_init_audio(input_channels, output_channels, sample_rate) { - 0 => Ok(()), - _ => Err(AudioInitializationError::InitializationFailed), - } +impl PdAudioContext { + pub fn receive_messages_from_pd(&mut self) { + self.instance.set_as_current(); + functions::receive::receive_messages_from_pd(); + } + + pub fn receive_midi_messages_from_pd(&mut self) { + self.instance.set_as_current(); + functions::receive::receive_midi_messages_from_pd(); + } + + pub fn process_float(&mut self, ticks: i32, input: &[f32], output: &mut [f32]) { + self.instance.set_as_current(); + functions::process::process_float(ticks, input, output); + } + + pub fn process_double(&mut self, ticks: i32, input: &[f64], output: &mut [f64]) { + self.instance.set_as_current(); + functions::process::process_double(ticks, input, output); } + + pub fn process_short(&mut self, ticks: i32, input: &[i16], output: &mut [i16]) { + self.instance.set_as_current(); + functions::process::process_short(ticks, input, output); + } + + pub fn process_raw(&mut self, input: &[f32], output: &mut [f32]) { + self.instance.set_as_current(); + functions::process::process_raw(input, output); + } + + pub fn process_raw_short(&mut self, input: &[i16], output: &mut [i16]) { + self.instance.set_as_current(); + functions::process::process_raw_short(input, output); + } + + pub fn process_raw_double(&mut self, input: &[f64], output: &mut [f64]) { + self.instance.set_as_current(); + functions::process::process_raw_double(input, output); + } +} + +struct ActiveInstanceGuard { + previous_instance: *mut _pdinstance, } -/// Sets the flag for the functionality of verbose printing to the pd console -pub fn verbose_print_state(active: bool) { - if active { - unsafe { libpd_sys::libpd_set_verbose(1) } - } else { - unsafe { libpd_sys::libpd_set_verbose(0) } +impl ActiveInstanceGuard { + const fn wrap(previous_instance: *mut _pdinstance) -> Self { + Self { previous_instance } } } -/// Checks if verbose printing functionality to the pd console is active -#[must_use] -pub fn verbose_print_state_active() -> bool { - unsafe { libpd_sys::libpd_get_verbose() == 1 } +impl Drop for ActiveInstanceGuard { + fn drop(&mut self) { + if self.previous_instance.is_null() { + // TODO: Maybe inform the user about this? + return; + } + + unsafe { + libpd_sys::libpd_set_instance(self.previous_instance); + } + } } diff --git a/src/types.rs b/src/types.rs index 3071b6f..88df4c2 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,3 +1,6 @@ +use core::ffi; +use std::fmt::{self, Display}; + /// A type to represent a pd Atom type in Rust side. /// /// Pd has floating point numbers and symbols as primitive types. @@ -74,8 +77,8 @@ impl From<&char> for Atom { } } -impl core::fmt::Display for Atom { - fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { +impl Display for Atom { + fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), fmt::Error> { match self { Self::Float(float) => write!(f, "{float}"), Self::Symbol(s) => write!(f, "{s}"), @@ -89,24 +92,20 @@ impl core::fmt::Display for Atom { /// /// This handle should be kept alive for the lifetime of the patch. #[derive(Debug)] -pub struct PatchFileHandle(usize); +pub struct PatchFileHandle(*mut ffi::c_void); impl PatchFileHandle { - pub(crate) const fn as_mut_ptr(&self) -> *mut std::ffi::c_void { - self.0 as *mut std::ffi::c_void + pub(crate) const fn as_mut_ptr(&self) -> *mut ffi::c_void { + self.0 } -} -impl From<*mut std::ffi::c_void> for PatchFileHandle { - fn from(ptr: *mut std::ffi::c_void) -> Self { - Self(ptr as usize) + pub const fn into_inner(self) -> *mut ffi::c_void { + self.0 } } -// Needed in this case. -#[allow(clippy::from_over_into)] -impl Into<*mut std::ffi::c_void> for PatchFileHandle { - fn into(self) -> *mut std::ffi::c_void { - self.0 as *mut std::ffi::c_void +impl From<*mut ffi::c_void> for PatchFileHandle { + fn from(ptr: *mut ffi::c_void) -> Self { + Self(ptr) } } @@ -116,19 +115,17 @@ impl Into<*mut std::ffi::c_void> for PatchFileHandle { /// /// This handle could be used later to unsubscribe from the sender. #[derive(Debug)] -pub struct ReceiverHandle(usize); +pub struct ReceiverHandle(*mut ffi::c_void); -impl From<*mut std::ffi::c_void> for ReceiverHandle { - fn from(ptr: *mut std::ffi::c_void) -> Self { - Self(ptr as usize) +impl ReceiverHandle { + pub const fn into_inner(self) -> *mut ffi::c_void { + self.0 } } -// Might be needed -#[allow(clippy::from_over_into)] -impl Into<*mut std::ffi::c_void> for ReceiverHandle { - fn into(self) -> *mut std::ffi::c_void { - self.0 as *mut std::ffi::c_void +impl From<*mut ffi::c_void> for ReceiverHandle { + fn from(ptr: *mut ffi::c_void) -> Self { + Self(ptr) } } diff --git a/tests/arrays.rs b/tests/arrays.rs index cf311be..ac2f96d 100644 --- a/tests/arrays.rs +++ b/tests/arrays.rs @@ -1,6 +1,6 @@ #![allow(clippy::restriction)] -use libpd_rs::{ +use libpd_rs::functions::{ array::{ array_size, read_double_array_from, read_float_array_from, resize_array, write_double_array_to, write_float_array_to, @@ -10,8 +10,9 @@ use libpd_rs::{ #[test] fn apply_array_operations_in_a_row() { - libpd_rs::init().unwrap(); - let handle = libpd_rs::open_patch("tests/patches/array_sketch_pad.pd").unwrap(); + libpd_rs::functions::init().unwrap(); + + let handle = libpd_rs::functions::open_patch("tests/patches/array_sketch_pad.pd").unwrap(); let bad_name = "not_exists"; let sketch_pad = "sketch_pad"; diff --git a/tests/general.rs b/tests/general.rs index d1c40a1..8032506 100644 --- a/tests/general.rs +++ b/tests/general.rs @@ -1,11 +1,11 @@ #![allow(clippy::restriction)] #![allow(clippy::unnecessary_cast)] -use libpd_rs::{ +use libpd_rs::functions::{ add_to_search_paths, block_size, clear_search_paths, close_patch, get_dollar_zero, init, - initialize_audio, open_patch, types::PatchFileHandle, verbose_print_state, - verbose_print_state_active, + initialize_audio, open_patch, verbose_print_state, verbose_print_state_active, }; +use libpd_rs::types::PatchFileHandle; #[test] fn all_main_functionality() { @@ -13,7 +13,7 @@ fn all_main_functionality() { assert!(result.is_ok()); let result = init(); - assert!(result.is_err()); + assert!(result.is_ok()); let result = initialize_audio(0, 2, 44100); assert!(result.is_ok()); diff --git a/tests/gui.rs b/tests/gui.rs index fecad82..8365661 100644 --- a/tests/gui.rs +++ b/tests/gui.rs @@ -1,12 +1,12 @@ #![allow(clippy::restriction)] #![allow(unused)] -use libpd_rs::gui::{poll_gui, start_gui, stop_gui}; +use libpd_rs::functions::gui::{poll_gui, start_gui, stop_gui}; use std::{env, path::PathBuf}; #[test] fn start_poll_stop_gui() { - libpd_rs::init().unwrap(); + libpd_rs::functions::init().unwrap(); #[cfg(target_os = "macos")] { diff --git a/tests/listening.rs b/tests/listening.rs index ad4b3d4..e163684 100644 --- a/tests/listening.rs +++ b/tests/listening.rs @@ -1,13 +1,12 @@ #![allow(clippy::restriction)] #![allow(clippy::unnecessary_cast)] -use libpd_rs::{ - close_patch, - convenience::dsp_on, - init, initialize_audio, open_patch, +use libpd_rs::functions::{ + close_patch, init, initialize_audio, open_patch, receive::{source_to_listen_from_exists, start_listening_from, stop_listening_from}, - types::ReceiverHandle, + util::dsp_on, }; +use libpd_rs::types::ReceiverHandle; #[test] fn listening() { @@ -35,8 +34,8 @@ fn listening() { let handle: ReceiverHandle = (std::ptr::null_mut() as *mut std::ffi::c_void).into(); stop_listening_from(handle); - assert!(source_to_listen_from_exists("list_from_pd")); - assert!(!source_to_listen_from_exists("endpoint_not_created")); + assert!(source_to_listen_from_exists("list_from_pd").unwrap()); + assert!(!source_to_listen_from_exists("endpoint_not_created").unwrap()); close_patch(patch_handle).unwrap(); } diff --git a/tests/message_building.rs b/tests/message_building.rs index 95da91e..2c5e544 100644 --- a/tests/message_building.rs +++ b/tests/message_building.rs @@ -1,13 +1,12 @@ #![allow(clippy::restriction)] -use libpd_rs::{ - close_patch, - convenience::dsp_on, - init, initialize_audio, open_patch, +use libpd_rs::functions::{ + close_patch, init, initialize_audio, open_patch, send::{ add_double_to_started_message, finish_message_as_list_and_send_to, finish_message_as_typed_message_and_send_to, start_message, }, + util::dsp_on, }; #[test] diff --git a/tests/pd_global_dollar_zero.rs b/tests/pd_global_dollar_zero.rs index 99ff03f..e778909 100644 --- a/tests/pd_global_dollar_zero.rs +++ b/tests/pd_global_dollar_zero.rs @@ -1,8 +1,8 @@ -use libpd_rs::convenience::PdGlobal; +use libpd_rs::Pd; #[test] fn dollar_zero() { - let mut pd = PdGlobal::init_and_configure(0, 2, 44100).unwrap(); + let mut pd = Pd::init_and_configure(0, 2, 44100).unwrap(); pd.open_patch("tests/patches/sine.pd").unwrap(); assert!(pd.dollar_zero().is_ok()); assert_ne!(pd.dollar_zero().unwrap(), 0); diff --git a/tests/pd_global_search_paths.rs b/tests/pd_global_search_paths.rs index 740bf35..da32ab4 100644 --- a/tests/pd_global_search_paths.rs +++ b/tests/pd_global_search_paths.rs @@ -1,10 +1,10 @@ use std::path::PathBuf; -use libpd_rs::convenience::PdGlobal; +use libpd_rs::Pd; #[test] fn search_paths() { - let mut pd = PdGlobal::init_and_configure(0, 2, 44100).unwrap(); + let mut pd = Pd::init_and_configure(0, 2, 44100).unwrap(); assert!(pd.add_path_to_search_paths("does not exist").is_err()); assert!(pd diff --git a/tests/pd_global_state.rs b/tests/pd_global_state.rs index e7b9f01..0c51817 100644 --- a/tests/pd_global_state.rs +++ b/tests/pd_global_state.rs @@ -1,8 +1,8 @@ -use libpd_rs::convenience::PdGlobal; +use libpd_rs::Pd; #[test] fn state() { - let mut pd = PdGlobal::init_and_configure(0, 2, 44100).unwrap(); + let mut pd = Pd::init_and_configure(0, 2, 44100).unwrap(); pd.open_patch("tests/patches/sine.pd").unwrap(); assert!(!pd.audio_active()); pd.activate_audio(true).unwrap(); diff --git a/tests/pd_open_close_patch.rs b/tests/pd_open_close_patch.rs index c00e929..285a3b1 100644 --- a/tests/pd_open_close_patch.rs +++ b/tests/pd_open_close_patch.rs @@ -1,10 +1,12 @@ use std::sync::mpsc; -use libpd_rs::{block_size, convenience::PdGlobal, process::process_float}; +use libpd_rs::functions::block_size; +use libpd_rs::Pd; #[test] fn open_close_patch() { - let mut pd = PdGlobal::init_and_configure(0, 2, 44100).unwrap(); + let mut pd = Pd::init_and_configure(0, 2, 44100).unwrap(); + let mut ctx = pd.audio_context(); assert!(pd.open_patch("tests/patches/sine.pd").is_ok()); @@ -43,6 +45,7 @@ fn open_close_patch() { let (tx, rx) = mpsc::channel::<()>(); let sum_clone = sum.clone(); + let handle = std::thread::spawn(move || { // Mimic audio callback buffers. let input_buffer = [0.0f32; 512]; @@ -59,7 +62,7 @@ fn open_close_patch() { )); let ticks = output_buffer.len() as i32 / (block_size() * output_channels); - process_float(ticks, &input_buffer, &mut output_buffer); + ctx.process_float(ticks, &input_buffer, &mut output_buffer); // Collect samples to check if the patch runs later. sum_clone.store( diff --git a/tests/pd_subscribe_unsubscribe.rs b/tests/pd_subscribe_unsubscribe.rs index 81fbc5a..191c5ff 100644 --- a/tests/pd_subscribe_unsubscribe.rs +++ b/tests/pd_subscribe_unsubscribe.rs @@ -1,20 +1,20 @@ -use libpd_rs::convenience::PdGlobal; +use libpd_rs::Pd; #[test] fn subscribe_unsubscribe() { - let mut pd = PdGlobal::init_and_configure(0, 2, 44100).unwrap(); + let mut pd = Pd::init_and_configure(0, 2, 44100).unwrap(); assert!(pd.open_patch("tests/patches/sine.pd").is_ok()); assert!(pd.subscribe_to("a_source").is_ok()); - assert!(pd.subscriptions.get("a_source").is_some()); + assert!(pd.subscriptions.contains_key("a_source")); assert!(pd.subscribe_to_many(&["other", "another"]).is_ok()); - assert!(pd.subscriptions.get("a_source").is_some()); - assert!(pd.subscriptions.get("other").is_some()); - assert!(pd.subscriptions.get("another").is_some()); + assert!(pd.subscriptions.contains_key("a_source")); + assert!(pd.subscriptions.contains_key("other")); + assert!(pd.subscriptions.contains_key("another")); pd.unsubscribe_from("a_source"); pd.unsubscribe_from("a_source"); - assert!(pd.subscriptions.get("a_source").is_none()); + assert!(!pd.subscriptions.contains_key("a_source")); pd.unsubscribe_from_all(); - assert!(pd.subscriptions.get("a_source").is_none()); - assert!(pd.subscriptions.get("other").is_none()); - assert!(pd.subscriptions.get("another").is_none()); + assert!(!pd.subscriptions.contains_key("a_source")); + assert!(!pd.subscriptions.contains_key("other")); + assert!(!pd.subscriptions.contains_key("another")); } diff --git a/tests/process.rs b/tests/process.rs index 5e8b41f..b55a3ef 100644 --- a/tests/process.rs +++ b/tests/process.rs @@ -2,9 +2,9 @@ use std::any::type_name; -use libpd_rs::{ +use libpd_rs::functions::{ block_size, close_patch, - convenience::dsp_on, + util::dsp_on, init, initialize_audio, open_patch, process::{ process_double, process_float, process_raw, process_raw_double, process_raw_short, diff --git a/tests/send_and_receive_after_touch.rs b/tests/send_and_receive_after_touch.rs index 1778dec..bc8dbf1 100644 --- a/tests/send_and_receive_after_touch.rs +++ b/tests/send_and_receive_after_touch.rs @@ -3,12 +3,11 @@ use std::sync::{mpsc, Arc, Mutex}; use libpd_rs::{ - block_size, close_patch, - convenience::dsp_on, - init, initialize_audio, open_patch, - process::process_float, - receive::{on_midi_after_touch, receive_midi_messages_from_pd}, - send::send_after_touch, + functions::{ + block_size, close_patch, open_patch, receive::on_midi_after_touch, send::send_after_touch, + util::dsp_on, + }, + Pd, }; #[test] @@ -18,8 +17,9 @@ fn send_and_receive_after_touch() { let after_touch_messages_received: Arc>> = Arc::new(Mutex::new(vec![])); - init().unwrap(); - initialize_audio(0, output_channels, sample_rate).unwrap(); + let pd = Pd::init_and_configure(0, output_channels, sample_rate).unwrap(); + let mut ctx = pd.audio_context(); + dsp_on().unwrap(); let patch_handle = open_patch("tests/patches/echo.pd").unwrap(); @@ -45,9 +45,9 @@ fn send_and_receive_after_touch() { approximate_buffer_duration as u64, )); - receive_midi_messages_from_pd(); + ctx.receive_midi_messages_from_pd(); let ticks = output_buffer.len() as i32 / (block_size() * output_channels); - process_float(ticks, &input_buffer, &mut output_buffer); + ctx.process_float(ticks, &input_buffer, &mut output_buffer); match rx.try_recv() { Ok(_) => break, _ => continue, diff --git a/tests/send_and_receive_bang.rs b/tests/send_and_receive_bang.rs index 3183091..5dc5f39 100644 --- a/tests/send_and_receive_bang.rs +++ b/tests/send_and_receive_bang.rs @@ -3,12 +3,13 @@ use std::sync::{mpsc, Arc, Mutex}; use libpd_rs::{ - block_size, close_patch, - convenience::dsp_on, - init, initialize_audio, open_patch, - process::process_float, - receive::{on_bang, receive_messages_from_pd, start_listening_from, stop_listening_from}, - send::send_bang_to, + functions::{ + block_size, close_patch, open_patch, + receive::{on_bang, start_listening_from, stop_listening_from}, + send::send_bang_to, + util::dsp_on, + }, + Pd, }; #[test] @@ -18,8 +19,9 @@ fn send_and_receive_bang() { let bangs: Arc>> = Arc::new(Mutex::new(vec![])); - init().unwrap(); - initialize_audio(0, output_channels, sample_rate).unwrap(); + let pd = Pd::init_and_configure(0, output_channels, sample_rate).unwrap(); + let mut ctx = pd.audio_context(); + dsp_on().unwrap(); let patch_handle = open_patch("tests/patches/echo.pd").unwrap(); @@ -47,9 +49,9 @@ fn send_and_receive_bang() { approximate_buffer_duration as u64, )); - receive_messages_from_pd(); + ctx.receive_messages_from_pd(); let ticks = output_buffer.len() as i32 / (block_size() * output_channels); - process_float(ticks, &input_buffer, &mut output_buffer); + ctx.process_float(ticks, &input_buffer, &mut output_buffer); match rx.try_recv() { Ok(_) => break, _ => continue, diff --git a/tests/send_and_receive_control_change.rs b/tests/send_and_receive_control_change.rs index c1a08c9..2518e9d 100644 --- a/tests/send_and_receive_control_change.rs +++ b/tests/send_and_receive_control_change.rs @@ -3,12 +3,11 @@ use std::sync::{mpsc, Arc, Mutex}; use libpd_rs::{ - block_size, close_patch, - convenience::dsp_on, - init, initialize_audio, open_patch, - process::process_float, - receive::{on_midi_control_change, receive_midi_messages_from_pd}, - send::send_control_change, + functions::{ + block_size, close_patch, open_patch, receive::on_midi_control_change, + send::send_control_change, util::dsp_on, + }, + Pd, }; #[test] @@ -19,8 +18,9 @@ fn send_and_receive_control_change() { let control_change_messages_received: Arc>> = Arc::new(Mutex::new(vec![])); - init().unwrap(); - initialize_audio(0, output_channels, sample_rate).unwrap(); + let pd = Pd::init_and_configure(0, output_channels, sample_rate).unwrap(); + let mut ctx = pd.audio_context(); + dsp_on().unwrap(); let patch_handle = open_patch("tests/patches/echo.pd").unwrap(); @@ -49,9 +49,9 @@ fn send_and_receive_control_change() { approximate_buffer_duration as u64, )); - receive_midi_messages_from_pd(); + ctx.receive_midi_messages_from_pd(); let ticks = output_buffer.len() as i32 / (block_size() * output_channels); - process_float(ticks, &input_buffer, &mut output_buffer); + ctx.process_float(ticks, &input_buffer, &mut output_buffer); match rx.try_recv() { Ok(_) => break, _ => continue, diff --git a/tests/send_and_receive_double.rs b/tests/send_and_receive_double.rs index 78f7110..0996a9f 100644 --- a/tests/send_and_receive_double.rs +++ b/tests/send_and_receive_double.rs @@ -3,12 +3,13 @@ use std::sync::{mpsc, Arc, Mutex}; use libpd_rs::{ - block_size, close_patch, - convenience::dsp_on, - init, initialize_audio, open_patch, - process::process_float, - receive::{on_double, receive_messages_from_pd, start_listening_from, stop_listening_from}, - send::send_double_to, + functions::{ + block_size, close_patch, open_patch, + receive::{on_double, start_listening_from, stop_listening_from}, + send::send_double_to, + util::dsp_on, + }, + Pd, }; #[test] @@ -18,8 +19,9 @@ fn send_and_receive_double() { let floats: Arc>> = Arc::new(Mutex::new(vec![])); - init().unwrap(); - initialize_audio(0, output_channels, sample_rate).unwrap(); + let pd = Pd::init_and_configure(0, output_channels, sample_rate).unwrap(); + let mut ctx = pd.audio_context(); + dsp_on().unwrap(); let patch_handle = open_patch("tests/patches/echo.pd").unwrap(); @@ -47,9 +49,9 @@ fn send_and_receive_double() { approximate_buffer_duration as u64, )); - receive_messages_from_pd(); + ctx.receive_messages_from_pd(); let ticks = output_buffer.len() as i32 / (block_size() * output_channels); - process_float(ticks, &input_buffer, &mut output_buffer); + ctx.process_float(ticks, &input_buffer, &mut output_buffer); match rx.try_recv() { Ok(_) => break, _ => continue, diff --git a/tests/send_and_receive_float.rs b/tests/send_and_receive_float.rs index 7fde375..8f8ea8c 100644 --- a/tests/send_and_receive_float.rs +++ b/tests/send_and_receive_float.rs @@ -3,12 +3,13 @@ use std::sync::{mpsc, Arc, Mutex}; use libpd_rs::{ - block_size, close_patch, - convenience::dsp_on, - init, initialize_audio, open_patch, - process::process_float, - receive::{on_float, receive_messages_from_pd, start_listening_from, stop_listening_from}, - send::send_float_to, + functions::{ + block_size, close_patch, open_patch, + receive::{on_float, start_listening_from, stop_listening_from}, + send::send_float_to, + util::dsp_on, + }, + Pd, }; #[test] @@ -18,8 +19,9 @@ fn send_and_receive_float() { let floats: Arc>> = Arc::new(Mutex::new(vec![])); - init().unwrap(); - initialize_audio(0, output_channels, sample_rate).unwrap(); + let pd = Pd::init_and_configure(0, output_channels, sample_rate).unwrap(); + let mut ctx = pd.audio_context(); + dsp_on().unwrap(); let patch_handle = open_patch("tests/patches/echo.pd").unwrap(); @@ -47,9 +49,9 @@ fn send_and_receive_float() { approximate_buffer_duration as u64, )); - receive_messages_from_pd(); + ctx.receive_messages_from_pd(); let ticks = output_buffer.len() as i32 / (block_size() * output_channels); - process_float(ticks, &input_buffer, &mut output_buffer); + ctx.process_float(ticks, &input_buffer, &mut output_buffer); match rx.try_recv() { Ok(_) => break, _ => continue, diff --git a/tests/send_and_receive_list.rs b/tests/send_and_receive_list.rs index 623046c..8180b91 100644 --- a/tests/send_and_receive_list.rs +++ b/tests/send_and_receive_list.rs @@ -3,18 +3,20 @@ use std::sync::{mpsc, Arc, Mutex}; use libpd_rs::{ - block_size, close_patch, - convenience::dsp_on, - init, initialize_audio, open_patch, - process::process_float, - receive::{on_list, receive_messages_from_pd, start_listening_from, stop_listening_from}, - send::{ - add_double_to_started_message, add_symbol_to_started_message, - finish_message_as_list_and_send_to, send_list_to, start_message, + functions::{ + block_size, close_patch, open_patch, + receive::{on_list, start_listening_from, stop_listening_from}, + send::{ + add_double_to_started_message, add_symbol_to_started_message, + finish_message_as_list_and_send_to, send_list_to, start_message, + }, + util::dsp_on, }, - types::Atom, + Pd, }; +use libpd_rs::types::Atom; + #[test] fn send_and_receive_list() { let sample_rate = 44100; @@ -22,8 +24,9 @@ fn send_and_receive_list() { let list_received: Arc>> = Arc::new(Mutex::new(vec![])); - init().unwrap(); - initialize_audio(0, output_channels, sample_rate).unwrap(); + let pd = Pd::init_and_configure(0, output_channels, sample_rate).unwrap(); + let mut ctx = pd.audio_context(); + dsp_on().unwrap(); let patch_handle = open_patch("tests/patches/echo.pd").unwrap(); @@ -54,8 +57,8 @@ fn send_and_receive_list() { )); let ticks = output_buffer.len() as i32 / (block_size() * output_channels); - process_float(ticks, &input_buffer, &mut output_buffer); - receive_messages_from_pd(); + ctx.process_float(ticks, &input_buffer, &mut output_buffer); + ctx.receive_messages_from_pd(); match rx.try_recv() { Ok(_) => break, _ => continue, @@ -89,10 +92,10 @@ fn send_and_receive_list() { send_list_to("list_from_rust", &list_to_send).unwrap(); start_message(list_to_send.len() as i32).unwrap(); - add_symbol_to_started_message("daisy"); + add_symbol_to_started_message("daisy").unwrap(); add_double_to_started_message(33.5_f64); add_double_to_started_message(42_f64); - add_symbol_to_started_message("bang"); + add_symbol_to_started_message("bang").unwrap(); add_double_to_started_message(12.0_f64); add_double_to_started_message(0.0_f64); finish_message_as_list_and_send_to("list_from_rust").unwrap(); diff --git a/tests/send_and_receive_midi_byte.rs b/tests/send_and_receive_midi_byte.rs index 1761a69..d621008 100644 --- a/tests/send_and_receive_midi_byte.rs +++ b/tests/send_and_receive_midi_byte.rs @@ -3,12 +3,11 @@ use std::sync::{mpsc, Arc, Mutex}; use libpd_rs::{ - block_size, close_patch, - convenience::dsp_on, - init, initialize_audio, open_patch, - process::process_float, - receive::{on_midi_byte, receive_midi_messages_from_pd}, - send::send_midi_byte, + functions::{ + block_size, close_patch, open_patch, receive::on_midi_byte, send::send_midi_byte, + util::dsp_on, + }, + Pd, }; #[test] @@ -18,8 +17,9 @@ fn send_and_receive_midi_byte() { let midi_byte_messages_received: Arc>> = Arc::new(Mutex::new(vec![])); - init().unwrap(); - initialize_audio(0, output_channels, sample_rate).unwrap(); + let pd = Pd::init_and_configure(0, output_channels, sample_rate).unwrap(); + let mut ctx = pd.audio_context(); + dsp_on().unwrap(); let patch_handle = open_patch("tests/patches/echo.pd").unwrap(); @@ -45,9 +45,9 @@ fn send_and_receive_midi_byte() { approximate_buffer_duration as u64, )); - receive_midi_messages_from_pd(); + ctx.receive_midi_messages_from_pd(); let ticks = output_buffer.len() as i32 / (block_size() * output_channels); - process_float(ticks, &input_buffer, &mut output_buffer); + ctx.process_float(ticks, &input_buffer, &mut output_buffer); match rx.try_recv() { Ok(_) => break, _ => continue, diff --git a/tests/send_and_receive_note_on.rs b/tests/send_and_receive_note_on.rs index 28a1a3a..59b0193 100644 --- a/tests/send_and_receive_note_on.rs +++ b/tests/send_and_receive_note_on.rs @@ -3,12 +3,11 @@ use std::sync::{mpsc, Arc, Mutex}; use libpd_rs::{ - block_size, close_patch, - convenience::dsp_on, - init, initialize_audio, open_patch, - process::process_float, - receive::{on_midi_note_on, receive_midi_messages_from_pd}, - send::send_note_on, + functions::{ + block_size, close_patch, open_patch, receive::on_midi_note_on, send::send_note_on, + util::dsp_on, + }, + Pd, }; #[test] @@ -18,8 +17,9 @@ fn send_and_receive_note_on() { let note_on_messages_received: Arc>> = Arc::new(Mutex::new(vec![])); - init().unwrap(); - initialize_audio(0, output_channels, sample_rate).unwrap(); + let pd = Pd::init_and_configure(0, output_channels, sample_rate).unwrap(); + let mut ctx = pd.audio_context(); + dsp_on().unwrap(); let patch_handle = open_patch("tests/patches/echo.pd").unwrap(); @@ -48,9 +48,9 @@ fn send_and_receive_note_on() { approximate_buffer_duration as u64, )); - receive_midi_messages_from_pd(); + ctx.receive_midi_messages_from_pd(); let ticks = output_buffer.len() as i32 / (block_size() * output_channels); - process_float(ticks, &input_buffer, &mut output_buffer); + ctx.process_float(ticks, &input_buffer, &mut output_buffer); match rx.try_recv() { Ok(_) => break, _ => continue, diff --git a/tests/send_and_receive_pitch_bend.rs b/tests/send_and_receive_pitch_bend.rs index 8076cbc..11a3a7d 100644 --- a/tests/send_and_receive_pitch_bend.rs +++ b/tests/send_and_receive_pitch_bend.rs @@ -3,12 +3,11 @@ use std::sync::{mpsc, Arc, Mutex}; use libpd_rs::{ - block_size, close_patch, - convenience::dsp_on, - init, initialize_audio, open_patch, - process::process_float, - receive::{on_midi_pitch_bend, receive_midi_messages_from_pd}, - send::send_pitch_bend, + functions::{ + block_size, close_patch, open_patch, receive::on_midi_pitch_bend, send::send_pitch_bend, + util::dsp_on, + }, + Pd, }; #[test] @@ -18,8 +17,9 @@ fn send_and_receive_pitch_bend() { let pitch_bend_messages_received: Arc>> = Arc::new(Mutex::new(vec![])); - init().unwrap(); - initialize_audio(0, output_channels, sample_rate).unwrap(); + let pd = Pd::init_and_configure(0, output_channels, sample_rate).unwrap(); + let mut ctx = pd.audio_context(); + dsp_on().unwrap(); let patch_handle = open_patch("tests/patches/echo.pd").unwrap(); @@ -48,9 +48,9 @@ fn send_and_receive_pitch_bend() { approximate_buffer_duration as u64, )); - receive_midi_messages_from_pd(); + ctx.receive_midi_messages_from_pd(); let ticks = output_buffer.len() as i32 / (block_size() * output_channels); - process_float(ticks, &input_buffer, &mut output_buffer); + ctx.process_float(ticks, &input_buffer, &mut output_buffer); match rx.try_recv() { Ok(_) => break, _ => continue, diff --git a/tests/send_and_receive_poly_after_touch.rs b/tests/send_and_receive_poly_after_touch.rs index 3bd7db8..5295d75 100644 --- a/tests/send_and_receive_poly_after_touch.rs +++ b/tests/send_and_receive_poly_after_touch.rs @@ -3,12 +3,11 @@ use std::sync::{mpsc, Arc, Mutex}; use libpd_rs::{ - block_size, close_patch, - convenience::dsp_on, - init, initialize_audio, open_patch, - process::process_float, - receive::{on_midi_poly_after_touch, receive_midi_messages_from_pd}, - send::send_poly_after_touch, + functions::{ + block_size, close_patch, open_patch, receive::on_midi_poly_after_touch, + send::send_poly_after_touch, util::dsp_on, + }, + Pd, }; #[test] @@ -19,8 +18,9 @@ fn send_and_receive_poly_after_touch() { let poly_after_touch_messages_received: Arc>> = Arc::new(Mutex::new(vec![])); - init().unwrap(); - initialize_audio(0, output_channels, sample_rate).unwrap(); + let pd = Pd::init_and_configure(0, output_channels, sample_rate).unwrap(); + let mut ctx = pd.audio_context(); + dsp_on().unwrap(); let patch_handle = open_patch("tests/patches/echo.pd").unwrap(); @@ -49,9 +49,9 @@ fn send_and_receive_poly_after_touch() { approximate_buffer_duration as u64, )); - receive_midi_messages_from_pd(); + ctx.receive_midi_messages_from_pd(); let ticks = output_buffer.len() as i32 / (block_size() * output_channels); - process_float(ticks, &input_buffer, &mut output_buffer); + ctx.process_float(ticks, &input_buffer, &mut output_buffer); match rx.try_recv() { Ok(_) => break, _ => continue, diff --git a/tests/send_and_receive_program_change.rs b/tests/send_and_receive_program_change.rs index 62104d8..ba958eb 100644 --- a/tests/send_and_receive_program_change.rs +++ b/tests/send_and_receive_program_change.rs @@ -3,12 +3,11 @@ use std::sync::{mpsc, Arc, Mutex}; use libpd_rs::{ - block_size, close_patch, - convenience::dsp_on, - init, initialize_audio, open_patch, - process::process_float, - receive::{on_midi_program_change, receive_midi_messages_from_pd}, - send::send_program_change, + functions::{ + block_size, close_patch, open_patch, receive::on_midi_program_change, + send::send_program_change, util::dsp_on, + }, + Pd, }; #[test] @@ -19,8 +18,9 @@ fn send_and_receive_program_change() { let program_change_messages_received: Arc>> = Arc::new(Mutex::new(vec![])); - init().unwrap(); - initialize_audio(0, output_channels, sample_rate).unwrap(); + let pd = Pd::init_and_configure(0, output_channels, sample_rate).unwrap(); + let mut ctx = pd.audio_context(); + dsp_on().unwrap(); let patch_handle = open_patch("tests/patches/echo.pd").unwrap(); @@ -49,9 +49,9 @@ fn send_and_receive_program_change() { approximate_buffer_duration as u64, )); - receive_midi_messages_from_pd(); + ctx.receive_midi_messages_from_pd(); let ticks = output_buffer.len() as i32 / (block_size() * output_channels); - process_float(ticks, &input_buffer, &mut output_buffer); + ctx.process_float(ticks, &input_buffer, &mut output_buffer); match rx.try_recv() { Ok(_) => break, _ => continue, diff --git a/tests/send_and_receive_sys_realtime.rs b/tests/send_and_receive_sys_realtime.rs index 7533bf8..c84561b 100644 --- a/tests/send_and_receive_sys_realtime.rs +++ b/tests/send_and_receive_sys_realtime.rs @@ -3,12 +3,11 @@ use std::sync::{mpsc, Arc, Mutex}; use libpd_rs::{ - block_size, close_patch, - convenience::dsp_on, - init, initialize_audio, open_patch, - process::process_float, - receive::{on_midi_byte, receive_midi_messages_from_pd}, - send::send_sys_realtime, + functions::{ + block_size, close_patch, open_patch, receive::on_midi_byte, send::send_sys_realtime, + util::dsp_on, + }, + Pd, }; #[test] @@ -18,8 +17,9 @@ fn send_and_receive_sys_realtime() { let sys_realtime_messages_received: Arc>> = Arc::new(Mutex::new(vec![])); - init().unwrap(); - initialize_audio(0, output_channels, sample_rate).unwrap(); + let pd = Pd::init_and_configure(0, output_channels, sample_rate).unwrap(); + let mut ctx = pd.audio_context(); + dsp_on().unwrap(); let patch_handle = open_patch("tests/patches/echo.pd").unwrap(); @@ -45,9 +45,9 @@ fn send_and_receive_sys_realtime() { approximate_buffer_duration as u64, )); - receive_midi_messages_from_pd(); + ctx.receive_midi_messages_from_pd(); let ticks = output_buffer.len() as i32 / (block_size() * output_channels); - process_float(ticks, &input_buffer, &mut output_buffer); + ctx.process_float(ticks, &input_buffer, &mut output_buffer); match rx.try_recv() { Ok(_) => break, _ => continue, diff --git a/tests/send_and_receive_sysex.rs b/tests/send_and_receive_sysex.rs index fb9471d..6c766fb 100644 --- a/tests/send_and_receive_sysex.rs +++ b/tests/send_and_receive_sysex.rs @@ -3,12 +3,10 @@ use std::sync::{mpsc, Arc, Mutex}; use libpd_rs::{ - block_size, close_patch, - convenience::dsp_on, - init, initialize_audio, open_patch, - process::process_float, - receive::{on_midi_byte, receive_midi_messages_from_pd}, - send::send_sysex, + functions::{ + block_size, close_patch, open_patch, receive::on_midi_byte, send::send_sysex, util::dsp_on, + }, + Pd, }; #[test] @@ -18,8 +16,9 @@ fn send_and_receive_sysex() { let sysex_messages_received: Arc>> = Arc::new(Mutex::new(vec![])); - init().unwrap(); - initialize_audio(0, output_channels, sample_rate).unwrap(); + let pd = Pd::init_and_configure(0, output_channels, sample_rate).unwrap(); + let mut ctx = pd.audio_context(); + dsp_on().unwrap(); let patch_handle = open_patch("tests/patches/echo.pd").unwrap(); @@ -45,9 +44,9 @@ fn send_and_receive_sysex() { approximate_buffer_duration as u64, )); - receive_midi_messages_from_pd(); + ctx.receive_midi_messages_from_pd(); let ticks = output_buffer.len() as i32 / (block_size() * output_channels); - process_float(ticks, &input_buffer, &mut output_buffer); + ctx.process_float(ticks, &input_buffer, &mut output_buffer); match rx.try_recv() { Ok(_) => break, _ => continue, diff --git a/tests/send_and_receive_typed_message.rs b/tests/send_and_receive_typed_message.rs index af5b163..b4cef0e 100644 --- a/tests/send_and_receive_typed_message.rs +++ b/tests/send_and_receive_typed_message.rs @@ -3,13 +3,14 @@ use std::sync::{mpsc, Arc, Mutex}; use libpd_rs::{ - block_size, close_patch, - convenience::dsp_on, - init, initialize_audio, open_patch, - process::process_float, - receive::{on_message, receive_messages_from_pd, start_listening_from, stop_listening_from}, - send::{finish_message_as_typed_message_and_send_to, send_message_to, start_message}, - verbose_print_state, + functions::{ + block_size, close_patch, open_patch, + receive::{on_message, start_listening_from, stop_listening_from}, + send::{finish_message_as_typed_message_and_send_to, send_message_to, start_message}, + util::dsp_on, + verbose_print_state, + }, + Pd, }; #[test] @@ -17,9 +18,11 @@ fn send_and_receive_typed_message() { let sample_rate = 44100; let output_channels = 2; - init().unwrap(); - initialize_audio(0, output_channels, sample_rate).unwrap(); + let pd = Pd::init_and_configure(0, output_channels, sample_rate).unwrap(); + let mut ctx = pd.audio_context(); + dsp_on().unwrap(); + verbose_print_state(true); let patch_handle = open_patch("tests/patches/echo.pd").unwrap(); @@ -50,8 +53,8 @@ fn send_and_receive_typed_message() { )); let ticks = output_buffer.len() as i32 / (block_size() * output_channels); - process_float(ticks, &input_buffer, &mut output_buffer); - receive_messages_from_pd(); + ctx.process_float(ticks, &input_buffer, &mut output_buffer); + ctx.receive_messages_from_pd(); match rx.try_recv() { Ok(_) => break, _ => continue,