@msb235/blits 0.16.0 → 0.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/CHANGELOG.md +149 -0
  2. package/README.md +1 -1
  3. package/dist/ahead.js +6 -9
  4. package/dist/clock.d.ts +11 -1
  5. package/dist/clock.js +28 -9
  6. package/dist/contribution.js +11 -4
  7. package/dist/cue.js +12 -3
  8. package/dist/due.d.ts +18 -0
  9. package/dist/due.js +101 -2
  10. package/dist/fade.d.ts +12 -2
  11. package/dist/fade.js +41 -8
  12. package/dist/fill.js +2 -1
  13. package/dist/fit.js +8 -0
  14. package/dist/gone.js +1 -0
  15. package/dist/handle.d.ts +1 -1
  16. package/dist/handle.js +3 -1
  17. package/dist/held.d.ts +2 -1
  18. package/dist/held.js +14 -6
  19. package/dist/hosts.js +5 -1
  20. package/dist/lane.d.ts +2 -0
  21. package/dist/lane.js +2 -0
  22. package/dist/lanes.js +2 -1
  23. package/dist/meet.js +9 -0
  24. package/dist/mixer.d.ts +2 -0
  25. package/dist/mixer.js +2 -0
  26. package/dist/motion.js +6 -6
  27. package/dist/move.js +309 -82
  28. package/dist/origin.d.ts +2 -0
  29. package/dist/origin.js +4 -0
  30. package/dist/owner.js +4 -2
  31. package/dist/paging.d.ts +5 -1
  32. package/dist/paging.js +13 -3
  33. package/dist/patch.js +24 -4
  34. package/dist/place.js +67 -31
  35. package/dist/project.d.ts +5 -2
  36. package/dist/project.js +7 -4
  37. package/dist/qualify.js +6 -1
  38. package/dist/rests.js +9 -1
  39. package/dist/revive.d.ts +11 -1
  40. package/dist/revive.js +7 -0
  41. package/dist/roles.js +5 -2
  42. package/dist/rows.js +4 -1
  43. package/dist/seat.d.ts +5 -1
  44. package/dist/seat.js +5 -1
  45. package/dist/seek.js +6 -4
  46. package/dist/shared.d.ts +1 -1
  47. package/dist/shared.js +1 -1
  48. package/dist/spans.d.ts +10 -4
  49. package/dist/spans.js +121 -31
  50. package/dist/trace.js +7 -2
  51. package/dist/types/transport.d.ts +2 -1
  52. package/dist/voice.d.ts +42 -4
  53. package/dist/voice.js +45 -5
  54. package/dist/weigh.js +1 -1
  55. package/docs/schema.html +1 -1
  56. package/package.json +3 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,155 @@ 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.16.1
