import { MOS6502, MOS6502State } from "../common/cpu/MOS6502"; import { Bus, BasicScanlineMachine, xorshift32, SavesState } from "../common/devices"; import { KeyFlags } from "../common/emu"; // TODO import { hex, lzgmini, stringToByteArray, RGBA, printFlags } from "../common/util"; const cpuFrequency = 1023000; const cpuCyclesPerLine = 65; // approx: http://www.cs.columbia.edu/~sedwards/apple2fpga/ const cpuCyclesPerFrame = 65*262; // TODO: read prodos/ca65 header? const VM_BASE = 0x803; // where to JMP after pr#6 const LOAD_BASE = VM_BASE; const PGM_BASE = VM_BASE; const HDR_SIZE = PGM_BASE - LOAD_BASE; interface AppleIIStateBase { ram : Uint8Array; rnd,soundstate : number; auxRAMselected,writeinhibit : boolean; auxRAMbank : number; } interface AppleIIControlsState { inputs : Uint8Array; // unused? kbdlatch : number; } interface AppleIIState extends AppleIIStateBase, AppleIIControlsState { c : MOS6502State; grswitch : number; slots: SlotDevice[]; } interface SlotDevice extends Bus { readROM(address: number) : number; readConst(address: number) : number; } export class AppleII extends BasicScanlineMachine { cpuFrequency = 1023000; sampleRate = this.cpuFrequency; cpuCyclesPerLine = 65; // approx: http://www.cs.columbia.edu/~sedwards/apple2fpga/ cpuCyclesPerFrame = 65*262; canvasWidth = 280; numVisibleScanlines = 192; numTotalScanlines = 262; defaultROMSize = 0xbf00-0x803; // TODO ram = new Uint8Array(0x13000); // 64K + 16K LC RAM - 4K hardware + 12K ROM bios : Uint8Array; cpu = new MOS6502(); grdirty = new Array(0xc000 >> 7); grparams = {dirty:this.grdirty, grswitch:GR_TXMODE, mem:this.ram}; ap2disp; rnd = 1; kbdlatch = 0; soundstate = 0; // language card switches auxRAMselected = false; auxRAMbank = 1; writeinhibit = true; // value to add when reading & writing each of these banks // bank 1 is E000-FFFF, bank 2 is D000-DFFF bank2rdoffset=0; bank2wroffset=0; // disk II slots : SlotDevice[] = new Array(8); // fake disk drive that loads program into RAM fakeDrive : SlotDevice = { readROM: (a) => { switch (a) { // JMP VM_BASE case 0: { // load program into RAM if (this.rom) this.ram.set(this.rom.slice(HDR_SIZE), PGM_BASE); return 0x4c; } case 1: return VM_BASE&0xff; case 2: return (VM_BASE>>8)&0xff; default: return 0; } }, readConst: (a) => { return 0; }, read: (a) => { return this.noise(); }, write: (a,v) => { } }; constructor() { super(); this.bios = new lzgmini().decode(stringToByteArray(atob(APPLEIIGO_LZG))); this.bios[0x39a] = 0x60; // $d39a = RTS this.ram.set(this.bios, 0xd000); this.ram[0xbf00] = 0x4c; // fake DOS detect for C this.ram[0xbf6f] = 0x01; // fake DOS detect for C this.connectCPUMemoryBus(this); } saveState() : AppleIIState { // TODO: automagic return { c: this.cpu.saveState(), ram: this.ram.slice(), rnd: this.rnd, kbdlatch: this.kbdlatch, soundstate: this.soundstate, grswitch: this.grparams.grswitch, auxRAMselected: this.auxRAMselected, auxRAMbank: this.auxRAMbank, writeinhibit: this.writeinhibit, slots: this.slots.map((slot) => { return slot && slot['saveState'] && slot['saveState']() }), inputs: null }; } loadState(s:AppleIIState) { this.cpu.loadState(s.c); this.ram.set(s.ram); this.rnd = s.rnd; this.kbdlatch = s.kbdlatch; this.soundstate = s.soundstate; this.grparams.grswitch = s.grswitch; this.auxRAMselected = s.auxRAMselected; this.auxRAMbank = s.auxRAMbank; this.writeinhibit = s.writeinhibit; this.setupLanguageCardConstants(); for (var i=0; i>8) == 0xc6) break; } // get out of $c600 boot for (var i=0; i<2000000; i++) { this.cpu.advanceClock(); if ((this.cpu.getPC()>>8) < 0xc6) break; } } noise() : number { return (this.rnd = xorshift32(this.rnd)) & 0xff; } readConst(address: number): number { if (address < 0xc000) { return this.ram[address]; } else if (address >= 0xd000) { if (!this.auxRAMselected) return this.bios[address - 0xd000]; else if (address >= 0xe000) return this.ram[address]; else return this.ram[address + this.bank2rdoffset]; } else if (address >= 0xc100 && address < 0xc800) { var slot = (address >> 8) & 7; return (this.slots[slot] && this.slots[slot].readConst(address & 0xff)) | 0; } else { return 0; } } read(address:number) : number { address &= 0xffff; if (address < 0xc000 || address >= 0xd000) { return this.readConst(address); } else if (address < 0xc100) { var slot = (address >> 4) & 0x0f; switch (slot) { case 0: return this.kbdlatch; case 1: this.kbdlatch &= 0x7f; break; case 3: this.soundstate = this.soundstate ^ 1; break; case 5: if ((address & 0x0f) < 8) { // graphics if ((address & 1) != 0) this.grparams.grswitch |= 1 << ((address >> 1) & 0x07); else this.grparams.grswitch &= ~(1 << ((address >> 1) & 0x07)); } break; case 6: // tapein, joystick, buttons switch (address & 7) { // buttons (off) case 1: case 2: case 3: return this.noise() & 0x7f; // joystick case 4: case 5: return this.noise() | 0x80; default: return this.noise(); } case 7: // joy reset if (address == 0xc070) return this.noise() | 0x80; case 8: return this.doLanguageCardIO(address); case 9: case 10: case 11: case 12: case 13: case 14: case 15: return (this.slots[slot-8] && this.slots[slot-8].read(address & 0xf)) | 0; } } else if (address >= 0xc100 && address < 0xc800) { var slot = (address >> 8) & 7; return (this.slots[slot] && this.slots[slot].readROM(address & 0xff)) | 0; } return this.noise(); } write(address:number, val:number) : void { address &= 0xffff; val &= 0xff; if (address < 0xc000) { this.ram[address] = val; this.grdirty[address>>7] = 1; } else if (address < 0xc080) { this.read(address); // strobe address, discard result } else if (address < 0xc100) { var slot = (address >> 4) & 0x0f; this.slots[slot-8] && this.slots[slot-8].write(address & 0xf, val); } else if (address >= 0xd000 && !this.writeinhibit) { if (address >= 0xe000) this.ram[address] = val; else this.ram[address + this.bank2wroffset] = val; } } connectVideo(pixels:Uint32Array) { super.connectVideo(pixels); this.ap2disp = this.pixels && new Apple2Display(this.pixels, this.grparams); } startScanline() { } drawScanline() { // TODO: draw scanline via ap2disp } advanceFrame(trap) : number { var clocks = super.advanceFrame(trap); this.ap2disp && this.ap2disp.updateScreen(); return clocks; } advanceCPU() { this.audio.feedSample(this.soundstate, 1); return super.advanceCPU(); } setKeyInput(key:number, code:number, flags:number) : void { if (flags & KeyFlags.KeyPress) { // convert to uppercase for Apple ][ if (code >= 0x61 && code <= 0x7a) code -= 32; if (code >= 32) { if (code >= 65 && code < 65+26) { if (flags & KeyFlags.Ctrl) code -= 64; // ctrl } this.kbdlatch = (code | 0x80) & 0xff; } } else if (flags & KeyFlags.KeyDown) { code = 0; switch (key) { case 8: code=8; break; // left case 13: code=13; break; // return case 27: code=27; break; // escape case 37: code=8; break; // left case 39: code=21; break; // right case 38: code=11; break; // up case 40: code=10; break; // down } if (code) this.kbdlatch = (code | 0x80) & 0xff; } } doLanguageCardIO(address:number) { switch (address & 0x0f) { // Select aux RAM bank 2, write protected. case 0x0: case 0x4: this.auxRAMselected = true; this.auxRAMbank = 2; this.writeinhibit = true; break; // Select ROM, write enable aux RAM bank 2. case 0x1: case 0x5: this.auxRAMselected = false; this.auxRAMbank = 2; this.writeinhibit = false; break; // Select ROM, write protect aux RAM (either bank). case 0x2: case 0x6: case 0xA: case 0xE: this.auxRAMselected = false; this.writeinhibit = true; break; // Select aux RAM bank 2, write enabled. case 0x3: case 0x7: this.auxRAMselected = true; this.auxRAMbank = 2; this.writeinhibit = false; break; // Select aux RAM bank 1, write protected. case 0x8: case 0xC: this.auxRAMselected = true; this.auxRAMbank = 1; this.writeinhibit = true; break; // Select ROM, write enable aux RAM bank 1. case 0x9: case 0xD: this.auxRAMselected = false; this.auxRAMbank = 1; this.writeinhibit = false; break; // Select aux RAM bank 1, write enabled. case 0xB: case 0xF: this.auxRAMselected = true; this.auxRAMbank = 1; this.writeinhibit = false; break; } this.setupLanguageCardConstants(); return this.noise(); } setupLanguageCardConstants() { // reset language card constants if (this.auxRAMbank == 2) this.bank2rdoffset = -0x1000; // map 0xd000-0xdfff -> 0xc000-0xcfff else this.bank2rdoffset = 0x3000; // map 0xd000-0xdfff -> 0x10000-0x10fff if (this.auxRAMbank == 2) this.bank2wroffset = -0x1000; // map 0xd000-0xdfff -> 0xc000-0xcfff else this.bank2wroffset = 0x3000; // map 0xd000-0xdfff -> 0x10000-0x10fff } getDebugCategories() { return ['CPU','Stack','I/O','Disk']; } getDebugInfo(category:string, state:AppleIIState) { switch (category) { case 'I/O': return "AUX RAM Bank: " + state.auxRAMbank + "\nAUX RAM Select: " + state.auxRAMselected + "\nAUX RAM Write: " + !state.writeinhibit + "\n\nGR Switches: " + printFlags(state.grswitch, ["Graphics","Mixed","Page2","Hires"], false) + "\n"; case 'Disk': return (this.slots[6] && this.slots[6]['toLongString'] && this.slots[6]['toLongString']()) || "\n"; } } } const GR_TXMODE = 1; const GR_MIXMODE = 2; const GR_PAGE1 = 4; const GR_HIRES = 8; type AppleGRParams = {dirty:boolean[], grswitch:number, mem:Uint8Array}; var Apple2Display = function(pixels : Uint32Array, apple : AppleGRParams) { var XSIZE = 280; var YSIZE = 192; var PIXELON = 0xffffffff; var PIXELOFF = 0xff000000; var oldgrmode = -1; var textbuf = new Array(40*24); const flashInterval = 500; // https://mrob.com/pub/xapple2/colors.html const loresColor = [ RGBA(0, 0, 0), RGBA(227, 30, 96), RGBA(96, 78, 189), RGBA(255, 68, 253), RGBA(0, 163, 96), RGBA(156, 156, 156), RGBA(20, 207, 253), RGBA(208, 195, 255), RGBA(96, 114, 3), RGBA(255, 106, 60), RGBA(156, 156, 156), RGBA(255, 160, 208), RGBA(20, 245, 60), RGBA(208, 221, 141), RGBA(114, 255, 208), RGBA(255, 255, 255) ]; const text_lut = [ 0x000, 0x080, 0x100, 0x180, 0x200, 0x280, 0x300, 0x380, 0x028, 0x0a8, 0x128, 0x1a8, 0x228, 0x2a8, 0x328, 0x3a8, 0x050, 0x0d0, 0x150, 0x1d0, 0x250, 0x2d0, 0x350, 0x3d0 ]; const hires_lut = [ 0x0000, 0x0400, 0x0800, 0x0c00, 0x1000, 0x1400, 0x1800, 0x1c00, 0x0080, 0x0480, 0x0880, 0x0c80, 0x1080, 0x1480, 0x1880, 0x1c80, 0x0100, 0x0500, 0x0900, 0x0d00, 0x1100, 0x1500, 0x1900, 0x1d00, 0x0180, 0x0580, 0x0980, 0x0d80, 0x1180, 0x1580, 0x1980, 0x1d80, 0x0200, 0x0600, 0x0a00, 0x0e00, 0x1200, 0x1600, 0x1a00, 0x1e00, 0x0280, 0x0680, 0x0a80, 0x0e80, 0x1280, 0x1680, 0x1a80, 0x1e80, 0x0300, 0x0700, 0x0b00, 0x0f00, 0x1300, 0x1700, 0x1b00, 0x1f00, 0x0380, 0x0780, 0x0b80, 0x0f80, 0x1380, 0x1780, 0x1b80, 0x1f80, 0x0028, 0x0428, 0x0828, 0x0c28, 0x1028, 0x1428, 0x1828, 0x1c28, 0x00a8, 0x04a8, 0x08a8, 0x0ca8, 0x10a8, 0x14a8, 0x18a8, 0x1ca8, 0x0128, 0x0528, 0x0928, 0x0d28, 0x1128, 0x1528, 0x1928, 0x1d28, 0x01a8, 0x05a8, 0x09a8, 0x0da8, 0x11a8, 0x15a8, 0x19a8, 0x1da8, 0x0228, 0x0628, 0x0a28, 0x0e28, 0x1228, 0x1628, 0x1a28, 0x1e28, 0x02a8, 0x06a8, 0x0aa8, 0x0ea8, 0x12a8, 0x16a8, 0x1aa8, 0x1ea8, 0x0328, 0x0728, 0x0b28, 0x0f28, 0x1328, 0x1728, 0x1b28, 0x1f28, 0x03a8, 0x07a8, 0x0ba8, 0x0fa8, 0x13a8, 0x17a8, 0x1ba8, 0x1fa8, 0x0050, 0x0450, 0x0850, 0x0c50, 0x1050, 0x1450, 0x1850, 0x1c50, 0x00d0, 0x04d0, 0x08d0, 0x0cd0, 0x10d0, 0x14d0, 0x18d0, 0x1cd0, 0x0150, 0x0550, 0x0950, 0x0d50, 0x1150, 0x1550, 0x1950, 0x1d50, 0x01d0, 0x05d0, 0x09d0, 0x0dd0, 0x11d0, 0x15d0, 0x19d0, 0x1dd0, 0x0250, 0x0650, 0x0a50, 0x0e50, 0x1250, 0x1650, 0x1a50, 0x1e50, 0x02d0, 0x06d0, 0x0ad0, 0x0ed0, 0x12d0, 0x16d0, 0x1ad0, 0x1ed0, 0x0350, 0x0750, 0x0b50, 0x0f50, 0x1350, 0x1750, 0x1b50, 0x1f50, 0x03d0, 0x07d0, 0x0bd0, 0x0fd0, 0x13d0, 0x17d0, 0x1bd0, 0x1fd0 ]; var colors_lut; /** * This function makes the color lookup table for hires mode. * We make a table of 1024 * 2 * 7 entries. * Why? Because we assume each color byte has 10 bits * (8 real bits + 1 on each side) and we need different colors * for odd and even addresses (2) and each byte displays 7 pixels. */ { colors_lut = new Array(256*4*2*7); var i,j; var c1,c2,c3 = 15; var base = 0; // go thru odd and even for (j=0; j<2; j++) { // go thru 1024 values for (var b1=0; b1<1024; b1++) { // see if the hi bit is set if ((b1 & 0x80) == 0) { c1 = 3; c2 = 12; // purple & green } else { c1 = 6; c2 = 9; // blue & orange } // make a value consisting of: // the 8th bit, then bits 0-7, then the 9th bit var b = ((b1 & 0x100) >> 8) | ((b1 & 0x7f) << 1) | ((b1 & 0x200) >> 1); // go through each pixel for (i=0; i<7; i++) { var c; // is this pixel lit? if (((2<> 4]; for (i=0; i<4; i++) { pixels[base] = pixels[base+1] = pixels[base+2] = pixels[base+3] = pixels[base+4] = pixels[base+5] = pixels[base+6] = c; base += XSIZE; } } function drawTextChar(x, y, b, invert) { var base = (y<<3)*XSIZE + x*7; // (x<<2) + (x<<1) + x var on,off; if (invert) { on = PIXELOFF; off = PIXELON; } else { on = PIXELON; off = PIXELOFF; } for (var yy=0; yy<8; yy++) { var chr = apple2_charset[(b<<3)+yy]; pixels[base] = ((chr & 64) > 0)?on:off; pixels[base+1] = ((chr & 32) > 0)?on:off; pixels[base+2] = ((chr & 16) > 0)?on:off; pixels[base+3] = ((chr & 8) > 0)?on:off; pixels[base+4] = ((chr & 4) > 0)?on:off; pixels[base+5] = ((chr & 2) > 0)?on:off; pixels[base+6] = ((chr & 1) > 0)?on:off; base += XSIZE; } } function drawHiresLines(y, maxy) { var yb = y*XSIZE; for (; y < maxy; y++) { var base = hires_lut[y] + (((apple.grswitch & GR_PAGE1) != 0) ? 0x4000 : 0x2000); if (!apple.dirty[base >> 7]) { yb += XSIZE; continue; } var c1, c2; var b = 0; var b1 = apple.mem[base] & 0xff; for (var x1=0; x1<20; x1++) { var b2 = apple.mem[base+1] & 0xff; var b3 = apple.mem[base+2] & 0xff; var d1 = (((b&0x40)<<2) | b1 | b2<<9) & 0x3ff; for (var i=0; i<7; i++) pixels[yb+i] = colors_lut[d1*7+i]; var d2 = (((b1&0x40)<<2) | b2 | b3<<9) & 0x3ff; for (var i=0; i<7; i++) pixels[yb+7+i] = colors_lut[d2*7+7168+i]; yb += 14; base += 2; b = b2; b1 = b3; } } } function drawLoresLine(y) { // get the base address of this line var base = text_lut[y] + (((apple.grswitch & GR_PAGE1) != 0) ? 0x800 : 0x400); // if (!dirty[base >> 7]) // return; for (var x=0; x<40; x++) { var b = apple.mem[base+x] & 0xff; // if the char. changed, draw it if (b != textbuf[y*40+x]) { drawLoresChar(x, y, b); textbuf[y*40+x] = b; } } } function drawTextLine(y, flash) { // get the base address of this line var base = text_lut[y] + (((apple.grswitch & GR_PAGE1) != 0) ? 0x800 : 0x400); // if (!dirty[base >> 7]) // return; for (var x=0; x<40; x++) { var b = apple.mem[base+x] & 0xff; var invert; // invert flash characters 1/2 of the time if (b >= 0x80) { invert = false; } else if (b >= 0x40) { invert = flash; if (flash) b -= 0x40; else b += 0x40; } else invert = true; // if the char. changed, draw it if (b != textbuf[y*40+x]) { drawTextChar(x, y, b & 0x7f, invert); textbuf[y*40+x] = b; } } } this.updateScreen = function(totalrepaint) { var y; var flash = (new Date().getTime() % (flashInterval<<1)) > flashInterval; // if graphics mode changed, repaint whole screen if (apple.grswitch != oldgrmode) { oldgrmode = apple.grswitch; totalrepaint = true; } if (totalrepaint) { // clear textbuf if in text mode if ((apple.grswitch & GR_TXMODE) != 0 || (apple.grswitch & GR_MIXMODE) != 0) { for (y=0; y<24; y++) for (var x=0; x<40; x++) textbuf[y*40+x] = -1; } for (var i=0; i { emu : AppleII; track_data : Uint8Array; constructor(emu : AppleII, image : Uint8Array) { super(); this.emu = emu; this.data = new Array(NUM_TRACKS); for (var i=0; i>1]; else this.track_data = null; } toLongString() { return "Track: " + (this.track / 2) + "\nOffset: " + (this.track_index) + "\nMode: " + (this.read_mode ? "READ" : "WRITE") + "\nMotor: " + this.motor + "\nData: " + (this.track_data ? hex(this.track_data[this.track_index]) : '-') + "\n"; } read_latch() : number { this.track_index = (this.track_index + 1) % TRACK_SIZE; if (this.track_data) { return (this.track_data[this.track_index] & 0xff); } else return this.emu.noise() | 0x80; } write_latch(value: number) { this.track_index = (this.track_index + 1) % TRACK_SIZE; if (this.track_data != null) this.track_data[this.track_index] = value; } readROM(address) { return DISKII_PROM[address]; } readConst(address) { return DISKII_PROM[address]; } read(address) { return this.doIO(address, 0); } write(address, value) { this.doIO(address, value); } doIO(address, value) : number { switch (address & 0x0f) { /* * Turn motor phases 0 to 3 on. Turning on the previous phase + 1 * increments the track position, turning on the previous phase - 1 * decrements the track position. In this scheme phase 0 and 3 are * considered to be adjacent. The previous phase number can be * computed as the track number % 4. */ case 0x1: case 0x3: case 0x5: case 0x7: var phase, lastphase, new_track; new_track = this.track; phase = (address >> 1) & 3; // if new phase is even and current phase is odd if (phase == ((new_track - 1) & 3)) { if (new_track > 0) new_track--; } else if (phase == ((new_track + 1) & 3)) { if (new_track < NUM_TRACKS*2-1) new_track++; } if ((new_track & 1) == 0) { this.track_data = this.data[new_track>>1]; console.log('track', new_track/2); } else this.track_data = null; this.track = new_track; break; /* * Turn drive motor off. */ case 0x8: this.motor = false; break; /* * Turn drive motor on. */ case 0x9: this.motor = true; break; /* * Select drive 1. */ case 0xa: //drive = 0; break; /* * Select drive 2. */ case 0xb: //drive = 1; break; /* * Select write mode. */ case 0xf: this.read_mode = false; /* * Read a disk byte if read mode is active. */ case 0xC: if (this.read_mode) return this.read_latch(); break; /* * Select read mode and read the write protect status. */ case 0xE: this.read_mode = true; /* * Write a disk byte if write mode is active and the disk is not * write protected. */ case 0xD: if (value >= 0 && !this.read_mode && !this.write_protect) this.write_latch(value); /* * Read the write protect status only. */ return this.write_protect ? 0x80 : 0x00; } return this.emu.noise(); } } /* --------------- TRACK CONVERSION ROUTINES ---------------------- */ /* * Normal byte (lower six bits only) -> disk byte translation table. */ const byte_translation = [ 0x96, 0x97, 0x9a, 0x9b, 0x9d, 0x9e, 0x9f, 0xa6, 0xa7, 0xab, 0xac, 0xad, 0xae, 0xaf, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb9, 0xba, 0xbb, 0xbc, 0xbd, 0xbe, 0xbf, 0xcb, 0xcd, 0xce, 0xcf, 0xd3, 0xd6, 0xd7, 0xd9, 0xda, 0xdb, 0xdc, 0xdd, 0xde, 0xdf, 0xe5, 0xe6, 0xe7, 0xe9, 0xea, 0xeb, 0xec, 0xed, 0xee, 0xef, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff ]; /* * Sector skewing table. */ const skewing_table = [ 0,7,14,6,13,5,12,4,11,3,10,2,9,1,8,15 ]; /* * Encode a 256-byte sector as SECTOR_SIZE disk bytes as follows: * * 14 sync bytes * 3 address header bytes * 8 address block bytes * 3 address trailer bytes * 6 sync bytes * 3 data header bytes * 343 data block bytes * 3 data trailer bytes */ function nibblizeSector(vol, trk, sector, inn, in_ofs, out, i) { var loop, checksum, prev_value, value; var sector_buffer = new Uint8Array(258); value = 0; /* * Step 1: write 6 sync bytes (0xff's). Normally these would be * written as 10-bit bytes with two extra zero bits, but for the * purpose of emulation normal 8-bit bytes will do, since the * emulated drive will always be in sync. */ for (loop = 0; loop < 14; loop++) out[i++] = 0xff; /* * Step 2: write the 3-byte address header (0xd5 0xaa 0x96). */ out[i++] = 0xd5; out[i++] = 0xaa; out[i++] = 0x96; /* * Step 3: write the address block. Use 4-and-4 encoding to convert * the volume, track and sector and checksum into 2 disk bytes each. * The checksum is a simple exclusive OR of the first three values. */ out[i++] = ((vol >> 1) | 0xaa); out[i++] = (vol | 0xaa); checksum = vol; out[i++] = ((trk >> 1) | 0xaa); out[i++] = (trk | 0xaa); checksum ^= trk; out[i++] = ((sector >> 1) | 0xaa); out[i++] = (sector | 0xaa); checksum ^= sector; out[i++] = ((checksum >> 1) | 0xaa); out[i++] = (checksum | 0xaa); /* * Step 4: write the 3-byte address trailer (0xde 0xaa 0xeb). */ out[i++] = (0xde); out[i++] = (0xaa); out[i++] = (0xeb); /* * Step 5: write another 6 sync bytes. */ for (loop = 0; loop < 6; loop++) out[i++] = (0xff); /* * Step 6: write the 3-byte data header. */ out[i++] = (0xd5); out[i++] = (0xaa); out[i++] = (0xad); /* * Step 7: read the next 256-byte sector from the old disk image file, * and add two zero bytes to bring the number of bytes up to a multiple * of 3. */ for (loop = 0; loop < 256; loop++) sector_buffer[loop] = inn[loop + in_ofs] & 0xff; sector_buffer[256] = 0; sector_buffer[257] = 0; /* * Step 8: write the first 86 disk bytes of the data block, which * encodes the bottom two bits of each sector byte into six-bit * values as follows: * * disk byte n, bit 0 = sector byte n, bit 1 * disk byte n, bit 1 = sector byte n, bit 0 * disk byte n, bit 2 = sector byte n + 86, bit 1 * disk byte n, bit 3 = sector byte n + 86, bit 0 * disk byte n, bit 4 = sector byte n + 172, bit 1 * disk byte n, bit 5 = sector byte n + 172, bit 0 * * The scheme allows each pair of bits to be shifted to the right out * of the disk byte, then shifted to the left into the sector byte. * * Before the 6-bit value is translated to a disk byte, it is exclusive * ORed with the previous 6-bit value, hence the values written are * really a running checksum. */ prev_value = 0; for (loop = 0; loop < 86; loop++) { value = (sector_buffer[loop] & 0x01) << 1; value |= (sector_buffer[loop] & 0x02) >> 1; value |= (sector_buffer[loop + 86] & 0x01) << 3; value |= (sector_buffer[loop + 86] & 0x02) << 1; value |= (sector_buffer[loop + 172] & 0x01) << 5; value |= (sector_buffer[loop + 172] & 0x02) << 3; out[i++] = (byte_translation[value ^ prev_value]); prev_value = value; } /* * Step 9: write the last 256 disk bytes of the data block, which * encodes the top six bits of each sector byte. Again, each value * is exclusive ORed with the previous value to create a running * checksum (the first value is exclusive ORed with the last value of * the previous step). */ for (loop = 0; loop < 256; loop++) { value = (sector_buffer[loop] >> 2); out[i++] = (byte_translation[value ^ prev_value]); prev_value = value; } /* * Step 10: write the last value as the checksum. */ out[i++] = (byte_translation[value]); /* * Step 11: write the 3-byte data trailer. */ out[i++] = (0xde); out[i++] = (0xaa); out[i++] = (0xeb); } function nibblizeTrack(vol, trk, inn) { var out = new Uint8Array(TRACK_SIZE); var out_pos = 0; for (var sector = 0; sector < 16; sector++) { nibblizeSector(vol, trk, sector, inn, skewing_table[sector] << 8, out, out_pos); out_pos += SECTOR_SIZE; } while (out_pos < TRACK_SIZE) out[out_pos++] = (0xff); return out; }