feat(audio): channel sample_rate caps Opus bandwidth (narrowband/wideband)
Some checks failed
Build Linux Binaries / linux/amd64 (push) Has been cancelled
Build Linux Binaries / linux/arm64 (push) Has been cancelled

The per-channel sample_rate field was inert after pinning the codec to
48 kHz. Make it meaningful without changing the 48 kHz clock: carry it as
OpusParams::max_bandwidth_hz and apply OPUS_SET_MAX_BANDWIDTH in
OpusEncoder::init (8000->narrowband, 16000->wideband, 24000->super-wideband,
48000->full). A low-bitrate room can now shed out-of-band content while
every endpoint keeps a single 48 kHz clock.

Make sample_rate channel-authoritative on the server: conn_session no
longer overrides effective sample_rate with the client's always-48000
request (it now behaves like frame_ms/mode). vc_get_stream_audio_config
reports the channel's configured rate for own streams too.

New ctest channel_samplerate: a 7 kHz tone is attenuated ~1000x on an
8 kHz (narrowband) channel vs a 48 kHz (full-band) channel, proving the cap
is in effect. ctest --preset dev 26/26.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-22 16:56:49 +02:00
parent a460009a2f
commit e155e342f4
9 changed files with 379 additions and 15 deletions

View File

@@ -4,6 +4,19 @@ namespace voicecat::codec {
#ifdef VOICECAT_HAS_OPUS
// Map an intended channel/capture sample rate (Hz) to the Opus max-bandwidth constant. The
// codec always runs at 48 kHz internally (docs/voice.md §3); this caps the bandwidth the
// encoder will select so a channel can request narrowband/wideband audio for low-bitrate rooms.
// 0 (unset) and >= 48000 map to FULLBAND, which is the encoder default (i.e. a no-op cap).
static opus_int32 opus_max_bandwidth_for(uint32_t rate_hz) {
if (rate_hz == 0) return OPUS_BANDWIDTH_FULLBAND;
if (rate_hz <= 8000) return OPUS_BANDWIDTH_NARROWBAND; // ~4 kHz audio
if (rate_hz <= 12000) return OPUS_BANDWIDTH_MEDIUMBAND; // ~6 kHz
if (rate_hz <= 16000) return OPUS_BANDWIDTH_WIDEBAND; // ~8 kHz
if (rate_hz <= 24000) return OPUS_BANDWIDTH_SUPERWIDEBAND; // ~12 kHz
return OPUS_BANDWIDTH_FULLBAND; // ~20 kHz
}
// ── OpusEncoder ──────────────────────────────────────────────────────────────
bool OpusEncoder::init(const OpusParams& p) {
@@ -27,6 +40,7 @@ bool OpusEncoder::init(const OpusParams& p) {
}
opus_encoder_ctl(enc_, OPUS_SET_BITRATE(static_cast<opus_int32>(p.bitrate_bps)));
opus_encoder_ctl(enc_, OPUS_SET_MAX_BANDWIDTH(opus_max_bandwidth_for(p.max_bandwidth_hz)));
opus_encoder_ctl(enc_, OPUS_SET_COMPLEXITY(static_cast<opus_int32>(p.complexity)));
opus_encoder_ctl(enc_, OPUS_SET_INBAND_FEC(p.fec ? 1 : 0));
opus_encoder_ctl(enc_, OPUS_SET_DTX(p.dtx ? 1 : 0));

View File

@@ -26,6 +26,11 @@ enum class OpusApplication {
struct OpusParams {
uint32_t sample_rate = 48000;
// Intended channel/capture sample rate (Hz), used ONLY to cap the encoder's audio bandwidth
// (narrowband/wideband/…) via OPUS_SET_MAX_BANDWIDTH. The codec always runs at 48 kHz
// internally (docs/voice.md §3); this lets a low-bitrate channel constrain encoded bandwidth
// without changing the PCM clock. 0 = unset → full band. Decoder ignores it.
uint32_t max_bandwidth_hz = 0;
uint32_t bitrate_bps = 24000;
uint32_t frame_ms = 20;
bool stereo = false;