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
5964WebIDL::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
64148WebIDL::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)
106191void OfflineAudioContext::visit_edges (Cell::Visitor& visitor)
107192{
108193 Base::visit_edges (visitor);
194+ visitor.visit (m_rendered_buffer);
109195}
110196
111197}
0 commit comments