jsbeeb 1.23.0 → 1.24.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.
Files changed (110) hide show
  1. package/README.md +2 -2
  2. package/package.json +14 -10
  3. package/src/6502.js +39 -42
  4. package/src/6502.opcodes.js +7 -13
  5. package/src/6847.js +4 -5
  6. package/src/6847_fontdata.js +0 -2
  7. package/src/acia.js +2 -3
  8. package/src/app/app.js +0 -1
  9. package/src/app/args.js +0 -2
  10. package/src/app/electron.js +4 -6
  11. package/src/app-bench.js +1 -3
  12. package/src/archive.js +243 -0
  13. package/src/basic-loader.js +0 -7
  14. package/src/basic-tokenise.js +19 -6
  15. package/src/bbc-palette.js +0 -2
  16. package/src/bbcdiscs.js +0 -2
  17. package/src/bem-snapshot.js +1 -3
  18. package/src/binary.js +175 -0
  19. package/src/cmos.js +0 -2
  20. package/src/ddnoise.js +0 -1
  21. package/src/debounce.js +7 -0
  22. package/src/disc-surface.js +0 -2
  23. package/src/disc.js +10 -9
  24. package/src/fake6502.js +7 -23
  25. package/src/fdc.js +9 -51
  26. package/src/filestore.js +10 -17
  27. package/src/hex.js +38 -0
  28. package/src/index.js +11 -0
  29. package/src/intel-fdc.js +18 -20
  30. package/src/{utils_atom.js → keymap-atom.js} +2 -18
  31. package/src/{utils.js → keymap.js} +47 -610
  32. package/src/loader.js +63 -0
  33. package/src/machine-session.js +37 -177
  34. package/src/machine-spec.js +59 -0
  35. package/src/main.js +66 -180
  36. package/src/media-resolver.js +68 -0
  37. package/src/mmc.js +3 -2
  38. package/src/models.js +17 -3
  39. package/src/music5000.js +0 -2
  40. package/src/ppia.js +3 -4
  41. package/src/printer.js +7 -14
  42. package/src/relaynoise.js +0 -1
  43. package/src/rewind.js +0 -2
  44. package/src/sample-player.js +2 -3
  45. package/src/serial.js +0 -2
  46. package/src/snapshot-helpers.js +0 -2
  47. package/src/snapshot.js +0 -2
  48. package/src/soundchip.js +1 -1
  49. package/src/state-utils.js +0 -2
  50. package/src/sth.js +2 -4
  51. package/src/tapes.js +10 -4
  52. package/src/teletext.js +2 -3
  53. package/src/teletext_adaptor.js +2 -4
  54. package/src/teletext_data.js +0 -2
  55. package/src/test-machine.js +307 -0
  56. package/src/touchscreen.js +2 -4
  57. package/src/tube.js +5 -6
  58. package/src/typist.js +103 -0
  59. package/src/uef-snapshot.js +0 -2
  60. package/src/url-params.js +4 -5
  61. package/src/vdu-capture.js +113 -0
  62. package/src/via.js +3 -4
  63. package/src/video-filters/pal-composite.js +0 -2
  64. package/src/video-filters/passthrough-filter.js +0 -2
  65. package/src/video-filters/pixel-grid.js +0 -2
  66. package/src/video-filters/shader-program.js +0 -2
  67. package/src/video-filters/xbr-filter.js +0 -2
  68. package/src/video.js +12 -11
  69. package/src/wd-fdc.js +20 -20
  70. package/src/web/analogue-inputs.js +6 -8
  71. package/src/web/analytics.js +13 -0
  72. package/src/web/audio-handler.js +2 -2
  73. package/src/{audio-utils.js → web/audio-utils.js} +0 -2
  74. package/src/web/autoboot.js +10 -13
  75. package/src/{canvas.js → web/canvas.js} +4 -5
  76. package/src/web/config.js +41 -74
  77. package/src/web/console-surface.js +25 -0
  78. package/src/web/debug.js +6 -5
  79. package/src/web/disc-visualiser.js +0 -2
  80. package/src/web/display.js +21 -4
  81. package/src/{dom-utils.js → web/dom-utils.js} +1 -11
  82. package/src/web/drives.js +1 -1
  83. package/src/web/emulation-loop.js +54 -33
  84. package/src/web/front-panel.js +13 -12
  85. package/src/{gamepads.js → web/gamepads.js} +2 -6
  86. package/src/web/google-drive-picker.js +14 -9
  87. package/src/{google-drive.js → web/google-drive.js} +9 -10
  88. package/src/web/hfe-picker.js +5 -4
  89. package/src/web/keyboard-setup.js +18 -38
  90. package/src/{keyboard.js → web/keyboard.js} +18 -119
  91. package/src/web/layout.js +4 -0
  92. package/src/web/local-disc.js +44 -0
  93. package/src/web/machine.js +27 -20
  94. package/src/web/media-loader.js +38 -147
  95. package/src/{microphone-input.js → web/microphone-input.js} +1 -1
  96. package/src/web/modals.js +6 -9
  97. package/src/web/page-actions.js +88 -0
  98. package/src/web/quick-settings.js +16 -15
  99. package/src/web/reporting.js +0 -7
  100. package/src/{rewind-thumbnail.js → web/rewind-thumbnail.js} +0 -2
  101. package/src/web/rewind-ui.js +29 -29
  102. package/src/web/run-controls.js +39 -0
  103. package/src/web/settings.js +64 -135
  104. package/src/web/snapshot-ui.js +10 -16
  105. package/src/{speech-output.js → web/speech-output.js} +0 -2
  106. package/src/web/sth-picker.js +9 -7
  107. package/src/web/url-state.js +20 -6
  108. package/src/econet/L3FS.dat +0 -0
  109. package/src/econet/scsi.dat +0 -0
  110. package/tests/test-machine.js +0 -609
