Skip to content

Commit bdb9575

Browse files
committed
[add] sound buffer implementation & implement wav/ogg decoding.
1 parent 7e8beb9 commit bdb9575

5 files changed

Lines changed: 454 additions & 13 deletions

File tree

crates/lambda-rs-platform/src/audio/symphonia/mod.rs

Lines changed: 294 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,21 @@ use std::{
1010
io::Cursor,
1111
};
1212

13+
#[cfg(feature = "audio-decode-vorbis")]
14+
use symphonia::core::codecs::CODEC_TYPE_VORBIS;
15+
#[cfg(feature = "audio-decode-wav")]
16+
use symphonia::core::sample::SampleFormat;
1317
use symphonia::core::{
18+
audio::SampleBuffer,
19+
codecs::{
20+
Decoder,
21+
DecoderOptions,
22+
},
1423
errors::Error,
15-
formats::FormatOptions,
24+
formats::{
25+
FormatOptions,
26+
FormatReader,
27+
},
1628
io::MediaSourceStream,
1729
meta::MetadataOptions,
1830
probe::Hint,
@@ -80,11 +92,39 @@ fn map_probe_error(source_description: &str, error: Error) -> AudioDecodeError {
8092
}
8193
}
8294

83-
fn probe_bytes(
95+
fn map_read_or_decode_error(
96+
source_description: &str,
97+
error: Error,
98+
) -> AudioDecodeError {
99+
match error {
100+
Error::Unsupported(_) => {
101+
return AudioDecodeError::UnsupportedFormat {
102+
details: format!("unsupported {source_description} audio codec"),
103+
};
104+
}
105+
Error::DecodeError(_) => {
106+
return AudioDecodeError::InvalidData {
107+
details: format!("{source_description} decode error: {error}"),
108+
};
109+
}
110+
Error::IoError(_) => {
111+
return AudioDecodeError::InvalidData {
112+
details: format!("{source_description} read error: {error}"),
113+
};
114+
}
115+
other => {
116+
return AudioDecodeError::DecodeFailed {
117+
details: format!("{source_description} decode failed: {other}"),
118+
};
119+
}
120+
}
121+
}
122+
123+
fn probe_format(
84124
bytes: &[u8],
85125
source_description: &str,
86126
extensions: &[&str],
87-
) -> Result<(), AudioDecodeError> {
127+
) -> Result<Box<dyn FormatReader>, AudioDecodeError> {
88128
let hint_value = hint_for_decode(extensions);
89129

90130
let cursor = Cursor::new(bytes.to_vec());
@@ -106,27 +146,270 @@ fn probe_bytes(
106146
});
107147
}
108148

149+
return Ok(probe_result.format);
150+
}
151+
152+
fn try_reserve_samples(
153+
samples: &mut Vec<f32>,
154+
source_description: &str,
155+
frames: Option<u64>,
156+
channels: Option<u16>,
157+
) -> Result<(), AudioDecodeError> {
158+
let (frames, channels) = match (frames, channels) {
159+
(Some(frames), Some(channels)) => (frames, channels),
160+
_ => {
161+
return Ok(());
162+
}
163+
};
164+
165+
let total_samples = frames.saturating_mul(channels as u64);
166+
if total_samples > usize::MAX as u64 {
167+
return Ok(());
168+
}
169+
170+
samples.try_reserve(total_samples as usize).map_err(|_| {
171+
return AudioDecodeError::DecodeFailed {
172+
details: format!("failed to allocate {source_description} sample buffer"),
173+
};
174+
})?;
109175
return Ok(());
110176
}
111177

