// 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)) } }