diff --git a/ctru-rs/examples/audio-filters.rs b/ctru-rs/examples/audio-filters.rs index 3ef76a6..34fa470 100644 --- a/ctru-rs/examples/audio-filters.rs +++ b/ctru-rs/examples/audio-filters.rs @@ -5,7 +5,7 @@ use std::f32::consts::PI; use ctru::linear::LinearAllocator; use ctru::prelude::*; use ctru::services::ndsp::{ - wave::{Wave, WaveStatus}, + wave::{Status, Wave}, AudioFormat, AudioMix, InterpolationType, Ndsp, OutputMode, }; @@ -146,7 +146,7 @@ fn main() { }; let status = current.status(); - if let WaveStatus::Done = status { + if let Status::Done = status { fill_buffer(current.get_buffer_mut().unwrap(), NOTEFREQ[note]); channel_zero.queue_wave(current).unwrap(); diff --git a/ctru-rs/src/services/ndsp/mod.rs b/ctru-rs/src/services/ndsp/mod.rs index f815fdb..fc76584 100644 --- a/ctru-rs/src/services/ndsp/mod.rs +++ b/ctru-rs/src/services/ndsp/mod.rs @@ -1,7 +1,11 @@ //! NDSP (Audio) service. +//! +//! The NDSP service is used to handle communications to the DSP processor present on the console's motherboard. +//! Thanks to the DSP processor the program can play sound effects and music on the console's built-in speakers or to any audio device +//! connected via the audio jack. pub mod wave; -use wave::{Wave, WaveStatus}; +use wave::{Status, Wave}; use crate::error::ResultCode; use crate::services::ServiceReference; @@ -27,7 +31,7 @@ pub enum OutputMode { Surround = ctru_sys::NDSP_OUTPUT_SURROUND, } -/// Audio PCM format. +/// PCM formats supported by the audio engine. #[derive(Copy, Clone, Debug, PartialEq, Eq)] #[repr(u32)] pub enum AudioFormat { @@ -35,13 +39,13 @@ pub enum AudioFormat { PCM8Mono = ctru_sys::NDSP_FORMAT_MONO_PCM8, /// PCM 16bit single-channel. PCM16Mono = ctru_sys::NDSP_FORMAT_MONO_PCM16, - /// PCM 8bit dual-channel. + /// PCM 8bit interleaved dual-channel. PCM8Stereo = ctru_sys::NDSP_FORMAT_STEREO_PCM8, - /// PCM 16bit dual-channel. + /// PCM 16bit interleaved dual-channel. PCM16Stereo = ctru_sys::NDSP_FORMAT_STEREO_PCM16, } -/// Representation of volume mix for a channel. +/// Representation of the volume mix for a channel. #[derive(Copy, Clone, Debug, PartialEq)] pub struct AudioMix { raw: [f32; 12], @@ -60,16 +64,16 @@ pub enum InterpolationType { None = ctru_sys::NDSP_INTERP_NONE, } -/// Error enum returned by NDSP methods. +/// Errors returned by [`ndsp`](self) functions. #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum NdspError { - /// Channel ID + /// Channel with the specified ID does not exist. InvalidChannel(u8), - /// Channel ID + /// Channel with the specified ID is already being used. ChannelAlreadyInUse(u8), - /// Channel ID + /// The wave is already busy playing in the channel with the specified ID. WaveBusy(u8), - /// Sample amount requested, Max sample amount + /// The sample amount requested was larger than the maximum. SampleCountOutOfBounds(usize, usize), } @@ -79,13 +83,15 @@ pub enum NdspError { /// /// # Default /// -/// NDSP initialises all channels with default values on creation, but the developer is supposed to change these values to correctly work with the service. +/// NDSP initialises all channels with default values on initialization, but the developer is supposed to change these values to correctly work with the service. /// /// In particular: /// - Default audio format is set to [`AudioFormat::PCM16Mono`]. /// - Default sample rate is set to 1 Hz. /// - Default interpolation type is set to [`InterpolationType::Polyphase`]. /// - Default mix is set to [`AudioMix::default()`] +/// +/// The handle to a channel can be retrieved with [`Ndsp::channel()`] pub struct Channel<'ndsp> { id: u8, _rf: RefMut<'ndsp, ()>, // we don't need to hold any data @@ -93,10 +99,9 @@ pub struct Channel<'ndsp> { static NDSP_ACTIVE: Mutex = Mutex::new(0); -/// Handler of the DSP service and DSP processor. +/// Handle to the DSP service. /// -/// This is the main struct to handle audio playback using the 3DS' speakers and headphone jack. -/// Only one "instance" of this struct can exist at a time. +/// Only one handle for this service can exist at a time. pub struct Ndsp { _service_handler: ServiceReference, channel_flags: [RefCell<()>; NUMBER_OF_CHANNELS as usize], @@ -107,8 +112,22 @@ impl Ndsp { /// /// # Errors /// - /// This function will return an error if an instance of the `Ndsp` struct already exists + /// This function will return an error if an instance of the [`Ndsp`] struct already exists /// or if there are any issues during initialization. + /// + /// # Example + /// + /// ```no_run + /// # use std::error::Error; + /// # fn main() -> Result<(), Box> { + /// # + /// use ctru::services::ndsp::Ndsp; + /// + /// let ndsp = Ndsp::new()?; + /// # + /// # Ok(()) + /// # } + /// ``` #[doc(alias = "ndspInit")] pub fn new() -> crate::Result { let _service_handler = ServiceReference::new( @@ -135,6 +154,21 @@ impl Ndsp { /// # Errors /// /// An error will be returned if the channel ID is not between 0 and 23 or if the specified channel is already being used. + /// + /// # Example + /// + /// ```no_run + /// # use std::error::Error; + /// # fn main() -> Result<(), Box> { + /// # + /// use ctru::services::ndsp::Ndsp; + /// let ndsp = Ndsp::new()?; + /// + /// let channel_0 = ndsp.channel(0)?; + /// # + /// # Ok(()) + /// # } + /// ``` pub fn channel(&self, id: u8) -> std::result::Result { let in_bounds = self.channel_flags.get(id as usize); @@ -150,7 +184,23 @@ impl Ndsp { } } - /// Set the audio output mode. Defaults to `OutputMode::Stereo`. + /// Set the audio output mode. Defaults to [`OutputMode::Stereo`]. + /// + /// # Example + /// + /// ```no_run + /// # use std::error::Error; + /// # fn main() -> Result<(), Box> { + /// # + /// use ctru::services::ndsp::{Ndsp, OutputMode}; + /// let mut ndsp = Ndsp::new()?; + /// + /// // Use dual-channel output. + /// ndsp.set_output_mode(OutputMode::Stereo); + /// # + /// # Ok(()) + /// # } + /// ``` #[doc(alias = "ndspSetOutputMode")] pub fn set_output_mode(&mut self, mode: OutputMode) { unsafe { ctru_sys::ndspSetOutputMode(mode.into()) }; @@ -158,31 +208,114 @@ impl Ndsp { } impl Channel<'_> { - /// Reset the channel + /// Reset the channel (clear the queue and reset parameters). + /// + /// # Example + /// + /// ```no_run + /// # use std::error::Error; + /// # fn main() -> Result<(), Box> { + /// # + /// use ctru::services::ndsp::Ndsp; + /// let ndsp = Ndsp::new()?; + /// let mut channel_0 = ndsp.channel(0)?; + /// + /// channel_0.reset(); + /// # + /// # Ok(()) + /// # } + /// ``` #[doc(alias = "ndspChnReset")] pub fn reset(&mut self) { unsafe { ctru_sys::ndspChnReset(self.id.into()) }; } - /// Initialize the channel's parameters + /// Initialize the channel's parameters with default values. + /// + /// # Example + /// + /// ```no_run + /// # use std::error::Error; + /// # fn main() -> Result<(), Box> { + /// # + /// use ctru::services::ndsp::Ndsp; + /// let ndsp = Ndsp::new()?; + /// let mut channel_0 = ndsp.channel(0)?; + /// + /// channel_0.init_parameters(); + /// # + /// # Ok(()) + /// # } + /// ``` #[doc(alias = "ndspChnInitParams")] - pub fn init_parameters(&self) { + pub fn init_parameters(&mut self) { unsafe { ctru_sys::ndspChnInitParams(self.id.into()) }; } /// Returns whether the channel is playing any audio. + /// + /// # Example + /// + /// ```no_run + /// # use std::error::Error; + /// # fn main() -> Result<(), Box> { + /// # + /// use ctru::services::ndsp::Ndsp; + /// let ndsp = Ndsp::new()?; + /// let mut channel_0 = ndsp.channel(0)?; + /// + /// // The channel is not playing any audio. + /// assert!(!channel_0.is_playing()); + /// # + /// # Ok(()) + /// # } + /// ``` #[doc(alias = "ndspChnIsPlaying")] pub fn is_playing(&self) -> bool { unsafe { ctru_sys::ndspChnIsPlaying(self.id.into()) } } /// Returns whether the channel's playback is currently paused. + /// + /// # Example + /// + /// ```no_run + /// # use std::error::Error; + /// # fn main() -> Result<(), Box> { + /// # + /// use ctru::services::ndsp::Ndsp; + /// let ndsp = Ndsp::new()?; + /// let mut channel_0 = ndsp.channel(0)?; + /// + /// // The channel is not paused. + /// assert!(!channel_0.is_paused()); + /// # + /// # Ok(()) + /// # } + /// ``` #[doc(alias = "ndspChnIsPaused")] pub fn is_paused(&self) -> bool { unsafe { ctru_sys::ndspChnIsPaused(self.id.into()) } } /// Returns the channel's index. + /// + /// # Example + /// + /// ```no_run + /// # use std::error::Error; + /// # fn main() -> Result<(), Box> { + /// # + /// use ctru::services::ndsp::Ndsp; + /// let ndsp = Ndsp::new()?; + /// let mut channel_0 = ndsp.channel(0)?; + /// + /// // The channel's index is 0. + /// assert_eq!(channel_0.id(), 0); + /// # + /// # Ok(()) + /// # } + /// ``` pub fn id(&self) -> u8 { self.id } @@ -202,41 +335,148 @@ impl Channel<'_> { } /// Pause or un-pause the channel's playback. + /// + /// # Example + /// + /// ```no_run + /// # use std::error::Error; + /// # fn main() -> Result<(), Box> { + /// # + /// use ctru::services::ndsp::Ndsp; + /// let ndsp = Ndsp::new()?; + /// let mut channel_0 = ndsp.channel(0)?; + /// + /// channel_0.set_paused(true); + /// + /// // The channel is paused. + /// assert!(channel_0.is_paused()); + /// # + /// # Ok(()) + /// # } + /// ``` #[doc(alias = "ndspChnSetPaused")] pub fn set_paused(&mut self, state: bool) { unsafe { ctru_sys::ndspChnSetPaused(self.id.into(), state) }; } /// Set the channel's output format. - /// Change this setting based on the used sample's format. + /// + /// Change this setting based on the used wave's format. + /// + /// # Example + /// + /// ```no_run + /// # use std::error::Error; + /// # fn main() -> Result<(), Box> { + /// # + /// use ctru::services::ndsp::{AudioFormat, Ndsp}; + /// let ndsp = Ndsp::new()?; + /// let mut channel_0 = ndsp.channel(0)?; + /// + /// // Use the PCM16 interleaved dual-channel audio format. + /// channel_0.set_format(AudioFormat::PCM16Stereo); + /// # + /// # Ok(()) + /// # } + /// ``` + // TODO: Channels treat all waves as equal and do not read their format when playing them. Another good reason to re-write the service. #[doc(alias = "ndspChnSetFormat")] pub fn set_format(&mut self, format: AudioFormat) { unsafe { ctru_sys::ndspChnSetFormat(self.id.into(), format.into()) }; } /// Set the channel's interpolation mode. + /// + /// # Example + /// + /// ```no_run + /// # use std::error::Error; + /// # fn main() -> Result<(), Box> { + /// # + /// use ctru::services::ndsp::{InterpolationType, Ndsp}; + /// let ndsp = Ndsp::new()?; + /// let mut channel_0 = ndsp.channel(0)?; + /// + /// // Use linear interpolation within frames. + /// channel_0.set_interpolation(InterpolationType::Linear); + /// # + /// # Ok(()) + /// # } + /// ``` #[doc(alias = "ndspChnSetInterp")] pub fn set_interpolation(&mut self, interp_type: InterpolationType) { unsafe { ctru_sys::ndspChnSetInterp(self.id.into(), interp_type.into()) }; } /// Set the channel's volume mix. + /// + /// Look at [`AudioMix`] for more information on the volume mix. + /// + /// # Example + /// + /// ```no_run + /// # use std::error::Error; + /// # use std::default::Default; + /// # fn main() -> Result<(), Box> { + /// # + /// use ctru::services::ndsp::{AudioMix, Ndsp}; + /// let ndsp = Ndsp::new()?; + /// let mut channel_0 = ndsp.channel(0)?; + /// + /// // Front-left and front-right channel maxed. + /// channel_0.set_mix(&AudioMix::default()); + /// # + /// # Ok(()) + /// # } + /// ``` #[doc(alias = "ndspChnSetMix")] pub fn set_mix(&mut self, mix: &AudioMix) { unsafe { ctru_sys::ndspChnSetMix(self.id.into(), mix.as_raw().as_ptr().cast_mut()) } } - /// Set the channel's rate of sampling. + /// Set the channel's rate of sampling in hertz. + /// + /// # Example + /// + /// ```no_run + /// # use std::error::Error; + /// # fn main() -> Result<(), Box> { + /// # + /// use ctru::services::ndsp::Ndsp; + /// let ndsp = Ndsp::new()?; + /// let mut channel_0 = ndsp.channel(0)?; + /// + /// // Standard CD sample rate. (44100 Hz) + /// channel_0.set_sample_rate(44100.); + /// # + /// # Ok(()) + /// # } + /// ``` #[doc(alias = "ndspChnSetRate")] pub fn set_sample_rate(&mut self, rate: f32) { unsafe { ctru_sys::ndspChnSetRate(self.id.into(), rate) }; } - // `ndspChnSetAdpcmCoefs` isn't wrapped on purpose. - // DSPADPCM is a proprietary format used by Nintendo, unavailable by "normal" means. - // We suggest using other wave formats when developing homebrew applications. + // TODO: wrap ADPCM format helpers. /// Clear the wave buffer queue and stop playback. + /// + /// # Example + /// + /// ```no_run + /// # use std::error::Error; + /// # fn main() -> Result<(), Box> { + /// # + /// use ctru::services::ndsp::Ndsp; + /// let ndsp = Ndsp::new()?; + /// let mut channel_0 = ndsp.channel(0)?; + /// + /// // Clear the audio queue and stop playback. + /// channel_0.clear_queue(); + /// # + /// # Ok(()) + /// # } + /// ``` #[doc(alias = "ndspChnWaveBufClear")] pub fn clear_queue(&mut self) { unsafe { ctru_sys::ndspChnWaveBufClear(self.id.into()) }; @@ -249,10 +489,36 @@ impl Channel<'_> { /// /// `libctru` expects the user to manually keep the info data (in this case [`Wave`]) alive during playback. /// To ensure safety, checks within [`Wave`] will clear the whole channel queue if any queued [`Wave`] is dropped prematurely. + /// + /// # Example + /// + /// ```no_run + /// # #![feature(allocator_api)] + /// # use std::error::Error; + /// # fn main() -> Result<(), Box> { + /// # + /// # use ctru::linear::LinearAllocator; + /// use ctru::services::ndsp::{AudioFormat, Ndsp, wave::Wave}; + /// let ndsp = Ndsp::new()?; + /// let mut channel_0 = ndsp.channel(0)?; + /// + /// # let _audio_data = Box::new_in([0u8; 96], LinearAllocator); + /// + /// // Provide your own audio data. + /// let mut wave = Wave::new(_audio_data, AudioFormat::PCM16Stereo, false); + /// + /// // Clear the audio queue and stop playback. + /// channel_0.queue_wave(&mut wave); + /// # + /// # Ok(()) + /// # } + /// ``` + // TODO: Find a better way to handle the wave lifetime problem. + // These "alive wave" shenanigans are the most substantial reason why I'd like to fully re-write this service in Rust. #[doc(alias = "ndspChnWaveBufAdd")] pub fn queue_wave(&mut self, wave: &mut Wave) -> std::result::Result<(), NdspError> { match wave.status() { - WaveStatus::Playing | WaveStatus::Queued => return Err(NdspError::WaveBusy(self.id)), + Status::Playing | Status::Queued => return Err(NdspError::WaveBusy(self.id)), _ => (), } @@ -354,9 +620,10 @@ impl Channel<'_> { impl AudioFormat { /// Returns the amount of bytes needed to store one sample /// - /// Eg. - /// 8 bit mono formats return 1 (byte) - /// 16 bit stereo (dual-channel) formats return 4 (bytes) + /// # Example + /// + /// - 8 bit mono formats return 1 (byte) + /// - 16 bit stereo (dual-channel) formats return 4 (bytes) pub const fn size(self) -> usize { match self { Self::PCM8Mono => 1, diff --git a/ctru-rs/src/services/ndsp/wave.rs b/ctru-rs/src/services/ndsp/wave.rs index 2819dbd..72cee96 100644 --- a/ctru-rs/src/services/ndsp/wave.rs +++ b/ctru-rs/src/services/ndsp/wave.rs @@ -1,9 +1,13 @@ -//! Audio wave representation. +//! Audio wave. +//! +//! This modules has all methods and structs required to work with audio waves meant to be played via the [`ndsp`](crate::services::ndsp) service. use super::{AudioFormat, NdspError}; use crate::linear::LinearAllocator; -/// Informational struct holding the raw audio data and playback info. This corresponds to [`ctru_sys::ndspWaveBuf`]. +/// Informational struct holding the raw audio data and playback info. +/// +/// You can play audio [`Wave`]s by using [`Channel::queue_wave()`](super::Channel::queue_wave). pub struct Wave { /// Data block of the audio wave (and its format information). buffer: Box<[u8], LinearAllocator>, @@ -15,8 +19,8 @@ pub struct Wave { #[derive(Copy, Clone, Debug, PartialEq, Eq)] #[repr(u8)] -/// Enum representing the playback status of a [`Wave`]. -pub enum WaveStatus { +/// Playback status of a [`Wave`]. +pub enum Status { /// Wave has never been used. Free = ctru_sys::NDSP_WBUF_FREE as u8, /// Wave is currently queued for usage. @@ -28,7 +32,23 @@ pub enum WaveStatus { } impl Wave { - /// Build a new playable wave object from a raw buffer on LINEAR memory and a some info. + /// Build a new playable wave object from a raw buffer on [LINEAR memory](`crate::linear`) and a some info. + /// + /// # Example + /// + /// ```no_run + /// # #![feature(allocator_api)] + /// # fn main() { + /// # + /// use ctru::linear::LinearAllocator; + /// use ctru::services::ndsp::{AudioFormat, wave::Wave}; + /// + /// // Zeroed box allocated in the LINEAR memory. + /// let audio_data = Box::new_in([0u8; 96], LinearAllocator); + /// + /// let wave = Wave::new(audio_data, AudioFormat::PCM16Stereo, false); + /// # } + /// ``` pub fn new( buffer: Box<[u8], LinearAllocator>, audio_format: AudioFormat, @@ -79,7 +99,7 @@ impl Wave { /// with the id to the channel in which it's queued. pub fn get_buffer_mut(&mut self) -> Result<&mut [u8], NdspError> { match self.status() { - WaveStatus::Playing | WaveStatus::Queued => { + Status::Playing | Status::Queued => { Err(NdspError::WaveBusy(self.played_on_channel.unwrap())) } _ => Ok(&mut self.buffer), @@ -87,7 +107,26 @@ impl Wave { } /// Return this wave's playback status. - pub fn status(&self) -> WaveStatus { + /// + /// # Example + /// + /// ```no_run + /// # #![feature(allocator_api)] + /// # fn main() { + /// # + /// # use ctru::linear::LinearAllocator; + /// # let _audio_data = Box::new_in([0u8; 96], LinearAllocator); + /// # + /// use ctru::services::ndsp::{AudioFormat, wave::{Wave, Status}}; + /// + /// // Provide your own audio data. + /// let wave = Wave::new(_audio_data, AudioFormat::PCM16Stereo, false); + /// + /// // The `Wave` is free if never played before. + /// assert!(matches!(wave.status(), Status::Free)); + /// # } + /// ``` + pub fn status(&self) -> Status { self.raw_data.status.try_into().unwrap() } @@ -118,7 +157,7 @@ impl Wave { /// # Note /// /// Operations of this kind are particularly useful to allocate memory pools - /// for VBR (Variable BitRate) Formats, like OGG Vorbis. + /// for VBR (Variable BitRate) formats, like OGG Vorbis. /// /// # Errors /// @@ -126,7 +165,7 @@ impl Wave { /// or if the [`Wave`] is currently queued. pub fn set_sample_count(&mut self, sample_count: usize) -> Result<(), NdspError> { match self.status() { - WaveStatus::Playing | WaveStatus::Queued => { + Status::Playing | Status::Queued => { return Err(NdspError::WaveBusy(self.played_on_channel.unwrap())); } _ => (), @@ -144,7 +183,7 @@ impl Wave { } } -impl TryFrom for WaveStatus { +impl TryFrom for Status { type Error = &'static str; fn try_from(value: u8) -> Result { @@ -163,7 +202,7 @@ impl Drop for Wave { // This was the only way I found I could check for improper drops of `Wave`. // A panic was considered, but it would cause issues with drop order against `Ndsp`. match self.status() { - WaveStatus::Free | WaveStatus::Done => (), + Status::Free | Status::Done => (), // If the status flag is "unfinished" _ => { // The unwrap is safe, since it must have a value in the case the status is "unfinished".