• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 */
87var INIT_GLOBAL_ENABLE_DRC = true;
88var INIT_GLOBAL_ENABLE_EQ = true;
89var INIT_GLOBAL_ENABLE_FFT = true;
90var INIT_GLOBAL_ENABLE_SWAP = false;
91var INIT_DRC_XO_LOW = 200;
92var INIT_DRC_XO_HIGH = 2000;
93var INIT_DRC_ENABLE = true;
94var INIT_DRC_THRESHOLD = -24;
95var INIT_DRC_KNEE = 30;
96var INIT_DRC_RATIO = 12;
97var INIT_DRC_ATTACK = 0.003;
98var INIT_DRC_RELEASE = 0.250;
99var INIT_DRC_BOOST = 0;
100var INIT_EQ_ENABLE = true;
101var INIT_EQ_TYPE = 'peaking';
102var INIT_EQ_FREQ = 350;
103var INIT_EQ_Q = 1;
104var INIT_EQ_GAIN = 0;
105
106var NEQ = 8;  /* The number of EQs per channel */
107var FFT_SIZE = 2048;  /* The size of FFT used for visualization */
108
109var audioContext;  /* Web Audio context */
110var nyquist;       /* Nyquist frequency, in Hz */
111var sourceNode;
112var audio_graph;
113var audio_ui;
114var analyzer_left;      /* The FFT analyzer for left channel */
115var 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. */
119var 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 */
128var browser_biquad_filter_uses_audio_cookbook_formula;
129
130/* Check the lowpass implementation and return a promise. */
131function 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 */
163function 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.*/
175function 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
186function init_audio() {
187  audioContext = new AudioContext();
188  nyquist = audioContext.sampleRate / 2;
189}
190
191function 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 */
219var all_configs = {};  /* stores all the configuration variables */
220
221function 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 '.' */
250function 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
258function get_config() {
259  var name = make_name(arguments, arguments.length);
260  return all_configs[name];
261}
262
263function 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 */
270function get_global(name) {
271  return get_config('global', name);
272}
273
274/* set_config and apply it to the audio graph and ui. */
275function 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. */
288function 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. */
300function two(n) {
301  var s = '00' + n;
302  return s.slice(-2);
303}
304
305/* Returns a time string, used for save file name */
306function 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. */
316function 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. */
329function load_config() {
330  document.getElementById('config_file').click();
331}
332
333function 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 */
368function 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 */
425function 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 */
452function 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.  */
483function 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) */
515function 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 */
537function 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 */
582function 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.*/
628function 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 */
695function 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 */
759function 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. */
817function 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 */
833function 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" */
843function 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 */
853function lr4_lowpass(freq) {
854  return new double(freq, create_lowpass);
855}
856
857/* Returns a LR4 highpass component */
858function lr4_highpass(freq) {
859  return new double(freq, create_highpass);
860}
861
862/* Returns a component by apply the same filter twice. */
863function 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 */
891function 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 */
900function 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 */
909function pin(node, index) {
910  var p = new Pin();
911  p.node = node;
912  p.index = index || 0;
913  return p;
914}
915
916function Pin(node, index) {
917}
918
919/* ============================ Event Handlers ============================ */
920
921function 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. */
930function load_audio() {
931  document.getElementById('audio_file').click();
932}
933
934function 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
946function 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
968function 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
975function update_source_node(mediaElement) {
976  sourceNode = audioContext.createMediaElementSource(mediaElement);
977  build_graph();
978}
979
980function toggle_global_checkbox(name, enable) {
981  use_config('global', name, enable);
982  build_graph();
983}
984
985function toggle_one_drc(index, enable) {
986  use_config('drc', index, 'enable', enable);
987  build_graph();
988}
989
990function 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. */
998function 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. */
1009function 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 */
1032function 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. */
1091function 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
1108function dummy() {
1109}
1110
1111/* Changes the opacity of a div. */
1112function 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 */
1123function 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. */
1252function 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 */
1301function 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 */
1413function 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
1453function 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
1494window.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
1509function init_ui() {
1510  audio_ui = new ui();
1511}
1512
1513/* Top-level UI */
1514function 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. */
1553function 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. */
1775function 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. */
1928function 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
1988function dBToLinear(db) {
1989  return Math.pow(10.0, 0.05 * db);
1990}
1991
1992function linearToDb(x) {
1993  return 20.0 * Math.log(x) / Math.LN10;
1994}
1995