diff --git a/src/emu.js b/src/emu.js index 4f637735..5a403c51 100644 --- a/src/emu.js +++ b/src/emu.js @@ -127,6 +127,9 @@ var VectorVideo = function(mainElement, width, height) { var canvas, ctx; var persistenceAlpha = 0.5; var jitter = 1.0; + var gamma = 0.8; + var sx = width/1024.0; + var sy = height/1024.0; this.create = function() { canvas = __createCanvas(mainElement, width, height); @@ -155,13 +158,13 @@ var VectorVideo = function(mainElement, width, height) { } var COLORS = [ - '#000000', - '#0000ff', - '#00ff00', - '#00ffff', - '#ff0000', - '#ff00ff', - '#ffff00', + '#111111', + '#1111ff', + '#11ff11', + '#11ffff', + '#ff1111', + '#ff11ff', + '#ffff11', '#ffffff' ]; @@ -169,7 +172,8 @@ var VectorVideo = function(mainElement, width, height) { //console.log(x1, y1, x2, y2, intensity); if (intensity > 0) { // TODO: landscape vs portrait - ctx.globalAlpha = intensity / 255.0; + var alpha = Math.pow(intensity / 255.0, gamma); + ctx.globalAlpha = alpha; ctx.beginPath(); // TODO: bright dots var jx = jitter * (Math.random() - 0.5); @@ -178,8 +182,11 @@ var VectorVideo = function(mainElement, width, height) { x2 += jx; y1 += jy; y2 += jy; - ctx.moveTo(x1, height-y1); - ctx.lineTo(x2+1, height-y2); + ctx.moveTo(x1*sx, height-y1*sy); + if (x1 == x2 && y1 == y2) + ctx.lineTo(x2*sx+1, height-y2*sy); + else + ctx.lineTo(x2*sx, height-y2*sy); ctx.strokeStyle = COLORS[color & 7]; ctx.stroke(); } @@ -248,13 +255,12 @@ var SampleAudio = function(clockfreq) { } function createContext() { - if ( typeof AudioContext !== 'undefined') { - self.context = new AudioContext(); - } else if (typeof webkitAudioContext !== 'undefined' ){ - self.context = new webkitAudioContext(); - } else { + var AudioContext = AudioContext || webkitAudioContext || mozAudioContext; + if (! AudioContext) { + console.log("no web audio context"); return; } + self.context = new AudioContext(); self.sr=self.context.sampleRate; self.bufferlen=(self.sr > 44100) ? 4096 : 2048; @@ -381,6 +387,180 @@ var AY38910_Audio = function(master) { } } +// https://en.wikipedia.org/wiki/POKEY +// https://user.xmission.com/~trevin/atari/pokey_regs.html +// http://krap.pl/mirrorz/atari/homepage.ntlworld.com/kryten_droid/Atari/800XL/atari_hw/pokey.htm + +var POKEYDeviceChannel = function() { + + /* definitions for AUDCx (D201, D203, D205, D207) */ + var NOTPOLY5 = 0x80 /* selects POLY5 or direct CLOCK */ + var POLY4 = 0x40 /* selects POLY4 or POLY17 */ + var PURE = 0x20 /* selects POLY4/17 or PURE tone */ + var VOL_ONLY = 0x10 /* selects VOLUME OUTPUT ONLY */ + var VOLUME_MASK = 0x0f /* volume mask */ + + /* definitions for AUDCTL (D208) */ + var POLY9 = 0x80 /* selects POLY9 or POLY17 */ + var CH1_179 = 0x40 /* selects 1.78979 MHz for Ch 1 */ + var CH3_179 = 0x20 /* selects 1.78979 MHz for Ch 3 */ + var CH1_CH2 = 0x10 /* clocks channel 1 w/channel 2 */ + var CH3_CH4 = 0x08 /* clocks channel 3 w/channel 4 */ + var CH1_FILTER = 0x04 /* selects channel 1 high pass filter */ + var CH2_FILTER = 0x02 /* selects channel 2 high pass filter */ + var CLOCK_15 = 0x01 /* selects 15.6999kHz or 63.9210kHz */ + + /* for accuracy, the 64kHz and 15kHz clocks are exact divisions of + the 1.79MHz clock */ + var DIV_64 = 28 /* divisor for 1.79MHz clock to 64 kHz */ + var DIV_15 = 114 /* divisor for 1.79MHz clock to 15 kHz */ + + /* the size (in entries) of the 4 polynomial tables */ + var POLY4_SIZE = 0x000f + var POLY5_SIZE = 0x001f + var POLY9_SIZE = 0x01ff + + var POLY17_SIZE = 0x0001ffff /* else use the full 17 bits */ + + /* channel/chip definitions */ + var CHAN1 = 0 + var CHAN2 = 1 + var CHAN3 = 2 + var CHAN4 = 3 + var CHIP1 = 0 + var CHIP2 = 4 + var CHIP3 = 8 + var CHIP4 = 12 + var SAMPLE = 127 + + var FREQ_17_EXACT = 1789790.0 /* exact 1.79 MHz clock freq */ + var FREQ_17_APPROX = 1787520.0 /* approximate 1.79 MHz clock freq */ + + // LFSR sequences + var bit1 = [ 0,1 ]; + var bit4 = [ 1,1,0,1,1,1,0,0,0,0,1,0,1,0,0 ]; + var bit5 = [ 0,0,1,1,0,0,0,1,1,1,1,0,0,1,0,1,0,1,1,0,1,1,1,0,1,0,0,0,0,0,1 ]; + var bit17 = new Uint8Array(1<<17); + var bit17_5 = new Uint8Array(1<<17); + var bit5_4 = new Uint8Array(1<<17); + for (var i=0; i 0.5; + bit17_5[i] = bit17[i] & bit5[i % bit5.length]; + bit5_4[i] = bit5[i % bit5.length] & bit4[i % bit4.length]; + } + var wavetones = [ + bit17_5, bit5, bit5_4, bit5, + bit17, bit1, bit4, bit1 + ]; + + // registers + var regs = new Uint8Array(16); + var counters = new Float32Array(4); + var deltas = new Float32Array(4); + var volume = new Float32Array(4); + var audc = new Uint8Array(4); + var waveforms = [bit1, bit1, bit1, bit1]; + var buffer; + var sampleRate = 44100; + var clock, baseDelta; + var dirty = true; + + // + + this.setBufferLength = function (length) { + buffer = new Int32Array(length); + }; + + this.getBuffer = function () { + return buffer; + }; + + this.setSampleRate = function (rate) { + sampleRate = rate; + baseDelta = FREQ_17_EXACT / rate / 1.2; // TODO? + }; + + function updateValues(addr) { + var ctrl = regs[8]; + var base = (ctrl & CLOCK_15) ? DIV_15 : DIV_64; + var div; + var i = addr & 4; + var j = i>>1; + var k = i>>2; + if (ctrl & (CH1_CH2>>k)) { + if (ctrl & (CH1_179>>k)) + div = regs[i+2] * 256 + regs[i+0] + 7; + else + div = (regs[i+2] * 256 + regs[i+0] + 1) * base; + deltas[j+1] = baseDelta / div; + deltas[j+0] = 0; + } else { + if (ctrl & (CH1_179>>k)) { + div = regs[i+0] + 4; + } else { + div = (regs[i+0] + 1) * base; + } + deltas[j+0] = baseDelta / div; + div = (regs[i+2] + 1) * base; + deltas[j+1] = baseDelta / div; + } + //console.log(addr, ctrl.toString(16), div, deltas[j+0], deltas[j+1]); + } + + this.setRegister = function(addr, value) { + addr &= 0xf; + value &= 0xff; + if (regs[addr] != value) { + regs[addr] = value; + switch (addr) { + case 0: + case 2: + case 4: + case 6: // AUDF + case 8: // ctrl + dirty = true; + break; + case 1: + case 3: + case 5: + case 7: // AUDC + volume[addr>>1] = value & 0xf; + waveforms[addr>>1] = wavetones[value>>5]; + break; + } + } + } + + this.generate = function (length) { + if (dirty) { + updateValues(0); + updateValues(4); + dirty = false; + } + for (var s=0; s 0 && d < 1 && v > 0) { + var wav = waveforms[i]; + var cnt = counters[i] += d; + if (cnt > POLY17_SIZE+1) { + counters[i] -= POLY17_SIZE+1; + } + var on = wav[Math.floor(cnt % wav.length)]; + if (on) { + sample += v; + } + } + } + sample *= 273; + buffer[s] = sample; + buffer[s+1] = sample; + } + } +} + ////// 6502 var Base6502Platform = function() { diff --git a/src/platform/vector.js b/src/platform/vector.js index a0ae2a81..f928e90b 100644 --- a/src/platform/vector.js +++ b/src/platform/vector.js @@ -31,6 +31,17 @@ var GRAVITAR_KEYCODE_MAP = makeKeycodeMap([ [Keys.VK_LEFT, 1, -0x8], ]); +function newPOKEYAudio() { + var pokey1 = new POKEYDeviceChannel(); + var pokey2 = new POKEYDeviceChannel(); + var audio = new MasterAudio(); + audio.pokey1 = pokey1; + audio.pokey2 = pokey2; + audio.master.addChannel(pokey1); + audio.master.addChannel(pokey2); + return audio; +} + var AtariVectorPlatform = function(mainElement) { var self = this; var cpuFrequency = 1500000.0; @@ -78,7 +89,7 @@ var AtariVectorPlatform = function(mainElement) { // create video/audio video = new VectorVideo(mainElement,1024,1024); dvg = new DVGBWStateMachine(bus, video, 0x4000); - audio = new SampleAudio(cpuFrequency); + audio = newPOKEYAudio(); video.create(); timer = new AnimationTimer(60, function() { video.clear(); @@ -120,9 +131,11 @@ var AtariVectorPlatform = function(mainElement) { } this.pause = function() { timer.stop(); + audio.stop(); } this.resume = function() { timer.start(); + audio.start(); } this.reset = function() { this.clearDebug(); @@ -202,8 +215,8 @@ var AtariColorVectorPlatform = function(mainElement) { write: new AddressDecoder([ [0x0, 0x7ff, 0x7ff, function(a,v) { cpuram.mem[a] = v; }], [0x2000, 0x27ff, 0x7ff, function(a,v) { dvgram.mem[a] = v; }], - [0x6000, 0x67ff, 0x7ff, function(a,v) { /* pokey1 */ }], - [0x6800, 0x6fff, 0x7ff, function(a,v) { /* pokey2 */ }], + [0x6000, 0x67ff, 0xf, function(a,v) { audio.pokey1.setRegister(a, v); }], + [0x6800, 0x6fff, 0xf, function(a,v) { audio.pokey2.setRegister(a, v); }], [0x8800, 0x8800, 0, function(a,v) { /* LEDs, etc */ }], [0x8840, 0x8840, 0, function(a,v) { dvg.runUntilHalt(0); }], [0x8880, 0x8880, 0, function(a,v) { dvg.reset(); }], @@ -223,7 +236,7 @@ var AtariColorVectorPlatform = function(mainElement) { // create video/audio video = new VectorVideo(mainElement,1024,1024); dvg = new DVGColorStateMachine(bus, video, 0x2000); - audio = new SampleAudio(cpuFrequency); + audio = newPOKEYAudio(); video.create(); timer = new AnimationTimer(60, function() { video.clear(); @@ -260,9 +273,11 @@ var AtariColorVectorPlatform = function(mainElement) { } this.pause = function() { timer.stop(); + audio.stop(); } this.resume = function() { timer.start(); + audio.start(); } this.reset = function() { this.clearDebug(); @@ -322,6 +337,8 @@ var Z80ColorVectorPlatform = function(mainElement, proto) { ]), write: new AddressDecoder([ + [0x8000, 0x800f, 0xf, function(a,v) { audio.pokey1.setRegister(a, v); }], + [0x8010, 0x801f, 0xf, function(a,v) { audio.pokey2.setRegister(a, v); }], [0x8840, 0x8840, 0, function(a,v) { dvg.runUntilHalt(0); }], [0x8880, 0x8880, 0, function(a,v) { dvg.reset(); }], [0xa000, 0xdfff, 0x3fff, function(a,v) { dvgram.mem[a] = v; }], @@ -333,7 +350,7 @@ var Z80ColorVectorPlatform = function(mainElement, proto) { // create video/audio video = new VectorVideo(mainElement,1024,1024); dvg = new DVGColorStateMachine(bus, video, 0xa000); - audio = new SampleAudio(cpuFrequency); + audio = newPOKEYAudio(); video.create(); timer = new AnimationTimer(60, function() { video.clear(); @@ -358,9 +375,11 @@ var Z80ColorVectorPlatform = function(mainElement, proto) { } this.pause = function() { timer.stop(); + audio.stop(); } this.resume = function() { timer.start(); + audio.start(); } this.reset = function() { cpu.reset();