@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/CHANGELOG.md CHANGED
@@ -4,6 +4,157 @@ This package follows [semver](https://semver.org). Below 1.0.0, a breaking chang
4
4
  version and everything else the patch. Each release lists its changes as **Breaking**, **Added** and
5
5
  **Fixed**, and the release workflow refuses a tag with no section here.
6
6
 
7
+ ## 0.11.0
8
+
9
+ ### Breaking
10
+
11
+ - A voice plays one pass unless told otherwise: `loop` defaults to `false`, where it defaulted to
12
+ `true`. A `cue` that left `loop` out and relied on looping for good now needs `loop: true`. A
13
+ patch with no duration, and a motion patch, have no passes and play as before.
14
+ - `Marked.mark` and an anchor's `mark` are a `MarkName`, one of a voice's five or a name it placed
15
+ inside itself, where they were a `Mark`. Code that reads `mark` as one of the five, such as an
16
+ exhaustive `switch`, needs a case for the rest.
17
+
18
+ ### Added
19
+
20
+ - `marks` on a voice's, an owner's and a span's spec: named marks inside it beside its five, each
21
+ at voice ms into a pass, `marks: { drop: 340 }`. An anchor names one as it names the five,
22
+ `{ of: 'lead', mark: 'drop' }`; `marks` lists it, `book` books it under its name, and a read
23
+ ahead stops at it. Each is reached once, on the first pass that reaches it from where the voice
24
+ starts, while its clock runs forward and before it ends. A name of one of the five, and a time
25
+ outside a pass, are refused at `cue`.
26
+ - `@msb235/blits/lean`: the package with a `mix` that leaves lanes out, for a host whose scenes
27
+ are too small to need them. It gives the same poses, and a bundle of `mix`, `kit`, `sum` and
28
+ `patch` is 44.5 kB gzipped against 59.9 from the main entry (esbuild, minified). `lanes: true`
29
+ is refused there. Its engine is exported as `lean`.
30
+ - A read ahead after a seek back plays the calls the tape holds: `project(t)` past a recorded
31
+ call threw, and now makes the call on its copies as a seek there makes it on the mix. It still
32
+ throws for a `drop` of a subject a motion patch holds, and for a retarget or push made before
33
+ its voice had met the subject. The tape moves forward and back while the read is made.
34
+ - `stepMs` on a voice's spec: the interval its `step` runs at, in place of the mix's. 0 steps it
35
+ once a frame by the frame's gap.
36
+ - `share(by, i, n)`: member `i` of `n`'s share of a signal, as `mix.blend` weighs it, to weight a
37
+ voice outside the blend as that member, such as one member played alone.
38
+ - `mix.blend` takes a `BlendMember`, `{ patch, name?, tags?, hits? }`, in place of a bare patch, so
39
+ a member answers to its own name in an anchor and carries its own tags and hits. Bare patches
40
+ take the spec's as before.
41
+ - A mix warns, once per patch and outside production, when the first delta a voice's `at` returns
42
+ holds a key outside the patch's `writes`, naming the voice and the key. The mix still drops the
43
+ key, as it always has; `checkPatch` in `@msb235/blits/testing` still finds one returned only on
44
+ some subjects or phases.
45
+ - `hits` and `seek` on an owner's and a span's spec. A hit is an event at a time on the owner's
46
+ clock, booked once while the owner plays, with its `score` and `tags`; a seek is where that clock
47
+ starts, so an owner cued at a rate below 0 with a seek plays its children back from there.
48
+ - `seek({ frame })` and `project({ frame })`, on a mix and a transport, reach a frame by its
49
+ number, which `mix.seq` and `transport.seq` read. Rate 0 can hold mix time still across several
50
+ frames, and a seek or read by time lands on the last of them; by number it lands on any one
51
+ history still holds.
52
+ - `catchUp: 'fresh'` on a voice's spec: a stateful subject first probed after its delay ran out
53
+ starts stepping from that probe with fresh state, where the default, `exact`, steps it from its
54
+ origin. The `stepMs` grid still counts from the origin, the fade-in is unchanged, and a handle
55
+ `seek` still rebuilds from the voice's start. Its state depends on when the host first looked,
56
+ so a read that meets such a subject late assesses it `held`.
57
+ - A read ahead stops at each known event between now and the time read: each call the tape holds,
58
+ and each mark `marks` would list. A subject asked about is read at every stop, so a patch that
59
+ steps takes one step per stretch and a signal that keeps state sees each boundary: a voice
60
+ starting on the way meets its subject at its start, where it met it at the time read. `assess`
61
+ says `exact` for a stateful signal a read ahead fed, where it said `stepped`. The stops are made
62
+ only while a voice has a `step` or a signal weight; otherwise a read ahead crosses the gap in
63
+ one go as before.
64
+
65
+ ### Fixed
66
+
67
+ - A seek ahead, or a read ahead, to the mix time rate 0 is holding stays at the host's own time.
68
+ It took the earliest host time the mix read that time at, behind calls already on the tape, so
69
+ a later seek back cued a voice still playing a second time and the next probe never returned.
70
+ - `weightOf` on lanes reports the weight a voice over every subject gave a subject before a
71
+ subject fade made it keep a record per subject, until the next read. It said 0, where the
72
+ general path said the weight.
73
+ - A seek or read back to before a `drop` finds a subject's fade out of a voice as it stood: mid
74
+ ramp, or faded out. It found the subject at full weight, and one faded out of a voice that had
75
+ not met it stayed out even before the fade. A subject a motion's retarget brought back after a
76
+ fade out is found faded out between the two.
77
+ - `rise` on a voice faded before its start leaves it `pending`, to start on time and follow its
78
+ anchor again, as a seek back finds it. It went `live` at once and played from where its clock
79
+ would have read 0, ahead of its start, so a seek back played on differently from playing
80
+ straight through.
81
+ - A read back to the frame a `drop` was made in, before anything read the mix that frame, shows
82
+ the subject met afresh, as the frame did. It showed the record the subject had before the drop,
83
+ a slewed weight's state included, in `project(t)` and `project({ frame })`.
84
+ - `assess` after a seek back says `stepped` for a stateful signal the seek put back from a copy
85
+ history kept and stepped on, where a read at the mix's own time said `exact`. It goes on saying
86
+ so for that record as the mix plays on, since the state carries on from the step.
87
+ - A voice running back from past its end shows nothing until it reaches its passes, unless it
88
+ freezes after, as a voice playing forward shows nothing before its start unless it freezes
89
+ before. It showed its last frame: `rate: -1, seek: 600` on a 300 ms pass showed it for its
90
+ first 300 ms, and so did a child its owner, turned back, had put past its end.
91
+ - An owner's `out` and `end` marks are known while it plays, at its last child's end, once every
92
+ child's end is. They were fixed only when it left, so `marks` listed them only then and only
93
+ under `history`, a booker took them a frame late, an anchor to an owner's end waited until it
94
+ left, and without `history` they were never booked. Its `coast` stays listed once it has left,
95
+ where it went with its last child.
96
+ - `pull` reads `undefined` at a position its list has gained. A list read as `[undefined]` and
97
+ then as `[undefined, undefined]` threw, reading the new position as the subject it read there
98
+ last time.
99
+ - A spring, glide or tween made by another copy of blits of the same release, loaded beside this
100
+ one, runs as a motion on this copy's mix: on lanes, and retargeted, pushed and read through its
101
+ handle, which refused it. It played through `at` off the lanes. Copies of different releases
102
+ still keep apart.
103
+ - `mix.span` refuses a rate below 0. A span cued backward placed none of its children, which
104
+ waited for good, and never left. Its handle's `rate` still turns one that plays.
105
+ - A voice whose patch has no duration books no hits while its clock runs back, as every other
106
+ voice does. One cued backward booked each hit ahead of it.
107
+ - A fade set for a time after a voice's passes run out no longer begins in place of the voice
108
+ ending, where one frame crossed both: `fade({ at })` on a voice with no `freeze` that ended
109
+ before `at` played on, held at its last frame, until the fade finished.
110
+ - A seek forward to the time of a recorded call lands that frame once. It landed it again after
111
+ the call, so a `rise` played there on a voice past its end began the voice's fade over.
112
+ - A voice cued with `seek`, or sought while pending, keeps its count of rebuilds with its
113
+ controls when it starts. A seek back to after its start, then a handle `seek` played again
114
+ before the subject was read, left the subject's state as it was rather than rebuilding it.
115
+ - A mix sought back time after time no longer steps a subject from the same copy of its state,
116
+ further each time. A seek lets go of the copies made after the moment it sought, so a mix
117
+ sought 300 ms back and to where it was before every frame stepped each subject from the copy
118
+ before its first seek: 70 steps at the first frame and 1069 at the 300th, at `stepMs` 5. The
119
+ copy a record is put back from now keeps its state stepped ahead to near the moment sought,
120
+ and the next record put back from it steps on from there: 67 to 74 steps at every frame. It
121
+ shows the same values. Under `maxDt`, without `stepMs`, or for a patch that `reads` a host
122
+ field, a record steps from the copy as before.
123
+ - Replacing a voice makes less garbage. A one-subject tween voice retargeted as it is cued, while
124
+ another leaves and its subject is dropped, allocates about 13 KB, from 21.5
125
+ (`bench/churnwho.mjs`, 5,000 voices), and a `pull` keeps what it knows of a channel between
126
+ calls, about 1.2 KB a call for four channels. Frame time is unchanged.
127
+ - A seek back, or a read back, places a subject whose start had moved while it waited where the
128
+ move put it. A copy of a record kept while its `stagger` delay was running, on a voice with
129
+ `freeze: 'before'`, holds the start as it was then; a rate, a ramp, a `seek` with
130
+ `state: 'keep'`, an owner's rate or the mix's rate then moved that start, and a record put back
131
+ from the copy to a moment past the old start kept the old one, so its `step` ran on from the
132
+ wrong time. A seek lets go of the later copies, so a mix sought back before every frame showed
133
+ it at every frame from then on; `project(t)` showed it for `t` up to one copy's interval past
134
+ the move.
135
+ - A seek back to before a subject faded out of a voice finds the voice reaching it again, where
136
+ the voice had not met the subject before the fade. The subject stayed out of the voice with
137
+ `lanes: false`, and with lanes on for a voice naming its `subjects`; otherwise lanes brought it
138
+ back, so the two paths read differently. A read back to before the fade, `project(t)`, left
139
+ it out on both, and reaches it now.
140
+ - A seek or a read back into a subject's ramp out of a voice, `fade({ subject, over })`, finds
141
+ the ramp once it has ended. The subject read at full weight there, and after a seek it stayed
142
+ in the voice at full weight for good.
143
+ - `weightOf` reads 0 after a seek back until the subject is probed again. What a subject's last
144
+ probe by then read is not kept, and it reported a weight from after the moment sought, or one
145
+ older than its last probe for a voice on a lane, so the two paths read differently.
146
+ - A write to the handle of a voice waiting for its cue after a seek back changes nothing, and the
147
+ tape does not record it. It put the voice on the mix's schedule, so the voice started at its
148
+ start time outside the mix, folded in but missing from `voices()`, and it branched the tape,
149
+ which left the voice's cue in the old future.
150
+ - Dropping a subject no longer costs time in step with how many subjects the mix holds. With a
151
+ tween voice per subject, stopping one voice and dropping its subject cost 2.2 µs among 1,000
152
+ subjects and 15.6 among 10,000; it costs 0.9 and 2.6 (`bench/restart.mjs`).
153
+ - The first `pull` after a voice naming a subject comes or goes, or a subject is dropped, no
154
+ longer reads every subject's chain to find the ones that changed, and a retargeted tween copies
155
+ its stretch without an iterator. The frame after 100 voices are replaced among 10,000 took
156
+ about 2.1 ms and takes about 1.35.
157
+
7
158
  ## 0.10.0
8
159
 
9
160
  ### Breaking
package/README.md CHANGED
@@ -28,7 +28,7 @@ const flicker = patch<Part, PartPose>(
28
28
  );
29
29
 
30
30
  const m = mix<Part, PartPose>(PART);
31
- const handle = m.cue({ patch: flicker, fade: { in: 300, out: 300 } });
31
+ const handle = m.cue({ patch: flicker, loop: true, fade: { in: 300, out: 300 } });
32
32
 
33
33
  // Once a frame: tell the mix what time it is, then ask for the parts you are about to draw.
34
34
  function frame(timestamp: number) {
@@ -48,12 +48,12 @@ In order, on the same mix:
48
48
 
49
49
  ```ts
50
50
  // Place a voice at a time on the host's clock, or against another voice's marks.
51
- m.cue({ patch: typeIn, name: 'title', start: 1000, loop: false });
52
- m.cue({ patch: tint, loop: false, anchor: { start: { after: 'title', by: 200 } } });
51
+ m.cue({ patch: typeIn, name: 'title', start: 1000 });
52
+ m.cue({ patch: tint, anchor: { start: { after: 'title', by: 200 } } });
53
53
 
54
54
  // A span fits the voices it holds into a budget, one after another by default.
55
55
  const intro = m.span({ duration: 2000 });
56
- for (const line of lines) m.cue({ patch: typeIn, loop: false, owner: intro });
56
+ for (const line of lines) m.cue({ patch: typeIn, owner: intro });
57
57
 
58
58
  // Read the mix half a second ahead without moving it, or move it: a scrubber.
59
59
  const next = m.project(m.now + 500).probe(part);
@@ -91,6 +91,17 @@ npm run site:smoke # build, then load every page headless and fail on any erro
91
91
 
92
92
  `.github/workflows/site.yml` deploys it to GitHub Pages on every push to `main`, and by hand.
93
93
 
94
+ ## A smaller bundle
95
+
96
+ Lanes, which work a channel out for every subject at once, are about a quarter of the package. A
97
+ host with tens of subjects does not need them:
98
+
99
+ ```ts
100
+ import { kit, mix, patch, sum } from '@msb235/blits/lean';
101
+ ```
102
+
103
+ Everything else is the same and so are the poses. `lanes: true` is refused there.
104
+
94
105
  ## Writing patches for blits in another library
95
106
 
96
107
  A library that ships patches exports them with the channels they write, as a `Kit` of its own
package/dist/blend.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { stateful } from './hosts.js';
2
2
  import { reading } from './reading.js';
3
+ import { shareOf } from './signals.js';
3
4
  import { Store } from './store.js';
4
5
  export function blendOf(by, stops) {
5
6
  return {
@@ -29,11 +30,6 @@ export function ownBlends(voices) {
29
30
  of.members.push(v);
30
31
  }
31
32
  }
32
- /** A blend member's weight for its signal's read `k`: the members' stops sit evenly along 0..1. */
33
- function shareOf(k, i, stops) {
34
- const d = stops === 0 ? 0 : Math.abs(k * stops - i);
35
- return d >= 1 ? 0 : 1 - d;
36
- }
37
33
  /** A blend member's share of its signal's read for this subject this frame, made by the first to ask. */
38
34
  export function blended(voice, subject, held, slot) {
39
35
  const { of, i } = voice.blend;
package/dist/book.d.ts CHANGED
@@ -22,6 +22,8 @@ export interface BookHost<I, O> {
22
22
  /** The booker canceled: forget it. */
23
23
  unhook(booker: Book<I, O>): void;
24
24
  }
25
+ /** Whether voice ms `at` falls in a pass of a patch `duration` long: 0 up to it, or 0 on for none. */
26
+ export declare function inPass(at: number, duration: number): boolean;
25
27
  /** Refuses hits a voice could never play. */
26
28
  export declare function checkHits(hits: readonly Hit[], duration: number): void;
27
29
  /** One `book` call: maps host time onto the outside clock and takes each item once. */
package/dist/book.js CHANGED
@@ -5,10 +5,14 @@ const SLEW = 0.05;
5
5
  /** A booking whose time moves by more than this is canceled and taken again. */
6
6
  const RETIME_MS = 1;
7
7
  const none = [];
8
+ /** Whether voice ms `at` falls in a pass of a patch `duration` long: 0 up to it, or 0 on for none. */
9
+ export function inPass(at, duration) {
10
+ return at >= 0 && (duration > 0 ? at < duration : Number.isFinite(at));
11
+ }
8
12
  /** Refuses hits a voice could never play. */
9
13
  export function checkHits(hits, duration) {
10
14
  for (const h of hits)
11
- if (!(h.at >= 0) || (duration > 0 ? !(h.at < duration) : !Number.isFinite(h.at)))
15
+ if (!inPass(h.at, duration))
12
16
  throw new RangeError(duration > 0
13
17
  ? `blits: a hit's at is voice ms into a pass, 0 up to ${duration}, not ${h.at}`
14
18
  : `blits: a hit's at is voice ms, 0 or more, not ${h.at}`);
@@ -198,6 +202,11 @@ export class Book {
198
202
  if (!(hi > t.seenTo))
199
203
  continue;
200
204
  const lo = Math.max(t.seenTo, voice.elapsedAt(loAt));
205
+ // Its clock runs back: hits come only as it runs forward.
206
+ if (!(lo <= hi)) {
207
+ t.seenTo = hi;
208
+ continue;
209
+ }
201
210
  const d = voice.duration;
202
211
  const last = d > 0 ? Math.min(voice.passes - 1, Math.floor(hi / d)) : 0;
203
212
  for (let pass = d > 0 ? Math.max(0, Math.floor(lo / d)) : 0; pass <= last; pass++) {
@@ -246,8 +255,23 @@ export class Book {
246
255
  return this.host.hostOf(t);
247
256
  }
248
257
  }
249
- const markIndex = { start: 1, in: 2, coast: 3, out: 4, end: 5 };
250
258
  /** A mark's identity: its voice and which of its marks, or the announcement. */
251
259
  function keyOf(m) {
252
- return m.order * 6 + (m.mark === undefined ? 0 : markIndex[m.mark]);
260
+ const k = m.order * 6;
261
+ switch (m.mark) {
262
+ case undefined:
263
+ return k;
264
+ case 'start':
265
+ return k + 1;
266
+ case 'in':
267
+ return k + 2;
268
+ case 'coast':
269
+ return k + 3;
270
+ case 'out':
271
+ return k + 4;
272
+ case 'end':
273
+ return k + 5;
274
+ default:
275
+ return `${m.order}\u0000${m.mark}`;
276
+ }
253
277
  }
package/dist/chain.d.ts CHANGED
@@ -12,12 +12,23 @@ export declare function relink<I, O>(this: Mixer<I, O>, subject: I, was: Subject
12
12
  export declare function linkable<I, O>(this: Mixer<I, O>, voice: Voice<I, O>, subject: I, slot: number): Subject<unknown> | null;
13
13
  /** Whether a voice reaches a subject by what its spec says: the subjects it names, or its `target`. */
14
14
  export declare function aims<I, O>(this: Mixer<I, O>, voice: Voice<I, O>, subject: I): boolean;
15
+ /**
16
+ * The record a voice has of a subject now, as a seek or a read back to mix time `now` and frame
17
+ * `seq` takes it: none where it says only that the voice does not reach a subject it reached
18
+ * then, before the subject faded out of it. The subject had no record then, and is met afresh.
19
+ */
20
+ export declare function heldAt<I, O>(voice: Voice<I, O>, subject: I, held: Subject<unknown> | undefined, now: number, seq: number): Subject<unknown> | undefined;
15
21
  /**
16
22
  * A voice joined or left the list, or started or stopped waiting: the chains it is linked into
17
23
  * are relinked, every subject's for a voice over all of them and only its own for one naming
18
24
  * its subjects, and lanes take it on or off.
19
25
  */
20
26
  export declare function changed<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>): void;
27
+ /**
28
+ * Makes one subject's chain stale, to be linked again at its next read, and counts it in `relinks`,
29
+ * which its number is stamped with for `pull`.
30
+ */
31
+ export declare function unlink<I, O>(mix: Mixer<I, O>, head: Subject<unknown>): void;
21
32
  /** Files a voice under each subject it names, or among the voices that name none. */
22
33
  export declare function index<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>): void;
23
34
  export declare function unindex<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>): void;
