use sim_kernel::{CapabilityName, Error, Expr, Result, Symbol};
use sim_lib_stream_core::{
BridgeLatency, ClockDomain, DomainBridgeDescriptor, LatencyClass, PlacedFragment,
StreamCapability, StreamEnvelope, TransportProfile,
};
const DEFAULT_BEATS_PER_BAR: u32 = 4;
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct AudioSiteKey(pub Symbol);
impl AudioSiteKey {
pub fn new(name: &str) -> Self {
Self(Symbol::new(name))
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AudioDeviceCard {
pub key: AudioSiteKey,
pub display_name: String,
pub channels_out: u16,
pub channels_in: u16,
pub sample_rates: Vec<u32>,
pub hardware_required: bool,
}
impl AudioDeviceCard {
pub fn modeled(key: AudioSiteKey, name: impl Into<String>) -> Self {
Self {
key,
display_name: name.into(),
channels_out: 2,
channels_in: 2,
sample_rates: vec![44_100, 48_000],
hardware_required: false,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AudioPlacementRequest {
pub site_key: AudioSiteKey,
pub stream_request: crate::HostStreamConfigRequest,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Placement {
Modeled,
Hardware {
transport: Symbol,
},
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DeviceKind {
Audio,
Midi,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DeviceDirection {
Input,
Output,
Duplex,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DeviceRecord {
pub id: Symbol,
pub display_name: String,
pub kind: DeviceKind,
pub direction: DeviceDirection,
pub placement: Placement,
}
impl DeviceRecord {
pub fn modeled_midi_input(id: &str, name: impl Into<String>) -> Self {
Self::modeled(id, name, DeviceKind::Midi, DeviceDirection::Input)
}
pub fn modeled_midi_output(id: &str, name: impl Into<String>) -> Self {
Self::modeled(id, name, DeviceKind::Midi, DeviceDirection::Output)
}
pub fn modeled_audio_output(id: &str, name: impl Into<String>) -> Self {
Self::modeled(id, name, DeviceKind::Audio, DeviceDirection::Output)
}
pub fn modeled_audio_from_card(card: &AudioDeviceCard) -> Self {
let direction = match (card.channels_in > 0, card.channels_out > 0) {
(true, true) => DeviceDirection::Duplex,
(true, false) => DeviceDirection::Input,
(false, true) | (false, false) => DeviceDirection::Output,
};
Self {
id: card.key.0.clone(),
display_name: card.display_name.clone(),
kind: DeviceKind::Audio,
direction,
placement: Placement::Modeled,
}
}
fn modeled(
id: &str,
name: impl Into<String>,
kind: DeviceKind,
direction: DeviceDirection,
) -> Self {
Self {
id: Symbol::new(id),
display_name: name.into(),
kind,
direction,
placement: Placement::Modeled,
}
}
}
pub fn lan_peer_site_symbol() -> Symbol {
Symbol::qualified("stream/site", "lan-peer")
}
pub fn lan_jitter_buffered_mode_symbol() -> Symbol {
Symbol::qualified("stream/lan-mode", "jitter-buffered")
}
pub fn lan_bar_delay_mode_symbol() -> Symbol {
Symbol::qualified("stream/lan-mode", "collab-bardelay")
}
pub fn lan_pinned_sample_refusal_diagnostic() -> Symbol {
Symbol::qualified("stream/lan-diagnostic", "pinned-sample-remote-refused")
}
pub fn lan_pinned_sample_experimental_diagnostic() -> Symbol {
Symbol::qualified("stream/lan-diagnostic", "pinned-sample-remote-experimental")
}
pub fn lan_experimental_remote_sample_capability() -> CapabilityName {
CapabilityName::new("stream.lan.experimental-remote-sample")
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum LanPlacementMode {
JitterBuffered {
jitter_packets: u32,
latency_comp_frames: u64,
},
BarDelay {
bars: u32,
beats_per_bar: u32,
tempo_bpm: u32,
jitter_packets: u32,
latency_comp_frames: u64,
},
}
impl LanPlacementMode {
pub fn jitter_buffered(jitter_packets: u32, latency_comp_frames: u64) -> Result<Self> {
if jitter_packets == 0 {
return Err(Error::Eval(
"LAN jitter buffer must retain at least one packet".to_owned(),
));
}
Ok(Self::JitterBuffered {
jitter_packets,
latency_comp_frames,
})
}
pub fn bar_delay(
bars: u32,
tempo_bpm: u32,
jitter_packets: u32,
latency_comp_frames: u64,
) -> Result<Self> {
if bars == 0 {
return Err(Error::Eval(
"LAN bar-delay mode must delay at least one bar".to_owned(),
));
}
if tempo_bpm == 0 {
return Err(Error::Eval(
"LAN bar-delay mode tempo must be greater than zero".to_owned(),
));
}
if jitter_packets == 0 {
return Err(Error::Eval(
"LAN jitter buffer must retain at least one packet".to_owned(),
));
}
Ok(Self::BarDelay {
bars,
beats_per_bar: DEFAULT_BEATS_PER_BAR,
tempo_bpm,
jitter_packets,
latency_comp_frames,
})
}
pub fn symbol(self) -> Symbol {
match self {
Self::JitterBuffered { .. } => lan_jitter_buffered_mode_symbol(),
Self::BarDelay { .. } => lan_bar_delay_mode_symbol(),
}
}
pub fn latency_class(self) -> LatencyClass {
match self {
Self::JitterBuffered { .. } => LatencyClass::BufferedPreview,
Self::BarDelay { .. } => LatencyClass::CollabBarDelay,
}
}
pub fn jitter_packets(self) -> u32 {
match self {
Self::JitterBuffered { jitter_packets, .. } | Self::BarDelay { jitter_packets, .. } => {
jitter_packets
}
}
}
pub fn latency_comp_frames(self) -> u64 {
match self {
Self::JitterBuffered {
latency_comp_frames,
..
}
| Self::BarDelay {
latency_comp_frames,
..
} => latency_comp_frames,
}
}
pub fn bar_delay_millis(self) -> Option<u64> {
match self {
Self::JitterBuffered { .. } => None,
Self::BarDelay {
bars,
beats_per_bar,
tempo_bpm,
..
} => Some(
u64::from(bars)
.saturating_mul(u64::from(beats_per_bar))
.saturating_mul(60_000)
/ u64::from(tempo_bpm),
),
}
}
pub fn transport_profile(self) -> Result<TransportProfile> {
match self {
Self::JitterBuffered { .. } => Ok(TransportProfile::lan_buffered_audio_preview()),
Self::BarDelay { .. } => TransportProfile::new(
Symbol::qualified("stream/profile", "lan-collab-bardelay"),
LatencyClass::CollabBarDelay,
vec![
StreamCapability::Remote,
StreamCapability::Bounded,
StreamCapability::Preview,
StreamCapability::Lossy,
],
),
}
}
fn bridges(self) -> Vec<DomainBridgeDescriptor> {
vec![
DomainBridgeDescriptor::jitter_buffer(self.jitter_packets()),
DomainBridgeDescriptor::latency_comp_delay(self.latency_comp_frames()),
]
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct LanPlacementRequest {
fragment: PlacedFragment,
mode: LanPlacementMode,
realtime_pinned: bool,
capabilities: Vec<CapabilityName>,
}
impl LanPlacementRequest {
pub fn new(fragment: PlacedFragment, mode: LanPlacementMode) -> Self {
Self {
fragment,
mode,
realtime_pinned: false,
capabilities: Vec::new(),
}
}
pub fn with_realtime_pin(mut self, realtime_pinned: bool) -> Self {
self.realtime_pinned = realtime_pinned;
self
}
pub fn with_capability(mut self, capability: CapabilityName) -> Self {
self.capabilities.push(capability);
self
}
pub fn plan(&self) -> Result<LanPlacementReport> {
let experimental = self
.capabilities
.contains(&lan_experimental_remote_sample_capability());
if self.realtime_pinned && self.fragment_has_sample_domain() && !experimental {
let diagnostic = lan_pinned_sample_refusal_diagnostic();
return Err(Error::Eval(format!(
"{}: pinned sample-domain nodes cannot be sample-locked across LAN",
diagnostic.as_qualified_str()
)));
}
let mut diagnostics = Vec::new();
if self.realtime_pinned && self.fragment_has_sample_domain() {
diagnostics.push(lan_pinned_sample_experimental_diagnostic());
}
let profile = self.mode.transport_profile()?;
let output_envelopes =
remote_output_envelopes(&self.fragment.output_envelopes(), &profile, &diagnostics)?;
Ok(LanPlacementReport {
fragment_id: self.fragment.id().clone(),
site: lan_peer_site_symbol(),
mode: self.mode,
bridges: self.mode.bridges(),
output_envelopes,
diagnostics,
})
}
fn fragment_has_sample_domain(&self) -> bool {
self.fragment
.input_edges()
.iter()
.chain(self.fragment.output_edges())
.any(|edge| edge.rate_contract().clock_domain() == ClockDomain::Sample)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct LanPlacementReport {
fragment_id: Symbol,
site: Symbol,
mode: LanPlacementMode,
bridges: Vec<DomainBridgeDescriptor>,
output_envelopes: Vec<StreamEnvelope>,
diagnostics: Vec<Symbol>,
}
impl LanPlacementReport {
pub fn fragment_id(&self) -> &Symbol {
&self.fragment_id
}
pub fn site(&self) -> &Symbol {
&self.site
}
pub fn mode(&self) -> LanPlacementMode {
self.mode
}
pub fn latency_class(&self) -> LatencyClass {
self.mode.latency_class()
}
pub fn bridges(&self) -> &[DomainBridgeDescriptor] {
&self.bridges
}
pub fn output_envelopes(&self) -> &[StreamEnvelope] {
&self.output_envelopes
}
pub fn diagnostics(&self) -> &[Symbol] {
&self.diagnostics
}
pub fn added_bridge_latency(&self) -> BridgeLatency {
self.bridges
.iter()
.fold(BridgeLatency::zero(), |latency, bridge| {
latency.plus(bridge.latency())
})
}
pub fn bar_delay_millis(&self) -> Option<u64> {
self.mode.bar_delay_millis()
}
pub fn to_expr(&self) -> Expr {
let latency = self.added_bridge_latency();
Expr::Map(vec![
(
Expr::Symbol(Symbol::new("fragment")),
Expr::Symbol(self.fragment_id.clone()),
),
(
Expr::Symbol(Symbol::new("site")),
Expr::Symbol(self.site.clone()),
),
(
Expr::Symbol(Symbol::new("mode")),
Expr::Symbol(self.mode.symbol()),
),
(
Expr::Symbol(Symbol::new("latency-class")),
Expr::Symbol(self.latency_class().symbol()),
),
(
Expr::Symbol(Symbol::new("bar-delay-ms")),
Expr::String(self.bar_delay_millis().unwrap_or(0).to_string()),
),
(
Expr::Symbol(Symbol::new("bridge-latency-frames")),
Expr::String(latency.frame_count().to_string()),
),
(
Expr::Symbol(Symbol::new("bridge-latency-packets")),
Expr::String(latency.packet_count().to_string()),
),
(
Expr::Symbol(Symbol::new("bridges")),
Expr::List(
self.bridges
.iter()
.map(|bridge| Expr::Symbol(bridge.kind().symbol()))
.collect(),
),
),
(
Expr::Symbol(Symbol::new("diagnostics")),
Expr::List(self.diagnostics.iter().cloned().map(Expr::Symbol).collect()),
),
(
Expr::Symbol(Symbol::new("output-profiles")),
Expr::List(
self.output_envelopes
.iter()
.map(|envelope| Expr::Symbol(envelope.profile().name().clone()))
.collect(),
),
),
])
}
}
fn remote_output_envelopes(
envelopes: &[StreamEnvelope],
profile: &TransportProfile,
diagnostics: &[Symbol],
) -> Result<Vec<StreamEnvelope>> {
envelopes
.iter()
.map(|envelope| {
let mut envelope_diagnostics = envelope.diagnostics().to_vec();
envelope_diagnostics.extend(diagnostics.iter().cloned());
StreamEnvelope::new_with_clock_domains(
envelope.stream_id().clone(),
envelope.packet_id().clone(),
envelope.media(),
envelope.direction(),
envelope.sequence(),
envelope.ticks().to_vec(),
envelope.clock_domain(),
envelope.clock_domains().to_vec(),
profile.clone(),
envelope_diagnostics,
envelope.packet().clone(),
)
})
.collect()
}