/** * @license * Copyright 2017 Google Inc. All Rights Reserved. * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * @file Late reverberation filter for Ambisonic content. * @author Andrew Allen */ 'use strict'; // Internal dependencies. import Utils from './utils.js'; /** * @class LateReflections * @description Late-reflections reverberation filter for Ambisonic content. * @param {AudioContext} context * Associated {@link https://developer.mozilla.org/en-US/docs/Web/API/AudioContext AudioContext}. * @param {Object} options * @param {Array} options.durations * Multiband RT60 durations (in seconds) for each frequency band, listed as * {@linkcode Utils.DEFAULT_REVERB_FREQUENCY_BANDS * FREQUDEFAULT_REVERB_FREQUENCY_BANDSENCY_BANDS}. Defaults to * {@linkcode Utils.DEFAULT_REVERB_DURATIONS DEFAULT_REVERB_DURATIONS}. * @param {Number} options.predelay Pre-delay (in milliseconds). Defaults to * {@linkcode Utils.DEFAULT_REVERB_PREDELAY DEFAULT_REVERB_PREDELAY}. * @param {Number} options.gain Output gain (linear). Defaults to * {@linkcode Utils.DEFAULT_REVERB_GAIN DEFAULT_REVERB_GAIN}. * @param {Number} options.bandwidth Bandwidth (in octaves) for each frequency * band. Defaults to * {@linkcode Utils.DEFAULT_REVERB_BANDWIDTH DEFAULT_REVERB_BANDWIDTH}. * @param {Number} options.tailonset Length (in milliseconds) of impulse * response to apply a half-Hann window. Defaults to * {@linkcode Utils.DEFAULT_REVERB_TAIL_ONSET DEFAULT_REVERB_TAIL_ONSET}. */ class LateReflections { constructor(context, options) { // Public variables. /** * Mono (1-channel) input {@link * https://developer.mozilla.org/en-US/docs/Web/API/AudioNode AudioNode}. * @member {AudioNode} input * @memberof LateReflections * @instance */ /** * Mono (1-channel) output {@link * https://developer.mozilla.org/en-US/docs/Web/API/AudioNode AudioNode}. * @member {AudioNode} output * @memberof LateReflections * @instance */ // Use defaults for undefined arguments. if (options == undefined) { options = {}; } if (options.durations == undefined) { options.durations = Utils.DEFAULT_REVERB_DURATIONS.slice(); } if (options.predelay == undefined) { options.predelay = Utils.DEFAULT_REVERB_PREDELAY; } if (options.gain == undefined) { options.gain = Utils.DEFAULT_REVERB_GAIN; } if (options.bandwidth == undefined) { options.bandwidth = Utils.DEFAULT_REVERB_BANDWIDTH; } if (options.tailonset == undefined) { options.tailonset = Utils.DEFAULT_REVERB_TAIL_ONSET; } // Assign pre-computed variables. let delaySecs = options.predelay / 1000; this._bandwidthCoeff = options.bandwidth * Utils.LOG2_DIV2; this._tailonsetSamples = options.tailonset / 1000; // Create nodes. this._context = context; this.input = context.createGain(); this._predelay = context.createDelay(delaySecs); this._convolver = context.createConvolver(); this.output = context.createGain(); // Set reverb attenuation. this.output.gain.value = options.gain; // Disable normalization. this._convolver.normalize = false; // Connect nodes. this.input.connect(this._predelay); this._predelay.connect(this._convolver); this._convolver.connect(this.output); // Compute IR using RT60 values. this.setDurations(options.durations); } /** * Re-compute a new impulse response by providing Multiband RT60 durations. * @param {Array} durations * Multiband RT60 durations (in seconds) for each frequency band, listed as * {@linkcode Utils.DEFAULT_REVERB_FREQUENCY_BANDS * DEFAULT_REVERB_FREQUENCY_BANDS}. */ setDurations(durations) { if (durations.length !== Utils.NUMBER_REVERB_FREQUENCY_BANDS) { Utils.log('Warning: invalid number of RT60 values provided to reverb.'); return; } // Compute impulse response. let durationsSamples = new Float32Array(Utils.NUMBER_REVERB_FREQUENCY_BANDS); let sampleRate = this._context.sampleRate; for (let i = 0; i < durations.length; i++) { // Clamp within suitable range. durations[i] = Math.max(0, Math.min(Utils.DEFAULT_REVERB_MAX_DURATION, durations[i])); // Convert seconds to samples. durationsSamples[i] = Math.round(durations[i] * sampleRate * Utils.DEFAULT_REVERB_DURATION_MULTIPLIER); } ; // Determine max RT60 length in samples. let durationsSamplesMax = 0; for (let i = 0; i < durationsSamples.length; i++) { if (durationsSamples[i] > durationsSamplesMax) { durationsSamplesMax = durationsSamples[i]; } } // Skip this step if there is no reverberation to compute. if (durationsSamplesMax < 1) { durationsSamplesMax = 1; } // Create impulse response buffer. let buffer = this._context.createBuffer(1, durationsSamplesMax, sampleRate); let bufferData = buffer.getChannelData(0); // Create noise signal (computed once, referenced in each band's routine). let noiseSignal = new Float32Array(durationsSamplesMax); for (let i = 0; i < durationsSamplesMax; i++) { noiseSignal[i] = Math.random() * 2 - 1; } // Compute the decay rate per-band and filter the decaying noise signal. for (let i = 0; i < Utils.NUMBER_REVERB_FREQUENCY_BANDS; i++) { // Compute decay rate. let decayRate = -Utils.LOG1000 / durationsSamples[i]; // Construct a standard one-zero, two-pole bandpass filter: // H(z) = (b0 * z^0 + b1 * z^-1 + b2 * z^-2) / (1 + a1 * z^-1 + a2 * z^-2) let omega = Utils.TWO_PI * Utils.DEFAULT_REVERB_FREQUENCY_BANDS[i] / sampleRate; let sinOmega = Math.sin(omega); let alpha = sinOmega * Math.sinh(this._bandwidthCoeff * omega / sinOmega); let a0CoeffReciprocal = 1 / (1 + alpha); let b0Coeff = alpha * a0CoeffReciprocal; let a1Coeff = -2 * Math.cos(omega) * a0CoeffReciprocal; let a2Coeff = (1 - alpha) * a0CoeffReciprocal; // We optimize since b2 = -b0, b1 = 0. // Update equation for two-pole bandpass filter: // u[n] = x[n] - a1 * x[n-1] - a2 * x[n-2] // y[n] = b0 * (u[n] - u[n-2]) let um1 = 0; let um2 = 0; for (let j = 0; j < durationsSamples[i]; j++) { // Exponentially-decaying white noise. let x = noiseSignal[j] * Math.exp(decayRate * j); // Filter signal with bandpass filter and add to output. let u = x - a1Coeff * um1 - a2Coeff * um2; bufferData[j] += b0Coeff * (u - um2); // Update coefficients. um2 = um1; um1 = u; } } // Create and apply half of a Hann window to the beginning of the // impulse response. let halfHannLength = Math.round(this._tailonsetSamples); for (let i = 0; i < Math.min(bufferData.length, halfHannLength); i++) { let halfHann = 0.5 * (1 - Math.cos(Utils.TWO_PI * i / (2 * halfHannLength - 1))); bufferData[i] *= halfHann; } this._convolver.buffer = buffer; } } export default LateReflections;