Skip to main content

sim_lib_stream_core/
bridge.rs

1//! Domain-bridge descriptors for streams that cross clock domains.
2//!
3//! A domain bridge sits between two stream segments whose rate contracts
4//! disagree -- a different clock domain, sample rate, or latency class -- and
5//! describes how the fabric reconciles them and what latency the crossing
6//! costs. [`DomainBridgeKind`] names the reconciliation strategy,
7//! [`BridgeLatency`] measures the cost in frames and packets, and
8//! [`DomainBridgeDescriptor`] binds a strategy to its input/output
9//! [`RateContract`]s, latency, and diagnostic symbols, and can project the
10//! input/output [`StreamEdge`]s the bridge presents to the graph.
11//!
12//! These are descriptive contract values: they record what a bridge promises,
13//! while the concrete resampling/buffering behavior lives in higher sim-stream
14//! crates.
15
16use sim_kernel::{Error, Result, Symbol};
17
18use crate::{
19    BufferPolicy, ClockDomain, LatencyClass, RateContract, StreamDirection, StreamEdge,
20    StreamMedia, StreamMetadata,
21};
22
23/// Reconciliation strategy a domain bridge uses to cross between rate contracts.
24#[derive(Clone, Copy, Debug, PartialEq, Eq)]
25pub enum DomainBridgeKind {
26    /// Converts between two exact sample rates by resampling.
27    Resampler,
28    /// Absorbs arrival jitter by holding late packets in a buffer.
29    JitterBuffer,
30    /// Inserts a fixed frame delay to compensate for downstream latency.
31    LatencyCompDelay,
32    /// Gates an event-rate (control or MIDI-tick) input into a block-local rate.
33    EventRateGate,
34}
35
36impl DomainBridgeKind {
37    /// Returns the stable wire label for this bridge kind.
38    pub fn name(self) -> &'static str {
39        match self {
40            Self::Resampler => "resampler",
41            Self::JitterBuffer => "jitter-buffer",
42            Self::LatencyCompDelay => "latency-comp-delay",
43            Self::EventRateGate => "event-rate-gate",
44        }
45    }
46
47    /// Returns the `stream/bridge/<name>` symbol identifying this bridge kind.
48    pub fn symbol(self) -> Symbol {
49        Symbol::qualified("stream/bridge", self.name())
50    }
51
52    /// Returns the `stream/bridge-diagnostic/<name>` symbol used to tag this
53    /// bridge kind's diagnostics.
54    pub fn diagnostic_symbol(self) -> Symbol {
55        Symbol::qualified("stream/bridge-diagnostic", self.name())
56    }
57}
58
59/// Latency a bridge incurs, measured in frames and packets.
60///
61/// The two axes are independent: frame latency reflects fixed sample/block
62/// delay, while packet latency reflects how many in-flight packets the bridge
63/// may hold (for example a jitter buffer's late-packet window).
64#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
65pub struct BridgeLatency {
66    frames: u64,
67    packets: u32,
68}
69
70impl BridgeLatency {
71    /// Returns a latency of zero frames and zero packets.
72    pub fn zero() -> Self {
73        Self {
74            frames: 0,
75            packets: 0,
76        }
77    }
78
79    /// Returns a latency of `frames` frames and zero packets.
80    pub fn frames(frames: u64) -> Self {
81        Self { frames, packets: 0 }
82    }
83
84    /// Returns a latency of `packets` packets and zero frames.
85    pub fn packets(packets: u32) -> Self {
86        Self { frames: 0, packets }
87    }
88
89    /// Returns a latency with both a frame and a packet component.
90    pub fn frames_and_packets(frames: u64, packets: u32) -> Self {
91        Self { frames, packets }
92    }
93
94    /// Returns the frame component of this latency.
95    pub fn frame_count(self) -> u64 {
96        self.frames
97    }
98
99    /// Returns the packet component of this latency.
100    pub fn packet_count(self) -> u32 {
101        self.packets
102    }
103
104    /// Returns the component-wise saturating sum of two latencies.
105    pub fn plus(self, other: Self) -> Self {
106        Self {
107            frames: self.frames.saturating_add(other.frames),
108            packets: self.packets.saturating_add(other.packets),
109        }
110    }
111}
112
113/// Full description of one domain bridge: its kind, the input and output rate
114/// contracts it joins, the latency it costs, and its diagnostic symbols.
115///
116/// The constructor helpers ([`resampler`](Self::resampler),
117/// [`jitter_buffer`](Self::jitter_buffer),
118/// [`latency_comp_delay`](Self::latency_comp_delay),
119/// [`event_rate_gate`](Self::event_rate_gate)) build the canonical descriptor
120/// for each [`DomainBridgeKind`] with its standard rates and latency.
121#[derive(Clone, Debug, PartialEq, Eq)]
122pub struct DomainBridgeDescriptor {
123    kind: DomainBridgeKind,
124    input_rate: RateContract,
125    output_rate: RateContract,
126    latency: BridgeLatency,
127    diagnostics: Vec<Symbol>,
128}
129
130impl DomainBridgeDescriptor {
131    /// Builds a descriptor from explicit kind, input/output rates, latency, and
132    /// diagnostics.
133    pub fn new(
134        kind: DomainBridgeKind,
135        input_rate: RateContract,
136        output_rate: RateContract,
137        latency: BridgeLatency,
138        diagnostics: Vec<Symbol>,
139    ) -> Self {
140        Self {
141            kind,
142            input_rate,
143            output_rate,
144            latency,
145            diagnostics,
146        }
147    }
148
149    /// Builds a resampler bridge between two exact sample rates.
150    ///
151    /// Returns an error if either rate is zero.
152    pub fn resampler(input_hz: u32, output_hz: u32) -> Result<Self> {
153        if input_hz == 0 || output_hz == 0 {
154            return Err(Error::Eval(
155                "resampler rates must be greater than zero".to_owned(),
156            ));
157        }
158        Ok(Self::new(
159            DomainBridgeKind::Resampler,
160            RateContract::sample_exact(Some(input_hz)),
161            RateContract::sample_exact(Some(output_hz)),
162            BridgeLatency::frames(32),
163            vec![DomainBridgeKind::Resampler.diagnostic_symbol()],
164        ))
165    }
166
167    /// Builds a jitter-buffer bridge that tolerates up to `max_late_packets`
168    /// late packets of wall-clock buffered-preview input.
169    pub fn jitter_buffer(max_late_packets: u32) -> Self {
170        Self::new(
171            DomainBridgeKind::JitterBuffer,
172            RateContract::new(ClockDomain::Wall, LatencyClass::BufferedPreview, None),
173            RateContract::new(ClockDomain::Wall, LatencyClass::BufferedPreview, None),
174            BridgeLatency::packets(max_late_packets),
175            vec![DomainBridgeKind::JitterBuffer.diagnostic_symbol()],
176        )
177    }
178
179    /// Builds a latency-compensation bridge that delays a block-local stream by
180    /// a fixed number of frames.
181    pub fn latency_comp_delay(frames: u64) -> Self {
182        Self::new(
183            DomainBridgeKind::LatencyCompDelay,
184            RateContract::block_local(),
185            RateContract::block_local(),
186            BridgeLatency::frames(frames),
187            vec![DomainBridgeKind::LatencyCompDelay.diagnostic_symbol()],
188        )
189    }
190
191    /// Builds an event-rate-gate bridge that gates a control or MIDI-tick input
192    /// domain into a block-local output.
193    ///
194    /// Returns an error if `input_domain` is neither
195    /// [`ClockDomain::Control`] nor [`ClockDomain::MidiTick`].
196    pub fn event_rate_gate(input_domain: ClockDomain) -> Result<Self> {
197        let input_rate = match input_domain {
198            ClockDomain::Control => RateContract::control(),
199            ClockDomain::MidiTick => RateContract::midi_tick(),
200            other => {
201                return Err(Error::Eval(format!(
202                    "event-rate-gate cannot accept {} input",
203                    other.wire_label()
204                )));
205            }
206        };
207        Ok(Self::new(
208            DomainBridgeKind::EventRateGate,
209            input_rate,
210            RateContract::block_local(),
211            BridgeLatency::zero(),
212            vec![DomainBridgeKind::EventRateGate.diagnostic_symbol()],
213        ))
214    }
215
216    /// Returns the bridge kind.
217    pub fn kind(&self) -> DomainBridgeKind {
218        self.kind
219    }
220
221    /// Returns the bridge kind's wire label.
222    pub fn name(&self) -> &'static str {
223        self.kind.name()
224    }
225
226    /// Returns the rate contract this bridge accepts on its input.
227    pub fn input_rate(&self) -> RateContract {
228        self.input_rate
229    }
230
231    /// Returns the rate contract this bridge emits on its output.
232    pub fn output_rate(&self) -> RateContract {
233        self.output_rate
234    }
235
236    /// Returns the latency this bridge incurs.
237    pub fn latency(&self) -> BridgeLatency {
238        self.latency
239    }
240
241    /// Returns the diagnostic symbols this bridge may raise.
242    pub fn diagnostics(&self) -> &[Symbol] {
243        &self.diagnostics
244    }
245
246    /// Builds the input [`StreamEdge`] this bridge presents as a sink for the
247    /// given media.
248    pub fn input_edge(&self, media: StreamMedia) -> StreamEdge {
249        StreamEdge::new(
250            Symbol::new("in"),
251            self.input_rate,
252            bridge_metadata(
253                self.kind,
254                "in",
255                media,
256                StreamDirection::Sink,
257                self.input_rate,
258            ),
259        )
260    }
261
262    /// Builds the output [`StreamEdge`] this bridge presents as a source for the
263    /// given media.
264    pub fn output_edge(&self, media: StreamMedia) -> StreamEdge {
265        StreamEdge::new(
266            Symbol::new("out"),
267            self.output_rate,
268            bridge_metadata(
269                self.kind,
270                "out",
271                media,
272                StreamDirection::Source,
273                self.output_rate,
274            ),
275        )
276    }
277}
278
279fn bridge_metadata(
280    kind: DomainBridgeKind,
281    port: &str,
282    media: StreamMedia,
283    direction: StreamDirection,
284    rate: RateContract,
285) -> StreamMetadata {
286    StreamMetadata::new(
287        Symbol::qualified("stream/bridge-edge", format!("{}-{port}", kind.name())),
288        media,
289        direction,
290        rate.clock_domain().symbol(),
291        BufferPolicy::bounded(1).expect("bridge metadata uses a nonzero buffer"),
292    )
293}