Skip to main content

sim_lib_stream_jack/
bridge.rs

1use sim_kernel::{Error, Result};
2use sim_lib_audio_graph_core::{
3    BlockArena, BlockEvent, NullEventSink, PrepareConfig, ProcessBlock, Processor,
4};
5use sim_lib_stream_audio::{PcmSpec, f32_interleaved_to_planar, f32_planar_to_interleaved};
6
7use crate::JackTransportState;
8
9/// Short MIDI event delivered by JACK for a process block.
10#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11pub struct JackMidiEvent {
12    offset: u32,
13    bytes: [u8; 3],
14    len: u8,
15}
16
17/// Drives an audio processor from a JACK process callback.
18#[derive(Debug)]
19pub struct JackGraphBridge<P> {
20    processor: P,
21    spec: PcmSpec,
22    input_channels: usize,
23    max_block_frames: u32,
24    scratch: BlockArena,
25    last_transport: JackTransportState,
26}
27
28impl JackMidiEvent {
29    /// Builds a short MIDI event delivered at sample `offset` within the block.
30    ///
31    /// `bytes` carries one to three status/data bytes; shorter messages are
32    /// zero-padded to the fixed three-byte buffer.
33    ///
34    /// # Errors
35    ///
36    /// Returns an error when `bytes` is empty or longer than three bytes.
37    ///
38    /// # Examples
39    ///
40    /// ```
41    /// use sim_lib_stream_jack::JackMidiEvent;
42    ///
43    /// let note_on = JackMidiEvent::short(64, &[0x90, 60, 100]).unwrap();
44    /// assert_eq!(note_on.offset(), 64);
45    /// assert_eq!(note_on.byte_len(), 3);
46    /// assert_eq!(note_on.bytes(), [0x90, 60, 100]);
47    ///
48    /// let too_long = JackMidiEvent::short(0, &[0, 1, 2, 3]);
49    /// assert!(too_long.is_err());
50    /// ```
51    pub fn short(offset: u32, bytes: &[u8]) -> Result<Self> {
52        if bytes.is_empty() || bytes.len() > 3 {
53            return Err(Error::Eval(
54                "JACK short MIDI event must contain one to three bytes".to_owned(),
55            ));
56        }
57        let mut padded = [0; 3];
58        padded[..bytes.len()].copy_from_slice(bytes);
59        Ok(Self {
60            offset,
61            bytes: padded,
62            len: bytes.len() as u8,
63        })
64    }
65
66    /// Returns the event's sample offset within its process block.
67    pub fn offset(self) -> u32 {
68        self.offset
69    }
70
71    /// Returns the three-byte message buffer, zero-padded past [`byte_len`](Self::byte_len).
72    pub fn bytes(self) -> [u8; 3] {
73        self.bytes
74    }
75
76    /// Returns the number of meaningful bytes in the message (one to three).
77    pub fn byte_len(self) -> u8 {
78        self.len
79    }
80
81    fn block_event(self) -> BlockEvent<'static> {
82        BlockEvent::Midi {
83            offset: self.offset,
84            bytes: self.bytes,
85            len: self.len,
86        }
87    }
88}
89
90impl<P: Processor> JackGraphBridge<P> {
91    /// Builds a bridge wrapping `processor` for a JACK client.
92    ///
93    /// The processor is prepared once with the PCM `spec` sample rate, the
94    /// `max_block_frames` upper bound, the `input_channels` count, and the
95    /// `spec` output channel count. Scratch storage is sized to the largest
96    /// block the callback may deliver.
97    ///
98    /// # Errors
99    ///
100    /// Returns an error when `max_block_frames` is zero or when either channel
101    /// count exceeds `u16`.
102    pub fn new(
103        mut processor: P,
104        spec: PcmSpec,
105        input_channels: usize,
106        max_block_frames: u32,
107    ) -> Result<Self> {
108        if max_block_frames == 0 {
109            return Err(Error::Eval(
110                "JACK block size must be greater than zero".to_owned(),
111            ));
112        }
113        processor.prepare(PrepareConfig::new(
114            spec.sample_rate_hz(),
115            max_block_frames,
116            checked_channels(input_channels, "input")?,
117            checked_channels(spec.channels(), "output")?,
118        ));
119        Ok(Self {
120            processor,
121            spec,
122            input_channels,
123            max_block_frames,
124            scratch: BlockArena::with_f32_capacity(
125                max_block_frames as usize * spec.channels().max(input_channels).max(1),
126            ),
127            last_transport: JackTransportState::stopped(0),
128        })
129    }
130
131    /// Runs one process block and returns interleaved output samples.
132    ///
133    /// `input` is interleaved capture audio (or silence when `None`), `frames`
134    /// is the block length, `transport` is the current JACK transport snapshot,
135    /// and `midi_events` are the short MIDI events for the block. The samples
136    /// are deinterleaved to planar buffers, the wrapped processor runs, and the
137    /// planar output is reinterleaved. The transport snapshot is retained for
138    /// [`last_transport`](Self::last_transport).
139    ///
140    /// # Errors
141    ///
142    /// Returns an error when `frames` exceeds the configured maximum block
143    /// size, when interleaving conversions fail, or when the processor's audio
144    /// lanes fail validation.
145    pub fn process_interleaved_f32(
146        &mut self,
147        input: Option<&[f32]>,
148        frames: usize,
149        transport: JackTransportState,
150        midi_events: &[JackMidiEvent],
151    ) -> Result<Vec<f32>> {
152        if frames > self.max_block_frames as usize {
153            return Err(Error::Eval(format!(
154                "JACK callback received {frames} frames, max block size is {}",
155                self.max_block_frames
156            )));
157        }
158        let input_planar = match input {
159            Some(samples) => f32_interleaved_to_planar(samples, self.input_channels)?,
160            None => vec![vec![0.0; frames]; self.input_channels],
161        };
162        let mut output_planar = vec![vec![0.0; frames]; self.spec.channels()];
163        let input_events = midi_events
164            .iter()
165            .copied()
166            .map(JackMidiEvent::block_event)
167            .collect::<Vec<_>>();
168        {
169            let input_refs = input_planar.iter().map(Vec::as_slice).collect::<Vec<_>>();
170            let mut output_refs = output_planar
171                .iter_mut()
172                .map(Vec::as_mut_slice)
173                .collect::<Vec<_>>();
174            self.scratch.reset();
175            let mut event_sink = NullEventSink;
176            let mut block = ProcessBlock {
177                frames: frames as u32,
178                in_audio: &input_refs,
179                out_audio: &mut output_refs,
180                in_events: &input_events,
181                out_events: &mut event_sink,
182                transport: transport.to_graph_transport(),
183                scratch: &mut self.scratch,
184            };
185            block.validate_audio_lanes()?;
186            self.processor.process(&mut block);
187            block.validate_audio_lanes()?;
188        }
189        self.last_transport = transport;
190        f32_planar_to_interleaved(&output_planar)
191    }
192
193    /// Returns the transport snapshot from the most recent process block.
194    pub fn last_transport(&self) -> JackTransportState {
195        self.last_transport
196    }
197
198    /// Resets the wrapped processor, scratch arena, and retained transport.
199    pub fn reset(&mut self) {
200        self.processor.reset();
201        self.last_transport = JackTransportState::stopped(0);
202        self.scratch.reset();
203    }
204}
205
206fn checked_channels(channels: usize, role: &str) -> Result<u16> {
207    u16::try_from(channels)
208        .map_err(|_| Error::Eval(format!("JACK {role} channel count exceeds u16")))
209}