LibWeb: Add ChannelSplitterNode interface

This commit is contained in:
Tim Ledbetter 2025-01-01 22:00:52 +00:00 committed by Tim Ledbetter
parent 819178a49e
commit e564d25ffb
Notes: github-actions[bot] 2025-01-02 11:40:31 +00:00
9 changed files with 315 additions and 1 deletions
Libraries/LibWeb
Tests/LibWeb/Text
expected
all-window-properties.txt
wpt-import/webaudio/the-audio-api/the-channelsplitternode-interface
input/wpt-import/webaudio/the-audio-api/the-channelsplitternode-interface

View file

@ -793,6 +793,7 @@ set(SOURCES
WebAudio/BaseAudioContext.cpp
WebAudio/BiquadFilterNode.cpp
WebAudio/ChannelMergerNode.cpp
WebAudio/ChannelSplitterNode.cpp
WebAudio/DynamicsCompressorNode.cpp
WebAudio/GainNode.cpp
WebAudio/OfflineAudioContext.cpp

View file

@ -65,7 +65,7 @@ public:
virtual WebIDL::ExceptionOr<void> set_channel_count_mode(Bindings::ChannelCountMode);
Bindings::ChannelCountMode channel_count_mode();
WebIDL::ExceptionOr<void> set_channel_interpretation(Bindings::ChannelInterpretation);
virtual WebIDL::ExceptionOr<void> set_channel_interpretation(Bindings::ChannelInterpretation);
Bindings::ChannelInterpretation channel_interpretation();
WebIDL::ExceptionOr<void> initialize_audio_node_options(AudioNodeOptions const& given_options, AudioNodeDefaultOptions const& default_options);

View file

@ -0,0 +1,87 @@
/*
* Copyright (c) 2025, Tim Ledbetter <tim.ledbetter@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/Bindings/ChannelSplitterNodePrototype.h>
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/WebAudio/BaseAudioContext.h>
#include <LibWeb/WebAudio/ChannelSplitterNode.h>
namespace Web::WebAudio {
GC_DEFINE_ALLOCATOR(ChannelSplitterNode);
ChannelSplitterNode::ChannelSplitterNode(JS::Realm& realm, GC::Ref<BaseAudioContext> context, ChannelSplitterOptions const& options)
: AudioNode(realm, context)
, m_number_of_outputs(options.number_of_outputs)
{
}
ChannelSplitterNode::~ChannelSplitterNode() = default;
WebIDL::ExceptionOr<GC::Ref<ChannelSplitterNode>> ChannelSplitterNode::create(JS::Realm& realm, GC::Ref<BaseAudioContext> context, ChannelSplitterOptions const& options)
{
return construct_impl(realm, context, options);
}
WebIDL::ExceptionOr<GC::Ref<ChannelSplitterNode>> ChannelSplitterNode::construct_impl(JS::Realm& realm, GC::Ref<BaseAudioContext> context, ChannelSplitterOptions const& options)
{
// https://webaudio.github.io/web-audio-api/#dom-baseaudiocontext-createchannelsplitter
// An IndexSizeError exception MUST be thrown if numberOfOutputs is less than 1 or is greater than the number of supported channels.
if (options.number_of_outputs < 1 || options.number_of_outputs > BaseAudioContext::MAX_NUMBER_OF_CHANNELS)
return WebIDL::IndexSizeError::create(realm, "Invalid number of outputs"_string);
auto node = realm.create<ChannelSplitterNode>(realm, context, options);
// Default options for channel count and interpretation
// https://webaudio.github.io/web-audio-api/#ChannelSplitterNode
AudioNodeDefaultOptions default_options;
default_options.channel_count_mode = Bindings::ChannelCountMode::Explicit;
default_options.channel_interpretation = Bindings::ChannelInterpretation::Discrete;
default_options.channel_count = node->number_of_outputs();
// FIXME: Set tail-time to no
TRY(node->initialize_audio_node_options(options, default_options));
return node;
}
void ChannelSplitterNode::initialize(JS::Realm& realm)
{
AudioNode::initialize(realm);
WEB_SET_PROTOTYPE_FOR_INTERFACE(ChannelSplitterNode);
}
WebIDL::ExceptionOr<void> ChannelSplitterNode::set_channel_count(WebIDL::UnsignedLong channel_count)
{
// https://webaudio.github.io/web-audio-api/#audionode-channelcount-constraints
// The channel count cannot be changed, and an InvalidStateError exception MUST be thrown for any attempt to change the value.
if (channel_count != m_number_of_outputs)
return WebIDL::InvalidStateError::create(realm(), "Channel count must be equal to number of outputs"_string);
return AudioNode::set_channel_count(channel_count);
}
WebIDL::ExceptionOr<void> ChannelSplitterNode::set_channel_count_mode(Bindings::ChannelCountMode channel_count_mode)
{
// https://webaudio.github.io/web-audio-api/#audionode-channelcountmode-constraints
// The channel count mode cannot be changed from "explicit" and an InvalidStateError exception MUST be thrown for any attempt to change the value.
if (channel_count_mode != Bindings::ChannelCountMode::Explicit)
return WebIDL::InvalidStateError::create(realm(), "Channel count mode must be 'explicit'"_string);
return AudioNode::set_channel_count_mode(channel_count_mode);
}
WebIDL::ExceptionOr<void> ChannelSplitterNode::set_channel_interpretation(Bindings::ChannelInterpretation channel_interpretation)
{
// https://webaudio.github.io/web-audio-api/#audionode-channelinterpretation-constraints
// The channel intepretation can not be changed from "discrete" and a InvalidStateError exception MUST be thrown for any attempt to change the value.
if (channel_interpretation != Bindings::ChannelInterpretation::Discrete)
return WebIDL::InvalidStateError::create(realm(), "Channel interpretation must be 'discrete'"_string);
return AudioNode::set_channel_interpretation(channel_interpretation);
}
}

View file

@ -0,0 +1,44 @@
/*
* Copyright (c) 2025, Tim Ledbetter <tim.ledbetter@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibWeb/WebAudio/AudioNode.h>
namespace Web::WebAudio {
// https://webaudio.github.io/web-audio-api/#ChannelSplitterOptions
struct ChannelSplitterOptions : AudioNodeOptions {
WebIDL::UnsignedLong number_of_outputs { 6 };
};
/// https://webaudio.github.io/web-audio-api/#ChannelSplitterNode
class ChannelSplitterNode final : public AudioNode {
WEB_PLATFORM_OBJECT(ChannelSplitterNode, AudioNode);
GC_DECLARE_ALLOCATOR(ChannelSplitterNode);
public:
virtual ~ChannelSplitterNode() override;
static WebIDL::ExceptionOr<GC::Ref<ChannelSplitterNode>> create(JS::Realm&, GC::Ref<BaseAudioContext>, ChannelSplitterOptions const& = {});
static WebIDL::ExceptionOr<GC::Ref<ChannelSplitterNode>> construct_impl(JS::Realm&, GC::Ref<BaseAudioContext>, ChannelSplitterOptions const& = {});
virtual WebIDL::UnsignedLong number_of_inputs() override { return 1; }
virtual WebIDL::UnsignedLong number_of_outputs() override { return m_number_of_outputs; }
virtual WebIDL::ExceptionOr<void> set_channel_count(WebIDL::UnsignedLong) override;
virtual WebIDL::ExceptionOr<void> set_channel_count_mode(Bindings::ChannelCountMode) override;
virtual WebIDL::ExceptionOr<void> set_channel_interpretation(Bindings::ChannelInterpretation) override;
private:
ChannelSplitterNode(JS::Realm&, GC::Ref<BaseAudioContext>, ChannelSplitterOptions const&);
virtual void initialize(JS::Realm&) override;
WebIDL::UnsignedLong m_number_of_outputs;
};
}

View file

@ -0,0 +1,13 @@
#import <WebAudio/AudioNode.idl>
#import <WebAudio/BaseAudioContext.idl>
// https://webaudio.github.io/web-audio-api/#ChannelSplitterNode
[Exposed=Window]
interface ChannelSplitterNode : AudioNode {
constructor (BaseAudioContext context, optional ChannelSplitterOptions options = {});
};
// https://webaudio.github.io/web-audio-api/#ChannelSplitterOptions
dictionary ChannelSplitterOptions : AudioNodeOptions {
unsigned long numberOfOutputs = 6;
};

View file

@ -370,6 +370,7 @@ libweb_js_bindings(WebAudio/BiquadFilterNode)
libweb_js_bindings(WebAudio/DynamicsCompressorNode)
libweb_js_bindings(WebAudio/GainNode)
libweb_js_bindings(WebAudio/ChannelMergerNode)
libweb_js_bindings(WebAudio/ChannelSplitterNode)
libweb_js_bindings(WebAudio/OfflineAudioContext)
libweb_js_bindings(WebAudio/OscillatorNode)
libweb_js_bindings(WebAudio/PannerNode)

View file

@ -57,6 +57,7 @@ CanvasGradient
CanvasPattern
CanvasRenderingContext2D
ChannelMergerNode
ChannelSplitterNode
CharacterData
Clipboard
ClipboardEvent

View file

@ -0,0 +1,52 @@
Harness status: OK
Found 47 tests
47 Pass
Pass # AUDIT TASK RUNNER STARTED.
Pass Executing "initialize"
Pass Executing "invalid constructor"
Pass Executing "default constructor"
Pass Executing "test AudioNodeOptions"
Pass Executing "constructor options"
Pass Audit report
Pass > [initialize]
Pass context = new OfflineAudioContext(...) did not throw an exception.
Pass < [initialize] All assertions passed. (total 1 assertions)
Pass > [invalid constructor]
Pass new ChannelSplitterNode() threw TypeError: "ChannelSplitterNode() needs one argument".
Pass new ChannelSplitterNode(1) threw TypeError: "Not an object of type BaseAudioContext".
Pass new ChannelSplitterNode(context, 42) threw TypeError: "Not an object of type ChannelSplitterOptions".
Pass < [invalid constructor] All assertions passed. (total 3 assertions)
Pass > [default constructor]
Pass node0 = new ChannelSplitterNode(context) did not throw an exception.
Pass node0 instanceof ChannelSplitterNode is equal to true.
Pass node0.numberOfInputs is equal to 1.
Pass node0.numberOfOutputs is equal to 6.
Pass node0.channelCount is equal to 6.
Pass node0.channelCountMode is equal to explicit.
Pass node0.channelInterpretation is equal to discrete.
Pass < [default constructor] All assertions passed. (total 7 assertions)
Pass > [test AudioNodeOptions]
Pass new ChannelSplitterNode(c, {channelCount: 6}) did not throw an exception.
Pass node.channelCount is equal to 6.
Pass new ChannelSplitterNode(c, {channelCount: 7}) threw InvalidStateError: "Channel count must be equal to number of outputs".
Pass (new ChannelSplitterNode(c, {channelCount: 6})).channelCount = 6 did not throw an exception.
Pass new ChannelSplitterNode(c, {channelCountMode: "explicit"} did not throw an exception.
Pass node.channelCountMode is equal to explicit.
Pass new ChannelSplitterNode(c, {channelCountMode: "max"}) threw InvalidStateError: "Channel count mode must be 'explicit'".
Pass new ChannelSplitterNode(c, {channelCountMode: "clamped-max"}) threw InvalidStateError: "Channel count mode must be 'explicit'".
Pass (new ChannelSplitterNode(c, {channelCountMode: "explicit"})).channelCountMode = "explicit" did not throw an exception.
Pass new ChannelSplitterNode(c, {channelInterpretation: "speakers"}) threw InvalidStateError: "Channel interpretation must be 'discrete'".
Pass (new ChannelSplitterNode(c, {channelInterpretation: "discrete"})).channelInterpretation = "discrete" did not throw an exception.
Pass < [test AudioNodeOptions] All assertions passed. (total 11 assertions)
Pass > [constructor options]
Pass node1 = new ChannelSplitterNode(context, {"numberOfInputs":3,"numberOfOutputs":9,"channelInterpretation":"discrete"}) did not throw an exception.
Pass node1.numberOfInputs is equal to 1.
Pass node1.numberOfOutputs is equal to 9.
Pass node1.channelInterpretation is equal to discrete.
Pass new ChannelSplitterNode(c, {"numberOfOutputs":99}) threw IndexSizeError: "Invalid number of outputs".
Pass new ChannelSplitterNode(c, {"channelCount":3}) threw InvalidStateError: "Channel count must be equal to number of outputs".
Pass new ChannelSplitterNode(c, {"channelCountMode":"max"}) threw InvalidStateError: "Channel count mode must be 'explicit'".
Pass < [constructor options] All assertions passed. (total 7 assertions)
Pass # AUDIT TASK RUNNER FINISHED: 5 tasks ran successfully.

View file

@ -0,0 +1,115 @@
<!DOCTYPE html>
<html>
<head>
<title>
Test Constructor: ChannelSplitter
</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>
<script src="../../../webaudio/resources/audionodeoptions.js"></script>
</head>
<body>
<script id="layout-test-code">
let context;
let audit = Audit.createTaskRunner();
audit.define('initialize', (task, should) => {
context = initializeContext(should);
task.done();
});
audit.define('invalid constructor', (task, should) => {
testInvalidConstructor(should, 'ChannelSplitterNode', context);
task.done();
});
audit.define('default constructor', (task, should) => {
testDefaultConstructor(should, 'ChannelSplitterNode', context, {
prefix: 'node0',
numberOfInputs: 1,
numberOfOutputs: 6,
channelCount: 6,
channelCountMode: 'explicit',
channelInterpretation: 'discrete'
});
task.done();
});
audit.define('test AudioNodeOptions', (task, should) => {
testAudioNodeOptions(should, context, 'ChannelSplitterNode', {
channelCount: {
value: 6,
isFixed: true,
exceptionType: 'InvalidStateError'
},
channelCountMode: {
value: 'explicit',
isFixed: true,
exceptionType: 'InvalidStateError'
},
channelInterpretation: {
value: 'discrete',
isFixed: true,
exceptionType: 'InvalidStateError'
},
});
task.done();
});
audit.define('constructor options', (task, should) => {
let node;
let options = {
numberOfInputs: 3,
numberOfOutputs: 9,
channelInterpretation: 'discrete'
};
should(
() => {
node = new ChannelSplitterNode(context, options);
},
'node1 = new ChannelSplitterNode(context, ' +
JSON.stringify(options) + ')')
.notThrow();
should(node.numberOfInputs, 'node1.numberOfInputs').beEqualTo(1);
should(node.numberOfOutputs, 'node1.numberOfOutputs')
.beEqualTo(options.numberOfOutputs);
should(node.channelInterpretation, 'node1.channelInterpretation')
.beEqualTo(options.channelInterpretation);
options = {numberOfOutputs: 99};
should(
() => {
node = new ChannelSplitterNode(context, options);
},
'new ChannelSplitterNode(c, ' + JSON.stringify(options) + ')')
.throw(DOMException, 'IndexSizeError');
options = {channelCount: 3};
should(
() => {
node = new ChannelSplitterNode(context, options);
},
'new ChannelSplitterNode(c, ' + JSON.stringify(options) + ')')
.throw(DOMException, 'InvalidStateError');
options = {channelCountMode: 'max'};
should(
() => {
node = new ChannelSplitterNode(context, options);
},
'new ChannelSplitterNode(c, ' + JSON.stringify(options) + ')')
.throw(DOMException, 'InvalidStateError');
task.done();
});
audit.run();
</script>
</body>
</html>