diff --git a/internal/celt/allocation.go b/internal/celt/allocation.go index be9cc51..0c90219 100644 --- a/internal/celt/allocation.go +++ b/internal/celt/allocation.go @@ -546,28 +546,6 @@ func pulsesToBits(band, lm, pulses int) int { return int(pulseCacheBits[cacheStart+pulses]) + 1 } -func intensityStartBand(bitrateBps, frameMs int) int { - framesPerSec := 1000 / frameMs - effectiveKbps := (bitrateBps - 80*framesPerSec) / 1000 - - switch { - case effectiveKbps < 35: - return 8 - case effectiveKbps < 50: - return 12 - case effectiveKbps < 68: - return 16 - case effectiveKbps < 84: - return 17 - case effectiveKbps < 102: - return 19 - case effectiveKbps < 130: - return 20 - default: - return maxBands - } -} - // decodeFineEnergy applies the first RFC 6716 Section 4.3.2.2 fine-energy // refinement, using the number of raw bits assigned by Section 4.3.3. func (d *Decoder) decodeFineEnergy(info *frameSideInfo, fineQuant [maxBands]int) { diff --git a/internal/celt/allocation_test.go b/internal/celt/allocation_test.go index 402a870..8f31cb3 100644 --- a/internal/celt/allocation_test.go +++ b/internal/celt/allocation_test.go @@ -81,36 +81,41 @@ func rangeDecoderWithRawBits(bits byte) rangecoding.Decoder { return decoder } -func TestIntensityStartBand(t *testing.T) { - // RFC 6716 Table 66 thresholds for 20ms frames (frameMs=20, framesPerSec=50). - // effectiveKbps = (bitrateBps - 80*50) / 1000. +func TestIntensityBandForRate(t *testing.T) { + // libopus intensity_thresholds, read against equiv_rate in kb/s. The values + // are the bands opus_demo picks at each of these rates for 20 ms stereo. cases := []struct { - bitrateBps int - startBand int + kbps int + band int }{ - {32000, 8}, - {45000, 12}, - {64000, 16}, - {96000, 19}, - {128000, 20}, - {160000, maxBands}, + {23, 9}, + {31, 10}, + {47, 12}, + {63, 15}, + {95, 19}, + {127, 20}, } for _, tc := range cases { - t.Run("", func(t *testing.T) { - got := intensityStartBand(tc.bitrateBps, 20) - assert.Equal(t, tc.startBand, got, - "bitrateBps=%d", tc.bitrateBps) - }) + got := intensityBandForRate(tc.kbps, 0) + assert.Equal(t, tc.band, got, "kbps=%d", tc.kbps) } } -func TestIntensityStartBandMonotonic(t *testing.T) { +func TestIntensityBandForRateHysteresis(t *testing.T) { + // 24 kb/s alone lands on band 10, but coming from 9 the entry's margin + // (thresholds[9]+hysteresis[9] = 26) holds it there for another frame. + assert.Equal(t, 10, intensityBandForRate(24, 0)) + assert.Equal(t, 9, intensityBandForRate(24, 9)) + assert.Equal(t, 10, intensityBandForRate(26, 9)) +} + +func TestIntensityBandForRateMonotonic(t *testing.T) { prev := 0 - for bitrate := range 200000 { - got := intensityStartBand(bitrate, 20) + for kbps := range 200 { + got := intensityBandForRate(kbps, 0) assert.GreaterOrEqual(t, got, prev, - "intensity must be monotonically non-decreasing: bitrate=%d got=%d prev=%d", bitrate, got, prev) + "intensity must be monotonically non-decreasing: kbps=%d got=%d prev=%d", kbps, got, prev) prev = got } } diff --git a/internal/celt/encoder.go b/internal/celt/encoder.go index b5fbac7..b3ca7ec 100644 --- a/internal/celt/encoder.go +++ b/internal/celt/encoder.go @@ -494,23 +494,14 @@ func (e *Encoder) computeIntensityAndDualStereo( return 0, 0 } - frameSampleCount := shortBlockSampleCount << info.lm - bitrateBps := int(info.totalBits) * sampleRate / frameSampleCount - frameMs := max(1, frameSampleCount*1000/sampleRate) - raw := intensityStartBand(bitrateBps, frameMs) - if e.prevIntensityBand == 0 { - e.prevIntensityBand = raw - } - // ±1 dead band: require two consecutive frames to confirm a direction - // change, matching the hysteresis pattern in libopus CELTEncoder. - if raw > e.prevIntensityBand+1 { - raw = e.prevIntensityBand + 1 - } else if raw < e.prevIntensityBand-1 { - raw = e.prevIntensityBand - 1 - } + // equiv_rate is the frame budget expressed as a steady bit rate, net of the + // per-packet overhead the reference charges (celt_encoder.c:1926). + frameBytes := int(info.totalBits) / 8 + equivRate := frameBytes*8*50<<(3-info.lm) - (40*info.channelCount+20)*((400>>info.lm)-50) + raw := intensityBandForRate(equivRate/1000, e.prevIntensityBand) e.prevIntensityBand = raw - targetIntensity = raw + targetIntensity = min(info.endBand, max(info.startBand, raw)) if chooseDualStereo(normalized, info.lm) { targetDualStereo = 1 } diff --git a/internal/celt/hysteresis.go b/internal/celt/hysteresis.go index a1dee7f..1e44c75 100644 --- a/internal/celt/hysteresis.go +++ b/internal/celt/hysteresis.go @@ -25,3 +25,35 @@ func hysteresisDecision(val float32, prevDecision int, thresholds [3]float32) in return spreadNone } } + +// intensityThresholds and intensityHysteresis are the reference tables from +// libopus celt_encoder.c, read against equiv_rate in kb/s. Index i is the +// intensity band chosen when the rate clears thresholds[i-1] but not +// thresholds[i]. +// +//nolint:gochecknoglobals // Reference tables. +var ( + intensityThresholds = [21]int{1, 2, 3, 4, 5, 6, 7, 8, 16, 24, 36, 44, 50, 56, 62, 67, 72, 79, 88, 106, 134} + intensityHysteresis = [21]int{1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 3, 3, 4, 5, 6, 8, 8} +) + +// intensityBandForRate picks the first band coded in intensity stereo, porting +// libopus hysteresis_decision (celt/bands.c). The per-entry hysteresis keeps +// the band from oscillating when the rate sits on a threshold: a move away from +// prev only sticks once the rate clears that entry's margin. +func intensityBandForRate(kbps, prev int) int { + band := 0 + for ; band < len(intensityThresholds); band++ { + if kbps < intensityThresholds[band] { + break + } + } + if band > prev && kbps < intensityThresholds[prev]+intensityHysteresis[prev] { + band = prev + } + if band < prev && kbps > intensityThresholds[prev-1]-intensityHysteresis[prev-1] { + band = prev + } + + return band +}