Title: [270157] trunk
Revision
270157
Author
[email protected]
Date
2020-11-21 20:51:13 -0800 (Sat, 21 Nov 2020)

Log Message

Poor resampling quality when using AudioContext sampleRate parameter
https://bugs.webkit.org/show_bug.cgi?id=219201

Reviewed by Geoff Garen.

Source/WebCore:

MultiChannelResampler uses a SincResampler per audio channel. In MultiChannelResampler::process(),
it was calling SincResampler::process() for each channel, which would potentially end up calling
MultiChannelResampler::ChannelProvider::provideInput() to provide channel data used for resampling.
The issue was that MultiChannelResampler::ChannelProvider::provideInput() is implemented in such
a way that things will break if provideInput() gets called more than once per channel. When using
an AudioContext's sample rate larger than the hardware sample rate, provideInput() was getting
called more than once per channel and this resulted in very poor resampling quality.

To address the issue, MultiChannelResampler::process() now processes the data in chunks that
are small enough to guarantee that MultiChannelResampler::ChannelProvider::provideInput() will
never get called more than once per audio channel.

The fix is based on the corresponding MultiChannelResampler / SincResampler implementation in
Chrome:
- https://github.com/chromium/chromium/blob/master/media/base/multi_channel_resampler.cc
- https://github.com/chromium/chromium/blob/master/media/base/sinc_resampler.cc

Tests: webaudio/audiocontext-large-samplerate.html
       webaudio/audiocontext-low-samplerate.html

* platform/audio/MultiChannelResampler.cpp:
(WebCore::MultiChannelResampler::ChannelProvider::setProvider):
(WebCore::MultiChannelResampler::ChannelProvider::setCurrentChannel):
(WebCore::MultiChannelResampler::process):
* platform/audio/MultiChannelResampler.h:
* platform/audio/SincResampler.cpp:
(WebCore::calculateChunkSize):
(WebCore::SincResampler::updateRegions):
* platform/audio/SincResampler.h:

LayoutTests:

Add layout test coverage that would hit assertions in debug.

* webaudio/audiocontext-large-samplerate-expected.txt: Added.
* webaudio/audiocontext-large-samplerate.html: Added.
* webaudio/audiocontext-low-samplerate-expected.txt: Added.
* webaudio/audiocontext-low-samplerate.html: Added.

Modified Paths

Added Paths

Diff

Modified: trunk/LayoutTests/ChangeLog (270156 => 270157)


--- trunk/LayoutTests/ChangeLog	2020-11-22 03:54:00 UTC (rev 270156)
+++ trunk/LayoutTests/ChangeLog	2020-11-22 04:51:13 UTC (rev 270157)
@@ -1,5 +1,19 @@
 2020-11-21  Chris Dumez  <[email protected]>
 
+        Poor resampling quality when using AudioContext sampleRate parameter
+        https://bugs.webkit.org/show_bug.cgi?id=219201
+
+        Reviewed by Geoff Garen.
+
+        Add layout test coverage that would hit assertions in debug.
+
+        * webaudio/audiocontext-large-samplerate-expected.txt: Added.
+        * webaudio/audiocontext-large-samplerate.html: Added.
+        * webaudio/audiocontext-low-samplerate-expected.txt: Added.
+        * webaudio/audiocontext-low-samplerate.html: Added.
+
+2020-11-21  Chris Dumez  <[email protected]>
+
         Unreviewed, reverting r270141.
 
         Caused assertions on bots

Added: trunk/LayoutTests/webaudio/audiocontext-large-samplerate-expected.txt (0 => 270157)


--- trunk/LayoutTests/webaudio/audiocontext-large-samplerate-expected.txt	                        (rev 0)
+++ trunk/LayoutTests/webaudio/audiocontext-large-samplerate-expected.txt	2020-11-22 04:51:13 UTC (rev 270157)
@@ -0,0 +1,11 @@
+Tests that we do not crash when using a very large sample rate
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+PASS context.sampleRate is 384000
+PASS context.state is "running"
+PASS successfullyParsed is true
+
+TEST COMPLETE
+

