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.
drawExample.ts
index.tsx
theme.ts
AudioDataProvider.ts
Radix2FFT.ts
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};
400This 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.
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.
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.
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.
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.

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.

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

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

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

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

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

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

Create React Correlation Plot with high performance SciChart.js. Easily render pre-defined point types. Supports custom shapes. Get your free trial now.
React **Semiconductors Dashboard** using SciChart.js, by leveraging the **FastRectangleRenderableSeries**, and its `customTextureOptions` property to have a custom tiling texture fill.

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