pixi-effects 0.27.1 → 0.28.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.
- package/CHANGELOG.md +19 -0
- package/README.md +7 -7
- package/ai/chat-template.html +3 -3
- package/ai/tools/check.mjs +30 -0
- package/dist/{Base-CKGl6jCo.d.ts → Base-C6f0fE8j.d.ts} +1 -1
- package/dist/{Base-DjhdL8B1.d.cts → Base-DI1FCGcH.d.cts} +1 -1
- package/dist/Composition-HFEVIYHN.js +7 -0
- package/dist/{Composition-BJVY5HP7.js.map → Composition-HFEVIYHN.js.map} +1 -1
- package/dist/Composition-NI74UJT3.cjs +16 -0
- package/dist/{Composition-YW2G6EXL.cjs.map → Composition-NI74UJT3.cjs.map} +1 -1
- package/dist/Controller.d.cts +2 -2
- package/dist/Controller.d.ts +2 -2
- package/dist/{Movie-CbYwTs6U.d.cts → Movie-CHcM8HNB.d.ts} +3 -1
- package/dist/{Movie-ILKq2fw_.d.ts → Movie-Cx5ozUGb.d.cts} +3 -1
- package/dist/Presenter.d.cts +2 -2
- package/dist/Presenter.d.ts +2 -2
- package/dist/chunk-4LEX3G3M.cjs +259 -0
- package/dist/chunk-4LEX3G3M.cjs.map +1 -0
- package/dist/{chunk-X42BGR6P.js → chunk-AK54FIWM.js} +7 -4
- package/dist/chunk-AK54FIWM.js.map +1 -0
- package/dist/{chunk-ETKTRQL2.cjs → chunk-MZCDBRTY.cjs} +7 -4
- package/dist/chunk-MZCDBRTY.cjs.map +1 -0
- package/dist/{chunk-VH5R473P.cjs → chunk-SBOVUZZY.cjs} +61 -61
- package/dist/{chunk-VH5R473P.cjs.map → chunk-SBOVUZZY.cjs.map} +1 -1
- package/dist/{chunk-AR3HVHKF.js → chunk-WEJAQAFX.js} +3 -3
- package/dist/{chunk-AR3HVHKF.js.map → chunk-WEJAQAFX.js.map} +1 -1
- package/dist/chunk-ZHUUIF6A.js +250 -0
- package/dist/chunk-ZHUUIF6A.js.map +1 -0
- package/dist/events-EE3SM5LM.cjs +16 -0
- package/dist/events-EE3SM5LM.cjs.map +1 -0
- package/dist/events-KXBWR4IV.js +3 -0
- package/dist/events-KXBWR4IV.js.map +1 -0
- package/dist/index.cjs +49 -33
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +19 -5
- package/dist/index.d.ts +19 -5
- package/dist/index.js +18 -7
- package/dist/index.js.map +1 -1
- package/dist/loader.css +1 -1
- package/dist/{music-YPUZTWTN.cjs → music-6WFTPVHU.cjs} +308 -297
- package/dist/music-6WFTPVHU.cjs.map +1 -0
- package/dist/{music-JVRNXHQ5.js → music-JKOM5YK6.js} +273 -287
- package/dist/music-JKOM5YK6.js.map +1 -0
- package/dist/three.cjs +5 -5
- package/dist/three.d.cts +2 -2
- package/dist/three.d.ts +2 -2
- package/dist/three.js +1 -1
- package/dist/{types-BNVsBBZf.d.cts → types-DpK1vwcl.d.cts} +79 -1
- package/dist/{types-BNVsBBZf.d.ts → types-DpK1vwcl.d.ts} +79 -1
- package/docs/api.md +28 -0
- package/docs/dsl.md +8 -4
- package/llms-full.txt +39 -6
- package/package.json +1 -1
- package/skills/pixi-effects/SKILL.md +1 -1
- package/skills/pixi-effects/reference/cheatsheet.md +1 -1
- package/skills/pixi-effects/reference/pitfalls.md +1 -0
- package/skills/pixi-effects/template.html +6 -6
- package/dist/Composition-BJVY5HP7.js +0 -7
- package/dist/Composition-YW2G6EXL.cjs +0 -16
- package/dist/chunk-ETKTRQL2.cjs.map +0 -1
- package/dist/chunk-X42BGR6P.js.map +0 -1
- package/dist/music-JVRNXHQ5.js.map +0 -1
- package/dist/music-YPUZTWTN.cjs.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.28.0
|
|
4
|
+
|
|
5
|
+
**Added**
|
|
6
|
+
|
|
7
|
+
- **`musicEvents(music)`: what a score plays, as data.** Every note and chord with its `beat`, `time` (seconds, tempo map applied), `duration`, `bar`, `beatInBar`, audible `midi` pitches and the `name` the score gave it, and every drum hit with the pattern repeated out. The picture can follow the score (light the keys that sound, caption the chord (`name` is as written; `midi` is what sounds, transpose included)) without analysing audio. A score that cannot play throws with the reason.
|
|
8
|
+
- **`music.meter`** (beats in a bar, default 4, 1–16): it names the bars in `musicEvents()` and in the `check` read-back; playback does not use it.
|
|
9
|
+
- **`check` reads a music layer back.** A `music` block per layer: its length in bars per track (a track that ends early, or has a short bar, is named), the names that start in each bar with their beat (`Am(2)` starts on beat 2), and the drum spans, so an author can verify a score without listening. `movie.inspectAudio()` sources carry the same summary (`source.music`).
|
|
10
|
+
- **The music docs say what the author had to guess**: drums repeat by themselves and `from` / `to` are beats; a plain capital is a major chord; sharps and flats; which octave each instrument sits in; `swing` moves only off-beat notes (a comp on beats 2 and 4 stays straight, and how to write it); pitfall 100. Found by fresh Sonnet and Haiku authors writing six scores from the old docs: all but one passed first time, but nearly every one said it could not check bar lengths, drum spans or pitches without hand counting.
|
|
11
|
+
- **`Sequence` is exported, so a layer type can be written outside the library** (`registerSequenceType` was public, its base class was not). A subclass builds a Pixi display object in `build()` and may draw each frame in `awaitFrameAt(local)`; keyframes, filters, masks and the lifespan then work on it like on the built-in layers. See "Your own layer type" in `docs/api.md`.
|
|
12
|
+
- **A gallery piece with d3 (51 now): `d3-story`.** d3 computes the numbers (scales, `line`, `area`, `pie`, `arc`, `geoPath`, a seeded `randomLcg`) and pixi-effects performs them: bars that count up, a self-drawing line, a donut, a world map with great-circle routes. It imports `d3` and `topojson-client` through the import map; the library did not change.
|
|
13
|
+
- **A gallery piece with Rough.js (52 now): `sketch-launch`.** Rough.js makes the wobbly outlines and hachure fills as path data; pixi-effects draws them on (`trimEnd`), lifts the rocket off as one composition and flips three flame drawings like a flipbook. It imports `roughjs` through the import map; the library did not change. (Rough.js's `dots` fill ignores its `seed`, so two exports differed: the page uses `cross-hatch`.)
|
|
14
|
+
|
|
15
|
+
## 0.27.2
|
|
16
|
+
|
|
17
|
+
**Changed**
|
|
18
|
+
|
|
19
|
+
- The footer link to the author's X page now reads "Author on X" (it said "Updates on X"), and the README says only who made it.
|
|
20
|
+
- **The music is made more than twice as fast, with the same samples (to the last bit).** The reel's 80 seconds: 4.45 s to 1.9 s in Node, the sound stage of its load 4.9 s to 2.5 s in Chrome (the whole load 7 s to 4.4 s). The electric piano no longer computes the 1.2 s of silence after each note; what depends only on the pitch and the sample index (the exponentials and the modulator sines of the electric piano and the bell) is computed once per pitch for the render and shared by its notes; a track is filtered and mixed only where it sounds (and until its filter has run out); the volume curve is followed instead of searched; the five saws of the pad, the drums' writes and the reverb are plain loops; the long loops of the mix run outside the generator that slices the render. Four checksums of full renders (two with `loop`) taken before the changes are pinned in tests, and the reel's own checksum was checked.
|
|
21
|
+
|
|
3
22
|
## 0.27.1
|
|
4
23
|
|
|
5
24
|
**Fixed**
|
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# pixi-effects
|
|
2
2
|
|
|
3
|
-
> **Status**: experimental — current release `0.
|
|
3
|
+
> **Status**: experimental — current release `0.28.0`. The API may still change between minor versions; see the [CHANGELOG](./CHANGELOG.md) for what each release changed.
|
|
4
4
|
|
|
5
|
-
**[Guide →](https://yjmtmtk.github.io/pixi-effects/guide/)** · **[Gallery →](https://yjmtmtk.github.io/pixi-effects/examples/gallery/)** ·
|
|
5
|
+
**[Guide →](https://yjmtmtk.github.io/pixi-effects/guide/)** · **[Gallery →](https://yjmtmtk.github.io/pixi-effects/examples/gallery/)** · 52 portfolio pieces written as plain data by AI models · **[Examples →](https://yjmtmtk.github.io/pixi-effects/examples/)** · 15 numbered examples, a music lab + an in-browser playground.
|
|
6
6
|
|
|
7
7
|
## Make a video by asking an AI
|
|
8
8
|
|
|
@@ -49,8 +49,8 @@ Drop the imports into an [importmap](https://developer.mozilla.org/docs/Web/HTML
|
|
|
49
49
|
"imports": {
|
|
50
50
|
"pixi.js": "https://esm.sh/pixi.js@8.22.0?bundle-deps",
|
|
51
51
|
"mediabunny": "https://esm.sh/mediabunny",
|
|
52
|
-
"pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
53
|
-
"pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
52
|
+
"pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.28.0/dist/index.js",
|
|
53
|
+
"pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.28.0/dist/Controller.js"
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
</script>
|
|
@@ -61,7 +61,7 @@ Drop the imports into an [importmap](https://developer.mozilla.org/docs/Web/HTML
|
|
|
61
61
|
</script>
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
-
Load the `dist/` files **as they are** (jsDelivr's `/npm/…/dist/…`, or unpkg's `https://unpkg.com/pixi-effects@0.
|
|
64
|
+
Load the `dist/` files **as they are** (jsDelivr's `/npm/…/dist/…`, or unpkg's `https://unpkg.com/pixi-effects@0.28.0/dist/index.js`) rather than a CDN-rebundled build such as `esm.sh/pixi-effects` or jsDelivr's `+esm`: the entries (`pixi-effects`, `…/controller`, `…/three`) share internal chunks, which only works when each file is served untouched.
|
|
65
65
|
|
|
66
66
|
> **Using three.js?** Add two more entries (`three` and `pixi-effects/three`) to this importmap — see [Adding three.js](#adding-threejs-optional) below.
|
|
67
67
|
>
|
|
@@ -77,7 +77,7 @@ Add two more entries to the importmap above: three.js itself, and the `pixi-effe
|
|
|
77
77
|
"imports": {
|
|
78
78
|
"...": "(everything from the importmap above)",
|
|
79
79
|
"three": "https://esm.sh/three@0.178.0",
|
|
80
|
-
"pixi-effects/three": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
80
|
+
"pixi-effects/three": "https://cdn.jsdelivr.net/npm/pixi-effects@0.28.0/dist/three.js"
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
</script>
|
|
@@ -221,7 +221,7 @@ Some codecs (notably `aac` audio in older Firefox) may need the [mediabunny poly
|
|
|
221
221
|
|
|
222
222
|
## Author
|
|
223
223
|
|
|
224
|
-
Made by [@t_yjm](https://x.com/t_yjm)
|
|
224
|
+
Made by [@t_yjm](https://x.com/t_yjm). Questions, bugs and ideas are welcome as [GitHub issues](https://github.com/yjmtmtk/pixi-effects/issues).
|
|
225
225
|
|
|
226
226
|
## License
|
|
227
227
|
|
package/ai/chat-template.html
CHANGED
|
@@ -59,9 +59,9 @@
|
|
|
59
59
|
"imports": {
|
|
60
60
|
"pixi.js": "https://cdn.jsdelivr.net/npm/pixi.js@8.22.0/dist/pixi.min.mjs",
|
|
61
61
|
"mediabunny": "https://cdn.jsdelivr.net/npm/mediabunny@1.61.3/+esm",
|
|
62
|
-
"pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
63
|
-
"pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
64
|
-
"pixi-effects/presenter": "https://cdn.jsdelivr.net/npm/pixi-effects@0.
|
|
62
|
+
"pixi-effects": "https://cdn.jsdelivr.net/npm/pixi-effects@0.28.0/dist/index.js",
|
|
63
|
+
"pixi-effects/controller": "https://cdn.jsdelivr.net/npm/pixi-effects@0.28.0/dist/Controller.js",
|
|
64
|
+
"pixi-effects/presenter": "https://cdn.jsdelivr.net/npm/pixi-effects@0.28.0/dist/Presenter.js"
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
</script>
|
package/ai/tools/check.mjs
CHANGED
|
@@ -28,6 +28,35 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
|
28
28
|
|
|
29
29
|
// ───────────────────────────── pure helpers (unit-tested) ─────────────────────────────
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* A music layer's score read back, so the author can check it without listening: the length of every track in bars (a track that ends early or
|
|
33
|
+
* has a short bar is named), the names that start in each bar, and where the drums play. One header line per music layer; none for other sources.
|
|
34
|
+
*/
|
|
35
|
+
export function formatMusic(sources) {
|
|
36
|
+
const out = [];
|
|
37
|
+
const pad = ' ';
|
|
38
|
+
const num = x => String(Math.round(x * 100) / 100);
|
|
39
|
+
for (const s of sources ?? []) {
|
|
40
|
+
const m = s.music;
|
|
41
|
+
if (!m) continue;
|
|
42
|
+
const assumed = m.meterSet === false ? ` (no meter set: ${m.meter} assumed; a waltz is meter: 3)` : '';
|
|
43
|
+
out.push(` music ${s.layer} · ${num(m.bpm)} bpm · ${num(m.beats)} beats = ${num(m.bars)} bars of ${m.meter} beats${assumed} · ${num(Math.round(m.seconds * 10) / 10)} s (+ ${num(m.tail)} s tail)`);
|
|
44
|
+
for (const t of m.tracks) {
|
|
45
|
+
const whole = Math.abs(t.bars - Math.round(t.bars)) < 1e-6;
|
|
46
|
+
// a track shorter than the longest stops early (whatever its length in bars); a short BAR can only be the longest track's, and only the author's meter can say so
|
|
47
|
+
const note = t.beats < m.beats - 1e-6 ? ` (ends early: silent from beat ${num(t.beats)})` : !whole && m.meterSet !== false ? ' (not a whole number of bars: a bar is short)' : '';
|
|
48
|
+
const cell = names => { const n = names.match(/\[[^\]]*\]\S*|\S+/g) ?? []; return n.length === 0 ? '–' : n.length > 4 ? `${n.slice(0, 4).join(' ')} …` : n.join(' '); };
|
|
49
|
+
const shown = t.perBar.slice(0, 16).map(cell).join(' | ');
|
|
50
|
+
out.push(`${pad}${t.inst.padEnd(6)} ${num(t.bars)} bar${t.bars === 1 ? '' : 's'}${note} | ${shown}${t.perBar.length > 16 ? ` | … (${t.perBar.length - 16} more)` : ''}`);
|
|
51
|
+
}
|
|
52
|
+
for (const d of m.drums) {
|
|
53
|
+
if (!d.kinds.length) continue;
|
|
54
|
+
out.push(`${pad}drums ${d.kinds.join(', ')} · beats ${num(d.from)}–${num(d.to)} (bars ${Math.floor(d.from / m.meter + 1e-9) + 1}–${Math.max(1, Math.ceil(d.to / m.meter - 1e-9))})`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
|
|
31
60
|
/** `assets 0.1 s · build 2.4 s (slowest) · sound 0.6 s · frames 0.2 s` from `movie.loadStages` (milliseconds); the slowest stage is named only when the load took 1 s or more. */
|
|
32
61
|
export function formatLoadStages(stages) {
|
|
33
62
|
if (!stages) return '';
|
|
@@ -510,6 +539,7 @@ function finish(report, outDir, log) {
|
|
|
510
539
|
if (report.audio !== undefined) {
|
|
511
540
|
const a = report.audio;
|
|
512
541
|
L.push(a ? ` audio ${a.sources.length} source(s) · mix peak ${a.peakDb} dBFS · ${a.loudness?.integratedLufs ?? 'n/a'} LUFS · true peak ${a.loudness?.truePeakDb ?? 'n/a'} dBTP · ${a.issues.length ? a.issues.join(' | ') : 'no issues (movie.inspectAudio)'}` : ' audio none');
|
|
542
|
+
for (const l of formatMusic(a?.sources)) L.push(l);
|
|
513
543
|
for (const n of a?.notes ?? []) L.push(` note ${n}`);
|
|
514
544
|
}
|
|
515
545
|
for (const ex of report.exports ?? []) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Filter, Container, RenderTexture, Matrix } from 'pixi.js';
|
|
2
|
-
import { S as SequenceSpec, C as CompositionShape,
|
|
2
|
+
import { S as SequenceSpec, C as CompositionShape, ac as Timeline, ad as AudioDescriptor } from './types-DpK1vwcl.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Resolves a routed keyframe path to a concrete tween target.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Filter, Container, RenderTexture, Matrix } from 'pixi.js';
|
|
2
|
-
import { S as SequenceSpec, C as CompositionShape,
|
|
2
|
+
import { S as SequenceSpec, C as CompositionShape, ac as Timeline, ad as AudioDescriptor } from './types-DpK1vwcl.cjs';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Resolves a routed keyframe path to a concrete tween target.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { CompositionSequence } from './chunk-WEJAQAFX.js';
|
|
2
|
+
import './chunk-AK54FIWM.js';
|
|
3
|
+
import './chunk-TYWFKCPY.js';
|
|
4
|
+
import './chunk-45QKLD6M.js';
|
|
5
|
+
import './chunk-BXOCEZ4U.js';
|
|
6
|
+
//# sourceMappingURL=Composition-HFEVIYHN.js.map
|
|
7
|
+
//# sourceMappingURL=Composition-HFEVIYHN.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":[],"names":[],"mappings":"","file":"Composition-
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"Composition-HFEVIYHN.js"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var chunkSBOVUZZY_cjs = require('./chunk-SBOVUZZY.cjs');
|
|
4
|
+
require('./chunk-MZCDBRTY.cjs');
|
|
5
|
+
require('./chunk-P4GY3Z4G.cjs');
|
|
6
|
+
require('./chunk-ZMAD3KJU.cjs');
|
|
7
|
+
require('./chunk-TWZNOUM7.cjs');
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
Object.defineProperty(exports, "CompositionSequence", {
|
|
12
|
+
enumerable: true,
|
|
13
|
+
get: function () { return chunkSBOVUZZY_cjs.CompositionSequence; }
|
|
14
|
+
});
|
|
15
|
+
//# sourceMappingURL=Composition-NI74UJT3.cjs.map
|
|
16
|
+
//# sourceMappingURL=Composition-NI74UJT3.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":[],"names":[],"mappings":"","file":"Composition-
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"Composition-NI74UJT3.cjs"}
|
package/dist/Controller.d.cts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { j as Movie } from './Movie-
|
|
1
|
+
import { j as Movie } from './Movie-Cx5ozUGb.cjs';
|
|
2
2
|
import 'pixi.js';
|
|
3
|
-
import './types-
|
|
3
|
+
import './types-DpK1vwcl.cjs';
|
|
4
4
|
|
|
5
5
|
/** The bar's default stylesheet. Its colours and sizes are CSS custom properties (`--mc-accent`, `--mc-fg`, `--mc-track`, `--mc-bar-bg`, `--mc-track-height`, `--mc-font`): set them in your page CSS, or with the `theme` option. */
|
|
6
6
|
declare const CONTROLLER_CSS = "\n.movie-controller-wrap { position: relative; display: inline-block; line-height: 0; }\n.movie-controller {\n position: absolute;\n pointer-events: none;\n font-family: var(--mc-font, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif);\n display: flex;\n flex-direction: column;\n justify-content: flex-end;\n opacity: 1;\n transition: opacity 200ms ease;\n}\n.movie-controller[data-state=\"hidden\"] { opacity: 0; }\n.movie-controller > * { pointer-events: auto; }\n\n.mc-progress {\n position: relative;\n width: 100%;\n height: 16px;\n cursor: pointer;\n display: flex;\n align-items: center;\n outline: none;\n}\n.mc-progress:focus-visible {\n outline: 2px solid var(--mc-accent, #007AFF);\n outline-offset: 2px;\n}\n.mc-progress::before {\n content: \"\"; position: absolute; left: 12px; right: 12px;\n height: var(--mc-track-height, 3px); background: var(--mc-track, rgba(255,255,255,0.25));\n transition: height 120ms ease;\n}\n.mc-progress:hover::before, .mc-progress.mc-scrubbing::before { height: calc(var(--mc-track-height, 3px) + 2px); }\n.mc-stops { position: absolute; left: 12px; right: 12px; top: 0; bottom: 0; pointer-events: none; }\n.mc-stop-tick { position: absolute; top: 50%; width: 7px; height: 7px; transform: translate(-50%, -50%); border-radius: 50%; background: var(--mc-fg, #fff); box-shadow: 0 0 0 1.5px rgba(0,0,0,0.5); }\n.mc-stop-page { width: 11px; height: 11px; box-shadow: 0 0 0 2.5px var(--mc-accent, #007AFF), 0 0 0 4px rgba(0,0,0,0.35); }\n.mc-progress-fill {\n position: absolute; left: 12px; top: 50%;\n height: var(--mc-track-height, 3px);\n width: calc((100% - 24px) * var(--mc-fill, 0));\n background: var(--mc-accent, #007AFF);\n transform: translateY(-50%);\n transition: height 120ms ease;\n pointer-events: none;\n}\n.mc-progress:hover .mc-progress-fill,\n.mc-progress.mc-scrubbing .mc-progress-fill { height: calc(var(--mc-track-height, 3px) + 2px); }\n.mc-progress-thumb {\n position: absolute; top: 50%;\n left: calc(12px + (100% - 24px) * var(--mc-fill, 0));\n width: 12px; height: 12px; border-radius: 50%;\n background: var(--mc-accent, #007AFF);\n transform: translate(-50%, -50%) scale(0);\n transition: transform 120ms ease;\n pointer-events: none;\n}\n.mc-progress:hover .mc-progress-thumb,\n.mc-progress.mc-scrubbing .mc-progress-thumb { transform: translate(-50%, -50%) scale(1); }\n\n.mc-bar {\n position: relative;\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 6px 12px 8px 12px;\n background: var(--mc-bar-bg, linear-gradient(to top, rgba(0,0,0,0.75) 0%, rgba(0,0,0,0) 100%));\n color: var(--mc-fg, #fff);\n line-height: 1;\n}\n.mc-btn {\n background: none; border: 0; padding: 0; margin: 0;\n width: 28px; height: 28px;\n display: inline-flex; align-items: center; justify-content: center;\n color: var(--mc-fg, #fff); opacity: 0.85; cursor: pointer;\n transition: opacity 120ms ease;\n}\n.mc-btn:hover { opacity: 1; }\n.mc-btn:disabled { opacity: 0.4; cursor: not-allowed; }\n.mc-time {\n font-size: 12px; font-family: Menlo, Monaco, monospace;\n color: var(--mc-fg, #fff); opacity: 0.85;\n min-width: 90px;\n}\n.mc-spacer { flex: 1; }\n\n.mc-volume { display: flex; align-items: center; }\n.mc-vol-slider {\n position: relative;\n width: 0;\n height: 28px;\n display: flex;\n align-items: center;\n cursor: pointer;\n margin-left: 0;\n outline: none;\n transition: width 200ms ease, margin-left 200ms ease;\n overflow: hidden;\n}\n.mc-volume:hover .mc-vol-slider,\n.mc-volume:focus-within .mc-vol-slider,\n.mc-vol-slider.mc-scrubbing {\n width: 70px;\n margin-left: 6px;\n}\n.mc-vol-slider:focus-visible { outline: 2px solid var(--mc-accent, #007AFF); outline-offset: 2px; }\n.mc-vol-slider::before {\n content: \"\"; position: absolute; left: 4px; right: 4px;\n height: 3px; background: var(--mc-track, rgba(255,255,255,0.25)); border-radius: 2px;\n}\n.mc-vol-fill {\n position: absolute; left: 4px; top: 50%;\n height: 3px;\n width: calc((100% - 8px) * var(--mc-volume, 1));\n background: var(--mc-fg, #fff);\n transform: translateY(-50%);\n border-radius: 2px;\n pointer-events: none;\n}\n.mc-vol-thumb {\n position: absolute; top: 50%;\n left: calc(4px + (100% - 8px) * var(--mc-volume, 1));\n width: 10px; height: 10px; border-radius: 50%;\n background: var(--mc-fg, #fff);\n transform: translate(-50%, -50%);\n pointer-events: none;\n}\n\n[data-mc-wrap]:fullscreen {\n width: 100vw; height: 100vh;\n display: flex; align-items: center; justify-content: center;\n background: #000;\n}\n[data-mc-wrap]:fullscreen > canvas {\n width: 100% !important;\n height: 100% !important;\n max-width: none !important;\n max-height: none !important;\n object-fit: contain;\n border-radius: 0;\n}\n\n.mc-settings-popover {\n position: absolute;\n bottom: calc(100% + 8px);\n right: 12px;\n background: rgba(20, 20, 20, 0.96);\n border-radius: 8px;\n padding: 10px 12px;\n min-width: 180px;\n color: #fff;\n font-size: 12px;\n box-shadow: 0 4px 16px rgba(0,0,0,0.4);\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n.mc-settings-popover[data-open=\"false\"] { display: none; }\n.mc-settings-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n}\n.mc-settings-row > span { color: rgba(255,255,255,0.85); }\n.mc-settings-row > select {\n font: inherit;\n background: rgba(255,255,255,0.1);\n color: #fff;\n border: 1px solid rgba(255,255,255,0.18);\n border-radius: 4px;\n padding: 2px 6px;\n cursor: pointer;\n}\n.mc-export-confirm {\n margin-top: 4px;\n width: 100%;\n background: var(--mc-accent, #007AFF);\n color: #fff;\n border: 0;\n border-radius: 4px;\n padding: 6px 10px;\n font: inherit;\n font-weight: 600;\n cursor: pointer;\n transition: background 120ms ease;\n}\n.mc-export-confirm:hover { background: #0066d6; }\n.mc-export-confirm:disabled { background: rgba(255,255,255,0.18); cursor: not-allowed; }\n\n.mc-export-overlay {\n position: absolute; inset: 0;\n background: rgba(0,0,0,0.7);\n display: flex; justify-content: center; align-items: center;\n z-index: 2;\n}\n.mc-export-panel {\n background: rgba(20,20,20,0.96); color: #fff;\n border-radius: 8px; padding: 16px 20px;\n text-align: center; min-width: 200px; max-width: 80%;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif;\n}\n.mc-export-title { font-size: 13px; margin-bottom: 10px; opacity: 0.9; }\n.mc-export-track {\n width: 100%; height: 6px;\n background: rgba(255,255,255,0.15); border-radius: 3px;\n margin-bottom: 8px; overflow: hidden;\n}\n.mc-export-fill {\n height: 100%; width: 0%;\n background: linear-gradient(90deg, var(--mc-accent, #007AFF), var(--mc-accent, #0056CC));\n transition: width 0.3s ease; border-radius: 3px;\n}\n.mc-export-text {\n color: rgba(255,255,255,0.7); font-size: 11px;\n font-family: Menlo, Monaco, monospace;\n}\n";
|
package/dist/Controller.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { j as Movie } from './Movie-
|
|
1
|
+
import { j as Movie } from './Movie-CHcM8HNB.js';
|
|
2
2
|
import 'pixi.js';
|
|
3
|
-
import './types-
|
|
3
|
+
import './types-DpK1vwcl.js';
|
|
4
4
|
|
|
5
5
|
/** The bar's default stylesheet. Its colours and sizes are CSS custom properties (`--mc-accent`, `--mc-fg`, `--mc-track`, `--mc-bar-bg`, `--mc-track-height`, `--mc-font`): set them in your page CSS, or with the `theme` option. */
|
|
6
6
|
declare const CONTROLLER_CSS = "\n.movie-controller-wrap { position: relative; display: inline-block; line-height: 0; }\n.movie-controller {\n position: absolute;\n pointer-events: none;\n font-family: var(--mc-font, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif);\n display: flex;\n flex-direction: column;\n justify-content: flex-end;\n opacity: 1;\n transition: opacity 200ms ease;\n}\n.movie-controller[data-state=\"hidden\"] { opacity: 0; }\n.movie-controller > * { pointer-events: auto; }\n\n.mc-progress {\n position: relative;\n width: 100%;\n height: 16px;\n cursor: pointer;\n display: flex;\n align-items: center;\n outline: none;\n}\n.mc-progress:focus-visible {\n outline: 2px solid var(--mc-accent, #007AFF);\n outline-offset: 2px;\n}\n.mc-progress::before {\n content: \"\"; position: absolute; left: 12px; right: 12px;\n height: var(--mc-track-height, 3px); background: var(--mc-track, rgba(255,255,255,0.25));\n transition: height 120ms ease;\n}\n.mc-progress:hover::before, .mc-progress.mc-scrubbing::before { height: calc(var(--mc-track-height, 3px) + 2px); }\n.mc-stops { position: absolute; left: 12px; right: 12px; top: 0; bottom: 0; pointer-events: none; }\n.mc-stop-tick { position: absolute; top: 50%; width: 7px; height: 7px; transform: translate(-50%, -50%); border-radius: 50%; background: var(--mc-fg, #fff); box-shadow: 0 0 0 1.5px rgba(0,0,0,0.5); }\n.mc-stop-page { width: 11px; height: 11px; box-shadow: 0 0 0 2.5px var(--mc-accent, #007AFF), 0 0 0 4px rgba(0,0,0,0.35); }\n.mc-progress-fill {\n position: absolute; left: 12px; top: 50%;\n height: var(--mc-track-height, 3px);\n width: calc((100% - 24px) * var(--mc-fill, 0));\n background: var(--mc-accent, #007AFF);\n transform: translateY(-50%);\n transition: height 120ms ease;\n pointer-events: none;\n}\n.mc-progress:hover .mc-progress-fill,\n.mc-progress.mc-scrubbing .mc-progress-fill { height: calc(var(--mc-track-height, 3px) + 2px); }\n.mc-progress-thumb {\n position: absolute; top: 50%;\n left: calc(12px + (100% - 24px) * var(--mc-fill, 0));\n width: 12px; height: 12px; border-radius: 50%;\n background: var(--mc-accent, #007AFF);\n transform: translate(-50%, -50%) scale(0);\n transition: transform 120ms ease;\n pointer-events: none;\n}\n.mc-progress:hover .mc-progress-thumb,\n.mc-progress.mc-scrubbing .mc-progress-thumb { transform: translate(-50%, -50%) scale(1); }\n\n.mc-bar {\n position: relative;\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 6px 12px 8px 12px;\n background: var(--mc-bar-bg, linear-gradient(to top, rgba(0,0,0,0.75) 0%, rgba(0,0,0,0) 100%));\n color: var(--mc-fg, #fff);\n line-height: 1;\n}\n.mc-btn {\n background: none; border: 0; padding: 0; margin: 0;\n width: 28px; height: 28px;\n display: inline-flex; align-items: center; justify-content: center;\n color: var(--mc-fg, #fff); opacity: 0.85; cursor: pointer;\n transition: opacity 120ms ease;\n}\n.mc-btn:hover { opacity: 1; }\n.mc-btn:disabled { opacity: 0.4; cursor: not-allowed; }\n.mc-time {\n font-size: 12px; font-family: Menlo, Monaco, monospace;\n color: var(--mc-fg, #fff); opacity: 0.85;\n min-width: 90px;\n}\n.mc-spacer { flex: 1; }\n\n.mc-volume { display: flex; align-items: center; }\n.mc-vol-slider {\n position: relative;\n width: 0;\n height: 28px;\n display: flex;\n align-items: center;\n cursor: pointer;\n margin-left: 0;\n outline: none;\n transition: width 200ms ease, margin-left 200ms ease;\n overflow: hidden;\n}\n.mc-volume:hover .mc-vol-slider,\n.mc-volume:focus-within .mc-vol-slider,\n.mc-vol-slider.mc-scrubbing {\n width: 70px;\n margin-left: 6px;\n}\n.mc-vol-slider:focus-visible { outline: 2px solid var(--mc-accent, #007AFF); outline-offset: 2px; }\n.mc-vol-slider::before {\n content: \"\"; position: absolute; left: 4px; right: 4px;\n height: 3px; background: var(--mc-track, rgba(255,255,255,0.25)); border-radius: 2px;\n}\n.mc-vol-fill {\n position: absolute; left: 4px; top: 50%;\n height: 3px;\n width: calc((100% - 8px) * var(--mc-volume, 1));\n background: var(--mc-fg, #fff);\n transform: translateY(-50%);\n border-radius: 2px;\n pointer-events: none;\n}\n.mc-vol-thumb {\n position: absolute; top: 50%;\n left: calc(4px + (100% - 8px) * var(--mc-volume, 1));\n width: 10px; height: 10px; border-radius: 50%;\n background: var(--mc-fg, #fff);\n transform: translate(-50%, -50%);\n pointer-events: none;\n}\n\n[data-mc-wrap]:fullscreen {\n width: 100vw; height: 100vh;\n display: flex; align-items: center; justify-content: center;\n background: #000;\n}\n[data-mc-wrap]:fullscreen > canvas {\n width: 100% !important;\n height: 100% !important;\n max-width: none !important;\n max-height: none !important;\n object-fit: contain;\n border-radius: 0;\n}\n\n.mc-settings-popover {\n position: absolute;\n bottom: calc(100% + 8px);\n right: 12px;\n background: rgba(20, 20, 20, 0.96);\n border-radius: 8px;\n padding: 10px 12px;\n min-width: 180px;\n color: #fff;\n font-size: 12px;\n box-shadow: 0 4px 16px rgba(0,0,0,0.4);\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n.mc-settings-popover[data-open=\"false\"] { display: none; }\n.mc-settings-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n}\n.mc-settings-row > span { color: rgba(255,255,255,0.85); }\n.mc-settings-row > select {\n font: inherit;\n background: rgba(255,255,255,0.1);\n color: #fff;\n border: 1px solid rgba(255,255,255,0.18);\n border-radius: 4px;\n padding: 2px 6px;\n cursor: pointer;\n}\n.mc-export-confirm {\n margin-top: 4px;\n width: 100%;\n background: var(--mc-accent, #007AFF);\n color: #fff;\n border: 0;\n border-radius: 4px;\n padding: 6px 10px;\n font: inherit;\n font-weight: 600;\n cursor: pointer;\n transition: background 120ms ease;\n}\n.mc-export-confirm:hover { background: #0066d6; }\n.mc-export-confirm:disabled { background: rgba(255,255,255,0.18); cursor: not-allowed; }\n\n.mc-export-overlay {\n position: absolute; inset: 0;\n background: rgba(0,0,0,0.7);\n display: flex; justify-content: center; align-items: center;\n z-index: 2;\n}\n.mc-export-panel {\n background: rgba(20,20,20,0.96); color: #fff;\n border-radius: 8px; padding: 16px 20px;\n text-align: center; min-width: 200px; max-width: 80%;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif;\n}\n.mc-export-title { font-size: 13px; margin-bottom: 10px; opacity: 0.9; }\n.mc-export-track {\n width: 100%; height: 6px;\n background: rgba(255,255,255,0.15); border-radius: 3px;\n margin-bottom: 8px; overflow: hidden;\n}\n.mc-export-fill {\n height: 100%; width: 0%;\n background: linear-gradient(90deg, var(--mc-accent, #007AFF), var(--mc-accent, #0056CC));\n transition: width 0.3s ease; border-radius: 3px;\n}\n.mc-export-text {\n color: rgba(255,255,255,0.7); font-size: 11px;\n font-family: Menlo, Monaco, monospace;\n}\n";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Application } from 'pixi.js';
|
|
2
|
-
import {
|
|
2
|
+
import { O as MusicSummary, ac as Timeline, a4 as Stop, f as AssetSpec, d as CompositionSpec } from './types-DpK1vwcl.js';
|
|
3
3
|
|
|
4
4
|
interface AudioInspectOptions {
|
|
5
5
|
/** Width of each loudness window in seconds. Default 0.1. */
|
|
@@ -33,6 +33,8 @@ interface AudioSourceReport {
|
|
|
33
33
|
/** Loudest sample of the MIX while this source plays, dBFS (−120 = silence). */
|
|
34
34
|
peakDb: number;
|
|
35
35
|
sound: SoundMeasure;
|
|
36
|
+
/** For a `music` layer: its score read back (bars per track, what starts in each bar, the drum spans). */
|
|
37
|
+
music?: MusicSummary;
|
|
36
38
|
}
|
|
37
39
|
interface AudioReport {
|
|
38
40
|
duration: number;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Application } from 'pixi.js';
|
|
2
|
-
import {
|
|
2
|
+
import { O as MusicSummary, ac as Timeline, a4 as Stop, f as AssetSpec, d as CompositionSpec } from './types-DpK1vwcl.cjs';
|
|
3
3
|
|
|
4
4
|
interface AudioInspectOptions {
|
|
5
5
|
/** Width of each loudness window in seconds. Default 0.1. */
|
|
@@ -33,6 +33,8 @@ interface AudioSourceReport {
|
|
|
33
33
|
/** Loudest sample of the MIX while this source plays, dBFS (−120 = silence). */
|
|
34
34
|
peakDb: number;
|
|
35
35
|
sound: SoundMeasure;
|
|
36
|
+
/** For a `music` layer: its score read back (bars per track, what starts in each bar, the drum spans). */
|
|
37
|
+
music?: MusicSummary;
|
|
36
38
|
}
|
|
37
39
|
interface AudioReport {
|
|
38
40
|
duration: number;
|
package/dist/Presenter.d.cts
CHANGED
package/dist/Presenter.d.ts
CHANGED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/audio/music/notation.ts
|
|
4
|
+
var SEMI = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 };
|
|
5
|
+
var accidental = (s) => s === "#" ? 1 : s === "b" ? -1 : 0;
|
|
6
|
+
var CHORD_KINDS = {
|
|
7
|
+
"": [0, 4, 7],
|
|
8
|
+
m: [0, 3, 7],
|
|
9
|
+
5: [0, 7],
|
|
10
|
+
aug: [0, 4, 8],
|
|
11
|
+
dim: [0, 3, 6],
|
|
12
|
+
sus2: [0, 2, 7],
|
|
13
|
+
sus4: [0, 5, 7],
|
|
14
|
+
6: [0, 4, 7, 9],
|
|
15
|
+
m6: [0, 3, 7, 9],
|
|
16
|
+
add9: [0, 4, 7, 14],
|
|
17
|
+
madd9: [0, 3, 7, 14],
|
|
18
|
+
add11: [0, 4, 7, 17],
|
|
19
|
+
7: [0, 4, 7, 10],
|
|
20
|
+
maj7: [0, 4, 7, 11],
|
|
21
|
+
m7: [0, 3, 7, 10],
|
|
22
|
+
mMaj7: [0, 3, 7, 11],
|
|
23
|
+
dim7: [0, 3, 6, 9],
|
|
24
|
+
m7b5: [0, 3, 6, 10],
|
|
25
|
+
"7sus4": [0, 5, 7, 10],
|
|
26
|
+
"7b9": [0, 4, 7, 10, 13],
|
|
27
|
+
"7#9": [0, 4, 7, 10, 15],
|
|
28
|
+
9: [0, 4, 7, 10, 14],
|
|
29
|
+
maj9: [0, 4, 7, 11, 14],
|
|
30
|
+
m9: [0, 3, 7, 10, 14],
|
|
31
|
+
11: [0, 4, 7, 10, 14, 17],
|
|
32
|
+
m11: [0, 3, 7, 10, 14, 17],
|
|
33
|
+
13: [0, 4, 7, 10, 14, 21],
|
|
34
|
+
maj13: [0, 4, 7, 11, 14, 21],
|
|
35
|
+
m13: [0, 3, 7, 10, 14, 21],
|
|
36
|
+
"maj7#11": [0, 4, 7, 11, 18]
|
|
37
|
+
};
|
|
38
|
+
var NOTE_FORMS = "a note in lowercase like c4, f#3, bb2; a chord starting with a capital like Am7, G7, C@4, Am7/e; [c4 e4 g4]; _ for a rest; ~ to hold";
|
|
39
|
+
var KIND_SPELLINGS = {
|
|
40
|
+
maj: "",
|
|
41
|
+
M: "",
|
|
42
|
+
major: "",
|
|
43
|
+
min: "m",
|
|
44
|
+
minor: "m",
|
|
45
|
+
mi: "m",
|
|
46
|
+
"-": "m",
|
|
47
|
+
min7: "m7",
|
|
48
|
+
mi7: "m7",
|
|
49
|
+
"-7": "m7",
|
|
50
|
+
minor7: "m7",
|
|
51
|
+
min9: "m9",
|
|
52
|
+
min6: "m6",
|
|
53
|
+
min11: "m11",
|
|
54
|
+
M7: "maj7",
|
|
55
|
+
Maj7: "maj7",
|
|
56
|
+
ma7: "maj7",
|
|
57
|
+
major7: "maj7",
|
|
58
|
+
M9: "maj9",
|
|
59
|
+
Maj9: "maj9",
|
|
60
|
+
sus: "sus4",
|
|
61
|
+
dom7: "7",
|
|
62
|
+
dom: "7",
|
|
63
|
+
"+": "aug",
|
|
64
|
+
o: "dim",
|
|
65
|
+
o7: "dim7",
|
|
66
|
+
"\xF8": "m7b5",
|
|
67
|
+
"m7-5": "m7b5",
|
|
68
|
+
add: "add9",
|
|
69
|
+
"7sus": "7sus4",
|
|
70
|
+
"sus7": "7sus4",
|
|
71
|
+
"9sus4": "7sus4",
|
|
72
|
+
"6/9": "6",
|
|
73
|
+
"69": "6"
|
|
74
|
+
};
|
|
75
|
+
function capitalHint(tok) {
|
|
76
|
+
if (/^[A-G][#b]?-?\d$/.test(tok)) return `it starts with a capital letter, so it is read as a chord; write the note in lowercase: ${tok.toLowerCase()}`;
|
|
77
|
+
const m = /^([A-G][#b]?)([^@/\s]*)((?:@\d)?(?:\/[a-gA-G][#b]?)?)$/.exec(tok);
|
|
78
|
+
if (!m) return null;
|
|
79
|
+
const fix = KIND_SPELLINGS[m[2]];
|
|
80
|
+
return fix === void 0 ? null : `the chord kind "${m[2]}" is not known; did you mean "${m[1]}${fix}${m[3]}"?`;
|
|
81
|
+
}
|
|
82
|
+
function noteToMidi(name) {
|
|
83
|
+
const m = /^([a-gA-G])([#b]?)(-?\d)$/.exec(name);
|
|
84
|
+
if (!m) return null;
|
|
85
|
+
return 12 * (Number(m[3]) + 1) + SEMI[m[1].toLowerCase()] + accidental(m[2]);
|
|
86
|
+
}
|
|
87
|
+
function chordToMidi(sym) {
|
|
88
|
+
const m = /^([A-G])([#b]?)([^@/\s]*)(?:@(\d))?(?:\/([a-gA-G][#b]?))?$/.exec(sym);
|
|
89
|
+
if (!m) return null;
|
|
90
|
+
const kind = CHORD_KINDS[m[3]];
|
|
91
|
+
if (!kind) return null;
|
|
92
|
+
const root = SEMI[m[1].toLowerCase()] + accidental(m[2]);
|
|
93
|
+
const oct = m[4] !== void 0 ? Number(m[4]) : 3;
|
|
94
|
+
const notes = kind.map((i) => 12 * (oct + 1) + root + i);
|
|
95
|
+
if (m[5]) notes.unshift(12 * oct + SEMI[m[5][0].toLowerCase()] + accidental(m[5][1] ?? ""));
|
|
96
|
+
return notes;
|
|
97
|
+
}
|
|
98
|
+
function parseNotes(str, step = 1) {
|
|
99
|
+
const events = [];
|
|
100
|
+
let t = 0;
|
|
101
|
+
for (const raw of String(str).replace(/\|/g, " ").match(/\[[^\]]*\](?::[\d.]+)?[!,]?|\S+/g) ?? []) {
|
|
102
|
+
let tok = raw, vel = 0.8;
|
|
103
|
+
const mark = (c) => {
|
|
104
|
+
vel = c === "!" ? 1 : 0.55;
|
|
105
|
+
};
|
|
106
|
+
if (/[!,]$/.test(tok)) {
|
|
107
|
+
mark(tok.slice(-1));
|
|
108
|
+
tok = tok.slice(0, -1);
|
|
109
|
+
}
|
|
110
|
+
let dur = step;
|
|
111
|
+
const dm = /:([\d.]+)$/.exec(tok);
|
|
112
|
+
if (dm) {
|
|
113
|
+
dur = Number(dm[1]);
|
|
114
|
+
tok = tok.slice(0, dm.index);
|
|
115
|
+
}
|
|
116
|
+
if (/[!,]$/.test(tok)) {
|
|
117
|
+
mark(tok.slice(-1));
|
|
118
|
+
tok = tok.slice(0, -1);
|
|
119
|
+
}
|
|
120
|
+
if (!(dur > 0) || !Number.isFinite(dur)) throw new Error(`"${raw}" has a length of ${dm?.[1]} beats; a length must be a positive number of beats (c4:2)`);
|
|
121
|
+
if (tok === "_") {
|
|
122
|
+
t += dur;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (tok === "~") {
|
|
126
|
+
if (events.length) events[events.length - 1].dur += dur;
|
|
127
|
+
t += dur;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
let midi;
|
|
131
|
+
if (tok.startsWith("[")) midi = tok.slice(1, -1).trim().split(/\s+/).map(noteToMidi);
|
|
132
|
+
else if (/^[A-G]/.test(tok)) midi = chordToMidi(tok);
|
|
133
|
+
else midi = [noteToMidi(tok)];
|
|
134
|
+
if (!midi || midi.length === 0 || midi.some((x) => x === null)) {
|
|
135
|
+
const hint = /^[A-G]/.test(tok) && !tok.startsWith("[") ? capitalHint(tok) : null;
|
|
136
|
+
throw new Error(`cannot read "${raw}"${hint ? `: ${hint}` : ""} (${NOTE_FORMS})`);
|
|
137
|
+
}
|
|
138
|
+
events.push({ t, dur, midi, vel, name: tok });
|
|
139
|
+
t += dur;
|
|
140
|
+
}
|
|
141
|
+
return { events, length: t };
|
|
142
|
+
}
|
|
143
|
+
function parseDrumGrid(grid) {
|
|
144
|
+
const hits = [];
|
|
145
|
+
let i = 0;
|
|
146
|
+
for (const ch of String(grid).replace(/[\s|]/g, "")) {
|
|
147
|
+
if (ch === "x" || ch === "X") hits.push({ i, vel: 1 });
|
|
148
|
+
else if (ch === "o" || ch === "O") hits.push({ i, vel: 0.5 });
|
|
149
|
+
else if (ch !== "." && ch !== "-" && ch !== "_") throw new Error(`"${ch}" in the drum pattern "${grid}": use x (hit), o (soft hit), . (nothing)`);
|
|
150
|
+
i++;
|
|
151
|
+
}
|
|
152
|
+
return { hits, steps: i };
|
|
153
|
+
}
|
|
154
|
+
var TempoMap = class _TempoMap {
|
|
155
|
+
points;
|
|
156
|
+
secAt = [0];
|
|
157
|
+
constructor(bpm) {
|
|
158
|
+
this.points = typeof bpm === "number" ? [[0, bpm]] : [...bpm].sort((a, b) => a[0] - b[0]);
|
|
159
|
+
if (this.points[0][0] > 0) this.points.unshift([0, this.points[0][1]]);
|
|
160
|
+
for (let i = 1; i < this.points.length; i++) {
|
|
161
|
+
const [b0, t0] = this.points[i - 1], [b1, t1] = this.points[i];
|
|
162
|
+
this.secAt.push(this.secAt[i - 1] + _TempoMap.span(b1 - b0, t0, t1));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/** Seconds taken by `db` beats while the tempo goes from `t0` to `t1` bpm. */
|
|
166
|
+
static span(db, t0, t1) {
|
|
167
|
+
return Math.abs(t1 - t0) < 1e-9 ? 60 * db / t0 : 60 * db * Math.log(t1 / t0) / (t1 - t0);
|
|
168
|
+
}
|
|
169
|
+
seconds(beat) {
|
|
170
|
+
if (beat <= 0) return 60 * beat / this.points[0][1];
|
|
171
|
+
let i = this.points.length - 1;
|
|
172
|
+
while (i > 0 && this.points[i][0] > beat) i--;
|
|
173
|
+
const [b0, t0] = this.points[i];
|
|
174
|
+
const next = this.points[i + 1];
|
|
175
|
+
if (!next) return this.secAt[i] + 60 * (beat - b0) / t0;
|
|
176
|
+
const t = t0 + (next[1] - t0) * (beat - b0) / (next[0] - b0);
|
|
177
|
+
return this.secAt[i] + _TempoMap.span(beat - b0, t0, t);
|
|
178
|
+
}
|
|
179
|
+
/** The tempo at a beat. */
|
|
180
|
+
bpmAt(beat) {
|
|
181
|
+
if (beat <= this.points[0][0]) return this.points[0][1];
|
|
182
|
+
for (let i = 1; i < this.points.length; i++) {
|
|
183
|
+
const [b0, t0] = this.points[i - 1], [b1, t1] = this.points[i];
|
|
184
|
+
if (beat < b1) return t0 + (t1 - t0) * (beat - b0) / (b1 - b0);
|
|
185
|
+
}
|
|
186
|
+
return this.points[this.points.length - 1][1];
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
// src/audio/music/events.ts
|
|
191
|
+
var round = (x, digits = 4) => Math.round(x * 10 ** digits) / 10 ** digits;
|
|
192
|
+
var barOf = (beat, meter) => Math.floor(beat / meter + 1e-9) + 1;
|
|
193
|
+
function describeEvents(m) {
|
|
194
|
+
const map = new TempoMap(m.tempo);
|
|
195
|
+
const place = (beat) => {
|
|
196
|
+
const bar = barOf(beat, m.meter);
|
|
197
|
+
return { beat: round(beat), time: round(map.seconds(beat), 6), bar, beatInBar: round(beat - (bar - 1) * m.meter + 1) };
|
|
198
|
+
};
|
|
199
|
+
const tracks = m.tracks.map((t) => ({
|
|
200
|
+
inst: t.inst,
|
|
201
|
+
beats: t.length,
|
|
202
|
+
bars: round(t.length / m.meter),
|
|
203
|
+
notes: t.events.map((e) => ({
|
|
204
|
+
...place(e.t),
|
|
205
|
+
duration: round(e.dur),
|
|
206
|
+
seconds: round(map.seconds(e.t + e.dur) - map.seconds(e.t), 6),
|
|
207
|
+
midi: e.midi.map((n) => n + t.transpose),
|
|
208
|
+
name: e.name,
|
|
209
|
+
chord: e.midi.length > 1,
|
|
210
|
+
velocity: e.vel
|
|
211
|
+
}))
|
|
212
|
+
}));
|
|
213
|
+
const byKind = /* @__PURE__ */ new Map();
|
|
214
|
+
for (const s of m.drums) {
|
|
215
|
+
const end = Math.min(s.to, m.beats);
|
|
216
|
+
for (let rep = 0; s.from + rep * s.repeat < end - 1e-6; rep++) for (const d of s.drums) for (const h of d.hits) {
|
|
217
|
+
const beat = s.from + rep * s.repeat + h.i / m.grid;
|
|
218
|
+
if (beat >= end) continue;
|
|
219
|
+
const list = byKind.get(d.kind) ?? [];
|
|
220
|
+
list.push({ ...place(beat), velocity: h.vel });
|
|
221
|
+
byKind.set(d.kind, list);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
const drums = [...byKind].map(([kind, hits]) => ({ kind, hits: hits.sort((a, b) => a.beat - b.beat) }));
|
|
225
|
+
return { bpm: round(map.bpmAt(0), 2), meter: m.meter, beats: m.beats, bars: round(m.beats / m.meter), seconds: m.seconds, tracks, drums };
|
|
226
|
+
}
|
|
227
|
+
function summarizeMusic(m) {
|
|
228
|
+
const map = new TempoMap(m.tempo);
|
|
229
|
+
return {
|
|
230
|
+
bpm: round(map.bpmAt(0), 2),
|
|
231
|
+
meter: m.meter,
|
|
232
|
+
meterSet: m.meterSet,
|
|
233
|
+
beats: m.beats,
|
|
234
|
+
bars: round(m.beats / m.meter),
|
|
235
|
+
seconds: m.seconds,
|
|
236
|
+
tail: m.tail,
|
|
237
|
+
tracks: m.tracks.map((t) => {
|
|
238
|
+
const perBar = Array.from({ length: Math.max(1, Math.ceil(t.length / m.meter - 1e-9)) }, () => []);
|
|
239
|
+
for (const e of t.events) {
|
|
240
|
+
const bar = barOf(e.t, m.meter), at = round(e.t - (bar - 1) * m.meter + 1);
|
|
241
|
+
perBar[Math.min(perBar.length - 1, bar - 1)].push(at === 1 ? e.name : `${e.name}(${at})`);
|
|
242
|
+
}
|
|
243
|
+
return { inst: t.inst, beats: t.length, bars: round(t.length / m.meter), events: t.events.length, perBar: perBar.map((names) => names.join(" ")) };
|
|
244
|
+
}),
|
|
245
|
+
// a section that starts at or after the end (or ends before it starts) never plays: render skips it, so the read-back does not list it
|
|
246
|
+
drums: m.drums.filter((s) => s.from < Math.min(s.to, m.beats) - 1e-6).map((s) => ({ from: s.from, to: Math.min(s.to, m.beats), kinds: s.drums.filter((d) => d.hits.length > 0).map((d) => d.kind) }))
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
exports.CHORD_KINDS = CHORD_KINDS;
|
|
251
|
+
exports.TempoMap = TempoMap;
|
|
252
|
+
exports.chordToMidi = chordToMidi;
|
|
253
|
+
exports.describeEvents = describeEvents;
|
|
254
|
+
exports.noteToMidi = noteToMidi;
|
|
255
|
+
exports.parseDrumGrid = parseDrumGrid;
|
|
256
|
+
exports.parseNotes = parseNotes;
|
|
257
|
+
exports.summarizeMusic = summarizeMusic;
|
|
258
|
+
//# sourceMappingURL=chunk-4LEX3G3M.cjs.map
|
|
259
|
+
//# sourceMappingURL=chunk-4LEX3G3M.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/audio/music/notation.ts","../src/audio/music/events.ts"],"names":[],"mappings":";;;AAKA,IAAM,IAAA,GAA+B,EAAE,CAAA,EAAG,CAAA,EAAG,GAAG,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,GAAG,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,GAAG,EAAA,EAAG;AACjF,IAAM,UAAA,GAAa,CAAC,CAAA,KAAuB,CAAA,KAAM,MAAM,CAAA,GAAI,CAAA,KAAM,MAAM,EAAA,GAAK,CAAA;AAGrE,IAAM,WAAA,GAAwC;AAAA,EACnD,EAAA,EAAI,CAAC,CAAA,EAAG,CAAA,EAAG,CAAC,CAAA;AAAA,EAAG,CAAA,EAAG,CAAC,CAAA,EAAG,CAAA,EAAG,CAAC,CAAA;AAAA,EAAG,CAAA,EAAG,CAAC,CAAA,EAAG,CAAC,CAAA;AAAA,EAAG,GAAA,EAAK,CAAC,CAAA,EAAG,CAAA,EAAG,CAAC,CAAA;AAAA,EAAG,GAAA,EAAK,CAAC,CAAA,EAAG,CAAA,EAAG,CAAC,CAAA;AAAA,EAAG,IAAA,EAAM,CAAC,CAAA,EAAG,CAAA,EAAG,CAAC,CAAA;AAAA,EAAG,IAAA,EAAM,CAAC,CAAA,EAAG,CAAA,EAAG,CAAC,CAAA;AAAA,EACvG,CAAA,EAAG,CAAC,CAAA,EAAG,CAAA,EAAG,GAAG,CAAC,CAAA;AAAA,EAAG,EAAA,EAAI,CAAC,CAAA,EAAG,CAAA,EAAG,GAAG,CAAC,CAAA;AAAA,EAAG,IAAA,EAAM,CAAC,CAAA,EAAG,CAAA,EAAG,GAAG,EAAE,CAAA;AAAA,EAAG,KAAA,EAAO,CAAC,CAAA,EAAG,CAAA,EAAG,GAAG,EAAE,CAAA;AAAA,EAAG,KAAA,EAAO,CAAC,CAAA,EAAG,CAAA,EAAG,GAAG,EAAE,CAAA;AAAA,EACjG,CAAA,EAAG,CAAC,CAAA,EAAG,CAAA,EAAG,GAAG,EAAE,CAAA;AAAA,EAAG,IAAA,EAAM,CAAC,CAAA,EAAG,CAAA,EAAG,GAAG,EAAE,CAAA;AAAA,EAAG,EAAA,EAAI,CAAC,CAAA,EAAG,CAAA,EAAG,GAAG,EAAE,CAAA;AAAA,EAAG,KAAA,EAAO,CAAC,CAAA,EAAG,CAAA,EAAG,GAAG,EAAE,CAAA;AAAA,EAAG,IAAA,EAAM,CAAC,CAAA,EAAG,CAAA,EAAG,GAAG,CAAC,CAAA;AAAA,EAAG,IAAA,EAAM,CAAC,CAAA,EAAG,CAAA,EAAG,GAAG,EAAE,CAAA;AAAA,EACtH,OAAA,EAAS,CAAC,CAAA,EAAG,CAAA,EAAG,GAAG,EAAE,CAAA;AAAA,EAAG,OAAO,CAAC,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,IAAI,EAAE,CAAA;AAAA,EAAG,OAAO,CAAC,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,IAAI,EAAE,CAAA;AAAA,EACzE,GAAG,CAAC,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,IAAI,EAAE,CAAA;AAAA,EAAG,MAAM,CAAC,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,IAAI,EAAE,CAAA;AAAA,EAAG,IAAI,CAAC,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,IAAI,EAAE,CAAA;AAAA,EACnE,IAAI,CAAC,CAAA,EAAG,GAAG,CAAA,EAAG,EAAA,EAAI,IAAI,EAAE,CAAA;AAAA,EAAG,KAAK,CAAC,CAAA,EAAG,GAAG,CAAA,EAAG,EAAA,EAAI,IAAI,EAAE,CAAA;AAAA,EAAG,IAAI,CAAC,CAAA,EAAG,GAAG,CAAA,EAAG,EAAA,EAAI,IAAI,EAAE,CAAA;AAAA,EAAG,OAAO,CAAC,CAAA,EAAG,GAAG,CAAA,EAAG,EAAA,EAAI,IAAI,EAAE,CAAA;AAAA,EAAG,KAAK,CAAC,CAAA,EAAG,GAAG,CAAA,EAAG,EAAA,EAAI,IAAI,EAAE,CAAA;AAAA,EACzI,WAAW,CAAC,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,IAAI,EAAE;AAC7B;AAEO,IAAM,UAAA,GAAa,sIAAA;AAG1B,IAAM,cAAA,GAAyC;AAAA,EAC7C,GAAA,EAAK,EAAA;AAAA,EAAI,CAAA,EAAG,EAAA;AAAA,EAAI,KAAA,EAAO,EAAA;AAAA,EAAI,GAAA,EAAK,GAAA;AAAA,EAAK,KAAA,EAAO,GAAA;AAAA,EAAK,EAAA,EAAI,GAAA;AAAA,EAAK,GAAA,EAAK,GAAA;AAAA,EAAK,IAAA,EAAM,IAAA;AAAA,EAAM,GAAA,EAAK,IAAA;AAAA,EAAM,IAAA,EAAM,IAAA;AAAA,EAAM,MAAA,EAAQ,IAAA;AAAA,EAAM,IAAA,EAAM,IAAA;AAAA,EAAM,IAAA,EAAM,IAAA;AAAA,EAAM,KAAA,EAAO,KAAA;AAAA,EACpJ,EAAA,EAAI,MAAA;AAAA,EAAQ,IAAA,EAAM,MAAA;AAAA,EAAQ,GAAA,EAAK,MAAA;AAAA,EAAQ,MAAA,EAAQ,MAAA;AAAA,EAAQ,EAAA,EAAI,MAAA;AAAA,EAAQ,IAAA,EAAM,MAAA;AAAA,EAAQ,GAAA,EAAK,MAAA;AAAA,EAAQ,IAAA,EAAM,GAAA;AAAA,EAAK,GAAA,EAAK,GAAA;AAAA,EAAK,GAAA,EAAK,KAAA;AAAA,EAAO,CAAA,EAAG,KAAA;AAAA,EAAO,EAAA,EAAI,MAAA;AAAA,EAC7I,MAAA,EAAK,MAAA;AAAA,EAAQ,MAAA,EAAQ,MAAA;AAAA,EAAQ,GAAA,EAAK,MAAA;AAAA,EAAQ,MAAA,EAAQ,OAAA;AAAA,EAAS,MAAA,EAAQ,OAAA;AAAA,EAAS,OAAA,EAAS,OAAA;AAAA,EAAS,KAAA,EAAO,GAAA;AAAA,EAAK,IAAA,EAAM;AAClH,CAAA;AAGA,SAAS,YAAY,GAAA,EAA4B;AAC/C,EAAA,IAAI,kBAAA,CAAmB,KAAK,GAAG,CAAA,SAAU,CAAA,wFAAA,EAA2F,GAAA,CAAI,aAAa,CAAA,CAAA;AACrJ,EAAA,MAAM,CAAA,GAAI,wDAAA,CAAyD,IAAA,CAAK,GAAG,CAAA;AAC3E,EAAA,IAAI,CAAC,GAAG,OAAO,IAAA;AACf,EAAA,MAAM,GAAA,GAAM,cAAA,CAAe,CAAA,CAAE,CAAC,CAAE,CAAA;AAChC,EAAA,OAAO,QAAQ,MAAA,GAAY,IAAA,GAAO,CAAA,gBAAA,EAAmB,CAAA,CAAE,CAAC,CAAC,CAAA,8BAAA,EAAiC,CAAA,CAAE,CAAC,CAAC,CAAA,EAAG,GAAG,CAAA,EAAG,CAAA,CAAE,CAAC,CAAC,CAAA,EAAA,CAAA;AAC7G;AAGO,SAAS,WAAW,IAAA,EAA6B;AACtD,EAAA,MAAM,CAAA,GAAI,2BAAA,CAA4B,IAAA,CAAK,IAAI,CAAA;AAC/C,EAAA,IAAI,CAAC,GAAG,OAAO,IAAA;AACf,EAAA,OAAO,MAAM,MAAA,CAAO,CAAA,CAAE,CAAC,CAAC,IAAI,CAAA,CAAA,GAAK,IAAA,CAAK,CAAA,CAAE,CAAC,EAAG,WAAA,EAAa,IAAK,UAAA,CAAW,CAAA,CAAE,CAAC,CAAE,CAAA;AAChF;AAGO,SAAS,YAAY,GAAA,EAA8B;AACxD,EAAA,MAAM,CAAA,GAAI,4DAAA,CAA6D,IAAA,CAAK,GAAG,CAAA;AAC/E,EAAA,IAAI,CAAC,GAAG,OAAO,IAAA;AACf,EAAA,MAAM,IAAA,GAAO,WAAA,CAAY,CAAA,CAAE,CAAC,CAAE,CAAA;AAC9B,EAAA,IAAI,CAAC,MAAM,OAAO,IAAA;AAClB,EAAA,MAAM,IAAA,GAAO,IAAA,CAAK,CAAA,CAAE,CAAC,CAAA,CAAG,WAAA,EAAa,CAAA,GAAK,UAAA,CAAW,CAAA,CAAE,CAAC,CAAE,CAAA;AAC1D,EAAA,MAAM,GAAA,GAAM,EAAE,CAAC,CAAA,KAAM,SAAY,MAAA,CAAO,CAAA,CAAE,CAAC,CAAC,CAAA,GAAI,CAAA;AAChD,EAAA,MAAM,KAAA,GAAQ,KAAK,GAAA,CAAI,CAAA,CAAA,KAAK,MAAM,GAAA,GAAM,CAAA,CAAA,GAAK,OAAO,CAAC,CAAA;AACrD,EAAA,IAAI,CAAA,CAAE,CAAC,CAAA,EAAG,KAAA,CAAM,QAAQ,EAAA,GAAK,GAAA,GAAM,IAAA,CAAK,CAAA,CAAE,CAAC,CAAA,CAAE,CAAC,CAAA,CAAG,WAAA,EAAa,CAAA,GAAK,UAAA,CAAW,CAAA,CAAE,CAAC,CAAA,CAAE,CAAC,CAAA,IAAK,EAAE,CAAC,CAAA;AAC5F,EAAA,OAAO,KAAA;AACT;AAeO,SAAS,UAAA,CAAW,GAAA,EAAa,IAAA,GAAO,CAAA,EAA6C;AAC1F,EAAA,MAAM,SAAuB,EAAC;AAC9B,EAAA,IAAI,CAAA,GAAI,CAAA;AACR,EAAA,KAAA,MAAW,GAAA,IAAO,MAAA,CAAO,GAAG,CAAA,CAAE,OAAA,CAAQ,KAAA,EAAO,GAAG,CAAA,CAAE,KAAA,CAAM,kCAAkC,CAAA,IAAK,EAAC,EAAG;AACjG,IAAA,IAAI,GAAA,GAAM,KAAK,GAAA,GAAM,GAAA;AACrB,IAAA,MAAM,IAAA,GAAO,CAAC,CAAA,KAAc;AAAE,MAAA,GAAA,GAAM,CAAA,KAAM,MAAM,CAAA,GAAI,IAAA;AAAA,IAAM,CAAA;AAC1D,IAAA,IAAI,OAAA,CAAQ,IAAA,CAAK,GAAG,CAAA,EAAG;AAAE,MAAA,IAAA,CAAK,GAAA,CAAI,KAAA,CAAM,EAAE,CAAC,CAAA;AAAG,MAAA,GAAA,GAAM,GAAA,CAAI,KAAA,CAAM,CAAA,EAAG,EAAE,CAAA;AAAA,IAAG;AACtE,IAAA,IAAI,GAAA,GAAM,IAAA;AACV,IAAA,MAAM,EAAA,GAAK,YAAA,CAAa,IAAA,CAAK,GAAG,CAAA;AAChC,IAAA,IAAI,EAAA,EAAI;AAAE,MAAA,GAAA,GAAM,MAAA,CAAO,EAAA,CAAG,CAAC,CAAC,CAAA;AAAG,MAAA,GAAA,GAAM,GAAA,CAAI,KAAA,CAAM,CAAA,EAAG,EAAA,CAAG,KAAK,CAAA;AAAA,IAAG;AAC7D,IAAA,IAAI,OAAA,CAAQ,IAAA,CAAK,GAAG,CAAA,EAAG;AAAE,MAAA,IAAA,CAAK,GAAA,CAAI,KAAA,CAAM,EAAE,CAAC,CAAA;AAAG,MAAA,GAAA,GAAM,GAAA,CAAI,KAAA,CAAM,CAAA,EAAG,EAAE,CAAA;AAAA,IAAG;AACtE,IAAA,IAAI,EAAE,GAAA,GAAM,CAAA,CAAA,IAAM,CAAC,MAAA,CAAO,SAAS,GAAG,CAAA,EAAG,MAAM,IAAI,MAAM,CAAA,CAAA,EAAI,GAAG,qBAAqB,EAAA,GAAK,CAAC,CAAC,CAAA,0DAAA,CAA4D,CAAA;AACxJ,IAAA,IAAI,QAAQ,GAAA,EAAK;AAAE,MAAA,CAAA,IAAK,GAAA;AAAK,MAAA;AAAA,IAAU;AACvC,IAAA,IAAI,QAAQ,GAAA,EAAK;AAAE,MAAA,IAAI,OAAO,MAAA,EAAQ,MAAA,CAAO,OAAO,MAAA,GAAS,CAAC,EAAG,GAAA,IAAO,GAAA;AAAK,MAAA,CAAA,IAAK,GAAA;AAAK,MAAA;AAAA,IAAU;AACjG,IAAA,IAAI,IAAA;AACJ,IAAA,IAAI,IAAI,UAAA,CAAW,GAAG,CAAA,EAAG,IAAA,GAAO,IAAI,KAAA,CAAM,CAAA,EAAG,EAAE,CAAA,CAAE,MAAK,CAAE,KAAA,CAAM,KAAK,CAAA,CAAE,IAAI,UAAU,CAAA;AAAA,SAAA,IAC1E,SAAS,IAAA,CAAK,GAAG,CAAA,EAAG,IAAA,GAAO,YAAY,GAAG,CAAA;AAAA,SAC9C,IAAA,GAAO,CAAC,UAAA,CAAW,GAAG,CAAC,CAAA;AAC5B,IAAA,IAAI,CAAC,IAAA,IAAQ,IAAA,CAAK,MAAA,KAAW,CAAA,IAAK,KAAK,IAAA,CAAK,CAAA,CAAA,KAAK,CAAA,KAAM,IAAI,CAAA,EAAG;AAC5D,MAAA,MAAM,IAAA,GAAO,QAAA,CAAS,IAAA,CAAK,GAAG,CAAA,IAAK,CAAC,GAAA,CAAI,UAAA,CAAW,GAAG,CAAA,GAAI,WAAA,CAAY,GAAG,CAAA,GAAI,IAAA;AAC7E,MAAA,MAAM,IAAI,KAAA,CAAM,CAAA,aAAA,EAAgB,GAAG,CAAA,CAAA,EAAI,IAAA,GAAO,CAAA,EAAA,EAAK,IAAI,CAAA,CAAA,GAAK,EAAE,CAAA,EAAA,EAAK,UAAU,CAAA,CAAA,CAAG,CAAA;AAAA,IAClF;AACA,IAAA,MAAA,CAAO,IAAA,CAAK,EAAE,CAAA,EAAG,GAAA,EAAK,MAAwB,GAAA,EAAK,IAAA,EAAM,KAAK,CAAA;AAC9D,IAAA,CAAA,IAAK,GAAA;AAAA,EACP;AACA,EAAA,OAAO,EAAE,MAAA,EAAQ,MAAA,EAAQ,CAAA,EAAE;AAC7B;AAGO,SAAS,cAAc,IAAA,EAA0E;AACtG,EAAA,MAAM,OAA0C,EAAC;AACjD,EAAA,IAAI,CAAA,GAAI,CAAA;AACR,EAAA,KAAA,MAAW,MAAM,MAAA,CAAO,IAAI,EAAE,OAAA,CAAQ,QAAA,EAAU,EAAE,CAAA,EAAG;AACnD,IAAA,IAAI,EAAA,KAAO,GAAA,IAAO,EAAA,KAAO,GAAA,EAAK,IAAA,CAAK,KAAK,EAAE,CAAA,EAAG,GAAA,EAAK,CAAA,EAAG,CAAA;AAAA,SAAA,IAC5C,EAAA,KAAO,GAAA,IAAO,EAAA,KAAO,GAAA,EAAK,IAAA,CAAK,KAAK,EAAE,CAAA,EAAG,GAAA,EAAK,GAAA,EAAK,CAAA;AAAA,SAAA,IACnD,EAAA,KAAO,GAAA,IAAO,EAAA,KAAO,GAAA,IAAO,EAAA,KAAO,GAAA,EAAK,MAAM,IAAI,KAAA,CAAM,CAAA,CAAA,EAAI,EAAE,CAAA,uBAAA,EAA0B,IAAI,CAAA,yCAAA,CAA2C,CAAA;AAChJ,IAAA,CAAA,EAAA;AAAA,EACF;AACA,EAAA,OAAO,EAAE,IAAA,EAAM,KAAA,EAAO,CAAA,EAAE;AAC1B;AAMO,IAAM,QAAA,GAAN,MAAM,SAAA,CAAS;AAAA,EACH,MAAA;AAAA,EACA,KAAA,GAAkB,CAAC,CAAC,CAAA;AAAA,EAErC,YAAY,GAAA,EAA2B;AACrC,IAAA,IAAA,CAAK,MAAA,GAAS,OAAO,GAAA,KAAQ,QAAA,GAAW,CAAC,CAAC,CAAA,EAAG,GAAG,CAAC,CAAA,GAAI,CAAC,GAAG,GAAG,CAAA,CAAE,IAAA,CAAK,CAAC,CAAA,EAAG,CAAA,KAAM,EAAE,CAAC,CAAA,GAAI,CAAA,CAAE,CAAC,CAAC,CAAA;AACxF,IAAA,IAAI,KAAK,MAAA,CAAO,CAAC,EAAG,CAAC,CAAA,GAAI,GAAG,IAAA,CAAK,MAAA,CAAO,OAAA,CAAQ,CAAC,GAAG,IAAA,CAAK,MAAA,CAAO,CAAC,CAAA,CAAG,CAAC,CAAC,CAAC,CAAA;AACvE,IAAA,KAAA,IAAS,IAAI,CAAA,EAAG,CAAA,GAAI,IAAA,CAAK,MAAA,CAAO,QAAQ,CAAA,EAAA,EAAK;AAC3C,MAAA,MAAM,CAAC,EAAA,EAAI,EAAE,CAAA,GAAI,KAAK,MAAA,CAAO,CAAA,GAAI,CAAC,CAAA,EAAI,CAAC,EAAA,EAAI,EAAE,CAAA,GAAI,IAAA,CAAK,OAAO,CAAC,CAAA;AAC9D,MAAA,IAAA,CAAK,KAAA,CAAM,IAAA,CAAK,IAAA,CAAK,KAAA,CAAM,CAAA,GAAI,CAAC,CAAA,GAAK,SAAA,CAAS,IAAA,CAAK,EAAA,GAAK,EAAA,EAAI,EAAA,EAAI,EAAE,CAAC,CAAA;AAAA,IACrE;AAAA,EACF;AAAA;AAAA,EAGA,OAAe,IAAA,CAAK,EAAA,EAAY,EAAA,EAAY,EAAA,EAAoB;AAC9D,IAAA,OAAO,KAAK,GAAA,CAAI,EAAA,GAAK,EAAE,CAAA,GAAI,OAAQ,EAAA,GAAK,EAAA,GAAM,EAAA,GAAM,EAAA,GAAK,KAAK,IAAA,CAAK,GAAA,CAAI,EAAA,GAAK,EAAE,KAAM,EAAA,GAAK,EAAA,CAAA;AAAA,EAC3F;AAAA,EAEA,QAAQ,IAAA,EAAsB;AAC5B,IAAA,IAAI,IAAA,IAAQ,GAAG,OAAQ,EAAA,GAAK,OAAQ,IAAA,CAAK,MAAA,CAAO,CAAC,CAAA,CAAG,CAAC,CAAA;AACrD,IAAA,IAAI,CAAA,GAAI,IAAA,CAAK,MAAA,CAAO,MAAA,GAAS,CAAA;AAC7B,IAAA,OAAO,CAAA,GAAI,KAAK,IAAA,CAAK,MAAA,CAAO,CAAC,CAAA,CAAG,CAAC,IAAI,IAAA,EAAM,CAAA,EAAA;AAC3C,IAAA,MAAM,CAAC,EAAA,EAAI,EAAE,CAAA,GAAI,IAAA,CAAK,OAAO,CAAC,CAAA;AAC9B,IAAA,MAAM,IAAA,GAAO,IAAA,CAAK,MAAA,CAAO,CAAA,GAAI,CAAC,CAAA;AAC9B,IAAA,IAAI,CAAC,MAAM,OAAO,IAAA,CAAK,MAAM,CAAC,CAAA,GAAM,EAAA,IAAM,IAAA,GAAO,EAAA,CAAA,GAAO,EAAA;AACxD,IAAA,MAAM,CAAA,GAAI,EAAA,GAAA,CAAO,IAAA,CAAK,CAAC,CAAA,GAAI,OAAO,IAAA,GAAO,EAAA,CAAA,IAAQ,IAAA,CAAK,CAAC,CAAA,GAAI,EAAA,CAAA;AAC3D,IAAA,OAAO,IAAA,CAAK,MAAM,CAAC,CAAA,GAAK,UAAS,IAAA,CAAK,IAAA,GAAO,EAAA,EAAI,EAAA,EAAI,CAAC,CAAA;AAAA,EACxD;AAAA;AAAA,EAGA,MAAM,IAAA,EAAsB;AAC1B,IAAA,IAAI,IAAA,IAAQ,IAAA,CAAK,MAAA,CAAO,CAAC,CAAA,CAAG,CAAC,CAAA,EAAG,OAAO,IAAA,CAAK,MAAA,CAAO,CAAC,CAAA,CAAG,CAAC,CAAA;AACxD,IAAA,KAAA,IAAS,IAAI,CAAA,EAAG,CAAA,GAAI,IAAA,CAAK,MAAA,CAAO,QAAQ,CAAA,EAAA,EAAK;AAC3C,MAAA,MAAM,CAAC,EAAA,EAAI,EAAE,CAAA,GAAI,KAAK,MAAA,CAAO,CAAA,GAAI,CAAC,CAAA,EAAI,CAAC,EAAA,EAAI,EAAE,CAAA,GAAI,IAAA,CAAK,OAAO,CAAC,CAAA;AAC9D,MAAA,IAAI,IAAA,GAAO,IAAI,OAAO,EAAA,GAAA,CAAO,KAAK,EAAA,KAAO,IAAA,GAAO,OAAQ,EAAA,GAAK,EAAA,CAAA;AAAA,IAC/D;AACA,IAAA,OAAO,KAAK,MAAA,CAAO,IAAA,CAAK,OAAO,MAAA,GAAS,CAAC,EAAG,CAAC,CAAA;AAAA,EAC/C;AACF;;;AC3FA,IAAM,KAAA,GAAQ,CAAC,CAAA,EAAW,MAAA,GAAS,CAAA,KAAc,IAAA,CAAK,KAAA,CAAM,CAAA,GAAI,EAAA,IAAM,MAAM,CAAA,GAAI,EAAA,IAAM,MAAA;AACtF,IAAM,KAAA,GAAQ,CAAC,IAAA,EAAc,KAAA,KAA0B,KAAK,KAAA,CAAM,IAAA,GAAO,KAAA,GAAQ,IAAI,CAAA,GAAI,CAAA;AAElF,SAAS,eAAe,CAAA,EAA+B;AAC5D,EAAA,MAAM,GAAA,GAAM,IAAI,QAAA,CAAS,CAAA,CAAE,KAAK,CAAA;AAChC,EAAA,MAAM,KAAA,GAAQ,CAAC,IAAA,KAAiB;AAAE,IAAA,MAAM,GAAA,GAAM,KAAA,CAAM,IAAA,EAAM,CAAA,CAAE,KAAK,CAAA;AAAG,IAAA,OAAO,EAAE,MAAM,KAAA,CAAM,IAAI,GAAG,IAAA,EAAM,KAAA,CAAM,GAAA,CAAI,OAAA,CAAQ,IAAI,CAAA,EAAG,CAAC,CAAA,EAAG,GAAA,EAAK,WAAW,KAAA,CAAM,IAAA,GAAA,CAAQ,MAAM,CAAA,IAAK,CAAA,CAAE,KAAA,GAAQ,CAAC,CAAA,EAAE;AAAA,EAAG,CAAA;AAC5L,EAAA,MAAM,MAAA,GAAS,CAAA,CAAE,MAAA,CAAO,GAAA,CAAI,CAAA,CAAA,MAAM;AAAA,IAChC,MAAM,CAAA,CAAE,IAAA;AAAA,IACR,OAAO,CAAA,CAAE,MAAA;AAAA,IACT,IAAA,EAAM,KAAA,CAAM,CAAA,CAAE,MAAA,GAAS,EAAE,KAAK,CAAA;AAAA,IAC9B,KAAA,EAAO,CAAA,CAAE,MAAA,CAAO,GAAA,CAAI,CAAC,CAAA,MAAkB;AAAA,MACrC,GAAG,KAAA,CAAM,CAAA,CAAE,CAAC,CAAA;AAAA,MAAG,QAAA,EAAU,KAAA,CAAM,CAAA,CAAE,GAAG,CAAA;AAAA,MAAG,OAAA,EAAS,KAAA,CAAM,GAAA,CAAI,OAAA,CAAQ,EAAE,CAAA,GAAI,CAAA,CAAE,GAAG,CAAA,GAAI,GAAA,CAAI,OAAA,CAAQ,CAAA,CAAE,CAAC,GAAG,CAAC,CAAA;AAAA,MACpG,MAAM,CAAA,CAAE,IAAA,CAAK,IAAI,CAAA,CAAA,KAAK,CAAA,GAAI,EAAE,SAAS,CAAA;AAAA,MAAG,MAAM,CAAA,CAAE,IAAA;AAAA,MAAM,KAAA,EAAO,CAAA,CAAE,IAAA,CAAK,MAAA,GAAS,CAAA;AAAA,MAAG,UAAU,CAAA,CAAE;AAAA,KAC9F,CAAE;AAAA,GACJ,CAAE,CAAA;AACF,EAAA,MAAM,MAAA,uBAAa,GAAA,EAA4B;AAC/C,EAAA,KAAA,MAAW,CAAA,IAAK,EAAE,KAAA,EAAO;AACvB,IAAA,MAAM,MAAM,IAAA,CAAK,GAAA,CAAI,CAAA,CAAE,EAAA,EAAI,EAAE,KAAK,CAAA;AAClC,IAAA,KAAA,IAAS,MAAM,CAAA,EAAG,CAAA,CAAE,IAAA,GAAO,GAAA,GAAM,EAAE,MAAA,GAAS,GAAA,GAAM,IAAA,EAAM,GAAA,EAAA,aAAkB,CAAA,IAAK,CAAA,CAAE,OAAO,KAAA,MAAW,CAAA,IAAK,EAAE,IAAA,EAAM;AAC9G,MAAA,MAAM,IAAA,GAAO,EAAE,IAAA,GAAO,GAAA,GAAM,EAAE,MAAA,GAAS,CAAA,CAAE,IAAI,CAAA,CAAE,IAAA;AAC/C,MAAA,IAAI,QAAQ,GAAA,EAAK;AACjB,MAAA,MAAM,OAAO,MAAA,CAAO,GAAA,CAAI,CAAA,CAAE,IAAI,KAAK,EAAC;AACpC,MAAA,IAAA,CAAK,IAAA,CAAK,EAAE,GAAG,KAAA,CAAM,IAAI,CAAA,EAAG,QAAA,EAAU,CAAA,CAAE,GAAA,EAAK,CAAA;AAC7C,MAAA,MAAA,CAAO,GAAA,CAAI,CAAA,CAAE,IAAA,EAAM,IAAI,CAAA;AAAA,IACzB;AAAA,EACF;AACA,EAAA,MAAM,KAAA,GAAQ,CAAC,GAAG,MAAM,CAAA,CAAE,IAAI,CAAC,CAAC,IAAA,EAAM,IAAI,CAAA,MAAO,EAAE,MAAM,IAAA,EAAM,IAAA,CAAK,IAAA,CAAK,CAAC,CAAA,EAAG,CAAA,KAAM,EAAE,IAAA,GAAO,CAAA,CAAE,IAAI,CAAA,EAAE,CAAE,CAAA;AACtG,EAAA,OAAO,EAAE,GAAA,EAAK,KAAA,CAAM,GAAA,CAAI,KAAA,CAAM,CAAC,CAAA,EAAG,CAAC,CAAA,EAAG,KAAA,EAAO,CAAA,CAAE,KAAA,EAAO,KAAA,EAAO,EAAE,KAAA,EAAO,IAAA,EAAM,KAAA,CAAM,CAAA,CAAE,KAAA,GAAQ,CAAA,CAAE,KAAK,CAAA,EAAG,OAAA,EAAS,CAAA,CAAE,OAAA,EAAS,MAAA,EAAQ,KAAA,EAAM;AAC1I;AAEO,SAAS,eAAe,CAAA,EAAgC;AAC7D,EAAA,MAAM,GAAA,GAAM,IAAI,QAAA,CAAS,CAAA,CAAE,KAAK,CAAA;AAChC,EAAA,OAAO;AAAA,IACL,KAAK,KAAA,CAAM,GAAA,CAAI,KAAA,CAAM,CAAC,GAAG,CAAC,CAAA;AAAA,IAAG,OAAO,CAAA,CAAE,KAAA;AAAA,IAAO,UAAU,CAAA,CAAE,QAAA;AAAA,IAAU,OAAO,CAAA,CAAE,KAAA;AAAA,IAAO,IAAA,EAAM,KAAA,CAAM,CAAA,CAAE,KAAA,GAAQ,EAAE,KAAK,CAAA;AAAA,IAAG,SAAS,CAAA,CAAE,OAAA;AAAA,IAAS,MAAM,CAAA,CAAE,IAAA;AAAA,IAC/I,MAAA,EAAQ,CAAA,CAAE,MAAA,CAAO,GAAA,CAAI,CAAA,CAAA,KAAK;AACxB,MAAA,MAAM,MAAA,GAAqB,MAAM,IAAA,CAAK,EAAE,QAAQ,IAAA,CAAK,GAAA,CAAI,GAAG,IAAA,CAAK,IAAA,CAAK,EAAE,MAAA,GAAS,CAAA,CAAE,QAAQ,IAAI,CAAC,GAAE,EAAG,MAAM,EAAE,CAAA;AAC7G,MAAA,KAAA,MAAW,CAAA,IAAK,EAAE,MAAA,EAAQ;AACxB,QAAA,MAAM,GAAA,GAAM,KAAA,CAAM,CAAA,CAAE,CAAA,EAAG,EAAE,KAAK,CAAA,EAAG,EAAA,GAAK,KAAA,CAAM,EAAE,CAAA,GAAA,CAAK,GAAA,GAAM,CAAA,IAAK,CAAA,CAAE,QAAQ,CAAC,CAAA;AACzE,QAAA,MAAA,CAAO,KAAK,GAAA,CAAI,MAAA,CAAO,SAAS,CAAA,EAAG,GAAA,GAAM,CAAC,CAAC,CAAA,CAAG,KAAK,EAAA,KAAO,CAAA,GAAI,EAAE,IAAA,GAAO,CAAA,EAAG,EAAE,IAAI,CAAA,CAAA,EAAI,EAAE,CAAA,CAAA,CAAG,CAAA;AAAA,MAC3F;AACA,MAAA,OAAO,EAAE,IAAA,EAAM,CAAA,CAAE,IAAA,EAAM,KAAA,EAAO,CAAA,CAAE,MAAA,EAAQ,IAAA,EAAM,KAAA,CAAM,CAAA,CAAE,MAAA,GAAS,CAAA,CAAE,KAAK,GAAG,MAAA,EAAQ,CAAA,CAAE,MAAA,CAAO,MAAA,EAAQ,MAAA,EAAQ,MAAA,CAAO,GAAA,CAAI,CAAA,KAAA,KAAS,KAAA,CAAM,IAAA,CAAK,GAAG,CAAC,CAAA,EAAE;AAAA,IACjJ,CAAC,CAAA;AAAA;AAAA,IAED,KAAA,EAAO,EAAE,KAAA,CAAM,MAAA,CAAO,OAAK,CAAA,CAAE,IAAA,GAAO,KAAK,GAAA,CAAI,CAAA,CAAE,IAAI,CAAA,CAAE,KAAK,IAAI,IAAI,CAAA,CAAE,IAAI,CAAA,CAAA,MAAM,EAAE,MAAM,CAAA,CAAE,IAAA,EAAM,IAAI,IAAA,CAAK,GAAA,CAAI,EAAE,EAAA,EAAI,CAAA,CAAE,KAAK,CAAA,EAAG,KAAA,EAAO,EAAE,KAAA,CAAM,MAAA,CAAO,OAAK,CAAA,CAAE,IAAA,CAAK,SAAS,CAAC,CAAA,CAAE,IAAI,CAAA,CAAA,KAAK,CAAA,CAAE,IAAI,CAAA,EAAE,CAAE;AAAA,GAC9L;AACF","file":"chunk-4LEX3G3M.cjs","sourcesContent":["/**\n * The text notation of `music`: notes, chords, rests and drum grids, and the tempo map. Pure functions, no audio.\n * Ported from the spike the owner approved by ear (a score of 8 lines of text → a lo-fi tune that sounds like one).\n */\n\nconst SEMI: Record<string, number> = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 };\nconst accidental = (s: string): number => (s === '#' ? 1 : s === 'b' ? -1 : 0);\n\n/** Chord kinds as semitones above the root (a ninth is 14, an eleventh 17, a thirteenth 21). Voiced close, around octave 3. */\nexport const CHORD_KINDS: Record<string, number[]> = {\n '': [0, 4, 7], m: [0, 3, 7], 5: [0, 7], aug: [0, 4, 8], dim: [0, 3, 6], sus2: [0, 2, 7], sus4: [0, 5, 7],\n 6: [0, 4, 7, 9], m6: [0, 3, 7, 9], add9: [0, 4, 7, 14], madd9: [0, 3, 7, 14], add11: [0, 4, 7, 17],\n 7: [0, 4, 7, 10], maj7: [0, 4, 7, 11], m7: [0, 3, 7, 10], mMaj7: [0, 3, 7, 11], dim7: [0, 3, 6, 9], m7b5: [0, 3, 6, 10],\n '7sus4': [0, 5, 7, 10], '7b9': [0, 4, 7, 10, 13], '7#9': [0, 4, 7, 10, 15],\n 9: [0, 4, 7, 10, 14], maj9: [0, 4, 7, 11, 14], m9: [0, 3, 7, 10, 14],\n 11: [0, 4, 7, 10, 14, 17], m11: [0, 3, 7, 10, 14, 17], 13: [0, 4, 7, 10, 14, 21], maj13: [0, 4, 7, 11, 14, 21], m13: [0, 3, 7, 10, 14, 21],\n 'maj7#11': [0, 4, 7, 11, 18],\n};\n\nexport const NOTE_FORMS = 'a note in lowercase like c4, f#3, bb2; a chord starting with a capital like Am7, G7, C@4, Am7/e; [c4 e4 g4]; _ for a rest; ~ to hold';\n\n/** Spellings people reach for that are not chord kinds here, and the kind they mean (`Cmin7` → `Cm7`). */\nconst KIND_SPELLINGS: Record<string, string> = {\n maj: '', M: '', major: '', min: 'm', minor: 'm', mi: 'm', '-': 'm', min7: 'm7', mi7: 'm7', '-7': 'm7', minor7: 'm7', min9: 'm9', min6: 'm6', min11: 'm11',\n M7: 'maj7', Maj7: 'maj7', ma7: 'maj7', major7: 'maj7', M9: 'maj9', Maj9: 'maj9', sus: 'sus4', dom7: '7', dom: '7', '+': 'aug', o: 'dim', o7: 'dim7',\n 'ø': 'm7b5', 'm7-5': 'm7b5', add: 'add9', '7sus': '7sus4', 'sus7': '7sus4', '9sus4': '7sus4', '6/9': '6', '69': '6',\n};\n\n/** Why a bare token that starts with a capital letter is not a chord, as one sentence with the fix (or null when it is something else). */\nfunction capitalHint(tok: string): string | null {\n if (/^[A-G][#b]?-?\\d$/.test(tok)) return `it starts with a capital letter, so it is read as a chord; write the note in lowercase: ${tok.toLowerCase()}`;\n const m = /^([A-G][#b]?)([^@/\\s]*)((?:@\\d)?(?:\\/[a-gA-G][#b]?)?)$/.exec(tok);\n if (!m) return null;\n const fix = KIND_SPELLINGS[m[2]!];\n return fix === undefined ? null : `the chord kind \"${m[2]}\" is not known; did you mean \"${m[1]}${fix}${m[3]}\"?`;\n}\n\n/** `c4` / `f#3` / `Bb2` → MIDI number (c4 = 60), or null. */\nexport function noteToMidi(name: string): number | null {\n const m = /^([a-gA-G])([#b]?)(-?\\d)$/.exec(name);\n if (!m) return null;\n return 12 * (Number(m[3]) + 1) + SEMI[m[1]!.toLowerCase()]! + accidental(m[2]!);\n}\n\n/** `Am7` → MIDI notes (root in octave 3); `C@4` puts the root in octave 4, `Am7/e` puts e in the bass. Null when it is not a chord. */\nexport function chordToMidi(sym: string): number[] | null {\n const m = /^([A-G])([#b]?)([^@/\\s]*)(?:@(\\d))?(?:\\/([a-gA-G][#b]?))?$/.exec(sym);\n if (!m) return null;\n const kind = CHORD_KINDS[m[3]!];\n if (!kind) return null;\n const root = SEMI[m[1]!.toLowerCase()]! + accidental(m[2]!);\n const oct = m[4] !== undefined ? Number(m[4]) : 3;\n const notes = kind.map(i => 12 * (oct + 1) + root + i);\n if (m[5]) notes.unshift(12 * oct + SEMI[m[5][0]!.toLowerCase()]! + accidental(m[5][1] ?? ''));\n return notes;\n}\n\n/** One note or chord of a track: where (beats from the start), how long (beats), which MIDI notes, how hard (0–1). */\nexport interface MusicEvent {\n t: number; dur: number; midi: number[]; vel: number;\n /** What the score called it, without length or accent: `Am7`, `c4`, `[c4 e4 g4]`. */\n name: string;\n}\n\n/**\n * A track's `notes` string → events. Tokens are separated by spaces (`|` is ignored: use it for bars).\n * `c4`, `c4:2`, `c4:0.5` a note (`:` = length in beats, default `step`) · `Am7:4`, `C@4:2`, `Am7/e:4` a chord · `[c4 e4 g4]:2` a chord note by note ·\n * `_`, `_:2` a rest · `~`, `~:1` hold (lengthens the note before) · `!` accent, `,` soft, before or after the length: `c4!:1`, `c4:1!`.\n * Throws an Error that names the token.\n */\nexport function parseNotes(str: string, step = 1): { events: MusicEvent[]; length: number } {\n const events: MusicEvent[] = [];\n let t = 0;\n for (const raw of String(str).replace(/\\|/g, ' ').match(/\\[[^\\]]*\\](?::[\\d.]+)?[!,]?|\\S+/g) ?? []) {\n let tok = raw, vel = 0.8;\n const mark = (c: string) => { vel = c === '!' ? 1 : 0.55; };\n if (/[!,]$/.test(tok)) { mark(tok.slice(-1)); tok = tok.slice(0, -1); }\n let dur = step;\n const dm = /:([\\d.]+)$/.exec(tok);\n if (dm) { dur = Number(dm[1]); tok = tok.slice(0, dm.index); }\n if (/[!,]$/.test(tok)) { mark(tok.slice(-1)); tok = tok.slice(0, -1); }\n if (!(dur > 0) || !Number.isFinite(dur)) throw new Error(`\"${raw}\" has a length of ${dm?.[1]} beats; a length must be a positive number of beats (c4:2)`);\n if (tok === '_') { t += dur; continue; }\n if (tok === '~') { if (events.length) events[events.length - 1]!.dur += dur; t += dur; continue; }\n let midi: Array<number | null> | null;\n if (tok.startsWith('[')) midi = tok.slice(1, -1).trim().split(/\\s+/).map(noteToMidi);\n else if (/^[A-G]/.test(tok)) midi = chordToMidi(tok); // a capital letter starts a chord (G7, C5, C6), a lowercase one a note (g7, c5, c6): never both\n else midi = [noteToMidi(tok)];\n if (!midi || midi.length === 0 || midi.some(x => x === null)) {\n const hint = /^[A-G]/.test(tok) && !tok.startsWith('[') ? capitalHint(tok) : null;\n throw new Error(`cannot read \"${raw}\"${hint ? `: ${hint}` : ''} (${NOTE_FORMS})`);\n }\n events.push({ t, dur, midi: midi as number[], vel, name: tok });\n t += dur;\n }\n return { events, length: t };\n}\n\n/** A drum pattern: `x` a hit, `o` a soft hit, `.` nothing (spaces and `|` are ignored). Step = 1/`grid` of a beat. */\nexport function parseDrumGrid(grid: string): { hits: Array<{ i: number; vel: number }>; steps: number } {\n const hits: Array<{ i: number; vel: number }> = [];\n let i = 0;\n for (const ch of String(grid).replace(/[\\s|]/g, '')) {\n if (ch === 'x' || ch === 'X') hits.push({ i, vel: 1 });\n else if (ch === 'o' || ch === 'O') hits.push({ i, vel: 0.5 });\n else if (ch !== '.' && ch !== '-' && ch !== '_') throw new Error(`\"${ch}\" in the drum pattern \"${grid}\": use x (hit), o (soft hit), . (nothing)`);\n i++;\n }\n return { hits, steps: i };\n}\n\n/** Tempo as `[beat, bpm]` points; between two points the tempo changes linearly (a ritardando), before / after it holds. */\nexport type TempoPoints = Array<[number, number]>;\n\n/** Beats → seconds for a tempo map (exact integral of 60 / bpm). */\nexport class TempoMap {\n private readonly points: TempoPoints;\n private readonly secAt: number[] = [0];\n\n constructor(bpm: number | TempoPoints) {\n this.points = typeof bpm === 'number' ? [[0, bpm]] : [...bpm].sort((a, b) => a[0] - b[0]);\n if (this.points[0]![0] > 0) this.points.unshift([0, this.points[0]![1]]);\n for (let i = 1; i < this.points.length; i++) {\n const [b0, t0] = this.points[i - 1]!, [b1, t1] = this.points[i]!;\n this.secAt.push(this.secAt[i - 1]! + TempoMap.span(b1 - b0, t0, t1));\n }\n }\n\n /** Seconds taken by `db` beats while the tempo goes from `t0` to `t1` bpm. */\n private static span(db: number, t0: number, t1: number): number {\n return Math.abs(t1 - t0) < 1e-9 ? (60 * db) / t0 : (60 * db * Math.log(t1 / t0)) / (t1 - t0);\n }\n\n seconds(beat: number): number {\n if (beat <= 0) return (60 * beat) / this.points[0]![1];\n let i = this.points.length - 1;\n while (i > 0 && this.points[i]![0] > beat) i--;\n const [b0, t0] = this.points[i]!;\n const next = this.points[i + 1];\n if (!next) return this.secAt[i]! + (60 * (beat - b0)) / t0;\n const t = t0 + ((next[1] - t0) * (beat - b0)) / (next[0] - b0);\n return this.secAt[i]! + TempoMap.span(beat - b0, t0, t);\n }\n\n /** The tempo at a beat. */\n bpmAt(beat: number): number {\n if (beat <= this.points[0]![0]) return this.points[0]![1];\n for (let i = 1; i < this.points.length; i++) {\n const [b0, t0] = this.points[i - 1]!, [b1, t1] = this.points[i]!;\n if (beat < b1) return t0 + ((t1 - t0) * (beat - b0)) / (b1 - b0);\n }\n return this.points[this.points.length - 1]![1];\n }\n}\n","import { TempoMap } from './notation';\nimport type { ResolvedMusic } from './resolve';\n\n/** One note or chord of a track, as a score reader sees it. */\nexport interface MusicNote {\n /** Where it starts: beats from the start, and the same moment in seconds (the tempo map applied; swing and humanize are not). */\n beat: number;\n time: number;\n /** How long it is held, in beats and in seconds. */\n duration: number;\n seconds: number;\n /** The bar it starts in (1-based, from `meter`) and its beat inside that bar (1 = the downbeat, 1.5 = the \"and\" of 1). */\n bar: number;\n beatInBar: number;\n /** The audible MIDI notes (`music.transpose` and the track's `transpose` are in). */\n midi: number[];\n /** What the score called it: `Am7`, `c4`, `[c4 e4 g4]`. */\n name: string;\n /** More than one note at once. */\n chord: boolean;\n /** 0–1 (0.8 normal, 1 accented with `!`, 0.55 soft with `,`). */\n velocity: number;\n}\n\nexport interface MusicTrackEvents {\n inst: string;\n /** The track's length in beats and in bars (not a whole number when a bar is short: that is what to look for). */\n beats: number;\n bars: number;\n notes: MusicNote[];\n}\n\nexport interface MusicDrumHit { beat: number; time: number; bar: number; beatInBar: number; velocity: number }\nexport interface MusicDrumEvents { kind: string; hits: MusicDrumHit[] }\n\nexport interface MusicEvents {\n /** The tempo at the start. */\n bpm: number;\n meter: number;\n beats: number;\n bars: number;\n /** The music without its reverb tail. */\n seconds: number;\n tracks: MusicTrackEvents[];\n /** Every drum that is hit, over all sections, with every hit written out. */\n drums: MusicDrumEvents[];\n}\n\n/** What the `check` tool prints about a music layer: lengths in bars (a short track shows), what starts in each bar, where the drums play. */\nexport interface MusicSummary {\n bpm: number;\n meter: number;\n /** The score set `meter`; when false 4 is only assumed. */\n meterSet: boolean;\n beats: number;\n bars: number;\n seconds: number;\n tail: number;\n tracks: Array<{ inst: string; beats: number; bars: number; events: number; perBar: string[] }>;\n drums: Array<{ from: number; to: number; kinds: string[] }>;\n}\n\nconst round = (x: number, digits = 4): number => Math.round(x * 10 ** digits) / 10 ** digits;\nconst barOf = (beat: number, meter: number): number => Math.floor(beat / meter + 1e-9) + 1;\n\nexport function describeEvents(m: ResolvedMusic): MusicEvents {\n const map = new TempoMap(m.tempo);\n const place = (beat: number) => { const bar = barOf(beat, m.meter); return { beat: round(beat), time: round(map.seconds(beat), 6), bar, beatInBar: round(beat - (bar - 1) * m.meter + 1) }; };\n const tracks = m.tracks.map(t => ({\n inst: t.inst,\n beats: t.length,\n bars: round(t.length / m.meter),\n notes: t.events.map((e): MusicNote => ({\n ...place(e.t), duration: round(e.dur), seconds: round(map.seconds(e.t + e.dur) - map.seconds(e.t), 6),\n midi: e.midi.map(n => n + t.transpose), name: e.name, chord: e.midi.length > 1, velocity: e.vel,\n })),\n }));\n const byKind = new Map<string, MusicDrumHit[]>();\n for (const s of m.drums) {\n const end = Math.min(s.to, m.beats);\n for (let rep = 0; s.from + rep * s.repeat < end - 1e-6; rep++) for (const d of s.drums) for (const h of d.hits) {\n const beat = s.from + rep * s.repeat + h.i / m.grid;\n if (beat >= end) continue;\n const list = byKind.get(d.kind) ?? [];\n list.push({ ...place(beat), velocity: h.vel });\n byKind.set(d.kind, list);\n }\n }\n const drums = [...byKind].map(([kind, hits]) => ({ kind, hits: hits.sort((a, b) => a.beat - b.beat) }));\n return { bpm: round(map.bpmAt(0), 2), meter: m.meter, beats: m.beats, bars: round(m.beats / m.meter), seconds: m.seconds, tracks, drums };\n}\n\nexport function summarizeMusic(m: ResolvedMusic): MusicSummary {\n const map = new TempoMap(m.tempo);\n return {\n bpm: round(map.bpmAt(0), 2), meter: m.meter, meterSet: m.meterSet, beats: m.beats, bars: round(m.beats / m.meter), seconds: m.seconds, tail: m.tail,\n tracks: m.tracks.map(t => {\n const perBar: string[][] = Array.from({ length: Math.max(1, Math.ceil(t.length / m.meter - 1e-9)) }, () => []);\n for (const e of t.events) {\n const bar = barOf(e.t, m.meter), at = round(e.t - (bar - 1) * m.meter + 1);\n perBar[Math.min(perBar.length - 1, bar - 1)]!.push(at === 1 ? e.name : `${e.name}(${at})`); // the beat in the bar, unless it is the downbeat (a bracket: `@` already means the octave in `C@4`)\n }\n return { inst: t.inst, beats: t.length, bars: round(t.length / m.meter), events: t.events.length, perBar: perBar.map(names => names.join(' ')) };\n }),\n // a section that starts at or after the end (or ends before it starts) never plays: render skips it, so the read-back does not list it\n drums: m.drums.filter(s => s.from < Math.min(s.to, m.beats) - 1e-6).map(s => ({ from: s.from, to: Math.min(s.to, m.beats), kinds: s.drums.filter(d => d.hits.length > 0).map(d => d.kind) })),\n };\n}\n"]}
|