Skip to content

Commit

Permalink
minor update to waveform
Browse files Browse the repository at this point in the history
  • Loading branch information
sphinxrave committed Nov 24, 2024
1 parent 3c6a86d commit 9da97f7
Show file tree
Hide file tree
Showing 7 changed files with 233 additions and 97 deletions.
10 changes: 10 additions & 0 deletions packages/react/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
"dayjs": "^1.11.10",
"history": "^5.3.0",
"i18next": "^23.7.6",
"i18next-browser-languagedetector": "^7.2.0",
"i18next-chained-backend": "^4.6.2",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,8 +158,47 @@ export const waveformGeneratorStateAtom = atom<WaveformGeneratorState>({
format: null,
});

// Utility function to get min value from array of tuples
const getMin = (data: [number, number][]) => {
return Math.min(...data.map(([_, value]) => value));
};

// Utility function to get max value from array of tuples
const getMax = (data: [number, number][]) => {
return Math.max(...data.map(([_, value]) => value));
};

// <time, negative db loudness>
export const waveformAtom = atom<[number, number][]>([]);

// <time, normalized loudness>
export const normalizedLoudnessAtom = atom<[number, number][]>((get) => {
const waveform = get(waveformAtom);

// If no data, return empty array
if (waveform.length === 0) return [];

const EPSILON = 1e-10;

// Convert each RMS dB value to amplitude
// amplitude = 10^((dB + epsilon) / 20)
const amplitudes = waveform.map(([time, db]) => {
const amplitude = Math.pow(10, (db + EPSILON) / 20);
return [time, amplitude] as [number, number];
});

// Get min and max for normalization
const minAmplitude = getMin(amplitudes);
const maxAmplitude = getMax(amplitudes);
const range = maxAmplitude - minAmplitude + EPSILON; // Add epsilon to prevent division by zero

// Normalize values to 0-1 range
return amplitudes.map(([time, amplitude]) => {
const normalized = (amplitude - minAmplitude) / range;
return [time, normalized] as [number, number];
});
});

// Action atom to trigger waveform generation

