Skip to content

Commit 01947de

Browse files
Prospero23gmta
authored andcommitted
LibWeb/WebAudio: Implement basic startRendering
Adds passing WPT. Does not handle actually rendering audio yet.
1 parent 5abb5d5 commit 01947de

File tree

6 files changed

+143
-8
lines changed

6 files changed

+143
-8
lines changed

Libraries/LibWeb/WebAudio/OfflineAudioContext.cpp

Lines changed: 89 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
/*
22
* Copyright (c) 2024, Shannon Booth <shannon@serenityos.org>
3+
* Copyright (c) 2025, Ben Eidson <b.e.eidson@gmail.com>
34
*
45
* SPDX-License-Identifier: BSD-2-Clause
56
*/
67

78
#include <LibWeb/Bindings/Intrinsics.h>
9+
#include <LibWeb/DOM/Event.h>
810
#include <LibWeb/HTML/EventNames.h>
11+
#include <LibWeb/HTML/Navigable.h>
12+
#include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h>
913
#include <LibWeb/HTML/Window.h>
14+
#include <LibWeb/WebAudio/AudioBuffer.h>
1015
#include <LibWeb/WebAudio/AudioDestinationNode.h>
1116
#include <LibWeb/WebAudio/OfflineAudioContext.h>
1217

@@ -23,7 +28,7 @@ WebIDL::ExceptionOr<GC::Ref<OfflineAudioContext>> OfflineAudioContext::construct
2328
TRY(verify_audio_options_inside_nominal_range(realm, context_options.number_of_channels, context_options.length, context_options.sample_rate));
2429

2530
// Let c be a new OfflineAudioContext object. Initialize c as follows:
26-
auto c = realm.create<OfflineAudioContext>(realm, context_options.length, context_options.sample_rate);
31+
auto c = realm.create<OfflineAudioContext>(realm, context_options.number_of_channels, context_options.length, context_options.sample_rate);
2732

