Kaynağa Gözat

LibWeb: Begin implementing the interface for AudioBuffer

Implement the constructor and getChannelData function, working towards
the functionality that we need in order to implement
OfflineAudioContext.
Shannon Booth 1 yıl önce
ebeveyn
işleme
0c8a98ac94

+ 18 - 0
Tests/LibWeb/Text/expected/WebAudio/AudioBuffer.txt

@@ -0,0 +1,18 @@
+Error creating AudioBuffer: 'NotSupportedError: Number of channels must not be '0''
+Error creating AudioBuffer: 'NotSupportedError: Number of channels is greater than allowed range'
+Error creating AudioBuffer: 'NotSupportedError: Length of buffer must be at least 1'
+Error creating AudioBuffer: 'NotSupportedError: Sample rate is outside of allowed range'
+Error creating AudioBuffer: 'NotSupportedError: Sample rate is outside of allowed range'
+3
+17
+10002
+Got Float32Array, length = 17
+17
+Data equals itself: true
+Got Float32Array, length = 17
+17
+Data equals itself: true
+Got Float32Array, length = 17
+17
+Data equals itself: true
+Error getting channel data: 'IndexSizeError: Channel index is out of range'

+ 48 - 0
Tests/LibWeb/Text/input/WebAudio/AudioBuffer.html

@@ -0,0 +1,48 @@
+<script src="../include.js"></script>
+<script>
+    test(() => {
+        // Invalid constructors
+        const invalidOptions = [
+            { numberOfChannels: 0, length: 17, sampleRate: 10_002 }, // 0 channels (below min)
+            { numberOfChannels: 33, length: 17, sampleRate: 10_002 }, // 33 channels (above max)
+            { numberOfChannels: 3, length: 0, sampleRate: 10_002 }, // 0 length
+            { numberOfChannels: 3, length: 17, sampleRate: 7999 }, // 7999 sample rate (below min)
+            { numberOfChannels: 3, length: 17, sampleRate: 192001 }, // 192001 sample rate (above max)
+        ];
+
+        for (let invalidOption of invalidOptions) {
+            try {
+                const buffer = new AudioBuffer(invalidOption);
+                println(`FAIL: created buffer ${buffer}`);
+            } catch (e) {
+                println(`Error creating AudioBuffer: '${e}'`);
+            }
+        }
+
+        // Valid Constructor
+        const options = { numberOfChannels: 3, length: 17, sampleRate: 10_002 };
+        const buffer = new AudioBuffer(options);
+
+        println(buffer.numberOfChannels);
+        println(buffer.length);
+        println(buffer.sampleRate);
+
+        // Check each of the channels
+        for (let k = 0; k < options.numberOfChannels; ++k) {
+            const data = buffer.getChannelData(k);
+            println(`Got ${data.constructor.name}, length = ${data.length}`);
+            println(data.length);
+
+            const dataAgain = buffer.getChannelData(k);
+            println(`Data equals itself: ${data === dataAgain}`);
+        }
+
+        // Out of range channel
+        try {
+            buffer.getChannelData(options.numberOfChannels);
+            println("FAIL: No exception thrown");
+        } catch (e) {
+            println(`Error getting channel data: '${e}'`);
+        }
+    });
+</script>

+ 1 - 0
Userland/Libraries/LibWeb/CMakeLists.txt

