Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 98 additions & 2 deletions encoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ package opus
import (
"encoding/binary"
"fmt"
"math"

"github.com/pion/opus/internal/celt"
"github.com/pion/opus/internal/silk"
)

// Application selects the encoder's tuning profile, mirroring libopus's
Expand Down Expand Up @@ -52,9 +54,31 @@ const (
// is separate (bit 2 of the TOC) and not part of this constant.
const celtOnlyFullband20msConfig = 31

// SILK-only, 20 ms TOC config numbers per RFC 6716 Table 2, one per bandwidth
// EncodeSILK supports.
const (
silkOnlyNarrowband20msConfig = 1
silkOnlyMediumband20msConfig = 5
silkOnlyWideband20msConfig = 9
)

// silkComplexityInterpolationThreshold is the encoder complexity at or above
// which SILK's NLSF interpolation search is enabled, mirroring libopus's
// silk_setup_complexity (control_codec.c): useInterpolatedNLSFs is 0 below
// complexity 4, 1 from 4 up.
const silkComplexityInterpolationThreshold = 4

// silkDCBlockCutoffHz is the high-pass cutoff EncodeSILK applies to its input
// before handing it to internal/silk. libopus applies this filter (dc_reject,
// src/opus_encoder.c:479-507) to the shared PCM ahead of both the SILK and
// CELT encoders, not inside silk/ itself — so this mirrors celt's
// dcBlockCutoffHz rather than living in internal/silk.
const silkDCBlockCutoffHz = 3.0

// Encoder encodes PCM into Opus packets.
type Encoder struct {
celtEncoder celt.Encoder
silkEncoder silk.Encoder
sampleRate int
channels int
bitrate int
Expand All @@ -65,6 +89,7 @@ type Encoder struct {
lossRate int
bandwidth Bandwidth
maxBandwidth Bandwidth
silkDCBlockMem float32
}

// EncoderOption configures an Encoder during construction.
Expand Down Expand Up @@ -208,11 +233,12 @@ func WithMaxBandwidth(bw Bandwidth) EncoderOption {
//
// Defaults: 48 kHz, mono, 24 kbit/s, complexity 5. Pass options to override
// any of these. The current implementation supports 48 kHz, 1 or 2 channels,
// 20 ms CELT-only packets. Transient detection and SILK encoding will land
// in follow-up PRs.
// 20 ms CELT-only packets, plus SILK-only encoding via EncodeSILK. Transient
// detection is a follow-up.
func NewEncoder(opts ...EncoderOption) (*Encoder, error) {
encoder := &Encoder{
celtEncoder: celt.NewEncoder(),
silkEncoder: silk.NewEncoder(),
sampleRate: celtSampleRate,
channels: 1,
bitrate: defaultBitrate,
Expand All @@ -235,6 +261,7 @@ func NewEncoder(opts ...EncoderOption) (*Encoder, error) {
encoder.celtEncoder.SetConstrainedVBR(encoder.constrainedVBR)
encoder.celtEncoder.SetLossRate(encoder.lossRate)
encoder.celtEncoder.SetComplexity(encoder.complexity)
encoder.silkEncoder.SetUseInterpolatedNLSFs(encoder.complexity >= silkComplexityInterpolationThreshold)

return encoder, nil
}
Expand All @@ -251,6 +278,7 @@ func (e *Encoder) SetComplexity(complexity int) error {
return err
}
e.celtEncoder.SetComplexity(complexity)
e.silkEncoder.SetUseInterpolatedNLSFs(complexity >= silkComplexityInterpolationThreshold)

return nil
}
Expand Down Expand Up @@ -378,6 +406,74 @@ func (e *Encoder) EncodeFloat32(in []float32, out []byte) (int, error) {
return 1 + n, nil
}

// EncodeSILK encodes one 20 ms mono SILK frame into a SILK-only Opus packet.
// pcm must hold exactly one 20 ms frame of mono s16 samples at the
// bandwidth's internal rate: 160 (Narrowband/8 kHz), 240 (Mediumband/12 kHz),
// or 320 (Wideband/16 kHz) samples. This is a separate entry point from
// Encode/EncodeFloat32 — bitrate-based auto-selection always picks CELT
// bandwidths (Wideband and up); SILK is for callers who specifically want a
// SILK-only voice packet (VoIP/narrowband use cases), not an automatic
// CELT/SILK/hybrid switch. Superwideband and Fullband aren't SILK
// bandwidths and are rejected. Applies a fixed DC-removal high-pass before
// encoding (libopus's dc_reject applied to the shared PCM path); the
// pitch-adaptive VoIP cutoff (hp_cutoff) is not implemented. Covers
// voiced/LTP prediction, noise shaping and NLSF interpolation (see
// internal/silk); the delayed-decision NSQ, stereo, hybrid mode, and the
// bitrate-control loop are not yet implemented.
func (e *Encoder) EncodeSILK(pcm []int16, bandwidth Bandwidth, out []byte) (int, error) {
var config int
switch bandwidth {
case BandwidthNarrowband:
config = silkOnlyNarrowband20msConfig
case BandwidthMediumband:
config = silkOnlyMediumband20msConfig
case BandwidthWideband:
config = silkOnlyWideband20msConfig
default:
return 0, fmt.Errorf("%w: %d", errInvalidBandwidth, bandwidth)
}

want := bandwidth.SampleRate() / 50 // 20 ms
if len(pcm) != want {
return 0, fmt.Errorf("%w: got %d samples, want %d", errInvalidFrameSize, len(pcm), want)
}

filtered := applySILKDCBlock(pcm, bandwidth.SampleRate(), &e.silkDCBlockMem)
payload := e.silkEncoder.Encode(filtered, silk.Bandwidth(bandwidth), e.bitrate)
if len(out) < len(payload)+1 {
return 0, errOutBufferTooSmall
}

out[0] = byte(config<<3) | byte(frameCodeOneFrame) // mono, one frame
n := copy(out[1:], payload)

return n + 1, nil
}

// applySILKDCBlock removes DC bias from pcm with a first-order IIR high-pass
// at silkDCBlockCutoffHz, returning a new slice (pcm is left untouched). mem
// must persist across calls for the same stream.
func applySILKDCBlock(pcm []int16, sampleRate int, mem *float32) []int16 {
coef := float32(6.3) * silkDCBlockCutoffHz / float32(sampleRate)
coef2 := 1 - coef
out := make([]int16, len(pcm))
for i, sample := range pcm {
x := float32(sample)
y := x - *mem
*mem = coef*x + coef2**mem
switch {
case y > 32767:
out[i] = 32767
case y < -32768:
out[i] = -32768
default:
out[i] = int16(math.Round(float64(y)))
}
}

return out
}

func (e *Encoder) tocHeader() tableOfContentsHeader {
bw := e.autoSelectBandwidth()
var config int
Expand Down
Loading
Loading