apphouse
Version:
Component library for React that uses observable state management and theme-able components.
281 lines (247 loc) • 7.79 kB
text/typescript
import { action, computed, makeObservable, observable } from "mobx";
import { audioLog } from "./AudioLog";
const BRIGHT_RED = "#ff0000";
const LIME_GREEN = "#45fc1e";
const DEFAULT_WAVE_COLOR = BRIGHT_RED;
const FFTSIZE = 2048;
export default class Visualizer {
barWidth: number;
canvas?: HTMLCanvasElement;
height: number;
on: boolean;
width: number;
constructor(canvas?: HTMLCanvasElement) {
this.canvas = canvas;
this.width = canvas !== null ? (canvas ? canvas.width : 0) : 0;
this.height = canvas !== null ? (canvas ? canvas.height : 0) : 0;
this.barWidth = 0;
this.on = false;
makeObservable(this, {
barWidth: observable,
currentBarWidth: computed,
height: observable,
on: observable,
turnOff: action,
visualizeFreqBar: action,
visualizeOsciloscope: action,
visualizeAudioMeter: action,
width: observable,
turnOn: action,
});
}
get currentBarWidth() {
return this.barWidth;
}
get visualizer() {
return this.canvas;
}
setCanvas = (canvas: HTMLCanvasElement) => {
this.canvas = canvas;
this.width = canvas.width;
this.height = canvas.height;
};
visualizeFreqBar = (
analyzerNode: AnalyserNode,
fftsize?: number,
color?: string,
) => {
this.turnOn();
const barColor = color || LIME_GREEN;
analyzerNode.fftSize = fftsize || FFTSIZE;
const draw = () => {
if (this.on === true && this.canvas) {
requestAnimationFrame(draw);
const bufferLength = analyzerNode.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);
analyzerNode.getByteFrequencyData(dataArray);
this.barWidth = this.width / bufferLength;
// console.log("canvas", this.barWidth);
const canvasContext = this.canvas.getContext("2d");
if (canvasContext) {
canvasContext.clearRect(0, 0, this.width, this.height);
dataArray.forEach((item, index) => {
const y = ((item / 255) * this.height) / 2;
const x = this.barWidth * index;
canvasContext.fillStyle = barColor;
canvasContext.fillRect(
x,
this.height - y,
this.barWidth,
y,
);
});
} else {
console.warn(
"No canvas context available to draw visualizer",
);
}
} else {
console.warn(
"No canvas element available to draw visualizer",
);
}
};
draw();
};
visualizeAudioMeter = (
analyzerNode: AnalyserNode,
color?: string,
) => {
this.turnOn();
analyzerNode.fftSize = FFTSIZE;
let meterColor = color || LIME_GREEN;
const maxStep = this.width;
if (this.canvas) {
const ctx = this.canvas.getContext("2d");
if (ctx !== null) {
const draw = () => {
if (this.on === true) {
requestAnimationFrame(draw);
ctx.clearRect(0, 0, this.width, this.height);
const bufferLength = analyzerNode.frequencyBinCount;
const data = new Uint8Array(bufferLength);
analyzerNode.getByteFrequencyData(data);
const step = Math.ceil(data.length / this.width);
const amp = this.height / 2;
for (let i = 0; i < this.width; i++) {
let neg = 0;
let pos = 0;
let max = Math.min(step, maxStep);
for (let j = 0; j < max; j++) {
const val = data[i * step + j];
if (val < 0) {
neg += val;
} else {
pos += val;
}
}
neg = neg / max;
pos = pos / max;
const displacementX = i;
const height = amp * (pos - neg);
const amplitude = amp - pos * amp;
const barWidth = 1;
if (displacementX < 150) {
ctx.fillStyle = "green";
} else {
ctx.fillStyle = meterColor;
}
ctx.fillRect(
displacementX,
amplitude,
barWidth,
height,
);
}
} else {
console.warn(
"No canvas context available to draw visualizer",
);
}
};
draw();
}
}
};
visualizeWinamp = (analyser: AnalyserNode) => {
this.turnOn();
analyser.minDecibels = -90;
analyser.maxDecibels = -10;
analyser.fftSize = 256;
var bufferLength = analyser.frequencyBinCount;
var dataArray = new Uint8Array(bufferLength);
if (this.canvas) {
const ctx = this.canvas.getContext("2d");
if (ctx) {
ctx.clearRect(0, 0, this.width, this.height);
const draw = () => {
requestAnimationFrame(draw);
if (this.on) {
analyser.getByteFrequencyData(dataArray);
ctx.fillStyle = "rgb(0, 0, 0)";
ctx.fillRect(0, 0, this.width, this.height);
var barWidth = (this.width / bufferLength) * 2.5 - 1;
var barHeight;
var x = 0;
for (var i = 0; i < bufferLength; i++) {
barHeight = dataArray[i];
ctx.fillStyle = "rgb(" + (barHeight + 100) + ",50,50)";
ctx.fillRect(
x,
this.height - barHeight / 2,
barWidth,
barHeight / 2,
);
x += barWidth;
}
}
};
draw();
}
}
};
visualizeOsciloscope = (
analyzerNode: AnalyserNode,
color?: string,
) => {
this.turnOn();
let waveColor: string = color || DEFAULT_WAVE_COLOR;
analyzerNode.fftSize = FFTSIZE;
var bufferLength = analyzerNode.frequencyBinCount;
var dataArray = new Uint8Array(bufferLength);
analyzerNode.getByteTimeDomainData(dataArray);
// We now have the audio data for that moment in time captured in our array,
// and can proceed to visualize it however we like,
if (this.canvas) {
const ctx = this.canvas.getContext("2d");
if (ctx !== null) {
ctx.clearRect(0, 0, this.width, this.height);
const draw = () => {
if (this.on === true) {
requestAnimationFrame(draw);
analyzerNode.getByteTimeDomainData(dataArray);
ctx.fillStyle = "rgb(1, 1, 1)";
ctx.fillRect(0, 0, this.width, this.height);
ctx.lineWidth = 2;
ctx.strokeStyle = `rgb(${hexToRGB(waveColor)})`;
ctx.beginPath();
let sliceWidth = (this.width * 1.0) / bufferLength;
let x = 0;
for (let i = 0; i < bufferLength; i++) {
let v = dataArray[i] / 128.0;
let y = (v * this.height) / 2;
if (i === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
x += sliceWidth;
}
ctx.lineTo(this.width, this.height / 2);
ctx.stroke();
}
};
draw();
}
}
};
turnOn = () => {
this.on = true;
audioLog.logVizualizer("On");
};
turnOff = () => {
this.on = false;
audioLog.logVizualizer("Off");
};
}
export const hexToRGB = (hex: string): string => {
if (typeof hex !== "string") {
throw new TypeError("Expected a string");
}
let hexColor = hex.replace(/^#/, "");
if (hexColor.length === 3) {
hexColor = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
}
const num = parseInt(hexColor, 16);
return `${num >> 16}, ${(num >> 8) & 255}, ${num & 255}`;
};