@msb235/blits 0.3.0 → 0.5.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 +314 -0
- package/README.md +3 -2
- package/dist/bare.d.ts +29 -0
- package/dist/bare.js +105 -0
- package/dist/blend.d.ts +34 -0
- package/dist/blend.js +102 -0
- package/dist/book.d.ts +53 -0
- package/dist/book.js +218 -0
- package/dist/chain.d.ts +22 -0
- package/dist/chain.js +150 -0
- package/dist/channels.d.ts +2 -0
- package/dist/channels.js +3 -1
- package/dist/clock.d.ts +50 -3
- package/dist/clock.js +102 -9
- package/dist/clone.d.ts +7 -0
- package/dist/clone.js +57 -0
- package/dist/columns.d.ts +29 -0
- package/dist/columns.js +183 -0
- package/dist/crowd.d.ts +128 -0
- package/dist/crowd.js +354 -0
- package/dist/cue.d.ts +3 -0
- package/dist/cue.js +115 -0
- package/dist/due.d.ts +12 -0
- package/dist/due.js +93 -0
- package/dist/fade.d.ts +19 -0
- package/dist/fade.js +133 -0
- package/dist/fill.d.ts +42 -0
- package/dist/fill.js +381 -0
- package/dist/fold.d.ts +51 -0
- package/dist/fold.js +475 -0
- package/dist/gather.d.ts +24 -0
- package/dist/gather.js +186 -0
- package/dist/handle.d.ts +31 -0
- package/dist/handle.js +56 -0
- package/dist/held.d.ts +14 -0
- package/dist/held.js +117 -0
- package/dist/history.d.ts +22 -0
- package/dist/history.js +149 -0
- package/dist/hosts.d.ts +16 -0
- package/dist/hosts.js +178 -0
- package/dist/index.d.ts +1 -1
- package/dist/keyrows.d.ts +36 -0
- package/dist/keyrows.js +138 -0
- package/dist/lane.d.ts +210 -0
- package/dist/lane.js +187 -0
- package/dist/lanes.d.ts +143 -122
- package/dist/lanes.js +236 -742
- package/dist/locus.d.ts +40 -0
- package/dist/locus.js +89 -0
- package/dist/marks.d.ts +12 -0
- package/dist/marks.js +80 -0
- package/dist/meet.d.ts +16 -0
- package/dist/meet.js +113 -0
- package/dist/mixer.d.ts +244 -169
- package/dist/mixer.js +321 -1793
- package/dist/motion.d.ts +104 -19
- package/dist/motion.js +448 -180
- package/dist/move.d.ts +9 -0
- package/dist/move.js +190 -0
- package/dist/named.d.ts +12 -0
- package/dist/named.js +39 -0
- package/dist/numbers.d.ts +18 -6
- package/dist/numbers.js +59 -31
- package/dist/owed.d.ts +24 -0
- package/dist/owed.js +51 -0
- package/dist/owner.d.ts +43 -0
- package/dist/owner.js +117 -0
- package/dist/pace.d.ts +40 -0
- package/dist/pace.js +113 -0
- package/dist/patch.d.ts +51 -10
- package/dist/patch.js +140 -45
- package/dist/place.d.ts +20 -0
- package/dist/place.js +196 -0
- package/dist/project.d.ts +4 -0
- package/dist/project.js +233 -0
- package/dist/pull.d.ts +5 -0
- package/dist/pull.js +175 -0
- package/dist/qualify.d.ts +31 -0
- package/dist/qualify.js +345 -0
- package/dist/relink.d.ts +25 -0
- package/dist/relink.js +59 -0
- package/dist/rows.d.ts +33 -0
- package/dist/rows.js +282 -0
- package/dist/sample.d.ts +21 -0
- package/dist/sample.js +232 -0
- package/dist/signals.d.ts +12 -9
- package/dist/signals.js +8 -5
- package/dist/store.d.ts +10 -3
- package/dist/store.js +44 -9
- package/dist/strays.d.ts +21 -0
- package/dist/strays.js +84 -0
- package/dist/tweened.d.ts +11 -0
- package/dist/tweened.js +48 -0
- package/dist/types.d.ts +261 -42
- package/dist/unreached.d.ts +5 -0
- package/dist/unreached.js +24 -0
- package/dist/voice.d.ts +240 -0
- package/dist/voice.js +322 -0
- package/dist/weigh.d.ts +26 -0
- package/dist/weigh.js +89 -0
- package/docs/schema.html +151 -46
- package/package.json +5 -2
package/dist/mixer.js
CHANGED
|
@@ -1,324 +1,29 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
1
|
+
import { blended, blendOf, laneRead, shareKept } from './blend.js';
|
|
2
|
+
import { aims, chain, linkable } from './chain.js';
|
|
3
|
+
import { lerpInto } from './channels.js';
|
|
4
|
+
import { cue } from './cue.js';
|
|
5
|
+
import { beginFade, parting } from './fade.js';
|
|
6
|
+
import { apply, baseFor, clamp, fold, folded, foldLoci, foldWith, influence, isRest, keyed, linked, near, owedBy, passes, read, slopeFor, tick, } from './fold.js';
|
|
7
|
+
import { held, shownOf, sinceOf, unreachedOf } from './held.js';
|
|
8
|
+
import { book, laneHost } from './hosts.js';
|
|
9
|
+
import { Lanes } from './lanes.js';
|
|
10
|
+
import { foldLocus } from './locus.js';
|
|
11
|
+
import { listed } from './marks.js';
|
|
12
|
+
import { move, nextFrame, waits } from './move.js';
|
|
13
|
+
import { ownerPatch } from './owner.js';
|
|
14
|
+
import { Pace } from './pace.js';
|
|
15
|
+
import { pin } from './place.js';
|
|
16
|
+
import { project } from './project.js';
|
|
17
|
+
import { keep, pull } from './pull.js';
|
|
18
|
+
import { Steps } from './relink.js';
|
|
7
19
|
import { Store } from './store.js';
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const made = init();
|
|
13
|
-
kept.set(owner, made);
|
|
14
|
-
reading.kept++;
|
|
15
|
-
return made;
|
|
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 = () => { };
|
|
94
|
-
const near = (a, b) => {
|
|
95
|
-
if (typeof a === 'number' && typeof b === 'number')
|
|
96
|
-
return Math.abs(a - b) < 1e-9;
|
|
97
|
-
if (Array.isArray(a) && Array.isArray(b))
|
|
98
|
-
return a.length === b.length && a.every((v, i) => near(v, b[i]));
|
|
99
|
-
return a === b;
|
|
100
|
-
};
|
|
101
|
-
const copy = (v) => (Array.isArray(v) ? [...v] : v);
|
|
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 {
|
|
131
|
-
id;
|
|
132
|
-
spec;
|
|
133
|
-
patch;
|
|
134
|
-
fade;
|
|
135
|
-
start;
|
|
136
|
-
state = 'pending';
|
|
137
|
-
rate;
|
|
138
|
-
weight;
|
|
139
|
-
/**
|
|
140
|
-
* elapsed = anchorElapsed + the rate integrated from anchorNow, rebased on a rate change or a seek.
|
|
141
|
-
* Without a ramp the rate is constant; with one it moves linearly from `from` to `to` over `over`
|
|
142
|
-
* mix ms starting at the anchor, and holds `to` after.
|
|
143
|
-
*/
|
|
144
|
-
anchorNow;
|
|
145
|
-
anchorElapsed = 0;
|
|
146
|
-
ramp = null;
|
|
147
|
-
out = null;
|
|
148
|
-
subjects = new Store();
|
|
149
|
-
/** The subjects its spec names, or null where it names none. */
|
|
150
|
-
named;
|
|
151
|
-
/** Kit slot of each channel the patch writes, in `writes` order. */
|
|
152
|
-
slots;
|
|
153
|
-
/** The stops built ahead of time, for a `keys` patch. */
|
|
154
|
-
built;
|
|
155
|
-
/** The kit's `lerp` for each channel the patch writes, which keyed stops interpolate through. */
|
|
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;
|
|
174
|
-
ease;
|
|
175
|
-
/** How many passes its `loop` plays, worked out once. */
|
|
176
|
-
passes;
|
|
177
|
-
/** Reused for every call this voice makes, so it is valid only during the call. */
|
|
178
|
-
setting;
|
|
179
|
-
/** Every subject this voice has been asked about, so a handover knows when it is finished. */
|
|
180
|
-
seen = 0;
|
|
181
|
-
/** The longest stagger of any subject seen, so a finite loop waits for the last of them. */
|
|
182
|
-
latest = 0;
|
|
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;
|
|
194
|
-
resolve;
|
|
195
|
-
done;
|
|
196
|
-
constructor(id, spec, patch, fade, now, start, slotOf, channels, host, send) {
|
|
197
|
-
this.id = id;
|
|
198
|
-
this.spec = spec;
|
|
199
|
-
this.patch = patch;
|
|
200
|
-
this.fade = fade;
|
|
201
|
-
this.start = start;
|
|
202
|
-
this.slots = patch.writes.map((k) => slotOf.get(k));
|
|
203
|
-
this.named = spec.subjects ? new Set(spec.subjects) : null;
|
|
204
|
-
this.built = patch.form === 'keys' && patch.keys ? builtOf(patch) : null;
|
|
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;
|
|
209
|
-
this.ease = fade.ease === undefined ? undefined : curve(fade.ease);
|
|
210
|
-
this.passes = passesOf(spec.loop);
|
|
211
|
-
this.setting = {
|
|
212
|
-
timestamp: 0,
|
|
213
|
-
dt: 0,
|
|
214
|
-
elapsed: 0,
|
|
215
|
-
pass: 0,
|
|
216
|
-
weight: 0,
|
|
217
|
-
state: undefined,
|
|
218
|
-
host,
|
|
219
|
-
keep: keeper(new Map()),
|
|
220
|
-
send,
|
|
221
|
-
};
|
|
222
|
-
this.rate = spec.rate ?? 1;
|
|
223
|
-
this.weight = typeof spec.weight === 'number' ? spec.weight : 1;
|
|
224
|
-
this.anchorNow = start;
|
|
225
|
-
this.done = new Promise((r) => {
|
|
226
|
-
this.resolve = r;
|
|
227
|
-
});
|
|
228
|
-
if (now >= start)
|
|
229
|
-
this.state = 'live';
|
|
230
|
-
}
|
|
231
|
-
elapsedAt(now) {
|
|
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;
|
|
237
|
-
const r = this.ramp;
|
|
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;
|
|
298
|
-
}
|
|
299
|
-
rateAt(now) {
|
|
300
|
-
const r = this.ramp;
|
|
301
|
-
if (r === null)
|
|
302
|
-
return this.rate;
|
|
303
|
-
const u = (now - this.anchorNow) / r.over;
|
|
304
|
-
return u >= 1 ? r.to : u <= 0 ? r.from : r.from + (r.to - r.from) * u;
|
|
305
|
-
}
|
|
306
|
-
/** Moves the anchor to `now`, carrying what is left of a ramp. */
|
|
307
|
-
rebase(now) {
|
|
308
|
-
const rate = this.rateAt(now);
|
|
309
|
-
this.anchorElapsed = this.elapsedAt(now);
|
|
310
|
-
const r = this.ramp;
|
|
311
|
-
if (r !== null) {
|
|
312
|
-
const left = this.anchorNow + r.over - now;
|
|
313
|
-
this.ramp = left > 0 ? { from: rate, to: r.to, over: left } : null;
|
|
314
|
-
}
|
|
315
|
-
this.anchorNow = now;
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
class Mixer {
|
|
20
|
+
import { none } from './voice.js';
|
|
21
|
+
import { base, capped, fadeOf, horizonFor, ownedBy, ownerBase, prime, weigh, weighOwned, } from './weigh.js';
|
|
22
|
+
// biome-ignore lint/suspicious/noUnsafeDeclarationMerging: every member of `Methods` is installed below
|
|
23
|
+
export class Mixer {
|
|
319
24
|
kit;
|
|
320
25
|
opts;
|
|
321
|
-
|
|
26
|
+
cued = [];
|
|
322
27
|
/** Under `history`, voices that have left but that a read back may still reach. */
|
|
323
28
|
gone = [];
|
|
324
29
|
/** Set on a projection's own mixer: it sends nothing, keeps no history, and reads without committing. */
|
|
@@ -333,24 +38,59 @@ class Mixer {
|
|
|
333
38
|
*/
|
|
334
39
|
hostLog = [];
|
|
335
40
|
/**
|
|
336
|
-
* Marks the host announced on a score:
|
|
337
|
-
* now before any sync; `made` is
|
|
41
|
+
* Marks the host announced on a score: `at` in host time, so the mix's rate never moves one, and
|
|
42
|
+
* NaN until the next sync for one announced as now before any sync; `made` is the mix time it was
|
|
43
|
+
* announced, for a read back to know what was known.
|
|
338
44
|
*/
|
|
339
45
|
announced = [];
|
|
340
46
|
hostThen;
|
|
341
47
|
/** Each subject's last two poses and when they were probed, for `from: 'current'`. */
|
|
342
48
|
pose = new Store();
|
|
343
49
|
nextId = 1;
|
|
344
|
-
/**
|
|
50
|
+
/**
|
|
51
|
+
* The mix clock: host time at the mix's rate. Host time is the host's timestamp less every gap
|
|
52
|
+
* `rebase` has taken out, `u` at the last sync.
|
|
53
|
+
*/
|
|
345
54
|
now = Number.NaN;
|
|
55
|
+
u = Number.NaN;
|
|
56
|
+
/** This mix's frame among every mix's: moved by a sync, and by a control change or drop within one. */
|
|
57
|
+
frame = nextFrame();
|
|
346
58
|
offset = 0;
|
|
59
|
+
/** The mix's own rate; null while it has never been set, when mix time is host time. */
|
|
60
|
+
pace = null;
|
|
61
|
+
/** Pending voices whose start the host gave, by its host time, kept where the rate puts it. */
|
|
62
|
+
pins = null;
|
|
347
63
|
last = Number.NaN;
|
|
348
64
|
rebasing = false;
|
|
349
65
|
wantsPose = false;
|
|
350
66
|
/** The pose `pull` folds a subject into where it cannot read straight from the lanes. */
|
|
351
67
|
scratch;
|
|
68
|
+
/** The last array `pull` read, by position, with each subject's chain head, to skip the lookup. */
|
|
69
|
+
/** Live voices by the earliest mix time `moveTo` would change each, a binary heap. */
|
|
70
|
+
due = [];
|
|
71
|
+
/** Voices retired since `moveTo` last pruned them, in the order they retired. */
|
|
72
|
+
retired = [];
|
|
73
|
+
/** Visit every voice each sync, as before the due queue; for tests that compare the two. */
|
|
74
|
+
walkAll = false;
|
|
75
|
+
pulled = [];
|
|
76
|
+
pulledHeads = [];
|
|
77
|
+
/** Each remembered head's lane slot, and the `version` and `relinks` they were all current at. */
|
|
78
|
+
pulledSlots = new Int32Array(0);
|
|
79
|
+
pulledVersion = Number.NaN;
|
|
80
|
+
pulledRelinks = -1;
|
|
81
|
+
/** Counts chains made stale one subject at a time, which `version` does not. */
|
|
82
|
+
relinks = 0;
|
|
352
83
|
/** Read once a sync, so a `reduce` function is not called per voice per subject. */
|
|
353
84
|
reducedNow = false;
|
|
85
|
+
/** Something was changed since the last sync, so the next frame may differ from this one. */
|
|
86
|
+
stirred = false;
|
|
87
|
+
/** Counts the changes `stir` records, which a frame's record of a probe is current only before. */
|
|
88
|
+
stirs = 0;
|
|
89
|
+
/** Records a change to the mix's voices or controls. */
|
|
90
|
+
stir() {
|
|
91
|
+
this.stirred = true;
|
|
92
|
+
this.stirs++;
|
|
93
|
+
}
|
|
354
94
|
/**
|
|
355
95
|
* Per subject, the first record of the chain through every live voice that reaches it, or a stub
|
|
356
96
|
* where none does. Relinked when `version` moves, which is whenever the list or a voice's pending
|
|
@@ -358,14 +98,28 @@ class Mixer {
|
|
|
358
98
|
*/
|
|
359
99
|
chains = new Store();
|
|
360
100
|
version = 0;
|
|
101
|
+
steps = new Steps();
|
|
361
102
|
/** Per subject, the voices whose `subjects` name it, in voice order. */
|
|
362
103
|
named = new Store();
|
|
104
|
+
/** Voices a subject has been faded out of, which `drop` looks in besides those that reach it. */
|
|
105
|
+
parters = new Set();
|
|
106
|
+
/**
|
|
107
|
+
* Motion patches that may hold state for a subject no voice still reaching it plays, which `drop`
|
|
108
|
+
* frees besides those voices' own: per subject, a patch a voice naming it left while another
|
|
109
|
+
* voice plays on, or that the host retargeted it on; and any patch a voice over every subject
|
|
110
|
+
* left while another plays on. How many live voices play each motion patch says which.
|
|
111
|
+
*/
|
|
112
|
+
strays = new Store();
|
|
113
|
+
strayAll = new Set();
|
|
114
|
+
playing = new Map();
|
|
363
115
|
/** How many voices in the list carry `subjects`. */
|
|
364
116
|
naming = 0;
|
|
365
117
|
/** The voices in the list that name no subjects, in voice order. */
|
|
366
118
|
general = [];
|
|
367
119
|
/** How many voices in the list carry a locus; with none, a fold allocates nothing. */
|
|
368
120
|
loci = 0;
|
|
121
|
+
/** What every motion patch it plays asks for its voice's time, holding this mix weakly. */
|
|
122
|
+
owner = null;
|
|
369
123
|
/** How many voices in the list are anchored, so a sync with none skips placing them. */
|
|
370
124
|
anchored = 0;
|
|
371
125
|
/** Events sent since the last drain, and who is being probed, so `send` knows whose they are. */
|
|
@@ -392,18 +146,29 @@ class Mixer {
|
|
|
392
146
|
channels;
|
|
393
147
|
/** Slots of the channels that declare bounds, clamped after every fold. */
|
|
394
148
|
bounded;
|
|
149
|
+
/** By slot, a stock array channel's in-place `lerp`. */
|
|
150
|
+
lerpsInto;
|
|
151
|
+
/** What a fold with a locus in play gathers into, one per depth of folds under way. */
|
|
152
|
+
locusScratch = [];
|
|
153
|
+
locusDepth = 0;
|
|
395
154
|
slotOf = new Map();
|
|
396
155
|
lanes;
|
|
156
|
+
handles = null;
|
|
157
|
+
/** What `book` made, still booking; null while there is none. */
|
|
158
|
+
bookers = null;
|
|
159
|
+
/** Every owner still in the mix, null until one is cued. */
|
|
160
|
+
owners = null;
|
|
397
161
|
constructor(kit, opts) {
|
|
398
162
|
this.kit = kit;
|
|
399
163
|
this.opts = opts;
|
|
400
164
|
this.names = Object.keys(kit);
|
|
401
165
|
this.channels = this.names.map((k) => kit[k]);
|
|
402
166
|
this.bounded = this.channels.flatMap((c, i) => (c.bounds ? [i] : []));
|
|
167
|
+
this.lerpsInto = this.channels.map((c) => lerpInto(c));
|
|
403
168
|
this.names.forEach((k, i) => {
|
|
404
169
|
this.slotOf.set(k, i);
|
|
405
170
|
});
|
|
406
|
-
this.lanes = opts.lanes === false ? null : new Lanes(
|
|
171
|
+
this.lanes = opts.lanes === false ? null : new Lanes(laneHost(this));
|
|
407
172
|
}
|
|
408
173
|
get reduced() {
|
|
409
174
|
const r = this.opts.reduce;
|
|
@@ -413,336 +178,71 @@ class Mixer {
|
|
|
413
178
|
return this.opts.band ?? { on: 0.6, off: 0.4 };
|
|
414
179
|
}
|
|
415
180
|
cue(spec) {
|
|
416
|
-
const patch = spec.patch;
|
|
417
181
|
const engine = this.opts.engine ?? mixer;
|
|
418
|
-
if (!engine.runs.has(patch.form))
|
|
419
|
-
throw new Error(`blits: engine ${engine.name} does not run ${patch.form} patches`);
|
|
420
|
-
|
|
421
|
-
if (!(channel in this.kit))
|
|
422
|
-
throw new Error(`blits: kit has no channel ${String(channel)}, which this patch writes`);
|
|
423
|
-
const wanted = patch.kit?.[channel];
|
|
424
|
-
const here = this.kit[channel];
|
|
425
|
-
if (wanted && wanted !== here && (wanted.kind === undefined || wanted.kind !== here.kind))
|
|
426
|
-
throw new Error(`blits: channel ${String(channel)} is ${here.kind ?? 'a custom channel'} in this kit, but the patch was written for ${wanted.kind ?? 'a custom channel'}`);
|
|
427
|
-
}
|
|
428
|
-
if (patch.reads) {
|
|
429
|
-
const host = this.opts.host;
|
|
430
|
-
for (const field of patch.reads)
|
|
431
|
-
if (typeof host !== 'object' || host === null || !(field in host))
|
|
432
|
-
throw new Error(`blits: the patch reads host.${field}, which this mix's host lacks`);
|
|
433
|
-
}
|
|
434
|
-
if (spec.from === 'current' && patch.form !== 'keys')
|
|
435
|
-
throw new Error("blits: from: 'current' needs a keys patch");
|
|
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;
|
|
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
|
-
}
|
|
458
|
-
this.voices.push(voice);
|
|
459
|
-
this.index(voice);
|
|
460
|
-
this.version++;
|
|
461
|
-
if (spec.locus !== undefined)
|
|
462
|
-
this.loci++;
|
|
463
|
-
if (spec.from === 'current')
|
|
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
|
-
}
|
|
478
|
-
return this.handle(voice);
|
|
182
|
+
if (!engine.runs.has(spec.patch.form))
|
|
183
|
+
throw new Error(`blits: engine ${engine.name} does not run ${spec.patch.form} patches`);
|
|
184
|
+
return cue(this, spec);
|
|
479
185
|
}
|
|
480
186
|
blend(patches, by, spec = {}) {
|
|
481
187
|
const locus = `blend:${this.nextId}`;
|
|
482
|
-
const
|
|
188
|
+
const of = blendOf(by, patches.length - 1);
|
|
483
189
|
return patches.map((patch, i) => {
|
|
484
|
-
const
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
const weight = by.input ? Object.assign(share, { input: true }) : share;
|
|
490
|
-
return this.cue({ ...spec, patch, weight, locus });
|
|
190
|
+
const handle = this.cue({ ...spec, patch, weight: by, locus });
|
|
191
|
+
const voice = this.cued[this.cued.length - 1];
|
|
192
|
+
voice.blend = { of, i };
|
|
193
|
+
of.members.push(voice);
|
|
194
|
+
return handle;
|
|
491
195
|
});
|
|
492
196
|
}
|
|
197
|
+
owns(spec) {
|
|
198
|
+
if (spec.loop !== undefined)
|
|
199
|
+
throw new Error('blits: an owner does not loop: a pass would have to restart its children');
|
|
200
|
+
return this.cue({ ...spec, patch: ownerPatch });
|
|
201
|
+
}
|
|
493
202
|
sync(timestamp) {
|
|
494
203
|
if (this.rebasing && !Number.isNaN(this.last))
|
|
495
204
|
this.offset += timestamp - this.last;
|
|
496
205
|
this.rebasing = false;
|
|
497
206
|
this.last = timestamp;
|
|
498
|
-
const
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
this.
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
this.
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
this.
|
|
207
|
+
const u = timestamp - this.offset;
|
|
208
|
+
const pace = this.pace;
|
|
209
|
+
const now = pace === null ? u : pace.sync(u);
|
|
210
|
+
const later = u !== this.u;
|
|
211
|
+
const still = later && now === this.now;
|
|
212
|
+
this.u = u;
|
|
213
|
+
// Host time moving while the mix clock stands still lands what waits on host time or the host.
|
|
214
|
+
if (now !== this.now || (later && pace !== null && waits(this)))
|
|
215
|
+
move(this, now);
|
|
216
|
+
// It is a frame too, which asks every weight signal again, so one reading input follows it.
|
|
217
|
+
if (still) {
|
|
218
|
+
this.frame = nextFrame();
|
|
219
|
+
this.lanes?.refill();
|
|
511
220
|
}
|
|
221
|
+
const bookers = this.bookers;
|
|
222
|
+
if (bookers !== null)
|
|
223
|
+
for (const b of bookers)
|
|
224
|
+
b.sync();
|
|
512
225
|
}
|
|
513
|
-
|
|
514
|
-
this
|
|
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
|
-
}
|
|
526
|
-
for (const voice of this.voices) {
|
|
527
|
-
if (voice.state === 'done')
|
|
528
|
-
continue;
|
|
529
|
-
if (voice.state === 'live' && voice.outAt <= now)
|
|
530
|
-
this.beginFade(voice, {}, Math.max(voice.start, voice.outAt));
|
|
531
|
-
if (voice.state === 'pending' && now >= voice.start) {
|
|
532
|
-
voice.state = 'live';
|
|
533
|
-
this.version++;
|
|
534
|
-
}
|
|
535
|
-
const period = voice.patch.period;
|
|
536
|
-
const passes = voice.passes;
|
|
537
|
-
if (voice.state === 'live' && period > 0 && Number.isFinite(passes)) {
|
|
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))));
|
|
543
|
-
}
|
|
544
|
-
if (voice.state === 'fading' && voice.out) {
|
|
545
|
-
const { at, over, rest, deadline } = voice.out;
|
|
546
|
-
const spent = now - at;
|
|
547
|
-
if (rest) {
|
|
548
|
-
const out = deadline !== undefined && spent >= deadline;
|
|
549
|
-
const settled = voice.seen > 0 && voice.restedCount >= voice.seen;
|
|
550
|
-
if (out)
|
|
551
|
-
this.retire(voice, at + deadline);
|
|
552
|
-
else if (settled)
|
|
553
|
-
this.retire(voice);
|
|
554
|
-
}
|
|
555
|
-
else if (spent >= over)
|
|
556
|
-
this.retire(voice, at + over);
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
let pruned = false;
|
|
560
|
-
for (let i = this.voices.length - 1; i >= 0; i--) {
|
|
561
|
-
const voice = this.voices[i];
|
|
562
|
-
if (voice.state === 'done') {
|
|
563
|
-
this.voices.splice(i, 1);
|
|
564
|
-
if (this.opts.history)
|
|
565
|
-
this.gone.push(voice);
|
|
566
|
-
this.version++;
|
|
567
|
-
if (voice.named === null)
|
|
568
|
-
pruned = true;
|
|
569
|
-
else
|
|
570
|
-
this.unindex(voice);
|
|
571
|
-
if (voice.spec.locus !== undefined)
|
|
572
|
-
this.loci--;
|
|
573
|
-
if (voice.spec.anchor !== undefined)
|
|
574
|
-
this.anchored--;
|
|
575
|
-
}
|
|
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
|
-
}
|
|
226
|
+
book(opts) {
|
|
227
|
+
return book(this, opts);
|
|
584
228
|
}
|
|
585
229
|
probe(subject, out) {
|
|
586
230
|
const pose = this.fold(subject, out);
|
|
587
231
|
this.keep(subject, pose, out);
|
|
232
|
+
// Where the lanes answer `atRest` from their own values, there is nothing to keep.
|
|
233
|
+
if (this.restStamps !== null && !(this.linkedLaned && this.restsLaned()))
|
|
234
|
+
this.restStamps.set(subject, 2 * this.restsKey() + (this.rests(pose) ? 1 : 0));
|
|
588
235
|
return pose;
|
|
589
236
|
}
|
|
590
|
-
/** After a probe, keeps the pose a retarget from `'current'` reads as the subject's last. */
|
|
591
|
-
keep(subject, pose, out) {
|
|
592
|
-
// Keeping last frame's pose is free when the mix allocated it; a host sampling into its own
|
|
593
|
-
// object only pays for the copy once something in the mix has asked to retarget from it.
|
|
594
|
-
let kept;
|
|
595
|
-
if (out === undefined)
|
|
596
|
-
kept = pose;
|
|
597
|
-
else if (this.wantsPose) {
|
|
598
|
-
kept = {};
|
|
599
|
-
for (const key of this.names) {
|
|
600
|
-
const v = pose[key];
|
|
601
|
-
if (v !== undefined)
|
|
602
|
-
kept[key] = copy(v);
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
if (kept !== undefined) {
|
|
606
|
-
const rec = this.pose.get(subject);
|
|
607
|
-
if (rec === undefined)
|
|
608
|
-
this.pose.set(subject, { pose: kept, at: this.now, prev: undefined, prevAt: Number.NaN });
|
|
609
|
-
else if (rec.at === this.now)
|
|
610
|
-
rec.pose = kept;
|
|
611
|
-
else {
|
|
612
|
-
rec.prev = rec.pose;
|
|
613
|
-
rec.prevAt = rec.at;
|
|
614
|
-
rec.pose = kept;
|
|
615
|
-
rec.at = this.now;
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
237
|
pull(subjects, into) {
|
|
620
|
-
|
|
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;
|
|
238
|
+
pull(this, subjects, into);
|
|
678
239
|
}
|
|
679
240
|
project(timestamp) {
|
|
680
|
-
const t = timestamp - this.offset;
|
|
681
241
|
const c = new Mixer(this.kit, { ...this.opts, history: undefined, lanes: false });
|
|
682
|
-
c
|
|
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
|
-
};
|
|
242
|
+
return project(this, c, timestamp);
|
|
743
243
|
}
|
|
744
244
|
announce(name, opts = {}) {
|
|
745
|
-
const at = opts.at !== undefined ? opts.at - this.offset : this.
|
|
245
|
+
const at = opts.at !== undefined ? opts.at - this.offset : this.u;
|
|
746
246
|
this.announced.push({
|
|
747
247
|
name,
|
|
748
248
|
score: opts.score,
|
|
@@ -753,41 +253,28 @@ class Mixer {
|
|
|
753
253
|
});
|
|
754
254
|
}
|
|
755
255
|
marks(from, to) {
|
|
756
|
-
|
|
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);
|
|
256
|
+
return listed(this, from, to).map(({ order: _, ...m }) => m);
|
|
788
257
|
}
|
|
789
258
|
atRest(subject) {
|
|
790
|
-
|
|
259
|
+
this.restStamps ??= new Store();
|
|
260
|
+
const head = this.linked(subject);
|
|
261
|
+
const laned = this.linkedLaned;
|
|
262
|
+
const lanes = this.lanes;
|
|
263
|
+
// Every voice on lanes and nothing to clamp: the pose a fold would make is the lanes' values.
|
|
264
|
+
if (laned && this.restsLaned()) {
|
|
265
|
+
const slot = head.slot;
|
|
266
|
+
if (!lanes.owes(slot))
|
|
267
|
+
return lanes.rests(slot);
|
|
268
|
+
}
|
|
269
|
+
// A probe this frame answers for the pose it gave the host.
|
|
270
|
+
const since = (this.restStamps.get(subject) ?? Number.NaN) - 2 * this.restsKey();
|
|
271
|
+
if (since >= 0)
|
|
272
|
+
return since === 1;
|
|
273
|
+
this.restScratch ??= {};
|
|
274
|
+
const pose = this.foldWith(subject, this.restScratch, head, laned, true);
|
|
275
|
+
return this.rests((this.bounded.length === 0 ? pose : this.clamp(pose)));
|
|
276
|
+
}
|
|
277
|
+
rests(pose) {
|
|
791
278
|
for (let i = 0; i < this.names.length; i++) {
|
|
792
279
|
const channel = this.channels[i];
|
|
793
280
|
const value = pose[this.names[i]];
|
|
@@ -803,23 +290,119 @@ class Mixer {
|
|
|
803
290
|
rebase() {
|
|
804
291
|
this.rebasing = true;
|
|
805
292
|
}
|
|
293
|
+
voices(tag) {
|
|
294
|
+
const out = [];
|
|
295
|
+
for (const voice of this.cued)
|
|
296
|
+
if (voice.state !== 'done' && (tag === undefined || voice.spec.tags?.includes(tag)))
|
|
297
|
+
out.push(voice.handle);
|
|
298
|
+
return out;
|
|
299
|
+
}
|
|
806
300
|
get live() {
|
|
807
|
-
return this.
|
|
301
|
+
return this.cued.some((v) => v.state !== 'done');
|
|
302
|
+
}
|
|
303
|
+
get inert() {
|
|
304
|
+
if (this.stirred)
|
|
305
|
+
return false;
|
|
306
|
+
// Standing still, a voice changes no pose but by its weight signal, or by an anchor or start
|
|
307
|
+
// that host time may yet reach.
|
|
308
|
+
if (this.pace?.stopped(this.u))
|
|
309
|
+
return this.cued.every((v) => v.state === 'done' ||
|
|
310
|
+
(v.state !== 'pending' &&
|
|
311
|
+
typeof v.spec.weight !== 'function' &&
|
|
312
|
+
v.spec.anchor === undefined));
|
|
313
|
+
return this.cued.every((v) => this.still(v, this.now));
|
|
314
|
+
}
|
|
315
|
+
get rate() {
|
|
316
|
+
return this.pace === null ? 1 : this.pace.rateAt(this.u);
|
|
317
|
+
}
|
|
318
|
+
set rate(r) {
|
|
319
|
+
this.ramp(r, 0);
|
|
320
|
+
}
|
|
321
|
+
ramp(rate, over) {
|
|
322
|
+
if (!(rate >= 0 && rate < Number.POSITIVE_INFINITY))
|
|
323
|
+
throw new RangeError(`blits: a mix's rate is a finite number, 0 or more, not ${rate}`);
|
|
324
|
+
if (this.pace === null) {
|
|
325
|
+
this.pace = new Pace(this.opts.history !== undefined);
|
|
326
|
+
// Until now mix time was host time, so a pending voice's start is the host time it was given.
|
|
327
|
+
for (const v of this.cued)
|
|
328
|
+
if (v.state === 'pending' && !v.placing && v.owner === null && v.spec.start !== undefined)
|
|
329
|
+
pin(this, v, v.start);
|
|
330
|
+
}
|
|
331
|
+
const history = this.opts.history;
|
|
332
|
+
const reach = history === undefined ? Number.NEGATIVE_INFINITY : this.now - history.ms;
|
|
333
|
+
this.pace.change(this.u, rate, over, reach);
|
|
334
|
+
this.stir();
|
|
335
|
+
}
|
|
336
|
+
/** Whether a voice will change no pose from `now` on, short of a change made to it. */
|
|
337
|
+
still(voice, now) {
|
|
338
|
+
if (voice.state === 'done')
|
|
339
|
+
return true;
|
|
340
|
+
if (voice.state !== 'held' && voice.state !== 'live')
|
|
341
|
+
return false;
|
|
342
|
+
if (typeof voice.spec.weight === 'function' || voice.parts !== null)
|
|
343
|
+
return false;
|
|
344
|
+
const fadeIn = this.reducedNow ? 0 : (voice.fade.in ?? 0);
|
|
345
|
+
if (fadeIn > 0 && now - this.sinceOf(voice, voice.latest) < fadeIn)
|
|
346
|
+
return false;
|
|
347
|
+
if (voice.state === 'held' || voice.holding !== null)
|
|
348
|
+
return true;
|
|
349
|
+
const motion = voice.motion;
|
|
350
|
+
return motion !== undefined && voice.elapsedAt(now) >= voice.latest && motion.landed();
|
|
808
351
|
}
|
|
809
352
|
mute(opts) {
|
|
810
|
-
for (const voice of this.
|
|
811
|
-
|
|
353
|
+
for (const voice of this.cued)
|
|
354
|
+
beginFade(this, voice, { over: opts?.over });
|
|
812
355
|
}
|
|
813
356
|
drop(subject) {
|
|
357
|
+
this.frame = nextFrame();
|
|
814
358
|
const head = this.chains.get(subject);
|
|
359
|
+
// Nothing that kept this head, such as `pull`'s remembered list, may take it as current again.
|
|
360
|
+
if (head !== undefined) {
|
|
361
|
+
head.version = Number.NaN;
|
|
362
|
+
this.relinks++;
|
|
363
|
+
}
|
|
815
364
|
if (head !== undefined && head.slot >= 0 && this.lanes !== null)
|
|
816
365
|
this.lanes.release(head.slot);
|
|
817
366
|
this.pose.delete(subject);
|
|
367
|
+
this.restStamps?.delete(subject);
|
|
818
368
|
this.chains.delete(subject);
|
|
819
|
-
|
|
369
|
+
this.stir();
|
|
370
|
+
// A record of it is only in a voice over every subject, one naming it, or one gone; a ramp out
|
|
371
|
+
// of a voice that does not name it is in `parters`, and a patch's own state in `motions`.
|
|
372
|
+
const forget = (voice) => {
|
|
820
373
|
voice.subjects.delete(subject);
|
|
821
|
-
|
|
822
|
-
|
|
374
|
+
voice.motion?.release(subject);
|
|
375
|
+
voice.parts?.delete(subject);
|
|
376
|
+
voice.parted?.delete(subject);
|
|
377
|
+
voice.blend?.of.reads.delete(subject);
|
|
378
|
+
};
|
|
379
|
+
for (const voice of this.general)
|
|
380
|
+
forget(voice);
|
|
381
|
+
const named = this.named.get(subject);
|
|
382
|
+
if (named !== undefined)
|
|
383
|
+
for (const voice of [...named])
|
|
384
|
+
forget(voice);
|
|
385
|
+
for (const voice of this.gone)
|
|
386
|
+
forget(voice);
|
|
387
|
+
if (this.owners !== null)
|
|
388
|
+
for (const voice of this.owners)
|
|
389
|
+
forget(voice);
|
|
390
|
+
for (const voice of this.parters) {
|
|
391
|
+
forget(voice);
|
|
392
|
+
if (voice.parts?.size === 0)
|
|
393
|
+
voice.parts = null;
|
|
394
|
+
if (voice.parted?.size === 0)
|
|
395
|
+
voice.parted = null;
|
|
396
|
+
if (voice.parts === null && voice.parted === null)
|
|
397
|
+
this.parters.delete(voice);
|
|
398
|
+
}
|
|
399
|
+
const strays = this.strays.get(subject);
|
|
400
|
+
if (strays !== undefined)
|
|
401
|
+
for (const motion of strays)
|
|
402
|
+
motion.release(subject);
|
|
403
|
+
this.strays.delete(subject);
|
|
404
|
+
for (const motion of this.strayAll)
|
|
405
|
+
motion.release(subject);
|
|
823
406
|
}
|
|
824
407
|
drain(tag) {
|
|
825
408
|
const all = this.sent;
|
|
@@ -837,1138 +420,83 @@ class Mixer {
|
|
|
837
420
|
}
|
|
838
421
|
return out.sort((a, b) => a.timestamp - b.timestamp);
|
|
839
422
|
}
|
|
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
|
-
}
|
|
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
|
-
}
|
|
1351
|
-
handle(voice) {
|
|
1352
|
-
const mix = this;
|
|
1353
|
-
return {
|
|
1354
|
-
id: voice.id,
|
|
1355
|
-
get state() {
|
|
1356
|
-
return voice.state;
|
|
1357
|
-
},
|
|
1358
|
-
get weight() {
|
|
1359
|
-
return voice.weight;
|
|
1360
|
-
},
|
|
1361
|
-
set weight(w) {
|
|
1362
|
-
voice.weight = w;
|
|
1363
|
-
mix.noted(voice);
|
|
1364
|
-
mix.lanes?.refill();
|
|
1365
|
-
},
|
|
1366
|
-
get rate() {
|
|
1367
|
-
return voice.rateAt(Number.isNaN(mix.now) ? voice.start : mix.now);
|
|
1368
|
-
},
|
|
1369
|
-
set rate(r) {
|
|
1370
|
-
voice.rebase(Number.isNaN(mix.now) ? voice.start : mix.now);
|
|
1371
|
-
voice.ramp = null;
|
|
1372
|
-
voice.rate = r;
|
|
1373
|
-
mix.noted(voice);
|
|
1374
|
-
mix.lanes?.refill();
|
|
1375
|
-
},
|
|
1376
|
-
ramp(r, over) {
|
|
1377
|
-
const now = Number.isNaN(mix.now) ? voice.start : mix.now;
|
|
1378
|
-
voice.rebase(now);
|
|
1379
|
-
const from = voice.rateAt(now);
|
|
1380
|
-
voice.ramp = over > 0 && r !== from ? { from, to: r, over } : null;
|
|
1381
|
-
voice.rate = r;
|
|
1382
|
-
mix.noted(voice);
|
|
1383
|
-
mix.lanes?.refill();
|
|
1384
|
-
},
|
|
1385
|
-
seek(elapsed) {
|
|
1386
|
-
voice.rebase(Number.isNaN(mix.now) ? voice.start : mix.now);
|
|
1387
|
-
voice.anchorElapsed = elapsed;
|
|
1388
|
-
voice.seeks++;
|
|
1389
|
-
mix.noted(voice);
|
|
1390
|
-
mix.lanes?.refill();
|
|
1391
|
-
},
|
|
1392
|
-
fade(opts) {
|
|
1393
|
-
mix.beginFade(voice, opts ?? {});
|
|
1394
|
-
},
|
|
1395
|
-
weightOf(subject) {
|
|
1396
|
-
if (voice.state === 'done')
|
|
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
|
-
}
|
|
1403
|
-
return voice.subjects.get(subject)?.weight ?? 0;
|
|
1404
|
-
},
|
|
1405
|
-
done: voice.done,
|
|
1406
|
-
};
|
|
1407
|
-
}
|
|
1408
|
-
beginFade(voice, opts, at) {
|
|
1409
|
-
if (voice.state === 'done' || voice.state === 'fading')
|
|
1410
|
-
return;
|
|
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++;
|
|
1415
|
-
voice.state = 'fading';
|
|
1416
|
-
voice.out = {
|
|
1417
|
-
from: 1,
|
|
1418
|
-
at: at ?? (Number.isNaN(this.now) ? voice.start : this.now),
|
|
1419
|
-
over,
|
|
1420
|
-
rest: opts.at === 'rest',
|
|
1421
|
-
deadline: opts.deadline,
|
|
1422
|
-
};
|
|
1423
|
-
this.noted(voice);
|
|
1424
|
-
if (opts.at === 'rest')
|
|
1425
|
-
this.lanes?.invalidate();
|
|
1426
|
-
else
|
|
1427
|
-
this.lanes?.refill();
|
|
1428
|
-
if (over === 0 && opts.at !== 'rest')
|
|
1429
|
-
this.retire(voice, voice.out.at);
|
|
1430
|
-
}
|
|
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;
|
|
1438
|
-
voice.state = 'done';
|
|
1439
|
-
voice.doneAt = at ?? (Number.isNaN(this.now) ? Number.NEGATIVE_INFINITY : this.now);
|
|
1440
|
-
voice.resolve();
|
|
1441
|
-
}
|
|
1442
|
-
/** The ramp a voice's own fade envelope applies this frame, 0..1. */
|
|
1443
|
-
envelope(voice, now, since) {
|
|
1444
|
-
return envelope(voice.fade.in ?? 0, voice.out, voice.ease, this.reducedNow, now, since);
|
|
1445
|
-
}
|
|
1446
|
-
/** What this voice holds for this subject, made on first sight with `target` and `stagger` asked once. */
|
|
1447
|
-
held(voice, subject, now) {
|
|
1448
|
-
let held = voice.subjects.get(subject);
|
|
1449
|
-
if (held !== undefined)
|
|
1450
|
-
return held;
|
|
1451
|
-
const reaches = this.aims(voice, subject);
|
|
1452
|
-
const delay = reaches && voice.spec.stagger ? voice.spec.stagger(subject) : 0;
|
|
1453
|
-
// When the voice clock reads `delay`, from where it is anchored now; during a ramp this assumes
|
|
1454
|
-
// the rate it is ramping to.
|
|
1455
|
-
const since = voice.rate > 0
|
|
1456
|
-
? voice.anchorNow + (delay - voice.anchorElapsed) / voice.rate
|
|
1457
|
-
: voice.start + delay;
|
|
1458
|
-
const kept = new Map();
|
|
1459
|
-
held = {
|
|
1460
|
-
reaches,
|
|
1461
|
-
delay,
|
|
1462
|
-
since,
|
|
1463
|
-
weight: 0,
|
|
1464
|
-
rested: false,
|
|
1465
|
-
bands: new Uint8Array(voice.slots.length),
|
|
1466
|
-
state: reaches && voice.patch.state
|
|
1467
|
-
? voice.patch.state(subject)
|
|
1468
|
-
: undefined,
|
|
1469
|
-
stepped: this.backward && since < now ? since : now,
|
|
1470
|
-
ticks: 0,
|
|
1471
|
-
probed: Number.NaN,
|
|
1472
|
-
delta: null,
|
|
1473
|
-
phase: 0,
|
|
1474
|
-
seeks: 0,
|
|
1475
|
-
kept,
|
|
1476
|
-
keep: keeper(kept),
|
|
1477
|
-
voice,
|
|
1478
|
-
next: null,
|
|
1479
|
-
version: Number.NaN,
|
|
1480
|
-
loci: null,
|
|
1481
|
-
slot: -1,
|
|
1482
|
-
};
|
|
1483
|
-
if (this.projecting) {
|
|
1484
|
-
held.from = held.stepped;
|
|
1485
|
-
held.unknown = this.backward && voice.spec.from === 'current';
|
|
1486
|
-
}
|
|
1487
|
-
voice.subjects.set(subject, held);
|
|
1488
|
-
if (reaches)
|
|
1489
|
-
voice.seen++;
|
|
1490
|
-
if (delay > voice.latest)
|
|
1491
|
-
voice.latest = delay;
|
|
1492
|
-
return held;
|
|
1493
|
-
}
|
|
1494
|
-
/** The weight a voice gives a subject this frame, with the setting already filled in. */
|
|
1495
|
-
weigh(voice, subject, now, held) {
|
|
1496
|
-
this.sending.voice = voice;
|
|
1497
|
-
this.sending.subject = subject;
|
|
1498
|
-
const signal = typeof voice.spec.weight === 'function' ? voice.spec.weight : null;
|
|
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));
|
|
1507
|
-
}
|
|
1508
|
-
/**
|
|
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`.
|
|
1511
|
-
*/
|
|
1512
|
-
influence(voice, subject, now, held) {
|
|
1513
|
-
held.weight = 0;
|
|
1514
|
-
if (!held.reaches)
|
|
1515
|
-
return null;
|
|
1516
|
-
if (voice.out?.rest && held.rested)
|
|
1517
|
-
return null;
|
|
1518
|
-
const elapsed = voice.elapsedAt(now) - held.delay;
|
|
1519
|
-
if (elapsed < 0)
|
|
1520
|
-
return null;
|
|
1521
|
-
const period = voice.patch.period;
|
|
1522
|
-
const phase = phaseAt(elapsed, period, voice.passes);
|
|
1523
|
-
const pass = passAt(elapsed, period, voice.passes);
|
|
1524
|
-
this.prime(voice, held, now, elapsed, pass);
|
|
1525
|
-
const setting = voice.setting;
|
|
1526
|
-
const weight = this.weigh(voice, subject, now, held);
|
|
1527
|
-
setting.weight = weight;
|
|
1528
|
-
held.weight = weight;
|
|
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);
|
|
1532
|
-
this.w = weight;
|
|
1533
|
-
return held.delta;
|
|
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);
|
|
1540
|
-
const tick = this.opts.stepMs;
|
|
1541
|
-
if (voice.patch.step && held.probed !== now) {
|
|
1542
|
-
if (tick !== undefined && tick > 0 && !this.reducedNow)
|
|
1543
|
-
this.tick(voice, subject, held, tick);
|
|
1544
|
-
else if (now !== held.stepped) {
|
|
1545
|
-
voice.patch.step(held.state, setting.dt, subject, setting);
|
|
1546
|
-
held.stepped = now;
|
|
1547
|
-
}
|
|
1548
|
-
}
|
|
1549
|
-
let delta;
|
|
1550
|
-
if (voice.built) {
|
|
1551
|
-
held.phase = phase;
|
|
1552
|
-
delta = this.keyed(voice, subject, held);
|
|
1553
|
-
}
|
|
1554
|
-
else {
|
|
1555
|
-
delta = voice.patch.at(phase, subject, setting);
|
|
1556
|
-
}
|
|
1557
|
-
held.delta = delta;
|
|
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);
|
|
1564
|
-
if (voice.out?.rest && this.isRest(delta)) {
|
|
1565
|
-
held.weight = 0;
|
|
1566
|
-
held.rested = true;
|
|
1567
|
-
voice.restedCount++;
|
|
1568
|
-
return null;
|
|
1569
|
-
}
|
|
1570
|
-
this.w = weight;
|
|
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;
|
|
1587
|
-
return delta;
|
|
1588
|
-
}
|
|
1589
|
-
/**
|
|
1590
|
-
* Runs `step` once per whole interval between where this subject last stopped and now, on a grid
|
|
1591
|
-
* counted from when its delay ran out, and leaves the remainder for the next sample. Counting
|
|
1592
|
-
* rather than adding keeps the grid where it is however many samples it is reached through.
|
|
1593
|
-
*/
|
|
1594
|
-
tick(voice, subject, held, tick) {
|
|
1595
|
-
const now = this.now;
|
|
1596
|
-
const setting = voice.setting;
|
|
1597
|
-
const frameDt = setting.dt;
|
|
1598
|
-
let n = held.ticks;
|
|
1599
|
-
// The epsilon keeps an interval like 1000 / 120 from landing a hair short of a whole count.
|
|
1600
|
-
const due = Math.floor((now - held.since) / tick + 1e-9);
|
|
1601
|
-
const cap = this.opts.maxDt;
|
|
1602
|
-
if (cap !== undefined) {
|
|
1603
|
-
const most = Math.floor(cap / tick);
|
|
1604
|
-
if (due - n > most)
|
|
1605
|
-
n = due - most;
|
|
1606
|
-
}
|
|
1607
|
-
const step = voice.patch.step;
|
|
1608
|
-
setting.dt = tick;
|
|
1609
|
-
for (n++; n <= due; n++) {
|
|
1610
|
-
setting.timestamp = held.since + n * tick;
|
|
1611
|
-
step(held.state, tick, subject, setting);
|
|
1612
|
-
}
|
|
1613
|
-
held.ticks = due > held.ticks ? due : held.ticks;
|
|
1614
|
-
held.stepped = held.since + held.ticks * tick;
|
|
1615
|
-
setting.timestamp = now;
|
|
1616
|
-
setting.dt = frameDt;
|
|
1617
|
-
}
|
|
1618
|
-
/**
|
|
1619
|
-
* Stop 0 for a retargeting voice: the subject's pose in the frame before this voice contributes.
|
|
1620
|
-
* With no such frame on record — a voice cued before the subject was ever probed, or a host that
|
|
1621
|
-
* samples into its own object — it is this frame's pose with every other voice folded in.
|
|
1622
|
-
*/
|
|
1623
|
-
baseFor(voice, subject) {
|
|
1624
|
-
let prior = this.pose.get(subject)?.pose;
|
|
1625
|
-
if (prior === undefined && !this.folding.has(voice.id)) {
|
|
1626
|
-
this.folding.add(voice.id);
|
|
1627
|
-
try {
|
|
1628
|
-
prior = this.fold(subject, undefined, false, voice.id);
|
|
1629
|
-
}
|
|
1630
|
-
finally {
|
|
1631
|
-
this.folding.delete(voice.id);
|
|
1632
|
-
}
|
|
1633
|
-
}
|
|
1634
|
-
const base = {};
|
|
1635
|
-
for (const key of voice.patch.writes) {
|
|
1636
|
-
const channel = this.kit[key];
|
|
1637
|
-
const value = prior?.[key];
|
|
1638
|
-
const fallback = channel.rest;
|
|
1639
|
-
if (value !== undefined)
|
|
1640
|
-
base[key] = copy(value);
|
|
1641
|
-
else if (fallback !== undefined)
|
|
1642
|
-
base[key] = copy(fallback);
|
|
1643
|
-
}
|
|
1644
|
-
return base;
|
|
1645
|
-
}
|
|
1646
|
-
/**
|
|
1647
|
-
* How fast each numeric channel this voice writes was moving, from the subject's last two probed
|
|
1648
|
-
* poses, in units per ms. Undefined with fewer than two on record, which starts the voice at rest.
|
|
1649
|
-
*/
|
|
1650
|
-
slopeFor(voice, subject) {
|
|
1651
|
-
const rec = this.pose.get(subject);
|
|
1652
|
-
if (rec?.prev === undefined || !(rec.at > rec.prevAt))
|
|
1653
|
-
return undefined;
|
|
1654
|
-
const dt = rec.at - rec.prevAt;
|
|
1655
|
-
const now = rec.pose;
|
|
1656
|
-
const before = rec.prev;
|
|
1657
|
-
const slope = {};
|
|
1658
|
-
for (const key of voice.patch.writes) {
|
|
1659
|
-
const a = before[key];
|
|
1660
|
-
const b = now[key];
|
|
1661
|
-
if (typeof a === 'number' && typeof b === 'number')
|
|
1662
|
-
slope[key] = (b - a) / dt;
|
|
1663
|
-
else if (Array.isArray(a) && Array.isArray(b) && a.length === b.length)
|
|
1664
|
-
slope[key] = b.map((v, i) => (v - a[i]) / dt);
|
|
1665
|
-
}
|
|
1666
|
-
return slope;
|
|
1667
|
-
}
|
|
1668
|
-
isRest(delta) {
|
|
1669
|
-
for (const key of Object.keys(delta)) {
|
|
1670
|
-
if (delta[key] === undefined)
|
|
1671
|
-
continue;
|
|
1672
|
-
const channel = this.kit[key];
|
|
1673
|
-
if (channel.rest === undefined)
|
|
1674
|
-
return false;
|
|
1675
|
-
if (!near(delta[key], channel.rest))
|
|
1676
|
-
return false;
|
|
1677
|
-
}
|
|
1678
|
-
return true;
|
|
1679
|
-
}
|
|
1680
|
-
/** Folds a locus's members into one influence through each channel's own `lerp`. */
|
|
1681
|
-
foldLocus(members) {
|
|
1682
|
-
let sum = 0;
|
|
1683
|
-
for (const m of members)
|
|
1684
|
-
sum += m.weight;
|
|
1685
|
-
const delta = {};
|
|
1686
|
-
if (sum <= 0)
|
|
1687
|
-
return { delta, weight: 0 };
|
|
1688
|
-
const taken = new Map();
|
|
1689
|
-
for (const member of members) {
|
|
1690
|
-
if (member.weight <= 0)
|
|
1691
|
-
continue;
|
|
1692
|
-
for (const key of Object.keys(member.delta)) {
|
|
1693
|
-
if (member.delta[key] === undefined)
|
|
1694
|
-
continue;
|
|
1695
|
-
const channel = this.kit[key];
|
|
1696
|
-
const held = taken.get(key);
|
|
1697
|
-
if (held === undefined) {
|
|
1698
|
-
delta[key] = copy(member.delta[key]);
|
|
1699
|
-
taken.set(key, member.weight);
|
|
1700
|
-
continue;
|
|
1701
|
-
}
|
|
1702
|
-
const total = held + member.weight;
|
|
1703
|
-
delta[key] = channel.lerp(delta[key], member.delta[key], member.weight / total);
|
|
1704
|
-
taken.set(key, total);
|
|
1705
|
-
}
|
|
1706
|
-
}
|
|
1707
|
-
return { delta, weight: sum > 1 ? 1 : sum };
|
|
1708
|
-
}
|
|
1709
|
-
/** Whether a rest-less channel's influence is switched on, across a band rather than an edge. */
|
|
1710
|
-
passes(was, w) {
|
|
1711
|
-
const { on, off } = this.band;
|
|
1712
|
-
return w >= on ? true : w <= off ? false : (was ?? w >= on);
|
|
1713
|
-
}
|
|
1714
|
-
/**
|
|
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.
|
|
1718
|
-
*/
|
|
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)
|
|
1788
|
-
continue;
|
|
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);
|
|
1794
|
-
}
|
|
1795
|
-
}
|
|
1796
423
|
/** Voices whose stop 0 is mid-computation, so a fold for one cannot re-enter itself. */
|
|
1797
424
|
folding = new Set();
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
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
|
-
}
|
|
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) {
|
|
1851
|
-
const now = this.now;
|
|
1852
|
-
const pose = out;
|
|
1853
|
-
const lanes = this.lanes;
|
|
1854
|
-
const skip = laned ? lanes.copies : null;
|
|
1855
|
-
for (let i = 0; i < this.names.length; i++) {
|
|
1856
|
-
if (skip?.[i])
|
|
1857
|
-
continue;
|
|
1858
|
-
const rest = this.channels[i].rest;
|
|
1859
|
-
const key = this.names[i];
|
|
1860
|
-
// Never `delete`: it drops a reused out object into dictionary mode for good.
|
|
1861
|
-
if (rest !== undefined)
|
|
1862
|
-
pose[key] = copy(rest);
|
|
1863
|
-
else if (pose[key] !== undefined)
|
|
1864
|
-
pose[key] = undefined;
|
|
1865
|
-
}
|
|
1866
|
-
if (head === null)
|
|
1867
|
-
return pose;
|
|
1868
|
-
if (laned) {
|
|
1869
|
-
lanes.copy(head.slot, pose);
|
|
1870
|
-
if (lanes.whole)
|
|
1871
|
-
return pose;
|
|
1872
|
-
}
|
|
1873
|
-
if (this.loci === 0) {
|
|
1874
|
-
for (let held = head; held !== null; held = held.next) {
|
|
1875
|
-
const voice = held.voice;
|
|
1876
|
-
if (voice === null || (laned && voice.laned))
|
|
1877
|
-
continue;
|
|
1878
|
-
if (voice.id === except || voice.state === 'done')
|
|
1879
|
-
continue;
|
|
1880
|
-
const delta = this.read(voice, subject, now, dry, held);
|
|
1881
|
-
if (delta === null || this.w <= 0)
|
|
1882
|
-
continue;
|
|
1883
|
-
this.apply(pose, voice, held, delta, this.w);
|
|
1884
|
-
}
|
|
1885
|
-
return pose;
|
|
1886
|
-
}
|
|
1887
|
-
const order = [];
|
|
1888
|
-
const weights = [];
|
|
1889
|
-
const loci = new Map();
|
|
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')
|
|
1895
|
-
continue;
|
|
1896
|
-
const delta = this.read(voice, subject, now, dry, held);
|
|
1897
|
-
if (delta === null)
|
|
1898
|
-
continue;
|
|
1899
|
-
const locus = voice.spec.locus;
|
|
1900
|
-
if (locus === undefined) {
|
|
1901
|
-
order.push({ voice, held, delta });
|
|
1902
|
-
weights.push(this.w);
|
|
1903
|
-
continue;
|
|
1904
|
-
}
|
|
1905
|
-
const members = loci.get(locus);
|
|
1906
|
-
if (members)
|
|
1907
|
-
members.push({ delta, weight: this.w });
|
|
1908
|
-
else {
|
|
1909
|
-
loci.set(locus, [{ delta, weight: this.w }]);
|
|
1910
|
-
order.push(locus);
|
|
1911
|
-
weights.push(0);
|
|
1912
|
-
}
|
|
1913
|
-
}
|
|
1914
|
-
let bands = head.loci;
|
|
1915
|
-
if (bands === null) {
|
|
1916
|
-
bands = new Map();
|
|
1917
|
-
head.loci = bands;
|
|
1918
|
-
}
|
|
1919
|
-
for (let n = 0; n < order.length; n++) {
|
|
1920
|
-
const at = order[n];
|
|
1921
|
-
if (typeof at !== 'string') {
|
|
1922
|
-
if (weights[n] > 0)
|
|
1923
|
-
this.apply(pose, at.voice, at.held, at.delta, weights[n]);
|
|
1924
|
-
continue;
|
|
1925
|
-
}
|
|
1926
|
-
const { delta, weight } = this.foldLocus(loci.get(at));
|
|
1927
|
-
if (weight <= 0)
|
|
1928
|
-
continue;
|
|
1929
|
-
for (const key of Object.keys(delta)) {
|
|
1930
|
-
const channel = this.kit[key];
|
|
1931
|
-
const value = delta[key];
|
|
1932
|
-
if (value === undefined)
|
|
1933
|
-
continue;
|
|
1934
|
-
if (channel.rest !== undefined && channel.scale) {
|
|
1935
|
-
// pose[key] is the copy of rest this fold made, so it is ours to write into.
|
|
1936
|
-
pose[key] = channel.fold
|
|
1937
|
-
? channel.fold(pose[key], value, weight)
|
|
1938
|
-
: channel.merge(pose[key], channel.scale(value, weight));
|
|
1939
|
-
continue;
|
|
1940
|
-
}
|
|
1941
|
-
const band = `${at}:${key}`;
|
|
1942
|
-
const on = this.passes(bands.get(band), weight);
|
|
1943
|
-
bands.set(band, on);
|
|
1944
|
-
if (on)
|
|
1945
|
-
pose[key] = pose[key] === undefined ? copy(value) : channel.merge(pose[key], value);
|
|
1946
|
-
}
|
|
1947
|
-
}
|
|
1948
|
-
return pose;
|
|
1949
|
-
}
|
|
1950
|
-
/** `influence`, or for a dry fold a subject already probed this frame, without advancing it. */
|
|
1951
|
-
read(voice, subject, now, dry, held) {
|
|
1952
|
-
if (dry) {
|
|
1953
|
-
if (held.reaches && held.probed === now && held.delta && held.seeks === voice.seeks) {
|
|
1954
|
-
const setting = voice.setting;
|
|
1955
|
-
setting.timestamp = now;
|
|
1956
|
-
setting.dt = 0;
|
|
1957
|
-
setting.elapsed = voice.elapsedAt(now);
|
|
1958
|
-
setting.pass = 0;
|
|
1959
|
-
setting.weight = 0;
|
|
1960
|
-
setting.state = held.state;
|
|
1961
|
-
setting.keep = held.keep;
|
|
1962
|
-
this.w = this.weigh(voice, subject, now, held);
|
|
1963
|
-
held.weight = this.w;
|
|
1964
|
-
if (voice.holder !== held && voice.scratch.length > 0)
|
|
1965
|
-
this.keyed(voice, subject, held);
|
|
1966
|
-
return held.delta;
|
|
1967
|
-
}
|
|
1968
|
-
}
|
|
1969
|
-
return this.influence(voice, subject, now, held);
|
|
425
|
+
linkedLaned = false;
|
|
426
|
+
/**
|
|
427
|
+
* Per subject, twice `restsKey` at its last probe plus 1 if the pose rested; null until `atRest`
|
|
428
|
+
* is first asked, when probes start writing it.
|
|
429
|
+
*/
|
|
430
|
+
restStamps = null;
|
|
431
|
+
/** What a probe's record of rest is good for: moved by a new frame, version or relink. */
|
|
432
|
+
restsEpoch = 0;
|
|
433
|
+
restsFrame = Number.NaN;
|
|
434
|
+
restsVersion = Number.NaN;
|
|
435
|
+
restsRelinks = -1;
|
|
436
|
+
restsStirs = -1;
|
|
437
|
+
/** The pose `atRest` folds into where no probe this frame answers. */
|
|
438
|
+
restScratch;
|
|
439
|
+
restsLaned() {
|
|
440
|
+
return this.lanes.whole && this.bounded.length === 0;
|
|
441
|
+
}
|
|
442
|
+
restsKey() {
|
|
443
|
+
if (this.frame !== this.restsFrame ||
|
|
444
|
+
this.version !== this.restsVersion ||
|
|
445
|
+
this.relinks !== this.restsRelinks ||
|
|
446
|
+
this.stirs !== this.restsStirs) {
|
|
447
|
+
this.restsEpoch++;
|
|
448
|
+
this.restsFrame = this.frame;
|
|
449
|
+
this.restsVersion = this.version;
|
|
450
|
+
this.restsRelinks = this.relinks;
|
|
451
|
+
this.restsStirs = this.stirs;
|
|
452
|
+
}
|
|
453
|
+
return this.restsEpoch;
|
|
1970
454
|
}
|
|
1971
455
|
}
|
|
456
|
+
// The probe's hot path is methods, installed here from the modules that own them: as functions
|
|
457
|
+
// called directly they made probes and fills up to 9% slower (bench/ab.sh on teitou, 2026-10-05).
|
|
458
|
+
const methods = {
|
|
459
|
+
fold,
|
|
460
|
+
folded,
|
|
461
|
+
linked,
|
|
462
|
+
foldWith,
|
|
463
|
+
owedBy,
|
|
464
|
+
foldLoci,
|
|
465
|
+
read,
|
|
466
|
+
influence,
|
|
467
|
+
keyed,
|
|
468
|
+
tick,
|
|
469
|
+
apply,
|
|
470
|
+
clamp,
|
|
471
|
+
passes,
|
|
472
|
+
isRest,
|
|
473
|
+
baseFor,
|
|
474
|
+
slopeFor,
|
|
475
|
+
prime,
|
|
476
|
+
capped,
|
|
477
|
+
horizonFor,
|
|
478
|
+
weigh,
|
|
479
|
+
weighOwned,
|
|
480
|
+
base,
|
|
481
|
+
envelope: fadeOf,
|
|
482
|
+
ownedBy,
|
|
483
|
+
ownerBase,
|
|
484
|
+
blended,
|
|
485
|
+
laneRead,
|
|
486
|
+
shareKept,
|
|
487
|
+
parting,
|
|
488
|
+
chain,
|
|
489
|
+
linkable,
|
|
490
|
+
aims,
|
|
491
|
+
held,
|
|
492
|
+
unreachedOf,
|
|
493
|
+
sinceOf,
|
|
494
|
+
shownOf,
|
|
495
|
+
keep,
|
|
496
|
+
foldLocus,
|
|
497
|
+
};
|
|
498
|
+
for (const [name, value] of Object.entries(methods))
|
|
499
|
+
Object.defineProperty(Mixer.prototype, name, { value, writable: true, configurable: true });
|
|
1972
500
|
/**
|
|
1973
501
|
* The one that ships: every form on the CPU, probed per subject on demand.
|
|
1974
502
|
*
|