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}