diff options
| author | Maze Winther <mazewinther@gmail.com> | 2026-03-31 23:23:36 +0200 |
|---|---|---|
| committer | Maze Winther <mazewinther@gmail.com> | 2026-03-31 23:23:36 +0200 |
| commit | f1d5932c54178c4f8eac61d3d3e0e4a30101780f (patch) | |
| tree | 73ebf833517b1f0a9925151a703f335bedf49216 /apps/web/src/lib/commands/timeline | |
| parent | d23be079cce6108482fc3e85401cccf949087174 (diff) | |
| download | opencut-f1d5932c54178c4f8eac61d3d3e0e4a30101780f.tar.gz | |
feat: separate audio from elements
Diffstat (limited to 'apps/web/src/lib/commands/timeline')
| -rw-r--r-- | apps/web/src/lib/commands/timeline/element/index.ts | 1 | ||||
| -rw-r--r-- | apps/web/src/lib/commands/timeline/element/toggle-source-audio-separation.ts | 143 |
2 files changed, 144 insertions, 0 deletions
diff --git a/apps/web/src/lib/commands/timeline/element/index.ts b/apps/web/src/lib/commands/timeline/element/index.ts index a7e513fe..5ce48d55 100644 --- a/apps/web/src/lib/commands/timeline/element/index.ts +++ b/apps/web/src/lib/commands/timeline/element/index.ts @@ -8,6 +8,7 @@ export { SplitElementsCommand } from "./split-elements"; export { UpdateElementCommand } from "./update-element";
export { ToggleElementsVisibilityCommand } from "./toggle-elements-visibility";
export { ToggleElementsMutedCommand } from "./toggle-elements-muted";
+export { ToggleSourceAudioSeparationCommand } from "./toggle-source-audio-separation";
export { MoveElementCommand } from "./move-elements";
export * from "./keyframes";
export * from "./effects";
diff --git a/apps/web/src/lib/commands/timeline/element/toggle-source-audio-separation.ts b/apps/web/src/lib/commands/timeline/element/toggle-source-audio-separation.ts new file mode 100644 index 00000000..d0907c5b --- /dev/null +++ b/apps/web/src/lib/commands/timeline/element/toggle-source-audio-separation.ts @@ -0,0 +1,143 @@ +import { EditorCore } from "@/core"; +import { Command } from "@/lib/commands/base-command"; +import { + buildSeparatedAudioElement, + canExtractSourceAudio, + canRecoverSourceAudio, +} from "@/lib/timeline/audio-separation"; +import { + applyPlacement, + resolveTrackPlacement, +} from "@/lib/timeline/placement"; +import { updateElementInTracks } from "@/lib/timeline/track-element-update"; +import type { TimelineTrack, VideoElement } from "@/lib/timeline/types"; +import { generateUUID } from "@/utils/id"; + +export class ToggleSourceAudioSeparationCommand extends Command { + private savedState: TimelineTrack[] | null = null; + + constructor( + private readonly params: { + trackId: string; + elementId: string; + }, + ) { + super(); + } + + execute(): void { + const editor = EditorCore.getInstance(); + this.savedState = editor.timeline.getTracks(); + + const trackIndex = this.savedState.findIndex( + (track) => track.id === this.params.trackId, + ); + if (trackIndex < 0) { + return; + } + + const sourceTrack = this.savedState[trackIndex]; + const sourceElement = sourceTrack.elements.find( + (element) => element.id === this.params.elementId, + ); + if (!sourceElement) { + return; + } + + if (canRecoverSourceAudio({ element: sourceElement })) { + 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) => + sourceElement.type === "video" ? asset.id === sourceElement.mediaId : false, + ); + if (!canExtractSourceAudio({ element: sourceElement, mediaAsset })) { + return; + } + if (sourceElement.duration <= 0) { + return; + } + + const separatedAudioElement = { + ...buildSeparatedAudioElement({ + sourceElement, + }), + id: generateUUID(), + }; + const placementResult = resolveTrackPlacement({ + tracks: this.savedState, + trackType: "audio", + timeSpans: [ + { + startTime: separatedAudioElement.startTime, + duration: separatedAudioElement.duration, + }, + ], + strategy: { type: "aboveSource", sourceTrackIndex: trackIndex }, + }); + 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: TimelineTrack[]; + trackId: string; + elementId: string; + isSourceAudioEnabled: boolean; +}): TimelineTrack[] { + return updateElementInTracks({ + tracks, + trackId, + elementId, + elementPredicate: (element): element is VideoElement => element.type === "video", + update: (element) => ({ + ...element, + isSourceAudioEnabled, + }), + }); +} |
