@msb235/blits 0.10.0 → 0.12.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.
Files changed (126) hide show
  1. package/CHANGELOG.md +248 -0
  2. package/README.md +15 -4
  3. package/dist/arcs.d.ts +40 -0
  4. package/dist/arcs.js +239 -0
  5. package/dist/blend.js +1 -5
  6. package/dist/book.d.ts +2 -0
  7. package/dist/book.js +27 -3
  8. package/dist/chain.d.ts +17 -1
  9. package/dist/chain.js +57 -15
  10. package/dist/channels.d.ts +2 -0
  11. package/dist/channels.js +12 -1
  12. package/dist/clock.d.ts +14 -0
  13. package/dist/clock.js +9 -1
  14. package/dist/clone.js +16 -4
  15. package/dist/columns.d.ts +4 -5
  16. package/dist/columns.js +11 -5
  17. package/dist/crowd.d.ts +1 -1
  18. package/dist/crowd.js +4 -1
  19. package/dist/cue.js +63 -7
  20. package/dist/departed.d.ts +8 -3
  21. package/dist/departed.js +76 -29
  22. package/dist/due.d.ts +4 -0
  23. package/dist/due.js +16 -3
  24. package/dist/easing.d.ts +12 -1
  25. package/dist/easing.js +7 -1
  26. package/dist/everyone.d.ts +14 -1
  27. package/dist/everyone.js +27 -2
  28. package/dist/fade.d.ts +12 -5
  29. package/dist/fade.js +57 -19
  30. package/dist/fill.js +16 -9
  31. package/dist/fit.d.ts +8 -1
  32. package/dist/fit.js +7 -3
  33. package/dist/fold.d.ts +1 -1
  34. package/dist/fold.js +112 -9
  35. package/dist/frames.d.ts +2 -0
  36. package/dist/frames.js +17 -0
  37. package/dist/gather.js +38 -7
  38. package/dist/handle.d.ts +7 -2
  39. package/dist/handle.js +28 -12
  40. package/dist/held.d.ts +13 -2
  41. package/dist/held.js +43 -9
  42. package/dist/history.d.ts +25 -2
  43. package/dist/history.js +163 -35
  44. package/dist/hosts.js +45 -17
  45. package/dist/index.d.ts +5 -2
  46. package/dist/index.js +3 -1
  47. package/dist/keyfill.js +3 -1
  48. package/dist/lane.d.ts +1 -1
  49. package/dist/lane.js +3 -1
  50. package/dist/lanes.d.ts +31 -7
  51. package/dist/lanes.js +57 -6
  52. package/dist/lean.d.ts +15 -0
  53. package/dist/lean.js +23 -0
  54. package/dist/leavings.d.ts +6 -1
  55. package/dist/leavings.js +16 -2
  56. package/dist/marks.d.ts +7 -3
  57. package/dist/marks.js +105 -27
  58. package/dist/meet.js +1 -0
  59. package/dist/mixer.d.ts +35 -6
  60. package/dist/mixer.js +113 -45
  61. package/dist/motion.d.ts +4 -1
  62. package/dist/motion.js +16 -2
  63. package/dist/motions.d.ts +4 -0
  64. package/dist/motions.js +45 -11
  65. package/dist/move.js +35 -12
  66. package/dist/numbers.d.ts +5 -1
  67. package/dist/numbers.js +4 -0
  68. package/dist/owner.d.ts +18 -2
  69. package/dist/owner.js +44 -3
  70. package/dist/pack.d.ts +1 -0
  71. package/dist/pack.js +3 -0
  72. package/dist/place.d.ts +8 -0
  73. package/dist/place.js +16 -8
  74. package/dist/placed.d.ts +19 -0
  75. package/dist/placed.js +54 -0
  76. package/dist/project.d.ts +5 -2
  77. package/dist/project.js +238 -34
  78. package/dist/pull.js +37 -8
  79. package/dist/qualify.d.ts +10 -0
  80. package/dist/qualify.js +60 -3
  81. package/dist/reading.d.ts +8 -1
  82. package/dist/reading.js +8 -1
  83. package/dist/record.d.ts +6 -0
  84. package/dist/record.js +2 -0
  85. package/dist/restored.d.ts +69 -17
  86. package/dist/restored.js +163 -50
  87. package/dist/revive.d.ts +6 -3
  88. package/dist/revive.js +11 -3
  89. package/dist/roster.js +2 -1
  90. package/dist/rows.js +15 -5
  91. package/dist/seek.d.ts +7 -2
  92. package/dist/seek.js +152 -43
  93. package/dist/shared.d.ts +5 -0
  94. package/dist/shared.js +5 -0
  95. package/dist/signals.d.ts +10 -0
  96. package/dist/signals.js +17 -0
  97. package/dist/spans.d.ts +5 -0
  98. package/dist/spans.js +75 -5
  99. package/dist/store.d.ts +13 -0
  100. package/dist/store.js +23 -0
  101. package/dist/strays.js +50 -2
  102. package/dist/tape.d.ts +29 -4
  103. package/dist/tape.js +49 -11
  104. package/dist/touch.js +3 -2
  105. package/dist/transport.d.ts +11 -2
  106. package/dist/transport.js +26 -10
  107. package/dist/types/arc.d.ts +55 -0
  108. package/dist/types/arc.js +1 -0
  109. package/dist/types/mix.d.ts +57 -14
  110. package/dist/types/patch.d.ts +5 -1
  111. package/dist/types/place.d.ts +11 -4
  112. package/dist/types/transport.d.ts +12 -3
  113. package/dist/types/voice.d.ts +72 -5
  114. package/dist/types/watch.d.ts +126 -0
  115. package/dist/types/watch.js +1 -0
  116. package/dist/types.d.ts +2 -0
  117. package/dist/unpage.d.ts +6 -1
  118. package/dist/unpage.js +34 -2
  119. package/dist/voice.d.ts +93 -16
  120. package/dist/voice.js +60 -3
  121. package/dist/watch.d.ts +26 -0
  122. package/dist/watch.js +441 -0
  123. package/dist/weigh.d.ts +13 -2
  124. package/dist/weigh.js +10 -3
  125. package/docs/schema.html +106 -38
  126. package/package.json +10 -4
