telegabber/calls/audio/opus.go
2026-05-25 06:27:20 -07:00

127 lines
3.8 KiB
Go

// bridges PCM frames between an ntgcalls P2P call and pion's opus-over-RTP
package audio
import (
"encoding/binary"
"errors"
"fmt"
"gopkg.in/hraban/opus.v2"
)
// hardcoded for 48kHz / stereo / 20ms / VoIP
const (
SampleRate = 48000
Channels = 2
OpusFrameMs = 20
NtgFrameMs = 10
// samples per channel
SamplesPerOpusFrame = SampleRate * OpusFrameMs / 1000 // 960
SamplesPerNtgFrame = SampleRate * NtgFrameMs / 1000 // 480
// libwebrtc may emit up to 120ms opus frames under load
MaxDecodeSamplesPerChannel = SampleRate * 120 / 1000 // 5760
MaxOpusPacketBytes = 1500
NtgFrameBytes = SamplesPerNtgFrame * Channels * 2 // 1920 (s16 stereo)
OpusInputBytes = SamplesPerOpusFrame * Channels * 2 // 3840 (s16 stereo)
// fallback when remote SDP omits maxaveragebitrate
DefaultBitrate = 128000
)
// thin opus.v2 wrapper fixed to the 48k/stereo/20ms/VoIP shape; fmtp.go
// layers SDP tuning on top. Keeps the CGO opus dep confined to this file.
type Encoder struct {
enc *opus.Encoder
}
func NewEncoder() (*Encoder, error) {
e, err := opus.NewEncoder(SampleRate, Channels, opus.AppVoIP)
if err != nil {
return nil, fmt.Errorf("opus.NewEncoder: %w", err)
}
if err := e.SetBitrate(DefaultBitrate); err != nil {
return nil, fmt.Errorf("opus SetBitrate: %w", err)
}
return &Encoder{enc: e}, nil
}
// one 20ms stereo frame into out; len(pcm) must be SamplesPerOpusFrame*Channels
func (e *Encoder) Encode(pcm []int16, out []byte) (int, error) {
if len(pcm) != SamplesPerOpusFrame*Channels {
return 0, fmt.Errorf("opus encode: pcm len %d, want %d", len(pcm), SamplesPerOpusFrame*Channels)
}
return e.enc.Encode(pcm, out)
}
func (e *Encoder) SetInBandFEC(on bool) error { return e.enc.SetInBandFEC(on) }
func (e *Encoder) SetDTX(on bool) error { return e.enc.SetDTX(on) }
func (e *Encoder) SetBitrate(bps int) error { return e.enc.SetBitrate(bps) }
func (e *Encoder) SetPacketLossPerc(p int) error { return e.enc.SetPacketLossPerc(p) }
func (e *Encoder) SetMaxBandwidth(bw opus.Bandwidth) error {
return e.enc.SetMaxBandwidth(bw)
}
// getters for tests asserting fmtp took effect
func (e *Encoder) InBandFEC() (bool, error) { return e.enc.InBandFEC() }
func (e *Encoder) DTX() (bool, error) { return e.enc.DTX() }
func (e *Encoder) Bitrate() (int, error) { return e.enc.Bitrate() }
func (e *Encoder) PacketLossPerc() (int, error) { return e.enc.PacketLossPerc() }
func (e *Encoder) MaxBandwidth() (opus.Bandwidth, error) { return e.enc.MaxBandwidth() }
type Decoder struct {
dec *opus.Decoder
}
func NewDecoder() (*Decoder, error) {
d, err := opus.NewDecoder(SampleRate, Channels)
if err != nil {
return nil, fmt.Errorf("opus.NewDecoder: %w", err)
}
return &Decoder{dec: d}, nil
}
// one opus packet -> interleaved s16 stereo PCM; out sized for max frame
func (d *Decoder) Decode(data []byte, out []int16) (int, error) {
if len(data) == 0 {
return 0, errors.New("opus decode: empty packet")
}
return d.dec.Decode(data, out)
}
// one PLC frame; out sized to the last successful Decode's frame
func (d *Decoder) DecodePLC(out []int16) error {
return d.dec.DecodePLC(out)
}
// SDP maxplaybackrate -> opus.Bandwidth (RFC 7587 §6.1)
func BandwidthForPlaybackRate(hz int) opus.Bandwidth {
switch {
case hz <= 8000:
return opus.Narrowband
case hz <= 12000:
return opus.Mediumband
case hz <= 16000:
return opus.Wideband
case hz <= 24000:
return opus.SuperWideband
default:
return opus.Fullband
}
}
// interleaved s16-LE bytes <-> int16 samples; dst must be sized correctly
func PCMBytesToInt16(src []byte, dst []int16) {
for i := range dst {
dst[i] = int16(binary.LittleEndian.Uint16(src[i*2:]))
}
}
func PCMInt16ToBytes(src []int16, dst []byte) {
for i, s := range src {
binary.LittleEndian.PutUint16(dst[i*2:], uint16(s))
}
}