Skip to content

Commit 7895a17

Browse files
committed
[refactor] audio module into separate modules and add features for audio decoders.
1 parent 4313064 commit 7895a17

6 files changed

Lines changed: 357 additions & 322 deletions

File tree

crates/lambda-rs/Cargo.toml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,18 @@ with-wgpu-gl=["with-wgpu", "lambda-rs-platform/wgpu-with-gl"]
3535
# ---------------------------------- AUDIO ------------------------------------
3636

3737
# Umbrella features
38-
audio = ["audio-output-device"]
38+
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 = []
44+
45+
# Umbrella feature
46+
audio-sound-buffer = [
47+
"audio-sound-buffer-wav",
48+
"audio-sound-buffer-vorbis",
49+
]
4250

4351
# ------------------------------ RENDER VALIDATION -----------------------------
4452
# Granular, opt-in validation flags for release builds. Debug builds enable
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#![allow(clippy::needless_return)]
2+
3+
pub mod output;
Lines changed: 305 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,305 @@
1+
#![allow(clippy::needless_return)]
2+
3+
//! Audio output devices.
4+
//!
5+
//! This module provides a backend-agnostic audio output device API for Lambda
6+
//! applications. Platform and vendor details are implemented in
7+
//! `lambda-rs-platform` and MUST NOT be exposed through the `lambda-rs` public
8+
//! API.
9+
10+
use lambda_platform::audio::cpal as platform_audio;
11+
12+
use crate::audio::AudioError;
13+
14+
/// Output sample format used by an audio stream callback.
15+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16+
pub enum AudioSampleFormat {
17+
/// 32-bit floating point samples in the nominal range `[-1.0, 1.0]`.
18+
F32,
19+
/// Signed 16-bit integer samples mapped from normalized `f32`.
20+
I16,
21+
/// Unsigned 16-bit integer samples mapped from normalized `f32`.
22+
U16,
23+
}
24+
25+
impl AudioSampleFormat {
26+
fn from_platform(value: platform_audio::AudioSampleFormat) -> Self {
27+
match value {
28+
platform_audio::AudioSampleFormat::F32 => {
29+
return Self::F32;
30+
}
31+
platform_audio::AudioSampleFormat::I16 => {
32+
return Self::I16;
33+
}
34+
platform_audio::AudioSampleFormat::U16 => {
35+
return Self::U16;
36+
}
37+
}
38+
}
39+
}
40+
41+
/// Information available to audio output callbacks.
42+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
43+
pub struct AudioCallbackInfo {
44+
/// Audio frames per second.
45+
pub sample_rate: u32,
46+
/// Interleaved output channel count.
47+
pub channels: u16,
48+
/// The selected stream sample format.
49+
pub sample_format: AudioSampleFormat,
50+
}
51+
52+
impl AudioCallbackInfo {
53+
fn from_platform(value: platform_audio::AudioCallbackInfo) -> Self {
54+
return Self {
55+
sample_rate: value.sample_rate,
56+
channels: value.channels,
57+
sample_format: AudioSampleFormat::from_platform(value.sample_format),
58+
};
59+
}
60+
}
61+
62+
fn map_platform_error(error: platform_audio::AudioError) -> AudioError {
63+
match error {
64+
platform_audio::AudioError::InvalidSampleRate { requested } => {
65+
return AudioError::InvalidSampleRate { requested };
66+
}
67+
platform_audio::AudioError::InvalidChannels { requested } => {
68+
return AudioError::InvalidChannels { requested };
69+
}
70+
platform_audio::AudioError::NoDefaultDevice => {
71+
return AudioError::NoDefaultDevice;
72+
}
73+
platform_audio::AudioError::UnsupportedConfig {
74+
requested_sample_rate,
75+
requested_channels,
76+
} => {
77+
return AudioError::UnsupportedConfig {
78+
requested_sample_rate,
79+
requested_channels,
80+
};
81+
}
82+
platform_audio::AudioError::UnsupportedSampleFormat { details } => {
83+
return AudioError::UnsupportedSampleFormat { details };
84+
}
85+
other => {
86+
return AudioError::Platform {
87+
details: other.to_string(),
88+
};
89+
}
90+
}
91+
}
92+
93+
/// Metadata describing an available audio output device.
94+
#[derive(Clone, Debug, PartialEq, Eq)]
95+
pub struct AudioOutputDeviceInfo {
96+
/// Human-readable device name.
97+
pub name: String,
98+
/// Whether this device is the current default output device.
99+
pub is_default: bool,
100+
}
101+
102+
/// Real-time writer for audio output buffers.
103+
///
104+
/// This writer MUST be implemented without allocation and MUST write into the
105+
/// underlying device output buffer for the current callback invocation.
106+
pub trait AudioOutputWriter {
107+
/// Return the output channel count for the current callback invocation.
108+
fn channels(&self) -> u16;
109+
/// Return the number of frames in the output buffer for the current callback
110+
/// invocation.
111+
fn frames(&self) -> usize;
112+
/// Clear the entire output buffer to silence.
113+
fn clear(&mut self);
114+
115+
/// Write a normalized sample in the range `[-1.0, 1.0]`.
116+
///
117+
/// Implementations MUST clamp values outside `[-1.0, 1.0]`. Implementations
118+
/// MUST NOT panic for out-of-range indices and MUST perform no write in that
119+
/// case.
120+
fn set_sample(
121+
&mut self,
122+
frame_index: usize,
123+
channel_index: usize,
124+
sample: f32,
125+
);
126+
}
127+
128+
struct OutputWriterAdapter<'writer> {
129+
writer: &'writer mut dyn platform_audio::AudioOutputWriter,
130+
}
131+
132+
impl<'writer> AudioOutputWriter for OutputWriterAdapter<'writer> {
133+
fn channels(&self) -> u16 {
134+
return self.writer.channels();
135+
}
136+
137+
fn frames(&self) -> usize {
138+
return self.writer.frames();
139+
}
140+
141+
fn clear(&mut self) {
142+
self.writer.clear();
143+
return;
144+
}
145+
146+
fn set_sample(
147+
&mut self,
148+
frame_index: usize,
149+
channel_index: usize,
150+
sample: f32,
151+
) {
152+
self.writer.set_sample(frame_index, channel_index, sample);
153+
return;
154+
}
155+
}
156+
157+
/// An initialized audio output device.
158+
///
159+
/// The returned handle MUST be kept alive for as long as audio output is
160+
/// required. Dropping the handle MUST stop output.
161+
pub struct AudioOutputDevice {
162+
_platform: platform_audio::AudioDevice,
163+
}
164+
165+
/// Builder for creating an [`AudioOutputDevice`].
166+
#[derive(Debug, Clone)]
167+
pub struct AudioOutputDeviceBuilder {
168+
sample_rate: Option<u32>,
169+
channels: Option<u16>,
170+
label: Option<String>,
171+
}
172+
173+
impl AudioOutputDeviceBuilder {
174+
/// Create a builder with engine defaults.
175+
pub fn new() -> Self {
176+
return Self {
177+
sample_rate: None,
178+
channels: None,
179+
label: None,
180+
};
181+
}
182+
183+
/// Request a specific sample rate (Hz).
184+
pub fn with_sample_rate(mut self, rate: u32) -> Self {
185+
self.sample_rate = Some(rate);
186+
return self;
187+
}
188+
189+
/// Request a specific channel count.
190+
pub fn with_channels(mut self, channels: u16) -> Self {
191+
self.channels = Some(channels);
192+
return self;
193+
}
194+
195+
/// Attach a label for diagnostics.
196+
pub fn with_label(mut self, label: &str) -> Self {
197+
self.label = Some(label.to_string());
198+
return self;
199+
}
200+
201+
/// Initialize the default audio output device using the requested
202+
/// configuration.
203+
pub fn build(self) -> Result<AudioOutputDevice, AudioError> {
204+
let mut platform_builder = platform_audio::AudioDeviceBuilder::new();
205+
206+
if let Some(sample_rate) = self.sample_rate {
207+
platform_builder = platform_builder.with_sample_rate(sample_rate);
208+
}
209+
210+
if let Some(channels) = self.channels {
211+
platform_builder = platform_builder.with_channels(channels);
212+
}
213+
214+
if let Some(label) = self.label {
215+
platform_builder = platform_builder.with_label(&label);
216+
}
217+
218+
let platform_device =
219+
platform_builder.build().map_err(map_platform_error)?;
220+
221+
return Ok(AudioOutputDevice {
222+
_platform: platform_device,
223+
});
224+
}
225+
226+
/// Initialize the default audio output device and play audio via a callback.
227+
pub fn build_with_output_callback<Callback>(
228+
self,
229+
callback: Callback,
230+
) -> Result<AudioOutputDevice, AudioError>
231+
where
232+
Callback:
233+
'static + Send + FnMut(&mut dyn AudioOutputWriter, AudioCallbackInfo),
234+
{
235+
let mut platform_builder = platform_audio::AudioDeviceBuilder::new();
236+
237+
if let Some(sample_rate) = self.sample_rate {
238+
platform_builder = platform_builder.with_sample_rate(sample_rate);
239+
}
240+
241+
if let Some(channels) = self.channels {
242+
platform_builder = platform_builder.with_channels(channels);
243+
}
244+
245+
if let Some(label) = self.label {
246+
platform_builder = platform_builder.with_label(&label);
247+
}
248+
249+
let mut callback = callback;
250+
let platform_device = platform_builder
251+
.build_with_output_callback(move |writer, callback_info| {
252+
let mut adapter = OutputWriterAdapter { writer };
253+
callback(
254+
&mut adapter,
255+
AudioCallbackInfo::from_platform(callback_info),
256+
);
257+
return;
258+
})
259+
.map_err(map_platform_error)?;
260+
261+
return Ok(AudioOutputDevice {
262+
_platform: platform_device,
263+
});
264+
}
265+
}
266+
267+
impl Default for AudioOutputDeviceBuilder {
268+
fn default() -> Self {
269+
return Self::new();
270+
}
271+
}
272+
273+
/// Enumerate available audio output devices via the platform layer.
274+
pub fn enumerate_output_devices(
275+
) -> Result<Vec<AudioOutputDeviceInfo>, AudioError> {
276+
let devices =
277+
platform_audio::enumerate_devices().map_err(map_platform_error)?;
278+
279+
let devices = devices
280+
.into_iter()
281+
.map(|device| AudioOutputDeviceInfo {
282+
name: device.name,
283+
is_default: device.is_default,
284+
})
285+
.collect();
286+
287+
return Ok(devices);
288+
}
289+
290+
#[cfg(test)]
291+
mod tests {
292+
use super::*;
293+
294+
#[test]
295+
fn errors_map_without_leaking_platform_types() {
296+
let result = AudioOutputDeviceBuilder::new().with_sample_rate(0).build();
297+
assert!(matches!(
298+
result,
299+
Err(AudioError::InvalidSampleRate { requested: 0 })
300+
));
301+
302+
let _result = enumerate_output_devices();
303+
return;
304+
}
305+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#![allow(clippy::needless_return)]
2+
3+
/// Actionable errors produced by the `lambda-rs` audio facade.
4+
///
5+
/// This error type MUST remain backend-agnostic and MUST NOT expose platform or
6+
/// vendor types.
7+
#[derive(Clone, Debug)]
8+
pub enum AudioError {
9+
/// The requested sample rate was invalid.
10+
InvalidSampleRate { requested: u32 },
11+
/// The requested channel count was invalid.
12+
InvalidChannels { requested: u16 },
13+
/// No default audio output device is available.
14+
NoDefaultDevice,
15+
/// No supported output configuration satisfied the request.
16+
UnsupportedConfig {
17+
requested_sample_rate: Option<u32>,
18+
requested_channels: Option<u16>,
19+
},
20+
/// The selected output sample format is unsupported by this abstraction.
21+
UnsupportedSampleFormat { details: String },
22+
/// A platform or backend specific error occurred.
23+
Platform { details: String },
24+
}

0 commit comments

Comments
 (0)