@msb235/blits 0.10.0 → 0.12.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 +248 -0
- package/README.md +15 -4
- package/dist/arcs.d.ts +40 -0
- package/dist/arcs.js +239 -0
- package/dist/blend.js +1 -5
- package/dist/book.d.ts +2 -0
- package/dist/book.js +27 -3
- package/dist/chain.d.ts +17 -1
- package/dist/chain.js +57 -15
- package/dist/channels.d.ts +2 -0
- package/dist/channels.js +12 -1
- package/dist/clock.d.ts +14 -0
- package/dist/clock.js +9 -1
- package/dist/clone.js +16 -4
- package/dist/columns.d.ts +4 -5
- package/dist/columns.js +11 -5
- package/dist/crowd.d.ts +1 -1
- package/dist/crowd.js +4 -1
- package/dist/cue.js +63 -7
- package/dist/departed.d.ts +8 -3
- package/dist/departed.js +76 -29
- package/dist/due.d.ts +4 -0
- package/dist/due.js +16 -3
- package/dist/easing.d.ts +12 -1
- package/dist/easing.js +7 -1
- package/dist/everyone.d.ts +14 -1
- package/dist/everyone.js +27 -2
- package/dist/fade.d.ts +12 -5
- package/dist/fade.js +57 -19
- package/dist/fill.js +16 -9
- package/dist/fit.d.ts +8 -1
- package/dist/fit.js +7 -3
- package/dist/fold.d.ts +1 -1
- package/dist/fold.js +112 -9
- package/dist/frames.d.ts +2 -0
- package/dist/frames.js +17 -0
- package/dist/gather.js +38 -7
- package/dist/handle.d.ts +7 -2
- package/dist/handle.js +28 -12
- package/dist/held.d.ts +13 -2
- package/dist/held.js +43 -9
- package/dist/history.d.ts +25 -2
- package/dist/history.js +163 -35
- package/dist/hosts.js +45 -17
- package/dist/index.d.ts +5 -2
- package/dist/index.js +3 -1
- package/dist/keyfill.js +3 -1
- package/dist/lane.d.ts +1 -1
- package/dist/lane.js +3 -1
- package/dist/lanes.d.ts +31 -7
- package/dist/lanes.js +57 -6
- package/dist/lean.d.ts +15 -0
- package/dist/lean.js +23 -0
- package/dist/leavings.d.ts +6 -1
- package/dist/leavings.js +16 -2
- package/dist/marks.d.ts +7 -3
- package/dist/marks.js +105 -27
- package/dist/meet.js +1 -0
- package/dist/mixer.d.ts +35 -6
- package/dist/mixer.js +113 -45
- package/dist/motion.d.ts +4 -1
- package/dist/motion.js +16 -2
- package/dist/motions.d.ts +4 -0
- package/dist/motions.js +45 -11
- package/dist/move.js +35 -12
- package/dist/numbers.d.ts +5 -1
- package/dist/numbers.js +4 -0
- package/dist/owner.d.ts +18 -2
- package/dist/owner.js +44 -3
- package/dist/pack.d.ts +1 -0
- package/dist/pack.js +3 -0
- package/dist/place.d.ts +8 -0
- package/dist/place.js +16 -8
- package/dist/placed.d.ts +19 -0
- package/dist/placed.js +54 -0
- package/dist/project.d.ts +5 -2
- package/dist/project.js +238 -34
- package/dist/pull.js +37 -8
- package/dist/qualify.d.ts +10 -0
- package/dist/qualify.js +60 -3
- package/dist/reading.d.ts +8 -1
- package/dist/reading.js +8 -1
- package/dist/record.d.ts +6 -0
- package/dist/record.js +2 -0
- package/dist/restored.d.ts +69 -17
- package/dist/restored.js +163 -50
- package/dist/revive.d.ts +6 -3
- package/dist/revive.js +11 -3
- package/dist/roster.js +2 -1
- package/dist/rows.js +15 -5
- package/dist/seek.d.ts +7 -2
- package/dist/seek.js +152 -43
- package/dist/shared.d.ts +5 -0
- package/dist/shared.js +5 -0
- package/dist/signals.d.ts +10 -0
- package/dist/signals.js +17 -0
- package/dist/spans.d.ts +5 -0
- package/dist/spans.js +75 -5
- package/dist/store.d.ts +13 -0
- package/dist/store.js +23 -0
- package/dist/strays.js +50 -2
- package/dist/tape.d.ts +29 -4
- package/dist/tape.js +49 -11
- package/dist/touch.js +3 -2
- package/dist/transport.d.ts +11 -2
- package/dist/transport.js +26 -10
- package/dist/types/arc.d.ts +55 -0
- package/dist/types/arc.js +1 -0
- package/dist/types/mix.d.ts +57 -14
- package/dist/types/patch.d.ts +5 -1
- package/dist/types/place.d.ts +11 -4
- package/dist/types/transport.d.ts +12 -3
- package/dist/types/voice.d.ts +72 -5
- package/dist/types/watch.d.ts +126 -0
- package/dist/types/watch.js +1 -0
- package/dist/types.d.ts +2 -0
- package/dist/unpage.d.ts +6 -1
- package/dist/unpage.js +34 -2
- package/dist/voice.d.ts +93 -16
- package/dist/voice.js +60 -3
- package/dist/watch.d.ts +26 -0
- package/dist/watch.js +441 -0
- package/dist/weigh.d.ts +13 -2
- package/dist/weigh.js +10 -3
- package/docs/schema.html +106 -38
- package/package.json +10 -4
package/dist/seek.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { heldAt } from './chain.js';
|
|
1
2
|
import { schedule } from './due.js';
|
|
2
|
-
import {
|
|
3
|
+
import { fadesAt } from './fade.js';
|
|
4
|
+
import { origin } from './held.js';
|
|
5
|
+
import { apart, copyHeld, copyHeldInto, lastIndex, lastWithin, unread } from './history.js';
|
|
3
6
|
import { laneHost } from './hosts.js';
|
|
4
|
-
import { Lanes } from './lanes.js';
|
|
5
7
|
import { Leavings } from './leavings.js';
|
|
6
8
|
import { move, nextFrame } from './move.js';
|
|
7
9
|
import { ownerReading } from './owner.js';
|
|
@@ -11,34 +13,112 @@ import { Restored } from './restored.js';
|
|
|
11
13
|
import { recount } from './roster.js';
|
|
12
14
|
import { refitAll } from './spans.js';
|
|
13
15
|
import { Store } from './store.js';
|
|
14
|
-
import { hostAt, replay } from './tape.js';
|
|
15
|
-
import { cover, cut } from './unpage.js';
|
|
16
|
+
import { hostAhead, hostAt, replay } from './tape.js';
|
|
17
|
+
import { cover, coverFrame, cut } from './unpage.js';
|
|
16
18
|
/**
|
|
17
19
|
* A voice's record of one subject as it stood at mix time `t`, from the latest copy kept by then;
|
|
18
20
|
* undefined for a stateful record first made after `t`, which is met afresh. A stateless record
|
|
19
|
-
* holds nothing that moves with time, so it is copied whenever it was made.
|
|
21
|
+
* holds nothing that moves with time, so it is copied whenever it was made. The weight its last
|
|
22
|
+
* probe by `t` read is not kept, and a lane holds it apart from the record, so the record reads 0
|
|
23
|
+
* until the next probe.
|
|
20
24
|
*/
|
|
21
|
-
function restore(voice, live,
|
|
25
|
+
function restore(mix, voice, live, at, every, mine) {
|
|
26
|
+
// Read here, not passed: a double passed to a call is boxed, once per record put back.
|
|
27
|
+
const t = at.t;
|
|
22
28
|
const inputs = live.inputs?.filter((e) => e.at <= t);
|
|
23
29
|
const snaps = live.snaps;
|
|
24
30
|
if (snaps === undefined) {
|
|
25
|
-
// Nothing a step or `setting.keep` holds: what it keeps does not move with time.
|
|
26
|
-
|
|
31
|
+
// Nothing a step or `setting.keep` holds: what it keeps does not move with time. One record
|
|
32
|
+
// stands for every subject a voice does not reach, so that one is copied.
|
|
33
|
+
const h = mine && live.reaches ? unread(live) : copyHeld(voice, live);
|
|
27
34
|
if (h.stepped > t)
|
|
28
35
|
h.stepped = t;
|
|
29
36
|
// Whether it had rested by `t` is not kept; the next probe finds it again.
|
|
30
37
|
h.rested = false;
|
|
38
|
+
h.weight = 0;
|
|
31
39
|
h.inputs = inputs;
|
|
40
|
+
if (!(h.since <= t))
|
|
41
|
+
origin(mix, voice, h);
|
|
32
42
|
return h;
|
|
33
43
|
}
|
|
34
|
-
|
|
35
|
-
|
|
44
|
+
// A seek back to just before now finds its copy among the newest, looked at before searching.
|
|
45
|
+
const n = snaps.length;
|
|
46
|
+
const i = n > 0 && snaps[n - 1].at <= t
|
|
47
|
+
? n - 1
|
|
48
|
+
: n > 1 && snaps[n - 2].at <= t
|
|
49
|
+
? n - 2
|
|
50
|
+
: lastIndex(snaps, t, true);
|
|
51
|
+
if (i < 0)
|
|
36
52
|
return undefined;
|
|
37
|
-
const
|
|
38
|
-
|
|
53
|
+
const snap = snaps[i];
|
|
54
|
+
// What the record replaced held, which a record put back over it would lose.
|
|
55
|
+
const { ticks, since, rebuilt, state, slot, restored } = live;
|
|
56
|
+
let h;
|
|
57
|
+
if (mine && live.reaches) {
|
|
58
|
+
// Its chain may still read its number from it, and its store has just stamped it.
|
|
59
|
+
h = copyHeldInto(voice, snap.held, live, true);
|
|
60
|
+
h.slot = slot;
|
|
61
|
+
h.restored = restored;
|
|
62
|
+
}
|
|
63
|
+
else
|
|
64
|
+
h = copyHeld(voice, snap.held, true);
|
|
65
|
+
h.weight = 0;
|
|
66
|
+
if (snap.at < t)
|
|
67
|
+
h.copied = true;
|
|
68
|
+
const kept = i + 1;
|
|
69
|
+
// The record replaced is let go, so its list is cut where it stands.
|
|
70
|
+
if (mine)
|
|
71
|
+
trim(voice, snaps, kept);
|
|
72
|
+
else
|
|
73
|
+
for (let i = 0; i < kept; i++)
|
|
74
|
+
snaps[i].shared = true;
|
|
75
|
+
h.snaps = mine ? snaps : snaps.slice(0, kept);
|
|
39
76
|
h.inputs = inputs;
|
|
77
|
+
// A start still ahead when the copy was kept may have moved before `t`, with no copy between.
|
|
78
|
+
if (!(h.since <= snap.at))
|
|
79
|
+
origin(mix, voice, h);
|
|
80
|
+
// The record a seek replaces, stepped on from this copy with nothing since rebuilding or moving
|
|
81
|
+
// its start, holds the copy's state at its own `ticks`, which a probe there need not step to.
|
|
82
|
+
const was = mine && ticks > h.ticks && since === h.since && rebuilt === h.rebuilt;
|
|
83
|
+
if (voice.patch.step !== undefined && (snap.ahead !== undefined || t - snap.at >= every || was)) {
|
|
84
|
+
let ahead = snap.ahead;
|
|
85
|
+
// Steps counted from a start that has moved since are not this record's.
|
|
86
|
+
if (ahead === undefined || ahead.since !== h.since) {
|
|
87
|
+
ahead = {
|
|
88
|
+
put: h,
|
|
89
|
+
sought: t,
|
|
90
|
+
since: h.since,
|
|
91
|
+
at: snap.at,
|
|
92
|
+
ticks: h.ticks,
|
|
93
|
+
state: undefined,
|
|
94
|
+
leftTicks: 0,
|
|
95
|
+
left: undefined,
|
|
96
|
+
};
|
|
97
|
+
snap.ahead = ahead;
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
ahead.put = h;
|
|
101
|
+
ahead.sought = t;
|
|
102
|
+
}
|
|
103
|
+
if (was) {
|
|
104
|
+
// Nothing else holds the record replaced, so its state is taken, not copied.
|
|
105
|
+
ahead.leftTicks = ticks;
|
|
106
|
+
ahead.left = state;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
40
109
|
return h;
|
|
41
110
|
}
|
|
111
|
+
/** Cuts a record's list of copies to its first `kept`, keeping those no other list holds as spares. */
|
|
112
|
+
function trim(voice, snaps, kept) {
|
|
113
|
+
while (snaps.length > kept) {
|
|
114
|
+
const snap = snaps.pop();
|
|
115
|
+
if (snap.shared)
|
|
116
|
+
continue;
|
|
117
|
+
snap.ahead = undefined;
|
|
118
|
+
voice.spares ??= [];
|
|
119
|
+
voice.spares.push(snap);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
42
122
|
/** Takes a voice cued after the moment sought out of the mix, until the tape cues it again. */
|
|
43
123
|
function park(mix, voice) {
|
|
44
124
|
const owner = voice.owner;
|
|
@@ -71,9 +151,15 @@ function restoreVoice(mix, voice, t, seq) {
|
|
|
71
151
|
voice.parts = null;
|
|
72
152
|
}
|
|
73
153
|
if (voice.parted !== null) {
|
|
74
|
-
for (const [subject, p] of voice.parted)
|
|
75
|
-
if (p.seq
|
|
76
|
-
|
|
154
|
+
for (const [subject, p] of voice.parted) {
|
|
155
|
+
if (p.seq <= seq)
|
|
156
|
+
continue;
|
|
157
|
+
voice.parted.delete(subject);
|
|
158
|
+
if (p.ramp === undefined || p.ramp.seq > seq)
|
|
159
|
+
continue;
|
|
160
|
+
voice.parts ??= new Map();
|
|
161
|
+
voice.parts.set(subject, p.ramp);
|
|
162
|
+
}
|
|
77
163
|
if (voice.parted.size === 0)
|
|
78
164
|
voice.parted = null;
|
|
79
165
|
}
|
|
@@ -86,32 +172,31 @@ function restoreVoice(mix, voice, t, seq) {
|
|
|
86
172
|
voice.unreached = null;
|
|
87
173
|
voice.unreachedBits = null;
|
|
88
174
|
voice.seeks++;
|
|
175
|
+
voice.spares = null;
|
|
89
176
|
const left = voice.left?.all() ?? [];
|
|
177
|
+
if (voice.left !== null)
|
|
178
|
+
fadesAt(voice, voice.left.after(cut), seq);
|
|
90
179
|
voice.left = Leavings.from(left.filter((e) => e.seq <= seq));
|
|
91
180
|
const future = Leavings.from(left.filter((e) => e.seq > seq));
|
|
92
181
|
let records = voice.subjects;
|
|
93
182
|
if (!(records instanceof Restored)) {
|
|
94
|
-
|
|
95
|
-
|
|
183
|
+
const every = apart(mix.opts.history);
|
|
184
|
+
const now = (subject, held, at) => heldAt(voice, subject, held, at.t, at.seq);
|
|
185
|
+
const restored = new Restored(records, now, (live, at, mine) => {
|
|
186
|
+
const h = restore(mix, voice, live, at, every, mine);
|
|
96
187
|
if (h === undefined)
|
|
97
188
|
return h;
|
|
98
189
|
if (h.reaches)
|
|
99
190
|
voice.seen++;
|
|
100
191
|
if (h.rested)
|
|
101
192
|
voice.restedCount++;
|
|
102
|
-
if (!(h.since <=
|
|
103
|
-
// Kept while its origin was still ahead: where the controls put back place it now.
|
|
104
|
-
h.since = mix.sinceOf(voice, h.delay);
|
|
105
|
-
h.shown = mix.shownOf(voice, h.since);
|
|
106
|
-
if (h.ticks === 0 && h.stepped > h.since)
|
|
107
|
-
h.stepped = h.since;
|
|
108
|
-
}
|
|
109
|
-
if (!(h.since <= top)) {
|
|
193
|
+
if (!(h.since <= restored.top)) {
|
|
110
194
|
voice.early ??= [];
|
|
111
195
|
voice.early.push(h);
|
|
112
196
|
}
|
|
113
197
|
return h;
|
|
114
198
|
});
|
|
199
|
+
records = restored;
|
|
115
200
|
voice.subjects = records;
|
|
116
201
|
}
|
|
117
202
|
records.back(t, cut, future);
|
|
@@ -127,8 +212,11 @@ function stateAt(voice, t) {
|
|
|
127
212
|
return 'frozen';
|
|
128
213
|
return 'live';
|
|
129
214
|
}
|
|
130
|
-
/**
|
|
131
|
-
|
|
215
|
+
/**
|
|
216
|
+
* Moves every mix on the transport to mix time `to`, as it stood at the end of that frame, or to
|
|
217
|
+
* the end of the frame numbered `to.frame`.
|
|
218
|
+
*/
|
|
219
|
+
export function seek(transport, to) {
|
|
132
220
|
if (transport.projecting)
|
|
133
221
|
throw new Error('blits: a projection does not seek');
|
|
134
222
|
const history = transport.history;
|
|
@@ -139,25 +227,42 @@ export function seek(transport, t) {
|
|
|
139
227
|
throw new Error('blits: seeking needs history.tape, which keeps the calls a seek plays again');
|
|
140
228
|
if (Number.isNaN(transport.now))
|
|
141
229
|
throw new Error('blits: a mix that has never synced has nothing to seek');
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if (
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
230
|
+
let t;
|
|
231
|
+
let frame;
|
|
232
|
+
if (typeof to === 'number') {
|
|
233
|
+
t = to;
|
|
234
|
+
if (t === Number.POSITIVE_INFINITY)
|
|
235
|
+
throw new RangeError('blits: a mix seeks to a finite time');
|
|
236
|
+
if (!(t >= transport.born))
|
|
237
|
+
throw new Error(`blits: ${t} is older than this mix's history reaches`);
|
|
238
|
+
cover(transport, t);
|
|
239
|
+
if (t < transport.now) {
|
|
240
|
+
frame = transport.frames.at(t);
|
|
241
|
+
if (frame === undefined)
|
|
242
|
+
throw new Error(`blits: ${t} is older than this mix's history reaches`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
const f = coverFrame(transport, to.frame);
|
|
247
|
+
t = f.at;
|
|
248
|
+
// The frame being played is where the mix stands; any other is behind it, whatever its time.
|
|
249
|
+
if (f.seq !== transport.seq)
|
|
250
|
+
frame = f;
|
|
251
|
+
}
|
|
252
|
+
if (frame === undefined) {
|
|
148
253
|
// Ahead, under the rate the recorded calls up to `t` set, past any frames rate 0 held at `t`.
|
|
149
254
|
const last = replay(transport, (at) => (transport.pace === null ? at : transport.pace.reading(at)) <= t);
|
|
150
|
-
const
|
|
255
|
+
const at = hostAhead(transport, t);
|
|
256
|
+
const u = Number.isNaN(last) ? at : Math.max(last, at);
|
|
151
257
|
if (!Number.isFinite(u))
|
|
152
258
|
throw new RangeError(`blits: the mix's rate never reaches ${t}`);
|
|
153
259
|
transport.u = u;
|
|
154
|
-
|
|
155
|
-
|
|
260
|
+
// A call played at `t` itself moved the mixes there before it, as a sync does.
|
|
261
|
+
if (Number.isNaN(last) || transport.now !== t)
|
|
262
|
+
for (const m of transport.members)
|
|
263
|
+
move(m, t);
|
|
156
264
|
}
|
|
157
265
|
else {
|
|
158
|
-
const frame = transport.frames.at(t);
|
|
159
|
-
if (frame === undefined)
|
|
160
|
-
throw new Error(`blits: ${t} is older than this mix's history reaches`);
|
|
161
266
|
// On the frame itself, its own host time: rate 0 may have held the clock there for several.
|
|
162
267
|
const u = frame.at === t ? frame.u : hostAt(transport, t);
|
|
163
268
|
const seq = frame.seq;
|
|
@@ -219,13 +324,17 @@ function back(mix, t, seq, on) {
|
|
|
219
324
|
mix.retired = [];
|
|
220
325
|
// Owners hold what is back; a voice that went live again counts its place in its owner anew.
|
|
221
326
|
for (const voice of kept)
|
|
222
|
-
if (voice.holding !== null)
|
|
327
|
+
if (voice.holding !== null) {
|
|
223
328
|
voice.holding.children = kept.filter((v) => v.owner === voice);
|
|
329
|
+
voice.holding.back(seq);
|
|
330
|
+
}
|
|
224
331
|
// Rebuilt from the voices that are back: the lanes, the chains and every count the fold reads.
|
|
332
|
+
// Subject numbers carry over, and with them the chains that hold them, each linked afresh.
|
|
225
333
|
const oldLanes = mix.lanes;
|
|
226
|
-
if (oldLanes !== null)
|
|
227
|
-
mix.lanes = new
|
|
228
|
-
|
|
334
|
+
if (oldLanes !== null && mix.lanesOf !== null)
|
|
335
|
+
mix.lanes = new mix.lanesOf(laneHost(mix), oldLanes);
|
|
336
|
+
else
|
|
337
|
+
mix.chains = new Store();
|
|
229
338
|
mix.owners = kept.some((v) => v.holding !== null) ? kept.filter((v) => v.holding !== null) : null;
|
|
230
339
|
recount(mix);
|
|
231
340
|
mix.parters.clear();
|
package/dist/shared.d.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The package version, for a fact whose shape follows the code too closely to version by hand:
|
|
3
|
+
* copies of different releases keep apart. `test/copies.test.ts` holds it to `package.json`.
|
|
4
|
+
*/
|
|
5
|
+
export declare const release = "0.12.0";
|
|
1
6
|
/**
|
|
2
7
|
* A WeakMap every copy of blits loaded in one realm shares, so a fact one copy records about a
|
|
3
8
|
* patch or channel it made is read by another copy's mix. The name carries a version, bumped when
|
package/dist/shared.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The package version, for a fact whose shape follows the code too closely to version by hand:
|
|
3
|
+
* copies of different releases keep apart. `test/copies.test.ts` holds it to `package.json`.
|
|
4
|
+
*/
|
|
5
|
+
export const release = '0.12.0';
|
|
1
6
|
/**
|
|
2
7
|
* A WeakMap every copy of blits loaded in one realm shares, so a fact one copy records about a
|
|
3
8
|
* patch or channel it made is read by another copy's mix. The name carries a version, bumped when
|
package/dist/signals.d.ts
CHANGED
|
@@ -7,6 +7,16 @@ export declare const heard: (signal: object) => boolean;
|
|
|
7
7
|
* @category signal
|
|
8
8
|
*/
|
|
9
9
|
export declare function peak<I, H = unknown>(...signals: readonly Signal<I, H>[]): Signal<I, H>;
|
|
10
|
+
/** A blend member's share of its signal's read `k`: the members' stops sit evenly along 0..1. */
|
|
11
|
+
export declare function shareOf(k: number, i: number, stops: number): number;
|
|
12
|
+
/**
|
|
13
|
+
* Member `i` of `n`'s share of `by`, as `mix.blend` weighs it: the hat of `by`'s read at the
|
|
14
|
+
* member's stop, the stops spaced evenly from 0 to 1. Weights a voice outside a blend as one of
|
|
15
|
+
* its members, such as one member played alone.
|
|
16
|
+
*
|
|
17
|
+
* @category signal
|
|
18
|
+
*/
|
|
19
|
+
export declare function share<I, H = unknown>(by: Signal<I, H>, i: number, n: number): Signal<I, H>;
|
|
10
20
|
/**
|
|
11
21
|
* Marks `read` as a signal driven from outside the clock, which a host's own signal has to be for
|
|
12
22
|
* `assess` and a history's `inputs` to treat it as one. Two uses:
|
package/dist/signals.js
CHANGED
|
@@ -27,6 +27,23 @@ export function peak(...signals) {
|
|
|
27
27
|
return out;
|
|
28
28
|
}, signals);
|
|
29
29
|
}
|
|
30
|
+
/** A blend member's share of its signal's read `k`: the members' stops sit evenly along 0..1. */
|
|
31
|
+
export function shareOf(k, i, stops) {
|
|
32
|
+
const d = stops === 0 ? 0 : Math.abs(k * stops - i);
|
|
33
|
+
return d >= 1 ? 0 : 1 - d;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Member `i` of `n`'s share of `by`, as `mix.blend` weighs it: the hat of `by`'s read at the
|
|
37
|
+
* member's stop, the stops spaced evenly from 0 to 1. Weights a voice outside a blend as one of
|
|
38
|
+
* its members, such as one member played alone.
|
|
39
|
+
*
|
|
40
|
+
* @category signal
|
|
41
|
+
*/
|
|
42
|
+
export function share(by, i, n) {
|
|
43
|
+
if (!Number.isInteger(n) || n < 1 || !Number.isInteger(i) || i < 0 || i >= n)
|
|
44
|
+
throw new Error(`blits: share wants a member 0 to ${n - 1}, not ${i}`);
|
|
45
|
+
return marked((subject, setting) => shareOf(by(subject, setting), i, n - 1), [by]);
|
|
46
|
+
}
|
|
30
47
|
const touch = () => {
|
|
31
48
|
reading.inputs++;
|
|
32
49
|
};
|
package/dist/spans.d.ts
CHANGED
|
@@ -22,6 +22,11 @@ export declare function checkChild<I, O>(spec: VoiceSpec<I, O>, child: Voice<I,
|
|
|
22
22
|
* after.
|
|
23
23
|
*/
|
|
24
24
|
export declare function refit<I, O>(mix: Mixer<I, O>, span: Voice<I, O>, from?: number): void;
|
|
25
|
+
/**
|
|
26
|
+
* Fits again each span with a child still to start whose anchored `earliest` answers differently
|
|
27
|
+
* than at its last fit: an anchor that has come to have an answer, or moved.
|
|
28
|
+
*/
|
|
29
|
+
export declare function refloor<I, O>(mix: Mixer<I, O>): void;
|
|
25
30
|
/** Whether a span whose last child just left at mix time `at` stays, its budget not yet out. */
|
|
26
31
|
export declare function lingers<I, O>(span: Voice<I, O>, at: number): boolean;
|
|
27
32
|
/** Lets the spans that are empty and out of budget leave, at the moment their budget ran out. */
|
package/dist/spans.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { arcEnd } from './arcs.js';
|
|
1
2
|
import { elapsedWith, retime } from './clock.js';
|
|
2
3
|
import { retire } from './fade.js';
|
|
3
4
|
import { defaultFit, layout, settle } from './fit.js';
|
|
@@ -6,6 +7,7 @@ import { noted } from './history.js';
|
|
|
6
7
|
import { nextFrame } from './move.js';
|
|
7
8
|
import { leave } from './origin.js';
|
|
8
9
|
import { ownerReading } from './owner.js';
|
|
10
|
+
import { resolve } from './place.js';
|
|
9
11
|
/** What a span keeps beside its owner: its claim, its fit, and how the last fit came out. */
|
|
10
12
|
export class Fitting {
|
|
11
13
|
budget;
|
|
@@ -39,9 +41,13 @@ export class Fitting {
|
|
|
39
41
|
export function checkChild(spec, child) {
|
|
40
42
|
if (spec.start !== undefined || spec.anchor?.start !== undefined || spec.anchor?.in !== undefined)
|
|
41
43
|
throw new Error('blits: a span places the voices it holds, so one takes no start of its own');
|
|
42
|
-
if (child.
|
|
44
|
+
if (child.arc !== null) {
|
|
45
|
+
if (!Number.isFinite(arcEnd(child)))
|
|
46
|
+
throw new Error('blits: a span cannot fit an arc held open, which never ends');
|
|
47
|
+
}
|
|
48
|
+
else if (child.holding !== null) {
|
|
43
49
|
if (child.fitting === null)
|
|
44
|
-
throw new Error('blits: a span holds voices and
|
|
50
|
+
throw new Error('blits: a span holds voices, spans and arcs, not owners');
|
|
45
51
|
}
|
|
46
52
|
else if (!Number.isFinite(child.span))
|
|
47
53
|
throw new Error('blits: a span cannot fit a voice that never ends');
|
|
@@ -59,7 +65,7 @@ function elapsedIn(child, local) {
|
|
|
59
65
|
}
|
|
60
66
|
/** How much of a child is left, voice ms at its own clock: what its span still has to fit. */
|
|
61
67
|
function leftOf(child, local) {
|
|
62
|
-
const length = child.fitting !== null ? child.fitting.end : child.span;
|
|
68
|
+
const length = child.fitting !== null ? child.fitting.end : child.arc !== null ? arcEnd(child) : child.span;
|
|
63
69
|
return Math.max(0, length - elapsedIn(child, local));
|
|
64
70
|
}
|
|
65
71
|
/**
|
|
@@ -73,10 +79,11 @@ export function refit(mix, span, from = mix.now) {
|
|
|
73
79
|
if (mix.projecting || span.state === 'done')
|
|
74
80
|
return;
|
|
75
81
|
const local = clockOf(span, from);
|
|
76
|
-
const kids
|
|
77
|
-
const claims = kids.map((c) => {
|
|
82
|
+
const { kids, floors, waiting } = lineUp(mix, span, local);
|
|
83
|
+
const claims = kids.map((c, i) => {
|
|
78
84
|
const s = c.spec;
|
|
79
85
|
return {
|
|
86
|
+
floor: floors[i],
|
|
80
87
|
natural: leftOf(c, local) / (s.rate ?? 1),
|
|
81
88
|
state: c.state === 'pending' ? 'pending' : 'playing',
|
|
82
89
|
faster: Math.max(1, s.faster ?? 1),
|
|
@@ -127,6 +134,17 @@ export function refit(mix, span, from = mix.now) {
|
|
|
127
134
|
noted(mix, c);
|
|
128
135
|
}
|
|
129
136
|
});
|
|
137
|
+
for (const c of waiting) {
|
|
138
|
+
if (c.start === Number.POSITIVE_INFINITY)
|
|
139
|
+
continue;
|
|
140
|
+
c.start = Number.POSITIVE_INFINITY;
|
|
141
|
+
c.anchorNow = Number.POSITIVE_INFINITY;
|
|
142
|
+
c.anchorElapsed = 0;
|
|
143
|
+
c.clocks = null;
|
|
144
|
+
mix.frame = nextFrame();
|
|
145
|
+
reorigin(mix, c, mix.now);
|
|
146
|
+
noted(mix, c);
|
|
147
|
+
}
|
|
130
148
|
if (kids.length > 0)
|
|
131
149
|
mix.lanes?.refill();
|
|
132
150
|
const reach = local + length;
|
|
@@ -147,6 +165,58 @@ export function refit(mix, span, from = mix.now) {
|
|
|
147
165
|
refit(mix, span.owner, from);
|
|
148
166
|
}
|
|
149
167
|
}
|
|
168
|
+
/**
|
|
169
|
+
* A span's children still to fit, in the order it lays them out: those playing first, as cued, then
|
|
170
|
+
* those still to start by rank. Each pending one's floor is where its `earliest` answers, span ms
|
|
171
|
+
* from `local`, and before `local` only in the frame it was cued. One whose anchor has no answer yet waits unplaced, and under `queue` or `stagger`
|
|
172
|
+
* so does every child after it, since each is laid out from the one before.
|
|
173
|
+
*/
|
|
174
|
+
function lineUp(mix, span, local) {
|
|
175
|
+
const live = (span.holding?.children ?? []).filter((c) => c.state !== 'done' && c.state !== 'frozen' && c.state !== 'fading' && leftOf(c, local) > 0);
|
|
176
|
+
const kids = live.filter((c) => c.state !== 'pending');
|
|
177
|
+
const floors = kids.map(() => undefined);
|
|
178
|
+
const waiting = [];
|
|
179
|
+
const pending = live
|
|
180
|
+
.filter((c) => c.state === 'pending')
|
|
181
|
+
.sort((a, b) => (a.spec.rank ?? 0) - (b.spec.rank ?? 0));
|
|
182
|
+
const chained = span.fitting.claim.order !== 'together';
|
|
183
|
+
for (const c of pending) {
|
|
184
|
+
const e = c.spec.earliest;
|
|
185
|
+
const t = e === undefined ? undefined : resolve(mix, e, c, Number.NEGATIVE_INFINITY);
|
|
186
|
+
c.floorSeen = t ?? Number.NaN;
|
|
187
|
+
if ((e !== undefined && t === undefined) || (chained && waiting.length > 0)) {
|
|
188
|
+
waiting.push(c);
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
kids.push(c);
|
|
192
|
+
// An anchor that answered before the span began has no time on its clock: no bound.
|
|
193
|
+
const floor = t === undefined || t === Number.NEGATIVE_INFINITY ? undefined : t - local;
|
|
194
|
+
// A past floor places a child only as it is cued: one waiting since waits on those before it.
|
|
195
|
+
floors.push(floor === undefined || c.cuedSeq === mix.transport.seq ? floor : Math.max(0, floor));
|
|
196
|
+
}
|
|
197
|
+
return { kids, floors, waiting };
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Fits again each span with a child still to start whose anchored `earliest` answers differently
|
|
201
|
+
* than at its last fit: an anchor that has come to have an answer, or moved.
|
|
202
|
+
*/
|
|
203
|
+
export function refloor(mix) {
|
|
204
|
+
if (mix.projecting)
|
|
205
|
+
return;
|
|
206
|
+
for (const span of mix.owners ?? []) {
|
|
207
|
+
if (span.fitting === null || span.state === 'done')
|
|
208
|
+
continue;
|
|
209
|
+
const moved = (span.holding?.children ?? []).some((c) => {
|
|
210
|
+
const e = c.spec.earliest;
|
|
211
|
+
if (c.state !== 'pending' || e === undefined || typeof e === 'number')
|
|
212
|
+
return false;
|
|
213
|
+
const t = resolve(mix, e, c, Number.NEGATIVE_INFINITY) ?? Number.NaN;
|
|
214
|
+
return !Object.is(t, c.floorSeen);
|
|
215
|
+
});
|
|
216
|
+
if (moved)
|
|
217
|
+
refit(mix, span);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
150
220
|
/** Whether a span whose last child just left at mix time `at` stays, its budget not yet out. */
|
|
151
221
|
export function lingers(span, at) {
|
|
152
222
|
const f = span.fitting;
|
package/dist/store.d.ts
CHANGED
|
@@ -16,7 +16,20 @@ export declare class Store<K, V> {
|
|
|
16
16
|
private oneValue;
|
|
17
17
|
get(key: K): V | undefined;
|
|
18
18
|
set(key: K, value: V): void;
|
|
19
|
+
/** As `get`, where `guess` may be the value for `key`, which a store that can tell spares a lookup. */
|
|
20
|
+
find(key: K, _guess: V): V | undefined;
|
|
19
21
|
has(key: K): boolean;
|
|
20
22
|
delete(key: K): void;
|
|
21
23
|
clear(): void;
|
|
22
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* A store over another, for a copy that must not write the one it reads: what is set or deleted
|
|
27
|
+
* here stays here, and a key deleted here is no longer read from the one under it.
|
|
28
|
+
*/
|
|
29
|
+
export declare class Masked<K, V> extends Store<K, V> {
|
|
30
|
+
private readonly under;
|
|
31
|
+
private readonly hidden;
|
|
32
|
+
constructor(under: Store<K, V>);
|
|
33
|
+
get(key: K): V | undefined;
|
|
34
|
+
delete(key: K): void;
|
|
35
|
+
}
|
package/dist/store.js
CHANGED
|
@@ -43,6 +43,10 @@ export class Store {
|
|
|
43
43
|
}
|
|
44
44
|
this.strong.set(key, value);
|
|
45
45
|
}
|
|
46
|
+
/** As `get`, where `guess` may be the value for `key`, which a store that can tell spares a lookup. */
|
|
47
|
+
find(key, _guess) {
|
|
48
|
+
return this.get(key);
|
|
49
|
+
}
|
|
46
50
|
has(key) {
|
|
47
51
|
return this.get(key) !== undefined;
|
|
48
52
|
}
|
|
@@ -62,6 +66,25 @@ export class Store {
|
|
|
62
66
|
this.strong.clear();
|
|
63
67
|
}
|
|
64
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* A store over another, for a copy that must not write the one it reads: what is set or deleted
|
|
71
|
+
* here stays here, and a key deleted here is no longer read from the one under it.
|
|
72
|
+
*/
|
|
73
|
+
export class Masked extends Store {
|
|
74
|
+
under;
|
|
75
|
+
hidden = new Store();
|
|
76
|
+
constructor(under) {
|
|
77
|
+
super();
|
|
78
|
+
this.under = under;
|
|
79
|
+
}
|
|
80
|
+
get(key) {
|
|
81
|
+
return super.get(key) ?? (this.hidden.get(key) ? undefined : this.under.get(key));
|
|
82
|
+
}
|
|
83
|
+
delete(key) {
|
|
84
|
+
super.delete(key);
|
|
85
|
+
this.hidden.set(key, true);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
65
88
|
/** No key held inline. */
|
|
66
89
|
const none = Symbol('none');
|
|
67
90
|
/** A Map's key equality, SameValueZero: NaN finds NaN. */
|
package/dist/strays.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { unpart } from './fade.js';
|
|
2
2
|
import { pageOut } from './paging.js';
|
|
3
|
-
import { record } from './tape.js';
|
|
3
|
+
import { record, unplayable } from './tape.js';
|
|
4
4
|
/** Notes a motion patch that may hold state for a subject no voice playing it reaches. */
|
|
5
5
|
export function stray(mix, motion, subject) {
|
|
6
6
|
const list = mix.strays.get(subject);
|
|
@@ -49,7 +49,12 @@ export function motionOwner(mix) {
|
|
|
49
49
|
const m = mix.deref();
|
|
50
50
|
if (m === undefined)
|
|
51
51
|
return;
|
|
52
|
-
record(m, 'motion', () => {
|
|
52
|
+
record(m, 'motion', (stage) => {
|
|
53
|
+
const to = stage.mix(m);
|
|
54
|
+
if (to === undefined)
|
|
55
|
+
return;
|
|
56
|
+
if (to !== m)
|
|
57
|
+
return changeAhead(to, id, motion, subject, change);
|
|
53
58
|
const v = cuedById(m, id) ?? m.gone.find((g) => g.id === id);
|
|
54
59
|
(v?.motion ?? motion).change(subject, { ...change });
|
|
55
60
|
});
|
|
@@ -79,6 +84,49 @@ export function motionOwner(mix) {
|
|
|
79
84
|
},
|
|
80
85
|
};
|
|
81
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* A retarget or push the tape holds, made for a read ahead: kept on its transport for the patch
|
|
89
|
+
* to apply as it reads, since the patch's own state is the live mix's.
|
|
90
|
+
*/
|
|
91
|
+
function changeAhead(to, id, motion, given, change) {
|
|
92
|
+
const subject = given;
|
|
93
|
+
const v = cuedById(to, id);
|
|
94
|
+
let at = change.at;
|
|
95
|
+
if (v !== undefined) {
|
|
96
|
+
// As the patch asks its mix live: a subject faded out of the voice is back, and a change with
|
|
97
|
+
// no time takes the voice's at this frame, or the subject's own where it is yet to be met.
|
|
98
|
+
const parted = v.parted?.has(subject) === true;
|
|
99
|
+
if (parted)
|
|
100
|
+
unpart(to, v, subject);
|
|
101
|
+
if (at === undefined) {
|
|
102
|
+
const frame = frameOf(to, v, subject);
|
|
103
|
+
const back = parted ? timeIn(to, v, v.spec.stagger ? v.spec.stagger(subject) : 0) : frame;
|
|
104
|
+
if (!Number.isNaN(frame))
|
|
105
|
+
at = frame;
|
|
106
|
+
else if (!Number.isNaN(back))
|
|
107
|
+
at = back;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (at === undefined)
|
|
111
|
+
unplayable('a change to a motion made before its voice met the subject');
|
|
112
|
+
const stamped = at;
|
|
113
|
+
const made = to.transport.ahead;
|
|
114
|
+
let bySubject = made.get(motion);
|
|
115
|
+
if (bySubject === undefined) {
|
|
116
|
+
bySubject = new Map();
|
|
117
|
+
made.set(motion, bySubject);
|
|
118
|
+
}
|
|
119
|
+
const list = bySubject.get(subject);
|
|
120
|
+
const c = { at, to: change.to, v: change.v };
|
|
121
|
+
if (list === undefined)
|
|
122
|
+
bySubject.set(subject, [c]);
|
|
123
|
+
else {
|
|
124
|
+
let i = list.length;
|
|
125
|
+
while (i > 0 && list[i - 1].at > stamped)
|
|
126
|
+
i--;
|
|
127
|
+
list.splice(i, 0, c);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
82
130
|
/** The voice in the list with this id, which is in id order. */
|
|
83
131
|
export function cuedById(mix, id) {
|
|
84
132
|
const cued = mix.cued;
|
package/dist/tape.d.ts
CHANGED
|
@@ -1,24 +1,49 @@
|
|
|
1
|
+
import type { Mixer } from './mixer.js';
|
|
1
2
|
import type { Pace } from './pace.js';
|
|
2
3
|
import type { Transport } from './transport.js';
|
|
3
4
|
import type { Tape } from './types.js';
|
|
5
|
+
/**
|
|
6
|
+
* Where a recorded call is made again: on the mixes the host made it on, or, for a read ahead, on
|
|
7
|
+
* its copies of them.
|
|
8
|
+
*/
|
|
9
|
+
export interface Stage {
|
|
10
|
+
transport(of: Transport): Transport;
|
|
11
|
+
/** The mix standing for `of`; undefined where a read ahead has no copy of it. */
|
|
12
|
+
mix<I, O>(of: Mixer<I, O>): Mixer<I, O> | undefined;
|
|
13
|
+
}
|
|
14
|
+
/** Refuses a recorded call a read ahead's copies cannot take. */
|
|
15
|
+
export declare function unplayable(what: string): never;
|
|
4
16
|
/**
|
|
5
17
|
* The tape `history.tape` makes for a transport, stamping each call with the host time it was made
|
|
6
18
|
* at: rate 0 holds mix time still across frames, and host time tells them apart.
|
|
7
19
|
*/
|
|
8
20
|
export declare function tapeOf(transport: Transport): Tape | undefined;
|
|
9
|
-
/**
|
|
21
|
+
/**
|
|
22
|
+
* Records a host call, which `again` makes once more when the mix plays past it after a seek, or
|
|
23
|
+
* when a read ahead does, on the stage it is given.
|
|
24
|
+
*/
|
|
10
25
|
export declare function record(on: {
|
|
11
26
|
readonly tape: Tape | undefined;
|
|
12
27
|
readonly replaying: boolean;
|
|
13
28
|
readonly projecting: boolean;
|
|
14
|
-
}, label: string, again: () => void): void;
|
|
29
|
+
}, label: string, again: (stage: Stage) => void): void;
|
|
15
30
|
/** The host time at which the mix clock reads `t`, under the rate it has now. */
|
|
16
31
|
export declare function hostAt(clock: {
|
|
17
32
|
readonly pace: Pace | null;
|
|
18
33
|
}, t: number): number;
|
|
34
|
+
/**
|
|
35
|
+
* The host time a move ahead to mix time `t` lands at: never behind the clock's own, since rate 0
|
|
36
|
+
* reads `t` at every host time from the earliest `hostAt` gives, and a call stamped earlier than
|
|
37
|
+
* one already on the tape would cut it out of order.
|
|
38
|
+
*/
|
|
39
|
+
export declare function hostAhead(clock: {
|
|
40
|
+
readonly pace: Pace | null;
|
|
41
|
+
readonly u: number;
|
|
42
|
+
}, t: number): number;
|
|
19
43
|
/**
|
|
20
44
|
* Makes the recorded calls `due` by the host time each was made at again, each host time's in a
|
|
21
45
|
* frame of its own with every mix on the transport moved to where it stood then, as the host made
|
|
22
|
-
* them. Answers the host time of the last frame it played, NaN for none.
|
|
46
|
+
* them. Answers the host time of the last frame it played, NaN for none. Given `onto` and the
|
|
47
|
+
* stage of its copies, it makes them on those, and the caller puts the tape back.
|
|
23
48
|
*/
|
|
24
|
-
export declare function replay(transport: Transport, due: (u: number) => boolean): number;
|
|
49
|
+
export declare function replay(transport: Transport, due: (u: number) => boolean, onto?: Transport, on?: Stage): number;
|