@msb235/blits 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +314 -0
- package/README.md +3 -2
- package/dist/bare.d.ts +29 -0
- package/dist/bare.js +105 -0
- package/dist/blend.d.ts +34 -0
- package/dist/blend.js +102 -0
- package/dist/book.d.ts +53 -0
- package/dist/book.js +218 -0
- package/dist/chain.d.ts +22 -0
- package/dist/chain.js +150 -0
- package/dist/channels.d.ts +2 -0
- package/dist/channels.js +3 -1
- package/dist/clock.d.ts +50 -3
- package/dist/clock.js +102 -9
- package/dist/clone.d.ts +7 -0
- package/dist/clone.js +57 -0
- package/dist/columns.d.ts +29 -0
- package/dist/columns.js +183 -0
- package/dist/crowd.d.ts +128 -0
- package/dist/crowd.js +354 -0
- package/dist/cue.d.ts +3 -0
- package/dist/cue.js +115 -0
- package/dist/due.d.ts +12 -0
- package/dist/due.js +93 -0
- package/dist/fade.d.ts +19 -0
- package/dist/fade.js +133 -0
- package/dist/fill.d.ts +42 -0
- package/dist/fill.js +381 -0
- package/dist/fold.d.ts +51 -0
- package/dist/fold.js +475 -0
- package/dist/gather.d.ts +24 -0
- package/dist/gather.js +186 -0
- package/dist/handle.d.ts +31 -0
- package/dist/handle.js +56 -0
- package/dist/held.d.ts +14 -0
- package/dist/held.js +117 -0
- package/dist/history.d.ts +22 -0
- package/dist/history.js +149 -0
- package/dist/hosts.d.ts +16 -0
- package/dist/hosts.js +178 -0
- package/dist/index.d.ts +1 -1
- package/dist/keyrows.d.ts +36 -0
- package/dist/keyrows.js +138 -0
- package/dist/lane.d.ts +210 -0
- package/dist/lane.js +187 -0
- package/dist/lanes.d.ts +143 -122
- package/dist/lanes.js +236 -742
- package/dist/locus.d.ts +40 -0
- package/dist/locus.js +89 -0
- package/dist/marks.d.ts +12 -0
- package/dist/marks.js +80 -0
- package/dist/meet.d.ts +16 -0
- package/dist/meet.js +113 -0
- package/dist/mixer.d.ts +244 -169
- package/dist/mixer.js +321 -1793
- package/dist/motion.d.ts +104 -19
- package/dist/motion.js +448 -180
- package/dist/move.d.ts +9 -0
- package/dist/move.js +190 -0
- package/dist/named.d.ts +12 -0
- package/dist/named.js +39 -0
- package/dist/numbers.d.ts +18 -6
- package/dist/numbers.js +59 -31
- package/dist/owed.d.ts +24 -0
- package/dist/owed.js +51 -0
- package/dist/owner.d.ts +43 -0
- package/dist/owner.js +117 -0
- package/dist/pace.d.ts +40 -0
- package/dist/pace.js +113 -0
- package/dist/patch.d.ts +51 -10
- package/dist/patch.js +140 -45
- package/dist/place.d.ts +20 -0
- package/dist/place.js +196 -0
- package/dist/project.d.ts +4 -0
- package/dist/project.js +233 -0
- package/dist/pull.d.ts +5 -0
- package/dist/pull.js +175 -0
- package/dist/qualify.d.ts +31 -0
- package/dist/qualify.js +345 -0
- package/dist/relink.d.ts +25 -0
- package/dist/relink.js +59 -0
- package/dist/rows.d.ts +33 -0
- package/dist/rows.js +282 -0
- package/dist/sample.d.ts +21 -0
- package/dist/sample.js +232 -0
- package/dist/signals.d.ts +12 -9
- package/dist/signals.js +8 -5
- package/dist/store.d.ts +10 -3
- package/dist/store.js +44 -9
- package/dist/strays.d.ts +21 -0
- package/dist/strays.js +84 -0
- package/dist/tweened.d.ts +11 -0
- package/dist/tweened.js +48 -0
- package/dist/types.d.ts +261 -42
- package/dist/unreached.d.ts +5 -0
- package/dist/unreached.js +24 -0
- package/dist/voice.d.ts +240 -0
- package/dist/voice.js +322 -0
- package/dist/weigh.d.ts +26 -0
- package/dist/weigh.js +89 -0
- package/docs/schema.html +151 -46
- package/package.json +5 -2
package/dist/lanes.js
CHANGED
|
@@ -1,218 +1,41 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
1
|
+
import { row } from './crowd.js';
|
|
2
|
+
import { call, fillAll, foldKeys, one, parting, readKeyed, reset, run, signalled, subjectAt, } from './fill.js';
|
|
3
|
+
import { fold, foldDelta, foldInto, foldRun, gather } from './gather.js';
|
|
4
|
+
import { meet, owable, reach } from './meet.js';
|
|
5
|
+
import { Numbers } from './numbers.js';
|
|
6
|
+
import { Owed } from './owed.js';
|
|
7
|
+
import { requalify, retouch } from './qualify.js';
|
|
6
8
|
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
|
-
}
|
|
9
|
+
import { reportedRow, unplace } from './rows.js';
|
|
203
10
|
/**
|
|
204
11
|
* The lanes of one mix: subject numbers, which channels and voices run on lanes, and the values a
|
|
205
12
|
* frame's fill leaves for probes to copy.
|
|
206
13
|
*/
|
|
14
|
+
// biome-ignore lint/suspicious/noUnsafeDeclarationMerging: every member of `Methods` is installed below
|
|
207
15
|
export class Lanes {
|
|
208
16
|
host;
|
|
209
17
|
numbers;
|
|
210
18
|
lanes = [];
|
|
211
|
-
/** The lanes a fill visits: every lane but the solo ones. */
|
|
212
|
-
runs = [];
|
|
213
19
|
/** The lanes over every subject that have started playing, by epoch. */
|
|
214
20
|
dense = [];
|
|
215
21
|
byId = new Map();
|
|
22
|
+
/** The crowds of single-subject motion voices, one per channel, and which holds each voice. */
|
|
23
|
+
crowds = [];
|
|
24
|
+
crowdOf = new Map();
|
|
25
|
+
/** Whether two crowds share a channel, so their rows must interleave in voice order. */
|
|
26
|
+
overlap = false;
|
|
27
|
+
/** The loci whose voices are on lanes, every member of each on one. */
|
|
28
|
+
loci = [];
|
|
29
|
+
/** While a fill gathers a locus: that locus and the member gathering, where folds go instead. */
|
|
30
|
+
into = null;
|
|
31
|
+
intoId = 0;
|
|
32
|
+
lawsByKey = new Map();
|
|
33
|
+
lastLaw = null;
|
|
34
|
+
/** The lanes the last `meet` gave a position, and whether it placed a crowd row. */
|
|
35
|
+
met = [];
|
|
36
|
+
metCrowd = false;
|
|
37
|
+
/** By subject number, the laned voices its probes fold on the general path this fill. */
|
|
38
|
+
owed = new Owed();
|
|
216
39
|
laned = [];
|
|
217
40
|
/** By kit slot, the laned channel there. */
|
|
218
41
|
bySlot = [];
|
|
@@ -222,6 +45,12 @@ export class Lanes {
|
|
|
222
45
|
whole = false;
|
|
223
46
|
cap = 0;
|
|
224
47
|
epochs = 0;
|
|
48
|
+
/** The latest epoch of a lane over every subject, which every subject meets on its next probe. */
|
|
49
|
+
wide = 0;
|
|
50
|
+
/** Voices that joined, started or left since the last qualify, settled at the next probe. */
|
|
51
|
+
touched = [];
|
|
52
|
+
/** The highest voice id the last qualify or settling of `touched` considered. */
|
|
53
|
+
known = 0;
|
|
225
54
|
/**
|
|
226
55
|
* Per subject number, `SLOT` numbers side by side, so a probe reads one place in memory: which
|
|
227
56
|
* fill last wrote it (0 for none); the latest lane epoch a probe of it has checked it against;
|
|
@@ -248,6 +77,15 @@ export class Lanes {
|
|
|
248
77
|
* the general path has to make.
|
|
249
78
|
*/
|
|
250
79
|
late = [];
|
|
80
|
+
/**
|
|
81
|
+
* What `writeLater` and `pullRun` have queued, for one set of columns, as runs: each run's first
|
|
82
|
+
* lane slot, the row it goes to, and how many subjects follow both one apart.
|
|
83
|
+
*/
|
|
84
|
+
queueSlots = new Int32Array(0);
|
|
85
|
+
queueRows = new Int32Array(0);
|
|
86
|
+
queueCounts = new Int32Array(0);
|
|
87
|
+
queued = 0;
|
|
88
|
+
queuedFor = null;
|
|
251
89
|
keeps = false;
|
|
252
90
|
now = Number.NaN;
|
|
253
91
|
filledAt = Number.NaN;
|
|
@@ -270,7 +108,7 @@ export class Lanes {
|
|
|
270
108
|
qualifiedVersion = Number.NaN;
|
|
271
109
|
constructor(host) {
|
|
272
110
|
this.host = host;
|
|
273
|
-
this.numbers = new Numbers(
|
|
111
|
+
this.numbers = new Numbers(this);
|
|
274
112
|
}
|
|
275
113
|
/** A change that may move a voice on or off its lane: qualify again, then fill again. */
|
|
276
114
|
invalidate() {
|
|
@@ -281,17 +119,25 @@ export class Lanes {
|
|
|
281
119
|
refill() {
|
|
282
120
|
this.filledVersion = Number.NaN;
|
|
283
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* A voice joined, started playing or left: the next probe takes it on or off its crowd where it
|
|
124
|
+
* can, and qualifies again where it cannot.
|
|
125
|
+
*/
|
|
126
|
+
touch(voice) {
|
|
127
|
+
this.touched.push(voice);
|
|
128
|
+
this.filledVersion = Number.NaN;
|
|
129
|
+
}
|
|
284
130
|
/** Numbers a subject the mix sees for the first time. */
|
|
285
131
|
number(subject) {
|
|
286
132
|
const slot = this.numbers.take(subject);
|
|
287
133
|
this.live++;
|
|
288
134
|
this.grow(slot + 1);
|
|
289
|
-
this.per[slot * SLOT + FILLED] = 0;
|
|
290
|
-
this.per[slot * SLOT + SEEN] =
|
|
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;
|
|
135
|
+
this.per[slot * 6 /* Per.SLOT */ + 0 /* Per.FILLED */] = 0;
|
|
136
|
+
this.per[slot * 6 /* Per.SLOT */ + 1 /* Per.SEEN */] = -1;
|
|
137
|
+
this.per[slot * 6 /* Per.SLOT */ + 2 /* Per.IDLE */] = 0;
|
|
138
|
+
this.per[slot * 6 /* Per.SLOT */ + 3 /* Per.LANE_PROBE */] = 0;
|
|
139
|
+
this.per[slot * 6 /* Per.SLOT */ + 4 /* Per.GENERAL_PROBE */] = 0;
|
|
140
|
+
this.per[slot * 6 /* Per.SLOT */ + 5 /* Per.LANE_FILL */] = 0;
|
|
295
141
|
if (this.filling)
|
|
296
142
|
this.late.push(slot);
|
|
297
143
|
return slot;
|
|
@@ -299,32 +145,104 @@ export class Lanes {
|
|
|
299
145
|
release(slot) {
|
|
300
146
|
this.numbers.release(slot);
|
|
301
147
|
}
|
|
148
|
+
/** A voice faded a subject out of itself alone: its lane lets the subject's position go. */
|
|
149
|
+
part(id, slot) {
|
|
150
|
+
const lane = this.byId.get(id);
|
|
151
|
+
if (lane !== undefined) {
|
|
152
|
+
if (lane.positionOf(slot) < 0)
|
|
153
|
+
return;
|
|
154
|
+
if (lane.idle)
|
|
155
|
+
reach(this, slot, -1);
|
|
156
|
+
lane.remove(slot);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const c = this.crowdOf.get(id);
|
|
160
|
+
const p = c?.rowOf.get(id);
|
|
161
|
+
if (c !== undefined && p !== undefined && c.list[p] === slot)
|
|
162
|
+
unplace(this, c, p);
|
|
163
|
+
}
|
|
164
|
+
/** A voice brought a subject back: its next probe meets every lane again, as it met them first. */
|
|
165
|
+
rejoin(slot) {
|
|
166
|
+
if (slot < this.cap)
|
|
167
|
+
this.per[slot * 6 /* Per.SLOT */ + 1 /* Per.SEEN */] = -1;
|
|
168
|
+
}
|
|
302
169
|
/**
|
|
303
170
|
* A laned voice's weight for a subject at the last frame a probe read it from the lane; undefined
|
|
304
171
|
* where the subject's record holds it instead, because its last probe took the general path or
|
|
305
172
|
* the voice is not on a lane.
|
|
306
173
|
*/
|
|
307
174
|
weightOf(id, slot) {
|
|
308
|
-
|
|
309
|
-
if (lane === undefined || slot < 0)
|
|
175
|
+
if (slot < 0)
|
|
310
176
|
return undefined;
|
|
311
|
-
|
|
177
|
+
const lane = this.byId.get(id);
|
|
178
|
+
if (lane !== undefined)
|
|
179
|
+
return this.reported(lane, slot);
|
|
180
|
+
const c = this.crowdOf.get(id);
|
|
181
|
+
const p = c?.rowOf.get(id);
|
|
182
|
+
return c === undefined || p === undefined ? undefined : reportedRow(this, c, p, slot);
|
|
312
183
|
}
|
|
313
184
|
reported(lane, slot) {
|
|
314
|
-
const o = slot * SLOT
|
|
315
|
-
if (!(this.per[o + LANE_PROBE] > this.per[o + GENERAL_PROBE]))
|
|
185
|
+
const o = slot * 6 /* Per.SLOT */;
|
|
186
|
+
if (!(this.per[o + 3 /* Per.LANE_PROBE */] > this.per[o + 4 /* Per.GENERAL_PROBE */]))
|
|
316
187
|
return undefined;
|
|
317
188
|
const p = lane.positionOf(slot);
|
|
318
189
|
if (p < 0)
|
|
319
190
|
return undefined;
|
|
320
|
-
return lane.data[p * STRIDE + (this.per[o + LANE_FILL] === this.fills ? WEIGHT : PROBED)];
|
|
191
|
+
return lane.data[p * 8 /* Row.STRIDE */ + (this.per[o + 5 /* Per.LANE_FILL */] === this.fills ? 2 /* Row.WEIGHT */ : 3 /* Row.PROBED */)];
|
|
321
192
|
}
|
|
322
193
|
/**
|
|
323
194
|
* Fills the lanes once a frame, and says whether this probe of a subject reads its values from
|
|
324
195
|
* them. One that a lane has not met yet, newly numbered or newly reached, takes the general path
|
|
325
196
|
* this frame, which is where it is first seen; so does one probed from inside a fill.
|
|
326
197
|
*/
|
|
327
|
-
prepare(slot, subject, now, version) {
|
|
198
|
+
prepare(slot, subject, now, version, head) {
|
|
199
|
+
const ready = this.filled(now, version) ? 1 /* Begin.READY */ : this.begin(now, version);
|
|
200
|
+
if (ready === 0 /* Begin.GENERAL */)
|
|
201
|
+
return this.general(slot);
|
|
202
|
+
if (slot < 0)
|
|
203
|
+
return false;
|
|
204
|
+
if (!this.probedThisFrame(slot))
|
|
205
|
+
this.distinct++;
|
|
206
|
+
const probe = ++this.probes;
|
|
207
|
+
const per = this.per;
|
|
208
|
+
const o = slot * 6 /* Per.SLOT */;
|
|
209
|
+
let lane = per[o + 0 /* Per.FILLED */] === this.fills && per[o + 2 /* Per.IDLE */] === 0;
|
|
210
|
+
if (per[o + 1 /* Per.SEEN */] < this.wide && meet(this, slot, subject, head)) {
|
|
211
|
+
// The fill ran before the subject had these positions. Where every voice it just met folds
|
|
212
|
+
// after every other laned voice, the general path folds just those onto the lanes' values;
|
|
213
|
+
// otherwise it reads the general path all frame.
|
|
214
|
+
if (lane && per[o + 2 /* Per.IDLE */] === 0 && owable(this, slot))
|
|
215
|
+
this.owed.owe(slot, this.met);
|
|
216
|
+
else {
|
|
217
|
+
per[o + 0 /* Per.FILLED */] = 0;
|
|
218
|
+
lane = false;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (lane) {
|
|
222
|
+
per[o + 3 /* Per.LANE_PROBE */] = probe;
|
|
223
|
+
per[o + 5 /* Per.LANE_FILL */] = this.fills;
|
|
224
|
+
this.subjects[slot] = subject;
|
|
225
|
+
this.holding = true;
|
|
226
|
+
}
|
|
227
|
+
else
|
|
228
|
+
per[o + 4 /* Per.GENERAL_PROBE */] = probe;
|
|
229
|
+
return lane;
|
|
230
|
+
}
|
|
231
|
+
/** Whether this frame's lanes are filled and nothing since asks `begin` to look again. */
|
|
232
|
+
filled(now, version) {
|
|
233
|
+
return (this.filledAt === now &&
|
|
234
|
+
this.frameAt === now &&
|
|
235
|
+
this.filledVersion === version &&
|
|
236
|
+
this.qualifiedVersion === version &&
|
|
237
|
+
this.moved === reading.moved &&
|
|
238
|
+
this.touched.length === 0 &&
|
|
239
|
+
!this.filling);
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* What a probe does before its subject, once a frame's lanes are settled: qualifies and fills as
|
|
243
|
+
* needed. Says whether the subject takes the general path or the lanes are filled.
|
|
244
|
+
*/
|
|
245
|
+
begin(now, version) {
|
|
328
246
|
if (now !== this.frameAt) {
|
|
329
247
|
this.lastFrom = this.frameProbes;
|
|
330
248
|
this.lastTo = this.probes;
|
|
@@ -335,87 +253,79 @@ export class Lanes {
|
|
|
335
253
|
this.fresh = true;
|
|
336
254
|
}
|
|
337
255
|
if (this.filling)
|
|
338
|
-
return
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
if (this.
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
return false;
|
|
347
|
-
if (this.per[slot * SLOT + SEEN] < this.epochs)
|
|
348
|
-
this.meet(slot, subject);
|
|
349
|
-
return this.general(slot);
|
|
256
|
+
return 0 /* Begin.GENERAL */;
|
|
257
|
+
// The mix's version moves only when a voice over every subject comes, goes, starts or stops
|
|
258
|
+
// waiting, each of which touches it, so a version past a qualify that stands is a set of touches.
|
|
259
|
+
if (this.qualifiedVersion !== version) {
|
|
260
|
+
if (!Number.isNaN(this.qualifiedVersion) && this.touched.length > 0 && retouch(this))
|
|
261
|
+
this.qualifiedVersion = version;
|
|
262
|
+
else
|
|
263
|
+
requalify(this, version);
|
|
350
264
|
}
|
|
351
|
-
if (
|
|
352
|
-
this
|
|
265
|
+
else if (this.touched.length > 0 && !retouch(this))
|
|
266
|
+
requalify(this, version);
|
|
267
|
+
if (this.laned.length === 0)
|
|
268
|
+
return 0 /* Begin.GENERAL */;
|
|
353
269
|
if (this.filledAt !== now || this.filledVersion !== version || this.moved !== reading.moved) {
|
|
354
|
-
|
|
270
|
+
fillAll(this, now, version);
|
|
355
271
|
// A patch call in that fill made kept state, which took its voice off its lane: fill without it.
|
|
356
272
|
if (this.qualifiedVersion !== version) {
|
|
357
|
-
|
|
273
|
+
requalify(this, version);
|
|
358
274
|
if (this.laned.length === 0)
|
|
359
|
-
return
|
|
360
|
-
|
|
275
|
+
return 0 /* Begin.GENERAL */;
|
|
276
|
+
fillAll(this, now, version);
|
|
361
277
|
}
|
|
362
278
|
}
|
|
363
|
-
|
|
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;
|
|
279
|
+
return 1 /* Begin.READY */;
|
|
383
280
|
}
|
|
384
281
|
general(slot) {
|
|
385
282
|
if (slot >= 0)
|
|
386
|
-
this.per[slot * SLOT + GENERAL_PROBE] = ++this.probes;
|
|
283
|
+
this.per[slot * 6 /* Per.SLOT */ + 4 /* Per.GENERAL_PROBE */] = ++this.probes;
|
|
387
284
|
return false;
|
|
388
285
|
}
|
|
389
286
|
/** Whether a probe has read the subject since the frame began, on either path. */
|
|
390
287
|
probedThisFrame(slot) {
|
|
391
288
|
const from = this.frameProbes;
|
|
392
|
-
const o = slot * SLOT
|
|
393
|
-
return (this.per[o +
|
|
289
|
+
const o = slot * 6 /* Per.SLOT */;
|
|
290
|
+
return (this.per[o + 3 /* Per.LANE_PROBE */] > from ||
|
|
291
|
+
this.per[o + 4 /* Per.GENERAL_PROBE */] > from);
|
|
292
|
+
}
|
|
293
|
+
/** Whether the subject at `slot` reads some laned voice from the general path this fill. */
|
|
294
|
+
owes(slot) {
|
|
295
|
+
return this.owed.owes(slot);
|
|
296
|
+
}
|
|
297
|
+
/** Whether the subject at `slot` reads laned voice `id` from the general path this fill. */
|
|
298
|
+
owesVoice(slot, id) {
|
|
299
|
+
return this.owed.owesVoice(slot, id);
|
|
300
|
+
}
|
|
301
|
+
/** The weight the general path gave a voice the subject at `slot` owes, as `weightOf` reports. */
|
|
302
|
+
paid(id, slot, w) {
|
|
303
|
+
const lane = this.byId.get(id);
|
|
304
|
+
const p = lane === undefined ? -1 : lane.positionOf(slot);
|
|
305
|
+
if (p >= 0)
|
|
306
|
+
lane.data[p * 8 /* Row.STRIDE */ + 2 /* Row.WEIGHT */] = w;
|
|
394
307
|
}
|
|
395
308
|
/**
|
|
396
|
-
*
|
|
397
|
-
*
|
|
309
|
+
* Whether a subject's laned values are at their channels' rest, as `atRest` reads the pose `copy`
|
|
310
|
+
* would write: each number within 1e-9 of its rest.
|
|
398
311
|
*/
|
|
399
|
-
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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];
|
|
312
|
+
rests(slot) {
|
|
313
|
+
for (const ch of this.laned) {
|
|
314
|
+
const values = ch.values;
|
|
315
|
+
if (ch.scalar) {
|
|
316
|
+
if (!(Math.abs(values[slot] - ch.rest) < 1e-9))
|
|
317
|
+
return false;
|
|
410
318
|
continue;
|
|
411
319
|
}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
320
|
+
// `copy` lengthens a rest shorter than the channel's axes, and a longer array is never near it.
|
|
321
|
+
if (ch.start.length < ch.axes)
|
|
322
|
+
return false;
|
|
323
|
+
const base = slot * ch.axes;
|
|
324
|
+
for (let a = 0; a < ch.axes; a++)
|
|
325
|
+
if (!(Math.abs(values[base + a] - ch.start[a]) < 1e-9))
|
|
326
|
+
return false;
|
|
418
327
|
}
|
|
328
|
+
return true;
|
|
419
329
|
}
|
|
420
330
|
/** Writes a subject's laned values into a pose, each array channel into a new array. */
|
|
421
331
|
copy(slot, pose) {
|
|
@@ -423,7 +333,7 @@ export class Lanes {
|
|
|
423
333
|
for (let c = 0; c < laned.length; c++) {
|
|
424
334
|
const ch = laned[c];
|
|
425
335
|
const axes = ch.axes;
|
|
426
|
-
if (
|
|
336
|
+
if (ch.scalar) {
|
|
427
337
|
pose[ch.name] = ch.values[slot];
|
|
428
338
|
continue;
|
|
429
339
|
}
|
|
@@ -435,60 +345,27 @@ export class Lanes {
|
|
|
435
345
|
pose[ch.name] = arr;
|
|
436
346
|
}
|
|
437
347
|
}
|
|
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
348
|
forget(slot) {
|
|
349
|
+
this.host.forgot(slot);
|
|
478
350
|
this.live--;
|
|
479
351
|
for (const lane of this.lanes)
|
|
480
352
|
lane.remove(slot);
|
|
353
|
+
for (const c of this.crowds)
|
|
354
|
+
for (let p = 0; p < c.size; p++)
|
|
355
|
+
if (c.list[p] === slot)
|
|
356
|
+
unplace(this, c, p);
|
|
481
357
|
if (slot < this.cap)
|
|
482
|
-
this.per[slot * SLOT + FILLED] = 0;
|
|
358
|
+
this.per[slot * 6 /* Per.SLOT */ + 0 /* Per.FILLED */] = 0;
|
|
483
359
|
this.subjects[slot] = undefined;
|
|
484
360
|
}
|
|
485
361
|
grow(size) {
|
|
486
362
|
if (size <= this.cap)
|
|
487
363
|
return;
|
|
488
364
|
const cap = Math.max(size, this.cap * 2, 64);
|
|
489
|
-
const per = new Float64Array(cap * SLOT);
|
|
365
|
+
const per = new Float64Array(cap * 6 /* Per.SLOT */);
|
|
490
366
|
per.set(this.per);
|
|
491
367
|
this.per = per;
|
|
368
|
+
this.owed.grow(cap);
|
|
492
369
|
for (const ch of this.laned) {
|
|
493
370
|
const values = new Float64Array(cap * ch.axes);
|
|
494
371
|
values.set(ch.values);
|
|
@@ -496,433 +373,50 @@ export class Lanes {
|
|
|
496
373
|
}
|
|
497
374
|
this.cap = cap;
|
|
498
375
|
}
|
|
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
376
|
/**
|
|
632
|
-
*
|
|
633
|
-
*
|
|
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.
|
|
377
|
+
* A laned voice's patch is about to change subject `s`'s stretch: a crowd copies it again at the
|
|
378
|
+
* next fill, and a lane keeps the sample the old one gave.
|
|
636
379
|
*/
|
|
637
|
-
|
|
638
|
-
const
|
|
639
|
-
const
|
|
640
|
-
|
|
641
|
-
|
|
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')
|
|
380
|
+
stretchChanged(id, s) {
|
|
381
|
+
const c = this.crowdOf.get(id);
|
|
382
|
+
const p = c?.rowOf.get(id);
|
|
383
|
+
if (c !== undefined && p !== undefined) {
|
|
384
|
+
c.restale(p, c.hot[p * c.stride + 0 /* Hot.FLAGS */]);
|
|
696
385
|
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
386
|
}
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
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);
|
|
387
|
+
const lane = this.byId.get(id);
|
|
388
|
+
const q = lane === undefined ? -1 : lane.positionOfMotion(s);
|
|
389
|
+
if (q >= 0)
|
|
390
|
+
lane.fix(q);
|
|
391
|
+
}
|
|
392
|
+
/** Something on a voice changed: a crowd copies it again at the next fill. */
|
|
393
|
+
voiceChanged(id) {
|
|
394
|
+
const c = this.crowdOf.get(id);
|
|
395
|
+
const p = c?.rowOf.get(id);
|
|
396
|
+
if (c === undefined || p === undefined)
|
|
918
397
|
return;
|
|
919
|
-
|
|
920
|
-
|
|
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
|
-
}
|
|
398
|
+
const f = p * c.stride + 0 /* Hot.FLAGS */;
|
|
399
|
+
c.hot[f] = c.hot[f] | 64 /* Flag.VOICE */;
|
|
927
400
|
}
|
|
928
401
|
}
|
|
402
|
+
// A fill's per-subject path is methods, installed here from the modules that own them: as
|
|
403
|
+
// functions called directly they made fills 5-8% slower (bench/ab.sh on teitou, 2026-10-05).
|
|
404
|
+
const methods = {
|
|
405
|
+
row,
|
|
406
|
+
signalled,
|
|
407
|
+
parting,
|
|
408
|
+
subjectAt,
|
|
409
|
+
reset,
|
|
410
|
+
run,
|
|
411
|
+
one,
|
|
412
|
+
foldKeys,
|
|
413
|
+
readKeyed,
|
|
414
|
+
call,
|
|
415
|
+
gather,
|
|
416
|
+
fold,
|
|
417
|
+
foldDelta,
|
|
418
|
+
foldRun,
|
|
419
|
+
foldInto,
|
|
420
|
+
};
|
|
421
|
+
for (const [name, value] of Object.entries(methods))
|
|
422
|
+
Object.defineProperty(Lanes.prototype, name, { value, writable: true, configurable: true });
|