Skip to content

Commit

Permalink
This is to support the automation methods of AudioNode.connect() meth…
Browse files Browse the repository at this point in the history
…od. The

current implementation does not return anything when these methods are executed.
This feature improves the control flow and the readability of Web Audio JS code.

BUG=552607, 554992
TEST=LayoutTests/webaudio/audionode-connect-method-chaining.html

Review URL: https://codereview.chromium.org/1410383021

Cr-Commit-Position: refs/heads/master@{#360013}
  • Loading branch information
hoch authored and Commit bot committed Nov 17, 2015
1 parent 74dd3b4 commit 166ccf3
Show file tree
Hide file tree
Showing 7 changed files with 253 additions and 11 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
Test method chaining feature of AudioNode.connect() method.

On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".


PASS The return value of AnalyserNode.connect(GainNode) matches the destination GainNode.
PASS The return value of AnalyserNode.connect(BiquadFilterNode, 0) matches the destination BiquadFilterNode.
PASS The return value of AnalyserNode.connect(ChannelMergerNode, 0, 1) matches the destination ChannelMergerNode.
PASS The return value of BiquadFilterNode.connect(GainNode) matches the destination GainNode.
PASS The return value of BiquadFilterNode.connect(BiquadFilterNode, 0) matches the destination BiquadFilterNode.
PASS The return value of BiquadFilterNode.connect(ChannelMergerNode, 0, 1) matches the destination ChannelMergerNode.
PASS The return value of AudioBufferSourceNode.connect(GainNode) matches the destination GainNode.
PASS The return value of AudioBufferSourceNode.connect(BiquadFilterNode, 0) matches the destination BiquadFilterNode.
PASS The return value of AudioBufferSourceNode.connect(ChannelMergerNode, 0, 1) matches the destination ChannelMergerNode.
PASS The return value of ChannelMergerNode.connect(GainNode) matches the destination GainNode.
PASS The return value of ChannelMergerNode.connect(BiquadFilterNode, 0) matches the destination BiquadFilterNode.
PASS The return value of ChannelMergerNode.connect(ChannelMergerNode, 0, 1) matches the destination ChannelMergerNode.
PASS The return value of ChannelSplitterNode.connect(GainNode) matches the destination GainNode.
PASS The return value of ChannelSplitterNode.connect(BiquadFilterNode, 0) matches the destination BiquadFilterNode.
PASS The return value of ChannelSplitterNode.connect(ChannelMergerNode, 0, 1) matches the destination ChannelMergerNode.
PASS The return value of ConvolverNode.connect(GainNode) matches the destination GainNode.
PASS The return value of ConvolverNode.connect(BiquadFilterNode, 0) matches the destination BiquadFilterNode.
PASS The return value of ConvolverNode.connect(ChannelMergerNode, 0, 1) matches the destination ChannelMergerNode.
PASS The return value of DelayNode.connect(GainNode) matches the destination GainNode.
PASS The return value of DelayNode.connect(BiquadFilterNode, 0) matches the destination BiquadFilterNode.
PASS The return value of DelayNode.connect(ChannelMergerNode, 0, 1) matches the destination ChannelMergerNode.
PASS The return value of DynamicsCompressorNode.connect(GainNode) matches the destination GainNode.
PASS The return value of DynamicsCompressorNode.connect(BiquadFilterNode, 0) matches the destination BiquadFilterNode.
PASS The return value of DynamicsCompressorNode.connect(ChannelMergerNode, 0, 1) matches the destination ChannelMergerNode.
PASS The return value of GainNode.connect(GainNode) matches the destination GainNode.
PASS The return value of GainNode.connect(BiquadFilterNode, 0) matches the destination BiquadFilterNode.
PASS The return value of GainNode.connect(ChannelMergerNode, 0, 1) matches the destination ChannelMergerNode.
PASS The return value of OscillatorNode.connect(GainNode) matches the destination GainNode.
PASS The return value of OscillatorNode.connect(BiquadFilterNode, 0) matches the destination BiquadFilterNode.
PASS The return value of OscillatorNode.connect(ChannelMergerNode, 0, 1) matches the destination ChannelMergerNode.
PASS The return value of PannerNode.connect(GainNode) matches the destination GainNode.
PASS The return value of PannerNode.connect(BiquadFilterNode, 0) matches the destination BiquadFilterNode.
PASS The return value of PannerNode.connect(ChannelMergerNode, 0, 1) matches the destination ChannelMergerNode.
PASS The return value of ScriptProcessorNode.connect(GainNode) matches the destination GainNode.
PASS The return value of ScriptProcessorNode.connect(BiquadFilterNode, 0) matches the destination BiquadFilterNode.
PASS The return value of ScriptProcessorNode.connect(ChannelMergerNode, 0, 1) matches the destination ChannelMergerNode.
PASS The return value of StereoPannerNode.connect(GainNode) matches the destination GainNode.
PASS The return value of StereoPannerNode.connect(BiquadFilterNode, 0) matches the destination BiquadFilterNode.
PASS The return value of StereoPannerNode.connect(ChannelMergerNode, 0, 1) matches the destination ChannelMergerNode.
PASS The return value of WaveShaperNode.connect(GainNode) matches the destination GainNode.
PASS The return value of WaveShaperNode.connect(BiquadFilterNode, 0) matches the destination BiquadFilterNode.
PASS The return value of WaveShaperNode.connect(ChannelMergerNode, 0, 1) matches the destination ChannelMergerNode.
PASS The return value of MediaElementAudioSourceNode.connect(GainNode) matches the destination GainNode.
PASS The return value of MediaElementAudioSourceNode.connect(BiquadFilterNode, 0) matches the destination BiquadFilterNode.
PASS The return value of MediaElementAudioSourceNode.connect(ChannelMergerNode, 0, 1) matches the destination ChannelMergerNode.
PASS The return value of MediaStreamAudioDestinationNode.connect(GainNode) matches the destination GainNode.
PASS The return value of MediaStreamAudioDestinationNode.connect(BiquadFilterNode, 0) matches the destination BiquadFilterNode.
PASS The return value of MediaStreamAudioDestinationNode.connect(ChannelMergerNode, 0, 1) matches the destination ChannelMergerNode.
PASS The return value of MediaStreamAudioSourceNode.connect(GainNode) matches the destination GainNode.
PASS The return value of MediaStreamAudioSourceNode.connect(BiquadFilterNode, 0) matches the destination BiquadFilterNode.
PASS The return value of MediaStreamAudioSourceNode.connect(ChannelMergerNode, 0, 1) matches the destination ChannelMergerNode.
PASS Connecting with an invalid output threw IndexSizeError: Failed to execute 'connect' on 'AudioNode': output index (1) exceeds number of outputs (1)..
PASS Connecting to a node from the different context threw SyntaxError: Failed to execute 'connect' on 'AudioNode': cannot connect to a destination belonging to a different audio context..
PASS The output of chained connection of gain nodes contains only the constant 0.125.
PASS successfullyParsed is true

TEST COMPLETE

Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
<!DOCTYPE html>
<html>

<head>
<script src="../resources/js-test.js"></script>
<script src="resources/compatibility.js"></script>
<script src="resources/audio-testing.js"></script>
</head>

<body>
<script>
description('Test method chaining feature of AudioNode.connect() method.');
window.jsTestIsAsync = true;

// AudioNode dictionary with associated arguments.
var nodeDictionary = [
{ name: 'Analyser' },
{ name: 'BiquadFilter' },
{ name: 'BufferSource' },
{ name: 'ChannelMerger', args: [6] },
{ name: 'ChannelSplitter', args: [6] },
{ name: 'Convolver' },
{ name: 'Delay', args: [] },
{ name: 'DynamicsCompressor' },
{ name: 'Gain' },
{ name: 'Oscillator' },
{ name: 'Panner' },
{ name: 'ScriptProcessor', args: [512, 1, 1] },
{ name: 'StereoPanner' },
{ name: 'WaveShaper' }
];


function verifyReturnedNode(config) {
if (config.destination === config.returned) {
testPassed('The return value of ' + config.desc + ' matches the destination ' +
config.returned.constructor.name + '.');
} else {
testFailed('The return value of ' + config.desc + ' does NOT match the destination ' +
config.destination.constructor.name + '.');
}
}

// Test utility for batch method checking: in order to test 3 method
// signatures, so we create 3 dummy destinations.
// 1) .connect(GainNode)
// 2) .connect(BiquadFilterNode, output)
// 3) .connect(ChannelMergerNode, output, input)
function testConnectMethod(context, options) {
var source = context['create' + options.name].apply(context, options.args);
var sourceName = source.constructor.name;

var destination1 = context.createGain();
verifyReturnedNode({
source: source,
destination: destination1,
returned: source.connect(destination1),
desc: sourceName + '.connect(' + destination1.constructor.name + ')'
});

var destination2 = context.createBiquadFilter();
verifyReturnedNode({
source: source,
destination: destination2,
returned: source.connect(destination2, 0),
desc: sourceName + '.connect(' + destination2.constructor.name + ', 0)'
});

var destination3 = context.createChannelMerger();
verifyReturnedNode({
source: source,
destination: destination3,
returned: source.connect(destination3, 0, 1),
desc: sourceName + '.connect(' + destination3.constructor.name + ', 0, 1)'
});
}


var audit = Audit.createTaskRunner();

// Task: testing entries from the dictionary.
audit.defineTask('from-dictionary', function (done) {
var context = new AudioContext();

for (var i = 0; i < nodeDictionary.length; i++)
testConnectMethod(context, nodeDictionary[i]);

done();
});

// Task: testing Media* nodes.
audit.defineTask('media-group', function (done) {
var context = new AudioContext();

// Test MediaElementSourceNode needs an <audio> element.
var mediaElement = document.createElement('audio');
testConnectMethod(context, { name: 'MediaElementSource', args: [mediaElement] });

testConnectMethod(context, { name: 'MediaStreamDestination' });

// MediaStreamSourceNode requires 'stream' object to be constructed, which
// is a part of MediaStreamDestinationNode.
var streamDestination = context.createMediaStreamDestination();
var stream = streamDestination.stream;
testConnectMethod(context, { name: 'MediaStreamSource', args: [stream] });

done();
});

// Task: test the exception thrown by invalid operation.
audit.defineTask('invalid-operation', function (done) {
var contextA = new AudioContext();
var contextB = new AudioContext();
var gain1 = contextA.createGain();
var gain2 = contextA.createGain();

// Test if the first connection throws correctly. The first gain node does
// not have the second output, so it should throw.
Should('Connecting with an invalid output', function () {
gain1.connect(gain2, 1).connect(contextA.destination);
}).throw('IndexSizeError');

// Test if the second connection throws correctly. The contextB's
// destination is not compatible with the nodes from contextA, thus the
// first connection succeeds but the second one should throw.
Should('Connecting to a node from the different context', function () {
gain1.connect(gain2).connect(contextB.destination);
}).throw('SyntaxError');

done();
});

// Task: verify if the method chaining actually works.
audit.defineTask('verification', function (done) {
// We pick the lowest sample rate allowed to run the test efficiently.
var context = new OfflineAudioContext(1, 128, 3000);

var constantBuffer = createConstantBuffer(context, 1, 1.0);

var source = context.createBufferSource();
source.buffer = constantBuffer;
source.loop = true;

var gain1 = context.createGain();
gain1.gain.value = 0.5;
var gain2 = context.createGain();
gain2.gain.value = 0.25;

source.connect(gain1).connect(gain2).connect(context.destination);
source.start();

context.startRendering().then(function (buffer) {
Should('The output of chained connection of gain nodes', buffer.getChannelData(0))
.beConstantValueOf(0.125);
}).then(done);
});

audit.defineTask('finish', function (done) {
finishJSTest();
done();
});

audit.runTasks(
'from-dictionary',
'media-group',
'invalid-operation',
'verification',
'finish'
);

successfullyParsed = true;
</script>
</body>

</html>
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,16 @@ void AudioBasicInspectorHandler::pullInputs(size_t framesToProcess)
input(0).pull(output(0).bus(), framesToProcess);
}

