@msb235/blits 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/mixer.js ADDED
@@ -0,0 +1,788 @@
1
+ import { curve } from './easing.js';
2
+ import { builtOf, readKeys } from './patch.js';
3
+ import { Store } from './store.js';
4
+ const none = Object.freeze([]);
5
+ const keeper = (kept) => function keep(owner, init) {
6
+ if (kept.has(owner))
7
+ return kept.get(owner);
8
+ const made = init();
9
+ kept.set(owner, made);
10
+ return made;
11
+ };
12
+ const near = (a, b) => {
13
+ if (typeof a === 'number' && typeof b === 'number')
14
+ return Math.abs(a - b) < 1e-9;
15
+ if (Array.isArray(a) && Array.isArray(b))
16
+ return a.length === b.length && a.every((v, i) => near(v, b[i]));
17
+ return a === b;
18
+ };
19
+ const copy = (v) => (Array.isArray(v) ? [...v] : v);
20
+ class Voice {
21
+ id;
22
+ spec;
23
+ patch;
24
+ fade;
25
+ start;
26
+ state = 'pending';
27
+ rate;
28
+ weight;
29
+ /** elapsed = anchorElapsed + (now − anchorNow) · rate, rebased on a rate change or a seek. */
30
+ anchorNow;
31
+ anchorElapsed = 0;
32
+ out = null;
33
+ subjects = new Store();
34
+ /** Kit slot of each channel the patch writes, in `writes` order. */
35
+ slots;
36
+ /** The stops built ahead of time, for a `keys` patch. */
37
+ built;
38
+ ease;
39
+ /** Reused for every call this voice makes, so it is valid only during the call. */
40
+ setting;
41
+ /** Every subject this voice has been asked about, so a handover knows when it is finished. */
42
+ seen = 0;
43
+ /** The longest stagger of any subject seen, so a finite loop waits for the last of them. */
44
+ latest = 0;
45
+ restedCount = 0;
46
+ resolve;
47
+ done;
48
+ constructor(id, spec, patch, fade, now, start, slotOf, host, send) {
49
+ this.id = id;
50
+ this.spec = spec;
51
+ this.patch = patch;
52
+ this.fade = fade;
53
+ this.start = start;
54
+ this.slots = patch.writes.map((k) => slotOf.get(k));
55
+ this.built = patch.form === 'keys' && patch.keys ? builtOf(patch) : null;
56
+ this.ease = fade.ease === undefined ? undefined : curve(fade.ease);
57
+ this.setting = {
58
+ timestamp: 0,
59
+ dt: 0,
60
+ elapsed: 0,
61
+ pass: 0,
62
+ weight: 0,
63
+ state: undefined,
64
+ host,
65
+ keep: keeper(new Map()),
66
+ send,
67
+ };
68
+ this.rate = spec.rate ?? 1;
69
+ this.weight = typeof spec.weight === 'number' ? spec.weight : 1;
70
+ this.anchorNow = start;
71
+ this.done = new Promise((r) => {
72
+ this.resolve = r;
73
+ });
74
+ if (now >= start)
75
+ this.state = 'live';
76
+ }
77
+ elapsedAt(now) {
78
+ return this.anchorElapsed + (now - this.anchorNow) * this.rate;
79
+ }
80
+ rebase(now) {
81
+ this.anchorElapsed = this.elapsedAt(now);
82
+ this.anchorNow = now;
83
+ }
84
+ }
85
+ class Mixer {
86
+ kit;
87
+ opts;
88
+ voices = [];
89
+ pose = new Store();
90
+ nextId = 1;
91
+ /** The mix clock: the host's timestamp less every gap `rebase` has taken out. */
92
+ now = Number.NaN;
93
+ offset = 0;
94
+ last = Number.NaN;
95
+ rebasing = false;
96
+ wantsPose = false;
97
+ /** Read once a sync, so a `reduce` function is not called per voice per subject. */
98
+ reducedNow = false;
99
+ /**
100
+ * Per subject, the voices that may reach it, in voice order, so a probe walks those rather than
101
+ * every voice: a voice targeted at one subject is asked about that subject alone. Rebuilt when
102
+ * `version` moves, which is whenever the list or a voice's pending state changes.
103
+ */
104
+ reach = new Store();
105
+ version = 0;
106
+ /** How many voices in the list carry a `target`; with none, every voice reaches every subject. */
107
+ targeted = 0;
108
+ /** How many voices in the list carry a locus; with none, a fold allocates nothing. */
109
+ loci = 0;
110
+ /** Events sent since the last drain, and who is being probed, so `send` knows whose they are. */
111
+ sent = [];
112
+ sending = {
113
+ voice: null,
114
+ subject: undefined,
115
+ };
116
+ send = (event) => {
117
+ const { voice, subject } = this.sending;
118
+ if (voice === null)
119
+ return;
120
+ this.sent.push({
121
+ timestamp: voice.setting.timestamp,
122
+ subject,
123
+ voice: voice.id,
124
+ tags: voice.spec.tags ?? none,
125
+ event,
126
+ });
127
+ };
128
+ /** What `influence` and `peek` found besides the delta, read by the caller at once. */
129
+ w = 0;
130
+ h = null;
131
+ names;
132
+ channels;
133
+ slotOf = new Map();
134
+ constructor(kit, opts) {
135
+ this.kit = kit;
136
+ this.opts = opts;
137
+ this.names = Object.keys(kit);
138
+ this.channels = this.names.map((k) => kit[k]);
139
+ this.names.forEach((k, i) => {
140
+ this.slotOf.set(k, i);
141
+ });
142
+ }
143
+ get reduced() {
144
+ const r = this.opts.reduce;
145
+ return typeof r === 'function' ? r() : r === true;
146
+ }
147
+ get band() {
148
+ return this.opts.band ?? { on: 0.6, off: 0.4 };
149
+ }
150
+ cue(spec) {
151
+ const patch = spec.patch;
152
+ const engine = this.opts.engine ?? mixer;
153
+ if (!engine.runs.has(patch.form))
154
+ throw new Error(`blits: engine ${engine.name} does not run ${patch.form} patches`);
155
+ for (const channel of patch.writes) {
156
+ if (!(channel in this.kit))
157
+ throw new Error(`blits: kit has no channel ${String(channel)}, which this patch writes`);
158
+ const wanted = patch.kit?.[channel];
159
+ const here = this.kit[channel];
160
+ if (wanted && wanted !== here && (wanted.kind === undefined || wanted.kind !== here.kind))
161
+ throw new Error(`blits: channel ${String(channel)} is ${here.kind ?? 'a custom channel'} in this kit, but the patch was written for ${wanted.kind ?? 'a custom channel'}`);
162
+ }
163
+ if (patch.reads) {
164
+ const host = this.opts.host;
165
+ for (const field of patch.reads)
166
+ if (typeof host !== 'object' || host === null || !(field in host))
167
+ throw new Error(`blits: the patch reads host.${field}, which this mix's host lacks`);
168
+ }
169
+ if (spec.from === 'current' && patch.form !== 'keys')
170
+ throw new Error("blits: from: 'current' needs a keys patch");
171
+ const start = spec.start !== undefined ? spec.start - this.offset : Number.isNaN(this.now) ? 0 : this.now;
172
+ const voice = new Voice(this.nextId++, spec, patch, spec.fade ?? {}, this.now, start, this.slotOf, this.opts.host, this.send);
173
+ this.voices.push(voice);
174
+ this.version++;
175
+ if (spec.target !== undefined)
176
+ this.targeted++;
177
+ if (spec.locus !== undefined)
178
+ this.loci++;
179
+ if (spec.from === 'current')
180
+ this.wantsPose = true;
181
+ return this.handle(voice);
182
+ }
183
+ blend(patches, by, spec = {}) {
184
+ const locus = `blend:${this.nextId}`;
185
+ const stops = patches.length - 1;
186
+ return patches.map((patch, i) => {
187
+ const share = (subject, setting) => {
188
+ const k = by(subject, setting);
189
+ const d = stops === 0 ? 0 : Math.abs(k * stops - i);
190
+ return d >= 1 ? 0 : 1 - d;
191
+ };
192
+ const weight = by.input ? Object.assign(share, { input: true }) : share;
193
+ return this.cue({ ...spec, patch, weight, locus });
194
+ });
195
+ }
196
+ sync(timestamp) {
197
+ if (this.rebasing && !Number.isNaN(this.last))
198
+ this.offset += timestamp - this.last;
199
+ this.rebasing = false;
200
+ this.last = timestamp;
201
+ const now = timestamp - this.offset;
202
+ if (now === this.now)
203
+ return;
204
+ this.now = now;
205
+ this.reducedNow = this.reduced;
206
+ for (const voice of this.voices) {
207
+ if (voice.state === 'done')
208
+ continue;
209
+ if (voice.state === 'pending' && now >= voice.start) {
210
+ voice.state = 'live';
211
+ this.version++;
212
+ }
213
+ const period = voice.patch.period;
214
+ const loop = voice.spec.loop ?? true;
215
+ const passes = loop === true ? Number.POSITIVE_INFINITY : loop === false ? 1 : loop;
216
+ if (voice.state === 'live' && period > 0 && Number.isFinite(passes)) {
217
+ if (voice.elapsedAt(now) >= period * passes + voice.latest)
218
+ this.beginFade(voice, {});
219
+ }
220
+ if (voice.state === 'fading' && voice.out) {
221
+ const { at, over, rest, deadline } = voice.out;
222
+ const spent = now - at;
223
+ if (rest) {
224
+ const out = deadline !== undefined && spent >= deadline;
225
+ const settled = voice.seen > 0 && voice.restedCount >= voice.seen;
226
+ if (out || settled)
227
+ this.retire(voice);
228
+ }
229
+ else if (spent >= over)
230
+ this.retire(voice);
231
+ }
232
+ }
233
+ for (let i = this.voices.length - 1; i >= 0; i--) {
234
+ const voice = this.voices[i];
235
+ if (voice.state === 'done') {
236
+ this.voices.splice(i, 1);
237
+ this.version++;
238
+ if (voice.spec.target !== undefined)
239
+ this.targeted--;
240
+ if (voice.spec.locus !== undefined)
241
+ this.loci--;
242
+ }
243
+ }
244
+ }
245
+ probe(subject, out) {
246
+ const pose = this.fold(subject, out);
247
+ // Keeping last frame's pose is free when the mix allocated it; a host sampling into its own
248
+ // object only pays for the copy once something in the mix has asked to retarget from it.
249
+ if (out === undefined)
250
+ this.pose.set(subject, pose);
251
+ else if (this.wantsPose) {
252
+ const kept = {};
253
+ for (const key of this.names) {
254
+ const v = pose[key];
255
+ if (v !== undefined)
256
+ kept[key] = copy(v);
257
+ }
258
+ this.pose.set(subject, kept);
259
+ }
260
+ return pose;
261
+ }
262
+ atRest(subject) {
263
+ const pose = this.fold(subject, undefined, true);
264
+ for (let i = 0; i < this.names.length; i++) {
265
+ const channel = this.channels[i];
266
+ const value = pose[this.names[i]];
267
+ if (channel.rest === undefined) {
268
+ if (value !== undefined)
269
+ return false;
270
+ }
271
+ else if (!near(value, channel.rest))
272
+ return false;
273
+ }
274
+ return true;
275
+ }
276
+ rebase() {
277
+ this.rebasing = true;
278
+ }
279
+ get live() {
280
+ return this.voices.some((v) => v.state !== 'done');
281
+ }
282
+ mute(opts) {
283
+ for (const voice of this.voices)
284
+ this.beginFade(voice, { over: opts?.over });
285
+ }
286
+ drop(subject) {
287
+ this.pose.delete(subject);
288
+ this.reach.delete(subject);
289
+ for (const voice of this.voices)
290
+ voice.subjects.delete(subject);
291
+ }
292
+ drain(tag) {
293
+ const all = this.sent;
294
+ if (all.length === 0)
295
+ return [];
296
+ let out = all;
297
+ if (tag === undefined)
298
+ this.sent = [];
299
+ else {
300
+ out = [];
301
+ const kept = [];
302
+ for (const e of all)
303
+ (e.tags.includes(tag) ? out : kept).push(e);
304
+ this.sent = kept;
305
+ }
306
+ return out.sort((a, b) => a.timestamp - b.timestamp);
307
+ }
308
+ // ── internals ──────────────────────────────────────────────────────────────
309
+ handle(voice) {
310
+ const mix = this;
311
+ return {
312
+ id: voice.id,
313
+ get state() {
314
+ return voice.state;
315
+ },
316
+ get weight() {
317
+ return voice.weight;
318
+ },
319
+ set weight(w) {
320
+ voice.weight = w;
321
+ },
322
+ get rate() {
323
+ return voice.rate;
324
+ },
325
+ set rate(r) {
326
+ voice.rebase(Number.isNaN(mix.now) ? voice.start : mix.now);
327
+ voice.rate = r;
328
+ },
329
+ seek(elapsed) {
330
+ voice.anchorNow = Number.isNaN(mix.now) ? voice.start : mix.now;
331
+ voice.anchorElapsed = elapsed;
332
+ },
333
+ fade(opts) {
334
+ mix.beginFade(voice, opts ?? {});
335
+ },
336
+ weightOf(subject) {
337
+ if (voice.state === 'done')
338
+ return 0;
339
+ return voice.subjects.get(subject)?.weight ?? 0;
340
+ },
341
+ done: voice.done,
342
+ };
343
+ }
344
+ beginFade(voice, opts) {
345
+ if (voice.state === 'done' || voice.state === 'fading')
346
+ return;
347
+ const over = this.reduced ? 0 : (opts.over ?? voice.fade.out ?? 0);
348
+ voice.state = 'fading';
349
+ voice.out = {
350
+ from: 1,
351
+ at: Number.isNaN(this.now) ? voice.start : this.now,
352
+ over,
353
+ rest: opts.at === 'rest',
354
+ deadline: opts.deadline,
355
+ };
356
+ if (over === 0 && opts.at !== 'rest')
357
+ this.retire(voice);
358
+ }
359
+ retire(voice) {
360
+ voice.state = 'done';
361
+ voice.resolve();
362
+ }
363
+ /** The ramp a voice's own fade envelope applies this frame, 0..1. */
364
+ envelope(voice, now, since) {
365
+ const reduced = this.reducedNow;
366
+ const ease = voice.ease;
367
+ let w = 1;
368
+ const fadeIn = voice.fade.in ?? 0;
369
+ if (fadeIn > 0 && !reduced) {
370
+ const u = (now - since) / fadeIn;
371
+ if (u < 1)
372
+ w *= ease ? ease(Math.max(0, u)) : Math.max(0, u);
373
+ }
374
+ const out = voice.out;
375
+ if (out && !out.rest) {
376
+ if (reduced || out.over === 0)
377
+ return 0;
378
+ const u = 1 - (now - out.at) / out.over;
379
+ const clamped = u < 0 ? 0 : u > 1 ? 1 : u;
380
+ w *= ease ? ease(clamped) : clamped;
381
+ }
382
+ return w;
383
+ }
384
+ /** What this voice holds for this subject, made on first sight with `target` and `stagger` asked once. */
385
+ held(voice, subject, now) {
386
+ let held = voice.subjects.get(subject);
387
+ if (held !== undefined)
388
+ return held;
389
+ const reaches = voice.spec.target ? voice.spec.target(subject) : true;
390
+ const delay = reaches && voice.spec.stagger ? voice.spec.stagger(subject) : 0;
391
+ // When the voice clock reads `delay`, from where it is anchored now.
392
+ const since = voice.rate > 0
393
+ ? voice.anchorNow + (delay - voice.anchorElapsed) / voice.rate
394
+ : voice.start + delay;
395
+ const kept = new Map();
396
+ held = {
397
+ reaches,
398
+ delay,
399
+ since,
400
+ weight: 0,
401
+ rested: false,
402
+ bands: new Uint8Array(voice.slots.length),
403
+ state: reaches && voice.patch.state
404
+ ? voice.patch.state(subject)
405
+ : undefined,
406
+ stepped: now,
407
+ ticks: 0,
408
+ probed: Number.NaN,
409
+ delta: null,
410
+ kept,
411
+ keep: keeper(kept),
412
+ };
413
+ voice.subjects.set(subject, held);
414
+ if (reaches)
415
+ voice.seen++;
416
+ if (delay > voice.latest)
417
+ voice.latest = delay;
418
+ return held;
419
+ }
420
+ /** The weight a voice gives a subject this frame, with the setting already filled in. */
421
+ weigh(voice, subject, now, held) {
422
+ this.sending.voice = voice;
423
+ this.sending.subject = subject;
424
+ const signal = typeof voice.spec.weight === 'function' ? voice.spec.weight : null;
425
+ const raw = (signal ? signal(subject, voice.setting) : voice.weight) *
426
+ this.envelope(voice, now, held.since);
427
+ return raw < 0 ? 0 : raw > 1 ? 1 : raw;
428
+ }
429
+ /**
430
+ * One voice's delta for one subject this frame, or null when it does not reach. Its weight and
431
+ * the subject's record are left in `w` and `h`.
432
+ */
433
+ influence(voice, subject, now) {
434
+ if (voice.state === 'pending' || voice.state === 'done')
435
+ return null;
436
+ const held = this.held(voice, subject, now);
437
+ held.weight = 0;
438
+ if (!held.reaches)
439
+ return null;
440
+ if (voice.out?.rest && held.rested)
441
+ return null;
442
+ const elapsed = voice.elapsedAt(now) - held.delay;
443
+ if (elapsed < 0)
444
+ return null;
445
+ const period = voice.patch.period;
446
+ const loop = voice.spec.loop ?? true;
447
+ const passes = loop === true ? Number.POSITIVE_INFINITY : loop === false ? 1 : loop;
448
+ let phase = 0;
449
+ let pass = 0;
450
+ if (period > 0) {
451
+ const done = Number.isFinite(passes) && elapsed >= period * passes;
452
+ phase = done ? 1 : (elapsed % period) / period;
453
+ pass = done ? passes - 1 : Math.floor(elapsed / period);
454
+ }
455
+ const setting = voice.setting;
456
+ setting.timestamp = now;
457
+ const gap = now - held.stepped;
458
+ const cap = this.opts.maxDt;
459
+ setting.dt = this.reducedNow
460
+ ? Number.POSITIVE_INFINITY
461
+ : cap !== undefined && gap > cap
462
+ ? cap
463
+ : gap;
464
+ setting.elapsed = elapsed;
465
+ setting.pass = pass;
466
+ setting.weight = 0;
467
+ setting.state = held.state;
468
+ setting.keep = held.keep;
469
+ const weight = this.weigh(voice, subject, now, held);
470
+ setting.weight = weight;
471
+ held.weight = weight;
472
+ if (held.probed === now && held.delta) {
473
+ this.w = weight;
474
+ this.h = held;
475
+ return held.delta;
476
+ }
477
+ const tick = this.opts.stepMs;
478
+ if (voice.patch.step && held.probed !== now) {
479
+ if (tick !== undefined && tick > 0 && !this.reducedNow)
480
+ this.tick(voice, subject, held, tick);
481
+ else if (now !== held.stepped) {
482
+ voice.patch.step(held.state, setting.dt, subject, setting);
483
+ held.stepped = now;
484
+ }
485
+ }
486
+ let delta;
487
+ if (voice.built) {
488
+ let base;
489
+ if (voice.spec.from === 'current') {
490
+ if (held.base === undefined)
491
+ held.base = this.baseFor(voice, subject);
492
+ base = held.base;
493
+ }
494
+ delta = readKeys(voice.built, phase, held.delta ?? {}, base);
495
+ }
496
+ else {
497
+ delta = voice.patch.at(phase, subject, setting);
498
+ }
499
+ held.delta = delta;
500
+ held.probed = now;
501
+ if (voice.out?.rest && this.isRest(delta)) {
502
+ held.weight = 0;
503
+ held.rested = true;
504
+ voice.restedCount++;
505
+ return null;
506
+ }
507
+ this.w = weight;
508
+ this.h = held;
509
+ return delta;
510
+ }
511
+ /**
512
+ * Runs `step` once per whole interval between where this subject last stopped and now, on a grid
513
+ * counted from when its delay ran out, and leaves the remainder for the next sample. Counting
514
+ * rather than adding keeps the grid where it is however many samples it is reached through.
515
+ */
516
+ tick(voice, subject, held, tick) {
517
+ const now = this.now;
518
+ const setting = voice.setting;
519
+ const frameDt = setting.dt;
520
+ let n = held.ticks;
521
+ // The epsilon keeps an interval like 1000 / 120 from landing a hair short of a whole count.
522
+ const due = Math.floor((now - held.since) / tick + 1e-9);
523
+ const cap = this.opts.maxDt;
524
+ if (cap !== undefined) {
525
+ const most = Math.floor(cap / tick);
526
+ if (due - n > most)
527
+ n = due - most;
528
+ }
529
+ const step = voice.patch.step;
530
+ setting.dt = tick;
531
+ for (n++; n <= due; n++) {
532
+ setting.timestamp = held.since + n * tick;
533
+ step(held.state, tick, subject, setting);
534
+ }
535
+ held.ticks = due > held.ticks ? due : held.ticks;
536
+ held.stepped = held.since + held.ticks * tick;
537
+ setting.timestamp = now;
538
+ setting.dt = frameDt;
539
+ }
540
+ /**
541
+ * Stop 0 for a retargeting voice: the subject's pose in the frame before this voice contributes.
542
+ * With no such frame on record — a voice cued before the subject was ever probed, or a host that
543
+ * samples into its own object — it is this frame's pose with every other voice folded in.
544
+ */
545
+ baseFor(voice, subject) {
546
+ let prior = this.pose.get(subject);
547
+ if (prior === undefined && !this.folding.has(voice.id)) {
548
+ this.folding.add(voice.id);
549
+ try {
550
+ prior = this.fold(subject, undefined, false, voice.id);
551
+ }
552
+ finally {
553
+ this.folding.delete(voice.id);
554
+ }
555
+ }
556
+ const base = {};
557
+ for (const key of voice.patch.writes) {
558
+ const channel = this.kit[key];
559
+ const value = prior?.[key];
560
+ const fallback = channel.rest;
561
+ if (value !== undefined)
562
+ base[key] = copy(value);
563
+ else if (fallback !== undefined)
564
+ base[key] = copy(fallback);
565
+ }
566
+ return base;
567
+ }
568
+ isRest(delta) {
569
+ for (const key of Object.keys(delta)) {
570
+ if (delta[key] === undefined)
571
+ continue;
572
+ const channel = this.kit[key];
573
+ if (channel.rest === undefined)
574
+ return false;
575
+ if (!near(delta[key], channel.rest))
576
+ return false;
577
+ }
578
+ return true;
579
+ }
580
+ /** Folds a locus's members into one influence through each channel's own `lerp`. */
581
+ foldLocus(members) {
582
+ let sum = 0;
583
+ for (const m of members)
584
+ sum += m.weight;
585
+ const delta = {};
586
+ if (sum <= 0)
587
+ return { delta, weight: 0 };
588
+ const taken = new Map();
589
+ for (const member of members) {
590
+ if (member.weight <= 0)
591
+ continue;
592
+ for (const key of Object.keys(member.delta)) {
593
+ if (member.delta[key] === undefined)
594
+ continue;
595
+ const channel = this.kit[key];
596
+ const held = taken.get(key);
597
+ if (held === undefined) {
598
+ delta[key] = copy(member.delta[key]);
599
+ taken.set(key, member.weight);
600
+ continue;
601
+ }
602
+ const total = held + member.weight;
603
+ delta[key] = channel.lerp(delta[key], member.delta[key], member.weight / total);
604
+ taken.set(key, total);
605
+ }
606
+ }
607
+ return { delta, weight: sum > 1 ? 1 : sum };
608
+ }
609
+ /** Whether a rest-less channel's influence is switched on, across a band rather than an edge. */
610
+ passes(was, w) {
611
+ const { on, off } = this.band;
612
+ return w >= on ? true : w <= off ? false : (was ?? w >= on);
613
+ }
614
+ /**
615
+ * The voices that may reach this subject. A live voice is asked once, through the record it keeps
616
+ * for the subject anyway; a pending one is kept until it goes live, since `target` is asked on
617
+ * first sight, and sight only comes once a voice plays.
618
+ */
619
+ reaching(subject, now) {
620
+ if (this.targeted === 0)
621
+ return this.voices;
622
+ const held = this.reach.get(subject);
623
+ if (held !== undefined && held.version === this.version)
624
+ return held.voices;
625
+ const voices = [];
626
+ for (const voice of this.voices) {
627
+ if (voice.state === 'done')
628
+ continue;
629
+ if (voice.state === 'pending' || this.held(voice, subject, now).reaches)
630
+ voices.push(voice);
631
+ }
632
+ this.reach.set(subject, { version: this.version, voices });
633
+ return voices;
634
+ }
635
+ locusBands = new Store();
636
+ /** Voices whose stop 0 is mid-computation, so a fold for one cannot re-enter itself. */
637
+ folding = new Set();
638
+ /** Folds one voice's delta into the pose, its band state kept on the subject's record. */
639
+ apply(pose, voice, held, delta, weight) {
640
+ const slots = voice.slots;
641
+ for (let i = 0; i < slots.length; i++) {
642
+ const slot = slots[i];
643
+ const key = this.names[slot];
644
+ const value = delta[key];
645
+ if (value === undefined)
646
+ continue;
647
+ const channel = this.channels[slot];
648
+ if (channel.rest !== undefined && channel.scale) {
649
+ // pose[key] is the copy of rest this fold made, so it is ours to write into.
650
+ pose[key] = channel.fold
651
+ ? channel.fold(pose[key], value, weight)
652
+ : channel.merge(pose[key], channel.scale(value, weight));
653
+ continue;
654
+ }
655
+ const band = held.bands[i];
656
+ const on = this.passes(band === 0 ? undefined : band === 1, weight);
657
+ held.bands[i] = on ? 1 : 2;
658
+ if (on)
659
+ pose[key] = pose[key] === undefined ? copy(value) : channel.merge(pose[key], value);
660
+ }
661
+ }
662
+ fold(subject, out, dry = false, except) {
663
+ const now = this.now;
664
+ const pose = (out ?? {});
665
+ for (let i = 0; i < this.names.length; i++) {
666
+ const rest = this.channels[i].rest;
667
+ const key = this.names[i];
668
+ // Never `delete`: it drops a reused out object into dictionary mode for good.
669
+ if (rest !== undefined)
670
+ pose[key] = copy(rest);
671
+ else if (pose[key] !== undefined)
672
+ pose[key] = undefined;
673
+ }
674
+ if (Number.isNaN(now))
675
+ return pose;
676
+ const voices = this.reaching(subject, now);
677
+ if (this.loci === 0) {
678
+ for (const voice of voices) {
679
+ if (voice.id === except)
680
+ continue;
681
+ const delta = this.read(voice, subject, now, dry);
682
+ if (delta === null || this.w <= 0)
683
+ continue;
684
+ this.apply(pose, voice, this.h, delta, this.w);
685
+ }
686
+ return pose;
687
+ }
688
+ const order = [];
689
+ const weights = [];
690
+ const loci = new Map();
691
+ for (const voice of voices) {
692
+ if (voice.id === except)
693
+ continue;
694
+ const delta = this.read(voice, subject, now, dry);
695
+ if (delta === null)
696
+ continue;
697
+ const locus = voice.spec.locus;
698
+ if (locus === undefined) {
699
+ order.push({ voice, held: this.h, delta });
700
+ weights.push(this.w);
701
+ continue;
702
+ }
703
+ const members = loci.get(locus);
704
+ if (members)
705
+ members.push({ delta, weight: this.w });
706
+ else {
707
+ loci.set(locus, [{ delta, weight: this.w }]);
708
+ order.push(locus);
709
+ weights.push(0);
710
+ }
711
+ }
712
+ let bands = this.locusBands.get(subject);
713
+ if (bands === undefined) {
714
+ bands = new Map();
715
+ this.locusBands.set(subject, bands);
716
+ }
717
+ for (let n = 0; n < order.length; n++) {
718
+ const at = order[n];
719
+ if (typeof at !== 'string') {
720
+ if (weights[n] > 0)
721
+ this.apply(pose, at.voice, at.held, at.delta, weights[n]);
722
+ continue;
723
+ }
724
+ const { delta, weight } = this.foldLocus(loci.get(at));
725
+ if (weight <= 0)
726
+ continue;
727
+ for (const key of Object.keys(delta)) {
728
+ const channel = this.kit[key];
729
+ const value = delta[key];
730
+ if (value === undefined)
731
+ continue;
732
+ if (channel.rest !== undefined && channel.scale) {
733
+ // pose[key] is the copy of rest this fold made, so it is ours to write into.
734
+ pose[key] = channel.fold
735
+ ? channel.fold(pose[key], value, weight)
736
+ : channel.merge(pose[key], channel.scale(value, weight));
737
+ continue;
738
+ }
739
+ const band = `${at}:${key}`;
740
+ const on = this.passes(bands.get(band), weight);
741
+ bands.set(band, on);
742
+ if (on)
743
+ pose[key] = pose[key] === undefined ? copy(value) : channel.merge(pose[key], value);
744
+ }
745
+ }
746
+ return pose;
747
+ }
748
+ /** `influence`, or for a dry fold a subject already probed this frame, without advancing it. */
749
+ read(voice, subject, now, dry) {
750
+ if (dry) {
751
+ const held = voice.subjects.get(subject);
752
+ if (held?.reaches && held.probed === now && held.delta) {
753
+ const setting = voice.setting;
754
+ setting.timestamp = now;
755
+ setting.dt = 0;
756
+ setting.elapsed = voice.elapsedAt(now);
757
+ setting.pass = 0;
758
+ setting.weight = 0;
759
+ setting.state = held.state;
760
+ setting.keep = held.keep;
761
+ this.w = this.weigh(voice, subject, now, held);
762
+ this.h = held;
763
+ return held.delta;
764
+ }
765
+ }
766
+ return this.influence(voice, subject, now);
767
+ }
768
+ }
769
+ /**
770
+ * The one that ships: both forms on the CPU, probed per subject on demand.
771
+ *
772
+ * @category engine
773
+ */
774
+ export const mixer = {
775
+ name: 'mixer',
776
+ runs: new Set(['fn', 'keys']),
777
+ create(kit, opts) {
778
+ return new Mixer(kit, opts);
779
+ },
780
+ };
781
+ /**
782
+ * Makes a mix over a kit, on the engine the options name or the stock one.
783
+ *
784
+ * @category mix
785
+ */
786
+ export function mix(kit, opts = {}) {
787
+ return (opts.engine ?? mixer).create(kit, opts);
788
+ }