UNPKG

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
// 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;