LibWeb: Initialize OfflineAudioContext with correct defaults

This commit is contained in:
Tim Ledbetter 2025-01-07 23:24:10 +00:00 committed by Sam Atkins
parent 2cac0dc20c
commit 27dbe49f00
Notes: github-actions[bot] 2025-01-08 11:25:09 +00:00
11 changed files with 288 additions and 22 deletions

View file

@ -11,6 +11,7 @@
#include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h>
#include <LibWeb/HTML/Window.h>
#include <LibWeb/WebAudio/AudioContext.h>
#include <LibWeb/WebAudio/AudioDestinationNode.h>
#include <LibWeb/WebIDL/Promise.h>
namespace Web::WebAudio {
@ -20,7 +21,9 @@ GC_DEFINE_ALLOCATOR(AudioContext);
// https://webaudio.github.io/web-audio-api/#dom-audiocontext-audiocontext
WebIDL::ExceptionOr<GC::Ref<AudioContext>> AudioContext::construct_impl(JS::Realm& realm, AudioContextOptions const& context_options)
{
return realm.create<AudioContext>(realm, context_options);
auto context = realm.create<AudioContext>(realm, context_options);
context->m_destination = TRY(AudioDestinationNode::construct_impl(realm, context));
return context;
}
AudioContext::AudioContext(JS::Realm& realm, AudioContextOptions const& context_options)

View file

@ -17,8 +17,8 @@ namespace Web::WebAudio {
GC_DEFINE_ALLOCATOR(AudioDestinationNode);
AudioDestinationNode::AudioDestinationNode(JS::Realm& realm, GC::Ref<BaseAudioContext> context)
: AudioNode(realm, context)
AudioDestinationNode::AudioDestinationNode(JS::Realm& realm, GC::Ref<BaseAudioContext> context, WebIDL::UnsignedLong channel_count)
: AudioNode(realm, context, channel_count)
{
}
@ -31,9 +31,21 @@ WebIDL::UnsignedLong AudioDestinationNode::max_channel_count()
return 2;
}
GC::Ref<AudioDestinationNode> AudioDestinationNode::construct_impl(JS::Realm& realm, GC::Ref<BaseAudioContext> context)
WebIDL::ExceptionOr<GC::Ref<AudioDestinationNode>> AudioDestinationNode::construct_impl(JS::Realm& realm, GC::Ref<BaseAudioContext> context, WebIDL::UnsignedLong channel_count)
{
return realm.create<AudioDestinationNode>(realm, context);
auto node = realm.create<AudioDestinationNode>(realm, context, channel_count);
// Default options for channel count and interpretation
// https://webaudio.github.io/web-audio-api/#AudioDestinationNode
AudioNodeDefaultOptions default_options;
default_options.channel_count_mode = Bindings::ChannelCountMode::Explicit;
default_options.channel_interpretation = Bindings::ChannelInterpretation::Speakers;
default_options.channel_count = channel_count;
// FIXME: Set tail-time to no
TRY(node->initialize_audio_node_options({}, default_options));
return node;
}
void AudioDestinationNode::initialize(JS::Realm& realm)
@ -50,6 +62,9 @@ void AudioDestinationNode::visit_edges(Cell::Visitor& visitor)
// https://webaudio.github.io/web-audio-api/#dom-audionode-channelcount
WebIDL::ExceptionOr<void> AudioDestinationNode::set_channel_count(WebIDL::UnsignedLong channel_count)
{
if (channel_count == this->channel_count())
return {};
// The behavior depends on whether the destination node is the destination of an AudioContext
// or OfflineAudioContext:

View file

@ -27,10 +27,10 @@ public:
WebIDL::UnsignedLong number_of_outputs() override { return 1; }
WebIDL::ExceptionOr<void> set_channel_count(WebIDL::UnsignedLong) override;
static GC::Ref<AudioDestinationNode> construct_impl(JS::Realm&, GC::Ref<BaseAudioContext>);
static WebIDL::ExceptionOr<GC::Ref<AudioDestinationNode>> construct_impl(JS::Realm& realm, GC::Ref<BaseAudioContext> context, WebIDL::UnsignedLong channel_count = 2);
protected:
AudioDestinationNode(JS::Realm&, GC::Ref<BaseAudioContext>);
AudioDestinationNode(JS::Realm&, GC::Ref<BaseAudioContext>, WebIDL::UnsignedLong channel_count);
virtual void initialize(JS::Realm&) override;
virtual void visit_edges(Cell::Visitor&) override;

View file

@ -12,9 +12,10 @@ namespace Web::WebAudio {
GC_DEFINE_ALLOCATOR(AudioNode);
AudioNode::AudioNode(JS::Realm& realm, GC::Ref<BaseAudioContext> context)
AudioNode::AudioNode(JS::Realm& realm, GC::Ref<BaseAudioContext> context, WebIDL::UnsignedLong channel_count)
: DOM::EventTarget(realm)
, m_context(context)
, m_channel_count(channel_count)
{
}

View file

@ -71,7 +71,7 @@ public:
WebIDL::ExceptionOr<void> initialize_audio_node_options(AudioNodeOptions const& given_options, AudioNodeDefaultOptions const& default_options);
protected:
AudioNode(JS::Realm&, GC::Ref<BaseAudioContext>);
AudioNode(JS::Realm&, GC::Ref<BaseAudioContext>, WebIDL::UnsignedLong channel_count = 2);
virtual void initialize(JS::Realm&) override;
virtual void visit_edges(Cell::Visitor&) override;

View file

@ -28,7 +28,6 @@ namespace Web::WebAudio {
BaseAudioContext::BaseAudioContext(JS::Realm& realm, float sample_rate)
: DOM::EventTarget(realm)
, m_destination(AudioDestinationNode::construct_impl(realm, *this))
, m_sample_rate(sample_rate)
, m_listener(AudioListener::create(realm))
{

View file

@ -40,7 +40,7 @@ public:
static constexpr float MIN_SAMPLE_RATE { 8000 };
static constexpr float MAX_SAMPLE_RATE { 192000 };
GC::Ref<AudioDestinationNode> destination() const { return m_destination; }
GC::Ref<AudioDestinationNode> destination() const { return *m_destination; }
float sample_rate() const { return m_sample_rate; }
double current_time() const { return m_current_time; }
GC::Ref<AudioListener> listener() const { return m_listener; }
@ -80,7 +80,7 @@ protected:
virtual void initialize(JS::Realm&) override;
virtual void visit_edges(Cell::Visitor&) override;
GC::Ref<AudioDestinationNode> m_destination;
GC::Ptr<AudioDestinationNode> m_destination;
Vector<GC::Ref<WebIDL::Promise>> m_pending_promises;
private:

View file

@ -7,6 +7,7 @@
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/HTML/EventNames.h>
#include <LibWeb/HTML/Window.h>
#include <LibWeb/WebAudio/AudioDestinationNode.h>
#include <LibWeb/WebAudio/OfflineAudioContext.h>
namespace Web::WebAudio {
@ -26,7 +27,9 @@ WebIDL::ExceptionOr<GC::Ref<OfflineAudioContext>> OfflineAudioContext::construct
// A NotSupportedError exception MUST be thrown if any of the arguments is negative, zero, or outside its nominal range.
TRY(verify_audio_options_inside_nominal_range(realm, number_of_channels, length, sample_rate));
return realm.create<OfflineAudioContext>(realm, number_of_channels, length, sample_rate);
auto context = realm.create<OfflineAudioContext>(realm, length, sample_rate);
context->m_destination = TRY(AudioDestinationNode::construct_impl(realm, context, number_of_channels));
return context;
}
OfflineAudioContext::~OfflineAudioContext() = default;
@ -67,16 +70,10 @@ void OfflineAudioContext::set_oncomplete(GC::Ptr<WebIDL::CallbackType> value)
set_event_handler_attribute(HTML::EventNames::complete, value);
}
OfflineAudioContext::OfflineAudioContext(JS::Realm& realm, OfflineAudioContextOptions const&)
: BaseAudioContext(realm)
{
}
OfflineAudioContext::OfflineAudioContext(JS::Realm& realm, WebIDL::UnsignedLong number_of_channels, WebIDL::UnsignedLong length, float sample_rate)
OfflineAudioContext::OfflineAudioContext(JS::Realm& realm, WebIDL::UnsignedLong length, float sample_rate)
: BaseAudioContext(realm, sample_rate)
, m_length(length)
{
(void)number_of_channels;
}
void OfflineAudioContext::initialize(JS::Realm& realm)

View file

@ -42,8 +42,7 @@ public:
void set_oncomplete(GC::Ptr<WebIDL::CallbackType>);
private:
OfflineAudioContext(JS::Realm&, OfflineAudioContextOptions const&);
OfflineAudioContext(JS::Realm&, WebIDL::UnsignedLong number_of_channels, WebIDL::UnsignedLong length, float sample_rate);
OfflineAudioContext(JS::Realm&, WebIDL::UnsignedLong length, float sample_rate);
virtual void initialize(JS::Realm&) override;
virtual void visit_edges(Cell::Visitor&) override;

View file

@ -0,0 +1,49 @@
Harness status: OK
Found 44 tests
44 Pass
Pass # AUDIT TASK RUNNER STARTED.
Pass Executing "basic"
Pass Executing "options-1"
Pass Executing "options-2"
Pass Executing "options-3"
Pass Audit report
Pass > [basic] Old-style constructor
Pass new OfflineAudioContext(3) threw TypeError: "Not an object of type OfflineAudioContextOptions".
Pass new OfflineAudioContext(3, 42) threw TypeError: "Overload resolution failed".
Pass context = new OfflineAudioContext(3, 42, 12345) did not throw an exception.
Pass context.length is equal to 42.
Pass context.sampleRate is equal to 12345.
Pass context.destination.channelCount is equal to 3.
Pass context.destination.channelCountMode is equal to explicit.
Pass context.destination.channelInterpretation is equal to speakers.
Pass < [basic] All assertions passed. (total 8 assertions)
Pass > [options-1] Required options
Pass new OfflineAudioContext() threw TypeError: "Overload resolution failed".
Pass new OfflineAudioContext({}) threw TypeError: "Required property length is missing or undefined".
Pass new OfflineAudioContext({"length":42}) threw TypeError: "Required property sampleRate is missing or undefined".
Pass new OfflineAudioContext({"sampleRate":12345}) threw TypeError: "Required property length is missing or undefined".
Pass c2 = new OfflineAudioContext({"length":42,"sampleRate":12345}) did not throw an exception.
Pass c2.destination.channelCount is equal to 1.
Pass c2.length is equal to 42.
Pass c2.sampleRate is equal to 12345.
Pass c2.destination.channelCountMode is equal to explicit.
Pass c2.destination.channelInterpretation is equal to speakers.
Pass < [options-1] All assertions passed. (total 10 assertions)
Pass > [options-2] Invalid options
Pass new OfflineAudioContext({"length":42,"sampleRate":8000,"numberOfChannels":33}) threw NotSupportedError: "Number of channels is greater than allowed range".
Pass new OfflineAudioContext({"length":0,"sampleRate":8000}) threw NotSupportedError: "Length of buffer must be at least 1".
Pass new OfflineAudioContext({"length":1,"sampleRate":1}) threw NotSupportedError: "Sample rate is outside of allowed range".
Pass < [options-2] All assertions passed. (total 3 assertions)
Pass > [options-3] Valid options
Pass c = new OfflineAudioContext{"length":1,"sampleRate":8000}) did not throw an exception.
Pass c.length is equal to 1.
Pass c.sampleRate is equal to 8000.
Pass c.destination.channelCount is equal to 1.
Pass c.destination.channelCountMode is equal to explicit.
Pass c.destination.channelCountMode is equal to speakers.
Pass c = new OfflineAudioContext{"length":1,"sampleRate":8000,"numberOfChannels":7}) did not throw an exception.
Pass c.destination.channelCount is equal to 7.
Pass < [options-3] All assertions passed. (total 8 assertions)
Pass # AUDIT TASK RUNNER FINISHED: 4 tasks ran successfully.

View file

@ -0,0 +1,203 @@
<!doctype html>
<html>
<head>
<title>Test Constructor: OfflineAudioContext</title>
<script src="../../../resources/testharness.js"></script>
<script src="../../../resources/testharnessreport.js"></script>
<script src="../../../webaudio/resources/audit.js"></script>
<script src="../../../webaudio/resources/audit-util.js"></script>
<script src="../../../webaudio/resources/audionodeoptions.js"></script>
</head>
<body>
<script>
let audit = Audit.createTaskRunner();
// Just a simple test of the 3-arg constructor; This should be
// well-covered by other layout tests that use the 3-arg constructor.
audit.define(
{label: 'basic', description: 'Old-style constructor'},
(task, should) => {
let context;
// First and only arg should be a dictionary.
should(() => {
new OfflineAudioContext(3);
}, 'new OfflineAudioContext(3)').throw(TypeError);
// Constructor needs 1 or 3 args, so 2 should throw.
should(() => {
new OfflineAudioContext(3, 42);
}, 'new OfflineAudioContext(3, 42)').throw(TypeError);
// Valid constructor
should(() => {
context = new OfflineAudioContext(3, 42, 12345);
}, 'context = new OfflineAudioContext(3, 42, 12345)').notThrow();
// Verify that the context was constructed correctly.
should(context.length, 'context.length').beEqualTo(42);
should(context.sampleRate, 'context.sampleRate').beEqualTo(12345);
should(
context.destination.channelCount,
'context.destination.channelCount')
.beEqualTo(3);
should(
context.destination.channelCountMode,
'context.destination.channelCountMode')
.beEqualTo('explicit');
should(
context.destination.channelInterpretation,
'context.destination.channelInterpretation')
.beEqualTo('speakers');
task.done();
});
// Test constructor throws an error if the required members of the
// dictionary are not given.
audit.define(
{label: 'options-1', description: 'Required options'},
(task, should) => {
let context2;
// No args should throw
should(() => {
new OfflineAudioContext();
}, 'new OfflineAudioContext()').throw(TypeError);
// Empty OfflineAudioContextOptions should throw
should(() => {
new OfflineAudioContext({});
}, 'new OfflineAudioContext({})').throw(TypeError);
let options = {length: 42};
// sampleRate is required.
should(
() => {
new OfflineAudioContext(options);
},
'new OfflineAudioContext(' + JSON.stringify(options) + ')')
.throw(TypeError);
options = {sampleRate: 12345};
// length is required.
should(
() => {
new OfflineAudioContext(options);
},
'new OfflineAudioContext(' + JSON.stringify(options) + ')')
.throw(TypeError);
// Valid constructor. Verify that the resulting context has the
// correct values.
options = {length: 42, sampleRate: 12345};
should(
() => {
context2 = new OfflineAudioContext(options);
},
'c2 = new OfflineAudioContext(' + JSON.stringify(options) + ')')
.notThrow();
should(
context2.destination.channelCount,
'c2.destination.channelCount')
.beEqualTo(1);
should(context2.length, 'c2.length').beEqualTo(options.length);
should(context2.sampleRate, 'c2.sampleRate')
.beEqualTo(options.sampleRate);
should(
context2.destination.channelCountMode,
'c2.destination.channelCountMode')
.beEqualTo('explicit');
should(
context2.destination.channelInterpretation,
'c2.destination.channelInterpretation')
.beEqualTo('speakers');
task.done();
});
// Constructor should throw errors for invalid values specified by
// OfflineAudioContextOptions.
audit.define(
{label: 'options-2', description: 'Invalid options'},
(task, should) => {
let options = {length: 42, sampleRate: 8000, numberOfChannels: 33};
// channelCount too large.
should(
() => {
new OfflineAudioContext(options);
},
'new OfflineAudioContext(' + JSON.stringify(options) + ')')
.throw(DOMException, 'NotSupportedError');
// length cannot be 0
options = {length: 0, sampleRate: 8000};
should(
() => {
new OfflineAudioContext(options);
},
'new OfflineAudioContext(' + JSON.stringify(options) + ')')
.throw(DOMException, 'NotSupportedError');
// sampleRate outside valid range
options = {length: 1, sampleRate: 1};
should(
() => {
new OfflineAudioContext(options);
},
'new OfflineAudioContext(' + JSON.stringify(options) + ')')
.throw(DOMException, 'NotSupportedError');
task.done();
});
audit.define(
{label: 'options-3', description: 'Valid options'},
(task, should) => {
let context;
let options = {
length: 1,
sampleRate: 8000,
};
// Verify context with valid constructor has the correct values.
should(
() => {
context = new OfflineAudioContext(options);
},
'c = new OfflineAudioContext' + JSON.stringify(options) + ')')
.notThrow();
should(context.length, 'c.length').beEqualTo(options.length);
should(context.sampleRate, 'c.sampleRate')
.beEqualTo(options.sampleRate);
should(
context.destination.channelCount, 'c.destination.channelCount')
.beEqualTo(1);
should(
context.destination.channelCountMode,
'c.destination.channelCountMode')
.beEqualTo('explicit');
should(
context.destination.channelInterpretation,
'c.destination.channelCountMode')
.beEqualTo('speakers');
options.numberOfChannels = 7;
should(
() => {
context = new OfflineAudioContext(options);
},
'c = new OfflineAudioContext' + JSON.stringify(options) + ')')
.notThrow();
should(
context.destination.channelCount, 'c.destination.channelCount')
.beEqualTo(options.numberOfChannels);
task.done();
});
audit.run();
</script>
</body>
</html>