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.
- package/.clangd +5 -0
- package/.mcp.json +12 -0
- package/CLAUDE.md +362 -0
- package/CMakeLists.txt +774 -0
- package/LICENSE +21 -0
- package/README.md +392 -0
- package/build-wasm/generated/roms.cpp +2447 -0
- package/docker-compose.staging.yml +9 -0
- package/docs/basic-rom-disassembly.md +6663 -0
- package/docs/softswitch-comparison.md +273 -0
- package/docs/thunderclock-debug.md +89 -0
- package/examples/cube.bas +72 -0
- package/examples/hello.s +55 -0
- package/examples/scroll.s +140 -0
- package/package.json +18 -0
- package/public/assets/apple-logo-old.png +0 -0
- package/public/assets/apple-logo.png +0 -0
- package/public/assets/drive-closed-light-on.png +0 -0
- package/public/assets/drive-closed.png +0 -0
- package/public/assets/drive-open-light-on.png +0 -0
- package/public/assets/drive-open.png +0 -0
- package/public/audio-worklet.js +82 -0
- package/public/disks/Apple DOS 3.3 January 1983.dsk +0 -0
- package/public/disks/ProDOS 2.4.3.po +0 -0
- package/public/disks/h32mb.2mg +0 -0
- package/public/disks/library.json +26 -0
- package/public/docs/llms/llm-assembler.txt +90 -0
- package/public/docs/llms/llm-basic-program.txt +256 -0
- package/public/docs/llms/llm-disk-drives.txt +72 -0
- package/public/docs/llms/llm-file-explorer.txt +50 -0
- package/public/docs/llms/llm-hard-drives.txt +80 -0
- package/public/docs/llms/llm-main.txt +51 -0
- package/public/docs/llms/llm-slot-configuration.txt +66 -0
- package/public/icons/icon-192.svg +4 -0
- package/public/icons/icon-512.svg +4 -0
- package/public/index.html +661 -0
- package/public/llms.txt +49 -0
- package/public/manifest.json +29 -0
- package/public/shaders/burnin.glsl +22 -0
- package/public/shaders/crt.glsl +706 -0
- package/public/shaders/edge.glsl +109 -0
- package/public/shaders/vertex.glsl +8 -0
- package/public/sw.js +186 -0
- package/roms/341-0027.bin +0 -0
- package/roms/341-0160-A-US-UK.bin +0 -0
- package/roms/341-0160-A.bin +0 -0
- package/roms/342-0273-A-US-UK.bin +0 -0
- package/roms/342-0349-B-C0-FF.bin +0 -0
- package/roms/Apple Mouse Interface Card ROM - 342-0270-C.bin +0 -0
- package/roms/Thunderclock Plus ROM.bin +0 -0
- package/scripts/generate_roms.sh +69 -0
- package/src/bindings/wasm_interface.cpp +1940 -0
- package/src/core/assembler/assembler.cpp +1239 -0
- package/src/core/assembler/assembler.hpp +115 -0
- package/src/core/audio/audio.cpp +160 -0
- package/src/core/audio/audio.hpp +81 -0
- package/src/core/basic/basic_detokenizer.cpp +436 -0
- package/src/core/basic/basic_detokenizer.hpp +41 -0
- package/src/core/basic/basic_tokenizer.cpp +286 -0
- package/src/core/basic/basic_tokenizer.hpp +26 -0
- package/src/core/basic/basic_tokens.hpp +295 -0
- package/src/core/cards/disk2_card.cpp +568 -0
- package/src/core/cards/disk2_card.hpp +316 -0
- package/src/core/cards/expansion_card.hpp +185 -0
- package/src/core/cards/mockingboard/ay8910.cpp +616 -0
- package/src/core/cards/mockingboard/ay8910.hpp +159 -0
- package/src/core/cards/mockingboard/via6522.cpp +530 -0
- package/src/core/cards/mockingboard/via6522.hpp +163 -0
- package/src/core/cards/mockingboard_card.cpp +312 -0
- package/src/core/cards/mockingboard_card.hpp +159 -0
- package/src/core/cards/mouse_card.cpp +654 -0
- package/src/core/cards/mouse_card.hpp +190 -0
- package/src/core/cards/smartport/block_device.cpp +202 -0
- package/src/core/cards/smartport/block_device.hpp +60 -0
- package/src/core/cards/smartport/smartport_card.cpp +603 -0
- package/src/core/cards/smartport/smartport_card.hpp +120 -0
- package/src/core/cards/thunderclock_card.cpp +237 -0
- package/src/core/cards/thunderclock_card.hpp +122 -0
- package/src/core/cpu/cpu6502.cpp +1609 -0
- package/src/core/cpu/cpu6502.hpp +203 -0
- package/src/core/debug/condition_evaluator.cpp +470 -0
- package/src/core/debug/condition_evaluator.hpp +87 -0
- package/src/core/disassembler/disassembler.cpp +552 -0
- package/src/core/disassembler/disassembler.hpp +171 -0
- package/src/core/disk-image/disk_image.hpp +267 -0
- package/src/core/disk-image/dsk_disk_image.cpp +827 -0
- package/src/core/disk-image/dsk_disk_image.hpp +204 -0
- package/src/core/disk-image/gcr_encoding.cpp +147 -0
- package/src/core/disk-image/gcr_encoding.hpp +78 -0
- package/src/core/disk-image/woz_disk_image.cpp +1049 -0
- package/src/core/disk-image/woz_disk_image.hpp +343 -0
- package/src/core/emulator.cpp +2126 -0
- package/src/core/emulator.hpp +434 -0
- package/src/core/filesystem/dos33.cpp +178 -0
- package/src/core/filesystem/dos33.hpp +66 -0
- package/src/core/filesystem/pascal.cpp +262 -0
- package/src/core/filesystem/pascal.hpp +87 -0
- package/src/core/filesystem/prodos.cpp +369 -0
- package/src/core/filesystem/prodos.hpp +119 -0
- package/src/core/input/keyboard.cpp +227 -0
- package/src/core/input/keyboard.hpp +111 -0
- package/src/core/mmu/mmu.cpp +1387 -0
- package/src/core/mmu/mmu.hpp +236 -0
- package/src/core/types.hpp +196 -0
- package/src/core/video/video.cpp +680 -0
- package/src/core/video/video.hpp +156 -0
- package/src/css/assembler-editor.css +1617 -0
- package/src/css/base.css +470 -0
- package/src/css/basic-debugger.css +791 -0
- package/src/css/basic-editor.css +792 -0
- package/src/css/controls.css +783 -0
- package/src/css/cpu-debugger.css +1413 -0
- package/src/css/debug-base.css +160 -0
- package/src/css/debug-windows.css +6455 -0
- package/src/css/disk-drives.css +406 -0
- package/src/css/documentation.css +392 -0
- package/src/css/file-explorer.css +867 -0
- package/src/css/hard-drive.css +180 -0
- package/src/css/layout.css +217 -0
- package/src/css/memory-windows.css +798 -0
- package/src/css/modals.css +510 -0
- package/src/css/monitor.css +425 -0
- package/src/css/release-notes.css +101 -0
- package/src/css/responsive.css +400 -0
- package/src/css/rule-builder.css +340 -0
- package/src/css/save-states.css +201 -0
- package/src/css/settings-windows.css +1231 -0
- package/src/css/window-switcher.css +150 -0
- package/src/js/agent/agent-manager.js +643 -0
- package/src/js/agent/agent-tools.js +293 -0
- package/src/js/agent/agent-version-tools.js +131 -0
- package/src/js/agent/assembler-tools.js +357 -0
- package/src/js/agent/basic-program-tools.js +894 -0
- package/src/js/agent/disk-tools.js +417 -0
- package/src/js/agent/file-explorer-tools.js +269 -0
- package/src/js/agent/index.js +13 -0
- package/src/js/agent/main-tools.js +222 -0
- package/src/js/agent/slot-tools.js +303 -0
- package/src/js/agent/smartport-tools.js +257 -0
- package/src/js/agent/window-tools.js +80 -0
- package/src/js/audio/audio-driver.js +417 -0
- package/src/js/audio/audio-worklet.js +85 -0
- package/src/js/audio/index.js +8 -0
- package/src/js/config/default-layout.js +34 -0
- package/src/js/config/version.js +8 -0
- package/src/js/data/apple2-rom-routines.js +577 -0
- package/src/js/debug/assembler-editor-window.js +2993 -0
- package/src/js/debug/basic-breakpoint-manager.js +529 -0
- package/src/js/debug/basic-program-parser.js +436 -0
- package/src/js/debug/basic-program-window.js +2594 -0
- package/src/js/debug/basic-variable-inspector.js +447 -0
- package/src/js/debug/breakpoint-manager.js +472 -0
- package/src/js/debug/cpu-debugger-window.js +2396 -0
- package/src/js/debug/index.js +22 -0
- package/src/js/debug/label-manager.js +238 -0
- package/src/js/debug/memory-browser-window.js +416 -0
- package/src/js/debug/memory-heat-map-window.js +481 -0
- package/src/js/debug/memory-map-window.js +206 -0
- package/src/js/debug/mockingboard-window.js +882 -0
- package/src/js/debug/mouse-card-window.js +355 -0
- package/src/js/debug/rule-builder-window.js +648 -0
- package/src/js/debug/soft-switch-window.js +458 -0
- package/src/js/debug/stack-viewer-window.js +221 -0
- package/src/js/debug/symbols.js +416 -0
- package/src/js/debug/trace-panel.js +291 -0
- package/src/js/debug/zero-page-watch-window.js +297 -0
- package/src/js/disk-manager/disk-drives-window.js +212 -0
- package/src/js/disk-manager/disk-operations.js +284 -0
- package/src/js/disk-manager/disk-persistence.js +301 -0
- package/src/js/disk-manager/disk-surface-renderer.js +388 -0
- package/src/js/disk-manager/drive-sounds.js +139 -0
- package/src/js/disk-manager/hard-drive-manager.js +481 -0
- package/src/js/disk-manager/hard-drive-persistence.js +187 -0
- package/src/js/disk-manager/hard-drive-window.js +57 -0
- package/src/js/disk-manager/index.js +890 -0
- package/src/js/display/display-settings-window.js +383 -0
- package/src/js/display/index.js +10 -0
- package/src/js/display/screen-window.js +342 -0
- package/src/js/display/webgl-renderer.js +705 -0
- package/src/js/file-explorer/disassembler.js +574 -0
- package/src/js/file-explorer/dos33.js +266 -0
- package/src/js/file-explorer/file-viewer.js +359 -0
- package/src/js/file-explorer/index.js +1261 -0
- package/src/js/file-explorer/prodos.js +549 -0
- package/src/js/file-explorer/utils.js +67 -0
- package/src/js/help/documentation-window.js +1096 -0
- package/src/js/help/index.js +10 -0
- package/src/js/help/release-notes-window.js +85 -0
- package/src/js/help/release-notes.js +612 -0
- package/src/js/input/gamepad-handler.js +176 -0
- package/src/js/input/index.js +12 -0
- package/src/js/input/input-handler.js +396 -0
- package/src/js/input/joystick-window.js +404 -0
- package/src/js/input/mouse-handler.js +99 -0
- package/src/js/input/text-selection.js +462 -0
- package/src/js/main.js +653 -0
- package/src/js/state/index.js +15 -0
- package/src/js/state/save-states-window.js +393 -0
- package/src/js/state/state-manager.js +409 -0
- package/src/js/state/state-persistence.js +218 -0
- package/src/js/ui/confirm.js +43 -0
- package/src/js/ui/disk-drive-positioner.js +347 -0
- package/src/js/ui/reminder-controller.js +129 -0
- package/src/js/ui/slot-configuration-window.js +560 -0
- package/src/js/ui/theme-manager.js +61 -0
- package/src/js/ui/toast.js +44 -0
- package/src/js/ui/ui-controller.js +897 -0
- package/src/js/ui/window-switcher.js +275 -0
- package/src/js/utils/basic-autocomplete.js +832 -0
- package/src/js/utils/basic-highlighting.js +473 -0
- package/src/js/utils/basic-tokenizer.js +153 -0
- package/src/js/utils/basic-tokens.js +117 -0
- package/src/js/utils/constants.js +28 -0
- package/src/js/utils/indexeddb-helper.js +225 -0
- package/src/js/utils/merlin-editor-support.js +905 -0
- package/src/js/utils/merlin-highlighting.js +551 -0
- package/src/js/utils/storage.js +125 -0
- package/src/js/utils/string-utils.js +19 -0
- package/src/js/utils/wasm-memory.js +54 -0
- package/src/js/windows/base-window.js +690 -0
- package/src/js/windows/index.js +9 -0
- package/src/js/windows/window-manager.js +375 -0
- package/tests/catch2/catch.hpp +17976 -0
- package/tests/common/basic_program_builder.cpp +119 -0
- package/tests/common/basic_program_builder.hpp +209 -0
- package/tests/common/disk_image_builder.cpp +444 -0
- package/tests/common/disk_image_builder.hpp +141 -0
- package/tests/common/test_helpers.hpp +118 -0
- package/tests/gcr/gcr-test.cpp +142 -0
- package/tests/integration/check-rom.js +70 -0
- package/tests/integration/compare-boot.js +239 -0
- package/tests/integration/crash-trace.js +102 -0
- package/tests/integration/disk-boot-test.js +264 -0
- package/tests/integration/memory-crash.js +108 -0
- package/tests/integration/nibble-read-test.js +249 -0
- package/tests/integration/phase-test.js +159 -0
- package/tests/integration/test_emulator.cpp +291 -0
- package/tests/integration/test_emulator_basic.cpp +91 -0
- package/tests/integration/test_emulator_debug.cpp +344 -0
- package/tests/integration/test_emulator_disk.cpp +153 -0
- package/tests/integration/test_emulator_state.cpp +163 -0
- package/tests/klaus/6502_functional_test.bin +0 -0
- package/tests/klaus/65C02_extended_opcodes_test.bin +0 -0
- package/tests/klaus/klaus_6502_test.cpp +184 -0
- package/tests/klaus/klaus_65c02_test.cpp +197 -0
- package/tests/thunderclock/thunderclock_mmu_test.cpp +304 -0
- package/tests/thunderclock/thunderclock_test.cpp +550 -0
- package/tests/unit/test_assembler.cpp +521 -0
- package/tests/unit/test_audio.cpp +196 -0
- package/tests/unit/test_ay8910.cpp +311 -0
- package/tests/unit/test_basic_detokenizer.cpp +265 -0
- package/tests/unit/test_basic_tokenizer.cpp +382 -0
- package/tests/unit/test_block_device.cpp +259 -0
- package/tests/unit/test_condition_evaluator.cpp +219 -0
- package/tests/unit/test_cpu6502.cpp +1301 -0
- package/tests/unit/test_cpu_addressing.cpp +361 -0
- package/tests/unit/test_cpu_cycle_counts.cpp +409 -0
- package/tests/unit/test_cpu_decimal.cpp +166 -0
- package/tests/unit/test_cpu_interrupts.cpp +285 -0
- package/tests/unit/test_disassembler.cpp +323 -0
- package/tests/unit/test_disk2_card.cpp +330 -0
- package/tests/unit/test_dos33.cpp +273 -0
- package/tests/unit/test_dsk_disk_image.cpp +315 -0
- package/tests/unit/test_expansion_card.cpp +178 -0
- package/tests/unit/test_gcr_encoding.cpp +232 -0
- package/tests/unit/test_keyboard.cpp +262 -0
- package/tests/unit/test_mmu.cpp +555 -0
- package/tests/unit/test_mmu_slots.cpp +323 -0
- package/tests/unit/test_mockingboard.cpp +352 -0
- package/tests/unit/test_mouse_card.cpp +386 -0
- package/tests/unit/test_pascal.cpp +248 -0
- package/tests/unit/test_prodos.cpp +259 -0
- package/tests/unit/test_smartport_card.cpp +321 -0
- package/tests/unit/test_thunderclock.cpp +354 -0
- package/tests/unit/test_via6522.cpp +323 -0
- package/tests/unit/test_video.cpp +319 -0
- package/tests/unit/test_woz_disk_image.cpp +257 -0
- package/vite.config.js +96 -0
- package/wiki/AI-Agent.md +372 -0
- package/wiki/Architecture-Overview.md +303 -0
- package/wiki/Audio-System.md +449 -0
- package/wiki/CPU-Emulation.md +477 -0
- package/wiki/Debugger.md +516 -0
- package/wiki/Disk-Drives.md +161 -0
- package/wiki/Disk-System-Internals.md +547 -0
- package/wiki/Display-Settings.md +88 -0
- package/wiki/Expansion-Slots.md +187 -0
- package/wiki/File-Explorer.md +259 -0
- package/wiki/Getting-Started.md +156 -0
- package/wiki/Home.md +69 -0
- package/wiki/Input-Devices.md +183 -0
- package/wiki/Keyboard-Shortcuts.md +158 -0
- package/wiki/Memory-System.md +364 -0
- package/wiki/Save-States.md +172 -0
- 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,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
|
+
];
|