音频 API 以频率为中心可视化
Audio API center visualisation on frequency
我正在开发一种用于网络的音频可视化工具,它还允许用户将原始音频信号可视化工具“调谐”到一个频率。这是许多硬件示波器的一个特性。基本上,当用户以 440Hz 为中心并且我有一个 440Hz 正弦波时,波应该保持在 canvas 上并且不会向左或向右移动。我的计划是根据频率将图表向左移动(440Hz = 每秒向左移动 1/440s,因为波应该每 1/440s 重复一次),但这似乎不起作用。
我找不到音频分析器节点的时域数据使用的单位。我猜是毫秒,但我不确定。
"use strict";
// Oscillator instead of mic for debugging
const USE_OSCILLATOR = true;
// Compatibility
if (!window.AudioContext)
window.AudioContext = window.webkitAudioContext;
if (!navigator.getUserMedia)
navigator.getUserMedia =
navigator.mozGetUserMedia ||
navigator.webkitGetUserMedia ||
navigator.msGetUserMedia;
// Main
class App {
constructor(visualizerElement, optionsElement) {
this.visualizerElement = visualizerElement;
this.optionsElement = optionsElement;
// HTML elements
this.canvas = document.createElement("canvas");
// Context
this.context = new AudioContext({
// Low latency
latencyHint: "interactive",
});
this.canvasCtx = this.canvas.getContext("2d", {
// Low latency
desynchronized: true,
alpha: false,
});
// Audio nodes
this.audioAnalyser = this.context.createAnalyser();
this.audioBuffer = new Uint8Array(this.audioAnalyser.frequencyBinCount);
this.audioInputStream = null;
this.audioInputNode = null;
if (this.canvasCtx === null)
throw new Error("2D rendering Context not supported by browser.");
this.updateCanvasSize();
window.addEventListener("resize", () => this.updateCanvasSize());
this.drawVisualizer();
this.visualizerElement.appendChild(this.canvas);
if (USE_OSCILLATOR) {
let oscillator = this.context.createOscillator();
oscillator.type = "sine";
oscillator.frequency.setValueAtTime(440, this.context.currentTime);
oscillator.connect(this.audioAnalyser);
oscillator.start();
}
else {
navigator.getUserMedia({ audio: true }, (stream) => {
this.audioInputStream = stream;
this.audioInputNode = this.context.createMediaStreamSource(stream);
this.audioInputNode.channelCountMode = "explicit";
this.audioInputNode.channelCount = 1;
this.audioBuffer = new Uint8Array(this.audioAnalyser.frequencyBinCount);
this.audioInputNode.connect(this.audioAnalyser);
}, (err) => console.error(err));
}
}
updateCanvasSize() {
var _a;
this.canvas.width = window.innerWidth;
this.canvas.height = window.innerHeight;
(_a = this.canvasCtx) === null || _a === void 0 ? void 0 : _a.setTransform(1, 0, 0, -1, 0, this.canvas.height * 0.5);
}
drawVisualizer() {
if (this.canvasCtx === null)
return;
const ctx = this.canvasCtx;
ctx.fillStyle = "black";
ctx.fillRect(0, -0.5 * this.canvas.height, this.canvas.width, this.canvas.height);
// Draw FFT
this.audioAnalyser.getByteFrequencyData(this.audioBuffer);
const step = this.canvas.width / this.audioBuffer.length;
const scale = this.canvas.height / (2 * 255);
ctx.beginPath();
ctx.moveTo(-step, this.audioBuffer[0] * scale);
this.audioBuffer.forEach((sample, index) => {
ctx.lineTo(index * step, scale * sample);
});
ctx.strokeStyle = "white";
ctx.stroke();
// Get the highest dominant frequency
let highestFreqHalfHz = 0;
{
/**
* Highest frequency in 0.5Hz
*/
let highestFreq = NaN;
let highestFreqAmp = NaN;
let remSteps = NaN;
for (let i = this.audioBuffer.length - 1; i >= 0; i--) {
const sample = this.audioBuffer[i];
if (sample > 20 && (isNaN(highestFreqAmp) || sample > highestFreqAmp)) {
highestFreq = i;
highestFreqAmp = sample;
if (isNaN(remSteps))
remSteps = 500;
}
if (!isNaN(remSteps)) {
if (remSteps-- < 0)
break;
}
}
if (!isNaN(highestFreq)) {
ctx.beginPath();
ctx.moveTo(highestFreq * step, 0);
ctx.lineTo(highestFreq * step, scale * 255);
ctx.strokeStyle = "green";
ctx.stroke();
highestFreqHalfHz = highestFreq;
}
}
// Draw Audio
this.audioAnalyser.getByteTimeDomainData(this.audioBuffer);
{
const bufferSize = this.audioBuffer.length;
const offsetY = -this.canvas.height * 0.5;
// I don't know what I am doing here:
const offsetX = highestFreqHalfHz == 0
? 0
: bufferSize -
Math.round(((this.context.currentTime * 1000) % (1 / 440)) % bufferSize);
// Draw the audio graph with the given offset
ctx.beginPath();
ctx.moveTo(-step, this.audioBuffer[0] * scale + offsetY);
for (let i = 0; i < bufferSize; i++) {
const index = (offsetX + i) % bufferSize;
const sample = this.audioBuffer[index];
ctx.lineTo(i * step, scale * sample + offsetY);
}
ctx.strokeStyle = "white";
ctx.stroke();
}
}
}
window.addEventListener("load", () => {
const app = new App(document.getElementById("visualizer"), document.getElementById("options"));
requestAnimationFrame(draw);
function draw() {
requestAnimationFrame(draw);
app.drawVisualizer();
}
});
html {
background: black;
}
body {
width: 100vw;
height: 100vh;
margin: 0;
overflow: hidden;
}
#visualizer {
position: fixed;
inset: 0;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Equalizer</title>
</head>
<body>
<div id="visualizer"></div>
<div id="options"></div>
</body>
</html>
以上代码段是从 TypeScript 生成的。你可以找到来源here。如果它按预期工作,振荡图(底部)将不会移动。
多亏了和我的数学老师(谢谢克莱恩先生),我才能够解决这个问题。解决方案是 Math.round((this.context.currentTime % iv) * sampleRate)
,其中 iv
是频率的间隔 (1/Hz
)。波浪不是完全居中的。但是 FFT 近似不是很准确。在下面的示例中,我将检测到的频率强制为指定频率。
"use strict";
// Oscillator instead of mic for debugging
const USE_OSCILLATOR = true;
const OSCILLATOR_HZ = 1000;
// Compatibility
if (!window.AudioContext)
window.AudioContext = window.webkitAudioContext;
if (!navigator.getUserMedia)
navigator.getUserMedia =
navigator.mozGetUserMedia ||
navigator.webkitGetUserMedia ||
navigator.msGetUserMedia;
// Main
class App {
constructor(visualizerElement, optionsElement) {
this.visualizerElement = visualizerElement;
this.optionsElement = optionsElement;
// HTML elements
this.canvas = document.createElement("canvas");
// Context
this.context = new AudioContext({
// Low latency
latencyHint: "interactive",
});
this.canvasCtx = this.canvas.getContext("2d", {
// Low latency
desynchronized: true,
alpha: false,
});
// Audio nodes
this.audioAnalyser = this.context.createAnalyser();
this.audioBuffer = new Uint8Array(0);
this.audioInputStream = null;
this.audioInputNode = null;
if (this.canvasCtx === null)
throw new Error("2D rendering Context not supported by browser.");
this.updateCanvasSize();
window.addEventListener("resize", () => this.updateCanvasSize());
this.drawVisualizer();
this.visualizerElement.appendChild(this.canvas);
this.audioAnalyser.fftSize = 2048;
this.audioAnalyser.maxDecibels = -10;
this.audioBuffer = new Uint8Array(this.audioAnalyser.frequencyBinCount * 2);
this.audioFilter = this.context.createBiquadFilter();
this.audioFilter.type = "bandpass";
this.audioFilter.frequency.value = 900;
this.audioFilter.Q.value = 20;
this.audioAmplifier = this.context.createGain();
this.audioAmplifier.gain.value = 5;
this.audioFilter.connect(this.audioAmplifier);
this.audioAmplifier.connect(this.audioAnalyser);
if (USE_OSCILLATOR) {
let oscillator = this.context.createOscillator();
oscillator.type = "sine";
oscillator.frequency.setValueAtTime(OSCILLATOR_HZ, this.context.currentTime);
oscillator.connect(this.audioFilter);
oscillator.start();
}
else {
navigator.getUserMedia({ audio: true }, (stream) => {
this.audioInputStream = stream;
this.audioInputNode = this.context.createMediaStreamSource(stream);
this.audioInputNode.channelCountMode = "explicit";
this.audioInputNode.channelCount = 1;
this.audioBuffer = new Uint8Array(this.audioAnalyser.frequencyBinCount);
this.audioInputNode.connect(this.audioFilter);
}, (err) => console.error(err));
}
}
updateCanvasSize() {
var _a;
this.canvas.width = window.innerWidth;
this.canvas.height = window.innerHeight;
(_a = this.canvasCtx) === null || _a === void 0 ? void 0 : _a.setTransform(1, 0, 0, -1, 0, this.canvas.height * 0.5);
}
drawVisualizer() {
if (this.canvasCtx === null)
return;
const ctx = this.canvasCtx;
ctx.globalAlpha = 0.5;
ctx.fillStyle = "black";
ctx.fillRect(0, -0.5 * this.canvas.height, this.canvas.width, this.canvas.height);
ctx.globalAlpha = 1;
// Draw FFT
this.audioAnalyser.getByteFrequencyData(this.audioBuffer);
const scale = this.canvas.height / (2 * 255);
const { frequencyBinCount } = this.audioAnalyser;
const { sampleRate } = this.context;
{
const step = this.canvas.width / frequencyBinCount;
ctx.beginPath();
ctx.moveTo(-step, this.audioBuffer[0] * scale);
for (let index = 0; index < frequencyBinCount; index++) {
ctx.lineTo(index * step, scale * this.audioBuffer[index]);
}
ctx.strokeStyle = "white";
ctx.stroke();
}
// Get the highest dominant frequency
const step = this.canvas.width / frequencyBinCount;
let highestFreqHz = 0;
{
/**
* Highest frequency index in the buffer
*/
let highestFreqIndex = NaN;
let highestFreqAmp = NaN;
let remSteps = NaN;
for (let i = frequencyBinCount - 1; i >= 0; i--) {
const sample = this.audioBuffer[i];
if (sample > 30) {
if (isNaN(highestFreqAmp)) {
highestFreqIndex = i;
highestFreqAmp = sample;
}
else {
if (sample > highestFreqAmp) {
highestFreqIndex = i;
highestFreqAmp = sample;
}
}
//if (isNaN(remSteps)) remSteps = 100;
}
if (!isNaN(remSteps)) {
if (remSteps-- < 0)
break;
}
}
if (!isNaN(highestFreqIndex)) {
// Force exact value: (not necessary)
highestFreqIndex =
(OSCILLATOR_HZ * (2 * frequencyBinCount)) / sampleRate;
ctx.beginPath();
ctx.moveTo(highestFreqIndex * step, 0);
ctx.lineTo(highestFreqIndex * step, scale * 255);
ctx.strokeStyle = "green";
ctx.stroke();
highestFreqHz =
(highestFreqIndex * sampleRate) / (2 * frequencyBinCount);
window.HZ = highestFreqHz;
}
}
// Draw Audio
this.audioAnalyser.getByteTimeDomainData(this.audioBuffer);
{
const iv = highestFreqHz == 0 ? 0 : 1 / highestFreqHz;
const bufferSize = this.audioBuffer.length;
const offsetY = -this.canvas.height / 2.4;
const startIndex = Math.round(iv * sampleRate);
const step = this.canvas.width / (this.audioBuffer.length - startIndex);
const scale = this.canvas.height / (3 * 255);
const offsetX = highestFreqHz == 0
? 0
: Math.round((this.context.currentTime % iv) * sampleRate) %
bufferSize;
// Draw the audio graph with the given offset
ctx.beginPath();
ctx.moveTo(-step, this.audioBuffer[startIndex - offsetX] * scale + offsetY);
for (let i = startIndex; i < bufferSize; i += 4) {
const index = (i - offsetX) % bufferSize;
const sample = this.audioBuffer[index];
ctx.lineTo((i - startIndex) * step, scale * sample + offsetY);
}
ctx.strokeStyle = "white";
ctx.stroke();
}
}
}
window.addEventListener("load", () => {
const app = new App(document.getElementById("visualizer"), document.getElementById("options"));
requestAnimationFrame(draw);
function draw() {
requestAnimationFrame(draw);
app.drawVisualizer();
}
});
html {
background: black;
}
body {
width: 100vw;
height: 100vh;
margin: 0;
overflow: hidden;
}
#visualizer {
position: fixed;
inset: 0;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Equalizer</title>
</head>
<body>
<div id="visualizer"></div>
<div id="options"></div>
</body>
</html>
我正在开发一种用于网络的音频可视化工具,它还允许用户将原始音频信号可视化工具“调谐”到一个频率。这是许多硬件示波器的一个特性。基本上,当用户以 440Hz 为中心并且我有一个 440Hz 正弦波时,波应该保持在 canvas 上并且不会向左或向右移动。我的计划是根据频率将图表向左移动(440Hz = 每秒向左移动 1/440s,因为波应该每 1/440s 重复一次),但这似乎不起作用。
我找不到音频分析器节点的时域数据使用的单位。我猜是毫秒,但我不确定。
"use strict";
// Oscillator instead of mic for debugging
const USE_OSCILLATOR = true;
// Compatibility
if (!window.AudioContext)
window.AudioContext = window.webkitAudioContext;
if (!navigator.getUserMedia)
navigator.getUserMedia =
navigator.mozGetUserMedia ||
navigator.webkitGetUserMedia ||
navigator.msGetUserMedia;
// Main
class App {
constructor(visualizerElement, optionsElement) {
this.visualizerElement = visualizerElement;
this.optionsElement = optionsElement;
// HTML elements
this.canvas = document.createElement("canvas");
// Context
this.context = new AudioContext({
// Low latency
latencyHint: "interactive",
});
this.canvasCtx = this.canvas.getContext("2d", {
// Low latency
desynchronized: true,
alpha: false,
});
// Audio nodes
this.audioAnalyser = this.context.createAnalyser();
this.audioBuffer = new Uint8Array(this.audioAnalyser.frequencyBinCount);
this.audioInputStream = null;
this.audioInputNode = null;
if (this.canvasCtx === null)
throw new Error("2D rendering Context not supported by browser.");
this.updateCanvasSize();
window.addEventListener("resize", () => this.updateCanvasSize());
this.drawVisualizer();
this.visualizerElement.appendChild(this.canvas);
if (USE_OSCILLATOR) {
let oscillator = this.context.createOscillator();
oscillator.type = "sine";
oscillator.frequency.setValueAtTime(440, this.context.currentTime);
oscillator.connect(this.audioAnalyser);
oscillator.start();
}
else {
navigator.getUserMedia({ audio: true }, (stream) => {
this.audioInputStream = stream;
this.audioInputNode = this.context.createMediaStreamSource(stream);
this.audioInputNode.channelCountMode = "explicit";
this.audioInputNode.channelCount = 1;
this.audioBuffer = new Uint8Array(this.audioAnalyser.frequencyBinCount);
this.audioInputNode.connect(this.audioAnalyser);
}, (err) => console.error(err));
}
}
updateCanvasSize() {
var _a;
this.canvas.width = window.innerWidth;
this.canvas.height = window.innerHeight;
(_a = this.canvasCtx) === null || _a === void 0 ? void 0 : _a.setTransform(1, 0, 0, -1, 0, this.canvas.height * 0.5);
}
drawVisualizer() {
if (this.canvasCtx === null)
return;
const ctx = this.canvasCtx;
ctx.fillStyle = "black";
ctx.fillRect(0, -0.5 * this.canvas.height, this.canvas.width, this.canvas.height);
// Draw FFT
this.audioAnalyser.getByteFrequencyData(this.audioBuffer);
const step = this.canvas.width / this.audioBuffer.length;
const scale = this.canvas.height / (2 * 255);
ctx.beginPath();
ctx.moveTo(-step, this.audioBuffer[0] * scale);
this.audioBuffer.forEach((sample, index) => {
ctx.lineTo(index * step, scale * sample);
});
ctx.strokeStyle = "white";
ctx.stroke();
// Get the highest dominant frequency
let highestFreqHalfHz = 0;
{
/**
* Highest frequency in 0.5Hz
*/
let highestFreq = NaN;
let highestFreqAmp = NaN;
let remSteps = NaN;
for (let i = this.audioBuffer.length - 1; i >= 0; i--) {
const sample = this.audioBuffer[i];
if (sample > 20 && (isNaN(highestFreqAmp) || sample > highestFreqAmp)) {
highestFreq = i;
highestFreqAmp = sample;
if (isNaN(remSteps))
remSteps = 500;
}
if (!isNaN(remSteps)) {
if (remSteps-- < 0)
break;
}
}
if (!isNaN(highestFreq)) {
ctx.beginPath();
ctx.moveTo(highestFreq * step, 0);
ctx.lineTo(highestFreq * step, scale * 255);
ctx.strokeStyle = "green";
ctx.stroke();
highestFreqHalfHz = highestFreq;
}
}
// Draw Audio
this.audioAnalyser.getByteTimeDomainData(this.audioBuffer);
{
const bufferSize = this.audioBuffer.length;
const offsetY = -this.canvas.height * 0.5;
// I don't know what I am doing here:
const offsetX = highestFreqHalfHz == 0
? 0
: bufferSize -
Math.round(((this.context.currentTime * 1000) % (1 / 440)) % bufferSize);
// Draw the audio graph with the given offset
ctx.beginPath();
ctx.moveTo(-step, this.audioBuffer[0] * scale + offsetY);
for (let i = 0; i < bufferSize; i++) {
const index = (offsetX + i) % bufferSize;
const sample = this.audioBuffer[index];
ctx.lineTo(i * step, scale * sample + offsetY);
}
ctx.strokeStyle = "white";
ctx.stroke();
}
}
}
window.addEventListener("load", () => {
const app = new App(document.getElementById("visualizer"), document.getElementById("options"));
requestAnimationFrame(draw);
function draw() {
requestAnimationFrame(draw);
app.drawVisualizer();
}
});
html {
background: black;
}
body {
width: 100vw;
height: 100vh;
margin: 0;
overflow: hidden;
}
#visualizer {
position: fixed;
inset: 0;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Equalizer</title>
</head>
<body>
<div id="visualizer"></div>
<div id="options"></div>
</body>
</html>
以上代码段是从 TypeScript 生成的。你可以找到来源here。如果它按预期工作,振荡图(底部)将不会移动。
多亏了Math.round((this.context.currentTime % iv) * sampleRate)
,其中 iv
是频率的间隔 (1/Hz
)。波浪不是完全居中的。但是 FFT 近似不是很准确。在下面的示例中,我将检测到的频率强制为指定频率。
"use strict";
// Oscillator instead of mic for debugging
const USE_OSCILLATOR = true;
const OSCILLATOR_HZ = 1000;
// Compatibility
if (!window.AudioContext)
window.AudioContext = window.webkitAudioContext;
if (!navigator.getUserMedia)
navigator.getUserMedia =
navigator.mozGetUserMedia ||
navigator.webkitGetUserMedia ||
navigator.msGetUserMedia;
// Main
class App {
constructor(visualizerElement, optionsElement) {
this.visualizerElement = visualizerElement;
this.optionsElement = optionsElement;
// HTML elements
this.canvas = document.createElement("canvas");
// Context
this.context = new AudioContext({
// Low latency
latencyHint: "interactive",
});
this.canvasCtx = this.canvas.getContext("2d", {
// Low latency
desynchronized: true,
alpha: false,
});
// Audio nodes
this.audioAnalyser = this.context.createAnalyser();
this.audioBuffer = new Uint8Array(0);
this.audioInputStream = null;
this.audioInputNode = null;
if (this.canvasCtx === null)
throw new Error("2D rendering Context not supported by browser.");
this.updateCanvasSize();
window.addEventListener("resize", () => this.updateCanvasSize());
this.drawVisualizer();
this.visualizerElement.appendChild(this.canvas);
this.audioAnalyser.fftSize = 2048;
this.audioAnalyser.maxDecibels = -10;
this.audioBuffer = new Uint8Array(this.audioAnalyser.frequencyBinCount * 2);
this.audioFilter = this.context.createBiquadFilter();
this.audioFilter.type = "bandpass";
this.audioFilter.frequency.value = 900;
this.audioFilter.Q.value = 20;
this.audioAmplifier = this.context.createGain();
this.audioAmplifier.gain.value = 5;
this.audioFilter.connect(this.audioAmplifier);
this.audioAmplifier.connect(this.audioAnalyser);
if (USE_OSCILLATOR) {
let oscillator = this.context.createOscillator();
oscillator.type = "sine";
oscillator.frequency.setValueAtTime(OSCILLATOR_HZ, this.context.currentTime);
oscillator.connect(this.audioFilter);
oscillator.start();
}
else {
navigator.getUserMedia({ audio: true }, (stream) => {
this.audioInputStream = stream;
this.audioInputNode = this.context.createMediaStreamSource(stream);
this.audioInputNode.channelCountMode = "explicit";
this.audioInputNode.channelCount = 1;
this.audioBuffer = new Uint8Array(this.audioAnalyser.frequencyBinCount);
this.audioInputNode.connect(this.audioFilter);
}, (err) => console.error(err));
}
}
updateCanvasSize() {
var _a;
this.canvas.width = window.innerWidth;
this.canvas.height = window.innerHeight;
(_a = this.canvasCtx) === null || _a === void 0 ? void 0 : _a.setTransform(1, 0, 0, -1, 0, this.canvas.height * 0.5);
}
drawVisualizer() {
if (this.canvasCtx === null)
return;
const ctx = this.canvasCtx;
ctx.globalAlpha = 0.5;
ctx.fillStyle = "black";
ctx.fillRect(0, -0.5 * this.canvas.height, this.canvas.width, this.canvas.height);
ctx.globalAlpha = 1;
// Draw FFT
this.audioAnalyser.getByteFrequencyData(this.audioBuffer);
const scale = this.canvas.height / (2 * 255);
const { frequencyBinCount } = this.audioAnalyser;
const { sampleRate } = this.context;
{
const step = this.canvas.width / frequencyBinCount;
ctx.beginPath();
ctx.moveTo(-step, this.audioBuffer[0] * scale);
for (let index = 0; index < frequencyBinCount; index++) {
ctx.lineTo(index * step, scale * this.audioBuffer[index]);
}
ctx.strokeStyle = "white";
ctx.stroke();
}
// Get the highest dominant frequency
const step = this.canvas.width / frequencyBinCount;
let highestFreqHz = 0;
{
/**
* Highest frequency index in the buffer
*/
let highestFreqIndex = NaN;
let highestFreqAmp = NaN;
let remSteps = NaN;
for (let i = frequencyBinCount - 1; i >= 0; i--) {
const sample = this.audioBuffer[i];
if (sample > 30) {
if (isNaN(highestFreqAmp)) {
highestFreqIndex = i;
highestFreqAmp = sample;
}
else {
if (sample > highestFreqAmp) {
highestFreqIndex = i;
highestFreqAmp = sample;
}
}
//if (isNaN(remSteps)) remSteps = 100;
}
if (!isNaN(remSteps)) {
if (remSteps-- < 0)
break;
}
}
if (!isNaN(highestFreqIndex)) {
// Force exact value: (not necessary)
highestFreqIndex =
(OSCILLATOR_HZ * (2 * frequencyBinCount)) / sampleRate;
ctx.beginPath();
ctx.moveTo(highestFreqIndex * step, 0);
ctx.lineTo(highestFreqIndex * step, scale * 255);
ctx.strokeStyle = "green";
ctx.stroke();
highestFreqHz =
(highestFreqIndex * sampleRate) / (2 * frequencyBinCount);
window.HZ = highestFreqHz;
}
}
// Draw Audio
this.audioAnalyser.getByteTimeDomainData(this.audioBuffer);
{
const iv = highestFreqHz == 0 ? 0 : 1 / highestFreqHz;
const bufferSize = this.audioBuffer.length;
const offsetY = -this.canvas.height / 2.4;
const startIndex = Math.round(iv * sampleRate);
const step = this.canvas.width / (this.audioBuffer.length - startIndex);
const scale = this.canvas.height / (3 * 255);
const offsetX = highestFreqHz == 0
? 0
: Math.round((this.context.currentTime % iv) * sampleRate) %
bufferSize;
// Draw the audio graph with the given offset
ctx.beginPath();
ctx.moveTo(-step, this.audioBuffer[startIndex - offsetX] * scale + offsetY);
for (let i = startIndex; i < bufferSize; i += 4) {
const index = (i - offsetX) % bufferSize;
const sample = this.audioBuffer[index];
ctx.lineTo((i - startIndex) * step, scale * sample + offsetY);
}
ctx.strokeStyle = "white";
ctx.stroke();
}
}
}
window.addEventListener("load", () => {
const app = new App(document.getElementById("visualizer"), document.getElementById("options"));
requestAnimationFrame(draw);
function draw() {
requestAnimationFrame(draw);
app.drawVisualizer();
}
});
html {
background: black;
}
body {
width: 100vw;
height: 100vh;
margin: 0;
overflow: hidden;
}
#visualizer {
position: fixed;
inset: 0;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Equalizer</title>
</head>
<body>
<div id="visualizer"></div>
<div id="options"></div>
</body>
</html>