v86/src/browser/speaker.js
2021-01-03 02:07:20 -06:00

1114 lines
31 KiB
JavaScript
Executable file

"use strict";
/** @const */
var DAC_QUEUE_RESERVE = 0.2;
/** @const */
var AUDIOBUFFER_MINIMUM_SAMPLING_RATE = 8000;
/**
* @constructor
* @param {!BusConnector} bus
*/
function SpeakerAdapter(bus)
{
if(typeof window === "undefined")
{
return;
}
if(!window.AudioContext && !window["webkitAudioContext"])
{
console.warn("Web browser doesn't support Web Audio API");
return;
}
var SpeakerDAC = window.AudioWorklet ? SpeakerWorkletDAC : SpeakerBufferSourceDAC;
/** @const */
this.bus = bus;
/** @const */
this.audio_context = new AudioContext();
/** @const */
this.mixer = new SpeakerMixer(bus, this.audio_context);
/** @const */
this.pcspeaker = new PCSpeaker(bus, this.audio_context, this.mixer);
/** @const */
this.dac = new SpeakerDAC(bus, this.audio_context, this.mixer);
this.pcspeaker.start();
bus.register("emulator-stopped", function()
{
this.audio_context.suspend();
}, this);
bus.register("emulator-started", function()
{
this.audio_context.resume();
}, this);
bus.register("speaker-confirm-initialized", function()
{
bus.send("speaker-has-initialized");
}, this);
bus.send("speaker-has-initialized");
}
/**
* @constructor
* @param {!BusConnector} bus
* @param {!AudioContext} audio_context
*/
function SpeakerMixer(bus, audio_context)
{
/** @const */
this.audio_context = audio_context;
this.sources = new Map();
// States
this.volume_both = 1;
this.volume_left = 1;
this.volume_right = 1;
this.gain_left = 1;
this.gain_right = 1;
// Nodes
// TODO: Find / calibrate / verify the filter frequencies
this.node_treble_left = this.audio_context.createBiquadFilter();
this.node_treble_right = this.audio_context.createBiquadFilter();
this.node_treble_left.type = "highshelf";
this.node_treble_right.type = "highshelf";
this.node_treble_left.frequency.setValueAtTime(2000, this.audio_context.currentTime);
this.node_treble_right.frequency.setValueAtTime(2000, this.audio_context.currentTime);
this.node_bass_left = this.audio_context.createBiquadFilter();
this.node_bass_right = this.audio_context.createBiquadFilter();
this.node_bass_left.type = "lowshelf";
this.node_bass_right.type = "lowshelf";
this.node_bass_left.frequency.setValueAtTime(200, this.audio_context.currentTime);
this.node_bass_right.frequency.setValueAtTime(200, this.audio_context.currentTime);
this.node_gain_left = this.audio_context.createGain();
this.node_gain_right = this.audio_context.createGain();
this.node_merger = this.audio_context.createChannelMerger(2);
// Graph
this.input_left = this.node_treble_left;
this.input_right = this.node_treble_right;
this.node_treble_left.connect(this.node_bass_left);
this.node_bass_left.connect(this.node_gain_left);
this.node_gain_left.connect(this.node_merger, 0, 0);
this.node_treble_right.connect(this.node_bass_right);
this.node_bass_right.connect(this.node_gain_right);
this.node_gain_right.connect(this.node_merger, 0, 1);
this.node_merger.connect(this.audio_context.destination);
// Interface
bus.register("mixer-connect", function(data)
{
var source_id = data[0];
var channel = data[1];
this.connect_source(source_id, channel);
}, this);
bus.register("mixer-disconnect", function(data)
{
var source_id = data[0];
var channel = data[1];
this.disconnect_source(source_id, channel);
}, this);
bus.register("mixer-volume", function(data)
{
var source_id = data[0];
var channel = data[1];
var decibels = data[2];
var gain = Math.pow(10, decibels / 20);
var source = source_id === MIXER_SRC_MASTER ? this : this.sources.get(source_id);
if(source === undefined)
{
dbg_assert(false, "Mixer set volume - cannot set volume for undefined source: " + source_id);
return;
}
source.set_volume(gain, channel);
}, this);
bus.register("mixer-gain-left", function(/** number */ decibels)
{
this.gain_left = Math.pow(10, decibels / 20);
this.update();
}, this);
bus.register("mixer-gain-right", function(/** number */ decibels)
{
this.gain_right = Math.pow(10, decibels / 20);
this.update();
}, this);
function create_gain_handler(audio_node)
{
return function(decibels)
{
audio_node.gain.setValueAtTime(decibels, this.audio_context.currentTime);
};
}
bus.register("mixer-treble-left", create_gain_handler(this.node_treble_left), this);
bus.register("mixer-treble-right", create_gain_handler(this.node_treble_right), this);
bus.register("mixer-bass-left", create_gain_handler(this.node_bass_left), this);
bus.register("mixer-bass-right", create_gain_handler(this.node_bass_right), this);
}
/**
* @param {!AudioNode} source_node
* @param {number} source_id
* @return {SpeakerMixerSource}
*/
SpeakerMixer.prototype.add_source = function(source_node, source_id)
{
var source = new SpeakerMixerSource(
this.audio_context,
source_node,
this.input_left,
this.input_right
);
dbg_assert(!this.sources.has(source_id), "Mixer add source - overwritting source: " + source_id);
this.sources.set(source_id, source);
return source;
};
/**
* @param {number} source_id
* @param {number=} channel
*/
SpeakerMixer.prototype.connect_source = function(source_id, channel)
{
var source = this.sources.get(source_id);
if(source === undefined)
{
dbg_assert(false, "Mixer connect - cannot connect undefined source: " + source_id);
return;
}
source.connect(channel);
};
/**
* @param {number} source_id
* @param {number=} channel
*/
SpeakerMixer.prototype.disconnect_source = function(source_id, channel)
{
var source = this.sources.get(source_id);
if(source === undefined)
{
dbg_assert(false, "Mixer disconnect - cannot disconnect undefined source: " + source_id);
return;
}
source.disconnect(channel);
};
/**
* @param {number} value
* @param {number=} channel
*/
SpeakerMixer.prototype.set_volume = function(value, channel)
{
if(channel === undefined)
{
channel = MIXER_CHANNEL_BOTH;
}
switch(channel)
{
case MIXER_CHANNEL_LEFT:
this.volume_left = value;
break;
case MIXER_CHANNEL_RIGHT:
this.volume_right = value;
break;
case MIXER_CHANNEL_BOTH:
this.volume_both = value;
break;
default:
dbg_assert(false, "Mixer set master volume - unknown channel: " + channel);
return;
}
this.update();
};
SpeakerMixer.prototype.update = function()
{
var net_gain_left = this.volume_both * this.volume_left * this.gain_left;
var net_gain_right = this.volume_both * this.volume_right * this.gain_right;
this.node_gain_left.gain.setValueAtTime(net_gain_left, this.audio_context.currentTime);
this.node_gain_right.gain.setValueAtTime(net_gain_right, this.audio_context.currentTime);
};
/**
* @constructor
* @param {!AudioContext} audio_context
* @param {!AudioNode} source_node
* @param {!AudioNode} destination_left
* @param {!AudioNode} destination_right
*/
function SpeakerMixerSource(audio_context, source_node, destination_left, destination_right)
{
/** @const */
this.audio_context = audio_context;
// States
this.connected_left = true;
this.connected_right = true;
this.gain_hidden = 1;
this.volume_both = 1;
this.volume_left = 1;
this.volume_right = 1;
// Nodes
this.node_splitter = audio_context.createChannelSplitter(2);
this.node_gain_left = audio_context.createGain();
this.node_gain_right = audio_context.createGain();
// Graph
source_node.connect(this.node_splitter);
this.node_splitter.connect(this.node_gain_left, 0);
this.node_gain_left.connect(destination_left);
this.node_splitter.connect(this.node_gain_right, 1);
this.node_gain_right.connect(destination_right);
}
SpeakerMixerSource.prototype.update = function()
{
var net_gain_left = this.connected_left * this.gain_hidden * this.volume_both * this.volume_left;
var net_gain_right = this.connected_right * this.gain_hidden * this.volume_both * this.volume_right;
this.node_gain_left.gain.setValueAtTime(net_gain_left, this.audio_context.currentTime);
this.node_gain_right.gain.setValueAtTime(net_gain_right, this.audio_context.currentTime);
};
/** @param {number=} channel */
SpeakerMixerSource.prototype.connect = function(channel)
{
var both = !channel || channel === MIXER_CHANNEL_BOTH;
if(both || channel === MIXER_CHANNEL_LEFT)
{
this.connected_left = true;
}
if(both || channel === MIXER_CHANNEL_RIGHT)
{
this.connected_right = true;
}
this.update();
};
/** @param {number=} channel */
SpeakerMixerSource.prototype.disconnect = function(channel)
{
var both = !channel || channel === MIXER_CHANNEL_BOTH;
if(both || channel === MIXER_CHANNEL_LEFT)
{
this.connected_left = false;
}
if(both || channel === MIXER_CHANNEL_RIGHT)
{
this.connected_right = false;
}
this.update();
};
/**
* @param {number} value
* @param {number=} channel
*/
SpeakerMixerSource.prototype.set_volume = function(value, channel)
{
if(channel === undefined)
{
channel = MIXER_CHANNEL_BOTH;
}
switch(channel)
{
case MIXER_CHANNEL_LEFT:
this.volume_left = value;
break;
case MIXER_CHANNEL_RIGHT:
this.volume_right = value;
break;
case MIXER_CHANNEL_BOTH:
this.volume_both = value;
break;
default:
dbg_assert(false, "Mixer set volume - unknown channel: " + channel);
return;
}
this.update();
};
SpeakerMixerSource.prototype.set_gain_hidden = function(value)
{
this.gain_hidden = value;
};
/**
* @constructor
* @param {!BusConnector} bus
* @param {!AudioContext} audio_context
* @param {!SpeakerMixer} mixer
*/
function PCSpeaker(bus, audio_context, mixer)
{
// Nodes
this.node_oscillator = audio_context.createOscillator();
this.node_oscillator.type = "square";
this.node_oscillator.frequency.setValueAtTime(440, audio_context.currentTime);
// Interface
this.mixer_connection = mixer.add_source(this.node_oscillator, MIXER_SRC_PCSPEAKER);
this.mixer_connection.disconnect();
bus.register("pcspeaker-enable", function()
{
mixer.connect_source(MIXER_SRC_PCSPEAKER);
}, this);
bus.register("pcspeaker-disable", function()
{
mixer.disconnect_source(MIXER_SRC_PCSPEAKER);
}, this);
bus.register("pcspeaker-update", function(data)
{
var counter_mode = data[0];
var counter_reload = data[1];
var frequency = 0;
var beep_enabled = counter_mode === 3;
if(beep_enabled)
{
frequency = OSCILLATOR_FREQ * 1000 / counter_reload;
frequency = Math.min(frequency, this.node_oscillator.frequency.maxValue);
frequency = Math.max(frequency, 0);
}
this.node_oscillator.frequency.setValueAtTime(frequency, audio_context.currentTime);
}, this);
}
PCSpeaker.prototype.start = function()
{
this.node_oscillator.start();
};
/**
* @constructor
* @param {!BusConnector} bus
* @param {!AudioContext} audio_context
* @param {!SpeakerMixer} mixer
*/
function SpeakerWorkletDAC(bus, audio_context, mixer)
{
/** @const */
this.bus = bus;
/** @const */
this.audio_context = audio_context;
// State
this.enabled = false;
this.sampling_rate = 48000;
// Worklet
function worklet()
{
/** @const */
var RENDER_QUANTUM = 128;
/** @const */
var MINIMUM_BUFFER_SIZE = 2 * RENDER_QUANTUM;
/** @const */
var QUEUE_RESERVE = 1024;
function sinc(x)
{
if(x === 0) return 1;
x *= Math.PI;
return Math.sin(x) / x;
}
var EMPTY_BUFFER =
[
new Float32Array(MINIMUM_BUFFER_SIZE),
new Float32Array(MINIMUM_BUFFER_SIZE),
];
/**
* @constructor
* @extends AudioWorkletProcessor
*/
function DACProcessor()
{
var self = Reflect.construct(AudioWorkletProcessor, [], DACProcessor);
// Params
self.kernel_size = 3;
// States
// Buffers waiting for their turn to be consumed
self.queue_data = new Array(1024);
self.queue_start = 0;
self.queue_end = 0;
self.queue_length = 0;
self.queue_size = self.queue_data.length;
self.queued_samples = 0;
// Buffers being actively consumed
/** @type{Array<Float32Array>} */
self.source_buffer_previous = EMPTY_BUFFER;
/** @type{Array<Float32Array>} */
self.source_buffer_current = EMPTY_BUFFER;
// Ratio of alienland sample rate to homeland sample rate.
self.source_samples_per_destination = 1.0;
// Integer representing the position of the first destination sample
// for the current block, relative to source_buffer_current.
self.source_block_start = 0;
// Real number representing the position of the current destination
// sample relative to source_buffer_current, since source_block_start.
self.source_time = 0.0;
// Same as source_time but rounded down to an index.
self.source_offset = 0;
// Interface
self.port.onmessage = (event) =>
{
switch(event.data.type)
{
case "queue":
self.queue_push(event.data.value);
break;
case "sampling-rate":
self.source_samples_per_destination = event.data.value / sampleRate;
break;
}
};
return self;
}
Reflect.setPrototypeOf(DACProcessor.prototype, AudioWorkletProcessor.prototype);
Reflect.setPrototypeOf(DACProcessor, AudioWorkletProcessor);
DACProcessor.prototype["process"] =
DACProcessor.prototype.process = function(inputs, outputs, parameters)
{
for(var i = 0; i < outputs[0][0].length; i++)
{
// Lanczos resampling
var sum0 = 0;
var sum1 = 0;
var start = this.source_offset - this.kernel_size + 1;
var end = this.source_offset + this.kernel_size;
for(var j = start; j <= end; j++)
{
var convolute_index = this.source_block_start + j;
sum0 += this.get_sample(convolute_index, 0) * this.kernel(this.source_time - j);
sum1 += this.get_sample(convolute_index, 1) * this.kernel(this.source_time - j);
}
if(isNaN(sum0) || isNaN(sum1))
{
// NaN values cause entire audio graph to cease functioning.
sum0 = sum1 = 0;
this.dbg_log("ERROR: NaN values! Ignoring for now.");
}
outputs[0][0][i] = sum0;
outputs[0][1][i] = sum1;
this.source_time += this.source_samples_per_destination;
this.source_offset = Math.floor(this.source_time);
}
// +2 to safeguard against rounding variations
var samples_needed_per_block = this.source_offset;
samples_needed_per_block += this.kernel_size + 2;
this.source_time -= this.source_offset;
this.source_block_start += this.source_offset;
this.source_offset = 0;
// Note: This needs to be done after source_block_start is updated.
this.ensure_enough_data(samples_needed_per_block);
return true;
};
DACProcessor.prototype.kernel = function(x)
{
return sinc(x) * sinc(x / this.kernel_size);
};
DACProcessor.prototype.get_sample = function(index, channel)
{
if(index < 0)
{
// -ve index represents previous buffer
// <-------|
// [Previous buffer][Current buffer]
index += this.source_buffer_previous[0].length;
return this.source_buffer_previous[channel][index];
}
else
{
return this.source_buffer_current[channel][index];
}
};
DACProcessor.prototype.ensure_enough_data = function(needed)
{
var current_length = this.source_buffer_current[0].length;
var remaining = current_length - this.source_block_start;
if(remaining < needed)
{
this.prepare_next_buffer();
this.source_block_start -= current_length;
}
};
DACProcessor.prototype.prepare_next_buffer = function()
{
if(this.queued_samples < MINIMUM_BUFFER_SIZE && this.queue_length)
{
this.dbg_log("Not enough samples - should not happen during midway of playback");
}
this.source_buffer_previous = this.source_buffer_current;
this.source_buffer_current = this.queue_shift();
var sample_count = this.source_buffer_current[0].length;
if(sample_count < MINIMUM_BUFFER_SIZE)
{
// Unfortunately, this single buffer is too small :(
var queue_pos = this.queue_start;
var buffer_count = 0;
// Figure out how many small buffers to combine.
while(sample_count < MINIMUM_BUFFER_SIZE && buffer_count < this.queue_length)
{
sample_count += this.queue_data[queue_pos][0].length;
queue_pos = queue_pos + 1 & this.queue_size - 1;
buffer_count++;
}
// Note: if not enough buffers, this will be end-padded with zeros:
var new_big_buffer_size = Math.max(sample_count, MINIMUM_BUFFER_SIZE);
var new_big_buffer =
[
new Float32Array(new_big_buffer_size),
new Float32Array(new_big_buffer_size),
];
// Copy the first, already-shifted, small buffer into the new buffer.
new_big_buffer[0].set(this.source_buffer_current[0]);
new_big_buffer[1].set(this.source_buffer_current[1]);
var new_big_buffer_pos = this.source_buffer_current[0].length;
// Copy the rest.
for(var i = 0; i < buffer_count; i++)
{
var small_buffer = this.queue_shift();
new_big_buffer[0].set(small_buffer[0], new_big_buffer_pos);
new_big_buffer[1].set(small_buffer[1], new_big_buffer_pos);
new_big_buffer_pos += small_buffer[0].length;
}
// Pretend that everything's just fine.
this.source_buffer_current = new_big_buffer;
}
this.pump();
};
DACProcessor.prototype.pump = function()
{
if(this.queued_samples / this.source_samples_per_destination < QUEUE_RESERVE)
{
this.port.postMessage(
{
type: "pump",
});
}
};
DACProcessor.prototype.queue_push = function(item)
{
if(this.queue_length < this.queue_size)
{
this.queue_data[this.queue_end] = item;
this.queue_end = this.queue_end + 1 & this.queue_size - 1;
this.queue_length++;
this.queued_samples += item[0].length;
this.pump();
}
};
DACProcessor.prototype.queue_shift = function()
{
if(!this.queue_length)
{
return EMPTY_BUFFER;
}
var item = this.queue_data[this.queue_start];
this.queue_data[this.queue_start] = null;
this.queue_start = this.queue_start + 1 & this.queue_size - 1;
this.queue_length--;
this.queued_samples -= item[0].length;
return item;
};
DACProcessor.prototype.dbg_log = function(message)
{
if(DEBUG)
{
this.port.postMessage(
{
type: "debug-log",
value: message,
});
}
};
registerProcessor("dac-processor", DACProcessor);
}
var worklet_string = worklet.toString();
var worklet_code_start = worklet_string.indexOf("{") + 1;
var worklet_code_end = worklet_string.lastIndexOf("}");
var worklet_code = worklet_string.substring(worklet_code_start, worklet_code_end);
if(DEBUG)
{
worklet_code = "var DEBUG = true;\n" + worklet_code;
}
var worklet_blob = new Blob([worklet_code], { type: "application/javascript" });
var worklet_url = URL.createObjectURL(worklet_blob);
/** @type {AudioWorkletNode} */
this.node_processor = null;
// Placeholder pass-through node to connect to, when worklet node is not ready yet.
this.node_output = this.audio_context.createGain();
this.audio_context
.audioWorklet
.addModule(worklet_url)
.then(() =>
{
URL.revokeObjectURL(worklet_url);
this.node_processor = new AudioWorkletNode(this.audio_context, "dac-processor",
{
numberOfInputs: 0,
numberOfOutputs: 1,
outputChannelCount: [2],
parameterData: {},
processorOptions: {},
});
this.node_processor.port.postMessage(
{
type: "sampling-rate",
value: this.sampling_rate,
});
this.node_processor.port.onmessage = (event) =>
{
switch(event.data.type)
{
case "pump":
this.pump();
break;
case "debug-log":
dbg_log("SpeakerWorkletDAC - Worklet: " + event.data.value);
break;
}
};
// Graph
this.node_processor.connect(this.node_output);
});
// Interface
this.mixer_connection = mixer.add_source(this.node_output, MIXER_SRC_DAC);
this.mixer_connection.set_gain_hidden(3);
bus.register("dac-send-data", function(data)
{
this.queue(data);
}, this);
bus.register("dac-enable", function(enabled)
{
this.enabled = true;
}, this);
bus.register("dac-disable", function()
{
this.enabled = false;
}, this);
bus.register("dac-tell-sampling-rate", function(/** number */ rate)
{
dbg_assert(rate > 0, "Sampling rate should be nonzero");
this.sampling_rate = rate;
if(!this.node_processor)
{
return;
}
this.node_processor.port.postMessage(
{
type: "sampling-rate",
value: rate,
});
}, this);
if(DEBUG)
{
this.debugger = new SpeakerDACDebugger(this.audio_context, this.node_output);
}
}
SpeakerWorkletDAC.prototype.queue = function(data)
{
if(!this.node_processor)
{
return;
}
if(DEBUG)
{
this.debugger.push_queued_data(data);
}
this.node_processor.port.postMessage(
{
type: "queue",
value: data,
}, [data[0].buffer, data[1].buffer]);
};
SpeakerWorkletDAC.prototype.pump = function()
{
if(!this.enabled)
{
return;
}
this.bus.send("dac-request-data");
};
/**
* @constructor
* @param {!BusConnector} bus
* @param {!AudioContext} audio_context
* @param {!SpeakerMixer} mixer
*/
function SpeakerBufferSourceDAC(bus, audio_context, mixer)
{
/** @const */
this.bus = bus;
/** @const */
this.audio_context = audio_context;
// States
this.enabled = false;
this.sampling_rate = 22050;
this.buffered_time = 0;
this.rate_ratio = 1;
// Nodes
this.node_lowpass = this.audio_context.createBiquadFilter();
this.node_lowpass.type = "lowpass";
// Interface
this.node_output = this.node_lowpass;
this.mixer_connection = mixer.add_source(this.node_output, MIXER_SRC_DAC);
this.mixer_connection.set_gain_hidden(3);
bus.register("dac-send-data", function(data)
{
this.queue(data);
}, this);
bus.register("dac-enable", function(enabled)
{
this.enabled = true;
this.pump();
}, this);
bus.register("dac-disable", function()
{
this.enabled = false;
}, this);
bus.register("dac-tell-sampling-rate", function(/** number */ rate)
{
dbg_assert(rate > 0, "Sampling rate should be nonzero");
this.sampling_rate = rate;
this.rate_ratio = Math.ceil(AUDIOBUFFER_MINIMUM_SAMPLING_RATE / rate);
this.node_lowpass.frequency.setValueAtTime(rate / 2, this.audio_context.currentTime);
}, this);
if(DEBUG)
{
this.debugger = new SpeakerDACDebugger(this.audio_context, this.node_output);
}
}
SpeakerBufferSourceDAC.prototype.queue = function(data)
{
if(DEBUG)
{
this.debugger.push_queued_data(data);
}
var sample_count = data[0].length;
var block_duration = sample_count / this.sampling_rate;
var buffer;
if(this.rate_ratio > 1)
{
var new_sample_count = sample_count * this.rate_ratio;
var new_sampling_rate = this.sampling_rate * this.rate_ratio;
buffer = this.audio_context.createBuffer(2, new_sample_count, new_sampling_rate);
var buffer_data0 = buffer.getChannelData(0);
var buffer_data1 = buffer.getChannelData(1);
var buffer_index = 0;
for(var i = 0; i < sample_count; i++)
{
for(var j = 0; j < this.rate_ratio; j++, buffer_index++)
{
buffer_data0[buffer_index] = data[0][i];
buffer_data1[buffer_index] = data[1][i];
}
}
}
else
{
// Allocating new AudioBuffer every block
// - Memory profiles show insignificant improvements if recycling old buffers.
buffer = this.audio_context.createBuffer(2, sample_count, this.sampling_rate);
if(buffer.copyToChannel)
{
buffer.copyToChannel(data[0], 0);
buffer.copyToChannel(data[1], 1);
}
else
{
// Safari doesn't support copyToChannel yet. See #286
buffer.getChannelData(0).set(data[0]);
buffer.getChannelData(1).set(data[1]);
}
}
var source = this.audio_context.createBufferSource();
source.buffer = buffer;
source.connect(this.node_lowpass);
source.addEventListener("ended", this.pump.bind(this));
var current_time = this.audio_context.currentTime;
if(this.buffered_time < current_time)
{
dbg_log("Speaker DAC - Creating/Recreating reserve - shouldn't occur frequently during playback");
// Schedule pump() to queue evenly, starting from current time
this.buffered_time = current_time;
var target_silence_duration = DAC_QUEUE_RESERVE - block_duration;
var current_silence_duration = 0;
while(current_silence_duration <= target_silence_duration)
{
current_silence_duration += block_duration;
this.buffered_time += block_duration;
setTimeout(() => this.pump(), current_silence_duration * 1000);
}
}
source.start(this.buffered_time);
this.buffered_time += block_duration;
// Chase the schedule - ensure reserve is full
setTimeout(() => this.pump(), 0);
};
SpeakerBufferSourceDAC.prototype.pump = function()
{
if(!this.enabled)
{
return;
}
if(this.buffered_time - this.audio_context.currentTime > DAC_QUEUE_RESERVE)
{
return;
}
this.bus.send("dac-request-data");
};
/**
* @constructor
*/
function SpeakerDACDebugger(audio_context, source_node)
{
/** @const */
this.audio_context = audio_context;
/** @const */
this.node_source = source_node;
this.node_processor = null;
this.node_gain = this.audio_context.createGain();
this.node_gain.gain.setValueAtTime(0, this.audio_context.currentTime);
this.node_gain.connect(this.audio_context.destination);
this.is_active = false;
this.queued_history = [];
this.output_history = [];
this.queued = [[], []];
this.output = [[], []];
}
/** @suppress {deprecated} */
SpeakerDACDebugger.prototype.start = function(duration_ms)
{
this.is_active = true;
this.queued = [[], []];
this.output = [[], []];
this.queued_history.push(this.queued);
this.output_history.push(this.output);
this.node_processor = this.audio_context.createScriptProcessor(1024, 2, 2);
this.node_processor.onaudioprocess = (event) =>
{
this.output[0].push(event.inputBuffer.getChannelData(0).slice());
this.output[1].push(event.inputBuffer.getChannelData(1).slice());
};
this.node_source.connect(this.node_processor);
this.node_processor.connect(this.node_gain);
setTimeout(() =>
{
this.stop();
}, duration_ms);
};
SpeakerDACDebugger.prototype.stop = function()
{
this.is_active = false;
this.node_source.disconnect(this.node_processor);
this.node_processor.disconnect();
this.node_processor = null;
};
SpeakerDACDebugger.prototype.push_queued_data = function(data)
{
if(this.is_active)
{
this.queued[0].push(data[0].slice());
this.queued[1].push(data[1].slice());
}
};
// Useful for Audacity imports
SpeakerDACDebugger.prototype.download_txt = function(history_id, channel)
{
var txt = this.output_history[history_id][channel]
.map((v) => v.join(" "))
.join(" ");
dump_file(txt, "dacdata.txt");
};
// Useful for general plotting
SpeakerDACDebugger.prototype.download_csv = function(history_id)
{
var buffers = this.output_history[history_id];
var csv_rows = [];
for(var buffer_id = 0; buffer_id < buffers[0].length; buffer_id++)
{
for(var i = 0; i < buffers[0][buffer_id].length; i++)
{
csv_rows.push(`${buffers[0][buffer_id][i]},${buffers[1][buffer_id][i]}`);
}
}
dump_file(csv_rows.join("\n"), "dacdata.csv");
};