mirror of
https://github.com/Boof2015/prism.git
synced 2026-08-21 13:03:27 +02:00
update VU bar
This commit is contained in:
@@ -56,8 +56,10 @@ export class VUMeter {
|
|||||||
private unsubscribeSessionChange: (() => void) | null = null
|
private unsubscribeSessionChange: (() => void) | null = null
|
||||||
|
|
||||||
// Meter state
|
// Meter state
|
||||||
private rmsL = VU_METER_MIN_DB
|
private vuL = VU_METER_MIN_DB
|
||||||
private rmsR = VU_METER_MIN_DB
|
private vuR = VU_METER_MIN_DB
|
||||||
|
private barL = VU_METER_MIN_DB
|
||||||
|
private barR = VU_METER_MIN_DB
|
||||||
private peakL = VU_METER_MIN_DB
|
private peakL = VU_METER_MIN_DB
|
||||||
private peakR = VU_METER_MIN_DB
|
private peakR = VU_METER_MIN_DB
|
||||||
private correlation = 0
|
private correlation = 0
|
||||||
@@ -124,8 +126,10 @@ export class VUMeter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private applySnapshot(snapshot: VUMeterSnapshot): void {
|
private applySnapshot(snapshot: VUMeterSnapshot): void {
|
||||||
this.rmsL = snapshot.rmsLDb
|
this.vuL = snapshot.vuLDb
|
||||||
this.rmsR = snapshot.rmsRDb
|
this.vuR = snapshot.vuRDb
|
||||||
|
this.barL = snapshot.barLDb
|
||||||
|
this.barR = snapshot.barRDb
|
||||||
this.peakL = snapshot.peakLDb
|
this.peakL = snapshot.peakLDb
|
||||||
this.peakR = snapshot.peakRDb
|
this.peakR = snapshot.peakRDb
|
||||||
this.correlation = snapshot.correlation
|
this.correlation = snapshot.correlation
|
||||||
@@ -178,15 +182,15 @@ export class VUMeter {
|
|||||||
|
|
||||||
// ---- L meter ----
|
// ---- L meter ----
|
||||||
const lY = topOffset
|
const lY = topOffset
|
||||||
this.drawHorizontalMeterBar(ctx, barLeft, lY, barWidth, meterHeight, this.rmsL, this.peakL, cr, cg, cb)
|
this.drawHorizontalMeterBar(ctx, barLeft, lY, barWidth, meterHeight, this.barL, this.peakL, cr, cg, cb)
|
||||||
this.drawMeterLabel(ctx, 0, lY, labelWidth, meterHeight, 'L')
|
this.drawMeterLabel(ctx, 0, lY, labelWidth, meterHeight, 'L')
|
||||||
this.drawDbLabel(ctx, barRight + 4, lY, dbLabelWidth, meterHeight, this.rmsL)
|
this.drawDbLabel(ctx, barRight + 4, lY, dbLabelWidth, meterHeight, this.barL)
|
||||||
|
|
||||||
// ---- R meter ----
|
// ---- R meter ----
|
||||||
const rY = lY + meterHeight + gap
|
const rY = lY + meterHeight + gap
|
||||||
this.drawHorizontalMeterBar(ctx, barLeft, rY, barWidth, meterHeight, this.rmsR, this.peakR, cr, cg, cb)
|
this.drawHorizontalMeterBar(ctx, barLeft, rY, barWidth, meterHeight, this.barR, this.peakR, cr, cg, cb)
|
||||||
this.drawMeterLabel(ctx, 0, rY, labelWidth, meterHeight, 'R')
|
this.drawMeterLabel(ctx, 0, rY, labelWidth, meterHeight, 'R')
|
||||||
this.drawDbLabel(ctx, barRight + 4, rY, dbLabelWidth, meterHeight, this.rmsR)
|
this.drawDbLabel(ctx, barRight + 4, rY, dbLabelWidth, meterHeight, this.barR)
|
||||||
|
|
||||||
// ---- Correlation meter ----
|
// ---- Correlation meter ----
|
||||||
const corrY = rY + meterHeight + gap
|
const corrY = rY + meterHeight + gap
|
||||||
@@ -219,12 +223,12 @@ export class VUMeter {
|
|||||||
const rX = meterLeft + meterWidth + channelGap
|
const rX = meterLeft + meterWidth + channelGap
|
||||||
|
|
||||||
this.drawMeterLabel(ctx, lX, 0, meterWidth, labelHeight, 'L')
|
this.drawMeterLabel(ctx, lX, 0, meterWidth, labelHeight, 'L')
|
||||||
this.drawVerticalMeterBar(ctx, lX, meterTop, meterWidth, meterHeight, this.rmsL, this.peakL, cr, cg, cb)
|
this.drawVerticalMeterBar(ctx, lX, meterTop, meterWidth, meterHeight, this.barL, this.peakL, cr, cg, cb)
|
||||||
this.drawCenteredDbLabel(ctx, lX, dbY, meterWidth, dbHeight, this.rmsL)
|
this.drawCenteredDbLabel(ctx, lX, dbY, meterWidth, dbHeight, this.barL)
|
||||||
|
|
||||||
this.drawMeterLabel(ctx, rX, 0, meterWidth, labelHeight, 'R')
|
this.drawMeterLabel(ctx, rX, 0, meterWidth, labelHeight, 'R')
|
||||||
this.drawVerticalMeterBar(ctx, rX, meterTop, meterWidth, meterHeight, this.rmsR, this.peakR, cr, cg, cb)
|
this.drawVerticalMeterBar(ctx, rX, meterTop, meterWidth, meterHeight, this.barR, this.peakR, cr, cg, cb)
|
||||||
this.drawCenteredDbLabel(ctx, rX, dbY, meterWidth, dbHeight, this.rmsR)
|
this.drawCenteredDbLabel(ctx, rX, dbY, meterWidth, dbHeight, this.barR)
|
||||||
|
|
||||||
this.drawCorrelationBar(ctx, corrX, corrY, corrWidth, corrHeight, cr, cg, cb)
|
this.drawCorrelationBar(ctx, corrX, corrY, corrWidth, corrHeight, cr, cg, cb)
|
||||||
}
|
}
|
||||||
@@ -232,28 +236,28 @@ export class VUMeter {
|
|||||||
private drawHorizontalMeterBar(
|
private drawHorizontalMeterBar(
|
||||||
ctx: CanvasRenderingContext2D,
|
ctx: CanvasRenderingContext2D,
|
||||||
x: number, y: number, w: number, h: number,
|
x: number, y: number, w: number, h: number,
|
||||||
rmsDb: number, peakDb: number,
|
levelDb: number, peakDb: number,
|
||||||
cr: number, cg: number, cb: number
|
cr: number, cg: number, cb: number
|
||||||
): void {
|
): void {
|
||||||
const rmsNorm = this.dbToNormalized(rmsDb)
|
const levelNorm = this.dbToNormalized(levelDb)
|
||||||
const peakNorm = this.dbToNormalized(peakDb)
|
const peakNorm = this.dbToNormalized(peakDb)
|
||||||
const rmsWidth = rmsNorm * w
|
const levelWidth = levelNorm * w
|
||||||
const hotThreshold = this.dbToNormalized(-6) * w
|
const hotThreshold = this.dbToNormalized(-6) * w
|
||||||
|
|
||||||
// Background track
|
// Background track
|
||||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.04)'
|
ctx.fillStyle = 'rgba(255, 255, 255, 0.04)'
|
||||||
ctx.fillRect(x, y, w, h)
|
ctx.fillRect(x, y, w, h)
|
||||||
|
|
||||||
// RMS bar
|
// Main level bar
|
||||||
if (rmsWidth > 0) {
|
if (levelWidth > 0) {
|
||||||
const safeWidth = Math.min(rmsWidth, hotThreshold)
|
const safeWidth = Math.min(levelWidth, hotThreshold)
|
||||||
if (safeWidth > 0) {
|
if (safeWidth > 0) {
|
||||||
ctx.fillStyle = colorWithAlpha(cr, cg, cb, 0.82)
|
ctx.fillStyle = colorWithAlpha(cr, cg, cb, 0.82)
|
||||||
ctx.fillRect(x, y, safeWidth, h)
|
ctx.fillRect(x, y, safeWidth, h)
|
||||||
}
|
}
|
||||||
if (rmsWidth > hotThreshold) {
|
if (levelWidth > hotThreshold) {
|
||||||
// Hot zone: transition to warm/red
|
// Hot zone: transition to warm/red
|
||||||
const hotWidth = rmsWidth - hotThreshold
|
const hotWidth = levelWidth - hotThreshold
|
||||||
const hotProgress = Math.min(1, hotWidth / Math.max(1, w - hotThreshold))
|
const hotProgress = Math.min(1, hotWidth / Math.max(1, w - hotThreshold))
|
||||||
const hotR = Math.round(cr + (255 - cr) * hotProgress * 0.7)
|
const hotR = Math.round(cr + (255 - cr) * hotProgress * 0.7)
|
||||||
const hotG = Math.round(cg * (1 - hotProgress * 0.6))
|
const hotG = Math.round(cg * (1 - hotProgress * 0.6))
|
||||||
@@ -285,31 +289,31 @@ export class VUMeter {
|
|||||||
private drawVerticalMeterBar(
|
private drawVerticalMeterBar(
|
||||||
ctx: CanvasRenderingContext2D,
|
ctx: CanvasRenderingContext2D,
|
||||||
x: number, y: number, w: number, h: number,
|
x: number, y: number, w: number, h: number,
|
||||||
rmsDb: number, peakDb: number,
|
levelDb: number, peakDb: number,
|
||||||
cr: number, cg: number, cb: number
|
cr: number, cg: number, cb: number
|
||||||
): void {
|
): void {
|
||||||
const rmsNorm = this.dbToNormalized(rmsDb)
|
const levelNorm = this.dbToNormalized(levelDb)
|
||||||
const peakNorm = this.dbToNormalized(peakDb)
|
const peakNorm = this.dbToNormalized(peakDb)
|
||||||
const rmsHeight = rmsNorm * h
|
const levelHeight = levelNorm * h
|
||||||
const hotThreshold = this.dbToNormalized(-6) * h
|
const hotThreshold = this.dbToNormalized(-6) * h
|
||||||
|
|
||||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.04)'
|
ctx.fillStyle = 'rgba(255, 255, 255, 0.04)'
|
||||||
ctx.fillRect(x, y, w, h)
|
ctx.fillRect(x, y, w, h)
|
||||||
|
|
||||||
if (rmsHeight > 0) {
|
if (levelHeight > 0) {
|
||||||
const safeHeight = Math.min(rmsHeight, hotThreshold)
|
const safeHeight = Math.min(levelHeight, hotThreshold)
|
||||||
if (safeHeight > 0) {
|
if (safeHeight > 0) {
|
||||||
ctx.fillStyle = colorWithAlpha(cr, cg, cb, 0.82)
|
ctx.fillStyle = colorWithAlpha(cr, cg, cb, 0.82)
|
||||||
ctx.fillRect(x, y + h - safeHeight, w, safeHeight)
|
ctx.fillRect(x, y + h - safeHeight, w, safeHeight)
|
||||||
}
|
}
|
||||||
if (rmsHeight > hotThreshold) {
|
if (levelHeight > hotThreshold) {
|
||||||
const hotHeight = rmsHeight - hotThreshold
|
const hotHeight = levelHeight - hotThreshold
|
||||||
const hotProgress = Math.min(1, hotHeight / Math.max(1, h - hotThreshold))
|
const hotProgress = Math.min(1, hotHeight / Math.max(1, h - hotThreshold))
|
||||||
const hotR = Math.round(cr + (255 - cr) * hotProgress * 0.7)
|
const hotR = Math.round(cr + (255 - cr) * hotProgress * 0.7)
|
||||||
const hotG = Math.round(cg * (1 - hotProgress * 0.6))
|
const hotG = Math.round(cg * (1 - hotProgress * 0.6))
|
||||||
const hotB = Math.round(cb * (1 - hotProgress * 0.7))
|
const hotB = Math.round(cb * (1 - hotProgress * 0.7))
|
||||||
ctx.fillStyle = colorWithAlpha(hotR, hotG, hotB, 0.82)
|
ctx.fillStyle = colorWithAlpha(hotR, hotG, hotB, 0.82)
|
||||||
ctx.fillRect(x, y + h - rmsHeight, w, hotHeight)
|
ctx.fillRect(x, y + h - levelHeight, w, hotHeight)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -427,9 +431,9 @@ export class VUMeter {
|
|||||||
const barWidth = Math.max(1, barRight - barLeft)
|
const barWidth = Math.max(1, barRight - barLeft)
|
||||||
|
|
||||||
// L needle
|
// L needle
|
||||||
this.drawNeedleMeter(ctx, 0, 0, meterWidth, meterAreaHeight, this.rmsL, this.peakL, 'L', cr, cg, cb)
|
this.drawNeedleMeter(ctx, 0, 0, meterWidth, meterAreaHeight, this.vuL, this.peakL, 'L', cr, cg, cb)
|
||||||
// R needle
|
// R needle
|
||||||
this.drawNeedleMeter(ctx, meterWidth + 4, 0, meterWidth, meterAreaHeight, this.rmsR, this.peakR, 'R', cr, cg, cb)
|
this.drawNeedleMeter(ctx, meterWidth + 4, 0, meterWidth, meterAreaHeight, this.vuR, this.peakR, 'R', cr, cg, cb)
|
||||||
|
|
||||||
// Correlation bar at bottom
|
// Correlation bar at bottom
|
||||||
this.drawCorrelationBar(ctx, barLeft, meterAreaHeight + gap, barWidth, corrHeight, cr, cg, cb)
|
this.drawCorrelationBar(ctx, barLeft, meterAreaHeight + gap, barWidth, corrHeight, cr, cg, cb)
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ export interface VUMeterStereoChunk {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface VUMeterSnapshot {
|
export interface VUMeterSnapshot {
|
||||||
rmsLDb: number
|
vuLDb: number
|
||||||
rmsRDb: number
|
vuRDb: number
|
||||||
|
barLDb: number
|
||||||
|
barRDb: number
|
||||||
peakLDb: number
|
peakLDb: number
|
||||||
peakRDb: number
|
peakRDb: number
|
||||||
correlation: number
|
correlation: number
|
||||||
@@ -16,10 +18,14 @@ export const VU_METER_MAX_DB = 0
|
|||||||
export const VU_INTEGRATION_WINDOW_MS = 300
|
export const VU_INTEGRATION_WINDOW_MS = 300
|
||||||
export const VU_PEAK_HOLD_MS = 750
|
export const VU_PEAK_HOLD_MS = 750
|
||||||
export const VU_PEAK_DECAY_DB_PER_SECOND = 18
|
export const VU_PEAK_DECAY_DB_PER_SECOND = 18
|
||||||
|
const BAR_ATTACK_MS = 5
|
||||||
|
const BAR_RELEASE_MS = 180
|
||||||
|
|
||||||
const INITIAL_SNAPSHOT: VUMeterSnapshot = {
|
const INITIAL_SNAPSHOT: VUMeterSnapshot = {
|
||||||
rmsLDb: VU_METER_MIN_DB,
|
vuLDb: VU_METER_MIN_DB,
|
||||||
rmsRDb: VU_METER_MIN_DB,
|
vuRDb: VU_METER_MIN_DB,
|
||||||
|
barLDb: VU_METER_MIN_DB,
|
||||||
|
barRDb: VU_METER_MIN_DB,
|
||||||
peakLDb: VU_METER_MIN_DB,
|
peakLDb: VU_METER_MIN_DB,
|
||||||
peakRDb: VU_METER_MIN_DB,
|
peakRDb: VU_METER_MIN_DB,
|
||||||
correlation: 0,
|
correlation: 0,
|
||||||
@@ -48,6 +54,10 @@ export class VUMeterBallistics {
|
|||||||
private peakHoldUntilL = 0
|
private peakHoldUntilL = 0
|
||||||
private peakHoldUntilR = 0
|
private peakHoldUntilR = 0
|
||||||
private lastPeakUpdateMs: number | null = null
|
private lastPeakUpdateMs: number | null = null
|
||||||
|
private barEnvelopeL = 0
|
||||||
|
private barEnvelopeR = 0
|
||||||
|
private barAttackCoeff = 0
|
||||||
|
private barReleaseCoeff = 0
|
||||||
private snapshot: VUMeterSnapshot = { ...INITIAL_SNAPSHOT }
|
private snapshot: VUMeterSnapshot = { ...INITIAL_SNAPSHOT }
|
||||||
|
|
||||||
constructor(sampleRate: number) {
|
constructor(sampleRate: number) {
|
||||||
@@ -67,6 +77,8 @@ export class VUMeterBallistics {
|
|||||||
this.sqL = new Float64Array(this.integrationWindowSamples)
|
this.sqL = new Float64Array(this.integrationWindowSamples)
|
||||||
this.sqR = new Float64Array(this.integrationWindowSamples)
|
this.sqR = new Float64Array(this.integrationWindowSamples)
|
||||||
this.cross = new Float64Array(this.integrationWindowSamples)
|
this.cross = new Float64Array(this.integrationWindowSamples)
|
||||||
|
this.barAttackCoeff = Math.exp(-1 / (this.sampleRate * (BAR_ATTACK_MS / 1000)))
|
||||||
|
this.barReleaseCoeff = Math.exp(-1 / (this.sampleRate * (BAR_RELEASE_MS / 1000)))
|
||||||
this.reset()
|
this.reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,6 +94,8 @@ export class VUMeterBallistics {
|
|||||||
this.peakHoldUntilL = 0
|
this.peakHoldUntilL = 0
|
||||||
this.peakHoldUntilR = 0
|
this.peakHoldUntilR = 0
|
||||||
this.lastPeakUpdateMs = null
|
this.lastPeakUpdateMs = null
|
||||||
|
this.barEnvelopeL = 0
|
||||||
|
this.barEnvelopeR = 0
|
||||||
this.snapshot = { ...INITIAL_SNAPSHOT }
|
this.snapshot = { ...INITIAL_SNAPSHOT }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,6 +136,8 @@ export class VUMeterBallistics {
|
|||||||
|
|
||||||
const absL = Math.abs(left)
|
const absL = Math.abs(left)
|
||||||
const absR = Math.abs(right)
|
const absR = Math.abs(right)
|
||||||
|
this.barEnvelopeL = this.updateBarEnvelope(this.barEnvelopeL, absL)
|
||||||
|
this.barEnvelopeR = this.updateBarEnvelope(this.barEnvelopeR, absR)
|
||||||
if (absL > maxPeakL) maxPeakL = absL
|
if (absL > maxPeakL) maxPeakL = absL
|
||||||
if (absR > maxPeakR) maxPeakR = absR
|
if (absR > maxPeakR) maxPeakR = absR
|
||||||
}
|
}
|
||||||
@@ -139,8 +155,10 @@ export class VUMeterBallistics {
|
|||||||
|
|
||||||
private recomputeSnapshot(): void {
|
private recomputeSnapshot(): void {
|
||||||
if (this.sampleCount <= 0) {
|
if (this.sampleCount <= 0) {
|
||||||
this.snapshot.rmsLDb = VU_METER_MIN_DB
|
this.snapshot.vuLDb = VU_METER_MIN_DB
|
||||||
this.snapshot.rmsRDb = VU_METER_MIN_DB
|
this.snapshot.vuRDb = VU_METER_MIN_DB
|
||||||
|
this.snapshot.barLDb = amplitudeToDb(this.barEnvelopeL)
|
||||||
|
this.snapshot.barRDb = amplitudeToDb(this.barEnvelopeR)
|
||||||
this.snapshot.correlation = 0
|
this.snapshot.correlation = 0
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -149,13 +167,20 @@ export class VUMeterBallistics {
|
|||||||
const meanSqR = this.sumSqR / this.sampleCount
|
const meanSqR = this.sumSqR / this.sampleCount
|
||||||
const denominator = Math.sqrt(this.sumSqL * this.sumSqR)
|
const denominator = Math.sqrt(this.sumSqL * this.sumSqR)
|
||||||
|
|
||||||
this.snapshot.rmsLDb = amplitudeToDb(Math.sqrt(meanSqL))
|
this.snapshot.vuLDb = amplitudeToDb(Math.sqrt(meanSqL))
|
||||||
this.snapshot.rmsRDb = amplitudeToDb(Math.sqrt(meanSqR))
|
this.snapshot.vuRDb = amplitudeToDb(Math.sqrt(meanSqR))
|
||||||
|
this.snapshot.barLDb = amplitudeToDb(this.barEnvelopeL)
|
||||||
|
this.snapshot.barRDb = amplitudeToDb(this.barEnvelopeR)
|
||||||
this.snapshot.correlation = denominator > 1e-10
|
this.snapshot.correlation = denominator > 1e-10
|
||||||
? Math.max(-1, Math.min(1, this.sumCross / denominator))
|
? Math.max(-1, Math.min(1, this.sumCross / denominator))
|
||||||
: 0
|
: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private updateBarEnvelope(current: number, input: number): number {
|
||||||
|
const coeff = input > current ? this.barAttackCoeff : this.barReleaseCoeff
|
||||||
|
return coeff * current + (1 - coeff) * input
|
||||||
|
}
|
||||||
|
|
||||||
private advancePeaks(nowMs: number): void {
|
private advancePeaks(nowMs: number): void {
|
||||||
if (!Number.isFinite(nowMs)) {
|
if (!Number.isFinite(nowMs)) {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -432,21 +432,23 @@ test('moveDockedScopeOrder preserves popped-out scope positions in the full orde
|
|||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
test('VUMeterBallistics holds RMS and correlation steady when an active frame receives no chunks', () => {
|
test('VUMeterBallistics holds VU, bar, and correlation steady when an active frame receives no chunks', () => {
|
||||||
const sampleRate = 48000
|
const sampleRate = 48000
|
||||||
const windowSamples = Math.round((sampleRate * VU_INTEGRATION_WINDOW_MS) / 1000)
|
const windowSamples = Math.round((sampleRate * VU_INTEGRATION_WINDOW_MS) / 1000)
|
||||||
const meter = new VUMeterBallistics(sampleRate)
|
const meter = new VUMeterBallistics(sampleRate)
|
||||||
const initial = meter.process([createFilledStereoChunk(0.5, 0.5, windowSamples)], 300)
|
const initial = meter.process([createFilledStereoChunk(0.5, 0.5, windowSamples)], 300)
|
||||||
const held = meter.process([], 316)
|
const held = meter.process([], 316)
|
||||||
|
|
||||||
assert.ok(initial.rmsLDb > -7 && initial.rmsLDb < -5)
|
assert.ok(initial.vuLDb > -7 && initial.vuLDb < -5)
|
||||||
assertAlmostEqual(held.rmsLDb, initial.rmsLDb, 1e-12, 'left RMS should hold across empty frames')
|
assertAlmostEqual(held.vuLDb, initial.vuLDb, 1e-12, 'left VU should hold across empty frames')
|
||||||
assertAlmostEqual(held.rmsRDb, initial.rmsRDb, 1e-12, 'right RMS should hold across empty frames')
|
assertAlmostEqual(held.vuRDb, initial.vuRDb, 1e-12, 'right VU should hold across empty frames')
|
||||||
|
assertAlmostEqual(held.barLDb, initial.barLDb, 1e-12, 'left bar should hold across empty frames')
|
||||||
|
assertAlmostEqual(held.barRDb, initial.barRDb, 1e-12, 'right bar should hold across empty frames')
|
||||||
assertAlmostEqual(held.correlation, initial.correlation, 1e-12, 'correlation should hold across empty frames')
|
assertAlmostEqual(held.correlation, initial.correlation, 1e-12, 'correlation should hold across empty frames')
|
||||||
assert.notEqual(held.rmsLDb, VU_METER_MIN_DB)
|
assert.notEqual(held.vuLDb, VU_METER_MIN_DB)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('VUMeterBallistics produces the same RMS and correlation for contiguous and irregular chunk delivery', () => {
|
test('VUMeterBallistics produces the same VU, bar, and correlation for contiguous and irregular chunk delivery', () => {
|
||||||
const sampleRate = 48000
|
const sampleRate = 48000
|
||||||
const windowSamples = Math.round((sampleRate * VU_INTEGRATION_WINDOW_MS) / 1000)
|
const windowSamples = Math.round((sampleRate * VU_INTEGRATION_WINDOW_MS) / 1000)
|
||||||
const program = createProgramSamples(windowSamples)
|
const program = createProgramSamples(windowSamples)
|
||||||
@@ -471,29 +473,28 @@ test('VUMeterBallistics produces the same RMS and correlation for contiguous and
|
|||||||
}
|
}
|
||||||
|
|
||||||
const irregularSnapshot = irregular.getSnapshot()
|
const irregularSnapshot = irregular.getSnapshot()
|
||||||
assertAlmostEqual(irregularSnapshot.rmsLDb, contiguousSnapshot.rmsLDb, 1e-6, 'left RMS should be chunking-invariant')
|
assertAlmostEqual(irregularSnapshot.vuLDb, contiguousSnapshot.vuLDb, 1e-6, 'left VU should be chunking-invariant')
|
||||||
assertAlmostEqual(irregularSnapshot.rmsRDb, contiguousSnapshot.rmsRDb, 1e-6, 'right RMS should be chunking-invariant')
|
assertAlmostEqual(irregularSnapshot.vuRDb, contiguousSnapshot.vuRDb, 1e-6, 'right VU should be chunking-invariant')
|
||||||
|
assertAlmostEqual(irregularSnapshot.barLDb, contiguousSnapshot.barLDb, 1e-6, 'left bar should be chunking-invariant')
|
||||||
|
assertAlmostEqual(irregularSnapshot.barRDb, contiguousSnapshot.barRDb, 1e-6, 'right bar should be chunking-invariant')
|
||||||
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 tracks a transient peak independently of the RMS bar and decays it by elapsed time', () => {
|
test('VUMeterBallistics lets the bar outrun the VU needle while peak hold remains highest', () => {
|
||||||
const sampleRate = 48000
|
const sampleRate = 48000
|
||||||
const windowSamples = Math.round((sampleRate * VU_INTEGRATION_WINDOW_MS) / 1000)
|
|
||||||
const left = new Float32Array(windowSamples)
|
|
||||||
const right = new Float32Array(windowSamples)
|
|
||||||
left.fill(0.1)
|
|
||||||
right.fill(0.1)
|
|
||||||
left[Math.floor(windowSamples / 2)] = 1.0
|
|
||||||
right[Math.floor(windowSamples / 2)] = 1.0
|
|
||||||
|
|
||||||
const meter = new VUMeterBallistics(sampleRate)
|
const meter = new VUMeterBallistics(sampleRate)
|
||||||
const initial = meter.process([{ left, right }], 300)
|
const baselineSamples = Math.floor(sampleRate * 0.2)
|
||||||
const beforeDecay = meter.process([], 300 + VU_PEAK_HOLD_MS - 10)
|
const burstSamples = Math.floor(sampleRate * 0.016)
|
||||||
const afterDecay = meter.process([], 300 + VU_PEAK_HOLD_MS + 100)
|
|
||||||
|
|
||||||
assert.ok(initial.rmsLDb < -19)
|
meter.process([createFilledStereoChunk(0.1, 0.1, baselineSamples)], 200)
|
||||||
assert.equal(initial.peakLDb, 0)
|
const burstSnapshot = meter.process([createFilledStereoChunk(1.0, 1.0, burstSamples)], 216)
|
||||||
assert.ok(initial.peakLDb > initial.rmsLDb + 10)
|
const beforeDecay = meter.process([], 216 + VU_PEAK_HOLD_MS - 10)
|
||||||
|
const afterDecay = meter.process([], 216 + VU_PEAK_HOLD_MS + 100)
|
||||||
|
|
||||||
|
assert.ok(burstSnapshot.vuLDb < -10)
|
||||||
|
assert.ok(burstSnapshot.barLDb > burstSnapshot.vuLDb + 8)
|
||||||
|
assert.ok(burstSnapshot.peakLDb >= burstSnapshot.barLDb)
|
||||||
|
assert.equal(burstSnapshot.peakLDb, 0)
|
||||||
assert.equal(beforeDecay.peakLDb, 0)
|
assert.equal(beforeDecay.peakLDb, 0)
|
||||||
assert.ok(afterDecay.peakLDb < beforeDecay.peakLDb)
|
assert.ok(afterDecay.peakLDb < beforeDecay.peakLDb)
|
||||||
assert.ok(afterDecay.peakLDb > -3)
|
assert.ok(afterDecay.peakLDb > -3)
|
||||||
|
|||||||
Reference in New Issue
Block a user