jsbeeb 1.12.0 → 1.13.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 +16 -2
- package/package.json +8 -8
- package/public/roms/atom/ATMMC3E.rom +0 -0
- package/public/roms/atom/Atom_Basic.rom +0 -0
- package/public/roms/atom/Atom_DOS.rom +0 -0
- package/public/roms/atom/Atom_FloatingPoint.rom +0 -0
- package/public/roms/atom/Atom_Kernel.rom +0 -0
- package/public/roms/atom/Atom_Kernel_E.rom +0 -0
- package/public/roms/atom/PCHARME.ROM +0 -0
- package/public/roms/atom/gags.rom +0 -0
- package/public/roms/atom/werom.rom +0 -0
- package/src/6502.js +344 -44
- package/src/6847.js +724 -0
- package/src/6847_fontdata.js +124 -0
- package/src/disc.js +2 -20
- package/src/fake6502.js +3 -2
- package/src/jsbeeb.css +23 -0
- package/src/keyboard.js +45 -23
- package/src/machine-session.js +85 -59
- package/src/main.js +142 -41
- package/src/mmc.js +1053 -0
- package/src/models.js +42 -1
- package/src/ppia.js +477 -0
- package/src/soundchip.js +99 -1
- package/src/tapes.js +73 -16
- package/src/url-params.js +7 -2
- package/src/utils.js +74 -1
- package/src/utils_atom.js +508 -0
- package/src/video.js +12 -1
- package/src/web/audio-handler.js +8 -3
- package/tests/test-machine.js +133 -8
package/src/tapes.js
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
import * as utils from "./utils.js";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
const BbcCpuSpeed = 2 * 1000 * 1000;
|
|
5
|
+
const AtomCpuSpeed = 1 * 1000 * 1000;
|
|
6
|
+
|
|
7
|
+
function secsToClocks(secs, cpuSpeed) {
|
|
8
|
+
return (cpuSpeed * secs) | 0;
|
|
6
9
|
}
|
|
7
10
|
|
|
11
|
+
// Atom tape encoding: wavebits sent one per poll via receiveBit().
|
|
12
|
+
// '0' bit = 4 cycles at 1200 Hz (toggle every 2 wavebits).
|
|
13
|
+
// '1' bit = 8 cycles at 2400 Hz (toggle every 1 wavebit).
|
|
14
|
+
// Carrier = continuous '1' bits.
|
|
15
|
+
|
|
8
16
|
function parityOf(curByte) {
|
|
9
17
|
let parity = false;
|
|
10
18
|
while (curByte) {
|
|
@@ -17,9 +25,11 @@ function parityOf(curByte) {
|
|
|
17
25
|
const ParityN = "N".charCodeAt(0);
|
|
18
26
|
|
|
19
27
|
class UefTape {
|
|
20
|
-
constructor(stream) {
|
|
28
|
+
constructor(stream, isAtom = false) {
|
|
21
29
|
this.stream = stream;
|
|
22
30
|
this.baseFrequency = 1200;
|
|
31
|
+
this.isAtom = isAtom;
|
|
32
|
+
this.cpuSpeed = isAtom ? AtomCpuSpeed : BbcCpuSpeed;
|
|
23
33
|
this.rewind();
|
|
24
34
|
|
|
25
35
|
this.curChunk = this.readChunk();
|
|
@@ -36,6 +46,11 @@ class UefTape {
|
|
|
36
46
|
this.numStopBits = 1;
|
|
37
47
|
this.carrierBefore = 0;
|
|
38
48
|
this.carrierAfter = 0;
|
|
49
|
+
this.shortWave = 0;
|
|
50
|
+
this.atomPhase = 0;
|
|
51
|
+
this.atomSubCount = 0;
|
|
52
|
+
this.atomWavebitsLeft = 0;
|
|
53
|
+
this.atomToggleEvery = 1;
|
|
39
54
|
|
|
40
55
|
this.stream.seek(10);
|
|
41
56
|
const minor = this.stream.readByte();
|
|
@@ -52,8 +67,22 @@ class UefTape {
|
|
|
52
67
|
};
|
|
53
68
|
}
|
|
54
69
|
|
|
70
|
+
// On BBC, acia is the ACIA (6850); on Atom, it's the PPIA (8255).
|
|
71
|
+
// Both provide setTapeCarrier(), tone(), and receive/receiveBit().
|
|
55
72
|
poll(acia) {
|
|
56
73
|
if (!this.curChunk) return;
|
|
74
|
+
|
|
75
|
+
// Atom: deliver one wavebit per poll.
|
|
76
|
+
if (this.isAtom && this.atomWavebitsLeft > 0) {
|
|
77
|
+
if (++this.atomSubCount >= this.atomToggleEvery) {
|
|
78
|
+
this.atomPhase ^= 1;
|
|
79
|
+
this.atomSubCount = 0;
|
|
80
|
+
}
|
|
81
|
+
acia.receiveBit(this.atomPhase);
|
|
82
|
+
this.atomWavebitsLeft--;
|
|
83
|
+
return secsToClocks(0.25 / this.baseFrequency, this.cpuSpeed);
|
|
84
|
+
}
|
|
85
|
+
|
|
57
86
|
if (this.state === -1) {
|
|
58
87
|
if (this.stream.eof()) {
|
|
59
88
|
this.curChunk = null;
|
|
@@ -77,13 +106,17 @@ class UefTape {
|
|
|
77
106
|
if (this.state === 0) {
|
|
78
107
|
// Start bit
|
|
79
108
|
acia.tone(this.baseFrequency);
|
|
109
|
+
if (this.isAtom) this.queueAtomBit(false);
|
|
80
110
|
} else {
|
|
81
|
-
|
|
111
|
+
const bit = this.curByte & (1 << (this.state - 1));
|
|
112
|
+
acia.tone(bit ? 2 * this.baseFrequency : this.baseFrequency);
|
|
113
|
+
if (this.isAtom) this.queueAtomBit(!!bit);
|
|
82
114
|
}
|
|
83
115
|
this.state++;
|
|
84
116
|
} else {
|
|
85
117
|
acia.receive(this.curByte);
|
|
86
118
|
acia.tone(2 * this.baseFrequency); // Stop bit
|
|
119
|
+
if (this.isAtom) this.queueAtomBit(true);
|
|
87
120
|
if (this.curChunk.stream.eof()) {
|
|
88
121
|
this.state = -1;
|
|
89
122
|
} else {
|
|
@@ -91,6 +124,7 @@ class UefTape {
|
|
|
91
124
|
this.curByte = this.curChunk.stream.readByte();
|
|
92
125
|
}
|
|
93
126
|
}
|
|
127
|
+
if (this.isAtom) return 0; // wavebits queued, drained on next poll
|
|
94
128
|
return this.cycles(1);
|
|
95
129
|
case 0x0104: // Defined data
|
|
96
130
|
acia.setTapeCarrier(false);
|
|
@@ -99,8 +133,14 @@ class UefTape {
|
|
|
99
133
|
this.parity = this.curChunk.stream.readByte();
|
|
100
134
|
this.numStopBits = this.curChunk.stream.readByte();
|
|
101
135
|
this.numParityBits = this.parity !== ParityN ? 1 : 0;
|
|
136
|
+
// Atom: negative stop bits (high bit set) means short wave
|
|
137
|
+
this.shortWave = 0;
|
|
138
|
+
if (this.isAtom && this.numStopBits & 0x80) {
|
|
139
|
+
this.numStopBits = Math.abs(this.numStopBits - 256);
|
|
140
|
+
this.shortWave = 1;
|
|
141
|
+
}
|
|
102
142
|
console.log(
|
|
103
|
-
`Defined data with ${this.numDataBits}${String.fromCharCode(this.parity)}${this.numStopBits}`,
|
|
143
|
+
`Defined data with ${this.numDataBits}${String.fromCharCode(this.parity)}${this.shortWave ? "-" : ""}${this.numStopBits}`,
|
|
104
144
|
);
|
|
105
145
|
this.state = 0;
|
|
106
146
|
}
|
|
@@ -110,24 +150,30 @@ class UefTape {
|
|
|
110
150
|
} else {
|
|
111
151
|
this.curByte = this.curChunk.stream.readByte() & ((1 << this.numDataBits) - 1);
|
|
112
152
|
acia.tone(this.baseFrequency); // Start bit
|
|
153
|
+
if (this.isAtom) this.queueAtomBit(false);
|
|
113
154
|
this.state++;
|
|
114
155
|
}
|
|
115
156
|
} else if (this.state < 1 + this.numDataBits) {
|
|
116
|
-
|
|
157
|
+
const bit = this.curByte & (1 << (this.state - 1));
|
|
158
|
+
acia.tone(bit ? 2 * this.baseFrequency : this.baseFrequency);
|
|
159
|
+
if (this.isAtom) this.queueAtomBit(!!bit);
|
|
117
160
|
this.state++;
|
|
118
161
|
} else if (this.state < 1 + this.numDataBits + this.numParityBits) {
|
|
119
162
|
let bit = parityOf(this.curByte);
|
|
120
163
|
if (this.parity === ParityN) bit = !bit;
|
|
121
164
|
acia.tone(bit ? 2 * this.baseFrequency : this.baseFrequency);
|
|
165
|
+
if (this.isAtom) this.queueAtomBit(!!bit);
|
|
122
166
|
this.state++;
|
|
123
167
|
} else if (this.state < 1 + this.numDataBits + this.numParityBits + this.numStopBits) {
|
|
124
168
|
acia.tone(2 * this.baseFrequency); // Stop bits
|
|
169
|
+
if (this.isAtom) this.queueAtomBit(true);
|
|
125
170
|
this.state++;
|
|
126
171
|
} else {
|
|
127
172
|
acia.receive(this.curByte);
|
|
128
173
|
this.state = 0;
|
|
129
174
|
return 0;
|
|
130
175
|
}
|
|
176
|
+
if (this.isAtom) return 0;
|
|
131
177
|
return this.cycles(1);
|
|
132
178
|
case 0x0111: // Carrier tone with dummy data
|
|
133
179
|
if (this.state === -1) {
|
|
@@ -139,11 +185,13 @@ class UefTape {
|
|
|
139
185
|
if (this.state === 0) {
|
|
140
186
|
acia.setTapeCarrier(true);
|
|
141
187
|
acia.tone(2 * this.baseFrequency);
|
|
188
|
+
if (this.isAtom) this.queueAtomBit(true);
|
|
142
189
|
this.carrierBefore--;
|
|
143
190
|
if (this.carrierBefore <= 0) this.state = 1;
|
|
144
191
|
} else if (this.state < 11) {
|
|
145
192
|
acia.setTapeCarrier(false);
|
|
146
193
|
acia.tone(this.dummyData[this.state - 1] ? this.baseFrequency : 2 * this.baseFrequency);
|
|
194
|
+
if (this.isAtom) this.queueAtomBit(!this.dummyData[this.state - 1]);
|
|
147
195
|
if (this.state === 10) {
|
|
148
196
|
acia.receive(0xaa);
|
|
149
197
|
}
|
|
@@ -151,9 +199,11 @@ class UefTape {
|
|
|
151
199
|
} else {
|
|
152
200
|
acia.setTapeCarrier(true);
|
|
153
201
|
acia.tone(2 * this.baseFrequency);
|
|
202
|
+
if (this.isAtom) this.queueAtomBit(true);
|
|
154
203
|
this.carrierAfter--;
|
|
155
204
|
if (this.carrierAfter <= 0) this.state = -1;
|
|
156
205
|
}
|
|
206
|
+
if (this.isAtom) return 0;
|
|
157
207
|
return this.cycles(1);
|
|
158
208
|
case 0x0114:
|
|
159
209
|
console.log("Ignoring security cycles");
|
|
@@ -165,11 +215,16 @@ class UefTape {
|
|
|
165
215
|
if (this.state === -1) {
|
|
166
216
|
this.state = 0;
|
|
167
217
|
this.count = this.curChunk.stream.readInt16();
|
|
218
|
+
// Each Atom carrier cycle expands to 16 wavebits, so
|
|
219
|
+
// divide the count to avoid 16x too many cycles.
|
|
220
|
+
if (this.isAtom) this.count = Math.max(1, (this.count / 16) | 0);
|
|
168
221
|
}
|
|
169
222
|
acia.setTapeCarrier(true);
|
|
170
223
|
acia.tone(2 * this.baseFrequency);
|
|
224
|
+
if (this.isAtom) this.queueAtomBit(true);
|
|
171
225
|
this.count--;
|
|
172
226
|
if (this.count <= 0) this.state = -1;
|
|
227
|
+
if (this.isAtom) return 0;
|
|
173
228
|
return this.cycles(1);
|
|
174
229
|
case 0x0113:
|
|
175
230
|
this.baseFrequency = this.curChunk.stream.readFloat32();
|
|
@@ -180,13 +235,13 @@ class UefTape {
|
|
|
180
235
|
gap = 1 / (2 * this.curChunk.stream.readInt16() * this.baseFrequency);
|
|
181
236
|
console.log("Tape gap of " + gap + "s");
|
|
182
237
|
acia.tone(0);
|
|
183
|
-
return secsToClocks(gap);
|
|
238
|
+
return secsToClocks(gap, this.cpuSpeed);
|
|
184
239
|
case 0x0116:
|
|
185
240
|
acia.setTapeCarrier(false);
|
|
186
241
|
gap = this.curChunk.stream.readFloat32();
|
|
187
242
|
console.log("Tape gap of " + gap + "s");
|
|
188
243
|
acia.tone(0);
|
|
189
|
-
return secsToClocks(gap);
|
|
244
|
+
return secsToClocks(gap, this.cpuSpeed);
|
|
190
245
|
default:
|
|
191
246
|
console.log("Skipping unknown chunk " + utils.hexword(this.curChunk.id));
|
|
192
247
|
this.curChunk = this.readChunk();
|
|
@@ -195,8 +250,15 @@ class UefTape {
|
|
|
195
250
|
return this.cycles(1);
|
|
196
251
|
}
|
|
197
252
|
|
|
253
|
+
// Queue 16 wavebits for an Atom tape bit. atomPhase and atomSubCount
|
|
254
|
+
// carry across calls so bit boundaries don't break carrier detection.
|
|
255
|
+
queueAtomBit(isOne) {
|
|
256
|
+
this.atomWavebitsLeft = 16;
|
|
257
|
+
this.atomToggleEvery = isOne ? 1 : 2;
|
|
258
|
+
}
|
|
259
|
+
|
|
198
260
|
cycles(count) {
|
|
199
|
-
return secsToClocks(count / this.baseFrequency);
|
|
261
|
+
return secsToClocks(count / this.baseFrequency, this.cpuSpeed);
|
|
200
262
|
}
|
|
201
263
|
}
|
|
202
264
|
|
|
@@ -245,7 +307,7 @@ class TapefileTape {
|
|
|
245
307
|
}
|
|
246
308
|
}
|
|
247
309
|
|
|
248
|
-
export async function loadTapeFromData(name, data) {
|
|
310
|
+
export async function loadTapeFromData(name, data, isAtom = false) {
|
|
249
311
|
const stream = await utils.DataStream.create(name, data);
|
|
250
312
|
if (stream.readByte(0) === 0xff && stream.readByte(1) === 0x04) {
|
|
251
313
|
console.log("Detected a 'tapefile' tape");
|
|
@@ -253,13 +315,8 @@ export async function loadTapeFromData(name, data) {
|
|
|
253
315
|
}
|
|
254
316
|
if (stream.readNulString(0) === "UEF File!") {
|
|
255
317
|
console.log("Detected a UEF tape");
|
|
256
|
-
return new UefTape(stream);
|
|
318
|
+
return new UefTape(stream, isAtom);
|
|
257
319
|
}
|
|
258
320
|
console.log("Unknown tape format");
|
|
259
321
|
return null;
|
|
260
322
|
}
|
|
261
|
-
|
|
262
|
-
export async function loadTape(name) {
|
|
263
|
-
console.log("Loading tape from " + name);
|
|
264
|
-
return loadTapeFromData(name, await utils.loadData(name));
|
|
265
|
-
}
|
package/src/url-params.js
CHANGED
|
@@ -240,6 +240,7 @@ export function processAutobootParams(parsedQuery) {
|
|
|
240
240
|
export function guessModelFromHostname(hostname) {
|
|
241
241
|
if (hostname.startsWith("bbc")) return "B-DFS1.2";
|
|
242
242
|
if (hostname.startsWith("master")) return "Master";
|
|
243
|
+
if (hostname.startsWith("atom")) return "Atom";
|
|
243
244
|
return "B-DFS1.2";
|
|
244
245
|
}
|
|
245
246
|
|
|
@@ -247,10 +248,14 @@ export function guessModelFromHostname(hostname) {
|
|
|
247
248
|
* Parse disc or tape images from the query parameters
|
|
248
249
|
* @param {Object} parsedQuery - The query parameters
|
|
249
250
|
* @returns {Object} Object containing disc and tape information
|
|
251
|
+
* - discImage: disc image URL (?disc= or ?disc1=)
|
|
252
|
+
* - secondDiscImage: second disc URL (?disc2=)
|
|
253
|
+
* - tapeImage: tape image URL (?tape=)
|
|
254
|
+
* - mmcImage: MMC/SD card image URL (?mmc=, Atom only)
|
|
250
255
|
*/
|
|
251
256
|
export function parseMediaParams(parsedQuery) {
|
|
252
|
-
const { disc, disc1, disc2, tape } = parsedQuery;
|
|
257
|
+
const { disc, disc1, disc2, tape, mmc } = parsedQuery;
|
|
253
258
|
const discImage = disc || disc1;
|
|
254
259
|
|
|
255
|
-
return { discImage, secondDiscImage: disc2, tapeImage: tape };
|
|
260
|
+
return { discImage, secondDiscImage: disc2, tapeImage: tape, mmcImage: mmc };
|
|
256
261
|
}
|
package/src/utils.js
CHANGED
|
@@ -77,7 +77,7 @@ export async function decompress(data, format) {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
// Extract all files from a ZIP archive. Returns {filename: Uint8Array}.
|
|
80
|
-
async function unzip(buf) {
|
|
80
|
+
export async function unzip(buf) {
|
|
81
81
|
if (!(buf instanceof Uint8Array)) buf = new Uint8Array(buf);
|
|
82
82
|
const eocdOff = findEocd(buf);
|
|
83
83
|
const cdOff = readU32(buf, eocdOff + 16);
|
|
@@ -120,6 +120,79 @@ async function unzip(buf) {
|
|
|
120
120
|
return files;
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
// Standard CRC-32/ISO-HDLC.
|
|
124
|
+
export function crc32(data) {
|
|
125
|
+
let crc = 0xffffffff;
|
|
126
|
+
for (let i = 0; i < data.length; ++i) {
|
|
127
|
+
crc ^= data[i];
|
|
128
|
+
for (let j = 0; j < 8; ++j) {
|
|
129
|
+
const doEor = crc & 1;
|
|
130
|
+
crc = crc >>> 1;
|
|
131
|
+
if (doEor) crc ^= 0xedb88320;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return ~crc;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Create a ZIP blob from an array of {name: string, data: Uint8Array} entries.
|
|
138
|
+
// Uses stored method (no compression) with CRC-32 for maximum compatibility.
|
|
139
|
+
export function createZipBlob(files) {
|
|
140
|
+
const encoder = new TextEncoder();
|
|
141
|
+
const localHeaders = [];
|
|
142
|
+
const centralHeaders = [];
|
|
143
|
+
let offset = 0;
|
|
144
|
+
|
|
145
|
+
for (const { name, data } of files) {
|
|
146
|
+
const nameBytes = encoder.encode(name);
|
|
147
|
+
const crc = crc32(data);
|
|
148
|
+
// Local file header (30 + nameLen + data)
|
|
149
|
+
const local = new Uint8Array(30 + nameBytes.length + data.length);
|
|
150
|
+
const lv = new DataView(local.buffer);
|
|
151
|
+
lv.setUint32(0, 0x04034b50, true); // signature
|
|
152
|
+
lv.setUint16(4, 20, true); // version needed
|
|
153
|
+
lv.setUint16(8, 0, true); // method: stored
|
|
154
|
+
lv.setUint32(14, crc, true); // CRC-32
|
|
155
|
+
lv.setUint32(18, data.length, true); // compressed size
|
|
156
|
+
lv.setUint32(22, data.length, true); // uncompressed size
|
|
157
|
+
lv.setUint16(26, nameBytes.length, true); // name length
|
|
158
|
+
local.set(nameBytes, 30);
|
|
159
|
+
local.set(data, 30 + nameBytes.length);
|
|
160
|
+
localHeaders.push(local);
|
|
161
|
+
|
|
162
|
+
// Central directory entry (46 + nameLen)
|
|
163
|
+
const central = new Uint8Array(46 + nameBytes.length);
|
|
164
|
+
const cv = new DataView(central.buffer);
|
|
165
|
+
cv.setUint32(0, 0x02014b50, true); // signature
|
|
166
|
+
cv.setUint16(4, 20, true); // version made by
|
|
167
|
+
cv.setUint16(6, 20, true); // version needed
|
|
168
|
+
cv.setUint16(10, 0, true); // method: stored
|
|
169
|
+
cv.setUint32(16, crc, true); // CRC-32
|
|
170
|
+
cv.setUint32(20, data.length, true); // compressed size
|
|
171
|
+
cv.setUint32(24, data.length, true); // uncompressed size
|
|
172
|
+
cv.setUint16(28, nameBytes.length, true); // name length
|
|
173
|
+
cv.setUint32(42, offset, true); // local header offset
|
|
174
|
+
central.set(nameBytes, 46);
|
|
175
|
+
centralHeaders.push(central);
|
|
176
|
+
|
|
177
|
+
offset += local.length;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const cdOffset = offset;
|
|
181
|
+
let cdSize = 0;
|
|
182
|
+
for (const c of centralHeaders) cdSize += c.length;
|
|
183
|
+
|
|
184
|
+
// End of central directory (22 bytes)
|
|
185
|
+
const eocd = new Uint8Array(22);
|
|
186
|
+
const ev = new DataView(eocd.buffer);
|
|
187
|
+
ev.setUint32(0, 0x06054b50, true); // signature
|
|
188
|
+
ev.setUint16(8, files.length, true); // entries on this disc
|
|
189
|
+
ev.setUint16(10, files.length, true); // total entries
|
|
190
|
+
ev.setUint32(12, cdSize, true); // central directory size
|
|
191
|
+
ev.setUint32(16, cdOffset, true); // central directory offset
|
|
192
|
+
|
|
193
|
+
return new Blob([...localHeaders, ...centralHeaders, eocd], { type: "application/zip" });
|
|
194
|
+
}
|
|
195
|
+
|
|
123
196
|
export function debounce(fn, wait) {
|
|
124
197
|
let timeout;
|
|
125
198
|
return function (...args) {
|