Javascript  |  1995行  |  53.31 KB

/* 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;
}