2833
// 1. Set the [[control thread state]] for c to "suspended".
2934
c->set_control_state(Bindings::AudioContextState::Suspended);
@@ -58,7 +63,86 @@ OfflineAudioContext::~OfflineAudioContext() = default;
5863
// https://webaudio.github.io/web-audio-api/#dom-offlineaudiocontext-startrendering
5964
WebIDL::ExceptionOr<GC::Ref<WebIDL::Promise>> OfflineAudioContext::start_rendering()
6065
{
61-
return WebIDL::NotSupportedError::create(realm(), "FIXME: Implement OfflineAudioContext::start_rendering"_utf16);
66+
auto& realm = this->realm();
67+
68+
// 1. If this’s relevant global object’s associated Document is not fully active then return a promise rejected with "InvalidStateError" DOMException.
69+
auto& window = as<HTML::Window>(HTML::relevant_global_object(*this));
70+
auto const& associated_document = window.associated_document();
71+
72+
if (!associated_document.is_fully_active()) {
73+
auto error = WebIDL::InvalidStateError::create(realm, "Document is not fully active"_utf16);
74+
return WebIDL::create_rejected_promise_from_exception(realm, error);
75+
}
76+
77+
// AD-HOC: Not in spec explicitly, but this should account for detached iframes too. See /the-offlineaudiocontext-interface/startrendering-after-discard.html WPT.
78+
auto navigable = window.navigable();
79+
if (navigable && navigable->has_been_destroyed()) {
80+
auto error = WebIDL::InvalidStateError::create(realm, "The iframe has been detached"_utf16);
81+
return WebIDL::create_rejected_promise_from_exception(realm, error);
82+
}
83+
84+
// 2. If the [[rendering started]] slot on the OfflineAudioContext is true, return a rejected promise with InvalidStateError, and abort these steps.
85+
if (m_rendering_started) {
86+
auto error = WebIDL::InvalidStateError::create(realm, "Rendering is already started"_utf16);
87+
return WebIDL::create_rejected_promise_from_exception(realm, error);
88+
}
89+
90+
// 3. Set the [[rendering started]] slot of the OfflineAudioContext to true.
91+
m_rendering_started = true;
92+
93+
// 4. Let promise be a new promise.
94+
auto promise = WebIDL::create_promise(realm);
95+
96+
// 5. Create a new AudioBuffer, with a number of channels, length and sample rate equal respectively to the
97+
// numberOfChannels, length and sampleRate values passed to this instance’s constructor in the contextOptions
98+
// parameter.
99+
auto buffer_result = create_buffer(m_number_of_channels, length(), sample_rate());
100+
101+
// 6. If an exception was thrown during the preceding AudioBuffer constructor call, reject promise with this exception.
102+
if (buffer_result.is_exception()) {
103+
return WebIDL::create_rejected_promise_from_exception(realm, buffer_result.exception());
104+
}
105+
106+
// Assign this buffer to an internal slot [[rendered buffer]] in the OfflineAudioContext.
107+
m_rendered_buffer = buffer_result.release_value();
108+
109+
// 7. Otherwise, in the case that the buffer was successfully constructed, begin offline rendering.
110+
begin_offline_rendering(promise);
111+
112+
// 8. Append promise to [[pending promises]].
113+
m_pending_promises.append(promise);
114+
115+
// 9. Return promise.
116+
return promise;
117+
}
118+
119+
void OfflineAudioContext::begin_offline_rendering(GC::Ref<WebIDL::Promise> promise)
120+
{
121+
auto& realm = this->realm();
122+
// To begin offline rendering, the following steps MUST happen on a rendering thread that is created for the occasion.
123+
// FIXME: 1: Given the current connections and scheduled changes, start rendering length sample-frames of audio into [[rendered buffer]]
124+
// FIXME: 2: For every render quantum, check and suspend rendering if necessary.
125+
// FIXME: 3: If a suspended context is resumed, continue to render the buffer.
126+
// 4: Once the rendering is complete, queue a media element task to execute the following steps:
127+
queue_a_media_element_task(GC::create_function(heap(), [&realm, promise, this]() {
128+
HTML::TemporaryExecutionContext context(realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes);
129+
130+
// 4.1 Resolve the promise created by startRendering() with [[rendered buffer]].
131+
WebIDL::resolve_promise(realm, promise, this->m_rendered_buffer);
132+
133+
// AD-HOC: Remove resolved promise from [[pending promises]]
134+
// https://github.com/WebAudio/web-audio-api/issues/2648
135+
m_pending_promises.remove_all_matching([promise](GC::Ref<WebIDL::Promise> const& p) {
136+
return p.ptr() == promise.ptr();
137+
});
138+
139+
// 4.2: Queue a media element task to fire an event named complete at the OfflineAudioContext using OfflineAudioCompletionEvent
140+
// whose renderedBuffer property is set to [[rendered buffer]].
141+
// FIXME: Need to implement OfflineAudioCompletionEvent.
142+
queue_a_media_element_task(GC::create_function(heap(), [&realm, this]() {
143+
this->dispatch_event(DOM::Event::create(realm, HTML::EventNames::complete));
144+
}));
145+
}));
62146
}
63147

64148
WebIDL::ExceptionOr<GC::Ref<WebIDL::Promise>> OfflineAudioContext::resume()
@@ -91,9 +175,10 @@ void OfflineAudioContext::set_oncomplete(GC::Ptr<WebIDL::CallbackType> value)
91175
set_event_handler_attribute(HTML::EventNames::complete, value);
92176
}
93177

94-
OfflineAudioContext::OfflineAudioContext(JS::Realm& realm, WebIDL::UnsignedLong length, float sample_rate)
178+
OfflineAudioContext::OfflineAudioContext(JS::Realm& realm, WebIDL::UnsignedLong number_of_channels, WebIDL::UnsignedLong length, float sample_rate)
95179
: BaseAudioContext(realm, sample_rate)
96180
, m_length(length)
181+
, m_number_of_channels(number_of_channels)
97182
{
98183
}
99184

