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/src/tapes.js CHANGED
@@ -1,10 +1,18 @@
1
1
  "use strict";
2
2
  import * as utils from "./utils.js";
3
3
 
4
- function secsToClocks(secs) {
5
- return (2 * 1000 * 1000 * secs) | 0;
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
- acia.tone(this.curByte & (1 << (this.state - 1)) ? 2 * this.baseFrequency : this.baseFrequency);
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
- acia.tone(this.curByte & (1 << (this.state - 1)) ? 2 * this.baseFrequency : this.baseFrequency);
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) {