From 1c4c875c10d813eb4e23a8b865d5458c389cb7b7 Mon Sep 17 00:00:00 2001 From: devilreef Date: Thu, 30 Apr 2026 19:18:03 +0600 Subject: [PATCH] fix(streamer): db-scaled spectrum so bars match frontend visibility --- streamer/src/fft.ts | 9 +++++++-- streamer/tests/fft.test.ts | 7 ++++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/streamer/src/fft.ts b/streamer/src/fft.ts index 57620ee..de193ed 100644 --- a/streamer/src/fft.ts +++ b/streamer/src/fft.ts @@ -69,14 +69,19 @@ export class SpectrumAnalyzer { fftInPlace(re, im); const alpha = this.opts.smoothing ?? 0.6; - const peakRef = this.fftSize / 4; + // db scaling — same idea as web audio's getByteFrequencyData. + // hann-windowed full-scale sine has peak mag ~ fftSize/2; reference to that. + const ref = this.fftSize / 2; + const dbMin = -80; + const dbMax = -20; for (let i = 0; i < this.opts.bars; i++) { let mag = 0; const s = this.bandStarts[i]!; const e = this.bandEnds[i]!; for (let k = s; k < e; k++) mag += Math.sqrt(re[k]! * re[k]! + im[k]! * im[k]!); mag /= e - s; - const norm = Math.min(1, mag / peakRef); + const db = 20 * Math.log10(Math.max(mag, 1e-9) / ref); + const norm = Math.max(0, Math.min(1, (db - dbMin) / (dbMax - dbMin))); this.smooth[i] = alpha * this.smooth[i]! + (1 - alpha) * norm; } } diff --git a/streamer/tests/fft.test.ts b/streamer/tests/fft.test.ts index 1a5e392..60f27d4 100644 --- a/streamer/tests/fft.test.ts +++ b/streamer/tests/fft.test.ts @@ -23,13 +23,14 @@ describe("SpectrumAnalyzer", () => { }); }); - it("a 1khz sine puts most energy in low bars (logarithmic binning)", () => { + it("a 200hz sine puts most energy in low bars (logarithmic binning)", () => { const a = new SpectrumAnalyzer({ bars: 48, sampleRate: 48000 }); - a.feed(sineFrame(1000)); + for (let n = 0; n < 4; n++) a.feed(sineFrame(200)); const bars = a.bars(); const lowSum = bars.slice(0, 16).reduce((s, v) => s + v, 0); const highSum = bars.slice(32).reduce((s, v) => s + v, 0); - expect(lowSum).toBeGreaterThan(highSum * 5); + expect(lowSum).toBeGreaterThan(0.1); + expect(lowSum).toBeGreaterThan(highSum + 0.1); }); it("silence produces all-near-zero bars", () => {