@msb235/blits 0.8.1 → 0.9.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 (52) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/README.md +12 -2
  3. package/dist/clock.d.ts +10 -0
  4. package/dist/clock.js +23 -0
  5. package/dist/cue.js +5 -8
  6. package/dist/due.js +20 -13
  7. package/dist/everyone.js +3 -0
  8. package/dist/fade.d.ts +1 -0
  9. package/dist/fade.js +10 -4
  10. package/dist/fill.js +5 -0
  11. package/dist/fold.js +28 -22
  12. package/dist/gone.d.ts +4 -4
  13. package/dist/gone.js +11 -10
  14. package/dist/handle.js +17 -9
  15. package/dist/held.d.ts +10 -0
  16. package/dist/held.js +28 -1
  17. package/dist/hosts.js +5 -1
  18. package/dist/keyfill.js +5 -1
  19. package/dist/locus.d.ts +4 -1
  20. package/dist/locus.js +12 -2
  21. package/dist/marks.js +4 -4
  22. package/dist/motion.d.ts +6 -3
  23. package/dist/motions.d.ts +5 -2
  24. package/dist/motions.js +3 -1
  25. package/dist/move.js +51 -14
  26. package/dist/origin.js +3 -2
  27. package/dist/owner.d.ts +2 -0
  28. package/dist/owner.js +10 -2
  29. package/dist/place.js +17 -3
  30. package/dist/project.d.ts +1 -1
  31. package/dist/project.js +11 -6
  32. package/dist/record.js +2 -1
  33. package/dist/revive.d.ts +2 -0
  34. package/dist/revive.js +2 -0
  35. package/dist/seek.js +2 -2
  36. package/dist/spans.d.ts +5 -4
  37. package/dist/spans.js +11 -10
  38. package/dist/strays.js +13 -5
  39. package/dist/transport.js +4 -2
  40. package/dist/types/mix.d.ts +3 -2
  41. package/dist/types/patch.d.ts +7 -3
  42. package/dist/types/place.d.ts +4 -1
  43. package/dist/types/voice.d.ts +22 -4
  44. package/dist/unpage.d.ts +1 -1
  45. package/dist/unpage.js +2 -3
  46. package/dist/voice.d.ts +52 -0
  47. package/dist/voice.js +103 -2
  48. package/dist/warn.d.ts +2 -0
  49. package/dist/warn.js +17 -0
  50. package/dist/weigh.js +5 -0
  51. package/docs/schema.html +92 -29
  52. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -4,6 +4,88 @@ 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.9.0
