@msb235/blits 0.3.0 → 0.4.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/lanes.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { foldNumber, numericOf } from './channels.js';
2
2
  import { clampWeight, passAt, phaseAt } from './clock.js';
3
- import { motionOf } from './motion.js';
3
+ import { closed, motionOf, noTouch } from './motion.js';
4
4
  import { absent, Numbers } from './numbers.js';
5
5
  import { readKeys } from './patch.js';
6
6
  import { reading } from './reading.js';
@@ -41,7 +41,7 @@ export function clampRun(out, at, n, [lo, hi]) {
41
41
  out[i] = x < lo ? lo : x > hi ? hi : x;
42
42
  }
43
43
  }
44
- const STRIDE = 7;
44
+ const STRIDE = 8;
45
45
  const DELAY = 0;
46
46
  const SINCE = 1;
47
47
  /** The weight the last fill gave the subject. */
@@ -52,8 +52,18 @@ const MSLOT = 4;
52
52
  /** For a motion voice, the last fill that sampled the subject, and the voice's seeks then. */
53
53
  const SAMPLED = 5;
54
54
  const SEEKS = 6;
55
+ /** 1 once the general path has made the voice's first call for the subject, which never undoes. */
56
+ const MET = 7;
57
+ /** What `begin` found: the general path, only solo lanes, or lanes filled. */
58
+ const GENERAL = 0;
59
+ const SOLO = 1;
60
+ const READY = 2;
55
61
  const SLOT = 6;
56
62
  const FILLED = 0;
63
+ /**
64
+ * The latest epoch a probe has checked the subject against; or, as `-1 - e`, that a voice naming
65
+ * it started after epoch `e`, so its next probe checks it against everything since `e`.
66
+ */
57
67
  const SEEN = 1;
58
68
  const IDLE = 2;
59
69
  const LANE_PROBE = 3;
@@ -147,6 +157,30 @@ class Lane {
147
157
  flat = false;
148
158
  /** A motion patch's state, which the lane samples in place of calling the patch. */
149
159
  motion;
160
+ voiceAt() {
161
+ return this.voice;
162
+ }
163
+ keep(p, xs, n) {
164
+ if (this.axes !== n) {
165
+ this.axes = n;
166
+ this.samples = new Float64Array(0);
167
+ }
168
+ if ((p + 1) * n > this.samples.length) {
169
+ const grown = new Float64Array(Math.max(p + 1, (this.samples.length / n) * 2, 4) * n);
170
+ grown.set(this.samples);
171
+ this.samples = grown;
172
+ }
173
+ for (let a = 0; a < n; a++)
174
+ this.samples[p * n + a] = xs[a];
175
+ }
176
+ kept(p, xs) {
177
+ const n = this.axes;
178
+ for (let a = 0; a < n; a++)
179
+ xs[a] = this.samples[p * n + a];
180
+ }
181
+ motionAt() {
182
+ return this.motion;
183
+ }
150
184
  constructor(voice) {
151
185
  this.voice = voice;
152
186
  this.motion = motionOf(voice.patch);
@@ -169,11 +203,12 @@ class Lane {
169
203
  }
170
204
  const o = p * STRIDE;
171
205
  this.data[o + DELAY] = held.delay;
172
- this.data[o + SINCE] = held.since;
206
+ this.data[o + SINCE] = held.shown;
173
207
  this.data[o + WEIGHT] = 0;
174
208
  this.data[o + PROBED] = 0;
175
209
  this.data[o + MSLOT] = -1;
176
210
  this.data[o + SAMPLED] = -1;
211
+ this.data[o + MET] = 0;
177
212
  this.records[p] = held;
178
213
  this.deltas[p] = null;
179
214
  }
@@ -200,6 +235,144 @@ class Lane {
200
235
  this.deltas[last] = null;
201
236
  }
202
237
  }
238
+ /** Whether a laned voice belongs in a crowd: one naming a single subject and writing one channel. */
239
+ function crowdable(v) {
240
+ return v.named !== null && v.named.size === 1 && v.slots.length === 1;
241
+ }
242
+ /**
243
+ * A subject's voice time once its voice's holds apply: held at 0 before it starts and at the end of
244
+ * its passes after, and NaN where it shows nothing.
245
+ */
246
+ function held(voice, elapsed) {
247
+ if (elapsed < 0)
248
+ return voice.holdsBefore ? 0 : Number.NaN;
249
+ return voice.holdsAfter && elapsed > voice.span ? voice.span : elapsed;
250
+ }
251
+ /** The law of a row that has none, a keys or fn voice's. */
252
+ const none = new Float64Array(0);
253
+ /** Per row of a crowd's `hot`: what the fill reads of the row's voice and its current stretch. */
254
+ const H_FLAGS = 0;
255
+ const H_NOW = 1;
256
+ const H_ELAPSED = 2;
257
+ const H_RATE = 3;
258
+ /** The voice's weight, held to 0..1. */
259
+ const H_WEIGHT = 4;
260
+ const H_SEEKS = 5;
261
+ const H_EPOCH = 6;
262
+ const H_ID = 7;
263
+ const H_AT = 8;
264
+ const H_MS = 9;
265
+ /** `x0`, `v0` and `to`, `axes` numbers each. */
266
+ const H_X0 = 10;
267
+ /** The voice is playing: live, held or fading. */
268
+ const F_PLAYING = 1;
269
+ /** Its weight and clock are `hot`'s: no fade in or out, rate ramp, subject ramp or kept state. */
270
+ const F_FAST = 2;
271
+ /** Its stretch changed since `hot` copied it. */
272
+ const F_STALE = 4;
273
+ /** `hot` holds its stretch, with nothing pending and no earlier stretch kept. */
274
+ const F_BARE = 8;
275
+ /** A probe has met its subject, so its per-subject numbers are set. */
276
+ const F_PLACED = 16;
277
+ /** Its samples fold as the channel's axes, a number for a number and an array for an array. */
278
+ const F_FOLDS = 32;
279
+ /** Something on the voice changed since `hot` copied it. */
280
+ const F_VOICE = 64;
281
+ /** The row's voice is a motion voice; otherwise its patch is keys or a stateless fn. */
282
+ const F_MOTION = 128;
283
+ /** The voice holds before or after, so its clock is read from the voice. */
284
+ const F_HOLDS = 256;
285
+ /**
286
+ * Every laned voice on one channel that reaches a single subject, a row each, in voice order: the
287
+ * per-subject numbers a lane keeps, and a copy of what a fill reads of the voice and, for a motion
288
+ * voice, its patch, so a fill over 100k of them reads a few flat arrays instead of 100k voices,
289
+ * lanes and patch buffers. Each copy is written when its source changes: the voice's by
290
+ * `voiceChanged`, the stretch through the patch's `touched`.
291
+ */
292
+ class Crowd {
293
+ chans;
294
+ axes;
295
+ /** The subject number at each row. */
296
+ list = [];
297
+ /** `STRIDE` numbers per row, laid out as a lane's. */
298
+ data = new Float64Array(0);
299
+ hot = new Float64Array(0);
300
+ stride;
301
+ samples = new Float64Array(0);
302
+ deltas = [];
303
+ records = [];
304
+ voices = [];
305
+ motions = [];
306
+ eases = [];
307
+ /** Each row's patch law, one array shared by every row whose law is the same. */
308
+ laws = [];
309
+ touches = [];
310
+ rowOf = new Map();
311
+ idle = false;
312
+ line = SPARSE.other;
313
+ /** Where the fill now under way has reached. */
314
+ cursor = 0;
315
+ /** Rows whose voices have left, kept in place until the crowds are next rebuilt. */
316
+ dead = 0;
317
+ xs;
318
+ vs;
319
+ /** What a keys row reads into, folded before the next row reads. */
320
+ delta = {};
321
+ scratch = [];
322
+ constructor(chans, axes) {
323
+ this.chans = chans;
324
+ this.axes = axes;
325
+ this.stride = H_X0 + 3 * axes;
326
+ this.xs = new Float64Array(axes);
327
+ this.vs = new Float64Array(axes);
328
+ }
329
+ voiceAt(p) {
330
+ return this.voices[p];
331
+ }
332
+ /** Samples of another width than the channel's, which `samples` cannot hold, by row. */
333
+ odd = null;
334
+ keep(p, xs, n) {
335
+ if (n === this.axes) {
336
+ for (let a = 0; a < n; a++)
337
+ this.samples[p * n + a] = xs[a];
338
+ this.odd?.delete(p);
339
+ return;
340
+ }
341
+ this.odd ??= new Map();
342
+ this.odd.set(p, xs.slice(0, n));
343
+ }
344
+ kept(p, xs) {
345
+ const odd = this.odd?.get(p);
346
+ if (odd !== undefined) {
347
+ xs.set(odd);
348
+ return;
349
+ }
350
+ const n = this.axes;
351
+ for (let a = 0; a < n; a++)
352
+ xs[a] = this.samples[p * n + a];
353
+ }
354
+ motionAt(p) {
355
+ return this.motions[p];
356
+ }
357
+ get size() {
358
+ return this.list.length;
359
+ }
360
+ /** Makes room for `rows` rows. */
361
+ reserve(rows) {
362
+ if (rows * STRIDE <= this.data.length)
363
+ return;
364
+ const cap = Math.max(rows, (this.data.length / STRIDE) * 2, 16);
365
+ const data = new Float64Array(cap * STRIDE);
366
+ data.set(this.data);
367
+ this.data = data;
368
+ const hot = new Float64Array(cap * this.stride);
369
+ hot.set(this.hot);
370
+ this.hot = hot;
371
+ const samples = new Float64Array(cap * this.axes);
372
+ samples.set(this.samples);
373
+ this.samples = samples;
374
+ }
375
+ }
203
376
  /**
204
377
  * The lanes of one mix: subject numbers, which channels and voices run on lanes, and the values a
205
378
  * frame's fill leaves for probes to copy.
@@ -213,6 +386,10 @@ export class Lanes {
213
386
  /** The lanes over every subject that have started playing, by epoch. */
