// Notes about generated waveforms:
//
// QUESTION: Why does the wave shape not look like the exact shape (sharp
// edges)? ANSWER: Because a shape with sharp edges has infinitely high
// frequency content. Since a digital audio signal must be band-limited based on
// the nyquist frequency (half the sample-rate) in order to avoid aliasing, this
// creates more rounded edges and "ringing" in the appearance of the waveform.
// See Nyquist-Shannon sampling theorem:
// http://en.wikipedia.org/wiki/Nyquist%E2%80%93Shannon_sampling_theorem
//
// QUESTION: Why does the very end of the generated signal appear to get
// slightly weaker? ANSWER: This is an artifact of the algorithm to avoid
// aliasing.
//
// QUESTION: Since the tests compare the actual result with an expected
// reference file, how are the reference files created? ANSWER: Run the test in
// a browser. When the test completes, a generated reference file with the name
// "<file>-actual.wav" is automatically downloaded. Use this as the new
// reference, after carefully inspecting to see if this is correct.
//
OscillatorTestingUtils = (function() {
let sampleRate = 44100.0;
let nyquist = 0.5 * sampleRate;
let lengthInSeconds = 4;
let lowFrequency = 10;
let highFrequency = nyquist + 2000; // go slightly higher than nyquist to
// make sure we generate silence there
let context = 0;
// Scaling factor for converting the 16-bit WAV data to float (and
// vice-versa).
let waveScaleFactor = 32768;
// Thresholds for verifying the test passes. The thresholds are
// experimentally determined. The default values here will cause the test to
// fail, which is useful for determining new thresholds, if needed.
// SNR must be greater than this to pass the test.
// Q: Why is the SNR threshold not infinity?
// A: The reference result is a 16-bit WAV file, so it won't compare exactly
// with the
// floating point result.
let thresholdSNR = 10000;
// Max diff must be less than this to pass the test.
let thresholdDiff = 0;
// Mostly for debugging
// An AudioBuffer for the reference (expected) result.
let reference = 0;
// Signal power of the reference
let signalPower = 0;
// Noise power of the difference between the reference and actual result.
let noisePower = 0;
function generateExponentialOscillatorSweep(context, oscillatorType) {
let osc = context.createOscillator();
if (oscillatorType == 'custom') {
// Create a simple waveform with three Fourier coefficients.
// Note the first values are expected to be zero (DC for coeffA and
// Nyquist for coeffB).
let coeffA = new Float32Array([0, 1, 0.5]);
let coeffB = new Float32Array([0, 0, 0]);
let wave = context.createPeriodicWave(coeffA, coeffB);
osc.setPeriodicWave(wave);
} else {
osc.type = oscillatorType;
}
// Scale by 1/2 to better visualize the waveform and to avoid clipping past
// full scale.
let gainNode = context.createGain();
gainNode.gain.value = 0.5;
osc.connect(gainNode);
gainNode.connect(context.destination);
osc.start(0);
osc.frequency.setValueAtTime(10, 0);
osc.frequency.exponentialRampToValueAtTime(highFrequency, lengthInSeconds);
}
function calculateSNR(sPower, nPower) {
return 10 * Math.log10(sPower / nPower);
}
function loadReferenceAndRunTest(context, oscType, task, should) {
Audit
.loadFileFromUrl(
'../Oscillator/oscillator-' + oscType + '-expected.wav')
.then(response => {
return context.decodeAudioData(response);
})
.then(audioBuffer => {
reference = audioBuffer.getChannelData(0);
generateExponentialOscillatorSweep(context, oscType);
return context.startRendering();
})
.then(resultBuffer => {
checkResult(resultBuffer, should, oscType);
})
.then(() => task.done());
}
function checkResult(renderedBuffer, should, oscType) {
let renderedData = renderedBuffer.getChannelData(0);
// Compute signal to noise ratio between the result and the reference. Also
// keep track of the max difference (and position).
let maxError = -1;
let errorPosition = -1;
let diffCount = 0;
for (let k = 0; k < renderedData.length; ++k) {
let diff = renderedData[k] - reference[k];
noisePower += diff * diff;
signalPower += reference[k] * reference[k];
if (Math.abs(diff) > maxError) {
maxError = Math.abs(diff);
errorPosition = k;
}
}
let snr = calculateSNR(signalPower, noisePower);
should(snr, 'SNR').beGreaterThanOrEqualTo(thresholdSNR);
should(maxError, 'Maximum difference').beLessThanOrEqualTo(thresholdDiff);
let filename = 'oscillator-' + oscType + '-actual.wav';
if (downloadAudioBuffer(renderedBuffer, filename, true))
should(true, 'Saved reference file').message(filename, '');
}
function setThresholds(thresholds) {
thresholdSNR = thresholds.snr;
thresholdDiff = thresholds.maxDiff;
thresholdDiffCount = thresholds.diffCount;
}
function runTest(context, oscType, description, task, should) {
loadReferenceAndRunTest(context, oscType, task, should);
}
return {
sampleRate: sampleRate,
lengthInSeconds: lengthInSeconds,
thresholdSNR: thresholdSNR,
thresholdDiff: thresholdDiff,
waveScaleFactor: waveScaleFactor,
setThresholds: setThresholds,
runTest: runTest,
};
}());