Skip to main content

sim_lib_stream_jack/
model.rs

1use sim_kernel::{Error, Result, Symbol};
2use sim_lib_audio_graph_core::Transport;
3use sim_lib_stream_clock::Clock;
4use sim_lib_stream_core::StreamMedia;
5use sim_lib_stream_host::HostDirection;
6
7/// JACK timing metadata for a registered client.
8#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9pub struct JackTiming {
10    sample_rate_hz: u32,
11    block_frames: usize,
12    input_latency_frames: u32,
13    output_latency_frames: u32,
14}
15
16/// Snapshot of JACK transport state for one process callback.
17#[derive(Clone, Copy, Debug, PartialEq)]
18pub struct JackTransportState {
19    rolling: bool,
20    sample_pos: u64,
21    tempo_bpm: f64,
22    ppq_pos: f64,
23}
24
25/// SIM-visible JACK client with registered audio and MIDI ports.
26#[derive(Clone, Debug, PartialEq, Eq)]
27pub struct JackClient {
28    id: Symbol,
29    name: String,
30    timing: JackTiming,
31    audio_inputs: usize,
32    audio_outputs: usize,
33    midi_inputs: usize,
34    midi_outputs: usize,
35}
36
37/// Routable JACK port owned by a client.
38#[derive(Clone, Debug, PartialEq, Eq)]
39pub struct JackPort {
40    id: Symbol,
41    client: Symbol,
42    name: String,
43    media: StreamMedia,
44    direction: HostDirection,
45    index: usize,
46}
47
48impl JackTiming {
49    /// Builds timing metadata for a JACK client.
50    ///
51    /// # Errors
52    ///
53    /// Returns an error when `sample_rate_hz` or `block_frames` is zero.
54    pub fn new(
55        sample_rate_hz: u32,
56        block_frames: usize,
57        input_latency_frames: u32,
58        output_latency_frames: u32,
59    ) -> Result<Self> {
60        if sample_rate_hz == 0 {
61            return Err(Error::Eval(
62                "JACK sample rate must be greater than zero".to_owned(),
63            ));
64        }
65        if block_frames == 0 {
66            return Err(Error::Eval(
67                "JACK block size must be greater than zero".to_owned(),
68            ));
69        }
70        Ok(Self {
71            sample_rate_hz,
72            block_frames,
73            input_latency_frames,
74            output_latency_frames,
75        })
76    }
77
78    /// Returns the pro-audio default timing: 48 kHz, 128-frame blocks, and
79    /// 128-frame input and output latency.
80    pub fn pro_audio_default() -> Self {
81        Self::new(48_000, 128, 128, 128).expect("valid JACK timing")
82    }
83
84    /// Returns the sample rate in hertz.
85    pub fn sample_rate_hz(self) -> u32 {
86        self.sample_rate_hz
87    }
88
89    /// Returns the JACK block size in frames.
90    pub fn block_frames(self) -> usize {
91        self.block_frames
92    }
93
94    /// Returns the reported input (capture) latency in frames.
95    pub fn input_latency_frames(self) -> u32 {
96        self.input_latency_frames
97    }
98
99    /// Returns the reported output (playback) latency in frames.
100    pub fn output_latency_frames(self) -> u32 {
101        self.output_latency_frames
102    }
103
104    /// Builds a frame-counting [`Clock`] keyed by [`jack_clock_symbol`] at this
105    /// timing's sample rate.
106    ///
107    /// # Errors
108    ///
109    /// Returns an error if the underlying clock rejects the rate.
110    pub fn frame_clock(self) -> Result<Clock> {
111        Clock::frame(jack_clock_symbol(), u64::from(self.sample_rate_hz))
112    }
113}
114
115impl JackTransportState {
116    /// Builds a stopped transport at `sample_pos` with a default 120 BPM tempo
117    /// and a zero PPQ position.
118    pub fn stopped(sample_pos: u64) -> Self {
119        Self {
120            rolling: false,
121            sample_pos,
122            tempo_bpm: 120.0,
123            ppq_pos: 0.0,
124        }
125    }
126
127    /// Builds a rolling transport at `sample_pos` with the given tempo and PPQ
128    /// position.
129    ///
130    /// # Errors
131    ///
132    /// Returns an error when `tempo_bpm` is non-finite or not positive, or when
133    /// `ppq_pos` is non-finite.
134    pub fn rolling(sample_pos: u64, tempo_bpm: f64, ppq_pos: f64) -> Result<Self> {
135        if !tempo_bpm.is_finite() || tempo_bpm <= 0.0 {
136            return Err(Error::Eval(
137                "JACK transport tempo must be finite and positive".to_owned(),
138            ));
139        }
140        if !ppq_pos.is_finite() {
141            return Err(Error::Eval(
142                "JACK transport PPQ position must be finite".to_owned(),
143            ));
144        }
145        Ok(Self {
146            rolling: true,
147            sample_pos,
148            tempo_bpm,
149            ppq_pos,
150        })
151    }
152
153    /// Returns whether the transport is rolling (playing).
154    pub fn rolling_flag(self) -> bool {
155        self.rolling
156    }
157
158    /// Returns the transport position in sample frames.
159    pub fn sample_pos(self) -> u64 {
160        self.sample_pos
161    }
162
163    /// Returns the transport tempo in beats per minute.
164    pub fn tempo_bpm(self) -> f64 {
165        self.tempo_bpm
166    }
167
168    /// Returns the transport position in pulses per quarter note.
169    pub fn ppq_pos(self) -> f64 {
170        self.ppq_pos
171    }
172
173    /// Converts this snapshot into the audio-graph [`Transport`] consumed by a
174    /// process block.
175    pub fn to_graph_transport(self) -> Transport {
176        Transport {
177            playing: self.rolling,
178            sample_pos: self.sample_pos,
179            tempo_bpm: self.tempo_bpm,
180            ppq_pos: self.ppq_pos,
181        }
182    }
183}
184
185impl JackClient {
186    /// Builds a JACK client with the given name, timing, and port counts.
187    ///
188    /// The client id is derived from `name` as `jack/<name>/client`.
189    ///
190    /// # Errors
191    ///
192    /// Returns an error when `name` is empty or when the client registers no
193    /// audio ports (both audio counts are zero).
194    pub fn new(
195        name: impl Into<String>,
196        timing: JackTiming,
197        audio_inputs: usize,
198        audio_outputs: usize,
199        midi_inputs: usize,
200        midi_outputs: usize,
201    ) -> Result<Self> {
202        let name = name.into();
203        if name.is_empty() {
204            return Err(Error::Eval("JACK client name must not be empty".to_owned()));
205        }
206        if audio_inputs == 0 && audio_outputs == 0 {
207            return Err(Error::Eval(
208                "JACK client must register at least one audio port".to_owned(),
209            ));
210        }
211        Ok(Self {
212            id: Symbol::new(format!("jack/{name}/client")),
213            name,
214            timing,
215            audio_inputs,
216            audio_outputs,
217            midi_inputs,
218            midi_outputs,
219        })
220    }
221
222    /// Builds the SIM default client: name `SIM`, [`JackTiming::pro_audio_default`],
223    /// two audio inputs and outputs, and one MIDI input and output.
224    ///
225    /// # Errors
226    ///
227    /// Returns an error if the constructed client is rejected (it is not under
228    /// the default configuration).
229    pub fn sim_default() -> Result<Self> {
230        Self::new("SIM", JackTiming::pro_audio_default(), 2, 2, 1, 1)
231    }
232
233    /// Returns the client's stable id symbol.
234    pub fn id(&self) -> &Symbol {
235        &self.id
236    }
237
238    /// Returns the client's display name.
239    pub fn name(&self) -> &str {
240        &self.name
241    }
242
243    /// Returns the client's timing metadata.
244    pub fn timing(&self) -> JackTiming {
245        self.timing
246    }
247
248    /// Returns the number of registered audio input ports.
249    pub fn audio_inputs(&self) -> usize {
250        self.audio_inputs
251    }
252
253    /// Returns the number of registered audio output ports.
254    pub fn audio_outputs(&self) -> usize {
255        self.audio_outputs
256    }
257
258    /// Returns the number of registered MIDI input ports.
259    pub fn midi_inputs(&self) -> usize {
260        self.midi_inputs
261    }
262
263    /// Returns the number of registered MIDI output ports.
264    pub fn midi_outputs(&self) -> usize {
265        self.midi_outputs
266    }
267
268    /// Returns the client's audio direction inferred from its port counts.
269    ///
270    /// Both-sided clients (and the degenerate no-audio case) report
271    /// [`HostDirection::Duplex`].
272    pub fn direction(&self) -> HostDirection {
273        match (self.audio_inputs > 0, self.audio_outputs > 0) {
274            (true, true) => HostDirection::Duplex,
275            (true, false) => HostDirection::Input,
276            (false, true) => HostDirection::Output,
277            (false, false) => HostDirection::Duplex,
278        }
279    }
280
281    /// Returns whether this client can serve a stream in the `requested`
282    /// direction.
283    ///
284    /// A client matches its own direction, and a duplex client matches any
285    /// request.
286    pub fn is_compatible_with(&self, requested: HostDirection) -> bool {
287        self.direction() == requested || self.direction() == HostDirection::Duplex
288    }
289
290    /// Returns the client's routable ports: audio inputs, audio outputs, MIDI
291    /// inputs, then MIDI outputs, each numbered from zero.
292    pub fn ports(&self) -> Vec<JackPort> {
293        let mut ports = Vec::new();
294        ports.extend(self.audio_ports(HostDirection::Input, self.audio_inputs, "audio_in"));
295        ports.extend(self.audio_ports(HostDirection::Output, self.audio_outputs, "audio_out"));
296        ports.extend(self.midi_ports(HostDirection::Input, self.midi_inputs, "midi_in"));
297        ports.extend(self.midi_ports(HostDirection::Output, self.midi_outputs, "midi_out"));
298        ports
299    }
300
301    fn audio_ports(&self, direction: HostDirection, count: usize, stem: &str) -> Vec<JackPort> {
302        self.numbered_ports(StreamMedia::Pcm, direction, count, stem)
303    }
304
305    fn midi_ports(&self, direction: HostDirection, count: usize, stem: &str) -> Vec<JackPort> {
306        self.numbered_ports(StreamMedia::Midi, direction, count, stem)
307    }
308
309    fn numbered_ports(
310        &self,
311        media: StreamMedia,
312        direction: HostDirection,
313        count: usize,
314        stem: &str,
315    ) -> Vec<JackPort> {
316        (0..count)
317            .map(|index| {
318                let name = format!("{stem}_{index}");
319                JackPort::new(
320                    Symbol::new(format!("jack/{}/{}", self.name, name)),
321                    self.id.clone(),
322                    name,
323                    media,
324                    direction,
325                    index,
326                )
327            })
328            .collect()
329    }
330}
331
332impl JackPort {
333    /// Builds a port with the given id, owning client, name, media, direction,
334    /// and per-direction index.
335    pub fn new(
336        id: Symbol,
337        client: Symbol,
338        name: impl Into<String>,
339        media: StreamMedia,
340        direction: HostDirection,
341        index: usize,
342    ) -> Self {
343        Self {
344            id,
345            client,
346            name: name.into(),
347            media,
348            direction,
349            index,
350        }
351    }
352
353    /// Returns the port's stable id symbol.
354    pub fn id(&self) -> &Symbol {
355        &self.id
356    }
357
358    /// Returns the id symbol of the client that owns this port.
359    pub fn client(&self) -> &Symbol {
360        &self.client
361    }
362
363    /// Returns the port's display name.
364    pub fn name(&self) -> &str {
365        &self.name
366    }
367
368    /// Returns the port's stream media (PCM audio or MIDI).
369    pub fn media(&self) -> StreamMedia {
370        self.media
371    }
372
373    /// Returns the port's direction.
374    pub fn direction(&self) -> HostDirection {
375        self.direction
376    }
377
378    /// Returns the port's index among same-direction ports of its media.
379    pub fn index(&self) -> usize {
380        self.index
381    }
382}
383
384/// Returns the symbol that identifies the JACK frame clock (`clock:jack`).
385pub fn jack_clock_symbol() -> Symbol {
386    Symbol::qualified("clock", "jack")
387}