jsbeeb 1.11.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.
@@ -0,0 +1,208 @@
1
+ "use strict";
2
+
3
+ // Shared helpers for snapshot importers (bem-snapshot.js, uef-snapshot.js).
4
+ // Contains volume table, default peripheral state, video state builder,
5
+ // and the common snapshot envelope that wraps per-format parsed state.
6
+
7
+ // Volume lookup table matching soundchip.js
8
+ export const volumeTable = new Float32Array(16);
9
+ (() => {
10
+ let f = 1.0;
11
+ for (let i = 0; i < 15; ++i) {
12
+ volumeTable[i] = f / 4;
13
+ f *= Math.pow(10, -0.1);
14
+ }
15
+ volumeTable[15] = 0;
16
+ })();
17
+
18
+ export const DefaultAcia = {
19
+ sr: 0x02,
20
+ cr: 0x00,
21
+ dr: 0x00,
22
+ rs423Selected: false,
23
+ motorOn: false,
24
+ tapeCarrierCount: 0,
25
+ tapeDcdLineLevel: false,
26
+ hadDcdHigh: false,
27
+ serialReceiveRate: 19200,
28
+ serialReceiveCyclesPerByte: 0,
29
+ txCompleteTaskOffset: null,
30
+ runTapeTaskOffset: null,
31
+ runRs423TaskOffset: null,
32
+ };
33
+
34
+ export const DefaultAdc = { status: 0x40, low: 0x00, high: 0x00, taskOffset: null };
35
+
36
+ /**
37
+ * Build jsbeeb video state from parsed CRTC, ULA, and palette data.
38
+ * @param {number} ulaControl - VideoULA control register
39
+ * @param {Uint8Array} ulaPalette - 16-entry raw ULA palette register values (lower nibble of &FE21 writes)
40
+ * @param {Uint8Array} crtcRegs - CRTC registers (at least 18 bytes)
41
+ * @param {Int32Array|null} [nulaCollook] - NULA colour lookup (16 ABGR entries), or null for default BBC palette
42
+ * @param {object|null} [crtcCounters] - CRTC counter state {hc, vc, sc, ma, maback}, or null for defaults
43
+ */
44
+ export function buildVideoState(ulaControl, ulaPalette, crtcRegs, nulaCollook, crtcCounters) {
45
+ const regs = new Uint8Array(32);
46
+ regs.set(crtcRegs.slice(0, 18));
47
+ const actualPal = new Uint8Array(16);
48
+ for (let i = 0; i < 16; i++) actualPal[i] = ulaPalette[i] & 0x0f;
49
+
50
+ // Use NULA collook if provided, otherwise use default BBC palette
51
+ const collook =
52
+ nulaCollook ||
53
+ new Int32Array([
54
+ 0xff000000, 0xff0000ff, 0xff00ff00, 0xff00ffff, 0xffff0000, 0xffff00ff, 0xffffff00, 0xffffffff, 0xff000000,
55
+ 0xff0000ff, 0xff00ff00, 0xff00ffff, 0xffff0000, 0xffff00ff, 0xffffff00, 0xffffffff,
56
+ ]);
57
+
58
+ // Compute ulaPal from actualPal + collook, matching jsbeeb's Ula._recomputeUlaPal
59
+ const flashEnabled = !!(ulaControl & 1);
60
+ const defaultFlash = new Uint8Array([1, 1, 1, 1, 1, 1, 1, 1]);
61
+ const flash = defaultFlash;
62
+ const ulaPal = new Int32Array(16);
63
+ for (let i = 0; i < 16; i++) {
64
+ const palVal = actualPal[i];
65
+ let colour = collook[(palVal & 0xf) ^ 7];
66
+ if (palVal & 8 && flashEnabled && flash[(palVal & 7) ^ 7]) {
67
+ colour = collook[palVal & 0xf];
68
+ }
69
+ ulaPal[i] = colour;
70
+ }
71
+
72
+ return {
73
+ regs,
74
+ bitmapX: 0,
75
+ bitmapY: 0,
76
+ oddClock: false,
77
+ frameCount: 0,
78
+ doEvenFrameLogic: false,
79
+ isEvenRender: true,
80
+ lastRenderWasEven: false,
81
+ firstScanline: true,
82
+ inHSync: false,
83
+ inVSync: false,
84
+ hadVSyncThisRow: false,
85
+ checkVertAdjust: false,
86
+ endOfMainLatched: false,
87
+ endOfVertAdjustLatched: false,
88
+ endOfFrameLatched: false,
89
+ inVertAdjust: false,
90
+ inDummyRaster: false,
91
+ hpulseWidth: regs[3] & 0x0f,
92
+ vpulseWidth: (regs[3] & 0xf0) >>> 4,
93
+ hpulseCounter: 0,
94
+ vpulseCounter: 0,
95
+ dispEnabled: 0x3f,
96
+ horizCounter: crtcCounters ? crtcCounters.hc : 0,
97
+ vertCounter: crtcCounters ? crtcCounters.vc : 0,
98
+ scanlineCounter: crtcCounters ? crtcCounters.sc : 0,
99
+ vertAdjustCounter: 0,
100
+ addr: crtcCounters ? crtcCounters.ma : (regs[13] | (regs[12] << 8)) & 0x3fff,
101
+ lineStartAddr: crtcCounters ? crtcCounters.maback : (regs[13] | (regs[12] << 8)) & 0x3fff,
102
+ nextLineStartAddr: crtcCounters ? crtcCounters.maback : (regs[13] | (regs[12] << 8)) & 0x3fff,
103
+ ulactrl: ulaControl,
104
+ pixelsPerChar: ulaControl & 0x10 ? 8 : 16,
105
+ halfClock: !(ulaControl & 0x10),
106
+ ulaMode: (ulaControl >>> 2) & 3,
107
+ teletextMode: !!(ulaControl & 2),
108
+ displayEnableSkew: Math.min((regs[8] & 0x30) >>> 4, 2),
109
+ ulaPal,
110
+ actualPal,
111
+ cursorOn: false,
112
+ cursorOff: false,
113
+ cursorOnThisFrame: false,
114
+ cursorDrawIndex: 0,
115
+ cursorPos: (regs[15] | (regs[14] << 8)) & 0x3fff,
116
+ interlacedSyncAndVideo: (regs[8] & 3) === 3,
117
+ screenSubtract: 0,
118
+ ula: {
119
+ collook: collook.slice(),
120
+ flash: new Uint8Array([1, 1, 1, 1, 1, 1, 1, 1]),
121
+ paletteWriteFlag: false,
122
+ paletteFirstByte: 0,
123
+ paletteMode: 0,
124
+ horizontalOffset: 0,
125
+ leftBlank: 0,
126
+ disabled: false,
127
+ attributeMode: 0,
128
+ attributeText: 0,
129
+ },
130
+ crtc: { curReg: 0 },
131
+ teletext: {
132
+ prevCol: 0,
133
+ col: 7,
134
+ bg: 0,
135
+ sep: false,
136
+ dbl: false,
137
+ oldDbl: false,
138
+ secondHalfOfDouble: false,
139
+ wasDbl: false,
140
+ gfx: false,
141
+ flash: false,
142
+ flashOn: false,
143
+ flashTime: 0,
144
+ heldChar: 0,
145
+ holdChar: false,
146
+ dataQueue: [0, 0, 0, 0],
147
+ scanlineCounter: 0,
148
+ levelDEW: false,
149
+ levelDISPTMG: false,
150
+ levelRA0: false,
151
+ nextGlyphs: "normal",
152
+ curGlyphs: "normal",
153
+ heldGlyphs: "normal",
154
+ },
155
+ };
156
+ }
157
+
158
+ /**
159
+ * Build a jsbeeb snapshot envelope from parsed emulator components.
160
+ * @param {string} importedFrom - source identifier (e.g. "b-em", "beebem-uef")
161
+ * @param {string} modelName - jsbeeb model name/synonym
162
+ * @param {object} cpuState - parsed CPU state {a, x, y, flags, s, pc, nmi, fe30, fe34}
163
+ * @param {Uint8Array} ram - 128KB RAM array
164
+ * @param {Uint8Array|null} roms - 256KB sideways ROM/RAM array, or null
165
+ * @param {object} sysvia - jsbeeb sys VIA state
166
+ * @param {object} uservia - jsbeeb user VIA state
167
+ * @param {object} video - jsbeeb video state (from buildVideoState)
168
+ * @param {object} soundChip - jsbeeb sound chip state
169
+ */
170
+ export function buildSnapshot(importedFrom, modelName, cpuState, ram, roms, sysvia, uservia, video, soundChip) {
171
+ return {
172
+ format: "jsbeeb-snapshot",
173
+ version: 2,
174
+ model: modelName,
175
+ timestamp: new Date().toISOString(),
176
+ importedFrom,
177
+ state: {
178
+ a: cpuState.a,
179
+ x: cpuState.x,
180
+ y: cpuState.y,
181
+ s: cpuState.s,
182
+ pc: cpuState.pc,
183
+ p: cpuState.flags | 0x30,
184
+ nmiLevel: !!cpuState.nmi,
185
+ nmiEdge: false,
186
+ halted: false,
187
+ takeInt: false,
188
+ romsel: cpuState.fe30 ?? 0,
189
+ acccon: cpuState.fe34 ?? 0,
190
+ videoDisplayPage: 0,
191
+ currentCycles: 0,
192
+ targetCycles: 0,
193
+ cycleSeconds: 0,
194
+ peripheralCycles: 0,
195
+ videoCycles: 0,
196
+ music5000PageSel: 0,
197
+ ram,
198
+ ...(roms instanceof Uint8Array ? { roms } : {}),
199
+ scheduler: { epoch: 0 },
200
+ sysvia,
201
+ uservia,
202
+ video,
203
+ soundChip,
204
+ acia: { ...DefaultAcia },
205
+ adc: { ...DefaultAdc },
206
+ },
207
+ };
208
+ }
package/src/snapshot.js CHANGED
@@ -7,24 +7,15 @@ const SnapshotFormat = "jsbeeb-snapshot";
7
7
  const SnapshotVersion = 2;
