From 5a08bd62bf7450ad9a8791046723ee3c73daf365 Mon Sep 17 00:00:00 2001 From: Rob Watson Date: Sat, 11 Sep 2021 18:42:37 +0200 Subject: [PATCH] Add overview waveform --- frontend/src/Waveform.tsx | 92 ++++++++++++++------------------- frontend/src/WaveformCanvas.tsx | 66 +++++++++++++++++++++++ 2 files changed, 105 insertions(+), 53 deletions(-) create mode 100644 frontend/src/WaveformCanvas.tsx diff --git a/frontend/src/Waveform.tsx b/frontend/src/Waveform.tsx index 3cfc423..845e251 100644 --- a/frontend/src/Waveform.tsx +++ b/frontend/src/Waveform.tsx @@ -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 = ({ - audioContext, -}: WaveformProps) => { +export const Waveform: React.FC = ({ audioContext }: Props) => { const [audioFile, setAudioFile] = useState(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(null); const hudCanvasRef = useRef(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 = ({ 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 = ({ })(); }, [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 = ({ })(); }, [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 = ({ 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 = ({ const hudCanvasProps = { width: '100%', + height: '100%', position: 'absolute', top: 0, left: 0, @@ -271,18 +240,27 @@ export const Waveform: React.FC = ({ 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 ( <>

clipper

- + > = ({ style={hudCanvasProps} >
+ diff --git a/frontend/src/WaveformCanvas.tsx b/frontend/src/WaveformCanvas.tsx new file mode 100644 index 0000000..a9d8b05 --- /dev/null +++ b/frontend/src/WaveformCanvas.tsx @@ -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) => { + const canvasRef = useRef(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 ( + <> + + + ); +};