@@ -653,6 +653,7 @@ set(SOURCES
     WebAssembly/Module.cpp
     WebAssembly/Table.cpp
     WebAssembly/WebAssembly.cpp
+    WebAudio/AudioBuffer.cpp
     WebAudio/AudioContext.cpp
     WebAudio/BaseAudioContext.cpp
     WebDriver/Capabilities.cpp

+ 115 - 0
Userland/Libraries/LibWeb/WebAudio/AudioBuffer.cpp

@@ -0,0 +1,115 @@
+/*
+ * Copyright (c) 2024, Shannon Booth <shannon@serenityos.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <LibJS/Runtime/Completion.h>
+#include <LibJS/Runtime/Realm.h>
+#include <LibJS/Runtime/TypedArray.h>
+#include <LibJS/Runtime/VM.h>
+#include <LibWeb/Bindings/Intrinsics.h>
+#include <LibWeb/WebAudio/AudioBuffer.h>
+#include <LibWeb/WebAudio/BaseAudioContext.h>
+#include <LibWeb/WebIDL/DOMException.h>
+
+namespace Web::WebAudio {
+
+JS_DEFINE_ALLOCATOR(AudioBuffer);
+
+WebIDL::ExceptionOr<JS::NonnullGCPtr<AudioBuffer>> AudioBuffer::construct_impl(JS::Realm& realm, AudioBufferOptions const& options)
+{
+    auto& vm = realm.vm();
+
+    // 1. If any of the values in options lie outside its nominal range, throw a NotSupportedError exception and abort the following steps.
+    TRY(BaseAudioContext::verify_audio_options_inside_nominal_range(realm, options.number_of_channels, options.length, options.sample_rate));
+
+    // 2. Let b be a new AudioBuffer object.
+    // 3. Respectively assign the values of the attributes numberOfChannels, length, sampleRate of the AudioBufferOptions passed in the
+    //    constructor to the internal slots [[number of channels]], [[length]], [[sample rate]].
+    auto buffer = vm.heap().allocate<AudioBuffer>(realm, realm, options);
+
+    // 4. Set the internal slot [[internal data]] of this AudioBuffer to the result of calling CreateByteDataBlock([[length]] * [[number of channels]]).
+    buffer->m_channels.ensure_capacity(options.number_of_channels);
+    for (WebIDL::UnsignedLong i = 0; i < options.number_of_channels; ++i)
+        buffer->m_channels.unchecked_append(TRY(JS::Float32Array::create(realm, options.length)));
+
+    return buffer;
+}
+
+AudioBuffer::~AudioBuffer() = default;
+
+// https://webaudio.github.io/web-audio-api/#dom-audiobuffer-samplerate
+float AudioBuffer::sample_rate() const
+{
+    // The sample-rate for the PCM audio data in samples per second. This MUST return the value of [[sample rate]].
+    return m_sample_rate;
+}
+
+// https://webaudio.github.io/web-audio-api/#dom-audiobuffer-length
+WebIDL::UnsignedLong AudioBuffer::length() const
+{
+    // Length of the PCM audio data in sample-frames. This MUST return the value of [[length]].
+    return m_length;
+}
+
+// https://webaudio.github.io/web-audio-api/#dom-audiobuffer-duration
+double AudioBuffer::duration() const
+{
+    // Duration of the PCM audio data in seconds.
+    // This is computed from the [[sample rate]] and the [[length]] of the AudioBuffer by performing a division between the [[length]] and the [[sample rate]].
+    return m_length / static_cast<double>(m_sample_rate);
+}
+
+// https://webaudio.github.io/web-audio-api/#dom-audiobuffer-numberofchannels
+WebIDL::UnsignedLong AudioBuffer::number_of_channels() const
+{
+    // The number of discrete audio channels. This MUST return the value of [[number of channels]].
+    return m_channels.size();
+}
+
+// https://webaudio.github.io/web-audio-api/#dom-audiobuffer-getchanneldata
+WebIDL::ExceptionOr<JS::NonnullGCPtr<JS::Float32Array>> AudioBuffer::get_channel_data(WebIDL::UnsignedLong channel) const
+{
+    if (channel >= m_channels.size())
+        return WebIDL::IndexSizeError::create(realm(), "Channel index is out of range"_fly_string);
+
+    return m_channels[channel];
+}
+
+// https://webaudio.github.io/web-audio-api/#dom-audiobuffer-copyfromchannel
+WebIDL::ExceptionOr<void> AudioBuffer::copy_from_channel(JS::Handle<WebIDL::BufferSource> const&, WebIDL::UnsignedLong channel_number, WebIDL::UnsignedLong buffer_offset) const
+{
+    (void)channel_number;
+    (void)buffer_offset;
+    return WebIDL::NotSupportedError::create(realm(), "FIXME: Implement AudioBuffer:copy_from_channel:"_fly_string);
+}
+
+// https://webaudio.github.io/web-audio-api/#dom-audiobuffer-copytochannel
+WebIDL::ExceptionOr<void> AudioBuffer::copy_to_channel(JS::Handle<WebIDL::BufferSource>&, WebIDL::UnsignedLong channel_number, WebIDL::UnsignedLong buffer_offset) const
+{
+    (void)channel_number;
+    (void)buffer_offset;
+    return WebIDL::NotSupportedError::create(realm(), "FIXME: Implement AudioBuffer:copy_to_channel:"_fly_string);
+}
+
+AudioBuffer::AudioBuffer(JS::Realm& realm, AudioBufferOptions const& options)
+    : Bindings::PlatformObject(realm)
+    , m_length(options.length)
+    , m_sample_rate(options.sample_rate)
+{
+}
+
+void AudioBuffer::initialize(JS::Realm& realm)
+{
+    Base::initialize(realm);
+    WEB_SET_PROTOTYPE_FOR_INTERFACE(AudioBuffer);
+}
+
+void AudioBuffer::visit_edges(Cell::Visitor& visitor)
+{
+    Base::visit_edges(visitor);
+    visitor.visit(m_channels);
+}
+
+}

+ 65 - 0
Userland/Libraries/LibWeb/WebAudio/AudioBuffer.h

@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 2024, Shannon Booth <shannon@serenityos.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#include <AK/Vector.h>
+#include <LibJS/Forward.h>
+#include <LibJS/Runtime/ArrayBuffer.h>
+#include <LibWeb/Bindings/PlatformObject.h>
+#include <LibWeb/WebIDL/Buffers.h>
+#include <LibWeb/WebIDL/ExceptionOr.h>
+#include <LibWeb/WebIDL/Types.h>
+
+namespace Web::WebAudio {
+
+struct AudioBufferOptions {
+    WebIDL::UnsignedLong number_of_channels { 1 };
+    WebIDL::UnsignedLong length {};
+    float sample_rate {};
+};
+
+// https://webaudio.github.io/web-audio-api/#AudioContext
+class AudioBuffer final : public Bindings::PlatformObject {
+    WEB_PLATFORM_OBJECT(AudioBuffer, Bindings::PlatformObject);
+    JS_DECLARE_ALLOCATOR(AudioBuffer);
+
+public:
+    static WebIDL::ExceptionOr<JS::NonnullGCPtr<AudioBuffer>> construct_impl(JS::Realm&, AudioBufferOptions const&);
+
+    virtual ~AudioBuffer() override;
+
+    float sample_rate() const;
+    WebIDL::UnsignedLong length() const;
+    double duration() const;
+    WebIDL::UnsignedLong number_of_channels() const;
+    WebIDL::ExceptionOr<JS::NonnullGCPtr<JS::Float32Array>> get_channel_data(WebIDL::UnsignedLong channel) const;
+    WebIDL::ExceptionOr<void> copy_from_channel(JS::Handle<WebIDL::BufferSource> const&, WebIDL::UnsignedLong channel_number, WebIDL::UnsignedLong buffer_offset = 0) const;
+    WebIDL::ExceptionOr<void> copy_to_channel(JS::Handle<WebIDL::BufferSource>&, WebIDL::UnsignedLong channel_number, WebIDL::UnsignedLong buffer_offset = 0) const;
+
+private:
+    explicit AudioBuffer(JS::Realm&, AudioBufferOptions const&);
+
+    virtual void initialize(JS::Realm&) override;
+    virtual void visit_edges(Cell::Visitor&) override;
+
+    // https://webaudio.github.io/web-audio-api/#dom-audiobuffer-number-of-channels-slot
+    // The number of audio channels for this AudioBuffer, which is an unsigned long.
+    //
+    // https://webaudio.github.io/web-audio-api/#dom-audiobuffer-internal-data-slot
+    // A data block holding the audio sample data.
+    Vector<JS::NonnullGCPtr<JS::Float32Array>> m_channels; // [[internal data]] / [[number_of_channels]]
+
+    // https://webaudio.github.io/web-audio-api/#dom-audiobuffer-length-slot
+    // The length of each channel of this AudioBuffer, which is an unsigned long.
+    WebIDL::UnsignedLong m_length {}; // [[length]]
+
+    // https://webaudio.github.io/web-audio-api/#dom-audiobuffer-sample-rate-slot
+    // The sample-rate, in Hz, of this AudioBuffer, a float.
+    float m_sample_rate {}; // [[sample rate]]
+};
+
+}

+ 23 - 0
Userland/Libraries/LibWeb/WebAudio/AudioBuffer.idl

@@ -0,0 +1,23 @@
+// https://webaudio.github.io/web-audio-api/#AudioBufferOptions
+dictionary AudioBufferOptions {
+    unsigned long numberOfChannels = 1;
+    required unsigned long length;
+    required float sampleRate;
+};
+
+// https://webaudio.github.io/web-audio-api/#AudioBuffer
+[Exposed=Window]
+interface AudioBuffer {
+    constructor (AudioBufferOptions options);
+    readonly attribute float sampleRate;
+    readonly attribute unsigned long length;
+    readonly attribute double duration;
+    readonly attribute unsigned long numberOfChannels;
+    Float32Array getChannelData(unsigned long channel);
+    undefined copyFromChannel(Float32Array destination,
+                              unsigned long channelNumber,
+                              optional unsigned long bufferOffset = 0);
+    undefined copyToChannel(Float32Array source,
+                            unsigned long channelNumber,
+                            optional unsigned long bufferOffset = 0);
+};

+ 1 - 0
Userland/Libraries/LibWeb/idl_files.cmake

@@ -293,6 +293,7 @@ libweb_js_bindings(WebAssembly/Memory)
 libweb_js_bindings(WebAssembly/Module)
 libweb_js_bindings(WebAssembly/Table)
 libweb_js_bindings(WebAssembly/WebAssembly NAMESPACE)
+libweb_js_bindings(WebAudio/AudioBuffer)
 libweb_js_bindings(WebAudio/AudioContext)
 libweb_js_bindings(WebAudio/BaseAudioContext)
 libweb_js_bindings(WebGL/WebGLContextEvent)