@msb235/blits 0.4.0 → 0.5.0

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.
Files changed (102) hide show
  1. package/CHANGELOG.md +262 -0
  2. package/README.md +2 -1
  3. package/dist/bare.d.ts +29 -0
  4. package/dist/bare.js +105 -0
  5. package/dist/blend.d.ts +34 -0
  6. package/dist/blend.js +102 -0
  7. package/dist/book.d.ts +53 -0
  8. package/dist/book.js +218 -0
  9. package/dist/chain.d.ts +22 -0
  10. package/dist/chain.js +150 -0
  11. package/dist/channels.d.ts +2 -0
  12. package/dist/channels.js +3 -1
  13. package/dist/clock.d.ts +50 -3
  14. package/dist/clock.js +102 -9
  15. package/dist/clone.d.ts +7 -0
  16. package/dist/clone.js +57 -0
  17. package/dist/columns.d.ts +29 -0
  18. package/dist/columns.js +183 -0
  19. package/dist/crowd.d.ts +128 -0
  20. package/dist/crowd.js +354 -0
  21. package/dist/cue.d.ts +3 -0
  22. package/dist/cue.js +115 -0
  23. package/dist/due.d.ts +12 -0
  24. package/dist/due.js +93 -0
  25. package/dist/fade.d.ts +19 -0
  26. package/dist/fade.js +133 -0
  27. package/dist/fill.d.ts +42 -0
  28. package/dist/fill.js +381 -0
  29. package/dist/fold.d.ts +51 -0
  30. package/dist/fold.js +475 -0
  31. package/dist/gather.d.ts +24 -0
  32. package/dist/gather.js +186 -0
  33. package/dist/handle.d.ts +31 -0
  34. package/dist/handle.js +56 -0
  35. package/dist/held.d.ts +14 -0
  36. package/dist/held.js +117 -0
  37. package/dist/history.d.ts +22 -0
  38. package/dist/history.js +149 -0
  39. package/dist/hosts.d.ts +16 -0
  40. package/dist/hosts.js +178 -0
  41. package/dist/index.d.ts +1 -1
  42. package/dist/keyrows.d.ts +36 -0
  43. package/dist/keyrows.js +138 -0
  44. package/dist/lane.d.ts +210 -0
  45. package/dist/lane.js +187 -0
  46. package/dist/lanes.d.ts +121 -246
  47. package/dist/lanes.js +150 -1578
  48. package/dist/locus.d.ts +40 -0
  49. package/dist/locus.js +89 -0
  50. package/dist/marks.d.ts +12 -0
  51. package/dist/marks.js +80 -0
  52. package/dist/meet.d.ts +16 -0
  53. package/dist/meet.js +113 -0
  54. package/dist/mixer.d.ts +244 -196
  55. package/dist/mixer.js +242 -2220
  56. package/dist/motion.d.ts +66 -21
  57. package/dist/motion.js +325 -165
  58. package/dist/move.d.ts +9 -0
  59. package/dist/move.js +190 -0
  60. package/dist/named.d.ts +12 -0
  61. package/dist/named.js +39 -0
  62. package/dist/numbers.d.ts +18 -6
  63. package/dist/numbers.js +59 -31
  64. package/dist/owed.d.ts +24 -0
  65. package/dist/owed.js +51 -0
  66. package/dist/owner.d.ts +43 -0
  67. package/dist/owner.js +117 -0
  68. package/dist/pace.d.ts +40 -0
  69. package/dist/pace.js +113 -0
  70. package/dist/patch.d.ts +51 -10
  71. package/dist/patch.js +140 -45
  72. package/dist/place.d.ts +20 -0
  73. package/dist/place.js +196 -0
  74. package/dist/project.d.ts +4 -0
  75. package/dist/project.js +233 -0
  76. package/dist/pull.d.ts +5 -0
  77. package/dist/pull.js +175 -0
  78. package/dist/qualify.d.ts +31 -0
  79. package/dist/qualify.js +345 -0
  80. package/dist/relink.d.ts +25 -0
  81. package/dist/relink.js +59 -0
  82. package/dist/rows.d.ts +33 -0
  83. package/dist/rows.js +282 -0
  84. package/dist/sample.d.ts +21 -0
  85. package/dist/sample.js +232 -0
  86. package/dist/signals.d.ts +12 -9
  87. package/dist/signals.js +8 -5
  88. package/dist/store.d.ts +10 -3
  89. package/dist/store.js +44 -9
  90. package/dist/strays.d.ts +21 -0
  91. package/dist/strays.js +84 -0
  92. package/dist/tweened.d.ts +11 -0
  93. package/dist/tweened.js +48 -0
  94. package/dist/types.d.ts +217 -34
  95. package/dist/unreached.d.ts +5 -0
  96. package/dist/unreached.js +24 -0
  97. package/dist/voice.d.ts +240 -0
  98. package/dist/voice.js +322 -0
  99. package/dist/weigh.d.ts +26 -0
  100. package/dist/weigh.js +89 -0
  101. package/docs/schema.html +118 -40
  102. package/package.json +5 -2
