Mark Drobnak
2 years ago
13 changed files with 907 additions and 72 deletions
@ -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 @@ |
|||||||
|
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 @@ |
|||||||
|
//! 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 @@ |
|||||||
|
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