mirror of
https://github.com/Boof2015/prism.git
synced 2026-08-12 05:10:51 +02:00
Fix NaN drift in VU
This commit is contained in:
@@ -606,7 +606,7 @@ export class VUMeter {
|
|||||||
x: number, y: number, _w: number, h: number,
|
x: number, y: number, _w: number, h: number,
|
||||||
db: number
|
db: number
|
||||||
): void {
|
): void {
|
||||||
const displayDb = Math.max(VU_METER_MIN_DB, Math.min(0, db))
|
const displayDb = Number.isFinite(db) ? Math.max(VU_METER_MIN_DB, Math.min(0, db)) : VU_METER_MIN_DB
|
||||||
const text = displayDb <= VU_METER_MIN_DB + 1 ? '-∞' : `${displayDb.toFixed(1)}`
|
const text = displayDb <= VU_METER_MIN_DB + 1 ? '-∞' : `${displayDb.toFixed(1)}`
|
||||||
ctx.fillStyle = alphaColor(this.options.labelColor, 0.8)
|
ctx.fillStyle = alphaColor(this.options.labelColor, 0.8)
|
||||||
ctx.font = `${Math.min(20, Math.max(9, h * 0.55))}px "JetBrains Mono", monospace`
|
ctx.font = `${Math.min(20, Math.max(9, h * 0.55))}px "JetBrains Mono", monospace`
|
||||||
@@ -620,7 +620,7 @@ export class VUMeter {
|
|||||||
x: number, y: number, w: number, h: number,
|
x: number, y: number, w: number, h: number,
|
||||||
db: number
|
db: number
|
||||||
): void {
|
): void {
|
||||||
const displayDb = Math.max(VU_METER_MIN_DB, Math.min(0, db))
|
const displayDb = Number.isFinite(db) ? Math.max(VU_METER_MIN_DB, Math.min(0, db)) : VU_METER_MIN_DB
|
||||||
const text = displayDb <= VU_METER_MIN_DB + 1 ? '-∞' : `${displayDb.toFixed(1)}`
|
const text = displayDb <= VU_METER_MIN_DB + 1 ? '-∞' : `${displayDb.toFixed(1)}`
|
||||||
ctx.fillStyle = alphaColor(this.options.labelColor, 0.8)
|
ctx.fillStyle = alphaColor(this.options.labelColor, 0.8)
|
||||||
ctx.font = `${Math.min(16, Math.max(8, h * 0.5))}px "JetBrains Mono", monospace`
|
ctx.font = `${Math.min(16, Math.max(8, h * 0.5))}px "JetBrains Mono", monospace`
|
||||||
@@ -1088,6 +1088,7 @@ export class VUMeter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private formatNeedleDb(db: number): string {
|
private formatNeedleDb(db: number): string {
|
||||||
|
if (!Number.isFinite(db)) return '-∞'
|
||||||
const displayDb = clamp(db, VU_METER_MIN_DB, VU_METER_MAX_DB)
|
const displayDb = clamp(db, VU_METER_MIN_DB, VU_METER_MAX_DB)
|
||||||
return displayDb <= VU_METER_MIN_DB + 1 ? '-∞' : displayDb.toFixed(1)
|
return displayDb <= VU_METER_MIN_DB + 1 ? '-∞' : displayDb.toFixed(1)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,9 @@ function clampSampleRate(sampleRate: number): number {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function amplitudeToDb(amplitude: number): number {
|
function amplitudeToDb(amplitude: number): number {
|
||||||
|
if (!Number.isFinite(amplitude) || amplitude <= 0) {
|
||||||
|
return VU_METER_MIN_DB
|
||||||
|
}
|
||||||
const db = 20 * Math.log10(Math.max(amplitude, 1e-10))
|
const db = 20 * Math.log10(Math.max(amplitude, 1e-10))
|
||||||
return Math.max(VU_METER_MIN_DB, Math.min(VU_METER_MAX_DB, db))
|
return Math.max(VU_METER_MIN_DB, Math.min(VU_METER_MAX_DB, db))
|
||||||
}
|
}
|
||||||
@@ -119,8 +122,11 @@ export class VUMeterBallistics {
|
|||||||
const cross = left * right
|
const cross = left * right
|
||||||
|
|
||||||
if (this.sampleCount === this.integrationWindowSamples) {
|
if (this.sampleCount === this.integrationWindowSamples) {
|
||||||
this.sumSqL -= this.sqL[this.writeIndex]
|
// Clamp running sums to non-negative: floating-point cancellation in
|
||||||
this.sumSqR -= this.sqR[this.writeIndex]
|
// the slide-out subtraction can drift them by ~1e-15 below zero on
|
||||||
|
// near-silent content, which would propagate as NaN through sqrt.
|
||||||
|
this.sumSqL = Math.max(0, this.sumSqL - this.sqL[this.writeIndex])
|
||||||
|
this.sumSqR = Math.max(0, this.sumSqR - this.sqR[this.writeIndex])
|
||||||
this.sumCross -= this.cross[this.writeIndex]
|
this.sumCross -= this.cross[this.writeIndex]
|
||||||
} else {
|
} else {
|
||||||
this.sampleCount += 1
|
this.sampleCount += 1
|
||||||
@@ -163,9 +169,9 @@ export class VUMeterBallistics {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const meanSqL = this.sumSqL / this.sampleCount
|
const meanSqL = Math.max(0, this.sumSqL) / this.sampleCount
|
||||||
const meanSqR = this.sumSqR / this.sampleCount
|
const meanSqR = Math.max(0, this.sumSqR) / this.sampleCount
|
||||||
const denominator = Math.sqrt(this.sumSqL * this.sumSqR)
|
const denominator = Math.sqrt(Math.max(0, this.sumSqL) * Math.max(0, this.sumSqR))
|
||||||
|
|
||||||
this.snapshot.vuLDb = amplitudeToDb(Math.sqrt(meanSqL))
|
this.snapshot.vuLDb = amplitudeToDb(Math.sqrt(meanSqL))
|
||||||
this.snapshot.vuRDb = amplitudeToDb(Math.sqrt(meanSqR))
|
this.snapshot.vuRDb = amplitudeToDb(Math.sqrt(meanSqR))
|
||||||
|
|||||||
@@ -3512,6 +3512,31 @@ test('VUMeterBallistics produces the same VU, bar, and correlation for contiguou
|
|||||||
assertAlmostEqual(irregularSnapshot.correlation, contiguousSnapshot.correlation, 1e-6, 'correlation should be chunking-invariant')
|
assertAlmostEqual(irregularSnapshot.correlation, contiguousSnapshot.correlation, 1e-6, 'correlation should be chunking-invariant')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('VUMeterBallistics keeps RMS finite across loud-then-silent sequences (NaN-drift regression)', () => {
|
||||||
|
const sampleRate = 48000
|
||||||
|
const meter = new VUMeterBallistics(sampleRate)
|
||||||
|
const windowSamples = Math.round((sampleRate * VU_INTEGRATION_WINDOW_MS) / 1000)
|
||||||
|
|
||||||
|
// Run loud audio long enough to fully populate the sliding-window buffer.
|
||||||
|
meter.process([createFilledStereoChunk(0.7, 0.7, windowSamples * 2)], 100)
|
||||||
|
|
||||||
|
// Then feed many windows of digital silence. Floating-point cancellation in
|
||||||
|
// the running-sum slide-out used to drift sumSqL/sumSqR slightly below zero,
|
||||||
|
// producing NaN through Math.sqrt. After the fix, the RMS must remain finite
|
||||||
|
// and decay all the way to VU_METER_MIN_DB.
|
||||||
|
let nowMs = 100
|
||||||
|
for (let i = 0; i < 20; i += 1) {
|
||||||
|
nowMs += 50
|
||||||
|
meter.process([createFilledStereoChunk(0, 0, windowSamples)], nowMs)
|
||||||
|
}
|
||||||
|
|
||||||
|
const snapshot = meter.getSnapshot()
|
||||||
|
assert.ok(Number.isFinite(snapshot.vuLDb), 'left VU must stay finite (no NaN)')
|
||||||
|
assert.ok(Number.isFinite(snapshot.vuRDb), 'right VU must stay finite (no NaN)')
|
||||||
|
assert.equal(snapshot.vuLDb, VU_METER_MIN_DB)
|
||||||
|
assert.equal(snapshot.vuRDb, VU_METER_MIN_DB)
|
||||||
|
})
|
||||||
|
|
||||||
test('VUMeterBallistics lets the bar outrun the VU needle while peak hold remains highest', () => {
|
test('VUMeterBallistics lets the bar outrun the VU needle while peak hold remains highest', () => {
|
||||||
const sampleRate = 48000
|
const sampleRate = 48000
|
||||||
const meter = new VUMeterBallistics(sampleRate)
|
const meter = new VUMeterBallistics(sampleRate)
|
||||||
@@ -3696,8 +3721,16 @@ test('LUFSMeter draws compact fast bars, a thicker LUFS bar, scale labels, and a
|
|||||||
recorder.fillRects.some((rect) => rect.fillStyle === 'rgb(255, 0, 96)' && rect.width > 12 && rect.width < 80),
|
recorder.fillRects.some((rect) => rect.fillStyle === 'rgb(255, 0, 96)' && rect.width > 12 && rect.width < 80),
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
|
// Readout tag pill: drawn with line color, fits to text width with padding,
|
||||||
|
// and uses the compact tag height. Sized smaller than the LUFS level bar.
|
||||||
assert.equal(
|
assert.equal(
|
||||||
recorder.fillRects.some((rect) => rect.fillStyle === 'rgb(255, 0, 96)' && rect.width > 120 && rect.height <= 34),
|
recorder.fillRects.some((rect) =>
|
||||||
|
rect.fillStyle === 'rgb(255, 0, 96)'
|
||||||
|
&& rect.width >= 40
|
||||||
|
&& rect.width <= 100
|
||||||
|
&& rect.height >= 14
|
||||||
|
&& rect.height <= 24,
|
||||||
|
),
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
assert.equal(recorder.fillRects.some((rect) => rect.fillStyle === 'rgb(1, 2, 3)'), true)
|
assert.equal(recorder.fillRects.some((rect) => rect.fillStyle === 'rgb(1, 2, 3)'), true)
|
||||||
|
|||||||
Reference in New Issue
Block a user