package/dist/store.js CHANGED
@@ -1,22 +1,47 @@
1
+ /** What every store reads before its first write: never written, so always empty. */
2
+ const noWeak = new WeakMap();
3
+ const noStrong = new Map();
1
4
  /**
2
5
  * Per-subject storage keyed by identity. Objects go in a WeakMap and are forgotten when the host
3
6
  * drops them; anything else — a string id, the unit subject of a mix with no subject dimension —
4
7
  * goes in a Map that only `drop` and `clear` empty, which is why a host keying by value should
5
- * call `drop`.
8
+ * call `drop`. Each map is made when first written, since a mix holds a store per voice and patch;
9
+ * until then it is a shared empty one, so the field always holds a map.
6
10
  */
7
11
  export class Store {
8
- weak = new WeakMap();
9
- strong = new Map();
12
+ weak = noWeak;
13
+ strong = noStrong;
14
+ /**
15
+ * The first key that is not an object, and its value, held here until a second comes: a voice of
16
+ * one subject keyed by id would otherwise spend a Map's smallest table on it.
17
+ */
18
+ oneKey = none;
19
+ oneValue = undefined;
10
20
  get(key) {
11
- return typeof key === 'object' && key !== null
12
- ? this.weak.get(key)
13
- : this.strong.get(key);
21
+ if (typeof key === 'object' && key !== null)
22
+ return this.weak.get(key);
23
+ return same(this.oneKey, key) ? this.oneValue : this.strong.get(key);
14
24
  }
15
25
  set(key, value) {
16
- if (typeof key === 'object' && key !== null)
26
+ if (typeof key === 'object' && key !== null) {
27
+ if (this.weak === noWeak)
28
+ this.weak = new WeakMap();
17
29
  this.weak.set(key, value);
18
- else
19
- this.strong.set(key, value);
30
+ return;
31
+ }
32
+ if (this.strong === noStrong && (this.oneKey === none || same(this.oneKey, key))) {
33
+ this.oneKey = key;
34
+ this.oneValue = value;
35
+ return;
36
+ }
37
+ if (this.strong === noStrong)
38
+ this.strong = new Map();
39
+ if (this.oneKey !== none) {
40
+ this.strong.set(this.oneKey, this.oneValue);
41
+ this.oneKey = none;
42
+ this.oneValue = undefined;
43
+ }
44
+ this.strong.set(key, value);
20
45
  }
21
46
  has(key) {
22
47
  return this.get(key) !== undefined;
@@ -24,10 +49,20 @@ export class Store {
24
49
  delete(key) {
25
50
  if (typeof key === 'object' && key !== null)
26
51
  this.weak.delete(key);
52
+ else if (same(this.oneKey, key)) {
53
+ this.oneKey = none;
54
+ this.oneValue = undefined;
55
+ }
27
56
  else
28
57
  this.strong.delete(key);
29
58
  }
30
59
  clear() {
60
+ this.oneKey = none;
61
+ this.oneValue = undefined;
31
62
  this.strong.clear();
32
63
  }
33
64
  }
65
+ /** No key held inline. */
66
+ const none = Symbol('none');
67
+ /** A Map's key equality, SameValueZero: NaN finds NaN. */
68
+ const same = (a, b) => a === b || Object.is(a, b);
@@ -0,0 +1,21 @@
1
+ import type { Mixer } from './mixer.js';
2
+ import type { MotionOwner, Motions } from './motion.js';
3
+ import type { Voice } from './voice.js';
4
+ /** Notes a motion patch that may hold state for a subject no voice playing it reaches. */
5
+ export declare function stray<I, O>(mix: Mixer<I, O>, motion: Motions<I>, subject: I): void;
6
+ /** A motion voice left the list: if its patch plays on, its subjects' state there may stray. */
7
+ export declare function unplay<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>): void;
8
+ /**
9
+ * What a motion patch asks of the mix playing it, by its voice's id. It holds the mix weakly, so a
10
+ * patch the host keeps does not keep a mix it has let go of alive; a retired voice's patch no
11
+ * longer asks.
12
+ */
13
+ export declare function motionOwner<I, O>(mix: WeakRef<Mixer<I, O>>): MotionOwner;
14
+ /** The voice in the list with this id, which is in id order. */
15
+ export declare function cuedById<I, O>(mix: Mixer<I, O>, id: number): Voice<I, O> | undefined;
16
+ /**
17
+ * A subject's voice time at the latest frame, where a motion patch places an untimed change and a
18
+ * `read` with no time; NaN until the voice has started and met the subject, and while the
19
+ * subject's own time is still short of its stagger. A retired voice's patch no longer asks.
20
+ */
21
+ export declare function frameOf<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>, subject: I): number;
package/dist/strays.js ADDED
@@ -0,0 +1,84 @@
1
+ import { unpart } from './fade.js';
2
+ /** Notes a motion patch that may hold state for a subject no voice playing it reaches. */
3
+ export function stray(mix, motion, subject) {
4
+ const list = mix.strays.get(subject);
5
+ if (list === undefined)
6
+ mix.strays.set(subject, [motion]);
7
+ else if (!list.includes(motion))
8
+ list.push(motion);
9
+ }
10
+ /** A motion voice left the list: if its patch plays on, its subjects' state there may stray. */
11
+ export function unplay(mix, voice) {
12
+ const motion = voice.motion;
13
+ if (motion === undefined)
14
+ return;
15
+ const left = (mix.playing.get(motion) ?? 1) - 1;
16
+ if (left <= 0) {
17
+ mix.playing.delete(motion);
18
+ mix.strayAll.delete(motion);
19
+ return;
20
+ }
21
+ mix.playing.set(motion, left);
22
+ if (voice.named === null)
23
+ mix.strayAll.add(motion);
24
+ else
25
+ for (const subject of voice.named)
26
+ stray(mix, motion, subject);
27
+ }
28
+ /**
29
+ * What a motion patch asks of the mix playing it, by its voice's id. It holds the mix weakly, so a
30
+ * patch the host keeps does not keep a mix it has let go of alive; a retired voice's patch no
31
+ * longer asks.
32
+ */
33
+ export function motionOwner(mix) {
34
+ return {
35
+ frame(id, subject) {
36
+ const m = mix.deref();
37
+ const v = m === undefined ? undefined : cuedById(m, id);
38
+ return m === undefined || v === undefined ? Number.NaN : frameOf(m, v, subject);
39
+ },
40
+ revive(id, subject) {
41
+ const m = mix.deref();
42
+ if (m === undefined)
43
+ return;
44
+ m.stir();
45
+ const v = cuedById(m, id);
46
+ if (v === undefined)
47
+ return;
48
+ if (v.motion !== undefined && v.named !== null && !v.named.has(subject))
49
+ stray(m, v.motion, subject);
50
+ unpart(m, v, subject);
51
+ },
52
+ };
53
+ }
54
+ /** The voice in the list with this id, which is in id order. */
55
+ export function cuedById(mix, id) {
56
+ const cued = mix.cued;
57
+ let lo = 0;
58
+ let hi = cued.length - 1;
59
+ while (lo <= hi) {
60
+ const mid = (lo + hi) >> 1;
61
+ const v = cued[mid];
62
+ if (v.id === id)
63
+ return v;
64
+ if (v.id < id)
65
+ lo = mid + 1;
66
+ else
67
+ hi = mid - 1;
68
+ }
69
+ return undefined;
70
+ }
71
+ /**
72
+ * A subject's voice time at the latest frame, where a motion patch places an untimed change and a
73
+ * `read` with no time; NaN until the voice has started and met the subject, and while the
74
+ * subject's own time is still short of its stagger. A retired voice's patch no longer asks.
75
+ */
76
+ export function frameOf(mix, voice, subject) {
77
+ if (Number.isNaN(mix.now) || voice.state === 'pending')
78
+ return Number.NaN;
79
+ const held = voice.subjects.get(subject);
80
+ if (held === undefined || !held.reaches)
81
+ return Number.NaN;
82
+ const t = voice.elapsedAt(mix.now) - held.delay;
83
+ return t < 0 ? Number.NaN : t;
84
+ }
@@ -0,0 +1,11 @@
1
+ import type { Curve } from './easing.js';
2
+ /** Turns the velocity `eased` works out on or off. */
3
+ export declare function sloped(on: boolean): void;
4
+ /** How far through a stretch released at `at` and lasting `ms` voice time `t` is, from 0. */
5
+ export declare function progress(t: number, at: number, ms: number): number;
6
+ /** The share of a tween's way left to go at `u`, 0 once it has arrived. */
7
+ export declare function left(ease: Curve, u: number): number;
8
+ /** An axis of a tween from `x0` to `goal` with `share` of the way left. */
9
+ export declare function between(x0: number, goal: number, share: number): number;
10
+ /** A tween's stretch at `u` into `xo` and `vo`: true once it has arrived. */
11
+ export declare function eased(ease: Curve, n: number, u: number, ms: number, x0: ArrayLike<number>, x: number, to: ArrayLike<number>, g: number, xo: Float64Array, vo: Float64Array): boolean;
@@ -0,0 +1,48 @@
1
+ /** How far a tween's slope is read either side of `u`, for an easing that is only a function. */
2
+ const SPAN = 1e-4;
3
+ /**
4
+ * Whether an evaluation wants a tween's velocity, which costs two more reads of its easing. Only
5
+ * `read` asks for it; a mix and a retarget never use it.
6
+ */
7
+ let slope = false;
8
+ /** The last tween sample's time, curve and length, and the share of the way it had left to go. */
9
+ const memo = { u: Number.NaN, ease: undefined, left: 0 };
10
+ /** Turns the velocity `eased` works out on or off. */
11
+ export function sloped(on) {
12
+ slope = on;
13
+ }
14
+ /** How far through a stretch released at `at` and lasting `ms` voice time `t` is, from 0. */
15
+ export function progress(t, at, ms) {
16
+ return Math.max(0, t - at) / ms;
17
+ }
18
+ /** The share of a tween's way left to go at `u`, 0 once it has arrived. */
19
+ export function left(ease, u) {
20
+ // Subjects released together share `u`, so a frame reads a bezier once, not once each.
21
+ if (u !== memo.u || ease !== memo.ease) {
22
+ memo.u = u;
23
+ memo.ease = ease;
24
+ memo.left = u >= 1 ? 0 : 1 - ease(u);
25
+ }
26
+ return memo.left;
27
+ }
28
+ /** An axis of a tween from `x0` to `goal` with `share` of the way left. */
29
+ export function between(x0, goal, share) {
30
+ return share === 0 ? goal : goal + (x0 - goal) * share;
31
+ }
32
+ /** A tween's stretch at `u` into `xo` and `vo`: true once it has arrived. */
33
+ export function eased(ease, n, u, ms, x0, x, to, g, xo, vo) {
34
+ const share = left(ease, u);
35
+ let rate = 0;
36
+ if (slope && u < 1) {
37
+ const lo = Math.max(0, u - SPAN);
38
+ const hi = Math.min(1, u + SPAN);
39
+ rate = ((ease(hi) - ease(lo)) / (hi - lo)) * (1000 / ms);
40
+ }
41
+ for (let i = 0; i < n; i++) {
42
+ const a = x0[x + i];
43
+ const goal = to[g + i];
44
+ xo[i] = between(a, goal, share);
45
+ vo[i] = -(a - goal) * rate;
46
+ }
47
+ return share === 0;
48
+ }
package/dist/types.d.ts CHANGED
@@ -67,10 +67,10 @@ export interface Keyframe<O> {
67
67
  *
68
68
  * @category state
69
69
  */
70
- export interface Setting<S = void> {
70
+ export interface Setting<S = void, H = unknown> {
71
71
  /**
72
- * The mix clock at this frame: the host's timestamp less any time `rebase` took out. Identical
73
- * for every probe in the frame.
72
+ * The mix clock at this frame: the host's timestamp less any time `rebase` took out, run at the
73
+ * mix's `rate`. Identical for every probe in the frame.
74
74
  */
75
75
  timestamp: number;
76
76
  /**
@@ -88,14 +88,15 @@ export interface Setting<S = void> {
88
88
  weight: number;
89
89
  /** This subject's state, when the patch declares one. */
90
90
  state: S;
91
- /** Anything the host adds for its own patches. */
92
- host: unknown;
91
+ /** Anything the host adds for its own patches: the mix's `host`, typed as its `H`. */
92
+ host: H;
93
93
  /**
94
94
  * The state `owner` keeps for this voice and this subject, made by `init` on first ask. The mix
95
95
  * holds it, so a read at another time can copy it instead of moving it. A stateful signal keeps
96
- * its state here and nowhere else. With lanes on, a patch that first calls it partway through
97
- * playing can have that state advanced once for one subject not probed on the frame it starts; a
98
- * patch that keeps state from its first call, or declares `state`, never does.
96
+ * its state here and nowhere else. With lanes on, a patch or weight signal that first calls it
97
+ * partway through playing can have that state advanced once for one subject not probed on the
98
+ * frame it starts; one that keeps state from its first call, or a patch that declares `state`,
99
+ * never does.
99
100
  */
100
101
  keep<K>(owner: object, init: () => K): K;
101
102
  /**
@@ -103,7 +104,7 @@ export interface Setting<S = void> {
103
104
  * host drains it, and never calls back. Meant for `step`, where under `stepMs` the timestamp is
104
105
  * the interval the event happened in rather than the frame that sampled it. Sent from a stateless
105
106
  * patch's `at` while it runs on a lane, it goes out for every subject the mix has met, probed that
106
- * frame or not.
107
+ * frame or not; `at` is not asked for a subject at weight 0, so nothing is sent from it there.
107
108
  */
108
109
  send(event: unknown): void;
109
110
  }
@@ -153,14 +154,16 @@ export type MotionSpec = {
153
154
  *
154
155
  * @category patch
155
156
  */
156
- export interface Patch<I, O, S = void> {
157
+ export interface Patch<I, O, S = void, H = unknown> {
157
158
  /**
158
159
  * Which authoring form built it: `'fn'` from `patch`, `'keys'` from `keys`, `'motion'` from
159
160
  * `spring`, `glide` or `tween`. An engine declares which forms it runs.
160
161
  */
161
162
  readonly form: 'fn' | 'keys' | 'motion';
162
- /** Milliseconds one pass lasts. 0 is aperiodic: phase and pass stay 0. */
163
- readonly period: number;
163
+ /** Milliseconds one pass lasts, whether or not the voice loops. 0 has no passes: phase and pass stay 0. */
164
+ readonly duration: number;
165
+ /** @deprecated Use `duration`. A patch that sets only `period` still plays. */
166
+ readonly period?: number;
164
167
  /** The channels this patch contributes to. Every key `at` sets, and no others. */
165
168
  readonly writes: readonly (keyof O)[];
166
169
  /**
@@ -170,15 +173,18 @@ export interface Patch<I, O, S = void> {
170
173
  readonly kit?: Partial<Kit<O>>;
171
174
  /** The fields of `setting.host` this patch reads. `cue` refuses a mix whose host lacks one. */
172
175
  readonly reads?: readonly string[];
173
- /** `phase` is 0..1 across one period, wrapping. */
174
- at(phase: number, subject: I, setting: Setting<S>): Partial<O>;
176
+ /**
177
+ * `phase` is 0..1 across one pass, wrapping. Not asked for a subject its voice gives no weight, unless the voice is fading to rest;
178
+ * `step` still runs.
179
+ */
180
+ at(phase: number, subject: I, setting: Setting<S, H>): Partial<O>;
175
181
  /** Per-subject state, created on the first frame this patch sees a subject. */
176
182
  state?(subject: I): S;
177
183
  /**
178
184
  * Advances state once per subject per sampled frame, by the whole gap since it last advanced; or,
179
185
  * where the mix sets `stepMs`, once per whole interval of that length.
180
186
  */
181
- step?(state: S, dt: number, subject: I, setting: Setting<S>): void;
187
+ step?(state: S, dt: number, subject: I, setting: Setting<S, H>): void;
182
188
  /** Present when the patch was authored as keyframes, so an engine that reads data can. */
183
189
  readonly keys?: readonly Keyframe<O>[];
184
190
  /** Present on a `'motion'` patch: its kind and constants, for an engine that reads data. */
@@ -197,7 +203,7 @@ export interface Patch<I, O, S = void> {
197
203
  *
198
204
  * @category signal
199
205
  */
200
- export type Signal<I> = ((subject: I, setting: Setting) => number) & {
206
+ export type Signal<I, H = unknown> = ((subject: I, setting: Setting<void, H>) => number) & {
201
207
  readonly input?: boolean;
202
208
  };
203
209
  /**
@@ -286,19 +292,91 @@ export interface Marked {
286
292
  name: string | undefined;
287
293
  tags: readonly string[];
288
294
  }
295
+ /**
296
+ * An event at a time on a voice's own clock, which `book` hands a host ahead of time. It plays once
297
+ * per pass, for the voice and not per subject, so `stagger` does not spread it.
298
+ *
299
+ * @category score
300
+ */
301
+ export interface Hit<E = unknown> {
302
+ /** Voice ms into each pass: 0 up to the patch's `duration`, or any time at all for a patch with none. */
303
+ at: number;
304
+ event: E;
305
+ }
306
+ /**
307
+ * One pass of one hit, as `book` takes it.
308
+ *
309
+ * @category score
310
+ */
311
+ export interface BookedHit<E = unknown> {
312
+ /** On the host's clock. */
313
+ timestamp: number;
314
+ voice: number;
315
+ /** Which of the voice's `hits`, by index. */
316
+ hit: number;
317
+ /** Which pass, 0 first. */
318
+ pass: number;
319
+ event: E;
320
+ score: string | undefined;
321
+ name: string | undefined;
322
+ tags: readonly string[];
323
+ }
324
+ /**
325
+ * What `book` takes: an outside clock, how far ahead to book on it, and what to do with each item.
326
+ *
327
+ * @category score
328
+ */
329
+ export interface BookOptions<E = unknown> {
330
+ /**
331
+ * The outside clock, in ms: `audio.currentTime * 1000`, `performance.now()`, or whatever the host
332
+ * schedules against. Read once a sync. The mix maps its host time onto it by an offset that folds
333
+ * in 5% of each frame's difference, and starts over from the clock's own reading on a jump past
334
+ * 50 ms, such as a suspended context or a hidden tab.
335
+ */
336
+ clock: () => number;
337
+ /** How far ahead to book, host ms. It has to cover the longest gap between two syncs. */
338
+ ahead: number;
339
+ /** How far past an item first seen late may be and still be taken, host ms. */
340
+ late: number;
341
+ /**
342
+ * Called once per item: a mark as `marks` lists it, or a hit. `when` is on the outside clock;
343
+ * `lateBy` is how many host ms past the item was when first seen, 0 for one booked ahead, which
344
+ * is then taken at the clock's now. Return a `stop` to hear about a booking whose time moved by
345
+ * more than 1 ms or that went away (a seek, a rate or ramp on the voice or the mix, a fade, a
346
+ * `rebase`, an anchor's target moving, the voice leaving); the item is then booked again, at its
347
+ * new time, at the same sync.
348
+ */
349
+ take(item: Marked | BookedHit<E>, when: number, lateBy: number): {
350
+ stop(): void;
351
+ } | undefined;
352
+ /** Book only the voices, and announced marks, carrying this tag. */
353
+ tag?: string;
354
+ /** Book only this score's voices and marks. */
355
+ score?: string;
356
+ }
357
+ /**
358
+ * A running booker, returned by `book`.
359
+ *
360
+ * @category score
361
+ */
362
+ export interface Booker {
363
+ /** Stops every booking still ahead, and books nothing more. */
364
+ stop(): void;
365
+ }
289
366
  /**
290
367
  * What `cue` takes: a patch, and the clock, weight and reach it plays with.
291
368
  *
292
369
  * @category voice
293
370
  */
294
- export interface VoiceSpec<I, O> {
295
- patch: Patch<I, O, unknown>;
371
+ export interface VoiceSpec<I, O, H = unknown> {
372
+ patch: Patch<I, O, unknown, H>;
296
373
  /**
297
374
  * Which subjects this voice reaches. Default: all of them. The predicate is fixed at `cue`; it
298
375
  * runs per subject the first time the mix sees that subject, and the answer is kept. Every
299
376
  * voice's predicate meets every subject, so voices that each reach a known few cost the square
300
- * of their number, in time and in memory (a record per voice per subject asked: about 800 MB for
301
- * 1,000 such voices); name those with `subjects` instead.
377
+ * of their number in time; name those with `subjects` instead. A no is kept as a bit by the
378
+ * number lanes give the subject, but with `lanes: false` as a map entry per voice per subject
379
+ * asked (about 37 MB for 1,000 such voices over 1,000 subjects).
302
380
  */
303
381
  target?: (subject: I) => boolean;
304
382
  /**
@@ -336,14 +414,14 @@ export interface VoiceSpec<I, O> {
336
414
  */
337
415
  hold?: 'before' | 'after' | 'both';
338
416
  /** Steady weight, or a signal read per subject per frame. Default 1. */
339
- weight?: number | Signal<I>;
417
+ weight?: number | Signal<I, H>;
340
418
  /** Ramp in and out, ms. Out applies on `fade()` and when a finite loop ends. */
341
419
  fade?: FadeSpec;
342
420
  /** Voices sharing a locus are alternatives: the mix folds them through each channel's `lerp`. */
343
421
  locus?: string;
344
422
  /** A `keys` voice whose first stop reads wherever this subject is now. */
345
423
  from?: 'current';
346
- /** Words a source attaches to this voice, so its events can be drained as a set. */
424
+ /** Words a source attaches to this voice, so its events and its handle can be taken as a set. */
347
425
  tags?: readonly string[];
348
426
  /** What other voices call this one by. A label blits never reads. */
349
427
  name?: string;
@@ -355,6 +433,52 @@ export interface VoiceSpec<I, O> {
355
433
  score?: string;
356
434
  /** Where it sits relative to the clock or to other voices, in place of `start`. */
357
435
  anchor?: Placement;
436
+ /**
437
+ * Events at times on this voice's clock, which `book` hands a host ahead of time. Patch-sent
438
+ * events are not known ahead, so they stay drain-only; these are the plan.
439
+ */
440
+ hits?: readonly Hit[];
441
+ /**
442
+ * The owner this voice plays under, a handle `owns` returned. Its `start` and `anchor` are then on
443
+ * the owner's clock, in ms from when the owner starts, and default to where that clock is now; a
444
+ * bare name in an anchor means a sibling, another voice the owner holds. Its clock runs on the
445
+ * owner's, so the owner's rate, ramp and seeks move it, and its weight is multiplied by the
446
+ * owner's weight and fade. Without a `hold` of its own it takes the owner's.
447
+ */
448
+ owner?: Handle<I>;
449
+ }
450
+ /**
451
+ * What `owns` takes: an owner has no patch, and plays only through the voices it holds.
452
+ *
453
+ * @category voice
454
+ */
455
+ export interface OwnerSpec<I, H = unknown> {
456
+ /**
457
+ * When the owner starts, on its own owner's clock: the host's for an owner on the mix, as a
458
+ * voice's `start` is. Default: now.
459
+ */
460
+ start?: number;
461
+ /** Its clock's rate, multiplied into every child's own. Default 1. */
462
+ rate?: number;
463
+ /**
464
+ * Multiplied into every child's weight per subject, as the child's own fade is, before the child's
465
+ * locus folds it: a number, or a signal read per subject per frame. Default 1.
466
+ */
467
+ weight?: number | Signal<I, H>;
468
+ /** One ramp in and out over every child, multiplied into their weights as `weight` is. */
469
+ fade?: FadeSpec;
470
+ /** The hold every child without one of its own takes. */
471
+ hold?: 'before' | 'after' | 'both';
472
+ /** Words a source attaches to it, as a voice's. */
473
+ tags?: readonly string[];
474
+ /** What other voices call it by, among its siblings. */
475
+ name?: string;
476
+ /** The plan it belongs to, as a voice's. */
477
+ score?: string;
478
+ /** Where it sits relative to its own owner's clock or to other voices, in place of `start`. */
479
+ anchor?: Placement;
480
+ /** The owner it plays under, so owners nest. */
481
+ owner?: Handle<I>;
358
482
  }
359
483
  /**
360
484
  * How a voice leaves when it is faded: the whole voice, or one subject.
@@ -388,6 +512,8 @@ export interface Handle<I = unknown> {
388
512
  readonly id: number;
389
513
  /** `held`: a voice holding after has played every pass and shows its last frame until faded. */
390
514
  readonly state: 'pending' | 'live' | 'held' | 'fading' | 'done';
515
+ /** The handle of the owner this voice plays under; undefined for one on the mix clock. */
516
+ readonly owner: Handle<I> | undefined;
391
517
  /** Live. Writes land on the next sync. */
392
518
  weight: number;
393
519
  /** Playback rate now. Setting it changes speed at once; `ramp` eases into a new one. */
@@ -425,12 +551,12 @@ export interface Handle<I = unknown> {
425
551
  *
426
552
  * @category mix
427
553
  */
428
- export interface MixOptions {
554
+ export interface MixOptions<H = unknown> {
429
555
  engine?: Engine;
430
556
  /** Reduced motion: fades and warm-ups snap, `dt` reads Infinity. */
431
557
  reduce?: boolean | (() => boolean);
432
- /** Merged into every `setting.host`. */
433
- host?: unknown;
558
+ /** Merged into every `setting.host`. Its type is the mix's `H`, which every patch and signal it cues reads. */
559
+ host?: H;
434
560
  /**
435
561
  * The band a rest-less channel's influence switches on and off across. It has to clear the frame
436
562
  * noise in a real signal; these are placeholders until the klieg port measures one.
@@ -514,11 +640,26 @@ export type Columns<O> = {
514
640
  *
515
641
  * @category mix
516
642
  */
517
- export interface Mix<I, O> {
643
+ export interface Mix<I, O, H = unknown> {
518
644
  /** Cues a voice. Throws when the engine cannot run the patch's form, or the kit lacks a channel. */
519
- cue(spec: VoiceSpec<I, O>): Handle<I>;
520
- /** N voices whose weights split one signal, cued into one locus so they fold as alternatives. */
521
- blend(patches: readonly Patch<I, O, unknown>[], by: Signal<I>, spec?: Omit<VoiceSpec<I, O>, 'patch' | 'weight' | 'locus'>): Handle<I>[];
645
+ cue(spec: VoiceSpec<I, O, H>): Handle<I>;
646
+ /**
647
+ * N voices whose weights split one signal, cued into one locus so they fold as alternatives. The
648
+ * signal is read once per subject per frame, with the first member's setting, and every member
649
+ * takes its share of that one reading; a signal keeping state keeps it once, not per member.
650
+ */
651
+ blend(patches: readonly Patch<I, O, unknown, H>[], by: Signal<I, H>, spec?: Omit<VoiceSpec<I, O, H>, 'patch' | 'weight' | 'locus'>): Handle<I>[];
652
+ /**
653
+ * Cues an owner: a voice with no patch that holds the voices cued with `owner` set to its handle,
654
+ * and plays them on its own clock, so they can be placed, timed and faded as one. Its handle acts
655
+ * on all of them: `rate` and `ramp` multiply into theirs, `seek` moves their clocks with its own
656
+ * and leaves their state where it is, and `weight` and `fade()` multiply into their weights, by
657
+ * what its `weightOf` reports; it fades as a whole, not by subject or at rest. It is
658
+ * `played` once every voice it held has finished its passes, and leaves, `done`, with its last; a
659
+ * fade that ends takes the rest with it. One that never holds a voice stays until faded. Owners
660
+ * nest. Throws for a `loop`: a pass would have to restart its children.
661
+ */
662
+ owns(spec: OwnerSpec<I, H>): Handle<I>;
522
663
  /** The host reports the clock, once a frame. Nothing advances at the call. */
523
664
  sync(timestamp: number): void;
524
665
  /**
@@ -526,7 +667,30 @@ export interface Mix<I, O> {
526
667
  * hidden tab comes back. Every voice, fade and subject resumes where it was left.
527
668
  */
528
669
  rebase(): void;
529
- /** The merged pose for one subject at the synced frame. */
670
+ /**
671
+ * The mix's own playback rate, multiplied into every voice's: 1 is real time and 0 pauses the
672
+ * whole mix. A negative rate throws, since the mix only goes forward. Setting it changes speed at
673
+ * once; `ramp` eases into a new one. It scales mix time, which everything a voice owns runs on:
674
+ * its clock, its fades and a handle's ramp, `stagger`, an anchor's `by`, `stepMs`, `maxDt`,
675
+ * `history.ms`, every `dt`, and `setting.timestamp`. Timestamps on the host's clock name the same
676
+ * moment whatever the rate does: a `start`, an anchor given as a number, an announced mark, what
677
+ * `marks` reports and the time `project` reads, so a start ahead of a paused mix waits for the
678
+ * host's clock to reach it. A weight signal is still asked at every probe, so one reading host
679
+ * input follows it while the mix is paused.
680
+ */
681
+ rate: number;
682
+ /**
683
+ * Moves the mix's rate to `rate` linearly over `over` ms of the host's clock, as `Handle.ramp`
684
+ * moves a voice's. Mix time integrates the ramp and a voice's clock integrates mix time, so a ramp
685
+ * on both multiplies and every position stays continuous. A later `rate` write or `ramp` replaces
686
+ * it.
687
+ */
688
+ ramp(rate: number, over: number): void;
689
+ /**
690
+ * The merged pose for one subject at the synced frame, written into `out` when given. Without
691
+ * it, each call makes a new object, so a host reading every subject every frame passes `out` or
692
+ * reads by `pull`.
693
+ */
530
694
  probe(subject: I, out?: O): O;
531
695
  /**
532
696
  * Writes each subject's pose into arrays, one per channel, subject by subject in the order given:
@@ -544,14 +708,24 @@ export interface Mix<I, O> {
544
708
  * older than the history reaches.
545
709
  */
546
710
  project(timestamp: number): Projection<I, O>;
547
- /** Every channel at rest for this subject this frame, so a host can skip the write. */
711
+ /**
712
+ * Every channel at rest for this subject this frame, so a host can skip the write. After a probe
713
+ * of the subject this frame, it answers for the pose that probe gave, without folding again.
714
+ */
548
715
  atRest(subject: I): boolean;
716
+ /**
717
+ * A handle on every voice still in the mix, pending, live, held or fading, in the order they were
718
+ * cued; given a tag, only the voices whose `tags` carry it. Each is the handle `cue` returned for
719
+ * that voice, so a listed handle is `===` the cued one.
720
+ */
721
+ voices(tag?: string): Handle<I>[];
549
722
  /** Anything still contributing, fading, or pending. */
550
723
  readonly live: boolean;
551
724
  /**
552
725
  * Another frame would change no pose, so a host's frame loop may sleep: every voice is done, held
553
- * at a plain weight, or a motion whose every subject has landed on its target. A change made since
554
- * the last sync, a retarget included, makes it false until the next.
726
+ * at a plain weight, or a motion whose every subject has landed on its target. At rate 0 it is
727
+ * enough that every voice not done has started, at a plain weight, with no anchor. A change made
728
+ * since the last sync, a retarget or a rate included, makes it false until the next.
555
729
  */
556
730
  readonly inert: boolean;
557
731
  /** Fades every voice out: over `over` when given, over each voice's own `fade.out` otherwise. */
@@ -588,6 +762,15 @@ export interface Mix<I, O> {
588
762
  * whoever drains them.
589
763
  */
590
764
  drain<E = unknown>(tag?: string): Sent<I, E>[];
765
+ /**
766
+ * Hands the host each mark and hit before it comes, against a clock of its own, so it can schedule
767
+ * the thing itself: a sound on an `AudioContext`, a MIDI message with a timestamp, a video loaded
768
+ * before the voice that shows it starts. Booking runs at each `sync`, `ahead` ms out, and a
769
+ * booking whose time moves is stopped and taken again. Nothing is booked twice: a mark is known by
770
+ * its voice and which mark, a hit by its voice, index and pass. `project` books nothing, and
771
+ * nothing about booking is replayed under `history`. A mix with no booker pays nothing for it.
772
+ */
773
+ book<E = unknown>(opts: BookOptions<E>): Booker;
591
774
  }
592
775
  /**
593
776
  * The implementation behind a mix: which patch forms it runs, and how it makes one.
@@ -598,5 +781,5 @@ export interface Engine {
598
781
  readonly name: string;
599
782
  /** Which patch forms this engine can run. A voice it cannot run is refused at `cue`, by name. */
600
783
  readonly runs: ReadonlySet<'fn' | 'keys' | 'motion'>;
601
- create<I, O>(kit: Kit<O>, opts: MixOptions): Mix<I, O>;
784
+ create<I, O, H = unknown>(kit: Kit<O>, opts: MixOptions<H>): Mix<I, O, H>;
602
785
  }
@@ -0,0 +1,5 @@
1
+ import type { Voice } from './voice.js';
2
+ /** Whether the voice's shared unreached record stands for the subject numbered `slot`. */
3
+ export declare function unreached<I, O>(voice: Voice<I, O>, slot: number): boolean;
4
+ /** Marks or clears the subject numbered `slot` as one the voice does not reach. */
5
+ export declare function unreach<I, O>(voice: Voice<I, O>, slot: number, on: boolean): void;