LibWeb: Add barebones BaseAudioContext.decodeAudioData()

Implement just enough steps to get https://zty.pe/ working! :^)

(cherry picked from commit 14b2e5849d4b82aec6900253f6f035a9b356bad3)
This commit is contained in:
Jelle Raaijmakers 2024-10-15 00:42:26 +02:00 committed by Nico Weber
parent 4e0c54fa61
commit fb8870d710
3 changed files with 130 additions and 3 deletions

View file

@ -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>
@ -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 {
@ -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);
}
}
}
}

View file

@ -59,6 +59,8 @@ public:
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);
@ -70,6 +72,8 @@ protected:
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 };

View file

@ -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]
@ -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);
};