Add overview waveform
This commit is contained in:
parent
faf818e4ae
commit
5a08bd62bf
|
@ -1,6 +1,7 @@
|
||||||
import { useEffect, useState, useRef, MouseEvent } from 'react';
|
import { useEffect, useState, useRef, MouseEvent } from 'react';
|
||||||
|
import { WaveformCanvas } from './WaveformCanvas';
|
||||||
|
|
||||||
type WaveformProps = {
|
type Props = {
|
||||||
audioContext: AudioContext;
|
audioContext: AudioContext;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -18,18 +19,18 @@ type ZoomSettings = {
|
||||||
|
|
||||||
const defaultZoomSettings: ZoomSettings = { startFrame: 0, endFrame: 0 };
|
const defaultZoomSettings: ZoomSettings = { startFrame: 0, endFrame: 0 };
|
||||||
|
|
||||||
export const Waveform: React.FC<WaveformProps> = ({
|
export const Waveform: React.FC<Props> = ({ audioContext }: Props) => {
|
||||||
audioContext,
|
|
||||||
}: WaveformProps) => {
|
|
||||||
const [audioFile, setAudioFile] = useState<AudioFile | null>(null);
|
const [audioFile, setAudioFile] = useState<AudioFile | null>(null);
|
||||||
const [currentTime, setCurrentTime] = useState(0);
|
const [currentTime, setCurrentTime] = useState(0);
|
||||||
const [audio, setAudio] = useState(new Audio());
|
const [audio, setAudio] = useState(new Audio());
|
||||||
const [zoomSettings, setZoomSettings] = useState(defaultZoomSettings);
|
const [zoomSettings, setZoomSettings] = useState(defaultZoomSettings);
|
||||||
|
const [waveformPeaks, setWaveformPeaks] = useState(null);
|
||||||
|
const [overviewPeaks, setOverviewPeaks] = useState(null);
|
||||||
|
|
||||||
const waveformCanvasRef = useRef<HTMLCanvasElement>(null);
|
|
||||||
const hudCanvasRef = useRef<HTMLCanvasElement>(null);
|
const hudCanvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
const canvasLogicalWidth = 2000;
|
const canvasLogicalWidth = 2000;
|
||||||
const canvasLogicalHeight = 500;
|
const canvasLogicalHeight = 500;
|
||||||
|
|
||||||
const videoID = new URLSearchParams(window.location.search).get('video_id');
|
const videoID = new URLSearchParams(window.location.search).get('video_id');
|
||||||
|
|
||||||
// helpers
|
// helpers
|
||||||
|
@ -49,14 +50,6 @@ export const Waveform: React.FC<WaveformProps> = ({
|
||||||
return Math.floor((x / canvasLogicalWidth) * audioFile.frames);
|
return Math.floor((x / canvasLogicalWidth) * audioFile.frames);
|
||||||
};
|
};
|
||||||
|
|
||||||
// const canvasXToSecs = (x: number): number => {
|
|
||||||
// if (audioFile == null) {
|
|
||||||
// return 0;
|
|
||||||
// }
|
|
||||||
// const duration = audioFile.frames / audioFile.sampleRate;
|
|
||||||
// return (canvasXToFrame(x) / audioFile.frames) * duration;
|
|
||||||
// };
|
|
||||||
|
|
||||||
const secsToCanvasX = (canvasWidth: number, secs: number): number => {
|
const secsToCanvasX = (canvasWidth: number, secs: number): number => {
|
||||||
if (audioFile == null) {
|
if (audioFile == null) {
|
||||||
return 0;
|
return 0;
|
||||||
|
@ -76,7 +69,7 @@ export const Waveform: React.FC<WaveformProps> = ({
|
||||||
})();
|
})();
|
||||||
}, [audio]);
|
}, [audio]);
|
||||||
|
|
||||||
// load audio data on page load:
|
// fetch audio data on page load:
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async function () {
|
(async function () {
|
||||||
console.log('fetching audio data...');
|
console.log('fetching audio data...');
|
||||||
|
@ -104,56 +97,30 @@ export const Waveform: React.FC<WaveformProps> = ({
|
||||||
})();
|
})();
|
||||||
}, [audioContext]);
|
}, [audioContext]);
|
||||||
|
|
||||||
// render waveform to canvas when zoom settings are updated:
|
// render overview waveform to canvas when the audio file is loaded:
|
||||||
|
|
||||||
|
// fetch new waveform peaks when zoom settings are updated:
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async function () {
|
(async function () {
|
||||||
if (audioFile == null) {
|
if (audioFile == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('audiofile is', audioFile);
|
|
||||||
|
|
||||||
const canvas = waveformCanvasRef.current;
|
|
||||||
if (canvas == null) {
|
|
||||||
console.error('no canvas ref available');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ctx = canvas.getContext('2d');
|
|
||||||
if (ctx == null) {
|
|
||||||
console.error('no 2d context available');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let endFrame = zoomSettings.endFrame;
|
let endFrame = zoomSettings.endFrame;
|
||||||
if (endFrame <= zoomSettings.startFrame) {
|
if (endFrame <= zoomSettings.startFrame) {
|
||||||
endFrame = audioFile.frames;
|
endFrame = audioFile.frames;
|
||||||
}
|
}
|
||||||
|
|
||||||
const resp = await fetch(
|
const resp = await fetch(
|
||||||
`http://localhost:8888/api/peaks?video_id=${videoID}&start=${zoomSettings.startFrame}&end=${endFrame}&bins=${canvas.width}`
|
`http://localhost:8888/api/peaks?video_id=${videoID}&start=${zoomSettings.startFrame}&end=${endFrame}&bins=${canvasLogicalWidth}`
|
||||||
);
|
);
|
||||||
const peaks = await resp.json();
|
const peaks = await resp.json();
|
||||||
console.log('respBody from peaks =', peaks);
|
console.log('respBody from peaks =', peaks);
|
||||||
|
|
||||||
ctx.strokeStyle = '#00aa00';
|
setWaveformPeaks(peaks);
|
||||||
ctx.fillStyle = 'black';
|
|
||||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
||||||
|
|
||||||
const numChannels = peaks.length;
|
if (overviewPeaks == null) {
|
||||||
const chanHeight = canvas.height / numChannels;
|
setOverviewPeaks(peaks);
|
||||||
for (let c = 0; c < numChannels; c++) {
|
|
||||||
const yOffset = chanHeight * c;
|
|
||||||
for (let i = 0; i < peaks[c].length; i++) {
|
|
||||||
const val = peaks[c][i];
|
|
||||||
const height = Math.floor((val / 32768) * chanHeight);
|
|
||||||
const y1 = (chanHeight - height) / 2 + yOffset;
|
|
||||||
const y2 = y1 + height;
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(i, y1);
|
|
||||||
ctx.lineTo(i, y2);
|
|
||||||
ctx.stroke();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}, [zoomSettings]);
|
}, [zoomSettings]);
|
||||||
|
@ -246,13 +213,14 @@ export const Waveform: React.FC<WaveformProps> = ({
|
||||||
|
|
||||||
const wrapperProps = {
|
const wrapperProps = {
|
||||||
width: '90%',
|
width: '90%',
|
||||||
height: '500px',
|
height: '350px',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
margin: '0 auto',
|
margin: '0 auto',
|
||||||
} as React.CSSProperties;
|
} as React.CSSProperties;
|
||||||
|
|
||||||
const waveformCanvasProps = {
|
const waveformCanvasProps = {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
|
@ -263,6 +231,7 @@ export const Waveform: React.FC<WaveformProps> = ({
|
||||||
|
|
||||||
const hudCanvasProps = {
|
const hudCanvasProps = {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
|
@ -271,18 +240,27 @@ export const Waveform: React.FC<WaveformProps> = ({
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
} as React.CSSProperties;
|
} as React.CSSProperties;
|
||||||
|
|
||||||
|
const overviewCanvasProps = {
|
||||||
|
width: '90%',
|
||||||
|
height: '90px',
|
||||||
|
margin: '0 auto',
|
||||||
|
display: 'block',
|
||||||
|
} as React.CSSProperties;
|
||||||
|
|
||||||
const clockTextAreaProps = { color: '#999', width: '400px' };
|
const clockTextAreaProps = { color: '#999', width: '400px' };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h1>clipper</h1>
|
<h1>clipper</h1>
|
||||||
<div style={wrapperProps}>
|
<div style={wrapperProps}>
|
||||||
<canvas
|
<WaveformCanvas
|
||||||
ref={waveformCanvasRef}
|
peaks={waveformPeaks}
|
||||||
width={canvasLogicalWidth}
|
logicalWidth={canvasLogicalWidth}
|
||||||
height={canvasLogicalHeight}
|
logicalHeight={canvasLogicalHeight}
|
||||||
|
fillStyle="black"
|
||||||
|
strokeStyle="green"
|
||||||
style={waveformCanvasProps}
|
style={waveformCanvasProps}
|
||||||
></canvas>
|
></WaveformCanvas>
|
||||||
<canvas
|
<canvas
|
||||||
ref={hudCanvasRef}
|
ref={hudCanvasRef}
|
||||||
width={canvasLogicalWidth}
|
width={canvasLogicalWidth}
|
||||||
|
@ -293,6 +271,14 @@ export const Waveform: React.FC<WaveformProps> = ({
|
||||||
style={hudCanvasProps}
|
style={hudCanvasProps}
|
||||||
></canvas>
|
></canvas>
|
||||||
</div>
|
</div>
|
||||||
|
<WaveformCanvas
|
||||||
|
peaks={overviewPeaks}
|
||||||
|
logicalWidth={canvasLogicalWidth}
|
||||||
|
logicalHeight={canvasLogicalHeight}
|
||||||
|
fillStyle="grey"
|
||||||
|
strokeStyle="black"
|
||||||
|
style={overviewCanvasProps}
|
||||||
|
></WaveformCanvas>
|
||||||
<button onClick={handlePlay}>Play</button>
|
<button onClick={handlePlay}>Play</button>
|
||||||
<button onClick={handlePause}>Pause</button>
|
<button onClick={handlePause}>Pause</button>
|
||||||
<button onClick={handleZoomIn}>+</button>
|
<button onClick={handleZoomIn}>+</button>
|
||||||
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
import { useEffect, useState, useRef, MouseEvent } from 'react';
|
||||||
|
|
||||||
|
const maxPeakValue = 32768;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
peaks: number[][] | null;
|
||||||
|
logicalWidth: number;
|
||||||
|
logicalHeight: number;
|
||||||
|
strokeStyle: string;
|
||||||
|
fillStyle: string;
|
||||||
|
style: React.CSSProperties;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WaveformCanvas: React.FC<Props> = (props: Props) => {
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
if (canvas == null) {
|
||||||
|
console.error('no canvas ref available');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
if (ctx == null) {
|
||||||
|
console.error('no 2d context available');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.strokeStyle = props.strokeStyle;
|
||||||
|
ctx.fillStyle = props.fillStyle;
|
||||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
if (props.peaks == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const numChannels = props.peaks.length;
|
||||||
|
const chanHeight = canvas.height / numChannels;
|
||||||
|
for (let i = 0; i < numChannels; i++) {
|
||||||
|
const yOffset = chanHeight * i;
|
||||||
|
// props.peaks[n].length must equal canvasLogicalWidth:
|
||||||
|
for (let j = 0; j < props.peaks[i].length; j++) {
|
||||||
|
const val = props.peaks[i][j];
|
||||||
|
const height = Math.floor((val / maxPeakValue) * chanHeight);
|
||||||
|
const y1 = (chanHeight - height) / 2 + yOffset;
|
||||||
|
const y2 = y1 + height;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(j, y1);
|
||||||
|
ctx.lineTo(j, y2);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [props.peaks]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<canvas
|
||||||
|
ref={canvasRef}
|
||||||
|
width={props.logicalWidth}
|
||||||
|
height={props.logicalHeight}
|
||||||
|
style={props.style}
|
||||||
|
></canvas>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
Loading…
Reference in New Issue