Skip to content

Commit 523e833

Browse files
committed
feat: add Opus codec support
1 parent 9f9df59 commit 523e833

10 files changed

Lines changed: 327 additions & 11 deletions

File tree

Cargo.toml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,14 @@ categories = ["network-programming", "multimedia"]
1414
vad_webrtc = ["webrtc-vad"]
1515
vad_silero = ["ort", "ort-sys"]
1616
vad_ten = ["ort", "ort-sys"]
17-
default = ["vad_webrtc", "vad_silero", "vad_ten"]
17+
opus = ["dep:opus"]
18+
default = ["vad_webrtc", "vad_silero", "vad_ten", "opus"]
1819
not_vad = []
1920

2021
[dependencies]
2122
anyhow = "1.0.98"
2223
async-trait = "0.1.88"
24+
opus = { version = "0.3", optional = true }
2325
axum = { version = "0.8.3", features = ["ws", "tokio"] }
2426
tower-http = { version = "0.6.2", features = ["fs", "cors"] }
2527
bytes = "1.10.1"
@@ -43,7 +45,7 @@ uuid = { version = "1.16.0", features = ["v4"] }
4345
webrtc = "0.13.0"
4446
webrtc-vad = { version = "0.4.0", optional = true }
4547
clap = { version = "4.5", features = ["derive"] }
46-
toml = "0.8"
48+
toml = "0.9.2"
4749
rand = "0.9.0"
4850
hound = "3.5.1"
4951
ort = { version = "2.0.0-rc.10", features = ["ndarray"], optional = true }

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
FROM rust:bookworm AS rust-builder
2-
RUN apt-get update && apt-get install -y libasound2-dev
2+
RUN apt-get update && apt-get install -y libasound2-dev libopus-dev
33
RUN mkdir /build
44
ADD . /build/
55
WORKDIR /build

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,17 @@ RustPBX is a high-performance, secure software-defined PBX (Private Branch Excha
3737
### Prerequisites
3838
- Rust 1.75 or later
3939
- Cargo package manager
40+
- opus, alsa
41+
42+
Linux:
43+
```bash
44+
apt-get install -y libasound2-dev libopus-dev
45+
```
46+
47+
Mac:
48+
```bash
49+
brew install opus
50+
```
4051

4152
### Installation
4253
```bash

src/media/codecs/mod.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
use crate::{PcmBuf, Sample};
22
pub mod g722;
3+
#[cfg(feature = "opus")]
4+
pub mod opus;
35
pub mod pcma;
46
pub mod pcmu;
57
pub mod resample;
@@ -11,6 +13,8 @@ pub enum CodecType {
1113
PCMU,
1214
PCMA,
1315
G722,
16+
#[cfg(feature = "opus")]
17+
Opus,
1418
TelephoneEvent,
1519
}
1620

@@ -41,6 +45,8 @@ pub fn create_decoder(codec: CodecType) -> Box<dyn Decoder> {
4145
CodecType::PCMU => Box::new(pcmu::PcmuDecoder::new()),
4246
CodecType::PCMA => Box::new(pcma::PcmaDecoder::new()),
4347
CodecType::G722 => Box::new(g722::G722Decoder::new()),
48+
#[cfg(feature = "opus")]
49+
CodecType::Opus => Box::new(opus::OpusDecoder::new_default()),
4450
CodecType::TelephoneEvent => Box::new(telephone_event::TelephoneEventDecoder::new()),
4551
}
4652
}
@@ -50,6 +56,8 @@ pub fn create_encoder(codec: CodecType) -> Box<dyn Encoder> {
5056
CodecType::PCMU => Box::new(pcmu::PcmuEncoder::new()),
5157
CodecType::PCMA => Box::new(pcma::PcmaEncoder::new()),
5258
CodecType::G722 => Box::new(g722::G722Encoder::new()),
59+
#[cfg(feature = "opus")]
60+
CodecType::Opus => Box::new(opus::OpusEncoder::new_default()),
5361
CodecType::TelephoneEvent => Box::new(telephone_event::TelephoneEventEncoder::new()),
5462
}
5563
}
@@ -60,6 +68,8 @@ impl CodecType {
6068
CodecType::PCMU => "audio/PCMU",
6169
CodecType::PCMA => "audio/PCMA",
6270
CodecType::G722 => "audio/G722",
71+
#[cfg(feature = "opus")]
72+
CodecType::Opus => "audio/opus",
6373
CodecType::TelephoneEvent => "audio/telephone-event",
6474
}
6575
}
@@ -68,6 +78,8 @@ impl CodecType {
6878
CodecType::PCMU => "PCMU/8000",
6979
CodecType::PCMA => "PCMA/8000",
7080
CodecType::G722 => "G722/16000",
81+
#[cfg(feature = "opus")]
82+
CodecType::Opus => "opus/48000",
7183
CodecType::TelephoneEvent => "telephone-event/8000",
7284
}
7385
}
@@ -77,6 +89,8 @@ impl CodecType {
7789
CodecType::PCMU => 8000,
7890
CodecType::PCMA => 8000,
7991
CodecType::G722 => 8000,
92+
#[cfg(feature = "opus")]
93+
CodecType::Opus => 48000,
8094
CodecType::TelephoneEvent => 8000,
8195
}
8296
}
@@ -85,6 +99,7 @@ impl CodecType {
8599
CodecType::PCMU => 0,
86100
CodecType::PCMA => 8,
87101
CodecType::G722 => 9,
102+
CodecType::Opus => 111, // Dynamic payload type
88103
CodecType::TelephoneEvent => 101,
89104
}
90105
}
@@ -93,12 +108,16 @@ impl CodecType {
93108
CodecType::PCMU => 8000,
94109
CodecType::PCMA => 8000,
95110
CodecType::G722 => 16000,
111+
#[cfg(feature = "opus")]
112+
CodecType::Opus => 48000,
96113
CodecType::TelephoneEvent => 8000,
97114
}
98115
}
99116
pub fn is_audio(&self) -> bool {
100117
match self {
101118
CodecType::PCMU | CodecType::PCMA | CodecType::G722 => true,
119+
#[cfg(feature = "opus")]
120+
CodecType::Opus => true,
102121
_ => false,
103122
}
104123
}
@@ -112,6 +131,8 @@ impl TryFrom<&String> for CodecType {
112131
"0" => Ok(CodecType::PCMU),
113132
"8" => Ok(CodecType::PCMA),
114133
"9" => Ok(CodecType::G722),
134+
#[cfg(feature = "opus")]
135+
"111" => Ok(CodecType::Opus), // Dynamic payload type
115136
"101" => Ok(CodecType::TelephoneEvent),
116137
_ => Err(anyhow::anyhow!("Invalid codec type: {}", value)),
117138
}

src/media/codecs/opus.rs

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
use super::{Decoder, Encoder};
2+
use crate::{PcmBuf, Sample};
3+
use opus::{Application, Channels, Decoder as OpusDecoderCore, Encoder as OpusEncoderCore};
4+
5+
/// Opus audio decoder
6+
pub struct OpusDecoder {
7+
decoder: OpusDecoderCore,
8+
sample_rate: u32,
9+
channels: u16,
10+
}
11+
12+
impl OpusDecoder {
13+
/// Create a new Opus decoder instance
14+
pub fn new(sample_rate: u32, channels: u16) -> Self {
15+
let channels = if channels == 1 {
16+
Channels::Mono
17+
} else {
18+
Channels::Stereo
19+
};
20+
21+
let decoder = match OpusDecoderCore::new(sample_rate, channels) {
22+
Ok(decoder) => decoder,
23+
Err(e) => {
24+
panic!("Failed to create Opus decoder: {}", e);
25+
}
26+
};
27+
28+
Self {
29+
decoder,
30+
sample_rate,
31+
channels: if matches!(channels, Channels::Mono) {
32+
1
33+
} else {
34+
2
35+
},
36+
}
37+
}
38+
39+
/// Create a default Opus decoder (48kHz, mono)
40+
pub fn new_default() -> Self {
41+
Self::new(48000, 1)
42+
}
43+
}
44+
45+
unsafe impl Send for OpusDecoder {}
46+
unsafe impl Sync for OpusDecoder {}
47+
48+
impl Decoder for OpusDecoder {
49+
fn decode(&mut self, data: &[u8]) -> PcmBuf {
50+
// Allocate output buffer - Opus can decode up to 120ms of audio
51+
// 48kHz * 0.12s * 2(stereo) = 11520 samples
52+
let max_samples = 11520;
53+
let mut output = vec![0i16; max_samples];
54+
55+
match self.decoder.decode(data, &mut output, false) {
56+
Ok(len) => {
57+
output.truncate(len);
58+
output
59+
}
60+
Err(_) => {
61+
// If decoding fails, return empty buffer
62+
vec![]
63+
}
64+
}
65+
}
66+
67+
fn sample_rate(&self) -> u32 {
68+
self.sample_rate
69+
}
70+
71+
fn channels(&self) -> u16 {
72+
self.channels
73+
}
74+
}
75+
76+
/// Opus audio encoder
77+
pub struct OpusEncoder {
78+
encoder: OpusEncoderCore,
79+
sample_rate: u32,
80+
channels: u16,
81+
}
82+
83+
impl OpusEncoder {
84+
/// Create a new Opus encoder instance
85+
pub fn new(sample_rate: u32, channels: u16) -> Self {
86+
let channels_enum = if channels == 1 {
87+
Channels::Mono
88+
} else {
89+
Channels::Stereo
90+
};
91+
92+
let encoder = match OpusEncoderCore::new(sample_rate, channels_enum, Application::Voip) {
93+
Ok(encoder) => encoder,
94+
Err(e) => {
95+
panic!("Failed to create Opus encoder: {}", e);
96+
}
97+
};
98+
99+
Self {
100+
encoder,
101+
sample_rate,
102+
channels,
103+
}
104+
}
105+
106+
/// Create a default Opus encoder (48kHz, mono)
107+
pub fn new_default() -> Self {
108+
Self::new(48000, 1)
109+
}
110+
}
111+
112+
unsafe impl Send for OpusEncoder {}
113+
unsafe impl Sync for OpusEncoder {}
114+
115+
impl Encoder for OpusEncoder {
116+
fn encode(&mut self, samples: &[Sample]) -> Vec<u8> {
117+
// Allocate output buffer - Opus encoded data is typically smaller than raw data
118+
let mut output = vec![0u8; samples.len()];
119+
120+
match self.encoder.encode(samples, &mut output) {
121+
Ok(len) => {
122+
output.truncate(len);
123+
output
124+
}
125+
Err(_) => {
126+
// If encoding fails, return empty buffer
127+
vec![]
128+
}
129+
}
130+
}
131+
132+
fn sample_rate(&self) -> u32 {
133+
self.sample_rate
134+
}
135+
136+
fn channels(&self) -> u16 {
137+
self.channels
138+
}
139+
}

src/media/codecs/tests.rs

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,3 +373,89 @@ fn test_pcma_edge_cases() {
373373
println!("Original: {:?}", samples);
374374
println!("Decoded: {:?}", decoded);
375375
}
376+
377+
#[cfg(feature = "opus")]
378+
#[test]
379+
fn test_opus_codec() {
380+
use super::opus::{OpusDecoder, OpusEncoder};
381+
use super::{Decoder, Encoder};
382+
383+
// Create encoder and decoder
384+
let mut encoder = OpusEncoder::new_default();
385+
let mut decoder = OpusDecoder::new_default();
386+
387+
// Create test audio data (48kHz mono, 20ms = 960 samples)
388+
let samples_per_frame = 960;
389+
let input_samples: Vec<i16> = (0..samples_per_frame)
390+
.map(|i| (1000.0 * (2.0 * std::f64::consts::PI * 440.0 * i as f64 / 48000.0).sin()) as i16)
391+
.collect();
392+
393+
// Encode
394+
let encoded = encoder.encode(&input_samples);
395+
assert!(!encoded.is_empty(), "Encoded data should not be empty");
396+
397+
// Decode
398+
let decoded = decoder.decode(&encoded);
399+
assert!(!decoded.is_empty(), "Decoded data should not be empty");
400+
401+
// Verify sample rate and channels
402+
assert_eq!(encoder.sample_rate(), 48000);
403+
assert_eq!(encoder.channels(), 1);
404+
assert_eq!(decoder.sample_rate(), 48000);
405+
assert_eq!(decoder.channels(), 1);
406+
}
407+
408+
#[cfg(feature = "opus")]
409+
#[test]
410+
fn test_opus_encode_decode() {
411+
use super::opus::{OpusDecoder, OpusEncoder};
412+
413+
// Test different configurations
414+
let configs = vec![
415+
(48000, 1), // 48kHz mono
416+
(48000, 2), // 48kHz stereo
417+
(16000, 1), // 16kHz mono
418+
(8000, 1), // 8kHz mono
419+
];
420+
421+
for (sample_rate, channels) in configs {
422+
let mut encoder = OpusEncoder::new(sample_rate, channels);
423+
let mut decoder = OpusDecoder::new(sample_rate, channels);
424+
425+
// Create test data
426+
let frame_size = (sample_rate / 50) as usize * channels as usize; // 20ms frame
427+
let input_samples: Vec<i16> = (0..frame_size)
428+
.map(|i| {
429+
(500.0 * (2.0 * std::f64::consts::PI * 440.0 * i as f64 / sample_rate as f64).sin())
430+
as i16
431+
})
432+
.collect();
433+
434+
// Encode
435+
let encoded = encoder.encode(&input_samples);
436+
assert!(
437+
!encoded.is_empty(),
438+
"Encoded data should not be empty for {}Hz {}ch",
439+
sample_rate,
440+
channels
441+
);
442+
443+
// Decode
444+
let decoded = decoder.decode(&encoded);
445+
assert!(
446+
!decoded.is_empty(),
447+
"Decoded data should not be empty for {}Hz {}ch",
448+
sample_rate,
449+
channels
450+
);
451+
452+
println!(
453+
"✓ Opus {}Hz {}ch: {} samples -> {} bytes -> {} samples",
454+
sample_rate,
455+
channels,
456+
input_samples.len(),
457+
encoded.len(),
458+
decoded.len()
459+
);
460+
}
461+
}

0 commit comments

Comments
 (0)