@msb235/blits 0.3.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 +314 -0
  2. package/README.md +3 -2
  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 +143 -122
  47. package/dist/lanes.js +236 -742
  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 -169
  55. package/dist/mixer.js +321 -1793
  56. package/dist/motion.d.ts +104 -19
  57. package/dist/motion.js +448 -180
  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 +261 -42
  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 +151 -46
  102. package/package.json +5 -2
package/dist/mixer.js CHANGED
@@ -1,324 +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 } 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
- weight: 0,
25
- rested: false,
26
- bands: new Uint8Array(0),
27
- state: undefined,
28
- stepped: 0,
29
- ticks: 0,
30
- probed: Number.NaN,
31
- delta: null,
32
- phase: 0,
33
- seeks: 0,
34
- kept,
35
- keep: keeper(kept),
36
- voice: null,
37
- next: null,
38
- version: Number.NaN,
39
- loci: null,
40
- slot: -1,
41
- };
42
- }
43
- function elapsedWith(c, now) {
44
- const dt = now - c.anchorNow;
45
- const r = c.ramp;
46
- if (r === null)
47
- return c.anchorElapsed + dt * c.rate;
48
- if (dt <= 0)
49
- return c.anchorElapsed + dt * r.from;
50
- const d = r.to - r.from;
51
- if (dt <= r.over)
52
- return c.anchorElapsed + r.from * dt + (d * dt * dt) / (2 * r.over);
53
- return c.anchorElapsed + r.from * r.over + (d * r.over) / 2 + r.to * (dt - r.over);
54
- }
55
- /**
56
- * The last entry taken before `t`, or at it where `inclusive` says so, as it does for a change a
57
- * sync made. A host's change between frames is taken strictly: it shows from the next frame on, as
58
- * it did live.
59
- */
60
- function last(list, t, inclusive) {
61
- let lo = 0;
62
- let hi = list.length - 1;
63
- let found;
64
- while (lo <= hi) {
65
- const mid = (lo + hi) >> 1;
66
- const e = list[mid];
67
- if (e.at < t || (e.at === t && (typeof inclusive === 'function' ? inclusive(e) : inclusive))) {
68
- found = e;
69
- lo = mid + 1;
70
- }
71
- else
72
- hi = mid - 1;
73
- }
74
- return found;
75
- }
76
- /** A store that fills a missing entry from `make` on first ask. */
77
- class Filled extends Store {
78
- make;
79
- constructor(make) {
80
- super();
81
- this.make = make;
82
- }
83
- get(key) {
84
- let v = super.get(key);
85
- if (v === undefined) {
86
- v = this.make(key);
87
- if (v !== undefined)
88
- super.set(key, v);
89
- }
90
- return v;
91
- }
92
- }
93
- const noSend = () => { };
94
- const near = (a, b) => {
95
- if (typeof a === 'number' && typeof b === 'number')
96
- return Math.abs(a - b) < 1e-9;
97
- if (Array.isArray(a) && Array.isArray(b))
98
- return a.length === b.length && a.every((v, i) => near(v, b[i]));
99
- return a === b;
100
- };
101
- const copy = (v) => (Array.isArray(v) ? [...v] : v);
102
- /** Writes a folded pose's value into subject `n`'s places in a column. */
103
- function writeValue(c, v, n) {
104
- const at = n * c.axes;
105
- if (typeof v === 'number' && c.axes === 1)
106
- c.out[at] = v;
107
- else if (Array.isArray(v) && v.length === c.axes)
108
- for (let a = 0; a < c.axes; a++)
109
- c.out[at + a] = v[a];
110
- else if (v === undefined)
111
- c.out.fill(Number.NaN, at, at + c.axes);
112
- else
113
- 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)}`);
114
- }
115
- /** Deep equality over plain data, for telling whether a host field changed. */
116
- function same(a, b) {
117
- if (a === b)
118
- return true;
119
- if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null)
120
- return false;
121
- const ka = Object.keys(a);
122
- const kb = Object.keys(b);
123
- if (ka.length !== kb.length)
124
- return false;
125
- for (const k of ka)
126
- if (!same(a[k], b[k]))
127
- return false;
128
- return true;
129
- }
130
- export class Voice {
131
- id;
132
- spec;
133
- patch;
134
- fade;
135
- start;
136
- state = 'pending';
137
- rate;
138
- weight;
139
- /**
140
- * elapsed = anchorElapsed + the rate integrated from anchorNow, rebased on a rate change or a seek.
141
- * Without a ramp the rate is constant; with one it moves linearly from `from` to `to` over `over`
142
- * mix ms starting at the anchor, and holds `to` after.
143
- */
144
- anchorNow;
145
- anchorElapsed = 0;
146
- ramp = null;
147
- out = null;
148
- subjects = new Store();
149
- /** The subjects its spec names, or null where it names none. */
150
- named;
151
- /** Kit slot of each channel the patch writes, in `writes` order. */
152
- slots;
153
- /** The stops built ahead of time, for a `keys` patch. */
154
- built;
155
- /** The kit's `lerp` for each channel the patch writes, which keyed stops interpolate through. */
156
- lerps;
157
- /** Per keyed channel, the in-place form of the lerp its stops take, where it has one. */
158
- intos;
159
- /**
160
- * The arrays keyed reads interpolate into: one set for the voice, not one per subject, since 10k
161
- * subjects' worth of long-lived arrays costs more in cache misses than allocating them did.
162
- */
163
- scratch = [];
164
- /** The record whose delta holds `scratch`'s arrays; any other's must be read again to be handed out. */
165
- holder = null;
166
- /** Whether its channels run as lanes this frame, so the general fold passes it by. */
167
- laned = false;
168
- /** How many times it has been sought, so a delta read before a seek is not handed out after it. */
169
- seeks = 0;
170
- /** Whether its patch has kept state on a record through `setting.keep`, which makes it stateful. */
171
- keeping = false;
172
- /** For a motion patch, the hook it was given to ask the mix for its subjects' voice time. */
173
- frame = null;
174
- ease;
175
- /** How many passes its `loop` plays, worked out once. */
176
- passes;
177
- /** Reused for every call this voice makes, so it is valid only during the call. */
178
- setting;
179
- /** Every subject this voice has been asked about, so a handover knows when it is finished. */
180
- seen = 0;
181
- /** The longest stagger of any subject seen, so a finite loop waits for the last of them. */
182
- latest = 0;
183
- restedCount = 0;
184
- /** The mix time it was cued at; -Infinity before the first sync. */
185
- cuedAt = Number.NEGATIVE_INFINITY;
186
- /** The mix time it left at. */
187
- doneAt = Number.POSITIVE_INFINITY;
188
- /** Under `history`, its controls after each change, oldest first. */
189
- log = null;
190
- /** Where an anchored `out` or `end` puts its fade's start, mix time; Infinity until known. */
191
- outAt = Number.POSITIVE_INFINITY;
192
- /** Whether its start is still to be fixed by an anchor, so it waits pending. */
193
- placing = false;
194
- resolve;
195
- done;
196
- constructor(id, spec, patch, fade, now, start, slotOf, channels, host, send) {
197
- this.id = id;
198
- this.spec = spec;
199
- this.patch = patch;
200
- this.fade = fade;
201
- this.start = start;
202
- this.slots = patch.writes.map((k) => slotOf.get(k));
203
- this.named = spec.subjects ? new Set(spec.subjects) : null;
204
- this.built = patch.form === 'keys' && patch.keys ? builtOf(patch) : null;
205
- this.lerps = this.slots.map((slot) => channels[slot].lerp);
206
- this.intos = this.built
207
- ? intosOf(this.built, this.slots.map((slot) => channels[slot]))
208
- : undefined;
209
- this.ease = fade.ease === undefined ? undefined : curve(fade.ease);
210
- this.passes = passesOf(spec.loop);
211
- this.setting = {
212
- timestamp: 0,
213
- dt: 0,
214
- elapsed: 0,
215
- pass: 0,
216
- weight: 0,
217
- state: undefined,
218
- host,
219
- keep: keeper(new Map()),
220
- send,
221
- };
222
- this.rate = spec.rate ?? 1;
223
- this.weight = typeof spec.weight === 'number' ? spec.weight : 1;
224
- this.anchorNow = start;
225
- this.done = new Promise((r) => {
226
- this.resolve = r;
227
- });
228
- if (now >= start)
229
- this.state = 'live';
230
- }
231
- elapsedAt(now) {
232
- return elapsedWith(this, now);
233
- }
234
- /** The mix time its clock reads `elapsed`, inverting `elapsedAt`; Infinity where it never will. */
235
- timeAt(elapsed) {
236
- const e0 = this.anchorElapsed;
237
- const r = this.ramp;
238
- if (r === null) {
239
- if (this.rate > 0)
240
- return this.anchorNow + (elapsed - e0) / this.rate;
241
- return elapsed <= e0 ? this.anchorNow : Number.POSITIVE_INFINITY;
242
- }
243
- if (elapsed <= e0)
244
- return r.from > 0 ? this.anchorNow + (elapsed - e0) / r.from : this.anchorNow;
245
- const a = (r.to - r.from) / (2 * r.over);
246
- const atEnd = e0 + r.from * r.over + a * r.over * r.over;
247
- if (elapsed <= atEnd) {
248
- const c = e0 - elapsed;
249
- const dt = Math.abs(a) < 1e-12
250
- ? -c / r.from
251
- : (-r.from + Math.sqrt(Math.max(0, r.from * r.from - 4 * a * c))) / (2 * a);
252
- return this.anchorNow + dt;
253
- }
254
- if (r.to <= 0)
255
- return Number.POSITIVE_INFINITY;
256
- return this.anchorNow + r.over + (elapsed - atEnd) / r.to;
257
- }
258
- /** Records this voice's controls as they stand, from mix time `at`. */
259
- note(at, sync = false) {
260
- this.log?.push({
261
- at,
262
- sync,
263
- anchorNow: this.anchorNow,
264
- anchorElapsed: this.anchorElapsed,
265
- rate: this.rate,
266
- ramp: this.ramp,
267
- weight: this.weight,
268
- out: this.out,
269
- start: this.start,
270
- outAt: this.outAt,
271
- });
272
- }
273
- /**
274
- * A copy for a projection: its own setting, which sends nothing, and its own per-subject records,
275
- * filled from `fill`. With `controls` it takes those, as the voice stood at an earlier time.
276
- */
277
- copy(fill, controls) {
278
- const v = Object.assign(Object.create(Voice.prototype), this);
279
- const w = v;
280
- w.subjects = new Filled(fill);
281
- w.setting = { ...this.setting, keep: keeper(new Map()), send: noSend };
282
- v.scratch = [];
283
- v.holder = null;
284
- v.laned = false;
285
- v.log = null;
286
- if (controls) {
287
- v.anchorNow = controls.anchorNow;
288
- v.anchorElapsed = controls.anchorElapsed;
289
- v.rate = controls.rate;
290
- v.ramp = controls.ramp;
291
- v.weight = controls.weight;
292
- v.out = controls.out;
293
- v.start = controls.start;
294
- v.outAt = controls.outAt;
295
- }
296
- v.resolve = noSend;
297
- return v;
298
- }
299
- rateAt(now) {
300
- const r = this.ramp;
301
- if (r === null)
302
- return this.rate;
303
- const u = (now - this.anchorNow) / r.over;
304
- return u >= 1 ? r.to : u <= 0 ? r.from : r.from + (r.to - r.from) * u;
305
- }
306
- /** Moves the anchor to `now`, carrying what is left of a ramp. */
307
- rebase(now) {
308
- const rate = this.rateAt(now);
309
- this.anchorElapsed = this.elapsedAt(now);
310
- const r = this.ramp;
311
- if (r !== null) {
312
- const left = this.anchorNow + r.over - now;
313
- this.ramp = left > 0 ? { from: rate, to: r.to, over: left } : null;
314
- }
315
- this.anchorNow = now;
316
- }
317
- }
318
- 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 {
319
24
  kit;
320
25
  opts;
321
- voices = [];
26
+ cued = [];
322
27
  /** Under `history`, voices that have left but that a read back may still reach. */
323
28
  gone = [];
324
29
  /** Set on a projection's own mixer: it sends nothing, keeps no history, and reads without committing. */
@@ -333,24 +38,59 @@ class Mixer {
333
38
  */
334
39
  hostLog = [];
335
40
  /**
336
- * Marks the host announced on a score: mix time, NaN until the next sync for one announced as
337
- * 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.
338
44
  */
339
45
  announced = [];
340
46
  hostThen;
341
47
  /** Each subject's last two poses and when they were probed, for `from: 'current'`. */
342
48
  pose = new Store();
343
49
  nextId = 1;
344
- /** 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
+ */
345
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();
346
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;
347
63
  last = Number.NaN;
348
64
  rebasing = false;
349
65
  wantsPose = false;
350
66
  /** The pose `pull` folds a subject into where it cannot read straight from the lanes. */
351
67
  scratch;
68
+ /** The last array `pull` read, by position, with each subject's chain head, to skip the lookup. */
69
+ /** Live voices by the earliest mix time `moveTo` would change each, a binary heap. */
70
+ due = [];
71
+ /** Voices retired since `moveTo` last pruned them, in the order they retired. */
72
+ retired = [];
73
+ /** Visit every voice each sync, as before the due queue; for tests that compare the two. */
74
+ walkAll = false;
75
+ pulled = [];
76
+ pulledHeads = [];
77
+ /** Each remembered head's lane slot, and the `version` and `relinks` they were all current at. */
78
+ pulledSlots = new Int32Array(0);
79
+ pulledVersion = Number.NaN;
80
+ pulledRelinks = -1;
81
+ /** Counts chains made stale one subject at a time, which `version` does not. */
82
+ relinks = 0;
352
83
  /** Read once a sync, so a `reduce` function is not called per voice per subject. */
353
84
  reducedNow = false;
85
+ /** Something was changed since the last sync, so the next frame may differ from this one. */
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
+ }
354
94
  /**
355
95
  * Per subject, the first record of the chain through every live voice that reaches it, or a stub
356
96
  * where none does. Relinked when `version` moves, which is whenever the list or a voice's pending
@@ -358,14 +98,28 @@ class Mixer {
358
98
  */
359
99
  chains = new Store();
360
100
  version = 0;
101
+ steps = new Steps();
361
102
  /** Per subject, the voices whose `subjects` name it, in voice order. */
362
103
  named = new Store();
104
+ /** Voices a subject has been faded out of, which `drop` looks in besides those that reach it. */
105
+ parters = new Set();
106
+ /**
107
+ * Motion patches that may hold state for a subject no voice still reaching it plays, which `drop`
108
+ * frees besides those voices' own: per subject, a patch a voice naming it left while another
109
+ * voice plays on, or that the host retargeted it on; and any patch a voice over every subject
110
+ * left while another plays on. How many live voices play each motion patch says which.
111
+ */
112
+ strays = new Store();
113
+ strayAll = new Set();
114
+ playing = new Map();
363
115
  /** How many voices in the list carry `subjects`. */
364
116
  naming = 0;
365
117
  /** The voices in the list that name no subjects, in voice order. */
366
118
  general = [];
367
119
  /** How many voices in the list carry a locus; with none, a fold allocates nothing. */
368
120
  loci = 0;
121
+ /** What every motion patch it plays asks for its voice's time, holding this mix weakly. */
122
+ owner = null;
369
123
  /** How many voices in the list are anchored, so a sync with none skips placing them. */
370
124
  anchored = 0;
371
125
  /** Events sent since the last drain, and who is being probed, so `send` knows whose they are. */
@@ -392,18 +146,29 @@ class Mixer {
392
146
  channels;
393
147
  /** Slots of the channels that declare bounds, clamped after every fold. */
394
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;
395
154
  slotOf = new Map();
396
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;
397
161
  constructor(kit, opts) {
398
162
  this.kit = kit;
399
163
  this.opts = opts;
400
164
  this.names = Object.keys(kit);
401
165
  this.channels = this.names.map((k) => kit[k]);
402
166
  this.bounded = this.channels.flatMap((c, i) => (c.bounds ? [i] : []));
167
+ this.lerpsInto = this.channels.map((c) => lerpInto(c));
403
168
  this.names.forEach((k, i) => {
404
169
  this.slotOf.set(k, i);
405
170
  });
406
- this.lanes = opts.lanes === false ? null : new Lanes(this.laneHost());
171
+ this.lanes = opts.lanes === false ? null : new Lanes(laneHost(this));
407
172
  }
408
173
  get reduced() {
409
174
  const r = this.opts.reduce;
@@ -413,336 +178,71 @@ class Mixer {
413
178
  return this.opts.band ?? { on: 0.6, off: 0.4 };
414
179
  }
415
180
  cue(spec) {
416
- const patch = spec.patch;
417
181
  const engine = this.opts.engine ?? mixer;
418
- if (!engine.runs.has(patch.form))
419
- throw new Error(`blits: engine ${engine.name} does not run ${patch.form} patches`);
420
- for (const channel of patch.writes) {
421
- if (!(channel in this.kit))
422
- throw new Error(`blits: kit has no channel ${String(channel)}, which this patch writes`);
423
- const wanted = patch.kit?.[channel];
424
- const here = this.kit[channel];
425
- if (wanted && wanted !== here && (wanted.kind === undefined || wanted.kind !== here.kind))
426
- 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'}`);
427
- }
428
- if (patch.reads) {
429
- const host = this.opts.host;
430
- for (const field of patch.reads)
431
- if (typeof host !== 'object' || host === null || !(field in host))
432
- throw new Error(`blits: the patch reads host.${field}, which this mix's host lacks`);
433
- }
434
- if (spec.from === 'current' && patch.form !== 'keys')
435
- throw new Error("blits: from: 'current' needs a keys patch");
436
- if (spec.subjects !== undefined && spec.target !== undefined)
437
- throw new Error('blits: a voice takes target or subjects, not both');
438
- const anchor = spec.anchor;
439
- if (anchor)
440
- this.checkPlacement(spec, anchor);
441
- const anchored = anchor !== undefined && (anchor.start !== undefined || anchor.in !== undefined);
442
- const start = anchored
443
- ? Number.POSITIVE_INFINITY
444
- : spec.start !== undefined
445
- ? spec.start - this.offset
446
- : Number.isNaN(this.now)
447
- ? 0
448
- : this.now;
449
- const voice = new Voice(this.nextId++, spec, patch, spec.fade ?? {}, this.now, start, this.slotOf, this.channels, this.opts.host, this.send);
450
- if (!Number.isNaN(this.now))
451
- voice.cuedAt = this.now;
452
- voice.placing = anchored;
453
- const motion = motionOf(patch);
454
- if (motion !== undefined) {
455
- voice.frame = Mixer.frameHook(new WeakRef(this), new WeakRef(voice));
456
- motion.frame = voice.frame;
457
- }
458
- this.voices.push(voice);
459
- this.index(voice);
460
- this.version++;
461
- if (spec.locus !== undefined)
462
- this.loci++;
463
- if (spec.from === 'current')
464
- this.wantsPose = true;
465
- if (anchor) {
466
- this.anchored++;
467
- this.place();
468
- if (!Number.isNaN(this.now) && voice.state === 'pending' && this.now >= voice.start) {
469
- voice.state = 'live';
470
- this.version++;
471
- }
472
- }
473
- // After placing, so the controls it starts with are where its anchors put it at the cue.
474
- if (this.opts.history) {
475
- voice.log = [];
476
- voice.note(Number.NEGATIVE_INFINITY);
477
- }
478
- 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);
479
185
  }
