@glissade/narrate 0.4.3 → 0.4.4
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/dist/index.d.ts +57 -1
- package/dist/index.js +50 -1
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -118,7 +118,63 @@ interface DuckOptions {
|
|
|
118
118
|
declare function duckEnvelope(timing: NarrationTiming, opts?: DuckOptions): {
|
|
119
119
|
keys: Key<number>[];
|
|
120
120
|
};
|
|
121
|
+
/**
|
|
122
|
+
* `<name>.music.timing.json` — committed next to its stem. The load-bearing
|
|
123
|
+
* invariant: BEAT 0 IS SAMPLE 0 of the stem (the prepare step trims the
|
|
124
|
+
* recording to the downbeat); `offsetSec` exists for stems that can't be
|
|
125
|
+
* trimmed (count-ins). Everything derives from bpm/beatsPerCycle — no
|
|
126
|
+
* per-beat marker arrays. Shape blessed from downstream production
|
|
127
|
+
* (TidalCycles render step), where `cps` is the native unit: when present it
|
|
128
|
+
* must agree with bpm/beatsPerCycle.
|
|
129
|
+
*/
|
|
130
|
+
interface MusicTiming {
|
|
131
|
+
musicVersion: 1;
|
|
132
|
+
name?: string;
|
|
133
|
+
bpm: number;
|
|
134
|
+
beatsPerCycle: number;
|
|
135
|
+
cycles?: number;
|
|
136
|
+
/** cycles per second — TidalCycles-native; must equal bpm / (60 · beatsPerCycle) */
|
|
137
|
+
cps?: number;
|
|
138
|
+
durationSec: number;
|
|
139
|
+
/** seconds into the stem where beat 0 sits; default 0 (trimmed-to-downbeat) */
|
|
140
|
+
offsetSec?: number;
|
|
141
|
+
/** stem audio file, relative to the manifest — required for render auto-mix */
|
|
142
|
+
stem?: string;
|
|
143
|
+
/** bed level in dB applied to the clip gain (auto-mix and clip()); default 0 */
|
|
144
|
+
gainDb?: number;
|
|
145
|
+
/** provenance (e.g. the .tidal pattern source) */
|
|
146
|
+
source?: string;
|
|
147
|
+
}
|
|
148
|
+
interface MusicClipOptions {
|
|
149
|
+
/** bed level in dB; overrides the manifest's gainDb */
|
|
150
|
+
gainDb?: number;
|
|
151
|
+
/** auto-duck under this narration (windows from its segments) */
|
|
152
|
+
duckUnder?: NarrationTiming;
|
|
153
|
+
duckOpts?: Omit<DuckOptions, 'clipAt'>;
|
|
154
|
+
}
|
|
155
|
+
interface MusicAnchors {
|
|
156
|
+
/** timeline second of beat n (beat 0 = clip at + offsetSec) */
|
|
157
|
+
beat(n: number): number;
|
|
158
|
+
/** timeline second of cycle n (beatsPerCycle beats each) */
|
|
159
|
+
cycle(n: number): number;
|
|
160
|
+
/** quantize t to the closest beat */
|
|
161
|
+
nearestBeat(t: number): number;
|
|
162
|
+
/** quantize t forward to the next beat (what choreography reaches for) */
|
|
163
|
+
nextBeat(t: number): number;
|
|
164
|
+
readonly beatLen: number;
|
|
165
|
+
readonly durationSec: number;
|
|
166
|
+
/** the grid parameters, for external quantizers */
|
|
167
|
+
grid(): {
|
|
168
|
+
bpm: number;
|
|
169
|
+
offsetSec: number;
|
|
170
|
+
};
|
|
171
|
+
/** the stem as an AudioClip, with bed gain and optional narration ducking composed */
|
|
172
|
+
clip(url?: string, opts?: MusicClipOptions): AudioClip;
|
|
173
|
+
}
|
|
174
|
+
declare function validateMusicTiming(timing: MusicTiming): void;
|
|
175
|
+
/** Beat-grid anchors over a music manifest; `at` places the clip on the timeline. */
|
|
176
|
+
declare function music(timing: MusicTiming, at?: number): MusicAnchors;
|
|
121
177
|
declare function toSrt(timing: NarrationTiming): string;
|
|
122
178
|
declare function toVtt(timing: NarrationTiming): string;
|
|
123
179
|
//#endregion
|
|
124
|
-
export { CaptionStyle, CaptionTrackOptions, DuckOptions, NarrationAnchors, NarrationError, NarrationScript, NarrationSegment, NarrationTiming, TimedSegment, TimedWord, captionNode, captionTrack, duckEnvelope, narration, toSrt, toVtt };
|
|
180
|
+
export { CaptionStyle, CaptionTrackOptions, DuckOptions, MusicAnchors, MusicClipOptions, MusicTiming, NarrationAnchors, NarrationError, NarrationScript, NarrationSegment, NarrationTiming, TimedSegment, TimedWord, captionNode, captionTrack, duckEnvelope, music, narration, toSrt, toVtt, validateMusicTiming };
|
package/dist/index.js
CHANGED
|
@@ -126,6 +126,55 @@ function duckEnvelope(timing, opts = {}) {
|
|
|
126
126
|
if (keys[0].t > 0) keys.unshift(key(0, base));
|
|
127
127
|
return { keys };
|
|
128
128
|
}
|
|
129
|
+
function validateMusicTiming(timing) {
|
|
130
|
+
if (timing.musicVersion !== 1) throw new NarrationError(`unsupported musicVersion ${String(timing.musicVersion)}`);
|
|
131
|
+
if (!(timing.bpm > 0) || !(timing.beatsPerCycle > 0)) throw new NarrationError("music timing needs bpm > 0 and beatsPerCycle > 0");
|
|
132
|
+
if (timing.cps !== void 0) {
|
|
133
|
+
const expected = timing.bpm / (60 * timing.beatsPerCycle);
|
|
134
|
+
if (Math.abs(timing.cps - expected) > 1e-9) throw new NarrationError(`music timing cps (${timing.cps}) disagrees with bpm/beatsPerCycle (expected ${expected})`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/** Beat-grid anchors over a music manifest; `at` places the clip on the timeline. */
|
|
138
|
+
function music(timing, at = 0) {
|
|
139
|
+
validateMusicTiming(timing);
|
|
140
|
+
const beatLen = 60 / timing.bpm;
|
|
141
|
+
const beat0 = at + (timing.offsetSec ?? 0);
|
|
142
|
+
return {
|
|
143
|
+
beat: (n) => beat0 + n * beatLen,
|
|
144
|
+
cycle: (n) => beat0 + n * timing.beatsPerCycle * beatLen,
|
|
145
|
+
nearestBeat: (t) => beat0 + Math.round((t - beat0) / beatLen) * beatLen,
|
|
146
|
+
nextBeat: (t) => beat0 + Math.ceil((t - beat0 - 1e-9) / beatLen) * beatLen,
|
|
147
|
+
beatLen,
|
|
148
|
+
durationSec: timing.durationSec,
|
|
149
|
+
grid: () => ({
|
|
150
|
+
bpm: timing.bpm,
|
|
151
|
+
offsetSec: beat0
|
|
152
|
+
}),
|
|
153
|
+
clip: (url, opts = {}) => {
|
|
154
|
+
const src = url ?? timing.stem;
|
|
155
|
+
if (!src) throw new NarrationError("music clip needs a url (or a stem field in the manifest)");
|
|
156
|
+
const gainDb = opts.gainDb ?? timing.gainDb ?? 0;
|
|
157
|
+
const scale = Math.pow(10, gainDb / 20);
|
|
158
|
+
let keys = null;
|
|
159
|
+
if (opts.duckUnder) keys = duckEnvelope(opts.duckUnder, {
|
|
160
|
+
...opts.duckOpts,
|
|
161
|
+
clipAt: at
|
|
162
|
+
}).keys;
|
|
163
|
+
else if (gainDb !== 0) keys = [key(0, 1)];
|
|
164
|
+
return {
|
|
165
|
+
asset: {
|
|
166
|
+
kind: "audio",
|
|
167
|
+
url: src
|
|
168
|
+
},
|
|
169
|
+
at,
|
|
170
|
+
...keys !== null ? { gain: { keys: keys.map((k) => ({
|
|
171
|
+
...k,
|
|
172
|
+
value: k.value * scale
|
|
173
|
+
})) } } : {}
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
}
|
|
129
178
|
function srtTime(t, sep) {
|
|
130
179
|
const ms = Math.round(t * 1e3);
|
|
131
180
|
const h = Math.floor(ms / 36e5);
|
|
@@ -142,4 +191,4 @@ function toVtt(timing) {
|
|
|
142
191
|
return "WEBVTT\n\n" + timing.segments.map((s) => `${srtTime(s.start, ".")} --> ${srtTime(s.start + s.duration, ".")}\n${s.text}`).join("\n\n") + "\n";
|
|
143
192
|
}
|
|
144
193
|
//#endregion
|
|
145
|
-
export { NarrationError, captionNode, captionTrack, duckEnvelope, narration, toSrt, toVtt };
|
|
194
|
+
export { NarrationError, captionNode, captionTrack, duckEnvelope, music, narration, toSrt, toVtt, validateMusicTiming };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glissade/narrate",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.4",
|
|
4
4
|
"description": "glissade narration + captions: TTS at prepare time (gs narrate), deterministic caching, narration-anchored timeline beats, and captions as plain tracks. Render stays offline.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
"dist"
|
|
20
20
|
],
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@glissade/
|
|
23
|
-
"@glissade/
|
|
22
|
+
"@glissade/core": "0.4.4",
|
|
23
|
+
"@glissade/scene": "0.4.4"
|
|
24
24
|
},
|
|
25
25
|
"repository": {
|
|
26
26
|
"type": "git",
|