summaryrefslogtreecommitdiff
path: root/apps/web/src/commands/timeline/element/toggle-source-audio-separation.ts
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-04-20 11:31:17 +0200
committerMaze Winther <mazewinther@gmail.com>2026-04-20 11:31:17 +0200
commit3e89d2998588f8e35c524e523ba2ee853f0f6475 (patch)
tree57221f96c1259b3b07e3ddf8c1adefc502b9d8eb /apps/web/src/commands/timeline/element/toggle-source-audio-separation.ts
parent729d10592f4e802f91c555c309ff3ba4b119f843 (diff)
downloadopencut-3e89d2998588f8e35c524e523ba2ee853f0f6475.tar.gz
refactor: restructure files to their domains, new preview overlay system, and dep graph
Diffstat (limited to 'apps/web/src/commands/timeline/element/toggle-source-audio-separation.ts')
-rw-r--r--apps/web/src/commands/timeline/element/toggle-source-audio-separation.ts145
1 files changed, 145 insertions, 0 deletions
diff --git a/apps/web/src/commands/timeline/element/toggle-source-audio-separation.ts b/apps/web/src/commands/timeline/element/toggle-source-audio-separation.ts
new file mode 100644
index 00000000..36d4cd87
--- /dev/null
+++ b/apps/web/src/commands/timeline/element/toggle-source-audio-separation.ts
@@ -0,0 +1,145 @@
+import { EditorCore } from "@/core";
+import { Command, type CommandResult } from "@/commands/base-command";
+import {
+ buildSeparatedAudioElement,
+ canExtractSourceAudio,
+ isSourceAudioSeparated,
+} from "@/timeline/audio-separation";
+import {
+ applyPlacement,
+ resolveTrackPlacement,
+} from "@/timeline/placement";
+import { updateElementInSceneTracks } from "@/timeline/track-element-update";
+import type {
+ SceneTracks,
+ TimelineElement,
+ VideoElement,
+} from "@/timeline/types";
+import { generateUUID } from "@/utils/id";
+
+export class ToggleSourceAudioSeparationCommand extends Command {
+ private savedState: SceneTracks | null = null;
+
+ constructor(
+ private readonly params: {
+ trackId: string;
+ elementId: string;
+ },
+ ) {
+ super();
+ }
+
+ execute(): CommandResult | undefined {
+ const editor = EditorCore.getInstance();
+ this.savedState = editor.scenes.getActiveScene().tracks;
+
+ const sourceTrack = [
+ ...this.savedState.overlay,
+ this.savedState.main,
+ ...this.savedState.audio,
+ ].find((track) => track.id === this.params.trackId);
+ if (!sourceTrack) {
+ return;
+ }
+ const sourceElement = sourceTrack.elements.find(
+ (element) => element.id === this.params.elementId,
+ ) as TimelineElement | undefined;
+ if (!sourceElement || sourceElement.type !== "video") {
+ return;
+ }
+ const videoElement: VideoElement = sourceElement;
+
+ if (isSourceAudioSeparated({ element: videoElement })) {
+ editor.timeline.updateTracks(
+ updateSourceAudioEnabled({
+ tracks: this.savedState,
+ trackId: this.params.trackId,
+ elementId: this.params.elementId,
+ isSourceAudioEnabled: true,
+ }),
+ );
+ return;
+ }
+
+ const mediaAsset = editor.media
+ .getAssets()
+ .find((asset) => asset.id === videoElement.mediaId);
+ if (!canExtractSourceAudio(videoElement, mediaAsset)) {
+ return;
+ }
+ if (videoElement.duration <= 0) {
+ return;
+ }
+
+ const separatedAudioElement = {
+ ...buildSeparatedAudioElement({
+ sourceElement: videoElement,
+ }),
+ id: generateUUID(),
+ };
+ const placementResult = resolveTrackPlacement({
+ tracks: this.savedState,
+ trackType: "audio",
+ timeSpans: [
+ {
+ startTime: separatedAudioElement.startTime,
+ duration: separatedAudioElement.duration,
+ },
+ ],
+ strategy: { type: "firstAvailable" },
+ });
+ if (!placementResult) {
+ return;
+ }
+ const appliedPlacement = applyPlacement({
+ tracks: this.savedState,
+ placementResult,
+ elements: [separatedAudioElement],
+ });
+ if (!appliedPlacement) {
+ return;
+ }
+
+ editor.timeline.updateTracks(
+ updateSourceAudioEnabled({
+ tracks: appliedPlacement.updatedTracks,
+ trackId: this.params.trackId,
+ elementId: this.params.elementId,
+ isSourceAudioEnabled: false,
+ }),
+ );
+ }
+
+ undo(): void {
+ if (!this.savedState) {
+ return;
+ }
+
+ const editor = EditorCore.getInstance();
+ editor.timeline.updateTracks(this.savedState);
+ }
+}
+
+function updateSourceAudioEnabled({
+ tracks,
+ trackId,
+ elementId,
+ isSourceAudioEnabled,
+}: {
+ tracks: SceneTracks;
+ trackId: string;
+ elementId: string;
+ isSourceAudioEnabled: boolean;
+}): SceneTracks {
+ return updateElementInSceneTracks({
+ tracks,
+ trackId,
+ elementId,
+ elementPredicate: (element): element is VideoElement =>
+ element.type === "video",
+ update: (element) => ({
+ ...element,
+ isSourceAudioEnabled,
+ }),
+ });
+}