zx-kit 0.26.0 → 0.27.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/README.md CHANGED
@@ -572,6 +572,7 @@ requestAnimationFrame(loop)
572
572
  | [`font.ts`](#fontts--rom-bitmap-font) | 96-character ROM font, raw bitmap access |
573
573
  | [`i18n.ts`](#i18nts--runtime-locale-selection) | Type-safe runtime locale selection for translated string packs |
574
574
  | [`lighting.ts`](#lightingts--dithered-cave-darkness) | Dithered cave darkness: pre-baked level tiles + dirty-cell buffer, one blit/frame (no per-frame putImageData) |
575
+ | [`music.ts`](#musicts--note-name-ay-music) | Write AY music by note name (`A5`, `C#4`) and loop it for background tracks |
575
576
 
576
577
  ---
577
578
 
@@ -2669,6 +2670,59 @@ The `darknessAt` callback is where you add **depth gradients** (darker the deepe
2669
2670
 
2670
2671
  ---
2671
2672
 
2673
+ ## `music.ts` — Note-Name AY Music
2674
+
2675
+ Write AY tunes by **note name** instead of raw frequencies, and **loop** them for
2676
+ background music. A thin, friendly layer over [`playAY`](#playaypattern-startdelay-void):
2677
+ the AY chip already plays three channels of `AYNote`s — this lets you *author* and
2678
+ *repeat* them without the maths (so "I don't read note tables" is no longer a blocker).
2679
+
2680
+ ### `noteToFreq(name): number`
2681
+
2682
+ Note name → frequency (Hz), equal temperament, **A4 = 440**. Accepts `A5`, `C#4`,
2683
+ `Db3`, `Fs5` (`s` = sharp). `r` / `-` is a rest → `0` (a silent note). Throws on a
2684
+ malformed name. Pure.
2685
+
2686
+ ```ts
2687
+ noteToFreq('A4') // 440
2688
+ noteToFreq('C4') // 261.63 (middle C)
2689
+ noteToFreq('A5') // 880
2690
+ ```
2691
+
2692
+ ### `seq(spec, options?): AYNote[]`
2693
+
2694
+ Parses a compact note string into one channel's `AYNote[]`. Tokens are
2695
+ whitespace-separated `Note` or `Note:durMs`; `r` (or `-`) is a rest. `options.dur`
2696
+ sets the default duration (200 ms); `options.noise` / `noisePeriod` mix LFSR noise
2697
+ into every note (handy for a texture/percussion channel).
2698
+
2699
+ ```ts
2700
+ seq('A4 C5:400 r:200 E5') // A4 @default, C5 @400ms, rest @200ms, E5 @default
2701
+ seq('r r r r', { dur: 240, noise: true }) // a noise-only texture line
2702
+ ```
2703
+
2704
+ ### `playAYLoop(pattern): { stop() }`
2705
+
2706
+ Plays a 3-channel pattern **on repeat** — background music. Re-schedules each loop
2707
+ after the pattern's length (its longest channel) and returns a handle to `stop()`.
2708
+ No-ops (returns a do-nothing stop) when there's no audio context yet or the pattern
2709
+ is empty. Call after a user gesture has unlocked audio.
2710
+
2711
+ ```ts
2712
+ const track = playAYLoop({
2713
+ a: seq('A4 C5 E5 C5', { dur: 240 }), // melody
2714
+ b: seq('A2:480 E2:480', { dur: 480 }), // slow bass drone
2715
+ c: seq('r r r r', { dur: 240, noise: true }), // texture
2716
+ })
2717
+ // later…
2718
+ track.stop()
2719
+ ```
2720
+
2721
+ > Looping re-schedules at the pattern boundary via a timer — fine for ambient /
2722
+ > background loops; for tight musical sync you'd want a sample-accurate scheduler.
2723
+
2724
+ ---
2725
+
2672
2726
  ## Architecture
2673
2727
 
2674
2728
  ### Module structure
package/dist/index.d.ts CHANGED
@@ -17,4 +17,5 @@ export * from './scene.js';
17
17
  export * from './save.js';
18
18
  export * from './i18n.js';
19
19
  export * from './lighting.js';
20
+ export * from './music.js';
20
21
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAA;AAC5B,cAAc,SAAS,CAAA;AACvB,cAAc,WAAW,CAAA;AACzB,cAAc,eAAe,CAAA;AAC7B,cAAc,YAAY,CAAA;AAC1B,cAAc,YAAY,CAAA;AAC1B,cAAc,SAAS,CAAA;AACvB,cAAc,cAAc,CAAA;AAC5B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,aAAa,CAAA;AAC3B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,UAAU,CAAA;AACxB,cAAc,gBAAgB,CAAA;AAC9B,cAAc,aAAa,CAAA;AAC3B,cAAc,YAAY,CAAA;AAC1B,cAAc,WAAW,CAAA;AACzB,cAAc,WAAW,CAAA;AACzB,cAAc,eAAe,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAA;AAC5B,cAAc,SAAS,CAAA;AACvB,cAAc,WAAW,CAAA;AACzB,cAAc,eAAe,CAAA;AAC7B,cAAc,YAAY,CAAA;AAC1B,cAAc,YAAY,CAAA;AAC1B,cAAc,SAAS,CAAA;AACvB,cAAc,cAAc,CAAA;AAC5B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,aAAa,CAAA;AAC3B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,UAAU,CAAA;AACxB,cAAc,gBAAgB,CAAA;AAC9B,cAAc,aAAa,CAAA;AAC3B,cAAc,YAAY,CAAA;AAC1B,cAAc,WAAW,CAAA;AACzB,cAAc,WAAW,CAAA;AACzB,cAAc,eAAe,CAAA;AAC7B,cAAc,YAAY,CAAA"}
package/dist/index.js CHANGED
@@ -17,4 +17,5 @@ export * from './scene.js';
17
17
  export * from './save.js';
18
18
  export * from './i18n.js';
19
19
  export * from './lighting.js';
20
+ export * from './music.js';
20
21
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAA;AAC5B,cAAc,SAAS,CAAA;AACvB,cAAc,WAAW,CAAA;AACzB,cAAc,eAAe,CAAA;AAC7B,cAAc,YAAY,CAAA;AAC1B,cAAc,YAAY,CAAA;AAC1B,cAAc,SAAS,CAAA;AACvB,cAAc,cAAc,CAAA;AAC5B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,aAAa,CAAA;AAC3B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,UAAU,CAAA;AACxB,cAAc,gBAAgB,CAAA;AAC9B,cAAc,aAAa,CAAA;AAC3B,cAAc,YAAY,CAAA;AAC1B,cAAc,WAAW,CAAA;AACzB,cAAc,WAAW,CAAA;AACzB,cAAc,eAAe,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAA;AAC5B,cAAc,SAAS,CAAA;AACvB,cAAc,WAAW,CAAA;AACzB,cAAc,eAAe,CAAA;AAC7B,cAAc,YAAY,CAAA;AAC1B,cAAc,YAAY,CAAA;AAC1B,cAAc,SAAS,CAAA;AACvB,cAAc,cAAc,CAAA;AAC5B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,aAAa,CAAA;AAC3B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,UAAU,CAAA;AACxB,cAAc,gBAAgB,CAAA;AAC9B,cAAc,aAAa,CAAA;AAC3B,cAAc,YAAY,CAAA;AAC1B,cAAc,WAAW,CAAA;AACzB,cAAc,WAAW,CAAA;AACzB,cAAc,eAAe,CAAA;AAC7B,cAAc,YAAY,CAAA"}
@@ -0,0 +1,61 @@
1
+ /**
2
+ * @module music
3
+ *
4
+ * Write AY music by **note name** instead of raw frequencies, and **loop** it for
5
+ * background tracks. A thin, friendly layer over {@link playAY} (the AY chip
6
+ * already plays three channels of {@link AYNote}s — this just lets you author and
7
+ * repeat them without doing the maths).
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * import { seq, playAYLoop, noteToFreq } from 'zx-kit'
12
+ *
13
+ * const loop = playAYLoop({
14
+ * a: seq('A4 C5 E5 C5', { dur: 240 }), // melody by name
15
+ * b: seq('A2:480 E2:480', { dur: 480 }), // slow bass drone
16
+ * c: seq('r r r r', { dur: 240, noise: true }), // a little texture
17
+ * })
18
+ * // later: loop.stop()
19
+ * ```
20
+ */
21
+ import { type AYNote } from './ay.js';
22
+ /**
23
+ * Note name → frequency (Hz), equal temperament, A4 = 440. Accepts `A5`, `C#4`,
24
+ * `Db3`, `Fs5` (`s` = sharp); `r` / `-` is a rest → `0` (a silent {@link AYNote}).
25
+ * Throws on a malformed name.
26
+ */
27
+ export declare function noteToFreq(name: string): number;
28
+ /** Options for {@link seq}. */
29
+ export interface SeqOptions {
30
+ /** Default note duration in ms when a token doesn't specify one (default 200). */
31
+ dur?: number;
32
+ /** Mix LFSR noise into every note (e.g. for a percussive/texture channel). */
33
+ noise?: boolean;
34
+ /** Noise period when `noise` is set. */
35
+ noisePeriod?: number;
36
+ }
37
+ /**
38
+ * Parses a compact note string into an {@link AYNote} array for one channel.
39
+ * Tokens are whitespace-separated `Note` or `Note:durMs`; `r` (or `-`) is a rest.
40
+ *
41
+ * @example
42
+ * seq('A4 C5:400 r:200 E5') // A4 @default, C5 @400ms, rest @200ms, E5 @default
43
+ */
44
+ export declare function seq(spec: string, opts?: SeqOptions): AYNote[];
45
+ /** A running looped track. Call {@link LoopHandle.stop} to end it. */
46
+ export interface LoopHandle {
47
+ stop(): void;
48
+ }
49
+ /**
50
+ * Plays a 3-channel AY pattern on repeat — background music. Re-schedules each
51
+ * loop after the pattern's length (the longest channel). No-ops (returns a stop
52
+ * that does nothing) when there is no audio context yet or the pattern is empty.
53
+ *
54
+ * Call after the audio context is unlocked by a user gesture.
55
+ */
56
+ export declare function playAYLoop(pattern: {
57
+ a?: AYNote[];
58
+ b?: AYNote[];
59
+ c?: AYNote[];
60
+ }): LoopHandle;
61
+ //# sourceMappingURL=music.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"music.d.ts","sourceRoot":"","sources":["../src/music.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EAAU,KAAK,MAAM,EAAE,MAAM,SAAS,CAAA;AAK7C;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAW/C;AAED,+BAA+B;AAC/B,MAAM,WAAW,UAAU;IACzB,kFAAkF;IAClF,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,8EAA8E;IAC9E,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,wCAAwC;IACxC,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED;;;;;;GAMG;AACH,wBAAgB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,UAAe,GAAG,MAAM,EAAE,CAajE;AAED,sEAAsE;AACtE,MAAM,WAAW,UAAU;IACzB,IAAI,IAAI,IAAI,CAAA;CACb;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE;IAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC;IAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC;IAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,GAAG,UAAU,CAa5F"}
package/dist/music.js ADDED
@@ -0,0 +1,89 @@
1
+ /**
2
+ * @module music
3
+ *
4
+ * Write AY music by **note name** instead of raw frequencies, and **loop** it for
5
+ * background tracks. A thin, friendly layer over {@link playAY} (the AY chip
6
+ * already plays three channels of {@link AYNote}s — this just lets you author and
7
+ * repeat them without doing the maths).
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * import { seq, playAYLoop, noteToFreq } from 'zx-kit'
12
+ *
13
+ * const loop = playAYLoop({
14
+ * a: seq('A4 C5 E5 C5', { dur: 240 }), // melody by name
15
+ * b: seq('A2:480 E2:480', { dur: 480 }), // slow bass drone
16
+ * c: seq('r r r r', { dur: 240, noise: true }), // a little texture
17
+ * })
18
+ * // later: loop.stop()
19
+ * ```
20
+ */
21
+ import { playAY } from './ay.js';
22
+ import { getAudioContext } from './audio.js';
23
+ const SEMITONE = { c: 0, d: 2, e: 4, f: 5, g: 7, a: 9, b: 11 };
24
+ /**
25
+ * Note name → frequency (Hz), equal temperament, A4 = 440. Accepts `A5`, `C#4`,
26
+ * `Db3`, `Fs5` (`s` = sharp); `r` / `-` is a rest → `0` (a silent {@link AYNote}).
27
+ * Throws on a malformed name.
28
+ */
29
+ export function noteToFreq(name) {
30
+ const n = name.trim().toLowerCase();
31
+ if (n === 'r' || n === '-' || n === '')
32
+ return 0;
33
+ const m = /^([a-g])([#sb]?)(-?\d)$/.exec(n);
34
+ if (!m)
35
+ throw new Error(`noteToFreq: bad note "${name}" (expected e.g. A5, C#4, Bb3, or r)`);
36
+ let semis = SEMITONE[m[1]];
37
+ if (m[2] === '#' || m[2] === 's')
38
+ semis += 1;
39
+ else if (m[2] === 'b')
40
+ semis -= 1;
41
+ const octave = parseInt(m[3], 10);
42
+ const midi = (octave + 1) * 12 + semis; // C4 = MIDI 60, A4 = 69
43
+ return 440 * Math.pow(2, (midi - 69) / 12);
44
+ }
45
+ /**
46
+ * Parses a compact note string into an {@link AYNote} array for one channel.
47
+ * Tokens are whitespace-separated `Note` or `Note:durMs`; `r` (or `-`) is a rest.
48
+ *
49
+ * @example
50
+ * seq('A4 C5:400 r:200 E5') // A4 @default, C5 @400ms, rest @200ms, E5 @default
51
+ */
52
+ export function seq(spec, opts = {}) {
53
+ const defDur = opts.dur ?? 200;
54
+ const out = [];
55
+ for (const tok of spec.split(/\s+/).filter(Boolean)) {
56
+ const [name, durStr] = tok.split(':');
57
+ const note = { freq: noteToFreq(name), dur: durStr ? Number(durStr) : defDur };
58
+ if (opts.noise) {
59
+ note.noise = true;
60
+ if (opts.noisePeriod !== undefined)
61
+ note.noisePeriod = opts.noisePeriod;
62
+ }
63
+ out.push(note);
64
+ }
65
+ return out;
66
+ }
67
+ /**
68
+ * Plays a 3-channel AY pattern on repeat — background music. Re-schedules each
69
+ * loop after the pattern's length (the longest channel). No-ops (returns a stop
70
+ * that does nothing) when there is no audio context yet or the pattern is empty.
71
+ *
72
+ * Call after the audio context is unlocked by a user gesture.
73
+ */
74
+ export function playAYLoop(pattern) {
75
+ if (!getAudioContext())
76
+ return { stop() { } };
77
+ const total = (ns) => (ns ? ns.reduce((s, n) => s + n.dur, 0) : 0);
78
+ const loopMs = Math.max(total(pattern.a), total(pattern.b), total(pattern.c));
79
+ if (loopMs <= 0)
80
+ return { stop() { } };
81
+ playAY(pattern);
82
+ const id = setInterval(() => playAY(pattern), loopMs);
83
+ return {
84
+ stop() {
85
+ clearInterval(id);
86
+ },
87
+ };
88
+ }
89
+ //# sourceMappingURL=music.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"music.js","sourceRoot":"","sources":["../src/music.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zx-kit",
3
- "version": "0.26.0",
3
+ "version": "0.27.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/zrebec/zx-kit.git"