blockyard 0.0.9 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/CHANGELOG.md +251 -1
  2. package/README.md +42 -23
  3. package/bin/blockyard.js +2 -1
  4. package/docs/API.md +16 -14
  5. package/docs/ARCHITECTURE.md +92 -5
  6. package/docs/CONFIGURATION.md +33 -26
  7. package/docs/GETTING-STARTED.md +5 -2
  8. package/docs/INSTALL.md +90 -33
  9. package/docs/MEASUREMENTS.md +147 -0
  10. package/docs/SECURITY.md +32 -15
  11. package/docs/TROUBLESHOOTING.md +35 -1
  12. package/docs/USER-GUIDE.md +266 -26
  13. package/package.json +1 -1
  14. package/public/404.html +1 -1
  15. package/public/css/app.css +306 -82
  16. package/public/donate-qr.png +0 -0
  17. package/public/index.html +295 -103
  18. package/public/js/agents.js +228 -51
  19. package/public/js/app.js +82 -8
  20. package/public/js/blockscene3d.js +179 -27
  21. package/public/js/charts.js +21 -21
  22. package/public/js/depthchart.js +31 -27
  23. package/public/js/details3d.js +1456 -71
  24. package/public/js/doom.js +31 -0
  25. package/public/js/dosaudio.js +48 -0
  26. package/public/js/dosgame.js +389 -0
  27. package/public/js/dosio.js +186 -0
  28. package/public/js/dospc.js +1353 -0
  29. package/public/js/dosworker.js +196 -0
  30. package/public/js/login.js +5 -0
  31. package/public/js/markets.js +46 -8
  32. package/public/js/mining.js +310 -32
  33. package/public/js/panels.js +14 -10
  34. package/public/js/pricechart.js +14 -13
  35. package/public/js/quake.js +20 -0
  36. package/public/js/settings.js +103 -21
  37. package/public/js/soundcard.js +459 -0
  38. package/public/js/theme.js +235 -0
  39. package/public/js/wolf3d.js +22 -0
  40. package/public/js/x86.js +1978 -0
  41. package/scripts/donate-qr.py +12 -9
  42. package/scripts/dos-bench.js +56 -0
  43. package/scripts/setup.js +34 -12
  44. package/scripts/shots.mjs +6 -0
  45. package/scripts/smoke.sh +1 -1
  46. package/scripts/tls.js +31 -0
  47. package/server/chain/index/build.js +21 -4
  48. package/server/collect/monitor.js +30 -1
  49. package/server/collect/network.js +295 -0
  50. package/server/config.js +46 -22
  51. package/server/http/api.js +49 -5
  52. package/server/http/games.js +77 -0
  53. package/server/http/server.js +8 -0
  54. package/server/main.js +53 -8
  55. package/server/tls/selfsigned.js +160 -0
  56. package/systemd/blockyard.service +7 -5
  57. package/docs/PRIVATE-LEADERBOARD.md +0 -230
  58. package/docs/STATE-2026-09-09.md +0 -200
