Skip to main content

sim_lib_stream_host/
rtp_midi.rs

1//! RTP-MIDI host backend and opened port.
2
3use std::sync::Arc;
4
5use sim_kernel::{Cx, Error, Result, Symbol};
6use sim_lib_stream_core::{
7    BufferPolicy, ClockDomain, MidiPacket, StreamEnvelope, StreamItem, StreamMedia, StreamPacket,
8    StreamValue, TransportProfile,
9};
10
11use crate::{
12    HostBackend, HostBackendCapability, HostBackendInfo, HostCallbackQueue, HostClockInfo,
13    HostDeviceInventory, HostDeviceSpec, HostDirection, HostLatencyInfo, HostOpenStream,
14    HostPortSpec, HostStreamConfig, HostStreamConfigRequest,
15};
16
17/// Host backend implementing the selected RTP-MIDI input subset.
18///
19/// Enumerates and opens MIDI sources without opening sockets during normal
20/// validation; host callbacks feed received packets through a bounded queue.
21#[derive(Clone, Debug)]
22pub struct RtpMidiBackend {
23    info: HostBackendInfo,
24}
25
26/// An opened RTP-MIDI source port and its callback queue.
27pub struct RtpMidiPort {
28    spec: HostDeviceSpec,
29    queue: HostCallbackQueue,
30}
31
32/// Returns the stable symbol identifying the RTP-MIDI backend.
33pub fn rtp_midi_backend_symbol() -> Symbol {
34    Symbol::qualified("stream/host", "rtp-midi")
35}
36
37impl Default for RtpMidiBackend {
38    fn default() -> Self {
39        Self::new()
40    }
41}
42
43impl RtpMidiBackend {
44    /// Creates a backend advertising MIDI input and offline enumeration.
45    pub fn new() -> Self {
46        Self {
47            info: HostBackendInfo::new(
48                rtp_midi_backend_symbol(),
49                Symbol::qualified("stream/transport", "rtp-midi"),
50                StreamMedia::Midi,
51                false,
52            )
53            .with_capabilities(vec![
54                HostBackendCapability::MidiInput,
55                HostBackendCapability::Offline,
56            ]),
57        }
58    }
59
60    /// Returns the backend metadata.
61    pub fn info(&self) -> &HostBackendInfo {
62        &self.info
63    }
64
65    /// Builds a MIDI input device spec named `id` with a bounded buffer of
66    /// `capacity` packets.
67    pub fn source_spec(id: impl Into<String>, capacity: usize) -> Result<HostDeviceSpec> {
68        Ok(HostDeviceSpec::new(
69            Symbol::new(id.into()),
70            rtp_midi_backend_symbol(),
71            StreamMedia::Midi,
72            HostDirection::Input,
73            ClockDomain::MidiTick.symbol(),
74            BufferPolicy::bounded(capacity)?,
75        ))
76    }
77
78    /// Opens a MIDI source port for `spec`.
79    ///
80    /// Returns an error when the spec belongs to another backend, is not MIDI
81    /// media, or is output-only.
82    pub fn open_source(&self, cx: &mut Cx, spec: HostDeviceSpec) -> Result<RtpMidiPort> {
83        if spec.backend() != self.info.id() {
84            return Err(Error::Eval(format!(
85                "RTP-MIDI backend cannot open {} device specs",
86                spec.backend()
87            )));
88        }
89        if spec.media() != StreamMedia::Midi {
90            return Err(Error::TypeMismatch {
91                expected: "MIDI host device",
92                found: "non-MIDI host device",
93            });
94        }
95        if spec.direction() == HostDirection::Output {
96            return Err(Error::TypeMismatch {
97                expected: "RTP-MIDI input or duplex device",
98                found: "output-only host device",
99            });
100        }
101        spec.open_plan().enforce(cx)?;
102        let stream = Arc::new(StreamValue::push(spec.metadata()));
103        Ok(RtpMidiPort {
104            spec,
105            queue: HostCallbackQueue::new(stream),
106        })
107    }
108
109    fn device_for_request(&self, request: &HostStreamConfigRequest) -> Result<HostDeviceSpec> {
110        <Self as HostBackend>::enumerate(self)?
111            .devices()
112            .iter()
113            .find(|device| device.id() == request.device())
114            .cloned()
115            .ok_or_else(|| Error::Eval(format!("unknown RTP-MIDI device {}", request.device())))
116    }
117}
118
119impl HostBackend for RtpMidiBackend {
120    fn info(&self) -> &HostBackendInfo {
121        &self.info
122    }
123
124    fn enumerate(&self) -> Result<HostDeviceInventory> {
125        let device = Self::source_spec("rtp-midi/default", 8)?;
126        let port = HostPortSpec::new(
127            Symbol::new("rtp-midi/default/in"),
128            device.id().clone(),
129            rtp_midi_backend_symbol(),
130            StreamMedia::Midi,
131            HostDirection::Input,
132        );
133        Ok(HostDeviceInventory::new(rtp_midi_backend_symbol())
134            .with_devices(vec![device])
135            .with_ports(vec![port]))
136    }
137
138    fn open(&self, request: HostStreamConfigRequest) -> Result<HostOpenStream> {
139        if request.backend() != self.info.id() {
140            return Err(Error::Eval(format!(
141                "RTP-MIDI backend cannot open {} requests",
142                request.backend()
143            )));
144        }
145        let device = self.device_for_request(&request)?;
146        if device.media() != request.media() {
147            return Err(Error::TypeMismatch {
148                expected: "MIDI host stream request",
149                found: "non-MIDI host stream request",
150            });
151        }
152        if device.direction() != request.direction() {
153            return Err(Error::TypeMismatch {
154                expected: "RTP-MIDI input or duplex stream request",
155                found: "request direction for another RTP-MIDI device",
156            });
157        }
158        let clock = HostClockInfo::new(request.clock().clone(), None, true);
159        let config = HostStreamConfig::from_request(request, HostLatencyInfo::default(), clock);
160        HostOpenStream::new_lan_midi_control(config)
161    }
162}
163
164impl RtpMidiPort {
165    /// Returns the device spec backing this port.
166    pub fn spec(&self) -> &HostDeviceSpec {
167        &self.spec
168    }
169
170    /// Returns the callback queue host callbacks enqueue into.
171    pub fn queue(&self) -> &HostCallbackQueue {
172        &self.queue
173    }
174
175    /// Returns a shared handle to the underlying stream value.
176    pub fn stream(&self) -> Arc<StreamValue> {
177        self.queue.stream()
178    }
179
180    /// Enqueues a MIDI packet received by a host callback.
181    pub fn receive_packet_from_callback(&self, packet: MidiPacket) -> Result<()> {
182        self.queue
183            .callback_packet(StreamPacket::Midi(packet))
184            .map(|_| ())
185    }
186
187    /// Enqueues a MIDI packet and returns the stream envelope describing it
188    /// under the LAN MIDI/control transport profile.
189    pub fn receive_envelope_from_callback(
190        &self,
191        sequence: u64,
192        packet: MidiPacket,
193    ) -> Result<StreamEnvelope> {
194        let item = StreamItem::new(StreamPacket::Midi(packet));
195        let envelope = StreamEnvelope::from_item_with_profile(
196            &self.spec.metadata(),
197            sequence,
198            &item,
199            TransportProfile::lan_midi_control(),
200        )?;
201        self.queue.callback_item(item)?;
202        Ok(envelope)
203    }
204
205    /// Closes the port's callback queue.
206    pub fn close(&self) -> Result<()> {
207        self.queue.close()
208    }
209}