fix(streamer): db-scaled spectrum so bars match frontend visibility

This commit is contained in:
devilreef 2026-04-30 19:18:03 +06:00
parent 9eaa16d818
commit 1c4c875c10
Signed by: devilreef
SSH key fingerprint: SHA256:UZisRr4iuXx+IhkbZnR655L2RWAT6o2rgbGv5F/6m3Y
2 changed files with 11 additions and 5 deletions

View file

@ -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;
}
}

View file

@ -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", () => {