@msb235/blits 0.2.1 → 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 +140 -0
- package/README.md +9 -2
- package/dist/channels.d.ts +34 -5
- package/dist/channels.js +172 -23
- package/dist/clock.d.ts +19 -0
- package/dist/clock.js +44 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/lanes.d.ts +353 -0
- package/dist/lanes.js +1850 -0
- package/dist/mixer.d.ts +212 -2
- package/dist/mixer.js +1775 -167
- package/dist/motion.d.ts +209 -9
- package/dist/motion.js +674 -114
- package/dist/numbers.d.ts +22 -0
- package/dist/numbers.js +64 -0
- package/dist/patch.d.ts +13 -2
- package/dist/patch.js +44 -18
- package/dist/reading.d.ts +23 -0
- package/dist/reading.js +23 -0
- package/dist/types.d.ts +279 -18
- package/docs/schema.html +146 -27
- package/package.json +3 -6
package/dist/mixer.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
import { clampWeight, envelope, passAt, passesOf, phaseAt } from './clock.js';
|
|
1
2
|
import { curve } from './easing.js';
|
|
2
|
-
import {
|
|
3
|
+
import { clampRun, Lanes } from './lanes.js';
|
|
4
|
+
import { motionOf, noFrame, noRevive } from './motion.js';
|
|
5
|
+
import { builtOf, intosOf, readKeys } from './patch.js';
|
|
6
|
+
import { reading } from './reading.js';
|
|
3
7
|
import { Store } from './store.js';
|
|
4
8
|
const none = Object.freeze([]);
|
|
5
9
|
const keeper = (kept) => function keep(owner, init) {
|
|
@@ -7,8 +11,87 @@ const keeper = (kept) => function keep(owner, init) {
|
|
|
7
11
|
return kept.get(owner);
|
|
8
12
|
const made = init();
|
|
9
13
|
kept.set(owner, made);
|
|
14
|
+
reading.kept++;
|
|
10
15
|
return made;
|
|
11
16
|
};
|
|
17
|
+
/** The first record of a subject no live voice reaches, so a probe of it still makes one lookup. */
|
|
18
|
+
function stub() {
|
|
19
|
+
const kept = new Map();
|
|
20
|
+
return {
|
|
21
|
+
reaches: false,
|
|
22
|
+
delay: 0,
|
|
23
|
+
since: 0,
|
|
24
|
+
shown: 0,
|
|
25
|
+
weight: 0,
|
|
26
|
+
rested: false,
|
|
27
|
+
bands: new Uint8Array(0),
|
|
28
|
+
state: undefined,
|
|
29
|
+
stepped: 0,
|
|
30
|
+
ticks: 0,
|
|
31
|
+
probed: Number.NaN,
|
|
32
|
+
delta: null,
|
|
33
|
+
phase: 0,
|
|
34
|
+
seeks: 0,
|
|
35
|
+
kept,
|
|
36
|
+
keep: keeper(kept),
|
|
37
|
+
voice: null,
|
|
38
|
+
next: null,
|
|
39
|
+
version: Number.NaN,
|
|
40
|
+
loci: null,
|
|
41
|
+
slot: -1,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function elapsedWith(c, now) {
|
|
45
|
+
const dt = now - c.anchorNow;
|
|
46
|
+
const r = c.ramp;
|
|
47
|
+
if (r === null)
|
|
48
|
+
return c.anchorElapsed + dt * c.rate;
|
|
49
|
+
if (dt <= 0)
|
|
50
|
+
return c.anchorElapsed + dt * r.from;
|
|
51
|
+
const d = r.to - r.from;
|
|
52
|
+
if (dt <= r.over)
|
|
53
|
+
return c.anchorElapsed + r.from * dt + (d * dt * dt) / (2 * r.over);
|
|
54
|
+
return c.anchorElapsed + r.from * r.over + (d * r.over) / 2 + r.to * (dt - r.over);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The last entry taken before `t`, or at it where `inclusive` says so, as it does for a change a
|
|
58
|
+
* sync made. A host's change between frames is taken strictly: it shows from the next frame on, as
|
|
59
|
+
* it did live.
|
|
60
|
+
*/
|
|
61
|
+
function last(list, t, inclusive) {
|
|
62
|
+
let lo = 0;
|
|
63
|
+
let hi = list.length - 1;
|
|
64
|
+
let found;
|
|
65
|
+
while (lo <= hi) {
|
|
66
|
+
const mid = (lo + hi) >> 1;
|
|
67
|
+
const e = list[mid];
|
|
68
|
+
if (e.at < t || (e.at === t && (typeof inclusive === 'function' ? inclusive(e) : inclusive))) {
|
|
69
|
+
found = e;
|
|
70
|
+
lo = mid + 1;
|
|
71
|
+
}
|
|
72
|
+
else
|
|
73
|
+
hi = mid - 1;
|
|
74
|
+
}
|
|
75
|
+
return found;
|
|
76
|
+
}
|
|
77
|
+
/** A store that fills a missing entry from `make` on first ask. */
|
|
78
|
+
class Filled extends Store {
|
|
79
|
+
make;
|
|
80
|
+
constructor(make) {
|
|
81
|
+
super();
|
|
82
|
+
this.make = make;
|
|
83
|
+
}
|
|
84
|
+
get(key) {
|
|
85
|
+
let v = super.get(key);
|
|
86
|
+
if (v === undefined) {
|
|
87
|
+
v = this.make(key);
|
|
88
|
+
if (v !== undefined)
|
|
89
|
+
super.set(key, v);
|
|
90
|
+
}
|
|
91
|
+
return v;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const noSend = () => { };
|
|
12
95
|
const near = (a, b) => {
|
|
13
96
|
if (typeof a === 'number' && typeof b === 'number')
|
|
14
97
|
return Math.abs(a - b) < 1e-9;
|
|
@@ -17,7 +100,35 @@ const near = (a, b) => {
|
|
|
17
100
|
return a === b;
|
|
18
101
|
};
|
|
19
102
|
const copy = (v) => (Array.isArray(v) ? [...v] : v);
|
|
20
|
-
|
|
103
|
+
/** Writes a folded pose's value into subject `n`'s places in a column. */
|
|
104
|
+
function writeValue(c, v, n) {
|
|
105
|
+
const at = n * c.axes;
|
|
106
|
+
if (typeof v === 'number' && c.axes === 1)
|
|
107
|
+
c.out[at] = v;
|
|
108
|
+
else if (Array.isArray(v) && v.length === c.axes)
|
|
109
|
+
for (let a = 0; a < c.axes; a++)
|
|
110
|
+
c.out[at + a] = v[a];
|
|
111
|
+
else if (v === undefined)
|
|
112
|
+
c.out.fill(Number.NaN, at, at + c.axes);
|
|
113
|
+
else
|
|
114
|
+
throw new TypeError(`blits: pull reads ${c.key} as ${c.axes === 1 ? 'a number' : `${c.axes} numbers`} a subject, and the pose has ${String(v)}`);
|
|
115
|
+
}
|
|
116
|
+
/** Deep equality over plain data, for telling whether a host field changed. */
|
|
117
|
+
function same(a, b) {
|
|
118
|
+
if (a === b)
|
|
119
|
+
return true;
|
|
120
|
+
if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null)
|
|
121
|
+
return false;
|
|
122
|
+
const ka = Object.keys(a);
|
|
123
|
+
const kb = Object.keys(b);
|
|
124
|
+
if (ka.length !== kb.length)
|
|
125
|
+
return false;
|
|
126
|
+
for (const k of ka)
|
|
127
|
+
if (!same(a[k], b[k]))
|
|
128
|
+
return false;
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
export class Voice {
|
|
21
132
|
id;
|
|
22
133
|
spec;
|
|
23
134
|
patch;
|
|
@@ -36,13 +147,53 @@ class Voice {
|
|
|
36
147
|
ramp = null;
|
|
37
148
|
out = null;
|
|
38
149
|
subjects = new Store();
|
|
150
|
+
/** The subjects its spec names, or null where it names none. */
|
|
151
|
+
named;
|
|
39
152
|
/** Kit slot of each channel the patch writes, in `writes` order. */
|
|
40
153
|
slots;
|
|
41
154
|
/** The stops built ahead of time, for a `keys` patch. */
|
|
42
155
|
built;
|
|
43
156
|
/** The kit's `lerp` for each channel the patch writes, which keyed stops interpolate through. */
|
|
44
157
|
lerps;
|
|
158
|
+
/** Per keyed channel, the in-place form of the lerp its stops take, where it has one. */
|
|
159
|
+
intos;
|
|
160
|
+
/**
|
|
161
|
+
* The arrays keyed reads interpolate into: one set for the voice, not one per subject, since 10k
|
|
162
|
+
* subjects' worth of long-lived arrays costs more in cache misses than allocating them did.
|
|
163
|
+
*/
|
|
164
|
+
scratch = [];
|
|
165
|
+
/** The record whose delta holds `scratch`'s arrays; any other's must be read again to be handed out. */
|
|
166
|
+
holder = null;
|
|
167
|
+
/** Whether its channels run as lanes this frame, so the general fold passes it by. */
|
|
168
|
+
laned = false;
|
|
169
|
+
/** How many times it has been sought, so a delta read before a seek is not handed out after it. */
|
|
170
|
+
seeks = 0;
|
|
171
|
+
/** Whether its patch has kept state on a record through `setting.keep`, which makes it stateful. */
|
|
172
|
+
keeping = false;
|
|
173
|
+
/** For a motion patch, the hook it was given to ask the mix for its subjects' voice time. */
|
|
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;
|
|
45
181
|
ease;
|
|
182
|
+
/** How many passes its `loop` plays, worked out once. */
|
|
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;
|
|
46
197
|
/** Reused for every call this voice makes, so it is valid only during the call. */
|
|
47
198
|
setting;
|
|
48
199
|
/** Every subject this voice has been asked about, so a handover knows when it is finished. */
|
|
@@ -50,8 +201,20 @@ class Voice {
|
|
|
50
201
|
/** The longest stagger of any subject seen, so a finite loop waits for the last of them. */
|
|
51
202
|
latest = 0;
|
|
52
203
|
restedCount = 0;
|
|
204
|
+
/** The mix time it was cued at; -Infinity before the first sync. */
|
|
205
|
+
cuedAt = Number.NEGATIVE_INFINITY;
|
|
206
|
+
/** The mix time it left at. */
|
|
207
|
+
doneAt = Number.POSITIVE_INFINITY;
|
|
208
|
+
/** Under `history`, its controls after each change, oldest first. */
|
|
209
|
+
log = null;
|
|
210
|
+
/** Where an anchored `out` or `end` puts its fade's start, mix time; Infinity until known. */
|
|
211
|
+
outAt = Number.POSITIVE_INFINITY;
|
|
212
|
+
/** Whether its start is still to be fixed by an anchor, so it waits pending. */
|
|
213
|
+
placing = false;
|
|
53
214
|
resolve;
|
|
54
215
|
done;
|
|
216
|
+
play;
|
|
217
|
+
played;
|
|
55
218
|
constructor(id, spec, patch, fade, now, start, slotOf, channels, host, send) {
|
|
56
219
|
this.id = id;
|
|
57
220
|
this.spec = spec;
|
|
@@ -59,9 +222,22 @@ class Voice {
|
|
|
59
222
|
this.fade = fade;
|
|
60
223
|
this.start = start;
|
|
61
224
|
this.slots = patch.writes.map((k) => slotOf.get(k));
|
|
225
|
+
this.named = spec.subjects ? new Set(spec.subjects) : null;
|
|
62
226
|
this.built = patch.form === 'keys' && patch.keys ? builtOf(patch) : null;
|
|
63
227
|
this.lerps = this.slots.map((slot) => channels[slot].lerp);
|
|
228
|
+
this.intos = this.built
|
|
229
|
+
? intosOf(this.built, this.slots.map((slot) => channels[slot]))
|
|
230
|
+
: undefined;
|
|
64
231
|
this.ease = fade.ease === undefined ? undefined : curve(fade.ease);
|
|
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';
|
|
65
241
|
this.setting = {
|
|
66
242
|
timestamp: 0,
|
|
67
243
|
dt: 0,
|
|
@@ -79,20 +255,81 @@ class Voice {
|
|
|
79
255
|
this.done = new Promise((r) => {
|
|
80
256
|
this.resolve = r;
|
|
81
257
|
});
|
|
258
|
+
this.played = new Promise((r) => {
|
|
259
|
+
this.play = r;
|
|
260
|
+
});
|
|
82
261
|
if (now >= start)
|
|
83
262
|
this.state = 'live';
|
|
84
263
|
}
|
|
85
264
|
elapsedAt(now) {
|
|
86
|
-
|
|
265
|
+
return elapsedWith(this, now);
|
|
266
|
+
}
|
|
267
|
+
/** The mix time its clock reads `elapsed`, inverting `elapsedAt`; Infinity where it never will. */
|
|
268
|
+
timeAt(elapsed) {
|
|
269
|
+
const e0 = this.anchorElapsed;
|
|
87
270
|
const r = this.ramp;
|
|
88
|
-
if (r === null)
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
return
|
|
92
|
-
|
|
93
|
-
if (
|
|
94
|
-
return
|
|
95
|
-
|
|
271
|
+
if (r === null) {
|
|
272
|
+
if (this.rate > 0)
|
|
273
|
+
return this.anchorNow + (elapsed - e0) / this.rate;
|
|
274
|
+
return elapsed <= e0 ? this.anchorNow : Number.POSITIVE_INFINITY;
|
|
275
|
+
}
|
|
276
|
+
if (elapsed <= e0)
|
|
277
|
+
return r.from > 0 ? this.anchorNow + (elapsed - e0) / r.from : this.anchorNow;
|
|
278
|
+
const a = (r.to - r.from) / (2 * r.over);
|
|
279
|
+
const atEnd = e0 + r.from * r.over + a * r.over * r.over;
|
|
280
|
+
if (elapsed <= atEnd) {
|
|
281
|
+
const c = e0 - elapsed;
|
|
282
|
+
const dt = Math.abs(a) < 1e-12
|
|
283
|
+
? -c / r.from
|
|
284
|
+
: (-r.from + Math.sqrt(Math.max(0, r.from * r.from - 4 * a * c))) / (2 * a);
|
|
285
|
+
return this.anchorNow + dt;
|
|
286
|
+
}
|
|
287
|
+
if (r.to <= 0)
|
|
288
|
+
return Number.POSITIVE_INFINITY;
|
|
289
|
+
return this.anchorNow + r.over + (elapsed - atEnd) / r.to;
|
|
290
|
+
}
|
|
291
|
+
/** Records this voice's controls as they stand, from mix time `at`. */
|
|
292
|
+
note(at, sync = false) {
|
|
293
|
+
this.log?.push({
|
|
294
|
+
at,
|
|
295
|
+
sync,
|
|
296
|
+
anchorNow: this.anchorNow,
|
|
297
|
+
anchorElapsed: this.anchorElapsed,
|
|
298
|
+
rate: this.rate,
|
|
299
|
+
ramp: this.ramp,
|
|
300
|
+
weight: this.weight,
|
|
301
|
+
out: this.out,
|
|
302
|
+
start: this.start,
|
|
303
|
+
outAt: this.outAt,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* A copy for a projection: its own setting, which sends nothing, and its own per-subject records,
|
|
308
|
+
* filled from `fill`. With `controls` it takes those, as the voice stood at an earlier time.
|
|
309
|
+
*/
|
|
310
|
+
copy(fill, controls) {
|
|
311
|
+
const v = Object.assign(Object.create(Voice.prototype), this);
|
|
312
|
+
const w = v;
|
|
313
|
+
w.subjects = new Filled(fill);
|
|
314
|
+
w.setting = { ...this.setting, keep: keeper(new Map()), send: noSend };
|
|
315
|
+
v.scratch = [];
|
|
316
|
+
v.holder = null;
|
|
317
|
+
v.laned = false;
|
|
318
|
+
v.log = null;
|
|
319
|
+
v.early = null;
|
|
320
|
+
if (controls) {
|
|
321
|
+
v.anchorNow = controls.anchorNow;
|
|
322
|
+
v.anchorElapsed = controls.anchorElapsed;
|
|
323
|
+
v.rate = controls.rate;
|
|
324
|
+
v.ramp = controls.ramp;
|
|
325
|
+
v.weight = controls.weight;
|
|
326
|
+
v.out = controls.out;
|
|
327
|
+
v.start = controls.start;
|
|
328
|
+
v.outAt = controls.outAt;
|
|
329
|
+
}
|
|
330
|
+
v.resolve = noSend;
|
|
331
|
+
v.play = noSend;
|
|
332
|
+
return v;
|
|
96
333
|
}
|
|
97
334
|
rateAt(now) {
|
|
98
335
|
const r = this.ramp;
|
|
@@ -117,6 +354,25 @@ class Mixer {
|
|
|
117
354
|
kit;
|
|
118
355
|
opts;
|
|
119
356
|
voices = [];
|
|
357
|
+
/** Under `history`, voices that have left but that a read back may still reach. */
|
|
358
|
+
gone = [];
|
|
359
|
+
/** Set on a projection's own mixer: it sends nothing, keeps no history, and reads without committing. */
|
|
360
|
+
projecting = false;
|
|
361
|
+
/** Set on a projection reading back: a subject it has nothing on starts from its voice's start. */
|
|
362
|
+
backward = false;
|
|
363
|
+
/** True during a sync, so a control change it makes is recorded as showing in that frame. */
|
|
364
|
+
moving = false;
|
|
365
|
+
/**
|
|
366
|
+
* Under `history` with `inputs`, the host fields patches read, copied each frame they changed. In
|
|
367
|
+
* a projection reading back, the copy in force then.
|
|
368
|
+
*/
|
|
369
|
+
hostLog = [];
|
|
370
|
+
/**
|
|
371
|
+
* Marks the host announced on a score: mix time, NaN until the next sync for one announced as
|
|
372
|
+
* now before any sync; `made` is when it was announced, for a read back to know what was known.
|
|
373
|
+
*/
|
|
374
|
+
announced = [];
|
|
375
|
+
hostThen;
|
|
120
376
|
/** Each subject's last two poses and when they were probed, for `from: 'current'`. */
|
|
121
377
|
pose = new Store();
|
|
122
378
|
nextId = 1;
|
|
@@ -126,19 +382,55 @@ class Mixer {
|
|
|
126
382
|
last = Number.NaN;
|
|
127
383
|
rebasing = false;
|
|
128
384
|
wantsPose = false;
|
|
385
|
+
/** The pose `pull` folds a subject into where it cannot read straight from the lanes. */
|
|
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;
|
|
129
402
|
/** Read once a sync, so a `reduce` function is not called per voice per subject. */
|
|
130
403
|
reducedNow = false;
|
|
404
|
+
/** Something was changed since the last sync, so the next frame may differ from this one. */
|
|
405
|
+
stirred = false;
|
|
131
406
|
/**
|
|
132
|
-
* Per subject, the
|
|
133
|
-
*
|
|
134
|
-
*
|
|
407
|
+
* Per subject, the first record of the chain through every live voice that reaches it, or a stub
|
|
408
|
+
* where none does. Relinked when `version` moves, which is whenever the list or a voice's pending
|
|
409
|
+
* state changes.
|
|
135
410
|
*/
|
|
136
|
-
|
|
411
|
+
chains = new Store();
|
|
137
412
|
version = 0;
|
|
138
|
-
/**
|
|
139
|
-
|
|
413
|
+
/** Per subject, the voices whose `subjects` name it, in voice order. */
|
|
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();
|
|
426
|
+
/** How many voices in the list carry `subjects`. */
|
|
427
|
+
naming = 0;
|
|
428
|
+
/** The voices in the list that name no subjects, in voice order. */
|
|
429
|
+
general = [];
|
|
140
430
|
/** How many voices in the list carry a locus; with none, a fold allocates nothing. */
|
|
141
431
|
loci = 0;
|
|
432
|
+
/** How many voices in the list are anchored, so a sync with none skips placing them. */
|
|
433
|
+
anchored = 0;
|
|
142
434
|
/** Events sent since the last drain, and who is being probed, so `send` knows whose they are. */
|
|
143
435
|
sent = [];
|
|
144
436
|
sending = {
|
|
@@ -147,7 +439,7 @@ class Mixer {
|
|
|
147
439
|
};
|
|
148
440
|
send = (event) => {
|
|
149
441
|
const { voice, subject } = this.sending;
|
|
150
|
-
if (voice === null)
|
|
442
|
+
if (voice === null || this.projecting)
|
|
151
443
|
return;
|
|
152
444
|
this.sent.push({
|
|
153
445
|
timestamp: voice.setting.timestamp,
|
|
@@ -157,14 +449,14 @@ class Mixer {
|
|
|
157
449
|
event,
|
|
158
450
|
});
|
|
159
451
|
};
|
|
160
|
-
/**
|
|
452
|
+
/** The weight `influence` found besides the delta, read by the caller at once. */
|
|
161
453
|
w = 0;
|
|
162
|
-
h = null;
|
|
163
454
|
names;
|
|
164
455
|
channels;
|
|
165
456
|
/** Slots of the channels that declare bounds, clamped after every fold. */
|
|
166
457
|
bounded;
|
|
167
458
|
slotOf = new Map();
|
|
459
|
+
lanes;
|
|
168
460
|
constructor(kit, opts) {
|
|
169
461
|
this.kit = kit;
|
|
170
462
|
this.opts = opts;
|
|
@@ -174,6 +466,7 @@ class Mixer {
|
|
|
174
466
|
this.names.forEach((k, i) => {
|
|
175
467
|
this.slotOf.set(k, i);
|
|
176
468
|
});
|
|
469
|
+
this.lanes = opts.lanes === false ? null : new Lanes(this.laneHost());
|
|
177
470
|
}
|
|
178
471
|
get reduced() {
|
|
179
472
|
const r = this.opts.reduce;
|
|
@@ -203,16 +496,56 @@ class Mixer {
|
|
|
203
496
|
}
|
|
204
497
|
if (spec.from === 'current' && patch.form !== 'keys')
|
|
205
498
|
throw new Error("blits: from: 'current' needs a keys patch");
|
|
206
|
-
|
|
499
|
+
if (spec.subjects !== undefined && spec.target !== undefined)
|
|
500
|
+
throw new Error('blits: a voice takes target or subjects, not both');
|
|
501
|
+
const anchor = spec.anchor;
|
|
502
|
+
if (anchor)
|
|
503
|
+
this.checkPlacement(spec, anchor);
|
|
504
|
+
const anchored = anchor !== undefined && (anchor.start !== undefined || anchor.in !== undefined);
|
|
505
|
+
const start = anchored
|
|
506
|
+
? Number.POSITIVE_INFINITY
|
|
507
|
+
: spec.start !== undefined
|
|
508
|
+
? spec.start - this.offset
|
|
509
|
+
: Number.isNaN(this.now)
|
|
510
|
+
? 0
|
|
511
|
+
: this.now;
|
|
207
512
|
const voice = new Voice(this.nextId++, spec, patch, spec.fade ?? {}, this.now, start, this.slotOf, this.channels, this.opts.host, this.send);
|
|
513
|
+
if (!Number.isNaN(this.now)) {
|
|
514
|
+
voice.cuedAt = this.now;
|
|
515
|
+
voice.opened = this.now;
|
|
516
|
+
}
|
|
517
|
+
voice.placing = anchored;
|
|
518
|
+
const motion = voice.motion;
|
|
519
|
+
if (motion !== undefined) {
|
|
520
|
+
this.playing.set(motion, (this.playing.get(motion) ?? 0) + 1);
|
|
521
|
+
voice.frame = Mixer.frameHook(new WeakRef(this), new WeakRef(voice));
|
|
522
|
+
motion.frame = voice.frame;
|
|
523
|
+
voice.revive = Mixer.reviveHook(new WeakRef(this), new WeakRef(voice));
|
|
524
|
+
motion.revive = voice.revive;
|
|
525
|
+
}
|
|
208
526
|
this.voices.push(voice);
|
|
209
|
-
this.
|
|
210
|
-
|
|
211
|
-
|
|
527
|
+
this.index(voice);
|
|
528
|
+
this.changed(voice);
|
|
529
|
+
this.stirred = true;
|
|
212
530
|
if (spec.locus !== undefined)
|
|
213
531
|
this.loci++;
|
|
214
532
|
if (spec.from === 'current')
|
|
215
533
|
this.wantsPose = true;
|
|
534
|
+
if (anchor) {
|
|
535
|
+
this.anchored++;
|
|
536
|
+
this.place();
|
|
537
|
+
if (!Number.isNaN(this.now) && voice.state === 'pending' && this.now >= voice.start) {
|
|
538
|
+
voice.state = 'live';
|
|
539
|
+
this.started(voice);
|
|
540
|
+
this.changed(voice);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
// After placing, so the controls it starts with are where its anchors put it at the cue.
|
|
544
|
+
if (this.opts.history) {
|
|
545
|
+
voice.log = [];
|
|
546
|
+
voice.note(Number.NEGATIVE_INFINITY);
|
|
547
|
+
}
|
|
548
|
+
this.schedule(voice);
|
|
216
549
|
return this.handle(voice);
|
|
217
550
|
}
|
|
218
551
|
blend(patches, by, spec = {}) {
|
|
@@ -236,49 +569,225 @@ class Mixer {
|
|
|
236
569
|
const now = timestamp - this.offset;
|
|
237
570
|
if (now === this.now)
|
|
238
571
|
return;
|
|
572
|
+
this.move(now);
|
|
573
|
+
}
|
|
574
|
+
/** Moves the mix clock to `now`: voices start, finite loops end, and fades finish. */
|
|
575
|
+
move(now) {
|
|
576
|
+
this.moving = true;
|
|
577
|
+
try {
|
|
578
|
+
this.moveTo(now);
|
|
579
|
+
}
|
|
580
|
+
finally {
|
|
581
|
+
this.moving = false;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
moveTo(now) {
|
|
239
585
|
this.now = now;
|
|
240
586
|
this.reducedNow = this.reduced;
|
|
241
|
-
for (const
|
|
587
|
+
for (const a of this.announced)
|
|
588
|
+
if (Number.isNaN(a.at))
|
|
589
|
+
a.at = now;
|
|
590
|
+
if (this.anchored > 0)
|
|
591
|
+
this.place();
|
|
592
|
+
if (this.announced.length > 0) {
|
|
593
|
+
// Kept while still ahead, or while history reaches it; anchors waiting on one were placed above.
|
|
594
|
+
const reach = now - (this.opts.history?.ms ?? 0);
|
|
595
|
+
this.announced = this.announced.filter((a) => a.at >= reach);
|
|
596
|
+
}
|
|
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) {
|
|
242
600
|
if (voice.state === 'done')
|
|
243
601
|
continue;
|
|
602
|
+
if (Number.isNaN(voice.opened))
|
|
603
|
+
voice.opened = now;
|
|
604
|
+
if ((voice.state === 'live' || voice.state === 'held') && voice.outAt <= now)
|
|
605
|
+
this.beginFade(voice, {}, Math.max(voice.start, voice.outAt));
|
|
244
606
|
if (voice.state === 'pending' && now >= voice.start) {
|
|
245
607
|
voice.state = 'live';
|
|
246
|
-
this.
|
|
608
|
+
this.started(voice);
|
|
609
|
+
this.changed(voice);
|
|
247
610
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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';
|
|
254
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);
|
|
255
632
|
if (voice.state === 'fading' && voice.out) {
|
|
256
633
|
const { at, over, rest, deadline } = voice.out;
|
|
257
634
|
const spent = now - at;
|
|
258
635
|
if (rest) {
|
|
259
636
|
const out = deadline !== undefined && spent >= deadline;
|
|
260
637
|
const settled = voice.seen > 0 && voice.restedCount >= voice.seen;
|
|
261
|
-
if (out
|
|
638
|
+
if (out)
|
|
639
|
+
this.retire(voice, at + deadline);
|
|
640
|
+
else if (settled)
|
|
262
641
|
this.retire(voice);
|
|
263
642
|
}
|
|
264
643
|
else if (spent >= over)
|
|
265
|
-
this.retire(voice);
|
|
644
|
+
this.retire(voice, at + over);
|
|
266
645
|
}
|
|
646
|
+
if (!this.projecting)
|
|
647
|
+
this.schedule(voice);
|
|
267
648
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
649
|
+
this.stirred = false;
|
|
650
|
+
if (this.retired === 0) {
|
|
651
|
+
this.forget(now);
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
this.retired = 0;
|
|
655
|
+
let pruned = false;
|
|
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;
|
|
277
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--;
|
|
678
|
+
}
|
|
679
|
+
voices.length = kept;
|
|
680
|
+
if (pruned)
|
|
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) {
|
|
686
|
+
const history = this.opts.history;
|
|
687
|
+
if (history && this.gone.length > 0) {
|
|
688
|
+
const reach = now - history.ms;
|
|
689
|
+
this.gone = this.gone.filter((v) => v.doneAt >= reach);
|
|
278
690
|
}
|
|
279
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
|
+
}
|
|
280
784
|
probe(subject, out) {
|
|
281
785
|
const pose = this.fold(subject, out);
|
|
786
|
+
this.keep(subject, pose, out);
|
|
787
|
+
return pose;
|
|
788
|
+
}
|
|
789
|
+
/** After a probe, keeps the pose a retarget from `'current'` reads as the subject's last. */
|
|
790
|
+
keep(subject, pose, out) {
|
|
282
791
|
// Keeping last frame's pose is free when the mix allocated it; a host sampling into its own
|
|
283
792
|
// object only pays for the copy once something in the mix has asked to retarget from it.
|
|
284
793
|
let kept;
|
|
@@ -305,7 +814,240 @@ class Mixer {
|
|
|
305
814
|
rec.at = this.now;
|
|
306
815
|
}
|
|
307
816
|
}
|
|
308
|
-
|
|
817
|
+
}
|
|
818
|
+
pull(subjects, into) {
|
|
819
|
+
const columns = this.columnsOf(into);
|
|
820
|
+
const lanes = this.lanes;
|
|
821
|
+
const now = this.now;
|
|
822
|
+
if (this.scratch === undefined)
|
|
823
|
+
this.scratch = {};
|
|
824
|
+
const scratch = this.scratch;
|
|
825
|
+
let room = Number.POSITIVE_INFINITY;
|
|
826
|
+
let tightest = '';
|
|
827
|
+
for (const c of columns) {
|
|
828
|
+
const fits = Math.floor(c.out.length / c.axes);
|
|
829
|
+
if (fits < room) {
|
|
830
|
+
room = fits;
|
|
831
|
+
tightest = c.key;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
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;
|
|
895
|
+
const folded = this.foldWith(subject, scratch, head, laned, false);
|
|
896
|
+
const pose = (this.bounded.length === 0 ? folded : this.clamp(folded));
|
|
897
|
+
this.keep(subject, pose, scratch);
|
|
898
|
+
for (const c of columns)
|
|
899
|
+
writeValue(c, pose[c.key], n);
|
|
900
|
+
}
|
|
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;
|
|
909
|
+
}
|
|
910
|
+
/** The kit slot, width, rest and bounds of every channel `pull` was handed an array for. */
|
|
911
|
+
columnsOf(into) {
|
|
912
|
+
const columns = [];
|
|
913
|
+
for (const key of Object.keys(into)) {
|
|
914
|
+
const out = into[key];
|
|
915
|
+
if (out === undefined)
|
|
916
|
+
continue;
|
|
917
|
+
const slot = this.slotOf.get(key);
|
|
918
|
+
if (slot === undefined)
|
|
919
|
+
throw new Error(`blits: pull was handed ${key}, which the kit lacks`);
|
|
920
|
+
const channel = this.channels[slot];
|
|
921
|
+
const rest = channel.rest;
|
|
922
|
+
const axes = Array.isArray(rest) ? rest.length : 1;
|
|
923
|
+
const bounds = channel.bounds;
|
|
924
|
+
const at = new Float64Array(axes).fill(Number.NaN);
|
|
925
|
+
if (typeof rest === 'number')
|
|
926
|
+
at[0] = rest;
|
|
927
|
+
else if (Array.isArray(rest))
|
|
928
|
+
at.set(rest);
|
|
929
|
+
if (bounds !== undefined)
|
|
930
|
+
clampRun(at, 0, axes, bounds);
|
|
931
|
+
columns.push({ key, slot, axes, out, rest: at, bounds });
|
|
932
|
+
}
|
|
933
|
+
return columns;
|
|
934
|
+
}
|
|
935
|
+
project(timestamp) {
|
|
936
|
+
const t = timestamp - this.offset;
|
|
937
|
+
const c = new Mixer(this.kit, { ...this.opts, history: undefined, lanes: false });
|
|
938
|
+
c.projecting = true;
|
|
939
|
+
c.pose = this.pose;
|
|
940
|
+
c.offset = this.offset;
|
|
941
|
+
c.wantsPose = this.wantsPose;
|
|
942
|
+
c.nextId = this.nextId;
|
|
943
|
+
c.reducedNow = this.reducedNow;
|
|
944
|
+
if (Number.isNaN(this.now) || t >= this.now) {
|
|
945
|
+
c.now = this.now;
|
|
946
|
+
c.voices = this.voices
|
|
947
|
+
.filter((v) => v.state !== 'done')
|
|
948
|
+
.map((v) => v.copy((subject) => this.carry(v, subject)));
|
|
949
|
+
c.announced = this.announced.map((a) => ({ ...a }));
|
|
950
|
+
c.count();
|
|
951
|
+
c.move(t);
|
|
952
|
+
}
|
|
953
|
+
else {
|
|
954
|
+
const history = this.opts.history;
|
|
955
|
+
if (!history)
|
|
956
|
+
throw new Error('blits: reading back needs a mix made with history');
|
|
957
|
+
if (t < this.now - history.ms)
|
|
958
|
+
throw new Error(`blits: ${timestamp} is older than this mix's history reaches`);
|
|
959
|
+
c.now = t;
|
|
960
|
+
c.backward = true;
|
|
961
|
+
const was = last(this.hostLog, t, true);
|
|
962
|
+
const host = this.opts.host;
|
|
963
|
+
const then = was && typeof host === 'object' && host !== null
|
|
964
|
+
? Object.assign(Object.create(host), was.fields)
|
|
965
|
+
: undefined;
|
|
966
|
+
if (was)
|
|
967
|
+
c.hostThen = was;
|
|
968
|
+
c.announced = this.announced.filter((a) => a.made < t).map((a) => ({ ...a }));
|
|
969
|
+
c.voices = [...this.voices, ...this.gone]
|
|
970
|
+
.filter((v) => v.cuedAt <= t && v.doneAt > t)
|
|
971
|
+
.sort((a, b) => a.id - b.id)
|
|
972
|
+
.map((v) => {
|
|
973
|
+
const log = v.log;
|
|
974
|
+
const copy = v.copy((subject) => this.recall(v, subject, t), last(log, t, (e) => e.sync) ?? log[0]);
|
|
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';
|
|
983
|
+
if (then !== undefined)
|
|
984
|
+
copy.setting.host = then;
|
|
985
|
+
return copy;
|
|
986
|
+
});
|
|
987
|
+
c.count();
|
|
988
|
+
}
|
|
989
|
+
const read = (f) => {
|
|
990
|
+
reading.live = false;
|
|
991
|
+
try {
|
|
992
|
+
return f();
|
|
993
|
+
}
|
|
994
|
+
finally {
|
|
995
|
+
reading.live = true;
|
|
996
|
+
}
|
|
997
|
+
};
|
|
998
|
+
return {
|
|
999
|
+
timestamp,
|
|
1000
|
+
probe: (subject, out) => read(() => c.fold(subject, out)),
|
|
1001
|
+
assess: (subject) => read(() => {
|
|
1002
|
+
c.fold(subject);
|
|
1003
|
+
return c.doubts(subject);
|
|
1004
|
+
}),
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
announce(name, opts = {}) {
|
|
1008
|
+
const at = opts.at !== undefined ? opts.at - this.offset : this.now;
|
|
1009
|
+
this.announced.push({
|
|
1010
|
+
name,
|
|
1011
|
+
score: opts.score,
|
|
1012
|
+
tags: opts.tags ?? none,
|
|
1013
|
+
at,
|
|
1014
|
+
made: Number.isNaN(this.now) ? Number.NEGATIVE_INFINITY : this.now,
|
|
1015
|
+
order: this.nextId++,
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
marks(from, to) {
|
|
1019
|
+
const lo = from - this.offset;
|
|
1020
|
+
const hi = to - this.offset;
|
|
1021
|
+
const out = [];
|
|
1022
|
+
for (const a of this.announced)
|
|
1023
|
+
if (a.at >= lo && a.at <= hi)
|
|
1024
|
+
out.push({
|
|
1025
|
+
timestamp: a.at + this.offset,
|
|
1026
|
+
mark: undefined,
|
|
1027
|
+
voice: undefined,
|
|
1028
|
+
score: a.score,
|
|
1029
|
+
name: a.name,
|
|
1030
|
+
tags: a.tags,
|
|
1031
|
+
order: a.order,
|
|
1032
|
+
});
|
|
1033
|
+
for (const voice of [...this.voices, ...this.gone]) {
|
|
1034
|
+
for (const mark of ['start', 'in', 'out', 'end']) {
|
|
1035
|
+
const t = this.markOf(voice, mark);
|
|
1036
|
+
if (t === undefined || t < lo || t > hi)
|
|
1037
|
+
continue;
|
|
1038
|
+
out.push({
|
|
1039
|
+
timestamp: t + this.offset,
|
|
1040
|
+
mark,
|
|
1041
|
+
voice: voice.id,
|
|
1042
|
+
score: voice.spec.score,
|
|
1043
|
+
name: voice.spec.name,
|
|
1044
|
+
tags: voice.spec.tags ?? none,
|
|
1045
|
+
order: voice.id,
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
out.sort((a, b) => a.timestamp - b.timestamp || a.order - b.order);
|
|
1050
|
+
return out.map(({ order: _, ...m }) => m);
|
|
309
1051
|
}
|
|
310
1052
|
atRest(subject) {
|
|
311
1053
|
const pose = this.fold(subject, undefined, true);
|
|
@@ -327,15 +1069,99 @@ class Mixer {
|
|
|
327
1069
|
get live() {
|
|
328
1070
|
return this.voices.some((v) => v.state !== 'done');
|
|
329
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
|
+
}
|
|
330
1091
|
mute(opts) {
|
|
331
1092
|
for (const voice of this.voices)
|
|
332
1093
|
this.beginFade(voice, { over: opts?.over });
|
|
333
1094
|
}
|
|
334
1095
|
drop(subject) {
|
|
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
|
+
}
|
|
1102
|
+
if (head !== undefined && head.slot >= 0 && this.lanes !== null)
|
|
1103
|
+
this.lanes.release(head.slot);
|
|
335
1104
|
this.pose.delete(subject);
|
|
336
|
-
this.
|
|
337
|
-
|
|
1105
|
+
this.chains.delete(subject);
|
|
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) => {
|
|
338
1110
|
voice.subjects.delete(subject);
|
|
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;
|
|
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);
|
|
339
1165
|
}
|
|
340
1166
|
drain(tag) {
|
|
341
1167
|
const all = this.sent;
|
|
@@ -353,7 +1179,533 @@ class Mixer {
|
|
|
353
1179
|
}
|
|
354
1180
|
return out.sort((a, b) => a.timestamp - b.timestamp);
|
|
355
1181
|
}
|
|
1182
|
+
laneHost() {
|
|
1183
|
+
const mix = this;
|
|
1184
|
+
return {
|
|
1185
|
+
get voices() {
|
|
1186
|
+
return mix.voices;
|
|
1187
|
+
},
|
|
1188
|
+
channels: this.channels,
|
|
1189
|
+
names: this.names,
|
|
1190
|
+
fits: (voice) => mix.fits(voice),
|
|
1191
|
+
meet: (voice, subject) => mix.held(voice, subject, mix.now),
|
|
1192
|
+
naming: (subject) => (mix.naming === 0 ? undefined : mix.named.get(subject)),
|
|
1193
|
+
slotOf: (subject) => mix.chains.get(subject)?.slot ?? -1,
|
|
1194
|
+
envelope: (voice, since) => mix.envelope(voice, mix.now, since),
|
|
1195
|
+
parting: (voice, subject) => mix.parting(voice, subject, mix.now),
|
|
1196
|
+
ready: (voice, subject, held, elapsed, pass, weight) => {
|
|
1197
|
+
mix.prime(voice, held, mix.now, elapsed, pass);
|
|
1198
|
+
voice.setting.weight = weight;
|
|
1199
|
+
mix.sending.voice = voice;
|
|
1200
|
+
mix.sending.subject = subject;
|
|
1201
|
+
},
|
|
1202
|
+
horizon: (voice, delay) => {
|
|
1203
|
+
reading.horizon = mix.horizonFor(voice, delay, mix.now);
|
|
1204
|
+
},
|
|
1205
|
+
keeps: this.opts.history !== undefined,
|
|
1206
|
+
after: (voice, held) => mix.remember(voice, held),
|
|
1207
|
+
kept: (voice) => mix.stateful(voice),
|
|
1208
|
+
};
|
|
1209
|
+
}
|
|
1210
|
+
/**
|
|
1211
|
+
* Whether a voice's patch and spec can run on a lane, its channels aside. A voice whose patch has
|
|
1212
|
+
* kept state on a record through `setting.keep` is stateful from then on, and leaves its lane;
|
|
1213
|
+
* one that starts keeping partway through may advance that state once for one subject not
|
|
1214
|
+
* probed on the frame it starts, which declaring `state` avoids.
|
|
1215
|
+
*/
|
|
1216
|
+
fits(voice) {
|
|
1217
|
+
const spec = voice.spec;
|
|
1218
|
+
const patch = voice.patch;
|
|
1219
|
+
if (voice.keeping)
|
|
1220
|
+
return false;
|
|
1221
|
+
if (voice.state === 'pending' && voice.holdsBefore)
|
|
1222
|
+
return false;
|
|
1223
|
+
if (typeof spec.weight === 'function')
|
|
1224
|
+
return false;
|
|
1225
|
+
if (spec.locus !== undefined || spec.from === 'current')
|
|
1226
|
+
return false;
|
|
1227
|
+
if (voice.out?.rest)
|
|
1228
|
+
return false;
|
|
1229
|
+
if (patch.state !== undefined || patch.step !== undefined)
|
|
1230
|
+
return false;
|
|
1231
|
+
if (this.opts.history?.inputs && patch.reads !== undefined && patch.reads.length > 0)
|
|
1232
|
+
return false;
|
|
1233
|
+
const built = voice.built;
|
|
1234
|
+
return (built === null ||
|
|
1235
|
+
built.tracks.every((t, i) => t.lerp === undefined || t.lerp === voice.lerps[i]));
|
|
1236
|
+
}
|
|
1237
|
+
/** A voice's patch kept state on a record: it is stateful from now on. */
|
|
1238
|
+
stateful(voice) {
|
|
1239
|
+
if (voice.keeping)
|
|
1240
|
+
return;
|
|
1241
|
+
voice.keeping = true;
|
|
1242
|
+
this.lanes?.invalidate();
|
|
1243
|
+
}
|
|
1244
|
+
/**
|
|
1245
|
+
* What a motion patch asks for its subjects' voice time at the latest frame. It holds the mix and
|
|
1246
|
+
* the voice weakly, so a patch the host keeps does not keep a mix it has let go of alive.
|
|
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
|
+
}
|
|
1262
|
+
static frameHook(mix, voice) {
|
|
1263
|
+
return (subject) => {
|
|
1264
|
+
const m = mix.deref();
|
|
1265
|
+
const v = voice.deref();
|
|
1266
|
+
return m === undefined || v === undefined ? Number.NaN : m.frameOf(v, subject);
|
|
1267
|
+
};
|
|
1268
|
+
}
|
|
1269
|
+
/**
|
|
1270
|
+
* A subject's voice time at the latest frame, where a motion patch places an untimed change and a
|
|
1271
|
+
* `read` with no time; NaN until the voice has started and met the subject, and while the
|
|
1272
|
+
* subject's own time is still short of its stagger. A retired voice's patch no longer asks.
|
|
1273
|
+
*/
|
|
1274
|
+
frameOf(voice, subject) {
|
|
1275
|
+
if (Number.isNaN(this.now) || voice.state === 'pending')
|
|
1276
|
+
return Number.NaN;
|
|
1277
|
+
const held = voice.subjects.get(subject);
|
|
1278
|
+
if (held === undefined || !held.reaches)
|
|
1279
|
+
return Number.NaN;
|
|
1280
|
+
const t = voice.elapsedAt(this.now) - held.delay;
|
|
1281
|
+
return t < 0 ? Number.NaN : t;
|
|
1282
|
+
}
|
|
1283
|
+
/** Fills a voice's setting for a call to its patch, the weight aside. */
|
|
1284
|
+
prime(voice, held, now, elapsed, pass) {
|
|
1285
|
+
const setting = voice.setting;
|
|
1286
|
+
setting.timestamp = now;
|
|
1287
|
+
setting.dt = this.capped(now - held.stepped);
|
|
1288
|
+
setting.elapsed = elapsed;
|
|
1289
|
+
setting.pass = pass;
|
|
1290
|
+
setting.weight = 0;
|
|
1291
|
+
setting.state = held.state;
|
|
1292
|
+
setting.keep = held.keep;
|
|
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
|
+
}
|
|
1299
|
+
/** The earliest voice time a read may still ask for, for a subject delayed `delay`. */
|
|
1300
|
+
horizonFor(voice, delay, now) {
|
|
1301
|
+
const history = this.opts.history;
|
|
1302
|
+
return history === undefined
|
|
1303
|
+
? Number.POSITIVE_INFINITY
|
|
1304
|
+
: this.elapsedThen(voice, now - history.ms) - delay;
|
|
1305
|
+
}
|
|
356
1306
|
// ── internals ──────────────────────────────────────────────────────────────
|
|
1307
|
+
/** Refuses a placement that names a mark twice on one side, or that waits on itself. */
|
|
1308
|
+
checkPlacement(spec, anchor) {
|
|
1309
|
+
if (anchor.start !== undefined && anchor.in !== undefined)
|
|
1310
|
+
throw new Error('blits: a placement anchors start or in, not both');
|
|
1311
|
+
if (anchor.out !== undefined && anchor.end !== undefined)
|
|
1312
|
+
throw new Error('blits: a placement anchors out or end, not both');
|
|
1313
|
+
if (spec.start !== undefined && (anchor.start !== undefined || anchor.in !== undefined))
|
|
1314
|
+
throw new Error('blits: a voice takes start or an anchored start, not both');
|
|
1315
|
+
const name = spec.name;
|
|
1316
|
+
if (name === undefined)
|
|
1317
|
+
return;
|
|
1318
|
+
// A voice is known by its score and its name; a bare name in an anchor is in the asker's score.
|
|
1319
|
+
const key = (score, n) => `${score ?? ''}\u0000${n}`;
|
|
1320
|
+
const names = (p, score) => [p.start, p.in, p.out, p.end].flatMap((a) => {
|
|
1321
|
+
if (a === undefined || typeof a === 'number')
|
|
1322
|
+
return [];
|
|
1323
|
+
const q = 'of' in a ? a.of : 'after' in a ? a.after : 'with' in a ? a.with : a.before;
|
|
1324
|
+
if (typeof q === 'string')
|
|
1325
|
+
return [key(score, q)];
|
|
1326
|
+
return q.name === undefined ? [] : [key(q.score ?? score, q.name)];
|
|
1327
|
+
});
|
|
1328
|
+
const self = key(spec.score, name);
|
|
1329
|
+
const seen = new Set();
|
|
1330
|
+
const waits = names(anchor, spec.score);
|
|
1331
|
+
while (waits.length > 0) {
|
|
1332
|
+
const n = waits.pop();
|
|
1333
|
+
if (n === self)
|
|
1334
|
+
throw new Error(`blits: ${name}'s placement waits on itself`);
|
|
1335
|
+
if (seen.has(n))
|
|
1336
|
+
continue;
|
|
1337
|
+
seen.add(n);
|
|
1338
|
+
for (const v of this.voices)
|
|
1339
|
+
if (v.spec.name !== undefined && key(v.spec.score, v.spec.name) === n && v.spec.anchor)
|
|
1340
|
+
waits.push(...names(v.spec.anchor, v.spec.score));
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
/**
|
|
1344
|
+
* Fixes every anchored voice's start and out from what its anchors answer now. A start is fixed
|
|
1345
|
+
* while the voice waits and an out until its fade begins; a target that has left keeps the time it
|
|
1346
|
+
* last gave. Repeated so a chain of anchors settles in one sync.
|
|
1347
|
+
*/
|
|
1348
|
+
place() {
|
|
1349
|
+
for (let round = 0; round <= this.voices.length; round++) {
|
|
1350
|
+
let moved = false;
|
|
1351
|
+
for (const voice of this.voices) {
|
|
1352
|
+
const anchor = voice.spec.anchor;
|
|
1353
|
+
if (anchor === undefined || voice.state === 'done')
|
|
1354
|
+
continue;
|
|
1355
|
+
if (voice.placing && voice.state === 'pending') {
|
|
1356
|
+
const by = anchor.start ?? anchor.in;
|
|
1357
|
+
const t = by === undefined ? undefined : this.resolve(by, voice);
|
|
1358
|
+
if (t !== undefined) {
|
|
1359
|
+
const start = anchor.start !== undefined ? t : t - (voice.fade.in ?? 0);
|
|
1360
|
+
if (start !== voice.start) {
|
|
1361
|
+
voice.start = start;
|
|
1362
|
+
voice.anchorNow = start;
|
|
1363
|
+
voice.anchorElapsed = 0;
|
|
1364
|
+
this.noted(voice);
|
|
1365
|
+
moved = true;
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
if (voice.state !== 'fading') {
|
|
1370
|
+
const by = anchor.out ?? anchor.end;
|
|
1371
|
+
const t = by === undefined ? undefined : this.resolve(by, voice);
|
|
1372
|
+
if (t !== undefined) {
|
|
1373
|
+
const at = anchor.out !== undefined ? t : t - (voice.fade.out ?? 0);
|
|
1374
|
+
if (at !== voice.outAt) {
|
|
1375
|
+
voice.outAt = at;
|
|
1376
|
+
this.noted(voice);
|
|
1377
|
+
moved = true;
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
if (!moved)
|
|
1383
|
+
return;
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
/** The mix time an anchor answers, or undefined while its target has none. */
|
|
1387
|
+
resolve(a, self) {
|
|
1388
|
+
if (typeof a === 'number')
|
|
1389
|
+
return a - this.offset;
|
|
1390
|
+
let query;
|
|
1391
|
+
let mark;
|
|
1392
|
+
let by = a.by ?? 0;
|
|
1393
|
+
if ('of' in a) {
|
|
1394
|
+
query = a.of;
|
|
1395
|
+
mark = a.mark;
|
|
1396
|
+
}
|
|
1397
|
+
else if ('after' in a) {
|
|
1398
|
+
query = a.after;
|
|
1399
|
+
mark = 'end';
|
|
1400
|
+
}
|
|
1401
|
+
else if ('with' in a) {
|
|
1402
|
+
query = a.with;
|
|
1403
|
+
mark = 'start';
|
|
1404
|
+
}
|
|
1405
|
+
else {
|
|
1406
|
+
query = a.before;
|
|
1407
|
+
mark = 'start';
|
|
1408
|
+
by = -by;
|
|
1409
|
+
}
|
|
1410
|
+
const t = this.timeOf(typeof query === 'string' ? { name: query } : query, mark, self);
|
|
1411
|
+
return t === undefined ? undefined : t + by;
|
|
1412
|
+
}
|
|
1413
|
+
/**
|
|
1414
|
+
* The time a query answers: `mark` of the voice it picks, among those cued and those that have
|
|
1415
|
+
* left, never the asker; or a mark the host announced on the score, which is the same time for
|
|
1416
|
+
* any of the four. Undefined while what it picks has no such time.
|
|
1417
|
+
*/
|
|
1418
|
+
timeOf(q, mark, self) {
|
|
1419
|
+
const score = q.score ?? self.spec.score;
|
|
1420
|
+
const found = [];
|
|
1421
|
+
for (const v of [...this.gone, ...this.voices])
|
|
1422
|
+
if (v !== self &&
|
|
1423
|
+
v.spec.score === score &&
|
|
1424
|
+
(q.name === undefined || v.spec.name === q.name) &&
|
|
1425
|
+
(q.tag === undefined || (v.spec.tags ?? none).includes(q.tag)) &&
|
|
1426
|
+
(q.writes === undefined || v.patch.writes.includes(q.writes)))
|
|
1427
|
+
found.push({ order: v.id, t: this.markOf(v, mark) });
|
|
1428
|
+
if (q.writes === undefined)
|
|
1429
|
+
for (const a of this.announced)
|
|
1430
|
+
if (a.score === score &&
|
|
1431
|
+
(q.name === undefined || a.name === q.name) &&
|
|
1432
|
+
(q.tag === undefined || a.tags.includes(q.tag)))
|
|
1433
|
+
found.push({ order: a.order, t: Number.isNaN(a.at) ? undefined : a.at });
|
|
1434
|
+
if (found.length === 0)
|
|
1435
|
+
return undefined;
|
|
1436
|
+
found.sort((x, y) => x.order - y.order);
|
|
1437
|
+
const resolver = q.resolver ?? 'last';
|
|
1438
|
+
if (resolver === 'first')
|
|
1439
|
+
return found[0]?.t;
|
|
1440
|
+
if (resolver === 'last')
|
|
1441
|
+
return found[found.length - 1]?.t;
|
|
1442
|
+
const times = found.flatMap((e) => (e.t === undefined ? [] : [e.t])).sort((x, y) => x - y);
|
|
1443
|
+
if (resolver === 'next') {
|
|
1444
|
+
const now = Number.isNaN(this.now) ? Number.NEGATIVE_INFINITY : this.now;
|
|
1445
|
+
return times.find((t) => t >= now);
|
|
1446
|
+
}
|
|
1447
|
+
return resolver === 'earliest' ? times[0] : times[times.length - 1];
|
|
1448
|
+
}
|
|
1449
|
+
/** When a voice reaches a mark, mix time, or undefined while nothing has fixed it. */
|
|
1450
|
+
markOf(voice, mark) {
|
|
1451
|
+
const start = voice.start;
|
|
1452
|
+
if (!Number.isFinite(start))
|
|
1453
|
+
return undefined;
|
|
1454
|
+
if (mark === 'start')
|
|
1455
|
+
return start;
|
|
1456
|
+
if (mark === 'in')
|
|
1457
|
+
return start + (voice.fade.in ?? 0);
|
|
1458
|
+
const out = voice.out;
|
|
1459
|
+
let outAt;
|
|
1460
|
+
if (out !== null)
|
|
1461
|
+
outAt = out.at;
|
|
1462
|
+
else if (Number.isFinite(voice.outAt))
|
|
1463
|
+
outAt = voice.outAt;
|
|
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);
|
|
1468
|
+
}
|
|
1469
|
+
if (mark === 'out')
|
|
1470
|
+
return voice.state === 'done' && outAt === undefined ? voice.doneAt : outAt;
|
|
1471
|
+
if (voice.state === 'done')
|
|
1472
|
+
return voice.doneAt;
|
|
1473
|
+
if (out !== null) {
|
|
1474
|
+
if (out.rest)
|
|
1475
|
+
return out.deadline === undefined ? undefined : out.at + out.deadline;
|
|
1476
|
+
return out.at + out.over;
|
|
1477
|
+
}
|
|
1478
|
+
return outAt === undefined ? undefined : outAt + (this.reduced ? 0 : (voice.fade.out ?? 0));
|
|
1479
|
+
}
|
|
1480
|
+
/** A voice's elapsed at an earlier mix time, by the controls it had then. */
|
|
1481
|
+
elapsedThen(voice, t) {
|
|
1482
|
+
const log = voice.log;
|
|
1483
|
+
const c = log === null ? undefined : last(log, t, true);
|
|
1484
|
+
return elapsedWith(c ?? voice, t);
|
|
1485
|
+
}
|
|
1486
|
+
/** Records a change to a voice's controls under `history`, and lets go of what it no longer reaches. */
|
|
1487
|
+
noted(voice) {
|
|
1488
|
+
this.stirred = true;
|
|
1489
|
+
this.schedule(voice);
|
|
1490
|
+
const log = voice.log;
|
|
1491
|
+
if (log === null)
|
|
1492
|
+
return;
|
|
1493
|
+
voice.note(Number.isNaN(this.now) ? Number.NEGATIVE_INFINITY : this.now, this.moving);
|
|
1494
|
+
const reach = this.now - this.opts.history.ms;
|
|
1495
|
+
let drop = 0;
|
|
1496
|
+
while (drop + 1 < log.length && log[drop + 1].at <= reach)
|
|
1497
|
+
drop++;
|
|
1498
|
+
if (drop > 0)
|
|
1499
|
+
log.splice(0, drop);
|
|
1500
|
+
}
|
|
1501
|
+
/** Recounts what the fold's shortcuts depend on, for a projection's freshly copied voices. */
|
|
1502
|
+
count() {
|
|
1503
|
+
this.named = new Store();
|
|
1504
|
+
this.naming = 0;
|
|
1505
|
+
this.general = [];
|
|
1506
|
+
for (const voice of this.voices)
|
|
1507
|
+
this.index(voice);
|
|
1508
|
+
this.loci = this.voices.filter((v) => v.spec.locus !== undefined).length;
|
|
1509
|
+
this.anchored = this.voices.filter((v) => v.spec.anchor !== undefined).length;
|
|
1510
|
+
this.version++;
|
|
1511
|
+
}
|
|
1512
|
+
/** A copy of a subject's record that shares nothing a read could change. */
|
|
1513
|
+
copyHeld(voice, h) {
|
|
1514
|
+
const kept = new Map();
|
|
1515
|
+
for (const [owner, value] of h.kept)
|
|
1516
|
+
kept.set(owner, structuredClone(value));
|
|
1517
|
+
const patch = voice.patch;
|
|
1518
|
+
return {
|
|
1519
|
+
...h,
|
|
1520
|
+
bands: h.bands.slice(),
|
|
1521
|
+
state: h.state === undefined
|
|
1522
|
+
? undefined
|
|
1523
|
+
: patch.clone
|
|
1524
|
+
? patch.clone(h.state)
|
|
1525
|
+
: structuredClone(h.state),
|
|
1526
|
+
kept,
|
|
1527
|
+
keep: keeper(kept),
|
|
1528
|
+
probed: Number.NaN,
|
|
1529
|
+
delta: null,
|
|
1530
|
+
phase: 0,
|
|
1531
|
+
seeks: 0,
|
|
1532
|
+
base: h.base === undefined ? undefined : structuredClone(h.base),
|
|
1533
|
+
slope: h.slope === undefined ? undefined : structuredClone(h.slope),
|
|
1534
|
+
snaps: undefined,
|
|
1535
|
+
inputs: undefined,
|
|
1536
|
+
next: null,
|
|
1537
|
+
version: Number.NaN,
|
|
1538
|
+
loci: null,
|
|
1539
|
+
slot: -1,
|
|
1540
|
+
};
|
|
1541
|
+
}
|
|
1542
|
+
/** A projection ahead starts each subject from where the live mix holds it. */
|
|
1543
|
+
carry(voice, subject) {
|
|
1544
|
+
const live = voice.subjects.get(subject);
|
|
1545
|
+
if (live === undefined)
|
|
1546
|
+
return undefined;
|
|
1547
|
+
const h = this.copyHeld(voice, live);
|
|
1548
|
+
h.from = h.stepped;
|
|
1549
|
+
return h;
|
|
1550
|
+
}
|
|
1551
|
+
/**
|
|
1552
|
+
* A projection back starts each subject from the latest copy kept at or before `t`, else from its
|
|
1553
|
+
* voice's start with fresh state, which is exact for a voice stepped at a fixed interval.
|
|
1554
|
+
*/
|
|
1555
|
+
recall(voice, subject, t) {
|
|
1556
|
+
const live = voice.subjects.get(subject);
|
|
1557
|
+
if (live === undefined)
|
|
1558
|
+
return undefined;
|
|
1559
|
+
const snap = live.snaps && last(live.snaps, t, true);
|
|
1560
|
+
if (snap) {
|
|
1561
|
+
const h = this.copyHeld(voice, snap.held);
|
|
1562
|
+
h.from = h.stepped;
|
|
1563
|
+
h.replay = live.inputs;
|
|
1564
|
+
return h;
|
|
1565
|
+
}
|
|
1566
|
+
const kept = new Map();
|
|
1567
|
+
const stepped = live.since < t ? live.since : t;
|
|
1568
|
+
return {
|
|
1569
|
+
reaches: live.reaches,
|
|
1570
|
+
delay: live.delay,
|
|
1571
|
+
since: live.since,
|
|
1572
|
+
shown: live.shown,
|
|
1573
|
+
weight: 0,
|
|
1574
|
+
rested: false,
|
|
1575
|
+
bands: new Uint8Array(voice.slots.length),
|
|
1576
|
+
state: live.reaches && voice.patch.state
|
|
1577
|
+
? voice.patch.state(subject)
|
|
1578
|
+
: undefined,
|
|
1579
|
+
stepped,
|
|
1580
|
+
ticks: 0,
|
|
1581
|
+
probed: Number.NaN,
|
|
1582
|
+
delta: null,
|
|
1583
|
+
phase: 0,
|
|
1584
|
+
seeks: 0,
|
|
1585
|
+
kept,
|
|
1586
|
+
keep: keeper(kept),
|
|
1587
|
+
voice,
|
|
1588
|
+
next: null,
|
|
1589
|
+
version: Number.NaN,
|
|
1590
|
+
loci: null,
|
|
1591
|
+
slot: -1,
|
|
1592
|
+
from: stepped,
|
|
1593
|
+
unknown: voice.spec.from === 'current',
|
|
1594
|
+
replay: live.inputs,
|
|
1595
|
+
};
|
|
1596
|
+
}
|
|
1597
|
+
/** Under `history` with `inputs`, copies the host fields any voice reads, once a frame, when they changed. */
|
|
1598
|
+
recordHost(now) {
|
|
1599
|
+
const log = this.hostLog;
|
|
1600
|
+
const prev = log[log.length - 1];
|
|
1601
|
+
if (prev !== undefined && prev.at === now)
|
|
1602
|
+
return;
|
|
1603
|
+
const host = this.opts.host;
|
|
1604
|
+
const fields = {};
|
|
1605
|
+
for (const v of this.voices)
|
|
1606
|
+
for (const f of v.patch.reads ?? none)
|
|
1607
|
+
if (!(f in fields))
|
|
1608
|
+
fields[f] = structuredClone(host[f]);
|
|
1609
|
+
if (prev !== undefined && same(prev.fields, fields))
|
|
1610
|
+
return;
|
|
1611
|
+
log.push({ at: now, fields });
|
|
1612
|
+
const reach = now - this.opts.history.ms;
|
|
1613
|
+
while (log.length > 1 && log[1].at <= reach)
|
|
1614
|
+
log.shift();
|
|
1615
|
+
}
|
|
1616
|
+
/** Under `history` with `inputs`, keeps what an input signal read, each time it changes. */
|
|
1617
|
+
record(held, value) {
|
|
1618
|
+
const history = this.opts.history;
|
|
1619
|
+
if (!history?.inputs || this.projecting)
|
|
1620
|
+
return;
|
|
1621
|
+
const inputs = held.inputs ?? [];
|
|
1622
|
+
held.inputs = inputs;
|
|
1623
|
+
const prev = inputs[inputs.length - 1];
|
|
1624
|
+
if (prev !== undefined && (prev.value === value || prev.at === this.now)) {
|
|
1625
|
+
if (prev.at === this.now)
|
|
1626
|
+
prev.value = value;
|
|
1627
|
+
return;
|
|
1628
|
+
}
|
|
1629
|
+
inputs.push({ at: this.now, value });
|
|
1630
|
+
const reach = this.now - history.ms;
|
|
1631
|
+
while (inputs.length > 1 && inputs[1].at <= reach)
|
|
1632
|
+
inputs.shift();
|
|
1633
|
+
}
|
|
1634
|
+
/** Under `history`, keeps a copy of a stateful voice's record for this subject every so often. */
|
|
1635
|
+
remember(voice, held) {
|
|
1636
|
+
const history = this.opts.history;
|
|
1637
|
+
if (!history || this.projecting)
|
|
1638
|
+
return;
|
|
1639
|
+
if (voice.patch.step === undefined && held.kept.size === 0 && held.base === undefined)
|
|
1640
|
+
return;
|
|
1641
|
+
const snaps = held.snaps ?? [];
|
|
1642
|
+
held.snaps = snaps;
|
|
1643
|
+
const prev = snaps[snaps.length - 1];
|
|
1644
|
+
if (prev !== undefined && this.now - prev.at < (history.every ?? 200))
|
|
1645
|
+
return;
|
|
1646
|
+
snaps.push({ at: this.now, held: this.copyHeld(voice, held) });
|
|
1647
|
+
const reach = this.now - history.ms;
|
|
1648
|
+
while (snaps.length > 1 && snaps[1].at <= reach)
|
|
1649
|
+
snaps.shift();
|
|
1650
|
+
}
|
|
1651
|
+
/** Per channel, the least sure voice that fed it this frame. */
|
|
1652
|
+
doubts(subject) {
|
|
1653
|
+
const out = {};
|
|
1654
|
+
for (const name of this.names)
|
|
1655
|
+
out[name] = 'exact';
|
|
1656
|
+
const rank = { exact: 0, stepped: 1, held: 2 };
|
|
1657
|
+
// A voice still waiting on an anchor nothing has answered may yet play, or stop, by now.
|
|
1658
|
+
for (const voice of this.voices) {
|
|
1659
|
+
const anchor = voice.spec.anchor;
|
|
1660
|
+
if (anchor === undefined || voice.state === 'done')
|
|
1661
|
+
continue;
|
|
1662
|
+
const waiting = (voice.state === 'pending' && !Number.isFinite(voice.start)) ||
|
|
1663
|
+
(voice.state !== 'pending' &&
|
|
1664
|
+
voice.state !== 'fading' &&
|
|
1665
|
+
(anchor.out !== undefined || anchor.end !== undefined) &&
|
|
1666
|
+
!Number.isFinite(voice.outAt));
|
|
1667
|
+
if (!waiting || !this.aims(voice, subject))
|
|
1668
|
+
continue;
|
|
1669
|
+
for (const slot of voice.slots)
|
|
1670
|
+
out[this.names[slot]] = 'held';
|
|
1671
|
+
}
|
|
1672
|
+
for (let held = this.chain(subject, this.now); held !== null; held = held.next) {
|
|
1673
|
+
const voice = held.voice;
|
|
1674
|
+
if (voice === null || voice.state === 'done')
|
|
1675
|
+
continue;
|
|
1676
|
+
if (held.weight <= 0)
|
|
1677
|
+
continue;
|
|
1678
|
+
const d = this.doubtOf(voice, held);
|
|
1679
|
+
for (const slot of voice.slots) {
|
|
1680
|
+
const name = this.names[slot];
|
|
1681
|
+
if (rank[d] > rank[out[name]])
|
|
1682
|
+
out[name] = d;
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
return out;
|
|
1686
|
+
}
|
|
1687
|
+
doubtOf(voice, held) {
|
|
1688
|
+
const weight = voice.spec.weight;
|
|
1689
|
+
if (held.unknown)
|
|
1690
|
+
return 'held';
|
|
1691
|
+
// A weight read back from a recording is what the mix used then, so its signal's state is moot.
|
|
1692
|
+
const replayed = held.replay !== undefined && last(held.replay, this.now, true) !== undefined;
|
|
1693
|
+
if (typeof weight === 'function' && weight.input && !replayed)
|
|
1694
|
+
return 'held';
|
|
1695
|
+
const reads = voice.patch.reads;
|
|
1696
|
+
if (reads !== undefined && reads.length > 0) {
|
|
1697
|
+
const then = this.hostThen?.fields;
|
|
1698
|
+
if (then === undefined || !reads.every((f) => f in then))
|
|
1699
|
+
return 'held';
|
|
1700
|
+
}
|
|
1701
|
+
if (this.now > (held.from ?? this.now)) {
|
|
1702
|
+
const tick = this.opts.stepMs;
|
|
1703
|
+
const fixed = tick !== undefined && tick > 0 && !this.reducedNow;
|
|
1704
|
+
if ((voice.patch.step && !fixed) || (held.kept.size > 0 && !replayed))
|
|
1705
|
+
return 'stepped';
|
|
1706
|
+
}
|
|
1707
|
+
return 'exact';
|
|
1708
|
+
}
|
|
357
1709
|
handle(voice) {
|
|
358
1710
|
const mix = this;
|
|
359
1711
|
return {
|
|
@@ -361,11 +1713,14 @@ class Mixer {
|
|
|
361
1713
|
get state() {
|
|
362
1714
|
return voice.state;
|
|
363
1715
|
},
|
|
1716
|
+
played: voice.played,
|
|
364
1717
|
get weight() {
|
|
365
1718
|
return voice.weight;
|
|
366
1719
|
},
|
|
367
1720
|
set weight(w) {
|
|
368
1721
|
voice.weight = w;
|
|
1722
|
+
mix.noted(voice);
|
|
1723
|
+
mix.lanes?.refill();
|
|
369
1724
|
},
|
|
370
1725
|
get rate() {
|
|
371
1726
|
return voice.rateAt(Number.isNaN(mix.now) ? voice.start : mix.now);
|
|
@@ -374,6 +1729,8 @@ class Mixer {
|
|
|
374
1729
|
voice.rebase(Number.isNaN(mix.now) ? voice.start : mix.now);
|
|
375
1730
|
voice.ramp = null;
|
|
376
1731
|
voice.rate = r;
|
|
1732
|
+
mix.noted(voice);
|
|
1733
|
+
mix.lanes?.refill();
|
|
377
1734
|
},
|
|
378
1735
|
ramp(r, over) {
|
|
379
1736
|
const now = Number.isNaN(mix.now) ? voice.start : mix.now;
|
|
@@ -381,182 +1738,311 @@ class Mixer {
|
|
|
381
1738
|
const from = voice.rateAt(now);
|
|
382
1739
|
voice.ramp = over > 0 && r !== from ? { from, to: r, over } : null;
|
|
383
1740
|
voice.rate = r;
|
|
1741
|
+
mix.noted(voice);
|
|
1742
|
+
mix.lanes?.refill();
|
|
384
1743
|
},
|
|
385
1744
|
seek(elapsed) {
|
|
386
1745
|
voice.rebase(Number.isNaN(mix.now) ? voice.start : mix.now);
|
|
387
1746
|
voice.anchorElapsed = elapsed;
|
|
1747
|
+
voice.seeks++;
|
|
1748
|
+
mix.noted(voice);
|
|
1749
|
+
mix.lanes?.refill();
|
|
388
1750
|
},
|
|
389
1751
|
fade(opts) {
|
|
390
|
-
|
|
1752
|
+
if (opts !== undefined && 'subject' in opts)
|
|
1753
|
+
mix.fadeSubject(voice, opts.subject, opts.over);
|
|
1754
|
+
else
|
|
1755
|
+
mix.beginFade(voice, opts ?? {});
|
|
391
1756
|
},
|
|
392
1757
|
weightOf(subject) {
|
|
393
1758
|
if (voice.state === 'done')
|
|
394
1759
|
return 0;
|
|
1760
|
+
if (voice.laned && mix.lanes !== null) {
|
|
1761
|
+
const w = mix.lanes.weightOf(voice.id, mix.chains.get(subject)?.slot ?? -1);
|
|
1762
|
+
if (w !== undefined)
|
|
1763
|
+
return w;
|
|
1764
|
+
}
|
|
395
1765
|
return voice.subjects.get(subject)?.weight ?? 0;
|
|
396
1766
|
},
|
|
397
1767
|
done: voice.done,
|
|
398
1768
|
};
|
|
399
1769
|
}
|
|
400
|
-
|
|
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
|
+
}
|
|
1842
|
+
beginFade(voice, opts, at) {
|
|
401
1843
|
if (voice.state === 'done' || voice.state === 'fading')
|
|
402
1844
|
return;
|
|
403
1845
|
const over = this.reduced ? 0 : (opts.over ?? voice.fade.out ?? 0);
|
|
1846
|
+
// A voice is linked into its subjects' chains once it plays, and a fading one plays.
|
|
1847
|
+
if (voice.state === 'pending')
|
|
1848
|
+
this.changed(voice);
|
|
404
1849
|
voice.state = 'fading';
|
|
405
1850
|
voice.out = {
|
|
406
1851
|
from: 1,
|
|
407
|
-
at: Number.isNaN(this.now) ? voice.start : this.now,
|
|
1852
|
+
at: at ?? (Number.isNaN(this.now) ? voice.start : this.now),
|
|
408
1853
|
over,
|
|
409
1854
|
rest: opts.at === 'rest',
|
|
410
1855
|
deadline: opts.deadline,
|
|
411
1856
|
};
|
|
1857
|
+
this.noted(voice);
|
|
1858
|
+
if (opts.at === 'rest')
|
|
1859
|
+
this.lanes?.invalidate();
|
|
1860
|
+
else
|
|
1861
|
+
this.lanes?.refill();
|
|
412
1862
|
if (over === 0 && opts.at !== 'rest')
|
|
413
|
-
this.retire(voice);
|
|
1863
|
+
this.retire(voice, voice.out.at);
|
|
414
1864
|
}
|
|
415
|
-
|
|
1865
|
+
/** Removes a voice, recording that it left at `at`, default now. */
|
|
1866
|
+
retire(voice, at) {
|
|
1867
|
+
this.lanes?.touch(voice);
|
|
1868
|
+
const motion = voice.motion;
|
|
1869
|
+
if (motion !== undefined && motion.frame === voice.frame)
|
|
1870
|
+
motion.frame = noFrame;
|
|
1871
|
+
if (motion !== undefined && motion.revive === voice.revive)
|
|
1872
|
+
motion.revive = noRevive;
|
|
1873
|
+
voice.frame = null;
|
|
1874
|
+
voice.revive = null;
|
|
416
1875
|
voice.state = 'done';
|
|
1876
|
+
this.retired++;
|
|
1877
|
+
voice.doneAt = at ?? (Number.isNaN(this.now) ? Number.NEGATIVE_INFINITY : this.now);
|
|
1878
|
+
voice.play(false);
|
|
417
1879
|
voice.resolve();
|
|
418
1880
|
}
|
|
419
1881
|
/** The ramp a voice's own fade envelope applies this frame, 0..1. */
|
|
420
1882
|
envelope(voice, now, since) {
|
|
421
|
-
|
|
422
|
-
const ease = voice.ease;
|
|
423
|
-
let w = 1;
|
|
424
|
-
const fadeIn = voice.fade.in ?? 0;
|
|
425
|
-
if (fadeIn > 0 && !reduced) {
|
|
426
|
-
const u = (now - since) / fadeIn;
|
|
427
|
-
if (u < 1)
|
|
428
|
-
w *= ease ? ease(Math.max(0, u)) : Math.max(0, u);
|
|
429
|
-
}
|
|
430
|
-
const out = voice.out;
|
|
431
|
-
if (out && !out.rest) {
|
|
432
|
-
if (reduced || out.over === 0)
|
|
433
|
-
return 0;
|
|
434
|
-
const u = 1 - (now - out.at) / out.over;
|
|
435
|
-
const clamped = u < 0 ? 0 : u > 1 ? 1 : u;
|
|
436
|
-
w *= ease ? ease(clamped) : clamped;
|
|
437
|
-
}
|
|
438
|
-
return w;
|
|
1883
|
+
return envelope(voice.fade.in ?? 0, voice.out, voice.ease, this.reducedNow, now, since);
|
|
439
1884
|
}
|
|
440
1885
|
/** What this voice holds for this subject, made on first sight with `target` and `stagger` asked once. */
|
|
441
1886
|
held(voice, subject, now) {
|
|
442
1887
|
let held = voice.subjects.get(subject);
|
|
443
1888
|
if (held !== undefined)
|
|
444
1889
|
return held;
|
|
445
|
-
const reaches = voice
|
|
1890
|
+
const reaches = this.aims(voice, subject);
|
|
446
1891
|
const delay = reaches && voice.spec.stagger ? voice.spec.stagger(subject) : 0;
|
|
447
|
-
|
|
448
|
-
// the rate it is ramping to.
|
|
449
|
-
const since = voice.rate > 0
|
|
450
|
-
? voice.anchorNow + (delay - voice.anchorElapsed) / voice.rate
|
|
451
|
-
: voice.start + delay;
|
|
1892
|
+
const since = this.sinceOf(voice, delay);
|
|
452
1893
|
const kept = new Map();
|
|
453
1894
|
held = {
|
|
454
1895
|
reaches,
|
|
455
1896
|
delay,
|
|
456
1897
|
since,
|
|
1898
|
+
shown: this.shownOf(voice, since),
|
|
457
1899
|
weight: 0,
|
|
458
1900
|
rested: false,
|
|
459
1901
|
bands: new Uint8Array(voice.slots.length),
|
|
460
1902
|
state: reaches && voice.patch.state
|
|
461
1903
|
? voice.patch.state(subject)
|
|
462
1904
|
: undefined,
|
|
463
|
-
stepped: now,
|
|
1905
|
+
stepped: this.backward && since < now ? since : now,
|
|
464
1906
|
ticks: 0,
|
|
465
1907
|
probed: Number.NaN,
|
|
466
1908
|
delta: null,
|
|
1909
|
+
phase: 0,
|
|
1910
|
+
seeks: 0,
|
|
467
1911
|
kept,
|
|
468
1912
|
keep: keeper(kept),
|
|
1913
|
+
voice,
|
|
1914
|
+
next: null,
|
|
1915
|
+
version: Number.NaN,
|
|
1916
|
+
loci: null,
|
|
1917
|
+
slot: -1,
|
|
469
1918
|
};
|
|
1919
|
+
if (this.projecting) {
|
|
1920
|
+
held.from = held.stepped;
|
|
1921
|
+
held.unknown = this.backward && voice.spec.from === 'current';
|
|
1922
|
+
}
|
|
470
1923
|
voice.subjects.set(subject, held);
|
|
1924
|
+
if (voice.state === 'pending') {
|
|
1925
|
+
voice.early ??= [];
|
|
1926
|
+
voice.early.push(held);
|
|
1927
|
+
}
|
|
471
1928
|
if (reaches)
|
|
472
1929
|
voice.seen++;
|
|
473
|
-
if (delay > voice.latest)
|
|
1930
|
+
if (delay > voice.latest) {
|
|
474
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
|
+
}
|
|
475
1936
|
return held;
|
|
476
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
|
+
}
|
|
477
1961
|
/** The weight a voice gives a subject this frame, with the setting already filled in. */
|
|
478
1962
|
weigh(voice, subject, now, held) {
|
|
479
1963
|
this.sending.voice = voice;
|
|
480
1964
|
this.sending.subject = subject;
|
|
481
1965
|
const signal = typeof voice.spec.weight === 'function' ? voice.spec.weight : null;
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
1966
|
+
let base = voice.weight;
|
|
1967
|
+
if (signal) {
|
|
1968
|
+
const was = held.replay && last(held.replay, now, true);
|
|
1969
|
+
base = was ? was.value : signal(subject, voice.setting);
|
|
1970
|
+
if (signal.input && !was)
|
|
1971
|
+
this.record(held, base);
|
|
1972
|
+
}
|
|
1973
|
+
const fade = this.envelope(voice, now, held.shown);
|
|
1974
|
+
return clampWeight(base * (voice.parts === null ? fade : fade * this.parting(voice, subject, now)));
|
|
485
1975
|
}
|
|
486
1976
|
/**
|
|
487
|
-
* One voice's delta for one subject this frame,
|
|
488
|
-
*
|
|
1977
|
+
* One live voice's delta for one subject this frame, through its record for the subject, or null
|
|
1978
|
+
* when it does not reach. Its weight is left in `w`.
|
|
489
1979
|
*/
|
|
490
|
-
influence(voice, subject, now) {
|
|
491
|
-
if (voice.state === 'pending' || voice.state === 'done')
|
|
492
|
-
return null;
|
|
493
|
-
const held = this.held(voice, subject, now);
|
|
1980
|
+
influence(voice, subject, now, held) {
|
|
494
1981
|
held.weight = 0;
|
|
495
1982
|
if (!held.reaches)
|
|
496
1983
|
return null;
|
|
497
1984
|
if (voice.out?.rest && held.rested)
|
|
498
1985
|
return null;
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
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;
|
|
511
1998
|
}
|
|
1999
|
+
const period = voice.patch.period;
|
|
2000
|
+
const phase = phaseAt(elapsed, period, voice.passes);
|
|
2001
|
+
const pass = passAt(elapsed, period, voice.passes);
|
|
2002
|
+
this.prime(voice, held, now, elapsed, pass);
|
|
512
2003
|
const setting = voice.setting;
|
|
513
|
-
setting.timestamp = now;
|
|
514
|
-
const gap = now - held.stepped;
|
|
515
|
-
const cap = this.opts.maxDt;
|
|
516
|
-
setting.dt = this.reducedNow
|
|
517
|
-
? Number.POSITIVE_INFINITY
|
|
518
|
-
: cap !== undefined && gap > cap
|
|
519
|
-
? cap
|
|
520
|
-
: gap;
|
|
521
|
-
setting.elapsed = elapsed;
|
|
522
|
-
setting.pass = pass;
|
|
523
|
-
setting.weight = 0;
|
|
524
|
-
setting.state = held.state;
|
|
525
|
-
setting.keep = held.keep;
|
|
526
2004
|
const weight = this.weigh(voice, subject, now, held);
|
|
527
2005
|
setting.weight = weight;
|
|
528
2006
|
held.weight = weight;
|
|
529
|
-
if (held.probed === now && held.delta) {
|
|
2007
|
+
if (held.probed === now && held.delta && held.seeks === voice.seeks) {
|
|
2008
|
+
if (voice.holder !== held && voice.scratch.length > 0)
|
|
2009
|
+
this.keyed(voice, subject, held);
|
|
530
2010
|
this.w = weight;
|
|
531
|
-
this.h = held;
|
|
532
2011
|
return held.delta;
|
|
533
2012
|
}
|
|
2013
|
+
const keptBefore = reading.kept;
|
|
2014
|
+
const history = this.opts.history;
|
|
2015
|
+
if (history?.inputs && voice.patch.reads !== undefined && !this.projecting)
|
|
2016
|
+
this.recordHost(now);
|
|
2017
|
+
reading.horizon = this.horizonFor(voice, held.delay, now);
|
|
534
2018
|
const tick = this.opts.stepMs;
|
|
535
|
-
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;
|
|
536
2022
|
if (tick !== undefined && tick > 0 && !this.reducedNow)
|
|
537
|
-
this.tick(voice, subject, held, tick);
|
|
538
|
-
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);
|
|
539
2027
|
voice.patch.step(held.state, setting.dt, subject, setting);
|
|
540
|
-
held.stepped =
|
|
2028
|
+
held.stepped = to;
|
|
541
2029
|
}
|
|
542
2030
|
}
|
|
543
2031
|
let delta;
|
|
544
2032
|
if (voice.built) {
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
if (held.base === undefined) {
|
|
548
|
-
held.base = this.baseFor(voice, subject);
|
|
549
|
-
held.slope = this.slopeFor(voice, subject);
|
|
550
|
-
}
|
|
551
|
-
base = held.base;
|
|
552
|
-
}
|
|
553
|
-
delta = readKeys(voice.built, phase, held.delta ?? {}, base, voice.lerps, held.slope);
|
|
2033
|
+
held.phase = phase;
|
|
2034
|
+
delta = this.keyed(voice, subject, held);
|
|
554
2035
|
}
|
|
555
2036
|
else {
|
|
556
2037
|
delta = voice.patch.at(phase, subject, setting);
|
|
557
2038
|
}
|
|
558
2039
|
held.delta = delta;
|
|
559
2040
|
held.probed = now;
|
|
2041
|
+
held.seeks = voice.seeks;
|
|
2042
|
+
if (history !== undefined)
|
|
2043
|
+
this.remember(voice, held);
|
|
2044
|
+
if (reading.kept !== keptBefore && !voice.keeping)
|
|
2045
|
+
this.stateful(voice);
|
|
560
2046
|
if (voice.out?.rest && this.isRest(delta)) {
|
|
561
2047
|
held.weight = 0;
|
|
562
2048
|
held.rested = true;
|
|
@@ -564,7 +2050,22 @@ class Mixer {
|
|
|
564
2050
|
return null;
|
|
565
2051
|
}
|
|
566
2052
|
this.w = weight;
|
|
567
|
-
|
|
2053
|
+
return delta;
|
|
2054
|
+
}
|
|
2055
|
+
/** Reads a keys voice's stops at `held.phase` into the subject's delta, through the voice's scratch. */
|
|
2056
|
+
keyed(voice, subject, held) {
|
|
2057
|
+
let base;
|
|
2058
|
+
if (voice.spec.from === 'current') {
|
|
2059
|
+
if (held.base === undefined) {
|
|
2060
|
+
held.base = this.baseFor(voice, subject);
|
|
2061
|
+
held.slope = this.slopeFor(voice, subject);
|
|
2062
|
+
}
|
|
2063
|
+
base = held.base;
|
|
2064
|
+
}
|
|
2065
|
+
const delta = readKeys(voice.built, held.phase, held.delta ?? {}, base, voice.lerps, held.slope, voice.intos, voice.scratch);
|
|
2066
|
+
held.delta = delta;
|
|
2067
|
+
if (voice.scratch.length > 0)
|
|
2068
|
+
voice.holder = held;
|
|
568
2069
|
return delta;
|
|
569
2070
|
}
|
|
570
2071
|
/**
|
|
@@ -572,13 +2073,13 @@ class Mixer {
|
|
|
572
2073
|
* counted from when its delay ran out, and leaves the remainder for the next sample. Counting
|
|
573
2074
|
* rather than adding keeps the grid where it is however many samples it is reached through.
|
|
574
2075
|
*/
|
|
575
|
-
tick(voice, subject, held, tick) {
|
|
2076
|
+
tick(voice, subject, held, tick, to) {
|
|
576
2077
|
const now = this.now;
|
|
577
2078
|
const setting = voice.setting;
|
|
578
2079
|
const frameDt = setting.dt;
|
|
579
2080
|
let n = held.ticks;
|
|
580
2081
|
// The epsilon keeps an interval like 1000 / 120 from landing a hair short of a whole count.
|
|
581
|
-
const due = Math.floor((
|
|
2082
|
+
const due = Math.floor((to - held.since) / tick + 1e-9);
|
|
582
2083
|
const cap = this.opts.maxDt;
|
|
583
2084
|
if (cap !== undefined) {
|
|
584
2085
|
const most = Math.floor(cap / tick);
|
|
@@ -693,27 +2194,111 @@ class Mixer {
|
|
|
693
2194
|
return w >= on ? true : w <= off ? false : (was ?? w >= on);
|
|
694
2195
|
}
|
|
695
2196
|
/**
|
|
696
|
-
* The
|
|
697
|
-
* for the subject anyway; a pending one is
|
|
698
|
-
* first sight, and sight only comes once a voice plays
|
|
2197
|
+
* The first of this subject's records, linked through every live voice that reaches it. A voice
|
|
2198
|
+
* is asked once, through the record it keeps for the subject anyway; a pending one is linked when
|
|
2199
|
+
* it goes live, since `target` is asked on first sight, and sight only comes once a voice plays
|
|
2200
|
+
* or holds before.
|
|
699
2201
|
*/
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
const
|
|
707
|
-
|
|
708
|
-
if (
|
|
2202
|
+
chain(subject, now) {
|
|
2203
|
+
const was = this.chains.get(subject);
|
|
2204
|
+
if (was !== undefined && was.version === this.version)
|
|
2205
|
+
return was;
|
|
2206
|
+
let first = null;
|
|
2207
|
+
let prev = null;
|
|
2208
|
+
const link = (voice) => {
|
|
2209
|
+
const state = voice.state;
|
|
2210
|
+
if (state === 'done' || (state === 'pending' && !voice.holdsBefore))
|
|
2211
|
+
return;
|
|
2212
|
+
const held = this.held(voice, subject, now);
|
|
2213
|
+
if (!held.reaches)
|
|
2214
|
+
return;
|
|
2215
|
+
held.voice = voice;
|
|
2216
|
+
held.next = null;
|
|
2217
|
+
if (prev === null)
|
|
2218
|
+
first = held;
|
|
2219
|
+
else
|
|
2220
|
+
prev.next = held;
|
|
2221
|
+
prev = held;
|
|
2222
|
+
};
|
|
2223
|
+
const named = this.naming === 0 ? undefined : this.named.get(subject);
|
|
2224
|
+
let j = 0;
|
|
2225
|
+
for (const voice of this.general) {
|
|
2226
|
+
while (named !== undefined && j < named.length && named[j].id < voice.id)
|
|
2227
|
+
link(named[j++]);
|
|
2228
|
+
link(voice);
|
|
2229
|
+
}
|
|
2230
|
+
while (named !== undefined && j < named.length)
|
|
2231
|
+
link(named[j++]);
|
|
2232
|
+
const head = first ?? stub();
|
|
2233
|
+
head.version = this.version;
|
|
2234
|
+
if (was !== undefined && was !== head) {
|
|
2235
|
+
head.loci = was.loci;
|
|
2236
|
+
was.loci = null;
|
|
2237
|
+
head.slot = was.slot;
|
|
2238
|
+
was.slot = -1;
|
|
2239
|
+
}
|
|
2240
|
+
else if (was === undefined && this.lanes !== null && head.slot < 0)
|
|
2241
|
+
head.slot = this.lanes.number(subject);
|
|
2242
|
+
if (was !== head)
|
|
2243
|
+
this.chains.set(subject, head);
|
|
2244
|
+
return head;
|
|
2245
|
+
}
|
|
2246
|
+
/** Whether a voice reaches a subject by what its spec says: the subjects it names, or its `target`. */
|
|
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;
|
|
2252
|
+
if (voice.named !== null)
|
|
2253
|
+
return voice.named.has(subject);
|
|
2254
|
+
return voice.spec.target ? voice.spec.target(subject) : true;
|
|
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
|
+
}
|
|
2274
|
+
/** Files a voice under each subject it names, or among the voices that name none. */
|
|
2275
|
+
index(voice) {
|
|
2276
|
+
if (voice.named === null) {
|
|
2277
|
+
this.general.push(voice);
|
|
2278
|
+
return;
|
|
2279
|
+
}
|
|
2280
|
+
this.naming++;
|
|
2281
|
+
for (const subject of voice.named) {
|
|
2282
|
+
const list = this.named.get(subject);
|
|
2283
|
+
if (list === undefined)
|
|
2284
|
+
this.named.set(subject, [voice]);
|
|
2285
|
+
else
|
|
2286
|
+
list.push(voice);
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
unindex(voice) {
|
|
2290
|
+
this.naming--;
|
|
2291
|
+
for (const subject of voice.named) {
|
|
2292
|
+
const list = this.named.get(subject);
|
|
2293
|
+
if (list === undefined)
|
|
709
2294
|
continue;
|
|
710
|
-
|
|
711
|
-
|
|
2295
|
+
const i = list.indexOf(voice);
|
|
2296
|
+
if (i >= 0)
|
|
2297
|
+
list.splice(i, 1);
|
|
2298
|
+
if (list.length === 0)
|
|
2299
|
+
this.named.delete(subject);
|
|
712
2300
|
}
|
|
713
|
-
this.reach.set(subject, { version: this.version, voices });
|
|
714
|
-
return voices;
|
|
715
2301
|
}
|
|
716
|
-
locusBands = new Store();
|
|
717
2302
|
/** Voices whose stop 0 is mid-computation, so a fold for one cannot re-enter itself. */
|
|
718
2303
|
folding = new Set();
|
|
719
2304
|
/** Folds one voice's delta into the pose, its band state kept on the subject's record. */
|
|
@@ -762,8 +2347,20 @@ class Mixer {
|
|
|
762
2347
|
}
|
|
763
2348
|
folded(subject, out, dry = false, except) {
|
|
764
2349
|
const now = this.now;
|
|
765
|
-
const
|
|
2350
|
+
const head = Number.isNaN(now) ? null : this.chain(subject, now);
|
|
2351
|
+
// A subject numbered since the frame's fill folds every voice here, laned ones included.
|
|
2352
|
+
const laned = head !== null && this.lanes?.prepare(head.slot, subject, now, this.version) === true;
|
|
2353
|
+
return this.foldWith(subject, out ?? {}, head, laned, dry, except);
|
|
2354
|
+
}
|
|
2355
|
+
/** The fold after a subject's chain is linked and its lanes asked whether it reads from them. */
|
|
2356
|
+
foldWith(subject, out, head, laned, dry, except) {
|
|
2357
|
+
const now = this.now;
|
|
2358
|
+
const pose = out;
|
|
2359
|
+
const lanes = this.lanes;
|
|
2360
|
+
const skip = laned ? lanes.copies : null;
|
|
766
2361
|
for (let i = 0; i < this.names.length; i++) {
|
|
2362
|
+
if (skip?.[i])
|
|
2363
|
+
continue;
|
|
767
2364
|
const rest = this.channels[i].rest;
|
|
768
2365
|
const key = this.names[i];
|
|
769
2366
|
// Never `delete`: it drops a reused out object into dictionary mode for good.
|
|
@@ -772,32 +2369,42 @@ class Mixer {
|
|
|
772
2369
|
else if (pose[key] !== undefined)
|
|
773
2370
|
pose[key] = undefined;
|
|
774
2371
|
}
|
|
775
|
-
if (
|
|
2372
|
+
if (head === null)
|
|
776
2373
|
return pose;
|
|
777
|
-
|
|
2374
|
+
if (laned) {
|
|
2375
|
+
lanes.copy(head.slot, pose);
|
|
2376
|
+
if (lanes.whole)
|
|
2377
|
+
return pose;
|
|
2378
|
+
}
|
|
778
2379
|
if (this.loci === 0) {
|
|
779
|
-
for (
|
|
780
|
-
|
|
2380
|
+
for (let held = head; held !== null; held = held.next) {
|
|
2381
|
+
const voice = held.voice;
|
|
2382
|
+
if (voice === null || (laned && voice.laned))
|
|
781
2383
|
continue;
|
|
782
|
-
|
|
2384
|
+
if (voice.id === except || voice.state === 'done')
|
|
2385
|
+
continue;
|
|
2386
|
+
const delta = this.read(voice, subject, now, dry, held);
|
|
783
2387
|
if (delta === null || this.w <= 0)
|
|
784
2388
|
continue;
|
|
785
|
-
this.apply(pose, voice,
|
|
2389
|
+
this.apply(pose, voice, held, delta, this.w);
|
|
786
2390
|
}
|
|
787
2391
|
return pose;
|
|
788
2392
|
}
|
|
789
2393
|
const order = [];
|
|
790
2394
|
const weights = [];
|
|
791
2395
|
const loci = new Map();
|
|
792
|
-
for (
|
|
793
|
-
|
|
2396
|
+
for (let held = head; held !== null; held = held.next) {
|
|
2397
|
+
const voice = held.voice;
|
|
2398
|
+
if (voice === null || (laned && voice.laned))
|
|
2399
|
+
continue;
|
|
2400
|
+
if (voice.id === except || voice.state === 'done')
|
|
794
2401
|
continue;
|
|
795
|
-
const delta = this.read(voice, subject, now, dry);
|
|
2402
|
+
const delta = this.read(voice, subject, now, dry, held);
|
|
796
2403
|
if (delta === null)
|
|
797
2404
|
continue;
|
|
798
2405
|
const locus = voice.spec.locus;
|
|
799
2406
|
if (locus === undefined) {
|
|
800
|
-
order.push({ voice, held
|
|
2407
|
+
order.push({ voice, held, delta });
|
|
801
2408
|
weights.push(this.w);
|
|
802
2409
|
continue;
|
|
803
2410
|
}
|
|
@@ -810,10 +2417,10 @@ class Mixer {
|
|
|
810
2417
|
weights.push(0);
|
|
811
2418
|
}
|
|
812
2419
|
}
|
|
813
|
-
let bands =
|
|
814
|
-
if (bands ===
|
|
2420
|
+
let bands = head.loci;
|
|
2421
|
+
if (bands === null) {
|
|
815
2422
|
bands = new Map();
|
|
816
|
-
|
|
2423
|
+
head.loci = bands;
|
|
817
2424
|
}
|
|
818
2425
|
for (let n = 0; n < order.length; n++) {
|
|
819
2426
|
const at = order[n];
|
|
@@ -847,10 +2454,9 @@ class Mixer {
|
|
|
847
2454
|
return pose;
|
|
848
2455
|
}
|
|
849
2456
|
/** `influence`, or for a dry fold a subject already probed this frame, without advancing it. */
|
|
850
|
-
read(voice, subject, now, dry) {
|
|
2457
|
+
read(voice, subject, now, dry, held) {
|
|
851
2458
|
if (dry) {
|
|
852
|
-
|
|
853
|
-
if (held?.reaches && held.probed === now && held.delta) {
|
|
2459
|
+
if (held.reaches && held.probed === now && held.delta && held.seeks === voice.seeks) {
|
|
854
2460
|
const setting = voice.setting;
|
|
855
2461
|
setting.timestamp = now;
|
|
856
2462
|
setting.dt = 0;
|
|
@@ -860,21 +2466,23 @@ class Mixer {
|
|
|
860
2466
|
setting.state = held.state;
|
|
861
2467
|
setting.keep = held.keep;
|
|
862
2468
|
this.w = this.weigh(voice, subject, now, held);
|
|
863
|
-
|
|
2469
|
+
held.weight = this.w;
|
|
2470
|
+
if (voice.holder !== held && voice.scratch.length > 0)
|
|
2471
|
+
this.keyed(voice, subject, held);
|
|
864
2472
|
return held.delta;
|
|
865
2473
|
}
|
|
866
2474
|
}
|
|
867
|
-
return this.influence(voice, subject, now);
|
|
2475
|
+
return this.influence(voice, subject, now, held);
|
|
868
2476
|
}
|
|
869
2477
|
}
|
|
870
2478
|
/**
|
|
871
|
-
* The one that ships:
|
|
2479
|
+
* The one that ships: every form on the CPU, probed per subject on demand.
|
|
872
2480
|
*
|
|
873
2481
|
* @category engine
|
|
874
2482
|
*/
|
|
875
2483
|
export const mixer = {
|
|
876
2484
|
name: 'mixer',
|
|
877
|
-
runs: new Set(['fn', 'keys']),
|
|
2485
|
+
runs: new Set(['fn', 'keys', 'motion']),
|
|
878
2486
|
create(kit, opts) {
|
|
879
2487
|
return new Mixer(kit, opts);
|
|
880
2488
|
},
|