package/src/main.js CHANGED
@@ -3,10 +3,8 @@ import { version } from "../package.json";
3
3
  import "bootswatch/dist/darkly/bootstrap.min.css";
4
4
  import "./jsbeeb.css";
5
5
 
6
- import * as utils from "./utils.js";
7
6
  import { Debugger } from "./web/debug.js";
8
- import * as utils_atom from "./utils_atom.js";
9
- import { GamePad } from "./gamepads.js";
7
+ import { GamePad } from "./web/gamepads.js";
10
8
  import { initialise as electron } from "./app/electron.js";
11
9
  import { AudioHandler } from "./web/audio-handler.js";
12
10
  import { installIcons } from "./web/icons.js";
@@ -21,7 +19,9 @@ import { isSnapshotFile, SnapshotUI } from "./web/snapshot-ui.js";
21
19
  import { Autoboot } from "./web/autoboot.js";
22
20
  import { Display } from "./web/display.js";
23
21
  import { Layout } from "./web/layout.js";
24
- import { EmulationLoop, RewindCaptureInterval } from "./web/emulation-loop.js";
22
+ import { EmulationLoop } from "./web/emulation-loop.js";
23
+ import { RunControls } from "./web/run-controls.js";
24
+ import { exposeConsoleSurface } from "./web/console-surface.js";
25
25
  import { KeyboardSetup } from "./web/keyboard-setup.js";
26
26
  import { AccessibilitySwitches } from "./web/accessibility-switches.js";
27
27
  import { AnalogueInputs } from "./web/analogue-inputs.js";
@@ -31,14 +31,17 @@ import { Drives } from "./web/drives.js";
31
31
  import { UrlState } from "./web/url-state.js";
32
32
  import { Modals } from "./web/modals.js";
33
33
  import { Settings } from "./web/settings.js";
34
+ import { Config } from "./web/config.js";
35
+ import { SpeechOutput } from "./web/speech-output.js";
34
36
  import { Printer } from "./printer.js";
35
- import { RewindBuffer } from "./rewind.js";
36
37
  import { RewindUI } from "./web/rewind-ui.js";
37
38
  import { DiscVisualiser } from "./web/disc-visualiser.js";
38
- import { downloadDriveData } from "./dom-utils.js";
39
+ import { PageActions } from "./web/page-actions.js";
39
40
  import { parseMediaParams, processAutobootParams, processDriveTrackParams, processInputParams } from "./url-params.js";
41
+ import { adaptKeyCodesToBrowser, keyCodes, userKeymap } from "./keymap.js";
40
42
 
41
43
  installIcons();
44
+ adaptKeyCodesToBrowser();
42
45
 
43
46
  // ------------------------------------------------------------------------
44
47
  // What the URL asked for.
@@ -70,11 +73,6 @@ const stationId = parsedQuery.stationId !== undefined ? parsedQuery.stationId :
70
73
  const tryGl = parsedQuery.glEnabled ?? true;
71
74
  const lowLatency = parsedQuery.lowLatency ?? true;
72
75
 
73
- if (parsedQuery.embed) {
74
- for (const el of document.querySelectorAll(".embed-hide")) el.style.display = "none";
75
- document.body.style.backgroundColor = "transparent";
76
- }
77
-
78
76
  // ------------------------------------------------------------------------
79
77
  // The pieces that exist before the machine: settings, screen, sound.
80
78
  // ------------------------------------------------------------------------
@@ -88,29 +86,26 @@ if (!window.isSecureContext)
88
86
  quietKey: "quietInsecureGamepads",
89
87
  });
