| "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 (window.AudioContext || window.webkitAudioContext)(); |
| |
| /** @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) |
| .connect(this.node_gain_left) |
| .connect(this.node_merger, 0, 0); |
| this.node_treble_right |
| .connect(this.node_bass_right) |
| .connect(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); |
| }, this); |
| |
| bus.register("mixer-gain-left", function(decibels) |
| { |
| decibels = /** @type{number} */(decibels); |
| this.gain_left = Math.pow(10, decibels / 20); |
| this.update(); |
| }, this); |
| |
| bus.register("mixer-gain-right", function(decibels) |
| { |
| decibels = /** @type{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) |
| { |
| 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) |
| .connect(destination_left); |
| this.node_splitter |
| .connect(this.node_gain_right, 1) |
| .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) |
| { |
| 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 |
| |
| var worklet_string = ` |
| 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), |
| ]; |
| |
| class DACProcessor extends AudioWorkletProcessor |
| { |
| constructor() |
| { |
| super(); |
| |
| // Params |
| |
| this.kernel_size = 3; |
| |
| // States |
| |
| // Buffers waiting for their turn to be consumed |
| this.queue_data = new Array(1024); |
| this.queue_start = 0; |
| this.queue_end = 0; |
| this.queue_length = 0; |
| this.queue_size = this.queue_data.length; |
| this.queued_samples = 0; |
| |
| // Buffers being actively consumed |
| /** @type{Array<Float32Array>} */ |
| this.source_buffer_previous = EMPTY_BUFFER; |
| /** @type{Array<Float32Array>} */ |
| this.source_buffer_current = EMPTY_BUFFER; |
| |
| // Cached length of source_buffer_previous |
| this.source_length_previous = this.source_buffer_previous.length; |
| |
| // Ratio of alienland sample rate to homeland sample rate. |
| this.source_samples_per_destination = 1.0; |
| |
| // Integer representing the position of the first destination sample |
| // for the current block, relative to source_buffer_current. |
| this.source_block_start = 0; |
| |
| // Real number representing the position of the current destination |
| // sample relative to source_buffer_current, since source_block_index. |
| this.source_time = 0.0; |
| |
| // Same as source_time but rounded down to an index. |
| this.source_offset = 0; |
| |
| // Interface |
| |
| this.port.onmessage = (event) => |
| { |
| switch(event.data.type) |
| { |
| case "queue": |
| this.queue_push(event.data.value); |
| break; |
| case "sampling-rate": |
| this.source_samples_per_destination = event.data.value / sampleRate; |
| break; |
| } |
| }; |
| } |
| |
| process(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; |
| } |
| |
| kernel(x) |
| { |
| return sinc(x) * sinc(x / this.kernel_size); |
| } |
| |
| get_sample(index, channel) |
| { |
| if(index < 0) |
| { |
| index += this.source_length_previous; |
| return this.source_buffer_previous[channel][index]; |
| } |
| else |
| { |
| return this.source_buffer_current[channel][index]; |
| } |
| } |
| |
| ensure_enough_data(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; |
| this.source_length_previous = current_length; |
| } |
| } |
| |
| prepare_next_buffer() |
| { |
| 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(); |
| } |
| |
| pump() |
| { |
| if(this.queued_samples / this.source_samples_per_destination < QUEUE_RESERVE) |
| { |
| this.port.postMessage( |
| { |
| type: "pump", |
| }); |
| } |
| } |
| |
| queue_push(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(); |
| } |
| } |
| |
| queue_shift() |
| { |
| 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; |
| } |
| |
| dbg_log(message) |
| { |
| 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); |
| |
| 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], |
| }); |
| |
| 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); |
| buffer.copyToChannel(data[0], 0); |
| buffer.copyToChannel(data[1], 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) |
| .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"); |
| }; |