web-a2e 1.0.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 (295) hide show
  1. package/.clangd +5 -0
  2. package/.mcp.json +12 -0
  3. package/CLAUDE.md +362 -0
  4. package/CMakeLists.txt +774 -0
  5. package/LICENSE +21 -0
  6. package/README.md +392 -0
  7. package/build-wasm/generated/roms.cpp +2447 -0
  8. package/docker-compose.staging.yml +9 -0
  9. package/docs/basic-rom-disassembly.md +6663 -0
  10. package/docs/softswitch-comparison.md +273 -0
  11. package/docs/thunderclock-debug.md +89 -0
  12. package/examples/cube.bas +72 -0
  13. package/examples/hello.s +55 -0
  14. package/examples/scroll.s +140 -0
  15. package/package.json +18 -0
  16. package/public/assets/apple-logo-old.png +0 -0
  17. package/public/assets/apple-logo.png +0 -0
  18. package/public/assets/drive-closed-light-on.png +0 -0
  19. package/public/assets/drive-closed.png +0 -0
  20. package/public/assets/drive-open-light-on.png +0 -0
  21. package/public/assets/drive-open.png +0 -0
  22. package/public/audio-worklet.js +82 -0
  23. package/public/disks/Apple DOS 3.3 January 1983.dsk +0 -0
  24. package/public/disks/ProDOS 2.4.3.po +0 -0
  25. package/public/disks/h32mb.2mg +0 -0
  26. package/public/disks/library.json +26 -0
  27. package/public/docs/llms/llm-assembler.txt +90 -0
  28. package/public/docs/llms/llm-basic-program.txt +256 -0
  29. package/public/docs/llms/llm-disk-drives.txt +72 -0
  30. package/public/docs/llms/llm-file-explorer.txt +50 -0
  31. package/public/docs/llms/llm-hard-drives.txt +80 -0
  32. package/public/docs/llms/llm-main.txt +51 -0
  33. package/public/docs/llms/llm-slot-configuration.txt +66 -0
  34. package/public/icons/icon-192.svg +4 -0
  35. package/public/icons/icon-512.svg +4 -0
  36. package/public/index.html +661 -0
  37. package/public/llms.txt +49 -0
  38. package/public/manifest.json +29 -0
  39. package/public/shaders/burnin.glsl +22 -0
  40. package/public/shaders/crt.glsl +706 -0
  41. package/public/shaders/edge.glsl +109 -0
  42. package/public/shaders/vertex.glsl +8 -0
  43. package/public/sw.js +186 -0
  44. package/roms/341-0027.bin +0 -0
  45. package/roms/341-0160-A-US-UK.bin +0 -0
  46. package/roms/341-0160-A.bin +0 -0
  47. package/roms/342-0273-A-US-UK.bin +0 -0
  48. package/roms/342-0349-B-C0-FF.bin +0 -0
  49. package/roms/Apple Mouse Interface Card ROM - 342-0270-C.bin +0 -0
  50. package/roms/Thunderclock Plus ROM.bin +0 -0
  51. package/scripts/generate_roms.sh +69 -0
  52. package/src/bindings/wasm_interface.cpp +1940 -0
  53. package/src/core/assembler/assembler.cpp +1239 -0
  54. package/src/core/assembler/assembler.hpp +115 -0
  55. package/src/core/audio/audio.cpp +160 -0
  56. package/src/core/audio/audio.hpp +81 -0
  57. package/src/core/basic/basic_detokenizer.cpp +436 -0
  58. package/src/core/basic/basic_detokenizer.hpp +41 -0
  59. package/src/core/basic/basic_tokenizer.cpp +286 -0
  60. package/src/core/basic/basic_tokenizer.hpp +26 -0
  61. package/src/core/basic/basic_tokens.hpp +295 -0
  62. package/src/core/cards/disk2_card.cpp +568 -0
  63. package/src/core/cards/disk2_card.hpp +316 -0
  64. package/src/core/cards/expansion_card.hpp +185 -0
  65. package/src/core/cards/mockingboard/ay8910.cpp +616 -0
  66. package/src/core/cards/mockingboard/ay8910.hpp +159 -0
  67. package/src/core/cards/mockingboard/via6522.cpp +530 -0
  68. package/src/core/cards/mockingboard/via6522.hpp +163 -0
  69. package/src/core/cards/mockingboard_card.cpp +312 -0
  70. package/src/core/cards/mockingboard_card.hpp +159 -0
  71. package/src/core/cards/mouse_card.cpp +654 -0
  72. package/src/core/cards/mouse_card.hpp +190 -0
  73. package/src/core/cards/smartport/block_device.cpp +202 -0
  74. package/src/core/cards/smartport/block_device.hpp +60 -0
  75. package/src/core/cards/smartport/smartport_card.cpp +603 -0
  76. package/src/core/cards/smartport/smartport_card.hpp +120 -0
  77. package/src/core/cards/thunderclock_card.cpp +237 -0
  78. package/src/core/cards/thunderclock_card.hpp +122 -0
  79. package/src/core/cpu/cpu6502.cpp +1609 -0
  80. package/src/core/cpu/cpu6502.hpp +203 -0
  81. package/src/core/debug/condition_evaluator.cpp +470 -0
  82. package/src/core/debug/condition_evaluator.hpp +87 -0
  83. package/src/core/disassembler/disassembler.cpp +552 -0
  84. package/src/core/disassembler/disassembler.hpp +171 -0
  85. package/src/core/disk-image/disk_image.hpp +267 -0
  86. package/src/core/disk-image/dsk_disk_image.cpp +827 -0
  87. package/src/core/disk-image/dsk_disk_image.hpp +204 -0
  88. package/src/core/disk-image/gcr_encoding.cpp +147 -0
  89. package/src/core/disk-image/gcr_encoding.hpp +78 -0
  90. package/src/core/disk-image/woz_disk_image.cpp +1049 -0
  91. package/src/core/disk-image/woz_disk_image.hpp +343 -0
  92. package/src/core/emulator.cpp +2126 -0
  93. package/src/core/emulator.hpp +434 -0
  94. package/src/core/filesystem/dos33.cpp +178 -0
  95. package/src/core/filesystem/dos33.hpp +66 -0
  96. package/src/core/filesystem/pascal.cpp +262 -0
  97. package/src/core/filesystem/pascal.hpp +87 -0
  98. package/src/core/filesystem/prodos.cpp +369 -0
  99. package/src/core/filesystem/prodos.hpp +119 -0
  100. package/src/core/input/keyboard.cpp +227 -0
  101. package/src/core/input/keyboard.hpp +111 -0
  102. package/src/core/mmu/mmu.cpp +1387 -0
  103. package/src/core/mmu/mmu.hpp +236 -0
  104. package/src/core/types.hpp +196 -0
  105. package/src/core/video/video.cpp +680 -0
  106. package/src/core/video/video.hpp +156 -0
  107. package/src/css/assembler-editor.css +1617 -0
  108. package/src/css/base.css +470 -0
  109. package/src/css/basic-debugger.css +791 -0
  110. package/src/css/basic-editor.css +792 -0
  111. package/src/css/controls.css +783 -0
  112. package/src/css/cpu-debugger.css +1413 -0
  113. package/src/css/debug-base.css +160 -0
  114. package/src/css/debug-windows.css +6455 -0
  115. package/src/css/disk-drives.css +406 -0
  116. package/src/css/documentation.css +392 -0
  117. package/src/css/file-explorer.css +867 -0
  118. package/src/css/hard-drive.css +180 -0
  119. package/src/css/layout.css +217 -0
  120. package/src/css/memory-windows.css +798 -0
  121. package/src/css/modals.css +510 -0
  122. package/src/css/monitor.css +425 -0
  123. package/src/css/release-notes.css +101 -0
  124. package/src/css/responsive.css +400 -0
  125. package/src/css/rule-builder.css +340 -0
  126. package/src/css/save-states.css +201 -0
  127. package/src/css/settings-windows.css +1231 -0
  128. package/src/css/window-switcher.css +150 -0
  129. package/src/js/agent/agent-manager.js +643 -0
  130. package/src/js/agent/agent-tools.js +293 -0
  131. package/src/js/agent/agent-version-tools.js +131 -0
  132. package/src/js/agent/assembler-tools.js +357 -0
  133. package/src/js/agent/basic-program-tools.js +894 -0
  134. package/src/js/agent/disk-tools.js +417 -0
  135. package/src/js/agent/file-explorer-tools.js +269 -0
  136. package/src/js/agent/index.js +13 -0
  137. package/src/js/agent/main-tools.js +222 -0
  138. package/src/js/agent/slot-tools.js +303 -0
  139. package/src/js/agent/smartport-tools.js +257 -0
  140. package/src/js/agent/window-tools.js +80 -0
  141. package/src/js/audio/audio-driver.js +417 -0
  142. package/src/js/audio/audio-worklet.js +85 -0
  143. package/src/js/audio/index.js +8 -0
  144. package/src/js/config/default-layout.js +34 -0
  145. package/src/js/config/version.js +8 -0
  146. package/src/js/data/apple2-rom-routines.js +577 -0
  147. package/src/js/debug/assembler-editor-window.js +2993 -0
  148. package/src/js/debug/basic-breakpoint-manager.js +529 -0
  149. package/src/js/debug/basic-program-parser.js +436 -0
  150. package/src/js/debug/basic-program-window.js +2594 -0
  151. package/src/js/debug/basic-variable-inspector.js +447 -0
  152. package/src/js/debug/breakpoint-manager.js +472 -0
  153. package/src/js/debug/cpu-debugger-window.js +2396 -0
  154. package/src/js/debug/index.js +22 -0
  155. package/src/js/debug/label-manager.js +238 -0
  156. package/src/js/debug/memory-browser-window.js +416 -0
  157. package/src/js/debug/memory-heat-map-window.js +481 -0
  158. package/src/js/debug/memory-map-window.js +206 -0
  159. package/src/js/debug/mockingboard-window.js +882 -0
  160. package/src/js/debug/mouse-card-window.js +355 -0
  161. package/src/js/debug/rule-builder-window.js +648 -0
  162. package/src/js/debug/soft-switch-window.js +458 -0
  163. package/src/js/debug/stack-viewer-window.js +221 -0
  164. package/src/js/debug/symbols.js +416 -0
  165. package/src/js/debug/trace-panel.js +291 -0
  166. package/src/js/debug/zero-page-watch-window.js +297 -0
  167. package/src/js/disk-manager/disk-drives-window.js +212 -0
  168. package/src/js/disk-manager/disk-operations.js +284 -0
  169. package/src/js/disk-manager/disk-persistence.js +301 -0
  170. package/src/js/disk-manager/disk-surface-renderer.js +388 -0
  171. package/src/js/disk-manager/drive-sounds.js +139 -0
  172. package/src/js/disk-manager/hard-drive-manager.js +481 -0
  173. package/src/js/disk-manager/hard-drive-persistence.js +187 -0
  174. package/src/js/disk-manager/hard-drive-window.js +57 -0
  175. package/src/js/disk-manager/index.js +890 -0
  176. package/src/js/display/display-settings-window.js +383 -0
  177. package/src/js/display/index.js +10 -0
  178. package/src/js/display/screen-window.js +342 -0
  179. package/src/js/display/webgl-renderer.js +705 -0
  180. package/src/js/file-explorer/disassembler.js +574 -0
  181. package/src/js/file-explorer/dos33.js +266 -0
  182. package/src/js/file-explorer/file-viewer.js +359 -0
  183. package/src/js/file-explorer/index.js +1261 -0
  184. package/src/js/file-explorer/prodos.js +549 -0
  185. package/src/js/file-explorer/utils.js +67 -0
  186. package/src/js/help/documentation-window.js +1096 -0
  187. package/src/js/help/index.js +10 -0
  188. package/src/js/help/release-notes-window.js +85 -0
  189. package/src/js/help/release-notes.js +612 -0
  190. package/src/js/input/gamepad-handler.js +176 -0
  191. package/src/js/input/index.js +12 -0
  192. package/src/js/input/input-handler.js +396 -0
  193. package/src/js/input/joystick-window.js +404 -0
  194. package/src/js/input/mouse-handler.js +99 -0
  195. package/src/js/input/text-selection.js +462 -0
  196. package/src/js/main.js +653 -0
  197. package/src/js/state/index.js +15 -0
  198. package/src/js/state/save-states-window.js +393 -0
  199. package/src/js/state/state-manager.js +409 -0
  200. package/src/js/state/state-persistence.js +218 -0
  201. package/src/js/ui/confirm.js +43 -0
  202. package/src/js/ui/disk-drive-positioner.js +347 -0
  203. package/src/js/ui/reminder-controller.js +129 -0
  204. package/src/js/ui/slot-configuration-window.js +560 -0
  205. package/src/js/ui/theme-manager.js +61 -0
  206. package/src/js/ui/toast.js +44 -0
  207. package/src/js/ui/ui-controller.js +897 -0
  208. package/src/js/ui/window-switcher.js +275 -0
  209. package/src/js/utils/basic-autocomplete.js +832 -0
  210. package/src/js/utils/basic-highlighting.js +473 -0
  211. package/src/js/utils/basic-tokenizer.js +153 -0
  212. package/src/js/utils/basic-tokens.js +117 -0
  213. package/src/js/utils/constants.js +28 -0
  214. package/src/js/utils/indexeddb-helper.js +225 -0
  215. package/src/js/utils/merlin-editor-support.js +905 -0
  216. package/src/js/utils/merlin-highlighting.js +551 -0
  217. package/src/js/utils/storage.js +125 -0
  218. package/src/js/utils/string-utils.js +19 -0
  219. package/src/js/utils/wasm-memory.js +54 -0
  220. package/src/js/windows/base-window.js +690 -0
  221. package/src/js/windows/index.js +9 -0
  222. package/src/js/windows/window-manager.js +375 -0
  223. package/tests/catch2/catch.hpp +17976 -0
  224. package/tests/common/basic_program_builder.cpp +119 -0
  225. package/tests/common/basic_program_builder.hpp +209 -0
  226. package/tests/common/disk_image_builder.cpp +444 -0
  227. package/tests/common/disk_image_builder.hpp +141 -0
  228. package/tests/common/test_helpers.hpp +118 -0
  229. package/tests/gcr/gcr-test.cpp +142 -0
  230. package/tests/integration/check-rom.js +70 -0
  231. package/tests/integration/compare-boot.js +239 -0
  232. package/tests/integration/crash-trace.js +102 -0
  233. package/tests/integration/disk-boot-test.js +264 -0
  234. package/tests/integration/memory-crash.js +108 -0
  235. package/tests/integration/nibble-read-test.js +249 -0
  236. package/tests/integration/phase-test.js +159 -0
  237. package/tests/integration/test_emulator.cpp +291 -0
  238. package/tests/integration/test_emulator_basic.cpp +91 -0
  239. package/tests/integration/test_emulator_debug.cpp +344 -0
  240. package/tests/integration/test_emulator_disk.cpp +153 -0
  241. package/tests/integration/test_emulator_state.cpp +163 -0
  242. package/tests/klaus/6502_functional_test.bin +0 -0
  243. package/tests/klaus/65C02_extended_opcodes_test.bin +0 -0
  244. package/tests/klaus/klaus_6502_test.cpp +184 -0
  245. package/tests/klaus/klaus_65c02_test.cpp +197 -0
  246. package/tests/thunderclock/thunderclock_mmu_test.cpp +304 -0
  247. package/tests/thunderclock/thunderclock_test.cpp +550 -0
  248. package/tests/unit/test_assembler.cpp +521 -0
  249. package/tests/unit/test_audio.cpp +196 -0
  250. package/tests/unit/test_ay8910.cpp +311 -0
  251. package/tests/unit/test_basic_detokenizer.cpp +265 -0
  252. package/tests/unit/test_basic_tokenizer.cpp +382 -0
  253. package/tests/unit/test_block_device.cpp +259 -0
  254. package/tests/unit/test_condition_evaluator.cpp +219 -0
  255. package/tests/unit/test_cpu6502.cpp +1301 -0
  256. package/tests/unit/test_cpu_addressing.cpp +361 -0
  257. package/tests/unit/test_cpu_cycle_counts.cpp +409 -0
  258. package/tests/unit/test_cpu_decimal.cpp +166 -0
  259. package/tests/unit/test_cpu_interrupts.cpp +285 -0
  260. package/tests/unit/test_disassembler.cpp +323 -0
  261. package/tests/unit/test_disk2_card.cpp +330 -0
  262. package/tests/unit/test_dos33.cpp +273 -0
  263. package/tests/unit/test_dsk_disk_image.cpp +315 -0
  264. package/tests/unit/test_expansion_card.cpp +178 -0
  265. package/tests/unit/test_gcr_encoding.cpp +232 -0
  266. package/tests/unit/test_keyboard.cpp +262 -0
  267. package/tests/unit/test_mmu.cpp +555 -0
  268. package/tests/unit/test_mmu_slots.cpp +323 -0
  269. package/tests/unit/test_mockingboard.cpp +352 -0
  270. package/tests/unit/test_mouse_card.cpp +386 -0
  271. package/tests/unit/test_pascal.cpp +248 -0
  272. package/tests/unit/test_prodos.cpp +259 -0
  273. package/tests/unit/test_smartport_card.cpp +321 -0
  274. package/tests/unit/test_thunderclock.cpp +354 -0
  275. package/tests/unit/test_via6522.cpp +323 -0
  276. package/tests/unit/test_video.cpp +319 -0
  277. package/tests/unit/test_woz_disk_image.cpp +257 -0
  278. package/vite.config.js +96 -0
  279. package/wiki/AI-Agent.md +372 -0
  280. package/wiki/Architecture-Overview.md +303 -0
  281. package/wiki/Audio-System.md +449 -0
  282. package/wiki/CPU-Emulation.md +477 -0
  283. package/wiki/Debugger.md +516 -0
  284. package/wiki/Disk-Drives.md +161 -0
  285. package/wiki/Disk-System-Internals.md +547 -0
  286. package/wiki/Display-Settings.md +88 -0
  287. package/wiki/Expansion-Slots.md +187 -0
  288. package/wiki/File-Explorer.md +259 -0
  289. package/wiki/Getting-Started.md +156 -0
  290. package/wiki/Home.md +69 -0
  291. package/wiki/Input-Devices.md +183 -0
  292. package/wiki/Keyboard-Shortcuts.md +158 -0
  293. package/wiki/Memory-System.md +364 -0
  294. package/wiki/Save-States.md +172 -0
  295. package/wiki/Video-Rendering.md +658 -0
