1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
//! Opus codec + resampling for the WebRTC media leg of `moshi-serve`. WebRTC audio is
//! Opus-over-RTP at a 48 kHz clock by the browser spec, while Moshi runs at 24 kHz — so this
//! module encodes/decodes Opus at 48 kHz (mono, 20 ms frames = 960 samples) and resamples the
//! clean 2× ratio to/from Moshi's 24 kHz. Feature-gated (`webrtc-media`): the only libopus (C)
//! link in the tree, off by default.
//!
//! Frame math: one Moshi step = 1920 samples @ 24 kHz = 80 ms = four 20 ms Opus frames @ 48 kHz.
use anyhow::{Context, Result};
/// WebRTC Opus clock.
pub const OPUS_RATE: u32 = 48000;
/// Moshi's sample rate.
pub const MOSHI_RATE: usize = 24000;
/// Opus frame at 48 kHz, 20 ms.
pub const OPUS_FRAME_48: usize = 960;
/// Streaming Opus encoder + decoder with the 24↔48 resamplers folded in, so the engine only
/// ever sees 24 kHz mono f32 and the RTP layer only ever sees 20 ms Opus packets. Internally
/// **stereo at 48 kHz** to match the browser's `opus/48000/2` negotiation — mono is duplicated
/// to both channels on encode and averaged back on decode.
pub struct OpusRtc {
enc: opus::Encoder,
dec: opus::Decoder,
/// 48 kHz mono f32 accumulated from the engine, drained in 960-sample (20 ms) frames.
out_buf: Vec<f32>,
}
impl OpusRtc {
pub fn new() -> Result<Self> {
let mut enc =
opus::Encoder::new(OPUS_RATE, opus::Channels::Stereo, opus::Application::Voip)
.context("opus encoder")?;
enc.set_bitrate(opus::Bitrate::Bits(32000)).ok(); // stereo voice-grade
// Complexity 5 halves encode CPU vs the default 9 for near-identical voice quality —
// the encoder shares a saturated box with everything else, and CPU here is jitter.
enc.set_complexity(5).ok();
let dec = opus::Decoder::new(OPUS_RATE, opus::Channels::Stereo).context("opus decoder")?;
Ok(Self { enc, dec, out_buf: Vec::with_capacity(OPUS_FRAME_48 * 8) })
}
/// Decode one inbound Opus packet → 24 kHz mono f32 (stereo 48 kHz decode → average → ↓2).
pub fn decode_to_24k(&mut self, packet: &[u8]) -> Result<Vec<f32>> {
let mut stereo = vec![0i16; OPUS_FRAME_48 * 6 * 2]; // headroom, interleaved L,R
let n = self.dec.decode(packet, &mut stereo, false).context("opus decode")?; // n = per-channel
stereo.truncate(n * 2);
// interleaved stereo → mono 48 kHz f32
let mono48: Vec<f32> = stereo
.chunks_exact(2)
.map(|lr| (lr[0] as f32 + lr[1] as f32) / 65536.0)
.collect();
Ok(down_48_to_24(&mono48))
}
/// Accept 24 kHz mono f32 from the engine (any length) and return complete 20 ms Opus
/// packets (one per 480 input samples), buffering the remainder for the next call.
pub fn encode_from_24k(&mut self, pcm24: &[f32]) -> Result<Vec<Vec<u8>>> {
self.out_buf.extend_from_slice(&up_24_to_48(pcm24));
let mut packets = Vec::new();
while self.out_buf.len() >= OPUS_FRAME_48 {
// mono → interleaved stereo i16 (duplicate to both channels)
let mut frame = vec![0i16; OPUS_FRAME_48 * 2];
for (i, s) in self.out_buf.drain(..OPUS_FRAME_48).enumerate() {
let q = (s.clamp(-1.0, 1.0) * 32767.0) as i16;
frame[2 * i] = q;
frame[2 * i + 1] = q;
}
let mut out = vec![0u8; 4000];
let len = self.enc.encode(&frame, &mut out).context("opus encode")?;
out.truncate(len);
packets.push(out);
}
Ok(packets)
}
}
/// Linear 2× upsample 24 kHz → 48 kHz (demo-grade; the ratio is exact so a linear kernel is
/// clean between the inserted samples).
pub fn up_24_to_48(x: &[f32]) -> Vec<f32> {
if x.is_empty() {
return Vec::new();
}
let mut y = Vec::with_capacity(x.len() * 2);
for i in 0..x.len() {
let a = x[i];
let b = if i + 1 < x.len() { x[i + 1] } else { a };
y.push(a);
y.push(0.5 * (a + b));
}
y
}
/// 2× downsample 48 kHz → 24 kHz by averaging sample pairs (a 2-tap box filter — demo-grade).
pub fn down_48_to_24(x: &[f32]) -> Vec<f32> {
x.chunks(2).map(|c| if c.len() == 2 { 0.5 * (c[0] + c[1]) } else { c[0] }).collect()
}