blob: bf6b453c1cfc32a799b33aed548eb3d51bd38fae [file] [log] [blame]
let context;
let lengthInSeconds = 2;
// Skip this many frames before comparing against reference to allow
// a steady-state to be reached in the up-sampling filters.
let filterStabilizeSkipFrames = 2048;
let numberOfCurveFrames = 65536;
let waveShapingCurve;
let waveshaper;
// FIXME: test at more frequencies.
// When using the up-sampling filters (2x, 4x) any significant aliasing
// components should be at very high frequencies near Nyquist. These tests
// could be improved to allow for a higher acceptable amount of aliasing near
// Nyquist, but then become more stringent for lower frequencies.
// These test parameters are set in runWaveShaperOversamplingTest().
let sampleRate;
let nyquist;
let oversample;
let fundamentalFrequency;
let acceptableAliasingThresholdDecibels;
let kScale = 0.25;
// Chebyshev Polynomials.
// Given an input sinusoid, returns an output sinusoid of the given frequency
// multiple.
function T0(x) {
return 1;
}
function T1(x) {
return x;
}
function T2(x) {
return 2 * x * x - 1;
}
function T3(x) {
return 4 * x * x * x - 3 * x;
}
function T4(x) {
return 8 * x * x * x * x - 8 * x * x + 1;
}
function generateWaveShapingCurve() {
let n = 65536;
let n2 = n / 2;
let curve = new Float32Array(n);
// The shaping curve uses Chebyshev Polynomial such that an input sinusoid
// at frequency f will generate an output of four sinusoids of frequencies:
// f, 2*f, 3*f, 4*f
// each of which is scaled.
for (let i = 0; i < n; ++i) {
let x = (i - n2) / n2;
let y = kScale * (T1(x) + T2(x) + T3(x) + T4(x));
curve[i] = y;
}
return curve;
}
function checkShapedCurve(buffer, should) {
let outputData = buffer.getChannelData(0);
let n = buffer.length;
// The WaveShaperNode will have a processing latency if oversampling is used,
// so we should account for it.
// FIXME: .latency should be exposed as an attribute of the node
// var waveShaperLatencyFrames = waveshaper.latency * sampleRate;
// But for now we'll use the hard-coded values corresponding to the actual
// latencies:
let waveShaperLatencyFrames = 0;
if (oversample == '2x')
waveShaperLatencyFrames = 128;
else if (oversample == '4x')
waveShaperLatencyFrames = 192;
let worstDeltaInDecibels = -1000;
for (let i = waveShaperLatencyFrames; i < n; ++i) {
let actual = outputData[i];
// Account for the expected processing latency.
let j = i - waveShaperLatencyFrames;
// Compute reference sinusoids.
let phaseInc = 2 * Math.PI * fundamentalFrequency / sampleRate;
// Generate an idealized reference based on the four generated frequencies
// truncated to the Nyquist rate. Ideally, we'd like the waveshaper's
// oversampling to perfectly remove all frequencies above Nyquist to avoid
// aliasing. In reality the oversampling filters are not quite perfect, so
// there will be a (hopefully small) amount of aliasing. We should be close
// to the ideal.
let reference = 0;
// Sum in fundamental frequency.
if (fundamentalFrequency < nyquist)
reference += Math.sin(phaseInc * j);
// Note that the phase of each of the expected generated harmonics is
// different.
if (fundamentalFrequency * 2 < nyquist)
reference += -Math.cos(phaseInc * j * 2);
if (fundamentalFrequency * 3 < nyquist)
reference += -Math.sin(phaseInc * j * 3);
if (fundamentalFrequency * 4 < nyquist)
reference += Math.cos(phaseInc * j * 4);
// Scale the reference the same as the waveshaping curve itself.
reference *= kScale;
let delta = Math.abs(actual - reference);
let deltaInDecibels =
delta > 0 ? 20 * Math.log(delta) / Math.log(10) : -200;
if (j >= filterStabilizeSkipFrames) {
if (deltaInDecibels > worstDeltaInDecibels) {
worstDeltaInDecibels = deltaInDecibels;
}
}
}
// console.log("worstDeltaInDecibels: " + worstDeltaInDecibels);
should(
worstDeltaInDecibels,
oversample + ' WaveshaperNode oversampling error (in dBFS)')
.beLessThan(acceptableAliasingThresholdDecibels);
}
function createImpulseBuffer(context, sampleFrameLength) {
let audioBuffer =
context.createBuffer(1, sampleFrameLength, context.sampleRate);
let n = audioBuffer.length;
let dataL = audioBuffer.getChannelData(0);
for (let k = 0; k < n; ++k)
dataL[k] = 0;
dataL[0] = 1;
return audioBuffer;
}
function runWaveShaperOversamplingTest(testParams) {
sampleRate = testParams.sampleRate;
nyquist = 0.5 * sampleRate;
oversample = testParams.oversample;
fundamentalFrequency = testParams.fundamentalFrequency;
acceptableAliasingThresholdDecibels =
testParams.acceptableAliasingThresholdDecibels;
let audit = Audit.createTaskRunner();
audit.define(
{label: 'test', description: testParams.description},
function(task, should) {
// Create offline audio context.
let numberOfRenderFrames = sampleRate * lengthInSeconds;
context = new OfflineAudioContext(1, numberOfRenderFrames, sampleRate);
// source -> waveshaper -> destination
let source = context.createBufferSource();
source.buffer =
createToneBuffer(context, fundamentalFrequency, lengthInSeconds, 1);
// Apply a non-linear distortion curve.
waveshaper = context.createWaveShaper();
waveshaper.curve = generateWaveShapingCurve();
waveshaper.oversample = oversample;
source.connect(waveshaper);
waveshaper.connect(context.destination);
source.start(0);
context.startRendering()
.then(buffer => checkShapedCurve(buffer, should))
.then(() => task.done());
});
audit.run();
}