A collection of VoIP audio codecs implemented for Rust. This crate provides a unified interface for encoding and decoding various audio formats commonly used in SIP, VoIP, and WebRTC applications.
| Codec | Implementation | Feature | no_std |
|---|---|---|---|
| G.711 (PCMA/PCMU) | Pure Rust | Built-in | yes (heap-free) |
| G.722 | Pure Rust | Built-in | yes (heap-free) |
| G.729 | Pure Rust (g729-sys) |
g729 (on by default) |
yes (heap-free) |
| Opus | Pure Rust (opus-rs) |
opus (on by default) |
yes (needs alloc) |
| Telephone Event | RFC 4733 | Built-in | yes (heap-free) |
| Resampler | Polyphase FIR | Built-in | yes (heap-free) |
- Unified API: Simple
EncoderandDecodertraits for all codecs. no_stdsupport: every codec runs withoutstd. G.711/G.722/G.729, the resampler and telephone-event are fully heap-free (no allocator needed); Opus needs analloc(its internal working set is ~250 KB and is boxed).- Resampler: Built-in audio resampling utility.
- Packet loss concealment:
Decoder::conceal_intofills in for a lost packet,Decoder::recover_intorebuilds it from Opus in-band FEC. Opus and G.729 use their own concealment; G.711 and G.722 get pitch-repetition concealment (G.711 Appendix I style), with G.722's ADPCM state carried through the loss, behind theplcfeature andenable_concealment().CodecType::is_silence_frametells DTX silence from loss.
Measured on Apple M2 Pro (processing 20ms audio frames):
| Codec | Encode (20ms) | Decode (20ms) | Rate |
|---|---|---|---|
| PCMU | ~50.09 ns | ~59.73 ns | 8kHz |
| PCMA | ~50.23 ns | ~59.63 ns | 8kHz |
| G.722 | ~5.02 µs | ~3.82 µs | 16kHz |
| G.729 | ~20.50 µs | ~6.16 µs | 8kHz |
| Opus | ~52.34 µs | ~23.19 µs | 48kHz |
Note: Benchmarks were run with cargo bench --bench codec_bench (Criterion). create_encoder(CodecType::Opus) currently uses the default Opus profile: 48kHz, stereo, Application::Audio, bitrate 64kbps, complexity 5.
Add this to your Cargo.toml:
[dependencies]
audio-codec = "0.4" # Opus and G.729 are enabled by defaultTo opt out of Opus and G.729 (smaller build, no alloc needed):
[dependencies]
audio-codec = { version = "0.4", default-features = false, features = ["std"] }Opt out of only one of them by keeping the default features and disabling the one you don't need:
[dependencies]
audio-codec = { version = "0.4", default-features = false, features = ["std", "g729"] } # no Opus
audio-codec = { version = "0.4", default-features = false, features = ["std", "opus"] } # no G.729use audio_codec::{create_decoder, CodecType};
fn main() {
let mut decoder = create_decoder(CodecType::PCMA);
let encoded_data: Vec<u8> = vec![/* ... */];
let pcm_samples = decoder.decode(&encoded_data);
println!("Decoded {} samples", pcm_samples.len());
}use audio_codec::{create_encoder, CodecType, Encoder};
fn main() {
let mut encoder = create_encoder(CodecType::G722);
let pcm_samples: Vec<i16> = vec![0; 320]; // 20ms @ 16kHz mono
let encoded_data = encoder.encode(&pcm_samples);
println!("Encoded into {} bytes", encoded_data.len());
}use audio_codec::{
create_opus_decoder, create_opus_encoder, Decoder, Encoder,
opus::OpusApplication,
};
fn main() {
// Explicit Opus encoder/decoder creation
// 48kHz stereo, Audio mode
let mut encoder = create_opus_encoder(48_000, 2, OpusApplication::Audio);
let mut decoder = create_opus_decoder(48_000, 2);
let pcm_samples: Vec<i16> = vec![0; 960 * 2]; // 20ms @ 48kHz, interleaved stereo
let encoded_data = encoder.encode(&pcm_samples);
let decoded_pcm = decoder.decode(&encoded_data);
println!("Encoded into {} bytes", encoded_data.len());
println!("Decoded {} samples", decoded_pcm.len());
}When a packet is lost, ask the decoder for audio to play in its place
instead of leaving a gap. Call conceal_into (or conceal) once per lost
packet, in order, between the decode calls of the packets around it.
use audio_codec::{create_decoder, CodecType, Decoder};
fn main() {
let mut decoder = create_decoder(CodecType::G722);
// off by default; G.711 and G.722 also need the `plc` feature
if decoder.enable_concealment() {
let concealed = decoder.conceal(320); // one 20 ms packet @ 16kHz
println!("Concealed {} samples", concealed.len());
}
}- Opus uses its own concealment, always available, faded through a
long loss like G.711's (full level for 10 ms, silence at 60 ms): on its
own it carries on near the level of the audio before the loss. The next
packet decoded is ramped back in. With the packet after the loss already
at hand,
recover_into(next, ..)(orrecover) rebuilds the lost one from its in-band FEC instead, where the sender coded it (OpusEncoder::set_inband_fecwithset_packet_loss_perc); for other codecs it conceals. - G.711 / G.722 repeat the last pitch period of the decoded audio,
continuing the waveform, fading to silence 60 ms into a loss, and
crossfade into the next decoded packet. G.722 also carries its ADPCM
state along the concealment (as G.722 Appendices III/IV do), so the
packets after the loss decode closer to what the sender encoded.
Enabling it boxes a ~1.7 KB history per decoder and copies each decoded
packet into it; with the
plcfeature off, or not enabled, the decoders are unchanged. - G.729 uses its own frame erasure concealment (G.729 §4.4), always
available: the last spectral envelope and pitch carry on with their
gains attenuated, and the decoder's predictors stay in step, so the
packets after the loss decode close to what the sender encoded (skipping
the gap instead leaves them with more error than signal for the next
50 ms or so). It's faded like the others, and in DTX, after a SID frame,
it carries on the comfort noise. SID frames (Annex B) decode to 10 ms of
comfort noise.
conceal_intotakes multiples of 10 ms. - Telephone Event has none:
conceal_intoreturnsOk(0)and the caller conceals the gap itself.
Not every missing packet is lost: a sender in discontinuous transmission
(DTX) goes quiet with a silence frame and sends nothing more until speech
resumes. CodecType::is_silence_frame(payload) tells those apart (Opus
DTX packets, G.729 Annex B SID frames), so a jitter buffer can play the
silence that follows as silence instead of concealing it.
The crate works on bare-metal targets with no std. G.711/G.722/G.729, the
resampler and telephone-event are additionally heap-free (no allocator);
Opus needs an alloc because its encoder/decoder working set (~250 KB) is
boxed. Disable the default std feature and use the slice-based *_into API:
[dependencies.audio-codec]
version = "0.4"
default-features = falseuse audio_codec::{Encoder, Decoder, CodecError, g722};
fn g722_roundtrip(
encoder: &mut g722::G722Encoder,
decoder: &mut g722::G722Decoder,
pcm: &[i16],
) -> Result<usize, CodecError> {
// Caller-provided scratch buffers — no allocation.
let mut enc_buf = [0u8; 160]; // 20ms @ 16kHz = 160 bytes
let mut dec_buf = [0i16; 320];
let n = encoder.encode_into(pcm, &mut enc_buf)?;
let m = decoder.decode_into(&enc_buf[..n], &mut dec_buf)?;
Ok(m)
}Use max_encode_bytes / max_decode_samples to size the buffers correctly:
let needed_bytes = encoder.max_encode_bytes(pcm.len());
let needed_samples = decoder.max_decode_samples(n_bytes);Resampler borrows a caller-provided coefficient buffer (~24 KB). The default
f32 core tracks the read position on an exact integer tick grid, so repeated
20 ms frames keep a constant output count (8 kHz → 48 kHz is always 960
samples, never 959/961):
use audio_codec::resampler::{Resampler, COEFFS_LEN};
let mut coeffs: [f32; COEFFS_LEN] = [0.0; COEFFS_LEN];
let mut r = Resampler::new(8000, 16000, &mut coeffs)?;
let mut out = [0i16; 160];
let n = r.resample_into(&input[..80], &mut out)?;This is the default f32 polyphase core. Targets without an FPU (e.g.
ESP32-S3) can use the Q15 fixed-point core instead via
audio_codec::resampler_fixed — it borrows i16 coefficients
(COEFFS_LEN = 6144, 12 KB) and is ~20× faster where soft-float is used,
but it is not the default.
Opus also works without std (it needs an alloc — the encoder/decoder are
boxed). Use the slice-based *_into API exactly like the other codecs:
use audio_codec::{Encoder, Decoder, opus::{OpusEncoder, OpusDecoder}};
// 48 kHz, mono. The structs are boxed internally, so this only needs `alloc`.
let mut encoder = OpusEncoder::new(48_000, 1);
let mut decoder = OpusDecoder::new(48_000, 1);
let pcm: &[i16] = /* 20 ms @ 48 kHz = 960 mono samples */;
let mut packet = [0u8; 1275];
let n = encoder.encode_into(pcm, &mut packet)?;
let mut out = [0i16; 960];
let m = decoder.decode_into(&packet[..n], &mut out)?;| Codec | Status |
|---|---|
| G.711 (PCMA/PCMU), G.722, G.729, Telephone Event | ✅ fully heap-free (no allocator) |
| Resampler | ✅ fully heap-free (borrowed coeffs buffer) |
| Opus | ✅ supported (needs alloc; encoder/decoder are boxed) |
The crate is verified to compile on thumbv7em-none-eabi and other bare-metal
targets. Float math (resampler + Opus) goes through libm when std is off.
This project is licensed under the MIT License.