summaryrefslogtreecommitdiff
path: root/apps/web/src/components/editor/panels/timeline
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/src/components/editor/panels/timeline')
-rw-r--r--apps/web/src/components/editor/panels/timeline/audio-waveform.tsx48
-rw-r--r--apps/web/src/components/editor/panels/timeline/timeline-element.tsx18
2 files changed, 61 insertions, 5 deletions
diff --git a/apps/web/src/components/editor/panels/timeline/audio-waveform.tsx b/apps/web/src/components/editor/panels/timeline/audio-waveform.tsx
index a481f1c2..430575e1 100644
--- a/apps/web/src/components/editor/panels/timeline/audio-waveform.tsx
+++ b/apps/web/src/components/editor/panels/timeline/audio-waveform.tsx
@@ -9,9 +9,34 @@ import { cn } from "@/utils/ui";
const BAR_WIDTH = 2;
const BAR_GAP = 1;
const BAR_STEP = BAR_WIDTH + BAR_GAP;
+export const WAVEFORM_GAIN_SAMPLE_COUNT = 200;
+
+function sampleGain({
+ samples,
+ startFraction,
+ endFraction,
+ barIndex,
+ barCount,
+}: {
+ samples: number[];
+ startFraction: number;
+ endFraction: number;
+ barIndex: number;
+ barCount: number;
+}): number {
+ if (samples.length === 0) return 1;
+ const progress =
+ startFraction +
+ ((barIndex + 0.5) / barCount) * (endFraction - startFraction);
+ const rawIndex = Math.max(0, Math.min(1, progress)) * (samples.length - 1);
+ const lo = Math.floor(rawIndex);
+ const hi = Math.min(samples.length - 1, lo + 1);
+ return samples[lo] + (samples[hi] - samples[lo]) * (rawIndex - lo);
+}
interface AudioWaveformProps {
audioUrl?: string;
audioBuffer?: AudioBuffer;
+ gainSamples?: number[];
color?: string;
className?: string;
}
@@ -19,6 +44,7 @@ interface AudioWaveformProps {
export function AudioWaveform({
audioUrl,
audioBuffer,
+ gainSamples,
color = "rgba(255, 255, 255, 0.7)",
className = "",
}: AudioWaveformProps) {
@@ -26,6 +52,7 @@ export function AudioWaveform({
const containerRef = useRef<HTMLDivElement>(null);
const bufferRef = useRef<AudioBuffer | null>(null);
const globalMaxRef = useRef<number>(1);
+ const gainSamplesRef = useRef<number[] | undefined>(gainSamples);
const scrollParentRef = useRef<HTMLElement | null>(null);
const heightRef = useRef<number>(0);
@@ -99,20 +126,37 @@ export function AudioWaveform({
const maxBarHeight = height * 0.7;
+ const samples = gainSamplesRef.current;
for (let i = 0; i < barCount; i++) {
- const scaled = Math.log1p(peaks[i]) / Math.log1p(1);
+ const gain =
+ samples != null
+ ? sampleGain({
+ samples,
+ startFraction,
+ endFraction,
+ barIndex: i,
+ barCount,
+ })
+ : 1;
+ const scaledPeak = Math.min(1, Math.max(0, peaks[i] * gain));
+ const scaled = Math.log1p(scaledPeak) / Math.log1p(1);
const barH = Math.max(1, scaled * maxBarHeight);
ctx.fillRect(i * BAR_STEP, height - barH, BAR_WIDTH, barH);
}
}, [color]);
useEffect(() => {
+ gainSamplesRef.current = gainSamples;
+ drawVisible();
+ }, [gainSamples, drawVisible]);
+
+ useEffect(() => {
let isCancelled = false;
async function load() {
let buffer = audioBuffer ?? null;
- if (!buffer && audioUrl) {
+ if (!buffer && audioUrl) {
try {
const resp = await fetch(audioUrl);
const arrayBuffer = await resp.arrayBuffer();
diff --git a/apps/web/src/components/editor/panels/timeline/timeline-element.tsx b/apps/web/src/components/editor/panels/timeline/timeline-element.tsx
index 644dbc56..9283c651 100644
--- a/apps/web/src/components/editor/panels/timeline/timeline-element.tsx
+++ b/apps/web/src/components/editor/panels/timeline/timeline-element.tsx
@@ -2,7 +2,7 @@
import { useEditor } from "@/hooks/use-editor";
import { useAssetsPanelStore } from "@/stores/assets-panel-store";
-import { AudioWaveform } from "./audio-waveform";
+import { AudioWaveform, WAVEFORM_GAIN_SAMPLE_COUNT } from "./audio-waveform";
import { useElementPreview } from "@/hooks/use-element-preview";
import {
useKeyframeDrag,
@@ -44,6 +44,7 @@ import {
getSourceAudioActionLabel,
isSourceAudioSeparated,
} from "@/lib/timeline/audio-separation";
+import { buildWaveformGainSamples } from "@/lib/timeline/audio-state";
import {
getActionDefinition,
type TAction,
@@ -382,6 +383,7 @@ export function TimelineElement({
>
<ElementInner
element={element}
+ displayElement={renderElement}
track={track}
isSelected={isSelected}
isExpanded={expandedRows.length > 0}
@@ -498,6 +500,7 @@ export function TimelineElement({
function ElementInner({
element,
+ displayElement,
track,
isSelected,
isExpanded,
@@ -509,6 +512,7 @@ function ElementInner({
isDropTarget = false,
}: {
element: TimelineElementType;
+ displayElement?: TimelineElementType;
track: TimelineTrack;
isSelected: boolean;
isExpanded: boolean;
@@ -530,8 +534,10 @@ function ElementInner({
}) => void;
isDropTarget?: boolean;
}) {
+ const visibleElement = displayElement ?? element;
const isReducedOpacity =
- (canElementBeHidden(element) && element.hidden) || isDropTarget;
+ (canElementBeHidden(visibleElement) && visibleElement.hidden) ||
+ isDropTarget;
return (
<div
className="absolute top-0 bottom-0"
@@ -576,7 +582,7 @@ function ElementInner({
style={{ height: `${baseTrackHeight}px` }}
>
<div className="flex flex-1 min-h-0 h-full items-center overflow-hidden">
- <ElementContent element={element} track={track} />
+ <ElementContent element={visibleElement} track={track} />
</div>
</div>
{expandedContent}
@@ -979,6 +985,11 @@ function AudioElementContent({ element }: { element: AudioElement }) {
const audioUrl =
element.sourceType === "library" ? element.sourceUrl : mediaAsset?.url;
const mediaLabel = mediaAsset?.name ?? element.name;
+ const gainSamples = useMemo(
+ () =>
+ buildWaveformGainSamples({ element, count: WAVEFORM_GAIN_SAMPLE_COUNT }),
+ [element],
+ );
if (audioBuffer || audioUrl) {
return (
@@ -986,6 +997,7 @@ function AudioElementContent({ element }: { element: AudioElement }) {
<AudioWaveform
audioBuffer={audioBuffer}
audioUrl={audioUrl}
+ gainSamples={gainSamples}
color={TIMELINE_TRACK_THEME.audio.waveformColor}
/>
<MediaElementHeader name={mediaLabel} hasFade={false} />