gigatron/rom/Contrib/lb3361/runjs/html/audio.js
2025-01-28 19:17:01 +03:00

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();
}
}
}
}