/src/serenity/Userland/Libraries/LibWeb/WebAudio/BaseAudioContext.cpp
Line | Count | Source (jump to first uncovered line) |
1 | | /* |
2 | | * Copyright (c) 2023, Luke Wilde <lukew@serenityos.org> |
3 | | * Copyright (c) 2024, Shannon Booth <shannon@serenityos.org> |
4 | | * Copyright (c) 2024, Jelle Raaijmakers <jelle@ladybird.org> |
5 | | * |
6 | | * SPDX-License-Identifier: BSD-2-Clause |
7 | | */ |
8 | | |
9 | | #include <LibWeb/Bindings/BaseAudioContextPrototype.h> |
10 | | #include <LibWeb/Bindings/Intrinsics.h> |
11 | | #include <LibWeb/HTML/EventNames.h> |
12 | | #include <LibWeb/HTML/Scripting/ExceptionReporter.h> |
13 | | #include <LibWeb/HTML/Window.h> |
14 | | #include <LibWeb/WebAudio/AudioBuffer.h> |
15 | | #include <LibWeb/WebAudio/AudioBufferSourceNode.h> |
16 | | #include <LibWeb/WebAudio/AudioDestinationNode.h> |
17 | | #include <LibWeb/WebAudio/BaseAudioContext.h> |
18 | | #include <LibWeb/WebAudio/BiquadFilterNode.h> |
19 | | #include <LibWeb/WebAudio/DynamicsCompressorNode.h> |
20 | | #include <LibWeb/WebAudio/GainNode.h> |
21 | | #include <LibWeb/WebAudio/OscillatorNode.h> |
22 | | #include <LibWeb/WebIDL/AbstractOperations.h> |
23 | | #include <LibWeb/WebIDL/Promise.h> |
24 | | |
25 | | namespace Web::WebAudio { |
26 | | |
27 | | BaseAudioContext::BaseAudioContext(JS::Realm& realm, float sample_rate) |
28 | 0 | : DOM::EventTarget(realm) |
29 | 0 | , m_destination(AudioDestinationNode::construct_impl(realm, *this)) |
30 | 0 | , m_sample_rate(sample_rate) |
31 | 0 | , m_listener(AudioListener::create(realm)) |
32 | 0 | { |
33 | 0 | } |
34 | | |
35 | 0 | BaseAudioContext::~BaseAudioContext() = default; |
36 | | |
37 | | void BaseAudioContext::initialize(JS::Realm& realm) |
38 | 0 | { |
39 | 0 | Base::initialize(realm); |
40 | 0 | WEB_SET_PROTOTYPE_FOR_INTERFACE(BaseAudioContext); |
41 | 0 | } |
42 | | |
43 | | void BaseAudioContext::visit_edges(Cell::Visitor& visitor) |
44 | 0 | { |
45 | 0 | Base::visit_edges(visitor); |
46 | 0 | visitor.visit(m_destination); |
47 | 0 | visitor.visit(m_pending_promises); |
48 | 0 | visitor.visit(m_listener); |
49 | 0 | } |
50 | | |
51 | | void BaseAudioContext::set_onstatechange(WebIDL::CallbackType* event_handler) |
52 | 0 | { |
53 | 0 | set_event_handler_attribute(HTML::EventNames::statechange, event_handler); |
54 | 0 | } |
55 | | |
56 | | WebIDL::CallbackType* BaseAudioContext::onstatechange() |
57 | 0 | { |
58 | 0 | return event_handler_attribute(HTML::EventNames::statechange); |
59 | 0 | } |
60 | | |
61 | | // https://webaudio.github.io/web-audio-api/#dom-baseaudiocontext-createbiquadfilter |
62 | | WebIDL::ExceptionOr<JS::NonnullGCPtr<BiquadFilterNode>> BaseAudioContext::create_biquad_filter() |
63 | 0 | { |
64 | | // Factory method for a BiquadFilterNode representing a second order filter which can be configured as one of several common filter types. |
65 | 0 | return BiquadFilterNode::create(realm(), *this); |
66 | 0 | } |
67 | | |
68 | | // https://webaudio.github.io/web-audio-api/#dom-baseaudiocontext-createbuffer |
69 | | WebIDL::ExceptionOr<JS::NonnullGCPtr<AudioBuffer>> BaseAudioContext::create_buffer(WebIDL::UnsignedLong number_of_channels, WebIDL::UnsignedLong length, float sample_rate) |
70 | 0 | { |
71 | | // Creates an AudioBuffer of the given size. The audio data in the buffer will be zero-initialized (silent). |
72 | | // A NotSupportedError exception MUST be thrown if any of the arguments is negative, zero, or outside its nominal range. |
73 | 0 | return AudioBuffer::create(realm(), number_of_channels, length, sample_rate); |
74 | 0 | } |
75 | | |
76 | | // https://webaudio.github.io/web-audio-api/#dom-baseaudiocontext-createbuffersource |
77 | | WebIDL::ExceptionOr<JS::NonnullGCPtr<AudioBufferSourceNode>> BaseAudioContext::create_buffer_source() |
78 | 0 | { |
79 | | // Factory method for a AudioBufferSourceNode. |
80 | 0 | return AudioBufferSourceNode::create(realm(), *this); |
81 | 0 | } |
82 | | |
83 | | // https://webaudio.github.io/web-audio-api/#dom-baseaudiocontext-createoscillator |
84 | | WebIDL::ExceptionOr<JS::NonnullGCPtr<OscillatorNode>> BaseAudioContext::create_oscillator() |
85 | 0 | { |
86 | | // Factory method for an OscillatorNode. |
87 | 0 | return OscillatorNode::create(realm(), *this); |
88 | 0 | } |
89 | | |
90 | | // https://webaudio.github.io/web-audio-api/#dom-baseaudiocontext-createdynamicscompressor |
91 | | WebIDL::ExceptionOr<JS::NonnullGCPtr<DynamicsCompressorNode>> BaseAudioContext::create_dynamics_compressor() |
92 | 0 | { |
93 | | // Factory method for a DynamicsCompressorNode. |
94 | 0 | return DynamicsCompressorNode::create(realm(), *this); |
95 | 0 | } |
96 | | |
97 | | // https://webaudio.github.io/web-audio-api/#dom-baseaudiocontext-creategain |
98 | | WebIDL::ExceptionOr<JS::NonnullGCPtr<GainNode>> BaseAudioContext::create_gain() |
99 | 0 | { |
100 | | // Factory method for GainNode. |
101 | 0 | return GainNode::create(realm(), *this); |
102 | 0 | } |
103 | | |
104 | | // https://webaudio.github.io/web-audio-api/#dom-baseaudiocontext-createbuffer |
105 | | WebIDL::ExceptionOr<void> BaseAudioContext::verify_audio_options_inside_nominal_range(JS::Realm& realm, WebIDL::UnsignedLong number_of_channels, WebIDL::UnsignedLong length, float sample_rate) |
106 | 0 | { |
107 | | // A NotSupportedError exception MUST be thrown if any of the arguments is negative, zero, or outside its nominal range. |
108 | |
|
109 | 0 | if (number_of_channels == 0) |
110 | 0 | return WebIDL::NotSupportedError::create(realm, "Number of channels must not be '0'"_string); |
111 | | |
112 | 0 | if (number_of_channels > MAX_NUMBER_OF_CHANNELS) |
113 | 0 | return WebIDL::NotSupportedError::create(realm, "Number of channels is greater than allowed range"_string); |
114 | | |
115 | 0 | if (length == 0) |
116 | 0 | return WebIDL::NotSupportedError::create(realm, "Length of buffer must be at least 1"_string); |
117 | | |
118 | 0 | if (sample_rate < MIN_SAMPLE_RATE || sample_rate > MAX_SAMPLE_RATE) |
119 | 0 | return WebIDL::NotSupportedError::create(realm, "Sample rate is outside of allowed range"_string); |
120 | | |
121 | 0 | return {}; |
122 | 0 | } |
123 | | |
124 | | void BaseAudioContext::queue_a_media_element_task(JS::NonnullGCPtr<JS::HeapFunction<void()>> steps) |
125 | 0 | { |
126 | 0 | auto task = HTML::Task::create(vm(), m_media_element_event_task_source.source, HTML::current_settings_object().responsible_document(), steps); |
127 | 0 | HTML::main_thread_event_loop().task_queue().add(task); |
128 | 0 | } |
129 | | |
130 | | // https://webaudio.github.io/web-audio-api/#dom-baseaudiocontext-decodeaudiodata |
131 | | 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) |
132 | 0 | { |
133 | 0 | auto& realm = this->realm(); |
134 | | |
135 | | // FIXME: When decodeAudioData is called, the following steps MUST be performed on the control thread: |
136 | | |
137 | | // 1. If this's relevant global object's associated Document is not fully active then return a |
138 | | // promise rejected with "InvalidStateError" DOMException. |
139 | 0 | auto const& associated_document = verify_cast<HTML::Window>(HTML::relevant_global_object(*this)).associated_document(); |
140 | 0 | if (!associated_document.is_fully_active()) { |
141 | 0 | auto error = WebIDL::InvalidStateError::create(realm, "The document is not fully active."_string); |
142 | 0 | return WebIDL::create_rejected_promise_from_exception(realm, error); |
143 | 0 | } |
144 | | |
145 | | // 2. Let promise be a new Promise. |
146 | 0 | auto promise = WebIDL::create_promise(realm); |
147 | | |
148 | | // FIXME: 3. If audioData is detached, execute the following steps: |
149 | 0 | if (true) { |
150 | | // 3.1. Append promise to [[pending promises]]. |
151 | 0 | m_pending_promises.append(promise); |
152 | | |
153 | | // FIXME: 3.2. Detach the audioData ArrayBuffer. If this operations throws, jump to the step 3. |
154 | | |
155 | | // 3.3. Queue a decoding operation to be performed on another thread. |
156 | 0 | queue_a_decoding_operation(promise, move(audio_data), success_callback, error_callback); |
157 | 0 | } |
158 | | |
159 | | // 4. Else, execute the following error steps: |
160 | 0 | else { |
161 | | // 4.1. Let error be a DataCloneError. |
162 | 0 | auto error = WebIDL::DataCloneError::create(realm, "Audio data is not detached."_string); |
163 | | |
164 | | // 4.2. Reject promise with error, and remove it from [[pending promises]]. |
165 | 0 | WebIDL::reject_promise(realm, promise, error); |
166 | 0 | m_pending_promises.remove_first_matching([&promise](auto& pending_promise) { |
167 | 0 | return pending_promise == promise; |
168 | 0 | }); |
169 | | |
170 | | // 4.3. Queue a media element task to invoke errorCallback with error. |
171 | 0 | if (error_callback) { |
172 | 0 | queue_a_media_element_task(JS::create_heap_function(heap(), [&realm, error_callback, error] { |
173 | 0 | auto completion = WebIDL::invoke_callback(*error_callback, {}, error); |
174 | 0 | if (completion.is_abrupt()) |
175 | 0 | HTML::report_exception(completion, realm); |
176 | 0 | })); |
177 | 0 | } |
178 | 0 | } |
179 | | |
180 | | // 5. Return promise. |
181 | 0 | return verify_cast<JS::Promise>(*promise->promise()); |
182 | 0 | } |
183 | | |
184 | | // https://webaudio.github.io/web-audio-api/#dom-baseaudiocontext-decodeaudiodata |
185 | | 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) |
186 | 0 | { |
187 | 0 | auto& realm = this->realm(); |
188 | | |
189 | | // FIXME: When queuing a decoding operation to be performed on another thread, the following steps |
190 | | // MUST happen on a thread that is not the control thread nor the rendering thread, called |
191 | | // the decoding thread. |
192 | | |
193 | | // 1. Let can decode be a boolean flag, initially set to true. |
194 | 0 | auto can_decode { true }; |
195 | | |
196 | | // FIXME: 2. Attempt to determine the MIME type of audioData, using MIME Sniffing § 6.2 Matching an |
197 | | // audio or video type pattern. If the audio or video type pattern matching algorithm returns |
198 | | // undefined, set can decode to false. |
199 | | |
200 | | // 3. If can decode is true, |
201 | 0 | if (can_decode) { |
202 | | // FIXME: attempt to decode the encoded audioData into linear PCM. In case of |
203 | | // failure, set can decode to false. |
204 | | |
205 | | // FIXME: If the media byte-stream contains multiple audio tracks, only decode the first track to linear pcm. |
206 | 0 | } |
207 | | |
208 | | // 4. If can decode is false, |
209 | 0 | if (!can_decode) { |
210 | | // queue a media element task to execute the following steps: |
211 | 0 | queue_a_media_element_task(JS::create_heap_function(heap(), [this, &realm, promise, error_callback] { |
212 | | // 4.1. Let error be a DOMException whose name is EncodingError. |
213 | 0 | auto error = WebIDL::EncodingError::create(realm, "Unable to decode."_string); |
214 | | |
215 | | // 4.1.2. Reject promise with error, and remove it from [[pending promises]]. |
216 | 0 | WebIDL::reject_promise(realm, promise, error); |
217 | 0 | m_pending_promises.remove_first_matching([&promise](auto& pending_promise) { |
218 | 0 | return pending_promise == promise; |
219 | 0 | }); |
220 | | |
221 | | // 4.2. If errorCallback is not missing, invoke errorCallback with error. |
222 | 0 | if (error_callback) { |
223 | 0 | auto completion = WebIDL::invoke_callback(*error_callback, {}, error); |
224 | 0 | if (completion.is_abrupt()) |
225 | 0 | HTML::report_exception(completion, realm); |
226 | 0 | } |
227 | 0 | })); |
228 | 0 | } |
229 | | |
230 | | // 5. Otherwise: |
231 | 0 | else { |
232 | | // FIXME: 5.1. Take the result, representing the decoded linear PCM audio data, and resample it to the |
233 | | // sample-rate of the BaseAudioContext if it is different from the sample-rate of |
234 | | // audioData. |
235 | | |
236 | | // FIXME: 5.2. queue a media element task to execute the following steps: |
237 | | |
238 | | // FIXME: 5.2.1. Let buffer be an AudioBuffer containing the final result (after possibly performing |
239 | | // sample-rate conversion). |
240 | 0 | auto buffer = MUST(create_buffer(2, 1, 44100)); |
241 | | |
242 | | // 5.2.2. Resolve promise with buffer. |
243 | 0 | WebIDL::resolve_promise(realm, promise, buffer); |
244 | | |
245 | | // 5.2.3. If successCallback is not missing, invoke successCallback with buffer. |
246 | 0 | if (success_callback) { |
247 | 0 | auto completion = WebIDL::invoke_callback(*success_callback, {}, buffer); |
248 | 0 | if (completion.is_abrupt()) |
249 | 0 | HTML::report_exception(completion, realm); |
250 | 0 | } |
251 | 0 | } |
252 | 0 | } |
253 | | |
254 | | } |