@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/mixer.js CHANGED
@@ -1,359 +1,29 @@
1
- import { clampWeight, envelope, passAt, passesOf, phaseAt } from './clock.js';
2
- import { curve } from './easing.js';
3
- import { clampRun, Lanes } from './lanes.js';
4
- import { motionOf, noFrame, noRevive } from './motion.js';
5
- import { builtOf, intosOf, readKeys } from './patch.js';
6
- import { reading } from './reading.js';
1
+ import { blended, blendOf, laneRead, shareKept } from './blend.js';
2
+ import { aims, chain, linkable } from './chain.js';
3
+ import { lerpInto } from './channels.js';
4
+ import { cue } from './cue.js';
5
+ import { beginFade, parting } from './fade.js';
6
+ import { apply, baseFor, clamp, fold, folded, foldLoci, foldWith, influence, isRest, keyed, linked, near, owedBy, passes, read, slopeFor, tick, } from './fold.js';
7
+ import { held, shownOf, sinceOf, unreachedOf } from './held.js';
8
+ import { book, laneHost } from './hosts.js';
9
+ import { Lanes } from './lanes.js';
10
+ import { foldLocus } from './locus.js';
11
+ import { listed } from './marks.js';
12
+ import { move, nextFrame, waits } from './move.js';
13
+ import { ownerPatch } from './owner.js';
14
+ import { Pace } from './pace.js';
15
+ import { pin } from './place.js';
16
+ import { project } from './project.js';
17
+ import { keep, pull } from './pull.js';
18
+ import { Steps } from './relink.js';
7
19
  import { Store } from './store.js';
