Skip to content

Commit dd0dea2

Browse files
committed
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. Canonical link: https://commits.webkit.org/231850@main git-svn-id: https://svn.webkit.org/repository/webkit/trunk@270141 268f45cc-cd09-0410-ab3c-d52691b4dbfc
1 parent ec25a93 commit dd0dea2

10 files changed

Lines changed: 177 additions & 44 deletions

LayoutTests/ChangeLog

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
1+
2020-11-20 Chris Dumez <cdumez@apple.com>
2+
3+
Poor resampling quality when using AudioContext sampleRate parameter
4+
https://bugs.webkit.org/show_bug.cgi?id=219201
5+
6+
Reviewed by Geoff Garen.
7+
8+
Add layout test coverage that would hit assertions in debug.
9+
10+
* webaudio/audiocontext-large-samplerate-expected.txt: Added.
11+
* webaudio/audiocontext-large-samplerate.html: Added.
12+
* webaudio/audiocontext-low-samplerate-expected.txt: Added.
13+
* webaudio/audiocontext-low-samplerate.html: Added.
14+
115
2020-11-20 Chris Dumez <cdumez@apple.com>
216

317
(r267253) [ Mac ] webaudio/AudioParam/audioparam-processing.html is flaky failing
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
Tests that we do not crash when using a very large sample rate
2+
3+
On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
4+
5+
6+
PASS context.sampleRate is 384000
7+
PASS context.state is "running"
8+
PASS successfullyParsed is true
9+
10+
TEST COMPLETE
11+
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<script src="../resources/js-test.js"></script>
4+
<body>
5+
<script>
6+
description("Tests that we do not crash when using a very large sample rate");
7+
jsTestIsAsync = true;
8+
9+
context = new AudioContext({ sampleRate: 384000 });
10+
shouldBe("context.sampleRate", "384000");
11+
12+
node = new ConstantSourceNode(context, { offset: 0.5 });
13+
node.connect(context.destination);
14+
node.start();
15+
16+
setTimeout(() => {
17+
shouldBeEqualToString("context.state", "running");
18+
finishJSTest();
19+
}, 100);
20+
</script>
21+
</body>
22+
</html>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
Tests that we do not crash when using a very low sample rate
2+
3+
On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
4+
5+
6+
PASS context.sampleRate is 3000
7+
PASS context.state is "running"
8+
PASS successfullyParsed is true
9+
10+
TEST COMPLETE
11+
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<script src="../resources/js-test.js"></script>
4+
<body>
5+
<script>
6+
description("Tests that we do not crash when using a very low sample rate");
7+
jsTestIsAsync = true;
8+
9+
context = new AudioContext({ sampleRate: 3000 });
10+
shouldBe("context.sampleRate", "3000");
11+
12+
node = new ConstantSourceNode(context, { offset: 0.5 });
13+
node.connect(context.destination);
14+
node.start();
15+
16+
setTimeout(() => {
17+
shouldBeEqualToString("context.state", "running");
18+
finishJSTest();
19+
}, 100);
20+
</script>
21+
</body>
22+
</html>

Source/WebCore/ChangeLog

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,40 @@
1+
2020-11-20 Chris Dumez <cdumez@apple.com>
2+
3+
Poor resampling quality when using AudioContext sampleRate parameter
4+
https://bugs.webkit.org/show_bug.cgi?id=219201
5+
6+
Reviewed by Geoff Garen.
7+
8+
MultiChannelResampler uses a SincResampler per audio channel. In MultiChannelResampler::process(),
9+
it was calling SincResampler::process() for each channel, which would potentially end up calling
10+
MultiChannelResampler::ChannelProvider::provideInput() to provide channel data used for resampling.
11+
The issue was that MultiChannelResampler::ChannelProvider::provideInput() is implemented in such
12+
a way that things will break if provideInput() gets called more than once per channel. When using
13+
an AudioContext's sample rate larger than the hardware sample rate, provideInput() was getting
14+
called more than once per channel and this resulted in very poor resampling quality.
15+
16+
To address the issue, MultiChannelResampler::process() now processes the data in chunks that
17+
are small enough to guarantee that MultiChannelResampler::ChannelProvider::provideInput() will
18+
never get called more than once per audio channel.
19+
20+
The fix is based on the corresponding MultiChannelResampler / SincResampler implementation in
21+
Chrome:
22+
- https://github.com/chromium/chromium/blob/master/media/base/multi_channel_resampler.cc
23+
- https://github.com/chromium/chromium/blob/master/media/base/sinc_resampler.cc
24+
25+
Tests: webaudio/audiocontext-large-samplerate.html
26+
webaudio/audiocontext-low-samplerate.html
27+
28+
* platform/audio/MultiChannelResampler.cpp:
29+
(WebCore::MultiChannelResampler::ChannelProvider::setProvider):
30+
(WebCore::MultiChannelResampler::ChannelProvider::setCurrentChannel):
31+
(WebCore::MultiChannelResampler::process):
32+
* platform/audio/MultiChannelResampler.h:
33+
* platform/audio/SincResampler.cpp:
34+
(WebCore::calculateChunkSize):
35+
(WebCore::SincResampler::updateRegions):
36+
* platform/audio/SincResampler.h:
37+
138
2020-11-20 Kate Cheney <katherine_cheney@apple.com>
239

