From 7f0bcea4ca91eccb9e35f25804a4f6feed8c4e11 Mon Sep 17 00:00:00 2001 From: creotove Date: Wed, 25 Jun 2025 15:29:20 +0530 Subject: [PATCH] Feat: basic audio waveform added. --- .../src/components/editor/audio-waveform.tsx | 115 ++++++++++++++++++ apps/web/src/components/editor/timeline.tsx | 17 ++- bun.lock | 3 + package.json | 3 +- 4 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 apps/web/src/components/editor/audio-waveform.tsx diff --git a/apps/web/src/components/editor/audio-waveform.tsx b/apps/web/src/components/editor/audio-waveform.tsx new file mode 100644 index 0000000..75a5ae0 --- /dev/null +++ b/apps/web/src/components/editor/audio-waveform.tsx @@ -0,0 +1,115 @@ +import React, { useEffect, useRef, useState } from 'react'; +import WaveSurfer from 'wavesurfer.js'; + +interface AudioWaveformProps { + audioUrl: string; + height?: number; + className?: string; +} + +const AudioWaveform: React.FC = ({ + audioUrl, + height = 32, + className = '' +}) => { + const waveformRef = useRef(null); + const wavesurfer = useRef(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(false); + + useEffect(() => { + let mounted = true; + + const initWaveSurfer = async () => { + if (!waveformRef.current || !audioUrl) return; + + try { + // Clean up any existing instance + if (wavesurfer.current) { + try { + wavesurfer.current.destroy(); + } catch (e) { + // Silently ignore destroy errors + } + wavesurfer.current = null; + } + + wavesurfer.current = WaveSurfer.create({ + container: waveformRef.current, + waveColor: 'rgba(255, 255, 255, 0.6)', + progressColor: 'rgba(255, 255, 255, 0.9)', + cursorColor: 'transparent', + barWidth: 2, + barGap: 1, + height: height, + normalize: true, + interact: false, + }); + + // Event listeners + wavesurfer.current.on('ready', () => { + if (mounted) { + setIsLoading(false); + setError(false); + } + }); + + wavesurfer.current.on('error', (err) => { + console.error('WaveSurfer error:', err); + if (mounted) { + setError(true); + setIsLoading(false); + } + }); + + await wavesurfer.current.load(audioUrl); + + } catch (err) { + console.error('Failed to initialize WaveSurfer:', err); + if (mounted) { + setError(true); + setIsLoading(false); + } + } + }; + + initWaveSurfer(); + + return () => { + mounted = false; + if (wavesurfer.current) { + try { + wavesurfer.current.destroy(); + } catch (e) { + // Silently ignore destroy errors + } + wavesurfer.current = null; + } + }; + }, [audioUrl, height]); + + if (error) { + return ( +
+ Audio unavailable +
+ ); + } + + return ( +
+ {isLoading && ( +
+ Loading... +
+ )} +
+
+ ); +}; + +export default AudioWaveform; \ No newline at end of file diff --git a/apps/web/src/components/editor/timeline.tsx b/apps/web/src/components/editor/timeline.tsx index ce0ffa3..cc7d7f3 100644 --- a/apps/web/src/components/editor/timeline.tsx +++ b/apps/web/src/components/editor/timeline.tsx @@ -35,6 +35,7 @@ import { SelectTrigger, SelectValue, } from "../ui/select"; +import AudioWaveform from "./audio-waveform"; export function Timeline() { // Timeline shows all tracks (video, audio, effects) and their clips. @@ -1626,7 +1627,21 @@ function TimelineTrackContent({ ); } - // Fallback for audio or videos without thumbnails + if (mediaItem.type === "audio") { + return ( +
+
+ +
+
+ ); + } + + // Fallback for videos without thumbnails return ( {clip.name} ); diff --git a/bun.lock b/bun.lock index 98bf573..0ed9895 100644 --- a/bun.lock +++ b/bun.lock @@ -4,6 +4,7 @@ "": { "dependencies": { "next": "^15.3.4", + "wavesurfer.js": "^7.9.8", }, "devDependencies": { "turbo": "^2.5.4", @@ -902,6 +903,8 @@ "victory-vendor": ["victory-vendor@36.9.2", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ=="], + "wavesurfer.js": ["wavesurfer.js@7.9.8", "", {}, "sha512-Mxz6qRwkSmuWVxLzp0XQ6EzSv1FTvQgMEUJTirLN1Ox76sn0YeyQlI99WuE+B0IuxShPHXIhvEuoBSJdaQs7tA=="], + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], "wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], diff --git a/package.json b/package.json index 61a80f5..28daa58 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "format": "turbo run format" }, "dependencies": { - "next": "^15.3.4" + "next": "^15.3.4", + "wavesurfer.js": "^7.9.8" } }