8
+
9
+ ### Breaking
10
+
11
+ - A stock `last()` channel in a locus takes its heaviest member's value, and the later cued of
12
+ two as heavy. It stepped through its members in cue order, so three voices at weights 0.3,
13
+ 0.3, and 0.4 showed the second. Two members read as before.
14
+ - A placement whose `end` comes before its start is an instant: the voice starts and ends at its
15
+ start, shows nothing, and warns once outside production. Its `end` mark was reported ahead of
16
+ its `start`, so a voice anchored after it started before it did, and with a `fade.out` it
17
+ showed a whole fade from its start. An `end` that leaves less room than `fade.out` now
18
+ shortens the fade to finish when asked, where the fade ran its whole length past it, and an
19
+ `out` before the start is reported at the start, where its fade always began.
20
+ - A `to` or `push` that brings back a subject faded out of its voice is made at the frame of
21
+ the call, as one for a subject never faded is. It was made at the subject's next read, so how
22
+ far the motion had got depended on when the host next probed the subject.
23
+ - A handle `seek` on a pending voice holds: the voice starts on time, already at the sought
24
+ position, and one freezing before shows that frame while it waits. The seek was taken from
25
+ the moment of the call, and lost outright when an anchor or the mix's rate moved the start.
26
+ - A rate of 0, a negative rate, or a ramp set on a pending voice applies from its start, as a
27
+ rate above 0 always did. At rate 0 the voice starts on time and holds its first frame, where
28
+ it stopped its clock short of its start for good and never showed.
29
+
30
+ - A voice playing backward ends where it starts. When its clock runs back to 0, `played`
31
+ resolves and it leaves over `fade.out`, showing its first frame through the fade, or holds
32
+ that frame where it freezes before; its `coast`, `out`, and `end` marks are there. A voice at
33
+ a rate below 0 showed nothing once past its start and never left the mix. A stateful patch
34
+ steps once more to the moment its subject reached its start.
35
+ - A voice cued at a rate below 0 with no `seek` starts at its end, where it started at 0 and
36
+ was over at once.
37
+ - A fade in counts from no earlier than its voice's start. A subject whose delay had already run
38
+ out when the voice started (a negative `stagger`, or a seek made in the frame of the cue)
39
+ came in with its fade partway or done.
40
+
41
+ ### Added
42
+
43
+ - `seek` on a cue: where in itself the voice starts, in voice ms.
44
+ - A voice with no end cued backward, a loop for good or a patch with no duration, plays from 0
45
+ into the pass before, with `setting.pass` and `setting.elapsed` below 0. It runs on the
46
+ general path.
47
+ - `handle.ramp` through 0 eases a voice into reverse, or out of it.
48
+ - `@msb235/blits-tone`, a package in `packages/tone` that drives Tone.js from a mix on the audio
49
+ clock. Its own changelog has what it does.
50
+
51
+ ### Fixed
52
+
53
+ - `project(mix.now)` reads what `probe` does. It landed what the next sync would, so after a
54
+ handle `seek` past a voice's end it showed the voice gone while the mix still showed its last
55
+ frame, and after a fade it placed a voice anchored on the faded one a frame early.
56
+ - A read back of a voice that was pending counts each subject's origin from the controls the
57
+ voice had then, not from where a later seek, rate, or anchor has since put it.
58
+
59
+ - A seek back inside `history.ms` plays the host's calls again when the mix's rate is above 1.
60
+ The tape was let go of by mix time while its calls are stamped in host time, so at rate 2 a call
61
+ made 250 ms of host time before a seek back was already gone and never played again.
62
+ - A voice taken out with a time already older than history reaches, such as `fade({ at })` given
63
+ a moment long past, comes back for a seek or a read back to before that call. It was forgotten
64
+ at the next sync, so `seek` and `project` to the frame before the call found no voice.
65
+ - A `HistoryStore` is told of every seek back through `cut`, as its doc says. It was told only
66
+ when the seek read records from the store, so a mark announced with a past `at`, paged at once,
67
+ stayed in the store after a seek back to before it was announced.
68
+ - A `color(last())` channel on a lane switches off when its voice's weight falls to 0, as it does
69
+ off lanes. A lane left it on, so a weight coming back inside the band (0.4 to 0.6 by default)
70
+ showed the color where a mix with `lanes: false` showed none.
71
+
72
+ - A span's children play from where the one before each ended, not from the frame that noticed
73
+ it. Each child leaving fitted the rest again from the sync's time, so the next started up to a
74
+ frame late and the rest sped up to make room: 200 children of 10 ms in a 2000 ms span, fitted
75
+ with none skipped at cue, had 199 skipped once synced every 16 ms. A read ahead never fitted
76
+ again, so `project` showed a child retimed by `faster` up to a frame ahead of the mix.
77
+
78
+ - A voice whose owner runs backward leaves over its `fade.out` from where it reached its start,
79
+ as one turned back by its own handle does. An owner run back to its start left at once and
80
+ took every voice it held with it, so a child with a 100 ms `fade.out` vanished in one frame
81
+ and listed no `coast`, `out`, or `end` mark. An owner at its start now stays for what it
82
+ holds and leaves with its last child.
83
+
84
+ - `setting.dt` is the gap since the subject was last sampled for a patch with no `step`, and for
85
+ a weight signal, as its doc says. It was the time since the subject's origin, growing every
86
+ frame: a mix synced at 16, 48, and 64 handed such a patch 16, 48, and 64, and now hands it 16,
87
+ 32, and 16.
88
+
7
89
  ## 0.8.1
8
90
 
9
91
  ### Fixed