214
387
  dense = [];
215
388
  byId = new Map();
389
+ /** The crowds of single-subject motion voices, one per channel, and which holds each voice. */
390
+ crowds = [];
391
+ crowdOf = new Map();
392
+ lawsByKey = new Map();
216
393
  laned = [];
217
394
  /** By kit slot, the laned channel there. */
218
395
  bySlot = [];
@@ -222,6 +399,12 @@ export class Lanes {
222
399
  whole = false;
223
400
  cap = 0;
224
401
  epochs = 0;
402
+ /** The latest epoch of a lane over every subject, which every subject meets on its next probe. */
403
+ wide = 0;
404
+ /** Voices that joined, started or left since the last qualify, settled at the next probe. */
405
+ touched = [];
406
+ /** The highest voice id the last qualify or settling of `touched` considered. */
407
+ known = 0;
225
408
  /**
226
409
  * Per subject number, `SLOT` numbers side by side, so a probe reads one place in memory: which
227
410
  * fill last wrote it (0 for none); the latest lane epoch a probe of it has checked it against;
@@ -248,6 +431,12 @@ export class Lanes {
248
431
  * the general path has to make.
249
432
  */
250
433
  late = [];
434
+ /** What `writeLater` has queued: lane slots and the rows they go to, for one set of columns. */
435
+ queueSlots = new Int32Array(0);
436
+ queueRows = new Int32Array(0);
437
+ queued = 0;
438
+ inOrder = true;
439
+ queuedFor = null;
251
440
  keeps = false;
252
441
  now = Number.NaN;
253
442
  filledAt = Number.NaN;
@@ -281,13 +470,21 @@ export class Lanes {
281
470
  refill() {
282
471
  this.filledVersion = Number.NaN;
283
472
  }
473
+ /**
474
+ * A voice joined, started playing or left: the next probe takes it on or off its crowd where it
475
+ * can, and qualifies again where it cannot.
476
+ */
477
+ touch(voice) {
478
+ this.touched.push(voice);
479
+ this.filledVersion = Number.NaN;
480
+ }
284
481
  /** Numbers a subject the mix sees for the first time. */
285
482
  number(subject) {
286
483
  const slot = this.numbers.take(subject);
287
484
  this.live++;
288
485
  this.grow(slot + 1);
289
486
  this.per[slot * SLOT + FILLED] = 0;
290
- this.per[slot * SLOT + SEEN] = 0;
487
+ this.per[slot * SLOT + SEEN] = -1;
291
488
  this.per[slot * SLOT + IDLE] = 0;
292
489
  this.per[slot * SLOT + LANE_PROBE] = 0;
293
490
  this.per[slot * SLOT + GENERAL_PROBE] = 0;
@@ -299,16 +496,56 @@ export class Lanes {
299
496
  release(slot) {
300
497
  this.numbers.release(slot);
301
498
  }
499
+ /** A voice faded a subject out of itself alone: its lane lets the subject's position go. */
500
+ part(id, slot) {
501
+ const lane = this.byId.get(id);
502
+ if (lane !== undefined) {
503
+ if (lane.positionOf(slot) < 0)
504
+ return;
505
+ if (lane.idle)
506
+ this.reach(slot, -1);
507
+ lane.remove(slot);
508
+ return;
509
+ }
510
+ const c = this.crowdOf.get(id);
511
+ const p = c?.rowOf.get(id);
512
+ if (c !== undefined && p !== undefined && c.list[p] === slot)
513
+ this.unplace(c, p);
514
+ }
515
+ /** Takes a crowd row off its subject, to be placed again if a probe meets it again. */
516
+ unplace(c, p) {
517
+ const slot = c.list[p];
518
+ if (c.idle && slot >= 0)
519
+ this.reach(slot, -1);
520
+ c.list[p] = -1;
521
+ c.records[p] = undefined;
522
+ c.deltas[p] = null;
523
+ const f = p * c.stride + H_FLAGS;
524
+ c.hot[f] = c.hot[f] & ~F_PLACED;
525
+ const o = p * STRIDE;
526
+ c.data[o + MSLOT] = -1;
527
+ c.data[o + SAMPLED] = -1;
528
+ c.data[o + MET] = 0;
529
+ }
530
+ /** A voice brought a subject back: its next probe meets every lane again, as it met them first. */
531
+ rejoin(slot) {
532
+ if (slot < this.cap)
533
+ this.per[slot * SLOT + SEEN] = -1;
534
+ }
302
535
  /**
303
536
  * A laned voice's weight for a subject at the last frame a probe read it from the lane; undefined
304
537
  * where the subject's record holds it instead, because its last probe took the general path or
305
538
  * the voice is not on a lane.
306
539
  */
307
540
  weightOf(id, slot) {
308
- const lane = this.byId.get(id);
309
- if (lane === undefined || slot < 0)
541
+ if (slot < 0)
310
542
  return undefined;
311
- return this.reported(lane, slot);
543
+ const lane = this.byId.get(id);
544
+ if (lane !== undefined)
545
+ return this.reported(lane, slot);
546
+ const c = this.crowdOf.get(id);
547
+ const p = c?.rowOf.get(id);
548
+ return c === undefined || p === undefined ? undefined : this.reportedRow(c, p, slot);
312
549
  }
313
550
  reported(lane, slot) {
314
551
  const o = slot * SLOT;
@@ -325,6 +562,91 @@ export class Lanes {
325
562
  * this frame, which is where it is first seen; so does one probed from inside a fill.
326
563
  */
327
564
  prepare(slot, subject, now, version) {
565
+ const ready = this.filled(now, version) ? READY : this.begin(now, version);
566
+ if (ready === GENERAL)
567
+ return this.general(slot);
568
+ if (slot < 0)
569
+ return false;
570
+ if (ready === SOLO) {
571
+ if (this.per[slot * SLOT + SEEN] < this.wide)
572
+ this.meet(slot, subject);
573
+ return this.general(slot);
574
+ }
575
+ if (!this.probedThisFrame(slot))
576
+ this.distinct++;
577
+ const probe = ++this.probes;
578
+ const per = this.per;
579
+ const o = slot * SLOT;
580
+ let lane = per[o + FILLED] === this.fills && per[o + IDLE] === 0;
581
+ if (per[o + SEEN] < this.wide && this.meet(slot, subject)) {
582
+ // The fill ran before the subject had these positions, so it reads the general path all frame.
583
+ per[o + FILLED] = 0;
584
+ lane = false;
585
+ }
586
+ if (lane) {
587
+ per[o + LANE_PROBE] = probe;
588
+ per[o + LANE_FILL] = this.fills;
589
+ this.subjects[slot] = subject;
590
+ this.holding = true;
591
+ }
592
+ else
593
+ per[o + GENERAL_PROBE] = probe;
594
+ return lane;
595
+ }
596
+ /**
597
+ * For a `pull` of a list it read last time in the same order, with every voice on a lane, where
598
+ * `heads` holds each position's remembered chain head unless all are known current: what
599
+ * `prepare` and `writeLater` do for each subject from position `from`, while the subject reads
600
+ * from the lanes; returns the first position that does not, or the list's length.
601
+ */
602
+ pullRun(slots, list, was, heads, from, columns, now, version) {
603
+ const ready = this.filled(now, version) ? READY : this.begin(now, version);
604
+ if (ready !== READY || !this.whole)
605
+ return from;
606
+ const per = this.per;
607
+ const fills = this.fills;
608
+ const wide = this.wide;
609
+ const frame = this.frameProbes;
610
+ const subjects = this.subjects;
611
+ let n = from;
612
+ for (; n < list.length; n++) {
613
+ const subject = list[n];
614
+ if (was[n] !== subject || (heads !== null && heads[n]?.version !== version))
615
+ break;
616
+ const slot = slots[n];
617
+ const o = slot * SLOT;
618
+ if (slot < 0 ||
619
+ per[o + FILLED] !== fills ||
620
+ per[o + IDLE] !== 0 ||
621
+ per[o + SEEN] < wide)
622
+ break;
623
+ if (!(per[o + LANE_PROBE] > frame || per[o + GENERAL_PROBE] > frame))
624
+ this.distinct++;
625
+ per[o + LANE_PROBE] = ++this.probes;
626
+ per[o + LANE_FILL] = fills;
627
+ subjects[slot] = subject;
628
+ this.writeLater(slot, columns, n);
629
+ }
630
+ if (n > from)
631
+ this.holding = true;
632
+ return n;
633
+ }
634
+ /** Whether this frame's lanes are filled and nothing since asks `begin` to look again. */
635
+ filled(now, version) {
636
+ return (this.filledAt === now &&
637
+ this.frameAt === now &&
638
+ this.filledVersion === version &&
639
+ this.qualifiedVersion === version &&
640
+ this.moved === reading.moved &&
641
+ this.touched.length === 0 &&
642
+ !this.filling);
643
+ }
644
+ /**
645
+ * What a probe does before its subject, once a frame's lanes are settled: qualifies and fills as
646
+ * needed. Says whether the subject takes the general path, whether only solo lanes exist, so
647
+ * there is nothing to fill, or whether the lanes are filled.
648
+ */
649
+ begin(now, version) {
328
650
  if (now !== this.frameAt) {
329
651
  this.lastFrom = this.frameProbes;
330
652
  this.lastTo = this.probes;
@@ -335,51 +657,27 @@ export class Lanes {
335
657
  this.fresh = true;
336
658
  }
337
659
  if (this.filling)
338
- return this.general(slot);
660
+ return GENERAL;
339
661
  if (this.qualifiedVersion !== version)
340
662
  this.requalify(version);
663
+ else if (this.touched.length > 0 && !this.retouch())
664
+ this.requalify(version);
341
665
  if (this.laned.length === 0)
342
- return this.general(slot);
666
+ return GENERAL;
343
667
  // Only solo lanes: nothing to fill, and every subject takes the general path.
344
- if (this.runs.length === 0) {
345
- if (slot < 0)
346
- return false;
347
- if (this.per[slot * SLOT + SEEN] < this.epochs)
348
- this.meet(slot, subject);
349
- return this.general(slot);
350
- }
351
- if (slot >= 0 && !this.probedThisFrame(slot))
352
- this.distinct++;
668
+ if (this.runs.length === 0 && this.crowds.length === 0)
669
+ return SOLO;
353
670
  if (this.filledAt !== now || this.filledVersion !== version || this.moved !== reading.moved) {
354
671
  this.fillAll(now, version);
355
672
  // A patch call in that fill made kept state, which took its voice off its lane: fill without it.
356
673
  if (this.qualifiedVersion !== version) {
357
674
  this.requalify(version);
358
675
  if (this.laned.length === 0)
359
- return this.general(slot);
676
+ return GENERAL;
360
677
  this.fillAll(now, version);
361
678
  }
362
679
  }
363
- if (slot < 0)
364
- return false;
365
- const probe = ++this.probes;
366
- const per = this.per;
367
- const o = slot * SLOT;
368
- let lane = per[o + FILLED] === this.fills && per[o + IDLE] === 0;
369
- if (per[o + SEEN] < this.epochs && this.meet(slot, subject)) {
370
- // The fill ran before the subject had these positions, so it reads the general path all frame.
371
- per[o + FILLED] = 0;
372
- lane = false;
373
- }
374
- if (lane) {
375
- per[o + LANE_PROBE] = probe;
376
- per[o + LANE_FILL] = this.fills;
377
- this.subjects[slot] = subject;
378
- this.holding = true;
379
- }
380
- else
381
- per[o + GENERAL_PROBE] = probe;
382
- return lane;
680
+ return READY;
383
681
  }
384
682
  general(slot) {
385
683
  if (slot >= 0)
@@ -393,28 +691,75 @@ export class Lanes {
393
691
  return (this.per[o + LANE_PROBE] > from || this.per[o + GENERAL_PROBE] > from);
394
692
  }
395
693
  /**
396
- * Writes a subject's values into each column at subject `n`'s places, for a probe that reads from
397
- * the lanes while every voice is on one: a channel no lane holds is at rest.
694
+ * Queues a subject's values for each column at subject `n`'s places, for a `pull` that reads
695
+ * from the lanes while every voice is on one; `flush` writes what is queued, column by column. A
696
+ * channel no lane holds is at rest. Anything queued is written before the lanes fill again.
398
697
  */
399
- write(slot, columns, n) {
400
- const bySlot = this.bySlot;
698
+ writeLater(slot, columns, n) {
699
+ if (columns !== this.queuedFor) {
700
+ this.flush();
701
+ this.queuedFor = columns;
702
+ }
703
+ const k = this.queued;
704
+ if (k === this.queueSlots.length) {
705
+ const slots = new Int32Array(Math.max(64, k * 2));
706
+ slots.set(this.queueSlots);
707
+ this.queueSlots = slots;
708
+ const rows = new Int32Array(slots.length);
709
+ rows.set(this.queueRows);
710
+ this.queueRows = rows;
711
+ }
712
+ this.queueSlots[k] = slot;
713
+ this.queueRows[k] = n;
714
+ if (k > 0 &&
715
+ (slot !== this.queueSlots[k - 1] + 1 ||
716
+ n !== this.queueRows[k - 1] + 1))
717
+ this.inOrder = false;
718
+ this.queued = k + 1;
719
+ }
720
+ flush() {
721
+ const count = this.queued;
722
+ const columns = this.queuedFor;
723
+ if (count === 0 || columns === null)
724
+ return;
725
+ this.queued = 0;
726
+ // Subjects numbered in the order the host lists them: each column is one block.
727
+ const block = this.inOrder;
728
+ this.inOrder = true;
729
+ const slots = this.queueSlots;
730
+ const rows = this.queueRows;
401
731
  for (let k = 0; k < columns.length; k++) {
402
732
  const c = columns[k];
403
733
  const axes = c.axes;
404
734
  const out = c.out;
405
- const at = n * axes;
406
- const ch = bySlot[c.slot];
735
+ const ch = this.bySlot[c.slot];
407
736
  if (ch === undefined) {
408
- for (let a = 0; a < axes; a++)
409
- out[at + a] = c.rest[a];
737
+ for (let i = 0; i < count; i++) {
738
+ const at = rows[i] * axes;
739
+ for (let a = 0; a < axes; a++)
740
+ out[at + a] = c.rest[a];
741
+ }
410
742
  continue;
411
743
  }
412
744
  const values = ch.values;
413
- const base = slot * axes;
414
- for (let a = 0; a < axes; a++)
415
- out[at + a] = values[base + a];
745
+ if (block) {
746
+ const s0 = slots[0] * axes;
747
+ out.set(values.subarray(s0, s0 + count * axes), rows[0] * axes);
748
+ }
749
+ else if (axes === 1) {
750
+ for (let i = 0; i < count; i++)
751
+ out[rows[i]] = values[slots[i]];
752
+ }
753
+ else
754
+ for (let i = 0; i < count; i++) {
755
+ const at = rows[i] * axes;
756
+ const base = slots[i] * axes;
757
+ for (let a = 0; a < axes; a++)
758
+ out[at + a] = values[base + a];
759
+ }
416
760
  if (c.bounds !== undefined)
417
- clampRun(out, at, axes, c.bounds);
761
+ for (let i = 0; i < count; i++)
762
+ clampRun(out, rows[i] * axes, axes, c.bounds);
418
763
  }
419
764
  }
420
765
  /** Writes a subject's laned values into a pose, each array channel into a new array. */
@@ -423,7 +768,7 @@ export class Lanes {
423
768
  for (let c = 0; c < laned.length; c++) {
424
769
  const ch = laned[c];
425
770
  const axes = ch.axes;
426
- if (axes === 1) {
771
+ if (ch.scalar) {
427
772
  pose[ch.name] = ch.values[slot];
428
773
  continue;
429
774
  }
@@ -440,7 +785,8 @@ export class Lanes {
440
785
  * checked and that reaches it. True when any did, so the probe takes the general path.
441
786
  */
442
787
  meet(slot, subject) {
443
- const from = this.per[slot * SLOT + SEEN];
788
+ const seen = this.per[slot * SLOT + SEEN];
789
+ const from = seen < 0 ? -1 - seen : seen;
444
790
  this.per[slot * SLOT + SEEN] = this.epochs;
445
791
  let met = false;
446
792
  const dense = this.dense;
@@ -448,6 +794,8 @@ export class Lanes {
448
794
  const lane = dense[i];
449
795
  if (lane.epoch <= from)
450
796
  break;
797
+ if (lane.positionOf(slot) >= 0)
798
+ continue;
451
799
  const held = this.host.meet(lane.voice, subject);
452
800
  if (!held.reaches)
453
801
  continue;
@@ -460,15 +808,52 @@ export class Lanes {
460
808
  if (naming !== undefined)
461
809
  for (const voice of naming) {
462
810
  const lane = this.byId.get(voice.id);
463
- if (lane === undefined || lane.epoch <= from || lane.positionOf(slot) >= 0)
811
+ if (lane === undefined) {
812
+ if (this.place(voice, slot, subject, from))
813
+ met = true;
814
+ continue;
815
+ }
816
+ if (lane.epoch <= from || lane.positionOf(slot) >= 0)
464
817
  continue;
465
- lane.add(slot, this.host.meet(voice, subject));
818
+ const held = this.host.meet(voice, subject);
819
+ if (!held.reaches)
820
+ continue;
821
+ lane.add(slot, held);
466
822
  if (lane.idle)
467
823
  this.reach(slot, 1);
468
824
  met = true;
469
825
  }
470
826
  return met;
471
827
  }
828
+ /** Places a crowd voice's row on the subject a probe met, as `Lane.add` adds a position. */
829
+ place(voice, slot, subject, from) {
830
+ const c = this.crowdOf.get(voice.id);
831
+ const p = c?.rowOf.get(voice.id);
832
+ if (c === undefined || p === undefined)
833
+ return false;
834
+ const h = p * c.stride;
835
+ const flags = c.hot[h + H_FLAGS];
836
+ if (c.hot[h + H_EPOCH] <= from || (flags & F_PLACED) !== 0)
837
+ return false;
838
+ const held = this.host.meet(voice, subject);
839
+ if (!held.reaches)
840
+ return false;
841
+ const o = p * STRIDE;
842
+ c.list[p] = slot;
843
+ c.data[o + DELAY] = held.delay;
844
+ c.data[o + SINCE] = held.shown;
845
+ c.data[o + WEIGHT] = 0;
846
+ c.data[o + PROBED] = 0;
847
+ c.data[o + MSLOT] = -1;
848
+ c.data[o + SAMPLED] = -1;
849
+ c.data[o + MET] = 0;
850
+ c.records[p] = held;
851
+ c.deltas[p] = null;
852
+ c.hot[h + H_FLAGS] = flags | F_PLACED | F_STALE;
853
+ if (c.idle)
854
+ this.reach(slot, 1);
855
+ return true;
856
+ }
472
857
  /** Counts an idle lane more or fewer reaching a subject. */
473
858
  reach(slot, by) {
474
859
  const i = slot * SLOT + IDLE;
@@ -478,6 +863,10 @@ export class Lanes {
478
863
  this.live--;
479
864
  for (const lane of this.lanes)
480
865
  lane.remove(slot);
866
+ for (const c of this.crowds)
867
+ for (let p = 0; p < c.size; p++)
868
+ if (c.list[p] === slot)
869
+ this.unplace(c, p);
481
870
  if (slot < this.cap)
482
871
  this.per[slot * SLOT + FILLED] = 0;
483
872
  this.subjects[slot] = undefined;
@@ -497,10 +886,15 @@ export class Lanes {
497
886
  this.cap = cap;
498
887
  }
499
888
  requalify(version) {
889
+ this.flush();
500
890
  const host = this.host;
501
891
  this.qualifiedVersion = version;
502
892
  this.filledVersion = Number.NaN;
893
+ this.touched.length = 0;
503
894
  const present = host.voices.filter((v) => v.state !== 'done');
895
+ for (const v of present)
896
+ if (v.id > this.known)
897
+ this.known = v.id;
504
898
  const numeric = host.channels.map((c) => numericOf(c) !== undefined);
505
899
  const { channels, voices } = qualify(numeric,
506
900
  // A voice writing nothing has no lane to fill, so it stays where its calls are made.
@@ -509,13 +903,18 @@ export class Lanes {
509
903
  if (!voices.has(lane.voice.id))
510
904
  this.leave(lane);
511
905
  const kept = [];
906
+ let crowded = 0;
512
907
  for (const v of present) {
513
908
  if (!voices.has(v.id))
514
909
  continue;
910
+ if (crowdable(v)) {
911
+ crowded++;
912
+ continue;
913
+ }
515
914
  const lane = this.byId.get(v.id) ?? new Lane(v);
516
915
  v.laned = true;
517
- if (lane.epoch === 0 && (v.state === 'live' || v.state === 'fading'))
518
- lane.epoch = ++this.epochs;
916
+ if (lane.epoch === 0 && v.state !== 'pending')
917
+ lane.epoch = this.open(v);
519
918
  kept.push(lane);
520
919
  }
521
920
  this.lanes = kept;
@@ -523,12 +922,12 @@ export class Lanes {
523
922
  this.dense = kept
524
923
  .filter((l) => l.voice.named === null && l.epoch > 0)
525
924
  .sort((a, b) => a.epoch - b.epoch);
526
- this.whole = kept.length === present.length;
925
+ this.whole = kept.length + crowded === present.length;
527
926
  this.byId.clear();
528
927
  for (const lane of kept)
529
928
  this.byId.set(lane.voice.id, lane);
530
929
  // With no lane left no fill comes to let go of the subjects the last probes held.
531
- if (kept.length === 0)
930
+ if (kept.length === 0 && crowded === 0)
532
931
  this.subjects = [];
533
932
  this.laned = [];
534
933
  this.bySlot = host.channels.map(() => undefined);
@@ -541,6 +940,7 @@ export class Lanes {
541
940
  const ch = {
542
941
  name: host.names[slot],
543
942
  op: n.op,
943
+ scalar: typeof rest === 'number',
544
944
  rest: typeof rest === 'number' ? rest : rest[0],
545
945
  axes: n.axes,
546
946
  values: new Float64Array(this.cap * n.axes),
@@ -556,6 +956,240 @@ export class Lanes {
556
956
  lane.chans = lane.voice.slots.map((s) => this.bySlot[s]);
557
957
  lane.values = lane.chans.map(() => undefined);
558
958
  }
959
+ this.regroup(present.filter((v) => voices.has(v.id) && crowdable(v)));
960
+ }
961
+ /**
962
+ * Rebuilds the crowds from the voices that belong in one, in voice order: a voice already in a
963
+ * crowd keeps its row's numbers, a new one gets a row a probe will place, and a voice no longer in
964
+ * one is handed back to the general path, as a lane's voice is when it leaves.
965
+ */
966
+ regroup(members) {
967
+ const keep = new Set(members.map((v) => v.id));
968
+ for (const c of this.crowds)
969
+ for (let p = 0; p < c.size; p++) {
970
+ const v = c.voices[p];
971
+ if (!keep.has(v.id) || v.state === 'done')
972
+ this.leaveCrowd(c, p);
973
+ }
974
+ const old = this.crowds;
975
+ const was = new Map(this.crowdOf);
976
+ this.crowds = [];
977
+ this.crowdOf.clear();
978
+ for (const v of members) {
979
+ const from = was.get(v.id);
980
+ this.join(v, from, from?.rowOf.get(v.id), old);
981
+ }
982
+ if (this.crowds.length === 0)
983
+ this.lawsByKey.clear();
984
+ }
985
+ /**
986
+ * Gives a voice the next row of its channel's crowd, made if there is none: a copy of the row it
987
+ * had in `from` at `at`, or a new one. `old` holds the crowds being replaced, whose idle state a
988
+ * new crowd on the same channel keeps. Rows stay in voice order only because voices join in it.
989
+ */
990
+ join(v, from, at, old) {
991
+ const ch = this.bySlot[v.slots[0]];
992
+ let c = this.crowds.find((o) => o.chans[0] === ch);
993
+ if (c === undefined) {
994
+ c = new Crowd([ch], ch.axes);
995
+ const was = old.find((o) => o.chans[0]?.name === ch.name);
996
+ if (was !== undefined)
997
+ c.idle = was.idle;
998
+ this.crowds.push(c);
999
+ }
1000
+ const p = c.size;
1001
+ c.reserve(p + 1);
1002
+ if (from !== undefined && at !== undefined)
1003
+ this.copyRow(from, at, c, p);
1004
+ else
1005
+ this.newRow(c, p, v);
1006
+ if (c.hot[p * c.stride + H_EPOCH] === 0 && v.state !== 'pending')
1007
+ c.hot[p * c.stride + H_EPOCH] = this.open(v);
1008
+ c.rowOf.set(v.id, p);
1009
+ this.crowdOf.set(v.id, c);
1010
+ v.laned = true;
1011
+ }
1012
+ /**
1013
+ * A voice starts playing on a lane or crowd: its epoch. Every subject meets a voice over all of
1014
+ * them on its next probe; a voice naming its subjects marks only those.
1015
+ */
1016
+ open(v) {
1017
+ const epoch = ++this.epochs;
1018
+ if (v.named === null) {
1019
+ this.wide = epoch;
1020
+ return epoch;
1021
+ }
1022
+ for (const subject of v.named) {
1023
+ const slot = this.host.slotOf(subject);
1024
+ if (slot < 0 || slot >= this.cap)
1025
+ continue;
1026
+ const i = slot * SLOT + SEEN;
1027
+ const seen = this.per[i];
1028
+ const from = seen < 0 ? -1 - seen : seen;
1029
+ this.per[i] = -1 - Math.min(from, epoch - 1);
1030
+ }
1031
+ return epoch;
1032
+ }
1033
+ /**
1034
+ * Takes the voices `touch` named on or off their crowds, each as it now stands; false at the
1035
+ * first that cannot be, which needs a qualify: one that leaves a lane or the general path, which
1036
+ * may open a channel, or one that joins anything but a crowd on a laned channel. Neither a voice
1037
+ * that fits joining laned channels nor a laned one leaving moves the fixed point `qualify` finds.
1038
+ * False too once a crowd is more than half empty rows, which a qualify rebuilds without.
1039
+ */
1040
+ retouch() {
1041
+ const touched = [...new Set(this.touched)].sort((a, b) => a.id - b.id);
1042
+ this.touched.length = 0;
1043
+ for (const v of touched) {
1044
+ const known = v.id <= this.known;
1045
+ if (v.id > this.known)
1046
+ this.known = v.id;
1047
+ const c = this.crowdOf.get(v.id);
1048
+ const p = c?.rowOf.get(v.id);
1049
+ if (c !== undefined && p !== undefined) {
1050
+ if (v.state === 'done') {
1051
+ this.bury(c, p);
1052
+ continue;
1053
+ }
1054
+ const h = p * c.stride;
1055
+ if (c.hot[h + H_EPOCH] === 0 && v.state !== 'pending')
1056
+ c.hot[h + H_EPOCH] = this.open(v);
1057
+ c.hot[h + H_FLAGS] = c.hot[h + H_FLAGS] | F_VOICE;
1058
+ continue;
1059
+ }
1060
+ if (v.state === 'done' && !known)
1061
+ continue;
1062
+ if (v.state === 'done' || this.byId.has(v.id))
1063
+ return false;
1064
+ const laned = v.slots.some((slot) => this.bySlot[slot] !== undefined);
1065
+ const fits = v.slots.length > 0 && this.host.fits(v);
1066
+ if (!laned && !fits) {
1067
+ this.whole = false;
1068
+ continue;
1069
+ }
1070
+ if (known ||
1071
+ !fits ||
1072
+ !laned ||
1073
+ !crowdable(v) ||
1074
+ this.bySlot[v.slots[0]] === undefined)
1075
+ return false;
1076
+ this.join(v, undefined, undefined, this.crowds);
1077
+ }
1078
+ return !this.crowds.some((c) => c.dead > 64 && c.dead * 2 > c.size);
1079
+ }
1080
+ /** A crowd voice left: its row stays, empty, until the crowds are next rebuilt. */
1081
+ bury(c, p) {
1082
+ const v = c.voices[p];
1083
+ this.leaveCrowd(c, p);
1084
+ c.list[p] = -1;
1085
+ c.records[p] = undefined;
1086
+ c.deltas[p] = null;
1087
+ c.motions[p] = undefined;
1088
+ c.hot[p * c.stride + H_FLAGS] = 0;
1089
+ c.rowOf.delete(v.id);
1090
+ this.crowdOf.delete(v.id);
1091
+ c.dead++;
1092
+ }
1093
+ copyRow(from, p, to, q) {
1094
+ to.list[q] = from.list[p];
1095
+ to.data.set(from.data.subarray(p * STRIDE, (p + 1) * STRIDE), q * STRIDE);
1096
+ to.hot.set(from.hot.subarray(p * from.stride, (p + 1) * from.stride), q * to.stride);
1097
+ to.samples.set(from.samples.subarray(p * from.axes, (p + 1) * from.axes), q * to.axes);
1098
+ to.deltas[q] = from.deltas[p] ?? null;
1099
+ to.records[q] = from.records[p];
1100
+ to.voices[q] = from.voices[p];
1101
+ to.motions[q] = from.motions[p];
1102
+ to.eases[q] = from.eases[p];
1103
+ to.laws[q] = from.laws[p];
1104
+ to.touches[q] = from.touches[p];
1105
+ const odd = from.odd?.get(p);
1106
+ if (odd !== undefined) {
1107
+ to.odd ??= new Map();
1108
+ to.odd.set(q, odd);
1109
+ }
1110
+ }
1111
+ newRow(c, p, v) {
1112
+ const run = motionOf(v.patch);
1113
+ const h = p * c.stride;
1114
+ c.hot.fill(0, h, h + c.stride);
1115
+ c.hot[h + H_FLAGS] = F_VOICE | F_STALE | (run === undefined ? 0 : F_MOTION);
1116
+ c.hot[h + H_ID] = v.id;
1117
+ c.list[p] = -1;
1118
+ const o = p * STRIDE;
1119
+ c.data.fill(0, o, o + STRIDE);
1120
+ c.data[o + MSLOT] = -1;
1121
+ c.data[o + SAMPLED] = -1;
1122
+ c.deltas[p] = null;
1123
+ c.records[p] = undefined;
1124
+ c.voices[p] = v;
1125
+ c.motions[p] = run;
1126
+ if (run === undefined) {
1127
+ c.laws[p] = none;
1128
+ c.eases[p] = undefined;
1129
+ c.touches[p] = noTouch;
1130
+ return;
1131
+ }
1132
+ c.laws[p] = this.lawOf(run);
1133
+ c.eases[p] = run.ease;
1134
+ const id = v.id;
1135
+ const touch = () => this.touchedCrowd(id);
1136
+ run.touched = touch;
1137
+ c.touches[p] = touch;
1138
+ }
1139
+ /** A patch's law, as the one array every crowd row with the same law shares. */
1140
+ lawOf(run) {
1141
+ const law = run.law();
1142
+ const key = law.join(' ');
1143
+ const known = this.lawsByKey.get(key);
1144
+ if (known !== undefined)
1145
+ return known;
1146
+ this.lawsByKey.set(key, law);
1147
+ return law;
1148
+ }
1149
+ /** A crowd voice's patch changed a stretch: its copy is read again at the next fill. */
1150
+ touchedCrowd(id) {
1151
+ const c = this.crowdOf.get(id);
1152
+ const p = c?.rowOf.get(id);
1153
+ if (c === undefined || p === undefined)
1154
+ return;
1155
+ const f = p * c.stride + H_FLAGS;
1156
+ c.hot[f] = c.hot[f] | F_STALE;
1157
+ }
1158
+ /** Something on a voice changed: a crowd copies it again at the next fill. */
1159
+ voiceChanged(id) {
1160
+ const c = this.crowdOf.get(id);
1161
+ const p = c?.rowOf.get(id);
1162
+ if (c === undefined || p === undefined)
1163
+ return;
1164
+ const f = p * c.stride + H_FLAGS;
1165
+ c.hot[f] = c.hot[f] | F_VOICE;
1166
+ }
1167
+ /** Hands a crowd voice back to the general path, as `leave` does a lane's. */
1168
+ leaveCrowd(c, p) {
1169
+ const slot = c.list[p];
1170
+ const rec = c.records[p];
1171
+ if (slot >= 0) {
1172
+ if (c.idle)
1173
+ this.reach(slot, -1);
1174
+ const w = this.reportedRow(c, p, slot);
1175
+ if (rec !== undefined && w !== undefined)
1176
+ rec.weight = w;
1177
+ if (rec !== undefined && c.motions[p] !== undefined)
1178
+ this.settle(c, p, slot, rec);
1179
+ }
1180
+ const run = c.motions[p];
1181
+ if (run !== undefined && run.touched === c.touches[p])
1182
+ run.touched = noTouch;
1183
+ c.voices[p].laned = false;
1184
+ }
1185
+ /** A crowd row's weight as `reported` reads a lane position's. */
1186
+ reportedRow(c, p, slot) {
1187
+ const o = slot * SLOT;
1188
+ if (!(this.per[o + LANE_PROBE] > this.per[o + GENERAL_PROBE]))
1189
+ return undefined;
1190
+ if (c.list[p] !== slot)
1191
+ return undefined;
1192
+ return c.data[p * STRIDE + (this.per[o + LANE_FILL] === this.fills ? WEIGHT : PROBED)];
559
1193
  }
560
1194
  /** Hands a voice back to the general path, with the weights `weightOf` reports kept on its records. */
561
1195
  leave(lane) {
@@ -573,6 +1207,7 @@ export class Lanes {
573
1207
  lane.voice.laned = false;
574
1208
  }
575
1209
  fillAll(now, version) {
1210
+ this.flush();
576
1211
  const host = this.host;
577
1212
  this.filling = true;
578
1213
  try {
@@ -585,7 +1220,9 @@ export class Lanes {
585
1220
  const decide = this.fresh;
586
1221
  this.fresh = false;
587
1222
  const share = this.live > 0 ? this.lastDistinct / this.live : 1;
588
- // One pass, deciding and running each lane, since a mix of many small lanes pays per visit.
1223
+ // Deciding which lanes and crowds go idle first, then running them, in voice order: a crowd's
1224
+ // rows are folded between the lanes on either side of their voices, as the general path
1225
+ // folds every voice in order.
589
1226
  let busy = false;
590
1227
  for (const lane of this.runs) {
591
1228
  if (decide) {
@@ -602,14 +1239,32 @@ export class Lanes {
602
1239
  else if (lane.idle ? share >= line.start : share < line.stop)
603
1240
  this.flip(lane);
604
1241
  }
605
- if (lane.idle)
606
- continue;
607
- if (!busy) {
1242
+ if (!lane.idle)
608
1243
  busy = true;
609
- for (const ch of this.laned)
610
- ch.values.fill(ch.rest, 0, size * ch.axes);
1244
+ }
1245
+ for (const c of this.crowds) {
1246
+ if (decide) {
1247
+ if (c.size >= 64)
1248
+ this.pace(c);
1249
+ else if (c.idle ? share >= c.line.start : share < c.line.stop)
1250
+ this.flip(c);
611
1251
  }
612
- this.run(lane);
1252
+ c.cursor = 0;
1253
+ if (!c.idle && c.size > 0)
1254
+ busy = true;
1255
+ }
1256
+ if (busy) {
1257
+ for (const ch of this.laned)
1258
+ ch.values.fill(ch.rest, 0, size * ch.axes);
1259
+ for (const lane of this.runs) {
1260
+ if (lane.idle)
1261
+ continue;
1262
+ if (this.crowds.length > 0)
1263
+ this.crowdsUpTo(lane.voice.id);
1264
+ this.run(lane);
1265
+ }
1266
+ if (this.crowds.length > 0)
1267
+ this.crowdsUpTo(Number.POSITIVE_INFINITY);
613
1268
  }
614
1269
  // With every lane idle there was nothing to fill, and no subject reads a lane this frame.
615
1270
  if (busy)
@@ -644,13 +1299,15 @@ export class Lanes {
644
1299
  let probed = 0;
645
1300
  for (let p = this.fills % step; p < list.length; p += step) {
646
1301
  const slot = list[p];
1302
+ if (slot < 0)
1303
+ continue;
647
1304
  const a = this.per[slot * SLOT + LANE_PROBE];
648
1305
  const b = this.per[slot * SLOT + GENERAL_PROBE];
649
1306
  if ((a > from && a <= to) || (b > from && b <= to))
650
1307
  probed++;
651
1308
  looked++;
652
1309
  }
653
- const share = probed / looked;
1310
+ const share = looked > 0 ? probed / looked : 1;
654
1311
  if (lane.idle ? share >= line.start : share < line.stop)
655
1312
  this.flip(lane);
656
1313
  }
@@ -667,6 +1324,8 @@ export class Lanes {
667
1324
  const data = lane.data;
668
1325
  for (let p = 0; p < list.length; p++) {
669
1326
  const slot = list[p];
1327
+ if (slot < 0)
1328
+ continue;
670
1329
  this.reach(slot, 1);
671
1330
  // A probe read this subject from the last fill, which this lane's weight stays from.
672
1331
  if (this.per[slot * SLOT + LANE_FILL] === this.fills - 1) {
@@ -678,7 +1337,8 @@ export class Lanes {
678
1337
  wake(lane) {
679
1338
  lane.idle = false;
680
1339
  for (const slot of lane.list)
681
- this.reach(slot, -1);
1340
+ if (slot >= 0)
1341
+ this.reach(slot, -1);
682
1342
  }
683
1343
  subjectAt(slot) {
684
1344
  let subject = this.subjects[slot];
@@ -692,7 +1352,7 @@ export class Lanes {
692
1352
  /** One voice's contribution to every subject it plays on. */
693
1353
  run(lane) {
694
1354
  const voice = lane.voice;
695
- if (voice.state !== 'live' && voice.state !== 'fading')
1355
+ if (voice.state === 'pending' || voice.state === 'done')
696
1356
  return;
697
1357
  lane.placed = false;
698
1358
  lane.read = false;
@@ -700,6 +1360,10 @@ export class Lanes {
700
1360
  // With no fade in or out the envelope is 1 for every subject, which is what it would return.
701
1361
  lane.flat = !((voice.fade.in ?? 0) > 0) && voice.out === null;
702
1362
  lane.fade = 1;
1363
+ if (lane.motion !== undefined && !voice.keeping) {
1364
+ this.runMotion(lane, lane.motion);
1365
+ return;
1366
+ }
703
1367
  const elapsed = voice.elapsedAt(this.now);
704
1368
  const period = voice.patch.period;
705
1369
  const passes = voice.passes;
@@ -720,8 +1384,8 @@ export class Lanes {
720
1384
  if (this.per[slot * SLOT + LANE_FILL] === this.fills - 1)
721
1385
  data[o + PROBED] = data[o + WEIGHT];
722
1386
  const delay = data[o + DELAY];
723
- const elapsed = elapsedNow - delay;
724
- if (elapsed < 0) {
1387
+ const elapsed = held(voice, elapsedNow - delay);
1388
+ if (!(elapsed >= 0)) {
725
1389
  data[o + WEIGHT] = 0;
726
1390
  return;
727
1391
  }
@@ -737,11 +1401,16 @@ export class Lanes {
737
1401
  lane.weighed = true;
738
1402
  lane.weighedSince = since;
739
1403
  }
740
- const w = clampWeight(voice.weight * lane.fade);
1404
+ let w = clampWeight(voice.weight * lane.fade);
1405
+ if (voice.parts !== null) {
1406
+ const subject = this.subjectAt(slot);
1407
+ if (subject !== absent)
1408
+ w *= host.parting(voice, subject);
1409
+ }
741
1410
  data[o + WEIGHT] = w;
742
1411
  if (voice.built !== null) {
743
1412
  if (!lane.read || !Object.is(elapsed, lane.readAt)) {
744
- readKeys(voice.built, lane.phase, lane.delta, undefined, voice.lerps, undefined, voice.intos, lane.scratch);
1413
+ this.readKeyed(voice, lane.phase, lane.delta, lane.scratch);
745
1414
  lane.read = true;
746
1415
  lane.readAt = elapsed;
747
1416
  this.gather(lane, lane.delta);
@@ -750,39 +1419,302 @@ export class Lanes {
750
1419
  this.fold(lane, slot, w);
751
1420
  return;
752
1421
  }
753
- const subject = this.subjectAt(slot);
754
- if (subject === absent)
755
- return;
756
- const held = lane.records[p];
1422
+ const rec = lane.records[p];
757
1423
  // The general path makes a voice's first call for a subject, where it first sees it play.
758
- if (Number.isNaN(held.probed)) {
1424
+ if (Number.isNaN(rec.probed)) {
759
1425
  this.late.push(slot);
760
1426
  return;
761
1427
  }
1428
+ // A motion patch needs the subject only to number it, so the lookup waits until then.
762
1429
  if (lane.motion !== undefined) {
763
- this.move(lane, lane.motion, p, slot, subject, held, elapsed, delay, w);
1430
+ this.move(lane, lane.motion, p, slot, rec, elapsed, delay, w);
764
1431
  return;
765
1432
  }
1433
+ this.call(voice, lane.chans, rec, slot, elapsed, lane.phase, lane.pass, delay, w);
1434
+ }
1435
+ /** Reads a keys voice's stops at `phase` into `delta`, interpolating into the reader's `scratch`. */
1436
+ readKeyed(voice, phase, delta, scratch) {
1437
+ readKeys(voice.built, phase, delta, undefined, voice.lerps, undefined, voice.intos, scratch);
1438
+ }
1439
+ /** Calls a stateless fn voice's patch for a subject its general path has met, and folds the delta. */
1440
+ call(voice, chans, rec, slot, elapsed, phase, pass, delay, w) {
1441
+ const host = this.host;
1442
+ const subject = this.subjectAt(slot);
1443
+ if (subject === absent)
1444
+ return;
766
1445
  // Called already this frame, by the general path or a fill before a refill: reuse, as a probe does.
767
- if (held.probed === this.now && held.delta !== null && held.seeks === voice.seeks) {
1446
+ if (rec.probed === this.now && rec.delta !== null && rec.seeks === voice.seeks) {
768
1447
  if (w > 0)
769
- this.foldDelta(lane, slot, held.delta, w);
1448
+ this.foldDelta(chans, slot, rec.delta, w);
770
1449
  return;
771
1450
  }
772
1451
  const kept = reading.kept;
773
- host.ready(voice, subject, held, elapsed, lane.pass, w);
1452
+ host.ready(voice, subject, rec, elapsed, pass, w);
774
1453
  host.horizon(voice, delay);
775
- const delta = voice.patch.at(lane.phase, subject, voice.setting);
1454
+ const delta = voice.patch.at(phase, subject, voice.setting);
776
1455
  // What `influence` leaves on the record, so a probe on the general path this frame reuses it.
777
- held.delta = delta;
778
- held.probed = this.now;
779
- held.seeks = voice.seeks;
1456
+ rec.delta = delta;
1457
+ rec.probed = this.now;
1458
+ rec.seeks = voice.seeks;
780
1459
  if (this.keeps)
781
- host.after(voice, held);
1460
+ host.after(voice, rec);
782
1461
  if (reading.kept !== kept && !voice.keeping)
783
1462
  host.kept(voice);
784
1463
  if (w > 0)
785
- this.foldDelta(lane, slot, delta, w);
1464
+ this.foldDelta(chans, slot, delta, w);
1465
+ }
1466
+ /** Runs every crowd's rows whose voices come before voice `id`, from where each crowd reached. */
1467
+ crowdsUpTo(id) {
1468
+ for (const c of this.crowds)
1469
+ if (!c.idle && c.cursor < c.size)
1470
+ this.runCrowd(c, id);
1471
+ }
1472
+ /**
1473
+ * A crowd's rows from its cursor up to voice `id`: what `runMotion` does for a lane's subjects,
1474
+ * reading each row's voice and stretch from `hot`, and through `move` for whatever `hot` cannot
1475
+ * answer, as `runMotion` does.
1476
+ */
1477
+ runCrowd(c, id) {
1478
+ const data = c.data;
1479
+ const hot = c.hot;
1480
+ const H = c.stride;
1481
+ const per = this.per;
1482
+ const list = c.list;
1483
+ const ch = c.chans[0];
1484
+ const n = c.axes;
1485
+ const fills = this.fills;
1486
+ const from = this.frameProbes;
1487
+ const probed = this.probes !== from;
1488
+ const now = this.now;
1489
+ const xs = c.xs;
1490
+ const vs = c.vs;
1491
+ const samples = c.samples;
1492
+ let p = c.cursor;
1493
+ for (; p < list.length; p++) {
1494
+ const h = p * H;
1495
+ if (hot[h + H_ID] >= id)
1496
+ break;
1497
+ let f = hot[h + H_FLAGS];
1498
+ if ((f & F_VOICE) !== 0)
1499
+ f = this.copyVoice(c, p);
1500
+ if ((f & F_PLAYING) === 0 || (f & F_PLACED) === 0)
1501
+ continue;
1502
+ const slot = list[p];
1503
+ const o = p * STRIDE;
1504
+ const q = slot * SLOT;
1505
+ if (per[q + LANE_FILL] === fills - 1)
1506
+ data[o + PROBED] = data[o + WEIGHT];
1507
+ const delay = data[o + DELAY];
1508
+ const fast = (f & F_FAST) !== 0;
1509
+ const voice = c.voices[p];
1510
+ const elapsedNow = fast
1511
+ ? hot[h + H_ELAPSED] +
1512
+ (now - hot[h + H_NOW]) * hot[h + H_RATE]
1513
+ : voice.elapsedAt(now);
1514
+ let elapsed = elapsedNow - delay;
1515
+ if ((f & F_HOLDS) !== 0)
1516
+ elapsed = held(voice, elapsed);
1517
+ if (!(elapsed >= 0)) {
1518
+ data[o + WEIGHT] = 0;
1519
+ continue;
1520
+ }
1521
+ let w;
1522
+ if (fast)
1523
+ w = hot[h + H_WEIGHT];
1524
+ else {
1525
+ w = clampWeight(voice.weight * this.host.envelope(voice, data[o + SINCE]));
1526
+ if (voice.parts !== null) {
1527
+ const subject = this.subjectAt(slot);
1528
+ if (subject !== absent)
1529
+ w *= this.host.parting(voice, subject);
1530
+ }
1531
+ }
1532
+ data[o + WEIGHT] = w;
1533
+ if ((f & F_MOTION) === 0) {
1534
+ this.row(c, p, voice, slot, elapsed, delay, w);
1535
+ continue;
1536
+ }
1537
+ if (data[o + MET] === 0) {
1538
+ if (Number.isNaN(c.records[p].probed)) {
1539
+ this.late.push(slot);
1540
+ continue;
1541
+ }
1542
+ data[o + MET] = 1;
1543
+ }
1544
+ const ms = data[o + MSLOT];
1545
+ if ((f & F_STALE) !== 0 && ms >= 0)
1546
+ f = this.copyStretch(c, p, ms);
1547
+ if (ms < 0 ||
1548
+ (f & F_BARE) === 0 ||
1549
+ (probed &&
1550
+ (per[q + LANE_PROBE] > from || per[q + GENERAL_PROBE] > from))) {
1551
+ const run = c.motions[p];
1552
+ this.move(c, run, p, slot, c.records[p], elapsed, delay, w);
1553
+ // The first sample numbered the subject in the patch: copy its stretch from the next fill.
1554
+ if (ms < 0)
1555
+ hot[h + H_FLAGS] = hot[h + H_FLAGS] | F_STALE;
1556
+ continue;
1557
+ }
1558
+ closed(c.laws[p], c.eases[p], n, hot[h + H_AT], hot[h + H_MS], hot, h + H_X0, hot, h + H_X0 + n, hot, h + H_X0 + 2 * n, elapsed, xs, vs);
1559
+ for (let a = 0; a < n; a++)
1560
+ samples[p * n + a] = xs[a];
1561
+ if (c.deltas[p] !== null)
1562
+ c.deltas[p] = null;
1563
+ data[o + SAMPLED] = fills;
1564
+ data[o + SEEKS] = hot[h + H_SEEKS];
1565
+ if (w <= 0)
1566
+ continue;
1567
+ if ((f & F_FOLDS) !== 0)
1568
+ this.foldRun(ch, slot, xs, w);
1569
+ else
1570
+ this.foldInto(ch, slot, c.motions[p].value(ms, xs), w);
1571
+ }
1572
+ c.cursor = p;
1573
+ }
1574
+ /** A keys or fn row's contribution, as `one` makes a lane position's. */
1575
+ row(c, p, voice, slot, elapsed, delay, w) {
1576
+ const period = voice.patch.period;
1577
+ const phase = phaseAt(elapsed, period, voice.passes);
1578
+ if (voice.built !== null) {
1579
+ if (w > 0) {
1580
+ this.readKeyed(voice, phase, c.delta, c.scratch);
1581
+ this.foldDelta(c.chans, slot, c.delta, w);
1582
+ }
1583
+ return;
1584
+ }
1585
+ const rec = c.records[p];
1586
+ if (Number.isNaN(rec.probed)) {
1587
+ this.late.push(slot);
1588
+ return;
1589
+ }
1590
+ this.call(voice, c.chans, rec, slot, elapsed, phase, passAt(elapsed, period, voice.passes), delay, w);
1591
+ }
1592
+ /** Copies what a fill reads of a crowd row's voice into `hot`; returns the row's flags. */
1593
+ copyVoice(c, p) {
1594
+ const v = c.voices[p];
1595
+ const h = p * c.stride;
1596
+ let f = c.hot[h + H_FLAGS] & ~(F_VOICE | F_PLAYING | F_FAST | F_HOLDS);
1597
+ if (v.state === 'live' || v.state === 'held' || v.state === 'fading')
1598
+ f |= F_PLAYING;
1599
+ if (v.holdsBefore || v.holdsAfter)
1600
+ f |= F_HOLDS;
1601
+ if (v.ramp === null &&
1602
+ !((v.fade.in ?? 0) > 0) &&
1603
+ v.out === null &&
1604
+ v.parts === null &&
1605
+ !v.keeping)
1606
+ f |= F_FAST;
1607
+ c.hot[h + H_NOW] = v.anchorNow;
1608
+ c.hot[h + H_ELAPSED] = v.anchorElapsed;
1609
+ c.hot[h + H_RATE] = v.rate;
1610
+ c.hot[h + H_WEIGHT] = clampWeight(v.weight);
1611
+ c.hot[h + H_SEEKS] = v.seeks;
1612
+ c.hot[h + H_FLAGS] = f;
1613
+ return f;
1614
+ }
1615
+ /** Copies a crowd row's current stretch from its patch into `hot`; returns the row's flags. */
1616
+ copyStretch(c, p, ms) {
1617
+ const run = c.motions[p];
1618
+ const h = p * c.stride;
1619
+ let f = c.hot[h + H_FLAGS] & ~(F_STALE | F_BARE | F_FOLDS);
1620
+ const ch = c.chans[0];
1621
+ if (run.n === c.axes && run.stretchInto(ms, c.hot, h + H_AT)) {
1622
+ f |= F_BARE;
1623
+ if (run.scalar(ms) === ch.scalar)
1624
+ f |= F_FOLDS;
1625
+ }
1626
+ c.hot[h + H_FLAGS] = f;
1627
+ return f;
1628
+ }
1629
+ /**
1630
+ * A motion voice's fill: what `one` and `move` do for each subject, in one loop for the common
1631
+ * case, a subject not yet probed this frame with nothing pending, and through `move` for the rest.
1632
+ */
1633
+ runMotion(lane, run) {
1634
+ const voice = lane.voice;
1635
+ const data = lane.data;
1636
+ const per = this.per;
1637
+ const list = lane.list;
1638
+ const records = lane.records;
1639
+ const ch = lane.chans[0];
1640
+ const fills = this.fills;
1641
+ const from = this.frameProbes;
1642
+ const seeks = voice.seeks;
1643
+ const elapsedNow = voice.elapsedAt(this.now);
1644
+ const flat = lane.flat;
1645
+ const parts = voice.parts !== null;
1646
+ // On a frame's first fill no probe has read a subject yet, so none needs `move` for that.
1647
+ const probed = this.probes !== from;
1648
+ const whole = clampWeight(voice.weight);
1649
+ const n = run.n;
1650
+ if (n > 0) {
1651
+ if (lane.axes !== n) {
1652
+ lane.axes = n;
1653
+ lane.samples = new Float64Array(0);
1654
+ }
1655
+ if (list.length * n > lane.samples.length) {
1656
+ const grown = new Float64Array(Math.max(list.length, (lane.samples.length / n) * 2, 4) * n);
1657
+ grown.set(lane.samples);
1658
+ lane.samples = grown;
1659
+ }
1660
+ }
1661
+ const samples = lane.samples;
1662
+ const xs = run.xs;
1663
+ for (let p = 0; p < list.length; p++) {
1664
+ const slot = list[p];
1665
+ const o = p * STRIDE;
1666
+ const q = slot * SLOT;
1667
+ if (per[q + LANE_FILL] === fills - 1)
1668
+ data[o + PROBED] = data[o + WEIGHT];
1669
+ const delay = data[o + DELAY];
1670
+ const elapsed = elapsedNow - delay;
1671
+ if (elapsed < 0) {
1672
+ data[o + WEIGHT] = 0;
1673
+ continue;
1674
+ }
1675
+ if (!flat) {
1676
+ const since = data[o + SINCE];
1677
+ if (!lane.weighed || !Object.is(since, lane.weighedSince)) {
1678
+ lane.fade = this.host.envelope(voice, since);
1679
+ lane.weighed = true;
1680
+ lane.weighedSince = since;
1681
+ }
1682
+ }
1683
+ let w = flat ? whole : clampWeight(voice.weight * lane.fade);
1684
+ if (parts) {
1685
+ const subject = this.subjectAt(slot);
1686
+ if (subject !== absent)
1687
+ w *= this.host.parting(voice, subject);
1688
+ }
1689
+ data[o + WEIGHT] = w;
1690
+ if (data[o + MET] === 0) {
1691
+ if (Number.isNaN(records[p].probed)) {
1692
+ this.late.push(slot);
1693
+ continue;
1694
+ }
1695
+ data[o + MET] = 1;
1696
+ }
1697
+ const ms = data[o + MSLOT];
1698
+ if (ms < 0 ||
1699
+ (probed &&
1700
+ (per[q + LANE_PROBE] > from || per[q + GENERAL_PROBE] > from)) ||
1701
+ !run.sampleBare(ms, elapsed, xs, run.vs)) {
1702
+ this.move(lane, run, p, slot, records[p], elapsed, delay, w);
1703
+ continue;
1704
+ }
1705
+ for (let a = 0; a < n; a++)
1706
+ samples[p * n + a] = xs[a];
1707
+ if (lane.deltas[p] !== null)
1708
+ lane.deltas[p] = null;
1709
+ data[o + SAMPLED] = fills;
1710
+ data[o + SEEKS] = seeks;
1711
+ if (w <= 0)
1712
+ continue;
1713
+ if (n === ch.axes && run.scalar(ms) === ch.scalar)
1714
+ this.foldRun(ch, slot, xs, w);
1715
+ else
1716
+ this.foldInto(ch, slot, run.value(ms, xs), w);
1717
+ }
786
1718
  }
787
1719
  /**
788
1720
  * A motion voice's value for a subject, sampled from the patch's state as its `at` would, but only
@@ -790,8 +1722,8 @@ export class Lanes {
790
1722
  * general path, which samples it if and when a probe asks, as it would with no lanes. A probe
791
1723
  * this frame already read keeps the value it read, as the general path's record would.
792
1724
  */
793
- move(lane, run, p, slot, subject, held, elapsed, delay, w) {
794
- const voice = lane.voice;
1725
+ move(lane, run, p, slot, held, elapsed, delay, w) {
1726
+ const voice = lane.voiceAt(p);
795
1727
  const data = lane.data;
796
1728
  const o = p * STRIDE;
797
1729
  const ch = lane.chans[0];
@@ -803,6 +1735,9 @@ export class Lanes {
803
1735
  !this.probedThisFrame(slot)) {
804
1736
  let ms = data[o + MSLOT];
805
1737
  if (ms < 0) {
1738
+ const subject = this.subjectAt(slot);
1739
+ if (subject === absent)
1740
+ return;
806
1741
  ms = run.slot(subject);
807
1742
  data[o + MSLOT] = ms;
808
1743
  }
@@ -814,24 +1749,14 @@ export class Lanes {
814
1749
  run.sample(ms, elapsed, run.xs, run.vs);
815
1750
  // Kept as numbers, not a delta: one is built only if a probe this frame asks for it.
816
1751
  const n = run.n;
817
- if (lane.axes !== n) {
818
- lane.axes = n;
819
- lane.samples = new Float64Array(0);
820
- }
821
- if ((p + 1) * n > lane.samples.length) {
822
- const grown = new Float64Array(Math.max(p + 1, (lane.samples.length / n) * 2, 4) * n);
823
- grown.set(lane.samples);
824
- lane.samples = grown;
825
- }
826
- for (let a = 0; a < n; a++)
827
- lane.samples[p * n + a] = run.xs[a];
1752
+ lane.keep(p, run.xs, n);
828
1753
  lane.deltas[p] = null;
829
1754
  data[o + SAMPLED] = this.fills;
830
1755
  data[o + SEEKS] = voice.seeks;
831
1756
  if (w <= 0)
832
1757
  return;
833
1758
  // A sample of other axes than the channel's folds as the general path folds it.
834
- if (n === ch.axes && run.scalar(ms) === (ch.axes === 1))
1759
+ if (n === ch.axes && run.scalar(ms) === ch.scalar)
835
1760
  this.foldRun(ch, slot, run.xs, w);
836
1761
  else
837
1762
  this.foldInto(ch, slot, run.value(ms, run.xs), w);
@@ -845,10 +1770,8 @@ export class Lanes {
845
1770
  }
846
1771
  /** The delta a motion voice's last sample for position `p` made, built from `samples`. */
847
1772
  sampled(lane, p) {
848
- const run = lane.motion;
849
- const n = lane.axes;
850
- for (let a = 0; a < n; a++)
851
- run.xs[a] = lane.samples[p * n + a];
1773
+ const run = lane.motionAt(p);
1774
+ lane.kept(p, run.xs);
852
1775
  const ms = lane.data[p * STRIDE + MSLOT];
853
1776
  const delta = { [lane.chans[0].name]: run.value(ms, run.xs) };
854
1777
  lane.deltas[p] = delta;
@@ -885,8 +1808,7 @@ export class Lanes {
885
1808
  * at a site of their own, which in most mixes sees one channel name and stays fast, where one
886
1809
  * site reading every name in turn slows every read.
887
1810
  */
888
- foldDelta(lane, slot, delta, w) {
889
- const chans = lane.chans;
1811
+ foldDelta(chans, slot, delta, w) {
890
1812
  const n = chans.length;
891
1813
  let ch = chans[0];
892
1814
  if (n > 0)
@@ -913,7 +1835,7 @@ export class Lanes {
913
1835
  if (value === undefined)
914
1836
  return;
915
1837
  const values = ch.values;
916
- if (ch.axes === 1) {
1838
+ if (ch.scalar) {
917
1839
  values[slot] = foldNumber(ch.op, values[slot], value, w);
918
1840
  return;
919
1841
  }