Source code

Revision control

Copy as Markdown

Other Tools

Test Info:

<!DOCTYPE html>
<html>
<head>
<title>
Test Convolver Channel Outputs for Response with 1 channel
</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/webaudio/resources/audit-util.js"></script>
<script src="/webaudio/resources/audit.js"></script>
</head>
<body>
<script id="layout-test-code">
// Test various convolver configurations when the convolver response has
// one channel (mono).
// This is somewhat arbitrary. It is the minimum value for which tests
// pass with both FFmpeg and KISS FFT implementations for 256 points.
// The value was similar for each implementation.
const absoluteThreshold = Math.pow(2, -21);
// Fairly arbitrary sample rate, except that we want the rate to be a
// power of two so that 1/sampleRate is exactly representable as a
// single-precision float.
let sampleRate = 8192;
// A fairly arbitrary number of frames, except the number of frames should
// be more than a few render quanta.
let renderFrames = 10 * 128;
let audit = Audit.createTaskRunner();
// Convolver response
let response;
audit.define(
{
label: 'initialize',
description: 'Convolver response with one channel'
},
(task, should) => {
// Convolver response
should(
() => {
response = new AudioBuffer(
{numberOfChannels: 1, length: 2, sampleRate: sampleRate});
response.getChannelData(0)[1] = 1;
},
'new AudioBuffer({numberOfChannels: 1, length: 2, sampleRate: ' +
sampleRate + '})')
.notThrow();
task.done();
});
audit.define(
{label: '1-channel input', description: 'produces 1-channel output'},
(task, should) => {
// Create a 3-channel context: channel 0 = convolver under test,
// channel 1: test that convolver output is not stereo, channel 2:
// expected output. The context MUST be discrete so that the
// channels don't get mixed in some unexpected way.
let context = new OfflineAudioContext(3, renderFrames, sampleRate);
context.destination.channelInterpretation = 'discrete';
let src = new OscillatorNode(context);
let conv = new ConvolverNode(
context, {disableNormalization: true, buffer: response});
// Splitter node to verify that the output of the convolver is mono.
// channelInterpretation must be 'discrete' so we don't do any
// mixing of the input to the node.
let splitter = new ChannelSplitterNode(
context,
{numberOfOutputs: 2, channelInterpretation: 'discrete'});
// Final merger to feed all of the individual channels into the
// destination.
let merger = new ChannelMergerNode(context, {numberOfInputs: 3});
src.connect(conv).connect(splitter);
splitter.connect(merger, 0, 0);
splitter.connect(merger, 1, 1);
// The convolver response is a 1-sample delay. Use a delay node to
// implement this.
let delay =
new DelayNode(context, {delayTime: 1 / context.sampleRate});
src.connect(delay);
delay.connect(merger, 0, 2);
merger.connect(context.destination);
src.start();
context.startRendering()
.then(audioBuffer => {
// Extract out the three channels
let actual = audioBuffer.getChannelData(0);
let c1 = audioBuffer.getChannelData(1);
let expected = audioBuffer.getChannelData(2);
// c1 is expected to be zero.
should(c1, '1: Channel 1').beConstantValueOf(0);
// The expected and actual results should be identical
should(actual, 'Convolver output')
.beCloseToArray(expected,
{absoluteThreshold: absoluteThreshold});
})
.then(() => task.done());
});
audit.define(
{label: '2-channel input', description: 'produces 2-channel output'},
(task, should) => {
downMixTest({numberOfInputs: 2, prefix: '2'}, should)
.then(() => task.done());
});
audit.define(
{
label: '3-channel input',
description: '3->2 downmix producing 2-channel output'
},
(task, should) => {
downMixTest({numberOfInputs: 3, prefix: '3'}, should)
.then(() => task.done());
});
audit.define(
{
label: '4-channel input',
description: '4->2 downmix producing 2-channel output'
},
(task, should) => {
downMixTest({numberOfInputs: 4, prefix: '4'}, should)
.then(() => task.done());
});
audit.define(
{
label: '5.1-channel input',
description: '5.1->2 downmix producing 2-channel output'
},
(task, should) => {
// Scale tolerance by maximum amplitude expected in down-mix
// output.
let threshold = (1.0 + Math.sqrt(0.5) * 2) * absoluteThreshold;
downMixTest({numberOfInputs: 6, prefix: '5.1',
absoluteThreshold: threshold}, should)
.then(() => task.done());
});
audit.define(
{
label: '3-channel input, explicit',
description: '3->2 explicit downmix producing 2-channel output'
},
(task, should) => {
downMixTest(
{
channelCountMode: 'explicit',
numberOfInputs: 3,
prefix: '3 chan downmix explicit'
},
should)
.then(() => task.done());
});
audit.define(
{
label: '4-channel input, explicit',
description: '4->2 explicit downmix producing 2-channel output'
},
(task, should) => {
downMixTest(
{
channelCountMode: 'explicit',
numberOfInputs: 4,
prefix: '4 chan downmix explicit'
},
should)
.then(() => task.done());
});
audit.define(
{
label: '5.1-channel input, explicit',
description: '5.1->2 explicit downmix producing 2-channel output'
},
(task, should) => {
// Scale tolerance by maximum amplitude expected in down-mix
// output.
let threshold = (1.0 + Math.sqrt(0.5) * 2) * absoluteThreshold;
downMixTest(
{
channelCountMode: 'explicit',
numberOfInputs: 6,
prefix: '5.1 chan downmix explicit',
absoluteThreshold: threshold
},
should)
.then(() => task.done());
});
audit.define(
{
label: 'mono-upmix-explicit',
description: '1->2 upmix, count mode explicit'
},
(task, should) => {
upMixTest(should, {channelCountMode: 'explicit'})
.then(buffer => {
let length = buffer.length;
let input = buffer.getChannelData(0);
let out0 = buffer.getChannelData(1);
let out1 = buffer.getChannelData(2);
// The convolver is basically a one-sample delay. Verify that
// that each channel is delayed by one sample.
should(out0.slice(1), '1->2 explicit upmix: channel 0')
.beCloseToArray(
input.slice(0, length - 1),
{absoluteThreshold: absoluteThreshold});
should(out1.slice(1), '1->2 explicit upmix: channel 1')
.beCloseToArray(
input.slice(0, length - 1),
{absoluteThreshold: absoluteThreshold});
})
.then(() => task.done());
});
audit.define(
{
label: 'mono-upmix-clamped-max',
description: '1->2 upmix, count mode clamped-max'
},
(task, should) => {
upMixTest(should, {channelCountMode: 'clamped-max'})
.then(buffer => {
let length = buffer.length;
let input = buffer.getChannelData(0);
let out0 = buffer.getChannelData(1);
let out1 = buffer.getChannelData(2);
// The convolver is basically a one-sample delay. With a mono
// input, the count set to 2, and a mode of 'clamped-max', the
// output should be mono
should(out0.slice(1), '1->2 clamped-max upmix: channel 0')
.beCloseToArray(
input.slice(0, length - 1),
{absoluteThreshold: absoluteThreshold});
should(out1, '1->2 clamped-max upmix: channel 1')
.beConstantValueOf(0);
})
.then(() => task.done());
});
function downMixTest(options, should) {
// Create an 4-channel offline context. The first two channels are for
// the stereo output of the convolver and the next two channels are for
// the reference stereo signal.
let context = new OfflineAudioContext(4, renderFrames, sampleRate);
context.destination.channelInterpretation = 'discrete';
// Create oscillators for use as the input. The type and frequency is
// arbitrary except that oscillators must be different.
let src = new Array(options.numberOfInputs);
for (let k = 0; k < src.length; ++k) {
src[k] = new OscillatorNode(
context, {type: 'square', frequency: 440 + 220 * k});
}
// Merger to combine the oscillators into one output stream.
let srcMerger =
new ChannelMergerNode(context, {numberOfInputs: src.length});
for (let k = 0; k < src.length; ++k) {
src[k].connect(srcMerger, 0, k);
}
// Convolver under test.
let conv = new ConvolverNode(context, {
disableNormalization: true,
buffer: response,
channelCountMode: options.channelCountMode
});
srcMerger.connect(conv);
// Splitter to get individual channels of the convolver output so we can
// feed them (eventually) to the context in the right set of channels.
let splitter = new ChannelSplitterNode(context, {numberOfOutputs: 2});
conv.connect(splitter);
// Reference graph consists of a delay node to simulate the response of
// the convolver. (The convolver response is designed this way.)
let delay = new DelayNode(context, {delayTime: 1 / context.sampleRate});
// Gain node to mix the sources to stereo in the desired way. (Could be
// done in the delay node, but let's keep the mixing separated from the
// functionality.)
let gainMixer = new GainNode(
context, {channelCount: 2, channelCountMode: 'explicit'});
srcMerger.connect(gainMixer);
// Splitter to extract the channels of the reference signal.
let refSplitter =
new ChannelSplitterNode(context, {numberOfOutputs: 2});
gainMixer.connect(delay).connect(refSplitter);
// Final merger to bring back the individual channels from the convolver
// and the reference in the right order for the destination.
let finalMerger = new ChannelMergerNode(
context, {numberOfInputs: context.destination.channelCount});
// First two channels are for the convolver output, and the next two are
// for the reference.
splitter.connect(finalMerger, 0, 0);
splitter.connect(finalMerger, 1, 1);
refSplitter.connect(finalMerger, 0, 2);
refSplitter.connect(finalMerger, 1, 3);
finalMerger.connect(context.destination);
// Start the sources at last.
for (let k = 0; k < src.length; ++k) {
src[k].start();
}
return context.startRendering().then(audioBuffer => {
// Extract the various channels out
let actual0 = audioBuffer.getChannelData(0);
let actual1 = audioBuffer.getChannelData(1);
let expected0 = audioBuffer.getChannelData(2);
let expected1 = audioBuffer.getChannelData(3);
let threshold = options.absoluteThreshold ?
options.absoluteThreshold : absoluteThreshold;
// Verify that each output channel of the convolver matches
// the delayed signal from the reference
should(actual0, options.prefix + ': Channel 0')
.beCloseToArray(expected0, {absoluteThreshold: threshold});
should(actual1, options.prefix + ': Channel 1')
.beCloseToArray(expected1, {absoluteThreshold: threshold});
});
}
function upMixTest(should, options) {
// Offline context with 3 channels: 0 = source
// 1 = convolver output, left, 2 = convolver output, right. Context
// destination must be discrete so that channels don't get mixed in
// unexpected ways.
let context = new OfflineAudioContext(3, renderFrames, sampleRate);
context.destination.channelInterpretation = 'discrete';
let merger = new ChannelMergerNode(
context, {numberOfInputs: context.destination.maxChannelCount});
merger.connect(context.destination);
let src = new OscillatorNode(context);
// Mono response for convolver. Just a simple 1-frame delay.
let response =
new AudioBuffer({length: 2, sampleRate: context.sampleRate});
response.getChannelData(0)[1] = 1;
// Set mode to explicit and count to 2 so we manually force the
// convolver to produce stereo output. Without this, it would be
// mono input with mono response, which produces a mono output.
let conv;
should(
() => {conv = new ConvolverNode(context, {
buffer: response,
disableNormalization: true,
channelCount: 2,
channelCountMode: options.channelCountMode
})},
`new ConvolverNode({channelCountMode: '${
options.channelCountMode}'})`)
.notThrow();
// Split output of convolver into individual channels.
let convSplit = new ChannelSplitterNode(context, {numberOfOutputs: 2});
src.connect(conv);
conv.connect(convSplit);
// Connect signals to destination in the desired way.
src.connect(merger, 0, 0);
convSplit.connect(merger, 0, 1);
convSplit.connect(merger, 1, 2);
src.start();
return context.startRendering();
}
audit.run();
</script>
</body>
</html>