Skip to main content

rusty_opus/
parallel.rs

1//! Frame/chunk-parallel Opus encoding (R1) — the structural win that beats a
2//! single-threaded libopus on wall-clock.
3//!
4//! Opus carries real inter-frame state (SILK LTP/NSQ/NLSF/entropy, CELT
5//! pre-emphasis/overlap/prefilter/energy, the HP filter and input resampler), so
6//! a frame range cannot be encoded byte-identically from a cold encoder the way
7//! AAC/Vorbis frames can. Instead each worker **primes** its encoder by
8//! re-encoding `warmup` frames *before* its chunk (output discarded), which
9//! converges the state to the true continuous state — a stable encoder forgets
10//! its initial conditions over a few frames. The primed boundary is
11//! perceptually-neutral (PEAQ ΔODG ≤ 0.03 vs serial), not byte-identical, so this
12//! is an opt-in fast path, gated perceptually.
13//!
14//! Deterministic: fixed chunk boundaries → identical output across runs. Uses
15//! only `std::thread` (no rayon).
16
17use crate::{Application, OpusEncoder};
18
19/// Configuration for a parallel encode; mirrors the knobs on [`OpusEncoder`].
20#[derive(Clone, Copy)]
21pub struct ParallelConfig {
22    pub sample_rate: i32,
23    pub channels: usize,
24    pub application: Application,
25    pub bitrate_bps: i32,
26    pub complexity: i32,
27    pub use_cbr: bool,
28    /// Frames of look-back each worker re-encodes to prime its state (discarded).
29    /// Must exceed the deepest inter-frame memory (SILK LTP lag + NSQ delay +
30    /// CELT overlap). 8 (~160 ms @20 ms frames) is a safe default; sweep down
31    /// under the PEAQ gate. `0` = no priming (equivalent to naive chunking).
32    pub warmup: usize,
33    /// Worker count; `0` selects `available_parallelism`.
34    pub threads: usize,
35}
36
37impl ParallelConfig {
38    pub fn new(sample_rate: i32, channels: usize, application: Application) -> Self {
39        ParallelConfig {
40            sample_rate,
41            channels,
42            application,
43            bitrate_bps: 64_000,
44            complexity: 9,
45            use_cbr: false,
46            warmup: 8,
47            threads: 0,
48        }
49    }
50}
51
52/// Encode `pcm` (interleaved f32, `channels`-interleaved) in `frame_size`
53/// samples-per-channel frames, across `cfg.threads` workers, returning one Opus
54/// packet per frame in order. Falls back to a single serial encoder when the
55/// input is too small to split usefully.
56///
57/// The serial equivalent is `encode_serial`; this returns the same *count* of
58/// packets and (with adequate `warmup`) a perceptually-identical bitstream.
59pub fn encode_parallel(cfg: &ParallelConfig, pcm: &[f32], frame_size: usize) -> Vec<Vec<u8>> {
60    let step = frame_size * cfg.channels;
61    if step == 0 {
62        return Vec::new();
63    }
64    let total_frames = pcm.len() / step;
65    if total_frames == 0 {
66        return Vec::new();
67    }
68
69    let threads = if cfg.threads == 0 {
70        std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1)
71    } else {
72        cfg.threads
73    };
74
75    // Each chunk must be ≫ warmup to keep the redundant-compute overhead small;
76    // require chunk ≥ 4·warmup (and ≥ 1). Cap the worker count accordingly.
77    let min_chunk = (cfg.warmup * 4).max(1);
78    let n_workers = threads.max(1).min((total_frames / min_chunk).max(1));
79    if n_workers <= 1 {
80        return encode_serial(cfg, pcm, frame_size);
81    }
82
83    // Contiguous, balanced frame ranges [start, end).
84    let base = total_frames / n_workers;
85    let rem = total_frames % n_workers;
86    let mut ranges = Vec::with_capacity(n_workers);
87    let mut start = 0usize;
88    for w in 0..n_workers {
89        let len = base + if w < rem { 1 } else { 0 };
90        ranges.push((start, start + len));
91        start += len;
92    }
93
94    let mut chunks: Vec<Vec<Vec<u8>>> = Vec::new();
95    std::thread::scope(|scope| {
96        let handles: Vec<_> = ranges
97            .iter()
98            .map(|&(cstart, cend)| {
99                let cfg = cfg;
100                scope.spawn(move || encode_chunk(cfg, pcm, frame_size, cstart, cend))
101            })
102            .collect();
103        for h in handles {
104            chunks.push(h.join().expect("opus parallel worker panicked"));
105        }
106    });
107
108    // Concatenate in range order.
109    let mut out = Vec::with_capacity(total_frames);
110    for c in chunks {
111        out.extend(c);
112    }
113    out
114}
115
116/// Encode frames `[cstart, cend)` with a fresh encoder primed by re-encoding the
117/// `warmup` frames before `cstart` (their packets discarded).
118fn encode_chunk(
119    cfg: &ParallelConfig,
120    pcm: &[f32],
121    frame_size: usize,
122    cstart: usize,
123    cend: usize,
124) -> Vec<Vec<u8>> {
125    let step = frame_size * cfg.channels;
126    let mut enc = new_encoder(cfg);
127    let warm_start = cstart.saturating_sub(cfg.warmup);
128    let mut buf = vec![0u8; 4000];
129    let mut packets = Vec::with_capacity(cend - cstart);
130    for f in warm_start..cend {
131        let frame = &pcm[f * step..(f + 1) * step];
132        let n = enc.encode(frame, frame_size, &mut buf).expect("opus encode");
133        if f >= cstart {
134            packets.push(buf[..n].to_vec());
135        }
136    }
137    packets
138}
139
140/// Single-threaded reference: encode every frame with one continuous encoder.
141/// The correctness/quality anchor for [`encode_parallel`].
142pub fn encode_serial(cfg: &ParallelConfig, pcm: &[f32], frame_size: usize) -> Vec<Vec<u8>> {
143    let step = frame_size * cfg.channels;
144    if step == 0 {
145        return Vec::new();
146    }
147    let total_frames = pcm.len() / step;
148    let mut enc = new_encoder(cfg);
149    let mut buf = vec![0u8; 4000];
150    let mut packets = Vec::with_capacity(total_frames);
151    for f in 0..total_frames {
152        let frame = &pcm[f * step..(f + 1) * step];
153        let n = enc.encode(frame, frame_size, &mut buf).expect("opus encode");
154        packets.push(buf[..n].to_vec());
155    }
156    packets
157}
158
159/// **R1a — per-stream parallelism (byte-identical).** Encode several *independent*
160/// PCM streams concurrently, one serial encoder per worker. Each stream's output
161/// is exactly its serial encode (no chunk seams), so this is **bit-identical** to
162/// encoding them one-by-one — the right tool for batch/many-stream workloads
163/// (and for short streams too small to split internally with [`encode_parallel`]).
164///
165/// `streams[i]` is `(config, pcm, frame_size)`; returns `out[i]` = that stream's
166/// packets. Order preserved. Uses a bounded pool (`threads`, or all cores) so a
167/// thousand tiny streams don't spawn a thousand threads.
168pub fn encode_streams(
169    streams: &[(ParallelConfig, &[f32], usize)],
170    threads: usize,
171) -> Vec<Vec<Vec<u8>>> {
172    let n = streams.len();
173    let mut out: Vec<Vec<Vec<u8>>> = (0..n).map(|_| Vec::new()).collect();
174    if n == 0 {
175        return out;
176    }
177    let workers = if threads == 0 {
178        std::thread::available_parallelism().map(|p| p.get()).unwrap_or(1)
179    } else {
180        threads
181    }
182    .max(1)
183    .min(n);
184
185    let next = std::sync::atomic::AtomicUsize::new(0);
186    let out_slots: Vec<std::sync::Mutex<Option<Vec<Vec<u8>>>>> =
187        (0..n).map(|_| std::sync::Mutex::new(None)).collect();
188    std::thread::scope(|scope| {
189        for _ in 0..workers {
190            let next = &next;
191            let out_slots = &out_slots;
192            scope.spawn(move || loop {
193                let idx = next.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
194                if idx >= n {
195                    break;
196                }
197                let (cfg, pcm, frame_size) = &streams[idx];
198                let pkts = encode_serial(cfg, pcm, *frame_size);
199                *out_slots[idx].lock().unwrap() = Some(pkts);
200            });
201        }
202    });
203    for (slot, dst) in out_slots.into_iter().zip(out.iter_mut()) {
204        *dst = slot.into_inner().unwrap().unwrap_or_default();
205    }
206    out
207}
208
209fn new_encoder(cfg: &ParallelConfig) -> OpusEncoder {
210    let mut enc = OpusEncoder::new(cfg.sample_rate, cfg.channels, cfg.application)
211        .expect("opus encoder init");
212    enc.bitrate_bps = cfg.bitrate_bps;
213    enc.complexity = cfg.complexity.clamp(0, 10);
214    enc.use_cbr = cfg.use_cbr;
215    enc
216}