188 lines
8.3 KiB
JavaScript
188 lines
8.3 KiB
JavaScript
/**
|
|
* @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 <bitllama@google.com>
|
|
*/
|
|
'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;
|