From f4af06d430c653d1b360bec3efb52cfa6c7ab09b Mon Sep 17 00:00:00 2001 From: Alan Trebugeais Date: Sat, 9 May 2026 18:07:28 +0200 Subject: [PATCH] add: project persistence of audio settings --- electron/ipc/recording/diagnostics.ts | 9 +- electron/ipc/recording/mac.ts | 6 - electron/ipc/recording/windows.ts | 26 +- .../video-editor/AnnotationSettingsPanel.tsx | 25 +- src/components/video-editor/SettingsPanel.tsx | 277 ++++++++++-------- src/components/video-editor/VideoEditor.tsx | 23 ++ .../video-editor/audio/useAudioPreviewSync.ts | 21 +- .../audio/useClipAudioSettingsController.ts | 43 ++- .../audio/useSourceAudioTrackSettings.ts | 21 +- .../video-editor/audio/useVideoEditorAudio.ts | 31 +- .../video-editor/projectPersistence.ts | 17 ++ .../video-editor/timeline/TimelineEditor.tsx | 26 +- src/components/video-editor/types.ts | 8 + src/i18n/locales/en/settings.json | 3 + src/i18n/locales/es/settings.json | 3 + src/i18n/locales/fr/settings.json | 3 + src/i18n/locales/zh-CN/settings.json | 11 +- 17 files changed, 366 insertions(+), 187 deletions(-) diff --git a/electron/ipc/recording/diagnostics.ts b/electron/ipc/recording/diagnostics.ts index b8fb6860..f097cea9 100644 --- a/electron/ipc/recording/diagnostics.ts +++ b/electron/ipc/recording/diagnostics.ts @@ -499,20 +499,21 @@ export async function getCompanionAudioFallbackInfo(videoPath: string) { let paths: string[]; if (await hasEmbeddedAudioStream(videoPath)) { - const microphoneCompanionPaths = Array.from( + const companionPaths = Array.from( new Set( companionCandidates.flatMap((candidate) => candidate.usablePaths.filter( - (companionPath) => companionPath === candidate.micPath, + (companionPath) => + companionPath === candidate.micPath || companionPath === candidate.systemPath, ), ), ), ); - if (microphoneCompanionPaths.length === 0) { + if (companionPaths.length === 0) { return { paths: [], startDelayMsByPath: {} }; } - paths = [videoPath, ...microphoneCompanionPaths]; + paths = [videoPath, ...companionPaths]; } else { paths = Array.from( new Set(companionCandidates.flatMap((candidate) => candidate.usablePaths)), diff --git a/electron/ipc/recording/mac.ts b/electron/ipc/recording/mac.ts index 769d22ca..1227b0af 100644 --- a/electron/ipc/recording/mac.ts +++ b/electron/ipc/recording/mac.ts @@ -277,12 +277,6 @@ export async function muxNativeMacRecordingWithAudio( await moveFileWithOverwrite(mixedOutputPath, videoPath); console.log("[mux] Successfully muxed audio into video:", videoPath); - - for (const audioPath of [systemAudioPath, microphonePath]) { - if (audioPath) { - await fs.rm(audioPath, { force: true }).catch(() => undefined); - } - } } export function attachNativeCaptureLifecycle(process: ChildProcessWithoutNullStreams) { diff --git a/electron/ipc/recording/windows.ts b/electron/ipc/recording/windows.ts index 6fc6c035..26e15b07 100644 --- a/electron/ipc/recording/windows.ts +++ b/electron/ipc/recording/windows.ts @@ -474,30 +474,6 @@ export async function muxNativeWindowsVideoWithAudio( throw error; } - if (keepAudioSidecars) { - console.log( - `[mux-win] Keeping native audio sidecars because ${RECORDING_AUDIO_SIDECAR_DEBUG_ENV} is enabled`, - ); - return { - muxed: true, - videoDurationSeconds: videoDuration, - muxTimeoutMs, - audioInputs, - audio, - outputPath: videoPath, - keptAudioSidecars: true, - }; - } - - for (const audioPath of [systemAudioPath, micAudioPath]) { - if (audioPath) { - await Promise.all([ - fs.rm(audioPath, { force: true }).catch(() => undefined), - fs.rm(`${audioPath}.json`, { force: true }).catch(() => undefined), - ]); - } - } - return { muxed: true, videoDurationSeconds: videoDuration, @@ -505,6 +481,6 @@ export async function muxNativeWindowsVideoWithAudio( audioInputs, audio, outputPath: videoPath, - keptAudioSidecars: false, + keptAudioSidecars: true, }; } diff --git a/src/components/video-editor/AnnotationSettingsPanel.tsx b/src/components/video-editor/AnnotationSettingsPanel.tsx index a0370a5d..be475809 100644 --- a/src/components/video-editor/AnnotationSettingsPanel.tsx +++ b/src/components/video-editor/AnnotationSettingsPanel.tsx @@ -139,7 +139,8 @@ export function AnnotationSettingsPanel({ }; return ( -
+
+
@@ -772,16 +773,6 @@ export function AnnotationSettingsPanel({ - -
@@ -796,6 +787,18 @@ export function AnnotationSettingsPanel({
+
+ +
+
); } diff --git a/src/components/video-editor/SettingsPanel.tsx b/src/components/video-editor/SettingsPanel.tsx index 7e05203d..88dd2659 100644 --- a/src/components/video-editor/SettingsPanel.tsx +++ b/src/components/video-editor/SettingsPanel.tsx @@ -3049,6 +3049,28 @@ export function SettingsPanel({ ); + const audioSectionContent = ( +
+
+ {tSettings("audio.volumeTitle", "Audio")} + + {Math.round((selectedAudioVolume ?? 1) * 100)}% + +
+ onAudioVolumeChange?.(v)} + formatValue={(v) => `${Math.round(v * 100)}%`} + parseInput={(text) => parseFloat(text.replace(/%$/, "")) / 100} + /> +
+ ); + const clipSectionContent = (
@@ -3101,44 +3123,88 @@ export function SettingsPanel({ })}
-
- - {selectedClipMuted - ? tSettings("clip.unmuteAudio", "Unmute Audio") - : tSettings("clip.muteAudio", "Mute Audio")} - - onClipMutedChange?.(!v)} - className="data-[state=checked]:bg-[#06b6d4] scale-75" - /> -
- {hasClipSourceAudio && ( -
+
+ {tSettings("audio.title", "Audio")} + +
- {tSettings("clip.separateClipFromAudio", "Separate clip from audio")} + {selectedClipMuted + ? tSettings("clip.unmuteAudio", "Unmute Audio") + : tSettings("clip.muteAudio", "Mute Audio")} onClipShowSourceAudioChange?.(v)} + checked={!(selectedClipMuted ?? false)} + onCheckedChange={(v) => onClipMutedChange?.(!v)} className="data-[state=checked]:bg-[#06b6d4] scale-75" />
- )} + {hasClipSourceAudio && ( +
+ + {tSettings("clip.separateClipFromAudio", "Separate clip from audio")} + + onClipShowSourceAudioChange?.(v)} + className="data-[state=checked]:bg-[#06b6d4] scale-75" + /> +
+ )} +
- {selectedClipId && ( - - )} + {selectedClipId && + hasClipSourceAudio && + selectedClipShowSourceAudio && + sourceAudioTrackMeta.length > 0 && ( +
+ {sourceAudioTrackMeta.map((track) => { + const settings = sourceAudioTrackSettings[track.id] ?? { + volume: 1, + normalize: false, + }; + return ( +
+
+ + {track.label} + + + {Math.round(settings.volume * 100)}% + +
+
+ + {tSettings("audio.normalize", "Normalize")} + + + onSourceAudioTrackNormalizeChange?.(track.id, v) + } + className="data-[state=checked]:bg-[#06b6d4] scale-75" + /> +
+ onSourceAudioTrackVolumeChange?.(track.id, v)} + formatValue={(v) => `${Math.round(v * 100)}%`} + parseInput={(text) => + parseFloat(text.replace(/%$/, "")) / 100 + } + /> +
+ ); + })} +
+ )}
); @@ -3151,6 +3217,8 @@ export function SettingsPanel({ return zoomItemSectionContent; case "clip": return clipSectionContent; + case "audio": + return audioSectionContent; case "frame": return sceneSectionContent; case "crop": @@ -3592,94 +3660,69 @@ export function SettingsPanel({
{ + if (activeEffectSection === "clip" && selectedClipId) return false; + if (activeEffectSection === "zoom" && selectedZoomId) return false; + if (activeEffectSection === "audio" && selectedAudioId) return false; + if (selectedAnnotationId) return false; // Annotation editor handles its own but let's see + return true; + })() && "hidden", )} > - {selectedAudioId ? ( -
-
- - {tSettings("audio.volumeTitle", "Audio Volume")} - - - {Math.round((selectedAudioVolume ?? 1) * 100)}% - -
- onAudioVolumeChange?.(v)} - formatValue={(v) => `${Math.round(v * 100)}%`} - parseInput={(text) => parseFloat(text.replace(/%$/, "")) / 100} - /> - -
- ) : selectedClipId && hasClipSourceAudio && selectedClipShowSourceAudio ? ( -
-
- {tSettings("audio.sourceTracksTitle", "Clip Source Audio")} -
- {sourceAudioTrackMeta.map((track) => { - const settings = sourceAudioTrackSettings[track.id] ?? { - volume: 1, - normalize: false, - }; - return ( -
-
- - {track.label} - - - {Math.round(settings.volume * 100)}% - -
-
- - {tSettings("audio.normalize", "Normalize")} - - - onSourceAudioTrackNormalizeChange?.(track.id, v) - } - className="data-[state=checked]:bg-[#2563EB] scale-75" - /> -
- onSourceAudioTrackVolumeChange?.(track.id, v)} - formatValue={(v) => `${Math.round(v * 100)}%`} - parseInput={(text) => parseFloat(text.replace(/%$/, "")) / 100} - /> -
- ); - })} -
- ) : null} + {activeEffectSection === "clip" && selectedClipId && ( + + )} + {activeEffectSection === "zoom" && selectedZoomId && ( + + )} + {activeEffectSection === "audio" && selectedAudioId && ( + + )} + {selectedAnnotationId && ( + + )}
); diff --git a/src/components/video-editor/VideoEditor.tsx b/src/components/video-editor/VideoEditor.tsx index dacffaa9..fbc61a63 100644 --- a/src/components/video-editor/VideoEditor.tsx +++ b/src/components/video-editor/VideoEditor.tsx @@ -197,6 +197,7 @@ import { type ZoomMotionBlurTuning, type ZoomRegion, type ZoomTransitionEasing, + type SourceAudioTrackSettings, } from "./types"; import VideoPlayback, { VideoPlaybackRef } from "./VideoPlayback"; import { @@ -664,6 +665,12 @@ export default function VideoEditor() { const [selectedAnnotationId, setSelectedAnnotationId] = useState(null); const [audioRegions, setAudioRegions] = useState([]); const [selectedAudioId, setSelectedAudioId] = useState(null); + const [sourceAudioTrackSettingsByClip, setSourceAudioTrackSettingsByClip] = useState< + Record + >({}); + const [defaultSourceAudioTrackSettings, setDefaultSourceAudioTrackSettings] = useState< + SourceAudioTrackSettings + >({}); const [hasClipSourceAudio, setHasClipSourceAudio] = useState(false); const [autoCaptions, setAutoCaptions] = useState([]); const [autoCaptionSettings, setAutoCaptionSettings] = useState( @@ -1752,6 +1759,8 @@ export default function VideoEditor() { gifFrameRate: GifFrameRate; gifLoop: boolean; gifSizePreset: GifSizePreset; + sourceAudioTrackSettingsByClip: Record; + defaultSourceAudioTrackSettings: SourceAudioTrackSettings; }>, ) => { return editor; @@ -1853,6 +1862,8 @@ export default function VideoEditor() { gifFrameRate, gifLoop, gifSizePreset, + sourceAudioTrackSettingsByClip, + defaultSourceAudioTrackSettings, }), [ buildPersistedEditorState, @@ -1913,6 +1924,8 @@ export default function VideoEditor() { gifLoop, gifSizePreset, frame, + sourceAudioTrackSettingsByClip, + defaultSourceAudioTrackSettings, ], ); @@ -2098,6 +2111,10 @@ export default function VideoEditor() { setSpeedRegions(normalizedEditor.speedRegions); setAnnotationRegions(normalizedEditor.annotationRegions); setAudioRegions(normalizedEditor.audioRegions); + setSourceAudioTrackSettingsByClip(normalizedEditor.sourceAudioTrackSettingsByClip ?? {}); + setDefaultSourceAudioTrackSettings( + normalizedEditor.defaultSourceAudioTrackSettings ?? {}, + ); setAutoCaptions(normalizedEditor.autoCaptions); setAutoCaptionSettings(normalizedEditor.autoCaptionSettings); setAspectRatio(normalizedEditor.aspectRatio); @@ -3314,6 +3331,10 @@ export default function VideoEditor() { clipRegions, audioRegions, effectiveSpeedRegions, + sourceAudioTrackSettingsByClip, + setSourceAudioTrackSettingsByClip, + defaultSourceAudioTrackSettings, + setDefaultSourceAudioTrackSettings, currentTime, timelineTime: timelinePlayheadTime, duration, @@ -3751,6 +3772,7 @@ export default function VideoEditor() { if (id) { setSelectedZoomId(null); setSelectedAnnotationId(null); + setActiveEffectSection("audio"); } }, []); @@ -3768,6 +3790,7 @@ export default function VideoEditor() { setSelectedAudioId(id); setSelectedZoomId(null); setSelectedAnnotationId(null); + setActiveEffectSection("audio"); }, []); const handleAudioSpanChange = useCallback((id: string, span: Span, trackIndex?: number) => { diff --git a/src/components/video-editor/audio/useAudioPreviewSync.ts b/src/components/video-editor/audio/useAudioPreviewSync.ts index b5668194..00b2b3b9 100644 --- a/src/components/video-editor/audio/useAudioPreviewSync.ts +++ b/src/components/video-editor/audio/useAudioPreviewSync.ts @@ -274,13 +274,27 @@ export function useAudioPreviewSync({ : SOURCE_AUDIO_PREVIEW_PAUSED_SEEK_DRIFT_SECONDS; for (const audio of sourceAudioElementsRef.current.values()) { + const sourceAudioPath = audio.dataset.sourceAudioPath ?? ""; + audio.volume = isCurrentClipMuted + ? 0 + : Math.max(0, Math.min(1, previewVolume * getSourceTrackPreviewGain(sourceAudioPath))); + enablePitchPreservingPlayback(audio); const audioDuration = Number.isFinite(audio.duration) ? audio.duration : null; - const startDelaySeconds = estimateCompanionAudioStartDelaySeconds( + const isMicCompanionTrack = /\.mic\./i.test(sourceAudioPath); + const rawStartDelaySeconds = estimateCompanionAudioStartDelaySeconds( duration, audioDuration, - sourceAudioFallbackStartDelayMsByPath[audio.dataset.sourceAudioPath ?? ""], + sourceAudioFallbackStartDelayMsByPath[sourceAudioPath], ); + const maxPreviewStartDelaySeconds = isMicCompanionTrack ? 2 : 5; + const startDelaySeconds = isMicCompanionTrack + ? 0 + : Number.isFinite(duration) && + (rawStartDelaySeconds >= Math.max(0, duration - 0.01) || + rawStartDelaySeconds > Math.max(maxPreviewStartDelaySeconds, duration * 0.9)) + ? 0 + : rawStartDelaySeconds; const beforeAudioStart = currentTime + 0.001 < startDelaySeconds; const targetTime = clampMediaTimeToDuration(currentTime - startDelaySeconds, audioDuration); @@ -317,7 +331,10 @@ export function useAudioPreviewSync({ currentTime, duration, effectiveSpeedRegions, + getSourceTrackPreviewGain, + isCurrentClipMuted, isPlaying, + previewVolume, previewSourceAudioFallbackPaths, sourceAudioFallbackStartDelayMsByPath, ]); diff --git a/src/components/video-editor/audio/useClipAudioSettingsController.ts b/src/components/video-editor/audio/useClipAudioSettingsController.ts index 1873fb52..8c79ac09 100644 --- a/src/components/video-editor/audio/useClipAudioSettingsController.ts +++ b/src/components/video-editor/audio/useClipAudioSettingsController.ts @@ -1,18 +1,31 @@ -import { useCallback, useMemo } from "react"; +import React, { useCallback, useMemo } from "react"; import { SOURCE_AUDIO_NORMALIZE_GAIN, getSourceTrackIdFromPath, } from "./sourceAudioTracks"; import { useSourceAudioTrackSettings } from "./useSourceAudioTrackSettings"; +import { SourceAudioTrackSettings } from "../types"; interface UseClipAudioSettingsControllerParams { selectedClipId: string | null; activeClipId: string | null; + sourceAudioTrackSettingsByClip: Record; + setSourceAudioTrackSettingsByClip: React.Dispatch< + React.SetStateAction> + >; + defaultSourceAudioTrackSettings: SourceAudioTrackSettings; + setDefaultSourceAudioTrackSettings: React.Dispatch< + React.SetStateAction + >; } export function useClipAudioSettingsController({ selectedClipId, activeClipId, + sourceAudioTrackSettingsByClip, + setSourceAudioTrackSettingsByClip, + defaultSourceAudioTrackSettings, + setDefaultSourceAudioTrackSettings, }: UseClipAudioSettingsControllerParams) { const { sourceAudioTrackMeta, @@ -25,25 +38,45 @@ export function useClipAudioSettingsController({ } = useSourceAudioTrackSettings({ selectedClipId, activeClipId, + sourceAudioTrackSettingsByClip, + setSourceAudioTrackSettingsByClip, + defaultSourceAudioTrackSettings, + setDefaultSourceAudioTrackSettings, }); + const previewSourceAudioTrackSettings = useMemo( + () => + activeClipId ? activeSourceAudioTrackSettings : selectedClipSourceAudioTrackSettings, + [activeClipId, activeSourceAudioTrackSettings, selectedClipSourceAudioTrackSettings], + ); + + const embeddedTrackId = useMemo<"mixed" | "system">(() => { + const hasMixedTrack = sourceAudioTrackMeta.some((track) => track.id === "mixed"); + if (hasMixedTrack) return "mixed"; + const hasSystemTrack = sourceAudioTrackMeta.some((track) => track.id === "system"); + return hasSystemTrack ? "system" : "mixed"; + }, [sourceAudioTrackMeta]); + const embeddedSourcePreviewGain = useMemo(() => { - const settings = activeSourceAudioTrackSettings.mixed ?? { volume: 1, normalize: false }; + const settings = previewSourceAudioTrackSettings[embeddedTrackId] ?? { + volume: 1, + normalize: false, + }; const normalizeGain = settings.normalize ? SOURCE_AUDIO_NORMALIZE_GAIN : 1; return Math.max(0, Math.min(2, settings.volume * normalizeGain)); - }, [activeSourceAudioTrackSettings]); + }, [embeddedTrackId, previewSourceAudioTrackSettings]); const getSourceTrackPreviewGain = useCallback( (audioPath: string) => { const trackId = getSourceTrackIdFromPath(audioPath); - const settings = activeSourceAudioTrackSettings[trackId] ?? { + const settings = previewSourceAudioTrackSettings[trackId] ?? { volume: 1, normalize: false, }; const normalizeGain = settings.normalize ? SOURCE_AUDIO_NORMALIZE_GAIN : 1; return Math.max(0, Math.min(2, settings.volume * normalizeGain)); }, - [activeSourceAudioTrackSettings], + [previewSourceAudioTrackSettings], ); return { diff --git a/src/components/video-editor/audio/useSourceAudioTrackSettings.ts b/src/components/video-editor/audio/useSourceAudioTrackSettings.ts index 5a8ca053..34ed0ee3 100644 --- a/src/components/video-editor/audio/useSourceAudioTrackSettings.ts +++ b/src/components/video-editor/audio/useSourceAudioTrackSettings.ts @@ -1,12 +1,17 @@ -import { useCallback, useMemo, useState } from "react"; +import React, { useCallback, useMemo, useState } from "react"; +import type { SourceAudioTrackSettings } from "../types"; -export type SourceAudioTrackSetting = { volume: number; normalize: boolean }; -export type SourceAudioTrackSettings = Record; export type SourceAudioTrackMeta = Array<{ id: string; label: string }>; interface UseSourceAudioTrackSettingsParams { selectedClipId: string | null; activeClipId: string | null; + sourceAudioTrackSettingsByClip: Record; + setSourceAudioTrackSettingsByClip: React.Dispatch< + React.SetStateAction> + >; + defaultSourceAudioTrackSettings: SourceAudioTrackSettings; + setDefaultSourceAudioTrackSettings: React.Dispatch>; } export interface UseSourceAudioTrackSettingsResult { @@ -22,14 +27,12 @@ export interface UseSourceAudioTrackSettingsResult { export function useSourceAudioTrackSettings({ selectedClipId, activeClipId, + sourceAudioTrackSettingsByClip, + setSourceAudioTrackSettingsByClip, + defaultSourceAudioTrackSettings, + setDefaultSourceAudioTrackSettings, }: UseSourceAudioTrackSettingsParams): UseSourceAudioTrackSettingsResult { const [sourceAudioTrackMeta, setSourceAudioTrackMeta] = useState([]); - const [sourceAudioTrackSettingsByClip, setSourceAudioTrackSettingsByClip] = useState< - Record - >({}); - const [defaultSourceAudioTrackSettings, setDefaultSourceAudioTrackSettings] = useState< - SourceAudioTrackSettings - >({}); const activeSourceAudioTrackSettings = useMemo(() => { if (!activeClipId) { diff --git a/src/components/video-editor/audio/useVideoEditorAudio.ts b/src/components/video-editor/audio/useVideoEditorAudio.ts index 02de6a23..d31f7d24 100644 --- a/src/components/video-editor/audio/useVideoEditorAudio.ts +++ b/src/components/video-editor/audio/useVideoEditorAudio.ts @@ -1,6 +1,11 @@ -import { useMemo } from "react"; +import React, { useMemo } from "react"; import { resolveSourceAudioFallbackPaths } from "@/lib/exporter/sourceAudioFallback"; -import type { AudioRegion, ClipRegion, SpeedRegion } from "../types"; +import type { + AudioRegion, + ClipRegion, + SourceAudioTrackSettings, + SpeedRegion, +} from "../types"; import { getActiveClipIdAtSourceTime, isClipMutedById } from "./clipAudio"; import { useAudioPreviewSync } from "./useAudioPreviewSync"; import { useClipAudioSettingsController } from "./useClipAudioSettingsController"; @@ -27,6 +32,14 @@ interface UseVideoEditorAudioParams { clipRegions: ClipRegion[]; audioRegions: AudioRegion[]; effectiveSpeedRegions: SpeedRegion[]; + sourceAudioTrackSettingsByClip: Record; + setSourceAudioTrackSettingsByClip: React.Dispatch< + React.SetStateAction> + >; + defaultSourceAudioTrackSettings: SourceAudioTrackSettings; + setDefaultSourceAudioTrackSettings: React.Dispatch< + React.SetStateAction + >; currentTime: number; timelineTime: number; duration: number; @@ -42,6 +55,10 @@ export function useVideoEditorAudio({ clipRegions, audioRegions, effectiveSpeedRegions, + sourceAudioTrackSettingsByClip, + setSourceAudioTrackSettingsByClip, + defaultSourceAudioTrackSettings, + setDefaultSourceAudioTrackSettings, currentTime, timelineTime, duration, @@ -65,8 +82,12 @@ export function useVideoEditorAudio({ () => resolveSourceAudioFallbackPaths(currentSourcePath, sourceAudioFallbackPaths), [currentSourcePath, sourceAudioFallbackPaths], ); + const hasSystemCompanionPreviewTrack = previewSourceAudioFallbackPaths.some((audioPath) => + audioPath.toLowerCase().includes(".system."), + ); const shouldMutePreviewVideo = - !hasEmbeddedSourceAudio && previewSourceAudioFallbackPaths.length > 0; + previewSourceAudioFallbackPaths.length > 0 && + (!hasEmbeddedSourceAudio || hasSystemCompanionPreviewTrack); const activeClipIdAtCurrentTime = useMemo( () => getActiveClipIdAtSourceTime(currentTime, clipRegions), @@ -90,6 +111,10 @@ export function useVideoEditorAudio({ } = useClipAudioSettingsController({ selectedClipId, activeClipId: activeClipIdAtCurrentTime, + sourceAudioTrackSettingsByClip, + setSourceAudioTrackSettingsByClip, + defaultSourceAudioTrackSettings, + setDefaultSourceAudioTrackSettings, }); useAudioPreviewSync({ diff --git a/src/components/video-editor/projectPersistence.ts b/src/components/video-editor/projectPersistence.ts index a75deb78..49f74348 100644 --- a/src/components/video-editor/projectPersistence.ts +++ b/src/components/video-editor/projectPersistence.ts @@ -62,6 +62,7 @@ import { DEFAULT_ZOOM_SMOOTHNESS, getDefaultCaptionFontFamily, type Padding, + SourceAudioTrackSettings, type SpeedRegion, type TrimRegion, type WebcamOverlaySettings, @@ -127,6 +128,8 @@ export interface ProjectEditorState { autoCaptionSettings: AutoCaptionSettings; webcam: WebcamOverlaySettings; aspectRatio: AspectRatio; + sourceAudioTrackSettingsByClip?: Record; + defaultSourceAudioTrackSettings?: SourceAudioTrackSettings; exportEncodingMode: ExportEncodingMode; exportBackendPreference: ExportBackendPreference; exportPipelineModel: ExportPipelineModel; @@ -496,6 +499,10 @@ export function normalizeProjectEditor(editor: Partial): Pro endMs, speed: isFiniteNumber(region.speed) ? region.speed : 1, muted: typeof region.muted === "boolean" ? region.muted : false, + showSourceAudio: + typeof region.showSourceAudio === "boolean" + ? region.showSourceAudio + : false, }; }) : []; @@ -983,6 +990,16 @@ export function normalizeProjectEditor(editor: Partial): Pro ? clamp(webcam.margin, 0, 96) : DEFAULT_WEBCAM_MARGIN, }, + sourceAudioTrackSettingsByClip: + editor.sourceAudioTrackSettingsByClip && + typeof editor.sourceAudioTrackSettingsByClip === "object" + ? editor.sourceAudioTrackSettingsByClip + : {}, + defaultSourceAudioTrackSettings: + editor.defaultSourceAudioTrackSettings && + typeof editor.defaultSourceAudioTrackSettings === "object" + ? editor.defaultSourceAudioTrackSettings + : {}, aspectRatio: typeof editor.aspectRatio === "string" && (validAspectRatios.has(editor.aspectRatio as AspectRatio) || diff --git a/src/components/video-editor/timeline/TimelineEditor.tsx b/src/components/video-editor/timeline/TimelineEditor.tsx index 8fb8a540..28e83a5f 100644 --- a/src/components/video-editor/timeline/TimelineEditor.tsx +++ b/src/components/video-editor/timeline/TimelineEditor.tsx @@ -291,12 +291,30 @@ const TimelineEditor = forwardRef( const sourceAudioTracks = useMemo>(() => { if (systemSidecarPeaks || micSidecarPeaks) { const tracks: Array<{ id: string; label: string; peaks: AudioPeaksData }> = []; - if (systemSidecarPeaks) tracks.push({ id: "system", label: "Source System", peaks: systemSidecarPeaks }); - if (micSidecarPeaks) tracks.push({ id: "mic", label: "Source Mic", peaks: micSidecarPeaks }); + if (systemSidecarPeaks) + tracks.push({ + id: "system", + label: t("audio.systemLabel", "Source System"), + peaks: systemSidecarPeaks, + }); + if (micSidecarPeaks) + tracks.push({ + id: "mic", + label: t("audio.micLabel", "Source Mic"), + peaks: micSidecarPeaks, + }); return tracks; } - return sourceAudioPeaks ? [{ id: "mixed", label: "Source", peaks: sourceAudioPeaks }] : []; - }, [micSidecarPeaks, sourceAudioPeaks, systemSidecarPeaks]); + return sourceAudioPeaks + ? [ + { + id: "mixed", + label: t("audio.mixedLabel", "Source"), + peaks: sourceAudioPeaks, + }, + ] + : []; + }, [micSidecarPeaks, sourceAudioPeaks, systemSidecarPeaks, t]); useEffect(() => { onSourceAudioTracksMetaChange?.(sourceAudioTracks.map((t) => ({ id: t.id, label: t.label }))); }, [onSourceAudioTracksMetaChange, sourceAudioTracks]); diff --git a/src/components/video-editor/types.ts b/src/components/video-editor/types.ts index ec65c44e..838b4646 100644 --- a/src/components/video-editor/types.ts +++ b/src/components/video-editor/types.ts @@ -64,6 +64,7 @@ export type EditorEffectSection = | "crop" | "extensions" | "clip" + | "audio" | `ext:${string}`; export type ZoomTransitionEasing = "recordly" | "glide" | "smooth" | "snappy" | "linear"; @@ -466,6 +467,13 @@ export const DEFAULT_PADDING: Padding = { linked: true, }; +export interface SourceAudioTrackSetting { + volume: number; + normalize: boolean; +} + +export type SourceAudioTrackSettings = Record; + export interface AudioRegion { id: string; startMs: number; diff --git a/src/i18n/locales/en/settings.json b/src/i18n/locales/en/settings.json index 7b942498..e8ebfebf 100644 --- a/src/i18n/locales/en/settings.json +++ b/src/i18n/locales/en/settings.json @@ -206,6 +206,9 @@ }, "audio": { "sourceTracksTitle": "Clip Source Audio", + "systemLabel": "Source System", + "micLabel": "Source Mic", + "mixedLabel": "Source", "volumeTitle": "Volume", "normalize": "Normalize", "deleteRegion": "Delete Audio" diff --git a/src/i18n/locales/es/settings.json b/src/i18n/locales/es/settings.json index 75f78d75..bec77412 100644 --- a/src/i18n/locales/es/settings.json +++ b/src/i18n/locales/es/settings.json @@ -186,6 +186,9 @@ }, "audio": { "sourceTracksTitle": "Audio fuente del clip", + "systemLabel": "Sonido del sistema", + "micLabel": "Micrófono", + "mixedLabel": "Fuente", "volumeTitle": "Volumen", "normalize": "Normalizar", "deleteRegion": "Eliminar audio" diff --git a/src/i18n/locales/fr/settings.json b/src/i18n/locales/fr/settings.json index 241bc269..ce314c55 100644 --- a/src/i18n/locales/fr/settings.json +++ b/src/i18n/locales/fr/settings.json @@ -186,6 +186,9 @@ }, "audio": { "sourceTracksTitle": "Audio source du clip", + "systemLabel": "Son Système", + "micLabel": "Microphone", + "mixedLabel": "Source", "volumeTitle": "Volume", "normalize": "Normaliser", "deleteRegion": "Supprimer l'audio" diff --git a/src/i18n/locales/zh-CN/settings.json b/src/i18n/locales/zh-CN/settings.json index f895653f..02c3868a 100644 --- a/src/i18n/locales/zh-CN/settings.json +++ b/src/i18n/locales/zh-CN/settings.json @@ -197,6 +197,15 @@ "saveProject": "保存项目", "exportVideo": "导出{{format}}", "reportBug": "报告问题", - "starOnGithub": "在 GitHub 上加星" + "starOnGithub": "在 GitHub 上点赞" + }, + "audio": { + "sourceTracksTitle": "片段原始音频", + "systemLabel": "系统声音", + "micLabel": "麦克风", + "mixedLabel": "来源", + "volumeTitle": "音量", + "normalize": "标准化", + "deleteRegion": "删除音频" } }