LibWeb: Implement OscillatorNode.setPeriodicWave()

This commit is contained in:
Tim Ledbetter 2025-01-03 18:07:01 +00:00 committed by Tim Ledbetter
parent acbae1b118
commit 8c4e4ec31b
Notes: github-actions[bot] 2025-01-04 10:13:24 +00:00
6 changed files with 205 additions and 18 deletions

View file

@ -1,5 +1,6 @@
/*
* Copyright (c) 2024, Shannon Booth <shannon@serenityos.org>
* Copyright (c) 2025, Tim Ledbetter <tim.ledbetter@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@ -25,9 +26,14 @@ WebIDL::ExceptionOr<GC::Ref<OscillatorNode>> OscillatorNode::create(JS::Realm& r
// https://webaudio.github.io/web-audio-api/#dom-oscillatornode-oscillatornode
WebIDL::ExceptionOr<GC::Ref<OscillatorNode>> OscillatorNode::construct_impl(JS::Realm& realm, GC::Ref<BaseAudioContext> context, OscillatorOptions const& options)
{
TRY(verify_valid_type(realm, options.type));
if (options.type == Bindings::OscillatorType::Custom && !options.periodic_wave)
return WebIDL::InvalidStateError::create(realm, "Oscillator node type 'custom' requires PeriodicWave to be provided"_string);
auto node = realm.create<OscillatorNode>(realm, context, options);
if (options.type == Bindings::OscillatorType::Custom)
node->set_periodic_wave(options.periodic_wave);
// Default options for channel count and interpretation
// https://webaudio.github.io/web-audio-api/#OscillatorNode
AudioNodeDefaultOptions default_options;
@ -56,24 +62,23 @@ Bindings::OscillatorType OscillatorNode::type() const
}
// https://webaudio.github.io/web-audio-api/#dom-oscillatornode-type
WebIDL::ExceptionOr<void> OscillatorNode::verify_valid_type(JS::Realm& realm, Bindings::OscillatorType type)
WebIDL::ExceptionOr<void> OscillatorNode::set_type(Bindings::OscillatorType type)
{
// The shape of the periodic waveform. It may directly be set to any of the type constant values except
// for "custom". ⌛ Doing so MUST throw an InvalidStateError exception. The setPeriodicWave() method can
// be used to set a custom waveform, which results in this attribute being set to "custom". The default
// value is "sine". When this attribute is set, the phase of the oscillator MUST be conserved.
if (type == Bindings::OscillatorType::Custom)
return WebIDL::InvalidStateError::create(realm, "Oscillator node type cannot be set to 'custom'"_string);
if (type == Bindings::OscillatorType::Custom && m_type != Bindings::OscillatorType::Custom)
return WebIDL::InvalidStateError::create(realm(), "Oscillator node type cannot be changed to 'custom'"_string);
// FIXME: An appropriate PeriodicWave should be set here based on the given type.
set_periodic_wave(nullptr);
m_type = type;
return {};
}
// https://webaudio.github.io/web-audio-api/#dom-oscillatornode-type
WebIDL::ExceptionOr<void> OscillatorNode::set_type(Bindings::OscillatorType type)
// https://webaudio.github.io/web-audio-api/#dom-oscillatornode-setperiodicwave
void OscillatorNode::set_periodic_wave(GC::Ptr<PeriodicWave> periodic_wave)
{
TRY(verify_valid_type(realm(), type));
m_type = type;
return {};
m_periodic_wave = periodic_wave;
m_type = Bindings::OscillatorType::Custom;
}
void OscillatorNode::initialize(JS::Realm& realm)
@ -87,6 +92,7 @@ void OscillatorNode::visit_edges(Cell::Visitor& visitor)
Base::visit_edges(visitor);
visitor.visit(m_frequency);
visitor.visit(m_detune);
visitor.visit(m_periodic_wave);
}
}

View file

@ -33,6 +33,8 @@ public:
Bindings::OscillatorType type() const;
WebIDL::ExceptionOr<void> set_type(Bindings::OscillatorType);
void set_periodic_wave(GC::Ptr<PeriodicWave>);
GC::Ref<AudioParam const> frequency() const { return m_frequency; }
GC::Ref<AudioParam const> detune() const { return m_detune; }
@ -46,8 +48,6 @@ protected:
virtual void visit_edges(Cell::Visitor&) override;
private:
static WebIDL::ExceptionOr<void> verify_valid_type(JS::Realm&, Bindings::OscillatorType);
// https://webaudio.github.io/web-audio-api/#dom-oscillatornode-type
Bindings::OscillatorType m_type { Bindings::OscillatorType::Sine };
@ -56,6 +56,8 @@ private:
// https://webaudio.github.io/web-audio-api/#dom-oscillatornode-detune
GC::Ref<AudioParam> m_detune;
GC::Ptr<PeriodicWave> m_periodic_wave;
};
}

View file

@ -25,5 +25,5 @@ interface OscillatorNode : AudioScheduledSourceNode {
attribute OscillatorType type;
readonly attribute AudioParam frequency;
readonly attribute AudioParam detune;
[FIXME] undefined setPeriodicWave(PeriodicWave periodicWave);
undefined setPeriodicWave(PeriodicWave periodicWave);
};

View file

@ -4,9 +4,9 @@ AudioNode
EventTarget
Object
context: '[object OfflineAudioContext], is same as original: true
Error creating node: 'InvalidStateError: Oscillator node type cannot be set to 'custom''
Error creating node: 'InvalidStateError: Oscillator node type 'custom' requires PeriodicWave to be provided'
oscillator node type: 'sine'
Error: 'InvalidStateError: Oscillator node type cannot be set to 'custom'', type is: sine
Error: 'InvalidStateError: Oscillator node type cannot be changed to 'custom'', type is: sine
oscillator node type: 'triangle'
[object AudioParam] current: 440, default: 440, min: -22050, max: 22050, rate: a-rate
[object AudioParam] current: -52, default: 440, min: -22050, max: 22050, rate: a-rate

View file

@ -0,0 +1,67 @@
Harness status: OK
Found 62 tests
62 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 OscillatorNode() threw TypeError: "OscillatorNode() needs one argument".
Pass new OscillatorNode(1) threw TypeError: "Not an object of type BaseAudioContext".
Pass new OscillatorNode(context, 42) threw TypeError: "Not an object of type OscillatorOptions".
Pass < [invalid constructor] All assertions passed. (total 3 assertions)
Pass > [default constructor]
Pass node0 = new OscillatorNode(context) did not throw an exception.
Pass node0 instanceof OscillatorNode is equal to true.
Pass node0.numberOfInputs is equal to 0.
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.type is equal to sine.
Pass node0.frequency.value is equal to 440.
Pass < [default constructor] All assertions passed. (total 9 assertions)
Pass > [test AudioNodeOptions]
Pass new OscillatorNode(c, {channelCount: 17}) did not throw an exception.
Pass node.channelCount is equal to 17.
Pass new OscillatorNode(c, {channelCount: 0}) threw NotSupportedError: "Invalid channel count".
Pass new OscillatorNode(c, {channelCount: 99}) threw NotSupportedError: "Invalid channel count".
Pass new OscillatorNode(c, {channelCountMode: "max"} did not throw an exception.
Pass node.channelCountMode is equal to max.
Pass new OscillatorNode(c, {channelCountMode: "max"}) did not throw an exception.
Pass node.channelCountMode after valid setter is equal to max.
Pass new OscillatorNode(c, {channelCountMode: "clamped-max"}) did not throw an exception.
Pass node.channelCountMode after valid setter is equal to clamped-max.
Pass new OscillatorNode(c, {channelCountMode: "explicit"}) did not throw an exception.
Pass node.channelCountMode after valid setter is equal to explicit.
Pass new OscillatorNode(c, {channelCountMode: "foobar"} threw TypeError: "Invalid value 'foobar' for enumeration type 'ChannelCountMode'".
Pass node.channelCountMode after invalid setter is equal to explicit.
Pass new OscillatorNode(c, {channelInterpretation: "speakers"}) did not throw an exception.
Pass node.channelInterpretation is equal to speakers.
Pass new OscillatorNode(c, {channelInterpretation: "discrete"}) did not throw an exception.
Pass node.channelInterpretation is equal to discrete.
Pass new OscillatorNode(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 OscillatorNode(c, {"type":"sawtooth","detune":7,"frequency":918}) did not throw an exception.
Pass node1.type is equal to sawtooth.
Pass node1.detune.value is equal to 7.
Pass node1.frequency.value is equal to 918.
Pass node1.channelCount is equal to 2.
Pass node1.channelCountMode is equal to max.
Pass node1.channelInterpretation is equal to speakers.
Pass new OscillatorNode(c, {"type":"sine","periodicWave":{}}) did not throw an exception.
Pass new OscillatorNode(c, {"type":"custom"}) threw InvalidStateError: "Oscillator node type 'custom' requires PeriodicWave to be provided".
Pass new OscillatorNode(c, {"type":"custom","periodicWave":{}}) did not throw an exception.
Pass new OscillatorNode(c, {periodicWave: null} threw TypeError: "Not an object of type PeriodicWave".
Pass < [constructor options] All assertions passed. (total 11 assertions)
Pass # AUDIT TASK RUNNER FINISHED: 5 tasks ran successfully.

View file

@ -0,0 +1,112 @@
<!DOCTYPE html>
<html>
<head>
<title>
Test Constructor: Oscillator
</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, 'OscillatorNode', context);
task.done();
});
audit.define('default constructor', (task, should) => {
let prefix = 'node0';
let node = testDefaultConstructor(should, 'OscillatorNode', context, {
prefix: prefix,
numberOfInputs: 0,
numberOfOutputs: 1,
channelCount: 2,
channelCountMode: 'max',
channelInterpretation: 'speakers'
});
testDefaultAttributes(
should, node, prefix,
[{name: 'type', value: 'sine'}, {name: 'frequency', value: 440}]);
task.done();
});
audit.define('test AudioNodeOptions', (task, should) => {
testAudioNodeOptions(should, context, 'OscillatorNode');
task.done();
});
audit.define('constructor options', (task, should) => {
let node;
let options = {type: 'sawtooth', detune: 7, frequency: 918};
should(
() => {
node = new OscillatorNode(context, options);
},
'node1 = new OscillatorNode(c, ' + JSON.stringify(options) + ')')
.notThrow();
should(node.type, 'node1.type').beEqualTo(options.type);
should(node.detune.value, 'node1.detune.value')
.beEqualTo(options.detune);
should(node.frequency.value, 'node1.frequency.value')
.beEqualTo(options.frequency);
should(node.channelCount, 'node1.channelCount').beEqualTo(2);
should(node.channelCountMode, 'node1.channelCountMode')
.beEqualTo('max');
should(node.channelInterpretation, 'node1.channelInterpretation')
.beEqualTo('speakers');
// Test that type and periodicWave options work as described.
options = {
type: 'sine',
periodicWave: new PeriodicWave(context, {real: [1, 1]})
};
should(() => {
node = new OscillatorNode(context, options);
}, 'new OscillatorNode(c, ' + JSON.stringify(options) + ')').notThrow();
options = {type: 'custom'};
should(
() => {
node = new OscillatorNode(context, options);
},
'new OscillatorNode(c, ' + JSON.stringify(options) + ')')
.throw(DOMException, 'InvalidStateError');
options = {
type: 'custom',
periodicWave: new PeriodicWave(context, {real: [1, 1]})
};
should(() => {
node = new OscillatorNode(context, options);
}, 'new OscillatorNode(c, ' + JSON.stringify(options) + ')').notThrow();
should(
() => {
node = new OscillatorNode(context, {periodicWave: null});
},
'new OscillatorNode(c, {periodicWave: null}')
.throw(DOMException, 'TypeError');
task.done();
});
audit.run();
</script>
</body>
</html>