123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119 |
- "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 = window.AudioContext ? new AudioContext() : new 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");
- }
- SpeakerAdapter.prototype.destroy = function()
- {
- this.audio_context && this.audio_context.close();
- this.dac && this.dac.node_processor && this.dac.node_processor.port.close();
- };
- /**
- * @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");
- };
|