ladybird/Userland/Libraries/LibWeb/WebAudio/AudioContext.cpp
Andreas Kling c0d7f748ed LibWeb: Avoid FlyString lookups when setting IDL interface prototypes
This commit introduces a WEB_SET_PROTOTYPE_FOR_INTERFACE macro that
caches the interface name in a local static FlyString. This means that
we only pay for FlyString-from-literal lookup once per browser lifetime
instead of every time the interface is instantiated.
2024-03-16 16:35:54 +01:00

306 lines
13 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/Intrinsics.h>
#include <LibWeb/DOM/Event.h>
#include <LibWeb/HTML/HTMLMediaElement.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);
for (auto& promise : m_pending_promises)
visitor.visit(promise);
for (auto& promise : m_pending_resume_promises)
visitor.visit(promise);
}
// 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();
// FIXME: 1. If this's relevant global object's associated Document is not fully active then return a promise rejected with "InvalidStateError" DOMException.
// 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& promise : m_pending_resume_promises) {
*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();
// FIXME: 1. If this's relevant global object's associated Document is not fully active then return a promise rejected with "InvalidStateError" DOMException.
// 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();
// FIXME: 1. If this's relevant global object's associated Document is not fully active then return a promise rejected with "InvalidStateError" DOMException.
// 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(JS::SafeFunction<void()> steps)
{
auto task = HTML::Task::create(m_media_element_event_task_source.source, HTML::current_settings_object().responsible_document(), 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;
}
}