480
186
  blend(patches, by, spec = {}) {
481
187
  const locus = `blend:${this.nextId}`;
482
- const stops = patches.length - 1;
188
+ const of = blendOf(by, patches.length - 1);
483
189
  return patches.map((patch, i) => {
484
- const share = (subject, setting) => {
485
- const k = by(subject, setting);
486
- const d = stops === 0 ? 0 : Math.abs(k * stops - i);
487
- return d >= 1 ? 0 : 1 - d;
488
- };
489
- const weight = by.input ? Object.assign(share, { input: true }) : share;
490
- 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;
491
195
  });
492
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
+ }
493
202
  sync(timestamp) {
494
203
  if (this.rebasing && !Number.isNaN(this.last))
495
204
  this.offset += timestamp - this.last;
496
205
  this.rebasing = false;
497
206
  this.last = timestamp;
498
- const now = timestamp - this.offset;
499
- if (now === this.now)
500
- return;
501
- this.move(now);
502
- }
503
- /** Moves the mix clock to `now`: voices start, finite loops end, and fades finish. */
504
- move(now) {
505
- this.moving = true;
506
- try {
507
- this.moveTo(now);
508
- }
509
- finally {
510
- this.moving = false;
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();
511
220
  }
221
+ const bookers = this.bookers;
222
+ if (bookers !== null)
223
+ for (const b of bookers)
224
+ b.sync();
512
225
  }
