Skip to content

Commit 3116004

Browse files
committed
[update] lambda to define audio output devices and to implement a high level implemenation for audio playback.
1 parent aa150c3 commit 3116004

2 files changed

Lines changed: 303 additions & 1 deletion

File tree

crates/lambda-rs/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ with-wgpu-gl=["with-wgpu", "lambda-rs-platform/wgpu-with-gl"]
3838
audio = ["audio-output-device"]
3939

4040
# Granular feature flags (disabled by default)
41-
audio-output-device = []
41+
audio-output-device = ["lambda-rs-platform/audio-device"]
4242

4343
# ------------------------------ RENDER VALIDATION -----------------------------
4444
# Granular, opt-in validation flags for release builds. Debug builds enable

crates/lambda-rs/src/audio.rs

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

0 commit comments

Comments
 (0)