@msb235/blits 0.15.0 → 0.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +44 -0
- package/README.md +1 -1
- package/dist/bare.js +22 -7
- package/dist/columns.d.ts +2 -1
- package/dist/columns.js +55 -35
- package/dist/crowd.d.ts +8 -4
- package/dist/crowd.js +17 -7
- package/dist/delay.d.ts +3 -0
- package/dist/delay.js +7 -0
- package/dist/due.js +1 -1
- package/dist/easing.d.ts +2 -0
- package/dist/easing.js +4 -0
- package/dist/everyone.d.ts +5 -0
- package/dist/everyone.js +30 -0
- package/dist/fill.js +1 -1
- package/dist/held.js +2 -1
- package/dist/index.d.ts +1 -1
- package/dist/lane.d.ts +2 -5
- package/dist/lane.js +14 -10
- package/dist/lanes.d.ts +7 -4
- package/dist/lanes.js +27 -28
- package/dist/meet.js +5 -3
- package/dist/mixer.d.ts +2 -1
- package/dist/mixer.js +4 -0
- package/dist/named.d.ts +6 -1
- package/dist/named.js +29 -1
- package/dist/numbers.d.ts +5 -0
- package/dist/numbers.js +29 -0
- package/dist/pull.js +11 -1
- package/dist/qualify.js +9 -10
- package/dist/renumber.d.ts +10 -0
- package/dist/renumber.js +75 -0
- package/dist/rows.js +27 -19
- package/dist/seat.d.ts +17 -0
- package/dist/seat.js +25 -0
- package/dist/shared.d.ts +1 -1
- package/dist/shared.js +1 -1
- package/dist/strays.js +3 -2
- package/dist/trace.d.ts +4 -0
- package/dist/trace.js +255 -0
- package/dist/types/mix.d.ts +15 -0
- package/dist/types/trace.d.ts +51 -0
- package/dist/types/trace.js +1 -0
- package/dist/types/voice.d.ts +10 -5
- package/dist/types.d.ts +1 -0
- package/dist/unreached.d.ts +2 -0
- package/dist/unreached.js +7 -0
- package/dist/voice.d.ts +3 -0
- package/dist/voice.js +9 -3
- package/docs/schema.html +11 -0
- package/package.json +1 -1
package/dist/strays.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { delayOf } from './delay.js';
|
|
1
2
|
import { unpart } from './fade.js';
|
|
2
3
|
import { pageOut } from './paging.js';
|
|
3
4
|
import { record, taping, unplayable } from './tape.js';
|
|
@@ -75,7 +76,7 @@ export function motionOwner(mix) {
|
|
|
75
76
|
return Number.NaN;
|
|
76
77
|
unpart(m, v, subject);
|
|
77
78
|
// Its record is gone until it is next read, so its stagger is asked here as it will be there.
|
|
78
|
-
return timeIn(m, v, v
|
|
79
|
+
return timeIn(m, v, delayOf(v, subject));
|
|
79
80
|
},
|
|
80
81
|
};
|
|
81
82
|
}
|
|
@@ -107,7 +108,7 @@ function changeAhead(to, id, motion, given, change) {
|
|
|
107
108
|
unpart(to, v, subject);
|
|
108
109
|
if (at === undefined) {
|
|
109
110
|
const frame = frameOf(to, v, subject);
|
|
110
|
-
const back = parted ? timeIn(to, v, v
|
|
111
|
+
const back = parted ? timeIn(to, v, delayOf(v, subject)) : frame;
|
|
111
112
|
if (!Number.isNaN(frame))
|
|
112
113
|
at = frame;
|
|
113
114
|
else if (!Number.isNaN(back))
|
package/dist/trace.d.ts
ADDED
package/dist/trace.js
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { straight } from './easing.js';
|
|
2
|
+
/**
|
|
3
|
+
* How far inside a sampled stretch its ends are read, ms, but for the window's own: a stretch ends
|
|
4
|
+
* where a voice may jump, and its end is read on its own side of the jump.
|
|
5
|
+
*/
|
|
6
|
+
const NUDGE = 1e-6;
|
|
7
|
+
/** Two values at one time this close are one point, not a jump. */
|
|
8
|
+
const close = (a, b) => Math.abs(a - b) <= 1e-6 * Math.max(1, Math.abs(a), Math.abs(b));
|
|
9
|
+
/** `mix.trace`: see `Mix.trace`. */
|
|
10
|
+
export function traceMix(mix, from, to, opts = {}) {
|
|
11
|
+
if (!(from <= to) || !Number.isFinite(from) || !Number.isFinite(to))
|
|
12
|
+
throw new RangeError(`blits: a trace runs from a time to one no earlier, not ${from} to ${to}`);
|
|
13
|
+
const grid = opts.grid ?? 5;
|
|
14
|
+
if (!(grid > 0 && grid < Number.POSITIVE_INFINITY))
|
|
15
|
+
throw new RangeError(`blits: a trace's grid is a finite number of ms above 0, not ${grid}`);
|
|
16
|
+
const known = knowable(mix, from);
|
|
17
|
+
const projections = new Map();
|
|
18
|
+
const poses = new Map();
|
|
19
|
+
const plans = new Map();
|
|
20
|
+
const poseAt = (subject, t) => {
|
|
21
|
+
let mine = poses.get(subject);
|
|
22
|
+
if (mine === undefined) {
|
|
23
|
+
mine = new Map();
|
|
24
|
+
poses.set(subject, mine);
|
|
25
|
+
}
|
|
26
|
+
let pose = mine.get(t);
|
|
27
|
+
if (pose === undefined) {
|
|
28
|
+
let p = projections.get(t);
|
|
29
|
+
if (p === undefined) {
|
|
30
|
+
p = mix.project(t);
|
|
31
|
+
projections.set(t, p);
|
|
32
|
+
}
|
|
33
|
+
pose = p.probe(subject, mix.pose());
|
|
34
|
+
mine.set(t, pose);
|
|
35
|
+
}
|
|
36
|
+
return pose;
|
|
37
|
+
};
|
|
38
|
+
return {
|
|
39
|
+
from,
|
|
40
|
+
to,
|
|
41
|
+
of(subject, channel) {
|
|
42
|
+
const name = channel;
|
|
43
|
+
const slot = mix.slotOf.get(name);
|
|
44
|
+
if (slot === undefined)
|
|
45
|
+
throw new Error(`blits: the mix has no channel ${name}`);
|
|
46
|
+
const value = (t) => {
|
|
47
|
+
const v = poseAt(subject, t)[name];
|
|
48
|
+
return typeof v === 'number' ? v : Number.NaN;
|
|
49
|
+
};
|
|
50
|
+
let plan = plans.get(subject);
|
|
51
|
+
if (plan === undefined) {
|
|
52
|
+
plan = planOf(mix, subject, from, to);
|
|
53
|
+
plans.set(subject, plan);
|
|
54
|
+
}
|
|
55
|
+
const op = mix.scalars[slot];
|
|
56
|
+
const shaped = known &&
|
|
57
|
+
!plan.unknown.has(slot) &&
|
|
58
|
+
!mix.bounded.includes(slot) &&
|
|
59
|
+
(op === 'sum' || (op === 'mul' && (plan.writers.get(slot) ?? 0) <= 1));
|
|
60
|
+
const bent = plan.bent.get(slot);
|
|
61
|
+
const straightOver = (a, b) => shaped && (bent === undefined || bent.every(([lo, hi]) => !(lo < b && hi > a)));
|
|
62
|
+
if (from === to)
|
|
63
|
+
return { at: [from], value: [value(from)], exact: shaped };
|
|
64
|
+
const at = [];
|
|
65
|
+
const values = [];
|
|
66
|
+
let exact = true;
|
|
67
|
+
const add = (t, v) => {
|
|
68
|
+
const n = at.length;
|
|
69
|
+
if (n > 0 && at[n - 1] === t && close(values[n - 1], v))
|
|
70
|
+
values[n - 1] = v;
|
|
71
|
+
else {
|
|
72
|
+
at.push(t);
|
|
73
|
+
values.push(v);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
const knots = [from, ...(plan.knots.get(slot) ?? []), to];
|
|
77
|
+
for (let i = 0; i + 1 < knots.length; i++) {
|
|
78
|
+
const a = knots[i];
|
|
79
|
+
const b = knots[i + 1];
|
|
80
|
+
if (straightOver(a, b)) {
|
|
81
|
+
// Read inside, so a jump at either end is not read; the line reaches each end exactly.
|
|
82
|
+
const t1 = a + (b - a) / 3;
|
|
83
|
+
const t2 = b - (b - a) / 3;
|
|
84
|
+
const v1 = value(t1);
|
|
85
|
+
const slope = (value(t2) - v1) / (t2 - t1);
|
|
86
|
+
add(a, v1 - slope * (t1 - a));
|
|
87
|
+
add(b, v1 + slope * (b - t1));
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
exact = false;
|
|
91
|
+
const e = Math.min(NUDGE, (b - a) / 4);
|
|
92
|
+
add(a, value(a === from ? a : a + e));
|
|
93
|
+
for (let k = Math.max(0, Math.ceil((a - from) / grid));; k++) {
|
|
94
|
+
const g = from + k * grid;
|
|
95
|
+
if (g >= b - e)
|
|
96
|
+
break;
|
|
97
|
+
if (g > a + e)
|
|
98
|
+
add(g, value(g));
|
|
99
|
+
}
|
|
100
|
+
add(b, value(b === to ? b : b - e));
|
|
101
|
+
}
|
|
102
|
+
return { at, value: values, exact };
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Whether the mix's own state lets a trace know any shape: a window at or after the mix's time,
|
|
108
|
+
* with no recorded call ahead.
|
|
109
|
+
*/
|
|
110
|
+
function knowable(mix, from) {
|
|
111
|
+
const transport = mix.transport;
|
|
112
|
+
if (!(from >= mix.now))
|
|
113
|
+
return false;
|
|
114
|
+
const tape = transport.tape;
|
|
115
|
+
return tape === undefined || tape.timestampAt(tape.undoDepth()) === undefined;
|
|
116
|
+
}
|
|
117
|
+
function planOf(mix, subject, from, to) {
|
|
118
|
+
const plan = { knots: new Map(), bent: new Map(), unknown: new Set(), writers: new Map() };
|
|
119
|
+
for (const voice of mix.cued) {
|
|
120
|
+
if (voice.state === 'done' || !mix.aims(voice, subject))
|
|
121
|
+
continue;
|
|
122
|
+
if (shapeOf(mix, voice, subject, from, to, plan))
|
|
123
|
+
continue;
|
|
124
|
+
for (const slot of voice.slots)
|
|
125
|
+
plan.unknown.add(slot);
|
|
126
|
+
}
|
|
127
|
+
for (const [slot, knots] of plan.knots)
|
|
128
|
+
plan.knots.set(slot, knots.sort((a, b) => a - b).filter((t, i, all) => i === 0 || t !== all[i - 1]));
|
|
129
|
+
return plan;
|
|
130
|
+
}
|
|
131
|
+
/** The most breakpoints one voice may add to a window before it is read on the grid instead. */
|
|
132
|
+
const MOST = 4096;
|
|
133
|
+
/**
|
|
134
|
+
* Adds a voice's breakpoints for one subject to `plan`, and returns true, where its shape is known
|
|
135
|
+
* through the window: a `keys` voice on its own clock at a steady rate and a number weight. Returns
|
|
136
|
+
* false, adding nothing, for any other.
|
|
137
|
+
*/
|
|
138
|
+
function shapeOf(mix, voice, subject, from, to, plan) {
|
|
139
|
+
const spec = voice.spec;
|
|
140
|
+
const built = voice.built;
|
|
141
|
+
const patch = voice.patch;
|
|
142
|
+
if (patch.form !== 'keys' || built === null || patch.step !== undefined)
|
|
143
|
+
return false;
|
|
144
|
+
if (typeof spec.weight === 'function' || voice.owner !== null || voice.holding !== null)
|
|
145
|
+
return false;
|
|
146
|
+
if (voice.fitting !== null || voice.arc !== null || voice.blend !== null)
|
|
147
|
+
return false;
|
|
148
|
+
if (spec.anchor !== undefined || spec.role !== undefined || spec.within !== undefined)
|
|
149
|
+
return false;
|
|
150
|
+
if (spec.stepMs !== undefined || spec.from !== undefined)
|
|
151
|
+
return false;
|
|
152
|
+
if (voice.ramp !== null || !(voice.rate > 0) || voice.out !== null || voice.back !== null)
|
|
153
|
+
return false;
|
|
154
|
+
if (voice.parts !== null || voice.parted !== null || voice.left !== null)
|
|
155
|
+
return false;
|
|
156
|
+
if (Number.isFinite(voice.outAt) || voice.state === 'fading')
|
|
157
|
+
return false;
|
|
158
|
+
// Under a mix rate, a waiting voice's start is pinned to the host's clock, where a ramp moves it.
|
|
159
|
+
if (voice.state === 'pending' && mix.transport.pace !== null)
|
|
160
|
+
return false;
|
|
161
|
+
const knots = [];
|
|
162
|
+
const bent = [];
|
|
163
|
+
const bentBy = new Map();
|
|
164
|
+
const at = (t) => {
|
|
165
|
+
if (t > from && t < to)
|
|
166
|
+
knots.push(t);
|
|
167
|
+
};
|
|
168
|
+
const reduced = mix.reduced;
|
|
169
|
+
const held = voice.sharing ? undefined : voice.subjects.get(subject);
|
|
170
|
+
const delay = held?.delay ?? (spec.stagger ? spec.stagger(subject) : 0);
|
|
171
|
+
const time = (raw) => voice.timeAt(raw + delay);
|
|
172
|
+
const fadeIn = reduced ? 0 : (voice.fade.in ?? 0);
|
|
173
|
+
if (fadeIn > 0) {
|
|
174
|
+
// Where its fade in counts from is known only once the subject has met the voice.
|
|
175
|
+
if (held === undefined)
|
|
176
|
+
return false;
|
|
177
|
+
if (held.shown + fadeIn > from) {
|
|
178
|
+
bent.push([held.shown, held.shown + fadeIn]);
|
|
179
|
+
at(held.shown);
|
|
180
|
+
at(held.shown + fadeIn);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const duration = voice.duration;
|
|
184
|
+
const span = voice.span;
|
|
185
|
+
at(voice.start);
|
|
186
|
+
at(time(0));
|
|
187
|
+
if (Number.isFinite(span))
|
|
188
|
+
at(time(span));
|
|
189
|
+
const ends = voice.endsAt();
|
|
190
|
+
at(ends);
|
|
191
|
+
if (!voice.freezesAfter && Number.isFinite(ends)) {
|
|
192
|
+
const over = reduced ? 0 : Number.isNaN(voice.outOver) ? (voice.fade.out ?? 0) : voice.outOver;
|
|
193
|
+
if (over > 0) {
|
|
194
|
+
bent.push([ends, ends + over]);
|
|
195
|
+
at(ends + over);
|
|
196
|
+
}
|
|
197
|
+
// It leaves when its latest subject ends, which a subject first met later can move.
|
|
198
|
+
if (spec.stagger !== undefined)
|
|
199
|
+
bent.push([time(span), Number.POSITIVE_INFINITY]);
|
|
200
|
+
}
|
|
201
|
+
if (duration > 0) {
|
|
202
|
+
const first = Math.max(0, Math.floor((voice.elapsedAt(from) - delay) / duration));
|
|
203
|
+
const last = Math.min(Math.ceil(voice.passes) - 1, Math.floor((voice.elapsedAt(to) - delay) / duration));
|
|
204
|
+
let stops = 0;
|
|
205
|
+
for (const track of built.tracks)
|
|
206
|
+
stops += track.all.length + 1;
|
|
207
|
+
if ((last - first + 1) * stops > MOST)
|
|
208
|
+
return false;
|
|
209
|
+
for (let pass = first; pass <= last; pass++) {
|
|
210
|
+
const base = pass * duration;
|
|
211
|
+
at(time(base));
|
|
212
|
+
at(time(base + duration));
|
|
213
|
+
for (const track of built.tracks) {
|
|
214
|
+
const slot = mix.slotOf.get(track.channel);
|
|
215
|
+
if (track.numbers === null)
|
|
216
|
+
return false;
|
|
217
|
+
const wait = track.delay;
|
|
218
|
+
if (wait >= duration)
|
|
219
|
+
continue;
|
|
220
|
+
if (wait > 0)
|
|
221
|
+
at(time(base + wait));
|
|
222
|
+
let before = Number.NaN;
|
|
223
|
+
for (const pt of track.all) {
|
|
224
|
+
const t = time(base + wait + Math.min(1, Math.max(0, pt.at)) * (duration - wait));
|
|
225
|
+
at(t);
|
|
226
|
+
if (!Number.isNaN(before) && !straight(pt.ease)) {
|
|
227
|
+
const list = bentBy.get(slot);
|
|
228
|
+
if (list === undefined)
|
|
229
|
+
bentBy.set(slot, [[before, t]]);
|
|
230
|
+
else
|
|
231
|
+
list.push([before, t]);
|
|
232
|
+
}
|
|
233
|
+
before = t;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
for (const slot of voice.slots) {
|
|
239
|
+
const times = plan.knots.get(slot);
|
|
240
|
+
if (times === undefined)
|
|
241
|
+
plan.knots.set(slot, [...knots]);
|
|
242
|
+
else
|
|
243
|
+
times.push(...knots);
|
|
244
|
+
plan.writers.set(slot, (plan.writers.get(slot) ?? 0) + 1);
|
|
245
|
+
const mine = [...bent, ...(bentBy.get(slot) ?? [])];
|
|
246
|
+
if (mine.length === 0)
|
|
247
|
+
continue;
|
|
248
|
+
const list = plan.bent.get(slot);
|
|
249
|
+
if (list === undefined)
|
|
250
|
+
plan.bent.set(slot, mine);
|
|
251
|
+
else
|
|
252
|
+
list.push(...mine);
|
|
253
|
+
}
|
|
254
|
+
return true;
|
|
255
|
+
}
|
package/dist/types/mix.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { Kit } from './channel.js';
|
|
|
3
3
|
import type { HistoryOptions, Tape } from './history.js';
|
|
4
4
|
import type { Band, Easing, Patch, Sent, Signal } from './patch.js';
|
|
5
5
|
import type { Booker, BookOptions, Marked } from './place.js';
|
|
6
|
+
import type { Trace, TraceOptions } from './trace.js';
|
|
6
7
|
import type { Transport } from './transport.js';
|
|
7
8
|
import type { BlendMember, Circle, Handle, OwnerSpec, SpanHandle, SpanSpec, VoiceSpec } from './voice.js';
|
|
8
9
|
/**
|
|
@@ -250,6 +251,20 @@ export interface Mix<I, O, H = unknown> {
|
|
|
250
251
|
project(at: number | {
|
|
251
252
|
readonly frame: number;
|
|
252
253
|
}): Projection<I, O>;
|
|
254
|
+
/**
|
|
255
|
+
* Reads the mix over a window of mix time, `from` to `to`, without moving it, as `project` reads
|
|
256
|
+
* one moment: `trace.of(subject, channel)` gives that channel as points joined by straight lines.
|
|
257
|
+
* Where every voice feeding the channel is a `keys` voice whose shape is known through the
|
|
258
|
+
* window, the points are its own breakpoints and the lines are exact; elsewhere the run is read
|
|
259
|
+
* every `grid` ms, as many `project` calls would read it.
|
|
260
|
+
*
|
|
261
|
+
* Exact needs, of each voice reaching the subject and writing the channel: a `keys` patch whose
|
|
262
|
+
* stops are plain numbers, eased `linear`; a number weight; no owner, anchor, role, blend,
|
|
263
|
+
* `within`, `stepMs`, `from: 'current'`, or rate ramp; a fade in finished by `from` and no fade
|
|
264
|
+
* out under way or due. The channel folds by `sum`, or by `mul` from one such voice, and has no
|
|
265
|
+
* `bounds`. The window lies at or after the mix's own time, and the tape holds no calls ahead.
|
|
266
|
+
*/
|
|
267
|
+
trace(from: number, to: number, opts?: TraceOptions): Trace<I, O>;
|
|
253
268
|
/**
|
|
254
269
|
* Moves the mix to mix time `time` and plays on from there. It lands as the mix stood at the end
|
|
255
270
|
* of that frame; where rate 0 held the clock at `time` for several frames, the last of them.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What `mix.trace` takes besides its window.
|
|
3
|
+
*
|
|
4
|
+
* @category mix
|
|
5
|
+
*/
|
|
6
|
+
export interface TraceOptions {
|
|
7
|
+
/**
|
|
8
|
+
* Where a run cannot be known exactly, how far apart it is read, in ms of mix time. Default 5.
|
|
9
|
+
*/
|
|
10
|
+
grid?: number;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* One channel of one subject over a trace's window, as points joined by straight lines: the value
|
|
14
|
+
* between two points is the straight line between them. Two points at one time are a jump, from
|
|
15
|
+
* the first value to the second. The first point is at the window's start and the last at its end.
|
|
16
|
+
*
|
|
17
|
+
* @category mix
|
|
18
|
+
*/
|
|
19
|
+
export interface Run {
|
|
20
|
+
/** Mix times, ascending; a time given twice is a jump. */
|
|
21
|
+
readonly at: readonly number[];
|
|
22
|
+
/** The channel's value at each of `at`; NaN where it holds no number. */
|
|
23
|
+
readonly value: readonly number[];
|
|
24
|
+
/**
|
|
25
|
+
* Whether the lines are the mix's own values everywhere in the window, not only at the points.
|
|
26
|
+
* Where they are not, the points are each `grid` apart and the lines between them approximate.
|
|
27
|
+
*/
|
|
28
|
+
readonly exact: boolean;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The channels of `O` that hold one number.
|
|
32
|
+
*
|
|
33
|
+
* @category mix
|
|
34
|
+
*/
|
|
35
|
+
export type NumberChannel<O> = {
|
|
36
|
+
[K in keyof O]-?: NonNullable<O[K]> extends number ? K : never;
|
|
37
|
+
}[keyof O];
|
|
38
|
+
/**
|
|
39
|
+
* The mix read over a window of mix time, with nothing in the live mix moved. Valid until the mix
|
|
40
|
+
* is next synced or cued.
|
|
41
|
+
*
|
|
42
|
+
* @category mix
|
|
43
|
+
*/
|
|
44
|
+
export interface Trace<I, O> {
|
|
45
|
+
/** The window's start, in mix time. */
|
|
46
|
+
readonly from: number;
|
|
47
|
+
/** The window's end, in mix time. */
|
|
48
|
+
readonly to: number;
|
|
49
|
+
/** One subject's channel over the window. */
|
|
50
|
+
of(subject: I, channel: NumberChannel<O>): Run;
|
|
51
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/types/voice.d.ts
CHANGED
|
@@ -135,13 +135,16 @@ export interface VoiceSpec<I, O, H = unknown> extends SpanHints {
|
|
|
135
135
|
/**
|
|
136
136
|
* How many passes the voice plays. true loops for good, false plays one pass, n plays n passes.
|
|
137
137
|
* A finite loop leaves when its
|
|
138
|
-
* passes are done for
|
|
139
|
-
*
|
|
138
|
+
* passes are done for its latest-staggered subject — over `fade.out` where one is set, at once
|
|
139
|
+
* where none is — unless it freezes after. Default: false.
|
|
140
140
|
*/
|
|
141
141
|
loop?: boolean | number;
|
|
142
142
|
/**
|
|
143
143
|
* Per-subject delay in voice ms. It is klieg's stagger grammar reduced to the one thing the mix
|
|
144
|
-
* needs.
|
|
144
|
+
* needs. It runs once per subject and the answer is kept: at `cue` for every subject a voice
|
|
145
|
+
* names in `subjects`, else, like `target`, on first sight. So a voice naming its subjects ends by
|
|
146
|
+
* all of them, and one reaching them by `target` ends by those the mix has been asked about: a
|
|
147
|
+
* subject first read later than that sees a finite loop already over.
|
|
145
148
|
*/
|
|
146
149
|
stagger?: (subject: I) => number;
|
|
147
150
|
/**
|
|
@@ -373,7 +376,8 @@ export interface SubjectFade<I = unknown> {
|
|
|
373
376
|
/**
|
|
374
377
|
* The one subject to fade out of this voice, which plays on for the rest. Once the ramp ends
|
|
375
378
|
* the voice forgets the subject and reaches it no more, until a motion patch's `to` or `push`
|
|
376
|
-
* or the mix's `drop` brings it back, met afresh: `from
|
|
379
|
+
* or the mix's `drop` brings it back, met afresh: `from` and `target` asked again, and
|
|
380
|
+
* `stagger` too unless the voice names its `subjects`.
|
|
377
381
|
*/
|
|
378
382
|
subject: I;
|
|
379
383
|
/** The ramp, ms. Defaults to the voice's own `fade.out`; 0 takes the subject out at once. */
|
|
@@ -466,7 +470,8 @@ export interface Handle<I = unknown> {
|
|
|
466
470
|
readonly done: Promise<void>;
|
|
467
471
|
/**
|
|
468
472
|
* Resolves true when a finite loop's last pass ends, false if the voice leaves before that. The
|
|
469
|
-
* last pass is the one for
|
|
473
|
+
* last pass is the one for its latest-staggered subject; `stagger` says which subjects count.
|
|
474
|
+
* Never rejects.
|
|
470
475
|
*/
|
|
471
476
|
readonly played: Promise<boolean>;
|
|
472
477
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export type * from './types/mix.js';
|
|
|
5
5
|
export type * from './types/motion.js';
|
|
6
6
|
export type * from './types/patch.js';
|
|
7
7
|
export type * from './types/place.js';
|
|
8
|
+
export type * from './types/trace.js';
|
|
8
9
|
export type * from './types/transport.js';
|
|
9
10
|
export type * from './types/voice.js';
|
|
10
11
|
export type * from './types/watch.js';
|
package/dist/unreached.d.ts
CHANGED
|
@@ -3,3 +3,5 @@ import type { Voice } from './voice.js';
|
|
|
3
3
|
export declare function unreached<I, O>(voice: Voice<I, O>, slot: number): boolean;
|
|
4
4
|
/** Marks or clears the subject numbered `slot` as one the voice does not reach. */
|
|
5
5
|
export declare function unreach<I, O>(voice: Voice<I, O>, slot: number, on: boolean): void;
|
|
6
|
+
/** The subject numbered `from` is numbered `to` now, a number let go of: its mark moves along. */
|
|
7
|
+
export declare function reunreach<I, O>(voice: Voice<I, O>, from: number, to: number): void;
|
package/dist/unreached.js
CHANGED
|
@@ -22,3 +22,10 @@ export function unreach(voice, slot, on) {
|
|
|
22
22
|
const bit = 1 << (slot & 31);
|
|
23
23
|
bits[word] = on ? bits[word] | bit : bits[word] & ~bit;
|
|
24
24
|
}
|
|
25
|
+
/** The subject numbered `from` is numbered `to` now, a number let go of: its mark moves along. */
|
|
26
|
+
export function reunreach(voice, from, to) {
|
|
27
|
+
if (!unreached(voice, from))
|
|
28
|
+
return;
|
|
29
|
+
unreach(voice, to, true);
|
|
30
|
+
unreach(voice, from, false);
|
|
31
|
+
}
|
package/dist/voice.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { Named } from './named.js';
|
|
|
9
9
|
import type { Past } from './origin.js';
|
|
10
10
|
import { Holding } from './owner.js';
|
|
11
11
|
import { type Built, intosOf, type Scratch } from './patch.js';
|
|
12
|
+
import type { Seat } from './seat.js';
|
|
12
13
|
import { VoiceSetting } from './setting.js';
|
|
13
14
|
import type { Fitting } from './spans.js';
|
|
14
15
|
import { Store } from './store.js';
|
|
@@ -101,6 +102,8 @@ export declare class Voice<I, O> {
|
|
|
101
102
|
holder: Subject<unknown> | null;
|
|
102
103
|
/** Whether its channels run as lanes this frame, so the general fold passes it by. */
|
|
103
104
|
laned: boolean;
|
|
105
|
+
/** Its crowd row, while its channels run in a crowd. */
|
|
106
|
+
seat: Seat<I, O> | null;
|
|
104
107
|
/**
|
|
105
108
|
* How many times it has been sought or touched, so a delta read before either is not handed out
|
|
106
109
|
* after it.
|
package/dist/voice.js
CHANGED
|
@@ -72,6 +72,8 @@ export class Voice {
|
|
|
72
72
|
holder = null;
|
|
73
73
|
/** Whether its channels run as lanes this frame, so the general fold passes it by. */
|
|
74
74
|
laned = false;
|
|
75
|
+
/** Its crowd row, while its channels run in a crowd. */
|
|
76
|
+
seat = null;
|
|
75
77
|
/**
|
|
76
78
|
* How many times it has been sought or touched, so a delta read before either is not handed out
|
|
77
79
|
* after it.
|
|
@@ -358,8 +360,8 @@ export class Voice {
|
|
|
358
360
|
this.dueToken++;
|
|
359
361
|
this.early = null;
|
|
360
362
|
this.seen = 0;
|
|
361
|
-
this.latest = 0;
|
|
362
|
-
this.soonest = 0;
|
|
363
|
+
this.latest = this.named?.latest ?? 0;
|
|
364
|
+
this.soonest = this.named?.soonest ?? 0;
|
|
363
365
|
this.restedCount = 0;
|
|
364
366
|
this.cuedAt = Number.NEGATIVE_INFINITY;
|
|
365
367
|
this.cuedSeq = Number.NEGATIVE_INFINITY;
|
|
@@ -393,7 +395,10 @@ export class Voice {
|
|
|
393
395
|
this.holding = patch === ownerPatch ? new Holding() : null;
|
|
394
396
|
const wrote = writing(patch.writes, slotOf, channels);
|
|
395
397
|
this.slots = wrote.slots;
|
|
396
|
-
this.named = spec.subjects ? new Named(spec.subjects) : null;
|
|
398
|
+
this.named = spec.subjects ? new Named(spec.subjects, spec.stagger) : null;
|
|
399
|
+
// Every subject's stagger is known at cue, so a finite loop ends by them all, read or not.
|
|
400
|
+
this.latest = this.named?.latest ?? 0;
|
|
401
|
+
this.soonest = this.named?.soonest ?? 0;
|
|
397
402
|
this.built = patch.form === 'keys' && patch.keys ? builtOf(patch) : null;
|
|
398
403
|
this.lerps = wrote.lerps;
|
|
399
404
|
this.intos = this.built
|
|
@@ -567,6 +572,7 @@ export class Voice {
|
|
|
567
572
|
v.scratch = [];
|
|
568
573
|
v.holder = null;
|
|
569
574
|
v.laned = false;
|
|
575
|
+
v.seat = null;
|
|
570
576
|
v.log = null;
|
|
571
577
|
v.early = null;
|
|
572
578
|
if (controls)
|
package/docs/schema.html
CHANGED
|
@@ -928,6 +928,7 @@ for (const step of steps) mix.arc({ owner: program, active: stage(step), hold: 4
|
|
|
928
928
|
</ul></li>
|
|
929
929
|
<li><b>What comes next.</b> <code>mix.marks(from, to)</code> lists every mark the plan has fixed in a range, a voice's five and those placed inside it, with each voice's id, name and tags. One nothing has fixed is left out.</li>
|
|
930
930
|
<li><b>Reading another time.</b> <code>mix.project(t)</code> copies what each subject holds and reads it at <code>t</code>, sending nothing and committing nothing, so the live mix plays on exactly as if it had not been read. Ahead, it plays what is cued forward, stopping at each known event on the way (see <b>Reading ahead at known events</b> below). Behind, it reads from <code>history</code>: the mix keeps every voice's control changes with when they landed, the voices that have left, and a copy of each stateful record per subject every <code>every</code> ms, and a read back restores the nearest copy at or before <code>t</code> and steps forward. A subject it kept nothing for starts from its voice's start with fresh state. Without <code>history</code> a read back works only where how the mix stands now says what it showed then: every voice plays as it was cued and reads by its clock alone, the mix's rate was never set, and <code>t</code> is no earlier than the last cue, <code>drop</code>, <code>touch</code>, <code>announce</code>, or <code>rebase</code>, or the last frame a voice left in; anywhere else it throws and says why. A patch's own <code>clone</code> copies state that <code>structuredClone</code> cannot. With <code>inputs</code>, history also keeps what each input signal on a voice's weight read per subject, and a copy of the host fields patches <code>reads</code>, each time they changed, and a read back uses those instead of asking the signal or the host. Where anchors fixed a voice's start or out after the time read, it reads back as it stood then.</li>
|
|
931
|
+
<li id="trace"><b>Reading over a window.</b> <code>mix.trace(from, to, { grid })</code> reads the mix from mix time <code>from</code> to <code>to</code>, moving nothing, and <code>trace.of(subject, channel)</code> gives one channel holding a number as a run: times and values, joined by straight lines, with a time given twice for a jump. Where every voice feeding the channel has a shape known through the window, the run's points are its breakpoints and the lines are the mix's own values, and <code>exact</code> says so; elsewhere the run is read every <code>grid</code> ms (default 5), as that many <code>project</code> calls would read it. A known shape is a <code>keys</code> voice with plain number stops eased <code>linear</code>, a number weight, a steady rate, and no owner, anchor, role, blend, <code>within</code>, <code>stepMs</code> or <code>from: 'current'</code>; its fade in finished by <code>from</code>, and no fade out under way or set. The channel folds by <code>sum</code>, or by <code>mul</code> from one such voice, with no <code>bounds</code>. The window starts at or after the mix's own time, and the tape holds no call ahead. Every value still comes from <code>project</code>: the trace only chooses where to read, and which stretches a line may join. One trace shares its reads among the subjects asked about. blits-tone schedules its curves from it.</li>
|
|
931
932
|
<li id="seek"><b>Seeking.</b> <code>mix.seek(t)</code> moves the live mix to mix time <code>t</code>, as it stood at the end of that frame, and it plays on from there. It needs <code>history</code> with a <code>tape</code>, and reaches back as far as a read back does. <code>history.tape</code> takes weasel-history's <code>createHistory</code> from <code>@weasel-js/history</code>, or anything of the <code>Tape</code> shape, and the mix records on it every call the host makes on the mix, its handles and its motion patches: cues, weight, rate, seeks and fades, retargets and pushes, the mix's rate, marks, <code>mute</code> and <code>drop</code>, each at the host time it was made, so the frames rate 0 holds at one mix time stay apart. Where it held the clock at <code>t</code> for several frames, a seek or read back to <code>t</code> lands on the last of them; <code>mix.seq</code> numbers the frame being played, higher with each sync and seek, and <code>seek({ frame })</code> and <code>project({ frame })</code> take that number to reach an earlier one. A frame history no longer holds, through age or because an earlier seek went back before it, throws, and one a store paged needs <code>prepare</code> of its mix time first. What history keeps is filed by the frame a change was decided in, so a fade given a time already passed, or a handle seek past a voice's end, comes back with a seek to before the call even though its time is earlier. The tape lives as long as the mix and is never saved.
|
|
932
933
|
<ul>
|
|
933
934
|
<li><b>Back.</b> The mix restores itself from what history kept: each voice's controls, the voices and marks, and the mix's rate, as they stood at <code>t</code>. Voices that left after it are back on the handles the host holds, with <code>done</code> and <code>played</code> starting over where they had settled since. Voices cued after it wait as <code>pending</code> on theirs until the mix plays their cue again; on a branch that never does, they wait for good. A write to the handle of a voice waiting so changes nothing, and the tape does not record it. A handle’s <code>weightOf</code> reads 0 for a subject until it is probed again, since the weight its last probe read by <code>t</code> is not kept. Each stateful record is put back from the copy kept nearest before <code>t</code> the first time it is touched, and steps once to where it is next probed, exact under <code>stepMs</code>. A subject faded out of a voice or dropped after <code>t</code> comes back with the record it had, a motion patch’s state for it included, which history keeps for it from when it left; the lanes qualify afresh. Recorded input and host fields after <code>t</code> are let go, so signals and host fields read live again, and <code>assess</code> says <code>held</code> for what they feed.</li>
|
|
@@ -1143,6 +1144,16 @@ export const mixer: Engine; <span class="cm">// the one that ships</span></cod
|
|
|
1143
1144
|
<li><b>The owner's calls on blits-tone</b>: its names (<code>drive</code>, <code>attach</code>, <code>detach</code>, <code>write</code>, <code>cancel</code>, and the options <code>ahead</code>, <code>grid</code>, <code>late</code>, <code>clock</code>, <code>take</code>, <code>tag</code>, <code>score</code>); its tests taking <code>node-web-audio-api</code>, a prebuilt native binary of about 40 MB, as a dev dependency so Tone's offline render runs in Node with no audio device; and a subject binding one <code>Target</code> per channel, so a host that wants every subject heard builds a synth per subject and attaches it (all 2026-10-11).</li>
|
|
1144
1145
|
</ul>
|
|
1145
1146
|
|
|
1147
|
+
<h3>Reading over a window</h3>
|
|
1148
|
+
<p>See <a href="#trace">Reading over a window</a>. Each is the owner’s to overturn (2026-10-29).</p>
|
|
1149
|
+
<ul>
|
|
1150
|
+
<li><b>The name <code>trace</code></b>, one syllable beside <code>project</code> and <code>marks</code>, for the read; <code>Trace</code> for what it returns, <code>of</code> for its read of one subject's channel (as <code>TransportProjection.of</code> reads one mix), and <code>Run</code> for the result.</li>
|
|
1151
|
+
<li><b>A run is a polyline</b>: parallel arrays of times and values, straight lines between points, a time given twice for a jump, and one <code>exact</code> for the whole run. Not a list of typed segments (step, linear, exponential, curve): nothing the engine knows exactly is other than straight or a step, and a polyline is what both a Web Audio parameter and a chart take.</li>
|
|
1152
|
+
<li><b>Known shapes come from the voices, values from <code>project</code></b>. The trace reads each straight stretch at its thirds and extends the line to both ends, so a jump at either end is never read across; a stretch it cannot vouch for is read on the grid, its ends a millionth of a ms inside. What counts as known is a whitelist in <code>shapeOf</code> (<code>src/trace.ts</code>); anything not on it is read on the grid, so a feature added later is sampled until someone vouches for it.</li>
|
|
1153
|
+
<li><b>Exact is per channel, not per stretch</b>: one eased segment makes the whole run inexact, though its straight stretches still come as their breakpoints.</li>
|
|
1154
|
+
<li><b>Windows behind the mix are read on the grid</b>, since the voices' controls then may differ from now.</li>
|
|
1155
|
+
</ul>
|
|
1156
|
+
|
|
1146
1157
|
<h3>Shipped as is</h3>
|
|
1147
1158
|
<p>Two behaviors shipped without a view from the owner, to reopen if a consumer trips on one (2026-10-18):</p>
|
|
1148
1159
|
<ul>
|
package/package.json
CHANGED