import { SampledAudioSink } from "./devices"; // from TSS declare var MasterChannel, AudioLooper, PsgDeviceChannel; export class MasterAudio { master = new MasterChannel(); looper; start() { if (!this.looper) { this.looper = new AudioLooper(512); this.looper.setChannel(this.master); this.looper.activate(); } } stop() { if (this.looper) { this.looper.setChannel(null); this.looper = null; } } } export class AY38910_Audio { master : MasterAudio; psg = new PsgDeviceChannel(); curreg = 0; constructor(master : MasterAudio) { this.master = master; this.psg.setMode(PsgDeviceChannel.MODE_SIGNED); this.psg.setDevice(PsgDeviceChannel.DEVICE_AY_3_8910); master.master.addChannel(this.psg); } reset() { for (var i=15; i>=0; i--) { this.selectRegister(i); this.setData(0); } } selectRegister(val : number) { this.curreg = val & 0xf; } setData(val : number) { this.psg.writeRegisterAY(this.curreg, val & 0xff); } readData() { return this.psg.readRegister(this.curreg); } currentRegister() { return this.curreg; } } export class SN76489_Audio { master : MasterAudio; psg = new PsgDeviceChannel(); constructor(master : MasterAudio) { this.master = master; this.psg.setMode(PsgDeviceChannel.MODE_SIGNED); this.psg.setDevice(PsgDeviceChannel.DEVICE_SN76489); master.master.addChannel(this.psg); } reset() { // TODO } setData(val : number) { this.psg.writeRegisterSN(0, val & 0xff); } } // 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 export function newPOKEYAudio(count:number) { var audio = new MasterAudio(); for (var i=1; i<=count; i++) { var pokey = new POKEYDeviceChannel(); audio['pokey'+i] = pokey; // TODO: cheezy audio.master.addChannel(pokey); } return audio; } function combinePolys(a, b) { var arr = new Uint8Array(a.length * b.length); var n = 0; for (var i=0; i 0.5 ? 1 : 0; } var bit17_5 = combinePolys(bit17, bit5); var bit5_4 = combinePolys(bit5, bit4); var wavetones = [ bit17_5, bit5, bit5_4, bit5, bit17, bit2, bit4, bit2 ]; // TIA var div2 = divideBy(2); var div6 = divideBy(6); var div31 = divideBy(31); var div93 = divideBy(93); var bit15_4 = combinePolys(bit15, bit4); var bit5_2 = combinePolys(bit5, div2); var bit5_6 = combinePolys(bit5, div6); var tiawavetones = [ bit1, bit4, bit15_4, bit5_4, div2, div2, div31, bit5_2, bit9, bit5, div31, bit1, div6, div6, div93, bit5_6 ]; // 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; 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.getRegister = function(addr) { addr &= 0xf; return regs[addr]; } this.setTIARegister = function(addr, value) { switch (addr) { case 0x17: case 0x18: regs[(addr&1)*4] = value & 0x1f; dirty = true; break; case 0x15: case 0x16: waveforms[(addr&1)*2] = tiawavetones[value & 0xf]; break; case 0x19: case 0x1a: volume[(addr&1)*2] = value & 0xf; 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 > wav.length) { cnt = counters[i] = cnt - Math.floor(cnt / wav.length) * wav.length; } var on = wav[Math.floor(cnt)]; if (on) { sample += v; } } } sample *= 64; buffer[s] = sample; buffer[s+1] = sample; } } } ////// Worker sound export var WorkerSoundChannel = function(worker) { var sampleRate; var output; var pending = []; var pendingLength = 0; worker.onmessage = function(e) { if (e && e.data && e.data.samples && output) { pending.push(e.data.samples); pendingLength += e.data.samples.length; } }; this.setBufferLength = function (length) { output = new Int16Array(length); //worker.postMessage({bufferLength:length,numChannels:2}); pendingLength = 0; }; this.getBuffer = function () { return output; }; this.setSampleRate = function (rate) { sampleRate = rate; worker.postMessage({sampleRate:rate}); }; this.generate = function (length) { if (pendingLength < length*3) { //console.log(length, pendingLength); output.fill(0); return; // TODO: send sync msg? } for (var i=0; i= buffer.length) { bufpos = 0; bufferlist[ifill] = buffer; var inext = (ifill + 1) % bufferlist.length; if (inext == idrain) { ifill = Math.floor(idrain + nbuffers/2) % bufferlist.length; //console.log('SampleAudio: skipped buffer', idrain, ifill); // TODO } else { ifill = inext; } buffer = bufferlist[ifill]; } } this.feedSample = function(value, count) { accum += value * count; sfrac += sinc * count; if (sfrac >= 1) { accum /= sfrac; while (sfrac >= 1) { this.addSingleSample(accum * sinc); sfrac -= 1; } accum *= sfrac; } } } export class SampledAudio { sa; constructor(sampleRate : number) { this.sa = new SampleAudio(sampleRate); } feedSample(value:number, count:number) { this.sa.feedSample(value, count); } start() { this.sa.start(); } stop() { this.sa.stop(); } } interface TssChannel { setBufferLength(len : number) : void; setSampleRate(rate : number) : void; getBuffer() : number[]; generate(numSamples : number) : void; } export class TssChannelAdapter { channels : TssChannel[]; audioGain = 1.0 / 8192; bufferLength : number; constructor(chans, oversample:number, sampleRate:number) { this.bufferLength = oversample * 2; this.channels = chans.generate ? [chans] : chans; // array or single channel this.channels.forEach((c) => { c.setBufferLength(this.bufferLength); c.setSampleRate(sampleRate); }); } generate(sink:SampledAudioSink) { var l = this.bufferLength; var bufs = this.channels.map((ch) => ch.getBuffer()); this.channels.forEach((ch) => { ch.generate(l); }); for (let i=0; i total += buf[i]); sink.feedSample(total * this.audioGain, 1); }; } }