From 13fc07634c2e4aebfb8f7d68e31d2b51e4e8caec Mon Sep 17 00:00:00 2001 From: Thomas Vilte Date: Wed, 22 Jul 2026 13:39:50 -0300 Subject: [PATCH] Add SILK gain quantization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: François Allais --- internal/silk/gains.go | 83 ++++++++++++++++++++ internal/silk/gains_test.go | 149 ++++++++++++++++++++++++++++++++++++ 2 files changed, 232 insertions(+) create mode 100644 internal/silk/gains.go create mode 100644 internal/silk/gains_test.go diff --git a/internal/silk/gains.go b/internal/silk/gains.go new file mode 100644 index 0000000..b9a1625 --- /dev/null +++ b/internal/silk/gains.go @@ -0,0 +1,83 @@ +// SPDX-FileCopyrightText: 2026 The Pion community +// 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<>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. diff --git a/internal/silk/gains_test.go b/internal/silk/gains_test.go new file mode 100644 index 0000000..e408eef --- /dev/null +++ b/internal/silk/gains_test.go @@ -0,0 +1,149 @@ +// SPDX-FileCopyrightText: 2026 The Pion community +// 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<>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<>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) + } +}