Realtime Audio Analyzer Bars Demo

Demonstrates how to create a JavaScript Frequency / Audio Analyzer Bars with Fourier Transform (Frequency spectra) and a real-time frequency history using heatmaps. Note: this example requires microphone permissions to run.

Fullscreen

Edit

 Edit

Docs

drawExample.ts

index.tsx

theme.ts

AudioDataProvider.ts

Radix2FFT.ts

Copy to clipboard
Minimise
Fullscreen
1import { AudioDataProvider } from "./AudioDataProvider";
2import { Radix2FFT } from "./Radix2FFT";
3import { appTheme } from "../../../theme";
4import {
5    XyDataSeries,
6    UniformHeatmapDataSeries,
7    TextAnnotation,
8    ECoordinateMode,
9    EHorizontalAnchorPoint,
10    EVerticalAnchorPoint,
11    SciChartSurface,
12    NumericAxis,
13    EAutoRange,
14    NumberRange,
15    FastLineRenderableSeries,
16    EColumnYMode,
17    EColumnMode,
18    XyxyDataSeries,
19    FastRectangleRenderableSeries,
20    IRenderableSeries,
21    parseColorToUIntArgb,
22    EFillPaletteMode,
23    IFillPaletteProvider,
24    EDataPointWidthMode,
25    XyyDataSeries,
26    DefaultPaletteProvider,
27} from "scichart";
28
29const AUDIO_STREAM_BUFFER_SIZE = 2048;
30
31export const getChartsInitializationApi = () => {
32    let createGauge: (value: number, position: number, label: string) => void = function () {};
33
34    const dataProvider = new AudioDataProvider();
35
36    const bufferSize = dataProvider.bufferSize;
37    const sampleRate = dataProvider.sampleRate;
38
39    const fft = new Radix2FFT(bufferSize);
40
41    const hzPerDataPoint = sampleRate / bufferSize;
42    const fftSize = fft.fftSize;
43    const fftCount = 200;
44
45    let audioDS: XyDataSeries;
46    let historyDS: XyDataSeries;
47    const updateFunctions: Array<(value: number, label: string) => void> = [];
48
49    let hasAudio: boolean;
50
51    const helpText = new TextAnnotation({
52        x1: 0,
53        y1: 0,
54        xAxisId: "history",
55        xCoordinateMode: ECoordinateMode.Relative,
56        yCoordinateMode: ECoordinateMode.Relative,
57        horizontalAnchorPoint: EHorizontalAnchorPoint.Left,
58        verticalAnchorPoint: EVerticalAnchorPoint.Top,
59        text: "This example requires microphone permissions.  Please click Allow in the popup.",
60        textColor: "#FFFFFF88",
61    });
62
63    function updateAnalysers(frame: number): void {
64        // Make sure Audio is initialized
65        if (dataProvider.initialized === false) {
66            return;
67        }
68
69        // Get audio data
70        const audioData = dataProvider.next();
71
72        // Update Audio Chart. When fifoCapacity is set, data automatically scrolls
73        audioDS.appendRange(audioData.xData, audioData.yData);
74
75        // Update History. When fifoCapacity is set, data automatically scrolls
76        historyDS.appendRange(audioData.xData, audioData.yData);
77
78        // Perform FFT
79        const fftData = fft.run(audioData.yData);
80
81        // Update FFT Chart. Clear() and appendRange() is a fast replace for data (if same size)
82        // fftDS.clear();
83        // fftDS.appendRange(fftXValues, fftData);
84
85        function calculateAverages(array: number[]) {
86            // Check if array has exactly 1024 elements
87            if (array.length !== 1024) {
88                throw new Error("Array must have exactly 1024 elements");
89            }
90
91            const result = [];
92            const groupSize = 128;
93
94            // Process each group of 128 elements
95            for (let i = 0; i < array.length; i += groupSize) {
96                const group = array.slice(i, i + groupSize);
97                const sum = group.reduce((acc: any, val: any) => acc + val, 0);
98                const average = sum / groupSize;
99                result.push(average);
100            }
101
102            return result;
103        }
104
105        // function findMinMax(array: number[]) {
106        //     if (array.length === 0) {
107        //         throw new Error("Array cannot be empty");
108        //     }
109
110        //     return JSON.stringify([Math.min(...array), Math.max(...array)]);
111        // }
112
113        // const averages = calculateAverages(fftData);
114
115        let calculateValues = [
116            (fftData[1] + fftData[2] + fftData[3]) / 3,
117            (fftData[4] + fftData[5] + fftData[6]) / 3,
118            (fftData[10] + fftData[11] + fftData[12]) / 3,
119            (fftData[21] + fftData[22] + fftData[23] + fftData[24]) / 4,
120            (fftData[44] + fftData[45] + fftData[46] + fftData[47] + fftData[48]) / 5,
121            (fftData[90] + fftData[91] + fftData[92] + fftData[93] + fftData[94]) / 5,
122            (fftData[183] + fftData[184] + fftData[185] + fftData[186] + fftData[187]) / 5,
123            (fftData[369] + fftData[370] + fftData[371] + fftData[372] + fftData[373]) / 5,
124            (fftData[740] + fftData[741] + fftData[742] + fftData[743] + fftData[744]) / 5,
125            (fftData[1019] + fftData[1020] + fftData[1021] + fftData[1022] + fftData[1023]) / 5,
126        ];
127
128        let frequencies = ["62Hz", "125Hz", "250Hz", "500Hz", "1Khz", "2Khz", "4Khz", "8Khz", "16Khz", "22Khz"];
129
130        calculateValues
131            .map((d) => d / 2 - 10)
132            .forEach((d, i) => {
133                updateFunctions[i](d, frequencies[i]);
134            });
135    }
136
137    // AUDIO CHART
138    const initAudioChart = async (rootElement: string | HTMLDivElement) => {
139        // Create a chart for the audio
140        const { sciChartSurface, wasmContext } = await SciChartSurface.create(rootElement, {
141            theme: appTheme.SciChartJsTheme,
142        });
143
144        // Create an XAxis for the live audio
145        const xAxis = new NumericAxis(wasmContext, {
146            id: "audio",
147            autoRange: EAutoRange.Always,
148            drawLabels: false,
149            drawMinorTickLines: false,
150            drawMajorTickLines: false,
151            drawMajorBands: false,
152            drawMinorGridLines: false,
153            drawMajorGridLines: false,
154        });
155        sciChartSurface.xAxes.add(xAxis);
156
157        // Create an XAxis for the history of the audio on the same chart
158        const xhistAxis = new NumericAxis(wasmContext, {
159            id: "history",
160            autoRange: EAutoRange.Always,
161            drawLabels: false,
162            drawMinorGridLines: false,
163            drawMajorTickLines: false,
164        });
165        sciChartSurface.xAxes.add(xhistAxis);
166
167        // Create a YAxis for the audio data
168        const yAxis = new NumericAxis(wasmContext, {
169            autoRange: EAutoRange.Never,
170            visibleRange: new NumberRange(-32768 * 0.8, 32767 * 0.8), // [short.MIN. short.MAX]
171            drawLabels: false,
172            drawMinorTickLines: false,
173            drawMajorTickLines: false,
174            drawMajorBands: false,
175            drawMinorGridLines: false,
176            drawMajorGridLines: false,
177        });
178        sciChartSurface.yAxes.add(yAxis);
179
180        // Initializing a series with fifoCapacity enables scrolling behaviour and auto discarding old data
181        audioDS = new XyDataSeries(wasmContext, { fifoCapacity: AUDIO_STREAM_BUFFER_SIZE });
182
183        // Fill the data series with zero values
184        for (let i = 0; i < AUDIO_STREAM_BUFFER_SIZE; i++) {
185            audioDS.append(0, 0);
186        }
187
188        // Add a line series for the live audio data
189        // using XAxisId=audio for the live audio trace scaling
190        const rs = new FastLineRenderableSeries(wasmContext, {
191            xAxisId: "audio",
192            stroke: "#4FBEE6",
193            strokeThickness: 2,
194            dataSeries: audioDS,
195        });
196
197        sciChartSurface.renderableSeries.add(rs);
198
199        // Initializing a series with fifoCapacity enables scrolling behaviour and auto discarding old data.
200        historyDS = new XyDataSeries(wasmContext, { fifoCapacity: AUDIO_STREAM_BUFFER_SIZE * fftCount });
201        for (let i = 0; i < AUDIO_STREAM_BUFFER_SIZE * fftCount; i++) {
202            historyDS.append(0, 0);
203        }
204
205        // Add a line series for the historical audio data
206        // using the XAxisId=history for separate scaling for this trace
207        const histrs = new FastLineRenderableSeries(wasmContext, {
208            stroke: "#208EAD33",
209            strokeThickness: 1,
210            opacity: 0.5,
211            xAxisId: "history",
212            dataSeries: historyDS,
213        });
214        sciChartSurface.renderableSeries.add(histrs);
215
216        // Add instructions
217        sciChartSurface.annotations.add(helpText);
218
219        hasAudio = await dataProvider.initAudio();
220
221        return { sciChartSurface };
222    };
223
224    // FFT CHART
225    const initFftChart = async (rootElement: string | HTMLDivElement) => {
226        const GRADIENT_COLOROS = [
227            "#1C5727",
228            "#277B09",
229            "#2C8A26",
230            "#3CAC45",
231            "#58FF80",
232            "#59FD03",
233            "#7FFC09",
234            "#98FA96",
235            "#AEFE2E",
236            "#FEFCD2",
237            "#FBFF09",
238            "#FBD802",
239            "#F9A700",
240            "#F88B01",
241            "#F54602",
242            "#F54702",
243            "#F50E02",
244            "#DA153D",
245            "#B22122",
246            "#B22122",
247        ];
248
249        const { sciChartSurface, wasmContext } = await SciChartSurface.create(rootElement, {
250            theme: appTheme.SciChartJsTheme,
251        });
252
253        const growByX = new NumberRange(0.01, 0.01);
254        const growByY = new NumberRange(0.1, 0.05);
255
256        const xAxis = new NumericAxis(wasmContext, {
257            isVisible: false,
258            growBy: growByX,
259        });
260
261        const yAxis = new NumericAxis(wasmContext, {
262            growBy: growByY,
263        });
264        sciChartSurface.xAxes.add(xAxis);
265        sciChartSurface.yAxes.add(yAxis);
266
267        class RectangleFillPaletteProvider extends DefaultPaletteProvider {
268            public readonly fillPaletteMode: EFillPaletteMode = EFillPaletteMode.SOLID;
269
270            private readonly colors: number[];
271
272            constructor(colorStrings: string[]) {
273                super();
274                // Convert hex color strings to ARGB numbers
275                this.colors = colorStrings.map((color) => parseColorToUIntArgb(color));
276            }
277
278            public overrideFillArgb(
279                xValue: number,
280                yValue: number,
281                index: number,
282                opacity?: number,
283                metadata?: any
284            ): number | undefined {
285                let color = this.colors[index - 1];
286
287                // Return different color based on index
288                return color;
289            }
290        }
291
292        const createGauge = (value: number, width: number, position: number, label: string) => {
293            const dataSeries = new XyyDataSeries(wasmContext);
294
295            const backgroundRectangle = new FastRectangleRenderableSeries(wasmContext, {
296                dataSeries: new XyyDataSeries(wasmContext, {
297                    xValues: [-2 + position * width * 2],
298                    yValues: [-10.5],
299                    y1Values: [10.5],
300                }),
301                columnXMode: EColumnMode.Start,
302                columnYMode: EColumnYMode.TopBottom,
303                dataPointWidth: width + 4,
304                dataPointWidthMode: EDataPointWidthMode.Range,
305                fill: appTheme.DarkIndigo,
306                strokeThickness: 2,
307                stroke: "gray",
308            });
309
310            const rectangleSeries = new FastRectangleRenderableSeries(wasmContext, {
311                dataSeries,
312                columnXMode: EColumnMode.Start,
313                columnYMode: EColumnYMode.TopBottom,
314                dataPointWidth: width,
315                dataPointWidthMode: EDataPointWidthMode.Range,
316                stroke: appTheme.DarkIndigo, // Thick stroke same color as background gives gaps between rectangles
317                strokeThickness: 4,
318                paletteProvider: new RectangleFillPaletteProvider(GRADIENT_COLOROS),
319                fill: appTheme.ForegroundColor + "00",
320            });
321
322            sciChartSurface.renderableSeries.add(backgroundRectangle, rectangleSeries);
323
324            const annotation = new TextAnnotation({
325                x1: 5.5 + position * width * 2,
326                y1: -11,
327                fontSize: 12,
328                textColor: "#FFFFFF",
329                horizontalAnchorPoint: EHorizontalAnchorPoint.Center,
330                verticalAnchorPoint: EVerticalAnchorPoint.Top,
331            });
332            sciChartSurface.annotations.add(annotation);
333
334            const updateGaugeData = (value: number, label: string) => {
335                dataSeries.clear();
336                const columnYValues = [
337                    -10, -9, -8, -7, -6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
338                ].filter((y) => y <= value);
339                const xValues = columnYValues.map((d) => position * width * 2);
340                const yValues = columnYValues.map((d, i) => (i === 0 ? columnYValues[0] : columnYValues[i - 1]));
341                dataSeries.appendRange(xValues, yValues, columnYValues);
342                annotation.text = label;
343            };
344            updateGaugeData(value, label);
345
346            return updateGaugeData;
347        };
348
349        for (let i = 0; i < 10; i++) {
350            updateFunctions.push(createGauge(0, 10, i, "0"));
351        }
352        return { sciChartSurface };
353    };
354
355    const onAllChartsInit = () => {
356        if (!hasAudio) {
357            console.log("dataProvider", dataProvider);
358            if (dataProvider.permissionError) {
359                helpText.text =
360                    "We were not able to access your microphone.  This may be because you did not accept the permissions.  Open your browser security settings and remove the block on microphone permissions from this site, then reload the page.";
361            } else if (!window.isSecureContext) {
362                helpText.text = "Cannot get microphone access if the site is not localhost or on https";
363            } else {
364                helpText.text = "There was an error trying to get microphone access.  Check the console";
365            }
366
367            return { startUpdate: () => {}, stopUpdate: () => {}, cleanup: () => {} };
368        } else {
369            helpText.text = "This example uses your microphone to generate waveforms. Say something!";
370
371            // START ANIMATION
372
373            let frameCounter = 0;
374            const updateChart = () => {
375                if (!dataProvider.isDeleted) {
376                    updateAnalysers(frameCounter++);
377                }
378            };
379
380            let timerId: NodeJS.Timeout;
381
382            const startUpdate = () => {
383                timerId = setInterval(updateChart, 20);
384            };
385
386            const stopUpdate = () => {
387                clearInterval(timerId);
388            };
389
390            const cleanup = () => {
391                dataProvider.closeAudio();
392            };
393
394            return { startUpdate, stopUpdate, cleanup };
395        }
396    };
397
398    return { initAudioChart, initFftChart, onAllChartsInit };
399};
400

