Source code

Revision control

Copy as Markdown

Other Tools

Test Info:

<!DOCTYPE html>
<title>Test that up-mixing signals in ConvolverNode processing is linear</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script>
const EPSILON = 3.0 * Math.pow(2, -22);
// sampleRate is a power of two so that delay times are exact in base-2
// floating point arithmetic.
const SAMPLE_RATE = 32768;
// Length of stereo convolver input in frames (arbitrary):
const STEREO_FRAMES = 256;
// Length of mono signal in frames. This is more than two blocks to ensure
// that at least one block will be mono, even if interpolation in the
// DelayNode means that stereo is output one block earlier and later than
// if frames are delayed without interpolation.
const MONO_FRAMES = 384;
// Length of response buffer:
const RESPONSE_FRAMES = 256;
function test_linear_upmixing(channelInterpretation, initial_mono_frames)
{
let stereo_input_end = initial_mono_frames + STEREO_FRAMES;
// Total length:
let length = stereo_input_end + RESPONSE_FRAMES + MONO_FRAMES + STEREO_FRAMES;
// The first two channels contain signal where some up-mixing occurs
// internally to a ConvolverNode when a stereo signal is added and removed.
// The last two channels are expected to contain the same signal, but mono
// and stereo signals are convolved independently before up-mixing the mono
// output to mix with the stereo output.
let context = new OfflineAudioContext({numberOfChannels: 4,
length: length,
sampleRate: SAMPLE_RATE});
let response = new AudioBuffer({numberOfChannels: 1,
length: RESPONSE_FRAMES,
sampleRate: context.sampleRate});
// Two stereo channel splitters will collect test and reference outputs.
let destinationMerger = new ChannelMergerNode(context, {numberOfInputs: 4});
destinationMerger.connect(context.destination);
let testSplitter =
new ChannelSplitterNode(context, {numberOfOutputs: 2});
let referenceSplitter =
new ChannelSplitterNode(context, {numberOfOutputs: 2});
testSplitter.connect(destinationMerger, 0, 0);
testSplitter.connect(destinationMerger, 1, 1);
referenceSplitter.connect(destinationMerger, 0, 2);
referenceSplitter.connect(destinationMerger, 1, 3);
// A GainNode mixes reference stereo and mono signals because up-mixing
// cannot be performed at a channel splitter.
let referenceGain = new GainNode(context);
referenceGain.connect(referenceSplitter);
referenceGain.channelInterpretation = channelInterpretation;
// The impulse response for convolution contains two impulses so as to test
// effects in at least two processing blocks.
response.getChannelData(0)[0] = 0.5;
response.getChannelData(0)[response.length - 1] = 0.5;
let testConvolver = new ConvolverNode(context, {disableNormalization: true,
buffer: response});
testConvolver.channelInterpretation = channelInterpretation;
let referenceMonoConvolver = new ConvolverNode(context,
{disableNormalization: true,
buffer: response});
let referenceStereoConvolver = new ConvolverNode(context,
{disableNormalization: true,
buffer: response});
// No need to set referenceStereoConvolver.channelInterpretation because
// input is either silent or stereo.
testConvolver.connect(testSplitter);
// Mix reference convolver output.
referenceMonoConvolver.connect(referenceGain);
referenceStereoConvolver.connect(referenceGain);
// The DelayNode initially has a single channel of silence, which is used to
// switch the stereo signal in and out. The output of the delay node is
// first mono silence (if there is a non-zero initial_mono_frames), then
// stereo, then mono silence, and finally stereo again. maxDelayTime is
// used to generate the middle mono silence period from the initial silence
// in the DelayNode and then generate the final period of stereo from its
// initial input.
let maxDelayTime = (length - STEREO_FRAMES) / context.sampleRate;
let delay =
new DelayNode(context,
{maxDelayTime: maxDelayTime,
delayTime: initial_mono_frames / context.sampleRate});
// Schedule an increase in the delay to return to mono silence.
delay.delayTime.setValueAtTime(maxDelayTime,
stereo_input_end / context.sampleRate);
delay.connect(testConvolver);
delay.connect(referenceStereoConvolver);
let stereoMerger = new ChannelMergerNode(context, {numberOfInputs: 2});
stereoMerger.connect(delay);
// Three independent signals
let monoSignal = new OscillatorNode(context, {frequency: 440});
let leftSignal = new OscillatorNode(context, {frequency: 450});
let rightSignal = new OscillatorNode(context, {frequency: 460});
monoSignal.connect(testConvolver);
monoSignal.connect(referenceMonoConvolver);
leftSignal.connect(stereoMerger, 0, 0);
rightSignal.connect(stereoMerger, 0, 1);
monoSignal.start();
leftSignal.start();
rightSignal.start();
return context.startRendering().
then((buffer) => {
let maxDiff = -1.0;
let frameIndex = 0;
let channelIndex = 0;
for (let c = 0; c < 2; ++c) {
let testOutput = buffer.getChannelData(0 + c);
let referenceOutput = buffer.getChannelData(2 + c);
for (var i = 0; i < buffer.length; ++i) {
var diff = Math.abs(testOutput[i] - referenceOutput[i]);
if (diff > maxDiff) {
maxDiff = diff;
frameIndex = i;
channelIndex = c;
}
}
}
assert_approx_equals(buffer.getChannelData(0 + channelIndex)[frameIndex],
buffer.getChannelData(2 + channelIndex)[frameIndex],
EPSILON,
`output at ${frameIndex} ` +
`in channel ${channelIndex}` );
});
}
promise_test(() => test_linear_upmixing("speakers", MONO_FRAMES),
"speakers, initially mono");
promise_test(() => test_linear_upmixing("discrete", MONO_FRAMES),
"discrete");
// Gecko uses a separate path for "speakers" up-mixing when the convolver's
// first input is stereo, so test that separately.
promise_test(() => test_linear_upmixing("speakers", 0),
"speakers, initially stereo");
</script>