Files
encoach_frontend/src/components/Waveform.tsx

176 lines
4.7 KiB
TypeScript

import React, { useEffect, useRef, useState } from "react";
import { BsPauseFill, BsPlayFill, BsScissors, BsTrash } from "react-icons/bs";
import WaveSurfer from "wavesurfer.js";
// @ts-ignore
import RegionsPlugin from 'wavesurfer.js/dist/plugin/wavesurfer.regions.min.js';
interface Props {
audio: string;
waveColor: string;
progressColor: string;
variant?: 'exercise' | 'edit';
onCutsChange?: (cuts: AudioCut[]) => void;
}
interface AudioCut {
id: string;
start: number;
end: number;
}
const Waveform = ({
audio,
waveColor,
progressColor,
variant = 'exercise',
onCutsChange
}: Props) => {
const containerRef = useRef(null);
const waveSurferRef = useRef<WaveSurfer | null>(null);
const [isPlaying, setIsPlaying] = useState(false);
const [cuts, setCuts] = useState<AudioCut[]>([]);
const [currentRegion, setCurrentRegion] = useState<any | null>(null);
const [duration, setDuration] = useState<number>(0);
useEffect(() => {
const waveSurfer = WaveSurfer.create({
container: containerRef?.current || "",
responsive: true,
cursorWidth: 0,
height: variant === 'edit' ? 96 : 24,
waveColor,
progressColor,
barGap: 5,
barWidth: 8,
barRadius: 4,
fillParent: true,
hideScrollbar: true,
normalize: true,
autoCenter: true,
ignoreSilenceMode: true,
barMinHeight: 4,
plugins: variant === 'edit' ? [
RegionsPlugin.create({
dragSelection: true,
slop: 5
})
] : []
});
waveSurfer.load(audio);
waveSurfer.on("ready", () => {
waveSurferRef.current = waveSurfer;
setDuration(waveSurfer.getDuration());
});
waveSurfer.on("finish", () => setIsPlaying(false));
if (variant === 'edit') {
waveSurfer.on('region-created', (region) => {
setCurrentRegion(region);
const newCut: AudioCut = {
id: region.id,
start: region.start,
end: region.end
};
setCuts(prev => [...prev, newCut]);
onCutsChange?.([...cuts, newCut]);
});
waveSurfer.on('region-updated', (region) => {
setCuts(prev => prev.map(cut =>
cut.id === region.id
? { ...cut, start: region.start, end: region.end }
: cut
));
onCutsChange?.(cuts.map(cut =>
cut.id === region.id
? { ...cut, start: region.start, end: region.end }
: cut
));
});
}
return () => {
waveSurfer.destroy();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [audio, progressColor, waveColor, variant]);
const handlePlayPause = () => {
setIsPlaying(prev => !prev);
waveSurferRef.current?.playPause();
};
const handleDeleteRegion = (cutId: string) => {
const region = waveSurferRef.current?.regions?.list[cutId];
if (region) {
region.remove();
setCuts(prev => prev.filter(cut => cut.id !== cutId));
onCutsChange?.(cuts.filter(cut => cut.id !== cutId));
}
};
const formatTime = (time: number) => {
const minutes = Math.floor(time / 60);
const seconds = Math.floor(time % 60);
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
};
return (
<div className="space-y-4">
<div className="flex items-center gap-4">
{isPlaying ? (
<BsPauseFill
className="text-mti-gray-cool cursor-pointer w-5 h-5"
onClick={handlePlayPause}
/>
) : (
<BsPlayFill
className="text-mti-gray-cool cursor-pointer w-5 h-5"
onClick={handlePlayPause}
/>
)}
{variant === 'edit' && duration > 0 && (
<div className="text-sm text-gray-500">
Total Duration: {formatTime(duration)}
</div>
)}
</div>
<div className="w-full max-w-4xl h-fit" ref={containerRef} />
{variant === 'edit' && cuts.length > 0 && (
<div className="space-y-2">
<h3 className="font-medium text-gray-700">Audio Cuts</h3>
<div className="space-y-2">
{cuts.map((cut) => (
<div
key={cut.id}
className="flex items-center justify-between p-2 bg-gray-50 rounded-md"
>
<div className="text-sm text-gray-600">
{formatTime(cut.start)} - {formatTime(cut.end)}
</div>
<button
onClick={() => handleDeleteRegion(cut.id)}
className="p-1 text-red-500 hover:bg-red-50 rounded"
>
<BsTrash className="w-4 h-4" />
</button>
</div>
))}
</div>
</div>
)}
</div>
);
};
export default Waveform;