JavaScript Audio Spectral Analyzer

Overview

The Audio Analyzer Bars example demonstrates real-time audio analysis using the Web Audio API in JavaScript. It captures microphone input via the browser by using navigator.mediaDevices.getUserMedia (MDN getUserMedia) and processes the audio signal in real time. The processed audio data is visualized using three charts: an audio waveform, an FFT spectrum, and a spectrogram heatmap.

Technical Implementation

The implementation captures audio from the user and applies real-time buffering with a FIFO mechanism to ensure smooth scrolling updates. This approach is similar to techniques discussed in articles like Creating an Interactive Audio Visualizer with JavaScript.

A key aspect of the example is the FFT analysis, which is implemented via a Radix2FFT algorithm that utilizes bit reversal and butterfly calculations. For a deeper understanding of these processes, one can refer to FFT: Bit Reversing and Butterfly.

Features and Capabilities

Real-time Visualization: The example renders an audio waveform using a FastLineRenderableSeries, an FFT spectrum via a FastMountainRenderableSeries with a gradient palette created by PaletteFactory.createGradient(), and a scrolling real-time spectrogram using a UniformHeatmapDataSeries. The spectrogram implementation leverages the concepts outlined in The Uniform Heatmap Chart Type.

Performance Optimizations: Smooth real-time updates are achieved through efficient animation loops using setInterval. Additionally, the application considers performance enhancements for handling complex arithmetic operations within the FFT computation, aligning with strategies found in Performance Optimisation of JavaScript Charts.

