2023-05-21 20:36:22 +01:00
/*
* Copyright ( c ) 2023 , Luke Wilde < lukew @ serenityos . org >
*
* SPDX - License - Identifier : BSD - 2 - Clause
*/
2024-04-27 12:09:58 +12:00
# include <LibWeb/Bindings/AudioContextPrototype.h>
2023-05-21 20:36:22 +01:00
# include <LibWeb/Bindings/Intrinsics.h>
2023-06-27 13:34:51 -04:00
# include <LibWeb/DOM/Event.h>
# include <LibWeb/HTML/HTMLMediaElement.h>
2025-01-09 15:17:45 +00:00
# include <LibWeb/HTML/MessageChannel.h>
# include <LibWeb/HTML/MessagePort.h>
2024-10-27 13:41:28 +13:00
# include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h>
2024-04-25 14:49:56 +12:00
# include <LibWeb/HTML/Window.h>
2023-05-21 20:36:22 +01:00
# include <LibWeb/WebAudio/AudioContext.h>
2025-01-07 23:24:10 +00:00
# include <LibWeb/WebAudio/AudioDestinationNode.h>
2023-06-27 13:34:51 -04:00
# include <LibWeb/WebIDL/Promise.h>
2023-05-21 20:36:22 +01:00
namespace Web : : WebAudio {
2024-11-15 04:01:23 +13:00
GC_DEFINE_ALLOCATOR ( AudioContext ) ;
2023-11-19 19:47:52 +01:00
2023-05-21 20:36:22 +01:00
// https://webaudio.github.io/web-audio-api/#dom-audiocontext-audiocontext
2025-01-09 15:17:45 +00:00
WebIDL : : ExceptionOr < GC : : Ref < AudioContext > > AudioContext : : construct_impl ( JS : : Realm & realm , Optional < AudioContextOptions > const & context_options )
2023-05-21 20:36:22 +01:00
{
2025-01-09 15:17:45 +00:00
// If the current settings object’ s responsible document is NOT fully active, throw an InvalidStateError and abort these steps.
auto & settings = HTML : : current_principal_settings_object ( ) ;
2023-05-21 20:36:22 +01:00
2025-01-09 15:17:45 +00:00
// FIXME: Not all settings objects currently return a responsible document.
// Therefore we only fail this check if responsible document is not null.
if ( ! settings . responsible_document ( ) | | ! settings . responsible_document ( ) - > is_fully_active ( ) ) {
return WebIDL : : InvalidStateError : : create ( realm , " Document is not fully active " _string ) ;
2023-06-27 13:34:51 -04:00
}
2025-01-09 15:17:45 +00:00
// AD-HOC: The spec doesn't currently require the sample rate to be validated here,
// but other browsers do perform a check and there is a WPT test that expects this.
if ( context_options . has_value ( ) & & context_options - > sample_rate . has_value ( ) )
TRY ( verify_audio_options_inside_nominal_range ( realm , * context_options - > sample_rate ) ) ;
// 1. Let context be a new AudioContext object.
auto context = realm . create < AudioContext > ( realm ) ;
context - > m_destination = TRY ( AudioDestinationNode : : construct_impl ( realm , context ) ) ;
// 2. Set a [[control thread state]] to suspended on context.
context - > set_control_state ( Bindings : : AudioContextState : : Suspended ) ;
// 3. Set a [[rendering thread state]] to suspended on context.
context - > set_rendering_state ( Bindings : : AudioContextState : : Suspended ) ;
// FIXME: 4. Let messageChannel be a new MessageChannel.
// FIXME: 5. Let controlSidePort be the value of messageChannel’ s port1 attribute.
// FIXME: 6. Let renderingSidePort be the value of messageChannel’ s port2 attribute.
// FIXME: 7. Let serializedRenderingSidePort be the result of StructuredSerializeWithTransfer(renderingSidePort, « renderingSidePort »).
// FIXME: 8. Set this audioWorklet's port to controlSidePort.
// FIXME: 9. Queue a control message to set the MessagePort on the AudioContextGlobalScope, with serializedRenderingSidePort.
// 10. If contextOptions is given, apply the options:
if ( context_options . has_value ( ) ) {
// 1. If sinkId is specified, let sinkId be the value of contextOptions.sinkId and run the following substeps:
// 2. Set the internal latency of context 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 ( ) ;
}
// 3: If contextOptions.sampleRate is specified, set the sampleRate of context to this value.
if ( context_options - > sample_rate . has_value ( ) ) {
context - > set_sample_rate ( context_options - > sample_rate . value ( ) ) ;
}
// Otherwise, follow these substeps:
else {
// FIXME: 1. If sinkId is the empty string or a type of AudioSinkOptions, use the sample rate of the default output device. Abort these substeps.
// FIXME: 2. If sinkId is a DOMString, use the sample rate of the output device identified by sinkId. Abort these substeps.
// If contextOptions.sampleRate differs from the sample rate of the output device, the user agent MUST resample the audio output to match the sample rate of the output device.
context - > set_sample_rate ( 44100 ) ;
}
2023-06-27 13:34:51 -04:00
}
2025-01-09 15:17:45 +00:00
// FIXME: 11. If context is allowed to start, send a control message to start processing.
2023-06-27 13:34:51 -04:00
// FIXME: Implement control message queue to run following steps on the rendering thread
2025-01-09 15:17:45 +00:00
if ( context - > m_allowed_to_start ) {
// FIXME: 1. Let document be the current settings object's relevant global object's associated Document.
// FIXME: 2. Attempt to acquire system resources to use a following audio output device based on [[sink ID]] for rendering
2023-06-27 13:34:51 -04:00
2025-01-09 15:17:45 +00:00
// 2. Set this [[rendering thread state]] to running on the AudioContext.
context - > set_rendering_state ( Bindings : : AudioContextState : : Running ) ;
2023-06-27 13:34:51 -04:00
2025-01-09 15:17:45 +00:00
// 3. Queue a media element task to execute the following steps:
context - > queue_a_media_element_task ( GC : : create_function ( context - > heap ( ) , [ & realm , context ] ( ) {
// 1. Set the state attribute of the AudioContext to "running".
context - > set_control_state ( Bindings : : AudioContextState : : Running ) ;
2023-06-27 13:34:51 -04:00
2025-01-09 15:17:45 +00:00
// 2. Fire an event named statechange at the AudioContext.
context - > dispatch_event ( DOM : : Event : : create ( realm , HTML : : EventNames : : statechange ) ) ;
2024-10-15 09:34:35 +02:00
} ) ) ;
2023-06-27 13:34:51 -04:00
}
2025-01-09 15:17:45 +00:00
// 12. Return context.
return context ;
2023-05-21 20:36:22 +01:00
}
AudioContext : : ~ AudioContext ( ) = default ;
2023-08-07 08:41:28 +02:00
void AudioContext : : initialize ( JS : : Realm & realm )
2023-05-21 20:36:22 +01:00
{
2024-03-16 13:13:08 +01:00
WEB_SET_PROTOTYPE_FOR_INTERFACE ( AudioContext ) ;
2025-04-20 16:22:57 +02:00
Base : : initialize ( realm ) ;
2023-05-21 20:36:22 +01:00
}
2023-06-27 13:34:51 -04:00
void AudioContext : : visit_edges ( Cell : : Visitor & visitor )
{
Base : : visit_edges ( visitor ) ;
2024-04-15 13:58:21 +02:00
visitor . visit ( m_pending_resume_promises ) ;
2023-06-27 13:34:51 -04:00
}
// 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
2024-11-15 04:01:23 +13:00
WebIDL : : ExceptionOr < GC : : Ref < WebIDL : : Promise > > AudioContext : : resume ( )
2023-06-27 13:34:51 -04:00
{
auto & realm = this - > realm ( ) ;
2024-04-25 14:49:56 +12:00
// 1. If this's relevant global object's associated Document is not fully active then return a promise rejected with "InvalidStateError" DOMException.
2025-01-21 09:12:05 -05:00
auto const & associated_document = as < HTML : : Window > ( HTML : : relevant_global_object ( * this ) ) . associated_document ( ) ;
2024-04-25 14:49:56 +12:00
if ( ! associated_document . is_fully_active ( ) )
2024-10-12 20:56:21 +02:00
return WebIDL : : InvalidStateError : : create ( realm , " Document is not fully active " _string ) ;
2023-06-27 13:34:51 -04:00
// 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 ) {
2024-10-12 20:56:21 +02:00
WebIDL : : reject_promise ( realm , promise , WebIDL : : InvalidStateError : : create ( realm , " Audio context is already closed. " _string ) ) ;
2024-10-25 12:38:19 -06:00
return promise ;
2023-06-27 13:34:51 -04:00
}
// 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 ) {
2024-10-17 10:46:27 +02:00
m_pending_promises . append ( promise ) ;
m_pending_resume_promises . append ( promise ) ;
2023-06-27 13:34:51 -04:00
}
// 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:
2024-11-15 04:01:23 +13:00
queue_a_media_element_task ( GC : : create_function ( heap ( ) , [ & realm , this ] ( ) {
2024-10-24 20:39:18 +13:00
HTML : : TemporaryExecutionContext context ( realm , HTML : : TemporaryExecutionContext : : CallbacksEnabled : : Yes ) ;
2024-10-27 13:41:28 +13:00
2023-06-27 13:34:51 -04:00
// 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 ( ) ) ;
2024-10-17 10:46:27 +02:00
// 7.4.2: Additionally, remove those promises from [[pending promises]].
m_pending_promises . remove_first_matching ( [ & promise ] ( auto & pending_promise ) {
return pending_promise = = promise ;
} ) ;
2023-06-27 13:34:51 -04:00
}
m_pending_resume_promises . clear ( ) ;
2024-10-15 09:34:35 +02:00
} ) ) ;
2023-06-27 13:34:51 -04:00
}
// 7.5: queue a media element task to execute the following steps:
2024-11-15 04:01:23 +13:00
queue_a_media_element_task ( GC : : create_function ( heap ( ) , [ & realm , promise , this ] ( ) {
2024-10-24 20:39:18 +13:00
HTML : : TemporaryExecutionContext context ( realm , HTML : : TemporaryExecutionContext : : CallbacksEnabled : : Yes ) ;
2024-10-27 13:41:28 +13:00
2023-06-27 13:34:51 -04:00
// 7.5.1: Resolve all promises from [[pending resume promises]] in order.
2024-10-17 10:46:27 +02:00
// 7.5.2: Clear [[pending resume promises]]. Additionally, remove those promises from
// [[pending promises]].
2024-03-30 08:16:03 +01:00
for ( auto const & pending_resume_promise : m_pending_resume_promises ) {
2024-10-24 22:35:41 +02:00
WebIDL : : resolve_promise ( realm , pending_resume_promise , JS : : js_undefined ( ) ) ;
2024-10-17 10:46:27 +02:00
m_pending_promises . remove_first_matching ( [ & pending_resume_promise ] ( auto & pending_promise ) {
return pending_promise = = pending_resume_promise ;
} ) ;
2023-06-27 13:34:51 -04:00
}
m_pending_resume_promises . clear ( ) ;
// 7.5.3: Resolve promise.
2024-10-24 22:35:41 +02:00
WebIDL : : resolve_promise ( realm , promise , JS : : js_undefined ( ) ) ;
2023-06-27 13:34:51 -04:00
// 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.
2024-11-15 04:01:23 +13:00
queue_a_media_element_task ( GC : : create_function ( heap ( ) , [ & realm , this ] ( ) {
2023-08-13 13:05:26 +02:00
this - > dispatch_event ( DOM : : Event : : create ( realm , HTML : : EventNames : : statechange ) ) ;
2024-10-15 09:34:35 +02:00
} ) ) ;
2023-06-27 13:34:51 -04:00
}
2024-10-15 09:34:35 +02:00
} ) ) ;
2023-06-27 13:34:51 -04:00
// 8. Return promise.
2024-10-25 12:38:19 -06:00
return promise ;
2023-06-27 13:34:51 -04:00
}
// https://www.w3.org/TR/webaudio/#dom-audiocontext-suspend
2024-11-15 04:01:23 +13:00
WebIDL : : ExceptionOr < GC : : Ref < WebIDL : : Promise > > AudioContext : : suspend ( )
2023-06-27 13:34:51 -04:00
{
auto & realm = this - > realm ( ) ;
2024-04-25 14:49:56 +12:00
// 1. If this's relevant global object's associated Document is not fully active then return a promise rejected with "InvalidStateError" DOMException.
2025-01-21 09:12:05 -05:00
auto const & associated_document = as < HTML : : Window > ( HTML : : relevant_global_object ( * this ) ) . associated_document ( ) ;
2024-04-25 14:49:56 +12:00
if ( ! associated_document . is_fully_active ( ) )
2024-10-12 20:56:21 +02:00
return WebIDL : : InvalidStateError : : create ( realm , " Document is not fully active " _string ) ;
2023-06-27 13:34:51 -04:00
// 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 ) {
2024-10-12 20:56:21 +02:00
WebIDL : : reject_promise ( realm , promise , WebIDL : : InvalidStateError : : create ( realm , " Audio context is already closed. " _string ) ) ;
2024-10-25 12:38:19 -06:00
return promise ;
2023-06-27 13:34:51 -04:00
}
// 4. Append promise to [[pending promises]].
2024-10-17 10:46:27 +02:00
m_pending_promises . append ( promise ) ;
2023-06-27 13:34:51 -04:00
// 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:
2024-11-15 04:01:23 +13:00
queue_a_media_element_task ( GC : : create_function ( heap ( ) , [ & realm , promise , this ] ( ) {
2024-10-24 20:39:18 +13:00
HTML : : TemporaryExecutionContext context ( realm , HTML : : TemporaryExecutionContext : : CallbacksEnabled : : Yes ) ;
2024-10-27 13:41:28 +13:00
2023-06-27 13:34:51 -04:00
// 7.3.1: Resolve promise.
2024-10-24 22:35:41 +02:00
WebIDL : : resolve_promise ( realm , promise , JS : : js_undefined ( ) ) ;
2023-06-27 13:34:51 -04:00
// 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.
2024-11-15 04:01:23 +13:00
queue_a_media_element_task ( GC : : create_function ( heap ( ) , [ & realm , this ] ( ) {
2023-08-13 13:05:26 +02:00
this - > dispatch_event ( DOM : : Event : : create ( realm , HTML : : EventNames : : statechange ) ) ;
2024-10-15 09:34:35 +02:00
} ) ) ;
2023-06-27 13:34:51 -04:00
}
2024-10-15 09:34:35 +02:00
} ) ) ;
2023-06-27 13:34:51 -04:00
// 8. Return promise.
2024-10-25 12:38:19 -06:00
return promise ;
2023-06-27 13:34:51 -04:00
}
// https://www.w3.org/TR/webaudio/#dom-audiocontext-close
2024-11-15 04:01:23 +13:00
WebIDL : : ExceptionOr < GC : : Ref < WebIDL : : Promise > > AudioContext : : close ( )
2023-06-27 13:34:51 -04:00
{
auto & realm = this - > realm ( ) ;
2024-04-25 14:49:56 +12:00
// 1. If this's relevant global object's associated Document is not fully active then return a promise rejected with "InvalidStateError" DOMException.
2025-01-21 09:12:05 -05:00
auto const & associated_document = as < HTML : : Window > ( HTML : : relevant_global_object ( * this ) ) . associated_document ( ) ;
2024-04-25 14:49:56 +12:00
if ( ! associated_document . is_fully_active ( ) )
2024-10-12 20:56:21 +02:00
return WebIDL : : InvalidStateError : : create ( realm , " Document is not fully active " _string ) ;
2023-06-27 13:34:51 -04:00
// 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 ) {
2024-10-12 20:56:21 +02:00
WebIDL : : reject_promise ( realm , promise , WebIDL : : InvalidStateError : : create ( realm , " Audio context is already closed. " _string ) ) ;
2024-10-25 12:38:19 -06:00
return promise ;
2023-06-27 13:34:51 -04:00
}
// 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:
2024-11-15 04:01:23 +13:00
queue_a_media_element_task ( GC : : create_function ( heap ( ) , [ & realm , promise , this ] ( ) {
2024-10-24 20:39:18 +13:00
HTML : : TemporaryExecutionContext context ( realm , HTML : : TemporaryExecutionContext : : CallbacksEnabled : : Yes ) ;
2024-10-27 13:41:28 +13:00
2023-06-27 13:34:51 -04:00
// 5.4.1: Resolve promise.
2024-10-24 22:35:41 +02:00
WebIDL : : resolve_promise ( realm , promise , JS : : js_undefined ( ) ) ;
2023-06-27 13:34:51 -04:00
// 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
2023-08-13 13:05:26 +02:00
this - > dispatch_event ( DOM : : Event : : create ( realm , HTML : : EventNames : : statechange ) ) ;
2024-10-15 09:34:35 +02:00
} ) ) ;
2023-06-27 13:34:51 -04:00
// 6. Return promise
2024-10-25 12:38:19 -06:00
return promise ;
2023-06-27 13:34:51 -04:00
}
// FIXME: Actually implement the rendering thread
bool AudioContext : : start_rendering_audio_graph ( )
{
bool render_result = true ;
return render_result ;
}
2025-01-15 23:08:23 +00:00
// https://webaudio.github.io/web-audio-api/#dom-audiocontext-createmediaelementsource
WebIDL : : ExceptionOr < GC : : Ref < MediaElementAudioSourceNode > > AudioContext : : create_media_element_source ( GC : : Ptr < HTML : : HTMLMediaElement > media_element )
{
MediaElementAudioSourceOptions options ;
options . media_element = media_element ;
return MediaElementAudioSourceNode : : create ( realm ( ) , * this , options ) ;
}
2023-05-21 20:36:22 +01:00
}