summaryrefslogtreecommitdiff
path: root/apps/web/src/lib/timeline/audio-separation/index.ts
diff options
context:
space:
mode:
authorMaze Winther <mazewinther@gmail.com>2026-03-31 23:23:36 +0200
committerMaze Winther <mazewinther@gmail.com>2026-03-31 23:23:36 +0200
commitf1d5932c54178c4f8eac61d3d3e0e4a30101780f (patch)
tree73ebf833517b1f0a9925151a703f335bedf49216 /apps/web/src/lib/timeline/audio-separation/index.ts
parentd23be079cce6108482fc3e85401cccf949087174 (diff)
downloadopencut-f1d5932c54178c4f8eac61d3d3e0e4a30101780f.tar.gz
feat: separate audio from elements
Diffstat (limited to 'apps/web/src/lib/timeline/audio-separation/index.ts')
-rw-r--r--apps/web/src/lib/timeline/audio-separation/index.ts133
1 files changed, 133 insertions, 0 deletions
diff --git a/apps/web/src/lib/timeline/audio-separation/index.ts b/apps/web/src/lib/timeline/audio-separation/index.ts
new file mode 100644
index 00000000..b92cb79b
--- /dev/null
+++ b/apps/web/src/lib/timeline/audio-separation/index.ts
@@ -0,0 +1,133 @@
+import { cloneAnimations, getChannel } from "@/lib/animation";
+import type { ElementAnimations } from "@/lib/animation/types";
+import type { MediaAsset } from "@/lib/media/types";
+import { DEFAULTS } from "@/lib/timeline/defaults";
+import type {
+ CreateUploadAudioElement,
+ TimelineElement,
+ AudioElement,
+ VideoElement,
+} from "../types";
+
+export function isSourceAudioEnabled({
+ element,
+}: {
+ element: VideoElement;
+}): boolean {
+ return element.isSourceAudioEnabled !== false;
+}
+
+export function isSourceAudioSeparated({
+ element,
+}: {
+ element: VideoElement;
+}): boolean {
+ return !isSourceAudioEnabled({ element });
+}
+
+export function canExtractSourceAudio({
+ element,
+ mediaAsset,
+}: {
+ element: TimelineElement;
+ mediaAsset: MediaAsset | null | undefined;
+}): element is VideoElement {
+ return (
+ element.type === "video" &&
+ isSourceAudioEnabled({ element }) &&
+ !!mediaAsset &&
+ mediaAsset.hasAudio !== false
+ );
+}
+
+export function canRecoverSourceAudio({
+ element,
+}: {
+ element: TimelineElement;
+}): element is VideoElement {
+ return element.type === "video" && isSourceAudioSeparated({ element });
+}
+
+export function canToggleSourceAudio({
+ element,
+ mediaAsset,
+}: {
+ element: TimelineElement;
+ mediaAsset: MediaAsset | null | undefined;
+}): element is VideoElement {
+ return (
+ canRecoverSourceAudio({ element }) ||
+ canExtractSourceAudio({ element, mediaAsset })
+ );
+}
+
+export function doesElementHaveEnabledAudio({
+ element,
+ mediaAsset,
+}: {
+ element: AudioElement | VideoElement;
+ mediaAsset?: MediaAsset | null;
+}): boolean {
+ if (element.type === "audio") {
+ return true;
+ }
+
+ return !!mediaAsset && mediaAsset.hasAudio !== false && isSourceAudioEnabled({ element });
+}
+
+export function buildSeparatedAudioElement({
+ sourceElement,
+}: {
+ sourceElement: VideoElement;
+}): CreateUploadAudioElement {
+ return {
+ type: "audio",
+ sourceType: "upload",
+ mediaId: sourceElement.mediaId,
+ name: sourceElement.name,
+ duration: sourceElement.duration,
+ startTime: sourceElement.startTime,
+ trimStart: sourceElement.trimStart,
+ trimEnd: sourceElement.trimEnd,
+ sourceDuration: sourceElement.sourceDuration,
+ volume: sourceElement.volume ?? DEFAULTS.element.volume,
+ muted: sourceElement.muted ?? false,
+ retime: sourceElement.retime
+ ? {
+ rate: sourceElement.retime.rate,
+ maintainPitch: sourceElement.retime.maintainPitch,
+ }
+ : undefined,
+ animations: cloneVolumeAnimations({
+ animations: sourceElement.animations,
+ }),
+ };
+}
+
+export function getSourceAudioActionLabel({
+ element,
+}: {
+ element: VideoElement;
+}): "Extract audio" | "Recover audio" {
+ return isSourceAudioSeparated({ element }) ? "Recover audio" : "Extract audio";
+}
+
+function cloneVolumeAnimations({
+ animations,
+}: {
+ animations: ElementAnimations | undefined;
+}): ElementAnimations | undefined {
+ const volumeChannel = getChannel({ animations, propertyPath: "volume" });
+ if (!volumeChannel) {
+ return undefined;
+ }
+
+ return cloneAnimations({
+ animations: {
+ channels: {
+ volume: volumeChannel,
+ },
+ },
+ shouldRegenerateKeyframeIds: true,
+ });
+}