@@ -106,6 +191,7 @@ void OfflineAudioContext::initialize(JS::Realm& realm)
106191
void OfflineAudioContext::visit_edges(Cell::Visitor& visitor)
107192
{
108193
Base::visit_edges(visitor);
194+
visitor.visit(m_rendered_buffer);
109195
}
110196

111197
}

Libraries/LibWeb/WebAudio/OfflineAudioContext.h

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/*
22
* Copyright (c) 2024, Shannon Booth <shannon@serenityos.org>
3+
* Copyright (c) 2025, Ben Eidson <b.e.eidson@gmail.com>
34
*
45
* SPDX-License-Identifier: BSD-2-Clause
56
*/
@@ -42,12 +43,18 @@ class OfflineAudioContext final : public BaseAudioContext {
4243
void set_oncomplete(GC::Ptr<WebIDL::CallbackType>);
4344

4445
private:
45-
OfflineAudioContext(JS::Realm&, WebIDL::UnsignedLong length, float sample_rate);
46+
OfflineAudioContext(JS::Realm&, WebIDL::UnsignedLong number_of_channels, WebIDL::UnsignedLong length, float sample_rate);
4647

4748
virtual void initialize(JS::Realm&) override;
4849
virtual void visit_edges(Cell::Visitor&) override;
4950

5051
WebIDL::UnsignedLong m_length {};
52+
WebIDL::UnsignedLong m_number_of_channels {};
53+
bool m_rendering_started { false };
54+
55+
GC::Ptr<AudioBuffer> m_rendered_buffer;
56+
57+
void begin_offline_rendering(GC::Ref<WebIDL::Promise> promise);
5158
};
5259

5360
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
128
2+
InvalidStateError: Rendering is already started
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Harness status: OK
2+
3+
Found 1 tests
4+
5+
1 Pass
6+
Pass startRendering()
Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,22 @@
11
<!DOCTYPE html>
22
<script src="../include.js"></script>
33
<script>
4-
5-
// Once the ctor‑offlineaudiocontext WPT test is updated to check
6-
// renderQuantumSize and renderSizeHint, this test is not needed.
7-
test(() => {
4+
asyncTest(async done => {
5+
// Once the ctor‑offlineaudiocontext WPT test is updated to check
6+
// renderQuantumSize and renderSizeHint, this test is not needed.
87
const audioContext = new OfflineAudioContext(1, 1, 44100)
98
println(`${audioContext.renderQuantumSize}`);
9+
10+
// Second call must reject with InvalidStateError
11+
await audioContext.startRendering();
12+
13+
try {
14+
await audioContext.startRendering();
15+
println('FAIL: started rendering on repeated call');
16+
} catch (e) {
17+
println(`${e}`);
18+
}
19+
20+
done();
1021
});
1122
</script>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<!doctype html>
2+
<title>Test for rejected promise from startRendering() on an
3+
OfflineAudioContext in a discarded browsing context</title>
4+
<script src=../../../resources/testharness.js></script>
5+
<script src=../../../resources/testharnessreport.js></script>
6+
<body></body>
7+
<script>
8+
let context;
9+
let childDOMException;
10+
setup(() => {
11+
const frame = document.createElement('iframe');
12+
document.body.appendChild(frame);
13+
context = new frame.contentWindow.OfflineAudioContext(
14+
{length: 1, sampleRate: 48000});
15+
childDOMException = frame.contentWindow.DOMException;
16+
frame.remove();
17+
});
18+
19+
promise_test((t) => promise_rejects_dom(
20+
t, 'InvalidStateError', childDOMException, context.startRendering()),
21+
'startRendering()');
22+
// decodeAudioData() is tested in
23+
// offlineaudiocontext-detached-execution-context.html
24+
</script>

0 commit comments

Comments
 (0)