Home | History | Annotate | Download | only in frontend
      1 /* Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
      2  * Use of this source code is governed by a BSD-style license that can be
      3  * found in the LICENSE file.
      4  */
      5 
      6 /* This is a program for tuning audio using Web Audio API. The processing
      7  * pipeline looks like this:
      8  *
      9  *                   INPUT
     10  *                     |
     11  *               +------------+
     12  *               | crossover  |
     13  *               +------------+
     14  *               /     |      \
     15  *      (low band) (mid band) (high band)
     16  *             /       |        \
     17  *         +------+ +------+ +------+
     18  *         |  DRC | |  DRC | |  DRC |
     19  *         +------+ +------+ +------+
     20  *              \      |        /
     21  *               \     |       /
     22  *              +-------------+
     23  *              |     (+)     |
     24  *              +-------------+
     25  *                 |        |
     26  *              (left)   (right)
     27  *                 |        |
     28  *              +----+   +----+
     29  *              | EQ |   | EQ |
     30  *              +----+   +----+
     31  *                 |        |
     32  *              +----+   +----+
     33  *              | EQ |   | EQ |
     34  *              +----+   +----+
     35  *                 .        .
     36  *                 .        .
     37  *              +----+   +----+
     38  *              | EQ |   | EQ |
     39  *              +----+   +----+
     40  *                  \     /
     41  *                   \   /
     42  *                     |
     43  *                   /   \
     44  *                  /     \
     45  *             +-----+   +-----+
     46  *             | FFT |   | FFT | (for visualization only)
     47  *             +-----+   +-----+
     48  *                  \     /
     49  *                   \   /
     50  *                     |
     51  *                   OUTPUT
     52  *
     53  * The parameters of each DRC and EQ can be adjusted or disabled independently.
     54  *
     55  * If enable_swap is set to true, the order of the DRC and the EQ stages are
     56  * swapped (EQ is applied first, then DRC).
     57  */
     58 
     59 /* The GLOBAL state has following parameters:
     60  * enable_drc - A switch to turn all DRC on/off.
     61  * enable_eq - A switch to turn all EQ on/off.
     62  * enable_fft - A switch to turn visualization on/off.
     63  * enable_swap - A switch to swap the order of EQ and DRC stages.
     64  */
     65 
     66 /* The DRC has following parameters:
     67  * f - The lower frequency of the band, in Hz.
     68  * enable - 1 to enable the compressor, 0 to disable it.
     69  * threshold - The value above which the compression starts, in dB.
     70  * knee - The value above which the knee region starts, in dB.
     71  * ratio - The input/output dB ratio after the knee region.
     72  * attack - The time to reduce the gain by 10dB, in seconds.
     73  * release - The time to increase the gain by 10dB, in seconds.
     74  * boost - The static boost value in output, in dB.
     75  */
     76 
     77 /* The EQ has following parameters:
     78  * enable - 1 to enable the eq, 0 to disable it.
     79  * type - The type of the eq, the available values are 'lowpass', 'highpass',
     80  *     'bandpass', 'lowshelf', 'highshelf', 'peaking', 'notch'.
     81  * freq - The frequency of the eq, in Hz.
     82  * q, gain - The meaning depends on the type of the filter. See Web Audio API
     83  *     for details.
     84  */
     85 
     86 /* The initial values of parameters for GLOBAL, DRC and EQ */
     87 var INIT_GLOBAL_ENABLE_DRC = true;
     88 var INIT_GLOBAL_ENABLE_EQ = true;
     89 var INIT_GLOBAL_ENABLE_FFT = true;
     90 var INIT_GLOBAL_ENABLE_SWAP = false;
     91 var INIT_DRC_XO_LOW = 200;
     92 var INIT_DRC_XO_HIGH = 2000;
     93 var INIT_DRC_ENABLE = true;
     94 var INIT_DRC_THRESHOLD = -24;
     95 var INIT_DRC_KNEE = 30;
     96 var INIT_DRC_RATIO = 12;
     97 var INIT_DRC_ATTACK = 0.003;
     98 var INIT_DRC_RELEASE = 0.250;
     99 var INIT_DRC_BOOST = 0;
    100 var INIT_EQ_ENABLE = true;
    101 var INIT_EQ_TYPE = 'peaking';
    102 var INIT_EQ_FREQ = 350;
    103 var INIT_EQ_Q = 1;
    104 var INIT_EQ_GAIN = 0;
    105 
    106 var NEQ = 8;  /* The number of EQs per channel */
    107 var FFT_SIZE = 2048;  /* The size of FFT used for visualization */
    108 
    109 var audioContext;  /* Web Audio context */
    110 var nyquist;       /* Nyquist frequency, in Hz */
    111 var sourceNode;
    112 var audio_graph;
    113 var audio_ui;
    114 var analyzer_left;      /* The FFT analyzer for left channel */
    115 var analyzer_right;     /* The FFT analyzer for right channel */
    116 /* get_emphasis_disabled detects if pre-emphasis in drc is disabled by browser.
    117  * The detection result will be stored in this value. When user saves config,
    118  * This value is stored in drc.emphasis_disabled in the config. */
    119 var browser_emphasis_disabled_detection_result;
    120 /* check_biquad_filter_q detects if the browser implements the lowpass and
    121  * highpass biquad filters with the original formula or the new formula from
    122  * Audio EQ Cookbook. Chrome changed the filter implementation in R53, see:
    123  * https://github.com/GoogleChrome/web-audio-samples/wiki/Detection-of-lowpass-BiquadFilter-implementation
    124  * The detection result is saved in this value before the page is initialized.
    125  * make_biquad_q() uses this value to compute Q to ensure consistent behavior
    126  * on different browser versions.
    127  */
    128 var browser_biquad_filter_uses_audio_cookbook_formula;
    129 
    130 /* Check the lowpass implementation and return a promise. */
    131 function check_biquad_filter_q() {
    132   'use strict';
    133   var context = new OfflineAudioContext(1, 128, 48000);
    134   var osc = context.createOscillator();
    135   var filter1 = context.createBiquadFilter();
    136   var filter2 = context.createBiquadFilter();
    137   var inverter = context.createGain();
    138 
    139   osc.type = 'sawtooth';
    140   osc.frequency.value = 8 * 440;
    141   inverter.gain.value = -1;
    142   /* each filter should get a different Q value */
    143   filter1.Q.value = -1;
    144   filter2.Q.value = -20;
    145   osc.connect(filter1);
    146   osc.connect(filter2);
    147   filter1.connect(context.destination);
    148   filter2.connect(inverter);
    149   inverter.connect(context.destination);
    150   osc.start();
    151 
    152   return context.startRendering().then(function (buffer) {
    153     return browser_biquad_filter_uses_audio_cookbook_formula =
    154       Math.max(...buffer.getChannelData(0)) !== 0;
    155   });
    156 }
    157 
    158 /* Return the Q value to be used with the lowpass and highpass biquad filters,
    159  * given Q in dB for the original filter formula. If the browser uses the new
    160  * formula, conversion is made to simulate the original frequency response
    161  * with the new formula.
    162  */
    163 function make_biquad_q(q_db) {
    164   if (!browser_biquad_filter_uses_audio_cookbook_formula)
    165     return q_db;
    166 
    167   var q_lin = dBToLinear(q_db);
    168   var q_new = 1 / Math.sqrt((4 - Math.sqrt(16 - 16 / (q_lin * q_lin))) / 2);
    169   q_new = linearToDb(q_new);
    170   return q_new;
    171 }
    172 
    173 /* The supported audio element names are different on browsers with different
    174  * versions.*/
    175 function fix_audio_elements() {
    176   try {
    177     window.AudioContext = window.AudioContext || window.webkitAudioContext;
    178     window.OfflineAudioContext = (window.OfflineAudioContext ||
    179         window.webkitOfflineAudioContext);
    180   }
    181   catch(e) {
    182     alert('Web Audio API is not supported in this browser');
    183   }
    184 }
    185 
    186 function init_audio() {
    187   audioContext = new AudioContext();
    188   nyquist = audioContext.sampleRate / 2;
    189 }
    190 
    191 function build_graph() {
    192   if (sourceNode) {
    193     audio_graph = new graph();
    194     sourceNode.disconnect();
    195     if (get_global('enable_drc') || get_global('enable_eq') ||
    196         get_global('enable_fft')) {
    197       connect_from_native(pin(sourceNode), audio_graph);
    198       connect_to_native(audio_graph, pin(audioContext.destination));
    199     } else {
    200       /* no processing needed, directly connect from source to destination. */
    201       sourceNode.connect(audioContext.destination);
    202     }
    203   }
    204   apply_all_configs();
    205 }
    206 
    207 /* The available configuration variables are:
    208  *
    209  * global.{enable_drc, enable_eq, enable_fft, enable_swap}
    210  * drc.[0-2].{f, enable, threshold, knee, ratio, attack, release, boost}
    211  * eq.[01].[0-7].{enable, type, freq, q, gain}.
    212  *
    213  * Each configuration variable maps a name to a value. For example,
    214  * "drc.1.attack" is the attack time for the second drc (the "1" is the index of
    215  * the drc instance), and "eq.0.2.freq" is the frequency of the third eq on the
    216  * left channel (the "0" means left channel, and the "2" is the index of the
    217  * eq).
    218  */
    219 var all_configs = {};  /* stores all the configuration variables */
    220 
    221 function init_config() {
    222   set_config('global', 'enable_drc', INIT_GLOBAL_ENABLE_DRC);
    223   set_config('global', 'enable_eq', INIT_GLOBAL_ENABLE_EQ);
    224   set_config('global', 'enable_fft', INIT_GLOBAL_ENABLE_FFT);
    225   set_config('global', 'enable_swap', INIT_GLOBAL_ENABLE_SWAP);
    226   set_config('drc', 0, 'f', 0);
    227   set_config('drc', 1, 'f', INIT_DRC_XO_LOW);
    228   set_config('drc', 2, 'f', INIT_DRC_XO_HIGH);
    229   for (var i = 0; i < 3; i++) {
    230     set_config('drc', i, 'enable', INIT_DRC_ENABLE);
    231     set_config('drc', i, 'threshold', INIT_DRC_THRESHOLD);
    232     set_config('drc', i, 'knee', INIT_DRC_KNEE);
    233     set_config('drc', i, 'ratio', INIT_DRC_RATIO);
    234     set_config('drc', i, 'attack', INIT_DRC_ATTACK);
    235     set_config('drc', i, 'release', INIT_DRC_RELEASE);
    236     set_config('drc', i, 'boost', INIT_DRC_BOOST);
    237   }
    238   for (var i = 0; i <= 1; i++) {
    239     for (var j = 0; j < NEQ; j++) {
    240       set_config('eq', i, j, 'enable', INIT_EQ_ENABLE);
    241       set_config('eq', i, j, 'type', INIT_EQ_TYPE);
    242       set_config('eq', i, j, 'freq', INIT_EQ_FREQ);
    243       set_config('eq', i, j, 'q', INIT_EQ_Q);
    244       set_config('eq', i, j, 'gain', INIT_EQ_GAIN);
    245     }
    246   }
    247 }
    248 
    249 /* Returns a string from the first n elements of a, joined by '.' */
    250 function make_name(a, n) {
    251   var sub = [];
    252   for (var i = 0; i < n; i++) {
    253     sub.push(a[i].toString());
    254   }
    255   return sub.join('.');
    256 }
    257 
    258 function get_config() {
    259   var name = make_name(arguments, arguments.length);
    260   return all_configs[name];
    261 }
    262 
    263 function set_config() {
    264   var n = arguments.length;
    265   var name = make_name(arguments, n - 1);
    266   all_configs[name] = arguments[n - 1];
    267 }
    268 
    269 /* Convenience function */
    270 function get_global(name) {
    271   return get_config('global', name);
    272 }
    273 
    274 /* set_config and apply it to the audio graph and ui. */
    275 function use_config() {
    276   var n = arguments.length;
    277   var name = make_name(arguments, n - 1);
    278   all_configs[name] = arguments[n - 1];
    279   if (audio_graph) {
    280     audio_graph.config(name.split('.'), all_configs[name]);
    281   }
    282   if (audio_ui) {
    283     audio_ui.config(name.split('.'), all_configs[name]);
    284   }
    285 }
    286 
    287 /* re-apply all the configs to audio graph and ui. */
    288 function apply_all_configs() {
    289   for (var name in all_configs) {
    290     if (audio_graph) {
    291       audio_graph.config(name.split('.'), all_configs[name]);
    292     }
    293     if (audio_ui) {
    294       audio_ui.config(name.split('.'), all_configs[name]);
    295     }
    296   }
    297 }
    298 
    299 /* Returns a zero-padded two digits number, for time formatting. */
    300 function two(n) {
    301   var s = '00' + n;
    302   return s.slice(-2);
    303 }
    304 
    305 /* Returns a time string, used for save file name */
    306 function time_str() {
    307   var d = new Date();
    308   var date = two(d.getDate());
    309   var month = two(d.getMonth() + 1);
    310   var hour = two(d.getHours());
    311   var minutes = two(d.getMinutes());
    312   return month + date + '-' + hour + minutes;
    313 }
    314 
    315 /* Downloads the current config to a file. */
    316 function save_config() {
    317   set_config('drc', 'emphasis_disabled',
    318              browser_emphasis_disabled_detection_result);
    319   var a = document.getElementById('save_config_anchor');
    320   var content = JSON.stringify(all_configs, undefined, 2);
    321   var uriContent = 'data:application/octet-stream,' +
    322       encodeURIComponent(content);
    323   a.href = uriContent;
    324   a.download = 'audio-' + time_str() + '.conf';
    325   a.click();
    326 }
    327 
    328 /* Loads a config file. */
    329 function load_config() {
    330   document.getElementById('config_file').click();
    331 }
    332 
    333 function config_file_changed() {
    334   var input = document.getElementById('config_file');
    335   var file = input.files[0];
    336   var reader = new FileReader();
    337   function onloadend() {
    338     var configs = JSON.parse(reader.result);
    339     init_config();
    340     for (var name in configs) {
    341       all_configs[name] = configs[name];
    342     }
    343     build_graph();
    344   }
    345   reader.onloadend = onloadend;
    346   reader.readAsText(file);
    347   input.value = '';
    348 }
    349 
    350 /* ============================ Audio components ============================ */
    351 
    352 /* We wrap Web Audio nodes into our own components. Each component has following
    353  * methods:
    354  *
    355  * function input(n) - Returns a list of pins which are the n-th input of the
    356  * component.
    357  *
    358  * function output(n) - Returns a list of pins which are the n-th output of the
    359  * component.
    360  *
    361  * function config(name, value) - Changes the configuration variable for the
    362  * component.
    363  *
    364  * Each "pin" is just one input/output of a Web Audio node.
    365  */
    366 
    367 /* Returns the top-level audio component */
    368 function graph() {
    369   var stages = [];
    370   var drcs, eqs, ffts;
    371   if (get_global('enable_drc')) {
    372     drcs = new drc_3band();
    373   }
    374   if (get_global('enable_eq')) {
    375     eqs = new eq_2chan();
    376   }
    377   if (get_global('enable_swap')) {
    378     if (eqs) stages.push(eqs);
    379     if (drcs) stages.push(drcs);
    380   } else {
    381     if (drcs) stages.push(drcs);
    382     if (eqs) stages.push(eqs);
    383   }
    384   if (get_global('enable_fft')) {
    385     ffts = new fft_2chan();
    386     stages.push(ffts);
    387   }
    388 
    389   for (var i = 1; i < stages.length; i++) {
    390     connect(stages[i - 1], stages[i]);
    391   }
    392 
    393   function input(n) {
    394     return stages[0].input(0);
    395   }
    396 
    397   function output(n) {
    398     return stages[stages.length - 1].output(0);
    399   }
    400 
    401   function config(name, value) {
    402     var p = name[0];
    403     var s = name.slice(1);
    404     if (p == 'global') {
    405       /* do nothing */
    406     } else if (p == 'drc') {
    407       if (drcs) {
    408         drcs.config(s, value);
    409       }
    410     } else if (p == 'eq') {
    411       if (eqs) {
    412         eqs.config(s, value);
    413       }
    414     } else {
    415       console.log('invalid parameter: name =', name, 'value =', value);
    416     }
    417   }
    418 
    419   this.input = input;
    420   this.output = output;
    421   this.config = config;
    422 }
    423 
    424 /* Returns the fft component for two channels */
    425 function fft_2chan() {
    426   var splitter = audioContext.createChannelSplitter(2);
    427   var merger = audioContext.createChannelMerger(2);
    428 
    429   analyzer_left = audioContext.createAnalyser();
    430   analyzer_right = audioContext.createAnalyser();
    431   analyzer_left.fftSize = FFT_SIZE;
    432   analyzer_right.fftSize = FFT_SIZE;
    433 
    434   splitter.connect(analyzer_left, 0, 0);
    435   splitter.connect(analyzer_right, 1, 0);
    436   analyzer_left.connect(merger, 0, 0);
    437   analyzer_right.connect(merger, 0, 1);
    438 
    439   function input(n) {
    440     return [pin(splitter)];
    441   }
    442 
    443   function output(n) {
    444     return [pin(merger)];
    445   }
    446 
    447   this.input = input;
    448   this.output = output;
    449 }
    450 
    451 /* Returns eq for two channels */
    452 function eq_2chan() {
    453   var eqcs = [new eq_channel(0), new eq_channel(1)];
    454   var splitter = audioContext.createChannelSplitter(2);
    455   var merger = audioContext.createChannelMerger(2);
    456 
    457   connect_from_native(pin(splitter, 0), eqcs[0]);
    458   connect_from_native(pin(splitter, 1), eqcs[1]);
    459   connect_to_native(eqcs[0], pin(merger, 0));
    460   connect_to_native(eqcs[1], pin(merger, 1));
    461 
    462   function input(n) {
    463     return [pin(splitter)];
    464   }
    465 
    466   function output(n) {
    467     return [pin(merger)];
    468   }
    469 
    470   function config(name, value) {
    471     var p = parseInt(name[0]);
    472     var s = name.slice(1);
    473     eqcs[p].config(s, value);
    474   }
    475 
    476   this.input = input;
    477   this.output = output;
    478   this.config = config;
    479 }
    480 
    481 /* Returns eq for one channel (left or right). It contains a series of eq
    482  * filters.  */
    483 function eq_channel(channel) {
    484   var eqs = [];
    485   var first = new delay(0);
    486   var last = first;
    487   for (var i = 0; i < NEQ; i++) {
    488     eqs.push(new eq());
    489     if (get_config('eq', channel, i, 'enable')) {
    490       connect(last, eqs[i]);
    491       last = eqs[i];
    492     }
    493   }
    494 
    495   function input(n) {
    496     return first.input(0);
    497   }
    498 
    499   function output(n) {
    500     return last.output(0);
    501   }
    502 
    503   function config(name, value) {
    504     var p = parseInt(name[0]);
    505     var s = name.slice(1);
    506     eqs[p].config(s, value);
    507   }
    508 
    509   this.input = input;
    510   this.output = output;
    511   this.config = config;
    512 }
    513 
    514 /* Returns a delay component (output = input with n seconds delay) */
    515 function delay(n) {
    516   var delay = audioContext.createDelay();
    517   delay.delayTime.value = n;
    518 
    519   function input(n) {
    520     return [pin(delay)];
    521   }
    522 
    523   function output(n) {
    524     return [pin(delay)];
    525   }
    526 
    527   function config(name, value) {
    528     console.log('invalid parameter: name =', name, 'value =', value);
    529   }
    530 
    531   this.input = input;
    532   this.output = output;
    533   this.config = config;
    534 }
    535 
    536 /* Returns an eq filter */
    537 function eq() {
    538   var filter = audioContext.createBiquadFilter();
    539   filter.type = INIT_EQ_TYPE;
    540   filter.frequency.value = INIT_EQ_FREQ;
    541   filter.Q.value = INIT_EQ_Q;
    542   filter.gain.value = INIT_EQ_GAIN;
    543 
    544   function input(n) {
    545     return [pin(filter)];
    546   }
    547 
    548   function output(n) {
    549     return [pin(filter)];
    550   }
    551 
    552   function config(name, value) {
    553     switch (name[0]) {
    554     case 'type':
    555       filter.type = value;
    556       break;
    557     case 'freq':
    558       filter.frequency.value = parseFloat(value);
    559       break;
    560     case 'q':
    561       value = parseFloat(value);
    562       if (filter.type == 'lowpass' || filter.type == 'highpass')
    563         value = make_biquad_q(value);
    564       filter.Q.value = value;
    565       break;
    566     case 'gain':
    567       filter.gain.value = parseFloat(value);
    568       break;
    569     case 'enable':
    570       break;
    571     default:
    572       console.log('invalid parameter: name =', name, 'value =', value);
    573     }
    574   }
    575 
    576   this.input = input;
    577   this.output = output;
    578   this.config = config;
    579 }
    580 
    581 /* Returns DRC for 3 bands */
    582 function drc_3band() {
    583   var xo = new xo3();
    584   var drcs = [new drc(), new drc(), new drc()];
    585 
    586   var out = [];
    587   for (var i = 0; i < 3; i++) {
    588     if (get_config('drc', i, 'enable')) {
    589       connect(xo, drcs[i], i);
    590       out = out.concat(drcs[i].output());
    591     } else {
    592       /* The DynamicsCompressorNode in Chrome has 6ms pre-delay buffer. So for
    593        * other bands we need to delay for the same amount of time.
    594        */
    595       var d = new delay(0.006);
    596       connect(xo, d, i);
    597       out = out.concat(d.output());
    598     }
    599   }
    600 
    601   function input(n) {
    602     return xo.input(0);
    603   }
    604 
    605   function output(n) {
    606     return out;
    607   }
    608 
    609   function config(name, value) {
    610     if (name[1] == 'f') {
    611       xo.config(name, value);
    612     } else if (name[0] != 'emphasis_disabled') {
    613       var n = parseInt(name[0]);
    614       drcs[n].config(name.slice(1), value);
    615     }
    616   }
    617 
    618   this.input = input;
    619   this.output = output;
    620   this.config = config;
    621 }
    622 
    623 
    624 /* This snippet came from LayoutTests/webaudio/dynamicscompressor-simple.html in
    625  * https://codereview.chromium.org/152333003/. It can determine if
    626  * emphasis/deemphasis is disabled in the browser. Then it sets the value to
    627  * drc.emphasis_disabled in the config.*/
    628 function get_emphasis_disabled() {
    629   var context;
    630   var sampleRate = 44100;
    631   var lengthInSeconds = 1;
    632   var renderedData;
    633   // This threshold is experimentally determined. It depends on the the gain
    634   // value of the gain node below and the dynamics compressor.  When the
    635   // DynamicsCompressor had the pre-emphasis filters, the peak value is about
    636   // 0.21.  Without it, the peak is 0.85.
    637   var peakThreshold = 0.85;
    638 
    639   function checkResult(event) {
    640     var renderedBuffer = event.renderedBuffer;
    641     renderedData = renderedBuffer.getChannelData(0);
    642     // Search for a peak in the last part of the data.
    643     var startSample = sampleRate * (lengthInSeconds - .1);
    644     var endSample = renderedData.length;
    645     var k;
    646     var peak = -1;
    647     var emphasis_disabled = 0;
    648 
    649     for (k = startSample; k < endSample; ++k) {
    650       var sample = Math.abs(renderedData[k]);
    651       if (peak < sample)
    652          peak = sample;
    653     }
    654 
    655     if (peak >= peakThreshold) {
    656       console.log("Pre-emphasis effect not applied as expected..");
    657       emphasis_disabled = 1;
    658     } else {
    659       console.log("Pre-emphasis caused output to be decreased to " + peak
    660                  + " (expected >= " + peakThreshold + ")");
    661       emphasis_disabled = 0;
    662     }
    663     browser_emphasis_disabled_detection_result = emphasis_disabled;
    664     /* save_config button will be disabled until we can decide
    665        emphasis_disabled in chrome. */
    666     document.getElementById('save_config').disabled = false;
    667   }
    668 
    669   function runTest() {
    670     context = new OfflineAudioContext(1, sampleRate * lengthInSeconds,
    671                                       sampleRate);
    672     // Connect an oscillator to a gain node to the compressor.  The
    673     // oscillator frequency is set to a high value for the (original)
    674     // emphasis to kick in. The gain is a little extra boost to get the
    675     // compressor enabled.
    676     //
    677     var osc = context.createOscillator();
    678     osc.frequency.value = 15000;
    679     var gain = context.createGain();
    680     gain.gain.value = 1.5;
    681     var compressor = context.createDynamicsCompressor();
    682     osc.connect(gain);
    683     gain.connect(compressor);
    684     compressor.connect(context.destination);
    685     osc.start();
    686     context.oncomplete = checkResult;
    687     context.startRendering();
    688   }
    689 
    690   runTest();
    691 
    692 }
    693 
    694 /* Returns one DRC filter */
    695 function drc() {
    696   var comp = audioContext.createDynamicsCompressor();
    697 
    698   /* The supported method names are different on browsers with different
    699    * versions.*/
    700   audioContext.createGainNode = (audioContext.createGainNode ||
    701                                  audioContext.createGain);
    702   var boost = audioContext.createGainNode();
    703   comp.threshold.value = INIT_DRC_THRESHOLD;
    704   comp.knee.value = INIT_DRC_KNEE;
    705   comp.ratio.value = INIT_DRC_RATIO;
    706   comp.attack.value = INIT_DRC_ATTACK;
    707   comp.release.value = INIT_DRC_RELEASE;
    708   boost.gain.value = dBToLinear(INIT_DRC_BOOST);
    709 
    710   comp.connect(boost);
    711 
    712   function input(n) {
    713     return [pin(comp)];
    714   }
    715 
    716   function output(n) {
    717     return [pin(boost)];
    718   }
    719 
    720   function config(name, value) {
    721     var p = name[0];
    722     switch (p) {
    723     case 'threshold':
    724     case 'knee':
    725     case 'ratio':
    726     case 'attack':
    727     case 'release':
    728       comp[p].value = parseFloat(value);
    729       break;
    730     case 'boost':
    731       boost.gain.value = dBToLinear(parseFloat(value));
    732       break;
    733     case 'enable':
    734       break;
    735     default:
    736       console.log('invalid parameter: name =', name, 'value =', value);
    737     }
    738   }
    739 
    740   this.input = input;
    741   this.output = output;
    742   this.config = config;
    743 }
    744 
    745 /* Crossover filter
    746  *
    747  * INPUT --+-- lp1 --+-- lp2a --+-- LOW (0)
    748  *         |         |          |
    749  *         |         \-- hp2a --/
    750  *         |
    751  *         \-- hp1 --+-- lp2 ------ MID (1)
    752  *                   |
    753  *                   \-- hp2 ------ HIGH (2)
    754  *
    755  *            [f1]       [f2]
    756  */
    757 
    758 /* Returns a crossover component which splits input into 3 bands */
    759 function xo3() {
    760   var f1 = INIT_DRC_XO_LOW;
    761   var f2 = INIT_DRC_XO_HIGH;
    762 
    763   var lp1 = lr4_lowpass(f1);
    764   var hp1 = lr4_highpass(f1);
    765   var lp2 = lr4_lowpass(f2);
    766   var hp2 = lr4_highpass(f2);
    767   var lp2a = lr4_lowpass(f2);
    768   var hp2a = lr4_highpass(f2);
    769 
    770   connect(lp1, lp2a);
    771   connect(lp1, hp2a);
    772   connect(hp1, lp2);
    773   connect(hp1, hp2);
    774 
    775   function input(n) {
    776     return lp1.input().concat(hp1.input());
    777   }
    778 
    779   function output(n) {
    780     switch (n) {
    781     case 0:
    782       return lp2a.output().concat(hp2a.output());
    783     case 1:
    784       return lp2.output();
    785     case 2:
    786       return hp2.output();
    787     default:
    788       console.log('invalid index ' + n);
    789       return [];
    790     }
    791   }
    792 
    793   function config(name, value) {
    794     var p = name[0];
    795     var s = name.slice(1);
    796     if (p == '0') {
    797       /* Ignore. The lower frequency of the low band is always 0. */
    798     } else if (p == '1') {
    799       lp1.config(s, value);
    800       hp1.config(s, value);
    801     } else if (p == '2') {
    802       lp2.config(s, value);
    803       hp2.config(s, value);
    804       lp2a.config(s, value);
    805       hp2a.config(s, value);
    806     } else {
    807       console.log('invalid parameter: name =', name, 'value =', value);
    808     }
    809   }
    810 
    811   this.output = output;
    812   this.input = input;
    813   this.config = config;
    814 }
    815 
    816 /* Connects two components: the n-th output of c1 and the m-th input of c2. */
    817 function connect(c1, c2, n, m) {
    818   n = n || 0; /* default is the first output */
    819   m = m || 0; /* default is the first input */
    820   outs = c1.output(n);
    821   ins = c2.input(m);
    822 
    823   for (var i = 0; i < outs.length; i++) {
    824     for (var j = 0; j < ins.length; j++) {
    825       var from = outs[i];
    826       var to = ins[j];
    827       from.node.connect(to.node, from.index, to.index);
    828     }
    829   }
    830 }
    831 
    832 /* Connects from pin "from" to the n-th input of component c2 */
    833 function connect_from_native(from, c2, n) {
    834   n = n || 0;  /* default is the first input */
    835   ins = c2.input(n);
    836   for (var i = 0; i < ins.length; i++) {
    837     var to = ins[i];
    838     from.node.connect(to.node, from.index, to.index);
    839   }
    840 }
    841 
    842 /* Connects from m-th output of component c1 to pin "to" */
    843 function connect_to_native(c1, to, m) {
    844   m = m || 0;  /* default is the first output */
    845   outs = c1.output(m);
    846   for (var i = 0; i < outs.length; i++) {
    847     var from = outs[i];
    848     from.node.connect(to.node, from.index, to.index);
    849   }
    850 }
    851 
    852 /* Returns a LR4 lowpass component */
    853 function lr4_lowpass(freq) {
    854   return new double(freq, create_lowpass);
    855 }
    856 
    857 /* Returns a LR4 highpass component */
    858 function lr4_highpass(freq) {
    859   return new double(freq, create_highpass);
    860 }
    861 
    862 /* Returns a component by apply the same filter twice. */
    863 function double(freq, creator) {
    864   var f1 = creator(freq);
    865   var f2 = creator(freq);
    866   f1.connect(f2);
    867 
    868   function input(n) {
    869     return [pin(f1)];
    870   }
    871 
    872   function output(n) {
    873     return [pin(f2)];
    874   }
    875 
    876   function config(name, value) {
    877     if (name[0] == 'f') {
    878       f1.frequency.value = parseFloat(value);
    879       f2.frequency.value = parseFloat(value);
    880     } else {
    881       console.log('invalid parameter: name =', name, 'value =', value);
    882     }
    883   }
    884 
    885   this.input = input;
    886   this.output = output;
    887   this.config = config;
    888 }
    889 
    890 /* Returns a lowpass filter */
    891 function create_lowpass(freq) {
    892   var lp = audioContext.createBiquadFilter();
    893   lp.type = 'lowpass';
    894   lp.frequency.value = freq;
    895   lp.Q.value = make_biquad_q(0);
    896   return lp;
    897 }
    898 
    899 /* Returns a highpass filter */
    900 function create_highpass(freq) {
    901   var hp = audioContext.createBiquadFilter();
    902   hp.type = 'highpass';
    903   hp.frequency.value = freq;
    904   hp.Q.value = make_biquad_q(0);
    905   return hp;
    906 }
    907 
    908 /* A pin specifies one of the input/output of a Web Audio node */
    909 function pin(node, index) {
    910   var p = new Pin();
    911   p.node = node;
    912   p.index = index || 0;
    913   return p;
    914 }
    915 
    916 function Pin(node, index) {
    917 }
    918 
    919 /* ============================ Event Handlers ============================ */
    920 
    921 function audio_source_select(select) {
    922   var index = select.selectedIndex;
    923   var url = document.getElementById('audio_source_url');
    924   url.value = select.options[index].value;
    925   url.blur();
    926   audio_source_set(url.value);
    927 }
    928 
    929 /* Loads a local audio file. */
    930 function load_audio() {
    931   document.getElementById('audio_file').click();
    932 }
    933 
    934 function audio_file_changed() {
    935   var input = document.getElementById('audio_file');
    936   var file = input.files[0];
    937   var file_url = window.webkitURL.createObjectURL(file);
    938   input.value = '';
    939 
    940   var url = document.getElementById('audio_source_url');
    941   url.value = file.name;
    942 
    943   audio_source_set(file_url);
    944 }
    945 
    946 function audio_source_set(url) {
    947   var player = document.getElementById('audio_player');
    948   var container = document.getElementById('audio_player_container');
    949   var loading = document.getElementById('audio_loading');
    950   loading.style.visibility = 'visible';
    951 
    952   /* Re-create an audio element when the audio source URL is changed. */
    953   player.pause();
    954   container.removeChild(player);
    955   player = document.createElement('audio');
    956   player.crossOrigin = 'anonymous';
    957   player.id = 'audio_player';
    958   player.loop = true;
    959   player.controls = true;
    960   player.addEventListener('canplay', audio_source_canplay);
    961   container.appendChild(player);
    962   update_source_node(player);
    963 
    964   player.src = url;
    965   player.load();
    966 }
    967 
    968 function audio_source_canplay() {
    969   var player = document.getElementById('audio_player');
    970   var loading = document.getElementById('audio_loading');
    971   loading.style.visibility = 'hidden';
    972   player.play();
    973 }
    974 
    975 function update_source_node(mediaElement) {
    976   sourceNode = audioContext.createMediaElementSource(mediaElement);
    977   build_graph();
    978 }
    979 
    980 function toggle_global_checkbox(name, enable) {
    981   use_config('global', name, enable);
    982   build_graph();
    983 }
    984 
    985 function toggle_one_drc(index, enable) {
    986   use_config('drc', index, 'enable', enable);
    987   build_graph();
    988 }
    989 
    990 function toggle_one_eq(channel, index, enable) {
    991   use_config('eq', channel, index, 'enable', enable);
    992   build_graph();
    993 }
    994 
    995 /* ============================== UI widgets ============================== */
    996 
    997 /* Adds a row to the table. The row contains an input box and a slider. */
    998 function slider_input(table, name, initial_value, min_value, max_value, step,
    999                       suffix, handler) {
   1000   function id(x) {
   1001     return x;
   1002   }
   1003 
   1004   return new slider_input_common(table, name, initial_value, min_value,
   1005                                  max_value, step, suffix, handler, id, id);
   1006 }
   1007 
   1008 /* This is similar to slider_input, but uses log scale for the slider. */
   1009 function slider_input_log(table, name, initial_value, min_value, max_value,
   1010                           suffix, precision, handler, mapping,
   1011                           inverse_mapping) {
   1012   function mapping(x) {
   1013     return Math.log(x + 1);
   1014   }
   1015 
   1016   function inv_mapping(x) {
   1017     return (Math.exp(x) - 1).toFixed(precision);
   1018   }
   1019 
   1020   return new slider_input_common(table, name, initial_value, min_value,
   1021                                  max_value, 1e-6, suffix, handler, mapping,
   1022                                  inv_mapping);
   1023 }
   1024 
   1025 /* The common implementation of linear and log-scale sliders. Each slider has
   1026  * the following methods:
   1027  *
   1028  * function update(v) - update the slider (and the text box) to the value v.
   1029  *
   1030  * function hide(h) - hide/unhide the slider.
   1031  */
   1032 function slider_input_common(table, name, initial_value, min_value, max_value,
   1033                              step, suffix, handler, mapping, inv_mapping) {
   1034   var row = table.insertRow(-1);
   1035   var col_name = row.insertCell(-1);
   1036   var col_box = row.insertCell(-1);
   1037   var col_slider = row.insertCell(-1);
   1038 
   1039   var name_span = document.createElement('span');
   1040   name_span.appendChild(document.createTextNode(name));
   1041   col_name.appendChild(name_span);
   1042 
   1043   var box = document.createElement('input');
   1044   box.defaultValue = initial_value;
   1045   box.type = 'text';
   1046   box.size = 5;
   1047   box.className = 'nbox';
   1048   col_box.appendChild(box);
   1049   var suffix_span = document.createElement('span');
   1050   suffix_span.appendChild(document.createTextNode(suffix));
   1051   col_box.appendChild(suffix_span);
   1052 
   1053   var slider = document.createElement('input');
   1054   slider.defaultValue = Math.log(initial_value);
   1055   slider.type = 'range';
   1056   slider.className = 'nslider';
   1057   slider.min = mapping(min_value);
   1058   slider.max = mapping(max_value);
   1059   slider.step = step;
   1060   col_slider.appendChild(slider);
   1061 
   1062   box.onchange = function() {
   1063     slider.value = mapping(box.value);
   1064     handler(parseFloat(box.value));
   1065   };
   1066 
   1067   slider.onchange = function() {
   1068     box.value = inv_mapping(slider.value);
   1069     handler(parseFloat(box.value));
   1070   };
   1071 
   1072   function update(v) {
   1073     box.value = v;
   1074     slider.value = mapping(v);
   1075   }
   1076 
   1077   function hide(h) {
   1078     var v = h ? 'hidden' : 'visible';
   1079     name_span.style.visibility = v;
   1080     box.style.visibility = v;
   1081     suffix_span.style.visibility = v;
   1082     slider.style.visibility = v;
   1083   }
   1084 
   1085   this.update = update;
   1086   this.hide = hide;
   1087 }
   1088 
   1089 /* Adds a enable/disable checkbox to a div. The method "update" can change the
   1090  * checkbox state. */
   1091 function check_button(div, handler) {
   1092   var check = document.createElement('input');
   1093   check.className = 'enable_check';
   1094   check.type = 'checkbox';
   1095   check.checked = true;
   1096   check.onchange = function() {
   1097     handler(check.checked);
   1098   };
   1099   div.appendChild(check);
   1100 
   1101   function update(v) {
   1102     check.checked = v;
   1103   }
   1104 
   1105   this.update = update;
   1106 }
   1107 
   1108 function dummy() {
   1109 }
   1110 
   1111 /* Changes the opacity of a div. */
   1112 function toggle_card(div, enable) {
   1113   div.style.opacity = enable ? 1 : 0.3;
   1114 }
   1115 
   1116 /* Appends a card of DRC controls and graphs to the specified parent.
   1117  * Args:
   1118  *     parent - The parent element
   1119  *     index - The index of this DRC component (0-2)
   1120  *     lower_freq - The lower frequency of this DRC component
   1121  *     freq_label - The label for the lower frequency input text box
   1122  */
   1123 function drc_card(parent, index, lower_freq, freq_label) {
   1124   var top = document.createElement('div');
   1125   top.className = 'drc_data';
   1126   parent.appendChild(top);
   1127   function toggle_drc_card(enable) {
   1128     toggle_card(div, enable);
   1129     toggle_one_drc(index, enable);
   1130   }
   1131   var enable_button = new check_button(top, toggle_drc_card);
   1132 
   1133   var div = document.createElement('div');
   1134   top.appendChild(div);
   1135 
   1136   /* Canvas */
   1137   var p = document.createElement('p');
   1138   div.appendChild(p);
   1139 
   1140   var canvas = document.createElement('canvas');
   1141   canvas.className = 'drc_curve';
   1142   p.appendChild(canvas);
   1143 
   1144   canvas.width = 240;
   1145   canvas.height = 180;
   1146   var dd = new DrcDrawer(canvas);
   1147   dd.init();
   1148 
   1149   /* Parameters */
   1150   var table = document.createElement('table');
   1151   div.appendChild(table);
   1152 
   1153   function change_lower_freq(v) {
   1154     use_config('drc', index, 'f', v);
   1155   }
   1156 
   1157   function change_threshold(v) {
   1158     dd.update_threshold(v);
   1159     use_config('drc', index, 'threshold', v);
   1160   }
   1161 
   1162   function change_knee(v) {
   1163     dd.update_knee(v);
   1164     use_config('drc', index, 'knee', v);
   1165   }
   1166 
   1167   function change_ratio(v) {
   1168     dd.update_ratio(v);
   1169     use_config('drc', index, 'ratio', v);
   1170   }
   1171 
   1172   function change_boost(v) {
   1173     dd.update_boost(v);
   1174     use_config('drc', index, 'boost', v);
   1175   }
   1176 
   1177   function change_attack(v) {
   1178     use_config('drc', index, 'attack', v);
   1179   }
   1180 
   1181   function change_release(v) {
   1182     use_config('drc', index, 'release', v);
   1183   }
   1184 
   1185   var f_slider;
   1186   if (lower_freq == 0) {  /* Special case for the lowest band */
   1187     f_slider = new slider_input_log(table, freq_label, lower_freq, 0, 1,
   1188                                     'Hz', 0, dummy);
   1189     f_slider.hide(true);
   1190   } else {
   1191     f_slider = new slider_input_log(table, freq_label, lower_freq, 1,
   1192                                     nyquist, 'Hz', 0, change_lower_freq);
   1193   }
   1194 
   1195   var sliders = {
   1196     'f': f_slider,
   1197     'threshold': new slider_input(table, 'Threshold', INIT_DRC_THRESHOLD,
   1198                                   -100, 0, 1, 'dB', change_threshold),
   1199     'knee': new slider_input(table, 'Knee', INIT_DRC_KNEE, 0, 40, 1, 'dB',
   1200                              change_knee),
   1201     'ratio': new slider_input(table, 'Ratio', INIT_DRC_RATIO, 1, 20, 0.001,
   1202                               '', change_ratio),
   1203     'boost': new slider_input(table, 'Boost', 0, -40, 40, 1, 'dB',
   1204                               change_boost),
   1205     'attack': new slider_input(table, 'Attack', INIT_DRC_ATTACK, 0.001,
   1206                                1, 0.001, 's', change_attack),
   1207     'release': new slider_input(table, 'Release', INIT_DRC_RELEASE,
   1208                                 0.001, 1, 0.001, 's', change_release)
   1209   };
   1210 
   1211   function config(name, value) {
   1212     var p = name[0];
   1213     var fv = parseFloat(value);
   1214     switch (p) {
   1215     case 'f':
   1216     case 'threshold':
   1217     case 'knee':
   1218     case 'ratio':
   1219     case 'boost':
   1220     case 'attack':
   1221     case 'release':
   1222       sliders[p].update(fv);
   1223       break;
   1224     case 'enable':
   1225       toggle_card(div, value);
   1226       enable_button.update(value);
   1227       break;
   1228     default:
   1229       console.log('invalid parameter: name =', name, 'value =', value);
   1230     }
   1231 
   1232     switch (p) {
   1233     case 'threshold':
   1234       dd.update_threshold(fv);
   1235       break;
   1236     case 'knee':
   1237       dd.update_knee(fv);
   1238       break;
   1239     case 'ratio':
   1240       dd.update_ratio(fv);
   1241       break;
   1242     case 'boost':
   1243       dd.update_boost(fv);
   1244       break;
   1245     }
   1246   }
   1247 
   1248   this.config = config;
   1249 }
   1250 
   1251 /* Appends a menu of biquad types to the specified table. */
   1252 function biquad_type_select(table, handler) {
   1253   var row = table.insertRow(-1);
   1254   var col_name = row.insertCell(-1);
   1255   var col_menu = row.insertCell(-1);
   1256 
   1257   col_name.appendChild(document.createTextNode('Type'));
   1258 
   1259   var select = document.createElement('select');
   1260   select.className = 'biquad_type_select';
   1261   var options = [
   1262     'lowpass',
   1263     'highpass',
   1264     'bandpass',
   1265     'lowshelf',
   1266     'highshelf',
   1267     'peaking',
   1268     'notch'
   1269     /* no need: 'allpass' */
   1270   ];
   1271 
   1272   for (var i = 0; i < options.length; i++) {
   1273     var o = document.createElement('option');
   1274     o.appendChild(document.createTextNode(options[i]));
   1275     select.appendChild(o);
   1276   }
   1277 
   1278   select.value = INIT_EQ_TYPE;
   1279   col_menu.appendChild(select);
   1280 
   1281   function onchange() {
   1282     handler(select.value);
   1283   }
   1284   select.onchange = onchange;
   1285 
   1286   function update(v) {
   1287     select.value = v;
   1288   }
   1289 
   1290   this.update = update;
   1291 }
   1292 
   1293 /* Appends a card of EQ controls to the specified parent.
   1294  * Args:
   1295  *     parent - The parent element
   1296  *     channel - The index of the channel this EQ component is on (0-1)
   1297  *     index - The index of this EQ on this channel (0-7)
   1298  *     ed - The EQ curve drawer. We will notify the drawer to redraw if the
   1299  *         parameters for this EQ changes.
   1300  */
   1301 function eq_card(parent, channel, index, ed) {
   1302   var top = document.createElement('div');
   1303   top.className = 'eq_data';
   1304   parent.appendChild(top);
   1305   function toggle_eq_card(enable) {
   1306     toggle_card(table, enable);
   1307     toggle_one_eq(channel, index, enable);
   1308     ed.update_enable(index, enable);
   1309   }
   1310   var enable_button = new check_button(top, toggle_eq_card);
   1311 
   1312   var table = document.createElement('table');
   1313   table.className = 'eq_table';
   1314   top.appendChild(table);
   1315 
   1316   function change_type(v) {
   1317     ed.update_type(index, v);
   1318     hide_unused_slider(v);
   1319     use_config('eq', channel, index, 'type', v);
   1320     /* Special case: automatically set Q to 0 for lowpass/highpass filters. */
   1321     if (v == 'lowpass' || v == 'highpass') {
   1322       use_config('eq', channel, index, 'q', 0);
   1323     }
   1324   }
   1325 
   1326   function change_freq(v)
   1327   {
   1328     ed.update_freq(index, v);
   1329     use_config('eq', channel, index, 'freq', v);
   1330   }
   1331 
   1332   function change_q(v)
   1333   {
   1334     ed.update_q(index, v);
   1335     use_config('eq', channel, index, 'q', v);
   1336   }
   1337 
   1338   function change_gain(v)
   1339   {
   1340     ed.update_gain(index, v);
   1341     use_config('eq', channel, index, 'gain', v);
   1342   }
   1343 
   1344   var type_select = new biquad_type_select(table, change_type);
   1345 
   1346   var sliders = {
   1347     'freq': new slider_input_log(table, 'Frequency', INIT_EQ_FREQ, 1,
   1348                                  nyquist, 'Hz', 0, change_freq),
   1349     'q': new slider_input_log(table, 'Q', INIT_EQ_Q, 0, 1000, '', 4,
   1350                               change_q),
   1351     'gain': new slider_input(table, 'Gain', INIT_EQ_GAIN, -40, 40, 0.1,
   1352                              'dB', change_gain)
   1353   };
   1354 
   1355   var unused = {
   1356     'lowpass': [0, 0, 1],
   1357     'highpass': [0, 0, 1],
   1358     'bandpass': [0, 0, 1],
   1359     'lowshelf': [0, 1, 0],
   1360     'highshelf': [0, 1, 0],
   1361     'peaking': [0, 0, 0],
   1362     'notch': [0, 0, 1],
   1363     'allpass': [0, 0, 1]
   1364   };
   1365   function hide_unused_slider(type) {
   1366     var u = unused[type];
   1367     sliders['freq'].hide(u[0]);
   1368     sliders['q'].hide(u[1]);
   1369     sliders['gain'].hide(u[2]);
   1370   }
   1371 
   1372   function config(name, value) {
   1373     var p = name[0];
   1374     var fv = parseFloat(value);
   1375     switch (p) {
   1376     case 'type':
   1377       type_select.update(value);
   1378       break;
   1379     case 'freq':
   1380     case 'q':
   1381     case 'gain':
   1382       sliders[p].update(fv);
   1383       break;
   1384     case 'enable':
   1385       toggle_card(table, value);
   1386       enable_button.update(value);
   1387       break;
   1388     default:
   1389       console.log('invalid parameter: name =', name, 'value =', value);
   1390     }
   1391 
   1392     switch (p) {
   1393     case 'type':
   1394       ed.update_type(index, value);
   1395       hide_unused_slider(value);
   1396       break;
   1397     case 'freq':
   1398       ed.update_freq(index, fv);
   1399       break;
   1400     case 'q':
   1401       ed.update_q(index, fv);
   1402       break;
   1403     case 'gain':
   1404       ed.update_gain(index, fv);
   1405       break;
   1406     }
   1407   }
   1408 
   1409   this.config = config;
   1410 }
   1411 
   1412 /* Appends the EQ UI for one channel to the specified parent */
   1413 function eq_section(parent, channel) {
   1414   /* Two canvas, one for eq curve, another for fft. */
   1415   var p = document.createElement('p');
   1416   p.className = 'eq_curve_parent';
   1417 
   1418   var canvas_eq = document.createElement('canvas');
   1419   canvas_eq.className = 'eq_curve';
   1420   canvas_eq.width = 960;
   1421   canvas_eq.height = 270;
   1422 
   1423   p.appendChild(canvas_eq);
   1424   var ed = new EqDrawer(canvas_eq, channel);
   1425   ed.init();
   1426 
   1427   var canvas_fft = document.createElement('canvas');
   1428   canvas_fft.className = 'eq_curve';
   1429   canvas_fft.width = 960;
   1430   canvas_fft.height = 270;
   1431 
   1432   p.appendChild(canvas_fft);
   1433   var fd = new FFTDrawer(canvas_fft, channel);
   1434   fd.init();
   1435 
   1436   parent.appendChild(p);
   1437 
   1438   /* Eq cards */
   1439   var eq = {};
   1440   for (var i = 0; i < NEQ; i++) {
   1441     eq[i] = new eq_card(parent, channel, i, ed);
   1442   }
   1443 
   1444   function config(name, value) {
   1445     var p = parseInt(name[0]);
   1446     var s = name.slice(1);
   1447     eq[p].config(s, value);
   1448   }
   1449 
   1450   this.config = config;
   1451 }
   1452 
   1453 function global_section(parent) {
   1454   var checkbox_data = [
   1455     /* config name, text label, checkbox object */
   1456     ['enable_drc', 'Enable DRC', null],
   1457     ['enable_eq', 'Enable EQ', null],
   1458     ['enable_fft', 'Show FFT', null],
   1459     ['enable_swap', 'Swap DRC/EQ', null]
   1460   ];
   1461 
   1462   for (var i = 0; i < checkbox_data.length; i++) {
   1463     config_name = checkbox_data[i][0];
   1464     text_label = checkbox_data[i][1];
   1465 
   1466     var cb = document.createElement('input');
   1467     cb.type = 'checkbox';
   1468     cb.checked = get_global(config_name);
   1469     cb.onchange = function(name) {
   1470       return function() { toggle_global_checkbox(name, this.checked); }
   1471     }(config_name);
   1472     checkbox_data[i][2] = cb;
   1473     parent.appendChild(cb);
   1474     parent.appendChild(document.createTextNode(text_label));
   1475   }
   1476 
   1477   function config(name, value) {
   1478     var i;
   1479     for (i = 0; i < checkbox_data.length; i++) {
   1480       if (checkbox_data[i][0] == name[0]) {
   1481         break;
   1482       }
   1483     }
   1484     if (i < checkbox_data.length) {
   1485       checkbox_data[i][2].checked = value;
   1486     } else {
   1487       console.log('invalid parameter: name =', name, 'value =', value);
   1488     }
   1489   }
   1490 
   1491   this.config = config;
   1492 }
   1493 
   1494 window.onload = function() {
   1495   fix_audio_elements();
   1496   check_biquad_filter_q().then(function (flag) {
   1497     console.log('Browser biquad filter uses Audio Cookbook formula:', flag);
   1498     /* Detects if emphasis is disabled and sets
   1499      * browser_emphasis_disabled_detection_result. */
   1500     get_emphasis_disabled();
   1501     init_config();
   1502     init_audio();
   1503     init_ui();
   1504   }).catch(function (reason) {
   1505     alert('Cannot detect browser biquad filter implementation:', reason);
   1506   });
   1507 };
   1508 
   1509 function init_ui() {
   1510   audio_ui = new ui();
   1511 }
   1512 
   1513 /* Top-level UI */
   1514 function ui() {
   1515   var global = new global_section(document.getElementById('global_section'));
   1516   var drc_div = document.getElementById('drc_section');
   1517   var drc_cards = [
   1518     new drc_card(drc_div, 0, 0, ''),
   1519     new drc_card(drc_div, 1, INIT_DRC_XO_LOW, 'Start From'),
   1520     new drc_card(drc_div, 2, INIT_DRC_XO_HIGH, 'Start From')
   1521   ];
   1522 
   1523   var left_div = document.getElementById('eq_left_section');
   1524   var right_div = document.getElementById('eq_right_section');
   1525   var eq_sections = [
   1526     new eq_section(left_div, 0),
   1527     new eq_section(right_div, 1)
   1528   ];
   1529 
   1530   function config(name, value) {
   1531     var p = name[0];
   1532     var i = parseInt(name[1]);
   1533     var s = name.slice(2);
   1534     if (p == 'global') {
   1535       global.config(name.slice(1), value);
   1536     } else if (p == 'drc') {
   1537       if (name[1] == 'emphasis_disabled') {
   1538         return;
   1539       }
   1540       drc_cards[i].config(s, value);
   1541     } else if (p == 'eq') {
   1542       eq_sections[i].config(s, value);
   1543     } else {
   1544       console.log('invalid parameter: name =', name, 'value =', value);
   1545     }
   1546   }
   1547 
   1548   this.config = config;
   1549 }
   1550 
   1551 /* Draws the DRC curve on a canvas. The update*() methods should be called when
   1552  * the parameters change, so the curve can be redrawn. */
   1553 function DrcDrawer(canvas) {
   1554   var canvasContext = canvas.getContext('2d');
   1555 
   1556   var backgroundColor = 'black';
   1557   var curveColor = 'rgb(192,192,192)';
   1558   var gridColor = 'rgb(200,200,200)';
   1559   var textColor = 'rgb(238,221,130)';
   1560   var thresholdColor = 'rgb(255,160,122)';
   1561 
   1562   var dbThreshold = INIT_DRC_THRESHOLD;
   1563   var dbKnee = INIT_DRC_KNEE;
   1564   var ratio = INIT_DRC_RATIO;
   1565   var boost = INIT_DRC_BOOST;
   1566 
   1567   var curve_slope;
   1568   var curve_k;
   1569   var linearThreshold;
   1570   var kneeThresholdDb;
   1571   var kneeThreshold;
   1572   var ykneeThresholdDb;
   1573   var masterLinearGain;
   1574 
   1575   var maxOutputDb = 6;
   1576   var minOutputDb = -36;
   1577 
   1578   function xpixelToDb(x) {
   1579     /* This is right even though it looks like we should scale by width. We
   1580      * want the same pixel/dB scale for both. */
   1581     var k = x / canvas.height;
   1582     var db = minOutputDb + k * (maxOutputDb - minOutputDb);
   1583     return db;
   1584   }
   1585 
   1586   function dBToXPixel(db) {
   1587     var k = (db - minOutputDb) / (maxOutputDb - minOutputDb);
   1588     var x = k * canvas.height;
   1589     return x;
   1590   }
   1591 
   1592   function ypixelToDb(y) {
   1593     var k = y / canvas.height;
   1594     var db = maxOutputDb - k * (maxOutputDb - minOutputDb);
   1595     return db;
   1596   }
   1597 
   1598   function dBToYPixel(db) {
   1599     var k = (maxOutputDb - db) / (maxOutputDb - minOutputDb);
   1600     var y = k * canvas.height;
   1601     return y;
   1602   }
   1603 
   1604   function kneeCurve(x, k) {
   1605     if (x < linearThreshold)
   1606       return x;
   1607 
   1608     return linearThreshold +
   1609         (1 - Math.exp(-k * (x - linearThreshold))) / k;
   1610   }
   1611 
   1612   function saturate(x, k) {
   1613     var y;
   1614     if (x < kneeThreshold) {
   1615       y = kneeCurve(x, k);
   1616     } else {
   1617       var xDb = linearToDb(x);
   1618       var yDb = ykneeThresholdDb + curve_slope * (xDb - kneeThresholdDb);
   1619       y = dBToLinear(yDb);
   1620     }
   1621     return y;
   1622   }
   1623 
   1624   function slopeAt(x, k) {
   1625     if (x < linearThreshold)
   1626       return 1;
   1627     var x2 = x * 1.001;
   1628     var xDb = linearToDb(x);
   1629     var x2Db = linearToDb(x2);
   1630     var yDb = linearToDb(kneeCurve(x, k));
   1631     var y2Db = linearToDb(kneeCurve(x2, k));
   1632     var m = (y2Db - yDb) / (x2Db - xDb);
   1633     return m;
   1634   }
   1635 
   1636   function kAtSlope(desiredSlope) {
   1637     var xDb = dbThreshold + dbKnee;
   1638     var x = dBToLinear(xDb);
   1639 
   1640     var minK = 0.1;
   1641     var maxK = 10000;
   1642     var k = 5;
   1643 
   1644     for (var i = 0; i < 15; i++) {
   1645       var slope = slopeAt(x, k);
   1646       if (slope < desiredSlope) {
   1647         maxK = k;
   1648       } else {
   1649         minK = k;
   1650       }
   1651       k = Math.sqrt(minK * maxK);
   1652     }
   1653     return k;
   1654   }
   1655 
   1656   function drawCurve() {
   1657     /* Update curve parameters */
   1658     linearThreshold = dBToLinear(dbThreshold);
   1659     curve_slope = 1 / ratio;
   1660     curve_k = kAtSlope(1 / ratio);
   1661     kneeThresholdDb = dbThreshold + dbKnee;
   1662     kneeThreshold = dBToLinear(kneeThresholdDb);
   1663     ykneeThresholdDb = linearToDb(kneeCurve(kneeThreshold, curve_k));
   1664 
   1665     /* Calculate masterLinearGain */
   1666     var fullRangeGain = saturate(1, curve_k);
   1667     var fullRangeMakeupGain = Math.pow(1 / fullRangeGain, 0.6);
   1668     masterLinearGain = dBToLinear(boost) * fullRangeMakeupGain;
   1669 
   1670     /* Clear canvas */
   1671     var width = canvas.width;
   1672     var height = canvas.height;
   1673     canvasContext.fillStyle = backgroundColor;
   1674     canvasContext.fillRect(0, 0, width, height);
   1675 
   1676     /* Draw linear response for reference. */
   1677     canvasContext.strokeStyle = gridColor;
   1678     canvasContext.lineWidth = 1;
   1679     canvasContext.beginPath();
   1680     canvasContext.moveTo(dBToXPixel(minOutputDb), dBToYPixel(minOutputDb));
   1681     canvasContext.lineTo(dBToXPixel(maxOutputDb), dBToYPixel(maxOutputDb));
   1682     canvasContext.stroke();
   1683 
   1684     /* Draw 0dBFS output levels from 0dBFS down to -36dBFS */
   1685     for (var dbFS = 0; dbFS >= -36; dbFS -= 6) {
   1686       canvasContext.beginPath();
   1687 
   1688       var y = dBToYPixel(dbFS);
   1689       canvasContext.setLineDash([1, 4]);
   1690       canvasContext.moveTo(0, y);
   1691       canvasContext.lineTo(width, y);
   1692       canvasContext.stroke();
   1693       canvasContext.setLineDash([]);
   1694 
   1695       canvasContext.textAlign = 'center';
   1696       canvasContext.strokeStyle = textColor;
   1697       canvasContext.strokeText(dbFS.toFixed(0) + ' dB', 15, y - 2);
   1698       canvasContext.strokeStyle = gridColor;
   1699     }
   1700 
   1701     /* Draw 0dBFS input line */
   1702     canvasContext.beginPath();
   1703     canvasContext.moveTo(dBToXPixel(0), 0);
   1704     canvasContext.lineTo(dBToXPixel(0), height);
   1705     canvasContext.stroke();
   1706     canvasContext.strokeText('0dB', dBToXPixel(0), height);
   1707 
   1708     /* Draw threshold input line */
   1709     canvasContext.beginPath();
   1710     canvasContext.moveTo(dBToXPixel(dbThreshold), 0);
   1711     canvasContext.lineTo(dBToXPixel(dbThreshold), height);
   1712     canvasContext.moveTo(dBToXPixel(kneeThresholdDb), 0);
   1713     canvasContext.lineTo(dBToXPixel(kneeThresholdDb), height);
   1714     canvasContext.strokeStyle = thresholdColor;
   1715     canvasContext.stroke();
   1716 
   1717     /* Draw the compressor curve */
   1718     canvasContext.strokeStyle = curveColor;
   1719     canvasContext.lineWidth = 3;
   1720 
   1721     canvasContext.beginPath();
   1722     var pixelsPerDb = (0.5 * height) / 40.0;
   1723 
   1724     for (var x = 0; x < width; ++x) {
   1725       var inputDb = xpixelToDb(x);
   1726       var inputLinear = dBToLinear(inputDb);
   1727       var outputLinear = saturate(inputLinear, curve_k);
   1728       outputLinear *= masterLinearGain;
   1729       var outputDb = linearToDb(outputLinear);
   1730       var y = dBToYPixel(outputDb);
   1731 
   1732       canvasContext.lineTo(x, y);
   1733     }
   1734     canvasContext.stroke();
   1735 
   1736   }
   1737 
   1738   function init() {
   1739     drawCurve();
   1740   }
   1741 
   1742   function update_threshold(v)
   1743   {
   1744     dbThreshold = v;
   1745     drawCurve();
   1746   }
   1747 
   1748   function update_knee(v)
   1749   {
   1750     dbKnee = v;
   1751     drawCurve();
   1752   }
   1753 
   1754   function update_ratio(v)
   1755   {
   1756     ratio = v;
   1757     drawCurve();
   1758   }
   1759 
   1760   function update_boost(v)
   1761   {
   1762     boost = v;
   1763     drawCurve();
   1764   }
   1765 
   1766   this.init = init;
   1767   this.update_threshold = update_threshold;
   1768   this.update_knee = update_knee;
   1769   this.update_ratio = update_ratio;
   1770   this.update_boost = update_boost;
   1771 }
   1772 
   1773 /* Draws the EQ curve on a canvas. The update*() methods should be called when
   1774  * the parameters change, so the curve can be redrawn. */
   1775 function EqDrawer(canvas, channel) {
   1776   var canvasContext = canvas.getContext('2d');
   1777   var curveColor = 'rgb(192,192,192)';
   1778   var gridColor = 'rgb(200,200,200)';
   1779   var textColor = 'rgb(238,221,130)';
   1780   var centerFreq = {};
   1781   var q = {};
   1782   var gain = {};
   1783 
   1784   for (var i = 0; i < NEQ; i++) {
   1785     centerFreq[i] = INIT_EQ_FREQ;
   1786     q[i] = INIT_EQ_Q;
   1787     gain[i] = INIT_EQ_GAIN;
   1788   }
   1789 
   1790   function drawCurve() {
   1791     /* Create a biquad node to calculate frequency response. */
   1792     var filter = audioContext.createBiquadFilter();
   1793     var width = canvas.width;
   1794     var height = canvas.height;
   1795     var pixelsPerDb = height / 48.0;
   1796     var noctaves = 10;
   1797 
   1798     /* Prepare the frequency array */
   1799     var frequencyHz = new Float32Array(width);
   1800     for (var i = 0; i < width; ++i) {
   1801       var f = i / width;
   1802 
   1803       /* Convert to log frequency scale (octaves). */
   1804       f = Math.pow(2.0, noctaves * (f - 1.0));
   1805       frequencyHz[i] = f * nyquist;
   1806     }
   1807 
   1808     /* Get the response */
   1809     var magResponse = new Float32Array(width);
   1810     var phaseResponse = new Float32Array(width);
   1811     var totalMagResponse = new Float32Array(width);
   1812 
   1813     for (var i = 0; i < width; i++) {
   1814       totalMagResponse[i] = 1;
   1815     }
   1816 
   1817     for (var i = 0; i < NEQ; i++) {
   1818       if (!get_config('eq', channel, i, 'enable')) {
   1819         continue;
   1820       }
   1821       filter.type = get_config('eq', channel, i, 'type');
   1822       filter.frequency.value = centerFreq[i];
   1823       if (filter.type == 'lowpass' || filter.type == 'highpass')
   1824         filter.Q.value = make_biquad_q(q[i]);
   1825       else
   1826         filter.Q.value = q[i];
   1827       filter.gain.value = gain[i];
   1828       filter.getFrequencyResponse(frequencyHz, magResponse,
   1829                                   phaseResponse);
   1830       for (var j = 0; j < width; j++) {
   1831         totalMagResponse[j] *= magResponse[j];
   1832       }
   1833     }
   1834 
   1835     /* Draw the response */
   1836     canvasContext.fillStyle = 'rgb(0, 0, 0)';
   1837     canvasContext.fillRect(0, 0, width, height);
   1838     canvasContext.strokeStyle = curveColor;
   1839     canvasContext.lineWidth = 3;
   1840     canvasContext.beginPath();
   1841 
   1842     for (var i = 0; i < width; ++i) {
   1843       var response = totalMagResponse[i];
   1844       var dbResponse = linearToDb(response);
   1845 
   1846       var x = i;
   1847       var y = height - (dbResponse + 24) * pixelsPerDb;
   1848 
   1849       canvasContext.lineTo(x, y);
   1850     }
   1851     canvasContext.stroke();
   1852 
   1853     /* Draw frequency scale. */
   1854     canvasContext.beginPath();
   1855     canvasContext.lineWidth = 1;
   1856     canvasContext.strokeStyle = gridColor;
   1857 
   1858     for (var octave = 0; octave <= noctaves; octave++) {
   1859       var x = octave * width / noctaves;
   1860 
   1861       canvasContext.moveTo(x, 30);
   1862       canvasContext.lineTo(x, height);
   1863       canvasContext.stroke();
   1864 
   1865       var f = nyquist * Math.pow(2.0, octave - noctaves);
   1866       canvasContext.textAlign = 'center';
   1867       canvasContext.strokeText(f.toFixed(0) + 'Hz', x, 20);
   1868     }
   1869 
   1870     /* Draw 0dB line. */
   1871     canvasContext.beginPath();
   1872     canvasContext.moveTo(0, 0.5 * height);
   1873     canvasContext.lineTo(width, 0.5 * height);
   1874     canvasContext.stroke();
   1875 
   1876     /* Draw decibel scale. */
   1877     for (var db = -24.0; db < 24.0; db += 6) {
   1878       var y = height - (db + 24) * pixelsPerDb;
   1879       canvasContext.beginPath();
   1880       canvasContext.setLineDash([1, 4]);
   1881       canvasContext.moveTo(0, y);
   1882       canvasContext.lineTo(width, y);
   1883       canvasContext.stroke();
   1884       canvasContext.setLineDash([]);
   1885       canvasContext.strokeStyle = textColor;
   1886       canvasContext.strokeText(db.toFixed(0) + 'dB', width - 20, y);
   1887       canvasContext.strokeStyle = gridColor;
   1888     }
   1889   }
   1890 
   1891   function update_freq(index, v) {
   1892     centerFreq[index] = v;
   1893     drawCurve();
   1894   }
   1895 
   1896   function update_q(index, v) {
   1897     q[index] = v;
   1898     drawCurve();
   1899   }
   1900 
   1901   function update_gain(index, v) {
   1902     gain[index] = v;
   1903     drawCurve();
   1904   }
   1905 
   1906   function update_enable(index, v) {
   1907     drawCurve();
   1908   }
   1909 
   1910   function update_type(index, v) {
   1911     drawCurve();
   1912   }
   1913 
   1914   function init() {
   1915     drawCurve();
   1916   }
   1917 
   1918   this.init = init;
   1919   this.update_freq = update_freq;
   1920   this.update_q = update_q;
   1921   this.update_gain = update_gain;
   1922   this.update_enable = update_enable;
   1923   this.update_type = update_type;
   1924 }
   1925 
   1926 /* Draws the FFT curve on a canvas. This will update continuously when the audio
   1927  * is playing. */
   1928 function FFTDrawer(canvas, channel) {
   1929   var canvasContext = canvas.getContext('2d');
   1930   var curveColor = 'rgb(255,160,122)';
   1931   var binCount = FFT_SIZE / 2;
   1932   var data = new Float32Array(binCount);
   1933 
   1934   function drawCurve() {
   1935     var width = canvas.width;
   1936     var height = canvas.height;
   1937     var pixelsPerDb = height / 96.0;
   1938 
   1939     canvasContext.clearRect(0, 0, width, height);
   1940 
   1941     /* Get the proper analyzer from the audio graph */
   1942     var analyzer = (channel == 0) ? analyzer_left : analyzer_right;
   1943     if (!analyzer || !get_global('enable_fft')) {
   1944       requestAnimationFrame(drawCurve);
   1945       return;
   1946     }
   1947 
   1948     /* Draw decibel scale. */
   1949     for (var db = -96.0; db <= 0; db += 12) {
   1950       var y = height - (db + 96) * pixelsPerDb;
   1951       canvasContext.strokeStyle = curveColor;
   1952       canvasContext.strokeText(db.toFixed(0) + 'dB', 10, y);
   1953     }
   1954 
   1955     /* Draw FFT */
   1956     analyzer.getFloatFrequencyData(data);
   1957     canvasContext.beginPath();
   1958     canvasContext.lineWidth = 1;
   1959     canvasContext.strokeStyle = curveColor;
   1960     canvasContext.moveTo(0, height);
   1961 
   1962     var frequencyHz = new Float32Array(width);
   1963     for (var i = 0; i < binCount; ++i) {
   1964       var f = i / binCount;
   1965 
   1966       /* Convert to log frequency scale (octaves). */
   1967       var noctaves = 10;
   1968       f = 1 + Math.log(f) / (noctaves * Math.LN2);
   1969 
   1970       /* Draw the magnitude */
   1971       var x = f * width;
   1972       var y = height - (data[i] + 96) * pixelsPerDb;
   1973 
   1974       canvasContext.lineTo(x, y);
   1975     }
   1976 
   1977     canvasContext.stroke();
   1978     requestAnimationFrame(drawCurve);
   1979   }
   1980 
   1981   function init() {
   1982     requestAnimationFrame(drawCurve);
   1983   }
   1984 
   1985   this.init = init;
   1986 }
   1987 
   1988 function dBToLinear(db) {
   1989   return Math.pow(10.0, 0.05 * db);
   1990 }
   1991 
   1992 function linearToDb(x) {
   1993   return 20.0 * Math.log(x) / Math.LN10;
   1994 }
   1995