@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/mixer.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { clampWeight, envelope, passAt, passesOf, phaseAt } from './clock.js';
|
|
2
2
|
import { curve } from './easing.js';
|
|
3
3
|
import { clampRun, Lanes } from './lanes.js';
|
|
4
|
-
import { motionOf, noFrame } from './motion.js';
|
|
4
|
+
import { motionOf, noFrame, noRevive } from './motion.js';
|
|
5
5
|
import { builtOf, intosOf, readKeys } from './patch.js';
|
|
6
6
|
import { reading } from './reading.js';
|
|
7
7
|
import { Store } from './store.js';
|
|
@@ -21,6 +21,7 @@ function stub() {
|
|
|
21
21
|
reaches: false,
|
|
22
22
|
delay: 0,
|
|
23
23
|
since: 0,
|
|
24
|
+
shown: 0,
|
|
24
25
|
weight: 0,
|
|
25
26
|
rested: false,
|
|
26
27
|
bands: new Uint8Array(0),
|
|
@@ -171,9 +172,28 @@ export class Voice {
|
|
|
171
172
|
keeping = false;
|
|
172
173
|
/** For a motion patch, the hook it was given to ask the mix for its subjects' voice time. */
|
|
173
174
|
frame = null;
|
|
175
|
+
/** For a motion patch, the hook it was given to bring a subject faded out of this voice back. */
|
|
176
|
+
revive = null;
|
|
177
|
+
/** Subjects fading out of this voice alone, by the ramp each started; null while none are. */
|
|
178
|
+
parts = null;
|
|
179
|
+
/** Subjects faded out of this voice, by the mix time each left at; null while none have. */
|
|
180
|
+
parted = null;
|
|
174
181
|
ease;
|
|
175
182
|
/** How many passes its `loop` plays, worked out once. */
|
|
176
183
|
passes;
|
|
184
|
+
/** Voice ms its passes last, Infinity for an aperiodic patch or a loop for good. */
|
|
185
|
+
span;
|
|
186
|
+
/** The state behind a motion patch, undefined for any other. */
|
|
187
|
+
motion;
|
|
188
|
+
/** Its `hold`, for a patch it applies to: motion holds its target already. */
|
|
189
|
+
holdsBefore;
|
|
190
|
+
holdsAfter;
|
|
191
|
+
/** The mix time it first showed: when it was cued, or the first sync after. */
|
|
192
|
+
opened = Number.NaN;
|
|
193
|
+
/** Which of its entries in the mix's due queue is current; older ones are skipped when popped. */
|
|
194
|
+
dueToken = 0;
|
|
195
|
+
/** Records made while it was pending, whose `since` its start may since have moved. */
|
|
196
|
+
early = null;
|
|
177
197
|
/** Reused for every call this voice makes, so it is valid only during the call. */
|
|
178
198
|
setting;
|
|
179
199
|
/** Every subject this voice has been asked about, so a handover knows when it is finished. */
|
|
@@ -193,6 +213,8 @@ export class Voice {
|
|
|
193
213
|
placing = false;
|
|
194
214
|
resolve;
|
|
195
215
|
done;
|
|
216
|
+
play;
|
|
217
|
+
played;
|
|
196
218
|
constructor(id, spec, patch, fade, now, start, slotOf, channels, host, send) {
|
|
197
219
|
this.id = id;
|
|
198
220
|
this.spec = spec;
|
|
@@ -208,6 +230,14 @@ export class Voice {
|
|
|
208
230
|
: undefined;
|
|
209
231
|
this.ease = fade.ease === undefined ? undefined : curve(fade.ease);
|
|
210
232
|
this.passes = passesOf(spec.loop);
|
|
233
|
+
this.span =
|
|
234
|
+
patch.period > 0 && Number.isFinite(this.passes)
|
|
235
|
+
? patch.period * this.passes
|
|
236
|
+
: Number.POSITIVE_INFINITY;
|
|
237
|
+
this.motion = motionOf(patch);
|
|
238
|
+
const holds = this.motion === undefined ? spec.hold : undefined;
|
|
239
|
+
this.holdsBefore = holds === 'before' || holds === 'both';
|
|
240
|
+
this.holdsAfter = holds === 'after' || holds === 'both';
|
|
211
241
|
this.setting = {
|
|
212
242
|
timestamp: 0,
|
|
213
243
|
dt: 0,
|
|
@@ -225,6 +255,9 @@ export class Voice {
|
|
|
225
255
|
this.done = new Promise((r) => {
|
|
226
256
|
this.resolve = r;
|
|
227
257
|
});
|
|
258
|
+
this.played = new Promise((r) => {
|
|
259
|
+
this.play = r;
|
|
260
|
+
});
|
|
228
261
|
if (now >= start)
|
|
229
262
|
this.state = 'live';
|
|
230
263
|
}
|
|
@@ -283,6 +316,7 @@ export class Voice {
|
|
|
283
316
|
v.holder = null;
|
|
284
317
|
v.laned = false;
|
|
285
318
|
v.log = null;
|
|
319
|
+
v.early = null;
|
|
286
320
|
if (controls) {
|
|
287
321
|
v.anchorNow = controls.anchorNow;
|
|
288
322
|
v.anchorElapsed = controls.anchorElapsed;
|
|
@@ -294,6 +328,7 @@ export class Voice {
|
|
|
294
328
|
v.outAt = controls.outAt;
|
|
295
329
|
}
|
|
296
330
|
v.resolve = noSend;
|
|
331
|
+
v.play = noSend;
|
|
297
332
|
return v;
|
|
298
333
|
}
|
|
299
334
|
rateAt(now) {
|
|
@@ -349,8 +384,25 @@ class Mixer {
|
|
|
349
384
|
wantsPose = false;
|
|
350
385
|
/** The pose `pull` folds a subject into where it cannot read straight from the lanes. */
|
|
351
386
|
scratch;
|
|
387
|
+
/** The last array `pull` read, by position, with each subject's chain head, to skip the lookup. */
|
|
388
|
+
/** Live voices by the earliest mix time `moveTo` would change each, a binary heap. */
|
|
389
|
+
due = [];
|
|
390
|
+
/** Voices retired since `moveTo` last pruned them. */
|
|
391
|
+
retired = 0;
|
|
392
|
+
/** Visit every voice each sync, as before the due queue; for tests that compare the two. */
|
|
393
|
+
walkAll = false;
|
|
394
|
+
pulled = [];
|
|
395
|
+
pulledHeads = [];
|
|
396
|
+
/** Each remembered head's lane slot, and the `version` and `relinks` they were all current at. */
|
|
397
|
+
pulledSlots = new Int32Array(0);
|
|
398
|
+
pulledVersion = Number.NaN;
|
|
399
|
+
pulledRelinks = -1;
|
|
400
|
+
/** Counts chains made stale one subject at a time, which `version` does not. */
|
|
401
|
+
relinks = 0;
|
|
352
402
|
/** Read once a sync, so a `reduce` function is not called per voice per subject. */
|
|
353
403
|
reducedNow = false;
|
|
404
|
+
/** Something was changed since the last sync, so the next frame may differ from this one. */
|
|
405
|
+
stirred = false;
|
|
354
406
|
/**
|
|
355
407
|
* Per subject, the first record of the chain through every live voice that reaches it, or a stub
|
|
356
408
|
* where none does. Relinked when `version` moves, which is whenever the list or a voice's pending
|
|
@@ -360,6 +412,17 @@ class Mixer {
|
|
|
360
412
|
version = 0;
|
|
361
413
|
/** Per subject, the voices whose `subjects` name it, in voice order. */
|
|
362
414
|
named = new Store();
|
|
415
|
+
/** Voices a subject has been faded out of, which `drop` looks in besides those that reach it. */
|
|
416
|
+
parters = new Set();
|
|
417
|
+
/**
|
|
418
|
+
* Motion patches that may hold state for a subject no voice still reaching it plays, which `drop`
|
|
419
|
+
* frees besides those voices' own: per subject, a patch a voice naming it left while another
|
|
420
|
+
* voice plays on, or that the host retargeted it on; and any patch a voice over every subject
|
|
421
|
+
* left while another plays on. How many live voices play each motion patch says which.
|
|
422
|
+
*/
|
|
423
|
+
strays = new Store();
|
|
424
|
+
strayAll = new Set();
|
|
425
|
+
playing = new Map();
|
|
363
426
|
/** How many voices in the list carry `subjects`. */
|
|
364
427
|
naming = 0;
|
|
365
428
|
/** The voices in the list that name no subjects, in voice order. */
|
|
@@ -447,17 +510,23 @@ class Mixer {
|
|
|
447
510
|
? 0
|
|
448
511
|
: this.now;
|
|
449
512
|
const voice = new Voice(this.nextId++, spec, patch, spec.fade ?? {}, this.now, start, this.slotOf, this.channels, this.opts.host, this.send);
|
|
450
|
-
if (!Number.isNaN(this.now))
|
|
513
|
+
if (!Number.isNaN(this.now)) {
|
|
451
514
|
voice.cuedAt = this.now;
|
|
515
|
+
voice.opened = this.now;
|
|
516
|
+
}
|
|
452
517
|
voice.placing = anchored;
|
|
453
|
-
const motion =
|
|
518
|
+
const motion = voice.motion;
|
|
454
519
|
if (motion !== undefined) {
|
|
520
|
+
this.playing.set(motion, (this.playing.get(motion) ?? 0) + 1);
|
|
455
521
|
voice.frame = Mixer.frameHook(new WeakRef(this), new WeakRef(voice));
|
|
456
522
|
motion.frame = voice.frame;
|
|
523
|
+
voice.revive = Mixer.reviveHook(new WeakRef(this), new WeakRef(voice));
|
|
524
|
+
motion.revive = voice.revive;
|
|
457
525
|
}
|
|
458
526
|
this.voices.push(voice);
|
|
459
527
|
this.index(voice);
|
|
460
|
-
this.
|
|
528
|
+
this.changed(voice);
|
|
529
|
+
this.stirred = true;
|
|
461
530
|
if (spec.locus !== undefined)
|
|
462
531
|
this.loci++;
|
|
463
532
|
if (spec.from === 'current')
|
|
@@ -467,7 +536,8 @@ class Mixer {
|
|
|
467
536
|
this.place();
|
|
468
537
|
if (!Number.isNaN(this.now) && voice.state === 'pending' && this.now >= voice.start) {
|
|
469
538
|
voice.state = 'live';
|
|
470
|
-
this.
|
|
539
|
+
this.started(voice);
|
|
540
|
+
this.changed(voice);
|
|
471
541
|
}
|
|
472
542
|
}
|
|
473
543
|
// After placing, so the controls it starts with are where its anchors put it at the cue.
|
|
@@ -475,6 +545,7 @@ class Mixer {
|
|
|
475
545
|
voice.log = [];
|
|
476
546
|
voice.note(Number.NEGATIVE_INFINITY);
|
|
477
547
|
}
|
|
548
|
+
this.schedule(voice);
|
|
478
549
|
return this.handle(voice);
|
|
479
550
|
}
|
|
480
551
|
blend(patches, by, spec = {}) {
|
|
@@ -523,24 +594,41 @@ class Mixer {
|
|
|
523
594
|
const reach = now - (this.opts.history?.ms ?? 0);
|
|
524
595
|
this.announced = this.announced.filter((a) => a.at >= reach);
|
|
525
596
|
}
|
|
526
|
-
|
|
597
|
+
// A live mix visits only the voices due by now; a projection, which copies few, visits all.
|
|
598
|
+
const visit = this.projecting || this.walkAll ? this.voices : this.popDue(now);
|
|
599
|
+
for (const voice of visit) {
|
|
527
600
|
if (voice.state === 'done')
|
|
528
601
|
continue;
|
|
529
|
-
if (
|
|
602
|
+
if (Number.isNaN(voice.opened))
|
|
603
|
+
voice.opened = now;
|
|
604
|
+
if ((voice.state === 'live' || voice.state === 'held') && voice.outAt <= now)
|
|
530
605
|
this.beginFade(voice, {}, Math.max(voice.start, voice.outAt));
|
|
531
606
|
if (voice.state === 'pending' && now >= voice.start) {
|
|
532
607
|
voice.state = 'live';
|
|
533
|
-
this.
|
|
608
|
+
this.started(voice);
|
|
609
|
+
this.changed(voice);
|
|
534
610
|
}
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
611
|
+
if (voice.state !== 'pending' && Number.isFinite(voice.span)) {
|
|
612
|
+
const end = voice.span + voice.latest;
|
|
613
|
+
if (voice.elapsedAt(now) >= end) {
|
|
614
|
+
voice.play(true);
|
|
615
|
+
// The fade starts when the last pass ended, not at the frame that noticed, so it plays
|
|
616
|
+
// the same at any frame rate and a read at another time can find it.
|
|
617
|
+
if (voice.state === 'live') {
|
|
618
|
+
if (voice.holdsAfter)
|
|
619
|
+
voice.state = 'held';
|
|
620
|
+
else
|
|
621
|
+
this.beginFade(voice, {}, Math.max(voice.start, Math.min(now, voice.timeAt(end))));
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
else if (voice.state === 'held')
|
|
625
|
+
voice.state = 'live';
|
|
543
626
|
}
|
|
627
|
+
// A projection reads a finished ramp as weight 0, and leaves the live voice's subjects be.
|
|
628
|
+
if (voice.parts !== null && !this.projecting)
|
|
629
|
+
for (const [subject, r] of voice.parts)
|
|
630
|
+
if (now - r.at >= r.over)
|
|
631
|
+
this.part(voice, subject, r.at + r.over);
|
|
544
632
|
if (voice.state === 'fading' && voice.out) {
|
|
545
633
|
const { at, over, rest, deadline } = voice.out;
|
|
546
634
|
const spent = now - at;
|
|
@@ -555,33 +643,144 @@ class Mixer {
|
|
|
555
643
|
else if (spent >= over)
|
|
556
644
|
this.retire(voice, at + over);
|
|
557
645
|
}
|
|
646
|
+
if (!this.projecting)
|
|
647
|
+
this.schedule(voice);
|
|
558
648
|
}
|
|
649
|
+
this.stirred = false;
|
|
650
|
+
if (this.retired === 0) {
|
|
651
|
+
this.forget(now);
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
this.retired = 0;
|
|
559
655
|
let pruned = false;
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
if (voice.named === null)
|
|
568
|
-
pruned = true;
|
|
569
|
-
else
|
|
570
|
-
this.unindex(voice);
|
|
571
|
-
if (voice.spec.locus !== undefined)
|
|
572
|
-
this.loci--;
|
|
573
|
-
if (voice.spec.anchor !== undefined)
|
|
574
|
-
this.anchored--;
|
|
656
|
+
let kept = 0;
|
|
657
|
+
const voices = this.voices;
|
|
658
|
+
for (let i = 0; i < voices.length; i++) {
|
|
659
|
+
const voice = voices[i];
|
|
660
|
+
if (voice.state !== 'done') {
|
|
661
|
+
voices[kept++] = voice;
|
|
662
|
+
continue;
|
|
575
663
|
}
|
|
664
|
+
if (this.opts.history)
|
|
665
|
+
this.gone.push(voice);
|
|
666
|
+
else
|
|
667
|
+
this.parters.delete(voice);
|
|
668
|
+
this.unplay(voice);
|
|
669
|
+
this.changed(voice);
|
|
670
|
+
if (voice.named === null)
|
|
671
|
+
pruned = true;
|
|
672
|
+
else
|
|
673
|
+
this.unindex(voice);
|
|
674
|
+
if (voice.spec.locus !== undefined)
|
|
675
|
+
this.loci--;
|
|
676
|
+
if (voice.spec.anchor !== undefined)
|
|
677
|
+
this.anchored--;
|
|
576
678
|
}
|
|
679
|
+
voices.length = kept;
|
|
577
680
|
if (pruned)
|
|
578
681
|
this.general = this.general.filter((v) => v.state !== 'done');
|
|
682
|
+
this.forget(now);
|
|
683
|
+
}
|
|
684
|
+
/** Lets go of the voices that have left and that history no longer reaches. */
|
|
685
|
+
forget(now) {
|
|
579
686
|
const history = this.opts.history;
|
|
580
687
|
if (history && this.gone.length > 0) {
|
|
581
688
|
const reach = now - history.ms;
|
|
582
689
|
this.gone = this.gone.filter((v) => v.doneAt >= reach);
|
|
583
690
|
}
|
|
584
691
|
}
|
|
692
|
+
/**
|
|
693
|
+
* The earliest mix time `moveTo` would change a voice: its start, its anchored out, the end of
|
|
694
|
+
* its last pass, the end of its fade or of a subject's ramp out; -Infinity where it must be
|
|
695
|
+
* visited every frame, Infinity where nothing will happen until a handle or anchor changes it.
|
|
696
|
+
* Early is safe, since a visit that finds nothing due schedules again; late is not, so anything
|
|
697
|
+
* that can bring it earlier calls `schedule`.
|
|
698
|
+
*/
|
|
699
|
+
dueOf(voice) {
|
|
700
|
+
if (voice.state === 'done')
|
|
701
|
+
return Number.POSITIVE_INFINITY;
|
|
702
|
+
if (Number.isNaN(voice.opened))
|
|
703
|
+
return Number.NEGATIVE_INFINITY;
|
|
704
|
+
let due = Number.POSITIVE_INFINITY;
|
|
705
|
+
if (voice.parts !== null)
|
|
706
|
+
for (const r of voice.parts.values())
|
|
707
|
+
due = Math.min(due, r.at + r.over);
|
|
708
|
+
if (voice.state === 'pending')
|
|
709
|
+
return Math.min(due, Number.isNaN(voice.start) ? due : voice.start);
|
|
710
|
+
if (voice.state === 'fading') {
|
|
711
|
+
const out = voice.out;
|
|
712
|
+
if (out === null || out.rest)
|
|
713
|
+
return Number.NEGATIVE_INFINITY;
|
|
714
|
+
return Math.min(due, out.at + out.over);
|
|
715
|
+
}
|
|
716
|
+
due = Math.min(due, voice.outAt);
|
|
717
|
+
if (Number.isFinite(voice.span)) {
|
|
718
|
+
const end = voice.span + voice.latest;
|
|
719
|
+
// A held voice goes live again once its clock is back before its end: by a seek, by a
|
|
720
|
+
// subject staggered later than any before, or by running backwards, which is checked each
|
|
721
|
+
// frame while it can.
|
|
722
|
+
if (voice.state === 'held') {
|
|
723
|
+
if (voice.rate <= 0 || voice.ramp !== null)
|
|
724
|
+
return Number.NEGATIVE_INFINITY;
|
|
725
|
+
if (!Number.isNaN(this.now) && voice.elapsedAt(this.now) < end)
|
|
726
|
+
return Number.NEGATIVE_INFINITY;
|
|
727
|
+
}
|
|
728
|
+
if (voice.state === 'live')
|
|
729
|
+
due = Math.min(due, voice.timeAt(end));
|
|
730
|
+
}
|
|
731
|
+
return due;
|
|
732
|
+
}
|
|
733
|
+
/** Files a voice in the due queue at its current due, replacing any entry it had. */
|
|
734
|
+
schedule(voice) {
|
|
735
|
+
if (this.projecting)
|
|
736
|
+
return;
|
|
737
|
+
this.lanes?.voiceChanged(voice.id);
|
|
738
|
+
const token = ++voice.dueToken;
|
|
739
|
+
const due = this.dueOf(voice);
|
|
740
|
+
if (due === Number.POSITIVE_INFINITY)
|
|
741
|
+
return;
|
|
742
|
+
const heap = this.due;
|
|
743
|
+
heap.push({ at: Number.isNaN(due) ? Number.NEGATIVE_INFINITY : due, voice, token });
|
|
744
|
+
let i = heap.length - 1;
|
|
745
|
+
while (i > 0) {
|
|
746
|
+
const up = (i - 1) >> 1;
|
|
747
|
+
if (heap[up].at <= heap[i].at)
|
|
748
|
+
break;
|
|
749
|
+
[heap[up], heap[i]] = [heap[i], heap[up]];
|
|
750
|
+
i = up;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
/** Takes every current entry due by `now` off the queue, in cue order. */
|
|
754
|
+
popDue(now) {
|
|
755
|
+
const heap = this.due;
|
|
756
|
+
const at = (k) => k < heap.length ? heap[k].at : Number.POSITIVE_INFINITY;
|
|
757
|
+
const out = [];
|
|
758
|
+
while (heap.length > 0 && heap[0].at <= now) {
|
|
759
|
+
const top = heap[0];
|
|
760
|
+
const last = heap.pop();
|
|
761
|
+
if (heap.length > 0) {
|
|
762
|
+
heap[0] = last;
|
|
763
|
+
let i = 0;
|
|
764
|
+
for (;;) {
|
|
765
|
+
const l = 2 * i + 1;
|
|
766
|
+
const r = l + 1;
|
|
767
|
+
const m0 = at(l) < at(i) ? l : i;
|
|
768
|
+
const m = at(r) < at(m0) ? r : m0;
|
|
769
|
+
if (m === i)
|
|
770
|
+
break;
|
|
771
|
+
[heap[m], heap[i]] = [heap[i], heap[m]];
|
|
772
|
+
i = m;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
if (top.token === top.voice.dueToken && top.voice.state !== 'done') {
|
|
776
|
+
top.voice.dueToken++;
|
|
777
|
+
out.push(top.voice);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
if (out.length > 1)
|
|
781
|
+
out.sort((a, b) => a.id - b.id);
|
|
782
|
+
return out;
|
|
783
|
+
}
|
|
585
784
|
probe(subject, out) {
|
|
586
785
|
const pose = this.fold(subject, out);
|
|
587
786
|
this.keep(subject, pose, out);
|
|
@@ -632,24 +831,81 @@ class Mixer {
|
|
|
632
831
|
tightest = c.key;
|
|
633
832
|
}
|
|
634
833
|
}
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
const
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
834
|
+
// Any other iterable is read into an array first, no further than one past the room there is.
|
|
835
|
+
let list;
|
|
836
|
+
if (Array.isArray(subjects))
|
|
837
|
+
list = subjects;
|
|
838
|
+
else {
|
|
839
|
+
const taken = [];
|
|
840
|
+
for (const subject of subjects)
|
|
841
|
+
if (taken.push(subject) > room)
|
|
842
|
+
break;
|
|
843
|
+
list = taken;
|
|
844
|
+
}
|
|
845
|
+
if (list.length > room)
|
|
846
|
+
throw new RangeError(`blits: pull's array for ${tightest} has room for ${room} subjects, and was given more`);
|
|
847
|
+
const whole = lanes !== null && !this.wantsPose;
|
|
848
|
+
// An array read again in the same order reuses each position's chain head while it is current:
|
|
849
|
+
// all of them at once while nothing has relinked since the last pull, else one by one.
|
|
850
|
+
const was = this.pulled;
|
|
851
|
+
const heads = this.pulledHeads;
|
|
852
|
+
was.length = list.length;
|
|
853
|
+
heads.length = list.length;
|
|
854
|
+
if (this.pulledSlots.length < list.length) {
|
|
855
|
+
const slots = new Int32Array(Math.max(64, list.length));
|
|
856
|
+
slots.set(this.pulledSlots);
|
|
857
|
+
this.pulledSlots = slots;
|
|
858
|
+
}
|
|
859
|
+
const slots = this.pulledSlots;
|
|
860
|
+
const version = this.version;
|
|
861
|
+
const relinks = this.relinks;
|
|
862
|
+
const live = !Number.isNaN(now);
|
|
863
|
+
const current = live && this.pulledVersion === version && this.pulledRelinks === relinks;
|
|
864
|
+
// Every subject read from the lanes, at the positions it had last time, goes in one run.
|
|
865
|
+
const runs = live && whole;
|
|
866
|
+
try {
|
|
867
|
+
for (let n = 0; n < list.length; n++) {
|
|
868
|
+
if (runs) {
|
|
869
|
+
n = lanes.pullRun(slots, list, was, current ? null : heads, n, columns, now, this.version);
|
|
870
|
+
if (n === list.length)
|
|
871
|
+
break;
|
|
872
|
+
}
|
|
873
|
+
const subject = list[n];
|
|
874
|
+
let slot = -1;
|
|
875
|
+
if (live) {
|
|
876
|
+
if (current && was[n] === subject)
|
|
877
|
+
slot = slots[n];
|
|
878
|
+
else {
|
|
879
|
+
const kept = heads[n];
|
|
880
|
+
const head = was[n] === subject && kept !== undefined && kept.version === version
|
|
881
|
+
? kept
|
|
882
|
+
: this.chain(subject, now);
|
|
883
|
+
was[n] = subject;
|
|
884
|
+
heads[n] = head;
|
|
885
|
+
slot = head.slot;
|
|
886
|
+
slots[n] = slot;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
const laned = live && lanes?.prepare(slot, subject, now, this.version) === true;
|
|
890
|
+
if (laned && whole && lanes.whole) {
|
|
891
|
+
lanes.writeLater(slot, columns, n);
|
|
892
|
+
continue;
|
|
893
|
+
}
|
|
894
|
+
const head = live ? heads[n] : null;
|
|
645
895
|
const folded = this.foldWith(subject, scratch, head, laned, false);
|
|
646
896
|
const pose = (this.bounded.length === 0 ? folded : this.clamp(folded));
|
|
647
897
|
this.keep(subject, pose, scratch);
|
|
648
898
|
for (const c of columns)
|
|
649
899
|
writeValue(c, pose[c.key], n);
|
|
650
900
|
}
|
|
651
|
-
n++;
|
|
652
901
|
}
|
|
902
|
+
finally {
|
|
903
|
+
lanes?.flush();
|
|
904
|
+
}
|
|
905
|
+
// Every head is current only if nothing relinked or changed version while they were read.
|
|
906
|
+
const unchanged = live && this.version === version && this.relinks === relinks;
|
|
907
|
+
this.pulledVersion = unchanged ? version : Number.NaN;
|
|
908
|
+
this.pulledRelinks = relinks;
|
|
653
909
|
}
|
|
654
910
|
/** The kit slot, width, rest and bounds of every channel `pull` was handed an array for. */
|
|
655
911
|
columnsOf(into) {
|
|
@@ -716,7 +972,14 @@ class Mixer {
|
|
|
716
972
|
.map((v) => {
|
|
717
973
|
const log = v.log;
|
|
718
974
|
const copy = v.copy((subject) => this.recall(v, subject, t), last(log, t, (e) => e.sync) ?? log[0]);
|
|
719
|
-
copy.state =
|
|
975
|
+
copy.state =
|
|
976
|
+
t < v.start
|
|
977
|
+
? 'pending'
|
|
978
|
+
: copy.out && copy.out.at <= t
|
|
979
|
+
? 'fading'
|
|
980
|
+
: copy.holdsAfter && copy.elapsedAt(t) >= copy.span + copy.latest
|
|
981
|
+
? 'held'
|
|
982
|
+
: 'live';
|
|
720
983
|
if (then !== undefined)
|
|
721
984
|
copy.setting.host = then;
|
|
722
985
|
return copy;
|
|
@@ -806,20 +1069,99 @@ class Mixer {
|
|
|
806
1069
|
get live() {
|
|
807
1070
|
return this.voices.some((v) => v.state !== 'done');
|
|
808
1071
|
}
|
|
1072
|
+
get inert() {
|
|
1073
|
+
return !this.stirred && this.voices.every((v) => this.still(v, this.now));
|
|
1074
|
+
}
|
|
1075
|
+
/** Whether a voice will change no pose from `now` on, short of a change made to it. */
|
|
1076
|
+
still(voice, now) {
|
|
1077
|
+
if (voice.state === 'done')
|
|
1078
|
+
return true;
|
|
1079
|
+
if (voice.state !== 'held' && voice.state !== 'live')
|
|
1080
|
+
return false;
|
|
1081
|
+
if (typeof voice.spec.weight === 'function' || voice.parts !== null)
|
|
1082
|
+
return false;
|
|
1083
|
+
const fadeIn = this.reducedNow ? 0 : (voice.fade.in ?? 0);
|
|
1084
|
+
if (fadeIn > 0 && now - this.sinceOf(voice, voice.latest) < fadeIn)
|
|
1085
|
+
return false;
|
|
1086
|
+
if (voice.state === 'held')
|
|
1087
|
+
return true;
|
|
1088
|
+
const motion = voice.motion;
|
|
1089
|
+
return motion !== undefined && voice.elapsedAt(now) >= voice.latest && motion.landed();
|
|
1090
|
+
}
|
|
809
1091
|
mute(opts) {
|
|
810
1092
|
for (const voice of this.voices)
|
|
811
1093
|
this.beginFade(voice, { over: opts?.over });
|
|
812
1094
|
}
|
|
813
1095
|
drop(subject) {
|
|
814
1096
|
const head = this.chains.get(subject);
|
|
1097
|
+
// Nothing that kept this head, such as `pull`'s remembered list, may take it as current again.
|
|
1098
|
+
if (head !== undefined) {
|
|
1099
|
+
head.version = Number.NaN;
|
|
1100
|
+
this.relinks++;
|
|
1101
|
+
}
|
|
815
1102
|
if (head !== undefined && head.slot >= 0 && this.lanes !== null)
|
|
816
1103
|
this.lanes.release(head.slot);
|
|
817
1104
|
this.pose.delete(subject);
|
|
818
1105
|
this.chains.delete(subject);
|
|
819
|
-
|
|
1106
|
+
this.stirred = true;
|
|
1107
|
+
// A record of it is only in a voice over every subject, one naming it, or one gone; a ramp out
|
|
1108
|
+
// of a voice that does not name it is in `parters`, and a patch's own state in `motions`.
|
|
1109
|
+
const forget = (voice) => {
|
|
820
1110
|
voice.subjects.delete(subject);
|
|
821
|
-
|
|
1111
|
+
voice.motion?.release(subject);
|
|
1112
|
+
voice.parts?.delete(subject);
|
|
1113
|
+
voice.parted?.delete(subject);
|
|
1114
|
+
};
|
|
1115
|
+
for (const voice of this.general)
|
|
1116
|
+
forget(voice);
|
|
1117
|
+
const named = this.named.get(subject);
|
|
1118
|
+
if (named !== undefined)
|
|
1119
|
+
for (const voice of [...named])
|
|
1120
|
+
forget(voice);
|
|
1121
|
+
for (const voice of this.gone)
|
|
1122
|
+
forget(voice);
|
|
1123
|
+
for (const voice of this.parters) {
|
|
1124
|
+
forget(voice);
|
|
1125
|
+
if (voice.parts?.size === 0)
|
|
1126
|
+
voice.parts = null;
|
|
1127
|
+
if (voice.parted?.size === 0)
|
|
1128
|
+
voice.parted = null;
|
|
1129
|
+
if (voice.parts === null && voice.parted === null)
|
|
1130
|
+
this.parters.delete(voice);
|
|
1131
|
+
}
|
|
1132
|
+
const strays = this.strays.get(subject);
|
|
1133
|
+
if (strays !== undefined)
|
|
1134
|
+
for (const motion of strays)
|
|
1135
|
+
motion.release(subject);
|
|
1136
|
+
this.strays.delete(subject);
|
|
1137
|
+
for (const motion of this.strayAll)
|
|
1138
|
+
motion.release(subject);
|
|
1139
|
+
}
|
|
1140
|
+
/** Notes a motion patch that may hold state for a subject no voice playing it reaches. */
|
|
1141
|
+
stray(motion, subject) {
|
|
1142
|
+
const list = this.strays.get(subject);
|
|
1143
|
+
if (list === undefined)
|
|
1144
|
+
this.strays.set(subject, [motion]);
|
|
1145
|
+
else if (!list.includes(motion))
|
|
1146
|
+
list.push(motion);
|
|
1147
|
+
}
|
|
1148
|
+
/** A motion voice left the list: if its patch plays on, its subjects' state there may stray. */
|
|
1149
|
+
unplay(voice) {
|
|
1150
|
+
const motion = voice.motion;
|
|
1151
|
+
if (motion === undefined)
|
|
1152
|
+
return;
|
|
1153
|
+
const left = (this.playing.get(motion) ?? 1) - 1;
|
|
1154
|
+
if (left <= 0) {
|
|
1155
|
+
this.playing.delete(motion);
|
|
1156
|
+
this.strayAll.delete(motion);
|
|
1157
|
+
return;
|
|
822
1158
|
}
|
|
1159
|
+
this.playing.set(motion, left);
|
|
1160
|
+
if (voice.named === null)
|
|
1161
|
+
this.strayAll.add(motion);
|
|
1162
|
+
else
|
|
1163
|
+
for (const subject of voice.named)
|
|
1164
|
+
this.stray(motion, subject);
|
|
823
1165
|
}
|
|
824
1166
|
drain(tag) {
|
|
825
1167
|
const all = this.sent;
|
|
@@ -848,7 +1190,9 @@ class Mixer {
|
|
|
848
1190
|
fits: (voice) => mix.fits(voice),
|
|
849
1191
|
meet: (voice, subject) => mix.held(voice, subject, mix.now),
|
|
850
1192
|
naming: (subject) => (mix.naming === 0 ? undefined : mix.named.get(subject)),
|
|
1193
|
+
slotOf: (subject) => mix.chains.get(subject)?.slot ?? -1,
|
|
851
1194
|
envelope: (voice, since) => mix.envelope(voice, mix.now, since),
|
|
1195
|
+
parting: (voice, subject) => mix.parting(voice, subject, mix.now),
|
|
852
1196
|
ready: (voice, subject, held, elapsed, pass, weight) => {
|
|
853
1197
|
mix.prime(voice, held, mix.now, elapsed, pass);
|
|
854
1198
|
voice.setting.weight = weight;
|
|
@@ -874,6 +1218,8 @@ class Mixer {
|
|
|
874
1218
|
const patch = voice.patch;
|
|
875
1219
|
if (voice.keeping)
|
|
876
1220
|
return false;
|
|
1221
|
+
if (voice.state === 'pending' && voice.holdsBefore)
|
|
1222
|
+
return false;
|
|
877
1223
|
if (typeof spec.weight === 'function')
|
|
878
1224
|
return false;
|
|
879
1225
|
if (spec.locus !== undefined || spec.from === 'current')
|
|
@@ -899,6 +1245,20 @@ class Mixer {
|
|
|
899
1245
|
* What a motion patch asks for its subjects' voice time at the latest frame. It holds the mix and
|
|
900
1246
|
* the voice weakly, so a patch the host keeps does not keep a mix it has let go of alive.
|
|
901
1247
|
*/
|
|
1248
|
+
static reviveHook(mix, voice) {
|
|
1249
|
+
return (subject) => {
|
|
1250
|
+
const m = mix.deref();
|
|
1251
|
+
if (m === undefined)
|
|
1252
|
+
return;
|
|
1253
|
+
m.stirred = true;
|
|
1254
|
+
const v = voice.deref();
|
|
1255
|
+
if (v === undefined)
|
|
1256
|
+
return;
|
|
1257
|
+
if (v.motion !== undefined && v.named !== null && !v.named.has(subject))
|
|
1258
|
+
m.stray(v.motion, subject);
|
|
1259
|
+
m.unpart(v, subject);
|
|
1260
|
+
};
|
|
1261
|
+
}
|
|
902
1262
|
static frameHook(mix, voice) {
|
|
903
1263
|
return (subject) => {
|
|
904
1264
|
const m = mix.deref();
|
|
@@ -924,19 +1284,18 @@ class Mixer {
|
|
|
924
1284
|
prime(voice, held, now, elapsed, pass) {
|
|
925
1285
|
const setting = voice.setting;
|
|
926
1286
|
setting.timestamp = now;
|
|
927
|
-
|
|
928
|
-
const cap = this.opts.maxDt;
|
|
929
|
-
setting.dt = this.reducedNow
|
|
930
|
-
? Number.POSITIVE_INFINITY
|
|
931
|
-
: cap !== undefined && gap > cap
|
|
932
|
-
? cap
|
|
933
|
-
: gap;
|
|
1287
|
+
setting.dt = this.capped(now - held.stepped);
|
|
934
1288
|
setting.elapsed = elapsed;
|
|
935
1289
|
setting.pass = pass;
|
|
936
1290
|
setting.weight = 0;
|
|
937
1291
|
setting.state = held.state;
|
|
938
1292
|
setting.keep = held.keep;
|
|
939
1293
|
}
|
|
1294
|
+
/** A gap as a `dt`: Infinity under reduced motion, and no more than `maxDt`. */
|
|
1295
|
+
capped(gap) {
|
|
1296
|
+
const cap = this.opts.maxDt;
|
|
1297
|
+
return this.reducedNow ? Number.POSITIVE_INFINITY : cap !== undefined && gap > cap ? cap : gap;
|
|
1298
|
+
}
|
|
940
1299
|
/** The earliest voice time a read may still ask for, for a subject delayed `delay`. */
|
|
941
1300
|
horizonFor(voice, delay, now) {
|
|
942
1301
|
const history = this.opts.history;
|
|
@@ -1102,14 +1461,10 @@ class Mixer {
|
|
|
1102
1461
|
outAt = out.at;
|
|
1103
1462
|
else if (Number.isFinite(voice.outAt))
|
|
1104
1463
|
outAt = voice.outAt;
|
|
1105
|
-
else {
|
|
1106
|
-
const
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
const t = voice.timeAt(period * passes + voice.latest);
|
|
1110
|
-
if (Number.isFinite(t))
|
|
1111
|
-
outAt = Math.max(start, t);
|
|
1112
|
-
}
|
|
1464
|
+
else if (!voice.holdsAfter && Number.isFinite(voice.span)) {
|
|
1465
|
+
const t = voice.timeAt(voice.span + voice.latest);
|
|
1466
|
+
if (Number.isFinite(t))
|
|
1467
|
+
outAt = Math.max(start, t);
|
|
1113
1468
|
}
|
|
1114
1469
|
if (mark === 'out')
|
|
1115
1470
|
return voice.state === 'done' && outAt === undefined ? voice.doneAt : outAt;
|
|
@@ -1130,6 +1485,8 @@ class Mixer {
|
|
|
1130
1485
|
}
|
|
1131
1486
|
/** Records a change to a voice's controls under `history`, and lets go of what it no longer reaches. */
|
|
1132
1487
|
noted(voice) {
|
|
1488
|
+
this.stirred = true;
|
|
1489
|
+
this.schedule(voice);
|
|
1133
1490
|
const log = voice.log;
|
|
1134
1491
|
if (log === null)
|
|
1135
1492
|
return;
|
|
@@ -1212,6 +1569,7 @@ class Mixer {
|
|
|
1212
1569
|
reaches: live.reaches,
|
|
1213
1570
|
delay: live.delay,
|
|
1214
1571
|
since: live.since,
|
|
1572
|
+
shown: live.shown,
|
|
1215
1573
|
weight: 0,
|
|
1216
1574
|
rested: false,
|
|
1217
1575
|
bands: new Uint8Array(voice.slots.length),
|
|
@@ -1355,6 +1713,7 @@ class Mixer {
|
|
|
1355
1713
|
get state() {
|
|
1356
1714
|
return voice.state;
|
|
1357
1715
|
},
|
|
1716
|
+
played: voice.played,
|
|
1358
1717
|
get weight() {
|
|
1359
1718
|
return voice.weight;
|
|
1360
1719
|
},
|
|
@@ -1390,7 +1749,10 @@ class Mixer {
|
|
|
1390
1749
|
mix.lanes?.refill();
|
|
1391
1750
|
},
|
|
1392
1751
|
fade(opts) {
|
|
1393
|
-
|
|
1752
|
+
if (opts !== undefined && 'subject' in opts)
|
|
1753
|
+
mix.fadeSubject(voice, opts.subject, opts.over);
|
|
1754
|
+
else
|
|
1755
|
+
mix.beginFade(voice, opts ?? {});
|
|
1394
1756
|
},
|
|
1395
1757
|
weightOf(subject) {
|
|
1396
1758
|
if (voice.state === 'done')
|
|
@@ -1405,13 +1767,85 @@ class Mixer {
|
|
|
1405
1767
|
done: voice.done,
|
|
1406
1768
|
};
|
|
1407
1769
|
}
|
|
1770
|
+
/** Starts one subject's ramp out of a voice, or takes it out at once for a ramp of 0. */
|
|
1771
|
+
fadeSubject(voice, subject, over) {
|
|
1772
|
+
if (voice.state === 'done' || voice.parted?.has(subject) || voice.parts?.has(subject))
|
|
1773
|
+
return;
|
|
1774
|
+
this.stirred = true;
|
|
1775
|
+
const ms = this.reduced ? 0 : (over ?? voice.fade.out ?? 0);
|
|
1776
|
+
const at = Number.isNaN(this.now) ? voice.start : this.now;
|
|
1777
|
+
if (ms === 0) {
|
|
1778
|
+
this.part(voice, subject, at);
|
|
1779
|
+
return;
|
|
1780
|
+
}
|
|
1781
|
+
voice.parts ??= new Map();
|
|
1782
|
+
voice.parts.set(subject, { at, over: ms });
|
|
1783
|
+
this.parters.add(voice);
|
|
1784
|
+
this.schedule(voice);
|
|
1785
|
+
this.lanes?.refill();
|
|
1786
|
+
}
|
|
1787
|
+
/** What a subject's own ramp out of a voice leaves of its weight this frame, 0..1. */
|
|
1788
|
+
parting(voice, subject, now) {
|
|
1789
|
+
const r = voice.parts?.get(subject);
|
|
1790
|
+
if (r === undefined)
|
|
1791
|
+
return 1;
|
|
1792
|
+
return envelope(0, { at: r.at, over: r.over, rest: false }, voice.ease, this.reducedNow, now, 0);
|
|
1793
|
+
}
|
|
1794
|
+
/**
|
|
1795
|
+
* Takes a subject out of one voice for good, as of mix time `at`: the voice forgets its record,
|
|
1796
|
+
* its lane position and a motion patch's state for it, and reaches it no more.
|
|
1797
|
+
*/
|
|
1798
|
+
part(voice, subject, at) {
|
|
1799
|
+
voice.parts?.delete(subject);
|
|
1800
|
+
if (voice.parts?.size === 0)
|
|
1801
|
+
voice.parts = null;
|
|
1802
|
+
voice.parted ??= new Map();
|
|
1803
|
+
voice.parted.set(subject, at);
|
|
1804
|
+
this.parters.add(voice);
|
|
1805
|
+
this.forgetIn(voice, subject);
|
|
1806
|
+
const motion = voice.motion;
|
|
1807
|
+
if (motion !== undefined && motion.frame === voice.frame)
|
|
1808
|
+
motion.release(subject);
|
|
1809
|
+
}
|
|
1810
|
+
/** Brings a subject faded out of a voice back, to be met afresh on its next probe. */
|
|
1811
|
+
unpart(voice, subject) {
|
|
1812
|
+
if (voice.parted?.delete(subject) !== true)
|
|
1813
|
+
return;
|
|
1814
|
+
if (voice.parted.size === 0)
|
|
1815
|
+
voice.parted = null;
|
|
1816
|
+
this.forgetIn(voice, subject);
|
|
1817
|
+
const slot = this.chains.get(subject)?.slot ?? -1;
|
|
1818
|
+
if (slot >= 0)
|
|
1819
|
+
this.lanes?.rejoin(slot);
|
|
1820
|
+
}
|
|
1821
|
+
/** Drops a voice's record of a subject and relinks the subject's chain without it. */
|
|
1822
|
+
forgetIn(voice, subject) {
|
|
1823
|
+
const held = voice.subjects.get(subject);
|
|
1824
|
+
if (held !== undefined) {
|
|
1825
|
+
voice.subjects.delete(subject);
|
|
1826
|
+
if (held.reaches)
|
|
1827
|
+
voice.seen--;
|
|
1828
|
+
if (held.rested)
|
|
1829
|
+
voice.restedCount--;
|
|
1830
|
+
if (voice.holder === held)
|
|
1831
|
+
voice.holder = null;
|
|
1832
|
+
}
|
|
1833
|
+
const head = this.chains.get(subject);
|
|
1834
|
+
if (head === undefined)
|
|
1835
|
+
return;
|
|
1836
|
+
head.version = Number.NaN;
|
|
1837
|
+
this.relinks++;
|
|
1838
|
+
if (voice.laned && head.slot >= 0)
|
|
1839
|
+
this.lanes?.part(voice.id, head.slot);
|
|
1840
|
+
this.lanes?.refill();
|
|
1841
|
+
}
|
|
1408
1842
|
beginFade(voice, opts, at) {
|
|
1409
1843
|
if (voice.state === 'done' || voice.state === 'fading')
|
|
1410
1844
|
return;
|
|
1411
1845
|
const over = this.reduced ? 0 : (opts.over ?? voice.fade.out ?? 0);
|
|
1412
1846
|
// A voice is linked into its subjects' chains once it plays, and a fading one plays.
|
|
1413
1847
|
if (voice.state === 'pending')
|
|
1414
|
-
this.
|
|
1848
|
+
this.changed(voice);
|
|
1415
1849
|
voice.state = 'fading';
|
|
1416
1850
|
voice.out = {
|
|
1417
1851
|
from: 1,
|
|
@@ -1430,13 +1864,18 @@ class Mixer {
|
|
|
1430
1864
|
}
|
|
1431
1865
|
/** Removes a voice, recording that it left at `at`, default now. */
|
|
1432
1866
|
retire(voice, at) {
|
|
1433
|
-
this.lanes?.
|
|
1434
|
-
const motion =
|
|
1867
|
+
this.lanes?.touch(voice);
|
|
1868
|
+
const motion = voice.motion;
|
|
1435
1869
|
if (motion !== undefined && motion.frame === voice.frame)
|
|
1436
1870
|
motion.frame = noFrame;
|
|
1871
|
+
if (motion !== undefined && motion.revive === voice.revive)
|
|
1872
|
+
motion.revive = noRevive;
|
|
1437
1873
|
voice.frame = null;
|
|
1874
|
+
voice.revive = null;
|
|
1438
1875
|
voice.state = 'done';
|
|
1876
|
+
this.retired++;
|
|
1439
1877
|
voice.doneAt = at ?? (Number.isNaN(this.now) ? Number.NEGATIVE_INFINITY : this.now);
|
|
1878
|
+
voice.play(false);
|
|
1440
1879
|
voice.resolve();
|
|
1441
1880
|
}
|
|
1442
1881
|
/** The ramp a voice's own fade envelope applies this frame, 0..1. */
|
|
@@ -1450,16 +1889,13 @@ class Mixer {
|
|
|
1450
1889
|
return held;
|
|
1451
1890
|
const reaches = this.aims(voice, subject);
|
|
1452
1891
|
const delay = reaches && voice.spec.stagger ? voice.spec.stagger(subject) : 0;
|
|
1453
|
-
|
|
1454
|
-
// the rate it is ramping to.
|
|
1455
|
-
const since = voice.rate > 0
|
|
1456
|
-
? voice.anchorNow + (delay - voice.anchorElapsed) / voice.rate
|
|
1457
|
-
: voice.start + delay;
|
|
1892
|
+
const since = this.sinceOf(voice, delay);
|
|
1458
1893
|
const kept = new Map();
|
|
1459
1894
|
held = {
|
|
1460
1895
|
reaches,
|
|
1461
1896
|
delay,
|
|
1462
1897
|
since,
|
|
1898
|
+
shown: this.shownOf(voice, since),
|
|
1463
1899
|
weight: 0,
|
|
1464
1900
|
rested: false,
|
|
1465
1901
|
bands: new Uint8Array(voice.slots.length),
|
|
@@ -1485,12 +1921,43 @@ class Mixer {
|
|
|
1485
1921
|
held.unknown = this.backward && voice.spec.from === 'current';
|
|
1486
1922
|
}
|
|
1487
1923
|
voice.subjects.set(subject, held);
|
|
1924
|
+
if (voice.state === 'pending') {
|
|
1925
|
+
voice.early ??= [];
|
|
1926
|
+
voice.early.push(held);
|
|
1927
|
+
}
|
|
1488
1928
|
if (reaches)
|
|
1489
1929
|
voice.seen++;
|
|
1490
|
-
if (delay > voice.latest)
|
|
1930
|
+
if (delay > voice.latest) {
|
|
1491
1931
|
voice.latest = delay;
|
|
1932
|
+
// A later end can take a held voice live again.
|
|
1933
|
+
if (voice.state === 'held')
|
|
1934
|
+
this.schedule(voice);
|
|
1935
|
+
}
|
|
1492
1936
|
return held;
|
|
1493
1937
|
}
|
|
1938
|
+
/**
|
|
1939
|
+
* When the voice clock reads `delay`, from where it is anchored now; during a ramp this assumes
|
|
1940
|
+
* the rate it is ramping to.
|
|
1941
|
+
*/
|
|
1942
|
+
sinceOf(voice, delay) {
|
|
1943
|
+
return voice.rate > 0
|
|
1944
|
+
? voice.anchorNow + (delay - voice.anchorElapsed) / voice.rate
|
|
1945
|
+
: voice.start + delay;
|
|
1946
|
+
}
|
|
1947
|
+
shownOf(voice, since) {
|
|
1948
|
+
return voice.holdsBefore && voice.opened < since ? voice.opened : since;
|
|
1949
|
+
}
|
|
1950
|
+
/** A voice has gone live: the records it made while pending count from where it started. */
|
|
1951
|
+
started(voice) {
|
|
1952
|
+
const early = voice.early;
|
|
1953
|
+
if (early === null)
|
|
1954
|
+
return;
|
|
1955
|
+
voice.early = null;
|
|
1956
|
+
for (const held of early) {
|
|
1957
|
+
held.since = this.sinceOf(voice, held.delay);
|
|
1958
|
+
held.shown = this.shownOf(voice, held.since);
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1494
1961
|
/** The weight a voice gives a subject this frame, with the setting already filled in. */
|
|
1495
1962
|
weigh(voice, subject, now, held) {
|
|
1496
1963
|
this.sending.voice = voice;
|
|
@@ -1503,7 +1970,8 @@ class Mixer {
|
|
|
1503
1970
|
if (signal.input && !was)
|
|
1504
1971
|
this.record(held, base);
|
|
1505
1972
|
}
|
|
1506
|
-
|
|
1973
|
+
const fade = this.envelope(voice, now, held.shown);
|
|
1974
|
+
return clampWeight(base * (voice.parts === null ? fade : fade * this.parting(voice, subject, now)));
|
|
1507
1975
|
}
|
|
1508
1976
|
/**
|
|
1509
1977
|
* One live voice's delta for one subject this frame, through its record for the subject, or null
|
|
@@ -1515,9 +1983,19 @@ class Mixer {
|
|
|
1515
1983
|
return null;
|
|
1516
1984
|
if (voice.out?.rest && held.rested)
|
|
1517
1985
|
return null;
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1986
|
+
let elapsed = voice.elapsedAt(now) - held.delay;
|
|
1987
|
+
// A held subject's clock stands still at the edge it holds: -1 before, 1 after, 0 playing.
|
|
1988
|
+
let still = 0;
|
|
1989
|
+
if (elapsed < 0) {
|
|
1990
|
+
if (!voice.holdsBefore)
|
|
1991
|
+
return null;
|
|
1992
|
+
elapsed = 0;
|
|
1993
|
+
still = -1;
|
|
1994
|
+
}
|
|
1995
|
+
else if (voice.holdsAfter && elapsed > voice.span) {
|
|
1996
|
+
elapsed = voice.span;
|
|
1997
|
+
still = 1;
|
|
1998
|
+
}
|
|
1521
1999
|
const period = voice.patch.period;
|
|
1522
2000
|
const phase = phaseAt(elapsed, period, voice.passes);
|
|
1523
2001
|
const pass = passAt(elapsed, period, voice.passes);
|
|
@@ -1538,12 +2016,16 @@ class Mixer {
|
|
|
1538
2016
|
this.recordHost(now);
|
|
1539
2017
|
reading.horizon = this.horizonFor(voice, held.delay, now);
|
|
1540
2018
|
const tick = this.opts.stepMs;
|
|
1541
|
-
if (voice.patch.step && held.probed !== now) {
|
|
2019
|
+
if (voice.patch.step && held.probed !== now && still >= 0) {
|
|
2020
|
+
// Held after, it steps once more to where its last pass ended, and no further.
|
|
2021
|
+
const to = still === 1 ? voice.timeAt(held.delay + voice.span) : now;
|
|
1542
2022
|
if (tick !== undefined && tick > 0 && !this.reducedNow)
|
|
1543
|
-
this.tick(voice, subject, held, tick);
|
|
1544
|
-
else if (now !== held.stepped) {
|
|
2023
|
+
this.tick(voice, subject, held, tick, to);
|
|
2024
|
+
else if (still === 0 ? now !== held.stepped : to > held.stepped) {
|
|
2025
|
+
if (still === 1)
|
|
2026
|
+
setting.dt = this.capped(to - held.stepped);
|
|
1545
2027
|
voice.patch.step(held.state, setting.dt, subject, setting);
|
|
1546
|
-
held.stepped =
|
|
2028
|
+
held.stepped = to;
|
|
1547
2029
|
}
|
|
1548
2030
|
}
|
|
1549
2031
|
let delta;
|
|
@@ -1591,13 +2073,13 @@ class Mixer {
|
|
|
1591
2073
|
* counted from when its delay ran out, and leaves the remainder for the next sample. Counting
|
|
1592
2074
|
* rather than adding keeps the grid where it is however many samples it is reached through.
|
|
1593
2075
|
*/
|
|
1594
|
-
tick(voice, subject, held, tick) {
|
|
2076
|
+
tick(voice, subject, held, tick, to) {
|
|
1595
2077
|
const now = this.now;
|
|
1596
2078
|
const setting = voice.setting;
|
|
1597
2079
|
const frameDt = setting.dt;
|
|
1598
2080
|
let n = held.ticks;
|
|
1599
2081
|
// The epsilon keeps an interval like 1000 / 120 from landing a hair short of a whole count.
|
|
1600
|
-
const due = Math.floor((
|
|
2082
|
+
const due = Math.floor((to - held.since) / tick + 1e-9);
|
|
1601
2083
|
const cap = this.opts.maxDt;
|
|
1602
2084
|
if (cap !== undefined) {
|
|
1603
2085
|
const most = Math.floor(cap / tick);
|
|
@@ -1714,7 +2196,8 @@ class Mixer {
|
|
|
1714
2196
|
/**
|
|
1715
2197
|
* The first of this subject's records, linked through every live voice that reaches it. A voice
|
|
1716
2198
|
* is asked once, through the record it keeps for the subject anyway; a pending one is linked when
|
|
1717
|
-
* it goes live, since `target` is asked on first sight, and sight only comes once a voice plays
|
|
2199
|
+
* it goes live, since `target` is asked on first sight, and sight only comes once a voice plays
|
|
2200
|
+
* or holds before.
|
|
1718
2201
|
*/
|
|
1719
2202
|
chain(subject, now) {
|
|
1720
2203
|
const was = this.chains.get(subject);
|
|
@@ -1723,7 +2206,8 @@ class Mixer {
|
|
|
1723
2206
|
let first = null;
|
|
1724
2207
|
let prev = null;
|
|
1725
2208
|
const link = (voice) => {
|
|
1726
|
-
|
|
2209
|
+
const state = voice.state;
|
|
2210
|
+
if (state === 'done' || (state === 'pending' && !voice.holdsBefore))
|
|
1727
2211
|
return;
|
|
1728
2212
|
const held = this.held(voice, subject, now);
|
|
1729
2213
|
if (!held.reaches)
|
|
@@ -1761,10 +2245,32 @@ class Mixer {
|
|
|
1761
2245
|
}
|
|
1762
2246
|
/** Whether a voice reaches a subject by what its spec says: the subjects it names, or its `target`. */
|
|
1763
2247
|
aims(voice, subject) {
|
|
2248
|
+
// A read back to before a subject left still finds it reached.
|
|
2249
|
+
const left = voice.parted?.get(subject);
|
|
2250
|
+
if (left !== undefined && !(this.now < left))
|
|
2251
|
+
return false;
|
|
1764
2252
|
if (voice.named !== null)
|
|
1765
2253
|
return voice.named.has(subject);
|
|
1766
2254
|
return voice.spec.target ? voice.spec.target(subject) : true;
|
|
1767
2255
|
}
|
|
2256
|
+
/**
|
|
2257
|
+
* A voice joined or left the list, or started or stopped waiting: the chains it is linked into
|
|
2258
|
+
* are relinked, every subject's for a voice over all of them and only its own for one naming
|
|
2259
|
+
* its subjects, and lanes take it on or off.
|
|
2260
|
+
*/
|
|
2261
|
+
changed(voice) {
|
|
2262
|
+
this.lanes?.touch(voice);
|
|
2263
|
+
if (voice.named === null) {
|
|
2264
|
+
this.version++;
|
|
2265
|
+
return;
|
|
2266
|
+
}
|
|
2267
|
+
for (const subject of voice.named) {
|
|
2268
|
+
const head = this.chains.get(subject);
|
|
2269
|
+
if (head !== undefined)
|
|
2270
|
+
head.version = Number.NaN;
|
|
2271
|
+
}
|
|
2272
|
+
this.relinks++;
|
|
2273
|
+
}
|
|
1768
2274
|
/** Files a voice under each subject it names, or among the voices that name none. */
|
|
1769
2275
|
index(voice) {
|
|
1770
2276
|
if (voice.named === null) {
|