@@ -0,0 +1,459 @@
1
+ // SOUNDCARD: the Sound Blaster Pro 2 in the DOOM Diversion's PC -- a DSP that plays digitised sound
2
+ // through DMA, and an OPL3 FM synthesiser for the music (operator, 2026-09-15: "Get DOOM working as
3
+ // a diversion inside blockyard with zero dependancies").
4
+ //
5
+ // Our own implementation, at the level a driver sees the card: the DSP's reset handshake, command
6
+ // bytes and interrupts; the 8237 DMA controller's address and count registers, which is where the
7
+ // samples come from (the driver leaves them in conventional memory and the card pulls them); and the
8
+ // OPL's register map, timers and status byte, which is how a driver decides the chip exists at all.
9
+ //
10
+ // THE SYNTHESIS IS A MODEL, NOT A CHIP. Each OPL operator is a phase accumulator, one of eight
11
+ // waveforms, and an envelope in decibels (attack, decay to the sustain level, release) with the
12
+ // chip's documented rate scaling: 2826 ms for a full attack and 39281 ms for a full 96 dB decay at
13
+ // rate 4, each rate step a quarter of an octave faster. Modulation is phase modulation in cycles --
14
+ // a modulator at full level moves its carrier by four cycles, the chip's own scaling. It sounds like
15
+ // an OPL; it is not sample-exact to one, and nothing here pretends it is.
16
+ //
17
+ // Time is the machine's: `tick(t)` is called with the PC's clock, and every millisecond of it
18
+ // produces `rate` samples a second of stereo output, mixed from the DSP's DAC and the FM voices, into
19
+ // a ring the caller drains. The DSP's end-of-block interrupt is raised from inside that same loop,
20
+ // so a driver's next block is requested exactly when the last one ran out, in machine time.
21
+
22
+ const OPL_RATE = 49716;
23
+
24
+ // ------------------------------------------------------------------ OPL tables
25
+ const WAVE_BITS = 10, WAVE_LEN = 1 << WAVE_BITS;
26
+ const WAVES = [];
27
+ {
28
+ const sin = (i) => Math.sin((2 * Math.PI * (i + 0.5)) / WAVE_LEN);
29
+ const mk = (f) => { const t = new Float32Array(WAVE_LEN); for (let i = 0; i < WAVE_LEN; i++) t[i] = f(i); return t; };
30
+ const half = WAVE_LEN / 2, quarter = WAVE_LEN / 4;
31
+ WAVES.push(mk((i) => sin(i))); // 0 sine
32
+ WAVES.push(mk((i) => (i < half ? sin(i) : 0))); // 1 half-sine
33
+ WAVES.push(mk((i) => Math.abs(sin(i)))); // 2 abs-sine
34
+ WAVES.push(mk((i) => ((i % half) < quarter ? Math.abs(sin(i)) : 0))); // 3 pulse-sine
35
+ WAVES.push(mk((i) => (i < half ? sin(i * 2) : 0))); // 4 even-periods sine
36
+ WAVES.push(mk((i) => (i < half ? Math.abs(sin(i * 2)) : 0))); // 5 even-periods abs-sine
37
+ WAVES.push(mk((i) => (i < half ? 1 : -1))); // 6 square
38
+ WAVES.push(mk((i) => (i < half ? Math.pow(10, -(i / half) * 4.8) : -Math.pow(10, -((WAVE_LEN - 1 - i) / half) * 4.8)))); // 7 derived square
39
+ }
40
+ // the tremolo LFO: a raised cosine, 0 to 1, over 256 steps
41
+ const LFO_TREM = new Float32Array(256);
42
+ for (let i = 0; i < 256; i++) LFO_TREM[i] = (1 - Math.cos((2 * Math.PI * i) / 256)) * 0.5;
43
+ const MULT = [0.5, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 12, 12, 15, 15];
44
+ const KSL_TABLE = [0, 18, 24, 27, 30, 32.25, 33.75, 35.25, 36, 37.5, 38.25, 39, 39.75, 40.5, 41.25, 42];
45
+ const KSL_SCALE = [0, 0.5, 0.25, 1]; // 0, 3, 1.5 and 6 dB an octave, as fractions of the table's 6
46
+ const MAX_DB = 96;
47
+ // dB -> linear, in tenths of a dB: the envelope is summed in dB and looked up once a sample
48
+ const DB_STEPS = 10;
49
+ const DB_TO_AMP = new Float32Array(MAX_DB * DB_STEPS * 2 + 2);
50
+ for (let i = 0; i < DB_TO_AMP.length; i++) DB_TO_AMP[i] = i >= MAX_DB * DB_STEPS ? 0 : Math.pow(10, -i / DB_STEPS / 20);
51
+
52
+ /** Seconds for a full attack and a full 96 dB decay at an effective rate (0..63). */
53
+ export function oplRateTimes(rate) {
54
+ if (rate < 4) return { attack: Infinity, decay: Infinity };
55
+ if (rate >= 60) return { attack: 0, decay: 0.3 / 1000 * Math.pow(2, -(Math.min(rate, 63) - 60) / 4) * 8 };
56
+ const k = Math.pow(2, (rate - 4) / 4);
57
+ return { attack: 2.82624 / k, decay: 39.28064 / k };
58
+ }
59
+
60
+ const EG_OFF = 0, EG_ATTACK = 1, EG_DECAY = 2, EG_SUSTAIN = 3, EG_RELEASE = 4;
61
+
62
+ class Operator {
63
+ constructor() {
64
+ this.am = 0; this.vib = 0; this.egt = 0; this.ksr = 0; this.mult = 1;
65
+ this.ksl = 0; this.tl = 0; this.ar = 0; this.dr = 0; this.sl = 0; this.rr = 0; this.wave = 0;
66
+ this.phase = 0; this.env = MAX_DB; this.stage = EG_OFF;
67
+ this.out = 0; this.prev = 0;
68
+ // worked out per render call from the registers (Opl3.prepare)
69
+ this.attackK = 0; this.decayInc = 0; this.releaseInc = 0; this.slLevel = 0; this.levelDb = 0; this.inc = 0; this.table = null;
70
+ }
71
+ }
72
+
73
+ class Channel {
74
+ constructor() {
75
+ this.fnum = 0; this.block = 0; this.key = false; this.fb = 0; this.cnt = 0;
76
+ this.left = true; this.right = true;
77
+ this.ops = [new Operator(), new Operator()];
78
+ }
79
+ }
80
+
81
+ export class Opl3 {
82
+ constructor(rate) {
83
+ this.rate = rate;
84
+ this.reg = new Uint8Array(0x200);
85
+ this.chans = Array.from({ length: 18 }, () => new Channel());
86
+ this.newMode = false; // register 0x105 bit 0: OPL3 features on
87
+ this.wse = false; // register 0x01 bit 5: waveform select (OPL2)
88
+ this.nts = 0;
89
+ this.amDepth = 1; this.vibDepth = 7;
90
+ this.lfoAm = 0; this.lfoVib = 0;
91
+ this.timer1 = 0; this.timer2 = 0; this.status = 0; this.timerCtl = 0;
92
+ this.index = [0, 0];
93
+ this.trem = new Float32Array(0); this.vib = new Float32Array(0);
94
+ }
95
+
96
+ /** The status byte: timer flags in bits 5-7, and OPL3's low bits read 0 (an OPL2 reads 6). */
97
+ readStatus() { return this.status; }
98
+
99
+ write(array, reg, v) {
100
+ const r = (array << 8) | reg;
101
+ this.reg[r] = v;
102
+ if (array === 1 && reg === 0x05) { this.newMode = (v & 1) === 1; return; }
103
+ if (array === 0) {
104
+ switch (reg) {
105
+ case 0x01: this.wse = (v & 0x20) !== 0; return;
106
+ case 0x02: this.timer1 = v; return;
107
+ case 0x03: this.timer2 = v; return;
108
+ case 0x04:
109
+ if (v & 0x80) { this.status = 0; return; } // reset the flags
110
+ this.timerCtl = v;
111
+ // a started timer has overflowed by the time a driver reads the status: detection waits
112
+ // longer than either period, so it is honest to raise the flag at once
113
+ if ((v & 1) && !(v & 0x40)) this.status |= 0xc0;
114
+ if ((v & 2) && !(v & 0x20)) this.status |= 0xa0;
115
+ return;
116
+ case 0x08: this.nts = (v >> 6) & 1; return;
117
+ case 0xbd: this.amDepth = v & 0x80 ? 4.8 : 1; this.vibDepth = v & 0x40 ? 14 : 7; return;
118
+ }
119
+ }
120
+ const base = array === 0 ? 0 : 9;
121
+ if (reg >= 0x20 && reg <= 0xf5 && reg < 0xa0 || reg >= 0xe0) {
122
+ const opi = OP_SLOT[reg & 0x1f];
123
+ if (opi < 0) return;
124
+ const ch = this.chans[base + Math.floor(opi / 2)], op = ch.ops[opi % 2];
125
+ switch (reg & 0xe0) {
126
+ case 0x20: op.am = (v >> 7) & 1; op.vib = (v >> 6) & 1; op.egt = (v >> 5) & 1; op.ksr = (v >> 4) & 1; op.mult = MULT[v & 15]; return;
127
+ case 0x40: op.ksl = (v >> 6) & 3; op.tl = v & 0x3f; return;
128
+ case 0x60: op.ar = (v >> 4) & 15; op.dr = v & 15; return;
129
+ case 0x80: op.sl = (v >> 4) & 15; op.rr = v & 15; if (op.stage === EG_SUSTAIN && op.env < this.slDb(op)) op.stage = EG_DECAY; return;
130
+ case 0xe0: op.wave = v & 7; return;
131
+ }
132
+ return;
133
+ }
134
+ if (reg >= 0xa0 && reg <= 0xa8) { const ch = this.chans[base + reg - 0xa0]; ch.fnum = (ch.fnum & 0x300) | v; return; }
135
+ if (reg >= 0xb0 && reg <= 0xb8) {
136
+ const ch = this.chans[base + reg - 0xb0];
137
+ ch.fnum = (ch.fnum & 0xff) | ((v & 3) << 8);
138
+ ch.block = (v >> 2) & 7;
139
+ const key = (v & 0x20) !== 0;
140
+ if (key && !ch.key) for (const op of ch.ops) { op.stage = EG_ATTACK; op.phase = 0; }
141
+ if (!key && ch.key) for (const op of ch.ops) if (op.stage !== EG_OFF) op.stage = EG_RELEASE;
142
+ ch.key = key;
143
+ return;
144
+ }
145
+ if (reg >= 0xc0 && reg <= 0xc8) {
146
+ const ch = this.chans[base + reg - 0xc0];
147
+ ch.fb = (v >> 1) & 7; ch.cnt = v & 1;
148
+ ch.left = (v & 0x10) !== 0; ch.right = (v & 0x20) !== 0;
149
+ }
150
+ }
151
+
152
+ slDb(op) { return op.sl === 15 ? 93 : op.sl * 3; }
153
+
154
+ effRate(ch, op, r) {
155
+ if (r === 0) return 0;
156
+ const rof = (ch.block << 1) | ((ch.fnum >> (this.nts ? 8 : 9)) & 1);
157
+ return Math.min(63, r * 4 + (op.ksr ? rof : rof >> 2));
158
+ }
159
+
160
+ /**
161
+ * Mix `n` stereo frames into out[off..] (added, not replaced). Registers do not change inside a
162
+ * call -- the machine writes them between calls -- so everything that depends only on them (a
163
+ * channel's frequency, an operator's level and envelope rates) is worked out once per call, and
164
+ * the per-sample loop is phase, envelope and a table read.
165
+ */
166
+ render(out, off, n) {
167
+ const dt = 1 / this.rate;
168
+ const waveMask = this.newMode ? 7 : this.wse ? 3 : 0;
169
+ if (this.trem.length < n) { this.trem = new Float32Array(n); this.vib = new Float32Array(n); }
170
+ const trem = this.trem, vib = this.vib;
171
+ const vibScale = this.vibDepth * Math.LN2 / 1200; // cents to a frequency ratio, to first order
172
+ for (let i = 0; i < n; i++) {
173
+ this.lfoAm += 3.7 * dt; if (this.lfoAm >= 1) this.lfoAm -= 1;
174
+ this.lfoVib += 6.1 * dt; if (this.lfoVib >= 1) this.lfoVib -= 1;
175
+ trem[i] = LFO_TREM[(this.lfoAm * 256) | 0] * this.amDepth;
176
+ vib[i] = 1 + WAVES[0][(this.lfoVib * WAVE_LEN) | 0] * vibScale;
177
+ }
178
+ for (let c = 0; c < 18; c++) {
179
+ const ch = this.chans[c];
180
+ const m = ch.ops[0], k = ch.ops[1];
181
+ if (m.stage === EG_OFF && k.stage === EG_OFF) continue;
182
+ if (c >= 9 && !this.newMode) continue;
183
+ const cycles = ch.fnum * (1 << ch.block) * OPL_RATE / 1048576 * dt; // one cycle a sample = 1
184
+ const kslDb = Math.max(0, KSL_TABLE[ch.fnum >> 6] - 6 * (7 - ch.block));
185
+ this.prepare(ch, m, cycles, kslDb, dt, waveMask);
186
+ this.prepare(ch, k, cycles, kslDb, dt, waveMask);
187
+ const fb = ch.fb ? Math.pow(2, ch.fb - 6) * 0.5 : 0;
188
+ const gl = (!this.newMode || ch.left) ? 0.11 : 0, gr = (!this.newMode || ch.right) ? 0.11 : 0;
189
+ for (let i = 0; i < n; i++) {
190
+ // modulator, with feedback from its own last two outputs
191
+ const mo = this.sample(ch, m, fb ? (m.out + m.prev) * fb : 0, trem[i], vib[i]);
192
+ m.prev = m.out; m.out = mo;
193
+ const s = ch.cnt === 0 ? this.sample(ch, k, mo * 4, trem[i], vib[i]) : mo + this.sample(ch, k, 0, trem[i], vib[i]);
194
+ out[off + i * 2] += s * gl;
195
+ out[off + i * 2 + 1] += s * gr;
196
+ }
197
+ }
198
+ }
199
+
200
+ // the per-call constants of one operator, stored on it
201
+ prepare(ch, op, cycles, kslDb, dt, waveMask) {
202
+ const at = oplRateTimes(this.effRate(ch, op, op.ar)).attack;
203
+ op.attackK = at === 0 ? -1 : at === Infinity ? 0 : (dt / at) * 3.22;
204
+ const dtime = oplRateTimes(this.effRate(ch, op, op.dr)).decay;
205
+ op.decayInc = dtime === Infinity ? 0 : MAX_DB * dt / dtime;
206
+ const rtime = oplRateTimes(this.effRate(ch, op, op.rr)).decay;
207
+ op.releaseInc = rtime === Infinity ? 0 : MAX_DB * dt / rtime;
208
+ op.slLevel = this.slDb(op);
209
+ op.levelDb = op.tl * 0.75 + kslDb * KSL_SCALE[op.ksl];
210
+ op.inc = cycles * op.mult;
211
+ op.table = WAVES[op.wave & waveMask];
212
+ }
213
+
214
+ sample(ch, op, pm, trem, vib) {
215
+ // envelope, in dB of attenuation
216
+ switch (op.stage) {
217
+ case EG_ATTACK:
218
+ // the chip's attack is exponential in dB: fast at first, easing into full level
219
+ if (op.attackK < 0) op.env = 0; else op.env -= (op.env + 4) * op.attackK;
220
+ if (op.env <= 0) { op.env = 0; op.stage = EG_DECAY; }
221
+ break;
222
+ case EG_DECAY:
223
+ op.env += op.decayInc;
224
+ if (op.env >= op.slLevel) { op.env = op.slLevel; op.stage = EG_SUSTAIN; }
225
+ break;
226
+ case EG_SUSTAIN:
227
+ if (!op.egt) op.env += op.releaseInc; // a percussive sound keeps decaying
228
+ break;
229
+ case EG_RELEASE:
230
+ op.env += op.releaseInc;
231
+ break;
232
+ default:
233
+ return 0;
234
+ }
235
+ if (op.env >= MAX_DB) {
236
+ op.env = MAX_DB;
237
+ if (op.stage === EG_RELEASE || !ch.key) op.stage = EG_OFF;
238
+ }
239
+ op.phase += op.vib ? op.inc * vib : op.inc;
240
+ if (op.phase >= 1) op.phase -= Math.floor(op.phase);
241
+ const db = op.env + op.levelDb + (op.am ? trem : 0);
242
+ if (db >= MAX_DB) return 0;
243
+ let p = op.phase + pm;
244
+ p -= Math.floor(p);
245
+ return op.table[(p * WAVE_LEN) | 0] * DB_TO_AMP[(db * DB_STEPS) | 0];
246
+ }
247
+ }
248
+ // register offset (low 5 bits of 0x20..0x95, 0xE0..0xF5) -> operator slot: channel * 2 + (0 mod, 1 car)
249
+ const OP_SLOT = [0, 2, 4, 1, 3, 5, -1, -1, 6, 8, 10, 7, 9, 11, -1, -1, 12, 14, 16, 13, 15, 17, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1];
250
+
251
+ // ------------------------------------------------------------------ the card
252
+ /**
253
+ * A Sound Blaster Pro 2 at `base` (0x220), IRQ `irq`, 8-bit DMA `dma`, with its OPL3 at 0x388 too.
254
+ * `mem` is the machine's memory (DMA reads it). Output is stereo at `rate`.
255
+ */
256
+ export function createSoundCard({ mem, base = 0x220, irq = 7, dma = 1, rate = 48000, ring = 1 << 16 } = {}) {
257
+ const opl = new Opl3(rate);
258
+ const dsp = {
259
+ resetting: false, queue: [], cmd: -1, args: [], need: 0,
260
+ speaker: true, rate: 11025, stereo: false, blockSize: 0x7ff,
261
+ playing: false, auto: false, paused: false, left: 0, exitAuto: false,
262
+ irqPending: false, test: 0, level: 0, lastSample: 0x80, pos: 0,
263
+ irqAt: -1,
264
+ };
265
+ const dmac = {
266
+ addr: new Uint16Array(4), count: new Uint16Array(4), baseAddr: new Uint16Array(4), baseCount: new Uint16Array(4),
267
+ page: new Uint8Array(4), mode: new Uint8Array(4), mask: 0x0f, flip: false, status: 0,
268
+ };
269
+ const mixer = { index: 0, reg: new Uint8Array(256) };
270
+ mixer.reg[0x22] = 0xff; mixer.reg[0x04] = 0xff; mixer.reg[0x26] = 0xff;
271
+ const PAGE_PORT = [0x87, 0x83, 0x81, 0x82];
272
+
273
+ // output ring: interleaved stereo float frames
274
+ const buf = new Float32Array(ring * 2);
275
+ let head = 0, count = 0;
276
+ const scratch = new Float32Array(8192);
277
+ let lastT = -1, outFrac = 0, dspFrac = 0;
278
+ let pendingIrq = null;
279
+
280
+ function resetDsp() {
281
+ dsp.playing = false; dsp.auto = false; dsp.paused = false; dsp.cmd = -1; dsp.need = 0; dsp.args = [];
282
+ dsp.queue = [0xaa]; dsp.stereo = false; dsp.exitAuto = false;
283
+ }
284
+ function command(c) {
285
+ dsp.cmd = c; dsp.args = [];
286
+ const needs = { 0x10: 1, 0x14: 2, 0x16: 2, 0x17: 2, 0x24: 2, 0x40: 1, 0x41: 2, 0x42: 2, 0x48: 2, 0x74: 2, 0x75: 2, 0x76: 2, 0x77: 2, 0x80: 2, 0xe0: 1, 0xe2: 1, 0xe4: 1 };
287
+ if ((c & 0xf0) === 0xb0 || (c & 0xf0) === 0xc0) dsp.need = 3;
288
+ else dsp.need = needs[c] ?? 0;
289
+ if (dsp.need === 0) execute();
290
+ }
291
+ function startBlock(len, auto) {
292
+ dsp.left = len; dsp.auto = auto; dsp.playing = true; dsp.paused = false; dsp.exitAuto = false;
293
+ }
294
+ function execute() {
295
+ const c = dsp.cmd, a = dsp.args;
296
+ dsp.cmd = -1;
297
+ switch (c) {
298
+ case 0x10: dsp.level = a[0]; dsp.lastSample = a[0]; return;
299
+ case 0x14: case 0x16: case 0x17: case 0x91: startBlock(c === 0x91 ? dsp.blockSize + 1 : (a[0] | (a[1] << 8)) + 1, false); return;
300
+ case 0x1c: case 0x90: startBlock(dsp.blockSize + 1, true); return;
301
+ case 0x20: dsp.queue.push(0x80); return;
302
+ case 0x40: dsp.rate = Math.round(1000000 / (256 - a[0])); return;
303
+ case 0x41: case 0x42: dsp.rate = (a[0] << 8) | a[1]; return;
304
+ case 0x48: dsp.blockSize = a[0] | (a[1] << 8); return;
305
+ case 0x80: dsp.silence = (a[0] | (a[1] << 8)) + 1; startBlock(dsp.silence, false); dsp.silent = true; return;
306
+ case 0xa0: dsp.stereo = false; return;
307
+ case 0xa8: dsp.stereo = true; return;
308
+ case 0xd0: dsp.paused = true; return;
309
+ case 0xd1: dsp.speaker = true; return;
310
+ case 0xd3: dsp.speaker = false; return;
311
+ case 0xd4: dsp.paused = false; return;
312
+ case 0xd8: dsp.queue.push(dsp.speaker ? 0xff : 0x00); return;
313
+ case 0xda: dsp.exitAuto = true; return;
314
+ case 0xe0: dsp.queue.push(~a[0] & 0xff); return;
315
+ case 0xe1: dsp.queue.push(3, 2); return;
316
+ case 0xe3: for (const ch of 'COPYRIGHT (C) CREATIVE TECHNOLOGY LTD, 1992.\0') dsp.queue.push(ch.charCodeAt(0)); return;
317
+ case 0xe4: dsp.test = a[0]; return;
318
+ case 0xe8: dsp.queue.push(dsp.test); return;
319
+ case 0xf2: case 0xf3: pendingIrq = 'soon'; return;
320
+ case 0xf8: dsp.queue.push(0); return;
321
+ }
322
+ if ((c & 0xf0) === 0xb0 || (c & 0xf0) === 0xc0) {
323
+ // SB16 transfers: mode byte then a length; the 8-bit ones are the ones this card honours
324
+ startBlock((a[1] | (a[2] << 8)) + 1, (c & 0x04) !== 0);
325
+ dsp.stereo = (a[0] & 0x20) !== 0;
326
+ }
327
+ }
328
+
329
+ // one sample off DMA: returns 0..255, or -1 when the channel has nothing
330
+ function dmaByte() {
331
+ const ch = dma & 3;
332
+ if (dmac.mask & (1 << ch)) return -1;
333
+ const addr = (dmac.page[ch] << 16) | dmac.addr[ch];
334
+ const v = mem[addr];
335
+ dmac.addr[ch] = (dmac.addr[ch] + ((dmac.mode[ch] & 0x20) ? -1 : 1)) & 0xffff;
336
+ dmac.count[ch] = (dmac.count[ch] - 1) & 0xffff;
337
+ if (dmac.count[ch] === 0xffff) {
338
+ dmac.status |= 1 << ch;
339
+ if (dmac.mode[ch] & 0x10) { dmac.addr[ch] = dmac.baseAddr[ch]; dmac.count[ch] = dmac.baseCount[ch]; }
340
+ else dmac.mask |= 1 << ch;
341
+ }
342
+ return v;
343
+ }
344
+
345
+ function portIn(port) {
346
+ if (port === 0x388 || port === 0x38a || port === base + 8 || port === base) return opl.readStatus();
347
+ if (port === base + 0x0a) { if (dsp.queue.length) dsp.last = dsp.queue.shift(); return dsp.last ?? 0xaa; }
348
+ if (port === base + 0x0c) return 0x7f; // ready for a command
349
+ if (port === base + 0x0e) { dsp.irqPending = false; return dsp.queue.length ? 0xff : 0x7f; }
350
+ if (port === base + 0x04) return mixer.index;
351
+ if (port === base + 0x05) return mixer.reg[mixer.index];
352
+ if (port === base + 0x06) return 0xff;
353
+ if (port <= 0x07) {
354
+ const ch = port >> 1, isCount = port & 1;
355
+ const v = isCount ? dmac.count[ch] : dmac.addr[ch];
356
+ const r = dmac.flip ? v >> 8 : v & 0xff;
357
+ dmac.flip = !dmac.flip;
358
+ return r;
359
+ }
360
+ if (port === 0x08) { const s = dmac.status; dmac.status &= 0xf0; return s; }
361
+ return undefined;
362
+ }
363
+
364
+ function portOut(port, v) {
365
+ if (port === 0x388 || port === base + 8 || port === base) { opl.index[0] = v; return true; }
366
+ if (port === 0x389 || port === base + 9 || port === base + 1) { opl.write(0, opl.index[0], v); return true; }
367
+ if (port === 0x38a || port === base + 2) { opl.index[1] = v; return true; }
368
+ if (port === 0x38b || port === base + 3) { opl.write(1, opl.index[1], v); return true; }
369
+ if (port === base + 0x06) {
370
+ if (v & 1) { dsp.resetting = true; return true; }
371
+ if (dsp.resetting) { dsp.resetting = false; resetDsp(); }
372
+ return true;
373
+ }
374
+ if (port === base + 0x0c) {
375
+ if (dsp.cmd >= 0) { dsp.args.push(v); if (dsp.args.length >= dsp.need) execute(); }
376
+ else command(v);
377
+ return true;
378
+ }
379
+ if (port === base + 0x04) { mixer.index = v; return true; }
380
+ if (port === base + 0x05) { mixer.reg[mixer.index] = v; if (mixer.index === 0x00) mixer.reg.fill(0); return true; }
381
+ if (port <= 0x07) {
382
+ const ch = port >> 1, isCount = port & 1;
383
+ const arr = isCount ? dmac.count : dmac.addr, baseArr = isCount ? dmac.baseCount : dmac.baseAddr;
384
+ arr[ch] = dmac.flip ? (arr[ch] & 0xff) | (v << 8) : (arr[ch] & 0xff00) | v;
385
+ baseArr[ch] = arr[ch];
386
+ dmac.flip = !dmac.flip;
387
+ return true;
388
+ }
389
+ if (port === 0x0a) { if (v & 4) dmac.mask |= 1 << (v & 3); else dmac.mask &= ~(1 << (v & 3)); return true; }
390
+ if (port === 0x0b) { dmac.mode[v & 3] = v; return true; }
391
+ if (port === 0x0c) { dmac.flip = false; return true; }
392
+ if (port === 0x0f) { dmac.mask = v & 0x0f; return true; }
393
+ const pi = PAGE_PORT.indexOf(port);
394
+ if (pi >= 0) { dmac.page[pi] = v; return true; }
395
+ return false;
396
+ }
397
+
398
+ function push(l, r) {
399
+ if (count === ring) { head = (head + 1) % ring; count--; } // overrun: drop the oldest
400
+ const at = (head + count) % ring;
401
+ buf[at * 2] = l; buf[at * 2 + 1] = r;
402
+ count++;
403
+ }
404
+
405
+ /** Advance the card to machine time `t` (ms), raising its interrupt through `raiseIrq`. */
406
+ function tick(t, raiseIrq) {
407
+ if (pendingIrq === 'soon') { pendingIrq = null; dsp.irqPending = true; raiseIrq(irq); }
408
+ if (lastT < 0) { lastT = t; return; }
409
+ // the machine calls this every few thousand instructions -- a sample or two each time. Work in
410
+ // batches of at least 128 frames (under 3 ms): a DSP block is thousands of samples, so its
411
+ // interrupt is no later for it, and the synthesiser's per-call set-up is paid 300 times a
412
+ // second instead of 40,000
413
+ if ((t - lastT) * rate / 1000 + outFrac < 128) return;
414
+ let ms = t - lastT;
415
+ lastT = t;
416
+ if (ms <= 0) return;
417
+ if (ms > 200) ms = 200; // a stall is not replayed as a burst
418
+ outFrac += ms * rate / 1000;
419
+ let n = Math.floor(outFrac);
420
+ outFrac -= n;
421
+ const step = dsp.rate / rate / (dsp.stereo ? 2 : 1);
422
+ let off = 0;
423
+ while (n > 0) {
424
+ const k = Math.min(n, scratch.length >> 1);
425
+ scratch.fill(0, 0, k * 2);
426
+ opl.render(scratch, 0, k);
427
+ for (let i = 0; i < k; i++) {
428
+ let s = 0;
429
+ if (dsp.playing && !dsp.paused) {
430
+ dspFrac += step;
431
+ while (dspFrac >= 1 && dsp.playing) {
432
+ dspFrac -= 1;
433
+ if (dsp.silent) dsp.lastSample = 0x80;
434
+ else { const b = dmaByte(); if (b >= 0) dsp.lastSample = b; }
435
+ if (--dsp.left <= 0) {
436
+ dsp.irqPending = true; raiseIrq(irq);
437
+ if (dsp.auto && !dsp.exitAuto) dsp.left = dsp.blockSize + 1;
438
+ else { dsp.playing = false; dsp.silent = false; dsp.lastSample = 0x80; }
439
+ }
440
+ }
441
+ }
442
+ if (dsp.speaker) s = (dsp.lastSample - 128) / 128 * 0.9;
443
+ push(scratch[i * 2] + s, scratch[i * 2 + 1] + s);
444
+ }
445
+ n -= k; off += k;
446
+ }
447
+ }
448
+
449
+ /** Take up to `max` frames of output (interleaved stereo), or all there is. */
450
+ function drain(max = count) {
451
+ const n = Math.min(max, count);
452
+ const out = new Float32Array(n * 2);
453
+ for (let i = 0; i < n; i++) { const at = (head + i) % ring; out[i * 2] = buf[at * 2]; out[i * 2 + 1] = buf[at * 2 + 1]; }
454
+ head = (head + n) % ring; count -= n;
455
+ return out;
456
+ }
457
+
458
+ return { opl, dsp, dmac, mixer, portIn, portOut, tick, drain, get buffered() { return count; }, rate };
459
+ }