@msb235/blits 0.10.0 → 0.11.0

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