@msb235/blits 0.7.1 → 0.8.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.
Files changed (156) hide show
  1. package/CHANGELOG.md +193 -0
  2. package/README.md +22 -4
  3. package/dist/book.js +4 -4
  4. package/dist/chain.d.ts +1 -1
  5. package/dist/chain.js +57 -54
  6. package/dist/channels.d.ts +26 -5
  7. package/dist/channels.js +12 -8
  8. package/dist/clock.d.ts +8 -1
  9. package/dist/clock.js +14 -3
  10. package/dist/clone.d.ts +8 -0
  11. package/dist/clone.js +22 -0
  12. package/dist/closed.d.ts +1 -1
  13. package/dist/closed.js +3 -2
  14. package/dist/color.d.ts +0 -11
  15. package/dist/color.js +4 -24
  16. package/dist/columns.js +0 -1
  17. package/dist/copyout.d.ts +6 -0
  18. package/dist/copyout.js +38 -0
  19. package/dist/crowd.d.ts +7 -5
  20. package/dist/crowd.js +32 -18
  21. package/dist/cue.d.ts +3 -1
  22. package/dist/cue.js +31 -20
  23. package/dist/departed.d.ts +34 -0
  24. package/dist/departed.js +73 -0
  25. package/dist/easing.d.ts +5 -0
  26. package/dist/easing.js +31 -2
  27. package/dist/everyone.d.ts +56 -0
  28. package/dist/everyone.js +221 -0
  29. package/dist/fade.d.ts +8 -1
  30. package/dist/fade.js +34 -6
  31. package/dist/fill.d.ts +22 -13
  32. package/dist/fill.js +104 -41
  33. package/dist/fit.js +2 -1
  34. package/dist/fold.d.ts +37 -0
  35. package/dist/fold.js +216 -31
  36. package/dist/frames.d.ts +21 -0
  37. package/dist/frames.js +59 -0
  38. package/dist/gather.d.ts +15 -9
  39. package/dist/gather.js +60 -25
  40. package/dist/gone.js +10 -0
  41. package/dist/handle.d.ts +16 -5
  42. package/dist/handle.js +56 -6
  43. package/dist/held.d.ts +8 -5
  44. package/dist/held.js +56 -58
  45. package/dist/history.d.ts +17 -13
  46. package/dist/history.js +47 -28
  47. package/dist/hosts.js +55 -8
  48. package/dist/index.d.ts +9 -4
  49. package/dist/index.js +5 -2
  50. package/dist/keyfill.d.ts +7 -0
  51. package/dist/keyfill.js +124 -0
  52. package/dist/keyrows.d.ts +1 -0
  53. package/dist/keyrows.js +1 -1
  54. package/dist/lane.d.ts +32 -2
  55. package/dist/lane.js +5 -0
  56. package/dist/lanes.d.ts +32 -8
  57. package/dist/lanes.js +69 -37
  58. package/dist/leavings.d.ts +26 -0
  59. package/dist/leavings.js +70 -0
  60. package/dist/locus.js +2 -2
  61. package/dist/meet.js +14 -8
  62. package/dist/mixer.d.ts +39 -7
  63. package/dist/mixer.js +50 -10
  64. package/dist/motion.d.ts +2 -0
  65. package/dist/motion.js +4 -1
  66. package/dist/motions.d.ts +22 -15
  67. package/dist/motions.js +40 -34
  68. package/dist/move.js +23 -16
  69. package/dist/moved.d.ts +11 -0
  70. package/dist/moved.js +29 -0
  71. package/dist/origin.d.ts +19 -0
  72. package/dist/origin.js +68 -0
  73. package/dist/owed.d.ts +4 -4
  74. package/dist/owed.js +8 -8
  75. package/dist/owner.d.ts +1 -1
  76. package/dist/owner.js +2 -2
  77. package/dist/pace.d.ts +13 -5
  78. package/dist/pace.js +21 -8
  79. package/dist/pack.d.ts +3 -1
  80. package/dist/pack.js +15 -24
  81. package/dist/paging.d.ts +8 -2
  82. package/dist/paging.js +39 -4
  83. package/dist/patch.d.ts +14 -9
  84. package/dist/patch.js +83 -21
  85. package/dist/place.js +18 -3
  86. package/dist/project.d.ts +12 -1
  87. package/dist/project.js +101 -83
  88. package/dist/pull.d.ts +2 -0
  89. package/dist/pull.js +8 -0
  90. package/dist/qualify.js +10 -2
  91. package/dist/reading.d.ts +5 -0
  92. package/dist/reading.js +5 -0
  93. package/dist/record.d.ts +9 -0
  94. package/dist/record.js +42 -0
  95. package/dist/relink.d.ts +11 -5
  96. package/dist/relink.js +7 -5
  97. package/dist/restored.d.ts +41 -0
  98. package/dist/restored.js +91 -0
  99. package/dist/revive.d.ts +10 -3
  100. package/dist/revive.js +31 -23
  101. package/dist/roster.d.ts +11 -0
  102. package/dist/roster.js +46 -0
  103. package/dist/rotation.d.ts +29 -0
  104. package/dist/rotation.js +91 -0
  105. package/dist/rows.d.ts +5 -5
  106. package/dist/rows.js +5 -5
  107. package/dist/sample.d.ts +3 -2
  108. package/dist/sample.js +32 -15
  109. package/dist/seek.js +97 -114
  110. package/dist/shared.d.ts +6 -0
  111. package/dist/shared.js +15 -0
  112. package/dist/signals.d.ts +24 -3
  113. package/dist/signals.js +50 -11
  114. package/dist/spans.js +7 -0
  115. package/dist/stack.d.ts +11 -0
  116. package/dist/stack.js +47 -0
  117. package/dist/standing.d.ts +10 -0
  118. package/dist/standing.js +79 -0
  119. package/dist/store.d.ts +3 -3
  120. package/dist/store.js +3 -3
  121. package/dist/strays.js +3 -0
  122. package/dist/tape.d.ts +8 -5
  123. package/dist/tape.js +21 -13
  124. package/dist/testing.d.ts +35 -0
  125. package/dist/testing.js +116 -0
  126. package/dist/touch.d.ts +6 -0
  127. package/dist/touch.js +28 -0
  128. package/dist/transport.d.ts +45 -6
  129. package/dist/transport.js +64 -19
  130. package/dist/turns.d.ts +18 -0
  131. package/dist/turns.js +72 -0
  132. package/dist/tweened.js +1 -1
  133. package/dist/types/channel.d.ts +46 -0
  134. package/dist/types/channel.js +1 -0
  135. package/dist/types/history.d.ts +139 -0
  136. package/dist/types/history.js +1 -0
  137. package/dist/types/mix.d.ts +336 -0
  138. package/dist/types/mix.js +1 -0
  139. package/dist/types/patch.d.ts +187 -0
  140. package/dist/types/patch.js +1 -0
  141. package/dist/types/place.d.ts +157 -0
  142. package/dist/types/place.js +1 -0
  143. package/dist/types/transport.d.ts +71 -0
  144. package/dist/types/transport.js +1 -0
  145. package/dist/types/voice.d.ts +323 -0
  146. package/dist/types/voice.js +1 -0
  147. package/dist/types.d.ts +7 -1162
  148. package/dist/unpage.d.ts +2 -2
  149. package/dist/unpage.js +53 -25
  150. package/dist/voice.d.ts +90 -22
  151. package/dist/voice.js +105 -43
  152. package/dist/wave.d.ts +42 -0
  153. package/dist/wave.js +62 -0
  154. package/dist/weigh.js +1 -1
  155. package/docs/schema.html +42 -36
  156. package/package.json +8 -3
