@msb235/blits 0.9.0 → 0.11.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 +167 -0
- package/README.md +16 -5
- package/dist/blend.js +1 -5
- package/dist/book.d.ts +8 -5
- package/dist/book.js +47 -19
- package/dist/chain.d.ts +11 -0
- package/dist/chain.js +24 -2
- package/dist/clock.d.ts +14 -0
- package/dist/clock.js +9 -1
- 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 +52 -5
- package/dist/departed.d.ts +3 -2
- package/dist/departed.js +7 -4
- package/dist/due.d.ts +4 -0
- package/dist/due.js +16 -3
- package/dist/fade.d.ts +12 -5
- package/dist/fade.js +55 -17
- package/dist/fold.d.ts +1 -1
- package/dist/fold.js +53 -6
- package/dist/frames.d.ts +2 -0
- package/dist/frames.js +17 -0
- package/dist/handle.d.ts +5 -2
- package/dist/handle.js +20 -12
- package/dist/held.d.ts +8 -0
- package/dist/held.js +37 -6
- package/dist/history.d.ts +6 -0
- package/dist/history.js +15 -5
- package/dist/hosts.d.ts +1 -1
- package/dist/hosts.js +18 -6
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -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 +8 -0
- package/dist/lanes.js +21 -4
- 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 +102 -27
- package/dist/meet.js +1 -0
- package/dist/mixer.d.ts +23 -7
- package/dist/mixer.js +73 -29
- 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 +28 -10
- package/dist/owner.d.ts +18 -2
- package/dist/owner.js +38 -2
- package/dist/pack.d.ts +1 -0
- package/dist/pack.js +3 -0
- package/dist/placed.d.ts +19 -0
- package/dist/placed.js +54 -0
- package/dist/project.d.ts +5 -2
- package/dist/project.js +236 -32
- package/dist/pull.js +37 -8
- package/dist/qualify.js +8 -2
- package/dist/reading.d.ts +6 -0
- package/dist/reading.js +6 -0
- package/dist/record.js +1 -0
- package/dist/restored.d.ts +7 -4
- package/dist/restored.js +13 -10
- package/dist/revive.d.ts +5 -3
- package/dist/revive.js +10 -3
- package/dist/rows.js +15 -5
- package/dist/seek.d.ts +7 -2
- package/dist/seek.js +90 -35
- 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/store.d.ts +11 -0
- package/dist/store.js +19 -0
- package/dist/strays.js +50 -2
- package/dist/tape.d.ts +29 -4
- package/dist/tape.js +49 -11
- package/dist/transport.d.ts +11 -2
- package/dist/transport.js +20 -9
- package/dist/types/mix.d.ts +42 -15
- package/dist/types/patch.d.ts +5 -1
- package/dist/types/place.d.ts +25 -11
- package/dist/types/transport.d.ts +12 -3
- package/dist/types/voice.d.ts +62 -4
- package/dist/unpage.d.ts +6 -1
- package/dist/unpage.js +34 -2
- package/dist/voice.d.ts +57 -11
- package/dist/voice.js +43 -3
- package/docs/schema.html +75 -40
- package/package.json +7 -2
package/dist/crowd.js
CHANGED
|
@@ -317,7 +317,10 @@ function copyVoice(c, p) {
|
|
|
317
317
|
let f = c.hot[h + 0 /* Hot.FLAGS */] & ~(64 /* Flag.VOICE */ | 1 /* Flag.PLAYING */ | 2 /* Flag.FAST */ | 256 /* Flag.FREEZES */);
|
|
318
318
|
if (v.state === 'live' || v.state === 'frozen' || v.state === 'fading')
|
|
319
319
|
f |= 1 /* Flag.PLAYING */;
|
|
320
|
-
|
|
320
|
+
// A keys or fn voice an owner holds can turn back with the owner, which copies it no more.
|
|
321
|
+
if (v.freezesBefore ||
|
|
322
|
+
v.freezesAfter ||
|
|
323
|
+
(v.motion === undefined && (v.owner !== null || v.hidesPast)))
|
|
321
324
|
f |= 256 /* Flag.FREEZES */;
|
|
322
325
|
if (v.ramp === null &&
|
|
323
326
|
v.owner === null &&
|
package/dist/cue.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { blendOf } from './blend.js';
|
|
1
2
|
import { checkHits } from './book.js';
|
|
2
3
|
import { changed } from './chain.js';
|
|
3
4
|
import { copyable } from './clone.js';
|
|
@@ -6,14 +7,15 @@ import { shares } from './everyone.js';
|
|
|
6
7
|
import { finiteRate, plainWeight, VoiceHandle } from './handle.js';
|
|
7
8
|
import { begin } from './held.js';
|
|
8
9
|
import { handle } from './hosts.js';
|
|
10
|
+
import { checkMarks } from './marks.js';
|
|
9
11
|
import { motionOf } from './motion.js';
|
|
10
|
-
import { adopt, ownerPatch, ownerReading } from './owner.js';
|
|
12
|
+
import { adopt, Holding, ownerPatch, ownerReading } from './owner.js';
|
|
11
13
|
import { checkPlacement, localNow, mixAt, pin, place } from './place.js';
|
|
12
14
|
import { enlist } from './roster.js';
|
|
13
15
|
import { scoredAdd } from './scored.js';
|
|
14
16
|
import { checkChild, refit } from './spans.js';
|
|
15
|
-
import { motionOwner } from './strays.js';
|
|
16
|
-
import { record } from './tape.js';
|
|
17
|
+
import { cuedById, motionOwner } from './strays.js';
|
|
18
|
+
import { record, unplayable } from './tape.js';
|
|
17
19
|
import { Voice } from './voice.js';
|
|
18
20
|
export function cue(mix, spec) {
|
|
19
21
|
const patch = spec.patch;
|
|
@@ -28,6 +30,8 @@ export function cue(mix, spec) {
|
|
|
28
30
|
checkPlacement(mix, spec, anchor, owner);
|
|
29
31
|
if (spec.hits !== undefined)
|
|
30
32
|
checkHits(spec.hits, patch.duration);
|
|
33
|
+
if (spec.marks !== undefined)
|
|
34
|
+
checkMarks(spec.marks, patch.duration);
|
|
31
35
|
const anchored = anchor !== undefined && (anchor.start !== undefined || anchor.in !== undefined);
|
|
32
36
|
// A voice an owner holds is placed on the owner's clock: ms from when it starts.
|
|
33
37
|
const local = owner === null ? mix.now : ownerReading(owner, mix.now);
|
|
@@ -51,7 +55,14 @@ export function cue(mix, spec) {
|
|
|
51
55
|
const h = handle(mix, voice);
|
|
52
56
|
voice.handle = h;
|
|
53
57
|
// By its handle, so a voice a history store paged out and a seek revived is the one cued again.
|
|
54
|
-
record(mix, 'cue', () =>
|
|
58
|
+
record(mix, 'cue', (stage) => {
|
|
59
|
+
const parked = VoiceHandle.voiceOf(h) ?? lost(h);
|
|
60
|
+
const to = stage.mix(mix);
|
|
61
|
+
if (to === mix)
|
|
62
|
+
recue(mix, parked);
|
|
63
|
+
else if (to !== undefined)
|
|
64
|
+
recue(to, aheadOf(to, parked));
|
|
65
|
+
});
|
|
55
66
|
return h;
|
|
56
67
|
}
|
|
57
68
|
/** Refuses a patch writing a channel `kit` lacks, or holds as another kind than it was written for. */
|
|
@@ -78,6 +89,10 @@ export function playable(mix, spec) {
|
|
|
78
89
|
finiteRate(spec.rate);
|
|
79
90
|
if (spec.seek !== undefined && !Number.isFinite(spec.seek))
|
|
80
91
|
throw new RangeError(`blits: a voice's seek is a finite number of ms, not ${spec.seek}`);
|
|
92
|
+
if (spec.stepMs !== undefined && !(spec.stepMs >= 0 && spec.stepMs < Number.POSITIVE_INFINITY))
|
|
93
|
+
throw new RangeError(`blits: a voice's stepMs is a finite number of ms, 0 or more, not ${spec.stepMs}`);
|
|
94
|
+
if (spec.catchUp !== undefined && spec.catchUp !== 'exact' && spec.catchUp !== 'fresh')
|
|
95
|
+
throw new RangeError(`blits: catchUp takes 'exact' or 'fresh', not ${String(spec.catchUp)}`);
|
|
81
96
|
if (typeof spec.weight === 'number')
|
|
82
97
|
plainWeight(spec.weight);
|
|
83
98
|
fitsKit(patch, mix.kit);
|
|
@@ -110,6 +125,36 @@ function recue(mix, voice) {
|
|
|
110
125
|
voice.state = 'live';
|
|
111
126
|
enter(mix, voice);
|
|
112
127
|
}
|
|
128
|
+
/** Each read ahead's own blends, by the live blend its copies' members were cued in. */
|
|
129
|
+
const blends = new WeakMap();
|
|
130
|
+
/** A read ahead's copy of a voice a seek parked, as its cue left it, for the copy `to` to cue. */
|
|
131
|
+
function aheadOf(to, parked) {
|
|
132
|
+
const copy = parked.copy(() => undefined);
|
|
133
|
+
if (parked.owner !== null) {
|
|
134
|
+
const owner = cuedById(to, parked.owner.id);
|
|
135
|
+
if (owner === undefined || owner.holding === null)
|
|
136
|
+
unplayable('a cue under an owner that has left');
|
|
137
|
+
copy.owner = owner;
|
|
138
|
+
}
|
|
139
|
+
if (copy.holding !== null)
|
|
140
|
+
copy.holding = new Holding();
|
|
141
|
+
if (copy.blend !== null) {
|
|
142
|
+
let own = blends.get(to);
|
|
143
|
+
if (own === undefined) {
|
|
144
|
+
own = new Map();
|
|
145
|
+
blends.set(to, own);
|
|
146
|
+
}
|
|
147
|
+
const live = copy.blend.of;
|
|
148
|
+
let of = own.get(live);
|
|
149
|
+
if (of === undefined) {
|
|
150
|
+
of = blendOf(copy.blend.of.by, copy.blend.of.stops);
|
|
151
|
+
own.set(live, of);
|
|
152
|
+
}
|
|
153
|
+
copy.blend = { of, i: copy.blend.i };
|
|
154
|
+
of.members.push(copy);
|
|
155
|
+
}
|
|
156
|
+
return copy;
|
|
157
|
+
}
|
|
113
158
|
function lost(h) {
|
|
114
159
|
throw new Error(`blits: voice ${h.id} is paged out, and the seek that plays its cue again did not load it`);
|
|
115
160
|
}
|
|
@@ -120,6 +165,7 @@ function enter(mix, voice) {
|
|
|
120
165
|
const anchor = spec.anchor;
|
|
121
166
|
const anchored = anchor !== undefined && (anchor.start !== undefined || anchor.in !== undefined);
|
|
122
167
|
const motion = voice.motion;
|
|
168
|
+
voice.parked = false;
|
|
123
169
|
if (owner !== null)
|
|
124
170
|
adopt(owner, voice);
|
|
125
171
|
if (!Number.isNaN(mix.now)) {
|
|
@@ -131,7 +177,8 @@ function enter(mix, voice) {
|
|
|
131
177
|
// A start the host gave by its own clock, where the mix's rate puts it.
|
|
132
178
|
if (!Number.isNaN(voice.pinned) && voice.state === 'pending')
|
|
133
179
|
pin(mix, voice, voice.pinned);
|
|
134
|
-
|
|
180
|
+
// A read ahead's copy leaves the patch asking the live mix for its time.
|
|
181
|
+
if (motion !== undefined && !mix.projecting) {
|
|
135
182
|
mix.playing.set(motion, (mix.playing.get(motion) ?? 0) + 1);
|
|
136
183
|
mix.owner ??= motionOwner(new WeakRef(mix));
|
|
137
184
|
motion.owner = mix.owner;
|
package/dist/departed.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Mixer } from './mixer.js';
|
|
2
|
-
import type {
|
|
2
|
+
import type { MarkName, Query } from './types.js';
|
|
3
3
|
import { type Voice } from './voice.js';
|
|
4
4
|
/**
|
|
5
5
|
* What a mix keeps of a voice it has forgotten, on leaving without history or past history's reach:
|
|
@@ -14,7 +14,8 @@ export interface Departed {
|
|
|
14
14
|
readonly name: string | undefined;
|
|
15
15
|
readonly tags: readonly string[];
|
|
16
16
|
readonly writes: readonly unknown[];
|
|
17
|
-
|
|
17
|
+
/** Its five, and those it placed inside itself. */
|
|
18
|
+
readonly marks: Readonly<Partial<Record<MarkName, number>>>;
|
|
18
19
|
/** How many of the index's ends hold it. */
|
|
19
20
|
held: number;
|
|
20
21
|
}
|
package/dist/departed.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { markOf } from './marks.js';
|
|
1
|
+
import { markOf, voiceMarks } from './marks.js';
|
|
2
2
|
import { none } from './voice.js';
|
|
3
|
-
const MARKS = ['start', 'in', 'coast', 'out', 'end'];
|
|
4
3
|
/**
|
|
5
4
|
* The departed voices a query could still pick: for each name, tag and channel within a score and
|
|
6
5
|
* owner, the first and the last to leave. It grows with those, never with how many voices left.
|
|
@@ -12,9 +11,13 @@ export class DepartedIndex {
|
|
|
12
11
|
// Nothing can be cued beside a voice whose owner has gone, so nothing will ask for it.
|
|
13
12
|
if (voice.owner?.state === 'done')
|
|
14
13
|
return;
|
|
15
|
-
const marks =
|
|
16
|
-
for (const mark of
|
|
14
|
+
const marks = Object.create(null);
|
|
15
|
+
for (const mark of voiceMarks)
|
|
17
16
|
marks[mark] = markOf(mix, voice, mark);
|
|
17
|
+
const inner = voice.spec.marks;
|
|
18
|
+
if (inner !== undefined)
|
|
19
|
+
for (const mark of Object.keys(inner))
|
|
20
|
+
marks[mark] = markOf(mix, voice, mark);
|
|
18
21
|
const d = {
|
|
19
22
|
id: voice.id,
|
|
20
23
|
score: voice.spec.score,
|
package/dist/due.d.ts
CHANGED
|
@@ -10,3 +10,7 @@ export interface Due<I, O> {
|
|
|
10
10
|
export declare function schedule<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>): void;
|
|
11
11
|
/** Takes every current entry due by `now` off the queue, in cue order. */
|
|
12
12
|
export declare function popDue<I, O>(mix: Mixer<I, O>, now: number): Voice<I, O>[];
|
|
13
|
+
/** Puts voices in id order in place, those of one id left in the order they had; returns `list`. */
|
|
14
|
+
export declare function byId<V extends {
|
|
15
|
+
readonly id: number;
|
|
16
|
+
}>(list: V[]): V[];
|
package/dist/due.js
CHANGED
|
@@ -97,7 +97,20 @@ export function popDue(mix, now) {
|
|
|
97
97
|
out.push(top.voice);
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
100
|
+
return byId(out);
|
|
101
|
+
}
|
|
102
|
+
/** Past this many voices the built-in sort, which makes working arrays however few it is given. */
|
|
103
|
+
const SHORT = 8;
|
|
104
|
+
/** Puts voices in id order in place, those of one id left in the order they had; returns `list`. */
|
|
105
|
+
export function byId(list) {
|
|
106
|
+
if (list.length > SHORT)
|
|
107
|
+
return list.sort((a, b) => a.id - b.id);
|
|
108
|
+
for (let i = 1; i < list.length; i++) {
|
|
109
|
+
const v = list[i];
|
|
110
|
+
let k = i;
|
|
111
|
+
for (; k > 0 && list[k - 1].id > v.id; k--)
|
|
112
|
+
list[k] = list[k - 1];
|
|
113
|
+
list[k] = v;
|
|
114
|
+
}
|
|
115
|
+
return list;
|
|
103
116
|
}
|
package/dist/fade.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Mixer } from './mixer.js';
|
|
2
|
-
import type {
|
|
2
|
+
import type { Left, Parted, Parting, Voice } from './voice.js';
|
|
3
3
|
/** Starts one subject's ramp out of a voice, or takes it out at once for a ramp of 0. */
|
|
4
4
|
export declare function fadeSubject<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>, subject: I, over?: number): void;
|
|
5
5
|
/** What a subject's own ramp out of a voice leaves of its weight this frame, 0..1. */
|
|
@@ -12,11 +12,18 @@ export declare function part<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>, subject
|
|
|
12
12
|
/** Brings a subject faded out of a voice back, to be met afresh on its next probe. */
|
|
13
13
|
export declare function unpart<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>, subject: I): void;
|
|
14
14
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* record of that or none.
|
|
15
|
+
* Keeps, under history, the record a subject leaves a voice with as it is let go of: for one faded
|
|
16
|
+
* out before the voice met it, a record saying it is not reached.
|
|
18
17
|
*/
|
|
19
|
-
export declare function
|
|
18
|
+
export declare function leaveVoice<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>, subject: I): void;
|
|
19
|
+
/**
|
|
20
|
+
* Puts back into a voice's fades, or a copy's, those of the subjects that left it after frame `seq`
|
|
21
|
+
* as they stood then: `leave` keeps them, since a drop or a subject brought back forgets them.
|
|
22
|
+
*/
|
|
23
|
+
export declare function fadesAt<I>(fades: {
|
|
24
|
+
parts: Map<I, Parting> | null;
|
|
25
|
+
parted: Map<I, Parted> | null;
|
|
26
|
+
}, left: readonly Left<I>[], seq: number): void;
|
|
20
27
|
export declare function beginFade<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>, opts: {
|
|
21
28
|
over?: number;
|
|
22
29
|
at?: 'rest';
|
package/dist/fade.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { changed } from './chain.js';
|
|
1
|
+
import { changed, unlink } from './chain.js';
|
|
2
2
|
import { envelope } from './clock.js';
|
|
3
3
|
import { schedule } from './due.js';
|
|
4
|
-
import { unshare } from './everyone.js';
|
|
4
|
+
import { unshare, unsight } from './everyone.js';
|
|
5
5
|
import { leave, noted, releasing } from './history.js';
|
|
6
|
-
import {
|
|
6
|
+
import { markOf } from './marks.js';
|
|
7
|
+
import { orphan, ownerReading } from './owner.js';
|
|
7
8
|
import { startOf } from './place.js';
|
|
8
9
|
import { scoreTouched } from './scored.js';
|
|
9
10
|
import { lingers, refit } from './spans.js';
|
|
@@ -38,11 +39,12 @@ export function parting(voice, subject, now) {
|
|
|
38
39
|
* its lane position and a motion patch's state for it, and reaches it no more.
|
|
39
40
|
*/
|
|
40
41
|
export function part(mix, voice, subject, at) {
|
|
42
|
+
const ramp = voice.parts?.get(subject);
|
|
41
43
|
voice.parts?.delete(subject);
|
|
42
44
|
if (voice.parts?.size === 0)
|
|
43
45
|
voice.parts = null;
|
|
44
46
|
voice.parted ??= new Map();
|
|
45
|
-
voice.parted.set(subject, { at, seq: mix.transport.seq });
|
|
47
|
+
voice.parted.set(subject, { at, seq: mix.transport.seq, ramp });
|
|
46
48
|
mix.parters.add(voice);
|
|
47
49
|
forgetIn(mix, voice, subject);
|
|
48
50
|
const motion = voice.motion;
|
|
@@ -55,31 +57,54 @@ export function unpart(mix, voice, subject) {
|
|
|
55
57
|
return;
|
|
56
58
|
// `forgetIn` keeps the subject's own record for a read back; without one, keep the voice's.
|
|
57
59
|
if (voice.subjects.get(subject) === undefined)
|
|
58
|
-
|
|
60
|
+
leaveVoice(mix, voice, subject);
|
|
61
|
+
// Left with its parting still on, which a seek back to before this puts back.
|
|
62
|
+
forgetIn(mix, voice, subject);
|
|
59
63
|
voice.parted.delete(subject);
|
|
60
64
|
if (voice.parted.size === 0)
|
|
61
65
|
voice.parted = null;
|
|
62
|
-
forgetIn(mix, voice, subject);
|
|
63
66
|
const slot = mix.chains.get(subject)?.slot ?? -1;
|
|
64
67
|
if (slot >= 0)
|
|
65
68
|
mix.lanes?.rejoin(slot);
|
|
66
69
|
}
|
|
67
70
|
/**
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
* record of that or none.
|
|
71
|
+
* Keeps, under history, the record a subject leaves a voice with as it is let go of: for one faded
|
|
72
|
+
* out before the voice met it, a record saying it is not reached.
|
|
71
73
|
*/
|
|
72
|
-
export function
|
|
74
|
+
export function leaveVoice(mix, voice, subject) {
|
|
73
75
|
const held = voice.subjects.get(subject);
|
|
74
76
|
if (held !== undefined || voice.parted?.has(subject) !== true)
|
|
75
|
-
|
|
76
|
-
|
|
77
|
+
leave(mix, voice, subject, held);
|
|
78
|
+
else
|
|
79
|
+
leave(mix, voice, subject, voice.unreached ?? mix.unreachedOf(voice, mix.now));
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Puts back into a voice's fades, or a copy's, those of the subjects that left it after frame `seq`
|
|
83
|
+
* as they stood then: `leave` keeps them, since a drop or a subject brought back forgets them.
|
|
84
|
+
*/
|
|
85
|
+
export function fadesAt(fades, left, seq) {
|
|
86
|
+
for (const e of left) {
|
|
87
|
+
const parted = e.parted;
|
|
88
|
+
if (parted !== null && parted.seq <= seq) {
|
|
89
|
+
fades.parted ??= new Map();
|
|
90
|
+
fades.parted.set(e.subject, parted);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const ramp = e.ramp ?? parted?.ramp;
|
|
94
|
+
if (ramp === undefined || ramp === null || ramp.seq > seq)
|
|
95
|
+
continue;
|
|
96
|
+
fades.parts ??= new Map();
|
|
97
|
+
fades.parts.set(e.subject, ramp);
|
|
98
|
+
}
|
|
77
99
|
}
|
|
78
100
|
/** Drops a voice's record of a subject and relinks the subject's chain without it. */
|
|
79
101
|
function forgetIn(mix, voice, subject) {
|
|
80
102
|
const head = mix.chains.get(subject);
|
|
81
|
-
if (head !== undefined && head.slot >= 0)
|
|
103
|
+
if (head !== undefined && head.slot >= 0) {
|
|
82
104
|
unreach(voice, head.slot, false);
|
|
105
|
+
// What the voice kept for the subject while it shared one record, which `weightOf` reads.
|
|
106
|
+
unsight(voice, head.slot);
|
|
107
|
+
}
|
|
83
108
|
const held = voice.subjects.get(subject);
|
|
84
109
|
if (held !== undefined) {
|
|
85
110
|
leave(mix, voice, subject, held);
|
|
@@ -93,8 +118,7 @@ function forgetIn(mix, voice, subject) {
|
|
|
93
118
|
}
|
|
94
119
|
if (head === undefined)
|
|
95
120
|
return;
|
|
96
|
-
head
|
|
97
|
-
mix.relinks++;
|
|
121
|
+
unlink(mix, head);
|
|
98
122
|
if (voice.laned && head.slot >= 0)
|
|
99
123
|
mix.lanes?.part(voice.id, head.slot);
|
|
100
124
|
mix.lanes?.refill();
|
|
@@ -153,12 +177,20 @@ export function beginRise(mix, voice, opts) {
|
|
|
153
177
|
const over = mix.reduced ? 0 : (opts.over ?? voice.fade.in ?? voice.fade.out ?? 0);
|
|
154
178
|
const left = out.rest || out.over === 0 ? 1 : 1 - (at - out.at) / out.over;
|
|
155
179
|
const from = left < 0 ? 0 : left > 1 ? 1 : left;
|
|
180
|
+
// A voice faded before its start waits for it again, as a seek back finds it.
|
|
181
|
+
const waiting = Number.isNaN(mix.now) ||
|
|
182
|
+
(voice.owner === null ? at : ownerReading(voice.owner, at)) < voice.start;
|
|
156
183
|
voice.out = null;
|
|
157
184
|
voice.outAt = Number.POSITIVE_INFINITY;
|
|
158
|
-
voice.state = 'live';
|
|
185
|
+
voice.state = waiting ? 'pending' : 'live';
|
|
159
186
|
voice.back = over > 0 && from < 1 ? { from, at, over } : null;
|
|
187
|
+
if (waiting)
|
|
188
|
+
changed(mix, voice);
|
|
160
189
|
noted(mix, voice);
|
|
161
|
-
|
|
190
|
+
if (waiting)
|
|
191
|
+
mix.lanes?.invalidate();
|
|
192
|
+
else
|
|
193
|
+
mix.lanes?.refill();
|
|
162
194
|
}
|
|
163
195
|
/**
|
|
164
196
|
* Fades a voice out from mix time `at`: ahead, it plays untouched until then and begins its fade
|
|
@@ -182,6 +214,11 @@ export function fadeAt(mix, voice, at, over) {
|
|
|
182
214
|
function asks(mix, motion, voice) {
|
|
183
215
|
return motion.owner === mix.owner && motion.ownerId === voice.id;
|
|
184
216
|
}
|
|
217
|
+
/** What a child leaving tells its owner of when the owner coasts. */
|
|
218
|
+
function leaving(mix, child) {
|
|
219
|
+
const h = child.owner.holding;
|
|
220
|
+
h.left(child.passed ? markOf(mix, child, 'coast') : undefined, child.doneSeq);
|
|
221
|
+
}
|
|
185
222
|
/** Removes a voice, recording that it left at `at`, default now. */
|
|
186
223
|
export function retire(mix, voice, at) {
|
|
187
224
|
if (voice.holding === null)
|
|
@@ -209,6 +246,7 @@ export function retire(mix, voice, at) {
|
|
|
209
246
|
retire(mix, child, voice.doneAt);
|
|
210
247
|
if (voice.owner !== null) {
|
|
211
248
|
const span = voice.owner;
|
|
249
|
+
leaving(mix, voice);
|
|
212
250
|
const owner = orphan(voice);
|
|
213
251
|
if (owner !== null && !lingers(owner, voice.doneAt))
|
|
214
252
|
retire(mix, owner, voice.doneAt);
|
package/dist/fold.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Mixer } from './mixer.js';
|
|
2
2
|
import type { Channel } from './types.js';
|
|
3
|
-
import type
|
|
3
|
+
import { type Subject, type Voice } from './voice.js';
|
|
4
4
|
export type Key<O> = keyof O & string;
|
|
5
5
|
export declare function near(a: unknown, b: unknown): boolean;
|
|
6
6
|
/**
|
package/dist/fold.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { passAt, phaseAt, phaseBefore, silent } from './clock.js';
|
|
1
|
+
import { passAt, phaseAt, phaseBefore, silent, stepOf } from './clock.js';
|
|
2
2
|
import { copy as copyValue, first } from './clone.js';
|
|
3
3
|
import { open as openRecord, shut as shutRecord, weighAt } from './everyone.js';
|
|
4
|
-
import { recordHost, remember } from './history.js';
|
|
4
|
+
import { apart, copyState, recordHost, remember } from './history.js';
|
|
5
5
|
import { stateful } from './hosts.js';
|
|
6
6
|
import { locusScratch } from './locus.js';
|
|
7
7
|
import { moved } from './moved.js';
|
|
8
8
|
import { readKeys } from './patch.js';
|
|
9
9
|
import { reading } from './reading.js';
|
|
10
|
+
import { checkWrites } from './voice.js';
|
|
10
11
|
// V8 reads a local const as a constant, where it reads an import from its module on every call.
|
|
11
12
|
const copy = copyValue;
|
|
12
13
|
const open = openRecord;
|
|
@@ -74,6 +75,8 @@ export function contribution(voice, subject, now, held) {
|
|
|
74
75
|
if (early && !voice.holdsFirst)
|
|
75
76
|
return null;
|
|
76
77
|
const elapsed = voice.shownAt(raw);
|
|
78
|
+
if (Number.isNaN(elapsed))
|
|
79
|
+
return null;
|
|
77
80
|
// A frozen subject's clock stands still at the edge it freezes at: -1 before, 1 after, 0 playing.
|
|
78
81
|
const still = early ? -1 : voice.freezesAfter && raw > voice.span ? 1 : 0;
|
|
79
82
|
const duration = voice.duration;
|
|
@@ -100,7 +103,6 @@ export function contribution(voice, subject, now, held) {
|
|
|
100
103
|
if (history?.inputs && voice.patch.reads !== undefined && !this.projecting)
|
|
101
104
|
recordHost(this, now);
|
|
102
105
|
reading.horizon = this.horizonFor(voice, held.delay, now);
|
|
103
|
-
const tick = this.opts.stepMs;
|
|
104
106
|
if (voice.patch.step && held.probed !== now) {
|
|
105
107
|
// Frozen after, it steps once more to where its last pass ended, and no further; run back
|
|
106
108
|
// past its start, to where it reached it. Before a start it has yet to reach, it does not step.
|
|
@@ -114,8 +116,13 @@ export function contribution(voice, subject, now, held) {
|
|
|
114
116
|
held.stepped = held.since;
|
|
115
117
|
setting.dt = this.capped(now - held.since);
|
|
116
118
|
}
|
|
117
|
-
|
|
119
|
+
const tick = stepOf(this, voice);
|
|
120
|
+
if (tick !== undefined) {
|
|
121
|
+
// Never probed, it may be a record a seek put back.
|
|
122
|
+
if (Number.isNaN(held.probed) && held.snaps !== undefined)
|
|
123
|
+
ahead(this, voice, subject, held, held.snaps, tick, to);
|
|
118
124
|
this.tick(voice, subject, held, tick, to);
|
|
125
|
+
}
|
|
119
126
|
else if (still === 0 ? now !== held.stepped : to > held.stepped) {
|
|
120
127
|
if (still !== 0)
|
|
121
128
|
setting.dt = this.capped(to - held.stepped);
|
|
@@ -143,6 +150,8 @@ export function contribution(voice, subject, now, held) {
|
|
|
143
150
|
}
|
|
144
151
|
else {
|
|
145
152
|
delta = voice.patch.at(phase, subject, setting);
|
|
153
|
+
if (voice.unchecked)
|
|
154
|
+
checkWrites(voice, delta);
|
|
146
155
|
}
|
|
147
156
|
held.delta = delta;
|
|
148
157
|
held.probed = now;
|
|
@@ -178,6 +187,45 @@ export function keyed(voice, subject, held) {
|
|
|
178
187
|
voice.holder = held;
|
|
179
188
|
return delta;
|
|
180
189
|
}
|
|
190
|
+
/** How many whole intervals of `tick` a subject's step grid holds by mix time `to`. */
|
|
191
|
+
function dueBy(held, tick, to) {
|
|
192
|
+
// The epsilon keeps an interval like 1000 / 120 from landing a hair short of a whole count.
|
|
193
|
+
return Math.floor((to - held.since) / tick + 1e-9);
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* The first step of a record a seek put back from a copy: takes up the state that copy was last
|
|
197
|
+
* stepped ahead to, where the steps due now run past it, and keeps the copy's stepped ahead to the
|
|
198
|
+
* moment sought, so a mix sought back time after time does not step from the copy each time. Only
|
|
199
|
+
* where the same steps give the same state: a fixed interval, no `maxDt`, no host field read.
|
|
200
|
+
*/
|
|
201
|
+
function ahead(mix, voice, subject, held, snaps, tick, to) {
|
|
202
|
+
const from = snaps[snaps.length - 1];
|
|
203
|
+
const got = from?.ahead;
|
|
204
|
+
if (from === undefined || got === undefined || got.put !== held)
|
|
205
|
+
return;
|
|
206
|
+
got.put = null;
|
|
207
|
+
const reads = voice.patch.reads;
|
|
208
|
+
if (mix.opts.maxDt !== undefined || (reads !== undefined && reads.length > 0))
|
|
209
|
+
return;
|
|
210
|
+
// Stepped, rebuilt or placed anew since it was put back, its steps are no longer the copy's.
|
|
211
|
+
const was = from.held;
|
|
212
|
+
if (held.ticks !== was.ticks || held.since !== got.since || held.rebuilt !== was.rebuilt)
|
|
213
|
+
return;
|
|
214
|
+
const at = got.sought;
|
|
215
|
+
const usable = got.state !== undefined && got.ticks <= dueBy(held, tick, to);
|
|
216
|
+
if (usable) {
|
|
217
|
+
held.state = copyState(voice, got.state);
|
|
218
|
+
voice.setting.state = held.state;
|
|
219
|
+
held.ticks = got.ticks;
|
|
220
|
+
}
|
|
221
|
+
const last = usable ? got.at : from.at;
|
|
222
|
+
if (!(at < to) || at - last < apart(mix.opts.history))
|
|
223
|
+
return;
|
|
224
|
+
mix.tick(voice, subject, held, tick, at);
|
|
225
|
+
got.state = copyState(voice, held.state);
|
|
226
|
+
got.ticks = held.ticks;
|
|
227
|
+
got.at = at;
|
|
228
|
+
}
|
|
181
229
|
/**
|
|
182
230
|
* Runs `step` once per whole interval between where this subject last stopped and now, on a grid
|
|
183
231
|
* counted from when its delay ran out, and leaves the remainder for the next sample. Counting
|
|
@@ -188,8 +236,7 @@ export function tick(voice, subject, held, tick, to) {
|
|
|
188
236
|
const setting = voice.setting;
|
|
189
237
|
const frameDt = setting.dt;
|
|
190
238
|
let n = held.ticks;
|
|
191
|
-
|
|
192
|
-
const due = Math.floor((to - held.since) / tick + 1e-9);
|
|
239
|
+
const due = dueBy(held, tick, to);
|
|
193
240
|
const cap = this.opts.maxDt;
|
|
194
241
|
if (cap !== undefined) {
|
|
195
242
|
const most = Math.floor(cap / tick);
|
package/dist/frames.d.ts
CHANGED
|
@@ -15,6 +15,8 @@ export declare class Frames {
|
|
|
15
15
|
* since rate 0 can hold the clock at `t` for many.
|
|
16
16
|
*/
|
|
17
17
|
at(t: number): Frame | undefined;
|
|
18
|
+
/** The frame numbered `seq`, if it is still held. */
|
|
19
|
+
numbered(seq: number): Frame | undefined;
|
|
18
20
|
/** Every frame held, oldest first. */
|
|
19
21
|
all(): Frame[];
|
|
20
22
|
set(frames: Frame[]): void;
|
package/dist/frames.js
CHANGED
|
@@ -48,6 +48,23 @@ export class Frames {
|
|
|
48
48
|
}
|
|
49
49
|
return found;
|
|
50
50
|
}
|
|
51
|
+
/** The frame numbered `seq`, if it is still held. */
|
|
52
|
+
numbered(seq) {
|
|
53
|
+
const list = this.list;
|
|
54
|
+
let lo = this.head;
|
|
55
|
+
let hi = list.length - 1;
|
|
56
|
+
while (lo <= hi) {
|
|
57
|
+
const mid = (lo + hi) >> 1;
|
|
58
|
+
const f = list[mid];
|
|
59
|
+
if (f.seq === seq)
|
|
60
|
+
return f;
|
|
61
|
+
if (f.seq < seq)
|
|
62
|
+
lo = mid + 1;
|
|
63
|
+
else
|
|
64
|
+
hi = mid - 1;
|
|
65
|
+
}
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
51
68
|
/** Every frame held, oldest first. */
|
|
52
69
|
all() {
|
|
53
70
|
return this.list.slice(this.head);
|
package/dist/handle.d.ts
CHANGED
|
@@ -17,8 +17,11 @@ export interface HandleHost<I, O> {
|
|
|
17
17
|
over?: number;
|
|
18
18
|
} | undefined): void;
|
|
19
19
|
weightOf(voice: Voice<I, O>, subject: I): number;
|
|
20
|
-
/**
|
|
21
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Records a write the host made through `h`, which `again` makes once more where a seek replays
|
|
22
|
+
* it, on the handle it is given: `h`, or a read ahead's handle on its copy of the voice.
|
|
23
|
+
*/
|
|
24
|
+
record(h: VoiceHandle<I, O>, label: string, again: (h: VoiceHandle<I, O>) => void): void;
|
|
22
25
|
}
|
|
23
26
|
/** Refuses a voice rate no clock can run at. */
|
|
24
27
|
export declare function finiteRate(rate: number): void;
|
package/dist/handle.js
CHANGED
|
@@ -59,17 +59,25 @@ export class VoiceHandle {
|
|
|
59
59
|
get weight() {
|
|
60
60
|
return this.#voice?.weight ?? this.#gone?.weight ?? 0;
|
|
61
61
|
}
|
|
62
|
-
|
|
62
|
+
/**
|
|
63
|
+
* The voice a write reaches: none while a history store holds it, or a seek back to before its
|
|
64
|
+
* cue has it waiting. It had left, or was yet to be cued, when the host made the write, as on
|
|
65
|
+
* replay, so the write changes nothing and the tape does not take it.
|
|
66
|
+
*/
|
|
67
|
+
get #cued() {
|
|
68
|
+
const voice = this.#voice;
|
|
69
|
+
return voice === null || voice.parked ? null : voice;
|
|
70
|
+
}
|
|
63
71
|
set weight(w) {
|
|
64
72
|
plainWeight(w);
|
|
65
|
-
const voice = this.#
|
|
73
|
+
const voice = this.#cued;
|
|
66
74
|
if (voice === null)
|
|
67
75
|
return;
|
|
68
76
|
voice.written = true;
|
|
69
77
|
voice.weight = w;
|
|
70
78
|
this.#host.changed(voice);
|
|
71
|
-
this.#host.record('weight', () => {
|
|
72
|
-
|
|
79
|
+
this.#host.record(this, 'weight', (h) => {
|
|
80
|
+
h.weight = w;
|
|
73
81
|
});
|
|
74
82
|
}
|
|
75
83
|
get rate() {
|
|
@@ -81,7 +89,7 @@ export class VoiceHandle {
|
|
|
81
89
|
}
|
|
82
90
|
ramp(r, over) {
|
|
83
91
|
finiteRate(r);
|
|
84
|
-
const voice = this.#
|
|
92
|
+
const voice = this.#cued;
|
|
85
93
|
if (voice === null)
|
|
86
94
|
return;
|
|
87
95
|
voice.written = true;
|
|
@@ -97,10 +105,10 @@ export class VoiceHandle {
|
|
|
97
105
|
retime(voice, now, r, over);
|
|
98
106
|
}
|
|
99
107
|
this.#host.changed(voice);
|
|
100
|
-
this.#host.record('rate', () =>
|
|
108
|
+
this.#host.record(this, 'rate', (h) => h.ramp(r, over));
|
|
101
109
|
}
|
|
102
110
|
seek(elapsed, opts) {
|
|
103
|
-
const voice = this.#
|
|
111
|
+
const voice = this.#cued;
|
|
104
112
|
if (voice === null)
|
|
105
113
|
return 'exact';
|
|
106
114
|
voice.written = true;
|
|
@@ -114,24 +122,24 @@ export class VoiceHandle {
|
|
|
114
122
|
voice.seeks++;
|
|
115
123
|
const doubt = this.#host.sought(voice, opts?.state !== 'keep');
|
|
116
124
|
this.#host.changed(voice);
|
|
117
|
-
this.#host.record('seek', () =>
|
|
125
|
+
this.#host.record(this, 'seek', (h) => h.seek(elapsed, opts));
|
|
118
126
|
return doubt;
|
|
119
127
|
}
|
|
120
128
|
fade(opts) {
|
|
121
|
-
const voice = this.#
|
|
129
|
+
const voice = this.#cued;
|
|
122
130
|
if (voice === null)
|
|
123
131
|
return;
|
|
124
132
|
voice.written = true;
|
|
125
133
|
this.#host.fade(voice, opts);
|
|
126
|
-
this.#host.record('fade', () =>
|
|
134
|
+
this.#host.record(this, 'fade', (h) => h.fade(opts));
|
|
127
135
|
}
|
|
128
136
|
rise(opts) {
|
|
129
|
-
const voice = this.#
|
|
137
|
+
const voice = this.#cued;
|
|
130
138
|
if (voice === null)
|
|
131
139
|
return;
|
|
132
140
|
voice.written = true;
|
|
133
141
|
this.#host.rise(voice, opts);
|
|
134
|
-
this.#host.record('rise', () =>
|
|
142
|
+
this.#host.record(this, 'rise', (h) => h.rise(opts));
|
|
135
143
|
}
|
|
136
144
|
weightOf(subject) {
|
|
137
145
|
const voice = this.#voice;
|
package/dist/held.d.ts
CHANGED
|
@@ -4,6 +4,12 @@ import type { Subject, Voice } from './voice.js';
|
|
|
4
4
|
export declare function unreachedOf<I, O>(this: Mixer<I, O>, voice: Voice<I, O>, now: number): Subject<unknown>;
|
|
5
5
|
/** What this voice holds for this subject, made on first sight with `target` and `stagger` asked once. */
|
|
6
6
|
export declare function held<I, O>(this: Mixer<I, O>, voice: Voice<I, O>, subject: I, slot: number): Subject<unknown>;
|
|
7
|
+
/**
|
|
8
|
+
* Where a new record met at `now` starts stepping: from its `since`, or from `now` for a voice that
|
|
9
|
+
* catches up `fresh`, on the `stepMs` grid. True where that skipped steps, so the state depends on
|
|
10
|
+
* when the subject was first probed.
|
|
11
|
+
*/
|
|
12
|
+
export declare function meet<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>, held: Subject<unknown>, now: number): boolean;
|
|
7
13
|
/**
|
|
8
14
|
* The mix time the voice clock first read `delay`, or will by its controls now. A stateful voice
|
|
9
15
|
* counts from its last seek that rebuilt state, as its records do.
|
|
@@ -20,6 +26,8 @@ export declare function shownOf<I, O>(this: Mixer<I, O>, voice: Voice<I, O>, sin
|
|
|
20
26
|
* while it waited is made again for where its clock is.
|
|
21
27
|
*/
|
|
22
28
|
export declare function begin<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>, now: number): void;
|
|
29
|
+
/** Counts a record whose `since` was still ahead from where its voice's clock now puts it. */
|
|
30
|
+
export declare function origin<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>, held: Subject<unknown>): void;
|
|
23
31
|
/**
|
|
24
32
|
* A voice's start or clock moved, or it went live at `now`: the records whose `since` was still
|
|
25
33
|
* ahead count from where its clock now puts them.
|