mirror of
https://github.com/whscullin/apple2js.git
synced 2024-01-12 14:14:38 +00:00
Typescriptify all of the UI peripherals (#73)
* Typescriptify all of the UI peripherals This converts the audio, gamepad, keyboard, printer, and tape peripherals into Typescript. This is a pretty mechanical change. It does fix issue #72, though. * Add and that were missing
This commit is contained in:
parent
08ff6f93d5
commit
4d7068208a
23
js/types.ts
23
js/types.ts
@ -9,6 +9,29 @@
|
||||
export type MemberOf<T extends ReadonlyArray<unknown>> =
|
||||
T extends ReadonlyArray<infer E> ? E : never;
|
||||
|
||||
/**
|
||||
* Recursively extracts all members of a constant array as a type. Used as:
|
||||
*
|
||||
* @example
|
||||
* const SOME_ARRAYS = [['a'],['b', 2], 3] as const;
|
||||
* type SomeArrayValues = DeepMemberOf<typeof SOME_ARRAYS>; // 'a' | 'b' | 2 | 3
|
||||
*/
|
||||
export type DeepMemberOf<T extends ReadonlyArray<unknown>> =
|
||||
T extends ReadonlyArray<infer E>
|
||||
? (E extends ReadonlyArray<unknown> ? DeepMemberOf<E> : E)
|
||||
: never;
|
||||
|
||||
/**
|
||||
* Extracts the declared keys of a type by removing `string` and `number`.
|
||||
*
|
||||
* Cribbed from the interwebs:
|
||||
* https://github.com/microsoft/TypeScript/issues/25987#issuecomment-408339599
|
||||
*/
|
||||
export type KnownKeys<T> = {
|
||||
[K in keyof T]: string extends K ? never : number extends K ? never : K
|
||||
} extends { [_ in keyof T]: infer U } ? U : never;
|
||||
|
||||
|
||||
/** A bit. */
|
||||
export type bit = 0 | 1;
|
||||
|
||||
|
@ -1,93 +0,0 @@
|
||||
/* Copyright 2010-2019 Will Scullin <scullin@scullinsteel.com>
|
||||
*
|
||||
* Permission to use, copy, modify, distribute, and sell this software and its
|
||||
* documentation for any purpose is hereby granted without fee, provided that
|
||||
* the above copyright notice appear in all copies and that both that
|
||||
* copyright notice and this permission notice appear in supporting
|
||||
* documentation. No representations are made about the suitability of this
|
||||
* software for any purpose. It is provided "as is" without express or
|
||||
* implied warranty.
|
||||
*/
|
||||
|
||||
import { debug } from '../util';
|
||||
|
||||
/*
|
||||
* Audio Handling
|
||||
*/
|
||||
|
||||
var SAMPLE_SIZE = 1024;
|
||||
var SAMPLE_RATE = 44000;
|
||||
|
||||
export default function Audio(io) {
|
||||
var sound = true;
|
||||
var _samples = [];
|
||||
|
||||
var audioContext;
|
||||
var AudioContext = window.AudioContext || window.webkitAudioContext;
|
||||
var audioNode;
|
||||
var started = false;
|
||||
|
||||
if (AudioContext) {
|
||||
audioContext = new AudioContext({
|
||||
sampleRate: SAMPLE_RATE
|
||||
});
|
||||
audioNode = audioContext.createScriptProcessor(SAMPLE_SIZE, 1, 1);
|
||||
|
||||
audioNode.onaudioprocess = function(event) {
|
||||
var data = event.outputBuffer.getChannelData(0);
|
||||
var sample = _samples.shift();
|
||||
var idx = 0;
|
||||
|
||||
var len = data.length;
|
||||
if (sample) {
|
||||
len = Math.min(sample.length, len);
|
||||
for (; idx < len; idx++) {
|
||||
data[idx] = sample[idx];
|
||||
}
|
||||
}
|
||||
|
||||
for (; idx < data.length; idx++) {
|
||||
data[idx] = 0.0;
|
||||
}
|
||||
};
|
||||
|
||||
audioNode.connect(audioContext.destination);
|
||||
}
|
||||
|
||||
function _initAudio(io) {
|
||||
if (audioContext) {
|
||||
debug('Using Webkit Audio');
|
||||
io.sampleRate(audioContext.sampleRate, SAMPLE_SIZE);
|
||||
io.addSampleListener(function(sample) {
|
||||
if (sound) {
|
||||
if (_samples.length < 5) {
|
||||
_samples.push(sample);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_initAudio(io);
|
||||
|
||||
return {
|
||||
autoStart: function () {
|
||||
if (audioContext && !started) {
|
||||
_samples = [];
|
||||
audioContext.resume();
|
||||
started = true;
|
||||
}
|
||||
},
|
||||
|
||||
start: function () {
|
||||
if (audioContext) {
|
||||
_samples = [];
|
||||
audioContext.resume();
|
||||
}
|
||||
},
|
||||
|
||||
enable: function(enable) {
|
||||
sound = enable;
|
||||
}
|
||||
};
|
||||
}
|
89
js/ui/audio.ts
Normal file
89
js/ui/audio.ts
Normal file
@ -0,0 +1,89 @@
|
||||
/* Copyright 2010-2019 Will Scullin <scullin@scullinsteel.com>
|
||||
*
|
||||
* Permission to use, copy, modify, distribute, and sell this software and its
|
||||
* documentation for any purpose is hereby granted without fee, provided that
|
||||
* the above copyright notice appear in all copies and that both that
|
||||
* copyright notice and this permission notice appear in supporting
|
||||
* documentation. No representations are made about the suitability of this
|
||||
* software for any purpose. It is provided "as is" without express or
|
||||
* implied warranty.
|
||||
*/
|
||||
|
||||
import Apple2IO from '../apple2io';
|
||||
import { debug } from '../util';
|
||||
|
||||
/*
|
||||
* Audio Handling
|
||||
*/
|
||||
|
||||
const SAMPLE_SIZE = 1024;
|
||||
const SAMPLE_RATE = 44000;
|
||||
|
||||
export default class Audio {
|
||||
private sound = true;
|
||||
private samples: number[][] = [];
|
||||
|
||||
private audioContext;
|
||||
private audioNode;
|
||||
private started = false;
|
||||
|
||||
constructor(io: Apple2IO) {
|
||||
this.audioContext = new AudioContext({
|
||||
sampleRate: SAMPLE_RATE
|
||||
});
|
||||
|
||||
// TODO(flan): MDN says that createScriptProcessor is deprecated and
|
||||
// replaced by AudioWorklet. FF and Chrome support AudioWorklet, but
|
||||
// Safari does not (yet).
|
||||
this.audioNode = this.audioContext.createScriptProcessor(SAMPLE_SIZE, 1, 1);
|
||||
|
||||
this.audioNode.onaudioprocess = (event) => {
|
||||
const data = event.outputBuffer.getChannelData(0);
|
||||
const sample = this.samples.shift();
|
||||
let idx = 0;
|
||||
let len = data.length;
|
||||
|
||||
if (sample) {
|
||||
len = Math.min(sample.length, len);
|
||||
for (; idx < len; idx++) {
|
||||
data[idx] = sample[idx];
|
||||
}
|
||||
}
|
||||
|
||||
for (; idx < data.length; idx++) {
|
||||
data[idx] = 0.0;
|
||||
}
|
||||
};
|
||||
|
||||
this.audioNode.connect(this.audioContext.destination);
|
||||
io.sampleRate(this.audioContext.sampleRate, SAMPLE_SIZE);
|
||||
io.addSampleListener((sample: number[]) => {
|
||||
if (this.sound) {
|
||||
if (this.samples.length < 5) {
|
||||
this.samples.push(sample);
|
||||
}
|
||||
}
|
||||
});
|
||||
debug('Sound initialized');
|
||||
}
|
||||
|
||||
|
||||
autoStart() {
|
||||
if (this.audioContext && !this.started) {
|
||||
this.samples = [];
|
||||
this.audioContext.resume();
|
||||
this.started = true;
|
||||
}
|
||||
}
|
||||
|
||||
start() {
|
||||
if (this.audioContext) {
|
||||
this.samples = [];
|
||||
this.audioContext.resume();
|
||||
}
|
||||
}
|
||||
|
||||
enable(enable: boolean) {
|
||||
this.sound = enable;
|
||||
}
|
||||
}
|
122
js/ui/gamepad.js
122
js/ui/gamepad.js
@ -1,122 +0,0 @@
|
||||
/* Copyright 2010-2019 Will Scullin <scullin@scullinsteel.com>
|
||||
*
|
||||
* Permission to use, copy, modify, distribute, and sell this software and its
|
||||
* documentation for any purpose is hereby granted without fee, provided that
|
||||
* the above copyright notice appear in all copies and that both that
|
||||
* copyright notice and this permission notice appear in supporting
|
||||
* documentation. No representations are made about the suitability of this
|
||||
* software for any purpose. It is provided "as is" without express or
|
||||
* implied warranty.
|
||||
*/
|
||||
|
||||
var getGamepads = navigator.getGamepads || navigator.webkitGetGamepads;
|
||||
export var gamepad;
|
||||
var gamepadMap = [];
|
||||
var gamepadState = [];
|
||||
var flipX = false;
|
||||
var flipY = false;
|
||||
|
||||
var BUTTON = {
|
||||
// Buttons
|
||||
'A': 0,
|
||||
'B': 1,
|
||||
'X': 2,
|
||||
'Y': 3,
|
||||
|
||||
// Triggers
|
||||
'L1': 4,
|
||||
'R1': 5,
|
||||
|
||||
// Analog stick buttons
|
||||
'L3': 6,
|
||||
'R3': 7,
|
||||
|
||||
// Special
|
||||
'START': 8,
|
||||
'SELECT': 9,
|
||||
'LOGO': 10,
|
||||
|
||||
// D pad
|
||||
'UP': 11,
|
||||
'DOWN': 12,
|
||||
'LEFT': 13,
|
||||
'RIGHT': 14
|
||||
};
|
||||
|
||||
var DEFAULT_GAMEPAD = {
|
||||
'A': 0,
|
||||
'B': 1,
|
||||
'L1': 0,
|
||||
'R1': 1,
|
||||
'START': 0x1B
|
||||
};
|
||||
|
||||
window.addEventListener('gamepadconnected', function(e) {
|
||||
gamepad = e.gamepad;
|
||||
});
|
||||
|
||||
export function processGamepad(io) {
|
||||
if (getGamepads) {
|
||||
gamepad = getGamepads.call(navigator)[0];
|
||||
}
|
||||
if (gamepad) {
|
||||
var x = (gamepad.axes[0] * 1.414 + 1) / 2.0;
|
||||
var y = (gamepad.axes[1] * 1.414 + 1) / 2.0;
|
||||
io.paddle(0, flipX ? 1.0 - x : x);
|
||||
io.paddle(1, flipY ? 1.0 - y : y);
|
||||
var val;
|
||||
for (var idx = 0; idx < gamepad.buttons.length; idx++) {
|
||||
val = gamepadMap[idx];
|
||||
if (val !== undefined) {
|
||||
var old = gamepadState[idx];
|
||||
var button = gamepad.buttons[idx];
|
||||
var pressed;
|
||||
if (typeof(button) == 'object') {
|
||||
pressed = button.pressed;
|
||||
} else {
|
||||
pressed = (button == 1.0);
|
||||
}
|
||||
|
||||
if (pressed && !old) {
|
||||
if (val <= 0) {
|
||||
io.buttonDown(-val);
|
||||
} else {
|
||||
io.keyDown(gamepadMap[idx]);
|
||||
}
|
||||
} else if (!pressed && old) {
|
||||
if (val <= 0) {
|
||||
io.buttonUp(-val);
|
||||
} else {
|
||||
io.keyUp();
|
||||
}
|
||||
}
|
||||
gamepadState[idx] = pressed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function configGamepad(configFlipX, configFlipY) {
|
||||
flipX = configFlipX;
|
||||
flipY = configFlipY;
|
||||
}
|
||||
|
||||
export function initGamepad(data) {
|
||||
for (var idx = 0; idx < 16; idx++) {
|
||||
gamepadMap[idx] = undefined;
|
||||
}
|
||||
var map = data || DEFAULT_GAMEPAD;
|
||||
Object.keys(map).forEach(function(key) {
|
||||
var val = map[key];
|
||||
if (typeof val == 'string') {
|
||||
val = val.charCodeAt(0);
|
||||
} else {
|
||||
val = -val;
|
||||
}
|
||||
if (key in BUTTON) {
|
||||
gamepadMap[BUTTON[key]] = val;
|
||||
} else {
|
||||
gamepadMap[parseInt(key, 10)] = val;
|
||||
}
|
||||
});
|
||||
}
|
152
js/ui/gamepad.ts
Normal file
152
js/ui/gamepad.ts
Normal file
@ -0,0 +1,152 @@
|
||||
/* Copyright 2010-2019 Will Scullin <scullin@scullinsteel.com>
|
||||
*
|
||||
* Permission to use, copy, modify, distribute, and sell this software and its
|
||||
* documentation for any purpose is hereby granted without fee, provided that
|
||||
* the above copyright notice appear in all copies and that both that
|
||||
* copyright notice and this permission notice appear in supporting
|
||||
* documentation. No representations are made about the suitability of this
|
||||
* software for any purpose. It is provided "as is" without express or
|
||||
* implied warranty.
|
||||
*/
|
||||
|
||||
import Apple2IO from '../apple2io';
|
||||
import { KnownKeys } from '../types';
|
||||
|
||||
export let gamepad: Gamepad | null = null;
|
||||
|
||||
const BUTTON = {
|
||||
// Buttons
|
||||
'A': 0,
|
||||
'B': 1,
|
||||
'X': 2,
|
||||
'Y': 3,
|
||||
|
||||
// Triggers
|
||||
'L1': 4,
|
||||
'R1': 5,
|
||||
|
||||
// Analog stick buttons
|
||||
'L3': 6,
|
||||
'R3': 7,
|
||||
|
||||
// Special
|
||||
'START': 8,
|
||||
'SELECT': 9,
|
||||
'LOGO': 10,
|
||||
|
||||
// D pad
|
||||
'UP': 11,
|
||||
'DOWN': 12,
|
||||
'LEFT': 13,
|
||||
'RIGHT': 14
|
||||
} as const;
|
||||
|
||||
type ButtonType = KnownKeys<typeof BUTTON>;
|
||||
|
||||
/**
|
||||
* A `GamepadConfiguration` maps buttons on the controller to Apple Paddle
|
||||
* buttons or keys on the keyboard. If the value is a number, it must be
|
||||
* 0 | 1 | 2 and will map to the corresponding paddle button. If the value
|
||||
* is a string, the _first_ character of the string is used as a key to
|
||||
* press on the keyboard.
|
||||
*/
|
||||
export type GamepadConfiguration = {
|
||||
[K in ButtonType]?: 0 | 1 | 2 | string;
|
||||
};
|
||||
|
||||
const DEFAULT_GAMEPAD: GamepadConfiguration = {
|
||||
'A': 0,
|
||||
'B': 1,
|
||||
'L1': 0,
|
||||
'R1': 1,
|
||||
'START': '\x1B'
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* An array with 16 entries. For each entry _e_:
|
||||
*
|
||||
* * if _e_ <= 0, then _-e_ is 0 | 1 | 2 and represents a joystick button;
|
||||
* * if _e_ > 0, then _e_ is a key on the keyboard that is pressed;
|
||||
* * if _e_ is undefined, nothing happens.
|
||||
*/
|
||||
const gamepadMap: Array<number | undefined> = [];
|
||||
/**
|
||||
* An array with 16 entries saying whether or not the given button is
|
||||
* currently pressed.
|
||||
*/
|
||||
const gamepadState: boolean[] = [];
|
||||
let flipX = false;
|
||||
let flipY = false;
|
||||
|
||||
|
||||
window.addEventListener('gamepadconnected', function (e: GamepadEvent) {
|
||||
gamepad = e.gamepad;
|
||||
});
|
||||
|
||||
export function processGamepad(io: Apple2IO) {
|
||||
// Always use the first gamepad
|
||||
gamepad = navigator.getGamepads()[0];
|
||||
if (!gamepad) {
|
||||
return;
|
||||
}
|
||||
const x = (gamepad.axes[0] * 1.414 + 1) / 2.0;
|
||||
const y = (gamepad.axes[1] * 1.414 + 1) / 2.0;
|
||||
io.paddle(0, flipX ? 1.0 - x : x);
|
||||
io.paddle(1, flipY ? 1.0 - y : y);
|
||||
for (let idx = 0; idx < gamepad.buttons.length; idx++) {
|
||||
const val = gamepadMap[idx];
|
||||
if (val !== undefined) {
|
||||
const old = gamepadState[idx];
|
||||
const button = gamepad.buttons[idx];
|
||||
let pressed: boolean;
|
||||
if (typeof button === 'object') {
|
||||
pressed = button.pressed;
|
||||
} else {
|
||||
pressed = (button === 1.0);
|
||||
}
|
||||
|
||||
if (pressed && !old) {
|
||||
if (val <= 0) {
|
||||
io.buttonDown(-val as 0 | 1 | 2);
|
||||
} else {
|
||||
io.keyDown(gamepadMap[idx]!);
|
||||
}
|
||||
} else if (!pressed && old) {
|
||||
if (val <= 0) {
|
||||
io.buttonUp(-val as 0 | 1 | 2);
|
||||
} else {
|
||||
io.keyUp();
|
||||
}
|
||||
}
|
||||
gamepadState[idx] = pressed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function configGamepad(configFlipX: boolean, configFlipY: boolean) {
|
||||
flipX = configFlipX;
|
||||
flipY = configFlipY;
|
||||
}
|
||||
|
||||
export function initGamepad(data?: GamepadConfiguration) {
|
||||
// Clear map
|
||||
for (let idx = 0; idx < 16; idx++) {
|
||||
gamepadMap[idx] = undefined;
|
||||
}
|
||||
const map = data || DEFAULT_GAMEPAD;
|
||||
for (const entry of Object.entries(map)) {
|
||||
const key = entry[0] as ButtonType;
|
||||
const val = entry[1] as number | string;
|
||||
let mapVal;
|
||||
if (typeof val === 'string') {
|
||||
mapVal = val.charCodeAt(0);
|
||||
} else {
|
||||
mapVal = -val;
|
||||
}
|
||||
if (key in BUTTON) {
|
||||
gamepadMap[BUTTON[key]] = mapVal;
|
||||
} else {
|
||||
gamepadMap[parseInt(key, 10)] = mapVal;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,468 +0,0 @@
|
||||
/* Copyright 2010-2019 Will Scullin <scullin@scullinsteel.com>
|
||||
*
|
||||
* Permission to use, copy, modify, distribute, and sell this software and its
|
||||
* documentation for any purpose is hereby granted without fee, provided that
|
||||
* the above copyright notice appear in all copies and that both that
|
||||
* copyright notice and this permission notice appear in supporting
|
||||
* documentation. No representations are made about the suitability of this
|
||||
* software for any purpose. It is provided "as is" without express or
|
||||
* implied warranty.
|
||||
*/
|
||||
|
||||
import { debug, toHex } from '../util';
|
||||
|
||||
export default function KeyBoard(cpu, io, e) {
|
||||
// keycode: [plain, cntl, shift]
|
||||
var keymap = {
|
||||
// Most of these won't happen
|
||||
0x00: [0x00, 0x00, 0x00], //
|
||||
0x01: [0x01, 0x01, 0x01], //
|
||||
0x02: [0x02, 0x02, 0x02], //
|
||||
0x03: [0x03, 0x03, 0x03], //
|
||||
0x04: [0x04, 0x04, 0x04], //
|
||||
0x05: [0x05, 0x05, 0x05], //
|
||||
0x06: [0x06, 0x06, 0x06], //
|
||||
0x07: [0x07, 0x07, 0x07], //
|
||||
0x08: [0x7F, 0x7F, 0x7F], // BS/DELETE
|
||||
0x09: [0x09, 0x09, 0x09], // TAB
|
||||
0x0A: [0x0A, 0x0A, 0x0A], //
|
||||
0x0B: [0x0B, 0x0B, 0x0B], //
|
||||
0x0C: [0x0C, 0x0C, 0x0C], //
|
||||
0x0D: [0x0D, 0x0D, 0x0D], // CR
|
||||
0x0E: [0x0E, 0x0E, 0x0E], //
|
||||
0x0F: [0x0F, 0x0F, 0x0F], //
|
||||
|
||||
0x10: [0xff, 0xff, 0xff], // SHIFT
|
||||
0x11: [0xff, 0xff, 0xff], // CTRL
|
||||
0x12: [0xff, 0xff, 0xff], // ALT/OPTION
|
||||
0x13: [0x13, 0x13, 0x13], //
|
||||
0x14: [0x14, 0x14, 0x14], //
|
||||
0x15: [0x15, 0x15, 0x15], //
|
||||
0x16: [0x16, 0x16, 0x16], //
|
||||
0x17: [0x17, 0x17, 0x18], //
|
||||
0x18: [0x18, 0x18, 0x18], //
|
||||
0x19: [0x19, 0x19, 0x19], //
|
||||
0x1A: [0x1A, 0x1A, 0x1A], //
|
||||
0x1B: [0x1B, 0x1B, 0x1B], // ESC
|
||||
0x1C: [0x1C, 0x1C, 0x1C], //
|
||||
0x1D: [0x1D, 0x1D, 0x1D], //
|
||||
0x1E: [0x1E, 0x1E, 0x1E], //
|
||||
0x1F: [0x1F, 0x1F, 0x1F], //
|
||||
|
||||
// Most of these besides space won't happen
|
||||
0x20: [0x20, 0x20, 0x20], //
|
||||
0x21: [0x21, 0x21, 0x21], //
|
||||
0x22: [0x22, 0x22, 0x22], //
|
||||
0x23: [0x23, 0x23, 0x23], //
|
||||
0x24: [0x24, 0x24, 0x24], //
|
||||
0x25: [0x08, 0x08, 0x08], // <- left
|
||||
0x26: [0x0B, 0x0B, 0x0B], // ^ up
|
||||
0x27: [0x15, 0x15, 0x15], // -> right
|
||||
0x28: [0x0A, 0x0A, 0x0A], // v down
|
||||
0x29: [0x29, 0x29, 0x29], // )
|
||||
0x2A: [0x2A, 0x2A, 0x2A], // *
|
||||
0x2B: [0x2B, 0x2B, 0x2B], // +
|
||||
0x2C: [0x2C, 0x2C, 0x3C], // , - <
|
||||
0x2D: [0x2D, 0x2D, 0x5F], // - - _
|
||||
0x2E: [0x2E, 0x2E, 0x3E], // . - >
|
||||
0x2F: [0x2F, 0x2F, 0x3F], // / - ?
|
||||
|
||||
0x30: [0x30, 0x30, 0x29], // 0 - )
|
||||
0x31: [0x31, 0x31, 0x21], // 1 - !
|
||||
0x32: [0x32, 0x00, 0x40], // 2 - @
|
||||
0x33: [0x33, 0x33, 0x23], // 3 - #
|
||||
0x34: [0x34, 0x34, 0x24], // 4 - $
|
||||
0x35: [0x35, 0x35, 0x25], // 5 - %
|
||||
0x36: [0x36, 0x36, 0x5E], // 6 - ^
|
||||
0x37: [0x37, 0x37, 0x26], // 7 - &
|
||||
0x38: [0x38, 0x38, 0x2A], // 8 - *
|
||||
0x39: [0x39, 0x39, 0x28], // 9 - (
|
||||
0x3A: [0x3A, 0x3A, 0x3A], // :
|
||||
0x3B: [0x3B, 0x3B, 0x3A], // ; - :
|
||||
0x3C: [0x3C, 0x3C, 0x3C], // <
|
||||
0x3D: [0x3D, 0x3D, 0x2B], // = - +
|
||||
0x3E: [0x3E, 0x3E, 0x3E], // >
|
||||
0x3F: [0x3F, 0x3F, 0x3F], // ?
|
||||
|
||||
// Alpha and control
|
||||
0x40: [0x40, 0x00, 0x40], // @
|
||||
0x41: [0x61, 0x01, 0x41], // A
|
||||
0x42: [0x62, 0x02, 0x42], // B
|
||||
0x43: [0x63, 0x03, 0x43], // C - BRK
|
||||
0x44: [0x64, 0x04, 0x44], // D
|
||||
0x45: [0x65, 0x05, 0x45], // E
|
||||
0x46: [0x66, 0x06, 0x46], // F
|
||||
0x47: [0x67, 0x07, 0x47], // G - BELL
|
||||
0x48: [0x68, 0x08, 0x48], // H
|
||||
0x49: [0x69, 0x09, 0x49], // I - TAB
|
||||
0x4A: [0x6A, 0x0A, 0x4A], // J - NL
|
||||
0x4B: [0x6B, 0x0B, 0x4B], // K - VT
|
||||
0x4C: [0x6C, 0x0C, 0x4C], // L
|
||||
0x4D: [0x6D, 0x0D, 0x4D], // M - CR
|
||||
0x4E: [0x6E, 0x0E, 0x4E], // N
|
||||
0x4F: [0x6F, 0x0F, 0x4F], // O
|
||||
|
||||
0x50: [0x70, 0x10, 0x50], // P
|
||||
0x51: [0x71, 0x11, 0x51], // Q
|
||||
0x52: [0x72, 0x12, 0x52], // R
|
||||
0x53: [0x73, 0x13, 0x53], // S
|
||||
0x54: [0x74, 0x14, 0x54], // T
|
||||
0x55: [0x75, 0x15, 0x55], // U
|
||||
0x56: [0x76, 0x16, 0x56], // V
|
||||
0x57: [0x77, 0x17, 0x57], // W
|
||||
0x58: [0x78, 0x18, 0x58], // X
|
||||
0x59: [0x79, 0x19, 0x59], // Y
|
||||
0x5A: [0x7A, 0x1A, 0x5A], // Z
|
||||
0x5B: [0xFF, 0xFF, 0xFF], // Left window
|
||||
0x5C: [0xFF, 0xFF, 0xFF], // Right window
|
||||
0x5D: [0xFF, 0xFF, 0xFF], // Select
|
||||
0x5E: [0x5E, 0x1E, 0x5E], //
|
||||
0x5F: [0x5F, 0x1F, 0x5F], // _
|
||||
|
||||
// Numeric pad
|
||||
0x60: [0x30, 0x30, 0x30], // 0
|
||||
0x61: [0x31, 0x31, 0x31], // 1
|
||||
0x62: [0x32, 0x32, 0x32], // 2
|
||||
0x63: [0x33, 0x33, 0x33], // 3
|
||||
0x64: [0x34, 0x34, 0x34], // 4
|
||||
0x65: [0x35, 0x35, 0x35], // 5
|
||||
0x66: [0x36, 0x36, 0x36], // 6
|
||||
0x67: [0x37, 0x37, 0x37], // 7
|
||||
0x68: [0x38, 0x38, 0x38], // 8
|
||||
0x69: [0x39, 0x39, 0x39], // 9
|
||||
|
||||
0x6A: [0x2A, 0x2A, 0x2A], // *
|
||||
0x6B: [0x2B, 0x2B, 0x2B], // +
|
||||
0x6D: [0x2D, 0x2D, 0x2D], // -
|
||||
0x6E: [0x2E, 0x2E, 0x2E], // .
|
||||
0x6F: [0x2F, 0x2F, 0x39], // /
|
||||
|
||||
// Stray keys
|
||||
0xAD: [0x2D, 0x2D, 0x5F], // - - _
|
||||
0xBA: [0x3B, 0x3B, 0x3A], // ; - :
|
||||
0xBB: [0x3D, 0x3D, 0x2B], // = - +
|
||||
0xBC: [0x2C, 0x2C, 0x3C], // , - <
|
||||
0xBD: [0x2D, 0x2D, 0x5F], // - - _
|
||||
0xBE: [0x2E, 0x2E, 0x3E], // . - >
|
||||
0xBF: [0x2F, 0x2F, 0x3F], // / - ?
|
||||
0xC0: [0x60, 0x60, 0x7E], // ` - ~
|
||||
0xDB: [0x5B, 0x1B, 0x7B], // [ - {
|
||||
0xDC: [0x5C, 0x1C, 0x7C], // \ - |
|
||||
0xDD: [0x5D, 0x1D, 0x7D], // ] - }
|
||||
0xDE: [0x27, 0x22, 0x22], // ' - '
|
||||
|
||||
0xFF: [0xFF, 0xFF, 0xFF] // No comma line
|
||||
};
|
||||
|
||||
var uiKitMap = {
|
||||
'Dead': 0xFF,
|
||||
'UIKeyInputLeftArrow': 0x08,
|
||||
'UIKeyInputRightArrow': 0x15,
|
||||
'UIKeyInputUpArrow': 0x0B,
|
||||
'UIKeyInputDownArrow': 0x0A,
|
||||
'UIKeyInputEscape': 0x1B
|
||||
};
|
||||
|
||||
var keys2 = [
|
||||
[
|
||||
['1','2','3','4','5','6','7','8','9','0',':','-','RESET'],
|
||||
['ESC','Q','W','E','R','T','Y','U','I','O','P','REPT','RETURN'],
|
||||
['CTRL','A','S','D','F','G','H','J','K','L',';','←','→'],
|
||||
['SHIFT','Z','X','C','V','B','N','M',',','.','/','SHIFT'],
|
||||
['POWER', ' ']
|
||||
], [
|
||||
['!','"','#','$','%','&','\'','(',')','0','*','=','RESET'],
|
||||
['ESC','Q','W','E','R','T','Y','U','I','O','@','REPT','RETURN'],
|
||||
['CTRL','A','S','D','F','BELL','H','J','K','L','+','←','→'],
|
||||
['SHIFT','Z','X','C','V','B','^',']','<','>','?','SHIFT'],
|
||||
['POWER', ' ']
|
||||
]
|
||||
];
|
||||
|
||||
var keys2e = [
|
||||
[
|
||||
['ESC','1','2','3','4','5','6','7','8','9','0','-','=','DELETE'],
|
||||
['TAB','Q','W','E','R','T','Y','U','I','O','P','[',']','\\'],
|
||||
['CTRL','A','S','D','F','G','H','J','K','L',';','"','RETURN'],
|
||||
['SHIFT','Z','X','C','V','B','N','M',',','.','/','SHIFT'],
|
||||
['LOCK','`','POW','OPEN_APPLE',' ','CLOSED_APPLE','←','→','↓','↑']
|
||||
], [
|
||||
['ESC','!','@','#','$','%','^','&','*','(',')','_','+','DELETE'],
|
||||
['TAB','Q','W','E','R','T','Y','U','I','O','P','{','}','|'],
|
||||
['CTRL','A','S','D','F','G','H','J','K','L',':','\'','RETURN'],
|
||||
['SHIFT','Z','X','C','V','B','N','M','<','>','?','SHIFT'],
|
||||
['CAPS','~','POW','OPEN_APPLE',' ','CLOSED_APPLE','←','→','↓','↑']
|
||||
]
|
||||
];
|
||||
|
||||
var keys = e ? keys2e : keys2;
|
||||
|
||||
var shifted = false;
|
||||
var controlled = false;
|
||||
var capslocked = true;
|
||||
// Initially caps lock on physical keyboard is assumed to be off,
|
||||
// but on emulated keyboard it is on.
|
||||
var capslockKeyUsed = false;
|
||||
var optioned = false;
|
||||
var commanded = false;
|
||||
|
||||
var kb;
|
||||
|
||||
return {
|
||||
mapKeyEvent: function keyboard_mapKeyEvent(evt) {
|
||||
var code = evt.keyCode, key = 0xff;
|
||||
|
||||
if (evt.key in uiKitMap) {
|
||||
key = uiKitMap[evt.key];
|
||||
} else if (code in keymap) {
|
||||
key = keymap[code][evt.shiftKey ? 2 : (evt.ctrlKey ? 1 : 0)];
|
||||
|
||||
if (code != 20 && capslockKeyUsed) {
|
||||
this.capslockKey(evt.getModifierState('CapsLock'));
|
||||
}
|
||||
|
||||
if (capslocked && key >= 0x61 && key <= 0x7A) {
|
||||
key -= 0x20;
|
||||
}
|
||||
} else {
|
||||
debug('Unhandled key = ' + toHex(code));
|
||||
}
|
||||
|
||||
if (key == 0x7F && evt.shiftKey && evt.ctrlKey) {
|
||||
cpu.reset();
|
||||
key = 0xff;
|
||||
}
|
||||
|
||||
return key;
|
||||
},
|
||||
|
||||
shiftKey: function keyboard_shiftKey(down) {
|
||||
var shiftKeys = kb.querySelectorAll('.key-SHIFT');
|
||||
shifted = down;
|
||||
if (down) {
|
||||
io.buttonUp(2);
|
||||
shiftKeys.forEach(function(key) { key.classList.add('active'); });
|
||||
} else {
|
||||
io.buttonDown(2);
|
||||
shiftKeys.forEach(function(key) { key.classList.remove('active'); });
|
||||
}
|
||||
},
|
||||
|
||||
controlKey: function keyboard_controlKey(down) {
|
||||
var ctrlKey = kb.querySelector('.key-CTRL');
|
||||
controlled = down;
|
||||
if (down) {
|
||||
ctrlKey.classList.add('active');
|
||||
} else {
|
||||
ctrlKey.classList.remove('active');
|
||||
}
|
||||
},
|
||||
|
||||
commandKey: function keyboard_commandKey(down) {
|
||||
var commandKey = kb.querySelector('.key-OPEN_APPLE');
|
||||
if (!commandKey) {
|
||||
return;
|
||||
}
|
||||
commanded = down;
|
||||
if (down) {
|
||||
io.buttonDown(0);
|
||||
commandKey.classList.add('active');
|
||||
} else {
|
||||
io.buttonUp(0);
|
||||
commandKey.classList.remove('active');
|
||||
}
|
||||
},
|
||||
|
||||
optionKey: function keyboard_optionKey(down) {
|
||||
var optionKey = kb.querySelector('.key-CLOSED_APPLE');
|
||||
if (!optionKey) {
|
||||
return;
|
||||
}
|
||||
optioned = down;
|
||||
if (down) {
|
||||
io.buttonDown(1);
|
||||
optionKey.classList.add('active');
|
||||
} else {
|
||||
io.buttonUp(1);
|
||||
optionKey.classList.remove('active');
|
||||
}
|
||||
},
|
||||
|
||||
capslockKey: function keyboard_caplockKey(down) {
|
||||
var capsLock = kb.querySelector('.key-LOCK');
|
||||
|
||||
if (arguments.length == 0) {
|
||||
if (capslockKeyUsed) {
|
||||
capslocked = !capslocked;
|
||||
} else {
|
||||
capslockKeyUsed = true;
|
||||
}
|
||||
} else if (down === undefined) {
|
||||
capslocked = !capslocked;
|
||||
capslockKeyUsed = false;
|
||||
} else {
|
||||
capslocked = down;
|
||||
}
|
||||
|
||||
if (capslocked) {
|
||||
capsLock.classList.add('active');
|
||||
} else {
|
||||
capsLock.classList.remove('active');
|
||||
}
|
||||
},
|
||||
|
||||
reset: function keyboard_reset(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
cpu.reset();
|
||||
},
|
||||
|
||||
create: function keyboard_create(el) {
|
||||
kb = document.querySelector(el);
|
||||
var x, y, row, key, key1, key2, label, label1, label2, self = this;
|
||||
|
||||
function buildLabel(k) {
|
||||
var span = document.createElement('span');
|
||||
span.innerHTML = k;
|
||||
if (k.length > 1 && k.substr(0,1) != '&')
|
||||
span.classList.add('small');
|
||||
return span;
|
||||
}
|
||||
|
||||
function _mouseup() {
|
||||
this.classList.remove('pressed');
|
||||
}
|
||||
|
||||
function _mousedown(ev) {
|
||||
ev.preventDefault();
|
||||
this.classList.add('pressed');
|
||||
var key = this.dataset[shifted ? 'key2' : 'key1'];
|
||||
switch (key) {
|
||||
case 'BELL':
|
||||
key = 'G';
|
||||
break;
|
||||
case 'RETURN':
|
||||
key = '\r';
|
||||
break;
|
||||
case 'TAB':
|
||||
key = '\t';
|
||||
break;
|
||||
case 'DELETE':
|
||||
key = '\x7F';
|
||||
break;
|
||||
case '←':
|
||||
key = '\x08';
|
||||
break;
|
||||
case '→':
|
||||
key = '\x15';
|
||||
break;
|
||||
case '↓':
|
||||
key = '\x0A';
|
||||
break;
|
||||
case '↑':
|
||||
key = '\x0B';
|
||||
break;
|
||||
case ' ':
|
||||
key = ' ';
|
||||
break;
|
||||
case 'ESC':
|
||||
key = '\x1B';
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (key.length > 1) {
|
||||
switch (key) {
|
||||
case 'SHIFT':
|
||||
self.shiftKey(!shifted);
|
||||
break;
|
||||
case 'CTRL':
|
||||
self.controlKey(!controlled);
|
||||
break;
|
||||
case 'CAPS':
|
||||
case 'LOCK':
|
||||
self.capslockKey(undefined);
|
||||
break;
|
||||
case 'POW':
|
||||
case 'POWER':
|
||||
if (window.confirm('Power Cycle?'))
|
||||
window.location.reload();
|
||||
break;
|
||||
case 'RESET':
|
||||
cpu.reset();
|
||||
break;
|
||||
case 'OPEN_APPLE':
|
||||
self.commandKey(!commanded);
|
||||
break;
|
||||
case 'CLOSED_APPLE':
|
||||
self.optionKey(!optioned);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
if (controlled && key >= '@' && key <= '_') {
|
||||
io.keyDown(key.charCodeAt(0) - 0x40);
|
||||
} else if (!e && !shifted && !capslocked &&
|
||||
key >= 'A' && key <= 'Z') {
|
||||
io.keyDown(key.charCodeAt(0) + 0x20);
|
||||
} else {
|
||||
io.keyDown(key.charCodeAt(0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (y = 0; y < 5; y++) {
|
||||
row = document.createElement('div');
|
||||
row.classList.add('row');
|
||||
row.classList.add('row' + y);
|
||||
kb.append(row);
|
||||
for (x = 0; x < keys[0][y].length; x++) {
|
||||
key1 = keys[0][y][x];
|
||||
key2 = keys[1][y][x];
|
||||
|
||||
label = document.createElement('div');
|
||||
label1 = buildLabel(key1);
|
||||
label2 = buildLabel(key2);
|
||||
|
||||
key = document.createElement('div');
|
||||
key.classList.add('key');
|
||||
key.classList.add('key-' + key1.replace(/[&#;]/g,''));
|
||||
|
||||
if (key1.length > 1) {
|
||||
if (key1 == 'LOCK')
|
||||
key.classList.add('v-center2');
|
||||
else
|
||||
key.classList.add('v-center');
|
||||
}
|
||||
if (key1 != key2) {
|
||||
key.classList.add('key-' + key2.replace(/[&;]/g,''));
|
||||
label.append(label2);
|
||||
label.append(document.createElement('br'));
|
||||
}
|
||||
if (key1 == 'LOCK') {
|
||||
key.classList.add('active');
|
||||
}
|
||||
|
||||
label.append(label1);
|
||||
key.append(label);
|
||||
key.dataset.key1 = key1;
|
||||
key.dataset.key2 = key2;
|
||||
|
||||
if (window.ontouchstart === undefined) {
|
||||
key.addEventListener('mousedown', _mousedown);
|
||||
key.addEventListener('mouseup', _mouseup);
|
||||
key.addEventListener('mouseleave', _mouseup);
|
||||
} else {
|
||||
key.addEventListener('touchstart', _mousedown);
|
||||
key.addEventListener('touchend', _mouseup);
|
||||
key.addEventListener('touchleave', _mouseup);
|
||||
}
|
||||
|
||||
row.append(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
503
js/ui/keyboard.ts
Normal file
503
js/ui/keyboard.ts
Normal file
@ -0,0 +1,503 @@
|
||||
/* Copyright 2010-2019 Will Scullin <scullin@scullinsteel.com>
|
||||
*
|
||||
* Permission to use, copy, modify, distribute, and sell this software and its
|
||||
* documentation for any purpose is hereby granted without fee, provided that
|
||||
* the above copyright notice appear in all copies and that both that
|
||||
* copyright notice and this permission notice appear in supporting
|
||||
* documentation. No representations are made about the suitability of this
|
||||
* software for any purpose. It is provided "as is" without express or
|
||||
* implied warranty.
|
||||
*/
|
||||
|
||||
import { byte, DeepMemberOf, KnownKeys } from '../types';
|
||||
import Apple2IO from '../apple2io';
|
||||
import CPU6502 from '../cpu6502';
|
||||
import { debug, toHex } from '../util';
|
||||
|
||||
// keycode: [plain, cntl, shift]
|
||||
const keymap = {
|
||||
// Most of these won't happen
|
||||
0x00: [0x00, 0x00, 0x00], //
|
||||
0x01: [0x01, 0x01, 0x01], //
|
||||
0x02: [0x02, 0x02, 0x02], //
|
||||
0x03: [0x03, 0x03, 0x03], //
|
||||
0x04: [0x04, 0x04, 0x04], //
|
||||
0x05: [0x05, 0x05, 0x05], //
|
||||
0x06: [0x06, 0x06, 0x06], //
|
||||
0x07: [0x07, 0x07, 0x07], //
|
||||
0x08: [0x7F, 0x7F, 0x7F], // BS/DELETE
|
||||
0x09: [0x09, 0x09, 0x09], // TAB
|
||||
0x0A: [0x0A, 0x0A, 0x0A], //
|
||||
0x0B: [0x0B, 0x0B, 0x0B], //
|
||||
0x0C: [0x0C, 0x0C, 0x0C], //
|
||||
0x0D: [0x0D, 0x0D, 0x0D], // CR
|
||||
0x0E: [0x0E, 0x0E, 0x0E], //
|
||||
0x0F: [0x0F, 0x0F, 0x0F], //
|
||||
|
||||
0x10: [0xff, 0xff, 0xff], // SHIFT
|
||||
0x11: [0xff, 0xff, 0xff], // CTRL
|
||||
0x12: [0xff, 0xff, 0xff], // ALT/OPTION
|
||||
0x13: [0x13, 0x13, 0x13], //
|
||||
0x14: [0x14, 0x14, 0x14], //
|
||||
0x15: [0x15, 0x15, 0x15], //
|
||||
0x16: [0x16, 0x16, 0x16], //
|
||||
0x17: [0x17, 0x17, 0x18], //
|
||||
0x18: [0x18, 0x18, 0x18], //
|
||||
0x19: [0x19, 0x19, 0x19], //
|
||||
0x1A: [0x1A, 0x1A, 0x1A], //
|
||||
0x1B: [0x1B, 0x1B, 0x1B], // ESC
|
||||
0x1C: [0x1C, 0x1C, 0x1C], //
|
||||
0x1D: [0x1D, 0x1D, 0x1D], //
|
||||
0x1E: [0x1E, 0x1E, 0x1E], //
|
||||
0x1F: [0x1F, 0x1F, 0x1F], //
|
||||
|
||||
// Most of these besides space won't happen
|
||||
0x20: [0x20, 0x20, 0x20], //
|
||||
0x21: [0x21, 0x21, 0x21], //
|
||||
0x22: [0x22, 0x22, 0x22], //
|
||||
0x23: [0x23, 0x23, 0x23], //
|
||||
0x24: [0x24, 0x24, 0x24], //
|
||||
0x25: [0x08, 0x08, 0x08], // <- left
|
||||
0x26: [0x0B, 0x0B, 0x0B], // ^ up
|
||||
0x27: [0x15, 0x15, 0x15], // -> right
|
||||
0x28: [0x0A, 0x0A, 0x0A], // v down
|
||||
0x29: [0x29, 0x29, 0x29], // )
|
||||
0x2A: [0x2A, 0x2A, 0x2A], // *
|
||||
0x2B: [0x2B, 0x2B, 0x2B], // +
|
||||
0x2C: [0x2C, 0x2C, 0x3C], // , - <
|
||||
0x2D: [0x2D, 0x2D, 0x5F], // - - _
|
||||
0x2E: [0x2E, 0x2E, 0x3E], // . - >
|
||||
0x2F: [0x2F, 0x2F, 0x3F], // / - ?
|
||||
|
||||
0x30: [0x30, 0x30, 0x29], // 0 - )
|
||||
0x31: [0x31, 0x31, 0x21], // 1 - !
|
||||
0x32: [0x32, 0x00, 0x40], // 2 - @
|
||||
0x33: [0x33, 0x33, 0x23], // 3 - #
|
||||
0x34: [0x34, 0x34, 0x24], // 4 - $
|
||||
0x35: [0x35, 0x35, 0x25], // 5 - %
|
||||
0x36: [0x36, 0x36, 0x5E], // 6 - ^
|
||||
0x37: [0x37, 0x37, 0x26], // 7 - &
|
||||
0x38: [0x38, 0x38, 0x2A], // 8 - *
|
||||
0x39: [0x39, 0x39, 0x28], // 9 - (
|
||||
0x3A: [0x3A, 0x3A, 0x3A], // :
|
||||
0x3B: [0x3B, 0x3B, 0x3A], // ; - :
|
||||
0x3C: [0x3C, 0x3C, 0x3C], // <
|
||||
0x3D: [0x3D, 0x3D, 0x2B], // = - +
|
||||
0x3E: [0x3E, 0x3E, 0x3E], // >
|
||||
0x3F: [0x3F, 0x3F, 0x3F], // ?
|
||||
|
||||
// Alpha and control
|
||||
0x40: [0x40, 0x00, 0x40], // @
|
||||
0x41: [0x61, 0x01, 0x41], // A
|
||||
0x42: [0x62, 0x02, 0x42], // B
|
||||
0x43: [0x63, 0x03, 0x43], // C - BRK
|
||||
0x44: [0x64, 0x04, 0x44], // D
|
||||
0x45: [0x65, 0x05, 0x45], // E
|
||||
0x46: [0x66, 0x06, 0x46], // F
|
||||
0x47: [0x67, 0x07, 0x47], // G - BELL
|
||||
0x48: [0x68, 0x08, 0x48], // H
|
||||
0x49: [0x69, 0x09, 0x49], // I - TAB
|
||||
0x4A: [0x6A, 0x0A, 0x4A], // J - NL
|
||||
0x4B: [0x6B, 0x0B, 0x4B], // K - VT
|
||||
0x4C: [0x6C, 0x0C, 0x4C], // L
|
||||
0x4D: [0x6D, 0x0D, 0x4D], // M - CR
|
||||
0x4E: [0x6E, 0x0E, 0x4E], // N
|
||||
0x4F: [0x6F, 0x0F, 0x4F], // O
|
||||
|
||||
0x50: [0x70, 0x10, 0x50], // P
|
||||
0x51: [0x71, 0x11, 0x51], // Q
|
||||
0x52: [0x72, 0x12, 0x52], // R
|
||||
0x53: [0x73, 0x13, 0x53], // S
|
||||
0x54: [0x74, 0x14, 0x54], // T
|
||||
0x55: [0x75, 0x15, 0x55], // U
|
||||
0x56: [0x76, 0x16, 0x56], // V
|
||||
0x57: [0x77, 0x17, 0x57], // W
|
||||
0x58: [0x78, 0x18, 0x58], // X
|
||||
0x59: [0x79, 0x19, 0x59], // Y
|
||||
0x5A: [0x7A, 0x1A, 0x5A], // Z
|
||||
0x5B: [0xFF, 0xFF, 0xFF], // Left window
|
||||
0x5C: [0xFF, 0xFF, 0xFF], // Right window
|
||||
0x5D: [0xFF, 0xFF, 0xFF], // Select
|
||||
0x5E: [0x5E, 0x1E, 0x5E], //
|
||||
0x5F: [0x5F, 0x1F, 0x5F], // _
|
||||
|
||||
// Numeric pad
|
||||
0x60: [0x30, 0x30, 0x30], // 0
|
||||
0x61: [0x31, 0x31, 0x31], // 1
|
||||
0x62: [0x32, 0x32, 0x32], // 2
|
||||
0x63: [0x33, 0x33, 0x33], // 3
|
||||
0x64: [0x34, 0x34, 0x34], // 4
|
||||
0x65: [0x35, 0x35, 0x35], // 5
|
||||
0x66: [0x36, 0x36, 0x36], // 6
|
||||
0x67: [0x37, 0x37, 0x37], // 7
|
||||
0x68: [0x38, 0x38, 0x38], // 8
|
||||
0x69: [0x39, 0x39, 0x39], // 9
|
||||
|
||||
0x6A: [0x2A, 0x2A, 0x2A], // *
|
||||
0x6B: [0x2B, 0x2B, 0x2B], // +
|
||||
0x6D: [0x2D, 0x2D, 0x2D], // -
|
||||
0x6E: [0x2E, 0x2E, 0x2E], // .
|
||||
0x6F: [0x2F, 0x2F, 0x39], // /
|
||||
|
||||
// Stray keys
|
||||
0xAD: [0x2D, 0x2D, 0x5F], // - - _
|
||||
0xBA: [0x3B, 0x3B, 0x3A], // ; - :
|
||||
0xBB: [0x3D, 0x3D, 0x2B], // = - +
|
||||
0xBC: [0x2C, 0x2C, 0x3C], // , - <
|
||||
0xBD: [0x2D, 0x2D, 0x5F], // - - _
|
||||
0xBE: [0x2E, 0x2E, 0x3E], // . - >
|
||||
0xBF: [0x2F, 0x2F, 0x3F], // / - ?
|
||||
0xC0: [0x60, 0x60, 0x7E], // ` - ~
|
||||
0xDB: [0x5B, 0x1B, 0x7B], // [ - {
|
||||
0xDC: [0x5C, 0x1C, 0x7C], // \ - |
|
||||
0xDD: [0x5D, 0x1D, 0x7D], // ] - }
|
||||
0xDE: [0x27, 0x22, 0x22], // ' - '
|
||||
|
||||
0xFF: [0xFF, 0xFF, 0xFF] // No comma line
|
||||
} as const;
|
||||
|
||||
function isKeyboardCode(code: number): code is KnownKeys<typeof keymap> {
|
||||
return code in keymap;
|
||||
}
|
||||
|
||||
const uiKitMap = {
|
||||
'Dead': 0xFF,
|
||||
'UIKeyInputLeftArrow': 0x08,
|
||||
'UIKeyInputRightArrow': 0x15,
|
||||
'UIKeyInputUpArrow': 0x0B,
|
||||
'UIKeyInputDownArrow': 0x0A,
|
||||
'UIKeyInputEscape': 0x1B
|
||||
} as const;
|
||||
|
||||
|
||||
function isUiKitKey(k: string): k is KnownKeys<typeof uiKitMap> {
|
||||
return k in uiKitMap;
|
||||
}
|
||||
|
||||
const keys2 = [
|
||||
[
|
||||
['1', '2', '3', '4', '5', '6', '7', '8', '9', '0', ':', '-', 'RESET'],
|
||||
['ESC', 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', 'REPT', 'RETURN'],
|
||||
['CTRL', 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', ';', '←', '→'],
|
||||
['SHIFT', 'Z', 'X', 'C', 'V', 'B', 'N', 'M', ',', '.', '/', 'SHIFT'],
|
||||
['POWER', ' ']
|
||||
], [
|
||||
['!', '"', '#', '$', '%', '&', '\'', '(', ')', '0', '*', '=', 'RESET'],
|
||||
['ESC', 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', '@', 'REPT', 'RETURN'],
|
||||
['CTRL', 'A', 'S', 'D', 'F', 'BELL', 'H', 'J', 'K', 'L', '+', '←', '→'],
|
||||
['SHIFT', 'Z', 'X', 'C', 'V', 'B', '^', ']', '<', '>', '?', 'SHIFT'],
|
||||
['POWER', ' ']
|
||||
]
|
||||
] as const;
|
||||
|
||||
|
||||
type Key2 = DeepMemberOf<typeof keys2>;
|
||||
|
||||
const keys2e = [
|
||||
[
|
||||
['ESC', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '=', 'DELETE'],
|
||||
['TAB', 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', '[', ']', '\\'],
|
||||
['CTRL', 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', ';', '"', 'RETURN'],
|
||||
['SHIFT', 'Z', 'X', 'C', 'V', 'B', 'N', 'M', ',', '.', '/', 'SHIFT'],
|
||||
['LOCK', '`', 'POW', 'OPEN_APPLE', ' ', 'CLOSED_APPLE', '←', '→', '↓', '↑']
|
||||
], [
|
||||
['ESC', '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '_', '+', 'DELETE'],
|
||||
['TAB', 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', '{', '}', '|'],
|
||||
['CTRL', 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', ':', '\'', 'RETURN'],
|
||||
['SHIFT', 'Z', 'X', 'C', 'V', 'B', 'N', 'M', '<', '>', '?', 'SHIFT'],
|
||||
['CAPS', '~', 'POW', 'OPEN_APPLE', ' ', 'CLOSED_APPLE', '←', '→', '↓', '↑']
|
||||
]
|
||||
] as const;
|
||||
|
||||
type Key2e = DeepMemberOf<typeof keys2e>;
|
||||
|
||||
type Key = Key2 | Key2e;
|
||||
|
||||
export default class KeyBoard {
|
||||
private kb: HTMLElement;
|
||||
private keys;
|
||||
|
||||
private shifted = false;
|
||||
private controlled = false;
|
||||
private capslocked = true;
|
||||
|
||||
// Initially caps lock on physical keyboard is assumed to be off,
|
||||
// but on emulated keyboard it is on.
|
||||
private capslockKeyUsed = false;
|
||||
private optioned = false;
|
||||
private commanded = false;
|
||||
|
||||
constructor(private cpu: CPU6502, private io: Apple2IO, private e: boolean) {
|
||||
this.keys = e ? keys2e : keys2;
|
||||
}
|
||||
|
||||
mapKeyEvent(evt: KeyboardEvent) {
|
||||
const code = evt.keyCode;
|
||||
let key: byte = 0xff;
|
||||
|
||||
if (isUiKitKey(evt.key)) {
|
||||
key = uiKitMap[evt.key];
|
||||
} else if (isKeyboardCode(code)) {
|
||||
key = keymap[code][evt.shiftKey ? 2 : (evt.ctrlKey ? 1 : 0)];
|
||||
|
||||
if (code != 20 && this.capslockKeyUsed) {
|
||||
this.capslockKey(evt.getModifierState('CapsLock'));
|
||||
}
|
||||
|
||||
if (this.capslocked && key >= 0x61 && key <= 0x7A) {
|
||||
key -= 0x20;
|
||||
}
|
||||
} else {
|
||||
debug('Unhandled key = ' + toHex(code));
|
||||
}
|
||||
|
||||
if (key == 0x7F && evt.shiftKey && evt.ctrlKey) {
|
||||
this.cpu.reset();
|
||||
key = 0xff;
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
shiftKey(down: boolean) {
|
||||
const shiftKeys = this.kb.querySelectorAll('.key-SHIFT');
|
||||
this.shifted = down;
|
||||
if (down) {
|
||||
this.io.buttonUp(2);
|
||||
shiftKeys.forEach((key) => { key.classList.add('active'); });
|
||||
} else {
|
||||
this.io.buttonDown(2);
|
||||
shiftKeys.forEach((key) => { key.classList.remove('active'); });
|
||||
}
|
||||
}
|
||||
|
||||
controlKey(down: boolean) {
|
||||
const ctrlKey = this.kb.querySelector('.key-CTRL');
|
||||
this.controlled = down;
|
||||
if (down) {
|
||||
ctrlKey!.classList.add('active');
|
||||
} else {
|
||||
ctrlKey!.classList.remove('active');
|
||||
}
|
||||
}
|
||||
|
||||
commandKey(down: boolean) {
|
||||
const commandKey = this.kb.querySelector('.key-OPEN_APPLE');
|
||||
if (!commandKey) {
|
||||
return;
|
||||
}
|
||||
this.commanded = down;
|
||||
if (down) {
|
||||
this.io.buttonDown(0);
|
||||
commandKey.classList.add('active');
|
||||
} else {
|
||||
this.io.buttonUp(0);
|
||||
commandKey.classList.remove('active');
|
||||
}
|
||||
}
|
||||
|
||||
optionKey(down: boolean) {
|
||||
const optionKey = this.kb.querySelector('.key-CLOSED_APPLE');
|
||||
if (!optionKey) {
|
||||
return;
|
||||
}
|
||||
this.optioned = down;
|
||||
if (down) {
|
||||
this.io.buttonDown(1);
|
||||
optionKey.classList.add('active');
|
||||
} else {
|
||||
this.io.buttonUp(1);
|
||||
optionKey.classList.remove('active');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the state of the Caps Lock key. It is very complicated.
|
||||
* @param down if `true`, Caps Lock is pressed; if `false` Caps Lock is not pressed;
|
||||
* if `undefined`, Caps Lock is toggled and its "used" state is set to false;
|
||||
* if called with no arguments, the state is toggled _if_ it has been used before,
|
||||
* otherwise the used state is set to true.
|
||||
*/
|
||||
capslockKey(down?: boolean | undefined) {
|
||||
const capsLock = this.kb.querySelector('.key-LOCK');
|
||||
|
||||
if (arguments.length == 0) {
|
||||
if (this.capslockKeyUsed) {
|
||||
this.capslocked = !this.capslocked;
|
||||
} else {
|
||||
this.capslockKeyUsed = true;
|
||||
}
|
||||
} else if (down === undefined) {
|
||||
this.capslocked = !this.capslocked;
|
||||
this.capslockKeyUsed = false;
|
||||
} else {
|
||||
this.capslocked = down;
|
||||
}
|
||||
|
||||
if (this.capslocked) {
|
||||
capsLock!.classList.add('active');
|
||||
} else {
|
||||
capsLock!.classList.remove('active');
|
||||
}
|
||||
}
|
||||
|
||||
reset(event: Event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.cpu.reset();
|
||||
}
|
||||
|
||||
create(el: string) {
|
||||
this.kb = document.querySelector(el)!;
|
||||
let x, y, row, key, label, label1, label2;
|
||||
|
||||
const buildLabel = (k: string) => {
|
||||
const span = document.createElement('span');
|
||||
span.innerHTML = k;
|
||||
if (k.length > 1 && k.substr(0, 1) != '&')
|
||||
span.classList.add('small');
|
||||
return span;
|
||||
};
|
||||
|
||||
for (y = 0; y < 5; y++) {
|
||||
row = document.createElement('div');
|
||||
row.classList.add('row');
|
||||
row.classList.add('row' + y);
|
||||
this.kb.append(row);
|
||||
for (x = 0; x < this.keys[0][y].length; x++) {
|
||||
const key1 = this.keys[0][y][x];
|
||||
const key2 = this.keys[1][y][x];
|
||||
|
||||
label = document.createElement('div');
|
||||
label1 = buildLabel(key1);
|
||||
label2 = buildLabel(key2);
|
||||
|
||||
key = document.createElement('div');
|
||||
key.classList.add('key');
|
||||
key.classList.add('key-' + key1.replace(/[&#;]/g, ''));
|
||||
|
||||
if (key1.length > 1) {
|
||||
if (key1 == 'LOCK')
|
||||
key.classList.add('v-center2');
|
||||
else
|
||||
key.classList.add('v-center');
|
||||
}
|
||||
if (key1 != key2) {
|
||||
key.classList.add('key-' + key2.replace(/[&;]/g, ''));
|
||||
label.append(label2);
|
||||
label.append(document.createElement('br'));
|
||||
}
|
||||
if (key1 == 'LOCK') {
|
||||
key.classList.add('active');
|
||||
}
|
||||
|
||||
label.append(label1);
|
||||
key.append(label);
|
||||
key.dataset.key1 = key1;
|
||||
key.dataset.key2 = key2;
|
||||
|
||||
const mouseDown = this.genMouseDown(key, key1, key2);
|
||||
const mouseUp = this.genMouseUp(key);
|
||||
if (window.ontouchstart === undefined) {
|
||||
|
||||
key.addEventListener('mousedown', mouseDown);
|
||||
key.addEventListener('mouseup', mouseUp);
|
||||
key.addEventListener('mouseleave', mouseUp);
|
||||
} else {
|
||||
key.addEventListener('touchstart', mouseDown);
|
||||
key.addEventListener('touchend', mouseUp);
|
||||
key.addEventListener('touchleave', mouseUp);
|
||||
}
|
||||
|
||||
row.append(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private genMouseDown(target: HTMLElement, key1: Key, key2: Key) {
|
||||
return (ev: MouseEvent) => {
|
||||
ev.preventDefault();
|
||||
target.classList.add('pressed');
|
||||
|
||||
let key: string = this.shifted ? key2 : key1;
|
||||
switch (key) {
|
||||
case 'BELL':
|
||||
key = 'G';
|
||||
break;
|
||||
case 'RETURN':
|
||||
key = '\r';
|
||||
break;
|
||||
case 'TAB':
|
||||
key = '\t';
|
||||
break;
|
||||
case 'DELETE':
|
||||
key = '\x7F';
|
||||
break;
|
||||
case '←':
|
||||
key = '\x08';
|
||||
break;
|
||||
case '→':
|
||||
key = '\x15';
|
||||
break;
|
||||
case '↓':
|
||||
key = '\x0A';
|
||||
break;
|
||||
case '↑':
|
||||
key = '\x0B';
|
||||
break;
|
||||
case ' ':
|
||||
key = ' ';
|
||||
break;
|
||||
case 'ESC':
|
||||
key = '\x1B';
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (key.length > 1) {
|
||||
switch (key) {
|
||||
case 'SHIFT':
|
||||
this.shiftKey(!this.shifted);
|
||||
break;
|
||||
case 'CTRL':
|
||||
this.controlKey(!this.controlled);
|
||||
break;
|
||||
case 'CAPS':
|
||||
case 'LOCK':
|
||||
this.capslockKey(undefined);
|
||||
break;
|
||||
case 'POW':
|
||||
case 'POWER':
|
||||
if (window.confirm('Power Cycle?'))
|
||||
window.location.reload();
|
||||
break;
|
||||
case 'RESET':
|
||||
this.cpu.reset();
|
||||
break;
|
||||
case 'OPEN_APPLE':
|
||||
this.commandKey(!this.commanded);
|
||||
break;
|
||||
case 'CLOSED_APPLE':
|
||||
this.optionKey(!this.optioned);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
if (this.controlled && key >= '@' && key <= '_') {
|
||||
this.io.keyDown(key.charCodeAt(0) - 0x40);
|
||||
} else if (this.e && !this.shifted && !this.capslocked &&
|
||||
key >= 'A' && key <= 'Z') {
|
||||
this.io.keyDown(key.charCodeAt(0) + 0x20);
|
||||
} else {
|
||||
this.io.keyDown(key.charCodeAt(0));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private genMouseUp(target: HTMLElement) {
|
||||
return () => target.classList.remove('pressed');
|
||||
}
|
||||
}
|
@ -1,86 +0,0 @@
|
||||
/* Copyright 2010-2019 Will Scullin <scullin@scullinsteel.com>
|
||||
*
|
||||
* Permission to use, copy, modify, distribute, and sell this software and its
|
||||
* documentation for any purpose is hereby granted without fee, provided that
|
||||
* the above copyright notice appear in all copies and that both that
|
||||
* copyright notice and this permission notice appear in supporting
|
||||
* documentation. No representations are made about the suitability of this
|
||||
* software for any purpose. It is provided "as is" without express or
|
||||
* implied warranty.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Printer UI. The "paper" is bound to the element selected by the input.
|
||||
*
|
||||
* Every line that is output to the printer is added as a <div> to the paper.
|
||||
* The high bit of all characters is stripped and only visible characters are
|
||||
* added to the output. The following characters receive special treatment:
|
||||
*
|
||||
* * `EOT` (ASCII 4): deletes last character
|
||||
* * `HT` (ASCII 9): replaced with 8 spaces
|
||||
* * `LF` (ASCII 10): silently removed
|
||||
* * `CR` (ASCII 13): a newline and carriage return
|
||||
*
|
||||
* @param {string} el The selector of the element on which to bind the "paper".
|
||||
*/
|
||||
export default function Printer(el) {
|
||||
var paper = document.querySelector(el);
|
||||
var _lineBuffer = '';
|
||||
var _line;
|
||||
var _rawLen = 0;
|
||||
var _raw = new Uint8Array(1024);
|
||||
|
||||
function newLine() {
|
||||
_line = document.createElement('div');
|
||||
_line.classList.add('line');
|
||||
_line.innerText = _lineBuffer;
|
||||
paper.append(_line);
|
||||
_lineBuffer = '';
|
||||
}
|
||||
|
||||
newLine();
|
||||
|
||||
return {
|
||||
putChar: function(val) {
|
||||
var ascii = val & 0x7f;
|
||||
var visible = val >= 0x20;
|
||||
var c = String.fromCharCode(ascii);
|
||||
|
||||
if (c === '\r') {
|
||||
newLine();
|
||||
} else if (c === '\n') {
|
||||
// eat for now
|
||||
} else if (c === '\t') {
|
||||
_lineBuffer += ' ';
|
||||
} else if (ascii === 0x04) {
|
||||
_lineBuffer = _lineBuffer.slice(0, -1);
|
||||
} else if (visible) {
|
||||
_lineBuffer += c;
|
||||
}
|
||||
_line.innerText = _lineBuffer;
|
||||
_raw[_rawLen] = val;
|
||||
_rawLen++;
|
||||
if (_rawLen > _raw.length) {
|
||||
const newRaw = new Uint8Array(_raw.length * 2);
|
||||
newRaw.set(_raw);
|
||||
_raw = newRaw;
|
||||
}
|
||||
},
|
||||
|
||||
clear: function() {
|
||||
_lineBuffer = '';
|
||||
paper.innerHTML = '';
|
||||
newLine();
|
||||
_raw = new Uint8Array(1024);
|
||||
_rawLen = 0;
|
||||
},
|
||||
|
||||
hasPrintout: function() {
|
||||
return paper.text.length;
|
||||
},
|
||||
|
||||
getRawOutput: function() {
|
||||
return _raw.slice(0, _rawLen);
|
||||
}
|
||||
};
|
||||
}
|
92
js/ui/printer.ts
Normal file
92
js/ui/printer.ts
Normal file
@ -0,0 +1,92 @@
|
||||
/* Copyright 2010-2019 Will Scullin <scullin@scullinsteel.com>
|
||||
*
|
||||
* Permission to use, copy, modify, distribute, and sell this software and its
|
||||
* documentation for any purpose is hereby granted without fee, provided that
|
||||
* the above copyright notice appear in all copies and that both that
|
||||
* copyright notice and this permission notice appear in supporting
|
||||
* documentation. No representations are made about the suitability of this
|
||||
* software for any purpose. It is provided "as is" without express or
|
||||
* implied warranty.
|
||||
*/
|
||||
|
||||
import { byte } from '../types';
|
||||
|
||||
/**
|
||||
* Printer UI. The "paper" is bound to the element selected by the input.
|
||||
*
|
||||
* Every line that is output to the printer is added as a <div> to the paper.
|
||||
* The high bit of all characters is stripped and only visible characters are
|
||||
* added to the output. The following characters receive special treatment:
|
||||
*
|
||||
* * `EOT` (ASCII 4): deletes last character
|
||||
* * `HT` (ASCII 9): replaced with 8 spaces
|
||||
* * `LF` (ASCII 10): silently removed
|
||||
* * `CR` (ASCII 13): a newline and carriage return
|
||||
*/
|
||||
export default class Printer {
|
||||
private paper: HTMLElement;
|
||||
private _lineBuffer = '';
|
||||
private _line: HTMLElement;
|
||||
private _rawLen = 0;
|
||||
private _raw = new Uint8Array(1024);
|
||||
|
||||
/**
|
||||
* Creates a new printer bound to the given element.
|
||||
* @param {string} el The selector of the element on which to bind the "paper".
|
||||
*/
|
||||
constructor(el: string) {
|
||||
this.paper = document.querySelector(el)!;
|
||||
this.newLine();
|
||||
}
|
||||
|
||||
private newLine() {
|
||||
this._line = document.createElement('div');
|
||||
this._line.classList.add('line');
|
||||
this._line.innerText = this._lineBuffer;
|
||||
this.paper.append(this._line);
|
||||
this._lineBuffer = '';
|
||||
}
|
||||
|
||||
putChar(val: byte) {
|
||||
const ascii = val & 0x7f;
|
||||
const visible = val >= 0x20;
|
||||
const c = String.fromCharCode(ascii);
|
||||
|
||||
if (c === '\r') {
|
||||
this.newLine();
|
||||
} else if (c === '\n') {
|
||||
// eat for now
|
||||
} else if (c === '\t') {
|
||||
// possibly not right due to tab stops
|
||||
this._lineBuffer += ' ';
|
||||
} else if (ascii === 0x04) {
|
||||
this._lineBuffer = this._lineBuffer.slice(0, -1);
|
||||
} else if (visible) {
|
||||
this._lineBuffer += c;
|
||||
}
|
||||
this._line.innerText = this._lineBuffer;
|
||||
this._raw[this._rawLen] = val;
|
||||
this._rawLen++;
|
||||
if (this._rawLen > this._raw.length) {
|
||||
const newRaw = new Uint8Array(this._raw.length * 2);
|
||||
newRaw.set(this._raw);
|
||||
this._raw = newRaw;
|
||||
}
|
||||
}
|
||||
|
||||
clear() {
|
||||
this._lineBuffer = '';
|
||||
this.paper.innerHTML = '';
|
||||
this.newLine();
|
||||
this._raw = new Uint8Array(1024);
|
||||
this._rawLen = 0;
|
||||
}
|
||||
|
||||
hasPrintout() {
|
||||
return this.paper.innerText.length > 0;
|
||||
}
|
||||
|
||||
getRawOutput() {
|
||||
return this._raw.slice(0, this._rawLen);
|
||||
}
|
||||
}
|
@ -1,81 +0,0 @@
|
||||
|
||||
/* Copyright 2010-2019 Will Scullin <scullin@scullinsteel.com>
|
||||
*
|
||||
* Permission to use, copy, modify, distribute, and sell this software and its
|
||||
* documentation for any purpose is hereby granted without fee, provided that
|
||||
* the above copyright notice appear in all copies and that both that
|
||||
* copyright notice and this permission notice appear in supporting
|
||||
* documentation. No representations are made about the suitability of this
|
||||
* software for any purpose. It is provided "as is" without express or
|
||||
* implied warranty.
|
||||
*/
|
||||
|
||||
import { debug } from '../util';
|
||||
|
||||
export var TAPE_TYPES = ['wav','aiff','aif','mp3','m4a'];
|
||||
|
||||
export default function Tape(io) {
|
||||
var AudioContext = window.AudioContext || window.webkitAudioContext;
|
||||
|
||||
return {
|
||||
doLoadLocalTape: function(file, done) {
|
||||
var kHz = io.getKHz();
|
||||
|
||||
// Audio Buffer Source
|
||||
var context;
|
||||
if (AudioContext) {
|
||||
context = new AudioContext();
|
||||
} else {
|
||||
window.alert('Not supported by your browser');
|
||||
done();
|
||||
return;
|
||||
}
|
||||
|
||||
var fileReader = new FileReader();
|
||||
fileReader.onload = function(ev) {
|
||||
context.decodeAudioData(ev.target.result, function(buffer) {
|
||||
var buf = [];
|
||||
var data = buffer.getChannelData(0), datum = data[0];
|
||||
var old = (datum > 0.0), current;
|
||||
var last = 0, delta, ival;
|
||||
debug('Sample Count: ' + data.length);
|
||||
debug('Sample rate: ' + buffer.sampleRate);
|
||||
for (var idx = 1; idx < data.length; idx++) {
|
||||
datum = data[idx];
|
||||
if ((datum > 0.1) || (datum < -0.1)) {
|
||||
current = (datum > 0.0);
|
||||
if (current != old) {
|
||||
delta = idx - last;
|
||||
if (delta > 2000000) {
|
||||
delta = 2000000;
|
||||
}
|
||||
ival = delta / buffer.sampleRate * 1000;
|
||||
if (ival >= 0.550 && ival < 0.750) {
|
||||
ival = 0.650; // Header
|
||||
} else if (ival >= 0.175 && ival < 0.225) {
|
||||
ival = 0.200; // sync 1
|
||||
} else if (ival >= 0.225 && ival < 0.275) {
|
||||
ival = 0.250; // 0 / sync 2
|
||||
} else if (ival >= 0.450 && ival < 0.550) {
|
||||
ival = 0.500; // 1
|
||||
} else {
|
||||
// debug(idx + ' ' + buf.length + ' ' + ival);
|
||||
}
|
||||
buf.push([parseInt(ival * kHz), current]);
|
||||
old = current;
|
||||
last = idx;
|
||||
}
|
||||
}
|
||||
}
|
||||
io.setTape(buf);
|
||||
if (done) {
|
||||
done();
|
||||
}
|
||||
}, function(error) {
|
||||
window.alert(error.message);
|
||||
});
|
||||
};
|
||||
fileReader.readAsArrayBuffer(file);
|
||||
}
|
||||
};
|
||||
}
|
85
js/ui/tape.ts
Normal file
85
js/ui/tape.ts
Normal file
@ -0,0 +1,85 @@
|
||||
|
||||
/* Copyright 2010-2019 Will Scullin <scullin@scullinsteel.com>
|
||||
*
|
||||
* Permission to use, copy, modify, distribute, and sell this software and its
|
||||
* documentation for any purpose is hereby granted without fee, provided that
|
||||
* the above copyright notice appear in all copies and that both that
|
||||
* copyright notice and this permission notice appear in supporting
|
||||
* documentation. No representations are made about the suitability of this
|
||||
* software for any purpose. It is provided "as is" without express or
|
||||
* implied warranty.
|
||||
*/
|
||||
|
||||
import { TapeData } from '../types';
|
||||
import Apple2IO from '../apple2io';
|
||||
import { debug } from '../util';
|
||||
|
||||
export const TAPE_TYPES = ['wav', 'aiff', 'aif', 'mp3', 'm4a'] as const;
|
||||
|
||||
export default class Tape {
|
||||
constructor(private readonly io: Apple2IO) {}
|
||||
|
||||
public doLoadLocalTape(file: File, done: () => void) {
|
||||
const kHz = this.io.getKHz();
|
||||
|
||||
// Audio Buffer Source
|
||||
let context: AudioContext;
|
||||
if (AudioContext) {
|
||||
context = new AudioContext();
|
||||
} else {
|
||||
window.alert('Not supported by your browser');
|
||||
done();
|
||||
return;
|
||||
}
|
||||
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onload = (ev: ProgressEvent) => {
|
||||
const target: FileReader = ev.target as FileReader;
|
||||
const result: ArrayBuffer = target.result as ArrayBuffer;
|
||||
context.decodeAudioData(result, (buffer) => {
|
||||
const buf: TapeData = [];
|
||||
const data = buffer.getChannelData(0);
|
||||
let datum = data[0];
|
||||
let old = (datum > 0.0), current;
|
||||
let last = 0;
|
||||
let delta: number;
|
||||
debug('Sample Count: ' + data.length);
|
||||
debug('Sample rate: ' + buffer.sampleRate);
|
||||
for (let idx = 1; idx < data.length; idx++) {
|
||||
datum = data[idx];
|
||||
if ((datum > 0.1) || (datum < -0.1)) {
|
||||
current = (datum > 0.0);
|
||||
if (current != old) {
|
||||
delta = idx - last;
|
||||
if (delta > 2000000) {
|
||||
delta = 2000000;
|
||||
}
|
||||
let ival = delta / buffer.sampleRate * 1000;
|
||||
if (ival >= 0.550 && ival < 0.750) {
|
||||
ival = 0.650; // Header
|
||||
} else if (ival >= 0.175 && ival < 0.225) {
|
||||
ival = 0.200; // sync 1
|
||||
} else if (ival >= 0.225 && ival < 0.275) {
|
||||
ival = 0.250; // 0 / sync 2
|
||||
} else if (ival >= 0.450 && ival < 0.550) {
|
||||
ival = 0.500; // 1
|
||||
} else {
|
||||
// debug(idx + ' ' + buf.length + ' ' + ival);
|
||||
}
|
||||
buf.push([ival * kHz, current]);
|
||||
old = current;
|
||||
last = idx;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.io.setTape(buf);
|
||||
if (done) {
|
||||
done();
|
||||
}
|
||||
}, function (error) {
|
||||
window.alert(error.message);
|
||||
});
|
||||
};
|
||||
fileReader.readAsArrayBuffer(file);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user