@@ -0,0 +1,56 @@
1
+ import type { Mixer } from './mixer.js';
2
+ import type { Subject, Voice } from './voice.js';
3
+ /**
4
+ * Whether a voice can keep one record for every subject, decided when it is cued. A record holds
5
+ * what a voice knows of one subject; where the voice is over every subject with no stagger, keeps
6
+ * no state, weighs every subject alike and writes only channels with a rest, a subject's record
7
+ * differs from another's only in the weight last given it and in what its patch was last called
8
+ * for it. The voice keeps those by subject number (`weights`, and for a patch that is called,
9
+ * `stamps` and `deltas`) and one record, `everyone`, that `open` points at a subject before a read
10
+ * and `shut` takes back after. About 32 B a subject where a record and its place in the voice's
11
+ * store took about 370 (bench/allocs.mjs on the `swap` row, 2026-10-09). It needs lanes for the
12
+ * numbers. What only shows later (a subject faded out of it alone, a fade at rest, state kept
13
+ * through `setting.keep`) turns it back with `unshare`.
14
+ */
15
+ export declare function shares<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>): boolean;
16
+ /**
17
+ * A sharing voice's one record, made when a chain first asks; null where its origin is still
18
+ * ahead, which a record of its own per subject follows as the clock moves, so it shares no more.
19
+ */
20
+ export declare function everyoneOf<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>): Subject<unknown> | null;
21
+ /** Whether a chain has asked a sharing voice about the subject numbered `slot`. */
22
+ export declare function sighted<I, O>(voice: Voice<I, O>, slot: number): boolean;
23
+ /** A chain asks a sharing voice about a subject: counted once, as a record of its own would be. */
24
+ export declare function sight<I, O>(voice: Voice<I, O>, slot: number, cap: number): void;
25
+ /** The weight a sharing voice last gave the subject numbered `slot`, 0 where it gave none. */
26
+ export declare function weightAt<I, O>(voice: Voice<I, O>, slot: number): number;
27
+ /** Keeps the weight a sharing voice gave the subject numbered `slot`. */
28
+ export declare function weighAt<I, O>(voice: Voice<I, O>, slot: number, w: number, cap: number): void;
29
+ /**
30
+ * Points a sharing voice's record at the subject numbered `slot` before a read: it holds when the
31
+ * voice's patch was last called for that subject and the delta it gave, as a record of the
32
+ * subject's own would, so the patch is called once a frame for a subject however its probes fall
33
+ * among other subjects'. A keys voice reads no subject, so its record needs no pointing.
34
+ */
35
+ export declare function open<I, O>(voice: Voice<I, O>, rec: Subject<unknown>, slot: number): void;
36
+ /** Takes back what a read left on a sharing voice's record for the subject numbered `slot`. */
37
+ export declare function shut<I, O>(voice: Voice<I, O>, rec: Subject<unknown>, slot: number, cap: number): void;
38
+ /** Makes stale what a sharing voice last read for the subject numbered `slot`, as `touch` asks. */
39
+ export declare function restamp<I, O>(voice: Voice<I, O>, slot: number): void;
40
+ /** A subject's number was let go of: the next subject to take it is one the voice has not seen. */
41
+ export declare function unsight<I, O>(voice: Voice<I, O>, slot: number): void;
42
+ /**
43
+ * Turns a sharing voice back to a record per subject, each made as it is next asked for from what
44
+ * the voice kept for the subject. With `kept`, a call just kept state on its one record, which is
45
+ * open on `keeper`, the subject called for: that subject takes the record as its own. A lane's
46
+ * positions take their own records now, since each held the one.
47
+ */
48
+ export declare function unshare<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>, kept?: boolean, keeper?: I): void;
49
+ /**
50
+ * Counts what makes a mix's fold more than a walk of each chain: a locus, a voice sharing one
51
+ * record, a voice that stopped sharing this frame. Called wherever one of the three changes, so
52
+ * `foldWith` asks one number.
53
+ */
54
+ export declare function detour<I, O>(mix: Mixer<I, O>): void;
55
+ /** Gives a record just made for a subject a sharing voice had seen what the voice kept for it. */
56
+ export declare function inherit<I, O>(voice: Voice<I, O>, held: Subject<unknown>, slot: number): void;
@@ -0,0 +1,221 @@
1
+ import { numericOf } from './channels.js';
2
+ import { record } from './record.js';
3
+ /**
4
+ * Whether a voice can keep one record for every subject, decided when it is cued. A record holds
5
+ * what a voice knows of one subject; where the voice is over every subject with no stagger, keeps
6
+ * no state, weighs every subject alike and writes only channels with a rest, a subject's record
7
+ * differs from another's only in the weight last given it and in what its patch was last called
8
+ * for it. The voice keeps those by subject number (`weights`, and for a patch that is called,
9
+ * `stamps` and `deltas`) and one record, `everyone`, that `open` points at a subject before a read
10
+ * and `shut` takes back after. About 32 B a subject where a record and its place in the voice's
11
+ * store took about 370 (bench/allocs.mjs on the `swap` row, 2026-10-09). It needs lanes for the
12
+ * numbers. What only shows later (a subject faded out of it alone, a fade at rest, state kept
13
+ * through `setting.keep`) turns it back with `unshare`.
14
+ */
15
+ export function shares(mix, voice) {
16
+ const spec = voice.spec;
17
+ const patch = voice.patch;
18
+ return (mix.lanes !== null &&
19
+ !mix.projecting &&
20
+ mix.opts.history === undefined &&
21
+ voice.named === null &&
22
+ spec.target === undefined &&
23
+ spec.stagger === undefined &&
24
+ spec.locus === undefined &&
25
+ spec.anchor === undefined &&
26
+ spec.from !== 'current' &&
27
+ typeof spec.weight !== 'function' &&
28
+ patch.state === undefined &&
29
+ patch.step === undefined &&
30
+ voice.motion === undefined &&
31
+ voice.owner === null &&
32
+ voice.holding === null &&
33
+ voice.blend === null &&
34
+ !voice.freezesBefore &&
35
+ voice.slots.every((s) => {
36
+ const channel = mix.channels[s];
37
+ return channel.rest !== undefined && numericOf(channel)?.op !== 'last';
38
+ }));
39
+ }
40
+ /**
41
+ * A sharing voice's one record, made when a chain first asks; null where its origin is still
42
+ * ahead, which a record of its own per subject follows as the clock moves, so it shares no more.
43
+ */
44
+ export function everyoneOf(mix, voice) {
45
+ if (voice.everyone !== null)
46
+ return voice.everyone;
47
+ const since = mix.sinceOf(voice, 0);
48
+ if (!(since <= mix.now)) {
49
+ unshare(mix, voice);
50
+ return null;
51
+ }
52
+ const all = record(voice, true, undefined);
53
+ all.since = since;
54
+ all.shown = mix.shownOf(voice, since);
55
+ all.stepped = since;
56
+ all.rebuilt = voice.rebuilds;
57
+ voice.everyone = all;
58
+ return all;
59
+ }
60
+ /** Whether a chain has asked a sharing voice about the subject numbered `slot`. */
61
+ export function sighted(voice, slot) {
62
+ const bits = voice.sighted;
63
+ const word = slot >>> 5;
64
+ return (bits !== null &&
65
+ slot >= 0 &&
66
+ word < bits.length &&
67
+ (bits[word] & (1 << (slot & 31))) !== 0);
68
+ }
69
+ /** A chain asks a sharing voice about a subject: counted once, as a record of its own would be. */
70
+ export function sight(voice, slot, cap) {
71
+ let bits = voice.sighted;
72
+ const word = slot >>> 5;
73
+ if (bits === null || word >= bits.length) {
74
+ const grown = new Uint32Array(Math.max(word + 1, (cap >>> 5) + 1));
75
+ if (bits !== null)
76
+ grown.set(bits);
77
+ voice.sighted = grown;
78
+ bits = grown;
79
+ }
80
+ const bit = 1 << (slot & 31);
81
+ if ((bits[word] & bit) !== 0)
82
+ return;
83
+ bits[word] = bits[word] | bit;
84
+ voice.seen++;
85
+ }
86
+ /** The weight a sharing voice last gave the subject numbered `slot`, 0 where it gave none. */
87
+ export function weightAt(voice, slot) {
88
+ const weights = voice.weights;
89
+ return weights !== null && slot >= 0 && slot < weights.length ? weights[slot] : 0;
90
+ }
91
+ /** Keeps the weight a sharing voice gave the subject numbered `slot`. */
92
+ export function weighAt(voice, slot, w, cap) {
93
+ let weights = voice.weights;
94
+ if (weights === null || slot >= weights.length) {
95
+ if (w === 0)
96
+ return;
97
+ const grown = new Float64Array(Math.max(slot + 1, cap));
98
+ if (weights !== null)
99
+ grown.set(weights);
100
+ voice.weights = grown;
101
+ weights = grown;
102
+ }
103
+ weights[slot] = w;
104
+ }
105
+ /**
106
+ * Points a sharing voice's record at the subject numbered `slot` before a read: it holds when the
107
+ * voice's patch was last called for that subject and the delta it gave, as a record of the
108
+ * subject's own would, so the patch is called once a frame for a subject however its probes fall
109
+ * among other subjects'. A keys voice reads no subject, so its record needs no pointing.
110
+ */
111
+ export function open(voice, rec, slot) {
112
+ if (voice.built !== null)
113
+ return;
114
+ const stamps = voice.stamps;
115
+ const o = slot * 2 /* Stamp.SIZE */;
116
+ if (stamps === null || o >= stamps.length) {
117
+ rec.probed = Number.NaN;
118
+ rec.seeks = 0;
119
+ rec.delta = null;
120
+ return;
121
+ }
122
+ const at = stamps[o + 0 /* Stamp.PROBED */];
123
+ rec.probed = at;
124
+ rec.seeks = stamps[o + 1 /* Stamp.SEEKS */];
125
+ // A subject never read holds no delta, and its place in `deltas` may never have been written.
126
+ rec.delta = Number.isNaN(at)
127
+ ? null
128
+ : (voice.deltas[slot] ?? null);
129
+ }
130
+ /** Takes back what a read left on a sharing voice's record for the subject numbered `slot`. */
131
+ export function shut(voice, rec, slot, cap) {
132
+ if (voice.built !== null)
133
+ return;
134
+ let stamps = voice.stamps;
135
+ const o = slot * 2 /* Stamp.SIZE */;
136
+ if (stamps === null || o >= stamps.length) {
137
+ if (Number.isNaN(rec.probed))
138
+ return;
139
+ const grown = new Float64Array(Math.max(slot + 1, cap) * 2 /* Stamp.SIZE */).fill(Number.NaN);
140
+ if (stamps !== null)
141
+ grown.set(stamps);
142
+ voice.stamps = grown;
143
+ stamps = grown;
144
+ voice.deltas ??= [];
145
+ }
146
+ stamps[o + 0 /* Stamp.PROBED */] = rec.probed;
147
+ stamps[o + 1 /* Stamp.SEEKS */] = rec.seeks;
148
+ voice.deltas[slot] = rec.delta;
149
+ }
150
+ /** Makes stale what a sharing voice last read for the subject numbered `slot`, as `touch` asks. */
151
+ export function restamp(voice, slot) {
152
+ const stamps = voice.stamps;
153
+ const o = slot * 2 /* Stamp.SIZE */;
154
+ if (stamps !== null && slot >= 0 && o < stamps.length)
155
+ stamps[o + 1 /* Stamp.SEEKS */] = -1;
156
+ // A keys voice's one record holds its one delta.
157
+ if (voice.built !== null && voice.everyone !== null)
158
+ voice.everyone.seeks = -1;
159
+ }
160
+ /** A subject's number was let go of: the next subject to take it is one the voice has not seen. */
161
+ export function unsight(voice, slot) {
162
+ const bits = voice.sighted;
163
+ const word = slot >>> 5;
164
+ if (bits !== null && word < bits.length)
165
+ bits[word] = bits[word] & ~(1 << (slot & 31));
166
+ const weights = voice.weights;
167
+ if (weights !== null && slot < weights.length)
168
+ weights[slot] = 0;
169
+ const stamps = voice.stamps;
170
+ const o = slot * 2 /* Stamp.SIZE */;
171
+ if (stamps !== null && o < stamps.length) {
172
+ stamps[o + 0 /* Stamp.PROBED */] = Number.NaN;
173
+ voice.deltas[slot] = null;
174
+ }
175
+ }
176
+ /**
177
+ * Turns a sharing voice back to a record per subject, each made as it is next asked for from what
178
+ * the voice kept for the subject. With `kept`, a call just kept state on its one record, which is
179
+ * open on `keeper`, the subject called for: that subject takes the record as its own. A lane's
180
+ * positions take their own records now, since each held the one.
181
+ */
182
+ export function unshare(mix, voice, kept = false, keeper) {
183
+ if (!voice.sharing)
184
+ return;
185
+ voice.sharing = false;
186
+ const all = voice.everyone;
187
+ voice.everyone = null;
188
+ const at = mix.sharers.indexOf(voice);
189
+ if (at >= 0)
190
+ mix.sharers.splice(at, 1);
191
+ const slot = kept ? (mix.chains.get(keeper)?.slot ?? -1) : -1;
192
+ // A projection numbers no subject, so its copy's record is the keeper's without being asked.
193
+ if (all !== null && kept && (mix.projecting || sighted(voice, slot))) {
194
+ if (slot >= 0)
195
+ unsight(voice, slot);
196
+ voice.subjects.set(keeper, all);
197
+ }
198
+ else if (voice.holder === all)
199
+ voice.holder = null;
200
+ mix.lanes?.rerecord(voice.id, (subject, s) => mix.held(voice, subject, s));
201
+ // Every chain links it from here on.
202
+ if (voice.state !== 'done')
203
+ mix.steps.push(voice, ++mix.version);
204
+ mix.unshared = true;
205
+ detour(mix);
206
+ mix.lanes?.refill();
207
+ }
208
+ /**
209
+ * Counts what makes a mix's fold more than a walk of each chain: a locus, a voice sharing one
210
+ * record, a voice that stopped sharing this frame. Called wherever one of the three changes, so
211
+ * `foldWith` asks one number.
212
+ */
213
+ export function detour(mix) {
214
+ mix.detours = mix.loci + mix.sharers.length + (mix.unshared ? 1 : 0);
215
+ }
216
+ /** Gives a record just made for a subject a sharing voice had seen what the voice kept for it. */
217
+ export function inherit(voice, held, slot) {
218
+ held.weight = weightAt(voice, slot);
219
+ open(voice, held, slot);
220
+ unsight(voice, slot);
221
+ }
package/dist/fade.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { Mixer } from './mixer.js';
2
- import type { Voice } from './voice.js';
2
+ import type { Subject, 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. */
@@ -11,6 +11,12 @@ export declare function parting<I, O>(this: Mixer<I, O>, voice: Voice<I, O>, sub
11
11
  export declare function part<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>, subject: I, at: number): void;
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
+ /**
15
+ * The record a read back finds a subject left a voice with: its own, or the voice's record for
16
+ * subjects it does not reach while faded out of it, since a probe on either path may have left a
17
+ * record of that or none.
18
+ */
19
+ export declare function leftWith<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>, subject: I): Subject<unknown> | undefined;
14
20
  export declare function beginFade<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>, opts: {
15
21
  over?: number;
16
22
  at?: 'rest';
@@ -29,4 +35,5 @@ export declare function beginRise<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>, op
29
35
  * exactly there; at or behind now, the fade began then and is partway.
30
36
  */
31
37
  export declare function fadeAt<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>, at: number, over: number | undefined): void;