void AudioBasicInspectorNode::connect(AudioNode* destination, unsigned outputIndex, unsigned inputIndex, ExceptionState& exceptionState)
AudioNode* AudioBasicInspectorNode::connect(AudioNode* destination, unsigned outputIndex, unsigned inputIndex, ExceptionState& exceptionState)
{
ASSERT(isMainThread());

AbstractAudioContext::AutoLocker locker(context());

AudioNode::connect(destination, outputIndex, inputIndex, exceptionState);
static_cast<AudioBasicInspectorHandler&>(handler()).updatePullStatus();

return destination;
}

void AudioBasicInspectorNode::disconnect(unsigned outputIndex, ExceptionState& exceptionState)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ class AudioBasicInspectorNode : public AudioNode {
private:
// TODO(tkent): Should AudioBasicInspectorNode override other variants of
// connect() and disconnect()?
void connect(AudioNode*, unsigned outputIndex, unsigned inputIndex, ExceptionState&) final;
AudioNode* connect(AudioNode*, unsigned outputIndex, unsigned inputIndex, ExceptionState&) final;
void disconnect(unsigned outputIndex, ExceptionState&) final;
};

Expand Down
16 changes: 9 additions & 7 deletions third_party/WebKit/Source/modules/webaudio/AudioNode.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -559,7 +559,7 @@ AbstractAudioContext* AudioNode::context() const
return m_context;
}

