zx-kit 0.35.0 → 0.37.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.
@@ -0,0 +1,291 @@
1
+ /**
2
+ * @module aydump
3
+ *
4
+ * **Sample-accurate AY register-dump player** — the "sample-accurate
5
+ * AudioWorklet backend" that {@link "ay" | ay.ts} promises on its roadmap.
6
+ * Where `ay.ts` *synthesises* notes (`AYNote[]` → band-limited oscillators),
7
+ * this module *reproduces the hardware*: it feeds a cycle-level emulation of
8
+ * the AY-3-8910 / YM2149 chip a stream of register writes (a **PSG dump**) and
9
+ * renders real PCM — so you can play **actual ZX-scene tunes** in a game.
10
+ *
11
+ * The pipeline is data-driven, exactly like a bitmap font:
12
+ *
13
+ * ```
14
+ * .psg file (register dump, bundled as data)
15
+ * │ parsePSG()
16
+ * ▼
17
+ * AYDump (flat typed-array frames: who-writes-what-when)
18
+ * │ AYDumpPlayer (applies each frame's writes every sampleRate/50 samples)
19
+ * ▼
20
+ * AYChipCore (16 registers → L/R PCM, HW logic from the datasheet)
21
+ * │ playAYDump() (realtime AudioWorklet) or renderAYDump() (offline)
22
+ * ▼
23
+ * master GainNode (shared with audio.ts / ay.ts — global M mute works for free)
24
+ * ```
25
+ *
26
+ * **Scope:** v1 is the **PSG** format (a raw register dump — HW-level, unambiguous,
27
+ * dependency-free). Anything from zxart.ee (PT3, STC…) is converted to `.psg`
28
+ * **offline** on a PC (Ay_Emul "save as PSG", `zxtune123 --convert`, Vortex
29
+ * Tracker II). The dump is *data*, not a code dependency — Zero Dependencies stays
30
+ * clean. A native PT3 replay routine over the same {@link AYChipCore} is a future
31
+ * (v2) module.
32
+ *
33
+ * **Authorship:** a PSG dump is a derivative of the original tune. zxart hosting is
34
+ * not a licence — ship a track only with the composer's permission (credit + link).
35
+ *
36
+ * @see {@link "ay" | ay.ts} for note-based music, {@link "audio" | audio.ts} for the beeper.
37
+ */
38
+ import type { AYStereoMode } from './ay.js';
39
+ /** Chip variant: `ay` = 16-step envelope; `ym` = 32-step envelope (finer). */
40
+ export type AYChipVariant = 'ay' | 'ym';
41
+ /** How to configure the emulated chip. All fields optional — see defaults below. */
42
+ export interface AYChipConfig {
43
+ /** Chip clock in Hz. Default `1_773_400` (ZX 128K). Melodik = `1_750_000`. */
44
+ clockHz?: number;
45
+ /** Chip variant. Default `'ay'`. */
46
+ variant?: AYChipVariant;
47
+ /** Stereo preset. Default `'acb'` (Melodik jack: A left, C centre, B right). */
48
+ stereo?: AYStereoMode;
49
+ /** 16-entry DAC table (level → amplitude). Default zx-kit `AY_VOL`; hook for measured tables. */
50
+ dacTable?: readonly number[];
51
+ }
52
+ /** Ready-made presets for real machines. */
53
+ export declare const AY_MACHINE: {
54
+ /** ZX Spectrum 128K / +2 / +3 — TV speaker, mono. */
55
+ readonly zx128: {
56
+ readonly clockHz: 1773400;
57
+ readonly variant: "ay";
58
+ readonly stereo: "mono";
59
+ };
60
+ /** Didaktik + Melodik add-on — 1.75 MHz, ACB stereo jack. */
61
+ readonly melodik: {
62
+ readonly clockHz: 1750000;
63
+ readonly variant: "ay";
64
+ readonly stereo: "acb";
65
+ };
66
+ /** Pentagon clone — 1.75 MHz, ACB. */
67
+ readonly pentagon: {
68
+ readonly clockHz: 1750000;
69
+ readonly variant: "ay";
70
+ readonly stereo: "acb";
71
+ };
72
+ /** Atari ST — YM2149 at 2 MHz, mono. */
73
+ readonly atariST: {
74
+ readonly clockHz: 2000000;
75
+ readonly variant: "ym";
76
+ readonly stereo: "mono";
77
+ };
78
+ };
79
+ /**
80
+ * Cycle-level AY-3-8910 / YM2149 emulator: register writes in, PCM samples out.
81
+ * Three square-wave tone channels, a shared 17-bit LFSR noise generator, and the
82
+ * hardware envelope generator (16 or 32 steps). Pure and self-contained so it runs
83
+ * inside an AudioWorklet **and** in a headless test with no `AudioContext`.
84
+ *
85
+ * Named `AYChipCore` (not `AYChip`) so it does not collide with the real-time
86
+ * {@link "ay".AYChip} handle interface exported from `ay.ts`.
87
+ */
88
+ export declare class AYChipCore {
89
+ private readonly envSteps;
90
+ private readonly dac;
91
+ private readonly regs;
92
+ private readonly toneCnt;
93
+ private readonly toneOut;
94
+ private noiseCnt;
95
+ private lfsr;
96
+ private noiseOut;
97
+ private envCnt;
98
+ private envPos;
99
+ private envAttack;
100
+ private envHolding;
101
+ private envCont;
102
+ private envAlt;
103
+ private envHold;
104
+ private cycleAcc;
105
+ private readonly cyclesPerSample;
106
+ private panL;
107
+ private panR;
108
+ private dcxL;
109
+ private dcyL;
110
+ private dcxR;
111
+ private dcyR;
112
+ /**
113
+ * @param sampleRate Output sample rate (Hz).
114
+ * @param clockHz Chip master clock (Hz).
115
+ * @param envSteps Envelope resolution: 16 (AY) or 32 (YM).
116
+ * @param dac 16-entry DAC amplitude table (level → 0..1).
117
+ * @param stereo Stereo preset (mono / abc / acb).
118
+ */
119
+ constructor(sampleRate: number, clockHz: number, envSteps: number, dac: readonly number[], stereo: AYStereoMode);
120
+ /** Clears counters, registers, LFSR, envelope and DC state. Call at start / seek. */
121
+ reset(): void;
122
+ /**
123
+ * Applies a demoscene stereo preset. Soft panning (0.75/0.25), so headphones
124
+ * don't get a hard 1/0 split; L+R gain per channel always sums to 1.
125
+ */
126
+ setStereo(mode: AYStereoMode): void;
127
+ /**
128
+ * Writes a chip register (0..15), masking to the hardware register width.
129
+ * Writing R13 (envelope shape) always restarts the envelope.
130
+ */
131
+ setRegister(r: number, v: number): void;
132
+ /** Steps the 17-bit LFSR once (feedback = bit0 XOR bit3) and latches the output bit. */
133
+ private stepNoise;
134
+ /** Advances the envelope generator by one step (called when its counter elapses). */
135
+ private envStep;
136
+ /** Current envelope amplitude as a DAC index (0..15). */
137
+ private envLevel;
138
+ /**
139
+ * Renders `count` stereo samples into `left`/`right` starting at `offset`.
140
+ * Box-filters the chip's raw ticks within each output sample (a cheap
141
+ * anti-alias), then DC-blocks each output channel.
142
+ */
143
+ renderInto(left: Float32Array, right: Float32Array, offset: number, count: number): void;
144
+ /**
145
+ * @internal Test hook — steps the LFSR `n` times, returning each output bit.
146
+ * Locks the noise sequence against a tap/shift typo (see stepNoise).
147
+ */
148
+ _stepNoiseForTest(n: number): number[];
149
+ /**
150
+ * @internal Test hook — advances the envelope generator `n` times (after an R13
151
+ * write), returning the amplitude level (0..15) after each step.
152
+ */
153
+ _stepEnvelopeForTest(n: number): number[];
154
+ }
155
+ /**
156
+ * A parsed PSG register dump — flat typed arrays so it transfers into the
157
+ * AudioWorklet with zero copying and allocates nothing on the audio thread.
158
+ * Frame `f`'s writes are `writeRegs[frameOffsets[f] .. frameOffsets[f+1])`.
159
+ */
160
+ export interface AYDump {
161
+ /** Frame rate (PSG = 50 Hz). */
162
+ frameRateHz: number;
163
+ /** Number of frames. */
164
+ frameCount: number;
165
+ /** Register index (0..15) per write, all frames concatenated. */
166
+ writeRegs: Uint8Array;
167
+ /** Value per write, parallel to {@link writeRegs}. */
168
+ writeVals: Uint8Array;
169
+ /** Length `frameCount + 1`; last element = `writeRegs.length`. */
170
+ frameOffsets: Uint32Array;
171
+ }
172
+ /**
173
+ * Parses a PSG register dump (the format Ay_Emul / zxtune produce) into an
174
+ * {@link AYDump}. A PSG file is a `"PSG\x1a"` header then a byte stream:
175
+ *
176
+ * - `0xFF` — start a new frame
177
+ * - `0xFE n` — `n × 4` empty frames (chip holds its state)
178
+ * - `0xFD` — end of song (often absent: end-of-data ends the song)
179
+ * - `0x00..0x0F` — register index; the next byte is its value
180
+ * - `0x10..0xFC` — foreign device (MSX / 2nd chip) → skip this byte and the next
181
+ *
182
+ * @throws if the header is wrong or a register/`0xFE` pair is truncated.
183
+ */
184
+ export declare function parsePSG(bytes: Uint8Array): AYDump;
185
+ /**
186
+ * Drives an {@link AYChipCore} from an {@link AYDump}: applies each frame's
187
+ * register writes on the right sample and fills PCM buffers. Uses a *fractional*
188
+ * samples-per-frame accumulator so a 3-minute tune doesn't drift at exotic rates.
189
+ * Pure — testable with a spy chip, runnable inside the worklet.
190
+ */
191
+ export declare class AYDumpPlayer {
192
+ private readonly chip;
193
+ private readonly writeRegs;
194
+ private readonly writeVals;
195
+ private readonly frameOffsets;
196
+ private readonly loop;
197
+ private readonly loopFrame;
198
+ private readonly frameCount;
199
+ private readonly samplesPerFrame;
200
+ private frame;
201
+ private frameAcc;
202
+ private ended;
203
+ /**
204
+ * @param chip The chip to drive.
205
+ * @param writeRegs Concatenated register indices (see {@link AYDump}).
206
+ * @param writeVals Concatenated values.
207
+ * @param frameOffsets Frame boundaries (length `frameCount + 1`).
208
+ * @param frameRateHz Frames per second (PSG = 50).
209
+ * @param sampleRate Output sample rate (Hz).
210
+ * @param loop Restart at `loopFrame` after the last frame.
211
+ * @param loopFrame Frame to jump to when looping.
212
+ */
213
+ constructor(chip: AYChipCore, writeRegs: Uint8Array, writeVals: Uint8Array, frameOffsets: Uint32Array, frameRateHz: number, sampleRate: number, loop: boolean, loopFrame: number);
214
+ private applyFrame;
215
+ /** Fills `left`/`right` completely. Returns `true` once the (non-looping) song has ended. */
216
+ render(left: Float32Array, right: Float32Array): boolean;
217
+ /** Fast-forwards to frame `f` by replaying its register writes without rendering. */
218
+ seekToFrame(f: number): void;
219
+ }
220
+ /** Live handle to a {@link playAYDump} call. */
221
+ export interface AYDumpHandle {
222
+ /** Detach the node and stop playback (irreversible). */
223
+ stop(): void;
224
+ /** Silence via the gain node (the worklet keeps running) — instant. */
225
+ pause(): void;
226
+ /** Restore the pre-pause volume. */
227
+ resume(): void;
228
+ /** Per-track volume, 0..1 (master volume stays with audio.ts). */
229
+ setVolume(v: number): void;
230
+ /** Change the stereo preset live. */
231
+ setStereo(mode: AYStereoMode): void;
232
+ /** `true` while the node is connected and not stopped. */
233
+ readonly playing: boolean;
234
+ /** Called once when a non-looping song finishes. */
235
+ onEnded?: () => void;
236
+ }
237
+ /** Options for {@link playAYDump}. */
238
+ export interface PlayAYDumpOptions extends AYChipConfig {
239
+ /** Loop at `loopFrame` after the last frame. Default `false`. */
240
+ loop?: boolean;
241
+ /** Frame to loop back to. Default `0`. */
242
+ loopFrame?: number;
243
+ /** Initial per-track volume 0..1. Default `1`. */
244
+ volume?: number;
245
+ }
246
+ /**
247
+ * Assembles the AudioWorklet source: the two worklet-safe classes stringified,
248
+ * plus a processor that loads a dump and renders it. The class names are read via
249
+ * `.name` (not hardcoded) so the glue stays consistent even if a bundler mangles
250
+ * them — {@link AYChipCore}.name and its `toString()` agree under minification.
251
+ *
252
+ * Internal; exported only so a test can assert the source is self-contained.
253
+ */
254
+ export declare function _buildAYDumpWorkletSource(): string;
255
+ /**
256
+ * Plays a parsed {@link AYDump} in real time through an AudioWorklet, routed to the
257
+ * shared master gain (so global `M` mute / `setMasterVolume` apply for free).
258
+ * Call after {@link initAudio} inside a user gesture — same contract as `playAY`.
259
+ *
260
+ * @example
261
+ * const dump = await loadPSG('./music/tune.psg')
262
+ * const track = await playAYDump(dump, { loop: true, ...AY_MACHINE.melodik })
263
+ * // later: track.setVolume(0.6); track.stop()
264
+ */
265
+ export declare function playAYDump(dump: AYDump, opts?: PlayAYDumpOptions): Promise<AYDumpHandle>;
266
+ /** Options for {@link renderAYDump}. */
267
+ export interface RenderAYDumpOptions extends AYChipConfig {
268
+ /** Output sample rate. Default `44100`. */
269
+ sampleRate?: number;
270
+ /** Cap the render length in seconds. Default = the whole song. */
271
+ maxSeconds?: number;
272
+ }
273
+ /**
274
+ * Renders a dump to stereo PCM **without an AudioContext** — deterministic, for
275
+ * unit tests, `AudioWorklet`-less fallbacks, and offline tooling (e.g. PSG → WAV).
276
+ * Beware RAM: a full 3-minute stereo render at 44.1 kHz is ≈ 64 MB — pass
277
+ * `maxSeconds` to bound it.
278
+ */
279
+ export declare function renderAYDump(dump: AYDump, opts?: RenderAYDumpOptions): {
280
+ left: Float32Array;
281
+ right: Float32Array;
282
+ };
283
+ /**
284
+ * Fetches a `.psg` file and parses it into an {@link AYDump}. Network / parse
285
+ * errors propagate so the game decides how to handle them.
286
+ *
287
+ * @example
288
+ * const dump = await loadPSG('./music/tune.psg')
289
+ */
290
+ export declare function loadPSG(url: string): Promise<AYDump>;
291
+ //# sourceMappingURL=aydump.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"aydump.d.ts","sourceRoot":"","sources":["../src/aydump.ts"],"names":[],"mappings":"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"}