@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 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",
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/scene": "0.4.3",
23
- "@glissade/core": "0.4.3"
22
+ "@glissade/core": "0.4.4",
23
+ "@glissade/scene": "0.4.4"
24
24
  },
25
25
  "repository": {
26
26
  "type": "git",