2024-04-27 07:45:57 +12:00
/*
* Copyright ( c ) 2024 , Shannon Booth < shannon @ serenityos . org >
2025-10-22 10:51:31 -04:00
* Copyright ( c ) 2025 , Ben Eidson < b . e . eidson @ gmail . com >
2024-04-27 07:45:57 +12:00
*
* SPDX - License - Identifier : BSD - 2 - Clause
*/
# include <LibWeb/Bindings/Intrinsics.h>
2025-10-22 10:51:31 -04:00
# include <LibWeb/DOM/Event.h>
2024-04-27 07:45:57 +12:00
# include <LibWeb/HTML/EventNames.h>
2025-10-22 10:51:31 -04:00
# include <LibWeb/HTML/Navigable.h>
# include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h>
2024-04-27 07:45:57 +12:00
# include <LibWeb/HTML/Window.h>
2025-10-22 10:51:31 -04:00
# include <LibWeb/WebAudio/AudioBuffer.h>
2025-01-07 23:24:10 +00:00
# include <LibWeb/WebAudio/AudioDestinationNode.h>
2025-11-18 16:49:02 +00:00
# include <LibWeb/WebAudio/OfflineAudioCompletionEvent.h>
2024-04-27 07:45:57 +12:00
# include <LibWeb/WebAudio/OfflineAudioContext.h>
namespace Web : : WebAudio {
2024-11-15 04:01:23 +13:00
GC_DEFINE_ALLOCATOR ( OfflineAudioContext ) ;
2024-04-27 07:45:57 +12:00
2025-01-09 22:16:55 +00:00
// https://webaudio.github.io/web-audio-api/#dom-offlineaudiocontext-offlineaudiocontext
2024-11-15 04:01:23 +13:00
WebIDL : : ExceptionOr < GC : : Ref < OfflineAudioContext > > OfflineAudioContext : : construct_impl ( JS : : Realm & realm , OfflineAudioContextOptions const & context_options )
2024-04-27 07:45:57 +12:00
{
2025-01-09 22:16:55 +00:00
// AD-HOC: This spec text is currently only mentioned in the constructor overload that takes separate arguments,
// but these parameters should be validated for both constructors.
// A NotSupportedError exception MUST be thrown if any of the arguments is negative, zero, or outside its nominal range.
TRY ( verify_audio_options_inside_nominal_range ( realm , context_options . number_of_channels , context_options . length , context_options . sample_rate ) ) ;
// Let c be a new OfflineAudioContext object. Initialize c as follows:
2025-10-22 10:51:31 -04:00
auto c = realm . create < OfflineAudioContext > ( realm , context_options . number_of_channels , context_options . length , context_options . sample_rate ) ;
2025-01-09 22:16:55 +00:00
// 1. Set the [[control thread state]] for c to "suspended".
c - > set_control_state ( Bindings : : AudioContextState : : Suspended ) ;
// 2. Set the [[rendering thread state]] for c to "suspended".
c - > set_rendering_state ( Bindings : : AudioContextState : : Suspended ) ;
// FIXME: 3. Determine the [[render quantum size]] for this OfflineAudioContext, based on the value of the renderSizeHint:
// 4. Construct an AudioDestinationNode with its channelCount set to contextOptions.numberOfChannels.
c - > m_destination = TRY ( AudioDestinationNode : : construct_impl ( realm , c , context_options . number_of_channels ) ) ;
// FIXME: 5. Let messageChannel be a new MessageChannel.
// FIXME: 6. Let controlSidePort be the value of messageChannel’ s port1 attribute.
// FIXME: 7. Let renderingSidePort be the value of messageChannel’ s port2 attribute.
// FIXME: 8. Let serializedRenderingSidePort be the result of StructuredSerializeWithTransfer(renderingSidePort, « renderingSidePort »).
// FIXME: 9. Set this audioWorklet's port to controlSidePort.
// FIXME: 10. Queue a control message to set the MessagePort on the AudioContextGlobalScope, with serializedRenderingSidePort.
return c ;
2024-04-27 07:45:57 +12:00
}
// https://webaudio.github.io/web-audio-api/#dom-offlineaudiocontext-offlineaudiocontext-numberofchannels-length-samplerate
2024-11-15 04:01:23 +13:00
WebIDL : : ExceptionOr < GC : : Ref < OfflineAudioContext > > OfflineAudioContext : : construct_impl ( JS : : Realm & realm ,
2024-04-27 07:45:57 +12:00
WebIDL : : UnsignedLong number_of_channels , WebIDL : : UnsignedLong length , float sample_rate )
{
2025-01-09 22:16:55 +00:00
return construct_impl ( realm , { number_of_channels , length , sample_rate } ) ;
2024-04-27 07:45:57 +12:00
}
OfflineAudioContext : : ~ OfflineAudioContext ( ) = default ;
// https://webaudio.github.io/web-audio-api/#dom-offlineaudiocontext-startrendering
2024-11-15 04:01:23 +13:00
WebIDL : : ExceptionOr < GC : : Ref < WebIDL : : Promise > > OfflineAudioContext : : start_rendering ( )
2024-04-27 07:45:57 +12:00
{
2025-10-22 10:51:31 -04:00
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 & window = as < HTML : : Window > ( HTML : : relevant_global_object ( * this ) ) ;
auto const & associated_document = window . associated_document ( ) ;
if ( ! associated_document . is_fully_active ( ) ) {
auto error = WebIDL : : InvalidStateError : : create ( realm , " Document is not fully active " _utf16 ) ;
return WebIDL : : create_rejected_promise_from_exception ( realm , error ) ;
}
// AD-HOC: Not in spec explicitly, but this should account for detached iframes too. See /the-offlineaudiocontext-interface/startrendering-after-discard.html WPT.
auto navigable = window . navigable ( ) ;
if ( navigable & & navigable - > has_been_destroyed ( ) ) {
auto error = WebIDL : : InvalidStateError : : create ( realm , " The iframe has been detached " _utf16 ) ;
return WebIDL : : create_rejected_promise_from_exception ( realm , error ) ;
}
// 2. If the [[rendering started]] slot on the OfflineAudioContext is true, return a rejected promise with InvalidStateError, and abort these steps.
if ( m_rendering_started ) {
auto error = WebIDL : : InvalidStateError : : create ( realm , " Rendering is already started " _utf16 ) ;
return WebIDL : : create_rejected_promise_from_exception ( realm , error ) ;
}
// 3. Set the [[rendering started]] slot of the OfflineAudioContext to true.
m_rendering_started = true ;
// 4. Let promise be a new promise.
auto promise = WebIDL : : create_promise ( realm ) ;
// 5. Create a new AudioBuffer, with a number of channels, length and sample rate equal respectively to the
// numberOfChannels, length and sampleRate values passed to this instance’ s constructor in the contextOptions
// parameter.
auto buffer_result = create_buffer ( m_number_of_channels , length ( ) , sample_rate ( ) ) ;
// 6. If an exception was thrown during the preceding AudioBuffer constructor call, reject promise with this exception.
if ( buffer_result . is_exception ( ) ) {
return WebIDL : : create_rejected_promise_from_exception ( realm , buffer_result . exception ( ) ) ;
}
// Assign this buffer to an internal slot [[rendered buffer]] in the OfflineAudioContext.
m_rendered_buffer = buffer_result . release_value ( ) ;
// 7. Otherwise, in the case that the buffer was successfully constructed, begin offline rendering.
begin_offline_rendering ( promise ) ;
// 8. Append promise to [[pending promises]].
m_pending_promises . append ( promise ) ;
// 9. Return promise.
return promise ;
}
void OfflineAudioContext : : begin_offline_rendering ( GC : : Ref < WebIDL : : Promise > promise )
{
auto & realm = this - > realm ( ) ;
// To begin offline rendering, the following steps MUST happen on a rendering thread that is created for the occasion.
// FIXME: 1: Given the current connections and scheduled changes, start rendering length sample-frames of audio into [[rendered buffer]]
// FIXME: 2: For every render quantum, check and suspend rendering if necessary.
// FIXME: 3: If a suspended context is resumed, continue to render the buffer.
// 4: Once the rendering is complete, queue a media element task to execute the following steps:
queue_a_media_element_task ( GC : : create_function ( heap ( ) , [ & realm , promise , this ] ( ) {
HTML : : TemporaryExecutionContext context ( realm , HTML : : TemporaryExecutionContext : : CallbacksEnabled : : Yes ) ;
// 4.1 Resolve the promise created by startRendering() with [[rendered buffer]].
WebIDL : : resolve_promise ( realm , promise , this - > m_rendered_buffer ) ;
// AD-HOC: Remove resolved promise from [[pending promises]]
// https://github.com/WebAudio/web-audio-api/issues/2648
m_pending_promises . remove_all_matching ( [ promise ] ( GC : : Ref < WebIDL : : Promise > const & p ) {
return p . ptr ( ) = = promise . ptr ( ) ;
} ) ;
// 4.2: Queue a media element task to fire an event named complete at the OfflineAudioContext using OfflineAudioCompletionEvent
// whose renderedBuffer property is set to [[rendered buffer]].
queue_a_media_element_task ( GC : : create_function ( heap ( ) , [ & realm , this ] ( ) {
2025-11-18 16:49:02 +00:00
auto event_init = OfflineAudioCompletionEventInit {
{
. bubbles = false ,
. cancelable = false ,
. composed = false ,
} ,
this - > m_rendered_buffer ,
} ;
auto event = MUST ( OfflineAudioCompletionEvent : : construct_impl ( realm , HTML : : EventNames : : complete , event_init ) ) ;
this - > dispatch_event ( event ) ;
2025-10-22 10:51:31 -04:00
} ) ) ;
} ) ) ;
2024-04-27 07:45:57 +12:00
}
2024-11-15 04:01:23 +13:00
WebIDL : : ExceptionOr < GC : : Ref < WebIDL : : Promise > > OfflineAudioContext : : resume ( )
2024-04-27 07:45:57 +12:00
{
2025-08-07 19:31:52 -04:00
return WebIDL : : NotSupportedError : : create ( realm ( ) , " FIXME: Implement OfflineAudioContext::resume " _utf16 ) ;
2024-04-27 07:45:57 +12:00
}
2024-11-15 04:01:23 +13:00
WebIDL : : ExceptionOr < GC : : Ref < WebIDL : : Promise > > OfflineAudioContext : : suspend ( double suspend_time )
2024-04-27 07:45:57 +12:00
{
( void ) suspend_time ;
2025-08-07 19:31:52 -04:00
return WebIDL : : NotSupportedError : : create ( realm ( ) , " FIXME: Implement OfflineAudioContext::suspend " _utf16 ) ;
2024-04-27 07:45:57 +12:00
}
// https://webaudio.github.io/web-audio-api/#dom-offlineaudiocontext-length
WebIDL : : UnsignedLong OfflineAudioContext : : length ( ) const
{
// The size of the buffer in sample-frames. This is the same as the value of the length parameter for the constructor.
return m_length ;
}
// https://webaudio.github.io/web-audio-api/#dom-offlineaudiocontext-oncomplete
2024-11-15 04:01:23 +13:00
GC : : Ptr < WebIDL : : CallbackType > OfflineAudioContext : : oncomplete ( )
2024-04-27 07:45:57 +12:00
{
return event_handler_attribute ( HTML : : EventNames : : complete ) ;
}
// https://webaudio.github.io/web-audio-api/#dom-offlineaudiocontext-oncomplete
2024-11-15 04:01:23 +13:00
void OfflineAudioContext : : set_oncomplete ( GC : : Ptr < WebIDL : : CallbackType > value )
2024-04-27 07:45:57 +12:00
{
set_event_handler_attribute ( HTML : : EventNames : : complete , value ) ;
}
2025-10-22 10:51:31 -04:00
OfflineAudioContext : : OfflineAudioContext ( JS : : Realm & realm , WebIDL : : UnsignedLong number_of_channels , WebIDL : : UnsignedLong length , float sample_rate )
2024-05-02 00:03:32 +12:00
: BaseAudioContext ( realm , sample_rate )
2024-04-27 07:45:57 +12:00
, m_length ( length )
2025-10-22 10:51:31 -04:00
, m_number_of_channels ( number_of_channels )
2024-04-27 07:45:57 +12:00
{
}
void OfflineAudioContext : : initialize ( JS : : Realm & realm )
{
WEB_SET_PROTOTYPE_FOR_INTERFACE ( OfflineAudioContext ) ;
2025-04-20 16:22:57 +02:00
Base : : initialize ( realm ) ;
2024-04-27 07:45:57 +12:00
}
void OfflineAudioContext : : visit_edges ( Cell : : Visitor & visitor )
{
Base : : visit_edges ( visitor ) ;
2025-10-22 10:51:31 -04:00
visitor . visit ( m_rendered_buffer ) ;
2024-04-27 07:45:57 +12:00
}
}