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/src/main.js CHANGED
@@ -7,12 +7,14 @@ import "./jsbeeb.css";
7
7
  import * as utils from "./utils.js";
8
8
  import { FakeVideo, Video } from "./video.js";
9
9
  import { Debugger } from "./web/debug.js";
10
- import { Cpu6502 } from "./6502.js";
10
+ import { Cpu6502, AtomCpu6502 } from "./6502.js";
11
+ import * as utils_atom from "./utils_atom.js";
12
+ import { LoadSD } from "./mmc.js";
11
13
  import { Cmos } from "./cmos.js";
12
14
  import { StairwayToHell } from "./sth.js";
13
15
  import { GamePad } from "./gamepads.js";
14
16
  import * as disc from "./fdc.js";
15
- import { loadTape, loadTapeFromData } from "./tapes.js";
17
+ import { loadTapeFromData } from "./tapes.js";
16
18
  import { GoogleDriveLoader } from "./google-drive.js";
17
19
  import * as tokeniser from "./basic-tokenise.js";
18
20
  import * as canvasLib from "./canvas.js";
@@ -28,8 +30,9 @@ import { MicrophoneInput } from "./microphone-input.js";
28
30
  import { SpeechOutput } from "./speech-output.js";
29
31
  import { MouseJoystickSource } from "./mouse-joystick-source.js";
30
32
  import { getFilterForMode } from "./canvas.js";
31
- import { createSnapshot, restoreSnapshot, snapshotToJSON, snapshotFromJSON, modelsCompatible } from "./snapshot.js";
33
+ import { createSnapshot, restoreSnapshot, snapshotToJSON, snapshotFromJSON, isSameModel } from "./snapshot.js";
32
34
  import { isBemSnapshot, parseBemSnapshot } from "./bem-snapshot.js";
35
+ import { isUefSnapshot, parseUefSnapshot } from "./uef-snapshot.js";
33
36
  import { RewindBuffer } from "./rewind.js";
34
37
  import { RewindUI } from "./rewind-ui.js";