90
88
 
91
- const printer = new Printer({
92
- onOutput: (char) => frontPanel.printChar(char),
93
- onFirstOutput: () =>
94
- toast("Printer output is being kept. Press Ctrl-B to open the printer window.", {
95
- title: "Printer",
96
- quietKey: "quietPrinterOutput",
97
- }),
98
- });
89
+ const printer = new Printer();
99
90
 
100
91
  const accessibilitySwitches = new AccessibilitySwitches();
101
92
 
102
93
  const settings = new Settings({ urlState });
103
- const { config, speechOutput, keyLayout, displayMode, audioOutput, speakerAmount } = settings;
94
+ const { keyLayout, displayMode, audioOutput, speakerAmount } = settings;
104
95
  const model = settings.model;
96
+ const config = new Config(settings);
97
+
98
+ const speechOutput = new SpeechOutput();
99
+ const speak = (enabled) => {
100
+ speechOutput.enabled = enabled;
101
+ if (enabled && typeof speechSynthesis === "undefined")
102
+ toast("This browser has no speech synthesis, so speech output has nothing to speak with.", { title: "Speech" });
103
+ };
104
+ speak(settings.speechOutput);
105
+ settings.on("speechOutput", speak);
105
106
 
106
107
  // Must come after we know the model, to validate names against those of the hardware.
