Skip to main content

sim_lib_stream_combinators/
bridge.rs

1use std::sync::{
2    Arc, Mutex,
3    atomic::{AtomicUsize, Ordering},
4};
5
6use sim_kernel::{Error, Result, Symbol};
7use sim_lib_stream_core::{
8    ClockDomain, ClockTickIndex, DomainBridgeDescriptor, PcmPacket, StreamItem, StreamMetadata,
9    StreamPacket, tick_clock_index,
10};
11
12use crate::{Stream, StreamNode};
13
14/// Resamples a PCM stream from `input_hz` to `output_hz`.
15///
16/// Each PCM packet is rate-converted by nearest-source-frame interleaving;
17/// non-PCM packets pass through unchanged. Errors if either rate is zero.
18pub fn resample_pcm(source: Stream, input_hz: u32, output_hz: u32) -> Result<Stream> {
19    let descriptor = DomainBridgeDescriptor::resampler(input_hz, output_hz)?;
20    let metadata = source.metadata().clone();
21    Ok(Stream::new(ResamplePcmNode {
22        source,
23        metadata,
24        input_hz,
25        output_hz,
26        _descriptor: descriptor,
27    }))
28}
29
30/// Reorders packets by `clock` tick within a bounded latency window.
31///
32/// The buffer keeps an online reordering window of `max_late_packets + 1`
33/// packets: it pulls only enough of `source` to fill that window (never draining
34/// a live source to its end), then emits the lowest-tick packet, breaking ties
35/// by arrival order so equal ticks stay stable. A packet whose tick falls below
36/// the last emitted tick has arrived more than `max_late_packets` positions
37/// behind the highest accepted tick; it is dropped and counted rather than
38/// reordered. With `max_late_packets` of `0` the window is a single packet, so
39/// any out-of-order packet is dropped.
40pub fn jitter_buffer(source: Stream, clock: Symbol, max_late_packets: u32) -> Stream {
41    jitter_buffer_with_drops(source, clock, max_late_packets).0
42}
43
44/// Builds a jitter buffer alongside a shared counter of late-dropped packets.
45///
46/// The public [`jitter_buffer`] wraps this and discards the counter; tests read
47/// the counter to assert the positive-lateness bound.
48pub(crate) fn jitter_buffer_with_drops(
49    source: Stream,
50    clock: Symbol,
51    max_late_packets: u32,
52) -> (Stream, Arc<AtomicUsize>) {
53    let descriptor = DomainBridgeDescriptor::jitter_buffer(max_late_packets);
54    let metadata = source.metadata().clone();
55    let late_dropped = Arc::new(AtomicUsize::new(0));
56    let stream = Stream::new(JitterBufferNode {
57        source,
58        metadata,
59        clock,
60        max_late_packets,
61        state: Mutex::new(JitterBufferState::default()),
62        late_dropped: Arc::clone(&late_dropped),
63        _descriptor: descriptor,
64    });
65    (stream, late_dropped)
66}
67
68/// Records a `frames`-frame latency-compensation delay over the stream.
69///
70/// The packets pass through untouched; the descriptor carries the declared
71/// delay so downstream clock alignment can account for it.
72pub fn latency_comp_delay(source: Stream, frames: u64) -> Stream {
73    let descriptor = DomainBridgeDescriptor::latency_comp_delay(frames);
74    let metadata = source.metadata().clone();
75    Stream::new(PassthroughBridgeNode {
76        source,
77        metadata,
78        _descriptor: descriptor,
79    })
80}
81
82/// Bridges an event stream into the control clock domain as a rate gate.
83///
84/// The source clock domain is read from its metadata and validated into a gate
85/// descriptor; packets pass through unchanged.
86pub fn event_rate_gate(source: Stream) -> Result<Stream> {
87    let input_domain = ClockDomain::for_stream_clock(source.metadata().clock())?;
88    let descriptor = DomainBridgeDescriptor::event_rate_gate(input_domain)?;
89    let metadata = source.metadata().clone();
90    Ok(Stream::new(PassthroughBridgeNode {
91        source,
92        metadata,
93        _descriptor: descriptor,
94    }))
95}
96
97struct ResamplePcmNode {
98    source: Stream,
99    metadata: StreamMetadata,
100    input_hz: u32,
101    output_hz: u32,
102    _descriptor: DomainBridgeDescriptor,
103}
104
105impl StreamNode for ResamplePcmNode {
106    fn metadata(&self) -> &StreamMetadata {
107        &self.metadata
108    }
109
110    fn next_packet(&self) -> Result<Option<StreamItem>> {
111        let Some(item) = self.source.next_packet()? else {
112            return Ok(None);
113        };
114        let StreamPacket::Pcm(packet) = item.packet() else {
115            return Ok(Some(item));
116        };
117        let packet = resample_packet(packet, self.input_hz, self.output_hz)?;
118        StreamItem::with_ticks(StreamPacket::Pcm(packet), item.ticks().to_vec()).map(Some)
119    }
120
121    fn is_done(&self) -> Result<bool> {
122        self.source.is_done()
123    }
124}
125
126struct JitterBufferNode {
127    source: Stream,
128    metadata: StreamMetadata,
129    clock: Symbol,
130    max_late_packets: u32,
131    state: Mutex<JitterBufferState>,
132    late_dropped: Arc<AtomicUsize>,
133    _descriptor: DomainBridgeDescriptor,
134}
135
136/// Online reordering window shared behind the node's mutex.
137#[derive(Default)]
138struct JitterBufferState {
139    /// Buffered packets awaiting emission, each tagged with its arrival ordinal.
140    window: Vec<(usize, StreamItem)>,
141    /// Monotonic arrival counter; breaks ties in tick order stably.
142    next_ordinal: usize,
143    /// Highest emitted tick; a lower newly accepted tick is late.
144    last_emitted: Option<ClockTickIndex>,
145    /// Whether the upstream source has reached its terminal `done`.
146    source_done: bool,
147}
148
149impl StreamNode for JitterBufferNode {
150    fn metadata(&self) -> &StreamMetadata {
151        &self.metadata
152    }
153
154    fn next_packet(&self) -> Result<Option<StreamItem>> {
155        let mut state = self
156            .state
157            .lock()
158            .map_err(|_| Error::PoisonedLock("jitter-buffer state"))?;
159        self.fill_window(&mut state)?;
160        let target = self.max_late_packets as usize + 1;
161        if state.window.len() < target && !state.source_done {
162            // Live source without enough ordering context yet: emit nothing.
163            return Ok(None);
164        }
165        self.pop_next(&mut state)
166    }
167
168    fn is_done(&self) -> Result<bool> {
169        let state = self
170            .state
171            .lock()
172            .map_err(|_| Error::PoisonedLock("jitter-buffer state"))?;
173        Ok(state.window.is_empty() && (state.source_done || self.source.is_done()?))
174    }
175}
176
177impl JitterBufferNode {
178    /// Pulls upstream packets until the ordering window is full or the source
179    /// signals no packet is currently available. Never drains to end of source.
180    fn fill_window(&self, state: &mut JitterBufferState) -> Result<()> {
181        let target = self.max_late_packets as usize + 1;
182        while !state.source_done && state.window.len() < target {
183            match self.source.next_packet()? {
184                Some(item) => self.accept_or_drop(state, item)?,
185                None => {
186                    if self.source.is_done()? {
187                        state.source_done = true;
188                    }
189                    break;
190                }
191            }
192        }
193        Ok(())
194    }
195
196    /// Buffers `item`, or drops it (and counts it) when it is more than
197    /// `max_late_packets` behind the highest accepted tick.
198    fn accept_or_drop(&self, state: &mut JitterBufferState, item: StreamItem) -> Result<()> {
199        let key = tick_key(&item, &self.clock)?;
200        let late = match (&key, &state.last_emitted) {
201            (Some(key), Some(last)) => key < last,
202            _ => false,
203        };
204        if late {
205            self.late_dropped.fetch_add(1, Ordering::Relaxed);
206            return Ok(());
207        }
208        let ordinal = state.next_ordinal;
209        state.next_ordinal = state.next_ordinal.saturating_add(1);
210        state.window.push((ordinal, item));
211        Ok(())
212    }
213
214    /// Removes and returns the lowest-tick buffered packet, advancing the
215    /// highest-emitted marker. Ties are broken by arrival order.
216    fn pop_next(&self, state: &mut JitterBufferState) -> Result<Option<StreamItem>> {
217        if state.window.is_empty() {
218            return Ok(None);
219        }
220        let mut best = 0usize;
221        for index in 1..state.window.len() {
222            if self.precedes(&state.window[index], &state.window[best])? {
223                best = index;
224            }
225        }
226        let (_, item) = state.window.remove(best);
227        if let Some(key) = tick_key(&item, &self.clock)? {
228            state.last_emitted = Some(key);
229        }
230        Ok(Some(item))
231    }
232
233    /// Reports whether `left` should be emitted before `right`: lower tick
234    /// first, ties (and keyless packets) by arrival order.
235    fn precedes(&self, left: &(usize, StreamItem), right: &(usize, StreamItem)) -> Result<bool> {
236        Ok(
237            match (
238                tick_key(&left.1, &self.clock)?,
239                tick_key(&right.1, &self.clock)?,
240            ) {
241                (Some(left_key), Some(right_key)) => (left_key, left.0) < (right_key, right.0),
242                _ => left.0 < right.0,
243            },
244        )
245    }
246}
247
248struct PassthroughBridgeNode {
249    source: Stream,
250    metadata: StreamMetadata,
251    _descriptor: DomainBridgeDescriptor,
252}
253
254impl StreamNode for PassthroughBridgeNode {
255    fn metadata(&self) -> &StreamMetadata {
256        &self.metadata
257    }
258
259    fn next_packet(&self) -> Result<Option<StreamItem>> {
260        self.source.next_packet()
261    }
262
263    fn is_done(&self) -> Result<bool> {
264        self.source.is_done()
265    }
266}
267
268fn resample_packet(packet: &PcmPacket, input_hz: u32, output_hz: u32) -> Result<PcmPacket> {
269    if input_hz == 0 || output_hz == 0 {
270        return Err(Error::Eval("PCM resample rates must be nonzero".to_owned()));
271    }
272    let output_frames = resampled_frame_count(packet.frames(), input_hz, output_hz);
273    match packet.sample_format() {
274        sim_lib_stream_core::PcmSampleFormat::I16 => PcmPacket::i16(
275            packet.channels(),
276            output_frames,
277            resample_interleaved(
278                packet.samples_i16(),
279                packet.channels(),
280                output_frames,
281                |v| v,
282            ),
283        ),
284        sim_lib_stream_core::PcmSampleFormat::F32 => PcmPacket::f32(
285            packet.channels(),
286            output_frames,
287            resample_interleaved(
288                packet.samples_f32(),
289                packet.channels(),
290                output_frames,
291                |v| v,
292            ),
293        ),
294    }
295}
296
297fn resampled_frame_count(input_frames: usize, input_hz: u32, output_hz: u32) -> usize {
298    if input_frames == 0 {
299        return 0;
300    }
301    let frames = (input_frames as u64)
302        .saturating_mul(u64::from(output_hz))
303        .saturating_add(u64::from(input_hz / 2))
304        / u64::from(input_hz);
305    frames.max(1) as usize
306}
307
308fn resample_interleaved<T: Copy>(
309    samples: &[T],
310    channels: usize,
311    output_frames: usize,
312    copy: impl Fn(T) -> T,
313) -> Vec<T> {
314    let input_frames = samples.len() / channels;
315    if output_frames == 0 || input_frames == 0 {
316        return Vec::new();
317    }
318    let mut out = Vec::with_capacity(output_frames * channels);
319    for frame in 0..output_frames {
320        let source_frame = frame.saturating_mul(input_frames) / output_frames;
321        let source_frame = source_frame.min(input_frames.saturating_sub(1));
322        for channel in 0..channels {
323            out.push(copy(samples[source_frame * channels + channel]));
324        }
325    }
326    out
327}
328
329fn tick_key(item: &StreamItem, clock: &Symbol) -> Result<Option<ClockTickIndex>> {
330    item.ticks().iter().try_fold(None, |found, tick| {
331        tick_clock_index(tick, clock).map(|parsed| found.or(parsed))
332    })
333}