256 lines
9.2 KiB
C++
256 lines
9.2 KiB
C++
/*
|
|
* Copyright (C) 2012, Google Inc. All rights reserved.
|
|
* Copyright (C) 2020-2021, 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"
|
|
|
|
#if ENABLE(WEB_AUDIO)
|
|
|
|
#include "OfflineAudioContext.h"
|
|
|
|
#include "AudioBuffer.h"
|
|
#include "AudioUtilities.h"
|
|
#include "Document.h"
|
|
#include "JSAudioBuffer.h"
|
|
#include "OfflineAudioCompletionEvent.h"
|
|
#include "OfflineAudioContextOptions.h"
|
|
#include <wtf/IsoMallocInlines.h>
|
|
#include <wtf/Scope.h>
|
|
|
|
namespace WebCore {
|
|
|
|
WTF_MAKE_ISO_ALLOCATED_IMPL(OfflineAudioContext);
|
|
|
|
OfflineAudioContext::OfflineAudioContext(Document& document, const OfflineAudioContextOptions& options)
|
|
: BaseAudioContext(document)
|
|
, m_destinationNode(makeUniqueRef<OfflineAudioDestinationNode>(*this, options.numberOfChannels, options.sampleRate, AudioBuffer::create(options.numberOfChannels, options.length, options.sampleRate)))
|
|
, m_length(options.length)
|
|
{
|
|
if (!renderTarget())
|
|
document.addConsoleMessage(MessageSource::JS, MessageLevel::Warning, makeString("Failed to construct internal AudioBuffer with ", options.numberOfChannels, " channel(s), a sample rate of ", options.sampleRate, " and a length of ", options.length, "."));
|
|
}
|
|
|
|
ExceptionOr<Ref<OfflineAudioContext>> OfflineAudioContext::create(ScriptExecutionContext& context, const OfflineAudioContextOptions& options)
|
|
{
|
|
if (!is<Document>(context))
|
|
return Exception { NotSupportedError, "OfflineAudioContext is only supported in Document contexts"_s };
|
|
if (!options.numberOfChannels || options.numberOfChannels > maxNumberOfChannels)
|
|
return Exception { NotSupportedError, "Number of channels is not in range"_s };
|
|
if (!options.length)
|
|
return Exception { NotSupportedError, "length cannot be 0"_s };
|
|
if (!isSupportedSampleRate(options.sampleRate))
|
|
return Exception { NotSupportedError, "sampleRate is not in range"_s };
|
|
|
|
auto audioContext = adoptRef(*new OfflineAudioContext(downcast<Document>(context), options));
|
|
audioContext->suspendIfNeeded();
|
|
return audioContext;
|
|
}
|
|
|
|
ExceptionOr<Ref<OfflineAudioContext>> OfflineAudioContext::create(ScriptExecutionContext& context, unsigned numberOfChannels, unsigned length, float sampleRate)
|
|
{
|
|
return create(context, { numberOfChannels, length, sampleRate });
|
|
}
|
|
|
|
void OfflineAudioContext::uninitialize()
|
|
{
|
|
if (!isInitialized())
|
|
return;
|
|
|
|
BaseAudioContext::uninitialize();
|
|
|
|
if (auto promise = std::exchange(m_pendingRenderingPromise, nullptr))
|
|
promise->reject(Exception { InvalidStateError, "Context is going away"_s });
|
|
}
|
|
|
|
const char* OfflineAudioContext::activeDOMObjectName() const
|
|
{
|
|
return "OfflineAudioContext";
|
|
}
|
|
|
|
void OfflineAudioContext::startRendering(Ref<DeferredPromise>&& promise)
|
|
{
|
|
if (isStopped()) {
|
|
promise->reject(Exception { InvalidStateError, "Context is stopped"_s });
|
|
return;
|
|
}
|
|
|
|
if (m_didStartRendering) {
|
|
promise->reject(Exception { InvalidStateError, "Rendering was already started"_s });
|
|
return;
|
|
}
|
|
|
|
if (!renderTarget()) {
|
|
promise->reject(Exception { NotSupportedError, "Failed to create audio buffer"_s });
|
|
return;
|
|
}
|
|
|
|
lazyInitialize();
|
|
|
|
destination().startRendering([this, promise = WTFMove(promise), pendingActivity = makePendingActivity(*this)](std::optional<Exception>&& exception) mutable {
|
|
if (exception) {
|
|
promise->reject(WTFMove(*exception));
|
|
return;
|
|
}
|
|
|
|
m_pendingRenderingPromise = WTFMove(promise);
|
|
m_didStartRendering = true;
|
|
setState(State::Running);
|
|
});
|
|
}
|
|
|
|
void OfflineAudioContext::suspendRendering(double suspendTime, Ref<DeferredPromise>&& promise)
|
|
{
|
|
if (isStopped()) {
|
|
promise->reject(Exception { InvalidStateError, "Context is stopped"_s });
|
|
return;
|
|
}
|
|
|
|
if (suspendTime < 0) {
|
|
promise->reject(Exception { InvalidStateError, "suspendTime cannot be negative"_s });
|
|
return;
|
|
}
|
|
|
|
double totalRenderDuration = length() / sampleRate();
|
|
if (totalRenderDuration <= suspendTime) {
|
|
promise->reject(Exception { InvalidStateError, "suspendTime cannot be greater than total rendering duration"_s });
|
|
return;
|
|
}
|
|
|
|
size_t frame = AudioUtilities::timeToSampleFrame(suspendTime, sampleRate());
|
|
frame = AudioUtilities::renderQuantumSize * ((frame + AudioUtilities::renderQuantumSize - 1) / AudioUtilities::renderQuantumSize);
|
|
if (frame < currentSampleFrame()) {
|
|
promise->reject(Exception { InvalidStateError, "Suspension frame is earlier than current frame"_s });
|
|
return;
|
|
}
|
|
|
|
Locker locker { graphLock() };
|
|
auto addResult = m_suspendRequests.add(frame, promise.ptr());
|
|
if (!addResult.isNewEntry) {
|
|
promise->reject(Exception { InvalidStateError, "There is already a pending suspend request at this frame"_s });
|
|
return;
|
|
}
|
|
}
|
|
|
|
void OfflineAudioContext::resumeRendering(Ref<DeferredPromise>&& promise)
|
|
{
|
|
if (!m_didStartRendering) {
|
|
promise->reject(Exception { InvalidStateError, "Cannot resume an offline audio context that has not started"_s });
|
|
return;
|
|
}
|
|
if (isClosed()) {
|
|
promise->reject(Exception { InvalidStateError, "Cannot resume an offline audio context that is closed"_s });
|
|
return;
|
|
}
|
|
if (state() == AudioContextState::Running) {
|
|
promise->resolve();
|
|
return;
|
|
}
|
|
ASSERT(state() == AudioContextState::Suspended);
|
|
|
|
destination().startRendering([this, promise = WTFMove(promise), pendingActivity = makePendingActivity(*this)](std::optional<Exception>&& exception) mutable {
|
|
if (exception) {
|
|
promise->reject(WTFMove(*exception));
|
|
return;
|
|
}
|
|
|
|
setState(State::Running);
|
|
promise->resolve();
|
|
});
|
|
}
|
|
|
|
bool OfflineAudioContext::shouldSuspend()
|
|
{
|
|
ASSERT(!isMainThread());
|
|
// Note that we are not using a tryLock() here. We usually avoid blocking the AudioThread
|
|
// on lock() but we don't have a choice here since the suspension need to be exact.
|
|
// Also, this not a real-time AudioContext so blocking the AudioThread is not as harmful.
|
|
Locker locker { graphLock() };
|
|
return m_suspendRequests.contains(currentSampleFrame());
|
|
}
|
|
|
|
void OfflineAudioContext::didSuspendRendering(size_t frame)
|
|
{
|
|
setState(State::Suspended);
|
|
|
|
RefPtr<DeferredPromise> promise;
|
|
{
|
|
Locker locker { graphLock() };
|
|
promise = m_suspendRequests.take(frame);
|
|
}
|
|
ASSERT(promise);
|
|
if (promise)
|
|
promise->resolve();
|
|
}
|
|
|
|
void OfflineAudioContext::finishedRendering(bool didRendering)
|
|
{
|
|
ASSERT(isMainThread());
|
|
ALWAYS_LOG(LOGIDENTIFIER);
|
|
|
|
auto uninitializeOnExit = WTF::makeScopeExit([this] {
|
|
uninitialize();
|
|
clear();
|
|
});
|
|
|
|
// Make sure our JSwrapper stays alive long enough to resolve the promise and queue the completion event.
|
|
// Otherwise, setting the state to Closed may cause our JS wrapper to get collected early.
|
|
auto protectedJSWrapper = makePendingActivity(*this);
|
|
setState(State::Closed);
|
|
|
|
// Avoid firing the event if the document has already gone away.
|
|
if (isStopped())
|
|
return;
|
|
|
|
RefPtr<AudioBuffer> renderedBuffer = renderTarget();
|
|
ASSERT(renderedBuffer);
|
|
|
|
if (didRendering) {
|
|
queueTaskToDispatchEvent(*this, TaskSource::MediaElement, OfflineAudioCompletionEvent::create(*renderedBuffer));
|
|
settleRenderingPromise(renderedBuffer.releaseNonNull());
|
|
} else
|
|
settleRenderingPromise(Exception { InvalidStateError, "Offline rendering failed"_s });
|
|
}
|
|
|
|
void OfflineAudioContext::settleRenderingPromise(ExceptionOr<Ref<AudioBuffer>>&& result)
|
|
{
|
|
auto promise = std::exchange(m_pendingRenderingPromise, nullptr);
|
|
if (!promise)
|
|
return;
|
|
|
|
if (result.hasException()) {
|
|
promise->reject(result.releaseException());
|
|
return;
|
|
}
|
|
promise->resolve<IDLInterface<AudioBuffer>>(result.releaseReturnValue());
|
|
}
|
|
|
|
bool OfflineAudioContext::virtualHasPendingActivity() const
|
|
{
|
|
return state() == State::Running;
|
|
}
|
|
|
|
} // namespace WebCore
|
|
|
|
#endif // ENABLE(WEB_AUDIO)
|