Added: trunk/LayoutTests/webaudio/audiocontext-large-samplerate.html (0 => 270157)


--- trunk/LayoutTests/webaudio/audiocontext-large-samplerate.html	                        (rev 0)
+++ trunk/LayoutTests/webaudio/audiocontext-large-samplerate.html	2020-11-22 04:51:13 UTC (rev 270157)
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html>
+<script src=""
+<body>
+<script>
+description("Tests that we do not crash when using a very large sample rate");
+jsTestIsAsync = true;
+
+context = new AudioContext({ sampleRate: 384000 });
+shouldBe("context.sampleRate", "384000");
+
+node = new ConstantSourceNode(context, { offset: 0.5 });
+node.connect(context.destination);
+node.start();
+
+setTimeout(() => {
+    shouldBeEqualToString("context.state", "running");
+    finishJSTest();
+}, 100);
+</script>
+</body>
+</html>

Added: trunk/LayoutTests/webaudio/audiocontext-low-samplerate-expected.txt (0 => 270157)


--- trunk/LayoutTests/webaudio/audiocontext-low-samplerate-expected.txt	                        (rev 0)
+++ trunk/LayoutTests/webaudio/audiocontext-low-samplerate-expected.txt	2020-11-22 04:51:13 UTC (rev 270157)
@@ -0,0 +1,11 @@
+Tests that we do not crash when using a very low sample rate
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+PASS context.sampleRate is 3000
+PASS context.state is "running"
+PASS successfullyParsed is true
+
+TEST COMPLETE
+

Added: trunk/LayoutTests/webaudio/audiocontext-low-samplerate.html (0 => 270157)


--- trunk/LayoutTests/webaudio/audiocontext-low-samplerate.html	                        (rev 0)
+++ trunk/LayoutTests/webaudio/audiocontext-low-samplerate.html	2020-11-22 04:51:13 UTC (rev 270157)
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html>
+<script src=""
+<body>
+<script>
+description("Tests that we do not crash when using a very low sample rate");
+jsTestIsAsync = true;
+
+context = new AudioContext({ sampleRate: 3000 });
+shouldBe("context.sampleRate", "3000");
+
+node = new ConstantSourceNode(context, { offset: 0.5 });
+node.connect(context.destination);
+node.start();
+
+setTimeout(() => {
+    shouldBeEqualToString("context.state", "running");
+    finishJSTest();
+}, 100);
+</script>
+</body>
+</html>

Modified: trunk/Source/WebCore/ChangeLog (270156 => 270157)


--- trunk/Source/WebCore/ChangeLog	2020-11-22 03:54:00 UTC (rev 270156)
+++ trunk/Source/WebCore/ChangeLog	2020-11-22 04:51:13 UTC (rev 270157)
@@ -1,3 +1,40 @@
+2020-11-21  Chris Dumez  <[email protected]>
+
+        Poor resampling quality when using AudioContext sampleRate parameter
+        https://bugs.webkit.org/show_bug.cgi?id=219201
+
+        Reviewed by Geoff Garen.
+
+        MultiChannelResampler uses a SincResampler per audio channel. In MultiChannelResampler::process(),
+        it was calling SincResampler::process() for each channel, which would potentially end up calling
+        MultiChannelResampler::ChannelProvider::provideInput() to provide channel data used for resampling.
+        The issue was that MultiChannelResampler::ChannelProvider::provideInput() is implemented in such
+        a way that things will break if provideInput() gets called more than once per channel. When using
+        an AudioContext's sample rate larger than the hardware sample rate, provideInput() was getting
+        called more than once per channel and this resulted in very poor resampling quality.
+
+        To address the issue, MultiChannelResampler::process() now processes the data in chunks that
+        are small enough to guarantee that MultiChannelResampler::ChannelProvider::provideInput() will
+        never get called more than once per audio channel.
+
+        The fix is based on the corresponding MultiChannelResampler / SincResampler implementation in
+        Chrome:
+        - https://github.com/chromium/chromium/blob/master/media/base/multi_channel_resampler.cc
+        - https://github.com/chromium/chromium/blob/master/media/base/sinc_resampler.cc
+
+        Tests: webaudio/audiocontext-large-samplerate.html
+               webaudio/audiocontext-low-samplerate.html
+
+        * platform/audio/MultiChannelResampler.cpp:
+        (WebCore::MultiChannelResampler::ChannelProvider::setProvider):
+        (WebCore::MultiChannelResampler::ChannelProvider::setCurrentChannel):
+        (WebCore::MultiChannelResampler::process):
+        * platform/audio/MultiChannelResampler.h:
+        * platform/audio/SincResampler.cpp:
+        (WebCore::calculateChunkSize):
+        (WebCore::SincResampler::updateRegions):
+        * platform/audio/SincResampler.h:
+
 2020-11-21  Simon Fraser  <[email protected]>
 
         Propagate the 'wheelEventGesturesBecomeNonBlocking' setting to the ScrollingTree

