jsbeeb 1.12.0 → 1.13.1

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";
@@ -54,6 +56,21 @@ let discSth;
54
56
  let tapeSth;
55
57
  let running;
56
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
+
57
74
  const gamepad = new GamePad();
58
75
  const availableImages = [
59
76
  {
@@ -121,6 +138,7 @@ const paramTypes = {
121
138
  disc1: ParamTypes.STRING,
122
139
  disc2: ParamTypes.STRING,
123
140
  tape: ParamTypes.STRING,
141
+ mmc: ParamTypes.STRING,
124
142
  keyLayout: ParamTypes.STRING,
125
143
  autotype: ParamTypes.STRING,
126
144
  displayMode: ParamTypes.STRING,
@@ -143,7 +161,7 @@ let stationId = 101;
143
161
  let econet = null;
144
162
 
145
163
  // Parse disc and tape images from query parameters
146
- const { discImage: queryDiscImage, secondDiscImage: querySecondDisc } = parseMediaParams(parsedQuery);
164
+ const { discImage: queryDiscImage, secondDiscImage: querySecondDisc, mmcImage } = parseMediaParams(parsedQuery);
147
165
 
148
166
  // Only assign if values are provided
149
167
  if (queryDiscImage) discImage = queryDiscImage;
@@ -321,7 +339,8 @@ if (parsedQuery.cpuMultiplier !== undefined) {
321
339
  cpuMultiplier = parsedQuery.cpuMultiplier;
322
340
  console.log("CPU multiplier set to " + cpuMultiplier);
323
341
  }
324
- const clocksPerSecond = (cpuMultiplier * 2 * 1000 * 1000) | 0;
342
+ const cpuSpeed = model.isAtom ? 1 * 1000 * 1000 : 2 * 1000 * 1000;
343
+ const clocksPerSecond = (cpuMultiplier * cpuSpeed) | 0;
325
344
  const MaxCyclesPerFrame = clocksPerSecond / 10;
326
345
 
327
346
  let tryGl = true;
@@ -377,12 +396,17 @@ function swapCanvas(newFilterClass) {
377
396
 
378
397
  let canvas = createCanvasForFilter(displayModeFilter);
379
398
 
380
- video = new Video(model.isMaster, canvas.fb32, function paint(minx, miny, maxx, maxy) {
381
- frames++;
382
- if (frames < frameSkip) return;
383
- frames = 0;
384
- canvas.paint(minx, miny, maxx, maxy, this.frameCount);
385
- });
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
+ );
386
410
  if (parsedQuery.fakeVideo !== undefined) video = new FakeVideo();
387
411
 
388
412
  const audioStatsEl = document.getElementById("audio-stats");
@@ -394,6 +418,8 @@ const audioHandler = new AudioHandler({
394
418
  audioFilterFreq,
395
419
  audioFilterQ,
396
420
  noSeek,
421
+ cpuSpeed,
422
+ isAtom: model.isAtom,
397
423
  });
398
424
  // Firefox will report that audio is suspended even when it will
399
425
  // start playing without user interaction, so we need to delay a
@@ -485,7 +511,7 @@ const pastetext = document.getElementById("paste-text");
485
511
  pastetext.closest("form").addEventListener("submit", (event) => event.preventDefault());
486
512
  pastetext.addEventListener("paste", function (event) {
487
513
  const text = event.clipboardData.getData("text/plain");
488
- sendRawKeyboardToBBC(utils.stringToBBCKeys(text), true);
514
+ sendRawKeyboard(stringToMachineKeys(text), true);
489
515
  });
490
516
  pastetext.addEventListener("dragover", function (event) {
491
517
  event.preventDefault();
@@ -500,7 +526,7 @@ pastetext.addEventListener("drop", async function (event) {
500
526
  await loadStateFromFile(file, arrayBuffer);
501
527
  } else if (file.name.toLowerCase().endsWith(".uef")) {
502
528
  // Regular UEF tape image (not a BeebEm save state)
503
- processor.acia.setTape(loadTapeFromData(file.name, new Uint8Array(arrayBuffer)));
529
+ setProcessorTape(await loadTapeFromData(file.name, new Uint8Array(arrayBuffer), model.isAtom));
504
530
  } else {
505
531
  await loadHTMLFile(file);
506
532
  }
@@ -624,7 +650,8 @@ function checkPrinterWindow() {
624
650
  processor.uservia.setca1(true);
625
651
  }
626
652
 
627
- processor = new Cpu6502(model, {
653
+ const CpuClass = model.isAtom ? AtomCpu6502 : Cpu6502;
654
+ processor = new CpuClass(model, {
628
655
  dbgr,
629
656
  video,
630
657
  soundChip: audioHandler.soundChip,
@@ -920,7 +947,7 @@ async function tapeSthClick(item) {
920
947
  popupLoading("Loading " + item);
921
948
  try {
922
949
  const tape = await loadTapeImage(parsedQuery.tape);
923
- processor.acia.setTape(tape);
950
+ setProcessorTape(tape);
924
951
  loadingFinished();
925
952
  } catch (err) {
926
953
  console.error("Error loading tape image:", err);
@@ -1008,9 +1035,9 @@ const sthFilter = document.getElementById("sth-filter");
1008
1035
  sthFilter.addEventListener("change", () => setSthFilter(sthFilter.value));
1009
1036
  sthFilter.addEventListener("keyup", () => setSthFilter(sthFilter.value));
1010
1037
 
1011
- function sendRawKeyboardToBBC(keysToSend, checkCapsAndShiftLocks) {
1038
+ function sendRawKeyboard(keysToSend, checkCapsAndShiftLocks) {
1012
1039
  if (keyboard) {
1013
- keyboard.sendRawKeyboardToBBC(keysToSend, checkCapsAndShiftLocks);
1040
+ keyboard.sendRawKeyboard(keysToSend, checkCapsAndShiftLocks);
1014
1041
  } else {
1015
1042
  console.warn("Tried to send keys before keyboard was initialised");
1016
1043
  }
@@ -1023,39 +1050,39 @@ function autoboot(image) {
1023
1050
  utils.noteEvent("init", "autoboot", image);
1024
1051
 
1025
1052
  // Shift-break simulation, hold SHIFT for 1000ms.
1026
- sendRawKeyboardToBBC([BBC.SHIFT, 1000], false);
1053
+ sendRawKeyboard([BBC.SHIFT, 1000], false);
1027
1054
  }
1028
1055
 
1029
1056
  function autoBootType(keys) {
1030
1057
  console.log("Auto typing '" + keys + "'");
1031
1058
  utils.noteEvent("init", "autochain");
1032
1059
 
1033
- const bbcKeys = utils.stringToBBCKeys(keys);
1034
- sendRawKeyboardToBBC([1000].concat(bbcKeys), false);
1060
+ const bbcKeys = stringToMachineKeys(keys);
1061
+ sendRawKeyboard([1000].concat(bbcKeys), false);
1035
1062
  }
1036
1063
 
1037
1064
  function autoChainTape() {
1038
1065
  console.log("Auto Chaining Tape");
1039
1066
  utils.noteEvent("init", "autochain");
1040
1067
 
1041
- const bbcKeys = utils.stringToBBCKeys('*TAPE\nCH.""\n');
1042
- sendRawKeyboardToBBC([1000].concat(bbcKeys), false);
1068
+ const bbcKeys = stringToMachineKeys('*TAPE\nCH.""\n');
1069
+ sendRawKeyboard([1000].concat(bbcKeys), false);
1043
1070
  }
1044
1071
 
1045
1072
  function autoRunTape() {
1046
1073
  console.log("Auto Running Tape");
1047
1074
  utils.noteEvent("init", "autorun");
1048
1075
 
1049
- const bbcKeys = utils.stringToBBCKeys("*TAPE\n*/\n");
1050
- sendRawKeyboardToBBC([1000].concat(bbcKeys), false);
1076
+ const bbcKeys = stringToMachineKeys("*TAPE\n*/\n");
1077
+ sendRawKeyboard([1000].concat(bbcKeys), false);
1051
1078
  }
1052
1079
 
1053
1080
  function autoRunBasic() {
1054
1081
  console.log("Auto Running basic");
1055
1082
  utils.noteEvent("init", "autorunbasic");
1056
1083
 
1057
- const bbcKeys = utils.stringToBBCKeys("RUN\n");
1058
- sendRawKeyboardToBBC([1000].concat(bbcKeys), false);
1084
+ const bbcKeys = stringToMachineKeys("RUN\n");
1085
+ sendRawKeyboard([1000].concat(bbcKeys), false);
1059
1086
  }
1060
1087
 
1061
1088
  function updateUrl() {
@@ -1175,16 +1202,17 @@ async function loadTapeImage(tapeImage) {
1175
1202
  const split = splitImage(tapeImage);
1176
1203
  tapeImage = split.image;
1177
1204
  const schema = split.schema;
1205
+ const isAtom = model.isAtom;
1178
1206
 
1179
1207
  switch (schema) {
1180
1208
  case "|":
1181
1209
  case "sth":
1182
- return await loadTapeFromData(tapeImage, await tapeSth.fetch(tapeImage));
1210
+ return await loadTapeFromData(tapeImage, await tapeSth.fetch(tapeImage), isAtom);
1183
1211
 
1184
1212
  case "data": {
1185
1213
  const arr = Array.prototype.map.call(atob(tapeImage), (x) => x.charCodeAt(0));
1186
1214
  const { name, data } = await utils.unzipDiscImage(arr);
1187
- return await loadTapeFromData(name, data);
1215
+ return await loadTapeFromData(name, data, isAtom);
1188
1216
  }
1189
1217
 
1190
1218
  case "http":
@@ -1199,11 +1227,20 @@ async function loadTapeImage(tapeImage) {
1199
1227
  tapeData = unzipped.data;
1200
1228
  tapeImage = unzipped.name;
1201
1229
  }
1202
- return await loadTapeFromData(tapeImage, tapeData);
1230
+ return await loadTapeFromData(tapeImage, tapeData, isAtom);
1203
1231
  }
1204
1232
 
1205
- default:
1206
- 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
+ }
1207
1244
  }
1208
1245
  }
1209
1246
 
@@ -1228,8 +1265,14 @@ document.getElementById("tape_load").addEventListener("change", async function (
1228
1265
  const file = evt.target.files[0];
1229
1266
  utils.noteEvent("local", "clickTape"); // NB no filename here
1230
1267
 
1231
- const binaryData = await readFileAsBinaryString(file);
1232
- 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));
1233
1276
  delete parsedQuery.tape;
1234
1277
  updateUrl();
1235
1278
  bootstrap.Modal.getInstance(document.getElementById("tapes"))?.hide();
@@ -1570,13 +1613,58 @@ for (const link of document.querySelectorAll("#tape-menu a")) {
1570
1613
 
1571
1614
  if (type === "rewind") {
1572
1615
  console.log("Rewinding tape to the start");
1573
- processor.acia.rewindTape();
1616
+ if (model.isAtom) {
1617
+ processor.atomppia.stopTape();
1618
+ processor.atomppia.rewindTape();
1619
+ updateTapeButton();
1620
+ } else {
1621
+ processor.acia.rewindTape();
1622
+ }
1574
1623
  } else {
1575
1624
  console.log("unknown type", type);
1576
1625
  }
1577
1626
  });
1578
1627
  }
1579
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
+
1580
1668
  function Light(name) {
1581
1669
  const dom = document.getElementById(name);
1582
1670
  let on = false;
@@ -1595,13 +1683,17 @@ const drive1 = new Light("drive1");
1595
1683
  const network = new Light("networklight");
1596
1684
 
1597
1685
  syncLights = function () {
1598
- caps.update(processor.sysvia.capsLockLight);
1599
- shift.update(processor.sysvia.shiftLockLight);
1600
- drive0.update(processor.fdc.motorOn[0]);
1601
- drive1.update(processor.fdc.motorOn[1]);
1602
- cassette.update(processor.acia.motorOn);
1603
- if (model.hasEconet) {
1604
- 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
+ }
1605
1697
  }
1606
1698
  };
1607
1699
 
@@ -1636,7 +1728,16 @@ const startPromise = (async () => {
1636
1728
  imageLoads.push(
1637
1729
  (async () => {
1638
1730
  const tape = await loadTapeImage(parsedQuery.tape);
1639
- 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);
1640
1741
  })(),
1641
1742
  );
1642
1743
  }