Skip to content

Commit 015b9be

Browse files
tcl3pbrw
authored andcommitted
LibWeb: Implement OscillatorNode.setPeriodicWave()
1 parent 630bf62 commit 015b9be

File tree

6 files changed

+205
-18
lines changed

6 files changed

+205
-18
lines changed

Libraries/LibWeb/WebAudio/OscillatorNode.cpp

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/*
22
* Copyright (c) 2024, Shannon Booth <[email protected]>
3+
* Copyright (c) 2025, Tim Ledbetter <[email protected]>
34
*
45
* SPDX-License-Identifier: BSD-2-Clause
56
*/
@@ -25,9 +26,14 @@ WebIDL::ExceptionOr<GC::Ref<OscillatorNode>> OscillatorNode::create(JS::Realm& r
2526
// https://webaudio.github.io/web-audio-api/#dom-oscillatornode-oscillatornode
2627
WebIDL::ExceptionOr<GC::Ref<OscillatorNode>> OscillatorNode::construct_impl(JS::Realm& realm, GC::Ref<BaseAudioContext> context, OscillatorOptions const& options)
2728
{
28-
TRY(verify_valid_type(realm, options.type));
29+
if (options.type == Bindings::OscillatorType::Custom && !options.periodic_wave)
30+
return WebIDL::InvalidStateError::create(realm, "Oscillator node type 'custom' requires PeriodicWave to be provided"_string);
31+
2932
auto node = realm.create<OscillatorNode>(realm, context, options);
3033

34+
if (options.type == Bindings::OscillatorType::Custom)
35+
node->set_periodic_wave(options.periodic_wave);
36+
3137
// Default options for channel count and interpretation
3238
// https://webaudio.github.io/web-audio-api/#OscillatorNode
3339
AudioNodeDefaultOptions default_options;
@@ -56,24 +62,23 @@ Bindings::OscillatorType OscillatorNode::type() const
5662
}
5763

5864
// https://webaudio.github.io/web-audio-api/#dom-oscillatornode-type
59-
WebIDL::ExceptionOr<void> OscillatorNode::verify_valid_type(JS::Realm& realm, Bindings::OscillatorType type)
65+
WebIDL::ExceptionOr<void> OscillatorNode::set_type(Bindings::OscillatorType type)
6066
{
61-
// The shape of the periodic waveform. It may directly be set to any of the type constant values except
62-
// for "custom". ⌛ Doing so MUST throw an InvalidStateError exception. The setPeriodicWave() method can
63-
// be used to set a custom waveform, which results in this attribute being set to "custom". The default
64-
// value is "sine". When this attribute is set, the phase of the oscillator MUST be conserved.
65-
if (type == Bindings::OscillatorType::Custom)
66-
return WebIDL::InvalidStateError::create(realm, "Oscillator node type cannot be set to 'custom'"_string);
67+
if (type == Bindings::OscillatorType::Custom && m_type != Bindings::OscillatorType::Custom)
68+
return WebIDL::InvalidStateError::create(realm(), "Oscillator node type cannot be changed to 'custom'"_string);
6769

70+
// FIXME: An appropriate PeriodicWave should be set here based on the given type.
71+
set_periodic_wave(nullptr);
72+
73+
m_type = type;
6874
return {};
6975
}
7076

71-
// https://webaudio.github.io/web-audio-api/#dom-oscillatornode-type
72-
WebIDL::ExceptionOr<void> OscillatorNode::set_type(Bindings::OscillatorType type)
77+
// https://webaudio.github.io/web-audio-api/#dom-oscillatornode-setperiodicwave
78+
void OscillatorNode::set_periodic_wave(GC::Ptr<PeriodicWave> periodic_wave)
7379
{
74-
TRY(verify_valid_type(realm(), type));
75-
m_type = type;
76-
return {};
80+
m_periodic_wave = periodic_wave;
81+
m_type = Bindings::OscillatorType::Custom;
7782
}
7883

7984
void OscillatorNode::initialize(JS::Realm& realm)
@@ -87,6 +92,7 @@ void OscillatorNode::visit_edges(Cell::Visitor& visitor)
8792
Base::visit_edges(visitor);
8893
visitor.visit(m_frequency);
8994
visitor.visit(m_detune);
95+
visitor.visit(m_periodic_wave);
9096
}
9197