Integration and Best Practices

The code follows best practices for secure and efficient real-time audio processing in JavaScript. It ensures that microphone permissions are obtained securely and that the audio context is managed properly. The modular design allows for extensibility and customization, while the careful handling of data buffering and real-time updates provides a responsive user experience. This example is ideal for developers looking to integrate the Web Audio API with advanced charting libraries such as SciChart.js to build high-performance, interactive audio visualizations.

javascript Chart Examples & Demos

See Also: Scientific & Medical Charts (10 Demos)

JavaScript Vital Signs ECG/EKG Medical Demo | SciChart.js

JavaScript Vital Signs ECG/EKG Medical Demo

In this example we are simulating four channels of data showing that SciChart.js can be used to draw real-time ECG/EKG charts and graphs to monitor heart reate, body temperature, blood pressure, pulse rate, SPO2 blood oxygen, volumetric flow and more.

JavaScript Chart with Logarithmic Axis Example | SciChart

JavaScript Chart with Logarithmic Axis Example

Demonstrates Logarithmic Axis on a JavaScript Chart using SciChart.js. SciChart supports logarithmic axis with scientific or engineering notation and positive and negative values

LiDAR 3D Point Cloud of Geospatial Data | SciChart.js

LiDAR 3D Point Cloud of Geospatial Data

