@msb235/blits 0.2.0 → 0.3.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 +96 -0
- package/README.md +9 -2
- package/dist/channels.d.ts +41 -7
- package/dist/channels.js +184 -29
- package/dist/clock.d.ts +19 -0
- package/dist/clock.js +44 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1 -1
- package/dist/lanes.d.ts +207 -0
- package/dist/lanes.js +928 -0
- package/dist/mixer.d.ts +185 -2
- package/dist/mixer.js +1264 -139
- package/dist/motion.d.ts +169 -9
- package/dist/motion.js +564 -112
- 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 +242 -11
- package/docs/schema.html +119 -24
- 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 } 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,86 @@ 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
|
+
weight: 0,
|
|
25
|
+
rested: false,
|
|
26
|
+
bands: new Uint8Array(0),
|
|
27
|
+
state: undefined,
|
|
28
|
+
stepped: 0,
|
|
29
|
+
ticks: 0,
|
|
30
|
+
probed: Number.NaN,
|
|
31
|
+
delta: null,
|
|
32
|
+
phase: 0,
|
|
33
|
+
seeks: 0,
|
|
34
|
+
kept,
|
|
35
|
+
keep: keeper(kept),
|
|
36
|
+
voice: null,
|
|
37
|
+
next: null,
|
|
38
|
+
version: Number.NaN,
|
|
39
|
+
loci: null,
|
|
40
|
+
slot: -1,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function elapsedWith(c, now) {
|
|
44
|
+
const dt = now - c.anchorNow;
|
|
45
|
+
const r = c.ramp;
|
|
46
|
+
if (r === null)
|
|
47
|
+
return c.anchorElapsed + dt * c.rate;
|
|
48
|
+
if (dt <= 0)
|
|
49
|
+
return c.anchorElapsed + dt * r.from;
|
|
50
|
+
const d = r.to - r.from;
|
|
51
|
+
if (dt <= r.over)
|
|
52
|
+
return c.anchorElapsed + r.from * dt + (d * dt * dt) / (2 * r.over);
|
|
53
|
+
return c.anchorElapsed + r.from * r.over + (d * r.over) / 2 + r.to * (dt - r.over);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The last entry taken before `t`, or at it where `inclusive` says so, as it does for a change a
|
|
57
|
+
* sync made. A host's change between frames is taken strictly: it shows from the next frame on, as
|
|
58
|
+
* it did live.
|
|
59
|
+
*/
|
|
60
|
+
function last(list, t, inclusive) {
|
|
61
|
+
let lo = 0;
|
|
62
|
+
let hi = list.length - 1;
|
|
63
|
+
let found;
|
|
64
|
+
while (lo <= hi) {
|
|
65
|
+
const mid = (lo + hi) >> 1;
|
|
66
|
+
const e = list[mid];
|
|
67
|
+
if (e.at < t || (e.at === t && (typeof inclusive === 'function' ? inclusive(e) : inclusive))) {
|
|
68
|
+
found = e;
|
|
69
|
+
lo = mid + 1;
|
|
70
|
+
}
|
|
71
|
+
else
|
|
72
|
+
hi = mid - 1;
|
|
73
|
+
}
|
|
74
|
+
return found;
|
|
75
|
+
}
|
|
76
|
+
/** A store that fills a missing entry from `make` on first ask. */
|
|
77
|
+
class Filled extends Store {
|
|
78
|
+
make;
|
|
79
|
+
constructor(make) {
|
|
80
|
+
super();
|
|
81
|
+
this.make = make;
|
|
82
|
+
}
|
|
83
|
+
get(key) {
|
|
84
|
+
let v = super.get(key);
|
|
85
|
+
if (v === undefined) {
|
|
86
|
+
v = this.make(key);
|
|
87
|
+
if (v !== undefined)
|
|
88
|
+
super.set(key, v);
|
|
89
|
+
}
|
|
90
|
+
return v;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const noSend = () => { };
|
|
12
94
|
const near = (a, b) => {
|
|
13
95
|
if (typeof a === 'number' && typeof b === 'number')
|
|
14
96
|
return Math.abs(a - b) < 1e-9;
|
|
@@ -17,7 +99,35 @@ const near = (a, b) => {
|
|
|
17
99
|
return a === b;
|
|
18
100
|
};
|
|
19
101
|
const copy = (v) => (Array.isArray(v) ? [...v] : v);
|
|
20
|
-
|
|
102
|
+
/** Writes a folded pose's value into subject `n`'s places in a column. */
|
|
103
|
+
function writeValue(c, v, n) {
|
|
104
|
+
const at = n * c.axes;
|
|
105
|
+
if (typeof v === 'number' && c.axes === 1)
|
|
106
|
+
c.out[at] = v;
|
|
107
|
+
else if (Array.isArray(v) && v.length === c.axes)
|
|
108
|
+
for (let a = 0; a < c.axes; a++)
|
|
109
|
+
c.out[at + a] = v[a];
|
|
110
|
+
else if (v === undefined)
|
|
111
|
+
c.out.fill(Number.NaN, at, at + c.axes);
|
|
112
|
+
else
|
|
113
|
+
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)}`);
|
|
114
|
+
}
|
|
115
|
+
/** Deep equality over plain data, for telling whether a host field changed. */
|
|
116
|
+
function same(a, b) {
|
|
117
|
+
if (a === b)
|
|
118
|
+
return true;
|
|
119
|
+
if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null)
|
|
120
|
+
return false;
|
|
121
|
+
const ka = Object.keys(a);
|
|
122
|
+
const kb = Object.keys(b);
|
|
123
|
+
if (ka.length !== kb.length)
|
|
124
|
+
return false;
|
|
125
|
+
for (const k of ka)
|
|
126
|
+
if (!same(a[k], b[k]))
|
|
127
|
+
return false;
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
export class Voice {
|
|
21
131
|
id;
|
|
22
132
|
spec;
|
|
23
133
|
patch;
|
|
@@ -36,13 +146,34 @@ class Voice {
|
|
|
36
146
|
ramp = null;
|
|
37
147
|
out = null;
|
|
38
148
|
subjects = new Store();
|
|
149
|
+
/** The subjects its spec names, or null where it names none. */
|
|
150
|
+
named;
|
|
39
151
|
/** Kit slot of each channel the patch writes, in `writes` order. */
|
|
40
152
|
slots;
|
|
41
153
|
/** The stops built ahead of time, for a `keys` patch. */
|
|
42
154
|
built;
|
|
43
155
|
/** The kit's `lerp` for each channel the patch writes, which keyed stops interpolate through. */
|
|
44
156
|
lerps;
|
|
157
|
+
/** Per keyed channel, the in-place form of the lerp its stops take, where it has one. */
|
|
158
|
+
intos;
|
|
159
|
+
/**
|
|
160
|
+
* The arrays keyed reads interpolate into: one set for the voice, not one per subject, since 10k
|
|
161
|
+
* subjects' worth of long-lived arrays costs more in cache misses than allocating them did.
|
|
162
|
+
*/
|
|
163
|
+
scratch = [];
|
|
164
|
+
/** The record whose delta holds `scratch`'s arrays; any other's must be read again to be handed out. */
|
|
165
|
+
holder = null;
|
|
166
|
+
/** Whether its channels run as lanes this frame, so the general fold passes it by. */
|
|
167
|
+
laned = false;
|
|
168
|
+
/** How many times it has been sought, so a delta read before a seek is not handed out after it. */
|
|
169
|
+
seeks = 0;
|
|
170
|
+
/** Whether its patch has kept state on a record through `setting.keep`, which makes it stateful. */
|
|
171
|
+
keeping = false;
|
|
172
|
+
/** For a motion patch, the hook it was given to ask the mix for its subjects' voice time. */
|
|
173
|
+
frame = null;
|
|
45
174
|
ease;
|
|
175
|
+
/** How many passes its `loop` plays, worked out once. */
|
|
176
|
+
passes;
|
|
46
177
|
/** Reused for every call this voice makes, so it is valid only during the call. */
|
|
47
178
|
setting;
|
|
48
179
|
/** Every subject this voice has been asked about, so a handover knows when it is finished. */
|
|
@@ -50,6 +181,16 @@ class Voice {
|
|
|
50
181
|
/** The longest stagger of any subject seen, so a finite loop waits for the last of them. */
|
|
51
182
|
latest = 0;
|
|
52
183
|
restedCount = 0;
|
|
184
|
+
/** The mix time it was cued at; -Infinity before the first sync. */
|
|
185
|
+
cuedAt = Number.NEGATIVE_INFINITY;
|
|
186
|
+
/** The mix time it left at. */
|
|
187
|
+
doneAt = Number.POSITIVE_INFINITY;
|
|
188
|
+
/** Under `history`, its controls after each change, oldest first. */
|
|
189
|
+
log = null;
|
|
190
|
+
/** Where an anchored `out` or `end` puts its fade's start, mix time; Infinity until known. */
|
|
191
|
+
outAt = Number.POSITIVE_INFINITY;
|
|
192
|
+
/** Whether its start is still to be fixed by an anchor, so it waits pending. */
|
|
193
|
+
placing = false;
|
|
53
194
|
resolve;
|
|
54
195
|
done;
|
|
55
196
|
constructor(id, spec, patch, fade, now, start, slotOf, channels, host, send) {
|
|
@@ -59,9 +200,14 @@ class Voice {
|
|
|
59
200
|
this.fade = fade;
|
|
60
201
|
this.start = start;
|
|
61
202
|
this.slots = patch.writes.map((k) => slotOf.get(k));
|
|
203
|
+
this.named = spec.subjects ? new Set(spec.subjects) : null;
|
|
62
204
|
this.built = patch.form === 'keys' && patch.keys ? builtOf(patch) : null;
|
|
63
205
|
this.lerps = this.slots.map((slot) => channels[slot].lerp);
|
|
206
|
+
this.intos = this.built
|
|
207
|
+
? intosOf(this.built, this.slots.map((slot) => channels[slot]))
|
|
208
|
+
: undefined;
|
|
64
209
|
this.ease = fade.ease === undefined ? undefined : curve(fade.ease);
|
|
210
|
+
this.passes = passesOf(spec.loop);
|
|
65
211
|
this.setting = {
|
|
66
212
|
timestamp: 0,
|
|
67
213
|
dt: 0,
|
|
@@ -83,16 +229,72 @@ class Voice {
|
|
|
83
229
|
this.state = 'live';
|
|
84
230
|
}
|
|
85
231
|
elapsedAt(now) {
|
|
86
|
-
|
|
232
|
+
return elapsedWith(this, now);
|
|
233
|
+
}
|
|
234
|
+
/** The mix time its clock reads `elapsed`, inverting `elapsedAt`; Infinity where it never will. */
|
|
235
|
+
timeAt(elapsed) {
|
|
236
|
+
const e0 = this.anchorElapsed;
|
|
87
237
|
const r = this.ramp;
|
|
88
|
-
if (r === null)
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
return
|
|
92
|
-
|
|
93
|
-
if (
|
|
94
|
-
return
|
|
95
|
-
|
|
238
|
+
if (r === null) {
|
|
239
|
+
if (this.rate > 0)
|
|
240
|
+
return this.anchorNow + (elapsed - e0) / this.rate;
|
|
241
|
+
return elapsed <= e0 ? this.anchorNow : Number.POSITIVE_INFINITY;
|
|
242
|
+
}
|
|
243
|
+
if (elapsed <= e0)
|
|
244
|
+
return r.from > 0 ? this.anchorNow + (elapsed - e0) / r.from : this.anchorNow;
|
|
245
|
+
const a = (r.to - r.from) / (2 * r.over);
|
|
246
|
+
const atEnd = e0 + r.from * r.over + a * r.over * r.over;
|
|
247
|
+
if (elapsed <= atEnd) {
|
|
248
|
+
const c = e0 - elapsed;
|
|
249
|
+
const dt = Math.abs(a) < 1e-12
|
|
250
|
+
? -c / r.from
|
|
251
|
+
: (-r.from + Math.sqrt(Math.max(0, r.from * r.from - 4 * a * c))) / (2 * a);
|
|
252
|
+
return this.anchorNow + dt;
|
|
253
|
+
}
|
|
254
|
+
if (r.to <= 0)
|
|
255
|
+
return Number.POSITIVE_INFINITY;
|
|
256
|
+
return this.anchorNow + r.over + (elapsed - atEnd) / r.to;
|
|
257
|
+
}
|
|
258
|
+
/** Records this voice's controls as they stand, from mix time `at`. */
|
|
259
|
+
note(at, sync = false) {
|
|
260
|
+
this.log?.push({
|
|
261
|
+
at,
|
|
262
|
+
sync,
|
|
263
|
+
anchorNow: this.anchorNow,
|
|
264
|
+
anchorElapsed: this.anchorElapsed,
|
|
265
|
+
rate: this.rate,
|
|
266
|
+
ramp: this.ramp,
|
|
267
|
+
weight: this.weight,
|
|
268
|
+
out: this.out,
|
|
269
|
+
start: this.start,
|
|
270
|
+
outAt: this.outAt,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* A copy for a projection: its own setting, which sends nothing, and its own per-subject records,
|
|
275
|
+
* filled from `fill`. With `controls` it takes those, as the voice stood at an earlier time.
|
|
276
|
+
*/
|
|
277
|
+
copy(fill, controls) {
|
|
278
|
+
const v = Object.assign(Object.create(Voice.prototype), this);
|
|
279
|
+
const w = v;
|
|
280
|
+
w.subjects = new Filled(fill);
|
|
281
|
+
w.setting = { ...this.setting, keep: keeper(new Map()), send: noSend };
|
|
282
|
+
v.scratch = [];
|
|
283
|
+
v.holder = null;
|
|
284
|
+
v.laned = false;
|
|
285
|
+
v.log = null;
|
|
286
|
+
if (controls) {
|
|
287
|
+
v.anchorNow = controls.anchorNow;
|
|
288
|
+
v.anchorElapsed = controls.anchorElapsed;
|
|
289
|
+
v.rate = controls.rate;
|
|
290
|
+
v.ramp = controls.ramp;
|
|
291
|
+
v.weight = controls.weight;
|
|
292
|
+
v.out = controls.out;
|
|
293
|
+
v.start = controls.start;
|
|
294
|
+
v.outAt = controls.outAt;
|
|
295
|
+
}
|
|
296
|
+
v.resolve = noSend;
|
|
297
|
+
return v;
|
|
96
298
|
}
|
|
97
299
|
rateAt(now) {
|
|
98
300
|
const r = this.ramp;
|
|
@@ -117,6 +319,25 @@ class Mixer {
|
|
|
117
319
|
kit;
|
|
118
320
|
opts;
|
|
119
321
|
voices = [];
|
|
322
|
+
/** Under `history`, voices that have left but that a read back may still reach. */
|
|
323
|
+
gone = [];
|
|
324
|
+
/** Set on a projection's own mixer: it sends nothing, keeps no history, and reads without committing. */
|
|
325
|
+
projecting = false;
|
|
326
|
+
/** Set on a projection reading back: a subject it has nothing on starts from its voice's start. */
|
|
327
|
+
backward = false;
|
|
328
|
+
/** True during a sync, so a control change it makes is recorded as showing in that frame. */
|
|
329
|
+
moving = false;
|
|
330
|
+
/**
|
|
331
|
+
* Under `history` with `inputs`, the host fields patches read, copied each frame they changed. In
|
|
332
|
+
* a projection reading back, the copy in force then.
|
|
333
|
+
*/
|
|
334
|
+
hostLog = [];
|
|
335
|
+
/**
|
|
336
|
+
* Marks the host announced on a score: mix time, NaN until the next sync for one announced as
|
|
337
|
+
* now before any sync; `made` is when it was announced, for a read back to know what was known.
|
|
338
|
+
*/
|
|
339
|
+
announced = [];
|
|
340
|
+
hostThen;
|
|
120
341
|
/** Each subject's last two poses and when they were probed, for `from: 'current'`. */
|
|
121
342
|
pose = new Store();
|
|
122
343
|
nextId = 1;
|
|
@@ -126,19 +347,27 @@ class Mixer {
|
|
|
126
347
|
last = Number.NaN;
|
|
127
348
|
rebasing = false;
|
|
128
349
|
wantsPose = false;
|
|
350
|
+
/** The pose `pull` folds a subject into where it cannot read straight from the lanes. */
|
|
351
|
+
scratch;
|
|
129
352
|
/** Read once a sync, so a `reduce` function is not called per voice per subject. */
|
|
130
353
|
reducedNow = false;
|
|
131
354
|
/**
|
|
132
|
-
* Per subject, the
|
|
133
|
-
*
|
|
134
|
-
*
|
|
355
|
+
* Per subject, the first record of the chain through every live voice that reaches it, or a stub
|
|
356
|
+
* where none does. Relinked when `version` moves, which is whenever the list or a voice's pending
|
|
357
|
+
* state changes.
|
|
135
358
|
*/
|
|
136
|
-
|
|
359
|
+
chains = new Store();
|
|
137
360
|
version = 0;
|
|
138
|
-
/**
|
|
139
|
-
|
|
361
|
+
/** Per subject, the voices whose `subjects` name it, in voice order. */
|
|
362
|
+
named = new Store();
|
|
363
|
+
/** How many voices in the list carry `subjects`. */
|
|
364
|
+
naming = 0;
|
|
365
|
+
/** The voices in the list that name no subjects, in voice order. */
|
|
366
|
+
general = [];
|
|
140
367
|
/** How many voices in the list carry a locus; with none, a fold allocates nothing. */
|
|
141
368
|
loci = 0;
|
|
369
|
+
/** How many voices in the list are anchored, so a sync with none skips placing them. */
|
|
370
|
+
anchored = 0;
|
|
142
371
|
/** Events sent since the last drain, and who is being probed, so `send` knows whose they are. */
|
|
143
372
|
sent = [];
|
|
144
373
|
sending = {
|
|
@@ -147,7 +376,7 @@ class Mixer {
|
|
|
147
376
|
};
|
|
148
377
|
send = (event) => {
|
|
149
378
|
const { voice, subject } = this.sending;
|
|
150
|
-
if (voice === null)
|
|
379
|
+
if (voice === null || this.projecting)
|
|
151
380
|
return;
|
|
152
381
|
this.sent.push({
|
|
153
382
|
timestamp: voice.setting.timestamp,
|
|
@@ -157,20 +386,24 @@ class Mixer {
|
|
|
157
386
|
event,
|
|
158
387
|
});
|
|
159
388
|
};
|
|
160
|
-
/**
|
|
389
|
+
/** The weight `influence` found besides the delta, read by the caller at once. */
|
|
161
390
|
w = 0;
|
|
162
|
-
h = null;
|
|
163
391
|
names;
|
|
164
392
|
channels;
|
|
393
|
+
/** Slots of the channels that declare bounds, clamped after every fold. */
|
|
394
|
+
bounded;
|
|
165
395
|
slotOf = new Map();
|
|
396
|
+
lanes;
|
|
166
397
|
constructor(kit, opts) {
|
|
167
398
|
this.kit = kit;
|
|
168
399
|
this.opts = opts;
|
|
169
400
|
this.names = Object.keys(kit);
|
|
170
401
|
this.channels = this.names.map((k) => kit[k]);
|
|
402
|
+
this.bounded = this.channels.flatMap((c, i) => (c.bounds ? [i] : []));
|
|
171
403
|
this.names.forEach((k, i) => {
|
|
172
404
|
this.slotOf.set(k, i);
|
|
173
405
|
});
|
|
406
|
+
this.lanes = opts.lanes === false ? null : new Lanes(this.laneHost());
|
|
174
407
|
}
|
|
175
408
|
get reduced() {
|
|
176
409
|
const r = this.opts.reduce;
|
|
@@ -200,16 +433,48 @@ class Mixer {
|
|
|
200
433
|
}
|
|
201
434
|
if (spec.from === 'current' && patch.form !== 'keys')
|
|
202
435
|
throw new Error("blits: from: 'current' needs a keys patch");
|
|
203
|
-
|
|
436
|
+
if (spec.subjects !== undefined && spec.target !== undefined)
|
|
437
|
+
throw new Error('blits: a voice takes target or subjects, not both');
|
|
438
|
+
const anchor = spec.anchor;
|
|
439
|
+
if (anchor)
|
|
440
|
+
this.checkPlacement(spec, anchor);
|
|
441
|
+
const anchored = anchor !== undefined && (anchor.start !== undefined || anchor.in !== undefined);
|
|
442
|
+
const start = anchored
|
|
443
|
+
? Number.POSITIVE_INFINITY
|
|
444
|
+
: spec.start !== undefined
|
|
445
|
+
? spec.start - this.offset
|
|
446
|
+
: Number.isNaN(this.now)
|
|
447
|
+
? 0
|
|
448
|
+
: this.now;
|
|
204
449
|
const voice = new Voice(this.nextId++, spec, patch, spec.fade ?? {}, this.now, start, this.slotOf, this.channels, this.opts.host, this.send);
|
|
450
|
+
if (!Number.isNaN(this.now))
|
|
451
|
+
voice.cuedAt = this.now;
|
|
452
|
+
voice.placing = anchored;
|
|
453
|
+
const motion = motionOf(patch);
|
|
454
|
+
if (motion !== undefined) {
|
|
455
|
+
voice.frame = Mixer.frameHook(new WeakRef(this), new WeakRef(voice));
|
|
456
|
+
motion.frame = voice.frame;
|
|
457
|
+
}
|
|
205
458
|
this.voices.push(voice);
|
|
459
|
+
this.index(voice);
|
|
206
460
|
this.version++;
|
|
207
|
-
if (spec.target !== undefined)
|
|
208
|
-
this.targeted++;
|
|
209
461
|
if (spec.locus !== undefined)
|
|
210
462
|
this.loci++;
|
|
211
463
|
if (spec.from === 'current')
|
|
212
464
|
this.wantsPose = true;
|
|
465
|
+
if (anchor) {
|
|
466
|
+
this.anchored++;
|
|
467
|
+
this.place();
|
|
468
|
+
if (!Number.isNaN(this.now) && voice.state === 'pending' && this.now >= voice.start) {
|
|
469
|
+
voice.state = 'live';
|
|
470
|
+
this.version++;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
// After placing, so the controls it starts with are where its anchors put it at the cue.
|
|
474
|
+
if (this.opts.history) {
|
|
475
|
+
voice.log = [];
|
|
476
|
+
voice.note(Number.NEGATIVE_INFINITY);
|
|
477
|
+
}
|
|
213
478
|
return this.handle(voice);
|
|
214
479
|
}
|
|
215
480
|
blend(patches, by, spec = {}) {
|
|
@@ -233,21 +498,48 @@ class Mixer {
|
|
|
233
498
|
const now = timestamp - this.offset;
|
|
234
499
|
if (now === this.now)
|
|
235
500
|
return;
|
|
501
|
+
this.move(now);
|
|
502
|
+
}
|
|
503
|
+
/** Moves the mix clock to `now`: voices start, finite loops end, and fades finish. */
|
|
504
|
+
move(now) {
|
|
505
|
+
this.moving = true;
|
|
506
|
+
try {
|
|
507
|
+
this.moveTo(now);
|
|
508
|
+
}
|
|
509
|
+
finally {
|
|
510
|
+
this.moving = false;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
moveTo(now) {
|
|
236
514
|
this.now = now;
|
|
237
515
|
this.reducedNow = this.reduced;
|
|
516
|
+
for (const a of this.announced)
|
|
517
|
+
if (Number.isNaN(a.at))
|
|
518
|
+
a.at = now;
|
|
519
|
+
if (this.anchored > 0)
|
|
520
|
+
this.place();
|
|
521
|
+
if (this.announced.length > 0) {
|
|
522
|
+
// Kept while still ahead, or while history reaches it; anchors waiting on one were placed above.
|
|
523
|
+
const reach = now - (this.opts.history?.ms ?? 0);
|
|
524
|
+
this.announced = this.announced.filter((a) => a.at >= reach);
|
|
525
|
+
}
|
|
238
526
|
for (const voice of this.voices) {
|
|
239
527
|
if (voice.state === 'done')
|
|
240
528
|
continue;
|
|
529
|
+
if (voice.state === 'live' && voice.outAt <= now)
|
|
530
|
+
this.beginFade(voice, {}, Math.max(voice.start, voice.outAt));
|
|
241
531
|
if (voice.state === 'pending' && now >= voice.start) {
|
|
242
532
|
voice.state = 'live';
|
|
243
533
|
this.version++;
|
|
244
534
|
}
|
|
245
535
|
const period = voice.patch.period;
|
|
246
|
-
const
|
|
247
|
-
const passes = loop === true ? Number.POSITIVE_INFINITY : loop === false ? 1 : loop;
|
|
536
|
+
const passes = voice.passes;
|
|
248
537
|
if (voice.state === 'live' && period > 0 && Number.isFinite(passes)) {
|
|
249
|
-
|
|
250
|
-
|
|
538
|
+
const end = period * passes + voice.latest;
|
|
539
|
+
// The fade starts when the last pass ended, not at the frame that noticed, so it plays the
|
|
540
|
+
// same at any frame rate and a read at another time can find it.
|
|
541
|
+
if (voice.elapsedAt(now) >= end)
|
|
542
|
+
this.beginFade(voice, {}, Math.max(voice.start, Math.min(now, voice.timeAt(end))));
|
|
251
543
|
}
|
|
252
544
|
if (voice.state === 'fading' && voice.out) {
|
|
253
545
|
const { at, over, rest, deadline } = voice.out;
|
|
@@ -255,27 +547,48 @@ class Mixer {
|
|
|
255
547
|
if (rest) {
|
|
256
548
|
const out = deadline !== undefined && spent >= deadline;
|
|
257
549
|
const settled = voice.seen > 0 && voice.restedCount >= voice.seen;
|
|
258
|
-
if (out
|
|
550
|
+
if (out)
|
|
551
|
+
this.retire(voice, at + deadline);
|
|
552
|
+
else if (settled)
|
|
259
553
|
this.retire(voice);
|
|
260
554
|
}
|
|
261
555
|
else if (spent >= over)
|
|
262
|
-
this.retire(voice);
|
|
556
|
+
this.retire(voice, at + over);
|
|
263
557
|
}
|
|
264
558
|
}
|
|
559
|
+
let pruned = false;
|
|
265
560
|
for (let i = this.voices.length - 1; i >= 0; i--) {
|
|
266
561
|
const voice = this.voices[i];
|
|
267
562
|
if (voice.state === 'done') {
|
|
268
563
|
this.voices.splice(i, 1);
|
|
564
|
+
if (this.opts.history)
|
|
565
|
+
this.gone.push(voice);
|
|
269
566
|
this.version++;
|
|
270
|
-
if (voice.
|
|
271
|
-
|
|
567
|
+
if (voice.named === null)
|
|
568
|
+
pruned = true;
|
|
569
|
+
else
|
|
570
|
+
this.unindex(voice);
|
|
272
571
|
if (voice.spec.locus !== undefined)
|
|
273
572
|
this.loci--;
|
|
573
|
+
if (voice.spec.anchor !== undefined)
|
|
574
|
+
this.anchored--;
|
|
274
575
|
}
|
|
275
576
|
}
|
|
577
|
+
if (pruned)
|
|
578
|
+
this.general = this.general.filter((v) => v.state !== 'done');
|
|
579
|
+
const history = this.opts.history;
|
|
580
|
+
if (history && this.gone.length > 0) {
|
|
581
|
+
const reach = now - history.ms;
|
|
582
|
+
this.gone = this.gone.filter((v) => v.doneAt >= reach);
|
|
583
|
+
}
|
|
276
584
|
}
|
|
277
585
|
probe(subject, out) {
|
|
278
586
|
const pose = this.fold(subject, out);
|
|
587
|
+
this.keep(subject, pose, out);
|
|
588
|
+
return pose;
|
|
589
|
+
}
|
|
590
|
+
/** After a probe, keeps the pose a retarget from `'current'` reads as the subject's last. */
|
|
591
|
+
keep(subject, pose, out) {
|
|
279
592
|
// Keeping last frame's pose is free when the mix allocated it; a host sampling into its own
|
|
280
593
|
// object only pays for the copy once something in the mix has asked to retarget from it.
|
|
281
594
|
let kept;
|
|
@@ -302,7 +615,176 @@ class Mixer {
|
|
|
302
615
|
rec.at = this.now;
|
|
303
616
|
}
|
|
304
617
|
}
|
|
305
|
-
|
|
618
|
+
}
|
|
619
|
+
pull(subjects, into) {
|
|
620
|
+
const columns = this.columnsOf(into);
|
|
621
|
+
const lanes = this.lanes;
|
|
622
|
+
const now = this.now;
|
|
623
|
+
if (this.scratch === undefined)
|
|
624
|
+
this.scratch = {};
|
|
625
|
+
const scratch = this.scratch;
|
|
626
|
+
let room = Number.POSITIVE_INFINITY;
|
|
627
|
+
let tightest = '';
|
|
628
|
+
for (const c of columns) {
|
|
629
|
+
const fits = Math.floor(c.out.length / c.axes);
|
|
630
|
+
if (fits < room) {
|
|
631
|
+
room = fits;
|
|
632
|
+
tightest = c.key;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
let n = 0;
|
|
636
|
+
for (const subject of subjects) {
|
|
637
|
+
if (n >= room)
|
|
638
|
+
throw new RangeError(`blits: pull's array for ${tightest} has room for ${room} subjects, and was given more`);
|
|
639
|
+
const head = Number.isNaN(now) ? null : this.chain(subject, now);
|
|
640
|
+
const laned = head !== null && lanes?.prepare(head.slot, subject, now, this.version) === true;
|
|
641
|
+
if (laned && lanes.whole && !this.wantsPose) {
|
|
642
|
+
lanes.write(head.slot, columns, n);
|
|
643
|
+
}
|
|
644
|
+
else {
|
|
645
|
+
const folded = this.foldWith(subject, scratch, head, laned, false);
|
|
646
|
+
const pose = (this.bounded.length === 0 ? folded : this.clamp(folded));
|
|
647
|
+
this.keep(subject, pose, scratch);
|
|
648
|
+
for (const c of columns)
|
|
649
|
+
writeValue(c, pose[c.key], n);
|
|
650
|
+
}
|
|
651
|
+
n++;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
/** The kit slot, width, rest and bounds of every channel `pull` was handed an array for. */
|
|
655
|
+
columnsOf(into) {
|
|
656
|
+
const columns = [];
|
|
657
|
+
for (const key of Object.keys(into)) {
|
|
658
|
+
const out = into[key];
|
|
659
|
+
if (out === undefined)
|
|
660
|
+
continue;
|
|
661
|
+
const slot = this.slotOf.get(key);
|
|
662
|
+
if (slot === undefined)
|
|
663
|
+
throw new Error(`blits: pull was handed ${key}, which the kit lacks`);
|
|
664
|
+
const channel = this.channels[slot];
|
|
665
|
+
const rest = channel.rest;
|
|
666
|
+
const axes = Array.isArray(rest) ? rest.length : 1;
|
|
667
|
+
const bounds = channel.bounds;
|
|
668
|
+
const at = new Float64Array(axes).fill(Number.NaN);
|
|
669
|
+
if (typeof rest === 'number')
|
|
670
|
+
at[0] = rest;
|
|
671
|
+
else if (Array.isArray(rest))
|
|
672
|
+
at.set(rest);
|
|
673
|
+
if (bounds !== undefined)
|
|
674
|
+
clampRun(at, 0, axes, bounds);
|
|
675
|
+
columns.push({ key, slot, axes, out, rest: at, bounds });
|
|
676
|
+
}
|
|
677
|
+
return columns;
|
|
678
|
+
}
|
|
679
|
+
project(timestamp) {
|
|
680
|
+
const t = timestamp - this.offset;
|
|
681
|
+
const c = new Mixer(this.kit, { ...this.opts, history: undefined, lanes: false });
|
|
682
|
+
c.projecting = true;
|
|
683
|
+
c.pose = this.pose;
|
|
684
|
+
c.offset = this.offset;
|
|
685
|
+
c.wantsPose = this.wantsPose;
|
|
686
|
+
c.nextId = this.nextId;
|
|
687
|
+
c.reducedNow = this.reducedNow;
|
|
688
|
+
if (Number.isNaN(this.now) || t >= this.now) {
|
|
689
|
+
c.now = this.now;
|
|
690
|
+
c.voices = this.voices
|
|
691
|
+
.filter((v) => v.state !== 'done')
|
|
692
|
+
.map((v) => v.copy((subject) => this.carry(v, subject)));
|
|
693
|
+
c.announced = this.announced.map((a) => ({ ...a }));
|
|
694
|
+
c.count();
|
|
695
|
+
c.move(t);
|
|
696
|
+
}
|
|
697
|
+
else {
|
|
698
|
+
const history = this.opts.history;
|
|
699
|
+
if (!history)
|
|
700
|
+
throw new Error('blits: reading back needs a mix made with history');
|
|
701
|
+
if (t < this.now - history.ms)
|
|
702
|
+
throw new Error(`blits: ${timestamp} is older than this mix's history reaches`);
|
|
703
|
+
c.now = t;
|
|
704
|
+
c.backward = true;
|
|
705
|
+
const was = last(this.hostLog, t, true);
|
|
706
|
+
const host = this.opts.host;
|
|
707
|
+
const then = was && typeof host === 'object' && host !== null
|
|
708
|
+
? Object.assign(Object.create(host), was.fields)
|
|
709
|
+
: undefined;
|
|
710
|
+
if (was)
|
|
711
|
+
c.hostThen = was;
|
|
712
|
+
c.announced = this.announced.filter((a) => a.made < t).map((a) => ({ ...a }));
|
|
713
|
+
c.voices = [...this.voices, ...this.gone]
|
|
714
|
+
.filter((v) => v.cuedAt <= t && v.doneAt > t)
|
|
715
|
+
.sort((a, b) => a.id - b.id)
|
|
716
|
+
.map((v) => {
|
|
717
|
+
const log = v.log;
|
|
718
|
+
const copy = v.copy((subject) => this.recall(v, subject, t), last(log, t, (e) => e.sync) ?? log[0]);
|
|
719
|
+
copy.state = t < v.start ? 'pending' : copy.out && copy.out.at <= t ? 'fading' : 'live';
|
|
720
|
+
if (then !== undefined)
|
|
721
|
+
copy.setting.host = then;
|
|
722
|
+
return copy;
|
|
723
|
+
});
|
|
724
|
+
c.count();
|
|
725
|
+
}
|
|
726
|
+
const read = (f) => {
|
|
727
|
+
reading.live = false;
|
|
728
|
+
try {
|
|
729
|
+
return f();
|
|
730
|
+
}
|
|
731
|
+
finally {
|
|
732
|
+
reading.live = true;
|
|
733
|
+
}
|
|
734
|
+
};
|
|
735
|
+
return {
|
|
736
|
+
timestamp,
|
|
737
|
+
probe: (subject, out) => read(() => c.fold(subject, out)),
|
|
738
|
+
assess: (subject) => read(() => {
|
|
739
|
+
c.fold(subject);
|
|
740
|
+
return c.doubts(subject);
|
|
741
|
+
}),
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
announce(name, opts = {}) {
|
|
745
|
+
const at = opts.at !== undefined ? opts.at - this.offset : this.now;
|
|
746
|
+
this.announced.push({
|
|
747
|
+
name,
|
|
748
|
+
score: opts.score,
|
|
749
|
+
tags: opts.tags ?? none,
|
|
750
|
+
at,
|
|
751
|
+
made: Number.isNaN(this.now) ? Number.NEGATIVE_INFINITY : this.now,
|
|
752
|
+
order: this.nextId++,
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
marks(from, to) {
|
|
756
|
+
const lo = from - this.offset;
|
|
757
|
+
const hi = to - this.offset;
|
|
758
|
+
const out = [];
|
|
759
|
+
for (const a of this.announced)
|
|
760
|
+
if (a.at >= lo && a.at <= hi)
|
|
761
|
+
out.push({
|
|
762
|
+
timestamp: a.at + this.offset,
|
|
763
|
+
mark: undefined,
|
|
764
|
+
voice: undefined,
|
|
765
|
+
score: a.score,
|
|
766
|
+
name: a.name,
|
|
767
|
+
tags: a.tags,
|
|
768
|
+
order: a.order,
|
|
769
|
+
});
|
|
770
|
+
for (const voice of [...this.voices, ...this.gone]) {
|
|
771
|
+
for (const mark of ['start', 'in', 'out', 'end']) {
|
|
772
|
+
const t = this.markOf(voice, mark);
|
|
773
|
+
if (t === undefined || t < lo || t > hi)
|
|
774
|
+
continue;
|
|
775
|
+
out.push({
|
|
776
|
+
timestamp: t + this.offset,
|
|
777
|
+
mark,
|
|
778
|
+
voice: voice.id,
|
|
779
|
+
score: voice.spec.score,
|
|
780
|
+
name: voice.spec.name,
|
|
781
|
+
tags: voice.spec.tags ?? none,
|
|
782
|
+
order: voice.id,
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
out.sort((a, b) => a.timestamp - b.timestamp || a.order - b.order);
|
|
787
|
+
return out.map(({ order: _, ...m }) => m);
|
|
306
788
|
}
|
|
307
789
|
atRest(subject) {
|
|
308
790
|
const pose = this.fold(subject, undefined, true);
|
|
@@ -329,10 +811,15 @@ class Mixer {
|
|
|
329
811
|
this.beginFade(voice, { over: opts?.over });
|
|
330
812
|
}
|
|
331
813
|
drop(subject) {
|
|
814
|
+
const head = this.chains.get(subject);
|
|
815
|
+
if (head !== undefined && head.slot >= 0 && this.lanes !== null)
|
|
816
|
+
this.lanes.release(head.slot);
|
|
332
817
|
this.pose.delete(subject);
|
|
333
|
-
this.
|
|
334
|
-
for (const voice of this.voices)
|
|
818
|
+
this.chains.delete(subject);
|
|
819
|
+
for (const voice of [...this.voices, ...this.gone]) {
|
|
335
820
|
voice.subjects.delete(subject);
|
|
821
|
+
motionOf(voice.patch)?.release(subject);
|
|
822
|
+
}
|
|
336
823
|
}
|
|
337
824
|
drain(tag) {
|
|
338
825
|
const all = this.sent;
|
|
@@ -350,7 +837,517 @@ class Mixer {
|
|
|
350
837
|
}
|
|
351
838
|
return out.sort((a, b) => a.timestamp - b.timestamp);
|
|
352
839
|
}
|
|
840
|
+
laneHost() {
|
|
841
|
+
const mix = this;
|
|
842
|
+
return {
|
|
843
|
+
get voices() {
|
|
844
|
+
return mix.voices;
|
|
845
|
+
},
|
|
846
|
+
channels: this.channels,
|
|
847
|
+
names: this.names,
|
|
848
|
+
fits: (voice) => mix.fits(voice),
|
|
849
|
+
meet: (voice, subject) => mix.held(voice, subject, mix.now),
|
|
850
|
+
naming: (subject) => (mix.naming === 0 ? undefined : mix.named.get(subject)),
|
|
851
|
+
envelope: (voice, since) => mix.envelope(voice, mix.now, since),
|
|
852
|
+
ready: (voice, subject, held, elapsed, pass, weight) => {
|
|
853
|
+
mix.prime(voice, held, mix.now, elapsed, pass);
|
|
854
|
+
voice.setting.weight = weight;
|
|
855
|
+
mix.sending.voice = voice;
|
|
856
|
+
mix.sending.subject = subject;
|
|
857
|
+
},
|
|
858
|
+
horizon: (voice, delay) => {
|
|
859
|
+
reading.horizon = mix.horizonFor(voice, delay, mix.now);
|
|
860
|
+
},
|
|
861
|
+
keeps: this.opts.history !== undefined,
|
|
862
|
+
after: (voice, held) => mix.remember(voice, held),
|
|
863
|
+
kept: (voice) => mix.stateful(voice),
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* Whether a voice's patch and spec can run on a lane, its channels aside. A voice whose patch has
|
|
868
|
+
* kept state on a record through `setting.keep` is stateful from then on, and leaves its lane;
|
|
869
|
+
* one that starts keeping partway through may advance that state once for one subject not
|
|
870
|
+
* probed on the frame it starts, which declaring `state` avoids.
|
|
871
|
+
*/
|
|
872
|
+
fits(voice) {
|
|
873
|
+
const spec = voice.spec;
|
|
874
|
+
const patch = voice.patch;
|
|
875
|
+
if (voice.keeping)
|
|
876
|
+
return false;
|
|
877
|
+
if (typeof spec.weight === 'function')
|
|
878
|
+
return false;
|
|
879
|
+
if (spec.locus !== undefined || spec.from === 'current')
|
|
880
|
+
return false;
|
|
881
|
+
if (voice.out?.rest)
|
|
882
|
+
return false;
|
|
883
|
+
if (patch.state !== undefined || patch.step !== undefined)
|
|
884
|
+
return false;
|
|
885
|
+
if (this.opts.history?.inputs && patch.reads !== undefined && patch.reads.length > 0)
|
|
886
|
+
return false;
|
|
887
|
+
const built = voice.built;
|
|
888
|
+
return (built === null ||
|
|
889
|
+
built.tracks.every((t, i) => t.lerp === undefined || t.lerp === voice.lerps[i]));
|
|
890
|
+
}
|
|
891
|
+
/** A voice's patch kept state on a record: it is stateful from now on. */
|
|
892
|
+
stateful(voice) {
|
|
893
|
+
if (voice.keeping)
|
|
894
|
+
return;
|
|
895
|
+
voice.keeping = true;
|
|
896
|
+
this.lanes?.invalidate();
|
|
897
|
+
}
|
|
898
|
+
/**
|
|
899
|
+
* What a motion patch asks for its subjects' voice time at the latest frame. It holds the mix and
|
|
900
|
+
* the voice weakly, so a patch the host keeps does not keep a mix it has let go of alive.
|
|
901
|
+
*/
|
|
902
|
+
static frameHook(mix, voice) {
|
|
903
|
+
return (subject) => {
|
|
904
|
+
const m = mix.deref();
|
|
905
|
+
const v = voice.deref();
|
|
906
|
+
return m === undefined || v === undefined ? Number.NaN : m.frameOf(v, subject);
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
/**
|
|
910
|
+
* A subject's voice time at the latest frame, where a motion patch places an untimed change and a
|
|
911
|
+
* `read` with no time; NaN until the voice has started and met the subject, and while the
|
|
912
|
+
* subject's own time is still short of its stagger. A retired voice's patch no longer asks.
|
|
913
|
+
*/
|
|
914
|
+
frameOf(voice, subject) {
|
|
915
|
+
if (Number.isNaN(this.now) || voice.state === 'pending')
|
|
916
|
+
return Number.NaN;
|
|
917
|
+
const held = voice.subjects.get(subject);
|
|
918
|
+
if (held === undefined || !held.reaches)
|
|
919
|
+
return Number.NaN;
|
|
920
|
+
const t = voice.elapsedAt(this.now) - held.delay;
|
|
921
|
+
return t < 0 ? Number.NaN : t;
|
|
922
|
+
}
|
|
923
|
+
/** Fills a voice's setting for a call to its patch, the weight aside. */
|
|
924
|
+
prime(voice, held, now, elapsed, pass) {
|
|
925
|
+
const setting = voice.setting;
|
|
926
|
+
setting.timestamp = now;
|
|
927
|
+
const gap = now - held.stepped;
|
|
928
|
+
const cap = this.opts.maxDt;
|
|
929
|
+
setting.dt = this.reducedNow
|
|
930
|
+
? Number.POSITIVE_INFINITY
|
|
931
|
+
: cap !== undefined && gap > cap
|
|
932
|
+
? cap
|
|
933
|
+
: gap;
|
|
934
|
+
setting.elapsed = elapsed;
|
|
935
|
+
setting.pass = pass;
|
|
936
|
+
setting.weight = 0;
|
|
937
|
+
setting.state = held.state;
|
|
938
|
+
setting.keep = held.keep;
|
|
939
|
+
}
|
|
940
|
+
/** The earliest voice time a read may still ask for, for a subject delayed `delay`. */
|
|
941
|
+
horizonFor(voice, delay, now) {
|
|
942
|
+
const history = this.opts.history;
|
|
943
|
+
return history === undefined
|
|
944
|
+
? Number.POSITIVE_INFINITY
|
|
945
|
+
: this.elapsedThen(voice, now - history.ms) - delay;
|
|
946
|
+
}
|
|
353
947
|
// ── internals ──────────────────────────────────────────────────────────────
|
|
948
|
+
/** Refuses a placement that names a mark twice on one side, or that waits on itself. */
|
|
949
|
+
checkPlacement(spec, anchor) {
|
|
950
|
+
if (anchor.start !== undefined && anchor.in !== undefined)
|
|
951
|
+
throw new Error('blits: a placement anchors start or in, not both');
|
|
952
|
+
if (anchor.out !== undefined && anchor.end !== undefined)
|
|
953
|
+
throw new Error('blits: a placement anchors out or end, not both');
|
|
954
|
+
if (spec.start !== undefined && (anchor.start !== undefined || anchor.in !== undefined))
|
|
955
|
+
throw new Error('blits: a voice takes start or an anchored start, not both');
|
|
956
|
+
const name = spec.name;
|
|
957
|
+
if (name === undefined)
|
|
958
|
+
return;
|
|
959
|
+
// A voice is known by its score and its name; a bare name in an anchor is in the asker's score.
|
|
960
|
+
const key = (score, n) => `${score ?? ''}\u0000${n}`;
|
|
961
|
+
const names = (p, score) => [p.start, p.in, p.out, p.end].flatMap((a) => {
|
|
962
|
+
if (a === undefined || typeof a === 'number')
|
|
963
|
+
return [];
|
|
964
|
+
const q = 'of' in a ? a.of : 'after' in a ? a.after : 'with' in a ? a.with : a.before;
|
|
965
|
+
if (typeof q === 'string')
|
|
966
|
+
return [key(score, q)];
|
|
967
|
+
return q.name === undefined ? [] : [key(q.score ?? score, q.name)];
|
|
968
|
+
});
|
|
969
|
+
const self = key(spec.score, name);
|
|
970
|
+
const seen = new Set();
|
|
971
|
+
const waits = names(anchor, spec.score);
|
|
972
|
+
while (waits.length > 0) {
|
|
973
|
+
const n = waits.pop();
|
|
974
|
+
if (n === self)
|
|
975
|
+
throw new Error(`blits: ${name}'s placement waits on itself`);
|
|
976
|
+
if (seen.has(n))
|
|
977
|
+
continue;
|
|
978
|
+
seen.add(n);
|
|
979
|
+
for (const v of this.voices)
|
|
980
|
+
if (v.spec.name !== undefined && key(v.spec.score, v.spec.name) === n && v.spec.anchor)
|
|
981
|
+
waits.push(...names(v.spec.anchor, v.spec.score));
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
/**
|
|
985
|
+
* Fixes every anchored voice's start and out from what its anchors answer now. A start is fixed
|
|
986
|
+
* while the voice waits and an out until its fade begins; a target that has left keeps the time it
|
|
987
|
+
* last gave. Repeated so a chain of anchors settles in one sync.
|
|
988
|
+
*/
|
|
989
|
+
place() {
|
|
990
|
+
for (let round = 0; round <= this.voices.length; round++) {
|
|
991
|
+
let moved = false;
|
|
992
|
+
for (const voice of this.voices) {
|
|
993
|
+
const anchor = voice.spec.anchor;
|
|
994
|
+
if (anchor === undefined || voice.state === 'done')
|
|
995
|
+
continue;
|
|
996
|
+
if (voice.placing && voice.state === 'pending') {
|
|
997
|
+
const by = anchor.start ?? anchor.in;
|
|
998
|
+
const t = by === undefined ? undefined : this.resolve(by, voice);
|
|
999
|
+
if (t !== undefined) {
|
|
1000
|
+
const start = anchor.start !== undefined ? t : t - (voice.fade.in ?? 0);
|
|
1001
|
+
if (start !== voice.start) {
|
|
1002
|
+
voice.start = start;
|
|
1003
|
+
voice.anchorNow = start;
|
|
1004
|
+
voice.anchorElapsed = 0;
|
|
1005
|
+
this.noted(voice);
|
|
1006
|
+
moved = true;
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
if (voice.state !== 'fading') {
|
|
1011
|
+
const by = anchor.out ?? anchor.end;
|
|
1012
|
+
const t = by === undefined ? undefined : this.resolve(by, voice);
|
|
1013
|
+
if (t !== undefined) {
|
|
1014
|
+
const at = anchor.out !== undefined ? t : t - (voice.fade.out ?? 0);
|
|
1015
|
+
if (at !== voice.outAt) {
|
|
1016
|
+
voice.outAt = at;
|
|
1017
|
+
this.noted(voice);
|
|
1018
|
+
moved = true;
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
if (!moved)
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
/** The mix time an anchor answers, or undefined while its target has none. */
|
|
1028
|
+
resolve(a, self) {
|
|
1029
|
+
if (typeof a === 'number')
|
|
1030
|
+
return a - this.offset;
|
|
1031
|
+
let query;
|
|
1032
|
+
let mark;
|
|
1033
|
+
let by = a.by ?? 0;
|
|
1034
|
+
if ('of' in a) {
|
|
1035
|
+
query = a.of;
|
|
1036
|
+
mark = a.mark;
|
|
1037
|
+
}
|
|
1038
|
+
else if ('after' in a) {
|
|
1039
|
+
query = a.after;
|
|
1040
|
+
mark = 'end';
|
|
1041
|
+
}
|
|
1042
|
+
else if ('with' in a) {
|
|
1043
|
+
query = a.with;
|
|
1044
|
+
mark = 'start';
|
|
1045
|
+
}
|
|
1046
|
+
else {
|
|
1047
|
+
query = a.before;
|
|
1048
|
+
mark = 'start';
|
|
1049
|
+
by = -by;
|
|
1050
|
+
}
|
|
1051
|
+
const t = this.timeOf(typeof query === 'string' ? { name: query } : query, mark, self);
|
|
1052
|
+
return t === undefined ? undefined : t + by;
|
|
1053
|
+
}
|
|
1054
|
+
/**
|
|
1055
|
+
* The time a query answers: `mark` of the voice it picks, among those cued and those that have
|
|
1056
|
+
* left, never the asker; or a mark the host announced on the score, which is the same time for
|
|
1057
|
+
* any of the four. Undefined while what it picks has no such time.
|
|
1058
|
+
*/
|
|
1059
|
+
timeOf(q, mark, self) {
|
|
1060
|
+
const score = q.score ?? self.spec.score;
|
|
1061
|
+
const found = [];
|
|
1062
|
+
for (const v of [...this.gone, ...this.voices])
|
|
1063
|
+
if (v !== self &&
|
|
1064
|
+
v.spec.score === score &&
|
|
1065
|
+
(q.name === undefined || v.spec.name === q.name) &&
|
|
1066
|
+
(q.tag === undefined || (v.spec.tags ?? none).includes(q.tag)) &&
|
|
1067
|
+
(q.writes === undefined || v.patch.writes.includes(q.writes)))
|
|
1068
|
+
found.push({ order: v.id, t: this.markOf(v, mark) });
|
|
1069
|
+
if (q.writes === undefined)
|
|
1070
|
+
for (const a of this.announced)
|
|
1071
|
+
if (a.score === score &&
|
|
1072
|
+
(q.name === undefined || a.name === q.name) &&
|
|
1073
|
+
(q.tag === undefined || a.tags.includes(q.tag)))
|
|
1074
|
+
found.push({ order: a.order, t: Number.isNaN(a.at) ? undefined : a.at });
|
|
1075
|
+
if (found.length === 0)
|
|
1076
|
+
return undefined;
|
|
1077
|
+
found.sort((x, y) => x.order - y.order);
|
|
1078
|
+
const resolver = q.resolver ?? 'last';
|
|
1079
|
+
if (resolver === 'first')
|
|
1080
|
+
return found[0]?.t;
|
|
1081
|
+
if (resolver === 'last')
|
|
1082
|
+
return found[found.length - 1]?.t;
|
|
1083
|
+
const times = found.flatMap((e) => (e.t === undefined ? [] : [e.t])).sort((x, y) => x - y);
|
|
1084
|
+
if (resolver === 'next') {
|
|
1085
|
+
const now = Number.isNaN(this.now) ? Number.NEGATIVE_INFINITY : this.now;
|
|
1086
|
+
return times.find((t) => t >= now);
|
|
1087
|
+
}
|
|
1088
|
+
return resolver === 'earliest' ? times[0] : times[times.length - 1];
|
|
1089
|
+
}
|
|
1090
|
+
/** When a voice reaches a mark, mix time, or undefined while nothing has fixed it. */
|
|
1091
|
+
markOf(voice, mark) {
|
|
1092
|
+
const start = voice.start;
|
|
1093
|
+
if (!Number.isFinite(start))
|
|
1094
|
+
return undefined;
|
|
1095
|
+
if (mark === 'start')
|
|
1096
|
+
return start;
|
|
1097
|
+
if (mark === 'in')
|
|
1098
|
+
return start + (voice.fade.in ?? 0);
|
|
1099
|
+
const out = voice.out;
|
|
1100
|
+
let outAt;
|
|
1101
|
+
if (out !== null)
|
|
1102
|
+
outAt = out.at;
|
|
1103
|
+
else if (Number.isFinite(voice.outAt))
|
|
1104
|
+
outAt = voice.outAt;
|
|
1105
|
+
else {
|
|
1106
|
+
const period = voice.patch.period;
|
|
1107
|
+
const passes = voice.passes;
|
|
1108
|
+
if (period > 0 && Number.isFinite(passes)) {
|
|
1109
|
+
const t = voice.timeAt(period * passes + voice.latest);
|
|
1110
|
+
if (Number.isFinite(t))
|
|
1111
|
+
outAt = Math.max(start, t);
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
if (mark === 'out')
|
|
1115
|
+
return voice.state === 'done' && outAt === undefined ? voice.doneAt : outAt;
|
|
1116
|
+
if (voice.state === 'done')
|
|
1117
|
+
return voice.doneAt;
|
|
1118
|
+
if (out !== null) {
|
|
1119
|
+
if (out.rest)
|
|
1120
|
+
return out.deadline === undefined ? undefined : out.at + out.deadline;
|
|
1121
|
+
return out.at + out.over;
|
|
1122
|
+
}
|
|
1123
|
+
return outAt === undefined ? undefined : outAt + (this.reduced ? 0 : (voice.fade.out ?? 0));
|
|
1124
|
+
}
|
|
1125
|
+
/** A voice's elapsed at an earlier mix time, by the controls it had then. */
|
|
1126
|
+
elapsedThen(voice, t) {
|
|
1127
|
+
const log = voice.log;
|
|
1128
|
+
const c = log === null ? undefined : last(log, t, true);
|
|
1129
|
+
return elapsedWith(c ?? voice, t);
|
|
1130
|
+
}
|
|
1131
|
+
/** Records a change to a voice's controls under `history`, and lets go of what it no longer reaches. */
|
|
1132
|
+
noted(voice) {
|
|
1133
|
+
const log = voice.log;
|
|
1134
|
+
if (log === null)
|
|
1135
|
+
return;
|
|
1136
|
+
voice.note(Number.isNaN(this.now) ? Number.NEGATIVE_INFINITY : this.now, this.moving);
|
|
1137
|
+
const reach = this.now - this.opts.history.ms;
|
|
1138
|
+
let drop = 0;
|
|
1139
|
+
while (drop + 1 < log.length && log[drop + 1].at <= reach)
|
|
1140
|
+
drop++;
|
|
1141
|
+
if (drop > 0)
|
|
1142
|
+
log.splice(0, drop);
|
|
1143
|
+
}
|
|
1144
|
+
/** Recounts what the fold's shortcuts depend on, for a projection's freshly copied voices. */
|
|
1145
|
+
count() {
|
|
1146
|
+
this.named = new Store();
|
|
1147
|
+
this.naming = 0;
|
|
1148
|
+
this.general = [];
|
|
1149
|
+
for (const voice of this.voices)
|
|
1150
|
+
this.index(voice);
|
|
1151
|
+
this.loci = this.voices.filter((v) => v.spec.locus !== undefined).length;
|
|
1152
|
+
this.anchored = this.voices.filter((v) => v.spec.anchor !== undefined).length;
|
|
1153
|
+
this.version++;
|
|
1154
|
+
}
|
|
1155
|
+
/** A copy of a subject's record that shares nothing a read could change. */
|
|
1156
|
+
copyHeld(voice, h) {
|
|
1157
|
+
const kept = new Map();
|
|
1158
|
+
for (const [owner, value] of h.kept)
|
|
1159
|
+
kept.set(owner, structuredClone(value));
|
|
1160
|
+
const patch = voice.patch;
|
|
1161
|
+
return {
|
|
1162
|
+
...h,
|
|
1163
|
+
bands: h.bands.slice(),
|
|
1164
|
+
state: h.state === undefined
|
|
1165
|
+
? undefined
|
|
1166
|
+
: patch.clone
|
|
1167
|
+
? patch.clone(h.state)
|
|
1168
|
+
: structuredClone(h.state),
|
|
1169
|
+
kept,
|
|
1170
|
+
keep: keeper(kept),
|
|
1171
|
+
probed: Number.NaN,
|
|
1172
|
+
delta: null,
|
|
1173
|
+
phase: 0,
|
|
1174
|
+
seeks: 0,
|
|
1175
|
+
base: h.base === undefined ? undefined : structuredClone(h.base),
|
|
1176
|
+
slope: h.slope === undefined ? undefined : structuredClone(h.slope),
|
|
1177
|
+
snaps: undefined,
|
|
1178
|
+
inputs: undefined,
|
|
1179
|
+
next: null,
|
|
1180
|
+
version: Number.NaN,
|
|
1181
|
+
loci: null,
|
|
1182
|
+
slot: -1,
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1185
|
+
/** A projection ahead starts each subject from where the live mix holds it. */
|
|
1186
|
+
carry(voice, subject) {
|
|
1187
|
+
const live = voice.subjects.get(subject);
|
|
1188
|
+
if (live === undefined)
|
|
1189
|
+
return undefined;
|
|
1190
|
+
const h = this.copyHeld(voice, live);
|
|
1191
|
+
h.from = h.stepped;
|
|
1192
|
+
return h;
|
|
1193
|
+
}
|
|
1194
|
+
/**
|
|
1195
|
+
* A projection back starts each subject from the latest copy kept at or before `t`, else from its
|
|
1196
|
+
* voice's start with fresh state, which is exact for a voice stepped at a fixed interval.
|
|
1197
|
+
*/
|
|
1198
|
+
recall(voice, subject, t) {
|
|
1199
|
+
const live = voice.subjects.get(subject);
|
|
1200
|
+
if (live === undefined)
|
|
1201
|
+
return undefined;
|
|
1202
|
+
const snap = live.snaps && last(live.snaps, t, true);
|
|
1203
|
+
if (snap) {
|
|
1204
|
+
const h = this.copyHeld(voice, snap.held);
|
|
1205
|
+
h.from = h.stepped;
|
|
1206
|
+
h.replay = live.inputs;
|
|
1207
|
+
return h;
|
|
1208
|
+
}
|
|
1209
|
+
const kept = new Map();
|
|
1210
|
+
const stepped = live.since < t ? live.since : t;
|
|
1211
|
+
return {
|
|
1212
|
+
reaches: live.reaches,
|
|
1213
|
+
delay: live.delay,
|
|
1214
|
+
since: live.since,
|
|
1215
|
+
weight: 0,
|
|
1216
|
+
rested: false,
|
|
1217
|
+
bands: new Uint8Array(voice.slots.length),
|
|
1218
|
+
state: live.reaches && voice.patch.state
|
|
1219
|
+
? voice.patch.state(subject)
|
|
1220
|
+
: undefined,
|
|
1221
|
+
stepped,
|
|
1222
|
+
ticks: 0,
|
|
1223
|
+
probed: Number.NaN,
|
|
1224
|
+
delta: null,
|
|
1225
|
+
phase: 0,
|
|
1226
|
+
seeks: 0,
|
|
1227
|
+
kept,
|
|
1228
|
+
keep: keeper(kept),
|
|
1229
|
+
voice,
|
|
1230
|
+
next: null,
|
|
1231
|
+
version: Number.NaN,
|
|
1232
|
+
loci: null,
|
|
1233
|
+
slot: -1,
|
|
1234
|
+
from: stepped,
|
|
1235
|
+
unknown: voice.spec.from === 'current',
|
|
1236
|
+
replay: live.inputs,
|
|
1237
|
+
};
|
|
1238
|
+
}
|
|
1239
|
+
/** Under `history` with `inputs`, copies the host fields any voice reads, once a frame, when they changed. */
|
|
1240
|
+
recordHost(now) {
|
|
1241
|
+
const log = this.hostLog;
|
|
1242
|
+
const prev = log[log.length - 1];
|
|
1243
|
+
if (prev !== undefined && prev.at === now)
|
|
1244
|
+
return;
|
|
1245
|
+
const host = this.opts.host;
|
|
1246
|
+
const fields = {};
|
|
1247
|
+
for (const v of this.voices)
|
|
1248
|
+
for (const f of v.patch.reads ?? none)
|
|
1249
|
+
if (!(f in fields))
|
|
1250
|
+
fields[f] = structuredClone(host[f]);
|
|
1251
|
+
if (prev !== undefined && same(prev.fields, fields))
|
|
1252
|
+
return;
|
|
1253
|
+
log.push({ at: now, fields });
|
|
1254
|
+
const reach = now - this.opts.history.ms;
|
|
1255
|
+
while (log.length > 1 && log[1].at <= reach)
|
|
1256
|
+
log.shift();
|
|
1257
|
+
}
|
|
1258
|
+
/** Under `history` with `inputs`, keeps what an input signal read, each time it changes. */
|
|
1259
|
+
record(held, value) {
|
|
1260
|
+
const history = this.opts.history;
|
|
1261
|
+
if (!history?.inputs || this.projecting)
|
|
1262
|
+
return;
|
|
1263
|
+
const inputs = held.inputs ?? [];
|
|
1264
|
+
held.inputs = inputs;
|
|
1265
|
+
const prev = inputs[inputs.length - 1];
|
|
1266
|
+
if (prev !== undefined && (prev.value === value || prev.at === this.now)) {
|
|
1267
|
+
if (prev.at === this.now)
|
|
1268
|
+
prev.value = value;
|
|
1269
|
+
return;
|
|
1270
|
+
}
|
|
1271
|
+
inputs.push({ at: this.now, value });
|
|
1272
|
+
const reach = this.now - history.ms;
|
|
1273
|
+
while (inputs.length > 1 && inputs[1].at <= reach)
|
|
1274
|
+
inputs.shift();
|
|
1275
|
+
}
|
|
1276
|
+
/** Under `history`, keeps a copy of a stateful voice's record for this subject every so often. */
|
|
1277
|
+
remember(voice, held) {
|
|
1278
|
+
const history = this.opts.history;
|
|
1279
|
+
if (!history || this.projecting)
|
|
1280
|
+
return;
|
|
1281
|
+
if (voice.patch.step === undefined && held.kept.size === 0 && held.base === undefined)
|
|
1282
|
+
return;
|
|
1283
|
+
const snaps = held.snaps ?? [];
|
|
1284
|
+
held.snaps = snaps;
|
|
1285
|
+
const prev = snaps[snaps.length - 1];
|
|
1286
|
+
if (prev !== undefined && this.now - prev.at < (history.every ?? 200))
|
|
1287
|
+
return;
|
|
1288
|
+
snaps.push({ at: this.now, held: this.copyHeld(voice, held) });
|
|
1289
|
+
const reach = this.now - history.ms;
|
|
1290
|
+
while (snaps.length > 1 && snaps[1].at <= reach)
|
|
1291
|
+
snaps.shift();
|
|
1292
|
+
}
|
|
1293
|
+
/** Per channel, the least sure voice that fed it this frame. */
|
|
1294
|
+
doubts(subject) {
|
|
1295
|
+
const out = {};
|
|
1296
|
+
for (const name of this.names)
|
|
1297
|
+
out[name] = 'exact';
|
|
1298
|
+
const rank = { exact: 0, stepped: 1, held: 2 };
|
|
1299
|
+
// A voice still waiting on an anchor nothing has answered may yet play, or stop, by now.
|
|
1300
|
+
for (const voice of this.voices) {
|
|
1301
|
+
const anchor = voice.spec.anchor;
|
|
1302
|
+
if (anchor === undefined || voice.state === 'done')
|
|
1303
|
+
continue;
|
|
1304
|
+
const waiting = (voice.state === 'pending' && !Number.isFinite(voice.start)) ||
|
|
1305
|
+
(voice.state !== 'pending' &&
|
|
1306
|
+
voice.state !== 'fading' &&
|
|
1307
|
+
(anchor.out !== undefined || anchor.end !== undefined) &&
|
|
1308
|
+
!Number.isFinite(voice.outAt));
|
|
1309
|
+
if (!waiting || !this.aims(voice, subject))
|
|
1310
|
+
continue;
|
|
1311
|
+
for (const slot of voice.slots)
|
|
1312
|
+
out[this.names[slot]] = 'held';
|
|
1313
|
+
}
|
|
1314
|
+
for (let held = this.chain(subject, this.now); held !== null; held = held.next) {
|
|
1315
|
+
const voice = held.voice;
|
|
1316
|
+
if (voice === null || voice.state === 'done')
|
|
1317
|
+
continue;
|
|
1318
|
+
if (held.weight <= 0)
|
|
1319
|
+
continue;
|
|
1320
|
+
const d = this.doubtOf(voice, held);
|
|
1321
|
+
for (const slot of voice.slots) {
|
|
1322
|
+
const name = this.names[slot];
|
|
1323
|
+
if (rank[d] > rank[out[name]])
|
|
1324
|
+
out[name] = d;
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
return out;
|
|
1328
|
+
}
|
|
1329
|
+
doubtOf(voice, held) {
|
|
1330
|
+
const weight = voice.spec.weight;
|
|
1331
|
+
if (held.unknown)
|
|
1332
|
+
return 'held';
|
|
1333
|
+
// A weight read back from a recording is what the mix used then, so its signal's state is moot.
|
|
1334
|
+
const replayed = held.replay !== undefined && last(held.replay, this.now, true) !== undefined;
|
|
1335
|
+
if (typeof weight === 'function' && weight.input && !replayed)
|
|
1336
|
+
return 'held';
|
|
1337
|
+
const reads = voice.patch.reads;
|
|
1338
|
+
if (reads !== undefined && reads.length > 0) {
|
|
1339
|
+
const then = this.hostThen?.fields;
|
|
1340
|
+
if (then === undefined || !reads.every((f) => f in then))
|
|
1341
|
+
return 'held';
|
|
1342
|
+
}
|
|
1343
|
+
if (this.now > (held.from ?? this.now)) {
|
|
1344
|
+
const tick = this.opts.stepMs;
|
|
1345
|
+
const fixed = tick !== undefined && tick > 0 && !this.reducedNow;
|
|
1346
|
+
if ((voice.patch.step && !fixed) || (held.kept.size > 0 && !replayed))
|
|
1347
|
+
return 'stepped';
|
|
1348
|
+
}
|
|
1349
|
+
return 'exact';
|
|
1350
|
+
}
|
|
354
1351
|
handle(voice) {
|
|
355
1352
|
const mix = this;
|
|
356
1353
|
return {
|
|
@@ -363,6 +1360,8 @@ class Mixer {
|
|
|
363
1360
|
},
|
|
364
1361
|
set weight(w) {
|
|
365
1362
|
voice.weight = w;
|
|
1363
|
+
mix.noted(voice);
|
|
1364
|
+
mix.lanes?.refill();
|
|
366
1365
|
},
|
|
367
1366
|
get rate() {
|
|
368
1367
|
return voice.rateAt(Number.isNaN(mix.now) ? voice.start : mix.now);
|
|
@@ -371,6 +1370,8 @@ class Mixer {
|
|
|
371
1370
|
voice.rebase(Number.isNaN(mix.now) ? voice.start : mix.now);
|
|
372
1371
|
voice.ramp = null;
|
|
373
1372
|
voice.rate = r;
|
|
1373
|
+
mix.noted(voice);
|
|
1374
|
+
mix.lanes?.refill();
|
|
374
1375
|
},
|
|
375
1376
|
ramp(r, over) {
|
|
376
1377
|
const now = Number.isNaN(mix.now) ? voice.start : mix.now;
|
|
@@ -378,10 +1379,15 @@ class Mixer {
|
|
|
378
1379
|
const from = voice.rateAt(now);
|
|
379
1380
|
voice.ramp = over > 0 && r !== from ? { from, to: r, over } : null;
|
|
380
1381
|
voice.rate = r;
|
|
1382
|
+
mix.noted(voice);
|
|
1383
|
+
mix.lanes?.refill();
|
|
381
1384
|
},
|
|
382
1385
|
seek(elapsed) {
|
|
383
1386
|
voice.rebase(Number.isNaN(mix.now) ? voice.start : mix.now);
|
|
384
1387
|
voice.anchorElapsed = elapsed;
|
|
1388
|
+
voice.seeks++;
|
|
1389
|
+
mix.noted(voice);
|
|
1390
|
+
mix.lanes?.refill();
|
|
385
1391
|
},
|
|
386
1392
|
fade(opts) {
|
|
387
1393
|
mix.beginFade(voice, opts ?? {});
|
|
@@ -389,57 +1395,60 @@ class Mixer {
|
|
|
389
1395
|
weightOf(subject) {
|
|
390
1396
|
if (voice.state === 'done')
|
|
391
1397
|
return 0;
|
|
1398
|
+
if (voice.laned && mix.lanes !== null) {
|
|
1399
|
+
const w = mix.lanes.weightOf(voice.id, mix.chains.get(subject)?.slot ?? -1);
|
|
1400
|
+
if (w !== undefined)
|
|
1401
|
+
return w;
|
|
1402
|
+
}
|
|
392
1403
|
return voice.subjects.get(subject)?.weight ?? 0;
|
|
393
1404
|
},
|
|
394
1405
|
done: voice.done,
|
|
395
1406
|
};
|
|
396
1407
|
}
|
|
397
|
-
beginFade(voice, opts) {
|
|
1408
|
+
beginFade(voice, opts, at) {
|
|
398
1409
|
if (voice.state === 'done' || voice.state === 'fading')
|
|
399
1410
|
return;
|
|
400
1411
|
const over = this.reduced ? 0 : (opts.over ?? voice.fade.out ?? 0);
|
|
1412
|
+
// A voice is linked into its subjects' chains once it plays, and a fading one plays.
|
|
1413
|
+
if (voice.state === 'pending')
|
|
1414
|
+
this.version++;
|
|
401
1415
|
voice.state = 'fading';
|
|
402
1416
|
voice.out = {
|
|
403
1417
|
from: 1,
|
|
404
|
-
at: Number.isNaN(this.now) ? voice.start : this.now,
|
|
1418
|
+
at: at ?? (Number.isNaN(this.now) ? voice.start : this.now),
|
|
405
1419
|
over,
|
|
406
1420
|
rest: opts.at === 'rest',
|
|
407
1421
|
deadline: opts.deadline,
|
|
408
1422
|
};
|
|
1423
|
+
this.noted(voice);
|
|
1424
|
+
if (opts.at === 'rest')
|
|
1425
|
+
this.lanes?.invalidate();
|
|
1426
|
+
else
|
|
1427
|
+
this.lanes?.refill();
|
|
409
1428
|
if (over === 0 && opts.at !== 'rest')
|
|
410
|
-
this.retire(voice);
|
|
1429
|
+
this.retire(voice, voice.out.at);
|
|
411
1430
|
}
|
|
412
|
-
|
|
1431
|
+
/** Removes a voice, recording that it left at `at`, default now. */
|
|
1432
|
+
retire(voice, at) {
|
|
1433
|
+
this.lanes?.invalidate();
|
|
1434
|
+
const motion = motionOf(voice.patch);
|
|
1435
|
+
if (motion !== undefined && motion.frame === voice.frame)
|
|
1436
|
+
motion.frame = noFrame;
|
|
1437
|
+
voice.frame = null;
|
|
413
1438
|
voice.state = 'done';
|
|
1439
|
+
voice.doneAt = at ?? (Number.isNaN(this.now) ? Number.NEGATIVE_INFINITY : this.now);
|
|
414
1440
|
voice.resolve();
|
|
415
1441
|
}
|
|
416
1442
|
/** The ramp a voice's own fade envelope applies this frame, 0..1. */
|
|
417
1443
|
envelope(voice, now, since) {
|
|
418
|
-
|
|
419
|
-
const ease = voice.ease;
|
|
420
|
-
let w = 1;
|
|
421
|
-
const fadeIn = voice.fade.in ?? 0;
|
|
422
|
-
if (fadeIn > 0 && !reduced) {
|
|
423
|
-
const u = (now - since) / fadeIn;
|
|
424
|
-
if (u < 1)
|
|
425
|
-
w *= ease ? ease(Math.max(0, u)) : Math.max(0, u);
|
|
426
|
-
}
|
|
427
|
-
const out = voice.out;
|
|
428
|
-
if (out && !out.rest) {
|
|
429
|
-
if (reduced || out.over === 0)
|
|
430
|
-
return 0;
|
|
431
|
-
const u = 1 - (now - out.at) / out.over;
|
|
432
|
-
const clamped = u < 0 ? 0 : u > 1 ? 1 : u;
|
|
433
|
-
w *= ease ? ease(clamped) : clamped;
|
|
434
|
-
}
|
|
435
|
-
return w;
|
|
1444
|
+
return envelope(voice.fade.in ?? 0, voice.out, voice.ease, this.reducedNow, now, since);
|
|
436
1445
|
}
|
|
437
1446
|
/** What this voice holds for this subject, made on first sight with `target` and `stagger` asked once. */
|
|
438
1447
|
held(voice, subject, now) {
|
|
439
1448
|
let held = voice.subjects.get(subject);
|
|
440
1449
|
if (held !== undefined)
|
|
441
1450
|
return held;
|
|
442
|
-
const reaches = voice
|
|
1451
|
+
const reaches = this.aims(voice, subject);
|
|
443
1452
|
const delay = reaches && voice.spec.stagger ? voice.spec.stagger(subject) : 0;
|
|
444
1453
|
// When the voice clock reads `delay`, from where it is anchored now; during a ramp this assumes
|
|
445
1454
|
// the rate it is ramping to.
|
|
@@ -457,13 +1466,24 @@ class Mixer {
|
|
|
457
1466
|
state: reaches && voice.patch.state
|
|
458
1467
|
? voice.patch.state(subject)
|
|
459
1468
|
: undefined,
|
|
460
|
-
stepped: now,
|
|
1469
|
+
stepped: this.backward && since < now ? since : now,
|
|
461
1470
|
ticks: 0,
|
|
462
1471
|
probed: Number.NaN,
|
|
463
1472
|
delta: null,
|
|
1473
|
+
phase: 0,
|
|
1474
|
+
seeks: 0,
|
|
464
1475
|
kept,
|
|
465
1476
|
keep: keeper(kept),
|
|
1477
|
+
voice,
|
|
1478
|
+
next: null,
|
|
1479
|
+
version: Number.NaN,
|
|
1480
|
+
loci: null,
|
|
1481
|
+
slot: -1,
|
|
466
1482
|
};
|
|
1483
|
+
if (this.projecting) {
|
|
1484
|
+
held.from = held.stepped;
|
|
1485
|
+
held.unknown = this.backward && voice.spec.from === 'current';
|
|
1486
|
+
}
|
|
467
1487
|
voice.subjects.set(subject, held);
|
|
468
1488
|
if (reaches)
|
|
469
1489
|
voice.seen++;
|
|
@@ -476,18 +1496,20 @@ class Mixer {
|
|
|
476
1496
|
this.sending.voice = voice;
|
|
477
1497
|
this.sending.subject = subject;
|
|
478
1498
|
const signal = typeof voice.spec.weight === 'function' ? voice.spec.weight : null;
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
1499
|
+
let base = voice.weight;
|
|
1500
|
+
if (signal) {
|
|
1501
|
+
const was = held.replay && last(held.replay, now, true);
|
|
1502
|
+
base = was ? was.value : signal(subject, voice.setting);
|
|
1503
|
+
if (signal.input && !was)
|
|
1504
|
+
this.record(held, base);
|
|
1505
|
+
}
|
|
1506
|
+
return clampWeight(base * this.envelope(voice, now, held.since));
|
|
482
1507
|
}
|
|
483
1508
|
/**
|
|
484
|
-
* One voice's delta for one subject this frame,
|
|
485
|
-
*
|
|
1509
|
+
* One live voice's delta for one subject this frame, through its record for the subject, or null
|
|
1510
|
+
* when it does not reach. Its weight is left in `w`.
|
|
486
1511
|
*/
|
|
487
|
-
influence(voice, subject, now) {
|
|
488
|
-
if (voice.state === 'pending' || voice.state === 'done')
|
|
489
|
-
return null;
|
|
490
|
-
const held = this.held(voice, subject, now);
|
|
1512
|
+
influence(voice, subject, now, held) {
|
|
491
1513
|
held.weight = 0;
|
|
492
1514
|
if (!held.reaches)
|
|
493
1515
|
return null;
|
|
@@ -497,37 +1519,24 @@ class Mixer {
|
|
|
497
1519
|
if (elapsed < 0)
|
|
498
1520
|
return null;
|
|
499
1521
|
const period = voice.patch.period;
|
|
500
|
-
const
|
|
501
|
-
const
|
|
502
|
-
|
|
503
|
-
let pass = 0;
|
|
504
|
-
if (period > 0) {
|
|
505
|
-
const done = Number.isFinite(passes) && elapsed >= period * passes;
|
|
506
|
-
phase = done ? 1 : (elapsed % period) / period;
|
|
507
|
-
pass = done ? passes - 1 : Math.floor(elapsed / period);
|
|
508
|
-
}
|
|
1522
|
+
const phase = phaseAt(elapsed, period, voice.passes);
|
|
1523
|
+
const pass = passAt(elapsed, period, voice.passes);
|
|
1524
|
+
this.prime(voice, held, now, elapsed, pass);
|
|
509
1525
|
const setting = voice.setting;
|
|
510
|
-
setting.timestamp = now;
|
|
511
|
-
const gap = now - held.stepped;
|
|
512
|
-
const cap = this.opts.maxDt;
|
|
513
|
-
setting.dt = this.reducedNow
|
|
514
|
-
? Number.POSITIVE_INFINITY
|
|
515
|
-
: cap !== undefined && gap > cap
|
|
516
|
-
? cap
|
|
517
|
-
: gap;
|
|
518
|
-
setting.elapsed = elapsed;
|
|
519
|
-
setting.pass = pass;
|
|
520
|
-
setting.weight = 0;
|
|
521
|
-
setting.state = held.state;
|
|
522
|
-
setting.keep = held.keep;
|
|
523
1526
|
const weight = this.weigh(voice, subject, now, held);
|
|
524
1527
|
setting.weight = weight;
|
|
525
1528
|
held.weight = weight;
|
|
526
|
-
if (held.probed === now && held.delta) {
|
|
1529
|
+
if (held.probed === now && held.delta && held.seeks === voice.seeks) {
|
|
1530
|
+
if (voice.holder !== held && voice.scratch.length > 0)
|
|
1531
|
+
this.keyed(voice, subject, held);
|
|
527
1532
|
this.w = weight;
|
|
528
|
-
this.h = held;
|
|
529
1533
|
return held.delta;
|
|
530
1534
|
}
|
|
1535
|
+
const keptBefore = reading.kept;
|
|
1536
|
+
const history = this.opts.history;
|
|
1537
|
+
if (history?.inputs && voice.patch.reads !== undefined && !this.projecting)
|
|
1538
|
+
this.recordHost(now);
|
|
1539
|
+
reading.horizon = this.horizonFor(voice, held.delay, now);
|
|
531
1540
|
const tick = this.opts.stepMs;
|
|
532
1541
|
if (voice.patch.step && held.probed !== now) {
|
|
533
1542
|
if (tick !== undefined && tick > 0 && !this.reducedNow)
|
|
@@ -539,21 +1548,19 @@ class Mixer {
|
|
|
539
1548
|
}
|
|
540
1549
|
let delta;
|
|
541
1550
|
if (voice.built) {
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
if (held.base === undefined) {
|
|
545
|
-
held.base = this.baseFor(voice, subject);
|
|
546
|
-
held.slope = this.slopeFor(voice, subject);
|
|
547
|
-
}
|
|
548
|
-
base = held.base;
|
|
549
|
-
}
|
|
550
|
-
delta = readKeys(voice.built, phase, held.delta ?? {}, base, voice.lerps, held.slope);
|
|
1551
|
+
held.phase = phase;
|
|
1552
|
+
delta = this.keyed(voice, subject, held);
|
|
551
1553
|
}
|
|
552
1554
|
else {
|
|
553
1555
|
delta = voice.patch.at(phase, subject, setting);
|
|
554
1556
|
}
|
|
555
1557
|
held.delta = delta;
|
|
556
1558
|
held.probed = now;
|
|
1559
|
+
held.seeks = voice.seeks;
|
|
1560
|
+
if (history !== undefined)
|
|
1561
|
+
this.remember(voice, held);
|
|
1562
|
+
if (reading.kept !== keptBefore && !voice.keeping)
|
|
1563
|
+
this.stateful(voice);
|
|
557
1564
|
if (voice.out?.rest && this.isRest(delta)) {
|
|
558
1565
|
held.weight = 0;
|
|
559
1566
|
held.rested = true;
|
|
@@ -561,7 +1568,22 @@ class Mixer {
|
|
|
561
1568
|
return null;
|
|
562
1569
|
}
|
|
563
1570
|
this.w = weight;
|
|
564
|
-
|
|
1571
|
+
return delta;
|
|
1572
|
+
}
|
|
1573
|
+
/** Reads a keys voice's stops at `held.phase` into the subject's delta, through the voice's scratch. */
|
|
1574
|
+
keyed(voice, subject, held) {
|
|
1575
|
+
let base;
|
|
1576
|
+
if (voice.spec.from === 'current') {
|
|
1577
|
+
if (held.base === undefined) {
|
|
1578
|
+
held.base = this.baseFor(voice, subject);
|
|
1579
|
+
held.slope = this.slopeFor(voice, subject);
|
|
1580
|
+
}
|
|
1581
|
+
base = held.base;
|
|
1582
|
+
}
|
|
1583
|
+
const delta = readKeys(voice.built, held.phase, held.delta ?? {}, base, voice.lerps, held.slope, voice.intos, voice.scratch);
|
|
1584
|
+
held.delta = delta;
|
|
1585
|
+
if (voice.scratch.length > 0)
|
|
1586
|
+
voice.holder = held;
|
|
565
1587
|
return delta;
|
|
566
1588
|
}
|
|
567
1589
|
/**
|
|
@@ -690,27 +1712,87 @@ class Mixer {
|
|
|
690
1712
|
return w >= on ? true : w <= off ? false : (was ?? w >= on);
|
|
691
1713
|
}
|
|
692
1714
|
/**
|
|
693
|
-
* The
|
|
694
|
-
* for the subject anyway; a pending one is
|
|
695
|
-
* first sight, and sight only comes once a voice plays.
|
|
1715
|
+
* The first of this subject's records, linked through every live voice that reaches it. A voice
|
|
1716
|
+
* is asked once, through the record it keeps for the subject anyway; a pending one is linked when
|
|
1717
|
+
* it goes live, since `target` is asked on first sight, and sight only comes once a voice plays.
|
|
696
1718
|
*/
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
const
|
|
704
|
-
|
|
705
|
-
|
|
1719
|
+
chain(subject, now) {
|
|
1720
|
+
const was = this.chains.get(subject);
|
|
1721
|
+
if (was !== undefined && was.version === this.version)
|
|
1722
|
+
return was;
|
|
1723
|
+
let first = null;
|
|
1724
|
+
let prev = null;
|
|
1725
|
+
const link = (voice) => {
|
|
1726
|
+
if (voice.state !== 'live' && voice.state !== 'fading')
|
|
1727
|
+
return;
|
|
1728
|
+
const held = this.held(voice, subject, now);
|
|
1729
|
+
if (!held.reaches)
|
|
1730
|
+
return;
|
|
1731
|
+
held.voice = voice;
|
|
1732
|
+
held.next = null;
|
|
1733
|
+
if (prev === null)
|
|
1734
|
+
first = held;
|
|
1735
|
+
else
|
|
1736
|
+
prev.next = held;
|
|
1737
|
+
prev = held;
|
|
1738
|
+
};
|
|
1739
|
+
const named = this.naming === 0 ? undefined : this.named.get(subject);
|
|
1740
|
+
let j = 0;
|
|
1741
|
+
for (const voice of this.general) {
|
|
1742
|
+
while (named !== undefined && j < named.length && named[j].id < voice.id)
|
|
1743
|
+
link(named[j++]);
|
|
1744
|
+
link(voice);
|
|
1745
|
+
}
|
|
1746
|
+
while (named !== undefined && j < named.length)
|
|
1747
|
+
link(named[j++]);
|
|
1748
|
+
const head = first ?? stub();
|
|
1749
|
+
head.version = this.version;
|
|
1750
|
+
if (was !== undefined && was !== head) {
|
|
1751
|
+
head.loci = was.loci;
|
|
1752
|
+
was.loci = null;
|
|
1753
|
+
head.slot = was.slot;
|
|
1754
|
+
was.slot = -1;
|
|
1755
|
+
}
|
|
1756
|
+
else if (was === undefined && this.lanes !== null && head.slot < 0)
|
|
1757
|
+
head.slot = this.lanes.number(subject);
|
|
1758
|
+
if (was !== head)
|
|
1759
|
+
this.chains.set(subject, head);
|
|
1760
|
+
return head;
|
|
1761
|
+
}
|
|
1762
|
+
/** Whether a voice reaches a subject by what its spec says: the subjects it names, or its `target`. */
|
|
1763
|
+
aims(voice, subject) {
|
|
1764
|
+
if (voice.named !== null)
|
|
1765
|
+
return voice.named.has(subject);
|
|
1766
|
+
return voice.spec.target ? voice.spec.target(subject) : true;
|
|
1767
|
+
}
|
|
1768
|
+
/** Files a voice under each subject it names, or among the voices that name none. */
|
|
1769
|
+
index(voice) {
|
|
1770
|
+
if (voice.named === null) {
|
|
1771
|
+
this.general.push(voice);
|
|
1772
|
+
return;
|
|
1773
|
+
}
|
|
1774
|
+
this.naming++;
|
|
1775
|
+
for (const subject of voice.named) {
|
|
1776
|
+
const list = this.named.get(subject);
|
|
1777
|
+
if (list === undefined)
|
|
1778
|
+
this.named.set(subject, [voice]);
|
|
1779
|
+
else
|
|
1780
|
+
list.push(voice);
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
unindex(voice) {
|
|
1784
|
+
this.naming--;
|
|
1785
|
+
for (const subject of voice.named) {
|
|
1786
|
+
const list = this.named.get(subject);
|
|
1787
|
+
if (list === undefined)
|
|
706
1788
|
continue;
|
|
707
|
-
|
|
708
|
-
|
|
1789
|
+
const i = list.indexOf(voice);
|
|
1790
|
+
if (i >= 0)
|
|
1791
|
+
list.splice(i, 1);
|
|
1792
|
+
if (list.length === 0)
|
|
1793
|
+
this.named.delete(subject);
|
|
709
1794
|
}
|
|
710
|
-
this.reach.set(subject, { version: this.version, voices });
|
|
711
|
-
return voices;
|
|
712
1795
|
}
|
|
713
|
-
locusBands = new Store();
|
|
714
1796
|
/** Voices whose stop 0 is mid-computation, so a fold for one cannot re-enter itself. */
|
|
715
1797
|
folding = new Set();
|
|
716
1798
|
/** Folds one voice's delta into the pose, its band state kept on the subject's record. */
|
|
@@ -737,10 +1819,42 @@ class Mixer {
|
|
|
737
1819
|
pose[key] = pose[key] === undefined ? copy(value) : channel.merge(pose[key], value);
|
|
738
1820
|
}
|
|
739
1821
|
}
|
|
1822
|
+
/** Clamps every bounded channel of a folded pose into its range, in place. */
|
|
1823
|
+
clamp(pose) {
|
|
1824
|
+
for (const slot of this.bounded) {
|
|
1825
|
+
const [lo, hi] = this.channels[slot].bounds;
|
|
1826
|
+
const key = this.names[slot];
|
|
1827
|
+
const v = pose[key];
|
|
1828
|
+
if (typeof v === 'number')
|
|
1829
|
+
pose[key] = v < lo ? lo : v > hi ? hi : v;
|
|
1830
|
+
else if (Array.isArray(v))
|
|
1831
|
+
for (let i = 0; i < v.length; i++) {
|
|
1832
|
+
const x = v[i];
|
|
1833
|
+
v[i] = x < lo ? lo : x > hi ? hi : x;
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
return pose;
|
|
1837
|
+
}
|
|
740
1838
|
fold(subject, out, dry = false, except) {
|
|
1839
|
+
const pose = this.folded(subject, out, dry, except);
|
|
1840
|
+
return (this.bounded.length === 0 ? pose : this.clamp(pose));
|
|
1841
|
+
}
|
|
1842
|
+
folded(subject, out, dry = false, except) {
|
|
1843
|
+
const now = this.now;
|
|
1844
|
+
const head = Number.isNaN(now) ? null : this.chain(subject, now);
|
|
1845
|
+
// A subject numbered since the frame's fill folds every voice here, laned ones included.
|
|
1846
|
+
const laned = head !== null && this.lanes?.prepare(head.slot, subject, now, this.version) === true;
|
|
1847
|
+
return this.foldWith(subject, out ?? {}, head, laned, dry, except);
|
|
1848
|
+
}
|
|
1849
|
+
/** The fold after a subject's chain is linked and its lanes asked whether it reads from them. */
|
|
1850
|
+
foldWith(subject, out, head, laned, dry, except) {
|
|
741
1851
|
const now = this.now;
|
|
742
|
-
const pose =
|
|
1852
|
+
const pose = out;
|
|
1853
|
+
const lanes = this.lanes;
|
|
1854
|
+
const skip = laned ? lanes.copies : null;
|
|
743
1855
|
for (let i = 0; i < this.names.length; i++) {
|
|
1856
|
+
if (skip?.[i])
|
|
1857
|
+
continue;
|
|
744
1858
|
const rest = this.channels[i].rest;
|
|
745
1859
|
const key = this.names[i];
|
|
746
1860
|
// Never `delete`: it drops a reused out object into dictionary mode for good.
|
|
@@ -749,32 +1863,42 @@ class Mixer {
|
|
|
749
1863
|
else if (pose[key] !== undefined)
|
|
750
1864
|
pose[key] = undefined;
|
|
751
1865
|
}
|
|
752
|
-
if (
|
|
1866
|
+
if (head === null)
|
|
753
1867
|
return pose;
|
|
754
|
-
|
|
1868
|
+
if (laned) {
|
|
1869
|
+
lanes.copy(head.slot, pose);
|
|
1870
|
+
if (lanes.whole)
|
|
1871
|
+
return pose;
|
|
1872
|
+
}
|
|
755
1873
|
if (this.loci === 0) {
|
|
756
|
-
for (
|
|
757
|
-
|
|
1874
|
+
for (let held = head; held !== null; held = held.next) {
|
|
1875
|
+
const voice = held.voice;
|
|
1876
|
+
if (voice === null || (laned && voice.laned))
|
|
758
1877
|
continue;
|
|
759
|
-
|
|
1878
|
+
if (voice.id === except || voice.state === 'done')
|
|
1879
|
+
continue;
|
|
1880
|
+
const delta = this.read(voice, subject, now, dry, held);
|
|
760
1881
|
if (delta === null || this.w <= 0)
|
|
761
1882
|
continue;
|
|
762
|
-
this.apply(pose, voice,
|
|
1883
|
+
this.apply(pose, voice, held, delta, this.w);
|
|
763
1884
|
}
|
|
764
1885
|
return pose;
|
|
765
1886
|
}
|
|
766
1887
|
const order = [];
|
|
767
1888
|
const weights = [];
|
|
768
1889
|
const loci = new Map();
|
|
769
|
-
for (
|
|
770
|
-
|
|
1890
|
+
for (let held = head; held !== null; held = held.next) {
|
|
1891
|
+
const voice = held.voice;
|
|
1892
|
+
if (voice === null || (laned && voice.laned))
|
|
1893
|
+
continue;
|
|
1894
|
+
if (voice.id === except || voice.state === 'done')
|
|
771
1895
|
continue;
|
|
772
|
-
const delta = this.read(voice, subject, now, dry);
|
|
1896
|
+
const delta = this.read(voice, subject, now, dry, held);
|
|
773
1897
|
if (delta === null)
|
|
774
1898
|
continue;
|
|
775
1899
|
const locus = voice.spec.locus;
|
|
776
1900
|
if (locus === undefined) {
|
|
777
|
-
order.push({ voice, held
|
|
1901
|
+
order.push({ voice, held, delta });
|
|
778
1902
|
weights.push(this.w);
|
|
779
1903
|
continue;
|
|
780
1904
|
}
|
|
@@ -787,10 +1911,10 @@ class Mixer {
|
|
|
787
1911
|
weights.push(0);
|
|
788
1912
|
}
|
|
789
1913
|
}
|
|
790
|
-
let bands =
|
|
791
|
-
if (bands ===
|
|
1914
|
+
let bands = head.loci;
|
|
1915
|
+
if (bands === null) {
|
|
792
1916
|
bands = new Map();
|
|
793
|
-
|
|
1917
|
+
head.loci = bands;
|
|
794
1918
|
}
|
|
795
1919
|
for (let n = 0; n < order.length; n++) {
|
|
796
1920
|
const at = order[n];
|
|
@@ -824,10 +1948,9 @@ class Mixer {
|
|
|
824
1948
|
return pose;
|
|
825
1949
|
}
|
|
826
1950
|
/** `influence`, or for a dry fold a subject already probed this frame, without advancing it. */
|
|
827
|
-
read(voice, subject, now, dry) {
|
|
1951
|
+
read(voice, subject, now, dry, held) {
|
|
828
1952
|
if (dry) {
|
|
829
|
-
|
|
830
|
-
if (held?.reaches && held.probed === now && held.delta) {
|
|
1953
|
+
if (held.reaches && held.probed === now && held.delta && held.seeks === voice.seeks) {
|
|
831
1954
|
const setting = voice.setting;
|
|
832
1955
|
setting.timestamp = now;
|
|
833
1956
|
setting.dt = 0;
|
|
@@ -837,21 +1960,23 @@ class Mixer {
|
|
|
837
1960
|
setting.state = held.state;
|
|
838
1961
|
setting.keep = held.keep;
|
|
839
1962
|
this.w = this.weigh(voice, subject, now, held);
|
|
840
|
-
|
|
1963
|
+
held.weight = this.w;
|
|
1964
|
+
if (voice.holder !== held && voice.scratch.length > 0)
|
|
1965
|
+
this.keyed(voice, subject, held);
|
|
841
1966
|
return held.delta;
|
|
842
1967
|
}
|
|
843
1968
|
}
|
|
844
|
-
return this.influence(voice, subject, now);
|
|
1969
|
+
return this.influence(voice, subject, now, held);
|
|
845
1970
|
}
|
|
846
1971
|
}
|
|
847
1972
|
/**
|
|
848
|
-
* The one that ships:
|
|
1973
|
+
* The one that ships: every form on the CPU, probed per subject on demand.
|
|
849
1974
|
*
|
|
850
1975
|
* @category engine
|
|
851
1976
|
*/
|
|
852
1977
|
export const mixer = {
|
|
853
1978
|
name: 'mixer',
|
|
854
|
-
runs: new Set(['fn', 'keys']),
|
|
1979
|
+
runs: new Set(['fn', 'keys', 'motion']),
|
|
855
1980
|
create(kit, opts) {
|
|
856
1981
|
return new Mixer(kit, opts);
|
|
857
1982
|
},
|