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