107
- const keyMappingWarnings = processInputParams(
108
- parsedQuery,
109
- model.isAtom ? utils_atom.ATOM : utils.BBC,
110
- utils.keyCodes,
111
- utils.userKeymap,
112
- gamepad,
113
- );
108
+ const keyMappingWarnings = processInputParams(parsedQuery, model.keys, keyCodes, userKeymap, gamepad);
114
109
  if (keyMappingWarnings.length) {
115
110
  toast(`${keyMappingWarnings.join(" ")} The key names are listed in the README.`, {
116
111
  title: "Mappings in the URL",
@@ -152,21 +147,14 @@ const audioHandler = new AudioHandler({
152
147
  noSeek,
153
148
  cpuSpeed,
154
149
  isAtom: model.isAtom,
155
- hasMusic5000: config.hasMusic5000,
150
+ hasMusic5000: settings.hasMusic5000,
156
151
  });
157
152
  // Firefox will report that audio is suspended even when it will
158
153
  // start playing without user interaction, so we need to delay a
159
154
  // little to get a reliable indication.
160
155
  window.setTimeout(() => audioHandler.checkStatus(), 1000);
161
156
 
162
- const quickSettings = new QuickSettings(
163
- {
164
- onAudioOutput: (output) => settings.applyAudioOutput(output),
165
- onSpeakerAmount: (amount) => settings.applySpeakerAmount(amount),
166
- onDisplayMode: (mode) => settings.applyDisplayMode(mode),
167
- },
168
- { audioOutput, speakerAmount, displayMode },
169
- );
157
+ new QuickSettings(settings);
170
158
 
171
159
  // ------------------------------------------------------------------------
172
160
  // The machine, the loop that runs it, and everything that feeds it.
@@ -175,7 +163,7 @@ const quickSettings = new QuickSettings(
175
163
  // Depends on the settings having applied the URL parameters.
176
164
  const machine = new Machine({
177
165
  model,
178
- config,
166
+ settings,
179
167
  parsedQuery,
180
168
  keyLayout,
181
169
  cpuMultiplier,
@@ -190,7 +178,7 @@ const machine = new Machine({
190
178
  });
191
179
  const processor = machine.processor;
192
180
 
193
- const keys = new KeyboardSetup({
181
+ const { keyboard } = new KeyboardSetup({
194
182
  actions: {
195
183
  enterDebugger: () => loop.stop(true),
196
184
  reload: () => window.location.reload(),
@@ -199,7 +187,7 @@ const keys = new KeyboardSetup({
199
187
  openPrinter: () => frontPanel.checkPrinterWindow(),
200
188
  pause: () => loop.stop(false),
201
189
  resume: () => loop.go(),
202
- paste: (text) => keys.sendRawKeyboard(autoBoot.stringToMachineKeys(text), true),
190
+ paste: (text) => keyboard.sendRawKeyboard(autoBoot.stringToMachineKeys(text), true),
203
191
  onAnyKeyDown: () => {
204
192
  audioHandler.tryResume();
205
193
  inputs.ensureMicrophoneRunning();
@@ -211,31 +199,20 @@ const keys = new KeyboardSetup({
211
199
  keyLayout,
212
200
  });
213
201
 
214
- const rewindBuffer = new RewindBuffer(30);
215
202
  const loop = new EmulationLoop({
216
203
  processor,
217
204
  display,
218
205
  audioHandler,
219
206
  dbgr,
220
207
  gamepad,
221
- keyboard: keys,
222
- syncLights: () => frontPanel.syncLights(),
223
- rewindBuffer,
224
- onRewindCaptured: () => rewindUI.updateButtonState(),
208
+ keyboard,
225
209
  clocksPerSecond,
226
210
  cpuSpeed,
227
211
  fastTape: !!parsedQuery.fasttape,
228
212
  audioStatsNode,
229
213
  });
230
214
 
231
- const debugPause = document.getElementById("debug-pause");
232
- const debugPlay = document.getElementById("debug-play");
233
- loop.addEventListener("running", () => {
234
- const running = loop.isRunning();
235
- keys.setRunning(running);
236
- debugPlay.disabled = running;
237
- debugPause.disabled = !running;
238
- });
215
+ const runControls = new RunControls({ loop, dbgr, keyboard });
239
216
 
240
217
  const modals = new Modals({ loop });
241
218
 
@@ -252,7 +229,7 @@ const media = new MediaLoader({
252
229
  const autoBoot = new Autoboot({
253
230
  model,
254
231
  processor,
255
- sendKeys: (keysToSend, check) => keys.sendRawKeyboard(keysToSend, check),
232
+ sendKeys: (keysToSend, check) => keyboard.sendRawKeyboard(keysToSend, check),
256
233
  });
257
234
  const autobootTicks = new AutobootTicks({ urlState });
258
235
  const sthPicker = new SthPicker({
@@ -287,12 +264,11 @@ const snapshots = new SnapshotUI({
287
264
  const inputs = new AnalogueInputs({
288
265
  processor,
289
266
  screenCanvas,
290
- getGamepads: machine.emulationConfig.getGamepads,
291
- urlState,
292
- config,
267
+ getGamepads: machine.spec.getGamepads,
268
+ settings,
293
269
  audioHandler,
294
270
  });
295
- if (parsedQuery.microphoneChannel !== undefined) {
271
+ if (settings.microphoneChannel !== undefined) {
296
272
  // We need to use setTimeout to make sure this runs after the page has loaded
297
273
  // This is needed because some browsers require user interaction for audio context
298
274
  setTimeout(async () => {
@@ -300,105 +276,54 @@ if (parsedQuery.microphoneChannel !== undefined) {
300
276
  }, 1000);
301
277
  }
302
278
  // Apply ADC source settings from URL parameters
303
- inputs.updateAdcSources(parsedQuery.mouseJoystickEnabled, parsedQuery.microphoneChannel);
279
+ inputs.updateAdcSources(settings.mouseJoystickEnabled, settings.microphoneChannel);
304
280
 
305
- const frontPanel = new FrontPanel({ processor, model, printer });
281
+ const frontPanel = new FrontPanel({ processor, model, printer, loop });
306
282
 
307
283
  // ------------------------------------------------------------------------
308
284
  // Rewind, the visualiser and the layout.
309
285
  // ------------------------------------------------------------------------
310
286
 
311
- const rewindUI = new RewindUI({
312
- rewindBuffer,
313
- processor,
314
- video,
315
- captureInterval: RewindCaptureInterval,
316
- loop,
317
- });
287
+ const rewindUI = new RewindUI({ processor, video, loop });
318
288
  rewindUI.updateButtonState();
319
289
 
320
- if (processor.fdc) new DiscVisualiser({ fdc: processor.fdc });
321
- else document.getElementById("disc-visualiser-open").classList.add("disabled");
290
+ new DiscVisualiser({ fdc: processor.fdc });
322
291
 
323
292
  const layout = new Layout({
324
293
  screenCanvas,
325
294
  display,
326
- embed: parsedQuery.embed !== undefined,
295
+ embed: !!parsedQuery.embed,
327
296
  sidebars: { left: parsedQuery.sbLeft, right: parsedQuery.sbRight, bottom: parsedQuery.sbBottom },
328
297
  });
329
298
 
330
299
  // Everything a setting can reach now exists.
331
- settings.wire({ audioHandler, display, layout, quickSettings, machine, keys, inputs, modals });
332
-
333
- // ------------------------------------------------------------------------
334
- // The page's own handlers.
335
- // ------------------------------------------------------------------------
336
-
337
- for (const el of document.querySelectorAll(".initially-hidden")) el.classList.remove("initially-hidden");
338
-
339
- document.getElementById("paste-form").addEventListener("submit", (event) => event.preventDefault());
340
-
341
- window.addEventListener("blur", function () {
342
- keys.clearKeys();
343
- loop.setEmulationLead(audioHandler.setWindowFocused(false));
300
+ settings.on("audioOutput", (output) => audioHandler.setAudioOutput(output));
301
+ settings.on("speakerAmount", (amount) => audioHandler.setSpeakerAmount(amount));
302
+ settings.on("displayMode", (mode) => {
303
+ display.setMode(mode);
304
+ // The monitor picture may have changed shape.
305
+ layout.resize();
344
306
  });
345
- window.addEventListener("focus", () => loop.setEmulationLead(audioHandler.setWindowFocused(true)));
346
-
347
- function pauseIntoDebugger() {
348
- loop.stop(true);
349
- }
350
-
351
- function resumeFromDebugger() {
352
- dbgr.hide();
353
- keys.resumeEmulation();
354
- }
355
-
356
- debugPause.addEventListener("click", pauseIntoDebugger);
357
- debugPlay.addEventListener("click", resumeFromDebugger);
358
-
359
- // To lower chance of data loss, only accept drop events in the drop
360
- // zone in the menu bar.
361
- document.addEventListener("dragover", function (event) {
362
- event.preventDefault();
363
- event.dataTransfer.dropEffect = "none";
364
- });
365
- document.addEventListener("drop", function (event) {
366
- event.preventDefault();
307
+ settings.on("keyLayout", (chosen) => keyboard.setKeyLayout(chosen));
308
+ settings.on("tubeCpuMultiplier", (multiplier) => {
309
+ if (processor.hasTube) processor.tube.cpuMultiplier = multiplier;
367
310
  });
368
-
369
- window.addEventListener("beforeunload", function (event) {
370
- if (loop.isRunning() && processor.sysvia.hasAnyKeyDown()) {
371
- const message =
372
- "It seems like you're still using the emulator. If you're in Chrome, it's impossible for jsbeeb to prevent some shortcuts (like ctrl-W) from performing their default behaviour (e.g. closing the window).\n" +
373
- "As a workarond, create an 'Application Shortcut' from the Tools menu. When jsbeeb runs as an application, it *can* prevent ctrl-W from closing the window.";
374
- event.preventDefault();
375
- event.returnValue = message;
376
- return message;
377
- }
378
- });
379
-
380
- function hardReset() {
381
- rewindUI.reset();
382
- processor.reset(true);
383
- }
384
-
385
- document.getElementById("hard-reset").addEventListener("click", function (event) {
386
- hardReset();
387
- event.preventDefault();
311
+ const rerouteAnalogue = () => inputs.updateAdcSources(settings.mouseJoystickEnabled, settings.microphoneChannel);
312
+ settings.on("mouseJoystickEnabled", rerouteAnalogue);
313
+ settings.on("microphoneChannel", (channel) => {
314
+ rerouteAnalogue();
315
+ if (channel !== undefined) inputs.setupMicrophone();
388
316
  });
389
-
390
- document.getElementById("soft-reset").addEventListener("click", function (event) {
391
- processor.reset(false);
392
- event.preventDefault();
393
- });
394
-
395
- document.getElementById("download-filestore-link").addEventListener("click", function () {
396
- if (!processor.filestore) return;
397
- downloadDriveData(processor.filestore.scsi, "scsi", ".dat");
317
+ config.addEventListener("restart-required", async () => {
318
+ const restart = await modals.confirm(
319
+ "Your change is saved, but only takes effect when the emulator restarts. Restart now?",
320
+ "Restart now",
321
+ "Later",
322
+ );
323
+ if (restart) window.location.reload();
398
324
  });
399
325
 
400
- if (Object.hasOwn(parsedQuery, "about")) modals.show("info");
401
- if (Object.hasOwn(parsedQuery, "pp-tos")) modals.show("pp-tos");
326
+ const page = new PageActions({ loop, processor, keyboard, audioHandler, rewindUI, modals, parsedQuery, version });
402
327
 
403
328
  // ------------------------------------------------------------------------
404
329
  // Start it up.
@@ -460,47 +385,14 @@ const startPromise = machine.start({
460
385
  // The console surface the wiki documents, and the desktop app's hooks.
461
386
  // ------------------------------------------------------------------------
462
387
 
463
- // Handy shortcuts. bench/profile stuff is delayed so that they can be
464
- // safely run from the JS console in firefox.
465
- window.benchmarkCpu = utils.debounce((numCycles) => loop.benchmarkCpu(numCycles), 1);
466
- window.profileCpu = utils.debounce((arg) => loop.profileCpu(arg), 1);
467
- window.benchmarkVideo = utils.debounce((numCycles) => loop.benchmarkVideo(numCycles), 1);
468
- window.profileVideo = utils.debounce((arg) => loop.profileVideo(arg), 1);
469
- window.go = () => loop.go();
470
- window.stop = (debug) => loop.stop(debug);
471
- window.soundChip = audioHandler.soundChip;
472
- window.processor = processor;
473
- window.video = video;
474
- window.hd = function (start, end) {
475
- console.log(
476
- utils.hd(
477
- function (x) {
478
- return processor.readmem(x);
479
- },
480
- start,
481
- end,
482
- ),
483
- );
484
- };
485
- window.m7dump = function () {
486
- console.log(
487
- utils.hd(
488
- function (x) {
489
- return processor.readmem(x) & 0x7f;
490
- },
491
- 0x7c00,
492
- 0x7fe8,
493
- { width: 40, gap: false },
494
- ),
495
- );
496
- };
388
+ exposeConsoleSurface(window, { loop, processor, video, audioHandler });
497
389
 
498
390
  // Hooks for electron.
499
391
  electron({
500
392
  loadDiscImage: media.loadDiscImage.bind(media),
501
393
  loadTapeImage: media.loadTapeImage.bind(media),
502
394
  processor,
503
- config,
395
+ settings,
504
396
  media,
505
397
  modals: {
506
398
  show: (modalId, sthType) => {
@@ -513,17 +405,11 @@ electron({
513
405
  },
514
406
  loadStateFile: snapshots.loadStateFromFile.bind(snapshots),
515
407
  actions: {
516
- "soft-reset": () => processor.reset(false),
517
- "hard-reset": hardReset,
408
+ "soft-reset": () => page.softReset(),
409
+ "hard-reset": () => page.hardReset(),
518
410
  "save-state": () => snapshots.saveState(),
519
411
  rewind: () => rewindUI.open(),
520
- pause: pauseIntoDebugger,
521
- resume: resumeFromDebugger,
412
+ pause: () => runControls.pause(),
413
+ resume: () => runControls.resume(),
522
414
  },
523
415
  });
524
-
525
- // Display version in About dialog
526
- const versionElement = document.getElementById("jsbeeb-version");
527
- if (versionElement) {
528
- versionElement.textContent = `Version ${version}`;
529
- }
@@ -0,0 +1,68 @@
1
+ import { unzipDiscImage } from "./archive.js";
2
+ import { stringToUint8Array } from "./binary.js";
3
+ import { loadData } from "./loader.js";
4
+
5
+ /** Splits an image reference into its schema (empty for a bare name) and the image it names. */
6
+ export function splitImage(image) {
7
+ const match = image.match(/(([^:]+):\/?\/?|[!^|])?(.*)/);
8
+ return { schema: match[2] || match[1] || "", image: match[3] };
9
+ }
10
+
11
+ // Where a bare name is looked for, and which registered source serves the archive.
12
+ const Kinds = {
13
+ disc: { folder: "discs", sth: "sth" },
14
+ tape: { folder: "tapes", sth: "tapeSth" },
15
+ };
16
+
17
+ /** The image itself when `name` is a zip, else the bytes as given, either way as `{ name, data, ignored }`. */
18
+ export function openIfZip(name, data) {
19
+ return /\.zip/i.test(name) ? unzipDiscImage(data) : { name, data, ignored: [] };
20
+ }
21
+
22
+ /**
23
+ * Turns any image reference the URL can name into bytes, `{ name, data, ignored }`,
24
+ * for a disc or a tape: the archives through the sources registered for them,
25
+ * `data:` and `b64data:` inline, `http:`, `https:` and `file:` by URL, and a
26
+ * bare name from the built-in folder. Zips are opened once, here. Nothing in
27
+ * it needs a page, so a headless machine can load the same references.
28
+ */
29
+ export class MediaResolver {
30
+ constructor({ load = loadData } = {}) {
31
+ this.sources = {};
32
+ this.load = load;
33
+ }
34
+
35
+ /** Registers the fetcher behind an archive schema; each archive registers as it is constructed. */
36
+ addSource(schema, fetcher) {
37
+ this.sources[schema] = fetcher;
38
+ }
39
+
40
+ async resolve(kind, ref) {
41
+ const { schema, image } = splitImage(ref);
42
+ const { folder, sth } = Kinds[kind];
43
+ switch (schema) {
44
+ case "|":
45
+ case "sth":
46
+ return this.source(sth)(image);
47
+ case "hfe":
48
+ return { name: image, data: await this.source("hfe")(image), ignored: [] };
49
+ case "b64data":
50
+ return { name: "disk.ssd", data: stringToUint8Array(atob(image)), ignored: [] };
51
+ case "data":
52
+ return unzipDiscImage(stringToUint8Array(atob(image)));
53
+ case "http":
54
+ case "https":
55
+ case "file":
56
+ // The URL may end in query parameters, which would upset the extension check.
57
+ return openIfZip(new URL(ref).pathname, await this.load(ref));
58
+ default:
59
+ return openIfZip(image, await this.load(`${folder}/${image}`));
60
+ }
61
+ }
62
+
63
+ source(schema) {
64
+ const fetcher = this.sources[schema];
65
+ if (!fetcher) throw new Error(`No ${schema} archive is available here`);
66
+ return fetcher;
67
+ }
68
+ }
package/src/mmc.js CHANGED
@@ -1,4 +1,5 @@
1
- import { loadData, unzip, createZipBlob } from "./utils.js";
1
+ import { createZipBlob, unzip } from "./archive.js";
2
+ import { loadData } from "./loader.js";
2
3
 
3
4
  /*
4
5
  highly adapted from:
@@ -617,7 +618,7 @@ class WFN {
617
618
  let fname = this.trimToFilename(this.globalData);
618
619
 
619
620
  if (this.filenum === 0) {
620
- this.fildataIndex = 0; // FIXME : should be one for each fildata if going down this line!
621
+ this.fildataIndex = 0; // FIXME(#1068) should be one for each fildata if going down this line!
621
622
  // The scratch file is fixed, so we are backwards compatible with 2.9 firmware
622
623
  let fopen = this.f_open(fname, mode);
623
624
  if (fopen.error == FR_OK) this.fildata[0] = this.allfiles[fopen.fp];
package/src/models.js CHANGED
@@ -1,8 +1,9 @@
1
- "use strict";
2
-
3
1
  import { NoiseAwareWdFdc } from "./wd-fdc.js";
4
2
  import { NoiseAwareIntelFdc } from "./intel-fdc.js";
5
3
  import * as opcodes from "./6502.opcodes.js";
4
+ import { AtomCpu6502, Cpu6502 } from "./6502.js";
5
+ import { BBC, stringToBBCKeys } from "./keymap.js";
6
+ import { ATOM, stringToATOMKeys } from "./keymap-atom.js";
6
7
 
7
8
  const CpuModel = Object.freeze({
8
9
  MOS6502: 0,
@@ -43,6 +44,19 @@ class Model {
43
44
  this._cpuModel = cpuModel;
44
45
  this.isMaster = isMaster;
45
46
  this.isAtom = !!isAtom;
47
+ // The one place the two machine families part: everything else asks the model.
48
+ this.Cpu = this.isAtom ? AtomCpu6502 : Cpu6502;
49
+ this.keys = this.isAtom ? ATOM : BBC;
50
+ this.stringToKeys = this.isAtom ? stringToATOMKeys : stringToBBCKeys;
51
+ // The OS write-character vector, watched to capture what the machine prints.
52
+ this.wrchvAddress = this.isAtom ? 0x0208 : 0x020e;
53
+ // Where the machine sits waiting for a key: the Atom kernel's read loop, or BASIC's.
54
+ this.idleAddress = this.isAtom ? 0xfe94 : isMaster ? 0xe7e6 : 0xe581;
55
+ // The Atom ROM polls its keyboard once per VSync, so pasted keys need longer apart.
56
+ this.pasteKeyDelayMs = this.isAtom ? 80 : 50;
57
+ // Two of those 60 Hz scans, 33 ms, with margin: the ROM wants a key seen up
58
+ // twice before it takes the next.
59
+ this.pasteReleaseGapMs = this.isAtom ? 40 : 0;
46
60
  this.Fdc = fdc;
47
61
  this.swram = swram;
48
62
  this.isTest = false;
@@ -106,7 +120,7 @@ function atomModel({ name, synonyms, os, banks }) {
106
120
  });
107
121
  }
108
122
 
109
- // TODO: semi-bplus-style to get swram for exile hardcoded here
123
+ // TODO(#1066) semi-bplus-style to get swram for exile hardcoded here
110
124
  const beebSwram = [
111
125
  true,
112
126
  true,
package/src/music5000.js CHANGED
@@ -1,5 +1,3 @@
1
- "use strict";
2
-
3
1
  // Code ported from Beebem (C to .js) by Jason Robson
4
2
  const AUDIO_BUFFER_SIZE = 256;
5
3
 
package/src/ppia.js CHANGED
@@ -1,4 +1,4 @@
1
- import { getKeyMapAtom } from "./utils_atom.js";
1
+ import { getKeyMapAtom } from "./keymap-atom.js";
2
2
 
3
3
  // 8255 Programmable Peripheral Interface Adapter for the Acorn Atom.
4
4
  // Reference: http://mdfs.net/Docs/Comp/Acorn/Atom/atap25.htm
@@ -415,9 +415,8 @@ export class AtomPPIA extends PPIA {
415
415
  }
416
416
 
417
417
  rewindTape() {
418
- if (this.tape) {
419
- this.tape.rewind();
420
- }
418
+ this.stopTape();
419
+ if (this.tape) this.tape.rewind();
421
420
  }
422
421
 
423
422
  playTape() {
package/src/printer.js CHANGED
@@ -1,21 +1,14 @@
1
- "use strict";
2
-
3
1
  /**
4
2
  * Centronics printer attached to the user VIA: acknowledges each character and keeps the most
5
- * recent output so it can be shown whenever a printer window is opened.
3
+ * recent output so it can be shown whenever a printer window is opened. Dispatches "output" with
4
+ * each character as its detail, and "first-output" once, when the machine first prints.
6
5
  */
7
6
 
8
7
  export const MaxBufferedChars = 64 * 1024;
9
8
 
10
- export class Printer {
11
- /**
12
- * @param {object} [handlers]
13
- * @param {function(string): void} [handlers.onOutput] called with each character as it is printed
14
- * @param {function(): void} [handlers.onFirstOutput] called once, when the machine first prints
15
- */
16
- constructor({ onOutput = () => {}, onFirstOutput = () => {} } = {}) {
17
- this._onOutput = onOutput;
18
- this._onFirstOutput = onFirstOutput;
9
+ export class Printer extends EventTarget {
10
+ constructor() {
11
+ super();
19
12
  this._uservia = null;
20
13
  this._olderText = "";
21
14
  this._newerText = "";
@@ -52,8 +45,8 @@ export class Printer {
52
45
  }
53
46
  if (!this._hasPrinted) {
54
47
  this._hasPrinted = true;
55
- this._onFirstOutput();
48
+ this.dispatchEvent(new Event("first-output"));
56
49
  }
57
- this._onOutput(char);
50
+ this.dispatchEvent(new CustomEvent("output", { detail: char }));
58
51
  }
59
52
  }
package/src/relaynoise.js CHANGED
@@ -1,4 +1,3 @@
1
- "use strict";
2
1
  // Cassette motor relay click sound (issue #296).
3
2
  // Audio samples recorded from a real BBC Master cassette motor relay.
4
3
  import { SamplePlayer } from "./sample-player.js";
package/src/rewind.js CHANGED
@@ -1,5 +1,3 @@
1
- "use strict";
2
-
3
1
  /**
4
2
  * Circular buffer of emulator state snapshots for rewind functionality.
5
3
  * Snapshots are stored directly without deep-copying, since
@@ -1,5 +1,4 @@
1
- "use strict";
2
- import * as utils from "./utils.js";
1
+ import { loadData } from "./loader.js";
3
2
 
4
3
  /**
5
4
  * Base class for audio components that load and play back sample buffers
@@ -27,7 +26,7 @@ export class SamplePlayer {
27
26
  const entries = Object.entries(pathMap);
28
27
  const decoded = await Promise.all(
29
28
  entries.map(async ([, path]) => {
30
- const data = await utils.loadData(path);
29
+ const data = await loadData(path);
31
30
  // Safari doesn't support the promise form of decodeAudioData.
32
31
  return new Promise((resolve, reject) => {
33
32
  this.context.decodeAudioData(
package/src/serial.js CHANGED
@@ -1,5 +1,3 @@
1
- "use strict";
2
-
3
1
  const table = [19200, 9600, 4800, 2400, 1200, 300, 150, 75];
4
2
 
5
3
  export class Serial {
@@ -1,5 +1,3 @@
1
- "use strict";
2
-
3
1
  // Shared helpers for snapshot importers (bem-snapshot.js, uef-snapshot.js).
4
2
  // Contains volume table, default peripheral state, video state builder,
5
3
  // and the common snapshot envelope that wraps per-format parsed state.
package/src/snapshot.js CHANGED
@@ -1,5 +1,3 @@
1
- "use strict";
2
-
3
1
  import { typedArrayToBase64, base64ToTypedArray } from "./state-utils.js";
4
2
  import { findModel } from "./models.js";
5
3
 
package/src/soundchip.js CHANGED
@@ -347,7 +347,7 @@ export class SoundChip {
347
347
  updateSlowDataBus(slowDataBus, active) {
348
348
  this.slowDataBus = slowDataBus;
349
349
  this.active = active;
350
- // TODO: this probably isn't modeled correctly. Currently the sound chip "notices" a new data bus value some
350
+ // TODO(#835) this probably isn't modeled correctly. Currently the sound chip "notices" a new data bus value some
351
351
  // fixed number of cycles after WE (write enable) is triggered. In reality, the sound chip likely pulls data off
352
352
  // the bus at a fixed point in its cycle, iff WE is active.
353
353
  if (active) {
@@ -1,5 +1,3 @@
1
- "use strict";
2
-
3
1
  /**
4
2
  * Convert a TypedArray to a base64 string for JSON serialization.
5
3
  * @param {ArrayBufferView} typedArray