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

React Audio Spectral Analyzer

Overview

This example demonstrates a real-time Audio Analyzer Bars built with React and SciChart.js. It leverages the Web Audio API to capture microphone input, processes the audio data with a Fast Fourier Transform using a custom Radix2FFT implementation, and visualizes the results in three different SciChart.js charts: an audio waveform chart, an FFT spectrum chart, and a spectrogram heatmap. This application requires microphone permissions and shows how to create advanced, real-time data visualizations in a React environment.

Technical Implementation

The example integrates SciChart.js into React by using the <SciChartReact/> component along with SciChartGroup to manage multiple charts in a React application. The chart initialization is handled asynchronously via hooks such as useState and useRef. Once the charts are initialized, an interval is set up to periodically fetch audio data, update the associated data series, and perform an FFT calculation on the incoming data. The use of fifoCapacity in the XyDataSeries enables efficient real-time streaming and auto-discarding of old data. Developers interested in understanding how SciChart.js integrates with React can refer to the React Charts with SciChart.js: Introducing “SciChart React” article and the Creating a SciChart React Component from the Ground Up tutorial for additional context.

Features and Capabilities

Real-Time Updates: The application continuously updates all three charts in real time by capturing microphone input and processing it through a Fourier Transform algorithm. The FFT and spectrogram charts offer dynamic visual feedback on frequency components, while the audio chart displays the raw waveform. For more details on real-time data handling, developers can review Adding Realtime Updates | JavaScript Chart Documentation - SciChart.

Advanced Customizations: The charts are highly customizable with options for axes configuration, color palettes, and rendering series. The spectrogram chart, for instance, utilizes a UniformHeatmapDataSeries and a custom HeatmapColorMap to translate FFT data into a visual heatmap.

Integration and Best Practices

The React integration is achieved using standard hooks like useState and useRef to manage chart instance references and asynchronous initialization routines. The SciChartGroup component is used for composing multiple charts together in a seamless layout, in line with best practices for component composition in React. Cleanup is properly handled by stopping the data update interval and closing the audio context when the component is unmounted, which can be seen as a practical example of React cleanup patterns. Additionally, the example demonstrates the integration of browser media APIs for microphone access, ensuring that all necessary permissions are acquired before initiating the data stream. For further insights on integrating media APIs within a React component, consider reading How to Access Microphones Through the Browser API | Speechmatics.

Performance Considerations

Efficient performance is achieved by utilizing techniques such as fifoCapacity in data series, reducing unnecessary redraws and memory allocations. Furthermore, the continuous update cycle via setInterval is optimized to handle high-frequency data updates while maintaining smooth rendering. For more information on performance optimization in React with SciChart.js, check out the article Creating a React Drag & Drop Chart Dashboard Performance Demo with 100 Charts.

react Chart Examples & Demos

See Also: Scientific & Medical Charts (10 Demos)

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

React 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.

React Chart with Logarithmic Axis Example | SciChart.js

React Chart with Logarithmic Axis Example

Demonstrates Logarithmic Axis on a React 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.

React Chart with Vertically Stacked Axes | SciChart.js

React Chart with Vertically Stacked Axes

Demonstrates Vertically Stacked Axes on a React 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 React audio spectrum analyzer example from SciChart. This real-time audio visualizer demo uses a Fourier Transform.

Interactive Waterfall Chart | React 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 | React Charts | SciChart.js

Phasor Diagram Chart Example

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

NEW!
React Correlation Plot | React Charts | SciChart.js Demo

React Correlation Plot

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

NEW!
React Semiconductors Dashboard | JavaScript Charts | SciChart.js

Semiconductors Dashboard

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

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

Wafer Analysis Chart

React **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.