Skip to content

Commit

Permalink
LibWeb: Add barebones BaseAudioContext.decodeAudioData()
Browse files Browse the repository at this point in the history
Implement just enough steps to get https://zty.pe/ working! :^)
  • Loading branch information
gmta committed Oct 15, 2024
1 parent 30382c2 commit 786062c
Show file tree
Hide file tree
Showing 3 changed files with 130 additions and 3 deletions.
122 changes: 122 additions & 0 deletions Userland/Libraries/LibWeb/WebAudio/BaseAudioContext.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
#include <LibWeb/Bindings/BaseAudioContextPrototype.h>
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/HTML/EventNames.h>
#include <LibWeb/HTML/Scripting/ExceptionReporter.h>
#include <LibWeb/HTML/Window.h>
#include <LibWeb/WebAudio/AudioBuffer.h>
#include <LibWeb/WebAudio/AudioBufferSourceNode.h>
#include <LibWeb/WebAudio/AudioDestinationNode.h>
Expand All @@ -17,6 +19,8 @@
#include <LibWeb/WebAudio/DynamicsCompressorNode.h>
#include <LibWeb/WebAudio/GainNode.h>
#include <LibWeb/WebAudio/OscillatorNode.h>
#include <LibWeb/WebIDL/AbstractOperations.h>
#include <LibWeb/WebIDL/Promise.h>

namespace Web::WebAudio {

Expand Down Expand Up @@ -120,4 +124,122 @@ void BaseAudioContext::queue_a_media_element_task(JS::NonnullGCPtr<JS::HeapFunct
HTML::main_thread_event_loop().task_queue().add(task);
}

// https://webaudio.github.io/web-audio-api/#dom-baseaudiocontext-decodeaudiodata
JS::NonnullGCPtr<JS::Promise> BaseAudioContext::decode_audio_data(JS::Handle<WebIDL::BufferSource> audio_data, JS::GCPtr<WebIDL::CallbackType> success_callback, JS::GCPtr<WebIDL::CallbackType> error_callback)
{
auto& realm = this->realm();

// FIXME: When decodeAudioData is called, the following steps MUST be performed on the control thread:

// 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()) {
auto error = WebIDL::InvalidStateError::create(realm, "The document is not fully active."_string);
return WebIDL::create_rejected_promise_from_exception(realm, error);
}

// 2. Let promise be a new Promise.
auto promise = WebIDL::create_promise(realm);

// FIXME: 3. If audioData is detached, execute the following steps:
if (true) {
// FIXME: 3.1. Append promise to [[pending promises]].

// FIXME: 3.2. Detach the audioData ArrayBuffer. If this operations throws, jump to the step 3.

// 3.3. Queue a decoding operation to be performed on another thread.
queue_a_decoding_operation(promise, move(audio_data), success_callback, error_callback);
}

// 4. Else, execute the following error steps:
else {
// 4.1. Let error be a DataCloneError.
auto error = WebIDL::DataCloneError::create(realm, "Audio data is not detached."_string);

// 4.2. Reject promise with error, and remove it from [[pending promises]].
WebIDL::reject_promise(realm, promise, error);

// 4.3. Queue a media element task to invoke errorCallback with error.
if (error_callback) {
queue_a_media_element_task(JS::create_heap_function(heap(), [&realm, error_callback, error] {
auto completion = WebIDL::invoke_callback(*error_callback, {}, error);
if (completion.is_abrupt())
HTML::report_exception(completion, realm);
}));
}
}

// 5. Return promise.
return verify_cast<JS::Promise>(*promise->promise());
}

