diff --git a/src/components/video-editor/VideoEditor.tsx b/src/components/video-editor/VideoEditor.tsx index c7394dda..0930186e 100644 --- a/src/components/video-editor/VideoEditor.tsx +++ b/src/components/video-editor/VideoEditor.tsx @@ -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); }} /> diff --git a/src/components/video-editor/audio/useAudioPreviewSync.ts b/src/components/video-editor/audio/useAudioPreviewSync.ts index 620ae2e6..b5668194 100644 --- a/src/components/video-editor/audio/useAudioPreviewSync.ts +++ b/src/components/video-editor/audio/useAudioPreviewSync.ts @@ -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) { diff --git a/src/components/video-editor/audio/useClipAudioSettingsController.ts b/src/components/video-editor/audio/useClipAudioSettingsController.ts index f6d69881..1873fb52 100644 --- a/src/components/video-editor/audio/useClipAudioSettingsController.ts +++ b/src/components/video-editor/audio/useClipAudioSettingsController.ts @@ -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, }; } - diff --git a/src/components/video-editor/audio/useSourceAudioTrackSettings.ts b/src/components/video-editor/audio/useSourceAudioTrackSettings.ts index 27ea0daf..5a8ca053 100644 --- a/src/components/video-editor/audio/useSourceAudioTrackSettings.ts +++ b/src/components/video-editor/audio/useSourceAudioTrackSettings.ts @@ -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, diff --git a/src/components/video-editor/audio/useVideoEditorAudio.ts b/src/components/video-editor/audio/useVideoEditorAudio.ts new file mode 100644 index 00000000..02de6a23 --- /dev/null +++ b/src/components/video-editor/audio/useVideoEditorAudio.ts @@ -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, + }; +} diff --git a/src/components/video-editor/timeline/Item.tsx b/src/components/video-editor/timeline/Item.tsx index e633e52e..3af2ad4d 100644 --- a/src/components/video-editor/timeline/Item.tsx +++ b/src/components/video-editor/timeline/Item.tsx @@ -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" /> )} diff --git a/src/components/video-editor/timeline/TimelineEditor.tsx b/src/components/video-editor/timeline/TimelineEditor.tsx index 6fc22499..8fb8a540 100644 --- a/src/components/video-editor/timeline/TimelineEditor.tsx +++ b/src/components/video-editor/timeline/TimelineEditor.tsx @@ -85,6 +85,9 @@ export interface TimelineEditorProps { showSourceAudioTrack?: boolean; onSourceAudioAvailabilityChange?: (available: boolean) => void; sourceAudioTrackSettings?: Record; + getSourceAudioTrackSettingsForClip?: ( + clipId: string | null, + ) => Record; onSourceAudioTracksMetaChange?: (tracks: Array<{ id: string; label: string }>) => void; } @@ -172,6 +175,7 @@ const TimelineEditor = forwardRef( showSourceAudioTrack = false, onSourceAudioAvailabilityChange, sourceAudioTrackSettings = {}, + getSourceAudioTrackSettingsForClip, onSourceAudioTracksMetaChange, }, ref, @@ -296,30 +300,7 @@ const TimelineEditor = forwardRef( 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( selectAllBlocksActive={selectAllBlocksActive} onClearBlockSelection={clearSelectedBlocks} keyframes={keyframes} - sourceAudioTracks={displaySourceAudioTracks} + sourceAudioTracks={sourceAudioTracks} + getSourceAudioTrackSettingsForClip={getSourceAudioTrackSettingsForClip} showSourceAudioTrack={showSourceAudioTrack} liveSpanPreviewById={liveZoomPreview.previewSpans} liveHiddenItemIds={Array.from(liveZoomPreview.hiddenZoomIds)} diff --git a/src/components/video-editor/timeline/components/viewport/TimelineCanvas.tsx b/src/components/video-editor/timeline/components/viewport/TimelineCanvas.tsx index 94d4cd88..12013a27 100644 --- a/src/components/video-editor/timeline/components/viewport/TimelineCanvas.tsx +++ b/src/components/video-editor/timeline/components/viewport/TimelineCanvas.tsx @@ -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; showSourceAudioTrack?: boolean; liveSpanPreviewById?: Record; 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; showSourceAudioTrack?: boolean; liveSpanPreviewById?: Record; 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) => ( - {clipItems.map((item) => ( - onSelectClip?.(item.id)} - variant="audio" - waveformPeaks={track.peaks} - waveformSegmentSpan={liveSpanPreviewById?.[item.id] ?? item.span} - > - {track.label} - - ))} + {clipItems.map((item) => { + const settings = getSourceAudioTrackSettingsForClip?.(item.id)?.[ + track.id + ] ?? { volume: 1, normalize: false }; + return ( + 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} + + ); + })} ))} @@ -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} diff --git a/src/components/video-editor/timeline/components/waveform/AudioWaveform.tsx b/src/components/video-editor/timeline/components/waveform/AudioWaveform.tsx index 4b8bf510..ae06fd49 100644 --- a/src/components/video-editor/timeline/components/waveform/AudioWaveform.tsx +++ b/src/components/video-editor/timeline/components/waveform/AudioWaveform.tsx @@ -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(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 (