8
+
9
+ ### Fixed
10
+
11
+ - A child anchored to a sibling under an owner at rate 0 plays where its anchor puts it when the
12
+ owner is sought forward past both. Its anchor's time went through mix time on a clock standing
13
+ still, which maps every time it has passed to the seek's moment, so the child was placed at the
14
+ time sought plus its offset and never played.
15
+ - `inert` reads false while a motion voice has a subject it met inside its `stagger` and has not
16
+ read since, which moves once it is probed. It read true, so a host that stops on `inert` stopped
17
+ a subject that had yet to start.
18
+ - A span's child whose cue a seek back plays again waits for the span's fit, as it did when first
19
+ cued, so the span lays it out as before. It started at once wherever its span's clock had passed
20
+ the start the fit first gave it: the fit then took it for ended, so it skipped nothing else, or,
21
+ in a span running back, the child ran back to its start and left.
22
+ - A child its owner's seek put back before its start stays where it was through a mix sought back to
23
+ after that seek: a later fit keeps it in its place, and an anchor does not place it again. The
24
+ seek took it for one still to start, so a span laid it out afresh, and a child anchored to a
25
+ sibling's end was placed again from that end, and played on as if its owner had not been sought.
26
+ - A voice cued while the mix is paused, its `start` a host time still ahead, waits for that time
27
+ again when a seek back plays its cue again. It started at once, at the paused mix time, since
28
+ the voice was taken for one cued while the mix slept and its start was moved to the next sync.
29
+ - A span whose budget runs out between two frames fits again from that moment, in turn with its
30
+ siblings' starts and ends: a child it places at a start already passed starts in that frame, and
31
+ one queued after it starts where it lapsed at any frame rate. The span lapsed only after the
32
+ frame had started its siblings, so a sibling whose old start the frame passed kept it, and one
33
+ placed in the past waited a frame to start.
34
+ - An owner running back holds a child placed where its clock stands unplayed, whether or not a
35
+ frame lands there. An owner cued backward with a child at 0 started that child, ended it, and
36
+ left with it only when a frame fell exactly on the owner's start; otherwise both waited for good.
37
+ - A read ahead that throws partway, on a call the tape holds that it cannot play, puts the tape
38
+ back. It left the live tape moved past the call, so a later seek restored the wrong state.
39
+ - Under `history`, a voice faded out of one subject is let go when history lets it go. The mix
40
+ held every such voice for good, and each `drop` walked them all.
41
+ - What `prepare` loads is let go once `history.ms` of mix time passes without a seek or read
42
+ taking it. Until one did, the mix kept every record paged since, for good.
43
+ - `trace` of a voice sought past its end, with a `fade.out`, follows the fade from the voice's
44
+ start, where the mix runs it. It called the run exact and read 0 throughout.
45
+ - `weightOf` with lanes on reports the weight a subject was given when its voice moved onto a lane
46
+ after the subject's probe that frame. It reported 0.
47
+ - A `pull` right after two or more cues folds the new voices in voice order, as a probe does. It
48
+ folded them newest first, which on a `mul` or `sum` channel could differ in the last bit.
49
+ - An owner's fade in counts from when the owner started, under an owner turned back or slowed.
50
+ It was worked out again each frame from where the outer owner's clock would next reach the
51
+ inner owner's start, so a clip under an inner owner with a fade in vanished when the outer
52
+ owner turned back, and slowing the outer owner jumped the fade to its end.
53
+ - A fade set for now or earlier, on a voice whose owner runs back, begins then. It was put off to
54
+ when the owner's clock would come back to the voice's start, playing at full weight until then.
55
+ - A fade in stands still while a fade out runs, and climbs on from there after `rise`, rather than
56
+ jumping to where its clock had reached. A subject's own fade out stops its fade in climbing too,
57
+ as the voice's does; before, the two multiplied into a rise.
58
+ - A span child with `faster` or `slower` at `Infinity` is retimed to fill its span. It got a rate
59
+ of Infinity, jumping to its end, or 0, and was skipped as never fitting.
60
+ - A `from: 'current'` voice whose stop lists fewer axes than its `vec` channel reads the missing
61
+ axes as the lerp does. They read NaN.
62
+ - `spring` and `glide` throw for an infinite parameter. A glide with an infinite `ms` or
63
+ `stiffness` read NaN, and a spring with infinite `damping` or `mass` jumped to its target.
64
+ - A role member at weight 0 holds its role's place whatever its patch's form, and a band switched
65
+ off by a frame at weight 0 stays off when the weight comes back between `off` and `on`. A `keys`
66
+ member at weight 0 was read and so held the role's place; any other was not, so its role folded
67
+ at the next member's place, which on a `last` channel changed which value won, and the role's
68
+ band, along with every band in a mix with a role in it, survived the frame at 0.
69
+ - A frame with span children in it costs less than before this release's span-order fixes, which
70
+ had made it about 5% dearer: a child's visit reads its owners' clocks once, and the voices
71
+ visited every frame, which include every voice an owner holds, are kept in a list of their own
72
+ rather than sifted through the due queue's heap and sorted each frame. The new `spans` bench row,
73
+ ten staggered clips cued under one owner every four frames, reads 5.0% faster than with the
74
+ fixes (0.141 to 0.134 ms a frame) and 3.4% faster than before them (0.179 to 0.173 on another
75
+ node), 7 rounds each, U 7.
76
+ - A voice an owner holds, first read while the owner runs back, shows at once instead of waiting
77
+ on its fade in. The fade in counted from where the owner's clock, running back, would next reach
78
+ the voice's start, a time still to come, so the voice read nothing until then.
79
+ - An owner sought forward passes the span children it crosses as played straight through,
80
+ whatever its rate: they start and end in order, and their span fits from each end. Sought ahead
81
+ while turned back, it started them in the order its clock running back put their starts, last
82
+ first, and ended none; turning forward later ended them all at once, out of order, and a span
83
+ holding spans left in a later frame at a time still to come, so a mix sought back left it
84
+ fading and its clips silent. A timeline's reel jumped ahead in reverse played its scenes slowed
85
+ and overlapping.
86
+ - A span child kept quiet at its start, its owner having run back to it, comes back and ends in
87
+ turn when a seek of the owner takes it past its end, and its span fits from that end. It stayed
88
+ quiet, so the children playing past it kept the rate they had before its end.
89
+ - Under `history`, an owner another owner holds no longer leaves once none of its children plays:
90
+ it stays with its own owner, quiet, so turning the outer owner back brings back a clip under an
91
+ owner inside it, as it does a direct child.
92
+ - A span an owner ran back over keeps the layout it made going forward. Before, each child it ran
93
+ back to the start of fitted the span again from there, and so did a seek back into it, counting
94
+ the children waiting for the clock to come back as playing: the ones still to start moved
95
+ earlier, and the playing ones changed speed.
96
+ - A span lays out its children the same at any frame rate. When a child ended between two frames,
97
+ its span fitted again from that moment, and a sibling whose start had passed by then, but which
98
+ no frame had started yet, was placed again from there at a new start and rate: two children of a
99
+ `stagger` span read 420 stepped every 10 ms and 389.47 every 25 ms.
100
+ - A voice under an owner whose rate ramps through 0 (from forward to back) ends where its passes
101
+ run out while the owner still runs forward. The time was taken as never, so it ended at the
102
+ frame that noticed, and a span holding it fitted again from there, its layout following the
103
+ frame rate.
104
+ - A placement whose names make a loop is taken where no mark in it waits on itself: a cue whose
105
+ `out` is its follow's `start`, the follow starting off the cue's `coast`, was refused as waiting
106
+ on itself, since the check followed every name whatever mark it asked for. A voice's `start`,
107
+ `in`, `coast` and the marks placed inside it wait only on what places its start; its `out` and
108
+ `end` on that and what places its out.
109
+ - A placement that waits on itself through a query by tag or by channel is refused at `cue`, as
110
+ one through names is: a voice starting after the one tagged `b`, cued beside one tagged `b`
111
+ starting after the one tagged `a`, was taken. A query is followed to the voices it can pick at
112
+ the cue, the voice being cued among them.
113
+ - A voice no owner holds, placed by a timestamp in its anchor, stays where it was placed through a
114
+ seek back. Each placing read the timestamp against the mix's offset at that moment, which a seek
115
+ moves, so after `seek(1000)` from 2100 an owner anchored to start at 2000 had started at 900, and
116
+ one anchored to end at 4000 left at 0. The timestamp is now read against the offset in force when
117
+ the voice was cued, as a plain `start` is.
118
+ - Under `history`, a span an owner holds no longer leaves once its budget is out and none of its
119
+ children plays: it stays with its owner, quiet, so turning the owner back brings back a clip
120
+ under a span, or under a span in a span, as it does one under an owner.
121
+ - A child cued into a span an owner ran back over queues after the children it holds. The span
122
+ was fitted again at the cue, counting the children waiting for the clock to come back as
123
+ playing, so the new one started beside them. No fit, from a cue, an arc, a sibling leaving or a
124
+ seek, moves a span run back over until the clock passes it again.
125
+ - A child whose rate a fit changed reads, run back over by an owner, what it read going forward. Its
126
+ clock read the new rate all the way back past where the fit changed it; it now runs back on the
127
+ clock it had before, and forward again the fit changes it at the same place.
128
+ - An owner sought past a span's child fits the rest of the span from where that child ended. The
129
+ owner's clock now put the end before the mix began, where a span read its own clock as 0, so the
130
+ children still to come were laid out from the span's start: a timeline's reel jumped into its
131
+ fifth sequence started the third scene beside the second.
132
+ - A span whose children end in one frame fits from each end in the order they came. They were
133
+ taken in cue order, so an end the frame noticed first could be one that a fit from an earlier
134
+ end would have moved, and two children of a `stagger` span read 509.5 stepped every 50 ms where
135
+ 1 ms steps read 525.3.
136
+ - A child a fit skipped reads, once its owner is sought or run back before the skip, what it read
137
+ going forward. One skipped while playing kept its jump to its end and read on from there, and
138
+ one skipped before it started, placed at its end at its start, played its passes backward from
139
+ its start.
140
+ - A span child its owner ran or was sought back over takes the change a fit made to it again as
141
+ the owner's clock comes forward to it, as the layout made going forward stands. The span was
142
+ fitted again there instead, with the children it had skipped counted as still playing or left
143
+ out, so the ones it had skipped together with them played on, slowed, beside them.
144
+ - An owner sought past a span lays the span out in that frame as played straight through. Its
145
+ children that started and ended in the frame were taken in cue order once their span had started,
146
+ each started and ended in one step, so a fit from one end missed the children it should have
147
+ moved.
148
+ - A span its owner was sought back before keeps the layout it made going forward, as one run
149
+ back over does. A child's end there fitted it again with the children ahead, left quiet by the
150
+ seek, counted as gone, so the playing ones slowed to fill the span.
151
+ - A mix sought back into a span plays on as it stood at that moment. A seek fitted every span
152
+ again from the moment sought, which a span played straight through is not, so its children's
153
+ rates followed where the seek landed. A span's end and `result` are now kept with its history and
154
+ put back by the seek.
155
+
7
156
  ## 0.16.0
