import { PeakMeterConfig, defaultConfig } from './config'; import { audioClipPath, createContainerDiv, createTicks, createChannelElements, createBars, createPeakLabels, } from './markup'; import { dbFromFloat } from './utils'; import peakSampleProcessor from './peak-sample-processor.txt'; import truePeakProcessor from './true-peak-processor.txt'; export class WebAudioPeakMeter { channelCount: number; srcNode: AudioNode; node?: AudioWorkletNode; config: PeakMeterConfig; parent?: HTMLElement; ticks?: Array; channelElements?: Array; bars?: Array; peakLabels?: Array; tempPeaks: Array; heldPeaks: Array; peakHoldTimeouts: Array; animationRequestId?: number; constructor(src: AudioNode, ele: HTMLElement, options = {}) { this.srcNode = src; this.config = Object.assign({ ...defaultConfig }, options); this.channelCount = src.channelCount; this.tempPeaks = new Array(this.channelCount).fill(0.0); this.heldPeaks = new Array(this.channelCount).fill(0.0); this.peakHoldTimeouts = new Array(this.channelCount).fill(0); if (ele) { this.parent = createContainerDiv(ele, this.config); this.channelElements = createChannelElements(this.parent, this.config, this.channelCount); this.peakLabels = createPeakLabels(this.channelElements, this.config); this.bars = createBars(this.channelElements, this.config); this.ticks = createTicks(this.parent, this.config); this.parent.addEventListener('click', this.clearPeaks.bind(this)); this.paintMeter(); } this.initNode(); } async initNode() { const { audioMeterStandard } = this.config; try { this.node = new AudioWorkletNode(this.srcNode.context, `${audioMeterStandard}-processor`, { parameterData: {}, }); } catch (err) { const workletString = audioMeterStandard === 'true-peak' ? truePeakProcessor : peakSampleProcessor; const blob = new Blob([workletString], { type: 'application/javascript' }); const objectURL = URL.createObjectURL(blob); await this.srcNode.context.audioWorklet.addModule(objectURL); this.node = new AudioWorkletNode(this.srcNode.context, `${audioMeterStandard}-processor`, { parameterData: {}, }); } this.node.port.onmessage = (ev: MessageEvent) => this.handleNodePortMessage(ev); this.srcNode.connect(this.node).connect(this.srcNode.context.destination); } handleNodePortMessage(ev: MessageEvent) { if (ev.data.type === 'message') { console.log(ev.data.message); } if (ev.data.type === 'peaks') { const { peaks } = ev.data; for (let i = 0; i < this.tempPeaks.length; i += 1) { if (peaks.length > i) { this.tempPeaks[i] = peaks[i]; } else { this.tempPeaks[i] = 0.0; } } if (peaks.length < this.channelCount) { this.tempPeaks.fill(0.0, peaks.length); } for (let i = 0; i < peaks.length; i += 1) { if (peaks[i] > this.heldPeaks[i]) { this.heldPeaks[i] = peaks[i]; if (this.peakHoldTimeouts[i]) { clearTimeout(this.peakHoldTimeouts[i]); } if (this.config.peakHoldDuration) { this.peakHoldTimeouts[i] = window.setTimeout(() => { this.clearPeak(i); }, this.config.peakHoldDuration); } } } } } paintMeter() { const { dbRangeMin, dbRangeMax, vertical } = this.config; if (this.bars) { this.bars.forEach((barDiv, i) => { const tempPeak = dbFromFloat(this.tempPeaks[i]); const clipPath = audioClipPath(tempPeak, dbRangeMin, dbRangeMax, vertical); barDiv.style.clipPath = clipPath; }); } if (this.peakLabels) { this.peakLabels.forEach((textLabel, i) => { if (this.heldPeaks[i] === 0.0) { textLabel.textContent = '-∞'; } else { const heldPeak = dbFromFloat(this.heldPeaks[i]); textLabel.textContent = heldPeak.toFixed(1); } }); } this.animationRequestId = window.requestAnimationFrame(this.paintMeter.bind(this)); } clearPeak(i: number) { this.heldPeaks[i] = this.tempPeaks[i]; } clearPeaks() { for (let i = 0; i < this.heldPeaks.length; i += 1) { this.clearPeak(i); } } getPeaks() { return { current: this.tempPeaks, maxes: this.heldPeaks, currentDB: this.tempPeaks.map(dbFromFloat), maxesDB: this.heldPeaks.map(dbFromFloat), }; } cleanup() { if (this.node) { this.node.disconnect(); } if (this.parent) { this.parent.removeEventListener('click', this.clearPeaks.bind(this)); if (this.animationRequestId !== undefined) { window.cancelAnimationFrame(this.animationRequestId); } this.parent.remove(); } } }