@msb235/blits 0.11.0 → 0.12.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.
- package/CHANGELOG.md +110 -0
- package/dist/arcs.d.ts +40 -0
- package/dist/arcs.js +241 -0
- package/dist/chain.d.ts +6 -1
- package/dist/chain.js +33 -13
- package/dist/channels.d.ts +2 -0
- package/dist/channels.js +12 -1
- package/dist/clone.js +16 -4
- package/dist/cue.js +11 -2
- package/dist/departed.d.ts +5 -1
- package/dist/departed.js +71 -27
- package/dist/easing.d.ts +12 -1
- package/dist/easing.js +7 -1
- package/dist/everyone.d.ts +14 -1
- package/dist/everyone.js +27 -2
- package/dist/fade.js +3 -3
- package/dist/fill.js +16 -9
- package/dist/fit.d.ts +8 -1
- package/dist/fit.js +7 -3
- package/dist/fold.js +69 -13
- package/dist/gather.js +38 -7
- package/dist/handle.d.ts +2 -0
- package/dist/handle.js +8 -0
- package/dist/held.d.ts +5 -2
- package/dist/held.js +6 -3
- package/dist/history.d.ts +19 -2
- package/dist/history.js +149 -31
- package/dist/hosts.js +27 -11
- package/dist/index.d.ts +4 -1
- package/dist/index.js +2 -0
- package/dist/lanes.d.ts +23 -7
- package/dist/lanes.js +36 -2
- package/dist/lean.js +2 -2
- package/dist/marks.js +3 -0
- package/dist/mixer.d.ts +16 -1
- package/dist/mixer.js +60 -30
- package/dist/move.js +7 -2
- package/dist/numbers.d.ts +5 -1
- package/dist/numbers.js +4 -0
- package/dist/owner.js +6 -1
- package/dist/place.d.ts +8 -0
- package/dist/place.js +16 -8
- package/dist/project.js +3 -3
- package/dist/qualify.d.ts +10 -0
- package/dist/qualify.js +52 -1
- package/dist/reading.d.ts +2 -1
- package/dist/reading.js +2 -1
- package/dist/record.d.ts +6 -0
- package/dist/record.js +1 -0
- package/dist/restored.d.ts +67 -18
- package/dist/restored.js +159 -49
- package/dist/revive.d.ts +1 -0
- package/dist/revive.js +1 -0
- package/dist/roster.js +2 -1
- package/dist/seek.js +72 -18
- package/dist/shared.d.ts +1 -1
- package/dist/shared.js +1 -1
- package/dist/spans.d.ts +5 -0
- package/dist/spans.js +75 -5
- package/dist/store.d.ts +2 -0
- package/dist/store.js +4 -0
- package/dist/touch.js +3 -2
- package/dist/transport.js +6 -1
- package/dist/types/arc.d.ts +55 -0
- package/dist/types/arc.js +1 -0
- package/dist/types/mix.d.ts +24 -0
- package/dist/types/voice.d.ts +10 -1
- package/dist/types/watch.d.ts +126 -0
- package/dist/types/watch.js +1 -0
- package/dist/types.d.ts +2 -0
- package/dist/voice.d.ts +37 -6
- package/dist/voice.js +17 -0
- package/dist/watch.d.ts +28 -0
- package/dist/watch.js +467 -0
- package/dist/weigh.d.ts +13 -2
- package/dist/weigh.js +10 -3
- package/docs/schema.html +39 -3
- package/package.json +5 -4
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,116 @@ 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.12.1
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `mix.close()`: takes a mix off the list debuggers read, for a host tearing it down, where it would
|
|
12
|
+
otherwise stay listed until collected. It stops every debugger watching it, and frees a debugger's
|
|
13
|
+
hold unless another listed mix shares its transport.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- An arc not yet started, cued late or under an owner yet to start, no longer shows its enter's
|
|
18
|
+
first frame from the moment it is cued.
|
|
19
|
+
|
|
20
|
+
## 0.12.0
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- `curve(easing)` and `Curve`: the function an easing stands for, from a CSS name, a `bezier`, a
|
|
25
|
+
`steps` or a function, for a host shaping its own ramps with the curves patches use.
|
|
26
|
+
- `mix.arc(spec)`: an owner holding three parts that hand over in order on its clock, `enter`,
|
|
27
|
+
`active` for the `hold` and `exit`, each a voice's spec or several layered, with a `blend`
|
|
28
|
+
crossfading across each boundary. Its marks are its own: `in` as the enter ends, `coast` and
|
|
29
|
+
`out` as the hold ends and the exit begins, `end` as the exit ends, where it leaves and `done`
|
|
30
|
+
resolves. `hold: 'open'` holds until `release()` on its handle, which a seek replays; `fade`
|
|
31
|
+
takes it out with no exit. An arc with a finite hold sits in a span. It is klieg's `fire` as a
|
|
32
|
+
blits primitive, under the same part names. The schema page's "Arcs" says what each part does.
|
|
33
|
+
- A span child's `earliest` and `rank`. `earliest` is the soonest it may start, a time on the
|
|
34
|
+
span's clock or an anchor; the fit never places it sooner, and the children after it are laid
|
|
35
|
+
out from where it starts. One already past when it is cued places it there, partway through, so a
|
|
36
|
+
span made afresh after a seek plays as it did. One whose anchor has no answer yet waits, with every child
|
|
37
|
+
after it under `queue` or `stagger`, and the span fits again once the anchor answers. `rank` orders the
|
|
38
|
+
children still to start, lower first, ties as cued. Together they replace astv's own change
|
|
39
|
+
order, which sorts its changes by line and starts each no sooner than its orb lands.
|
|
40
|
+
- `watched()`: every mix on the page, for a debugger, in one registry that all copies of blits
|
|
41
|
+
share. Each entry can:
|
|
42
|
+
- `watch` what the host reads each frame
|
|
43
|
+
- `read` a subject the host never probes, now or back in history, through `project`
|
|
44
|
+
- `label` a subject
|
|
45
|
+
- list its kit's `channels`, and the `name` and `tags` a voice was cued with
|
|
46
|
+
- `hold` the mix's clock to pause, step and seek it, without writing to the tape, then `free`
|
|
47
|
+
it
|
|
48
|
+
|
|
49
|
+
A mix nobody watches runs the same code as before, and one a debugger has let go of runs as fast
|
|
50
|
+
as one it never watched. `watched().history` gives history to mixes
|
|
51
|
+
made later that name none. `MixOptions.labelOf` names subjects for it.
|
|
52
|
+
|
|
53
|
+
### Fixed
|
|
54
|
+
|
|
55
|
+
- Replacing a voice makes less garbage again: a one-subject tween voice retargeted as it is cued,
|
|
56
|
+
while another leaves and its subject is dropped, allocates 11.4–11.7 KB, from 13.0–13.4
|
|
57
|
+
(`bench/churnwho.mjs`, 5,000 voices, teitou, three passes a side). Frame time is unchanged.
|
|
58
|
+
- A voice over every subject that plays more than 60 frames costs less a frame: it now keeps a
|
|
59
|
+
record for each subject, where it kept one for them all that every read had to point at its
|
|
60
|
+
subject and back. Three `fn` voices over 1,000 subjects (`fn`) take 0.212 ms a frame against
|
|
61
|
+
0.226, eight 0.468 against 0.526 (teitou, medians of nine alternated runs of 1,500 frames against
|
|
62
|
+
`a77c5c9`). A voice cued and gone within a few frames still shares one record, which is what it
|
|
63
|
+
was for.
|
|
64
|
+
- A crowd of `fn` voices writing arrays, one voice per subject, costs about a tenth less a frame
|
|
65
|
+
where nothing else writes its channels: it runs off lanes, where its subjects are walked once
|
|
66
|
+
instead of twice. A `fn` voice writing a vec for each of 10,000 subjects (`fns`) takes 1.348 ms
|
|
67
|
+
a frame against 1.519 (msb-uai, medians of nine alternated runs of one build with lanes kept
|
|
68
|
+
for it and not). So does a single `fn` voice over many subjects writing a vec, where no other
|
|
69
|
+
voice writes it: weasel's animator written as one (`weaselfn`) takes 1.734 ms against 1.823,
|
|
70
|
+
and one writing a position from each subject's own numbers (`tweenfn`) 1.163 against 1.410.
|
|
71
|
+
- A mix with lanes on whose voices all run off lanes costs a probe less bookkeeping: `tweenfn`
|
|
72
|
+
takes 1.097 ms a frame against 1.172 and `weaselfn` 1.644 against 1.723 (msb-uai, medians of
|
|
73
|
+
thirteen alternated runs of one build with the change switched on and off).
|
|
74
|
+
- A voice on a lane costs a call less a subject each frame: since 0.11.0 the check for a voice
|
|
75
|
+
hiding past its end left its clock read out of line. A glow weighted by a signal over 10,000
|
|
76
|
+
subjects (`glow`) takes 1.636 ms a frame against 1.696 (teitou, medians of fifteen alternated runs
|
|
77
|
+
against `11a1471`).
|
|
78
|
+
- A mix keeping history costs about a fifth less a frame and allocates a third less: the copy of a
|
|
79
|
+
record that falls out of the window is written over as the next one kept, only a motion voice
|
|
80
|
+
works out how far back a read may still reach, and a stock `sum`, `mul` or `max` channel folds a
|
|
81
|
+
voice's number without calling the channel. Three voices over 10,000 subjects with 5 s of history
|
|
82
|
+
(`kept`) take 5.33 ms a frame against 6.56 and allocate 5.7 MB against 8.5; sought back and
|
|
83
|
+
returned before every sync (`seek`), 5.90 ms against 6.76; at 1,000 subjects, 0.412 against
|
|
84
|
+
0.500 and 0.540 against 0.598 (teitou, medians of nine alternated runs of 1,500 frames against
|
|
85
|
+
`00ad148`, each in its own process).
|
|
86
|
+
- A `seek` back and a return to where the mix was cost about a third of what they did: a record put
|
|
87
|
+
back takes up the state of the record the seek replaced instead of stepping again from its copy,
|
|
88
|
+
is written over that record instead of copied anew, and the copy its next probe keeps is written
|
|
89
|
+
over one the seek cut; subjects keep their numbers across the seek. Three voices over 10,000
|
|
90
|
+
subjects with 5 s of history, sought 300 ms back and returned before every sync (`seek`), take
|
|
91
|
+
9.83 ms a frame against 28.60, and allocate 8.6 MB a frame against 35.2; 1,000 subjects take 0.86
|
|
92
|
+
ms against 1.88 (msb-uai, medians of nine alternated runs against `4374f34`, each in its own
|
|
93
|
+
process). The same mix never sought (`kept`) reads 7.41 ms against 7.33 at 10,000 subjects and
|
|
94
|
+
0.645 against 0.676 at 1,000. A `step` that reads `setting.weight`, `elapsed` or `pass` now shows,
|
|
95
|
+
after a seek back and return, what a mix never sought shows. A seek no longer boxes its moment
|
|
96
|
+
once per record it puts back: the `seek` row at 10,000 subjects allocates 7.9 MB a frame against
|
|
97
|
+
8.8, and takes 10.44 ms against 9.88, inside its runs' spread (msb-uai, medians of nine
|
|
98
|
+
alternated runs against `1077ab4`). A seek's relink takes each voice's record from the subject's
|
|
99
|
+
old chain where its store can tell the record is still its own, instead of looking it up by
|
|
100
|
+
subject: the `seek` row at 10,000 subjects takes 9.42 ms a frame against 9.95, and 1,000 subjects
|
|
101
|
+
0.864 against 0.884 (msb-uai, medians of nine alternated runs against `c3fe7a8`).
|
|
102
|
+
- A mix sought back often no longer grows without bound: each voice kept a little for every seek
|
|
103
|
+
back to a later moment than the last, and the records subjects left with after each seek, and
|
|
104
|
+
let go of neither. Three voices sought back and returned every frame held 2.26 MB more after
|
|
105
|
+
12,000 frames; they now hold what their live records need.
|
|
106
|
+
- A lane fill no longer boxes numbers it hands between its own calls, which it did for 3 to 5
|
|
107
|
+
numbers a subject in some processes and none in others, by the order V8 happened to compile in.
|
|
108
|
+
10k subjects under a `fn` voice and a signal-weighted glow over 200 of them, with 200 subjects
|
|
109
|
+
leaving the glow and 200 entering each frame (bench/reach.mjs `part:200`), allocate 1.27 MB a
|
|
110
|
+
frame against 1.89 and take 1.924 ms against 2.037 read by `probe`, and 1.255 against 1.405 by
|
|
111
|
+
`pull`; with nothing moving by `pull`, 1.108 against 1.221 (keiei). `probed` allocates 1.02 MB a
|
|
112
|
+
frame against 1.65. No steady row moves past its spread: `fn` 2.942 against 2.903, `fn^` 2.323
|
|
113
|
+
against 2.390, `keys` 1.355 against 1.342 (msb-uai); `signal` reads 3.820 against 3.578, inside
|
|
114
|
+
its spread but slower in each of three such comparisons. Medians of six alternated runs, each in
|
|
115
|
+
its own process, `bench/ab.sh` against `main` at `4374f34`.
|
|
116
|
+
|
|
7
117
|
## 0.11.0
|
|
8
118
|
|
|
9
119
|
### Breaking
|
package/dist/arcs.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Mixer } from './mixer.js';
|
|
2
|
+
import type { ArcHandle, ArcSpec } from './types.js';
|
|
3
|
+
import type { Voice } from './voice.js';
|
|
4
|
+
/** What an arc keeps beside its owner: how long its enter and exit last, which parts it has, its blend. */
|
|
5
|
+
export declare class Arc {
|
|
6
|
+
readonly enter: number;
|
|
7
|
+
readonly exit: number;
|
|
8
|
+
readonly blend: number;
|
|
9
|
+
/** By part, whether it has one. */
|
|
10
|
+
readonly has: readonly boolean[];
|
|
11
|
+
/** How long its hold lasts as cued; Infinity for one held open. */
|
|
12
|
+
readonly hold: number;
|
|
13
|
+
constructor(enter: number, exit: number, blend: number,
|
|
14
|
+
/** By part, whether it has one. */
|
|
15
|
+
has: readonly boolean[],
|
|
16
|
+
/** How long its hold lasts as cued; Infinity for one held open. */
|
|
17
|
+
hold: number);
|
|
18
|
+
}
|
|
19
|
+
/** Where an arc ends on its own clock: Infinity while its hold is open. */
|
|
20
|
+
export declare function arcEnd<I, O>(voice: Voice<I, O>): number;
|
|
21
|
+
/**
|
|
22
|
+
* What an arc multiplies into the weight of a voice it holds at mix time `now`: its part's ramp,
|
|
23
|
+
* scaled down where a hold shorter than the blend leaves all three ramps up at once and their sum
|
|
24
|
+
* would pass 1.
|
|
25
|
+
*/
|
|
26
|
+
export declare function arcShare<I, O>(arcVoice: Voice<I, O>, child: Voice<I, O>, now: number, reduced: boolean): number;
|
|
27
|
+
/** Cues an arc: its owner, then each part's voices on its clock. */
|
|
28
|
+
export declare function cueArc<I, O>(mix: Mixer<I, O>, spec: ArcSpec<I, O>): ArcHandle<I>;
|
|
29
|
+
/**
|
|
30
|
+
* Ends an arc's hold at mix time now, so its exit begins half a blend later and its blend starts
|
|
31
|
+
* now; where the enter is still playing, the exit follows it. Nothing where the exit has begun.
|
|
32
|
+
*/
|
|
33
|
+
export declare function release<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>): void;
|
|
34
|
+
/**
|
|
35
|
+
* Lets each arc's parts leave once the blend after them is over, and the arc itself at its end:
|
|
36
|
+
* played, as a voice that ran its passes is.
|
|
37
|
+
*/
|
|
38
|
+
export declare function settleArcs<I, O>(mix: Mixer<I, O>, now: number): void;
|
|
39
|
+
/** An arc's marks: when its enter ends, its hold ends and its exit begins and ends. */
|
|
40
|
+
export declare function arcMark<I, O>(voice: Voice<I, O>, start: number, mark: 'in' | 'coast' | 'out' | 'end'): number | undefined;
|
package/dist/arcs.js
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { retire } from './fade.js';
|
|
2
|
+
import { noted } from './history.js';
|
|
3
|
+
import { ownerReading } from './owner.js';
|
|
4
|
+
import { startAt } from './place.js';
|
|
5
|
+
import { refit } from './spans.js';
|
|
6
|
+
/** An arc's parts in the order they play: its enter, its active and its exit. */
|
|
7
|
+
const PARTS = ['enter', 'active', 'exit'];
|
|
8
|
+
/** What an arc keeps beside its owner: how long its enter and exit last, which parts it has, its blend. */
|
|
9
|
+
export class Arc {
|
|
10
|
+
enter;
|
|
11
|
+
exit;
|
|
12
|
+
blend;
|
|
13
|
+
has;
|
|
14
|
+
hold;
|
|
15
|
+
constructor(enter, exit, blend,
|
|
16
|
+
/** By part, whether it has one. */
|
|
17
|
+
has,
|
|
18
|
+
/** How long its hold lasts as cued; Infinity for one held open. */
|
|
19
|
+
hold) {
|
|
20
|
+
this.enter = enter;
|
|
21
|
+
this.exit = exit;
|
|
22
|
+
this.blend = blend;
|
|
23
|
+
this.has = has;
|
|
24
|
+
this.hold = hold;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** Where each part an arc has starts and ends on its clock, for a hold of `hold`; null for a part it lacks. */
|
|
28
|
+
function segments(arc, hold) {
|
|
29
|
+
const bounds = [0, arc.enter, arc.enter + hold, arc.enter + hold + arc.exit];
|
|
30
|
+
const end = bounds[3];
|
|
31
|
+
const out = [null, null, null];
|
|
32
|
+
for (let p = 0; p < 3; p++) {
|
|
33
|
+
if (!arc.has[p])
|
|
34
|
+
continue;
|
|
35
|
+
let next = p + 1;
|
|
36
|
+
while (next < 3 && !arc.has[next])
|
|
37
|
+
next++;
|
|
38
|
+
out[p] = [bounds[p], next < 3 ? bounds[next] : end];
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
/** Where an arc ends on its own clock: Infinity while its hold is open. */
|
|
43
|
+
export function arcEnd(voice) {
|
|
44
|
+
const arc = voice.arc;
|
|
45
|
+
return arc.enter + voice.hold + arc.exit;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* A part's weight at `t` on its arc's clock, klieg's: up over the blend centered on where it
|
|
49
|
+
* starts and down over the one where it ends, except at the arc's own start and end, where it
|
|
50
|
+
* holds full weight.
|
|
51
|
+
*/
|
|
52
|
+
function rampOf(s, e, end, blend, t) {
|
|
53
|
+
if (e <= s)
|
|
54
|
+
return 0;
|
|
55
|
+
const half = blend / 2;
|
|
56
|
+
const first = s === 0;
|
|
57
|
+
const last = e === end;
|
|
58
|
+
if ((!first && t < s - half) || (!last && t >= e + half))
|
|
59
|
+
return 0;
|
|
60
|
+
const up = first ? 1 : blend > 0 ? Math.min(1, (t - s + half) / blend) : 1;
|
|
61
|
+
const down = last ? 1 : blend > 0 ? Math.min(1, (e + half - t) / blend) : 1;
|
|
62
|
+
return Math.min(up, down);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* What an arc multiplies into the weight of a voice it holds at mix time `now`: its part's ramp,
|
|
66
|
+
* scaled down where a hold shorter than the blend leaves all three ramps up at once and their sum
|
|
67
|
+
* would pass 1.
|
|
68
|
+
*/
|
|
69
|
+
export function arcShare(arcVoice, child, now, reduced) {
|
|
70
|
+
const arc = arcVoice.arc;
|
|
71
|
+
const t = ownerReading(arcVoice, now);
|
|
72
|
+
// Its parts freeze on their first frames, so an arc yet to start has to silence them itself.
|
|
73
|
+
if (!(t >= 0))
|
|
74
|
+
return 0;
|
|
75
|
+
const segs = segments(arc, arcVoice.hold);
|
|
76
|
+
const end = arc.enter + arcVoice.hold + arc.exit;
|
|
77
|
+
const blend = reduced ? 0 : arc.blend;
|
|
78
|
+
const mine = segs[child.part];
|
|
79
|
+
if (mine == null)
|
|
80
|
+
return 0;
|
|
81
|
+
const own = rampOf(mine[0], mine[1], end, blend, t);
|
|
82
|
+
if (own <= 0)
|
|
83
|
+
return 0;
|
|
84
|
+
let total = 0;
|
|
85
|
+
for (const s of segs)
|
|
86
|
+
if (s !== null)
|
|
87
|
+
total += rampOf(s[0], s[1], end, blend, t);
|
|
88
|
+
return total > 1 ? own / total : own;
|
|
89
|
+
}
|
|
90
|
+
/** How long a part lasts: its longest voice's `duration`, or patch duration times passes. */
|
|
91
|
+
function lengthOf(part) {
|
|
92
|
+
let most = 0;
|
|
93
|
+
for (const v of part) {
|
|
94
|
+
const loop = v.loop;
|
|
95
|
+
const passes = loop === undefined || loop === false ? 1 : loop === true ? Infinity : loop;
|
|
96
|
+
const own = v.duration ?? (v.patch.duration > 0 ? v.patch.duration * passes : 0);
|
|
97
|
+
if (!(own >= 0))
|
|
98
|
+
throw new RangeError(`blits: an arc's part lasts 0 ms or more, not ${own}`);
|
|
99
|
+
if (own > most)
|
|
100
|
+
most = own;
|
|
101
|
+
}
|
|
102
|
+
return most;
|
|
103
|
+
}
|
|
104
|
+
const listOf = (part) => part === undefined ? undefined : Array.isArray(part) ? part : [part];
|
|
105
|
+
/** Cues an arc: its owner, then each part's voices on its clock. */
|
|
106
|
+
export function cueArc(mix, spec) {
|
|
107
|
+
if ((spec.rate ?? 1) < 0)
|
|
108
|
+
throw new Error('blits: an arc plays its parts forward from its start, so it is not cued backward');
|
|
109
|
+
if (spec.freeze !== undefined)
|
|
110
|
+
throw new Error('blits: an arc holds each part’s first and last frames itself, so it takes no freeze');
|
|
111
|
+
const { enter, active, exit, hold, blend, ...own } = spec;
|
|
112
|
+
const parts = [listOf(enter), listOf(active), listOf(exit)];
|
|
113
|
+
const lengths = parts.map((p) => (p === undefined ? 0 : lengthOf(p)));
|
|
114
|
+
for (const p of [0, 2])
|
|
115
|
+
if (!Number.isFinite(lengths[p]))
|
|
116
|
+
throw new Error(`blits: an arc's ${PARTS[p]} has to end, so it cannot loop for good`);
|
|
117
|
+
const held = hold === 'open' ? Number.POSITIVE_INFINITY : (hold ?? lengths[1] ?? 0);
|
|
118
|
+
if (!(held >= 0))
|
|
119
|
+
throw new RangeError(`blits: an arc holds for 0 ms or more, not ${hold}`);
|
|
120
|
+
const fade = blend ?? 0;
|
|
121
|
+
if (!(fade >= 0 && Number.isFinite(fade)))
|
|
122
|
+
throw new RangeError(`blits: an arc blends over 0 ms or more, not ${blend}`);
|
|
123
|
+
const arc = new Arc(lengths[0], lengths[2], fade, parts.map((p) => p !== undefined), held);
|
|
124
|
+
mix.arcing = arc;
|
|
125
|
+
let handle;
|
|
126
|
+
try {
|
|
127
|
+
handle = mix.owns(own);
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
mix.arcing = null;
|
|
131
|
+
}
|
|
132
|
+
const starts = [0, arc.enter, arc.enter + held];
|
|
133
|
+
parts.forEach((part, p) => {
|
|
134
|
+
for (const v of part ?? []) {
|
|
135
|
+
const { duration: _, ...rest } = v;
|
|
136
|
+
const child = {
|
|
137
|
+
...rest,
|
|
138
|
+
owner: handle,
|
|
139
|
+
start: starts[p],
|
|
140
|
+
freeze: 'both',
|
|
141
|
+
};
|
|
142
|
+
// The hold, not the patch, says how long the active plays, so it loops unless told otherwise.
|
|
143
|
+
if (p === 1 && hold !== undefined && v.loop === undefined)
|
|
144
|
+
child.loop = true;
|
|
145
|
+
mix.cue(child);
|
|
146
|
+
mix.cued[mix.cued.length - 1].part = p;
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
return handle;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Ends an arc's hold at mix time now, so its exit begins half a blend later and its blend starts
|
|
153
|
+
* now; where the enter is still playing, the exit follows it. Nothing where the exit has begun.
|
|
154
|
+
*/
|
|
155
|
+
export function release(mix, voice) {
|
|
156
|
+
const arc = voice.arc;
|
|
157
|
+
if (arc === null)
|
|
158
|
+
throw new Error('blits: release ends an arc’s hold, and this is no arc');
|
|
159
|
+
if (voice.state === 'done' || voice.state === 'fading')
|
|
160
|
+
return;
|
|
161
|
+
const local = ownerReading(voice, mix.now);
|
|
162
|
+
const lead = arc.exit > 0 && !mix.reduced ? arc.blend / 2 : 0;
|
|
163
|
+
const at = Math.max(arc.enter, local + lead);
|
|
164
|
+
if (!(at < arc.enter + voice.hold))
|
|
165
|
+
return;
|
|
166
|
+
voice.hold = at - arc.enter;
|
|
167
|
+
for (const child of voice.holding?.children ?? [])
|
|
168
|
+
if (child.part === 2 && child.state === 'pending')
|
|
169
|
+
startAt(mix, child, at);
|
|
170
|
+
noteArc(mix, voice);
|
|
171
|
+
}
|
|
172
|
+
/** Records an arc's new hold, and has the span holding it fit again. */
|
|
173
|
+
function noteArc(mix, voice) {
|
|
174
|
+
noted(mix, voice);
|
|
175
|
+
if (voice.owner?.fitting)
|
|
176
|
+
refit(mix, voice.owner);
|
|
177
|
+
mix.lanes?.refill();
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Lets each arc's parts leave once the blend after them is over, and the arc itself at its end:
|
|
181
|
+
* played, as a voice that ran its passes is.
|
|
182
|
+
*/
|
|
183
|
+
export function settleArcs(mix, now) {
|
|
184
|
+
for (const voice of mix.owners ?? []) {
|
|
185
|
+
const arc = voice.arc;
|
|
186
|
+
if (arc === null || voice.state === 'done' || voice.state === 'pending')
|
|
187
|
+
continue;
|
|
188
|
+
const local = ownerReading(voice, now);
|
|
189
|
+
if (!Number.isFinite(local))
|
|
190
|
+
continue;
|
|
191
|
+
const end = arcEnd(voice);
|
|
192
|
+
if (local >= end) {
|
|
193
|
+
const at = voice.timeAt(end);
|
|
194
|
+
voice.play(true, at, mix.transport.seq);
|
|
195
|
+
retire(mix, voice, at);
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
const segs = segments(arc, voice.hold);
|
|
199
|
+
const half = mix.reduced ? 0 : arc.blend / 2;
|
|
200
|
+
for (const child of [...(voice.holding?.children ?? [])]) {
|
|
201
|
+
const s = segs[child.part];
|
|
202
|
+
if (s == null || s[1] === end || child.state === 'done')
|
|
203
|
+
continue;
|
|
204
|
+
if (local >= s[1] + half)
|
|
205
|
+
retire(mix, child, voice.timeAt(s[1] + half));
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/** An arc's marks: when its enter ends, its hold ends and its exit begins and ends. */
|
|
210
|
+
export function arcMark(voice, start, mark) {
|
|
211
|
+
const arc = voice.arc;
|
|
212
|
+
const at = (t) => {
|
|
213
|
+
const u = voice.timeAt(t);
|
|
214
|
+
return Number.isFinite(u) ? Math.max(start, u) : undefined;
|
|
215
|
+
};
|
|
216
|
+
if (mark === 'in') {
|
|
217
|
+
const t = at(arc.enter);
|
|
218
|
+
return t === undefined ? t : Math.max(t, start + (voice.fade.in ?? 0));
|
|
219
|
+
}
|
|
220
|
+
const coast = at(arc.enter + voice.hold);
|
|
221
|
+
// Faded, or set to fade, before its hold ended: it left with no exit.
|
|
222
|
+
const out = voice.out;
|
|
223
|
+
const cut = out !== null ? out.at : Number.isFinite(voice.outAt) ? voice.outAt : undefined;
|
|
224
|
+
const cutShort = cut !== undefined && (coast === undefined || cut < coast);
|
|
225
|
+
if (mark === 'coast')
|
|
226
|
+
return cutShort ? undefined : coast;
|
|
227
|
+
if (mark === 'out') {
|
|
228
|
+
if (cutShort)
|
|
229
|
+
return cut;
|
|
230
|
+
return voice.state === 'done' && coast === undefined ? voice.doneAt : coast;
|
|
231
|
+
}
|
|
232
|
+
if (voice.state === 'done')
|
|
233
|
+
return voice.doneAt;
|
|
234
|
+
if (cutShort) {
|
|
235
|
+
if (out !== null)
|
|
236
|
+
return out.at + out.over;
|
|
237
|
+
const over = Number.isNaN(voice.outOver) ? (voice.fade.out ?? 0) : voice.outOver;
|
|
238
|
+
return cut + over;
|
|
239
|
+
}
|
|
240
|
+
return at(arcEnd(voice));
|
|
241
|
+
}
|
package/dist/chain.d.ts
CHANGED
|
@@ -8,8 +8,13 @@ import type { Subject, Voice } from './voice.js';
|
|
|
8
8
|
*/
|
|
9
9
|
export declare function chain<I, O>(this: Mixer<I, O>, subject: I): Subject<unknown>;
|
|
10
10
|
export declare function relink<I, O>(this: Mixer<I, O>, subject: I, was: Subject<unknown> | undefined): Subject<unknown>;
|
|
11
|
+
/**
|
|
12
|
+
* The subject's chain as last linked; undefined where it has none, as for a chain a seek kept only
|
|
13
|
+
* for its number and not linked since, whose records are those from before the seek.
|
|
14
|
+
*/
|
|
15
|
+
export declare function headOf<I, O>(mix: Mixer<I, O>, subject: I): Subject<unknown> | undefined;
|
|
11
16
|
/** A voice's record for a subject where its chain links it: the voice plays or freezes, and reaches it. */
|
|
12
|
-
export declare function linkable<I, O>(this: Mixer<I, O>, voice: Voice<I, O>, subject: I, slot: number): Subject<unknown> | null;
|
|
17
|
+
export declare function linkable<I, O>(this: Mixer<I, O>, voice: Voice<I, O>, subject: I, slot: number, guess?: Subject<unknown> | null): Subject<unknown> | null;
|
|
13
18
|
/** Whether a voice reaches a subject by what its spec says: the subjects it names, or its `target`. */
|
|
14
19
|
export declare function aims<I, O>(this: Mixer<I, O>, voice: Voice<I, O>, subject: I): boolean;
|
|
15
20
|
/**
|
package/dist/chain.js
CHANGED
|
@@ -16,30 +16,46 @@ export function chain(subject) {
|
|
|
16
16
|
}
|
|
17
17
|
// Apart from `chain`, which stays small enough for V8 to inline where a probe links.
|
|
18
18
|
export function relink(subject, was) {
|
|
19
|
-
const
|
|
19
|
+
const lanes = this.lanes;
|
|
20
|
+
const slot = was !== undefined ? was.slot : lanes !== null ? lanes.number(subject) : -1;
|
|
21
|
+
// A chain a seek kept for its number alone links afresh, as a subject first seen does.
|
|
22
|
+
const sought = was !== undefined && slot >= 0 && lanes?.renew(slot) === true;
|
|
20
23
|
// Where only voices over every subject changed since, those alone are taken off or put on.
|
|
21
|
-
let first = was === undefined
|
|
24
|
+
let first = was === undefined || sought
|
|
22
25
|
? undefined
|
|
23
26
|
: this.steps.patch(was.voice === null ? null : was, was.version, this, subject, slot);
|
|
24
27
|
if (first === undefined)
|
|
25
|
-
first = linkAll(this, subject, slot);
|
|
28
|
+
first = linkAll(this, subject, slot, was ?? null);
|
|
26
29
|
const head = first ?? stub();
|
|
27
30
|
head.version = this.version;
|
|
28
31
|
if (was !== undefined && was !== head) {
|
|
29
|
-
|
|
32
|
+
if (!sought)
|
|
33
|
+
head.loci = was.loci;
|
|
30
34
|
was.loci = null;
|
|
31
|
-
head.slot = was.slot;
|
|
32
35
|
was.slot = -1;
|
|
33
36
|
}
|
|
34
|
-
|
|
35
|
-
head.slot = slot;
|
|
37
|
+
head.slot = slot;
|
|
36
38
|
if (was !== head)
|
|
37
39
|
this.chains.set(subject, head);
|
|
38
40
|
return head;
|
|
39
41
|
}
|
|
40
|
-
/**
|
|
41
|
-
|
|
42
|
+
/**
|
|
43
|
+
* The subject's chain as last linked; undefined where it has none, as for a chain a seek kept only
|
|
44
|
+
* for its number and not linked since, whose records are those from before the seek.
|
|
45
|
+
*/
|
|
46
|
+
export function headOf(mix, subject) {
|
|
47
|
+
const head = mix.chains.get(subject);
|
|
48
|
+
return head !== undefined && head.slot >= 0 && mix.lanes?.sought(head.slot) === true
|
|
49
|
+
? undefined
|
|
50
|
+
: head;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* A subject's chain linked afresh through every voice that reaches it, in voice order; `was` is its
|
|
54
|
+
* chain as last linked, whose records each voice's store is asked whether it still holds.
|
|
55
|
+
*/
|
|
56
|
+
function linkAll(mix, subject, slot, was) {
|
|
42
57
|
let first = null;
|
|
58
|
+
let old = was;
|
|
43
59
|
let prev = null;
|
|
44
60
|
const named = mix.naming === 0 ? undefined : mix.named.get(subject);
|
|
45
61
|
let j = 0;
|
|
@@ -54,7 +70,11 @@ function linkAll(mix, subject, slot) {
|
|
|
54
70
|
break;
|
|
55
71
|
if (next === mine)
|
|
56
72
|
j++;
|
|
57
|
-
|
|
73
|
+
// Taken before linking, which writes over `next`.
|
|
74
|
+
const guess = old !== null && old.voice === next ? old : null;
|
|
75
|
+
if (guess !== null)
|
|
76
|
+
old = guess.next;
|
|
77
|
+
const held = mix.linkable(next, subject, slot, guess);
|
|
58
78
|
if (held !== null) {
|
|
59
79
|
held.voice = next;
|
|
60
80
|
held.next = null;
|
|
@@ -71,7 +91,7 @@ function linkAll(mix, subject, slot) {
|
|
|
71
91
|
return first;
|
|
72
92
|
}
|
|
73
93
|
/** A voice's record for a subject where its chain links it: the voice plays or freezes, and reaches it. */
|
|
74
|
-
export function linkable(voice, subject, slot) {
|
|
94
|
+
export function linkable(voice, subject, slot, guess = null) {
|
|
75
95
|
const state = voice.state;
|
|
76
96
|
if (state === 'done' || (state === 'pending' && !voice.freezesBefore))
|
|
77
97
|
return null;
|
|
@@ -82,7 +102,7 @@ export function linkable(voice, subject, slot) {
|
|
|
82
102
|
sight(voice, slot, this.lanes.cap);
|
|
83
103
|
return null;
|
|
84
104
|
}
|
|
85
|
-
const held = this.held(voice, subject, slot);
|
|
105
|
+
const held = this.held(voice, subject, slot, guess);
|
|
86
106
|
return held.reaches ? held : null;
|
|
87
107
|
}
|
|
88
108
|
/** Whether a voice reaches a subject by what its spec says: the subjects it names, or its `target`. */
|
|
@@ -124,7 +144,7 @@ export function changed(mix, voice) {
|
|
|
124
144
|
return;
|
|
125
145
|
}
|
|
126
146
|
for (const subject of voice.named) {
|
|
127
|
-
const head = mix
|
|
147
|
+
const head = headOf(mix, subject);
|
|
128
148
|
if (head !== undefined)
|
|
129
149
|
unlink(mix, head);
|
|
130
150
|
}
|
package/dist/channels.d.ts
CHANGED
|
@@ -38,6 +38,8 @@ export declare function axesOf(channel: Channel<unknown>): number;
|
|
|
38
38
|
* `'own'` channel, and an angle's `v` is taken the short way round first.
|
|
39
39
|
*/
|
|
40
40
|
export declare function foldNumber(op: Numeric['op'], acc: number, v: number, w: number): number;
|
|
41
|
+
/** The arithmetic `foldNumber` can apply for a channel in place of its `scale` and `merge`, if any. */
|
|
42
|
+
export declare function scalarOp(channel: Channel<unknown>): 'sum' | 'mul' | 'max' | undefined;
|
|
41
43
|
/**
|
|
42
44
|
* What a stock numeric channel takes.
|
|
43
45
|
*
|
package/dist/channels.js
CHANGED
|
@@ -49,7 +49,18 @@ export function foldNumber(op, acc, v, w) {
|
|
|
49
49
|
return product(acc, toward(v, w));
|
|
50
50
|
if (op === 'max')
|
|
51
51
|
return larger(acc, times(v, w));
|
|
52
|
-
|
|
52
|
+
// A double on every branch: where `v` arrives untyped, a branch returning it as it came makes V8
|
|
53
|
+
// box each other branch's result to merge them, 16 bytes a fold (bench/allocs.mjs, 2026-10-20).
|
|
54
|
+
return +v;
|
|
55
|
+
}
|
|
56
|
+
/** The arithmetic `foldNumber` can apply for a channel in place of its `scale` and `merge`, if any. */
|
|
57
|
+
export function scalarOp(channel) {
|
|
58
|
+
const n = numericOf(channel);
|
|
59
|
+
if (n === undefined || n.axes !== 1 || n.turn !== undefined || channel.fold !== undefined)
|
|
60
|
+
return undefined;
|
|
61
|
+
if (typeof channel.rest !== 'number')
|
|
62
|
+
return undefined;
|
|
63
|
+
return n.op === 'sum' || n.op === 'mul' || n.op === 'max' ? n.op : undefined;
|
|
53
64
|
}
|
|
54
65
|
/** Bounds change how a value folds at its edges, so they are part of the kind. */
|
|
55
66
|
const kindOf = (name, bounds) => bounds === undefined ? name : `${name}[${bounds[0]}, ${bounds[1]}]`;
|
package/dist/clone.js
CHANGED
|
@@ -6,19 +6,31 @@ const slow = Symbol('slow');
|
|
|
6
6
|
*/
|
|
7
7
|
export function clone(v) {
|
|
8
8
|
const quick = copied(v, 2);
|
|
9
|
-
|
|
9
|
+
for (let i = 0; i < metCount; i++)
|
|
10
|
+
met[i] = undefined;
|
|
11
|
+
metCount = 0;
|
|
10
12
|
return quick === slow ? structuredClone(v) : quick;
|
|
11
13
|
}
|
|
12
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* The objects one `clone` has met: one met twice is shared, which `structuredClone` keeps. Cleared
|
|
16
|
+
* by hand, as `length = 0` frees the array's storage and the next push allocates it again.
|
|
17
|
+
*/
|
|
13
18
|
const met = [];
|
|
19
|
+
let metCount = 0;
|
|
20
|
+
function hasMet(v) {
|
|
21
|
+
for (let i = 0; i < metCount; i++)
|
|
22
|
+
if (met[i] === v)
|
|
23
|
+
return true;
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
14
26
|
function copied(v, depth) {
|
|
15
27
|
if (typeof v === 'symbol' || typeof v === 'function')
|
|
16
28
|
return slow;
|
|
17
29
|
if (typeof v !== 'object' || v === null)
|
|
18
30
|
return v;
|
|
19
|
-
if (depth === 0 ||
|
|
31
|
+
if (depth === 0 || hasMet(v))
|
|
20
32
|
return slow;
|
|
21
|
-
met
|
|
33
|
+
met[metCount++] = v;
|
|
22
34
|
if (Array.isArray(v)) {
|
|
23
35
|
if (Object.getPrototypeOf(v) !== Array.prototype)
|
|
24
36
|
return slow;
|
package/dist/cue.js
CHANGED
|
@@ -44,9 +44,13 @@ export function cue(mix, spec) {
|
|
|
44
44
|
: Number.isNaN(mix.now)
|
|
45
45
|
? 0
|
|
46
46
|
: mix.now;
|
|
47
|
-
const voice = new Voice(mix.nextId++, spec, patch, spec.fade ??
|
|
47
|
+
const voice = new Voice(mix.nextId++, spec, patch, spec.fade ?? noFade, local, start, mix.slotOf, mix.channels, mix.opts.host, mix.send, owner);
|
|
48
48
|
if (patch === ownerPatch && mix.fitting !== null)
|
|
49
49
|
voice.fitting = mix.fitting;
|
|
50
|
+
if (patch === ownerPatch && mix.arcing !== null) {
|
|
51
|
+
voice.arc = mix.arcing;
|
|
52
|
+
voice.hold = mix.arcing.hold;
|
|
53
|
+
}
|
|
50
54
|
if (owner?.fitting)
|
|
51
55
|
checkChild(spec, voice);
|
|
52
56
|
if (mix.pace !== null && owner === null && spec.start !== undefined && voice.state === 'pending')
|
|
@@ -54,6 +58,12 @@ export function cue(mix, spec) {
|
|
|
54
58
|
enter(mix, voice);
|
|
55
59
|
const h = handle(mix, voice);
|
|
56
60
|
voice.handle = h;
|
|
61
|
+
recordCue(mix, h);
|
|
62
|
+
return h;
|
|
63
|
+
}
|
|
64
|
+
const noFade = Object.freeze({});
|
|
65
|
+
// Apart from `cue`, whose body would otherwise make this closure's context on every call.
|
|
66
|
+
function recordCue(mix, h) {
|
|
57
67
|
// By its handle, so a voice a history store paged out and a seek revived is the one cued again.
|
|
58
68
|
record(mix, 'cue', (stage) => {
|
|
59
69
|
const parked = VoiceHandle.voiceOf(h) ?? lost(h);
|
|
@@ -63,7 +73,6 @@ export function cue(mix, spec) {
|
|
|
63
73
|
else if (to !== undefined)
|
|
64
74
|
recue(to, aheadOf(to, parked));
|
|
65
75
|
});
|
|
66
|
-
return h;
|
|
67
76
|
}
|
|
68
77
|
/** Refuses a patch writing a channel `kit` lacks, or holds as another kind than it was written for. */
|
|
69
78
|
export function fitsKit(patch, kit) {
|
package/dist/departed.d.ts
CHANGED
|
@@ -24,11 +24,15 @@ export interface Departed {
|
|
|
24
24
|
* owner, the first and the last to leave. It grows with those, never with how many voices left.
|
|
25
25
|
*/
|
|
26
26
|
export declare class DepartedIndex {
|
|
27
|
-
private readonly
|
|
27
|
+
private readonly scopes;
|
|
28
28
|
readonly all: Set<Departed>;
|
|
29
29
|
add<I, O>(mix: Mixer<I, O>, voice: Voice<I, O>): void;
|
|
30
30
|
/** Lets go of the voices an owner that has left held: nothing can be cued beside them again. */
|
|
31
31
|
dropOwner(owner: number): void;
|
|
32
|
+
private scopeOf;
|
|
33
|
+
private keep;
|
|
34
|
+
private hold;
|
|
35
|
+
private forget;
|
|
32
36
|
private release;
|
|
33
37
|
}
|
|
34
38
|
/** Whether a departed voice answers `q`, asked by a voice of `owner` (-1 for none) on `score`. */
|