513
- moveTo(now) {
514
- this.now = now;
515
- this.reducedNow = this.reduced;
516
- for (const a of this.announced)
517
- if (Number.isNaN(a.at))
518
- a.at = now;
519
- if (this.anchored > 0)
520
- this.place();
521
- if (this.announced.length > 0) {
522
- // Kept while still ahead, or while history reaches it; anchors waiting on one were placed above.
523
- const reach = now - (this.opts.history?.ms ?? 0);
524
- this.announced = this.announced.filter((a) => a.at >= reach);
525
- }
526
- for (const voice of this.voices) {
527
- if (voice.state === 'done')
528
- continue;
529
- if (voice.state === 'live' && voice.outAt <= now)
530
- this.beginFade(voice, {}, Math.max(voice.start, voice.outAt));
531
- if (voice.state === 'pending' && now >= voice.start) {
532
- voice.state = 'live';
533
- this.version++;
534
- }
535
- const period = voice.patch.period;
536
- const passes = voice.passes;
537
- if (voice.state === 'live' && period > 0 && Number.isFinite(passes)) {
538
- const end = period * passes + voice.latest;
539
- // The fade starts when the last pass ended, not at the frame that noticed, so it plays the
540
- // same at any frame rate and a read at another time can find it.
541
- if (voice.elapsedAt(now) >= end)
542
- this.beginFade(voice, {}, Math.max(voice.start, Math.min(now, voice.timeAt(end))));
543
- }
544
- if (voice.state === 'fading' && voice.out) {
545
- const { at, over, rest, deadline } = voice.out;
546
- const spent = now - at;
547
- if (rest) {
548
- const out = deadline !== undefined && spent >= deadline;
549
- const settled = voice.seen > 0 && voice.restedCount >= voice.seen;
550
- if (out)
551
- this.retire(voice, at + deadline);
552
- else if (settled)
553
- this.retire(voice);
554
- }
555
- else if (spent >= over)
556
- this.retire(voice, at + over);
557
- }
558
- }
559
- let pruned = false;
560
- for (let i = this.voices.length - 1; i >= 0; i--) {
561
- const voice = this.voices[i];
562
- if (voice.state === 'done') {
563
- this.voices.splice(i, 1);
564
- if (this.opts.history)
565
- this.gone.push(voice);
566
- this.version++;
567
- if (voice.named === null)
568
- pruned = true;
569
- else
570
- this.unindex(voice);
571
- if (voice.spec.locus !== undefined)
572
- this.loci--;
573
- if (voice.spec.anchor !== undefined)
574
- this.anchored--;
575
- }
576
- }
577
- if (pruned)
578
- this.general = this.general.filter((v) => v.state !== 'done');
579
- const history = this.opts.history;
580
- if (history && this.gone.length > 0) {
581
- const reach = now - history.ms;
582
- this.gone = this.gone.filter((v) => v.doneAt >= reach);
583
- }
226
+ book(opts) {
227
+ return book(this, opts);
584
228
  }
585
229
  probe(subject, out) {
586
230
  const pose = this.fold(subject, out);
587
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));
588
235
  return pose;
589
236
  }
590
- /** After a probe, keeps the pose a retarget from `'current'` reads as the subject's last. */
591
- keep(subject, pose, out) {
592
- // Keeping last frame's pose is free when the mix allocated it; a host sampling into its own
593
- // object only pays for the copy once something in the mix has asked to retarget from it.
594
- let kept;
595
- if (out === undefined)
596
- kept = pose;
597
- else if (this.wantsPose) {
598
- kept = {};
599
- for (const key of this.names) {
600
- const v = pose[key];
601
- if (v !== undefined)
602
- kept[key] = copy(v);
603
- }
604
- }
605
- if (kept !== undefined) {
606
- const rec = this.pose.get(subject);
607
- if (rec === undefined)
608
- this.pose.set(subject, { pose: kept, at: this.now, prev: undefined, prevAt: Number.NaN });
609
- else if (rec.at === this.now)
610
- rec.pose = kept;
611
- else {
612
- rec.prev = rec.pose;
613
- rec.prevAt = rec.at;
614
- rec.pose = kept;
615
- rec.at = this.now;
616
- }
617
- }
618
- }
619
237
  pull(subjects, into) {
620
- const columns = this.columnsOf(into);
621
- const lanes = this.lanes;
622
- const now = this.now;
623
- if (this.scratch === undefined)
624
- this.scratch = {};
625
- const scratch = this.scratch;
626
- let room = Number.POSITIVE_INFINITY;
627
- let tightest = '';
628
- for (const c of columns) {
629
- const fits = Math.floor(c.out.length / c.axes);
630
- if (fits < room) {
631
- room = fits;
632
- tightest = c.key;
633
- }
634
- }
635
- let n = 0;
636
- for (const subject of subjects) {
637
- if (n >= room)
638
- throw new RangeError(`blits: pull's array for ${tightest} has room for ${room} subjects, and was given more`);
639
- const head = Number.isNaN(now) ? null : this.chain(subject, now);
640
- const laned = head !== null && lanes?.prepare(head.slot, subject, now, this.version) === true;
641
- if (laned && lanes.whole && !this.wantsPose) {
642
- lanes.write(head.slot, columns, n);
643
- }
644
- else {
645
- const folded = this.foldWith(subject, scratch, head, laned, false);
646
- const pose = (this.bounded.length === 0 ? folded : this.clamp(folded));
647
- this.keep(subject, pose, scratch);
648
- for (const c of columns)
649
- writeValue(c, pose[c.key], n);
650
- }
651
- n++;
652
- }
653
- }
654
- /** The kit slot, width, rest and bounds of every channel `pull` was handed an array for. */
655
- columnsOf(into) {
656
- const columns = [];
657
- for (const key of Object.keys(into)) {
658
- const out = into[key];
659
- if (out === undefined)
660
- continue;
661
- const slot = this.slotOf.get(key);
662
- if (slot === undefined)
663
- throw new Error(`blits: pull was handed ${key}, which the kit lacks`);
664
- const channel = this.channels[slot];
665
- const rest = channel.rest;
666
- const axes = Array.isArray(rest) ? rest.length : 1;
667
- const bounds = channel.bounds;
668
- const at = new Float64Array(axes).fill(Number.NaN);
669
- if (typeof rest === 'number')
670
- at[0] = rest;
671
- else if (Array.isArray(rest))
672
- at.set(rest);
673
- if (bounds !== undefined)
674
- clampRun(at, 0, axes, bounds);
675
- columns.push({ key, slot, axes, out, rest: at, bounds });
676
- }
677
- return columns;
238
+ pull(this, subjects, into);
678
239
  }
679
240
  project(timestamp) {
680
- const t = timestamp - this.offset;
681
241
  const c = new Mixer(this.kit, { ...this.opts, history: undefined, lanes: false });
682
- c.projecting = true;
683
- c.pose = this.pose;
684
- c.offset = this.offset;
685
- c.wantsPose = this.wantsPose;
686
- c.nextId = this.nextId;
687
- c.reducedNow = this.reducedNow;
688
- if (Number.isNaN(this.now) || t >= this.now) {
689
- c.now = this.now;
690
- c.voices = this.voices
691
- .filter((v) => v.state !== 'done')
692
- .map((v) => v.copy((subject) => this.carry(v, subject)));
693
- c.announced = this.announced.map((a) => ({ ...a }));
694
- c.count();
695
- c.move(t);
696
- }
697
- else {
698
- const history = this.opts.history;
699
- if (!history)
700
- throw new Error('blits: reading back needs a mix made with history');
701
- if (t < this.now - history.ms)
702
- throw new Error(`blits: ${timestamp} is older than this mix's history reaches`);
703
- c.now = t;
704
- c.backward = true;
705
- const was = last(this.hostLog, t, true);
706
- const host = this.opts.host;
707
- const then = was && typeof host === 'object' && host !== null
708
- ? Object.assign(Object.create(host), was.fields)
709
- : undefined;
710
- if (was)
711
- c.hostThen = was;
712
- c.announced = this.announced.filter((a) => a.made < t).map((a) => ({ ...a }));
713
- c.voices = [...this.voices, ...this.gone]
714
- .filter((v) => v.cuedAt <= t && v.doneAt > t)
715
- .sort((a, b) => a.id - b.id)
716
- .map((v) => {
717
- const log = v.log;
718
- const copy = v.copy((subject) => this.recall(v, subject, t), last(log, t, (e) => e.sync) ?? log[0]);
719
- copy.state = t < v.start ? 'pending' : copy.out && copy.out.at <= t ? 'fading' : 'live';
720
- if (then !== undefined)
721
- copy.setting.host = then;
722
- return copy;
723
- });
724
- c.count();
725
- }
726
- const read = (f) => {
727
- reading.live = false;
728
- try {
729
- return f();
730
- }
731
- finally {
732
- reading.live = true;
733
- }
734
- };
735
- return {
736
- timestamp,
737
- probe: (subject, out) => read(() => c.fold(subject, out)),
738
- assess: (subject) => read(() => {
739
- c.fold(subject);
740
- return c.doubts(subject);
741
- }),
742
- };
242
+ return project(this, c, timestamp);
743
243
  }
