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,616 @@
1
+ /*
2
+ * ay8910.cpp - AY-3-8910 sound chip emulation implementation
3
+ *
4
+ * Written by
5
+ * Mike Daley <michael_daley@icloud.com>
6
+ */
7
+
8
+ #include "ay8910.hpp"
9
+ #include <cmath>
10
+ #ifdef __EMSCRIPTEN__
11
+ #include <emscripten.h>
12
+ #endif
13
+
14
+ namespace a2e {
15
+
16
+ // Debug logging flag - set via setDebugLogging()
17
+ static bool debugLogging_ = false;
18
+
19
+ void AY8910::setDebugLogging(bool enabled) {
20
+ debugLogging_ = enabled;
21
+ }
22
+
23
+ void AY8910::setPsgId(int id) {
24
+ psgId_ = id;
25
+ }
26
+
27
+ static const char* getRegisterName(int reg) {
28
+ static const char* names[] = {
29
+ "ToneA_Fine", "ToneA_Coarse", "ToneB_Fine", "ToneB_Coarse",
30
+ "ToneC_Fine", "ToneC_Coarse", "NoisePeriod", "Mixer",
31
+ "AmpA", "AmpB", "AmpC", "EnvFine", "EnvCoarse", "EnvShape",
32
+ "IOPortA", "IOPortB"
33
+ };
34
+ return (reg >= 0 && reg < 16) ? names[reg] : "Unknown";
35
+ }
36
+
37
+ // Volume table based on AppleWin/MAME measurements
38
+ // Values represent amplitude levels for 4-bit volume (0-15)
39
+ // Converted from 16-bit values: 0x0000, 0x0385, 0x053D, 0x0770, etc.
40
+ const float AY8910::volumeTable_[16] = {
41
+ 0.0000f, 0.0137f, 0.0205f, 0.0291f,
42
+ 0.0423f, 0.0618f, 0.0847f, 0.1369f,
43
+ 0.1691f, 0.2647f, 0.3527f, 0.4499f,
44
+ 0.5704f, 0.6873f, 0.8482f, 1.0000f
45
+ };
46
+
47
+ AY8910::AY8910() {
48
+ reset();
49
+ }
50
+
51
+ void AY8910::reset() {
52
+ registers_.fill(0);
53
+ currentRegister_ = 0;
54
+
55
+ // Set mixer to 0x3F - all tone and noise DISABLED for silence
56
+ // Bits 0-2: tone disable (1=disabled), Bits 3-5: noise disable (1=disabled)
57
+ registers_[REG_MIXER] = 0x3F;
58
+
59
+ toneCounters_.fill(0);
60
+ toneOutput_.fill(false);
61
+
62
+ noiseCounter_ = 0;
63
+ noiseShiftReg_ = 1; // Must be non-zero
64
+ noiseToggle_ = false;
65
+
66
+ envCounter_ = 0;
67
+ envVolume_ = 0;
68
+ envHolding_ = false;
69
+ envContinue_ = false;
70
+ envAttack_ = false;
71
+ envAlternate_ = false;
72
+ envHold_ = false;
73
+
74
+ phaseAccumulator_ = 0.0;
75
+
76
+ // Clear any pending register writes
77
+ pendingWrites_.clear();
78
+ }
79
+
80
+ void AY8910::setRegisterAddress(uint8_t address) {
81
+ currentRegister_ = address & 0x0F;
82
+ }
83
+
84
+ void AY8910::writeRegister(uint8_t value) {
85
+ // Track writes for debugging
86
+ writeCount_++;
87
+ lastWriteReg_ = currentRegister_;
88
+ lastWriteVal_ = value;
89
+
90
+ // Apply register write immediately
91
+ applyRegisterWrite(currentRegister_, value);
92
+
93
+ #ifdef __EMSCRIPTEN__
94
+ if (debugLogging_) {
95
+ const char* regName = getRegisterName(currentRegister_);
96
+ EM_ASM({
97
+ const reg = $0;
98
+ const val = $1;
99
+ const regName = UTF8ToString($2);
100
+ const psgId = $3;
101
+ console.log(`PSG${psgId}: R${reg} (${regName}) = $${val.toString(16).toUpperCase().padStart(2,'0')} (${val})`);
102
+ }, currentRegister_, value, regName, psgId_);
103
+ }
104
+ #endif
105
+ }
106
+
107
+ void AY8910::applyRegisterWrite(uint8_t reg, uint8_t value) {
108
+ // Apply masks based on register
109
+ switch (reg) {
110
+ case REG_TONE_A_COARSE:
111
+ case REG_TONE_B_COARSE:
112
+ case REG_TONE_C_COARSE:
113
+ value &= 0x0F; // 4-bit coarse tune
114
+ break;
115
+ case REG_NOISE_PERIOD:
116
+ value &= 0x1F; // 5-bit noise period
117
+ break;
118
+ case REG_AMP_A:
119
+ case REG_AMP_B:
120
+ case REG_AMP_C:
121
+ value &= 0x1F; // 5-bit (bit 4 = envelope mode)
122
+ break;
123
+ case REG_ENV_SHAPE:
124
+ value &= 0x0F; // 4-bit envelope shape
125
+ // Writing to envelope shape resets the envelope
126
+ envCounter_ = 0;
127
+ envHolding_ = false;
128
+ // Decode envelope shape bits: CONT ATT ALT HOLD (bits 3-0)
129
+ envContinue_ = (value & 0x08) != 0;
130
+ envAttack_ = (value & 0x04) != 0;
131
+ envAlternate_ = (value & 0x02) != 0;
132
+ envHold_ = (value & 0x01) != 0;
133
+ // Set initial volume based on direction
134
+ if (envAttack_) {
135
+ envVolume_ = 0; // Start at 0 for attack (rising)
136
+ } else {
137
+ envVolume_ = 15; // Start at 15 for decay (falling)
138
+ }
139
+ break;
140
+ }
141
+
142
+ registers_[reg] = value;
143
+ }
144
+
145
+ uint8_t AY8910::readRegister() const {
146
+ return registers_[currentRegister_];
147
+ }
148
+
149
+ uint16_t AY8910::getTonePeriod(int channel) const {
150
+ int fineReg = channel * 2;
151
+ int coarseReg = channel * 2 + 1;
152
+ return registers_[fineReg] | ((registers_[coarseReg] & 0x0F) << 8);
153
+ }
154
+
155
+ uint8_t AY8910::getNoisePeriod() const {
156
+ return registers_[REG_NOISE_PERIOD] & 0x1F;
157
+ }
158
+
159
+ uint16_t AY8910::getEnvPeriod() const {
160
+ return registers_[REG_ENV_FINE] | (registers_[REG_ENV_COARSE] << 8);
161
+ }
162
+
163
+ void AY8910::updateToneGenerator(int channel) {
164
+ uint16_t period = getTonePeriod(channel);
165
+ if (period == 0) period = 1; // Avoid division by zero
166
+
167
+ toneCounters_[channel]++;
168
+ if (toneCounters_[channel] >= period) {
169
+ toneCounters_[channel] = 0;
170
+ toneOutput_[channel] = !toneOutput_[channel];
171
+ }
172
+ }
173
+
174
+ void AY8910::updateNoiseGenerator() {
175
+ uint8_t period = getNoisePeriod();
176
+
177
+ // Period 0 acts as period 1 (highest frequency noise)
178
+ if (period == 0) period = 1;
179
+
180
+ noiseCounter_++;
181
+ // Noise runs at clock/16 while tones run at clock/8
182
+ // Since we step at clock/8 rate, double the period comparison
183
+ if (noiseCounter_ >= static_cast<uint32_t>(period) * 2) {
184
+ noiseCounter_ = 0;
185
+
186
+ // MAME-style 17-bit LFSR: feedback = bit0 XOR bit3, inject at bit16, shift right
187
+ // Noise output is directly bit 0 of the shift register (no toggle mechanism)
188
+ uint32_t feedback = (noiseShiftReg_ & 1) ^ ((noiseShiftReg_ >> 3) & 1);
189
+ noiseShiftReg_ = (noiseShiftReg_ >> 1) | (feedback << 16);
190
+ }
191
+ }
192
+
193
+ void AY8910::updateEnvelopeGenerator() {
194
+ if (envHolding_) return;
195
+
196
+ uint16_t period = getEnvPeriod();
197
+
198
+ envCounter_++;
199
+ // Envelope counter runs at the same rate as tone counters (master/8).
200
+ // FUSE/AppleWin compare directly against the period value — no multiplier.
201
+ // The datasheet's fE = fCLOCK/(256*EP) refers to a full 32-step triangle
202
+ // (16 up + 16 down), so each individual step = EP ticks at master/8.
203
+ // Period 0 should be treated same as period 1 (minimum period, highest frequency)
204
+ // per MAME/AppleWin implementations - avoids undefined behavior.
205
+ uint32_t effectivePeriod = (period == 0) ? 1 : period;
206
+ uint32_t threshold = static_cast<uint32_t>(effectivePeriod);
207
+ if (envCounter_ >= threshold) {
208
+ envCounter_ = 0;
209
+
210
+ // Update envelope volume based on current direction
211
+ if (envAttack_) {
212
+ // Attack (rising)
213
+ if (envVolume_ < 15) {
214
+ envVolume_++;
215
+ } else {
216
+ // Reached max (15) - handle end of cycle
217
+ handleEnvelopeCycleEnd();
218
+ }
219
+ } else {
220
+ // Decay (falling)
221
+ if (envVolume_ > 0) {
222
+ envVolume_--;
223
+ } else {
224
+ // Reached min (0) - handle end of cycle
225
+ handleEnvelopeCycleEnd();
226
+ }
227
+ }
228
+ }
229
+ }
230
+
231
+ void AY8910::handleEnvelopeCycleEnd() {
232
+ // Called when envelope reaches its limit (0 or 15)
233
+ // Envelope shape bits: CONT(3) ATT(2) ALT(1) HOLD(0)
234
+
235
+ if (!envContinue_) {
236
+ // CONT=0: After first cycle, always hold at 0
237
+ envVolume_ = 0;
238
+ envHolding_ = true;
239
+ return;
240
+ }
241
+
242
+ // CONT=1: Continue behavior depends on ALT and HOLD
243
+ if (envHold_) {
244
+ // HOLD=1: Stop after this cycle
245
+ if (envAlternate_) {
246
+ // ALT=1, HOLD=1: Hold at opposite extreme
247
+ // If we were attacking (going up), hold at 0
248
+ // If we were decaying (going down), hold at 15
249
+ envVolume_ = envAttack_ ? 0 : 15;
250
+ }
251
+ // else ALT=0, HOLD=1: Hold at current extreme (already there)
252
+ envHolding_ = true;
253
+ } else {
254
+ // HOLD=0: Continue cycling
255
+ if (envAlternate_) {
256
+ // ALT=1: Reverse direction (triangle wave)
257
+ envAttack_ = !envAttack_;
258
+ } else {
259
+ // ALT=0: Reset to start (sawtooth wave)
260
+ envVolume_ = envAttack_ ? 0 : 15;
261
+ }
262
+ }
263
+ }
264
+
265
+ float AY8910::getChannelOutput(int channel) const {
266
+ uint8_t mixer = registers_[REG_MIXER];
267
+ uint8_t ampReg = registers_[REG_AMP_A + channel];
268
+
269
+ uint8_t volume;
270
+ if (ampReg & 0x10) {
271
+ volume = envVolume_;
272
+ } else {
273
+ volume = ampReg & 0x0F;
274
+ }
275
+
276
+ if (volume == 0) return 0.0f;
277
+
278
+ float level = volumeTable_[volume];
279
+
280
+ // MAME mixer: output = (tone_out | tone_disable) & (noise_out | noise_disable)
281
+ // Register bit = 1 means disabled (bypassed/always high)
282
+ // Unipolar output matching real hardware: 0 or +level (never negative)
283
+ bool toneDisable = (mixer & (1 << channel)) != 0;
284
+ bool noiseDisable = (mixer & (1 << (channel + 3))) != 0;
285
+
286
+ bool toneOut = toneOutput_[channel] || toneDisable;
287
+ bool noiseOut = ((noiseShiftReg_ & 1) != 0) || noiseDisable;
288
+
289
+ return (toneOut && noiseOut) ? level : 0.0f;
290
+ }
291
+
292
+ void AY8910::setChannelMute(int channel, bool muted) {
293
+ if (channel >= 0 && channel < NUM_CHANNELS) {
294
+ channelMuted_[channel] = muted;
295
+ }
296
+ }
297
+
298
+ bool AY8910::isChannelMuted(int channel) const {
299
+ if (channel >= 0 && channel < NUM_CHANNELS) {
300
+ return channelMuted_[channel];
301
+ }
302
+ return false;
303
+ }
304
+
305
+ float AY8910::computeMixerOutput() const {
306
+ uint8_t mixer = registers_[REG_MIXER];
307
+ float sample = 0.0f;
308
+ for (int ch = 0; ch < NUM_CHANNELS; ch++) {
309
+ if (channelMuted_[ch]) continue;
310
+
311
+ uint8_t ampReg = registers_[REG_AMP_A + ch];
312
+ uint8_t volume = (ampReg & 0x10) ? envVolume_ : (ampReg & 0x0F);
313
+ if (volume == 0) continue;
314
+
315
+ float level = volumeTable_[volume];
316
+
317
+ bool toneDisable = (mixer & (1 << ch)) != 0;
318
+ bool noiseDisable = (mixer & (1 << (ch + 3))) != 0;
319
+ bool toneOut = toneOutput_[ch] || toneDisable;
320
+ bool noiseOut = ((noiseShiftReg_ & 1) != 0) || noiseDisable;
321
+
322
+ sample += (toneOut && noiseOut) ? level : 0.0f;
323
+ }
324
+ return sample / 3.0f;
325
+ }
326
+
327
+ void AY8910::generateSamples(float* buffer, int count, int sampleRate) {
328
+ // Legacy version - apply any pending writes immediately and generate
329
+ for (const auto& write : pendingWrites_) {
330
+ applyRegisterWrite(write.reg, write.value);
331
+ }
332
+ pendingWrites_.clear();
333
+
334
+ // PSG clock cycles per audio sample
335
+ double cyclesPerSample = static_cast<double>(PSG_CLOCK) / sampleRate;
336
+ double toneStepsPerSample = cyclesPerSample / 8.0;
337
+
338
+ for (int i = 0; i < count; i++) {
339
+ phaseAccumulator_ += toneStepsPerSample;
340
+
341
+ // Advance PSG state and average output across all ticks
342
+ // This properly captures noise-tone gate interactions at clock resolution
343
+ float sampleAccum = 0.0f;
344
+ int ticks = 0;
345
+ while (phaseAccumulator_ >= 1.0) {
346
+ phaseAccumulator_ -= 1.0;
347
+ for (int ch = 0; ch < NUM_CHANNELS; ch++) {
348
+ updateToneGenerator(ch);
349
+ }
350
+ updateNoiseGenerator();
351
+ updateEnvelopeGenerator();
352
+ sampleAccum += computeMixerOutput();
353
+ ticks++;
354
+ }
355
+
356
+ float sample = (ticks > 0) ? sampleAccum / static_cast<float>(ticks) : computeMixerOutput();
357
+
358
+ buffer[i] = sample;
359
+ }
360
+ }
361
+
362
+ void AY8910::generateSamples(float* buffer, int count, int sampleRate, uint64_t startCycle, uint64_t endCycle) {
363
+ // PSG clock cycles per audio sample
364
+ double cyclesPerSample = static_cast<double>(PSG_CLOCK) / sampleRate;
365
+ double toneStepsPerSample = cyclesPerSample / 8.0;
366
+
367
+ // Calculate CPU cycles per sample for timing
368
+ double cpuCyclesTotal = static_cast<double>(endCycle - startCycle);
369
+ double cpuCyclesPerSample = (count > 0) ? cpuCyclesTotal / count : 0;
370
+
371
+ // Index into pending writes
372
+ size_t writeIdx = 0;
373
+
374
+ for (int i = 0; i < count; i++) {
375
+ // Calculate the CPU cycle for this sample
376
+ uint64_t sampleCycle = startCycle + static_cast<uint64_t>(i * cpuCyclesPerSample);
377
+
378
+ // Apply any pending writes that should happen before this sample
379
+ while (writeIdx < pendingWrites_.size() && pendingWrites_[writeIdx].cycle <= sampleCycle) {
380
+ applyRegisterWrite(pendingWrites_[writeIdx].reg, pendingWrites_[writeIdx].value);
381
+ writeIdx++;
382
+ }
383
+
384
+ // Advance PSG state and average output across all ticks
385
+ phaseAccumulator_ += toneStepsPerSample;
386
+
387
+ float sampleAccum = 0.0f;
388
+ int ticks = 0;
389
+ while (phaseAccumulator_ >= 1.0) {
390
+ phaseAccumulator_ -= 1.0;
391
+ for (int ch = 0; ch < NUM_CHANNELS; ch++) {
392
+ updateToneGenerator(ch);
393
+ }
394
+ updateNoiseGenerator();
395
+ updateEnvelopeGenerator();
396
+ sampleAccum += computeMixerOutput();
397
+ ticks++;
398
+ }
399
+
400
+ float sample = (ticks > 0) ? sampleAccum / static_cast<float>(ticks) : computeMixerOutput();
401
+
402
+ buffer[i] = sample;
403
+ }
404
+
405
+ // Apply any remaining writes (for the end of the buffer)
406
+ while (writeIdx < pendingWrites_.size()) {
407
+ applyRegisterWrite(pendingWrites_[writeIdx].reg, pendingWrites_[writeIdx].value);
408
+ writeIdx++;
409
+ }
410
+
411
+ // Clear processed writes
412
+ pendingWrites_.clear();
413
+ }
414
+
415
+ float AY8910::generateSingleSample() {
416
+ // Precomputed constants for 48kHz sample rate
417
+ // toneStepsPerSample = PSG_CLOCK / (48000 * 8) = 1023000 / 384000 ≈ 2.6640625
418
+ static constexpr double TONE_STEPS = 1023000.0 / (48000.0 * 8.0);
419
+
420
+ // Advance PSG state and average output across all ticks
421
+ // Evaluating the mixer at each tick captures noise-tone gate interactions
422
+ // at the PSG clock resolution, preventing aliased modulation artifacts
423
+ phaseAccumulator_ += TONE_STEPS;
424
+
425
+ float sampleAccum = 0.0f;
426
+ int ticks = 0;
427
+ while (phaseAccumulator_ >= 1.0) {
428
+ phaseAccumulator_ -= 1.0;
429
+ for (int ch = 0; ch < NUM_CHANNELS; ch++) {
430
+ updateToneGenerator(ch);
431
+ }
432
+ updateNoiseGenerator();
433
+ updateEnvelopeGenerator();
434
+ sampleAccum += computeMixerOutput();
435
+ ticks++;
436
+ }
437
+
438
+ return (ticks > 0) ? sampleAccum / static_cast<float>(ticks) : computeMixerOutput();
439
+ }
440
+
441
+ void AY8910::generateChannelSamples(float* buffer, int count, int sampleRate, int channel) {
442
+ if (channel < 0 || channel >= NUM_CHANNELS) {
443
+ for (int i = 0; i < count; i++) {
444
+ buffer[i] = 0.0f;
445
+ }
446
+ return;
447
+ }
448
+
449
+ // PSG clock cycles per audio sample
450
+ double cyclesPerSample = static_cast<double>(PSG_CLOCK) / sampleRate;
451
+ double toneStepsPerSample = cyclesPerSample / 8.0;
452
+
453
+ for (int i = 0; i < count; i++) {
454
+ phaseAccumulator_ += toneStepsPerSample;
455
+
456
+ // Advance all generators (needed for accurate state)
457
+ while (phaseAccumulator_ >= 1.0) {
458
+ phaseAccumulator_ -= 1.0;
459
+ for (int ch = 0; ch < NUM_CHANNELS; ch++) {
460
+ updateToneGenerator(ch);
461
+ }
462
+ updateNoiseGenerator();
463
+ updateEnvelopeGenerator();
464
+ }
465
+
466
+ // Unipolar output for visualization (no DC removal - shows raw waveform)
467
+ uint8_t mixer = registers_[REG_MIXER];
468
+ uint8_t ampReg = registers_[REG_AMP_A + channel];
469
+ uint8_t volume = (ampReg & 0x10) ? envVolume_ : (ampReg & 0x0F);
470
+ if (volume == 0) {
471
+ buffer[i] = 0.0f;
472
+ continue;
473
+ }
474
+
475
+ float level = volumeTable_[volume];
476
+
477
+ bool toneDisable = (mixer & (1 << channel)) != 0;
478
+ bool noiseDisable = (mixer & (1 << (channel + 3))) != 0;
479
+ bool toneOut = toneOutput_[channel] || toneDisable;
480
+ bool noiseOut = ((noiseShiftReg_ & 1) != 0) || noiseDisable;
481
+
482
+ buffer[i] = (toneOut && noiseOut) ? level : 0.0f;
483
+ }
484
+ }
485
+
486
+ size_t AY8910::exportState(uint8_t* buffer) const {
487
+ size_t offset = 0;
488
+
489
+ // 16 registers
490
+ for (int i = 0; i < 16; i++) {
491
+ buffer[offset++] = registers_[i];
492
+ }
493
+
494
+ // Current register address
495
+ buffer[offset++] = currentRegister_;
496
+
497
+ // Tone counters (3 x 4 bytes = 12 bytes)
498
+ for (int i = 0; i < 3; i++) {
499
+ buffer[offset++] = (toneCounters_[i] >> 0) & 0xFF;
500
+ buffer[offset++] = (toneCounters_[i] >> 8) & 0xFF;
501
+ buffer[offset++] = (toneCounters_[i] >> 16) & 0xFF;
502
+ buffer[offset++] = (toneCounters_[i] >> 24) & 0xFF;
503
+ }
504
+
505
+ // Tone outputs (1 byte packed)
506
+ buffer[offset++] = (toneOutput_[0] ? 1 : 0) |
507
+ (toneOutput_[1] ? 2 : 0) |
508
+ (toneOutput_[2] ? 4 : 0);
509
+
510
+ // Noise state
511
+ buffer[offset++] = noiseToggle_ ? 1 : 0;
512
+
513
+ // Envelope state
514
+ buffer[offset++] = envVolume_;
515
+
516
+ // Additional state for proper audio continuity (added in state version 5)
517
+ // Noise counter (4 bytes)
518
+ buffer[offset++] = (noiseCounter_ >> 0) & 0xFF;
519
+ buffer[offset++] = (noiseCounter_ >> 8) & 0xFF;
520
+ buffer[offset++] = (noiseCounter_ >> 16) & 0xFF;
521
+ buffer[offset++] = (noiseCounter_ >> 24) & 0xFF;
522
+
523
+ // Noise shift register (4 bytes) - critical for noise pattern continuity
524
+ buffer[offset++] = (noiseShiftReg_ >> 0) & 0xFF;
525
+ buffer[offset++] = (noiseShiftReg_ >> 8) & 0xFF;
526
+ buffer[offset++] = (noiseShiftReg_ >> 16) & 0xFF;
527
+ buffer[offset++] = (noiseShiftReg_ >> 24) & 0xFF;
528
+
529
+ // Envelope counter (4 bytes)
530
+ buffer[offset++] = (envCounter_ >> 0) & 0xFF;
531
+ buffer[offset++] = (envCounter_ >> 8) & 0xFF;
532
+ buffer[offset++] = (envCounter_ >> 16) & 0xFF;
533
+ buffer[offset++] = (envCounter_ >> 24) & 0xFF;
534
+
535
+ // Envelope flags (1 byte packed)
536
+ buffer[offset++] = (envHolding_ ? 0x01 : 0) |
537
+ (envAttack_ ? 0x02 : 0);
538
+
539
+ // Pad to STATE_SIZE for consistent serialization
540
+ while (offset < STATE_SIZE) {
541
+ buffer[offset++] = 0;
542
+ }
543
+
544
+ return offset; // Exactly STATE_SIZE bytes
545
+ }
546
+
547
+ void AY8910::importState(const uint8_t* buffer) {
548
+ size_t offset = 0;
549
+
550
+ // Clear any pending register writes from before state import
551
+ pendingWrites_.clear();
552
+
553
+ // 16 registers
554
+ for (int i = 0; i < 16; i++) {
555
+ registers_[i] = buffer[offset++];
556
+ }
557
+
558
+ // Current register address
559
+ currentRegister_ = buffer[offset++];
560
+
561
+ // Tone counters
562
+ for (int i = 0; i < 3; i++) {
563
+ toneCounters_[i] = buffer[offset] |
564
+ (buffer[offset + 1] << 8) |
565
+ (buffer[offset + 2] << 16) |
566
+ (buffer[offset + 3] << 24);
567
+ offset += 4;
568
+ }
569
+
570
+ // Tone outputs
571
+ uint8_t outputs = buffer[offset++];
572
+ toneOutput_[0] = (outputs & 1) != 0;
573
+ toneOutput_[1] = (outputs & 2) != 0;
574
+ toneOutput_[2] = (outputs & 4) != 0;
575
+
576
+ // Noise state
577
+ noiseToggle_ = buffer[offset++] != 0;
578
+
579
+ // Envelope state
580
+ envVolume_ = buffer[offset++];
581
+
582
+ // Additional state for proper audio continuity (added in state version 5)
583
+ // Noise counter (4 bytes)
584
+ noiseCounter_ = buffer[offset] |
585
+ (buffer[offset + 1] << 8) |
586
+ (buffer[offset + 2] << 16) |
587
+ (buffer[offset + 3] << 24);
588
+ offset += 4;
589
+
590
+ // Noise shift register (4 bytes)
591
+ noiseShiftReg_ = buffer[offset] |
592
+ (buffer[offset + 1] << 8) |
593
+ (buffer[offset + 2] << 16) |
594
+ (buffer[offset + 3] << 24);
595
+ offset += 4;
596
+
597
+ // Envelope counter (4 bytes)
598
+ envCounter_ = buffer[offset] |
599
+ (buffer[offset + 1] << 8) |
600
+ (buffer[offset + 2] << 16) |
601
+ (buffer[offset + 3] << 24);
602
+ offset += 4;
603
+
604
+ // Envelope flags (1 byte packed)
605
+ uint8_t envFlags = buffer[offset++];
606
+ envHolding_ = (envFlags & 0x01) != 0;
607
+ envAttack_ = (envFlags & 0x02) != 0;
608
+
609
+ // Restore envelope shape flags from register 13 (these are constant per shape)
610
+ uint8_t envShape = registers_[REG_ENV_SHAPE] & 0x0F;
611
+ envContinue_ = (envShape & 0x08) != 0;
612
+ envAlternate_ = (envShape & 0x02) != 0;
613
+ envHold_ = (envShape & 0x01) != 0;
614
+ }
615
+
616
+ } // namespace a2e