@msb235/blits 0.4.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 +262 -0
- package/README.md +2 -1
- 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 +121 -246
- package/dist/lanes.js +150 -1578
- 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 -196
- package/dist/mixer.js +242 -2220
- package/dist/motion.d.ts +66 -21
- package/dist/motion.js +325 -165
- 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 +217 -34
- 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 +118 -40
- package/package.json +5 -2
package/dist/mixer.js
CHANGED
|
@@ -1,359 +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
|
-
shown: 0,
|
|
25
|
-
weight: 0,
|
|
26
|
-
rested: false,
|
|
27
|
-
bands: new Uint8Array(0),
|
|
28
|
-
state: undefined,
|
|
29
|
-
stepped: 0,
|
|
30
|
-
ticks: 0,
|
|
31
|
-
probed: Number.NaN,
|
|
32
|
-
delta: null,
|
|
33
|
-
phase: 0,
|
|
34
|
-
seeks: 0,
|
|
35
|
-
kept,
|
|
36
|
-
keep: keeper(kept),
|
|
37
|
-
voice: null,
|
|
38
|
-
next: null,
|
|
39
|
-
version: Number.NaN,
|
|
40
|
-
loci: null,
|
|
41
|
-
slot: -1,
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
function elapsedWith(c, now) {
|
|
45
|
-
const dt = now - c.anchorNow;
|
|
46
|
-
const r = c.ramp;
|
|
47
|
-
if (r === null)
|
|
48
|
-
return c.anchorElapsed + dt * c.rate;
|
|
49
|
-
if (dt <= 0)
|
|
50
|
-
return c.anchorElapsed + dt * r.from;
|
|
51
|
-
const d = r.to - r.from;
|
|
52
|
-
if (dt <= r.over)
|
|
53
|
-
return c.anchorElapsed + r.from * dt + (d * dt * dt) / (2 * r.over);
|
|
54
|
-
return c.anchorElapsed + r.from * r.over + (d * r.over) / 2 + r.to * (dt - r.over);
|
|
55
|
-
}
|
|
56
|
-
/**
|
|
57
|
-
* The last entry taken before `t`, or at it where `inclusive` says so, as it does for a change a
|
|
58
|
-
* sync made. A host's change between frames is taken strictly: it shows from the next frame on, as
|
|
59
|
-
* it did live.
|
|
60
|
-
*/
|
|
61
|
-
function last(list, t, inclusive) {
|
|
62
|
-
let lo = 0;
|
|
63
|
-
let hi = list.length - 1;
|
|
64
|
-
let found;
|
|
65
|
-
while (lo <= hi) {
|
|
66
|
-
const mid = (lo + hi) >> 1;
|
|
67
|
-
const e = list[mid];
|
|
68
|
-
if (e.at < t || (e.at === t && (typeof inclusive === 'function' ? inclusive(e) : inclusive))) {
|
|
69
|
-
found = e;
|
|
70
|
-
lo = mid + 1;
|
|
71
|
-
}
|
|
72
|
-
else
|
|
73
|
-
hi = mid - 1;
|
|
74
|
-
}
|
|
75
|
-
return found;
|
|
76
|
-
}
|
|
77
|
-
/** A store that fills a missing entry from `make` on first ask. */
|
|
78
|
-
class Filled extends Store {
|
|
79
|
-
make;
|
|
80
|
-
constructor(make) {
|
|
81
|
-
super();
|
|
82
|
-
this.make = make;
|
|
83
|
-
}
|
|
84
|
-
get(key) {
|
|
85
|
-
let v = super.get(key);
|
|
86
|
-
if (v === undefined) {
|
|
87
|
-
v = this.make(key);
|
|
88
|
-
if (v !== undefined)
|
|
89
|
-
super.set(key, v);
|
|
90
|
-
}
|
|
91
|
-
return v;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
const noSend = () => { };
|
|
95
|
-
const near = (a, b) => {
|
|
96
|
-
if (typeof a === 'number' && typeof b === 'number')
|
|
97
|
-
return Math.abs(a - b) < 1e-9;
|
|
98
|
-
if (Array.isArray(a) && Array.isArray(b))
|
|
99
|
-
return a.length === b.length && a.every((v, i) => near(v, b[i]));
|
|
100
|
-
return a === b;
|
|
101
|
-
};
|
|
102
|
-
const copy = (v) => (Array.isArray(v) ? [...v] : v);
|
|
103
|
-
/** Writes a folded pose's value into subject `n`'s places in a column. */
|
|
104
|
-
function writeValue(c, v, n) {
|
|
105
|
-
const at = n * c.axes;
|
|
106
|
-
if (typeof v === 'number' && c.axes === 1)
|
|
107
|
-
c.out[at] = v;
|
|
108
|
-
else if (Array.isArray(v) && v.length === c.axes)
|
|
109
|
-
for (let a = 0; a < c.axes; a++)
|
|
110
|
-
c.out[at + a] = v[a];
|
|
111
|
-
else if (v === undefined)
|
|
112
|
-
c.out.fill(Number.NaN, at, at + c.axes);
|
|
113
|
-
else
|
|
114
|
-
throw new TypeError(`blits: pull reads ${c.key} as ${c.axes === 1 ? 'a number' : `${c.axes} numbers`} a subject, and the pose has ${String(v)}`);
|
|
115
|
-
}
|
|
116
|
-
/** Deep equality over plain data, for telling whether a host field changed. */
|
|
117
|
-
function same(a, b) {
|
|
118
|
-
if (a === b)
|
|
119
|
-
return true;
|
|
120
|
-
if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null)
|
|
121
|
-
return false;
|
|
122
|
-
const ka = Object.keys(a);
|
|
123
|
-
const kb = Object.keys(b);
|
|
124
|
-
if (ka.length !== kb.length)
|
|
125
|
-
return false;
|
|
126
|
-
for (const k of ka)
|
|
127
|
-
if (!same(a[k], b[k]))
|
|
128
|
-
return false;
|
|
129
|
-
return true;
|
|
130
|
-
}
|
|
131
|
-
export class Voice {
|
|
132
|
-
id;
|
|
133
|
-
spec;
|
|
134
|
-
patch;
|
|
135
|
-
fade;
|
|
136
|
-
start;
|
|
137
|
-
state = 'pending';
|
|
138
|
-
rate;
|
|
139
|
-
weight;
|
|
140
|
-
/**
|
|
141
|
-
* elapsed = anchorElapsed + the rate integrated from anchorNow, rebased on a rate change or a seek.
|
|
142
|
-
* Without a ramp the rate is constant; with one it moves linearly from `from` to `to` over `over`
|
|
143
|
-
* mix ms starting at the anchor, and holds `to` after.
|
|
144
|
-
*/
|
|
145
|
-
anchorNow;
|
|
146
|
-
anchorElapsed = 0;
|
|
147
|
-
ramp = null;
|
|
148
|
-
out = null;
|
|
149
|
-
subjects = new Store();
|
|
150
|
-
/** The subjects its spec names, or null where it names none. */
|
|
151
|
-
named;
|
|
152
|
-
/** Kit slot of each channel the patch writes, in `writes` order. */
|
|
153
|
-
slots;
|
|
154
|
-
/** The stops built ahead of time, for a `keys` patch. */
|
|
155
|
-
built;
|
|
156
|
-
/** The kit's `lerp` for each channel the patch writes, which keyed stops interpolate through. */
|
|
157
|
-
lerps;
|
|
158
|
-
/** Per keyed channel, the in-place form of the lerp its stops take, where it has one. */
|
|
159
|
-
intos;
|
|
160
|
-
/**
|
|
161
|
-
* The arrays keyed reads interpolate into: one set for the voice, not one per subject, since 10k
|
|
162
|
-
* subjects' worth of long-lived arrays costs more in cache misses than allocating them did.
|
|
163
|
-
*/
|
|
164
|
-
scratch = [];
|
|
165
|
-
/** The record whose delta holds `scratch`'s arrays; any other's must be read again to be handed out. */
|
|
166
|
-
holder = null;
|
|
167
|
-
/** Whether its channels run as lanes this frame, so the general fold passes it by. */
|
|
168
|
-
laned = false;
|
|
169
|
-
/** How many times it has been sought, so a delta read before a seek is not handed out after it. */
|
|
170
|
-
seeks = 0;
|
|
171
|
-
/** Whether its patch has kept state on a record through `setting.keep`, which makes it stateful. */
|
|
172
|
-
keeping = false;
|
|
173
|
-
/** For a motion patch, the hook it was given to ask the mix for its subjects' voice time. */
|
|
174
|
-
frame = null;
|
|
175
|
-
/** For a motion patch, the hook it was given to bring a subject faded out of this voice back. */
|
|
176
|
-
revive = null;
|
|
177
|
-
/** Subjects fading out of this voice alone, by the ramp each started; null while none are. */
|
|
178
|
-
parts = null;
|
|
179
|
-
/** Subjects faded out of this voice, by the mix time each left at; null while none have. */
|
|
180
|
-
parted = null;
|
|
181
|
-
ease;
|
|
182
|
-
/** How many passes its `loop` plays, worked out once. */
|
|
183
|
-
passes;
|
|
184
|
-
/** Voice ms its passes last, Infinity for an aperiodic patch or a loop for good. */
|
|
185
|
-
span;
|
|
186
|
-
/** The state behind a motion patch, undefined for any other. */
|
|
187
|
-
motion;
|
|
188
|
-
/** Its `hold`, for a patch it applies to: motion holds its target already. */
|
|
189
|
-
holdsBefore;
|
|
190
|
-
holdsAfter;
|
|
191
|
-
/** The mix time it first showed: when it was cued, or the first sync after. */
|
|
192
|
-
opened = Number.NaN;
|
|
193
|
-
/** Which of its entries in the mix's due queue is current; older ones are skipped when popped. */
|
|
194
|
-
dueToken = 0;
|
|
195
|
-
/** Records made while it was pending, whose `since` its start may since have moved. */
|
|
196
|
-
early = null;
|
|
197
|
-
/** Reused for every call this voice makes, so it is valid only during the call. */
|
|
198
|
-
setting;
|
|
199
|
-
/** Every subject this voice has been asked about, so a handover knows when it is finished. */
|
|
200
|
-
seen = 0;
|
|
201
|
-
/** The longest stagger of any subject seen, so a finite loop waits for the last of them. */
|
|
202
|
-
latest = 0;
|
|
203
|
-
restedCount = 0;
|
|
204
|
-
/** The mix time it was cued at; -Infinity before the first sync. */
|
|
205
|
-
cuedAt = Number.NEGATIVE_INFINITY;
|
|
206
|
-
/** The mix time it left at. */
|
|
207
|
-
doneAt = Number.POSITIVE_INFINITY;
|
|
208
|
-
/** Under `history`, its controls after each change, oldest first. */
|
|
209
|
-
log = null;
|
|
210
|
-
/** Where an anchored `out` or `end` puts its fade's start, mix time; Infinity until known. */
|
|
211
|
-
outAt = Number.POSITIVE_INFINITY;
|
|
212
|
-
/** Whether its start is still to be fixed by an anchor, so it waits pending. */
|
|
213
|
-
placing = false;
|
|
214
|
-
resolve;
|
|
215
|
-
done;
|
|
216
|
-
play;
|
|
217
|
-
played;
|
|
218
|
-
constructor(id, spec, patch, fade, now, start, slotOf, channels, host, send) {
|
|
219
|
-
this.id = id;
|
|
220
|
-
this.spec = spec;
|
|
221
|
-
this.patch = patch;
|
|
222
|
-
this.fade = fade;
|
|
223
|
-
this.start = start;
|
|
224
|
-
this.slots = patch.writes.map((k) => slotOf.get(k));
|
|
225
|
-
this.named = spec.subjects ? new Set(spec.subjects) : null;
|
|
226
|
-
this.built = patch.form === 'keys' && patch.keys ? builtOf(patch) : null;
|
|
227
|
-
this.lerps = this.slots.map((slot) => channels[slot].lerp);
|
|
228
|
-
this.intos = this.built
|
|
229
|
-
? intosOf(this.built, this.slots.map((slot) => channels[slot]))
|
|
230
|
-
: undefined;
|
|
231
|
-
this.ease = fade.ease === undefined ? undefined : curve(fade.ease);
|
|
232
|
-
this.passes = passesOf(spec.loop);
|
|
233
|
-
this.span =
|
|
234
|
-
patch.period > 0 && Number.isFinite(this.passes)
|
|
235
|
-
? patch.period * this.passes
|
|
236
|
-
: Number.POSITIVE_INFINITY;
|
|
237
|
-
this.motion = motionOf(patch);
|
|
238
|
-
const holds = this.motion === undefined ? spec.hold : undefined;
|
|
239
|
-
this.holdsBefore = holds === 'before' || holds === 'both';
|
|
240
|
-
this.holdsAfter = holds === 'after' || holds === 'both';
|
|
241
|
-
this.setting = {
|
|
242
|
-
timestamp: 0,
|
|
243
|
-
dt: 0,
|
|
244
|
-
elapsed: 0,
|
|
245
|
-
pass: 0,
|
|
246
|
-
weight: 0,
|
|
247
|
-
state: undefined,
|
|
248
|
-
host,
|
|
249
|
-
keep: keeper(new Map()),
|
|
250
|
-
send,
|
|
251
|
-
};
|
|
252
|
-
this.rate = spec.rate ?? 1;
|
|
253
|
-
this.weight = typeof spec.weight === 'number' ? spec.weight : 1;
|
|
254
|
-
this.anchorNow = start;
|
|
255
|
-
this.done = new Promise((r) => {
|
|
256
|
-
this.resolve = r;
|
|
257
|
-
});
|
|
258
|
-
this.played = new Promise((r) => {
|
|
259
|
-
this.play = r;
|
|
260
|
-
});
|
|
261
|
-
if (now >= start)
|
|
262
|
-
this.state = 'live';
|
|
263
|
-
}
|
|
264
|
-
elapsedAt(now) {
|
|
265
|
-
return elapsedWith(this, now);
|
|
266
|
-
}
|
|
267
|
-
/** The mix time its clock reads `elapsed`, inverting `elapsedAt`; Infinity where it never will. */
|
|
268
|
-
timeAt(elapsed) {
|
|
269
|
-
const e0 = this.anchorElapsed;
|
|
270
|
-
const r = this.ramp;
|
|
271
|
-
if (r === null) {
|
|
272
|
-
if (this.rate > 0)
|
|
273
|
-
return this.anchorNow + (elapsed - e0) / this.rate;
|
|
274
|
-
return elapsed <= e0 ? this.anchorNow : Number.POSITIVE_INFINITY;
|
|
275
|
-
}
|
|
276
|
-
if (elapsed <= e0)
|
|
277
|
-
return r.from > 0 ? this.anchorNow + (elapsed - e0) / r.from : this.anchorNow;
|
|
278
|
-
const a = (r.to - r.from) / (2 * r.over);
|
|
279
|
-
const atEnd = e0 + r.from * r.over + a * r.over * r.over;
|
|
280
|
-
if (elapsed <= atEnd) {
|
|
281
|
-
const c = e0 - elapsed;
|
|
282
|
-
const dt = Math.abs(a) < 1e-12
|
|
283
|
-
? -c / r.from
|
|
284
|
-
: (-r.from + Math.sqrt(Math.max(0, r.from * r.from - 4 * a * c))) / (2 * a);
|
|
285
|
-
return this.anchorNow + dt;
|
|
286
|
-
}
|
|
287
|
-
if (r.to <= 0)
|
|
288
|
-
return Number.POSITIVE_INFINITY;
|
|
289
|
-
return this.anchorNow + r.over + (elapsed - atEnd) / r.to;
|
|
290
|
-
}
|
|
291
|
-
/** Records this voice's controls as they stand, from mix time `at`. */
|
|
292
|
-
note(at, sync = false) {
|
|
293
|
-
this.log?.push({
|
|
294
|
-
at,
|
|
295
|
-
sync,
|
|
296
|
-
anchorNow: this.anchorNow,
|
|
297
|
-
anchorElapsed: this.anchorElapsed,
|
|
298
|
-
rate: this.rate,
|
|
299
|
-
ramp: this.ramp,
|
|
300
|
-
weight: this.weight,
|
|
301
|
-
out: this.out,
|
|
302
|
-
start: this.start,
|
|
303
|
-
outAt: this.outAt,
|
|
304
|
-
});
|
|
305
|
-
}
|
|
306
|
-
/**
|
|
307
|
-
* A copy for a projection: its own setting, which sends nothing, and its own per-subject records,
|
|
308
|
-
* filled from `fill`. With `controls` it takes those, as the voice stood at an earlier time.
|
|
309
|
-
*/
|
|
310
|
-
copy(fill, controls) {
|
|
311
|
-
const v = Object.assign(Object.create(Voice.prototype), this);
|
|
312
|
-
const w = v;
|
|
313
|
-
w.subjects = new Filled(fill);
|
|
314
|
-
w.setting = { ...this.setting, keep: keeper(new Map()), send: noSend };
|
|
315
|
-
v.scratch = [];
|
|
316
|
-
v.holder = null;
|
|
317
|
-
v.laned = false;
|
|
318
|
-
v.log = null;
|
|
319
|
-
v.early = null;
|
|
320
|
-
if (controls) {
|
|
321
|
-
v.anchorNow = controls.anchorNow;
|
|
322
|
-
v.anchorElapsed = controls.anchorElapsed;
|
|
323
|
-
v.rate = controls.rate;
|
|
324
|
-
v.ramp = controls.ramp;
|
|
325
|
-
v.weight = controls.weight;
|
|
326
|
-
v.out = controls.out;
|
|
327
|
-
v.start = controls.start;
|
|
328
|
-
v.outAt = controls.outAt;
|
|
329
|
-
}
|
|
330
|
-
v.resolve = noSend;
|
|
331
|
-
v.play = noSend;
|
|
332
|
-
return v;
|
|
333
|
-
}
|
|
334
|
-
rateAt(now) {
|
|
335
|
-
const r = this.ramp;
|
|
336
|
-
if (r === null)
|
|
337
|
-
return this.rate;
|
|
338
|
-
const u = (now - this.anchorNow) / r.over;
|
|
339
|
-
return u >= 1 ? r.to : u <= 0 ? r.from : r.from + (r.to - r.from) * u;
|
|
340
|
-
}
|
|
341
|
-
/** Moves the anchor to `now`, carrying what is left of a ramp. */
|
|
342
|
-
rebase(now) {
|
|
343
|
-
const rate = this.rateAt(now);
|
|
344
|
-
this.anchorElapsed = this.elapsedAt(now);
|
|
345
|
-
const r = this.ramp;
|
|
346
|
-
if (r !== null) {
|
|
347
|
-
const left = this.anchorNow + r.over - now;
|
|
348
|
-
this.ramp = left > 0 ? { from: rate, to: r.to, over: left } : null;
|
|
349
|
-
}
|
|
350
|
-
this.anchorNow = now;
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
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 {
|
|
354
24
|
kit;
|
|
355
25
|
opts;
|
|
356
|
-
|
|
26
|
+
cued = [];
|
|
357
27
|
/** Under `history`, voices that have left but that a read back may still reach. */
|
|
358
28
|
gone = [];
|
|
359
29
|
/** Set on a projection's own mixer: it sends nothing, keeps no history, and reads without committing. */
|
|
@@ -368,17 +38,28 @@ class Mixer {
|
|
|
368
38
|
*/
|
|
369
39
|
hostLog = [];
|
|
370
40
|
/**
|
|
371
|
-
* Marks the host announced on a score:
|
|
372
|
-
* 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.
|
|
373
44
|
*/
|
|
374
45
|
announced = [];
|
|
375
46
|
hostThen;
|
|
376
47
|
/** Each subject's last two poses and when they were probed, for `from: 'current'`. */
|
|
377
48
|
pose = new Store();
|
|
378
49
|
nextId = 1;
|
|
379
|
-
/**
|
|
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
|
+
*/
|
|
380
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();
|
|
381
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;
|
|
382
63
|
last = Number.NaN;
|
|
383
64
|
rebasing = false;
|
|
384
65
|
wantsPose = false;
|
|
@@ -387,8 +68,8 @@ class Mixer {
|
|
|
387
68
|
/** The last array `pull` read, by position, with each subject's chain head, to skip the lookup. */
|
|
388
69
|
/** Live voices by the earliest mix time `moveTo` would change each, a binary heap. */
|
|
389
70
|
due = [];
|
|
390
|
-
/** Voices retired since `moveTo` last pruned them. */
|
|
391
|
-
retired =
|
|
71
|
+
/** Voices retired since `moveTo` last pruned them, in the order they retired. */
|
|
72
|
+
retired = [];
|
|
392
73
|
/** Visit every voice each sync, as before the due queue; for tests that compare the two. */
|
|
393
74
|
walkAll = false;
|
|
394
75
|
pulled = [];
|
|
@@ -403,6 +84,13 @@ class Mixer {
|
|
|
403
84
|
reducedNow = false;
|
|
404
85
|
/** Something was changed since the last sync, so the next frame may differ from this one. */
|
|
405
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
|
+
}
|
|
406
94
|
/**
|
|
407
95
|
* Per subject, the first record of the chain through every live voice that reaches it, or a stub
|
|
408
96
|
* where none does. Relinked when `version` moves, which is whenever the list or a voice's pending
|
|
@@ -410,6 +98,7 @@ class Mixer {
|
|
|
410
98
|
*/
|
|
411
99
|
chains = new Store();
|
|
412
100
|
version = 0;
|
|
101
|
+
steps = new Steps();
|
|
413
102
|
/** Per subject, the voices whose `subjects` name it, in voice order. */
|
|
414
103
|
named = new Store();
|
|
415
104
|
/** Voices a subject has been faded out of, which `drop` looks in besides those that reach it. */
|
|
@@ -429,6 +118,8 @@ class Mixer {
|
|
|
429
118
|
general = [];
|
|
430
119
|
/** How many voices in the list carry a locus; with none, a fold allocates nothing. */
|
|
431
120
|
loci = 0;
|
|
121
|
+
/** What every motion patch it plays asks for its voice's time, holding this mix weakly. */
|
|
122
|
+
owner = null;
|
|
432
123
|
/** How many voices in the list are anchored, so a sync with none skips placing them. */
|
|
433
124
|
anchored = 0;
|
|
434
125
|
/** Events sent since the last drain, and who is being probed, so `send` knows whose they are. */
|
|
@@ -455,18 +146,29 @@ class Mixer {
|
|
|
455
146
|
channels;
|
|
456
147
|
/** Slots of the channels that declare bounds, clamped after every fold. */
|
|
457
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;
|
|
458
154
|
slotOf = new Map();
|
|
459
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;
|
|
460
161
|
constructor(kit, opts) {
|
|
461
162
|
this.kit = kit;
|
|
462
163
|
this.opts = opts;
|
|
463
164
|
this.names = Object.keys(kit);
|
|
464
165
|
this.channels = this.names.map((k) => kit[k]);
|
|
465
166
|
this.bounded = this.channels.flatMap((c, i) => (c.bounds ? [i] : []));
|
|
167
|
+
this.lerpsInto = this.channels.map((c) => lerpInto(c));
|
|
466
168
|
this.names.forEach((k, i) => {
|
|
467
169
|
this.slotOf.set(k, i);
|
|
468
170
|
});
|
|
469
|
-
this.lanes = opts.lanes === false ? null : new Lanes(
|
|
171
|
+
this.lanes = opts.lanes === false ? null : new Lanes(laneHost(this));
|
|
470
172
|
}
|
|
471
173
|
get reduced() {
|
|
472
174
|
const r = this.opts.reduce;
|
|
@@ -476,536 +178,71 @@ class Mixer {
|
|
|
476
178
|
return this.opts.band ?? { on: 0.6, off: 0.4 };
|
|
477
179
|
}
|
|
478
180
|
cue(spec) {
|
|
479
|
-
const patch = spec.patch;
|
|
480
181
|
const engine = this.opts.engine ?? mixer;
|
|
481
|
-
if (!engine.runs.has(patch.form))
|
|
482
|
-
throw new Error(`blits: engine ${engine.name} does not run ${patch.form} patches`);
|
|
483
|
-
|
|
484
|
-
if (!(channel in this.kit))
|
|
485
|
-
throw new Error(`blits: kit has no channel ${String(channel)}, which this patch writes`);
|
|
486
|
-
const wanted = patch.kit?.[channel];
|
|
487
|
-
const here = this.kit[channel];
|
|
488
|
-
if (wanted && wanted !== here && (wanted.kind === undefined || wanted.kind !== here.kind))
|
|
489
|
-
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'}`);
|
|
490
|
-
}
|
|
491
|
-
if (patch.reads) {
|
|
492
|
-
const host = this.opts.host;
|
|
493
|
-
for (const field of patch.reads)
|
|
494
|
-
if (typeof host !== 'object' || host === null || !(field in host))
|
|
495
|
-
throw new Error(`blits: the patch reads host.${field}, which this mix's host lacks`);
|
|
496
|
-
}
|
|
497
|
-
if (spec.from === 'current' && patch.form !== 'keys')
|
|
498
|
-
throw new Error("blits: from: 'current' needs a keys patch");
|
|
499
|
-
if (spec.subjects !== undefined && spec.target !== undefined)
|
|
500
|
-
throw new Error('blits: a voice takes target or subjects, not both');
|
|
501
|
-
const anchor = spec.anchor;
|
|
502
|
-
if (anchor)
|
|
503
|
-
this.checkPlacement(spec, anchor);
|
|
504
|
-
const anchored = anchor !== undefined && (anchor.start !== undefined || anchor.in !== undefined);
|
|
505
|
-
const start = anchored
|
|
506
|
-
? Number.POSITIVE_INFINITY
|
|
507
|
-
: spec.start !== undefined
|
|
508
|
-
? spec.start - this.offset
|
|
509
|
-
: Number.isNaN(this.now)
|
|
510
|
-
? 0
|
|
511
|
-
: this.now;
|
|
512
|
-
const voice = new Voice(this.nextId++, spec, patch, spec.fade ?? {}, this.now, start, this.slotOf, this.channels, this.opts.host, this.send);
|
|
513
|
-
if (!Number.isNaN(this.now)) {
|
|
514
|
-
voice.cuedAt = this.now;
|
|
515
|
-
voice.opened = this.now;
|
|
516
|
-
}
|
|
517
|
-
voice.placing = anchored;
|
|
518
|
-
const motion = voice.motion;
|
|
519
|
-
if (motion !== undefined) {
|
|
520
|
-
this.playing.set(motion, (this.playing.get(motion) ?? 0) + 1);
|
|
521
|
-
voice.frame = Mixer.frameHook(new WeakRef(this), new WeakRef(voice));
|
|
522
|
-
motion.frame = voice.frame;
|
|
523
|
-
voice.revive = Mixer.reviveHook(new WeakRef(this), new WeakRef(voice));
|
|
524
|
-
motion.revive = voice.revive;
|
|
525
|
-
}
|
|
526
|
-
this.voices.push(voice);
|
|
527
|
-
this.index(voice);
|
|
528
|
-
this.changed(voice);
|
|
529
|
-
this.stirred = true;
|
|
530
|
-
if (spec.locus !== undefined)
|
|
531
|
-
this.loci++;
|
|
532
|
-
if (spec.from === 'current')
|
|
533
|
-
this.wantsPose = true;
|
|
534
|
-
if (anchor) {
|
|
535
|
-
this.anchored++;
|
|
536
|
-
this.place();
|
|
537
|
-
if (!Number.isNaN(this.now) && voice.state === 'pending' && this.now >= voice.start) {
|
|
538
|
-
voice.state = 'live';
|
|
539
|
-
this.started(voice);
|
|
540
|
-
this.changed(voice);
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
// After placing, so the controls it starts with are where its anchors put it at the cue.
|
|
544
|
-
if (this.opts.history) {
|
|
545
|
-
voice.log = [];
|
|
546
|
-
voice.note(Number.NEGATIVE_INFINITY);
|
|
547
|
-
}
|
|
548
|
-
this.schedule(voice);
|
|
549
|
-
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);
|
|
550
185
|
}
|
|
551
186
|
blend(patches, by, spec = {}) {
|
|
552
187
|
const locus = `blend:${this.nextId}`;
|
|
553
|
-
const
|
|
188
|
+
const of = blendOf(by, patches.length - 1);
|
|
554
189
|
return patches.map((patch, i) => {
|
|
555
|
-
const
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
const weight = by.input ? Object.assign(share, { input: true }) : share;
|
|
561
|
-
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;
|
|
562
195
|
});
|
|
563
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
|
+
}
|
|
564
202
|
sync(timestamp) {
|
|
565
203
|
if (this.rebasing && !Number.isNaN(this.last))
|
|
566
204
|
this.offset += timestamp - this.last;
|
|
567
205
|
this.rebasing = false;
|
|
568
206
|
this.last = timestamp;
|
|
569
|
-
const
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
this.
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
this.
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
this.
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
moveTo(now) {
|
|
585
|
-
this.now = now;
|
|
586
|
-
this.reducedNow = this.reduced;
|
|
587
|
-
for (const a of this.announced)
|
|
588
|
-
if (Number.isNaN(a.at))
|
|
589
|
-
a.at = now;
|
|
590
|
-
if (this.anchored > 0)
|
|
591
|
-
this.place();
|
|
592
|
-
if (this.announced.length > 0) {
|
|
593
|
-
// Kept while still ahead, or while history reaches it; anchors waiting on one were placed above.
|
|
594
|
-
const reach = now - (this.opts.history?.ms ?? 0);
|
|
595
|
-
this.announced = this.announced.filter((a) => a.at >= reach);
|
|
596
|
-
}
|
|
597
|
-
// A live mix visits only the voices due by now; a projection, which copies few, visits all.
|
|
598
|
-
const visit = this.projecting || this.walkAll ? this.voices : this.popDue(now);
|
|
599
|
-
for (const voice of visit) {
|
|
600
|
-
if (voice.state === 'done')
|
|
601
|
-
continue;
|
|
602
|
-
if (Number.isNaN(voice.opened))
|
|
603
|
-
voice.opened = now;
|
|
604
|
-
if ((voice.state === 'live' || voice.state === 'held') && voice.outAt <= now)
|
|
605
|
-
this.beginFade(voice, {}, Math.max(voice.start, voice.outAt));
|
|
606
|
-
if (voice.state === 'pending' && now >= voice.start) {
|
|
607
|
-
voice.state = 'live';
|
|
608
|
-
this.started(voice);
|
|
609
|
-
this.changed(voice);
|
|
610
|
-
}
|
|
611
|
-
if (voice.state !== 'pending' && Number.isFinite(voice.span)) {
|
|
612
|
-
const end = voice.span + voice.latest;
|
|
613
|
-
if (voice.elapsedAt(now) >= end) {
|
|
614
|
-
voice.play(true);
|
|
615
|
-
// The fade starts when the last pass ended, not at the frame that noticed, so it plays
|
|
616
|
-
// the same at any frame rate and a read at another time can find it.
|
|
617
|
-
if (voice.state === 'live') {
|
|
618
|
-
if (voice.holdsAfter)
|
|
619
|
-
voice.state = 'held';
|
|
620
|
-
else
|
|
621
|
-
this.beginFade(voice, {}, Math.max(voice.start, Math.min(now, voice.timeAt(end))));
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
else if (voice.state === 'held')
|
|
625
|
-
voice.state = 'live';
|
|
626
|
-
}
|
|
627
|
-
// A projection reads a finished ramp as weight 0, and leaves the live voice's subjects be.
|
|
628
|
-
if (voice.parts !== null && !this.projecting)
|
|
629
|
-
for (const [subject, r] of voice.parts)
|
|
630
|
-
if (now - r.at >= r.over)
|
|
631
|
-
this.part(voice, subject, r.at + r.over);
|
|
632
|
-
if (voice.state === 'fading' && voice.out) {
|
|
633
|
-
const { at, over, rest, deadline } = voice.out;
|
|
634
|
-
const spent = now - at;
|
|
635
|
-
if (rest) {
|
|
636
|
-
const out = deadline !== undefined && spent >= deadline;
|
|
637
|
-
const settled = voice.seen > 0 && voice.restedCount >= voice.seen;
|
|
638
|
-
if (out)
|
|
639
|
-
this.retire(voice, at + deadline);
|
|
640
|
-
else if (settled)
|
|
641
|
-
this.retire(voice);
|
|
642
|
-
}
|
|
643
|
-
else if (spent >= over)
|
|
644
|
-
this.retire(voice, at + over);
|
|
645
|
-
}
|
|
646
|
-
if (!this.projecting)
|
|
647
|
-
this.schedule(voice);
|
|
648
|
-
}
|
|
649
|
-
this.stirred = false;
|
|
650
|
-
if (this.retired === 0) {
|
|
651
|
-
this.forget(now);
|
|
652
|
-
return;
|
|
653
|
-
}
|
|
654
|
-
this.retired = 0;
|
|
655
|
-
let pruned = false;
|
|
656
|
-
let kept = 0;
|
|
657
|
-
const voices = this.voices;
|
|
658
|
-
for (let i = 0; i < voices.length; i++) {
|
|
659
|
-
const voice = voices[i];
|
|
660
|
-
if (voice.state !== 'done') {
|
|
661
|
-
voices[kept++] = voice;
|
|
662
|
-
continue;
|
|
663
|
-
}
|
|
664
|
-
if (this.opts.history)
|
|
665
|
-
this.gone.push(voice);
|
|
666
|
-
else
|
|
667
|
-
this.parters.delete(voice);
|
|
668
|
-
this.unplay(voice);
|
|
669
|
-
this.changed(voice);
|
|
670
|
-
if (voice.named === null)
|
|
671
|
-
pruned = true;
|
|
672
|
-
else
|
|
673
|
-
this.unindex(voice);
|
|
674
|
-
if (voice.spec.locus !== undefined)
|
|
675
|
-
this.loci--;
|
|
676
|
-
if (voice.spec.anchor !== undefined)
|
|
677
|
-
this.anchored--;
|
|
678
|
-
}
|
|
679
|
-
voices.length = kept;
|
|
680
|
-
if (pruned)
|
|
681
|
-
this.general = this.general.filter((v) => v.state !== 'done');
|
|
682
|
-
this.forget(now);
|
|
683
|
-
}
|
|
684
|
-
/** Lets go of the voices that have left and that history no longer reaches. */
|
|
685
|
-
forget(now) {
|
|
686
|
-
const history = this.opts.history;
|
|
687
|
-
if (history && this.gone.length > 0) {
|
|
688
|
-
const reach = now - history.ms;
|
|
689
|
-
this.gone = this.gone.filter((v) => v.doneAt >= reach);
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
/**
|
|
693
|
-
* The earliest mix time `moveTo` would change a voice: its start, its anchored out, the end of
|
|
694
|
-
* its last pass, the end of its fade or of a subject's ramp out; -Infinity where it must be
|
|
695
|
-
* visited every frame, Infinity where nothing will happen until a handle or anchor changes it.
|
|
696
|
-
* Early is safe, since a visit that finds nothing due schedules again; late is not, so anything
|
|
697
|
-
* that can bring it earlier calls `schedule`.
|
|
698
|
-
*/
|
|
699
|
-
dueOf(voice) {
|
|
700
|
-
if (voice.state === 'done')
|
|
701
|
-
return Number.POSITIVE_INFINITY;
|
|
702
|
-
if (Number.isNaN(voice.opened))
|
|
703
|
-
return Number.NEGATIVE_INFINITY;
|
|
704
|
-
let due = Number.POSITIVE_INFINITY;
|
|
705
|
-
if (voice.parts !== null)
|
|
706
|
-
for (const r of voice.parts.values())
|
|
707
|
-
due = Math.min(due, r.at + r.over);
|
|
708
|
-
if (voice.state === 'pending')
|
|
709
|
-
return Math.min(due, Number.isNaN(voice.start) ? due : voice.start);
|
|
710
|
-
if (voice.state === 'fading') {
|
|
711
|
-
const out = voice.out;
|
|
712
|
-
if (out === null || out.rest)
|
|
713
|
-
return Number.NEGATIVE_INFINITY;
|
|
714
|
-
return Math.min(due, out.at + out.over);
|
|
715
|
-
}
|
|
716
|
-
due = Math.min(due, voice.outAt);
|
|
717
|
-
if (Number.isFinite(voice.span)) {
|
|
718
|
-
const end = voice.span + voice.latest;
|
|
719
|
-
// A held voice goes live again once its clock is back before its end: by a seek, by a
|
|
720
|
-
// subject staggered later than any before, or by running backwards, which is checked each
|
|
721
|
-
// frame while it can.
|
|
722
|
-
if (voice.state === 'held') {
|
|
723
|
-
if (voice.rate <= 0 || voice.ramp !== null)
|
|
724
|
-
return Number.NEGATIVE_INFINITY;
|
|
725
|
-
if (!Number.isNaN(this.now) && voice.elapsedAt(this.now) < end)
|
|
726
|
-
return Number.NEGATIVE_INFINITY;
|
|
727
|
-
}
|
|
728
|
-
if (voice.state === 'live')
|
|
729
|
-
due = Math.min(due, voice.timeAt(end));
|
|
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();
|
|
730
220
|
}
|
|
731
|
-
|
|
221
|
+
const bookers = this.bookers;
|
|
222
|
+
if (bookers !== null)
|
|
223
|
+
for (const b of bookers)
|
|
224
|
+
b.sync();
|
|
732
225
|
}
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
if (this.projecting)
|
|
736
|
-
return;
|
|
737
|
-
this.lanes?.voiceChanged(voice.id);
|
|
738
|
-
const token = ++voice.dueToken;
|
|
739
|
-
const due = this.dueOf(voice);
|
|
740
|
-
if (due === Number.POSITIVE_INFINITY)
|
|
741
|
-
return;
|
|
742
|
-
const heap = this.due;
|
|
743
|
-
heap.push({ at: Number.isNaN(due) ? Number.NEGATIVE_INFINITY : due, voice, token });
|
|
744
|
-
let i = heap.length - 1;
|
|
745
|
-
while (i > 0) {
|
|
746
|
-
const up = (i - 1) >> 1;
|
|
747
|
-
if (heap[up].at <= heap[i].at)
|
|
748
|
-
break;
|
|
749
|
-
[heap[up], heap[i]] = [heap[i], heap[up]];
|
|
750
|
-
i = up;
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
/** Takes every current entry due by `now` off the queue, in cue order. */
|
|
754
|
-
popDue(now) {
|
|
755
|
-
const heap = this.due;
|
|
756
|
-
const at = (k) => k < heap.length ? heap[k].at : Number.POSITIVE_INFINITY;
|
|
757
|
-
const out = [];
|
|
758
|
-
while (heap.length > 0 && heap[0].at <= now) {
|
|
759
|
-
const top = heap[0];
|
|
760
|
-
const last = heap.pop();
|
|
761
|
-
if (heap.length > 0) {
|
|
762
|
-
heap[0] = last;
|
|
763
|
-
let i = 0;
|
|
764
|
-
for (;;) {
|
|
765
|
-
const l = 2 * i + 1;
|
|
766
|
-
const r = l + 1;
|
|
767
|
-
const m0 = at(l) < at(i) ? l : i;
|
|
768
|
-
const m = at(r) < at(m0) ? r : m0;
|
|
769
|
-
if (m === i)
|
|
770
|
-
break;
|
|
771
|
-
[heap[m], heap[i]] = [heap[i], heap[m]];
|
|
772
|
-
i = m;
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
|
-
if (top.token === top.voice.dueToken && top.voice.state !== 'done') {
|
|
776
|
-
top.voice.dueToken++;
|
|
777
|
-
out.push(top.voice);
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
if (out.length > 1)
|
|
781
|
-
out.sort((a, b) => a.id - b.id);
|
|
782
|
-
return out;
|
|
226
|
+
book(opts) {
|
|
227
|
+
return book(this, opts);
|
|
783
228
|
}
|
|
784
229
|
probe(subject, out) {
|
|
785
230
|
const pose = this.fold(subject, out);
|
|
786
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));
|
|
787
235
|
return pose;
|
|
788
236
|
}
|
|
789
|
-
/** After a probe, keeps the pose a retarget from `'current'` reads as the subject's last. */
|
|
790
|
-
keep(subject, pose, out) {
|
|
791
|
-
// Keeping last frame's pose is free when the mix allocated it; a host sampling into its own
|
|
792
|
-
// object only pays for the copy once something in the mix has asked to retarget from it.
|
|
793
|
-
let kept;
|
|
794
|
-
if (out === undefined)
|
|
795
|
-
kept = pose;
|
|
796
|
-
else if (this.wantsPose) {
|
|
797
|
-
kept = {};
|
|
798
|
-
for (const key of this.names) {
|
|
799
|
-
const v = pose[key];
|
|
800
|
-
if (v !== undefined)
|
|
801
|
-
kept[key] = copy(v);
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
|
-
if (kept !== undefined) {
|
|
805
|
-
const rec = this.pose.get(subject);
|
|
806
|
-
if (rec === undefined)
|
|
807
|
-
this.pose.set(subject, { pose: kept, at: this.now, prev: undefined, prevAt: Number.NaN });
|
|
808
|
-
else if (rec.at === this.now)
|
|
809
|
-
rec.pose = kept;
|
|
810
|
-
else {
|
|
811
|
-
rec.prev = rec.pose;
|
|
812
|
-
rec.prevAt = rec.at;
|
|
813
|
-
rec.pose = kept;
|
|
814
|
-
rec.at = this.now;
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
237
|
pull(subjects, into) {
|
|
819
|
-
|
|
820
|
-
const lanes = this.lanes;
|
|
821
|
-
const now = this.now;
|
|
822
|
-
if (this.scratch === undefined)
|
|
823
|
-
this.scratch = {};
|
|
824
|
-
const scratch = this.scratch;
|
|
825
|
-
let room = Number.POSITIVE_INFINITY;
|
|
826
|
-
let tightest = '';
|
|
827
|
-
for (const c of columns) {
|
|
828
|
-
const fits = Math.floor(c.out.length / c.axes);
|
|
829
|
-
if (fits < room) {
|
|
830
|
-
room = fits;
|
|
831
|
-
tightest = c.key;
|
|
832
|
-
}
|
|
833
|
-
}
|
|
834
|
-
// Any other iterable is read into an array first, no further than one past the room there is.
|
|
835
|
-
let list;
|
|
836
|
-
if (Array.isArray(subjects))
|
|
837
|
-
list = subjects;
|
|
838
|
-
else {
|
|
839
|
-
const taken = [];
|
|
840
|
-
for (const subject of subjects)
|
|
841
|
-
if (taken.push(subject) > room)
|
|
842
|
-
break;
|
|
843
|
-
list = taken;
|
|
844
|
-
}
|
|
845
|
-
if (list.length > room)
|
|
846
|
-
throw new RangeError(`blits: pull's array for ${tightest} has room for ${room} subjects, and was given more`);
|
|
847
|
-
const whole = lanes !== null && !this.wantsPose;
|
|
848
|
-
// An array read again in the same order reuses each position's chain head while it is current:
|
|
849
|
-
// all of them at once while nothing has relinked since the last pull, else one by one.
|
|
850
|
-
const was = this.pulled;
|
|
851
|
-
const heads = this.pulledHeads;
|
|
852
|
-
was.length = list.length;
|
|
853
|
-
heads.length = list.length;
|
|
854
|
-
if (this.pulledSlots.length < list.length) {
|
|
855
|
-
const slots = new Int32Array(Math.max(64, list.length));
|
|
856
|
-
slots.set(this.pulledSlots);
|
|
857
|
-
this.pulledSlots = slots;
|
|
858
|
-
}
|
|
859
|
-
const slots = this.pulledSlots;
|
|
860
|
-
const version = this.version;
|
|
861
|
-
const relinks = this.relinks;
|
|
862
|
-
const live = !Number.isNaN(now);
|
|
863
|
-
const current = live && this.pulledVersion === version && this.pulledRelinks === relinks;
|
|
864
|
-
// Every subject read from the lanes, at the positions it had last time, goes in one run.
|
|
865
|
-
const runs = live && whole;
|
|
866
|
-
try {
|
|
867
|
-
for (let n = 0; n < list.length; n++) {
|
|
868
|
-
if (runs) {
|
|
869
|
-
n = lanes.pullRun(slots, list, was, current ? null : heads, n, columns, now, this.version);
|
|
870
|
-
if (n === list.length)
|
|
871
|
-
break;
|
|
872
|
-
}
|
|
873
|
-
const subject = list[n];
|
|
874
|
-
let slot = -1;
|
|
875
|
-
if (live) {
|
|
876
|
-
if (current && was[n] === subject)
|
|
877
|
-
slot = slots[n];
|
|
878
|
-
else {
|
|
879
|
-
const kept = heads[n];
|
|
880
|
-
const head = was[n] === subject && kept !== undefined && kept.version === version
|
|
881
|
-
? kept
|
|
882
|
-
: this.chain(subject, now);
|
|
883
|
-
was[n] = subject;
|
|
884
|
-
heads[n] = head;
|
|
885
|
-
slot = head.slot;
|
|
886
|
-
slots[n] = slot;
|
|
887
|
-
}
|
|
888
|
-
}
|
|
889
|
-
const laned = live && lanes?.prepare(slot, subject, now, this.version) === true;
|
|
890
|
-
if (laned && whole && lanes.whole) {
|
|
891
|
-
lanes.writeLater(slot, columns, n);
|
|
892
|
-
continue;
|
|
893
|
-
}
|
|
894
|
-
const head = live ? heads[n] : null;
|
|
895
|
-
const folded = this.foldWith(subject, scratch, head, laned, false);
|
|
896
|
-
const pose = (this.bounded.length === 0 ? folded : this.clamp(folded));
|
|
897
|
-
this.keep(subject, pose, scratch);
|
|
898
|
-
for (const c of columns)
|
|
899
|
-
writeValue(c, pose[c.key], n);
|
|
900
|
-
}
|
|
901
|
-
}
|
|
902
|
-
finally {
|
|
903
|
-
lanes?.flush();
|
|
904
|
-
}
|
|
905
|
-
// Every head is current only if nothing relinked or changed version while they were read.
|
|
906
|
-
const unchanged = live && this.version === version && this.relinks === relinks;
|
|
907
|
-
this.pulledVersion = unchanged ? version : Number.NaN;
|
|
908
|
-
this.pulledRelinks = relinks;
|
|
909
|
-
}
|
|
910
|
-
/** The kit slot, width, rest and bounds of every channel `pull` was handed an array for. */
|
|
911
|
-
columnsOf(into) {
|
|
912
|
-
const columns = [];
|
|
913
|
-
for (const key of Object.keys(into)) {
|
|
914
|
-
const out = into[key];
|
|
915
|
-
if (out === undefined)
|
|
916
|
-
continue;
|
|
917
|
-
const slot = this.slotOf.get(key);
|
|
918
|
-
if (slot === undefined)
|
|
919
|
-
throw new Error(`blits: pull was handed ${key}, which the kit lacks`);
|
|
920
|
-
const channel = this.channels[slot];
|
|
921
|
-
const rest = channel.rest;
|
|
922
|
-
const axes = Array.isArray(rest) ? rest.length : 1;
|
|
923
|
-
const bounds = channel.bounds;
|
|
924
|
-
const at = new Float64Array(axes).fill(Number.NaN);
|
|
925
|
-
if (typeof rest === 'number')
|
|
926
|
-
at[0] = rest;
|
|
927
|
-
else if (Array.isArray(rest))
|
|
928
|
-
at.set(rest);
|
|
929
|
-
if (bounds !== undefined)
|
|
930
|
-
clampRun(at, 0, axes, bounds);
|
|
931
|
-
columns.push({ key, slot, axes, out, rest: at, bounds });
|
|
932
|
-
}
|
|
933
|
-
return columns;
|
|
238
|
+
pull(this, subjects, into);
|
|
934
239
|
}
|
|
935
240
|
project(timestamp) {
|
|
936
|
-
const t = timestamp - this.offset;
|
|
937
241
|
const c = new Mixer(this.kit, { ...this.opts, history: undefined, lanes: false });
|
|
938
|
-
c
|
|
939
|
-
c.pose = this.pose;
|
|
940
|
-
c.offset = this.offset;
|
|
941
|
-
c.wantsPose = this.wantsPose;
|
|
942
|
-
c.nextId = this.nextId;
|
|
943
|
-
c.reducedNow = this.reducedNow;
|
|
944
|
-
if (Number.isNaN(this.now) || t >= this.now) {
|
|
945
|
-
c.now = this.now;
|
|
946
|
-
c.voices = this.voices
|
|
947
|
-
.filter((v) => v.state !== 'done')
|
|
948
|
-
.map((v) => v.copy((subject) => this.carry(v, subject)));
|
|
949
|
-
c.announced = this.announced.map((a) => ({ ...a }));
|
|
950
|
-
c.count();
|
|
951
|
-
c.move(t);
|
|
952
|
-
}
|
|
953
|
-
else {
|
|
954
|
-
const history = this.opts.history;
|
|
955
|
-
if (!history)
|
|
956
|
-
throw new Error('blits: reading back needs a mix made with history');
|
|
957
|
-
if (t < this.now - history.ms)
|
|
958
|
-
throw new Error(`blits: ${timestamp} is older than this mix's history reaches`);
|
|
959
|
-
c.now = t;
|
|
960
|
-
c.backward = true;
|
|
961
|
-
const was = last(this.hostLog, t, true);
|
|
962
|
-
const host = this.opts.host;
|
|
963
|
-
const then = was && typeof host === 'object' && host !== null
|
|
964
|
-
? Object.assign(Object.create(host), was.fields)
|
|
965
|
-
: undefined;
|
|
966
|
-
if (was)
|
|
967
|
-
c.hostThen = was;
|
|
968
|
-
c.announced = this.announced.filter((a) => a.made < t).map((a) => ({ ...a }));
|
|
969
|
-
c.voices = [...this.voices, ...this.gone]
|
|
970
|
-
.filter((v) => v.cuedAt <= t && v.doneAt > t)
|
|
971
|
-
.sort((a, b) => a.id - b.id)
|
|
972
|
-
.map((v) => {
|
|
973
|
-
const log = v.log;
|
|
974
|
-
const copy = v.copy((subject) => this.recall(v, subject, t), last(log, t, (e) => e.sync) ?? log[0]);
|
|
975
|
-
copy.state =
|
|
976
|
-
t < v.start
|
|
977
|
-
? 'pending'
|
|
978
|
-
: copy.out && copy.out.at <= t
|
|
979
|
-
? 'fading'
|
|
980
|
-
: copy.holdsAfter && copy.elapsedAt(t) >= copy.span + copy.latest
|
|
981
|
-
? 'held'
|
|
982
|
-
: 'live';
|
|
983
|
-
if (then !== undefined)
|
|
984
|
-
copy.setting.host = then;
|
|
985
|
-
return copy;
|
|
986
|
-
});
|
|
987
|
-
c.count();
|
|
988
|
-
}
|
|
989
|
-
const read = (f) => {
|
|
990
|
-
reading.live = false;
|
|
991
|
-
try {
|
|
992
|
-
return f();
|
|
993
|
-
}
|
|
994
|
-
finally {
|
|
995
|
-
reading.live = true;
|
|
996
|
-
}
|
|
997
|
-
};
|
|
998
|
-
return {
|
|
999
|
-
timestamp,
|
|
1000
|
-
probe: (subject, out) => read(() => c.fold(subject, out)),
|
|
1001
|
-
assess: (subject) => read(() => {
|
|
1002
|
-
c.fold(subject);
|
|
1003
|
-
return c.doubts(subject);
|
|
1004
|
-
}),
|
|
1005
|
-
};
|
|
242
|
+
return project(this, c, timestamp);
|
|
1006
243
|
}
|
|
1007
244
|
announce(name, opts = {}) {
|
|
1008
|
-
const at = opts.at !== undefined ? opts.at - this.offset : this.
|
|
245
|
+
const at = opts.at !== undefined ? opts.at - this.offset : this.u;
|
|
1009
246
|
this.announced.push({
|
|
1010
247
|
name,
|
|
1011
248
|
score: opts.score,
|
|
@@ -1016,41 +253,28 @@ class Mixer {
|
|
|
1016
253
|
});
|
|
1017
254
|
}
|
|
1018
255
|
marks(from, to) {
|
|
1019
|
-
|
|
1020
|
-
const hi = to - this.offset;
|
|
1021
|
-
const out = [];
|
|
1022
|
-
for (const a of this.announced)
|
|
1023
|
-
if (a.at >= lo && a.at <= hi)
|
|
1024
|
-
out.push({
|
|
1025
|
-
timestamp: a.at + this.offset,
|
|
1026
|
-
mark: undefined,
|
|
1027
|
-
voice: undefined,
|
|
1028
|
-
score: a.score,
|
|
1029
|
-
name: a.name,
|
|
1030
|
-
tags: a.tags,
|
|
1031
|
-
order: a.order,
|
|
1032
|
-
});
|
|
1033
|
-
for (const voice of [...this.voices, ...this.gone]) {
|
|
1034
|
-
for (const mark of ['start', 'in', 'out', 'end']) {
|
|
1035
|
-
const t = this.markOf(voice, mark);
|
|
1036
|
-
if (t === undefined || t < lo || t > hi)
|
|
1037
|
-
continue;
|
|
1038
|
-
out.push({
|
|
1039
|
-
timestamp: t + this.offset,
|
|
1040
|
-
mark,
|
|
1041
|
-
voice: voice.id,
|
|
1042
|
-
score: voice.spec.score,
|
|
1043
|
-
name: voice.spec.name,
|
|
1044
|
-
tags: voice.spec.tags ?? none,
|
|
1045
|
-
order: voice.id,
|
|
1046
|
-
});
|
|
1047
|
-
}
|
|
1048
|
-
}
|
|
1049
|
-
out.sort((a, b) => a.timestamp - b.timestamp || a.order - b.order);
|
|
1050
|
-
return out.map(({ order: _, ...m }) => m);
|
|
256
|
+
return listed(this, from, to).map(({ order: _, ...m }) => m);
|
|
1051
257
|
}
|
|
1052
258
|
atRest(subject) {
|
|
1053
|
-
|
|
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) {
|
|
1054
278
|
for (let i = 0; i < this.names.length; i++) {
|
|
1055
279
|
const channel = this.channels[i];
|
|
1056
280
|
const value = pose[this.names[i]];
|
|
@@ -1066,11 +290,48 @@ class Mixer {
|
|
|
1066
290
|
rebase() {
|
|
1067
291
|
this.rebasing = true;
|
|
1068
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
|
+
}
|
|
1069
300
|
get live() {
|
|
1070
|
-
return this.
|
|
301
|
+
return this.cued.some((v) => v.state !== 'done');
|
|
1071
302
|
}
|
|
1072
303
|
get inert() {
|
|
1073
|
-
|
|
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();
|
|
1074
335
|
}
|
|
1075
336
|
/** Whether a voice will change no pose from `now` on, short of a change made to it. */
|
|
1076
337
|
still(voice, now) {
|
|
@@ -1083,16 +344,17 @@ class Mixer {
|
|
|
1083
344
|
const fadeIn = this.reducedNow ? 0 : (voice.fade.in ?? 0);
|
|
1084
345
|
if (fadeIn > 0 && now - this.sinceOf(voice, voice.latest) < fadeIn)
|
|
1085
346
|
return false;
|
|
1086
|
-
if (voice.state === 'held')
|
|
347
|
+
if (voice.state === 'held' || voice.holding !== null)
|
|
1087
348
|
return true;
|
|
1088
349
|
const motion = voice.motion;
|
|
1089
350
|
return motion !== undefined && voice.elapsedAt(now) >= voice.latest && motion.landed();
|
|
1090
351
|
}
|
|
1091
352
|
mute(opts) {
|
|
1092
|
-
for (const voice of this.
|
|
1093
|
-
|
|
353
|
+
for (const voice of this.cued)
|
|
354
|
+
beginFade(this, voice, { over: opts?.over });
|
|
1094
355
|
}
|
|
1095
356
|
drop(subject) {
|
|
357
|
+
this.frame = nextFrame();
|
|
1096
358
|
const head = this.chains.get(subject);
|
|
1097
359
|
// Nothing that kept this head, such as `pull`'s remembered list, may take it as current again.
|
|
1098
360
|
if (head !== undefined) {
|
|
@@ -1102,8 +364,9 @@ class Mixer {
|
|
|
1102
364
|
if (head !== undefined && head.slot >= 0 && this.lanes !== null)
|
|
1103
365
|
this.lanes.release(head.slot);
|
|
1104
366
|
this.pose.delete(subject);
|
|
367
|
+
this.restStamps?.delete(subject);
|
|
1105
368
|
this.chains.delete(subject);
|
|
1106
|
-
this.
|
|
369
|
+
this.stir();
|
|
1107
370
|
// A record of it is only in a voice over every subject, one naming it, or one gone; a ramp out
|
|
1108
371
|
// of a voice that does not name it is in `parters`, and a patch's own state in `motions`.
|
|
1109
372
|
const forget = (voice) => {
|
|
@@ -1111,6 +374,7 @@ class Mixer {
|
|
|
1111
374
|
voice.motion?.release(subject);
|
|
1112
375
|
voice.parts?.delete(subject);
|
|
1113
376
|
voice.parted?.delete(subject);
|
|
377
|
+
voice.blend?.of.reads.delete(subject);
|
|
1114
378
|
};
|
|
1115
379
|
for (const voice of this.general)
|
|
1116
380
|
forget(voice);
|
|
@@ -1120,6 +384,9 @@ class Mixer {
|
|
|
1120
384
|
forget(voice);
|
|
1121
385
|
for (const voice of this.gone)
|
|
1122
386
|
forget(voice);
|
|
387
|
+
if (this.owners !== null)
|
|
388
|
+
for (const voice of this.owners)
|
|
389
|
+
forget(voice);
|
|
1123
390
|
for (const voice of this.parters) {
|
|
1124
391
|
forget(voice);
|
|
1125
392
|
if (voice.parts?.size === 0)
|
|
@@ -1137,32 +404,6 @@ class Mixer {
|
|
|
1137
404
|
for (const motion of this.strayAll)
|
|
1138
405
|
motion.release(subject);
|
|
1139
406
|
}
|
|
1140
|
-
/** Notes a motion patch that may hold state for a subject no voice playing it reaches. */
|
|
1141
|
-
stray(motion, subject) {
|
|
1142
|
-
const list = this.strays.get(subject);
|
|
1143
|
-
if (list === undefined)
|
|
1144
|
-
this.strays.set(subject, [motion]);
|
|
1145
|
-
else if (!list.includes(motion))
|
|
1146
|
-
list.push(motion);
|
|
1147
|
-
}
|
|
1148
|
-
/** A motion voice left the list: if its patch plays on, its subjects' state there may stray. */
|
|
1149
|
-
unplay(voice) {
|
|
1150
|
-
const motion = voice.motion;
|
|
1151
|
-
if (motion === undefined)
|
|
1152
|
-
return;
|
|
1153
|
-
const left = (this.playing.get(motion) ?? 1) - 1;
|
|
1154
|
-
if (left <= 0) {
|
|
1155
|
-
this.playing.delete(motion);
|
|
1156
|
-
this.strayAll.delete(motion);
|
|
1157
|
-
return;
|
|
1158
|
-
}
|
|
1159
|
-
this.playing.set(motion, left);
|
|
1160
|
-
if (voice.named === null)
|
|
1161
|
-
this.strayAll.add(motion);
|
|
1162
|
-
else
|
|
1163
|
-
for (const subject of voice.named)
|
|
1164
|
-
this.stray(motion, subject);
|
|
1165
|
-
}
|
|
1166
407
|
drain(tag) {
|
|
1167
408
|
const all = this.sent;
|
|
1168
409
|
if (all.length === 0)
|
|
@@ -1179,1302 +420,83 @@ class Mixer {
|
|
|
1179
420
|
}
|
|
1180
421
|
return out.sort((a, b) => a.timestamp - b.timestamp);
|
|
1181
422
|
}
|
|
1182
|
-
laneHost() {
|
|
1183
|
-
const mix = this;
|
|
1184
|
-
return {
|
|
1185
|
-
get voices() {
|
|
1186
|
-
return mix.voices;
|
|
1187
|
-
},
|
|
1188
|
-
channels: this.channels,
|
|
1189
|
-
names: this.names,
|
|
1190
|
-
fits: (voice) => mix.fits(voice),
|
|
1191
|
-
meet: (voice, subject) => mix.held(voice, subject, mix.now),
|
|
1192
|
-
naming: (subject) => (mix.naming === 0 ? undefined : mix.named.get(subject)),
|
|
1193
|
-
slotOf: (subject) => mix.chains.get(subject)?.slot ?? -1,
|
|
1194
|
-
envelope: (voice, since) => mix.envelope(voice, mix.now, since),
|
|
1195
|
-
parting: (voice, subject) => mix.parting(voice, subject, mix.now),
|
|
1196
|
-
ready: (voice, subject, held, elapsed, pass, weight) => {
|
|
1197
|
-
mix.prime(voice, held, mix.now, elapsed, pass);
|
|
1198
|
-
voice.setting.weight = weight;
|
|
1199
|
-
mix.sending.voice = voice;
|
|
1200
|
-
mix.sending.subject = subject;
|
|
1201
|
-
},
|
|
1202
|
-
horizon: (voice, delay) => {
|
|
1203
|
-
reading.horizon = mix.horizonFor(voice, delay, mix.now);
|
|
1204
|
-
},
|
|
1205
|
-
keeps: this.opts.history !== undefined,
|
|
1206
|
-
after: (voice, held) => mix.remember(voice, held),
|
|
1207
|
-
kept: (voice) => mix.stateful(voice),
|
|
1208
|
-
};
|
|
1209
|
-
}
|
|
1210
|
-
/**
|
|
1211
|
-
* Whether a voice's patch and spec can run on a lane, its channels aside. A voice whose patch has
|
|
1212
|
-
* kept state on a record through `setting.keep` is stateful from then on, and leaves its lane;
|
|
1213
|
-
* one that starts keeping partway through may advance that state once for one subject not
|
|
1214
|
-
* probed on the frame it starts, which declaring `state` avoids.
|
|
1215
|
-
*/
|
|
1216
|
-
fits(voice) {
|
|
1217
|
-
const spec = voice.spec;
|
|
1218
|
-
const patch = voice.patch;
|
|
1219
|
-
if (voice.keeping)
|
|
1220
|
-
return false;
|
|
1221
|
-
if (voice.state === 'pending' && voice.holdsBefore)
|
|
1222
|
-
return false;
|
|
1223
|
-
if (typeof spec.weight === 'function')
|
|
1224
|
-
return false;
|
|
1225
|
-
if (spec.locus !== undefined || spec.from === 'current')
|
|
1226
|
-
return false;
|
|
1227
|
-
if (voice.out?.rest)
|
|
1228
|
-
return false;
|
|
1229
|
-
if (patch.state !== undefined || patch.step !== undefined)
|
|
1230
|
-
return false;
|
|
1231
|
-
if (this.opts.history?.inputs && patch.reads !== undefined && patch.reads.length > 0)
|
|
1232
|
-
return false;
|
|
1233
|
-
const built = voice.built;
|
|
1234
|
-
return (built === null ||
|
|
1235
|
-
built.tracks.every((t, i) => t.lerp === undefined || t.lerp === voice.lerps[i]));
|
|
1236
|
-
}
|
|
1237
|
-
/** A voice's patch kept state on a record: it is stateful from now on. */
|
|
1238
|
-
stateful(voice) {
|
|
1239
|
-
if (voice.keeping)
|
|
1240
|
-
return;
|
|
1241
|
-
voice.keeping = true;
|
|
1242
|
-
this.lanes?.invalidate();
|
|
1243
|
-
}
|
|
1244
|
-
/**
|
|
1245
|
-
* What a motion patch asks for its subjects' voice time at the latest frame. It holds the mix and
|
|
1246
|
-
* the voice weakly, so a patch the host keeps does not keep a mix it has let go of alive.
|
|
1247
|
-
*/
|
|
1248
|
-
static reviveHook(mix, voice) {
|
|
1249
|
-
return (subject) => {
|
|
1250
|
-
const m = mix.deref();
|
|
1251
|
-
if (m === undefined)
|
|
1252
|
-
return;
|
|
1253
|
-
m.stirred = true;
|
|
1254
|
-
const v = voice.deref();
|
|
1255
|
-
if (v === undefined)
|
|
1256
|
-
return;
|
|
1257
|
-
if (v.motion !== undefined && v.named !== null && !v.named.has(subject))
|
|
1258
|
-
m.stray(v.motion, subject);
|
|
1259
|
-
m.unpart(v, subject);
|
|
1260
|
-
};
|
|
1261
|
-
}
|
|
1262
|
-
static frameHook(mix, voice) {
|
|
1263
|
-
return (subject) => {
|
|
1264
|
-
const m = mix.deref();
|
|
1265
|
-
const v = voice.deref();
|
|
1266
|
-
return m === undefined || v === undefined ? Number.NaN : m.frameOf(v, subject);
|
|
1267
|
-
};
|
|
1268
|
-
}
|
|
1269
|
-
/**
|
|
1270
|
-
* A subject's voice time at the latest frame, where a motion patch places an untimed change and a
|
|
1271
|
-
* `read` with no time; NaN until the voice has started and met the subject, and while the
|
|
1272
|
-
* subject's own time is still short of its stagger. A retired voice's patch no longer asks.
|
|
1273
|
-
*/
|
|
1274
|
-
frameOf(voice, subject) {
|
|
1275
|
-
if (Number.isNaN(this.now) || voice.state === 'pending')
|
|
1276
|
-
return Number.NaN;
|
|
1277
|
-
const held = voice.subjects.get(subject);
|
|
1278
|
-
if (held === undefined || !held.reaches)
|
|
1279
|
-
return Number.NaN;
|
|
1280
|
-
const t = voice.elapsedAt(this.now) - held.delay;
|
|
1281
|
-
return t < 0 ? Number.NaN : t;
|
|
1282
|
-
}
|
|
1283
|
-
/** Fills a voice's setting for a call to its patch, the weight aside. */
|
|
1284
|
-
prime(voice, held, now, elapsed, pass) {
|
|
1285
|
-
const setting = voice.setting;
|
|
1286
|
-
setting.timestamp = now;
|
|
1287
|
-
setting.dt = this.capped(now - held.stepped);
|
|
1288
|
-
setting.elapsed = elapsed;
|
|
1289
|
-
setting.pass = pass;
|
|
1290
|
-
setting.weight = 0;
|
|
1291
|
-
setting.state = held.state;
|
|
1292
|
-
setting.keep = held.keep;
|
|
1293
|
-
}
|
|
1294
|
-
/** A gap as a `dt`: Infinity under reduced motion, and no more than `maxDt`. */
|
|
1295
|
-
capped(gap) {
|
|
1296
|
-
const cap = this.opts.maxDt;
|
|
1297
|
-
return this.reducedNow ? Number.POSITIVE_INFINITY : cap !== undefined && gap > cap ? cap : gap;
|
|
1298
|
-
}
|
|
1299
|
-
/** The earliest voice time a read may still ask for, for a subject delayed `delay`. */
|
|
1300
|
-
horizonFor(voice, delay, now) {
|
|
1301
|
-
const history = this.opts.history;
|
|
1302
|
-
return history === undefined
|
|
1303
|
-
? Number.POSITIVE_INFINITY
|
|
1304
|
-
: this.elapsedThen(voice, now - history.ms) - delay;
|
|
1305
|
-
}
|
|
1306
|
-
// ── internals ──────────────────────────────────────────────────────────────
|
|
1307
|
-
/** Refuses a placement that names a mark twice on one side, or that waits on itself. */
|
|
1308
|
-
checkPlacement(spec, anchor) {
|
|
1309
|
-
if (anchor.start !== undefined && anchor.in !== undefined)
|
|
1310
|
-
throw new Error('blits: a placement anchors start or in, not both');
|
|
1311
|
-
if (anchor.out !== undefined && anchor.end !== undefined)
|
|
1312
|
-
throw new Error('blits: a placement anchors out or end, not both');
|
|
1313
|
-
if (spec.start !== undefined && (anchor.start !== undefined || anchor.in !== undefined))
|
|
1314
|
-
throw new Error('blits: a voice takes start or an anchored start, not both');
|
|
1315
|
-
const name = spec.name;
|
|
1316
|
-
if (name === undefined)
|
|
1317
|
-
return;
|
|
1318
|
-
// A voice is known by its score and its name; a bare name in an anchor is in the asker's score.
|
|
1319
|
-
const key = (score, n) => `${score ?? ''}\u0000${n}`;
|
|
1320
|
-
const names = (p, score) => [p.start, p.in, p.out, p.end].flatMap((a) => {
|
|
1321
|
-
if (a === undefined || typeof a === 'number')
|
|
1322
|
-
return [];
|
|
1323
|
-
const q = 'of' in a ? a.of : 'after' in a ? a.after : 'with' in a ? a.with : a.before;
|
|
1324
|
-
if (typeof q === 'string')
|
|
1325
|
-
return [key(score, q)];
|
|
1326
|
-
return q.name === undefined ? [] : [key(q.score ?? score, q.name)];
|
|
1327
|
-
});
|
|
1328
|
-
const self = key(spec.score, name);
|
|
1329
|
-
const seen = new Set();
|
|
1330
|
-
const waits = names(anchor, spec.score);
|
|
1331
|
-
while (waits.length > 0) {
|
|
1332
|
-
const n = waits.pop();
|
|
1333
|
-
if (n === self)
|
|
1334
|
-
throw new Error(`blits: ${name}'s placement waits on itself`);
|
|
1335
|
-
if (seen.has(n))
|
|
1336
|
-
continue;
|
|
1337
|
-
seen.add(n);
|
|
1338
|
-
for (const v of this.voices)
|
|
1339
|
-
if (v.spec.name !== undefined && key(v.spec.score, v.spec.name) === n && v.spec.anchor)
|
|
1340
|
-
waits.push(...names(v.spec.anchor, v.spec.score));
|
|
1341
|
-
}
|
|
1342
|
-
}
|
|
1343
|
-
/**
|
|
1344
|
-
* Fixes every anchored voice's start and out from what its anchors answer now. A start is fixed
|
|
1345
|
-
* while the voice waits and an out until its fade begins; a target that has left keeps the time it
|
|
1346
|
-
* last gave. Repeated so a chain of anchors settles in one sync.
|
|
1347
|
-
*/
|
|
1348
|
-
place() {
|
|
1349
|
-
for (let round = 0; round <= this.voices.length; round++) {
|
|
1350
|
-
let moved = false;
|
|
1351
|
-
for (const voice of this.voices) {
|
|
1352
|
-
const anchor = voice.spec.anchor;
|
|
1353
|
-
if (anchor === undefined || voice.state === 'done')
|
|
1354
|
-
continue;
|
|
1355
|
-
if (voice.placing && voice.state === 'pending') {
|
|
1356
|
-
const by = anchor.start ?? anchor.in;
|
|
1357
|
-
const t = by === undefined ? undefined : this.resolve(by, voice);
|
|
1358
|
-
if (t !== undefined) {
|
|
1359
|
-
const start = anchor.start !== undefined ? t : t - (voice.fade.in ?? 0);
|
|
1360
|
-
if (start !== voice.start) {
|
|
1361
|
-
voice.start = start;
|
|
1362
|
-
voice.anchorNow = start;
|
|
1363
|
-
voice.anchorElapsed = 0;
|
|
1364
|
-
this.noted(voice);
|
|
1365
|
-
moved = true;
|
|
1366
|
-
}
|
|
1367
|
-
}
|
|
1368
|
-
}
|
|
1369
|
-
if (voice.state !== 'fading') {
|
|
1370
|
-
const by = anchor.out ?? anchor.end;
|
|
1371
|
-
const t = by === undefined ? undefined : this.resolve(by, voice);
|
|
1372
|
-
if (t !== undefined) {
|
|
1373
|
-
const at = anchor.out !== undefined ? t : t - (voice.fade.out ?? 0);
|
|
1374
|
-
if (at !== voice.outAt) {
|
|
1375
|
-
voice.outAt = at;
|
|
1376
|
-
this.noted(voice);
|
|
1377
|
-
moved = true;
|
|
1378
|
-
}
|
|
1379
|
-
}
|
|
1380
|
-
}
|
|
1381
|
-
}
|
|
1382
|
-
if (!moved)
|
|
1383
|
-
return;
|
|
1384
|
-
}
|
|
1385
|
-
}
|
|
1386
|
-
/** The mix time an anchor answers, or undefined while its target has none. */
|
|
1387
|
-
resolve(a, self) {
|
|
1388
|
-
if (typeof a === 'number')
|
|
1389
|
-
return a - this.offset;
|
|
1390
|
-
let query;
|
|
1391
|
-
let mark;
|
|
1392
|
-
let by = a.by ?? 0;
|
|
1393
|
-
if ('of' in a) {
|
|
1394
|
-
query = a.of;
|
|
1395
|
-
mark = a.mark;
|
|
1396
|
-
}
|
|
1397
|
-
else if ('after' in a) {
|
|
1398
|
-
query = a.after;
|
|
1399
|
-
mark = 'end';
|
|
1400
|
-
}
|
|
1401
|
-
else if ('with' in a) {
|
|
1402
|
-
query = a.with;
|
|
1403
|
-
mark = 'start';
|
|
1404
|
-
}
|
|
1405
|
-
else {
|
|
1406
|
-
query = a.before;
|
|
1407
|
-
mark = 'start';
|
|
1408
|
-
by = -by;
|
|
1409
|
-
}
|
|
1410
|
-
const t = this.timeOf(typeof query === 'string' ? { name: query } : query, mark, self);
|
|
1411
|
-
return t === undefined ? undefined : t + by;
|
|
1412
|
-
}
|
|
1413
|
-
/**
|
|
1414
|
-
* The time a query answers: `mark` of the voice it picks, among those cued and those that have
|
|
1415
|
-
* left, never the asker; or a mark the host announced on the score, which is the same time for
|
|
1416
|
-
* any of the four. Undefined while what it picks has no such time.
|
|
1417
|
-
*/
|
|
1418
|
-
timeOf(q, mark, self) {
|
|
1419
|
-
const score = q.score ?? self.spec.score;
|
|
1420
|
-
const found = [];
|
|
1421
|
-
for (const v of [...this.gone, ...this.voices])
|
|
1422
|
-
if (v !== self &&
|
|
1423
|
-
v.spec.score === score &&
|
|
1424
|
-
(q.name === undefined || v.spec.name === q.name) &&
|
|
1425
|
-
(q.tag === undefined || (v.spec.tags ?? none).includes(q.tag)) &&
|
|
1426
|
-
(q.writes === undefined || v.patch.writes.includes(q.writes)))
|
|
1427
|
-
found.push({ order: v.id, t: this.markOf(v, mark) });
|
|
1428
|
-
if (q.writes === undefined)
|
|
1429
|
-
for (const a of this.announced)
|
|
1430
|
-
if (a.score === score &&
|
|
1431
|
-
(q.name === undefined || a.name === q.name) &&
|
|
1432
|
-
(q.tag === undefined || a.tags.includes(q.tag)))
|
|
1433
|
-
found.push({ order: a.order, t: Number.isNaN(a.at) ? undefined : a.at });
|
|
1434
|
-
if (found.length === 0)
|
|
1435
|
-
return undefined;
|
|
1436
|
-
found.sort((x, y) => x.order - y.order);
|
|
1437
|
-
const resolver = q.resolver ?? 'last';
|
|
1438
|
-
if (resolver === 'first')
|
|
1439
|
-
return found[0]?.t;
|
|
1440
|
-
if (resolver === 'last')
|
|
1441
|
-
return found[found.length - 1]?.t;
|
|
1442
|
-
const times = found.flatMap((e) => (e.t === undefined ? [] : [e.t])).sort((x, y) => x - y);
|
|
1443
|
-
if (resolver === 'next') {
|
|
1444
|
-
const now = Number.isNaN(this.now) ? Number.NEGATIVE_INFINITY : this.now;
|
|
1445
|
-
return times.find((t) => t >= now);
|
|
1446
|
-
}
|
|
1447
|
-
return resolver === 'earliest' ? times[0] : times[times.length - 1];
|
|
1448
|
-
}
|
|
1449
|
-
/** When a voice reaches a mark, mix time, or undefined while nothing has fixed it. */
|
|
1450
|
-
markOf(voice, mark) {
|
|
1451
|
-
const start = voice.start;
|
|
1452
|
-
if (!Number.isFinite(start))
|
|
1453
|
-
return undefined;
|
|
1454
|
-
if (mark === 'start')
|
|
1455
|
-
return start;
|
|
1456
|
-
if (mark === 'in')
|
|
1457
|
-
return start + (voice.fade.in ?? 0);
|
|
1458
|
-
const out = voice.out;
|
|
1459
|
-
let outAt;
|
|
1460
|
-
if (out !== null)
|
|
1461
|
-
outAt = out.at;
|
|
1462
|
-
else if (Number.isFinite(voice.outAt))
|
|
1463
|
-
outAt = voice.outAt;
|
|
1464
|
-
else if (!voice.holdsAfter && Number.isFinite(voice.span)) {
|
|
1465
|
-
const t = voice.timeAt(voice.span + voice.latest);
|
|
1466
|
-
if (Number.isFinite(t))
|
|
1467
|
-
outAt = Math.max(start, t);
|
|
1468
|
-
}
|
|
1469
|
-
if (mark === 'out')
|
|
1470
|
-
return voice.state === 'done' && outAt === undefined ? voice.doneAt : outAt;
|
|
1471
|
-
if (voice.state === 'done')
|
|
1472
|
-
return voice.doneAt;
|
|
1473
|
-
if (out !== null) {
|
|
1474
|
-
if (out.rest)
|
|
1475
|
-
return out.deadline === undefined ? undefined : out.at + out.deadline;
|
|
1476
|
-
return out.at + out.over;
|
|
1477
|
-
}
|
|
1478
|
-
return outAt === undefined ? undefined : outAt + (this.reduced ? 0 : (voice.fade.out ?? 0));
|
|
1479
|
-
}
|
|
1480
|
-
/** A voice's elapsed at an earlier mix time, by the controls it had then. */
|
|
1481
|
-
elapsedThen(voice, t) {
|
|
1482
|
-
const log = voice.log;
|
|
1483
|
-
const c = log === null ? undefined : last(log, t, true);
|
|
1484
|
-
return elapsedWith(c ?? voice, t);
|
|
1485
|
-
}
|
|
1486
|
-
/** Records a change to a voice's controls under `history`, and lets go of what it no longer reaches. */
|
|
1487
|
-
noted(voice) {
|
|
1488
|
-
this.stirred = true;
|
|
1489
|
-
this.schedule(voice);
|
|
1490
|
-
const log = voice.log;
|
|
1491
|
-
if (log === null)
|
|
1492
|
-
return;
|
|
1493
|
-
voice.note(Number.isNaN(this.now) ? Number.NEGATIVE_INFINITY : this.now, this.moving);
|
|
1494
|
-
const reach = this.now - this.opts.history.ms;
|
|
1495
|
-
let drop = 0;
|
|
1496
|
-
while (drop + 1 < log.length && log[drop + 1].at <= reach)
|
|
1497
|
-
drop++;
|
|
1498
|
-
if (drop > 0)
|
|
1499
|
-
log.splice(0, drop);
|
|
1500
|
-
}
|
|
1501
|
-
/** Recounts what the fold's shortcuts depend on, for a projection's freshly copied voices. */
|
|
1502
|
-
count() {
|
|
1503
|
-
this.named = new Store();
|
|
1504
|
-
this.naming = 0;
|
|
1505
|
-
this.general = [];
|
|
1506
|
-
for (const voice of this.voices)
|
|
1507
|
-
this.index(voice);
|
|
1508
|
-
this.loci = this.voices.filter((v) => v.spec.locus !== undefined).length;
|
|
1509
|
-
this.anchored = this.voices.filter((v) => v.spec.anchor !== undefined).length;
|
|
1510
|
-
this.version++;
|
|
1511
|
-
}
|
|
1512
|
-
/** A copy of a subject's record that shares nothing a read could change. */
|
|
1513
|
-
copyHeld(voice, h) {
|
|
1514
|
-
const kept = new Map();
|
|
1515
|
-
for (const [owner, value] of h.kept)
|
|
1516
|
-
kept.set(owner, structuredClone(value));
|
|
1517
|
-
const patch = voice.patch;
|
|
1518
|
-
return {
|
|
1519
|
-
...h,
|
|
1520
|
-
bands: h.bands.slice(),
|
|
1521
|
-
state: h.state === undefined
|
|
1522
|
-
? undefined
|
|
1523
|
-
: patch.clone
|
|
1524
|
-
? patch.clone(h.state)
|
|
1525
|
-
: structuredClone(h.state),
|
|
1526
|
-
kept,
|
|
1527
|
-
keep: keeper(kept),
|
|
1528
|
-
probed: Number.NaN,
|
|
1529
|
-
delta: null,
|
|
1530
|
-
phase: 0,
|
|
1531
|
-
seeks: 0,
|
|
1532
|
-
base: h.base === undefined ? undefined : structuredClone(h.base),
|
|
1533
|
-
slope: h.slope === undefined ? undefined : structuredClone(h.slope),
|
|
1534
|
-
snaps: undefined,
|
|
1535
|
-
inputs: undefined,
|
|
1536
|
-
next: null,
|
|
1537
|
-
version: Number.NaN,
|
|
1538
|
-
loci: null,
|
|
1539
|
-
slot: -1,
|
|
1540
|
-
};
|
|
1541
|
-
}
|
|
1542
|
-
/** A projection ahead starts each subject from where the live mix holds it. */
|
|
1543
|
-
carry(voice, subject) {
|
|
1544
|
-
const live = voice.subjects.get(subject);
|
|
1545
|
-
if (live === undefined)
|
|
1546
|
-
return undefined;
|
|
1547
|
-
const h = this.copyHeld(voice, live);
|
|
1548
|
-
h.from = h.stepped;
|
|
1549
|
-
return h;
|
|
1550
|
-
}
|
|
1551
|
-
/**
|
|
1552
|
-
* A projection back starts each subject from the latest copy kept at or before `t`, else from its
|
|
1553
|
-
* voice's start with fresh state, which is exact for a voice stepped at a fixed interval.
|
|
1554
|
-
*/
|
|
1555
|
-
recall(voice, subject, t) {
|
|
1556
|
-
const live = voice.subjects.get(subject);
|
|
1557
|
-
if (live === undefined)
|
|
1558
|
-
return undefined;
|
|
1559
|
-
const snap = live.snaps && last(live.snaps, t, true);
|
|
1560
|
-
if (snap) {
|
|
1561
|
-
const h = this.copyHeld(voice, snap.held);
|
|
1562
|
-
h.from = h.stepped;
|
|
1563
|
-
h.replay = live.inputs;
|
|
1564
|
-
return h;
|
|
1565
|
-
}
|
|
1566
|
-
const kept = new Map();
|
|
1567
|
-
const stepped = live.since < t ? live.since : t;
|
|
1568
|
-
return {
|
|
1569
|
-
reaches: live.reaches,
|
|
1570
|
-
delay: live.delay,
|
|
1571
|
-
since: live.since,
|
|
1572
|
-
shown: live.shown,
|
|
1573
|
-
weight: 0,
|
|
1574
|
-
rested: false,
|
|
1575
|
-
bands: new Uint8Array(voice.slots.length),
|
|
1576
|
-
state: live.reaches && voice.patch.state
|
|
1577
|
-
? voice.patch.state(subject)
|
|
1578
|
-
: undefined,
|
|
1579
|
-
stepped,
|
|
1580
|
-
ticks: 0,
|
|
1581
|
-
probed: Number.NaN,
|
|
1582
|
-
delta: null,
|
|
1583
|
-
phase: 0,
|
|
1584
|
-
seeks: 0,
|
|
1585
|
-
kept,
|
|
1586
|
-
keep: keeper(kept),
|
|
1587
|
-
voice,
|
|
1588
|
-
next: null,
|
|
1589
|
-
version: Number.NaN,
|
|
1590
|
-
loci: null,
|
|
1591
|
-
slot: -1,
|
|
1592
|
-
from: stepped,
|
|
1593
|
-
unknown: voice.spec.from === 'current',
|
|
1594
|
-
replay: live.inputs,
|
|
1595
|
-
};
|
|
1596
|
-
}
|
|
1597
|
-
/** Under `history` with `inputs`, copies the host fields any voice reads, once a frame, when they changed. */
|
|
1598
|
-
recordHost(now) {
|
|
1599
|
-
const log = this.hostLog;
|
|
1600
|
-
const prev = log[log.length - 1];
|
|
1601
|
-
if (prev !== undefined && prev.at === now)
|
|
1602
|
-
return;
|
|
1603
|
-
const host = this.opts.host;
|
|
1604
|
-
const fields = {};
|
|
1605
|
-
for (const v of this.voices)
|
|
1606
|
-
for (const f of v.patch.reads ?? none)
|
|
1607
|
-
if (!(f in fields))
|
|
1608
|
-
fields[f] = structuredClone(host[f]);
|
|
1609
|
-
if (prev !== undefined && same(prev.fields, fields))
|
|
1610
|
-
return;
|
|
1611
|
-
log.push({ at: now, fields });
|
|
1612
|
-
const reach = now - this.opts.history.ms;
|
|
1613
|
-
while (log.length > 1 && log[1].at <= reach)
|
|
1614
|
-
log.shift();
|
|
1615
|
-
}
|
|
1616
|
-
/** Under `history` with `inputs`, keeps what an input signal read, each time it changes. */
|
|
1617
|
-
record(held, value) {
|
|
1618
|
-
const history = this.opts.history;
|
|
1619
|
-
if (!history?.inputs || this.projecting)
|
|
1620
|
-
return;
|
|
1621
|
-
const inputs = held.inputs ?? [];
|
|
1622
|
-
held.inputs = inputs;
|
|
1623
|
-
const prev = inputs[inputs.length - 1];
|
|
1624
|
-
if (prev !== undefined && (prev.value === value || prev.at === this.now)) {
|
|
1625
|
-
if (prev.at === this.now)
|
|
1626
|
-
prev.value = value;
|
|
1627
|
-
return;
|
|
1628
|
-
}
|
|
1629
|
-
inputs.push({ at: this.now, value });
|
|
1630
|
-
const reach = this.now - history.ms;
|
|
1631
|
-
while (inputs.length > 1 && inputs[1].at <= reach)
|
|
1632
|
-
inputs.shift();
|
|
1633
|
-
}
|
|
1634
|
-
/** Under `history`, keeps a copy of a stateful voice's record for this subject every so often. */
|
|
1635
|
-
remember(voice, held) {
|
|
1636
|
-
const history = this.opts.history;
|
|
1637
|
-
if (!history || this.projecting)
|
|
1638
|
-
return;
|
|
1639
|
-
if (voice.patch.step === undefined && held.kept.size === 0 && held.base === undefined)
|
|
1640
|
-
return;
|
|
1641
|
-
const snaps = held.snaps ?? [];
|
|
1642
|
-
held.snaps = snaps;
|
|
1643
|
-
const prev = snaps[snaps.length - 1];
|
|
1644
|
-
if (prev !== undefined && this.now - prev.at < (history.every ?? 200))
|
|
1645
|
-
return;
|
|
1646
|
-
snaps.push({ at: this.now, held: this.copyHeld(voice, held) });
|
|
1647
|
-
const reach = this.now - history.ms;
|
|
1648
|
-
while (snaps.length > 1 && snaps[1].at <= reach)
|
|
1649
|
-
snaps.shift();
|
|
1650
|
-
}
|
|
1651
|
-
/** Per channel, the least sure voice that fed it this frame. */
|
|
1652
|
-
doubts(subject) {
|
|
1653
|
-
const out = {};
|
|
1654
|
-
for (const name of this.names)
|
|
1655
|
-
out[name] = 'exact';
|
|
1656
|
-
const rank = { exact: 0, stepped: 1, held: 2 };
|
|
1657
|
-
// A voice still waiting on an anchor nothing has answered may yet play, or stop, by now.
|
|
1658
|
-
for (const voice of this.voices) {
|
|
1659
|
-
const anchor = voice.spec.anchor;
|
|
1660
|
-
if (anchor === undefined || voice.state === 'done')
|
|
1661
|
-
continue;
|
|
1662
|
-
const waiting = (voice.state === 'pending' && !Number.isFinite(voice.start)) ||
|
|
1663
|
-
(voice.state !== 'pending' &&
|
|
1664
|
-
voice.state !== 'fading' &&
|
|
1665
|
-
(anchor.out !== undefined || anchor.end !== undefined) &&
|
|
1666
|
-
!Number.isFinite(voice.outAt));
|
|
1667
|
-
if (!waiting || !this.aims(voice, subject))
|
|
1668
|
-
continue;
|
|
1669
|
-
for (const slot of voice.slots)
|
|
1670
|
-
out[this.names[slot]] = 'held';
|
|
1671
|
-
}
|
|
1672
|
-
for (let held = this.chain(subject, this.now); held !== null; held = held.next) {
|
|
1673
|
-
const voice = held.voice;
|
|
1674
|
-
if (voice === null || voice.state === 'done')
|
|
1675
|
-
continue;
|
|
1676
|
-
if (held.weight <= 0)
|
|
1677
|
-
continue;
|
|
1678
|
-
const d = this.doubtOf(voice, held);
|
|
1679
|
-
for (const slot of voice.slots) {
|
|
1680
|
-
const name = this.names[slot];
|
|
1681
|
-
if (rank[d] > rank[out[name]])
|
|
1682
|
-
out[name] = d;
|
|
1683
|
-
}
|
|
1684
|
-
}
|
|
1685
|
-
return out;
|
|
1686
|
-
}
|
|
1687
|
-
doubtOf(voice, held) {
|
|
1688
|
-
const weight = voice.spec.weight;
|
|
1689
|
-
if (held.unknown)
|
|
1690
|
-
return 'held';
|
|
1691
|
-
// A weight read back from a recording is what the mix used then, so its signal's state is moot.
|
|
1692
|
-
const replayed = held.replay !== undefined && last(held.replay, this.now, true) !== undefined;
|
|
1693
|
-
if (typeof weight === 'function' && weight.input && !replayed)
|
|
1694
|
-
return 'held';
|
|
1695
|
-
const reads = voice.patch.reads;
|
|
1696
|
-
if (reads !== undefined && reads.length > 0) {
|
|
1697
|
-
const then = this.hostThen?.fields;
|
|
1698
|
-
if (then === undefined || !reads.every((f) => f in then))
|
|
1699
|
-
return 'held';
|
|
1700
|
-
}
|
|
1701
|
-
if (this.now > (held.from ?? this.now)) {
|
|
1702
|
-
const tick = this.opts.stepMs;
|
|
1703
|
-
const fixed = tick !== undefined && tick > 0 && !this.reducedNow;
|
|
1704
|
-
if ((voice.patch.step && !fixed) || (held.kept.size > 0 && !replayed))
|
|
1705
|
-
return 'stepped';
|
|
1706
|
-
}
|
|
1707
|
-
return 'exact';
|
|
1708
|
-
}
|
|
1709
|
-
handle(voice) {
|
|
1710
|
-
const mix = this;
|
|
1711
|
-
return {
|
|
1712
|
-
id: voice.id,
|
|
1713
|
-
get state() {
|
|
1714
|
-
return voice.state;
|
|
1715
|
-
},
|
|
1716
|
-
played: voice.played,
|
|
1717
|
-
get weight() {
|
|
1718
|
-
return voice.weight;
|
|
1719
|
-
},
|
|
1720
|
-
set weight(w) {
|
|
1721
|
-
voice.weight = w;
|
|
1722
|
-
mix.noted(voice);
|
|
1723
|
-
mix.lanes?.refill();
|
|
1724
|
-
},
|
|
1725
|
-
get rate() {
|
|
1726
|
-
return voice.rateAt(Number.isNaN(mix.now) ? voice.start : mix.now);
|
|
1727
|
-
},
|
|
1728
|
-
set rate(r) {
|
|
1729
|
-
voice.rebase(Number.isNaN(mix.now) ? voice.start : mix.now);
|
|
1730
|
-
voice.ramp = null;
|
|
1731
|
-
voice.rate = r;
|
|
1732
|
-
mix.noted(voice);
|
|
1733
|
-
mix.lanes?.refill();
|
|
1734
|
-
},
|
|
1735
|
-
ramp(r, over) {
|
|
1736
|
-
const now = Number.isNaN(mix.now) ? voice.start : mix.now;
|
|
1737
|
-
voice.rebase(now);
|
|
1738
|
-
const from = voice.rateAt(now);
|
|
1739
|
-
voice.ramp = over > 0 && r !== from ? { from, to: r, over } : null;
|
|
1740
|
-
voice.rate = r;
|
|
1741
|
-
mix.noted(voice);
|
|
1742
|
-
mix.lanes?.refill();
|
|
1743
|
-
},
|
|
1744
|
-
seek(elapsed) {
|
|
1745
|
-
voice.rebase(Number.isNaN(mix.now) ? voice.start : mix.now);
|
|
1746
|
-
voice.anchorElapsed = elapsed;
|
|
1747
|
-
voice.seeks++;
|
|
1748
|
-
mix.noted(voice);
|
|
1749
|
-
mix.lanes?.refill();
|
|
1750
|
-
},
|
|
1751
|
-
fade(opts) {
|
|
1752
|
-
if (opts !== undefined && 'subject' in opts)
|
|
1753
|
-
mix.fadeSubject(voice, opts.subject, opts.over);
|
|
1754
|
-
else
|
|
1755
|
-
mix.beginFade(voice, opts ?? {});
|
|
1756
|
-
},
|
|
1757
|
-
weightOf(subject) {
|
|
1758
|
-
if (voice.state === 'done')
|
|
1759
|
-
return 0;
|
|
1760
|
-
if (voice.laned && mix.lanes !== null) {
|
|
1761
|
-
const w = mix.lanes.weightOf(voice.id, mix.chains.get(subject)?.slot ?? -1);
|
|
1762
|
-
if (w !== undefined)
|
|
1763
|
-
return w;
|
|
1764
|
-
}
|
|
1765
|
-
return voice.subjects.get(subject)?.weight ?? 0;
|
|
1766
|
-
},
|
|
1767
|
-
done: voice.done,
|
|
1768
|
-
};
|
|
1769
|
-
}
|
|
1770
|
-
/** Starts one subject's ramp out of a voice, or takes it out at once for a ramp of 0. */
|
|
1771
|
-
fadeSubject(voice, subject, over) {
|
|
1772
|
-
if (voice.state === 'done' || voice.parted?.has(subject) || voice.parts?.has(subject))
|
|
1773
|
-
return;
|
|
1774
|
-
this.stirred = true;
|
|
1775
|
-
const ms = this.reduced ? 0 : (over ?? voice.fade.out ?? 0);
|
|
1776
|
-
const at = Number.isNaN(this.now) ? voice.start : this.now;
|
|
1777
|
-
if (ms === 0) {
|
|
1778
|
-
this.part(voice, subject, at);
|
|
1779
|
-
return;
|
|
1780
|
-
}
|
|
1781
|
-
voice.parts ??= new Map();
|
|
1782
|
-
voice.parts.set(subject, { at, over: ms });
|
|
1783
|
-
this.parters.add(voice);
|
|
1784
|
-
this.schedule(voice);
|
|
1785
|
-
this.lanes?.refill();
|
|
1786
|
-
}
|
|
1787
|
-
/** What a subject's own ramp out of a voice leaves of its weight this frame, 0..1. */
|
|
1788
|
-
parting(voice, subject, now) {
|
|
1789
|
-
const r = voice.parts?.get(subject);
|
|
1790
|
-
if (r === undefined)
|
|
1791
|
-
return 1;
|
|
1792
|
-
return envelope(0, { at: r.at, over: r.over, rest: false }, voice.ease, this.reducedNow, now, 0);
|
|
1793
|
-
}
|
|
1794
|
-
/**
|
|
1795
|
-
* Takes a subject out of one voice for good, as of mix time `at`: the voice forgets its record,
|
|
1796
|
-
* its lane position and a motion patch's state for it, and reaches it no more.
|
|
1797
|
-
*/
|
|
1798
|
-
part(voice, subject, at) {
|
|
1799
|
-
voice.parts?.delete(subject);
|
|
1800
|
-
if (voice.parts?.size === 0)
|
|
1801
|
-
voice.parts = null;
|
|
1802
|
-
voice.parted ??= new Map();
|
|
1803
|
-
voice.parted.set(subject, at);
|
|
1804
|
-
this.parters.add(voice);
|
|
1805
|
-
this.forgetIn(voice, subject);
|
|
1806
|
-
const motion = voice.motion;
|
|
1807
|
-
if (motion !== undefined && motion.frame === voice.frame)
|
|
1808
|
-
motion.release(subject);
|
|
1809
|
-
}
|
|
1810
|
-
/** Brings a subject faded out of a voice back, to be met afresh on its next probe. */
|
|
1811
|
-
unpart(voice, subject) {
|
|
1812
|
-
if (voice.parted?.delete(subject) !== true)
|
|
1813
|
-
return;
|
|
1814
|
-
if (voice.parted.size === 0)
|
|
1815
|
-
voice.parted = null;
|
|
1816
|
-
this.forgetIn(voice, subject);
|
|
1817
|
-
const slot = this.chains.get(subject)?.slot ?? -1;
|
|
1818
|
-
if (slot >= 0)
|
|
1819
|
-
this.lanes?.rejoin(slot);
|
|
1820
|
-
}
|
|
1821
|
-
/** Drops a voice's record of a subject and relinks the subject's chain without it. */
|
|
1822
|
-
forgetIn(voice, subject) {
|
|
1823
|
-
const held = voice.subjects.get(subject);
|
|
1824
|
-
if (held !== undefined) {
|
|
1825
|
-
voice.subjects.delete(subject);
|
|
1826
|
-
if (held.reaches)
|
|
1827
|
-
voice.seen--;
|
|
1828
|
-
if (held.rested)
|
|
1829
|
-
voice.restedCount--;
|
|
1830
|
-
if (voice.holder === held)
|
|
1831
|
-
voice.holder = null;
|
|
1832
|
-
}
|
|
1833
|
-
const head = this.chains.get(subject);
|
|
1834
|
-
if (head === undefined)
|
|
1835
|
-
return;
|
|
1836
|
-
head.version = Number.NaN;
|
|
1837
|
-
this.relinks++;
|
|
1838
|
-
if (voice.laned && head.slot >= 0)
|
|
1839
|
-
this.lanes?.part(voice.id, head.slot);
|
|
1840
|
-
this.lanes?.refill();
|
|
1841
|
-
}
|
|
1842
|
-
beginFade(voice, opts, at) {
|
|
1843
|
-
if (voice.state === 'done' || voice.state === 'fading')
|
|
1844
|
-
return;
|
|
1845
|
-
const over = this.reduced ? 0 : (opts.over ?? voice.fade.out ?? 0);
|
|
1846
|
-
// A voice is linked into its subjects' chains once it plays, and a fading one plays.
|
|
1847
|
-
if (voice.state === 'pending')
|
|
1848
|
-
this.changed(voice);
|
|
1849
|
-
voice.state = 'fading';
|
|
1850
|
-
voice.out = {
|
|
1851
|
-
from: 1,
|
|
1852
|
-
at: at ?? (Number.isNaN(this.now) ? voice.start : this.now),
|
|
1853
|
-
over,
|
|
1854
|
-
rest: opts.at === 'rest',
|
|
1855
|
-
deadline: opts.deadline,
|
|
1856
|
-
};
|
|
1857
|
-
this.noted(voice);
|
|
1858
|
-
if (opts.at === 'rest')
|
|
1859
|
-
this.lanes?.invalidate();
|
|
1860
|
-
else
|
|
1861
|
-
this.lanes?.refill();
|
|
1862
|
-
if (over === 0 && opts.at !== 'rest')
|
|
1863
|
-
this.retire(voice, voice.out.at);
|
|
1864
|
-
}
|
|
1865
|
-
/** Removes a voice, recording that it left at `at`, default now. */
|
|
1866
|
-
retire(voice, at) {
|
|
1867
|
-
this.lanes?.touch(voice);
|
|
1868
|
-
const motion = voice.motion;
|
|
1869
|
-
if (motion !== undefined && motion.frame === voice.frame)
|
|
1870
|
-
motion.frame = noFrame;
|
|
1871
|
-
if (motion !== undefined && motion.revive === voice.revive)
|
|
1872
|
-
motion.revive = noRevive;
|
|
1873
|
-
voice.frame = null;
|
|
1874
|
-
voice.revive = null;
|
|
1875
|
-
voice.state = 'done';
|
|
1876
|
-
this.retired++;
|
|
1877
|
-
voice.doneAt = at ?? (Number.isNaN(this.now) ? Number.NEGATIVE_INFINITY : this.now);
|
|
1878
|
-
voice.play(false);
|
|
1879
|
-
voice.resolve();
|
|
1880
|
-
}
|
|
1881
|
-
/** The ramp a voice's own fade envelope applies this frame, 0..1. */
|
|
1882
|
-
envelope(voice, now, since) {
|
|
1883
|
-
return envelope(voice.fade.in ?? 0, voice.out, voice.ease, this.reducedNow, now, since);
|
|
1884
|
-
}
|
|
1885
|
-
/** What this voice holds for this subject, made on first sight with `target` and `stagger` asked once. */
|
|
1886
|
-
held(voice, subject, now) {
|
|
1887
|
-
let held = voice.subjects.get(subject);
|
|
1888
|
-
if (held !== undefined)
|
|
1889
|
-
return held;
|
|
1890
|
-
const reaches = this.aims(voice, subject);
|
|
1891
|
-
const delay = reaches && voice.spec.stagger ? voice.spec.stagger(subject) : 0;
|
|
1892
|
-
const since = this.sinceOf(voice, delay);
|
|
1893
|
-
const kept = new Map();
|
|
1894
|
-
held = {
|
|
1895
|
-
reaches,
|
|
1896
|
-
delay,
|
|
1897
|
-
since,
|
|
1898
|
-
shown: this.shownOf(voice, since),
|
|
1899
|
-
weight: 0,
|
|
1900
|
-
rested: false,
|
|
1901
|
-
bands: new Uint8Array(voice.slots.length),
|
|
1902
|
-
state: reaches && voice.patch.state
|
|
1903
|
-
? voice.patch.state(subject)
|
|
1904
|
-
: undefined,
|
|
1905
|
-
stepped: this.backward && since < now ? since : now,
|
|
1906
|
-
ticks: 0,
|
|
1907
|
-
probed: Number.NaN,
|
|
1908
|
-
delta: null,
|
|
1909
|
-
phase: 0,
|
|
1910
|
-
seeks: 0,
|
|
1911
|
-
kept,
|
|
1912
|
-
keep: keeper(kept),
|
|
1913
|
-
voice,
|
|
1914
|
-
next: null,
|
|
1915
|
-
version: Number.NaN,
|
|
1916
|
-
loci: null,
|
|
1917
|
-
slot: -1,
|
|
1918
|
-
};
|
|
1919
|
-
if (this.projecting) {
|
|
1920
|
-
held.from = held.stepped;
|
|
1921
|
-
held.unknown = this.backward && voice.spec.from === 'current';
|
|
1922
|
-
}
|
|
1923
|
-
voice.subjects.set(subject, held);
|
|
1924
|
-
if (voice.state === 'pending') {
|
|
1925
|
-
voice.early ??= [];
|
|
1926
|
-
voice.early.push(held);
|
|
1927
|
-
}
|
|
1928
|
-
if (reaches)
|
|
1929
|
-
voice.seen++;
|
|
1930
|
-
if (delay > voice.latest) {
|
|
1931
|
-
voice.latest = delay;
|
|
1932
|
-
// A later end can take a held voice live again.
|
|
1933
|
-
if (voice.state === 'held')
|
|
1934
|
-
this.schedule(voice);
|
|
1935
|
-
}
|
|
1936
|
-
return held;
|
|
1937
|
-
}
|
|
1938
|
-
/**
|
|
1939
|
-
* When the voice clock reads `delay`, from where it is anchored now; during a ramp this assumes
|
|
1940
|
-
* the rate it is ramping to.
|
|
1941
|
-
*/
|
|
1942
|
-
sinceOf(voice, delay) {
|
|
1943
|
-
return voice.rate > 0
|
|
1944
|
-
? voice.anchorNow + (delay - voice.anchorElapsed) / voice.rate
|
|
1945
|
-
: voice.start + delay;
|
|
1946
|
-
}
|
|
1947
|
-
shownOf(voice, since) {
|
|
1948
|
-
return voice.holdsBefore && voice.opened < since ? voice.opened : since;
|
|
1949
|
-
}
|
|
1950
|
-
/** A voice has gone live: the records it made while pending count from where it started. */
|
|
1951
|
-
started(voice) {
|
|
1952
|
-
const early = voice.early;
|
|
1953
|
-
if (early === null)
|
|
1954
|
-
return;
|
|
1955
|
-
voice.early = null;
|
|
1956
|
-
for (const held of early) {
|
|
1957
|
-
held.since = this.sinceOf(voice, held.delay);
|
|
1958
|
-
held.shown = this.shownOf(voice, held.since);
|
|
1959
|
-
}
|
|
1960
|
-
}
|
|
1961
|
-
/** The weight a voice gives a subject this frame, with the setting already filled in. */
|
|
1962
|
-
weigh(voice, subject, now, held) {
|
|
1963
|
-
this.sending.voice = voice;
|
|
1964
|
-
this.sending.subject = subject;
|
|
1965
|
-
const signal = typeof voice.spec.weight === 'function' ? voice.spec.weight : null;
|
|
1966
|
-
let base = voice.weight;
|
|
1967
|
-
if (signal) {
|
|
1968
|
-
const was = held.replay && last(held.replay, now, true);
|
|
1969
|
-
base = was ? was.value : signal(subject, voice.setting);
|
|
1970
|
-
if (signal.input && !was)
|
|
1971
|
-
this.record(held, base);
|
|
1972
|
-
}
|
|
1973
|
-
const fade = this.envelope(voice, now, held.shown);
|
|
1974
|
-
return clampWeight(base * (voice.parts === null ? fade : fade * this.parting(voice, subject, now)));
|
|
1975
|
-
}
|
|
1976
|
-
/**
|
|
1977
|
-
* One live voice's delta for one subject this frame, through its record for the subject, or null
|
|
1978
|
-
* when it does not reach. Its weight is left in `w`.
|
|
1979
|
-
*/
|
|
1980
|
-
influence(voice, subject, now, held) {
|
|
1981
|
-
held.weight = 0;
|
|
1982
|
-
if (!held.reaches)
|
|
1983
|
-
return null;
|
|
1984
|
-
if (voice.out?.rest && held.rested)
|
|
1985
|
-
return null;
|
|
1986
|
-
let elapsed = voice.elapsedAt(now) - held.delay;
|
|
1987
|
-
// A held subject's clock stands still at the edge it holds: -1 before, 1 after, 0 playing.
|
|
1988
|
-
let still = 0;
|
|
1989
|
-
if (elapsed < 0) {
|
|
1990
|
-
if (!voice.holdsBefore)
|
|
1991
|
-
return null;
|
|
1992
|
-
elapsed = 0;
|
|
1993
|
-
still = -1;
|
|
1994
|
-
}
|
|
1995
|
-
else if (voice.holdsAfter && elapsed > voice.span) {
|
|
1996
|
-
elapsed = voice.span;
|
|
1997
|
-
still = 1;
|
|
1998
|
-
}
|
|
1999
|
-
const period = voice.patch.period;
|
|
2000
|
-
const phase = phaseAt(elapsed, period, voice.passes);
|
|
2001
|
-
const pass = passAt(elapsed, period, voice.passes);
|
|
2002
|
-
this.prime(voice, held, now, elapsed, pass);
|
|
2003
|
-
const setting = voice.setting;
|
|
2004
|
-
const weight = this.weigh(voice, subject, now, held);
|
|
2005
|
-
setting.weight = weight;
|
|
2006
|
-
held.weight = weight;
|
|
2007
|
-
if (held.probed === now && held.delta && held.seeks === voice.seeks) {
|
|
2008
|
-
if (voice.holder !== held && voice.scratch.length > 0)
|
|
2009
|
-
this.keyed(voice, subject, held);
|
|
2010
|
-
this.w = weight;
|
|
2011
|
-
return held.delta;
|
|
2012
|
-
}
|
|
2013
|
-
const keptBefore = reading.kept;
|
|
2014
|
-
const history = this.opts.history;
|
|
2015
|
-
if (history?.inputs && voice.patch.reads !== undefined && !this.projecting)
|
|
2016
|
-
this.recordHost(now);
|
|
2017
|
-
reading.horizon = this.horizonFor(voice, held.delay, now);
|
|
2018
|
-
const tick = this.opts.stepMs;
|
|
2019
|
-
if (voice.patch.step && held.probed !== now && still >= 0) {
|
|
2020
|
-
// Held after, it steps once more to where its last pass ended, and no further.
|
|
2021
|
-
const to = still === 1 ? voice.timeAt(held.delay + voice.span) : now;
|
|
2022
|
-
if (tick !== undefined && tick > 0 && !this.reducedNow)
|
|
2023
|
-
this.tick(voice, subject, held, tick, to);
|
|
2024
|
-
else if (still === 0 ? now !== held.stepped : to > held.stepped) {
|
|
2025
|
-
if (still === 1)
|
|
2026
|
-
setting.dt = this.capped(to - held.stepped);
|
|
2027
|
-
voice.patch.step(held.state, setting.dt, subject, setting);
|
|
2028
|
-
held.stepped = to;
|
|
2029
|
-
}
|
|
2030
|
-
}
|
|
2031
|
-
let delta;
|
|
2032
|
-
if (voice.built) {
|
|
2033
|
-
held.phase = phase;
|
|
2034
|
-
delta = this.keyed(voice, subject, held);
|
|
2035
|
-
}
|
|
2036
|
-
else {
|
|
2037
|
-
delta = voice.patch.at(phase, subject, setting);
|
|
2038
|
-
}
|
|
2039
|
-
held.delta = delta;
|
|
2040
|
-
held.probed = now;
|
|
2041
|
-
held.seeks = voice.seeks;
|
|
2042
|
-
if (history !== undefined)
|
|
2043
|
-
this.remember(voice, held);
|
|
2044
|
-
if (reading.kept !== keptBefore && !voice.keeping)
|
|
2045
|
-
this.stateful(voice);
|
|
2046
|
-
if (voice.out?.rest && this.isRest(delta)) {
|
|
2047
|
-
held.weight = 0;
|
|
2048
|
-
held.rested = true;
|
|
2049
|
-
voice.restedCount++;
|
|
2050
|
-
return null;
|
|
2051
|
-
}
|
|
2052
|
-
this.w = weight;
|
|
2053
|
-
return delta;
|
|
2054
|
-
}
|
|
2055
|
-
/** Reads a keys voice's stops at `held.phase` into the subject's delta, through the voice's scratch. */
|
|
2056
|
-
keyed(voice, subject, held) {
|
|
2057
|
-
let base;
|
|
2058
|
-
if (voice.spec.from === 'current') {
|
|
2059
|
-
if (held.base === undefined) {
|
|
2060
|
-
held.base = this.baseFor(voice, subject);
|
|
2061
|
-
held.slope = this.slopeFor(voice, subject);
|
|
2062
|
-
}
|
|
2063
|
-
base = held.base;
|
|
2064
|
-
}
|
|
2065
|
-
const delta = readKeys(voice.built, held.phase, held.delta ?? {}, base, voice.lerps, held.slope, voice.intos, voice.scratch);
|
|
2066
|
-
held.delta = delta;
|
|
2067
|
-
if (voice.scratch.length > 0)
|
|
2068
|
-
voice.holder = held;
|
|
2069
|
-
return delta;
|
|
2070
|
-
}
|
|
2071
|
-
/**
|
|
2072
|
-
* Runs `step` once per whole interval between where this subject last stopped and now, on a grid
|
|
2073
|
-
* counted from when its delay ran out, and leaves the remainder for the next sample. Counting
|
|
2074
|
-
* rather than adding keeps the grid where it is however many samples it is reached through.
|
|
2075
|
-
*/
|
|
2076
|
-
tick(voice, subject, held, tick, to) {
|
|
2077
|
-
const now = this.now;
|
|
2078
|
-
const setting = voice.setting;
|
|
2079
|
-
const frameDt = setting.dt;
|
|
2080
|
-
let n = held.ticks;
|
|
2081
|
-
// The epsilon keeps an interval like 1000 / 120 from landing a hair short of a whole count.
|
|
2082
|
-
const due = Math.floor((to - held.since) / tick + 1e-9);
|
|
2083
|
-
const cap = this.opts.maxDt;
|
|
2084
|
-
if (cap !== undefined) {
|
|
2085
|
-
const most = Math.floor(cap / tick);
|
|
2086
|
-
if (due - n > most)
|
|
2087
|
-
n = due - most;
|
|
2088
|
-
}
|
|
2089
|
-
const step = voice.patch.step;
|
|
2090
|
-
setting.dt = tick;
|
|
2091
|
-
for (n++; n <= due; n++) {
|
|
2092
|
-
setting.timestamp = held.since + n * tick;
|
|
2093
|
-
step(held.state, tick, subject, setting);
|
|
2094
|
-
}
|
|
2095
|
-
held.ticks = due > held.ticks ? due : held.ticks;
|
|
2096
|
-
held.stepped = held.since + held.ticks * tick;
|
|
2097
|
-
setting.timestamp = now;
|
|
2098
|
-
setting.dt = frameDt;
|
|
2099
|
-
}
|
|
2100
|
-
/**
|
|
2101
|
-
* Stop 0 for a retargeting voice: the subject's pose in the frame before this voice contributes.
|
|
2102
|
-
* With no such frame on record — a voice cued before the subject was ever probed, or a host that
|
|
2103
|
-
* samples into its own object — it is this frame's pose with every other voice folded in.
|
|
2104
|
-
*/
|
|
2105
|
-
baseFor(voice, subject) {
|
|
2106
|
-
let prior = this.pose.get(subject)?.pose;
|
|
2107
|
-
if (prior === undefined && !this.folding.has(voice.id)) {
|
|
2108
|
-
this.folding.add(voice.id);
|
|
2109
|
-
try {
|
|
2110
|
-
prior = this.fold(subject, undefined, false, voice.id);
|
|
2111
|
-
}
|
|
2112
|
-
finally {
|
|
2113
|
-
this.folding.delete(voice.id);
|
|
2114
|
-
}
|
|
2115
|
-
}
|
|
2116
|
-
const base = {};
|
|
2117
|
-
for (const key of voice.patch.writes) {
|
|
2118
|
-
const channel = this.kit[key];
|
|
2119
|
-
const value = prior?.[key];
|
|
2120
|
-
const fallback = channel.rest;
|
|
2121
|
-
if (value !== undefined)
|
|
2122
|
-
base[key] = copy(value);
|
|
2123
|
-
else if (fallback !== undefined)
|
|
2124
|
-
base[key] = copy(fallback);
|
|
2125
|
-
}
|
|
2126
|
-
return base;
|
|
2127
|
-
}
|
|
2128
|
-
/**
|
|
2129
|
-
* How fast each numeric channel this voice writes was moving, from the subject's last two probed
|
|
2130
|
-
* poses, in units per ms. Undefined with fewer than two on record, which starts the voice at rest.
|
|
2131
|
-
*/
|
|
2132
|
-
slopeFor(voice, subject) {
|
|
2133
|
-
const rec = this.pose.get(subject);
|
|
2134
|
-
if (rec?.prev === undefined || !(rec.at > rec.prevAt))
|
|
2135
|
-
return undefined;
|
|
2136
|
-
const dt = rec.at - rec.prevAt;
|
|
2137
|
-
const now = rec.pose;
|
|
2138
|
-
const before = rec.prev;
|
|
2139
|
-
const slope = {};
|
|
2140
|
-
for (const key of voice.patch.writes) {
|
|
2141
|
-
const a = before[key];
|
|
2142
|
-
const b = now[key];
|
|
2143
|
-
if (typeof a === 'number' && typeof b === 'number')
|
|
2144
|
-
slope[key] = (b - a) / dt;
|
|
2145
|
-
else if (Array.isArray(a) && Array.isArray(b) && a.length === b.length)
|
|
2146
|
-
slope[key] = b.map((v, i) => (v - a[i]) / dt);
|
|
2147
|
-
}
|
|
2148
|
-
return slope;
|
|
2149
|
-
}
|
|
2150
|
-
isRest(delta) {
|
|
2151
|
-
for (const key of Object.keys(delta)) {
|
|
2152
|
-
if (delta[key] === undefined)
|
|
2153
|
-
continue;
|
|
2154
|
-
const channel = this.kit[key];
|
|
2155
|
-
if (channel.rest === undefined)
|
|
2156
|
-
return false;
|
|
2157
|
-
if (!near(delta[key], channel.rest))
|
|
2158
|
-
return false;
|
|
2159
|
-
}
|
|
2160
|
-
return true;
|
|
2161
|
-
}
|
|
2162
|
-
/** Folds a locus's members into one influence through each channel's own `lerp`. */
|
|
2163
|
-
foldLocus(members) {
|
|
2164
|
-
let sum = 0;
|
|
2165
|
-
for (const m of members)
|
|
2166
|
-
sum += m.weight;
|
|
2167
|
-
const delta = {};
|
|
2168
|
-
if (sum <= 0)
|
|
2169
|
-
return { delta, weight: 0 };
|
|
2170
|
-
const taken = new Map();
|
|
2171
|
-
for (const member of members) {
|
|
2172
|
-
if (member.weight <= 0)
|
|
2173
|
-
continue;
|
|
2174
|
-
for (const key of Object.keys(member.delta)) {
|
|
2175
|
-
if (member.delta[key] === undefined)
|
|
2176
|
-
continue;
|
|
2177
|
-
const channel = this.kit[key];
|
|
2178
|
-
const held = taken.get(key);
|
|
2179
|
-
if (held === undefined) {
|
|
2180
|
-
delta[key] = copy(member.delta[key]);
|
|
2181
|
-
taken.set(key, member.weight);
|
|
2182
|
-
continue;
|
|
2183
|
-
}
|
|
2184
|
-
const total = held + member.weight;
|
|
2185
|
-
delta[key] = channel.lerp(delta[key], member.delta[key], member.weight / total);
|
|
2186
|
-
taken.set(key, total);
|
|
2187
|
-
}
|
|
2188
|
-
}
|
|
2189
|
-
return { delta, weight: sum > 1 ? 1 : sum };
|
|
2190
|
-
}
|
|
2191
|
-
/** Whether a rest-less channel's influence is switched on, across a band rather than an edge. */
|
|
2192
|
-
passes(was, w) {
|
|
2193
|
-
const { on, off } = this.band;
|
|
2194
|
-
return w >= on ? true : w <= off ? false : (was ?? w >= on);
|
|
2195
|
-
}
|
|
2196
|
-
/**
|
|
2197
|
-
* The first of this subject's records, linked through every live voice that reaches it. A voice
|
|
2198
|
-
* is asked once, through the record it keeps for the subject anyway; a pending one is linked when
|
|
2199
|
-
* it goes live, since `target` is asked on first sight, and sight only comes once a voice plays
|
|
2200
|
-
* or holds before.
|
|
2201
|
-
*/
|
|
2202
|
-
chain(subject, now) {
|
|
2203
|
-
const was = this.chains.get(subject);
|
|
2204
|
-
if (was !== undefined && was.version === this.version)
|
|
2205
|
-
return was;
|
|
2206
|
-
let first = null;
|
|
2207
|
-
let prev = null;
|
|
2208
|
-
const link = (voice) => {
|
|
2209
|
-
const state = voice.state;
|
|
2210
|
-
if (state === 'done' || (state === 'pending' && !voice.holdsBefore))
|
|
2211
|
-
return;
|
|
2212
|
-
const held = this.held(voice, subject, now);
|
|
2213
|
-
if (!held.reaches)
|
|
2214
|
-
return;
|
|
2215
|
-
held.voice = voice;
|
|
2216
|
-
held.next = null;
|
|
2217
|
-
if (prev === null)
|
|
2218
|
-
first = held;
|
|
2219
|
-
else
|
|
2220
|
-
prev.next = held;
|
|
2221
|
-
prev = held;
|
|
2222
|
-
};
|
|
2223
|
-
const named = this.naming === 0 ? undefined : this.named.get(subject);
|
|
2224
|
-
let j = 0;
|
|
2225
|
-
for (const voice of this.general) {
|
|
2226
|
-
while (named !== undefined && j < named.length && named[j].id < voice.id)
|
|
2227
|
-
link(named[j++]);
|
|
2228
|
-
link(voice);
|
|
2229
|
-
}
|
|
2230
|
-
while (named !== undefined && j < named.length)
|
|
2231
|
-
link(named[j++]);
|
|
2232
|
-
const head = first ?? stub();
|
|
2233
|
-
head.version = this.version;
|
|
2234
|
-
if (was !== undefined && was !== head) {
|
|
2235
|
-
head.loci = was.loci;
|
|
2236
|
-
was.loci = null;
|
|
2237
|
-
head.slot = was.slot;
|
|
2238
|
-
was.slot = -1;
|
|
2239
|
-
}
|
|
2240
|
-
else if (was === undefined && this.lanes !== null && head.slot < 0)
|
|
2241
|
-
head.slot = this.lanes.number(subject);
|
|
2242
|
-
if (was !== head)
|
|
2243
|
-
this.chains.set(subject, head);
|
|
2244
|
-
return head;
|
|
2245
|
-
}
|
|
2246
|
-
/** Whether a voice reaches a subject by what its spec says: the subjects it names, or its `target`. */
|
|
2247
|
-
aims(voice, subject) {
|
|
2248
|
-
// A read back to before a subject left still finds it reached.
|
|
2249
|
-
const left = voice.parted?.get(subject);
|
|
2250
|
-
if (left !== undefined && !(this.now < left))
|
|
2251
|
-
return false;
|
|
2252
|
-
if (voice.named !== null)
|
|
2253
|
-
return voice.named.has(subject);
|
|
2254
|
-
return voice.spec.target ? voice.spec.target(subject) : true;
|
|
2255
|
-
}
|
|
2256
|
-
/**
|
|
2257
|
-
* A voice joined or left the list, or started or stopped waiting: the chains it is linked into
|
|
2258
|
-
* are relinked, every subject's for a voice over all of them and only its own for one naming
|
|
2259
|
-
* its subjects, and lanes take it on or off.
|
|
2260
|
-
*/
|
|
2261
|
-
changed(voice) {
|
|
2262
|
-
this.lanes?.touch(voice);
|
|
2263
|
-
if (voice.named === null) {
|
|
2264
|
-
this.version++;
|
|
2265
|
-
return;
|
|
2266
|
-
}
|
|
2267
|
-
for (const subject of voice.named) {
|
|
2268
|
-
const head = this.chains.get(subject);
|
|
2269
|
-
if (head !== undefined)
|
|
2270
|
-
head.version = Number.NaN;
|
|
2271
|
-
}
|
|
2272
|
-
this.relinks++;
|
|
2273
|
-
}
|
|
2274
|
-
/** Files a voice under each subject it names, or among the voices that name none. */
|
|
2275
|
-
index(voice) {
|
|
2276
|
-
if (voice.named === null) {
|
|
2277
|
-
this.general.push(voice);
|
|
2278
|
-
return;
|
|
2279
|
-
}
|
|
2280
|
-
this.naming++;
|
|
2281
|
-
for (const subject of voice.named) {
|
|
2282
|
-
const list = this.named.get(subject);
|
|
2283
|
-
if (list === undefined)
|
|
2284
|
-
this.named.set(subject, [voice]);
|
|
2285
|
-
else
|
|
2286
|
-
list.push(voice);
|
|
2287
|
-
}
|
|
2288
|
-
}
|
|
2289
|
-
unindex(voice) {
|
|
2290
|
-
this.naming--;
|
|
2291
|
-
for (const subject of voice.named) {
|
|
2292
|
-
const list = this.named.get(subject);
|
|
2293
|
-
if (list === undefined)
|
|
2294
|
-
continue;
|
|
2295
|
-
const i = list.indexOf(voice);
|
|
2296
|
-
if (i >= 0)
|
|
2297
|
-
list.splice(i, 1);
|
|
2298
|
-
if (list.length === 0)
|
|
2299
|
-
this.named.delete(subject);
|
|
2300
|
-
}
|
|
2301
|
-
}
|
|
2302
423
|
/** Voices whose stop 0 is mid-computation, so a fold for one cannot re-enter itself. */
|
|
2303
424
|
folding = new Set();
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
const v = pose[key];
|
|
2334
|
-
if (typeof v === 'number')
|
|
2335
|
-
pose[key] = v < lo ? lo : v > hi ? hi : v;
|
|
2336
|
-
else if (Array.isArray(v))
|
|
2337
|
-
for (let i = 0; i < v.length; i++) {
|
|
2338
|
-
const x = v[i];
|
|
2339
|
-
v[i] = x < lo ? lo : x > hi ? hi : x;
|
|
2340
|
-
}
|
|
2341
|
-
}
|
|
2342
|
-
return pose;
|
|
2343
|
-
}
|
|
2344
|
-
fold(subject, out, dry = false, except) {
|
|
2345
|
-
const pose = this.folded(subject, out, dry, except);
|
|
2346
|
-
return (this.bounded.length === 0 ? pose : this.clamp(pose));
|
|
2347
|
-
}
|
|
2348
|
-
folded(subject, out, dry = false, except) {
|
|
2349
|
-
const now = this.now;
|
|
2350
|
-
const head = Number.isNaN(now) ? null : this.chain(subject, now);
|
|
2351
|
-
// A subject numbered since the frame's fill folds every voice here, laned ones included.
|
|
2352
|
-
const laned = head !== null && this.lanes?.prepare(head.slot, subject, now, this.version) === true;
|
|
2353
|
-
return this.foldWith(subject, out ?? {}, head, laned, dry, except);
|
|
2354
|
-
}
|
|
2355
|
-
/** The fold after a subject's chain is linked and its lanes asked whether it reads from them. */
|
|
2356
|
-
foldWith(subject, out, head, laned, dry, except) {
|
|
2357
|
-
const now = this.now;
|
|
2358
|
-
const pose = out;
|
|
2359
|
-
const lanes = this.lanes;
|
|
2360
|
-
const skip = laned ? lanes.copies : null;
|
|
2361
|
-
for (let i = 0; i < this.names.length; i++) {
|
|
2362
|
-
if (skip?.[i])
|
|
2363
|
-
continue;
|
|
2364
|
-
const rest = this.channels[i].rest;
|
|
2365
|
-
const key = this.names[i];
|
|
2366
|
-
// Never `delete`: it drops a reused out object into dictionary mode for good.
|
|
2367
|
-
if (rest !== undefined)
|
|
2368
|
-
pose[key] = copy(rest);
|
|
2369
|
-
else if (pose[key] !== undefined)
|
|
2370
|
-
pose[key] = undefined;
|
|
2371
|
-
}
|
|
2372
|
-
if (head === null)
|
|
2373
|
-
return pose;
|
|
2374
|
-
if (laned) {
|
|
2375
|
-
lanes.copy(head.slot, pose);
|
|
2376
|
-
if (lanes.whole)
|
|
2377
|
-
return pose;
|
|
2378
|
-
}
|
|
2379
|
-
if (this.loci === 0) {
|
|
2380
|
-
for (let held = head; held !== null; held = held.next) {
|
|
2381
|
-
const voice = held.voice;
|
|
2382
|
-
if (voice === null || (laned && voice.laned))
|
|
2383
|
-
continue;
|
|
2384
|
-
if (voice.id === except || voice.state === 'done')
|
|
2385
|
-
continue;
|
|
2386
|
-
const delta = this.read(voice, subject, now, dry, held);
|
|
2387
|
-
if (delta === null || this.w <= 0)
|
|
2388
|
-
continue;
|
|
2389
|
-
this.apply(pose, voice, held, delta, this.w);
|
|
2390
|
-
}
|
|
2391
|
-
return pose;
|
|
2392
|
-
}
|
|
2393
|
-
const order = [];
|
|
2394
|
-
const weights = [];
|
|
2395
|
-
const loci = new Map();
|
|
2396
|
-
for (let held = head; held !== null; held = held.next) {
|
|
2397
|
-
const voice = held.voice;
|
|
2398
|
-
if (voice === null || (laned && voice.laned))
|
|
2399
|
-
continue;
|
|
2400
|
-
if (voice.id === except || voice.state === 'done')
|
|
2401
|
-
continue;
|
|
2402
|
-
const delta = this.read(voice, subject, now, dry, held);
|
|
2403
|
-
if (delta === null)
|
|
2404
|
-
continue;
|
|
2405
|
-
const locus = voice.spec.locus;
|
|
2406
|
-
if (locus === undefined) {
|
|
2407
|
-
order.push({ voice, held, delta });
|
|
2408
|
-
weights.push(this.w);
|
|
2409
|
-
continue;
|
|
2410
|
-
}
|
|
2411
|
-
const members = loci.get(locus);
|
|
2412
|
-
if (members)
|
|
2413
|
-
members.push({ delta, weight: this.w });
|
|
2414
|
-
else {
|
|
2415
|
-
loci.set(locus, [{ delta, weight: this.w }]);
|
|
2416
|
-
order.push(locus);
|
|
2417
|
-
weights.push(0);
|
|
2418
|
-
}
|
|
2419
|
-
}
|
|
2420
|
-
let bands = head.loci;
|
|
2421
|
-
if (bands === null) {
|
|
2422
|
-
bands = new Map();
|
|
2423
|
-
head.loci = bands;
|
|
2424
|
-
}
|
|
2425
|
-
for (let n = 0; n < order.length; n++) {
|
|
2426
|
-
const at = order[n];
|
|
2427
|
-
if (typeof at !== 'string') {
|
|
2428
|
-
if (weights[n] > 0)
|
|
2429
|
-
this.apply(pose, at.voice, at.held, at.delta, weights[n]);
|
|
2430
|
-
continue;
|
|
2431
|
-
}
|
|
2432
|
-
const { delta, weight } = this.foldLocus(loci.get(at));
|
|
2433
|
-
if (weight <= 0)
|
|
2434
|
-
continue;
|
|
2435
|
-
for (const key of Object.keys(delta)) {
|
|
2436
|
-
const channel = this.kit[key];
|
|
2437
|
-
const value = delta[key];
|
|
2438
|
-
if (value === undefined)
|
|
2439
|
-
continue;
|
|
2440
|
-
if (channel.rest !== undefined && channel.scale) {
|
|
2441
|
-
// pose[key] is the copy of rest this fold made, so it is ours to write into.
|
|
2442
|
-
pose[key] = channel.fold
|
|
2443
|
-
? channel.fold(pose[key], value, weight)
|
|
2444
|
-
: channel.merge(pose[key], channel.scale(value, weight));
|
|
2445
|
-
continue;
|
|
2446
|
-
}
|
|
2447
|
-
const band = `${at}:${key}`;
|
|
2448
|
-
const on = this.passes(bands.get(band), weight);
|
|
2449
|
-
bands.set(band, on);
|
|
2450
|
-
if (on)
|
|
2451
|
-
pose[key] = pose[key] === undefined ? copy(value) : channel.merge(pose[key], value);
|
|
2452
|
-
}
|
|
2453
|
-
}
|
|
2454
|
-
return pose;
|
|
2455
|
-
}
|
|
2456
|
-
/** `influence`, or for a dry fold a subject already probed this frame, without advancing it. */
|
|
2457
|
-
read(voice, subject, now, dry, held) {
|
|
2458
|
-
if (dry) {
|
|
2459
|
-
if (held.reaches && held.probed === now && held.delta && held.seeks === voice.seeks) {
|
|
2460
|
-
const setting = voice.setting;
|
|
2461
|
-
setting.timestamp = now;
|
|
2462
|
-
setting.dt = 0;
|
|
2463
|
-
setting.elapsed = voice.elapsedAt(now);
|
|
2464
|
-
setting.pass = 0;
|
|
2465
|
-
setting.weight = 0;
|
|
2466
|
-
setting.state = held.state;
|
|
2467
|
-
setting.keep = held.keep;
|
|
2468
|
-
this.w = this.weigh(voice, subject, now, held);
|
|
2469
|
-
held.weight = this.w;
|
|
2470
|
-
if (voice.holder !== held && voice.scratch.length > 0)
|
|
2471
|
-
this.keyed(voice, subject, held);
|
|
2472
|
-
return held.delta;
|
|
2473
|
-
}
|
|
2474
|
-
}
|
|
2475
|
-
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;
|
|
2476
454
|
}
|
|
2477
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 });
|
|
2478
500
|
/**
|
|
2479
501
|
* The one that ships: every form on the CPU, probed per subject on demand.
|
|
2480
502
|
*
|