ctru/services/ndsp/wave.rs
1//! Audio wave.
2//!
3//! This modules has all methods and structs required to work with audio waves meant to be played via the [`ndsp`](crate::services::ndsp) service.
4
5use super::{AudioFormat, Error};
6use crate::linear::LinearAllocation;
7
8/// Informational struct holding the raw audio data and playback info.
9///
10/// You can play audio [`Wave`]s by using [`Channel::queue_wave()`](super::Channel::queue_wave).
11pub struct Wave<Buffer: LinearAllocation + AsRef<[u8]>> {
12 /// Data block of the audio wave (and its format information).
13 buffer: Buffer,
14 audio_format: AudioFormat,
15 // Holding the data with the raw format is necessary since `libctru` will access it.
16 pub(crate) raw_data: ctru_sys::ndspWaveBuf,
17 played_on_channel: Option<u8>,
18}
19
20#[derive(Copy, Clone, Debug, PartialEq, Eq)]
21#[repr(u8)]
22/// Playback status of a [`Wave`].
23pub enum Status {
24 /// Wave has never been used.
25 Free = ctru_sys::NDSP_WBUF_FREE,
26 /// Wave is currently queued for usage.
27 Queued = ctru_sys::NDSP_WBUF_QUEUED,
28 /// Wave is currently playing.
29 Playing = ctru_sys::NDSP_WBUF_PLAYING,
30 /// Wave has finished playing.
31 Done = ctru_sys::NDSP_WBUF_DONE,
32}
33
34impl<Buffer> Wave<Buffer>
35where
36 Buffer: LinearAllocation + AsRef<[u8]>,
37{
38 /// Build a new playable wave object from a raw buffer on [LINEAR memory](`crate::linear`) and some info.
39 ///
40 /// # Example
41 ///
42 /// ```
43 /// # #![feature(allocator_api)]
44 /// # fn main() {
45 /// # let _runner = test_runner::GdbRunner::default();
46 /// #
47 /// use ctru::linear::LinearAllocator;
48 /// use ctru::services::ndsp::{AudioFormat, wave::Wave};
49 ///
50 /// // Zeroed box allocated in the LINEAR memory.
51 /// let audio_data: Box<[_], _> = Box::new_in([0u8; 96], LinearAllocator);
52 ///
53 /// let wave = Wave::new(audio_data, AudioFormat::PCM16Stereo, false);
54 /// # }
55 /// ```
56 pub fn new(buffer: Buffer, audio_format: AudioFormat, looping: bool) -> Self {
57 let buf = buffer.as_ref();
58 let sample_count = buf.len() / audio_format.size();
59
60 // Signal to the DSP processor the buffer's RAM sector.
61 // This step may seem delicate, but testing reports failure most of the time, while still having no repercussions on the resulting audio.
62 unsafe {
63 let _r = ctru_sys::DSP_FlushDataCache(buf.as_ptr().cast(), buf.len() as u32);
64 }
65
66 let address = ctru_sys::tag_ndspWaveBuf__bindgen_ty_1 {
67 data_vaddr: buf.as_ptr().cast(),
68 };
69
70 let raw_data = ctru_sys::ndspWaveBuf {
71 __bindgen_anon_1: address, // Buffer data virtual address
72 nsamples: sample_count as u32,
73 adpcm_data: std::ptr::null_mut(),
74 offset: 0,
75 looping,
76 // The ones after this point aren't supposed to be setup by the user
77 status: 0,
78 sequence_id: 0,
79 next: std::ptr::null_mut(),
80 };
81
82 Self {
83 buffer,
84 audio_format,
85 raw_data,
86 played_on_channel: None,
87 }
88 }
89
90 /// Returns the original data structure used for the audio data.
91 pub fn get_raw_buffer(&self) -> &Buffer {
92 &self.buffer
93 }
94
95 /// Returns a mutable reference to the original data structure used for the audio data.
96 ///
97 /// # Errors
98 ///
99 /// This function will return an error if the [`Wave`] is currently busy,
100 /// with the id to the channel in which it's queued.
101 pub fn get_raw_buffer_mut(&mut self) -> Result<&mut Buffer, Error> {
102 match self.status() {
103 Status::Playing | Status::Queued => {
104 Err(Error::WaveBusy(self.played_on_channel.unwrap()))
105 }
106 _ => Ok(&mut self.buffer),
107 }
108 }
109
110 /// Returns a slice to the audio data (on the LINEAR memory).
111 pub fn get_buffer(&self) -> &[u8] {
112 self.get_raw_buffer().as_ref()
113 }
114
115 /// Returns a mutable slice to the audio data (on the LINEAR memory).
116 ///
117 /// # Errors
118 ///
119 /// This function will return an error if the [`Wave`] is currently busy,
120 /// with the id to the channel in which it's queued.
121 pub fn get_buffer_mut(&mut self) -> Result<&mut [u8], Error>
122 where
123 Buffer: AsMut<[u8]>,
124 {
125 Ok(self.get_raw_buffer_mut()?.as_mut())
126 }
127
128 /// Returns this wave's playback status.
129 ///
130 /// # Example
131 ///
132 /// ```
133 /// # #![feature(allocator_api)]
134 /// # fn main() {
135 /// # let _runner = test_runner::GdbRunner::default();
136 /// #
137 /// # use ctru::linear::LinearAllocator;
138 /// # let _audio_data: Box<[_], _> = Box::new_in([0u8; 96], LinearAllocator);
139 /// #
140 /// use ctru::services::ndsp::{AudioFormat, wave::{Wave, Status}};
141 ///
142 /// // Provide your own audio data.
143 /// let wave = Wave::new(_audio_data, AudioFormat::PCM16Stereo, false);
144 ///
145 /// // The `Wave` is free if never played before.
146 /// assert!(matches!(wave.status(), Status::Free));
147 /// # }
148 /// ```
149 pub fn status(&self) -> Status {
150 self.raw_data.status.try_into().unwrap()
151 }
152
153 /// Returns the amount of samples *read* by the NDSP process.
154 ///
155 /// # Notes
156 ///
157 /// This value varies depending on [`Wave::set_sample_count`].
158 pub fn sample_count(&self) -> usize {
159 self.raw_data.nsamples as usize
160 }
161
162 /// Returns the format of the audio data.
163 pub fn format(&self) -> AudioFormat {
164 self.audio_format
165 }
166
167 // Set the internal flag for the id of the channel playing this wave.
168 //
169 // Internal Use Only.
170 pub(crate) fn set_channel(&mut self, id: u8) {
171 self.played_on_channel = Some(id)
172 }
173
174 /// Set the amount of samples to be read.
175 ///
176 /// # Note
177 ///
178 ///
179 /// This function doesn't resize the internal buffer. Operations of this kind are particularly useful to allocate memory pools
180 /// for VBR (Variable BitRate) formats, like OGG Vorbis.
181 ///
182 /// # Errors
183 ///
184 /// This function will return an error if the sample size exceeds the buffer's capacity
185 /// or if the [`Wave`] is currently queued.
186 pub fn set_sample_count(&mut self, sample_count: usize) -> Result<(), Error> {
187 match self.status() {
188 Status::Playing | Status::Queued => {
189 return Err(Error::WaveBusy(self.played_on_channel.unwrap()));
190 }
191 _ => (),
192 }
193
194 let max_count = self.buffer.as_ref().len() / self.audio_format.size();
195
196 if sample_count > max_count {
197 return Err(Error::SampleCountOutOfBounds(sample_count, max_count));
198 }
199
200 self.raw_data.nsamples = sample_count as u32;
201
202 Ok(())
203 }
204}
205
206impl TryFrom<u8> for Status {
207 type Error = &'static str;
208
209 fn try_from(value: u8) -> Result<Self, Self::Error> {
210 match value {
211 0 => Ok(Self::Free),
212 1 => Ok(Self::Queued),
213 2 => Ok(Self::Playing),
214 3 => Ok(Self::Done),
215 _ => Err("Invalid Wave Status code"),
216 }
217 }
218}
219
220impl<Buffer> Drop for Wave<Buffer>
221where
222 Buffer: LinearAllocation + AsRef<[u8]>,
223{
224 fn drop(&mut self) {
225 // This was the only way I found I could check for improper drops of `Wave`.
226 // A panic was considered, but it would cause issues with drop order against `Ndsp`.
227 match self.status() {
228 Status::Free | Status::Done => (),
229 // If the status flag is "unfinished"
230 _ => {
231 // The unwrap is safe, since it must have a value in the case the status is "unfinished".
232 unsafe { ctru_sys::ndspChnWaveBufClear(self.played_on_channel.unwrap().into()) };
233 }
234 }
235
236 unsafe {
237 // Flag the buffer's RAM sector as unused
238 // This step has no real effect in normal applications and is skipped even by devkitPRO's own examples.
239 let _r = ctru_sys::DSP_InvalidateDataCache(
240 self.get_buffer().as_ptr().cast(),
241 self.get_buffer().len().try_into().unwrap(),
242 );
243 }
244 }
245}