/* Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
/* This is a program for tuning audio using Web Audio API. The processing
* pipeline looks like this:
*
* INPUT
* |
* +------------+
* | crossover |
* +------------+
* / | \
* (low band) (mid band) (high band)
* / | \
* +------+ +------+ +------+
* | DRC | | DRC | | DRC |
* +------+ +------+ +------+
* \ | /
* \ | /
* +-------------+
* | (+) |
* +-------------+
* | |
* (left) (right)
* | |
* +----+ +----+
* | EQ | | EQ |
* +----+ +----+
* | |
* +----+ +----+
* | EQ | | EQ |
* +----+ +----+
* . .
* . .
* +----+ +----+
* | EQ | | EQ |
* +----+ +----+
* \ /
* \ /
* |
* / \
* / \
* +-----+ +-----+
* | FFT | | FFT | (for visualization only)
* +-----+ +-----+
* \ /
* \ /
* |
* OUTPUT
*
* The parameters of each DRC and EQ can be adjusted or disabled independently.
*
* If enable_swap is set to true, the order of the DRC and the EQ stages are
* swapped (EQ is applied first, then DRC).
*/
/* The GLOBAL state has following parameters:
* enable_drc - A switch to turn all DRC on/off.
* enable_eq - A switch to turn all EQ on/off.
* enable_fft - A switch to turn visualization on/off.
* enable_swap - A switch to swap the order of EQ and DRC stages.
*/
/* The DRC has following parameters:
* f - The lower frequency of the band, in Hz.
* enable - 1 to enable the compressor, 0 to disable it.
* threshold - The value above which the compression starts, in dB.
* knee - The value above which the knee region starts, in dB.
* ratio - The input/output dB ratio after the knee region.
* attack - The time to reduce the gain by 10dB, in seconds.
* release - The time to increase the gain by 10dB, in seconds.
* boost - The static boost value in output, in dB.
*/
/* The EQ has following parameters:
* enable - 1 to enable the eq, 0 to disable it.
* type - The type of the eq, the available values are 'lowpass', 'highpass',
* 'bandpass', 'lowshelf', 'highshelf', 'peaking', 'notch'.
* freq - The frequency of the eq, in Hz.
* q, gain - The meaning depends on the type of the filter. See Web Audio API
* for details.
*/
/* The initial values of parameters for GLOBAL, DRC and EQ */
var INIT_GLOBAL_ENABLE_DRC = true;
var INIT_GLOBAL_ENABLE_EQ = true;
var INIT_GLOBAL_ENABLE_FFT = true;
var INIT_GLOBAL_ENABLE_SWAP = false;
var INIT_DRC_XO_LOW = 200;
var INIT_DRC_XO_HIGH = 2000;
var INIT_DRC_ENABLE = true;
var INIT_DRC_THRESHOLD = -24;
var INIT_DRC_KNEE = 30;
var INIT_DRC_RATIO = 12;
var INIT_DRC_ATTACK = 0.003;
var INIT_DRC_RELEASE = 0.250;
var INIT_DRC_BOOST = 0;
var INIT_EQ_ENABLE = true;
var INIT_EQ_TYPE = 'peaking';
var INIT_EQ_FREQ = 350;
var INIT_EQ_Q = 1;
var INIT_EQ_GAIN = 0;
var NEQ = 8; /* The number of EQs per channel */
var FFT_SIZE = 2048; /* The size of FFT used for visualization */
var audioContext; /* Web Audio context */
var nyquist; /* Nyquist frequency, in Hz */
var sourceNode;
var audio_graph;
var audio_ui;
var analyzer_left; /* The FFT analyzer for left channel */
var analyzer_right; /* The FFT analyzer for right channel */
/* get_emphasis_disabled detects if pre-emphasis in drc is disabled by browser.
* The detection result will be stored in this value. When user saves config,
* This value is stored in drc.emphasis_disabled in the config. */
var browser_emphasis_disabled_detection_result;
/* check_biquad_filter_q detects if the browser implements the lowpass and
* highpass biquad filters with the original formula or the new formula from
* Audio EQ Cookbook. Chrome changed the filter implementation in R53, see:
* https://github.com/GoogleChrome/web-audio-samples/wiki/Detection-of-lowpass-BiquadFilter-implementation
* The detection result is saved in this value before the page is initialized.
* make_biquad_q() uses this value to compute Q to ensure consistent behavior
* on different browser versions.
*/
var browser_biquad_filter_uses_audio_cookbook_formula;
/* Check the lowpass implementation and return a promise. */
function check_biquad_filter_q() {
'use strict';
var context = new OfflineAudioContext(1, 128, 48000);
var osc = context.createOscillator();
var filter1 = context.createBiquadFilter();
var filter2 = context.createBiquadFilter();
var inverter = context.createGain();
osc.type = 'sawtooth';
osc.frequency.value = 8 * 440;
inverter.gain.value = -1;
/* each filter should get a different Q value */
filter1.Q.value = -1;
filter2.Q.value = -20;
osc.connect(filter1);
osc.connect(filter2);
filter1.connect(context.destination);
filter2.connect(inverter);
inverter.connect(context.destination);
osc.start();
return context.startRendering().then(function (buffer) {
return browser_biquad_filter_uses_audio_cookbook_formula =
Math.max(...buffer.getChannelData(0)) !== 0;
});
}
/* Return the Q value to be used with the lowpass and highpass biquad filters,
* given Q in dB for the original filter formula. If the browser uses the new
* formula, conversion is made to simulate the original frequency response
* with the new formula.
*/
function make_biquad_q(q_db) {
if (!browser_biquad_filter_uses_audio_cookbook_formula)
return q_db;
var q_lin = dBToLinear(q_db);
var q_new = 1 / Math.sqrt((4 - Math.sqrt(16 - 16 / (q_lin * q_lin))) / 2);
q_new = linearToDb(q_new);
return q_new;
}
/* The supported audio element names are different on browsers with different
* versions.*/
function fix_audio_elements() {
try {
window.AudioContext = window.AudioContext || window.webkitAudioContext;
window.OfflineAudioContext = (window.OfflineAudioContext ||
window.webkitOfflineAudioContext);
}
catch(e) {
alert('Web Audio API is not supported in this browser');
}
}
function init_audio() {
audioContext = new AudioContext();
nyquist = audioContext.sampleRate / 2;
}
function build_graph() {
if (sourceNode) {
audio_graph = new graph();
sourceNode.disconnect();
if (get_global('enable_drc') || get_global('enable_eq') ||
get_global('enable_fft')) {
connect_from_native(pin(sourceNode), audio_graph);
connect_to_native(audio_graph, pin(audioContext.destination));
} else {
/* no processing needed, directly connect from source to destination. */
sourceNode.connect(audioContext.destination);
}
}
apply_all_configs();
}
/* The available configuration variables are:
*
* global.{enable_drc, enable_eq, enable_fft, enable_swap}
* drc.[0-2].{f, enable, threshold, knee, ratio, attack, release, boost}
* eq.[01].[0-7].{enable, type, freq, q, gain}.
*
* Each configuration variable maps a name to a value. For example,
* "drc.1.attack" is the attack time for the second drc (the "1" is the index of
* the drc instance), and "eq.0.2.freq" is the frequency of the third eq on the
* left channel (the "0" means left channel, and the "2" is the index of the
* eq).
*/
var all_configs = {}; /* stores all the configuration variables */
function init_config() {
set_config('global', 'enable_drc', INIT_GLOBAL_ENABLE_DRC);
set_config('global', 'enable_eq', INIT_GLOBAL_ENABLE_EQ);
set_config('global', 'enable_fft', INIT_GLOBAL_ENABLE_FFT);
set_config('global', 'enable_swap', INIT_GLOBAL_ENABLE_SWAP);
set_config('drc', 0, 'f', 0);
set_config('drc', 1, 'f', INIT_DRC_XO_LOW);
set_config('drc', 2, 'f', INIT_DRC_XO_HIGH);
for (var i = 0; i < 3; i++) {
set_config('drc', i, 'enable', INIT_DRC_ENABLE);
set_config('drc', i, 'threshold', INIT_DRC_THRESHOLD);
set_config('drc', i, 'knee', INIT_DRC_KNEE);
set_config('drc', i, 'ratio', INIT_DRC_RATIO);
set_config('drc', i, 'attack', INIT_DRC_ATTACK);
set_config('drc', i, 'release', INIT_DRC_RELEASE);
set_config('drc', i, 'boost', INIT_DRC_BOOST);
}
for (var i = 0; i <= 1; i++) {
for (var j = 0; j < NEQ; j++) {
set_config('eq', i, j, 'enable', INIT_EQ_ENABLE);
set_config('eq', i, j, 'type', INIT_EQ_TYPE);
set_config('eq', i, j, 'freq', INIT_EQ_FREQ);
set_config('eq', i, j, 'q', INIT_EQ_Q);
set_config('eq', i, j, 'gain', INIT_EQ_GAIN);
}
}
}
/* Returns a string from the first n elements of a, joined by '.' */
function make_name(a, n) {
var sub = [];
for (var i = 0; i < n; i++) {
sub.push(a[i].toString());
}
return sub.join('.');
}
function get_config() {
var name = make_name(arguments, arguments.length);
return all_configs[name];
}
function set_config() {
var n = arguments.length;
var name = make_name(arguments, n - 1);
all_configs[name] = arguments[n - 1];
}
/* Convenience function */
function get_global(name) {
return get_config('global', name);
}
/* set_config and apply it to the audio graph and ui. */
function use_config() {
var n = arguments.length;
var name = make_name(arguments, n - 1);
all_configs[name] = arguments[n - 1];
if (audio_graph) {
audio_graph.config(name.split('.'), all_configs[name]);
}
if (audio_ui) {
audio_ui.config(name.split('.'), all_configs[name]);
}
}
/* re-apply all the configs to audio graph and ui. */
function apply_all_configs() {
for (var name in all_configs) {
if (audio_graph) {
audio_graph.config(name.split('.'), all_configs[name]);
}
if (audio_ui) {
audio_ui.config(name.split('.'), all_configs[name]);
}
}
}
/* Returns a zero-padded two digits number, for time formatting. */
function two(n) {
var s = '00' + n;
return s.slice(-2);
}
/* Returns a time string, used for save file name */
function time_str() {
var d = new Date();
var date = two(d.getDate());
var month = two(d.getMonth() + 1);
var hour = two(d.getHours());
var minutes = two(d.getMinutes());
return month + date + '-' + hour + minutes;
}
/* Downloads the current config to a file. */
function save_config() {
set_config('drc', 'emphasis_disabled',
browser_emphasis_disabled_detection_result);
var a = document.getElementById('save_config_anchor');
var content = JSON.stringify(all_configs, undefined, 2);
var uriContent = 'data:application/octet-stream,' +
encodeURIComponent(content);
a.href = uriContent;
a.download = 'audio-' + time_str() + '.conf';
a.click();
}
/* Loads a config file. */
function load_config() {
document.getElementById('config_file').click();
}
function config_file_changed() {
var input = document.getElementById('config_file');
var file = input.files[0];
var reader = new FileReader();
function onloadend() {
var configs = JSON.parse(reader.result);
init_config();
for (var name in configs) {
all_configs[name] = configs[name];
}
build_graph();
}
reader.onloadend = onloadend;
reader.readAsText(file);
input.value = '';
}
/* ============================ Audio components ============================ */
/* We wrap Web Audio nodes into our own components. Each component has following
* methods:
*
* function input(n) - Returns a list of pins which are the n-th input of the
* component.
*
* function output(n) - Returns a list of pins which are the n-th output of the
* component.
*
* function config(name, value) - Changes the configuration variable for the
* component.
*
* Each "pin" is just one input/output of a Web Audio node.
*/
/* Returns the top-level audio component */
function graph() {
var stages = [];
var drcs, eqs, ffts;
if (get_global('enable_drc')) {
drcs = new drc_3band();
}
if (get_global('enable_eq')) {
eqs = new eq_2chan();
}
if (get_global('enable_swap')) {
if (eqs) stages.push(eqs);
if (drcs) stages.push(drcs);
} else {
if (drcs) stages.push(drcs);
if (eqs) stages.push(eqs);
}
if (get_global('enable_fft')) {
ffts = new fft_2chan();
stages.push(ffts);
}
for (var i = 1; i < stages.length; i++) {
connect(stages[i - 1], stages[i]);
}
function input(n) {
return stages[0].input(0);
}
function output(n) {
return stages[stages.length - 1].output(0);
}
function config(name, value) {
var p = name[0];
var s = name.slice(1);
if (p == 'global') {
/* do nothing */
} else if (p == 'drc') {
if (drcs) {
drcs.config(s, value);
}
} else if (p == 'eq') {
if (eqs) {
eqs.config(s, value);
}
} else {
console.log('invalid parameter: name =', name, 'value =', value);
}
}
this.input = input;
this.output = output;
this.config = config;
}
/* Returns the fft component for two channels */
function fft_2chan() {
var splitter = audioContext.createChannelSplitter(2);
var merger = audioContext.createChannelMerger(2);
analyzer_left = audioContext.createAnalyser();
analyzer_right = audioContext.createAnalyser();
analyzer_left.fftSize = FFT_SIZE;
analyzer_right.fftSize = FFT_SIZE;
splitter.connect(analyzer_left, 0, 0);
splitter.connect(analyzer_right, 1, 0);
analyzer_left.connect(merger, 0, 0);
analyzer_right.connect(merger, 0, 1);
function input(n) {
return [pin(splitter)];
}
function output(n) {
return [pin(merger)];
}
this.input = input;
this.output = output;
}
/* Returns eq for two channels */
function eq_2chan() {
var eqcs = [new eq_channel(0), new eq_channel(1)];
var splitter = audioContext.createChannelSplitter(2);
var merger = audioContext.createChannelMerger(2);
connect_from_native(pin(splitter, 0), eqcs[0]);
connect_from_native(pin(splitter, 1), eqcs[1]);
connect_to_native(eqcs[0], pin(merger, 0));
connect_to_native(eqcs[1], pin(merger, 1));
function input(n) {
return [pin(splitter)];
}
function output(n) {
return [pin(merger)];
}
function config(name, value) {
var p = parseInt(name[0]);
var s = name.slice(1);
eqcs[p].config(s, value);
}
this.input = input;
this.output = output;
this.config = config;
}
/* Returns eq for one channel (left or right). It contains a series of eq
* filters. */
function eq_channel(channel) {
var eqs = [];
var first = new delay(0);
var last = first;
for (var i = 0; i < NEQ; i++) {
eqs.push(new eq());
if (get_config('eq', channel, i, 'enable')) {
connect(last, eqs[i]);
last = eqs[i];
}
}
function input(n) {
return first.input(0);
}
function output(n) {
return last.output(0);
}
function config(name, value) {
var p = parseInt(name[0]);
var s = name.slice(1);
eqs[p].config(s, value);
}
this.input = input;
this.output = output;
this.config = config;
}
/* Returns a delay component (output = input with n seconds delay) */
function delay(n) {
var delay = audioContext.createDelay();
delay.delayTime.value = n;
function input(n) {
return [pin(delay)];
}
function output(n) {
return [pin(delay)];
}
function config(name, value) {
console.log('invalid parameter: name =', name, 'value =', value);
}
this.input = input;
this.output = output;
this.config = config;
}
/* Returns an eq filter */
function eq() {
var filter = audioContext.createBiquadFilter();
filter.type = INIT_EQ_TYPE;
filter.frequency.value = INIT_EQ_FREQ;
filter.Q.value = INIT_EQ_Q;
filter.gain.value = INIT_EQ_GAIN;
function input(n) {
return [pin(filter)];
}
function output(n) {
return [pin(filter)];
}
function config(name, value) {
switch (name[0]) {
case 'type':
filter.type = value;
break;
case 'freq':
filter.frequency.value = parseFloat(value);
break;
case 'q':
value = parseFloat(value);
if (filter.type == 'lowpass' || filter.type == 'highpass')
value = make_biquad_q(value);
filter.Q.value = value;
break;
case 'gain':
filter.gain.value = parseFloat(value);
break;
case 'enable':
break;
default:
console.log('invalid parameter: name =', name, 'value =', value);
}
}
this.input = input;
this.output = output;
this.config = config;
}
/* Returns DRC for 3 bands */
function drc_3band() {
var xo = new xo3();
var drcs = [new drc(), new drc(), new drc()];
var out = [];
for (var i = 0; i < 3; i++) {
if (get_config('drc', i, 'enable')) {
connect(xo, drcs[i], i);
out = out.concat(drcs[i].output());
} else {
/* The DynamicsCompressorNode in Chrome has 6ms pre-delay buffer. So for
* other bands we need to delay for the same amount of time.
*/
var d = new delay(0.006);
connect(xo, d, i);
out = out.concat(d.output());
}
}
function input(n) {
return xo.input(0);
}
function output(n) {
return out;
}
function config(name, value) {
if (name[1] == 'f') {
xo.config(name, value);
} else if (name[0] != 'emphasis_disabled') {
var n = parseInt(name[0]);
drcs[n].config(name.slice(1), value);
}
}
this.input = input;
this.output = output;
this.config = config;
}
/* This snippet came from LayoutTests/webaudio/dynamicscompressor-simple.html in
* https://codereview.chromium.org/152333003/. It can determine if
* emphasis/deemphasis is disabled in the browser. Then it sets the value to
* drc.emphasis_disabled in the config.*/
function get_emphasis_disabled() {
var context;
var sampleRate = 44100;
var lengthInSeconds = 1;
var renderedData;
// This threshold is experimentally determined. It depends on the the gain
// value of the gain node below and the dynamics compressor. When the
// DynamicsCompressor had the pre-emphasis filters, the peak value is about
// 0.21. Without it, the peak is 0.85.
var peakThreshold = 0.85;
function checkResult(event) {
var renderedBuffer = event.renderedBuffer;
renderedData = renderedBuffer.getChannelData(0);
// Search for a peak in the last part of the data.
var startSample = sampleRate * (lengthInSeconds - .1);
var endSample = renderedData.length;
var k;
var peak = -1;
var emphasis_disabled = 0;
for (k = startSample; k < endSample; ++k) {
var sample = Math.abs(renderedData[k]);
if (peak < sample)
peak = sample;
}
if (peak >= peakThreshold) {
console.log("Pre-emphasis effect not applied as expected..");
emphasis_disabled = 1;
} else {
console.log("Pre-emphasis caused output to be decreased to " + peak
+ " (expected >= " + peakThreshold + ")");
emphasis_disabled = 0;
}
browser_emphasis_disabled_detection_result = emphasis_disabled;
/* save_config button will be disabled until we can decide
emphasis_disabled in chrome. */
document.getElementById('save_config').disabled = false;
}
function runTest() {
context = new OfflineAudioContext(1, sampleRate * lengthInSeconds,
sampleRate);
// Connect an oscillator to a gain node to the compressor. The
// oscillator frequency is set to a high value for the (original)
// emphasis to kick in. The gain is a little extra boost to get the
// compressor enabled.
//
var osc = context.createOscillator();
osc.frequency.value = 15000;
var gain = context.createGain();
gain.gain.value = 1.5;
var compressor = context.createDynamicsCompressor();
osc.connect(gain);
gain.connect(compressor);
compressor.connect(context.destination);
osc.start();
context.oncomplete = checkResult;
context.startRendering();
}
runTest();
}
/* Returns one DRC filter */
function drc() {
var comp = audioContext.createDynamicsCompressor();
/* The supported method names are different on browsers with different
* versions.*/
audioContext.createGainNode = (audioContext.createGainNode ||
audioContext.createGain);
var boost = audioContext.createGainNode();
comp.threshold.value = INIT_DRC_THRESHOLD;
comp.knee.value = INIT_DRC_KNEE;
comp.ratio.value = INIT_DRC_RATIO;
comp.attack.value = INIT_DRC_ATTACK;
comp.release.value = INIT_DRC_RELEASE;
boost.gain.value = dBToLinear(INIT_DRC_BOOST);
comp.connect(boost);
function input(n) {
return [pin(comp)];
}
function output(n) {
return [pin(boost)];
}
function config(name, value) {
var p = name[0];
switch (p) {
case 'threshold':
case 'knee':
case 'ratio':
case 'attack':
case 'release':
comp[p].value = parseFloat(value);
break;
case 'boost':
boost.gain.value = dBToLinear(parseFloat(value));
break;
case 'enable':
break;
default:
console.log('invalid parameter: name =', name, 'value =', value);
}
}
this.input = input;
this.output = output;
this.config = config;
}
/* Crossover filter
*
* INPUT --+-- lp1 --+-- lp2a --+-- LOW (0)
* | | |
* | \-- hp2a --/
* |
* \-- hp1 --+-- lp2 ------ MID (1)
* |
* \-- hp2 ------ HIGH (2)
*
* [f1] [f2]
*/
/* Returns a crossover component which splits input into 3 bands */
function xo3() {
var f1 = INIT_DRC_XO_LOW;
var f2 = INIT_DRC_XO_HIGH;
var lp1 = lr4_lowpass(f1);
var hp1 = lr4_highpass(f1);
var lp2 = lr4_lowpass(f2);
var hp2 = lr4_highpass(f2);
var lp2a = lr4_lowpass(f2);
var hp2a = lr4_highpass(f2);
connect(lp1, lp2a);
connect(lp1, hp2a);
connect(hp1, lp2);
connect(hp1, hp2);
function input(n) {
return lp1.input().concat(hp1.input());
}
function output(n) {
switch (n) {
case 0:
return lp2a.output().concat(hp2a.output());
case 1:
return lp2.output();
case 2:
return hp2.output();
default:
console.log('invalid index ' + n);
return [];
}
}
function config(name, value) {
var p = name[0];
var s = name.slice(1);
if (p == '0') {
/* Ignore. The lower frequency of the low band is always 0. */
} else if (p == '1') {
lp1.config(s, value);
hp1.config(s, value);
} else if (p == '2') {
lp2.config(s, value);
hp2.config(s, value);
lp2a.config(s, value);
hp2a.config(s, value);
} else {
console.log('invalid parameter: name =', name, 'value =', value);
}
}
this.output = output;
this.input = input;
this.config = config;
}
/* Connects two components: the n-th output of c1 and the m-th input of c2. */
function connect(c1, c2, n, m) {
n = n || 0; /* default is the first output */
m = m || 0; /* default is the first input */
outs = c1.output(n);
ins = c2.input(m);
for (var i = 0; i < outs.length; i++) {
for (var j = 0; j < ins.length; j++) {
var from = outs[i];
var to = ins[j];
from.node.connect(to.node, from.index, to.index);
}
}
}
/* Connects from pin "from" to the n-th input of component c2 */
function connect_from_native(from, c2, n) {
n = n || 0; /* default is the first input */
ins = c2.input(n);
for (var i = 0; i < ins.length; i++) {
var to = ins[i];
from.node.connect(to.node, from.index, to.index);
}
}
/* Connects from m-th output of component c1 to pin "to" */
function connect_to_native(c1, to, m) {
m = m || 0; /* default is the first output */
outs = c1.output(m);
for (var i = 0; i < outs.length; i++) {
var from = outs[i];
from.node.connect(to.node, from.index, to.index);
}
}
/* Returns a LR4 lowpass component */
function lr4_lowpass(freq) {
return new double(freq, create_lowpass);
}
/* Returns a LR4 highpass component */
function lr4_highpass(freq) {
return new double(freq, create_highpass);
}
/* Returns a component by apply the same filter twice. */
function double(freq, creator) {
var f1 = creator(freq);
var f2 = creator(freq);
f1.connect(f2);
function input(n) {
return [pin(f1)];
}
function output(n) {
return [pin(f2)];
}
function config(name, value) {
if (name[0] == 'f') {
f1.frequency.value = parseFloat(value);
f2.frequency.value = parseFloat(value);
} else {
console.log('invalid parameter: name =', name, 'value =', value);
}
}
this.input = input;
this.output = output;
this.config = config;
}
/* Returns a lowpass filter */
function create_lowpass(freq) {
var lp = audioContext.createBiquadFilter();
lp.type = 'lowpass';
lp.frequency.value = freq;
lp.Q.value = make_biquad_q(0);
return lp;
}
/* Returns a highpass filter */
function create_highpass(freq) {
var hp = audioContext.createBiquadFilter();
hp.type = 'highpass';
hp.frequency.value = freq;
hp.Q.value = make_biquad_q(0);
return hp;
}
/* A pin specifies one of the input/output of a Web Audio node */
function pin(node, index) {
var p = new Pin();
p.node = node;
p.index = index || 0;
return p;
}
function Pin(node, index) {
}
/* ============================ Event Handlers ============================ */
function audio_source_select(select) {
var index = select.selectedIndex;
var url = document.getElementById('audio_source_url');
url.value = select.options[index].value;
url.blur();
audio_source_set(url.value);
}
/* Loads a local audio file. */
function load_audio() {
document.getElementById('audio_file').click();
}
function audio_file_changed() {
var input = document.getElementById('audio_file');
var file = input.files[0];
var file_url = window.webkitURL.createObjectURL(file);
input.value = '';
var url = document.getElementById('audio_source_url');
url.value = file.name;
audio_source_set(file_url);
}
function audio_source_set(url) {
var player = document.getElementById('audio_player');
var container = document.getElementById('audio_player_container');
var loading = document.getElementById('audio_loading');
loading.style.visibility = 'visible';
/* Re-create an audio element when the audio source URL is changed. */
player.pause();
container.removeChild(player);
player = document.createElement('audio');
player.crossOrigin = 'anonymous';
player.id = 'audio_player';
player.loop = true;
player.controls = true;
player.addEventListener('canplay', audio_source_canplay);
container.appendChild(player);
update_source_node(player);
player.src = url;
player.load();
}
function audio_source_canplay() {
var player = document.getElementById('audio_player');
var loading = document.getElementById('audio_loading');
loading.style.visibility = 'hidden';
player.play();
}
function update_source_node(mediaElement) {
sourceNode = audioContext.createMediaElementSource(mediaElement);
build_graph();
}
function toggle_global_checkbox(name, enable) {
use_config('global', name, enable);
build_graph();
}
function toggle_one_drc(index, enable) {
use_config('drc', index, 'enable', enable);
build_graph();
}
function toggle_one_eq(channel, index, enable) {
use_config('eq', channel, index, 'enable', enable);
build_graph();
}
/* ============================== UI widgets ============================== */
/* Adds a row to the table. The row contains an input box and a slider. */
function slider_input(table, name, initial_value, min_value, max_value, step,
suffix, handler) {
function id(x) {
return x;
}
return new slider_input_common(table, name, initial_value, min_value,
max_value, step, suffix, handler, id, id);
}
/* This is similar to slider_input, but uses log scale for the slider. */
function slider_input_log(table, name, initial_value, min_value, max_value,
suffix, precision, handler, mapping,
inverse_mapping) {
function mapping(x) {
return Math.log(x + 1);
}
function inv_mapping(x) {
return (Math.exp(x) - 1).toFixed(precision);
}
return new slider_input_common(table, name, initial_value, min_value,
max_value, 1e-6, suffix, handler, mapping,
inv_mapping);
}
/* The common implementation of linear and log-scale sliders. Each slider has
* the following methods:
*
* function update(v) - update the slider (and the text box) to the value v.
*
* function hide(h) - hide/unhide the slider.
*/
function slider_input_common(table, name, initial_value, min_value, max_value,
step, suffix, handler, mapping, inv_mapping) {
var row = table.insertRow(-1);
var col_name = row.insertCell(-1);
var col_box = row.insertCell(-1);
var col_slider = row.insertCell(-1);
var name_span = document.createElement('span');
name_span.appendChild(document.createTextNode(name));
col_name.appendChild(name_span);
var box = document.createElement('input');
box.defaultValue = initial_value;
box.type = 'text';
box.size = 5;
box.className = 'nbox';
col_box.appendChild(box);
var suffix_span = document.createElement('span');
suffix_span.appendChild(document.createTextNode(suffix));
col_box.appendChild(suffix_span);
var slider = document.createElement('input');
slider.defaultValue = Math.log(initial_value);
slider.type = 'range';
slider.className = 'nslider';
slider.min = mapping(min_value);
slider.max = mapping(max_value);
slider.step = step;
col_slider.appendChild(slider);
box.onchange = function() {
slider.value = mapping(box.value);
handler(parseFloat(box.value));
};
slider.onchange = function() {
box.value = inv_mapping(slider.value);
handler(parseFloat(box.value));
};
function update(v) {
box.value = v;
slider.value = mapping(v);
}
function hide(h) {
var v = h ? 'hidden' : 'visible';
name_span.style.visibility = v;
box.style.visibility = v;
suffix_span.style.visibility = v;
slider.style.visibility = v;
}
this.update = update;
this.hide = hide;
}
/* Adds a enable/disable checkbox to a div. The method "update" can change the
* checkbox state. */
function check_button(div, handler) {
var check = document.createElement('input');
check.className = 'enable_check';
check.type = 'checkbox';
check.checked = true;
check.onchange = function() {
handler(check.checked);
};
div.appendChild(check);
function update(v) {
check.checked = v;
}
this.update = update;
}
function dummy() {
}
/* Changes the opacity of a div. */
function toggle_card(div, enable) {
div.style.opacity = enable ? 1 : 0.3;
}
/* Appends a card of DRC controls and graphs to the specified parent.
* Args:
* parent - The parent element
* index - The index of this DRC component (0-2)
* lower_freq - The lower frequency of this DRC component
* freq_label - The label for the lower frequency input text box
*/
function drc_card(parent, index, lower_freq, freq_label) {
var top = document.createElement('div');
top.className = 'drc_data';
parent.appendChild(top);
function toggle_drc_card(enable) {
toggle_card(div, enable);
toggle_one_drc(index, enable);
}
var enable_button = new check_button(top, toggle_drc_card);
var div = document.createElement('div');
top.appendChild(div);
/* Canvas */
var p = document.createElement('p');
div.appendChild(p);
var canvas = document.createElement('canvas');
canvas.className = 'drc_curve';
p.appendChild(canvas);
canvas.width = 240;
canvas.height = 180;
var dd = new DrcDrawer(canvas);
dd.init();
/* Parameters */
var table = document.createElement('table');
div.appendChild(table);
function change_lower_freq(v) {
use_config('drc', index, 'f', v);
}
function change_threshold(v) {
dd.update_threshold(v);
use_config('drc', index, 'threshold', v);
}
function change_knee(v) {
dd.update_knee(v);
use_config('drc', index, 'knee', v);
}
function change_ratio(v) {
dd.update_ratio(v);
use_config('drc', index, 'ratio', v);
}
function change_boost(v) {
dd.update_boost(v);
use_config('drc', index, 'boost', v);
}
function change_attack(v) {
use_config('drc', index, 'attack', v);
}
function change_release(v) {
use_config('drc', index, 'release', v);
}
var f_slider;
if (lower_freq == 0) { /* Special case for the lowest band */
f_slider = new slider_input_log(table, freq_label, lower_freq, 0, 1,
'Hz', 0, dummy);
f_slider.hide(true);
} else {
f_slider = new slider_input_log(table, freq_label, lower_freq, 1,
nyquist, 'Hz', 0, change_lower_freq);
}
var sliders = {
'f': f_slider,
'threshold': new slider_input(table, 'Threshold', INIT_DRC_THRESHOLD,
-100, 0, 1, 'dB', change_threshold),
'knee': new slider_input(table, 'Knee', INIT_DRC_KNEE, 0, 40, 1, 'dB',
change_knee),
'ratio': new slider_input(table, 'Ratio', INIT_DRC_RATIO, 1, 20, 0.001,
'', change_ratio),
'boost': new slider_input(table, 'Boost', 0, -40, 40, 1, 'dB',
change_boost),
'attack': new slider_input(table, 'Attack', INIT_DRC_ATTACK, 0.001,
1, 0.001, 's', change_attack),
'release': new slider_input(table, 'Release', INIT_DRC_RELEASE,
0.001, 1, 0.001, 's', change_release)
};
function config(name, value) {
var p = name[0];
var fv = parseFloat(value);
switch (p) {
case 'f':
case 'threshold':
case 'knee':
case 'ratio':
case 'boost':
case 'attack':
case 'release':
sliders[p].update(fv);
break;
case 'enable':
toggle_card(div, value);
enable_button.update(value);
break;
default:
console.log('invalid parameter: name =', name, 'value =', value);
}
switch (p) {
case 'threshold':
dd.update_threshold(fv);
break;
case 'knee':
dd.update_knee(fv);
break;
case 'ratio':
dd.update_ratio(fv);
break;
case 'boost':
dd.update_boost(fv);
break;
}
}
this.config = config;
}
/* Appends a menu of biquad types to the specified table. */
function biquad_type_select(table, handler) {
var row = table.insertRow(-1);
var col_name = row.insertCell(-1);
var col_menu = row.insertCell(-1);
col_name.appendChild(document.createTextNode('Type'));
var select = document.createElement('select');
select.className = 'biquad_type_select';
var options = [
'lowpass',
'highpass',
'bandpass',
'lowshelf',
'highshelf',
'peaking',
'notch'
/* no need: 'allpass' */
];
for (var i = 0; i < options.length; i++) {
var o = document.createElement('option');
o.appendChild(document.createTextNode(options[i]));
select.appendChild(o);
}
select.value = INIT_EQ_TYPE;
col_menu.appendChild(select);
function onchange() {
handler(select.value);
}
select.onchange = onchange;
function update(v) {
select.value = v;
}
this.update = update;
}
/* Appends a card of EQ controls to the specified parent.
* Args:
* parent - The parent element
* channel - The index of the channel this EQ component is on (0-1)
* index - The index of this EQ on this channel (0-7)
* ed - The EQ curve drawer. We will notify the drawer to redraw if the
* parameters for this EQ changes.
*/
function eq_card(parent, channel, index, ed) {
var top = document.createElement('div');
top.className = 'eq_data';
parent.appendChild(top);
function toggle_eq_card(enable) {
toggle_card(table, enable);
toggle_one_eq(channel, index, enable);
ed.update_enable(index, enable);
}
var enable_button = new check_button(top, toggle_eq_card);
var table = document.createElement('table');
table.className = 'eq_table';
top.appendChild(table);
function change_type(v) {
ed.update_type(index, v);
hide_unused_slider(v);
use_config('eq', channel, index, 'type', v);
/* Special case: automatically set Q to 0 for lowpass/highpass filters. */
if (v == 'lowpass' || v == 'highpass') {
use_config('eq', channel, index, 'q', 0);
}
}
function change_freq(v)
{
ed.update_freq(index, v);
use_config('eq', channel, index, 'freq', v);
}
function change_q(v)
{
ed.update_q(index, v);
use_config('eq', channel, index, 'q', v);
}
function change_gain(v)
{
ed.update_gain(index, v);
use_config('eq', channel, index, 'gain', v);
}
var type_select = new biquad_type_select(table, change_type);
var sliders = {
'freq': new slider_input_log(table, 'Frequency', INIT_EQ_FREQ, 1,
nyquist, 'Hz', 0, change_freq),
'q': new slider_input_log(table, 'Q', INIT_EQ_Q, 0, 1000, '', 4,
change_q),
'gain': new slider_input(table, 'Gain', INIT_EQ_GAIN, -40, 40, 0.1,
'dB', change_gain)
};
var unused = {
'lowpass': [0, 0, 1],
'highpass': [0, 0, 1],
'bandpass': [0, 0, 1],
'lowshelf': [0, 1, 0],
'highshelf': [0, 1, 0],
'peaking': [0, 0, 0],
'notch': [0, 0, 1],
'allpass': [0, 0, 1]
};
function hide_unused_slider(type) {
var u = unused[type];
sliders['freq'].hide(u[0]);
sliders['q'].hide(u[1]);
sliders['gain'].hide(u[2]);
}
function config(name, value) {
var p = name[0];
var fv = parseFloat(value);
switch (p) {
case 'type':
type_select.update(value);
break;
case 'freq':
case 'q':
case 'gain':
sliders[p].update(fv);
break;
case 'enable':
toggle_card(table, value);
enable_button.update(value);
break;
default:
console.log('invalid parameter: name =', name, 'value =', value);
}
switch (p) {
case 'type':
ed.update_type(index, value);
hide_unused_slider(value);
break;
case 'freq':
ed.update_freq(index, fv);
break;
case 'q':
ed.update_q(index, fv);
break;
case 'gain':
ed.update_gain(index, fv);
break;
}
}
this.config = config;
}
/* Appends the EQ UI for one channel to the specified parent */
function eq_section(parent, channel) {
/* Two canvas, one for eq curve, another for fft. */
var p = document.createElement('p');
p.className = 'eq_curve_parent';
var canvas_eq = document.createElement('canvas');
canvas_eq.className = 'eq_curve';
canvas_eq.width = 960;
canvas_eq.height = 270;
p.appendChild(canvas_eq);
var ed = new EqDrawer(canvas_eq, channel);
ed.init();
var canvas_fft = document.createElement('canvas');
canvas_fft.className = 'eq_curve';
canvas_fft.width = 960;
canvas_fft.height = 270;
p.appendChild(canvas_fft);
var fd = new FFTDrawer(canvas_fft, channel);
fd.init();
parent.appendChild(p);
/* Eq cards */
var eq = {};
for (var i = 0; i < NEQ; i++) {
eq[i] = new eq_card(parent, channel, i, ed);
}
function config(name, value) {
var p = parseInt(name[0]);
var s = name.slice(1);
eq[p].config(s, value);
}
this.config = config;
}
function global_section(parent) {
var checkbox_data = [
/* config name, text label, checkbox object */
['enable_drc', 'Enable DRC', null],
['enable_eq', 'Enable EQ', null],
['enable_fft', 'Show FFT', null],
['enable_swap', 'Swap DRC/EQ', null]
];
for (var i = 0; i < checkbox_data.length; i++) {
config_name = checkbox_data[i][0];
text_label = checkbox_data[i][1];
var cb = document.createElement('input');
cb.type = 'checkbox';
cb.checked = get_global(config_name);
cb.onchange = function(name) {
return function() { toggle_global_checkbox(name, this.checked); }
}(config_name);
checkbox_data[i][2] = cb;
parent.appendChild(cb);
parent.appendChild(document.createTextNode(text_label));
}
function config(name, value) {
var i;
for (i = 0; i < checkbox_data.length; i++) {
if (checkbox_data[i][0] == name[0]) {
break;
}
}
if (i < checkbox_data.length) {
checkbox_data[i][2].checked = value;
} else {
console.log('invalid parameter: name =', name, 'value =', value);
}
}
this.config = config;
}
window.onload = function() {
fix_audio_elements();
check_biquad_filter_q().then(function (flag) {
console.log('Browser biquad filter uses Audio Cookbook formula:', flag);
/* Detects if emphasis is disabled and sets
* browser_emphasis_disabled_detection_result. */
get_emphasis_disabled();
init_config();
init_audio();
init_ui();
}).catch(function (reason) {
alert('Cannot detect browser biquad filter implementation:', reason);
});
};
function init_ui() {
audio_ui = new ui();
}
/* Top-level UI */
function ui() {
var global = new global_section(document.getElementById('global_section'));
var drc_div = document.getElementById('drc_section');
var drc_cards = [
new drc_card(drc_div, 0, 0, ''),
new drc_card(drc_div, 1, INIT_DRC_XO_LOW, 'Start From'),
new drc_card(drc_div, 2, INIT_DRC_XO_HIGH, 'Start From')
];
var left_div = document.getElementById('eq_left_section');
var right_div = document.getElementById('eq_right_section');
var eq_sections = [
new eq_section(left_div, 0),
new eq_section(right_div, 1)
];
function config(name, value) {
var p = name[0];
var i = parseInt(name[1]);
var s = name.slice(2);
if (p == 'global') {
global.config(name.slice(1), value);
} else if (p == 'drc') {
if (name[1] == 'emphasis_disabled') {
return;
}
drc_cards[i].config(s, value);
} else if (p == 'eq') {
eq_sections[i].config(s, value);
} else {
console.log('invalid parameter: name =', name, 'value =', value);
}
}
this.config = config;
}
/* Draws the DRC curve on a canvas. The update*() methods should be called when
* the parameters change, so the curve can be redrawn. */
function DrcDrawer(canvas) {
var canvasContext = canvas.getContext('2d');
var backgroundColor = 'black';
var curveColor = 'rgb(192,192,192)';
var gridColor = 'rgb(200,200,200)';
var textColor = 'rgb(238,221,130)';
var thresholdColor = 'rgb(255,160,122)';
var dbThreshold = INIT_DRC_THRESHOLD;
var dbKnee = INIT_DRC_KNEE;
var ratio = INIT_DRC_RATIO;
var boost = INIT_DRC_BOOST;
var curve_slope;
var curve_k;
var linearThreshold;
var kneeThresholdDb;
var kneeThreshold;
var ykneeThresholdDb;
var masterLinearGain;
var maxOutputDb = 6;
var minOutputDb = -36;
function xpixelToDb(x) {
/* This is right even though it looks like we should scale by width. We
* want the same pixel/dB scale for both. */
var k = x / canvas.height;
var db = minOutputDb + k * (maxOutputDb - minOutputDb);
return db;
}
function dBToXPixel(db) {
var k = (db - minOutputDb) / (maxOutputDb - minOutputDb);
var x = k * canvas.height;
return x;
}
function ypixelToDb(y) {
var k = y / canvas.height;
var db = maxOutputDb - k * (maxOutputDb - minOutputDb);
return db;
}
function dBToYPixel(db) {
var k = (maxOutputDb - db) / (maxOutputDb - minOutputDb);
var y = k * canvas.height;
return y;
}
function kneeCurve(x, k) {
if (x < linearThreshold)
return x;
return linearThreshold +
(1 - Math.exp(-k * (x - linearThreshold))) / k;
}
function saturate(x, k) {
var y;
if (x < kneeThreshold) {
y = kneeCurve(x, k);
} else {
var xDb = linearToDb(x);
var yDb = ykneeThresholdDb + curve_slope * (xDb - kneeThresholdDb);
y = dBToLinear(yDb);
}
return y;
}
function slopeAt(x, k) {
if (x < linearThreshold)
return 1;
var x2 = x * 1.001;
var xDb = linearToDb(x);
var x2Db = linearToDb(x2);
var yDb = linearToDb(kneeCurve(x, k));
var y2Db = linearToDb(kneeCurve(x2, k));
var m = (y2Db - yDb) / (x2Db - xDb);
return m;
}
function kAtSlope(desiredSlope) {
var xDb = dbThreshold + dbKnee;
var x = dBToLinear(xDb);
var minK = 0.1;
var maxK = 10000;
var k = 5;
for (var i = 0; i < 15; i++) {
var slope = slopeAt(x, k);
if (slope < desiredSlope) {
maxK = k;
} else {
minK = k;
}
k = Math.sqrt(minK * maxK);
}
return k;
}
function drawCurve() {
/* Update curve parameters */
linearThreshold = dBToLinear(dbThreshold);
curve_slope = 1 / ratio;
curve_k = kAtSlope(1 / ratio);
kneeThresholdDb = dbThreshold + dbKnee;
kneeThreshold = dBToLinear(kneeThresholdDb);
ykneeThresholdDb = linearToDb(kneeCurve(kneeThreshold, curve_k));
/* Calculate masterLinearGain */
var fullRangeGain = saturate(1, curve_k);
var fullRangeMakeupGain = Math.pow(1 / fullRangeGain, 0.6);
masterLinearGain = dBToLinear(boost) * fullRangeMakeupGain;
/* Clear canvas */
var width = canvas.width;
var height = canvas.height;
canvasContext.fillStyle = backgroundColor;
canvasContext.fillRect(0, 0, width, height);
/* Draw linear response for reference. */
canvasContext.strokeStyle = gridColor;
canvasContext.lineWidth = 1;
canvasContext.beginPath();
canvasContext.moveTo(dBToXPixel(minOutputDb), dBToYPixel(minOutputDb));
canvasContext.lineTo(dBToXPixel(maxOutputDb), dBToYPixel(maxOutputDb));
canvasContext.stroke();
/* Draw 0dBFS output levels from 0dBFS down to -36dBFS */
for (var dbFS = 0; dbFS >= -36; dbFS -= 6) {
canvasContext.beginPath();
var y = dBToYPixel(dbFS);
canvasContext.setLineDash([1, 4]);
canvasContext.moveTo(0, y);
canvasContext.lineTo(width, y);
canvasContext.stroke();
canvasContext.setLineDash([]);
canvasContext.textAlign = 'center';
canvasContext.strokeStyle = textColor;
canvasContext.strokeText(dbFS.toFixed(0) + ' dB', 15, y - 2);
canvasContext.strokeStyle = gridColor;
}
/* Draw 0dBFS input line */
canvasContext.beginPath();
canvasContext.moveTo(dBToXPixel(0), 0);
canvasContext.lineTo(dBToXPixel(0), height);
canvasContext.stroke();
canvasContext.strokeText('0dB', dBToXPixel(0), height);
/* Draw threshold input line */
canvasContext.beginPath();
canvasContext.moveTo(dBToXPixel(dbThreshold), 0);
canvasContext.lineTo(dBToXPixel(dbThreshold), height);
canvasContext.moveTo(dBToXPixel(kneeThresholdDb), 0);
canvasContext.lineTo(dBToXPixel(kneeThresholdDb), height);
canvasContext.strokeStyle = thresholdColor;
canvasContext.stroke();
/* Draw the compressor curve */
canvasContext.strokeStyle = curveColor;
canvasContext.lineWidth = 3;
canvasContext.beginPath();
var pixelsPerDb = (0.5 * height) / 40.0;
for (var x = 0; x < width; ++x) {
var inputDb = xpixelToDb(x);
var inputLinear = dBToLinear(inputDb);
var outputLinear = saturate(inputLinear, curve_k);
outputLinear *= masterLinearGain;
var outputDb = linearToDb(outputLinear);
var y = dBToYPixel(outputDb);
canvasContext.lineTo(x, y);
}
canvasContext.stroke();
}
function init() {
drawCurve();
}
function update_threshold(v)
{
dbThreshold = v;
drawCurve();
}
function update_knee(v)
{
dbKnee = v;
drawCurve();
}
function update_ratio(v)
{
ratio = v;
drawCurve();
}
function update_boost(v)
{
boost = v;
drawCurve();
}
this.init = init;
this.update_threshold = update_threshold;
this.update_knee = update_knee;
this.update_ratio = update_ratio;
this.update_boost = update_boost;
}
/* Draws the EQ curve on a canvas. The update*() methods should be called when
* the parameters change, so the curve can be redrawn. */
function EqDrawer(canvas, channel) {
var canvasContext = canvas.getContext('2d');
var curveColor = 'rgb(192,192,192)';
var gridColor = 'rgb(200,200,200)';
var textColor = 'rgb(238,221,130)';
var centerFreq = {};
var q = {};
var gain = {};
for (var i = 0; i < NEQ; i++) {
centerFreq[i] = INIT_EQ_FREQ;
q[i] = INIT_EQ_Q;
gain[i] = INIT_EQ_GAIN;
}
function drawCurve() {
/* Create a biquad node to calculate frequency response. */
var filter = audioContext.createBiquadFilter();
var width = canvas.width;
var height = canvas.height;
var pixelsPerDb = height / 48.0;
var noctaves = 10;
/* Prepare the frequency array */
var frequencyHz = new Float32Array(width);
for (var i = 0; i < width; ++i) {
var f = i / width;
/* Convert to log frequency scale (octaves). */
f = Math.pow(2.0, noctaves * (f - 1.0));
frequencyHz[i] = f * nyquist;
}
/* Get the response */
var magResponse = new Float32Array(width);
var phaseResponse = new Float32Array(width);
var totalMagResponse = new Float32Array(width);
for (var i = 0; i < width; i++) {
totalMagResponse[i] = 1;
}
for (var i = 0; i < NEQ; i++) {
if (!get_config('eq', channel, i, 'enable')) {
continue;
}
filter.type = get_config('eq', channel, i, 'type');
filter.frequency.value = centerFreq[i];
if (filter.type == 'lowpass' || filter.type == 'highpass')
filter.Q.value = make_biquad_q(q[i]);
else
filter.Q.value = q[i];
filter.gain.value = gain[i];
filter.getFrequencyResponse(frequencyHz, magResponse,
phaseResponse);
for (var j = 0; j < width; j++) {
totalMagResponse[j] *= magResponse[j];
}
}
/* Draw the response */
canvasContext.fillStyle = 'rgb(0, 0, 0)';
canvasContext.fillRect(0, 0, width, height);
canvasContext.strokeStyle = curveColor;
canvasContext.lineWidth = 3;
canvasContext.beginPath();
for (var i = 0; i < width; ++i) {
var response = totalMagResponse[i];
var dbResponse = linearToDb(response);
var x = i;
var y = height - (dbResponse + 24) * pixelsPerDb;
canvasContext.lineTo(x, y);
}
canvasContext.stroke();
/* Draw frequency scale. */
canvasContext.beginPath();
canvasContext.lineWidth = 1;
canvasContext.strokeStyle = gridColor;
for (var octave = 0; octave <= noctaves; octave++) {
var x = octave * width / noctaves;
canvasContext.moveTo(x, 30);
canvasContext.lineTo(x, height);
canvasContext.stroke();
var f = nyquist * Math.pow(2.0, octave - noctaves);
canvasContext.textAlign = 'center';
canvasContext.strokeText(f.toFixed(0) + 'Hz', x, 20);
}
/* Draw 0dB line. */
canvasContext.beginPath();
canvasContext.moveTo(0, 0.5 * height);
canvasContext.lineTo(width, 0.5 * height);
canvasContext.stroke();
/* Draw decibel scale. */
for (var db = -24.0; db < 24.0; db += 6) {
var y = height - (db + 24) * pixelsPerDb;
canvasContext.beginPath();
canvasContext.setLineDash([1, 4]);
canvasContext.moveTo(0, y);
canvasContext.lineTo(width, y);
canvasContext.stroke();
canvasContext.setLineDash([]);
canvasContext.strokeStyle = textColor;
canvasContext.strokeText(db.toFixed(0) + 'dB', width - 20, y);
canvasContext.strokeStyle = gridColor;
}
}
function update_freq(index, v) {
centerFreq[index] = v;
drawCurve();
}
function update_q(index, v) {
q[index] = v;
drawCurve();
}
function update_gain(index, v) {
gain[index] = v;
drawCurve();
}
function update_enable(index, v) {
drawCurve();
}
function update_type(index, v) {
drawCurve();
}
function init() {
drawCurve();
}
this.init = init;
this.update_freq = update_freq;
this.update_q = update_q;
this.update_gain = update_gain;
this.update_enable = update_enable;
this.update_type = update_type;
}
/* Draws the FFT curve on a canvas. This will update continuously when the audio
* is playing. */
function FFTDrawer(canvas, channel) {
var canvasContext = canvas.getContext('2d');
var curveColor = 'rgb(255,160,122)';
var binCount = FFT_SIZE / 2;
var data = new Float32Array(binCount);
function drawCurve() {
var width = canvas.width;
var height = canvas.height;
var pixelsPerDb = height / 96.0;
canvasContext.clearRect(0, 0, width, height);
/* Get the proper analyzer from the audio graph */
var analyzer = (channel == 0) ? analyzer_left : analyzer_right;
if (!analyzer || !get_global('enable_fft')) {
requestAnimationFrame(drawCurve);
return;
}
/* Draw decibel scale. */
for (var db = -96.0; db <= 0; db += 12) {
var y = height - (db + 96) * pixelsPerDb;
canvasContext.strokeStyle = curveColor;
canvasContext.strokeText(db.toFixed(0) + 'dB', 10, y);
}
/* Draw FFT */
analyzer.getFloatFrequencyData(data);
canvasContext.beginPath();
canvasContext.lineWidth = 1;
canvasContext.strokeStyle = curveColor;
canvasContext.moveTo(0, height);
var frequencyHz = new Float32Array(width);
for (var i = 0; i < binCount; ++i) {
var f = i / binCount;
/* Convert to log frequency scale (octaves). */
var noctaves = 10;
f = 1 + Math.log(f) / (noctaves * Math.LN2);
/* Draw the magnitude */
var x = f * width;
var y = height - (data[i] + 96) * pixelsPerDb;
canvasContext.lineTo(x, y);
}
canvasContext.stroke();
requestAnimationFrame(drawCurve);
}
function init() {
requestAnimationFrame(drawCurve);
}
this.init = init;
}
function dBToLinear(db) {
return Math.pow(10.0, 0.05 * db);
}
function linearToDb(x) {
return 20.0 * Math.log(x) / Math.LN10;
}