-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Cherry-pick a78127a. rdar://123634374
[Cocoa] Audio distortion during media playback when many AudioContexts are created https://bugs.webkit.org/show_bug.cgi?id=269833 rdar://122590884 Reviewed by Chris Dumez. In WebKit, each AudioContext created results in an additional thread serving that context's AudioDestination. (In WebKitLegacy, each AudioContext will result in an additional AudioOutputUnit running on a single high-priority audio thread.) When many threads (or AudioOutputUnits) are created, the overhead alone can cause underruns. And when this happens on the high-priority audio thread, it affects all audio playback within that process. Rather than create new threads or AudioOutputUnits (that are all rendering on the same thread to the same buffer in the end) for each AudioContext, a shared AudioDestination can be used for multiple AudioContext's with the same number of channels and sample rate. For common scenarios, this means only one high-priority audio thread will be created and serviced by a single AudioDestination. Specifically for WebKit, it means a single RemoteAudioDestination/Proxy pair for each WebContent process. * Source/WebCore/platform/audio/SharedAudioDestination.cpp: Added. (WebCore::SharedAudioDestinationAdapter::framesPerBuffer const): (WebCore::SharedAudioDestinationAdapter::sharedMap): (WebCore::SharedAudioDestinationAdapter::ensureAdapter): (WebCore::SharedAudioDestinationAdapter::SharedAudioDestinationAdapter): (WebCore::m_configurationSemaphore): (WebCore::SharedAudioDestinationAdapter::addRenderer): (WebCore::SharedAudioDestinationAdapter::removeRenderer): (WebCore::SharedAudioDestinationAdapter::configureRenderThread): (WebCore::SharedAudioDestinationAdapter::render): (WebCore::SharedAudioDestination::create): (WebCore::SharedAudioDestination::SharedAudioDestination): (WebCore::SharedAudioDestination::~SharedAudioDestination): (WebCore::SharedAudioDestination::start): (WebCore::SharedAudioDestination::stop): (WebCore::SharedAudioDestination::framesPerBuffer const): (WebCore::SharedAudioDestination::setIsPlaying): * Source/WebCore/platform/audio/SharedAudioDestination.h: Added. * Source/WebCore/platform/audio/cocoa/AudioDestinationCocoa.cpp: (WebCore::AudioDestination::create): * Source/WebKit/WebProcess/GPU/media/WebMediaStrategy.cpp: (WebKit::WebMediaStrategy::createAudioDestination): Canonical link: https://commits.webkit.org/275262@main Identifier: 273664.1304@safari-7619.1.5-branch
- Loading branch information
Showing
8 changed files
with
418 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
328 changes: 328 additions & 0 deletions
328
Source/WebCore/platform/audio/SharedAudioDestination.cpp
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,328 @@ | ||
/* | ||
* Copyright (C) 2024 Apple Inc. All rights reserved. | ||
* | ||
* Redistribution and use in source and binary forms, with or without | ||
* modification, are permitted provided that the following conditions | ||
* are met: | ||
* 1. Redistributions of source code must retain the above copyright | ||
* notice, this list of conditions and the following disclaimer. | ||
* 2. Redistributions in binary form must reproduce the above copyright | ||
* notice, this list of conditions and the following disclaimer in the | ||
* documentation and/or other materials provided with the distribution. | ||
* | ||
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' | ||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, | ||
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR | ||
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS | ||
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR | ||
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF | ||
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS | ||
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN | ||
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) | ||
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF | ||
* THE POSSIBILITY OF SUCH DAMAGE. | ||
*/ | ||
|
||
#include "config.h" | ||
#include "SharedAudioDestination.h" | ||
|
||
#if ENABLE(WEB_AUDIO) | ||
|
||
#include "AudioUtilities.h" | ||
#include <wtf/NeverDestroyed.h> | ||
#include <wtf/WTFSemaphore.h> | ||
#include <wtf/WeakPtr.h> | ||
#include <wtf/WorkQueue.h> | ||
|
||
namespace WebCore { | ||
|
||
class SharedAudioDestinationAdapter : public ThreadSafeRefCountedAndCanMakeThreadSafeWeakPtr<SharedAudioDestinationAdapter>, public AudioIOCallback { | ||
public: | ||
using AudioDestinationCreationFunction = SharedAudioDestination::AudioDestinationCreationFunction; | ||
static Ref<SharedAudioDestinationAdapter> ensureAdapter(unsigned numberOfOutputChannels, float sampleRate, AudioDestinationCreationFunction&& ensureFunction); | ||
~SharedAudioDestinationAdapter(); | ||
|
||
void addRenderer(SharedAudioDestination&, CompletionHandler<void(bool)>&&); | ||
void removeRenderer(SharedAudioDestination&, CompletionHandler<void(bool)>&&); | ||
|
||
unsigned framesPerBuffer() const | ||
{ | ||
return m_workBus->length(); | ||
} | ||
|
||
private: | ||
using AdapterKey = std::tuple<unsigned, float>; | ||
using AdapterMap = HashMap<AdapterKey, ThreadSafeWeakPtr<SharedAudioDestinationAdapter>>; | ||
static AdapterMap& sharedMap(); | ||
|
||
SharedAudioDestinationAdapter(unsigned numberOfOutputChannels, float sampleRate, AudioDestinationCreationFunction&&); | ||
|
||
void render(AudioBus* sourceBus, AudioBus* destinationBus, size_t framesToProcess, const AudioIOPosition& outputPosition) final; | ||
void isPlayingDidChange() final { } | ||
|
||
void configureRenderThread(CompletionHandler<void(bool)>&&); | ||
|
||
Ref<AudioDestination> protectedDestination() { return m_destination; } | ||
Ref<AudioBus> protectedWorkBus() { return m_workBus; } | ||
Ref<WorkQueue> protectedConfigurationQueue() { return m_configurationQueue; } | ||
|
||
unsigned m_numberOfOutputChannels; | ||
float m_sampleRate; | ||
|
||
Ref<AudioDestination> m_destination; | ||
Ref<AudioBus> m_workBus; | ||
Ref<WorkQueue> m_configurationQueue; | ||
|
||
bool m_started { false }; | ||
|
||
Lock m_renderLock; | ||
Semaphore m_configurationSemaphore; | ||
|
||
// Only accessed on m_configurationQueue: | ||
Vector<RefPtr<SharedAudioDestination>> m_renderers; | ||
|
||
bool m_needsConfiguration WTF_GUARDED_BY_LOCK(m_renderLock) { true }; | ||
Vector<RefPtr<SharedAudioDestination>> m_newRenderers WTF_GUARDED_BY_LOCK(m_renderLock); | ||
|
||
// Only accessed on the audio thread: | ||
Vector<RefPtr<SharedAudioDestination>> m_configuredRenderers; | ||
}; | ||
|
||
auto SharedAudioDestinationAdapter::sharedMap() -> AdapterMap& | ||
{ | ||
static MainThreadNeverDestroyed<AdapterMap> map; | ||
return map; | ||
} | ||
|
||
Ref<SharedAudioDestinationAdapter> SharedAudioDestinationAdapter::ensureAdapter(unsigned numberOfOutputChannels, float sampleRate, AudioDestinationCreationFunction&& ensureFunction) | ||
{ | ||
std::tuple key { numberOfOutputChannels, sampleRate }; | ||
auto results = sharedMap().find(key); | ||
if (results != sharedMap().end()) { | ||
if (RefPtr existingAdapter = results->value.get()) | ||
return existingAdapter.releaseNonNull(); | ||
} | ||
|
||
Ref newAdapter = adoptRef(*new SharedAudioDestinationAdapter(numberOfOutputChannels, sampleRate, WTFMove(ensureFunction))); | ||
auto weakAdapter = ThreadSafeWeakPtr<SharedAudioDestinationAdapter> { newAdapter.get() }; | ||
sharedMap().set(key, WTFMove(weakAdapter)); | ||
return newAdapter; | ||
} | ||
|
||
SharedAudioDestinationAdapter::SharedAudioDestinationAdapter(unsigned numberOfOutputChannels, float sampleRate, AudioDestinationCreationFunction&& ensureFunction) | ||
: m_numberOfOutputChannels { numberOfOutputChannels } | ||
, m_sampleRate { sampleRate } | ||
, m_destination { ensureFunction(*this) } | ||
, m_workBus { AudioBus::create(numberOfOutputChannels, AudioUtilities::renderQuantumSize).releaseNonNull() } | ||
, m_configurationQueue { WorkQueue::create("SharedAudioDestinationAdapter configuration queue") } | ||
, m_configurationSemaphore(0) | ||
{ | ||
} | ||
|
||
SharedAudioDestinationAdapter::~SharedAudioDestinationAdapter() | ||
{ | ||
auto key = std::make_tuple(m_numberOfOutputChannels, m_sampleRate); | ||
sharedMap().remove(key); | ||
protectedDestination()->clearCallback(); | ||
} | ||
|
||
void SharedAudioDestinationAdapter::addRenderer(SharedAudioDestination& renderer, CompletionHandler<void(bool)>&& completionHandler) | ||
{ | ||
protectedConfigurationQueue()->dispatch([this, weakThis = ThreadSafeWeakPtr { *this }, renderer = RefPtr { &renderer }, completionHandler = WTFMove(completionHandler)] () mutable { | ||
RefPtr protectedThis = weakThis.get(); | ||
if (!protectedThis) { | ||
callOnMainThread([completionHandler = WTFMove(completionHandler)] () mutable { completionHandler(false); | ||
}); | ||
return; | ||
} | ||
|
||
if (!m_renderers.contains(renderer)) | ||
m_renderers.append(WTFMove(renderer)); | ||
configureRenderThread(WTFMove(completionHandler)); | ||
}); | ||
} | ||
|
||
void SharedAudioDestinationAdapter::removeRenderer(SharedAudioDestination& renderer, CompletionHandler<void(bool)>&& completionHandler) | ||
{ | ||
protectedConfigurationQueue()->dispatch([this, weakThis = ThreadSafeWeakPtr { *this }, renderer = RefPtr { &renderer }, completionHandler = WTFMove(completionHandler)] () mutable { | ||
RefPtr protectedThis = weakThis.get(); | ||
if (!protectedThis) { | ||
callOnMainThread([completionHandler = WTFMove(completionHandler)] () mutable { completionHandler(false); | ||
}); | ||
return; | ||
} | ||
|
||
m_renderers.removeFirst(renderer); | ||
configureRenderThread(WTFMove(completionHandler)); | ||
}); | ||
} | ||
|
||
void SharedAudioDestinationAdapter::configureRenderThread(CompletionHandler<void(bool)>&& completionHandler) | ||
{ | ||
ASSERT(m_configurationQueue->isCurrent()); | ||
{ | ||
Locker locker { m_renderLock }; | ||
m_newRenderers = m_renderers; | ||
m_needsConfiguration = true; | ||
} | ||
|
||
bool shouldStart = !m_started && !m_renderers.isEmpty(); | ||
bool shouldStop = m_started && m_renderers.isEmpty(); | ||
|
||
// If the destination has not been started, and the list of | ||
// renderers is empty, do not wait for the render thread to | ||
// finish configuration, as it will never run. | ||
bool shouldSkipRendering = !m_started && m_renderers.isEmpty(); | ||
|
||
if (!shouldSkipRendering) { | ||
// The AudioDestination must be started for configuration to take place. | ||
if (shouldStart) { | ||
// Dispatching to the main thread is required for destinations | ||
// which are subclasses of AudioDestinationResampler. | ||
callOnMainThreadAndWait([this] { | ||
protectedDestination()->start(nullptr, [] (bool) { }); | ||
}); | ||
m_started = true; | ||
} | ||
|
||
m_configurationSemaphore.wait(); | ||
|
||
// The AudioDestination must not be stopped before configuration takes place. | ||
if (shouldStop) { | ||
// Dispatching to the main thread is required for destinations | ||
// which are subclasses of AudioDestinationResampler. | ||
callOnMainThreadAndWait([this] { | ||
protectedDestination()->stop(); | ||
}); | ||
m_started = false; | ||
} | ||
} | ||
|
||
// Move the previously configured list of renderers to the MainThread for destruction: | ||
Vector<RefPtr<SharedAudioDestination>> renderersToDispose; | ||
{ | ||
Locker locker { m_renderLock }; | ||
renderersToDispose = std::exchange(m_newRenderers, { }); | ||
} | ||
|
||
callOnMainThread([completionHandler = WTFMove(completionHandler), renderersToDispose = WTFMove(renderersToDispose)]() mutable { | ||
renderersToDispose.clear(); | ||
completionHandler(true); | ||
}); | ||
} | ||
|
||
void SharedAudioDestinationAdapter::render(AudioBus* sourceBus, AudioBus* destinationBus, size_t numberOfFrames, const AudioIOPosition& outputPosition) | ||
{ | ||
if (m_renderLock.tryLock()) { | ||
Locker locker { AdoptLock, m_renderLock }; | ||
if (m_needsConfiguration) { | ||
// The SharedAudioDestinationAdapter avoids allocing or deallocing on the | ||
// high priority audio thread by merely swapping the contents of the renderer | ||
// configuration vectors. After the swap, the contents of m_newRenderers will | ||
// be destroyed by configureRenderThread() on the m_configurationWorkQueue. | ||
std::swap(m_newRenderers, m_configuredRenderers); | ||
m_needsConfiguration = false; | ||
m_configurationSemaphore.signal(); | ||
} | ||
} | ||
|
||
bool isFirstRenderer = true; | ||
for (RefPtr renderer : m_configuredRenderers) { | ||
if (isFirstRenderer) { | ||
// The first renderer should render directly to destinationBus. | ||
renderer->sharedRender(sourceBus, destinationBus, numberOfFrames, outputPosition); | ||
isFirstRenderer = false; | ||
continue; | ||
} | ||
// Subsequent renderers should render to the m_workBus, which will | ||
// then be summed to the destinationBus. | ||
Ref protectedWorkBus = this->protectedWorkBus(); | ||
renderer->sharedRender(sourceBus, protectedWorkBus.ptr(), numberOfFrames, outputPosition); | ||
destinationBus->sumFrom(protectedWorkBus); | ||
} | ||
} | ||
Ref<SharedAudioDestination> SharedAudioDestination::create(AudioIOCallback& callback, unsigned numberOfOutputChannels, float sampleRate, AudioDestinationCreationFunction&& ensureFunction) | ||
{ | ||
return adoptRef(*new SharedAudioDestination(callback, numberOfOutputChannels, sampleRate, WTFMove(ensureFunction))); | ||
} | ||
|
||
SharedAudioDestination::SharedAudioDestination(AudioIOCallback& callback, unsigned numberOfOutputChannels, float sampleRate, AudioDestinationCreationFunction&& ensureFunction) | ||
: AudioDestination(callback, sampleRate) | ||
, m_outputAdapter(SharedAudioDestinationAdapter::ensureAdapter(numberOfOutputChannels, sampleRate, WTFMove(ensureFunction))) | ||
{ | ||
} | ||
|
||
SharedAudioDestination::~SharedAudioDestination() | ||
{ | ||
if (isPlaying()) | ||
stop([] (bool) { }); | ||
} | ||
|
||
void SharedAudioDestination::start(Function<void(Function<void()>&&)>&& dispatchToRenderThread, CompletionHandler<void(bool)>&& completionHandler) | ||
{ | ||
{ | ||
Locker locker { m_dispatchToRenderThreadLock }; | ||
m_dispatchToRenderThread = WTFMove(dispatchToRenderThread); | ||
} | ||
|
||
setIsPlaying(true); | ||
protectedOutputAdapter()->addRenderer(*this, WTFMove(completionHandler)); | ||
} | ||
|
||
void SharedAudioDestination::stop(CompletionHandler<void(bool)>&& completionHandler) | ||
{ | ||
setIsPlaying(false); | ||
protectedOutputAdapter()->removeRenderer(*this, WTFMove(completionHandler)); | ||
|
||
{ | ||
Locker locker { m_dispatchToRenderThreadLock }; | ||
m_dispatchToRenderThread = nullptr; | ||
} | ||
} | ||
|
||
unsigned SharedAudioDestination::framesPerBuffer() const | ||
{ | ||
return m_outputAdapter->framesPerBuffer(); | ||
} | ||
|
||
void SharedAudioDestination::setIsPlaying(bool isPlaying) | ||
{ | ||
ASSERT(isMainThread()); | ||
|
||
if (m_isPlaying == isPlaying) | ||
return; | ||
|
||
m_isPlaying = isPlaying; | ||
|
||
{ | ||
Locker locker { m_callbackLock }; | ||
if (m_callback) | ||
m_callback->isPlayingDidChange(); | ||
} | ||
} | ||
|
||
void SharedAudioDestination::sharedRender(AudioBus* sourceBus, AudioBus* destinationBus, size_t numberOfFrames, const AudioIOPosition& outputPosition) | ||
{ | ||
if (!m_dispatchToRenderThreadLock.tryLock()) { | ||
destinationBus->zero(); | ||
return; | ||
} | ||
|
||
Locker locker { AdoptLock, m_dispatchToRenderThreadLock }; | ||
if (!m_dispatchToRenderThread) | ||
callRenderCallback(sourceBus, destinationBus, numberOfFrames, outputPosition); | ||
else { | ||
m_dispatchToRenderThread([protectedThis = Ref { *this }, sourceBus = RefPtr { sourceBus }, destinationBus = RefPtr { destinationBus }, numberOfFrames, outputPosition]() mutable { | ||
protectedThis->callRenderCallback(sourceBus.get(), destinationBus.get(), numberOfFrames, outputPosition); | ||
}); | ||
} | ||
} | ||
|
||
Ref<SharedAudioDestinationAdapter> SharedAudioDestination::protectedOutputAdapter() | ||
{ | ||
return m_outputAdapter; | ||
} | ||
|
||
} // namespace WebCore | ||
|
||
#endif // ENABLE(WEB_AUDIO) |
Oops, something went wrong.