2021-09-11 10:05:58 +00:00
|
|
|
import { useEffect, useState, useRef, MouseEvent } from 'react';
|
2021-09-11 16:42:37 +00:00
|
|
|
import { WaveformCanvas } from './WaveformCanvas';
|
2021-09-06 14:25:23 +00:00
|
|
|
|
2021-09-11 16:42:37 +00:00
|
|
|
type Props = {
|
2021-09-06 14:25:23 +00:00
|
|
|
audioContext: AudioContext;
|
|
|
|
};
|
|
|
|
|
2021-09-11 09:00:27 +00:00
|
|
|
type AudioFile = {
|
|
|
|
bytes: number;
|
|
|
|
channels: number;
|
|
|
|
frames: number;
|
|
|
|
sampleRate: number;
|
|
|
|
};
|
|
|
|
|
2021-09-11 10:58:43 +00:00
|
|
|
type ZoomSettings = {
|
|
|
|
startFrame: number;
|
|
|
|
endFrame: number;
|
|
|
|
};
|
|
|
|
|
|
|
|
const defaultZoomSettings: ZoomSettings = { startFrame: 0, endFrame: 0 };
|
|
|
|
|
2021-09-11 16:42:37 +00:00
|
|
|
export const Waveform: React.FC<Props> = ({ audioContext }: Props) => {
|
2021-09-11 09:00:27 +00:00
|
|
|
const [audioFile, setAudioFile] = useState<AudioFile | null>(null);
|
|
|
|
const [currentTime, setCurrentTime] = useState(0);
|
|
|
|
const [audio, setAudio] = useState(new Audio());
|
2021-09-11 10:58:43 +00:00
|
|
|
const [zoomSettings, setZoomSettings] = useState(defaultZoomSettings);
|
2021-09-11 16:42:37 +00:00
|
|
|
const [waveformPeaks, setWaveformPeaks] = useState(null);
|
|
|
|
const [overviewPeaks, setOverviewPeaks] = useState(null);
|
2021-09-11 09:00:27 +00:00
|
|
|
|
|
|
|
const hudCanvasRef = useRef<HTMLCanvasElement>(null);
|
|
|
|
const canvasLogicalWidth = 2000;
|
|
|
|
const canvasLogicalHeight = 500;
|
2021-09-11 16:42:37 +00:00
|
|
|
|
2021-09-11 10:05:58 +00:00
|
|
|
const videoID = new URLSearchParams(window.location.search).get('video_id');
|
2021-09-11 09:00:27 +00:00
|
|
|
|
|
|
|
// helpers
|
|
|
|
|
|
|
|
const mouseEventToCanvasX = (evt: MouseEvent<HTMLCanvasElement>): number => {
|
|
|
|
// TODO: use offsetX/offsetY?
|
|
|
|
const rect = evt.currentTarget.getBoundingClientRect();
|
|
|
|
const elementX = evt.clientX - rect.left;
|
|
|
|
const canvas = evt.target as HTMLCanvasElement;
|
2021-09-11 10:05:58 +00:00
|
|
|
return (elementX * canvas.width) / rect.width;
|
2021-09-11 09:00:27 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
const canvasXToFrame = (x: number): number => {
|
|
|
|
if (audioFile == null) {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
return Math.floor((x / canvasLogicalWidth) * audioFile.frames);
|
2021-09-11 10:05:58 +00:00
|
|
|
};
|
2021-09-11 09:00:27 +00:00
|
|
|
|
|
|
|
const secsToCanvasX = (canvasWidth: number, secs: number): number => {
|
|
|
|
if (audioFile == null) {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
const duration = audioFile.frames / audioFile.sampleRate;
|
|
|
|
return Math.floor(canvasWidth * (secs / duration));
|
|
|
|
};
|
|
|
|
|
|
|
|
// effects
|
2021-09-11 10:05:58 +00:00
|
|
|
|
2021-09-11 09:00:27 +00:00
|
|
|
// setup player on page load:
|
|
|
|
useEffect(() => {
|
2021-09-11 10:05:58 +00:00
|
|
|
(async function () {
|
|
|
|
audio.addEventListener('timeupdate', () => {
|
|
|
|
setCurrentTime(audio.currentTime);
|
|
|
|
});
|
|
|
|
})();
|
2021-09-11 09:00:27 +00:00
|
|
|
}, [audio]);
|
2021-09-06 14:25:23 +00:00
|
|
|
|
2021-09-11 16:42:37 +00:00
|
|
|
// fetch audio data on page load:
|
2021-09-06 14:25:23 +00:00
|
|
|
useEffect(() => {
|
2021-09-11 10:05:58 +00:00
|
|
|
(async function () {
|
|
|
|
console.log('fetching audio data...');
|
2021-09-06 14:25:23 +00:00
|
|
|
|
2021-09-11 10:05:58 +00:00
|
|
|
const resp = await fetch(
|
|
|
|
`http://localhost:8888/api/download?video_id=${videoID}`
|
|
|
|
);
|
2021-09-11 09:00:27 +00:00
|
|
|
const respBody = await resp.json();
|
2021-09-06 14:25:23 +00:00
|
|
|
|
2021-09-11 09:00:27 +00:00
|
|
|
if (respBody.error) {
|
2021-09-11 10:05:58 +00:00
|
|
|
console.log('error fetching audio data:', respBody.error);
|
2021-09-11 09:00:27 +00:00
|
|
|
return;
|
|
|
|
}
|
2021-09-06 14:25:23 +00:00
|
|
|
|
2021-09-11 09:00:27 +00:00
|
|
|
// TODO: safer deserialization?
|
|
|
|
const audioFile: AudioFile = {
|
|
|
|
bytes: respBody.bytes,
|
|
|
|
channels: respBody.channels,
|
|
|
|
frames: respBody.frames,
|
|
|
|
sampleRate: respBody.sample_rate,
|
|
|
|
};
|
2021-09-06 14:25:23 +00:00
|
|
|
|
2021-09-11 09:00:27 +00:00
|
|
|
setAudioFile(audioFile);
|
2021-09-11 10:58:43 +00:00
|
|
|
setZoomSettings({ startFrame: 0, endFrame: audioFile.frames });
|
2021-09-06 14:25:23 +00:00
|
|
|
})();
|
|
|
|
}, [audioContext]);
|
|
|
|
|
2021-09-11 16:42:37 +00:00
|
|
|
// render overview waveform to canvas when the audio file is loaded:
|
|
|
|
|
|
|
|
// fetch new waveform peaks when zoom settings are updated:
|
2021-09-06 14:25:23 +00:00
|
|
|
useEffect(() => {
|
2021-09-11 10:05:58 +00:00
|
|
|
(async function () {
|
|
|
|
if (audioFile == null) {
|
|
|
|
return;
|
|
|
|
}
|
2021-09-11 09:00:27 +00:00
|
|
|
|
2021-09-11 10:58:43 +00:00
|
|
|
let endFrame = zoomSettings.endFrame;
|
|
|
|
if (endFrame <= zoomSettings.startFrame) {
|
|
|
|
endFrame = audioFile.frames;
|
|
|
|
}
|
2021-09-11 10:05:58 +00:00
|
|
|
|
|
|
|
const resp = await fetch(
|
2021-09-11 16:42:37 +00:00
|
|
|
`http://localhost:8888/api/peaks?video_id=${videoID}&start=${zoomSettings.startFrame}&end=${endFrame}&bins=${canvasLogicalWidth}`
|
2021-09-11 10:05:58 +00:00
|
|
|
);
|
|
|
|
const peaks = await resp.json();
|
|
|
|
console.log('respBody from peaks =', peaks);
|
|
|
|
|
2021-09-11 16:42:37 +00:00
|
|
|
setWaveformPeaks(peaks);
|
|
|
|
|
|
|
|
if (overviewPeaks == null) {
|
|
|
|
setOverviewPeaks(peaks);
|
2021-09-11 10:05:58 +00:00
|
|
|
}
|
2021-09-11 09:00:27 +00:00
|
|
|
})();
|
2021-09-11 10:58:43 +00:00
|
|
|
}, [zoomSettings]);
|
2021-09-11 09:00:27 +00:00
|
|
|
|
|
|
|
// redraw HUD
|
|
|
|
useEffect(() => {
|
2021-09-11 10:05:58 +00:00
|
|
|
(async function () {
|
2021-09-11 09:00:27 +00:00
|
|
|
const canvas = hudCanvasRef.current;
|
|
|
|
if (canvas == null) {
|
2021-09-11 10:05:58 +00:00
|
|
|
console.error('no hud canvas ref available');
|
2021-09-11 09:00:27 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2021-09-11 10:05:58 +00:00
|
|
|
const ctx = canvas.getContext('2d');
|
2021-09-11 09:00:27 +00:00
|
|
|
if (ctx == null) {
|
2021-09-11 10:05:58 +00:00
|
|
|
console.error('no hud 2d context available');
|
2021-09-11 09:00:27 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
|
|
|
2021-09-11 10:58:43 +00:00
|
|
|
if (audioFile == null) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2021-09-11 09:00:27 +00:00
|
|
|
const x = secsToCanvasX(canvas.width, currentTime);
|
2021-09-06 14:25:23 +00:00
|
|
|
|
2021-09-11 10:05:58 +00:00
|
|
|
ctx.strokeStyle = 'red';
|
2021-09-11 09:00:27 +00:00
|
|
|
ctx.beginPath();
|
|
|
|
ctx.moveTo(x, 0);
|
|
|
|
ctx.lineTo(x, canvas.height);
|
|
|
|
ctx.stroke();
|
2021-09-11 10:05:58 +00:00
|
|
|
})();
|
2021-09-11 09:00:27 +00:00
|
|
|
}, [currentTime]);
|
|
|
|
|
|
|
|
// callbacks
|
2021-09-11 10:05:58 +00:00
|
|
|
|
2021-09-11 09:00:27 +00:00
|
|
|
const handleMouseMove = (evt: MouseEvent<HTMLCanvasElement>) => {
|
|
|
|
const canvasX = mouseEventToCanvasX(evt);
|
2021-09-11 10:05:58 +00:00
|
|
|
console.log('mousemove, x =', canvasX, 'frame =', canvasXToFrame(canvasX));
|
|
|
|
};
|
2021-09-11 09:00:27 +00:00
|
|
|
|
2021-09-11 10:58:43 +00:00
|
|
|
const handleMouseDown = () => {
|
|
|
|
return null;
|
2021-09-11 09:00:27 +00:00
|
|
|
};
|
2021-09-06 14:25:23 +00:00
|
|
|
|
2021-09-11 09:00:27 +00:00
|
|
|
const handleMouseUp = () => {
|
|
|
|
return null;
|
|
|
|
};
|
|
|
|
|
|
|
|
const handlePlay = async () => {
|
|
|
|
const url = `http://localhost:8888/api/audio?video_id=${videoID}`;
|
|
|
|
audio.src = url;
|
|
|
|
await audio.play();
|
2021-09-11 10:05:58 +00:00
|
|
|
console.log('playing audio from', url);
|
|
|
|
};
|
2021-09-06 14:25:23 +00:00
|
|
|
|
2021-09-11 09:00:27 +00:00
|
|
|
const handlePause = () => {
|
2021-09-11 10:05:58 +00:00
|
|
|
audio.pause();
|
|
|
|
console.log('paused audio');
|
|
|
|
};
|
2021-09-11 09:00:27 +00:00
|
|
|
|
|
|
|
const handleZoomIn = () => {
|
2021-09-11 10:58:43 +00:00
|
|
|
if (audioFile == null) {
|
|
|
|
return;
|
|
|
|
}
|
2021-09-11 10:05:58 +00:00
|
|
|
console.log('zoom in');
|
2021-09-11 10:58:43 +00:00
|
|
|
const diff = zoomSettings.endFrame - zoomSettings.startFrame;
|
|
|
|
const endFrame = zoomSettings.startFrame + Math.floor(diff / 2);
|
|
|
|
const settings = { ...zoomSettings, endFrame: endFrame };
|
|
|
|
setZoomSettings(settings);
|
2021-09-11 09:00:27 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
const handleZoomOut = () => {
|
2021-09-11 10:58:43 +00:00
|
|
|
if (audioFile == null) {
|
|
|
|
return;
|
|
|
|
}
|
2021-09-11 10:05:58 +00:00
|
|
|
console.log('zoom out');
|
2021-09-11 10:58:43 +00:00
|
|
|
const diff = zoomSettings.endFrame - zoomSettings.startFrame;
|
|
|
|
const newDiff = diff * 2;
|
|
|
|
const endFrame = Math.min(
|
|
|
|
zoomSettings.endFrame + newDiff,
|
|
|
|
audioFile.frames
|
|
|
|
);
|
|
|
|
const settings = { ...zoomSettings, endFrame: endFrame };
|
|
|
|
setZoomSettings(settings);
|
2021-09-11 09:00:27 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
// render component:
|
2021-09-06 14:25:23 +00:00
|
|
|
|
2021-09-11 10:05:58 +00:00
|
|
|
const wrapperProps = {
|
|
|
|
width: '90%',
|
2021-09-11 16:42:37 +00:00
|
|
|
height: '350px',
|
2021-09-11 10:05:58 +00:00
|
|
|
position: 'relative',
|
|
|
|
margin: '0 auto',
|
|
|
|
} as React.CSSProperties;
|
2021-09-11 10:58:43 +00:00
|
|
|
|
2021-09-11 10:05:58 +00:00
|
|
|
const waveformCanvasProps = {
|
|
|
|
width: '100%',
|
2021-09-11 16:42:37 +00:00
|
|
|
height: '100%',
|
2021-09-11 10:05:58 +00:00
|
|
|
position: 'absolute',
|
|
|
|
top: 0,
|
|
|
|
left: 0,
|
|
|
|
right: 0,
|
|
|
|
bottom: 0,
|
|
|
|
zIndex: 0,
|
|
|
|
} as React.CSSProperties;
|
2021-09-11 10:58:43 +00:00
|
|
|
|
2021-09-11 10:05:58 +00:00
|
|
|
const hudCanvasProps = {
|
|
|
|
width: '100%',
|
2021-09-11 16:42:37 +00:00
|
|
|
height: '100%',
|
2021-09-11 10:05:58 +00:00
|
|
|
position: 'absolute',
|
|
|
|
top: 0,
|
|
|
|
left: 0,
|
|
|
|
right: 0,
|
|
|
|
bottom: 0,
|
|
|
|
zIndex: 1,
|
|
|
|
} as React.CSSProperties;
|
2021-09-11 10:58:43 +00:00
|
|
|
|
2021-09-11 16:42:37 +00:00
|
|
|
const overviewCanvasProps = {
|
|
|
|
width: '90%',
|
|
|
|
height: '90px',
|
|
|
|
margin: '0 auto',
|
|
|
|
display: 'block',
|
|
|
|
} as React.CSSProperties;
|
|
|
|
|
2021-09-11 10:05:58 +00:00
|
|
|
const clockTextAreaProps = { color: '#999', width: '400px' };
|
|
|
|
|
|
|
|
return (
|
|
|
|
<>
|
|
|
|
<h1>clipper</h1>
|
|
|
|
<div style={wrapperProps}>
|
2021-09-11 16:42:37 +00:00
|
|
|
<WaveformCanvas
|
|
|
|
peaks={waveformPeaks}
|
|
|
|
logicalWidth={canvasLogicalWidth}
|
|
|
|
logicalHeight={canvasLogicalHeight}
|
|
|
|
fillStyle="black"
|
|
|
|
strokeStyle="green"
|
2021-09-11 10:05:58 +00:00
|
|
|
style={waveformCanvasProps}
|
2021-09-11 16:42:37 +00:00
|
|
|
></WaveformCanvas>
|
2021-09-11 10:05:58 +00:00
|
|
|
<canvas
|
|
|
|
ref={hudCanvasRef}
|
|
|
|
width={canvasLogicalWidth}
|
|
|
|
height={canvasLogicalHeight}
|
|
|
|
onMouseMove={handleMouseMove}
|
|
|
|
onMouseDown={handleMouseDown}
|
|
|
|
onMouseUp={handleMouseUp}
|
|
|
|
style={hudCanvasProps}
|
|
|
|
></canvas>
|
|
|
|
</div>
|
2021-09-11 16:42:37 +00:00
|
|
|
<WaveformCanvas
|
|
|
|
peaks={overviewPeaks}
|
|
|
|
logicalWidth={canvasLogicalWidth}
|
|
|
|
logicalHeight={canvasLogicalHeight}
|
|
|
|
fillStyle="grey"
|
|
|
|
strokeStyle="black"
|
|
|
|
style={overviewCanvasProps}
|
|
|
|
></WaveformCanvas>
|
2021-09-11 10:05:58 +00:00
|
|
|
<button onClick={handlePlay}>Play</button>
|
|
|
|
<button onClick={handlePause}>Pause</button>
|
|
|
|
<button onClick={handleZoomIn}>+</button>
|
|
|
|
<button onClick={handleZoomOut}>-</button>
|
|
|
|
<input type="readonly" style={clockTextAreaProps} />
|
|
|
|
</>
|
|
|
|
);
|
|
|
|
};
|