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

Angular Audio Spectral Analyzer

Overview

This example demonstrates a real-time Audio Analyzer Bars built with Angular and SciChart.js. It captures live microphone input using the Web Audio API and visualizes both time-domain and frequency-domain data through multiple SciChart charts embedded via the ScichartAngularComponent. Microphone permissions are required, and the example showcases how to leverage Angular standalone components for high-performance charting.

Technical Implementation

The implementation is encapsulated in an Angular standalone component which is imported from scichart-angular. The component makes use of Angular's template features with inline CSS styling (using ngStyle) to dynamically apply themes, as explained in Getting started with standalone components - Angular. Chart initialization occurs asynchronously through the (onInit) event, allowing each chart instance (audio waveform, FFT spectrum, and spectrogram) to be configured when ready. Real-time updates are managed with a setInterval loop, echoing techniques from setInterval and updating values AngularJS. Furthermore, the audio data is processed by a custom FFT implementation Radix2FFT, ensuring that computational optimizations are in place for live data streaming.

Features and Capabilities

Real-Time Data Updates: The demo continuously processes incoming audio data to update three separate chart instances, offering dynamic visual feedback.

Advanced Customizations: Each chart is specifically configured with custom axes, FIFO data series for auto scrolling, and tailored renderable series, which improves performance and clarity. For further details on managing multiple chart instances, developers can refer to Tutorial 09 - Linking Multiple Charts.

Integration and Best Practices

The Angular integration utilizes standard lifecycle events such as onInit to handle asynchronous chart initialization and resource management. The example follows best practices by leveraging Angular's dynamic styling capabilities via ngStyle, as highlighted in Using ngStyle in Angular for dynamic styling - Ultimate Courses. Additionally, cleanup procedures are implemented to properly manage subscriptions and intervals in line with insights from Angular component cleanup patterns. The integration of the Web Audio API is seamlessly achieved within the Angular framework, following the guidelines provided in Using the Web Audio API - MDN Web Docs.

Conclusion

This Realtime Audio Analyzer Bars Demo (Angular) showcases how to build an advanced, real-time audio analysis application using Angular and SciChart.js. It combines dynamic theming, asynchronous chart initialization, and efficient resource management techniques to deliver high-performance visualizations in a modern Angular environment.

angular Chart Examples & Demos

See Also: Scientific & Medical Charts (10 Demos)

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

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

Angular Chart with Logarithmic Axis Example | SciChart.js

Angular Chart with Logarithmic Axis Example

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

Angular Chart with Vertically Stacked Axes | SciChart.js

Angular Chart with Vertically Stacked Axes

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

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

Phasor Diagram Chart Example

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

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

Angular Correlation Plot

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

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

Angular Semiconductors Dashboard

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

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

Angular Wafer Analysis Chart

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