Skip to main content

sim_lib_stream_jack/
backend.rs

1use sim_kernel::{Error, Result, Symbol};
2use sim_lib_stream_core::{BufferPolicy, StreamMedia};
3use sim_lib_stream_host::{
4    HostBackend, HostBackendCapability, HostBackendInfo, HostClockInfo, HostDeviceInventory,
5    HostDeviceSpec, HostDirection, HostLatencyInfo, HostOpenStream, HostPortSpec, HostStreamConfig,
6    HostStreamConfigRequest,
7};
8
9use crate::{JackClient, JackTiming, jack_clock_symbol};
10
11/// Returns the symbol that identifies the JACK host backend (`stream/host:jack`).
12pub fn jack_backend_symbol() -> Symbol {
13    Symbol::qualified("stream/host", "jack")
14}
15
16/// Returns the symbol that identifies the JACK transport (`stream/transport:jack`).
17pub fn jack_transport_symbol() -> Symbol {
18    Symbol::qualified("stream/transport", "jack")
19}
20
21/// JACK host backend with deterministic provider data.
22#[derive(Clone, Debug)]
23pub struct JackBackend {
24    info: HostBackendInfo,
25    clients: Vec<JackClient>,
26}
27
28impl Default for JackBackend {
29    fn default() -> Self {
30        Self::new(Vec::new())
31    }
32}
33
34impl JackBackend {
35    /// Builds a live backend over the given JACK clients.
36    ///
37    /// The backend reports itself as connected (`true`) and derives its
38    /// capability set from the supplied clients' audio, MIDI, and duplex ports.
39    pub fn new(clients: Vec<JackClient>) -> Self {
40        Self {
41            info: HostBackendInfo::new(
42                jack_backend_symbol(),
43                jack_transport_symbol(),
44                StreamMedia::Pcm,
45                true,
46            )
47            .with_capabilities(capabilities_for(&clients, true)),
48            clients,
49        }
50    }
51
52    /// Builds an offline backend seeded with the SIM default client.
53    ///
54    /// The backend reports itself as disconnected and adds the
55    /// [`HostBackendCapability::Offline`] and [`HostBackendCapability::Fake`]
56    /// capabilities, making it suitable for validation without a running JACK
57    /// server.
58    pub fn fake() -> Self {
59        let clients = vec![JackClient::sim_default().expect("valid SIM JACK client")];
60        Self {
61            info: HostBackendInfo::new(
62                jack_backend_symbol(),
63                jack_transport_symbol(),
64                StreamMedia::Pcm,
65                false,
66            )
67            .with_capabilities(capabilities_for(&clients, false)),
68            clients,
69        }
70    }
71
72    /// Returns the JACK clients registered with this backend.
73    pub fn list_clients(&self) -> &[JackClient] {
74        &self.clients
75    }
76
77    /// Returns the client named `SIM`, if one is registered.
78    pub fn sim_client(&self) -> Option<&JackClient> {
79        self.clients.iter().find(|client| client.name() == "SIM")
80    }
81
82    /// Opens a duplex stream on the SIM client with a bounded buffer of
83    /// `capacity` frames.
84    ///
85    /// # Errors
86    ///
87    /// Returns an error if no SIM client is registered or if the open request
88    /// is rejected.
89    pub fn open_sim_client(&self, capacity: usize) -> Result<HostOpenStream> {
90        let client = self
91            .sim_client()
92            .ok_or_else(|| Error::Eval("JACK SIM client was not found".to_owned()))?;
93        self.open(request(client, HostDirection::Duplex, capacity)?)
94    }
95
96    fn require_client(&self, client_id: &Symbol, direction: HostDirection) -> Result<&JackClient> {
97        let Some(client) = self
98            .clients
99            .iter()
100            .find(|candidate| candidate.id() == client_id)
101        else {
102            return Err(Error::Eval(format!(
103                "JACK client {client_id} was not found"
104            )));
105        };
106        if !client.is_compatible_with(direction) {
107            return Err(Error::TypeMismatch {
108                expected: "JACK client with requested audio direction",
109                found: "JACK client with another audio direction",
110            });
111        }
112        Ok(client)
113    }
114}
115
116impl HostBackend for JackBackend {
117    fn info(&self) -> &HostBackendInfo {
118        &self.info
119    }
120
121    fn enumerate(&self) -> Result<HostDeviceInventory> {
122        let devices = self
123            .clients
124            .iter()
125            .map(|client| {
126                Ok(HostDeviceSpec::new(
127                    client.id().clone(),
128                    jack_backend_symbol(),
129                    StreamMedia::Pcm,
130                    client.direction(),
131                    jack_clock_symbol(),
132                    BufferPolicy::bounded(client.timing().block_frames())?,
133                ))
134            })
135            .collect::<Result<Vec<_>>>()?;
136        let ports = self
137            .clients
138            .iter()
139            .flat_map(JackClient::ports)
140            .map(|port| {
141                HostPortSpec::new(
142                    port.id().clone(),
143                    port.client().clone(),
144                    jack_backend_symbol(),
145                    port.media(),
146                    port.direction(),
147                )
148            })
149            .collect();
150        Ok(HostDeviceInventory::new(jack_backend_symbol())
151            .with_devices(devices)
152            .with_ports(ports))
153    }
154
155    fn open(&self, request: HostStreamConfigRequest) -> Result<HostOpenStream> {
156        if request.backend() != self.info.id() {
157            return Err(Error::Eval(format!(
158                "JACK backend cannot open {} requests",
159                request.backend()
160            )));
161        }
162        if request.media() != StreamMedia::Pcm {
163            return Err(Error::TypeMismatch {
164                expected: "PCM stream request",
165                found: "non-PCM stream request",
166            });
167        }
168        let direction = request.direction();
169        let client = self.require_client(request.device(), direction)?;
170        let timing = client.timing();
171        let config = HostStreamConfig::from_request(
172            request,
173            latency_for(direction, timing),
174            HostClockInfo::new(jack_clock_symbol(), Some(timing.sample_rate_hz()), true),
175        );
176        Ok(HostOpenStream::new(config))
177    }
178}
179
180fn request(
181    client: &JackClient,
182    direction: HostDirection,
183    capacity: usize,
184) -> Result<HostStreamConfigRequest> {
185    Ok(HostStreamConfigRequest::new(
186        jack_backend_symbol(),
187        client.id().clone(),
188        StreamMedia::Pcm,
189        direction,
190        BufferPolicy::bounded(capacity)?,
191    ))
192}
193
194fn capabilities_for(clients: &[JackClient], fake: bool) -> Vec<HostBackendCapability> {
195    let mut capabilities = Vec::new();
196    if clients.iter().any(|client| client.audio_inputs() > 0) {
197        capabilities.push(HostBackendCapability::AudioInput);
198    }
199    if clients.iter().any(|client| client.audio_outputs() > 0) {
200        capabilities.push(HostBackendCapability::AudioOutput);
201    }
202    if clients.iter().any(|client| client.midi_inputs() > 0) {
203        capabilities.push(HostBackendCapability::MidiInput);
204    }
205    if clients.iter().any(|client| client.midi_outputs() > 0) {
206        capabilities.push(HostBackendCapability::MidiOutput);
207    }
208    if clients
209        .iter()
210        .any(|client| client.direction() == HostDirection::Duplex)
211    {
212        capabilities.push(HostBackendCapability::Duplex);
213    }
214    capabilities.push(HostBackendCapability::Hotplug);
215    capabilities.push(HostBackendCapability::Reconnect);
216    if fake {
217        capabilities.push(HostBackendCapability::Offline);
218        capabilities.push(HostBackendCapability::Fake);
219    }
220    capabilities
221}
222
223fn latency_for(direction: HostDirection, timing: JackTiming) -> HostLatencyInfo {
224    match direction {
225        HostDirection::Input => HostLatencyInfo::new(timing.input_latency_frames(), 0),
226        HostDirection::Output => HostLatencyInfo::new(0, timing.output_latency_frames()),
227        HostDirection::Duplex => HostLatencyInfo::new(
228            timing.input_latency_frames(),
229            timing.output_latency_frames(),
230        ),
231    }
232}