@msb235/blits 0.7.0 → 0.7.1
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 +16 -0
- package/README.md +1 -1
- package/dist/cue.js +2 -0
- package/dist/fade.js +2 -0
- package/dist/gone.d.ts +32 -0
- package/dist/gone.js +134 -0
- package/dist/history.js +2 -0
- package/dist/hosts.js +2 -1
- package/dist/mixer.d.ts +4 -0
- package/dist/mixer.js +10 -1
- package/dist/move.js +8 -4
- package/dist/place.js +172 -29
- package/dist/scored.d.ts +57 -0
- package/dist/scored.js +196 -0
- package/dist/transport.d.ts +4 -0
- package/dist/transport.js +13 -1
- package/dist/unpage.js +2 -0
- package/dist/voice.d.ts +2 -0
- package/dist/voice.js +3 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,22 @@ This package follows [semver](https://semver.org). Below 1.0.0, a breaking chang
|
|
|
4
4
|
version and everything else the patch. Each release lists its changes as **Breaking**, **Added** and
|
|
5
5
|
**Fixed**, and the release workflow refuses a tag with no section here.
|
|
6
6
|
|
|
7
|
+
## 0.7.1
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Under `history`, a mix whose subjects come and go no longer slows as the voices that have left
|
|
12
|
+
pile up. `drop` and a freed slot visit only the voices that left that can hold the subject,
|
|
13
|
+
history lets go of them from the front of the list, and the tape is pruned a second at a time
|
|
14
|
+
rather than every sync. A 200-subject roster replacing 4 subjects a frame climbed to about 1.2 ms
|
|
15
|
+
a frame as 14,000 voices that had left built up; it now holds at about 0.065 ms.
|
|
16
|
+
- An anchor naming a score reads only the voices on that score, and only in the mixes on the
|
|
17
|
+
transport that hold one; it reads each voice that has left once rather than every frame, and
|
|
18
|
+
picks in one pass instead of sorting. astv's busy stage, whose rail waits on the latest `coast`
|
|
19
|
+
of its motion score, takes about 0.45 ms a frame in astv's motion bench, against 4.3 ms on 0.7.0.
|
|
20
|
+
With 200 mixes on scores of their own beside it, a frame's sync takes about 14 µs, against 21
|
|
21
|
+
before the transport listed the mixes holding each score (Node, medians of six alternated runs).
|
|
22
|
+
|
|
7
23
|
## 0.7.0
|
|
8
24
|
|
|
9
25
|
### Breaking
|
package/README.md
CHANGED
|
@@ -100,7 +100,7 @@ so the engine keeps no dependencies; its README says how it works.
|
|
|
100
100
|
|
|
101
101
|
## Status
|
|
102
102
|
|
|
103
|
-
v0.7.
|
|
103
|
+
v0.7.1, with five consumers. magicsmoke runs every fault on it, wod its wheel, tricks and
|
|
104
104
|
transitions, weasel its animator's tweens, sherpa its seams, and
|
|
105
105
|
[klieg](https://github.com/orochi235/klieg) composes all three of its systems — letter motion,
|
|
106
106
|
part effects, and the environment a sign is lit by — on a mix each.
|
package/dist/cue.js
CHANGED
|
@@ -8,6 +8,7 @@ import { motionOf } from './motion.js';
|
|
|
8
8
|
import { adopt, ownerPatch, ownerReading } from './owner.js';
|
|
9
9
|
import { durationOf } from './patch.js';
|
|
10
10
|
import { checkPlacement, localNow, mixAt, pin, place } from './place.js';
|
|
11
|
+
import { scoredAdd } from './scored.js';
|
|
11
12
|
import { checkChild, refit } from './spans.js';
|
|
12
13
|
import { motionOwner } from './strays.js';
|
|
13
14
|
import { record } from './tape.js';
|
|
@@ -127,6 +128,7 @@ function enter(mix, voice) {
|
|
|
127
128
|
while (i > 0 && cued[i - 1].id > voice.id)
|
|
128
129
|
i--;
|
|
129
130
|
cued.splice(i, 0, voice);
|
|
131
|
+
scoredAdd(mix, voice);
|
|
130
132
|
index(mix, voice);
|
|
131
133
|
changed(mix, voice);
|
|
132
134
|
mix.stir();
|
package/dist/fade.js
CHANGED
|
@@ -4,6 +4,7 @@ import { schedule } from './due.js';
|
|
|
4
4
|
import { leave, noted, releasing } from './history.js';
|
|
5
5
|
import { orphan } from './owner.js';
|
|
6
6
|
import { startOf } from './place.js';
|
|
7
|
+
import { scoreTouched } from './scored.js';
|
|
7
8
|
import { lingers, refit } from './spans.js';
|
|
8
9
|
import { unreach } from './unreached.js';
|
|
9
10
|
/** Starts one subject's ramp out of a voice, or takes it out at once for a ramp of 0. */
|
|
@@ -162,6 +163,7 @@ export function retire(mix, voice, at) {
|
|
|
162
163
|
if (motion !== undefined && asks(mix, motion, voice))
|
|
163
164
|
motion.owner = null;
|
|
164
165
|
voice.state = 'done';
|
|
166
|
+
scoreTouched(mix, voice);
|
|
165
167
|
// The record its setting last wrote to, which a retired voice no longer calls for.
|
|
166
168
|
voice.keepOn = null;
|
|
167
169
|
mix.retired.push(voice);
|
package/dist/gone.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Mixer } from './mixer.js';
|
|
2
|
+
import { Store } from './store.js';
|
|
3
|
+
import type { Voice } from './voice.js';
|
|
4
|
+
/**
|
|
5
|
+
* A mix's gone voices as `drop` and a freed slot look for them: those over every subject, and the
|
|
6
|
+
* rest by each subject they name. Kept in step with `gone`, which only grows by push and is
|
|
7
|
+
* otherwise replaced whole, so it catches up lazily and rebuilds only on a replacement it did not
|
|
8
|
+
* make itself. Under history a churning mix holds thousands, too many to walk per subject dropped.
|
|
9
|
+
*/
|
|
10
|
+
export declare class GoneIndex<I, O> {
|
|
11
|
+
/** The `gone` list it was built over, and how much of it. */
|
|
12
|
+
of: readonly Voice<I, O>[] | null;
|
|
13
|
+
count: number;
|
|
14
|
+
general: Voice<I, O>[];
|
|
15
|
+
named: Store<I, Voice<I, O>[]>;
|
|
16
|
+
/** The latest `doneAt` in `of`, and how far any voice finished before one listed ahead of it. */
|
|
17
|
+
latest: number;
|
|
18
|
+
slack: number;
|
|
19
|
+
}
|
|
20
|
+
/** The index caught up with `mix.gone`. */
|
|
21
|
+
export declare function goneIndex<I, O>(mix: Mixer<I, O>): GoneIndex<I, O>;
|
|
22
|
+
/**
|
|
23
|
+
* Keeps the gone voices `keep` says yes to, asking it once each, and takes the rest out of the
|
|
24
|
+
* index one by one. Leaves `gone` as it was when every one stays.
|
|
25
|
+
*/
|
|
26
|
+
export declare function keepGone<I, O>(mix: Mixer<I, O>, keep: (v: Voice<I, O>) => boolean): void;
|
|
27
|
+
/**
|
|
28
|
+
* Lets go of the gone voices that finished before `reach`. Voices go in about the order they
|
|
29
|
+
* finish, so only the front can hold one: past the point where a voice finished `slack` after
|
|
30
|
+
* `reach`, none behind it finished before `reach`.
|
|
31
|
+
*/
|
|
32
|
+
export declare function expireGone<I, O>(mix: Mixer<I, O>, reach: number): void;
|
package/dist/gone.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { scoredCut } from './scored.js';
|
|
2
|
+
import { Store } from './store.js';
|
|
3
|
+
/**
|
|
4
|
+
* A mix's gone voices as `drop` and a freed slot look for them: those over every subject, and the
|
|
5
|
+
* rest by each subject they name. Kept in step with `gone`, which only grows by push and is
|
|
6
|
+
* otherwise replaced whole, so it catches up lazily and rebuilds only on a replacement it did not
|
|
7
|
+
* make itself. Under history a churning mix holds thousands, too many to walk per subject dropped.
|
|
8
|
+
*/
|
|
9
|
+
export class GoneIndex {
|
|
10
|
+
/** The `gone` list it was built over, and how much of it. */
|
|
11
|
+
of = null;
|
|
12
|
+
count = 0;
|
|
13
|
+
general = [];
|
|
14
|
+
named = new Store();
|
|
15
|
+
/** The latest `doneAt` in `of`, and how far any voice finished before one listed ahead of it. */
|
|
16
|
+
latest = Number.NEGATIVE_INFINITY;
|
|
17
|
+
slack = 0;
|
|
18
|
+
}
|
|
19
|
+
function add(ix, voice) {
|
|
20
|
+
if (voice.doneAt < ix.latest)
|
|
21
|
+
ix.slack = Math.max(ix.slack, ix.latest - voice.doneAt);
|
|
22
|
+
else
|
|
23
|
+
ix.latest = voice.doneAt;
|
|
24
|
+
if (voice.named === null) {
|
|
25
|
+
ix.general.push(voice);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
for (const subject of voice.named) {
|
|
29
|
+
const list = ix.named.get(subject);
|
|
30
|
+
if (list === undefined)
|
|
31
|
+
ix.named.set(subject, [voice]);
|
|
32
|
+
else
|
|
33
|
+
list.push(voice);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function remove(ix, out) {
|
|
37
|
+
let general = false;
|
|
38
|
+
for (const v of out) {
|
|
39
|
+
if (v.named === null) {
|
|
40
|
+
general = true;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
for (const subject of v.named) {
|
|
44
|
+
const named = ix.named.get(subject);
|
|
45
|
+
if (named === undefined)
|
|
46
|
+
continue;
|
|
47
|
+
const at = named.indexOf(v);
|
|
48
|
+
if (at >= 0)
|
|
49
|
+
named.splice(at, 1);
|
|
50
|
+
if (named.length === 0)
|
|
51
|
+
ix.named.delete(subject);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (general) {
|
|
55
|
+
const leaving = new Set(out);
|
|
56
|
+
ix.general = ix.general.filter((v) => !leaving.has(v));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** The index caught up with `mix.gone`. */
|
|
60
|
+
export function goneIndex(mix) {
|
|
61
|
+
const ix = mix.goneIx;
|
|
62
|
+
const list = mix.gone;
|
|
63
|
+
if (ix.of !== list || ix.count > list.length) {
|
|
64
|
+
ix.general = [];
|
|
65
|
+
ix.named = new Store();
|
|
66
|
+
ix.count = 0;
|
|
67
|
+
ix.of = list;
|
|
68
|
+
ix.latest = Number.NEGATIVE_INFINITY;
|
|
69
|
+
ix.slack = 0;
|
|
70
|
+
}
|
|
71
|
+
for (let i = ix.count; i < list.length; i++)
|
|
72
|
+
add(ix, list[i]);
|
|
73
|
+
ix.count = list.length;
|
|
74
|
+
return ix;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Keeps the gone voices `keep` says yes to, asking it once each, and takes the rest out of the
|
|
78
|
+
* index one by one. Leaves `gone` as it was when every one stays.
|
|
79
|
+
*/
|
|
80
|
+
export function keepGone(mix, keep) {
|
|
81
|
+
const ix = goneIndex(mix);
|
|
82
|
+
const list = mix.gone;
|
|
83
|
+
let i = 0;
|
|
84
|
+
while (i < list.length && keep(list[i]))
|
|
85
|
+
i++;
|
|
86
|
+
if (i === list.length)
|
|
87
|
+
return;
|
|
88
|
+
const kept = list.slice(0, i);
|
|
89
|
+
const out = [list[i]];
|
|
90
|
+
for (i++; i < list.length; i++) {
|
|
91
|
+
const v = list[i];
|
|
92
|
+
(keep(v) ? kept : out).push(v);
|
|
93
|
+
}
|
|
94
|
+
remove(ix, out);
|
|
95
|
+
mix.gone = kept;
|
|
96
|
+
scoredCut(mix, list, out);
|
|
97
|
+
ix.of = kept;
|
|
98
|
+
ix.count = kept.length;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Lets go of the gone voices that finished before `reach`. Voices go in about the order they
|
|
102
|
+
* finish, so only the front can hold one: past the point where a voice finished `slack` after
|
|
103
|
+
* `reach`, none behind it finished before `reach`.
|
|
104
|
+
*/
|
|
105
|
+
export function expireGone(mix, reach) {
|
|
106
|
+
const ix = goneIndex(mix);
|
|
107
|
+
if (!Number.isFinite(ix.slack)) {
|
|
108
|
+
keepGone(mix, (v) => v.doneAt >= reach);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const list = mix.gone;
|
|
112
|
+
const edge = reach + ix.slack;
|
|
113
|
+
let end = 0;
|
|
114
|
+
let any = false;
|
|
115
|
+
for (let seen = Number.NEGATIVE_INFINITY; end < list.length && seen < edge; end++) {
|
|
116
|
+
const done = list[end].doneAt;
|
|
117
|
+
if (done < reach)
|
|
118
|
+
any = true;
|
|
119
|
+
if (done > seen)
|
|
120
|
+
seen = done;
|
|
121
|
+
}
|
|
122
|
+
if (!any)
|
|
123
|
+
return;
|
|
124
|
+
const kept = [];
|
|
125
|
+
const out = [];
|
|
126
|
+
for (let i = 0; i < end; i++) {
|
|
127
|
+
const v = list[i];
|
|
128
|
+
(v.doneAt < reach ? out : kept).push(v);
|
|
129
|
+
}
|
|
130
|
+
list.splice(0, end, ...kept);
|
|
131
|
+
remove(ix, out);
|
|
132
|
+
scoredCut(mix, list, out);
|
|
133
|
+
ix.count = list.length;
|
|
134
|
+
}
|
package/dist/history.js
CHANGED
|
@@ -3,6 +3,7 @@ import { clone } from './clone.js';
|
|
|
3
3
|
import { schedule } from './due.js';
|
|
4
4
|
import { packHeld } from './pack.js';
|
|
5
5
|
import { pageOut } from './paging.js';
|
|
6
|
+
import { scoreTouched } from './scored.js';
|
|
6
7
|
import { none } from './voice.js';
|
|
7
8
|
/**
|
|
8
9
|
* The last entry taken before `t`, or at it where `inclusive` says so, as it does for a change a
|
|
@@ -52,6 +53,7 @@ function ownerThen(mix, owner, t) {
|
|
|
52
53
|
}
|
|
53
54
|
/** Records a change to a voice's controls under `history`, and lets go of what it no longer reaches. */
|
|
54
55
|
export function noted(mix, voice) {
|
|
56
|
+
scoreTouched(mix, voice, true);
|
|
55
57
|
mix.stir();
|
|
56
58
|
schedule(mix, voice);
|
|
57
59
|
const log = voice.log;
|
package/dist/hosts.js
CHANGED
|
@@ -3,6 +3,7 @@ import { numericOf } from './channels.js';
|
|
|
3
3
|
import { crowdable } from './crowd.js';
|
|
4
4
|
import { schedule } from './due.js';
|
|
5
5
|
import { beginFade, beginRise, fadeAt, fadeSubject } from './fade.js';
|
|
6
|
+
import { goneIndex } from './gone.js';
|
|
6
7
|
import { VoiceHandle } from './handle.js';
|
|
7
8
|
import { noted, remember } from './history.js';
|
|
8
9
|
import { hostOf, listed, markOf, readingAt } from './marks.js';
|
|
@@ -57,7 +58,7 @@ export function laneHost(mix) {
|
|
|
57
58
|
// Only a voice over every subject keeps bits, and one gone may still be read back.
|
|
58
59
|
for (const voice of mix.general)
|
|
59
60
|
unreach(voice, slot, false);
|
|
60
|
-
for (const voice of mix.
|
|
61
|
+
for (const voice of goneIndex(mix).general)
|
|
61
62
|
unreach(voice, slot, false);
|
|
62
63
|
},
|
|
63
64
|
passes: (was, w) => mix.passes(was, w),
|
package/dist/mixer.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { type LerpInto } from './channels.js';
|
|
|
5
5
|
import type { Due } from './due.js';
|
|
6
6
|
import { parting } from './fade.js';
|
|
7
7
|
import { apply, baseFor, clamp, contribution, fold, folded, foldLoci, foldWith, isRest, type Key, keyed, linked, owedBy, passes, read, slopeFor, tick } from './fold.js';
|
|
8
|
+
import { GoneIndex } from './gone.js';
|
|
8
9
|
import type { HandleHost } from './handle.js';
|
|
9
10
|
import { held, shownOf, sinceOf, unreachedOf } from './held.js';
|
|
10
11
|
import { Lanes } from './lanes.js';
|
|
@@ -15,6 +16,7 @@ import { Keys } from './paging.js';
|
|
|
15
16
|
import { keep } from './pull.js';
|
|
16
17
|
import { Steps } from './relink.js';
|
|
17
18
|
import { Outs } from './revive.js';
|
|
19
|
+
import { ScoreIndex } from './scored.js';
|
|
18
20
|
import { Fitting } from './spans.js';
|
|
19
21
|
import { Store } from './store.js';
|
|
20
22
|
import { type Announced, Transport } from './transport.js';
|
|
@@ -27,6 +29,8 @@ export declare class Mixer<I, O> implements Mix<I, O> {
|
|
|
27
29
|
cued: Voice<I, O>[];
|
|
28
30
|
/** Under `history`, voices that have left but that a read back may still reach. */
|
|
29
31
|
gone: Voice<I, O>[];
|
|
32
|
+
goneIx: GoneIndex<I, O>;
|
|
33
|
+
scored: ScoreIndex<I, O>;
|
|
30
34
|
/** Set on a projection's own mixer: it sends nothing, keeps no history, and reads without committing. */
|
|
31
35
|
projecting: boolean;
|
|
32
36
|
/** Set on a projection reading back: a subject it has nothing on starts from its voice's start. */
|
package/dist/mixer.js
CHANGED
|
@@ -4,6 +4,7 @@ import { lerpInto } from './channels.js';
|
|
|
4
4
|
import { cue } from './cue.js';
|
|
5
5
|
import { beginFade, parting } from './fade.js';
|
|
6
6
|
import { apply, baseFor, clamp, contribution, fold, folded, foldLoci, foldWith, isRest, keyed, linked, near, owedBy, passes, read, slopeFor, tick, } from './fold.js';
|
|
7
|
+
import { GoneIndex, goneIndex } from './gone.js';
|
|
7
8
|
import { held, shownOf, sinceOf, unreachedOf } from './held.js';
|
|
8
9
|
import { leave, releasing } from './history.js';
|
|
9
10
|
import { book, laneHost } from './hosts.js';
|
|
@@ -17,6 +18,7 @@ import { projectAll } from './project.js';
|
|
|
17
18
|
import { keep, pull } from './pull.js';
|
|
18
19
|
import { Steps } from './relink.js';
|
|
19
20
|
import { Outs } from './revive.js';
|
|
21
|
+
import { ScoreIndex } from './scored.js';
|
|
20
22
|
import { seek } from './seek.js';
|
|
21
23
|
import { Fitting, refit } from './spans.js';
|
|
22
24
|
import { Store } from './store.js';
|
|
@@ -31,6 +33,8 @@ export class Mixer {
|
|
|
31
33
|
cued = [];
|
|
32
34
|
/** Under `history`, voices that have left but that a read back may still reach. */
|
|
33
35
|
gone = [];
|
|
36
|
+
goneIx = new GoneIndex();
|
|
37
|
+
scored = new ScoreIndex();
|
|
34
38
|
/** Set on a projection's own mixer: it sends nothing, keeps no history, and reads without committing. */
|
|
35
39
|
projecting = false;
|
|
36
40
|
/** Set on a projection reading back: a subject it has nothing on starts from its voice's start. */
|
|
@@ -462,8 +466,13 @@ export class Mixer {
|
|
|
462
466
|
if (named !== undefined)
|
|
463
467
|
for (const voice of [...named])
|
|
464
468
|
forget(voice);
|
|
465
|
-
|
|
469
|
+
const gone = goneIndex(this);
|
|
470
|
+
for (const voice of gone.general)
|
|
466
471
|
forget(voice);
|
|
472
|
+
const goneNaming = gone.named.get(subject);
|
|
473
|
+
if (goneNaming !== undefined)
|
|
474
|
+
for (const voice of goneNaming)
|
|
475
|
+
forget(voice);
|
|
467
476
|
if (this.owners !== null)
|
|
468
477
|
for (const voice of this.owners)
|
|
469
478
|
forget(voice);
|
package/dist/move.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { changed, unindex } from './chain.js';
|
|
2
2
|
import { popDue, schedule } from './due.js';
|
|
3
3
|
import { beginFade, part, retire } from './fade.js';
|
|
4
|
+
import { expireGone, keepGone } from './gone.js';
|
|
4
5
|
import { started } from './held.js';
|
|
5
6
|
import { ownerReading } from './owner.js';
|
|
6
7
|
import { mixAt, place, repin, startOf } from './place.js';
|
|
7
8
|
import { pageVoice } from './revive.js';
|
|
9
|
+
import { scoredLeft, scoredRemove } from './scored.js';
|
|
8
10
|
import { lapse } from './spans.js';
|
|
9
11
|
import { unplay } from './strays.js';
|
|
10
12
|
/** Numbers every mix's frames apart, so a blend's read is never taken for another mix's or frame's. */
|
|
@@ -173,8 +175,11 @@ function indexOf(voices, voice) {
|
|
|
173
175
|
}
|
|
174
176
|
/** Lets go of a retired voice's place in the mix; whether it was among the voices naming no subject. */
|
|
175
177
|
function leave(mix, voice) {
|
|
176
|
-
|
|
178
|
+
scoredRemove(mix, [voice]);
|
|
179
|
+
if (mix.opts.history) {
|
|
177
180
|
mix.gone.push(voice);
|
|
181
|
+
scoredLeft(mix, voice);
|
|
182
|
+
}
|
|
178
183
|
else
|
|
179
184
|
mix.parters.delete(voice);
|
|
180
185
|
unplay(mix, voice);
|
|
@@ -196,11 +201,10 @@ export function forget(mix) {
|
|
|
196
201
|
// With a store a voice that has left pages out where it can, and stays where it cannot.
|
|
197
202
|
if (mix.transport.pager !== null) {
|
|
198
203
|
const reach = mix.now - history.ms;
|
|
199
|
-
mix
|
|
204
|
+
keepGone(mix, (v) => v.doneAt >= reach || !pageVoice(mix, v, reach));
|
|
200
205
|
}
|
|
201
206
|
else {
|
|
202
|
-
|
|
203
|
-
mix.gone = mix.gone.filter((v) => v.doneAt >= reach);
|
|
207
|
+
expireGone(mix, mix.transport.keepsFrom());
|
|
204
208
|
}
|
|
205
209
|
}
|
|
206
210
|
}
|
package/dist/place.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { noted } from './history.js';
|
|
2
2
|
import { markOf } from './marks.js';
|
|
3
3
|
import { mixTime, ownerReading } from './owner.js';
|
|
4
|
+
import { holders, scored, scoredGone, scoreVersion, unhold } from './scored.js';
|
|
4
5
|
import { none } from './voice.js';
|
|
5
6
|
/** What a voice's own owner's clock reads now: the mix clock for a voice no owner holds. */
|
|
6
7
|
export function localNow(mix, voice) {
|
|
@@ -178,7 +179,7 @@ function resolve(mix, a, self) {
|
|
|
178
179
|
mark = 'start';
|
|
179
180
|
by = -by;
|
|
180
181
|
}
|
|
181
|
-
const t = timeOf(mix,
|
|
182
|
+
const t = timeOf(mix, query, mark, self);
|
|
182
183
|
if (t === undefined)
|
|
183
184
|
return undefined;
|
|
184
185
|
if (o === null)
|
|
@@ -200,46 +201,188 @@ function member(mix, a, self) {
|
|
|
200
201
|
}
|
|
201
202
|
return t;
|
|
202
203
|
}
|
|
204
|
+
/** Picks among the times a query meets, as its resolver says, in the order they are met. */
|
|
205
|
+
class Pick {
|
|
206
|
+
resolver;
|
|
207
|
+
now;
|
|
208
|
+
any = false;
|
|
209
|
+
firstOrder = Number.POSITIVE_INFINITY;
|
|
210
|
+
lastOrder = Number.NEGATIVE_INFINITY;
|
|
211
|
+
first;
|
|
212
|
+
last;
|
|
213
|
+
pick;
|
|
214
|
+
constructor(resolver, now) {
|
|
215
|
+
this.resolver = resolver;
|
|
216
|
+
this.now = now;
|
|
217
|
+
}
|
|
218
|
+
take(order, t) {
|
|
219
|
+
this.any = true;
|
|
220
|
+
if (order < this.firstOrder) {
|
|
221
|
+
this.firstOrder = order;
|
|
222
|
+
this.first = t;
|
|
223
|
+
}
|
|
224
|
+
if (order > this.lastOrder) {
|
|
225
|
+
this.lastOrder = order;
|
|
226
|
+
this.last = t;
|
|
227
|
+
}
|
|
228
|
+
if (t !== undefined)
|
|
229
|
+
this.consider(t);
|
|
230
|
+
}
|
|
231
|
+
/** Takes what `o` picked as though its times had been met here. */
|
|
232
|
+
merge(o) {
|
|
233
|
+
if (!o.any)
|
|
234
|
+
return;
|
|
235
|
+
this.any = true;
|
|
236
|
+
if (o.firstOrder < this.firstOrder) {
|
|
237
|
+
this.firstOrder = o.firstOrder;
|
|
238
|
+
this.first = o.first;
|
|
239
|
+
}
|
|
240
|
+
if (o.lastOrder > this.lastOrder) {
|
|
241
|
+
this.lastOrder = o.lastOrder;
|
|
242
|
+
this.last = o.last;
|
|
243
|
+
}
|
|
244
|
+
if (o.pick !== undefined)
|
|
245
|
+
this.consider(o.pick);
|
|
246
|
+
}
|
|
247
|
+
consider(t) {
|
|
248
|
+
const p = this.pick;
|
|
249
|
+
if (this.resolver === 'earliest'
|
|
250
|
+
? p === undefined || t < p
|
|
251
|
+
: this.resolver === 'latest'
|
|
252
|
+
? p === undefined || t > p
|
|
253
|
+
: this.resolver === 'next' && t >= this.now && (p === undefined || t < p))
|
|
254
|
+
this.pick = t;
|
|
255
|
+
}
|
|
256
|
+
answer() {
|
|
257
|
+
if (!this.any)
|
|
258
|
+
return undefined;
|
|
259
|
+
if (this.resolver === 'first')
|
|
260
|
+
return this.first;
|
|
261
|
+
if (this.resolver === 'last')
|
|
262
|
+
return this.last;
|
|
263
|
+
return this.pick;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
203
266
|
/**
|
|
204
267
|
* The mix time a query answers: `mark` of the voice it picks, among those cued and those that have
|
|
205
268
|
* left, never the asker; or a mark the host announced on the score, which is the same time for
|
|
206
269
|
* any of the four. Undefined while what it picks has no such time. A query naming no score looks
|
|
207
270
|
* among the asker's siblings, the voices its owner holds.
|
|
208
271
|
*/
|
|
209
|
-
function timeOf(mix,
|
|
272
|
+
function timeOf(mix, key, mark, self) {
|
|
273
|
+
const q = typeof key === 'string' ? { name: key } : key;
|
|
210
274
|
const score = q.score ?? self.spec.score;
|
|
211
275
|
const anywhere = q.score !== undefined;
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
276
|
+
const resolver = q.resolver ?? 'last';
|
|
277
|
+
const now = Number.isNaN(mix.now) ? Number.NEGATIVE_INFINITY : mix.now;
|
|
278
|
+
const picked = new Pick(resolver, now);
|
|
279
|
+
const matches = (v) => v !== self &&
|
|
280
|
+
v.spec.score === score &&
|
|
281
|
+
(anywhere || v.owner === self.owner) &&
|
|
282
|
+
(q.name === undefined || v.spec.name === q.name) &&
|
|
283
|
+
(q.tag === undefined || (v.spec.tags ?? none).includes(q.tag)) &&
|
|
284
|
+
(q.writes === undefined || v.patch.writes.includes(q.writes));
|
|
285
|
+
// A query naming a score looks in every mix on the transport that holds a voice on it; one
|
|
286
|
+
// naming none, in its own.
|
|
287
|
+
const listed = anywhere && score !== undefined && !mix.projecting;
|
|
288
|
+
const mixes = !anywhere
|
|
289
|
+
? [mix]
|
|
290
|
+
: listed
|
|
291
|
+
? holders(mix, score)
|
|
292
|
+
: mix.transport.members;
|
|
293
|
+
for (let i = 0; i < mixes.length; i++) {
|
|
294
|
+
const m = mixes[i];
|
|
295
|
+
if (score === undefined || m.projecting || mix.projecting) {
|
|
296
|
+
for (const v of [...m.gone, ...m.cued])
|
|
297
|
+
if (matches(v))
|
|
298
|
+
picked.take(v.id, markOf(m, v, mark));
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
const cued = scored(m, score);
|
|
302
|
+
const list = scoredGone(m, score);
|
|
303
|
+
if (cued.size === 0 && list.length === 0) {
|
|
304
|
+
if (listed) {
|
|
305
|
+
unhold(m, score);
|
|
306
|
+
i--;
|
|
307
|
+
}
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
// `next` asks what is still ahead of now, which a running pick cannot keep.
|
|
311
|
+
if (resolver === 'next') {
|
|
312
|
+
for (const v of cued)
|
|
313
|
+
if (matches(v))
|
|
314
|
+
picked.take(v.id, markOf(m, v, mark));
|
|
315
|
+
for (const v of list)
|
|
316
|
+
if (matches(v))
|
|
317
|
+
picked.take(v.id, markOf(m, v, mark));
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
const fold = foldOf(m, list, key, mark, self, resolver);
|
|
321
|
+
const version = scoreVersion(m, score);
|
|
322
|
+
const reduced = m.reduced;
|
|
323
|
+
if (fold.live === null ||
|
|
324
|
+
fold.liveOf !== m.cued ||
|
|
325
|
+
fold.liveVersion !== version ||
|
|
326
|
+
fold.liveReduced !== reduced) {
|
|
327
|
+
const live = new Pick(resolver, Number.NEGATIVE_INFINITY);
|
|
328
|
+
let keeps = true;
|
|
329
|
+
for (const v of cued) {
|
|
330
|
+
if (!matches(v))
|
|
331
|
+
continue;
|
|
332
|
+
if (v.owner !== null || v.holding !== null || v.fitting !== null)
|
|
333
|
+
keeps = false;
|
|
334
|
+
live.take(v.id, markOf(m, v, mark));
|
|
335
|
+
}
|
|
336
|
+
fold.live = live;
|
|
337
|
+
fold.liveOf = keeps ? m.cued : null;
|
|
338
|
+
fold.liveVersion = version;
|
|
339
|
+
fold.liveReduced = reduced;
|
|
340
|
+
}
|
|
341
|
+
picked.merge(fold.live);
|
|
342
|
+
for (; fold.seen < list.length; fold.seen++) {
|
|
343
|
+
const v = list[fold.seen];
|
|
344
|
+
if (!matches(v))
|
|
345
|
+
continue;
|
|
346
|
+
if (v.owner === null)
|
|
347
|
+
fold.pick.take(v.id, markOf(m, v, mark));
|
|
348
|
+
else
|
|
349
|
+
fold.owned.push(v);
|
|
350
|
+
}
|
|
351
|
+
picked.merge(fold.pick);
|
|
352
|
+
for (const v of fold.owned)
|
|
353
|
+
picked.take(v.id, markOf(m, v, mark));
|
|
354
|
+
}
|
|
224
355
|
if (q.writes === undefined && (anywhere || self.owner === null))
|
|
225
356
|
for (const a of mix.announced)
|
|
226
357
|
if (a.score === score &&
|
|
227
358
|
(score !== undefined || a.slot === mix.slot) &&
|
|
228
359
|
(q.name === undefined || a.name === q.name) &&
|
|
229
360
|
(q.tag === undefined || a.tags.includes(q.tag)))
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
if (
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
361
|
+
picked.take(a.order, Number.isNaN(a.at) ? undefined : mixAt(mix, a.at));
|
|
362
|
+
return picked.answer();
|
|
363
|
+
}
|
|
364
|
+
/** `self`'s read of `m`'s gone voices for one query, started over where the list has changed under it. */
|
|
365
|
+
function foldOf(m, list, key, mark, self, resolver) {
|
|
366
|
+
self.folds ??= new Map();
|
|
367
|
+
let byMix = self.folds.get(key);
|
|
368
|
+
if (byMix === undefined) {
|
|
369
|
+
byMix = new Map();
|
|
370
|
+
self.folds.set(key, byMix);
|
|
371
|
+
}
|
|
372
|
+
let fold = byMix.get(m);
|
|
373
|
+
if (fold === undefined || fold.list !== list || fold.mark !== mark) {
|
|
374
|
+
fold = {
|
|
375
|
+
list,
|
|
376
|
+
mark,
|
|
377
|
+
seen: 0,
|
|
378
|
+
pick: new Pick(resolver, Number.NEGATIVE_INFINITY),
|
|
379
|
+
owned: [],
|
|
380
|
+
live: null,
|
|
381
|
+
liveOf: null,
|
|
382
|
+
liveVersion: -1,
|
|
383
|
+
liveReduced: false,
|
|
384
|
+
};
|
|
385
|
+
byMix.set(m, fold);
|
|
386
|
+
}
|
|
387
|
+
return fold;
|
|
245
388
|
}
|
package/dist/scored.d.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { Mixer } from './mixer.js';
|
|
2
|
+
import type { Voice } from './voice.js';
|
|
3
|
+
/**
|
|
4
|
+
* A mix's voices by the score they were cued on, for an anchor that asks for a score's marks: the
|
|
5
|
+
* cued ones as a set, the gone ones in the order they left, which an anchor reads once each. Built
|
|
6
|
+
* over the lists it saw: a seek or a copy that replaces either rebuilds that half on the next read.
|
|
7
|
+
* A score whose gone voices history lets go of gets a fresh list, so a read of the old one starts
|
|
8
|
+
* over.
|
|
9
|
+
*/
|
|
10
|
+
export declare class ScoreIndex<I, O> {
|
|
11
|
+
cued: readonly Voice<I, O>[] | null;
|
|
12
|
+
by: Map<string, Set<Voice<I, O>>>;
|
|
13
|
+
gone: readonly Voice<I, O>[] | null;
|
|
14
|
+
goneBy: Map<string, Voice<I, O>[]>;
|
|
15
|
+
/** Bumped for a score each time a voice on it is cued, retimed, retired or leaves. */
|
|
16
|
+
versions: Map<string, number>;
|
|
17
|
+
/** The scores whose list on the transport names this mix. */
|
|
18
|
+
held: Set<string>;
|
|
19
|
+
}
|
|
20
|
+
type Member = Mixer<unknown, unknown>;
|
|
21
|
+
/**
|
|
22
|
+
* Per score, the mixes on a transport that may hold a voice on it, cued or gone, in the
|
|
23
|
+
* transport's order. Never short of one that does; one found holding none is let go on that read.
|
|
24
|
+
*/
|
|
25
|
+
export type ScoreHolders = Map<string, Member[]>;
|
|
26
|
+
/** The mixes on `mix`'s transport that may hold a voice on `score`; the list is the transport's own. */
|
|
27
|
+
export declare function holders<I, O>(mix: Mixer<I, O>, score: string): readonly Mixer<I, O>[];
|
|
28
|
+
/** Takes `mix` off `score`'s list, having found it holds nothing there. */
|
|
29
|
+
export declare function unhold<I, O>(mix: Mixer<I, O>, score: string): void;
|
|
30
|
+
/** A mix joining its transport, with whatever voices it already has. */
|
|
31
|
+
export declare function scoredJoin<I, O>(mix: Mixer<I, O>): void;
|
|
32
|
+
/** A mix leaving its transport. */
|
|
33
|
+
export declare function scoredDrop<I, O>(mix: Mixer<I, O>): void;
|
|
34
|
+
/**
|
|
35
|
+
* A voice on its score was cued, retimed, retired or left: what was read of the score is stale. A
|
|
36
|
+
* host can still retime a gone voice through a handle it kept, so `retimed` on a done voice
|
|
37
|
+
* replaces its score's gone list too, which starts every read of it over.
|
|
38
|
+
*/
|
|
39
|
+
export declare function scoreTouched<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>, retimed?: boolean): void;
|
|
40
|
+
/** How many times `score` has been touched in `mix`. */
|
|
41
|
+
export declare function scoreVersion<I, O>(mix: Mixer<I, O>, score: string): number;
|
|
42
|
+
/** The voices of `mix` cued on `score` and not yet gone, in no order. */
|
|
43
|
+
export declare function scored<I, O>(mix: Mixer<I, O>, score: string): ReadonlySet<Voice<I, O>>;
|
|
44
|
+
/** The voices of `mix` cued on `score` that have gone, in the order they left. */
|
|
45
|
+
export declare function scoredGone<I, O>(mix: Mixer<I, O>, score: string): readonly Voice<I, O>[];
|
|
46
|
+
/** A voice just pushed onto `gone`. */
|
|
47
|
+
export declare function scoredLeft<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>): void;
|
|
48
|
+
/**
|
|
49
|
+
* Gone voices history let go of, out of `was`; `mix.gone` is already what is left, whether that
|
|
50
|
+
* is `was` cut in place or a new list.
|
|
51
|
+
*/
|
|
52
|
+
export declare function scoredCut<I, O>(mix: Mixer<I, O>, was: readonly Voice<I, O>[], out: readonly Voice<I, O>[]): void;
|
|
53
|
+
/** A voice just cued. */
|
|
54
|
+
export declare function scoredAdd<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>): void;
|
|
55
|
+
/** Voices no longer cued. */
|
|
56
|
+
export declare function scoredRemove<I, O>(mix: Mixer<I, O>, voices: Iterable<Voice<I, O>>): void;
|
|
57
|
+
export {};
|
package/dist/scored.js
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A mix's voices by the score they were cued on, for an anchor that asks for a score's marks: the
|
|
3
|
+
* cued ones as a set, the gone ones in the order they left, which an anchor reads once each. Built
|
|
4
|
+
* over the lists it saw: a seek or a copy that replaces either rebuilds that half on the next read.
|
|
5
|
+
* A score whose gone voices history lets go of gets a fresh list, so a read of the old one starts
|
|
6
|
+
* over.
|
|
7
|
+
*/
|
|
8
|
+
export class ScoreIndex {
|
|
9
|
+
cued = null;
|
|
10
|
+
by = new Map();
|
|
11
|
+
gone = null;
|
|
12
|
+
goneBy = new Map();
|
|
13
|
+
/** Bumped for a score each time a voice on it is cued, retimed, retired or leaves. */
|
|
14
|
+
versions = new Map();
|
|
15
|
+
/** The scores whose list on the transport names this mix. */
|
|
16
|
+
held = new Set();
|
|
17
|
+
}
|
|
18
|
+
function hold(mix, score) {
|
|
19
|
+
const held = mix.scored.held;
|
|
20
|
+
// A dropped mix is off the transport's lists until a seek puts it back, and `scoredJoin` then.
|
|
21
|
+
if (held.has(score) || mix.dropped)
|
|
22
|
+
return;
|
|
23
|
+
held.add(score);
|
|
24
|
+
const by = mix.transport.holders;
|
|
25
|
+
const m = mix;
|
|
26
|
+
const list = by.get(score);
|
|
27
|
+
if (list === undefined) {
|
|
28
|
+
by.set(score, [m]);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const i = list.findIndex((o) => o.slot > m.slot);
|
|
32
|
+
if (i < 0)
|
|
33
|
+
list.push(m);
|
|
34
|
+
else
|
|
35
|
+
list.splice(i, 0, m);
|
|
36
|
+
}
|
|
37
|
+
/** The mixes on `mix`'s transport that may hold a voice on `score`; the list is the transport's own. */
|
|
38
|
+
export function holders(mix, score) {
|
|
39
|
+
return (mix.transport.holders.get(score) ?? none);
|
|
40
|
+
}
|
|
41
|
+
/** Takes `mix` off `score`'s list, having found it holds nothing there. */
|
|
42
|
+
export function unhold(mix, score) {
|
|
43
|
+
mix.scored.held.delete(score);
|
|
44
|
+
const by = mix.transport.holders;
|
|
45
|
+
const list = by.get(score);
|
|
46
|
+
if (list === undefined)
|
|
47
|
+
return;
|
|
48
|
+
const i = list.indexOf(mix);
|
|
49
|
+
if (i >= 0)
|
|
50
|
+
list.splice(i, 1);
|
|
51
|
+
if (list.length === 0)
|
|
52
|
+
by.delete(score);
|
|
53
|
+
}
|
|
54
|
+
/** A mix joining its transport, with whatever voices it already has. */
|
|
55
|
+
export function scoredJoin(mix) {
|
|
56
|
+
for (const v of mix.cued)
|
|
57
|
+
if (v.spec.score !== undefined)
|
|
58
|
+
hold(mix, v.spec.score);
|
|
59
|
+
for (const v of mix.gone)
|
|
60
|
+
if (v.spec.score !== undefined)
|
|
61
|
+
hold(mix, v.spec.score);
|
|
62
|
+
}
|
|
63
|
+
/** A mix leaving its transport. */
|
|
64
|
+
export function scoredDrop(mix) {
|
|
65
|
+
for (const score of [...mix.scored.held])
|
|
66
|
+
unhold(mix, score);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* A voice on its score was cued, retimed, retired or left: what was read of the score is stale. A
|
|
70
|
+
* host can still retime a gone voice through a handle it kept, so `retimed` on a done voice
|
|
71
|
+
* replaces its score's gone list too, which starts every read of it over.
|
|
72
|
+
*/
|
|
73
|
+
export function scoreTouched(mix, voice, retimed = false) {
|
|
74
|
+
const score = voice.spec.score;
|
|
75
|
+
if (score === undefined)
|
|
76
|
+
return;
|
|
77
|
+
const ix = mix.scored;
|
|
78
|
+
ix.versions.set(score, (ix.versions.get(score) ?? 0) + 1);
|
|
79
|
+
if (!retimed || voice.state !== 'done')
|
|
80
|
+
return;
|
|
81
|
+
const gone = ix.goneBy.get(score);
|
|
82
|
+
if (gone !== undefined)
|
|
83
|
+
ix.goneBy.set(score, [...gone]);
|
|
84
|
+
}
|
|
85
|
+
/** How many times `score` has been touched in `mix`. */
|
|
86
|
+
export function scoreVersion(mix, score) {
|
|
87
|
+
return mix.scored.versions.get(score) ?? 0;
|
|
88
|
+
}
|
|
89
|
+
function put(by, voice) {
|
|
90
|
+
const score = voice.spec.score;
|
|
91
|
+
if (score === undefined)
|
|
92
|
+
return;
|
|
93
|
+
const set = by.get(score);
|
|
94
|
+
if (set === undefined)
|
|
95
|
+
by.set(score, new Set([voice]));
|
|
96
|
+
else
|
|
97
|
+
set.add(voice);
|
|
98
|
+
}
|
|
99
|
+
/** The voices of `mix` cued on `score` and not yet gone, in no order. */
|
|
100
|
+
export function scored(mix, score) {
|
|
101
|
+
const ix = mix.scored;
|
|
102
|
+
if (ix.cued !== mix.cued) {
|
|
103
|
+
ix.by = new Map();
|
|
104
|
+
for (const v of mix.cued)
|
|
105
|
+
put(ix.by, v);
|
|
106
|
+
ix.cued = mix.cued;
|
|
107
|
+
}
|
|
108
|
+
return ix.by.get(score) ?? noVoices;
|
|
109
|
+
}
|
|
110
|
+
const noVoices = new Set();
|
|
111
|
+
/** The voices of `mix` cued on `score` that have gone, in the order they left. */
|
|
112
|
+
export function scoredGone(mix, score) {
|
|
113
|
+
const ix = mix.scored;
|
|
114
|
+
if (ix.gone !== mix.gone) {
|
|
115
|
+
ix.goneBy = new Map();
|
|
116
|
+
for (const v of mix.gone)
|
|
117
|
+
goneAdd(ix.goneBy, v);
|
|
118
|
+
ix.gone = mix.gone;
|
|
119
|
+
}
|
|
120
|
+
return ix.goneBy.get(score) ?? none;
|
|
121
|
+
}
|
|
122
|
+
const none = [];
|
|
123
|
+
function goneAdd(by, voice) {
|
|
124
|
+
const score = voice.spec.score;
|
|
125
|
+
if (score === undefined)
|
|
126
|
+
return;
|
|
127
|
+
const list = by.get(score);
|
|
128
|
+
if (list === undefined)
|
|
129
|
+
by.set(score, [voice]);
|
|
130
|
+
else
|
|
131
|
+
list.push(voice);
|
|
132
|
+
}
|
|
133
|
+
/** A voice just pushed onto `gone`. */
|
|
134
|
+
export function scoredLeft(mix, voice) {
|
|
135
|
+
if (voice.spec.score !== undefined)
|
|
136
|
+
hold(mix, voice.spec.score);
|
|
137
|
+
const ix = mix.scored;
|
|
138
|
+
if (ix.gone === mix.gone)
|
|
139
|
+
goneAdd(ix.goneBy, voice);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Gone voices history let go of, out of `was`; `mix.gone` is already what is left, whether that
|
|
143
|
+
* is `was` cut in place or a new list.
|
|
144
|
+
*/
|
|
145
|
+
export function scoredCut(mix, was, out) {
|
|
146
|
+
const ix = mix.scored;
|
|
147
|
+
if (ix.gone !== was)
|
|
148
|
+
return;
|
|
149
|
+
ix.gone = mix.gone;
|
|
150
|
+
const by = new Map();
|
|
151
|
+
for (const v of out) {
|
|
152
|
+
const score = v.spec.score;
|
|
153
|
+
if (score === undefined)
|
|
154
|
+
continue;
|
|
155
|
+
const set = by.get(score);
|
|
156
|
+
if (set === undefined)
|
|
157
|
+
by.set(score, new Set([v]));
|
|
158
|
+
else
|
|
159
|
+
set.add(v);
|
|
160
|
+
}
|
|
161
|
+
for (const [score, gone] of by) {
|
|
162
|
+
const left = (ix.goneBy.get(score) ?? []).filter((v) => !gone.has(v));
|
|
163
|
+
if (left.length === 0)
|
|
164
|
+
ix.goneBy.delete(score);
|
|
165
|
+
else
|
|
166
|
+
ix.goneBy.set(score, left);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/** A voice just cued. */
|
|
170
|
+
export function scoredAdd(mix, voice) {
|
|
171
|
+
scoreTouched(mix, voice);
|
|
172
|
+
if (voice.spec.score !== undefined)
|
|
173
|
+
hold(mix, voice.spec.score);
|
|
174
|
+
const ix = mix.scored;
|
|
175
|
+
if (ix.cued === mix.cued)
|
|
176
|
+
put(ix.by, voice);
|
|
177
|
+
}
|
|
178
|
+
/** Voices no longer cued. */
|
|
179
|
+
export function scoredRemove(mix, voices) {
|
|
180
|
+
for (const v of voices)
|
|
181
|
+
scoreTouched(mix, v);
|
|
182
|
+
const ix = mix.scored;
|
|
183
|
+
if (ix.cued !== mix.cued)
|
|
184
|
+
return;
|
|
185
|
+
for (const v of voices) {
|
|
186
|
+
const score = v.spec.score;
|
|
187
|
+
if (score === undefined)
|
|
188
|
+
continue;
|
|
189
|
+
const set = ix.by.get(score);
|
|
190
|
+
if (set === undefined)
|
|
191
|
+
continue;
|
|
192
|
+
set.delete(v);
|
|
193
|
+
if (set.size === 0)
|
|
194
|
+
ix.by.delete(score);
|
|
195
|
+
}
|
|
196
|
+
}
|
package/dist/transport.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Mixer } from './mixer.js';
|
|
2
2
|
import { Pace } from './pace.js';
|
|
3
3
|
import { Pager } from './paging.js';
|
|
4
|
+
import { type ScoreHolders } from './scored.js';
|
|
4
5
|
import type { Marked, Mix, MixOptions, Tape, Transport as TransportApi, TransportOptions, TransportProjection } from './types.js';
|
|
5
6
|
/**
|
|
6
7
|
* A mark the host announced on a score: `at` in host time, so the rate never moves one, and NaN
|
|
@@ -37,6 +38,7 @@ export declare class Transport implements TransportApi {
|
|
|
37
38
|
}[];
|
|
38
39
|
/** Numbers the mixes as they join, for their order and for whose unnamed score a mark is on. */
|
|
39
40
|
slots: number;
|
|
41
|
+
holders: ScoreHolders;
|
|
40
42
|
private wakeFns;
|
|
41
43
|
/** Whether it has woken since the last sync, so it wakes once. */
|
|
42
44
|
private woken;
|
|
@@ -51,6 +53,8 @@ export declare class Transport implements TransportApi {
|
|
|
51
53
|
floor: number;
|
|
52
54
|
last: number;
|
|
53
55
|
rebasing: boolean;
|
|
56
|
+
/** The reach the tape was last pruned to. */
|
|
57
|
+
taped: number;
|
|
54
58
|
/** The rate; null while it has never been set, when mix time is host time. */
|
|
55
59
|
pace: Pace | null;
|
|
56
60
|
/** True while the tape makes a recorded call again, so it is not recorded twice. */
|
package/dist/transport.js
CHANGED
|
@@ -4,8 +4,11 @@ import { Pace } from './pace.js';
|
|
|
4
4
|
import { Pager, prepare } from './paging.js';
|
|
5
5
|
import { pin } from './place.js';
|
|
6
6
|
import { projectAll } from './project.js';
|
|
7
|
+
import { scoredDrop, scoredJoin } from './scored.js';
|
|
7
8
|
import { seek } from './seek.js';
|
|
8
9
|
import { record, replay, tapeOf } from './tape.js';
|
|
10
|
+
/** How far the tape's reach moves before it is pruned again, ms of mix time. */
|
|
11
|
+
const TAPE_STEP_MS = 1000;
|
|
9
12
|
/**
|
|
10
13
|
* The clock its mixes play on: host time less what `rebase` took out, at the rate its `pace`
|
|
11
14
|
* sets, moved by `sync` and `seek`, and the tape of calls a seek plays again. A mix made alone
|
|
@@ -20,6 +23,7 @@ export class Transport {
|
|
|
20
23
|
dropped = [];
|
|
21
24
|
/** Numbers the mixes as they join, for their order and for whose unnamed score a mark is on. */
|
|
22
25
|
slots = 0;
|
|
26
|
+
holders = new Map();
|
|
23
27
|
wakeFns = [];
|
|
24
28
|
/** Whether it has woken since the last sync, so it wakes once. */
|
|
25
29
|
woken = false;
|
|
@@ -34,6 +38,8 @@ export class Transport {
|
|
|
34
38
|
floor = Number.NEGATIVE_INFINITY;
|
|
35
39
|
last = Number.NaN;
|
|
36
40
|
rebasing = false;
|
|
41
|
+
/** The reach the tape was last pruned to. */
|
|
42
|
+
taped = Number.NEGATIVE_INFINITY;
|
|
37
43
|
/** The rate; null while it has never been set, when mix time is host time. */
|
|
38
44
|
pace = null;
|
|
39
45
|
/** True while the tape makes a recorded call again, so it is not recorded twice. */
|
|
@@ -66,6 +72,7 @@ export class Transport {
|
|
|
66
72
|
this.members.push(mix);
|
|
67
73
|
else
|
|
68
74
|
this.members.splice(i, 0, mix);
|
|
75
|
+
scoredJoin(mix);
|
|
69
76
|
}
|
|
70
77
|
drop(mix) {
|
|
71
78
|
const m = mix;
|
|
@@ -73,6 +80,7 @@ export class Transport {
|
|
|
73
80
|
if (i < 0)
|
|
74
81
|
throw new Error(`blits: ${m.name ?? 'that mix'} is not on this transport`);
|
|
75
82
|
this.members.splice(i, 1);
|
|
83
|
+
scoredDrop(m);
|
|
76
84
|
m.dropped = true;
|
|
77
85
|
if (this.history !== undefined) {
|
|
78
86
|
this.dropped.push({ mix: m, at: this.now });
|
|
@@ -173,8 +181,12 @@ export class Transport {
|
|
|
173
181
|
const history = this.history;
|
|
174
182
|
this.kept();
|
|
175
183
|
const reach = this.keepsFrom();
|
|
176
|
-
|
|
184
|
+
// In steps: the tape shifts every entry it keeps to let go of the oldest, and one a second
|
|
185
|
+
// behind reach is never played again.
|
|
186
|
+
if (this.tape !== undefined && history !== undefined && !(reach - this.taped < TAPE_STEP_MS)) {
|
|
177
187
|
this.tape.prune(reach);
|
|
188
|
+
this.taped = reach;
|
|
189
|
+
}
|
|
178
190
|
if (history !== undefined && this.dropped.length > 0)
|
|
179
191
|
this.dropped = this.dropped.filter((d) => d.at >= reach);
|
|
180
192
|
this.woken = false;
|
package/dist/unpage.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { unpackHeld } from './pack.js';
|
|
2
2
|
import { HistoryMiss } from './paging.js';
|
|
3
3
|
import { reviveVoice } from './revive.js';
|
|
4
|
+
import { scoredLeft } from './scored.js';
|
|
4
5
|
/**
|
|
5
6
|
* Makes memory reach mix time `t` for a seek or read there, from what `prepare` loaded; true where
|
|
6
7
|
* it took records from the store. Throws before anything moves where it cannot.
|
|
@@ -66,6 +67,7 @@ function unpageMix(mix, records) {
|
|
|
66
67
|
const v = reviveVoice(mix, r.voice, r.data);
|
|
67
68
|
voices.set(v.id, v);
|
|
68
69
|
mix.gone.push(v);
|
|
70
|
+
scoredLeft(mix, v);
|
|
69
71
|
}
|
|
70
72
|
const host = records.filter((r) => r.stream === 'host');
|
|
71
73
|
mix.hostLog = before(host.map((r) => ({ at: r.at, fields: r.data })), mix.hostLog);
|
package/dist/voice.d.ts
CHANGED
|
@@ -222,6 +222,8 @@ export declare class Voice<I, O> {
|
|
|
222
222
|
placing: boolean;
|
|
223
223
|
/** What each member of a join last answered, kept for once its target has left. */
|
|
224
224
|
answers: Map<Anchor, number> | null;
|
|
225
|
+
/** What each of its anchors' queries has read of each mix's gone voices (`place.ts`'s `Fold`). */
|
|
226
|
+
folds: Map<unknown, Map<unknown, unknown>> | null;
|
|
225
227
|
/** The record `setting.keep` writes to: the one its patch or signal is being called for. */
|
|
226
228
|
keepOn: Subject<unknown> | null;
|
|
227
229
|
/** What `setting.keep` holds when called before any record is. */
|
package/dist/voice.js
CHANGED
|
@@ -175,6 +175,8 @@ export class Voice {
|
|
|
175
175
|
placing = false;
|
|
176
176
|
/** What each member of a join last answered, kept for once its target has left. */
|
|
177
177
|
answers = null;
|
|
178
|
+
/** What each of its anchors' queries has read of each mix's gone voices (`place.ts`'s `Fold`). */
|
|
179
|
+
folds = null;
|
|
178
180
|
/** The record `setting.keep` writes to: the one its patch or signal is being called for. */
|
|
179
181
|
keepOn = null;
|
|
180
182
|
/** What `setting.keep` holds when called before any record is. */
|
|
@@ -317,6 +319,7 @@ export class Voice {
|
|
|
317
319
|
this.doneAt = Number.POSITIVE_INFINITY;
|
|
318
320
|
this.placing = false;
|
|
319
321
|
this.answers = null;
|
|
322
|
+
this.folds = null;
|
|
320
323
|
this.keepOn = null;
|
|
321
324
|
this.ownKept = null;
|
|
322
325
|
this.unreached = null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@msb235/blits",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "Concurrent effects: each on its own clock with its own state and weight, folded into one value per subject per frame",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"animation",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"@biomejs/biome": "^2.5.8",
|
|
55
55
|
"@types/node": "^26.2.0",
|
|
56
|
-
"@weasel-js/history": "^1.9.
|
|
56
|
+
"@weasel-js/history": "^1.9.1",
|
|
57
57
|
"typescript": "^7.0.2",
|
|
58
58
|
"vitest": "^4.1.10"
|
|
59
59
|
}
|