178+
fn decode_track_to_interleaved_f32(
179+
format: &mut dyn FormatReader,
180+
track_id: u32,
181+
decoder: &mut dyn Decoder,
182+
source_description: &str,
183+
reserve_frames: Option<u64>,
184+
reserve_channels: Option<u16>,
185+
) -> Result<DecodedAudio, AudioDecodeError> {
186+
let mut samples: Vec<f32> = Vec::new();
187+
try_reserve_samples(
188+
&mut samples,
189+
source_description,
190+
reserve_frames,
191+
reserve_channels,
192+
)?;
193+
194+
let mut sample_rate: Option<u32> = None;
195+
let mut channel_count: Option<u16> = None;
196+
197+
loop {
198+
let packet = match format.next_packet() {
199+
Ok(packet) => packet,
200+
Err(Error::IoError(error))
201+
if error.kind() == std::io::ErrorKind::UnexpectedEof =>
202+
{
203+
break;
204+
}
205+
Err(error) => {
206+
return Err(map_read_or_decode_error(source_description, error));
207+
}
208+
};
209+
210+
if packet.track_id() != track_id {
211+
continue;
212+
}
213+
214+
let decoded = match decoder.decode(&packet) {
215+
Ok(decoded) => decoded,
216+
Err(Error::ResetRequired) => {
217+
decoder.reset();
218+
continue;
219+
}
220+
Err(error) => {
221+
return Err(map_read_or_decode_error(source_description, error));
222+
}
223+
};
224+
225+
let rate = decoded.spec().rate;
226+
if rate == 0 {
227+
return Err(AudioDecodeError::InvalidData {
228+
details: format!("{source_description} decoded sample rate was 0"),
229+
});
230+
}
231+
232+
let channels = decoded.spec().channels.count() as u16;
233+
if channels == 0 {
234+
return Err(AudioDecodeError::InvalidData {
235+
details: format!("{source_description} decoded channel count was 0"),
236+
});
237+
}
238+
239+
if channels != 1 && channels != 2 {
240+
return Err(AudioDecodeError::UnsupportedFormat {
241+
details: format!(
242+
"unsupported {source_description} channel count: {channels}"
243+
),
244+
});
245+
}
246+
247+
if let Some(previous_rate) = sample_rate {
248+
if previous_rate != rate {
249+
return Err(AudioDecodeError::InvalidData {
250+
details: format!(
251+
"{source_description} sample rate changed during decoding"
252+
),
253+
});
254+
}
255+
} else {
256+
sample_rate = Some(rate);
257+
}
258+
259+
if let Some(previous_channels) = channel_count {
260+
if previous_channels != channels {
261+
return Err(AudioDecodeError::InvalidData {
262+
details: format!(
263+
"{source_description} channel count changed during decoding"
264+
),
265+
});
266+
}
267+
} else {
268+
channel_count = Some(channels);
269+
}
270+
271+
let frames = decoded.frames();
272+
let mut sample_buffer =
273+
SampleBuffer::<f32>::new(frames as u64, *decoded.spec());
274+
sample_buffer.copy_interleaved_ref(decoded);
275+
samples.extend_from_slice(sample_buffer.samples());
276+
}
277+
278+
let sample_rate = sample_rate.ok_or(AudioDecodeError::InvalidData {
279+
details: format!(
280+
"{source_description} contained no decodable audio frames"
281+
),
282+
})?;
283+
let channels = channel_count.ok_or(AudioDecodeError::InvalidData {
284+
details: format!(
285+
"{source_description} contained no decodable channel configuration"
286+
),
287+
})?;
288+
289+
if samples.is_empty() {
290+
return Err(AudioDecodeError::InvalidData {
291+
details: format!("{source_description} contained no decoded samples"),
292+
});
293+
}
294+
295+
return Ok(DecodedAudio {
296+
samples,
297+
sample_rate,
298+
channels,
299+
});
300+
}
301+
112302
/// Decode WAV bytes into interleaved `f32` samples.
113303
#[cfg(feature = "audio-decode-wav")]
114304
pub fn decode_wav_bytes(
115305
bytes: &[u8],
116306
) -> Result<DecodedAudio, AudioDecodeError> {
117-
probe_bytes(bytes, "WAV", &["wav"])?;
118-
return Err(AudioDecodeError::DecodeFailed {
119-
details: "WAV decoding not implemented yet".to_string(),
120-
});
307+
let mut format = probe_format(bytes, "WAV", &["wav"])?;
308+
let (track_id, codec_params) = match format.default_track() {
309+
Some(track) => (track.id, track.codec_params.clone()),
310+
None => {
311+
return Err(AudioDecodeError::InvalidData {
312+
details: "no default audio track found".to_string(),
313+
});
314+
}
315+
};
316+
317+
let sample_format =
318+
codec_params
319+
.sample_format
320+
.ok_or(AudioDecodeError::UnsupportedFormat {
321+
details: "WAV sample format is unspecified".to_string(),
322+
})?;
323+
324+
match sample_format {
325+
SampleFormat::S16 | SampleFormat::S24 | SampleFormat::F32 => {}
326+
other => {
327+
return Err(AudioDecodeError::UnsupportedFormat {
328+
details: format!("unsupported WAV sample format: {other:?}"),
329+
});
330+
}
331+
}
332+
333+
let mut decoder = symphonia::default::get_codecs()
334+
.make(&codec_params, &DecoderOptions::default())
335+
.map_err(|error| map_read_or_decode_error("WAV", error))?;
336+
337+
return decode_track_to_interleaved_f32(
338+
&mut *format,
339+
track_id,
340+
&mut *decoder,
341+
"WAV",
342+
codec_params.n_frames,
343+
codec_params
344+
.channels
345+
.map(|channels| channels.count() as u16),
346+
);
121347
}
122348

