Skip to content

Commit 6759146

Browse files
committed
[add] error types and audio output writing.
1 parent 3116004 commit 6759146

2 files changed

Lines changed: 284 additions & 0 deletions

File tree

crates/lambda-rs-platform/src/cpal/device.rs

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,184 @@ pub trait AudioOutputWriter {
6363
);
6464
}
6565

66+
/// A typed view of an interleaved output buffer for a single callback.
67+
///
68+
/// This type is internal and exists to support backend callback adapters.
69+
#[allow(dead_code)]
70+
pub(crate) enum AudioOutputBuffer<'buffer> {
71+
/// Interleaved `f32` samples.
72+
F32(&'buffer mut [f32]),
73+
/// Interleaved `i16` samples.
74+
I16(&'buffer mut [i16]),
75+
/// Interleaved `u16` samples.
76+
U16(&'buffer mut [u16]),
77+
}
78+
79+
impl<'buffer> AudioOutputBuffer<'buffer> {
80+
#[allow(dead_code)]
81+
fn len(&self) -> usize {
82+
match self {
83+
Self::F32(buffer) => {
84+
return buffer.len();
85+
}
86+
Self::I16(buffer) => {
87+
return buffer.len();
88+
}
89+
Self::U16(buffer) => {
90+
return buffer.len();
91+
}
92+
}
93+
}
94+
95+
fn sample_format(&self) -> AudioSampleFormat {
96+
match self {
97+
Self::F32(_) => {
98+
return AudioSampleFormat::F32;
99+
}
100+
Self::I16(_) => {
101+
return AudioSampleFormat::I16;
102+
}
103+
Self::U16(_) => {
104+
return AudioSampleFormat::U16;
105+
}
106+
}
107+
}
108+
}
109+
110+
/// An [`AudioOutputWriter`] implementation for interleaved buffers.
111+
///
112+
/// This type is internal and exists to support backend callback adapters.
113+
#[allow(dead_code)]
114+
pub(crate) struct InterleavedAudioOutputWriter<'buffer> {
115+
channels: u16,
116+
frames: usize,
117+
buffer: AudioOutputBuffer<'buffer>,
118+
}
119+
120+
impl<'buffer> InterleavedAudioOutputWriter<'buffer> {
121+
#[allow(dead_code)]
122+
pub fn new(channels: u16, buffer: AudioOutputBuffer<'buffer>) -> Self {
123+
let channels_usize = channels as usize;
124+
let frames = if channels_usize == 0 {
125+
0
126+
} else {
127+
buffer.len() / channels_usize
128+
};
129+
130+
return Self {
131+
channels,
132+
frames,
133+
buffer,
134+
};
135+
}
136+
137+
#[allow(dead_code)]
138+
pub fn sample_format(&self) -> AudioSampleFormat {
139+
return self.buffer.sample_format();
140+
}
141+
}
142+
143+
#[allow(dead_code)]
144+
fn clamp_normalized_sample(sample: f32) -> f32 {
145+
if sample > 1.0 {
146+
return 1.0;
147+
}
148+
149+
if sample < -1.0 {
150+
return -1.0;
151+
}
152+
153+
return sample;
154+
}
155+
156+
impl<'buffer> AudioOutputWriter for InterleavedAudioOutputWriter<'buffer> {
157+
fn channels(&self) -> u16 {
158+
return self.channels;
159+
}
160+
161+
fn frames(&self) -> usize {
162+
return self.frames;
163+
}
164+
165+
fn clear(&mut self) {
166+
match &mut self.buffer {
167+
AudioOutputBuffer::F32(buffer) => {
168+
buffer.fill(0.0);
169+
return;
170+
}
171+
AudioOutputBuffer::I16(buffer) => {
172+
buffer.fill(0);
173+
return;
174+
}
175+
AudioOutputBuffer::U16(buffer) => {
176+
buffer.fill(32768);
177+
return;
178+
}
179+
}
180+
}
181+
182+
fn set_sample(
183+
&mut self,
184+
frame_index: usize,
185+
channel_index: usize,
186+
sample: f32,
187+
) {
188+
let channels = self.channels as usize;
189+
if channels == 0 {
190+
return;
191+
}
192+
193+
if channel_index >= channels {
194+
if cfg!(all(debug_assertions, not(test))) {
195+
eprintln!(
196+
"audio: set_sample channel_index out of range (channel_index={channel_index} channels={channels})"
197+
);
198+
}
199+
return;
200+
}
201+
202+
if frame_index >= self.frames {
203+
if cfg!(all(debug_assertions, not(test))) {
204+
eprintln!(
205+
"audio: set_sample frame_index out of range (frame_index={frame_index} frames={})",
206+
self.frames
207+
);
208+
}
209+
return;
210+
}
211+
212+
let sample_index = frame_index * channels + channel_index;
213+
if sample_index >= self.buffer.len() {
214+
if cfg!(all(debug_assertions, not(test))) {
215+
eprintln!(
216+
"audio: set_sample buffer index out of range (sample_index={sample_index} len={})",
217+
self.buffer.len()
218+
);
219+
}
220+
return;
221+
}
222+
223+
let sample = clamp_normalized_sample(sample);
224+
225+
match &mut self.buffer {
226+
AudioOutputBuffer::F32(buffer) => {
227+
buffer[sample_index] = sample;
228+
return;
229+
}
230+
AudioOutputBuffer::I16(buffer) => {
231+
let scaled = (sample * 32767.0).round();
232+
buffer[sample_index] = scaled as i16;
233+
return;
234+
}
235+
AudioOutputBuffer::U16(buffer) => {
236+
let scaled = ((sample + 1.0) * 0.5 * 65535.0).round();
237+
buffer[sample_index] = scaled as u16;
238+
return;
239+
}
240+
}
241+
}
242+
}
243+
66244
/// Metadata describing an available audio output device.
67245
#[derive(Clone, Debug, PartialEq, Eq)]
68246
pub struct AudioDeviceInfo {
@@ -329,4 +507,82 @@ mod tests {
329507
}
330508
}
331509
}
510+
511+
#[test]
512+
fn writer_clear_sets_silence_for_all_formats() {
513+
let mut buffer_f32 = [1.0, -1.0, 0.5, -0.5];
514+
let mut writer = InterleavedAudioOutputWriter::new(
515+
2,
516+
AudioOutputBuffer::F32(&mut buffer_f32),
517+
);
518+
writer.clear();
519+
assert_eq!(buffer_f32, [0.0, 0.0, 0.0, 0.0]);
520+
521+
let mut buffer_i16 = [1, -1, 200, -200];
522+
let mut writer = InterleavedAudioOutputWriter::new(
523+
2,
524+
AudioOutputBuffer::I16(&mut buffer_i16),
525+
);
526+
writer.clear();
527+
assert_eq!(buffer_i16, [0, 0, 0, 0]);
528+
529+
let mut buffer_u16 = [0, 1, 65535, 12345];
530+
let mut writer = InterleavedAudioOutputWriter::new(
531+
2,
532+
AudioOutputBuffer::U16(&mut buffer_u16),
533+
);
534+
writer.clear();
535+
assert_eq!(buffer_u16, [32768, 32768, 32768, 32768]);
536+
}
537+
538+
#[test]
539+
fn writer_set_sample_clamps_and_converts() {
540+
let mut buffer_f32 = [0.0, 0.0, 0.0, 0.0];
541+
let mut writer = InterleavedAudioOutputWriter::new(
542+
2,
543+
AudioOutputBuffer::F32(&mut buffer_f32),
544+
);
545+
writer.set_sample(0, 0, 2.0);
546+
writer.set_sample(0, 1, -2.0);
547+
assert_eq!(buffer_f32[0], 1.0);
548+
assert_eq!(buffer_f32[1], -1.0);
549+
550+
let mut buffer_i16 = [0, 0, 0, 0];
551+
let mut writer = InterleavedAudioOutputWriter::new(
552+
2,
553+
AudioOutputBuffer::I16(&mut buffer_i16),
554+
);
555+
writer.set_sample(0, 0, 1.0);
556+
writer.set_sample(0, 1, -1.0);
557+
writer.set_sample(1, 0, 0.0);
558+
assert_eq!(buffer_i16[0], 32767);
559+
assert_eq!(buffer_i16[1], -32767);
560+
assert_eq!(buffer_i16[2], 0);
561+
562+
let mut buffer_u16 = [0, 0, 0, 0];
563+
let mut writer = InterleavedAudioOutputWriter::new(
564+
2,
565+
AudioOutputBuffer::U16(&mut buffer_u16),
566+
);
567+
writer.set_sample(0, 0, -1.0);
568+
writer.set_sample(0, 1, 0.0);
569+
writer.set_sample(1, 0, 1.0);
570+
assert_eq!(buffer_u16[0], 0);
571+
assert_eq!(buffer_u16[1], 32768);
572+
assert_eq!(buffer_u16[2], 65535);
573+
}
574+
575+
#[test]
576+
fn writer_set_sample_is_noop_for_out_of_range_indices() {
577+
let mut buffer_f32 = [0.25, 0.25, 0.25, 0.25];
578+
let mut writer = InterleavedAudioOutputWriter::new(
579+
2,
580+
AudioOutputBuffer::F32(&mut buffer_f32),
581+
);
582+
583+
writer.set_sample(10, 0, 1.0);
584+
writer.set_sample(0, 10, 1.0);
585+
586+
assert_eq!(buffer_f32, [0.25, 0.25, 0.25, 0.25]);
587+
}
332588
}

crates/lambda-rs/src/audio.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,3 +301,31 @@ pub fn enumerate_output_devices(
301301

302302
return Ok(devices);
303303
}
304+
305+
#[cfg(test)]
306+
mod tests {
307+
use super::*;
308+
309+
#[test]
310+
fn errors_map_without_leaking_platform_types() {
311+
let result = AudioOutputDeviceBuilder::new().with_sample_rate(0).build();
312+
assert!(matches!(
313+
result,
314+
Err(AudioError::InvalidSampleRate { requested: 0 })
315+
));
316+
317+
let result = enumerate_output_devices();
318+
match result {
319+
Err(AudioError::Platform { details }) => {
320+
assert_eq!(details, "audio host unavailable: audio backend not wired");
321+
return;
322+
}
323+
Ok(_devices) => {
324+
panic!("expected platform error, got Ok");
325+
}
326+
Err(error) => {
327+
panic!("expected platform error, got {error:?}");
328+
}
329+
}
330+
}
331+
}

0 commit comments

Comments
 (0)