feat: audio waveform track

This commit is contained in:
aarthificial
2022-05-05 19:30:33 +02:00
parent 0290a9cb07
commit 9aff955ef4
4 changed files with 141 additions and 38 deletions

View File

@@ -0,0 +1,71 @@
import styles from './Timeline.module.scss';
import {useContext, useLayoutEffect, useMemo, useRef} from 'preact/hooks';
import {TimelineContext} from './TimelineContext';
import {usePlayer} from '../../hooks';
const HEIGHT = 48;
export function AudioTrack() {
const ref = useRef<HTMLCanvasElement>();
const player = usePlayer();
const context = useMemo(() => ref.current?.getContext('2d'), [ref.current]);
const {viewLength, startFrame, endFrame, duration, density} =
useContext(TimelineContext);
useLayoutEffect(() => {
if (!context) return;
const audio = player.audio.meta;
const samplesPerSeconds = audio.sample_rate / audio.samples_per_pixel;
const step = Math.ceil(density / 256);
const start = Math.floor(
player.project.framesToSeconds(startFrame) * samplesPerSeconds,
);
const end = Math.floor(
player.project.framesToSeconds(endFrame) * samplesPerSeconds,
);
context.clearRect(0, 0, viewLength, 64);
context.strokeStyle = 'white';
context.lineWidth = 1;
context.beginPath();
context.moveTo(0, HEIGHT);
const length = end - start;
for (let offset = 0; offset < length; offset += step) {
const sample = (start + offset) * 2;
if (sample >= audio.data.length) break;
context.lineTo(
(offset / length) * viewLength,
(audio.data[sample] / 32767) * HEIGHT + HEIGHT,
);
context.lineTo(
((offset + 0.5) / length) * viewLength,
(audio.data[sample + 1] / 32767) * HEIGHT + HEIGHT,
);
}
context.stroke();
}, [context, density, viewLength, startFrame, endFrame]);
const style = useMemo(
() => ({
marginLeft: `${(startFrame / duration) * 100}%`,
width: `${((endFrame - startFrame) / duration) * 100}%`,
height: `${HEIGHT * 2}px`,
}),
[startFrame, endFrame, duration],
);
return (
<canvas
style={style}
width={viewLength}
height={64}
ref={ref}
className={styles.audioTrack}
/>
);
}

View File

@@ -2,8 +2,22 @@
background-color: #242424;
width: 100%;
height: 100%;
display: flex;
align-items: stretch;
position: relative;
}
.sidebar {
width: 320px;
flex-shrink: 0;
flex-grow: 0;
}
.timeline {
overflow-x: auto;
overflow-y: auto;
flex-grow: 1;
position: relative;
&::-webkit-scrollbar {
@@ -155,7 +169,7 @@
opacity: 0;
pointer-events: none;
.root:hover & {
.timeline:hover & {
opacity: 0.24;
}
}
@@ -190,3 +204,7 @@
margin-top: 4px;
}
.audioTrack {
opacity: 0.16;
}

View File

@@ -20,6 +20,7 @@ import {SceneTrack} from './SceneTrack';
import {RangeTrack} from './RangeTrack';
import {TimelineContext, TimelineState} from './TimelineContext';
import {clamp} from '@motion-canvas/core/tweening';
import {AudioTrack} from './AudioTrack';
const ZOOM_SPEED = 0.1;
@@ -110,44 +111,54 @@ export function Timeline() {
return (
<TimelineContext.Provider value={state}>
<div
className={styles.root}
ref={containerRef}
onScroll={event => setOffset((event.target as HTMLElement).scrollLeft)}
onWheel={event => {
const ratio = 1 - Math.sign(event.deltaY) * ZOOM_SPEED;
const newScale = scale * ratio < 1 ? 1 : scale * ratio;
const pointer = offset + event.x - rect.x;
const newOffset = offset - pointer + pointer * ratio;
const newTrackSize = rect.width * newScale;
const maxOffset = newTrackSize - rect.width;
containerRef.current.scrollLeft = newOffset;
setScale(newScale);
setOffset(clamp(0, maxOffset, newOffset));
}}
onMouseUp={event => {
if (event.button === 0) {
player.requestSeek(
Math.floor(
((offset + event.x - rect.x) / state.fullLength) * duration,
),
);
<div className={styles.root}>
<div
className={styles.timeline}
ref={containerRef}
onScroll={event =>
setOffset((event.target as HTMLElement).scrollLeft)
}
}}
onMouseMove={event => {
playheadRef.current.style.left = `${event.x - rect.x + offset}px`;
}}
>
<div className={styles.track} style={{width: `${state.fullLength}px`}}>
<RangeTrack />
<TimestampTrack />
<SceneTrack />
<LabelTrack />
onWheel={event => {
if (event.shiftKey) return;
const ratio = 1 - Math.sign(event.deltaY) * ZOOM_SPEED;
const newScale = scale * ratio < 1 ? 1 : scale * ratio;
const pointer = offset + event.x - rect.x;
const newOffset = offset - pointer + pointer * ratio;
const newTrackSize = rect.width * newScale;
const maxOffset = newTrackSize - rect.width;
containerRef.current.scrollLeft = newOffset;
setScale(newScale);
setOffset(clamp(0, maxOffset, newOffset));
}}
onMouseUp={event => {
if (event.button === 0) {
player.requestSeek(
Math.floor(
((offset + event.x - rect.x) / state.fullLength) * duration,
),
);
}
}}
onMouseMove={event => {
playheadRef.current.style.left = `${event.x - rect.x + offset}px`;
}}
>
<div
className={styles.track}
style={{width: `${state.fullLength}px`}}
>
<RangeTrack />
<TimestampTrack />
<SceneTrack />
<LabelTrack />
<AudioTrack />
</div>
<div ref={playheadRef} className={styles.playheadPreview} />
<Playhead />
</div>
<div ref={playheadRef} className={styles.playheadPreview} />
<Playhead />
</div>
</TimelineContext.Provider>
);

View File

@@ -5,7 +5,10 @@ export function useSize<T extends Element>(
): DOMRectReadOnly {
const [rect, setRect] = useState<DOMRect>(new DOMRect());
const observer = useMemo(
() => new ResizeObserver(entries => setRect(entries[0].contentRect)),
() =>
new ResizeObserver(() =>
setRect(ref.current.getBoundingClientRect()),
),
[],
);
useEffect(() => {