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
83 changes: 83 additions & 0 deletions internal/silk/gains.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT

package silk

// Gain quantization constants from silk/define.h and the derived macros at the
// top of silk/gain_quant.c.
const (
gainOffsetQ7 = 2090 // OFFSET = (MIN_QGAIN_DB*128)/6 + 16*128
gainScaleQ16 = 2251 // SCALE_Q16 = (65536*(N_LEVELS_QGAIN-1)) / (((MAX-MIN)*128)/6)
gainInvScaleQ16 = 1907825 // INV_SCALE_Q16 = (65536*(((MAX-MIN)*128)/6)) / (N_LEVELS_QGAIN-1) == 0x1D1C71
gainNLevels = 64 // N_LEVELS_QGAIN
gainMaxDelta = 36 // MAX_DELTA_GAIN_QUANT
gainMinDelta = -4 // MIN_DELTA_GAIN_QUANT
gainMaxLogQ7 = 3967 // 31 in Q7, the upper clamp used by silk_log2lin()
)

// encodeSubframeGains (the *Encoder wrapper that also range-encodes the
// indices via emitGainIndices) is deferred to the orchestration piece — same
// reasoning as findPitchLags/encodeNLSF/quantLTPGains, since it needs
// e.haveEncoded and e.rangeEncoder, and that type doesn't exist here yet.

// quantizeGains is silk_gains_quant: it turns target gains into transmit
// indices and the dequantized gains, updating the running previousLogGain
// (an in/out parameter, same pattern as pitchAnalysisCore's ltpCorr — this
// used to be an *Encoder method keyed on e.previousLogGain, but nothing else
// here needs the rest of *Encoder, so the state is threaded explicitly
// instead of deferring the whole function). For the independent first
// subframe the index is the full 6-bit gain index; for delta subframes it is
// the non-negative transmit index (0..40).
func quantizeGains(
gainsTargetQ16 []int32,
previousLogGain *int32,
subframeCount int,
conditional bool,
) (indices []int8, gainQ16 []float32, gainQ16Int []int32) {
indices = make([]int8, subframeCount)
gainQ16 = make([]float32, subframeCount)
gainQ16Int = make([]int32, subframeCount)

for subframeIndex := range subframeCount {
ind := smulwb(gainScaleQ16, lin2log(gainsTargetQ16[subframeIndex])-gainOffsetQ7)
if ind < *previousLogGain {
ind++
}
ind = clamp(0, ind, gainNLevels-1)

if subframeIndex == 0 && !conditional { //nolint:nestif // faithful port of silk_gains_quant.
ind = clamp(*previousLogGain+gainMinDelta, ind, gainNLevels-1)
*previousLogGain = ind
indices[subframeIndex] = int8(ind) //nolint:gosec // G115: ind is in [0,63].
} else {
delta := ind - *previousLogGain
doubleStepThreshold := 2*gainMaxDelta - gainNLevels + *previousLogGain
if delta > doubleStepThreshold {
delta = doubleStepThreshold + ((delta - doubleStepThreshold + 1) >> 1)
}
delta = clamp(gainMinDelta, delta, gainMaxDelta)
if delta > doubleStepThreshold {
*previousLogGain += (delta << 1) - doubleStepThreshold
if *previousLogGain > gainNLevels-1 {
*previousLogGain = gainNLevels - 1
}
} else {
*previousLogGain += delta
}
indices[subframeIndex] = int8(delta - gainMinDelta) //nolint:gosec // G115: delta is in [gainMinDelta,gainMaxDelta].
}

inLogQ7 := (gainInvScaleQ16 * (*previousLogGain) >> 16) + gainOffsetQ7
inLogQ7 = min(inLogQ7, gainMaxLogQ7)
i := inLogQ7 >> 7
f := inLogQ7 & 127
gain := (1 << i) + ((-174*f*(128-f)>>16)+f)*((1<<i)>>7)
gainQ16Int[subframeIndex] = gain
gainQ16[subframeIndex] = float32(gain)
}

return indices, gainQ16, gainQ16Int
}

// emitGainIndices (the range-coder wiring for quantizeGains's output) is
// deferred alongside encodeSubframeGains, for the same *Encoder reason.
149 changes: 149 additions & 0 deletions internal/silk/gains_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT

package silk

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// reconstructGainsQ16 replays the decoder's index-to-gain reconstruction
// (decodeSubframeQuantizations's per-subframe math) directly against
// already-known indices, instead of reading them from a range-coded
// bitstream — same approach as reconstructNLSF in nlsf_encode_test.go.
func reconstructGainsQ16(indices []int8, previousLogGain int32, conditional bool) []float32 {
gainQ16 := make([]float32, len(indices))
prevLogGain := previousLogGain

for subframeIndex, idx := range indices {
var logGain int32
if subframeIndex == 0 && !conditional {
gainIndex := int32(idx)
logGain = maxInt32(gainIndex, prevLogGain-16)
} else {
deltaGainIndex := int32(idx)
logGain = clamp(0, maxInt32(2*deltaGainIndex-16, prevLogGain+deltaGainIndex-4), 63)
}
prevLogGain = logGain

inLogQ7 := min((gainInvScaleQ16*logGain>>16)+gainOffsetQ7, gainMaxLogQ7)
i := inLogQ7 >> 7
f := inLogQ7 & 127
gainQ16[subframeIndex] = float32((1 << i) + ((-174*f*(128-f)>>16)+f)*((1<<i)>>7))
}

return gainQ16
}

// TestQuantizeGainsRoundTrip checks that quantizeGains's own returned gains
// match what the decoder would reconstruct from the same indices, across the
// independent/delta and double-step-threshold branches.
func TestQuantizeGainsRoundTrip(t *testing.T) {
cases := []struct {
name string
subframeCount int
conditional bool
prevLogGain int32
targetsQ16 []int32
}{
{
name: "independent_first_low_state",
subframeCount: 4,
conditional: false,
prevLogGain: 10,
targetsQ16: []int32{200000, 500000, 1500000, 800000},
},
{
name: "independent_first_small_gains",
subframeCount: 2,
conditional: false,
prevLogGain: 10,
targetsQ16: []int32{81920, 120000},
},
{
name: "conditional_delta",
subframeCount: 4,
conditional: true,
prevLogGain: 30,
targetsQ16: []int32{600000, 650000, 500000, 700000},
},
{
name: "large_upward_jump_double_step",
subframeCount: 4,
conditional: false,
prevLogGain: 10,
targetsQ16: []int32{81920, 1000000000, 1500000000, 90000},
},
{
name: "monotone_decrease",
subframeCount: 4,
conditional: true,
prevLogGain: 45,
targetsQ16: []int32{900000, 400000, 150000, 82000},
},
{
// Raw delta is so far past doubleStepThreshold that it gets
// pre-shrunk before the gainMinDelta/gainMaxDelta clamp — a
// different branch than the double-step accumulation in
// large_upward_jump_double_step above.
name: "delta_pre_shrink_before_clamp",
subframeCount: 4,
conditional: true,
prevLogGain: 0,
targetsQ16: []int32{100000, 2000000000, 2000000000, 2000000000},
},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
previousLogGain := tc.prevLogGain
indices, gainQ16, _ := quantizeGains(tc.targetsQ16, &previousLogGain, tc.subframeCount, tc.conditional)

reconstructed := reconstructGainsQ16(indices, tc.prevLogGain, tc.conditional)

require.Equal(t, reconstructed, gainQ16, "reconstructed gains differ")

// The independent-subframe transmit index is the raw 6-bit gain
// index; delta indices are shifted non-negative (0..40).
if !tc.conditional {
assert.GreaterOrEqualf(t, indices[0], int8(0), "independent index")
assert.LessOrEqualf(t, indices[0], int8(gainNLevels-1), "independent index")
}
for k := 1; k < tc.subframeCount; k++ {
if k == 0 {
continue
}
assert.GreaterOrEqualf(t, indices[k], int8(0), "delta index %d", k)
assert.LessOrEqualf(t, indices[k], int8(gainMaxDelta-gainMinDelta), "delta index %d", k)
}
})
}
}

// TestLin2LogRoundTripAgainstGainDequant checks that lin2log is a near-inverse
// of the silk_log2lin() spelled out inline in the gain code, matching the
// reference tolerance (they are only approximate inverses).
func TestLin2LogRoundTripAgainstGainDequant(t *testing.T) {
// For every gain index 0..63, the log domain value fed to log2lin is
// exactly recoverable, so lin2log(log2lin(x)) must return x for the gain
// grid points used by the codec.
for logGain := range int32(gainNLevels) {
inLogQ7 := (gainInvScaleQ16 * logGain >> 16) + gainOffsetQ7
inLogQ7 = min(inLogQ7, gainMaxLogQ7)
i := inLogQ7 >> 7
f := inLogQ7 & 127
gainQ16 := (1 << i) + ((-174*f*(128-f)>>16)+f)*((1<<i)>>7)

got := lin2log(gainQ16)
// lin2log/log2lin are approximate inverses; the reference keeps the
// round-trip within a couple of Q7 units.
diff := got - inLogQ7
if diff < 0 {
diff = -diff
}
assert.LessOrEqualf(t, diff, int32(3), "lin2log(log2lin(%d))=%d want ~%d", logGain, got, inLogQ7)
}
}
Loading