package/dist/chain.js CHANGED
@@ -87,14 +87,26 @@ export function linkable(voice, subject, slot) {
87
87
  }
88
88
  /** Whether a voice reaches a subject by what its spec says: the subjects it names, or its `target`. */
89
89
  export function aims(voice, subject) {
90
+ return aimsAt(voice, subject, this.now);
91
+ }
92
+ /** As `aims` at mix time `now`, counting a subject as faded out only by frame `seq`. */
93
+ function aimsAt(voice, subject, now, seq = Number.POSITIVE_INFINITY) {
90
94
  // A read back to before a subject left still finds it reached.
91
95
  const left = voice.parted?.get(subject);
92
- if (left !== undefined && !(this.now < left.at))
96
+ if (left !== undefined && !(now < left.at) && left.seq <= seq)
93
97
  return false;
94
98
  if (voice.named !== null)
95
99
  return voice.named.has(subject);
96
100
  return voice.spec.target ? voice.spec.target(subject) : true;
97
101
  }
102
+ /**
103
+ * The record a voice has of a subject now, as a seek or a read back to mix time `now` and frame
104
+ * `seq` takes it: none where it says only that the voice does not reach a subject it reached
105
+ * then, before the subject faded out of it. The subject had no record then, and is met afresh.
106
+ */
107
+ export function heldAt(voice, subject, held, now, seq) {
108
+ return held !== undefined && !held.reaches && aimsAt(voice, subject, now, seq) ? undefined : held;
109
+ }
98
110
  /**
99
111
  * A voice joined or left the list, or started or stopped waiting: the chains it is linked into
100
112
  * are relinked, every subject's for a voice over all of them and only its own for one naming
@@ -114,10 +126,20 @@ export function changed(mix, voice) {
114
126
  for (const subject of voice.named) {
115
127
  const head = mix.chains.get(subject);
116
128
  if (head !== undefined)
117
- head.version = Number.NaN;
129
+ unlink(mix, head);
118
130
  }
119
131
  mix.relinks++;
120
132
  }
133
+ /**
134
+ * Makes one subject's chain stale, to be linked again at its next read, and counts it in `relinks`,
135
+ * which its number is stamped with for `pull`.
136
+ */
137
+ export function unlink(mix, head) {
138
+ head.version = Number.NaN;
139
+ const at = ++mix.relinks;
140
+ if (head.slot >= 0 && mix.lanes !== null)
141
+ mix.lanes.unlinked[head.slot] = at;
142
+ }
121
143
  /** Files a voice under each subject it names, or among the voices that name none. */