export const generateWaveformAtom = atom(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import React from "react";
import { Rnd } from "react-rnd";
import { useSpecificSubtitle } from "../hooks/subtitles";

/**
* Properties for the RndSubtitle component.
*
* @property {string} subtitleId Unique identifier for the subtitle.
* @property {number} startTime Start time of the timeline range for the subtitle.
* @property {number} endTime End time of the timeline range for the subtitle.
* @property {number} containerWidth Width of the container in which the subtitle is displayed.
*/
interface RndSubtitleProps {
subtitleId: string;
startTime: number;
endTime: number;
containerWidth: number;
}
/**
* RndSubtitle is a resizable and draggable subtitle component for editing video subtitles.
* It utilizes the react-rnd library to allow users to adjust the position
* and duration of a subtitle by dragging or resizing the subtitle box.
*
* Props:
* - subtitleId: Unique identifier for the subtitle.
* - startTime: Start time of the timeline range for the subtitle.
* - endTime: End time of the timeline range for the subtitle.
* - containerWidth: Width of the container in which the subtitle is displayed.
*
* The component provides internal functions to convert time to position and vice versa,
* and handlers to update subtitle start time and duration upon dragging or resizing.
*/
export const RndSubtitle: React.FC<RndSubtitleProps> = ({
subtitleId,
startTime,
endTime,
containerWidth,
}) => {
const [subtitle, edit] = useSpecificSubtitle(subtitleId);

const timeToPosition = (time: number) => {
const timeRange = endTime - startTime;
const position = ((time - startTime) / timeRange) * containerWidth;
return position;
};

const positionToTime = (position: number) => {
const timeRange = endTime - startTime;
const time = (position / containerWidth) * timeRange + startTime;
return Math.max(startTime, Math.min(time, endTime));
};

const handleDrag = (_: unknown, d: { x: number; y: number }) => {
const newStartTime = positionToTime(d.x);
edit({
type: "UPDATE_SUBTITLE",
payload: {
...subtitle,
video_offset: newStartTime,
end: newStartTime + subtitle.duration / 1000,
},
});
};

const handleResize = (_: unknown, _direction: string, ref: HTMLElement) => {
const newWidth = ref.offsetWidth;
const newDuration =
(newWidth / containerWidth) * (endTime - startTime) * 1000;
edit({
type: "UPDATE_SUBTITLE",
payload: {
...subtitle,
duration: newDuration,
end: subtitle.video_offset + newDuration / 1000,
},
});
};

return (
<Rnd
position={{
x: timeToPosition(subtitle.video_offset),
y: 15,
}}
size={{
width:
timeToPosition(
subtitle.video_offset + (subtitle.duration || 3000) / 1000,
) - timeToPosition(subtitle.video_offset),
height: 30,
}}
onDragStop={handleDrag}
onResize={handleResize}
bounds="parent"
className="border border-x-2 border-primary-9"
enableResizing={{ left: true, right: true }}
>
<div
className="h-full w-full cursor-move text-wrap bg-base-3 text-base-12 opacity-80 bg-blend-multiply"
title={subtitle.message}
>
<span className="truncate text-xs text-white">{subtitle.message}</span>
</div>
</Rnd>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,14 @@ import { useTimelineRendererBase } from "../hooks/timeline";
import { playerRefAtom, videoStatusAtomFamily } from "@/store/player";
import { useAtomValue } from "jotai";

import { waveformAtom } from "../atoms/waveformAtoms";
import { normalizedLoudnessAtom } from "../atoms/waveformAtoms";
import "./WaveformEditor.scss";

import React from "react";
import { Rnd } from "react-rnd";
import { subtitleManagerAtom, useSpecificSubtitle } from "../hooks/subtitles";
import { subtitleManagerAtom } from "../hooks/subtitles";
import { RndSubtitle } from "./RndSubtitle";

export const WaveformEditor = ({ videoId }: { videoId: string }) => {
const waveform = useAtomValue(waveformAtom);
const waveform = useAtomValue(normalizedLoudnessAtom);
const player = useAtomValue(playerRefAtom);
const videoStatusAtom = videoStatusAtomFamily(videoId);
const videoStatus = useAtomValue(videoStatusAtom);
Expand Down Expand Up @@ -71,85 +70,3 @@ export const WaveformEditor = ({ videoId }: { videoId: string }) => {
</div>
);
};

interface RndSubtitleProps {
subtitleId: string;
startTime: number;
endTime: number;
containerWidth: number;
}

export const RndSubtitle: React.FC<RndSubtitleProps> = ({
subtitleId,
startTime,
endTime,
containerWidth,
}) => {
const [subtitle, edit] = useSpecificSubtitle(subtitleId);

const timeToPosition = (time: number) => {
const timeRange = endTime - startTime;
const position = ((time - startTime) / timeRange) * containerWidth;
return position;
};

const positionToTime = (position: number) => {
const timeRange = endTime - startTime;
const time = (position / containerWidth) * timeRange + startTime;
return Math.max(startTime, Math.min(time, endTime));
};

const handleDrag = (_: unknown, d: { x: number; y: number }) => {
const newStartTime = positionToTime(d.x);
edit({
type: "UPDATE_SUBTITLE",
payload: {
...subtitle,
video_offset: newStartTime,
end: newStartTime + subtitle.duration / 1000,
},
});
};

const handleResize = (_: unknown, _direction: string, ref: HTMLElement) => {
const newWidth = ref.offsetWidth;
const newDuration =
(newWidth / containerWidth) * (endTime - startTime) * 1000;
edit({
type: "UPDATE_SUBTITLE",
payload: {
...subtitle,
duration: newDuration,
end: subtitle.video_offset + newDuration / 1000,
},
});
};

return (
<Rnd
position={{
x: timeToPosition(subtitle.video_offset),
y: 15,
}}
size={{
width:
timeToPosition(
subtitle.video_offset + (subtitle.duration || 3000) / 1000,
) - timeToPosition(subtitle.video_offset),
height: 30,
}}
onDragStop={handleDrag}
onResize={handleResize}
bounds="parent"
className="border border-x-2 border-primary-9"
enableResizing={{ left: true, right: true }}
>
<div
className="h-full w-full cursor-move text-wrap bg-base-3 text-base-12 opacity-80 bg-blend-multiply"
title={subtitle.message}
>
<span className="truncate text-xs text-white">{subtitle.message}</span>
</div>
</Rnd>
);
};
20 changes: 10 additions & 10 deletions packages/react/src/components/tldex/new-editor/frame.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useCallback, useEffect, useState } from "react";
import "./frame.css";
import { useBeforeUnload, useBlocker, useNavigate } from "react-router-dom";
import { useBeforeUnload, useNavigate } from "react-router-dom";
import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels";
import { PlayerWrapper } from "@/components/layout/PlayerWrapper";
import { idToVideoURL } from "@/lib/utils";
Expand All @@ -17,12 +17,13 @@ import { clientAtom } from "@/hooks/useClient";
import { useQuery } from "@tanstack/react-query";
import { getSubtitlesForVideo, subtitleManagerAtom } from "./hooks/subtitles";
import { TLEditorHeader } from "./TLEditorHeader";
import { useNavBlocker } from "@/hooks/useBlock";

export function TLEditorFrame() {
const {
id,
currentVideo,
isPending: isVideoPending,
isPending: isVideoPending, // download progress pending
editorLanguage,
} = useScriptEditorParams();
const makeHeaderHide = useSetAtom(headerHiddenAtom);
Expand Down Expand Up @@ -60,20 +61,19 @@ export function TLEditorFrame() {
}, [isSuccess, script]);

// Prevent soft navigation within the SPA
const blocker = useBlocker(isBlocking);

useEffect(() => {
if (blocker.state === "blocked") {
useNavBlocker({
enabled: isBlocking,
onBlock(control) {
const proceed = window.confirm(
"Are you sure you want to leave? You may lose unsaved changes.",
);
if (proceed) {
blocker.proceed();
control.confirm();
} else {
blocker.reset();
control.cancel();
}
}
}, [blocker]);
},
});

// Prevent hard refresh and closing tab
const handleBeforeUnload = useCallback(
Expand Down
Loading

0 comments on commit 9da97f7

Please sign in to comment.