Skip to main content

sim_lib_stream_host/
placement.rs

1//! Host stream placement vocabulary and LAN peer placement policy.
2
3use sim_kernel::{CapabilityName, Error, Expr, Result, Symbol};
4use sim_lib_stream_core::{
5    BridgeLatency, ClockDomain, DomainBridgeDescriptor, LatencyClass, PlacedFragment,
6    StreamCapability, StreamEnvelope, TransportProfile,
7};
8
9const DEFAULT_BEATS_PER_BAR: u32 = 4;
10
11/// Stable runtime key for an audio evaluation site.
12///
13/// The key is plain data so device catalogs can store and compare audio sites
14/// without carrying platform handles.
15#[derive(Clone, Debug, PartialEq, Eq, Hash)]
16pub struct AudioSiteKey(pub Symbol);
17
18impl AudioSiteKey {
19    /// Builds an audio site key from a stable symbolic name.
20    pub fn new(name: &str) -> Self {
21        Self(Symbol::new(name))
22    }
23}
24
25/// Export-record-style descriptor for an audio device.
26///
27/// The card carries only stable metadata and never owns platform handles.
28#[derive(Clone, Debug, PartialEq, Eq)]
29pub struct AudioDeviceCard {
30    /// Stable site key used for registration and lookup.
31    pub key: AudioSiteKey,
32    /// Human-facing device name.
33    pub display_name: String,
34    /// Number of output channels supported by this site.
35    pub channels_out: u16,
36    /// Number of input channels supported by this site.
37    pub channels_in: u16,
38    /// Advertised sample rates in hertz.
39    pub sample_rates: Vec<u32>,
40    /// Whether opening this site requires real host hardware.
41    pub hardware_required: bool,
42}
43
44impl AudioDeviceCard {
45    /// Builds a deterministic modeled stereo device card for validation.
46    pub fn modeled(key: AudioSiteKey, name: impl Into<String>) -> Self {
47        Self {
48            key,
49            display_name: name.into(),
50            channels_out: 2,
51            channels_in: 2,
52            sample_rates: vec![44_100, 48_000],
53            hardware_required: false,
54        }
55    }
56}
57
58/// Caller-side request to place an audio graph on a registered audio site.
59#[derive(Clone, Debug, PartialEq, Eq)]
60pub struct AudioPlacementRequest {
61    /// Target site key selected by placement.
62    pub site_key: AudioSiteKey,
63    /// Host stream request forwarded to the selected backend.
64    pub stream_request: crate::HostStreamConfigRequest,
65}
66
67/// Placement tier for a host-visible stream device.
68#[derive(Clone, Debug, PartialEq, Eq)]
69pub enum Placement {
70    /// Deterministic in-process fixture that needs no host hardware.
71    Modeled,
72    /// Real host hardware behind the named transport.
73    Hardware {
74        /// Transport responsible for opening the hardware device.
75        transport: Symbol,
76    },
77}
78
79/// Stream-device media family surfaced by the shared device catalog.
80#[derive(Clone, Copy, Debug, PartialEq, Eq)]
81pub enum DeviceKind {
82    /// Audio PCM device.
83    Audio,
84    /// MIDI event device.
85    Midi,
86}
87
88/// Direction supported by a cataloged stream device.
89#[derive(Clone, Copy, Debug, PartialEq, Eq)]
90pub enum DeviceDirection {
91    /// Device produces stream input.
92    Input,
93    /// Device consumes stream output.
94    Output,
95    /// Device supports input and output.
96    Duplex,
97}
98
99/// Catalog row for a host-visible stream device.
100#[derive(Clone, Debug, PartialEq, Eq)]
101pub struct DeviceRecord {
102    /// Stable catalog identifier.
103    pub id: Symbol,
104    /// Human-facing device label.
105    pub display_name: String,
106    /// Device media family.
107    pub kind: DeviceKind,
108    /// Device stream direction.
109    pub direction: DeviceDirection,
110    /// Placement tier for the device.
111    pub placement: Placement,
112}
113
114impl DeviceRecord {
115    /// Builds a modeled MIDI input record.
116    pub fn modeled_midi_input(id: &str, name: impl Into<String>) -> Self {
117        Self::modeled(id, name, DeviceKind::Midi, DeviceDirection::Input)
118    }
119
120    /// Builds a modeled MIDI output record.
121    pub fn modeled_midi_output(id: &str, name: impl Into<String>) -> Self {
122        Self::modeled(id, name, DeviceKind::Midi, DeviceDirection::Output)
123    }
124
125    /// Builds a modeled audio output record.
126    pub fn modeled_audio_output(id: &str, name: impl Into<String>) -> Self {
127        Self::modeled(id, name, DeviceKind::Audio, DeviceDirection::Output)
128    }
129
130    /// Builds a modeled audio record from an audio device card.
131    pub fn modeled_audio_from_card(card: &AudioDeviceCard) -> Self {
132        let direction = match (card.channels_in > 0, card.channels_out > 0) {
133            (true, true) => DeviceDirection::Duplex,
134            (true, false) => DeviceDirection::Input,
135            (false, true) | (false, false) => DeviceDirection::Output,
136        };
137        Self {
138            id: card.key.0.clone(),
139            display_name: card.display_name.clone(),
140            kind: DeviceKind::Audio,
141            direction,
142            placement: Placement::Modeled,
143        }
144    }
145
146    fn modeled(
147        id: &str,
148        name: impl Into<String>,
149        kind: DeviceKind,
150        direction: DeviceDirection,
151    ) -> Self {
152        Self {
153            id: Symbol::new(id),
154            display_name: name.into(),
155            kind,
156            direction,
157            placement: Placement::Modeled,
158        }
159    }
160}
161
162/// Stable site name for a non-real-time node hosted by a LAN peer.
163pub fn lan_peer_site_symbol() -> Symbol {
164    Symbol::qualified("stream/site", "lan-peer")
165}
166
167/// Stable mode name for jitter-buffered LAN placement.
168pub fn lan_jitter_buffered_mode_symbol() -> Symbol {
169    Symbol::qualified("stream/lan-mode", "jitter-buffered")
170}
171
172/// Stable mode name for musically aligned bar-delay collaboration.
173pub fn lan_bar_delay_mode_symbol() -> Symbol {
174    Symbol::qualified("stream/lan-mode", "collab-bardelay")
175}
176
177/// Diagnostic emitted when a pinned sample-domain node is refused across LAN.
178pub fn lan_pinned_sample_refusal_diagnostic() -> Symbol {
179    Symbol::qualified("stream/lan-diagnostic", "pinned-sample-remote-refused")
180}
181
182/// Diagnostic emitted when experimental pinned sample-domain LAN placement is used.
183pub fn lan_pinned_sample_experimental_diagnostic() -> Symbol {
184    Symbol::qualified("stream/lan-diagnostic", "pinned-sample-remote-experimental")
185}
186
187/// Capability required to try pinned sample-domain placement across LAN.
188pub fn lan_experimental_remote_sample_capability() -> CapabilityName {
189    CapabilityName::new("stream.lan.experimental-remote-sample")
190}
191
192/// Placement mode for a stream fragment hosted on a LAN peer.
193///
194/// Selects how packets crossing the LAN are buffered and time-aligned: a plain
195/// jitter buffer for buffered preview, or a musically aligned bar delay for
196/// collaborative play.
197#[derive(Clone, Copy, Debug, PartialEq, Eq)]
198pub enum LanPlacementMode {
199    /// Jitter-buffered preview placement.
200    JitterBuffered {
201        /// Packets retained in the jitter buffer.
202        jitter_packets: u32,
203        /// Latency-compensation delay applied, in frames.
204        latency_comp_frames: u64,
205    },
206    /// Musically aligned collaborative placement delayed by whole bars.
207    BarDelay {
208        /// Number of bars of alignment delay.
209        bars: u32,
210        /// Beats per bar used to size the bar delay.
211        beats_per_bar: u32,
212        /// Tempo in beats per minute used to size the bar delay.
213        tempo_bpm: u32,
214        /// Packets retained in the jitter buffer.
215        jitter_packets: u32,
216        /// Latency-compensation delay applied, in frames.
217        latency_comp_frames: u64,
218    },
219}
220
221impl LanPlacementMode {
222    /// Builds a jitter-buffered mode retaining `jitter_packets` packets.
223    ///
224    /// Returns an evaluation error when `jitter_packets` is zero.
225    ///
226    /// # Examples
227    ///
228    /// ```
229    /// use sim_lib_stream_host::LanPlacementMode;
230    ///
231    /// let mode = LanPlacementMode::jitter_buffered(4, 128).unwrap();
232    /// assert_eq!(mode.jitter_packets(), 4);
233    /// assert_eq!(mode.latency_comp_frames(), 128);
234    /// assert!(mode.bar_delay_millis().is_none());
235    /// ```
236    pub fn jitter_buffered(jitter_packets: u32, latency_comp_frames: u64) -> Result<Self> {
237        if jitter_packets == 0 {
238            return Err(Error::Eval(
239                "LAN jitter buffer must retain at least one packet".to_owned(),
240            ));
241        }
242        Ok(Self::JitterBuffered {
243            jitter_packets,
244            latency_comp_frames,
245        })
246    }
247
248    /// Builds a bar-delay mode delaying `bars` bars at `tempo_bpm`.
249    ///
250    /// Uses a default of four beats per bar. Returns an evaluation error when
251    /// `bars`, `tempo_bpm`, or `jitter_packets` is zero.
252    pub fn bar_delay(
253        bars: u32,
254        tempo_bpm: u32,
255        jitter_packets: u32,
256        latency_comp_frames: u64,
257    ) -> Result<Self> {
258        if bars == 0 {
259            return Err(Error::Eval(
260                "LAN bar-delay mode must delay at least one bar".to_owned(),
261            ));
262        }
263        if tempo_bpm == 0 {
264            return Err(Error::Eval(
265                "LAN bar-delay mode tempo must be greater than zero".to_owned(),
266            ));
267        }
268        if jitter_packets == 0 {
269            return Err(Error::Eval(
270                "LAN jitter buffer must retain at least one packet".to_owned(),
271            ));
272        }
273        Ok(Self::BarDelay {
274            bars,
275            beats_per_bar: DEFAULT_BEATS_PER_BAR,
276            tempo_bpm,
277            jitter_packets,
278            latency_comp_frames,
279        })
280    }
281
282    /// Returns the stable mode symbol.
283    pub fn symbol(self) -> Symbol {
284        match self {
285            Self::JitterBuffered { .. } => lan_jitter_buffered_mode_symbol(),
286            Self::BarDelay { .. } => lan_bar_delay_mode_symbol(),
287        }
288    }
289
290    /// Returns the latency class this mode places the fragment into.
291    pub fn latency_class(self) -> LatencyClass {
292        match self {
293            Self::JitterBuffered { .. } => LatencyClass::BufferedPreview,
294            Self::BarDelay { .. } => LatencyClass::CollabBarDelay,
295        }
296    }
297
298    /// Returns the number of packets retained in the jitter buffer.
299    pub fn jitter_packets(self) -> u32 {
300        match self {
301            Self::JitterBuffered { jitter_packets, .. } | Self::BarDelay { jitter_packets, .. } => {
302                jitter_packets
303            }
304        }
305    }
306
307    /// Returns the latency-compensation delay in frames.
308    pub fn latency_comp_frames(self) -> u64 {
309        match self {
310            Self::JitterBuffered {
311                latency_comp_frames,
312                ..
313            }
314            | Self::BarDelay {
315                latency_comp_frames,
316                ..
317            } => latency_comp_frames,
318        }
319    }
320
321    /// Returns the bar-delay length in milliseconds, or `None` for
322    /// jitter-buffered placement.
323    pub fn bar_delay_millis(self) -> Option<u64> {
324        match self {
325            Self::JitterBuffered { .. } => None,
326            Self::BarDelay {
327                bars,
328                beats_per_bar,
329                tempo_bpm,
330                ..
331            } => Some(
332                u64::from(bars)
333                    .saturating_mul(u64::from(beats_per_bar))
334                    .saturating_mul(60_000)
335                    / u64::from(tempo_bpm),
336            ),
337        }
338    }
339
340    /// Returns the transport profile advertised for this mode.
341    pub fn transport_profile(self) -> Result<TransportProfile> {
342        match self {
343            Self::JitterBuffered { .. } => Ok(TransportProfile::lan_buffered_audio_preview()),
344            Self::BarDelay { .. } => TransportProfile::new(
345                Symbol::qualified("stream/profile", "lan-collab-bardelay"),
346                LatencyClass::CollabBarDelay,
347                vec![
348                    StreamCapability::Remote,
349                    StreamCapability::Bounded,
350                    StreamCapability::Preview,
351                    StreamCapability::Lossy,
352                ],
353            ),
354        }
355    }
356
357    fn bridges(self) -> Vec<DomainBridgeDescriptor> {
358        vec![
359            DomainBridgeDescriptor::jitter_buffer(self.jitter_packets()),
360            DomainBridgeDescriptor::latency_comp_delay(self.latency_comp_frames()),
361        ]
362    }
363}
364
365/// Request to place a stream fragment on a LAN peer under a chosen mode.
366#[derive(Clone, Debug, PartialEq, Eq)]
367pub struct LanPlacementRequest {
368    fragment: PlacedFragment,
369    mode: LanPlacementMode,
370    realtime_pinned: bool,
371    capabilities: Vec<CapabilityName>,
372}
373
374impl LanPlacementRequest {
375    /// Builds a request to place `fragment` using `mode`, unpinned and with no
376    /// extra capabilities.
377    pub fn new(fragment: PlacedFragment, mode: LanPlacementMode) -> Self {
378        Self {
379            fragment,
380            mode,
381            realtime_pinned: false,
382            capabilities: Vec::new(),
383        }
384    }
385
386    /// Marks whether the fragment is pinned to realtime (sample-locked) play.
387    pub fn with_realtime_pin(mut self, realtime_pinned: bool) -> Self {
388        self.realtime_pinned = realtime_pinned;
389        self
390    }
391
392    /// Grants an additional capability to the request.
393    pub fn with_capability(mut self, capability: CapabilityName) -> Self {
394        self.capabilities.push(capability);
395        self
396    }
397
398    /// Plans the placement, returning a report or an evaluation error.
399    ///
400    /// Refuses a realtime-pinned sample-domain fragment across the LAN unless
401    /// the experimental remote-sample capability is granted, in which case it
402    /// proceeds and records an experimental diagnostic.
403    pub fn plan(&self) -> Result<LanPlacementReport> {
404        let experimental = self
405            .capabilities
406            .contains(&lan_experimental_remote_sample_capability());
407        if self.realtime_pinned && self.fragment_has_sample_domain() && !experimental {
408            let diagnostic = lan_pinned_sample_refusal_diagnostic();
409            return Err(Error::Eval(format!(
410                "{}: pinned sample-domain nodes cannot be sample-locked across LAN",
411                diagnostic.as_qualified_str()
412            )));
413        }
414
415        let mut diagnostics = Vec::new();
416        if self.realtime_pinned && self.fragment_has_sample_domain() {
417            diagnostics.push(lan_pinned_sample_experimental_diagnostic());
418        }
419
420        let profile = self.mode.transport_profile()?;
421        let output_envelopes =
422            remote_output_envelopes(&self.fragment.output_envelopes(), &profile, &diagnostics)?;
423        Ok(LanPlacementReport {
424            fragment_id: self.fragment.id().clone(),
425            site: lan_peer_site_symbol(),
426            mode: self.mode,
427            bridges: self.mode.bridges(),
428            output_envelopes,
429            diagnostics,
430        })
431    }
432
433    fn fragment_has_sample_domain(&self) -> bool {
434        self.fragment
435            .input_edges()
436            .iter()
437            .chain(self.fragment.output_edges())
438            .any(|edge| edge.rate_contract().clock_domain() == ClockDomain::Sample)
439    }
440}
441
442/// Outcome of planning a LAN fragment placement.
443///
444/// Records the placement site, mode, the domain bridges inserted, the rewritten
445/// output envelopes carrying the remote transport profile, and any diagnostics.
446#[derive(Clone, Debug, PartialEq, Eq)]
447pub struct LanPlacementReport {
448    fragment_id: Symbol,
449    site: Symbol,
450    mode: LanPlacementMode,
451    bridges: Vec<DomainBridgeDescriptor>,
452    output_envelopes: Vec<StreamEnvelope>,
453    diagnostics: Vec<Symbol>,
454}
455
456impl LanPlacementReport {
457    /// Returns the placed fragment identifier.
458    pub fn fragment_id(&self) -> &Symbol {
459        &self.fragment_id
460    }
461
462    /// Returns the placement site symbol.
463    pub fn site(&self) -> &Symbol {
464        &self.site
465    }
466
467    /// Returns the placement mode.
468    pub fn mode(&self) -> LanPlacementMode {
469        self.mode
470    }
471
472    /// Returns the latency class of the placement.
473    pub fn latency_class(&self) -> LatencyClass {
474        self.mode.latency_class()
475    }
476
477    /// Returns the domain bridges inserted by the placement.
478    pub fn bridges(&self) -> &[DomainBridgeDescriptor] {
479        &self.bridges
480    }
481
482    /// Returns the rewritten output envelopes.
483    pub fn output_envelopes(&self) -> &[StreamEnvelope] {
484        &self.output_envelopes
485    }
486
487    /// Returns the diagnostics recorded during planning.
488    pub fn diagnostics(&self) -> &[Symbol] {
489        &self.diagnostics
490    }
491
492    /// Returns the total latency added by the inserted bridges.
493    pub fn added_bridge_latency(&self) -> BridgeLatency {
494        self.bridges
495            .iter()
496            .fold(BridgeLatency::zero(), |latency, bridge| {
497                latency.plus(bridge.latency())
498            })
499    }
500
501    /// Returns the bar-delay length in milliseconds when the mode uses one.
502    pub fn bar_delay_millis(&self) -> Option<u64> {
503        self.mode.bar_delay_millis()
504    }
505
506    /// Builds a browse/inspection expression summarizing the placement.
507    pub fn to_expr(&self) -> Expr {
508        let latency = self.added_bridge_latency();
509        Expr::Map(vec![
510            (
511                Expr::Symbol(Symbol::new("fragment")),
512                Expr::Symbol(self.fragment_id.clone()),
513            ),
514            (
515                Expr::Symbol(Symbol::new("site")),
516                Expr::Symbol(self.site.clone()),
517            ),
518            (
519                Expr::Symbol(Symbol::new("mode")),
520                Expr::Symbol(self.mode.symbol()),
521            ),
522            (
523                Expr::Symbol(Symbol::new("latency-class")),
524                Expr::Symbol(self.latency_class().symbol()),
525            ),
526            (
527                Expr::Symbol(Symbol::new("bar-delay-ms")),
528                Expr::String(self.bar_delay_millis().unwrap_or(0).to_string()),
529            ),
530            (
531                Expr::Symbol(Symbol::new("bridge-latency-frames")),
532                Expr::String(latency.frame_count().to_string()),
533            ),
534            (
535                Expr::Symbol(Symbol::new("bridge-latency-packets")),
536                Expr::String(latency.packet_count().to_string()),
537            ),
538            (
539                Expr::Symbol(Symbol::new("bridges")),
540                Expr::List(
541                    self.bridges
542                        .iter()
543                        .map(|bridge| Expr::Symbol(bridge.kind().symbol()))
544                        .collect(),
545                ),
546            ),
547            (
548                Expr::Symbol(Symbol::new("diagnostics")),
549                Expr::List(self.diagnostics.iter().cloned().map(Expr::Symbol).collect()),
550            ),
551            (
552                Expr::Symbol(Symbol::new("output-profiles")),
553                Expr::List(
554                    self.output_envelopes
555                        .iter()
556                        .map(|envelope| Expr::Symbol(envelope.profile().name().clone()))
557                        .collect(),
558                ),
559            ),
560        ])
561    }
562}
563
564fn remote_output_envelopes(
565    envelopes: &[StreamEnvelope],
566    profile: &TransportProfile,
567    diagnostics: &[Symbol],
568) -> Result<Vec<StreamEnvelope>> {
569    envelopes
570        .iter()
571        .map(|envelope| {
572            let mut envelope_diagnostics = envelope.diagnostics().to_vec();
573            envelope_diagnostics.extend(diagnostics.iter().cloned());
574            StreamEnvelope::new_with_clock_domains(
575                envelope.stream_id().clone(),
576                envelope.packet_id().clone(),
577                envelope.media(),
578                envelope.direction(),
579                envelope.sequence(),
580                envelope.ticks().to_vec(),
581                envelope.clock_domain(),
582                envelope.clock_domains().to_vec(),
583                profile.clone(),
584                envelope_diagnostics,
585                envelope.packet().clone(),
586            )
587        })
588        .collect()
589}