OpenCut/apps/web/src/lib/commands/timeline/element/toggle-source-audio-separat...

146 lines
3.3 KiB
TypeScript

import { EditorCore } from "@/core";
import { Command, type CommandResult } from "@/lib/commands/base-command";
import {
buildSeparatedAudioElement,
canExtractSourceAudio,
isSourceAudioSeparated,
} from "@/lib/timeline/audio-separation";
import {
applyPlacement,
resolveTrackPlacement,
} from "@/lib/timeline/placement";
import { updateElementInSceneTracks } from "@/lib/timeline/track-element-update";
import type {
SceneTracks,
TimelineElement,
VideoElement,
} from "@/lib/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,
}),
});
}