@msb235/blits 0.5.0 → 0.7.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 (99) hide show
  1. package/CHANGELOG.md +163 -0
  2. package/README.md +32 -8
  3. package/dist/bare.js +7 -1
  4. package/dist/book.d.ts +8 -1
  5. package/dist/book.js +35 -4
  6. package/dist/chain.d.ts +4 -3
  7. package/dist/chain.js +10 -6
  8. package/dist/channels.d.ts +12 -19
  9. package/dist/channels.js +18 -118
  10. package/dist/clock.d.ts +12 -5
  11. package/dist/clock.js +13 -5
  12. package/dist/closed.d.ts +15 -0
  13. package/dist/closed.js +144 -0
  14. package/dist/color.d.ts +61 -0
  15. package/dist/color.js +229 -0
  16. package/dist/columns.js +2 -1
  17. package/dist/crowd.d.ts +4 -4
  18. package/dist/crowd.js +11 -8
  19. package/dist/cue.d.ts +2 -0
  20. package/dist/cue.js +78 -27
  21. package/dist/due.js +5 -2
  22. package/dist/fade.d.ts +13 -0
  23. package/dist/fade.js +55 -4
  24. package/dist/fill.d.ts +1 -1
  25. package/dist/fill.js +17 -10
  26. package/dist/fit.d.ts +144 -0
  27. package/dist/fit.js +181 -0
  28. package/dist/fold.d.ts +3 -3
  29. package/dist/fold.js +25 -18
  30. package/dist/gather.d.ts +6 -0
  31. package/dist/gather.js +18 -0
  32. package/dist/handle.d.ts +27 -2
  33. package/dist/handle.js +60 -11
  34. package/dist/held.js +7 -3
  35. package/dist/history.d.ts +15 -2
  36. package/dist/history.js +54 -9
  37. package/dist/hosts.d.ts +2 -2
  38. package/dist/hosts.js +19 -6
  39. package/dist/index.d.ts +10 -2
  40. package/dist/index.js +6 -1
  41. package/dist/lane.d.ts +4 -4
  42. package/dist/lane.js +5 -5
  43. package/dist/lanes.d.ts +32 -9
  44. package/dist/lanes.js +53 -24
  45. package/dist/locus.d.ts +3 -3
  46. package/dist/locus.js +1 -1
  47. package/dist/marks.d.ts +3 -0
  48. package/dist/marks.js +81 -13
  49. package/dist/meet.d.ts +3 -2
  50. package/dist/meet.js +15 -3
  51. package/dist/mixer.d.ts +55 -35
  52. package/dist/mixer.js +173 -83
  53. package/dist/motion.d.ts +2 -199
  54. package/dist/motion.js +2 -652
  55. package/dist/motions.d.ts +266 -0
  56. package/dist/motions.js +724 -0
  57. package/dist/move.d.ts +1 -1
  58. package/dist/move.js +27 -11
  59. package/dist/owner.d.ts +2 -2
  60. package/dist/owner.js +5 -5
  61. package/dist/pace.d.ts +2 -0
  62. package/dist/pace.js +8 -0
  63. package/dist/pack.d.ts +31 -0
  64. package/dist/pack.js +65 -0
  65. package/dist/paging.d.ts +63 -0
  66. package/dist/paging.js +110 -0
  67. package/dist/patch.d.ts +3 -0
  68. package/dist/patch.js +2 -0
  69. package/dist/place.js +74 -25
  70. package/dist/project.d.ts +7 -4
  71. package/dist/project.js +117 -78
  72. package/dist/pull.js +4 -3
  73. package/dist/qualify.js +4 -2
  74. package/dist/revive.d.ts +58 -0
  75. package/dist/revive.js +158 -0
  76. package/dist/sample.d.ts +1 -1
  77. package/dist/sample.js +6 -1
  78. package/dist/seek.d.ts +3 -0
  79. package/dist/seek.js +311 -0
  80. package/dist/signals.d.ts +3 -2
  81. package/dist/signals.js +12 -6
  82. package/dist/spans.d.ts +29 -0
  83. package/dist/spans.js +165 -0
  84. package/dist/strays.d.ts +1 -1
  85. package/dist/strays.js +21 -0
  86. package/dist/tape.d.ts +21 -0
  87. package/dist/tape.js +65 -0
  88. package/dist/ticker.d.ts +73 -0
  89. package/dist/ticker.js +115 -0
  90. package/dist/transport.d.ts +121 -0
  91. package/dist/transport.js +271 -0
  92. package/dist/types.d.ts +427 -50
  93. package/dist/unpage.d.ts +8 -0
  94. package/dist/unpage.js +163 -0
  95. package/dist/voice.d.ts +78 -12
  96. package/dist/voice.js +126 -12
  97. package/dist/weigh.js +2 -2
  98. package/docs/schema.html +140 -48
  99. package/package.json +2 -1
