add: audio fixed in the wrong layer and stuff

This commit is contained in:
Alan Trebugeais
2026-05-09 15:13:26 +02:00
parent a3baf105e0
commit 3df4ca873a
9 changed files with 237 additions and 114 deletions
+36 -68
View File
@@ -80,7 +80,6 @@ import {
canUseInMemoryExportSaveFallback,
describeBlockedInMemoryExportSave,
} from "@/lib/exporter/exportSavePolicy";
import { resolveSourceAudioFallbackPaths } from "@/lib/exporter/sourceAudioFallback";
import { matchesShortcut } from "@/lib/shortcuts";
import { cn } from "@/lib/utils";
import {
@@ -140,10 +139,7 @@ import {
validateProjectData,
} from "./projectPersistence";
import { SettingsPanel } from "./SettingsPanel";
import { useAudioPreviewSync } from "./audio/useAudioPreviewSync";
import { useClipAudioSettingsController } from "./audio/useClipAudioSettingsController";
import { useSourceAudioFallback } from "./audio/useSourceAudioFallback";
import { getActiveClipIdAtSourceTime, isClipMutedById } from "./audio/clipAudio";
import { useVideoEditorAudio } from "./audio/useVideoEditorAudio";
import {
APP_HEADER_ICON_BUTTON_CLASS,
DiscordLinkButton,
@@ -1768,38 +1764,6 @@ export default function VideoEditor() {
() => videoSourcePath ?? (videoPath ? fromFileUrl(videoPath) : null),
[videoPath, videoSourcePath],
);
const { sourceAudioFallbackPaths, sourceAudioFallbackStartDelayMsByPath } =
useSourceAudioFallback({
currentSourcePath,
summarizeErrorMessage,
});
const { hasEmbeddedSourceAudio, externalAudioPaths: previewSourceAudioFallbackPaths } = useMemo(
() => resolveSourceAudioFallbackPaths(currentSourcePath, sourceAudioFallbackPaths),
[currentSourcePath, sourceAudioFallbackPaths],
);
const shouldMutePreviewVideo =
!hasEmbeddedSourceAudio && previewSourceAudioFallbackPaths.length > 0;
const activeClipIdAtCurrentTime = useMemo(
() => getActiveClipIdAtSourceTime(currentTime, clipRegions),
[clipRegions, currentTime],
);
const {
sourceAudioTrackMeta,
activeSourceAudioTrackSettings,
selectedClipSourceAudioTrackSettings,
onSourceAudioTracksMetaChange,
onSelectedClipSourceAudioTrackVolumeChange,
onSelectedClipSourceAudioTrackNormalizeChange,
embeddedSourcePreviewGain,
getSourceTrackPreviewGain,
} = useClipAudioSettingsController({
selectedClipId,
activeClipId: activeClipIdAtCurrentTime,
});
const isCurrentClipMuted = useMemo(() => {
return isClipMutedById(activeClipIdAtCurrentTime, clipRegions);
}, [activeClipIdAtCurrentTime, clipRegions]);
const projectDisplayName = useMemo(() => {
const fileName =
currentProjectPath?.split(/[\\/]/).pop() ??
@@ -3345,6 +3309,25 @@ export default function VideoEditor() {
}
return result;
}, [clipRegions, speedRegions]);
const audio = useVideoEditorAudio({
currentSourcePath,
selectedClipId,
clipRegions,
audioRegions,
effectiveSpeedRegions,
currentTime,
timelineTime: timelinePlayheadTime,
duration,
isPlaying,
previewVolume,
summarizeErrorMessage,
onSourceFallbackLoadError: (error) => {
toast.warning(
`Could not load companion audio source: ${summarizeErrorMessage(getErrorMessage(error))}`,
{ duration: 10000 },
);
},
});
function togglePlayPause() {
const playback = videoPlaybackRef.current;
@@ -4061,25 +4044,6 @@ export default function VideoEditor() {
}
}, [selectedAudioId, audioRegions]);
useAudioPreviewSync({
audioRegions,
previewVolume,
isPlaying,
currentTime,
duration,
effectiveSpeedRegions,
previewSourceAudioFallbackPaths,
sourceAudioFallbackStartDelayMsByPath,
isCurrentClipMuted,
getSourceTrackPreviewGain,
onSourceFallbackLoadError: (error) => {
toast.warning(
`Could not load companion audio source: ${summarizeErrorMessage(getErrorMessage(error))}`,
{ duration: 10000 },
);
},
});
const showExportSuccessToast = useCallback((filePath: string) => {
toast.success(`Exported successfully to ${filePath}`, {
action: {
@@ -4404,8 +4368,9 @@ export default function VideoEditor() {
cursorSway,
frame,
audioRegions,
sourceAudioFallbackPaths,
sourceAudioFallbackStartDelayMsByPath,
sourceAudioFallbackPaths: audio.sourceAudioFallbackPaths,
sourceAudioFallbackStartDelayMsByPath:
audio.sourceAudioFallbackStartDelayMsByPath,
previewWidth,
previewHeight,
onProgress: (progress: ExportProgress) => {
@@ -4652,8 +4617,8 @@ export default function VideoEditor() {
cursorClickBounceDuration,
cursorSway,
audioRegions,
sourceAudioFallbackPaths,
sourceAudioFallbackStartDelayMsByPath,
audio.sourceAudioFallbackPaths,
audio.sourceAudioFallbackStartDelayMsByPath,
exportEncodingMode,
exportBackendPreference,
exportPipelineModel,
@@ -5752,13 +5717,13 @@ export default function VideoEditor() {
hasClipSourceAudio={hasClipSourceAudio}
showClipSourceAudioTrack={showClipSourceAudioTrack}
onShowClipSourceAudioTrackChange={setShowClipSourceAudioTrack}
sourceAudioTrackMeta={sourceAudioTrackMeta}
sourceAudioTrackSettings={selectedClipSourceAudioTrackSettings}
sourceAudioTrackMeta={audio.sourceAudioTrackMeta}
sourceAudioTrackSettings={audio.selectedClipSourceAudioTrackSettings}
onSourceAudioTrackVolumeChange={
onSelectedClipSourceAudioTrackVolumeChange
audio.onSelectedClipSourceAudioTrackVolumeChange
}
onSourceAudioTrackNormalizeChange={
onSelectedClipSourceAudioTrackNormalizeChange
audio.onSelectedClipSourceAudioTrackNormalizeChange
}
selectedAudioId={selectedAudioId}
selectedAudioVolume={
@@ -6065,13 +6030,13 @@ export default function VideoEditor() {
}
cursorSway={cursorSway}
volume={
shouldMutePreviewVideo || isCurrentClipMuted
audio.isCurrentClipMuted
? 0
: Math.max(
0,
Math.min(
1,
previewVolume * embeddedSourcePreviewGain,
previewVolume * audio.embeddedSourcePreviewGain,
),
)
}
@@ -6350,7 +6315,10 @@ export default function VideoEditor() {
onSelectAnnotation={handleSelectAnnotation}
aspectRatio={aspectRatio}
showSourceAudioTrack={showClipSourceAudioTrack}
sourceAudioTrackSettings={activeSourceAudioTrackSettings}
sourceAudioTrackSettings={audio.activeSourceAudioTrackSettings}
getSourceAudioTrackSettingsForClip={
audio.getSourceAudioTrackSettingsForClip
}
onSourceAudioAvailabilityChange={(available) => {
setHasClipSourceAudio(available);
if (!available) {
@@ -6358,7 +6326,7 @@ export default function VideoEditor() {
}
}}
onSourceAudioTracksMetaChange={(tracks) => {
onSourceAudioTracksMetaChange(tracks);
audio.onSourceAudioTracksMetaChange(tracks);
}}
/>
</div>
@@ -19,6 +19,7 @@ interface UseAudioPreviewSyncParams {
previewVolume: number;
isPlaying: boolean;
currentTime: number;
timelineTime: number;
duration: number;
effectiveSpeedRegions: SpeedRegion[];
previewSourceAudioFallbackPaths: string[];
@@ -33,6 +34,7 @@ export function useAudioPreviewSync({
previewVolume,
isPlaying,
currentTime,
timelineTime,
duration,
effectiveSpeedRegions,
previewSourceAudioFallbackPaths,
@@ -219,7 +221,7 @@ export function useAudioPreviewSync({
}, []);
useEffect(() => {
const currentTimeMs = currentTime * 1000;
const currentTimeMs = timelineTime * 1000;
const activeSpeedRegion = effectiveSpeedRegions.find(
(region) => currentTimeMs >= region.startMs && currentTimeMs < region.endMs,
);
@@ -252,7 +254,7 @@ export function useAudioPreviewSync({
audio.pause();
}
}
}, [audioRegions, currentTime, effectiveSpeedRegions, isPlaying]);
}, [audioRegions, timelineTime, effectiveSpeedRegions, isPlaying]);
useEffect(() => {
if (previewSourceAudioFallbackPaths.length === 0) {
@@ -18,6 +18,7 @@ export function useClipAudioSettingsController({
sourceAudioTrackMeta,
activeSourceAudioTrackSettings,
selectedClipSourceAudioTrackSettings,
getSourceAudioTrackSettingsForClip,
onSourceAudioTracksMetaChange,
onSelectedClipSourceAudioTrackVolumeChange,
onSelectedClipSourceAudioTrackNormalizeChange,
@@ -49,6 +50,7 @@ export function useClipAudioSettingsController({
sourceAudioTrackMeta,
activeSourceAudioTrackSettings,
selectedClipSourceAudioTrackSettings,
getSourceAudioTrackSettingsForClip,
onSourceAudioTracksMetaChange,
onSelectedClipSourceAudioTrackVolumeChange,
onSelectedClipSourceAudioTrackNormalizeChange,
@@ -56,4 +58,3 @@ export function useClipAudioSettingsController({
getSourceTrackPreviewGain,
};
}
@@ -13,6 +13,7 @@ export interface UseSourceAudioTrackSettingsResult {
sourceAudioTrackMeta: SourceAudioTrackMeta;
activeSourceAudioTrackSettings: SourceAudioTrackSettings;
selectedClipSourceAudioTrackSettings: SourceAudioTrackSettings;
getSourceAudioTrackSettingsForClip: (clipId: string | null) => SourceAudioTrackSettings;
onSourceAudioTracksMetaChange: (tracks: SourceAudioTrackMeta) => void;
onSelectedClipSourceAudioTrackVolumeChange: (id: string, volume: number) => void;
onSelectedClipSourceAudioTrackNormalizeChange: (id: string, normalize: boolean) => void;
@@ -61,6 +62,19 @@ export function useSourceAudioTrackSettings({
});
}, []);
const getSourceAudioTrackSettingsForClip = useCallback(
(clipId: string | null): SourceAudioTrackSettings => {
if (!clipId) {
return defaultSourceAudioTrackSettings;
}
return {
...defaultSourceAudioTrackSettings,
...(sourceAudioTrackSettingsByClip[clipId] ?? {}),
};
},
[defaultSourceAudioTrackSettings, sourceAudioTrackSettingsByClip],
);
const onSelectedClipSourceAudioTrackVolumeChange = useCallback(
(id: string, volume: number) => {
if (!selectedClipId) return;
@@ -105,6 +119,7 @@ export function useSourceAudioTrackSettings({
sourceAudioTrackMeta,
activeSourceAudioTrackSettings,
selectedClipSourceAudioTrackSettings,
getSourceAudioTrackSettingsForClip,
onSourceAudioTracksMetaChange,
onSelectedClipSourceAudioTrackVolumeChange,
onSelectedClipSourceAudioTrackNormalizeChange,
@@ -0,0 +1,127 @@
import { useMemo } from "react";
import { resolveSourceAudioFallbackPaths } from "@/lib/exporter/sourceAudioFallback";
import type { AudioRegion, ClipRegion, SpeedRegion } from "../types";
import { getActiveClipIdAtSourceTime, isClipMutedById } from "./clipAudio";
import { useAudioPreviewSync } from "./useAudioPreviewSync";
import { useClipAudioSettingsController } from "./useClipAudioSettingsController";
import { useSourceAudioFallback } from "./useSourceAudioFallback";
function extractLocalPathFromMediaServerUrl(input: string | null | undefined): string | null {
if (!input) return null;
try {
const url = new URL(input);
const isLocalMediaServer =
(url.protocol === "http:" || url.protocol === "https:") &&
(url.hostname === "127.0.0.1" || url.hostname === "localhost") &&
url.pathname === "/video";
if (!isLocalMediaServer) return null;
return url.searchParams.get("path");
} catch {
return null;
}
}
interface UseVideoEditorAudioParams {
currentSourcePath: string | null;
selectedClipId: string | null;
clipRegions: ClipRegion[];
audioRegions: AudioRegion[];
effectiveSpeedRegions: SpeedRegion[];
currentTime: number;
timelineTime: number;
duration: number;
isPlaying: boolean;
previewVolume: number;
summarizeErrorMessage: (message: string) => string;
onSourceFallbackLoadError: (error: unknown) => void;
}
export function useVideoEditorAudio({
currentSourcePath,
selectedClipId,
clipRegions,
audioRegions,
effectiveSpeedRegions,
currentTime,
timelineTime,
duration,
isPlaying,
previewVolume,
summarizeErrorMessage,
onSourceFallbackLoadError,
}: UseVideoEditorAudioParams) {
const fallbackLookupSourcePath = useMemo(
() => extractLocalPathFromMediaServerUrl(currentSourcePath) ?? currentSourcePath,
[currentSourcePath],
);
const { sourceAudioFallbackPaths, sourceAudioFallbackStartDelayMsByPath } =
useSourceAudioFallback({
currentSourcePath: fallbackLookupSourcePath,
summarizeErrorMessage,
});
const { hasEmbeddedSourceAudio, externalAudioPaths: previewSourceAudioFallbackPaths } = useMemo(
() => resolveSourceAudioFallbackPaths(currentSourcePath, sourceAudioFallbackPaths),
[currentSourcePath, sourceAudioFallbackPaths],
);
const shouldMutePreviewVideo =
!hasEmbeddedSourceAudio && previewSourceAudioFallbackPaths.length > 0;
const activeClipIdAtCurrentTime = useMemo(
() => getActiveClipIdAtSourceTime(currentTime, clipRegions),
[clipRegions, currentTime],
);
const isCurrentClipMuted = useMemo(
() => isClipMutedById(activeClipIdAtCurrentTime, clipRegions),
[activeClipIdAtCurrentTime, clipRegions],
);
const {
sourceAudioTrackMeta,
activeSourceAudioTrackSettings,
selectedClipSourceAudioTrackSettings,
getSourceAudioTrackSettingsForClip,
onSourceAudioTracksMetaChange,
onSelectedClipSourceAudioTrackVolumeChange,
onSelectedClipSourceAudioTrackNormalizeChange,
embeddedSourcePreviewGain,
getSourceTrackPreviewGain,
} = useClipAudioSettingsController({
selectedClipId,
activeClipId: activeClipIdAtCurrentTime,
});
useAudioPreviewSync({
audioRegions,
previewVolume,
isPlaying,
currentTime,
timelineTime,
duration,
effectiveSpeedRegions,
previewSourceAudioFallbackPaths,
sourceAudioFallbackStartDelayMsByPath,
isCurrentClipMuted,
getSourceTrackPreviewGain,
onSourceFallbackLoadError,
});
return {
sourceAudioFallbackPaths,
sourceAudioFallbackStartDelayMsByPath,
previewSourceAudioFallbackPaths,
shouldMutePreviewVideo,
activeClipIdAtCurrentTime,
isCurrentClipMuted,
sourceAudioTrackMeta,
activeSourceAudioTrackSettings,
selectedClipSourceAudioTrackSettings,
getSourceAudioTrackSettingsForClip,
onSourceAudioTracksMetaChange,
onSelectedClipSourceAudioTrackVolumeChange,
onSelectedClipSourceAudioTrackNormalizeChange,
embeddedSourcePreviewGain,
getSourceTrackPreviewGain,
};
}
@@ -29,6 +29,8 @@ interface ItemProps {
speedValue?: number;
waveformPeaks?: AudioPeaksData | null;
waveformSegmentSpan?: Span;
waveformGain?: number;
waveformNormalize?: boolean;
variant?: "zoom" | "trim" | "clip" | "annotation" | "speed" | "audio";
}
@@ -65,6 +67,8 @@ export default function Item({
speedValue,
waveformPeaks = null,
waveformSegmentSpan,
waveformGain = 1,
waveformNormalize = false,
variant = "zoom",
children,
}: ItemProps) {
@@ -161,6 +165,8 @@ export default function Item({
peaks={waveformPeaks}
segmentStartMs={waveformSegmentSpan?.start ?? span.start}
segmentEndMs={waveformSegmentSpan?.end ?? span.end}
gain={waveformGain}
normalize={waveformNormalize}
className="absolute inset-0 w-full h-full pointer-events-none opacity-45"
/>
)}
@@ -85,6 +85,9 @@ export interface TimelineEditorProps {
showSourceAudioTrack?: boolean;
onSourceAudioAvailabilityChange?: (available: boolean) => void;
sourceAudioTrackSettings?: Record<string, { volume: number; normalize: boolean }>;
getSourceAudioTrackSettingsForClip?: (
clipId: string | null,
) => Record<string, { volume: number; normalize: boolean }>;
onSourceAudioTracksMetaChange?: (tracks: Array<{ id: string; label: string }>) => void;
}
@@ -172,6 +175,7 @@ const TimelineEditor = forwardRef<TimelineEditorHandle, TimelineEditorProps>(
showSourceAudioTrack = false,
onSourceAudioAvailabilityChange,
sourceAudioTrackSettings = {},
getSourceAudioTrackSettingsForClip,
onSourceAudioTracksMetaChange,
},
ref,
@@ -296,30 +300,7 @@ const TimelineEditor = forwardRef<TimelineEditorHandle, TimelineEditorProps>(
useEffect(() => {
onSourceAudioTracksMetaChange?.(sourceAudioTracks.map((t) => ({ id: t.id, label: t.label })));
}, [onSourceAudioTracksMetaChange, sourceAudioTracks]);
const displaySourceAudioTracks = useMemo(() => {
return sourceAudioTracks.map((track) => {
const settings = sourceAudioTrackSettings[track.id] ?? { volume: 1, normalize: false };
const volume = Math.max(0, Math.min(2, settings.volume));
const normalize = settings.normalize;
const input = track.peaks.peaks;
const adjusted = new Float32Array(input.length);
for (let i = 0; i < input.length; i++) {
let amp = input[i];
if (normalize) {
amp = Math.sqrt(amp);
}
adjusted[i] = Math.max(0, Math.min(1, amp * volume));
}
return {
id: track.id,
label: track.label,
peaks: {
durationMs: track.peaks.durationMs,
peaks: adjusted,
} satisfies AudioPeaksData,
};
});
}, [sourceAudioTrackSettings, sourceAudioTracks]);
void sourceAudioTrackSettings;
useEffect(() => {
onSourceAudioAvailabilityChange?.(sourceAudioTracks.length > 0);
}, [onSourceAudioAvailabilityChange, sourceAudioTracks.length]);
@@ -565,7 +546,8 @@ const TimelineEditor = forwardRef<TimelineEditorHandle, TimelineEditorProps>(
selectAllBlocksActive={selectAllBlocksActive}
onClearBlockSelection={clearSelectedBlocks}
keyframes={keyframes}
sourceAudioTracks={displaySourceAudioTracks}
sourceAudioTracks={sourceAudioTracks}
getSourceAudioTrackSettingsForClip={getSourceAudioTrackSettingsForClip}
showSourceAudioTrack={showSourceAudioTrack}
liveSpanPreviewById={liveZoomPreview.previewSpans}
liveHiddenItemIds={Array.from(liveZoomPreview.hiddenZoomIds)}
@@ -58,6 +58,9 @@ interface TimelineCanvasProps {
onClearBlockSelection?: () => void;
keyframes?: { id: string; time: number }[];
sourceAudioTracks?: Array<{ id: string; label: string; peaks: AudioPeaksData }>;
getSourceAudioTrackSettingsForClip?: (
clipId: string | null,
) => Record<string, { volume: number; normalize: boolean }>;
showSourceAudioTrack?: boolean;
liveSpanPreviewById?: Record<string, { start: number; end: number }>;
liveHiddenItemIds?: string[];
@@ -222,6 +225,9 @@ interface TimelineCanvasRowsProps {
onSelectAnnotation?: (id: string | null) => void;
onSelectAudio?: (id: string | null) => void;
sourceAudioTracks?: Array<{ id: string; label: string; peaks: AudioPeaksData }>;
getSourceAudioTrackSettingsForClip?: (
clipId: string | null,
) => Record<string, { volume: number; normalize: boolean }>;
showSourceAudioTrack?: boolean;
liveSpanPreviewById?: Record<string, { start: number; end: number }>;
liveHiddenItemIds?: string[];
@@ -285,6 +291,7 @@ const TimelineCanvasRows = memo(function TimelineCanvasRows({
onSelectAnnotation,
onSelectAudio,
sourceAudioTracks = [],
getSourceAudioTrackSettingsForClip,
showSourceAudioTrack = false,
liveSpanPreviewById,
liveHiddenItemIds,
@@ -371,22 +378,29 @@ const TimelineCanvasRows = memo(function TimelineCanvasRows({
{showSourceAudioTrack &&
sourceAudioTracks.map((track) => (
<Row key={track.id} id={`${SOURCE_AUDIO_ROW_ID}-${track.id}`}>
{clipItems.map((item) => (
<Item
key={`source-audio-${track.id}-${item.id}`}
id={`source-audio-${track.id}-${item.id}`}
rowId={`${SOURCE_AUDIO_ROW_ID}-${track.id}`}
span={liveSpanPreviewById?.[item.id] ?? item.span}
disabled
isSelected={selectAllBlocksActive || item.id === selectedClipId}
onSelect={() => onSelectClip?.(item.id)}
variant="audio"
waveformPeaks={track.peaks}
waveformSegmentSpan={liveSpanPreviewById?.[item.id] ?? item.span}
>
{track.label}
</Item>
))}
{clipItems.map((item) => {
const settings = getSourceAudioTrackSettingsForClip?.(item.id)?.[
track.id
] ?? { volume: 1, normalize: false };
return (
<Item
key={`source-audio-${track.id}-${item.id}`}
id={`source-audio-${track.id}-${item.id}`}
rowId={`${SOURCE_AUDIO_ROW_ID}-${track.id}`}
span={liveSpanPreviewById?.[item.id] ?? item.span}
disabled
isSelected={selectAllBlocksActive || item.id === selectedClipId}
onSelect={() => onSelectClip?.(item.id)}
variant="audio"
waveformPeaks={track.peaks}
waveformSegmentSpan={liveSpanPreviewById?.[item.id] ?? item.span}
waveformGain={Math.max(0, Math.min(2, settings.volume))}
waveformNormalize={Boolean(settings.normalize)}
>
{track.label}
</Item>
);
})}
</Row>
))}
@@ -497,6 +511,7 @@ export default function TimelineCanvas({
onClearBlockSelection,
keyframes = [],
sourceAudioTracks = [],
getSourceAudioTrackSettingsForClip,
showSourceAudioTrack = false,
liveSpanPreviewById,
liveHiddenItemIds,
@@ -730,6 +745,7 @@ export default function TimelineCanvas({
onSelectAnnotation={onSelectAnnotation}
onSelectAudio={onSelectAudio}
sourceAudioTracks={sourceAudioTracks}
getSourceAudioTrackSettingsForClip={getSourceAudioTrackSettingsForClip}
showSourceAudioTrack={showSourceAudioTrack}
liveSpanPreviewById={liveSpanPreviewById}
liveHiddenItemIds={liveHiddenItemIds}
@@ -6,6 +6,8 @@ interface AudioWaveformProps {
peaks: AudioPeaksData;
segmentStartMs?: number;
segmentEndMs?: number;
gain?: number;
normalize?: boolean;
className?: string;
}
@@ -18,6 +20,8 @@ function AudioWaveformComponent({
peaks,
segmentStartMs,
segmentEndMs,
gain = 1,
normalize = false,
className,
}: AudioWaveformProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
@@ -90,7 +94,9 @@ function AudioWaveformComponent({
const leftIndex = Math.floor(exactIndex);
const rightIndex = Math.min(peakData.length - 1, leftIndex + 1);
const mix = exactIndex - leftIndex;
const amplitude = peakData[leftIndex] * (1 - mix) + peakData[rightIndex] * mix;
let amplitude = peakData[leftIndex] * (1 - mix) + peakData[rightIndex] * mix;
if (normalize) amplitude = Math.sqrt(Math.max(0, amplitude));
amplitude = Math.max(0, Math.min(1, amplitude * gain));
const barHeight = amplitude * midY * 0.85;
ctx.moveTo(px, midY - barHeight);
@@ -103,7 +109,7 @@ function AudioWaveformComponent({
};
rafId = requestAnimationFrame(draw);
return () => cancelAnimationFrame(rafId);
}, [peaks, range.start, range.end, resizeKey, segmentStartMs, segmentEndMs]);
}, [gain, normalize, peaks, range.start, range.end, resizeKey, segmentStartMs, segmentEndMs]);
return (
<canvas