diff --git a/ctru-rs/examples/audio_filters.rs b/ctru-rs/examples/audio_filters.rs new file mode 100644 index 0000000..af46feb --- /dev/null +++ b/ctru-rs/examples/audio_filters.rs @@ -0,0 +1,183 @@ +use ctru::prelude::*; +use ctru::services::ndsp::{Ndsp, OutputMode, InterpolationType}; + +const SAMPLERATE: u32 = 22050; +const SAMPLESPERBUF: u32 = SAMPLERATE / 30; // 735 +const BYTESPERSAMPLE: u32 = 4; + +fn array_size(array: &[u8]) -> usize { + array.len() +} // (sizeof(array)/sizeof(array[0])) + +// audioBuffer is stereo PCM16 +void fill_buffer(void* audioBuffer, size_t offset, size_t size, int frequency) { + u32* dest = (u32*) audioBuffer; + + for (int i = 0; i < size; i++) { + // This is a simple sine wave, with a frequency of `frequency` Hz, and an amplitude 30% of maximum. + s16 sample = 0.3 * 0x7FFF * sin(frequency * (2 * M_PI) * (offset + i) / SAMPLERATE); + + // Stereo samples are interleaved: left and right channels. + dest[i] = (sample << 16) | (sample & 0xffff); + } + + DSP_FlushDataCache(audioBuffer, size); +} + +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()); + + println!("libctru filtered streamed audio\n"); + + let audioBuffer = [0u32; (SAMPLESPERBUF * BYTESPERSAMPLE * 2)]; + + let fillBlock = false; + + let ndsp = Ndsp::init().expect("Couldn't obtain NDSP controller"); + ndsp.set_output_mode(OutputMode::Stereo); + + let channel_zero = ndsp.channel(0); + channel_zero.set_interpolation(InterpolationType::Linear); + channel_zero.set_sample_rate(SAMPLERATE); + channel_zero.set_format(NDSP_FORMAT_STEREO_PCM16); + + // Output at 100% on the first pair of left and right channels. + + let mix = [0f32; 12]; + memset(mix, 0, sizeof(mix)); + mix[0] = 1.0; + mix[1] = 1.0; + ndspChnSetMix(0, mix); + + // Note Frequencies + + int notefreq[] = { + 220, + 440, 880, 1760, 3520, 7040, + 14080, + 7040, 3520, 1760, 880, 440 + }; + + int note = 4; + + // Filters + + const char* filter_names[] = { + "None", + "Low-Pass", + "High-Pass", + "Band-Pass", + "Notch", + "Peaking" + }; + + int filter = 0; + + // We set up two wave buffers and alternate between the two, + // effectively streaming an infinitely long sine wave. + + ndspWaveBuf waveBuf[2]; + memset(waveBuf,0,sizeof(waveBuf)); + waveBuf[0].data_vaddr = &audioBuffer[0]; + waveBuf[0].nsamples = SAMPLESPERBUF; + waveBuf[1].data_vaddr = &audioBuffer[SAMPLESPERBUF]; + waveBuf[1].nsamples = SAMPLESPERBUF; + + size_t stream_offset = 0; + + fill_buffer(audioBuffer,stream_offset, SAMPLESPERBUF * 2, notefreq[note]); + + stream_offset += SAMPLESPERBUF; + + ndspChnWaveBufAdd(0, &waveBuf[0]); + ndspChnWaveBufAdd(0, &waveBuf[1]); + + println!("Press up/down to change tone frequency\n"); + println!("Press left/right to change filter\n"); + println!("\x1b[6;1Hnote = {} Hz ", notefreq[note]); + println!("\x1b[7;1Hfilter = {} ", filter_names[filter]); + + while(aptMainLoop()) { + + gfxSwapBuffers(); + gfxFlushBuffers(); + gspWaitForVBlank(); + + hidScanInput(); + u32 kDown = hidKeysDown(); + + if (kDown & KEY_START) + break; // break in order to return to hbmenu + + if (kDown & KEY_DOWN) { + note--; + if (note < 0) { + note = ARRAY_SIZE(notefreq) - 1; + } + println!("\x1b[6;1Hnote = {} Hz ", notefreq[note]); + } else if (kDown & KEY_UP) { + note++; + if (note >= ARRAY_SIZE(notefreq)) { + note = 0; + } + println!("\x1b[6;1Hnote = {} Hz ", notefreq[note]); + } + + bool update_params = false; + if (kDown & KEY_LEFT) { + filter--; + if (filter < 0) { + filter = ARRAY_SIZE(filter_names) - 1; + } + update_params = true; + } else if (kDown & KEY_RIGHT) { + filter++; + if (filter >= ARRAY_SIZE(filter_names)) { + filter = 0; + } + update_params = true; + } + + if (update_params) { + println!("\x1b[7;1Hfilter = {} ", filter_names[filter]); + switch (filter) { + default: + ndspChnIirBiquadSetEnable(0, false); + break; + case 1: + ndspChnIirBiquadSetParamsLowPassFilter(0, 1760.f, 0.707f); + break; + case 2: + ndspChnIirBiquadSetParamsHighPassFilter(0, 1760.f, 0.707f); + break; + case 3: + ndspChnIirBiquadSetParamsBandPassFilter(0, 1760.f, 0.707f); + break; + case 4: + ndspChnIirBiquadSetParamsNotchFilter(0, 1760.f, 0.707f); + break; + case 5: + ndspChnIirBiquadSetParamsPeakingEqualizer(0, 1760.f, 0.707f, 3.0f); + break; + } + } + + if (waveBuf[fillBlock].status == NDSP_WBUF_DONE) { + fill_buffer(waveBuf[fillBlock].data_pcm16, stream_offset, waveBuf[fillBlock].nsamples, notefreq[note]); + ndspChnWaveBufAdd(0, &waveBuf[fillBlock]); + stream_offset += waveBuf[fillBlock].nsamples; + fillBlock = !fillBlock; + } + } + + ndspExit(); + + linearFree(audioBuffer); + + gfxExit(); + return 0; +} diff --git a/ctru-rs/src/error.rs b/ctru-rs/src/error.rs index 18fd175..0c3a9bf 100644 --- a/ctru-rs/src/error.rs +++ b/ctru-rs/src/error.rs @@ -51,6 +51,7 @@ pub enum Error { Libc(String), ServiceAlreadyActive, OutputAlreadyRedirected, + InvalidChannel, } impl Error { diff --git a/ctru-rs/src/services/mod.rs b/ctru-rs/src/services/mod.rs index 5a52608..507be0a 100644 --- a/ctru-rs/src/services/mod.rs +++ b/ctru-rs/src/services/mod.rs @@ -4,6 +4,7 @@ pub mod cfgu; pub mod fs; pub mod gspgpu; pub mod hid; +pub mod ndsp; pub mod ps; mod reference; pub mod soc; diff --git a/ctru-rs/src/services/ndsp.rs b/ctru-rs/src/services/ndsp.rs new file mode 100644 index 0000000..3c03ab9 --- /dev/null +++ b/ctru-rs/src/services/ndsp.rs @@ -0,0 +1,192 @@ +use crate::error::ResultCode; +use crate::services::ServiceReference; + +#[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 InterpolationType { + Polyphase = ctru_sys::NDSP_INTERP_POLYPHASE, + Linear = ctru_sys::NDSP_INTERP_LINEAR, + None = ctru_sys::NDSP_INTERP_NONE, +} + +#[derive(Copy, Clone, Debug)] +#[repr(u16)] +pub enum AudioFormat { + PCM8Mono = ctru_sys::NDSP_FORMAT_MONO_PCM8, + PCM16Mono = ctru_sys::NDSP_FORMAT_MONO_PCM16, + ADPCMMono = ctru_sys::NDSP_FORMAT_MONO_ADPCM, + PCM8Stereo = ctru_sys::NDSP_FORMAT_STEREO_PCM8, + PCM16Stereo = ctru_sys::NDSP_FORMAT_STEREO_PCM16, + FrontBypass = ctru_sys::NDSP_FRONT_BYPASS, + SurroundPreprocessed = ctru_sys::NDSP_3D_SURROUND_PREPROCESSED, +} + +pub struct Wave { + /// Buffer data with specified type + buffer: Vector, + // adpcm_data: AdpcmData, TODO: Requires research on how this WaveBuffer type is handled + /// Whether to loop the track or not + looping: bool, +} + +#[non_exhaustive] +pub struct Ndsp { + // To ensure safe tracking of the different channels, there must be only ONE + // existing instance of the service at a time. + _service_handler: ServiceReference, + channel_mask: u32, +} + +#[non_exhaustive] +pub struct Channel { + id: u8, // From 0 to 23. +} + +static NDSP_ACTIVE: Lazy> = Lazy::new(|| Mutex::new(0)); + +impl Ndsp { + pub fn init() -> crate::Result { + let _service_handler = ServiceReference::new( + &NDSP_ACTIVE, + false, + || unsafe { + ctru_sys::ndspInit(); + + Ok(()) + }, + || unsafe { ctru_sys::ndspExit() }, + )?; + + Ok(Self { + _service_handler, + channel_mask: 0, + }) + } + + /// Return the representation of the specified channel. + /// + /// # Errors + /// + /// An error will be returned if the channel id is greater than 23. + pub fn channel(&self, id: u8) -> crate::Result { + if channel > 23 { + return Err(crate::Error::InvalidChannel); + } + + Ok(Channel(id as i32)) + } + + /// Set the audio output mode. Defaults to `OutputMode::Stereo`. + pub fn set_output_mode(&mut self, mode: OutputMode) { + unsafe { ndspSetOutputMode(mode) }; + } +} + +impl Channel { + /// Reset the channel + pub fn reset(&mut self) { + unsafe { ctru_sys::ndspChnReset(self.id) }; + } + + /// Initialize the channel's parameters + pub fn init_parameters(&mut self) { + unsafe { ctru_sys::ndspChnInitParams(self.id) }; + } + + /// Returns whether the channel is playing any audio. + pub fn is_playing(&self) -> bool { + unsafe { ctru_sys::ndspChnIsPlaying(self.id) } + } + + /// Returns whether the channel's playback is currently paused. + pub fn is_paused(&self) -> bool { + unsafe { ctru_sys::ndspChnIsPaused(self.id) } + } + + /// Returns the channel's current sample's position. + pub fn get_sample_position(&self) -> u32 { + unsafe { ctru_sys::ndspChnGetSamplePos(self.id) } + } + + /// Returns the channel's current wave sequence's id. + pub fn get_wave_sequence_id(&self) -> u16 { + unsafe { ctru_sys::ndspChnGetWaveBufSeq(self.id) } + } + + /// Pause or un-pause the channel's playback. + pub fn set_paused(&mut self, state: bool) { + unsafe { ctru_sys::ndspChnSetPaused(self.id, state) }; + } + + /// Set the channel's output format. + /// Change this setting based on the used sample's format. + pub fn set_format(&mut self, format: AudioFormat) { + unsafe { ctru_sys::ndspChnSetFormat(self.id, format) }; + } + + /// Set the channel's interpolation mode. + pub fn set_interpolation(&mut self, interp_type: InterpolationType) { + unsafe { ctru_sys::ndspChnSetInterp(self.id, interp_type) }; + } + + /// Set the channel's volume mix. + /// Docs about the buffer usage: https://libctru.devkitpro.org/channel_8h.html#a30eb26f1972cc3ec28370263796c0444 + pub fn set_mix(&mut self, mix: [f32; 12]) { + unsafe { ctru_sys::ndspChnSetMix(self.id, mix) } + } + + /// Set the channel's rate of sampling. + pub fn set_sample_rate(&mut self, rate: f32) { + unsafe { ctru_sys::ndspChnSetRate(self.id, rate) }; + } + + // TODO: find a way to wrap `ndspChnSetAdpcmCoefs` + + /// Clear the wave buffer's queue and stop playback. + pub fn clear_wave_buffer(&mut self) { + unsafe { ctru_sys::ndspChnWaveBufClear(self.id) }; + } + + /// Add a wave buffer to the channel's queue. + /// Note: if there are no other buffers in queue, playback for this buffer will start. + pub fn add_wave_buffer(&mut self, buffer: buf) { + unsafe { ctru_sys::ndspChnWaveBufAdd(self.id, buffer) }; + } +} + +impl Drop for Ndsp { + fn drop(&mut self) { + unsafe { + ctru_sys::ndspExit(); + } + } +} + +impl From for ctru_sys::ndspWaveBuf { + fn from(wave: Wave) -> ctru_sys::ndspWaveBuf { + let WaveBuffer(vector) = wave.buffer; + let length = wave.buffer.len(); + let buffer_data = ctru_sys::tag_ndspWaveBuf__bindgen_ty_1 { + data_vaddr: vector.as_ptr(), + }; + + ctru_sys::ndspWaveBuf { + __bindgen_anon_1: buffer_data, + looping: wave.looping, + next: 0 as *const _, + sequence_id: 0, + nsamples: lenght, + adpcm_data: 0 as *const _, + offset: 0, + status: 0, + } + } +}