package/README.md CHANGED
@@ -116,9 +116,14 @@ state shared between subjects, a `step` that lands differently from the same sta
116
116
  [three.quarks](https://github.com/Alchemist0823/three.quarks) particles. It is published separately
117
117
  so the engine keeps no dependencies; its README says how it works.
118
118
 
119
+ `packages/tone` is `@msb235/blits-tone`, a driver that schedules a mix on the audio clock through
120
+ [Tone.js](https://tonejs.github.io): channels as parameter curves read ahead, and hits and marks at
121
+ their sample. It is published separately for the same reason, and its README says what it does not
122
+ do yet.
123
+
119
124
  ## Status
120
125
 
121
- v0.8.1, with five consumers. magicsmoke runs every fault on it, wod its wheel, tricks and
126
+ v0.9.0, with five consumers. magicsmoke runs every fault on it, wod its wheel, tricks and
122
127
  transitions, weasel its animator's tweens, sherpa its seams, and
123
128
  [klieg](https://github.com/orochi235/klieg) composes all three of its systems — letter motion,
124
129
  part effects, and the environment a sign is lit by — on a mix each.
@@ -135,7 +140,12 @@ Zero runtime dependencies, ESM only, types included.
135
140
 
136
141
  ```
137
142
  npm install
138
- npm run check # lint, typecheck, test
143
+ npm run check # lint, typecheck, test
144
+ npm run test:general # the tests again with lanes off
139
145
  ```
140
146
 
147
+ A lane is a channel the mix computes for every subject at once, and the general path is the same
148
+ fold made subject by subject. `test:general` turns lanes off in every mix that does not set `lanes`
149
+ itself, so the whole suite runs on the general path.
150
+
141
151
  MIT.
package/dist/clock.d.ts CHANGED
@@ -69,6 +69,16 @@ export interface Clock {
69
69
  export declare function elapsedWith(c: Clock, now: number): number;
70
70
  /** The time a clock reads `elapsed`, inverting `elapsedWith`; Infinity where it never will. */
71
71
  export declare function timeWith(c: Clock, elapsed: number): number;
72
+ /**
73
+ * The time a clock running back reads `elapsed`: `timeWith` for the clock that reads the negative
74
+ * of this one, which runs forward where this runs back.
75
+ */
76
+ export declare function backWith(c: Clock, elapsed: number): number;
77
+ /**
78
+ * The phase of a time before 0, for a loop with no first pass: where the pass before 0, and each
79
+ * one before that, would read it.
80
+ */
81
+ export declare function phaseBefore(elapsed: number, duration: number): number;
72
82
  /** A clock's rate at `now`. */
73
83
  export declare function rateWith(c: Clock, now: number): number;
74
84
  /** Moves a clock's anchor to `now`, carrying what is left of a ramp. */
package/dist/clock.js CHANGED
@@ -125,6 +125,29 @@ export function timeWith(c, elapsed) {
125
125
  return Number.POSITIVE_INFINITY;
126
126
  return c.anchorNow + r.over + (elapsed - atEnd) / r.to;
127
127
  }
128
+ /**
129
+ * The time a clock running back reads `elapsed`: `timeWith` for the clock that reads the negative
130
+ * of this one, which runs forward where this runs back.
131
+ */
132
+ export function backWith(c, elapsed) {
133
+ const r = c.ramp;
134
+ return timeWith({
135
+ anchorNow: c.anchorNow,
136
+ anchorElapsed: -c.anchorElapsed,
137
+ rate: -c.rate,
138
+ ramp: r === null ? null : { from: -r.from, to: -r.to, over: r.over },
139
+ }, -elapsed);
140
+ }
141
+ /**
142
+ * The phase of a time before 0, for a loop with no first pass: where the pass before 0, and each
143
+ * one before that, would read it.
144
+ */
145
+ export function phaseBefore(elapsed, duration) {
146
+ if (!(duration > 0))
147
+ return 0;
148
+ const into = elapsed % duration;
149
+ return (into < 0 ? into + duration : into) / duration;
150
+ }
128
151
  /** A clock's rate at `now`. */
129
152
  export function rateWith(c, now) {
130
153
  const r = c.ramp;
package/dist/cue.js CHANGED
@@ -4,7 +4,7 @@ import { copyable } from './clone.js';
4
4
  import { schedule } from './due.js';
5
5
  import { shares } from './everyone.js';
6
6
  import { finiteRate, plainWeight, VoiceHandle } from './handle.js';
7
- import { reorigin } from './held.js';
7
+ import { begin } from './held.js';
8
8
  import { handle } from './hosts.js';
9
9
  import { motionOf } from './motion.js';
10
10
  import { adopt, ownerPatch, ownerReading } from './owner.js';
@@ -76,6 +76,8 @@ export function playable(mix, spec) {
76
76
  throw new RangeError(`blits: loop takes true, false or a whole number of passes, not ${loop}`);
77
77
  if (spec.rate !== undefined)
78
78
  finiteRate(spec.rate);
79
+ if (spec.seek !== undefined && !Number.isFinite(spec.seek))
80
+ throw new RangeError(`blits: a voice's seek is a finite number of ms, not ${spec.seek}`);
79
81
  if (typeof spec.weight === 'number')
80
82
  plainWeight(spec.weight);
81
83
  fitsKit(patch, mix.kit);
@@ -150,13 +152,8 @@ function enter(mix, voice) {
150
152
  mix.wantsPose = true;
151
153
  if (anchor) {
152
154
  place(mix);
153
- if (!Number.isNaN(mix.now) &&
154
- voice.state === 'pending' &&
155
- localNow(mix, voice) >= voice.start) {
156
- voice.state = 'live';
157
- reorigin(mix, voice, mix.now);
158
- changed(mix, voice);
159
- }
155
+ if (!Number.isNaN(mix.now) && voice.state === 'pending' && localNow(mix, voice) >= voice.start)
156
+ begin(mix, voice, mix.now);
160
157
  }
161
158
  if (owner?.fitting)
162
159
  refit(mix, owner);
package/dist/due.js CHANGED
@@ -20,6 +20,9 @@ function dueOf(mix, voice) {
20
20
  due = Math.min(due, back.at + (1 - back.from) * back.over);
21
21
  if (voice.state === 'pending')
22
22
  return Math.min(due, Number.isNaN(voice.start) ? due : voice.start);
23
+ // A ramp through 0 turns the clock around, which is looked for each frame until it has.
24
+ if (voice.turns)
25
+ return Number.NEGATIVE_INFINITY;
23
26
  if (voice.state === 'fading') {
24
27
  const out = voice.out;
25
28
  if (out === null || out.rest)
@@ -27,20 +30,24 @@ function dueOf(mix, voice) {
27
30
  return Math.min(due, out.at + out.over);
28
31
  }
29
32
  due = Math.min(due, voice.outAt);
30
- if (Number.isFinite(voice.span)) {
31
- const end = voice.span + voice.latest;
32
- // A frozen voice goes live again once its clock is back before its end: by a seek, by a
33
- // subject staggered later than any before, or by running backwards, which is checked each
34
- // frame while it can.
35
- if (voice.state === 'frozen') {
36
- if (voice.rate <= 0 || voice.ramp !== null)
37
- return Number.NEGATIVE_INFINITY;
38
- if (!Number.isNaN(mix.now) && voice.elapsedAt(mix.now) < end)
39
- return Number.NEGATIVE_INFINITY;
40
- }
41
- if (voice.state === 'live')
42
- due = Math.min(due, voice.timeAt(end));
33
+ if (voice.state === 'frozen') {
34
+ const at = Number.isNaN(mix.now) ? Number.NaN : voice.elapsedAt(mix.now);
35
+ // Frozen at its start, having run back to it, it stays while its rate is never above 0,
36
+ // until a handle or a subject staggered ahead of every other moves it.
37
+ const r = voice.ramp;
38
+ const never = r === null ? voice.rate <= 0 : r.from <= 0 && r.to <= 0;
39
+ if (never && !voice.beginless && at <= voice.soonest)
40
+ return due;
41
+ // Frozen at its end, it goes live again once its clock is back before it: by a seek, by a
42
+ // subject staggered later than any before, or by running back, which is checked each frame
43
+ // while it can.
44
+ if (voice.rate <= 0 || r !== null)
45
+ return Number.NEGATIVE_INFINITY;
46
+ if (at < voice.span + voice.latest)
47
+ return Number.NEGATIVE_INFINITY;
43
48
  }
49
+ if (voice.state === 'live')
50
+ due = Math.min(due, voice.endsAt());
44
51
  return due;
45
52
  }
46
53
  /** Files a voice in the due queue at its current due, replacing any entry it had. */
package/dist/everyone.js CHANGED
@@ -32,6 +32,7 @@ export function shares(mix, voice) {
32
32
  voice.holding === null &&
33
33
  voice.blend === null &&
34
34
  !voice.freezesBefore &&
35
+ !voice.beginless &&
35
36
  voice.slots.every((s) => {
36
37
  const channel = mix.channels[s];
37
38
  return channel.rest !== undefined && numericOf(channel)?.op !== 'last';
@@ -113,6 +114,8 @@ export function open(voice, rec, slot) {
113
114
  return;
114
115
  const stamps = voice.stamps;
115
116
  const o = slot * 2 /* Stamp.SIZE */;
117
+ // A subject never read counts its `dt` from the voice's origin, not another subject's last call.
118
+ rec.stepped = rec.since;
116
119
  if (stamps === null || o >= stamps.length) {
117
120
  rec.probed = Number.NaN;
118
121
  rec.seeks = 0;
package/dist/fade.d.ts CHANGED
@@ -21,6 +21,7 @@ export declare function beginFade<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>, op
21
21
  over?: number;
22
22
  at?: 'rest';
23
23
  deadline?: number;
24
+ back?: boolean;
24
25
  }, at?: number): void;
25
26
  /**
26
27
  * Turns a fading voice around: it climbs back from where its fade out had got to, up the same
package/dist/fade.js CHANGED
@@ -104,7 +104,8 @@ export function beginFade(mix, voice, opts, at) {
104
104
  return;
105
105
  const over = mix.reduced ? 0 : (opts.over ?? voice.fade.out ?? 0);
106
106
  // A voice is linked into its subjects' chains once it plays, and a fading one plays.
107
- if (voice.state === 'pending')
107
+ const waiting = voice.state === 'pending';
108
+ if (waiting)
108
109
  changed(mix, voice);
109
110
  // A fade at rest marks each subject's record as it comes to rest, at its next read: one already
110
111
  // read this frame is read again, as after `touch`.
@@ -120,8 +121,11 @@ export function beginFade(mix, voice, opts, at) {
120
121
  rest: opts.at === 'rest',
121
122
  deadline: opts.deadline,
122
123
  };
124
+ if (opts.back)
125
+ voice.out.back = true;
123
126
  noted(mix, voice);
124
- if (opts.at === 'rest')
127
+ // A fade at rest, from a voice's start, or of a voice yet to start takes it off its lane.
128
+ if (opts.at === 'rest' || opts.back || waiting)
125
129
  mix.lanes?.invalidate();
126
130
  else
127
131
  mix.lanes?.refill();
@@ -190,7 +194,9 @@ export function retire(mix, voice, at) {
190
194
  // The record its setting last wrote to, which a retired voice no longer calls for.
191
195
  voice.keepOn = null;
192
196
  mix.retired.push(voice);
193
- voice.doneAt = at ?? (Number.isNaN(mix.now) ? Number.NEGATIVE_INFINITY : mix.now);
197
+ const now = Number.isNaN(mix.now) ? Number.NEGATIVE_INFINITY : mix.now;
198
+ voice.doneAt = at ?? now;
199
+ voice.decidedAt = now;
194
200
  voice.doneSeq = mix.transport.seq;
195
201
  // The frames up to this one showed it, though its fade ended before: it leaves the list now.
196
202
  mix.transport.settle();
@@ -209,6 +215,6 @@ export function retire(mix, voice, at) {
209
215
  // A child the fit skipped had no time in it, so the rest of the fit stands: fitting again
210
216
  // between siblings skipped together would play the ones still to leave.
211
217
  else if (span.fitting !== null && span.state !== 'done' && !voice.skipped)
212
- refit(mix, span);
218
+ refit(mix, span, voice.doneAt);
213
219
  }
214
220
  }
package/dist/fill.js CHANGED
@@ -3,6 +3,7 @@ import { passAt, phaseAt, same, silent, weighed } from './clock.js';
3
3
  import { flush } from './columns.js';
4
4
  import { crowdsUpTo, freshen } from './crowd.js';
5
5
  import { open as openRecord, shut as shutRecord } from './everyone.js';
6
+ import { unband } from './fold.js';
6
7
  import { foldLocus, gatherLocus } from './gather.js';
7
8
  import { runKeys } from './keyfill.js';
8
9
  import { frozenAt } from './lane.js';
@@ -301,6 +302,7 @@ export function one(lane, p, slot) {
301
302
  const elapsed = frozenAt(voice, elapsedNow - delay);
302
303
  if (!(elapsed >= 0)) {
303
304
  data[o + 2 /* Row.WEIGHT */] = 0;
305
+ unband(lane.records[p]);
304
306
  return false;
305
307
  }
306
308
  if (!lane.placed || !same(elapsed, lane.placedAt)) {
@@ -322,6 +324,7 @@ export function one(lane, p, slot) {
322
324
  arg[5 /* Arg.FADE */] = lane.fade;
323
325
  if (!this.signalled(voice, slot, lane.records[p])) {
324
326
  data[o + 2 /* Row.WEIGHT */] = 0;
327
+ unband(lane.records[p]);
325
328
  return false;
326
329
  }
327
330
  w = arg[0 /* Arg.WEIGHT */];
@@ -329,6 +332,8 @@ export function one(lane, p, slot) {
329
332
  else
330
333
  w = weighed(voice.weight, lane.fade, this.parting(voice, slot));
331
334
  data[o + 2 /* Row.WEIGHT */] = w;
335
+ if (!(w > 0))
336
+ unband(lane.records[p]);
332
337
  this.folding = lane;
333
338
  this.rec = lane.records[p] ?? null;
334
339
  if (voice.built !== null) {
package/dist/fold.js CHANGED
@@ -1,4 +1,4 @@
1
- import { frozenTime, passAt, phaseAt, silent } from './clock.js';
1
+ import { passAt, phaseAt, phaseBefore, silent } 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
4
  import { recordHost, remember } from './history.js';
@@ -69,16 +69,15 @@ export function contribution(voice, subject, now, held) {
69
69
  return null;
70
70
  if (held.rebuilt !== voice.rebuilds)
71
71
  rebuild(this, voice, subject, now, held);
72
- const raw = voice.elapsedAt(now) - held.delay;
73
- // Not `raw < 0`: see `frozenTime` for the clock that reads NaN before its start.
74
- const early = !(raw >= 0);
75
- if (early && !voice.freezesBefore)
72
+ const raw = voice.rawAt(now, held.delay);
73
+ const early = voice.before(raw, now);
74
+ if (early && !voice.holdsFirst)
76
75
  return null;
77
- const elapsed = frozenTime(raw, voice.freezesBefore, voice.freezesAfter, voice.span);
76
+ const elapsed = voice.shownAt(raw);
78
77
  // A frozen subject's clock stands still at the edge it freezes at: -1 before, 1 after, 0 playing.
79
78
  const still = early ? -1 : voice.freezesAfter && raw > voice.span ? 1 : 0;
80
79
  const duration = voice.duration;
81
- const phase = phaseAt(elapsed, duration, voice.passes);
80
+ const phase = elapsed < 0 ? phaseBefore(elapsed, duration) : phaseAt(elapsed, duration, voice.passes);
82
81
  const pass = passAt(elapsed, duration, voice.passes);
83
82
  this.prime(voice, held, now, elapsed, pass);
84
83
  const setting = voice.setting;
@@ -102,20 +101,27 @@ export function contribution(voice, subject, now, held) {
102
101
  recordHost(this, now);
103
102
  reading.horizon = this.horizonFor(voice, held.delay, now);
104
103
  const tick = this.opts.stepMs;
105
- if (voice.patch.step && held.probed !== now && still >= 0) {
106
- // Frozen after, it steps once more to where its last pass ended, and no further.
107
- const to = still === 1 ? voice.timeAt(held.delay + voice.span) : now;
108
- if (held.stepped < held.since) {
109
- held.stepped = held.since;
110
- setting.dt = this.capped(now - held.since);
111
- }
112
- if (tick !== undefined && tick > 0 && !this.reducedNow)
113
- this.tick(voice, subject, held, tick, to);
114
- else if (still === 0 ? now !== held.stepped : to > held.stepped) {
115
- if (still === 1)
116
- setting.dt = this.capped(to - held.stepped);
117
- voice.patch.step(held.state, setting.dt, subject, setting);
118
- held.stepped = to;
104
+ if (voice.patch.step && held.probed !== now) {
105
+ // Frozen after, it steps once more to where its last pass ended, and no further; run back
106
+ // past its start, to where it reached it. Before a start it has yet to reach, it does not step.
107
+ const to = still === 0
108
+ ? now
109
+ : still === 1
110
+ ? voice.timeAt(held.delay + voice.span)
111
+ : voice.ranBackTo(held.delay, now);
112
+ if (!Number.isNaN(to)) {
113
+ if (held.stepped < held.since) {
114
+ held.stepped = held.since;
115
+ setting.dt = this.capped(now - held.since);
116
+ }
117
+ if (tick !== undefined && tick > 0 && !this.reducedNow)
118
+ this.tick(voice, subject, held, tick, to);
119
+ else if (still === 0 ? now !== held.stepped : to > held.stepped) {
120
+ if (still !== 0)
121
+ setting.dt = this.capped(to - held.stepped);
122
+ voice.patch.step(held.state, setting.dt, subject, setting);
123
+ held.stepped = to;
124
+ }
119
125
  }
120
126
  }
121
127
  let delta = null;
@@ -648,7 +654,7 @@ export function read(voice, subject, now, dry, held) {
648
654
  if (held.reaches && held.probed === now && held.delta && held.seeks === voice.seeks) {
649
655
  const setting = voice.setting;
650
656
  // The setting the probe that read the delta had, but for `dt`: a dry read advances nothing.
651
- const elapsed = frozenTime(voice.elapsedAt(now) - held.delay, voice.freezesBefore, voice.freezesAfter, voice.span);
657
+ const elapsed = voice.shownAt(voice.rawAt(now, held.delay));
652
658
  setting.timestamp = now;
653
659
  setting.dt = 0;
654
660
  setting.elapsed = elapsed;
package/dist/gone.d.ts CHANGED
@@ -13,7 +13,7 @@ export declare class GoneIndex<I, O> {
13
13
  count: number;
14
14
  general: Voice<I, O>[];
15
15
  named: Store<I, Voice<I, O>[]>;
16
- /** The latest `doneAt` in `of`, and how far any voice finished before one listed ahead of it. */
16
+ /** The latest `reachedTo` in `of`, and how far any voice's falls before one listed ahead of it. */
17
17
  latest: number;
18
18
  slack: number;
19
19
  }
@@ -25,8 +25,8 @@ export declare function goneIndex<I, O>(mix: Mixer<I, O>): GoneIndex<I, O>;
25
25
  */
26
26
  export declare function keepGone<I, O>(mix: Mixer<I, O>, keep: (v: Voice<I, O>) => boolean): void;
27
27
  /**
28
- * Lets go of the gone voices that finished before `reach`. Voices go in about the order they
29
- * finish, so only the front can hold one: past the point where a voice finished `slack` after
30
- * `reach`, none behind it finished before `reach`.
28
+ * Lets go of the gone voices no seek or read back to `reach` finds playing: those that finished
29
+ * before it, in a frame before it. Voices go in about that order, so only the front can hold one:
30
+ * past the point where a voice is reached `slack` after `reach`, none behind it falls before.
31
31
  */
32
32
  export declare function expireGone<I, O>(mix: Mixer<I, O>, reach: number): void;
package/dist/gone.js CHANGED
@@ -12,15 +12,16 @@ export class GoneIndex {
12
12
  count = 0;
13
13
  general = [];
14
14
  named = new Store();
15
- /** The latest `doneAt` in `of`, and how far any voice finished before one listed ahead of it. */
15
+ /** The latest `reachedTo` in `of`, and how far any voice's falls before one listed ahead of it. */
16
16
  latest = Number.NEGATIVE_INFINITY;
17
17
  slack = 0;
18
18
  }
19
19
  function add(ix, voice) {
20
- if (voice.doneAt < ix.latest)
21
- ix.slack = Math.max(ix.slack, ix.latest - voice.doneAt);
20
+ const to = voice.reachedTo;
21
+ if (to < ix.latest)
22
+ ix.slack = Math.max(ix.slack, ix.latest - to);
22
23
  else
23
- ix.latest = voice.doneAt;
24
+ ix.latest = to;
24
25
  if (voice.named === null) {
25
26
  ix.general.push(voice);
26
27
  return;
@@ -107,14 +108,14 @@ export function keepGone(mix, keep) {
107
108
  ix.count = kept.length;
108
109
  }
109
110
  /**
110
- * Lets go of the gone voices that finished before `reach`. Voices go in about the order they
111
- * finish, so only the front can hold one: past the point where a voice finished `slack` after
112
- * `reach`, none behind it finished before `reach`.
111
+ * Lets go of the gone voices no seek or read back to `reach` finds playing: those that finished
112
+ * before it, in a frame before it. Voices go in about that order, so only the front can hold one:
113
+ * past the point where a voice is reached `slack` after `reach`, none behind it falls before.
113
114
  */
114
115
  export function expireGone(mix, reach) {
115
116
  const ix = goneIndex(mix);
116
117
  if (!Number.isFinite(ix.slack)) {
117
- keepGone(mix, (v) => v.doneAt >= reach);
118
+ keepGone(mix, (v) => v.reachedTo >= reach);
118
119
  return;
119
120
  }
120
121
  const list = mix.gone;
@@ -122,7 +123,7 @@ export function expireGone(mix, reach) {
122
123
  let end = 0;
123
124
  let any = false;
124
125
  for (let seen = Number.NEGATIVE_INFINITY; end < list.length && seen < edge; end++) {
125
- const done = list[end].doneAt;
126
+ const done = list[end].reachedTo;
126
127
  if (done < reach)
127
128
  any = true;
128
129
  if (done > seen)
@@ -134,7 +135,7 @@ export function expireGone(mix, reach) {
134
135
  const out = [];
135
136
  for (let i = 0; i < end; i++) {
136
137
  const v = list[i];
137
- (v.doneAt < reach ? out : kept).push(v);
138
+ (v.reachedTo < reach ? out : kept).push(v);
138
139
  }
139
140
  list.splice(0, end, ...kept);
140
141
  remove(ix, out);
package/dist/handle.js CHANGED
@@ -85,12 +85,17 @@ export class VoiceHandle {
85
85
  if (voice === null)
86
86
  return;
87
87
  voice.written = true;
88
- const now = this.#host.nowFor(voice);
89
- // A pending voice's clock reads 0 at its start, and a rate taken from now would cross 0 sooner:
90
- // a subject it freezes before would play, and step, ahead of a voice that has not started.
91
- const u = voice.state === 'pending' && voice.start > now && r > 0 ? voice.start : now;
92
- leave(voice, u);
93
- retime(voice, u, r, over);
88
+ if (voice.state === 'pending') {
89
+ // Its clock is anchored at its start, wherever that comes, and its rate is taken from there.
90
+ const from = voice.ramp === null ? voice.rate : voice.ramp.from;
91
+ voice.ramp = over > 0 && r !== from ? { from, to: r, over } : null;
92
+ voice.rate = r;
93
+ }
94
+ else {
95
+ const now = this.#host.nowFor(voice);
96
+ leave(voice, now);
97
+ retime(voice, now, r, over);
98
+ }
94
99
  this.#host.changed(voice);
95
100
  this.#host.record('rate', () => this.ramp(r, over));
96
101
  }
@@ -99,9 +104,12 @@ export class VoiceHandle {
99
104
  if (voice === null)
100
105
  return 'exact';
101
106
  voice.written = true;
102
- const u = this.#host.nowFor(voice);
103
- leave(voice, u);
104
- voice.rebase(u);
107
+ // A pending voice's clock is anchored at its start, so it starts there on time.
108
+ if (voice.state !== 'pending') {
109
+ const u = this.#host.nowFor(voice);
110
+ leave(voice, u);
111
+ voice.rebase(u);
112
+ }
105
113
  voice.anchorElapsed = elapsed;
106
114
  voice.seeks++;
107
115
  const doubt = this.#host.sought(voice, opts?.state !== 'keep');
package/dist/held.d.ts CHANGED
@@ -9,7 +9,17 @@ export declare function held<I, O>(this: Mixer<I, O>, voice: Voice<I, O>, subjec
9
9
  * counts from its last seek that rebuilt state, as its records do.
10
10
  */
11
11
  export declare function sinceOf<I, O>(this: Mixer<I, O>, voice: Voice<I, O>, delay: number): number;
12
+ /**
13
+ * The mix time a subject's fade in counts from: `since`, but no earlier than its voice starts,
14
+ * where a voice sought ahead of its start has a subject's delay run out before it; and from when
15
+ * the voice first showed, for one freezing before.
16
+ */
12
17
  export declare function shownOf<I, O>(this: Mixer<I, O>, voice: Voice<I, O>, since: number): number;
18
+ /**
19
+ * A pending voice starts at `now`. Starting anywhere but 0 it was sought there, so a record made
20
+ * while it waited is made again for where its clock is.
21
+ */
22
+ export declare function begin<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>, now: number): void;
13
23
  /**
14
24
  * A voice's start or clock moved, or it went live at `now`: the records whose `since` was still
15
25
  * ahead count from where its clock now puts them.
package/dist/held.js CHANGED
@@ -1,6 +1,8 @@
1
+ import { changed } from './chain.js';
1
2
  import { schedule } from './due.js';
2
3
  import { everyoneOf, inherit, sighted } from './everyone.js';
3
4
  import { originOf } from './origin.js';
5
+ import { startOf } from './place.js';
4
6
  import { record } from './record.js';
5
7
  import { unreach, unreached } from './unreached.js';
6
8
  /** The record a voice gives every subject it does not reach. */
@@ -82,6 +84,11 @@ export function held(voice, subject, slot) {
82
84
  if (voice.state === 'frozen')
83
85
  schedule(this, voice);
84
86
  }
87
+ if (delay < voice.soonest) {
88
+ voice.soonest = delay;
89
+ // An earlier start moves where a voice running back ends.
90
+ schedule(this, voice);
91
+ }
85
92
  return held;
86
93
  }
87
94
  /**
@@ -92,8 +99,28 @@ export function sinceOf(voice, delay) {
92
99
  const p = voice.patch;
93
100
  return originOf(voice, delay, p.state !== undefined || p.step !== undefined || voice.keeping);
94
101
  }
102
+ /**
103
+ * The mix time a subject's fade in counts from: `since`, but no earlier than its voice starts,
104
+ * where a voice sought ahead of its start has a subject's delay run out before it; and from when
105
+ * the voice first showed, for one freezing before.
106
+ */
95
107
  export function shownOf(voice, since) {
96
- return voice.freezesBefore && voice.opened < since ? voice.opened : since;
108
+ const start = startOf(voice);
109
+ const from = since < start ? start : since;
110
+ return voice.freezesBefore && voice.opened < from ? voice.opened : from;
111
+ }
112
+ /**
113
+ * A pending voice starts at `now`. Starting anywhere but 0 it was sought there, so a record made
114
+ * while it waited is made again for where its clock is.
115
+ */
116
+ export function begin(mix, voice, now) {
117
+ voice.state = 'live';
118
+ if (voice.anchorElapsed !== 0) {
119
+ voice.seeks++;
120
+ voice.rebuilds++;
121
+ }
122
+ reorigin(mix, voice, now);
123
+ changed(mix, voice);
97
124
  }
98
125
  /**
99
126
  * A voice's start or clock moved, or it went live at `now`: the records whose `since` was still
package/dist/hosts.js CHANGED
@@ -111,7 +111,11 @@ export function fits(mix, voice) {
111
111
  const patch = voice.patch;
112
112
  if (voice.keeping)
113
113
  return false;
114
- if (voice.state === 'pending' && voice.freezesBefore)
114
+ // Waiting to start, a voice shows the frame it will start on, which no fill reads its clock for.
115
+ if ((voice.freezesBefore || voice.state === 'fading') && voice.unstarted(mix.now))
116
+ return false;
117
+ // A fill takes a time before 0 as before a subject's start, which no time is for this voice.
118
+ if (voice.beginless || voice.out?.back === true)
115
119
  return false;
116
120
  // A fill weighs a voice's owners once for every subject, which a signal on one would not be.
117
121
  if (voice.owner !== null && signalled(voice))