diff --git a/ctru-rs/Cargo.toml b/ctru-rs/Cargo.toml index 097417f..94ff0bb 100644 --- a/ctru-rs/Cargo.toml +++ b/ctru-rs/Cargo.toml @@ -31,6 +31,7 @@ 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/ogg-audio.rs b/ctru-rs/examples/ogg-audio.rs new file mode 100644 index 0000000..b31216b --- /dev/null +++ b/ctru-rs/examples/ogg-audio.rs @@ -0,0 +1,135 @@ +#![feature(allocator_api)] +use ctru::linear::LinearAllocator; + +use ctru::prelude::*; +use ctru::services::ndsp::{ + wave::{WaveInfo, WaveStatus}, + AudioFormat, Channel, InterpolationType, Ndsp, OutputMode, +}; + +use lewton::inside_ogg::OggStreamReader; +use lewton::samples::InterleavedSamples; + +use std::fs::File; + +const CHANNEL_COUNT: usize = 2; +// It should never get this big, but extra space is useful since we can't know the size beforehand +// Read +const AUDIO_WAVE_SIZE: usize = 4000 * CHANNEL_COUNT; + +fn setup_next_wave( + stream_reader: &mut OggStreamReader, + channel: &Channel, + wave_info: &mut WaveInfo, +) { + // Interleaved Dual Channel (Stereo PCM16) + match stream_reader + .read_dec_packet_generic::>() + .unwrap() + { + Some(pck) => { + // A good way to handle the data would be to allocate it on LINEAR memory directly within `lewton`, + // but since that API isn't exposed (for its instability) a memcopy is needed either way. + + let mut samples = pck.samples; + let raw_samples = bytemuck::cast_slice_mut::<_, u8>(samples.as_mut_slice()); + + // We need only the first part of the slice to clone the data over + let buf = wave_info.get_buffer_mut(); + let (buf, _) = buf.split_at_mut(raw_samples.len()); + + buf.copy_from_slice(raw_samples); + + // We change the sample_count of the WaveInfo so NDSP won't read the unused bytes + wave_info + .set_sample_count((samples.len() / pck.channel_count) as u32) + .unwrap(); + + channel.queue_wave(wave_info).unwrap(); + } + None => return, // do nothing when the audio ends + } +} + +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()); + + cfg_if::cfg_if! { + if #[cfg(not(all(feature = "romfs", romfs_exists)))] { + panic!("No RomFS was found, are you sure you included it?") + } + } + + let _romfs = ctru::romfs::RomFS::init().unwrap(); + let f = File::open("romfs:/music.ogg").unwrap(); + let mut srr = OggStreamReader::new(f).unwrap(); + + println!("\nPress START to exit"); + println!("Sample rate: {}", srr.ident_hdr.audio_sample_rate); + + // If the audio is Mono or it has more channels than 2, it's incompatibile with this current setup + if srr.ident_hdr.audio_channels != 2 { + panic!( + "Audio file improperly modified: Number of channels incompatible with Stereo output" + ); + } + + // We setup the alternating buffers + let audio_data1 = Box::new_in([0u8; AUDIO_WAVE_SIZE], LinearAllocator); + 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); + + // NDSP setup + let mut ndsp = Ndsp::init().expect("Couldn't obtain NDSP controller"); + ndsp.set_output_mode(OutputMode::Stereo); + + let channel_zero = ndsp.channel(0).unwrap(); + channel_zero.set_interpolation(InterpolationType::Linear); + channel_zero.set_sample_rate(srr.ident_hdr.audio_sample_rate as f32); + channel_zero.set_format(AudioFormat::PCM16Stereo); + + setup_next_wave(&mut srr, &channel_zero, &mut wave_info1); + setup_next_wave(&mut srr, &channel_zero, &mut wave_info2); + + // Audio volume mix + let mut mix: [f32; 12] = [0f32; 12]; + mix[0] = 0.8; + mix[1] = 0.8; + channel_zero.set_mix(&mix); + + let mut altern = true; // true is wave_info1, false is wave_info2 + + // 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 current: &mut WaveInfo = if altern { + &mut wave_info1 + } else { + &mut wave_info2 + }; + + match current.get_status() { + WaveStatus::Free | WaveStatus::Done => { + setup_next_wave(&mut srr, &channel_zero, current); + + altern = !altern; + } + _ => (), + } + + // Flush and swap framebuffers + gfx.flush_buffers(); + gfx.swap_buffers(); + } +} diff --git a/ctru-rs/examples/romfs/music.ogg b/ctru-rs/examples/romfs/music.ogg new file mode 100644 index 0000000..677dc2b Binary files /dev/null and b/ctru-rs/examples/romfs/music.ogg differ diff --git a/ctru-rs/src/services/ndsp/mod.rs b/ctru-rs/src/services/ndsp/mod.rs index a548259..a13b224 100644 --- a/ctru-rs/src/services/ndsp/mod.rs +++ b/ctru-rs/src/services/ndsp/mod.rs @@ -40,9 +40,10 @@ pub enum InterpolationType { #[derive(Copy, Clone, Debug)] pub enum NdspError { - InvalidChannel(u8), // channel id - ChannelAlreadyInUse(u8), // channel id - WaveAlreadyQueued(u8), // channel id + InvalidChannel(u8), // channel id + ChannelAlreadyInUse(u8), // channel id + WaveAlreadyQueued(u8), // channel id + SampleCountOutOfBounds(u32, u32), // amount requested, maximum amount } pub struct Channel<'ndsp> { @@ -288,7 +289,8 @@ impl fmt::Display for NdspError { 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::WaveAlreadyQueued(id) => write!(f, "The selected WaveInfo is already playing on channel {id}.") + Self::WaveAlreadyQueued(id) => write!(f, "The selected WaveInfo is already 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}."), } } } diff --git a/ctru-rs/src/services/ndsp/wave.rs b/ctru-rs/src/services/ndsp/wave.rs index 349372f..1d922c3 100644 --- a/ctru-rs/src/services/ndsp/wave.rs +++ b/ctru-rs/src/services/ndsp/wave.rs @@ -1,4 +1,4 @@ -use super::AudioFormat; +use super::{AudioFormat, NdspError}; use crate::linear::LinearAllocator; /// Informational struct holding the raw audio data and playback info. This corresponds to [ctru_sys::ndspWaveBuf] @@ -81,6 +81,42 @@ impl WaveInfo { 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 usefulto 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::WaveAlreadyQueued( + 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 {