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}