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/models.js
CHANGED
|
@@ -11,12 +11,14 @@ const CpuModel = Object.freeze({
|
|
|
11
11
|
});
|
|
12
12
|
|
|
13
13
|
class Model {
|
|
14
|
-
constructor({ name, synonyms, os, cpuModel, isMaster, swram, fdc, tube, cmosOverride } = {}) {
|
|
14
|
+
constructor({ name, synonyms, os, cpuModel, isMaster, isAtom, swram, fdc, tube, cmosOverride, banks } = {}) {
|
|
15
15
|
this.name = name;
|
|
16
16
|
this.synonyms = synonyms;
|
|
17
17
|
this.os = os;
|
|
18
|
+
this.banks = banks;
|
|
18
19
|
this._cpuModel = cpuModel;
|
|
19
20
|
this.isMaster = isMaster;
|
|
21
|
+
this.isAtom = !!isAtom;
|
|
20
22
|
this.Fdc = fdc;
|
|
21
23
|
this.swram = swram;
|
|
22
24
|
this.isTest = false;
|
|
@@ -58,6 +60,20 @@ function pickDfs(cmos) {
|
|
|
58
60
|
return cmos;
|
|
59
61
|
}
|
|
60
62
|
|
|
63
|
+
function atomModel({ name, synonyms, os, banks }) {
|
|
64
|
+
return new Model({
|
|
65
|
+
name,
|
|
66
|
+
synonyms,
|
|
67
|
+
os,
|
|
68
|
+
cpuModel: CpuModel.MOS6502,
|
|
69
|
+
isMaster: false,
|
|
70
|
+
isAtom: true,
|
|
71
|
+
swram: beebSwram,
|
|
72
|
+
fdc: NoiseAwareIntelFdc,
|
|
73
|
+
banks,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
61
77
|
// TODO: semi-bplus-style to get swram for exile hardcoded here
|
|
62
78
|
const beebSwram = [
|
|
63
79
|
true,
|
|
@@ -164,6 +180,31 @@ export const allModels = [
|
|
|
164
180
|
fdc: NoiseAwareWdFdc,
|
|
165
181
|
cmosOverride: pickAnfs,
|
|
166
182
|
}),
|
|
183
|
+
// Atom OS ROM layout: Block F (kernel), E (DOS/MMC), D (FP), C (BASIC).
|
|
184
|
+
// Empty strings represent unpopulated ROM slots.
|
|
185
|
+
atomModel({
|
|
186
|
+
name: "Acorn Atom (MMC)",
|
|
187
|
+
synonyms: ["Atom"],
|
|
188
|
+
os: ["atom/Atom_Kernel_E.rom", "atom/ATMMC3E.rom", "atom/Atom_FloatingPoint.rom", "atom/Atom_Basic.rom"],
|
|
189
|
+
// Utility ROMs in banks 0 and 1 of Block A (0xA000-0xAFFF), up to 8 banks.
|
|
190
|
+
// Bank selected by Branquart latch at 0xBFFF (bits 0-3 = bank, bit 6 = lock).
|
|
191
|
+
banks: ["atom/PCHARME.ROM", "atom/gags.rom"],
|
|
192
|
+
}),
|
|
193
|
+
atomModel({
|
|
194
|
+
name: "Acorn Atom (Tape)",
|
|
195
|
+
synonyms: ["Atom-Tape"],
|
|
196
|
+
os: ["atom/Atom_Kernel.rom", "", "", "atom/Atom_Basic.rom"],
|
|
197
|
+
}),
|
|
198
|
+
atomModel({
|
|
199
|
+
name: "Acorn Atom (Tape with FP)",
|
|
200
|
+
synonyms: ["Atom-Tape-FP"],
|
|
201
|
+
os: ["atom/Atom_Kernel.rom", "", "atom/Atom_FloatingPoint.rom", "atom/Atom_Basic.rom"],
|
|
202
|
+
}),
|
|
203
|
+
atomModel({
|
|
204
|
+
name: "Acorn Atom (DOS)",
|
|
205
|
+
synonyms: ["Atom-DOS"],
|
|
206
|
+
os: ["atom/Atom_Kernel.rom", "atom/Atom_DOS.rom", "atom/Atom_FloatingPoint.rom", "atom/Atom_Basic.rom"],
|
|
207
|
+
}),
|
|
167
208
|
// Although this can not be explicitly selected as a model, it is required by the configuration builder later
|
|
168
209
|
new Model({
|
|
169
210
|
name: "Tube65C02",
|
package/src/ppia.js
ADDED
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
import { getKeyMapAtom } from "./utils_atom.js";
|
|
2
|
+
|
|
3
|
+
// 8255 Programmable Peripheral Interface Adapter for the Acorn Atom.
|
|
4
|
+
// Reference: http://mdfs.net/Docs/Comp/Acorn/Atom/atap25.htm
|
|
5
|
+
// Memory map: http://mdfs.net/Docs/Comp/Acorn/Atom/MemoryMap
|
|
6
|
+
//
|
|
7
|
+
// Port A - 0xB000 (output)
|
|
8
|
+
// bits 0-3: Keyboard row select
|
|
9
|
+
// bits 4-7: MC6847 graphics mode
|
|
10
|
+
//
|
|
11
|
+
// Port B - 0xB001 (input)
|
|
12
|
+
// bits 0-5: Keyboard column (active low)
|
|
13
|
+
// bit 6: CTRL key (low when pressed)
|
|
14
|
+
// bit 7: SHIFT key (low when pressed)
|
|
15
|
+
//
|
|
16
|
+
// Port C - 0xB002 (mixed I/O)
|
|
17
|
+
// Output bits:
|
|
18
|
+
// 0: Tape output
|
|
19
|
+
// 1: Enable 2.4 kHz to cassette output
|
|
20
|
+
// 2: Loudspeaker
|
|
21
|
+
// 3: Colour Set Select (CSS)
|
|
22
|
+
// Input bits:
|
|
23
|
+
// 4: 2.4 kHz input
|
|
24
|
+
// 5: Cassette input
|
|
25
|
+
// 6: REPT key (low when pressed)
|
|
26
|
+
// 7: 60 Hz VSync signal (low during flyback)
|
|
27
|
+
//
|
|
28
|
+
// Keyboard matrix (active low, active when key pressed):
|
|
29
|
+
// Port A row → 9 8 7 6 5 4 3 2 1 0
|
|
30
|
+
// Port B col ↓
|
|
31
|
+
// ~b0 : SPC [ \ ] ^ LCK <-> ^-v Lft Rgt
|
|
32
|
+
// ~b1 : Dwn Up CLR ENT CPY DEL 0 1 2 3
|
|
33
|
+
// ~b2 : 4 5 6 7 8 9 : ; < =
|
|
34
|
+
// ~b3 : > ? @ A B C D E F G
|
|
35
|
+
// ~b4 : H I J K L M N O P Q
|
|
36
|
+
// ~b5 : R S T U V W X Y Z ESC
|
|
37
|
+
// ~b6 : Ctrl
|
|
38
|
+
// ~b7 : Shift
|
|
39
|
+
|
|
40
|
+
const PORTA = 0x0,
|
|
41
|
+
PORTB = 0x1,
|
|
42
|
+
PORTC = 0x2,
|
|
43
|
+
CREG = 0x3; // control register
|
|
44
|
+
|
|
45
|
+
class PPIA {
|
|
46
|
+
constructor(cpu) {
|
|
47
|
+
this.cpu = cpu;
|
|
48
|
+
|
|
49
|
+
this.latcha = 0;
|
|
50
|
+
this.latchb = 0;
|
|
51
|
+
this.latchc = 0;
|
|
52
|
+
this.portapins = 0;
|
|
53
|
+
this.portbpins = 0;
|
|
54
|
+
this.portcpins = 0;
|
|
55
|
+
this.creg = 0;
|
|
56
|
+
this.speaker = 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
reset() {
|
|
60
|
+
//http://members.casema.nl/hhaydn/8255_pin.html
|
|
61
|
+
this.latcha = this.latchb = this.latchc = 0x00;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
setVBlankInt(level) {
|
|
65
|
+
// level == 1 when in the vsync
|
|
66
|
+
// FE66_wait_for_flyback_start will loop until bit 7 (copied into N register using BIT)
|
|
67
|
+
// of B002 is not 0 (i.e until BPL fails when bit 7 is 1)
|
|
68
|
+
// then
|
|
69
|
+
// FE6B_wait_for_flyback will loop until bit 7 of B002 is (copied into N register using BIT)
|
|
70
|
+
// of B002 is not 1 (i.e until BMI fails when bit 7 is 0)
|
|
71
|
+
|
|
72
|
+
//60 Hz sync signal - normally 1 during the frame, but goes 0 at start of flyback (at the end of a frame).
|
|
73
|
+
//opposite of the 'level'
|
|
74
|
+
if (!level) {
|
|
75
|
+
// set bit 7 to 1 - in frame
|
|
76
|
+
this.latchc |= 0x80;
|
|
77
|
+
} else {
|
|
78
|
+
// set bit 7 to 0 - in vsync
|
|
79
|
+
this.latchc &= ~0x80;
|
|
80
|
+
}
|
|
81
|
+
this.recalculatePortCPins();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/*
|
|
85
|
+
Port C - #B002
|
|
86
|
+
Output bits: Function:
|
|
87
|
+
0 Tape output
|
|
88
|
+
1 Enable 2.4 kHz to cassette output
|
|
89
|
+
2 Loudspeaker
|
|
90
|
+
3 Not used
|
|
91
|
+
|
|
92
|
+
Input bits: Function:
|
|
93
|
+
4 2.4 kHz input
|
|
94
|
+
5 Cassette input
|
|
95
|
+
6 REPT key (low when pressed)
|
|
96
|
+
7 60 Hz sync signal (low during flyback)
|
|
97
|
+
The port C output lines, bits 0 to 3, may be used for user applications when the cassette interface is not being used.
|
|
98
|
+
|
|
99
|
+
*/
|
|
100
|
+
write(addr, val) {
|
|
101
|
+
val |= 0;
|
|
102
|
+
// The 8255 only decodes A0-A1; addresses 4-7 mirror 0-3.
|
|
103
|
+
// Addresses 8-15 are outside the 8255's decode range.
|
|
104
|
+
if ((addr & 0xf) >= 0x8) return;
|
|
105
|
+
switch (addr & 0x3) {
|
|
106
|
+
case PORTA:
|
|
107
|
+
this.latcha = val;
|
|
108
|
+
this.recalculatePortAPins();
|
|
109
|
+
break;
|
|
110
|
+
|
|
111
|
+
case PORTB:
|
|
112
|
+
// Port B is input-only; writes are ignored.
|
|
113
|
+
break;
|
|
114
|
+
|
|
115
|
+
case PORTC:
|
|
116
|
+
this.latchc = (this.latchc & 0xf0) | (val & 0x0f);
|
|
117
|
+
|
|
118
|
+
this.recalculatePortCPins();
|
|
119
|
+
break;
|
|
120
|
+
case CREG: {
|
|
121
|
+
this.creg = val & 0xff;
|
|
122
|
+
// 8255 CREG: D7=1 is mode-set (ignored; Atom uses fixed port directions),
|
|
123
|
+
// D7=0 is Bit Set/Reset (BSR) for individual port C output bits.
|
|
124
|
+
if (val & 0x80) break; // mode-set: no action needed
|
|
125
|
+
|
|
126
|
+
// BSR: set or clear a single port C output bit.
|
|
127
|
+
// NOTE: Simplified: only handles bits 2 (speaker) and 3 (CSS),
|
|
128
|
+
// and rebuilds the lower nibble. Works because the Atom ROM
|
|
129
|
+
// only BSR-toggles these two bits.
|
|
130
|
+
let speaker = this.latchc & 0x04;
|
|
131
|
+
let css = this.latchc & 0x08;
|
|
132
|
+
switch (val & 0xe) {
|
|
133
|
+
case 0x4: // port C pin 2 (speaker)
|
|
134
|
+
speaker = (val & 1) << 2;
|
|
135
|
+
break;
|
|
136
|
+
case 0x6: // port C pin 3 (CSS)
|
|
137
|
+
css = (val & 1) << 3;
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
this.latchc = (this.latchc & 0xf0) | css | speaker;
|
|
141
|
+
this.recalculatePortCPins();
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
read(addr) {
|
|
148
|
+
if ((addr & 0xf) >= 0x8) return addr >>> 8; // open bus
|
|
149
|
+
switch (addr & 0x3) {
|
|
150
|
+
case PORTA:
|
|
151
|
+
this.recalculatePortAPins();
|
|
152
|
+
return this.portapins;
|
|
153
|
+
case PORTB: {
|
|
154
|
+
this.recalculatePortBPins();
|
|
155
|
+
const keyrow = this.portapins & 0x0f;
|
|
156
|
+
const n = this.keys[keyrow];
|
|
157
|
+
let r = 0xff; // all keys unpressed
|
|
158
|
+
for (let b = 0; b <= 9; b++) r &= ~(n[b] << b);
|
|
159
|
+
// CTRL and SHIFT are always readable regardless of row selection
|
|
160
|
+
const ctrl_shift = (this.keys[0][7] << 7) | (this.keys[0][6] << 6);
|
|
161
|
+
r &= ~(ctrl_shift & 0xc0);
|
|
162
|
+
return r;
|
|
163
|
+
}
|
|
164
|
+
case PORTC: {
|
|
165
|
+
this.recalculatePortCPins();
|
|
166
|
+
|
|
167
|
+
// Force HZIN (bit 4) high
|
|
168
|
+
this.portcpins = (this.portcpins & 0xef) | (1 << 4);
|
|
169
|
+
|
|
170
|
+
// Read back full port C state: output bits 0-3 from latch,
|
|
171
|
+
// input bits 4-7 from pins, with REPT key overlay on bit 6.
|
|
172
|
+
let val = this.portcpins;
|
|
173
|
+
|
|
174
|
+
// REPT key: bit 6 is LOW when pressed
|
|
175
|
+
const rept_key = (!this.keys[1][6] << 6) & 0x40;
|
|
176
|
+
val = (val & ~0x40) | rept_key;
|
|
177
|
+
|
|
178
|
+
// Track cassette input transitions. The Atom ROM tape routines:
|
|
179
|
+
// 0xfc0a - OSBGET: get byte from tape (every 3.34ms)
|
|
180
|
+
// 0xfcd2 - test tape input pulse (every 0.033ms / 33 cycles)
|
|
181
|
+
// 0xfcc2 - count duration of tape pulse (<8 loops = '1', >=8 = '0')
|
|
182
|
+
// 0xfe6e, 0xfe9d, 0xfe69 - flyback/VSync routines
|
|
183
|
+
// Between each receiveBit, fcd2 is called ~6 times (33 cycles each).
|
|
184
|
+
return val;
|
|
185
|
+
}
|
|
186
|
+
default:
|
|
187
|
+
return addr >>> 8; // CREG and unmapped registers return open bus
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
recalculatePortAPins() {
|
|
192
|
+
this.portapins = this.latcha;
|
|
193
|
+
this.drivePortA();
|
|
194
|
+
this.portAUpdated();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
recalculatePortBPins() {
|
|
198
|
+
this.portbpins = this.latchb;
|
|
199
|
+
this.drivePortB();
|
|
200
|
+
this.portBUpdated();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
recalculatePortCPins() {
|
|
204
|
+
this.portcpins = this.latchc;
|
|
205
|
+
this.drivePortC();
|
|
206
|
+
this.portCUpdated();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// No-op hooks for subclass override
|
|
210
|
+
drivePortA() {}
|
|
211
|
+
drivePortB() {}
|
|
212
|
+
drivePortC() {}
|
|
213
|
+
portAUpdated() {}
|
|
214
|
+
portBUpdated() {}
|
|
215
|
+
portCUpdated() {}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// On the atom, the PPIA does the keyboard, speaker and tape.
|
|
219
|
+
// On the BBC, sysVIA does the keyboard and pokes the soundchip
|
|
220
|
+
// and the ACIA does the tape
|
|
221
|
+
export class AtomPPIA extends PPIA {
|
|
222
|
+
constructor(cpu, initialLayout, scheduler) {
|
|
223
|
+
super(cpu);
|
|
224
|
+
|
|
225
|
+
this.keys = [];
|
|
226
|
+
for (let i = 0; i < 16; ++i) {
|
|
227
|
+
this.keys[i] = new Uint8Array(16);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
this.setKeyLayout(initialLayout);
|
|
231
|
+
|
|
232
|
+
this.keyboardEnabled = true;
|
|
233
|
+
this.lastSpeakerBit = 0;
|
|
234
|
+
// The Atom has no lock lights. Report caps lock as "on" and shift
|
|
235
|
+
// lock as "off" so the paste routine's lock-toggle logic is never
|
|
236
|
+
// triggered (it toggles CAPSLOCK when !capsLockLight, and SHIFTLOCK
|
|
237
|
+
// when shiftLockLight).
|
|
238
|
+
this.capsLockLight = true;
|
|
239
|
+
this.shiftLockLight = false;
|
|
240
|
+
this.tapeCarrierCount = 0;
|
|
241
|
+
this.tapeDcdLineLevel = false;
|
|
242
|
+
this.motorOn = false;
|
|
243
|
+
|
|
244
|
+
this.reset();
|
|
245
|
+
|
|
246
|
+
// from ACIA
|
|
247
|
+
this.runTapeTask = scheduler.newTask(() => this.runTape());
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
setKeyLayout(map) {
|
|
251
|
+
this.keycodeToRowCol = getKeyMapAtom(map);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// from SysVIA
|
|
255
|
+
clearKeys() {
|
|
256
|
+
for (let i = 0; i < this.keys.length; ++i) {
|
|
257
|
+
for (let j = 0; j < this.keys[i].length; ++j) {
|
|
258
|
+
this.keys[i][j] = 0;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
this.updateKeys();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// from SysVIA
|
|
265
|
+
disableKeyboard() {
|
|
266
|
+
this.keyboardEnabled = false;
|
|
267
|
+
this.clearKeys();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// from SysVIA
|
|
271
|
+
enableKeyboard() {
|
|
272
|
+
this.keyboardEnabled = true;
|
|
273
|
+
this.clearKeys();
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// from SysVIA
|
|
277
|
+
set(key, val, shiftDown) {
|
|
278
|
+
if (!this.keyboardEnabled) return;
|
|
279
|
+
const colrow = this.keycodeToRowCol[!!shiftDown][key];
|
|
280
|
+
if (!colrow) return;
|
|
281
|
+
|
|
282
|
+
this.keys[colrow[0]][colrow[1]] = val;
|
|
283
|
+
this.updateKeys();
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// from SysVIA
|
|
287
|
+
keyDown(key, shiftDown) {
|
|
288
|
+
this.set(key, 1, shiftDown);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// from SysVIA
|
|
292
|
+
keyUp(key) {
|
|
293
|
+
// set up for both keymaps
|
|
294
|
+
// (with and without shift)
|
|
295
|
+
this.set(key, 0, true);
|
|
296
|
+
this.set(key, 0, false);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// from SysVIA
|
|
300
|
+
keyDownRaw(colrow) {
|
|
301
|
+
this.keys[colrow[0]][colrow[1]] = 1;
|
|
302
|
+
this.updateKeys();
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// from SysVIA
|
|
306
|
+
keyUpRaw(colrow) {
|
|
307
|
+
this.keys[colrow[0]][colrow[1]] = 0;
|
|
308
|
+
this.updateKeys();
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// from SysVIA
|
|
312
|
+
keyToggleRaw(colrow) {
|
|
313
|
+
this.keys[colrow[0]][colrow[1]] = 1 - this.keys[colrow[0]][colrow[1]];
|
|
314
|
+
this.updateKeys();
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// from SysVIA
|
|
318
|
+
hasAnyKeyDown() {
|
|
319
|
+
// 10 for ATOM
|
|
320
|
+
const numCols = 10;
|
|
321
|
+
|
|
322
|
+
for (let i = 0; i < numCols; ++i) {
|
|
323
|
+
for (let j = 0; j < 8; ++j) {
|
|
324
|
+
if (this.keys[i][j]) {
|
|
325
|
+
return true;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// nothing on ATOM
|
|
333
|
+
updateKeys() {}
|
|
334
|
+
|
|
335
|
+
// nothing on ATOM
|
|
336
|
+
polltime() {}
|
|
337
|
+
|
|
338
|
+
portAUpdated() {
|
|
339
|
+
this.updateKeys();
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// nothing on ATOM
|
|
343
|
+
portBUpdated() {}
|
|
344
|
+
|
|
345
|
+
portCUpdated() {
|
|
346
|
+
const speakerBit = (this.portcpins & 0x04) >>> 2;
|
|
347
|
+
if (speakerBit !== this.lastSpeakerBit) {
|
|
348
|
+
this.lastSpeakerBit = speakerBit;
|
|
349
|
+
this.cpu.soundChip.speakerGenerator.pushBit(speakerBit, this.cpu.currentCycles, this.cpu.cycleSeconds);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
drivePortA() {
|
|
354
|
+
this.updateKeys();
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
drivePortB() {
|
|
358
|
+
// Nothing driving here.
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
drivePortC() {
|
|
362
|
+
// Nothing driving here.
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// ATOM TAPE SUPPORT
|
|
366
|
+
// from ACIA on BBC
|
|
367
|
+
|
|
368
|
+
// set by TAPE
|
|
369
|
+
tone(freq) {
|
|
370
|
+
let toneGen = this.cpu.soundChip.toneGenerator;
|
|
371
|
+
if (!freq) toneGen.mute();
|
|
372
|
+
else toneGen.tone(freq);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// nothing on ATOM
|
|
376
|
+
dcdLineUpdated() {}
|
|
377
|
+
|
|
378
|
+
// set by TAPE
|
|
379
|
+
setTapeCarrier(level) {
|
|
380
|
+
if (!level) {
|
|
381
|
+
this.tapeCarrierCount = 0;
|
|
382
|
+
this.tapeDcdLineLevel = false;
|
|
383
|
+
} else {
|
|
384
|
+
this.tapeCarrierCount++;
|
|
385
|
+
// The tape hardware doesn't raise DCD until the carrier tone
|
|
386
|
+
// has persisted for a while. The BBC service manual opines,
|
|
387
|
+
// "The DCD flag in the 6850 should change 0.1 to 0.4 seconds
|
|
388
|
+
// after a continuous tone appears".
|
|
389
|
+
// Star Drifter doesn't load without this.
|
|
390
|
+
// We use 0.174s, measured on an issue 3 model B.
|
|
391
|
+
// Testing on real hardware, DCD is blipped, it lowers about
|
|
392
|
+
// 210us after it raises, even though the carrier tone
|
|
393
|
+
// may be continuing.
|
|
394
|
+
this.tapeDcdLineLevel = this.tapeCarrierCount === 209;
|
|
395
|
+
}
|
|
396
|
+
this.dcdLineUpdated();
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Receive bits from tape (called by tape.poll via PPIA, not ACIA like BBC).
|
|
400
|
+
// Called once every ~208 clock cycles (208us at 1 MHz).
|
|
401
|
+
// Recognition: '1' = 4 half-cycles at 1.2 kHz (duration < 8),
|
|
402
|
+
// '0' = 8 half-cycles at 2.4 kHz (duration >= 8).
|
|
403
|
+
// Leader tone is a stream of '1' bits.
|
|
404
|
+
receiveBit(bit) {
|
|
405
|
+
bit |= 0;
|
|
406
|
+
this.latchc = (this.latchc & 0xdf) | (bit << 5);
|
|
407
|
+
this.recalculatePortCPins();
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// nothing on ATOM
|
|
411
|
+
receive(/*_byte*/) {}
|
|
412
|
+
|
|
413
|
+
setTape(tape) {
|
|
414
|
+
this.tape = tape;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
rewindTape() {
|
|
418
|
+
if (this.tape) {
|
|
419
|
+
this.tape.rewind();
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
playTape() {
|
|
424
|
+
if (this.tape) {
|
|
425
|
+
this.motorOn = true;
|
|
426
|
+
this.runTape();
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
stopTape() {
|
|
431
|
+
this.motorOn = false;
|
|
432
|
+
if (this.tape) {
|
|
433
|
+
const toneGen = this.cpu.soundChip.toneGenerator;
|
|
434
|
+
toneGen.mute();
|
|
435
|
+
this.runTapeTask.cancel();
|
|
436
|
+
this.setTapeCarrier(false);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
runTape() {
|
|
441
|
+
if (this.tape) this.runTapeTask.reschedule(this.tape.poll(this));
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
updateIrq() {}
|
|
445
|
+
|
|
446
|
+
snapshotState() {
|
|
447
|
+
return {
|
|
448
|
+
latcha: this.latcha,
|
|
449
|
+
latchb: this.latchb,
|
|
450
|
+
latchc: this.latchc,
|
|
451
|
+
portapins: this.portapins,
|
|
452
|
+
portbpins: this.portbpins,
|
|
453
|
+
portcpins: this.portcpins,
|
|
454
|
+
creg: this.creg,
|
|
455
|
+
keys: this.keys.map((row) => Array.from(row)),
|
|
456
|
+
keyboardEnabled: this.keyboardEnabled,
|
|
457
|
+
lastSpeakerBit: this.lastSpeakerBit,
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
restoreState(state) {
|
|
462
|
+
this.latcha = state.latcha;
|
|
463
|
+
this.latchb = state.latchb;
|
|
464
|
+
this.latchc = state.latchc;
|
|
465
|
+
this.portapins = state.portapins;
|
|
466
|
+
this.portbpins = state.portbpins;
|
|
467
|
+
this.portcpins = state.portcpins;
|
|
468
|
+
this.creg = state.creg;
|
|
469
|
+
if (state.keys) {
|
|
470
|
+
for (let i = 0; i < state.keys.length; i++) {
|
|
471
|
+
this.keys[i].set(state.keys[i]);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
this.keyboardEnabled = state.keyboardEnabled;
|
|
475
|
+
this.lastSpeakerBit = state.lastSpeakerBit;
|
|
476
|
+
}
|
|
477
|
+
}
|
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
|
}
|