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.
- package/CHANGELOG.md +251 -1
- package/README.md +42 -23
- package/bin/blockyard.js +2 -1
- package/docs/API.md +16 -14
- package/docs/ARCHITECTURE.md +92 -5
- package/docs/CONFIGURATION.md +33 -26
- package/docs/GETTING-STARTED.md +5 -2
- package/docs/INSTALL.md +90 -33
- package/docs/MEASUREMENTS.md +147 -0
- package/docs/SECURITY.md +32 -15
- package/docs/TROUBLESHOOTING.md +35 -1
- package/docs/USER-GUIDE.md +266 -26
- package/package.json +1 -1
- package/public/404.html +1 -1
- package/public/css/app.css +306 -82
- package/public/donate-qr.png +0 -0
- package/public/index.html +295 -103
- package/public/js/agents.js +228 -51
- package/public/js/app.js +82 -8
- package/public/js/blockscene3d.js +179 -27
- package/public/js/charts.js +21 -21
- package/public/js/depthchart.js +31 -27
- package/public/js/details3d.js +1456 -71
- package/public/js/doom.js +31 -0
- package/public/js/dosaudio.js +48 -0
- package/public/js/dosgame.js +389 -0
- package/public/js/dosio.js +186 -0
- package/public/js/dospc.js +1353 -0
- package/public/js/dosworker.js +196 -0
- package/public/js/login.js +5 -0
- package/public/js/markets.js +46 -8
- package/public/js/mining.js +310 -32
- package/public/js/panels.js +14 -10
- package/public/js/pricechart.js +14 -13
- package/public/js/quake.js +20 -0
- package/public/js/settings.js +103 -21
- package/public/js/soundcard.js +459 -0
- package/public/js/theme.js +235 -0
- package/public/js/wolf3d.js +22 -0
- package/public/js/x86.js +1978 -0
- package/scripts/donate-qr.py +12 -9
- package/scripts/dos-bench.js +56 -0
- package/scripts/setup.js +34 -12
- package/scripts/shots.mjs +6 -0
- package/scripts/smoke.sh +1 -1
- package/scripts/tls.js +31 -0
- package/server/chain/index/build.js +21 -4
- package/server/collect/monitor.js +30 -1
- package/server/collect/network.js +295 -0
- package/server/config.js +46 -22
- package/server/http/api.js +49 -5
- package/server/http/games.js +77 -0
- package/server/http/server.js +8 -0
- package/server/main.js +53 -8
- package/server/tls/selfsigned.js +160 -0
- package/systemd/blockyard.service +7 -5
- package/docs/PRIVATE-LEADERBOARD.md +0 -230
- 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
|
+
}
|