@msb235/blits 0.12.1 → 0.13.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 +26 -0
- package/dist/channels.d.ts +5 -0
- package/dist/channels.js +2 -0
- package/dist/crowd.d.ts +2 -0
- package/dist/crowd.js +9 -1
- package/dist/cue.d.ts +1 -1
- package/dist/cue.js +18 -1
- package/dist/fold.js +13 -0
- package/dist/handle.js +1 -1
- package/dist/held.d.ts +2 -0
- package/dist/held.js +21 -13
- package/dist/mixer.js +2 -2
- package/dist/move.js +16 -9
- package/dist/origin.d.ts +3 -1
- package/dist/origin.js +16 -3
- package/dist/rows.js +3 -1
- package/dist/seek.js +9 -1
- package/dist/shared.d.ts +1 -1
- package/dist/shared.js +1 -1
- package/dist/stack.d.ts +2 -2
- package/dist/stack.js +4 -5
- package/dist/transport.d.ts +5 -0
- package/dist/transport.js +2 -0
- package/dist/types/mix.d.ts +7 -2
- package/dist/types/voice.d.ts +2 -1
- package/docs/schema.html +4 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,32 @@ 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.13.0
|
|
8
|
+
|
|
9
|
+
### Breaking
|
|
10
|
+
|
|
11
|
+
- A mix's stock `band` is on at any weight above 0 and off at 0, where it was on above 0.6 and off
|
|
12
|
+
below 0.4. A channel with no rest, such as `last()`, now shows its value through a whole fade in or
|
|
13
|
+
out. Pass `band: { on: 0.6, off: 0.4 }` for the old behavior. `fold` follows the same default.
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- A mix's `loci`: tags that act as loci, so a voice carrying one folds with the others carrying it,
|
|
18
|
+
as if its `locus` named the tag.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- A seek with `state: 'keep'` that sends a subject back before its start now starts it afresh there.
|
|
23
|
+
It kept the state it had stepped to, so what it showed, and how it played on from its start,
|
|
24
|
+
depended on how often the host had read it, and a mix seek could put back the state from before.
|
|
25
|
+
- A mix sought back and then ahead to where it stood reads the same `now` on its next sync as one
|
|
26
|
+
never sought. At some mix rates it read one rounding step off, and every later sync kept the error.
|
|
27
|
+
- Replacing voices costs less a frame: a crowd catches up only the rows marked stale, where it
|
|
28
|
+
scanned every row, and a few retired voices are spliced out of the list. One subject leaving for
|
|
29
|
+
good each frame and a new one coming (`turnover^`, 10,000 subjects) takes 0.286 ms a frame
|
|
30
|
+
against 0.315, and one voice replaced in place each frame (`churn^`) 0.224 against 0.252 (teitou,
|
|
31
|
+
medians of nine runs of 5,000 frames, each in its own process); steady voices are unchanged.
|
|
32
|
+
|
|
7
33
|
## 0.12.1
|
|
8
34
|
|
|
9
35
|
### Added
|
package/dist/channels.d.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import type { Channel, Kit } from './types.js';
|
|
2
|
+
/** The band a mix switches a rest-less channel's contribution across where its options name none. */
|
|
3
|
+
export declare const STOCK_BAND: {
|
|
4
|
+
readonly on: number;
|
|
5
|
+
readonly off: 0;
|
|
6
|
+
};
|
|
2
7
|
/** The stock numeric channels' `lerp`, which `vec` applies per axis and lanes call directly. */
|
|
3
8
|
export declare const lerpNumber: (a: number, b: number, u: number) => number;
|
|
4
9
|
/**
|
package/dist/channels.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { shared } from './shared.js';
|
|
2
|
+
/** The band a mix switches a rest-less channel's contribution across where its options name none. */
|
|
3
|
+
export const STOCK_BAND = { on: Number.MIN_VALUE, off: 0 };
|
|
2
4
|
/** The stock numeric channels' `lerp`, which `vec` applies per axis and lanes call directly. */
|
|
3
5
|
export const lerpNumber = (a, b, u) => a + (b - a) * u;
|
|
4
6
|
const mix = lerpNumber;
|
package/dist/crowd.d.ts
CHANGED
|
@@ -92,6 +92,8 @@ export declare class Crowd<I, O> implements Positions<I, O> {
|
|
|
92
92
|
dead: number;
|
|
93
93
|
/** Whether some row is `Flag.STALE`, for `freshen` to copy before the fill's loop. */
|
|
94
94
|
stale: boolean;
|
|
95
|
+
/** The rows marked stale since the last `freshen`, which checks each is still stale. */
|
|
96
|
+
staleRows: number[];
|
|
95
97
|
readonly xs: Float64Array;
|
|
96
98
|
readonly vs: Float64Array;
|
|
97
99
|
/** What a keys row reads into, folded before the next row reads. */
|
package/dist/crowd.js
CHANGED
|
@@ -54,6 +54,8 @@ export class Crowd {
|
|
|
54
54
|
dead = 0;
|
|
55
55
|
/** Whether some row is `Flag.STALE`, for `freshen` to copy before the fill's loop. */
|
|
56
56
|
stale = false;
|
|
57
|
+
/** The rows marked stale since the last `freshen`, which checks each is still stale. */
|
|
58
|
+
staleRows = [];
|
|
57
59
|
xs;
|
|
58
60
|
vs;
|
|
59
61
|
/** What a keys row reads into, folded before the next row reads. */
|
|
@@ -82,6 +84,7 @@ export class Crowd {
|
|
|
82
84
|
restale(p, flags) {
|
|
83
85
|
this.hot[p * this.stride + 0 /* Hot.FLAGS */] = flags | 4 /* Flag.STALE */;
|
|
84
86
|
this.stale = true;
|
|
87
|
+
this.staleRows.push(p);
|
|
85
88
|
}
|
|
86
89
|
/** Samples of another width than the channel's, which `samples` cannot hold, by row. */
|
|
87
90
|
odd = null;
|
|
@@ -347,7 +350,12 @@ export function freshen(c) {
|
|
|
347
350
|
c.stale = false;
|
|
348
351
|
const hot = c.hot;
|
|
349
352
|
const H = c.stride;
|
|
350
|
-
|
|
353
|
+
const rows = c.staleRows;
|
|
354
|
+
c.staleRows = [];
|
|
355
|
+
for (let i = 0; i < rows.length; i++) {
|
|
356
|
+
const p = rows[i];
|
|
357
|
+
if (p >= c.list.length)
|
|
358
|
+
continue;
|
|
351
359
|
const f = hot[p * H + 0 /* Hot.FLAGS */];
|
|
352
360
|
if ((f & 4 /* Flag.STALE */) === 0 || (f & 128 /* Flag.MOTION */) === 0)
|
|
353
361
|
continue;
|
package/dist/cue.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Mixer } from './mixer.js';
|
|
2
2
|
import type { Handle, Kit, Patch, VoiceSpec } from './types.js';
|
|
3
|
-
export declare function cue<I, O>(mix: Mixer<I, O>,
|
|
3
|
+
export declare function cue<I, O>(mix: Mixer<I, O>, given: VoiceSpec<I, O>): Handle<I>;
|
|
4
4
|
/** Refuses a patch writing a channel `kit` lacks, or holds as another kind than it was written for. */
|
|
5
5
|
export declare function fitsKit<I, O, S, H>(patch: Patch<I, O, S, H>, kit: Kit<O>): void;
|
|
6
6
|
/** Refuses a spec whose patch the mix cannot play, or whose state its history cannot keep. */
|
package/dist/cue.js
CHANGED
|
@@ -17,7 +17,8 @@ import { checkChild, refit } from './spans.js';
|
|
|
17
17
|
import { cuedById, motionOwner } from './strays.js';
|
|
18
18
|
import { record, unplayable } from './tape.js';
|
|
19
19
|
import { Voice } from './voice.js';
|
|
20
|
-
export function cue(mix,
|
|
20
|
+
export function cue(mix, given) {
|
|
21
|
+
const spec = locusOf(mix, given);
|
|
21
22
|
const patch = spec.patch;
|
|
22
23
|
playable(mix, spec);
|
|
23
24
|
if (spec.from === 'current' && patch.form !== 'keys')
|
|
@@ -88,6 +89,22 @@ export function fitsKit(patch, kit) {
|
|
|
88
89
|
throw new Error(`blits: channel ${String(channel)} is ${here.kind ?? 'a custom channel'} in this kit, but the patch was written for ${wanted.kind ?? 'a custom channel'}`);
|
|
89
90
|
}
|
|
90
91
|
}
|
|
92
|
+
/** The spec with its `locus` set from a tag the mix declares one, where it carries one. */
|
|
93
|
+
function locusOf(mix, spec) {
|
|
94
|
+
const loci = mix.opts.loci;
|
|
95
|
+
const tags = spec.tags;
|
|
96
|
+
if (loci === undefined || tags === undefined)
|
|
97
|
+
return spec;
|
|
98
|
+
let locus = spec.locus;
|
|
99
|
+
for (const tag of tags) {
|
|
100
|
+
if (!loci.includes(tag) || tag === locus)
|
|
101
|
+
continue;
|
|
102
|
+
if (locus !== undefined)
|
|
103
|
+
throw new Error(`blits: a voice folds in one locus, not both ${locus} and ${tag}`);
|
|
104
|
+
locus = tag;
|
|
105
|
+
}
|
|
106
|
+
return locus === spec.locus ? spec : { ...spec, locus };
|
|
107
|
+
}
|
|
91
108
|
/** Refuses a spec whose patch the mix cannot play, or whose state its history cannot keep. */
|
|
92
109
|
export function playable(mix, spec) {
|
|
93
110
|
const patch = spec.patch;
|
package/dist/fold.js
CHANGED
|
@@ -2,6 +2,7 @@ import { foldNumber } from './channels.js';
|
|
|
2
2
|
import { passAt, phaseAt, phaseBefore, silent, stepOf } from './clock.js';
|
|
3
3
|
import { copy as copyValue, first } from './clone.js';
|
|
4
4
|
import { open as openRecord, shut as shutRecord, weighAt } from './everyone.js';
|
|
5
|
+
import { waits } from './held.js';
|
|
5
6
|
import { apart, copyState, recordHost, remember } from './history.js';
|
|
6
7
|
import { stateful } from './hosts.js';
|
|
7
8
|
import { locusScratch } from './locus.js';
|
|
@@ -59,6 +60,13 @@ function rebuild(mix, voice, subject, now, held) {
|
|
|
59
60
|
held.stepped = held.since < now ? held.since : now;
|
|
60
61
|
held.probed = Number.NaN;
|
|
61
62
|
}
|
|
63
|
+
/** Whether a seek since its last read moved a stateful record's origin, as one back before it does. */
|
|
64
|
+
function sentBack(mix, voice, held) {
|
|
65
|
+
const p = voice.patch;
|
|
66
|
+
if (p.state === undefined && p.step === undefined && !voice.keeping)
|
|
67
|
+
return false;
|
|
68
|
+
return mix.sinceOf(voice, held.delay) !== held.since;
|
|
69
|
+
}
|
|
62
70
|
/**
|
|
63
71
|
* One live voice's delta for one subject this frame, through its record for the subject, or null
|
|
64
72
|
* when it does not reach. Its weight is left in `w`.
|
|
@@ -71,6 +79,11 @@ export function contribution(voice, subject, now, held) {
|
|
|
71
79
|
return null;
|
|
72
80
|
if (held.rebuilt !== voice.rebuilds)
|
|
73
81
|
rebuild(this, voice, subject, now, held);
|
|
82
|
+
else if (held.seeks !== voice.seeks && sentBack(this, voice, held)) {
|
|
83
|
+
// A seek sent it back before its start, so it starts afresh there, whatever state it kept.
|
|
84
|
+
rebuild(this, voice, subject, now, held);
|
|
85
|
+
waits(voice, held, now);
|
|
86
|
+
}
|
|
74
87
|
const raw = voice.rawAt(now, held.delay);
|
|
75
88
|
const early = voice.before(raw, now);
|
|
76
89
|
if (early && !voice.holdsFirst)
|
package/dist/handle.js
CHANGED
|
@@ -115,7 +115,7 @@ export class VoiceHandle {
|
|
|
115
115
|
// A pending voice's clock is anchored at its start, so it starts there on time.
|
|
116
116
|
if (voice.state !== 'pending') {
|
|
117
117
|
const u = this.#host.nowFor(voice);
|
|
118
|
-
leave(voice, u);
|
|
118
|
+
leave(voice, u, true);
|
|
119
119
|
voice.rebase(u);
|
|
120
120
|
}
|
|
121
121
|
voice.anchorElapsed = elapsed;
|
package/dist/held.d.ts
CHANGED
|
@@ -24,6 +24,8 @@ export declare function sinceOf<I, O>(this: Mixer<I, O>, voice: Voice<I, O>, del
|
|
|
24
24
|
* the voice first showed, for one freezing before.
|
|
25
25
|
*/
|
|
26
26
|
export declare function shownOf<I, O>(this: Mixer<I, O>, voice: Voice<I, O>, since: number): number;
|
|
27
|
+
/** Lists a record whose origin is still ahead of `now`, for a later change to the clock to move. */
|
|
28
|
+
export declare function waits<I, O>(voice: Voice<I, O>, held: Subject<unknown>, now: number): void;
|
|
27
29
|
/**
|
|
28
30
|
* A pending voice starts at `now`. Starting anywhere but 0 it was sought there, so a record made
|
|
29
31
|
* while it waited is made again for where its clock is.
|
package/dist/held.js
CHANGED
|
@@ -64,19 +64,7 @@ export function held(voice, subject, slot, guess = null) {
|
|
|
64
64
|
held.unknown = (this.backward && voice.spec.from === 'current') || fresh;
|
|
65
65
|
}
|
|
66
66
|
voice.subjects.set(subject, held);
|
|
67
|
-
|
|
68
|
-
voice.early ??= [];
|
|
69
|
-
const early = voice.early;
|
|
70
|
-
// Prunes at each doubling, so records whose origin has passed are not kept to the voice's end.
|
|
71
|
-
if (early.length >= 64 && (early.length & (early.length - 1)) === 0) {
|
|
72
|
-
let kept = 0;
|
|
73
|
-
for (const h of early)
|
|
74
|
-
if (!(h.since <= now))
|
|
75
|
-
early[kept++] = h;
|
|
76
|
-
early.length = kept;
|
|
77
|
-
}
|
|
78
|
-
early.push(held);
|
|
79
|
-
}
|
|
67
|
+
waits(voice, held, now);
|
|
80
68
|
// A subject a voice saw while it shared one record was counted then.
|
|
81
69
|
if (sighted(voice, slot))
|
|
82
70
|
inherit(voice, held, slot);
|
|
@@ -137,6 +125,22 @@ export function shownOf(voice, since) {
|
|
|
137
125
|
const from = since < start ? start : since;
|
|
138
126
|
return voice.freezesBefore && voice.opened < from ? voice.opened : from;
|
|
139
127
|
}
|
|
128
|
+
/** Lists a record whose origin is still ahead of `now`, for a later change to the clock to move. */
|
|
129
|
+
export function waits(voice, held, now) {
|
|
130
|
+
if (held.since <= now)
|
|
131
|
+
return;
|
|
132
|
+
voice.early ??= [];
|
|
133
|
+
const early = voice.early;
|
|
134
|
+
// Prunes at each doubling, so records whose origin has passed are not kept to the voice's end.
|
|
135
|
+
if (early.length >= 64 && (early.length & (early.length - 1)) === 0) {
|
|
136
|
+
let kept = 0;
|
|
137
|
+
for (const h of early)
|
|
138
|
+
if (!(h.since <= now))
|
|
139
|
+
early[kept++] = h;
|
|
140
|
+
early.length = kept;
|
|
141
|
+
}
|
|
142
|
+
early.push(held);
|
|
143
|
+
}
|
|
140
144
|
/**
|
|
141
145
|
* A pending voice starts at `now`. Starting anywhere but 0 it was sought there, so a record made
|
|
142
146
|
* while it waited is made again for where its clock is.
|
|
@@ -154,7 +158,11 @@ export function begin(mix, voice, now) {
|
|
|
154
158
|
}
|
|
155
159
|
/** Counts a record whose `since` was still ahead from where its voice's clock now puts it. */
|
|
156
160
|
export function origin(mix, voice, held) {
|
|
161
|
+
const was = held.since;
|
|
157
162
|
held.since = mix.sinceOf(voice, held.delay);
|
|
163
|
+
// Sent back before a start it had passed: its next read starts it afresh.
|
|
164
|
+
if (held.since !== was && (held.ticks > 0 || held.stepped > was))
|
|
165
|
+
held.rebuilt = -1;
|
|
158
166
|
held.shown = mix.shownOf(voice, held.since);
|
|
159
167
|
if (held.ticks === 0 && held.stepped > held.since)
|
|
160
168
|
held.stepped = held.since;
|
package/dist/mixer.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { cueArc } from './arcs.js';
|
|
2
2
|
import { blended, blendOf, laneRead, shareKept } from './blend.js';
|
|
3
3
|
import { aims, chain, headOf, linkable, relink, unlink } from './chain.js';
|
|
4
|
-
import { lerpInto, scalarOp } from './channels.js';
|
|
4
|
+
import { lerpInto, STOCK_BAND, scalarOp } from './channels.js';
|
|
5
5
|
import { cue } from './cue.js';
|
|
6
6
|
import { DepartedIndex } from './departed.js';
|
|
7
7
|
import { beginFade, leaveVoice, parting } from './fade.js';
|
|
@@ -328,7 +328,7 @@ export class Mixer {
|
|
|
328
328
|
return typeof r === 'function' ? r() : r === true;
|
|
329
329
|
}
|
|
330
330
|
get band() {
|
|
331
|
-
return this.opts.band ??
|
|
331
|
+
return this.opts.band ?? STOCK_BAND;
|
|
332
332
|
}
|
|
333
333
|
cue(spec) {
|
|
334
334
|
this.attached();
|
package/dist/move.js
CHANGED
|
@@ -172,6 +172,8 @@ function turn(mix, voice, now) {
|
|
|
172
172
|
*/
|
|
173
173
|
const SEARCHED = 8;
|
|
174
174
|
const SPARSE = 32;
|
|
175
|
+
/** The most retired voices `prune` splices out one at a time, rather than closing up in one pass. */
|
|
176
|
+
const SPLICED = 4;
|
|
175
177
|
/**
|
|
176
178
|
* Takes the voices retired since the last prune out of the list, in list order. A few, or a small
|
|
177
179
|
* share of a long list, are found by their id, which the list is in order of, so the voices still
|
|
@@ -200,16 +202,21 @@ function prune(mix) {
|
|
|
200
202
|
if (at !== null) {
|
|
201
203
|
for (const voice of retired)
|
|
202
204
|
pruned = leave(mix, voice) || pruned;
|
|
203
|
-
//
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
205
|
+
// A splice moves the tail natively, several times faster than a loop, but once a voice.
|
|
206
|
+
if (at.length <= SPLICED)
|
|
207
|
+
for (let k = at.length - 1; k >= 0; k--)
|
|
208
|
+
voices.splice(at[k], 1);
|
|
209
|
+
else {
|
|
210
|
+
let kept = at[0];
|
|
211
|
+
let k = 0;
|
|
212
|
+
for (let i = kept; i < voices.length; i++) {
|
|
213
|
+
if (at[k] === i)
|
|
214
|
+
k++;
|
|
215
|
+
else
|
|
216
|
+
voices[kept++] = voices[i];
|
|
217
|
+
}
|
|
218
|
+
voices.length = kept;
|
|
211
219
|
}
|
|
212
|
-
voices.length = kept;
|
|
213
220
|
}
|
|
214
221
|
else {
|
|
215
222
|
let kept = 0;
|
package/dist/origin.d.ts
CHANGED
|
@@ -5,12 +5,14 @@ export interface Past extends Clock {
|
|
|
5
5
|
until: number;
|
|
6
6
|
/** The voice's `rebuilds` while it ran on this clock. */
|
|
7
7
|
rebuilds: number;
|
|
8
|
+
/** Whether the voice left it by a seek. */
|
|
9
|
+
sought: boolean;
|
|
8
10
|
}
|
|
9
11
|
/**
|
|
10
12
|
* Keeps the clock a voice is leaving at its local time `u`, ahead of a rate change, ramp or seek,
|
|
11
13
|
* where a subject's origin could still be found on it.
|
|
12
14
|
*/
|
|
13
|
-
export declare function leave<I, O>(voice: Voice<I, O>, u: number): void;
|
|
15
|
+
export declare function leave<I, O>(voice: Voice<I, O>, u: number, sought?: boolean): void;
|
|
14
16
|
/**
|
|
15
17
|
* The mix time a voice's clock first read `elapsed`, or will by its controls now, through its owners'
|
|
16
18
|
* clocks. A clock entered already past it, by a seek, answers where that clock would have read it.
|
package/dist/origin.js
CHANGED
|
@@ -5,7 +5,7 @@ const MOST = 256;
|
|
|
5
5
|
* Keeps the clock a voice is leaving at its local time `u`, ahead of a rate change, ramp or seek,
|
|
6
6
|
* where a subject's origin could still be found on it.
|
|
7
7
|
*/
|
|
8
|
-
export function leave(voice, u) {
|
|
8
|
+
export function leave(voice, u, sought = false) {
|
|
9
9
|
const past = voice.clocks;
|
|
10
10
|
if (past !== null && past.length >= MOST)
|
|
11
11
|
return;
|
|
@@ -14,7 +14,7 @@ export function leave(voice, u) {
|
|
|
14
14
|
if (past !== null && reached(past, 0) >= reach && reached(past, fresh(voice)) >= reach)
|
|
15
15
|
return;
|
|
16
16
|
const { anchorNow, anchorElapsed, rate, ramp, rebuilds } = voice;
|
|
17
|
-
const left = { anchorNow, anchorElapsed, rate, ramp, until: u, rebuilds };
|
|
17
|
+
const left = { anchorNow, anchorElapsed, rate, ramp, until: u, rebuilds, sought };
|
|
18
18
|
if (past === null)
|
|
19
19
|
voice.clocks = [left];
|
|
20
20
|
else
|
|
@@ -41,6 +41,18 @@ function fresh(voice) {
|
|
|
41
41
|
i++;
|
|
42
42
|
return i;
|
|
43
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* Where a walk for a stateful record at `elapsed` starts after a seek back before it: the clock that
|
|
46
|
+
* seek entered, since a subject sent back before its start starts afresh.
|
|
47
|
+
*/
|
|
48
|
+
function backFrom(voice, past, elapsed) {
|
|
49
|
+
for (let i = past.length - 1; i >= 0; i--) {
|
|
50
|
+
const p = past[i];
|
|
51
|
+
if (p.sought && (past[i + 1]?.anchorElapsed ?? voice.anchorElapsed) < elapsed)
|
|
52
|
+
return i + 1;
|
|
53
|
+
}
|
|
54
|
+
return 0;
|
|
55
|
+
}
|
|
44
56
|
/**
|
|
45
57
|
* The mix time a voice's clock first read `elapsed`, or will by its controls now, through its owners'
|
|
46
58
|
* clocks. A clock entered already past it, by a seek, answers where that clock would have read it.
|
|
@@ -51,7 +63,8 @@ export function originOf(voice, elapsed, rebuilt) {
|
|
|
51
63
|
// A voice with no first pass shows every subject from its start, whatever its stagger.
|
|
52
64
|
let t = voice.beginless ? voice.start : Number.NaN;
|
|
53
65
|
if (past !== null && Number.isNaN(t)) {
|
|
54
|
-
|
|
66
|
+
const from = rebuilt ? Math.max(fresh(voice), backFrom(voice, past, elapsed)) : 0;
|
|
67
|
+
for (let i = from; i < past.length; i++) {
|
|
55
68
|
const p = past[i];
|
|
56
69
|
const at = timeWith(p, elapsed);
|
|
57
70
|
if (at <= p.until) {
|
package/dist/rows.js
CHANGED
|
@@ -181,8 +181,10 @@ function copyRow(from, p, to, q, keys = from.keys) {
|
|
|
181
181
|
carry(from.data, p * 8 /* Row.STRIDE */, to.data, q * 8 /* Row.STRIDE */, 8 /* Row.STRIDE */);
|
|
182
182
|
carry(from.hot, p * from.stride, to.hot, q * to.stride, from.stride);
|
|
183
183
|
copyKeys(keys, to, q);
|
|
184
|
-
if ((from.hot[p * from.stride + 0 /* Hot.FLAGS */] & 4 /* Flag.STALE */) !== 0)
|
|
184
|
+
if ((from.hot[p * from.stride + 0 /* Hot.FLAGS */] & 4 /* Flag.STALE */) !== 0) {
|
|
185
185
|
to.stale = true;
|
|
186
|
+
to.staleRows.push(q);
|
|
187
|
+
}
|
|
186
188
|
carry(from.samples, p * from.axes, to.samples, q * to.axes, from.axes);
|
|
187
189
|
to.deltas[q] = from.deltas[p] ?? null;
|
|
188
190
|
to.records[q] = from.records[p];
|
package/dist/seek.js
CHANGED
|
@@ -252,7 +252,14 @@ export function seek(transport, to) {
|
|
|
252
252
|
if (frame === undefined) {
|
|
253
253
|
// Ahead, under the rate the recorded calls up to `t` set, past any frames rate 0 held at `t`.
|
|
254
254
|
const last = replay(transport, (at) => (transport.pace === null ? at : transport.pace.reading(at)) <= t);
|
|
255
|
-
const
|
|
255
|
+
const left = transport.left;
|
|
256
|
+
// Inverting the rate can land an ulp from where the host stood, which every later sync would carry.
|
|
257
|
+
const at = left !== null &&
|
|
258
|
+
left.t === t &&
|
|
259
|
+
left.u >= transport.u &&
|
|
260
|
+
(transport.pace === null ? left.u : transport.pace.reading(left.u)) === t
|
|
261
|
+
? left.u
|
|
262
|
+
: hostAhead(transport, t);
|
|
256
263
|
const u = Number.isNaN(last) ? at : Math.max(last, at);
|
|
257
264
|
if (!Number.isFinite(u))
|
|
258
265
|
throw new RangeError(`blits: the mix's rate never reaches ${t}`);
|
|
@@ -266,6 +273,7 @@ export function seek(transport, to) {
|
|
|
266
273
|
// On the frame itself, its own host time: rate 0 may have held the clock there for several.
|
|
267
274
|
const u = frame.at === t ? frame.u : hostAt(transport, t);
|
|
268
275
|
const seq = frame.seq;
|
|
276
|
+
transport.left = { t: transport.now, u: transport.u };
|
|
269
277
|
tape.goto(tape.depthAt(u));
|
|
270
278
|
transport.undrop(seq);
|
|
271
279
|
// What history kept after the frame is let go; the tape holds the calls that made it.
|
package/dist/shared.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* The package version, for a fact whose shape follows the code too closely to version by hand:
|
|
3
3
|
* copies of different releases keep apart. `test/copies.test.ts` holds it to `package.json`.
|
|
4
4
|
*/
|
|
5
|
-
export declare const release = "0.
|
|
5
|
+
export declare const release = "0.13.0";
|
|
6
6
|
/**
|
|
7
7
|
* A WeakMap every copy of blits loaded in one realm shares, so a fact one copy records about a
|
|
8
8
|
* patch or channel it made is read by another copy's mix. The name carries a version, bumped when
|
package/dist/shared.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* The package version, for a fact whose shape follows the code too closely to version by hand:
|
|
3
3
|
* copies of different releases keep apart. `test/copies.test.ts` holds it to `package.json`.
|
|
4
4
|
*/
|
|
5
|
-
export const release = '0.
|
|
5
|
+
export const release = '0.13.0';
|
|
6
6
|
/**
|
|
7
7
|
* A WeakMap every copy of blits loaded in one realm shares, so a fact one copy records about a
|
|
8
8
|
* patch or channel it made is read by another copy's mix. The name carries a version, bumped when
|
package/dist/stack.d.ts
CHANGED
|
@@ -3,8 +3,8 @@ import type { Kit } from './types.js';
|
|
|
3
3
|
* Folds deltas into one pose by a kit's arithmetic, in order, with no mix and no clock: what a mix
|
|
4
4
|
* gives for voices contributing these deltas at these weights. Each channel starts at its rest, a
|
|
5
5
|
* weight (default 1, held to 0..1) fades a delta toward rest, and the pose is clamped to each
|
|
6
|
-
* channel's bounds. A channel with no rest takes the last delta
|
|
7
|
-
*
|
|
6
|
+
* channel's bounds. A channel with no rest takes the last delta a mix's default band switches on, any
|
|
7
|
+
* weight above 0, and stays undefined where none is.
|
|
8
8
|
*
|
|
9
9
|
* @category channel
|
|
10
10
|
*/
|
package/dist/stack.js
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
|
+
import { STOCK_BAND } from './channels.js';
|
|
1
2
|
import { copy, first } from './clone.js';
|
|
2
3
|
import { bound, merged } from './fold.js';
|
|
3
|
-
/** The weight a mix's default band first switches a rest-less channel on at. */
|
|
4
|
-
const ON = 0.6;
|
|
5
4
|
/**
|
|
6
5
|
* Folds deltas into one pose by a kit's arithmetic, in order, with no mix and no clock: what a mix
|
|
7
6
|
* gives for voices contributing these deltas at these weights. Each channel starts at its rest, a
|
|
8
7
|
* weight (default 1, held to 0..1) fades a delta toward rest, and the pose is clamped to each
|
|
9
|
-
* channel's bounds. A channel with no rest takes the last delta
|
|
10
|
-
*
|
|
8
|
+
* channel's bounds. A channel with no rest takes the last delta a mix's default band switches on, any
|
|
9
|
+
* weight above 0, and stays undefined where none is.
|
|
11
10
|
*
|
|
12
11
|
* @category channel
|
|
13
12
|
*/
|
|
@@ -34,7 +33,7 @@ export function fold(kit, deltas, weights) {
|
|
|
34
33
|
pose[key] = channel.fold
|
|
35
34
|
? channel.fold(pose[key], value, w)
|
|
36
35
|
: merged(channel, pose[key], channel.scale(value, w));
|
|
37
|
-
else if (w >=
|
|
36
|
+
else if (w >= STOCK_BAND.on)
|
|
38
37
|
pose[key] = pose[key] === undefined ? first(value) : merged(channel, pose[key], value);
|
|
39
38
|
}
|
|
40
39
|
}
|
package/dist/transport.d.ts
CHANGED
|
@@ -73,6 +73,11 @@ export declare class Transport implements TransportApi {
|
|
|
73
73
|
/** Host time at the last sync: the host's timestamp less every gap `rebase` took out. */
|
|
74
74
|
u: number;
|
|
75
75
|
offset: number;
|
|
76
|
+
/** Where the last seek back left from, so a seek ahead to the same mix time lands on the same host time. */
|
|
77
|
+
left: {
|
|
78
|
+
t: number;
|
|
79
|
+
u: number;
|
|
80
|
+
} | null;
|
|
76
81
|
/** The mix time of the first sync. */
|
|
77
82
|
born: number;
|
|
78
83
|
/** The earliest mix time what history keeps in memory restores exactly. */
|
package/dist/transport.js
CHANGED
|
@@ -45,6 +45,8 @@ export class Transport {
|
|
|
45
45
|
/** Host time at the last sync: the host's timestamp less every gap `rebase` took out. */
|
|
46
46
|
u = Number.NaN;
|
|
47
47
|
offset = 0;
|
|
48
|
+
/** Where the last seek back left from, so a seek ahead to the same mix time lands on the same host time. */
|
|
49
|
+
left = null;
|
|
48
50
|
/** The mix time of the first sync. */
|
|
49
51
|
born = Number.NaN;
|
|
50
52
|
/** The earliest mix time what history keeps in memory restores exactly. */
|
package/dist/types/mix.d.ts
CHANGED
|
@@ -17,13 +17,18 @@ export interface MixOptions<H = unknown> {
|
|
|
17
17
|
/** Merged into every `setting.host`. Its type is the mix's `H`, which every patch and signal it cues reads. */
|
|
18
18
|
host?: H;
|
|
19
19
|
/**
|
|
20
|
-
* The band a rest-less channel's contribution switches on and off across.
|
|
21
|
-
*
|
|
20
|
+
* The band a rest-less channel's contribution switches on and off across. Default: on at any weight
|
|
21
|
+
* above 0, off at 0, so the contribution passes whole while its voice has any weight.
|
|
22
22
|
*/
|
|
23
23
|
band?: {
|
|
24
24
|
on: number;
|
|
25
25
|
off: number;
|
|
26
26
|
};
|
|
27
|
+
/**
|
|
28
|
+
* Tags that act as loci: a voice carrying one folds with the others carrying it, as if its `locus`
|
|
29
|
+
* named that tag.
|
|
30
|
+
*/
|
|
31
|
+
loci?: readonly string[];
|
|
27
32
|
/**
|
|
28
33
|
* The most `dt` a `step` or signal is handed, however long the gap. Off by default, since a
|
|
29
34
|
* patch with a closed form wants the whole gap; an integrator wants this set.
|
package/dist/types/voice.d.ts
CHANGED
|
@@ -399,7 +399,8 @@ export interface Handle<I = unknown> {
|
|
|
399
399
|
}
|
|
400
400
|
/**
|
|
401
401
|
* What `Handle.seek` does with each subject's state: `rebuild` (the default) makes it fresh and
|
|
402
|
-
* steps it to the new position; `keep` leaves it as it was
|
|
402
|
+
* steps it to the new position; `keep` leaves it as it was, except for a subject the seek sends back
|
|
403
|
+
* before its start, which starts afresh there.
|
|
403
404
|
*
|
|
404
405
|
* @category voice
|
|
405
406
|
*/
|
package/docs/schema.html
CHANGED
|
@@ -295,7 +295,7 @@ type Kit<O> = { readonly [K in keyof O]-?: Channel<NonNullable<O[K]&
|
|
|
295
295
|
|
|
296
296
|
<p><b>Color is a value; how it merges is a parameter.</b> A <code>color</code> value is OKLab <code>[L, a, b, coverage]</code>, and <code>oklab(0xrrggbb)</code> makes one at full coverage. <code>color()</code> averages: voices stack as a premultiplied sum, so the pose is <code>[Σw·L, Σw·a, Σw·b, Σw]</code>, which lanes and <code>pull</code> fold as any <code>vec(4, sum())</code>. blits knows no subject's own color, so the host lays the result over its base at write: <code>toHex(pose, under)</code> divides the coverage out, caps it at 1, mixes over <code>under</code> in OKLab and clips to sRGB per channel, and <code>css(pose)</code> writes an <code>oklab()</code> with the coverage as alpha. <code>color(last())</code> replaces, the last voice to pass winning behind the band. <code>mul</code> and <code>max</code> per OKLab axis mean nothing anyone asks for, so <code>color</code> refuses them; a tint, a hue shift, chroma and lightness are channels of their own. OKLab is the store because voices average per axis there, and OKLCH's hue wraps. How two colors interpolate is the channel's choice, <code>{ lerp: 'oklab' | 'oklch' }</code>: OKLab, the default, crosses opposite hues through a pale gray, and OKLCH goes round the hue as <code>mixHex</code> does, at the cost of leaving the lanes.</p>
|
|
297
297
|
|
|
298
|
-
<p>A channel with no rest cannot fade. The mix handles that in one way: below a weight threshold the contribution is dropped, above it the value passes through whole. That is the rule hinge's blend mode already applies to color, and it is stated here once so no consumer rediscovers it. <b>The threshold is a band, not an edge</b> — it drops at <code>off</code> and passes at <code>on</code>, and between the two it holds whatever it last did. One edge chatters: a weight resting on it is carried back and forth by frame noise, and every crossing drops or restores the whole contribution. Retargeting is the way a rest-less channel does move smoothly, since a lerp from a sampled value needs no identity.</p>
|
|
298
|
+
<p>A channel with no rest cannot fade. The mix handles that in one way: below a weight threshold the contribution is dropped, above it the value passes through whole. That is the rule hinge's blend mode already applies to color, and it is stated here once so no consumer rediscovers it. <b>The threshold is a band, not an edge</b> — it drops at <code>off</code> and passes at <code>on</code>, and between the two it holds whatever it last did. One edge chatters: a weight resting on it is carried back and forth by frame noise, and every crossing drops or restores the whole contribution. Retargeting is the way a rest-less channel does move smoothly, since a lerp from a sampled value needs no identity. <b>The stock band is no band</b>: on at any weight above 0 and off at 0, so a fade in shows the value at once and a fade out holds it to its end. The band was meant to be sized from frame noise in klieg's <code>near</code> and <code>dwell</code>, but klieg, its one measured user, set its mix to exactly this to match <code>hinge</code>, so it is the default (2026-10-23); a host whose weight rests near a threshold passes a band of its own.</p>
|
|
299
299
|
|
|
300
300
|
<p>The stock set is not the limit: a consumer that needs arithmetic it lacks writes its own <code>Channel</code>, and nothing in the mix knows the difference. sherpa's <code>transform</code> is one, composed by the host from whatever it puts in a matrix. A channel whose values are objects can add <code>fold</code>, which writes each contribution into the pose's own value where <code>merge</code> and <code>scale</code> would each make a new one. The mix starts that value as a copy of the channel's <code>rest</code>, and copies an array, a typed array, or a plain object itself; where <code>rest</code> is anything else, such as a class instance, the channel gives <code>copy</code>, or <code>cue</code> refuses a patch that writes it.</p>
|
|
301
301
|
|
|
@@ -554,7 +554,7 @@ interface MixOptions<H = unknown> {
|
|
|
554
554
|
reduce?: boolean | (() => boolean);
|
|
555
555
|
<span class="cm">/** Merged into every `setting.host`. `H` types it for every patch and signal the mix cues, so none needs a cast. */</span>
|
|
556
556
|
host?: H;
|
|
557
|
-
<span class="cm">/** The band a rest-less channel's contribution switches across. Default on
|
|
557
|
+
<span class="cm">/** The band a rest-less channel's contribution switches across. Default on above 0 and off at 0: no band. */</span>
|
|
558
558
|
band?: { on: number; off: number };
|
|
559
559
|
<span class="cm">/** The most `dt` a step or signal is handed, however long the gap. Off by default. */</span>
|
|
560
560
|
maxDt?: number;
|
|
@@ -741,6 +741,7 @@ mix.book({
|
|
|
741
741
|
<tr><td>Retarget on interruption</td><td><code>from: 'current'</code> on a <code>keys</code> voice takes stop 0 from the subject's pose in the frame before it first contributes — every other voice folded in, itself excluded — and lerps from there through the channel's <code>lerp</code>. It also leaves at the velocity the subject had, taken from its last two probed poses: the first segment gains <code>c·u(1−u)²</code>, which is zero at both ends and flat at the end, so the segment's own curve still lands on its stop while the start matches the subject's speed. A subject that was still leaves at rest. With fewer than two poses on record, as on a mix whose host probes into its own object before any retarget is cued, it starts on the segment's own curve.</td><td>wod's base-sampling. New to klieg: a regroup could tween from mid-float instead of carrying it away.</td></tr>
|
|
742
742
|
</table></div>
|
|
743
743
|
|
|
744
|
+
<p><b>A tag can be a locus.</b> A mix made with <code>loci: ['phase']</code> folds every voice carrying the tag <code>phase</code> as one locus, as if each had <code>locus: 'phase'</code>, so a source that already tags its voices need not name a locus too. A voice whose tags and <code>locus</code> name two loci is refused. Decided 2026-09-29 that tags absorb loci; the declaration was put on the mix (2026-10-23), and <code>locus</code> stays, since klieg, wod, magicsmoke and astv cue with it.</p>
|
|
744
745
|
<p><b>A locus is folded by <code>lerp</code>, not by scaling and merging.</b> Voices sharing a <code>locus</code> are alternatives in time, so the mix combines them into one contribution first: each member weighted by its share of the locus's summed weight, through the channel's own <code>lerp</code>, in cue order. Each channel of that contribution then contributes at <code>min(1, Σw)</code> over the members that wrote it, and is scaled toward rest like any other: a channel only one member writes fades with that member, rather than holding at the whole locus’s weight while it leaves. On a <code>sum</code> channel this is exactly <code>w₁a + w₂b</code>, the arithmetic klieg's timeline already runs, so the cap it was written as is a special case of the fold rather than a rule of its own. On <code>mul</code>, <code>max</code> and <code>last</code> it is the difference between an alternative and a pile-up: two voices halfway through a handover, both holding a gain of 0.06, fold to 0.06 — which is what one alternative means — where scaling and merging reads 0.28, the product of two half-scaled copies. A grouped voice may write only channels that have a <code>lerp</code>, which every stock channel does. A stock <code>last()</code> channel has nothing to interpolate, so the locus takes its heaviest member’s value, and the later cued of two as heavy. Cue order is otherwise visible only to a <code>lerp</code> that is not linear: a <code>color({ lerp: 'oklch' })</code> or a <code>hex</code> takes the hue the short way round at each step, so three or more of them in a locus can read differently by the order they were cued in.</p>
|
|
745
746
|
|
|
746
747
|
<div class="callout">
|
|
@@ -901,7 +902,7 @@ for (const step of steps) mix.arc({ owner: program, active: stage(step), hold: 4
|
|
|
901
902
|
</ul>
|
|
902
903
|
Decided 2026-10-06 and 2026-10-07, reversing the 2026-10-01 decision that mix time is only ever a reading. The mix goes back by itself rather than through snapshots the host saves and loads, since a host-kept log of its own calls would be a second record of what the mix did. What happened after <code>t</code> plays again rather than vanishing, because scrubbing must never cut off a recorded future. A call made while rewound branches rather than dropping the old future, and the mix drops its own copies of state after the fork rather than keeping them per branch, so memory stays inside the horizon. History lives only for the session. The mix's own logs hold what happened up to the playhead and the tape holds the calls after it, so nothing is recorded twice and the logs never branch. A recorded call's <code>invert</code> does nothing, since the mix restores rather than undoing; so the mix moves its tape back only with <code>goto</code>, forward one <code>redo</code> at a time, and opens no journals.</li>
|
|
903
904
|
<li><b>How sure.</b> <code>assess</code> says per channel <code>held</code> where a voice feeding it reads input from outside the clock with no recording of it (a signal marked <code>input</code>, a patch that <code>reads</code> host fields, or a <code>from: 'current'</code> start read back with nothing kept), <code>stepped</code> where state crossed a gap in one go without <code>stepMs</code>, or a read back, or a seek back the record goes on from, stepped a stateful signal on from a copy, and <code>exact</code> otherwise; a read ahead reads a stateful signal at every known event, which makes it <code>exact</code>. The least sure voice decides. Under <code>stepMs</code> a read back lands on the pose the mix showed, so a scrub back and forth gives the same picture. A control change the host makes between a sync and the frame's first probe or pull showed in that frame, so a read back shows it there; one made after shows from the next frame.</li>
|
|
904
|
-
<li><b>A voice's own seek.</b> <code>handle.seek(elapsed)</code> moves one voice's clock, which leaves the state its patch steps and its signals keep belonging to the old position. By default each subject's state is rebuilt: made fresh and stepped from the voice's start to the new position at its next probe, on the <code>stepMs</code> grid where there is one, so under <code>stepMs</code> it lands where a voice cued fresh and played there would. <code>state: 'keep'</code> leaves it as it was. The seek answers how sure the state now is: <code>exact</code> for a voice with no state, or rebuilt under <code>stepMs</code> with no <code>maxDt</code>; <code>stepped</code> where it was caught up in one step, or a signal's kept state was started over; <code>held</code> where it was kept, or depends on host fields or an input weight a rebuild cannot know. A rebuild does not read <code>history</code> yet: starting from a copy kept near the new position would save the stepping, and could make state fed by recorded input exact. Decided and built 2026-10-08.</li>
|
|
905
|
+
<li><b>A voice's own seek.</b> <code>handle.seek(elapsed)</code> moves one voice's clock, which leaves the state its patch steps and its signals keep belonging to the old position. By default each subject's state is rebuilt: made fresh and stepped from the voice's start to the new position at its next probe, on the <code>stepMs</code> grid where there is one, so under <code>stepMs</code> it lands where a voice cued fresh and played there would. <code>state: 'keep'</code> leaves it as it was, except for a subject sent back before its start, which starts afresh there, since it has run no steps yet. The seek answers how sure the state now is: <code>exact</code> for a voice with no state, or rebuilt under <code>stepMs</code> with no <code>maxDt</code>; <code>stepped</code> where it was caught up in one step, or a signal's kept state was started over; <code>held</code> where it was kept, or depends on host fields or an input weight a rebuild cannot know. A rebuild does not read <code>history</code> yet: starting from a copy kept near the new position would save the stepping, and could make state fed by recorded input exact. Decided and built 2026-10-08.</li>
|
|
905
906
|
<li><b>Cost.</b> A projection copies each subject's record on first probe: about 3 ms for 1000 subjects under three voices, three times a live frame (<code>npm run bench</code>, rows <code>ahead</code> and <code>back</code>). That is cheap enough to make one per frame for a continuous scrub. History costs one control entry per handle change and one copy per stateful subject per <code>every</code>, all dropped past the horizon. A seek back puts each stateful record back from the last copy at or before the moment sought, at the subject's next probe, and steps it on from there. A seek lets go of the copies made after the moment it sought, so a mix sought back before every frame would step each subject from the same copy, further each time. The copy a record was put back from therefore keeps its state stepped ahead to near the moment sought, a state per copy a seek has used, moved on once that moment is <code>every</code> past it, and the next record put back from the copy steps on from that state where the steps due run past it. It does so only where the same steps give the same state: under <code>stepMs</code>, with no <code>maxDt</code>, for a patch that <code>reads</code> no host field.</li>
|
|
906
907
|
</ul>
|
|
907
908
|
|
|
@@ -1059,13 +1060,11 @@ export const mixer: Engine; <span class="cm">// the one that ships</span></cod
|
|
|
1059
1060
|
<h2 id="open">Open</h2>
|
|
1060
1061
|
<ul>
|
|
1061
1062
|
<li><b>The shared channel list.</b> Channels a second package might write onto another's subject, fixed with a kind, a unit and a rest so that hosts and packages agree past what <code>kind</code> checks. Candidates, names unpicked: <code>opacity</code> mul 0..1; <code>scale</code> mul factor; <code>position</code> vec(3, sum) world units; <code>rotation</code> vec(3, sum) radians; <code>bump</code> vec(2, sum) CSS px, picked on 2026-09-30 over <code>offset</code>, which blits already split into delta and pose; <code>turn</code> sum radians, the flat rotation, counting past 2π so a wheel can spin many times, which is why it blends in a straight line — an orientation that wraps, where 350° and 10° must meet at 0°, is <code>angle</code>, whose <code>lerp</code> goes the short way round; <code>color</code> as <code>color()</code>, OKLab with coverage. Undecided too: whether wod's radial push takes a shared name, and whether sherpa's <code>clip</code> and wod's <code>aperture</code> are one shared <code>reveal</code>.</li>
|
|
1062
|
-
<li><b>What the score still lacks.</b> The score is built (see Score). Decided 2026-09-29 and not built: <b>tags absorb loci</b>: a tag declared to fold its voices as alternatives does what <code>locus</code> does now; where the declaration lives is not decided.</li>
|
|
1063
1063
|
<li><b>What spans do not do yet.</b> Hold back part of a budget for children the host knows are coming, short of a strategy of its own; skip a span held by a span; send the events a skipped child would have sent on the way. Found while building them, 2026-10-07.</li>
|
|
1064
1064
|
<li><b>The package name.</b> Published as <code>@msb235/blits</code> for now, decided 2026-09-30, while npm answers a request for the bare <code>blits</code>, which the registry 404s. Moving to <code>blits</code> later changes every import. <code>@lightningjs/blits</code> is a live TV app framework with its own animation plugin, so searches for the name land there first.</li>
|
|
1065
1065
|
<li><b>Whether <code>at</code> writes into a buffer: no, it returns.</b> A patch allocates one partial delta per subject per frame, and <code>writes</code> plus the fold order would let the mix hand it one scratch object instead. The port measured what that would buy: a frame of klieg’s motion — 12 letters through four voices — costs 0.017 ms, and a frame of its effects — 60 parts under three effects — costs 0.134 ms, against a 16.7 ms budget. Under 1% of a frame is not worth the nicest property <code>at</code> has, which is being a pure function a test calls once. The p99 of the effects frame is 1.3 ms, which is the collector catching up on exactly these allocations, so a subject count an order of magnitude past klieg’s would reopen it. <code>packages/core/test/bench/frame-cost.bench.ts</code> in klieg is the measurement</li>
|
|
1066
1066
|
<li><b>A subject brought back, read back to while it was out.</b> Under <code>history</code> a voice keeps the record a subject left with, faded out or dropped, and a motion patch keeps its stretches, so a <code>seek</code> or <code>project</code> back to before it left finds both (built 2026-10-07). Untested: a read back to a moment between the subject leaving and a motion's <code>to</code> bringing it back, which may read it as never having left. Found 2026-10-02 while building subject fades.</li>
|
|
1067
1067
|
<li><b>Layered subjects.</b> A lamp that is part of a cell and also reached on its own, as a sparkle on one bulb of a chase, is its own subject and folds every voice of the cell again. Starting its fold from the cell's pose and adding only the voices that reach the lamp would save that, and only the mix knows which voices reach what. Unbuilt until a host needs it; the saving matters at thousands of subjects (<code>named N=10000 V=1</code> is 1.592 ms a frame on teitou, 2026-10-21). Raised 2026-10-22.</li>
|
|
1068
|
-
<li><b>The stock band's width.</b> Whether <code>gate</code>'s default band is symmetric around <code>at</code> and how wide it is. It has to clear the frame noise in a real signal, so it is a measurement from klieg's <code>near</code> and <code>dwell</code>, not a guess.</li>
|
|
1069
1068
|
</ul>
|
|
1070
1069
|
</main>
|
|
1071
1070
|
</div>
|
package/package.json
CHANGED