package/dist/fold.js CHANGED
@@ -1,12 +1,14 @@
1
- import { passAt, phaseAt, phaseBefore, silent } from './clock.js';
1
+ import { foldNumber } from './channels.js';
2
+ import { passAt, phaseAt, phaseBefore, silent, stepOf } from './clock.js';
2
3
  import { copy as copyValue, first } from './clone.js';
3
4
  import { open as openRecord, shut as shutRecord, weighAt } from './everyone.js';
4
- import { recordHost, remember } from './history.js';
5
+ import { apart, copyState, recordHost, remember } from './history.js';
5
6
  import { stateful } from './hosts.js';
6
7
  import { locusScratch } from './locus.js';
7
8
  import { moved } from './moved.js';
8
9
  import { readKeys } from './patch.js';
9
10
  import { reading } from './reading.js';
11
+ import { checkWrites } from './voice.js';
10
12
  // V8 reads a local const as a constant, where it reads an import from its module on every call.
11
13
  const copy = copyValue;
12
14
  const open = openRecord;
@@ -74,12 +76,18 @@ export function contribution(voice, subject, now, held) {
74
76
  if (early && !voice.holdsFirst)
75
77
  return null;
76
78
  const elapsed = voice.shownAt(raw);
79
+ if (Number.isNaN(elapsed))
80
+ return null;
77
81
  // A frozen subject's clock stands still at the edge it freezes at: -1 before, 1 after, 0 playing.
78
82
  const still = early ? -1 : voice.freezesAfter && raw > voice.span ? 1 : 0;
79
83
  const duration = voice.duration;
80
84
  const phase = elapsed < 0 ? phaseBefore(elapsed, duration) : phaseAt(elapsed, duration, voice.passes);
81
85
  const pass = passAt(elapsed, duration, voice.passes);
82
- this.prime(voice, held, now, elapsed, pass);
86
+ const primed = this.primed;
87
+ primed[0 /* Primed.NOW */] = now;
88
+ primed[1 /* Primed.ELAPSED */] = elapsed;
89
+ primed[2 /* Primed.PASS */] = pass;
90
+ this.prime(voice, held);
83
91
  const setting = voice.setting;
84
92
  // A signal that keeps state makes its voice stateful, as a patch that does: a lane would call
85
93
  // it for subjects no probe asked about.
@@ -99,8 +107,9 @@ export function contribution(voice, subject, now, held) {
99
107
  const history = this.opts.history;
100
108
  if (history?.inputs && voice.patch.reads !== undefined && !this.projecting)
101
109
  recordHost(this, now);
102
- reading.horizon = this.horizonFor(voice, held.delay, now);
103
- const tick = this.opts.stepMs;
110
+ // Only a motion reads it.
111
+ if (voice.motion !== undefined)
112
+ reading.horizon = this.horizonFor(voice, held.delay, now);
104
113
  if (voice.patch.step && held.probed !== now) {
105
114
  // Frozen after, it steps once more to where its last pass ended, and no further; run back
106
115
  // past its start, to where it reached it. Before a start it has yet to reach, it does not step.
@@ -109,12 +118,20 @@ export function contribution(voice, subject, now, held) {
109
118
  : still === 1
110
119
  ? voice.timeAt(held.delay + voice.span)
111
120
  : voice.ranBackTo(held.delay, now);
121
+ // Never probed, it may be a record a seek put back, sharing its copy's state until it steps.
122
+ const snaps = held.snaps;
123
+ const sought = Number.isNaN(held.probed) && snaps !== undefined;
112
124
  if (!Number.isNaN(to)) {
113
125
  if (held.stepped < held.since) {
114
126
  held.stepped = held.since;
115
127
  setting.dt = this.capped(now - held.since);
116
128
  }
117
- if (tick !== undefined && tick > 0 && !this.reducedNow)
129
+ const tick = stepOf(this, voice);
130
+ if (tick !== undefined && sought)
131
+ ahead(this, voice, subject, held, snaps, tick, to);
132
+ if (sought)
133
+ own(voice, held);
134
+ if (tick !== undefined)
118
135
  this.tick(voice, subject, held, tick, to);
119
136
  else if (still === 0 ? now !== held.stepped : to > held.stepped) {
120
137
  if (still !== 0)
@@ -123,6 +140,8 @@ export function contribution(voice, subject, now, held) {
123
140
  held.stepped = to;
124
141
  }
125
142
  }
143
+ else if (sought)
144
+ own(voice, held);
126
145
  }
127
146
  let delta = null;
128
147
  // A keys voice in a locus reads at weight 0 too: having a delta makes it a member, and the
@@ -143,6 +162,8 @@ export function contribution(voice, subject, now, held) {
143
162
  }
144
163
  else {
145
164
  delta = voice.patch.at(phase, subject, setting);
165
+ if (voice.unchecked)
166
+ checkWrites(voice, delta);
146
167
  }
147
168
  held.delta = delta;
148
169
  held.probed = now;
@@ -178,6 +199,69 @@ export function keyed(voice, subject, held) {
178
199
  voice.holder = held;
179
200
  return delta;
180
201
  }
202
+ /** How many whole intervals of `tick` a subject's step grid holds by mix time `to`. */
203
+ function dueBy(held, tick, to) {
204
+ // The epsilon keeps an interval like 1000 / 120 from landing a hair short of a whole count.
205
+ return Math.floor((to - held.since) / tick + 1e-9);
206
+ }
207
+ /**
208
+ * The first step of a record a seek put back from a copy: takes up the furthest state stepped on
209
+ * from that copy that the steps due now run past, the one the record the seek replaced reached or
210
+ * the one the copy was last stepped ahead to, and otherwise keeps the copy's stepped ahead to the
211
+ * moment sought, so a mix sought back time after time does not step from the copy each time, nor a
212
+ * mix sought back and returned step again to where it was. Only where the same steps give the same
213
+ * state: a fixed interval, no `maxDt`, no host field read.
214
+ */
215
+ function ahead(mix, voice, subject, held, snaps, tick, to) {
216
+ const from = snaps[snaps.length - 1];
217
+ const got = from?.ahead;
218
+ if (from === undefined || got === undefined || got.put !== held)
219
+ return;
220
+ got.put = null;
221
+ const reads = voice.patch.reads;
222
+ if (mix.opts.maxDt !== undefined || (reads !== undefined && reads.length > 0))
223
+ return;
224
+ // Stepped, rebuilt or placed anew since it was put back, its steps are no longer the copy's.
225
+ const was = from.held;
226
+ if (held.ticks !== was.ticks || held.since !== got.since || held.rebuilt !== was.rebuilt)
227
+ return;
228
+ const at = got.sought;
229
+ const due = dueBy(held, tick, to);
230
+ const usable = got.state !== undefined && got.ticks <= due;
231
+ if (got.left !== undefined && got.leftTicks <= due && !(usable && got.ticks > got.leftTicks)) {
232
+ // Taken, not copied: the next seek back keeps the state this record steps on to in its place.
233
+ held.state = got.left;
234
+ got.left = undefined;
235
+ voice.setting.state = held.state;
236
+ held.ticks = got.leftTicks;
237
+ return;
238
+ }
239
+ if (usable) {
240
+ held.state = copyState(voice, got.state);
241
+ voice.setting.state = held.state;
242
+ held.ticks = got.ticks;
243
+ }
244
+ const last = usable ? got.at : from.at;
245
+ if (!(at < to) || at - last < apart(mix.opts.history))
246
+ return;
247
+ own(voice, held);
248
+ mix.tick(voice, subject, held, tick, at);
249
+ got.state = copyState(voice, held.state);
250
+ got.ticks = held.ticks;
251
+ got.at = at;
252
+ }
253
+ /**
254
+ * A record a seek put back shares the state of the copy it was made from, which no read changes,
255
+ * until its first probe: before it steps, it takes a copy of its own.
256
+ */
257
+ function own(voice, held) {
258
+ const snaps = held.snaps;
259
+ const from = snaps[snaps.length - 1];
260
+ if (from !== undefined && held.state !== undefined && held.state === from.held.state) {
261
+ held.state = copyState(voice, held.state);
262
+ voice.setting.state = held.state;
263
+ }
264
+ }
181
265
  /**
182
266
  * Runs `step` once per whole interval between where this subject last stopped and now, on a grid
183
267
  * counted from when its delay ran out, and leaves the remainder for the next sample. Counting
@@ -188,8 +272,7 @@ export function tick(voice, subject, held, tick, to) {
188
272
  const setting = voice.setting;
189
273
  const frameDt = setting.dt;
190
274
  let n = held.ticks;
191
- // The epsilon keeps an interval like 1000 / 120 from landing a hair short of a whole count.
192
- const due = Math.floor((to - held.since) / tick + 1e-9);
275
+ const due = dueBy(held, tick, to);
193
276
  const cap = this.opts.maxDt;
194
277
  if (cap !== undefined) {
195
278
  const most = Math.floor(cap / tick);
@@ -291,6 +374,12 @@ export function apply(pose, voice, held, delta, weight) {
291
374
  const value = delta[key];
292
375
  if (value === undefined)
293
376
  continue;
377
+ const op = this.scalars[slot];
378
+ if (op !== undefined && typeof value === 'number') {
379
+ // Called through the channel, `scale` boxes the double it hands `merge`.
380
+ pose[key] = foldNumber(op, pose[key], value, weight);
381
+ continue;
382
+ }
294
383
  const channel = this.channels[slot];
295
384
  if (channel.rest !== undefined && channel.scale) {
296
385
  // pose[key] is the copy of rest this fold made, so it is ours to write into.
@@ -374,8 +463,22 @@ export function folded(subject, out, dry = false, except) {
374
463
  */
375
464
  export function linked(subject) {
376
465
  const head = Number.isNaN(this.now) ? null : this.chain(subject);
466
+ const lanes = this.lanes;
467
+ // What `prepare` answers once this frame's qualify left nothing laned, without calling it.
468
+ if (head !== null &&
469
+ lanes !== null &&
470
+ lanes.laned.length === 0 &&
471
+ lanes.frameAt === this.now &&
472
+ lanes.qualifiedVersion === this.version &&
473
+ lanes.touched.length === 0 &&
474
+ !lanes.filling) {
475
+ if (head.slot >= 0)
476
+ lanes.per[head.slot * 6 /* Per.SLOT */ + 4 /* Per.GENERAL_PROBE */] = ++lanes.probes;
477
+ this.linkedLaned = false;
478
+ return head;
479
+ }
377
480
  this.linkedLaned =
378
- head !== null && this.lanes?.prepare(head.slot, subject, this.version, head) === true;
481
+ head !== null && lanes?.prepare(head.slot, subject, this.version, head) === true;
379
482
  return head;
380
483
  }
381
484
  /** The fold after a subject's chain is linked and its lanes asked whether it reads from them. */
package/dist/frames.d.ts CHANGED
@@ -15,6 +15,8 @@ export declare class Frames {
15
15
  * since rate 0 can hold the clock at `t` for many.
16
16
  */
17
17
  at(t: number): Frame | undefined;
18
+ /** The frame numbered `seq`, if it is still held. */
19
+ numbered(seq: number): Frame | undefined;
18
20
  /** Every frame held, oldest first. */
19
21
  all(): Frame[];
20
22
  set(frames: Frame[]): void;
package/dist/frames.js CHANGED
@@ -48,6 +48,23 @@ export class Frames {
48
48
  }
49
49
  return found;
50
50
  }
51
+ /** The frame numbered `seq`, if it is still held. */
52
+ numbered(seq) {
53
+ const list = this.list;
54
+ let lo = this.head;
55
+ let hi = list.length - 1;
56
+ while (lo <= hi) {
57
+ const mid = (lo + hi) >> 1;
58
+ const f = list[mid];
59
+ if (f.seq === seq)
60
+ return f;
61
+ if (f.seq < seq)
62
+ lo = mid + 1;
63
+ else
64
+ hi = mid - 1;
65
+ }
66
+ return undefined;
67
+ }
51
68
  /** Every frame held, oldest first. */
52
69
  all() {
53
70
  return this.list.slice(this.head);
package/dist/gather.js CHANGED
@@ -154,18 +154,49 @@ export function fold(lane, slot) {
154
154
  */
155
155
  export function foldDelta(chans, slot, delta) {
156
156
  const n = chans.length;
157
+ const arg = this.arg;
158
+ // A plain number is folded where it is read: passed to a `foldInto` V8 did not inline, it is
159
+ // boxed, 16 bytes a channel a subject, and whether V8 inlines it turns on the order its compiles
160
+ // ran (bench/allocs.mjs, 2026-10-20).
157
161
  let ch = chans[0];
158
- if (n > 0)
159
- this.foldInto(ch, slot, delta[ch.name]);
162
+ if (n > 0) {
163
+ const v = delta[ch.name];
164
+ if (typeof v === 'number' && ch.scalar && ch.plain && this.into === null) {
165
+ const values = ch.values;
166
+ values[slot] = foldNumber(ch.op, values[slot], v, arg[0 /* Arg.WEIGHT */]);
167
+ }
168
+ else
169
+ this.foldInto(ch, slot, v);
170
+ }
160
171
  ch = chans[1];
161
- if (n > 1)
162
- this.foldInto(ch, slot, delta[ch.name]);
172
+ if (n > 1) {
173
+ const v = delta[ch.name];
174
+ if (typeof v === 'number' && ch.scalar && ch.plain && this.into === null) {
175
+ const values = ch.values;
176
+ values[slot] = foldNumber(ch.op, values[slot], v, arg[0 /* Arg.WEIGHT */]);
177
+ }
178
+ else
179
+ this.foldInto(ch, slot, v);
180
+ }
163
181
  ch = chans[2];
164
- if (n > 2)
165
- this.foldInto(ch, slot, delta[ch.name]);
182
+ if (n > 2) {
183
+ const v = delta[ch.name];
184
+ if (typeof v === 'number' && ch.scalar && ch.plain && this.into === null) {
185
+ const values = ch.values;
186
+ values[slot] = foldNumber(ch.op, values[slot], v, arg[0 /* Arg.WEIGHT */]);
187
+ }
188
+ else
189
+ this.foldInto(ch, slot, v);
190
+ }
166
191
  for (let i = 3; i < n; i++) {
167
192
  ch = chans[i];
168
- this.foldInto(ch, slot, delta[ch.name]);
193
+ const v = delta[ch.name];
194
+ if (typeof v === 'number' && ch.scalar && ch.plain && this.into === null) {
195
+ const values = ch.values;
196
+ values[slot] = foldNumber(ch.op, values[slot], v, arg[0 /* Arg.WEIGHT */]);
197
+ }
198
+ else
199
+ this.foldInto(ch, slot, v);
169
200
  }
170
201
  }
171
202
  /** Folds a motion sample's axes, kept apart from `foldInto` so neither reads two array kinds. */
package/dist/handle.d.ts CHANGED
@@ -16,9 +16,13 @@ export interface HandleHost<I, O> {
16
16
  rise(voice: Voice<I, O>, opts: {
17
17
  over?: number;
18
18
  } | undefined): void;
19
+ release(voice: Voice<I, O>): void;
19
20
  weightOf(voice: Voice<I, O>, subject: I): number;
20
- /** Records a write the host made, which `again` makes once more where a seek replays it. */
21
- record(label: string, again: () => void): void;
21
+ /**
22
+ * Records a write the host made through `h`, which `again` makes once more where a seek replays
23
+ * it, on the handle it is given: `h`, or a read ahead's handle on its copy of the voice.
24
+ */
25
+ record(h: VoiceHandle<I, O>, label: string, again: (h: VoiceHandle<I, O>) => void): void;
22
26
  }
23
27
  /** Refuses a voice rate no clock can run at. */
24
28
  export declare function finiteRate(rate: number): void;
@@ -60,6 +64,7 @@ export declare class VoiceHandle<I, O> implements Handle<I> {
60
64
  rise(opts?: {
61
65
  over?: number;
62
66
  }): void;
67
+ release(): void;
63
68
  weightOf(subject: I): number;
64
69
  to(subject: I, target: Value, at?: number): void;
65
70
  push(subject: I, velocity: Value, at?: number): void;
package/dist/handle.js CHANGED
@@ -59,17 +59,25 @@ export class VoiceHandle {
59
59
  get weight() {
60
60
  return this.#voice?.weight ?? this.#gone?.weight ?? 0;
61
61
  }
62
- // A write to a paged voice changes nothing: it had left when the host made it, as it has on replay.
62
+ /**
63
+ * The voice a write reaches: none while a history store holds it, or a seek back to before its
64
+ * cue has it waiting. It had left, or was yet to be cued, when the host made the write, as on
65
+ * replay, so the write changes nothing and the tape does not take it.
66
+ */
67
+ get #cued() {
68
+ const voice = this.#voice;
69
+ return voice === null || voice.parked ? null : voice;
70
+ }
63
71
  set weight(w) {
64
72
  plainWeight(w);
65
- const voice = this.#voice;
73
+ const voice = this.#cued;
66
74
  if (voice === null)
67
75
  return;
68
76
  voice.written = true;
69
77
  voice.weight = w;
70
78
  this.#host.changed(voice);
71
- this.#host.record('weight', () => {
72
- this.weight = w;
79
+ this.#host.record(this, 'weight', (h) => {
80
+ h.weight = w;
73
81
  });
74
82
  }
75
83
  get rate() {
@@ -81,7 +89,7 @@ export class VoiceHandle {
81
89
  }
82
90
  ramp(r, over) {
83
91
  finiteRate(r);
84
- const voice = this.#voice;
92
+ const voice = this.#cued;
85
93
  if (voice === null)
86
94
  return;
87
95
  voice.written = true;
@@ -97,10 +105,10 @@ export class VoiceHandle {
97
105
  retime(voice, now, r, over);
98
106
  }
99
107
  this.#host.changed(voice);
100
- this.#host.record('rate', () => this.ramp(r, over));
108
+ this.#host.record(this, 'rate', (h) => h.ramp(r, over));
101
109
  }
102
110
  seek(elapsed, opts) {
103
- const voice = this.#voice;
111
+ const voice = this.#cued;
104
112
  if (voice === null)
105
113
  return 'exact';
106
114
  voice.written = true;
@@ -114,24 +122,32 @@ export class VoiceHandle {
114
122
  voice.seeks++;
115
123
  const doubt = this.#host.sought(voice, opts?.state !== 'keep');
116
124
  this.#host.changed(voice);
117
- this.#host.record('seek', () => this.seek(elapsed, opts));
125
+ this.#host.record(this, 'seek', (h) => h.seek(elapsed, opts));
118
126
  return doubt;
119
127
  }
120
128
  fade(opts) {
121
- const voice = this.#voice;
129
+ const voice = this.#cued;
122
130
  if (voice === null)
123
131
  return;
124
132
  voice.written = true;
125
133
  this.#host.fade(voice, opts);
126
- this.#host.record('fade', () => this.fade(opts));
134
+ this.#host.record(this, 'fade', (h) => h.fade(opts));
127
135
  }
128
136
  rise(opts) {
129
- const voice = this.#voice;
137
+ const voice = this.#cued;
130
138
  if (voice === null)
131
139
  return;
132
140
  voice.written = true;
133
141
  this.#host.rise(voice, opts);
134
- this.#host.record('rise', () => this.rise(opts));
142
+ this.#host.record(this, 'rise', (h) => h.rise(opts));
143
+ }
144
+ release() {
145
+ const voice = this.#cued;
146
+ if (voice === null)
147
+ return;
148
+ voice.written = true;
149
+ this.#host.release(voice);
150
+ this.#host.record(this, 'release', (h) => h.release());
135
151
  }
136
152
  weightOf(subject) {
137
153
  const voice = this.#voice;
package/dist/held.d.ts CHANGED
@@ -2,8 +2,17 @@ import type { Mixer } from './mixer.js';
2
2
  import type { Subject, Voice } from './voice.js';
3
3
  /** The record a voice gives every subject it does not reach. */
4
4
  export declare function unreachedOf<I, O>(this: Mixer<I, O>, voice: Voice<I, O>, now: number): Subject<unknown>;
5
- /** What this voice holds for this subject, made on first sight with `target` and `stagger` asked once. */
6
- export declare function held<I, O>(this: Mixer<I, O>, voice: Voice<I, O>, subject: I, slot: number): Subject<unknown>;
5
+ /**
6
+ * What this voice holds for this subject, made on first sight with `target` and `stagger` asked
7
+ * once; `guess` is a record it may be, from the subject's chain as last linked.
8
+ */
9
+ export declare function held<I, O>(this: Mixer<I, O>, voice: Voice<I, O>, subject: I, slot: number, guess?: Subject<unknown> | null): Subject<unknown>;
10
+ /**
11
+ * Where a new record met at `now` starts stepping: from its `since`, or from `now` for a voice that
12
+ * catches up `fresh`, on the `stepMs` grid. True where that skipped steps, so the state depends on
13
+ * when the subject was first probed.
14
+ */
15
+ export declare function meet<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>, held: Subject<unknown>, now: number): boolean;
7
16
  /**
8
17
  * The mix time the voice clock first read `delay`, or will by its controls now. A stateful voice
9
18
  * counts from its last seek that rebuilt state, as its records do.
@@ -20,6 +29,8 @@ export declare function shownOf<I, O>(this: Mixer<I, O>, voice: Voice<I, O>, sin
20
29
  * while it waited is made again for where its clock is.
21
30
  */
22
31
  export declare function begin<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>, now: number): void;
32
+ /** Counts a record whose `since` was still ahead from where its voice's clock now puts it. */
33
+ export declare function origin<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>, held: Subject<unknown>): void;
23
34
  /**
24
35
  * A voice's start or clock moved, or it went live at `now`: the records whose `since` was still
25
36
  * ahead count from where its clock now puts them.
package/dist/held.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { changed } from './chain.js';
2
+ import { stepOf } from './clock.js';
2
3
  import { schedule } from './due.js';
3
4
  import { everyoneOf, inherit, sighted } from './everyone.js';
4
5
  import { originOf } from './origin.js';
@@ -16,8 +17,11 @@ export function unreachedOf(voice, now) {
16
17
  voice.unreached = none;
17
18
  return none;
18
19
  }
19
- /** What this voice holds for this subject, made on first sight with `target` and `stagger` asked once. */
20
- export function held(voice, subject, slot) {
20
+ /**
21
+ * What this voice holds for this subject, made on first sight with `target` and `stagger` asked
22
+ * once; `guess` is a record it may be, from the subject's chain as last linked.
23
+ */
24
+ export function held(voice, subject, slot, guess = null) {
21
25
  if (voice.sharing) {
22
26
  const all = everyoneOf(this, voice);
23
27
  if (all !== null)
@@ -25,7 +29,7 @@ export function held(voice, subject, slot) {
25
29
  }
26
30
  if (unreached(voice, slot))
27
31
  return voice.unreached;
28
- let held = voice.subjects.get(subject);
32
+ let held = (guess === null ? voice.subjects.get(subject) : voice.subjects.find(subject, guess));
29
33
  if (held !== undefined)
30
34
  return held;
31
35
  const now = this.now;
@@ -53,11 +57,11 @@ export function held(voice, subject, slot) {
53
57
  held.delay = delay;
54
58
  held.since = since;
55
59
  held.shown = shown;
56
- held.stepped = since < now ? since : now;
60
+ const fresh = meet(this, voice, held, now);
57
61
  held.rebuilt = voice.rebuilds;
58
62
  if (this.projecting) {
59
63
  held.from = held.stepped;
60
- held.unknown = this.backward && voice.spec.from === 'current';
64
+ held.unknown = (this.backward && voice.spec.from === 'current') || fresh;
61
65
  }
62
66
  voice.subjects.set(subject, held);
63
67
  if (!(since <= now)) {
@@ -91,6 +95,30 @@ export function held(voice, subject, slot) {
91
95
  }
92
96
  return held;
93
97
  }
98
+ /**
99
+ * Where a new record met at `now` starts stepping: from its `since`, or from `now` for a voice that
100
+ * catches up `fresh`, on the `stepMs` grid. True where that skipped steps, so the state depends on
101
+ * when the subject was first probed.
102
+ */
103
+ export function meet(mix, voice, held, now) {
104
+ const since = held.since;
105
+ if (!(since < now)) {
106
+ held.stepped = now;
107
+ return false;
108
+ }
109
+ if (voice.spec.catchUp !== 'fresh' || voice.patch.step === undefined) {
110
+ held.stepped = since;
111
+ return false;
112
+ }
113
+ const tick = stepOf(mix, voice);
114
+ if (tick === undefined)
115
+ held.stepped = now;
116
+ else {
117
+ held.ticks = Math.floor((now - since) / tick + 1e-9);
118
+ held.stepped = since + held.ticks * tick;
119
+ }
120
+ return held.stepped > since;
121
+ }
94
122
  /**
95
123
  * The mix time the voice clock first read `delay`, or will by its controls now. A stateful voice
96
124
  * counts from its last seek that rebuilt state, as its records do.
@@ -118,10 +146,19 @@ export function begin(mix, voice, now) {
118
146
  if (voice.anchorElapsed !== 0) {
119
147
  voice.seeks++;
120
148
  voice.rebuilds++;
149
+ // Kept with its controls, so a seek back to after this puts back the count its records hold.
150
+ voice.note(now, mix.transport.seq, true);
121
151
  }
122
152
  reorigin(mix, voice, now);
123
153
  changed(mix, voice);
124
154
  }
155
+ /** Counts a record whose `since` was still ahead from where its voice's clock now puts it. */
156
+ export function origin(mix, voice, held) {
157
+ held.since = mix.sinceOf(voice, held.delay);
158
+ held.shown = mix.shownOf(voice, held.since);
159
+ if (held.ticks === 0 && held.stepped > held.since)
160
+ held.stepped = held.since;
161
+ }
125
162
  /**
126
163
  * A voice's start or clock moved, or it went live at `now`: the records whose `since` was still
127
164
  * ahead count from where its clock now puts them.
@@ -132,10 +169,7 @@ export function reorigin(mix, voice, now) {
132
169
  return;
133
170
  let kept = 0;
134
171
  for (const held of early) {
135
- held.since = mix.sinceOf(voice, held.delay);
136
- held.shown = mix.shownOf(voice, held.since);
137
- if (held.ticks === 0 && held.stepped > held.since)
138
- held.stepped = held.since;
172
+ origin(mix, voice, held);
139
173
  if (!(held.since <= now))
140
174
  early[kept++] = held;
141
175
  }
package/dist/history.d.ts CHANGED
@@ -5,6 +5,10 @@ import { type Subject, type Voice } from './voice.js';
5
5
  export declare function last<T extends {
6
6
  at: number;
7
7
  }>(list: readonly T[], t: number, inclusive: boolean | ((e: T) => boolean)): T | undefined;
8
+ /** Where `last` finds its entry in the list, -1 where it finds none. */
9
+ export declare function lastIndex<T extends {
10
+ at: number;
11
+ }>(list: readonly T[], t: number, inclusive: boolean | ((e: T) => boolean)): number;
8
12
  /**
9
13
  * The last entry of a list kept in the order it was made that falls inside `cut`, an entry made
10
14
  * before its frame was read counting as `early`.
@@ -19,12 +23,31 @@ export declare function same(a: unknown, b: unknown): boolean;
19
23
  export declare function elapsedThen<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>, t: number): number;
20
24
  /** Records a change to a voice's controls under `history`, and lets go of what it no longer reaches. */
21
25
  export declare function noted<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>): void;
22
- /** A copy of a subject's record that shares nothing a read could change. */
23
- export declare function copyHeld<I, O>(voice: Voice<I, O>, h: Subject<unknown>): Subject<unknown>;
26
+ /** A copy of the state a voice's patch keeps for a subject. */
27
+ export declare function copyState<I, O>(voice: Voice<I, O>, state: unknown): unknown;
28
+ /**
29
+ * A copy of a subject's record that shares nothing a read could change; with `shared`, its state is
30
+ * the record's own, for a copy that takes one of its own before it steps.
31
+ */
32
+ export declare function copyHeld<I, O>(voice: Voice<I, O>, h: Subject<unknown>, shared?: boolean): Subject<unknown>;
33
+ /**
34
+ * As `copyHeld`, written over `into`, a record nothing else holds, so a copy made in its place
35
+ * leaves nothing new to collect. test/history.test.ts holds this to `copyHeld` field by field.
36
+ */
37
+ export declare function copyHeldInto<I, O>(voice: Voice<I, O>, h: Subject<unknown>, into: Subject<unknown>, shared?: boolean): Subject<unknown>;
38
+ /**
39
+ * Lets go of what a record holds from its last probe and its place in a chain, as a copy of it
40
+ * starts: a seek puts back a record nothing else holds this way, in place of a copy.
41
+ */
42
+ export declare function unread(h: Subject<unknown>): Subject<unknown>;
24
43
  /** Under `history` with `inputs`, copies the host fields any voice reads, once a frame, when they changed. */
25
44
  export declare function recordHost<I, O>(mix: Mixer<I, O>, now: number): void;
26
45
  /** Under `history` with `inputs`, keeps what an input signal read, each time it changes. */
27
46
  export declare function record<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>, subject: I, held: Subject<unknown>, value: number): void;
47
+ /** How far apart, in mix ms, a record's copies are kept. */
48
+ export declare function apart(history: {
49
+ readonly every?: number;
50
+ }): number;
28
51
  /** Under `history`, keeps a copy of a stateful voice's record for this subject every so often. */
29
52
  export declare function remember<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>, subject: I, held: Subject<unknown>): void;
30
53
  /**