From 1d010c082d7a5d1177bf2cf5b17294ba58f898cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Allais?= Date: Fri, 17 Jul 2026 18:07:19 -0300 Subject: [PATCH 1/2] Add SILK fixed-point math primitives --- internal/silk/lin2log.go | 54 ++++++++++ internal/silk/math_fix.go | 178 +++++++++++++++++++++++++++++++++ internal/silk/math_fix_test.go | 45 +++++++++ 3 files changed, 277 insertions(+) create mode 100644 internal/silk/lin2log.go create mode 100644 internal/silk/math_fix.go create mode 100644 internal/silk/math_fix_test.go diff --git a/internal/silk/lin2log.go b/internal/silk/lin2log.go new file mode 100644 index 0000000..e02b767 --- /dev/null +++ b/internal/silk/lin2log.go @@ -0,0 +1,54 @@ +// SPDX-FileCopyrightText: 2026 The Pion community +// SPDX-License-Identifier: MIT + +package silk + +import "math" + +// Fixed-point log helpers needed by the encoder. silk_log2lin() is not here +// because it is spelled out inline wherever gains are dequantized. + +// ror32 rotates a 32-bit value right by rot bits; a negative rot rotates left. +func ror32(a32 int32, rot int) int32 { //nolint:unused // Used by clzFrac. + x := uint32(a32) //nolint:gosec // G115: bit-level rotate, sign is irrelevant. + switch { + case rot == 0: + return a32 + case rot < 0: + m := uint(-rot) + + return int32((x << m) | (x >> (32 - m))) //nolint:gosec // G115 + default: + r := uint(rot) + + return int32((x << (32 - r)) | (x >> r)) //nolint:gosec // G115 + } +} + +// clzFrac returns the leading-zero count of in and the 7 bits after the +// leading one. +func clzFrac(in int32) (lz, fracQ7 int32) { //nolint:unused // Used by lin2log, sqrtApprox. + lz = int32(clz32(in)) //nolint:gosec // G115: leading-zero count is in [0,32]. + fracQ7 = ror32(in, 24-int(lz)) & 0x7f + + return lz, fracQ7 +} + +// smlawb returns a + smulwb(b, c). +func smlawb(a, b, c int32) int32 { //nolint:unused // Used by 8+ files in subsequent sub-PRs. + return a + smulwb(b, c) +} + +// lin2log approximates 128*log2(inLin) with a piecewise-parabolic fit; it is +// the near-inverse of the inline log2lin used for gains. +func lin2log(inLin int32) int32 { //nolint:unused // Used by gains, LTP, VAD in subsequent sub-PRs. + lz, fracQ7 := clzFrac(inLin) + + // silk_ADD_LSHIFT32(silk_SMLAWB(frac_Q7, silk_MUL(frac_Q7, 128-frac_Q7), 179), 31-lz, 7) + return smlawb(fracQ7, fracQ7*(128-fracQ7), 179) + ((31 - lz) << 7) +} + +// silkLog2 approximates log2 (silk_log2). +func silkLog2(x float64) float32 { //nolint:unused // Used by pitch analysis, noise shaping in subsequent sub-PRs. + return float32(3.32192809488736 * math.Log10(x)) +} diff --git a/internal/silk/math_fix.go b/internal/silk/math_fix.go new file mode 100644 index 0000000..3ee283f --- /dev/null +++ b/internal/silk/math_fix.go @@ -0,0 +1,178 @@ +// SPDX-FileCopyrightText: 2026 The Pion community +// SPDX-License-Identifier: MIT + +package silk + +import "math" + +// Additional fixed-point primitives from the RFC 6716 C macros, used by the +// analysis stages. + +// smulbb returns (int16)a * (int16)b. +func smulbb(a, b int32) int32 { //nolint:unused // Used by VAD, NSQ, LTP in subsequent sub-PRs. + return int32(int16(a)) * int32(int16(b)) //nolint:gosec // G115: Q-format conversion. +} + +// smlabb returns a + (int16)b * (int16)c. +func smlabb(a, b, c int32) int32 { //nolint:unused // Used by VAD, NSQ, LTP in subsequent sub-PRs. + return a + int32(int16(b))*int32(int16(c)) //nolint:gosec // G115: Q-format conversion. +} + +// smulww returns (a * b) >> 16 at 64-bit width. +func smulww(a, b int32) int32 { //nolint:unused // Used by NSQ in subsequent sub-PRs. + return int32((int64(a) * int64(b)) >> 16) //nolint:gosec // G115: Q-format conversion. +} + +// smulwt returns (a * (b>>16)) >> 16. +func smulwt(a, b int32) int32 { //nolint:unused // Used by NSQ, VAD in subsequent sub-PRs. + return int32((int64(a) * int64(b>>16)) >> 16) //nolint:gosec // G115: Q-format conversion. +} + +// smlawt returns a + smulwt(b, c). +func smlawt(a, b, c int32) int32 { //nolint:unused // Used by NSQ in subsequent sub-PRs. + return a + smulwt(b, c) +} + +// sub32Ovflw / add32Ovflw are two's-complement wrapping arithmetic. +// +//nolint:unused // Used by NSQ in subsequent sub-PRs. +func sub32Ovflw(a, b int32) int32 { + return int32(uint32(a) - uint32(b)) //nolint:gosec // G115: wrapping arithmetic. +} + +func add32Ovflw(a, b int32) int32 { //nolint:unused // Used by NSQ in subsequent sub-PRs. + return int32(uint32(a) + uint32(b)) //nolint:gosec // G115: wrapping arithmetic. +} + +// addLShift32 returns a + (b << shift). +func addLShift32(a, b int32, shift uint) int32 { //nolint:unused // Used by NSQ in subsequent sub-PRs. + return a + (b << shift) +} + +// addSat32 saturates the signed sum of a and b to int32. +func addSat32(a, b int32) int32 { //nolint:unused // Used by NSQ in subsequent sub-PRs. + sum := int64(a) + int64(b) + if sum > math.MaxInt32 { + return math.MaxInt32 + } + if sum < math.MinInt32 { + return math.MinInt32 + } + + return int32(sum) +} + +// lshiftSat32 saturates a << shift to int32. +func lshiftSat32(a int32, shift uint) int32 { //nolint:unused // Used by NSQ in subsequent sub-PRs. + return int32(clampI64(int64(a)< hi { + return hi + } + + return v +} + +// silkRand advances the LCG dither seed (silk_RAND). +func silkRand(seed int32) int32 { //nolint:unused // Used by NSQ in subsequent sub-PRs. + return int32(uint32(907633515) + uint32(seed)*uint32(196314165)) //nolint:gosec // G115: wrapping LCG. +} + +// smlabbOvflw returns a + (int16)b * (int16)c with wrapping. +func smlabbOvflw(a, b, c int32) int32 { //nolint:unused // Used by NSQ in subsequent sub-PRs. + return add32Ovflw(a, int32(int16(b))*int32(int16(c))) //nolint:gosec // G115: Q-format conversion. +} + +// div32VarQ approximates (a << qres) / b (silk_DIV32_varQ). +func div32VarQ(a, b int32, qres int) int32 { //nolint:unused // Used by NSQ, VAD in subsequent sub-PRs. + aHeadrm := clz32(absInt32(a)) - 1 + a32Nrm := a << uint(aHeadrm) //nolint:gosec // G115: shift count is non-negative. + bHeadrm := clz32(absInt32(b)) - 1 + b32Nrm := b << uint(bHeadrm) //nolint:gosec // G115: shift count is non-negative. + b32Inv := (math.MaxInt32 >> 2) / (b32Nrm >> 16) + result := smulwb(a32Nrm, b32Inv) + a32Nrm = sub32Ovflw(a32Nrm, lshiftOvflw(smmul(b32Nrm, result), 3)) + result = smlawb(result, a32Nrm, b32Inv) + + lshift := 29 + aHeadrm - bHeadrm - qres + if lshift < 0 { + return lshiftSat32(result, uint(-lshift)) + } + if lshift < 32 { + return result >> uint(lshift) + } + + return 0 +} + +// sat16 saturates to the int16 range. +func sat16(a int32) int32 { //nolint:unused // Used by pitch, VAD, NSQ in subsequent sub-PRs. + switch { + case a > math.MaxInt16: + return math.MaxInt16 + case a < math.MinInt16: + return math.MinInt16 + default: + return a + } +} + +// addPosSat32 saturates the sum of two non-negative values to int32. +func addPosSat32(a, b int32) int32 { //nolint:unused // Used by LTP, VAD in subsequent sub-PRs. + if (uint32(a)+uint32(b))&0x80000000 != 0 { //nolint:gosec // G115: bit test on the sum. + return math.MaxInt32 + } + + return a + b +} + +// sqrtApprox approximates the square root (silk_SQRT_APPROX). +func sqrtApprox(x int32) int32 { //nolint:unused // Used by VAD in subsequent sub-PRs. + if x <= 0 { + return 0 + } + lz, fracQ7 := clzFrac(x) + y := int32(46214) + if lz&1 != 0 { + y = 32768 + } + y >>= lz >> 1 + + return smlawb(y, y, smulbb(213, fracQ7)) +} + +//nolint:gochecknoglobals,unused // fixed sigmoid lookup tables from sigm_Q15.c, used by VAD. +var ( + sigmLUTSlopeQ10 = [6]int32{237, 153, 73, 30, 12, 7} + sigmLUTPosQ15 = [6]int32{16384, 23955, 28861, 31213, 32178, 32548} + sigmLUTNegQ15 = [6]int32{16384, 8812, 3906, 1554, 589, 219} +) + +// sigmQ15 approximates the sigmoid in Q15 (silk_sigm_Q15). +func sigmQ15(inQ5 int32) int32 { //nolint:unused // Used by VAD in subsequent sub-PRs. + if inQ5 < 0 { + inQ5 = -inQ5 + if inQ5 >= 6*32 { + return 0 + } + ind := inQ5 >> 5 + + return sigmLUTNegQ15[ind] - smulbb(sigmLUTSlopeQ10[ind], inQ5&0x1F) + } + if inQ5 >= 6*32 { + return 32767 + } + ind := inQ5 >> 5 + + return sigmLUTPosQ15[ind] + smulbb(sigmLUTSlopeQ10[ind], inQ5&0x1F) +} diff --git a/internal/silk/math_fix_test.go b/internal/silk/math_fix_test.go new file mode 100644 index 0000000..c34d705 --- /dev/null +++ b/internal/silk/math_fix_test.go @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: 2026 The Pion community +// SPDX-License-Identifier: MIT + +package silk + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestDiv32VarQ(t *testing.T) { + cases := []struct { + a, b int32 + q int + }{ + {1000, 7, 16}, + {-500000, 13, 10}, + {123456, 789, 10}, + {1 << 20, 3, 12}, + } + for _, c := range cases { + got := float64(div32VarQ(c.a, c.b, c.q)) + want := float64(c.a) * float64(int64(1)<>16)) >> 16 + assert.Equal(t, int32(int64(1000000)*int64(0x00030000>>16)>>16), smulwt(1000000, 0x00030000)) +} + +func TestAddSat32(t *testing.T) { + assert.Equal(t, int32(3), addSat32(1, 2)) + assert.Equal(t, int32(2147483647), addSat32(2147483647, 100)) + assert.Equal(t, int32(-2147483648), addSat32(-2147483648, -100)) +} + +func TestSilkRandDeterministic(t *testing.T) { + // Matches the decoder's LCG update: seed = 196314165*seed + 907633515. + seed := int32(12345) + want := int32(uint32(907633515) + uint32(seed)*uint32(196314165)) //nolint:gosec + assert.Equal(t, want, silkRand(seed)) +} From e1a9a36ccfabd5d75b773db92e3002b6cfe457f3 Mon Sep 17 00:00:00 2001 From: Thomas Vilte Date: Sat, 18 Jul 2026 00:04:28 -0300 Subject: [PATCH 2/2] Add fixed-point math tests --- internal/silk/lin2log.go | 10 +-- internal/silk/math_fix.go | 40 ++++----- internal/silk/math_fix_test.go | 154 +++++++++++++++++++++++++++++++++ 3 files changed, 178 insertions(+), 26 deletions(-) diff --git a/internal/silk/lin2log.go b/internal/silk/lin2log.go index e02b767..9799a6b 100644 --- a/internal/silk/lin2log.go +++ b/internal/silk/lin2log.go @@ -9,7 +9,7 @@ import "math" // because it is spelled out inline wherever gains are dequantized. // ror32 rotates a 32-bit value right by rot bits; a negative rot rotates left. -func ror32(a32 int32, rot int) int32 { //nolint:unused // Used by clzFrac. +func ror32(a32 int32, rot int) int32 { x := uint32(a32) //nolint:gosec // G115: bit-level rotate, sign is irrelevant. switch { case rot == 0: @@ -27,7 +27,7 @@ func ror32(a32 int32, rot int) int32 { //nolint:unused // Used by clzFrac. // clzFrac returns the leading-zero count of in and the 7 bits after the // leading one. -func clzFrac(in int32) (lz, fracQ7 int32) { //nolint:unused // Used by lin2log, sqrtApprox. +func clzFrac(in int32) (lz, fracQ7 int32) { lz = int32(clz32(in)) //nolint:gosec // G115: leading-zero count is in [0,32]. fracQ7 = ror32(in, 24-int(lz)) & 0x7f @@ -35,13 +35,13 @@ func clzFrac(in int32) (lz, fracQ7 int32) { //nolint:unused // Used by lin2log, } // smlawb returns a + smulwb(b, c). -func smlawb(a, b, c int32) int32 { //nolint:unused // Used by 8+ files in subsequent sub-PRs. +func smlawb(a, b, c int32) int32 { return a + smulwb(b, c) } // lin2log approximates 128*log2(inLin) with a piecewise-parabolic fit; it is // the near-inverse of the inline log2lin used for gains. -func lin2log(inLin int32) int32 { //nolint:unused // Used by gains, LTP, VAD in subsequent sub-PRs. +func lin2log(inLin int32) int32 { lz, fracQ7 := clzFrac(inLin) // silk_ADD_LSHIFT32(silk_SMLAWB(frac_Q7, silk_MUL(frac_Q7, 128-frac_Q7), 179), 31-lz, 7) @@ -49,6 +49,6 @@ func lin2log(inLin int32) int32 { //nolint:unused // Used by gains, LTP, VAD in } // silkLog2 approximates log2 (silk_log2). -func silkLog2(x float64) float32 { //nolint:unused // Used by pitch analysis, noise shaping in subsequent sub-PRs. +func silkLog2(x float64) float32 { return float32(3.32192809488736 * math.Log10(x)) } diff --git a/internal/silk/math_fix.go b/internal/silk/math_fix.go index 3ee283f..d58fbb8 100644 --- a/internal/silk/math_fix.go +++ b/internal/silk/math_fix.go @@ -9,48 +9,46 @@ import "math" // analysis stages. // smulbb returns (int16)a * (int16)b. -func smulbb(a, b int32) int32 { //nolint:unused // Used by VAD, NSQ, LTP in subsequent sub-PRs. +func smulbb(a, b int32) int32 { return int32(int16(a)) * int32(int16(b)) //nolint:gosec // G115: Q-format conversion. } // smlabb returns a + (int16)b * (int16)c. -func smlabb(a, b, c int32) int32 { //nolint:unused // Used by VAD, NSQ, LTP in subsequent sub-PRs. +func smlabb(a, b, c int32) int32 { return a + int32(int16(b))*int32(int16(c)) //nolint:gosec // G115: Q-format conversion. } // smulww returns (a * b) >> 16 at 64-bit width. -func smulww(a, b int32) int32 { //nolint:unused // Used by NSQ in subsequent sub-PRs. +func smulww(a, b int32) int32 { return int32((int64(a) * int64(b)) >> 16) //nolint:gosec // G115: Q-format conversion. } // smulwt returns (a * (b>>16)) >> 16. -func smulwt(a, b int32) int32 { //nolint:unused // Used by NSQ, VAD in subsequent sub-PRs. +func smulwt(a, b int32) int32 { return int32((int64(a) * int64(b>>16)) >> 16) //nolint:gosec // G115: Q-format conversion. } // smlawt returns a + smulwt(b, c). -func smlawt(a, b, c int32) int32 { //nolint:unused // Used by NSQ in subsequent sub-PRs. +func smlawt(a, b, c int32) int32 { return a + smulwt(b, c) } // sub32Ovflw / add32Ovflw are two's-complement wrapping arithmetic. -// -//nolint:unused // Used by NSQ in subsequent sub-PRs. func sub32Ovflw(a, b int32) int32 { return int32(uint32(a) - uint32(b)) //nolint:gosec // G115: wrapping arithmetic. } -func add32Ovflw(a, b int32) int32 { //nolint:unused // Used by NSQ in subsequent sub-PRs. +func add32Ovflw(a, b int32) int32 { return int32(uint32(a) + uint32(b)) //nolint:gosec // G115: wrapping arithmetic. } // addLShift32 returns a + (b << shift). -func addLShift32(a, b int32, shift uint) int32 { //nolint:unused // Used by NSQ in subsequent sub-PRs. +func addLShift32(a, b int32, shift uint) int32 { return a + (b << shift) } // addSat32 saturates the signed sum of a and b to int32. -func addSat32(a, b int32) int32 { //nolint:unused // Used by NSQ in subsequent sub-PRs. +func addSat32(a, b int32) int32 { sum := int64(a) + int64(b) if sum > math.MaxInt32 { return math.MaxInt32 @@ -63,16 +61,16 @@ func addSat32(a, b int32) int32 { //nolint:unused // Used by NSQ in subsequent s } // lshiftSat32 saturates a << shift to int32. -func lshiftSat32(a int32, shift uint) int32 { //nolint:unused // Used by NSQ in subsequent sub-PRs. +func lshiftSat32(a int32, shift uint) int32 { return int32(clampI64(int64(a)< math.MaxInt16: return math.MaxInt16 @@ -128,7 +126,7 @@ func sat16(a int32) int32 { //nolint:unused // Used by pitch, VAD, NSQ in subseq } // addPosSat32 saturates the sum of two non-negative values to int32. -func addPosSat32(a, b int32) int32 { //nolint:unused // Used by LTP, VAD in subsequent sub-PRs. +func addPosSat32(a, b int32) int32 { if (uint32(a)+uint32(b))&0x80000000 != 0 { //nolint:gosec // G115: bit test on the sum. return math.MaxInt32 } @@ -137,7 +135,7 @@ func addPosSat32(a, b int32) int32 { //nolint:unused // Used by LTP, VAD in subs } // sqrtApprox approximates the square root (silk_SQRT_APPROX). -func sqrtApprox(x int32) int32 { //nolint:unused // Used by VAD in subsequent sub-PRs. +func sqrtApprox(x int32) int32 { if x <= 0 { return 0 } @@ -151,7 +149,7 @@ func sqrtApprox(x int32) int32 { //nolint:unused // Used by VAD in subsequent su return smlawb(y, y, smulbb(213, fracQ7)) } -//nolint:gochecknoglobals,unused // fixed sigmoid lookup tables from sigm_Q15.c, used by VAD. +//nolint:gochecknoglobals // fixed sigmoid lookup tables from sigm_Q15.c. var ( sigmLUTSlopeQ10 = [6]int32{237, 153, 73, 30, 12, 7} sigmLUTPosQ15 = [6]int32{16384, 23955, 28861, 31213, 32178, 32548} @@ -159,7 +157,7 @@ var ( ) // sigmQ15 approximates the sigmoid in Q15 (silk_sigm_Q15). -func sigmQ15(inQ5 int32) int32 { //nolint:unused // Used by VAD in subsequent sub-PRs. +func sigmQ15(inQ5 int32) int32 { if inQ5 < 0 { inQ5 = -inQ5 if inQ5 >= 6*32 { diff --git a/internal/silk/math_fix_test.go b/internal/silk/math_fix_test.go index c34d705..e466247 100644 --- a/internal/silk/math_fix_test.go +++ b/internal/silk/math_fix_test.go @@ -4,6 +4,7 @@ package silk import ( + "math" "testing" "github.com/stretchr/testify/assert" @@ -43,3 +44,156 @@ func TestSilkRandDeterministic(t *testing.T) { want := int32(uint32(907633515) + uint32(seed)*uint32(196314165)) //nolint:gosec assert.Equal(t, want, silkRand(seed)) } + +func TestSmulbb(t *testing.T) { + assert.Equal(t, int32(6), smulbb(2, 3)) + assert.Equal(t, int32(-6), smulbb(-2, 3)) + // Only the low 16 bits of each operand matter (Q-format truncation). + assert.Equal(t, smulbb(1, 2), smulbb(0x10000+1, 2)) +} + +func TestSmlabb(t *testing.T) { + assert.Equal(t, int32(100+6), smlabb(100, 2, 3)) + assert.Equal(t, int32(100-6), smlabb(100, -2, 3)) +} + +func TestSmulww(t *testing.T) { + assert.Equal(t, int32((int64(1000000)*int64(1000000))>>16), smulww(1000000, 1000000)) + assert.Equal(t, int32(0), smulww(0, 12345)) +} + +func TestSmlawt(t *testing.T) { + got := smlawt(100, 1000000, 0x00030000) + want := int32(100) + smulwt(1000000, 0x00030000) + assert.Equal(t, want, got) +} + +func TestSub32OvflwAdd32Ovflw(t *testing.T) { + assert.Equal(t, int32(5), sub32Ovflw(8, 3)) + assert.Equal(t, int32(5), add32Ovflw(2, 3)) + // Two's-complement wrap at the int32 boundary, unlike a saturating add. + assert.Equal(t, int32(math.MinInt32), add32Ovflw(math.MaxInt32, 1)) + assert.Equal(t, int32(math.MaxInt32), sub32Ovflw(math.MinInt32, 1)) +} + +func TestAddLShift32(t *testing.T) { + assert.Equal(t, int32(5+(3<<2)), addLShift32(5, 3, 2)) + // A zero shift still adds b unshifted, it doesn't zero it out. + assert.Equal(t, int32(8), addLShift32(5, 3, 0)) +} + +func TestLshiftSat32(t *testing.T) { + assert.Equal(t, int32(4), lshiftSat32(1, 2)) + // Saturates instead of wrapping when the shift overflows int32. + assert.Equal(t, int32(math.MaxInt32), lshiftSat32(math.MaxInt32, 4)) + assert.Equal(t, int32(math.MinInt32), lshiftSat32(math.MinInt32, 4)) +} + +func TestLshiftOvflw(t *testing.T) { + assert.Equal(t, int32(4), lshiftOvflw(1, 2)) + // Wraps instead of saturating (the counterpart to lshiftSat32). + maxAsUint32 := uint32(math.MaxInt32) + assert.Equal(t, int32(maxAsUint32<<4), lshiftOvflw(math.MaxInt32, 4)) //nolint:gosec +} + +func TestClampI64(t *testing.T) { + assert.Equal(t, int64(5), clampI64(5, 0, 10)) + assert.Equal(t, int64(0), clampI64(-5, 0, 10)) + assert.Equal(t, int64(10), clampI64(15, 0, 10)) +} + +func TestSmlabbOvflw(t *testing.T) { + assert.Equal(t, int32(100+6), smlabbOvflw(100, 2, 3)) + // Wraps on overflow instead of saturating, unlike a plain saturating add. + assert.Equal(t, add32Ovflw(math.MaxInt32, 6), smlabbOvflw(math.MaxInt32, 2, 3)) +} + +func TestSat16(t *testing.T) { + assert.Equal(t, int32(100), sat16(100)) + assert.Equal(t, int32(math.MaxInt16), sat16(math.MaxInt16+1)) + assert.Equal(t, int32(math.MinInt16), sat16(math.MinInt16-1)) +} + +func TestAddPosSat32(t *testing.T) { + assert.Equal(t, int32(30), addPosSat32(10, 20)) + assert.Equal(t, int32(math.MaxInt32), addPosSat32(math.MaxInt32, 100)) +} + +func TestSqrtApprox(t *testing.T) { + assert.Zero(t, sqrtApprox(0)) + assert.Zero(t, sqrtApprox(-5)) + + // silk_SQRT_APPROX's own doc comment (Inlines.h) claims <10% error for + // *output* values above 15 and <2.5% for output values above 120 — the + // bound is on sqrt(x), not on x itself, so the inputs below are chosen + // so each output value actually falls in the tier it's meant to check. + cases := []int32{400, 20000, 1 << 20, math.MaxInt32} // sqrt: 20, ~141, 1024, ~46341 + for _, x := range cases { + got := float64(sqrtApprox(x)) + want := math.Sqrt(float64(x)) + tolerance := 0.10 + if want > 120 { + tolerance = 0.025 + } + assert.InEpsilonf(t, want, got, tolerance, "sqrtApprox(%d)", x) + } +} + +func TestRor32(t *testing.T) { + assert.Equal(t, int32(1), ror32(1, 0)) + // Rotating a single set bit by 1 moves it to the top bit. + assert.Equal(t, int32(math.MinInt32), ror32(1, 1)) + // A negative rot rotates the other way: right-by-r then right-by-(-r) + // (i.e. left-by-r) is a round trip back to the original value. + assert.Equal(t, int32(12345), ror32(ror32(12345, 7), -7)) +} + +func TestClzFrac(t *testing.T) { + lz, frac := clzFrac(1) + assert.Equal(t, int32(31), lz) + assert.Equal(t, int32(0), frac) + + // A value with the leading bit set (after normalization) has lz=0. + lz, _ = clzFrac(math.MinInt32 + 1) // 0x80000001, top bit set + assert.Equal(t, int32(0), lz) +} + +func TestSmlawb(t *testing.T) { + got := smlawb(100, 1000000, 0x00030000) + want := int32(100) + smulwb(1000000, 0x00030000) + assert.Equal(t, want, got) +} + +func TestLin2Log(t *testing.T) { + // lin2log approximates 128*log2(x); libopus doesn't document a formal + // error bound for this one, but the fit is tight enough in practice to + // hold to 1% for representative inputs. + for _, x := range []int32{1, 2, 100, 1 << 10, 1 << 20, math.MaxInt32} { + got := float64(lin2log(x)) / 128.0 + want := math.Log2(float64(x)) + assert.InDeltaf(t, want, got, 0.05, "lin2log(%d)", x) + } +} + +func TestSilkLog2(t *testing.T) { + for _, x := range []float64{1, 2, 10, 1000} { + want := float32(3.32192809488736 * math.Log10(x)) + assert.Equal(t, want, silkLog2(x)) + } +} + +func TestSigmQ15(t *testing.T) { + // Midpoint: sigma(0) = 0.5, i.e. 16384 in Q15. + assert.Equal(t, int32(16384), sigmQ15(0)) + // Clips at the LUT boundary (in_Q5 = 6*32 = 192). + assert.Equal(t, int32(32767), sigmQ15(192)) + assert.Equal(t, int32(0), sigmQ15(-192)) + // One step inside the boundary, from the last LUT segment. + assert.Equal(t, 219-smulbb(7, 31), sigmQ15(-191)) + assert.Equal(t, 32548+smulbb(7, 31), sigmQ15(191)) + // Interior point on the second LUT segment (positive and negative side + // use separate, independently-fitted LUTs, so these aren't exact + // mirrors of each other — check each directly). + assert.Equal(t, 23955+smulbb(153, 18), sigmQ15(50)) + assert.Equal(t, 8812-smulbb(153, 18), sigmQ15(-50)) +}