feat(audio): channel sample_rate caps Opus bandwidth (narrowband/wideband)
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:
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -946,10 +946,12 @@ voicecat::codec::OpusParams opus_params_from_audio_config(const voicecat::v1::Au
|
||||
voicecat::codec::OpusParams p;
|
||||
// Opus always runs at 48 kHz internally (docs/voice.md §3): the whole AudioEngine clock is
|
||||
// 48 kHz and external PCM is fed at 48 kHz, so the codec must match regardless of what a
|
||||
// channel advertises. Honoring a non-48k effective sample_rate here would create an encoder
|
||||
// expecting e.g. 16k PCM while being fed 48k frames — wrong pitch/duration. The wire
|
||||
// sample_rate field mainly tags narrowband intent; Opus handles that via bitrate at 48k.
|
||||
// channel advertises. Honoring a non-48k effective sample_rate as the codec rate would
|
||||
// create an encoder expecting e.g. 16k PCM while being fed 48k frames — wrong pitch/duration.
|
||||
// Instead the channel's sample_rate is carried as max_bandwidth_hz and caps the encoder's
|
||||
// selected audio bandwidth (narrowband/wideband/…) — a low-bitrate room still benefits.
|
||||
p.sample_rate = 48000;
|
||||
p.max_bandwidth_hz = a.sample_rate(); // 0 = unset → full band
|
||||
p.bitrate_bps = a.bitrate_bps() ? a.bitrate_bps() : 24000;
|
||||
p.frame_ms = a.frame_ms() ? a.frame_ms() : 20;
|
||||
p.stereo = (a.mode() == voicecat::v1::MODE_STEREO);
|
||||
@@ -1524,7 +1526,9 @@ vc_result vc_client::get_stream_audio_config(uint32_t user_id, uint32_t stream_i
|
||||
const auto& p = ls->effective_params;
|
||||
out->codec = 0;
|
||||
out->mode = p.stereo ? 1u : 0u;
|
||||
out->sample_rate = p.sample_rate;
|
||||
// Report the channel's configured sample_rate (carried as max_bandwidth_hz), not the
|
||||
// fixed 48 kHz codec clock — matches what the remote-stream path below reports.
|
||||
out->sample_rate = p.max_bandwidth_hz ? p.max_bandwidth_hz : p.sample_rate;
|
||||
out->bitrate_bps = p.bitrate_bps;
|
||||
out->frame_ms = p.frame_ms;
|
||||
out->application = static_cast<uint32_t>(p.application);
|
||||
|
||||
Reference in New Issue
Block a user