9298
}

Libraries/LibWeb/WebAudio/OscillatorNode.h

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ class OscillatorNode : public AudioScheduledSourceNode {
3333
Bindings::OscillatorType type() const;
3434
WebIDL::ExceptionOr<void> set_type(Bindings::OscillatorType);
3535

36+
void set_periodic_wave(GC::Ptr<PeriodicWave>);
37+
3638
GC::Ref<AudioParam const> frequency() const { return m_frequency; }
3739
GC::Ref<AudioParam const> detune() const { return m_detune; }
3840

@@ -46,8 +48,6 @@ class OscillatorNode : public AudioScheduledSourceNode {
4648
virtual void visit_edges(Cell::Visitor&) override;
4749

4850
private:
49-
static WebIDL::ExceptionOr<void> verify_valid_type(JS::Realm&, Bindings::OscillatorType);
50-
5151
// https://webaudio.github.io/web-audio-api/#dom-oscillatornode-type
5252
Bindings::OscillatorType m_type { Bindings::OscillatorType::Sine };
5353

@@ -56,6 +56,8 @@ class OscillatorNode : public AudioScheduledSourceNode {
5656

5757
// https://webaudio.github.io/web-audio-api/#dom-oscillatornode-detune
5858
GC::Ref<AudioParam> m_detune;
59+
60+
GC::Ptr<PeriodicWave> m_periodic_wave;
5961
};
6062

6163
}

Libraries/LibWeb/WebAudio/OscillatorNode.idl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,5 @@ interface OscillatorNode : AudioScheduledSourceNode {
2525
attribute OscillatorType type;
2626
readonly attribute AudioParam frequency;
2727
readonly attribute AudioParam detune;
28-
[FIXME] undefined setPeriodicWave(PeriodicWave periodicWave);
28+
undefined setPeriodicWave(PeriodicWave periodicWave);
2929
};

Tests/LibWeb/Text/expected/WebAudio/OscillatorNode.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ AudioNode
44
EventTarget
55
Object
66
context: '[object OfflineAudioContext], is same as original: true
7-
Error creating node: 'InvalidStateError: Oscillator node type cannot be set to 'custom''
7+
Error creating node: 'InvalidStateError: Oscillator node type 'custom' requires PeriodicWave to be provided'
88
oscillator node type: 'sine'
9-
Error: 'InvalidStateError: Oscillator node type cannot be set to 'custom'', type is: sine
9+
Error: 'InvalidStateError: Oscillator node type cannot be changed to 'custom'', type is: sine
1010
oscillator node type: 'triangle'
1111
[object AudioParam] current: 440, default: 440, min: -22050, max: 22050, rate: a-rate
1212
[object AudioParam] current: -52, default: 440, min: -22050, max: 22050, rate: a-rate
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
Harness status: OK
2+
3+
Found 62 tests
4+
5+
62 Pass
6+
Pass # AUDIT TASK RUNNER STARTED.
7+
Pass Executing "initialize"
8+
Pass Executing "invalid constructor"
9+
Pass Executing "default constructor"
10+
Pass Executing "test AudioNodeOptions"
11+
Pass Executing "constructor options"
12+
Pass Audit report
13+
Pass > [initialize]
14+
Pass context = new OfflineAudioContext(...) did not throw an exception.
15+
Pass < [initialize] All assertions passed. (total 1 assertions)
16+
Pass > [invalid constructor]
17+
Pass new OscillatorNode() threw TypeError: "OscillatorNode() needs one argument".
18+
Pass new OscillatorNode(1) threw TypeError: "Not an object of type BaseAudioContext".
19+
Pass new OscillatorNode(context, 42) threw TypeError: "Not an object of type OscillatorOptions".
20+
Pass < [invalid constructor] All assertions passed. (total 3 assertions)
21+
Pass > [default constructor]
22+
Pass node0 = new OscillatorNode(context) did not throw an exception.
23+
Pass node0 instanceof OscillatorNode is equal to true.
24+
Pass node0.numberOfInputs is equal to 0.
25+
Pass node0.numberOfOutputs is equal to 1.
26+
Pass node0.channelCount is equal to 2.
27+
Pass node0.channelCountMode is equal to max.
28+
Pass node0.channelInterpretation is equal to speakers.
29+
Pass node0.type is equal to sine.
30+
Pass node0.frequency.value is equal to 440.
31+
Pass < [default constructor] All assertions passed. (total 9 assertions)
32+
Pass > [test AudioNodeOptions]
33+
Pass new OscillatorNode(c, {channelCount: 17}) did not throw an exception.
34+
Pass node.channelCount is equal to 17.
35+
Pass new OscillatorNode(c, {channelCount: 0}) threw NotSupportedError: "Invalid channel count".
36+
Pass new OscillatorNode(c, {channelCount: 99}) threw NotSupportedError: "Invalid channel count".
37+
Pass new OscillatorNode(c, {channelCountMode: "max"} did not throw an exception.
38+
Pass node.channelCountMode is equal to max.
39+
Pass new OscillatorNode(c, {channelCountMode: "max"}) did not throw an exception.
40+
Pass node.channelCountMode after valid setter is equal to max.
41+
Pass new OscillatorNode(c, {channelCountMode: "clamped-max"}) did not throw an exception.
42+
Pass node.channelCountMode after valid setter is equal to clamped-max.
43+
Pass new OscillatorNode(c, {channelCountMode: "explicit"}) did not throw an exception.
44+
Pass node.channelCountMode after valid setter is equal to explicit.
45+
Pass new OscillatorNode(c, {channelCountMode: "foobar"} threw TypeError: "Invalid value 'foobar' for enumeration type 'ChannelCountMode'".
46+
Pass node.channelCountMode after invalid setter is equal to explicit.
47+
Pass new OscillatorNode(c, {channelInterpretation: "speakers"}) did not throw an exception.
48+
Pass node.channelInterpretation is equal to speakers.
49+
Pass new OscillatorNode(c, {channelInterpretation: "discrete"}) did not throw an exception.
50+
Pass node.channelInterpretation is equal to discrete.
51+
Pass new OscillatorNode(c, {channelInterpretation: "foobar"}) threw TypeError: "Invalid value 'foobar' for enumeration type 'ChannelInterpretation'".
52+
Pass node.channelInterpretation after invalid setter is equal to discrete.
53+
Pass < [test AudioNodeOptions] All assertions passed. (total 20 assertions)
54+
Pass > [constructor options]
55+
Pass node1 = new OscillatorNode(c, {"type":"sawtooth","detune":7,"frequency":918}) did not throw an exception.
56+
Pass node1.type is equal to sawtooth.
57+
Pass node1.detune.value is equal to 7.
58+
Pass node1.frequency.value is equal to 918.
59+
Pass node1.channelCount is equal to 2.
60+
Pass node1.channelCountMode is equal to max.
61+
Pass node1.channelInterpretation is equal to speakers.
62+
Pass new OscillatorNode(c, {"type":"sine","periodicWave":{}}) did not throw an exception.
63+
Pass new OscillatorNode(c, {"type":"custom"}) threw InvalidStateError: "Oscillator node type 'custom' requires PeriodicWave to be provided".
64+
Pass new OscillatorNode(c, {"type":"custom","periodicWave":{}}) did not throw an exception.
65+
Pass new OscillatorNode(c, {periodicWave: null} threw TypeError: "Not an object of type PeriodicWave".
66+
Pass < [constructor options] All assertions passed. (total 11 assertions)
67+
Pass # AUDIT TASK RUNNER FINISHED: 5 tasks ran successfully.
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<title>
5+
Test Constructor: Oscillator
6+
</title>
7+
<script src="../../../resources/testharness.js"></script>
8+
<script src="../../../resources/testharnessreport.js"></script>
9+
<script src="../../../webaudio/resources/audit-util.js"></script>
10+
<script src="../../../webaudio/resources/audit.js"></script>
11+
<script src="../../../webaudio/resources/audionodeoptions.js"></script>
12+
</head>
13+
<body>
14+
<script id="layout-test-code">
15+
let context;
16+
17+
let audit = Audit.createTaskRunner();
18+
19+
audit.define('initialize', (task, should) => {
20+
context = initializeContext(should);
21+
task.done();
22+
});
23+
24+
audit.define('invalid constructor', (task, should) => {
25+
testInvalidConstructor(should, 'OscillatorNode', context);
26+
task.done();
27+
});
28+
29+
audit.define('default constructor', (task, should) => {
30+
let prefix = 'node0';
31+
let node = testDefaultConstructor(should, 'OscillatorNode', context, {
32+
prefix: prefix,
33+
numberOfInputs: 0,
34+
numberOfOutputs: 1,
35+
channelCount: 2,
36+
channelCountMode: 'max',
37+
channelInterpretation: 'speakers'
38+
});
39+
40+
testDefaultAttributes(
41+
should, node, prefix,
42+
[{name: 'type', value: 'sine'}, {name: 'frequency', value: 440}]);
43+
44+
task.done();
45+
});
46+
47+
audit.define('test AudioNodeOptions', (task, should) => {
48+
testAudioNodeOptions(should, context, 'OscillatorNode');
49+
task.done();
50+
});
51+
52+
audit.define('constructor options', (task, should) => {
53+
let node;
54+
let options = {type: 'sawtooth', detune: 7, frequency: 918};
55+
56+
should(
57+
() => {
58+
node = new OscillatorNode(context, options);
59+
},
60+
'node1 = new OscillatorNode(c, ' + JSON.stringify(options) + ')')
61+
.notThrow();
62+
63+
should(node.type, 'node1.type').beEqualTo(options.type);
64+
should(node.detune.value, 'node1.detune.value')
65+
.beEqualTo(options.detune);
66+
should(node.frequency.value, 'node1.frequency.value')
67+
.beEqualTo(options.frequency);
68+
69+
should(node.channelCount, 'node1.channelCount').beEqualTo(2);
70+
should(node.channelCountMode, 'node1.channelCountMode')
71+
.beEqualTo('max');
72+
should(node.channelInterpretation, 'node1.channelInterpretation')
73+
.beEqualTo('speakers');
74+
75+
// Test that type and periodicWave options work as described.
76+
options = {
77+
type: 'sine',
78+
periodicWave: new PeriodicWave(context, {real: [1, 1]})
79+
};
80+
should(() => {
81+
node = new OscillatorNode(context, options);
82+
}, 'new OscillatorNode(c, ' + JSON.stringify(options) + ')').notThrow();
83+
84+
options = {type: 'custom'};
85+
should(
86+
() => {
87+
node = new OscillatorNode(context, options);
88+
},
89+
'new OscillatorNode(c, ' + JSON.stringify(options) + ')')
90+
.throw(DOMException, 'InvalidStateError');
91+
92+
options = {
93+
type: 'custom',
94+
periodicWave: new PeriodicWave(context, {real: [1, 1]})
95+
};
96+
should(() => {
97+
node = new OscillatorNode(context, options);
98+
}, 'new OscillatorNode(c, ' + JSON.stringify(options) + ')').notThrow();
99+
100+
should(
101+
() => {
102+
node = new OscillatorNode(context, {periodicWave: null});
103+
},
104+
'new OscillatorNode(c, {periodicWave: null}')
105+
.throw(DOMException, 'TypeError');
106+
task.done();
107+
});
108+
109+
audit.run();
110+
</script>
111+
</body>
112+
</html>

0 commit comments

Comments
 (0)