jsbeeb 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.editorconfig +15 -0
- package/.git-blame-ignore-revs +3 -0
- package/.github/copilot-instructions.md +94 -0
- package/.github/workflows/claude-issue-triage.yml +105 -0
- package/.github/workflows/claude.yml +63 -0
- package/.github/workflows/release-please.yml +75 -0
- package/.github/workflows/test-and-deploy.yml +86 -0
- package/.gitmodules +6 -0
- package/.husky/pre-commit +1 -0
- package/.idea/codeStyleSettings.xml +9 -0
- package/.idea/codeStyles/Project.xml +62 -0
- package/.idea/codeStyles/codeStyleConfig.xml +5 -0
- package/.idea/compiler.xml +22 -0
- package/.idea/copyright/profiles_settings.xml +3 -0
- package/.idea/encodings.xml +6 -0
- package/.idea/inspectionProfiles/Project_Default.xml +7 -0
- package/.idea/jsLibraryMappings.xml +6 -0
- package/.idea/jsLinters/jshint.xml +85 -0
- package/.idea/jsLinters/jslint.xml +15 -0
- package/.idea/jsbeeb.iml +11 -0
- package/.idea/misc.xml +6 -0
- package/.idea/modules.xml +8 -0
- package/.idea/prettier.xml +7 -0
- package/.idea/runConfigurations/Debug.xml +5 -0
- package/.idea/scopes/scope_settings.xml +5 -0
- package/.idea/vcs.xml +8 -0
- package/.prettierignore +4 -0
- package/.prettierrc.json +1 -0
- package/.release-please-manifest.json +3 -0
- package/.vscode/launch.json +14 -0
- package/.vscode/settings.json +6 -0
- package/CHANGELOG.md +32 -0
- package/CLAUDE.md +136 -0
- package/COPYING +674 -0
- package/Dockerfile +22 -0
- package/Makefile +30 -0
- package/README.md +259 -0
- package/docker/nginx-default.conf +10 -0
- package/docs/pal-comb-filter-research.md +129 -0
- package/docs/pal-simulation-design.md +368 -0
- package/eslint.config.js +35 -0
- package/index.html +954 -0
- package/jsconfig.json +10 -0
- package/package.json +102 -0
- package/public/discs/README.Irq-Timing +3 -0
- package/public/discs/README.bcdtest +5 -0
- package/public/discs/README.elite +6 -0
- package/public/discs/README.eng_test +3 -0
- package/public/discs/README.protection +7 -0
- package/public/favicon.ico +0 -0
- package/public/images/botbar.png +0 -0
- package/public/images/cub-monitor.png +0 -0
- package/public/images/jsbeeb-example.png +0 -0
- package/public/images/placeholder.png +0 -0
- package/public/images/red-off-16.png +0 -0
- package/public/images/red-on-16.png +0 -0
- package/public/images/sb/CD-left.jpg +0 -0
- package/public/images/sb/CD-right.jpg +0 -0
- package/public/images/sidebar.png +0 -0
- package/public/images/tv.png +0 -0
- package/public/images/yellow-off-16.png +0 -0
- package/public/images/yellow-on-16.png +0 -0
- package/public/jsbeeb-icon.png +0 -0
- package/public/robots.txt +3 -0
- package/public/roms/ADFS1-53.rom +0 -0
- package/public/roms/BASIC.ROM +0 -0
- package/public/roms/README +4 -0
- package/public/roms/a01/BASIC1.rom +0 -0
- package/public/roms/ample.rom +0 -0
- package/public/roms/ats-3.0.rom +0 -0
- package/public/roms/b/DFS-0.9.rom +0 -0
- package/public/roms/b/DFS-1.2.rom +0 -0
- package/public/roms/b1770/dfs1770.rom +0 -0
- package/public/roms/b1770/zADFS.ROM +0 -0
- package/public/roms/bp/dfs.rom +0 -0
- package/public/roms/bp/zADFS.ROM +0 -0
- package/public/roms/bpos.rom +0 -0
- package/public/roms/compact/adfs210.rom +0 -0
- package/public/roms/compact/basic48.rom +0 -0
- package/public/roms/compact/basic486.rom +0 -0
- package/public/roms/compact/os51.rom +0 -0
- package/public/roms/compact/utils.rom +0 -0
- package/public/roms/deos.rom +0 -0
- package/public/roms/master/anfs-4.25.rom +0 -0
- package/public/roms/master/mos.txt +68819 -0
- package/public/roms/master/mos3.20 +0 -0
- package/public/roms/os.rom +0 -0
- package/public/roms/os01.rom +0 -0
- package/public/roms/tube/6502Tube.rom +0 -0
- package/public/roms/tube/ARMeval_100.rom +0 -0
- package/public/roms/tube/BIOS.ROM +0 -0
- package/public/roms/tube/ReCo6502ROM_816 +0 -0
- package/public/roms/tube/Z80_120.rom +0 -0
- package/public/roms/us/USBASIC.rom +0 -0
- package/public/roms/us/USDNFS.rom +0 -0
- package/public/roms/usmos.rom +0 -0
- package/public/sounds/disc525/motor.wav +0 -0
- package/public/sounds/disc525/motoroff.wav +0 -0
- package/public/sounds/disc525/motoron.wav +0 -0
- package/public/sounds/disc525/seek.wav +0 -0
- package/public/sounds/disc525/seek2.wav +0 -0
- package/public/sounds/disc525/seek3.wav +0 -0
- package/public/sounds/disc525/step.wav +0 -0
- package/public/teletext/txt0.dat +0 -0
- package/public/teletext/txt1.dat +0 -0
- package/public/teletext/txt2.dat +0 -0
- package/public/teletext/txt3.dat +0 -0
- package/release-please-config.json +13 -0
- package/run-container.sh +92 -0
- package/src/6502.js +1347 -0
- package/src/6502.opcodes.js +1411 -0
- package/src/acia.js +261 -0
- package/src/adc.js +149 -0
- package/src/analogue-source.js +21 -0
- package/src/app/app.js +175 -0
- package/src/app/electron.js +20 -0
- package/src/app/preload.js +8 -0
- package/src/app-bench.js +33 -0
- package/src/basic/multiline-tetris +9 -0
- package/src/basic-tokenise.js +104 -0
- package/src/canvas.js +177 -0
- package/src/cmos.js +141 -0
- package/src/config.js +165 -0
- package/src/ddnoise.js +138 -0
- package/src/disc-drive.js +371 -0
- package/src/disc-hfe.js +396 -0
- package/src/disc.js +997 -0
- package/src/econet/L3FS.dat +0 -0
- package/src/econet/scsi.dat +0 -0
- package/src/econet.js +714 -0
- package/src/fake6502.js +32 -0
- package/src/fdc.js +248 -0
- package/src/filestore.js +666 -0
- package/src/gamepad-source.js +59 -0
- package/src/gamepads.js +268 -0
- package/src/google-drive.js +160 -0
- package/src/intel-fdc.js +1717 -0
- package/src/jsbeeb.css +363 -0
- package/src/keyboard.js +411 -0
- package/src/lib/README +1 -0
- package/src/lib/webgl-debug.js +911 -0
- package/src/main.js +1759 -0
- package/src/microphone-input.js +149 -0
- package/src/models.js +200 -0
- package/src/mouse-joystick-source.js +107 -0
- package/src/music5000-worklet.js +43 -0
- package/src/music5000.js +207 -0
- package/src/scheduler.js +148 -0
- package/src/serial.js +31 -0
- package/src/soundchip.js +314 -0
- package/src/sth.js +59 -0
- package/src/tapes.js +265 -0
- package/src/teletext.js +348 -0
- package/src/teletext_adaptor.js +172 -0
- package/src/teletext_data.js +1064 -0
- package/src/touchscreen.js +86 -0
- package/src/tube.js +349 -0
- package/src/url-params.js +256 -0
- package/src/utils.js +1090 -0
- package/src/via.js +702 -0
- package/src/video-filters/pal-composite.js +94 -0
- package/src/video-filters/passthrough-filter.js +70 -0
- package/src/video-filters/shaders/pal-composite.frag.glsl +147 -0
- package/src/video-filters/shaders/pal-composite.vert.glsl +8 -0
- package/src/video-filters/shaders/passthrough.frag.glsl +6 -0
- package/src/video-filters/shaders/passthrough.vert.glsl +7 -0
- package/src/video.js +794 -0
- package/src/wd-fdc.js +1344 -0
- package/src/web/audio-handler.js +146 -0
- package/src/web/audio-renderer.js +115 -0
- package/src/web/debug.js +529 -0
- package/tests/integration/RmwX.asm +47 -0
- package/tests/integration/TestInstructionsSource +27 -0
- package/tests/integration/TestTimingsResults +27 -0
- package/tests/integration/TestTimingsSource +61 -0
- package/tests/integration/bcd.js +23 -0
- package/tests/integration/dormann.js +101 -0
- package/tests/integration/dp111timing.js +42 -0
- package/tests/integration/ensure-submodules.js +25 -0
- package/tests/integration/nops.bas +119 -0
- package/tests/integration/nops.js +24 -0
- package/tests/integration/protection.js +26 -0
- package/tests/integration/rmw.js +69 -0
- package/tests/integration/teletext/expected_bug_469.png +0 -0
- package/tests/integration/teletext/expected_flash_0.png +0 -0
- package/tests/integration/teletext/expected_flash_1.png +0 -0
- package/tests/integration/teletext/expected_hoglet_held_char.png +0 -0
- package/tests/integration/teletext/expected_reveal_flash_0.png +0 -0
- package/tests/integration/teletext/expected_reveal_flash_1.png +0 -0
- package/tests/integration/teletext.js +126 -0
- package/tests/integration/timings.js +56 -0
- package/tests/integration/via.js +1125 -0
- package/tests/suite/README.md +7 -0
- package/tests/suite/Test Suite 2.15.txt +373 -0
- package/tests/suite/bin/ start +0 -0
- package/tests/suite/bin/adca +0 -0
- package/tests/suite/bin/adcax +0 -0
- package/tests/suite/bin/adcay +0 -0
- package/tests/suite/bin/adcb +0 -0
- package/tests/suite/bin/adcix +0 -0
- package/tests/suite/bin/adciy +0 -0
- package/tests/suite/bin/adcz +0 -0
- package/tests/suite/bin/adczx +0 -0
- package/tests/suite/bin/alrb +0 -0
- package/tests/suite/bin/ancb +0 -0
- package/tests/suite/bin/anda +0 -0
- package/tests/suite/bin/andax +0 -0
- package/tests/suite/bin/anday +0 -0
- package/tests/suite/bin/andb +0 -0
- package/tests/suite/bin/andix +0 -0
- package/tests/suite/bin/andiy +0 -0
- package/tests/suite/bin/andz +0 -0
- package/tests/suite/bin/andzx +0 -0
- package/tests/suite/bin/aneb +0 -0
- package/tests/suite/bin/arrb +0 -0
- package/tests/suite/bin/asla +0 -0
- package/tests/suite/bin/aslax +0 -0
- package/tests/suite/bin/asln +0 -0
- package/tests/suite/bin/aslz +0 -0
- package/tests/suite/bin/aslzx +0 -0
- package/tests/suite/bin/asoa +0 -0
- package/tests/suite/bin/asoax +0 -0
- package/tests/suite/bin/asoay +0 -0
- package/tests/suite/bin/asoix +0 -0
- package/tests/suite/bin/asoiy +0 -0
- package/tests/suite/bin/asoz +0 -0
- package/tests/suite/bin/asozx +0 -0
- package/tests/suite/bin/axsa +0 -0
- package/tests/suite/bin/axsix +0 -0
- package/tests/suite/bin/axsz +0 -0
- package/tests/suite/bin/axszy +0 -0
- package/tests/suite/bin/bccr +0 -0
- package/tests/suite/bin/bcsr +0 -0
- package/tests/suite/bin/beqr +0 -0
- package/tests/suite/bin/bita +0 -0
- package/tests/suite/bin/bitz +0 -0
- package/tests/suite/bin/bmir +0 -0
- package/tests/suite/bin/bner +0 -0
- package/tests/suite/bin/bplr +0 -0
- package/tests/suite/bin/branchwrap +0 -0
- package/tests/suite/bin/brkn +0 -0
- package/tests/suite/bin/bvcr +0 -0
- package/tests/suite/bin/bvsr +0 -0
- package/tests/suite/bin/cia1pb6 +0 -0
- package/tests/suite/bin/cia1pb7 +0 -0
- package/tests/suite/bin/cia1ta +0 -0
- package/tests/suite/bin/cia1tab +0 -0
- package/tests/suite/bin/cia1tb +0 -0
- package/tests/suite/bin/cia1tb123 +0 -0
- package/tests/suite/bin/cia2pb6 +0 -0
- package/tests/suite/bin/cia2pb7 +0 -0
- package/tests/suite/bin/cia2ta +0 -0
- package/tests/suite/bin/cia2tb +0 -0
- package/tests/suite/bin/cia2tb123 +0 -0
- package/tests/suite/bin/clcn +0 -0
- package/tests/suite/bin/cldn +0 -0
- package/tests/suite/bin/clin +0 -0
- package/tests/suite/bin/clvn +0 -0
- package/tests/suite/bin/cmpa +0 -0
- package/tests/suite/bin/cmpax +0 -0
- package/tests/suite/bin/cmpay +0 -0
- package/tests/suite/bin/cmpb +0 -0
- package/tests/suite/bin/cmpix +0 -0
- package/tests/suite/bin/cmpiy +0 -0
- package/tests/suite/bin/cmpz +0 -0
- package/tests/suite/bin/cmpzx +0 -0
- package/tests/suite/bin/cntdef +0 -0
- package/tests/suite/bin/cnto2 +0 -0
- package/tests/suite/bin/cpuport +0 -0
- package/tests/suite/bin/cputiming +0 -0
- package/tests/suite/bin/cpxa +0 -0
- package/tests/suite/bin/cpxb +0 -0
- package/tests/suite/bin/cpxz +0 -0
- package/tests/suite/bin/cpya +0 -0
- package/tests/suite/bin/cpyb +0 -0
- package/tests/suite/bin/cpyz +0 -0
- package/tests/suite/bin/dcma +0 -0
- package/tests/suite/bin/dcmax +0 -0
- package/tests/suite/bin/dcmay +0 -0
- package/tests/suite/bin/dcmix +0 -0
- package/tests/suite/bin/dcmiy +0 -0
- package/tests/suite/bin/dcmz +0 -0
- package/tests/suite/bin/dcmzx +0 -0
- package/tests/suite/bin/deca +0 -0
- package/tests/suite/bin/decax +0 -0
- package/tests/suite/bin/decz +0 -0
- package/tests/suite/bin/deczx +0 -0
- package/tests/suite/bin/dexn +0 -0
- package/tests/suite/bin/deyn +0 -0
- package/tests/suite/bin/eora +0 -0
- package/tests/suite/bin/eorax +0 -0
- package/tests/suite/bin/eoray +0 -0
- package/tests/suite/bin/eorb +0 -0
- package/tests/suite/bin/eorix +0 -0
- package/tests/suite/bin/eoriy +0 -0
- package/tests/suite/bin/eorz +0 -0
- package/tests/suite/bin/eorzx +0 -0
- package/tests/suite/bin/finish +0 -0
- package/tests/suite/bin/flipos +0 -0
- package/tests/suite/bin/icr01 +0 -0
- package/tests/suite/bin/imr +0 -0
- package/tests/suite/bin/inca +0 -0
- package/tests/suite/bin/incax +0 -0
- package/tests/suite/bin/incz +0 -0
- package/tests/suite/bin/inczx +0 -0
- package/tests/suite/bin/insa +0 -0
- package/tests/suite/bin/insax +0 -0
- package/tests/suite/bin/insay +0 -0
- package/tests/suite/bin/insix +0 -0
- package/tests/suite/bin/insiy +0 -0
- package/tests/suite/bin/insz +0 -0
- package/tests/suite/bin/inszx +0 -0
- package/tests/suite/bin/inxn +0 -0
- package/tests/suite/bin/inyn +0 -0
- package/tests/suite/bin/irq +0 -0
- package/tests/suite/bin/jmpi +0 -0
- package/tests/suite/bin/jmpw +0 -0
- package/tests/suite/bin/jsrw +0 -0
- package/tests/suite/bin/lasay +0 -0
- package/tests/suite/bin/laxa +0 -0
- package/tests/suite/bin/laxay +0 -0
- package/tests/suite/bin/laxix +0 -0
- package/tests/suite/bin/laxiy +0 -0
- package/tests/suite/bin/laxz +0 -0
- package/tests/suite/bin/laxzy +0 -0
- package/tests/suite/bin/ldaa +0 -0
- package/tests/suite/bin/ldaax +0 -0
- package/tests/suite/bin/ldaay +0 -0
- package/tests/suite/bin/ldab +0 -0
- package/tests/suite/bin/ldaix +0 -0
- package/tests/suite/bin/ldaiy +0 -0
- package/tests/suite/bin/ldaz +0 -0
- package/tests/suite/bin/ldazx +0 -0
- package/tests/suite/bin/ldxa +0 -0
- package/tests/suite/bin/ldxay +0 -0
- package/tests/suite/bin/ldxb +0 -0
- package/tests/suite/bin/ldxz +0 -0
- package/tests/suite/bin/ldxzy +0 -0
- package/tests/suite/bin/ldya +0 -0
- package/tests/suite/bin/ldyax +0 -0
- package/tests/suite/bin/ldyb +0 -0
- package/tests/suite/bin/ldyz +0 -0
- package/tests/suite/bin/ldyzx +0 -0
- package/tests/suite/bin/loadth +0 -0
- package/tests/suite/bin/lsea +0 -0
- package/tests/suite/bin/lseax +0 -0
- package/tests/suite/bin/lseay +0 -0
- package/tests/suite/bin/lseix +0 -0
- package/tests/suite/bin/lseiy +0 -0
- package/tests/suite/bin/lsez +0 -0
- package/tests/suite/bin/lsezx +0 -0
- package/tests/suite/bin/lsra +0 -0
- package/tests/suite/bin/lsrax +0 -0
- package/tests/suite/bin/lsrn +0 -0
- package/tests/suite/bin/lsrz +0 -0
- package/tests/suite/bin/lsrzx +0 -0
- package/tests/suite/bin/lxab +0 -0
- package/tests/suite/bin/mmu +0 -0
- package/tests/suite/bin/mmufetch +0 -0
- package/tests/suite/bin/nmi +0 -0
- package/tests/suite/bin/nopa +0 -0
- package/tests/suite/bin/nopax +0 -0
- package/tests/suite/bin/nopb +0 -0
- package/tests/suite/bin/nopn +0 -0
- package/tests/suite/bin/nopz +0 -0
- package/tests/suite/bin/nopzx +0 -0
- package/tests/suite/bin/oneshot +0 -0
- package/tests/suite/bin/oraa +0 -0
- package/tests/suite/bin/oraax +0 -0
- package/tests/suite/bin/oraay +0 -0
- package/tests/suite/bin/orab +0 -0
- package/tests/suite/bin/oraix +0 -0
- package/tests/suite/bin/oraiy +0 -0
- package/tests/suite/bin/oraz +0 -0
- package/tests/suite/bin/orazx +0 -0
- package/tests/suite/bin/phan +0 -0
- package/tests/suite/bin/phpn +0 -0
- package/tests/suite/bin/plan +0 -0
- package/tests/suite/bin/plpn +0 -0
- package/tests/suite/bin/rlaa +0 -0
- package/tests/suite/bin/rlaax +0 -0
- package/tests/suite/bin/rlaay +0 -0
- package/tests/suite/bin/rlaix +0 -0
- package/tests/suite/bin/rlaiy +0 -0
- package/tests/suite/bin/rlaz +0 -0
- package/tests/suite/bin/rlazx +0 -0
- package/tests/suite/bin/rola +0 -0
- package/tests/suite/bin/rolax +0 -0
- package/tests/suite/bin/roln +0 -0
- package/tests/suite/bin/rolz +0 -0
- package/tests/suite/bin/rolzx +0 -0
- package/tests/suite/bin/rora +0 -0
- package/tests/suite/bin/rorax +0 -0
- package/tests/suite/bin/rorn +0 -0
- package/tests/suite/bin/rorz +0 -0
- package/tests/suite/bin/rorzx +0 -0
- package/tests/suite/bin/rraa +0 -0
- package/tests/suite/bin/rraax +0 -0
- package/tests/suite/bin/rraay +0 -0
- package/tests/suite/bin/rraix +0 -0
- package/tests/suite/bin/rraiy +0 -0
- package/tests/suite/bin/rraz +0 -0
- package/tests/suite/bin/rrazx +0 -0
- package/tests/suite/bin/rtin +0 -0
- package/tests/suite/bin/rtsn +0 -0
- package/tests/suite/bin/sbca +0 -0
- package/tests/suite/bin/sbcax +0 -0
- package/tests/suite/bin/sbcay +0 -0
- package/tests/suite/bin/sbcb +0 -0
- package/tests/suite/bin/sbcb(eb) +0 -0
- package/tests/suite/bin/sbcix +0 -0
- package/tests/suite/bin/sbciy +0 -0
- package/tests/suite/bin/sbcz +0 -0
- package/tests/suite/bin/sbczx +0 -0
- package/tests/suite/bin/sbxb +0 -0
- package/tests/suite/bin/secn +0 -0
- package/tests/suite/bin/sedn +0 -0
- package/tests/suite/bin/sein +0 -0
- package/tests/suite/bin/shaay +0 -0
- package/tests/suite/bin/shaiy +0 -0
- package/tests/suite/bin/shsay +0 -0
- package/tests/suite/bin/shxay +0 -0
- package/tests/suite/bin/shyax +0 -0
- package/tests/suite/bin/staa +0 -0
- package/tests/suite/bin/staax +0 -0
- package/tests/suite/bin/staay +0 -0
- package/tests/suite/bin/staix +0 -0
- package/tests/suite/bin/staiy +0 -0
- package/tests/suite/bin/staz +0 -0
- package/tests/suite/bin/stazx +0 -0
- package/tests/suite/bin/stxa +0 -0
- package/tests/suite/bin/stxz +0 -0
- package/tests/suite/bin/stxzy +0 -0
- package/tests/suite/bin/stya +0 -0
- package/tests/suite/bin/styz +0 -0
- package/tests/suite/bin/styzx +0 -0
- package/tests/suite/bin/taxn +0 -0
- package/tests/suite/bin/tayn +0 -0
- package/tests/suite/bin/trap1 +0 -0
- package/tests/suite/bin/trap10 +0 -0
- package/tests/suite/bin/trap11 +0 -0
- package/tests/suite/bin/trap12 +0 -0
- package/tests/suite/bin/trap13 +0 -0
- package/tests/suite/bin/trap14 +0 -0
- package/tests/suite/bin/trap15 +0 -0
- package/tests/suite/bin/trap16 +0 -0
- package/tests/suite/bin/trap17 +0 -0
- package/tests/suite/bin/trap2 +0 -0
- package/tests/suite/bin/trap3 +0 -0
- package/tests/suite/bin/trap4 +0 -0
- package/tests/suite/bin/trap5 +0 -0
- package/tests/suite/bin/trap6 +0 -0
- package/tests/suite/bin/trap7 +0 -0
- package/tests/suite/bin/trap8 +0 -0
- package/tests/suite/bin/trap9 +0 -0
- package/tests/suite/bin/tsxn +0 -0
- package/tests/suite/bin/txan +0 -0
- package/tests/suite/bin/txsn +0 -0
- package/tests/suite/bin/tyan +0 -0
- package/tests/suite/cbm-hackers-post.html +178 -0
- package/tests/suite/cbm-hackers-post.md +78 -0
- package/tests/test-machine.js +288 -0
- package/tests/test-suite.js +147 -0
- package/tests/test.css +7 -0
- package/tests/unit/gzip/test-1 +0 -0
- package/tests/unit/gzip/test-1.gz +0 -0
- package/tests/unit/gzip/test-2 +0 -0
- package/tests/unit/gzip/test-2.gz +0 -0
- package/tests/unit/gzip/test-3 +0 -0
- package/tests/unit/gzip/test-3.gz +0 -0
- package/tests/unit/gzip/test-4 +0 -0
- package/tests/unit/gzip/test-4.gz +0 -0
- package/tests/unit/test-adc.js +307 -0
- package/tests/unit/test-bcd.js +30 -0
- package/tests/unit/test-cmos.js +266 -0
- package/tests/unit/test-disc-drive.js +85 -0
- package/tests/unit/test-disc-hfe.js +347 -0
- package/tests/unit/test-disc.js +232 -0
- package/tests/unit/test-fifo.js +35 -0
- package/tests/unit/test-gamepad-source.js +67 -0
- package/tests/unit/test-gzip.js +22 -0
- package/tests/unit/test-intel-fdc.js +93 -0
- package/tests/unit/test-keyboard.js +410 -0
- package/tests/unit/test-mouse-joystick-source.js +128 -0
- package/tests/unit/test-scheduler.js +190 -0
- package/tests/unit/test-serial.js +154 -0
- package/tests/unit/test-teletext-adaptor.js +359 -0
- package/tests/unit/test-tokenise.js +65 -0
- package/tests/unit/test-url-params.js +398 -0
- package/tests/unit/test-utils.js +276 -0
- package/tests/unit/test-video.js +498 -0
- package/tests/unit/test-zip.js +56 -0
- package/tests/unit/zip/test-mixed.zip +0 -0
- package/tests/unit/zip/test-rom.zip +0 -0
- package/tests/unit/zip/test-ssd.zip +0 -0
- package/tools/fir-generator.js +80 -0
- package/tools/vite-plugin-fir-shader.js +131 -0
- package/vite.config.js +34 -0
package/src/wd-fdc.js
ADDED
|
@@ -0,0 +1,1344 @@
|
|
|
1
|
+
// Translated from beebjit by Chris Evans.
|
|
2
|
+
// https://github.com/scarybeasts/beebjit
|
|
3
|
+
// eslint-disable-next-line no-unused-vars
|
|
4
|
+
import { Cpu6502 } from "./6502.js";
|
|
5
|
+
// eslint-disable-next-line no-unused-vars
|
|
6
|
+
import { BaseDiscDrive, DiscDrive } from "./disc-drive.js";
|
|
7
|
+
import { IbmDiscFormat } from "./disc.js";
|
|
8
|
+
// eslint-disable-next-line no-unused-vars
|
|
9
|
+
import { Scheduler } from "./scheduler.js";
|
|
10
|
+
import * as utils from "./utils.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Commands.
|
|
14
|
+
*
|
|
15
|
+
* @readonly
|
|
16
|
+
* @enum {Number}
|
|
17
|
+
*/
|
|
18
|
+
const Command = Object.freeze({
|
|
19
|
+
restore: 0x00,
|
|
20
|
+
seek: 0x10,
|
|
21
|
+
stepInNoUpdate: 0x40,
|
|
22
|
+
stepInWithUpdate: 0x50,
|
|
23
|
+
stepOutNoUpdate: 0x60,
|
|
24
|
+
stepOutWithUpdate: 0x70,
|
|
25
|
+
readSector: 0x80,
|
|
26
|
+
readSectorMulti: 0x90,
|
|
27
|
+
writeSector: 0xa0,
|
|
28
|
+
writeSectorMulti: 0xb0,
|
|
29
|
+
readAddress: 0xc0,
|
|
30
|
+
forceInterrupt: 0xd0,
|
|
31
|
+
readTrack: 0xe0,
|
|
32
|
+
writeTrack: 0xf0,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Command bits.
|
|
37
|
+
*
|
|
38
|
+
* @readonly
|
|
39
|
+
* @enum {Number}
|
|
40
|
+
*/
|
|
41
|
+
const CommandBits = Object.freeze({
|
|
42
|
+
typeIIMulti: 0x10,
|
|
43
|
+
disableSpinUp: 0x08,
|
|
44
|
+
typeIVerify: 0x04,
|
|
45
|
+
typeIIorIIISettle: 0x04,
|
|
46
|
+
typeIIDeleted: 0x01,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The drive control register is documented here:
|
|
51
|
+
* https://www.cloud9.co.uk/james/BBCMicro/Documentation/wd1770.html
|
|
52
|
+
*
|
|
53
|
+
* @readonly
|
|
54
|
+
* @enum {Number}
|
|
55
|
+
*/
|
|
56
|
+
const Control = Object.freeze({
|
|
57
|
+
reset: 0x20,
|
|
58
|
+
density: 0x08,
|
|
59
|
+
side: 0x04,
|
|
60
|
+
drive1: 0x02,
|
|
61
|
+
drive0: 0x01,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Status bits.
|
|
66
|
+
*
|
|
67
|
+
* @readonly
|
|
68
|
+
* @enum {Number}
|
|
69
|
+
*/
|
|
70
|
+
const Status = Object.freeze({
|
|
71
|
+
motorOn: 0x80,
|
|
72
|
+
writeProtected: 0x40,
|
|
73
|
+
typeISpinUpDone: 0x20,
|
|
74
|
+
typeIIorIIIDeletedMark: 0x20,
|
|
75
|
+
recordNotFound: 0x10,
|
|
76
|
+
crcError: 0x08,
|
|
77
|
+
typeITrack0: 0x04,
|
|
78
|
+
typeIIorIIILostByte: 0x04,
|
|
79
|
+
typeIIndex: 0x02,
|
|
80
|
+
typeIIorIIIDrq: 0x02,
|
|
81
|
+
busy: 0x01,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Controller state.
|
|
86
|
+
*
|
|
87
|
+
* @readonly
|
|
88
|
+
* @enum {Number}
|
|
89
|
+
*/
|
|
90
|
+
const State = Object.freeze({
|
|
91
|
+
null: 0,
|
|
92
|
+
idle: 1,
|
|
93
|
+
timerWait: 2,
|
|
94
|
+
spinUpWait: 3,
|
|
95
|
+
waitIndex: 4,
|
|
96
|
+
searchId: 5,
|
|
97
|
+
inId: 6,
|
|
98
|
+
searchData: 7,
|
|
99
|
+
inData: 8,
|
|
100
|
+
inReadTrack: 8,
|
|
101
|
+
writeSectorDelay: 9,
|
|
102
|
+
writeSectorLeadInFm: 10,
|
|
103
|
+
writeSectorLeadInMfm: 11,
|
|
104
|
+
writeSectorMarkerFm: 12,
|
|
105
|
+
writeSectorMarkerMfm: 13,
|
|
106
|
+
writeSectorBody: 14,
|
|
107
|
+
writeTrackSetup: 15,
|
|
108
|
+
inWriteTrack: 16,
|
|
109
|
+
checkMulti: 17,
|
|
110
|
+
done: 18,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Timer state.
|
|
115
|
+
*
|
|
116
|
+
* @readonly
|
|
117
|
+
* @enum {Number}
|
|
118
|
+
*/
|
|
119
|
+
const TimerState = Object.freeze({
|
|
120
|
+
none: 1,
|
|
121
|
+
settle: 2,
|
|
122
|
+
seek: 3,
|
|
123
|
+
done: 4,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
export class WdFdc {
|
|
127
|
+
/**
|
|
128
|
+
* @param {Cpu6502} cpu
|
|
129
|
+
* @param {Scheduler} scheduler
|
|
130
|
+
* @param {BaseDiscDrive[] | undefined} drives
|
|
131
|
+
* @param {*} debugFlags
|
|
132
|
+
*/
|
|
133
|
+
constructor(cpu, scheduler, drives, debugFlags) {
|
|
134
|
+
this._cpu = cpu;
|
|
135
|
+
if (drives) this._drives = drives;
|
|
136
|
+
else this._drives = [new DiscDrive(0, scheduler), new DiscDrive(1, scheduler)];
|
|
137
|
+
|
|
138
|
+
this._isMaster = cpu.model.isMaster;
|
|
139
|
+
this._is1772 = false; // TODO - if we ever support Master Compact
|
|
140
|
+
this._isOpus = false; // TODO - if we ever support Opus
|
|
141
|
+
|
|
142
|
+
this._controlRegister = 0;
|
|
143
|
+
/** @type {Status|Number} */
|
|
144
|
+
this._statusRegister = 0;
|
|
145
|
+
this._trackRegister = 0;
|
|
146
|
+
this._sectorRegister = 0;
|
|
147
|
+
this._dataRegister = 0;
|
|
148
|
+
this._isIntRq = false;
|
|
149
|
+
this._isDrq = false;
|
|
150
|
+
this._doRaiseIntRq = false;
|
|
151
|
+
|
|
152
|
+
/** @type {BaseDiscDrive|null} */
|
|
153
|
+
this._currentDrive = null;
|
|
154
|
+
this._isIndexPulse = false;
|
|
155
|
+
this._isInterruptOnIndexPulse = false;
|
|
156
|
+
this._isWriteTrackCrcSecondByte = false;
|
|
157
|
+
this._command = 0;
|
|
158
|
+
this._commandType = 0;
|
|
159
|
+
this._isCommandSettle = false;
|
|
160
|
+
this._isCommandWrite = false;
|
|
161
|
+
this._isCommandVerify = false;
|
|
162
|
+
this._isCommandMulti = false;
|
|
163
|
+
this._isCommandDeleted = false;
|
|
164
|
+
|
|
165
|
+
this._commandStepRateMs = 0;
|
|
166
|
+
this._state = State.idle;
|
|
167
|
+
this._timerState = TimerState.none;
|
|
168
|
+
this._timerTask = scheduler.newTask(() => this._timerFired());
|
|
169
|
+
this._stateCount = 0;
|
|
170
|
+
this._indexPulseCount = 0;
|
|
171
|
+
this._markDetector = 0n;
|
|
172
|
+
this._dataShifter = 0;
|
|
173
|
+
this._dataShiftCount = 0;
|
|
174
|
+
this._deliverData = 0;
|
|
175
|
+
this._deliverIsMarker = false;
|
|
176
|
+
this._crc = 0;
|
|
177
|
+
this._onDiscTrack = 0;
|
|
178
|
+
this._onDiscSector = 0;
|
|
179
|
+
this._onDiscLength = 0;
|
|
180
|
+
this._onDiscCrc = 0;
|
|
181
|
+
this._lastMfmBit = false;
|
|
182
|
+
|
|
183
|
+
this._logCommands = debugFlags ? !!debugFlags.logFdcCommands : false;
|
|
184
|
+
this._logStateChanges = debugFlags ? !!debugFlags.logFdcStateChanges : false;
|
|
185
|
+
|
|
186
|
+
const callback = (pulses, count) => this._pulsesCallback(pulses, count);
|
|
187
|
+
for (const drive of this._drives) drive.setPulsesCallback(callback);
|
|
188
|
+
|
|
189
|
+
this.powerOnReset();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
reset() {
|
|
193
|
+
// This will:
|
|
194
|
+
// - Spin down.
|
|
195
|
+
// - Raise reset, which:
|
|
196
|
+
// - Clears status register.
|
|
197
|
+
// - Sets other registers as per how a real machine behaves.
|
|
198
|
+
// - Clears IRQs.
|
|
199
|
+
this._writeControl(0);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
powerOnReset() {
|
|
203
|
+
this.reset();
|
|
204
|
+
// The reset line doesn't seem to affect the track or data registers.
|
|
205
|
+
this._trackRegister = 0;
|
|
206
|
+
this._dataRegister = 0;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
_updateNmi() {
|
|
210
|
+
const newLevel = this._isDrq | (this._isOpus ? false : this._isIntRq);
|
|
211
|
+
// TODO: the cpu handling of NMIs is bad here. Should update to handle multiple
|
|
212
|
+
// NMI/interrupt sources. And when we do go back and implement the checks in the beebjit
|
|
213
|
+
// source here too.
|
|
214
|
+
this._cpu.NMI(newLevel);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* @param {boolean} level
|
|
219
|
+
*/
|
|
220
|
+
_setIntRq(level) {
|
|
221
|
+
this._isIntRq = level;
|
|
222
|
+
this._updateNmi();
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* @param {boolean} level
|
|
227
|
+
*/
|
|
228
|
+
_setDrq(level) {
|
|
229
|
+
this._isDrq = level;
|
|
230
|
+
if (level) {
|
|
231
|
+
if (this._statusRegister & Status.typeIIorIIIDrq) {
|
|
232
|
+
this._statusRegister |= Status.typeIIorIIILostByte;
|
|
233
|
+
}
|
|
234
|
+
this._statusRegister |= Status.typeIIorIIIDrq;
|
|
235
|
+
} else {
|
|
236
|
+
this._statusRegister &= ~Status.typeIIorIIIDrq;
|
|
237
|
+
}
|
|
238
|
+
this._updateNmi();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
_log(message) {
|
|
242
|
+
console.log(`WD1770: ${message}`);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
_logCommand(message) {
|
|
246
|
+
if (this._logCommands) this._log(message);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
_opusRemapAddr(addr) {
|
|
250
|
+
return addr ^ 4;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
_opusRemapVal(addr, val) {
|
|
254
|
+
// Only remap control register values.
|
|
255
|
+
if (addr >= 4) return val;
|
|
256
|
+
let remapped = Control.reset;
|
|
257
|
+
if (val & 0x01) remapped |= Control.drive0;
|
|
258
|
+
else remapped |= Control.drive1;
|
|
259
|
+
if (val & 0x02) remapped |= Control.side;
|
|
260
|
+
if (val & 0x40) remapped |= Control.density;
|
|
261
|
+
return remapped;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
_masterRemapVal(addr, val) {
|
|
265
|
+
// Only remap control register values.
|
|
266
|
+
if (addr >= 4) return val;
|
|
267
|
+
let remapped = 0;
|
|
268
|
+
if (val & 0x04) remapped |= Control.reset;
|
|
269
|
+
if (val & 0x01) remapped |= Control.drive0;
|
|
270
|
+
if (val & 0x02) remapped |= Control.drive1;
|
|
271
|
+
if (val & 0x10) remapped |= Control.side;
|
|
272
|
+
if (val & 0x20) remapped |= Control.density;
|
|
273
|
+
return remapped;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
_remapVal(addr, val) {
|
|
277
|
+
if (this._isMaster) return this._masterRemapVal(addr, val);
|
|
278
|
+
if (this._isOpus) return this._opusRemapVal(addr, val);
|
|
279
|
+
return val;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
_remapAddr(addr) {
|
|
283
|
+
addr &= 0x07;
|
|
284
|
+
if (this._isMaster) return addr ^ 0x04;
|
|
285
|
+
if (this._isOpus) return this._opusRemapAddr(addr);
|
|
286
|
+
return addr;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* @param {Number} addr hardware address
|
|
291
|
+
* @returns {Number} byte at the given hardware address
|
|
292
|
+
*/
|
|
293
|
+
read(addr) {
|
|
294
|
+
switch (this._remapAddr(addr)) {
|
|
295
|
+
case 4:
|
|
296
|
+
// Reading status register clears INTRQ.
|
|
297
|
+
this._setIntRq(false);
|
|
298
|
+
return this._statusRegister;
|
|
299
|
+
case 5:
|
|
300
|
+
return this._trackRegister;
|
|
301
|
+
case 6:
|
|
302
|
+
return this._sectorRegister;
|
|
303
|
+
case 7:
|
|
304
|
+
if (this._commandType === 2 || this._commandType === 3) {
|
|
305
|
+
this._setDrq(false);
|
|
306
|
+
}
|
|
307
|
+
return this._dataRegister;
|
|
308
|
+
case 0:
|
|
309
|
+
case 1:
|
|
310
|
+
case 2:
|
|
311
|
+
case 3:
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
314
|
+
return 0xfe;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* @param {Number} addr hardware address
|
|
319
|
+
* @param {Number} val byte to write
|
|
320
|
+
*/
|
|
321
|
+
write(addr, val) {
|
|
322
|
+
addr = this._remapAddr(addr);
|
|
323
|
+
val = this._remapVal(addr, val);
|
|
324
|
+
switch (addr) {
|
|
325
|
+
case 0:
|
|
326
|
+
case 1:
|
|
327
|
+
case 2:
|
|
328
|
+
case 3:
|
|
329
|
+
this._logCommand(`control register now ${utils.hexbyte(val)}`);
|
|
330
|
+
if (this._statusRegister & Status.busy && !this._isReset(val)) {
|
|
331
|
+
throw new Error(`Control register updated while busy; without reset`);
|
|
332
|
+
}
|
|
333
|
+
this._writeControl(val);
|
|
334
|
+
break;
|
|
335
|
+
case 4:
|
|
336
|
+
// Ignore commands while in reset.
|
|
337
|
+
if (!this._isReset(this._controlRegister)) this._doCommand(val);
|
|
338
|
+
break;
|
|
339
|
+
case 5:
|
|
340
|
+
this._logCommand(`track register now ${val}`);
|
|
341
|
+
this._trackRegister = val;
|
|
342
|
+
break;
|
|
343
|
+
case 6:
|
|
344
|
+
// Ignore sector reg changes in reset; note that track/data registers will still be accepted.
|
|
345
|
+
if (!this._isReset(this._controlRegister)) {
|
|
346
|
+
this._logCommand(`sector register now ${val}`);
|
|
347
|
+
this._sectorRegister = val;
|
|
348
|
+
} else {
|
|
349
|
+
this._logCommand(`ignoring sector write of ${val}`);
|
|
350
|
+
}
|
|
351
|
+
break;
|
|
352
|
+
case 7:
|
|
353
|
+
if (this._commandType === 2 || this._commandType === 3) {
|
|
354
|
+
this._setDrq(false);
|
|
355
|
+
}
|
|
356
|
+
this._dataRegister = val;
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
_doCommand(val) {
|
|
362
|
+
if (!this._currentDrive) throw new Error("Command while no selected drive");
|
|
363
|
+
this._logCommand(
|
|
364
|
+
`command ${utils.hexbyte(val)} tr ${this._trackRegister} sr ${this._sectorRegister} dr ${this._dataRegister} ` +
|
|
365
|
+
`cr ${utils.hexbyte(this._controlRegister)} ` +
|
|
366
|
+
`ptrk ${this._currentDrive.track} hpos ${this._currentDrive.headPosition}`,
|
|
367
|
+
);
|
|
368
|
+
const command = val & 0xf0;
|
|
369
|
+
|
|
370
|
+
if (command === Command.forceInterrupt) {
|
|
371
|
+
this._handleForceInterrupt(val);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
if (this._statusRegister & Status.busy) {
|
|
375
|
+
// EMU NOTE: this is a very murky area. There does not appear to be a simple
|
|
376
|
+
// rule here. Whether a command will do anything when busy seems to depend on
|
|
377
|
+
// the current command, the new command and also the current place in the
|
|
378
|
+
// internal state machine!
|
|
379
|
+
this._log(`command ${utils.hexbyte(val)} while busy with ${utils.hexbyte(this._command)} - ignoring`);
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
this._command = command;
|
|
384
|
+
this._isCommandSettle = false;
|
|
385
|
+
this._isCommandWrite = false;
|
|
386
|
+
this._isCommandVerify = false;
|
|
387
|
+
this._isCommandMulti = false;
|
|
388
|
+
this._isCommandDeleted = false;
|
|
389
|
+
this._isInterruptOnIndexPulse = false;
|
|
390
|
+
this._isWriteTrackCrcSecondByte = false;
|
|
391
|
+
|
|
392
|
+
switch (command) {
|
|
393
|
+
case Command.restore:
|
|
394
|
+
case Command.seek:
|
|
395
|
+
case Command.stepInNoUpdate:
|
|
396
|
+
case Command.stepInWithUpdate:
|
|
397
|
+
case Command.stepOutNoUpdate:
|
|
398
|
+
case Command.stepOutWithUpdate:
|
|
399
|
+
this._commandType = 1;
|
|
400
|
+
this._isCommandVerify = !!(val & CommandBits.typeIVerify);
|
|
401
|
+
this._commandStepRateMs = this._stepRateMsFor(val);
|
|
402
|
+
break;
|
|
403
|
+
case Command.readSector:
|
|
404
|
+
case Command.readSectorMulti:
|
|
405
|
+
case Command.writeSector:
|
|
406
|
+
case Command.writeSectorMulti:
|
|
407
|
+
this._commandType = 2;
|
|
408
|
+
this._isCommandMulti = !!(val & CommandBits.typeIIMulti);
|
|
409
|
+
break;
|
|
410
|
+
case Command.readAddress:
|
|
411
|
+
case Command.readTrack:
|
|
412
|
+
case Command.writeTrack:
|
|
413
|
+
this._commandType = 3;
|
|
414
|
+
break;
|
|
415
|
+
default:
|
|
416
|
+
throw new Error(`unimplemented command ${utils.hexbyte(val)}`);
|
|
417
|
+
}
|
|
418
|
+
if (this._commandType === 2 || (this._commandType === 3 && val & CommandBits.typeIIorIIISettle))
|
|
419
|
+
this._isCommandSettle = true;
|
|
420
|
+
if (
|
|
421
|
+
this._command === Command.writeSector ||
|
|
422
|
+
this._command === Command.writeSectorMulti ||
|
|
423
|
+
this._command === Command.writeTrack
|
|
424
|
+
) {
|
|
425
|
+
this._isCommandWrite = true;
|
|
426
|
+
this._isCommandDeleted = !!(val & CommandBits.typeIIDeleted);
|
|
427
|
+
}
|
|
428
|
+
// All commands except force interrupt (handled above):
|
|
429
|
+
// - Clear INTRQ and DRQ.
|
|
430
|
+
// - Clear status register result bits.
|
|
431
|
+
// - Set busy.
|
|
432
|
+
// - Spin up if necessary and not inhibited.
|
|
433
|
+
this._setDrq(false);
|
|
434
|
+
this._setIntRq(false);
|
|
435
|
+
this._statusRegister = (this._statusRegister & Status.motorOn) | Status.busy;
|
|
436
|
+
|
|
437
|
+
this._indexPulseCount = 0;
|
|
438
|
+
if (this._statusRegister & Status.motorOn) {
|
|
439
|
+
// Short circuit spin-up if motor is on.
|
|
440
|
+
this._dispatchCommand();
|
|
441
|
+
} else {
|
|
442
|
+
this._statusRegister |= Status.motorOn;
|
|
443
|
+
this._currentDrive.startSpinning();
|
|
444
|
+
// Short circuit spin-up if command requests it.
|
|
445
|
+
// /* NOTE: disabling spin-up wait is a strange facility. It makes a lot of
|
|
446
|
+
// sense for a seek because the disc head can usefully get moving while the
|
|
447
|
+
// motor is spinning up. But other commands like a read track also seem to
|
|
448
|
+
// start immediately. It is unclear whether such a command would be
|
|
449
|
+
// unreliable on a drive that takes a while to come up to speed.
|
|
450
|
+
if (val & CommandBits.disableSpinUp) {
|
|
451
|
+
this._indexPulseCount = 6;
|
|
452
|
+
this._log(`command ${utils.hexbyte(val)} spin up wait disabled, motor was off`);
|
|
453
|
+
this._dispatchCommand();
|
|
454
|
+
} else {
|
|
455
|
+
this._setState(State.spinUpWait);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* @param {State|Number} state
|
|
462
|
+
*/
|
|
463
|
+
_setState(state) {
|
|
464
|
+
if (this._logStateChanges && state !== this._state) {
|
|
465
|
+
this._log(
|
|
466
|
+
`State ${this._state} -> ${state} @ tr ${this._trackRegister} ` +
|
|
467
|
+
`sr ${this._sectorRegister} dr ${this._dataRegister} ` +
|
|
468
|
+
`cr ${utils.hexbyte(this._controlRegister)} ` +
|
|
469
|
+
`ptrk ${this._currentDrive.track} hpos ${this._currentDrive.headPosition}`,
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
this._state = state;
|
|
473
|
+
this._stateCount = 0;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
_clearTimer() {
|
|
477
|
+
if (this._timerState !== TimerState.none) {
|
|
478
|
+
this._timerTask.cancel();
|
|
479
|
+
this._timerState = TimerState.none;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
_clearState() {
|
|
484
|
+
this._setState(State.idle);
|
|
485
|
+
this._clearTimer();
|
|
486
|
+
this._indexPulseCount = 0;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
_stepRateMsFor(val) {
|
|
490
|
+
switch (val & 0x03) {
|
|
491
|
+
case 0:
|
|
492
|
+
return 6;
|
|
493
|
+
case 1:
|
|
494
|
+
return 12;
|
|
495
|
+
case 2:
|
|
496
|
+
return this._is1772 ? 2 : 20;
|
|
497
|
+
case 3:
|
|
498
|
+
return this._is1772 ? 3 : 30;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
_handleForceInterrupt(val) {
|
|
503
|
+
const forceInterruptBits = val & 0x0f;
|
|
504
|
+
// EMU NOTE: force interrupt is pretty unclear on the datasheet. From
|
|
505
|
+
// testing on a real 1772:
|
|
506
|
+
// - The command is aborted right away in all cases.
|
|
507
|
+
// - The command completion INTRQ / NMI is _inhibited_ for $D0. In
|
|
508
|
+
// particular, Watford Electronics DDFS will be unhappy unless you behave
|
|
509
|
+
// correctly here.
|
|
510
|
+
// - Force interrupt will spin up the motor and enter an idle state if
|
|
511
|
+
// the motor is off. The idle state behaves a little like a type 1 command
|
|
512
|
+
// insofar as index pulse appears to be reported in the status register.
|
|
513
|
+
// - Interrupt on index pulse is only active for the current command.
|
|
514
|
+
if (this._statusRegister & Status.busy) {
|
|
515
|
+
this._commandDone(false);
|
|
516
|
+
} else {
|
|
517
|
+
if (this._state !== State.idle) throw new Error(`Unexpected state when force interrupt: ${this._state}`);
|
|
518
|
+
this._indexPulseCount = 0;
|
|
519
|
+
this._commandType = 1;
|
|
520
|
+
this._statusRegister &= Status.motorOn;
|
|
521
|
+
if (!(this._statusRegister & Status.motorOn)) {
|
|
522
|
+
this._statusRegister |= Status.motorOn;
|
|
523
|
+
this._currentDrive.startSpinning();
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
if (forceInterruptBits === 0) {
|
|
527
|
+
this._isInterruptOnIndexPulse = false;
|
|
528
|
+
} else if (forceInterruptBits === 4) {
|
|
529
|
+
this._isInterruptOnIndexPulse = true;
|
|
530
|
+
} else {
|
|
531
|
+
throw new Error(`1700 force interrupt flags not handled: ${forceInterruptBits}`);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
_timerFired() {
|
|
536
|
+
if (!(this._statusRegister & Status.busy)) throw new Error("Should be busy");
|
|
537
|
+
const timerState = this._timerState;
|
|
538
|
+
this._timerState = TimerState.none;
|
|
539
|
+
switch (timerState) {
|
|
540
|
+
case TimerState.settle:
|
|
541
|
+
this._dispatchCommand();
|
|
542
|
+
break;
|
|
543
|
+
case TimerState.seek:
|
|
544
|
+
if (
|
|
545
|
+
this._command === Command.stepInNoUpdate ||
|
|
546
|
+
this._command === Command.stepInWithUpdate ||
|
|
547
|
+
this._command === Command.stepOutNoUpdate ||
|
|
548
|
+
this._command === Command.stepOutWithUpdate
|
|
549
|
+
)
|
|
550
|
+
this._checkVerify();
|
|
551
|
+
else this._doSeekStepOrVerify();
|
|
552
|
+
break;
|
|
553
|
+
case TimerState.done:
|
|
554
|
+
this._doneTimer();
|
|
555
|
+
break;
|
|
556
|
+
default:
|
|
557
|
+
throw new Error(`Unexpected timer state ${timerState}`);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* @param {Control} value
|
|
563
|
+
*/
|
|
564
|
+
_isSide(value) {
|
|
565
|
+
return !!(value & Control.side);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* @param {Control} value
|
|
570
|
+
*/
|
|
571
|
+
_isDoubleDensity(value) {
|
|
572
|
+
// Double density (MFM) is active low.
|
|
573
|
+
return !(value & Control.density);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* @param {Control} value
|
|
578
|
+
*/
|
|
579
|
+
_isReset(value) {
|
|
580
|
+
// Reset is active low.
|
|
581
|
+
return !(value & Control.reset);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* @param {Control|Number} val
|
|
586
|
+
*/
|
|
587
|
+
_writeControl(val) {
|
|
588
|
+
const isMotorOn = !!(this._statusRegister & Status.motorOn);
|
|
589
|
+
if (this._currentDrive && this._currentDrive.spinning) {
|
|
590
|
+
if (!isMotorOn) {
|
|
591
|
+
throw new Error(
|
|
592
|
+
`Unexpected motor control bit off when setting the control register to ${utils.hexbyte(val)}`,
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
this._currentDrive.stopSpinning();
|
|
596
|
+
}
|
|
597
|
+
if (val & Control.drive0 || val & Control.drive1) {
|
|
598
|
+
this._currentDrive = this._drives[val & Control.drive0 ? 0 : 1];
|
|
599
|
+
} else {
|
|
600
|
+
this._currentDrive = null;
|
|
601
|
+
}
|
|
602
|
+
if (this._currentDrive) {
|
|
603
|
+
if (isMotorOn) this._currentDrive.startSpinning();
|
|
604
|
+
this._currentDrive.selectSide(this._isSide(val));
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// Set up single or double density
|
|
608
|
+
for (const drive of this._drives) drive.set32usMode(this._isDoubleDensity(val));
|
|
609
|
+
|
|
610
|
+
this._controlRegister = val;
|
|
611
|
+
|
|
612
|
+
if (this._isReset(val)) {
|
|
613
|
+
// Go idle, etc
|
|
614
|
+
this._clearState();
|
|
615
|
+
if (this._currentDrive && isMotorOn) this._currentDrive.stopSpinning();
|
|
616
|
+
this._statusRegister = 0;
|
|
617
|
+
|
|
618
|
+
// EMU NOTE: on a real machine, the reset condition appears to hold the
|
|
619
|
+
// sector register at 1 but leave track / data alone (and permit changes
|
|
620
|
+
// to them).
|
|
621
|
+
this._sectorRegister = 1;
|
|
622
|
+
this._isIntRq = false;
|
|
623
|
+
this._isDrq = false;
|
|
624
|
+
this._updateNmi();
|
|
625
|
+
|
|
626
|
+
this._markDetector = 0n;
|
|
627
|
+
this._dataShifter = 0;
|
|
628
|
+
this._dataShiftCount = 0;
|
|
629
|
+
this._isIndexPulse = false;
|
|
630
|
+
this._lastMfmBit = false;
|
|
631
|
+
this._deliverData = 0;
|
|
632
|
+
this._deliverIsMarker = false;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
_makeSeekNoise(delta) {
|
|
637
|
+
if (this._currentDrive) this._currentDrive.notifySeekAmount(delta);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
_dispatchCommand() {
|
|
641
|
+
if (!this._currentDrive) throw new Error("Unexpectedly dispatching a command with no drive set");
|
|
642
|
+
if (this._isCommandWrite && this._currentDrive.writeProtect) {
|
|
643
|
+
this._statusRegister |= Status.writeProtected;
|
|
644
|
+
this._commandDone(true);
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
switch (this._command) {
|
|
649
|
+
case Command.restore:
|
|
650
|
+
this._trackRegister = 0xff;
|
|
651
|
+
this._logCommand(`track register now ${this._trackRegister}`);
|
|
652
|
+
this._dataRegister = 0;
|
|
653
|
+
// Falls through...
|
|
654
|
+
case Command.seek:
|
|
655
|
+
this._doSeekStepOrVerify();
|
|
656
|
+
this._makeSeekNoise(this._dataRegister - this._trackRegister);
|
|
657
|
+
break;
|
|
658
|
+
case Command.stepInNoUpdate:
|
|
659
|
+
this._doSeekStep(1, false);
|
|
660
|
+
this._makeSeekNoise(1);
|
|
661
|
+
break;
|
|
662
|
+
case Command.stepInWithUpdate:
|
|
663
|
+
this._doSeekStep(1, true);
|
|
664
|
+
this._makeSeekNoise(1);
|
|
665
|
+
break;
|
|
666
|
+
case Command.stepOutNoUpdate:
|
|
667
|
+
this._doSeekStep(-1, false);
|
|
668
|
+
this._makeSeekNoise(-1);
|
|
669
|
+
break;
|
|
670
|
+
case Command.stepOutWithUpdate:
|
|
671
|
+
this._doSeekStep(-1, true);
|
|
672
|
+
this._makeSeekNoise(-1);
|
|
673
|
+
break;
|
|
674
|
+
case Command.readSector:
|
|
675
|
+
case Command.readSectorMulti:
|
|
676
|
+
case Command.writeSector:
|
|
677
|
+
case Command.writeSectorMulti:
|
|
678
|
+
case Command.readAddress:
|
|
679
|
+
this._setState(State.searchId);
|
|
680
|
+
this._indexPulseCount = 0;
|
|
681
|
+
break;
|
|
682
|
+
case Command.readTrack:
|
|
683
|
+
this._setState(State.waitIndex);
|
|
684
|
+
this._indexPulseCount = 0;
|
|
685
|
+
break;
|
|
686
|
+
case Command.writeTrack:
|
|
687
|
+
this._setState(State.writeTrackSetup);
|
|
688
|
+
this._indexPulseCount = 0;
|
|
689
|
+
break;
|
|
690
|
+
default:
|
|
691
|
+
throw new Error(`Invalid command ${this._command} in dispatch`);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
_pulsesCallback(pulses, count) {
|
|
696
|
+
// This callback routine is also used for seek/settle timing which not a precise 64us basis.
|
|
697
|
+
if (!this._currentDrive || !this._currentDrive.spinning || !(this._statusRegister & Status.motorOn)) {
|
|
698
|
+
throw new Error("Something unfortunate happened in the 1770 pulses callback");
|
|
699
|
+
}
|
|
700
|
+
const wasIndexPulse = this._isIndexPulse;
|
|
701
|
+
this._isIndexPulse = this._currentDrive.indexPulse;
|
|
702
|
+
const isIndexPulsePositiveEdge = this._isIndexPulse && !wasIndexPulse;
|
|
703
|
+
const isMfm = count === 16;
|
|
704
|
+
|
|
705
|
+
if (this._isInterruptOnIndexPulse && isIndexPulsePositiveEdge) this._setIntRq(true);
|
|
706
|
+
|
|
707
|
+
// EMU Note: if the chip is idle after copmletion of a type I command, this index pulse and
|
|
708
|
+
// track 0 bits appear maintained. They disappear on spin-down.
|
|
709
|
+
this._updateTypeIStatusBits();
|
|
710
|
+
|
|
711
|
+
switch (this._state) {
|
|
712
|
+
case State.idle:
|
|
713
|
+
this._pulsesCallbackIdle();
|
|
714
|
+
break;
|
|
715
|
+
case State.timerWait:
|
|
716
|
+
break;
|
|
717
|
+
case State.spinUpWait:
|
|
718
|
+
this._pulsesCallbackSpinUpWait();
|
|
719
|
+
break;
|
|
720
|
+
case State.waitIndex:
|
|
721
|
+
if (isIndexPulsePositiveEdge) {
|
|
722
|
+
this._setState(State.inReadTrack);
|
|
723
|
+
// Need to include this byte (directly after the index pulse) in the read
|
|
724
|
+
// track data. Confirmed with a real 1772 & Gotek.
|
|
725
|
+
this._bitstreamReceived(pulses, count, false);
|
|
726
|
+
}
|
|
727
|
+
break;
|
|
728
|
+
case State.searchId:
|
|
729
|
+
case State.inId:
|
|
730
|
+
case State.searchData:
|
|
731
|
+
case State.inData:
|
|
732
|
+
case State.readTrack:
|
|
733
|
+
this._bitstreamReceived(pulses, count, isIndexPulsePositiveEdge);
|
|
734
|
+
if (this._indexPulseCount >= 6) {
|
|
735
|
+
this._statusRegister |= Status.recordNotFound;
|
|
736
|
+
this._commandDone(true);
|
|
737
|
+
}
|
|
738
|
+
break;
|
|
739
|
+
case State.writeSectorDelay:
|
|
740
|
+
this._pulsesCallbackSectorDelay(isMfm);
|
|
741
|
+
break;
|
|
742
|
+
case State.writeSectorLeadInFm:
|
|
743
|
+
this._writeByte(isMfm, 0x00, false);
|
|
744
|
+
if (++this._stateCount === 6) this._setState(State.writeSectorMarkerFm);
|
|
745
|
+
break;
|
|
746
|
+
case State.writeSectorLeadInMfm:
|
|
747
|
+
if (this._stateCount >= 11) this._writeByte(isMfm, 0x00, false);
|
|
748
|
+
if (++this._stateCount === 23) this._setState(State.writeSectorMarkerMfm);
|
|
749
|
+
break;
|
|
750
|
+
case State.writeSectorMarkerFm: {
|
|
751
|
+
const dataByte = this._isCommandDeleted
|
|
752
|
+
? IbmDiscFormat.deletedDataMarkDataPattern
|
|
753
|
+
: IbmDiscFormat.dataMarkDataPattern;
|
|
754
|
+
this._crc = IbmDiscFormat.crcAddByte(IbmDiscFormat.crcInit(false), dataByte);
|
|
755
|
+
this._writeByte(false, dataByte, true);
|
|
756
|
+
this._setState(State.writeSectorBody);
|
|
757
|
+
break;
|
|
758
|
+
}
|
|
759
|
+
case State.writeSectorMarkerMfm:
|
|
760
|
+
this._pulsesCallbackWriteSectorMarkerMfm();
|
|
761
|
+
break;
|
|
762
|
+
case State.writeSectorBody:
|
|
763
|
+
this._pulsesCallbackWriteSectorBody(isMfm);
|
|
764
|
+
break;
|
|
765
|
+
case State.checkMulti:
|
|
766
|
+
if (this._isCommandMulti) {
|
|
767
|
+
this._sectorRegister++;
|
|
768
|
+
this._indexPulseCount = 0;
|
|
769
|
+
this._setState(State.searchId);
|
|
770
|
+
} else {
|
|
771
|
+
this._commandDone(true);
|
|
772
|
+
}
|
|
773
|
+
break;
|
|
774
|
+
case State.writeTrackSetup:
|
|
775
|
+
this._pulsesCallbackWriteTrackSetup();
|
|
776
|
+
break;
|
|
777
|
+
case State.inWriteTrack:
|
|
778
|
+
this._pulsesCallbackInWriteTrack(isMfm, isIndexPulsePositiveEdge);
|
|
779
|
+
break;
|
|
780
|
+
case State.done:
|
|
781
|
+
this._commandDone(true);
|
|
782
|
+
break;
|
|
783
|
+
default:
|
|
784
|
+
throw new Error(`Unexpected state ${this._state}`);
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
if (isIndexPulsePositiveEdge) this._indexPulseCount++;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
_pulsesCallbackIdle() {
|
|
791
|
+
if (this._statusRegister & Status.busy) throw new Error("Unexpectedly busy in idle state");
|
|
792
|
+
// different sources disagree on 10 vs 9 index pulses for spin down.
|
|
793
|
+
if (this._indexPulseCount < 9) return;
|
|
794
|
+
this._logCommand("automatic motor off");
|
|
795
|
+
this._currentDrive.stopSpinning();
|
|
796
|
+
this._statusRegister &= ~Status.motorOn;
|
|
797
|
+
// In @scarybeasts's testing on a 1772 the polled type 1 status bits get cleared on spin down.
|
|
798
|
+
if (this._commandType === 1) {
|
|
799
|
+
this._statusRegister &= ~(Status.typeITrack0 | Status.typeIIndex);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
_pulsesCallbackSpinUpWait() {
|
|
804
|
+
if (this._indexPulseCount < 6) return;
|
|
805
|
+
if (this._commandType === 1) this._statusRegister |= Status.typeISpinUpDone;
|
|
806
|
+
if (this._isCommandSettle) {
|
|
807
|
+
const settleMs = this._is1772 ? 15 : 30;
|
|
808
|
+
this._startTimer(TimerState.settle, settleMs * 1000);
|
|
809
|
+
} else {
|
|
810
|
+
this._dispatchCommand();
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
_pulsesCallbackSectorDelay(isMfm) {
|
|
815
|
+
// Following the data sheet here for byte-for-byte behaviour.
|
|
816
|
+
if (this._stateCount === 0) {
|
|
817
|
+
this._indexPulseCount = 0;
|
|
818
|
+
} else if (this._stateCount === 1) {
|
|
819
|
+
this._setDrq(true);
|
|
820
|
+
} else if (this._stateCount === 10 && this._statusRegister & Status.typeIIorIIIDrq) {
|
|
821
|
+
this._statusRegister |= Status.typeIIorIIILostByte;
|
|
822
|
+
this._commandDone(true);
|
|
823
|
+
}
|
|
824
|
+
this._stateCount++;
|
|
825
|
+
if (this._stateCount === 12) this._setState(isMfm ? State.writeSectorLeadInMfm : State.writeSectorLeadInFm);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
_pulsesCallbackWriteSectorMarkerMfm() {
|
|
829
|
+
if (this._stateCount < 3) this._writeByte(true, 0xa1, true);
|
|
830
|
+
if (++this._stateCount === 4) {
|
|
831
|
+
const dataByte = this._isCommandDeleted
|
|
832
|
+
? IbmDiscFormat.deletedDataMarkDataPattern
|
|
833
|
+
: IbmDiscFormat.dataMarkDataPattern;
|
|
834
|
+
this._crc = IbmDiscFormat.crcAddByte(IbmDiscFormat.crcInit(true), dataByte);
|
|
835
|
+
this._writeByte(true, dataByte, false);
|
|
836
|
+
this._setState(State.writeSectorBody);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
_pulsesCallbackWriteSectorBody(isMfm) {
|
|
841
|
+
if (this._stateCount < this._onDiscLength) {
|
|
842
|
+
let dataByte = this._dataRegister;
|
|
843
|
+
if (this._statusRegister & Status.typeIIorIIIDrq) {
|
|
844
|
+
dataByte = 0;
|
|
845
|
+
this._statusRegister |= Status.typeIIorIIILostByte;
|
|
846
|
+
}
|
|
847
|
+
this._crc = IbmDiscFormat.crcAddByte(this._crc, dataByte);
|
|
848
|
+
this._writeByte(isMfm, dataByte, false);
|
|
849
|
+
if (this._stateCount !== this._onDiscLength - 1) this._setDrq(true);
|
|
850
|
+
} else if (this._stateCount < this._onDiscLength + 2) {
|
|
851
|
+
this._writeByte(isMfm, (this._crc >>> 8) & 0xff, false);
|
|
852
|
+
this._crc = (this._crc << 8) & 0xffff;
|
|
853
|
+
} else {
|
|
854
|
+
this._writeByte(isMfm, 0xff, false);
|
|
855
|
+
this._setState(State.checkMulti);
|
|
856
|
+
}
|
|
857
|
+
this._stateCount++;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
_pulsesCallbackWriteTrackSetup() {
|
|
861
|
+
if (this._stateCount === 0) {
|
|
862
|
+
this._indexPulseCount = 0;
|
|
863
|
+
this._setDrq(true);
|
|
864
|
+
} else if (this._stateCount === 3) {
|
|
865
|
+
if (this._statusRegister & Status.typeIIorIIIDrq) {
|
|
866
|
+
this._statusRegister |= Status.typeIIorIIILostByte;
|
|
867
|
+
this._commandDone(true);
|
|
868
|
+
} else {
|
|
869
|
+
this._setState(State.inWriteTrack);
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
this._stateCount++;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
_pulsesCallbackInWriteTrack(isMfm, isIndexPulsePositiveEdge) {
|
|
877
|
+
if (this._stateCount === 0 && !isIndexPulsePositiveEdge) return;
|
|
878
|
+
if (this._stateCount > 0 && isIndexPulsePositiveEdge) {
|
|
879
|
+
this._commandDone(true);
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
if (this._isWriteTrackCrcSecondByte) {
|
|
883
|
+
this._writeByte(isMfm, this._crc & 0xff, false);
|
|
884
|
+
this._isWriteTrackCrcSecondByte = false;
|
|
885
|
+
this._setDrq(true);
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
let dataByte = this._dataRegister;
|
|
889
|
+
if (this._statusRegister & Status.typeIIorIIIDrq) {
|
|
890
|
+
dataByte = 0;
|
|
891
|
+
this._statusRegister |= Status.typeIIorIIILostByte;
|
|
892
|
+
}
|
|
893
|
+
let isMarker = false;
|
|
894
|
+
let isPresetCrc = false;
|
|
895
|
+
switch (dataByte) {
|
|
896
|
+
// 0xF5 and 0xF6 are documented as "not allowed" in FM mode. They
|
|
897
|
+
// actually write 0xA1 / 0xC2 respectively, as per MFM, but it's not
|
|
898
|
+
// known whether any clock bits are omitted, or whether CRC is preset,
|
|
899
|
+
// so bailing for now rather than guessing.
|
|
900
|
+
case 0xf5:
|
|
901
|
+
if (!isMfm) throw new Error("Unhandled 0xf5 in FM");
|
|
902
|
+
isMarker = true;
|
|
903
|
+
isPresetCrc = true;
|
|
904
|
+
dataByte = 0xa1;
|
|
905
|
+
break;
|
|
906
|
+
case 0xf6:
|
|
907
|
+
if (!isMfm) throw new Error("Unhandled 0xf6 in FM");
|
|
908
|
+
isMarker = true;
|
|
909
|
+
dataByte = 0xc2;
|
|
910
|
+
break;
|
|
911
|
+
case 0xf8:
|
|
912
|
+
case 0xf9:
|
|
913
|
+
case 0xfa:
|
|
914
|
+
case 0xfb:
|
|
915
|
+
case 0xfe:
|
|
916
|
+
if (!isMfm) {
|
|
917
|
+
isMarker = true;
|
|
918
|
+
isPresetCrc = true;
|
|
919
|
+
}
|
|
920
|
+
break;
|
|
921
|
+
case 0xfc:
|
|
922
|
+
if (!isMfm) isMarker = true;
|
|
923
|
+
break;
|
|
924
|
+
default:
|
|
925
|
+
break;
|
|
926
|
+
}
|
|
927
|
+
if (isPresetCrc) {
|
|
928
|
+
this._crc = IbmDiscFormat.crcInit(isMfm);
|
|
929
|
+
}
|
|
930
|
+
if (dataByte === 0xf7) {
|
|
931
|
+
this._writeByte(isMfm, (this._crc >>> 8) & 0xff, false);
|
|
932
|
+
this._isWriteTrackCrcSecondByte = true;
|
|
933
|
+
} else {
|
|
934
|
+
this._writeByte(isMfm, dataByte, isMarker);
|
|
935
|
+
if (isMfm && isPresetCrc) {
|
|
936
|
+
// Nothing.
|
|
937
|
+
} else {
|
|
938
|
+
this._crc = IbmDiscFormat.crcAddByte(this._crc, dataByte);
|
|
939
|
+
}
|
|
940
|
+
this._setDrq(true);
|
|
941
|
+
}
|
|
942
|
+
this._stateCount++;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
_markDetectorTriggered() {
|
|
946
|
+
if (this._isDoubleDensity(this._controlRegister)) {
|
|
947
|
+
// EMU NOTE: unsure as to exactly when MFM sync bytes are spotted. Here we look for MFM 0x00 then MFM 0xa1 (sync).
|
|
948
|
+
// The documented sequence is 12 0x00, 3x 0xa1 (sync).
|
|
949
|
+
if ((this._markDetector & 0xffffffffn) === 0xaaaa4489n) {
|
|
950
|
+
this._deliverData = 0xa1;
|
|
951
|
+
return true;
|
|
952
|
+
}
|
|
953
|
+
// TODO: sync to c2 (5224).
|
|
954
|
+
// Note than an early, naive attempt had it triggered in in the middle of the sector data,
|
|
955
|
+
// so we'll need to study how it actually works in detail.
|
|
956
|
+
// Tag the byte after 3 sync bytes as a marker.
|
|
957
|
+
if ((this._markDetector & 0xffffffffffff0000n) === 0x4489448944890000n) {
|
|
958
|
+
this._deliverIsMarker = true;
|
|
959
|
+
}
|
|
960
|
+
} else {
|
|
961
|
+
// The FM mark detector appears to need 4 data bits' worth of zeros, with clock bits set to 1, to be able to trigger.
|
|
962
|
+
// Tried on @scarybeasts's real 1772-based machine.
|
|
963
|
+
if ((this._markDetector & 0x0000ffff00000000n) === 0x0000888800000000n) {
|
|
964
|
+
const { clocks, data, iffyPulses } = IbmDiscFormat._2usPulsesToFm(
|
|
965
|
+
Number(this._markDetector & 0xffffffffn),
|
|
966
|
+
);
|
|
967
|
+
if (!iffyPulses && clocks === 0xc7) {
|
|
968
|
+
// TODO: see http://info-coach.fr/atari/documents/_mydoc/WD1772-JLG.pdf
|
|
969
|
+
// This suggests that a wider ranges of byte values will function as markers. It may also differ FM vs. MFM.
|
|
970
|
+
if (data === 0xf8 || data === 0xfb || data === 0xfe) {
|
|
971
|
+
// Resync to marker.
|
|
972
|
+
this._deliverData = data;
|
|
973
|
+
this._deliverIsMarker = true;
|
|
974
|
+
return true;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
return false;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
/**
|
|
983
|
+
* @param {boolean} bit
|
|
984
|
+
*/
|
|
985
|
+
_bitReceived(bit) {
|
|
986
|
+
// Always run the mark detector. For a command like "read track", the 1770
|
|
987
|
+
// will re-sync in the middle of the command as appropriate.
|
|
988
|
+
this._markDetector = ((this._markDetector << 1n) & 0xffffffffffffffffn) | (bit ? 1n : 0n);
|
|
989
|
+
if (this._markDetectorTriggered()) {
|
|
990
|
+
this._dataShifter = 0;
|
|
991
|
+
this._dataShiftCount = 0;
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
this._dataShifter = ((this._dataShifter << 1) | (bit ? 1 : 0)) & 0xffffffff;
|
|
996
|
+
this._dataShiftCount++;
|
|
997
|
+
if (this._isDoubleDensity(this._controlRegister)) {
|
|
998
|
+
if (this._dataShiftCount === 16) {
|
|
999
|
+
this._deliverData = IbmDiscFormat._2usPulsesToMfm(this._dataShifter);
|
|
1000
|
+
this._dataShifter = 0;
|
|
1001
|
+
this._dataShiftCount = 0;
|
|
1002
|
+
}
|
|
1003
|
+
} else {
|
|
1004
|
+
if (this._dataShiftCount === 32) {
|
|
1005
|
+
const { data, iffyPulses } = IbmDiscFormat._2usPulsesToFm(this._dataShifter);
|
|
1006
|
+
// If we're reading MFM as FM, the pulses won't all fall on 4us boundaries. This is fuzzy bits;
|
|
1007
|
+
// we'll return a non-stable read.
|
|
1008
|
+
if (iffyPulses) {
|
|
1009
|
+
const { data: unstableBits } = IbmDiscFormat._2usPulsesToFm(
|
|
1010
|
+
this._currentDrive.getQuasiRandomPulses(),
|
|
1011
|
+
);
|
|
1012
|
+
this._deliverData = unstableBits;
|
|
1013
|
+
} else {
|
|
1014
|
+
this._deliverData = data;
|
|
1015
|
+
}
|
|
1016
|
+
this._dataShifter = 0;
|
|
1017
|
+
this._dataShiftCount = 0;
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
_bitstreamReceived(pulses, pulsesCount, isIndexPulsePositiveEdge) {
|
|
1023
|
+
pulses = (pulses << (32 - pulsesCount)) & 0xffffffff;
|
|
1024
|
+
for (let i = 0; i < pulsesCount; ++i) {
|
|
1025
|
+
this._bitReceived(!!(pulses & 0x80000000));
|
|
1026
|
+
pulses = (pulses << 1) & 0xffffffff;
|
|
1027
|
+
}
|
|
1028
|
+
this._byteReceived(isIndexPulsePositiveEdge);
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
/**
|
|
1032
|
+
* @param {boolean} isMfm
|
|
1033
|
+
* @param {Number} byte
|
|
1034
|
+
* @param {boolean} isMarker
|
|
1035
|
+
*/
|
|
1036
|
+
_writeByte(isMfm, byte, isMarker) {
|
|
1037
|
+
let pulses;
|
|
1038
|
+
if (isMfm) {
|
|
1039
|
+
if (isMarker) pulses = this._mfmMarkerFor(byte);
|
|
1040
|
+
else {
|
|
1041
|
+
const result = IbmDiscFormat.mfmTo2usPulses(this._lastMfmBit, byte);
|
|
1042
|
+
this._lastMfmBit = result.lastBit;
|
|
1043
|
+
pulses = result.pulses;
|
|
1044
|
+
}
|
|
1045
|
+
} else {
|
|
1046
|
+
const clocks = isMarker ? this._fmMarkerClocksFor(byte) : 0xff;
|
|
1047
|
+
pulses = IbmDiscFormat.fmTo2usPulses(clocks, byte);
|
|
1048
|
+
}
|
|
1049
|
+
this._currentDrive.writePulses(pulses);
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
_fmMarkerClocksFor(byte) {
|
|
1053
|
+
switch (byte) {
|
|
1054
|
+
case 0xfc:
|
|
1055
|
+
return 0xd7;
|
|
1056
|
+
case 0xf8:
|
|
1057
|
+
case 0xf9:
|
|
1058
|
+
case 0xfa:
|
|
1059
|
+
case 0xfb:
|
|
1060
|
+
case 0xfe:
|
|
1061
|
+
return IbmDiscFormat.markClockPattern;
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
_mfmMarkerFor(byte) {
|
|
1066
|
+
switch (byte) {
|
|
1067
|
+
case 0xa1:
|
|
1068
|
+
return IbmDiscFormat.mfmA1Sync;
|
|
1069
|
+
case 0xc2:
|
|
1070
|
+
return IbmDiscFormat.mfmC2Sync;
|
|
1071
|
+
default:
|
|
1072
|
+
throw new Error(`Bad marker byte ${utils.hexbyte(byte)}`);
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
_updateTypeIStatusBits() {
|
|
1077
|
+
if (this._commandType !== 1) return;
|
|
1078
|
+
this._statusRegister &= ~(Status.typeITrack0 | Status.typeIIndex);
|
|
1079
|
+
if (this._currentDrive.track === 0) this._statusRegister |= Status.typeITrack0;
|
|
1080
|
+
if (this._currentDrive.indexPulse) this._statusRegister |= Status.typeIIndex;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
_startTimer(timerState, waitUs) {
|
|
1084
|
+
if (!(this._statusRegister & Status.busy)) throw new Error("Should be busy");
|
|
1085
|
+
if (this._timerState !== TimerState.none) throw new Error("Timer started but still running");
|
|
1086
|
+
this._timerTask.cancel();
|
|
1087
|
+
this._timerState = timerState;
|
|
1088
|
+
this._setState(State.timerWait);
|
|
1089
|
+
this._timerTask.schedule(waitUs * 2);
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
_commandDone(doRaiseIntRq) {
|
|
1093
|
+
if (!(this._statusRegister & Status.busy)) throw new Error("Should be busy");
|
|
1094
|
+
this._doRaiseIntRq = doRaiseIntRq;
|
|
1095
|
+
this._startTimer(TimerState.done, 32);
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
_doneTimer() {
|
|
1099
|
+
this._statusRegister &= ~Status.busy;
|
|
1100
|
+
this._clearState();
|
|
1101
|
+
// Make sure the status are up to date.
|
|
1102
|
+
this._updateTypeIStatusBits();
|
|
1103
|
+
|
|
1104
|
+
// EMU NOTE: leave DRQ alone, if it is raised, leave it raised.
|
|
1105
|
+
if (this._doRaiseIntRq) this._setIntRq(true);
|
|
1106
|
+
|
|
1107
|
+
this._logCommand(`result status ${utils.hexbyte(this._statusRegister)}`);
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
_checkVerify() {
|
|
1111
|
+
if (this._isCommandVerify) {
|
|
1112
|
+
this._indexPulseCount = 0;
|
|
1113
|
+
this._setState(State.searchId);
|
|
1114
|
+
} else {
|
|
1115
|
+
this._commandDone(true);
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
_doSeekStep(stepDirection, doUpdateTr) {
|
|
1120
|
+
this._currentDrive.seekOneTrack(stepDirection);
|
|
1121
|
+
if (doUpdateTr) this._trackRegister += stepDirection;
|
|
1122
|
+
// TRK0 signal may have been raised or lowered.
|
|
1123
|
+
this._updateTypeIStatusBits();
|
|
1124
|
+
this._startTimer(TimerState.seek, this._commandStepRateMs * 1000);
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
_doSeekStepOrVerify() {
|
|
1128
|
+
if (this._trackRegister === this._dataRegister) {
|
|
1129
|
+
this._checkVerify();
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
const stepDirection = this._trackRegister > this._dataRegister ? -1 : 1;
|
|
1133
|
+
if (this._currentDrive.track === 0 && stepDirection === -1) {
|
|
1134
|
+
this._trackRegister = 0;
|
|
1135
|
+
this._checkVerify();
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1138
|
+
this._doSeekStep(stepDirection, true);
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
_byteReceived(isIndexPulsePositiveEdge) {
|
|
1142
|
+
const isMfm = this._isDoubleDensity(this._controlRegister);
|
|
1143
|
+
const isMarker = this._deliverIsMarker;
|
|
1144
|
+
const data = this._deliverData;
|
|
1145
|
+
this._deliverIsMarker = false;
|
|
1146
|
+
|
|
1147
|
+
switch (this._state) {
|
|
1148
|
+
case State.searchId:
|
|
1149
|
+
if (!isMarker || data !== IbmDiscFormat.idMarkDataPattern) break;
|
|
1150
|
+
this._setState(State.inId);
|
|
1151
|
+
this._crc = IbmDiscFormat.crcAddByte(IbmDiscFormat.crcInit(isMfm), IbmDiscFormat.idMarkDataPattern);
|
|
1152
|
+
break;
|
|
1153
|
+
case State.inId:
|
|
1154
|
+
this._byteReceivedInId(data);
|
|
1155
|
+
break;
|
|
1156
|
+
case State.searchData:
|
|
1157
|
+
this._byteReceivedSearchData(data, isMarker);
|
|
1158
|
+
break;
|
|
1159
|
+
case State.inData:
|
|
1160
|
+
this._byteReceivedInData(data);
|
|
1161
|
+
break;
|
|
1162
|
+
case State.inReadTrack:
|
|
1163
|
+
if (!isIndexPulsePositiveEdge) {
|
|
1164
|
+
this._sendDataToHost(data);
|
|
1165
|
+
} else {
|
|
1166
|
+
this._commandDone(true);
|
|
1167
|
+
}
|
|
1168
|
+
break;
|
|
1169
|
+
default:
|
|
1170
|
+
throw new Error(`Bad state ${this._state}`);
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
_byteReceivedInId(data) {
|
|
1175
|
+
const isReadAddress = this._command === Command.readAddress;
|
|
1176
|
+
switch (this._stateCount) {
|
|
1177
|
+
case 0:
|
|
1178
|
+
this._onDiscTrack = data;
|
|
1179
|
+
if (isReadAddress) {
|
|
1180
|
+
// The datasheet says "The Track Address of the ID field is written into the sector register"
|
|
1181
|
+
this._sectorRegister = data;
|
|
1182
|
+
}
|
|
1183
|
+
break;
|
|
1184
|
+
case 2:
|
|
1185
|
+
this._onDiscSector = data;
|
|
1186
|
+
break;
|
|
1187
|
+
case 3:
|
|
1188
|
+
// From http://info-coach.fr/atari/documents/_mydoc/WD1772-JLG.pdf, only the lower two bits affect anything.
|
|
1189
|
+
this._onDiscLength = 128 << (data & 0x03);
|
|
1190
|
+
break;
|
|
1191
|
+
}
|
|
1192
|
+
if (isReadAddress) {
|
|
1193
|
+
// Note that unlike the 8271, the CRC bytes are sent along too.
|
|
1194
|
+
this._sendDataToHost(data);
|
|
1195
|
+
}
|
|
1196
|
+
if (this._stateCount < 4) {
|
|
1197
|
+
this._crc = IbmDiscFormat.crcAddByte(this._crc, data);
|
|
1198
|
+
} else {
|
|
1199
|
+
this._onDiscCrc = ((this._onDiscCrc << 8) & 0xffff) | data;
|
|
1200
|
+
}
|
|
1201
|
+
if (++this._stateCount !== 6) return;
|
|
1202
|
+
|
|
1203
|
+
const isCrcError = this._crc !== this._onDiscCrc;
|
|
1204
|
+
|
|
1205
|
+
if (isReadAddress) {
|
|
1206
|
+
if (isCrcError) this._statusRegister |= Status.crcError;
|
|
1207
|
+
// Unlike the 8271, read address returns just a single record. It is also not synchronized
|
|
1208
|
+
// to the index pulse.
|
|
1209
|
+
// EMU TODO: it's likely that timing is generally off for most states,
|
|
1210
|
+
// i.e. the 1770 takes various numbers of internal clock cycles before it
|
|
1211
|
+
// delivers the CRC error, before it goes not busy, etc.
|
|
1212
|
+
// EMU NOTE: must not clear busy flag right away. The 1770 delivers the
|
|
1213
|
+
// last header byte DRQ separately from lowering the busy flag.
|
|
1214
|
+
this._setState(State.done);
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
// The data sheet specifies no CRC error unless the fields match so check those first.
|
|
1219
|
+
if (this._trackRegister !== this._onDiscTrack) {
|
|
1220
|
+
this._setState(State.searchId);
|
|
1221
|
+
return;
|
|
1222
|
+
}
|
|
1223
|
+
if (this._commandType === 2 && this._sectorRegister !== this._onDiscSector) {
|
|
1224
|
+
this._setState(State.searchId);
|
|
1225
|
+
return;
|
|
1226
|
+
}
|
|
1227
|
+
if (isCrcError) {
|
|
1228
|
+
this._statusRegister |= Status.crcError;
|
|
1229
|
+
// Unlike the 8271, the 1770 keeps going.
|
|
1230
|
+
this._setState(State.searchId);
|
|
1231
|
+
return;
|
|
1232
|
+
}
|
|
1233
|
+
if (this._commandType === 1) this._commandDone(true);
|
|
1234
|
+
else if (this._isCommandWrite) this._setState(State.writeSectorDelay);
|
|
1235
|
+
else this._setState(State.searchData);
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
_byteReceivedSearchData(data, isMarker) {
|
|
1239
|
+
this._stateCount++;
|
|
1240
|
+
const isMfm = this._isDoubleDensity(this._controlRegister);
|
|
1241
|
+
const multiplier = isMfm ? 2 : 1;
|
|
1242
|
+
// Like the 8271 the data mark is only recognized if 14 bytes have passed.
|
|
1243
|
+
// Unlike the 8271, it gives up after a while longer.
|
|
1244
|
+
if (this._stateCount < 14 * multiplier) return;
|
|
1245
|
+
if (this._stateCount > 31 * multiplier) {
|
|
1246
|
+
this._setState(State.searchId);
|
|
1247
|
+
return;
|
|
1248
|
+
}
|
|
1249
|
+
if (!isMarker) return;
|
|
1250
|
+
if (data === IbmDiscFormat.dataMarkDataPattern) {
|
|
1251
|
+
// Nothing...
|
|
1252
|
+
} else if (data === IbmDiscFormat.deletedDataMarkDataPattern) {
|
|
1253
|
+
// EMU NOTE: the datasheet is ambiguous on whether the deleted mark is
|
|
1254
|
+
// visible in the status register immediately, or at the end of a read.
|
|
1255
|
+
// The state machine diagram says "DAM in time" -> "Set Record Type in
|
|
1256
|
+
// Status Bit 5". But later on it says "At the end of the Read... is
|
|
1257
|
+
// recorded...".
|
|
1258
|
+
// Testing on @scarybeasts's 1772, the state machine diagram is correct: the bit is
|
|
1259
|
+
// visible in the status register immediately during the read.
|
|
1260
|
+
// EMU NOTE: on a multi-sector read, the deleted mark bit is set, and left
|
|
1261
|
+
// set, if _any_ deleted data sector was encountered. The datasheet would
|
|
1262
|
+
// seem to imply that only the most recent sector type is reflected in
|
|
1263
|
+
// the bit, but testing on @scarybeasts's 1772, the bit is set and left set even if
|
|
1264
|
+
// a non-deleted sector is encountered subsequently.
|
|
1265
|
+
this._statusRegister |= Status.typeIIorIIIDeletedMark;
|
|
1266
|
+
} else return;
|
|
1267
|
+
this._setState(State.inData);
|
|
1268
|
+
// CRC error is reset here. It's possible to hit a CRC error in a sector header and then find
|
|
1269
|
+
// an OK matching sector header.
|
|
1270
|
+
this._statusRegister &= ~Status.crcError;
|
|
1271
|
+
this._crc = IbmDiscFormat.crcAddByte(IbmDiscFormat.crcInit(isMfm), data);
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
_byteReceivedInData(data) {
|
|
1275
|
+
this._stateCount++;
|
|
1276
|
+
if (this._stateCount <= this._onDiscLength) {
|
|
1277
|
+
this._crc = IbmDiscFormat.crcAddByte(this._crc, data);
|
|
1278
|
+
this._sendDataToHost(data);
|
|
1279
|
+
return;
|
|
1280
|
+
} else if (this._stateCount <= this._onDiscLength + 2) {
|
|
1281
|
+
this._onDiscCrc = ((this._onDiscCrc << 8) & 0xffff) | data;
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1284
|
+
if (this._crc !== this._onDiscCrc) {
|
|
1285
|
+
this._statusRegister |= Status.crcError;
|
|
1286
|
+
// Sector data CRC error is terminal, even for a multi-sector read.
|
|
1287
|
+
this._commandDone(true);
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1290
|
+
this._setState(State.checkMulti);
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
_sendDataToHost(data) {
|
|
1294
|
+
if (this._commandType !== 2 && this._commandType !== 3) throw new Error("Bad command type");
|
|
1295
|
+
this._setDrq(true);
|
|
1296
|
+
this._dataRegister = data;
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
/// jsbeeb compatibility stuff TODO combine with the noise aware stuff?
|
|
1300
|
+
/**
|
|
1301
|
+
*
|
|
1302
|
+
* @param {Number} drive
|
|
1303
|
+
* @param {Disc} disc
|
|
1304
|
+
*/
|
|
1305
|
+
loadDisc(drive, disc) {
|
|
1306
|
+
this._drives[drive].setDisc(disc);
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
get motorOn() {
|
|
1310
|
+
return [this._drives[0] ? this._drives[0].spinning : false, this._drives[0] ? this._drives[1].spinning : false];
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
get drives() {
|
|
1314
|
+
return this._drives;
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
export class NoiseAwareWdFdc extends WdFdc {
|
|
1319
|
+
// TODO: consider deduplicating with the IntelFdc equivalent.
|
|
1320
|
+
constructor(cpu, ddNoise, scheduler, debugFlags) {
|
|
1321
|
+
super(cpu, scheduler, undefined, debugFlags);
|
|
1322
|
+
let nextSeekTime = 0;
|
|
1323
|
+
let numSpinning = 0;
|
|
1324
|
+
// Update the spin status shortly after the drive state changes to debounce it slightly.
|
|
1325
|
+
const updateSpinStatus = () => {
|
|
1326
|
+
if (numSpinning) ddNoise.spinUp();
|
|
1327
|
+
else ddNoise.spinDown();
|
|
1328
|
+
};
|
|
1329
|
+
for (const drive of this.drives) {
|
|
1330
|
+
drive.addEventListener("startSpinning", () => {
|
|
1331
|
+
numSpinning++;
|
|
1332
|
+
setTimeout(updateSpinStatus, 2);
|
|
1333
|
+
});
|
|
1334
|
+
drive.addEventListener("stopSpinning", () => {
|
|
1335
|
+
--numSpinning;
|
|
1336
|
+
setTimeout(updateSpinStatus, 2);
|
|
1337
|
+
});
|
|
1338
|
+
drive.addEventListener("step", (evt) => {
|
|
1339
|
+
const now = Date.now();
|
|
1340
|
+
if (now > nextSeekTime) nextSeekTime = now + ddNoise.seek(evt.stepAmount) * 1000;
|
|
1341
|
+
});
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
}
|