void AudioNode::connect(AudioNode* destination, unsigned outputIndex, unsigned inputIndex, ExceptionState& exceptionState)
AudioNode* AudioNode::connect(AudioNode* destination, unsigned outputIndex, unsigned inputIndex, ExceptionState& exceptionState)
{
ASSERT(isMainThread());
AbstractAudioContext::AutoLocker locker(context());
Expand All @@ -568,36 +568,36 @@ void AudioNode::connect(AudioNode* destination, unsigned outputIndex, unsigned i
exceptionState.throwDOMException(
InvalidStateError,
"Cannot connect after the context has been closed.");
return;
return nullptr;
}

if (!destination) {
exceptionState.throwDOMException(
SyntaxError,
"invalid destination node.");
return;
return nullptr;
}

// Sanity check input and output indices.
if (outputIndex >= numberOfOutputs()) {
exceptionState.throwDOMException(
IndexSizeError,
"output index (" + String::number(outputIndex) + ") exceeds number of outputs (" + String::number(numberOfOutputs()) + ").");
return;
return nullptr;
}

if (destination && inputIndex >= destination->numberOfInputs()) {
exceptionState.throwDOMException(
IndexSizeError,
"input index (" + String::number(inputIndex) + ") exceeds number of inputs (" + String::number(destination->numberOfInputs()) + ").");
return;
return nullptr;
}

if (context() != destination->context()) {
exceptionState.throwDOMException(
SyntaxError,
"cannot connect to a destination belonging to a different audio context.");
return;
return nullptr;
}

// ScriptProcessorNodes with 0 output channels can't be connected to any destination. If there
Expand All @@ -607,7 +607,7 @@ void AudioNode::connect(AudioNode* destination, unsigned outputIndex, unsigned i
exceptionState.throwDOMException(
InvalidAccessError,
"cannot connect a ScriptProcessorNode with 0 output channels to any destination node.");
return;
return nullptr;
}

destination->handler().input(inputIndex).connect(handler().output(outputIndex));
Expand All @@ -617,6 +617,8 @@ void AudioNode::connect(AudioNode* destination, unsigned outputIndex, unsigned i

// Let context know that a connection has been made.
context()->incrementConnectionCount();

return destination;
}

void AudioNode::connect(AudioParam* param, unsigned outputIndex, ExceptionState& exceptionState)
Expand Down
2 changes: 1 addition & 1 deletion third_party/WebKit/Source/modules/webaudio/AudioNode.h
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,7 @@ class MODULES_EXPORT AudioNode : public RefCountedGarbageCollectedEventTargetWit
DECLARE_VIRTUAL_TRACE();
AudioHandler& handler() const;

virtual void connect(AudioNode*, unsigned outputIndex, unsigned inputIndex, ExceptionState&);
virtual AudioNode* connect(AudioNode*, unsigned outputIndex, unsigned inputIndex, ExceptionState&);
void connect(AudioParam*, unsigned outputIndex, ExceptionState&);
void disconnect();
virtual void disconnect(unsigned outputIndex, ExceptionState&);
Expand Down
2 changes: 1 addition & 1 deletion third_party/WebKit/Source/modules/webaudio/AudioNode.idl
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ enum ChannelInterpretation {
GarbageCollected,
TypeChecking=Interface
] interface AudioNode : EventTarget {
[RaisesException] void connect(AudioNode destination, optional unsigned long output = 0, optional unsigned long input = 0);
[RaisesException] AudioNode connect(AudioNode destination, optional unsigned long output = 0, optional unsigned long input = 0);
[RaisesException] void connect(AudioParam destination, optional unsigned long output = 0);
void disconnect();
[RaisesException] void disconnect(unsigned long output);
Expand Down

0 comments on commit 166ccf3

Please sign in to comment.