// https://webaudio.github.io/web-audio-api/#dom-baseaudiocontext-decodeaudiodata
void BaseAudioContext::queue_a_decoding_operation(JS::NonnullGCPtr<JS::PromiseCapability> promise, [[maybe_unused]] JS::Handle<WebIDL::BufferSource> audio_data, JS::GCPtr<WebIDL::CallbackType> success_callback, JS::GCPtr<WebIDL::CallbackType> error_callback)
{
auto& realm = this->realm();

// FIXME: When queuing a decoding operation to be performed on another thread, the following steps
// MUST happen on a thread that is not the control thread nor the rendering thread, called
// the decoding thread.

// 1. Let can decode be a boolean flag, initially set to true.
auto can_decode { true };

// FIXME: 2. Attempt to determine the MIME type of audioData, using MIME Sniffing § 6.2 Matching an
// audio or video type pattern. If the audio or video type pattern matching algorithm returns
// undefined, set can decode to false.

// 3. If can decode is true,
if (can_decode) {
// FIXME: attempt to decode the encoded audioData into linear PCM. In case of
// failure, set can decode to false.

// FIXME: If the media byte-stream contains multiple audio tracks, only decode the first track to linear pcm.
}

// 4. If can decode is false,
if (!can_decode) {
// queue a media element task to execute the following steps:
queue_a_media_element_task(JS::create_heap_function(heap(), [&realm, promise, error_callback] {
// 4.1. Let error be a DOMException whose name is EncodingError.
auto error = WebIDL::EncodingError::create(realm, "Unable to decode."_string);

// 4.1.2. Reject promise with error,
WebIDL::reject_promise(realm, promise, error);
// FIXME: and remove it from [[pending promises]].

// 4.2. If errorCallback is not missing, invoke errorCallback with error.
if (error_callback) {
auto completion = WebIDL::invoke_callback(*error_callback, {}, error);
if (completion.is_abrupt())
HTML::report_exception(completion, realm);
}
}));
}

// 5. Otherwise:
else {
// FIXME: 5.1. Take the result, representing the decoded linear PCM audio data, and resample it to the
// sample-rate of the BaseAudioContext if it is different from the sample-rate of
// audioData.

// FIXME: 5.2. queue a media element task to execute the following steps:

// FIXME: 5.2.1. Let buffer be an AudioBuffer containing the final result (after possibly performing
// sample-rate conversion).
auto buffer = MUST(create_buffer(2, 1, 44100));

// 5.2.2. Resolve promise with buffer.
WebIDL::resolve_promise(realm, promise, buffer);

// 5.2.3. If successCallback is not missing, invoke successCallback with buffer.
if (success_callback) {
auto completion = WebIDL::invoke_callback(*success_callback, {}, buffer);
if (completion.is_abrupt())
HTML::report_exception(completion, realm);
}
}
}

}
4 changes: 4 additions & 0 deletions Userland/Libraries/LibWeb/WebAudio/BaseAudioContext.h
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ class BaseAudioContext : public DOM::EventTarget {
WebIDL::ExceptionOr<JS::NonnullGCPtr<DynamicsCompressorNode>> create_dynamics_compressor();
JS::NonnullGCPtr<GainNode> create_gain();

JS::NonnullGCPtr<JS::Promise> decode_audio_data(JS::Handle<WebIDL::BufferSource>, JS::GCPtr<WebIDL::CallbackType>, JS::GCPtr<WebIDL::CallbackType>);

protected:
explicit BaseAudioContext(JS::Realm&, float m_sample_rate = 0);

Expand All @@ -70,6 +72,8 @@ class BaseAudioContext : public DOM::EventTarget {
JS::NonnullGCPtr<AudioDestinationNode> m_destination;

private:
void queue_a_decoding_operation(JS::NonnullGCPtr<JS::PromiseCapability>, JS::Handle<WebIDL::BufferSource>, JS::GCPtr<WebIDL::CallbackType>, JS::GCPtr<WebIDL::CallbackType>);

float m_sample_rate { 0 };
double m_current_time { 0 };

Expand Down
7 changes: 4 additions & 3 deletions Userland/Libraries/LibWeb/WebAudio/BaseAudioContext.idl
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@
#import <WebAudio/DynamicsCompressorNode.idl>
#import <WebAudio/GainNode.idl>
#import <WebAudio/OscillatorNode.idl>
#import <WebIDL/DOMException.idl>

// https://www.w3.org/TR/webaudio/#enumdef-audiocontextstate
enum AudioContextState { "suspended", "running", "closed" };

// FIXME: callback DecodeErrorCallback = undefined (DOMException error);
callback DecodeErrorCallback = undefined (DOMException error);

// FIXME: callback DecodeSuccessCallback = undefined (AudioBuffer decodedData);
callback DecodeSuccessCallback = undefined (AudioBuffer decodedData);

// https://webaudio.github.io/web-audio-api/#BaseAudioContext
[Exposed=Window]
Expand Down Expand Up @@ -45,5 +46,5 @@ interface BaseAudioContext : EventTarget {
[FIXME] StereoPannerNode createStereoPanner ();
[FIXME] WaveShaperNode createWaveShaper ();

[FIXME] Promise<AudioBuffer> decodeAudioData (ArrayBuffer audioData, optional DecodeSuccessCallback? successCallback, optional DecodeErrorCallback? errorCallback);
Promise<AudioBuffer> decodeAudioData (ArrayBuffer audioData, optional DecodeSuccessCallback? successCallback, optional DecodeErrorCallback? errorCallback);
};

0 comments on commit 786062c

Please sign in to comment.