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": "删除音频"
}
}