@ossclip/scenes 0.1.34 → 0.1.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/SfxTrack.tsx +50 -0
- package/src/index.ts +2 -0
- package/src/sfx-track.ts +80 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ossclip/scenes",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.35",
|
|
4
4
|
"description": "ossclip's scene library and stage geometry — React components shared by preview and render",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
],
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"zod": "^3.25.0",
|
|
22
|
-
"@ossclip/core": "0.1.
|
|
22
|
+
"@ossclip/core": "0.1.35"
|
|
23
23
|
},
|
|
24
24
|
"peerDependencies": {
|
|
25
25
|
"react": ">=18",
|
package/src/SfxTrack.tsx
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Audio, Sequence, staticFile, useVideoConfig } from "remotion";
|
|
3
|
+
import { visibleSfxCues, type SfxCueProps } from "./sfx-track";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The `--sfx` sound-effect track: one `<Sequence>` per cue, each mounting an
|
|
7
|
+
* `<Audio>` at its own instant.
|
|
8
|
+
*
|
|
9
|
+
* MIXED INSIDE REMOTION ON PURPOSE, and this is the whole reason the feature
|
|
10
|
+
* is cheap: the render's audio goes through loudnorm afterwards (core's
|
|
11
|
+
* ingest.ts), so a mix done here is MEASURED as part of the programme — the
|
|
12
|
+
* effects sit at a level normalized against the speech instead of being
|
|
13
|
+
* stamped on after the loudness pass, which is what a post-render ffmpeg
|
|
14
|
+
* overlay would have meant (and would have needed its own gain staging to
|
|
15
|
+
* avoid clipping the take it lands on).
|
|
16
|
+
*
|
|
17
|
+
* No duration on the Sequences: an effect plays for its own length. That is
|
|
18
|
+
* also why cues can overlap the composition's own end — Remotion truncates the
|
|
19
|
+
* tail at the last frame, and a whoosh clipped by the end of the video is the
|
|
20
|
+
* same thing an editor would do by hand.
|
|
21
|
+
*
|
|
22
|
+
* Deliberately invisible to the editor, the Watermark/CoverInVideo rule: no
|
|
23
|
+
* `data-edit-id`, nothing to hit-test, nothing rendered at all. Placement
|
|
24
|
+
* editing arrives through the overrides doc, not through the stage.
|
|
25
|
+
*/
|
|
26
|
+
export const SfxTrack: React.FC<{ cues: readonly SfxCueProps[] }> = ({ cues }) => {
|
|
27
|
+
const { fps, durationInFrames } = useVideoConfig();
|
|
28
|
+
return (
|
|
29
|
+
<>
|
|
30
|
+
{visibleSfxCues(cues, fps, durationInFrames).map((cue, i) => (
|
|
31
|
+
<Sequence
|
|
32
|
+
// Index in the VISIBLE list plus the frame: two effects can share a
|
|
33
|
+
// frame (different sounds, same beat), so neither alone is a stable
|
|
34
|
+
// key, and React would remount one of them on any list change.
|
|
35
|
+
key={`${cue.from}-${i}`}
|
|
36
|
+
from={cue.from}
|
|
37
|
+
layout="none"
|
|
38
|
+
>
|
|
39
|
+
<Audio
|
|
40
|
+
// An http(s) URL (or the editor's `/media/…`) passes through
|
|
41
|
+
// untouched, everything else is a name in the render's public dir
|
|
42
|
+
// — CoverInVideo's exact rule.
|
|
43
|
+
src={/^https?:\/\//.test(cue.soundFile) ? cue.soundFile : staticFile(cue.soundFile)}
|
|
44
|
+
volume={cue.gain}
|
|
45
|
+
/>
|
|
46
|
+
</Sequence>
|
|
47
|
+
))}
|
|
48
|
+
</>
|
|
49
|
+
);
|
|
50
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -4,8 +4,10 @@ export { VideoStage } from "./VideoStage";
|
|
|
4
4
|
export { SceneLayer } from "./SceneLayer";
|
|
5
5
|
export { Watermark } from "./Watermark";
|
|
6
6
|
export { CoverInVideo } from "./CoverInVideo";
|
|
7
|
+
export { SfxTrack } from "./SfxTrack";
|
|
7
8
|
export * from "./stage";
|
|
8
9
|
export * from "./watermark-layout";
|
|
9
10
|
export * from "./cover-in-video";
|
|
11
|
+
export * from "./sfx-track";
|
|
10
12
|
export * from "./punch-plan";
|
|
11
13
|
export * from "./caption-visibility";
|
package/src/sfx-track.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `--sfx` sound track's props and frame math (core's `resolveSfxCues`
|
|
3
|
+
* owns the word→output-time half and the gain product).
|
|
4
|
+
*
|
|
5
|
+
* Pure and JSX-free, `cover-in-video.ts`'s posture: the props gate, the frame
|
|
6
|
+
* rounding and the volume clamp are the whole behavior, and this package
|
|
7
|
+
* carries no jsdom — none of it would be assertable inside the component.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** One effect: a staged file, an OUTPUT-time instant, a mix level. */
|
|
11
|
+
export interface SfxCueProps {
|
|
12
|
+
/** File under the render's public dir — `sfx/<id>.<ext>` (or a `/media/…` URL). */
|
|
13
|
+
soundFile: string;
|
|
14
|
+
atSec: number;
|
|
15
|
+
gain: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The loudest a cue may play. `HTMLMediaElement.volume` THROWS an
|
|
20
|
+
* IndexSizeError above 1, so an un-clamped gain would take the editor's
|
|
21
|
+
* preview down entirely — while the render, which mixes the samples itself,
|
|
22
|
+
* would happily amplify. Preview and render must agree, so both get the clamp
|
|
23
|
+
* and the ceiling is 1.
|
|
24
|
+
*/
|
|
25
|
+
export const SFX_MAX_VOLUME = 1;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Whether a render-props `sfxCues` entry is a cue this renderer will mount —
|
|
29
|
+
* `coverInVideoPropsFor`'s posture (parse, never coerce, CLAUDE.md):
|
|
30
|
+
* render-props.json is user-visible and hand-editable, every pre-feature file
|
|
31
|
+
* has no key at all, and a mangled entry must fall back to SILENCE rather than
|
|
32
|
+
* mount an `undefined` src or a NaN-frame Sequence over the take.
|
|
33
|
+
*
|
|
34
|
+
* Per ENTRY, not all-or-nothing: one bad cue costs that cue, the drop-bad-item
|
|
35
|
+
* rule the whole SFX path is built on.
|
|
36
|
+
*/
|
|
37
|
+
export function sfxCuesFor(value: unknown): SfxCueProps[] {
|
|
38
|
+
if (!Array.isArray(value)) return [];
|
|
39
|
+
const cues: SfxCueProps[] = [];
|
|
40
|
+
for (const entry of value) {
|
|
41
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
42
|
+
const v = entry as { soundFile?: unknown; atSec?: unknown; gain?: unknown };
|
|
43
|
+
if (typeof v.soundFile !== "string" || v.soundFile.length === 0) continue;
|
|
44
|
+
if (typeof v.atSec !== "number" || !Number.isFinite(v.atSec) || v.atSec < 0) continue;
|
|
45
|
+
// An absent gain is 1 (play it as recorded); a mangled one is refused
|
|
46
|
+
// rather than defaulted, because a wrong LEVEL is audible and silent
|
|
47
|
+
// about being wrong.
|
|
48
|
+
if (v.gain !== undefined && (typeof v.gain !== "number" || !Number.isFinite(v.gain))) continue;
|
|
49
|
+
cues.push({
|
|
50
|
+
soundFile: v.soundFile,
|
|
51
|
+
atSec: v.atSec,
|
|
52
|
+
gain: Math.min(SFX_MAX_VOLUME, Math.max(0, v.gain ?? 1)),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return cues;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The cues that actually have a frame to fire on, with that frame.
|
|
60
|
+
*
|
|
61
|
+
* `Math.round`, matching `frameWindow`'s start (FINDINGS §115) — an effect is
|
|
62
|
+
* an instant, so there is no end time whose independent rounding could collide
|
|
63
|
+
* with it. A cue at or past the composition's last frame is DROPPED rather
|
|
64
|
+
* than clamped to it: a whoosh planned for a moment the render no longer
|
|
65
|
+
* reaches must not pile onto the final frame with everything else that fell
|
|
66
|
+
* off the end.
|
|
67
|
+
*/
|
|
68
|
+
export function visibleSfxCues(
|
|
69
|
+
cues: readonly SfxCueProps[],
|
|
70
|
+
fps: number,
|
|
71
|
+
durationInFrames: number,
|
|
72
|
+
): Array<SfxCueProps & { from: number }> {
|
|
73
|
+
const out: Array<SfxCueProps & { from: number }> = [];
|
|
74
|
+
for (const cue of cues) {
|
|
75
|
+
const from = Math.round(cue.atSec * fps);
|
|
76
|
+
if (from < 0 || from >= durationInFrames) continue;
|
|
77
|
+
out.push({ ...cue, from });
|
|
78
|
+
}
|
|
79
|
+
return out;
|
|
80
|
+
}
|