From f249c1dbe5a1dce198a025249bcbec9bcfdcbd6e Mon Sep 17 00:00:00 2001 From: Oliver Bryan Date: Sat, 31 Jan 2026 15:54:59 +0000 Subject: [PATCH] waveform visualiser --- src/Index.tsx | 2 +- src/Layout.tsx | 8 +- src/components/AudioWaveform.tsx | 132 +++++++++++++++++++++++++++++++ 3 files changed, 140 insertions(+), 2 deletions(-) create mode 100644 src/components/AudioWaveform.tsx diff --git a/src/Index.tsx b/src/Index.tsx index 88351b7..3cb0d54 100644 --- a/src/Index.tsx +++ b/src/Index.tsx @@ -282,7 +282,7 @@ function Index() { ); return ( - + ); diff --git a/src/Layout.tsx b/src/Layout.tsx index 0a12e03..69f93a5 100644 --- a/src/Layout.tsx +++ b/src/Layout.tsx @@ -1,3 +1,4 @@ +import AudioWaveform from "@/components/AudioWaveform"; import ThemeToggle from "@/components/theme-toggle"; import { Button } from "@/components/ui/button"; import { Home, Settings } from "lucide-react"; @@ -6,9 +7,11 @@ import { Link, useLocation } from "react-router-dom"; export default function Layout({ children, sidebarContent, + waveformColor, }: { children: React.ReactNode; sidebarContent?: React.ReactNode; + waveformColor?: string; }) { const location = useLocation(); @@ -20,7 +23,10 @@ export default function Layout({

The Shape of Sound

-
{sidebarContent || null}
+
{sidebarContent || null}
+ + {/* Audio Waveform Visualization */} +
diff --git a/src/components/AudioWaveform.tsx b/src/components/AudioWaveform.tsx new file mode 100644 index 0000000..49d5e52 --- /dev/null +++ b/src/components/AudioWaveform.tsx @@ -0,0 +1,132 @@ +import { useEffect, useRef } from "react"; +import * as Tone from "tone"; + +export default function AudioWaveform({ color }: { color?: string }) { + const canvasRef = useRef(null); + const analyzerRef = useRef(null); + const animationFrameRef = useRef(null); + + useEffect(() => { + // create analyzer and connect to destination + const analyzer = new Tone.Waveform(512); + Tone.getDestination().connect(analyzer); + analyzerRef.current = analyzer; + + const canvas = canvasRef.current; + if (!canvas) return; + + const ctx = canvas.getContext("2d"); + if (!ctx) return; + + // Set canvas size + const updateCanvasSize = () => { + const dpr = window.devicePixelRatio || 1; + const rect = canvas.getBoundingClientRect(); + canvas.width = rect.width * dpr; + canvas.height = rect.height * dpr; + ctx.scale(dpr, dpr); + }; + + updateCanvasSize(); + window.addEventListener("resize", updateCanvasSize); + + // Animation loop + const draw = () => { + if (!canvas || !ctx || !analyzerRef.current) return; + + const values = analyzerRef.current.getValue(); + const width = canvas.getBoundingClientRect().width; + const height = canvas.getBoundingClientRect().height; + + ctx.clearRect(0, 0, width, height); + + let strokeColor = color || "#caa3ff"; + if (!color) { + const computedStyle = getComputedStyle(canvas); + const primaryColor = computedStyle.getPropertyValue("--primary") || "210 100% 50%"; + + // Convert HSL to RGB for canvas + const hslMatch = primaryColor.match(/(\d+)\s+(\d+)%\s+(\d+)%/); + + if (hslMatch) { + const h = Number.parseInt(hslMatch[1]); + const s = Number.parseInt(hslMatch[2]) / 100; + const l = Number.parseInt(hslMatch[3]) / 100; + + // HSL to RGB conversion + const c = (1 - Math.abs(2 * l - 1)) * s; + const x = c * (1 - Math.abs(((h / 60) % 2) - 1)); + const m = l - c / 2; + + let r = 0; + let g = 0; + let b = 0; + + if (h >= 0 && h < 60) { + r = c; g = x; b = 0; + } else if (h >= 60 && h < 120) { + r = x; g = c; b = 0; + } else if (h >= 120 && h < 180) { + r = 0; g = c; b = x; + } else if (h >= 180 && h < 240) { + r = 0; g = x; b = c; + } else if (h >= 240 && h < 300) { + r = x; g = 0; b = c; + } else { + r = c; g = 0; b = x; + } + + strokeColor = `rgb(${Math.round((r + m) * 255)}, ${Math.round((g + m) * 255)}, ${Math.round((b + m) * 255)})`; + } + } + + ctx.beginPath(); + ctx.strokeStyle = strokeColor; + ctx.lineWidth = 2; + + const sliceWidth = width / values.length; + let x = 0; + + for (let i = 0; i < values.length; i++) { + const value = values[i] as number; + // multiplied amplitude for more prominent visualisation + const amplifiedValue = Math.max(-1, Math.min(1, value * 5)); + const y = ((amplifiedValue + 1) / 2) * height; + + if (i === 0) { + ctx.moveTo(x, y); + } else { + ctx.lineTo(x, y); + } + + x += sliceWidth; + } + + ctx.stroke(); + + animationFrameRef.current = requestAnimationFrame(draw); + }; + + draw(); + + return () => { + if (animationFrameRef.current) { + cancelAnimationFrame(animationFrameRef.current); + } + if (analyzerRef.current) { + analyzerRef.current.dispose(); + } + window.removeEventListener("resize", updateCanvasSize); + }; + }, [color]); + + return ( +
+ +
+ ); +}