Adds Opus 1.6 DRED support end-to-end: encoder embeds 20 ms of ML redundancy in every packet when enabled; decoder recovers lost frames from the next buffered packet's DRED extension rather than falling back to PLC comfort noise. Protocol: bool dred = 11 added to AudioConfig (backward-compatible, defaults false). C ABI: int dred added to vc_audio_config. Encoder: OPUS_SET_DRED_DURATION(2) when dred=true. Decoder: OpusDREDDecoder + per-stream OpusDRED scratch pre-allocated off the RT thread; JitterBuffer::try_copy_front_payload peeks at the next packet without popping on every PLC step; opus_decoder_dred_decode reconstructs the lost frame if DRED data is present, otherwise falls back to PLC. New test: test_dred_toggle (22/22 ctest green). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
128 lines
4.4 KiB
C++
128 lines
4.4 KiB
C++
/*
|
||
* codec/opus_codec.h — Opus encode/decode (libopus 1.6).
|
||
*
|
||
* Design: docs/voice.md §3–4. Per-channel AudioConfig (mono/stereo, bitrate, frame size,
|
||
* FEC, DTX, complexity). The server relays Opus payloads unmodified (no transcode).
|
||
*/
|
||
#ifndef VOICECAT_CODEC_OPUS_CODEC_H
|
||
#define VOICECAT_CODEC_OPUS_CODEC_H
|
||
|
||
#include <cstdint>
|
||
#include <vector>
|
||
|
||
#ifdef VOICECAT_HAS_OPUS
|
||
#include <opus/opus.h>
|
||
#endif
|
||
|
||
namespace voicecat::codec {
|
||
|
||
// Mirrors voicecat::v1::OpusApplication (core/proto/voicecat.proto) without depending on
|
||
// generated protobuf headers from this low-level codec module.
|
||
enum class OpusApplication {
|
||
Voip = 0, // speech, optimized for low-rate intelligibility
|
||
Audio = 1, // music/screen-audio, optimized for fidelity
|
||
LowDelay = 2, // monitoring, minimal algorithmic delay
|
||
};
|
||
|
||
struct OpusParams {
|
||
uint32_t sample_rate = 48000;
|
||
uint32_t bitrate_bps = 24000;
|
||
uint32_t frame_ms = 20;
|
||
bool stereo = false;
|
||
bool fec = true;
|
||
bool dtx = false;
|
||
uint32_t complexity = 10;
|
||
uint32_t expected_packet_loss = 0; // % 0..100
|
||
OpusApplication application = OpusApplication::Voip;
|
||
bool dred = false;
|
||
};
|
||
|
||
// Returns frame_samples for a given sample_rate + frame_ms.
|
||
inline int opus_frame_samples(const OpusParams& p) {
|
||
return static_cast<int>(p.sample_rate / 1000 * p.frame_ms);
|
||
}
|
||
|
||
class OpusEncoder {
|
||
public:
|
||
OpusEncoder() = default;
|
||
~OpusEncoder() { destroy(); }
|
||
|
||
OpusEncoder(const OpusEncoder&) = delete;
|
||
OpusEncoder& operator=(const OpusEncoder&) = delete;
|
||
|
||
// Initialise with the given params. Must be called before encode().
|
||
// Returns true on success; check error_string() on failure.
|
||
bool init(const OpusParams& p);
|
||
|
||
// Encode one frame of PCM (frame_ms ms @ sample_rate Hz, mono or stereo).
|
||
// pcm: interleaved int16 samples (frame_samples * channels samples).
|
||
// out_buf: caller-allocated output buffer (recommend >= 4000 bytes).
|
||
// Returns number of bytes written to out_buf, or -1 on error.
|
||
int encode(const int16_t* pcm, int frame_samples, uint8_t* out_buf, int out_cap);
|
||
|
||
void destroy();
|
||
|
||
bool valid() const { return enc_ != nullptr; }
|
||
int frame_samples()const { return frame_samples_; }
|
||
int channels() const { return channels_; }
|
||
const char* error_string() const { return err_; }
|
||
|
||
private:
|
||
#ifdef VOICECAT_HAS_OPUS
|
||
::OpusEncoder* enc_ = nullptr;
|
||
#else
|
||
void* enc_ = nullptr;
|
||
#endif
|
||
int frame_samples_ = 0;
|
||
int channels_ = 1;
|
||
const char* err_ = nullptr;
|
||
};
|
||
|
||
class OpusDecoder {
|
||
public:
|
||
OpusDecoder() = default;
|
||
~OpusDecoder() { destroy(); }
|
||
|
||
OpusDecoder(const OpusDecoder&) = delete;
|
||
OpusDecoder& operator=(const OpusDecoder&) = delete;
|
||
|
||
// Initialise. Must be called before decode().
|
||
bool init(const OpusParams& p);
|
||
|
||
// Decode one Opus packet into out_pcm (frame_samples * channels int16 samples).
|
||
// opus_data=nullptr, len=0 → PLC (free, always enabled by libopus).
|
||
// fec=true, next valid packet in opus_data → FEC recovery from previous loss.
|
||
// Returns number of samples decoded (= frame_samples), or -1 on error.
|
||
int decode(const uint8_t* opus_data, int len, int16_t* out_pcm, int max_samples,
|
||
bool fec = false);
|
||
|
||
// Decode a lost frame using pre-parsed DRED state from the next received packet.
|
||
// dred_offset=0 means the frame immediately before the next packet.
|
||
// Returns frame_samples on success, -1 if DRED unavailable or decode failed.
|
||
#ifdef VOICECAT_HAS_OPUS
|
||
int decode_dred(::OpusDRED* dred, int32_t dred_offset, int16_t* out_pcm, int max_samples);
|
||
::OpusDecoder* raw() const { return dec_; }
|
||
#endif
|
||
|
||
void destroy();
|
||
|
||
bool valid() const { return dec_ != nullptr; }
|
||
int frame_samples()const { return frame_samples_; }
|
||
int channels() const { return channels_; }
|
||
const char* error_string() const { return err_; }
|
||
|
||
private:
|
||
#ifdef VOICECAT_HAS_OPUS
|
||
::OpusDecoder* dec_ = nullptr;
|
||
#else
|
||
void* dec_ = nullptr;
|
||
#endif
|
||
int frame_samples_ = 0;
|
||
int channels_ = 1;
|
||
const char* err_ = nullptr;
|
||
};
|
||
|
||
} // namespace voicecat::codec
|
||
|
||
#endif // VOICECAT_CODEC_OPUS_CODEC_H
|