Skip to main content

sim_lib_audio_graph_live/
runner.rs

1use sim_kernel::{Error, Result, Symbol};
2use sim_lib_audio_graph_core::{
3    BlockArena, BlockEvent, EventSink, PrepareConfig, ProcessBlock, Processor, Transport,
4};
5use sim_lib_stream_audio::PcmSpec;
6use sim_lib_stream_core::{
7    PcmPacket, StreamEnvelope, StreamInspectorSnapshot, StreamInspectorStatus, StreamPacket,
8    TransportProfile,
9};
10
11use crate::{
12    AudioToControlQueue, ControlToAudioQueue, LiveAudioEvent, LiveControlEvent, LiveStreamLane,
13    validate_realtime_local_audio_profile,
14};
15
16const MAX_LIVE_CHANNELS: usize = 2;
17const MAX_LIVE_EVENTS: usize = 64;
18
19/// Live runner configuration.
20#[derive(Clone, Copy, Debug, PartialEq, Eq)]
21pub struct LiveGraphConfig {
22    spec: PcmSpec,
23    input_channels: usize,
24    max_block_frames: u32,
25    control_queue_capacity: usize,
26    audio_queue_capacity: usize,
27}
28
29/// Process result for one live callback.
30#[derive(Clone, Copy, Debug, PartialEq, Eq)]
31pub struct LiveProcessReport {
32    frames: u32,
33    control_events: usize,
34    dropped_control_events: u64,
35}
36
37/// Capacity snapshot used to verify steady-state processing does not grow.
38#[derive(Clone, Debug, PartialEq, Eq)]
39pub struct LiveSteadyStateSnapshot {
40    input_lane_capacity: Vec<usize>,
41    output_lane_capacity: Vec<usize>,
42    scratch_capacity: usize,
43    control_queue_capacity: usize,
44    audio_queue_capacity: usize,
45}
46
47/// Preallocated live graph runner for host audio callbacks.
48#[derive(Debug)]
49pub struct LiveGraphRunner<P> {
50    processor: P,
51    config: LiveGraphConfig,
52    input_planar: Vec<Vec<f32>>,
53    output_planar: Vec<Vec<f32>>,
54    scratch: BlockArena,
55    event_slots: [BlockEvent<'static>; MAX_LIVE_EVENTS],
56    control_to_audio: ControlToAudioQueue,
57    audio_to_control: AudioToControlQueue,
58}
59
60impl LiveGraphConfig {
61    /// Builds a runner configuration, validating block size, channel counts
62    /// (up to stereo), and bounded queue capacities.
63    pub fn new(
64        spec: PcmSpec,
65        input_channels: usize,
66        max_block_frames: u32,
67        control_queue_capacity: usize,
68        audio_queue_capacity: usize,
69    ) -> Result<Self> {
70        if max_block_frames == 0 {
71            return Err(Error::Eval(
72                "live graph max block frames must be greater than zero".to_owned(),
73            ));
74        }
75        if input_channels > MAX_LIVE_CHANNELS || spec.channels() > MAX_LIVE_CHANNELS {
76            return Err(Error::Eval(format!(
77                "live graph runner supports up to {MAX_LIVE_CHANNELS} channels"
78            )));
79        }
80        if control_queue_capacity == 0 || audio_queue_capacity == 0 {
81            return Err(Error::Eval(
82                "live graph queues must be bounded and non-zero".to_owned(),
83            ));
84        }
85        if control_queue_capacity > MAX_LIVE_EVENTS {
86            return Err(Error::Eval(format!(
87                "live graph control queue supports up to {MAX_LIVE_EVENTS} events per block"
88            )));
89        }
90        if audio_queue_capacity > MAX_LIVE_EVENTS {
91            return Err(Error::Eval(format!(
92                "live graph audio diagnostic queue supports up to {MAX_LIVE_EVENTS} events per block"
93            )));
94        }
95        Ok(Self {
96            spec,
97            input_channels,
98            max_block_frames,
99            control_queue_capacity,
100            audio_queue_capacity,
101        })
102    }
103
104    /// Builds a stereo-in/stereo-out configuration with full event queues.
105    pub fn stereo(sample_rate_hz: u32, max_block_frames: u32) -> Result<Self> {
106        Self::new(
107            PcmSpec::f32(2, sample_rate_hz)?,
108            2,
109            max_block_frames,
110            MAX_LIVE_EVENTS,
111            MAX_LIVE_EVENTS,
112        )
113    }
114
115    /// Returns the output PCM spec.
116    pub fn spec(self) -> PcmSpec {
117        self.spec
118    }
119
120    /// Returns the input channel count.
121    pub fn input_channels(self) -> usize {
122        self.input_channels
123    }
124
125    /// Returns the output channel count.
126    pub fn output_channels(self) -> usize {
127        self.spec.channels()
128    }
129
130    /// Returns the maximum block size in frames.
131    pub fn max_block_frames(self) -> u32 {
132        self.max_block_frames
133    }
134}
135
136impl<P: Processor> LiveGraphRunner<P> {
137    /// Creates a runner after validating the realtime local audio profile.
138    pub fn new_realtime(
139        processor: P,
140        config: LiveGraphConfig,
141        profile: &TransportProfile,
142    ) -> Result<Self> {
143        validate_realtime_local_audio_profile(profile)?;
144        Self::new(processor, config)
145    }
146
147    /// Creates a runner, preparing the processor and preallocating all buffers
148    /// and queues for allocation-free steady-state processing.
149    pub fn new(mut processor: P, config: LiveGraphConfig) -> Result<Self> {
150        processor.prepare(PrepareConfig::new(
151            config.spec.sample_rate_hz(),
152            config.max_block_frames,
153            checked_channels(config.input_channels, "input")?,
154            checked_channels(config.spec.channels(), "output")?,
155        ));
156        let max_frames = config.max_block_frames as usize;
157        Ok(Self {
158            processor,
159            config,
160            input_planar: vec![vec![0.0; max_frames]; config.input_channels],
161            output_planar: vec![vec![0.0; max_frames]; config.spec.channels()],
162            scratch: BlockArena::with_f32_capacity(
163                max_frames * config.input_channels.max(config.spec.channels()).max(1),
164            ),
165            event_slots: [empty_event(); MAX_LIVE_EVENTS],
166            control_to_audio: ControlToAudioQueue::with_capacity(config.control_queue_capacity)?,
167            audio_to_control: AudioToControlQueue::with_capacity(config.audio_queue_capacity)?,
168        })
169    }
170
171    /// Enqueues a control event for delivery on the next process call.
172    pub fn enqueue_control_event(&mut self, event: LiveControlEvent) -> crate::LiveQueuePush {
173        self.control_to_audio.push(event)
174    }
175
176    /// Enqueues a short MIDI control event built from `bytes`.
177    pub fn enqueue_midi_short(
178        &mut self,
179        offset: u32,
180        bytes: &[u8],
181    ) -> Result<crate::LiveQueuePush> {
182        Ok(self.enqueue_control_event(LiveControlEvent::midi_short(offset, bytes)?))
183    }
184
185    /// Enqueues a parameter-set control event.
186    pub fn enqueue_param_set(
187        &mut self,
188        offset: u32,
189        param: u32,
190        value: f64,
191    ) -> Result<crate::LiveQueuePush> {
192        Ok(self.enqueue_control_event(LiveControlEvent::param_set(offset, param, value)?))
193    }
194
195    /// Processes one interleaved audio block: drains queued control events,
196    /// runs the processor, and writes the interleaved output.
197    pub fn process_interleaved_f32(
198        &mut self,
199        input: Option<&[f32]>,
200        output: &mut [f32],
201        frames: usize,
202        transport: Transport,
203    ) -> Result<LiveProcessReport> {
204        self.validate_block(input, output, frames)?;
205        let dropped_control_events = self.control_to_audio.take_dropped();
206        if dropped_control_events > 0 {
207            self.record_audio_event(LiveAudioEvent::DroppedControlEvents {
208                count: dropped_control_events,
209            });
210        }
211        let event_count = self.drain_control_events(frames)?;
212        self.copy_input(input, frames);
213        self.clear_output(frames);
214        self.run_processor(frames, event_count, transport)?;
215        self.copy_output(output, frames);
216        Ok(LiveProcessReport {
217            frames: frames as u32,
218            control_events: event_count,
219            dropped_control_events,
220        })
221    }
222
223    /// Drains audio-thread events back to the control thread, appending a
224    /// dropped-events marker if the queue overflowed.
225    pub fn drain_audio_events(&mut self) -> Vec<LiveAudioEvent> {
226        let mut events = Vec::new();
227        while let Some(event) = self.audio_to_control.pop() {
228            events.push(event);
229        }
230        let dropped = self.audio_to_control.take_dropped();
231        if dropped > 0 {
232            events.push(LiveAudioEvent::DroppedAudioEvents { count: dropped });
233        }
234        events
235    }
236
237    /// Drains audio-thread events and renders each as a diagnostic packet.
238    pub fn drain_audio_diagnostics(&mut self) -> Vec<sim_lib_stream_core::StreamPacket> {
239        self.drain_audio_events()
240            .into_iter()
241            .map(LiveAudioEvent::to_diagnostic_packet)
242            .collect()
243    }
244
245    /// Returns a stream inspector snapshot for the audio-to-control queue.
246    pub fn diagnostic_inspector(&self) -> Result<StreamInspectorSnapshot> {
247        let metadata = LiveStreamLane::Diagnostic.metadata(self.audio_to_control.capacity())?;
248        let stats = self.audio_to_control.stats();
249        Ok(StreamInspectorSnapshot::new(
250            &metadata,
251            Symbol::qualified("stream/route", "live-audio-callback"),
252            TransportProfile::realtime_local_audio().name().clone(),
253            StreamInspectorStatus::from_stats(&stats, false),
254            self.audio_to_control.len(),
255            &stats,
256            stats.pushed.checked_sub(1),
257            Vec::new(),
258        ))
259    }
260
261    /// Captures buffer and queue capacities for steady-state growth checks.
262    pub fn steady_state_snapshot(&self) -> LiveSteadyStateSnapshot {
263        LiveSteadyStateSnapshot {
264            input_lane_capacity: self.input_planar.iter().map(Vec::capacity).collect(),
265            output_lane_capacity: self.output_planar.iter().map(Vec::capacity).collect(),
266            scratch_capacity: self.scratch.f32_capacity(),
267            control_queue_capacity: self.control_to_audio.allocated_capacity(),
268            audio_queue_capacity: self.audio_to_control.allocated_capacity(),
269        }
270    }
271
272    /// Wraps an interleaved output block as a LAN buffered preview envelope.
273    pub fn buffered_preview_chunk(
274        &self,
275        output: &[f32],
276        frames: usize,
277        sequence: u64,
278    ) -> Result<StreamEnvelope> {
279        let samples = self.validate_preview_block(output, frames)?;
280        let packet = StreamPacket::Pcm(PcmPacket::f32(
281            self.config.spec.channels(),
282            frames,
283            output[..samples].to_vec(),
284        )?);
285        LiveStreamLane::AudioOutput.lan_buffered_preview_envelope(sequence, Vec::new(), packet)
286    }
287
288    fn validate_block(
289        &mut self,
290        input: Option<&[f32]>,
291        output: &[f32],
292        frames: usize,
293    ) -> Result<()> {
294        if frames > self.config.max_block_frames as usize {
295            self.record_audio_event(LiveAudioEvent::Xrun {
296                frames: frames as u32,
297                max_frames: self.config.max_block_frames,
298            });
299            return Err(Error::Eval(format!(
300                "live graph block has {frames} frames, max block is {}",
301                self.config.max_block_frames
302            )));
303        }
304        let input_samples = frames.saturating_mul(self.config.input_channels);
305        if let Some(samples) = input
306            && samples.len() < input_samples
307        {
308            return Err(Error::Eval(format!(
309                "live graph input has {} samples, expected at least {input_samples}",
310                samples.len()
311            )));
312        }
313        let output_samples = frames.saturating_mul(self.config.spec.channels());
314        if output.len() < output_samples {
315            return Err(Error::Eval(format!(
316                "live graph output has {} samples, expected at least {output_samples}",
317                output.len()
318            )));
319        }
320        Ok(())
321    }
322
323    fn validate_preview_block(&self, output: &[f32], frames: usize) -> Result<usize> {
324        if frames > self.config.max_block_frames as usize {
325            return Err(Error::Eval(format!(
326                "live graph preview has {frames} frames, max block is {}",
327                self.config.max_block_frames
328            )));
329        }
330        let output_samples = frames
331            .checked_mul(self.config.spec.channels())
332            .ok_or_else(|| Error::Eval("live graph preview sample count overflowed".to_owned()))?;
333        if output.len() < output_samples {
334            return Err(Error::Eval(format!(
335                "live graph preview has {} samples, expected at least {output_samples}",
336                output.len()
337            )));
338        }
339        Ok(output_samples)
340    }
341
342    fn drain_control_events(&mut self, frames: usize) -> Result<usize> {
343        let mut count = 0;
344        while let Some(event) = self.control_to_audio.pop() {
345            if event.offset() > frames as u32 {
346                return Err(Error::Eval(format!(
347                    "live control event offset {} exceeds block frames {frames}",
348                    event.offset()
349                )));
350            }
351            self.event_slots[count] = event.to_block_event();
352            count += 1;
353        }
354        Ok(count)
355    }
356
357    fn copy_input(&mut self, input: Option<&[f32]>, frames: usize) {
358        for lane in &mut self.input_planar {
359            lane[..frames].fill(0.0);
360        }
361        if let Some(samples) = input {
362            for frame in 0..frames {
363                for channel in 0..self.config.input_channels {
364                    self.input_planar[channel][frame] =
365                        samples[frame * self.config.input_channels + channel];
366                }
367            }
368        }
369    }
370
371    fn clear_output(&mut self, frames: usize) {
372        for lane in &mut self.output_planar {
373            lane[..frames].fill(0.0);
374        }
375    }
376
377    fn copy_output(&self, output: &mut [f32], frames: usize) {
378        let channels = self.config.spec.channels();
379        for frame in 0..frames {
380            for channel in 0..channels {
381                output[frame * channels + channel] = self.output_planar[channel][frame];
382            }
383        }
384    }
385
386    fn run_processor(
387        &mut self,
388        frames: usize,
389        event_count: usize,
390        transport: Transport,
391    ) -> Result<()> {
392        let in_events = &self.event_slots[..event_count];
393        let processor = &mut self.processor;
394        let scratch = &mut self.scratch;
395        let input_planar = &self.input_planar;
396        let output_planar = &mut self.output_planar;
397        let audio_to_control = &mut self.audio_to_control;
398
399        macro_rules! run_block {
400            ($in_audio:expr, $out_audio:expr) => {{
401                let mut event_sink = LiveEventSink {
402                    queue: audio_to_control,
403                };
404                scratch.reset();
405                let mut block = ProcessBlock {
406                    frames: frames as u32,
407                    in_audio: $in_audio,
408                    out_audio: $out_audio,
409                    in_events,
410                    out_events: &mut event_sink,
411                    transport,
412                    scratch,
413                };
414                block.validate_audio_lanes()?;
415                processor.process(&mut block);
416                block.validate_audio_lanes()
417            }};
418        }
419
420        match (self.config.input_channels, self.config.spec.channels()) {
421            (0, 1) => {
422                let in_audio: [&[f32]; 0] = [];
423                let mut out_audio = [&mut output_planar[0][..frames]];
424                run_block!(&in_audio, &mut out_audio)
425            }
426            (0, 2) => {
427                let in_audio: [&[f32]; 0] = [];
428                let (left, right) = output_planar.split_at_mut(1);
429                let mut out_audio = [&mut left[0][..frames], &mut right[0][..frames]];
430                run_block!(&in_audio, &mut out_audio)
431            }
432            (1, 1) => {
433                let in_audio = [&input_planar[0][..frames]];
434                let mut out_audio = [&mut output_planar[0][..frames]];
435                run_block!(&in_audio, &mut out_audio)
436            }
437            (1, 2) => {
438                let in_audio = [&input_planar[0][..frames]];
439                let (left, right) = output_planar.split_at_mut(1);
440                let mut out_audio = [&mut left[0][..frames], &mut right[0][..frames]];
441                run_block!(&in_audio, &mut out_audio)
442            }
443            (2, 1) => {
444                let in_audio = [&input_planar[0][..frames], &input_planar[1][..frames]];
445                let mut out_audio = [&mut output_planar[0][..frames]];
446                run_block!(&in_audio, &mut out_audio)
447            }
448            (2, 2) => {
449                let in_audio = [&input_planar[0][..frames], &input_planar[1][..frames]];
450                let (left, right) = output_planar.split_at_mut(1);
451                let mut out_audio = [&mut left[0][..frames], &mut right[0][..frames]];
452                run_block!(&in_audio, &mut out_audio)
453            }
454            _ => Err(Error::Eval(
455                "live graph runner supports mono and stereo I/O".to_owned(),
456            )),
457        }
458    }
459
460    fn record_audio_event(&mut self, event: LiveAudioEvent) {
461        let _ = self.audio_to_control.push(event);
462    }
463}
464
465struct LiveEventSink<'a> {
466    queue: &'a mut AudioToControlQueue,
467}
468
469impl EventSink for LiveEventSink<'_> {
470    fn push(&mut self, event: BlockEvent<'_>) -> Result<()> {
471        if let Some(event) = LiveAudioEvent::from_processor_event(event) {
472            let _ = self.queue.push(event);
473        }
474        Ok(())
475    }
476}
477
478fn checked_channels(channels: usize, role: &str) -> Result<u16> {
479    u16::try_from(channels)
480        .map_err(|_| Error::Eval(format!("live graph {role} channel count exceeds u16")))
481}
482
483const fn empty_event() -> BlockEvent<'static> {
484    BlockEvent::ParamSet {
485        offset: 0,
486        param: 0,
487        value: 0.0,
488    }
489}
490
491impl LiveProcessReport {
492    /// Returns the number of frames processed.
493    pub fn frames(self) -> u32 {
494        self.frames
495    }
496
497    /// Returns the number of control events applied this block.
498    pub fn control_events(self) -> usize {
499        self.control_events
500    }
501
502    /// Returns the number of control events dropped before this block.
503    pub fn dropped_control_events(self) -> u64 {
504        self.dropped_control_events
505    }
506}