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::{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, 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        let stream = Arc::new(StreamValue::push(spec.metadata()));
102        Ok(RtpMidiPort {
103            spec,
104            queue: HostCallbackQueue::new(stream),
105        })
106    }
107}
108
109impl HostBackend for RtpMidiBackend {
110    fn info(&self) -> &HostBackendInfo {
111        &self.info
112    }
113
114    fn enumerate(&self) -> Result<HostDeviceInventory> {
115        let device = Self::source_spec("rtp-midi/default", 8)?;
116        let port = HostPortSpec::new(
117            Symbol::new("rtp-midi/default/in"),
118            device.id().clone(),
119            rtp_midi_backend_symbol(),
120            StreamMedia::Midi,
121            HostDirection::Input,
122        );
123        Ok(HostDeviceInventory::new(rtp_midi_backend_symbol())
124            .with_devices(vec![device])
125            .with_ports(vec![port]))
126    }
127
128    fn open(&self, request: HostStreamConfigRequest) -> Result<HostOpenStream> {
129        if request.backend() != self.info.id() {
130            return Err(Error::Eval(format!(
131                "RTP-MIDI backend cannot open {} requests",
132                request.backend()
133            )));
134        }
135        if request.media() != StreamMedia::Midi {
136            return Err(Error::TypeMismatch {
137                expected: "MIDI host stream request",
138                found: "non-MIDI host stream request",
139            });
140        }
141        if request.direction() == HostDirection::Output {
142            return Err(Error::TypeMismatch {
143                expected: "RTP-MIDI input or duplex stream request",
144                found: "output-only host stream request",
145            });
146        }
147        let clock = HostClockInfo::new(request.clock().clone(), None, true);
148        let config = HostStreamConfig::from_request(request, HostLatencyInfo::default(), clock);
149        HostOpenStream::new_lan_midi_control(config)
150    }
151}
152
153impl RtpMidiPort {
154    /// Returns the device spec backing this port.
155    pub fn spec(&self) -> &HostDeviceSpec {
156        &self.spec
157    }
158
159    /// Returns the callback queue host callbacks enqueue into.
160    pub fn queue(&self) -> &HostCallbackQueue {
161        &self.queue
162    }
163
164    /// Returns a shared handle to the underlying stream value.
165    pub fn stream(&self) -> Arc<StreamValue> {
166        self.queue.stream()
167    }
168
169    /// Enqueues a MIDI packet received by a host callback.
170    pub fn receive_packet_from_callback(&self, packet: MidiPacket) -> Result<()> {
171        self.queue
172            .callback_packet(StreamPacket::Midi(packet))
173            .map(|_| ())
174    }
175
176    /// Enqueues a MIDI packet and returns the stream envelope describing it
177    /// under the LAN MIDI/control transport profile.
178    pub fn receive_envelope_from_callback(
179        &self,
180        sequence: u64,
181        packet: MidiPacket,
182    ) -> Result<StreamEnvelope> {
183        let item = StreamItem::new(StreamPacket::Midi(packet));
184        let envelope = StreamEnvelope::from_item_with_profile(
185            &self.spec.metadata(),
186            sequence,
187            &item,
188            TransportProfile::lan_midi_control(),
189        )?;
190        self.queue.callback_item(item)?;
191        Ok(envelope)
192    }
193
194    /// Closes the port's callback queue.
195    pub fn close(&self) -> Result<()> {
196        self.queue.close()
197    }
198}