UNPKG

apphouse

Version:

Component library for React that uses observable state management and theme-able components.

320 lines (303 loc) 9.82 kB
const options = { borderSize: 2, fontSize: 9, backgroundColor: "black", tickColor: "#ddd", labelColor: "#ddd", gradient: ["red 1%", "#ff0 16%", "lime 45%", "#080 100%"], dbRange: 48, dbTickSize: 6, maskTransition: "0.1s", }; export default class PeakMeter { tickWidth?: number; elementWidth?: number; elementHeight?: number; meterHeight?: number; meterWidth?: number; meterTop?: number; vertical?: boolean; channelCount: number; channelMasks: any[]; channelPeaks: any[]; channelPeakLabels: any[]; maskSizes: any[]; textLabels: any[]; constructor() { this.vertical = true; this.channelCount = 1; this.channelMasks = []; this.channelPeaks = []; this.channelPeakLabels = []; this.maskSizes = []; this.textLabels = []; } getBaseLog = (x, y) => { return Math.log(y) / Math.log(x); }; dbFromFloat = (floatVal) => { return this.getBaseLog(10, floatVal) * 20; }; setOptions = (userOptions) => { for (let k in userOptions) { if (userOptions.hasOwnProperty(k)) { options[k] = userOptions[k]; } } this.tickWidth = options.fontSize * 2.0; this.meterTop = options.fontSize * 1.5 + options.borderSize; }; createMeterNode = (sourceNode, audioCtx) => { const c = sourceNode.channelCount; const meterNode = audioCtx.createScriptProcessor(2048, c, c); sourceNode.connect(meterNode); meterNode.connect(audioCtx.destination); return meterNode; }; createContainerDiv = (parent) => { const meterElement = document.createElement("div"); meterElement.style.position = "relative"; meterElement.style.width = this.elementWidth + "px"; meterElement.style.height = this.elementHeight + "px"; meterElement.style.backgroundColor = options.backgroundColor; parent.appendChild(meterElement); return meterElement; }; createMeter = (domElement, meterNode, optionsOverrides) => { this.setOptions(optionsOverrides); this.elementWidth = domElement.clientWidth; this.elementHeight = domElement.clientHeight; const meterElement = this.createContainerDiv(domElement); if (this.elementHeight && this.elementWidth) { if (this.elementWidth > this.elementHeight) { this.vertical = false; } this.meterHeight = this.elementHeight - (this.meterTop || 0) - options.borderSize; this.meterWidth = this.elementWidth - (this.tickWidth || 0) - options.borderSize; this.createTicks(meterElement); this.createRainbow( meterElement, this.meterWidth, this.meterHeight, this.meterTop, this.tickWidth, ); this.channelCount = meterNode.channelCount; let channelWidth = this.meterWidth / this.channelCount; if (!this.vertical) { channelWidth = this.meterHeight / this.channelCount; } let channelLeft = this.tickWidth; if (!this.vertical) { channelLeft = this.meterTop; } for (let i = 0; i < this.channelCount; i++) { this.createChannelMask( meterElement, options.borderSize, this.meterTop, channelLeft, false, ); this.channelMasks[i] = this.createChannelMask( meterElement, channelWidth, this.meterTop, channelLeft, options.maskTransition, ); this.channelPeaks[i] = 0.0; this.channelPeakLabels[i] = this.createPeakLabel( meterElement, channelWidth, channelLeft, ); if (channelLeft) { channelLeft += channelWidth; } this.maskSizes[i] = 0; this.textLabels[i] = "-∞"; } meterNode.onaudioprocess = this.updateMeter; meterElement.addEventListener( "click", () => { for (let i = 0; i < this.channelCount; i++) { this.channelPeaks[i] = 0.0; this.textLabels[i] = "-∞"; } }, false, ); this.paintMeter(); } }; createTicks = (parent) => { const numTicks = Math.floor(options.dbRange / options.dbTickSize); let dbTickLabel = 0; if (this.vertical) { let dbTickTop = options.fontSize + options.borderSize; for (let i = 0; i < numTicks; i++) { const dbTick = document.createElement("div"); parent.appendChild(dbTick); dbTick.style.width = this.tickWidth + "px"; dbTick.style.textAlign = "right"; dbTick.style.color = options.tickColor; dbTick.style.fontSize = options.fontSize + "px"; dbTick.style.position = "absolute"; dbTick.style.top = dbTickTop + "px"; dbTick.textContent = dbTickLabel + ""; dbTickLabel -= options.dbTickSize; dbTickTop += (this.meterHeight || 0) / numTicks; } } else { this.tickWidth = (this.meterWidth || 0) / numTicks; let dbTickRight = options.fontSize * 2; for (let i = 0; i < numTicks; i++) { let dbTick = document.createElement("div"); parent.appendChild(dbTick); dbTick.style.width = this.tickWidth + "px"; dbTick.style.textAlign = "right"; dbTick.style.color = options.tickColor; dbTick.style.fontSize = options.fontSize + "px"; dbTick.style.position = "absolute"; dbTick.style.right = dbTickRight + "px"; dbTick.textContent = dbTickLabel + ""; dbTickLabel -= options.dbTickSize; dbTickRight += this.tickWidth; } } }; createRainbow = (parent, width, height, top, left) => { let rainbow = document.createElement("div"); parent.appendChild(rainbow); rainbow.style.width = width + "px"; rainbow.style.height = height + "px"; rainbow.style.position = "absolute"; rainbow.style.top = top + "px"; if (this.vertical) { rainbow.style.left = left + "px"; var gradientStyle = "linear-gradient(to bottom, " + options.gradient.join(", ") + ")"; } else { rainbow.style.left = options.borderSize + "px"; var gradientStyle = "linear-gradient(to left, " + options.gradient.join(", ") + ")"; } rainbow.style.backgroundImage = gradientStyle; return rainbow; }; createPeakLabel = (parent, width, left) => { const label = document.createElement("div"); parent.appendChild(label); label.style.textAlign = "center"; label.style.color = options.labelColor; label.style.fontSize = options.fontSize + "px"; label.style.position = "absolute"; label.textContent = "-∞"; if (this.vertical) { label.style.width = width + "px"; label.style.top = options.borderSize + "px"; label.style.left = left + "px"; } else { label.style.width = options.fontSize * 2 + "px"; label.style.right = options.borderSize + "px"; label.style.top = width * 0.25 + left + "px"; } return label; }; createChannelMask = (parent, width, top, left, transition) => { const channelMask = document.createElement("div"); parent.appendChild(channelMask); channelMask.style.position = "absolute"; if (this.vertical) { channelMask.style.width = width + "px"; channelMask.style.height = this.meterHeight + "px"; channelMask.style.top = top + "px"; channelMask.style.left = left + "px"; } else { channelMask.style.width = this.meterWidth + "px"; channelMask.style.height = width + "px"; channelMask.style.top = left + "px"; channelMask.style.right = options.fontSize * 2 + "px"; } channelMask.style.backgroundColor = options.backgroundColor; if (transition) { if (this.vertical) { channelMask.style.transition = "height " + options.maskTransition; } else { channelMask.style.transition = "width " + options.maskTransition; } } return channelMask; }; maskSize = (floatVal) => { const meterDimension = this.vertical ? this.meterHeight : this.meterWidth; if (floatVal === 0.0) { return meterDimension; } else { if (meterDimension) { const d = options.dbRange * -1; const returnVal = Math.floor( (this.dbFromFloat(floatVal) * meterDimension) / d, ); if (returnVal > meterDimension) { return meterDimension; } else { return returnVal; } } } }; updateMeter = (audioProcessingEvent) => { const inputBuffer = audioProcessingEvent.inputBuffer; let i; const channelData: any = []; const channelMaxes: any = []; for (i = 0; i < this.channelCount; i++) { channelData[i] = inputBuffer.getChannelData(i); channelMaxes[i] = 0.0; } for (let sample = 0; sample < inputBuffer.length; sample++) { for (i = 0; i < this.channelCount; i++) { if (Math.abs(channelData[i][sample]) > channelMaxes[i]) { channelMaxes[i] = Math.abs(channelData[i][sample]); } } } for (i = 0; i < this.channelCount; i++) { this.maskSizes[i] = this.maskSize(channelMaxes[i]); if (channelMaxes[i] > this.channelPeaks[i]) { this.channelPeaks[i] = channelMaxes[i]; this.textLabels[i] = this.dbFromFloat( this.channelPeaks[i], ).toFixed(1); } } }; paintMeter = () => { for (let i = 0; i < this.channelCount; i++) { if (this.vertical) { this.channelMasks[i].style.height = this.maskSizes[i] + "px"; } else { this.channelMasks[i].style.width = this.maskSizes[i] + "px"; } this.channelPeakLabels[i].textContent = this.textLabels[i]; } window.requestAnimationFrame(this.paintMeter); }; }