/src/serenity/Userland/Libraries/LibWeb/WebAudio/AudioContext.cpp
Line | Count | Source |
1 | | /* |
2 | | * Copyright (c) 2023, Luke Wilde <lukew@serenityos.org> |
3 | | * |
4 | | * SPDX-License-Identifier: BSD-2-Clause |
5 | | */ |
6 | | |
7 | | #include <LibWeb/Bindings/AudioContextPrototype.h> |
8 | | #include <LibWeb/Bindings/Intrinsics.h> |
9 | | #include <LibWeb/DOM/Event.h> |
10 | | #include <LibWeb/HTML/HTMLMediaElement.h> |
11 | | #include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h> |
12 | | #include <LibWeb/HTML/Window.h> |
13 | | #include <LibWeb/WebAudio/AudioContext.h> |
14 | | #include <LibWeb/WebIDL/Promise.h> |
15 | | |
16 | | namespace Web::WebAudio { |
17 | | |
18 | | JS_DEFINE_ALLOCATOR(AudioContext); |
19 | | |
20 | | // https://webaudio.github.io/web-audio-api/#dom-audiocontext-audiocontext |
21 | | WebIDL::ExceptionOr<JS::NonnullGCPtr<AudioContext>> AudioContext::construct_impl(JS::Realm& realm, AudioContextOptions const& context_options) |
22 | 0 | { |
23 | 0 | return realm.heap().allocate<AudioContext>(realm, realm, context_options); |
24 | 0 | } |
25 | | |
26 | | AudioContext::AudioContext(JS::Realm& realm, AudioContextOptions const& context_options) |
27 | 0 | : BaseAudioContext(realm) |
28 | 0 | { |
29 | | // FIXME: If the current settings object’s responsible document is NOT fully active, throw an InvalidStateError and abort these steps. |
30 | | |
31 | | // 1: Set a [[control thread state]] to suspended on the AudioContext. |
32 | 0 | BaseAudioContext::set_control_state(Bindings::AudioContextState::Suspended); |
33 | | |
34 | | // 2: Set a [[rendering thread state]] to suspended on the AudioContext. |
35 | 0 | BaseAudioContext::set_rendering_state(Bindings::AudioContextState::Suspended); |
36 | | |
37 | | // 3: Let [[pending resume promises]] be a slot on this AudioContext, that is an initially empty ordered list of promises. |
38 | | |
39 | | // 4: If contextOptions is given, apply the options: |
40 | | // 4.1: Set the internal latency of this AudioContext according to contextOptions.latencyHint, as described in latencyHint. |
41 | 0 | switch (context_options.latency_hint) { |
42 | 0 | case Bindings::AudioContextLatencyCategory::Balanced: |
43 | | // FIXME: Determine optimal settings for balanced. |
44 | 0 | break; |
45 | 0 | case Bindings::AudioContextLatencyCategory::Interactive: |
46 | | // FIXME: Determine optimal settings for interactive. |
47 | 0 | break; |
48 | 0 | case Bindings::AudioContextLatencyCategory::Playback: |
49 | | // FIXME: Determine optimal settings for playback. |
50 | 0 | break; |
51 | 0 | default: |
52 | 0 | VERIFY_NOT_REACHED(); |
53 | 0 | } |
54 | | |
55 | | // 4.2: If contextOptions.sampleRate is specified, set the sampleRate of this AudioContext to this value. Otherwise, |
56 | | // use the sample rate of the default output device. If the selected sample rate differs from the sample rate of the output device, |
57 | | // this AudioContext MUST resample the audio output to match the sample rate of the output device. |
58 | 0 | if (context_options.sample_rate.has_value()) { |
59 | 0 | BaseAudioContext::set_sample_rate(context_options.sample_rate.value()); |
60 | 0 | } else { |
61 | | // FIXME: This would ideally be coming from the default output device, but we can only get this on Serenity |
62 | | // For now we'll just have to resample |
63 | 0 | BaseAudioContext::set_sample_rate(44100); |
64 | 0 | } |
65 | | |
66 | | // FIXME: 5: If the context is allowed to start, send a control message to start processing. |
67 | | // FIXME: Implement control message queue to run following steps on the rendering thread |
68 | 0 | if (m_allowed_to_start) { |
69 | | // FIXME: 5.1: Attempt to acquire system resources. In case of failure, abort the following steps. |
70 | | |
71 | | // 5.2: Set the [[rendering thread state]] to "running" on the AudioContext. |
72 | 0 | BaseAudioContext::set_rendering_state(Bindings::AudioContextState::Running); |
73 | | |
74 | | // 5.3: queue a media element task to execute the following steps: |
75 | 0 | queue_a_media_element_task(JS::create_heap_function(heap(), [&realm, this]() { |
76 | | // 5.3.1: Set the state attribute of the AudioContext to "running". |
77 | 0 | BaseAudioContext::set_control_state(Bindings::AudioContextState::Running); |
78 | | |
79 | | // 5.3.2: queue a media element task to fire an event named statechange at the AudioContext. |
80 | 0 | this->dispatch_event(DOM::Event::create(realm, HTML::EventNames::statechange)); |
81 | 0 | })); |
82 | 0 | } |
83 | 0 | } |
84 | | |
85 | 0 | AudioContext::~AudioContext() = default; |
86 | | |
87 | | void AudioContext::initialize(JS::Realm& realm) |
88 | 0 | { |
89 | 0 | Base::initialize(realm); |
90 | 0 | WEB_SET_PROTOTYPE_FOR_INTERFACE(AudioContext); |
91 | 0 | } |
92 | | |
93 | | void AudioContext::visit_edges(Cell::Visitor& visitor) |
94 | 0 | { |
95 | 0 | Base::visit_edges(visitor); |
96 | 0 | visitor.visit(m_pending_resume_promises); |
97 | 0 | } |
98 | | |
99 | | // https://www.w3.org/TR/webaudio/#dom-audiocontext-getoutputtimestamp |
100 | | AudioTimestamp AudioContext::get_output_timestamp() |
101 | 0 | { |
102 | 0 | dbgln("(STUBBED) getOutputTimestamp()"); |
103 | 0 | return {}; |
104 | 0 | } |
105 | | |
106 | | // https://www.w3.org/TR/webaudio/#dom-audiocontext-resume |
107 | | WebIDL::ExceptionOr<JS::NonnullGCPtr<JS::Promise>> AudioContext::resume() |
108 | 0 | { |
109 | 0 | auto& realm = this->realm(); |
110 | | |
111 | | // 1. If this's relevant global object's associated Document is not fully active then return a promise rejected with "InvalidStateError" DOMException. |
112 | 0 | auto const& associated_document = verify_cast<HTML::Window>(HTML::relevant_global_object(*this)).associated_document(); |
113 | 0 | if (!associated_document.is_fully_active()) |
114 | 0 | return WebIDL::InvalidStateError::create(realm, "Document is not fully active"_string); |
115 | | |
116 | | // 2. Let promise be a new Promise. |
117 | 0 | auto promise = WebIDL::create_promise(realm); |
118 | | |
119 | | // 3. If the [[control thread state]] on the AudioContext is closed reject the promise with InvalidStateError, abort these steps, returning promise. |
120 | 0 | if (state() == Bindings::AudioContextState::Closed) { |
121 | 0 | WebIDL::reject_promise(realm, promise, WebIDL::InvalidStateError::create(realm, "Audio context is already closed."_string)); |
122 | 0 | return JS::NonnullGCPtr { verify_cast<JS::Promise>(*promise->promise()) }; |
123 | 0 | } |
124 | | |
125 | | // 4. Set [[suspended by user]] to true. |
126 | 0 | m_suspended_by_user = true; |
127 | | |
128 | | // 5. If the context is not allowed to start, append promise to [[pending promises]] and [[pending resume promises]] and abort these steps, returning promise. |
129 | 0 | if (m_allowed_to_start) { |
130 | 0 | m_pending_promises.append(promise); |
131 | 0 | m_pending_resume_promises.append(promise); |
132 | 0 | } |
133 | | |
134 | | // 6. Set the [[control thread state]] on the AudioContext to running. |
135 | 0 | set_control_state(Bindings::AudioContextState::Running); |
136 | | |
137 | | // 7. Queue a control message to resume the AudioContext. |
138 | | // FIXME: Implement control message queue to run following steps on the rendering thread |
139 | | |
140 | | // FIXME: 7.1: Attempt to acquire system resources. |
141 | | |
142 | | // 7.2: Set the [[rendering thread state]] on the AudioContext to running. |
143 | 0 | set_rendering_state(Bindings::AudioContextState::Running); |
144 | | |
145 | | // 7.3: Start rendering the audio graph. |
146 | 0 | if (!start_rendering_audio_graph()) { |
147 | | // 7.4: In case of failure, queue a media element task to execute the following steps: |
148 | 0 | queue_a_media_element_task(JS::create_heap_function(heap(), [&realm, this]() { |
149 | 0 | HTML::TemporaryExecutionContext context(Bindings::host_defined_environment_settings_object(realm), HTML::TemporaryExecutionContext::CallbacksEnabled::Yes); |
150 | | |
151 | | // 7.4.1: Reject all promises from [[pending resume promises]] in order, then clear [[pending resume promises]]. |
152 | 0 | for (auto const& promise : m_pending_resume_promises) { |
153 | 0 | WebIDL::reject_promise(realm, promise, JS::js_null()); |
154 | | |
155 | | // 7.4.2: Additionally, remove those promises from [[pending promises]]. |
156 | 0 | m_pending_promises.remove_first_matching([&promise](auto& pending_promise) { |
157 | 0 | return pending_promise == promise; |
158 | 0 | }); |
159 | 0 | } |
160 | 0 | m_pending_resume_promises.clear(); |
161 | 0 | })); |
162 | 0 | } |
163 | | |
164 | | // 7.5: queue a media element task to execute the following steps: |
165 | 0 | queue_a_media_element_task(JS::create_heap_function(heap(), [&realm, promise, this]() { |
166 | 0 | HTML::TemporaryExecutionContext context(Bindings::host_defined_environment_settings_object(realm), HTML::TemporaryExecutionContext::CallbacksEnabled::Yes); |
167 | | |
168 | | // 7.5.1: Resolve all promises from [[pending resume promises]] in order. |
169 | | // 7.5.2: Clear [[pending resume promises]]. Additionally, remove those promises from |
170 | | // [[pending promises]]. |
171 | 0 | for (auto const& pending_resume_promise : m_pending_resume_promises) { |
172 | 0 | *pending_resume_promise->resolve(); |
173 | 0 | m_pending_promises.remove_first_matching([&pending_resume_promise](auto& pending_promise) { |
174 | 0 | return pending_promise == pending_resume_promise; |
175 | 0 | }); |
176 | 0 | } |
177 | 0 | m_pending_resume_promises.clear(); |
178 | | |
179 | | // 7.5.3: Resolve promise. |
180 | 0 | *promise->resolve(); |
181 | | |
182 | | // 7.5.4: If the state attribute of the AudioContext is not already "running": |
183 | 0 | if (state() != Bindings::AudioContextState::Running) { |
184 | | // 7.5.4.1: Set the state attribute of the AudioContext to "running". |
185 | 0 | set_control_state(Bindings::AudioContextState::Running); |
186 | | |
187 | | // 7.5.4.2: queue a media element task to fire an event named statechange at the AudioContext. |
188 | 0 | queue_a_media_element_task(JS::create_heap_function(heap(), [&realm, this]() { |
189 | 0 | this->dispatch_event(DOM::Event::create(realm, HTML::EventNames::statechange)); |
190 | 0 | })); |
191 | 0 | } |
192 | 0 | })); |
193 | | |
194 | | // 8. Return promise. |
195 | 0 | return JS::NonnullGCPtr { verify_cast<JS::Promise>(*promise->promise()) }; |
196 | 0 | } |
197 | | |
198 | | // https://www.w3.org/TR/webaudio/#dom-audiocontext-suspend |
199 | | WebIDL::ExceptionOr<JS::NonnullGCPtr<JS::Promise>> AudioContext::suspend() |
200 | 0 | { |
201 | 0 | auto& realm = this->realm(); |
202 | | |
203 | | // 1. If this's relevant global object's associated Document is not fully active then return a promise rejected with "InvalidStateError" DOMException. |
204 | 0 | auto const& associated_document = verify_cast<HTML::Window>(HTML::relevant_global_object(*this)).associated_document(); |
205 | 0 | if (!associated_document.is_fully_active()) |
206 | 0 | return WebIDL::InvalidStateError::create(realm, "Document is not fully active"_string); |
207 | | |
208 | | // 2. Let promise be a new Promise. |
209 | 0 | auto promise = WebIDL::create_promise(realm); |
210 | | |
211 | | // 3. If the [[control thread state]] on the AudioContext is closed reject the promise with InvalidStateError, abort these steps, returning promise. |
212 | 0 | if (state() == Bindings::AudioContextState::Closed) { |
213 | 0 | WebIDL::reject_promise(realm, promise, WebIDL::InvalidStateError::create(realm, "Audio context is already closed."_string)); |
214 | 0 | return JS::NonnullGCPtr { verify_cast<JS::Promise>(*promise->promise()) }; |
215 | 0 | } |
216 | | |
217 | | // 4. Append promise to [[pending promises]]. |
218 | 0 | m_pending_promises.append(promise); |
219 | | |
220 | | // 5. Set [[suspended by user]] to true. |
221 | 0 | m_suspended_by_user = true; |
222 | | |
223 | | // 6. Set the [[control thread state]] on the AudioContext to suspended. |
224 | 0 | set_control_state(Bindings::AudioContextState::Suspended); |
225 | | |
226 | | // 7. Queue a control message to suspend the AudioContext. |
227 | | // FIXME: Implement control message queue to run following steps on the rendering thread |
228 | | |
229 | | // FIXME: 7.1: Attempt to release system resources. |
230 | | |
231 | | // 7.2: Set the [[rendering thread state]] on the AudioContext to suspended. |
232 | 0 | set_rendering_state(Bindings::AudioContextState::Suspended); |
233 | | |
234 | | // 7.3: queue a media element task to execute the following steps: |
235 | 0 | queue_a_media_element_task(JS::create_heap_function(heap(), [&realm, promise, this]() { |
236 | 0 | HTML::TemporaryExecutionContext context(Bindings::host_defined_environment_settings_object(realm), HTML::TemporaryExecutionContext::CallbacksEnabled::Yes); |
237 | | |
238 | | // 7.3.1: Resolve promise. |
239 | 0 | *promise->resolve(); |
240 | | |
241 | | // 7.3.2: If the state attribute of the AudioContext is not already "suspended": |
242 | 0 | if (state() != Bindings::AudioContextState::Suspended) { |
243 | | // 7.3.2.1: Set the state attribute of the AudioContext to "suspended". |
244 | 0 | set_control_state(Bindings::AudioContextState::Suspended); |
245 | | |
246 | | // 7.3.2.2: queue a media element task to fire an event named statechange at the AudioContext. |
247 | 0 | queue_a_media_element_task(JS::create_heap_function(heap(), [&realm, this]() { |
248 | 0 | this->dispatch_event(DOM::Event::create(realm, HTML::EventNames::statechange)); |
249 | 0 | })); |
250 | 0 | } |
251 | 0 | })); |
252 | | |
253 | | // 8. Return promise. |
254 | 0 | return JS::NonnullGCPtr { verify_cast<JS::Promise>(*promise->promise()) }; |
255 | 0 | } |
256 | | |
257 | | // https://www.w3.org/TR/webaudio/#dom-audiocontext-close |
258 | | WebIDL::ExceptionOr<JS::NonnullGCPtr<JS::Promise>> AudioContext::close() |
259 | 0 | { |
260 | 0 | auto& realm = this->realm(); |
261 | | |
262 | | // 1. If this's relevant global object's associated Document is not fully active then return a promise rejected with "InvalidStateError" DOMException. |
263 | 0 | auto const& associated_document = verify_cast<HTML::Window>(HTML::relevant_global_object(*this)).associated_document(); |
264 | 0 | if (!associated_document.is_fully_active()) |
265 | 0 | return WebIDL::InvalidStateError::create(realm, "Document is not fully active"_string); |
266 | | |
267 | | // 2. Let promise be a new Promise. |
268 | 0 | auto promise = WebIDL::create_promise(realm); |
269 | | |
270 | | // 3. If the [[control thread state]] flag on the AudioContext is closed reject the promise with InvalidStateError, abort these steps, returning promise. |
271 | 0 | if (state() == Bindings::AudioContextState::Closed) { |
272 | 0 | WebIDL::reject_promise(realm, promise, WebIDL::InvalidStateError::create(realm, "Audio context is already closed."_string)); |
273 | 0 | return JS::NonnullGCPtr { verify_cast<JS::Promise>(*promise->promise()) }; |
274 | 0 | } |
275 | | |
276 | | // 4. Set the [[control thread state]] flag on the AudioContext to closed. |
277 | 0 | set_control_state(Bindings::AudioContextState::Closed); |
278 | | |
279 | | // 5. Queue a control message to close the AudioContext. |
280 | | // FIXME: Implement control message queue to run following steps on the rendering thread |
281 | | |
282 | | // FIXME: 5.1: Attempt to release system resources. |
283 | | |
284 | | // 5.2: Set the [[rendering thread state]] to "suspended". |
285 | 0 | set_rendering_state(Bindings::AudioContextState::Suspended); |
286 | | |
287 | | // FIXME: 5.3: If this control message is being run in a reaction to the document being unloaded, abort this algorithm. |
288 | | |
289 | | // 5.4: queue a media element task to execute the following steps: |
290 | 0 | queue_a_media_element_task(JS::create_heap_function(heap(), [&realm, promise, this]() { |
291 | 0 | HTML::TemporaryExecutionContext context(Bindings::host_defined_environment_settings_object(realm), HTML::TemporaryExecutionContext::CallbacksEnabled::Yes); |
292 | | |
293 | | // 5.4.1: Resolve promise. |
294 | 0 | *promise->resolve(); |
295 | | |
296 | | // 5.4.2: If the state attribute of the AudioContext is not already "closed": |
297 | 0 | if (state() != Bindings::AudioContextState::Closed) { |
298 | | // 5.4.2.1: Set the state attribute of the AudioContext to "closed". |
299 | 0 | set_control_state(Bindings::AudioContextState::Closed); |
300 | 0 | } |
301 | | |
302 | | // 5.4.2.2: queue a media element task to fire an event named statechange at the AudioContext. |
303 | | // FIXME: Attempting to queue another task in here causes an assertion fail at Vector.h:148 |
304 | 0 | this->dispatch_event(DOM::Event::create(realm, HTML::EventNames::statechange)); |
305 | 0 | })); |
306 | | |
307 | | // 6. Return promise |
308 | 0 | return JS::NonnullGCPtr { verify_cast<JS::Promise>(*promise->promise()) }; |
309 | 0 | } |
310 | | |
311 | | // FIXME: Actually implement the rendering thread |
312 | | bool AudioContext::start_rendering_audio_graph() |
313 | 0 | { |
314 | 0 | bool render_result = true; |
315 | 0 | return render_result; |
316 | 0 | } |
317 | | |
318 | | } |