8
157
 
9
158
  ### Added
package/README.md CHANGED
@@ -151,7 +151,7 @@ do yet.
151
151
 
152
152
  ## Status
153
153
 
154
- v0.16.0, with five consumers. magicsmoke runs every fault on it, wod its wheel, tricks and
154
+ v0.16.1, with five consumers. magicsmoke runs every fault on it, wod its wheel, tricks and
155
155
  transitions, weasel its animator's tweens, sherpa its seams, and
156
156
  [klieg](https://github.com/orochi235/klieg) composes all three of its systems — letter motion,
157
157
  part effects, and the environment a sign is lit by — on a mix each.
package/dist/ahead.js CHANGED
@@ -20,13 +20,12 @@ import { unreached } from './unreached.js';
20
20
  export function playAhead(transport, c, copies, t, earlier) {
21
21
  const tape = transport.tape;
22
22
  const depth = tape?.undoDepth() ?? 0;
23
- let played = false;
24
23
  try {
25
24
  for (;;) {
26
25
  const e = nextEvent(tape, c, t);
27
26
  if (!(e < t))
28
27
  break;
29
- played = playTo(transport, c, copies, e) || played;
28
+ playTo(transport, c, copies, e);
30
29
  if (!carries(c))
31
30
  continue;
32
31
  // Placed now, as the next move would place them, while a target that has just left is here.
@@ -49,11 +48,12 @@ export function playAhead(transport, c, copies, t, earlier) {
49
48
  }
50
49
  c = next;
51
50
  }
52
- played = playTo(transport, c, copies, t) || played;
51
+ playTo(transport, c, copies, t);
53
52
  }
54
53
  finally {
55
- if (played)
56
- tape?.goto(depth);
54
+ // A call that throws partway has moved the tape already, so this asks the tape, not `playTo`.
55
+ if (tape !== undefined && tape.undoDepth() !== depth)
56
+ tape.goto(depth);
57
57
  }
58
58
  return c;
59
59
  }
@@ -108,14 +108,13 @@ function nextEvent(tape, c, t) {
108
108
  }
109
109
  /**
110
110
  * Makes the calls the tape holds up to mix time `to` on a read ahead's copies and moves them
111
- * there. Answers whether it moved the tape.
111
+ * there.
112
112
  */
113
113
  function playTo(transport, c, copies, to) {
114
114
  const tape = transport.tape;
115
115
  const due = (at) => (c.pace === null ? at : c.pace.reading(at)) <= to;
116
116
  const next = tape?.timestampAt(tape.undoDepth());
117
117
  c.u = hostAhead(c, to);
118
- let played = false;
119
118
  if (next !== undefined && due(next)) {
120
119
  // A call may write what a copy shares with the voice it was copied from.
121
120
  for (const copy of c.members)
@@ -125,7 +124,6 @@ function playTo(transport, c, copies, to) {
125
124
  if (v.parted !== null)
126
125
  v.parted = new Map(v.parted);
127
126
  }
128
- played = true;
129
127
  const last = replay(transport, due, c, {
130
128
  transport: () => c,
131
129
  mix: (of) => copies.get(of),
@@ -136,7 +134,6 @@ function playTo(transport, c, copies, to) {
136
134
  if (!(c.now >= to))
137
135
  for (const copy of c.members)
138
136
  move(copy, to);
139
- return played;
140
137
  }
141
138
  /** A transport for a read ahead's copies to go on from where those on `c` stand. */
142
139
  function restage(c) {
package/dist/clock.d.ts CHANGED
@@ -9,6 +9,16 @@ export declare function passesOf(loop: boolean | number | undefined): number;
9
9
  export declare function phaseAt(elapsed: number, duration: number, passes: number): number;
10
10
  /** The pass `elapsed` voice ms falls in, held at the last once they are done. */
11
11
  export declare function passAt(elapsed: number, duration: number, passes: number): number;
12
+ /**
13
+ * A fade in from `since` over `fadeIn` at mix time `now`, 0..1. It stands still where a fade out
14
+ * began, or the two ramps multiply into a rise, from `stop` on, and through `stalls`, pairs of mix
15
+ * times.
16
+ */
17
+ export declare function fadeInAt(fadeIn: number, out: {
18
+ at: number;
19
+ over: number;
20
+ rest: boolean;
21
+ } | null, ease: Curve | undefined, now: number, since: number, stalls: readonly number[] | null, stop?: number): number;
12
22
  /**
13
23
  * A voice's own fade this frame, 0..1: in from `since` over `fadeIn`, out along its ramp, and back
14
24
  * up the same curve where a fade out was turned around.
@@ -21,7 +31,7 @@ export declare function envelope(fadeIn: number, out: {
21
31
  from: number;
22
32
  at: number;
23
33
  over: number;
24
- } | null): number;
34
+ } | null, stalls?: readonly number[] | null): number;
25
35
  /**
26
36
  * A subject's voice time `raw` once the voice's freezes apply: 0 before it starts if it freezes
27
37
  * before, else NaN, where it shows nothing; `span` once its passes end if it freezes after. A `raw`
package/dist/clock.js CHANGED
@@ -21,19 +21,36 @@ export function passAt(elapsed, duration, passes) {
21
21
  return 0;
22
22
  return over(elapsed, duration, passes) ? passes - 1 : Math.floor(elapsed / duration);
23
23
  }
24
+ /**
25
+ * A fade in from `since` over `fadeIn` at mix time `now`, 0..1. It stands still where a fade out
26
+ * began, or the two ramps multiply into a rise, from `stop` on, and through `stalls`, pairs of mix
27
+ * times.
28
+ */
29
+ export function fadeInAt(fadeIn, out, ease, now, since, stalls, stop = Number.POSITIVE_INFINITY) {
30
+ let end = out && !out.rest && out.at < now ? out.at : now;
31
+ if (stop < end)
32
+ end = stop;
33
+ let climbed = end - since;
34
+ if (stalls !== null)
35
+ for (let i = 0; i < stalls.length; i += 2) {
36
+ const a = Math.max(stalls[i], since);
37
+ const b = Math.min(stalls[i + 1], end);
38
+ if (b > a)
39
+ climbed -= b - a;
40
+ }
41
+ const u = climbed / fadeIn;
42
+ if (u >= 1)
43
+ return 1;
44
+ return ease ? ease(Math.max(0, u)) : Math.max(0, u);
45
+ }
24
46
  /**
25
47
  * A voice's own fade this frame, 0..1: in from `since` over `fadeIn`, out along its ramp, and back
26
48
  * up the same curve where a fade out was turned around.
27
49
  */
28
- export function envelope(fadeIn, out, ease, reduced, now, since, back = null) {
50
+ export function envelope(fadeIn, out, ease, reduced, now, since, back = null, stalls = null) {
29
51
  let w = 1;
30
- if (fadeIn > 0 && !reduced) {
31
- // A fade in stops climbing where a fade out begins, or the two ramps multiply into a rise.
32
- const end = out && !out.rest && out.at < now ? out.at : now;
33
- const u = (end - since) / fadeIn;
34
- if (u < 1)
35
- w *= ease ? ease(Math.max(0, u)) : Math.max(0, u);
36
- }
52
+ if (fadeIn > 0 && !reduced)
53
+ w *= fadeInAt(fadeIn, out, ease, now, since, stalls);
37
54
  if (back && !reduced) {
38
55
  const u = back.from + (now - back.at) / back.over;
39
56
  if (u < 1)
@@ -114,7 +131,9 @@ export function timeWith(c, elapsed) {
114
131
  return r.from > 0 ? c.anchorNow + (elapsed - e0) / r.from : c.anchorNow;
115
132
  const a = (r.to - r.from) / (2 * r.over);
116
133
  const atEnd = e0 + r.from * r.over + a * r.over * r.over;
117
- if (elapsed <= atEnd) {
134
+ // A ramp that turns the clock back reads furthest where its rate crosses 0, not at its end.
135
+ const top = r.from > 0 && r.to < 0 ? e0 - (r.from * r.from) / (4 * a) : atEnd;
136
+ if (elapsed <= top) {
118
137
  const k = e0 - elapsed;
119
138
  const dt = Math.abs(a) < 1e-12
120
139
  ? -k / r.from
@@ -41,6 +41,7 @@ function sentBack(mix, voice, held) {
41
41
  function phaseOf(elapsed, duration, passes) {
42
42
  return elapsed < 0 ? phaseBefore(elapsed, duration) : phaseAt(elapsed, duration, passes);
43
43
  }
44
+ const NOTHING = Object.freeze({});
44
45
  /**
45
46
  * One live voice's delta for one subject this frame, through its record for the subject, or null
46
47
  * when it does not reach. Its weight is left in `w`.
@@ -139,9 +140,11 @@ export function contribution(voice, subject, now, held) {
139
140
  own(voice, held);
140
141
  }
141
142
  let delta = null;
142
- // A keys voice in a role reads at weight 0 too: having a delta makes it a member, and the
143
- // role folds at its first member's place.
143
+ // A voice in a role is a member at weight 0 too, and the role folds at its first member's
144
+ // place. A keys member reads; any other is not asked, and contributes nothing.
145
+ let member = false;
144
146
  if (silent(voice, weight) && (voice.built === null || voice.spec.role === undefined)) {
147
+ member = voice.spec.role !== undefined;
145
148
  // A `from: 'current'` voice takes its base on its first read, silent or not.
146
149
  if (voice.built && voice.spec.from === 'current' && held.base === undefined) {
147
150
  held.base = this.baseFor(voice, subject);
@@ -168,8 +171,12 @@ export function contribution(voice, subject, now, held) {
168
171
  remember(this, voice, subject, held);
169
172
  if (reading.kept !== keptBefore && !voice.keeping)
170
173
  stateful(this, voice);
171
- if (delta === null)
172
- return null;
174
+ if (delta === null) {
175
+ if (!member)
176
+ return null;
177
+ this.w = weight;
178
+ return NOTHING;
179
+ }
173
180
  if (voice.out?.rest && this.isRest(delta)) {
174
181
  held.weight = 0;
175
182
  held.restedAt = now;
package/dist/cue.js CHANGED
@@ -52,6 +52,7 @@ export function cue(mix, given) {
52
52
  ? Number.POSITIVE_INFINITY
53
53
  : mix.now;
54
54
  const voice = new Voice(mix.nextId++, spec, patch, spec.fade ?? noFade, local, start, mix.slotOf, mix.channels, mix.opts.host, mix.send, owner);
55
+ voice.offset = mix.offset;
55
56
  if (patch === ownerPatch && mix.fitting !== null)
56
57
  voice.fitting = mix.fitting;
57
58
  if (patch === ownerPatch && mix.arcing !== null) {
@@ -62,7 +63,10 @@ export function cue(mix, given) {
62
63
  checkChild(spec, voice);
63
64
  if (mix.pace !== null && owner === null && spec.start !== undefined && voice.state === 'pending')
64
65
  voice.pinned = spec.start - mix.offset;
65
- if (start === Number.POSITIVE_INFINITY && !anchored && !owner?.fitting) {
66
+ if (start === Number.POSITIVE_INFINITY &&
67
+ !anchored &&
68
+ !owner?.fitting &&
69
+ Number.isNaN(voice.pinned)) {
66
70
  voice.waking = true;
67
71
  mix.waking = true;
68
72
  }
@@ -180,10 +184,13 @@ export function playable(mix, spec) {
180
184
  if (spec.as !== undefined && mix.transport.pager !== null && !mix.opts.history?.revive)
181
185
  throw new Error('blits: a voice cued with as needs history.revive to rebuild it from');
182
186
  }
183
- /** A voice parked by a seek, cued again as the mix plays past its cue, on the handle it had. */
187
+ /**
188
+ * A voice parked by a seek, cued again as the mix plays past its cue, on the handle it had. A span's
189
+ * child waits for the fit its cue makes again, as it did when first cued: its start is the last fit's.
190
+ */
184
191
  function recue(mix, voice) {
185
192
  const owner = voice.owner;
186
- if ((owner === null ? mix.now : ownerReading(owner, mix.now)) >= voice.start)
193
+ if (!owner?.fitting && (owner === null ? mix.now : ownerReading(owner, mix.now)) >= voice.start)
187
194
  voice.state = 'live';
188
195
  enter(mix, voice);
189
196
  }
@@ -234,6 +241,8 @@ function enter(mix, voice) {
234
241
  voice.cuedAt = mix.now;
235
242
  voice.cuedSeq = mix.transport.seq;
236
243
  voice.opened = mix.now;
244
+ if (voice.state !== 'pending')
245
+ voice.begunSeq = mix.transport.seq;
237
246
  }
238
247
  voice.placing = anchored;
239
248
  // A start the host gave by its own clock, where the mix's rate puts it.
package/dist/due.d.ts CHANGED
@@ -7,6 +7,17 @@ import type { Voice } from './voice.js';
7
7
  */
8
8
  export declare class DueQueue<I, O> {
9
9
  size: number;
10
+ /**
11
+ * The entries due every frame, -Infinity, kept out of the heap in the order they came: an owned
12
+ * voice is filed so each frame, and sifting each to the top and off again cost more than its visit.
13
+ */
14
+ private every;
15
+ private everyTokens;
16
+ /** The list `takeEvery` last handed out, and its tokens: the next one fills it again. */
17
+ private spare;
18
+ private spareTokens;
19
+ /** Whether the list `takeEvery` last handed out was in id order. */
20
+ everySorted: boolean;
10
21
  private at;
11
22
  private tokens;
12
23
  private voices;
@@ -15,6 +26,11 @@ export declare class DueQueue<I, O> {
15
26
  push(at: number, voice: Voice<I, O>, token: number): void;
16
27
  /** The earliest entry's time, Infinity while empty. */
17
28
  first(): number;
29
+ /**
30
+ * The current entries due every frame, in the order they came, leaving none; `everySorted` says
31
+ * whether that was id order. The list is the queue's own, good until the next call.
32
+ */
33
+ takeEvery(): Voice<I, O>[];
18
34
  /** Takes off the earliest entry, giving its voice where it is current and undefined where stale. */
19
35
  pop(): Voice<I, O> | undefined;
20
36
  clear(): void;
@@ -22,6 +38,8 @@ export declare class DueQueue<I, O> {
22
38
  /** Puts an entry in the hole at `i` and sifts it down to where it belongs. */
23
39
  private sink;
24
40
  private grow;
41
+ /** Drops the entries due every frame made stale by a later one. */
42
+ private pruneEvery;
25
43
  /** Drops the entries made stale by a later one and heapifies what is left. */
26
44
  private prune;
27
45
  }
package/dist/due.js CHANGED
@@ -5,12 +5,30 @@
5
5
  */
6
6
  export class DueQueue {
7
7
  size = 0;
8
+ /**
9
+ * The entries due every frame, -Infinity, kept out of the heap in the order they came: an owned
10
+ * voice is filed so each frame, and sifting each to the top and off again cost more than its visit.
11
+ */
12
+ every = [];
13
+ everyTokens = [];
14
+ /** The list `takeEvery` last handed out, and its tokens: the next one fills it again. */
15
+ spare = [];
16
+ spareTokens = [];
17
+ /** Whether the list `takeEvery` last handed out was in id order. */
18
+ everySorted = true;
8
19
  at = new Float64Array(64);
9
20
  tokens = new Float64Array(64);
10
21
  voices = [];
11
22
  /** The size past which stale entries are pruned, so a voice rescheduled often cannot grow it. */
12
23
  limit = 64;
13
24
  push(at, voice, token) {
25
+ if (at === Number.NEGATIVE_INFINITY) {
26
+ if (this.every.length >= this.limit)
27
+ this.pruneEvery();
28
+ this.every.push(voice);
29
+ this.everyTokens.push(token);
30
+ return;
31
+ }
14
32
  if (this.size >= this.limit)
15
33
  this.prune();
16
34
  if (this.size === this.at.length)
@@ -30,8 +48,41 @@ export class DueQueue {
30
48
  }
31
49
  /** The earliest entry's time, Infinity while empty. */
32
50
  first() {
51
+ if (this.every.length > 0)
52
+ return Number.NEGATIVE_INFINITY;
33
53
  return this.size > 0 ? this.at[0] : Number.POSITIVE_INFINITY;
34
54
  }
55
+ /**
56
+ * The current entries due every frame, in the order they came, leaving none; `everySorted` says
57
+ * whether that was id order. The list is the queue's own, good until the next call.
58
+ */
59
+ takeEvery() {
60
+ const every = this.every;
61
+ const tokens = this.everyTokens;
62
+ let n = 0;
63
+ let sorted = true;
64
+ let last = Number.NEGATIVE_INFINITY;
65
+ for (let i = 0; i < every.length; i++) {
66
+ const voice = every[i];
67
+ if (tokens[i] !== voice.dueToken || voice.state === 'done')
68
+ continue;
69
+ // A voice filed twice keeps its last entry, so it is taken once.
70
+ voice.dueToken++;
71
+ if (voice.id < last)
72
+ sorted = false;
73
+ last = voice.id;
74
+ every[n++] = voice;
75
+ }
76
+ every.length = n;
77
+ tokens.length = 0;
78
+ this.spare.length = 0;
79
+ this.every = this.spare;
80
+ this.everyTokens = this.spareTokens;
81
+ this.spare = every;
82
+ this.spareTokens = tokens;
83
+ this.everySorted = sorted;
84
+ return every;
85
+ }
35
86
  /** Takes off the earliest entry, giving its voice where it is current and undefined where stale. */
36
87
  pop() {
37
88
  const voice = this.voices[0];
@@ -44,6 +95,10 @@ export class DueQueue {
44
95
  }
45
96
  clear() {
46
97
  this.size = 0;
98
+ this.every.length = 0;
99
+ this.everyTokens.length = 0;
100
+ this.spare.length = 0;
101
+ this.spareTokens.length = 0;
47
102
  this.voices = [];
48
103
  this.limit = 64;
49
104
  }
@@ -84,6 +139,22 @@ export class DueQueue {
84
139
  tokens.set(this.tokens);
85
140
  this.tokens = tokens;
86
141
  }
142
+ /** Drops the entries due every frame made stale by a later one. */
143
+ pruneEvery() {
144
+ const every = this.every;
145
+ const tokens = this.everyTokens;
146
+ let n = 0;
147
+ for (let i = 0; i < every.length; i++) {
148
+ const voice = every[i];
149
+ if (tokens[i] !== voice.dueToken)
150
+ continue;
151
+ every[n] = voice;
152
+ tokens[n++] = tokens[i];
153
+ }
154
+ every.length = n;
155
+ tokens.length = n;
156
+ this.limit = Math.max(this.limit, 2 * n);
157
+ }
87
158
  /** Drops the entries made stale by a later one and heapifies what is left. */
88
159
  prune() {
89
160
  let n = 0;
@@ -169,7 +240,9 @@ export function schedule(mix, voice) {
169
240
  /** Takes every current entry due by `now` off the queue, in cue order. */
170
241
  export function popDue(mix, now) {
171
242
  const queue = mix.due;
172
- const out = [];
243
+ const out = queue.takeEvery();
244
+ const sorted = queue.everySorted;
245
+ const every = out.length;
173
246
  while (queue.first() <= now) {
174
247
  const voice = queue.pop();
175
248
  if (voice === undefined)
@@ -177,7 +250,33 @@ export function popDue(mix, now) {
177
250
  voice.dueToken++;
178
251
  out.push(voice);
179
252
  }
180
- return byId(out);
253
+ if (every === 0 || !sorted)
254
+ return byId(out);
255
+ return every === out.length ? out : merged(out, every);
256
+ }
257
+ /** `list` in id order, its first `n` already so and the rest few: those sorted, then merged in. */
258
+ function merged(list, n) {
259
+ const rest = byId(list.slice(n));
260
+ const out = [];
261
+ let i = 0;
262
+ let k = 0;
263
+ while (i < n && k < rest.length) {
264
+ const a = list[i];
265
+ const b = rest[k];
266
+ if (b.id < a.id) {
267
+ out.push(b);
268
+ k++;
269
+ }
270
+ else {
271
+ out.push(a);
272
+ i++;
273
+ }
274
+ }
275
+ while (i < n)
276
+ out.push(list[i++]);
277
+ while (k < rest.length)
278
+ out.push(rest[k++]);
279
+ return out;
181
280
  }
182
281
  /** Past this many voices the built-in sort, which makes working arrays however few it is given. */
183
282
  const SHORT = 8;
package/dist/fade.d.ts CHANGED
@@ -3,7 +3,11 @@ import type { Left, Parted, Parting } from './subject.js';
3
3
  import type { Voice } from './voice.js';
4
4
  /** Starts one subject's ramp out of a voice, or takes it out at once for a ramp of 0. */
5
5
  export declare function fadeSubject<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>, subject: I, over?: number): void;
6
- /** What a subject's own ramp out of a voice leaves of its weight this frame, 0..1. */
6
+ /**
7
+ * What a subject's own ramp out of a voice leaves of its weight this frame. It also holds the
8
+ * voice's fade in where the ramp began, as a fade out of the whole voice does, so it is a ratio to
9
+ * the fade in the voice's envelope gives.
10
+ */
7
11
  export declare function parting<I, O>(this: Mixer<I, O>, voice: Voice<I, O>, subject: I, now: number): number;
8
12
  /**
9
13
  * Takes a subject out of one voice for good, as of mix time `at`: the voice forgets its record,
@@ -34,9 +38,15 @@ export declare function beginFade<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>, op
34
38
  }, at?: number): void;
35
39
  /**
36
40
  * Keeps a voice faded at the end of its passes with its owner, live and quiet, rather than letting
37
- * it leave; the owner leaves at `at` if it holds no child still playing.
41
+ * it leave; the owner, where it holds no child still playing, leaves at `at`, or stays quiet in
42
+ * turn where an owner holds it.
38
43
  */
39
44
  export declare function stay<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>, at: number): void;
45
+ /**
46
+ * Lets an owner holding no child still playing leave at `at`; under history one an owner holds
47
+ * stays with it instead, quiet while its children are.
48
+ */
49
+ export declare function release<I, O>(mix: Mixer<I, O>, owner: Voice<I, O>, at: number): void;
40
50
  /**
41
51
  * Turns a fading voice around: it climbs back from where its fade out had got to, up the same
42
52
  * curve, at the whole curve per `over` ms. A fade waiting for rest is taken back at once. A voice