122
144
  export function index(mix, voice) {
123
145
  if (voice.holding !== null)
package/dist/clock.d.ts CHANGED
@@ -88,3 +88,17 @@ export declare function rebaseWith(c: Clock, now: number): void;
88
88
  * has then, so what it reads stays continuous and so does its speed.
89
89
  */
90
90
  export declare function retime(c: Clock, now: number, rate: number, over: number): void;
91
+ /**
92
+ * The fixed interval a voice's `step` runs at: its own `stepMs`, else the mix's. Undefined where
93
+ * it steps once a frame by the frame's gap, as every voice does under reduced motion.
94
+ */
95
+ export declare function stepOf(mix: {
96
+ readonly opts: {
97
+ readonly stepMs?: number;
98
+ };
99
+ readonly reducedNow: boolean;
100
+ }, voice: {
101
+ readonly spec: {
102
+ readonly stepMs?: number;
103
+ };
104
+ }): number | undefined;
package/dist/clock.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /** How many passes a voice's `loop` plays. */
2
2
  export function passesOf(loop) {
3
- const l = loop ?? true;
3
+ const l = loop ?? false;
4
4
  return l === true ? Number.POSITIVE_INFINITY : l === false ? 1 : l;
5
5
  }
6
6
  /** Whether `elapsed` voice ms is past the last of `passes` passes of a patch of `duration` ms. */
@@ -177,3 +177,11 @@ export function retime(c, now, rate, over) {
177
177
  c.ramp = over > 0 && rate !== from ? { from, to: rate, over } : null;
178
178
  c.rate = rate;
179
179
  }
180
+ /**
181
+ * The fixed interval a voice's `step` runs at: its own `stepMs`, else the mix's. Undefined where
182
+ * it steps once a frame by the frame's gap, as every voice does under reduced motion.
183
+ */
184
+ export function stepOf(mix, voice) {
185
+ const tick = voice.spec.stepMs ?? mix.opts.stepMs;
186
+ return tick !== undefined && tick > 0 && !mix.reducedNow ? tick : undefined;
187
+ }
package/dist/columns.d.ts CHANGED
@@ -12,13 +12,12 @@ export interface Column {
12
12
  export declare function clampRun(out: Float64Array, at: number, n: number, [lo, hi]: readonly [number, number]): void;
13
13
  /**
14
14
  * For a `pull` of a list it read last time in the same order, with every voice on a lane, where
15
- * `heads` holds each position's remembered chain head unless all are known current: what
15
+ * each position's remembered chain head was current when the mix's `relinks` read `since`: what
16
16
  * `prepare` and `writeLater` do for each subject from position `from`, while the subject reads
17
- * from the lanes; returns the first position that does not, or the list's length.
17
+ * from the lanes and its head has not been made stale since; returns the first position that does
18
+ * not, or the list's length.
18
19
  */
19
- export declare function pullRun<I, O>(lanes: Lanes<I, O>, slots: Int32Array, list: readonly I[], was: readonly I[], heads: readonly ({
20
- version: number;
21
- } | undefined)[] | null, from: number, columns: readonly Column[], now: number, version: number): number;
20
+ export declare function pullRun<I, O>(lanes: Lanes<I, O>, slots: Int32Array, list: readonly I[], was: readonly I[], since: number, from: number, columns: readonly Column[], now: number, version: number): number;
22
21
  /**
23
22
  * Queues a subject's values for each column at subject `n`'s places, for a `pull` that reads
24
23
  * from the lanes while every voice is on one; `flush` writes what is queued, column by column. A
package/dist/columns.js CHANGED
@@ -6,11 +6,12 @@ export function clampRun(out, at, n, [lo, hi]) {
6
6
  }
7
7
  /**
8
8
  * For a `pull` of a list it read last time in the same order, with every voice on a lane, where
9
- * `heads` holds each position's remembered chain head unless all are known current: what
9
+ * each position's remembered chain head was current when the mix's `relinks` read `since`: what
10
10
  * `prepare` and `writeLater` do for each subject from position `from`, while the subject reads
11
- * from the lanes; returns the first position that does not, or the list's length.
11
+ * from the lanes and its head has not been made stale since; returns the first position that does
12
+ * not, or the list's length.
12
13
  */
13
- export function pullRun(lanes, slots, list, was, heads, from, columns, now, version) {
14
+ export function pullRun(lanes, slots, list, was, since, from, columns, now, version) {
14
15
  const ready = lanes.filled(now, version) ? 1 /* Begin.READY */ : lanes.begin(now, version);
15
16
  if (ready !== 1 /* Begin.READY */ || !lanes.whole)
16
17
  return from;
@@ -20,6 +21,8 @@ export function pullRun(lanes, slots, list, was, heads, from, columns, now, vers
20
21
  const wide = lanes.wide;
21
22
  const frame = lanes.frameProbes;
22
23
  const subjects = lanes.subjects;
24
+ const unlinked = lanes.unlinked;
25
+ const current = since === Number.POSITIVE_INFINITY;
23
26
  if (columns !== lanes.queuedFor) {
24
27
  flush(lanes);
25
28
  lanes.queuedFor = columns;
@@ -35,11 +38,12 @@ export function pullRun(lanes, slots, list, was, heads, from, columns, now, vers
35
38
  let start = from;
36
39
  for (; n < list.length; n++) {
37
40
  const subject = list[n];
38
- if (was[n] !== subject || (heads !== null && heads[n]?.version !== version))
41
+ if (was[n] !== subject)
39
42
  break;
40
43
  const slot = slots[n];
41
44
  const o = slot * 6 /* Per.SLOT */;
42
45
  if (slot < 0 ||
46
+ (!current && !(unlinked[slot] <= since)) ||
43
47
  per[o + 0 /* Per.FILLED */] >= busy ||
44
48
  per[o + 2 /* Per.IDLE */] !== 0 ||
45
49
  per[o + 1 /* Per.SEEN */] < wide ||
@@ -163,6 +167,8 @@ function rests(lanes, c, runs) {
163
167
  out[i + a] = c.rest[a];
164
168
  }
165
169
  }
170
+ /** The longest run of values `copies` copies itself: `set` takes a view, an object a run. */
171
+ const VIEWED = 256;
166
172
  /** Copies a laned channel's values for every queued run into its rows. */
167
173
  function copies(lanes, c, values, runs) {
168
174
  const slots = lanes.queueSlots;
@@ -174,7 +180,7 @@ function copies(lanes, c, values, runs) {
174
180
  const at = rows[r] * axes;
175
181
  const base = slots[r] * axes;
176
182
  const span = counts[r] * axes;
177
- if (span > 64)
183
+ if (span > VIEWED)
178
184
  out.set(values.subarray(base, base + span), at);
179
185
  else
180
186
  for (let i = 0; i < span; i++)
package/dist/crowd.d.ts CHANGED
@@ -47,7 +47,7 @@ export declare const enum Flag {
47
47
  VOICE = 64,
48
48
  /** The row's voice is a motion voice; otherwise its patch is keys or a stateless fn. */
49
49
  MOTION = 128,
50
- /** The voice freezes before or after, so its clock is read from the voice. */
50
+ /** The voice freezes, or may run back past its end, so its clock is read from the voice. */
51
51
  FREEZES = 256,
52
52
  /** A keys row whose stops `keys` holds, at `Hot.AT`, with its duration at `Hot.X0` and passes at `Hot.MS`. */
53
53
  FLAT = 512,
package/dist/crowd.js CHANGED
@@ -317,7 +317,10 @@ function copyVoice(c, p) {
317
317
  let f = c.hot[h + 0 /* Hot.FLAGS */] & ~(64 /* Flag.VOICE */ | 1 /* Flag.PLAYING */ | 2 /* Flag.FAST */ | 256 /* Flag.FREEZES */);
318
318
  if (v.state === 'live' || v.state === 'frozen' || v.state === 'fading')
319
319
  f |= 1 /* Flag.PLAYING */;
320
- if (v.freezesBefore || v.freezesAfter)
320
+ // A keys or fn voice an owner holds can turn back with the owner, which copies it no more.
321
+ if (v.freezesBefore ||
322
+ v.freezesAfter ||
323
+ (v.motion === undefined && (v.owner !== null || v.hidesPast)))
321
324
  f |= 256 /* Flag.FREEZES */;
322
325
  if (v.ramp === null &&
323
326
  v.owner === null &&
package/dist/cue.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { blendOf } from './blend.js';
1
2
  import { checkHits } from './book.js';
2
3
  import { changed } from './chain.js';
3
4
  import { copyable } from './clone.js';
@@ -6,14 +7,15 @@ import { shares } from './everyone.js';
6
7
  import { finiteRate, plainWeight, VoiceHandle } from './handle.js';
7
8
  import { begin } from './held.js';
8
9
  import { handle } from './hosts.js';
10
+ import { checkMarks } from './marks.js';
9
11
  import { motionOf } from './motion.js';
10
- import { adopt, ownerPatch, ownerReading } from './owner.js';
12
+ import { adopt, Holding, ownerPatch, ownerReading } from './owner.js';
11
13
  import { checkPlacement, localNow, mixAt, pin, place } from './place.js';
12
14
  import { enlist } from './roster.js';
13
15
  import { scoredAdd } from './scored.js';
14
16
  import { checkChild, refit } from './spans.js';
15
- import { motionOwner } from './strays.js';
16
- import { record } from './tape.js';
17
+ import { cuedById, motionOwner } from './strays.js';
18
+ import { record, unplayable } from './tape.js';
17
19
  import { Voice } from './voice.js';
18
20
  export function cue(mix, spec) {
19
21
  const patch = spec.patch;
@@ -28,6 +30,8 @@ export function cue(mix, spec) {
28
30
  checkPlacement(mix, spec, anchor, owner);
29
31
  if (spec.hits !== undefined)
30
32
  checkHits(spec.hits, patch.duration);
33
+ if (spec.marks !== undefined)
34
+ checkMarks(spec.marks, patch.duration);
31
35
  const anchored = anchor !== undefined && (anchor.start !== undefined || anchor.in !== undefined);
32
36
  // A voice an owner holds is placed on the owner's clock: ms from when it starts.
33
37
  const local = owner === null ? mix.now : ownerReading(owner, mix.now);
@@ -51,7 +55,14 @@ export function cue(mix, spec) {
51
55
  const h = handle(mix, voice);
52
56
  voice.handle = h;
53
57
  // By its handle, so a voice a history store paged out and a seek revived is the one cued again.
54
- record(mix, 'cue', () => recue(mix, VoiceHandle.voiceOf(h) ?? lost(h)));
58
+ record(mix, 'cue', (stage) => {
59
+ const parked = VoiceHandle.voiceOf(h) ?? lost(h);
60
+ const to = stage.mix(mix);
61
+ if (to === mix)
62
+ recue(mix, parked);
63
+ else if (to !== undefined)
64
+ recue(to, aheadOf(to, parked));
65
+ });
55
66
  return h;
56
67
  }
57
68
  /** Refuses a patch writing a channel `kit` lacks, or holds as another kind than it was written for. */
@@ -78,6 +89,10 @@ export function playable(mix, spec) {
78
89
  finiteRate(spec.rate);
79
90
  if (spec.seek !== undefined && !Number.isFinite(spec.seek))
80
91
  throw new RangeError(`blits: a voice's seek is a finite number of ms, not ${spec.seek}`);
92
+ if (spec.stepMs !== undefined && !(spec.stepMs >= 0 && spec.stepMs < Number.POSITIVE_INFINITY))
93
+ throw new RangeError(`blits: a voice's stepMs is a finite number of ms, 0 or more, not ${spec.stepMs}`);
94
+ if (spec.catchUp !== undefined && spec.catchUp !== 'exact' && spec.catchUp !== 'fresh')
95
+ throw new RangeError(`blits: catchUp takes 'exact' or 'fresh', not ${String(spec.catchUp)}`);
81
96
  if (typeof spec.weight === 'number')
82
97
  plainWeight(spec.weight);
83
98
  fitsKit(patch, mix.kit);
@@ -110,6 +125,36 @@ function recue(mix, voice) {
110
125
  voice.state = 'live';
111
126
  enter(mix, voice);
112
127
  }
128
+ /** Each read ahead's own blends, by the live blend its copies' members were cued in. */
129
+ const blends = new WeakMap();
130
+ /** A read ahead's copy of a voice a seek parked, as its cue left it, for the copy `to` to cue. */
131
+ function aheadOf(to, parked) {
132
+ const copy = parked.copy(() => undefined);
133
+ if (parked.owner !== null) {
134
+ const owner = cuedById(to, parked.owner.id);
135
+ if (owner === undefined || owner.holding === null)
136
+ unplayable('a cue under an owner that has left');
137
+ copy.owner = owner;
138
+ }
139
+ if (copy.holding !== null)
140
+ copy.holding = new Holding();
141
+ if (copy.blend !== null) {
142
+ let own = blends.get(to);
143
+ if (own === undefined) {
144
+ own = new Map();
145
+ blends.set(to, own);
146
+ }
147
+ const live = copy.blend.of;
148
+ let of = own.get(live);
149
+ if (of === undefined) {
150
+ of = blendOf(copy.blend.of.by, copy.blend.of.stops);
151
+ own.set(live, of);
152
+ }
153
+ copy.blend = { of, i: copy.blend.i };
154
+ of.members.push(copy);
155
+ }
156
+ return copy;
157
+ }
113
158
  function lost(h) {
114
159
  throw new Error(`blits: voice ${h.id} is paged out, and the seek that plays its cue again did not load it`);
115
160
  }
@@ -120,6 +165,7 @@ function enter(mix, voice) {
120
165
  const anchor = spec.anchor;
121
166
  const anchored = anchor !== undefined && (anchor.start !== undefined || anchor.in !== undefined);
122
167
  const motion = voice.motion;
168
+ voice.parked = false;
123
169
  if (owner !== null)
124
170
  adopt(owner, voice);
125
171
  if (!Number.isNaN(mix.now)) {
@@ -131,7 +177,8 @@ function enter(mix, voice) {
131
177
  // A start the host gave by its own clock, where the mix's rate puts it.
132
178
  if (!Number.isNaN(voice.pinned) && voice.state === 'pending')
133
179
  pin(mix, voice, voice.pinned);
134
- if (motion !== undefined) {
180
+ // A read ahead's copy leaves the patch asking the live mix for its time.
181
+ if (motion !== undefined && !mix.projecting) {
135
182
  mix.playing.set(motion, (mix.playing.get(motion) ?? 0) + 1);
136
183
  mix.owner ??= motionOwner(new WeakRef(mix));
137
184
  motion.owner = mix.owner;
@@ -1,5 +1,5 @@
1
1
  import type { Mixer } from './mixer.js';
2
- import type { Mark, Query } from './types.js';
2
+ import type { MarkName, Query } from './types.js';
3
3
  import { type Voice } from './voice.js';
4
4
  /**
5
5
  * What a mix keeps of a voice it has forgotten, on leaving without history or past history's reach:
@@ -14,7 +14,8 @@ export interface Departed {
14
14
  readonly name: string | undefined;
15
15
  readonly tags: readonly string[];
16
16
  readonly writes: readonly unknown[];
17
- readonly marks: Readonly<Record<Mark, number | undefined>>;
17
+ /** Its five, and those it placed inside itself. */
18
+ readonly marks: Readonly<Partial<Record<MarkName, number>>>;
18
19
  /** How many of the index's ends hold it. */
19
20
  held: number;
20
21
  }