35
38
  import {
@@ -53,6 +56,21 @@ let discSth;
53
56
  let tapeSth;
54
57
  let running;
55
58
  let model;
59
+
60
+ // Route tape to the correct interface (ACIA for BBC, PPIA for Atom)
61
+ function setProcessorTape(tape) {
62
+ if (model.isAtom) {
63
+ processor.atomppia.setTape(tape);
64
+ } else {
65
+ processor.acia.setTape(tape);
66
+ }
67
+ }
68
+
69
+ // Convert text to machine-appropriate key sequences (BBC or Atom)
70
+ function stringToMachineKeys(text) {
71
+ return model.isAtom ? utils_atom.stringToATOMKeys(text) : utils.stringToBBCKeys(text);
72
+ }
73
+
56
74
  const gamepad = new GamePad();
57
75
  const availableImages = [
58
76
  {
@@ -102,6 +120,7 @@ const paramTypes = {
102
120
  coProcessor: ParamTypes.BOOL,
103
121
  mouseJoystickEnabled: ParamTypes.BOOL,
104
122
  speechOutput: ParamTypes.BOOL,
123
+ audioDebug: ParamTypes.BOOL,
105
124
 
106
125
  // Numeric parameters
107
126
  speed: ParamTypes.INT,
@@ -119,6 +138,7 @@ const paramTypes = {
119
138
  disc1: ParamTypes.STRING,
120
139
  disc2: ParamTypes.STRING,
121
140
  tape: ParamTypes.STRING,
141
+ mmc: ParamTypes.STRING,
122
142
  keyLayout: ParamTypes.STRING,
123
143
  autotype: ParamTypes.STRING,
124
144
  displayMode: ParamTypes.STRING,
@@ -141,7 +161,7 @@ let stationId = 101;
141
161
  let econet = null;
142
162
 
143
163
  // Parse disc and tape images from query parameters
144
- const { discImage: queryDiscImage, secondDiscImage: querySecondDisc } = parseMediaParams(parsedQuery);
164
+ const { discImage: queryDiscImage, secondDiscImage: querySecondDisc, mmcImage } = parseMediaParams(parsedQuery);
145
165
 
146
166
  // Only assign if values are provided
147
167
  if (queryDiscImage) discImage = queryDiscImage;
@@ -259,7 +279,7 @@ const config = new Config(
259
279
  updateAdcSources(parsedQuery.mouseJoystickEnabled, parsedQuery.microphoneChannel);
260
280
 
261
281
  if (changed.microphoneChannel !== undefined) {
262
- setupMicrophone().then(() => {});
282
+ setupMicrophone();
263
283
  }
264
284
  }
265
285
  if (changed.speechOutput !== undefined) {
@@ -319,7 +339,8 @@ if (parsedQuery.cpuMultiplier !== undefined) {
319
339
  cpuMultiplier = parsedQuery.cpuMultiplier;
320
340
  console.log("CPU multiplier set to " + cpuMultiplier);
321
341
  }
322
- const clocksPerSecond = (cpuMultiplier * 2 * 1000 * 1000) | 0;
342
+ const cpuSpeed = model.isAtom ? 1 * 1000 * 1000 : 2 * 1000 * 1000;
343
+ const clocksPerSecond = (cpuMultiplier * cpuSpeed) | 0;
323
344
  const MaxCyclesPerFrame = clocksPerSecond / 10;
324
345
 
325
346
  let tryGl = true;
@@ -375,23 +396,31 @@ function swapCanvas(newFilterClass) {
375
396
 
376
397
  let canvas = createCanvasForFilter(displayModeFilter);
377
398
 
378
- video = new Video(model.isMaster, canvas.fb32, function paint(minx, miny, maxx, maxy) {
379
- frames++;
380
- if (frames < frameSkip) return;
381
- frames = 0;
382
- canvas.paint(minx, miny, maxx, maxy, this.frameCount);
383
- });
399
+ video = new Video(
400
+ model.isMaster,
401
+ canvas.fb32,
402
+ function paint(minx, miny, maxx, maxy) {
403
+ frames++;
404
+ if (frames < frameSkip) return;
405
+ frames = 0;
406
+ canvas.paint(minx, miny, maxx, maxy, this.frameCount);
407
+ },
408
+ { isAtom: model.isAtom },
409
+ );
384
410
  if (parsedQuery.fakeVideo !== undefined) video = new FakeVideo();
385
411
 
386
- const audioStatsNode = document.getElementById("audio-stats");
412
+ const audioStatsEl = document.getElementById("audio-stats");
413
+ if (audioStatsEl) audioStatsEl.hidden = !parsedQuery.audioDebug;
414
+ const audioStatsNode = parsedQuery.audioDebug ? audioStatsEl : null;
387
415
  const audioHandler = new AudioHandler({
388
416
  warningNode: document.getElementById("audio-warning"),
389
417
  statsNode: audioStatsNode,
390
418
  audioFilterFreq,
391
419
  audioFilterQ,
392
420
  noSeek,
421
+ cpuSpeed,
422
+ isAtom: model.isAtom,
393
423
  });
394
- if (!parsedQuery.audioDebug) audioStatsNode.style.display = "none";
395
424
  // Firefox will report that audio is suspended even when it will
396
425
  // start playing without user interaction, so we need to delay a
397
426
  // little to get a reliable indication.
@@ -482,7 +511,7 @@ const pastetext = document.getElementById("paste-text");
482
511
  pastetext.closest("form").addEventListener("submit", (event) => event.preventDefault());
483
512
  pastetext.addEventListener("paste", function (event) {
484
513
  const text = event.clipboardData.getData("text/plain");
485
- sendRawKeyboardToBBC(utils.stringToBBCKeys(text), true);
514
+ sendRawKeyboard(stringToMachineKeys(text), true);
486
515
  });
487
516
  pastetext.addEventListener("dragover", function (event) {
488
517
  event.preventDefault();
@@ -492,8 +521,12 @@ pastetext.addEventListener("dragover", function (event) {
492
521
  pastetext.addEventListener("drop", async function (event) {
493
522
  utils.noteEvent("local", "drop");
494
523
  const file = event.dataTransfer.files[0];
495
- if (isSnapshotFile(file.name)) {
496
- await loadStateFromFile(file);
524
+ const arrayBuffer = await file.arrayBuffer();
525
+ if (isSnapshotFile(file.name, arrayBuffer)) {
526
+ await loadStateFromFile(file, arrayBuffer);
527
+ } else if (file.name.toLowerCase().endsWith(".uef")) {
528
+ // Regular UEF tape image (not a BeebEm save state)
529
+ setProcessorTape(await loadTapeFromData(file.name, new Uint8Array(arrayBuffer), model.isAtom));
497
530
  } else {
498
531
  await loadHTMLFile(file);
499
532
  }
@@ -501,7 +534,7 @@ pastetext.addEventListener("drop", async function (event) {
501
534
 
502
535
  const cubMonitor = document.getElementById("cub-monitor");
503
536
  function onCubMouseEvent(evt) {
504
- audioHandler.tryResume().then(() => {});
537
+ audioHandler.tryResume();
505
538
  if (document.activeElement !== document.body) document.activeElement.blur();
506
539
  const cubRect = cubMonitor.getBoundingClientRect();
507
540
  const screenRect = screenCanvas.getBoundingClientRect();
@@ -617,7 +650,8 @@ function checkPrinterWindow() {
617
650
  processor.uservia.setca1(true);
618
651
  }
619
652
 
620
- processor = new Cpu6502(model, {
653
+ const CpuClass = model.isAtom ? AtomCpu6502 : Cpu6502;
654
+ processor = new CpuClass(model, {
621
655
  dbgr,
622
656
  video,
623
657
  soundChip: audioHandler.soundChip,
@@ -737,12 +771,12 @@ keyboard = new Keyboard({
737
771
  keyLayout,
738
772
  dbgr,
739
773
  });
740
- keyboard.on("showError", ({ context, error }) => showError(context, error));
741
- keyboard.on("pause", () => stop(false));
742
- keyboard.on("resume", () => go());
743
- keyboard.on("break", (pressed) => {
774
+ keyboard.addEventListener("showError", (e) => showError(e.detail.context, e.detail.error));
775
+ keyboard.addEventListener("pause", () => stop(false));
776
+ keyboard.addEventListener("resume", () => go());
777
+ keyboard.addEventListener("break", (e) => {
744
778
  // F12/Break: Reset processor
745
- if (pressed) utils.noteEvent("keyboard", "press", "break");
779
+ if (e.detail) utils.noteEvent("keyboard", "press", "break");
746
780
  });
747
781
 
748
782
  // Register default key handlers
@@ -846,8 +880,8 @@ keyboard.registerKeyHandler(
846
880
 
847
881
  // Setup key handlers
848
882
  document.addEventListener("keydown", (evt) => {
849
- audioHandler.tryResume().then(() => {});
850
- ensureMicrophoneRunning().then(() => {});
883
+ audioHandler.tryResume();
884
+ ensureMicrophoneRunning();
851
885
  keyboard.keyDown(evt);
852
886
  });
853
887
  document.addEventListener("keypress", (evt) => keyboard.keyPress(evt));
@@ -857,19 +891,19 @@ function setDisc1Image(name) {
857
891
  delete parsedQuery.disc;
858
892
  parsedQuery.disc1 = name;
859
893
  updateUrl();
860
- config.emit("media-changed", { disc1: name });
894
+ config.dispatchEvent(new CustomEvent("media-changed", { detail: { disc1: name } }));
861
895
  }
862
896
 
863
897
  function setDisc2Image(name) {
864
898
  parsedQuery.disc2 = name;
865
899
  updateUrl();
866
- config.emit("media-changed", { disc2: name });
900
+ config.dispatchEvent(new CustomEvent("media-changed", { detail: { disc2: name } }));
867
901
  }
868
902
 
869
903
  function setTapeImage(name) {
870
904
  parsedQuery.tape = name;
871
905
  updateUrl();
872
- config.emit("media-changed", { tape: name });
906
+ config.dispatchEvent(new CustomEvent("media-changed", { detail: { tape: name } }));
873
907
  }
874
908
 
875
909
  function sthClearList() {
@@ -913,7 +947,7 @@ async function tapeSthClick(item) {
913
947
  popupLoading("Loading " + item);
914
948
  try {
915
949
  const tape = await loadTapeImage(parsedQuery.tape);
916
- processor.acia.setTape(tape);
950
+ setProcessorTape(tape);
917
951
  loadingFinished();
918
952
  } catch (err) {
919
953
  console.error("Error loading tape image:", err);
@@ -1001,9 +1035,9 @@ const sthFilter = document.getElementById("sth-filter");
1001
1035
  sthFilter.addEventListener("change", () => setSthFilter(sthFilter.value));
1002
1036
  sthFilter.addEventListener("keyup", () => setSthFilter(sthFilter.value));
1003
1037
 
1004
- function sendRawKeyboardToBBC(keysToSend, checkCapsAndShiftLocks) {
1038
+ function sendRawKeyboard(keysToSend, checkCapsAndShiftLocks) {
1005
1039
  if (keyboard) {
1006
- keyboard.sendRawKeyboardToBBC(keysToSend, checkCapsAndShiftLocks);
1040
+ keyboard.sendRawKeyboard(keysToSend, checkCapsAndShiftLocks);
1007
1041
  } else {
1008
1042
  console.warn("Tried to send keys before keyboard was initialised");
1009
1043
  }
@@ -1016,39 +1050,39 @@ function autoboot(image) {
1016
1050
  utils.noteEvent("init", "autoboot", image);
1017
1051
 
1018
1052
  // Shift-break simulation, hold SHIFT for 1000ms.
1019
- sendRawKeyboardToBBC([BBC.SHIFT, 1000], false);
1053
+ sendRawKeyboard([BBC.SHIFT, 1000], false);
1020
1054
  }
1021
1055
 
1022
1056
  function autoBootType(keys) {
1023
1057
  console.log("Auto typing '" + keys + "'");
1024
1058
  utils.noteEvent("init", "autochain");
1025
1059
 
1026
- const bbcKeys = utils.stringToBBCKeys(keys);
1027
- sendRawKeyboardToBBC([1000].concat(bbcKeys), false);
1060
+ const bbcKeys = stringToMachineKeys(keys);
1061
+ sendRawKeyboard([1000].concat(bbcKeys), false);
1028
1062
  }
1029
1063
 
1030
1064
  function autoChainTape() {
1031
1065
  console.log("Auto Chaining Tape");
1032
1066
  utils.noteEvent("init", "autochain");
1033
1067
 
1034
- const bbcKeys = utils.stringToBBCKeys('*TAPE\nCH.""\n');
1035
- sendRawKeyboardToBBC([1000].concat(bbcKeys), false);
1068
+ const bbcKeys = stringToMachineKeys('*TAPE\nCH.""\n');
1069
+ sendRawKeyboard([1000].concat(bbcKeys), false);
1036
1070
  }
1037
1071
 
1038
1072
  function autoRunTape() {
1039
1073
  console.log("Auto Running Tape");
1040
1074
  utils.noteEvent("init", "autorun");
1041
1075
 
1042
- const bbcKeys = utils.stringToBBCKeys("*TAPE\n*/\n");
1043
- sendRawKeyboardToBBC([1000].concat(bbcKeys), false);
1076
+ const bbcKeys = stringToMachineKeys("*TAPE\n*/\n");
1077
+ sendRawKeyboard([1000].concat(bbcKeys), false);
1044
1078
  }
1045
1079
 
1046
1080
  function autoRunBasic() {
1047
1081
  console.log("Auto Running basic");
1048
1082
  utils.noteEvent("init", "autorunbasic");
1049
1083
 
1050
- const bbcKeys = utils.stringToBBCKeys("RUN\n");
1051
- sendRawKeyboardToBBC([1000].concat(bbcKeys), false);
1084
+ const bbcKeys = stringToMachineKeys("RUN\n");
1085
+ sendRawKeyboard([1000].concat(bbcKeys), false);
1052
1086
  }
1053
1087
 
1054
1088
  function updateUrl() {
@@ -1168,16 +1202,17 @@ async function loadTapeImage(tapeImage) {
1168
1202
  const split = splitImage(tapeImage);
1169
1203
  tapeImage = split.image;
1170
1204
  const schema = split.schema;
1205
+ const isAtom = model.isAtom;
1171
1206
 
1172
1207
  switch (schema) {
1173
1208
  case "|":
1174
1209
  case "sth":
1175
- return await loadTapeFromData(tapeImage, await tapeSth.fetch(tapeImage));
1210
+ return await loadTapeFromData(tapeImage, await tapeSth.fetch(tapeImage), isAtom);
1176
1211
 
1177
1212
  case "data": {
1178
1213
  const arr = Array.prototype.map.call(atob(tapeImage), (x) => x.charCodeAt(0));
1179
1214
  const { name, data } = await utils.unzipDiscImage(arr);
1180
- return await loadTapeFromData(name, data);
1215
+ return await loadTapeFromData(name, data, isAtom);
1181
1216
  }
1182
1217
 
1183
1218
  case "http":
@@ -1192,11 +1227,20 @@ async function loadTapeImage(tapeImage) {
1192
1227
  tapeData = unzipped.data;
1193
1228
  tapeImage = unzipped.name;
1194
1229
  }
1195
- return await loadTapeFromData(tapeImage, tapeData);
1230
+ return await loadTapeFromData(tapeImage, tapeData, isAtom);
1196
1231
  }
1197
1232
 
1198
- default:
1199
- return await loadTape("tapes/" + tapeImage);
1233
+ default: {
1234
+ const tapePath = "tapes/" + tapeImage;
1235
+ let tapeData = await utils.loadData(tapePath);
1236
+ let tapeName = tapeImage;
1237
+ if (/\.zip/i.test(tapeName)) {
1238
+ const unzipped = await utils.unzipDiscImage(tapeData);
1239
+ tapeData = unzipped.data;
1240
+ tapeName = unzipped.name;
1241
+ }
1242
+ return await loadTapeFromData(tapeName, tapeData, isAtom);
1243
+ }
1200
1244
  }
1201
1245
  }
1202
1246
 
@@ -1221,8 +1265,14 @@ document.getElementById("tape_load").addEventListener("change", async function (
1221
1265
  const file = evt.target.files[0];
1222
1266
  utils.noteEvent("local", "clickTape"); // NB no filename here
1223
1267
 
1224
- const binaryData = await readFileAsBinaryString(file);
1225
- processor.acia.setTape(await loadTapeFromData("local file", binaryData));
1268
+ let tapeData = await readFileAsBinaryString(file);
1269
+ let tapeName = file.name;
1270
+ if (/\.zip/i.test(tapeName)) {
1271
+ const unzipped = await utils.unzipDiscImage(utils.stringToUint8Array(tapeData));
1272
+ tapeData = unzipped.data;
1273
+ tapeName = unzipped.name;
1274
+ }
1275
+ setProcessorTape(await loadTapeFromData(tapeName, tapeData, model.isAtom));
1226
1276
  delete parsedQuery.tape;
1227
1277
  updateUrl();
1228
1278
  bootstrap.Modal.getInstance(document.getElementById("tapes"))?.hide();
@@ -1354,13 +1404,12 @@ googleDriveEl.addEventListener("show.bs.modal", async function () {
1354
1404
  row.classList.remove("template");
1355
1405
  dbList.appendChild(row);
1356
1406
  row.querySelector(".name").textContent = item.name;
1357
- row.addEventListener("click", function () {
1407
+ row.addEventListener("click", async function () {
1358
1408
  utils.noteEvent("google-drive", "click", item.name);
1359
1409
  setDisc1Image(`gd:${item.id}/${item.name}`);
1360
- gdLoad(item).then(function (ssd) {
1361
- processor.fdc.loadDisc(0, ssd);
1362
- });
1363
1410
  googleDriveModal.hide();
1411
+ const ssd = await gdLoad(item);
1412
+ if (ssd) processor.fdc.loadDisc(0, ssd);
1364
1413
  });
1365
1414
  }
1366
1415
  });
@@ -1372,13 +1421,11 @@ for (const image of availableImages) {
1372
1421
  discList.appendChild(elem);
1373
1422
  elem.querySelector(".name").textContent = image.name;
1374
1423
  elem.querySelector(".description").textContent = image.desc;
1375
- elem.addEventListener("click", function () {
1424
+ elem.addEventListener("click", async function () {
1376
1425
  utils.noteEvent("images", "click", image.file);
1377
1426
  setDisc1Image(image.file);
1378
- loadDiscImage(parsedQuery.disc1).then(function (disc) {
1379
- processor.fdc.loadDisc(0, disc);
1380
- });
1381
1427
  $discsModal.hide();
1428
+ processor.fdc.loadDisc(0, await loadDiscImage(parsedQuery.disc1));
1382
1429
  });
1383
1430
  }
1384
1431
 
@@ -1501,14 +1548,16 @@ document.getElementById("save-state").addEventListener("click", async function (
1501
1548
  if (wasRunning) go();
1502
1549
  });
1503
1550
 
1504
- async function loadStateFromFile(file) {
1551
+ async function loadStateFromFile(file, preReadBuffer) {
1505
1552
  const wasRunning = running;
1506
1553
  if (running) stop(false);
1507
1554
  try {
1508
- const arrayBuffer = await file.arrayBuffer();
1555
+ const arrayBuffer = preReadBuffer || (await file.arrayBuffer());
1509
1556
  let snapshot;
1510
1557
  if (isBemSnapshot(arrayBuffer)) {
1511
1558
  snapshot = await parseBemSnapshot(arrayBuffer);
1559
+ } else if (isUefSnapshot(arrayBuffer)) {
1560
+ snapshot = parseUefSnapshot(arrayBuffer);
1512
1561
  } else {
1513
1562
  // Detect gzip (magic bytes 0x1f 0x8b) or plain JSON
1514
1563
  const bytes = new Uint8Array(arrayBuffer);
@@ -1521,7 +1570,7 @@ async function loadStateFromFile(file) {
1521
1570
  }
1522
1571
  snapshot = snapshotFromJSON(text);
1523
1572
  }
1524
- if (!modelsCompatible(snapshot.model, model.name)) {
1573
+ if (!isSameModel(snapshot.model, model.name)) {
1525
1574
  // Model mismatch: stash state and reload with correct model
1526
1575
  sessionStorage.setItem("jsbeeb-pending-state", snapshotToJSON(snapshot));
1527
1576
  const newQuery = { ...parsedQuery, model: snapshot.model };
@@ -1541,9 +1590,13 @@ async function loadStateFromFile(file) {
1541
1590
  if (wasRunning) go();
1542
1591
  }
1543
1592
 
1544
- function isSnapshotFile(filename) {
1593
+ function isSnapshotFile(filename, arrayBuffer) {
1545
1594
  const lower = filename.toLowerCase();
1546
- return lower.endsWith(".snp") || lower.endsWith(".json") || lower.endsWith(".json.gz") || lower.endsWith(".gz");
1595
+ if (lower.endsWith(".snp") || lower.endsWith(".json") || lower.endsWith(".json.gz") || lower.endsWith(".gz"))
1596
+ return true;
1597
+ // .uef can be either a BeebEm save state or a regular tape image - check content
1598
+ if (lower.endsWith(".uef") && arrayBuffer) return isUefSnapshot(arrayBuffer);
1599
+ return false;
1547
1600
  }
1548
1601
 
1549
1602
  document.getElementById("load-state").addEventListener("change", async function (event) {
@@ -1560,13 +1613,58 @@ for (const link of document.querySelectorAll("#tape-menu a")) {
1560
1613
 
1561
1614
  if (type === "rewind") {
1562
1615
  console.log("Rewinding tape to the start");
1563
- processor.acia.rewindTape();
1616
+ if (model.isAtom) {
1617
+ processor.atomppia.stopTape();
1618
+ processor.atomppia.rewindTape();
1619
+ updateTapeButton();
1620
+ } else {
1621
+ processor.acia.rewindTape();
1622
+ }
1564
1623
  } else {
1565
1624
  console.log("unknown type", type);
1566
1625
  }
1567
1626
  });
1568
1627
  }
1569
1628
 
1629
+ const tapePlayStopBtn = document.getElementById("tape-play-stop");
1630
+ const tapeControlHeader = document.getElementById("tape-control-header");
1631
+ const tapeControlCell = document.getElementById("tape-control-cell");
1632
+
1633
+ function updateTapeButton() {
1634
+ if (!model.isAtom) return;
1635
+ const playing = processor.atomppia.motorOn;
1636
+ const label = playing ? "Stop cassette" : "Play cassette";
1637
+ tapePlayStopBtn.textContent = playing ? "\u25A0" : "\u25B6";
1638
+ tapePlayStopBtn.title = label;
1639
+ tapePlayStopBtn.setAttribute("aria-label", label);
1640
+ tapePlayStopBtn.classList.toggle("playing", playing);
1641
+ }
1642
+
1643
+ function showTapeControl(visible) {
1644
+ const display = visible ? "" : "none";
1645
+ tapeControlHeader.style.display = display;
1646
+ tapeControlCell.style.display = display;
1647
+ }
1648
+
1649
+ function updateLedVisibility() {
1650
+ const bbcDisplay = model.isAtom ? "none" : "";
1651
+ for (const el of document.querySelectorAll(".bbc-only")) {
1652
+ el.style.display = bbcDisplay;
1653
+ }
1654
+ showTapeControl(model.isAtom);
1655
+ }
1656
+
1657
+ updateLedVisibility();
1658
+
1659
+ tapePlayStopBtn.addEventListener("click", () => {
1660
+ if (processor.atomppia.motorOn) {
1661
+ processor.atomppia.stopTape();
1662
+ } else {
1663
+ processor.atomppia.playTape();
1664
+ }
1665
+ updateTapeButton();
1666
+ });
1667
+
1570
1668
  function Light(name) {
1571
1669
  const dom = document.getElementById(name);
1572
1670
  let on = false;
@@ -1585,13 +1683,17 @@ const drive1 = new Light("drive1");
1585
1683
  const network = new Light("networklight");
1586
1684
 
1587
1685
  syncLights = function () {
1588
- caps.update(processor.sysvia.capsLockLight);
1589
- shift.update(processor.sysvia.shiftLockLight);
1590
- drive0.update(processor.fdc.motorOn[0]);
1591
- drive1.update(processor.fdc.motorOn[1]);
1592
- cassette.update(processor.acia.motorOn);
1593
- if (model.hasEconet) {
1594
- network.update(processor.econet.activityLight());
1686
+ if (model.isAtom) {
1687
+ cassette.update(processor.atomppia.motorOn);
1688
+ } else {
1689
+ caps.update(processor.sysvia.capsLockLight);
1690
+ shift.update(processor.sysvia.shiftLockLight);
1691
+ drive0.update(processor.fdc.motorOn[0]);
1692
+ drive1.update(processor.fdc.motorOn[1]);
1693
+ cassette.update(processor.acia.motorOn);
1694
+ if (model.hasEconet) {
1695
+ network.update(processor.econet.activityLight());
1696
+ }
1595
1697
  }
1596
1698
  };
1597
1699
 
@@ -1626,7 +1728,16 @@ const startPromise = (async () => {
1626
1728
  imageLoads.push(
1627
1729
  (async () => {
1628
1730
  const tape = await loadTapeImage(parsedQuery.tape);
1629
- processor.acia.setTape(tape);
1731
+ setProcessorTape(tape);
1732
+ })(),
1733
+ );
1734
+ }
1735
+
1736
+ if (mmcImage && model.isAtom) {
1737
+ imageLoads.push(
1738
+ (async () => {
1739
+ const files = await LoadSD(mmcImage);
1740
+ processor.atommc.SetMMCData(files);
1630
1741
  })(),
1631
1742
  );
1632
1743
  }
@@ -1684,8 +1795,10 @@ const startPromise = (async () => {
1684
1795
  return Promise.all(imageLoads);
1685
1796
  })();
1686
1797
 
1687
- startPromise
1688
- .then(async () => {
1798
+ (async () => {
1799
+ try {
1800
+ await startPromise;
1801
+
1689
1802
  switch (needsAutoboot) {
1690
1803
  case "boot":
1691
1804
  sthAutoboot.checked = true;
@@ -1726,11 +1839,11 @@ startPromise
1726
1839
  }
1727
1840
 
1728
1841
  go();
1729
- })
1730
- .catch((error) => {
1842
+ } catch (error) {
1731
1843
  console.error("Error initialising emulator:", error);
1732
1844
  showError("initialising", error);
1733
- });
1845
+ }
1846
+ })();
1734
1847
 
1735
1848
  const aysEl = document.getElementById("are-you-sure");
1736
1849
  const aysModal = new bootstrap.Modal(aysEl);