LibWeb/WebAudio: Add DelayNode interface

This commit is contained in:
Tim Ledbetter 2025-01-03 23:42:22 +00:00 committed by Tim Ledbetter
parent 876d26c32e
commit 6c4c925f02
Notes: github-actions[bot] 2025-01-08 15:33:08 +00:00
8 changed files with 262 additions and 0 deletions

View file

@ -797,6 +797,7 @@ set(SOURCES
WebAudio/ChannelMergerNode.cpp
WebAudio/ChannelSplitterNode.cpp
WebAudio/ConstantSourceNode.cpp
WebAudio/DelayNode.cpp
WebAudio/DynamicsCompressorNode.cpp
WebAudio/GainNode.cpp
WebAudio/OfflineAudioContext.cpp

View file

@ -0,0 +1,64 @@
/*
* Copyright (c) 2025, Tim Ledbetter <tim.ledbetter@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/Bindings/DelayNodePrototype.h>
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/WebAudio/BaseAudioContext.h>
#include <LibWeb/WebAudio/DelayNode.h>
namespace Web::WebAudio {
GC_DEFINE_ALLOCATOR(DelayNode);
DelayNode::DelayNode(JS::Realm& realm, GC::Ref<BaseAudioContext> context, DelayOptions const& options)
: AudioNode(realm, context)
, m_delay_time(AudioParam::create(realm, options.delay_time, 0, options.max_delay_time, Bindings::AutomationRate::ARate))
{
}
DelayNode::~DelayNode() = default;
WebIDL::ExceptionOr<GC::Ref<DelayNode>> DelayNode::create(JS::Realm& realm, GC::Ref<BaseAudioContext> context, DelayOptions const& options)
{
return construct_impl(realm, context, options);
}
WebIDL::ExceptionOr<GC::Ref<DelayNode>> DelayNode::construct_impl(JS::Realm& realm, GC::Ref<BaseAudioContext> context, DelayOptions const& options)
{
// https://webaudio.github.io/web-audio-api/#dom-delayoptions-maxdelaytime
// If specified, this value MUST be greater than zero and less than three minutes or a NotSupportedError exception MUST be thrown.
static constexpr double maximum_delay_time_seconds = 180;
if (options.max_delay_time <= 0 || options.max_delay_time >= maximum_delay_time_seconds || isnan(options.max_delay_time))
return WebIDL::NotSupportedError::create(realm, "Max delay time must be between 0 and 180 seconds exclusive"_string);
auto node = realm.create<DelayNode>(realm, context, options);
// Default options for channel count and interpretation
// https://webaudio.github.io/web-audio-api/#DelayNode
AudioNodeDefaultOptions default_options;
default_options.channel_count = 2;
default_options.channel_count_mode = Bindings::ChannelCountMode::Max;
default_options.channel_interpretation = Bindings::ChannelInterpretation::Speakers;
// FIXME: Set tail-time to yes
TRY(node->initialize_audio_node_options(options, default_options));
return node;
}
void DelayNode::initialize(JS::Realm& realm)
{
Base::initialize(realm);
WEB_SET_PROTOTYPE_FOR_INTERFACE(DelayNode);
}
void DelayNode::visit_edges(Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_delay_time);
}
}

View file

@ -0,0 +1,45 @@
/*
* 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/#DelayOptions
struct DelayOptions : AudioNodeOptions {
double max_delay_time { 1 };
double delay_time { 0 };
};
// https://webaudio.github.io/web-audio-api/#DelayNode
class DelayNode final : public AudioNode {
WEB_PLATFORM_OBJECT(DelayNode, AudioNode);
GC_DECLARE_ALLOCATOR(DelayNode);
public:
virtual ~DelayNode() override;
static WebIDL::ExceptionOr<GC::Ref<DelayNode>> create(JS::Realm&, GC::Ref<BaseAudioContext>, DelayOptions const& = {});
static WebIDL::ExceptionOr<GC::Ref<DelayNode>> construct_impl(JS::Realm&, GC::Ref<BaseAudioContext>, DelayOptions const& = {});
virtual WebIDL::UnsignedLong number_of_inputs() override { return 1; }
virtual WebIDL::UnsignedLong number_of_outputs() override { return 1; }
GC::Ref<AudioParam const> delay_time() const { return m_delay_time; }
private:
DelayNode(JS::Realm&, GC::Ref<BaseAudioContext>, DelayOptions const&);
virtual void initialize(JS::Realm&) override;
virtual void visit_edges(Cell::Visitor&) override;
// https://webaudio.github.io/web-audio-api/#dom-delaynode-delaytime
GC::Ref<AudioParam> m_delay_time;
};
}

View file

@ -0,0 +1,16 @@
#import <WebAudio/AudioNode.idl>
#import <WebAudio/AudioParam.idl>
#import <WebAudio/BaseAudioContext.idl>
// https://webaudio.github.io/web-audio-api/#DelayOptions
dictionary DelayOptions : AudioNodeOptions {
double maxDelayTime = 1;
double delayTime = 0;
};
// https://webaudio.github.io/web-audio-api/#DelayNode
[Exposed=Window]
interface DelayNode : AudioNode {
constructor (BaseAudioContext context, optional DelayOptions options = {});
readonly attribute AudioParam delayTime;
};

View file

@ -372,6 +372,7 @@ libweb_js_bindings(WebAudio/GainNode)
libweb_js_bindings(WebAudio/ChannelMergerNode)
libweb_js_bindings(WebAudio/ChannelSplitterNode)
libweb_js_bindings(WebAudio/ConstantSourceNode)
libweb_js_bindings(WebAudio/DelayNode)
libweb_js_bindings(WebAudio/OfflineAudioContext)
libweb_js_bindings(WebAudio/OscillatorNode)
libweb_js_bindings(WebAudio/PannerNode)

View file

@ -93,6 +93,7 @@ DataTransferItemList
DataView
Date
DecompressionStream
DelayNode
DisposableStack
Document
DocumentFragment

View file

@ -0,0 +1,58 @@
Harness status: OK
Found 53 tests
53 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 DelayNode() threw TypeError: "DelayNode() needs one argument".
Pass new DelayNode(1) threw TypeError: "Not an object of type BaseAudioContext".
Pass new DelayNode(context, 42) threw TypeError: "Not an object of type DelayOptions".
Pass < [invalid constructor] All assertions passed. (total 3 assertions)
Pass > [default constructor]
Pass node0 = new DelayNode(context) did not throw an exception.
Pass node0 instanceof DelayNode is equal to true.
Pass node0.numberOfInputs is equal to 1.
Pass node0.numberOfOutputs is equal to 1.
Pass node0.channelCount is equal to 2.
Pass node0.channelCountMode is equal to max.
Pass node0.channelInterpretation is equal to speakers.
Pass node0.delayTime.value is equal to 0.
Pass < [default constructor] All assertions passed. (total 8 assertions)
Pass > [test AudioNodeOptions]
Pass new DelayNode(c, {channelCount: 17}) did not throw an exception.
Pass node.channelCount is equal to 17.
Pass new DelayNode(c, {channelCount: 0}) threw NotSupportedError: "Invalid channel count".
Pass new DelayNode(c, {channelCount: 99}) threw NotSupportedError: "Invalid channel count".
Pass new DelayNode(c, {channelCountMode: "max"} did not throw an exception.
Pass node.channelCountMode is equal to max.
Pass new DelayNode(c, {channelCountMode: "max"}) did not throw an exception.
Pass node.channelCountMode after valid setter is equal to max.
Pass new DelayNode(c, {channelCountMode: "clamped-max"}) did not throw an exception.
Pass node.channelCountMode after valid setter is equal to clamped-max.
Pass new DelayNode(c, {channelCountMode: "explicit"}) did not throw an exception.
Pass node.channelCountMode after valid setter is equal to explicit.
Pass new DelayNode(c, {channelCountMode: "foobar"} threw TypeError: "Invalid value 'foobar' for enumeration type 'ChannelCountMode'".
Pass node.channelCountMode after invalid setter is equal to explicit.
Pass new DelayNode(c, {channelInterpretation: "speakers"}) did not throw an exception.
Pass node.channelInterpretation is equal to speakers.
Pass new DelayNode(c, {channelInterpretation: "discrete"}) did not throw an exception.
Pass node.channelInterpretation is equal to discrete.
Pass new DelayNode(c, {channelInterpretation: "foobar"}) threw TypeError: "Invalid value 'foobar' for enumeration type 'ChannelInterpretation'".
Pass node.channelInterpretation after invalid setter is equal to discrete.
Pass < [test AudioNodeOptions] All assertions passed. (total 20 assertions)
Pass > [constructor options]
Pass node1 = new DelayNode(c, {"delayTime":0.5,"maxDelayTime":1.5}) did not throw an exception.
Pass node1.delayTime.value is equal to 0.5.
Pass node1.delayTime.maxValue is equal to 1.5.
Pass < [constructor options] All assertions passed. (total 3 assertions)
Pass # AUDIT TASK RUNNER FINISHED: 5 tasks ran successfully.

View file

@ -0,0 +1,76 @@
<!DOCTYPE html>
<html>
<head>
<title>
Test Constructor: Delay
</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, 'DelayNode', context);
task.done();
});
audit.define('default constructor', (task, should) => {
let prefix = 'node0';
let node = testDefaultConstructor(should, 'DelayNode', context, {
prefix: prefix,
numberOfInputs: 1,
numberOfOutputs: 1,
channelCount: 2,
channelCountMode: 'max',
channelInterpretation: 'speakers'
});
testDefaultAttributes(
should, node, prefix, [{name: 'delayTime', value: 0}]);
task.done();
});
audit.define('test AudioNodeOptions', (task, should) => {
testAudioNodeOptions(should, context, 'DelayNode');
task.done();
});
audit.define('constructor options', (task, should) => {
let node;
let options = {
delayTime: 0.5,
maxDelayTime: 1.5,
};
should(
() => {
node = new DelayNode(context, options);
},
'node1 = new DelayNode(c, ' + JSON.stringify(options) + ')')
.notThrow();
should(node.delayTime.value, 'node1.delayTime.value')
.beEqualTo(options.delayTime);
should(node.delayTime.maxValue, 'node1.delayTime.maxValue')
.beEqualTo(options.maxDelayTime);
task.done();
});
audit.run();
</script>
</body>
</html>