744
244
  announce(name, opts = {}) {
745
- const at = opts.at !== undefined ? opts.at - this.offset : this.now;
245
+ const at = opts.at !== undefined ? opts.at - this.offset : this.u;
746
246
  this.announced.push({
747
247
  name,
748
248
  score: opts.score,
@@ -753,41 +253,28 @@ class Mixer {
753
253
  });
754
254
  }
755
255
  marks(from, to) {
756
- const lo = from - this.offset;
757
- const hi = to - this.offset;
758
- const out = [];
759
- for (const a of this.announced)
760
- if (a.at >= lo && a.at <= hi)
761
- out.push({
762
- timestamp: a.at + this.offset,
763
- mark: undefined,
764
- voice: undefined,
765
- score: a.score,
766
- name: a.name,
767
- tags: a.tags,
768
- order: a.order,
769
- });
770
- for (const voice of [...this.voices, ...this.gone]) {
771
- for (const mark of ['start', 'in', 'out', 'end']) {
772
- const t = this.markOf(voice, mark);
773
- if (t === undefined || t < lo || t > hi)
774
- continue;
775
- out.push({
776
- timestamp: t + this.offset,
777
- mark,
778
- voice: voice.id,
779
- score: voice.spec.score,
780
- name: voice.spec.name,
781
- tags: voice.spec.tags ?? none,
782
- order: voice.id,
783
- });
784
- }
785
- }
786
- out.sort((a, b) => a.timestamp - b.timestamp || a.order - b.order);
787
- return out.map(({ order: _, ...m }) => m);
256
+ return listed(this, from, to).map(({ order: _, ...m }) => m);
788
257
  }
789
258
  atRest(subject) {
790
- 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) {
791
278
  for (let i = 0; i < this.names.length; i++) {
792
279
  const channel = this.channels[i];
793
280
  const value = pose[this.names[i]];
@@ -803,23 +290,119 @@ class Mixer {
803
290
  rebase() {
804
291
  this.rebasing = true;
805
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
+ }
806
300
  get live() {
807
- return this.voices.some((v) => v.state !== 'done');
301
+ return this.cued.some((v) => v.state !== 'done');
302
+ }
303
+ get inert() {
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();
335
+ }
336
+ /** Whether a voice will change no pose from `now` on, short of a change made to it. */
337
+ still(voice, now) {
338
+ if (voice.state === 'done')
339
+ return true;
340
+ if (voice.state !== 'held' && voice.state !== 'live')
341
+ return false;
342
+ if (typeof voice.spec.weight === 'function' || voice.parts !== null)
343
+ return false;
344
+ const fadeIn = this.reducedNow ? 0 : (voice.fade.in ?? 0);
345
+ if (fadeIn > 0 && now - this.sinceOf(voice, voice.latest) < fadeIn)
346
+ return false;
347
+ if (voice.state === 'held' || voice.holding !== null)
348
+ return true;
349
+ const motion = voice.motion;
350
+ return motion !== undefined && voice.elapsedAt(now) >= voice.latest && motion.landed();
808
351
  }
809
352
  mute(opts) {
810
- for (const voice of this.voices)
811
- this.beginFade(voice, { over: opts?.over });
353
+ for (const voice of this.cued)
354
+ beginFade(this, voice, { over: opts?.over });
812
355
  }
813
356
  drop(subject) {
357
+ this.frame = nextFrame();
814
358
  const head = this.chains.get(subject);
359
+ // Nothing that kept this head, such as `pull`'s remembered list, may take it as current again.
360
+ if (head !== undefined) {
361
+ head.version = Number.NaN;
362
+ this.relinks++;
363
+ }
815
364
  if (head !== undefined && head.slot >= 0 && this.lanes !== null)
816
365
  this.lanes.release(head.slot);
817
366
  this.pose.delete(subject);
367
+ this.restStamps?.delete(subject);
818
368
  this.chains.delete(subject);
819
- for (const voice of [...this.voices, ...this.gone]) {
369
+ this.stir();
370
+ // A record of it is only in a voice over every subject, one naming it, or one gone; a ramp out
371
+ // of a voice that does not name it is in `parters`, and a patch's own state in `motions`.
372
+ const forget = (voice) => {
820
373
  voice.subjects.delete(subject);
821
- motionOf(voice.patch)?.release(subject);
822
- }
374
+ voice.motion?.release(subject);
375
+ voice.parts?.delete(subject);
376
+ voice.parted?.delete(subject);
377
+ voice.blend?.of.reads.delete(subject);
378
+ };
379
+ for (const voice of this.general)
380
+ forget(voice);
381
+ const named = this.named.get(subject);
382
+ if (named !== undefined)
383
+ for (const voice of [...named])
384
+ forget(voice);
385
+ for (const voice of this.gone)
386
+ forget(voice);
387
+ if (this.owners !== null)
388
+ for (const voice of this.owners)
389
+ forget(voice);
390
+ for (const voice of this.parters) {
391
+ forget(voice);
392
+ if (voice.parts?.size === 0)
393
+ voice.parts = null;
394
+ if (voice.parted?.size === 0)
395
+ voice.parted = null;
396
+ if (voice.parts === null && voice.parted === null)
397
+ this.parters.delete(voice);
398
+ }
399
+ const strays = this.strays.get(subject);
400
+ if (strays !== undefined)
401
+ for (const motion of strays)
402
+ motion.release(subject);
403
+ this.strays.delete(subject);
404
+ for (const motion of this.strayAll)
405
+ motion.release(subject);
823
406
  }
824
407
  drain(tag) {
825
408
  const all = this.sent;
@@ -837,1138 +420,83 @@ class Mixer {
837
420
  }
838
421
  return out.sort((a, b) => a.timestamp - b.timestamp);
839
422
  }
840
- laneHost() {
841
- const mix = this;
842
- return {
843
- get voices() {
844
- return mix.voices;
845
- },
846
- channels: this.channels,
847
- names: this.names,
848
- fits: (voice) => mix.fits(voice),
849
- meet: (voice, subject) => mix.held(voice, subject, mix.now),
850
- naming: (subject) => (mix.naming === 0 ? undefined : mix.named.get(subject)),
851
- envelope: (voice, since) => mix.envelope(voice, mix.now, since),
852
- ready: (voice, subject, held, elapsed, pass, weight) => {
853
- mix.prime(voice, held, mix.now, elapsed, pass);
854
- voice.setting.weight = weight;
855
- mix.sending.voice = voice;
856
- mix.sending.subject = subject;
857
- },
858
- horizon: (voice, delay) => {
859
- reading.horizon = mix.horizonFor(voice, delay, mix.now);
860
- },
861
- keeps: this.opts.history !== undefined,
862
- after: (voice, held) => mix.remember(voice, held),
863
- kept: (voice) => mix.stateful(voice),
864
- };
865
- }
866
- /**
867
- * Whether a voice's patch and spec can run on a lane, its channels aside. A voice whose patch has
868
- * kept state on a record through `setting.keep` is stateful from then on, and leaves its lane;
869
- * one that starts keeping partway through may advance that state once for one subject not
870
- * probed on the frame it starts, which declaring `state` avoids.
871
- */
872
- fits(voice) {
873
- const spec = voice.spec;
874
- const patch = voice.patch;
875
- if (voice.keeping)
876
- return false;
877
- if (typeof spec.weight === 'function')
878
- return false;
879
- if (spec.locus !== undefined || spec.from === 'current')
880
- return false;
881
- if (voice.out?.rest)
882
- return false;
883
- if (patch.state !== undefined || patch.step !== undefined)
884
- return false;
885
- if (this.opts.history?.inputs && patch.reads !== undefined && patch.reads.length > 0)
886
- return false;
887
- const built = voice.built;
888
- return (built === null ||
889
- built.tracks.every((t, i) => t.lerp === undefined || t.lerp === voice.lerps[i]));
890
- }
891
- /** A voice's patch kept state on a record: it is stateful from now on. */
892
- stateful(voice) {
893
- if (voice.keeping)
894
- return;
895
- voice.keeping = true;
896
- this.lanes?.invalidate();
897
- }
898
- /**
899
- * What a motion patch asks for its subjects' voice time at the latest frame. It holds the mix and
900
- * the voice weakly, so a patch the host keeps does not keep a mix it has let go of alive.
901
- */
902
- static frameHook(mix, voice) {
903
- return (subject) => {
904
- const m = mix.deref();
905
- const v = voice.deref();
906
- return m === undefined || v === undefined ? Number.NaN : m.frameOf(v, subject);
907
- };
908
- }
909
- /**
910
- * A subject's voice time at the latest frame, where a motion patch places an untimed change and a
911
- * `read` with no time; NaN until the voice has started and met the subject, and while the
912
- * subject's own time is still short of its stagger. A retired voice's patch no longer asks.
913
- */
914
- frameOf(voice, subject) {
915
- if (Number.isNaN(this.now) || voice.state === 'pending')
916
- return Number.NaN;
917
- const held = voice.subjects.get(subject);
918
- if (held === undefined || !held.reaches)
919
- return Number.NaN;
920
- const t = voice.elapsedAt(this.now) - held.delay;
921
- return t < 0 ? Number.NaN : t;
922
- }
923
- /** Fills a voice's setting for a call to its patch, the weight aside. */
924
- prime(voice, held, now, elapsed, pass) {
925
- const setting = voice.setting;
926
- setting.timestamp = now;
927
- const gap = now - held.stepped;
928
- const cap = this.opts.maxDt;
929
- setting.dt = this.reducedNow
930
- ? Number.POSITIVE_INFINITY
931
- : cap !== undefined && gap > cap
932
- ? cap
933
- : gap;
934
- setting.elapsed = elapsed;
935
- setting.pass = pass;
936
- setting.weight = 0;
937
- setting.state = held.state;
938
- setting.keep = held.keep;
939
- }
940
- /** The earliest voice time a read may still ask for, for a subject delayed `delay`. */
941
- horizonFor(voice, delay, now) {
942
- const history = this.opts.history;
943
- return history === undefined
944
- ? Number.POSITIVE_INFINITY
945
- : this.elapsedThen(voice, now - history.ms) - delay;
946
- }
947
- // ── internals ──────────────────────────────────────────────────────────────
948
- /** Refuses a placement that names a mark twice on one side, or that waits on itself. */
949
- checkPlacement(spec, anchor) {
950
- if (anchor.start !== undefined && anchor.in !== undefined)
951
- throw new Error('blits: a placement anchors start or in, not both');
952
- if (anchor.out !== undefined && anchor.end !== undefined)
953
- throw new Error('blits: a placement anchors out or end, not both');
954
- if (spec.start !== undefined && (anchor.start !== undefined || anchor.in !== undefined))
955
- throw new Error('blits: a voice takes start or an anchored start, not both');
956
- const name = spec.name;
957
- if (name === undefined)
958
- return;
959
- // A voice is known by its score and its name; a bare name in an anchor is in the asker's score.
960
- const key = (score, n) => `${score ?? ''}\u0000${n}`;
961
- const names = (p, score) => [p.start, p.in, p.out, p.end].flatMap((a) => {
962
- if (a === undefined || typeof a === 'number')
963
- return [];
964
- const q = 'of' in a ? a.of : 'after' in a ? a.after : 'with' in a ? a.with : a.before;
965
- if (typeof q === 'string')
966
- return [key(score, q)];
967
- return q.name === undefined ? [] : [key(q.score ?? score, q.name)];
968
- });
969
- const self = key(spec.score, name);
970
- const seen = new Set();
971
- const waits = names(anchor, spec.score);
972
- while (waits.length > 0) {
973
- const n = waits.pop();
974
- if (n === self)
975
- throw new Error(`blits: ${name}'s placement waits on itself`);
976
- if (seen.has(n))
977
- continue;
978
- seen.add(n);
979
- for (const v of this.voices)
980
- if (v.spec.name !== undefined && key(v.spec.score, v.spec.name) === n && v.spec.anchor)
981
- waits.push(...names(v.spec.anchor, v.spec.score));
982
- }
983
- }
984
- /**
985
- * Fixes every anchored voice's start and out from what its anchors answer now. A start is fixed
986
- * while the voice waits and an out until its fade begins; a target that has left keeps the time it
987
- * last gave. Repeated so a chain of anchors settles in one sync.
988
- */
989
- place() {
990
- for (let round = 0; round <= this.voices.length; round++) {
991
- let moved = false;
992
- for (const voice of this.voices) {
993
- const anchor = voice.spec.anchor;
994
- if (anchor === undefined || voice.state === 'done')
995
- continue;
996
- if (voice.placing && voice.state === 'pending') {
997
- const by = anchor.start ?? anchor.in;
998
- const t = by === undefined ? undefined : this.resolve(by, voice);
999
- if (t !== undefined) {
1000
- const start = anchor.start !== undefined ? t : t - (voice.fade.in ?? 0);
1001
- if (start !== voice.start) {
1002
- voice.start = start;
1003
- voice.anchorNow = start;
1004
- voice.anchorElapsed = 0;
1005
- this.noted(voice);
1006
- moved = true;
1007
- }
1008
- }
1009
- }
1010
- if (voice.state !== 'fading') {
1011
- const by = anchor.out ?? anchor.end;
1012
- const t = by === undefined ? undefined : this.resolve(by, voice);
1013
- if (t !== undefined) {
1014
- const at = anchor.out !== undefined ? t : t - (voice.fade.out ?? 0);
1015
- if (at !== voice.outAt) {
1016
- voice.outAt = at;
1017
- this.noted(voice);
1018
- moved = true;
1019
- }
1020
- }
1021
- }
1022
- }
1023
- if (!moved)
1024
- return;
1025
- }
1026
- }
1027
- /** The mix time an anchor answers, or undefined while its target has none. */
1028
- resolve(a, self) {
1029
- if (typeof a === 'number')
1030
- return a - this.offset;
1031
- let query;
1032
- let mark;
1033
- let by = a.by ?? 0;
1034
- if ('of' in a) {
1035
- query = a.of;
1036
- mark = a.mark;
1037
- }
1038
- else if ('after' in a) {
1039
- query = a.after;
1040
- mark = 'end';
1041
- }
1042
- else if ('with' in a) {
1043
- query = a.with;
1044
- mark = 'start';
1045
- }
1046
- else {
1047
- query = a.before;
1048
- mark = 'start';
1049
- by = -by;
1050
- }
1051
- const t = this.timeOf(typeof query === 'string' ? { name: query } : query, mark, self);
1052
- return t === undefined ? undefined : t + by;
1053
- }
1054
- /**
1055
- * The time a query answers: `mark` of the voice it picks, among those cued and those that have
1056
- * left, never the asker; or a mark the host announced on the score, which is the same time for
1057
- * any of the four. Undefined while what it picks has no such time.
1058
- */
1059
- timeOf(q, mark, self) {
1060
- const score = q.score ?? self.spec.score;
1061
- const found = [];
1062
- for (const v of [...this.gone, ...this.voices])
1063
- if (v !== self &&
1064
- v.spec.score === score &&
1065
- (q.name === undefined || v.spec.name === q.name) &&
1066
- (q.tag === undefined || (v.spec.tags ?? none).includes(q.tag)) &&
1067
- (q.writes === undefined || v.patch.writes.includes(q.writes)))
1068
- found.push({ order: v.id, t: this.markOf(v, mark) });
1069
- if (q.writes === undefined)
1070
- for (const a of this.announced)
1071
- if (a.score === score &&
1072
- (q.name === undefined || a.name === q.name) &&
1073
- (q.tag === undefined || a.tags.includes(q.tag)))
1074
- found.push({ order: a.order, t: Number.isNaN(a.at) ? undefined : a.at });
1075
- if (found.length === 0)
1076
- return undefined;
1077
- found.sort((x, y) => x.order - y.order);
1078
- const resolver = q.resolver ?? 'last';
1079
- if (resolver === 'first')
1080
- return found[0]?.t;
1081
- if (resolver === 'last')
1082
- return found[found.length - 1]?.t;
1083
- const times = found.flatMap((e) => (e.t === undefined ? [] : [e.t])).sort((x, y) => x - y);
1084
- if (resolver === 'next') {
1085
- const now = Number.isNaN(this.now) ? Number.NEGATIVE_INFINITY : this.now;
1086
- return times.find((t) => t >= now);
1087
- }
1088
- return resolver === 'earliest' ? times[0] : times[times.length - 1];
1089
- }
1090
- /** When a voice reaches a mark, mix time, or undefined while nothing has fixed it. */
1091
- markOf(voice, mark) {
1092
- const start = voice.start;
1093
- if (!Number.isFinite(start))
1094
- return undefined;
1095
- if (mark === 'start')
1096
- return start;
1097
- if (mark === 'in')
1098
- return start + (voice.fade.in ?? 0);
1099
- const out = voice.out;
1100
- let outAt;
1101
- if (out !== null)
1102
- outAt = out.at;
1103
- else if (Number.isFinite(voice.outAt))
1104
- outAt = voice.outAt;
1105
- else {
1106
- const period = voice.patch.period;
1107
- const passes = voice.passes;
1108
- if (period > 0 && Number.isFinite(passes)) {
1109
- const t = voice.timeAt(period * passes + voice.latest);
1110
- if (Number.isFinite(t))
1111
- outAt = Math.max(start, t);
1112
- }
1113
- }
1114
- if (mark === 'out')
1115
- return voice.state === 'done' && outAt === undefined ? voice.doneAt : outAt;
1116
- if (voice.state === 'done')
1117
- return voice.doneAt;
1118
- if (out !== null) {
1119
- if (out.rest)
1120
- return out.deadline === undefined ? undefined : out.at + out.deadline;
1121
- return out.at + out.over;
1122
- }
1123
- return outAt === undefined ? undefined : outAt + (this.reduced ? 0 : (voice.fade.out ?? 0));
1124
- }
1125
- /** A voice's elapsed at an earlier mix time, by the controls it had then. */
1126
- elapsedThen(voice, t) {
1127
- const log = voice.log;
1128
- const c = log === null ? undefined : last(log, t, true);
1129
- return elapsedWith(c ?? voice, t);
1130
- }
1131
- /** Records a change to a voice's controls under `history`, and lets go of what it no longer reaches. */
1132
- noted(voice) {
1133
- const log = voice.log;
1134
- if (log === null)
1135
- return;
1136
- voice.note(Number.isNaN(this.now) ? Number.NEGATIVE_INFINITY : this.now, this.moving);
1137
- const reach = this.now - this.opts.history.ms;
1138
- let drop = 0;
1139
- while (drop + 1 < log.length && log[drop + 1].at <= reach)
1140
- drop++;
1141
- if (drop > 0)
1142
- log.splice(0, drop);
1143
- }
1144
- /** Recounts what the fold's shortcuts depend on, for a projection's freshly copied voices. */
1145
- count() {
1146
- this.named = new Store();
1147
- this.naming = 0;
1148
- this.general = [];
1149
- for (const voice of this.voices)
1150
- this.index(voice);
1151
- this.loci = this.voices.filter((v) => v.spec.locus !== undefined).length;
1152
- this.anchored = this.voices.filter((v) => v.spec.anchor !== undefined).length;
1153
- this.version++;
1154
- }
1155
- /** A copy of a subject's record that shares nothing a read could change. */
1156
- copyHeld(voice, h) {
1157
- const kept = new Map();
1158
- for (const [owner, value] of h.kept)
1159
- kept.set(owner, structuredClone(value));
1160
- const patch = voice.patch;
1161
- return {
1162
- ...h,
1163
- bands: h.bands.slice(),
1164
- state: h.state === undefined
1165
- ? undefined
1166
- : patch.clone
1167
- ? patch.clone(h.state)
1168
- : structuredClone(h.state),
1169
- kept,
1170
- keep: keeper(kept),
1171
- probed: Number.NaN,
1172
- delta: null,
1173
- phase: 0,
1174
- seeks: 0,
1175
- base: h.base === undefined ? undefined : structuredClone(h.base),
1176
- slope: h.slope === undefined ? undefined : structuredClone(h.slope),
1177
- snaps: undefined,
1178
- inputs: undefined,
1179
- next: null,
1180
- version: Number.NaN,
1181
- loci: null,
1182
- slot: -1,
1183
- };
1184
- }
1185
- /** A projection ahead starts each subject from where the live mix holds it. */
1186
- carry(voice, subject) {
1187
- const live = voice.subjects.get(subject);
1188
- if (live === undefined)
1189
- return undefined;
1190
- const h = this.copyHeld(voice, live);
1191
- h.from = h.stepped;
1192
- return h;
1193
- }
1194
- /**
1195
- * A projection back starts each subject from the latest copy kept at or before `t`, else from its
1196
- * voice's start with fresh state, which is exact for a voice stepped at a fixed interval.
1197
- */
1198
- recall(voice, subject, t) {
1199
- const live = voice.subjects.get(subject);
1200
- if (live === undefined)
1201
- return undefined;
1202
- const snap = live.snaps && last(live.snaps, t, true);
1203
- if (snap) {
1204
- const h = this.copyHeld(voice, snap.held);
1205
- h.from = h.stepped;
1206
- h.replay = live.inputs;
1207
- return h;
1208
- }
1209
- const kept = new Map();
1210
- const stepped = live.since < t ? live.since : t;
1211
- return {
1212
- reaches: live.reaches,
1213
- delay: live.delay,
1214
- since: live.since,
1215
- weight: 0,
1216
- rested: false,
1217
- bands: new Uint8Array(voice.slots.length),
1218
- state: live.reaches && voice.patch.state
1219
- ? voice.patch.state(subject)
1220
- : undefined,
1221
- stepped,
1222
- ticks: 0,
1223
- probed: Number.NaN,
1224
- delta: null,
1225
- phase: 0,
1226
- seeks: 0,
1227
- kept,
1228
- keep: keeper(kept),
1229
- voice,
1230
- next: null,
1231
- version: Number.NaN,
1232
- loci: null,
1233
- slot: -1,
1234
- from: stepped,
1235
- unknown: voice.spec.from === 'current',
1236
- replay: live.inputs,
1237
- };
1238
- }
1239
- /** Under `history` with `inputs`, copies the host fields any voice reads, once a frame, when they changed. */
1240
- recordHost(now) {
1241
- const log = this.hostLog;
1242
- const prev = log[log.length - 1];
1243
- if (prev !== undefined && prev.at === now)
1244
- return;
1245
- const host = this.opts.host;
1246
- const fields = {};
1247
- for (const v of this.voices)
1248
- for (const f of v.patch.reads ?? none)
1249
- if (!(f in fields))
1250
- fields[f] = structuredClone(host[f]);
1251
- if (prev !== undefined && same(prev.fields, fields))
1252
- return;
1253
- log.push({ at: now, fields });
1254
- const reach = now - this.opts.history.ms;
1255
- while (log.length > 1 && log[1].at <= reach)
1256
- log.shift();
1257
- }
1258
- /** Under `history` with `inputs`, keeps what an input signal read, each time it changes. */
1259
- record(held, value) {
1260
- const history = this.opts.history;
1261
- if (!history?.inputs || this.projecting)
1262
- return;
1263
- const inputs = held.inputs ?? [];
1264
- held.inputs = inputs;
1265
- const prev = inputs[inputs.length - 1];
1266
- if (prev !== undefined && (prev.value === value || prev.at === this.now)) {
1267
- if (prev.at === this.now)
1268
- prev.value = value;
1269
- return;
1270
- }
1271
- inputs.push({ at: this.now, value });
1272
- const reach = this.now - history.ms;
1273
- while (inputs.length > 1 && inputs[1].at <= reach)
1274
- inputs.shift();
1275
- }
1276
- /** Under `history`, keeps a copy of a stateful voice's record for this subject every so often. */
1277
- remember(voice, held) {
1278
- const history = this.opts.history;
1279
- if (!history || this.projecting)
1280
- return;
1281
- if (voice.patch.step === undefined && held.kept.size === 0 && held.base === undefined)
1282
- return;
1283
- const snaps = held.snaps ?? [];
1284
- held.snaps = snaps;
1285
- const prev = snaps[snaps.length - 1];
1286
- if (prev !== undefined && this.now - prev.at < (history.every ?? 200))
1287
- return;
1288
- snaps.push({ at: this.now, held: this.copyHeld(voice, held) });
1289
- const reach = this.now - history.ms;
1290
- while (snaps.length > 1 && snaps[1].at <= reach)
1291
- snaps.shift();
1292
- }
1293
- /** Per channel, the least sure voice that fed it this frame. */
1294
- doubts(subject) {
1295
- const out = {};
1296
- for (const name of this.names)
1297
- out[name] = 'exact';
1298
- const rank = { exact: 0, stepped: 1, held: 2 };
1299
- // A voice still waiting on an anchor nothing has answered may yet play, or stop, by now.
1300
- for (const voice of this.voices) {
1301
- const anchor = voice.spec.anchor;
1302
- if (anchor === undefined || voice.state === 'done')
1303
- continue;
1304
- const waiting = (voice.state === 'pending' && !Number.isFinite(voice.start)) ||
1305
- (voice.state !== 'pending' &&
1306
- voice.state !== 'fading' &&
1307
- (anchor.out !== undefined || anchor.end !== undefined) &&
1308
- !Number.isFinite(voice.outAt));
1309
- if (!waiting || !this.aims(voice, subject))
1310
- continue;
1311
- for (const slot of voice.slots)
1312
- out[this.names[slot]] = 'held';
1313
- }
1314
- for (let held = this.chain(subject, this.now); held !== null; held = held.next) {
1315
- const voice = held.voice;
1316
- if (voice === null || voice.state === 'done')
1317
- continue;
1318
- if (held.weight <= 0)
1319
- continue;
1320
- const d = this.doubtOf(voice, held);
1321
- for (const slot of voice.slots) {
1322
- const name = this.names[slot];
1323
- if (rank[d] > rank[out[name]])
1324
- out[name] = d;
1325
- }
1326
- }
1327
- return out;
1328
- }
1329
- doubtOf(voice, held) {
1330
- const weight = voice.spec.weight;
1331
- if (held.unknown)
1332
- return 'held';
1333
- // A weight read back from a recording is what the mix used then, so its signal's state is moot.
1334
- const replayed = held.replay !== undefined && last(held.replay, this.now, true) !== undefined;
1335
- if (typeof weight === 'function' && weight.input && !replayed)
1336
- return 'held';
1337
- const reads = voice.patch.reads;
1338
- if (reads !== undefined && reads.length > 0) {
1339
- const then = this.hostThen?.fields;
1340
- if (then === undefined || !reads.every((f) => f in then))
1341
- return 'held';
1342
- }
1343
- if (this.now > (held.from ?? this.now)) {
1344
- const tick = this.opts.stepMs;
1345
- const fixed = tick !== undefined && tick > 0 && !this.reducedNow;
1346
- if ((voice.patch.step && !fixed) || (held.kept.size > 0 && !replayed))
1347
- return 'stepped';
1348
- }
1349
- return 'exact';
1350
- }
1351
- handle(voice) {
1352
- const mix = this;
1353
- return {
1354
- id: voice.id,
1355
- get state() {
1356
- return voice.state;
1357
- },
1358
- get weight() {
1359
- return voice.weight;
1360
- },
1361
- set weight(w) {
1362
- voice.weight = w;
1363
- mix.noted(voice);
1364
- mix.lanes?.refill();
1365
- },
1366
- get rate() {
1367
- return voice.rateAt(Number.isNaN(mix.now) ? voice.start : mix.now);
1368
- },
1369
- set rate(r) {
1370
- voice.rebase(Number.isNaN(mix.now) ? voice.start : mix.now);
1371
- voice.ramp = null;
1372
- voice.rate = r;
1373
- mix.noted(voice);
1374
- mix.lanes?.refill();
1375
- },
1376
- ramp(r, over) {
1377
- const now = Number.isNaN(mix.now) ? voice.start : mix.now;
1378
- voice.rebase(now);
1379
- const from = voice.rateAt(now);
1380
- voice.ramp = over > 0 && r !== from ? { from, to: r, over } : null;
1381
- voice.rate = r;
1382
- mix.noted(voice);
1383
- mix.lanes?.refill();
1384
- },
1385
- seek(elapsed) {
1386
- voice.rebase(Number.isNaN(mix.now) ? voice.start : mix.now);
1387
- voice.anchorElapsed = elapsed;
1388
- voice.seeks++;
1389
- mix.noted(voice);
1390
- mix.lanes?.refill();
1391
- },
1392
- fade(opts) {
1393
- mix.beginFade(voice, opts ?? {});
1394
- },
1395
- weightOf(subject) {
1396
- if (voice.state === 'done')
1397
- return 0;
1398
- if (voice.laned && mix.lanes !== null) {
1399
- const w = mix.lanes.weightOf(voice.id, mix.chains.get(subject)?.slot ?? -1);
1400
- if (w !== undefined)
1401
- return w;
1402
- }
1403
- return voice.subjects.get(subject)?.weight ?? 0;
1404
- },
1405
- done: voice.done,
1406
- };
1407
- }
1408
- beginFade(voice, opts, at) {
1409
- if (voice.state === 'done' || voice.state === 'fading')
1410
- return;
1411
- const over = this.reduced ? 0 : (opts.over ?? voice.fade.out ?? 0);
1412
- // A voice is linked into its subjects' chains once it plays, and a fading one plays.
1413
- if (voice.state === 'pending')
1414
- this.version++;
1415
- voice.state = 'fading';
1416
- voice.out = {
1417
- from: 1,
1418
- at: at ?? (Number.isNaN(this.now) ? voice.start : this.now),
1419
- over,
1420
- rest: opts.at === 'rest',
1421
- deadline: opts.deadline,
1422
- };
1423
- this.noted(voice);
1424
- if (opts.at === 'rest')
1425
- this.lanes?.invalidate();
1426
- else
1427
- this.lanes?.refill();
1428
- if (over === 0 && opts.at !== 'rest')
1429
- this.retire(voice, voice.out.at);
1430
- }
1431
- /** Removes a voice, recording that it left at `at`, default now. */
1432
- retire(voice, at) {
1433
- this.lanes?.invalidate();
1434
- const motion = motionOf(voice.patch);
1435
- if (motion !== undefined && motion.frame === voice.frame)
1436
- motion.frame = noFrame;
1437
- voice.frame = null;
1438
- voice.state = 'done';
1439
- voice.doneAt = at ?? (Number.isNaN(this.now) ? Number.NEGATIVE_INFINITY : this.now);
1440
- voice.resolve();
1441
- }
1442
- /** The ramp a voice's own fade envelope applies this frame, 0..1. */
1443
- envelope(voice, now, since) {
1444
- return envelope(voice.fade.in ?? 0, voice.out, voice.ease, this.reducedNow, now, since);
1445
- }
1446
- /** What this voice holds for this subject, made on first sight with `target` and `stagger` asked once. */
1447
- held(voice, subject, now) {
1448
- let held = voice.subjects.get(subject);
1449
- if (held !== undefined)
1450
- return held;
1451
- const reaches = this.aims(voice, subject);
1452
- const delay = reaches && voice.spec.stagger ? voice.spec.stagger(subject) : 0;
1453
- // When the voice clock reads `delay`, from where it is anchored now; during a ramp this assumes
1454
- // the rate it is ramping to.
1455
- const since = voice.rate > 0
1456
- ? voice.anchorNow + (delay - voice.anchorElapsed) / voice.rate
1457
- : voice.start + delay;
1458
- const kept = new Map();
1459
- held = {
1460
- reaches,
1461
- delay,
1462
- since,
1463
- weight: 0,
1464
- rested: false,
1465
- bands: new Uint8Array(voice.slots.length),
1466
- state: reaches && voice.patch.state
1467
- ? voice.patch.state(subject)
1468
- : undefined,
1469
- stepped: this.backward && since < now ? since : now,
1470
- ticks: 0,
1471
- probed: Number.NaN,
1472
- delta: null,
1473
- phase: 0,
1474
- seeks: 0,
1475
- kept,
1476
- keep: keeper(kept),
1477
- voice,
1478
- next: null,
1479
- version: Number.NaN,
1480
- loci: null,
1481
- slot: -1,
1482
- };
1483
- if (this.projecting) {
1484
- held.from = held.stepped;
1485
- held.unknown = this.backward && voice.spec.from === 'current';
1486
- }
1487
- voice.subjects.set(subject, held);
1488
- if (reaches)
1489
- voice.seen++;
1490
- if (delay > voice.latest)
1491
- voice.latest = delay;
1492
- return held;
1493
- }
1494
- /** The weight a voice gives a subject this frame, with the setting already filled in. */
1495
- weigh(voice, subject, now, held) {
1496
- this.sending.voice = voice;
1497
- this.sending.subject = subject;
1498
- const signal = typeof voice.spec.weight === 'function' ? voice.spec.weight : null;
1499
- let base = voice.weight;
1500
- if (signal) {
1501
- const was = held.replay && last(held.replay, now, true);
1502
- base = was ? was.value : signal(subject, voice.setting);
1503
- if (signal.input && !was)
1504
- this.record(held, base);
1505
- }
1506
- return clampWeight(base * this.envelope(voice, now, held.since));
1507
- }
1508
- /**
1509
- * One live voice's delta for one subject this frame, through its record for the subject, or null
1510
- * when it does not reach. Its weight is left in `w`.
1511
- */
1512
- influence(voice, subject, now, held) {
1513
- held.weight = 0;
1514
- if (!held.reaches)
1515
- return null;
1516
- if (voice.out?.rest && held.rested)
1517
- return null;
1518
- const elapsed = voice.elapsedAt(now) - held.delay;
1519
- if (elapsed < 0)
1520
- return null;
1521
- const period = voice.patch.period;
1522
- const phase = phaseAt(elapsed, period, voice.passes);
1523
- const pass = passAt(elapsed, period, voice.passes);
1524
- this.prime(voice, held, now, elapsed, pass);
1525
- const setting = voice.setting;
1526
- const weight = this.weigh(voice, subject, now, held);
1527
- setting.weight = weight;
1528
- held.weight = weight;
1529
- if (held.probed === now && held.delta && held.seeks === voice.seeks) {
1530
- if (voice.holder !== held && voice.scratch.length > 0)
1531
- this.keyed(voice, subject, held);
1532
- this.w = weight;
1533
- return held.delta;
1534
- }
1535
- const keptBefore = reading.kept;
1536
- const history = this.opts.history;
1537
- if (history?.inputs && voice.patch.reads !== undefined && !this.projecting)
1538
- this.recordHost(now);
1539
- reading.horizon = this.horizonFor(voice, held.delay, now);
1540
- const tick = this.opts.stepMs;
1541
- if (voice.patch.step && held.probed !== now) {
1542
- if (tick !== undefined && tick > 0 && !this.reducedNow)
1543
- this.tick(voice, subject, held, tick);
1544
- else if (now !== held.stepped) {
1545
- voice.patch.step(held.state, setting.dt, subject, setting);
1546
- held.stepped = now;
1547
- }
1548
- }
1549
- let delta;
1550
- if (voice.built) {
1551
- held.phase = phase;
1552
- delta = this.keyed(voice, subject, held);
1553
- }
1554
- else {
1555
- delta = voice.patch.at(phase, subject, setting);
1556
- }
1557
- held.delta = delta;
1558
- held.probed = now;
1559
- held.seeks = voice.seeks;
1560
- if (history !== undefined)
1561
- this.remember(voice, held);
1562
- if (reading.kept !== keptBefore && !voice.keeping)
1563
- this.stateful(voice);
1564
- if (voice.out?.rest && this.isRest(delta)) {
1565
- held.weight = 0;
1566
- held.rested = true;
1567
- voice.restedCount++;
1568
- return null;
1569
- }
1570
- this.w = weight;
1571
- return delta;
1572
- }
1573
- /** Reads a keys voice's stops at `held.phase` into the subject's delta, through the voice's scratch. */
1574
- keyed(voice, subject, held) {
1575
- let base;
1576
- if (voice.spec.from === 'current') {
1577
- if (held.base === undefined) {
1578
- held.base = this.baseFor(voice, subject);
1579
- held.slope = this.slopeFor(voice, subject);
1580
- }
1581
- base = held.base;
1582
- }
1583
- const delta = readKeys(voice.built, held.phase, held.delta ?? {}, base, voice.lerps, held.slope, voice.intos, voice.scratch);
1584
- held.delta = delta;
1585
- if (voice.scratch.length > 0)
1586
- voice.holder = held;
1587
- return delta;
1588
- }
1589
- /**
1590
- * Runs `step` once per whole interval between where this subject last stopped and now, on a grid
1591
- * counted from when its delay ran out, and leaves the remainder for the next sample. Counting
1592
- * rather than adding keeps the grid where it is however many samples it is reached through.
1593
- */
1594
- tick(voice, subject, held, tick) {
1595
- const now = this.now;
1596
- const setting = voice.setting;
1597
- const frameDt = setting.dt;
1598
- let n = held.ticks;
1599
- // The epsilon keeps an interval like 1000 / 120 from landing a hair short of a whole count.
1600
- const due = Math.floor((now - held.since) / tick + 1e-9);
1601
- const cap = this.opts.maxDt;
1602
- if (cap !== undefined) {
1603
- const most = Math.floor(cap / tick);
1604
- if (due - n > most)
1605
- n = due - most;
1606
- }
1607
- const step = voice.patch.step;
1608
- setting.dt = tick;
1609
- for (n++; n <= due; n++) {
1610
- setting.timestamp = held.since + n * tick;
1611
- step(held.state, tick, subject, setting);
1612
- }
1613
- held.ticks = due > held.ticks ? due : held.ticks;
1614
- held.stepped = held.since + held.ticks * tick;
1615
- setting.timestamp = now;
1616
- setting.dt = frameDt;
1617
- }
1618
- /**
1619
- * Stop 0 for a retargeting voice: the subject's pose in the frame before this voice contributes.
1620
- * With no such frame on record — a voice cued before the subject was ever probed, or a host that
1621
- * samples into its own object — it is this frame's pose with every other voice folded in.
1622
- */
1623
- baseFor(voice, subject) {
1624
- let prior = this.pose.get(subject)?.pose;
1625
- if (prior === undefined && !this.folding.has(voice.id)) {
1626
- this.folding.add(voice.id);
1627
- try {
1628
- prior = this.fold(subject, undefined, false, voice.id);
1629
- }
1630
- finally {
1631
- this.folding.delete(voice.id);
1632
- }
1633
- }
1634
- const base = {};
1635
- for (const key of voice.patch.writes) {
1636
- const channel = this.kit[key];
1637
- const value = prior?.[key];
1638
- const fallback = channel.rest;
1639
- if (value !== undefined)
1640
- base[key] = copy(value);
1641
- else if (fallback !== undefined)
1642
- base[key] = copy(fallback);
1643
- }
1644
- return base;
1645
- }
1646
- /**
1647
- * How fast each numeric channel this voice writes was moving, from the subject's last two probed
1648
- * poses, in units per ms. Undefined with fewer than two on record, which starts the voice at rest.
1649
- */
1650
- slopeFor(voice, subject) {
1651
- const rec = this.pose.get(subject);
1652
- if (rec?.prev === undefined || !(rec.at > rec.prevAt))
1653
- return undefined;
1654
- const dt = rec.at - rec.prevAt;
1655
- const now = rec.pose;
1656
- const before = rec.prev;
1657
- const slope = {};
1658
- for (const key of voice.patch.writes) {
1659
- const a = before[key];
1660
- const b = now[key];
1661
- if (typeof a === 'number' && typeof b === 'number')
1662
- slope[key] = (b - a) / dt;
1663
- else if (Array.isArray(a) && Array.isArray(b) && a.length === b.length)
1664
- slope[key] = b.map((v, i) => (v - a[i]) / dt);
1665
- }
1666
- return slope;
1667
- }
1668
- isRest(delta) {
1669
- for (const key of Object.keys(delta)) {
1670
- if (delta[key] === undefined)
1671
- continue;
1672
- const channel = this.kit[key];
1673
- if (channel.rest === undefined)
1674
- return false;
1675
- if (!near(delta[key], channel.rest))
1676
- return false;
1677
- }
1678
- return true;
1679
- }
1680
- /** Folds a locus's members into one influence through each channel's own `lerp`. */
1681
- foldLocus(members) {
1682
- let sum = 0;
1683
- for (const m of members)
1684
- sum += m.weight;
1685
- const delta = {};
1686
- if (sum <= 0)
1687
- return { delta, weight: 0 };
1688
- const taken = new Map();
1689
- for (const member of members) {
1690
- if (member.weight <= 0)
1691
- continue;
1692
- for (const key of Object.keys(member.delta)) {
1693
- if (member.delta[key] === undefined)
1694
- continue;
1695
- const channel = this.kit[key];
1696
- const held = taken.get(key);
1697
- if (held === undefined) {
1698
- delta[key] = copy(member.delta[key]);
1699
- taken.set(key, member.weight);
1700
- continue;
1701
- }
1702
- const total = held + member.weight;
1703
- delta[key] = channel.lerp(delta[key], member.delta[key], member.weight / total);
1704
- taken.set(key, total);
1705
- }
1706
- }
1707
- return { delta, weight: sum > 1 ? 1 : sum };
1708
- }
1709
- /** Whether a rest-less channel's influence is switched on, across a band rather than an edge. */
1710
- passes(was, w) {
1711
- const { on, off } = this.band;
1712
- return w >= on ? true : w <= off ? false : (was ?? w >= on);
1713
- }
1714
- /**
1715
- * The first of this subject's records, linked through every live voice that reaches it. A voice
1716
- * is asked once, through the record it keeps for the subject anyway; a pending one is linked when
1717
- * it goes live, since `target` is asked on first sight, and sight only comes once a voice plays.
1718
- */
1719
- chain(subject, now) {
1720
- const was = this.chains.get(subject);
1721
- if (was !== undefined && was.version === this.version)
1722
- return was;
1723
- let first = null;
1724
- let prev = null;
1725
- const link = (voice) => {
1726
- if (voice.state !== 'live' && voice.state !== 'fading')
1727
- return;
1728
- const held = this.held(voice, subject, now);
1729
- if (!held.reaches)
1730
- return;
1731
- held.voice = voice;
1732
- held.next = null;
1733
- if (prev === null)
1734
- first = held;
1735
- else
1736
- prev.next = held;
1737
- prev = held;
1738
- };
1739
- const named = this.naming === 0 ? undefined : this.named.get(subject);
1740
- let j = 0;
1741
- for (const voice of this.general) {
1742
- while (named !== undefined && j < named.length && named[j].id < voice.id)
1743
- link(named[j++]);
1744
- link(voice);
1745
- }
1746
- while (named !== undefined && j < named.length)
1747
- link(named[j++]);
1748
- const head = first ?? stub();
1749
- head.version = this.version;
1750
- if (was !== undefined && was !== head) {
1751
- head.loci = was.loci;
1752
- was.loci = null;
1753
- head.slot = was.slot;
1754
- was.slot = -1;
1755
- }
1756
- else if (was === undefined && this.lanes !== null && head.slot < 0)
1757
- head.slot = this.lanes.number(subject);
1758
- if (was !== head)
1759
- this.chains.set(subject, head);
1760
- return head;
1761
- }
1762
- /** Whether a voice reaches a subject by what its spec says: the subjects it names, or its `target`. */
1763
- aims(voice, subject) {
1764
- if (voice.named !== null)
1765
- return voice.named.has(subject);
1766
- return voice.spec.target ? voice.spec.target(subject) : true;
1767
- }
1768
- /** Files a voice under each subject it names, or among the voices that name none. */
1769
- index(voice) {
1770
- if (voice.named === null) {
1771
- this.general.push(voice);
1772
- return;
1773
- }
1774
- this.naming++;
1775
- for (const subject of voice.named) {
1776
- const list = this.named.get(subject);
1777
- if (list === undefined)
1778
- this.named.set(subject, [voice]);
1779
- else
1780
- list.push(voice);
1781
- }
1782
- }
1783
- unindex(voice) {
1784
- this.naming--;
1785
- for (const subject of voice.named) {
1786
- const list = this.named.get(subject);
1787
- if (list === undefined)
1788
- continue;
1789
- const i = list.indexOf(voice);
1790
- if (i >= 0)
1791
- list.splice(i, 1);
1792
- if (list.length === 0)
1793
- this.named.delete(subject);
1794
- }
1795
- }
1796
423
  /** Voices whose stop 0 is mid-computation, so a fold for one cannot re-enter itself. */
1797
424
  folding = new Set();
1798
- /** Folds one voice's delta into the pose, its band state kept on the subject's record. */
1799
- apply(pose, voice, held, delta, weight) {
1800
- const slots = voice.slots;
1801
- for (let i = 0; i < slots.length; i++) {
1802
- const slot = slots[i];
1803
- const key = this.names[slot];
1804
- const value = delta[key];
1805
- if (value === undefined)
1806
- continue;
1807
- const channel = this.channels[slot];
1808
- if (channel.rest !== undefined && channel.scale) {
1809
- // pose[key] is the copy of rest this fold made, so it is ours to write into.
1810
- pose[key] = channel.fold
1811
- ? channel.fold(pose[key], value, weight)
1812
- : channel.merge(pose[key], channel.scale(value, weight));
1813
- continue;
1814
- }
1815
- const band = held.bands[i];
1816
- const on = this.passes(band === 0 ? undefined : band === 1, weight);
1817
- held.bands[i] = on ? 1 : 2;
1818
- if (on)
1819
- pose[key] = pose[key] === undefined ? copy(value) : channel.merge(pose[key], value);
1820
- }
1821
- }
1822
- /** Clamps every bounded channel of a folded pose into its range, in place. */
1823
- clamp(pose) {
1824
- for (const slot of this.bounded) {
1825
- const [lo, hi] = this.channels[slot].bounds;
1826
- const key = this.names[slot];
1827
- const v = pose[key];
1828
- if (typeof v === 'number')
1829
- pose[key] = v < lo ? lo : v > hi ? hi : v;
1830
- else if (Array.isArray(v))
1831
- for (let i = 0; i < v.length; i++) {
1832
- const x = v[i];
1833
- v[i] = x < lo ? lo : x > hi ? hi : x;
1834
- }
1835
- }
1836
- return pose;
1837
- }
1838
- fold(subject, out, dry = false, except) {
1839
- const pose = this.folded(subject, out, dry, except);
1840
- return (this.bounded.length === 0 ? pose : this.clamp(pose));
1841
- }
1842
- folded(subject, out, dry = false, except) {
1843
- const now = this.now;
1844
- const head = Number.isNaN(now) ? null : this.chain(subject, now);
1845
- // A subject numbered since the frame's fill folds every voice here, laned ones included.
1846
- const laned = head !== null && this.lanes?.prepare(head.slot, subject, now, this.version) === true;
1847
- return this.foldWith(subject, out ?? {}, head, laned, dry, except);
1848
- }
1849
- /** The fold after a subject's chain is linked and its lanes asked whether it reads from them. */
1850
- foldWith(subject, out, head, laned, dry, except) {
1851
- const now = this.now;
1852
- const pose = out;
1853
- const lanes = this.lanes;
1854
- const skip = laned ? lanes.copies : null;
1855
- for (let i = 0; i < this.names.length; i++) {
1856
- if (skip?.[i])
1857
- continue;
1858
- const rest = this.channels[i].rest;
1859
- const key = this.names[i];
1860
- // Never `delete`: it drops a reused out object into dictionary mode for good.
1861
- if (rest !== undefined)
1862
- pose[key] = copy(rest);
1863
- else if (pose[key] !== undefined)
1864
- pose[key] = undefined;
1865
- }
1866
- if (head === null)
1867
- return pose;
1868
- if (laned) {
1869
- lanes.copy(head.slot, pose);
1870
- if (lanes.whole)
1871
- return pose;
1872
- }
1873
- if (this.loci === 0) {
1874
- for (let held = head; held !== null; held = held.next) {
1875
- const voice = held.voice;
1876
- if (voice === null || (laned && voice.laned))
1877
- continue;
1878
- if (voice.id === except || voice.state === 'done')
1879
- continue;
1880
- const delta = this.read(voice, subject, now, dry, held);
1881
- if (delta === null || this.w <= 0)
1882
- continue;
1883
- this.apply(pose, voice, held, delta, this.w);
1884
- }
1885
- return pose;
1886
- }
1887
- const order = [];
1888
- const weights = [];
1889
- const loci = new Map();
1890
- for (let held = head; held !== null; held = held.next) {
1891
- const voice = held.voice;
1892
- if (voice === null || (laned && voice.laned))
1893
- continue;
1894
- if (voice.id === except || voice.state === 'done')
1895
- continue;
1896
- const delta = this.read(voice, subject, now, dry, held);
1897
- if (delta === null)
1898
- continue;
1899
- const locus = voice.spec.locus;
1900
- if (locus === undefined) {
1901
- order.push({ voice, held, delta });
1902
- weights.push(this.w);
1903
- continue;
1904
- }
1905
- const members = loci.get(locus);
1906
- if (members)
1907
- members.push({ delta, weight: this.w });
1908
- else {
1909
- loci.set(locus, [{ delta, weight: this.w }]);
1910
- order.push(locus);
1911
- weights.push(0);
1912
- }
1913
- }
1914
- let bands = head.loci;
1915
- if (bands === null) {
1916
- bands = new Map();
1917
- head.loci = bands;
1918
- }
1919
- for (let n = 0; n < order.length; n++) {
1920
- const at = order[n];
1921
- if (typeof at !== 'string') {
1922
- if (weights[n] > 0)
1923
- this.apply(pose, at.voice, at.held, at.delta, weights[n]);
1924
- continue;
1925
- }
1926
- const { delta, weight } = this.foldLocus(loci.get(at));
1927
- if (weight <= 0)
1928
- continue;
1929
- for (const key of Object.keys(delta)) {
1930
- const channel = this.kit[key];
1931
- const value = delta[key];
1932
- if (value === undefined)
1933
- continue;
1934
- if (channel.rest !== undefined && channel.scale) {
1935
- // pose[key] is the copy of rest this fold made, so it is ours to write into.
1936
- pose[key] = channel.fold
1937
- ? channel.fold(pose[key], value, weight)
1938
- : channel.merge(pose[key], channel.scale(value, weight));
1939
- continue;
1940
- }
1941
- const band = `${at}:${key}`;
1942
- const on = this.passes(bands.get(band), weight);
1943
- bands.set(band, on);
1944
- if (on)
1945
- pose[key] = pose[key] === undefined ? copy(value) : channel.merge(pose[key], value);
1946
- }
1947
- }
1948
- return pose;
1949
- }
1950
- /** `influence`, or for a dry fold a subject already probed this frame, without advancing it. */
1951
- read(voice, subject, now, dry, held) {
1952
- if (dry) {
1953
- if (held.reaches && held.probed === now && held.delta && held.seeks === voice.seeks) {
1954
- const setting = voice.setting;
1955
- setting.timestamp = now;
1956
- setting.dt = 0;
1957
- setting.elapsed = voice.elapsedAt(now);
1958
- setting.pass = 0;
1959
- setting.weight = 0;
1960
- setting.state = held.state;
1961
- setting.keep = held.keep;
1962
- this.w = this.weigh(voice, subject, now, held);
1963
- held.weight = this.w;
1964
- if (voice.holder !== held && voice.scratch.length > 0)
1965
- this.keyed(voice, subject, held);
1966
- return held.delta;
1967
- }
1968
- }
1969
- 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;
1970
454
  }
1971
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 });
1972
500
  /**
1973
501
  * The one that ships: every form on the CPU, probed per subject on demand.
1974
502
  *