@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/CHANGELOG.md +52 -0
- package/README.md +2 -2
- package/dist/lanes.d.ts +149 -3
- package/dist/lanes.js +1031 -109
- package/dist/mixer.d.ts +29 -2
- package/dist/mixer.js +588 -82
- package/dist/motion.d.ts +46 -6
- package/dist/motion.js +173 -65
- package/dist/types.d.ts +46 -10
- package/docs/schema.html +37 -10
- package/package.json +1 -1
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 =
|
|
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.
|
|
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] =
|
|
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
|
-
|
|
309
|
-
if (lane === undefined || slot < 0)
|
|
541
|
+
if (slot < 0)
|
|
310
542
|
return undefined;
|
|
311
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
676
|
+
return GENERAL;
|
|
360
677
|
this.fillAll(now, version);
|
|
361
678
|
}
|
|
362
679
|
}
|
|
363
|
-
|
|
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
|
-
*
|
|
397
|
-
* the lanes while every voice is on one
|
|
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
|
-
|
|
400
|
-
|
|
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
|
|
406
|
-
const ch = bySlot[c.slot];
|
|
735
|
+
const ch = this.bySlot[c.slot];
|
|
407
736
|
if (ch === undefined) {
|
|
408
|
-
for (let
|
|
409
|
-
|
|
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
|
-
|
|
414
|
-
|
|
415
|
-
out
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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
|
|
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
|
-
|
|
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 &&
|
|
518
|
-
lane.epoch =
|
|
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
|
-
//
|
|
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
|
-
|
|
610
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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,
|
|
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 (
|
|
1446
|
+
if (rec.probed === this.now && rec.delta !== null && rec.seeks === voice.seeks) {
|
|
768
1447
|
if (w > 0)
|
|
769
|
-
this.foldDelta(
|
|
1448
|
+
this.foldDelta(chans, slot, rec.delta, w);
|
|
770
1449
|
return;
|
|
771
1450
|
}
|
|
772
1451
|
const kept = reading.kept;
|
|
773
|
-
host.ready(voice, subject,
|
|
1452
|
+
host.ready(voice, subject, rec, elapsed, pass, w);
|
|
774
1453
|
host.horizon(voice, delay);
|
|
775
|
-
const delta = voice.patch.at(
|
|
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
|
-
|
|
778
|
-
|
|
779
|
-
|
|
1456
|
+
rec.delta = delta;
|
|
1457
|
+
rec.probed = this.now;
|
|
1458
|
+
rec.seeks = voice.seeks;
|
|
780
1459
|
if (this.keeps)
|
|
781
|
-
host.after(voice,
|
|
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(
|
|
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,
|
|
794
|
-
const voice = lane.
|
|
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
|
-
|
|
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) ===
|
|
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.
|
|
849
|
-
|
|
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(
|
|
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.
|
|
1838
|
+
if (ch.scalar) {
|
|
917
1839
|
values[slot] = foldNumber(ch.op, values[slot], value, w);
|
|
918
1840
|
return;
|
|
919
1841
|
}
|