340
PCM: Persist pending ad clicks and attributions so they can survive browser restart

Source/WebCore/platform/audio/MultiChannelResampler.cpp

Lines changed: 31 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -33,62 +33,59 @@
3333
#include "MultiChannelResampler.h"
3434

3535
#include "AudioBus.h"
36+
#include "AudioUtilities.h"
3637

3738
namespace WebCore {
3839

3940
// ChannelProvider provides a single channel of audio data (one channel at a time) for each channel
4041
// of data provided to us in a multi-channel provider.
41-
class MultiChannelResampler::ChannelProvider : public AudioSourceProvider {
42+
class MultiChannelResampler::ChannelProvider {
4243
WTF_MAKE_FAST_ALLOCATED;
4344
public:
4445
explicit ChannelProvider(unsigned numberOfChannels)
45-
: m_numberOfChannels(numberOfChannels)
46+
: m_multiChannelBus(AudioBus::create(numberOfChannels, AudioUtilities::renderQuantumSize))
4647
{
4748
}
4849

4950
void setProvider(AudioSourceProvider* multiChannelProvider)
5051
{
51-
m_currentChannel = 0;
52-
m_framesToProcess = 0;
5352
m_multiChannelProvider = multiChannelProvider;
5453
}
5554

5655
// provideInput() will be called once for each channel, starting with the first channel.
5756
// Each time it's called, it will provide the next channel of data.
58-
void provideInput(AudioBus* bus, size_t framesToProcess) override
57+
void provideInputForChannel(AudioBus* bus, size_t framesToProcess, unsigned channelIndex)
5958
{
59+
ASSERT(channelIndex < m_multiChannelBus->numberOfChannels());
60+
ASSERT(framesToProcess <= AudioUtilities::renderQuantumSize);
61+
if (framesToProcess > AudioUtilities::renderQuantumSize)
62+
return;
63+
6064
bool isBusGood = bus && bus->numberOfChannels() == 1;
6165
ASSERT(isBusGood);
6266
if (!isBusGood)
6367
return;
6468

6569
// Get the data from the multi-channel provider when the first channel asks for it.
6670
// For subsequent channels, we can just dish out the channel data from that (stored in m_multiChannelBus).
67-
if (!m_currentChannel) {
71+
if (!channelIndex) {
6872
m_framesToProcess = framesToProcess;
69-
m_multiChannelBus = AudioBus::create(m_numberOfChannels, framesToProcess);
7073
m_multiChannelProvider->provideInput(m_multiChannelBus.get(), framesToProcess);
7174
}
7275

7376
// All channels must ask for the same amount. This should always be the case, but let's just make sure.
74-
bool isGood = m_multiChannelBus.get() && framesToProcess == m_framesToProcess;
77+
bool isGood = framesToProcess == m_framesToProcess;
7578
ASSERT(isGood);
7679
if (!isGood)
7780
return;
7881

7982
// Copy the channel data from what we received from m_multiChannelProvider.
80-
ASSERT(m_currentChannel <= m_numberOfChannels);
81-
if (m_currentChannel < m_numberOfChannels) {
82-
memcpy(bus->channel(0)->mutableData(), m_multiChannelBus->channel(m_currentChannel)->data(), sizeof(float) * framesToProcess);
83-
++m_currentChannel;
84-
}
83+
memcpy(bus->channel(0)->mutableData(), m_multiChannelBus->channel(channelIndex)->data(), sizeof(float) * framesToProcess);
8584
}
8685

8786
private:
8887
AudioSourceProvider* m_multiChannelProvider { nullptr };
8988
RefPtr<AudioBus> m_multiChannelBus;
90-
unsigned m_numberOfChannels { 0 };
91-
unsigned m_currentChannel { 0 };
9289
size_t m_framesToProcess { 0 }; // Used to verify that all channels ask for the same amount.
9390
};
9491

@@ -112,14 +109,25 @@ void MultiChannelResampler::process(AudioSourceProvider* provider, AudioBus* des
112109
// channelProvider wraps the original multi-channel provider and dishes out one channel at a time.
113110
m_channelProvider->setProvider(provider);
114111

115-
for (unsigned channelIndex = 0; channelIndex < m_numberOfChannels; ++channelIndex) {
116-
// Depending on the sample-rate scale factor, and the internal buffering used in a SincResampler
117-
// kernel, this call to process() will only sometimes call provideInput() on the channelProvider.
118-
// However, if it calls provideInput() for the first channel, then it will call it for the remaining
119-
// channels, since they all buffer in the same way and are processing the same number of frames.
120-
m_kernels[channelIndex]->process(m_channelProvider.get(),
121-
destination->channel(channelIndex)->mutableData(),
122-
framesToProcess);
112+
// We need to ensure that SincResampler only calls provideInput() once for each channel or it will confuse the logic
113+
// inside ChannelProvider. To ensure this, we chunk the number of requested frames into SincResampler::chunkSize()
114+
// sized chunks. SincResampler guarantees it will only call provideInput() once once we resample this way.
115+
m_outputFramesReady = 0;
116+
while (m_outputFramesReady < framesToProcess) {
117+
size_t chunkSize = m_kernels[0]->chunkSize();
118+
size_t framesThisTime = std::min(framesToProcess - m_outputFramesReady, chunkSize);
119+
120+
for (unsigned channelIndex = 0; channelIndex < m_numberOfChannels; ++channelIndex) {
121+
ASSERT(chunkSize == m_kernels[channelIndex]->chunkSize());
122+
bool wasProvideInputCalled = false;
123+
m_kernels[channelIndex]->process(destination->channel(channelIndex)->mutableData() + m_outputFramesReady, framesThisTime, [this, channelIndex, &wasProvideInputCalled](AudioBus* bus, size_t framesToProcess) {
124+
ASSERT_WITH_MESSAGE(!wasProvideInputCalled, "provideInputForChannel should only be called once");
125+
wasProvideInputCalled = true;
126+
m_channelProvider->provideInputForChannel(bus, framesToProcess, channelIndex);
127+
});
128+
}
129+
130+
m_outputFramesReady += framesThisTime;
123131
}
124132

125133
m_channelProvider->setProvider(nullptr);

Source/WebCore/platform/audio/MultiChannelResampler.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ class MultiChannelResampler final {
5959

6060
class ChannelProvider;
6161
std::unique_ptr<ChannelProvider> m_channelProvider;
62+
size_t m_outputFramesReady { 0 };
6263
};
6364

6465
} // namespace WebCore

Source/WebCore/platform/audio/SincResampler.cpp

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,11 @@ constexpr unsigned defaultRequestFrames { 512 };
115115
constexpr unsigned kernelSize { 32 };
116116
constexpr unsigned numberOfKernelOffsets { 32 };
117117

118+
static size_t calculateChunkSize(unsigned blockSize, double scaleFactor)
119+
{
120+
return blockSize / scaleFactor;
121+
}
122+
118123
SincResampler::SincResampler(double scaleFactor, Optional<unsigned> requestFrames)
119124
: m_scaleFactor(scaleFactor)
120125
, m_kernelStorage(kernelSize * (numberOfKernelOffsets + 1))
@@ -137,6 +142,7 @@ void SincResampler::updateRegions(bool isSecondLoad)
137142
m_r3 = m_r0 + m_requestFrames - kernelSize;
138143
m_r4 = m_r0 + m_requestFrames - kernelSize / 2;
139144
m_blockSize = m_r4 - m_r2;
145+
m_chunkSize = calculateChunkSize(m_blockSize, m_scaleFactor);
140146

141147
// m_r1 at the beginning of the buffer.
142148
ASSERT(m_r1 == m_inputBuffer.data());
@@ -187,10 +193,10 @@ void SincResampler::initializeKernel()
187193
}
188194
}
189195

190-
void SincResampler::consumeSource(float* buffer, unsigned numberOfSourceFrames)
196+
void SincResampler::consumeSource(float* buffer, unsigned numberOfSourceFrames, const Function<void(AudioBus* bus, size_t framesToProcess)>& provideInput)
191197
{
192-
ASSERT(m_sourceProvider);
193-
if (!m_sourceProvider)
198+
ASSERT(provideInput);
199+
if (!provideInput)
194200
return;
195201

196202
// Wrap the provided buffer by an AudioBus for use by the source provider.
@@ -200,14 +206,14 @@ void SincResampler::consumeSource(float* buffer, unsigned numberOfSourceFrames)
200206
// FIXME: Find a way to make the following const-correct:
201207
m_internalBus->setChannelMemory(0, buffer, numberOfSourceFrames);
202208

203-
m_sourceProvider->provideInput(m_internalBus.get(), numberOfSourceFrames);
209+
provideInput(m_internalBus.get(), numberOfSourceFrames);
204210
}
205211

206212
namespace {
207213

208-
// BufferSourceProvider is an AudioSourceProvider wrapping an in-memory buffer.
214+
// BufferSourceProvider is an audio source provider wrapping an in-memory buffer.
209215

210-
class BufferSourceProvider : public AudioSourceProvider {
216+
class BufferSourceProvider {
211217
public:
212218
explicit BufferSourceProvider(const float* source, size_t numberOfSourceFrames)
213219
: m_source(source)
@@ -216,7 +222,7 @@ class BufferSourceProvider : public AudioSourceProvider {
216222
}
217223

218224
// Consumes samples from the in-memory buffer.
219-
void provideInput(AudioBus* bus, size_t framesToProcess) override
225+
void provideInput(AudioBus* bus, size_t framesToProcess)
220226
{
221227
ASSERT(m_source && bus);
222228
if (!m_source || !bus)
@@ -253,27 +259,27 @@ void SincResampler::process(const float* source, float* destination, unsigned nu
253259

254260
while (remaining) {
255261
unsigned framesThisTime = std::min(remaining, m_requestFrames);
256-
process(&sourceProvider, destination, framesThisTime);
262+
process(destination, framesThisTime, [&sourceProvider](AudioBus* bus, size_t framesToProcess) {
263+
sourceProvider.provideInput(bus, framesToProcess);
264+
});
257265

258266
destination += framesThisTime;
259267
remaining -= framesThisTime;
260268
}
261269
}
262270

263-
void SincResampler::process(AudioSourceProvider* sourceProvider, float* destination, size_t framesToProcess)
271+
void SincResampler::process(float* destination, size_t framesToProcess, const Function<void(AudioBus* bus, size_t framesToProcess)>& provideInput)
264272
{
265-
ASSERT(sourceProvider);
266-
if (!sourceProvider)
273+
ASSERT(provideInput);
274+
if (!provideInput)
267275
return;
268-
269-
m_sourceProvider = sourceProvider;
270276

271277
unsigned numberOfDestinationFrames = framesToProcess;
272278

273279
// Step (1)
274280
// Prime the input buffer at the start of the input stream.
275281
if (!m_isBufferPrimed) {
276-
consumeSource(m_r0, m_requestFrames);
282+
consumeSource(m_r0, m_requestFrames, provideInput);
277283
m_isBufferPrimed = true;
278284
}
279285

@@ -521,7 +527,7 @@ void SincResampler::process(AudioSourceProvider* sourceProvider, float* destinat
521527

522528
// Step (5)
523529
// Refresh the buffer with more input.
524-
consumeSource(m_r0, m_requestFrames);
530+
consumeSource(m_r0, m_requestFrames, provideInput);
525531
}
526532
}
527533

Source/WebCore/platform/audio/SincResampler.h

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,15 +45,17 @@ class SincResampler final {
4545
// requestFrames controls the size in frames of the buffer requested by each provideInput() call.
4646
SincResampler(double scaleFactor, Optional<unsigned> requestFrames = WTF::nullopt);
4747

48+
size_t chunkSize() const { return m_chunkSize; }
49+
4850
// Processes numberOfSourceFrames from source to produce numberOfSourceFrames / scaleFactor frames in destination.
4951
void process(const float* source, float* destination, unsigned numberOfSourceFrames);
5052

51-
// Process with input source callback function for streaming applications.
52-
void process(AudioSourceProvider*, float* destination, size_t framesToProcess);
53+
// Process with provideInput callback function for streaming applications.
54+
void process(float* destination, size_t framesToProcess, const Function<void(AudioBus* bus, size_t framesToProcess)>& provideInput);
5355

5456
protected:
5557
void initializeKernel();
56-
void consumeSource(float* buffer, unsigned numberOfSourceFrames);
58+
void consumeSource(float* buffer, unsigned numberOfSourceFrames, const Function<void(AudioBus* bus, size_t framesToProcess)>& provideInput);
5759
void updateRegions(bool isSecondLoad);
5860

5961
double m_scaleFactor;
@@ -72,6 +74,8 @@ class SincResampler final {
7274
// The number of source frames processed per pass.
7375
unsigned m_blockSize { 0 };
7476

77+
size_t m_chunkSize { 0 };
78+
7579
// Source is copied into this buffer for each processing pass.
7680
AudioFloatArray m_inputBuffer;
7781

@@ -85,9 +89,6 @@ class SincResampler final {
8589

8690
const float* m_source { nullptr };
8791
unsigned m_sourceFramesAvailable { 0 };
88-
89-
// m_sourceProvider is used to provide the audio input stream to the resampler.
90-
AudioSourceProvider* m_sourceProvider { nullptr };
9192

9293
// The buffer is primed once at the very beginning of processing.
9394
bool m_isBufferPrimed { false };

0 commit comments

Comments
 (0)