Skip to main content

dioxus_audio/
decoding.rs

1//! Complete-file decoding into immutable planar samples.
2
3use std::fmt;
4use std::mem::size_of;
5use std::sync::Arc;
6use std::time::Duration;
7
8use crate::AudioData;
9
10#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
11mod web;
12
13/// Resource options for one complete-file decode operation.
14#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15pub struct DecodeOptions {
16    max_decoded_bytes: u64,
17}
18
19impl DecodeOptions {
20    /// Default ceiling for the Rust-owned planar `f32` copy (128 MiB).
21    pub const DEFAULT_MAX_DECODED_BYTES: u64 = 128 * 1024 * 1024;
22
23    /// Override the ceiling for the Rust-owned planar `f32` copy.
24    ///
25    /// The browser may allocate the complete decoded PCM before this limit can
26    /// be checked. A successful copy may transiently retain both representations.
27    #[must_use]
28    pub fn with_max_decoded_bytes(mut self, max_decoded_bytes: u64) -> Self {
29        self.max_decoded_bytes = max_decoded_bytes;
30        self
31    }
32
33    pub fn max_decoded_bytes(self) -> u64 {
34        self.max_decoded_bytes
35    }
36
37    /// Check the Rust-owned planar `f32` bytes required by decoded metadata.
38    ///
39    /// Actual channel and frame counts are not known until the browser has
40    /// decoded the complete input. This check therefore cannot prevent the
41    /// browser's first PCM allocation.
42    pub fn check_decoded_size(
43        self,
44        channel_count: u64,
45        frame_count: u64,
46    ) -> Result<u64, DecodeError> {
47        if channel_count == 0 || frame_count == 0 {
48            return Err(DecodeError::backend(
49                "decoded channel and frame counts must be positive",
50            ));
51        }
52
53        let required_bytes = channel_count
54            .checked_mul(frame_count)
55            .and_then(|samples| samples.checked_mul(size_of::<f32>() as u64))
56            .ok_or_else(|| DecodeError::backend("decoded byte count overflowed"))?;
57        if required_bytes > self.max_decoded_bytes {
58            return Err(DecodeError::resource_limit(
59                required_bytes,
60                self.max_decoded_bytes,
61            ));
62        }
63
64        Ok(required_bytes)
65    }
66}
67
68impl Default for DecodeOptions {
69    fn default() -> Self {
70        Self {
71            max_decoded_bytes: Self::DEFAULT_MAX_DECODED_BYTES,
72        }
73    }
74}
75
76/// Portable category for a complete-file decode failure.
77#[derive(Clone, Copy, Debug, PartialEq, Eq)]
78#[non_exhaustive]
79pub enum DecodeErrorKind {
80    UnsupportedPlatform,
81    ResourceLimit,
82    AllocationFailure,
83    DecodeRejected,
84    Backend,
85}
86
87/// Failure from a complete-file decode operation.
88#[derive(Clone, Debug, PartialEq, Eq)]
89pub struct DecodeError {
90    kind: DecodeErrorKind,
91    message: String,
92    required_bytes: Option<u64>,
93    configured_bytes: Option<u64>,
94}
95
96impl DecodeError {
97    #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
98    fn unsupported() -> Self {
99        Self {
100            kind: DecodeErrorKind::UnsupportedPlatform,
101            message: "audio decoding requires the wasm32 web backend".into(),
102            required_bytes: None,
103            configured_bytes: None,
104        }
105    }
106
107    fn resource_limit(required_bytes: u64, configured_bytes: u64) -> Self {
108        Self {
109            kind: DecodeErrorKind::ResourceLimit,
110            message: format!(
111                "decoded PCM requires {required_bytes} Rust-owned bytes, exceeding the configured {configured_bytes}-byte limit"
112            ),
113            required_bytes: Some(required_bytes),
114            configured_bytes: Some(configured_bytes),
115        }
116    }
117
118    #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
119    fn allocation_failure(required_bytes: u64) -> Self {
120        Self {
121            kind: DecodeErrorKind::AllocationFailure,
122            message: format!(
123                "could not allocate {required_bytes} bytes for decoded planar samples"
124            ),
125            required_bytes: Some(required_bytes),
126            configured_bytes: None,
127        }
128    }
129
130    #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
131    fn decode_rejected() -> Self {
132        Self {
133            kind: DecodeErrorKind::DecodeRejected,
134            message: "browser rejected the complete audio data".into(),
135            required_bytes: None,
136            configured_bytes: None,
137        }
138    }
139
140    fn backend(message: impl Into<String>) -> Self {
141        Self {
142            kind: DecodeErrorKind::Backend,
143            message: message.into(),
144            required_bytes: None,
145            configured_bytes: None,
146        }
147    }
148
149    pub fn kind(&self) -> DecodeErrorKind {
150        self.kind
151    }
152
153    pub fn message(&self) -> &str {
154        &self.message
155    }
156
157    /// Bytes required for the Rust-owned planar PCM copy, when known.
158    pub fn required_bytes(&self) -> Option<u64> {
159        self.required_bytes
160    }
161
162    /// Configured Rust-copy ceiling for a resource-limit failure.
163    pub fn configured_bytes(&self) -> Option<u64> {
164        self.configured_bytes
165    }
166}
167
168impl fmt::Display for DecodeError {
169    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
170        formatter.write_str(&self.message)
171    }
172}
173
174impl std::error::Error for DecodeError {}
175
176/// Decode complete Audio Data into immutable planar samples.
177///
178/// The operation consumes its input and releases the Rust encoded allocation
179/// before awaiting browser decoding. Every settled or dropped browser
180/// operation requests context cleanup. Dropping suppresses its result but does
181/// not promise to abort work already started by the browser.
182pub async fn decode_audio_data(
183    audio: AudioData,
184    options: DecodeOptions,
185) -> Result<DecodedAudio, DecodeError> {
186    #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
187    {
188        web::decode_audio_data(audio, options).await
189    }
190
191    #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
192    {
193        let _ = (audio, options);
194        Err(DecodeError::unsupported())
195    }
196}
197
198/// An invalid Decoded Audio sample layout.
199#[derive(Clone, Debug, PartialEq, Eq)]
200#[non_exhaustive]
201pub enum DecodedAudioError {
202    NoChannels,
203    NoFrames,
204    MisalignedSamples { samples: usize, channels: usize },
205    InvalidSampleRate,
206}
207
208impl fmt::Display for DecodedAudioError {
209    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
210        match self {
211            Self::NoChannels => formatter.write_str("channel count must be positive"),
212            Self::NoFrames => formatter.write_str("frame count must be positive"),
213            Self::MisalignedSamples { samples, channels } => write!(
214                formatter,
215                "{samples} planar samples cannot form {channels} equal channels"
216            ),
217            Self::InvalidSampleRate => {
218                formatter.write_str("sample rate must be positive and finite")
219            }
220        }
221    }
222}
223
224impl std::error::Error for DecodedAudioError {}
225
226/// Immutable Decoded Audio backed by one flat-planar sample allocation.
227///
228/// Clones share the same sample storage.
229#[derive(Clone, Debug)]
230pub struct DecodedAudio {
231    inner: Arc<DecodedAudioInner>,
232}
233
234#[derive(Debug)]
235struct DecodedAudioInner {
236    samples: Vec<f32>,
237    channels: usize,
238    frames: usize,
239    sample_rate: f32,
240    duration: Duration,
241}
242
243impl DecodedAudio {
244    /// Consume flat channel-major samples with an equal positive frame count.
245    pub fn from_planar(
246        samples: Vec<f32>,
247        channels: usize,
248        sample_rate: f32,
249    ) -> Result<Self, DecodedAudioError> {
250        if channels == 0 {
251            return Err(DecodedAudioError::NoChannels);
252        }
253        if samples.is_empty() {
254            return Err(DecodedAudioError::NoFrames);
255        }
256        if !samples.len().is_multiple_of(channels) {
257            return Err(DecodedAudioError::MisalignedSamples {
258                samples: samples.len(),
259                channels,
260            });
261        }
262        if !sample_rate.is_finite() || sample_rate <= 0.0 {
263            return Err(DecodedAudioError::InvalidSampleRate);
264        }
265
266        let frames = samples.len() / channels;
267        let duration = Duration::try_from_secs_f64(frames as f64 / f64::from(sample_rate))
268            .map_err(|_| DecodedAudioError::InvalidSampleRate)?;
269
270        Ok(Self {
271            inner: Arc::new(DecodedAudioInner {
272                samples,
273                channels,
274                frames,
275                sample_rate,
276                duration,
277            }),
278        })
279    }
280
281    pub fn channel_count(&self) -> usize {
282        self.inner.channels
283    }
284
285    pub fn frame_count(&self) -> usize {
286        self.inner.frames
287    }
288
289    /// Effective sample rate of the decode context, in hertz.
290    pub fn sample_rate(&self) -> f32 {
291        self.inner.sample_rate
292    }
293
294    /// Duration derived from frame count and effective sample rate.
295    pub fn duration(&self) -> Duration {
296        self.inner.duration
297    }
298
299    pub fn channel(&self, index: usize) -> Option<&[f32]> {
300        let start = index.checked_mul(self.frame_count())?;
301        let end = start.checked_add(self.frame_count())?;
302        self.inner.samples.get(start..end)
303    }
304
305    pub fn channels(&self) -> impl ExactSizeIterator<Item = &[f32]> {
306        self.inner.samples.chunks_exact(self.frame_count())
307    }
308}