38
+ /** Removes a voice, recording that it left at `at`, default now. */
32
39
  export declare function retire<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>, at?: number): void;
package/dist/fade.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { changed } from './chain.js';
2
2
  import { envelope } from './clock.js';
3
3
  import { schedule } from './due.js';
4
+ import { unshare } from './everyone.js';
4
5
  import { leave, noted, releasing } from './history.js';
5
6
  import { orphan } from './owner.js';
6
7
  import { startOf } from './place.js';
@@ -11,6 +12,7 @@ import { unreach } from './unreached.js';
11
12
  export function fadeSubject(mix, voice, subject, over) {
12
13
  if (voice.state === 'done' || voice.parted?.has(subject) || voice.parts?.has(subject))
13
14
  return;
15
+ unshare(mix, voice);
14
16
  mix.stir();
15
17
  const ms = mix.reduced ? 0 : (over ?? voice.fade.out ?? 0);
16
18
  const at = Number.isNaN(mix.now) ? startOf(voice) : mix.now;
@@ -19,7 +21,7 @@ export function fadeSubject(mix, voice, subject, over) {
19
21
  return;
20
22
  }
21
23
  voice.parts ??= new Map();
22
- voice.parts.set(subject, { at, over: ms });
24
+ voice.parts.set(subject, { at, over: ms, seq: mix.transport.seq });
23
25
  mix.parters.add(voice);
24
26
  schedule(mix, voice);
25
27
  mix.lanes?.refill();
@@ -40,7 +42,7 @@ export function part(mix, voice, subject, at) {
40
42
  if (voice.parts?.size === 0)
41
43
  voice.parts = null;
42
44
  voice.parted ??= new Map();
43
- voice.parted.set(subject, at);
45
+ voice.parted.set(subject, { at, seq: mix.transport.seq });
44
46
  mix.parters.add(voice);
45
47
  forgetIn(mix, voice, subject);
46
48
  const motion = voice.motion;
@@ -49,8 +51,12 @@ export function part(mix, voice, subject, at) {
49
51
  }
50
52
  /** Brings a subject faded out of a voice back, to be met afresh on its next probe. */
51
53
  export function unpart(mix, voice, subject) {
52
- if (voice.parted?.delete(subject) !== true)
54
+ if (voice.parted?.has(subject) !== true)
53
55
  return;
56
+ // `forgetIn` keeps the subject's own record for a read back; without one, keep the voice's.
57
+ if (voice.subjects.get(subject) === undefined)
58
+ leave(mix, voice, subject, leftWith(mix, voice, subject));
59
+ voice.parted.delete(subject);
54
60
  if (voice.parted.size === 0)
55
61
  voice.parted = null;
56
62
  forgetIn(mix, voice, subject);
@@ -58,6 +64,17 @@ export function unpart(mix, voice, subject) {
58
64
  if (slot >= 0)
59
65
  mix.lanes?.rejoin(slot);
60
66
  }
67
+ /**
68
+ * The record a read back finds a subject left a voice with: its own, or the voice's record for
69
+ * subjects it does not reach while faded out of it, since a probe on either path may have left a
70
+ * record of that or none.
71
+ */
72
+ export function leftWith(mix, voice, subject) {
73
+ const held = voice.subjects.get(subject);
74
+ if (held !== undefined || voice.parted?.has(subject) !== true)
75
+ return held;
76
+ return voice.unreached ?? mix.unreachedOf(voice, mix.now);
77
+ }
61
78
  /** Drops a voice's record of a subject and relinks the subject's chain without it. */
62
79
  function forgetIn(mix, voice, subject) {
63
80
  const head = mix.chains.get(subject);
@@ -89,6 +106,12 @@ export function beginFade(mix, voice, opts, at) {
89
106
  // A voice is linked into its subjects' chains once it plays, and a fading one plays.
90
107
  if (voice.state === 'pending')
91
108
  changed(mix, voice);
109
+ // A fade at rest marks each subject's record as it comes to rest, at its next read: one already
110
+ // read this frame is read again, as after `touch`.
111
+ if (opts.at === 'rest') {
112
+ unshare(mix, voice);
113
+ voice.seeks++;
114
+ }
92
115
  voice.state = 'fading';
93
116
  voice.out = {
94
117
  from: 1,
@@ -151,11 +174,11 @@ export function fadeAt(mix, voice, at, over) {
151
174
  voice.outSet = true;
152
175
  noted(mix, voice);
153
176
  }
154
- /** Removes a voice, recording that it left at `at`, default now. */
155
177
  /** Whether a motion patch still asks this voice of this mix for its time. */
156
178
  function asks(mix, motion, voice) {
157
179
  return motion.owner === mix.owner && motion.ownerId === voice.id;
158
180
  }
181
+ /** Removes a voice, recording that it left at `at`, default now. */
159
182
  export function retire(mix, voice, at) {
160
183
  if (voice.holding === null)
161
184
  mix.lanes?.touch(voice);
@@ -168,7 +191,10 @@ export function retire(mix, voice, at) {
168
191
  voice.keepOn = null;
169
192
  mix.retired.push(voice);
170
193
  voice.doneAt = at ?? (Number.isNaN(mix.now) ? Number.NEGATIVE_INFINITY : mix.now);
171
- voice.play(false, voice.doneAt);
194
+ voice.doneSeq = mix.transport.seq;
195
+ // The frames up to this one showed it, though its fade ended before: it leaves the list now.
196
+ mix.transport.settle();
197
+ voice.play(false, voice.doneAt, voice.doneSeq);
172
198
  voice.resolve();
173
199
  // An owner takes what it holds with it, and leaves with its last child.
174
200
  if (voice.holding !== null)
@@ -180,7 +206,9 @@ export function retire(mix, voice, at) {
180
206
  const owner = orphan(voice);
181
207
  if (owner !== null && !lingers(owner, voice.doneAt))
182
208
  retire(mix, owner, voice.doneAt);
183
- else if (span.fitting !== null && span.state !== 'done')
209
+ // A child the fit skipped had no time in it, so the rest of the fit stands: fitting again
210
+ // between siblings skipped together would play the ones still to leave.
211
+ else if (span.fitting !== null && span.state !== 'done' && !voice.skipped)
184
212
  refit(mix, span);
185
213
  }
186
214
  }
package/dist/fill.d.ts CHANGED
@@ -8,12 +8,13 @@ export declare function fillAll<I, O>(lanes: Lanes<I, O>, now: number, version:
8
8
  export declare function rest<I, O>(lanes: Lanes<I, O>, lane: Paced): void;
9
9
  export declare function wake<I, O>(lanes: Lanes<I, O>, lane: Paced): void;
10
10
  /**
11
- * A voice with a signal weight: its weight for the subject at `slot` under the voice's fade `fade`,
12
- * through the general path's own arithmetic; NaN where that path has not made the voice's first
13
- * call for the subject, which it then makes this frame, so a signal that keeps state is first
14
- * called where the general path would call it.
11
+ * A voice with a signal weight: its weight for the subject at `slot`, left in `Arg.WEIGHT`, at the
12
+ * voice time, pass and fade in `Arg.ELAPSED`, `Arg.PASS` and `Arg.FADE`, through the general path's
13
+ * own arithmetic. False where that path has not made the voice's first call for the subject, which
14
+ * it then makes this frame, so a signal that keeps state is first called where the general path
15
+ * would call it.
15
16
  */
16
- export declare function signalled<I, O>(this: Lanes<I, O>, voice: Voice<I, O>, slot: number, rec: Subject<unknown>, elapsed: number, pass: number, fade: number): number;
17
+ export declare function signalled<I, O>(this: Lanes<I, O>, voice: Voice<I, O>, slot: number, rec: Subject<unknown>): boolean;
17
18
  /** What a subject's own ramp out of a voice leaves of its weight: 1 with none. */
18
19
  export declare function parting<I, O>(this: Lanes<I, O>, voice: Voice<I, O>, slot: number): number;
19
20
  export declare function subjectAt<I, O>(this: Lanes<I, O>, slot: number): I | typeof absent;
@@ -21,22 +22,30 @@ export declare function subjectAt<I, O>(this: Lanes<I, O>, slot: number): I | ty
21
22
  export declare function reset<I, O>(this: Lanes<I, O>, lane: Lane<I, O>): void;
22
23
  /** One voice's contribution to every subject it plays on. */
23
24
  export declare function run<I, O>(this: Lanes<I, O>, lane: Lane<I, O>): void;
25
+ /**
26
+ * `run`'s loop for a voice keeping one record for every subject: the record is pointed at each
27
+ * subject before its call and taken back after, so `one` and `call` read it as a record of the
28
+ * subject's own. A loop of its own, which leaves theirs as they were for every other voice.
29
+ */
30
+ export declare function runEveryone<I, O>(this: Lanes<I, O>, lane: Lane<I, O>): void;
24
31
  /**
25
32
  * One subject's contribution from a lane's voice. True where the voice gave the subject a delta,
26
33
  * as the general path's `contribution` does where it returns one, which is what makes a voice in a
27
34
  * locus one of its members for the subject.
28
35
  */
29
- export declare function one<I, O>(this: Lanes<I, O>, lane: Lane<I, O>, p: number, slot: number, elapsedNow: number, duration: number, passes: number): boolean;
36
+ export declare function one<I, O>(this: Lanes<I, O>, lane: Lane<I, O>, p: number, slot: number): boolean;
30
37
  /**
31
- * A keys voice's stops at `phase` folded straight into a subject's values, without a delta: what
32
- * `readKeyed` then `foldDelta` give, through the same segment search and the stock channels' own
33
- * lerp, which is all a laned keys voice can use.
38
+ * A keys voice's stops at `Arg.PHASE` folded straight into a subject's values at `Arg.WEIGHT`,
39
+ * without a delta: what `readKeyed` then `foldDelta` give, through the same segment search and the
40
+ * stock channels' own lerp, which is all a laned keys voice can use.
34
41
  */
35
- export declare function foldKeys<I, O>(this: Lanes<I, O>, voice: Voice<I, O>, chans: readonly Laned[], phase: number, slot: number, w: number): void;
42
+ export declare function foldKeys<I, O>(this: Lanes<I, O>, voice: Voice<I, O>, chans: readonly Laned[], slot: number): void;
36
43
  /** Reads a keys voice's stops at `phase` into `delta`, interpolating into the reader's `scratch`. */
37
44
  export declare function readKeyed<I, O>(this: Lanes<I, O>, voice: Voice<I, O>, phase: number, delta: Record<string, unknown>, scratch: Scratch): void;
38
45
  /**
39
- * Calls a stateless fn voice's patch for a subject its general path has met, and folds the delta;
40
- * false for a subject the host has let go of.
46
+ * Calls a stateless fn voice's patch for a subject its general path has met, and folds the delta,
47
+ * at the voice time, phase, pass, delay and weight in `Arg`; false where it gave no delta, for a
48
+ * subject the host has let go of or a weight the voice is silent at. As arguments those cost nothing where V8 inlines the call and 31 bytes a call where it
49
+ * does not, which differs by V8 version: Node 26.1 does not (2026-10-09).
41
50
  */
42
- export declare function call<I, O>(this: Lanes<I, O>, voice: Voice<I, O>, chans: readonly Laned[], rec: Subject<unknown>, slot: number, elapsed: number, phase: number, pass: number, delay: number, w: number): boolean;
51
+ export declare function call<I, O>(this: Lanes<I, O>, voice: Voice<I, O>, chans: readonly Laned[], rec: Subject<unknown>, slot: number): boolean;