8
8
 
9
9
  /**
10
- * Check if two model names are compatible for state restore.
11
- * Resolves synonyms via findModel, then compares by stripping any
12
- * trailing parenthesised suffix (e.g. treating "BBC Master 128 (DFS)"
13
- * and "BBC Master 128 (ADFS)" as compatible). Does not treat
14
- * differently-named models like "BBC B with DFS 0.9" vs "BBC B with DFS 1.2"
15
- * as compatible — those are distinct models.
10
+ * Check if two model names resolve to the same model (accounting for
11
+ * synonyms and old names). Used to decide whether a page reload is
12
+ * needed when loading a snapshot saved under a different model name.
16
13
  */
17
- export function modelsCompatible(snapshotModel, currentModel) {
18
- if (snapshotModel === currentModel) return true;
19
- const resolvedSnapshot = findModel(snapshotModel);
20
- const resolvedCurrent = findModel(currentModel);
21
- if (resolvedSnapshot && resolvedCurrent) {
22
- // Same model object, or same base machine (strip filesystem suffix)
23
- if (resolvedSnapshot === resolvedCurrent) return true;
24
- const base = (name) => name.replace(/\s*\(.*\)$/, "");
25
- return base(resolvedSnapshot.name) === base(resolvedCurrent.name);
26
- }
27
- return false;
14
+ export function isSameModel(nameA, nameB) {
15
+ if (nameA === nameB) return true;
16
+ const a = findModel(nameA);
17
+ const b = findModel(nameB);
18
+ return a !== null && a === b;
28
19
  }
29
20
 
30
21
  // Map of TypedArray constructor names for deserialization
@@ -100,7 +91,7 @@ export function restoreSnapshot(cpu, model, snapshot) {
100
91
  if (snapshot.version > SnapshotVersion) {
101
92
  throw new Error(`Snapshot version ${snapshot.version} is newer than supported version ${SnapshotVersion}`);
102
93
  }
103
- if (!modelsCompatible(snapshot.model, model.name)) {
94
+ if (!isSameModel(snapshot.model, model.name)) {
104
95
  throw new Error(`Model mismatch: snapshot is for "${snapshot.model}" but current model is "${model.name}"`);
105
96
  }
106
97
  cpu.restoreState(snapshot.state);
package/src/soundchip.js CHANGED
@@ -1,3 +1,7 @@
1
+ // Atom speaker volume, scaled to be comparable with BBC tone channels.
2
+ // The BBC volumeTable[0] (loudest) is 0.25 (1.0 / 4 channels).
3
+ const speakerVolume = 0.5;
4
+
1
5
  const volumeTable = new Float32Array(16);
2
6
  (() => {
3
7
  let f = 1.0;
@@ -327,6 +331,94 @@ export class SoundChip {
327
331
  }
328
332
  }
329
333
 
334
+ /**
335
+ * AtomSoundChip -- Acorn Atom sound via a 1-bit speaker driven by the PPIA.
336
+ * Uses only the sine channel (shared with BBC for tape tones) and a speaker
337
+ * channel with DC-blocking filter.
338
+ */
339
+ export class AtomSoundChip extends SoundChip {
340
+ constructor(onBuffer, { cpuSpeed = 1000000 } = {}) {
341
+ super(onBuffer);
342
+ this.samplesPerCycle = this.soundchipFreq / cpuSpeed;
343
+ this.secondsPerCycle = 1 / cpuSpeed;
344
+
345
+ // Replace the BBC tone/noise generators with just sine + speaker.
346
+ this.generators = [this.sineChannel.bind(this), this.speakerChannel.bind(this)];
347
+ // Recompute sine attenuation for the Atom's 2-channel mix
348
+ // (parent computed it for 5 BBC channels).
349
+ this.sineTable = makeSineTable(1 / this.generators.length);
350
+
351
+ this.speakerGenerator = {
352
+ mute: () => {
353
+ this.catchUp();
354
+ this.speakerReset();
355
+ },
356
+ pushBit: (bit, cycles, seconds) => {
357
+ this.catchUp();
358
+ this.updateSpeaker(bit, cycles, seconds);
359
+ },
360
+ };
361
+
362
+ this.bitChange = [];
363
+ this.currentSpeakerBit = 0.0;
364
+ this._speakerPrevIn = 0;
365
+ this._speakerPrevOut = 0;
366
+ this._speakerCycleOffset = 0;
367
+ }
368
+
369
+ reset(hard) {
370
+ super.reset(hard);
371
+ if (hard) this.speakerReset();
372
+ }
373
+
374
+ catchUp() {
375
+ this._speakerCycleOffset = 0;
376
+ super.catchUp();
377
+ this._speakerCycleOffset = 0;
378
+ }
379
+
380
+ speakerReset() {
381
+ this.bitChange = [];
382
+ this.currentSpeakerBit = 0.0;
383
+ this._speakerPrevIn = 0;
384
+ this._speakerPrevOut = 0;
385
+ this._speakerCycleOffset = 0;
386
+ }
387
+
388
+ speakerChannel(channel, out, offset, length) {
389
+ const fromCycle = this.lastRunEpoch + this._speakerCycleOffset;
390
+ this._speakerCycleOffset += length / this.samplesPerCycle;
391
+ let bitIndex = 0;
392
+ // DC-blocking high-pass filter: y[n] = x[n] - x[n-1] + alpha * y[n-1]
393
+ // The SoundChip runs at 500 kHz (4 MHz / 8). For a ~20 Hz cutoff:
394
+ // alpha = 1 - 2*pi*fc/fs = 1 - 2*pi*20/500000 ≈ 0.99975
395
+ const alpha = 0.99975;
396
+
397
+ for (let i = 0; i < length; ++i) {
398
+ while (
399
+ bitIndex < this.bitChange.length &&
400
+ this.bitChange[bitIndex].cycles <= fromCycle + i / this.samplesPerCycle
401
+ ) {
402
+ this.currentSpeakerBit = this.bitChange[bitIndex].bit;
403
+ bitIndex++;
404
+ }
405
+ const input = this.currentSpeakerBit * speakerVolume;
406
+ this._speakerPrevOut = input - this._speakerPrevIn + alpha * this._speakerPrevOut;
407
+ this._speakerPrevIn = input;
408
+ out[i + offset] += this._speakerPrevOut;
409
+ }
410
+
411
+ if (bitIndex > 0) {
412
+ this.bitChange.splice(0, bitIndex);
413
+ }
414
+ }
415
+
416
+ updateSpeaker(value, microCycle, seconds) {
417
+ const cycles = microCycle + seconds / this.secondsPerCycle;
418
+ this.bitChange.push({ bit: value ? 1.0 : 0.0, cycles });
419
+ }
420
+ }
421
+
330
422
  /**
331
423
  * InstrumentedSoundChip - wraps a real SoundChip and captures all writes
332
424
  * for debugging purposes. Provides the same interface as SoundChip.
@@ -423,11 +515,17 @@ export class FakeSoundChip {
423
515
  mute: () => {},
424
516
  tone: () => {},
425
517
  };
518
+ this.speakerGenerator = {
519
+ mute: () => {},
520
+ pushBit: () => {},
521
+ };
426
522
  }
427
523
 
428
524
  snapshotState() {
429
525
  return {};
430
526
  }
431
527
 
432
- restoreState() {}
528
+ restoreState() {
529
+ this._speakerCycleOffset = 0;
530
+ }
433
531
  }
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
- }