@msb235/blits 0.2.1 → 0.3.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.
package/dist/mixer.js CHANGED
@@ -1,5 +1,9 @@
1
+ import { clampWeight, envelope, passAt, passesOf, phaseAt } from './clock.js';
1
2
  import { curve } from './easing.js';
2
- import { builtOf, readKeys } from './patch.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';
3
7
  import { Store } from './store.js';
4
8
  const none = Object.freeze([]);
5
9
  const keeper = (kept) => function keep(owner, init) {
@@ -7,8 +11,86 @@ const keeper = (kept) => function keep(owner, init) {
7
11
  return kept.get(owner);
8
12
  const made = init();
9
13
  kept.set(owner, made);
14
+ reading.kept++;
10
15
  return made;
11
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 = () => { };
12
94
  const near = (a, b) => {
13
95
  if (typeof a === 'number' && typeof b === 'number')
14
96
  return Math.abs(a - b) < 1e-9;
@@ -17,7 +99,35 @@ const near = (a, b) => {
17
99
  return a === b;
18
100
  };
19
101
  const copy = (v) => (Array.isArray(v) ? [...v] : v);
20
- class Voice {
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 {
21
131
  id;
22
132
  spec;
23
133
  patch;
@@ -36,13 +146,34 @@ class Voice {
36
146
  ramp = null;
37
147
  out = null;
38
148
  subjects = new Store();
149
+ /** The subjects its spec names, or null where it names none. */
150
+ named;
39
151
  /** Kit slot of each channel the patch writes, in `writes` order. */
40
152
  slots;
41
153
  /** The stops built ahead of time, for a `keys` patch. */
42
154
  built;
43
155
  /** The kit's `lerp` for each channel the patch writes, which keyed stops interpolate through. */
44
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;
45
174
  ease;
175
+ /** How many passes its `loop` plays, worked out once. */
176
+ passes;
46
177
  /** Reused for every call this voice makes, so it is valid only during the call. */
47
178
  setting;
48
179
  /** Every subject this voice has been asked about, so a handover knows when it is finished. */
@@ -50,6 +181,16 @@ class Voice {
50
181
  /** The longest stagger of any subject seen, so a finite loop waits for the last of them. */
51
182
  latest = 0;
52
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;
53
194
  resolve;
54
195
  done;
55
196
  constructor(id, spec, patch, fade, now, start, slotOf, channels, host, send) {
@@ -59,9 +200,14 @@ class Voice {
59
200
  this.fade = fade;
60
201
  this.start = start;
61
202
  this.slots = patch.writes.map((k) => slotOf.get(k));
203
+ this.named = spec.subjects ? new Set(spec.subjects) : null;
62
204
  this.built = patch.form === 'keys' && patch.keys ? builtOf(patch) : null;
63
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;
64
209
  this.ease = fade.ease === undefined ? undefined : curve(fade.ease);
210
+ this.passes = passesOf(spec.loop);
65
211
  this.setting = {
66
212
  timestamp: 0,
67
213
  dt: 0,
@@ -83,16 +229,72 @@ class Voice {
83
229
  this.state = 'live';
84
230
  }
85
231
  elapsedAt(now) {
86
- const dt = now - this.anchorNow;
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;
87
237
  const r = this.ramp;
88
- if (r === null)
89
- return this.anchorElapsed + dt * this.rate;
90
- if (dt <= 0)
91
- return this.anchorElapsed + dt * r.from;
92
- const d = r.to - r.from;
93
- if (dt <= r.over)
94
- return this.anchorElapsed + r.from * dt + (d * dt * dt) / (2 * r.over);
95
- return this.anchorElapsed + r.from * r.over + (d * r.over) / 2 + r.to * (dt - r.over);
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;
96
298
  }
97
299
  rateAt(now) {
98
300
  const r = this.ramp;
@@ -117,6 +319,25 @@ class Mixer {
117
319
  kit;
118
320
  opts;
119
321
  voices = [];
322
+ /** Under `history`, voices that have left but that a read back may still reach. */
323
+ gone = [];
324
+ /** Set on a projection's own mixer: it sends nothing, keeps no history, and reads without committing. */
325
+ projecting = false;
326
+ /** Set on a projection reading back: a subject it has nothing on starts from its voice's start. */
327
+ backward = false;
328
+ /** True during a sync, so a control change it makes is recorded as showing in that frame. */
329
+ moving = false;
330
+ /**
331
+ * Under `history` with `inputs`, the host fields patches read, copied each frame they changed. In
332
+ * a projection reading back, the copy in force then.
333
+ */
334
+ hostLog = [];
335
+ /**
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.
338
+ */
339
+ announced = [];
340
+ hostThen;
120
341
  /** Each subject's last two poses and when they were probed, for `from: 'current'`. */
121
342
  pose = new Store();
122
343
  nextId = 1;
@@ -126,19 +347,27 @@ class Mixer {
126
347
  last = Number.NaN;
127
348
  rebasing = false;
128
349
  wantsPose = false;
350
+ /** The pose `pull` folds a subject into where it cannot read straight from the lanes. */
351
+ scratch;
129
352
  /** Read once a sync, so a `reduce` function is not called per voice per subject. */
130
353
  reducedNow = false;
131
354
  /**
132
- * Per subject, the voices that may reach it, in voice order, so a probe walks those rather than
133
- * every voice: a voice targeted at one subject is asked about that subject alone. Rebuilt when
134
- * `version` moves, which is whenever the list or a voice's pending state changes.
355
+ * Per subject, the first record of the chain through every live voice that reaches it, or a stub
356
+ * where none does. Relinked when `version` moves, which is whenever the list or a voice's pending
357
+ * state changes.
135
358
  */
136
- reach = new Store();
359
+ chains = new Store();
137
360
  version = 0;
138
- /** How many voices in the list carry a `target`; with none, every voice reaches every subject. */
139
- targeted = 0;
361
+ /** Per subject, the voices whose `subjects` name it, in voice order. */
362
+ named = new Store();
363
+ /** How many voices in the list carry `subjects`. */
364
+ naming = 0;
365
+ /** The voices in the list that name no subjects, in voice order. */
366
+ general = [];
140
367
  /** How many voices in the list carry a locus; with none, a fold allocates nothing. */
141
368
  loci = 0;
369
+ /** How many voices in the list are anchored, so a sync with none skips placing them. */
370
+ anchored = 0;
142
371
  /** Events sent since the last drain, and who is being probed, so `send` knows whose they are. */
143
372
  sent = [];
144
373
  sending = {
@@ -147,7 +376,7 @@ class Mixer {
147
376
  };
148
377
  send = (event) => {
149
378
  const { voice, subject } = this.sending;
150
- if (voice === null)
379
+ if (voice === null || this.projecting)
151
380
  return;
152
381
  this.sent.push({
153
382
  timestamp: voice.setting.timestamp,
@@ -157,14 +386,14 @@ class Mixer {
157
386
  event,
158
387
  });
159
388
  };
160
- /** What `influence` and `peek` found besides the delta, read by the caller at once. */
389
+ /** The weight `influence` found besides the delta, read by the caller at once. */
161
390
  w = 0;
162
- h = null;
163
391
  names;
164
392
  channels;
165
393
  /** Slots of the channels that declare bounds, clamped after every fold. */
166
394
  bounded;
167
395
  slotOf = new Map();
396
+ lanes;
168
397
  constructor(kit, opts) {
169
398
  this.kit = kit;
170
399
  this.opts = opts;
@@ -174,6 +403,7 @@ class Mixer {
174
403
  this.names.forEach((k, i) => {
175
404
  this.slotOf.set(k, i);
176
405
  });
406
+ this.lanes = opts.lanes === false ? null : new Lanes(this.laneHost());
177
407
  }
178
408
  get reduced() {
179
409
  const r = this.opts.reduce;
@@ -203,16 +433,48 @@ class Mixer {
203
433
  }
204
434
  if (spec.from === 'current' && patch.form !== 'keys')
205
435
  throw new Error("blits: from: 'current' needs a keys patch");
206
- const start = spec.start !== undefined ? spec.start - this.offset : Number.isNaN(this.now) ? 0 : this.now;
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;
207
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
+ }
208
458
  this.voices.push(voice);
459
+ this.index(voice);
209
460
  this.version++;
210
- if (spec.target !== undefined)
211
- this.targeted++;
212
461
  if (spec.locus !== undefined)
213
462
  this.loci++;
214
463
  if (spec.from === 'current')
215
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
+ }
216
478
  return this.handle(voice);
217
479
  }
218
480
  blend(patches, by, spec = {}) {
@@ -236,21 +498,48 @@ class Mixer {
236
498
  const now = timestamp - this.offset;
237
499
  if (now === this.now)
238
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;
511
+ }
512
+ }
513
+ moveTo(now) {
239
514
  this.now = now;
240
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
+ }
241
526
  for (const voice of this.voices) {
242
527
  if (voice.state === 'done')
243
528
  continue;
529
+ if (voice.state === 'live' && voice.outAt <= now)
530
+ this.beginFade(voice, {}, Math.max(voice.start, voice.outAt));
244
531
  if (voice.state === 'pending' && now >= voice.start) {
245
532
  voice.state = 'live';
246
533
  this.version++;
247
534
  }
248
535
  const period = voice.patch.period;
249
- const loop = voice.spec.loop ?? true;
250
- const passes = loop === true ? Number.POSITIVE_INFINITY : loop === false ? 1 : loop;
536
+ const passes = voice.passes;
251
537
  if (voice.state === 'live' && period > 0 && Number.isFinite(passes)) {
252
- if (voice.elapsedAt(now) >= period * passes + voice.latest)
253
- this.beginFade(voice, {});
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))));
254
543
  }
255
544
  if (voice.state === 'fading' && voice.out) {
256
545
  const { at, over, rest, deadline } = voice.out;
@@ -258,27 +547,48 @@ class Mixer {
258
547
  if (rest) {
259
548
  const out = deadline !== undefined && spent >= deadline;
260
549
  const settled = voice.seen > 0 && voice.restedCount >= voice.seen;
261
- if (out || settled)
550
+ if (out)
551
+ this.retire(voice, at + deadline);
552
+ else if (settled)
262
553
  this.retire(voice);
263
554
  }
264
555
  else if (spent >= over)
265
- this.retire(voice);
556
+ this.retire(voice, at + over);
266
557
  }
267
558
  }
559
+ let pruned = false;
268
560
  for (let i = this.voices.length - 1; i >= 0; i--) {
269
561
  const voice = this.voices[i];
270
562
  if (voice.state === 'done') {
271
563
  this.voices.splice(i, 1);
564
+ if (this.opts.history)
565
+ this.gone.push(voice);
272
566
  this.version++;
273
- if (voice.spec.target !== undefined)
274
- this.targeted--;
567
+ if (voice.named === null)
568
+ pruned = true;
569
+ else
570
+ this.unindex(voice);
275
571
  if (voice.spec.locus !== undefined)
276
572
  this.loci--;
573
+ if (voice.spec.anchor !== undefined)
574
+ this.anchored--;
277
575
  }
278
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
+ }
279
584
  }
280
585
  probe(subject, out) {
281
586
  const pose = this.fold(subject, out);
587
+ this.keep(subject, pose, out);
588
+ return pose;
589
+ }
590
+ /** After a probe, keeps the pose a retarget from `'current'` reads as the subject's last. */
591
+ keep(subject, pose, out) {
282
592
  // Keeping last frame's pose is free when the mix allocated it; a host sampling into its own
283
593
  // object only pays for the copy once something in the mix has asked to retarget from it.
284
594
  let kept;
@@ -305,7 +615,176 @@ class Mixer {
305
615
  rec.at = this.now;
306
616
  }
307
617
  }
308
- return pose;
618
+ }
619
+ 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;
678
+ }
679
+ project(timestamp) {
680
+ const t = timestamp - this.offset;
681
+ 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
+ };
743
+ }
744
+ announce(name, opts = {}) {
745
+ const at = opts.at !== undefined ? opts.at - this.offset : this.now;
746
+ this.announced.push({
747
+ name,
748
+ score: opts.score,
749
+ tags: opts.tags ?? none,
750
+ at,
751
+ made: Number.isNaN(this.now) ? Number.NEGATIVE_INFINITY : this.now,
752
+ order: this.nextId++,
753
+ });
754
+ }
755
+ 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);
309
788
  }
310
789
  atRest(subject) {
311
790
  const pose = this.fold(subject, undefined, true);
@@ -332,10 +811,15 @@ class Mixer {
332
811
  this.beginFade(voice, { over: opts?.over });
333
812
  }
334
813
  drop(subject) {
814
+ const head = this.chains.get(subject);
815
+ if (head !== undefined && head.slot >= 0 && this.lanes !== null)
816
+ this.lanes.release(head.slot);
335
817
  this.pose.delete(subject);
336
- this.reach.delete(subject);
337
- for (const voice of this.voices)
818
+ this.chains.delete(subject);
819
+ for (const voice of [...this.voices, ...this.gone]) {
338
820
  voice.subjects.delete(subject);
821
+ motionOf(voice.patch)?.release(subject);
822
+ }
339
823
  }
340
824
  drain(tag) {
341
825
  const all = this.sent;
@@ -353,7 +837,517 @@ class Mixer {
353
837
  }
354
838
  return out.sort((a, b) => a.timestamp - b.timestamp);
355
839
  }
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
+ }
356
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
+ }
357
1351
  handle(voice) {
358
1352
  const mix = this;
359
1353
  return {
@@ -366,6 +1360,8 @@ class Mixer {
366
1360
  },
367
1361
  set weight(w) {
368
1362
  voice.weight = w;
1363
+ mix.noted(voice);
1364
+ mix.lanes?.refill();
369
1365
  },
370
1366
  get rate() {
371
1367
  return voice.rateAt(Number.isNaN(mix.now) ? voice.start : mix.now);
@@ -374,6 +1370,8 @@ class Mixer {
374
1370
  voice.rebase(Number.isNaN(mix.now) ? voice.start : mix.now);
375
1371
  voice.ramp = null;
376
1372
  voice.rate = r;
1373
+ mix.noted(voice);
1374
+ mix.lanes?.refill();
377
1375
  },
378
1376
  ramp(r, over) {
379
1377
  const now = Number.isNaN(mix.now) ? voice.start : mix.now;
@@ -381,10 +1379,15 @@ class Mixer {
381
1379
  const from = voice.rateAt(now);
382
1380
  voice.ramp = over > 0 && r !== from ? { from, to: r, over } : null;
383
1381
  voice.rate = r;
1382
+ mix.noted(voice);
1383
+ mix.lanes?.refill();
384
1384
  },
385
1385
  seek(elapsed) {
386
1386
  voice.rebase(Number.isNaN(mix.now) ? voice.start : mix.now);
387
1387
  voice.anchorElapsed = elapsed;
1388
+ voice.seeks++;
1389
+ mix.noted(voice);
1390
+ mix.lanes?.refill();
388
1391
  },
389
1392
  fade(opts) {
390
1393
  mix.beginFade(voice, opts ?? {});
@@ -392,57 +1395,60 @@ class Mixer {
392
1395
  weightOf(subject) {
393
1396
  if (voice.state === 'done')
394
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
+ }
395
1403
  return voice.subjects.get(subject)?.weight ?? 0;
396
1404
  },
397
1405
  done: voice.done,
398
1406
  };
399
1407
  }
400
- beginFade(voice, opts) {
1408
+ beginFade(voice, opts, at) {
401
1409
  if (voice.state === 'done' || voice.state === 'fading')
402
1410
  return;
403
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++;
404
1415
  voice.state = 'fading';
405
1416
  voice.out = {
406
1417
  from: 1,
407
- at: Number.isNaN(this.now) ? voice.start : this.now,
1418
+ at: at ?? (Number.isNaN(this.now) ? voice.start : this.now),
408
1419
  over,
409
1420
  rest: opts.at === 'rest',
410
1421
  deadline: opts.deadline,
411
1422
  };
1423
+ this.noted(voice);
1424
+ if (opts.at === 'rest')
1425
+ this.lanes?.invalidate();
1426
+ else
1427
+ this.lanes?.refill();
412
1428
  if (over === 0 && opts.at !== 'rest')
413
- this.retire(voice);
1429
+ this.retire(voice, voice.out.at);
414
1430
  }
415
- retire(voice) {
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;
416
1438
  voice.state = 'done';
1439
+ voice.doneAt = at ?? (Number.isNaN(this.now) ? Number.NEGATIVE_INFINITY : this.now);
417
1440
  voice.resolve();
418
1441
  }
419
1442
  /** The ramp a voice's own fade envelope applies this frame, 0..1. */
420
1443
  envelope(voice, now, since) {
421
- const reduced = this.reducedNow;
422
- const ease = voice.ease;
423
- let w = 1;
424
- const fadeIn = voice.fade.in ?? 0;
425
- if (fadeIn > 0 && !reduced) {
426
- const u = (now - since) / fadeIn;
427
- if (u < 1)
428
- w *= ease ? ease(Math.max(0, u)) : Math.max(0, u);
429
- }
430
- const out = voice.out;
431
- if (out && !out.rest) {
432
- if (reduced || out.over === 0)
433
- return 0;
434
- const u = 1 - (now - out.at) / out.over;
435
- const clamped = u < 0 ? 0 : u > 1 ? 1 : u;
436
- w *= ease ? ease(clamped) : clamped;
437
- }
438
- return w;
1444
+ return envelope(voice.fade.in ?? 0, voice.out, voice.ease, this.reducedNow, now, since);
439
1445
  }
440
1446
  /** What this voice holds for this subject, made on first sight with `target` and `stagger` asked once. */
441
1447
  held(voice, subject, now) {
442
1448
  let held = voice.subjects.get(subject);
443
1449
  if (held !== undefined)
444
1450
  return held;
445
- const reaches = voice.spec.target ? voice.spec.target(subject) : true;
1451
+ const reaches = this.aims(voice, subject);
446
1452
  const delay = reaches && voice.spec.stagger ? voice.spec.stagger(subject) : 0;
447
1453
  // When the voice clock reads `delay`, from where it is anchored now; during a ramp this assumes
448
1454
  // the rate it is ramping to.
@@ -460,13 +1466,24 @@ class Mixer {
460
1466
  state: reaches && voice.patch.state
461
1467
  ? voice.patch.state(subject)
462
1468
  : undefined,
463
- stepped: now,
1469
+ stepped: this.backward && since < now ? since : now,
464
1470
  ticks: 0,
465
1471
  probed: Number.NaN,
466
1472
  delta: null,
1473
+ phase: 0,
1474
+ seeks: 0,
467
1475
  kept,
468
1476
  keep: keeper(kept),
1477
+ voice,
1478
+ next: null,
1479
+ version: Number.NaN,
1480
+ loci: null,
1481
+ slot: -1,
469
1482
  };
1483
+ if (this.projecting) {
1484
+ held.from = held.stepped;
1485
+ held.unknown = this.backward && voice.spec.from === 'current';
1486
+ }
470
1487
  voice.subjects.set(subject, held);
471
1488
  if (reaches)
472
1489
  voice.seen++;
@@ -479,18 +1496,20 @@ class Mixer {
479
1496
  this.sending.voice = voice;
480
1497
  this.sending.subject = subject;
481
1498
  const signal = typeof voice.spec.weight === 'function' ? voice.spec.weight : null;
482
- const raw = (signal ? signal(subject, voice.setting) : voice.weight) *
483
- this.envelope(voice, now, held.since);
484
- return raw < 0 ? 0 : raw > 1 ? 1 : raw;
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));
485
1507
  }
486
1508
  /**
487
- * One voice's delta for one subject this frame, or null when it does not reach. Its weight and
488
- * the subject's record are left in `w` and `h`.
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`.
489
1511
  */
490
- influence(voice, subject, now) {
491
- if (voice.state === 'pending' || voice.state === 'done')
492
- return null;
493
- const held = this.held(voice, subject, now);
1512
+ influence(voice, subject, now, held) {
494
1513
  held.weight = 0;
495
1514
  if (!held.reaches)
496
1515
  return null;
@@ -500,37 +1519,24 @@ class Mixer {
500
1519
  if (elapsed < 0)
501
1520
  return null;
502
1521
  const period = voice.patch.period;
503
- const loop = voice.spec.loop ?? true;
504
- const passes = loop === true ? Number.POSITIVE_INFINITY : loop === false ? 1 : loop;
505
- let phase = 0;
506
- let pass = 0;
507
- if (period > 0) {
508
- const done = Number.isFinite(passes) && elapsed >= period * passes;
509
- phase = done ? 1 : (elapsed % period) / period;
510
- pass = done ? passes - 1 : Math.floor(elapsed / period);
511
- }
1522
+ const phase = phaseAt(elapsed, period, voice.passes);
1523
+ const pass = passAt(elapsed, period, voice.passes);
1524
+ this.prime(voice, held, now, elapsed, pass);
512
1525
  const setting = voice.setting;
513
- setting.timestamp = now;
514
- const gap = now - held.stepped;
515
- const cap = this.opts.maxDt;
516
- setting.dt = this.reducedNow
517
- ? Number.POSITIVE_INFINITY
518
- : cap !== undefined && gap > cap
519
- ? cap
520
- : gap;
521
- setting.elapsed = elapsed;
522
- setting.pass = pass;
523
- setting.weight = 0;
524
- setting.state = held.state;
525
- setting.keep = held.keep;
526
1526
  const weight = this.weigh(voice, subject, now, held);
527
1527
  setting.weight = weight;
528
1528
  held.weight = weight;
529
- if (held.probed === now && held.delta) {
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);
530
1532
  this.w = weight;
531
- this.h = held;
532
1533
  return held.delta;
533
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);
534
1540
  const tick = this.opts.stepMs;
535
1541
  if (voice.patch.step && held.probed !== now) {
536
1542
  if (tick !== undefined && tick > 0 && !this.reducedNow)
@@ -542,21 +1548,19 @@ class Mixer {
542
1548
  }
543
1549
  let delta;
544
1550
  if (voice.built) {
545
- let base;
546
- if (voice.spec.from === 'current') {
547
- if (held.base === undefined) {
548
- held.base = this.baseFor(voice, subject);
549
- held.slope = this.slopeFor(voice, subject);
550
- }
551
- base = held.base;
552
- }
553
- delta = readKeys(voice.built, phase, held.delta ?? {}, base, voice.lerps, held.slope);
1551
+ held.phase = phase;
1552
+ delta = this.keyed(voice, subject, held);
554
1553
  }
555
1554
  else {
556
1555
  delta = voice.patch.at(phase, subject, setting);
557
1556
  }
558
1557
  held.delta = delta;
559
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);
560
1564
  if (voice.out?.rest && this.isRest(delta)) {
561
1565
  held.weight = 0;
562
1566
  held.rested = true;
@@ -564,7 +1568,22 @@ class Mixer {
564
1568
  return null;
565
1569
  }
566
1570
  this.w = weight;
567
- this.h = held;
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;
568
1587
  return delta;
569
1588
  }
570
1589
  /**
@@ -693,27 +1712,87 @@ class Mixer {
693
1712
  return w >= on ? true : w <= off ? false : (was ?? w >= on);
694
1713
  }
695
1714
  /**
696
- * The voices that may reach this subject. A live voice is asked once, through the record it keeps
697
- * for the subject anyway; a pending one is kept until it goes live, since `target` is asked on
698
- * first sight, and sight only comes once a voice plays.
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.
699
1718
  */
700
- reaching(subject, now) {
701
- if (this.targeted === 0)
702
- return this.voices;
703
- const held = this.reach.get(subject);
704
- if (held !== undefined && held.version === this.version)
705
- return held.voices;
706
- const voices = [];
707
- for (const voice of this.voices) {
708
- if (voice.state === 'done')
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)
709
1788
  continue;
710
- if (voice.state === 'pending' || this.held(voice, subject, now).reaches)
711
- voices.push(voice);
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);
712
1794
  }
713
- this.reach.set(subject, { version: this.version, voices });
714
- return voices;
715
1795
  }
716
- locusBands = new Store();
717
1796
  /** Voices whose stop 0 is mid-computation, so a fold for one cannot re-enter itself. */
718
1797
  folding = new Set();
719
1798
  /** Folds one voice's delta into the pose, its band state kept on the subject's record. */
@@ -762,8 +1841,20 @@ class Mixer {
762
1841
  }
763
1842
  folded(subject, out, dry = false, except) {
764
1843
  const now = this.now;
765
- const pose = (out ?? {});
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;
766
1855
  for (let i = 0; i < this.names.length; i++) {
1856
+ if (skip?.[i])
1857
+ continue;
767
1858
  const rest = this.channels[i].rest;
768
1859
  const key = this.names[i];
769
1860
  // Never `delete`: it drops a reused out object into dictionary mode for good.
@@ -772,32 +1863,42 @@ class Mixer {
772
1863
  else if (pose[key] !== undefined)
773
1864
  pose[key] = undefined;
774
1865
  }
775
- if (Number.isNaN(now))
1866
+ if (head === null)
776
1867
  return pose;
777
- const voices = this.reaching(subject, now);
1868
+ if (laned) {
1869
+ lanes.copy(head.slot, pose);
1870
+ if (lanes.whole)
1871
+ return pose;
1872
+ }
778
1873
  if (this.loci === 0) {
779
- for (const voice of voices) {
780
- if (voice.id === except)
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')
781
1879
  continue;
782
- const delta = this.read(voice, subject, now, dry);
1880
+ const delta = this.read(voice, subject, now, dry, held);
783
1881
  if (delta === null || this.w <= 0)
784
1882
  continue;
785
- this.apply(pose, voice, this.h, delta, this.w);
1883
+ this.apply(pose, voice, held, delta, this.w);
786
1884
  }
787
1885
  return pose;
788
1886
  }
789
1887
  const order = [];
790
1888
  const weights = [];
791
1889
  const loci = new Map();
792
- for (const voice of voices) {
793
- if (voice.id === except)
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')
794
1895
  continue;
795
- const delta = this.read(voice, subject, now, dry);
1896
+ const delta = this.read(voice, subject, now, dry, held);
796
1897
  if (delta === null)
797
1898
  continue;
798
1899
  const locus = voice.spec.locus;
799
1900
  if (locus === undefined) {
800
- order.push({ voice, held: this.h, delta });
1901
+ order.push({ voice, held, delta });
801
1902
  weights.push(this.w);
802
1903
  continue;
803
1904
  }
@@ -810,10 +1911,10 @@ class Mixer {
810
1911
  weights.push(0);
811
1912
  }
812
1913
  }
813
- let bands = this.locusBands.get(subject);
814
- if (bands === undefined) {
1914
+ let bands = head.loci;
1915
+ if (bands === null) {
815
1916
  bands = new Map();
816
- this.locusBands.set(subject, bands);
1917
+ head.loci = bands;
817
1918
  }
818
1919
  for (let n = 0; n < order.length; n++) {
819
1920
  const at = order[n];
@@ -847,10 +1948,9 @@ class Mixer {
847
1948
  return pose;
848
1949
  }
849
1950
  /** `influence`, or for a dry fold a subject already probed this frame, without advancing it. */
850
- read(voice, subject, now, dry) {
1951
+ read(voice, subject, now, dry, held) {
851
1952
  if (dry) {
852
- const held = voice.subjects.get(subject);
853
- if (held?.reaches && held.probed === now && held.delta) {
1953
+ if (held.reaches && held.probed === now && held.delta && held.seeks === voice.seeks) {
854
1954
  const setting = voice.setting;
855
1955
  setting.timestamp = now;
856
1956
  setting.dt = 0;
@@ -860,21 +1960,23 @@ class Mixer {
860
1960
  setting.state = held.state;
861
1961
  setting.keep = held.keep;
862
1962
  this.w = this.weigh(voice, subject, now, held);
863
- this.h = held;
1963
+ held.weight = this.w;
1964
+ if (voice.holder !== held && voice.scratch.length > 0)
1965
+ this.keyed(voice, subject, held);
864
1966
  return held.delta;
865
1967
  }
866
1968
  }
867
- return this.influence(voice, subject, now);
1969
+ return this.influence(voice, subject, now, held);
868
1970
  }
869
1971
  }
870
1972
  /**
871
- * The one that ships: both forms on the CPU, probed per subject on demand.
1973
+ * The one that ships: every form on the CPU, probed per subject on demand.
872
1974
  *
873
1975
  * @category engine
874
1976
  */
875
1977
  export const mixer = {
876
1978
  name: 'mixer',
877
- runs: new Set(['fn', 'keys']),
1979
+ runs: new Set(['fn', 'keys', 'motion']),
878
1980
  create(kit, opts) {
879
1981
  return new Mixer(kit, opts);
880
1982
  },