116 lines
3.6 KiB
JavaScript
116 lines
3.6 KiB
JavaScript
const SAMPLES_PER_SECOND = 44100;
|
|
|
|
var AudioContext = window.AudioContext || window.webkitAudioContext;
|
|
|
|
/** Audio output */
|
|
export class Audio {
|
|
/**
|
|
* Create an Audio.
|
|
* @param {Gigatron} cpu - The CPU
|
|
*/
|
|
constructor(cpu) {
|
|
this.cpu = cpu;
|
|
let context = this.context = new AudioContext();
|
|
|
|
this.mute = false;
|
|
this.volume = 0.33;
|
|
this.cycle = 0;
|
|
this.bias = 0;
|
|
this.alpha = 0.99;
|
|
this.scheduled = 0;
|
|
this.full = false;
|
|
|
|
let numSamples = Math.floor(SAMPLES_PER_SECOND / 50);
|
|
this.buffers = [];
|
|
for (let i = 0; i < 8; i++) {
|
|
let buffer = context.createBuffer(1,
|
|
numSamples,
|
|
SAMPLES_PER_SECOND);
|
|
this.buffers.push(buffer);
|
|
}
|
|
|
|
this.headBufferIndex = 0;
|
|
this.tailBufferIndex = 0;
|
|
this.duration = this.buffers[0].duration;
|
|
this.tailTime = 0; // time at which tail buffer will start
|
|
this.headTime = this.duration; // time at which head buffer will end
|
|
this.channelData = this.buffers[0].getChannelData(0);
|
|
this.sampleIndex = 0;
|
|
}
|
|
|
|
/** drain completed head buffers */
|
|
drain() {
|
|
let currentTime = this.context.currentTime;
|
|
let headTime = this.headTime;
|
|
let headBufferIndex = this.headBufferIndex;
|
|
let scheduled = this.scheduled;
|
|
let numBuffers = this.buffers.length;
|
|
|
|
while (scheduled > 0 && headTime < currentTime) {
|
|
headBufferIndex = (headBufferIndex == numBuffers - 1) ? 0 :
|
|
(headBufferIndex + 1);
|
|
headTime += this.duration;
|
|
scheduled--;
|
|
}
|
|
|
|
this.headTime = headTime;
|
|
this.scheduled = scheduled;
|
|
this.full = scheduled == numBuffers;
|
|
}
|
|
|
|
/** flush current tail buffer */
|
|
_flushChannelData() {
|
|
let context = this.context;
|
|
let currentTime = context.currentTime;
|
|
let tailBufferIndex = this.tailBufferIndex;
|
|
let buffer = this.buffers[tailBufferIndex];
|
|
let scheduled = this.scheduled;
|
|
let numBuffers = this.buffers.length;
|
|
|
|
/* if the tail can't keep ahead of realtime, jump it to now */
|
|
if (this.tailTime < currentTime) {
|
|
// console.log('audio skip');
|
|
this.tailTime = currentTime;
|
|
this.headTime = currentTime + this.duration;
|
|
this.headBufferIndex = tailBufferIndex;
|
|
scheduled = 0;
|
|
}
|
|
|
|
if (!this.mute) {
|
|
let source = context.createBufferSource();
|
|
source.buffer = buffer;
|
|
source.connect(context.destination);
|
|
source.start(this.tailTime);
|
|
}
|
|
|
|
scheduled++;
|
|
this.tailTime += this.duration;
|
|
|
|
tailBufferIndex = (tailBufferIndex == numBuffers - 1) ? 0 :
|
|
(tailBufferIndex + 1);
|
|
|
|
this.channelData = this.buffers[tailBufferIndex].getChannelData(0);
|
|
this.sampleIndex = 0;
|
|
this.tailBufferIndex = tailBufferIndex;
|
|
this.scheduled = scheduled;
|
|
this.full = scheduled == numBuffers;
|
|
}
|
|
|
|
/** advance simulation by one tick */
|
|
tick() {
|
|
this.cycle += SAMPLES_PER_SECOND;
|
|
if (this.cycle >= this.cpu.hz) {
|
|
this.cycle -= this.cpu.hz;
|
|
|
|
let sample = (this.cpu.outx >> 4) / 8;
|
|
this.bias = (this.alpha * this.bias) + ((1 - this.alpha) * sample);
|
|
sample = (sample - this.bias) * this.volume;
|
|
this.channelData[this.sampleIndex++] = sample;
|
|
|
|
if (this.sampleIndex == this.channelData.length) {
|
|
this._flushChannelData();
|
|
}
|
|
}
|
|
}
|
|
}
|