Add overview waveform

This commit is contained in:
Rob Watson 2021-09-11 18:42:37 +02:00
parent faf818e4ae
commit 5a08bd62bf
2 changed files with 105 additions and 53 deletions

View File

@ -1,6 +1,7 @@
import { useEffect, useState, useRef, MouseEvent } from 'react';
import { WaveformCanvas } from './WaveformCanvas';
type WaveformProps = {
type Props = {
audioContext: AudioContext;
};
@ -18,18 +19,18 @@ type ZoomSettings = {
const defaultZoomSettings: ZoomSettings = { startFrame: 0, endFrame: 0 };
export const Waveform: React.FC<WaveformProps> = ({
audioContext,
}: WaveformProps) => {
export const Waveform: React.FC<Props> = ({ audioContext }: Props) => {
const [audioFile, setAudioFile] = useState<AudioFile | null>(null);
const [currentTime, setCurrentTime] = useState(0);
const [audio, setAudio] = useState(new Audio());
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 canvasLogicalWidth = 2000;
const canvasLogicalHeight = 500;
const videoID = new URLSearchParams(window.location.search).get('video_id');
// helpers
@ -49,14 +50,6 @@ export const Waveform: React.FC<WaveformProps> = ({
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 => {
if (audioFile == null) {
return 0;
@ -76,7 +69,7 @@ export const Waveform: React.FC<WaveformProps> = ({
})();
}, [audio]);
// load audio data on page load:
// fetch audio data on page load:
useEffect(() => {
(async function () {
console.log('fetching audio data...');
@ -104,56 +97,30 @@ export const Waveform: React.FC<WaveformProps> = ({
})();
}, [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(() => {
(async function () {
if (audioFile == null) {
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;
if (endFrame <= zoomSettings.startFrame) {
endFrame = audioFile.frames;
}
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();
console.log('respBody from peaks =', peaks);
ctx.strokeStyle = '#00aa00';
ctx.fillStyle = 'black';
ctx.fillRect(0, 0, canvas.width, canvas.height);
setWaveformPeaks(peaks);
const numChannels = peaks.length;
const chanHeight = canvas.height / numChannels;
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();
}
if (overviewPeaks == null) {
setOverviewPeaks(peaks);
}
})();
}, [zoomSettings]);
@ -246,13 +213,14 @@ export const Waveform: React.FC<WaveformProps> = ({
const wrapperProps = {
width: '90%',
height: '500px',
height: '350px',
position: 'relative',
margin: '0 auto',
} as React.CSSProperties;
const waveformCanvasProps = {
width: '100%',
height: '100%',
position: 'absolute',
top: 0,
left: 0,
@ -263,6 +231,7 @@ export const Waveform: React.FC<WaveformProps> = ({
const hudCanvasProps = {
width: '100%',
height: '100%',
position: 'absolute',
top: 0,
left: 0,
@ -271,18 +240,27 @@ export const Waveform: React.FC<WaveformProps> = ({
zIndex: 1,
} as React.CSSProperties;
const overviewCanvasProps = {
width: '90%',
height: '90px',
margin: '0 auto',
display: 'block',
} as React.CSSProperties;
const clockTextAreaProps = { color: '#999', width: '400px' };
return (
<>
<h1>clipper</h1>
<div style={wrapperProps}>
<canvas
ref={waveformCanvasRef}
width={canvasLogicalWidth}
height={canvasLogicalHeight}
<WaveformCanvas
peaks={waveformPeaks}
logicalWidth={canvasLogicalWidth}
logicalHeight={canvasLogicalHeight}
fillStyle="black"
strokeStyle="green"
style={waveformCanvasProps}
></canvas>
></WaveformCanvas>
<canvas
ref={hudCanvasRef}
width={canvasLogicalWidth}
@ -293,6 +271,14 @@ export const Waveform: React.FC<WaveformProps> = ({
style={hudCanvasProps}
></canvas>
</div>
<WaveformCanvas
peaks={overviewPeaks}
logicalWidth={canvasLogicalWidth}
logicalHeight={canvasLogicalHeight}
fillStyle="grey"
strokeStyle="black"
style={overviewCanvasProps}
></WaveformCanvas>
<button onClick={handlePlay}>Play</button>
<button onClick={handlePause}>Pause</button>
<button onClick={handleZoomIn}>+</button>

View File

@ -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>
</>
);
};