Skip to content

Commit 3cef2eb

Browse files
authored
Merge pull request #165 from MartinBarker/waveform-test
waveform-test
2 parents a43b5c5 + 2e34b13 commit 3cef2eb

File tree

8 files changed

+1906
-22
lines changed

8 files changed

+1906
-22
lines changed

app/(main)/auto-split/auto-split.module.css

Lines changed: 502 additions & 0 deletions
Large diffs are not rendered by default.

app/(main)/auto-split/page.js

Lines changed: 741 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 308 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,308 @@
1+
'use client'
2+
import React, { useState, useRef, useEffect, useContext } from 'react';
3+
import styles from './waveform-visualizer.module.css';
4+
import { ColorContext } from '../ColorContext';
5+
import FileDrop from '../FileDrop/FileDrop';
6+
import { Play, Pause, Volume2, Download } from 'lucide-react';
7+
8+
export default function WaveformVisualizer() {
9+
const { colors } = useContext(ColorContext);
10+
const [audioFile, setAudioFile] = useState(null);
11+
const [audioUrl, setAudioUrl] = useState(null);
12+
const [isPlaying, setIsPlaying] = useState(false);
13+
const [currentTime, setCurrentTime] = useState(0);
14+
const [duration, setDuration] = useState(0);
15+
const [volume, setVolume] = useState(1);
16+
const [waveformData, setWaveformData] = useState(null);
17+
const [isLoading, setIsLoading] = useState(false);
18+
const [error, setError] = useState(null);
19+
20+
const audioRef = useRef(null);
21+
const canvasRef = useRef(null);
22+
const animationRef = useRef(null);
23+
24+
// Handle file selection
25+
const handleFilesSelected = (files) => {
26+
const file = files[0];
27+
if (file && file.type.startsWith('audio/')) {
28+
setAudioFile(file);
29+
setError(null);
30+
const url = URL.createObjectURL(file);
31+
setAudioUrl(url);
32+
loadWaveformData(file);
33+
} else {
34+
setError('Please select a valid audio file (mp3, wav, etc.)');
35+
}
36+
};
37+
38+
// Load waveform data using the waveform-data library
39+
const loadWaveformData = async (file) => {
40+
setIsLoading(true);
41+
try {
42+
const arrayBuffer = await file.arrayBuffer();
43+
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
44+
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
45+
46+
// Generate waveform data
47+
const channelData = audioBuffer.getChannelData(0);
48+
const samplesPerPixel = Math.floor(channelData.length / 1000); // 1000 pixels width
49+
const waveform = [];
50+
51+
for (let i = 0; i < 1000; i++) {
52+
const start = i * samplesPerPixel;
53+
const end = Math.min(start + samplesPerPixel, channelData.length);
54+
let sum = 0;
55+
let max = 0;
56+
57+
for (let j = start; j < end; j++) {
58+
const sample = Math.abs(channelData[j]);
59+
sum += sample;
60+
max = Math.max(max, sample);
61+
}
62+
63+
waveform.push({
64+
min: -max,
65+
max: max,
66+
avg: sum / (end - start)
67+
});
68+
}
69+
70+
setWaveformData(waveform);
71+
setDuration(audioBuffer.duration);
72+
} catch (err) {
73+
console.error('Error loading waveform data:', err);
74+
setError('Error loading audio file. Please try a different file.');
75+
} finally {
76+
setIsLoading(false);
77+
}
78+
};
79+
80+
// Draw waveform on canvas
81+
const drawWaveform = () => {
82+
const canvas = canvasRef.current;
83+
if (!canvas || !waveformData) return;
84+
85+
const ctx = canvas.getContext('2d');
86+
const width = canvas.width;
87+
const height = canvas.height;
88+
const centerY = height / 2;
89+
90+
// Clear canvas
91+
ctx.fillStyle = colors.LightMuted || '#f0f0f0';
92+
ctx.fillRect(0, 0, width, height);
93+
94+
// Draw waveform
95+
ctx.strokeStyle = colors.DarkVibrant || '#333';
96+
ctx.lineWidth = 1;
97+
ctx.beginPath();
98+
99+
waveformData.forEach((point, index) => {
100+
const x = (index / waveformData.length) * width;
101+
const y1 = centerY + (point.min * centerY);
102+
const y2 = centerY + (point.max * centerY);
103+
104+
if (index === 0) {
105+
ctx.moveTo(x, y1);
106+
} else {
107+
ctx.lineTo(x, y1);
108+
}
109+
});
110+
111+
// Draw the bottom half
112+
for (let i = waveformData.length - 1; i >= 0; i--) {
113+
const x = (i / waveformData.length) * width;
114+
const y = centerY + (waveformData[i].max * centerY);
115+
ctx.lineTo(x, y);
116+
}
117+
118+
ctx.closePath();
119+
ctx.fillStyle = colors.Vibrant || '#007bff';
120+
ctx.fill();
121+
ctx.stroke();
122+
123+
// Draw progress indicator
124+
if (duration > 0) {
125+
const progress = currentTime / duration;
126+
const progressX = progress * width;
127+
128+
ctx.fillStyle = colors.DarkMuted || '#666';
129+
ctx.fillRect(0, 0, progressX, height);
130+
}
131+
};
132+
133+
// Audio event handlers
134+
useEffect(() => {
135+
const audio = audioRef.current;
136+
if (!audio) return;
137+
138+
const handleTimeUpdate = () => setCurrentTime(audio.currentTime);
139+
const handleLoadedMetadata = () => setDuration(audio.duration);
140+
const handleEnded = () => setIsPlaying(false);
141+
142+
audio.addEventListener('timeupdate', handleTimeUpdate);
143+
audio.addEventListener('loadedmetadata', handleLoadedMetadata);
144+
audio.addEventListener('ended', handleEnded);
145+
146+
return () => {
147+
audio.removeEventListener('timeupdate', handleTimeUpdate);
148+
audio.removeEventListener('loadedmetadata', handleLoadedMetadata);
149+
audio.removeEventListener('ended', handleEnded);
150+
};
151+
}, [audioUrl]);
152+
153+
// Draw waveform when data changes
154+
useEffect(() => {
155+
drawWaveform();
156+
}, [waveformData, currentTime, colors]);
157+
158+
// Animation loop
159+
useEffect(() => {
160+
const animate = () => {
161+
drawWaveform();
162+
animationRef.current = requestAnimationFrame(animate);
163+
};
164+
animationRef.current = requestAnimationFrame(animate);
165+
166+
return () => {
167+
if (animationRef.current) {
168+
cancelAnimationFrame(animationRef.current);
169+
}
170+
};
171+
}, [waveformData, currentTime, colors]);
172+
173+
// Control functions
174+
const togglePlayPause = () => {
175+
const audio = audioRef.current;
176+
if (!audio) return;
177+
178+
if (isPlaying) {
179+
audio.pause();
180+
} else {
181+
audio.play();
182+
}
183+
setIsPlaying(!isPlaying);
184+
};
185+
186+
const handleSeek = (e) => {
187+
const audio = audioRef.current;
188+
if (!audio || !duration) return;
189+
190+
const rect = e.currentTarget.getBoundingClientRect();
191+
const x = e.clientX - rect.left;
192+
const percentage = x / rect.width;
193+
const newTime = percentage * duration;
194+
195+
audio.currentTime = newTime;
196+
setCurrentTime(newTime);
197+
};
198+
199+
const handleVolumeChange = (e) => {
200+
const newVolume = parseFloat(e.target.value);
201+
setVolume(newVolume);
202+
if (audioRef.current) {
203+
audioRef.current.volume = newVolume;
204+
}
205+
};
206+
207+
const formatTime = (time) => {
208+
const minutes = Math.floor(time / 60);
209+
const seconds = Math.floor(time % 60);
210+
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
211+
};
212+
213+
return (
214+
<div className={styles.container}>
215+
<div className={styles.header}>
216+
<h1 className={styles.title}>Waveform Visualizer</h1>
217+
<p className={styles.subtitle}>Drop an audio file to visualize its waveform</p>
218+
</div>
219+
220+
{!audioFile ? (
221+
<div className={styles.uploadSection}>
222+
<FileDrop onFilesSelected={handleFilesSelected} />
223+
{error && <div className={styles.error}>{error}</div>}
224+
</div>
225+
) : (
226+
<div className={styles.playerSection}>
227+
{isLoading && (
228+
<div className={styles.loading}>
229+
<div className={styles.spinner}></div>
230+
<p>Loading waveform...</p>
231+
</div>
232+
)}
233+
234+
{waveformData && (
235+
<div className={styles.waveformContainer}>
236+
<canvas
237+
ref={canvasRef}
238+
className={styles.waveformCanvas}
239+
width={800}
240+
height={200}
241+
onClick={handleSeek}
242+
/>
243+
</div>
244+
)}
245+
246+
<div className={styles.controls}>
247+
<button
248+
className={styles.playButton}
249+
onClick={togglePlayPause}
250+
disabled={!audioUrl}
251+
>
252+
{isPlaying ? <Pause size={24} /> : <Play size={24} />}
253+
</button>
254+
255+
<div className={styles.timeDisplay}>
256+
<span>{formatTime(currentTime)}</span>
257+
<span>/</span>
258+
<span>{formatTime(duration)}</span>
259+
</div>
260+
261+
<div className={styles.volumeControl}>
262+
<Volume2 size={20} />
263+
<input
264+
type="range"
265+
min="0"
266+
max="1"
267+
step="0.1"
268+
value={volume}
269+
onChange={handleVolumeChange}
270+
className={styles.volumeSlider}
271+
/>
272+
</div>
273+
</div>
274+
275+
<div className={styles.fileInfo}>
276+
<h3>Current File: {audioFile.name}</h3>
277+
<p>Duration: {formatTime(duration)}</p>
278+
<p>Size: {(audioFile.size / 1024 / 1024).toFixed(2)} MB</p>
279+
</div>
280+
281+
<button
282+
className={styles.resetButton}
283+
onClick={() => {
284+
setAudioFile(null);
285+
setAudioUrl(null);
286+
setWaveformData(null);
287+
setCurrentTime(0);
288+
setDuration(0);
289+
setIsPlaying(false);
290+
setError(null);
291+
}}
292+
>
293+
Load Different File
294+
</button>
295+
</div>
296+
)}
297+
298+
{audioUrl && (
299+
<audio
300+
ref={audioRef}
301+
src={audioUrl}
302+
preload="metadata"
303+
style={{ display: 'none' }}
304+
/>
305+
)}
306+
</div>
307+
);
308+
}

0 commit comments

Comments
 (0)