"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} */ self.source_buffer_previous = EMPTY_BUFFER; /** @type{Array} */ 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"); };