apphouse
Version:
Component library for React that uses observable state management and theme-able components.
320 lines (303 loc) • 9.82 kB
text/typescript
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);
};
}