From 2793c25c9ffde84ee385cd0e97b5f38e8858e0c8 Mon Sep 17 00:00:00 2001 From: Ian Flanigan Date: Sat, 17 Sep 2022 15:41:35 +0200 Subject: [PATCH] Split disk data out into its own record (#158) * Harmonize drive and disk type hierarchies Before, the `XXXDrive` and `XXXDisk` type hierarchies were similar, but not exactly the same. For example, `encoding` and `format` were missing on some `XXXDisk` types where they existed on the `XXXDrive` type. This change attempts to bring the hierarchies closer together. However, the biggest visible consequence is the introduction of the `FLOPPY_FORMATS` array and its associated `FloppyFormat` type. This replaces `NIBBLE_FORMATS` in most places. A couple of new type guards for disk formats and disks have been added as well. All tests pass, everything compiles with no errors, and both WOZ and nibble format disks load in the emulator. * Move disk data to a `disk` field in the drive Before, disk data was mixed in with state about the drive itself (like track, motor phase, etc.). This made it hard to know exactly what data was necessary for different image formats. Now, the disk data is in a `disk` field whose type depends on the drive type. This makes responisbility a bit easier. One oddity, though, is that the `Drive` has metadata _and_ the `Disk` has metadata. When a disk is in the drive, these should be `===`, but when there is no disk in the drive, obviously only the drive metadata is set. All tests pass, everything compiles, and both WOZ and nibble disks work in the emulator (both preact and classic). * Squash the `Drive` type hierarchy Before, the type of the drive depended on the type of the disk in the drive. Thus, `NibbleDrive` contained a `NibbleDisk` and a `WozDrive` contained a `WozDisk`. With the extraction of the disk data to a single field, this type hierarchy makes no sense. Instead, it suffices to check the type of the disk. This change removes the `NibbleDrive` and `WozDrive` types and type guards, checking the disk type where necessary. This change also introduces the `NoFloppyDisk` type to represent the lack of a disk. This allows the drive to have metadata, for one. All tests pass, everything compiles, and both WOZ and nibble disks work locally. * Use more destructuring assignment Now, more places use constructs like: ```TypeScript const { metadata, readOnly, track, head, phase, dirty } = drive; return { disk: getDiskState(drive.disk), metadata: {...metadata}, readOnly, track, head, phase, dirty, }; ``` * Remove the `Disk` object from the `Drive` object This change splits out the disk objects into a record parallel to the drive objects. The idea is that the `Drive` structure becomes a representation of the state of the drive that is separate from the disk image actually in the drive. This helps in an upcoming refactoring. This also changes the default empty disks to be writable. While odd, the write protect switch should be in the "off" position since there is no disk pressing on it. Finally, `insertDisk` now resets the head position to 0 since there is no way of preserving the head position across disks. (Even in the real world, the motor-off delay plus spindle spin-down would make it impossible to know the disk head position with any accuracy.) --- js/cards/cffa.ts | 5 +- js/cards/disk2.ts | 370 +++++++++++++++---------------- js/cards/smartport.ts | 10 +- js/components/DiskDragTarget.tsx | 4 +- js/components/DiskII.tsx | 4 +- js/components/FileModal.tsx | 4 +- js/components/debugger/Disks.tsx | 12 +- js/components/util/files.ts | 26 +-- js/formats/block.ts | 5 +- js/formats/create_disk.ts | 19 +- js/formats/format_utils.ts | 5 +- js/formats/types.ts | 73 +++++- js/formats/woz.ts | 2 +- js/ui/apple2.ts | 8 +- test/js/cards/disk2.spec.ts | 23 +- test/js/formats/2mg.spec.ts | 1 + test/js/formats/woz.spec.ts | 6 +- 17 files changed, 315 insertions(+), 262 deletions(-) diff --git a/js/cards/cffa.ts b/js/cards/cffa.ts index b3df6f6..b8c5262 100644 --- a/js/cards/cffa.ts +++ b/js/cards/cffa.ts @@ -389,6 +389,7 @@ export default class CFFA implements Card, MassStorage, Restorable< (block) => new Uint8Array(block) ), encoding: ENCODING_BLOCK, + format: disk.format, readOnly: disk.readOnly, metadata: { ...disk.metadata }, }; @@ -472,7 +473,7 @@ export default class CFFA implements Card, MassStorage, Restorable< volume, readOnly }; - const disk = createBlockDisk(options); + const disk = createBlockDisk(ext, options); return this.setBlockVolume(drive, disk); } @@ -485,7 +486,7 @@ export default class CFFA implements Card, MassStorage, Restorable< } const { blocks, readOnly } = blockDisk; const { name } = blockDisk.metadata; - let ext; + let ext: '2mg' | 'po'; let data: ArrayBuffer; if (this._metadata[drive]) { ext = '2mg'; diff --git a/js/cards/disk2.ts b/js/cards/disk2.ts index e4faa9e..e04cb30 100644 --- a/js/cards/disk2.ts +++ b/js/cards/disk2.ts @@ -2,7 +2,6 @@ import { base64_encode } from '../base64'; import type { byte, Card, - memory, nibble, ReadonlyUint8Array, } from '../types'; @@ -15,16 +14,21 @@ import { DRIVE_NUMBERS, DriveNumber, JSONDisk, - ENCODING_NIBBLE, PROCESS_BINARY, PROCESS_JSON_DISK, PROCESS_JSON, - ENCODING_BITSTREAM, MassStorage, MassStorageData, - DiskMetadata, SupportedSectors, FloppyDisk, + FloppyFormat, + WozDisk, + NibbleDisk, + isNibbleDisk, + isWozDisk, + NoFloppyDisk, + isNoFloppyDisk, + NO_DISK, } from '../formats/types'; import { @@ -212,66 +216,26 @@ export interface Callbacks { } /** Common information for Nibble and WOZ disks. */ -interface BaseDrive { - /** Current disk format. */ - format: NibbleFormat; - /** Current disk volume number. */ - volume: byte; +interface Drive { + /** Whether the drive write protect is on. */ + readOnly: boolean; /** Quarter track position of read/write head. */ track: byte; /** Position of the head on the track. */ head: byte; /** Current active coil in the head stepper motor. */ phase: Phase; - /** Whether the drive write protect is on. */ - readOnly: boolean; /** Whether the drive has been written to since it was loaded. */ dirty: boolean; - /** Metadata about the disk image */ - metadata: DiskMetadata; -} - -/** WOZ format track data from https://applesaucefdc.com/woz/reference2/. */ -interface WozDrive extends BaseDrive { - /** Woz encoding */ - encoding: typeof ENCODING_BITSTREAM; - /** Maps quarter tracks to data in rawTracks; `0xFF` = random garbage. */ - trackMap: byte[]; - /** Unique track bitstreams. The index is arbitrary; it is NOT the track number. */ - rawTracks: Uint8Array[]; -} - -/** Nibble format track data. */ -interface NibbleDrive extends BaseDrive { - /** Nibble encoding */ - encoding: typeof ENCODING_NIBBLE; - /** Nibble data. The index is the track number. */ - tracks: memory[]; -} - -type Drive = WozDrive | NibbleDrive; - -function isNibbleDrive(drive: Drive): drive is NibbleDrive { - return drive.encoding === ENCODING_NIBBLE; -} - -function isWozDrive(drive: Drive): drive is WozDrive { - return drive.encoding === ENCODING_BITSTREAM; } interface DriveState { - format: NibbleFormat; - encoding: typeof ENCODING_BITSTREAM | typeof ENCODING_NIBBLE; - volume: byte; - tracks: memory[]; + disk: FloppyDisk; + readOnly: boolean; track: byte; head: byte; phase: Phase; - readOnly: boolean; dirty: boolean; - trackMap: number[]; - rawTracks: Uint8Array[]; - metadata: DiskMetadata; } /** State of the controller for saving/restoring. */ @@ -282,73 +246,54 @@ interface State { controllerState: ControllerState; } -function getDriveState(drive: Drive): DriveState { - const result: DriveState = { - format: drive.format, - encoding: drive.encoding, - volume: drive.volume, - tracks: [], - track: drive.track, - head: drive.head, - phase: drive.phase, - readOnly: drive.readOnly, - dirty: drive.dirty, - trackMap: [], - rawTracks: [], - metadata: { ...drive.metadata }, - }; - - if (isNibbleDrive(drive)) { - for (let idx = 0; idx < drive.tracks.length; idx++) { - result.tracks.push(new Uint8Array(drive.tracks[idx])); - } +function getDiskState(disk: NoFloppyDisk): NoFloppyDisk; +function getDiskState(disk: NibbleDisk): NibbleDisk; +function getDiskState(disk: WozDisk): WozDisk; +function getDiskState(disk: FloppyDisk): FloppyDisk; +function getDiskState(disk: FloppyDisk): FloppyDisk { + if (isNoFloppyDisk(disk)) { + const { encoding, metadata, readOnly } = disk; + return { + encoding, + metadata: {...metadata}, + readOnly, + }; } - if (isWozDrive(drive)) { - result.trackMap = [...drive.trackMap]; - for (let idx = 0; idx < drive.rawTracks.length; idx++) { - result.rawTracks.push(new Uint8Array(drive.rawTracks[idx])); - } - } - return result; -} - -function setDriveState(state: DriveState) { - let result: Drive; - if (state.encoding === ENCODING_NIBBLE) { - result = { - format: state.format, - encoding: ENCODING_NIBBLE, - volume: state.volume, + if (isNibbleDisk(disk)) { + const { format, encoding, metadata, readOnly, volume, tracks } = disk; + const result: NibbleDisk = { + format, + encoding, + volume, tracks: [], - track: state.track, - head: state.head, - phase: state.phase, - readOnly: state.readOnly, - dirty: state.dirty, - metadata: { ...state.metadata }, + readOnly, + metadata: { ...metadata }, }; - for (let idx = 0; idx < state.tracks.length; idx++) { - result.tracks.push(new Uint8Array(state.tracks[idx])); - } - } else { - result = { - format: state.format, - encoding: ENCODING_BITSTREAM, - volume: state.volume, - track: state.track, - head: state.head, - phase: state.phase, - readOnly: state.readOnly, - dirty: state.dirty, - trackMap: [...state.trackMap], - rawTracks: [], - metadata: { ...state.metadata }, - }; - for (let idx = 0; idx < state.rawTracks.length; idx++) { - result.rawTracks.push(new Uint8Array(state.rawTracks[idx])); + for (let idx = 0; idx < tracks.length; idx++) { + result.tracks.push(new Uint8Array(tracks[idx])); } + return result; } - return result; + + if (isWozDisk(disk)) { + const { format, encoding, metadata, readOnly, trackMap, rawTracks } = disk; + const result: WozDisk = { + format, + encoding, + readOnly, + trackMap: [], + rawTracks: [], + metadata: { ...metadata }, + info: disk.info, + }; + result.trackMap = [...trackMap]; + for (let idx = 0; idx < rawTracks.length; idx++) { + result.rawTracks.push(new Uint8Array(rawTracks[idx])); + } + return result; + } + + throw new Error('Unknown drive state'); } /** @@ -358,27 +303,30 @@ export default class DiskII implements Card, MassStorage { private drives: Record = { 1: { // Drive 1 - format: 'dsk', - encoding: ENCODING_NIBBLE, - volume: 254, - tracks: [], track: 0, head: 0, phase: 0, readOnly: false, dirty: false, - metadata: { name: 'Disk 1' }, }, 2: { // Drive 2 - format: 'dsk', - encoding: ENCODING_NIBBLE, - volume: 254, - tracks: [], track: 0, head: 0, phase: 0, readOnly: false, dirty: false, + } + }; + + private disks: Record = { + 1: { + encoding: NO_DISK, + readOnly: false, + metadata: { name: 'Disk 1' }, + }, + 2: { + encoding: NO_DISK, + readOnly: false, metadata: { name: 'Disk 2' }, } }; @@ -393,8 +341,10 @@ export default class DiskII implements Card, MassStorage { private skip = 0; /** Drive off timeout id or null. */ private offTimeout: number | null = null; - /** Current drive object. */ - private cur: Drive; + /** Current drive object. Must only be set by `updateActiveDrive()`. */ + private curDrive: Drive; + /** Current disk object. Must only be set by `updateActiveDrive()`. */ + private curDisk: FloppyDisk; /** Nibbles read this on cycle */ private nibbleCount = 0; @@ -432,17 +382,23 @@ export default class DiskII implements Card, MassStorage { state: 2, }; - this.cur = this.drives[this.state.drive]; + this.updateActiveDrive(); this.initWorker(); } + /** Updates the active drive based on the controller state. */ + private updateActiveDrive() { + this.curDrive = this.drives[this.state.drive]; + this.curDisk = this.disks[this.state.drive]; + } + private debug(..._args: unknown[]) { // debug(..._args); } public head(): number { - return this.cur.head; + return this.curDrive.head; } /** @@ -487,18 +443,18 @@ export default class DiskII implements Card, MassStorage { let workCycles = (cycles - this.lastCycles) * 2; this.lastCycles = cycles; - if (!isWozDrive(this.cur)) { + if (!isWozDisk(this.curDisk)) { return; } const track = - this.cur.rawTracks[this.cur.trackMap[this.cur.track]] || [0]; + this.curDisk.rawTracks[this.curDisk.trackMap[this.curDrive.track]] || [0]; const state = this.state; while (workCycles-- > 0) { let pulse: number = 0; if (state.clock === 4) { - pulse = track[this.cur.head]; + pulse = track[this.curDrive.head]; if (!pulse) { // More than 2 zeros can not be read reliably. if (++this.zeros > 2) { @@ -531,7 +487,7 @@ export default class DiskII implements Card, MassStorage { break; case 0xA: // SR state.latch >>= 1; - if (this.cur.readOnly) { + if (this.curDrive.readOnly) { state.latch |= 0x80; } break; @@ -550,12 +506,12 @@ export default class DiskII implements Card, MassStorage { if (state.clock === 4) { if (state.on) { if (state.q7) { - track[this.cur.head] = state.state & 0x8 ? 0x01 : 0x00; + track[this.curDrive.head] = state.state & 0x8 ? 0x01 : 0x00; this.debug('Wrote', state.state & 0x8 ? 0x01 : 0x00); } - if (++this.cur.head >= track.length) { - this.cur.head = 0; + if (++this.curDrive.head >= track.length) { + this.curDrive.head = 0; } } } @@ -568,29 +524,29 @@ export default class DiskII implements Card, MassStorage { // Only called for non-WOZ disks private readWriteNext() { - if (!isNibbleDrive(this.cur)) { + if (!isNibbleDisk(this.curDisk)) { return; } const state = this.state; if (state.on && (this.skip || state.q7)) { - const track = this.cur.tracks[this.cur.track >> 2]; + const track = this.curDisk.tracks[this.curDrive.track >> 2]; if (track && track.length) { - if (this.cur.head >= track.length) { - this.cur.head = 0; + if (this.curDrive.head >= track.length) { + this.curDrive.head = 0; } if (state.q7) { - if (!this.cur.readOnly) { - track[this.cur.head] = state.bus; - if (!this.cur.dirty) { + if (!this.curDrive.readOnly) { + track[this.curDrive.head] = state.bus; + if (!this.curDrive.dirty) { this.updateDirty(state.drive, true); } } } else { - state.latch = track[this.cur.head]; + state.latch = track[this.curDrive.head]; } - ++this.cur.head; + ++this.curDrive.head; } } else { state.latch = 0; @@ -619,22 +575,24 @@ export default class DiskII implements Card, MassStorage { this.debug(`phase ${phase}${on ? ' on' : ' off'}`); if (on) { - this.cur.track += PHASE_DELTA[this.cur.phase][phase] * 2; - this.cur.phase = phase; + this.curDrive.track += PHASE_DELTA[this.curDrive.phase][phase] * 2; + this.curDrive.phase = phase; } // The emulator clamps the track to the valid track range available // in the image, but real Disk II drives can seek past track 34 by // at least a half track, usually a full track. Some 3rd party // drives can seek to track 39. - const maxTrack = isNibbleDrive(this.cur) - ? this.cur.tracks.length * 4 - 1 - : this.cur.trackMap.length - 1; - if (this.cur.track > maxTrack) { - this.cur.track = maxTrack; + const maxTrack = isNibbleDisk(this.curDisk) + ? this.curDisk.tracks.length * 4 - 1 + : (isWozDisk(this.curDisk) + ? this.curDisk.trackMap.length - 1 + : 0); + if (this.curDrive.track > maxTrack) { + this.curDrive.track = maxTrack; } - if (this.cur.track < 0x0) { - this.cur.track = 0x0; + if (this.curDrive.track < 0x0) { + this.curDrive.track = 0x0; } // debug( @@ -706,7 +664,7 @@ export default class DiskII implements Card, MassStorage { case LOC.DRIVE1: // 0x0a this.debug('Disk 1'); state.drive = 1; - this.cur = this.drives[state.drive]; + this.updateActiveDrive(); if (state.on) { this.callbacks.driveLight(2, false); this.callbacks.driveLight(1, true); @@ -715,7 +673,7 @@ export default class DiskII implements Card, MassStorage { case LOC.DRIVE2: // 0x0b this.debug('Disk 2'); state.drive = 2; - this.cur = this.drives[state.drive]; + this.updateActiveDrive(); if (state.on) { this.callbacks.driveLight(1, false); this.callbacks.driveLight(2, true); @@ -727,7 +685,7 @@ export default class DiskII implements Card, MassStorage { if (state.q7) { this.debug('clearing _q6/SHIFT'); } - if (isNibbleDrive(this.cur)) { + if (isNibbleDisk(this.curDisk)) { this.readWriteNext(); } break; @@ -737,9 +695,9 @@ export default class DiskII implements Card, MassStorage { if (state.q7) { this.debug('setting _q6/LOAD'); } - if (isNibbleDrive(this.cur)) { + if (isNibbleDisk(this.curDisk)) { if (readMode && !state.q7) { - if (this.cur.readOnly) { + if (this.curDrive.readOnly) { state.latch = 0xff; this.debug('Setting readOnly'); } else { @@ -812,60 +770,85 @@ export default class DiskII implements Card, MassStorage { state.q7 = false; state.on = false; state.drive = 1; - this.cur = this.drives[state.drive]; } + this.updateActiveDrive(); } tick() { this.moveHead(); } + private getDriveState(drive: DriveNumber): DriveState { + const curDrive = this.drives[drive]; + const curDisk = this.disks[drive]; + const { readOnly, track, head, phase, dirty } = curDrive; + return { + disk: getDiskState(curDisk), + readOnly, + track, + head, + phase, + dirty, + }; + } + getState(): State { const result = { drives: [] as DriveState[], skip: this.skip, controllerState: { ...this.state }, }; - result.drives[1] = getDriveState(this.drives[1]); - result.drives[2] = getDriveState(this.drives[2]); + result.drives[1] = this.getDriveState(1); + result.drives[2] = this.getDriveState(2); return result; } + private setDriveState(drive: DriveNumber, state: DriveState) { + const { track, head, phase, readOnly, dirty } = state; + this.drives[drive] = { + track, + head, + phase, + readOnly, + dirty, + }; + this.disks[drive] = getDiskState(state.disk); + } + + setState(state: State) { this.skip = state.skip; this.state = { ...state.controllerState }; for (const d of DRIVE_NUMBERS) { - this.drives[d] = setDriveState(state.drives[d]); - const { name, side } = state.drives[d].metadata; + this.setDriveState(d, state.drives[d]); + const { name, side } = state.drives[d].disk.metadata; const { dirty } = state.drives[d]; this.callbacks.label(d, name, side); this.callbacks.driveLight(d, this.state.on); this.callbacks.dirty(d, dirty); } - this.cur = this.drives[this.state.drive]; + this.updateActiveDrive(); } getMetadata(driveNo: DriveNumber) { - const drive = this.drives[driveNo]; + const { track, head, phase, readOnly, dirty } = this.drives[driveNo]; return { - format: drive.format, - volume: drive.volume, - track: drive.track, - head: drive.head, - phase: drive.phase, - readOnly: drive.readOnly, - dirty: drive.dirty + track, + head, + phase, + readOnly, + dirty, }; } // TODO(flan): Does not work on WOZ disks - rwts(disk: DriveNumber, track: byte, sector: byte) { - const cur = this.drives[disk]; - if (!isNibbleDrive(cur)) { + rwts(drive: DriveNumber, track: byte, sector: byte) { + const curDisk = this.disks[drive]; + if (!isNibbleDisk(curDisk)) { throw new Error('Can\'t read WOZ disks'); } - return readSector(cur, track, sector); + return readSector(curDisk, track, sector); } /** Sets the data for `drive` from `disk`, which is expected to be JSON. */ @@ -892,11 +875,11 @@ export default class DiskII implements Card, MassStorage { } getJSON(drive: DriveNumber, pretty: boolean = false) { - const cur = this.drives[drive]; - if (!isNibbleDrive(cur)) { + const curDisk = this.disks[drive]; + if (!isNibbleDisk(curDisk)) { throw new Error('Can\'t save WOZ disks to JSON'); } - return jsonEncode(cur, pretty); + return jsonEncode(curDisk, pretty); } setJSON(drive: DriveNumber, json: string) { @@ -916,7 +899,7 @@ export default class DiskII implements Card, MassStorage { return true; } - setBinary(drive: DriveNumber, name: string, fmt: NibbleFormat, rawData: ArrayBuffer) { + setBinary(drive: DriveNumber, name: string, fmt: FloppyFormat, rawData: ArrayBuffer) { const readOnly = false; const volume = 254; const options = { @@ -975,27 +958,28 @@ export default class DiskII implements Card, MassStorage { } private insertDisk(drive: DriveNumber, disk: FloppyDisk) { - const cur = this.drives[drive]; - Object.assign(cur, disk); - const { name, side } = cur.metadata; + this.disks[drive] = disk; + this.drives[drive].head = 0; + this.updateActiveDrive(); + const { name, side } = disk.metadata; this.updateDirty(drive, true); this.callbacks.label(drive, name, side); } // TODO(flan): Does not work with WOZ or D13 disks - getBinary(drive: DriveNumber, ext?: NibbleFormat): MassStorageData | null { - const cur = this.drives[drive]; - if (!isNibbleDrive(cur)) { + getBinary(drive: DriveNumber, ext?: Exclude): MassStorageData | null { + const curDisk = this.disks[drive]; + if (!isNibbleDisk(curDisk)) { return null; } - const { format, readOnly, tracks, volume } = cur; - const { name } = cur.metadata; + const { format, readOnly, tracks, volume } = curDisk; + const { name } = curDisk.metadata; const len = format === 'nib' ? tracks.reduce((acc, track) => acc + track.length, 0) : this.sectors * tracks.length * 256; const data = new Uint8Array(len); - ext = ext ?? format; + const extension = ext ?? format; let idx = 0; for (let t = 0; t < tracks.length; t++) { if (ext === 'nib') { @@ -1003,7 +987,7 @@ export default class DiskII implements Card, MassStorage { idx += tracks[t].length; } else { for (let s = 0; s < 0x10; s++) { - const sector = readSector({ ...cur, format: ext }, t, s); + const sector = readSector({ ...curDisk, format: extension }, t, s); data.set(sector, idx); idx += sector.length; } @@ -1011,7 +995,7 @@ export default class DiskII implements Card, MassStorage { } return { - ext, + ext: extension, metadata: { name }, data: data.buffer, readOnly, @@ -1021,18 +1005,18 @@ export default class DiskII implements Card, MassStorage { // TODO(flan): Does not work with WOZ or D13 disks getBase64(drive: DriveNumber) { - const cur = this.drives[drive]; - if (!isNibbleDrive(cur)) { + const curDisk = this.disks[drive]; + if (!isNibbleDisk(curDisk)) { return null; } const data: string[][] | string[] = []; - for (let t = 0; t < cur.tracks.length; t++) { - if (cur.format === 'nib') { - data[t] = base64_encode(cur.tracks[t]); + for (let t = 0; t < curDisk.tracks.length; t++) { + if (isNibbleDisk(curDisk)) { + data[t] = base64_encode(curDisk.tracks[t]); } else { const track: string[] = []; for (let s = 0; s < 0x10; s++) { - track[s] = base64_encode(readSector(cur, t, s)); + track[s] = base64_encode(readSector(curDisk, t, s)); } data[t] = track; } diff --git a/js/cards/smartport.ts b/js/cards/smartport.ts index 5dccb1c..1137e14 100644 --- a/js/cards/smartport.ts +++ b/js/cards/smartport.ts @@ -1,7 +1,7 @@ import { debug, toHex } from '../util'; import { rom as smartPortRom } from '../roms/cards/smartport'; import { Card, Restorable, byte, word, rom } from '../types'; -import { MassStorage, BlockDisk, ENCODING_BLOCK, BlockFormat, MassStorageData } from '../formats/types'; +import { MassStorage, BlockDisk, ENCODING_BLOCK, BlockFormat, MassStorageData, DiskFormat } from '../formats/types'; import CPU6502, { CpuState, flags } from '../cpu6502'; import { create2MGFromBlockDisk, HeaderData, read2MGHeader } from '../formats/2mg'; import createBlockDisk from '../formats/block'; @@ -129,7 +129,7 @@ export default class SmartPort implements Card, MassStorage, Restor private disks: BlockDisk[] = []; private busy: boolean[] = []; private busyTimeout: ReturnType[] = []; - private ext: string[] = []; + private ext: DiskFormat[] = []; private metadata: Array = []; constructor( @@ -522,6 +522,7 @@ export default class SmartPort implements Card, MassStorage, Restor (block) => new Uint8Array(block) ), encoding: ENCODING_BLOCK, + format: disk.format, readOnly: disk.readOnly, metadata: { ...disk.metadata }, }; @@ -539,6 +540,7 @@ export default class SmartPort implements Card, MassStorage, Restor (block) => new Uint8Array(block) ), encoding: ENCODING_BLOCK, + format: disk.format, readOnly: disk.readOnly, metadata: { ...disk.metadata }, }; @@ -547,7 +549,7 @@ export default class SmartPort implements Card, MassStorage, Restor ); } - setBinary(drive: DriveNumber, name: string, fmt: string, rawData: ArrayBuffer) { + setBinary(drive: DriveNumber, name: string, fmt: BlockFormat, rawData: ArrayBuffer) { let volume = 254; let readOnly = false; if (fmt === '2mg') { @@ -568,7 +570,7 @@ export default class SmartPort implements Card, MassStorage, Restor }; this.ext[drive] = fmt; - this.disks[drive] = createBlockDisk(options); + this.disks[drive] = createBlockDisk(fmt, options); this.callbacks?.label(drive, name); return true; diff --git a/js/components/DiskDragTarget.tsx b/js/components/DiskDragTarget.tsx index 2d2b805..6494795 100644 --- a/js/components/DiskDragTarget.tsx +++ b/js/components/DiskDragTarget.tsx @@ -1,4 +1,4 @@ -import { BLOCK_FORMATS, DISK_FORMATS, DriveNumber, MassStorage, NIBBLE_FORMATS } from 'js/formats/types'; +import { BLOCK_FORMATS, DISK_FORMATS, DriveNumber, FLOPPY_FORMATS, MassStorage } from 'js/formats/types'; import { h, JSX, RefObject } from 'preact'; import { useEffect, useRef } from 'preact/hooks'; import { loadLocalFile } from './util/files'; @@ -7,7 +7,7 @@ import { spawn } from './util/promises'; export interface DiskDragTargetProps extends JSX.HTMLAttributes { storage: MassStorage | undefined; drive?: DriveNumber; - formats: typeof NIBBLE_FORMATS + formats: typeof FLOPPY_FORMATS | typeof BLOCK_FORMATS | typeof DISK_FORMATS; dropRef?: RefObject; diff --git a/js/components/DiskII.tsx b/js/components/DiskII.tsx index f01ae17..400aa83 100644 --- a/js/components/DiskII.tsx +++ b/js/components/DiskII.tsx @@ -7,7 +7,7 @@ import { FileModal } from './FileModal'; import styles from './css/DiskII.module.css'; import { DiskDragTarget } from './DiskDragTarget'; -import { NIBBLE_FORMATS } from 'js/formats/types'; +import { FLOPPY_FORMATS } from 'js/formats/types'; import { DownloadModal } from './DownloadModal'; /** @@ -66,7 +66,7 @@ export const DiskII = ({ disk2, number, on, name, side }: DiskIIProps) => { className={styles.disk} storage={disk2} drive={number} - formats={NIBBLE_FORMATS} + formats={FLOPPY_FORMATS} onError={setError} > diff --git a/js/components/FileModal.tsx b/js/components/FileModal.tsx index 3bb0b91..3a636d7 100644 --- a/js/components/FileModal.tsx +++ b/js/components/FileModal.tsx @@ -1,6 +1,6 @@ import { h, Fragment, JSX } from 'preact'; import { useCallback, useEffect, useState } from 'preact/hooks'; -import { DiskDescriptor, DriveNumber, NibbleFormat, NIBBLE_FORMATS } from '../formats/types'; +import { DiskDescriptor, DriveNumber, FLOPPY_FORMATS, NibbleFormat } from '../formats/types'; import { Modal, ModalContent, ModalFooter } from './Modal'; import { loadLocalNibbleFile, loadJSON, getHashParts, setHashParts } from './util/files'; import DiskII from '../cards/disk2'; @@ -15,7 +15,7 @@ import styles from './css/FileModal.module.css'; const DISK_TYPES: FilePickerAcceptType[] = [ { description: 'Disk Images', - accept: { 'application/octet-stream': NIBBLE_FORMATS.map(x => '.' + x) }, + accept: { 'application/octet-stream': FLOPPY_FORMATS.map(x => '.' + x) }, } ]; diff --git a/js/components/debugger/Disks.tsx b/js/components/debugger/Disks.tsx index 11d5e84..b2f535c 100644 --- a/js/components/debugger/Disks.tsx +++ b/js/components/debugger/Disks.tsx @@ -2,7 +2,7 @@ import { h, Fragment } from 'preact'; import { useMemo } from 'preact/hooks'; import cs from 'classnames'; import { Apple2 as Apple2Impl } from 'js/apple2'; -import { BlockDisk, DiskFormat, DriveNumber, MassStorage, NibbleDisk } from 'js/formats/types'; +import { BlockDisk, DiskFormat, DriveNumber, FloppyDisk, isBlockDiskFormat, isNibbleDisk, MassStorage } from 'js/formats/types'; import { slot } from 'js/apple2io'; import DiskII from 'js/cards/disk2'; import SmartPort from 'js/cards/smartport'; @@ -38,7 +38,7 @@ const formatDate = (date: Date) => { * @param disk NibbleDisk or BlockDisk * @returns true if is BlockDisk */ -function isBlockDisk(disk: NibbleDisk | BlockDisk): disk is BlockDisk { +function isBlockDisk(disk: FloppyDisk | BlockDisk): disk is BlockDisk { return !!((disk as BlockDisk).blocks); } @@ -256,7 +256,7 @@ const DiskInfo = ({ massStorage, drive, setFileData }: DiskInfoProps) => { if (massStorageData) { const { data, readOnly, ext } = massStorageData; const { name } = massStorageData.metadata; - let disk: BlockDisk | NibbleDisk | null = null; + let disk: BlockDisk | FloppyDisk | null = null; if (ext === '2mg') { disk = createDiskFrom2MG({ name, @@ -277,8 +277,8 @@ const DiskInfo = ({ massStorage, drive, setFileData }: DiskInfoProps) => { } } } - if (!disk) { - disk = createBlockDisk({ + if (!disk && isBlockDiskFormat(ext)) { + disk = createBlockDisk(ext, { name, rawData: data, readOnly, @@ -330,7 +330,7 @@ const DiskInfo = ({ massStorage, drive, setFileData }: DiskInfoProps) => { ); } - } else { + } else if (isNibbleDisk(disk)) { const dos = new DOS33(disk); return (
diff --git a/js/components/util/files.ts b/js/components/util/files.ts index f02d7bd..d365c35 100644 --- a/js/components/util/files.ts +++ b/js/components/util/files.ts @@ -1,18 +1,18 @@ -import { includes, word } from 'js/types'; -import { initGamepad } from 'js/ui/gamepad'; +import Disk2 from 'js/cards/disk2'; +import SmartPort from 'js/cards/smartport'; +import Debugger from 'js/debugger'; import { BlockFormat, BLOCK_FORMATS, DISK_FORMATS, DriveNumber, + FloppyFormat, + FLOPPY_FORMATS, JSONDisk, MassStorage, - NibbleFormat, - NIBBLE_FORMATS, } from 'js/formats/types'; -import Disk2 from 'js/cards/disk2'; -import SmartPort from 'js/cards/smartport'; -import Debugger from 'js/debugger'; +import { includes, word } from 'js/types'; +import { initGamepad } from 'js/ui/gamepad'; type ProgressCallback = (current: number, total: number) => void; @@ -46,8 +46,8 @@ export const getNameAndExtension = (url: string) => { }; export const loadLocalFile = ( - storage: MassStorage, - formats: typeof NIBBLE_FORMATS | typeof BLOCK_FORMATS | typeof DISK_FORMATS, + storage: MassStorage, + formats: typeof FLOPPY_FORMATS | typeof BLOCK_FORMATS | typeof DISK_FORMATS, number: DriveNumber, file: File, ) => { @@ -94,7 +94,7 @@ export const loadLocalBlockFile = (smartPort: SmartPort, number: DriveNumber, fi * @returns true if successful */ export const loadLocalNibbleFile = (disk2: Disk2, number: DriveNumber, file: File) => { - return loadLocalFile(disk2, NIBBLE_FORMATS, number, file); + return loadLocalFile(disk2, FLOPPY_FORMATS, number, file); }; /** @@ -117,7 +117,7 @@ export const loadJSON = async ( throw new Error(`Error loading: ${response.statusText}`); } const data = await response.json() as JSONDisk; - if (!includes(NIBBLE_FORMATS, data.type)) { + if (!includes(FLOPPY_FORMATS, data.type)) { throw new Error(`Type "${data.type}" not recognized.`); } disk2.setDisk(number, data); @@ -209,7 +209,7 @@ export const loadHttpNibbleFile = async ( return loadJSON(disk2, number, url); } const { name, ext } = getNameAndExtension(url); - if (!includes(NIBBLE_FORMATS, ext)) { + if (!includes(FLOPPY_FORMATS, ext)) { throw new Error(`Extension "${ext}" not recognized.`); } const data = await loadHttpFile(url, signal, onProgress); @@ -241,7 +241,7 @@ export class SmartStorageBroker implements MassStorage { } else { throw new Error(`Unable to load "${name}"`); } - } else if (includes(NIBBLE_FORMATS, ext)) { + } else if (includes(FLOPPY_FORMATS, ext)) { this.disk2.setBinary(drive, name, ext, data); } else { throw new Error(`Unable to load "${name}"`); diff --git a/js/formats/block.ts b/js/formats/block.ts index 1750ea8..97fc1da 100644 --- a/js/formats/block.ts +++ b/js/formats/block.ts @@ -1,10 +1,10 @@ -import { DiskOptions, BlockDisk, ENCODING_BLOCK } from './types'; +import { DiskOptions, BlockDisk, ENCODING_BLOCK, BlockFormat } from './types'; /** * Returns a `Disk` object for a block volume with block-ordered data. * @param options the disk image and options */ -export default function createBlockDisk(options: DiskOptions): BlockDisk { +export default function createBlockDisk(fmt: BlockFormat, options: DiskOptions): BlockDisk { const { rawData, readOnly, name } = options; if (!rawData) { @@ -20,6 +20,7 @@ export default function createBlockDisk(options: DiskOptions): BlockDisk { const disk: BlockDisk = { encoding: ENCODING_BLOCK, + format: fmt, blocks, metadata: { name }, readOnly, diff --git a/js/formats/create_disk.ts b/js/formats/create_disk.ts index 5dc884d..01a28b8 100644 --- a/js/formats/create_disk.ts +++ b/js/formats/create_disk.ts @@ -1,6 +1,6 @@ import { includes, memory } from '../types'; import { base64_decode } from '../base64'; -import { DiskOptions, FloppyDisk, JSONDisk, NibbleFormat, NIBBLE_FORMATS } from './types'; +import { BitstreamFormat, DiskOptions, FloppyDisk, FloppyFormat, JSONDisk, NibbleDisk, NibbleFormat, NIBBLE_FORMATS, WozDisk } from './types'; import createDiskFrom2MG from './2mg'; import createDiskFromD13 from './d13'; import createDiskFromDOS from './do'; @@ -8,13 +8,13 @@ import createDiskFromProDOS from './po'; import createDiskFromWoz from './woz'; import createDiskFromNibble from './nib'; -/** - * - * @param fmt Type of - * @param options - * @returns A nibblized disk - */ -export function createDisk(fmt: NibbleFormat, options: DiskOptions): FloppyDisk | null { +/** Creates a `NibbleDisk` from the given format and options. */ +export function createDisk(fmt: NibbleFormat, options: DiskOptions): NibbleDisk | null; +/** Creates a `WozDisk` from the given format and options. */ +export function createDisk(fmt: BitstreamFormat, options: DiskOptions): WozDisk | null; +/** Creates a `FloppyDisk` (either a `NibbleDisk` or a `WozDisk`) from the given format and options. */ +export function createDisk(fmt: FloppyFormat, options: DiskOptions): FloppyDisk | null; +export function createDisk(fmt: FloppyFormat, options: DiskOptions): FloppyDisk | null { let disk: FloppyDisk | null = null; switch (fmt) { @@ -42,7 +42,8 @@ export function createDisk(fmt: NibbleFormat, options: DiskOptions): FloppyDisk return disk; } -export function createDiskFromJsonDisk(disk: JSONDisk): FloppyDisk | null { +/** Creates a NibbleDisk from JSON */ +export function createDiskFromJsonDisk(disk: JSONDisk): NibbleDisk | null { const fmt = disk.type; const readOnly = disk.readOnly; const name = disk.name; diff --git a/js/formats/format_utils.ts b/js/formats/format_utils.ts index 0bc409a..a81a57e 100644 --- a/js/formats/format_utils.ts +++ b/js/formats/format_utils.ts @@ -1,7 +1,7 @@ import { bit, byte, memory } from '../types'; import { base64_decode, base64_encode } from '../base64'; import { bytify, debug, toHex } from '../util'; -import { NibbleDisk, ENCODING_NIBBLE, JSONDisk } from './types'; +import { NibbleDisk, ENCODING_NIBBLE, JSONDisk, isNibbleDiskFormat } from './types'; /** * DOS 3.3 Physical sector order (index is physical sector, value is DOS sector). @@ -550,6 +550,9 @@ export function jsonDecode(data: string): NibbleDisk { } tracks[t] = bytify(track); } + if (!isNibbleDiskFormat(json.type)) { + throw new Error(`JSON disks of type ${json.type} are not supported`); + } const disk: NibbleDisk = { volume: v, format: json.type, diff --git a/js/formats/types.ts b/js/formats/types.ts index 272bc9e..a3553b9 100644 --- a/js/formats/types.ts +++ b/js/formats/types.ts @@ -64,22 +64,29 @@ export interface Disk { readOnly: boolean; } +export const NO_DISK = 'empty'; export const ENCODING_NIBBLE = 'nibble'; export const ENCODING_BITSTREAM = 'bitstream'; export const ENCODING_BLOCK = 'block'; export interface FloppyDisk extends Disk { - tracks: memory[]; + encoding: typeof ENCODING_NIBBLE | typeof ENCODING_BITSTREAM | typeof NO_DISK; +} + +export interface NoFloppyDisk extends FloppyDisk { + encoding: typeof NO_DISK; } export interface NibbleDisk extends FloppyDisk { encoding: typeof ENCODING_NIBBLE; - format: DiskFormat; + format: Exclude; volume: byte; + tracks: memory[]; } export interface WozDisk extends FloppyDisk { encoding: typeof ENCODING_BITSTREAM; + format: 'woz'; trackMap: number[]; rawTracks: Uint8Array[]; info: InfoChunk | undefined; @@ -87,14 +94,13 @@ export interface WozDisk extends FloppyDisk { export interface BlockDisk extends Disk { encoding: typeof ENCODING_BLOCK; + format: BlockFormat; blocks: Uint8Array[]; } /** - * File types supported by the disk format processors and - * block devices. + * File types supported by floppy devices in nibble mode. */ - export const NIBBLE_FORMATS = [ '2mg', 'd13', @@ -102,21 +108,70 @@ export const NIBBLE_FORMATS = [ 'dsk', 'po', 'nib', - 'woz' ] as const; +/** + * File types supported by floppy devices in bitstream mode. + */ +export const BITSTREAM_FORMATS = [ + 'woz', +] as const; + +/** + * All file types supported by floppy devices. + */ +export const FLOPPY_FORMATS = [ + ...NIBBLE_FORMATS, + ...BITSTREAM_FORMATS, +] as const; + +/** + * File types supported by block devices. + */ export const BLOCK_FORMATS = [ '2mg', 'hdv', 'po', ] as const; -export const DISK_FORMATS = [...NIBBLE_FORMATS, ...BLOCK_FORMATS] as const; +/** + * All supported disk formats. + */ +export const DISK_FORMATS = [ + ...FLOPPY_FORMATS, + ...BLOCK_FORMATS, +] as const; +export type FloppyFormat = MemberOf; export type NibbleFormat = MemberOf; +export type BitstreamFormat = 'woz'; export type BlockFormat = MemberOf; export type DiskFormat = MemberOf; +/** Type guard for nibble disk formats. */ +export function isNibbleDiskFormat(f: DiskFormat): f is NibbleFormat { + return f in NIBBLE_FORMATS; +} + +/** Type guard for block disk formats. */ +export function isBlockDiskFormat(f: DiskFormat): f is BlockFormat { + return f in BLOCK_FORMATS; +} + +export function isNoFloppyDisk(disk: Disk): disk is NoFloppyDisk { + return (disk as NoFloppyDisk)?.encoding === NO_DISK; +} + +/** Type guard for NibbleDisks */ +export function isNibbleDisk(disk: Disk): disk is NibbleDisk { + return (disk as NibbleDisk)?.encoding === ENCODING_NIBBLE; +} + +/** Type guard for NibbleDisks */ +export function isWozDisk(disk: Disk): disk is WozDisk { + return (disk as WozDisk)?.encoding === ENCODING_BITSTREAM; +} + /** * Base format for JSON defined disks */ @@ -180,7 +235,7 @@ export interface ProcessBinaryMessage { type: typeof PROCESS_BINARY; payload: { drive: DriveNumber; - fmt: NibbleFormat; + fmt: FloppyFormat; options: DiskOptions; }; } @@ -227,7 +282,7 @@ export type FormatWorkerResponse = export interface MassStorageData { metadata: DiskMetadata; - ext: string; + ext: DiskFormat; readOnly: boolean; volume?: byte; data: ArrayBuffer; diff --git a/js/formats/woz.ts b/js/formats/woz.ts index c632407..5160cae 100644 --- a/js/formats/woz.ts +++ b/js/formats/woz.ts @@ -293,8 +293,8 @@ export default function createDiskFromWoz(options: DiskOptions): WozDisk { const disk: WozDisk = { encoding: ENCODING_BITSTREAM, + format: 'woz', trackMap: tmap?.trackMap || [], - tracks: trks?.tracks || [], rawTracks: trks?.rawTracks || [], readOnly: true, //chunks.info.writeProtected === 1; metadata: { diff --git a/js/ui/apple2.ts b/js/ui/apple2.ts index 5866824..498b5dc 100644 --- a/js/ui/apple2.ts +++ b/js/ui/apple2.ts @@ -11,10 +11,10 @@ import { DriveNumber, DRIVE_NUMBERS, MassStorage, - NIBBLE_FORMATS, JSONBinaryImage, JSONDisk, - BlockFormat + BlockFormat, + FLOPPY_FORMATS } from '../formats/types'; import { initGamepad } from './gamepad'; import KeyBoard from './keyboard'; @@ -392,7 +392,7 @@ function doLoadLocalDisk(drive: DriveNumber, file: File) { } } else { if ( - includes(NIBBLE_FORMATS, ext) && + includes(FLOPPY_FORMATS, ext) && _disk2.setBinary(drive, name, ext, result) ) { initGamepad(); @@ -459,7 +459,7 @@ export function doLoadHTTP(drive: DriveNumber, url?: string) { } } else { if ( - includes(NIBBLE_FORMATS, ext) && + includes(FLOPPY_FORMATS, ext) && _disk2.setBinary(drive, name, ext, data) ) { initGamepad(); diff --git a/test/js/cards/disk2.spec.ts b/test/js/cards/disk2.spec.ts index 38f6b5e..9e31cd0 100644 --- a/test/js/cards/disk2.spec.ts +++ b/test/js/cards/disk2.spec.ts @@ -4,7 +4,7 @@ import fs from 'fs'; import Apple2IO from 'js/apple2io'; import DiskII, { Callbacks } from 'js/cards/disk2'; import CPU6502 from 'js/cpu6502'; -import { DriveNumber } from 'js/formats/types'; +import { DriveNumber, NibbleDisk, WozDisk } from 'js/formats/types'; import { byte } from 'js/types'; import { toHex } from 'js/util'; import { VideoModes } from 'js/videomodes'; @@ -71,7 +71,8 @@ describe('DiskII', () => { state.controllerState.latch = 0x42; state.controllerState.on = true; state.controllerState.q7 = true; - state.drives[2].tracks[14][12] = 0x80; + const disk2 = state.drives[2].disk as NibbleDisk; + disk2.tracks[14][12] = 0x80; state.drives[2].head = 1000; state.drives[2].phase = 3; diskII.setState(state); @@ -478,21 +479,24 @@ describe('DiskII', () => { it('writes a nibble to the disk', () => { const diskII = new DiskII(mockApple2IO, callbacks); diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE); - let track0 = diskII.getState().drives[1].tracks[0]; + let disk1 = diskII.getState().drives[1].disk as NibbleDisk; + let track0 = disk1.tracks[0]; expect(track0[0]).toBe(0xFF); diskII.ioSwitch(0x89); // turn on the motor diskII.ioSwitch(0x8F, 0x80); // write diskII.ioSwitch(0x8C); // shift - track0 = diskII.getState().drives[1].tracks[0]; + disk1 = diskII.getState().drives[1].disk as NibbleDisk; + track0 = disk1.tracks[0]; expect(track0[0]).toBe(0x80); }); it('writes two nibbles to the disk', () => { const diskII = new DiskII(mockApple2IO, callbacks); diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE); - let track0 = diskII.getState().drives[1].tracks[0]; + let disk1 = diskII.getState().drives[1].disk as NibbleDisk; + let track0 = disk1.tracks[0]; expect(track0[0]).toBe(0xFF); diskII.ioSwitch(0x89); // turn on the motor @@ -501,7 +505,8 @@ describe('DiskII', () => { diskII.ioSwitch(0x8F, 0x81); // write diskII.ioSwitch(0x8C); // shift - track0 = diskII.getState().drives[1].tracks[0]; + disk1 = diskII.getState().drives[1].disk as NibbleDisk; + track0 = disk1.tracks[0]; expect(track0[0]).toBe(0x80); expect(track0[1]).toBe(0x81); }); @@ -819,10 +824,10 @@ class TestDiskReader { rawTracks() { // NOTE(flan): Hack to access private properties. - const disk = this.diskII as unknown as { cur: { rawTracks: Uint8Array[] } }; + const disk = (this.diskII as unknown as { curDisk: WozDisk }).curDisk; const result: Uint8Array[] = []; - for (let i = 0; i < disk.cur.rawTracks.length; i++) { - result[i] = disk.cur.rawTracks[i].slice(0); + for (let i = 0; i < disk.rawTracks.length; i++) { + result[i] = disk.rawTracks[i].slice(0); } return result; diff --git a/test/js/formats/2mg.spec.ts b/test/js/formats/2mg.spec.ts index 653acaa..350e495 100644 --- a/test/js/formats/2mg.spec.ts +++ b/test/js/formats/2mg.spec.ts @@ -193,6 +193,7 @@ describe('2mg format', () => { metadata: { name: 'Good disk' }, readOnly: false, encoding: ENCODING_BLOCK, + format: 'hdv', }; const image = create2MGFromBlockDisk(header, disk); expect(VALID_PRODOS_IMAGE.buffer).toEqual(image); diff --git a/test/js/formats/woz.spec.ts b/test/js/formats/woz.spec.ts index 9855351..c5a5c5c 100644 --- a/test/js/formats/woz.spec.ts +++ b/test/js/formats/woz.spec.ts @@ -21,16 +21,16 @@ describe('woz', () => { const disk = createDiskFromWoz(options); expect(disk).toEqual({ - metadata: { name: 'Mock Woz 1' }, + metadata: { name: 'Mock Woz 1', side: undefined }, readOnly: true, encoding: ENCODING_BITSTREAM, + format: 'woz', trackMap: mockTMAP, rawTracks: [new Uint8Array([ 1, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, ])], - tracks: [new Uint8Array([0xD5, 0xAA, 0x96])], info: { bitTiming: 0, bootSector: 0, @@ -64,13 +64,13 @@ describe('woz', () => { }, readOnly: true, encoding: ENCODING_BITSTREAM, + format: 'woz', trackMap: mockTMAP, rawTracks: [new Uint8Array([ 1, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, ])], - tracks: [new Uint8Array([0xD5, 0xAA, 0x96])], info: { bitTiming: 0, bootSector: 0,