mirror of
https://github.com/Boof2015/prism.git
synced 2026-08-17 11:11:18 +02:00
update VU bar
This commit is contained in:
@@ -56,8 +56,10 @@ export class VUMeter {
|
||||
private unsubscribeSessionChange: (() => void) | null = null
|
||||
|
||||
// Meter state
|
||||
private rmsL = VU_METER_MIN_DB
|
||||
private rmsR = VU_METER_MIN_DB
|
||||
private vuL = 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 peakR = VU_METER_MIN_DB
|
||||
private correlation = 0
|
||||
@@ -124,8 +126,10 @@ export class VUMeter {
|
||||
}
|
||||
|
||||
private applySnapshot(snapshot: VUMeterSnapshot): void {
|
||||
this.rmsL = snapshot.rmsLDb
|
||||
this.rmsR = snapshot.rmsRDb
|
||||
this.vuL = snapshot.vuLDb
|
||||
this.vuR = snapshot.vuRDb
|
||||
this.barL = snapshot.barLDb
|
||||
this.barR = snapshot.barRDb
|
||||
this.peakL = snapshot.peakLDb
|
||||
this.peakR = snapshot.peakRDb
|
||||
this.correlation = snapshot.correlation
|
||||
@@ -178,15 +182,15 @@ export class VUMeter {
|
||||
|
||||
// ---- L meter ----
|
||||
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.drawDbLabel(ctx, barRight + 4, lY, dbLabelWidth, meterHeight, this.rmsL)
|
||||
this.drawDbLabel(ctx, barRight + 4, lY, dbLabelWidth, meterHeight, this.barL)
|
||||
|
||||
// ---- R meter ----
|
||||
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.drawDbLabel(ctx, barRight + 4, rY, dbLabelWidth, meterHeight, this.rmsR)
|
||||
this.drawDbLabel(ctx, barRight + 4, rY, dbLabelWidth, meterHeight, this.barR)
|
||||
|
||||
// ---- Correlation meter ----
|
||||
const corrY = rY + meterHeight + gap
|
||||
@@ -219,12 +223,12 @@ export class VUMeter {
|
||||
const rX = meterLeft + meterWidth + channelGap
|
||||
|
||||
this.drawMeterLabel(ctx, lX, 0, meterWidth, labelHeight, 'L')
|
||||
this.drawVerticalMeterBar(ctx, lX, meterTop, meterWidth, meterHeight, this.rmsL, this.peakL, cr, cg, cb)
|
||||
this.drawCenteredDbLabel(ctx, lX, dbY, meterWidth, dbHeight, this.rmsL)
|
||||
this.drawVerticalMeterBar(ctx, lX, meterTop, meterWidth, meterHeight, this.barL, this.peakL, cr, cg, cb)
|
||||
this.drawCenteredDbLabel(ctx, lX, dbY, meterWidth, dbHeight, this.barL)
|
||||
|
||||
this.drawMeterLabel(ctx, rX, 0, meterWidth, labelHeight, 'R')
|
||||
this.drawVerticalMeterBar(ctx, rX, meterTop, meterWidth, meterHeight, this.rmsR, this.peakR, cr, cg, cb)
|
||||
this.drawCenteredDbLabel(ctx, rX, dbY, meterWidth, dbHeight, this.rmsR)
|
||||
this.drawVerticalMeterBar(ctx, rX, meterTop, meterWidth, meterHeight, this.barR, this.peakR, cr, cg, cb)
|
||||
this.drawCenteredDbLabel(ctx, rX, dbY, meterWidth, dbHeight, this.barR)
|
||||
|
||||
this.drawCorrelationBar(ctx, corrX, corrY, corrWidth, corrHeight, cr, cg, cb)
|
||||
}
|
||||
@@ -232,28 +236,28 @@ export class VUMeter {
|
||||
private drawHorizontalMeterBar(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
x: number, y: number, w: number, h: number,
|
||||
rmsDb: number, peakDb: number,
|
||||
levelDb: number, peakDb: number,
|
||||
cr: number, cg: number, cb: number
|
||||
): void {
|
||||
const rmsNorm = this.dbToNormalized(rmsDb)
|
||||
const levelNorm = this.dbToNormalized(levelDb)
|
||||
const peakNorm = this.dbToNormalized(peakDb)
|
||||
const rmsWidth = rmsNorm * w
|
||||
const levelWidth = levelNorm * w
|
||||
const hotThreshold = this.dbToNormalized(-6) * w
|
||||
|
||||
// Background track
|
||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.04)'
|
||||
ctx.fillRect(x, y, w, h)
|
||||
|
||||
// RMS bar
|
||||
if (rmsWidth > 0) {
|
||||
const safeWidth = Math.min(rmsWidth, hotThreshold)
|
||||
// Main level bar
|
||||
if (levelWidth > 0) {
|
||||
const safeWidth = Math.min(levelWidth, hotThreshold)
|
||||
if (safeWidth > 0) {
|
||||
ctx.fillStyle = colorWithAlpha(cr, cg, cb, 0.82)
|
||||
ctx.fillRect(x, y, safeWidth, h)
|
||||
}
|
||||
if (rmsWidth > hotThreshold) {
|
||||
if (levelWidth > hotThreshold) {
|
||||
// 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 hotR = Math.round(cr + (255 - cr) * hotProgress * 0.7)
|
||||
const hotG = Math.round(cg * (1 - hotProgress * 0.6))
|
||||
@@ -285,31 +289,31 @@ export class VUMeter {
|
||||
private drawVerticalMeterBar(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
x: number, y: number, w: number, h: number,
|
||||
rmsDb: number, peakDb: number,
|
||||
levelDb: number, peakDb: number,
|
||||
cr: number, cg: number, cb: number
|
||||
): void {
|
||||
const rmsNorm = this.dbToNormalized(rmsDb)
|
||||
const levelNorm = this.dbToNormalized(levelDb)
|
||||
const peakNorm = this.dbToNormalized(peakDb)
|
||||
const rmsHeight = rmsNorm * h
|
||||
const levelHeight = levelNorm * h
|
||||
const hotThreshold = this.dbToNormalized(-6) * h
|
||||
|
||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.04)'
|
||||
ctx.fillRect(x, y, w, h)
|
||||
|
||||
if (rmsHeight > 0) {
|
||||
const safeHeight = Math.min(rmsHeight, hotThreshold)
|
||||
if (levelHeight > 0) {
|
||||
const safeHeight = Math.min(levelHeight, hotThreshold)
|
||||
if (safeHeight > 0) {
|
||||
ctx.fillStyle = colorWithAlpha(cr, cg, cb, 0.82)
|
||||
ctx.fillRect(x, y + h - safeHeight, w, safeHeight)
|
||||
}
|
||||
if (rmsHeight > hotThreshold) {
|
||||
const hotHeight = rmsHeight - hotThreshold
|
||||
if (levelHeight > hotThreshold) {
|
||||
const hotHeight = levelHeight - hotThreshold
|
||||
const hotProgress = Math.min(1, hotHeight / Math.max(1, h - hotThreshold))
|
||||
const hotR = Math.round(cr + (255 - cr) * hotProgress * 0.7)
|
||||
const hotG = Math.round(cg * (1 - hotProgress * 0.6))
|
||||
const hotB = Math.round(cb * (1 - hotProgress * 0.7))
|
||||
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)
|
||||
|
||||
// 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
|
||||
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
|
||||
this.drawCorrelationBar(ctx, barLeft, meterAreaHeight + gap, barWidth, corrHeight, cr, cg, cb)
|
||||
|
||||
@@ -4,8 +4,10 @@ export interface VUMeterStereoChunk {
|
||||
}
|
||||
|
||||
export interface VUMeterSnapshot {
|
||||
rmsLDb: number
|
||||
rmsRDb: number
|
||||
vuLDb: number
|
||||
vuRDb: number
|
||||
barLDb: number
|
||||
barRDb: number
|
||||
peakLDb: number
|
||||
peakRDb: number
|
||||
correlation: number
|
||||
@@ -16,10 +18,14 @@ export const VU_METER_MAX_DB = 0
|
||||
export const VU_INTEGRATION_WINDOW_MS = 300
|
||||
export const VU_PEAK_HOLD_MS = 750
|
||||
export const VU_PEAK_DECAY_DB_PER_SECOND = 18
|
||||
const BAR_ATTACK_MS = 5
|
||||
const BAR_RELEASE_MS = 180
|
||||
|
||||
const INITIAL_SNAPSHOT: VUMeterSnapshot = {
|
||||
rmsLDb: VU_METER_MIN_DB,
|
||||
rmsRDb: VU_METER_MIN_DB,
|
||||
vuLDb: VU_METER_MIN_DB,
|
||||
vuRDb: VU_METER_MIN_DB,
|
||||
barLDb: VU_METER_MIN_DB,
|
||||
barRDb: VU_METER_MIN_DB,
|
||||
peakLDb: VU_METER_MIN_DB,
|
||||
peakRDb: VU_METER_MIN_DB,
|
||||
correlation: 0,
|
||||
@@ -48,6 +54,10 @@ export class VUMeterBallistics {
|
||||
private peakHoldUntilL = 0
|
||||
private peakHoldUntilR = 0
|
||||
private lastPeakUpdateMs: number | null = null
|
||||
private barEnvelopeL = 0
|
||||
private barEnvelopeR = 0
|
||||
private barAttackCoeff = 0
|
||||
private barReleaseCoeff = 0
|
||||
private snapshot: VUMeterSnapshot = { ...INITIAL_SNAPSHOT }
|
||||
|
||||
constructor(sampleRate: number) {
|
||||
@@ -67,6 +77,8 @@ export class VUMeterBallistics {
|
||||
this.sqL = new Float64Array(this.integrationWindowSamples)
|
||||
this.sqR = 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()
|
||||
}
|
||||
|
||||
@@ -82,6 +94,8 @@ export class VUMeterBallistics {
|
||||
this.peakHoldUntilL = 0
|
||||
this.peakHoldUntilR = 0
|
||||
this.lastPeakUpdateMs = null
|
||||
this.barEnvelopeL = 0
|
||||
this.barEnvelopeR = 0
|
||||
this.snapshot = { ...INITIAL_SNAPSHOT }
|
||||
}
|
||||
|
||||
@@ -122,6 +136,8 @@ export class VUMeterBallistics {
|
||||
|
||||
const absL = Math.abs(left)
|
||||
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 (absR > maxPeakR) maxPeakR = absR
|
||||
}
|
||||
@@ -139,8 +155,10 @@ export class VUMeterBallistics {
|
||||
|
||||
private recomputeSnapshot(): void {
|
||||
if (this.sampleCount <= 0) {
|
||||
this.snapshot.rmsLDb = VU_METER_MIN_DB
|
||||
this.snapshot.rmsRDb = VU_METER_MIN_DB
|
||||
this.snapshot.vuLDb = 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
|
||||
return
|
||||
}
|
||||
@@ -149,13 +167,20 @@ export class VUMeterBallistics {
|
||||
const meanSqR = this.sumSqR / this.sampleCount
|
||||
const denominator = Math.sqrt(this.sumSqL * this.sumSqR)
|
||||
|
||||
this.snapshot.rmsLDb = amplitudeToDb(Math.sqrt(meanSqL))
|
||||
this.snapshot.rmsRDb = amplitudeToDb(Math.sqrt(meanSqR))
|
||||
this.snapshot.vuLDb = amplitudeToDb(Math.sqrt(meanSqL))
|
||||
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
|
||||
? Math.max(-1, Math.min(1, this.sumCross / denominator))
|
||||
: 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 {
|
||||
if (!Number.isFinite(nowMs)) {
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user