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