package/dist/tape.js ADDED
@@ -0,0 +1,65 @@
1
+ import { move } from './move.js';
2
+ /** What a recorded call's `invert` gives: a mix goes back by restoring, never by undoing a call. */
3
+ const undone = { apply() { }, invert: () => undone };
4
+ /** The tape `history.tape` makes for a transport, stamping each call with the mix time it was made at. */
5
+ export function tapeOf(transport) {
6
+ const make = transport.history?.tape;
7
+ if (make === undefined)
8
+ return undefined;
9
+ return make(transport, {
10
+ now: () => (Number.isNaN(transport.now) ? Number.NEGATIVE_INFINITY : transport.now),
11
+ branching: true,
12
+ coalesceWindowMs: 0,
13
+ });
14
+ }
15
+ /** Records a host call, which `again` makes once more when the mix plays past it after a seek. */
16
+ export function record(on, label, again) {
17
+ const tape = on.tape;
18
+ if (tape === undefined || on.replaying || on.projecting)
19
+ return;
20
+ const op = {
21
+ label,
22
+ apply() {
23
+ again();
24
+ },
25
+ invert: () => undone,
26
+ };
27
+ tape.recordEntry([op], label);
28
+ }
29
+ /** The host time at which the mix clock reads `t`, under the rate it has now. */
30
+ export function hostAt(clock, t) {
31
+ return clock.pace === null ? t : clock.pace.timeOf(t);
32
+ }
33
+ /**
34
+ * Makes the recorded calls due by mix time `to()` again, each with every mix on the transport
35
+ * moved to the time it was made at, as the host made it then. `to` is asked again after every
36
+ * call, since a call may change the rate and so where a host time lands.
37
+ */
38
+ export function replay(transport, to) {
39
+ const tape = transport.tape;
40
+ if (tape === undefined)
41
+ return;
42
+ const u = transport.u;
43
+ try {
44
+ for (;;) {
45
+ const at = tape.timestampAt(tape.undoDepth());
46
+ if (at === undefined || !(at <= to()))
47
+ return;
48
+ if (at > transport.now) {
49
+ transport.u = hostAt(transport, at);
50
+ for (const m of transport.members)
51
+ move(m, at);
52
+ }
53
+ transport.replaying = true;
54
+ try {
55
+ tape.redo();
56
+ }
57
+ finally {
58
+ transport.replaying = false;
59
+ }
60
+ }
61
+ }
62
+ finally {
63
+ transport.u = u;
64
+ }
65
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * What a ticker runs: a mix, or anything that syncs, sleeps on `inert` and wakes by `onWake`.
3
+ *
4
+ * @category mix
5
+ */
6
+ export interface Ticked {
7
+ sync(timestamp: number): void;
8
+ readonly inert: boolean;
9
+ onWake(fn: () => void): () => void;
10
+ }
11
+ /**
12
+ * How a ticker paces its frames and reads its clock.
13
+ *
14
+ * @category mix
15
+ */
16
+ export interface TickerOptions {
17
+ /**
18
+ * The most frames a second it syncs. Under animation frames it skips a frame that comes sooner
19
+ * than that, on a grid, so a 60 Hz display capped at 30 runs every other frame. Under a timer it
20
+ * is the timer's rate, 60 unless given.
21
+ */
22
+ fps?: number;
23
+ /**
24
+ * Where frames come from: `'frame'`, `requestAnimationFrame`, or `'timer'`, `setTimeout`. By
25
+ * default animation frames where they exist, and a timer where they don't, as in a worker
26
+ * without them or in Node.
27
+ */
28
+ grain?: 'frame' | 'timer';
29
+ /**
30
+ * The clock every mix is synced by. By default an animation frame's own timestamp, or
31
+ * `performance.now()` under a timer. A worker's `performance.now()` counts from its own origin,
32
+ * not the page's.
33
+ */
34
+ now?: () => number;
35
+ /** Stands in for `requestAnimationFrame`, as in a test. */
36
+ raf?: (callback: (timestamp: number) => void) => number;
37
+ /** Stands in for `cancelAnimationFrame`. */
38
+ caf?: (id: number) => void;
39
+ }
40
+ /**
41
+ * One frame loop that syncs the mixes added to it.
42
+ *
43
+ * @category mix
44
+ */
45
+ export interface Ticker {
46
+ /**
47
+ * Syncs `mix` every frame from now on, and wakes the loop on its `onWake`. Asks for a frame, even
48
+ * for a mix it already holds. Returns a remove.
49
+ */
50
+ add(mix: Ticked): () => void;
51
+ /** Calls `fn` each frame, after every mix has synced, with the frame's timestamp. Returns an unsubscribe. */
52
+ after(fn: (timestamp: number) => void): () => void;
53
+ /** Keeps frames coming whatever the mixes say, until the returned release is called. */
54
+ hold(): () => void;
55
+ /** Cancels the waiting frame; `wake`, a mix waking, `hold` or `add` starts the loop again. */
56
+ stop(): void;
57
+ /**
58
+ * Asks for a frame, after which the loop runs or sleeps as the mixes say. A mix changed before
59
+ * `stop` and not synced since does not wake the loop by itself, since it fires `onWake` once
60
+ * between syncs.
61
+ */
62
+ wake(): void;
63
+ /** What the ticker's clock reads now. */
64
+ now(): number;
65
+ }
66
+ /**
67
+ * One frame loop for any number of mixes. It runs while a mix would still change or something
68
+ * holds it, and sleeps otherwise, until a mix wakes it. A mix or `after` callback that throws stops
69
+ * neither the loop nor the others: its error is thrown again on a microtask.
70
+ *
71
+ * @category mix
72
+ */
73
+ export declare function ticker(opts?: TickerOptions): Ticker;
package/dist/ticker.js ADDED
@@ -0,0 +1,115 @@
1
+ /** Slack under the interval that still counts as due, for a display whose frames jitter. */
2
+ const SLACK = 1;
3
+ /**
4
+ * One frame loop for any number of mixes. It runs while a mix would still change or something
5
+ * holds it, and sleeps otherwise, until a mix wakes it. A mix or `after` callback that throws stops
6
+ * neither the loop nor the others: its error is thrown again on a microtask.
7
+ *
8
+ * @category mix
9
+ */
10
+ export function ticker(opts = {}) {
11
+ const g = globalThis;
12
+ const perfNow = () => (g.performance === undefined ? Date.now() : g.performance.now());
13
+ const byFrame = opts.raf !== undefined ||
14
+ (opts.grain !== 'timer' && typeof g.requestAnimationFrame === 'function');
15
+ const fps = opts.fps ?? (byFrame ? undefined : 60);
16
+ if (fps !== undefined && !(fps > 0 && fps < Number.POSITIVE_INFINITY))
17
+ throw new RangeError(`blits: a ticker's fps is a finite number above 0, not ${fps}`);
18
+ const interval = fps === undefined ? 0 : 1000 / fps;
19
+ const mixes = [];
20
+ const unwakes = new Map();
21
+ const fns = [];
22
+ let holds = 0;
23
+ let pending = null;
24
+ let last = Number.NaN;
25
+ const now = opts.now ?? perfNow;
26
+ const later = (err) => g.queueMicrotask(() => {
27
+ throw err;
28
+ });
29
+ const request = byFrame
30
+ ? (opts.raf ??
31
+ ((cb) => g.requestAnimationFrame(cb)))
32
+ : (cb) => g.setTimeout(() => cb(now()), Number.isNaN(last) ? 0 : Math.max(0, last + interval - now()));
33
+ const cancel = byFrame
34
+ ? (opts.caf ?? ((id) => g.cancelAnimationFrame?.(id)))
35
+ : (id) => g.clearTimeout(id);
36
+ const schedule = () => {
37
+ if (pending === null)
38
+ pending = request(frame);
39
+ };
40
+ function frame(stamp) {
41
+ pending = null;
42
+ const t = opts.now === undefined ? stamp : opts.now();
43
+ // A capped loop skips a frame that comes early, and steps a grid so its rate does not drift
44
+ // with the display's or with a timer's rounding. A long gap, as after sleeping, starts it again.
45
+ if (interval > 0 && !Number.isNaN(last)) {
46
+ const gap = t - last;
47
+ if (gap < interval - SLACK) {
48
+ pending = request(frame);
49
+ return;
50
+ }
51
+ last = gap >= 2 * interval ? t : last + interval;
52
+ }
53
+ else
54
+ last = t;
55
+ for (const mix of [...mixes])
56
+ try {
57
+ mix.sync(t);
58
+ }
59
+ catch (err) {
60
+ later(err);
61
+ }
62
+ for (const fn of [...fns])
63
+ try {
64
+ fn(t);
65
+ }
66
+ catch (err) {
67
+ later(err);
68
+ }
69
+ if (holds > 0 || mixes.some((mix) => !mix.inert))
70
+ schedule();
71
+ }
72
+ return {
73
+ add(mix) {
74
+ if (!unwakes.has(mix)) {
75
+ mixes.push(mix);
76
+ unwakes.set(mix, mix.onWake(schedule));
77
+ }
78
+ schedule();
79
+ return () => {
80
+ const unwake = unwakes.get(mix);
81
+ if (unwake === undefined)
82
+ return;
83
+ unwake();
84
+ unwakes.delete(mix);
85
+ mixes.splice(mixes.indexOf(mix), 1);
86
+ };
87
+ },
88
+ after(fn) {
89
+ fns.push(fn);
90
+ return () => {
91
+ const i = fns.indexOf(fn);
92
+ if (i >= 0)
93
+ fns.splice(i, 1);
94
+ };
95
+ },
96
+ hold() {
97
+ holds++;
98
+ schedule();
99
+ let released = false;
100
+ return () => {
101
+ if (released)
102
+ return;
103
+ released = true;
104
+ holds--;
105
+ };
106
+ },
107
+ stop() {
108
+ if (pending !== null)
109
+ cancel(pending);
110
+ pending = null;
111
+ },
112
+ wake: schedule,
113
+ now,
114
+ };
115
+ }
@@ -0,0 +1,121 @@
1
+ import type { Mixer } from './mixer.js';
2
+ import { Pace } from './pace.js';
3
+ import { Pager } from './paging.js';
4
+ import type { Marked, Mix, MixOptions, Tape, Transport as TransportApi, TransportOptions, TransportProjection } from './types.js';
5
+ /**
6
+ * A mark the host announced on a score: `at` in host time, so the rate never moves one, and NaN
7
+ * until the next sync for one announced as now before any sync; `made` is the mix time it was
8
+ * announced, for a read back to know what was known.
9
+ */
10
+ export interface Announced {
11
+ name: string;
12
+ score: string | undefined;
13
+ tags: readonly string[];
14
+ at: number;
15
+ made: number;
16
+ order: number;
17
+ /** The slot of the mix that announced it, -1 for the transport; only it sees an unnamed score. */
18
+ slot: number;
19
+ /** That mix's name, for `Marked.mix`. */
20
+ mix: string | undefined;
21
+ }
22
+ type Member = Mixer<unknown, unknown>;
23
+ /**
24
+ * The clock its mixes play on: host time less what `rebase` took out, at the rate its `pace`
25
+ * sets, moved by `sync` and `seek`, and the tape of calls a seek plays again. A mix made alone
26
+ * gets one holding only itself.
27
+ */
28
+ export declare class Transport implements TransportApi {
29
+ readonly history: MixOptions['history'];
30
+ readonly shared: boolean;
31
+ /** The mixes on it, in the order they joined. */
32
+ members: Member[];
33
+ /** Under history, mixes dropped while a seek back may still reach them, by when. */
34
+ dropped: {
35
+ mix: Member;
36
+ at: number;
37
+ }[];
38
+ /** Numbers the mixes as they join, for their order and for whose unnamed score a mark is on. */
39
+ slots: number;
40
+ private wakeFns;
41
+ /** Whether it has woken since the last sync, so it wakes once. */
42
+ private woken;
43
+ /** The mix clock at the last sync or seek. */
44
+ now: number;
45
+ /** Host time at the last sync: the host's timestamp less every gap `rebase` took out. */
46
+ u: number;
47
+ offset: number;
48
+ /** The mix time of the first sync. */
49
+ born: number;
50
+ /** The earliest mix time what history keeps in memory restores exactly. */
51
+ floor: number;
52
+ last: number;
53
+ rebasing: boolean;
54
+ /** The rate; null while it has never been set, when mix time is host time. */
55
+ pace: Pace | null;
56
+ /** True while the tape makes a recorded call again, so it is not recorded twice. */
57
+ replaying: boolean;
58
+ announced: Announced[];
59
+ /** Numbers voices and announced marks across every member, in the order they were made. */
60
+ nextId: number;
61
+ readonly tape: Tape | undefined;
62
+ /** With a history store, what left memory for it; null without one. */
63
+ readonly pager: Pager | null;
64
+ /**
65
+ * `shared` for one the host made, whose mixes may not move it themselves; a mix made alone makes
66
+ * one that is not.
67
+ */
68
+ constructor(history: MixOptions['history'], shared: boolean);
69
+ /** Whether this is a projection's own transport, which records and seeks nothing. */
70
+ get projecting(): boolean;
71
+ /** Takes a mix on, after every mix that joined before it; `slot` keeps a copy's in place. */
72
+ join(mix: Member, slot?: number): void;
73
+ drop<I, O, H>(mix: Mix<I, O, H>): void;
74
+ /** A seek back to `t`: every mix dropped after it is back on, as it stood then. */
75
+ undrop(t: number): void;
76
+ seek(time: number): void;
77
+ prepare(time: number): Promise<void>;
78
+ announce(name: string, opts: {
79
+ at?: number;
80
+ score: string;
81
+ tags?: readonly string[];
82
+ }): void;
83
+ marks(from: number, to: number): Marked[];
84
+ project(time: number): TransportProjection;
85
+ get live(): boolean;
86
+ get inert(): boolean;
87
+ onWake(fn: () => void): () => void;
88
+ /** A mix on it was changed outside a sync: wakes whoever sleeps on the transport, once. */
89
+ woke(): void;
90
+ sync(timestamp: number): void;
91
+ private syncAt;
92
+ rebase(): void;
93
+ /**
94
+ * Moves `floor` past what the mix clock standing at `now` has let go of. With a store nothing is
95
+ * let go: what leaves memory is paged, which moves it.
96
+ */
97
+ kept(): void;
98
+ /** Whether history reaches back to mix time `t`, from memory. */
99
+ reaches(t: number): boolean;
100
+ /**
101
+ * The earliest mix time it keeps what no store pages, the tape, dropped mixes, rate changes and
102
+ * announced marks among them: with a store, all of it, since the host bounds the tape.
103
+ */
104
+ keepsFrom(): number;
105
+ get rate(): number;
106
+ set rate(r: number);
107
+ ramp(rate: number, over: number): void;
108
+ }
109
+ /** Puts a mark on a score: one the transport announced, or a mix on it, `slot`. */
110
+ export declare function announce(transport: Transport, name: string, opts: {
111
+ at?: number;
112
+ score?: string;
113
+ tags?: readonly string[];
114
+ }, slot: number, mix: string | undefined): void;
115
+ /**
116
+ * Makes a transport several mixes can share, each made with `transport` in its options.
117
+ *
118
+ * @category mix
119
+ */
120
+ export declare function transport(opts?: TransportOptions): TransportApi;
121
+ export {};
@@ -0,0 +1,271 @@
1
+ import { listedAll } from './marks.js';
2
+ import { move, nextFrame, waits } from './move.js';
3
+ import { Pace } from './pace.js';
4
+ import { Pager, prepare } from './paging.js';
5
+ import { pin } from './place.js';
6
+ import { projectAll } from './project.js';
7
+ import { seek } from './seek.js';
8
+ import { record, replay, tapeOf } from './tape.js';
9
+ /**
10
+ * The clock its mixes play on: host time less what `rebase` took out, at the rate its `pace`
11
+ * sets, moved by `sync` and `seek`, and the tape of calls a seek plays again. A mix made alone
12
+ * gets one holding only itself.
13
+ */
14
+ export class Transport {
15
+ history;
16
+ shared;
17
+ /** The mixes on it, in the order they joined. */
18
+ members = [];
19
+ /** Under history, mixes dropped while a seek back may still reach them, by when. */
20
+ dropped = [];
21
+ /** Numbers the mixes as they join, for their order and for whose unnamed score a mark is on. */
22
+ slots = 0;
23
+ wakeFns = [];
24
+ /** Whether it has woken since the last sync, so it wakes once. */
25
+ woken = false;
26
+ /** The mix clock at the last sync or seek. */
27
+ now = Number.NaN;
28
+ /** Host time at the last sync: the host's timestamp less every gap `rebase` took out. */
29
+ u = Number.NaN;
30
+ offset = 0;
31
+ /** The mix time of the first sync. */
32
+ born = Number.NaN;
33
+ /** The earliest mix time what history keeps in memory restores exactly. */
34
+ floor = Number.NEGATIVE_INFINITY;
35
+ last = Number.NaN;
36
+ rebasing = false;
37
+ /** The rate; null while it has never been set, when mix time is host time. */
38
+ pace = null;
39
+ /** True while the tape makes a recorded call again, so it is not recorded twice. */
40
+ replaying = false;
41
+ announced = [];
42
+ /** Numbers voices and announced marks across every member, in the order they were made. */
43
+ nextId = 1;
44
+ tape;
45
+ /** With a history store, what left memory for it; null without one. */
46
+ pager;
47
+ /**
48
+ * `shared` for one the host made, whose mixes may not move it themselves; a mix made alone makes
49
+ * one that is not.
50
+ */
51
+ constructor(history, shared) {
52
+ this.history = history;
53
+ this.shared = shared;
54
+ this.tape = tapeOf(this);
55
+ this.pager = history?.store === undefined ? null : new Pager(history.store);
56
+ }
57
+ /** Whether this is a projection's own transport, which records and seeks nothing. */
58
+ get projecting() {
59
+ return this.members.some((m) => m.projecting);
60
+ }
61
+ /** Takes a mix on, after every mix that joined before it; `slot` keeps a copy's in place. */
62
+ join(mix, slot = this.slots++) {
63
+ mix.slot = slot;
64
+ const i = this.members.findIndex((m) => m.slot > slot);
65
+ if (i < 0)
66
+ this.members.push(mix);
67
+ else
68
+ this.members.splice(i, 0, mix);
69
+ }
70
+ drop(mix) {
71
+ const m = mix;
72
+ const i = this.members.indexOf(m);
73
+ if (i < 0)
74
+ throw new Error(`blits: ${m.name ?? 'that mix'} is not on this transport`);
75
+ this.members.splice(i, 1);
76
+ m.dropped = true;
77
+ if (this.history !== undefined) {
78
+ this.dropped.push({ mix: m, at: this.now });
79
+ record(this, 'drop', () => this.drop(mix));
80
+ }
81
+ }
82
+ /** A seek back to `t`: every mix dropped after it is back on, as it stood then. */
83
+ undrop(t) {
84
+ const back = this.dropped.filter((d) => d.at > t);
85
+ if (back.length === 0)
86
+ return;
87
+ this.dropped = this.dropped.filter((d) => d.at <= t);
88
+ for (const d of back) {
89
+ d.mix.dropped = false;
90
+ this.join(d.mix, d.mix.slot);
91
+ }
92
+ }
93
+ seek(time) {
94
+ seek(this, time);
95
+ }
96
+ prepare(time) {
97
+ return prepare(this, time);
98
+ }
99
+ announce(name, opts) {
100
+ if (opts?.score === undefined)
101
+ throw new Error('blits: a transport announces on a named score, which every mix on it sees');
102
+ announce(this, name, opts, -1, undefined);
103
+ }
104
+ marks(from, to) {
105
+ return listedAll(this, from, to).map(({ order: _, ...m }) => m);
106
+ }
107
+ project(time) {
108
+ return projectAll(this, time);
109
+ }
110
+ get live() {
111
+ return this.members.some((m) => m.live);
112
+ }
113
+ get inert() {
114
+ return this.members.every((m) => m.inert);
115
+ }
116
+ onWake(fn) {
117
+ const fns = this.wakeFns;
118
+ fns.push(fn);
119
+ return () => {
120
+ const i = fns.indexOf(fn);
121
+ if (i >= 0)
122
+ fns.splice(i, 1);
123
+ };
124
+ }
125
+ /** A mix on it was changed outside a sync: wakes whoever sleeps on the transport, once. */
126
+ woke() {
127
+ if (this.woken)
128
+ return;
129
+ this.woken = true;
130
+ for (const fn of [...this.wakeFns])
131
+ fn();
132
+ }
133
+ sync(timestamp) {
134
+ const members = this.members;
135
+ for (const m of members)
136
+ m.syncing = true;
137
+ try {
138
+ this.syncAt(timestamp);
139
+ }
140
+ finally {
141
+ for (const m of members)
142
+ m.syncing = false;
143
+ }
144
+ }
145
+ syncAt(timestamp) {
146
+ if (timestamp < this.last && !this.rebasing)
147
+ throw new RangeError(`blits: sync went back from ${this.last} to ${timestamp}; the host's clock only goes forward, and seek moves the mix`);
148
+ if (this.rebasing && !Number.isNaN(this.last))
149
+ this.offset += timestamp - this.last;
150
+ this.rebasing = false;
151
+ this.last = timestamp;
152
+ const u = timestamp - this.offset;
153
+ let pace = this.pace;
154
+ let now = pace === null ? u : pace.sync(u);
155
+ const later = u !== this.u;
156
+ const still = later && now === this.now;
157
+ this.u = u;
158
+ if (Number.isNaN(this.born))
159
+ this.born = now;
160
+ // Calls the host made before, played again where a seek went back past them; one may set the
161
+ // rate, which moves where this sync lands.
162
+ if (this.tape !== undefined && now > this.now) {
163
+ const reading = () => (this.pace === null ? u : this.pace.reading(u));
164
+ replay(this, reading);
165
+ pace = this.pace;
166
+ now = reading();
167
+ }
168
+ // Host time moving while the mix clock stands still lands what waits on host time or the host.
169
+ const moved = now !== this.now;
170
+ for (const m of this.members)
171
+ if (moved || (later && pace !== null && waits(m)))
172
+ move(m, now);
173
+ const history = this.history;
174
+ this.kept();
175
+ const reach = this.keepsFrom();
176
+ if (this.tape !== undefined && history !== undefined)
177
+ this.tape.prune(reach);
178
+ if (history !== undefined && this.dropped.length > 0)
179
+ this.dropped = this.dropped.filter((d) => d.at >= reach);
180
+ this.woken = false;
181
+ // It is a frame too, which asks every weight signal again, so one reading input follows it.
182
+ if (still)
183
+ for (const m of this.members) {
184
+ m.frame = nextFrame();
185
+ m.lanes?.refill();
186
+ }
187
+ for (const m of this.members) {
188
+ const bookers = m.bookers;
189
+ if (bookers !== null)
190
+ for (const b of bookers)
191
+ b.sync();
192
+ }
193
+ }
194
+ rebase() {
195
+ this.rebasing = true;
196
+ }
197
+ /**
198
+ * Moves `floor` past what the mix clock standing at `now` has let go of. With a store nothing is
199
+ * let go: what leaves memory is paged, which moves it.
200
+ */
201
+ kept() {
202
+ const history = this.history;
203
+ if (history !== undefined && this.pager === null && this.now - history.ms > this.floor)
204
+ this.floor = this.now - history.ms;
205
+ }
206
+ /** Whether history reaches back to mix time `t`, from memory. */
207
+ reaches(t) {
208
+ if (this.pager !== null)
209
+ return t >= this.floor;
210
+ return t >= this.floor && t >= this.now - (this.history?.ms ?? 0);
211
+ }
212
+ /**
213
+ * The earliest mix time it keeps what no store pages, the tape, dropped mixes, rate changes and
214
+ * announced marks among them: with a store, all of it, since the host bounds the tape.
215
+ */
216
+ keepsFrom() {
217
+ const history = this.history;
218
+ if (history === undefined)
219
+ return Number.POSITIVE_INFINITY;
220
+ return this.pager === null ? this.now - history.ms : Number.NEGATIVE_INFINITY;
221
+ }
222
+ get rate() {
223
+ return this.pace === null ? 1 : this.pace.rateAt(this.u);
224
+ }
225
+ set rate(r) {
226
+ this.ramp(r, 0);
227
+ }
228
+ ramp(rate, over) {
229
+ if (!(rate >= 0 && rate < Number.POSITIVE_INFINITY))
230
+ throw new RangeError(`blits: a mix's rate is a finite number, 0 or more, not ${rate}`);
231
+ if (this.pace === null) {
232
+ this.pace = new Pace(this.history !== undefined);
233
+ // Until now mix time was host time, so a pending voice's start is the host time it was given.
234
+ for (const m of this.members)
235
+ for (const v of m.cued)
236
+ if (v.state === 'pending' && !v.placing && v.owner === null && v.spec.start !== undefined)
237
+ pin(m, v, v.start);
238
+ }
239
+ const reach = this.history === undefined ? Number.NEGATIVE_INFINITY : this.keepsFrom();
240
+ this.pace.change(this.u, rate, over, reach);
241
+ for (const m of this.members)
242
+ m.stir();
243
+ record(this, 'rate', () => this.ramp(rate, over));
244
+ }
245
+ }
246
+ /** Puts a mark on a score: one the transport announced, or a mix on it, `slot`. */
247
+ export function announce(transport, name, opts, slot, mix) {
248
+ const at = opts.at !== undefined ? opts.at - transport.offset : transport.u;
249
+ const mark = {
250
+ name,
251
+ score: opts.score,
252
+ tags: opts.tags ?? [],
253
+ at,
254
+ made: Number.isNaN(transport.now) ? Number.NEGATIVE_INFINITY : transport.now,
255
+ order: transport.nextId++,
256
+ slot,
257
+ mix,
258
+ };
259
+ transport.announced.push(mark);
260
+ record(transport, 'announce', () => {
261
+ transport.announced.push({ ...mark });
262
+ });
263
+ }
264
+ /**
265
+ * Makes a transport several mixes can share, each made with `transport` in its options.
266
+ *
267
+ * @category mix
268
+ */
269
+ export function transport(opts = {}) {
270
+ return new Transport(opts.history, true);
271
+ }