// SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: BSD 2-Clause License import { useRef, useEffect } from "react"; interface AudioWaveFormProps { streamOrTrack?: MediaStream | MediaStreamTrack | null; width?: number; height?: number; lineColor?: string; backgroundColor?: string; } export function AudioWaveForm({ streamOrTrack, width = 300, height = 80, lineColor = "#76B900", // NVIDIA green color backgroundColor = "transparent", }: AudioWaveFormProps) { const canvasRef = useRef(null); const animationRef = useRef(null); const audioContextRef = useRef(null); const analyserRef = useRef(null); const sourceRef = useRef(null); useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; const canvasCtx = canvas.getContext("2d"); if (!canvasCtx) return; // Clear canvas and apply background color canvasCtx.clearRect(0, 0, canvas.width, canvas.height); if (backgroundColor !== "transparent") { canvasCtx.fillStyle = backgroundColor; canvasCtx.fillRect(0, 0, canvas.width, canvas.height); } // If no stream is provided, we just leave the canvas empty if (!streamOrTrack) return; let stream: MediaStream; if (streamOrTrack instanceof MediaStreamTrack) { // If a track is provided, create a MediaStream from it stream = new MediaStream([streamOrTrack]); } else { stream = streamOrTrack; } const audioContext = new AudioContext(); const analyser = audioContext.createAnalyser(); const source = audioContext.createMediaStreamSource(stream); // Connect source to analyzer source.connect(analyser); analyser.fftSize = 256; // Store references for cleanup audioContextRef.current = audioContext; analyserRef.current = analyser; sourceRef.current = source; const bufferLength = analyser.frequencyBinCount; const dataArray = new Uint8Array(bufferLength); const draw = () => { animationRef.current = requestAnimationFrame(draw); const width = canvas.width; const height = canvas.height; analyser.getByteTimeDomainData(dataArray); // Clear canvas canvasCtx.clearRect(0, 0, width, height); if (backgroundColor !== "transparent") { canvasCtx.fillStyle = backgroundColor; canvasCtx.fillRect(0, 0, width, height); } // Draw histogram const barCount = 12; const barWidth = width / barCount; const barSpacing = barWidth * 0.1; // 10% of barWidth for spacing const adjustedBarWidth = barWidth - barSpacing; canvasCtx.fillStyle = lineColor; // Process audio data for each bar for (let i = 0; i < barCount; i++) { // Calculate which portion of the data to use for this bar const dataStart = Math.floor((i / barCount) * bufferLength); const dataEnd = Math.floor(((i + 1) / barCount) * bufferLength); // Calculate average value for this section of data let sum = 0; for (let j = dataStart; j < dataEnd; j++) { // Convert from 0-255 scale to amplitude (-1 to 1) const amplitude = (dataArray[j] - 128) / 128; sum += Math.abs(amplitude); // Use absolute value for energy level } const avgAmplitude = sum / (dataEnd - dataStart) || 0; const minBarHeight = height * 0.02; const scalingFactor = 8; const barHeight = Math.max( minBarHeight, height * Math.min(1, Math.pow(avgAmplitude * scalingFactor, 1.2)) ); // Draw the bar (from bottom of canvas) canvasCtx.fillRect( i * barWidth + barSpacing / 2, height - barHeight, adjustedBarWidth, barHeight ); } }; // Start animation draw(); // Cleanup function return () => { if (animationRef.current) { cancelAnimationFrame(animationRef.current); } if (sourceRef.current) { sourceRef.current.disconnect(); } if ( audioContextRef.current && audioContextRef.current.state !== "closed" ) { audioContextRef.current.close(); } }; }, [streamOrTrack, lineColor, backgroundColor]); return ( ); }