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_core::{
6    PcmPacket, StreamEnvelope, StreamInspectorSnapshot, StreamInspectorStatus, StreamPacket,
7    TransportProfile,
8};
9
10pub use crate::runner_types::{LiveGraphConfig, LiveProcessReport, LiveSteadyStateSnapshot};
11use crate::{
12    AudioToControlQueue, ControlToAudioQueue, LiveAudioEvent, LiveControlEvent, LiveStreamLane,
13    runner_types::MAX_LIVE_EVENTS, validate_realtime_local_audio_profile,
14};
15
16/// Preallocated live graph runner for host audio callbacks.
17#[derive(Debug)]
18pub struct LiveGraphRunner<P> {
19    processor: P,
20    config: LiveGraphConfig,
21    input_planar: Vec<Vec<f32>>,
22    output_planar: Vec<Vec<f32>>,
23    scratch: BlockArena,
24    event_slots: [BlockEvent<'static>; MAX_LIVE_EVENTS],
25    control_to_audio: ControlToAudioQueue,
26    audio_to_control: AudioToControlQueue,
27}
28
29impl<P: Processor> LiveGraphRunner<P> {
30    /// Creates a runner after validating the realtime local audio profile.
31    pub fn new_realtime(
32        processor: P,
33        config: LiveGraphConfig,
34        profile: &TransportProfile,
35    ) -> Result<Self> {
36        validate_realtime_local_audio_profile(profile)?;
37        Self::new(processor, config)
38    }
39
40    /// Creates a runner, preparing the processor and preallocating all buffers
41    /// and queues for allocation-free steady-state processing.
42    pub fn new(mut processor: P, config: LiveGraphConfig) -> Result<Self> {
43        processor.prepare(PrepareConfig::new(
44            config.spec.sample_rate_hz(),
45            config.max_block_frames,
46            checked_channels(config.input_channels, "input")?,
47            checked_channels(config.spec.channels(), "output")?,
48        ));
49        let max_frames = config.max_block_frames as usize;
50        Ok(Self {
51            processor,
52            config,
53            input_planar: vec![vec![0.0; max_frames]; config.input_channels],
54            output_planar: vec![vec![0.0; max_frames]; config.spec.channels()],
55            scratch: BlockArena::with_f32_capacity(
56                max_frames * config.input_channels.max(config.spec.channels()).max(1),
57            ),
58            event_slots: [empty_event(); MAX_LIVE_EVENTS],
59            control_to_audio: ControlToAudioQueue::with_capacity(config.control_queue_capacity)?,
60            audio_to_control: AudioToControlQueue::with_capacity(config.audio_queue_capacity)?,
61        })
62    }
63
64    /// Enqueues a control event for delivery on the next process call.
65    pub fn enqueue_control_event(&mut self, event: LiveControlEvent) -> crate::LiveQueuePush {
66        self.control_to_audio.push(event)
67    }
68
69    /// Enqueues a short MIDI control event built from `bytes`.
70    pub fn enqueue_midi_short(
71        &mut self,
72        offset: u32,
73        bytes: &[u8],
74    ) -> Result<crate::LiveQueuePush> {
75        Ok(self.enqueue_control_event(LiveControlEvent::midi_short(offset, bytes)?))
76    }
77
78    /// Enqueues a parameter-set control event.
79    pub fn enqueue_param_set(
80        &mut self,
81        offset: u32,
82        param: u32,
83        value: f64,
84    ) -> Result<crate::LiveQueuePush> {
85        Ok(self.enqueue_control_event(LiveControlEvent::param_set(offset, param, value)?))
86    }
87
88    /// Processes one interleaved audio block: drains queued control events,
89    /// runs the processor, and writes the interleaved output.
90    pub fn process_interleaved_f32(
91        &mut self,
92        input: Option<&[f32]>,
93        output: &mut [f32],
94        frames: usize,
95        transport: Transport,
96    ) -> Result<LiveProcessReport> {
97        self.validate_block(input, output, frames)?;
98        let dropped_control_events = self.control_to_audio.take_dropped();
99        if dropped_control_events > 0 {
100            self.record_audio_event(LiveAudioEvent::DroppedControlEvents {
101                count: dropped_control_events,
102            });
103        }
104        let event_count = self.drain_control_events(frames)?;
105        self.copy_input(input, frames);
106        self.clear_output(frames);
107        self.run_processor(frames, event_count, transport)?;
108        self.copy_output(output, frames);
109        Ok(LiveProcessReport {
110            frames: frames as u32,
111            control_events: event_count,
112            dropped_control_events,
113        })
114    }
115
116    /// Drains audio-thread events back to the control thread, appending a
117    /// dropped-events marker if the queue overflowed.
118    pub fn drain_audio_events(&mut self) -> Vec<LiveAudioEvent> {
119        let mut events = Vec::new();
120        while let Some(event) = self.audio_to_control.pop() {
121            events.push(event);
122        }
123        let dropped = self.audio_to_control.take_dropped();
124        if dropped > 0 {
125            events.push(LiveAudioEvent::DroppedAudioEvents { count: dropped });
126        }
127        events
128    }
129
130    /// Drains audio-thread events and renders each as a diagnostic packet.
131    pub fn drain_audio_diagnostics(&mut self) -> Vec<sim_lib_stream_core::StreamPacket> {
132        self.drain_audio_events()
133            .into_iter()
134            .map(LiveAudioEvent::to_diagnostic_packet)
135            .collect()
136    }
137
138    /// Returns a stream inspector snapshot for the audio-to-control queue.
139    pub fn diagnostic_inspector(&self) -> Result<StreamInspectorSnapshot> {
140        let metadata = LiveStreamLane::Diagnostic.metadata(self.audio_to_control.capacity())?;
141        let stats = self.audio_to_control.stats();
142        Ok(StreamInspectorSnapshot::new(
143            &metadata,
144            Symbol::qualified("stream/route", "live-audio-callback"),
145            TransportProfile::realtime_local_audio().name().clone(),
146            StreamInspectorStatus::from_stats(&stats, false),
147            self.audio_to_control.len(),
148            &stats,
149            stats.pushed.checked_sub(1),
150            Vec::new(),
151        ))
152    }
153
154    /// Captures buffer and queue capacities for steady-state growth checks.
155    pub fn steady_state_snapshot(&self) -> LiveSteadyStateSnapshot {
156        LiveSteadyStateSnapshot {
157            input_lane_capacity: self.input_planar.iter().map(Vec::capacity).collect(),
158            output_lane_capacity: self.output_planar.iter().map(Vec::capacity).collect(),
159            scratch_capacity: self.scratch.f32_capacity(),
160            control_queue_capacity: self.control_to_audio.allocated_capacity(),
161            audio_queue_capacity: self.audio_to_control.allocated_capacity(),
162        }
163    }
164
165    /// Wraps an interleaved output block as a LAN buffered preview envelope.
166    pub fn buffered_preview_chunk(
167        &self,
168        output: &[f32],
169        frames: usize,
170        sequence: u64,
171    ) -> Result<StreamEnvelope> {
172        let samples = self.validate_preview_block(output, frames)?;
173        let packet = StreamPacket::Pcm(PcmPacket::f32(
174            self.config.spec.channels(),
175            frames,
176            output[..samples].to_vec(),
177        )?);
178        LiveStreamLane::AudioOutput.lan_buffered_preview_envelope(sequence, Vec::new(), packet)
179    }
180
181    fn validate_block(
182        &mut self,
183        input: Option<&[f32]>,
184        output: &[f32],
185        frames: usize,
186    ) -> Result<()> {
187        if frames > self.config.max_block_frames as usize {
188            self.record_audio_event(LiveAudioEvent::Xrun {
189                frames: frames as u32,
190                max_frames: self.config.max_block_frames,
191            });
192            return Err(Error::Eval(format!(
193                "live graph block has {frames} frames, max block is {}",
194                self.config.max_block_frames
195            )));
196        }
197        let input_samples = frames.saturating_mul(self.config.input_channels);
198        if let Some(samples) = input
199            && samples.len() < input_samples
200        {
201            return Err(Error::Eval(format!(
202                "live graph input has {} samples, expected at least {input_samples}",
203                samples.len()
204            )));
205        }
206        let output_samples = frames.saturating_mul(self.config.spec.channels());
207        if output.len() < output_samples {
208            return Err(Error::Eval(format!(
209                "live graph output has {} samples, expected at least {output_samples}",
210                output.len()
211            )));
212        }
213        Ok(())
214    }
215
216    fn validate_preview_block(&self, output: &[f32], frames: usize) -> Result<usize> {
217        if frames > self.config.max_block_frames as usize {
218            return Err(Error::Eval(format!(
219                "live graph preview has {frames} frames, max block is {}",
220                self.config.max_block_frames
221            )));
222        }
223        let output_samples = frames
224            .checked_mul(self.config.spec.channels())
225            .ok_or_else(|| Error::Eval("live graph preview sample count overflowed".to_owned()))?;
226        if output.len() < output_samples {
227            return Err(Error::Eval(format!(
228                "live graph preview has {} samples, expected at least {output_samples}",
229                output.len()
230            )));
231        }
232        Ok(output_samples)
233    }
234
235    fn drain_control_events(&mut self, frames: usize) -> Result<usize> {
236        let mut count = 0;
237        while let Some(event) = self.control_to_audio.pop() {
238            if event.offset() >= frames as u32 {
239                return Err(Error::Eval(format!(
240                    "live control event offset {} is outside block frames 0..{frames}",
241                    event.offset()
242                )));
243            }
244            self.event_slots[count] = event.to_block_event();
245            count += 1;
246        }
247        Ok(count)
248    }
249
250    fn copy_input(&mut self, input: Option<&[f32]>, frames: usize) {
251        for lane in &mut self.input_planar {
252            lane[..frames].fill(0.0);
253        }
254        if let Some(samples) = input {
255            for frame in 0..frames {
256                for channel in 0..self.config.input_channels {
257                    self.input_planar[channel][frame] =
258                        samples[frame * self.config.input_channels + channel];
259                }
260            }
261        }
262    }
263
264    fn clear_output(&mut self, frames: usize) {
265        for lane in &mut self.output_planar {
266            lane[..frames].fill(0.0);
267        }
268    }
269
270    fn copy_output(&self, output: &mut [f32], frames: usize) {
271        let channels = self.config.spec.channels();
272        for frame in 0..frames {
273            for channel in 0..channels {
274                output[frame * channels + channel] = self.output_planar[channel][frame];
275            }
276        }
277    }
278
279    fn run_processor(
280        &mut self,
281        frames: usize,
282        event_count: usize,
283        transport: Transport,
284    ) -> Result<()> {
285        let in_events = &self.event_slots[..event_count];
286        let processor = &mut self.processor;
287        let scratch = &mut self.scratch;
288        let input_planar = &self.input_planar;
289        let output_planar = &mut self.output_planar;
290        let audio_to_control = &mut self.audio_to_control;
291
292        macro_rules! run_block {
293            ($in_audio:expr, $out_audio:expr) => {{
294                let mut event_sink = LiveEventSink {
295                    queue: audio_to_control,
296                };
297                scratch.reset();
298                let mut block = ProcessBlock {
299                    frames: frames as u32,
300                    in_audio: $in_audio,
301                    out_audio: $out_audio,
302                    in_events,
303                    out_events: &mut event_sink,
304                    transport,
305                    scratch,
306                };
307                block.validate_audio_lanes()?;
308                processor.process(&mut block);
309                block.validate_audio_lanes()
310            }};
311        }
312
313        match (self.config.input_channels, self.config.spec.channels()) {
314            (0, 1) => {
315                let in_audio: [&[f32]; 0] = [];
316                let mut out_audio = [&mut output_planar[0][..frames]];
317                run_block!(&in_audio, &mut out_audio)
318            }
319            (0, 2) => {
320                let in_audio: [&[f32]; 0] = [];
321                let (left, right) = output_planar.split_at_mut(1);
322                let mut out_audio = [&mut left[0][..frames], &mut right[0][..frames]];
323                run_block!(&in_audio, &mut out_audio)
324            }
325            (1, 1) => {
326                let in_audio = [&input_planar[0][..frames]];
327                let mut out_audio = [&mut output_planar[0][..frames]];
328                run_block!(&in_audio, &mut out_audio)
329            }
330            (1, 2) => {
331                let in_audio = [&input_planar[0][..frames]];
332                let (left, right) = output_planar.split_at_mut(1);
333                let mut out_audio = [&mut left[0][..frames], &mut right[0][..frames]];
334                run_block!(&in_audio, &mut out_audio)
335            }
336            (2, 1) => {
337                let in_audio = [&input_planar[0][..frames], &input_planar[1][..frames]];
338                let mut out_audio = [&mut output_planar[0][..frames]];
339                run_block!(&in_audio, &mut out_audio)
340            }
341            (2, 2) => {
342                let in_audio = [&input_planar[0][..frames], &input_planar[1][..frames]];
343                let (left, right) = output_planar.split_at_mut(1);
344                let mut out_audio = [&mut left[0][..frames], &mut right[0][..frames]];
345                run_block!(&in_audio, &mut out_audio)
346            }
347            _ => Err(Error::Eval(
348                "live graph runner supports mono and stereo I/O".to_owned(),
349            )),
350        }
351    }
352
353    fn record_audio_event(&mut self, event: LiveAudioEvent) {
354        let _ = self.audio_to_control.push(event);
355    }
356}
357
358struct LiveEventSink<'a> {
359    queue: &'a mut AudioToControlQueue,
360}
361
362impl EventSink for LiveEventSink<'_> {
363    fn push(&mut self, event: BlockEvent<'_>) -> Result<()> {
364        if let Some(event) = LiveAudioEvent::from_processor_event(event) {
365            let _ = self.queue.push(event);
366        }
367        Ok(())
368    }
369}
370
371fn checked_channels(channels: usize, role: &str) -> Result<u16> {
372    u16::try_from(channels)
373        .map_err(|_| Error::Eval(format!("live graph {role} channel count exceeds u16")))
374}
375
376const fn empty_event() -> BlockEvent<'static> {
377    BlockEvent::ParamSet {
378        offset: 0,
379        param: 0,
380        value: 0.0,
381    }
382}