@msb235/blits 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +96 -0
- package/README.md +9 -2
- package/dist/channels.d.ts +41 -7
- package/dist/channels.js +184 -29
- package/dist/clock.d.ts +19 -0
- package/dist/clock.js +44 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1 -1
- package/dist/lanes.d.ts +207 -0
- package/dist/lanes.js +928 -0
- package/dist/mixer.d.ts +185 -2
- package/dist/mixer.js +1264 -139
- package/dist/motion.d.ts +169 -9
- package/dist/motion.js +564 -112
- package/dist/numbers.d.ts +22 -0
- package/dist/numbers.js +64 -0
- package/dist/patch.d.ts +13 -2
- package/dist/patch.js +44 -18
- package/dist/reading.d.ts +23 -0
- package/dist/reading.js +23 -0
- package/dist/types.d.ts +242 -11
- package/docs/schema.html +119 -24
- package/package.json +3 -6
package/dist/lanes.js
ADDED
|
@@ -0,0 +1,928 @@
|
|
|
1
|
+
import { foldNumber, numericOf } from './channels.js';
|
|
2
|
+
import { clampWeight, passAt, phaseAt } from './clock.js';
|
|
3
|
+
import { motionOf } from './motion.js';
|
|
4
|
+
import { absent, Numbers } from './numbers.js';
|
|
5
|
+
import { readKeys } from './patch.js';
|
|
6
|
+
import { reading } from './reading.js';
|
|
7
|
+
/**
|
|
8
|
+
* Which channels run as lanes and which voices run on them: the fixed point where every voice
|
|
9
|
+
* writing a laned channel fits and writes only laned channels. Only channels some laned voice
|
|
10
|
+
* writes are laned.
|
|
11
|
+
*/
|
|
12
|
+
export function qualify(numeric, voices) {
|
|
13
|
+
const open = numeric.slice();
|
|
14
|
+
let changed = true;
|
|
15
|
+
while (changed) {
|
|
16
|
+
changed = false;
|
|
17
|
+
for (const v of voices) {
|
|
18
|
+
if (v.fits && v.slots.every((s) => open[s]))
|
|
19
|
+
continue;
|
|
20
|
+
for (const s of v.slots)
|
|
21
|
+
if (open[s]) {
|
|
22
|
+
open[s] = false;
|
|
23
|
+
changed = true;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const laned = new Set();
|
|
28
|
+
const channels = numeric.map(() => false);
|
|
29
|
+
for (const v of voices) {
|
|
30
|
+
if (!v.fits || !v.slots.every((s) => open[s]))
|
|
31
|
+
continue;
|
|
32
|
+
laned.add(v.id);
|
|
33
|
+
for (const s of v.slots)
|
|
34
|
+
channels[s] = true;
|
|
35
|
+
}
|
|
36
|
+
return { channels, voices: laned };
|
|
37
|
+
}
|
|
38
|
+
export function clampRun(out, at, n, [lo, hi]) {
|
|
39
|
+
for (let i = at; i < at + n; i++) {
|
|
40
|
+
const x = out[i];
|
|
41
|
+
out[i] = x < lo ? lo : x > hi ? hi : x;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const STRIDE = 7;
|
|
45
|
+
const DELAY = 0;
|
|
46
|
+
const SINCE = 1;
|
|
47
|
+
/** The weight the last fill gave the subject. */
|
|
48
|
+
const WEIGHT = 2;
|
|
49
|
+
/** The weight a fill gave it the last time a probe read it from the lane. */
|
|
50
|
+
const PROBED = 3;
|
|
51
|
+
const MSLOT = 4;
|
|
52
|
+
/** For a motion voice, the last fill that sampled the subject, and the voice's seeks then. */
|
|
53
|
+
const SAMPLED = 5;
|
|
54
|
+
const SEEKS = 6;
|
|
55
|
+
const SLOT = 6;
|
|
56
|
+
const FILLED = 0;
|
|
57
|
+
const SEEN = 1;
|
|
58
|
+
const IDLE = 2;
|
|
59
|
+
const LANE_PROBE = 3;
|
|
60
|
+
const GENERAL_PROBE = 4;
|
|
61
|
+
const LANE_FILL = 5;
|
|
62
|
+
/**
|
|
63
|
+
* Below what share of its subjects probed last frame a lane stops filling, and above what share it
|
|
64
|
+
* fills again; between the two it keeps doing what it did, so a host near the line does not flip
|
|
65
|
+
* every frame. Measured at 10k and 1k subjects (2026-10-02, one local machine): filling breaks even
|
|
66
|
+
* with probing through the general path at about 19% probed for keys at both sizes, 71-77% for a
|
|
67
|
+
* spring, and 63% (1k) to about 100% (10k) for a stateless fn.
|
|
68
|
+
*/
|
|
69
|
+
const SPARSE = { keys: { stop: 0.15, start: 0.25 }, other: { stop: 0.6, start: 0.8 } };
|
|
70
|
+
/** The least prime at least `n`, and 1 below 2. */
|
|
71
|
+
function primeFrom(n) {
|
|
72
|
+
if (n < 2)
|
|
73
|
+
return 1;
|
|
74
|
+
for (let c = n;; c++) {
|
|
75
|
+
let prime = true;
|
|
76
|
+
for (let d = 2; d * d <= c; d++)
|
|
77
|
+
if (c % d === 0) {
|
|
78
|
+
prime = false;
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
if (prime)
|
|
82
|
+
return c;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* What one laned voice keeps per subject it reaches, by position in `list`. A subject gets a
|
|
87
|
+
* position when a probe of it first finds the voice playing, which is when the general path first
|
|
88
|
+
* sees it too.
|
|
89
|
+
*/
|
|
90
|
+
class Lane {
|
|
91
|
+
voice;
|
|
92
|
+
/** The subject number at each position. */
|
|
93
|
+
list = [];
|
|
94
|
+
at = new Map();
|
|
95
|
+
/** Whether too few of its subjects were probed lately to fill it: theirs take the general path. */
|
|
96
|
+
idle = false;
|
|
97
|
+
/** The lines it goes idle and busy at, by its patch form. */
|
|
98
|
+
line;
|
|
99
|
+
/**
|
|
100
|
+
* A `fn` or `keys` voice naming one subject, which shares nothing across subjects for a fill to
|
|
101
|
+
* save, so it stays idle: its subject takes the general path, and the channel stays a lane for
|
|
102
|
+
* the voices that do share.
|
|
103
|
+
*/
|
|
104
|
+
solo;
|
|
105
|
+
/**
|
|
106
|
+
* When the voice started playing on its lane, in the order lanes did; 0 while it waits. A subject
|
|
107
|
+
* checked against every lane up to some epoch has met this one if this one's is no later.
|
|
108
|
+
*/
|
|
109
|
+
epoch = 0;
|
|
110
|
+
/**
|
|
111
|
+
* Per position, `STRIDE` numbers side by side, so one subject's are one read from memory: its
|
|
112
|
+
* delay, its `since`, its weight at the last fill, its weight at the last fill a probe read, a
|
|
113
|
+
* motion patch's own number for it (-1 until asked), and the fill and seeks of its last sample.
|
|
114
|
+
*/
|
|
115
|
+
data = new Float64Array(0);
|
|
116
|
+
records = [];
|
|
117
|
+
/**
|
|
118
|
+
* For a motion voice, per position, the delta its last sample made; null where that sample was
|
|
119
|
+
* kept only in `samples`, until a probe asks for it.
|
|
120
|
+
*/
|
|
121
|
+
deltas = [];
|
|
122
|
+
/** For a motion voice, per position, its last sample's value on each of `axes` axes. */
|
|
123
|
+
samples = new Float64Array(0);
|
|
124
|
+
axes = 0;
|
|
125
|
+
/** What a keys read writes into, reused across subjects. */
|
|
126
|
+
delta = {};
|
|
127
|
+
/** The arrays keyed reads interpolate into: the lane's own, so a probe mid-fill cannot move them. */
|
|
128
|
+
scratch = [];
|
|
129
|
+
/** The laned channel of each channel the voice writes, in `writes` order. */
|
|
130
|
+
chans = [];
|
|
131
|
+
/** The value read for each of `chans`, for the subject being folded. */
|
|
132
|
+
values = [];
|
|
133
|
+
/**
|
|
134
|
+
* Within one fill, the last elapsed placed and the last `since` weighed, with what they gave, so
|
|
135
|
+
* subjects sharing a delay or a start share the arithmetic: the same inputs give the same bits.
|
|
136
|
+
*/
|
|
137
|
+
placed = false;
|
|
138
|
+
placedAt = 0;
|
|
139
|
+
phase = 0;
|
|
140
|
+
pass = 0;
|
|
141
|
+
read = false;
|
|
142
|
+
readAt = 0;
|
|
143
|
+
weighed = false;
|
|
144
|
+
weighedSince = 0;
|
|
145
|
+
fade = 1;
|
|
146
|
+
/** Whether the voice has no fade in or out this fill, so its envelope is 1 for every subject. */
|
|
147
|
+
flat = false;
|
|
148
|
+
/** A motion patch's state, which the lane samples in place of calling the patch. */
|
|
149
|
+
motion;
|
|
150
|
+
constructor(voice) {
|
|
151
|
+
this.voice = voice;
|
|
152
|
+
this.motion = motionOf(voice.patch);
|
|
153
|
+
this.line = voice.built !== null ? SPARSE.keys : SPARSE.other;
|
|
154
|
+
this.solo = voice.named !== null && voice.named.size === 1 && this.motion === undefined;
|
|
155
|
+
this.idle = this.solo;
|
|
156
|
+
}
|
|
157
|
+
positionOf(slot) {
|
|
158
|
+
return this.at.get(slot) ?? -1;
|
|
159
|
+
}
|
|
160
|
+
/** Gives a subject the voice reaches a position, from what its record fixed on first sight. */
|
|
161
|
+
add(slot, held) {
|
|
162
|
+
const p = this.list.length;
|
|
163
|
+
this.list.push(slot);
|
|
164
|
+
this.at.set(slot, p);
|
|
165
|
+
if ((p + 1) * STRIDE > this.data.length) {
|
|
166
|
+
const data = new Float64Array(Math.max(p + 1, (this.data.length / STRIDE) * 2, 4) * STRIDE);
|
|
167
|
+
data.set(this.data);
|
|
168
|
+
this.data = data;
|
|
169
|
+
}
|
|
170
|
+
const o = p * STRIDE;
|
|
171
|
+
this.data[o + DELAY] = held.delay;
|
|
172
|
+
this.data[o + SINCE] = held.since;
|
|
173
|
+
this.data[o + WEIGHT] = 0;
|
|
174
|
+
this.data[o + PROBED] = 0;
|
|
175
|
+
this.data[o + MSLOT] = -1;
|
|
176
|
+
this.data[o + SAMPLED] = -1;
|
|
177
|
+
this.records[p] = held;
|
|
178
|
+
this.deltas[p] = null;
|
|
179
|
+
}
|
|
180
|
+
/** A subject lost its number: the last position moves into its place. */
|
|
181
|
+
remove(slot) {
|
|
182
|
+
const p = this.at.get(slot);
|
|
183
|
+
if (p === undefined)
|
|
184
|
+
return;
|
|
185
|
+
const last = this.list.length - 1;
|
|
186
|
+
if (p !== last) {
|
|
187
|
+
const moved = this.list[last];
|
|
188
|
+
this.list[p] = moved;
|
|
189
|
+
this.at.set(moved, p);
|
|
190
|
+
this.data.copyWithin(p * STRIDE, last * STRIDE, (last + 1) * STRIDE);
|
|
191
|
+
this.records[p] = this.records[last];
|
|
192
|
+
this.deltas[p] = this.deltas[last] ?? null;
|
|
193
|
+
const n = this.axes;
|
|
194
|
+
if (n > 0)
|
|
195
|
+
this.samples.copyWithin(p * n, last * n, (last + 1) * n);
|
|
196
|
+
}
|
|
197
|
+
this.list.pop();
|
|
198
|
+
this.at.delete(slot);
|
|
199
|
+
this.records[last] = undefined;
|
|
200
|
+
this.deltas[last] = null;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* The lanes of one mix: subject numbers, which channels and voices run on lanes, and the values a
|
|
205
|
+
* frame's fill leaves for probes to copy.
|
|
206
|
+
*/
|
|
207
|
+
export class Lanes {
|
|
208
|
+
host;
|
|
209
|
+
numbers;
|
|
210
|
+
lanes = [];
|
|
211
|
+
/** The lanes a fill visits: every lane but the solo ones. */
|
|
212
|
+
runs = [];
|
|
213
|
+
/** The lanes over every subject that have started playing, by epoch. */
|
|
214
|
+
dense = [];
|
|
215
|
+
byId = new Map();
|
|
216
|
+
laned = [];
|
|
217
|
+
/** By kit slot, the laned channel there. */
|
|
218
|
+
bySlot = [];
|
|
219
|
+
/** By kit slot, whether a fill leaves the channel's values for probes to copy. */
|
|
220
|
+
copies = [];
|
|
221
|
+
/** Whether every voice in the mix is on a lane, so a filled subject has nothing left to fold. */
|
|
222
|
+
whole = false;
|
|
223
|
+
cap = 0;
|
|
224
|
+
epochs = 0;
|
|
225
|
+
/**
|
|
226
|
+
* Per subject number, `SLOT` numbers side by side, so a probe reads one place in memory: which
|
|
227
|
+
* fill last wrote it (0 for none); the latest lane epoch a probe of it has checked it against;
|
|
228
|
+
* how many idle lanes reach it, since a probe reads lanes only where none do; and the probe count
|
|
229
|
+
* at its last probe read from the lanes and at its last probe folded by the general path, with
|
|
230
|
+
* the fill the former read, which say whether a lane or a record holds what `weightOf` reports.
|
|
231
|
+
*/
|
|
232
|
+
per = new Float64Array(0);
|
|
233
|
+
fills = 0;
|
|
234
|
+
probes = 0;
|
|
235
|
+
/**
|
|
236
|
+
* Each subject by number as last probed, held from that probe until the next fill uses it, so a
|
|
237
|
+
* fill rarely looks one up through its weak reference, which costs on every look. A host that
|
|
238
|
+
* stops syncing keeps at most one frame's probed subjects alive until its next fill, and none
|
|
239
|
+
* once no lane remains.
|
|
240
|
+
*/
|
|
241
|
+
subjects = [];
|
|
242
|
+
/** Whether a probe has held a subject since the last fill let them go. */
|
|
243
|
+
holding = false;
|
|
244
|
+
/** While a fill runs, so a probe a patch makes from inside it takes the general path. */
|
|
245
|
+
filling = false;
|
|
246
|
+
/**
|
|
247
|
+
* Numbers a fill left to the general path: handed out while it ran, or with a voice whose call
|
|
248
|
+
* the general path has to make.
|
|
249
|
+
*/
|
|
250
|
+
late = [];
|
|
251
|
+
keeps = false;
|
|
252
|
+
now = Number.NaN;
|
|
253
|
+
filledAt = Number.NaN;
|
|
254
|
+
/** `reading.moved` at the last fill, so a retarget or push between two probes refills. */
|
|
255
|
+
moved = 0;
|
|
256
|
+
/** The `now` of the latest probe, and the probe count before its first, to tell a probe this frame. */
|
|
257
|
+
frameAt = Number.NaN;
|
|
258
|
+
frameProbes = 0;
|
|
259
|
+
/** The probe counts the last frame with a probe began and ended at, which lanes go idle by. */
|
|
260
|
+
lastFrom = 0;
|
|
261
|
+
lastTo = 0;
|
|
262
|
+
/** Set when a frame's first probe arrives, so its first fill decides which lanes go idle. */
|
|
263
|
+
fresh = false;
|
|
264
|
+
/** Subjects probed this frame and in the last frame with a probe, counted once each. */
|
|
265
|
+
distinct = 0;
|
|
266
|
+
lastDistinct = 0;
|
|
267
|
+
/** Numbered subjects alive. */
|
|
268
|
+
live = 0;
|
|
269
|
+
filledVersion = Number.NaN;
|
|
270
|
+
qualifiedVersion = Number.NaN;
|
|
271
|
+
constructor(host) {
|
|
272
|
+
this.host = host;
|
|
273
|
+
this.numbers = new Numbers((slot) => this.forget(slot));
|
|
274
|
+
}
|
|
275
|
+
/** A change that may move a voice on or off its lane: qualify again, then fill again. */
|
|
276
|
+
invalidate() {
|
|
277
|
+
this.qualifiedVersion = Number.NaN;
|
|
278
|
+
this.filledVersion = Number.NaN;
|
|
279
|
+
}
|
|
280
|
+
/** A change to a laned voice's weight or clock between probes: fill again. */
|
|
281
|
+
refill() {
|
|
282
|
+
this.filledVersion = Number.NaN;
|
|
283
|
+
}
|
|
284
|
+
/** Numbers a subject the mix sees for the first time. */
|
|
285
|
+
number(subject) {
|
|
286
|
+
const slot = this.numbers.take(subject);
|
|
287
|
+
this.live++;
|
|
288
|
+
this.grow(slot + 1);
|
|
289
|
+
this.per[slot * SLOT + FILLED] = 0;
|
|
290
|
+
this.per[slot * SLOT + SEEN] = 0;
|
|
291
|
+
this.per[slot * SLOT + IDLE] = 0;
|
|
292
|
+
this.per[slot * SLOT + LANE_PROBE] = 0;
|
|
293
|
+
this.per[slot * SLOT + GENERAL_PROBE] = 0;
|
|
294
|
+
this.per[slot * SLOT + LANE_FILL] = 0;
|
|
295
|
+
if (this.filling)
|
|
296
|
+
this.late.push(slot);
|
|
297
|
+
return slot;
|
|
298
|
+
}
|
|
299
|
+
release(slot) {
|
|
300
|
+
this.numbers.release(slot);
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* A laned voice's weight for a subject at the last frame a probe read it from the lane; undefined
|
|
304
|
+
* where the subject's record holds it instead, because its last probe took the general path or
|
|
305
|
+
* the voice is not on a lane.
|
|
306
|
+
*/
|
|
307
|
+
weightOf(id, slot) {
|
|
308
|
+
const lane = this.byId.get(id);
|
|
309
|
+
if (lane === undefined || slot < 0)
|
|
310
|
+
return undefined;
|
|
311
|
+
return this.reported(lane, slot);
|
|
312
|
+
}
|
|
313
|
+
reported(lane, slot) {
|
|
314
|
+
const o = slot * SLOT;
|
|
315
|
+
if (!(this.per[o + LANE_PROBE] > this.per[o + GENERAL_PROBE]))
|
|
316
|
+
return undefined;
|
|
317
|
+
const p = lane.positionOf(slot);
|
|
318
|
+
if (p < 0)
|
|
319
|
+
return undefined;
|
|
320
|
+
return lane.data[p * STRIDE + (this.per[o + LANE_FILL] === this.fills ? WEIGHT : PROBED)];
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Fills the lanes once a frame, and says whether this probe of a subject reads its values from
|
|
324
|
+
* them. One that a lane has not met yet, newly numbered or newly reached, takes the general path
|
|
325
|
+
* this frame, which is where it is first seen; so does one probed from inside a fill.
|
|
326
|
+
*/
|
|
327
|
+
prepare(slot, subject, now, version) {
|
|
328
|
+
if (now !== this.frameAt) {
|
|
329
|
+
this.lastFrom = this.frameProbes;
|
|
330
|
+
this.lastTo = this.probes;
|
|
331
|
+
this.lastDistinct = this.distinct;
|
|
332
|
+
this.distinct = 0;
|
|
333
|
+
this.frameAt = now;
|
|
334
|
+
this.frameProbes = this.probes;
|
|
335
|
+
this.fresh = true;
|
|
336
|
+
}
|
|
337
|
+
if (this.filling)
|
|
338
|
+
return this.general(slot);
|
|
339
|
+
if (this.qualifiedVersion !== version)
|
|
340
|
+
this.requalify(version);
|
|
341
|
+
if (this.laned.length === 0)
|
|
342
|
+
return this.general(slot);
|
|
343
|
+
// Only solo lanes: nothing to fill, and every subject takes the general path.
|
|
344
|
+
if (this.runs.length === 0) {
|
|
345
|
+
if (slot < 0)
|
|
346
|
+
return false;
|
|
347
|
+
if (this.per[slot * SLOT + SEEN] < this.epochs)
|
|
348
|
+
this.meet(slot, subject);
|
|
349
|
+
return this.general(slot);
|
|
350
|
+
}
|
|
351
|
+
if (slot >= 0 && !this.probedThisFrame(slot))
|
|
352
|
+
this.distinct++;
|
|
353
|
+
if (this.filledAt !== now || this.filledVersion !== version || this.moved !== reading.moved) {
|
|
354
|
+
this.fillAll(now, version);
|
|
355
|
+
// A patch call in that fill made kept state, which took its voice off its lane: fill without it.
|
|
356
|
+
if (this.qualifiedVersion !== version) {
|
|
357
|
+
this.requalify(version);
|
|
358
|
+
if (this.laned.length === 0)
|
|
359
|
+
return this.general(slot);
|
|
360
|
+
this.fillAll(now, version);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
if (slot < 0)
|
|
364
|
+
return false;
|
|
365
|
+
const probe = ++this.probes;
|
|
366
|
+
const per = this.per;
|
|
367
|
+
const o = slot * SLOT;
|
|
368
|
+
let lane = per[o + FILLED] === this.fills && per[o + IDLE] === 0;
|
|
369
|
+
if (per[o + SEEN] < this.epochs && this.meet(slot, subject)) {
|
|
370
|
+
// The fill ran before the subject had these positions, so it reads the general path all frame.
|
|
371
|
+
per[o + FILLED] = 0;
|
|
372
|
+
lane = false;
|
|
373
|
+
}
|
|
374
|
+
if (lane) {
|
|
375
|
+
per[o + LANE_PROBE] = probe;
|
|
376
|
+
per[o + LANE_FILL] = this.fills;
|
|
377
|
+
this.subjects[slot] = subject;
|
|
378
|
+
this.holding = true;
|
|
379
|
+
}
|
|
380
|
+
else
|
|
381
|
+
per[o + GENERAL_PROBE] = probe;
|
|
382
|
+
return lane;
|
|
383
|
+
}
|
|
384
|
+
general(slot) {
|
|
385
|
+
if (slot >= 0)
|
|
386
|
+
this.per[slot * SLOT + GENERAL_PROBE] = ++this.probes;
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
389
|
+
/** Whether a probe has read the subject since the frame began, on either path. */
|
|
390
|
+
probedThisFrame(slot) {
|
|
391
|
+
const from = this.frameProbes;
|
|
392
|
+
const o = slot * SLOT;
|
|
393
|
+
return (this.per[o + LANE_PROBE] > from || this.per[o + GENERAL_PROBE] > from);
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Writes a subject's values into each column at subject `n`'s places, for a probe that reads from
|
|
397
|
+
* the lanes while every voice is on one: a channel no lane holds is at rest.
|
|
398
|
+
*/
|
|
399
|
+
write(slot, columns, n) {
|
|
400
|
+
const bySlot = this.bySlot;
|
|
401
|
+
for (let k = 0; k < columns.length; k++) {
|
|
402
|
+
const c = columns[k];
|
|
403
|
+
const axes = c.axes;
|
|
404
|
+
const out = c.out;
|
|
405
|
+
const at = n * axes;
|
|
406
|
+
const ch = bySlot[c.slot];
|
|
407
|
+
if (ch === undefined) {
|
|
408
|
+
for (let a = 0; a < axes; a++)
|
|
409
|
+
out[at + a] = c.rest[a];
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
const values = ch.values;
|
|
413
|
+
const base = slot * axes;
|
|
414
|
+
for (let a = 0; a < axes; a++)
|
|
415
|
+
out[at + a] = values[base + a];
|
|
416
|
+
if (c.bounds !== undefined)
|
|
417
|
+
clampRun(out, at, axes, c.bounds);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
/** Writes a subject's laned values into a pose, each array channel into a new array. */
|
|
421
|
+
copy(slot, pose) {
|
|
422
|
+
const laned = this.laned;
|
|
423
|
+
for (let c = 0; c < laned.length; c++) {
|
|
424
|
+
const ch = laned[c];
|
|
425
|
+
const axes = ch.axes;
|
|
426
|
+
if (axes === 1) {
|
|
427
|
+
pose[ch.name] = ch.values[slot];
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
// Made as the general path makes it, a copy of rest, so poses from either path share a shape.
|
|
431
|
+
const arr = [...ch.start];
|
|
432
|
+
const base = slot * axes;
|
|
433
|
+
for (let a = 0; a < axes; a++)
|
|
434
|
+
arr[a] = ch.values[base + a];
|
|
435
|
+
pose[ch.name] = arr;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Gives a probed subject a position on every lane that started playing since the subject was last
|
|
440
|
+
* checked and that reaches it. True when any did, so the probe takes the general path.
|
|
441
|
+
*/
|
|
442
|
+
meet(slot, subject) {
|
|
443
|
+
const from = this.per[slot * SLOT + SEEN];
|
|
444
|
+
this.per[slot * SLOT + SEEN] = this.epochs;
|
|
445
|
+
let met = false;
|
|
446
|
+
const dense = this.dense;
|
|
447
|
+
for (let i = dense.length - 1; i >= 0; i--) {
|
|
448
|
+
const lane = dense[i];
|
|
449
|
+
if (lane.epoch <= from)
|
|
450
|
+
break;
|
|
451
|
+
const held = this.host.meet(lane.voice, subject);
|
|
452
|
+
if (!held.reaches)
|
|
453
|
+
continue;
|
|
454
|
+
lane.add(slot, held);
|
|
455
|
+
if (lane.idle)
|
|
456
|
+
this.reach(slot, 1);
|
|
457
|
+
met = true;
|
|
458
|
+
}
|
|
459
|
+
const naming = this.host.naming(subject);
|
|
460
|
+
if (naming !== undefined)
|
|
461
|
+
for (const voice of naming) {
|
|
462
|
+
const lane = this.byId.get(voice.id);
|
|
463
|
+
if (lane === undefined || lane.epoch <= from || lane.positionOf(slot) >= 0)
|
|
464
|
+
continue;
|
|
465
|
+
lane.add(slot, this.host.meet(voice, subject));
|
|
466
|
+
if (lane.idle)
|
|
467
|
+
this.reach(slot, 1);
|
|
468
|
+
met = true;
|
|
469
|
+
}
|
|
470
|
+
return met;
|
|
471
|
+
}
|
|
472
|
+
/** Counts an idle lane more or fewer reaching a subject. */
|
|
473
|
+
reach(slot, by) {
|
|
474
|
+
const i = slot * SLOT + IDLE;
|
|
475
|
+
this.per[i] = this.per[i] + by;
|
|
476
|
+
}
|
|
477
|
+
forget(slot) {
|
|
478
|
+
this.live--;
|
|
479
|
+
for (const lane of this.lanes)
|
|
480
|
+
lane.remove(slot);
|
|
481
|
+
if (slot < this.cap)
|
|
482
|
+
this.per[slot * SLOT + FILLED] = 0;
|
|
483
|
+
this.subjects[slot] = undefined;
|
|
484
|
+
}
|
|
485
|
+
grow(size) {
|
|
486
|
+
if (size <= this.cap)
|
|
487
|
+
return;
|
|
488
|
+
const cap = Math.max(size, this.cap * 2, 64);
|
|
489
|
+
const per = new Float64Array(cap * SLOT);
|
|
490
|
+
per.set(this.per);
|
|
491
|
+
this.per = per;
|
|
492
|
+
for (const ch of this.laned) {
|
|
493
|
+
const values = new Float64Array(cap * ch.axes);
|
|
494
|
+
values.set(ch.values);
|
|
495
|
+
ch.values = values;
|
|
496
|
+
}
|
|
497
|
+
this.cap = cap;
|
|
498
|
+
}
|
|
499
|
+
requalify(version) {
|
|
500
|
+
const host = this.host;
|
|
501
|
+
this.qualifiedVersion = version;
|
|
502
|
+
this.filledVersion = Number.NaN;
|
|
503
|
+
const present = host.voices.filter((v) => v.state !== 'done');
|
|
504
|
+
const numeric = host.channels.map((c) => numericOf(c) !== undefined);
|
|
505
|
+
const { channels, voices } = qualify(numeric,
|
|
506
|
+
// A voice writing nothing has no lane to fill, so it stays where its calls are made.
|
|
507
|
+
present.map((v) => ({ id: v.id, fits: v.slots.length > 0 && host.fits(v), slots: v.slots })));
|
|
508
|
+
for (const lane of this.lanes)
|
|
509
|
+
if (!voices.has(lane.voice.id))
|
|
510
|
+
this.leave(lane);
|
|
511
|
+
const kept = [];
|
|
512
|
+
for (const v of present) {
|
|
513
|
+
if (!voices.has(v.id))
|
|
514
|
+
continue;
|
|
515
|
+
const lane = this.byId.get(v.id) ?? new Lane(v);
|
|
516
|
+
v.laned = true;
|
|
517
|
+
if (lane.epoch === 0 && (v.state === 'live' || v.state === 'fading'))
|
|
518
|
+
lane.epoch = ++this.epochs;
|
|
519
|
+
kept.push(lane);
|
|
520
|
+
}
|
|
521
|
+
this.lanes = kept;
|
|
522
|
+
this.runs = kept.filter((l) => !l.solo);
|
|
523
|
+
this.dense = kept
|
|
524
|
+
.filter((l) => l.voice.named === null && l.epoch > 0)
|
|
525
|
+
.sort((a, b) => a.epoch - b.epoch);
|
|
526
|
+
this.whole = kept.length === present.length;
|
|
527
|
+
this.byId.clear();
|
|
528
|
+
for (const lane of kept)
|
|
529
|
+
this.byId.set(lane.voice.id, lane);
|
|
530
|
+
// With no lane left no fill comes to let go of the subjects the last probes held.
|
|
531
|
+
if (kept.length === 0)
|
|
532
|
+
this.subjects = [];
|
|
533
|
+
this.laned = [];
|
|
534
|
+
this.bySlot = host.channels.map(() => undefined);
|
|
535
|
+
channels.forEach((on, slot) => {
|
|
536
|
+
if (!on)
|
|
537
|
+
return;
|
|
538
|
+
const channel = host.channels[slot];
|
|
539
|
+
const n = numericOf(channel);
|
|
540
|
+
const rest = channel.rest;
|
|
541
|
+
const ch = {
|
|
542
|
+
name: host.names[slot],
|
|
543
|
+
op: n.op,
|
|
544
|
+
rest: typeof rest === 'number' ? rest : rest[0],
|
|
545
|
+
axes: n.axes,
|
|
546
|
+
values: new Float64Array(this.cap * n.axes),
|
|
547
|
+
start: typeof rest === 'number' ? [] : rest,
|
|
548
|
+
};
|
|
549
|
+
this.laned.push(ch);
|
|
550
|
+
this.bySlot[slot] = ch;
|
|
551
|
+
});
|
|
552
|
+
this.copies.length = 0;
|
|
553
|
+
for (const ch of this.bySlot)
|
|
554
|
+
this.copies.push(ch !== undefined);
|
|
555
|
+
for (const lane of kept) {
|
|
556
|
+
lane.chans = lane.voice.slots.map((s) => this.bySlot[s]);
|
|
557
|
+
lane.values = lane.chans.map(() => undefined);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
/** Hands a voice back to the general path, with the weights `weightOf` reports kept on its records. */
|
|
561
|
+
leave(lane) {
|
|
562
|
+
if (lane.idle)
|
|
563
|
+
this.wake(lane);
|
|
564
|
+
for (let p = 0; p < lane.list.length; p++) {
|
|
565
|
+
const rec = lane.records[p];
|
|
566
|
+
const slot = lane.list[p];
|
|
567
|
+
const w = this.reported(lane, slot);
|
|
568
|
+
if (rec !== undefined && w !== undefined)
|
|
569
|
+
rec.weight = w;
|
|
570
|
+
if (rec !== undefined && lane.motion !== undefined)
|
|
571
|
+
this.settle(lane, p, slot, rec);
|
|
572
|
+
}
|
|
573
|
+
lane.voice.laned = false;
|
|
574
|
+
}
|
|
575
|
+
fillAll(now, version) {
|
|
576
|
+
const host = this.host;
|
|
577
|
+
this.filling = true;
|
|
578
|
+
try {
|
|
579
|
+
this.fills++;
|
|
580
|
+
this.now = now;
|
|
581
|
+
this.filledAt = now;
|
|
582
|
+
this.filledVersion = version;
|
|
583
|
+
const size = this.numbers.size;
|
|
584
|
+
this.keeps = host.keeps;
|
|
585
|
+
const decide = this.fresh;
|
|
586
|
+
this.fresh = false;
|
|
587
|
+
const share = this.live > 0 ? this.lastDistinct / this.live : 1;
|
|
588
|
+
// One pass, deciding and running each lane, since a mix of many small lanes pays per visit.
|
|
589
|
+
let busy = false;
|
|
590
|
+
for (const lane of this.runs) {
|
|
591
|
+
if (decide) {
|
|
592
|
+
// A lane with few subjects, such as one naming its own, goes by the share of all probed;
|
|
593
|
+
// one with a single subject shares nothing a fill could save, as a solo lane does not.
|
|
594
|
+
const line = lane.line;
|
|
595
|
+
const n = lane.list.length;
|
|
596
|
+
if (n >= 64)
|
|
597
|
+
this.pace(lane);
|
|
598
|
+
else if (n === 1 && lane.motion === undefined) {
|
|
599
|
+
if (!lane.idle)
|
|
600
|
+
this.rest(lane);
|
|
601
|
+
}
|
|
602
|
+
else if (lane.idle ? share >= line.start : share < line.stop)
|
|
603
|
+
this.flip(lane);
|
|
604
|
+
}
|
|
605
|
+
if (lane.idle)
|
|
606
|
+
continue;
|
|
607
|
+
if (!busy) {
|
|
608
|
+
busy = true;
|
|
609
|
+
for (const ch of this.laned)
|
|
610
|
+
ch.values.fill(ch.rest, 0, size * ch.axes);
|
|
611
|
+
}
|
|
612
|
+
this.run(lane);
|
|
613
|
+
}
|
|
614
|
+
// With every lane idle there was nothing to fill, and no subject reads a lane this frame.
|
|
615
|
+
if (busy)
|
|
616
|
+
for (let s = 0; s < size; s++)
|
|
617
|
+
this.per[s * SLOT + FILLED] = this.fills;
|
|
618
|
+
for (const slot of this.late)
|
|
619
|
+
this.per[slot * SLOT + FILLED] = 0;
|
|
620
|
+
if (this.holding) {
|
|
621
|
+
this.subjects.fill(undefined, 0, size);
|
|
622
|
+
this.holding = false;
|
|
623
|
+
}
|
|
624
|
+
this.moved = reading.moved;
|
|
625
|
+
}
|
|
626
|
+
finally {
|
|
627
|
+
this.filling = false;
|
|
628
|
+
this.late.length = 0;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Sets a lane idle or busy by the share of its subjects probed in the last frame that had a probe,
|
|
633
|
+
* read from about 256 of them a prime stride apart, from an offset that moves each frame, so a host
|
|
634
|
+
* probing every second or third subject is not read as probing all or none. Both paths give the
|
|
635
|
+
* same values, so this only picks the cheaper.
|
|
636
|
+
*/
|
|
637
|
+
pace(lane) {
|
|
638
|
+
const list = lane.list;
|
|
639
|
+
const line = lane.line;
|
|
640
|
+
const from = this.lastFrom;
|
|
641
|
+
const to = this.lastTo;
|
|
642
|
+
const step = primeFrom(Math.floor(list.length / 256));
|
|
643
|
+
let looked = 0;
|
|
644
|
+
let probed = 0;
|
|
645
|
+
for (let p = this.fills % step; p < list.length; p += step) {
|
|
646
|
+
const slot = list[p];
|
|
647
|
+
const a = this.per[slot * SLOT + LANE_PROBE];
|
|
648
|
+
const b = this.per[slot * SLOT + GENERAL_PROBE];
|
|
649
|
+
if ((a > from && a <= to) || (b > from && b <= to))
|
|
650
|
+
probed++;
|
|
651
|
+
looked++;
|
|
652
|
+
}
|
|
653
|
+
const share = probed / looked;
|
|
654
|
+
if (lane.idle ? share >= line.start : share < line.stop)
|
|
655
|
+
this.flip(lane);
|
|
656
|
+
}
|
|
657
|
+
flip(lane) {
|
|
658
|
+
if (lane.idle)
|
|
659
|
+
this.wake(lane);
|
|
660
|
+
else
|
|
661
|
+
this.rest(lane);
|
|
662
|
+
}
|
|
663
|
+
/** Leaves a lane's subjects to the general path until it wakes, keeping what `weightOf` reports. */
|
|
664
|
+
rest(lane) {
|
|
665
|
+
lane.idle = true;
|
|
666
|
+
const list = lane.list;
|
|
667
|
+
const data = lane.data;
|
|
668
|
+
for (let p = 0; p < list.length; p++) {
|
|
669
|
+
const slot = list[p];
|
|
670
|
+
this.reach(slot, 1);
|
|
671
|
+
// A probe read this subject from the last fill, which this lane's weight stays from.
|
|
672
|
+
if (this.per[slot * SLOT + LANE_FILL] === this.fills - 1) {
|
|
673
|
+
const o = p * STRIDE;
|
|
674
|
+
data[o + PROBED] = data[o + WEIGHT];
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
wake(lane) {
|
|
679
|
+
lane.idle = false;
|
|
680
|
+
for (const slot of lane.list)
|
|
681
|
+
this.reach(slot, -1);
|
|
682
|
+
}
|
|
683
|
+
subjectAt(slot) {
|
|
684
|
+
let subject = this.subjects[slot];
|
|
685
|
+
if (subject === undefined) {
|
|
686
|
+
subject = this.numbers.subject(slot);
|
|
687
|
+
this.subjects[slot] = subject;
|
|
688
|
+
this.holding = true;
|
|
689
|
+
}
|
|
690
|
+
return subject;
|
|
691
|
+
}
|
|
692
|
+
/** One voice's contribution to every subject it plays on. */
|
|
693
|
+
run(lane) {
|
|
694
|
+
const voice = lane.voice;
|
|
695
|
+
if (voice.state !== 'live' && voice.state !== 'fading')
|
|
696
|
+
return;
|
|
697
|
+
lane.placed = false;
|
|
698
|
+
lane.read = false;
|
|
699
|
+
lane.weighed = false;
|
|
700
|
+
// With no fade in or out the envelope is 1 for every subject, which is what it would return.
|
|
701
|
+
lane.flat = !((voice.fade.in ?? 0) > 0) && voice.out === null;
|
|
702
|
+
lane.fade = 1;
|
|
703
|
+
const elapsed = voice.elapsedAt(this.now);
|
|
704
|
+
const period = voice.patch.period;
|
|
705
|
+
const passes = voice.passes;
|
|
706
|
+
const list = lane.list;
|
|
707
|
+
for (let p = 0; p < list.length; p++) {
|
|
708
|
+
this.one(lane, p, list[p], elapsed, period, passes);
|
|
709
|
+
// Its patch just made kept state: no further call this fill, the general path makes them.
|
|
710
|
+
if (voice.keeping)
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
one(lane, p, slot, elapsedNow, period, passes) {
|
|
715
|
+
const host = this.host;
|
|
716
|
+
const voice = lane.voice;
|
|
717
|
+
const data = lane.data;
|
|
718
|
+
const o = p * STRIDE;
|
|
719
|
+
// A probe read this subject from the last fill: keep the weight it read for `weightOf`.
|
|
720
|
+
if (this.per[slot * SLOT + LANE_FILL] === this.fills - 1)
|
|
721
|
+
data[o + PROBED] = data[o + WEIGHT];
|
|
722
|
+
const delay = data[o + DELAY];
|
|
723
|
+
const elapsed = elapsedNow - delay;
|
|
724
|
+
if (elapsed < 0) {
|
|
725
|
+
data[o + WEIGHT] = 0;
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
if (!lane.placed || !Object.is(elapsed, lane.placedAt)) {
|
|
729
|
+
lane.placed = true;
|
|
730
|
+
lane.placedAt = elapsed;
|
|
731
|
+
lane.phase = phaseAt(elapsed, period, passes);
|
|
732
|
+
lane.pass = passAt(elapsed, period, passes);
|
|
733
|
+
}
|
|
734
|
+
const since = data[o + SINCE];
|
|
735
|
+
if (!lane.flat && (!lane.weighed || !Object.is(since, lane.weighedSince))) {
|
|
736
|
+
lane.fade = host.envelope(voice, since);
|
|
737
|
+
lane.weighed = true;
|
|
738
|
+
lane.weighedSince = since;
|
|
739
|
+
}
|
|
740
|
+
const w = clampWeight(voice.weight * lane.fade);
|
|
741
|
+
data[o + WEIGHT] = w;
|
|
742
|
+
if (voice.built !== null) {
|
|
743
|
+
if (!lane.read || !Object.is(elapsed, lane.readAt)) {
|
|
744
|
+
readKeys(voice.built, lane.phase, lane.delta, undefined, voice.lerps, undefined, voice.intos, lane.scratch);
|
|
745
|
+
lane.read = true;
|
|
746
|
+
lane.readAt = elapsed;
|
|
747
|
+
this.gather(lane, lane.delta);
|
|
748
|
+
}
|
|
749
|
+
if (w > 0)
|
|
750
|
+
this.fold(lane, slot, w);
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
const subject = this.subjectAt(slot);
|
|
754
|
+
if (subject === absent)
|
|
755
|
+
return;
|
|
756
|
+
const held = lane.records[p];
|
|
757
|
+
// The general path makes a voice's first call for a subject, where it first sees it play.
|
|
758
|
+
if (Number.isNaN(held.probed)) {
|
|
759
|
+
this.late.push(slot);
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
if (lane.motion !== undefined) {
|
|
763
|
+
this.move(lane, lane.motion, p, slot, subject, held, elapsed, delay, w);
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
// Called already this frame, by the general path or a fill before a refill: reuse, as a probe does.
|
|
767
|
+
if (held.probed === this.now && held.delta !== null && held.seeks === voice.seeks) {
|
|
768
|
+
if (w > 0)
|
|
769
|
+
this.foldDelta(lane, slot, held.delta, w);
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
const kept = reading.kept;
|
|
773
|
+
host.ready(voice, subject, held, elapsed, lane.pass, w);
|
|
774
|
+
host.horizon(voice, delay);
|
|
775
|
+
const delta = voice.patch.at(lane.phase, subject, voice.setting);
|
|
776
|
+
// What `influence` leaves on the record, so a probe on the general path this frame reuses it.
|
|
777
|
+
held.delta = delta;
|
|
778
|
+
held.probed = this.now;
|
|
779
|
+
held.seeks = voice.seeks;
|
|
780
|
+
if (this.keeps)
|
|
781
|
+
host.after(voice, held);
|
|
782
|
+
if (reading.kept !== kept && !voice.keeping)
|
|
783
|
+
host.kept(voice);
|
|
784
|
+
if (w > 0)
|
|
785
|
+
this.foldDelta(lane, slot, delta, w);
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* A motion voice's value for a subject, sampled from the patch's state as its `at` would, but only
|
|
789
|
+
* where that sample changes nothing: one with a change to stamp, commit or let go of goes to the
|
|
790
|
+
* general path, which samples it if and when a probe asks, as it would with no lanes. A probe
|
|
791
|
+
* this frame already read keeps the value it read, as the general path's record would.
|
|
792
|
+
*/
|
|
793
|
+
move(lane, run, p, slot, subject, held, elapsed, delay, w) {
|
|
794
|
+
const voice = lane.voice;
|
|
795
|
+
const data = lane.data;
|
|
796
|
+
const o = p * STRIDE;
|
|
797
|
+
const ch = lane.chans[0];
|
|
798
|
+
this.settle(lane, p, slot, held);
|
|
799
|
+
const delta = held.delta;
|
|
800
|
+
if (delta === null ||
|
|
801
|
+
held.probed !== this.now ||
|
|
802
|
+
held.seeks !== voice.seeks ||
|
|
803
|
+
!this.probedThisFrame(slot)) {
|
|
804
|
+
let ms = data[o + MSLOT];
|
|
805
|
+
if (ms < 0) {
|
|
806
|
+
ms = run.slot(subject);
|
|
807
|
+
data[o + MSLOT] = ms;
|
|
808
|
+
}
|
|
809
|
+
this.host.horizon(voice, delay);
|
|
810
|
+
if (!run.quiet(ms, elapsed)) {
|
|
811
|
+
this.late.push(slot);
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
run.sample(ms, elapsed, run.xs, run.vs);
|
|
815
|
+
// Kept as numbers, not a delta: one is built only if a probe this frame asks for it.
|
|
816
|
+
const n = run.n;
|
|
817
|
+
if (lane.axes !== n) {
|
|
818
|
+
lane.axes = n;
|
|
819
|
+
lane.samples = new Float64Array(0);
|
|
820
|
+
}
|
|
821
|
+
if ((p + 1) * n > lane.samples.length) {
|
|
822
|
+
const grown = new Float64Array(Math.max(p + 1, (lane.samples.length / n) * 2, 4) * n);
|
|
823
|
+
grown.set(lane.samples);
|
|
824
|
+
lane.samples = grown;
|
|
825
|
+
}
|
|
826
|
+
for (let a = 0; a < n; a++)
|
|
827
|
+
lane.samples[p * n + a] = run.xs[a];
|
|
828
|
+
lane.deltas[p] = null;
|
|
829
|
+
data[o + SAMPLED] = this.fills;
|
|
830
|
+
data[o + SEEKS] = voice.seeks;
|
|
831
|
+
if (w <= 0)
|
|
832
|
+
return;
|
|
833
|
+
// A sample of other axes than the channel's folds as the general path folds it.
|
|
834
|
+
if (n === ch.axes && run.scalar(ms) === (ch.axes === 1))
|
|
835
|
+
this.foldRun(ch, slot, run.xs, w);
|
|
836
|
+
else
|
|
837
|
+
this.foldInto(ch, slot, run.value(ms, run.xs), w);
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
lane.deltas[p] = delta;
|
|
841
|
+
data[o + SAMPLED] = this.fills;
|
|
842
|
+
data[o + SEEKS] = voice.seeks;
|
|
843
|
+
if (w > 0)
|
|
844
|
+
this.foldInto(ch, slot, delta[ch.name], w);
|
|
845
|
+
}
|
|
846
|
+
/** The delta a motion voice's last sample for position `p` made, built from `samples`. */
|
|
847
|
+
sampled(lane, p) {
|
|
848
|
+
const run = lane.motion;
|
|
849
|
+
const n = lane.axes;
|
|
850
|
+
for (let a = 0; a < n; a++)
|
|
851
|
+
run.xs[a] = lane.samples[p * n + a];
|
|
852
|
+
const ms = lane.data[p * STRIDE + MSLOT];
|
|
853
|
+
const delta = { [lane.chans[0].name]: run.value(ms, run.xs) };
|
|
854
|
+
lane.deltas[p] = delta;
|
|
855
|
+
return delta;
|
|
856
|
+
}
|
|
857
|
+
/**
|
|
858
|
+
* A probe this frame read a motion voice's value for the subject from an earlier fill: write onto
|
|
859
|
+
* its record what the general path's sample would have, so a later read this frame reuses it.
|
|
860
|
+
*/
|
|
861
|
+
settle(lane, p, slot, held) {
|
|
862
|
+
const o = p * STRIDE;
|
|
863
|
+
if ((held.probed === this.now && held.seeks === lane.data[o + SEEKS]) ||
|
|
864
|
+
!(this.per[slot * SLOT + LANE_PROBE] > this.frameProbes) ||
|
|
865
|
+
lane.data[o + SAMPLED] !== this.per[slot * SLOT + LANE_FILL])
|
|
866
|
+
return;
|
|
867
|
+
held.delta = lane.deltas[p] ?? this.sampled(lane, p);
|
|
868
|
+
held.probed = this.now;
|
|
869
|
+
held.seeks = lane.data[o + SEEKS];
|
|
870
|
+
}
|
|
871
|
+
/** For a keys voice: the delta's value for each channel it writes, read once per phase. */
|
|
872
|
+
gather(lane, delta) {
|
|
873
|
+
const chans = lane.chans;
|
|
874
|
+
for (let i = 0; i < chans.length; i++)
|
|
875
|
+
lane.values[i] = delta[chans[i].name];
|
|
876
|
+
}
|
|
877
|
+
/** Folds what `gather` read into a subject's laned values. */
|
|
878
|
+
fold(lane, slot, w) {
|
|
879
|
+
const chans = lane.chans;
|
|
880
|
+
for (let i = 0; i < chans.length; i++)
|
|
881
|
+
this.foldInto(chans[i], slot, lane.values[i], w);
|
|
882
|
+
}
|
|
883
|
+
/**
|
|
884
|
+
* Folds a delta a patch returned into a subject's laned values. The first few channels are read
|
|
885
|
+
* at a site of their own, which in most mixes sees one channel name and stays fast, where one
|
|
886
|
+
* site reading every name in turn slows every read.
|
|
887
|
+
*/
|
|
888
|
+
foldDelta(lane, slot, delta, w) {
|
|
889
|
+
const chans = lane.chans;
|
|
890
|
+
const n = chans.length;
|
|
891
|
+
let ch = chans[0];
|
|
892
|
+
if (n > 0)
|
|
893
|
+
this.foldInto(ch, slot, delta[ch.name], w);
|
|
894
|
+
ch = chans[1];
|
|
895
|
+
if (n > 1)
|
|
896
|
+
this.foldInto(ch, slot, delta[ch.name], w);
|
|
897
|
+
ch = chans[2];
|
|
898
|
+
if (n > 2)
|
|
899
|
+
this.foldInto(ch, slot, delta[ch.name], w);
|
|
900
|
+
for (let i = 3; i < n; i++) {
|
|
901
|
+
ch = chans[i];
|
|
902
|
+
this.foldInto(ch, slot, delta[ch.name], w);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
/** Folds a motion sample's axes, kept apart from `foldInto` so neither reads two array kinds. */
|
|
906
|
+
foldRun(ch, slot, xs, w) {
|
|
907
|
+
const values = ch.values;
|
|
908
|
+
const base = slot * ch.axes;
|
|
909
|
+
for (let a = 0; a < ch.axes; a++)
|
|
910
|
+
values[base + a] = foldNumber(ch.op, values[base + a], xs[a], w);
|
|
911
|
+
}
|
|
912
|
+
foldInto(ch, slot, value, w) {
|
|
913
|
+
if (value === undefined)
|
|
914
|
+
return;
|
|
915
|
+
const values = ch.values;
|
|
916
|
+
if (ch.axes === 1) {
|
|
917
|
+
values[slot] = foldNumber(ch.op, values[slot], value, w);
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
const base = slot * ch.axes;
|
|
921
|
+
// Indexed as `vec`'s fold indexes it, so a typed array folds as an array does.
|
|
922
|
+
const arr = Array.isArray(value) || ArrayBuffer.isView(value) ? value : null;
|
|
923
|
+
for (let a = 0; a < ch.axes; a++) {
|
|
924
|
+
const v = arr === null ? ch.rest : (arr[a] ?? ch.rest);
|
|
925
|
+
values[base + a] = foldNumber(ch.op, values[base + a], v, w);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
}
|