Frequency-focused audio visualization at the heart of the Audio API

Currently, I am developing a web-based audio visualizer that allows users to adjust the raw audio signal visualizer to a specific frequency, similar to the functionality found in hardware oscilloscopes. The goal is for the wave to remain stationary on the canvas when centered at 440Hz with a corresponding sine wave that repeats every 1/440s. My initial approach involved shifting the graph to the left based on the frequency (e.g., 440Hz = move 1/440s to the left per second), but this method proved ineffective.

I encountered difficulty determining the units used by the Audio Analyzer Node's time domain data. While I believe it may be in milliseconds, I cannot confirm this.

"use strict";
// Debugging using Oscillator instead of microphone
const USE_OSCILLATOR = true;
// Checking compatibility
if (!window.AudioContext)
    window.AudioContext = window.webkitAudioContext;
if (!navigator.getUserMedia)
    navigator.getUserMedia =
        navigator.mozGetUserMedia ||
            navigator.webkitGetUserMedia ||
            navigator.msGetUserMedia;
// Main Application Class
class App {
    constructor(visualizerElement, optionsElement) {
        this.visualizerElement = visualizerElement;
        this.optionsElement = optionsElement;
        // Create HTML elements
        this.canvas = document.createElement("canvas");
        // Context initialization
        this.context = new AudioContext({
            // Optimize for low latency
            latencyHint: "interactive",
        });
        this.canvasCtx = this.canvas.getContext("2d", {
            // Optimize for low latency and performance
            desynchronized: true,
            alpha: false,
        });
        // Initialize 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();
        // Identify the highest dominant frequency
        let highestFreqHalfHz = 0;
        {
            /**
             * Highest frequency in 0.5Hz increments
             */
            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 waveform
        this.audioAnalyser.getByteTimeDomainData(this.audioBuffer);
        {
            const bufferSize = this.audioBuffer.length;
            const offsetY = -this.canvas.height * 0.5;
            // Determining offset value
            const offsetX = highestFreqHalfHz == 0
                ? 0
                : bufferSize -
                    Math.round(((this.context.currentTime * 1000) % (1 / 440)) % bufferSize);
            // Rendering the audio graph with the calculated 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>

The TypeScript snippet above demonstrates the integration of an oscillating graph within an audio visualizer application. For further details, refer to the source here. If functioning correctly, the oscillating graph should remain motionless.

Answer №1

This problem was successfully resolved with the help of Raymond Toy's comment and Mr. Klein, my mathematics teacher. The solution involved using the formula

Math.round((this.context.currentTime % iv) * sampleRate)
, where iv represents the interval of the frequency (1/Hz). Although the wave is not perfectly centered, I managed to force the detected frequency to match the specified one in the example provided below.

"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) {
        // Implementation details
    }
    
    // Additional methods
    
}
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>

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Guide to implementing event handling with JavaScript on an HTML form webpage

On my HTML page, I have a PHP extension file named 'welcome.php'. Within this page, there is a button labeled "baggage.php" that, when clicked, opens another page. On this new page, I have created dropdown menus for 'From' and 'To& ...

Retrieving POST data in Ajax requests

I am currently using an ajax-based commenting system. This system is set up to create a div section for comments and a reply box through a php loop, which results in the form being duplicated multiple times. How can I ensure that the variable 'parent ...

What is the process of using the split() method to extract specific text from a string in JavaScript?

Working with AngularJS and facing a requirement to extract a specific text from a scope value. Here's my scope value: $scope.myLogMsg = '1111 order is placed by Sukumar Ms(User)'; //output: by Sukumar Ms(User) Desired output: by Sukumar Ms ...

Creating multi-dimensional arrays using array lists with Axios and Vue.js

My knowledge of axios and JavaScript is limited, and I find myself struggling with creating a multi-dimensional array. 1. This is how I want my data to be structured : https://i.stack.imgur.com/kboNU.png userList: [ { ...

Generate a customizable URL for my search bar

I have developed a single HTML page with full AJAX functionality, displaying all content including form submits, main page, form results, and more through various DOM manipulations. The issue I am currently encountering is related to a search box where use ...

How can you use jQuery to display an image when hovering over text?

Looking for a tutorial or script that displays an image when hovering over HTML text with the mouse? ...

"Retrieve and transfer image data from a web browser to Python's memory with the help

Is there a way to transfer images from a browser directly into Python memory without having to re-download them using urllib? The images are already loaded in the browser and have links associated with them. I want to avoid downloading them again and ins ...

Incorporate a Vue component into a string for seamless integration with Buefy's confirmation feature

Can a vue component be loaded into a string for use with buefy confirm? I attempted the following approach, but it did not work as expected: archiveChannelPrompt () { const archiveChannel = document.createElement('template'); new Vue({ ...

Experiencing challenges with socket io connectivity between my backend and Quasar application. Encountering a 403 Forbidden error while trying to establish a connection with

I recently updated my Quasar app and ran into issues with the websocket connection after switching from development to production mode. The app is hosted on an Express server via pm2 on my Ubuntu server, which also connects to a database. Here are some sn ...

Modifying the state object in ReactJS: A step-by-step guide on updating values

Below my query and explanation, you will find all the code. I am currently attempting to update the state in a grandparent class. This is necessary due to file-related reasons, and I am using Material-UI for text boxes. Additionally, I am implementing Red ...

By utilizing a function provided within the context, React state will consistently have its original value

After passing functions from one component to its parent and then through context, updating the state works fine. However, there is an issue with accessing the data inside these functions. They continue to show as the initial data rather than the updated v ...

The absence of defined exports in TypeScript has presented a challenge, despite attempting various improvement methods

I've exhausted all available resources on the web regarding the TypeScript export issues, but none seem to resolve the problem. Watching a tutorial on YouTube, the presenter faced no such obstacles as I am encountering now. After updating the tsconf ...

Guide on utilizing fs.readStream and fs.writesream for transmitting and receiving video file (.mp4) either from server to client or vice versa using Node Js

## My Attempt to Receive and Save Video Stream in .mp4 Format ## ---------- > Setting up Server Side Code > Receiving Video stream from client and saving it as a .mp4 file var express = require('express'); var app = global.app = expor ...

Unlocking the Potential of Vue Class Components: Exploring Advanced Customization Options

Currently, I am working on a project using Vue 2 with Typescript. However, I am facing an issue where I cannot add options to the component. <script lang="ts"> import { Component, Vue } from "vue-property-decorator"; import HelloW ...

Tips on getting the bot to react to a single "event" mentioned in the sentence, without multiple occurrences

Things are a bit complicated, but here's an example to illustrate: I've set up this event: client.on('message', async message => { if (message.content.toLowerCase().includes("megumin")) { message.channel.send("W ...

Transform a C# DateTime object into a JavaScript DateTime object

I'm attempting to convert a C# DateTime variable into a format that can be passed into a JavaScript function using NewtonSoft.Json. Currently, I am using the following code: var jsonSettings = new JsonSerializerSettings(); jsonSettings.DateFormatStr ...

The message sent back by Django Rest Framework is: "a legitimate integer must be provided"

I have integrated a react form within my Django application, supported by the rest framework in the backend. When I submit the form without entering any value in the integer field, I encounter the following error message from the rest API: "a valid integer ...

A more cost-effective method to replicate an image multiple times using Angular

In the productivity tool I've created, there is a daily log of pomodoros completed. Each day's progress is displayed on a timeline with a tomato icon representing each pomodoro. To achieve this visual representation, I am using AngularJS directiv ...

Guide to using three.js for applying a texture to an mtl and obj file

I'm attempting to apply a png file onto an mtl/obj file. Prior to mapping However, after mapping, the mtl texture seems to disappear. After mapping What should I do? My English skills are not great. If you have trouble understanding my question, p ...

Identify circular surfaces in three.js

I've successfully developed a dynamic scene using three.js, featuring a textured and meshed cylinder along with a grid containing lines and vertices. The cylinder is programmed to have a spinning effect when the mouse is dragged left or right. <!D ...