diff options
Diffstat (limited to 'scripts/audio_tuning/frontend/audio.js')
-rw-r--r-- | scripts/audio_tuning/frontend/audio.js | 1994 |
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; -} |