ladybird/Userland/Libraries/LibWeb/WebAudio/AudioContext.cpp
Shannon Booth bad44f8fc9 LibWeb: Remove Bindings/Forward.h from LibWeb/Forward.h
This was resulting in a whole lot of rebuilding whenever a new IDL
interface was added.

Instead, just directly include the prototype in every C++ file which
needs it. While we only really need a forward declaration in each cpp
file; including the full prototype header (which itself only includes
LibJS/Object.h, which is already transitively brought in by
PlatformObject) - it seems like a small price to pay compared to what
feels like a full rebuild of LibWeb whenever a new IDL file is added.

Given all of these includes are only needed for the ::initialize
method, there is probably a smart way of avoiding this problem
altogether. I've considered both using some macro trickery or generating
these functions somehow instead.
2024-04-27 18:29:35 -04:00

315 lines
14 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* Copyright (c) 2023, Luke Wilde <lukew@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/Bindings/AudioContextPrototype.h>
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/DOM/Event.h>
#include <LibWeb/HTML/HTMLMediaElement.h>
#include <LibWeb/HTML/Window.h>
#include <LibWeb/WebAudio/AudioContext.h>
#include <LibWeb/WebIDL/Promise.h>
namespace Web::WebAudio {
JS_DEFINE_ALLOCATOR(AudioContext);
// https://webaudio.github.io/web-audio-api/#dom-audiocontext-audiocontext
WebIDL::ExceptionOr<JS::NonnullGCPtr<AudioContext>> AudioContext::construct_impl(JS::Realm& realm, AudioContextOptions const& context_options)
{
return realm.heap().allocate<AudioContext>(realm, realm, context_options);
}
AudioContext::AudioContext(JS::Realm& realm, AudioContextOptions const& context_options)
: BaseAudioContext(realm)
{
// FIXME: If the current settings objects responsible document is NOT fully active, throw an InvalidStateError and abort these steps.
// 1: Set a [[control thread state]] to suspended on the AudioContext.
BaseAudioContext::set_control_state(Bindings::AudioContextState::Suspended);
// 2: Set a [[rendering thread state]] to suspended on the AudioContext.
BaseAudioContext::set_rendering_state(Bindings::AudioContextState::Suspended);
// 3: Let [[pending resume promises]] be a slot on this AudioContext, that is an initially empty ordered list of promises.
// 4: If contextOptions is given, apply the options:
// 4.1: Set the internal latency of this AudioContext according to contextOptions.latencyHint, as described in latencyHint.
switch (context_options.latency_hint) {
case Bindings::AudioContextLatencyCategory::Balanced:
// FIXME: Determine optimal settings for balanced.
break;
case Bindings::AudioContextLatencyCategory::Interactive:
// FIXME: Determine optimal settings for interactive.
break;
case Bindings::AudioContextLatencyCategory::Playback:
// FIXME: Determine optimal settings for playback.
break;
default:
VERIFY_NOT_REACHED();
}
// 4.2: If contextOptions.sampleRate is specified, set the sampleRate of this AudioContext to this value. Otherwise,
// use the sample rate of the default output device. If the selected sample rate differs from the sample rate of the output device,
// this AudioContext MUST resample the audio output to match the sample rate of the output device.
if (context_options.sample_rate.has_value()) {
BaseAudioContext::set_sample_rate(context_options.sample_rate.value());
} else {
// FIXME: This would ideally be coming from the default output device, but we can only get this on Serenity
// For now we'll just have to resample
BaseAudioContext::set_sample_rate(44100);
}
// FIXME: 5: If the context is allowed to start, send a control message to start processing.
// FIXME: Implement control message queue to run following steps on the rendering thread
if (m_allowed_to_start) {
// FIXME: 5.1: Attempt to acquire system resources. In case of failure, abort the following steps.
// 5.2: Set the [[rendering thread state]] to "running" on the AudioContext.
BaseAudioContext::set_rendering_state(Bindings::AudioContextState::Running);
// 5.3: queue a media element task to execute the following steps:
queue_a_media_element_task([&realm, this]() {
// 5.3.1: Set the state attribute of the AudioContext to "running".
BaseAudioContext::set_control_state(Bindings::AudioContextState::Running);
// 5.3.2: queue a media element task to fire an event named statechange at the AudioContext.
this->dispatch_event(DOM::Event::create(realm, HTML::EventNames::statechange));
});
}
}
AudioContext::~AudioContext() = default;
void AudioContext::initialize(JS::Realm& realm)
{
Base::initialize(realm);
WEB_SET_PROTOTYPE_FOR_INTERFACE(AudioContext);
}
void AudioContext::visit_edges(Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_pending_promises);
visitor.visit(m_pending_resume_promises);
}
// https://www.w3.org/TR/webaudio/#dom-audiocontext-getoutputtimestamp
AudioTimestamp AudioContext::get_output_timestamp()
{
dbgln("(STUBBED) getOutputTimestamp()");
return {};
}
// https://www.w3.org/TR/webaudio/#dom-audiocontext-resume
WebIDL::ExceptionOr<JS::NonnullGCPtr<JS::Promise>> AudioContext::resume()
{
auto& realm = this->realm();
auto& vm = realm.vm();
// 1. If this's relevant global object's associated Document is not fully active then return a promise rejected with "InvalidStateError" DOMException.
auto const& associated_document = verify_cast<HTML::Window>(HTML::relevant_global_object(*this)).associated_document();
if (!associated_document.is_fully_active())
return WebIDL::InvalidStateError::create(realm, "Document is not fully active"_fly_string);
// 2. Let promise be a new Promise.
auto promise = WebIDL::create_promise(realm);
// 3. If the [[control thread state]] on the AudioContext is closed reject the promise with InvalidStateError, abort these steps, returning promise.
if (state() == Bindings::AudioContextState::Closed) {
WebIDL::reject_promise(realm, promise, WebIDL::InvalidStateError::create(realm, "Audio context is already closed."_fly_string));
return JS::NonnullGCPtr { verify_cast<JS::Promise>(*promise->promise()) };
}
// 4. Set [[suspended by user]] to true.
m_suspended_by_user = true;
// 5. If the context is not allowed to start, append promise to [[pending promises]] and [[pending resume promises]] and abort these steps, returning promise.
if (m_allowed_to_start) {
TRY_OR_THROW_OOM(vm, m_pending_promises.try_append(promise));
TRY_OR_THROW_OOM(vm, m_pending_resume_promises.try_append(promise));
}
// 6. Set the [[control thread state]] on the AudioContext to running.
set_control_state(Bindings::AudioContextState::Running);
// 7. Queue a control message to resume the AudioContext.
// FIXME: Implement control message queue to run following steps on the rendering thread
// FIXME: 7.1: Attempt to acquire system resources.
// 7.2: Set the [[rendering thread state]] on the AudioContext to running.
set_rendering_state(Bindings::AudioContextState::Running);
// 7.3: Start rendering the audio graph.
if (!start_rendering_audio_graph()) {
// 7.4: In case of failure, queue a media element task to execute the following steps:
queue_a_media_element_task([&realm, this]() {
// 7.4.1: Reject all promises from [[pending resume promises]] in order, then clear [[pending resume promises]].
for (auto const& promise : m_pending_resume_promises) {
WebIDL::reject_promise(realm, promise, JS::js_null());
}
m_pending_resume_promises.clear();
// FIXME: 7.4.2: Additionally, remove those promises from [[pending promises]].
});
}
// 7.5: queue a media element task to execute the following steps:
queue_a_media_element_task([&realm, promise, this]() {
// 7.5.1: Resolve all promises from [[pending resume promises]] in order.
for (auto const& pending_resume_promise : m_pending_resume_promises) {
*pending_resume_promise->resolve();
}
// 7.5.2: Clear [[pending resume promises]].
m_pending_resume_promises.clear();
// FIXME: Additionally, remove those promises from [[pending promises]].
// 7.5.3: Resolve promise.
*promise->resolve();
// 7.5.4: If the state attribute of the AudioContext is not already "running":
if (state() != Bindings::AudioContextState::Running) {
// 7.5.4.1: Set the state attribute of the AudioContext to "running".
set_control_state(Bindings::AudioContextState::Running);
// 7.5.4.2: queue a media element task to fire an event named statechange at the AudioContext.
queue_a_media_element_task([&realm, this]() {
this->dispatch_event(DOM::Event::create(realm, HTML::EventNames::statechange));
});
}
});
// 8. Return promise.
return JS::NonnullGCPtr { verify_cast<JS::Promise>(*promise->promise()) };
}
// https://www.w3.org/TR/webaudio/#dom-audiocontext-suspend
WebIDL::ExceptionOr<JS::NonnullGCPtr<JS::Promise>> AudioContext::suspend()
{
auto& realm = this->realm();
auto& vm = realm.vm();
// 1. If this's relevant global object's associated Document is not fully active then return a promise rejected with "InvalidStateError" DOMException.
auto const& associated_document = verify_cast<HTML::Window>(HTML::relevant_global_object(*this)).associated_document();
if (!associated_document.is_fully_active())
return WebIDL::InvalidStateError::create(realm, "Document is not fully active"_fly_string);
// 2. Let promise be a new Promise.
auto promise = WebIDL::create_promise(realm);
// 3. If the [[control thread state]] on the AudioContext is closed reject the promise with InvalidStateError, abort these steps, returning promise.
if (state() == Bindings::AudioContextState::Closed) {
WebIDL::reject_promise(realm, promise, WebIDL::InvalidStateError::create(realm, "Audio context is already closed."_fly_string));
return JS::NonnullGCPtr { verify_cast<JS::Promise>(*promise->promise()) };
}
// 4. Append promise to [[pending promises]].
TRY_OR_THROW_OOM(vm, m_pending_promises.try_append(promise));
// 5. Set [[suspended by user]] to true.
m_suspended_by_user = true;
// 6. Set the [[control thread state]] on the AudioContext to suspended.
set_control_state(Bindings::AudioContextState::Suspended);
// 7. Queue a control message to suspend the AudioContext.
// FIXME: Implement control message queue to run following steps on the rendering thread
// FIXME: 7.1: Attempt to release system resources.
// 7.2: Set the [[rendering thread state]] on the AudioContext to suspended.
set_rendering_state(Bindings::AudioContextState::Suspended);
// 7.3: queue a media element task to execute the following steps:
queue_a_media_element_task([&realm, promise, this]() {
// 7.3.1: Resolve promise.
*promise->resolve();
// 7.3.2: If the state attribute of the AudioContext is not already "suspended":
if (state() != Bindings::AudioContextState::Suspended) {
// 7.3.2.1: Set the state attribute of the AudioContext to "suspended".
set_control_state(Bindings::AudioContextState::Suspended);
// 7.3.2.2: queue a media element task to fire an event named statechange at the AudioContext.
queue_a_media_element_task([&realm, this]() {
this->dispatch_event(DOM::Event::create(realm, HTML::EventNames::statechange));
});
}
});
// 8. Return promise.
return JS::NonnullGCPtr { verify_cast<JS::Promise>(*promise->promise()) };
}
// https://www.w3.org/TR/webaudio/#dom-audiocontext-close
WebIDL::ExceptionOr<JS::NonnullGCPtr<JS::Promise>> AudioContext::close()
{
auto& realm = this->realm();
// 1. If this's relevant global object's associated Document is not fully active then return a promise rejected with "InvalidStateError" DOMException.
auto const& associated_document = verify_cast<HTML::Window>(HTML::relevant_global_object(*this)).associated_document();
if (!associated_document.is_fully_active())
return WebIDL::InvalidStateError::create(realm, "Document is not fully active"_fly_string);
// 2. Let promise be a new Promise.
auto promise = WebIDL::create_promise(realm);
// 3. If the [[control thread state]] flag on the AudioContext is closed reject the promise with InvalidStateError, abort these steps, returning promise.
if (state() == Bindings::AudioContextState::Closed) {
WebIDL::reject_promise(realm, promise, WebIDL::InvalidStateError::create(realm, "Audio context is already closed."_fly_string));
return JS::NonnullGCPtr { verify_cast<JS::Promise>(*promise->promise()) };
}
// 4. Set the [[control thread state]] flag on the AudioContext to closed.
set_control_state(Bindings::AudioContextState::Closed);
// 5. Queue a control message to close the AudioContext.
// FIXME: Implement control message queue to run following steps on the rendering thread
// FIXME: 5.1: Attempt to release system resources.
// 5.2: Set the [[rendering thread state]] to "suspended".
set_rendering_state(Bindings::AudioContextState::Suspended);
// FIXME: 5.3: If this control message is being run in a reaction to the document being unloaded, abort this algorithm.
// 5.4: queue a media element task to execute the following steps:
queue_a_media_element_task([&realm, promise, this]() {
// 5.4.1: Resolve promise.
*promise->resolve();
// 5.4.2: If the state attribute of the AudioContext is not already "closed":
if (state() != Bindings::AudioContextState::Closed) {
// 5.4.2.1: Set the state attribute of the AudioContext to "closed".
set_control_state(Bindings::AudioContextState::Closed);
}
// 5.4.2.2: queue a media element task to fire an event named statechange at the AudioContext.
// FIXME: Attempting to queue another task in here causes an assertion fail at Vector.h:148
this->dispatch_event(DOM::Event::create(realm, HTML::EventNames::statechange));
});
// 6. Return promise
return JS::NonnullGCPtr { verify_cast<JS::Promise>(*promise->promise()) };
}
void AudioContext::queue_a_media_element_task(Function<void()> steps)
{
auto task = HTML::Task::create(vm(), m_media_element_event_task_source.source, HTML::current_settings_object().responsible_document(), JS::create_heap_function(heap(), move(steps)));
HTML::main_thread_event_loop().task_queue().add(move(task));
}
// FIXME: Actually implement the rendering thread
bool AudioContext::start_rendering_audio_graph()
{
bool render_result = true;
return render_result;
}
}