mirror of
https://github.com/whscullin/apple2js.git
synced 2024-01-12 14:14:38 +00:00
04ae0327c2
This adds both the recommended TypeScript checks, plus the recommended TypeScript checks that require type checking. This latter addition means that eslint essentially has to compile all of the TypeScript in the project, causing it to be slower. This isn't much of a problem in VS Code because there's a lot of caching being done, but it's clearly slower when run on the commandline. All of the errors are either fixed or suppressed. Some errors are suppressed because fixing them would be too laborious for the little value gained. The eslint config is also slightly refactored to separate the strictly TypeScript checks from the JavaScript checks.
486 lines
15 KiB
TypeScript
486 lines
15 KiB
TypeScript
import CPU6502 from './cpu6502';
|
|
import { Card, Memory, MemoryPages, TapeData, byte, Restorable } from './types';
|
|
import { debug, garbage } from './util';
|
|
import { VideoModes } from './videomodes';
|
|
|
|
export type slot = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;
|
|
export type button = 0 | 1 | 2;
|
|
export type paddle = 0 | 1 | 2 | 3;
|
|
export type annunciator = 0 | 1 | 2 | 3;
|
|
|
|
type Annunciators = Record<annunciator, boolean>;
|
|
|
|
export interface Apple2IOState {
|
|
annunciators: Annunciators;
|
|
cards: Array<unknown | null>;
|
|
}
|
|
|
|
export type SampleListener = (sample: number[]) => void;
|
|
|
|
const LOC = {
|
|
KEYBOARD: 0x00, // keyboard data (latched) (Read),
|
|
STROBE: 0x10, // clear bit 7 of keyboard data ($C000)
|
|
TAPEOUT: 0x20, // toggle the cassette output.
|
|
SPEAKER: 0x30, // toggle speaker diaphragm
|
|
C040STB: 0x40, // trigger game port sync
|
|
CLRTEXT: 0x50, // display graphics
|
|
SETTEXT: 0x51, // display text
|
|
CLRMIXED: 0x52, // clear mixed mode- enable full graphics
|
|
SETMIXED: 0x53, // enable graphics/text mixed mode
|
|
PAGE1: 0x54, // select text/graphics page1
|
|
PAGE2: 0x55, // select text/graphics page2
|
|
CLRHIRES: 0x56, // select Lo-res
|
|
SETHIRES: 0x57, // select Hi-res
|
|
CLRAN0: 0x58, // Set annunciator-0 output to 0
|
|
SETAN0: 0x59, // Set annunciator-0 output to 1
|
|
CLRAN1: 0x5A, // Set annunciator-1 output to 0
|
|
SETAN1: 0x5B, // Set annunciator-1 output to 1
|
|
CLRAN2: 0x5C, // Set annunciator-2 output to 0
|
|
SETAN2: 0x5D, // Set annunciator-2 output to 1
|
|
CLRAN3: 0x5E, // Set annunciator-3 output to 0
|
|
SETAN3: 0x5F, // Set annunciator-3 output to 1
|
|
TAPEIN: 0x60, // bit 7: data from cassette
|
|
PB0: 0x61, // game Pushbutton 0 / open apple (command) key data
|
|
PB1: 0x62, // game Pushbutton 1 / closed apple (option) key data
|
|
PB2: 0x63, // game Pushbutton 2 (read)
|
|
PADDLE0: 0x64, // bit 7: status of pdl-0 timer (read)
|
|
PADDLE1: 0x65, // bit 7: status of pdl-1 timer (read)
|
|
PADDLE2: 0x66, // bit 7: status of pdl-2 timer (read)
|
|
PADDLE3: 0x67, // bit 7: status of pdl-3 timer (read)
|
|
PDLTRIG: 0x70, // trigger paddles
|
|
ACCEL: 0x74, // CPU Speed control
|
|
};
|
|
|
|
export default class Apple2IO implements MemoryPages, Restorable<Apple2IOState> {
|
|
private _slot: Array<Card | null> = new Array<Card | null>(7).fill(null);
|
|
private _auxRom: Memory | null = null;
|
|
|
|
private _khz = 1023;
|
|
private _rate = 44000;
|
|
private _sample_size = 4096;
|
|
|
|
private _cycles_per_sample: number;
|
|
|
|
private _buffer: string[] = [];
|
|
private _key = 0;
|
|
private _keyDown = false;
|
|
private _button = [false, false, false];
|
|
private _paddle = [0.0, 0.0, 0.0, 0.0];
|
|
private _phase = -1;
|
|
private _sample: number[] = [];
|
|
private _sampleIdx = 0;
|
|
private _sampleTime = 0;
|
|
private _didAudio = false;
|
|
|
|
private _high = 0.5;
|
|
private _low = -0.5;
|
|
|
|
private _audioListener: SampleListener | undefined;
|
|
|
|
private _trigger = 0;
|
|
private _annunciators: Annunciators = [false, false, false, false];
|
|
|
|
private _tape: TapeData = [];
|
|
private _tapeOffset: number = 0;
|
|
private _tapeNext: number = 0;
|
|
private _tapeCurrent = false;
|
|
|
|
constructor(private readonly cpu: CPU6502, private readonly vm: VideoModes) {
|
|
this.init();
|
|
}
|
|
|
|
init() {
|
|
this._calcSampleRate();
|
|
}
|
|
|
|
_debug(..._args: unknown[]) {
|
|
// debug.apply(this, arguments);
|
|
}
|
|
|
|
_tick() {
|
|
const now = this.cpu.getCycles();
|
|
const phase = this._didAudio ? (this._phase > 0 ? this._high : this._low) : 0.0;
|
|
for (; this._sampleTime < now; this._sampleTime += this._cycles_per_sample) {
|
|
this._sample[this._sampleIdx++] = phase;
|
|
if (this._sampleIdx === this._sample_size) {
|
|
if (this._audioListener) {
|
|
this._audioListener(this._sample);
|
|
}
|
|
this._sample = new Array<number>(this._sample_size);
|
|
this._sampleIdx = 0;
|
|
}
|
|
}
|
|
this._didAudio = false;
|
|
}
|
|
|
|
_calcSampleRate() {
|
|
this._cycles_per_sample = this._khz * 1000 / this._rate;
|
|
}
|
|
|
|
_updateKHz(khz: number) {
|
|
this._khz = khz;
|
|
this._calcSampleRate();
|
|
}
|
|
|
|
_access(off: byte, val?: byte): byte | undefined {
|
|
let result: number | undefined = 0;
|
|
const now = this.cpu.getCycles();
|
|
const writeMode = val === undefined;
|
|
const delta = now - this._trigger;
|
|
switch (off) {
|
|
case LOC.CLRTEXT:
|
|
this._debug('Graphics Mode');
|
|
this.vm.text(false);
|
|
break;
|
|
case LOC.SETTEXT:
|
|
this._debug('Text Mode');
|
|
this.vm.text(true);
|
|
break;
|
|
case LOC.CLRMIXED:
|
|
this._debug('Mixed Mode off');
|
|
this.vm.mixed(false);
|
|
break;
|
|
case LOC.SETMIXED:
|
|
this._debug('Mixed Mode on');
|
|
this.vm.mixed(true);
|
|
break;
|
|
case LOC.CLRHIRES:
|
|
this._debug('LoRes Mode');
|
|
this.vm.hires(false);
|
|
break;
|
|
case LOC.SETHIRES:
|
|
this._debug('HiRes Mode');
|
|
this.vm.hires(true);
|
|
break;
|
|
case LOC.PAGE1:
|
|
this.vm.page(1);
|
|
break;
|
|
case LOC.PAGE2:
|
|
this.vm.page(2);
|
|
break;
|
|
case LOC.SETAN0:
|
|
this._debug('Annunciator 0 on');
|
|
this._annunciators[0] = true;
|
|
break;
|
|
case LOC.SETAN1:
|
|
this._debug('Annunciator 1 on');
|
|
this._annunciators[1] = true;
|
|
break;
|
|
case LOC.SETAN2:
|
|
this._debug('Annunciator 2 on');
|
|
this._annunciators[2] = true;
|
|
break;
|
|
case LOC.SETAN3:
|
|
this._debug('Annunciator 3 on');
|
|
this._annunciators[3] = true;
|
|
break;
|
|
case LOC.CLRAN0:
|
|
this._debug('Annunciator 0 off');
|
|
this._annunciators[0] = false;
|
|
break;
|
|
case LOC.CLRAN1:
|
|
this._debug('Annunciator 1 off');
|
|
this._annunciators[1] = false;
|
|
break;
|
|
case LOC.CLRAN2:
|
|
this._debug('Annunciator 2 off');
|
|
this._annunciators[2] = false;
|
|
break;
|
|
case LOC.CLRAN3:
|
|
this._debug('Annunciator 3 off');
|
|
this._annunciators[3] = false;
|
|
break;
|
|
case LOC.PB0:
|
|
result = this._button[0] ? 0x80 : 0;
|
|
break;
|
|
case LOC.PB1:
|
|
result = this._button[1] ? 0x80 : 0;
|
|
break;
|
|
case LOC.PB2:
|
|
result = this._button[2] ? 0x80 : 0;
|
|
break;
|
|
case LOC.PADDLE0:
|
|
result = (delta < (this._paddle[0] * 2756) ? 0x80 : 0x00);
|
|
break;
|
|
case LOC.PADDLE1:
|
|
result = (delta < (this._paddle[1] * 2756) ? 0x80 : 0x00);
|
|
break;
|
|
case LOC.PADDLE2:
|
|
result = (delta < (this._paddle[2] * 2756) ? 0x80 : 0x00);
|
|
break;
|
|
case LOC.PADDLE3:
|
|
result = (delta < (this._paddle[3] * 2756) ? 0x80 : 0x00);
|
|
break;
|
|
case LOC.ACCEL:
|
|
if (val !== undefined) {
|
|
this._updateKHz(val & 0x01 ? 1023 : 4096);
|
|
}
|
|
break;
|
|
case LOC.TAPEIN:
|
|
if (this._tapeOffset === -1) {
|
|
this._tapeOffset = 0;
|
|
this._tapeNext = now;
|
|
}
|
|
|
|
if (this._tapeOffset < this._tape.length) {
|
|
this._tapeCurrent = this._tape[this._tapeOffset][1];
|
|
while (now >= this._tapeNext) {
|
|
if ((this._tapeOffset % 1000) === 0) {
|
|
debug(`Read ${this._tapeOffset / 1000}`);
|
|
}
|
|
this._tapeCurrent = this._tape[this._tapeOffset][1];
|
|
this._tapeNext += this._tape[this._tapeOffset++][0];
|
|
}
|
|
|
|
}
|
|
|
|
result = this._tapeCurrent ? 0x80 : 0x00;
|
|
break;
|
|
|
|
default:
|
|
switch (off & 0xf0) {
|
|
case LOC.KEYBOARD: // C00x
|
|
result = this._key;
|
|
break;
|
|
case LOC.STROBE: // C01x
|
|
if (off === LOC.STROBE || writeMode) {
|
|
this._key &= 0x7f;
|
|
}
|
|
if (this._buffer.length > 0) {
|
|
let val = this._buffer.shift() as string;
|
|
if (val === '\n') {
|
|
val = '\r';
|
|
}
|
|
this._key = val.charCodeAt(0) | 0x80;
|
|
}
|
|
result = this._key & 0x7f;
|
|
if (off === LOC.STROBE) {
|
|
result |= this._keyDown ? 0x80 : 0x00;
|
|
}
|
|
break;
|
|
case LOC.TAPEOUT: // C02x
|
|
// this._phase = -this._phase;
|
|
// this._didAudio = true;
|
|
// this._tick();
|
|
break;
|
|
case LOC.SPEAKER: // C03x
|
|
this._phase = -this._phase;
|
|
this._didAudio = true;
|
|
this._tick();
|
|
break;
|
|
case LOC.C040STB: // C04x
|
|
// I/O Strobe
|
|
break;
|
|
case LOC.PDLTRIG: // C07x
|
|
this._trigger = this.cpu.getCycles();
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (val !== undefined) {
|
|
result = undefined;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
start() {
|
|
return 0xc0;
|
|
}
|
|
|
|
end() {
|
|
return 0xcf;
|
|
}
|
|
|
|
ioSwitch(off: byte, val?: byte) {
|
|
let result;
|
|
if (off < 0x80) {
|
|
result = this._access(off, val);
|
|
} else {
|
|
const slot = (off & 0x70) >> 4;
|
|
const card = this._slot[slot];
|
|
if (card && card.ioSwitch) {
|
|
result = card.ioSwitch(off, val);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
reset() {
|
|
for (let slot = 0; slot < 8; slot++) {
|
|
const card = this._slot[slot];
|
|
if (card) {
|
|
card.reset?.();
|
|
}
|
|
}
|
|
this.vm.reset();
|
|
}
|
|
|
|
blit() {
|
|
const card = this._slot[3];
|
|
if (card) {
|
|
return card.blit?.();
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
read(page: byte, off: byte) {
|
|
let result: number = 0;
|
|
let slot;
|
|
let card;
|
|
|
|
switch (page) {
|
|
case 0xc0:
|
|
result = this.ioSwitch(off, undefined) || 0;
|
|
break;
|
|
case 0xc1:
|
|
case 0xc2:
|
|
case 0xc3:
|
|
case 0xc4:
|
|
case 0xc5:
|
|
case 0xc6:
|
|
case 0xc7:
|
|
slot = page & 0x0f;
|
|
card = this._slot[slot];
|
|
if (this._auxRom !== card) {
|
|
// _debug('Setting auxRom to slot', slot);
|
|
this._auxRom = card;
|
|
}
|
|
if (card) {
|
|
result = card.read(page, off);
|
|
} else {
|
|
result = garbage();
|
|
}
|
|
break;
|
|
default:
|
|
if (this._auxRom) {
|
|
result = this._auxRom.read(page, off);
|
|
}
|
|
break;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
write(page: byte, off: byte, val: byte) {
|
|
let slot;
|
|
let card;
|
|
|
|
switch (page) {
|
|
case 0xc0:
|
|
this.ioSwitch(off, val);
|
|
break;
|
|
case 0xc1:
|
|
case 0xc2:
|
|
case 0xc3:
|
|
case 0xc4:
|
|
case 0xc5:
|
|
case 0xc6:
|
|
case 0xc7:
|
|
slot = page & 0x0f;
|
|
card = this._slot[slot];
|
|
if (this._auxRom !== card) {
|
|
// _debug('Setting auxRom to slot', slot);
|
|
this._auxRom = card;
|
|
}
|
|
if (card) {
|
|
card.write(page, off, val);
|
|
}
|
|
break;
|
|
default:
|
|
if (this._auxRom) {
|
|
this._auxRom.write(page, off, val);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
getState(): Apple2IOState {
|
|
// TODO vet more potential state
|
|
return {
|
|
annunciators: this._annunciators,
|
|
cards: this._slot.map((card) => card ? card.getState() : null)
|
|
};
|
|
}
|
|
|
|
setState(state: Apple2IOState) {
|
|
this._annunciators = state.annunciators;
|
|
state.cards.map((cardState, idx) => this._slot[idx]?.setState(cardState));
|
|
}
|
|
|
|
setSlot(slot: slot, card: Card) {
|
|
this._slot[slot] = card;
|
|
}
|
|
|
|
keyDown(ascii: byte) {
|
|
this._keyDown = true;
|
|
this._key = ascii | 0x80;
|
|
}
|
|
|
|
keyUp() {
|
|
this._keyDown = false;
|
|
}
|
|
|
|
buttonDown(b: button) {
|
|
this._button[b] = true;
|
|
}
|
|
|
|
buttonUp(b: button) {
|
|
this._button[b] = false;
|
|
}
|
|
|
|
paddle(p: paddle, v: byte) {
|
|
this._paddle[p] = v;
|
|
}
|
|
|
|
updateKHz(khz: number) {
|
|
this._updateKHz(khz);
|
|
}
|
|
|
|
getKHz() {
|
|
return this._khz;
|
|
}
|
|
|
|
setKeyBuffer(buffer: string) {
|
|
this._buffer = buffer.split(''); // split to charaters
|
|
if (this._buffer.length > 0) {
|
|
this._keyDown = true;
|
|
const key = this._buffer.shift() as string; // never undefined
|
|
this._key = key.charCodeAt(0) | 0x80;
|
|
}
|
|
}
|
|
|
|
setTape(tape: TapeData) {
|
|
debug(`Tape length: ${tape.length}`);
|
|
this._tape = tape;
|
|
this._tapeOffset = -1;
|
|
}
|
|
|
|
sampleRate(rate: number, sample_size: number) {
|
|
this._rate = rate;
|
|
this._sample_size = sample_size;
|
|
this._sample = new Array<number>(this._sample_size);
|
|
this._sampleIdx = 0;
|
|
this._calcSampleRate();
|
|
}
|
|
|
|
tick() {
|
|
this._tick();
|
|
for (let idx = 0; idx < 8; idx++) {
|
|
this._slot[idx]?.tick?.();
|
|
}
|
|
}
|
|
|
|
addSampleListener(cb: SampleListener) {
|
|
this._audioListener = cb;
|
|
}
|
|
|
|
annunciator(idx: annunciator) {
|
|
return this._annunciators[idx];
|
|
}
|
|
|
|
cycles() {
|
|
return this.cpu.getCycles();
|
|
}
|
|
}
|