1use 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#[derive(Clone, Debug, PartialEq, Eq, Hash)]
16pub struct AudioSiteKey(pub Symbol);
17
18impl AudioSiteKey {
19 pub fn new(name: &str) -> Self {
21 Self(Symbol::new(name))
22 }
23}
24
25#[derive(Clone, Debug, PartialEq, Eq)]
29pub struct AudioDeviceCard {
30 pub key: AudioSiteKey,
32 pub display_name: String,
34 pub channels_out: u16,
36 pub channels_in: u16,
38 pub sample_rates: Vec<u32>,
40 pub hardware_required: bool,
42}
43
44impl AudioDeviceCard {
45 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#[derive(Clone, Debug, PartialEq, Eq)]
60pub struct AudioPlacementRequest {
61 pub site_key: AudioSiteKey,
63 pub stream_request: crate::HostStreamConfigRequest,
65}
66
67#[derive(Clone, Debug, PartialEq, Eq)]
69pub enum Placement {
70 Modeled,
72 Hardware {
74 transport: Symbol,
76 },
77}
78
79#[derive(Clone, Copy, Debug, PartialEq, Eq)]
81pub enum DeviceKind {
82 Audio,
84 Midi,
86}
87
88#[derive(Clone, Copy, Debug, PartialEq, Eq)]
90pub enum DeviceDirection {
91 Input,
93 Output,
95 Duplex,
97}
98
99#[derive(Clone, Debug, PartialEq, Eq)]
101pub struct DeviceRecord {
102 pub id: Symbol,
104 pub display_name: String,
106 pub kind: DeviceKind,
108 pub direction: DeviceDirection,
110 pub placement: Placement,
112}
113
114impl DeviceRecord {
115 pub fn modeled_midi_input(id: &str, name: impl Into<String>) -> Self {
117 Self::modeled(id, name, DeviceKind::Midi, DeviceDirection::Input)
118 }
119
120 pub fn modeled_midi_output(id: &str, name: impl Into<String>) -> Self {
122 Self::modeled(id, name, DeviceKind::Midi, DeviceDirection::Output)
123 }
124
125 pub fn modeled_audio_output(id: &str, name: impl Into<String>) -> Self {
127 Self::modeled(id, name, DeviceKind::Audio, DeviceDirection::Output)
128 }
129
130 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
162pub fn lan_peer_site_symbol() -> Symbol {
164 Symbol::qualified("stream/site", "lan-peer")
165}
166
167pub fn lan_jitter_buffered_mode_symbol() -> Symbol {
169 Symbol::qualified("stream/lan-mode", "jitter-buffered")
170}
171
172pub fn lan_bar_delay_mode_symbol() -> Symbol {
174 Symbol::qualified("stream/lan-mode", "collab-bardelay")
175}
176
177pub fn lan_pinned_sample_refusal_diagnostic() -> Symbol {
179 Symbol::qualified("stream/lan-diagnostic", "pinned-sample-remote-refused")
180}
181
182pub fn lan_pinned_sample_experimental_diagnostic() -> Symbol {
184 Symbol::qualified("stream/lan-diagnostic", "pinned-sample-remote-experimental")
185}
186
187pub fn lan_experimental_remote_sample_capability() -> CapabilityName {
189 CapabilityName::new("stream.lan.experimental-remote-sample")
190}
191
192#[derive(Clone, Copy, Debug, PartialEq, Eq)]
198pub enum LanPlacementMode {
199 JitterBuffered {
201 jitter_packets: u32,
203 latency_comp_frames: u64,
205 },
206 BarDelay {
208 bars: u32,
210 beats_per_bar: u32,
212 tempo_bpm: u32,
214 jitter_packets: u32,
216 latency_comp_frames: u64,
218 },
219}
220
221impl LanPlacementMode {
222 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 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 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 pub fn latency_class(self) -> LatencyClass {
292 match self {
293 Self::JitterBuffered { .. } => LatencyClass::BufferedPreview,
294 Self::BarDelay { .. } => LatencyClass::CollabBarDelay,
295 }
296 }
297
298 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 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 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 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#[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 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 pub fn with_realtime_pin(mut self, realtime_pinned: bool) -> Self {
388 self.realtime_pinned = realtime_pinned;
389 self
390 }
391
392 pub fn with_capability(mut self, capability: CapabilityName) -> Self {
394 self.capabilities.push(capability);
395 self
396 }
397
398 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#[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 pub fn fragment_id(&self) -> &Symbol {
459 &self.fragment_id
460 }
461
462 pub fn site(&self) -> &Symbol {
464 &self.site
465 }
466
467 pub fn mode(&self) -> LanPlacementMode {
469 self.mode
470 }
471
472 pub fn latency_class(&self) -> LatencyClass {
474 self.mode.latency_class()
475 }
476
477 pub fn bridges(&self) -> &[DomainBridgeDescriptor] {
479 &self.bridges
480 }
481
482 pub fn output_envelopes(&self) -> &[StreamEnvelope] {
484 &self.output_envelopes
485 }
486
487 pub fn diagnostics(&self) -> &[Symbol] {
489 &self.diagnostics
490 }
491
492 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 pub fn bar_delay_millis(&self) -> Option<u64> {
503 self.mode.bar_delay_millis()
504 }
505
506 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}