fciannella's picture
Working with service run on 7860
53ea588
// 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<HTMLCanvasElement | null>(null);
const animationRef = useRef<number | null>(null);
const audioContextRef = useRef<AudioContext | null>(null);
const analyserRef = useRef<AnalyserNode | null>(null);
const sourceRef = useRef<MediaStreamAudioSourceNode | null>(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 (
<canvas
ref={canvasRef}
width={width}
height={height}
style={{
width: "100%",
maxWidth: `${width}px`,
height: `${height}px`,
borderRadius: "4px",
}}
/>
);
}