sinsintro-ai-assistant-dh
Version:
A single npm package providing a React-based voice assistant with OpenAI function-calling, purely client-side.
228 lines (198 loc) • 6.17 kB
JSX
// src/frontend/AudioVisualizer.jsx
import React, { useRef, useEffect, forwardRef, useImperativeHandle } from 'react';
import PropTypes from 'prop-types';
import * as dat from 'dat.gui';
import './AudioVisualizer.css'; // Import the CSS for styling
const AudioVisualizer = forwardRef(({ mediaElementSource }, ref) => {
const canvasRef = useRef(null);
const guiRef = useRef(null);
const animationIdRef = useRef(null);
const analyserRef = useRef(null);
const freqsRef = useRef(null);
useImperativeHandle(ref, () => ({
startVisualization: () => {
if (!mediaElementSource) {
console.error('No MediaElementSource provided.');
return;
}
initializeVisualizer();
},
}));
const initializeVisualizer = () => {
const canvas = canvasRef.current;
const ctx = canvas.getContext('2d');
// Visualization options
const opts = {
smoothing: 0.6,
fft: 8,
minDecibels: -70,
scale: 0.2,
glow: 10,
color1: [203, 36, 128],
color2: [41, 200, 192],
color3: [24, 137, 218],
fillOpacity: 0.6,
lineWidth: 1,
blend: 'screen',
shift: 50,
width: 60,
amp: 1,
};
// Initialize dat.GUI
const gui = new dat.GUI();
gui.close(); // Hide by default
// Add controls to GUI
gui.addColor(opts, 'color1').name('Color 1');
gui.addColor(opts, 'color2').name('Color 2');
gui.addColor(opts, 'color3').name('Color 3');
gui.add(opts, 'fillOpacity', 0, 1).name('Fill Opacity');
gui.add(opts, 'lineWidth', 0, 10).step(1).name('Line Width');
gui.add(opts, 'glow', 0, 100).name('Glow');
gui
.add(opts, 'blend', [
'normal',
'multiply',
'screen',
'overlay',
'lighten',
'difference',
])
.name('Blend Mode');
gui
.add(opts, 'smoothing', 0, 1)
.name('Smoothing')
.onChange((value) => {
if (analyserRef.current) {
analyserRef.current.smoothingTimeConstant = value;
}
});
gui
.add(opts, 'minDecibels', -100, 0)
.name('Min Decibels')
.onChange((value) => {
if (analyserRef.current) {
analyserRef.current.minDecibels = value;
}
});
gui.add(opts, 'amp', 0, 5).name('Amplitude');
gui.add(opts, 'width', 0, 60).name('Width');
gui.add(opts, 'shift', 0, 200).name('Shift');
guiRef.current = gui;
// Initialize AudioContext and Analyser
const audioContext = mediaElementSource.context;
const analyser = audioContext.createAnalyser();
analyser.smoothingTimeConstant = opts.smoothing;
analyser.fftSize = Math.pow(2, opts.fft);
analyser.minDecibels = opts.minDecibels;
analyser.maxDecibels = 0;
analyserRef.current = analyser;
// Connect MediaElementSource to Analyser
mediaElementSource.connect(analyser);
analyser.connect(audioContext.destination);
// Frequency Data Array
const freqs = new Uint8Array(analyser.frequencyBinCount);
freqsRef.current = freqs;
// Utility Functions
const range = (i) => Array.from({ length: i }, (_, index) => index);
const shuffle = [1, 3, 0, 4, 2];
const freqFn = (channel, i) => freqs[2 * channel + shuffle[i] * 6] || 0;
const scaleFn = (i) => {
const x = Math.abs(2 - i);
const s = 3 - x;
return (s / 3) * opts.amp;
};
const path = (channel) => {
const color = opts[`color${channel + 1}`].map(Math.floor);
ctx.fillStyle = `rgba(${color}, ${opts.fillOpacity})`;
ctx.strokeStyle = ctx.shadowColor = `rgb(${color})`;
ctx.lineWidth = opts.lineWidth;
ctx.shadowBlur = opts.glow;
ctx.globalCompositeOperation = opts.blend;
const m = canvas.height / 2;
const offset = (canvas.width - 15 * opts.width) / 2;
const xPositions = range(15).map(
(i) => offset + channel * opts.shift + i * opts.width
);
const yValues = range(5).map((i) =>
Math.max(0, m - scaleFn(i) * freqFn(channel, i))
);
const h = 2 * m;
ctx.beginPath();
ctx.moveTo(0, m);
ctx.lineTo(xPositions[0], m + 1);
// Upper Path
for (let i = 0; i < 5; i++) {
ctx.bezierCurveTo(
xPositions[1 + i * 2],
m + 1,
xPositions[2 + i * 2],
yValues[i],
xPositions[3 + i * 2],
yValues[i]
);
}
ctx.bezierCurveTo(
xPositions[13],
m,
xPositions[12],
m,
xPositions[13],
m + 1
);
ctx.lineTo(canvas.width, m + 1);
ctx.lineTo(xPositions[13], m - 1);
// Lower Path
for (let i = 4; i >= 0; i--) {
ctx.bezierCurveTo(
xPositions[12 - i * 2],
m,
xPositions[12 - i * 2],
h - yValues[i],
xPositions[11 - i * 2],
h - yValues[i]
);
}
ctx.lineTo(0, m);
ctx.closePath();
ctx.fill();
ctx.stroke();
};
const visualize = () => {
analyser.getByteFrequencyData(freqs);
// Clear Canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw Paths for Each Channel
path(0);
path(1);
path(2);
// Schedule next frame
animationIdRef.current = requestAnimationFrame(visualize);
};
// Start Visualization
visualize();
};
// Cleanup on Unmount
useEffect(() => {
return () => {
cancelAnimationFrame(animationIdRef.current);
if (guiRef.current) {
guiRef.current.destroy();
}
if (analyserRef.current) {
analyserRef.current.disconnect();
}
};
}, []);
return (
<canvas
ref={canvasRef}
width={1000}
height={400}
className="audio-visualizer-canvas"
></canvas>
);
});
AudioVisualizer.propTypes = {
mediaElementSource: PropTypes.object.isRequired,
};
export default AudioVisualizer;