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,508 @@
|
|
|
1
|
+
import { keyCodes, userKeymap } from "./utils.js";
|
|
2
|
+
|
|
3
|
+
// Detect keyboard layout locally — mirrors the logic in utils.js but avoids
|
|
4
|
+
// importing the non-exported isUKlayout. Should be replaced with an import
|
|
5
|
+
// if utils.js ever exports it.
|
|
6
|
+
function detectKeyboardLayout() {
|
|
7
|
+
if (typeof navigator === "undefined") return "UK";
|
|
8
|
+
if (typeof localStorage !== "undefined" && localStorage.keyboardLayout) {
|
|
9
|
+
return localStorage.keyboardLayout === "US" ? "US" : "UK";
|
|
10
|
+
}
|
|
11
|
+
if (navigator.language) {
|
|
12
|
+
if (navigator.language.toLowerCase() === "en-gb") return "UK";
|
|
13
|
+
if (navigator.language.toLowerCase() === "en-us") return "US";
|
|
14
|
+
}
|
|
15
|
+
return "UK";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const isUKlayout = detectKeyboardLayout() === "UK";
|
|
19
|
+
|
|
20
|
+
// ATOM
|
|
21
|
+
|
|
22
|
+
/*
|
|
23
|
+
Acorn Atom
|
|
24
|
+
|
|
25
|
+
&B001 - keyboard matrix column:
|
|
26
|
+
~b0 : SPC [ \ ] ^ LCK <-> ^-v Lft Rgt
|
|
27
|
+
~b1 : Dwn Up CLR ENT CPY DEL 0 1 2 3
|
|
28
|
+
~b2 : 4 5 6 7 8 9 : ; < =
|
|
29
|
+
~b3 : > ? @ A B C D E F G
|
|
30
|
+
~b4 : H I J K L M N O P Q
|
|
31
|
+
~b5 : R S T U V W X Y Z ESC
|
|
32
|
+
~b6 : Ctrl
|
|
33
|
+
~b7 : Shift
|
|
34
|
+
9 8 7 6 5 4 3 2 1 0
|
|
35
|
+
|
|
36
|
+
&B002 - REPT key
|
|
37
|
+
~b6 : Rept
|
|
38
|
+
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
export const ATOM = {
|
|
42
|
+
RIGHT: [0, 0],
|
|
43
|
+
LEFT: [1, 0],
|
|
44
|
+
UP_DOWN: [2, 0],
|
|
45
|
+
LEFT_RIGHT: [3, 0],
|
|
46
|
+
LOCK: [4, 0], //CAPSLOCK
|
|
47
|
+
|
|
48
|
+
UP_ARROW: [5, 0], // big uparrow next to break
|
|
49
|
+
RIGHT_SQUARE_BRACKET: [6, 0],
|
|
50
|
+
BACKSLASH: [7, 0],
|
|
51
|
+
LEFT_SQUARE_BRACKET: [8, 0],
|
|
52
|
+
SPACE: [9, 0],
|
|
53
|
+
|
|
54
|
+
K3: [0, 1],
|
|
55
|
+
K2: [1, 1],
|
|
56
|
+
K1: [2, 1],
|
|
57
|
+
K0: [3, 1],
|
|
58
|
+
DELETE: [4, 1],
|
|
59
|
+
COPY: [5, 1],
|
|
60
|
+
RETURN: [6, 1],
|
|
61
|
+
CLEAR: [7, 1],
|
|
62
|
+
UP: [8, 1],
|
|
63
|
+
DOWN: [9, 1],
|
|
64
|
+
|
|
65
|
+
MINUS_EQUALS: [0, 2],
|
|
66
|
+
COMMA_LESSTHAN: [1, 2],
|
|
67
|
+
SEMICOLON_PLUS: [2, 2],
|
|
68
|
+
COLON_STAR: [3, 2],
|
|
69
|
+
K9: [4, 2],
|
|
70
|
+
K8: [5, 2],
|
|
71
|
+
K7: [6, 2],
|
|
72
|
+
K6: [7, 2],
|
|
73
|
+
K5: [8, 2],
|
|
74
|
+
K4: [9, 2],
|
|
75
|
+
|
|
76
|
+
G: [0, 3],
|
|
77
|
+
F: [1, 3],
|
|
78
|
+
E: [2, 3],
|
|
79
|
+
D: [3, 3],
|
|
80
|
+
C: [4, 3],
|
|
81
|
+
B: [5, 3],
|
|
82
|
+
A: [6, 3],
|
|
83
|
+
AT: [7, 3],
|
|
84
|
+
SLASH_QUESTIONMARK: [8, 3], // AND QUESTION MARK
|
|
85
|
+
PERIOD_GREATERTHAN: [9, 3], // AND GREATER
|
|
86
|
+
|
|
87
|
+
Q: [0, 4],
|
|
88
|
+
P: [1, 4],
|
|
89
|
+
O: [2, 4],
|
|
90
|
+
N: [3, 4],
|
|
91
|
+
M: [4, 4],
|
|
92
|
+
L: [5, 4],
|
|
93
|
+
K: [6, 4],
|
|
94
|
+
J: [7, 4],
|
|
95
|
+
I: [8, 4],
|
|
96
|
+
H: [9, 4],
|
|
97
|
+
|
|
98
|
+
ESCAPE: [0, 5],
|
|
99
|
+
Z: [1, 5],
|
|
100
|
+
Y: [2, 5],
|
|
101
|
+
X: [3, 5],
|
|
102
|
+
W: [4, 5],
|
|
103
|
+
V: [5, 5],
|
|
104
|
+
U: [6, 5],
|
|
105
|
+
T: [7, 5],
|
|
106
|
+
S: [8, 5],
|
|
107
|
+
R: [9, 5],
|
|
108
|
+
|
|
109
|
+
// special codes
|
|
110
|
+
CTRL: [0, 6],
|
|
111
|
+
SHIFT: [0, 7],
|
|
112
|
+
REPT: [1, 6],
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export function stringToATOMKeys(str) {
|
|
116
|
+
const array = [];
|
|
117
|
+
let shiftState = false;
|
|
118
|
+
let capsLockState = true;
|
|
119
|
+
for (let i = 0; i < str.length; ++i) {
|
|
120
|
+
const c = str.charCodeAt(i);
|
|
121
|
+
let charStr = str.charAt(i);
|
|
122
|
+
let atomKey = null;
|
|
123
|
+
let needsShift = false;
|
|
124
|
+
// Only letters care about caps lock state; non-letter characters
|
|
125
|
+
// leave it wherever it is to avoid unnecessary LOCK toggles.
|
|
126
|
+
let needsCapsLock = capsLockState;
|
|
127
|
+
if (c >= 65 && c <= 90) {
|
|
128
|
+
// A-Z
|
|
129
|
+
atomKey = ATOM[charStr];
|
|
130
|
+
needsCapsLock = true;
|
|
131
|
+
} else if (c >= 97 && c <= 122) {
|
|
132
|
+
// a-z (LOCK toggles the ROM's internal caps lock state)
|
|
133
|
+
charStr = String.fromCharCode(c - 32);
|
|
134
|
+
atomKey = ATOM[charStr];
|
|
135
|
+
needsCapsLock = false;
|
|
136
|
+
} else if (c >= 48 && c <= 57) {
|
|
137
|
+
// 0-9
|
|
138
|
+
atomKey = ATOM["K" + charStr];
|
|
139
|
+
} else if (c >= 33 && c <= 41) {
|
|
140
|
+
// ! to )
|
|
141
|
+
charStr = String.fromCharCode(c + 16);
|
|
142
|
+
atomKey = ATOM["K" + charStr];
|
|
143
|
+
needsShift = true;
|
|
144
|
+
} else {
|
|
145
|
+
switch (charStr) {
|
|
146
|
+
case "\n":
|
|
147
|
+
atomKey = ATOM.RETURN;
|
|
148
|
+
break;
|
|
149
|
+
case "\t":
|
|
150
|
+
atomKey = ATOM.SPACE; // Atom has no TAB key
|
|
151
|
+
break;
|
|
152
|
+
case " ":
|
|
153
|
+
atomKey = ATOM.SPACE;
|
|
154
|
+
break;
|
|
155
|
+
case "-":
|
|
156
|
+
atomKey = ATOM.MINUS_EQUALS;
|
|
157
|
+
break;
|
|
158
|
+
case "=":
|
|
159
|
+
atomKey = ATOM.MINUS_EQUALS;
|
|
160
|
+
needsShift = true;
|
|
161
|
+
break;
|
|
162
|
+
case "\\":
|
|
163
|
+
atomKey = ATOM.BACKSLASH;
|
|
164
|
+
break;
|
|
165
|
+
case "@":
|
|
166
|
+
atomKey = ATOM.AT;
|
|
167
|
+
break;
|
|
168
|
+
case "[":
|
|
169
|
+
atomKey = ATOM.LEFT_SQUARE_BRACKET;
|
|
170
|
+
break;
|
|
171
|
+
case ";":
|
|
172
|
+
atomKey = ATOM.SEMICOLON_PLUS;
|
|
173
|
+
break;
|
|
174
|
+
case "+":
|
|
175
|
+
atomKey = ATOM.SEMICOLON_PLUS;
|
|
176
|
+
needsShift = true;
|
|
177
|
+
break;
|
|
178
|
+
case ":":
|
|
179
|
+
atomKey = ATOM.COLON_STAR;
|
|
180
|
+
break;
|
|
181
|
+
case "*":
|
|
182
|
+
atomKey = ATOM.COLON_STAR;
|
|
183
|
+
needsShift = true;
|
|
184
|
+
break;
|
|
185
|
+
case "]":
|
|
186
|
+
atomKey = ATOM.RIGHT_SQUARE_BRACKET;
|
|
187
|
+
break;
|
|
188
|
+
case ",":
|
|
189
|
+
atomKey = ATOM.COMMA_LESSTHAN;
|
|
190
|
+
break;
|
|
191
|
+
case "<":
|
|
192
|
+
atomKey = ATOM.COMMA_LESSTHAN;
|
|
193
|
+
needsShift = true;
|
|
194
|
+
break;
|
|
195
|
+
case ".":
|
|
196
|
+
atomKey = ATOM.PERIOD_GREATERTHAN;
|
|
197
|
+
break;
|
|
198
|
+
case ">":
|
|
199
|
+
atomKey = ATOM.PERIOD_GREATERTHAN;
|
|
200
|
+
needsShift = true;
|
|
201
|
+
break;
|
|
202
|
+
case "/":
|
|
203
|
+
atomKey = ATOM.SLASH_QUESTIONMARK;
|
|
204
|
+
break;
|
|
205
|
+
case "?":
|
|
206
|
+
atomKey = ATOM.SLASH_QUESTIONMARK;
|
|
207
|
+
needsShift = true;
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (!atomKey) continue;
|
|
213
|
+
|
|
214
|
+
if ((needsShift && !shiftState) || (!needsShift && shiftState)) {
|
|
215
|
+
array.push(ATOM.SHIFT);
|
|
216
|
+
shiftState = !shiftState;
|
|
217
|
+
}
|
|
218
|
+
if ((needsCapsLock && !capsLockState) || (!needsCapsLock && capsLockState)) {
|
|
219
|
+
array.push(ATOM.LOCK);
|
|
220
|
+
capsLockState = !capsLockState;
|
|
221
|
+
}
|
|
222
|
+
array.push(atomKey);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (shiftState) array.push(ATOM.SHIFT);
|
|
226
|
+
if (!capsLockState) array.push(ATOM.LOCK);
|
|
227
|
+
return array;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function getKeyMapAtom(keyLayout) {
|
|
231
|
+
const keys2 = [];
|
|
232
|
+
|
|
233
|
+
// shift pressed
|
|
234
|
+
keys2[true] = {};
|
|
235
|
+
|
|
236
|
+
// shift not pressed
|
|
237
|
+
keys2[false] = {};
|
|
238
|
+
|
|
239
|
+
// shiftDown MUST be true or false (not undefined)
|
|
240
|
+
function doMap(s, colRow, shiftDown) {
|
|
241
|
+
if (keys2[shiftDown][s] && keys2[shiftDown][s] !== colRow) {
|
|
242
|
+
console.log(
|
|
243
|
+
"Warning: duplicate binding for atom key",
|
|
244
|
+
(shiftDown ? "<SHIFT>" : "") + s,
|
|
245
|
+
colRow,
|
|
246
|
+
keys2[shiftDown][s],
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
keys2[shiftDown][s] = colRow;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// shiftDown undefined -> map both
|
|
253
|
+
function map(s, colRow, shiftDown) {
|
|
254
|
+
if ((!s && s !== 0) || !colRow) {
|
|
255
|
+
console.log("error binding key", s, colRow);
|
|
256
|
+
}
|
|
257
|
+
if (typeof s === "string") {
|
|
258
|
+
s = s.charCodeAt(0);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (shiftDown === undefined) {
|
|
262
|
+
doMap(s, colRow, true);
|
|
263
|
+
doMap(s, colRow, false);
|
|
264
|
+
} else {
|
|
265
|
+
doMap(s, colRow, shiftDown);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
map(keyCodes.Q, ATOM.Q);
|
|
270
|
+
map(keyCodes.W, ATOM.W);
|
|
271
|
+
map(keyCodes.E, ATOM.E);
|
|
272
|
+
map(keyCodes.R, ATOM.R);
|
|
273
|
+
map(keyCodes.T, ATOM.T);
|
|
274
|
+
map(keyCodes.Y, ATOM.Y);
|
|
275
|
+
map(keyCodes.U, ATOM.U);
|
|
276
|
+
map(keyCodes.I, ATOM.I);
|
|
277
|
+
map(keyCodes.O, ATOM.O);
|
|
278
|
+
map(keyCodes.P, ATOM.P);
|
|
279
|
+
|
|
280
|
+
map(keyCodes.A, ATOM.A);
|
|
281
|
+
map(keyCodes.S, ATOM.S);
|
|
282
|
+
map(keyCodes.D, ATOM.D);
|
|
283
|
+
map(keyCodes.F, ATOM.F);
|
|
284
|
+
map(keyCodes.G, ATOM.G);
|
|
285
|
+
map(keyCodes.H, ATOM.H);
|
|
286
|
+
map(keyCodes.J, ATOM.J);
|
|
287
|
+
map(keyCodes.K, ATOM.K);
|
|
288
|
+
map(keyCodes.L, ATOM.L);
|
|
289
|
+
|
|
290
|
+
map(keyCodes.Z, ATOM.Z);
|
|
291
|
+
map(keyCodes.X, ATOM.X);
|
|
292
|
+
map(keyCodes.C, ATOM.C);
|
|
293
|
+
map(keyCodes.V, ATOM.V);
|
|
294
|
+
map(keyCodes.B, ATOM.B);
|
|
295
|
+
map(keyCodes.N, ATOM.N);
|
|
296
|
+
map(keyCodes.M, ATOM.M);
|
|
297
|
+
|
|
298
|
+
// these keys are in the same place on PC/Mac and ATOM keyboards
|
|
299
|
+
// including shifted characters
|
|
300
|
+
// so can be the same for "natural" and "gaming"
|
|
301
|
+
map(keyCodes.COMMA, ATOM.COMMA_LESSTHAN);
|
|
302
|
+
map(keyCodes.PERIOD, ATOM.PERIOD_GREATERTHAN);
|
|
303
|
+
map(keyCodes.SLASH, ATOM.SLASH_QUESTIONMARK);
|
|
304
|
+
map(keyCodes.SPACE, ATOM.SPACE);
|
|
305
|
+
map(keyCodes.ENTER, ATOM.RETURN);
|
|
306
|
+
|
|
307
|
+
// other keys to map to these in "game" layout too
|
|
308
|
+
map(keyCodes.F9, ATOM.CLEAR); // not actually on an ATOM keyboard
|
|
309
|
+
map(keyCodes.LEFT, ATOM.LEFT); // arrow left
|
|
310
|
+
map(keyCodes.RIGHT, ATOM.LEFT_RIGHT); // arrow right
|
|
311
|
+
map(keyCodes.DOWN, ATOM.DOWN); // arrow down
|
|
312
|
+
map(keyCodes.UP, ATOM.UP_DOWN); // arrow up
|
|
313
|
+
|
|
314
|
+
map(keyCodes.BACKSPACE, ATOM.DELETE); // delete
|
|
315
|
+
map(keyCodes.DELETE, ATOM.DELETE); // delete
|
|
316
|
+
|
|
317
|
+
map(keyCodes.ESCAPE, ATOM.ESCAPE);
|
|
318
|
+
map(keyCodes.TAB, ATOM.COPY);
|
|
319
|
+
|
|
320
|
+
map(keyCodes.F10, ATOM.REPT);
|
|
321
|
+
|
|
322
|
+
map(keyCodes.F1, ATOM.LOCK); // which is better for ATOM.LOCK - use all of them?
|
|
323
|
+
map(keyCodes.WINDOWS, ATOM.LOCK);
|
|
324
|
+
map(keyCodes.ALT_LEFT, ATOM.LOCK);
|
|
325
|
+
|
|
326
|
+
if (keyLayout === "natural") {
|
|
327
|
+
// "natural" keyboard
|
|
328
|
+
// Like a PC/Mac keyboard
|
|
329
|
+
|
|
330
|
+
// US Keyboard: has Tilde on <Shift>BACK_QUOTE
|
|
331
|
+
map(keyCodes.BACK_QUOTE, ATOM.UP_ARROW); // ` on PC, § on Mac
|
|
332
|
+
map(keyCodes.APOSTROPHE, isUKlayout ? ATOM.AT : ATOM.K2, true);
|
|
333
|
+
map(keyCodes.K2, isUKlayout ? ATOM.K2 : ATOM.AT, true);
|
|
334
|
+
|
|
335
|
+
// 1st row
|
|
336
|
+
map(keyCodes.K3, ATOM.K3, true);
|
|
337
|
+
map(keyCodes.K6, ATOM.UP_ARROW, true);
|
|
338
|
+
map(keyCodes.K7, ATOM.K6, true);
|
|
339
|
+
map(keyCodes.K8, ATOM.COLON_STAR, true);
|
|
340
|
+
map(keyCodes.K9, ATOM.K8, true);
|
|
341
|
+
map(keyCodes.K0, ATOM.K9, true);
|
|
342
|
+
|
|
343
|
+
map(keyCodes.K2, ATOM.K2, false);
|
|
344
|
+
map(keyCodes.K3, ATOM.K3, false);
|
|
345
|
+
map(keyCodes.K6, ATOM.K6, false);
|
|
346
|
+
map(keyCodes.K7, ATOM.K7, false);
|
|
347
|
+
map(keyCodes.K8, ATOM.K8, false);
|
|
348
|
+
map(keyCodes.K9, ATOM.K9, false);
|
|
349
|
+
map(keyCodes.K0, ATOM.K0, false);
|
|
350
|
+
|
|
351
|
+
map(keyCodes.K1, ATOM.K1);
|
|
352
|
+
map(keyCodes.K4, ATOM.K4);
|
|
353
|
+
map(keyCodes.K5, ATOM.K5);
|
|
354
|
+
|
|
355
|
+
// 3rd row
|
|
356
|
+
|
|
357
|
+
map(keyCodes.HASH, ATOM.BACKSLASH); // Atom has no # key; map to nearest
|
|
358
|
+
|
|
359
|
+
map(keyCodes.MINUS, ATOM.MINUS_EQUALS);
|
|
360
|
+
|
|
361
|
+
// 2nd row
|
|
362
|
+
map(keyCodes.LEFT_SQUARE_BRACKET, ATOM.LEFT_SQUARE_BRACKET);
|
|
363
|
+
|
|
364
|
+
map(keyCodes.RIGHT_SQUARE_BRACKET, ATOM.RIGHT_SQUARE_BRACKET);
|
|
365
|
+
|
|
366
|
+
// 3rd row
|
|
367
|
+
|
|
368
|
+
map(keyCodes.SEMICOLON, ATOM.SEMICOLON_PLUS);
|
|
369
|
+
|
|
370
|
+
map(keyCodes.APOSTROPHE, ATOM.COLON_STAR, false);
|
|
371
|
+
|
|
372
|
+
map(keyCodes.EQUALS, ATOM.SEMICOLON_PLUS); // OK for <Shift> at least
|
|
373
|
+
|
|
374
|
+
map(keyCodes.END, ATOM.COPY);
|
|
375
|
+
map(keyCodes.F11, ATOM.COPY);
|
|
376
|
+
|
|
377
|
+
map(keyCodes.CTRL, ATOM.CTRL);
|
|
378
|
+
map(keyCodes.CTRL_LEFT, ATOM.CTRL);
|
|
379
|
+
map(keyCodes.CTRL_RIGHT, ATOM.CTRL);
|
|
380
|
+
map(keyCodes.SHIFT, ATOM.SHIFT);
|
|
381
|
+
map(keyCodes.SHIFT_LEFT, ATOM.SHIFT);
|
|
382
|
+
map(keyCodes.SHIFT_RIGHT, ATOM.SHIFT);
|
|
383
|
+
|
|
384
|
+
map(keyCodes.BACKSLASH, ATOM.BACKSLASH);
|
|
385
|
+
} else if (keyLayout === "gaming") {
|
|
386
|
+
// gaming keyboard
|
|
387
|
+
|
|
388
|
+
// 1st row
|
|
389
|
+
map(keyCodes.ESCAPE, ATOM.ESCAPE);
|
|
390
|
+
|
|
391
|
+
// 2nd row
|
|
392
|
+
map(keyCodes.BACK_QUOTE, ATOM.ESCAPE);
|
|
393
|
+
map(keyCodes.K1, ATOM.K1);
|
|
394
|
+
map(keyCodes.K2, ATOM.K2);
|
|
395
|
+
map(keyCodes.K3, ATOM.K3);
|
|
396
|
+
map(keyCodes.K4, ATOM.K4);
|
|
397
|
+
map(keyCodes.K5, ATOM.K5);
|
|
398
|
+
map(keyCodes.K6, ATOM.K6);
|
|
399
|
+
map(keyCodes.K7, ATOM.K7);
|
|
400
|
+
map(keyCodes.K8, ATOM.K8);
|
|
401
|
+
map(keyCodes.K9, ATOM.K9);
|
|
402
|
+
map(keyCodes.K0, ATOM.K0);
|
|
403
|
+
map(keyCodes.MINUS, ATOM.MINUS_EQUALS);
|
|
404
|
+
map(keyCodes.EQUALS, ATOM.UP_ARROW);
|
|
405
|
+
map(keyCodes.BACKSPACE, ATOM.BACKSLASH);
|
|
406
|
+
map(keyCodes.INSERT, ATOM.LEFT);
|
|
407
|
+
map(keyCodes.HOME, ATOM.RIGHT);
|
|
408
|
+
|
|
409
|
+
// 3rd row
|
|
410
|
+
map(keyCodes.LEFT_SQUARE_BRACKET, ATOM.AT);
|
|
411
|
+
map(keyCodes.RIGHT_SQUARE_BRACKET, ATOM.LEFT_SQUARE_BRACKET);
|
|
412
|
+
// no key for ATOM.UNDERSCORE_POUND in UK
|
|
413
|
+
// see 4th row for US mapping keyCodes.BACKSLASH
|
|
414
|
+
map(keyCodes.DELETE, ATOM.UP);
|
|
415
|
+
map(keyCodes.END, ATOM.DOWN);
|
|
416
|
+
|
|
417
|
+
// 4th row
|
|
418
|
+
// no key for ATOM.CAPSLOCK (mapped to CTRL_LEFT below)
|
|
419
|
+
map(keyCodes.CAPSLOCK, ATOM.CTRL);
|
|
420
|
+
map(keyCodes.SEMICOLON, ATOM.SEMICOLON_PLUS);
|
|
421
|
+
map(keyCodes.APOSTROPHE, ATOM.COLON_STAR);
|
|
422
|
+
// UK keyboard (key missing on US)
|
|
423
|
+
map(keyCodes.HASH, ATOM.RIGHT_SQUARE_BRACKET);
|
|
424
|
+
|
|
425
|
+
// UK has extra key \| for SHIFT
|
|
426
|
+
map(keyCodes.SHIFT_LEFT, isUKlayout ? ATOM.LOCK : ATOM.SHIFT);
|
|
427
|
+
// UK: key is between SHIFT and Z
|
|
428
|
+
// US: key is above ENTER
|
|
429
|
+
map(keyCodes.BACKSLASH, isUKlayout ? ATOM.SHIFT : ATOM.BACKSLASH);
|
|
430
|
+
|
|
431
|
+
// 5th row
|
|
432
|
+
|
|
433
|
+
// Atom uses CTRL as shift, so map PC Ctrl to Atom's LOCK key
|
|
434
|
+
map(keyCodes.CTRL_LEFT, ATOM.LOCK);
|
|
435
|
+
map(keyCodes.SHIFT, ATOM.CTRL);
|
|
436
|
+
|
|
437
|
+
// ATOM.DELETE is covered by the common mapping above
|
|
438
|
+
map(keyCodes.CTRL_RIGHT, ATOM.COPY);
|
|
439
|
+
} else {
|
|
440
|
+
// Physical, and default
|
|
441
|
+
// Like a real ATOM
|
|
442
|
+
// mainly the CTRL key is still CTRL (as CAPSLOCK locks on the MAC)
|
|
443
|
+
// UP/DOWN/LEFT/RIGHT are using arrow keys
|
|
444
|
+
// REPT is using RIGHT_SHIFT
|
|
445
|
+
// note: LOCK is on LEFT_ALT
|
|
446
|
+
map(keyCodes.K1, ATOM.K1);
|
|
447
|
+
map(keyCodes.K2, ATOM.K2);
|
|
448
|
+
map(keyCodes.K3, ATOM.K3);
|
|
449
|
+
map(keyCodes.K4, ATOM.K4);
|
|
450
|
+
map(keyCodes.K5, ATOM.K5);
|
|
451
|
+
map(keyCodes.K6, ATOM.K6);
|
|
452
|
+
map(keyCodes.K7, ATOM.K7);
|
|
453
|
+
map(keyCodes.K8, ATOM.K8);
|
|
454
|
+
map(keyCodes.K9, ATOM.K9);
|
|
455
|
+
map(keyCodes.K0, ATOM.K0);
|
|
456
|
+
map(keyCodes.MINUS, ATOM.MINUS_EQUALS); // - / _ becomes - / =
|
|
457
|
+
map(keyCodes.EQUALS, ATOM.COLON_STAR); // = / + becomes : / *
|
|
458
|
+
//BREAK is code in 'main.js' to F12
|
|
459
|
+
|
|
460
|
+
// Q-P normal
|
|
461
|
+
map(keyCodes.LEFT_SQUARE_BRACKET, ATOM.AT); // maps to @
|
|
462
|
+
map(keyCodes.RIGHT_SQUARE_BRACKET, ATOM.BACKSLASH); // maps to \
|
|
463
|
+
|
|
464
|
+
map(keyCodes.SHIFT, ATOM.CTRL);
|
|
465
|
+
map(keyCodes.SHIFT_LEFT, ATOM.CTRL); // using CAPSLOCK for CTRL doesn't work on MAC
|
|
466
|
+
map(keyCodes.CTRL, ATOM.SHIFT);
|
|
467
|
+
map(keyCodes.CTRL_LEFT, ATOM.SHIFT);
|
|
468
|
+
|
|
469
|
+
map(keyCodes.CTRL_RIGHT, ATOM.SHIFT);
|
|
470
|
+
map(keyCodes.SHIFT_RIGHT, ATOM.REPT);
|
|
471
|
+
|
|
472
|
+
// A-L normal
|
|
473
|
+
map(keyCodes.SEMICOLON, ATOM.SEMICOLON_PLUS); // ; / +
|
|
474
|
+
map(keyCodes.APOSTROPHE, ATOM.LEFT_SQUARE_BRACKET);
|
|
475
|
+
map(keyCodes.BACKSLASH, ATOM.RIGHT_SQUARE_BRACKET); // HASH is \| key on Mac
|
|
476
|
+
|
|
477
|
+
// Z - M normal
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// user keymapping
|
|
481
|
+
// do last (to override defaults)
|
|
482
|
+
while (userKeymap.length > 0) {
|
|
483
|
+
const mapping = userKeymap.pop();
|
|
484
|
+
map(keyCodes[mapping.native], ATOM[mapping.atom]);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
return keys2;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
export function remapGamepad(gamepad) {
|
|
491
|
+
//mmcdefaults
|
|
492
|
+
|
|
493
|
+
// 3-key pressed left
|
|
494
|
+
// G-key pressed right
|
|
495
|
+
// Q-key pressed up
|
|
496
|
+
// =-key pressed down
|
|
497
|
+
// rightarrow-key pressed fire
|
|
498
|
+
|
|
499
|
+
gamepad.gamepadMapping[14] = ATOM.K3;
|
|
500
|
+
gamepad.gamepadMapping[15] = ATOM.G;
|
|
501
|
+
gamepad.gamepadMapping[13] = ATOM.MINUS_EQUALS;
|
|
502
|
+
gamepad.gamepadMapping[12] = ATOM.Q;
|
|
503
|
+
|
|
504
|
+
// button 0 = right arrow key (fire in many Atom games)
|
|
505
|
+
gamepad.gamepadMapping[0] = ATOM.RIGHT;
|
|
506
|
+
// "start" (often <Space> to start game)
|
|
507
|
+
gamepad.gamepadMapping[9] = ATOM.SPACE;
|
|
508
|
+
}
|
package/src/video.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { Teletext } from "./teletext.js";
|
|
3
3
|
import * as utils from "./utils.js";
|
|
4
4
|
import { BbcDefaultPalette as NulaDefaultPalette } from "./bbc-palette.js";
|
|
5
|
+
import { Video6847 } from "./6847.js";
|
|
5
6
|
|
|
6
7
|
export const VDISPENABLE = 1 << 0;
|
|
7
8
|
export const HDISPENABLE = 1 << 1;
|
|
@@ -326,7 +327,7 @@ function table4bppOffset(ulamode, byte) {
|
|
|
326
327
|
////////////////////
|
|
327
328
|
// The video class
|
|
328
329
|
export class Video {
|
|
329
|
-
constructor(isMaster, fb32_param, paint_ext_param) {
|
|
330
|
+
constructor(isMaster, fb32_param, paint_ext_param, { isAtom = false } = {}) {
|
|
330
331
|
this.isMaster = isMaster;
|
|
331
332
|
this.fb32 = utils.makeFast32(fb32_param);
|
|
332
333
|
this.collook = utils.makeFast32(
|
|
@@ -422,6 +423,13 @@ export class Video {
|
|
|
422
423
|
this.crtc = new Crtc(this);
|
|
423
424
|
this.ula = new Ula(this);
|
|
424
425
|
|
|
426
|
+
// Atom: attach the MC6847 VDG and use its polltime
|
|
427
|
+
this.video6847 = null;
|
|
428
|
+
if (isAtom) {
|
|
429
|
+
this.video6847 = new Video6847(this);
|
|
430
|
+
this.polltime = this.video6847.polltimeFacade;
|
|
431
|
+
}
|
|
432
|
+
|
|
425
433
|
this.reset(null);
|
|
426
434
|
this.clearPaintBuffer();
|
|
427
435
|
this.paint();
|
|
@@ -536,6 +544,9 @@ export class Video {
|
|
|
536
544
|
this.cpu = cpu;
|
|
537
545
|
this.sysvia = via;
|
|
538
546
|
if (via) via.cb2changecallback = this.cb2changed.bind(this);
|
|
547
|
+
if (this.video6847 && cpu) {
|
|
548
|
+
this.video6847.reset(cpu, cpu.atomppia);
|
|
549
|
+
}
|
|
539
550
|
}
|
|
540
551
|
|
|
541
552
|
paint() {
|
package/src/web/audio-handler.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { FakeSoundChip, SoundChip } from "../soundchip.js";
|
|
1
|
+
import { FakeSoundChip, SoundChip, AtomSoundChip } from "../soundchip.js";
|
|
3
2
|
import { DdNoise, FakeDdNoise } from "../ddnoise.js";
|
|
4
3
|
import { RelayNoise, FakeRelayNoise } from "../relaynoise.js";
|
|
5
4
|
import { Music5000, FakeMusic5000 } from "../music5000.js";
|
|
@@ -12,25 +11,27 @@ const rendererUrl = new URL("./audio-renderer.js", import.meta.url).href;
|
|
|
12
11
|
const music5000WorkletUrl = new URL("../music5000-worklet.js", import.meta.url).href;
|
|
13
12
|
|
|
14
13
|
export class AudioHandler {
|
|
15
|
-
constructor({ warningNode, statsNode, audioFilterFreq, audioFilterQ, noSeek } = {}) {
|
|
14
|
+
constructor({ warningNode, statsNode, audioFilterFreq, audioFilterQ, noSeek, cpuSpeed, isAtom } = {}) {
|
|
15
|
+
this.cpuSpeed = cpuSpeed;
|
|
16
|
+
this.isAtom = isAtom;
|
|
16
17
|
this.warningNode = warningNode;
|
|
17
18
|
toggle(this.warningNode, false);
|
|
18
|
-
this.chart = new SmoothieChart({
|
|
19
|
-
tooltip: true,
|
|
20
|
-
labels: { precision: 0 },
|
|
21
|
-
yRangeFunction: (range) => {
|
|
22
|
-
return { min: 0, max: range.max };
|
|
23
|
-
},
|
|
24
|
-
});
|
|
25
19
|
this.stats = {};
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
20
|
+
if (statsNode) {
|
|
21
|
+
this._initStats(statsNode).catch((error) => {
|
|
22
|
+
console.error("Unable to initialise audio stats", error);
|
|
23
|
+
this.stats = {};
|
|
24
|
+
toggle(statsNode, false);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
29
27
|
this.audioContext = createAudioContext();
|
|
30
28
|
this._jsAudioNode = null;
|
|
31
29
|
if (this.audioContext && this.audioContext.audioWorklet) {
|
|
32
30
|
this.audioContext.onstatechange = () => this.checkStatus();
|
|
33
|
-
|
|
31
|
+
const onBuffer = (buffer, time) => this._onBuffer(buffer, time);
|
|
32
|
+
this.soundChip = this.isAtom
|
|
33
|
+
? new AtomSoundChip(onBuffer, { cpuSpeed: this.cpuSpeed })
|
|
34
|
+
: new SoundChip(onBuffer);
|
|
34
35
|
// Master gain node for all sample-based audio (disc, relay, etc.).
|
|
35
36
|
this.masterGain = this.audioContext.createGain();
|
|
36
37
|
this.masterGain.connect(this.audioContext.destination);
|
|
@@ -75,6 +76,22 @@ export class AudioHandler {
|
|
|
75
76
|
}
|
|
76
77
|
}
|
|
77
78
|
|
|
79
|
+
// Lazily load smoothie and set up the audio stats chart.
|
|
80
|
+
async _initStats(statsNode) {
|
|
81
|
+
const { SmoothieChart, TimeSeries } = await import("smoothie");
|
|
82
|
+
this._TimeSeries = TimeSeries;
|
|
83
|
+
this.chart = new SmoothieChart({
|
|
84
|
+
tooltip: true,
|
|
85
|
+
labels: { precision: 0 },
|
|
86
|
+
yRangeFunction: (range) => {
|
|
87
|
+
return { min: 0, max: range.max };
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
this._addStat("queueSize", { strokeStyle: "rgb(51,126,108)" });
|
|
91
|
+
this._addStat("queueAge", { strokeStyle: "rgb(162,119,22)" });
|
|
92
|
+
this.chart.streamTo(statsNode, 100);
|
|
93
|
+
}
|
|
94
|
+
|
|
78
95
|
async _setup(audioFilterFreq, audioFilterQ) {
|
|
79
96
|
await this.audioContext.audioWorklet.addModule(rendererUrl);
|
|
80
97
|
if (audioFilterFreq !== 0) {
|
|
@@ -99,7 +116,7 @@ export class AudioHandler {
|
|
|
99
116
|
}
|
|
100
117
|
|
|
101
118
|
_addStat(stat, info) {
|
|
102
|
-
const timeSeries = new
|
|
119
|
+
const timeSeries = new this._TimeSeries();
|
|
103
120
|
this.stats[stat] = timeSeries;
|
|
104
121
|
info.tooltipLabel = stat;
|
|
105
122
|
this.chart.addTimeSeries(timeSeries, info);
|
|
@@ -110,9 +127,14 @@ export class AudioHandler {
|
|
|
110
127
|
}
|
|
111
128
|
|
|
112
129
|
// Recent browsers, particularly Safari and Chrome, require a user interaction in order to enable sound playback.
|
|
130
|
+
// Errors are swallowed — resume() can fail due to autoplay policy and callers can't do anything about it.
|
|
113
131
|
async tryResume() {
|
|
114
|
-
|
|
115
|
-
|
|
132
|
+
try {
|
|
133
|
+
if (this.audioContext) await this.audioContext.resume();
|
|
134
|
+
if (this.audioContextM5000) await this.audioContextM5000.resume();
|
|
135
|
+
} catch {
|
|
136
|
+
// Autoplay policy prevented resume; will retry on next user gesture.
|
|
137
|
+
}
|
|
116
138
|
}
|
|
117
139
|
|
|
118
140
|
_onBufferMusic5000(buffer) {
|