@@ -0,0 +1,417 @@
1
+ /*
2
+ * audio-driver.js - Web Audio API driver for emulator audio and timing
3
+ *
4
+ * Written by
5
+ * Mike Daley <michael_daley@icloud.com>
6
+ */
7
+
8
+ // Audio constants
9
+ const SAMPLE_RATE = 48000;
10
+ const AUDIO_BUFFER_SIZE = 128; // AudioWorklet processes 128 samples at a time
11
+ const CYCLES_PER_SECOND = 1023000;
12
+ const DEFAULT_VOLUME = 0.5;
13
+
14
+ export class AudioDriver {
15
+ constructor(wasmModule) {
16
+ this.wasmModule = wasmModule;
17
+ this.audioContext = null;
18
+ this.workletNode = null;
19
+ this.gainNode = null;
20
+
21
+ this.sampleRate = SAMPLE_RATE;
22
+ this.bufferSize = AUDIO_BUFFER_SIZE;
23
+ this.running = false;
24
+ this.muted = this.loadMuted();
25
+ this.volume = this.loadVolume(); // Load saved volume or default to 0.5
26
+ this.speed = 1;
27
+
28
+ // Fallback to ScriptProcessorNode if AudioWorklet not available
29
+ this.useWorklet = typeof AudioWorkletNode !== "undefined";
30
+ this.scriptProcessor = null;
31
+
32
+ // Frame synchronization callback
33
+ this.onFrameReady = null;
34
+
35
+ // Pre-allocated WASM buffer for audio sample generation to avoid
36
+ // malloc/free churn (~375 calls/sec) that causes GC stutter in Safari/Brave
37
+ this.wasmBufferPtr = 0;
38
+ this.wasmBufferSamples = 0;
39
+
40
+ // Sync C++ audio state with saved JS settings
41
+ if (this.wasmModule._setAudioVolume) {
42
+ this.wasmModule._setAudioVolume(this.volume);
43
+ }
44
+ if (this.wasmModule._setAudioMuted) {
45
+ this.wasmModule._setAudioMuted(this.muted);
46
+ }
47
+ }
48
+
49
+ async start() {
50
+ if (this.running) return;
51
+
52
+ try {
53
+ // Create audio context
54
+ this.audioContext = new (window.AudioContext ||
55
+ window.webkitAudioContext)({
56
+ sampleRate: this.sampleRate,
57
+ });
58
+
59
+ // Check if audio context is suspended (browser autoplay policy)
60
+ if (this.audioContext.state === "suspended") {
61
+ console.log("Audio context suspended, using fallback timing until user interaction");
62
+ this.startFallbackTiming();
63
+
64
+ // Set up listener to resume audio on user interaction
65
+ this.setupAutoResumeAudio();
66
+ return;
67
+ }
68
+
69
+ await this.initAudioNodes();
70
+ } catch (error) {
71
+ console.error("Failed to start audio driver:", error);
72
+ // Fall back to non-audio timing
73
+ this.startFallbackTiming();
74
+ }
75
+ }
76
+
77
+ async initAudioNodes() {
78
+ // Create gain node for volume control
79
+ this.gainNode = this.audioContext.createGain();
80
+ this.gainNode.connect(this.audioContext.destination);
81
+ this.gainNode.gain.value = this.muted ? 0 : this.volume;
82
+
83
+ if (this.useWorklet) {
84
+ await this.startWithWorklet();
85
+ } else {
86
+ this.startWithScriptProcessor();
87
+ }
88
+
89
+ this.running = true;
90
+ console.log("Audio driver started");
91
+ }
92
+
93
+ setupAutoResumeAudio() {
94
+ const resumeAudio = async () => {
95
+ if (this.audioContext && this.audioContext.state === "suspended") {
96
+ try {
97
+ await this.audioContext.resume();
98
+ console.log("Audio context resumed");
99
+
100
+ // Stop fallback timing
101
+ if (this.fallbackInterval) {
102
+ clearInterval(this.fallbackInterval);
103
+ this.fallbackInterval = null;
104
+ }
105
+
106
+ // Initialize proper audio nodes
107
+ await this.initAudioNodes();
108
+ } catch (e) {
109
+ console.error("Failed to resume audio context:", e);
110
+ }
111
+ }
112
+
113
+ // Remove listeners after first interaction
114
+ document.removeEventListener("click", resumeAudio);
115
+ document.removeEventListener("keydown", resumeAudio);
116
+ };
117
+
118
+ document.addEventListener("click", resumeAudio, { once: true });
119
+ document.addEventListener("keydown", resumeAudio, { once: true });
120
+ }
121
+
122
+ async startWithWorklet() {
123
+ // Register the audio worklet
124
+ // In dev mode, load from src; in production, load from root
125
+ const workletPath = import.meta.env.DEV
126
+ ? "/src/js/audio/audio-worklet.js"
127
+ : "/audio-worklet.js";
128
+ await this.audioContext.audioWorklet.addModule(workletPath);
129
+
130
+ // Create worklet node (stereo output)
131
+ this.workletNode = new AudioWorkletNode(
132
+ this.audioContext,
133
+ "apple-audio-processor",
134
+ {
135
+ numberOfInputs: 0,
136
+ numberOfOutputs: 1,
137
+ outputChannelCount: [2],
138
+ },
139
+ );
140
+
141
+ // Handle messages from worklet
142
+ this.workletNode.port.onmessage = (event) => {
143
+ // Check if worklet is still valid (may be null after stop)
144
+ if (!this.workletNode || !this.running) {
145
+ return;
146
+ }
147
+ if (event.data.type === "requestSamples") {
148
+ const samples = this.generateSamples(event.data.count);
149
+ if (this.workletNode && this.workletNode.port) {
150
+ this.workletNode.port.postMessage({
151
+ type: "samples",
152
+ data: samples,
153
+ }, [samples.buffer]);
154
+ }
155
+ }
156
+ };
157
+
158
+ this.workletNode.connect(this.gainNode);
159
+
160
+ // Start the worklet
161
+ this.workletNode.port.postMessage({ type: "start" });
162
+ }
163
+
164
+ startWithScriptProcessor() {
165
+ // Fallback for browsers without AudioWorklet support (stereo)
166
+ this.scriptProcessor = this.audioContext.createScriptProcessor(4096, 0, 2);
167
+
168
+ this.scriptProcessor.onaudioprocess = (event) => {
169
+ const leftChannel = event.outputBuffer.getChannelData(0);
170
+ const rightChannel = event.outputBuffer.getChannelData(1);
171
+ const samples = this.generateSamples(leftChannel.length);
172
+ // Deinterleave stereo samples
173
+ for (let i = 0; i < leftChannel.length; i++) {
174
+ leftChannel[i] = samples[i * 2];
175
+ rightChannel[i] = samples[i * 2 + 1];
176
+ }
177
+ };
178
+
179
+ this.scriptProcessor.connect(this.gainNode);
180
+ }
181
+
182
+ /**
183
+ * Start fallback timing when Web Audio API is unavailable or suspended.
184
+ *
185
+ * The Apple IIe runs at 1.023 MHz. Normally, timing is driven by the Web Audio API's
186
+ * sample rate (48kHz), which provides precise timing through the audio callback.
187
+ * When audio is unavailable (browser autoplay policy, no audio hardware, etc.),
188
+ * this fallback uses setInterval at 60Hz to maintain approximate timing.
189
+ *
190
+ * At 60Hz, each tick should execute ~17,050 cycles (1,023,000 / 60).
191
+ * Speed multiplier allows for fast-forward (speed > 1) or unlimited speed (speed = 0).
192
+ *
193
+ * Note: setInterval timing is less precise than Web Audio, so emulation timing
194
+ * may be slightly off. This is acceptable for basic functionality until audio resumes.
195
+ */
196
+ startFallbackTiming() {
197
+ const cyclesPerTick = CYCLES_PER_SECOND / 60;
198
+
199
+ this.fallbackInterval = setInterval(() => {
200
+ if (this.speed === 0) {
201
+ // Unlimited speed - run as fast as possible
202
+ this.wasmModule._runCycles(cyclesPerTick * 10);
203
+ } else {
204
+ this.wasmModule._runCycles(cyclesPerTick * this.speed);
205
+ }
206
+
207
+ // Check for frame updates
208
+ const framesReady = this.wasmModule._consumeFrameSamples();
209
+ if (framesReady > 0 && this.onFrameReady) {
210
+ this.onFrameReady(framesReady);
211
+ }
212
+ }, 1000 / 60);
213
+
214
+ this.running = true;
215
+ console.log("Using fallback timing (no audio)");
216
+ }
217
+
218
+ /**
219
+ * Generate audio samples by running the emulator and collecting speaker output.
220
+ *
221
+ * This is the core timing mechanism: the Web Audio API requests samples at 48kHz,
222
+ * and we run the emulator for exactly enough cycles to produce those samples.
223
+ * The WASM module tracks the ratio of CPU cycles to audio samples (1,023,000 Hz / 48,000 Hz ≈ 21.3 cycles per sample).
224
+ *
225
+ * The emulator's speaker produces 1-bit audio by toggling a soft switch ($C030).
226
+ * The WASM module converts these toggles into floating-point samples.
227
+ *
228
+ * Frame synchronization: The emulator also counts audio samples to determine when
229
+ * a video frame is complete (~17,050 cycles = ~800 samples per 60Hz frame).
230
+ * When a frame's worth of samples is generated, we trigger onFrameReady for rendering.
231
+ *
232
+ * @param {number} count - Number of audio samples to generate
233
+ * @returns {Float32Array} Generated audio samples in range [-1, 1]
234
+ */
235
+ ensureWasmBuffer(count) {
236
+ if (this.wasmBufferSamples < count) {
237
+ if (this.wasmBufferPtr) {
238
+ this.wasmModule._free(this.wasmBufferPtr);
239
+ }
240
+ this.wasmBufferPtr = this.wasmModule._malloc(count * 2 * 4);
241
+ this.wasmBufferSamples = count;
242
+ }
243
+ }
244
+
245
+ generateSamples(count) {
246
+ this.ensureWasmBuffer(count);
247
+
248
+ this.wasmModule._generateStereoAudioSamples(this.wasmBufferPtr, count);
249
+
250
+ // Copy interleaved stereo samples from WASM memory into a new Float32Array.
251
+ // A new array is required because the worklet path transfers buffer ownership.
252
+ const samples = new Float32Array(
253
+ this.wasmModule.HEAPF32.buffer,
254
+ this.wasmBufferPtr,
255
+ count * 2,
256
+ ).slice();
257
+
258
+ // Check if we've accumulated enough samples for one or more frames
259
+ const framesReady = this.wasmModule._consumeFrameSamples();
260
+ if (framesReady > 0 && this.onFrameReady) {
261
+ this.onFrameReady(framesReady);
262
+ }
263
+
264
+ return samples;
265
+ }
266
+
267
+ stop() {
268
+ if (!this.running) return;
269
+
270
+ // Set running to false first to stop any pending callbacks
271
+ this.running = false;
272
+
273
+ if (this.workletNode) {
274
+ try {
275
+ this.workletNode.port.postMessage({ type: "stop" });
276
+ this.workletNode.disconnect();
277
+ } catch (e) {
278
+ // Ignore errors during cleanup
279
+ }
280
+ this.workletNode = null;
281
+ }
282
+
283
+ if (this.scriptProcessor) {
284
+ try {
285
+ this.scriptProcessor.disconnect();
286
+ } catch (e) {
287
+ // Ignore errors during cleanup
288
+ }
289
+ this.scriptProcessor = null;
290
+ }
291
+
292
+ if (this.fallbackInterval) {
293
+ clearInterval(this.fallbackInterval);
294
+ this.fallbackInterval = null;
295
+ }
296
+
297
+ if (this.wasmBufferPtr) {
298
+ this.wasmModule._free(this.wasmBufferPtr);
299
+ this.wasmBufferPtr = 0;
300
+ this.wasmBufferSamples = 0;
301
+ }
302
+
303
+ if (this.audioContext) {
304
+ try {
305
+ this.audioContext.close();
306
+ } catch (e) {
307
+ // Ignore errors during cleanup
308
+ }
309
+ this.audioContext = null;
310
+ }
311
+
312
+ console.log("Audio driver stopped");
313
+ }
314
+
315
+ /**
316
+ * Set emulation speed multiplier.
317
+ * Reserved for future speed control feature (e.g., fast-forward, slow-motion).
318
+ * @param {number} speed - Speed multiplier (1 = normal, 0 = unlimited)
319
+ */
320
+ setSpeed(speed) {
321
+ this.speed = speed;
322
+ }
323
+
324
+ toggleMute() {
325
+ this.muted = !this.muted;
326
+ if (this.gainNode) {
327
+ this.gainNode.gain.value = this.muted ? 0 : this.volume;
328
+ }
329
+ if (this.wasmModule._setAudioMuted) {
330
+ this.wasmModule._setAudioMuted(this.muted);
331
+ }
332
+ this.saveMuted();
333
+ }
334
+
335
+ isMuted() {
336
+ return this.muted;
337
+ }
338
+
339
+ mute() {
340
+ this.muted = true;
341
+ if (this.gainNode) {
342
+ this.gainNode.gain.value = 0;
343
+ }
344
+ if (this.wasmModule._setAudioMuted) {
345
+ this.wasmModule._setAudioMuted(true);
346
+ }
347
+ this.saveMuted();
348
+ }
349
+
350
+ unmute() {
351
+ this.muted = false;
352
+ if (this.gainNode) {
353
+ this.gainNode.gain.value = this.volume;
354
+ }
355
+ if (this.wasmModule._setAudioMuted) {
356
+ this.wasmModule._setAudioMuted(false);
357
+ }
358
+ this.saveMuted();
359
+ }
360
+
361
+ loadMuted() {
362
+ try {
363
+ return localStorage.getItem('a2e-muted') === 'true';
364
+ } catch (e) {
365
+ // Ignore localStorage errors
366
+ }
367
+ return false;
368
+ }
369
+
370
+ saveMuted() {
371
+ try {
372
+ localStorage.setItem('a2e-muted', this.muted.toString());
373
+ } catch (e) {
374
+ // Ignore localStorage errors
375
+ }
376
+ }
377
+
378
+ getVolume() {
379
+ return this.volume;
380
+ }
381
+
382
+ setVolume(volume) {
383
+ this.volume = Math.max(0, Math.min(1, volume));
384
+ if (this.gainNode) {
385
+ this.gainNode.gain.value = this.muted ? 0 : this.volume;
386
+ }
387
+ // Sync C++ audio volume for speaker and Mockingboard pre-mix scaling
388
+ if (this.wasmModule._setAudioVolume) {
389
+ this.wasmModule._setAudioVolume(this.volume);
390
+ }
391
+ this.saveVolume();
392
+ }
393
+
394
+ loadVolume() {
395
+ try {
396
+ const saved = localStorage.getItem('a2e-volume');
397
+ if (saved !== null) {
398
+ const vol = parseFloat(saved);
399
+ if (!isNaN(vol) && vol >= 0 && vol <= 1) {
400
+ return vol;
401
+ }
402
+ }
403
+ } catch (e) {
404
+ // Ignore localStorage errors
405
+ }
406
+ return DEFAULT_VOLUME;
407
+ }
408
+
409
+ saveVolume() {
410
+ try {
411
+ localStorage.setItem('a2e-volume', this.volume.toString());
412
+ } catch (e) {
413
+ // Ignore localStorage errors
414
+ }
415
+ }
416
+
417
+ }
@@ -0,0 +1,85 @@
1
+ /*
2
+ * audio-worklet.js - AudioWorklet processor for sample generation and emulator timing
3
+ *
4
+ * Written by
5
+ * Mike Daley <michael_daley@icloud.com>
6
+ */
7
+
8
+ class AppleAudioProcessor extends AudioWorkletProcessor {
9
+ constructor() {
10
+ super();
11
+
12
+ this.running = false;
13
+ this.sampleBuffer = new Float32Array(0); // Interleaved stereo [L0, R0, L1, R1, ...]
14
+ this.bufferReadPos = 0;
15
+ this.pendingRequest = false;
16
+
17
+ // Handle messages from main thread
18
+ this.port.onmessage = (event) => {
19
+ if (event.data.type === "start") {
20
+ this.running = true;
21
+ this.pendingRequest = false;
22
+ } else if (event.data.type === "stop") {
23
+ this.running = false;
24
+ this.pendingRequest = false;
25
+ } else if (event.data.type === "samples") {
26
+ // Append new interleaved stereo samples to existing buffer
27
+ const newSamples = event.data.data;
28
+ const remaining = this.sampleBuffer.length - this.bufferReadPos;
29
+
30
+ if (remaining > 0) {
31
+ // Append to remaining samples
32
+ const combined = new Float32Array(remaining + newSamples.length);
33
+ combined.set(this.sampleBuffer.subarray(this.bufferReadPos), 0);
34
+ combined.set(newSamples, remaining);
35
+ this.sampleBuffer = combined;
36
+ } else {
37
+ this.sampleBuffer = newSamples;
38
+ }
39
+ this.bufferReadPos = 0;
40
+ this.pendingRequest = false;
41
+ }
42
+ };
43
+ }
44
+
45
+ process(inputs, outputs, parameters) {
46
+ const output = outputs[0];
47
+ const leftChannel = output[0];
48
+ const rightChannel = output[1];
49
+
50
+ if (!this.running || !leftChannel) {
51
+ // Fill with silence
52
+ if (leftChannel) leftChannel.fill(0);
53
+ if (rightChannel) rightChannel.fill(0);
54
+ return true;
55
+ }
56
+
57
+ // Buffer contains interleaved stereo samples, so remaining frames = remaining / 2
58
+ const remainingFrames = (this.sampleBuffer.length - this.bufferReadPos) / 2;
59
+
60
+ // Request more samples if buffer is getting low and no request pending
61
+ // Keep at least 2 frames worth of buffer to avoid underruns
62
+ if (remainingFrames < 1600 && !this.pendingRequest) {
63
+ this.pendingRequest = true;
64
+ this.port.postMessage({
65
+ type: "requestSamples",
66
+ count: 1600, // Number of sample frames (stereo pairs)
67
+ });
68
+ }
69
+
70
+ // Copy interleaved samples to separate L/R channels
71
+ for (let i = 0; i < leftChannel.length; i++) {
72
+ if (this.bufferReadPos + 1 < this.sampleBuffer.length) {
73
+ leftChannel[i] = this.sampleBuffer[this.bufferReadPos++];
74
+ rightChannel[i] = this.sampleBuffer[this.bufferReadPos++];
75
+ } else {
76
+ leftChannel[i] = 0;
77
+ rightChannel[i] = 0;
78
+ }
79
+ }
80
+
81
+ return true;
82
+ }
83
+ }
84
+
85
+ registerProcessor("apple-audio-processor", AppleAudioProcessor);
@@ -0,0 +1,8 @@
1
+ /*
2
+ * index.js - Audio subsystem initialization and exports
3
+ *
4
+ * Written by
5
+ * Mike Daley <michael_daley@icloud.com>
6
+ */
7
+
8
+ export { AudioDriver } from "./audio-driver.js";
@@ -0,0 +1,34 @@
1
+ /*
2
+ * default-layout.js - Default window layout for first-time users
3
+ *
4
+ * Defines which windows are visible and their position/size when the app
5
+ * starts with no saved state. Windows not listed stay hidden at their
6
+ * constructor defaults.
7
+ *
8
+ * Special values:
9
+ * "viewport-fill" — fills available viewport with margin (screen-window)
10
+ *
11
+ * Written by
12
+ * Mike Daley <michael_daley@icloud.com>
13
+ */
14
+
15
+ export const DEFAULT_LAYOUT = [
16
+ {
17
+ id: "screen-window",
18
+ x: 20,
19
+ y: 70,
20
+ width: 587,
21
+ height: 475,
22
+ visible: true,
23
+ viewportLocked: false,
24
+ },
25
+ {
26
+ id: "disk-drives",
27
+ x: 624,
28
+ y: 156,
29
+ width: 600,
30
+ height: 310,
31
+ visible: true,
32
+ viewportLocked: false,
33
+ },
34
+ ];
@@ -0,0 +1,8 @@
1
+ /*
2
+ * version.js - Application version configuration
3
+ *
4
+ * Written by
5
+ * Mike Daley <michael_daley@icloud.com>
6
+ */
7
+
8
+ export const VERSION = "1.0.9";