Skip to main content

sim_lib_stream_alsa/
bridge.rs

1use sim_kernel::{Error, Result};
2use sim_lib_audio_graph_core::{
3    BlockArena, NullEventSink, PrepareConfig, ProcessBlock, Processor, Transport,
4};
5use sim_lib_stream_audio::{
6    PcmBuffer, PcmSampleFormat, PcmSpec, f32_interleaved_to_planar, f32_planar_to_interleaved,
7    f32_samples_to_i16, i16_samples_to_f32,
8};
9use sim_lib_stream_core::{PcmPacket, PushResult, StreamPacket};
10use sim_lib_stream_host::HostCallbackQueue;
11
12/// Drives an audio graph processor from an ALSA playback callback.
13#[derive(Debug)]
14pub struct AlsaPlaybackBridge<P> {
15    processor: P,
16    spec: PcmSpec,
17    max_block_frames: u32,
18    scratch: BlockArena,
19    sample_pos: u64,
20}
21
22/// Converts ALSA capture buffers into PCM stream packets.
23#[derive(Clone)]
24pub struct AlsaCaptureBridge {
25    queue: HostCallbackQueue,
26    spec: PcmSpec,
27}
28
29impl<P: Processor> AlsaPlaybackBridge<P> {
30    /// Builds a playback bridge, preparing `processor` for the given spec.
31    ///
32    /// `max_block_frames` is the largest callback block the bridge will accept
33    /// and must be greater than zero. The processor is prepared with the spec's
34    /// sample rate, channel count, and this block bound.
35    pub fn new(mut processor: P, spec: PcmSpec, max_block_frames: u32) -> Result<Self> {
36        if max_block_frames == 0 {
37            return Err(Error::Eval(
38                "ALSA playback max block frames must be greater than zero".to_owned(),
39            ));
40        }
41        processor.prepare(PrepareConfig::new(
42            spec.sample_rate_hz(),
43            max_block_frames,
44            0,
45            checked_channels(spec.channels(), "playback output")?,
46        ));
47        Ok(Self {
48            processor,
49            spec,
50            max_block_frames,
51            scratch: BlockArena::with_f32_capacity(max_block_frames as usize * spec.channels()),
52            sample_pos: 0,
53        })
54    }
55
56    /// Renders `frames` of audio into a `PcmBuffer` in the spec's format.
57    ///
58    /// F32 output is returned directly; I16 output is converted from the
59    /// rendered F32 samples.
60    pub fn render_buffer(&mut self, frames: usize) -> Result<PcmBuffer> {
61        let interleaved = self.render_interleaved_f32(frames)?;
62        match self.spec.sample_format() {
63            PcmSampleFormat::F32 => PcmBuffer::f32(self.spec, frames, interleaved),
64            PcmSampleFormat::I16 => {
65                PcmBuffer::i16(self.spec, frames, f32_samples_to_i16(&interleaved)?)
66            }
67        }
68    }
69
70    /// Renders `frames` of audio and returns interleaved F32 samples.
71    ///
72    /// Errors when `frames` exceeds the configured `max_block_frames`. The
73    /// processor runs over a single `ProcessBlock` with a playing transport;
74    /// the bridge's sample position advances by `frames`.
75    pub fn render_interleaved_f32(&mut self, frames: usize) -> Result<Vec<f32>> {
76        if frames > self.max_block_frames as usize {
77            return Err(Error::Eval(format!(
78                "ALSA playback callback received {frames} frames, max is {}",
79                self.max_block_frames
80            )));
81        }
82        let mut output_planar = vec![vec![0.0; frames]; self.spec.channels()];
83        {
84            let input_refs: [&[f32]; 0] = [];
85            let mut output_refs = output_planar
86                .iter_mut()
87                .map(Vec::as_mut_slice)
88                .collect::<Vec<_>>();
89            self.scratch.reset();
90            let mut event_sink = NullEventSink;
91            let mut block = ProcessBlock {
92                frames: frames as u32,
93                in_audio: &input_refs,
94                out_audio: &mut output_refs,
95                in_events: &[],
96                out_events: &mut event_sink,
97                transport: Transport {
98                    playing: true,
99                    sample_pos: self.sample_pos,
100                    tempo_bpm: 120.0,
101                    ppq_pos: 0.0,
102                },
103                scratch: &mut self.scratch,
104            };
105            block.validate_audio_lanes()?;
106            self.processor.process(&mut block);
107            block.validate_audio_lanes()?;
108        }
109        self.sample_pos = self.sample_pos.saturating_add(frames as u64);
110        f32_planar_to_interleaved(&output_planar)
111    }
112
113    /// Returns the running sample position across rendered blocks.
114    pub fn sample_pos(&self) -> u64 {
115        self.sample_pos
116    }
117
118    /// Resets the processor, sample position, and scratch arena.
119    pub fn reset(&mut self) {
120        self.processor.reset();
121        self.sample_pos = 0;
122        self.scratch.reset();
123    }
124}
125
126impl AlsaCaptureBridge {
127    /// Builds a capture bridge that pushes packets onto `queue` in `spec`.
128    pub fn new(queue: HostCallbackQueue, spec: PcmSpec) -> Self {
129        Self { queue, spec }
130    }
131
132    /// Captures interleaved F32 `samples` into a PCM packet on the queue.
133    ///
134    /// The sample count must be a multiple of the spec's channel count. The
135    /// packet is encoded in the spec's sample format (F32 directly, or I16 by
136    /// conversion). Returns the queue's `PushResult`.
137    pub fn capture_interleaved_f32(&self, samples: &[f32]) -> Result<PushResult> {
138        let frames = samples_to_frames(samples.len(), self.spec.channels())?;
139        let planar = f32_interleaved_to_planar(samples, self.spec.channels())?;
140        let packet_samples = f32_planar_to_interleaved(&planar)?;
141        let packet = match self.spec.sample_format() {
142            PcmSampleFormat::F32 => PcmPacket::f32(self.spec.channels(), frames, packet_samples)?,
143            PcmSampleFormat::I16 => PcmPacket::i16(
144                self.spec.channels(),
145                frames,
146                f32_samples_to_i16(&packet_samples)?,
147            )?,
148        };
149        self.queue.callback_packet(StreamPacket::Pcm(packet))
150    }
151
152    /// Captures interleaved I16 `samples` into a PCM packet on the queue.
153    ///
154    /// The sample count must be a multiple of the spec's channel count. The
155    /// packet is encoded in the spec's sample format (I16 directly, or F32 by
156    /// conversion). Returns the queue's `PushResult`.
157    pub fn capture_interleaved_i16(&self, samples: &[i16]) -> Result<PushResult> {
158        let frames = samples_to_frames(samples.len(), self.spec.channels())?;
159        let f32_samples = i16_samples_to_f32(samples);
160        let packet = match self.spec.sample_format() {
161            PcmSampleFormat::F32 => PcmPacket::f32(self.spec.channels(), frames, f32_samples)?,
162            PcmSampleFormat::I16 => PcmPacket::i16(self.spec.channels(), frames, samples.to_vec())?,
163        };
164        self.queue.callback_packet(StreamPacket::Pcm(packet))
165    }
166
167    /// Returns the PCM spec the bridge encodes packets in.
168    pub fn spec(&self) -> PcmSpec {
169        self.spec
170    }
171}
172
173fn samples_to_frames(samples: usize, channels: usize) -> Result<usize> {
174    if channels == 0 {
175        return Err(Error::Eval(
176            "ALSA capture channel count must be greater than zero".to_owned(),
177        ));
178    }
179    if !samples.is_multiple_of(channels) {
180        return Err(Error::Eval(format!(
181            "ALSA capture sample length {samples} is not divisible by channels {channels}"
182        )));
183    }
184    Ok(samples / channels)
185}
186
187fn checked_channels(channels: usize, role: &str) -> Result<u16> {
188    u16::try_from(channels)
189        .map_err(|_| Error::Eval(format!("ALSA {role} channel count exceeds u16")))
190}