8
- const none = Object.freeze([]);
9
- const keeper = (kept) => function keep(owner, init) {
10
- if (kept.has(owner))
11
- return kept.get(owner);
12
- const made = init();
13
- kept.set(owner, made);
14
- reading.kept++;
15
- return made;
16
- };
17
- /** The first record of a subject no live voice reaches, so a probe of it still makes one lookup. */
18
- function stub() {
19
- const kept = new Map();
20
- return {
21
- reaches: false,
22
- delay: 0,
23
- since: 0,
24
- shown: 0,
25
- weight: 0,
26
- rested: false,
27
- bands: new Uint8Array(0),
28
- state: undefined,
29
- stepped: 0,
30
- ticks: 0,
31
- probed: Number.NaN,
32
- delta: null,
33
- phase: 0,
34
- seeks: 0,
35
- kept,
36
- keep: keeper(kept),
37
- voice: null,
38
- next: null,
39
- version: Number.NaN,
40
- loci: null,
41
- slot: -1,
42
- };
43
- }
44
- function elapsedWith(c, now) {
45
- const dt = now - c.anchorNow;
46
- const r = c.ramp;
47
- if (r === null)
48
- return c.anchorElapsed + dt * c.rate;
49
- if (dt <= 0)
50
- return c.anchorElapsed + dt * r.from;
51
- const d = r.to - r.from;
52
- if (dt <= r.over)
53
- return c.anchorElapsed + r.from * dt + (d * dt * dt) / (2 * r.over);
54
- return c.anchorElapsed + r.from * r.over + (d * r.over) / 2 + r.to * (dt - r.over);
55
- }
56
- /**
57
- * The last entry taken before `t`, or at it where `inclusive` says so, as it does for a change a
58
- * sync made. A host's change between frames is taken strictly: it shows from the next frame on, as
59
- * it did live.
60
- */
61
- function last(list, t, inclusive) {
62
- let lo = 0;
63
- let hi = list.length - 1;
64
- let found;
65
- while (lo <= hi) {
66
- const mid = (lo + hi) >> 1;
67
- const e = list[mid];
68
- if (e.at < t || (e.at === t && (typeof inclusive === 'function' ? inclusive(e) : inclusive))) {
69
- found = e;
70
- lo = mid + 1;
71
- }
72
- else
73
- hi = mid - 1;
74
- }
75
- return found;
76
- }
77
- /** A store that fills a missing entry from `make` on first ask. */
78
- class Filled extends Store {
79
- make;
80
- constructor(make) {
81
- super();
82
- this.make = make;
83
- }
84
- get(key) {
85
- let v = super.get(key);
86
- if (v === undefined) {
87
- v = this.make(key);
88
- if (v !== undefined)
89
- super.set(key, v);
90
- }
91
- return v;
92
- }
93
- }
94
- const noSend = () => { };
95
- const near = (a, b) => {
96
- if (typeof a === 'number' && typeof b === 'number')
97
- return Math.abs(a - b) < 1e-9;
98
- if (Array.isArray(a) && Array.isArray(b))
99
- return a.length === b.length && a.every((v, i) => near(v, b[i]));
100
- return a === b;
101
- };
102
- const copy = (v) => (Array.isArray(v) ? [...v] : v);
103
- /** Writes a folded pose's value into subject `n`'s places in a column. */
104
- function writeValue(c, v, n) {
105
- const at = n * c.axes;
106
- if (typeof v === 'number' && c.axes === 1)
107
- c.out[at] = v;
108
- else if (Array.isArray(v) && v.length === c.axes)
109
- for (let a = 0; a < c.axes; a++)
110
- c.out[at + a] = v[a];
111
- else if (v === undefined)
112
- c.out.fill(Number.NaN, at, at + c.axes);
113
- else
114
- throw new TypeError(`blits: pull reads ${c.key} as ${c.axes === 1 ? 'a number' : `${c.axes} numbers`} a subject, and the pose has ${String(v)}`);
115
- }
116
- /** Deep equality over plain data, for telling whether a host field changed. */
117
- function same(a, b) {
118
- if (a === b)
119
- return true;
120
- if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null)
121
- return false;
122
- const ka = Object.keys(a);
123
- const kb = Object.keys(b);
124
- if (ka.length !== kb.length)
125
- return false;
126
- for (const k of ka)
127
- if (!same(a[k], b[k]))
128
- return false;
129
- return true;
130
- }
131
- export class Voice {
132
- id;
133
- spec;
134
- patch;
135
- fade;
136
- start;
137
- state = 'pending';
138
- rate;
139
- weight;
140
- /**
141
- * elapsed = anchorElapsed + the rate integrated from anchorNow, rebased on a rate change or a seek.
142
- * Without a ramp the rate is constant; with one it moves linearly from `from` to `to` over `over`
143
- * mix ms starting at the anchor, and holds `to` after.
144
- */
145
- anchorNow;
146
- anchorElapsed = 0;
147
- ramp = null;
148
- out = null;
149
- subjects = new Store();
150
- /** The subjects its spec names, or null where it names none. */
151
- named;
152
- /** Kit slot of each channel the patch writes, in `writes` order. */
153
- slots;
154
- /** The stops built ahead of time, for a `keys` patch. */
155
- built;
156
- /** The kit's `lerp` for each channel the patch writes, which keyed stops interpolate through. */
157
- lerps;
158
- /** Per keyed channel, the in-place form of the lerp its stops take, where it has one. */
159
- intos;
160
- /**
161
- * The arrays keyed reads interpolate into: one set for the voice, not one per subject, since 10k
162
- * subjects' worth of long-lived arrays costs more in cache misses than allocating them did.
163
- */
164
- scratch = [];
165
- /** The record whose delta holds `scratch`'s arrays; any other's must be read again to be handed out. */
166
- holder = null;
167
- /** Whether its channels run as lanes this frame, so the general fold passes it by. */
168
- laned = false;
169
- /** How many times it has been sought, so a delta read before a seek is not handed out after it. */
170
- seeks = 0;
171
- /** Whether its patch has kept state on a record through `setting.keep`, which makes it stateful. */
172
- keeping = false;
173
- /** For a motion patch, the hook it was given to ask the mix for its subjects' voice time. */
174
- frame = null;
175
- /** For a motion patch, the hook it was given to bring a subject faded out of this voice back. */
176
- revive = null;
177
- /** Subjects fading out of this voice alone, by the ramp each started; null while none are. */
178
- parts = null;
179
- /** Subjects faded out of this voice, by the mix time each left at; null while none have. */
180
- parted = null;
181
- ease;
182
- /** How many passes its `loop` plays, worked out once. */
183
- passes;
184
- /** Voice ms its passes last, Infinity for an aperiodic patch or a loop for good. */
185
- span;
186
- /** The state behind a motion patch, undefined for any other. */
187
- motion;
188
- /** Its `hold`, for a patch it applies to: motion holds its target already. */
189
- holdsBefore;
190
- holdsAfter;
191
- /** The mix time it first showed: when it was cued, or the first sync after. */
192
- opened = Number.NaN;
193
- /** Which of its entries in the mix's due queue is current; older ones are skipped when popped. */
194
- dueToken = 0;
195
- /** Records made while it was pending, whose `since` its start may since have moved. */
196
- early = null;
197
- /** Reused for every call this voice makes, so it is valid only during the call. */
198
- setting;
199
- /** Every subject this voice has been asked about, so a handover knows when it is finished. */
200
- seen = 0;
201
- /** The longest stagger of any subject seen, so a finite loop waits for the last of them. */
202
- latest = 0;
203
- restedCount = 0;
204
- /** The mix time it was cued at; -Infinity before the first sync. */
205
- cuedAt = Number.NEGATIVE_INFINITY;
206
- /** The mix time it left at. */
207
- doneAt = Number.POSITIVE_INFINITY;
208
- /** Under `history`, its controls after each change, oldest first. */
209
- log = null;
210
- /** Where an anchored `out` or `end` puts its fade's start, mix time; Infinity until known. */
211
- outAt = Number.POSITIVE_INFINITY;
212
- /** Whether its start is still to be fixed by an anchor, so it waits pending. */
213
- placing = false;
214
- resolve;
215
- done;
216
- play;
217
- played;
218
- constructor(id, spec, patch, fade, now, start, slotOf, channels, host, send) {
219
- this.id = id;
220
- this.spec = spec;
221
- this.patch = patch;
222
- this.fade = fade;
223
- this.start = start;
224
- this.slots = patch.writes.map((k) => slotOf.get(k));
225
- this.named = spec.subjects ? new Set(spec.subjects) : null;
226
- this.built = patch.form === 'keys' && patch.keys ? builtOf(patch) : null;
227
- this.lerps = this.slots.map((slot) => channels[slot].lerp);
228
- this.intos = this.built
229
- ? intosOf(this.built, this.slots.map((slot) => channels[slot]))
230
- : undefined;
231
- this.ease = fade.ease === undefined ? undefined : curve(fade.ease);
232
- this.passes = passesOf(spec.loop);
233
- this.span =
234
- patch.period > 0 && Number.isFinite(this.passes)
235
- ? patch.period * this.passes
236
- : Number.POSITIVE_INFINITY;
237
- this.motion = motionOf(patch);
238
- const holds = this.motion === undefined ? spec.hold : undefined;
239
- this.holdsBefore = holds === 'before' || holds === 'both';
240
- this.holdsAfter = holds === 'after' || holds === 'both';
241
- this.setting = {
242
- timestamp: 0,
243
- dt: 0,
244
- elapsed: 0,
245
- pass: 0,
246
- weight: 0,
247
- state: undefined,
248
- host,
249
- keep: keeper(new Map()),
250
- send,
251
- };
252
- this.rate = spec.rate ?? 1;
253
- this.weight = typeof spec.weight === 'number' ? spec.weight : 1;
254
- this.anchorNow = start;
255
- this.done = new Promise((r) => {
256
- this.resolve = r;
257
- });
258
- this.played = new Promise((r) => {
259
- this.play = r;
260
- });
261
- if (now >= start)
262
- this.state = 'live';
263
- }
264
- elapsedAt(now) {
265
- return elapsedWith(this, now);
266
- }
267
- /** The mix time its clock reads `elapsed`, inverting `elapsedAt`; Infinity where it never will. */
268
- timeAt(elapsed) {
269
- const e0 = this.anchorElapsed;
270
- const r = this.ramp;
271
- if (r === null) {
272
- if (this.rate > 0)
273
- return this.anchorNow + (elapsed - e0) / this.rate;
274
- return elapsed <= e0 ? this.anchorNow : Number.POSITIVE_INFINITY;
275
- }
276
- if (elapsed <= e0)
277
- return r.from > 0 ? this.anchorNow + (elapsed - e0) / r.from : this.anchorNow;
278
- const a = (r.to - r.from) / (2 * r.over);
279
- const atEnd = e0 + r.from * r.over + a * r.over * r.over;
280
- if (elapsed <= atEnd) {
281
- const c = e0 - elapsed;
282
- const dt = Math.abs(a) < 1e-12
283
- ? -c / r.from
284
- : (-r.from + Math.sqrt(Math.max(0, r.from * r.from - 4 * a * c))) / (2 * a);
285
- return this.anchorNow + dt;
286
- }
287
- if (r.to <= 0)
288
- return Number.POSITIVE_INFINITY;
289
- return this.anchorNow + r.over + (elapsed - atEnd) / r.to;
290
- }
291
- /** Records this voice's controls as they stand, from mix time `at`. */
292
- note(at, sync = false) {
293
- this.log?.push({
294
- at,
295
- sync,
296
- anchorNow: this.anchorNow,
297
- anchorElapsed: this.anchorElapsed,
298
- rate: this.rate,
299
- ramp: this.ramp,
300
- weight: this.weight,
301
- out: this.out,
302
- start: this.start,
303
- outAt: this.outAt,
304
- });
305
- }
306
- /**
307
- * A copy for a projection: its own setting, which sends nothing, and its own per-subject records,
308
- * filled from `fill`. With `controls` it takes those, as the voice stood at an earlier time.
309
- */
310
- copy(fill, controls) {
311
- const v = Object.assign(Object.create(Voice.prototype), this);
312
- const w = v;
313
- w.subjects = new Filled(fill);
314
- w.setting = { ...this.setting, keep: keeper(new Map()), send: noSend };
315
- v.scratch = [];
316
- v.holder = null;
317
- v.laned = false;
318
- v.log = null;
319
- v.early = null;
320
- if (controls) {
321
- v.anchorNow = controls.anchorNow;
322
- v.anchorElapsed = controls.anchorElapsed;
323
- v.rate = controls.rate;
324
- v.ramp = controls.ramp;
325
- v.weight = controls.weight;
326
- v.out = controls.out;
327
- v.start = controls.start;
328
- v.outAt = controls.outAt;
329
- }
330
- v.resolve = noSend;
331
- v.play = noSend;
332
- return v;
333
- }
334
- rateAt(now) {
335
- const r = this.ramp;
336
- if (r === null)
337
- return this.rate;
338
- const u = (now - this.anchorNow) / r.over;
339
- return u >= 1 ? r.to : u <= 0 ? r.from : r.from + (r.to - r.from) * u;
340
- }
341
- /** Moves the anchor to `now`, carrying what is left of a ramp. */
342
- rebase(now) {
343
- const rate = this.rateAt(now);
344
- this.anchorElapsed = this.elapsedAt(now);
345
- const r = this.ramp;
346
- if (r !== null) {
347
- const left = this.anchorNow + r.over - now;
348
- this.ramp = left > 0 ? { from: rate, to: r.to, over: left } : null;
349
- }
350
- this.anchorNow = now;
351
- }
352
- }
353
- class Mixer {
20
+ import { none } from './voice.js';
21
+ import { base, capped, fadeOf, horizonFor, ownedBy, ownerBase, prime, weigh, weighOwned, } from './weigh.js';
22
+ // biome-ignore lint/suspicious/noUnsafeDeclarationMerging: every member of `Methods` is installed below
23
+ export class Mixer {
354
24
  kit;
355
25
  opts;
356
- voices = [];
26
+ cued = [];
357
27
  /** Under `history`, voices that have left but that a read back may still reach. */
358
28
  gone = [];
359
29
  /** Set on a projection's own mixer: it sends nothing, keeps no history, and reads without committing. */
@@ -368,17 +38,28 @@ class Mixer {
368
38
  */
369
39
  hostLog = [];
370
40
  /**
371
- * Marks the host announced on a score: mix time, NaN until the next sync for one announced as
372
- * now before any sync; `made` is when it was announced, for a read back to know what was known.
41
+ * Marks the host announced on a score: `at` in host time, so the mix's rate never moves one, and
42
+ * NaN until the next sync for one announced as now before any sync; `made` is the mix time it was
43
+ * announced, for a read back to know what was known.
373
44
  */
374
45
  announced = [];
375
46
  hostThen;
376
47
  /** Each subject's last two poses and when they were probed, for `from: 'current'`. */
377
48
  pose = new Store();
378
49
  nextId = 1;
379
- /** The mix clock: the host's timestamp less every gap `rebase` has taken out. */
50
+ /**
51
+ * The mix clock: host time at the mix's rate. Host time is the host's timestamp less every gap
52
+ * `rebase` has taken out, `u` at the last sync.
53
+ */
380
54
  now = Number.NaN;
55
+ u = Number.NaN;
56
+ /** This mix's frame among every mix's: moved by a sync, and by a control change or drop within one. */
57
+ frame = nextFrame();
381
58
  offset = 0;
59
+ /** The mix's own rate; null while it has never been set, when mix time is host time. */
60
+ pace = null;
61
+ /** Pending voices whose start the host gave, by its host time, kept where the rate puts it. */
62
+ pins = null;
382
63
  last = Number.NaN;
383
64
  rebasing = false;
384
65
  wantsPose = false;
@@ -387,8 +68,8 @@ class Mixer {
387
68
  /** The last array `pull` read, by position, with each subject's chain head, to skip the lookup. */
388
69
  /** Live voices by the earliest mix time `moveTo` would change each, a binary heap. */
389
70
  due = [];
390
- /** Voices retired since `moveTo` last pruned them. */
391
- retired = 0;
71
+ /** Voices retired since `moveTo` last pruned them, in the order they retired. */
72
+ retired = [];
392
73
  /** Visit every voice each sync, as before the due queue; for tests that compare the two. */
393
74
  walkAll = false;
394
75
  pulled = [];
@@ -403,6 +84,13 @@ class Mixer {
403
84
  reducedNow = false;
404
85
  /** Something was changed since the last sync, so the next frame may differ from this one. */
405
86
  stirred = false;
87
+ /** Counts the changes `stir` records, which a frame's record of a probe is current only before. */
88
+ stirs = 0;
89
+ /** Records a change to the mix's voices or controls. */
90
+ stir() {
91
+ this.stirred = true;
92
+ this.stirs++;
93
+ }
406
94
  /**
407
95
  * Per subject, the first record of the chain through every live voice that reaches it, or a stub
408
96
  * where none does. Relinked when `version` moves, which is whenever the list or a voice's pending
@@ -410,6 +98,7 @@ class Mixer {
410
98
  */
411
99
  chains = new Store();
412
100
  version = 0;
101
+ steps = new Steps();
413
102
  /** Per subject, the voices whose `subjects` name it, in voice order. */
414
103
  named = new Store();
415
104
  /** Voices a subject has been faded out of, which `drop` looks in besides those that reach it. */
@@ -429,6 +118,8 @@ class Mixer {
429
118
  general = [];
430
119
  /** How many voices in the list carry a locus; with none, a fold allocates nothing. */
431
120
  loci = 0;
121
+ /** What every motion patch it plays asks for its voice's time, holding this mix weakly. */
122
+ owner = null;
432
123
  /** How many voices in the list are anchored, so a sync with none skips placing them. */
433
124
  anchored = 0;
434
125
  /** Events sent since the last drain, and who is being probed, so `send` knows whose they are. */
@@ -455,18 +146,29 @@ class Mixer {
455
146
  channels;
456
147
  /** Slots of the channels that declare bounds, clamped after every fold. */
457
148
  bounded;
149
+ /** By slot, a stock array channel's in-place `lerp`. */
150
+ lerpsInto;
151
+ /** What a fold with a locus in play gathers into, one per depth of folds under way. */
152
+ locusScratch = [];
153
+ locusDepth = 0;
458
154
  slotOf = new Map();
459
155
  lanes;
156
+ handles = null;
157
+ /** What `book` made, still booking; null while there is none. */
158
+ bookers = null;
159
+ /** Every owner still in the mix, null until one is cued. */
160
+ owners = null;
460
161
  constructor(kit, opts) {
461
162
  this.kit = kit;
462
163
  this.opts = opts;
463
164
  this.names = Object.keys(kit);
464
165
  this.channels = this.names.map((k) => kit[k]);
465
166
  this.bounded = this.channels.flatMap((c, i) => (c.bounds ? [i] : []));
167
+ this.lerpsInto = this.channels.map((c) => lerpInto(c));
466
168
  this.names.forEach((k, i) => {
467
169
  this.slotOf.set(k, i);
468
170
  });
469
- this.lanes = opts.lanes === false ? null : new Lanes(this.laneHost());
171
+ this.lanes = opts.lanes === false ? null : new Lanes(laneHost(this));
470
172
  }
471
173
  get reduced() {
472
174
  const r = this.opts.reduce;
@@ -476,536 +178,71 @@ class Mixer {
476
178
  return this.opts.band ?? { on: 0.6, off: 0.4 };
477
179
  }
478
180
  cue(spec) {
479
- const patch = spec.patch;
480
181
  const engine = this.opts.engine ?? mixer;
481
- if (!engine.runs.has(patch.form))
482
- throw new Error(`blits: engine ${engine.name} does not run ${patch.form} patches`);
483
- for (const channel of patch.writes) {
484
- if (!(channel in this.kit))
485
- throw new Error(`blits: kit has no channel ${String(channel)}, which this patch writes`);
486
- const wanted = patch.kit?.[channel];
487
- const here = this.kit[channel];
488
- if (wanted && wanted !== here && (wanted.kind === undefined || wanted.kind !== here.kind))
489
- throw new Error(`blits: channel ${String(channel)} is ${here.kind ?? 'a custom channel'} in this kit, but the patch was written for ${wanted.kind ?? 'a custom channel'}`);
490
- }
491
- if (patch.reads) {
492
- const host = this.opts.host;
493
- for (const field of patch.reads)
494
- if (typeof host !== 'object' || host === null || !(field in host))
495
- throw new Error(`blits: the patch reads host.${field}, which this mix's host lacks`);
496
- }
497
- if (spec.from === 'current' && patch.form !== 'keys')
498
- throw new Error("blits: from: 'current' needs a keys patch");
499
- if (spec.subjects !== undefined && spec.target !== undefined)
500
- throw new Error('blits: a voice takes target or subjects, not both');
501
- const anchor = spec.anchor;
502
- if (anchor)
503
- this.checkPlacement(spec, anchor);
504
- const anchored = anchor !== undefined && (anchor.start !== undefined || anchor.in !== undefined);
505
- const start = anchored
506
- ? Number.POSITIVE_INFINITY
507
- : spec.start !== undefined
508
- ? spec.start - this.offset
509
- : Number.isNaN(this.now)
510
- ? 0
511
- : this.now;
512
- const voice = new Voice(this.nextId++, spec, patch, spec.fade ?? {}, this.now, start, this.slotOf, this.channels, this.opts.host, this.send);
513
- if (!Number.isNaN(this.now)) {
514
- voice.cuedAt = this.now;
515
- voice.opened = this.now;
516
- }
517
- voice.placing = anchored;
518
- const motion = voice.motion;
519
- if (motion !== undefined) {
520
- this.playing.set(motion, (this.playing.get(motion) ?? 0) + 1);
521
- voice.frame = Mixer.frameHook(new WeakRef(this), new WeakRef(voice));
522
- motion.frame = voice.frame;
523
- voice.revive = Mixer.reviveHook(new WeakRef(this), new WeakRef(voice));
524
- motion.revive = voice.revive;
525
- }
526
- this.voices.push(voice);
527
- this.index(voice);
528
- this.changed(voice);
529
- this.stirred = true;
530
- if (spec.locus !== undefined)
531
- this.loci++;
532
- if (spec.from === 'current')
533
- this.wantsPose = true;
534
- if (anchor) {
535
- this.anchored++;
536
- this.place();
537
- if (!Number.isNaN(this.now) && voice.state === 'pending' && this.now >= voice.start) {
538
- voice.state = 'live';
539
- this.started(voice);
540
- this.changed(voice);
541
- }
542
- }
543
- // After placing, so the controls it starts with are where its anchors put it at the cue.
544
- if (this.opts.history) {
545
- voice.log = [];
546
- voice.note(Number.NEGATIVE_INFINITY);
547
- }
548
- this.schedule(voice);
549
- return this.handle(voice);
182
+ if (!engine.runs.has(spec.patch.form))
183
+ throw new Error(`blits: engine ${engine.name} does not run ${spec.patch.form} patches`);
184
+ return cue(this, spec);
550
185
  }
551
186
  blend(patches, by, spec = {}) {
552
187
  const locus = `blend:${this.nextId}`;
553
- const stops = patches.length - 1;
188
+ const of = blendOf(by, patches.length - 1);
554
189
  return patches.map((patch, i) => {
555
- const share = (subject, setting) => {
556
- const k = by(subject, setting);
557
- const d = stops === 0 ? 0 : Math.abs(k * stops - i);
558
- return d >= 1 ? 0 : 1 - d;
559
- };
560
- const weight = by.input ? Object.assign(share, { input: true }) : share;
561
- return this.cue({ ...spec, patch, weight, locus });
190
+ const handle = this.cue({ ...spec, patch, weight: by, locus });
191
+ const voice = this.cued[this.cued.length - 1];
192
+ voice.blend = { of, i };
193
+ of.members.push(voice);
194
+ return handle;
562
195
  });
563
196
  }
197
+ owns(spec) {
198
+ if (spec.loop !== undefined)
199
+ throw new Error('blits: an owner does not loop: a pass would have to restart its children');
200
+ return this.cue({ ...spec, patch: ownerPatch });
201
+ }
564
202
  sync(timestamp) {
565
203
  if (this.rebasing && !Number.isNaN(this.last))
566
204
  this.offset += timestamp - this.last;
567
205
  this.rebasing = false;
568
206
  this.last = timestamp;
569
- const now = timestamp - this.offset;
570
- if (now === this.now)
571
- return;
572
- this.move(now);
573
- }
574
- /** Moves the mix clock to `now`: voices start, finite loops end, and fades finish. */
575
- move(now) {
576
- this.moving = true;
577
- try {
578
- this.moveTo(now);
579
- }
580
- finally {
581
- this.moving = false;
582
- }
583
- }
584
- moveTo(now) {
585
- this.now = now;
586
- this.reducedNow = this.reduced;
587
- for (const a of this.announced)
588
- if (Number.isNaN(a.at))
589
- a.at = now;
590
- if (this.anchored > 0)
591
- this.place();
592
- if (this.announced.length > 0) {
593
- // Kept while still ahead, or while history reaches it; anchors waiting on one were placed above.
594
- const reach = now - (this.opts.history?.ms ?? 0);
595
- this.announced = this.announced.filter((a) => a.at >= reach);
596
- }
597
- // A live mix visits only the voices due by now; a projection, which copies few, visits all.
598
- const visit = this.projecting || this.walkAll ? this.voices : this.popDue(now);
599
- for (const voice of visit) {
600
- if (voice.state === 'done')
601
- continue;
602
- if (Number.isNaN(voice.opened))
603
- voice.opened = now;
604
- if ((voice.state === 'live' || voice.state === 'held') && voice.outAt <= now)
605
- this.beginFade(voice, {}, Math.max(voice.start, voice.outAt));
606
- if (voice.state === 'pending' && now >= voice.start) {
607
- voice.state = 'live';
608
- this.started(voice);
609
- this.changed(voice);
610
- }
611
- if (voice.state !== 'pending' && Number.isFinite(voice.span)) {
612
- const end = voice.span + voice.latest;
613
- if (voice.elapsedAt(now) >= end) {
614
- voice.play(true);
615
- // The fade starts when the last pass ended, not at the frame that noticed, so it plays
616
- // the same at any frame rate and a read at another time can find it.
617
- if (voice.state === 'live') {
618
- if (voice.holdsAfter)
619
- voice.state = 'held';
620
- else
621
- this.beginFade(voice, {}, Math.max(voice.start, Math.min(now, voice.timeAt(end))));
622
- }
623
- }
624
- else if (voice.state === 'held')
625
- voice.state = 'live';
626
- }
627
- // A projection reads a finished ramp as weight 0, and leaves the live voice's subjects be.
628
- if (voice.parts !== null && !this.projecting)
629
- for (const [subject, r] of voice.parts)
630
- if (now - r.at >= r.over)
631
- this.part(voice, subject, r.at + r.over);
632
- if (voice.state === 'fading' && voice.out) {
633
- const { at, over, rest, deadline } = voice.out;
634
- const spent = now - at;
635
- if (rest) {
636
- const out = deadline !== undefined && spent >= deadline;
637
- const settled = voice.seen > 0 && voice.restedCount >= voice.seen;
638
- if (out)
639
- this.retire(voice, at + deadline);
640
- else if (settled)
641
- this.retire(voice);
642
- }
643
- else if (spent >= over)
644
- this.retire(voice, at + over);
645
- }
646
- if (!this.projecting)
647
- this.schedule(voice);
648
- }
649
- this.stirred = false;
650
- if (this.retired === 0) {
651
- this.forget(now);
652
- return;
653
- }
654
- this.retired = 0;
655
- let pruned = false;
656
- let kept = 0;
657
- const voices = this.voices;
658
- for (let i = 0; i < voices.length; i++) {
659
- const voice = voices[i];
660
- if (voice.state !== 'done') {
661
- voices[kept++] = voice;
662
- continue;
663
- }
664
- if (this.opts.history)
665
- this.gone.push(voice);
666
- else
667
- this.parters.delete(voice);
668
- this.unplay(voice);
669
- this.changed(voice);
670
- if (voice.named === null)
671
- pruned = true;
672
- else
673
- this.unindex(voice);
674
- if (voice.spec.locus !== undefined)
675
- this.loci--;
676
- if (voice.spec.anchor !== undefined)
677
- this.anchored--;
678
- }
679
- voices.length = kept;
680
- if (pruned)
681
- this.general = this.general.filter((v) => v.state !== 'done');
682
- this.forget(now);
683
- }
684
- /** Lets go of the voices that have left and that history no longer reaches. */
685
- forget(now) {
686
- const history = this.opts.history;
687
- if (history && this.gone.length > 0) {
688
- const reach = now - history.ms;
689
- this.gone = this.gone.filter((v) => v.doneAt >= reach);
690
- }
691
- }
692
- /**
693
- * The earliest mix time `moveTo` would change a voice: its start, its anchored out, the end of
694
- * its last pass, the end of its fade or of a subject's ramp out; -Infinity where it must be
695
- * visited every frame, Infinity where nothing will happen until a handle or anchor changes it.
696
- * Early is safe, since a visit that finds nothing due schedules again; late is not, so anything
697
- * that can bring it earlier calls `schedule`.
698
- */
699
- dueOf(voice) {
700
- if (voice.state === 'done')
701
- return Number.POSITIVE_INFINITY;
702
- if (Number.isNaN(voice.opened))
703
- return Number.NEGATIVE_INFINITY;
704
- let due = Number.POSITIVE_INFINITY;
705
- if (voice.parts !== null)
706
- for (const r of voice.parts.values())
707
- due = Math.min(due, r.at + r.over);
708
- if (voice.state === 'pending')
709
- return Math.min(due, Number.isNaN(voice.start) ? due : voice.start);
710
- if (voice.state === 'fading') {
711
- const out = voice.out;
712
- if (out === null || out.rest)
713
- return Number.NEGATIVE_INFINITY;
714
- return Math.min(due, out.at + out.over);
715
- }
716
- due = Math.min(due, voice.outAt);
717
- if (Number.isFinite(voice.span)) {
718
- const end = voice.span + voice.latest;
719
- // A held voice goes live again once its clock is back before its end: by a seek, by a
720
- // subject staggered later than any before, or by running backwards, which is checked each
721
- // frame while it can.
722
- if (voice.state === 'held') {
723
- if (voice.rate <= 0 || voice.ramp !== null)
724
- return Number.NEGATIVE_INFINITY;
725
- if (!Number.isNaN(this.now) && voice.elapsedAt(this.now) < end)
726
- return Number.NEGATIVE_INFINITY;
727
- }
728
- if (voice.state === 'live')
729
- due = Math.min(due, voice.timeAt(end));
207
+ const u = timestamp - this.offset;
208
+ const pace = this.pace;
209
+ const now = pace === null ? u : pace.sync(u);
210
+ const later = u !== this.u;
211
+ const still = later && now === this.now;
212
+ this.u = u;
213
+ // Host time moving while the mix clock stands still lands what waits on host time or the host.
214
+ if (now !== this.now || (later && pace !== null && waits(this)))
215
+ move(this, now);
216
+ // It is a frame too, which asks every weight signal again, so one reading input follows it.
217
+ if (still) {
218
+ this.frame = nextFrame();
219
+ this.lanes?.refill();
730
220
  }
731
- return due;
221
+ const bookers = this.bookers;
222
+ if (bookers !== null)
223
+ for (const b of bookers)
224
+ b.sync();
732
225
  }
733
- /** Files a voice in the due queue at its current due, replacing any entry it had. */
734
- schedule(voice) {
735
- if (this.projecting)
736
- return;
737
- this.lanes?.voiceChanged(voice.id);
738
- const token = ++voice.dueToken;
739
- const due = this.dueOf(voice);
740
- if (due === Number.POSITIVE_INFINITY)
741
- return;
742
- const heap = this.due;
743
- heap.push({ at: Number.isNaN(due) ? Number.NEGATIVE_INFINITY : due, voice, token });
744
- let i = heap.length - 1;
745
- while (i > 0) {
746
- const up = (i - 1) >> 1;
747
- if (heap[up].at <= heap[i].at)
748
- break;
749
- [heap[up], heap[i]] = [heap[i], heap[up]];
750
- i = up;
751
- }
752
- }
753
- /** Takes every current entry due by `now` off the queue, in cue order. */
754
- popDue(now) {
755
- const heap = this.due;
756
- const at = (k) => k < heap.length ? heap[k].at : Number.POSITIVE_INFINITY;
757
- const out = [];
758
- while (heap.length > 0 && heap[0].at <= now) {
759
- const top = heap[0];
760
- const last = heap.pop();
761
- if (heap.length > 0) {
762
- heap[0] = last;
763
- let i = 0;
764
- for (;;) {
765
- const l = 2 * i + 1;
766
- const r = l + 1;
767
- const m0 = at(l) < at(i) ? l : i;
768
- const m = at(r) < at(m0) ? r : m0;
769
- if (m === i)
770
- break;
771
- [heap[m], heap[i]] = [heap[i], heap[m]];
772
- i = m;
773
- }
774
- }
775
- if (top.token === top.voice.dueToken && top.voice.state !== 'done') {
776
- top.voice.dueToken++;
777
- out.push(top.voice);
778
- }
779
- }
780
- if (out.length > 1)
781
- out.sort((a, b) => a.id - b.id);
782
- return out;
226
+ book(opts) {
227
+ return book(this, opts);
783
228
  }
784
229
  probe(subject, out) {
785
230
  const pose = this.fold(subject, out);
786
231
  this.keep(subject, pose, out);
232
+ // Where the lanes answer `atRest` from their own values, there is nothing to keep.
233
+ if (this.restStamps !== null && !(this.linkedLaned && this.restsLaned()))
234
+ this.restStamps.set(subject, 2 * this.restsKey() + (this.rests(pose) ? 1 : 0));
787
235
  return pose;
788
236
  }
789
- /** After a probe, keeps the pose a retarget from `'current'` reads as the subject's last. */
790
- keep(subject, pose, out) {
791
- // Keeping last frame's pose is free when the mix allocated it; a host sampling into its own
792
- // object only pays for the copy once something in the mix has asked to retarget from it.
793
- let kept;
794
- if (out === undefined)
795
- kept = pose;
796
- else if (this.wantsPose) {
797
- kept = {};
798
- for (const key of this.names) {
799
- const v = pose[key];
800
- if (v !== undefined)
801
- kept[key] = copy(v);
802
- }
803
- }
804
- if (kept !== undefined) {
805
- const rec = this.pose.get(subject);
806
- if (rec === undefined)
807
- this.pose.set(subject, { pose: kept, at: this.now, prev: undefined, prevAt: Number.NaN });
808
- else if (rec.at === this.now)
809
- rec.pose = kept;
810
- else {
811
- rec.prev = rec.pose;
812
- rec.prevAt = rec.at;
813
- rec.pose = kept;
814
- rec.at = this.now;
815
- }
816
- }
817
- }
818
237
  pull(subjects, into) {
819
- const columns = this.columnsOf(into);
820
- const lanes = this.lanes;
821
- const now = this.now;
822
- if (this.scratch === undefined)
823
- this.scratch = {};
824
- const scratch = this.scratch;
825
- let room = Number.POSITIVE_INFINITY;
826
- let tightest = '';
827
- for (const c of columns) {
828
- const fits = Math.floor(c.out.length / c.axes);
829
- if (fits < room) {
830
- room = fits;
831
- tightest = c.key;
832
- }
833
- }
834
- // Any other iterable is read into an array first, no further than one past the room there is.
835
- let list;
836
- if (Array.isArray(subjects))
837
- list = subjects;
838
- else {
839
- const taken = [];
840
- for (const subject of subjects)
841
- if (taken.push(subject) > room)
842
- break;
843
- list = taken;
844
- }
845
- if (list.length > room)
846
- throw new RangeError(`blits: pull's array for ${tightest} has room for ${room} subjects, and was given more`);
847
- const whole = lanes !== null && !this.wantsPose;
848
- // An array read again in the same order reuses each position's chain head while it is current:
849
- // all of them at once while nothing has relinked since the last pull, else one by one.
850
- const was = this.pulled;
851
- const heads = this.pulledHeads;
852
- was.length = list.length;
853
- heads.length = list.length;
854
- if (this.pulledSlots.length < list.length) {
855
- const slots = new Int32Array(Math.max(64, list.length));
856
- slots.set(this.pulledSlots);
857
- this.pulledSlots = slots;
858
- }
859
- const slots = this.pulledSlots;
860
- const version = this.version;
861
- const relinks = this.relinks;
862
- const live = !Number.isNaN(now);
863
- const current = live && this.pulledVersion === version && this.pulledRelinks === relinks;
864
- // Every subject read from the lanes, at the positions it had last time, goes in one run.
865
- const runs = live && whole;
866
- try {
867
- for (let n = 0; n < list.length; n++) {
868
- if (runs) {
869
- n = lanes.pullRun(slots, list, was, current ? null : heads, n, columns, now, this.version);
870
- if (n === list.length)
871
- break;
872
- }
873
- const subject = list[n];
874
- let slot = -1;
875
- if (live) {
876
- if (current && was[n] === subject)
877
- slot = slots[n];
878
- else {
879
- const kept = heads[n];
880
- const head = was[n] === subject && kept !== undefined && kept.version === version
881
- ? kept
882
- : this.chain(subject, now);
883
- was[n] = subject;
884
- heads[n] = head;
885
- slot = head.slot;
886
- slots[n] = slot;
887
- }
888
- }
889
- const laned = live && lanes?.prepare(slot, subject, now, this.version) === true;
890
- if (laned && whole && lanes.whole) {
891
- lanes.writeLater(slot, columns, n);
892
- continue;
893
- }
894
- const head = live ? heads[n] : null;
895
- const folded = this.foldWith(subject, scratch, head, laned, false);
896
- const pose = (this.bounded.length === 0 ? folded : this.clamp(folded));
897
- this.keep(subject, pose, scratch);
898
- for (const c of columns)
899
- writeValue(c, pose[c.key], n);
900
- }
901
- }
902
- finally {
903
- lanes?.flush();
904
- }
905
- // Every head is current only if nothing relinked or changed version while they were read.
906
- const unchanged = live && this.version === version && this.relinks === relinks;
907
- this.pulledVersion = unchanged ? version : Number.NaN;
908
- this.pulledRelinks = relinks;
909
- }
910
- /** The kit slot, width, rest and bounds of every channel `pull` was handed an array for. */
911
- columnsOf(into) {
912
- const columns = [];
913
- for (const key of Object.keys(into)) {
914
- const out = into[key];
915
- if (out === undefined)
916
- continue;
917
- const slot = this.slotOf.get(key);
918
- if (slot === undefined)
919
- throw new Error(`blits: pull was handed ${key}, which the kit lacks`);
920
- const channel = this.channels[slot];
921
- const rest = channel.rest;
922
- const axes = Array.isArray(rest) ? rest.length : 1;
923
- const bounds = channel.bounds;
924
- const at = new Float64Array(axes).fill(Number.NaN);
925
- if (typeof rest === 'number')
926
- at[0] = rest;
927
- else if (Array.isArray(rest))
928
- at.set(rest);
929
- if (bounds !== undefined)
930
- clampRun(at, 0, axes, bounds);
931
- columns.push({ key, slot, axes, out, rest: at, bounds });
932
- }
933
- return columns;
238
+ pull(this, subjects, into);
934
239
  }
935
240
  project(timestamp) {
936
- const t = timestamp - this.offset;
937
241
  const c = new Mixer(this.kit, { ...this.opts, history: undefined, lanes: false });
938
- c.projecting = true;
939
- c.pose = this.pose;
940
- c.offset = this.offset;
941
- c.wantsPose = this.wantsPose;
942
- c.nextId = this.nextId;
943
- c.reducedNow = this.reducedNow;
944
- if (Number.isNaN(this.now) || t >= this.now) {
945
- c.now = this.now;
946
- c.voices = this.voices
947
- .filter((v) => v.state !== 'done')
948
- .map((v) => v.copy((subject) => this.carry(v, subject)));
949
- c.announced = this.announced.map((a) => ({ ...a }));
950
- c.count();
951
- c.move(t);
952
- }
953
- else {
954
- const history = this.opts.history;
955
- if (!history)
956
- throw new Error('blits: reading back needs a mix made with history');
957
- if (t < this.now - history.ms)
958
- throw new Error(`blits: ${timestamp} is older than this mix's history reaches`);
959
- c.now = t;
960
- c.backward = true;
961
- const was = last(this.hostLog, t, true);
962
- const host = this.opts.host;
963
- const then = was && typeof host === 'object' && host !== null
964
- ? Object.assign(Object.create(host), was.fields)
965
- : undefined;
966
- if (was)
967
- c.hostThen = was;
968
- c.announced = this.announced.filter((a) => a.made < t).map((a) => ({ ...a }));
969
- c.voices = [...this.voices, ...this.gone]
970
- .filter((v) => v.cuedAt <= t && v.doneAt > t)
971
- .sort((a, b) => a.id - b.id)
972
- .map((v) => {
973
- const log = v.log;
974
- const copy = v.copy((subject) => this.recall(v, subject, t), last(log, t, (e) => e.sync) ?? log[0]);
975
- copy.state =
976
- t < v.start
977
- ? 'pending'
978
- : copy.out && copy.out.at <= t
979
- ? 'fading'
980
- : copy.holdsAfter && copy.elapsedAt(t) >= copy.span + copy.latest
981
- ? 'held'
982
- : 'live';
983
- if (then !== undefined)
984
- copy.setting.host = then;
985
- return copy;
986
- });
987
- c.count();
988
- }
989
- const read = (f) => {
990
- reading.live = false;
991
- try {
992
- return f();
993
- }
994
- finally {
995
- reading.live = true;
996
- }
997
- };
998
- return {
999
- timestamp,
1000
- probe: (subject, out) => read(() => c.fold(subject, out)),
1001
- assess: (subject) => read(() => {
1002
- c.fold(subject);
1003
- return c.doubts(subject);
1004
- }),
1005
- };
242
+ return project(this, c, timestamp);
1006
243
  }
1007
244
  announce(name, opts = {}) {
1008
- const at = opts.at !== undefined ? opts.at - this.offset : this.now;
245
+ const at = opts.at !== undefined ? opts.at - this.offset : this.u;
1009
246
  this.announced.push({
1010
247
  name,
1011
248
  score: opts.score,
@@ -1016,41 +253,28 @@ class Mixer {
1016
253
  });
1017
254
  }
1018
255
  marks(from, to) {
1019
- const lo = from - this.offset;
1020
- const hi = to - this.offset;
1021
- const out = [];
1022
- for (const a of this.announced)
1023
- if (a.at >= lo && a.at <= hi)
1024
- out.push({
1025
- timestamp: a.at + this.offset,
1026
- mark: undefined,
1027
- voice: undefined,
1028
- score: a.score,
1029
- name: a.name,
1030
- tags: a.tags,
1031
- order: a.order,
1032
- });
1033
- for (const voice of [...this.voices, ...this.gone]) {
1034
- for (const mark of ['start', 'in', 'out', 'end']) {
1035
- const t = this.markOf(voice, mark);
1036
- if (t === undefined || t < lo || t > hi)
1037
- continue;
1038
- out.push({
1039
- timestamp: t + this.offset,
1040
- mark,
1041
- voice: voice.id,
1042
- score: voice.spec.score,
1043
- name: voice.spec.name,
1044
- tags: voice.spec.tags ?? none,
1045
- order: voice.id,
1046
- });
1047
- }
1048
- }
1049
- out.sort((a, b) => a.timestamp - b.timestamp || a.order - b.order);
1050
- return out.map(({ order: _, ...m }) => m);
256
+ return listed(this, from, to).map(({ order: _, ...m }) => m);
1051
257
  }
1052
258
  atRest(subject) {
1053
- const pose = this.fold(subject, undefined, true);
259
+ this.restStamps ??= new Store();
260
+ const head = this.linked(subject);
261
+ const laned = this.linkedLaned;
262
+ const lanes = this.lanes;
263
+ // Every voice on lanes and nothing to clamp: the pose a fold would make is the lanes' values.
264
+ if (laned && this.restsLaned()) {
265
+ const slot = head.slot;
266
+ if (!lanes.owes(slot))
267
+ return lanes.rests(slot);
268
+ }
269
+ // A probe this frame answers for the pose it gave the host.
270
+ const since = (this.restStamps.get(subject) ?? Number.NaN) - 2 * this.restsKey();
271
+ if (since >= 0)
272
+ return since === 1;
273
+ this.restScratch ??= {};
274
+ const pose = this.foldWith(subject, this.restScratch, head, laned, true);
275
+ return this.rests((this.bounded.length === 0 ? pose : this.clamp(pose)));
276
+ }
277
+ rests(pose) {
1054
278
  for (let i = 0; i < this.names.length; i++) {
1055
279
  const channel = this.channels[i];
1056
280
  const value = pose[this.names[i]];
@@ -1066,11 +290,48 @@ class Mixer {
1066
290
  rebase() {
1067
291
  this.rebasing = true;
1068
292
  }
293
+ voices(tag) {
294
+ const out = [];
295
+ for (const voice of this.cued)
296
+ if (voice.state !== 'done' && (tag === undefined || voice.spec.tags?.includes(tag)))
297
+ out.push(voice.handle);
298
+ return out;
299
+ }
1069
300
  get live() {
1070
- return this.voices.some((v) => v.state !== 'done');
301
+ return this.cued.some((v) => v.state !== 'done');
1071
302
  }
1072
303
  get inert() {
1073
- return !this.stirred && this.voices.every((v) => this.still(v, this.now));
304
+ if (this.stirred)
305
+ return false;
306
+ // Standing still, a voice changes no pose but by its weight signal, or by an anchor or start
307
+ // that host time may yet reach.
308
+ if (this.pace?.stopped(this.u))
309
+ return this.cued.every((v) => v.state === 'done' ||
310
+ (v.state !== 'pending' &&
311
+ typeof v.spec.weight !== 'function' &&
312
+ v.spec.anchor === undefined));
313
+ return this.cued.every((v) => this.still(v, this.now));
314
+ }
315
+ get rate() {
316
+ return this.pace === null ? 1 : this.pace.rateAt(this.u);
317
+ }
318
+ set rate(r) {
319
+ this.ramp(r, 0);
320
+ }
321
+ ramp(rate, over) {
322
+ if (!(rate >= 0 && rate < Number.POSITIVE_INFINITY))
323
+ throw new RangeError(`blits: a mix's rate is a finite number, 0 or more, not ${rate}`);
324
+ if (this.pace === null) {
325
+ this.pace = new Pace(this.opts.history !== undefined);
326
+ // Until now mix time was host time, so a pending voice's start is the host time it was given.
327
+ for (const v of this.cued)
328
+ if (v.state === 'pending' && !v.placing && v.owner === null && v.spec.start !== undefined)
329
+ pin(this, v, v.start);
330
+ }
331
+ const history = this.opts.history;
332
+ const reach = history === undefined ? Number.NEGATIVE_INFINITY : this.now - history.ms;
333
+ this.pace.change(this.u, rate, over, reach);
334
+ this.stir();
1074
335
  }
1075
336
  /** Whether a voice will change no pose from `now` on, short of a change made to it. */
1076
337
  still(voice, now) {
@@ -1083,16 +344,17 @@ class Mixer {
1083
344
  const fadeIn = this.reducedNow ? 0 : (voice.fade.in ?? 0);
1084
345
  if (fadeIn > 0 && now - this.sinceOf(voice, voice.latest) < fadeIn)
1085
346
  return false;
1086
- if (voice.state === 'held')
347
+ if (voice.state === 'held' || voice.holding !== null)
1087
348
  return true;
1088
349
  const motion = voice.motion;
1089
350
  return motion !== undefined && voice.elapsedAt(now) >= voice.latest && motion.landed();
1090
351
  }
1091
352
  mute(opts) {
1092
- for (const voice of this.voices)
1093
- this.beginFade(voice, { over: opts?.over });
353
+ for (const voice of this.cued)
354
+ beginFade(this, voice, { over: opts?.over });
1094
355
  }
1095
356
  drop(subject) {
357
+ this.frame = nextFrame();
1096
358
  const head = this.chains.get(subject);
1097
359
  // Nothing that kept this head, such as `pull`'s remembered list, may take it as current again.
1098
360
  if (head !== undefined) {
@@ -1102,8 +364,9 @@ class Mixer {
1102
364
  if (head !== undefined && head.slot >= 0 && this.lanes !== null)
1103
365
  this.lanes.release(head.slot);
1104
366
  this.pose.delete(subject);
367
+ this.restStamps?.delete(subject);
1105
368
  this.chains.delete(subject);
1106
- this.stirred = true;
369
+ this.stir();
1107
370
  // A record of it is only in a voice over every subject, one naming it, or one gone; a ramp out
1108
371
  // of a voice that does not name it is in `parters`, and a patch's own state in `motions`.
1109
372
  const forget = (voice) => {
@@ -1111,6 +374,7 @@ class Mixer {
1111
374
  voice.motion?.release(subject);
1112
375
  voice.parts?.delete(subject);
1113
376
  voice.parted?.delete(subject);
377
+ voice.blend?.of.reads.delete(subject);
1114
378
  };
1115
379
  for (const voice of this.general)
1116
380
  forget(voice);
@@ -1120,6 +384,9 @@ class Mixer {
1120
384
  forget(voice);
1121
385
  for (const voice of this.gone)
1122
386
  forget(voice);
387
+ if (this.owners !== null)
388
+ for (const voice of this.owners)
389
+ forget(voice);
1123
390
  for (const voice of this.parters) {
1124
391
  forget(voice);
1125
392
  if (voice.parts?.size === 0)
@@ -1137,32 +404,6 @@ class Mixer {
1137
404
  for (const motion of this.strayAll)
1138
405
  motion.release(subject);
1139
406
  }
1140
- /** Notes a motion patch that may hold state for a subject no voice playing it reaches. */
1141
- stray(motion, subject) {
1142
- const list = this.strays.get(subject);
1143
- if (list === undefined)
1144
- this.strays.set(subject, [motion]);
1145
- else if (!list.includes(motion))
1146
- list.push(motion);
1147
- }
1148
- /** A motion voice left the list: if its patch plays on, its subjects' state there may stray. */
1149
- unplay(voice) {
1150
- const motion = voice.motion;
1151
- if (motion === undefined)
1152
- return;
1153
- const left = (this.playing.get(motion) ?? 1) - 1;
1154
- if (left <= 0) {
1155
- this.playing.delete(motion);
1156
- this.strayAll.delete(motion);
1157
- return;
1158
- }
1159
- this.playing.set(motion, left);
1160
- if (voice.named === null)
1161
- this.strayAll.add(motion);
1162
- else
1163
- for (const subject of voice.named)
1164
- this.stray(motion, subject);
1165
- }
1166
407
  drain(tag) {
1167
408
  const all = this.sent;
1168
409
  if (all.length === 0)
@@ -1179,1302 +420,83 @@ class Mixer {
1179
420
  }
1180
421
  return out.sort((a, b) => a.timestamp - b.timestamp);
1181
422
  }
1182
- laneHost() {
1183
- const mix = this;
1184
- return {
1185
- get voices() {
1186
- return mix.voices;
1187
- },
1188
- channels: this.channels,
1189
- names: this.names,
1190
- fits: (voice) => mix.fits(voice),
1191
- meet: (voice, subject) => mix.held(voice, subject, mix.now),
1192
- naming: (subject) => (mix.naming === 0 ? undefined : mix.named.get(subject)),
1193
- slotOf: (subject) => mix.chains.get(subject)?.slot ?? -1,
1194
- envelope: (voice, since) => mix.envelope(voice, mix.now, since),
1195
- parting: (voice, subject) => mix.parting(voice, subject, mix.now),
1196
- ready: (voice, subject, held, elapsed, pass, weight) => {
1197
- mix.prime(voice, held, mix.now, elapsed, pass);
1198
- voice.setting.weight = weight;
1199
- mix.sending.voice = voice;
1200
- mix.sending.subject = subject;
1201
- },
1202
- horizon: (voice, delay) => {
1203
- reading.horizon = mix.horizonFor(voice, delay, mix.now);
1204
- },
1205
- keeps: this.opts.history !== undefined,
1206
- after: (voice, held) => mix.remember(voice, held),
1207
- kept: (voice) => mix.stateful(voice),
1208
- };
1209
- }
1210
- /**
1211
- * Whether a voice's patch and spec can run on a lane, its channels aside. A voice whose patch has
1212
- * kept state on a record through `setting.keep` is stateful from then on, and leaves its lane;
1213
- * one that starts keeping partway through may advance that state once for one subject not
1214
- * probed on the frame it starts, which declaring `state` avoids.
1215
- */
1216
- fits(voice) {
1217
- const spec = voice.spec;
1218
- const patch = voice.patch;
1219
- if (voice.keeping)
1220
- return false;
1221
- if (voice.state === 'pending' && voice.holdsBefore)
1222
- return false;
1223
- if (typeof spec.weight === 'function')
1224
- return false;
1225
- if (spec.locus !== undefined || spec.from === 'current')
1226
- return false;
1227
- if (voice.out?.rest)
1228
- return false;
1229
- if (patch.state !== undefined || patch.step !== undefined)
1230
- return false;
1231
- if (this.opts.history?.inputs && patch.reads !== undefined && patch.reads.length > 0)
1232
- return false;
1233
- const built = voice.built;
1234
- return (built === null ||
1235
- built.tracks.every((t, i) => t.lerp === undefined || t.lerp === voice.lerps[i]));
1236
- }
1237
- /** A voice's patch kept state on a record: it is stateful from now on. */
1238
- stateful(voice) {
1239
- if (voice.keeping)
1240
- return;
1241
- voice.keeping = true;
1242
- this.lanes?.invalidate();
1243
- }
1244
- /**
1245
- * What a motion patch asks for its subjects' voice time at the latest frame. It holds the mix and
1246
- * the voice weakly, so a patch the host keeps does not keep a mix it has let go of alive.
1247
- */
1248
- static reviveHook(mix, voice) {
1249
- return (subject) => {
1250
- const m = mix.deref();
1251
- if (m === undefined)
1252
- return;
1253
- m.stirred = true;
1254
- const v = voice.deref();
1255
- if (v === undefined)
1256
- return;
1257
- if (v.motion !== undefined && v.named !== null && !v.named.has(subject))
1258
- m.stray(v.motion, subject);
1259
- m.unpart(v, subject);
1260
- };
1261
- }
1262
- static frameHook(mix, voice) {
1263
- return (subject) => {
1264
- const m = mix.deref();
1265
- const v = voice.deref();
1266
- return m === undefined || v === undefined ? Number.NaN : m.frameOf(v, subject);
1267
- };
1268
- }
1269
- /**
1270
- * A subject's voice time at the latest frame, where a motion patch places an untimed change and a
1271
- * `read` with no time; NaN until the voice has started and met the subject, and while the
1272
- * subject's own time is still short of its stagger. A retired voice's patch no longer asks.
1273
- */
1274
- frameOf(voice, subject) {
1275
- if (Number.isNaN(this.now) || voice.state === 'pending')
1276
- return Number.NaN;
1277
- const held = voice.subjects.get(subject);
1278
- if (held === undefined || !held.reaches)
1279
- return Number.NaN;
1280
- const t = voice.elapsedAt(this.now) - held.delay;
1281
- return t < 0 ? Number.NaN : t;
1282
- }
1283
- /** Fills a voice's setting for a call to its patch, the weight aside. */
1284
- prime(voice, held, now, elapsed, pass) {
1285
- const setting = voice.setting;
1286
- setting.timestamp = now;
1287
- setting.dt = this.capped(now - held.stepped);
1288
- setting.elapsed = elapsed;
1289
- setting.pass = pass;
1290
- setting.weight = 0;
1291
- setting.state = held.state;
1292
- setting.keep = held.keep;
1293
- }
1294
- /** A gap as a `dt`: Infinity under reduced motion, and no more than `maxDt`. */
1295
- capped(gap) {
1296
- const cap = this.opts.maxDt;
1297
- return this.reducedNow ? Number.POSITIVE_INFINITY : cap !== undefined && gap > cap ? cap : gap;
1298
- }
1299
- /** The earliest voice time a read may still ask for, for a subject delayed `delay`. */
1300
- horizonFor(voice, delay, now) {
1301
- const history = this.opts.history;
1302
- return history === undefined
1303
- ? Number.POSITIVE_INFINITY
1304
- : this.elapsedThen(voice, now - history.ms) - delay;
1305
- }
1306
- // ── internals ──────────────────────────────────────────────────────────────
1307
- /** Refuses a placement that names a mark twice on one side, or that waits on itself. */
1308
- checkPlacement(spec, anchor) {
1309
- if (anchor.start !== undefined && anchor.in !== undefined)
1310
- throw new Error('blits: a placement anchors start or in, not both');
1311
- if (anchor.out !== undefined && anchor.end !== undefined)
1312
- throw new Error('blits: a placement anchors out or end, not both');
1313
- if (spec.start !== undefined && (anchor.start !== undefined || anchor.in !== undefined))
1314
- throw new Error('blits: a voice takes start or an anchored start, not both');
1315
- const name = spec.name;
1316
- if (name === undefined)
1317
- return;
1318
- // A voice is known by its score and its name; a bare name in an anchor is in the asker's score.
1319
- const key = (score, n) => `${score ?? ''}\u0000${n}`;
1320
- const names = (p, score) => [p.start, p.in, p.out, p.end].flatMap((a) => {
1321
- if (a === undefined || typeof a === 'number')
1322
- return [];
1323
- const q = 'of' in a ? a.of : 'after' in a ? a.after : 'with' in a ? a.with : a.before;
1324
- if (typeof q === 'string')
1325
- return [key(score, q)];
1326
- return q.name === undefined ? [] : [key(q.score ?? score, q.name)];
1327
- });
1328
- const self = key(spec.score, name);
1329
- const seen = new Set();
1330
- const waits = names(anchor, spec.score);
1331
- while (waits.length > 0) {
1332
- const n = waits.pop();
1333
- if (n === self)
1334
- throw new Error(`blits: ${name}'s placement waits on itself`);
1335
- if (seen.has(n))
1336
- continue;
1337
- seen.add(n);
1338
- for (const v of this.voices)
1339
- if (v.spec.name !== undefined && key(v.spec.score, v.spec.name) === n && v.spec.anchor)
1340
- waits.push(...names(v.spec.anchor, v.spec.score));
1341
- }
1342
- }
1343
- /**
1344
- * Fixes every anchored voice's start and out from what its anchors answer now. A start is fixed
1345
- * while the voice waits and an out until its fade begins; a target that has left keeps the time it
1346
- * last gave. Repeated so a chain of anchors settles in one sync.
1347
- */
1348
- place() {
1349
- for (let round = 0; round <= this.voices.length; round++) {
1350
- let moved = false;
1351
- for (const voice of this.voices) {
1352
- const anchor = voice.spec.anchor;
1353
- if (anchor === undefined || voice.state === 'done')
1354
- continue;
1355
- if (voice.placing && voice.state === 'pending') {
1356
- const by = anchor.start ?? anchor.in;
1357
- const t = by === undefined ? undefined : this.resolve(by, voice);
1358
- if (t !== undefined) {
1359
- const start = anchor.start !== undefined ? t : t - (voice.fade.in ?? 0);
1360
- if (start !== voice.start) {
1361
- voice.start = start;
1362
- voice.anchorNow = start;
1363
- voice.anchorElapsed = 0;
1364
- this.noted(voice);
1365
- moved = true;
1366
- }
1367
- }
1368
- }
1369
- if (voice.state !== 'fading') {
1370
- const by = anchor.out ?? anchor.end;
1371
- const t = by === undefined ? undefined : this.resolve(by, voice);
1372
- if (t !== undefined) {
1373
- const at = anchor.out !== undefined ? t : t - (voice.fade.out ?? 0);
1374
- if (at !== voice.outAt) {
1375
- voice.outAt = at;
1376
- this.noted(voice);
1377
- moved = true;
1378
- }
1379
- }
1380
- }
1381
- }
1382
- if (!moved)
1383
- return;
1384
- }
1385
- }
1386
- /** The mix time an anchor answers, or undefined while its target has none. */
1387
- resolve(a, self) {
1388
- if (typeof a === 'number')
1389
- return a - this.offset;
1390
- let query;
1391
- let mark;
1392
- let by = a.by ?? 0;
1393
- if ('of' in a) {
1394
- query = a.of;
1395
- mark = a.mark;
1396
- }
1397
- else if ('after' in a) {
1398
- query = a.after;
1399
- mark = 'end';
1400
- }
1401
- else if ('with' in a) {
1402
- query = a.with;
1403
- mark = 'start';
1404
- }
1405
- else {
1406
- query = a.before;
1407
- mark = 'start';
1408
- by = -by;
1409
- }
1410
- const t = this.timeOf(typeof query === 'string' ? { name: query } : query, mark, self);
1411
- return t === undefined ? undefined : t + by;
1412
- }
1413
- /**
1414
- * The time a query answers: `mark` of the voice it picks, among those cued and those that have
1415
- * left, never the asker; or a mark the host announced on the score, which is the same time for
1416
- * any of the four. Undefined while what it picks has no such time.
1417
- */
1418
- timeOf(q, mark, self) {
1419
- const score = q.score ?? self.spec.score;
1420
- const found = [];
1421
- for (const v of [...this.gone, ...this.voices])
1422
- if (v !== self &&
1423
- v.spec.score === score &&
1424
- (q.name === undefined || v.spec.name === q.name) &&
1425
- (q.tag === undefined || (v.spec.tags ?? none).includes(q.tag)) &&
1426
- (q.writes === undefined || v.patch.writes.includes(q.writes)))
1427
- found.push({ order: v.id, t: this.markOf(v, mark) });
1428
- if (q.writes === undefined)
1429
- for (const a of this.announced)
1430
- if (a.score === score &&
1431
- (q.name === undefined || a.name === q.name) &&
1432
- (q.tag === undefined || a.tags.includes(q.tag)))
1433
- found.push({ order: a.order, t: Number.isNaN(a.at) ? undefined : a.at });
1434
- if (found.length === 0)
1435
- return undefined;
1436
- found.sort((x, y) => x.order - y.order);
1437
- const resolver = q.resolver ?? 'last';
1438
- if (resolver === 'first')
1439
- return found[0]?.t;
1440
- if (resolver === 'last')
1441
- return found[found.length - 1]?.t;
1442
- const times = found.flatMap((e) => (e.t === undefined ? [] : [e.t])).sort((x, y) => x - y);
1443
- if (resolver === 'next') {
1444
- const now = Number.isNaN(this.now) ? Number.NEGATIVE_INFINITY : this.now;
1445
- return times.find((t) => t >= now);
1446
- }
1447
- return resolver === 'earliest' ? times[0] : times[times.length - 1];
1448
- }
1449
- /** When a voice reaches a mark, mix time, or undefined while nothing has fixed it. */
1450
- markOf(voice, mark) {
1451
- const start = voice.start;
1452
- if (!Number.isFinite(start))
1453
- return undefined;
1454
- if (mark === 'start')
1455
- return start;
1456
- if (mark === 'in')
1457
- return start + (voice.fade.in ?? 0);
1458
- const out = voice.out;
1459
- let outAt;
1460
- if (out !== null)
1461
- outAt = out.at;
1462
- else if (Number.isFinite(voice.outAt))
1463
- outAt = voice.outAt;
1464
- else if (!voice.holdsAfter && Number.isFinite(voice.span)) {
1465
- const t = voice.timeAt(voice.span + voice.latest);
1466
- if (Number.isFinite(t))
1467
- outAt = Math.max(start, t);
1468
- }
1469
- if (mark === 'out')
1470
- return voice.state === 'done' && outAt === undefined ? voice.doneAt : outAt;
1471
- if (voice.state === 'done')
1472
- return voice.doneAt;
1473
- if (out !== null) {
1474
- if (out.rest)
1475
- return out.deadline === undefined ? undefined : out.at + out.deadline;
1476
- return out.at + out.over;
1477
- }
1478
- return outAt === undefined ? undefined : outAt + (this.reduced ? 0 : (voice.fade.out ?? 0));
1479
- }
1480
- /** A voice's elapsed at an earlier mix time, by the controls it had then. */
1481
- elapsedThen(voice, t) {
1482
- const log = voice.log;
1483
- const c = log === null ? undefined : last(log, t, true);
1484
- return elapsedWith(c ?? voice, t);
1485
- }
1486
- /** Records a change to a voice's controls under `history`, and lets go of what it no longer reaches. */
1487
- noted(voice) {
1488
- this.stirred = true;
1489
- this.schedule(voice);
1490
- const log = voice.log;
1491
- if (log === null)
1492
- return;
1493
- voice.note(Number.isNaN(this.now) ? Number.NEGATIVE_INFINITY : this.now, this.moving);
1494
- const reach = this.now - this.opts.history.ms;
1495
- let drop = 0;
1496
- while (drop + 1 < log.length && log[drop + 1].at <= reach)
1497
- drop++;
1498
- if (drop > 0)
1499
- log.splice(0, drop);
1500
- }
1501
- /** Recounts what the fold's shortcuts depend on, for a projection's freshly copied voices. */
1502
- count() {
1503
- this.named = new Store();
1504
- this.naming = 0;
1505
- this.general = [];
1506
- for (const voice of this.voices)
1507
- this.index(voice);
1508
- this.loci = this.voices.filter((v) => v.spec.locus !== undefined).length;
1509
- this.anchored = this.voices.filter((v) => v.spec.anchor !== undefined).length;
1510
- this.version++;
1511
- }
1512
- /** A copy of a subject's record that shares nothing a read could change. */
1513
- copyHeld(voice, h) {
1514
- const kept = new Map();
1515
- for (const [owner, value] of h.kept)
1516
- kept.set(owner, structuredClone(value));
1517
- const patch = voice.patch;
1518
- return {
1519
- ...h,
1520
- bands: h.bands.slice(),
1521
- state: h.state === undefined
1522
- ? undefined
1523
- : patch.clone
1524
- ? patch.clone(h.state)
1525
- : structuredClone(h.state),
1526
- kept,
1527
- keep: keeper(kept),
1528
- probed: Number.NaN,
1529
- delta: null,
1530
- phase: 0,
1531
- seeks: 0,
1532
- base: h.base === undefined ? undefined : structuredClone(h.base),
1533
- slope: h.slope === undefined ? undefined : structuredClone(h.slope),
1534
- snaps: undefined,
1535
- inputs: undefined,
1536
- next: null,
1537
- version: Number.NaN,
1538
- loci: null,
1539
- slot: -1,
1540
- };
1541
- }
1542
- /** A projection ahead starts each subject from where the live mix holds it. */
1543
- carry(voice, subject) {
1544
- const live = voice.subjects.get(subject);
1545
- if (live === undefined)
1546
- return undefined;
1547
- const h = this.copyHeld(voice, live);
1548
- h.from = h.stepped;
1549
- return h;
1550
- }
1551
- /**
1552
- * A projection back starts each subject from the latest copy kept at or before `t`, else from its
1553
- * voice's start with fresh state, which is exact for a voice stepped at a fixed interval.
1554
- */
1555
- recall(voice, subject, t) {
1556
- const live = voice.subjects.get(subject);
1557
- if (live === undefined)
1558
- return undefined;
1559
- const snap = live.snaps && last(live.snaps, t, true);
1560
- if (snap) {
1561
- const h = this.copyHeld(voice, snap.held);
1562
- h.from = h.stepped;
1563
- h.replay = live.inputs;
1564
- return h;
1565
- }
1566
- const kept = new Map();
1567
- const stepped = live.since < t ? live.since : t;
1568
- return {
1569
- reaches: live.reaches,
1570
- delay: live.delay,
1571
- since: live.since,
1572
- shown: live.shown,
1573
- weight: 0,
1574
- rested: false,
1575
- bands: new Uint8Array(voice.slots.length),
1576
- state: live.reaches && voice.patch.state
1577
- ? voice.patch.state(subject)
1578
- : undefined,
1579
- stepped,
1580
- ticks: 0,
1581
- probed: Number.NaN,
1582
- delta: null,
1583
- phase: 0,
1584
- seeks: 0,
1585
- kept,
1586
- keep: keeper(kept),
1587
- voice,
1588
- next: null,
1589
- version: Number.NaN,
1590
- loci: null,
1591
- slot: -1,
1592
- from: stepped,
1593
- unknown: voice.spec.from === 'current',
1594
- replay: live.inputs,
1595
- };
1596
- }
1597
- /** Under `history` with `inputs`, copies the host fields any voice reads, once a frame, when they changed. */
1598
- recordHost(now) {
1599
- const log = this.hostLog;
1600
- const prev = log[log.length - 1];
1601
- if (prev !== undefined && prev.at === now)
1602
- return;
1603
- const host = this.opts.host;
1604
- const fields = {};
1605
- for (const v of this.voices)
1606
- for (const f of v.patch.reads ?? none)
1607
- if (!(f in fields))
1608
- fields[f] = structuredClone(host[f]);
1609
- if (prev !== undefined && same(prev.fields, fields))
1610
- return;
1611
- log.push({ at: now, fields });
1612
- const reach = now - this.opts.history.ms;
1613
- while (log.length > 1 && log[1].at <= reach)
1614
- log.shift();
1615
- }
1616
- /** Under `history` with `inputs`, keeps what an input signal read, each time it changes. */
1617
- record(held, value) {
1618
- const history = this.opts.history;
1619
- if (!history?.inputs || this.projecting)
1620
- return;
1621
- const inputs = held.inputs ?? [];
1622
- held.inputs = inputs;
1623
- const prev = inputs[inputs.length - 1];
1624
- if (prev !== undefined && (prev.value === value || prev.at === this.now)) {
1625
- if (prev.at === this.now)
1626
- prev.value = value;
1627
- return;
1628
- }
1629
- inputs.push({ at: this.now, value });
1630
- const reach = this.now - history.ms;
1631
- while (inputs.length > 1 && inputs[1].at <= reach)
1632
- inputs.shift();
1633
- }
1634
- /** Under `history`, keeps a copy of a stateful voice's record for this subject every so often. */
1635
- remember(voice, held) {
1636
- const history = this.opts.history;
1637
- if (!history || this.projecting)
1638
- return;
1639
- if (voice.patch.step === undefined && held.kept.size === 0 && held.base === undefined)
1640
- return;
1641
- const snaps = held.snaps ?? [];
1642
- held.snaps = snaps;
1643
- const prev = snaps[snaps.length - 1];
1644
- if (prev !== undefined && this.now - prev.at < (history.every ?? 200))
1645
- return;
1646
- snaps.push({ at: this.now, held: this.copyHeld(voice, held) });
1647
- const reach = this.now - history.ms;
1648
- while (snaps.length > 1 && snaps[1].at <= reach)
1649
- snaps.shift();
1650
- }
1651
- /** Per channel, the least sure voice that fed it this frame. */
1652
- doubts(subject) {
1653
- const out = {};
1654
- for (const name of this.names)
1655
- out[name] = 'exact';
1656
- const rank = { exact: 0, stepped: 1, held: 2 };
1657
- // A voice still waiting on an anchor nothing has answered may yet play, or stop, by now.
1658
- for (const voice of this.voices) {
1659
- const anchor = voice.spec.anchor;
1660
- if (anchor === undefined || voice.state === 'done')
1661
- continue;
1662
- const waiting = (voice.state === 'pending' && !Number.isFinite(voice.start)) ||
1663
- (voice.state !== 'pending' &&
1664
- voice.state !== 'fading' &&
1665
- (anchor.out !== undefined || anchor.end !== undefined) &&
1666
- !Number.isFinite(voice.outAt));
1667
- if (!waiting || !this.aims(voice, subject))
1668
- continue;
1669
- for (const slot of voice.slots)
1670
- out[this.names[slot]] = 'held';
1671
- }
1672
- for (let held = this.chain(subject, this.now); held !== null; held = held.next) {
1673
- const voice = held.voice;
1674
- if (voice === null || voice.state === 'done')
1675
- continue;
1676
- if (held.weight <= 0)
1677
- continue;
1678
- const d = this.doubtOf(voice, held);
1679
- for (const slot of voice.slots) {
1680
- const name = this.names[slot];
1681
- if (rank[d] > rank[out[name]])
1682
- out[name] = d;
1683
- }
1684
- }
1685
- return out;
1686
- }
1687
- doubtOf(voice, held) {
1688
- const weight = voice.spec.weight;
1689
- if (held.unknown)
1690
- return 'held';
1691
- // A weight read back from a recording is what the mix used then, so its signal's state is moot.
1692
- const replayed = held.replay !== undefined && last(held.replay, this.now, true) !== undefined;
1693
- if (typeof weight === 'function' && weight.input && !replayed)
1694
- return 'held';
1695
- const reads = voice.patch.reads;
1696
- if (reads !== undefined && reads.length > 0) {
1697
- const then = this.hostThen?.fields;
1698
- if (then === undefined || !reads.every((f) => f in then))
1699
- return 'held';
1700
- }
1701
- if (this.now > (held.from ?? this.now)) {
1702
- const tick = this.opts.stepMs;
1703
- const fixed = tick !== undefined && tick > 0 && !this.reducedNow;
1704
- if ((voice.patch.step && !fixed) || (held.kept.size > 0 && !replayed))
1705
- return 'stepped';
1706
- }
1707
- return 'exact';
1708
- }
1709
- handle(voice) {
1710
- const mix = this;
1711
- return {
1712
- id: voice.id,
1713
- get state() {
1714
- return voice.state;
1715
- },
1716
- played: voice.played,
1717
- get weight() {
1718
- return voice.weight;
1719
- },
1720
- set weight(w) {
1721
- voice.weight = w;
1722
- mix.noted(voice);
1723
- mix.lanes?.refill();
1724
- },
1725
- get rate() {
1726
- return voice.rateAt(Number.isNaN(mix.now) ? voice.start : mix.now);
1727
- },
1728
- set rate(r) {
1729
- voice.rebase(Number.isNaN(mix.now) ? voice.start : mix.now);
1730
- voice.ramp = null;
1731
- voice.rate = r;
1732
- mix.noted(voice);
1733
- mix.lanes?.refill();
1734
- },
1735
- ramp(r, over) {
1736
- const now = Number.isNaN(mix.now) ? voice.start : mix.now;
1737
- voice.rebase(now);
1738
- const from = voice.rateAt(now);
1739
- voice.ramp = over > 0 && r !== from ? { from, to: r, over } : null;
1740
- voice.rate = r;
1741
- mix.noted(voice);
1742
- mix.lanes?.refill();
1743
- },
1744
- seek(elapsed) {
1745
- voice.rebase(Number.isNaN(mix.now) ? voice.start : mix.now);
1746
- voice.anchorElapsed = elapsed;
1747
- voice.seeks++;
1748
- mix.noted(voice);
1749
- mix.lanes?.refill();
1750
- },
1751
- fade(opts) {
1752
- if (opts !== undefined && 'subject' in opts)
1753
- mix.fadeSubject(voice, opts.subject, opts.over);
1754
- else
1755
- mix.beginFade(voice, opts ?? {});
1756
- },
1757
- weightOf(subject) {
1758
- if (voice.state === 'done')
1759
- return 0;
1760
- if (voice.laned && mix.lanes !== null) {
1761
- const w = mix.lanes.weightOf(voice.id, mix.chains.get(subject)?.slot ?? -1);
1762
- if (w !== undefined)
1763
- return w;
1764
- }
1765
- return voice.subjects.get(subject)?.weight ?? 0;
1766
- },
1767
- done: voice.done,
1768
- };
1769
- }
1770
- /** Starts one subject's ramp out of a voice, or takes it out at once for a ramp of 0. */
1771
- fadeSubject(voice, subject, over) {
1772
- if (voice.state === 'done' || voice.parted?.has(subject) || voice.parts?.has(subject))
1773
- return;
1774
- this.stirred = true;
1775
- const ms = this.reduced ? 0 : (over ?? voice.fade.out ?? 0);
1776
- const at = Number.isNaN(this.now) ? voice.start : this.now;
1777
- if (ms === 0) {
1778
- this.part(voice, subject, at);
1779
- return;
1780
- }
1781
- voice.parts ??= new Map();
1782
- voice.parts.set(subject, { at, over: ms });
1783
- this.parters.add(voice);
1784
- this.schedule(voice);
1785
- this.lanes?.refill();
1786
- }
1787
- /** What a subject's own ramp out of a voice leaves of its weight this frame, 0..1. */
1788
- parting(voice, subject, now) {
1789
- const r = voice.parts?.get(subject);
1790
- if (r === undefined)
1791
- return 1;
1792
- return envelope(0, { at: r.at, over: r.over, rest: false }, voice.ease, this.reducedNow, now, 0);
1793
- }
1794
- /**
1795
- * Takes a subject out of one voice for good, as of mix time `at`: the voice forgets its record,
1796
- * its lane position and a motion patch's state for it, and reaches it no more.
1797
- */
1798
- part(voice, subject, at) {
1799
- voice.parts?.delete(subject);
1800
- if (voice.parts?.size === 0)
1801
- voice.parts = null;
1802
- voice.parted ??= new Map();
1803
- voice.parted.set(subject, at);
1804
- this.parters.add(voice);
1805
- this.forgetIn(voice, subject);
1806
- const motion = voice.motion;
1807
- if (motion !== undefined && motion.frame === voice.frame)
1808
- motion.release(subject);
1809
- }
1810
- /** Brings a subject faded out of a voice back, to be met afresh on its next probe. */
1811
- unpart(voice, subject) {
1812
- if (voice.parted?.delete(subject) !== true)
1813
- return;
1814
- if (voice.parted.size === 0)
1815
- voice.parted = null;
1816
- this.forgetIn(voice, subject);
1817
- const slot = this.chains.get(subject)?.slot ?? -1;
1818
- if (slot >= 0)
1819
- this.lanes?.rejoin(slot);
1820
- }
1821
- /** Drops a voice's record of a subject and relinks the subject's chain without it. */
1822
- forgetIn(voice, subject) {
1823
- const held = voice.subjects.get(subject);
1824
- if (held !== undefined) {
1825
- voice.subjects.delete(subject);
1826
- if (held.reaches)
1827
- voice.seen--;
1828
- if (held.rested)
1829
- voice.restedCount--;
1830
- if (voice.holder === held)
1831
- voice.holder = null;
1832
- }
1833
- const head = this.chains.get(subject);
1834
- if (head === undefined)
1835
- return;
1836
- head.version = Number.NaN;
1837
- this.relinks++;
1838
- if (voice.laned && head.slot >= 0)
1839
- this.lanes?.part(voice.id, head.slot);
1840
- this.lanes?.refill();
1841
- }
1842
- beginFade(voice, opts, at) {
1843
- if (voice.state === 'done' || voice.state === 'fading')
1844
- return;
1845
- const over = this.reduced ? 0 : (opts.over ?? voice.fade.out ?? 0);
1846
- // A voice is linked into its subjects' chains once it plays, and a fading one plays.
1847
- if (voice.state === 'pending')
1848
- this.changed(voice);
1849
- voice.state = 'fading';
1850
- voice.out = {
1851
- from: 1,
1852
- at: at ?? (Number.isNaN(this.now) ? voice.start : this.now),
1853
- over,
1854
- rest: opts.at === 'rest',
1855
- deadline: opts.deadline,
1856
- };
1857
- this.noted(voice);
1858
- if (opts.at === 'rest')
1859
- this.lanes?.invalidate();
1860
- else
1861
- this.lanes?.refill();
1862
- if (over === 0 && opts.at !== 'rest')
1863
- this.retire(voice, voice.out.at);
1864
- }
1865
- /** Removes a voice, recording that it left at `at`, default now. */
1866
- retire(voice, at) {
1867
- this.lanes?.touch(voice);
1868
- const motion = voice.motion;
1869
- if (motion !== undefined && motion.frame === voice.frame)
1870
- motion.frame = noFrame;
1871
- if (motion !== undefined && motion.revive === voice.revive)
1872
- motion.revive = noRevive;
1873
- voice.frame = null;
1874
- voice.revive = null;
1875
- voice.state = 'done';
1876
- this.retired++;
1877
- voice.doneAt = at ?? (Number.isNaN(this.now) ? Number.NEGATIVE_INFINITY : this.now);
1878
- voice.play(false);
1879
- voice.resolve();
1880
- }
1881
- /** The ramp a voice's own fade envelope applies this frame, 0..1. */
1882
- envelope(voice, now, since) {
1883
- return envelope(voice.fade.in ?? 0, voice.out, voice.ease, this.reducedNow, now, since);
1884
- }
1885
- /** What this voice holds for this subject, made on first sight with `target` and `stagger` asked once. */
1886
- held(voice, subject, now) {
1887
- let held = voice.subjects.get(subject);
1888
- if (held !== undefined)
1889
- return held;
1890
- const reaches = this.aims(voice, subject);
1891
- const delay = reaches && voice.spec.stagger ? voice.spec.stagger(subject) : 0;
1892
- const since = this.sinceOf(voice, delay);
1893
- const kept = new Map();
1894
- held = {
1895
- reaches,
1896
- delay,
1897
- since,
1898
- shown: this.shownOf(voice, since),
1899
- weight: 0,
1900
- rested: false,
1901
- bands: new Uint8Array(voice.slots.length),
1902
- state: reaches && voice.patch.state
1903
- ? voice.patch.state(subject)
1904
- : undefined,
1905
- stepped: this.backward && since < now ? since : now,
1906
- ticks: 0,
1907
- probed: Number.NaN,
1908
- delta: null,
1909
- phase: 0,
1910
- seeks: 0,
1911
- kept,
1912
- keep: keeper(kept),
1913
- voice,
1914
- next: null,
1915
- version: Number.NaN,
1916
- loci: null,
1917
- slot: -1,
1918
- };
1919
- if (this.projecting) {
1920
- held.from = held.stepped;
1921
- held.unknown = this.backward && voice.spec.from === 'current';
1922
- }
1923
- voice.subjects.set(subject, held);
1924
- if (voice.state === 'pending') {
1925
- voice.early ??= [];
1926
- voice.early.push(held);
1927
- }
1928
- if (reaches)
1929
- voice.seen++;
1930
- if (delay > voice.latest) {
1931
- voice.latest = delay;
1932
- // A later end can take a held voice live again.
1933
- if (voice.state === 'held')
1934
- this.schedule(voice);
1935
- }
1936
- return held;
1937
- }
1938
- /**
1939
- * When the voice clock reads `delay`, from where it is anchored now; during a ramp this assumes
1940
- * the rate it is ramping to.
1941
- */
1942
- sinceOf(voice, delay) {
1943
- return voice.rate > 0
1944
- ? voice.anchorNow + (delay - voice.anchorElapsed) / voice.rate
1945
- : voice.start + delay;
1946
- }
1947
- shownOf(voice, since) {
1948
- return voice.holdsBefore && voice.opened < since ? voice.opened : since;
1949
- }
1950
- /** A voice has gone live: the records it made while pending count from where it started. */
1951
- started(voice) {
1952
- const early = voice.early;
1953
- if (early === null)
1954
- return;
1955
- voice.early = null;
1956
- for (const held of early) {
1957
- held.since = this.sinceOf(voice, held.delay);
1958
- held.shown = this.shownOf(voice, held.since);
1959
- }
1960
- }
1961
- /** The weight a voice gives a subject this frame, with the setting already filled in. */
1962
- weigh(voice, subject, now, held) {
1963
- this.sending.voice = voice;
1964
- this.sending.subject = subject;
1965
- const signal = typeof voice.spec.weight === 'function' ? voice.spec.weight : null;
1966
- let base = voice.weight;
1967
- if (signal) {
1968
- const was = held.replay && last(held.replay, now, true);
1969
- base = was ? was.value : signal(subject, voice.setting);
1970
- if (signal.input && !was)
1971
- this.record(held, base);
1972
- }
1973
- const fade = this.envelope(voice, now, held.shown);
1974
- return clampWeight(base * (voice.parts === null ? fade : fade * this.parting(voice, subject, now)));
1975
- }
1976
- /**
1977
- * One live voice's delta for one subject this frame, through its record for the subject, or null
1978
- * when it does not reach. Its weight is left in `w`.
1979
- */
1980
- influence(voice, subject, now, held) {
1981
- held.weight = 0;
1982
- if (!held.reaches)
1983
- return null;
1984
- if (voice.out?.rest && held.rested)
1985
- return null;
1986
- let elapsed = voice.elapsedAt(now) - held.delay;
1987
- // A held subject's clock stands still at the edge it holds: -1 before, 1 after, 0 playing.
1988
- let still = 0;
1989
- if (elapsed < 0) {
1990
- if (!voice.holdsBefore)
1991
- return null;
1992
- elapsed = 0;
1993
- still = -1;
1994
- }
1995
- else if (voice.holdsAfter && elapsed > voice.span) {
1996
- elapsed = voice.span;
1997
- still = 1;
1998
- }
1999
- const period = voice.patch.period;
2000
- const phase = phaseAt(elapsed, period, voice.passes);
2001
- const pass = passAt(elapsed, period, voice.passes);
2002
- this.prime(voice, held, now, elapsed, pass);
2003
- const setting = voice.setting;
2004
- const weight = this.weigh(voice, subject, now, held);
2005
- setting.weight = weight;
2006
- held.weight = weight;
2007
- if (held.probed === now && held.delta && held.seeks === voice.seeks) {
2008
- if (voice.holder !== held && voice.scratch.length > 0)
2009
- this.keyed(voice, subject, held);
2010
- this.w = weight;
2011
- return held.delta;
2012
- }
2013
- const keptBefore = reading.kept;
2014
- const history = this.opts.history;
2015
- if (history?.inputs && voice.patch.reads !== undefined && !this.projecting)
2016
- this.recordHost(now);
2017
- reading.horizon = this.horizonFor(voice, held.delay, now);
2018
- const tick = this.opts.stepMs;
2019
- if (voice.patch.step && held.probed !== now && still >= 0) {
2020
- // Held after, it steps once more to where its last pass ended, and no further.
2021
- const to = still === 1 ? voice.timeAt(held.delay + voice.span) : now;
2022
- if (tick !== undefined && tick > 0 && !this.reducedNow)
2023
- this.tick(voice, subject, held, tick, to);
2024
- else if (still === 0 ? now !== held.stepped : to > held.stepped) {
2025
- if (still === 1)
2026
- setting.dt = this.capped(to - held.stepped);
2027
- voice.patch.step(held.state, setting.dt, subject, setting);
2028
- held.stepped = to;
2029
- }
2030
- }
2031
- let delta;
2032
- if (voice.built) {
2033
- held.phase = phase;
2034
- delta = this.keyed(voice, subject, held);
2035
- }
2036
- else {
2037
- delta = voice.patch.at(phase, subject, setting);
2038
- }
2039
- held.delta = delta;
2040
- held.probed = now;
2041
- held.seeks = voice.seeks;
2042
- if (history !== undefined)
2043
- this.remember(voice, held);
2044
- if (reading.kept !== keptBefore && !voice.keeping)
2045
- this.stateful(voice);
2046
- if (voice.out?.rest && this.isRest(delta)) {
2047
- held.weight = 0;
2048
- held.rested = true;
2049
- voice.restedCount++;
2050
- return null;
2051
- }
2052
- this.w = weight;
2053
- return delta;
2054
- }
2055
- /** Reads a keys voice's stops at `held.phase` into the subject's delta, through the voice's scratch. */
2056
- keyed(voice, subject, held) {
2057
- let base;
2058
- if (voice.spec.from === 'current') {
2059
- if (held.base === undefined) {
2060
- held.base = this.baseFor(voice, subject);
2061
- held.slope = this.slopeFor(voice, subject);
2062
- }
2063
- base = held.base;
2064
- }
2065
- const delta = readKeys(voice.built, held.phase, held.delta ?? {}, base, voice.lerps, held.slope, voice.intos, voice.scratch);
2066
- held.delta = delta;
2067
- if (voice.scratch.length > 0)
2068
- voice.holder = held;
2069
- return delta;
2070
- }
2071
- /**
2072
- * Runs `step` once per whole interval between where this subject last stopped and now, on a grid
2073
- * counted from when its delay ran out, and leaves the remainder for the next sample. Counting
2074
- * rather than adding keeps the grid where it is however many samples it is reached through.
2075
- */
2076
- tick(voice, subject, held, tick, to) {
2077
- const now = this.now;
2078
- const setting = voice.setting;
2079
- const frameDt = setting.dt;
2080
- let n = held.ticks;
2081
- // The epsilon keeps an interval like 1000 / 120 from landing a hair short of a whole count.
2082
- const due = Math.floor((to - held.since) / tick + 1e-9);
2083
- const cap = this.opts.maxDt;
2084
- if (cap !== undefined) {
2085
- const most = Math.floor(cap / tick);
2086
- if (due - n > most)
2087
- n = due - most;
2088
- }
2089
- const step = voice.patch.step;
2090
- setting.dt = tick;
2091
- for (n++; n <= due; n++) {
2092
- setting.timestamp = held.since + n * tick;
2093
- step(held.state, tick, subject, setting);
2094
- }
2095
- held.ticks = due > held.ticks ? due : held.ticks;
2096
- held.stepped = held.since + held.ticks * tick;
2097
- setting.timestamp = now;
2098
- setting.dt = frameDt;
2099
- }
2100
- /**
2101
- * Stop 0 for a retargeting voice: the subject's pose in the frame before this voice contributes.
2102
- * With no such frame on record — a voice cued before the subject was ever probed, or a host that
2103
- * samples into its own object — it is this frame's pose with every other voice folded in.
2104
- */
2105
- baseFor(voice, subject) {
2106
- let prior = this.pose.get(subject)?.pose;
2107
- if (prior === undefined && !this.folding.has(voice.id)) {
2108
- this.folding.add(voice.id);
2109
- try {
2110
- prior = this.fold(subject, undefined, false, voice.id);
2111
- }
2112
- finally {
2113
- this.folding.delete(voice.id);
2114
- }
2115
- }
2116
- const base = {};
2117
- for (const key of voice.patch.writes) {
2118
- const channel = this.kit[key];
2119
- const value = prior?.[key];
2120
- const fallback = channel.rest;
2121
- if (value !== undefined)
2122
- base[key] = copy(value);
2123
- else if (fallback !== undefined)
2124
- base[key] = copy(fallback);
2125
- }
2126
- return base;
2127
- }
2128
- /**
2129
- * How fast each numeric channel this voice writes was moving, from the subject's last two probed
2130
- * poses, in units per ms. Undefined with fewer than two on record, which starts the voice at rest.
2131
- */
2132
- slopeFor(voice, subject) {
2133
- const rec = this.pose.get(subject);
2134
- if (rec?.prev === undefined || !(rec.at > rec.prevAt))
2135
- return undefined;
2136
- const dt = rec.at - rec.prevAt;
2137
- const now = rec.pose;
2138
- const before = rec.prev;
2139
- const slope = {};
2140
- for (const key of voice.patch.writes) {
2141
- const a = before[key];
2142
- const b = now[key];
2143
- if (typeof a === 'number' && typeof b === 'number')
2144
- slope[key] = (b - a) / dt;
2145
- else if (Array.isArray(a) && Array.isArray(b) && a.length === b.length)
2146
- slope[key] = b.map((v, i) => (v - a[i]) / dt);
2147
- }
2148
- return slope;
2149
- }
2150
- isRest(delta) {
2151
- for (const key of Object.keys(delta)) {
2152
- if (delta[key] === undefined)
2153
- continue;
2154
- const channel = this.kit[key];
2155
- if (channel.rest === undefined)
2156
- return false;
2157
- if (!near(delta[key], channel.rest))
2158
- return false;
2159
- }
2160
- return true;
2161
- }
2162
- /** Folds a locus's members into one influence through each channel's own `lerp`. */
2163
- foldLocus(members) {
2164
- let sum = 0;
2165
- for (const m of members)
2166
- sum += m.weight;
2167
- const delta = {};
2168
- if (sum <= 0)
2169
- return { delta, weight: 0 };
2170
- const taken = new Map();
2171
- for (const member of members) {
2172
- if (member.weight <= 0)
2173
- continue;
2174
- for (const key of Object.keys(member.delta)) {
2175
- if (member.delta[key] === undefined)
2176
- continue;
2177
- const channel = this.kit[key];
2178
- const held = taken.get(key);
2179
- if (held === undefined) {
2180
- delta[key] = copy(member.delta[key]);
2181
- taken.set(key, member.weight);
2182
- continue;
2183
- }
2184
- const total = held + member.weight;
2185
- delta[key] = channel.lerp(delta[key], member.delta[key], member.weight / total);
2186
- taken.set(key, total);
2187
- }
2188
- }
2189
- return { delta, weight: sum > 1 ? 1 : sum };
2190
- }
2191
- /** Whether a rest-less channel's influence is switched on, across a band rather than an edge. */
2192
- passes(was, w) {
2193
- const { on, off } = this.band;
2194
- return w >= on ? true : w <= off ? false : (was ?? w >= on);
2195
- }
2196
- /**
2197
- * The first of this subject's records, linked through every live voice that reaches it. A voice
2198
- * is asked once, through the record it keeps for the subject anyway; a pending one is linked when
2199
- * it goes live, since `target` is asked on first sight, and sight only comes once a voice plays
2200
- * or holds before.
2201
- */
2202
- chain(subject, now) {
2203
- const was = this.chains.get(subject);
2204
- if (was !== undefined && was.version === this.version)
2205
- return was;
2206
- let first = null;
2207
- let prev = null;
2208
- const link = (voice) => {
2209
- const state = voice.state;
2210
- if (state === 'done' || (state === 'pending' && !voice.holdsBefore))
2211
- return;
2212
- const held = this.held(voice, subject, now);
2213
- if (!held.reaches)
2214
- return;
2215
- held.voice = voice;
2216
- held.next = null;
2217
- if (prev === null)
2218
- first = held;
2219
- else
2220
- prev.next = held;
2221
- prev = held;
2222
- };
2223
- const named = this.naming === 0 ? undefined : this.named.get(subject);
2224
- let j = 0;
2225
- for (const voice of this.general) {
2226
- while (named !== undefined && j < named.length && named[j].id < voice.id)
2227
- link(named[j++]);
2228
- link(voice);
2229
- }
2230
- while (named !== undefined && j < named.length)
2231
- link(named[j++]);
2232
- const head = first ?? stub();
2233
- head.version = this.version;
2234
- if (was !== undefined && was !== head) {
2235
- head.loci = was.loci;
2236
- was.loci = null;
2237
- head.slot = was.slot;
2238
- was.slot = -1;
2239
- }
2240
- else if (was === undefined && this.lanes !== null && head.slot < 0)
2241
- head.slot = this.lanes.number(subject);
2242
- if (was !== head)
2243
- this.chains.set(subject, head);
2244
- return head;
2245
- }
2246
- /** Whether a voice reaches a subject by what its spec says: the subjects it names, or its `target`. */
2247
- aims(voice, subject) {
2248
- // A read back to before a subject left still finds it reached.
2249
- const left = voice.parted?.get(subject);
2250
- if (left !== undefined && !(this.now < left))
2251
- return false;
2252
- if (voice.named !== null)
2253
- return voice.named.has(subject);
2254
- return voice.spec.target ? voice.spec.target(subject) : true;
2255
- }
2256
- /**
2257
- * A voice joined or left the list, or started or stopped waiting: the chains it is linked into
2258
- * are relinked, every subject's for a voice over all of them and only its own for one naming
2259
- * its subjects, and lanes take it on or off.
2260
- */
2261
- changed(voice) {
2262
- this.lanes?.touch(voice);
2263
- if (voice.named === null) {
2264
- this.version++;
2265
- return;
2266
- }
2267
- for (const subject of voice.named) {
2268
- const head = this.chains.get(subject);
2269
- if (head !== undefined)
2270
- head.version = Number.NaN;
2271
- }
2272
- this.relinks++;
2273
- }
2274
- /** Files a voice under each subject it names, or among the voices that name none. */
2275
- index(voice) {
2276
- if (voice.named === null) {
2277
- this.general.push(voice);
2278
- return;
2279
- }
2280
- this.naming++;
2281
- for (const subject of voice.named) {
2282
- const list = this.named.get(subject);
2283
- if (list === undefined)
2284
- this.named.set(subject, [voice]);
2285
- else
2286
- list.push(voice);
2287
- }
2288
- }
2289
- unindex(voice) {
2290
- this.naming--;
2291
- for (const subject of voice.named) {
2292
- const list = this.named.get(subject);
2293
- if (list === undefined)
2294
- continue;
2295
- const i = list.indexOf(voice);
2296
- if (i >= 0)
2297
- list.splice(i, 1);
2298
- if (list.length === 0)
2299
- this.named.delete(subject);
2300
- }
2301
- }
2302
423
  /** Voices whose stop 0 is mid-computation, so a fold for one cannot re-enter itself. */
2303
424
  folding = new Set();
2304
- /** Folds one voice's delta into the pose, its band state kept on the subject's record. */
2305
- apply(pose, voice, held, delta, weight) {
2306
- const slots = voice.slots;
2307
- for (let i = 0; i < slots.length; i++) {
2308
- const slot = slots[i];
2309
- const key = this.names[slot];
2310
- const value = delta[key];
2311
- if (value === undefined)
2312
- continue;
2313
- const channel = this.channels[slot];
2314
- if (channel.rest !== undefined && channel.scale) {
2315
- // pose[key] is the copy of rest this fold made, so it is ours to write into.
2316
- pose[key] = channel.fold
2317
- ? channel.fold(pose[key], value, weight)
2318
- : channel.merge(pose[key], channel.scale(value, weight));
2319
- continue;
2320
- }
2321
- const band = held.bands[i];
2322
- const on = this.passes(band === 0 ? undefined : band === 1, weight);
2323
- held.bands[i] = on ? 1 : 2;
2324
- if (on)
2325
- pose[key] = pose[key] === undefined ? copy(value) : channel.merge(pose[key], value);
2326
- }
2327
- }
2328
- /** Clamps every bounded channel of a folded pose into its range, in place. */
2329
- clamp(pose) {
2330
- for (const slot of this.bounded) {
2331
- const [lo, hi] = this.channels[slot].bounds;
2332
- const key = this.names[slot];
2333
- const v = pose[key];
2334
- if (typeof v === 'number')
2335
- pose[key] = v < lo ? lo : v > hi ? hi : v;
2336
- else if (Array.isArray(v))
2337
- for (let i = 0; i < v.length; i++) {
2338
- const x = v[i];
2339
- v[i] = x < lo ? lo : x > hi ? hi : x;
2340
- }
2341
- }
2342
- return pose;
2343
- }
2344
- fold(subject, out, dry = false, except) {
2345
- const pose = this.folded(subject, out, dry, except);
2346
- return (this.bounded.length === 0 ? pose : this.clamp(pose));
2347
- }
2348
- folded(subject, out, dry = false, except) {
2349
- const now = this.now;
2350
- const head = Number.isNaN(now) ? null : this.chain(subject, now);
2351
- // A subject numbered since the frame's fill folds every voice here, laned ones included.
2352
- const laned = head !== null && this.lanes?.prepare(head.slot, subject, now, this.version) === true;
2353
- return this.foldWith(subject, out ?? {}, head, laned, dry, except);
2354
- }
2355
- /** The fold after a subject's chain is linked and its lanes asked whether it reads from them. */
2356
- foldWith(subject, out, head, laned, dry, except) {
2357
- const now = this.now;
2358
- const pose = out;
2359
- const lanes = this.lanes;
2360
- const skip = laned ? lanes.copies : null;
2361
- for (let i = 0; i < this.names.length; i++) {
2362
- if (skip?.[i])
2363
- continue;
2364
- const rest = this.channels[i].rest;
2365
- const key = this.names[i];
2366
- // Never `delete`: it drops a reused out object into dictionary mode for good.
2367
- if (rest !== undefined)
2368
- pose[key] = copy(rest);
2369
- else if (pose[key] !== undefined)
2370
- pose[key] = undefined;
2371
- }
2372
- if (head === null)
2373
- return pose;
2374
- if (laned) {
2375
- lanes.copy(head.slot, pose);
2376
- if (lanes.whole)
2377
- return pose;
2378
- }
2379
- if (this.loci === 0) {
2380
- for (let held = head; held !== null; held = held.next) {
2381
- const voice = held.voice;
2382
- if (voice === null || (laned && voice.laned))
2383
- continue;
2384
- if (voice.id === except || voice.state === 'done')
2385
- continue;
2386
- const delta = this.read(voice, subject, now, dry, held);
2387
- if (delta === null || this.w <= 0)
2388
- continue;
2389
- this.apply(pose, voice, held, delta, this.w);
2390
- }
2391
- return pose;
2392
- }
2393
- const order = [];
2394
- const weights = [];
2395
- const loci = new Map();
2396
- for (let held = head; held !== null; held = held.next) {
2397
- const voice = held.voice;
2398
- if (voice === null || (laned && voice.laned))
2399
- continue;
2400
- if (voice.id === except || voice.state === 'done')
2401
- continue;
2402
- const delta = this.read(voice, subject, now, dry, held);
2403
- if (delta === null)
2404
- continue;
2405
- const locus = voice.spec.locus;
2406
- if (locus === undefined) {
2407
- order.push({ voice, held, delta });
2408
- weights.push(this.w);
2409
- continue;
2410
- }
2411
- const members = loci.get(locus);
2412
- if (members)
2413
- members.push({ delta, weight: this.w });
2414
- else {
2415
- loci.set(locus, [{ delta, weight: this.w }]);
2416
- order.push(locus);
2417
- weights.push(0);
2418
- }
2419
- }
2420
- let bands = head.loci;
2421
- if (bands === null) {
2422
- bands = new Map();
2423
- head.loci = bands;
2424
- }
2425
- for (let n = 0; n < order.length; n++) {
2426
- const at = order[n];
2427
- if (typeof at !== 'string') {
2428
- if (weights[n] > 0)
2429
- this.apply(pose, at.voice, at.held, at.delta, weights[n]);
2430
- continue;
2431
- }
2432
- const { delta, weight } = this.foldLocus(loci.get(at));
2433
- if (weight <= 0)
2434
- continue;
2435
- for (const key of Object.keys(delta)) {
2436
- const channel = this.kit[key];
2437
- const value = delta[key];
2438
- if (value === undefined)
2439
- continue;
2440
- if (channel.rest !== undefined && channel.scale) {
2441
- // pose[key] is the copy of rest this fold made, so it is ours to write into.
2442
- pose[key] = channel.fold
2443
- ? channel.fold(pose[key], value, weight)
2444
- : channel.merge(pose[key], channel.scale(value, weight));
2445
- continue;
2446
- }
2447
- const band = `${at}:${key}`;
2448
- const on = this.passes(bands.get(band), weight);
2449
- bands.set(band, on);
2450
- if (on)
2451
- pose[key] = pose[key] === undefined ? copy(value) : channel.merge(pose[key], value);
2452
- }
2453
- }
2454
- return pose;
2455
- }
2456
- /** `influence`, or for a dry fold a subject already probed this frame, without advancing it. */
2457
- read(voice, subject, now, dry, held) {
2458
- if (dry) {
2459
- if (held.reaches && held.probed === now && held.delta && held.seeks === voice.seeks) {
2460
- const setting = voice.setting;
2461
- setting.timestamp = now;
2462
- setting.dt = 0;
2463
- setting.elapsed = voice.elapsedAt(now);
2464
- setting.pass = 0;
2465
- setting.weight = 0;
2466
- setting.state = held.state;
2467
- setting.keep = held.keep;
2468
- this.w = this.weigh(voice, subject, now, held);
2469
- held.weight = this.w;
2470
- if (voice.holder !== held && voice.scratch.length > 0)
2471
- this.keyed(voice, subject, held);
2472
- return held.delta;
2473
- }
2474
- }
2475
- return this.influence(voice, subject, now, held);
425
+ linkedLaned = false;
426
+ /**
427
+ * Per subject, twice `restsKey` at its last probe plus 1 if the pose rested; null until `atRest`
428
+ * is first asked, when probes start writing it.
429
+ */
430
+ restStamps = null;
431
+ /** What a probe's record of rest is good for: moved by a new frame, version or relink. */
432
+ restsEpoch = 0;
433
+ restsFrame = Number.NaN;
434
+ restsVersion = Number.NaN;
435
+ restsRelinks = -1;
436
+ restsStirs = -1;
437
+ /** The pose `atRest` folds into where no probe this frame answers. */
438
+ restScratch;
439
+ restsLaned() {
440
+ return this.lanes.whole && this.bounded.length === 0;
441
+ }
442
+ restsKey() {
443
+ if (this.frame !== this.restsFrame ||
444
+ this.version !== this.restsVersion ||
445
+ this.relinks !== this.restsRelinks ||
446
+ this.stirs !== this.restsStirs) {
447
+ this.restsEpoch++;
448
+ this.restsFrame = this.frame;
449
+ this.restsVersion = this.version;
450
+ this.restsRelinks = this.relinks;
451
+ this.restsStirs = this.stirs;
452
+ }
453
+ return this.restsEpoch;
2476
454
  }
2477
455
  }
456
+ // The probe's hot path is methods, installed here from the modules that own them: as functions
457
+ // called directly they made probes and fills up to 9% slower (bench/ab.sh on teitou, 2026-10-05).
458
+ const methods = {
459
+ fold,
460
+ folded,
461
+ linked,
462
+ foldWith,
463
+ owedBy,
464
+ foldLoci,
465
+ read,
466
+ influence,
467
+ keyed,
468
+ tick,
469
+ apply,
470
+ clamp,
471
+ passes,
472
+ isRest,
473
+ baseFor,
474
+ slopeFor,
475
+ prime,
476
+ capped,
477
+ horizonFor,
478
+ weigh,
479
+ weighOwned,
480
+ base,
481
+ envelope: fadeOf,
482
+ ownedBy,
483
+ ownerBase,
484
+ blended,
485
+ laneRead,
486
+ shareKept,
487
+ parting,
488
+ chain,
489
+ linkable,
490
+ aims,
491
+ held,
492
+ unreachedOf,
493
+ sinceOf,
494
+ shownOf,
495
+ keep,
496
+ foldLocus,
497
+ };
498
+ for (const [name, value] of Object.entries(methods))
499
+ Object.defineProperty(Mixer.prototype, name, { value, writable: true, configurable: true });
2478
500
  /**
2479
501
  * The one that ships: every form on the CPU, probed per subject on demand.
2480
502
  *