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/disc.js
ADDED
|
@@ -0,0 +1,997 @@
|
|
|
1
|
+
// Translated from beebjit by Chris Evans.
|
|
2
|
+
// https://github.com/scarybeasts/beebjit
|
|
3
|
+
|
|
4
|
+
import * as utils from "./utils.js";
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
* TODO: use in fingerprinting
|
|
8
|
+
class Crc32Builder {
|
|
9
|
+
constructor() {
|
|
10
|
+
this._crc = 0xffffffff;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
add(data) {
|
|
14
|
+
for (let i = 0; i < data.length; ++i) {
|
|
15
|
+
const byte = data[i];
|
|
16
|
+
this._crc ^= byte;
|
|
17
|
+
for (let j = 0; j < 8; ++j) {
|
|
18
|
+
const doEor = this._crc & 1;
|
|
19
|
+
this._crc = this._crc >>> 1;
|
|
20
|
+
if (doEor) this._crc ^= 0xedb88320;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
get crc() {
|
|
26
|
+
return ~this._crc;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
*/
|
|
30
|
+
class TrackBuilder {
|
|
31
|
+
/**
|
|
32
|
+
* @param {Track} track
|
|
33
|
+
*/
|
|
34
|
+
constructor(track) {
|
|
35
|
+
this._track = track;
|
|
36
|
+
this._track.length = IbmDiscFormat.bytesPerTrack;
|
|
37
|
+
this._index = 0;
|
|
38
|
+
this._pulsesIndex = 0;
|
|
39
|
+
this._lastMfmBit = 0;
|
|
40
|
+
this._crc = 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
get track() {
|
|
44
|
+
return this._track;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
setTrackLength() {
|
|
48
|
+
if (this._index > this._track.pulses2Us.length)
|
|
49
|
+
throw new Error(`Track buffer overflow in ${this._track.description}`);
|
|
50
|
+
if (this._index !== 0) this._track.length = this._index;
|
|
51
|
+
return this;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
resetCrc() {
|
|
55
|
+
this._crc = IbmDiscFormat.crcInit(false);
|
|
56
|
+
return this;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
appendFmDataAndClocks(data, clocks) {
|
|
60
|
+
if (this._index >= this._track.pulses2Us.length)
|
|
61
|
+
throw new Error(`Track buffer overflow in ${this._track.description}`);
|
|
62
|
+
this._track.pulses2Us[this._index++] = IbmDiscFormat.fmTo2usPulses(clocks, data);
|
|
63
|
+
this._crc = IbmDiscFormat.crcAddByte(this._crc, data);
|
|
64
|
+
return this;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
appendFmByte(data) {
|
|
68
|
+
this.appendFmDataAndClocks(data, 0xff);
|
|
69
|
+
return this;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
appendRepeatFmByte(data, count) {
|
|
73
|
+
for (let i = 0; i < count; ++i) this.appendFmByte(data);
|
|
74
|
+
return this;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
fillFmByte(data) {
|
|
78
|
+
if (this._index >= this._track.pulses2Us.length)
|
|
79
|
+
throw new Error(`Track buffer overflow in ${this._track.description}`);
|
|
80
|
+
// Fill to standard track size or buffer capacity, whichever is smaller
|
|
81
|
+
const fillCount = Math.min(IbmDiscFormat.bytesPerTrack, this._track.pulses2Us.length) - this._index;
|
|
82
|
+
this.appendRepeatFmByte(data, fillCount);
|
|
83
|
+
return this;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
appendRepeatFmByteWithClocks(data, clocks, count) {
|
|
87
|
+
for (let i = 0; i < count; ++i) this.appendFmDataAndClocks(data, clocks);
|
|
88
|
+
return this;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
appendFmChunk(bytes) {
|
|
92
|
+
for (const byte of bytes) this.appendFmByte(byte);
|
|
93
|
+
return this;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
appendCrc(isMfm) {
|
|
97
|
+
// TODO consider remembering isMfM if nothing else needs to know/
|
|
98
|
+
// could then break this into MFM and FM builder
|
|
99
|
+
const firstByte = (this._crc >>> 8) & 0xff;
|
|
100
|
+
const secondByte = this._crc & 0xff;
|
|
101
|
+
if (isMfm) {
|
|
102
|
+
this.appendMfmByte(firstByte);
|
|
103
|
+
this.appendMfmByte(secondByte);
|
|
104
|
+
} else {
|
|
105
|
+
this.appendFmByte(firstByte);
|
|
106
|
+
this.appendFmByte(secondByte);
|
|
107
|
+
}
|
|
108
|
+
return this;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
appendMfmPulses(pulses) {
|
|
112
|
+
if (this._index >= this._track.pulses2Us.length)
|
|
113
|
+
throw new Error(`Track buffer overflow in ${this._track.description}`);
|
|
114
|
+
const existingPulses = this._track.pulses2Us[this._index];
|
|
115
|
+
const mask = 0xffff << this._pulsesIndex;
|
|
116
|
+
this._pulsesIndex = (this._pulsesIndex + 16) & 31;
|
|
117
|
+
this._track.pulses2Us[this._index] = (existingPulses & mask) | (pulses << this._pulsesIndex);
|
|
118
|
+
if (this._pulsesIndex === 0) this._index++;
|
|
119
|
+
return this;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
appendMfmByte(data) {
|
|
123
|
+
const { lastBit, pulses } = IbmDiscFormat.mfmTo2usPulses(this._lastMfmBit, data);
|
|
124
|
+
this._lastMfmBit = lastBit;
|
|
125
|
+
this.appendMfmPulses(pulses);
|
|
126
|
+
this._crc = IbmDiscFormat.crcAddByte(this._crc, data);
|
|
127
|
+
return this;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
appendRepeatMfmByte(data, count) {
|
|
131
|
+
for (let i = 0; i < count; ++i) this.appendMfmByte(data);
|
|
132
|
+
return this;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
appendMfm3xA1Sync() {
|
|
136
|
+
for (let i = 0; i < 3; ++i) {
|
|
137
|
+
this.appendMfmPulses(IbmDiscFormat.mfmA1Sync);
|
|
138
|
+
this._crc = IbmDiscFormat.crcAddByte(this._crc, 0xa1);
|
|
139
|
+
}
|
|
140
|
+
return this;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
appendMfmChunk(bytes) {
|
|
144
|
+
for (const byte of bytes) this.appendMfmByte(byte);
|
|
145
|
+
return this;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
fillMfmByte(data) {
|
|
149
|
+
if (this._index >= this._track.pulses2Us.length)
|
|
150
|
+
throw new Error(`Track buffer overflow in ${this._track.description}`);
|
|
151
|
+
// Fill to standard track size or buffer capacity, whichever is smaller
|
|
152
|
+
const maxFill = Math.min(IbmDiscFormat.bytesPerTrack, this._track.pulses2Us.length);
|
|
153
|
+
while (this._index < maxFill) this.appendMfmByte(data);
|
|
154
|
+
return this;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* @param {number[]} pulseDeltas array of lengths between pulses
|
|
159
|
+
* @param {boolean} isMfm whether this is an MFM track
|
|
160
|
+
*/
|
|
161
|
+
buildFromPulses(pulseDeltas, isMfm) {
|
|
162
|
+
let hasWarned = false;
|
|
163
|
+
for (const pulse of pulseDeltas) {
|
|
164
|
+
if (!IbmDiscFormat.checkPulse(pulse, isMfm)) {
|
|
165
|
+
console.log(`Found a bad pulse for ${this.track.description}`);
|
|
166
|
+
}
|
|
167
|
+
if (!this.appendPulseDelta(pulse, isMfm) && !hasWarned) {
|
|
168
|
+
console.log(`Truncated disc data for ${this.track.description}, ignoring the rest`);
|
|
169
|
+
hasWarned = true;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
this.setTrackLength();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
appendPulseDelta(deltaUs, quantizeMfm) {
|
|
176
|
+
let num2UsUnits = quantizeMfm ? Math.round(deltaUs / 2) : 2 * Math.round(deltaUs / 4);
|
|
177
|
+
while (num2UsUnits--) {
|
|
178
|
+
if (this._index >= this._track.pulses2Us.length) return false;
|
|
179
|
+
if (num2UsUnits === 0) {
|
|
180
|
+
this._track.pulses2Us[this._index] |= 0x80000000 >>> this._pulsesIndex;
|
|
181
|
+
}
|
|
182
|
+
this._pulsesIndex++;
|
|
183
|
+
if (this._pulsesIndex === 32) {
|
|
184
|
+
this._pulsesIndex = 0;
|
|
185
|
+
this._index++;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
class RawDiscReader {
|
|
193
|
+
/**
|
|
194
|
+
* @param {Track} track
|
|
195
|
+
* @param {Number} bitOffset
|
|
196
|
+
*/
|
|
197
|
+
constructor(track, bitOffset) {
|
|
198
|
+
this._track = track;
|
|
199
|
+
this._pos = bitOffset;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
readPulses() {
|
|
203
|
+
let pulsesPos = this._pos >>> 5;
|
|
204
|
+
const bitPos = this._pos & 0x1f;
|
|
205
|
+
let sourcePulses = this._track.pulses2Us[pulsesPos];
|
|
206
|
+
let pulses = (sourcePulses << bitPos) & 0xfffffffff;
|
|
207
|
+
if (pulsesPos === this._track.length) {
|
|
208
|
+
pulsesPos = 0;
|
|
209
|
+
this._pos = bitPos;
|
|
210
|
+
} else {
|
|
211
|
+
pulsesPos++;
|
|
212
|
+
this._pos += 32;
|
|
213
|
+
}
|
|
214
|
+
if (bitPos > 0) {
|
|
215
|
+
sourcePulses = this._track.pulses2Us[pulsesPos];
|
|
216
|
+
pulses |= sourcePulses >>> (32 - bitPos);
|
|
217
|
+
}
|
|
218
|
+
return pulses;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
class MfmReader {
|
|
223
|
+
/**
|
|
224
|
+
* @param {RawDiscReader} rawReader
|
|
225
|
+
*/
|
|
226
|
+
constructor(rawReader) {
|
|
227
|
+
this._rawReader = rawReader;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
read(numBytes) {
|
|
231
|
+
const data = new Uint8Array(numBytes);
|
|
232
|
+
let pulses = 0;
|
|
233
|
+
for (let offset = 0; offset < numBytes; ++offset) {
|
|
234
|
+
if ((offset & 1) === 0) {
|
|
235
|
+
pulses = this._rawReader.readPulses();
|
|
236
|
+
} else {
|
|
237
|
+
pulses = (pulses << 16) & 0xffffffff;
|
|
238
|
+
}
|
|
239
|
+
data[offset] = IbmDiscFormat._2usPulsesToMfm(pulses >>> 16);
|
|
240
|
+
}
|
|
241
|
+
return { data, clocks: null, iffyPulses: false };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
get initialCrc() {
|
|
245
|
+
let crc = IbmDiscFormat.crcInit(false);
|
|
246
|
+
crc = IbmDiscFormat.crcAddByte(crc, 0xa1);
|
|
247
|
+
crc = IbmDiscFormat.crcAddByte(crc, 0xa1);
|
|
248
|
+
crc = IbmDiscFormat.crcAddByte(crc, 0xa1);
|
|
249
|
+
return crc;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
class FmReader {
|
|
254
|
+
/**
|
|
255
|
+
* @param {RawDiscReader} rawReader
|
|
256
|
+
*/
|
|
257
|
+
constructor(rawReader) {
|
|
258
|
+
this._rawReader = rawReader;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
read(numBytes) {
|
|
262
|
+
const data = new Uint8Array(numBytes);
|
|
263
|
+
const clocks = new Uint8Array(numBytes);
|
|
264
|
+
let iffyPulses = false;
|
|
265
|
+
for (let offset = 0; offset < numBytes; ++offset) {
|
|
266
|
+
const pulses = this._rawReader.readPulses();
|
|
267
|
+
const { data: dataByte, clock: clockByte, iffyPulses: iffy } = IbmDiscFormat._2usPulsesToFm(pulses);
|
|
268
|
+
data[offset] = dataByte;
|
|
269
|
+
clocks[offset] = clockByte;
|
|
270
|
+
iffyPulses |= iffy;
|
|
271
|
+
}
|
|
272
|
+
return { data, clocks, iffyPulses };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
get initialCrc() {
|
|
276
|
+
return IbmDiscFormat.crcInit(false);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
class Sector {
|
|
281
|
+
/**
|
|
282
|
+
* @param {Track} track
|
|
283
|
+
* @param {boolean} isMfm
|
|
284
|
+
* @param {Number} idPosBitOffset
|
|
285
|
+
*/
|
|
286
|
+
constructor(track, isMfm, idPosBitOffset) {
|
|
287
|
+
this.track = track;
|
|
288
|
+
this.isMfm = isMfm;
|
|
289
|
+
this.idPosBitOffset = idPosBitOffset;
|
|
290
|
+
this.dataPosBitOffset = null;
|
|
291
|
+
this.isDeleted = false;
|
|
292
|
+
this.sectorData = null;
|
|
293
|
+
this.hasDataCrcError = false;
|
|
294
|
+
this.byteLength = null;
|
|
295
|
+
|
|
296
|
+
const idReader = this._readerAt(this.idPosBitOffset);
|
|
297
|
+
const { data: headerData, iffyPulses } = idReader.read(6);
|
|
298
|
+
if (iffyPulses) {
|
|
299
|
+
console.log(`Iffy pulse in sector header ${this.description}`);
|
|
300
|
+
}
|
|
301
|
+
this.header = headerData;
|
|
302
|
+
let crc = idReader.initialCrc;
|
|
303
|
+
crc = IbmDiscFormat.crcAddByte(crc, IbmDiscFormat.idMarkDataPattern);
|
|
304
|
+
crc = IbmDiscFormat.crcAddBytes(crc, this.header.slice(0, 4));
|
|
305
|
+
const discCrc = (this.header[4] << 8) | this.header[5];
|
|
306
|
+
this.hasHeaderCrcError = crc !== discCrc;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
_readerAt(bitOffset) {
|
|
310
|
+
const rawReader = new RawDiscReader(this.track, bitOffset);
|
|
311
|
+
return this.isMfm ? new MfmReader(rawReader) : new FmReader(rawReader);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
get trackNumber() {
|
|
315
|
+
return this.header ? this.header[0] : undefined;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
get sectorNumber() {
|
|
319
|
+
return this.header ? this.header[2] : undefined;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
get description() {
|
|
323
|
+
return `${this.track.description} idpos ${this.idPosBitOffset} idtrack ${this.trackNumber} idsector ${this.sectorNumber} datapos ${this.dataPosBitOffset}`;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* @param {Sector|undefined} nextSector
|
|
328
|
+
*/
|
|
329
|
+
read(nextSector) {
|
|
330
|
+
const pulsesPerByte = this.isMfm ? 16 : 32; // todo put in reader
|
|
331
|
+
if (this.dataPosBitOffset === null) {
|
|
332
|
+
console.log(`"Sector header without data ${this.description}"`);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const dataMarker = this.isDeleted
|
|
337
|
+
? IbmDiscFormat.deletedDataMarkDataPattern
|
|
338
|
+
: IbmDiscFormat.dataMarkDataPattern;
|
|
339
|
+
const sectorStartByte = (this.dataPosBitOffset / pulsesPerByte) | 0;
|
|
340
|
+
const sectorEndByte =
|
|
341
|
+
(nextSector ? nextSector.idPosBitOffset / pulsesPerByte : (this.track.length * 32) / pulsesPerByte) | 0;
|
|
342
|
+
// Account for CRC and sync bytes.
|
|
343
|
+
let sectorSize = Sector.toSectorSize(sectorEndByte - sectorStartByte - 5);
|
|
344
|
+
|
|
345
|
+
this.hasDataCrcError = true;
|
|
346
|
+
let seenIffyData = false;
|
|
347
|
+
do {
|
|
348
|
+
const { crcOk, sectorData, iffyPulses } = this._tryLoadSectorData(dataMarker, sectorSize);
|
|
349
|
+
seenIffyData = iffyPulses;
|
|
350
|
+
if (crcOk) {
|
|
351
|
+
this.byteLength = sectorSize;
|
|
352
|
+
this.hasDataCrcError = false;
|
|
353
|
+
this.sectorData = sectorData;
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
sectorSize = sectorSize >>> 1;
|
|
357
|
+
} while (sectorSize >= 128);
|
|
358
|
+
if (seenIffyData) {
|
|
359
|
+
console.log(`"Iffy pulse in sector data ${this.description}"`);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
_tryLoadSectorData(dataMarker, sectorSize) {
|
|
364
|
+
const dataReader = this._readerAt(this.dataPosBitOffset);
|
|
365
|
+
let crc = IbmDiscFormat.crcAddByte(dataReader.initialCrc, dataMarker);
|
|
366
|
+
const { data: sectorData, iffyPulses } = dataReader.read(sectorSize + 2);
|
|
367
|
+
crc = IbmDiscFormat.crcAddBytes(crc, sectorData.slice(0, sectorSize));
|
|
368
|
+
const dataCrc = (sectorData[sectorSize] << 8) | sectorData[sectorSize + 1];
|
|
369
|
+
// The CRC bytes are used for error-checking and are not part of the actual sector data payload.
|
|
370
|
+
// Therefore, we exclude the last two bytes (CRC) from the returned `sectorData`.
|
|
371
|
+
return { crcOk: dataCrc === crc, sectorData: sectorData.slice(0, sectorSize), iffyPulses };
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
static toSectorSize(size) {
|
|
375
|
+
if (size < 256) return 128;
|
|
376
|
+
if (size < 512) return 256;
|
|
377
|
+
if (size < 1024) return 512;
|
|
378
|
+
if (size < 2048) return 1024;
|
|
379
|
+
return 2048;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
class Track {
|
|
384
|
+
constructor(upper, trackNum, initialByte) {
|
|
385
|
+
this.length = IbmDiscFormat.bytesPerTrack; // Default size, will be updated when track is populated
|
|
386
|
+
this.upper = upper;
|
|
387
|
+
this.trackNum = trackNum;
|
|
388
|
+
// Make room for any extra pulses that might come from non-standard discs.
|
|
389
|
+
this.pulses2Us = new Uint32Array(IbmDiscFormat.bytesPerTrack * 2);
|
|
390
|
+
this.pulses2Us.fill(initialByte | (initialByte << 8) | (initialByte << 16) | (initialByte << 24));
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
get description() {
|
|
394
|
+
return `Track ${this.trackNum} ${this.upper ? "upper" : "lower"}`;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Debug functionality to try and interpret the track.
|
|
399
|
+
* @returns {Sector[]}
|
|
400
|
+
*/
|
|
401
|
+
findSectors() {
|
|
402
|
+
const sectors = this.findSectorIds();
|
|
403
|
+
for (let sectorIndex = 0; sectorIndex !== sectors.length; ++sectorIndex) {
|
|
404
|
+
const nextSector = sectors[sectorIndex + 1]; // Will be unset for last
|
|
405
|
+
sectors[sectorIndex].read(nextSector);
|
|
406
|
+
}
|
|
407
|
+
return sectors;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* @returns {Sector[]}
|
|
412
|
+
*/
|
|
413
|
+
findSectorIds() {
|
|
414
|
+
const sectors = [];
|
|
415
|
+
// Pass 1: walk the track and find header and data markers.
|
|
416
|
+
const bitLength = this.length * 32;
|
|
417
|
+
let shiftRegister = 0;
|
|
418
|
+
let numShifts = 0;
|
|
419
|
+
let doMfmMarkerByte = false;
|
|
420
|
+
let isMfm = false;
|
|
421
|
+
let pulses = 0;
|
|
422
|
+
let markDetector = 0n;
|
|
423
|
+
let markDetectorPrev = 0n;
|
|
424
|
+
const all64b = 0xffffffffffffffffn;
|
|
425
|
+
const top32of64b = 0xffffffff00000000n;
|
|
426
|
+
const fmMarker = 0x8888888800000000n;
|
|
427
|
+
const mfmMarker = 0xaaaa448944894489n;
|
|
428
|
+
let dataByte = 0;
|
|
429
|
+
let sector = null;
|
|
430
|
+
for (let pulseIndex = 0; pulseIndex < bitLength; ++pulseIndex) {
|
|
431
|
+
if ((pulseIndex & 31) === 0) pulses = this.pulses2Us[pulseIndex >>> 5];
|
|
432
|
+
markDetectorPrev = (markDetectorPrev << 1n) & all64b;
|
|
433
|
+
markDetectorPrev |= markDetector >> 63n;
|
|
434
|
+
markDetector = (markDetector << 1n) & all64b;
|
|
435
|
+
shiftRegister = (shiftRegister << 1) & 0xffffffff;
|
|
436
|
+
numShifts++;
|
|
437
|
+
if (pulses & 0x80000000) {
|
|
438
|
+
markDetector |= 1n;
|
|
439
|
+
shiftRegister |= 1;
|
|
440
|
+
}
|
|
441
|
+
pulses = (pulses << 1) & 0xffffffff;
|
|
442
|
+
if ((markDetector & top32of64b) === fmMarker) {
|
|
443
|
+
const { clocks, data, iffyPulses } = IbmDiscFormat._2usPulsesToFm(Number(markDetector & 0xffffffffn));
|
|
444
|
+
if (iffyPulses || clocks !== IbmDiscFormat.markClockPattern) continue;
|
|
445
|
+
isMfm = false;
|
|
446
|
+
doMfmMarkerByte = false;
|
|
447
|
+
let num0s = 8;
|
|
448
|
+
for (let bits = markDetectorPrev; (bits & 0xfn) === 0x8n; bits >>= 4n) {
|
|
449
|
+
num0s++;
|
|
450
|
+
}
|
|
451
|
+
if (num0s <= 16) {
|
|
452
|
+
console.log(`Short zeros sync ${this.description}`);
|
|
453
|
+
}
|
|
454
|
+
dataByte = data;
|
|
455
|
+
} else if (markDetector === mfmMarker) {
|
|
456
|
+
// Next byte is MFM marker.
|
|
457
|
+
isMfm = true;
|
|
458
|
+
doMfmMarkerByte = true;
|
|
459
|
+
shiftRegister = 0;
|
|
460
|
+
numShifts = 0;
|
|
461
|
+
continue;
|
|
462
|
+
} else if (doMfmMarkerByte && numShifts === 16) {
|
|
463
|
+
dataByte = IbmDiscFormat._2usPulsesToMfm(shiftRegister);
|
|
464
|
+
doMfmMarkerByte = false;
|
|
465
|
+
} else {
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
switch (dataByte) {
|
|
469
|
+
case IbmDiscFormat.idMarkDataPattern: {
|
|
470
|
+
sector = new Sector(this, isMfm, pulseIndex + 1);
|
|
471
|
+
sectors.push(sector);
|
|
472
|
+
shiftRegister = 0;
|
|
473
|
+
numShifts = 0;
|
|
474
|
+
break;
|
|
475
|
+
}
|
|
476
|
+
case IbmDiscFormat.dataMarkDataPattern:
|
|
477
|
+
case IbmDiscFormat.deletedDataMarkDataPattern:
|
|
478
|
+
if (!sector || sector.dataPosBitOffset) {
|
|
479
|
+
console.log(
|
|
480
|
+
`Sector data without header ${this.description}; mark bitpos ${pulseIndex}; previous good sector ${sector ? sector.description : "none"}`,
|
|
481
|
+
);
|
|
482
|
+
} else {
|
|
483
|
+
sector.dataPosBitOffset = pulseIndex + 1;
|
|
484
|
+
if (dataByte === IbmDiscFormat.deletedDataMarkDataPattern) {
|
|
485
|
+
sector.isDeleted = true;
|
|
486
|
+
}
|
|
487
|
+
shiftRegister = 0;
|
|
488
|
+
numShifts = 0;
|
|
489
|
+
}
|
|
490
|
+
break;
|
|
491
|
+
default:
|
|
492
|
+
console.log(`Unknown marker byte ${utils.hexbyte(dataByte)} ${this.description}`);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
return sectors;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
class Side {
|
|
500
|
+
constructor(upper, initialByte) {
|
|
501
|
+
this.tracks = [];
|
|
502
|
+
for (let i = 0; i < IbmDiscFormat.tracksPerDisc; ++i) this.tracks[i] = new Track(upper, i, initialByte);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
export class DiscConfig {
|
|
507
|
+
constructor() {
|
|
508
|
+
// TODO is this even useful?
|
|
509
|
+
this.logProtection = false;
|
|
510
|
+
this.logIffyPulses = false;
|
|
511
|
+
this.expandTo80 = false;
|
|
512
|
+
this.isQuantizeFm = false;
|
|
513
|
+
this.isSkipOddTracks = false;
|
|
514
|
+
this.isSkipUpperSide = false;
|
|
515
|
+
this.rev = 0;
|
|
516
|
+
this.revSpec = "";
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
class SsdFormat {
|
|
521
|
+
static get sectorSize() {
|
|
522
|
+
return 256;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
static get sectorsPerTrack() {
|
|
526
|
+
return 10;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
static get tracksPerDisc() {
|
|
530
|
+
return 80;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Load a disc image in SSD (Single Sided Disc) or DSD (Double Sided Disc) format
|
|
536
|
+
* @param {Disc} disc - The disc object to load into
|
|
537
|
+
* @param {Uint8Array} data - The disc image data
|
|
538
|
+
* @param {boolean} isDsd - True if loading a double-sided disc
|
|
539
|
+
* @param {function(Uint8Array): void} onChange - Optional callback when disc content changes
|
|
540
|
+
*/
|
|
541
|
+
export function loadSsd(disc, data, isDsd, onChange) {
|
|
542
|
+
const blankSector = new Uint8Array(SsdFormat.sectorSize);
|
|
543
|
+
const numSides = isDsd ? 2 : 1;
|
|
544
|
+
if (data.length % SsdFormat.sectorSize !== 0) {
|
|
545
|
+
throw new Error("SSD file size is not a multiple of sector size");
|
|
546
|
+
}
|
|
547
|
+
const maxSize = SsdFormat.sectorSize * SsdFormat.sectorsPerTrack * SsdFormat.tracksPerDisc * numSides;
|
|
548
|
+
if (data.length > maxSize) {
|
|
549
|
+
throw new Error("SSD file is too large");
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
let offset = 0;
|
|
553
|
+
for (let track = 0; track < SsdFormat.tracksPerDisc; ++track) {
|
|
554
|
+
for (let side = 0; side < numSides; ++side) {
|
|
555
|
+
const trackBuilder = disc.buildTrack(side === 1, track);
|
|
556
|
+
// Sync pattern at start of track, as the index pulse starts, aka GAP 5.
|
|
557
|
+
trackBuilder
|
|
558
|
+
.appendRepeatFmByte(0xff, IbmDiscFormat.stdGap1FFs)
|
|
559
|
+
.appendRepeatFmByte(0x00, IbmDiscFormat.stdSync00s);
|
|
560
|
+
|
|
561
|
+
for (let sector = 0; sector < SsdFormat.sectorsPerTrack; ++sector) {
|
|
562
|
+
// Sector header, aka ID.
|
|
563
|
+
trackBuilder
|
|
564
|
+
.resetCrc()
|
|
565
|
+
.appendFmDataAndClocks(IbmDiscFormat.idMarkDataPattern, IbmDiscFormat.markClockPattern)
|
|
566
|
+
.appendFmByte(track)
|
|
567
|
+
.appendFmByte(0)
|
|
568
|
+
.appendFmByte(sector)
|
|
569
|
+
.appendFmByte(1)
|
|
570
|
+
.appendCrc(false);
|
|
571
|
+
|
|
572
|
+
// Sync pattern between sector header and sector data, aka GAP 2.
|
|
573
|
+
trackBuilder
|
|
574
|
+
.appendRepeatFmByte(0xff, IbmDiscFormat.stdGap2FFs)
|
|
575
|
+
.appendRepeatFmByte(0x00, IbmDiscFormat.stdSync00s);
|
|
576
|
+
|
|
577
|
+
// Sector data.
|
|
578
|
+
const sectorData =
|
|
579
|
+
offset < data.length ? data.subarray(offset, offset + SsdFormat.sectorSize) : blankSector;
|
|
580
|
+
offset += SsdFormat.sectorSize;
|
|
581
|
+
trackBuilder
|
|
582
|
+
.resetCrc()
|
|
583
|
+
.appendFmDataAndClocks(IbmDiscFormat.dataMarkDataPattern, IbmDiscFormat.markClockPattern)
|
|
584
|
+
.appendFmChunk(sectorData)
|
|
585
|
+
.appendCrc(false);
|
|
586
|
+
|
|
587
|
+
if (sector !== SsdFormat.sectorsPerTrack - 1) {
|
|
588
|
+
// Sync pattern between sectors, aka GAP 3.
|
|
589
|
+
trackBuilder
|
|
590
|
+
.appendRepeatFmByte(0xff, IbmDiscFormat.std10SectorGap3FFs)
|
|
591
|
+
.appendRepeatFmByte(0x00, IbmDiscFormat.stdSync00s);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
trackBuilder.fillFmByte(0xff);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
if (onChange) {
|
|
599
|
+
// TODO, maybe construct the disc directly with this stuff?
|
|
600
|
+
// TODO maybe change this entirely and make it lazy; and have the onChange "pull" the disc as the format it wants
|
|
601
|
+
// instead of doing this here. Most stuff doesn't care about changes and only needs the image on save.
|
|
602
|
+
// Create a dataCopy large enough for all the sectors and tracks.
|
|
603
|
+
const dataCopy = new Uint8Array(maxSize);
|
|
604
|
+
dataCopy.set(data);
|
|
605
|
+
disc.setWriteTrackCallback(
|
|
606
|
+
/** @param {Track} trackObj */
|
|
607
|
+
(side, trackNum, trackObj) => {
|
|
608
|
+
const trackOffset =
|
|
609
|
+
SsdFormat.sectorSize * SsdFormat.sectorsPerTrack * (trackNum * numSides + (side ? 1 : 0));
|
|
610
|
+
for (const sector of trackObj.findSectors()) {
|
|
611
|
+
const sectorOffset = sector.sectorNumber * SsdFormat.sectorSize;
|
|
612
|
+
for (let x = 0; x < SsdFormat.sectorSize; ++x)
|
|
613
|
+
dataCopy[trackOffset + sectorOffset + x] = sector.sectorData[x];
|
|
614
|
+
}
|
|
615
|
+
onChange(dataCopy);
|
|
616
|
+
},
|
|
617
|
+
);
|
|
618
|
+
}
|
|
619
|
+
return disc;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
class AdfFormat {
|
|
623
|
+
static get sectorSize() {
|
|
624
|
+
return 256;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
static get sectorsPerTrack() {
|
|
628
|
+
return 16;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
static get tracksPerDisc() {
|
|
632
|
+
return 80;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* @param {Disc} disc
|
|
638
|
+
* @param {Uint8Array} data
|
|
639
|
+
* @param {boolean} isDsd
|
|
640
|
+
*/
|
|
641
|
+
export function loadAdf(disc, data, isDsd) {
|
|
642
|
+
const blankSector = new Uint8Array(AdfFormat.sectorSize);
|
|
643
|
+
const numSides = isDsd ? 2 : 1;
|
|
644
|
+
if (data.length % AdfFormat.sectorSize !== 0) {
|
|
645
|
+
throw new Error("ADF file size is not a multiple of sector size");
|
|
646
|
+
}
|
|
647
|
+
const maxSize = AdfFormat.sectorSize * AdfFormat.sectorsPerTrack * AdfFormat.tracksPerDisc * numSides;
|
|
648
|
+
if (data.length > maxSize) {
|
|
649
|
+
throw new Error("ADF file is too large");
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
let offset = 0;
|
|
653
|
+
for (let track = 0; track < AdfFormat.tracksPerDisc; ++track) {
|
|
654
|
+
if (offset >= data.length) break;
|
|
655
|
+
|
|
656
|
+
for (let side = 0; side < numSides; ++side) {
|
|
657
|
+
// Using recommended values from the 177x datasheet.
|
|
658
|
+
const trackBuilder = disc.buildTrack(side === 1, track);
|
|
659
|
+
trackBuilder.appendRepeatMfmByte(0x4e, 60);
|
|
660
|
+
for (let sector = 0; sector < AdfFormat.sectorsPerTrack; ++sector) {
|
|
661
|
+
trackBuilder
|
|
662
|
+
.appendRepeatMfmByte(0x00, 12)
|
|
663
|
+
.resetCrc()
|
|
664
|
+
.appendMfm3xA1Sync()
|
|
665
|
+
.appendMfmByte(IbmDiscFormat.idMarkDataPattern)
|
|
666
|
+
.appendMfmByte(track)
|
|
667
|
+
.appendMfmByte(0)
|
|
668
|
+
.appendMfmByte(sector)
|
|
669
|
+
.appendMfmByte(1)
|
|
670
|
+
.appendCrc(true);
|
|
671
|
+
|
|
672
|
+
// Sync pattern between sector header and sector data, aka GAP 2.
|
|
673
|
+
trackBuilder.appendRepeatMfmByte(0x4e, 22).appendRepeatMfmByte(0x00, 12);
|
|
674
|
+
|
|
675
|
+
// Sector data.
|
|
676
|
+
const sectorData =
|
|
677
|
+
offset < data.length ? data.subarray(offset, offset + AdfFormat.sectorSize) : blankSector;
|
|
678
|
+
offset += AdfFormat.sectorSize;
|
|
679
|
+
trackBuilder
|
|
680
|
+
.resetCrc()
|
|
681
|
+
.appendMfm3xA1Sync()
|
|
682
|
+
.appendMfmByte(IbmDiscFormat.dataMarkDataPattern)
|
|
683
|
+
.appendMfmChunk(sectorData)
|
|
684
|
+
.appendCrc(true);
|
|
685
|
+
|
|
686
|
+
// Sync pattern between sectors, aka GAP 3.
|
|
687
|
+
trackBuilder.appendRepeatMfmByte(0x4e, 24);
|
|
688
|
+
}
|
|
689
|
+
trackBuilder.fillMfmByte(0x4e);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// TODO writeback
|
|
694
|
+
return disc;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* @returns {Uint8Array}
|
|
699
|
+
* @param {Disc} disc
|
|
700
|
+
*/
|
|
701
|
+
export function toSsdOrDsd(disc) {
|
|
702
|
+
const numSides = disc.isDoubleSided ? 2 : 1;
|
|
703
|
+
const result = new Uint8Array(
|
|
704
|
+
numSides * SsdFormat.tracksPerDisc * SsdFormat.sectorsPerTrack * SsdFormat.sectorSize,
|
|
705
|
+
);
|
|
706
|
+
let offset = 0;
|
|
707
|
+
for (let trackNum = 0; trackNum < disc.tracksUsed; ++trackNum) {
|
|
708
|
+
for (let side = 0; side < numSides; ++side) {
|
|
709
|
+
const trackObj = disc.getTrack(side === 1, trackNum);
|
|
710
|
+
for (const sector of trackObj.findSectors()) {
|
|
711
|
+
const sectorOffset = offset + sector.sectorNumber * SsdFormat.sectorSize;
|
|
712
|
+
if (sector.hasDataCrcError || sector.hasHeaderCrcError) {
|
|
713
|
+
console.log(`Skipping sector ${sector.description} with bad CRC`);
|
|
714
|
+
continue;
|
|
715
|
+
}
|
|
716
|
+
for (let x = 0; x < SsdFormat.sectorSize; ++x) result[sectorOffset + x] = sector.sectorData[x];
|
|
717
|
+
}
|
|
718
|
+
offset += SsdFormat.sectorsPerTrack * SsdFormat.sectorSize;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
return result.slice(0, offset);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
export class Disc {
|
|
725
|
+
/**
|
|
726
|
+
* @returns {Disc} a new blank disc
|
|
727
|
+
*/
|
|
728
|
+
static createBlank() {
|
|
729
|
+
return new Disc(true, new DiscConfig());
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* @param {boolean} isWriteable
|
|
734
|
+
* @param {DiscConfig} config
|
|
735
|
+
* @param {string} name
|
|
736
|
+
*/
|
|
737
|
+
constructor(isWriteable, config, name) {
|
|
738
|
+
this.config = config;
|
|
739
|
+
|
|
740
|
+
this.name = name;
|
|
741
|
+
this.isDirty = false;
|
|
742
|
+
this.dirtySide = -1;
|
|
743
|
+
this.dirtyTrack = -1;
|
|
744
|
+
this.tracksUsed = 0;
|
|
745
|
+
this.isDoubleSided = false;
|
|
746
|
+
|
|
747
|
+
this.writeTrackCallback = undefined;
|
|
748
|
+
this.isWriteable = isWriteable;
|
|
749
|
+
|
|
750
|
+
this.initSurface(0);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
setWriteTrackCallback(callback) {
|
|
754
|
+
this.writeTrackCallback = callback;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
get writeProtected() {
|
|
758
|
+
return !this.isWriteable;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* @param {boolean} isSideUpper
|
|
763
|
+
* @param {Number} trackNum
|
|
764
|
+
* @returns {Track} */
|
|
765
|
+
getTrack(isSideUpper, trackNum) {
|
|
766
|
+
return isSideUpper ? this.upperSide.tracks[trackNum] : this.lowerSide.tracks[trackNum];
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
buildTrack(isSideUpper, trackNum) {
|
|
770
|
+
this.setTrackUsed(isSideUpper, trackNum);
|
|
771
|
+
return new TrackBuilder(this.getTrack(isSideUpper, trackNum));
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
setTrackUsed(isSideUpper, trackNum) {
|
|
775
|
+
if (isSideUpper) this.isDoubleSided = true;
|
|
776
|
+
this.tracksUsed = Math.max(this.tracksUsed, trackNum + 1);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
initSurface(initialByte) {
|
|
780
|
+
this.lowerSide = new Side(false, initialByte);
|
|
781
|
+
this.upperSide = new Side(true, initialByte);
|
|
782
|
+
|
|
783
|
+
this.tracksUsed = 0;
|
|
784
|
+
this.isDoubleSided = false;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
readPulses(isSideUpper, track, position) {
|
|
788
|
+
return this.getTrack(isSideUpper, track).pulses2Us[position];
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/**
|
|
792
|
+
* @param {boolean} isSideUpper
|
|
793
|
+
* @param {Number} track
|
|
794
|
+
* @param {Number} position
|
|
795
|
+
* @param {Number} pulses
|
|
796
|
+
*/
|
|
797
|
+
writePulses(isSideUpper, track, position, pulses) {
|
|
798
|
+
const trackObj = this.getTrack(isSideUpper, track);
|
|
799
|
+
if (position >= trackObj.length)
|
|
800
|
+
throw new Error(`Attempt to write off end of track ${position} > ${track.length}`);
|
|
801
|
+
if (this.isDirty) {
|
|
802
|
+
if (isSideUpper !== this.dirtySide || track !== this.dirtyTrack)
|
|
803
|
+
throw new Error("Switched dirty track or side");
|
|
804
|
+
}
|
|
805
|
+
this.isDirty = true;
|
|
806
|
+
this.dirtySide = isSideUpper;
|
|
807
|
+
this.dirtyTrack = track;
|
|
808
|
+
trackObj.pulses2Us[position] = pulses;
|
|
809
|
+
// TODO a debug log flag for this
|
|
810
|
+
// console.log(`wrote to ${track}:${position * 32}`);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
flushWrites() {
|
|
814
|
+
if (!this.isDirty) {
|
|
815
|
+
if (this.dirtySide !== -1 || this.dirtyTrack !== -1) throw new Error("Bad state in disc dirty tracking");
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
const dirtySide = this.dirtySide;
|
|
820
|
+
const dirtyTrack = this.dirtyTrack;
|
|
821
|
+
this.isDirty = false;
|
|
822
|
+
this.dirtySide = -1;
|
|
823
|
+
this.dirtyTrack = -1;
|
|
824
|
+
if (!this.writeTrackCallback) return;
|
|
825
|
+
const trackObj = this.getTrack(dirtySide, dirtyTrack);
|
|
826
|
+
this.writeTrackCallback(dirtySide, dirtyTrack, trackObj);
|
|
827
|
+
this.setTrackUsed(dirtySide, dirtyTrack);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
logSummary() {
|
|
831
|
+
const maxTrack = this.tracksUsed;
|
|
832
|
+
const numSides = this.isDoubleSided ? 2 : 1;
|
|
833
|
+
for (let side = 0; side < numSides; ++side) {
|
|
834
|
+
for (let trackNum = 0; trackNum < maxTrack; ++trackNum) {
|
|
835
|
+
const track = this.getTrack(side === 1, trackNum);
|
|
836
|
+
const sectors = track.findSectors();
|
|
837
|
+
if (sectors.length) {
|
|
838
|
+
if (track.length >= IbmDiscFormat.bytesPerTrack * 1.015) {
|
|
839
|
+
console.log(`Long track ${track.description}, ${track.length} bytes`);
|
|
840
|
+
} else if (track.length <= IbmDiscFormat.bytesPerTrack * 0.985) {
|
|
841
|
+
console.log(`Short track ${track.description}, ${track.length} bytes`);
|
|
842
|
+
}
|
|
843
|
+
if (sectors[0].isMfm) {
|
|
844
|
+
if (sectors.length !== 16 && sectors.length !== 18) {
|
|
845
|
+
console.log(`Non-standard MFM sector count ${track.description} count ${sectors.length}`);
|
|
846
|
+
}
|
|
847
|
+
} else {
|
|
848
|
+
if (sectors.length !== 10) {
|
|
849
|
+
console.log(`Non-standard FM sector count ${track.description} count ${sectors.length}`);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
/// MG stuff
|
|
853
|
+
for (const sector of sectors) {
|
|
854
|
+
if (sector.hasHeaderCrcError) console.log(`${sector.description} has bad header crc`);
|
|
855
|
+
if (sector.trackNumber !== trackNum) console.log(`${sector.description} has bad track id`);
|
|
856
|
+
if (sector.hasDataCrcError) console.log(`${sector.description} has bad data crc`);
|
|
857
|
+
}
|
|
858
|
+
} else {
|
|
859
|
+
console.log(`"Unformatted track ${track.description}"`);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
// TODO add fingerprintings, catalog etcetc
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
export class IbmDiscFormat {
|
|
868
|
+
static get bytesPerTrack() {
|
|
869
|
+
return 3125;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
static get tracksPerDisc() {
|
|
873
|
+
return 84;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
static get markClockPattern() {
|
|
877
|
+
return 0xc7;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
static get idMarkDataPattern() {
|
|
881
|
+
return 0xfe;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
static get dataMarkDataPattern() {
|
|
885
|
+
return 0xfb;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
static get deletedDataMarkDataPattern() {
|
|
889
|
+
return 0xf8;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
static get mfmA1Sync() {
|
|
893
|
+
return 0x4489;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
static get mfmC2Sync() {
|
|
897
|
+
return 0x5224;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
static get stdSync00s() {
|
|
901
|
+
return 6;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
static get stdGap1FFs() {
|
|
905
|
+
return 16;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
static get stdGap2FFs() {
|
|
909
|
+
return 11;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
static get std10SectorGap3FFs() {
|
|
913
|
+
return 21;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
/**
|
|
917
|
+
* @param {boolean} isMfm
|
|
918
|
+
* @returns {Number} initial CRC for type
|
|
919
|
+
*/
|
|
920
|
+
static crcInit(isMfm) {
|
|
921
|
+
// MFM starts with 3x 0xA1 sync bytes added.
|
|
922
|
+
return isMfm ? 0xcdb4 : 0xffff;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
static crcAddByte(crc, byte) {
|
|
926
|
+
for (let i = 0; i < 8; ++i) {
|
|
927
|
+
const bit = byte & 0x80;
|
|
928
|
+
const bitTest = (crc & 0x8000) ^ (bit << 8);
|
|
929
|
+
crc = (crc << 1) & 0xffff;
|
|
930
|
+
if (bitTest) crc ^= 0x1021;
|
|
931
|
+
byte <<= 1;
|
|
932
|
+
}
|
|
933
|
+
return crc;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
static crcAddBytes(crc, bytes) {
|
|
937
|
+
for (const byte of bytes) crc = IbmDiscFormat.crcAddByte(crc, byte);
|
|
938
|
+
return crc;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
static fmTo2usPulses(clocks, data) {
|
|
942
|
+
let ret = 0;
|
|
943
|
+
for (let i = 0; i < 8; ++i) {
|
|
944
|
+
ret <<= 4;
|
|
945
|
+
if (clocks & 0x80) ret |= 0x04;
|
|
946
|
+
if (data & 0x80) ret |= 0x01;
|
|
947
|
+
clocks = (clocks << 1) & 0xff;
|
|
948
|
+
data = (data << 1) & 0xff;
|
|
949
|
+
}
|
|
950
|
+
return ret;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
static _2usPulsesToFm(pulses) {
|
|
954
|
+
let clocks = 0;
|
|
955
|
+
let data = 0;
|
|
956
|
+
let iffyPulses = false;
|
|
957
|
+
for (let i = 0; i < 8; ++i) {
|
|
958
|
+
clocks <<= 1;
|
|
959
|
+
data <<= 1;
|
|
960
|
+
if (pulses & 0x80000000) clocks |= 0x01;
|
|
961
|
+
if (pulses & 0x20000000) data |= 0x01;
|
|
962
|
+
// Any pulses off the 2us clock are suspicious FM data.
|
|
963
|
+
if (pulses & 0x50000000) iffyPulses = true;
|
|
964
|
+
pulses = (pulses << 4) & 0xffffffff;
|
|
965
|
+
}
|
|
966
|
+
return { clocks, data, iffyPulses };
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
static mfmTo2usPulses(lastBit, data) {
|
|
970
|
+
let pulses = 0;
|
|
971
|
+
for (let i = 0; i < 8; ++i) {
|
|
972
|
+
const bit = !!(data & 0x80);
|
|
973
|
+
pulses = (pulses << 2) & 0xffff;
|
|
974
|
+
data <<= 1;
|
|
975
|
+
if (bit) pulses |= 0x01;
|
|
976
|
+
else if (!lastBit) pulses |= 0x02;
|
|
977
|
+
lastBit = bit;
|
|
978
|
+
}
|
|
979
|
+
return { lastBit, pulses };
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
static _2usPulsesToMfm(pulses) {
|
|
983
|
+
let byte = 0;
|
|
984
|
+
for (let i = 0; i < 8; ++i) {
|
|
985
|
+
byte <<= 1;
|
|
986
|
+
if ((pulses & 0xc000) === 0x4000) byte |= 1;
|
|
987
|
+
pulses = (pulses << 2) & 0xffff;
|
|
988
|
+
}
|
|
989
|
+
return byte;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
static checkPulse(pulseUs, isMfm) {
|
|
993
|
+
if (pulseUs < 3.5 || pulseUs > 8.5) return false;
|
|
994
|
+
if (isMfm && pulseUs > 5.5 && pulseUs < 6.5) return true;
|
|
995
|
+
return !(pulseUs > 4.5 && pulseUs < 7.5);
|
|
996
|
+
}
|
|
997
|
+
}
|