123349
/// Decode OGG Vorbis bytes into interleaved `f32` samples.
124350
#[cfg(feature = "audio-decode-vorbis")]
125351
pub fn decode_ogg_vorbis_bytes(
126352
bytes: &[u8],
127353
) -> Result<DecodedAudio, AudioDecodeError> {
128-
probe_bytes(bytes, "OGG Vorbis", &["ogg", "oga"])?;
129-
return Err(AudioDecodeError::DecodeFailed {
130-
details: "OGG Vorbis decoding not implemented yet".to_string(),
131-
});
354+
let mut format = probe_format(bytes, "OGG Vorbis", &["ogg", "oga"])?;
355+
let (track_id, codec_params) = match format.default_track() {
356+
Some(track) => (track.id, track.codec_params.clone()),
357+
None => {
358+
return Err(AudioDecodeError::InvalidData {
359+
details: "no default audio track found".to_string(),
360+
});
361+
}
362+
};
363+
364+
if codec_params.codec != CODEC_TYPE_VORBIS {
365+
return Err(AudioDecodeError::UnsupportedFormat {
366+
details: "OGG stream is not Vorbis".to_string(),
367+
});
368+
}
369+
370+
let mut decoder = symphonia::default::get_codecs()
371+
.make(&codec_params, &DecoderOptions::default())
372+
.map_err(|error| map_read_or_decode_error("OGG Vorbis", error))?;
373+
374+
return decode_track_to_interleaved_f32(
375+
&mut *format,
376+
track_id,
377+
&mut *decoder,
378+
"OGG Vorbis",
379+
codec_params.n_frames,
380+
codec_params
381+
.channels
382+
.map(|channels| channels.count() as u16),
383+
);
384+
}
385+
386+
#[cfg(test)]
387+
mod tests {
388+
use super::*;
389+
390+
#[cfg(feature = "audio-decode-wav")]
391+
#[test]
392+
fn wav_decode_rejects_invalid_bytes() {
393+
let result = decode_wav_bytes(&[0u8, 1u8, 2u8, 3u8]);
394+
assert!(matches!(
395+
result,
396+
Err(AudioDecodeError::UnsupportedFormat { .. })
397+
| Err(AudioDecodeError::InvalidData { .. })
398+
| Err(AudioDecodeError::DecodeFailed { .. })
399+
));
400+
return;
401+
}
402+
403+
#[cfg(feature = "audio-decode-vorbis")]
404+
#[test]
405+
fn ogg_vorbis_decode_rejects_invalid_bytes() {
406+
let result = decode_ogg_vorbis_bytes(&[0u8, 1u8, 2u8, 3u8]);
407+
assert!(matches!(
408+
result,
409+
Err(AudioDecodeError::UnsupportedFormat { .. })
410+
| Err(AudioDecodeError::InvalidData { .. })
411+
| Err(AudioDecodeError::DecodeFailed { .. })
412+
));
413+
return;
414+
}
132415
}

crates/lambda-rs/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@ audio = ["audio-output-device", "audio-sound-buffer"]
3939

4040
# Granular feature flags
4141
audio-output-device = ["lambda-rs-platform/audio-device"]
42-
audio-sound-buffer-wav = []
43-
audio-sound-buffer-vorbis = []
42+
audio-sound-buffer-wav = ["lambda-rs-platform/audio-decode-wav"]
43+
audio-sound-buffer-vorbis = ["lambda-rs-platform/audio-decode-vorbis"]
4444

4545
# Umbrella feature
4646
audio-sound-buffer = [

0 commit comments

Comments
 (0)