Files
OpenCut/apps/web/src/timeline/audio-state.ts
T

134 lines
2.9 KiB
TypeScript
Raw Normal View History

import { hasKeyframesForPath } from "@/animation/keyframe-query";
2026-04-26 04:04:30 +02:00
import { resolveNumberAtTime } from "@/animation/values";
import { VOLUME_DB_MAX, VOLUME_DB_MIN } from "./audio-constants";
import type { TimelineElement } from "./types";
const DEFAULT_STEP_SECONDS = 1 / 60;
export type AudioCapableElement = Extract<
TimelineElement,
{ type: "audio" | "video" }
>;
export function clampDb(value: number): number {
if (!Number.isFinite(value)) {
return 0;
}
return Math.min(VOLUME_DB_MAX, Math.max(VOLUME_DB_MIN, value));
}
export function dBToLinear(db: number): number {
return 10 ** (clampDb(db) / 20);
}
export function getElementVolume({
element,
}: {
element: AudioCapableElement;
}): number {
const value = element.params.volume;
return typeof value === "number" ? value : 0;
}
export function isElementMuted({
element,
}: {
element: AudioCapableElement;
}): boolean {
return element.params.muted === true;
}
export function hasAnimatedVolume({
element,
}: {
element: AudioCapableElement;
}): boolean {
return hasKeyframesForPath({
animations: element.animations,
propertyPath: "volume",
});
}
import { TICKS_PER_SECOND } from "@/wasm";
export function resolveEffectiveAudioGain({
element,
trackMuted = false,
localTime,
}: {
element: AudioCapableElement;
trackMuted?: boolean;
localTime: number;
}): number {
if (trackMuted || isElementMuted({ element })) {
return 0;
}
const resolvedDb = resolveNumberAtTime({
baseValue: getElementVolume({ element }),
animations: element.animations,
propertyPath: "volume",
localTime: Math.round(localTime * TICKS_PER_SECOND),
});
return dBToLinear(resolvedDb);
}
export function buildWaveformGainSamples({
element,
count,
}: {
element: AudioCapableElement;
count: number;
}): number[] {
const durationSeconds = element.duration / TICKS_PER_SECOND;
return Array.from({ length: count }, (_, i) => {
const localTime = ((i + 0.5) / count) * durationSeconds;
return resolveEffectiveAudioGain({ element, localTime });
});
}
export function buildAudioGainAutomation({
element,
trackMuted = false,
fromLocalTime,
toLocalTime,
stepSeconds = DEFAULT_STEP_SECONDS,
}: {
element: AudioCapableElement;
trackMuted?: boolean;
fromLocalTime: number;
toLocalTime: number;
stepSeconds?: number;
}): Array<{ localTime: number; gain: number }> {
const startTime = Math.max(0, fromLocalTime);
const endTime = Math.max(startTime, toLocalTime);
const safeStep =
Number.isFinite(stepSeconds) && stepSeconds > 0
? stepSeconds
: DEFAULT_STEP_SECONDS;
const points: Array<{ localTime: number; gain: number }> = [];
for (let localTime = startTime; localTime < endTime; localTime += safeStep) {
points.push({
localTime,
gain: resolveEffectiveAudioGain({
element,
trackMuted,
localTime,
}),
});
}
points.push({
localTime: endTime,
gain: resolveEffectiveAudioGain({
element,
trackMuted,
localTime: endTime,
}),
});
return points;
}