Modified: trunk/Source/WebCore/platform/audio/MultiChannelResampler.cpp (270156 => 270157)


--- trunk/Source/WebCore/platform/audio/MultiChannelResampler.cpp	2020-11-22 03:54:00 UTC (rev 270156)
+++ trunk/Source/WebCore/platform/audio/MultiChannelResampler.cpp	2020-11-22 04:51:13 UTC (rev 270157)
@@ -38,25 +38,28 @@
 
 // ChannelProvider provides a single channel of audio data (one channel at a time) for each channel
 // of data provided to us in a multi-channel provider.
-class MultiChannelResampler::ChannelProvider : public AudioSourceProvider {
+class MultiChannelResampler::ChannelProvider {
     WTF_MAKE_FAST_ALLOCATED;
 public:
-    explicit ChannelProvider(unsigned numberOfChannels)
-        : m_numberOfChannels(numberOfChannels)
+    explicit ChannelProvider(unsigned numberOfChannels, unsigned requestFrames)
+        : m_multiChannelBus(AudioBus::create(numberOfChannels, requestFrames))
     {
     }
 
     void setProvider(AudioSourceProvider* multiChannelProvider)
     {
-        m_currentChannel = 0;
-        m_framesToProcess = 0;
         m_multiChannelProvider = multiChannelProvider;
     }
 
     // provideInput() will be called once for each channel, starting with the first channel.
     // Each time it's called, it will provide the next channel of data.
-    void provideInput(AudioBus* bus, size_t framesToProcess) override
+    void provideInputForChannel(AudioBus* bus, size_t framesToProcess, unsigned channelIndex)
     {
+        ASSERT(channelIndex < m_multiChannelBus->numberOfChannels());
+        ASSERT(framesToProcess <= m_multiChannelBus->length());
+        if (framesToProcess > m_multiChannelBus->length())
+            return;
+
         bool isBusGood = bus && bus->numberOfChannels() == 1;
         ASSERT(isBusGood);
         if (!isBusGood)
@@ -64,37 +67,30 @@
 
         // Get the data from the multi-channel provider when the first channel asks for it.
         // For subsequent channels, we can just dish out the channel data from that (stored in m_multiChannelBus).
-        if (!m_currentChannel) {
+        if (!channelIndex) {
             m_framesToProcess = framesToProcess;
-            m_multiChannelBus = AudioBus::create(m_numberOfChannels, framesToProcess);
             m_multiChannelProvider->provideInput(m_multiChannelBus.get(), framesToProcess);
         }
 
         // All channels must ask for the same amount. This should always be the case, but let's just make sure.
-        bool isGood = m_multiChannelBus.get() && framesToProcess == m_framesToProcess;
+        bool isGood = framesToProcess == m_framesToProcess;
         ASSERT(isGood);
         if (!isGood)
             return;
 
         // Copy the channel data from what we received from m_multiChannelProvider.
-        ASSERT(m_currentChannel <= m_numberOfChannels);
-        if (m_currentChannel < m_numberOfChannels) {
-            memcpy(bus->channel(0)->mutableData(), m_multiChannelBus->channel(m_currentChannel)->data(), sizeof(float) * framesToProcess);
-            ++m_currentChannel;
-        }
+        memcpy(bus->channel(0)->mutableData(), m_multiChannelBus->channel(channelIndex)->data(), sizeof(float) * framesToProcess);
     }
 
 private:
     AudioSourceProvider* m_multiChannelProvider { nullptr };
     RefPtr<AudioBus> m_multiChannelBus;
-    unsigned m_numberOfChannels { 0 };
-    unsigned m_currentChannel { 0 };
     size_t m_framesToProcess { 0 }; // Used to verify that all channels ask for the same amount.
 };
 
-MultiChannelResampler::MultiChannelResampler(double scaleFactor, unsigned numberOfChannels, Optional<unsigned> requestFrames)
+MultiChannelResampler::MultiChannelResampler(double scaleFactor, unsigned numberOfChannels, unsigned requestFrames)
     : m_numberOfChannels(numberOfChannels)
-    , m_channelProvider(makeUnique<ChannelProvider>(m_numberOfChannels))
+    , m_channelProvider(makeUnique<ChannelProvider>(m_numberOfChannels, requestFrames))
 {
     // Create each channel's resampler.
     for (unsigned channelIndex = 0; channelIndex < numberOfChannels; ++channelIndex)
@@ -112,14 +108,25 @@
     // channelProvider wraps the original multi-channel provider and dishes out one channel at a time.
     m_channelProvider->setProvider(provider);
 
-    for (unsigned channelIndex = 0; channelIndex < m_numberOfChannels; ++channelIndex) {
-        // Depending on the sample-rate scale factor, and the internal buffering used in a SincResampler
-        // kernel, this call to process() will only sometimes call provideInput() on the channelProvider.
-        // However, if it calls provideInput() for the first channel, then it will call it for the remaining
-        // channels, since they all buffer in the same way and are processing the same number of frames.
-        m_kernels[channelIndex]->process(m_channelProvider.get(),
-                                         destination->channel(channelIndex)->mutableData(),
-                                         framesToProcess);
+    // We need to ensure that SincResampler only calls provideInput() once for each channel or it will confuse the logic
+    // inside ChannelProvider. To ensure this, we chunk the number of requested frames into SincResampler::chunkSize()
+    // sized chunks. SincResampler guarantees it will only call provideInput() once once we resample this way.
+    m_outputFramesReady = 0;
+    while (m_outputFramesReady < framesToProcess) {
+        size_t chunkSize = m_kernels[0]->chunkSize();
+        size_t framesThisTime = std::min(framesToProcess - m_outputFramesReady, chunkSize);
+
+        for (unsigned channelIndex = 0; channelIndex < m_numberOfChannels; ++channelIndex) {
+            ASSERT(chunkSize == m_kernels[channelIndex]->chunkSize());
+            bool wasProvideInputCalled = false;
+            m_kernels[channelIndex]->process(destination->channel(channelIndex)->mutableData() + m_outputFramesReady, framesThisTime, [this, channelIndex, &wasProvideInputCalled](AudioBus* bus, size_t framesToProcess) {
+                ASSERT_WITH_MESSAGE(!wasProvideInputCalled, "provideInputForChannel should only be called once");
+                wasProvideInputCalled = true;
+                m_channelProvider->provideInputForChannel(bus, framesToProcess, channelIndex);
+            });
+        }
+
+        m_outputFramesReady += framesThisTime;
     }
 
     m_channelProvider->setProvider(nullptr);

Modified: trunk/Source/WebCore/platform/audio/MultiChannelResampler.h (270156 => 270157)


--- trunk/Source/WebCore/platform/audio/MultiChannelResampler.h	2020-11-22 03:54:00 UTC (rev 270156)
+++ trunk/Source/WebCore/platform/audio/MultiChannelResampler.h	2020-11-22 04:51:13 UTC (rev 270157)
@@ -41,7 +41,7 @@
     WTF_MAKE_FAST_ALLOCATED;
 public:   
     // requestFrames constrols the size of the buffer in frames when AudioSourceProvider::provideInput() is called.
-    explicit MultiChannelResampler(double scaleFactor, unsigned numberOfChannels, Optional<unsigned> requestFrames = WTF::nullopt);
+    explicit MultiChannelResampler(double scaleFactor, unsigned numberOfChannels, unsigned requestFrames = SincResampler::defaultRequestFrames);
     ~MultiChannelResampler();
 
     // Process given AudioSourceProvider for streaming applications.
@@ -59,6 +59,7 @@
 
     class ChannelProvider;
     std::unique_ptr<ChannelProvider> m_channelProvider;
+    size_t m_outputFramesReady { 0 };
 };
 
 } // namespace WebCore

Modified: trunk/Source/WebCore/platform/audio/SincResampler.cpp (270156 => 270157)


--- trunk/Source/WebCore/platform/audio/SincResampler.cpp	2020-11-22 03:54:00 UTC (rev 270156)
+++ trunk/Source/WebCore/platform/audio/SincResampler.cpp	2020-11-22 04:51:13 UTC (rev 270157)
@@ -111,14 +111,18 @@
 
 namespace WebCore {
 
-constexpr unsigned defaultRequestFrames { 512 };
 constexpr unsigned kernelSize { 32 };
 constexpr unsigned numberOfKernelOffsets { 32 };
 
-SincResampler::SincResampler(double scaleFactor, Optional<unsigned> requestFrames)
+static size_t calculateChunkSize(unsigned blockSize, double scaleFactor)
+{
+    return blockSize / scaleFactor;
+}
+
+SincResampler::SincResampler(double scaleFactor, unsigned requestFrames)
     : m_scaleFactor(scaleFactor)
     , m_kernelStorage(kernelSize * (numberOfKernelOffsets + 1))
-    , m_requestFrames(requestFrames.valueOr(defaultRequestFrames))
+    , m_requestFrames(requestFrames)
     , m_inputBuffer(m_requestFrames + kernelSize) // See input buffer layout above.
     , m_r1(m_inputBuffer.data())
     , m_r2(m_inputBuffer.data() + kernelSize / 2)
@@ -137,6 +141,7 @@
     m_r3 = m_r0 + m_requestFrames - kernelSize;
     m_r4 = m_r0 + m_requestFrames - kernelSize / 2;
     m_blockSize = m_r4 - m_r2;
+    m_chunkSize = calculateChunkSize(m_blockSize, m_scaleFactor);
 
     // m_r1 at the beginning of the buffer.
     ASSERT(m_r1 == m_inputBuffer.data());
@@ -187,10 +192,10 @@
     }
 }
 
-void SincResampler::consumeSource(float* buffer, unsigned numberOfSourceFrames)
+void SincResampler::consumeSource(float* buffer, unsigned numberOfSourceFrames, const Function<void(AudioBus* bus, size_t framesToProcess)>& provideInput)
 {
-    ASSERT(m_sourceProvider);
-    if (!m_sourceProvider)
+    ASSERT(provideInput);
+    if (!provideInput)
         return;
 
     // Wrap the provided buffer by an AudioBus for use by the source provider.
@@ -200,14 +205,14 @@
     // FIXME: Find a way to make the following const-correct:
     m_internalBus->setChannelMemory(0, buffer, numberOfSourceFrames);
     
-    m_sourceProvider->provideInput(m_internalBus.get(), numberOfSourceFrames);
+    provideInput(m_internalBus.get(), numberOfSourceFrames);
 }
 
 namespace {
 
-// BufferSourceProvider is an AudioSourceProvider wrapping an in-memory buffer.
+// BufferSourceProvider is an audio source provider wrapping an in-memory buffer.
 
-class BufferSourceProvider : public AudioSourceProvider {
+class BufferSourceProvider {
 public:
     explicit BufferSourceProvider(const float* source, size_t numberOfSourceFrames)
         : m_source(source)
@@ -216,7 +221,7 @@
     }
     
     // Consumes samples from the in-memory buffer.
-    void provideInput(AudioBus* bus, size_t framesToProcess) override
+    void provideInput(AudioBus* bus, size_t framesToProcess)
     {
         ASSERT(m_source && bus);
         if (!m_source || !bus)
@@ -253,7 +258,9 @@
     
     while (remaining) {
         unsigned framesThisTime = std::min(remaining, m_requestFrames);
-        process(&sourceProvider, destination, framesThisTime);
+        process(destination, framesThisTime, [&sourceProvider](AudioBus* bus, size_t framesToProcess) {
+            sourceProvider.provideInput(bus, framesToProcess);
+        });
         
         destination += framesThisTime;
         remaining -= framesThisTime;
@@ -260,13 +267,11 @@
     }
 }
 
-void SincResampler::process(AudioSourceProvider* sourceProvider, float* destination, size_t framesToProcess)
+void SincResampler::process(float* destination, size_t framesToProcess, const Function<void(AudioBus* bus, size_t framesToProcess)>& provideInput)
 {
-    ASSERT(sourceProvider);
-    if (!sourceProvider)
+    ASSERT(provideInput);
+    if (!provideInput)
         return;
-    
-    m_sourceProvider = sourceProvider;
 
     unsigned numberOfDestinationFrames = framesToProcess;
 
@@ -273,7 +278,7 @@
     // Step (1)
     // Prime the input buffer at the start of the input stream.
     if (!m_isBufferPrimed) {
-        consumeSource(m_r0, m_requestFrames);
+        consumeSource(m_r0, m_requestFrames, provideInput);
         m_isBufferPrimed = true;
     }
     
@@ -521,7 +526,7 @@
 
         // Step (5)
         // Refresh the buffer with more input.
-        consumeSource(m_r0, m_requestFrames);
+        consumeSource(m_r0, m_requestFrames, provideInput);
     }
 }
 

Modified: trunk/Source/WebCore/platform/audio/SincResampler.h (270156 => 270157)


--- trunk/Source/WebCore/platform/audio/SincResampler.h	2020-11-22 03:54:00 UTC (rev 270156)
+++ trunk/Source/WebCore/platform/audio/SincResampler.h	2020-11-22 04:51:13 UTC (rev 270157)
@@ -41,19 +41,23 @@
 class SincResampler final {
     WTF_MAKE_FAST_ALLOCATED;
 public:
+    static constexpr unsigned defaultRequestFrames { 512 };
+
     // scaleFactor == sourceSampleRate / destinationSampleRate
     // requestFrames controls the size in frames of the buffer requested by each provideInput() call.
-    SincResampler(double scaleFactor, Optional<unsigned> requestFrames = WTF::nullopt);
+    SincResampler(double scaleFactor, unsigned requestFrames = defaultRequestFrames);
     
+    size_t chunkSize() const { return m_chunkSize; }
+
     // Processes numberOfSourceFrames from source to produce numberOfSourceFrames / scaleFactor frames in destination.
     void process(const float* source, float* destination, unsigned numberOfSourceFrames);
 
-    // Process with input source callback function for streaming applications.
-    void process(AudioSourceProvider*, float* destination, size_t framesToProcess);
+    // Process with provideInput callback function for streaming applications.
+    void process(float* destination, size_t framesToProcess, const Function<void(AudioBus* bus, size_t framesToProcess)>& provideInput);
 
 protected:
     void initializeKernel();
-    void consumeSource(float* buffer, unsigned numberOfSourceFrames);
+    void consumeSource(float* buffer, unsigned numberOfSourceFrames, const Function<void(AudioBus* bus, size_t framesToProcess)>& provideInput);
     void updateRegions(bool isSecondLoad);
     
     double m_scaleFactor;
@@ -72,6 +76,8 @@
     // The number of source frames processed per pass.
     unsigned m_blockSize { 0 };
 
+    size_t m_chunkSize { 0 };
+
     // Source is copied into this buffer for each processing pass.
     AudioFloatArray m_inputBuffer;
 
@@ -85,9 +91,6 @@
 
     const float* m_source { nullptr };
     unsigned m_sourceFramesAvailable { 0 };
-    
-    // m_sourceProvider is used to provide the audio input stream to the resampler.
-    AudioSourceProvider* m_sourceProvider { nullptr };
 
     // The buffer is primed once at the very beginning of processing.
     bool m_isBufferPrimed { false };
_______________________________________________
webkit-changes mailing list
[email protected]
https://lists.webkit.org/mailman/listinfo/webkit-changes

Reply via email to