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.
- package/README.md +17 -3
- package/package.json +8 -9
- 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 +351 -44
- package/src/6847.js +724 -0
- package/src/6847_fontdata.js +124 -0
- package/src/app/app.js +1 -1
- package/src/app/electron.js +4 -4
- package/src/bem-snapshot.js +5 -184
- package/src/config.js +20 -9
- package/src/disc.js +2 -20
- package/src/fake6502.js +3 -2
- package/src/jsbeeb.css +23 -0
- package/src/keyboard.js +62 -37
- package/src/machine-session.js +85 -59
- package/src/main.js +188 -75
- package/src/mmc.js +1053 -0
- package/src/models.js +46 -5
- package/src/ppia.js +477 -0
- package/src/snapshot-helpers.js +208 -0
- package/src/snapshot.js +9 -18
- package/src/soundchip.js +99 -1
- package/src/tapes.js +73 -16
- package/src/uef-snapshot.js +402 -0
- package/src/url-params.js +7 -2
- package/src/utils.js +81 -2
- package/src/utils_atom.js +508 -0
- package/src/video.js +12 -1
- package/src/web/audio-handler.js +39 -17
- package/tests/test-machine.js +133 -8
|
@@ -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
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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
|
|
18
|
-
if (
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
|
|
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 (!
|
|
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
|
-
|
|
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
|
-
}
|