Skip to main content

flexaudio_core/
chunk_ring.rs

1//! 完成 [`AudioChunk`] の SPSC リング(ringbuf バック)。
2//!
3//! 満杯時は最古を pop して新規を push する(DROP_OLDEST)。ドロップ数を
4//! [`AtomicU64`] で数えて次チャンクの `dropped_before` に反映する。consumer は
5//! `try_pop()`。
6//!
7//! ringbuf の overwrite(`push_overwrite`)は producer が最古を pop する必要があり、
8//! これは consumer 側のインデックスにも触れるので SPSC のロックフリー前提を満たさない
9//! (overwrite を並行に行うにはロックが要る)。このリングの producer は RT スレッド
10//! ではなく取り込み/加工スレッド(通常優先度)なので、リング本体を短い [`Mutex`]
11//! で保護する。RT 経路([`mod@crate::raw_ring`])はこのロックに触れない。
12
13use std::sync::atomic::{AtomicU64, Ordering};
14use std::sync::{Arc, Mutex};
15
16use ringbuf::traits::{Consumer, Observer, RingBuffer};
17use ringbuf::HeapRb;
18
19use crate::types::AudioChunk;
20
21type Shared = Arc<Mutex<HeapRb<AudioChunk>>>;
22
23/// 容量 `capacity_chunks` のチャンクリングを作る。
24///
25/// producer は加工スレッドへ、consumer は poll スレッドへ渡す。`dropped` カウンタは
26/// DROP_OLDEST で捨てたチャンク数を数え、次に push されるチャンクの `dropped_before`
27/// に反映される。
28pub fn chunk_ring(capacity_chunks: usize) -> (ChunkProducer, ChunkConsumer) {
29    let cap = capacity_chunks.max(1);
30    let rb: Shared = Arc::new(Mutex::new(HeapRb::<AudioChunk>::new(cap)));
31    let dropped = Arc::new(AtomicU64::new(0));
32    (
33        ChunkProducer {
34            rb: rb.clone(),
35            dropped: dropped.clone(),
36        },
37        ChunkConsumer { rb, dropped },
38    )
39}
40
41/// 加工スレッド側のハンドル。DROP_OLDEST 方針で push する。
42pub struct ChunkProducer {
43    rb: Shared,
44    dropped: Arc<AtomicU64>,
45}
46
47impl ChunkProducer {
48    /// チャンクを push する。満杯なら最古を捨て(DROP_OLDEST)、捨てた数を数える。
49    ///
50    /// `dropped_before` には、このチャンクが入るまでに累計で捨てたチャンク数
51    /// (このチャンクのための追い出し 1 件を含む)を入れる。消費側は連続チャンクの
52    /// `dropped_before` の差分で直前の欠落数を、絶対値で累計欠落数を知れる。
53    ///
54    /// この push でドロップが起きたら `Some(累計ドロップ数)` を返す
55    /// ([`crate::types::Event::ChunkDropped`] の発火判断に使える)。起きなければ `None`。
56    pub fn push(&mut self, mut chunk: AudioChunk) -> Option<u64> {
57        // poison(別スレッドがロック保持中に panic)でもリング本体は壊れないので、連鎖
58        // panic させず内部値を回収して続ける。
59        let mut rb = self.rb.lock().unwrap_or_else(|e| e.into_inner());
60
61        // この push が最古を追い出す(満杯)か先に見ておく。
62        let will_evict = rb.is_full();
63
64        if will_evict {
65            self.dropped.fetch_add(1, Ordering::Relaxed);
66        }
67        // この push の追い出し分を含めた累計ドロップ数を載せる。
68        let total = self.dropped.load(Ordering::Relaxed);
69        chunk.dropped_before = u32::try_from(total).unwrap_or(u32::MAX);
70
71        let evicted = rb.push_overwrite(chunk);
72        drop(rb);
73
74        debug_assert_eq!(evicted.is_some(), will_evict);
75
76        if will_evict {
77            Some(total)
78        } else {
79            None
80        }
81    }
82
83    /// これまでに DROP_OLDEST で捨てた累計チャンク数。
84    pub fn dropped_count(&self) -> u64 {
85        self.dropped.load(Ordering::Relaxed)
86    }
87}
88
89/// poll スレッド側のハンドル。`try_pop` で消費する。
90pub struct ChunkConsumer {
91    rb: Shared,
92    dropped: Arc<AtomicU64>,
93}
94
95impl ChunkConsumer {
96    /// 最古のチャンクを 1 つ取り出す。無ければ `None`(非ブロッキング)。
97    pub fn try_pop(&mut self) -> Option<AudioChunk> {
98        // poison でもリングは壊れないので回収して続ける。
99        let mut rb = self.rb.lock().unwrap_or_else(|e| e.into_inner());
100        rb.try_pop()
101    }
102
103    /// 現在リングに溜まっているチャンク数。
104    pub fn len(&self) -> usize {
105        // poison でもリングは壊れないので回収して続ける。
106        self.rb
107            .lock()
108            .unwrap_or_else(|e| e.into_inner())
109            .occupied_len()
110    }
111
112    /// リングが空か。
113    pub fn is_empty(&self) -> bool {
114        self.len() == 0
115    }
116
117    /// これまでに DROP_OLDEST で捨てた累計チャンク数。
118    pub fn dropped_count(&self) -> u64 {
119        self.dropped.load(Ordering::Relaxed)
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126    use crate::types::ChunkFlags;
127
128    fn chunk(seq: u64) -> AudioChunk {
129        AudioChunk {
130            data: vec![0.0; 1920],
131            frames: 960,
132            pts_ns: seq as i64 * 20_000_000,
133            seq,
134            flags: ChunkFlags::empty(),
135            dropped_before: 0,
136            peak: 0.0,
137            rms: 0.0,
138        }
139    }
140
141    #[test]
142    fn fifo_order_when_not_full() {
143        let (mut p, mut c) = chunk_ring(4);
144        assert!(c.is_empty());
145        for s in 0..3 {
146            assert_eq!(p.push(chunk(s)), None);
147        }
148        assert_eq!(c.len(), 3);
149        assert_eq!(c.try_pop().unwrap().seq, 0);
150        assert_eq!(c.try_pop().unwrap().seq, 1);
151        assert_eq!(c.try_pop().unwrap().seq, 2);
152        assert!(c.try_pop().is_none());
153    }
154
155    #[test]
156    fn drop_oldest_when_full_and_counts() {
157        let (mut p, mut c) = chunk_ring(2);
158        // 容量 2 を埋める。
159        assert_eq!(p.push(chunk(0)), None);
160        assert_eq!(p.push(chunk(1)), None);
161        assert_eq!(p.dropped_count(), 0);
162
163        // 満杯 → 最古(seq0)を捨てて seq2 を入れる。
164        let dropped_total = p.push(chunk(2));
165        assert_eq!(dropped_total, Some(1));
166        assert_eq!(p.dropped_count(), 1);
167
168        // 次の push でさらにもう 1 件ドロップ。
169        let dropped_total = p.push(chunk(3));
170        assert_eq!(dropped_total, Some(2));
171        assert_eq!(p.dropped_count(), 2);
172
173        // 残っているのは最新 2 件 seq2, seq3。
174        let first = c.try_pop().unwrap();
175        assert_eq!(first.seq, 2);
176        // seq2 が入るまでの累計ドロップ = 1(seq0 を捨てた)。
177        assert_eq!(first.dropped_before, 1);
178
179        let second = c.try_pop().unwrap();
180        assert_eq!(second.seq, 3);
181        // seq3 が入るまでの累計ドロップ = 2(seq0, seq1 を捨てた)。
182        assert_eq!(second.dropped_before, 2);
183
184        assert!(c.try_pop().is_none());
185    }
186
187    #[test]
188    fn dropped_before_is_cumulative() {
189        let (mut p, mut c) = chunk_ring(1);
190        p.push(chunk(0)); // 入る(dropped_before=0)
191                          // 容量1で満杯 → 毎回ドロップ。
192        p.push(chunk(1)); // seq0 捨て、累計ドロップ=1
193        c.try_pop(); // seq1 取り出し → 空く
194        let r = p.push(chunk(2)); // 空いているので入る、ドロップ無し
195        assert_eq!(r, None);
196        let got = c.try_pop().unwrap();
197        assert_eq!(got.seq, 2);
198        // ドロップは増えていないが累計は 1 のまま保持される。
199        assert_eq!(got.dropped_before, 1);
200        assert_eq!(p.dropped_count(), 1);
201    }
202
203    /// 容量 0 は `max(1)` へ丸められ、容量 1 のリングとして機能する(panic しない)。
204    #[test]
205    fn zero_capacity_is_clamped_to_one() {
206        let (mut p, mut c) = chunk_ring(0);
207        assert!(c.is_empty());
208        assert_eq!(p.push(chunk(0)), None); // 1 件入る。
209        assert_eq!(c.len(), 1);
210        // 満杯 → 次は最古を捨てる。
211        assert_eq!(p.push(chunk(1)), Some(1));
212        let got = c.try_pop().unwrap();
213        assert_eq!(got.seq, 1);
214    }
215
216    /// `len` / `is_empty` が push/pop に追従し、容量を超えない(off-by-one 防止)。
217    #[test]
218    fn len_tracks_occupancy_and_is_capped() {
219        let (mut p, mut c) = chunk_ring(3);
220        assert!(c.is_empty());
221        for s in 0..3 {
222            p.push(chunk(s));
223        }
224        assert_eq!(c.len(), 3);
225        assert!(!c.is_empty());
226        // 満杯後にさらに 2 件 → 容量は 3 のまま(最古を捨てて入れ替え)。
227        p.push(chunk(3));
228        p.push(chunk(4));
229        assert_eq!(c.len(), 3, "占有数は容量 3 を超えない");
230        // 残るのは最新 3 件 seq2,3,4。
231        assert_eq!(c.try_pop().unwrap().seq, 2);
232        assert_eq!(c.try_pop().unwrap().seq, 3);
233        assert_eq!(c.try_pop().unwrap().seq, 4);
234        assert!(c.try_pop().is_none());
235        assert!(c.is_empty());
236    }
237
238    /// 空リングからの `try_pop` は None(非ブロッキング)。producer/consumer は
239    /// dropped カウンタを共有する。
240    #[test]
241    fn empty_pop_is_none_and_dropped_is_shared() {
242        let (mut p, mut c) = chunk_ring(2);
243        assert!(c.try_pop().is_none());
244        // 容量 2 → 1 件ドロップさせる。
245        p.push(chunk(0));
246        p.push(chunk(1));
247        p.push(chunk(2)); // seq0 を捨てる。
248        assert_eq!(p.dropped_count(), 1);
249        assert_eq!(c.dropped_count(), 1, "consumer 側も同じ dropped を観測する");
250    }
251}