Demonstrating the capability of SciChart.js to create JavaScript 3D Point Cloud charts and visualize LiDAR data from the UK Defra Survey.

JavaScript Chart with Vertically Stacked Axes | SciChart

JavaScript Chart with Vertically Stacked Axes

Demonstrates Vertically Stacked Axes on a JavaScript Chart using SciChart.js, allowing data to overlap

Realtime Audio Spectrum Analyzer Chart | SciChart.js Demo

Realtime Audio Spectrum Analyzer Chart Example

See the frequency of recordings with the JavaScript audio spectrum analyzer example from SciChart. This real-time visualizer demo uses a Fourier Transform.

Interactive Waterfall Chart | Javascript Charts | SciChart.js

Interactive Waterfall Spectral Chart

Demonstrates how to create a Waterfall chart in SciChart.js, showing chromotragraphy data with interactive selection of points.

Interactive Phasor Diagram chart | Javascript Charts | SciChart.js

Phasor Diagram Chart Example

See the JavaScript Phasor Diagram example to combine a Cartesian surface with a Polar subsurface. Get seamless JS integration with SciChart. View demo now.

NEW!
JavaScript Correlation Plot | Javascript Charts | SciChart.js

JavaScript Correlation Plot

Create JavaScript Correlation Plot with high performance SciChart.js. Easily render pre-defined point types. Supports custom shapes. Get your free trial now.

NEW!
Semiconductors Dashboard | JavaScript Charts | SciChart.js

Semiconductors Dashboard

JavaScript **Semiconductors Dashboard** using SciChart.js, by leveraging the **FastRectangleRenderableSeries**, and its `customTextureOptions` property to have a custom tiling texture fill.

NEW!
Wafer Analysis Chart | JavaScript Charts | SciChart.js

Wafer Analysis Chart

JavaScript **Wafer Analysis Chart** using SciChart.js, by leveraging the **FastRectangleRenderableSeries**, and crossfilter to enable live filtering.

SciChart Ltd, 16 Beaufort Court, Admirals Way, Docklands, London, E14 9XL.