diff --git a/ctru-rs/Cargo.toml b/ctru-rs/Cargo.toml index f824d6d..94ff0bb 100644 --- a/ctru-rs/Cargo.toml +++ b/ctru-rs/Cargo.toml @@ -20,7 +20,6 @@ pthread-3ds = { git = "https://github.com/rust3ds/pthread-3ds.git" } libc = "0.2.121" bitflags = "1.0.0" widestring = "0.2.2" -once_cell = "1.10.0" [build-dependencies] toml = "0.5" @@ -31,6 +30,8 @@ futures = "0.3" time = "0.3.7" tokio = { version = "1.16", features = ["rt", "time", "sync", "macros"] } cfg-if = "1.0.0" +bytemuck = "1.12.3" +lewton = "0.10.2" [features] default = ["romfs", "big-stack"] diff --git a/ctru-rs/examples/audio-filters.rs b/ctru-rs/examples/audio-filters.rs new file mode 100644 index 0000000..d171dc0 --- /dev/null +++ b/ctru-rs/examples/audio-filters.rs @@ -0,0 +1,166 @@ +#![feature(allocator_api)] + +use std::f32::consts::PI; + +use ctru::linear::LinearAllocator; +use ctru::prelude::*; +use ctru::services::ndsp::{ + wave::{WaveInfo, WaveStatus}, + AudioFormat, InterpolationType, Ndsp, OutputMode, +}; + +const SAMPLE_RATE: usize = 22050; +const SAMPLES_PER_BUF: usize = SAMPLE_RATE / 10; // 2205 +const BYTES_PER_SAMPLE: usize = 4; +const AUDIO_WAVE_LENGTH: usize = SAMPLES_PER_BUF * BYTES_PER_SAMPLE; + +// Note Frequencies +const NOTEFREQ: [f32; 7] = [220., 440., 880., 1760., 3520., 7040., 14080.]; + +// The audio format is Stereo PCM16 +// As such, a sample is made up of 2 "Mono" samples (2 * i16 = u32), one for each channel (left and right) +fn fill_buffer(audio_data: &mut [u8], frequency: f32) { + let formatted_data = bytemuck::cast_slice_mut::<_, [i16; 2]>(audio_data); + + for (i, chunk) in formatted_data.iter_mut().enumerate() { + // This is a simple sine wave, with a frequency of `frequency` Hz, and an amplitude 30% of maximum. + let sample: f32 = (frequency * (i as f32 / SAMPLE_RATE as f32) * 2. * PI).sin(); + let amplitude = 0.3 * i16::MAX as f32; + + let result = (sample * amplitude) as i16; + + // Stereo samples are interleaved: left and right channels. + *chunk = [result, result]; + } +} + +fn main() { + ctru::init(); + let gfx = Gfx::init().expect("Couldn't obtain GFX controller"); + let hid = Hid::init().expect("Couldn't obtain HID controller"); + let apt = Apt::init().expect("Couldn't obtain APT controller"); + let _console = Console::init(gfx.top_screen.borrow_mut()); + + let mut note: usize = 4; + + // Filters + + let filter_names = [ + "None", + "Low-Pass", + "High-Pass", + "Band-Pass", + "Notch", + "Peaking", + ]; + + let mut filter: i32 = 0; + + // We set up two wave buffers and alternate between the two, + // effectively streaming an infinitely long sine wave. + + let mut audio_data1 = Box::new_in([0u8; AUDIO_WAVE_LENGTH], LinearAllocator); + fill_buffer(audio_data1.as_mut_slice(), NOTEFREQ[4]); + + let audio_data2 = audio_data1.clone(); + + let mut wave_info1 = WaveInfo::new(audio_data1, AudioFormat::PCM16Stereo, false); + let mut wave_info2 = WaveInfo::new(audio_data2, AudioFormat::PCM16Stereo, false); + + let mut ndsp = Ndsp::init().expect("Couldn't obtain NDSP controller"); + + // This line isn't needed since the default NDSP configuration already sets the output mode to `Stereo` + ndsp.set_output_mode(OutputMode::Stereo); + + let channel_zero = ndsp.channel(0).unwrap(); + channel_zero.set_interpolation(InterpolationType::Linear); + channel_zero.set_sample_rate(SAMPLE_RATE as f32); + channel_zero.set_format(AudioFormat::PCM16Stereo); + + // Output at 100% on the first pair of left and right channels. + + let mut mix: [f32; 12] = [0f32; 12]; + mix[0] = 1.0; + mix[1] = 1.0; + channel_zero.set_mix(&mix); + + channel_zero.queue_wave(&mut wave_info1).unwrap(); + channel_zero.queue_wave(&mut wave_info2).unwrap(); + + println!("\x1b[1;1HPress up/down to change tone frequency"); + println!("\x1b[2;1HPress left/right to change filter"); + println!("\x1b[4;1Hnote = {} Hz ", NOTEFREQ[note]); + println!( + "\x1b[5;1Hfilter = {} ", + filter_names[filter as usize] + ); + + let mut altern = true; // true is wave_info1, false is wave_info2 + + while apt.main_loop() { + hid.scan_input(); + let keys_down = hid.keys_down(); + + if keys_down.contains(KeyPad::KEY_START) { + break; + } // break in order to return to hbmenu + + if keys_down.intersects(KeyPad::KEY_DOWN) { + note = note.saturating_sub(1); + } else if keys_down.intersects(KeyPad::KEY_UP) { + note = std::cmp::min(note + 1, NOTEFREQ.len() - 1); + } + + let mut update_params = false; + if keys_down.intersects(KeyPad::KEY_LEFT) { + filter -= 1; + filter = filter.rem_euclid(filter_names.len() as _); + + update_params = true; + } else if keys_down.intersects(KeyPad::KEY_RIGHT) { + filter += 1; + filter = filter.rem_euclid(filter_names.len() as _); + + update_params = true; + } + + println!("\x1b[4;1Hnote = {} Hz ", NOTEFREQ[note]); + println!( + "\x1b[5;1Hfilter = {} ", + filter_names[filter as usize] + ); + + if update_params { + match filter { + 1 => channel_zero.iir_biquad_set_params_low_pass_filter(1760., 0.707), + 2 => channel_zero.iir_biquad_set_params_high_pass_filter(1760., 0.707), + 3 => channel_zero.iir_biquad_set_params_band_pass_filter(1760., 0.707), + 4 => channel_zero.iir_biquad_set_params_notch_filter(1760., 0.707), + 5 => channel_zero.iir_biquad_set_params_peaking_equalizer(1760., 0.707, 3.), + _ => channel_zero.iir_biquad_set_enabled(false), + } + } + + let current: &mut WaveInfo = if altern { + &mut wave_info1 + } else { + &mut wave_info2 + }; + + let status = current.get_status(); + if let WaveStatus::Done = status { + fill_buffer(current.get_buffer_mut().unwrap(), NOTEFREQ[note]); + + channel_zero.queue_wave(current).unwrap(); + + altern = !altern; + } + + // Flush and swap framebuffers + gfx.flush_buffers(); + gfx.swap_buffers(); + + //Wait for VBlank + gfx.wait_for_vblank(); + } +} diff --git a/ctru-rs/src/error.rs b/ctru-rs/src/error.rs index 18fd175..bdd2d2e 100644 --- a/ctru-rs/src/error.rs +++ b/ctru-rs/src/error.rs @@ -117,11 +117,7 @@ impl fmt::Display for Error { } } -impl error::Error for Error { - fn description(&self) -> &str { - "error originating from a libctru function" - } -} +impl error::Error for Error {} fn result_code_level_str(result: ctru_sys::Result) -> Cow<'static, str> { use ctru_sys::{ diff --git a/ctru-rs/src/gfx.rs b/ctru-rs/src/gfx.rs index 2e83832..fd439a0 100644 --- a/ctru-rs/src/gfx.rs +++ b/ctru-rs/src/gfx.rs @@ -1,6 +1,5 @@ //! LCD screens manipulation helper -use once_cell::sync::Lazy; use std::cell::{Ref, RefCell, RefMut}; use std::marker::PhantomData; use std::sync::Mutex; @@ -119,14 +118,13 @@ pub enum Side { /// provides helper functions and utilities for software rendering. /// /// The service exits when this struct is dropped. -#[non_exhaustive] pub struct Gfx { pub top_screen: RefCell, pub bottom_screen: RefCell, _service_handler: ServiceReference, } -static GFX_ACTIVE: Lazy> = Lazy::new(|| Mutex::new(0)); +static GFX_ACTIVE: Mutex = Mutex::new(0); impl Gfx { /// Initialize the Gfx module with the chosen framebuffer formats for the top and bottom diff --git a/ctru-rs/src/linear.rs b/ctru-rs/src/linear.rs index da91eb7..6076dc9 100644 --- a/ctru-rs/src/linear.rs +++ b/ctru-rs/src/linear.rs @@ -17,6 +17,7 @@ use std::ptr::NonNull; /// [`std::alloc::Allocator`] struct for LINEAR memory /// To use this struct the main crate must activate the `allocator_api` unstable feature. +#[derive(Copy, Clone, Default, Debug)] pub struct LinearAllocator; impl LinearAllocator { diff --git a/ctru-rs/src/romfs.rs b/ctru-rs/src/romfs.rs index 2a3def2..e601898 100644 --- a/ctru-rs/src/romfs.rs +++ b/ctru-rs/src/romfs.rs @@ -11,18 +11,16 @@ //! ``` use crate::error::ResultCode; -use once_cell::sync::Lazy; use std::ffi::CStr; use std::sync::Mutex; use crate::services::ServiceReference; -#[non_exhaustive] pub struct RomFS { _service_handler: ServiceReference, } -static ROMFS_ACTIVE: Lazy> = Lazy::new(|| Mutex::new(0)); +static ROMFS_ACTIVE: Mutex = Mutex::new(0); impl RomFS { pub fn init() -> crate::Result { diff --git a/ctru-rs/src/services/mod.rs b/ctru-rs/src/services/mod.rs index 5a52608..b0cc8ef 100644 --- a/ctru-rs/src/services/mod.rs +++ b/ctru-rs/src/services/mod.rs @@ -1,9 +1,17 @@ +//! System services used to handle system-specific functionalities. +//! +//! Most of the 3DS console's functionalities (when writing homebrew) are locked behind services, +//! which need to be initialized before accessing any particular feature. +//! +//! Some include: button input, audio playback, graphics rendering, built-in cameras, etc. + pub mod apt; pub mod cam; pub mod cfgu; pub mod fs; pub mod gspgpu; pub mod hid; +pub mod ndsp; pub mod ps; mod reference; pub mod soc; @@ -11,6 +19,5 @@ pub mod sslc; pub use self::apt::Apt; pub use self::hid::Hid; -pub use self::sslc::SslC; pub(crate) use self::reference::ServiceReference; diff --git a/ctru-rs/src/services/ndsp/mod.rs b/ctru-rs/src/services/ndsp/mod.rs new file mode 100644 index 0000000..39c485c --- /dev/null +++ b/ctru-rs/src/services/ndsp/mod.rs @@ -0,0 +1,342 @@ +//! NDSP (Audio) service + +pub mod wave; +use wave::{WaveInfo, WaveStatus}; + +use crate::error::ResultCode; +use crate::services::ServiceReference; + +use std::cell::{RefCell, RefMut}; +use std::error; +use std::fmt; +use std::sync::Mutex; + +const NUMBER_OF_CHANNELS: u8 = 24; + +#[derive(Copy, Clone, Debug)] +#[repr(u32)] +pub enum OutputMode { + Mono = ctru_sys::NDSP_OUTPUT_MONO, + Stereo = ctru_sys::NDSP_OUTPUT_STEREO, + Surround = ctru_sys::NDSP_OUTPUT_SURROUND, +} + +#[derive(Copy, Clone, Debug)] +#[repr(u32)] +pub enum AudioFormat { + PCM8Mono = ctru_sys::NDSP_FORMAT_MONO_PCM8, + PCM16Mono = ctru_sys::NDSP_FORMAT_MONO_PCM16, + PCM8Stereo = ctru_sys::NDSP_FORMAT_STEREO_PCM8, + PCM16Stereo = ctru_sys::NDSP_FORMAT_STEREO_PCM16, +} + +#[derive(Copy, Clone, Debug)] +#[repr(u32)] +pub enum InterpolationType { + Polyphase = ctru_sys::NDSP_INTERP_POLYPHASE, + Linear = ctru_sys::NDSP_INTERP_LINEAR, + None = ctru_sys::NDSP_INTERP_NONE, +} + +#[derive(Copy, Clone, Debug)] +pub enum NdspError { + /// Channel ID + InvalidChannel(u8), + /// Channel ID + ChannelAlreadyInUse(u8), + /// Channel ID + WaveBusy(u8), + /// Sample amount requested, Max sample amount + SampleCountOutOfBounds(u32, u32), +} + +pub struct Channel<'ndsp> { + id: u8, + _rf: RefMut<'ndsp, ()>, // we don't need to hold any data +} + +static NDSP_ACTIVE: Mutex = Mutex::new(0); + +/// Handler of the DSP service and DSP processor. +/// +/// 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. +pub struct Ndsp { + _service_handler: ServiceReference, + channel_flags: [RefCell<()>; NUMBER_OF_CHANNELS as usize], +} + +impl Ndsp { + /// Initialize the DSP service and audio units. + /// + /// # Errors + /// + /// This function will return an error if an instance of the `Ndsp` struct already exists + /// or if there are any issues during initialization. + pub fn init() -> crate::Result { + let _service_handler = ServiceReference::new( + &NDSP_ACTIVE, + false, + || { + ResultCode(unsafe { ctru_sys::ndspInit() })?; + + Ok(()) + }, + || unsafe { + ctru_sys::ndspExit(); + }, + )?; + + Ok(Self { + _service_handler, + channel_flags: Default::default(), + }) + } + + /// Return a representation of the specified channel. + /// + /// # 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. + pub fn channel(&self, id: u8) -> std::result::Result { + let in_bounds = self.channel_flags.get(id as usize); + + match in_bounds { + Some(ref_cell) => { + let flag = ref_cell.try_borrow_mut(); + match flag { + Ok(_rf) => Ok(Channel { id, _rf }), + Err(_) => Err(NdspError::ChannelAlreadyInUse(id)), + } + } + None => Err(NdspError::InvalidChannel(id)), + } + } + + /// Set the audio output mode. Defaults to `OutputMode::Stereo`. + pub fn set_output_mode(&mut self, mode: OutputMode) { + unsafe { ctru_sys::ndspSetOutputMode(mode as u32) }; + } +} + +impl Channel<'_> { + /// Reset the channel + pub fn reset(&self) { + unsafe { ctru_sys::ndspChnReset(self.id.into()) }; + } + + /// Initialize the channel's parameters + pub fn init_parameters(&self) { + unsafe { ctru_sys::ndspChnInitParams(self.id.into()) }; + } + + /// Returns whether the channel is playing any audio. + pub fn is_playing(&self) -> bool { + unsafe { ctru_sys::ndspChnIsPlaying(self.id.into()) } + } + + /// Returns whether the channel's playback is currently paused. + pub fn is_paused(&self) -> bool { + unsafe { ctru_sys::ndspChnIsPaused(self.id.into()) } + } + + // Returns the channel's id + pub fn get_id(&self) -> u8 { + self.id + } + + /// Returns the channel's current sample's position. + pub fn get_sample_position(&self) -> u32 { + unsafe { ctru_sys::ndspChnGetSamplePos(self.id.into()) } + } + + /// Returns the channel's current wave sequence's id. + pub fn get_wave_sequence_id(&self) -> u16 { + unsafe { ctru_sys::ndspChnGetWaveBufSeq(self.id.into()) } + } + + /// Pause or un-pause the channel's playback. + pub fn set_paused(&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. + pub fn set_format(&self, format: AudioFormat) { + unsafe { ctru_sys::ndspChnSetFormat(self.id.into(), format as u16) }; + } + + /// Set the channel's interpolation mode. + pub fn set_interpolation(&self, interp_type: InterpolationType) { + unsafe { ctru_sys::ndspChnSetInterp(self.id.into(), interp_type as u32) }; + } + + /// Set the channel's volume mix. + /// + /// # Notes + /// + /// The buffer's format is read as: + /// + /// Index 0: Front left volume
+ /// Index 1: Front right volume
+ /// Index 2: Back left volume
+ /// Index 3: Back right volume
+ /// Index 4..7: Same as 0..3 but for auxiliary output 0
+ /// Index 8..11: Same as 0..3 but for auxiliary output 1
+ pub fn set_mix(&self, mix: &[f32; 12]) { + unsafe { ctru_sys::ndspChnSetMix(self.id.into(), mix.as_ptr().cast_mut()) } + } + + /// Set the channel's rate of sampling. + pub fn set_sample_rate(&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. + + /// Clear the wave buffer queue and stop playback. + pub fn clear_queue(&self) { + unsafe { ctru_sys::ndspChnWaveBufClear(self.id.into()) }; + } + + /// Add a wave buffer to the channel's queue. + /// If there are no other buffers in queue, playback for this buffer will start. + /// + /// # Warning + /// + /// `libctru` expects the user to manually keep the info data (in this case [WaveInfo]) alive during playback. + /// To ensure safety, checks within [WaveInfo] will clear the whole channel queue if any queued [WaveInfo] is dropped prematurely. + pub fn queue_wave(&self, wave: &mut WaveInfo) -> std::result::Result<(), NdspError> { + match wave.get_status() { + WaveStatus::Playing | WaveStatus::Queued => return Err(NdspError::WaveBusy(self.id)), + _ => (), + } + + wave.set_channel(self.id); + + unsafe { ctru_sys::ndspChnWaveBufAdd(self.id.into(), &mut wave.raw_data) }; + + Ok(()) + } +} + +/// Functions to handle audio filtering. +/// +/// Refer to [libctru](https://libctru.devkitpro.org/channel_8h.html#a1da3b363c2edfd318c92276b527daae6) for more info. +impl Channel<'_> { + /// Enables/disables monopole filters. + pub fn iir_mono_set_enabled(&self, enable: bool) { + unsafe { ctru_sys::ndspChnIirMonoSetEnable(self.id.into(), enable) }; + } + + /// Sets the monopole to be a high pass filter. + /// + /// # Notes + /// + /// This is a lower quality filter than the Biquad alternative. + pub fn iir_mono_set_params_high_pass_filter(&self, cut_off_freq: f32) { + unsafe { ctru_sys::ndspChnIirMonoSetParamsHighPassFilter(self.id.into(), cut_off_freq) }; + } + + /// Sets the monopole to be a low pass filter. + /// + /// # Notes + /// + /// This is a lower quality filter than the Biquad alternative. + pub fn iir_mono_set_params_low_pass_filter(&self, cut_off_freq: f32) { + unsafe { ctru_sys::ndspChnIirMonoSetParamsLowPassFilter(self.id.into(), cut_off_freq) }; + } + + /// Enables/disables biquad filters. + pub fn iir_biquad_set_enabled(&self, enable: bool) { + unsafe { ctru_sys::ndspChnIirBiquadSetEnable(self.id.into(), enable) }; + } + + /// Sets the biquad to be a high pass filter. + pub fn iir_biquad_set_params_high_pass_filter(&self, cut_off_freq: f32, quality: f32) { + unsafe { + ctru_sys::ndspChnIirBiquadSetParamsHighPassFilter(self.id.into(), cut_off_freq, quality) + }; + } + + /// Sets the biquad to be a low pass filter. + pub fn iir_biquad_set_params_low_pass_filter(&self, cut_off_freq: f32, quality: f32) { + unsafe { + ctru_sys::ndspChnIirBiquadSetParamsLowPassFilter(self.id.into(), cut_off_freq, quality) + }; + } + + /// Sets the biquad to be a notch filter. + pub fn iir_biquad_set_params_notch_filter(&self, notch_freq: f32, quality: f32) { + unsafe { + ctru_sys::ndspChnIirBiquadSetParamsNotchFilter(self.id.into(), notch_freq, quality) + }; + } + + /// Sets the biquad to be a band pass filter. + pub fn iir_biquad_set_params_band_pass_filter(&self, mid_freq: f32, quality: f32) { + unsafe { + ctru_sys::ndspChnIirBiquadSetParamsBandPassFilter(self.id.into(), mid_freq, quality) + }; + } + + /// Sets the biquad to be a peaking equalizer. + pub fn iir_biquad_set_params_peaking_equalizer( + &self, + central_freq: f32, + quality: f32, + gain: f32, + ) { + unsafe { + ctru_sys::ndspChnIirBiquadSetParamsPeakingEqualizer( + self.id.into(), + central_freq, + quality, + gain, + ) + }; + } +} + +impl AudioFormat { + /// Returns the amount of bytes needed to store one sample + /// Eg. + /// 8 bit formats return 1 (byte) + /// 16 bit formats return 2 (bytes) + pub fn sample_size(self) -> u8 { + match self { + AudioFormat::PCM8Mono => 1, + AudioFormat::PCM16Mono | AudioFormat::PCM8Stereo => 2, + AudioFormat::PCM16Stereo => 4, + } + } +} + +impl fmt::Display for NdspError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::InvalidChannel(id) => write!(f, "Audio Channel with ID {id} doesn't exist. Valid channels have an ID between 0 and 23."), + Self::ChannelAlreadyInUse(id) => write!(f, "Audio Channel with ID {id} is already being used. Drop the other instance if you want to use it here."), + Self::WaveBusy(id) => write!(f, "The selected WaveInfo is busy playing on channel {id}."), + Self::SampleCountOutOfBounds(samples_requested, max_samples) => write!(f, "The sample count requested is too big. Requested amount was {samples_requested} while the maximum sample count is {max_samples}."), + } + } +} + +impl error::Error for NdspError {} + +impl<'ndsp> Drop for Channel<'ndsp> { + fn drop(&mut self) { + self.reset(); + } +} + +impl Drop for Ndsp { + fn drop(&mut self) { + for i in 0..NUMBER_OF_CHANNELS { + self.channel(i).unwrap().clear_queue(); + } + } +} diff --git a/ctru-rs/src/services/ndsp/wave.rs b/ctru-rs/src/services/ndsp/wave.rs new file mode 100644 index 0000000..4a5de98 --- /dev/null +++ b/ctru-rs/src/services/ndsp/wave.rs @@ -0,0 +1,180 @@ +use super::{AudioFormat, NdspError}; +use crate::linear::LinearAllocator; + +/// Informational struct holding the raw audio data and playback info. This corresponds to [ctru_sys::ndspWaveBuf] +pub struct WaveInfo { + /// Data block of the audio wave (and its format information). + buffer: Box<[u8], LinearAllocator>, + audio_format: AudioFormat, + // Holding the data with the raw format is necessary since `libctru` will access it. + pub(crate) raw_data: ctru_sys::ndspWaveBuf, + played_on_channel: Option, +} + +#[derive(Copy, Clone, Debug)] +#[repr(u8)] +/// Enum representing the playback status of a [WaveInfo]. +pub enum WaveStatus { + Free = ctru_sys::NDSP_WBUF_FREE as u8, + Queued = ctru_sys::NDSP_WBUF_QUEUED as u8, + Playing = ctru_sys::NDSP_WBUF_PLAYING as u8, + Done = ctru_sys::NDSP_WBUF_DONE as u8, +} + +impl WaveInfo { + /// Build a new playable wave object from a raw buffer on LINEAR memory and a some info. + pub fn new( + buffer: Box<[u8], LinearAllocator>, + audio_format: AudioFormat, + looping: bool, + ) -> Self { + let sample_count: usize = buffer.len() / (audio_format.sample_size() as usize); + + // Signal to the DSP processor the buffer's RAM sector. + // This step may seem delicate, but testing reports failure most of the time, while still having no repercussions on the resulting audio. + unsafe { + let _r = ctru_sys::DSP_FlushDataCache(buffer.as_ptr().cast(), buffer.len() as u32); + } + + let address = ctru_sys::tag_ndspWaveBuf__bindgen_ty_1 { + data_vaddr: buffer.as_ptr().cast(), + }; + + let raw_data = ctru_sys::ndspWaveBuf { + __bindgen_anon_1: address, // Buffer data virtual address + nsamples: sample_count as u32, + adpcm_data: std::ptr::null_mut(), + offset: 0, + looping, + // The ones after this point aren't supposed to be setup by the user + status: 0, + sequence_id: 0, + next: std::ptr::null_mut(), + }; + + Self { + buffer, + audio_format, + raw_data, + played_on_channel: None, + } + } + + /// Return a slice to the audio data (on the LINEAR memory). + pub fn get_buffer(&self) -> &[u8] { + &self.buffer + } + + /// Return a mutable slice to the audio data (on the LINEAR memory). + /// + /// # Errors + /// + /// This function will return an error if the [WaveInfo] is currently busy, + /// with the id to the channel in which it's queued. + pub fn get_buffer_mut(&mut self) -> Result<&mut [u8], NdspError> { + match self.get_status() { + WaveStatus::Playing | WaveStatus::Queued => { + Err(NdspError::WaveBusy(self.played_on_channel.unwrap())) + } + _ => Ok(&mut self.buffer), + } + } + + /// Return this wave's playback status. + pub fn get_status(&self) -> WaveStatus { + self.raw_data.status.try_into().unwrap() + } + + /// Get the amounts of samples *read* by the NDSP process. + /// + /// # Notes + /// + /// This value varies depending on [Self::set_sample_count]. + pub fn get_sample_count(&self) -> u32 { + self.raw_data.nsamples + } + + /// Get the format of the audio data. + pub fn get_format(&self) -> AudioFormat { + self.audio_format + } + + // Set the internal flag for the id of the channel playing this wave. + // + // Internal Use Only. + pub(crate) fn set_channel(&mut self, id: u8) { + self.played_on_channel = Some(id) + } + + /// Set the amount of samples to be read. + /// This function doesn't resize the internal buffer. + /// + /// # Note + /// + /// Operations of this kind are particularly useful to allocate memory pools + /// for VBR (Variable BitRate) Formats, like OGG Vorbis. + /// + /// # Errors + /// + /// This function will return an error if the sample size exceeds the buffer's capacity + /// or if the WaveInfo is currently queued. + pub fn set_sample_count(&mut self, sample_count: u32) -> Result<(), NdspError> { + match self.get_status() { + WaveStatus::Playing | WaveStatus::Queued => { + return Err(NdspError::WaveBusy(self.played_on_channel.unwrap())); + } + _ => (), + } + + let max_count: usize = self.buffer.len() / (self.audio_format.sample_size() as usize); + + if sample_count > max_count as u32 { + return Err(NdspError::SampleCountOutOfBounds( + sample_count, + max_count as u32, + )); + } + + self.raw_data.nsamples = sample_count; + + Ok(()) + } +} + +impl TryFrom for WaveStatus { + type Error = &'static str; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(Self::Free), + 1 => Ok(Self::Queued), + 2 => Ok(Self::Playing), + 3 => Ok(Self::Done), + _ => Err("Invalid WaveInfo Status code"), + } + } +} + +impl Drop for WaveInfo { + fn drop(&mut self) { + // This was the only way I found I could check for improper drops of `WaveInfos`. + // A panic was considered, but it would cause issues with drop order against `Ndsp`. + match self.get_status() { + WaveStatus::Free | WaveStatus::Done => (), + // If the status flag is "unfinished" + _ => { + // The unwrap is safe, since it must have a value in the case the status is "unfinished". + unsafe { ctru_sys::ndspChnWaveBufClear(self.played_on_channel.unwrap().into()) }; + } + } + + unsafe { + // Flag the buffer's RAM sector as unused + // This step has no real effect in normal applications and is skipped even by devkitPRO's own examples. + let _r = ctru_sys::DSP_InvalidateDataCache( + self.buffer.as_ptr().cast(), + self.buffer.len().try_into().unwrap(), + ); + } + } +} diff --git a/ctru-rs/src/services/soc.rs b/ctru-rs/src/services/soc.rs index 88fb4aa..aecd889 100644 --- a/ctru-rs/src/services/soc.rs +++ b/ctru-rs/src/services/soc.rs @@ -1,5 +1,4 @@ use libc::memalign; -use once_cell::sync::Lazy; use std::net::Ipv4Addr; use std::sync::Mutex; @@ -9,13 +8,12 @@ use crate::Error; /// Soc service. Initializing this service will enable the use of network sockets and utilities /// such as those found in `std::net`. The service will be closed when this struct is is dropped. -#[non_exhaustive] pub struct Soc { _service_handler: ServiceReference, sock_3dslink: libc::c_int, } -static SOC_ACTIVE: Lazy> = Lazy::new(|| Mutex::new(0)); +static SOC_ACTIVE: Mutex = Mutex::new(0); impl Soc { /// Initialize the Soc service with a default buffer size of 0x100000 bytes