summaryrefslogtreecommitdiff
path: root/scripts/audio_tuning/frontend/audio.js
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/audio_tuning/frontend/audio.js')
-rw-r--r--scripts/audio_tuning/frontend/audio.js1994
1 files changed, 0 insertions, 1994 deletions
diff --git a/scripts/audio_tuning/frontend/audio.js b/scripts/audio_tuning/frontend/audio.js
deleted file mode 100644
index 98870cdd..00000000
--- a/scripts/audio_tuning/frontend/audio.js
+++ /dev/null
@@ -1,1994 +0,0 @@
-/* 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 empty() {
-}
-
-/* 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, empty);
- 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 mainLinearGain;
-
- 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 mainLinearGain */
- var fullRangeGain = saturate(1, curve_k);
- var fullRangeMakeupGain = Math.pow(1 / fullRangeGain, 0.6);
- mainLinearGain = 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 *= mainLinearGain;
- 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;
-}