Mark Drobnak
2 years ago
13 changed files with 907 additions and 72 deletions
@ -0,0 +1,166 @@
@@ -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(); |
||||
} |
||||
} |
@ -0,0 +1,70 @@
@@ -0,0 +1,70 @@
|
||||
use ctru::gfx::{Screen, Side, TopScreen3D}; |
||||
use ctru::prelude::*; |
||||
|
||||
/// See `graphics-bitmap.rs` for details on how the image is generated.
|
||||
///
|
||||
/// WARNING: this example uses 3D mode in a rather unnatural way, and should
|
||||
/// probably not be viewed for too long or at all if you are photosensitive.
|
||||
|
||||
const IMAGE: &[u8] = include_bytes!("assets/ferris.rgb"); |
||||
static ZERO: &[u8] = &[0; IMAGE.len()]; |
||||
|
||||
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.bottom_screen.borrow_mut()); |
||||
|
||||
println!("Press Start to exit.\nPress A to switch sides (be sure to have 3D mode enabled)."); |
||||
|
||||
gfx.top_screen.borrow_mut().set_double_buffering(true); |
||||
|
||||
let top_screen = TopScreen3D::from(&gfx.top_screen); |
||||
let (mut left, mut right) = top_screen.split_mut(); |
||||
|
||||
let mut current_side = Side::Left; |
||||
|
||||
// Main loop
|
||||
while apt.main_loop() { |
||||
//Scan all the inputs. This should be done once for each frame
|
||||
hid.scan_input(); |
||||
|
||||
if hid.keys_down().contains(KeyPad::KEY_START) { |
||||
break; |
||||
} |
||||
|
||||
let left_buf = left.get_raw_framebuffer(); |
||||
let right_buf = right.get_raw_framebuffer(); |
||||
|
||||
// Clear both buffers every time, in case the user switches sides this loop
|
||||
unsafe { |
||||
left_buf.ptr.copy_from(ZERO.as_ptr(), ZERO.len()); |
||||
right_buf.ptr.copy_from(ZERO.as_ptr(), ZERO.len()); |
||||
} |
||||
|
||||
if hid.keys_down().contains(KeyPad::KEY_A) { |
||||
// flip which buffer we're writing to
|
||||
current_side = match current_side { |
||||
Side::Left => Side::Right, |
||||
Side::Right => Side::Left, |
||||
}; |
||||
} |
||||
|
||||
let buf = match current_side { |
||||
Side::Left => left_buf.ptr, |
||||
Side::Right => right_buf.ptr, |
||||
}; |
||||
|
||||
unsafe { |
||||
buf.copy_from(IMAGE.as_ptr(), IMAGE.len()); |
||||
} |
||||
|
||||
// Flush and swap framebuffers
|
||||
gfx.flush_buffers(); |
||||
gfx.swap_buffers(); |
||||
|
||||
//Wait for VBlank
|
||||
gfx.wait_for_vblank(); |
||||
} |
||||
} |
@ -0,0 +1,342 @@
@@ -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<usize> = 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<Self> { |
||||
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<Channel, NdspError> { |
||||
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 <br>
|
||||
/// Index 1: Front right volume <br>
|
||||
/// Index 2: Back left volume <br>
|
||||
/// Index 3: Back right volume <br>
|
||||
/// Index 4..7: Same as 0..3 but for auxiliary output 0 <br>
|
||||
/// Index 8..11: Same as 0..3 but for auxiliary output 1 <br>
|
||||
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(); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,180 @@
@@ -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<u8>, |
||||
} |
||||
|
||||
#[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<u8> for WaveStatus { |
||||
type Error = &'static str; |
||||
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> { |
||||
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(), |
||||
); |
||||
} |
||||
} |
||||
} |
Loading…
Reference in new issue