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,449 @@
1
+ # Audio System
2
+
3
+ The emulator's audio subsystem handles two distinct sound sources: the Apple IIe's built-in 1-bit speaker and the optional Mockingboard expansion card with its dual AY-3-8910 programmable sound generators. Audio also serves as the primary timing mechanism for the entire emulator.
4
+
5
+ ---
6
+
7
+ ## Table of Contents
8
+
9
+ - [Architecture Overview](#architecture-overview)
10
+ - [Audio-Driven Timing](#audio-driven-timing)
11
+ - [Speaker Emulation](#speaker-emulation)
12
+ - [Toggle Recording](#toggle-recording)
13
+ - [Sample Generation](#sample-generation)
14
+ - [Low-Pass Filter](#low-pass-filter)
15
+ - [DC Offset Removal](#dc-offset-removal)
16
+ - [Mockingboard Sound Card](#mockingboard-sound-card)
17
+ - [Hardware Layout](#hardware-layout)
18
+ - [VIA 6522 Emulation](#via-6522-emulation)
19
+ - [AY-3-8910 Sound Generator](#ay-3-8910-sound-generator)
20
+ - [Tone Generation](#tone-generation)
21
+ - [Noise Generation](#noise-generation)
22
+ - [Envelope Generator](#envelope-generator)
23
+ - [Mixer and Channel Output](#mixer-and-channel-output)
24
+ - [Phase Coherence](#phase-coherence)
25
+ - [Incremental Audio Generation](#incremental-audio-generation)
26
+ - [Audio Mixing Pipeline](#audio-mixing-pipeline)
27
+ - [JavaScript Audio Layer](#javascript-audio-layer)
28
+ - [AudioDriver](#audiodriver)
29
+ - [AudioWorklet](#audioworklet)
30
+ - [Fallback Timing](#fallback-timing)
31
+ - [Volume and Mute Control](#volume-and-mute-control)
32
+ - [State Serialization](#state-serialization)
33
+ - [Source Files](#source-files)
34
+
35
+ ---
36
+
37
+ ## Architecture Overview
38
+
39
+ The audio system spans two layers:
40
+
41
+ | Layer | Component | Responsibility |
42
+ |-------|-----------|----------------|
43
+ | C++ (WASM) | `Audio` | Speaker toggle recording, sample generation, mixing |
44
+ | C++ (WASM) | `MockingboardCard` | VIA 6522 timers, AY-3-8910 PSG emulation |
45
+ | JavaScript | `AudioDriver` | Web Audio API context, worklet management, volume |
46
+ | JavaScript | `AudioWorklet` | Real-time sample buffering on the audio thread |
47
+
48
+ Audio flows from C++ through WASM memory to the JavaScript AudioWorklet, which feeds the Web Audio API output.
49
+
50
+ ---
51
+
52
+ ## Audio-Driven Timing
53
+
54
+ The emulator uses the Web Audio API as its master clock rather than `requestAnimationFrame` or `setInterval`. This provides several advantages:
55
+
56
+ 1. The AudioWorklet runs on a dedicated audio thread at a precise 48 kHz sample rate
57
+ 2. Timing remains accurate even when the browser tab is backgrounded
58
+ 3. Frame synchronization derives naturally from the audio sample count
59
+
60
+ The timing chain works as follows:
61
+
62
+ 1. The AudioWorklet requests 1600 sample frames when its buffer runs low
63
+ 2. The `AudioDriver` calls `_generateStereoAudioSamples()` in WASM
64
+ 3. WASM runs the CPU for approximately 21.3 cycles per sample (1,023,000 Hz / 48,000 Hz)
65
+ 4. After approximately 800 samples (~17,030 cycles), a 60 Hz video frame boundary is reached
66
+ 5. The `onFrameReady` callback triggers the display to render the completed frame
67
+
68
+ ---
69
+
70
+ ## Speaker Emulation
71
+
72
+ The Apple IIe speaker is a simple 1-bit output device toggled by any access (read or write) to soft switch `$C030`. There is no volume control or waveform selection in hardware -- all sounds are produced by toggling the speaker at specific frequencies from software.
73
+
74
+ ### Toggle Recording
75
+
76
+ Each access to `$C030` records the CPU cycle count when the toggle occurred:
77
+
78
+ ```
79
+ toggleSpeaker(cycleCount) -> toggleCycles_[] vector (up to 8192 entries)
80
+ ```
81
+
82
+ The `MAX_TOGGLES` constant (8192) provides sufficient headroom for high-frequency audio within a single audio buffer period.
83
+
84
+ ### Sample Generation
85
+
86
+ The `generateStereoSamples()` method converts toggle events into audio samples using a bandwidth-limited reconstruction approach:
87
+
88
+ 1. For each output sample, determine the time window in CPU cycles
89
+ 2. Walk the toggle event list to find all toggles within that window
90
+ 3. Compute the ratio of "high time" vs "low time" within the sample period
91
+ 4. Output `(highTime - lowTime) / totalTime` as the raw sample value (-1.0 to +1.0)
92
+
93
+ This fractional approach naturally handles the case where multiple toggles occur within a single audio sample period, producing correct anti-aliased output.
94
+
95
+ ### Low-Pass Filter
96
+
97
+ A single-pole IIR low-pass filter smooths the speaker output:
98
+
99
+ | Parameter | Value | Effect |
100
+ |-----------|-------|--------|
101
+ | `FILTER_ALPHA` | 0.15 | Cutoff ~7.8 kHz at 48 kHz sample rate |
102
+
103
+ The filter equation is:
104
+
105
+ ```
106
+ filterState = filterState + FILTER_ALPHA * (rawValue - filterState)
107
+ ```
108
+
109
+ The 7.8 kHz cutoff preserves speaker harmonics while removing aliasing artifacts from the 1-bit reconstruction.
110
+
111
+ ### DC Offset Removal
112
+
113
+ A high-pass filter removes DC bias that accumulates when the speaker is held in one state:
114
+
115
+ | Parameter | Value | Effect |
116
+ |-----------|-------|--------|
117
+ | `DC_ALPHA` | 0.995 | Fast tracking of speaker state changes |
118
+
119
+ ```
120
+ dcOffset = DC_ALPHA * dcOffset + (1 - DC_ALPHA) * filterState
121
+ output = filterState - dcOffset
122
+ ```
123
+
124
+ The relatively fast time constant (compared to the Mockingboard's 0.9999) allows the DC removal to track the speaker's binary state transitions without introducing audible artifacts.
125
+
126
+ ---
127
+
128
+ ## Mockingboard Sound Card
129
+
130
+ The Mockingboard is a popular third-party sound card that adds multi-channel music and sound effects to the Apple IIe. The emulation models the complete hardware architecture: two VIA 6522 interface adapters, each controlling an AY-3-8910 programmable sound generator.
131
+
132
+ ### Hardware Layout
133
+
134
+ The Mockingboard occupies slot 4 by default. Unlike most expansion cards, it uses the slot ROM address space for its registers rather than the I/O space:
135
+
136
+ | Address Range | Component | Function |
137
+ |---------------|-----------|----------|
138
+ | `$C400-$C47F` | VIA 1 | Left channel PSG control (bit 7 = 0) |
139
+ | `$C480-$C4FF` | VIA 2 | Right channel PSG control (bit 7 = 1) |
140
+ | `$C0C0-$C0CF` | (unused) | Standard I/O space not used |
141
+
142
+ Register selection uses bits 0-3 of the address. Bit 7 selects which VIA is addressed.
143
+
144
+ ### VIA 6522 Emulation
145
+
146
+ Each VIA provides timer-driven interrupt generation and an 8-bit parallel port interface to its connected PSG. The VIA register map:
147
+
148
+ | Register | Offset | Name | Description |
149
+ |----------|--------|------|-------------|
150
+ | ORB | `$00` | Output Register B | PSG control lines (BC1, BDIR, RESET) |
151
+ | ORA | `$01` | Output Register A | PSG data bus |
152
+ | DDRB | `$02` | Data Direction B | Port B direction (1 = output) |
153
+ | DDRA | `$03` | Data Direction A | Port A direction (1 = output) |
154
+ | T1CL | `$04` | Timer 1 Counter Low | Read: counter low byte |
155
+ | T1CH | `$05` | Timer 1 Counter High | Write: loads counter and starts timer |
156
+ | T1LL | `$06` | Timer 1 Latch Low | Low byte of reload value |
157
+ | T1LH | `$07` | Timer 1 Latch High | High byte; clears T1 interrupt |
158
+ | T2CL | `$08` | Timer 2 Counter Low | Read: counter low; Write: latch low |
159
+ | T2CH | `$09` | Timer 2 Counter High | Write: loads counter and starts timer |
160
+ | SR | `$0A` | Shift Register | Serial data shift register |
161
+ | ACR | `$0B` | Auxiliary Control | Timer modes (bit 6: T1 free-running) |
162
+ | PCR | `$0C` | Peripheral Control | Handshake control |
163
+ | IFR | `$0D` | Interrupt Flag | Active interrupt sources |
164
+ | IER | `$0E` | Interrupt Enable | Interrupt mask (bit 7: set/clear mode) |
165
+ | ORA_NH | `$0F` | ORA (no handshake) | Same as ORA without clearing flags |
166
+
167
+ **Timer Behavior:**
168
+ - Timer 1 supports one-shot and free-running modes (ACR bit 6)
169
+ - Timer period = latch value + 2 cycles (per Rockwell datasheet)
170
+ - Counter is loaded with latch + 1 on write to T1CH (the +2nd cycle is the write itself)
171
+ - In free-running mode, the counter automatically reloads from the latch on underflow
172
+ - Timer 2 is one-shot only; it wraps after underflow but does not reload or re-fire
173
+
174
+ **IRQ Handling:**
175
+ - IRQ flag bits: T1 (`$40`), T2 (`$20`), CB1 (`$10`), CB2 (`$08`), SR (`$04`), CA1 (`$02`), CA2 (`$01`)
176
+ - IRQ callback only fires on 0-to-1 transition (edge detection prevents duplicate assertions)
177
+ - IER bit 7 controls set/clear mode: writing with bit 7 = 1 sets specified bits, bit 7 = 0 clears them
178
+
179
+ **PSG Control Protocol:**
180
+
181
+ The VIA communicates with the AY-3-8910 through Port B control lines:
182
+
183
+ | Port B Bit | Signal | Function |
184
+ |------------|--------|----------|
185
+ | 0 | BC1 | Bus Control 1 |
186
+ | 1 | BDIR | Bus Direction |
187
+ | 2 | ~RESET | PSG reset (active low) |
188
+
189
+ The PSG function is determined by the BC1/BDIR combination:
190
+
191
+ | BC1 | BDIR | Function | Operation |
192
+ |-----|------|----------|-----------|
193
+ | 0 | 0 | INACTIVE | No operation |
194
+ | 1 | 0 | READ | PSG drives data onto Port A |
195
+ | 0 | 1 | WRITE | Port A data written to selected PSG register |
196
+ | 1 | 1 | LATCH | Port A value selects PSG register address |
197
+
198
+ Operations only execute when transitioning from the INACTIVE state (AppleWin-compatible behavior). The address must be latched before reads or writes; writes to an unlatched address are rejected.
199
+
200
+ ### AY-3-8910 Sound Generator
201
+
202
+ Each PSG provides three independent tone channels, a noise generator, and an envelope generator. The PSG clock is derived from the Apple IIe CPU clock at approximately 1.023 MHz.
203
+
204
+ **Register Map:**
205
+
206
+ | Register | Address | Bits | Description |
207
+ |----------|---------|------|-------------|
208
+ | Tone A Fine | R0 | 7-0 | Channel A period low byte |
209
+ | Tone A Coarse | R1 | 3-0 | Channel A period high nibble |
210
+ | Tone B Fine | R2 | 7-0 | Channel B period low byte |
211
+ | Tone B Coarse | R3 | 3-0 | Channel B period high nibble |
212
+ | Tone C Fine | R4 | 7-0 | Channel C period low byte |
213
+ | Tone C Coarse | R5 | 3-0 | Channel C period high nibble |
214
+ | Noise Period | R6 | 4-0 | Noise generator period (5-bit) |
215
+ | Mixer | R7 | 5-0 | Tone/noise enable per channel |
216
+ | Amplitude A | R8 | 4-0 | Channel A volume or envelope mode |
217
+ | Amplitude B | R9 | 4-0 | Channel B volume or envelope mode |
218
+ | Amplitude C | R10 | 4-0 | Channel C volume or envelope mode |
219
+ | Envelope Fine | R11 | 7-0 | Envelope period low byte |
220
+ | Envelope Coarse | R12 | 7-0 | Envelope period high byte |
221
+ | Envelope Shape | R13 | 3-0 | Envelope shape control |
222
+ | I/O Port A | R14 | 7-0 | General-purpose I/O |
223
+ | I/O Port B | R15 | 7-0 | General-purpose I/O |
224
+
225
+ ### Tone Generation
226
+
227
+ Each tone channel uses a 12-bit period counter (fine + coarse registers). The counter increments at the master clock / 8 rate. When the counter reaches the period value, the tone output toggles:
228
+
229
+ ```
230
+ Tone frequency = PSG_CLOCK / (8 * period)
231
+ ```
232
+
233
+ Where `PSG_CLOCK` = 1,023,000 Hz. A period of 0 is treated as 1 (highest frequency).
234
+
235
+ ### Noise Generation
236
+
237
+ The noise generator uses a 17-bit linear feedback shift register (LFSR) following the MAME/hardware implementation:
238
+
239
+ ```
240
+ feedback = bit0 XOR bit3
241
+ noiseShiftReg = (noiseShiftReg >> 1) | (feedback << 16)
242
+ ```
243
+
244
+ The noise generator clocks at half the tone generator rate (master / 16 vs master / 8), so the period comparison is doubled internally. Noise output is taken directly from bit 0 of the shift register.
245
+
246
+ ### Envelope Generator
247
+
248
+ The envelope generator produces a 4-bit volume ramp (0-15) controlled by the shape register (R13):
249
+
250
+ | Bit | Name | Function |
251
+ |-----|------|----------|
252
+ | 3 | CONT | Continue after first cycle (0 = hold at 0) |
253
+ | 2 | ATT | Attack direction (1 = rising, 0 = falling) |
254
+ | 1 | ALT | Alternate direction each cycle |
255
+ | 0 | HOLD | Hold final value after first cycle |
256
+
257
+ Common envelope shapes:
258
+
259
+ | Shape | CONT-ATT-ALT-HOLD | Waveform |
260
+ |-------|-------------------|----------|
261
+ | `$00` | 0-0-0-0 | Decay then silence |
262
+ | `$04` | 0-1-0-0 | Attack then silence |
263
+ | `$08` | 1-0-0-0 | Repeating sawtooth (falling) |
264
+ | `$0A` | 1-0-1-0 | Repeating triangle |
265
+ | `$0C` | 1-1-0-0 | Repeating sawtooth (rising) |
266
+ | `$0E` | 1-1-1-0 | Repeating triangle (inverted) |
267
+
268
+ Writing to R13 resets the envelope counter and sets the initial volume based on the attack direction.
269
+
270
+ The envelope period ticks at the same rate as tone counters (master / 8). Each step of the 16-level ramp takes `period` ticks, giving a full envelope cycle frequency of:
271
+
272
+ ```
273
+ fEnvelope = PSG_CLOCK / (256 * period)
274
+ ```
275
+
276
+ ### Mixer and Channel Output
277
+
278
+ The mixer register (R7) controls which generators feed each channel. A bit value of 1 means **disabled** (bypassed/always high):
279
+
280
+ | Bit | Function |
281
+ |-----|----------|
282
+ | 0 | Tone A disable |
283
+ | 1 | Tone B disable |
284
+ | 2 | Tone C disable |
285
+ | 3 | Noise A disable |
286
+ | 4 | Noise B disable |
287
+ | 5 | Noise C disable |
288
+
289
+ Channel output follows MAME's mixer logic:
290
+
291
+ ```
292
+ output = (tone_output OR tone_disable) AND (noise_output OR noise_disable)
293
+ ```
294
+
295
+ The output is unipolar (0 or +level), matching real hardware. The level is determined by either the fixed volume (amplitude register bits 3-0) or the envelope volume (when amplitude bit 4 is set).
296
+
297
+ **Volume Table:**
298
+
299
+ The 4-bit volume uses a logarithmic scale based on AppleWin/MAME measurements:
300
+
301
+ | Level | Amplitude | Level | Amplitude |
302
+ |-------|-----------|-------|-----------|
303
+ | 0 | 0.0000 | 8 | 0.1691 |
304
+ | 1 | 0.0137 | 9 | 0.2647 |
305
+ | 2 | 0.0205 | 10 | 0.3527 |
306
+ | 3 | 0.0291 | 11 | 0.4499 |
307
+ | 4 | 0.0423 | 12 | 0.5704 |
308
+ | 5 | 0.0618 | 13 | 0.6873 |
309
+ | 6 | 0.0847 | 14 | 0.8482 |
310
+ | 7 | 0.1369 | 15 | 1.0000 |
311
+
312
+ The final PSG output is the sum of all three unmuted channels divided by 3 for normalization.
313
+
314
+ ### Phase Coherence
315
+
316
+ Many Mockingboard music players program both PSGs with identical register values to produce mono output on both channels. Because each PSG maintains independent tone counters, the two chips can drift out of phase, causing cancellation artifacts when the signals are summed.
317
+
318
+ The emulator detects when both PSGs have identical sound registers (R0-R13) and substitutes PSG1's output for both channels. This eliminates phase cancellation while preserving true stereo when the PSGs are programmed differently.
319
+
320
+ ### Incremental Audio Generation
321
+
322
+ Rather than generating all Mockingboard audio in bulk when the audio buffer is requested, the emulator generates samples incrementally during CPU execution. The `update()` method is called after each CPU instruction:
323
+
324
+ 1. Accumulate CPU cycles in a fractional counter (`cycleAccum_`)
325
+ 2. When enough cycles accumulate for one audio sample (~21.3 cycles), generate a single sample from each PSG
326
+ 3. Store interleaved stereo samples in `sampleAccum_`
327
+ 4. When the audio system requests samples, `consumeStereoSamples()` drains the accumulated buffer
328
+
329
+ This approach ensures that PSG register changes from VIA timer IRQ handlers are immediately reflected in the audio output at the correct time, rather than being batched.
330
+
331
+ ---
332
+
333
+ ## Audio Mixing Pipeline
334
+
335
+ The final stereo output combines the speaker and Mockingboard:
336
+
337
+ ```
338
+ Speaker (mono, centered) -----> [mix scale 0.5] --+
339
+ +--> Left channel --> clamp [-1, +1]
340
+ Mockingboard PSG1 (left) -----> [mix scale 0.5] --+
341
+
342
+ Speaker (mono, centered) -----> [mix scale 0.5] --+
343
+ +--> Right channel --> clamp [-1, +1]
344
+ Mockingboard PSG2 (right) ----> [mix scale 0.5] --+
345
+ ```
346
+
347
+ Both sources are scaled by 0.5 before mixing to prevent clipping when both are active simultaneously. The Mockingboard output includes per-channel DC offset removal (alpha = 0.9999, ~200 ms time constant at 48 kHz) to convert the unipolar PSG output to bipolar for audio playback.
348
+
349
+ When the system is muted, the speaker state tracking continues (toggle events are still recorded and processed) but the output buffer is zeroed. Mockingboard samples are consumed and discarded to keep the PSG state synchronized.
350
+
351
+ ---
352
+
353
+ ## JavaScript Audio Layer
354
+
355
+ ### AudioDriver
356
+
357
+ `AudioDriver` (`src/js/audio/audio-driver.js`) manages the Web Audio API context and serves as the bridge between the browser and the WASM audio generation.
358
+
359
+ Key constants:
360
+
361
+ | Constant | Value | Description |
362
+ |----------|-------|-------------|
363
+ | `SAMPLE_RATE` | 48,000 Hz | Audio output sample rate |
364
+ | `AUDIO_BUFFER_SIZE` | 128 | AudioWorklet quantum size |
365
+ | `CYCLES_PER_SECOND` | 1,023,000 | Apple IIe CPU clock |
366
+ | `DEFAULT_VOLUME` | 0.5 | Initial volume level |
367
+
368
+ The audio graph is:
369
+
370
+ ```
371
+ AudioWorkletNode ("apple-audio-processor")
372
+ --> GainNode (volume/mute control)
373
+ --> AudioContext.destination (speakers)
374
+ ```
375
+
376
+ If AudioWorklet is unavailable, the driver falls back to a `ScriptProcessorNode` with a 4096-sample buffer. The ScriptProcessor path deinterleaves stereo samples into separate left/right channel buffers.
377
+
378
+ ### AudioWorklet
379
+
380
+ `AppleAudioProcessor` (`src/js/audio/audio-worklet.js`) runs on the Web Audio rendering thread. It maintains a sample buffer and requests new samples from the main thread when the buffer drops below 1600 frames:
381
+
382
+ 1. Worklet posts `requestSamples` message with count = 1600
383
+ 2. Main thread runs WASM to generate 1600 stereo sample frames
384
+ 3. Main thread posts `samples` message with `Float32Array` (transferred, not copied)
385
+ 4. Worklet appends to its buffer and deinterleaves into left/right output channels
386
+
387
+ The 128-sample AudioWorklet quantum means the worklet's `process()` method is called approximately 375 times per second.
388
+
389
+ ### Fallback Timing
390
+
391
+ When the Web Audio API is unavailable or suspended (browser autoplay policy), a `setInterval`-based fallback runs at 60 Hz:
392
+
393
+ ```
394
+ cyclesPerTick = 1,023,000 / 60 = ~17,050 cycles
395
+ ```
396
+
397
+ The fallback runs `_runCycles()` and checks `_consumeFrameSamples()` for frame updates. Once the user interacts with the page (click or keypress), the AudioContext is resumed and proper audio timing takes over.
398
+
399
+ ---
400
+
401
+ ## Volume and Mute Control
402
+
403
+ All audio sources share a single unified volume control:
404
+
405
+ - **JavaScript GainNode** controls the final output volume (0.0 to 1.0)
406
+ - **C++ mute flag** zeroes the output buffer while maintaining internal state tracking
407
+ - Volume and mute state persist in `localStorage` (`a2e-volume`, `a2e-muted`)
408
+ - On startup, saved values are restored and synchronized to both the JS GainNode and the C++ Audio class
409
+
410
+ The volume setter clamps values to [0.0, 1.0] and updates both the GainNode and the WASM module's internal volume state.
411
+
412
+ ---
413
+
414
+ ## State Serialization
415
+
416
+ Both the Mockingboard and speaker state are included in save states.
417
+
418
+ **Mockingboard state size:** 161 bytes total
419
+
420
+ | Component | Size | Contents |
421
+ |-----------|------|----------|
422
+ | Enabled flag | 1 byte | Card enabled/disabled |
423
+ | VIA 1 | 32 bytes | Port registers, timers, control, PSG state machine |
424
+ | PSG 1 | 48 bytes | 16 registers, tone/noise/envelope counters and state |
425
+ | VIA 2 | 32 bytes | Same as VIA 1 |
426
+ | PSG 2 | 48 bytes | Same as PSG 1 |
427
+
428
+ PSG state includes the 17-bit noise shift register, envelope counter, and envelope flags to ensure seamless audio continuity across save/load. The DC filter states are reset on load for a clean audio restart.
429
+
430
+ ---
431
+
432
+ ## Source Files
433
+
434
+ | File | Description |
435
+ |------|-------------|
436
+ | `src/core/audio/audio.hpp` | Speaker audio class declaration |
437
+ | `src/core/audio/audio.cpp` | Speaker toggle recording and sample generation |
438
+ | `src/core/cards/mockingboard_card.hpp` | Mockingboard card interface |
439
+ | `src/core/cards/mockingboard_card.cpp` | Card I/O routing, mixing, incremental generation |
440
+ | `src/core/cards/mockingboard/ay8910.hpp` | AY-3-8910 PSG class declaration |
441
+ | `src/core/cards/mockingboard/ay8910.cpp` | Tone, noise, envelope generators |
442
+ | `src/core/cards/mockingboard/via6522.hpp` | VIA 6522 class declaration |
443
+ | `src/core/cards/mockingboard/via6522.cpp` | Timer, port, IRQ, PSG control protocol |
444
+ | `src/js/audio/audio-driver.js` | Web Audio API driver and timing |
445
+ | `src/js/audio/audio-worklet.js` | AudioWorklet sample buffer processor |
446
+
447
+ ---
448
+
449
+ See also: [[Architecture Overview]] | [[Expansion Slots]] | [[CPU Emulation]]