2017-11-15 03:10:23 +01:00
|
|
|
"use strict";
|
|
|
|
|
2018-03-07 19:44:23 +01:00
|
|
|
/** @const */
|
|
|
|
var DAC_QUEUE_RESERVE = 0.2;
|
2018-03-13 02:32:06 +01:00
|
|
|
|
|
|
|
/** @const */
|
|
|
|
var AUDIOBUFFER_MINIMUM_SAMPLING_RATE = 8000;
|
2018-03-07 19:44:23 +01:00
|
|
|
|
2017-11-15 03:10:23 +01:00
|
|
|
/**
|
|
|
|
* @constructor
|
2018-03-10 09:51:23 +01:00
|
|
|
* @param {!BusConnector} bus
|
2017-11-15 03:10:23 +01:00
|
|
|
*/
|
|
|
|
function SpeakerAdapter(bus)
|
|
|
|
{
|
2017-12-09 21:33:48 +01:00
|
|
|
if(typeof window === "undefined")
|
2017-11-15 03:10:23 +01:00
|
|
|
{
|
|
|
|
return;
|
|
|
|
}
|
2019-07-04 01:23:39 +02:00
|
|
|
if(!window.AudioContext && !window["webkitAudioContext"])
|
2017-12-11 20:39:20 +01:00
|
|
|
{
|
|
|
|
console.warn("Web browser doesn't support Web Audio API");
|
|
|
|
return;
|
|
|
|
}
|
2017-11-15 03:10:23 +01:00
|
|
|
|
2018-03-17 09:15:23 +01:00
|
|
|
var SpeakerDAC = window.AudioWorklet ? SpeakerWorkletDAC : SpeakerBufferSourceDAC;
|
2018-03-12 01:18:05 +01:00
|
|
|
|
2018-03-10 09:51:23 +01:00
|
|
|
/** @const */
|
2017-11-15 03:10:23 +01:00
|
|
|
this.bus = bus;
|
|
|
|
|
2018-03-10 09:51:23 +01:00
|
|
|
/** @const */
|
2021-01-03 07:51:29 +01:00
|
|
|
this.audio_context = new AudioContext();
|
2017-11-15 03:10:23 +01:00
|
|
|
|
2018-03-10 09:51:23 +01:00
|
|
|
/** @const */
|
|
|
|
this.mixer = new SpeakerMixer(bus, this.audio_context);
|
2017-11-15 03:10:23 +01:00
|
|
|
|
2018-03-10 09:51:23 +01:00
|
|
|
/** @const */
|
|
|
|
this.pcspeaker = new PCSpeaker(bus, this.audio_context, this.mixer);
|
2018-03-04 05:40:28 +01:00
|
|
|
|
2018-03-10 09:51:23 +01:00
|
|
|
/** @const */
|
|
|
|
this.dac = new SpeakerDAC(bus, this.audio_context, this.mixer);
|
|
|
|
|
2018-03-18 01:07:28 +01:00
|
|
|
this.pcspeaker.start();
|
|
|
|
|
2018-03-10 09:51:23 +01:00
|
|
|
bus.register("emulator-stopped", function()
|
2018-03-04 05:40:28 +01:00
|
|
|
{
|
2018-03-10 09:51:23 +01:00
|
|
|
this.audio_context.suspend();
|
2018-03-04 05:40:28 +01:00
|
|
|
}, this);
|
2018-03-10 09:51:23 +01:00
|
|
|
|
|
|
|
bus.register("emulator-started", function()
|
2018-03-04 05:40:28 +01:00
|
|
|
{
|
2018-03-10 09:51:23 +01:00
|
|
|
this.audio_context.resume();
|
2018-03-04 05:40:28 +01:00
|
|
|
}, this);
|
2018-03-10 09:51:23 +01:00
|
|
|
|
2018-03-18 01:07:28 +01:00
|
|
|
bus.register("speaker-confirm-initialized", function()
|
|
|
|
{
|
|
|
|
bus.send("speaker-has-initialized");
|
|
|
|
}, this);
|
|
|
|
bus.send("speaker-has-initialized");
|
2018-03-10 09:51:23 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @constructor
|
|
|
|
* @param {!BusConnector} bus
|
|
|
|
* @param {!AudioContext} audio_context
|
|
|
|
*/
|
|
|
|
function SpeakerMixer(bus, audio_context)
|
|
|
|
{
|
|
|
|
/** @const */
|
|
|
|
this.audio_context = audio_context;
|
|
|
|
|
|
|
|
this.sources = new Map();
|
|
|
|
|
2018-03-10 23:13:03 +01:00
|
|
|
// States
|
2018-03-10 09:51:23 +01:00
|
|
|
|
2018-03-10 23:13:03 +01:00
|
|
|
this.volume_both = 1;
|
|
|
|
this.volume_left = 1;
|
|
|
|
this.volume_right = 1;
|
|
|
|
this.gain_left = 1;
|
|
|
|
this.gain_right = 1;
|
2018-03-10 09:51:23 +01:00
|
|
|
|
2018-03-10 23:13:03 +01:00
|
|
|
// Nodes
|
|
|
|
// TODO: Find / calibrate / verify the filter frequencies
|
2018-03-10 09:51:23 +01:00
|
|
|
|
|
|
|
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
|
|
|
|
|
2018-03-10 23:13:03 +01:00
|
|
|
this.input_left = this.node_treble_left;
|
|
|
|
this.input_right = this.node_treble_right;
|
2018-03-10 09:51:23 +01:00
|
|
|
|
2018-06-23 02:55:38 +02:00
|
|
|
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);
|
2018-03-10 09:51:23 +01:00
|
|
|
|
|
|
|
// Interface
|
|
|
|
|
|
|
|
bus.register("mixer-connect", function(data)
|
2018-03-04 05:40:28 +01:00
|
|
|
{
|
2018-03-17 22:09:45 +01:00
|
|
|
var source_id = data[0];
|
2018-03-10 09:51:23 +01:00
|
|
|
var channel = data[1];
|
2018-03-17 22:09:45 +01:00
|
|
|
this.connect_source(source_id, channel);
|
2018-03-04 05:40:28 +01:00
|
|
|
}, this);
|
2018-03-10 09:51:23 +01:00
|
|
|
|
|
|
|
bus.register("mixer-disconnect", function(data)
|
2018-02-18 08:07:19 +01:00
|
|
|
{
|
2018-03-17 22:09:45 +01:00
|
|
|
var source_id = data[0];
|
2018-03-10 09:51:23 +01:00
|
|
|
var channel = data[1];
|
2018-03-17 22:09:45 +01:00
|
|
|
this.disconnect_source(source_id, channel);
|
2018-02-18 08:07:19 +01:00
|
|
|
}, this);
|
|
|
|
|
2018-03-10 09:51:23 +01:00
|
|
|
bus.register("mixer-volume", function(data)
|
2018-03-04 05:40:28 +01:00
|
|
|
{
|
2018-03-17 22:09:45 +01:00
|
|
|
var source_id = data[0];
|
2018-03-10 09:51:23 +01:00
|
|
|
var channel = data[1];
|
|
|
|
var decibels = data[2];
|
|
|
|
|
|
|
|
var gain = Math.pow(10, decibels / 20);
|
|
|
|
|
2018-03-17 22:09:45 +01:00
|
|
|
var source = source_id === MIXER_SRC_MASTER ? this : this.sources.get(source_id);
|
2018-03-10 09:51:23 +01:00
|
|
|
|
2018-03-17 08:30:29 +01:00
|
|
|
if(source === undefined)
|
2018-03-10 09:51:23 +01:00
|
|
|
{
|
2018-03-17 22:09:45 +01:00
|
|
|
dbg_assert(false, "Mixer set volume - cannot set volume for undefined source: " + source_id);
|
2018-03-10 09:51:23 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-06-04 09:29:13 +02:00
|
|
|
source.set_volume(gain, channel);
|
2018-03-10 23:13:03 +01:00
|
|
|
}, this);
|
|
|
|
|
2018-03-20 02:14:45 +01:00
|
|
|
bus.register("mixer-gain-left", function(/** number */ decibels)
|
2018-03-10 23:13:03 +01:00
|
|
|
{
|
|
|
|
this.gain_left = Math.pow(10, decibels / 20);
|
|
|
|
this.update();
|
|
|
|
}, this);
|
2018-03-10 09:51:23 +01:00
|
|
|
|
2018-03-20 02:14:45 +01:00
|
|
|
bus.register("mixer-gain-right", function(/** number */ decibels)
|
2018-03-10 23:13:03 +01:00
|
|
|
{
|
|
|
|
this.gain_right = Math.pow(10, decibels / 20);
|
|
|
|
this.update();
|
2018-03-10 09:51:23 +01:00
|
|
|
}, this);
|
|
|
|
|
2018-03-10 23:13:03 +01:00
|
|
|
function create_gain_handler(audio_node)
|
2018-03-10 09:51:23 +01:00
|
|
|
{
|
|
|
|
return function(decibels)
|
|
|
|
{
|
2018-03-10 23:13:03 +01:00
|
|
|
audio_node.gain.setValueAtTime(decibels, this.audio_context.currentTime);
|
2018-03-10 09:51:23 +01:00
|
|
|
};
|
2018-03-17 09:15:23 +01:00
|
|
|
}
|
2018-03-10 23:13:03 +01:00
|
|
|
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);
|
2018-03-10 09:51:23 +01:00
|
|
|
}
|
2018-03-04 05:40:28 +01:00
|
|
|
|
2018-03-10 23:13:03 +01:00
|
|
|
/**
|
|
|
|
* @param {!AudioNode} source_node
|
2018-03-17 22:09:45 +01:00
|
|
|
* @param {number} source_id
|
2018-03-10 23:13:03 +01:00
|
|
|
* @return {SpeakerMixerSource}
|
|
|
|
*/
|
2018-03-17 22:09:45 +01:00
|
|
|
SpeakerMixer.prototype.add_source = function(source_node, source_id)
|
2018-03-10 09:51:23 +01:00
|
|
|
{
|
2018-03-10 23:13:03 +01:00
|
|
|
var source = new SpeakerMixerSource(
|
|
|
|
this.audio_context,
|
|
|
|
source_node,
|
|
|
|
this.input_left,
|
|
|
|
this.input_right
|
|
|
|
);
|
2018-03-04 05:40:28 +01:00
|
|
|
|
2018-03-17 22:09:45 +01:00
|
|
|
dbg_assert(!this.sources.has(source_id), "Mixer add source - overwritting source: " + source_id);
|
2018-03-04 05:40:28 +01:00
|
|
|
|
2018-03-17 22:09:45 +01:00
|
|
|
this.sources.set(source_id, source);
|
2018-03-10 23:13:03 +01:00
|
|
|
return source;
|
2018-03-10 09:51:23 +01:00
|
|
|
};
|
2018-03-04 05:40:28 +01:00
|
|
|
|
2018-03-10 09:51:23 +01:00
|
|
|
/**
|
2018-03-17 22:09:45 +01:00
|
|
|
* @param {number} source_id
|
|
|
|
* @param {number=} channel
|
2018-03-10 09:51:23 +01:00
|
|
|
*/
|
2018-03-17 22:09:45 +01:00
|
|
|
SpeakerMixer.prototype.connect_source = function(source_id, channel)
|
2018-03-10 09:51:23 +01:00
|
|
|
{
|
2018-03-17 22:09:45 +01:00
|
|
|
var source = this.sources.get(source_id);
|
2018-03-04 05:40:28 +01:00
|
|
|
|
2018-03-17 08:30:29 +01:00
|
|
|
if(source === undefined)
|
2018-03-10 09:51:23 +01:00
|
|
|
{
|
2018-03-17 22:09:45 +01:00
|
|
|
dbg_assert(false, "Mixer connect - cannot connect undefined source: " + source_id);
|
2018-03-10 09:51:23 +01:00
|
|
|
return;
|
|
|
|
}
|
2018-03-04 05:40:28 +01:00
|
|
|
|
2018-03-10 09:51:23 +01:00
|
|
|
source.connect(channel);
|
|
|
|
};
|
2018-03-04 05:40:28 +01:00
|
|
|
|
2018-03-10 09:51:23 +01:00
|
|
|
/**
|
2018-03-17 22:09:45 +01:00
|
|
|
* @param {number} source_id
|
|
|
|
* @param {number=} channel
|
2018-03-10 09:51:23 +01:00
|
|
|
*/
|
2018-03-17 22:09:45 +01:00
|
|
|
SpeakerMixer.prototype.disconnect_source = function(source_id, channel)
|
2018-03-10 09:51:23 +01:00
|
|
|
{
|
2018-03-17 22:09:45 +01:00
|
|
|
var source = this.sources.get(source_id);
|
2018-03-04 05:40:28 +01:00
|
|
|
|
2018-03-17 08:30:29 +01:00
|
|
|
if(source === undefined)
|
2018-03-10 09:51:23 +01:00
|
|
|
{
|
2018-03-17 22:09:45 +01:00
|
|
|
dbg_assert(false, "Mixer disconnect - cannot disconnect undefined source: " + source_id);
|
2018-03-10 09:51:23 +01:00
|
|
|
return;
|
|
|
|
}
|
2018-03-04 05:40:28 +01:00
|
|
|
|
2018-03-10 09:51:23 +01:00
|
|
|
source.disconnect(channel);
|
|
|
|
};
|
2018-03-04 05:40:28 +01:00
|
|
|
|
2018-03-10 23:13:03 +01:00
|
|
|
/**
|
|
|
|
* @param {number} value
|
2018-03-17 22:09:45 +01:00
|
|
|
* @param {number=} channel
|
2018-03-10 23:13:03 +01:00
|
|
|
*/
|
|
|
|
SpeakerMixer.prototype.set_volume = function(value, channel)
|
|
|
|
{
|
2018-03-20 02:28:13 +01:00
|
|
|
if(channel === undefined)
|
2018-03-10 23:13:03 +01:00
|
|
|
{
|
2018-03-17 22:09:45 +01:00
|
|
|
channel = MIXER_CHANNEL_BOTH;
|
2018-03-10 23:13:03 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
switch(channel)
|
|
|
|
{
|
2018-03-17 22:09:45 +01:00
|
|
|
case MIXER_CHANNEL_LEFT:
|
2018-03-10 23:13:03 +01:00
|
|
|
this.volume_left = value;
|
|
|
|
break;
|
2018-03-17 22:09:45 +01:00
|
|
|
case MIXER_CHANNEL_RIGHT:
|
2018-03-10 23:13:03 +01:00
|
|
|
this.volume_right = value;
|
|
|
|
break;
|
2018-03-17 22:09:45 +01:00
|
|
|
case MIXER_CHANNEL_BOTH:
|
2018-03-10 23:13:03 +01:00
|
|
|
this.volume_both = value;
|
|
|
|
break;
|
|
|
|
default:
|
2018-03-17 08:30:29 +01:00
|
|
|
dbg_assert(false, "Mixer set master volume - unknown channel: " + channel);
|
2018-03-10 23:13:03 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.update();
|
|
|
|
};
|
|
|
|
|
|
|
|
SpeakerMixer.prototype.update = function()
|
|
|
|
{
|
2018-03-17 09:15:23 +01:00
|
|
|
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;
|
2018-03-10 23:13:03 +01:00
|
|
|
|
|
|
|
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);
|
|
|
|
};
|
|
|
|
|
2018-03-10 09:51:23 +01:00
|
|
|
/**
|
|
|
|
* @constructor
|
|
|
|
* @param {!AudioContext} audio_context
|
|
|
|
* @param {!AudioNode} source_node
|
2018-03-10 23:13:03 +01:00
|
|
|
* @param {!AudioNode} destination_left
|
|
|
|
* @param {!AudioNode} destination_right
|
2018-03-10 09:51:23 +01:00
|
|
|
*/
|
2018-03-10 23:13:03 +01:00
|
|
|
function SpeakerMixerSource(audio_context, source_node, destination_left, destination_right)
|
2018-03-10 09:51:23 +01:00
|
|
|
{
|
2018-03-10 23:13:03 +01:00
|
|
|
/** @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;
|
|
|
|
|
2018-03-10 09:51:23 +01:00
|
|
|
// Nodes
|
|
|
|
|
|
|
|
this.node_splitter = audio_context.createChannelSplitter(2);
|
|
|
|
this.node_gain_left = audio_context.createGain();
|
|
|
|
this.node_gain_right = audio_context.createGain();
|
|
|
|
|
|
|
|
// Graph
|
|
|
|
|
2018-06-23 02:55:38 +02:00
|
|
|
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);
|
2018-03-10 09:51:23 +01:00
|
|
|
}
|
2018-03-04 05:40:28 +01:00
|
|
|
|
2018-03-10 23:13:03 +01:00
|
|
|
SpeakerMixerSource.prototype.update = function()
|
|
|
|
{
|
2018-03-17 09:15:23 +01:00
|
|
|
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;
|
2018-03-10 23:13:03 +01:00
|
|
|
|
|
|
|
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);
|
|
|
|
};
|
|
|
|
|
2018-03-17 22:09:45 +01:00
|
|
|
/** @param {number=} channel */
|
2018-03-10 09:51:23 +01:00
|
|
|
SpeakerMixerSource.prototype.connect = function(channel)
|
|
|
|
{
|
2018-03-17 22:09:45 +01:00
|
|
|
var both = !channel || channel === MIXER_CHANNEL_BOTH;
|
|
|
|
if(both || channel === MIXER_CHANNEL_LEFT)
|
2018-03-10 09:51:23 +01:00
|
|
|
{
|
2018-03-10 23:13:03 +01:00
|
|
|
this.connected_left = true;
|
2018-03-10 09:51:23 +01:00
|
|
|
}
|
2018-03-17 22:09:45 +01:00
|
|
|
if(both || channel === MIXER_CHANNEL_RIGHT)
|
2018-03-10 09:51:23 +01:00
|
|
|
{
|
2018-03-10 23:13:03 +01:00
|
|
|
this.connected_right = true;
|
2018-03-10 09:51:23 +01:00
|
|
|
}
|
2018-03-10 23:13:03 +01:00
|
|
|
this.update();
|
2018-03-10 09:51:23 +01:00
|
|
|
};
|
2018-03-04 05:40:28 +01:00
|
|
|
|
2018-03-17 22:09:45 +01:00
|
|
|
/** @param {number=} channel */
|
2018-03-10 09:51:23 +01:00
|
|
|
SpeakerMixerSource.prototype.disconnect = function(channel)
|
|
|
|
{
|
2018-03-17 22:09:45 +01:00
|
|
|
var both = !channel || channel === MIXER_CHANNEL_BOTH;
|
|
|
|
if(both || channel === MIXER_CHANNEL_LEFT)
|
2018-03-04 05:40:28 +01:00
|
|
|
{
|
2018-03-10 23:13:03 +01:00
|
|
|
this.connected_left = false;
|
2018-03-10 09:51:23 +01:00
|
|
|
}
|
2018-03-17 22:09:45 +01:00
|
|
|
if(both || channel === MIXER_CHANNEL_RIGHT)
|
2018-02-18 08:07:19 +01:00
|
|
|
{
|
2018-03-10 23:13:03 +01:00
|
|
|
this.connected_right = false;
|
2018-03-10 09:51:23 +01:00
|
|
|
}
|
2018-03-10 23:13:03 +01:00
|
|
|
this.update();
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {number} value
|
2018-03-17 22:09:45 +01:00
|
|
|
* @param {number=} channel
|
2018-03-10 23:13:03 +01:00
|
|
|
*/
|
|
|
|
SpeakerMixerSource.prototype.set_volume = function(value, channel)
|
|
|
|
{
|
2018-03-20 02:28:13 +01:00
|
|
|
if(channel === undefined)
|
2018-03-10 23:13:03 +01:00
|
|
|
{
|
2018-03-17 22:09:45 +01:00
|
|
|
channel = MIXER_CHANNEL_BOTH;
|
2018-03-10 23:13:03 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
switch(channel)
|
|
|
|
{
|
2018-03-17 22:09:45 +01:00
|
|
|
case MIXER_CHANNEL_LEFT:
|
2018-03-10 23:13:03 +01:00
|
|
|
this.volume_left = value;
|
|
|
|
break;
|
2018-03-17 22:09:45 +01:00
|
|
|
case MIXER_CHANNEL_RIGHT:
|
2018-03-10 23:13:03 +01:00
|
|
|
this.volume_right = value;
|
|
|
|
break;
|
2018-03-17 22:09:45 +01:00
|
|
|
case MIXER_CHANNEL_BOTH:
|
2018-03-10 23:13:03 +01:00
|
|
|
this.volume_both = value;
|
|
|
|
break;
|
|
|
|
default:
|
2018-03-17 08:30:29 +01:00
|
|
|
dbg_assert(false, "Mixer set volume - unknown channel: " + channel);
|
2018-03-10 23:13:03 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.update();
|
|
|
|
};
|
|
|
|
|
|
|
|
SpeakerMixerSource.prototype.set_gain_hidden = function(value)
|
|
|
|
{
|
|
|
|
this.gain_hidden = value;
|
2018-03-10 09:51:23 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @constructor
|
|
|
|
* @param {!BusConnector} bus
|
|
|
|
* @param {!AudioContext} audio_context
|
2018-03-12 01:18:05 +01:00
|
|
|
* @param {!SpeakerMixer} mixer
|
2018-03-10 09:51:23 +01:00
|
|
|
*/
|
|
|
|
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
|
|
|
|
|
2018-03-17 22:09:45 +01:00
|
|
|
this.mixer_connection = mixer.add_source(this.node_oscillator, MIXER_SRC_PCSPEAKER);
|
2018-03-10 23:13:03 +01:00
|
|
|
this.mixer_connection.disconnect();
|
2018-02-18 08:07:19 +01:00
|
|
|
|
2018-03-04 05:40:28 +01:00
|
|
|
bus.register("pcspeaker-enable", function()
|
2017-11-15 03:10:23 +01:00
|
|
|
{
|
2018-03-17 22:09:45 +01:00
|
|
|
mixer.connect_source(MIXER_SRC_PCSPEAKER);
|
2018-03-04 05:40:28 +01:00
|
|
|
}, this);
|
2018-03-10 09:51:23 +01:00
|
|
|
|
2018-03-04 05:40:28 +01:00
|
|
|
bus.register("pcspeaker-disable", function()
|
|
|
|
{
|
2018-03-17 22:09:45 +01:00
|
|
|
mixer.disconnect_source(MIXER_SRC_PCSPEAKER);
|
2017-11-15 03:10:23 +01:00
|
|
|
}, this);
|
2018-03-10 09:51:23 +01:00
|
|
|
|
2017-12-11 22:34:38 +01:00
|
|
|
bus.register("pcspeaker-update", function(data)
|
2017-11-15 03:10:23 +01:00
|
|
|
{
|
2017-12-11 22:34:38 +01:00
|
|
|
var counter_mode = data[0];
|
|
|
|
var counter_reload = data[1];
|
2018-03-04 05:40:28 +01:00
|
|
|
|
|
|
|
var frequency = 0;
|
|
|
|
var beep_enabled = counter_mode === 3;
|
|
|
|
|
|
|
|
if(beep_enabled)
|
|
|
|
{
|
|
|
|
frequency = OSCILLATOR_FREQ * 1000 / counter_reload;
|
2018-03-10 09:51:23 +01:00
|
|
|
frequency = Math.min(frequency, this.node_oscillator.frequency.maxValue);
|
2018-03-04 05:40:28 +01:00
|
|
|
frequency = Math.max(frequency, 0);
|
|
|
|
}
|
|
|
|
|
2018-03-10 09:51:23 +01:00
|
|
|
this.node_oscillator.frequency.setValueAtTime(frequency, audio_context.currentTime);
|
2017-11-15 03:10:23 +01:00
|
|
|
}, this);
|
2018-03-10 09:51:23 +01:00
|
|
|
}
|
|
|
|
|
2018-03-18 01:07:28 +01:00
|
|
|
PCSpeaker.prototype.start = function()
|
|
|
|
{
|
|
|
|
this.node_oscillator.start();
|
|
|
|
};
|
|
|
|
|
2018-03-10 09:51:23 +01:00
|
|
|
/**
|
|
|
|
* @constructor
|
|
|
|
* @param {!BusConnector} bus
|
|
|
|
* @param {!AudioContext} audio_context
|
2018-03-12 01:18:05 +01:00
|
|
|
* @param {!SpeakerMixer} mixer
|
|
|
|
*/
|
|
|
|
function SpeakerWorkletDAC(bus, audio_context, mixer)
|
|
|
|
{
|
2018-03-13 02:32:06 +01:00
|
|
|
/** @const */
|
2018-03-12 01:18:05 +01:00
|
|
|
this.bus = bus;
|
2018-03-13 02:32:06 +01:00
|
|
|
|
|
|
|
/** @const */
|
2018-03-12 01:18:05 +01:00
|
|
|
this.audio_context = audio_context;
|
|
|
|
|
|
|
|
// State
|
|
|
|
|
|
|
|
this.enabled = false;
|
|
|
|
this.sampling_rate = 48000;
|
|
|
|
|
|
|
|
// Worklet
|
|
|
|
|
|
|
|
function worklet()
|
|
|
|
{
|
|
|
|
/** @const */
|
|
|
|
var RENDER_QUANTUM = 128;
|
|
|
|
|
2018-03-12 10:40:50 +01:00
|
|
|
/** @const */
|
|
|
|
var MINIMUM_BUFFER_SIZE = 2 * RENDER_QUANTUM;
|
|
|
|
|
2018-03-12 01:18:05 +01:00
|
|
|
/** @const */
|
|
|
|
var QUEUE_RESERVE = 1024;
|
|
|
|
|
|
|
|
function sinc(x)
|
|
|
|
{
|
|
|
|
if(x === 0) return 1;
|
|
|
|
x *= Math.PI;
|
|
|
|
return Math.sin(x) / x;
|
|
|
|
}
|
|
|
|
|
2018-03-17 10:29:39 +01:00
|
|
|
var EMPTY_BUFFER =
|
|
|
|
[
|
|
|
|
new Float32Array(MINIMUM_BUFFER_SIZE),
|
|
|
|
new Float32Array(MINIMUM_BUFFER_SIZE),
|
|
|
|
];
|
2018-03-12 01:18:05 +01:00
|
|
|
|
2018-03-18 03:31:59 +01:00
|
|
|
/**
|
|
|
|
* @constructor
|
|
|
|
* @extends AudioWorkletProcessor
|
|
|
|
*/
|
|
|
|
function DACProcessor()
|
2018-03-12 01:18:05 +01:00
|
|
|
{
|
2018-03-18 03:31:59 +01:00
|
|
|
var self = Reflect.construct(AudioWorkletProcessor, [], DACProcessor);
|
2018-03-12 01:18:05 +01:00
|
|
|
|
2018-03-18 03:31:59 +01:00
|
|
|
// Params
|
2018-03-12 01:18:05 +01:00
|
|
|
|
2018-03-18 03:31:59 +01:00
|
|
|
self.kernel_size = 3;
|
2018-03-12 01:18:05 +01:00
|
|
|
|
2018-03-18 03:31:59 +01:00
|
|
|
// States
|
2018-03-12 01:18:05 +01:00
|
|
|
|
2018-03-18 03:31:59 +01:00
|
|
|
// 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;
|
2018-03-12 01:18:05 +01:00
|
|
|
|
2018-03-18 03:31:59 +01:00
|
|
|
// Buffers being actively consumed
|
|
|
|
/** @type{Array<Float32Array>} */
|
|
|
|
self.source_buffer_previous = EMPTY_BUFFER;
|
|
|
|
/** @type{Array<Float32Array>} */
|
|
|
|
self.source_buffer_current = EMPTY_BUFFER;
|
2018-03-12 01:18:05 +01:00
|
|
|
|
2018-03-18 03:31:59 +01:00
|
|
|
// Ratio of alienland sample rate to homeland sample rate.
|
|
|
|
self.source_samples_per_destination = 1.0;
|
2018-03-12 01:18:05 +01:00
|
|
|
|
2018-03-18 03:31:59 +01:00
|
|
|
// Integer representing the position of the first destination sample
|
|
|
|
// for the current block, relative to source_buffer_current.
|
|
|
|
self.source_block_start = 0;
|
2018-03-12 01:18:05 +01:00
|
|
|
|
2018-03-18 03:31:59 +01:00
|
|
|
// Real number representing the position of the current destination
|
2018-03-19 23:05:55 +01:00
|
|
|
// sample relative to source_buffer_current, since source_block_start.
|
2018-03-18 03:31:59 +01:00
|
|
|
self.source_time = 0.0;
|
2018-03-12 01:18:05 +01:00
|
|
|
|
2018-03-18 03:31:59 +01:00
|
|
|
// Same as source_time but rounded down to an index.
|
|
|
|
self.source_offset = 0;
|
2018-03-12 01:18:05 +01:00
|
|
|
|
2018-03-18 03:31:59 +01:00
|
|
|
// Interface
|
2018-03-12 01:18:05 +01:00
|
|
|
|
2018-03-18 03:31:59 +01:00
|
|
|
self.port.onmessage = (event) =>
|
|
|
|
{
|
|
|
|
switch(event.data.type)
|
2018-03-12 01:18:05 +01:00
|
|
|
{
|
2018-03-18 03:31:59 +01:00
|
|
|
case "queue":
|
|
|
|
self.queue_push(event.data.value);
|
|
|
|
break;
|
|
|
|
case "sampling-rate":
|
|
|
|
self.source_samples_per_destination = event.data.value / sampleRate;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
};
|
2018-03-12 01:18:05 +01:00
|
|
|
|
2018-03-18 03:31:59 +01:00
|
|
|
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++)
|
2018-03-12 01:18:05 +01:00
|
|
|
{
|
2018-03-18 03:31:59 +01:00
|
|
|
// 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++)
|
2018-03-12 01:18:05 +01:00
|
|
|
{
|
2018-03-18 03:31:59 +01:00
|
|
|
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);
|
2018-03-12 01:18:05 +01:00
|
|
|
}
|
|
|
|
|
2018-03-18 03:31:59 +01:00
|
|
|
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.");
|
|
|
|
}
|
2018-03-12 01:18:05 +01:00
|
|
|
|
2018-03-18 03:31:59 +01:00
|
|
|
outputs[0][0][i] = sum0;
|
|
|
|
outputs[0][1][i] = sum1;
|
2018-03-12 10:40:50 +01:00
|
|
|
|
2018-03-18 03:31:59 +01:00
|
|
|
this.source_time += this.source_samples_per_destination;
|
|
|
|
this.source_offset = Math.floor(this.source_time);
|
2018-03-12 01:18:05 +01:00
|
|
|
}
|
|
|
|
|
2018-03-18 03:31:59 +01:00
|
|
|
// +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)
|
2018-03-12 01:18:05 +01:00
|
|
|
{
|
2018-03-19 23:05:55 +01:00
|
|
|
// -ve index represents previous buffer
|
|
|
|
// <-------|
|
|
|
|
// [Previous buffer][Current buffer]
|
|
|
|
index += this.source_buffer_previous[0].length;
|
2018-03-18 03:31:59 +01:00
|
|
|
return this.source_buffer_previous[channel][index];
|
2018-03-12 01:18:05 +01:00
|
|
|
}
|
2018-03-18 03:31:59 +01:00
|
|
|
else
|
2018-03-12 01:18:05 +01:00
|
|
|
{
|
2018-03-18 03:31:59 +01:00
|
|
|
return this.source_buffer_current[channel][index];
|
2018-03-12 01:18:05 +01:00
|
|
|
}
|
2018-03-18 03:31:59 +01:00
|
|
|
};
|
2018-03-12 01:18:05 +01:00
|
|
|
|
2018-03-18 03:31:59 +01:00
|
|
|
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)
|
2018-03-12 01:18:05 +01:00
|
|
|
{
|
2018-03-18 03:31:59 +01:00
|
|
|
this.prepare_next_buffer();
|
|
|
|
this.source_block_start -= current_length;
|
|
|
|
}
|
|
|
|
};
|
2018-03-12 01:18:05 +01:00
|
|
|
|
2018-03-18 03:31:59 +01:00
|
|
|
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");
|
2018-03-12 01:18:05 +01:00
|
|
|
}
|
|
|
|
|
2018-03-18 03:31:59 +01:00
|
|
|
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)
|
2018-03-12 01:18:05 +01:00
|
|
|
{
|
2018-03-18 03:31:59 +01:00
|
|
|
// Unfortunately, this single buffer is too small :(
|
2018-03-12 01:35:34 +01:00
|
|
|
|
2018-03-18 03:31:59 +01:00
|
|
|
var queue_pos = this.queue_start;
|
|
|
|
var buffer_count = 0;
|
2018-03-12 01:18:05 +01:00
|
|
|
|
2018-03-18 03:31:59 +01:00
|
|
|
// 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++;
|
|
|
|
}
|
2018-03-12 01:18:05 +01:00
|
|
|
|
2018-03-18 03:31:59 +01:00
|
|
|
// 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++)
|
2018-03-12 01:18:05 +01:00
|
|
|
{
|
2018-03-18 03:31:59 +01:00
|
|
|
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;
|
2018-03-12 01:18:05 +01:00
|
|
|
}
|
|
|
|
|
2018-03-18 03:31:59 +01:00
|
|
|
// Pretend that everything's just fine.
|
|
|
|
this.source_buffer_current = new_big_buffer;
|
2018-03-12 01:18:05 +01:00
|
|
|
}
|
|
|
|
|
2018-03-18 03:31:59 +01:00
|
|
|
this.pump();
|
|
|
|
};
|
|
|
|
|
|
|
|
DACProcessor.prototype.pump = function()
|
|
|
|
{
|
|
|
|
if(this.queued_samples / this.source_samples_per_destination < QUEUE_RESERVE)
|
2018-03-12 01:18:05 +01:00
|
|
|
{
|
2018-03-18 03:31:59 +01:00
|
|
|
this.port.postMessage(
|
2018-03-12 01:18:05 +01:00
|
|
|
{
|
2018-03-18 03:31:59 +01:00
|
|
|
type: "pump",
|
|
|
|
});
|
2018-03-12 01:18:05 +01:00
|
|
|
}
|
2018-03-18 03:31:59 +01:00
|
|
|
};
|
2018-03-12 01:18:05 +01:00
|
|
|
|
2018-03-18 03:31:59 +01:00
|
|
|
DACProcessor.prototype.queue_push = function(item)
|
|
|
|
{
|
|
|
|
if(this.queue_length < this.queue_size)
|
2018-03-12 01:18:05 +01:00
|
|
|
{
|
2018-03-18 03:31:59 +01:00
|
|
|
this.queue_data[this.queue_end] = item;
|
|
|
|
this.queue_end = this.queue_end + 1 & this.queue_size - 1;
|
|
|
|
this.queue_length++;
|
2018-03-12 01:18:05 +01:00
|
|
|
|
2018-03-18 03:31:59 +01:00
|
|
|
this.queued_samples += item[0].length;
|
2018-03-12 01:18:05 +01:00
|
|
|
|
2018-03-18 03:31:59 +01:00
|
|
|
this.pump();
|
2018-03-12 01:18:05 +01:00
|
|
|
}
|
2018-03-18 03:31:59 +01:00
|
|
|
};
|
2018-03-12 01:18:05 +01:00
|
|
|
|
2018-03-18 03:31:59 +01:00
|
|
|
DACProcessor.prototype.queue_shift = function()
|
|
|
|
{
|
|
|
|
if(!this.queue_length)
|
2018-03-12 01:18:05 +01:00
|
|
|
{
|
2018-03-18 03:31:59 +01:00
|
|
|
return EMPTY_BUFFER;
|
|
|
|
}
|
2018-03-12 01:18:05 +01:00
|
|
|
|
2018-03-18 03:31:59 +01:00
|
|
|
var item = this.queue_data[this.queue_start];
|
2018-03-12 01:18:05 +01:00
|
|
|
|
2018-03-18 03:31:59 +01:00
|
|
|
this.queue_data[this.queue_start] = null;
|
|
|
|
this.queue_start = this.queue_start + 1 & this.queue_size - 1;
|
|
|
|
this.queue_length--;
|
2018-03-12 01:18:05 +01:00
|
|
|
|
2018-03-18 03:31:59 +01:00
|
|
|
this.queued_samples -= item[0].length;
|
2018-03-12 01:18:05 +01:00
|
|
|
|
2018-03-18 03:31:59 +01:00
|
|
|
return item;
|
|
|
|
};
|
2018-03-12 01:35:34 +01:00
|
|
|
|
2018-03-18 03:31:59 +01:00
|
|
|
DACProcessor.prototype.dbg_log = function(message)
|
|
|
|
{
|
|
|
|
if(DEBUG)
|
2018-03-12 01:35:34 +01:00
|
|
|
{
|
|
|
|
this.port.postMessage(
|
|
|
|
{
|
|
|
|
type: "debug-log",
|
2018-03-17 09:15:23 +01:00
|
|
|
value: message,
|
2018-03-12 01:35:34 +01:00
|
|
|
});
|
|
|
|
}
|
2018-04-07 22:46:13 +02:00
|
|
|
};
|
2018-03-12 01:18:05 +01:00
|
|
|
|
|
|
|
registerProcessor("dac-processor", DACProcessor);
|
2018-03-18 03:31:59 +01:00
|
|
|
}
|
2018-03-12 01:18:05 +01:00
|
|
|
|
2018-03-18 03:31:59 +01:00
|
|
|
var worklet_string = worklet.toString();
|
2018-03-12 01:18:05 +01:00
|
|
|
|
|
|
|
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);
|
|
|
|
|
2018-06-04 10:07:50 +02:00
|
|
|
if(DEBUG)
|
|
|
|
{
|
|
|
|
worklet_code = "var DEBUG = true;\n" + worklet_code;
|
|
|
|
}
|
|
|
|
|
2018-03-12 01:18:05 +01:00
|
|
|
var worklet_blob = new Blob([worklet_code], { type: "application/javascript" });
|
|
|
|
var worklet_url = URL.createObjectURL(worklet_blob);
|
|
|
|
|
|
|
|
/** @type {AudioWorkletNode} */
|
|
|
|
this.node_processor = null;
|
|
|
|
|
2018-03-12 02:46:14 +01:00
|
|
|
// Placeholder pass-through node to connect to, when worklet node is not ready yet.
|
|
|
|
this.node_output = this.audio_context.createGain();
|
|
|
|
|
2018-03-12 01:18:05 +01:00
|
|
|
this.audio_context
|
|
|
|
.audioWorklet
|
|
|
|
.addModule(worklet_url)
|
|
|
|
.then(() =>
|
|
|
|
{
|
2018-03-17 09:49:54 +01:00
|
|
|
URL.revokeObjectURL(worklet_url);
|
|
|
|
|
2018-03-12 01:18:05 +01:00
|
|
|
this.node_processor = new AudioWorkletNode(this.audio_context, "dac-processor",
|
|
|
|
{
|
2021-01-03 07:51:29 +01:00
|
|
|
numberOfInputs: 0,
|
|
|
|
numberOfOutputs: 1,
|
|
|
|
outputChannelCount: [2],
|
|
|
|
parameterData: {},
|
|
|
|
processorOptions: {},
|
2018-03-12 01:18:05 +01:00
|
|
|
});
|
2018-03-12 02:46:14 +01:00
|
|
|
|
2018-03-12 01:18:05 +01:00
|
|
|
this.node_processor.port.postMessage(
|
|
|
|
{
|
|
|
|
type: "sampling-rate",
|
2018-03-17 09:15:23 +01:00
|
|
|
value: this.sampling_rate,
|
2018-03-12 01:18:05 +01:00
|
|
|
});
|
2018-03-12 02:46:14 +01:00
|
|
|
|
2018-03-12 01:18:05 +01:00
|
|
|
this.node_processor.port.onmessage = (event) =>
|
|
|
|
{
|
2018-03-12 01:35:34 +01:00
|
|
|
switch(event.data.type)
|
2018-03-12 01:18:05 +01:00
|
|
|
{
|
|
|
|
case "pump":
|
|
|
|
this.pump();
|
|
|
|
break;
|
2018-03-12 01:35:34 +01:00
|
|
|
case "debug-log":
|
2018-03-17 08:30:29 +01:00
|
|
|
dbg_log("SpeakerWorkletDAC - Worklet: " + event.data.value);
|
2018-03-12 01:35:34 +01:00
|
|
|
break;
|
2018-03-12 01:18:05 +01:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2018-03-12 02:46:14 +01:00
|
|
|
// Graph
|
|
|
|
|
2018-06-23 02:55:38 +02:00
|
|
|
this.node_processor.connect(this.node_output);
|
2018-03-12 01:18:05 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
// Interface
|
|
|
|
|
2018-03-17 22:09:45 +01:00
|
|
|
this.mixer_connection = mixer.add_source(this.node_output, MIXER_SRC_DAC);
|
2018-03-12 04:32:58 +01:00
|
|
|
this.mixer_connection.set_gain_hidden(3);
|
|
|
|
|
2018-03-12 01:18:05 +01:00
|
|
|
bus.register("dac-send-data", function(data)
|
|
|
|
{
|
2018-03-12 02:46:14 +01:00
|
|
|
this.queue(data);
|
2018-03-12 01:18:05 +01:00
|
|
|
}, this);
|
|
|
|
|
|
|
|
bus.register("dac-enable", function(enabled)
|
|
|
|
{
|
|
|
|
this.enabled = true;
|
|
|
|
}, this);
|
|
|
|
|
|
|
|
bus.register("dac-disable", function()
|
|
|
|
{
|
|
|
|
this.enabled = false;
|
|
|
|
}, this);
|
|
|
|
|
2018-03-17 10:23:50 +01:00
|
|
|
bus.register("dac-tell-sampling-rate", function(/** number */ rate)
|
2018-03-12 01:18:05 +01:00
|
|
|
{
|
2018-03-13 11:26:06 +01:00
|
|
|
dbg_assert(rate > 0, "Sampling rate should be nonzero");
|
2018-03-12 01:18:05 +01:00
|
|
|
this.sampling_rate = rate;
|
|
|
|
|
|
|
|
if(!this.node_processor)
|
|
|
|
{
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.node_processor.port.postMessage(
|
|
|
|
{
|
|
|
|
type: "sampling-rate",
|
2018-03-17 09:15:23 +01:00
|
|
|
value: rate,
|
2018-03-12 01:18:05 +01:00
|
|
|
});
|
|
|
|
}, this);
|
2018-03-12 02:46:14 +01:00
|
|
|
|
|
|
|
if(DEBUG)
|
|
|
|
{
|
|
|
|
this.debugger = new SpeakerDACDebugger(this.audio_context, this.node_output);
|
|
|
|
}
|
2018-03-12 01:18:05 +01:00
|
|
|
}
|
|
|
|
|
2018-03-12 02:46:14 +01:00
|
|
|
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",
|
2018-03-17 09:15:23 +01:00
|
|
|
value: data,
|
2018-03-12 02:46:14 +01:00
|
|
|
}, [data[0].buffer, data[1].buffer]);
|
|
|
|
};
|
|
|
|
|
2018-03-12 01:18:05 +01:00
|
|
|
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
|
2018-03-10 09:51:23 +01:00
|
|
|
*/
|
2018-03-12 01:18:05 +01:00
|
|
|
function SpeakerBufferSourceDAC(bus, audio_context, mixer)
|
2018-03-10 09:51:23 +01:00
|
|
|
{
|
|
|
|
/** @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
|
|
|
|
|
2018-03-12 02:46:14 +01:00
|
|
|
this.node_output = this.node_lowpass;
|
|
|
|
|
2018-03-17 22:09:45 +01:00
|
|
|
this.mixer_connection = mixer.add_source(this.node_output, MIXER_SRC_DAC);
|
2018-03-10 23:13:03 +01:00
|
|
|
this.mixer_connection.set_gain_hidden(3);
|
2018-03-10 09:51:23 +01:00
|
|
|
|
2018-03-07 19:44:23 +01:00
|
|
|
bus.register("dac-send-data", function(data)
|
2017-12-14 08:23:05 +01:00
|
|
|
{
|
2018-03-10 09:51:23 +01:00
|
|
|
this.queue(data);
|
2017-12-14 08:23:05 +01:00
|
|
|
}, this);
|
2018-03-11 05:25:55 +01:00
|
|
|
|
2018-03-04 05:40:28 +01:00
|
|
|
bus.register("dac-enable", function(enabled)
|
2017-12-16 10:04:11 +01:00
|
|
|
{
|
2018-03-10 09:51:23 +01:00
|
|
|
this.enabled = true;
|
|
|
|
this.pump();
|
2018-03-04 05:40:28 +01:00
|
|
|
}, this);
|
2018-03-11 05:25:55 +01:00
|
|
|
|
2018-03-04 05:40:28 +01:00
|
|
|
bus.register("dac-disable", function()
|
|
|
|
{
|
2018-03-10 09:51:23 +01:00
|
|
|
this.enabled = false;
|
2017-12-16 10:04:11 +01:00
|
|
|
}, this);
|
2018-03-11 05:25:55 +01:00
|
|
|
|
2018-03-17 10:23:50 +01:00
|
|
|
bus.register("dac-tell-sampling-rate", function(/** number */ rate)
|
2017-11-21 01:05:17 +01:00
|
|
|
{
|
2018-03-13 11:26:06 +01:00
|
|
|
dbg_assert(rate > 0, "Sampling rate should be nonzero");
|
2018-03-10 22:37:52 +01:00
|
|
|
this.sampling_rate = rate;
|
2018-03-13 02:32:06 +01:00
|
|
|
this.rate_ratio = Math.ceil(AUDIOBUFFER_MINIMUM_SAMPLING_RATE / rate);
|
2018-03-10 22:37:52 +01:00
|
|
|
this.node_lowpass.frequency.setValueAtTime(rate / 2, this.audio_context.currentTime);
|
2018-03-07 19:44:23 +01:00
|
|
|
}, this);
|
2017-11-15 03:10:23 +01:00
|
|
|
|
2018-03-10 01:33:10 +01:00
|
|
|
if(DEBUG)
|
|
|
|
{
|
2018-03-12 02:46:14 +01:00
|
|
|
this.debugger = new SpeakerDACDebugger(this.audio_context, this.node_output);
|
2018-03-10 01:33:10 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-03-12 01:18:05 +01:00
|
|
|
SpeakerBufferSourceDAC.prototype.queue = function(data)
|
2017-11-15 03:13:43 +01:00
|
|
|
{
|
2018-03-10 01:33:10 +01:00
|
|
|
if(DEBUG)
|
|
|
|
{
|
2018-03-12 02:46:14 +01:00
|
|
|
this.debugger.push_queued_data(data);
|
2018-03-10 01:33:10 +01:00
|
|
|
}
|
|
|
|
|
2018-03-09 08:24:42 +01:00
|
|
|
var sample_count = data[0].length;
|
2018-03-10 09:51:23 +01:00
|
|
|
var block_duration = sample_count / this.sampling_rate;
|
2018-03-09 08:24:42 +01:00
|
|
|
|
2018-03-11 04:13:46 +01:00
|
|
|
var buffer;
|
2018-03-10 09:51:23 +01:00
|
|
|
if(this.rate_ratio > 1)
|
2018-03-09 23:48:10 +01:00
|
|
|
{
|
2018-03-11 04:13:46 +01:00
|
|
|
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);
|
2018-03-09 23:48:10 +01:00
|
|
|
var buffer_data0 = buffer.getChannelData(0);
|
|
|
|
var buffer_data1 = buffer.getChannelData(1);
|
2018-03-10 11:54:32 +01:00
|
|
|
|
|
|
|
var buffer_index = 0;
|
2018-03-09 23:48:10 +01:00
|
|
|
for(var i = 0; i < sample_count; i++)
|
|
|
|
{
|
2018-03-10 11:54:32 +01:00
|
|
|
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];
|
|
|
|
}
|
2018-03-09 23:48:10 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2018-03-11 04:13:46 +01:00
|
|
|
// 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);
|
2019-05-12 09:12:14 +02:00
|
|
|
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]);
|
|
|
|
}
|
2018-03-09 23:48:10 +01:00
|
|
|
}
|
2017-12-16 10:04:11 +01:00
|
|
|
|
2018-03-07 19:44:23 +01:00
|
|
|
var source = this.audio_context.createBufferSource();
|
|
|
|
source.buffer = buffer;
|
2018-03-10 09:51:23 +01:00
|
|
|
source.connect(this.node_lowpass);
|
2018-03-11 04:13:46 +01:00
|
|
|
source.addEventListener("ended", this.pump.bind(this));
|
2017-12-14 08:17:18 +01:00
|
|
|
|
2018-03-07 19:44:23 +01:00
|
|
|
var current_time = this.audio_context.currentTime;
|
2018-03-10 01:33:10 +01:00
|
|
|
|
2018-03-10 09:51:23 +01:00
|
|
|
if(this.buffered_time < current_time)
|
2017-11-21 01:05:17 +01:00
|
|
|
{
|
2018-03-17 08:30:29 +01:00
|
|
|
dbg_log("Speaker DAC - Creating/Recreating reserve - shouldn't occur frequently during playback");
|
2018-03-10 01:33:10 +01:00
|
|
|
|
2018-03-07 19:44:23 +01:00
|
|
|
// Schedule pump() to queue evenly, starting from current time
|
2018-03-10 09:51:23 +01:00
|
|
|
this.buffered_time = current_time;
|
2018-03-09 08:24:42 +01:00
|
|
|
var target_silence_duration = DAC_QUEUE_RESERVE - block_duration;
|
|
|
|
var current_silence_duration = 0;
|
|
|
|
while(current_silence_duration <= target_silence_duration)
|
2017-12-11 20:41:34 +01:00
|
|
|
{
|
2018-03-09 08:24:42 +01:00
|
|
|
current_silence_duration += block_duration;
|
2018-03-10 09:51:23 +01:00
|
|
|
this.buffered_time += block_duration;
|
|
|
|
setTimeout(() => this.pump(), current_silence_duration * 1000);
|
2017-12-11 20:41:34 +01:00
|
|
|
}
|
2017-11-21 01:05:17 +01:00
|
|
|
}
|
2018-03-07 19:44:23 +01:00
|
|
|
|
2018-03-10 09:51:23 +01:00
|
|
|
source.start(this.buffered_time);
|
|
|
|
this.buffered_time += block_duration;
|
2018-03-07 19:44:23 +01:00
|
|
|
|
2018-03-10 01:33:10 +01:00
|
|
|
// Chase the schedule - ensure reserve is full
|
2018-03-10 09:51:23 +01:00
|
|
|
setTimeout(() => this.pump(), 0);
|
2018-03-07 19:44:23 +01:00
|
|
|
};
|
|
|
|
|
2018-03-12 01:18:05 +01:00
|
|
|
SpeakerBufferSourceDAC.prototype.pump = function()
|
2018-03-07 19:44:23 +01:00
|
|
|
{
|
2018-03-10 09:51:23 +01:00
|
|
|
if(!this.enabled)
|
2018-03-07 19:44:23 +01:00
|
|
|
{
|
|
|
|
return;
|
|
|
|
}
|
2018-03-10 09:51:23 +01:00
|
|
|
if(this.buffered_time - this.audio_context.currentTime > DAC_QUEUE_RESERVE)
|
2018-03-07 19:44:23 +01:00
|
|
|
{
|
|
|
|
return;
|
|
|
|
}
|
2018-03-09 08:24:42 +01:00
|
|
|
this.bus.send("dac-request-data");
|
2017-12-11 21:31:39 +01:00
|
|
|
};
|
2018-03-10 09:51:23 +01:00
|
|
|
|
2018-03-12 02:46:14 +01:00
|
|
|
/**
|
|
|
|
* @constructor
|
|
|
|
*/
|
|
|
|
function SpeakerDACDebugger(audio_context, source_node)
|
2018-03-10 09:51:23 +01:00
|
|
|
{
|
2018-03-12 02:46:14 +01:00
|
|
|
/** @const */
|
|
|
|
this.audio_context = audio_context;
|
|
|
|
|
|
|
|
/** @const */
|
|
|
|
this.node_source = source_node;
|
2018-03-10 09:51:23 +01:00
|
|
|
|
2018-03-12 02:46:14 +01:00
|
|
|
this.node_processor = null;
|
2018-03-17 09:42:22 +01:00
|
|
|
|
2018-03-12 02:46:14 +01:00
|
|
|
this.node_gain = this.audio_context.createGain();
|
2018-03-17 09:42:22 +01:00
|
|
|
this.node_gain.gain.setValueAtTime(0, this.audio_context.currentTime);
|
2018-03-10 09:51:23 +01:00
|
|
|
|
2018-06-23 02:55:38 +02:00
|
|
|
this.node_gain.connect(this.audio_context.destination);
|
2018-03-10 09:51:23 +01:00
|
|
|
|
2018-03-12 02:46:14 +01:00
|
|
|
this.is_active = false;
|
|
|
|
this.queued_history = [];
|
|
|
|
this.output_history = [];
|
|
|
|
this.queued = [[], []];
|
|
|
|
this.output = [[], []];
|
|
|
|
}
|
2018-03-10 09:51:23 +01:00
|
|
|
|
2018-03-12 02:46:14 +01:00
|
|
|
/** @suppress {deprecated} */
|
2018-03-17 09:15:23 +01:00
|
|
|
SpeakerDACDebugger.prototype.start = function(duration_ms)
|
2018-03-12 02:46:14 +01:00
|
|
|
{
|
|
|
|
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) =>
|
2018-03-10 09:51:23 +01:00
|
|
|
{
|
2018-03-12 02:46:14 +01:00
|
|
|
this.output[0].push(event.inputBuffer.getChannelData(0).slice());
|
|
|
|
this.output[1].push(event.inputBuffer.getChannelData(1).slice());
|
2018-03-10 09:51:23 +01:00
|
|
|
};
|
|
|
|
|
2018-06-23 02:55:38 +02:00
|
|
|
this.node_source.connect(this.node_processor);
|
|
|
|
this.node_processor.connect(this.node_gain);
|
2018-03-12 02:46:14 +01:00
|
|
|
|
|
|
|
setTimeout(() =>
|
2018-03-10 09:51:23 +01:00
|
|
|
{
|
2018-03-12 02:46:14 +01:00
|
|
|
this.stop();
|
2018-03-17 09:15:23 +01:00
|
|
|
}, duration_ms);
|
2018-03-12 02:46:14 +01:00
|
|
|
};
|
2018-03-10 09:51:23 +01:00
|
|
|
|
2018-03-12 02:46:14 +01:00
|
|
|
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(" ");
|
|
|
|
|
2018-03-17 09:41:25 +01:00
|
|
|
dump_file(txt, "dacdata.txt");
|
2018-03-12 02:46:14 +01:00
|
|
|
};
|
2018-03-10 09:51:23 +01:00
|
|
|
|
2018-03-12 02:46:14 +01:00
|
|
|
// 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++)
|
2018-03-10 09:51:23 +01:00
|
|
|
{
|
2018-03-12 02:46:14 +01:00
|
|
|
for(var i = 0; i < buffers[0][buffer_id].length; i++)
|
2018-03-10 09:51:23 +01:00
|
|
|
{
|
2018-03-12 02:46:14 +01:00
|
|
|
csv_rows.push(`${buffers[0][buffer_id][i]},${buffers[1][buffer_id][i]}`);
|
2018-03-10 09:51:23 +01:00
|
|
|
}
|
2018-03-12 02:46:14 +01:00
|
|
|
}
|
2018-03-17 09:41:25 +01:00
|
|
|
dump_file(csv_rows.join("\n"), "dacdata.csv");
|
2018-03-12 02:46:14 +01:00
|
|
|
};
|