mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-24 23:05:49 +00:00
add: audio fixed in the wrong layer and stuff
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user