blob: 19db10f103d12aa0ef0fe098c43104bca2604e68 [file] [log] [blame]
/*
* Copyright (C) 2012, Google 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 "third_party/blink/renderer/modules/webaudio/offline_audio_context.h"
#include "third_party/blink/public/platform/platform.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/dom/dom_exception.h"
#include "third_party/blink/renderer/core/execution_context/execution_context.h"
#include "third_party/blink/renderer/modules/webaudio/audio_listener.h"
#include "third_party/blink/renderer/modules/webaudio/deferred_task_handler.h"
#include "third_party/blink/renderer/modules/webaudio/offline_audio_completion_event.h"
#include "third_party/blink/renderer/modules/webaudio/offline_audio_context_options.h"
#include "third_party/blink/renderer/modules/webaudio/offline_audio_destination_node.h"
#include "third_party/blink/renderer/platform/audio/audio_utilities.h"
#include "third_party/blink/renderer/platform/bindings/exception_messages.h"
#include "third_party/blink/renderer/platform/bindings/exception_state.h"
#include "third_party/blink/renderer/platform/bindings/script_state.h"
#include "third_party/blink/renderer/platform/cross_thread_functional.h"
#include "third_party/blink/renderer/platform/histogram.h"
namespace blink {
OfflineAudioContext* OfflineAudioContext::Create(
ExecutionContext* context,
unsigned number_of_channels,
unsigned number_of_frames,
float sample_rate,
ExceptionState& exception_state) {
// FIXME: add support for workers.
auto* document = DynamicTo<Document>(context);
if (!document) {
exception_state.ThrowDOMException(DOMExceptionCode::kNotSupportedError,
"Workers are not supported.");
return nullptr;
}
if (!number_of_frames) {
exception_state.ThrowDOMException(
DOMExceptionCode::kNotSupportedError,
ExceptionMessages::IndexExceedsMinimumBound<unsigned>(
"number of frames", number_of_frames, 1));
return nullptr;
}
if (number_of_channels == 0 ||
number_of_channels > BaseAudioContext::MaxNumberOfChannels()) {
exception_state.ThrowDOMException(
DOMExceptionCode::kNotSupportedError,
ExceptionMessages::IndexOutsideRange<unsigned>(
"number of channels", number_of_channels, 1,
ExceptionMessages::kInclusiveBound,
BaseAudioContext::MaxNumberOfChannels(),
ExceptionMessages::kInclusiveBound));
return nullptr;
}
if (!audio_utilities::IsValidAudioBufferSampleRate(sample_rate)) {
exception_state.ThrowDOMException(
DOMExceptionCode::kNotSupportedError,
ExceptionMessages::IndexOutsideRange(
"sampleRate", sample_rate,
audio_utilities::MinAudioBufferSampleRate(),
ExceptionMessages::kInclusiveBound,
audio_utilities::MaxAudioBufferSampleRate(),
ExceptionMessages::kInclusiveBound));
return nullptr;
}
OfflineAudioContext* audio_context =
MakeGarbageCollected<OfflineAudioContext>(document, number_of_channels,
number_of_frames, sample_rate,
exception_state);
audio_context->UpdateStateIfNeeded();
#if DEBUG_AUDIONODE_REFERENCES
fprintf(stderr, "[%16p]: OfflineAudioContext::OfflineAudioContext()\n",
audio_context);
#endif
DEFINE_STATIC_LOCAL(SparseHistogram, offline_context_channel_count_histogram,
("WebAudio.OfflineAudioContext.ChannelCount"));
// Arbitrarly limit the maximum length to 1 million frames (about 20 sec
// at 48kHz). The number of buckets is fairly arbitrary.
DEFINE_STATIC_LOCAL(CustomCountHistogram, offline_context_length_histogram,
("WebAudio.OfflineAudioContext.Length", 1, 1000000, 50));
// The limits are the min and max AudioBuffer sample rates currently
// supported. We use explicit values here instead of
// audio_utilities::minAudioBufferSampleRate() and
// audio_utilities::maxAudioBufferSampleRate(). The number of buckets is
// fairly arbitrary.
DEFINE_STATIC_LOCAL(
CustomCountHistogram, offline_context_sample_rate_histogram,
("WebAudio.OfflineAudioContext.SampleRate384kHz", 3000, 384000, 50));
offline_context_channel_count_histogram.Sample(number_of_channels);
offline_context_length_histogram.Count(number_of_frames);
offline_context_sample_rate_histogram.Count(sample_rate);
return audio_context;
}
OfflineAudioContext* OfflineAudioContext::Create(
ExecutionContext* context,
const OfflineAudioContextOptions* options,
ExceptionState& exception_state) {
OfflineAudioContext* offline_context =
Create(context, options->numberOfChannels(), options->length(),
options->sampleRate(), exception_state);
return offline_context;
}
OfflineAudioContext::OfflineAudioContext(Document* document,
unsigned number_of_channels,
uint32_t number_of_frames,
float sample_rate,
ExceptionState& exception_state)
: BaseAudioContext(document, kOfflineContext),
is_rendering_started_(false),
total_render_frames_(number_of_frames) {
destination_node_ = OfflineAudioDestinationNode::Create(
this, number_of_channels, number_of_frames, sample_rate);
Initialize();
}
OfflineAudioContext::~OfflineAudioContext() {
#if DEBUG_AUDIONODE_REFERENCES
fprintf(stderr, "[%16p]: OfflineAudioContext::~OfflineAudioContext()\n",
this);
#endif
}
void OfflineAudioContext::Trace(blink::Visitor* visitor) {
visitor->Trace(complete_resolver_);
visitor->Trace(scheduled_suspends_);
BaseAudioContext::Trace(visitor);
}
ScriptPromise OfflineAudioContext::startOfflineRendering(
ScriptState* script_state) {
DCHECK(IsMainThread());
// Calling close() on an OfflineAudioContext is not supported/allowed,
// but it might well have been stopped by its execution context.
//
// See: crbug.com/435867
if (IsContextClosed()) {
return ScriptPromise::RejectWithDOMException(
script_state,
DOMException::Create(DOMExceptionCode::kInvalidStateError,
"cannot call startRendering on an "
"OfflineAudioContext in a stopped state."));
}
// If the context is not in the suspended state (i.e. running), reject the
// promise.
if (ContextState() != AudioContextState::kSuspended) {
return ScriptPromise::RejectWithDOMException(
script_state,
DOMException::Create(
DOMExceptionCode::kInvalidStateError,
"cannot startRendering when an OfflineAudioContext is " + state()));
}
// Can't call startRendering more than once. Return a rejected promise now.
if (is_rendering_started_) {
return ScriptPromise::RejectWithDOMException(
script_state,
DOMException::Create(DOMExceptionCode::kInvalidStateError,
"cannot call startRendering more than once"));
}
DCHECK(!is_rendering_started_);
complete_resolver_ = ScriptPromiseResolver::Create(script_state);
// Allocate the AudioBuffer to hold the rendered result.
float sample_rate = DestinationHandler().SampleRate();
unsigned number_of_channels = DestinationHandler().NumberOfChannels();
AudioBuffer* render_target = AudioBuffer::CreateUninitialized(
number_of_channels, total_render_frames_, sample_rate);
if (!render_target) {
return ScriptPromise::RejectWithDOMException(
script_state,
DOMException::Create(DOMExceptionCode::kNotSupportedError,
"startRendering failed to create AudioBuffer(" +
String::Number(number_of_channels) + ", " +
String::Number(total_render_frames_) + ", " +
String::Number(sample_rate) + ")"));
}
// Start rendering and return the promise.
is_rendering_started_ = true;
SetContextState(kRunning);
DestinationHandler().InitializeOfflineRenderThread(render_target);
DestinationHandler().StartRendering();
return complete_resolver_->Promise();
}
ScriptPromise OfflineAudioContext::suspendContext(ScriptState* script_state,
double when) {
DCHECK(IsMainThread());
ScriptPromiseResolver* resolver = ScriptPromiseResolver::Create(script_state);
ScriptPromise promise = resolver->Promise();
// If the rendering is finished, reject the promise.
if (ContextState() == AudioContextState::kClosed) {
resolver->Reject(DOMException::Create(DOMExceptionCode::kInvalidStateError,
"the rendering is already finished"));
return promise;
}
// The specified suspend time is negative; reject the promise.
if (when < 0) {
resolver->Reject(DOMException::Create(
DOMExceptionCode::kInvalidStateError,
"negative suspend time (" + String::Number(when) + ") is not allowed"));
return promise;
}
// The suspend time should be earlier than the total render frame. If the
// requested suspension time is equal to the total render frame, the promise
// will be rejected.
double total_render_duration = total_render_frames_ / sampleRate();
if (total_render_duration <= when) {
resolver->Reject(DOMException::Create(
DOMExceptionCode::kInvalidStateError,
"cannot schedule a suspend at " +
String::NumberToStringECMAScript(when) +
" seconds because it is greater than "
"or equal to the total "
"render duration of " +
String::Number(total_render_frames_) + " frames (" +
String::NumberToStringECMAScript(total_render_duration) +
" seconds)"));
return promise;
}
// Find the sample frame and round up to the nearest render quantum
// boundary. This assumes the render quantum is a power of two.
size_t frame = when * sampleRate();
frame = audio_utilities::kRenderQuantumFrames *
((frame + audio_utilities::kRenderQuantumFrames - 1) /
audio_utilities::kRenderQuantumFrames);
// The specified suspend time is in the past; reject the promise.
if (frame < CurrentSampleFrame()) {
size_t current_frame_clamped =
std::min(CurrentSampleFrame(), static_cast<size_t>(length()));
double current_time_clamped =
std::min(currentTime(), length() / static_cast<double>(sampleRate()));
resolver->Reject(DOMException::Create(
DOMExceptionCode::kInvalidStateError,
"suspend(" + String::Number(when) + ") failed to suspend at frame " +
String::Number(frame) + " because it is earlier than the current " +
"frame of " + String::Number(current_frame_clamped) + " (" +
String::Number(current_time_clamped) + " seconds)"));
return promise;
}
// Wait until the suspend map is available for the insertion. Here we should
// use GraphAutoLocker because it locks the graph from the main thread.
GraphAutoLocker locker(this);
// If there is a duplicate suspension at the same quantized frame,
// reject the promise.
if (scheduled_suspends_.Contains(frame)) {
resolver->Reject(DOMException::Create(
DOMExceptionCode::kInvalidStateError,
"cannot schedule more than one suspend at frame " +
String::Number(frame) + " (" + String::Number(when) + " seconds)"));
return promise;
}
scheduled_suspends_.insert(frame, resolver);
return promise;
}
ScriptPromise OfflineAudioContext::resumeContext(ScriptState* script_state) {
DCHECK(IsMainThread());
ScriptPromiseResolver* resolver = ScriptPromiseResolver::Create(script_state);
ScriptPromise promise = resolver->Promise();
// If the rendering has not started, reject the promise.
if (!is_rendering_started_) {
resolver->Reject(DOMException::Create(
DOMExceptionCode::kInvalidStateError,
"cannot resume an offline context that has not started"));
return promise;
}
// If the context is in a closed state, reject the promise.
if (ContextState() == AudioContextState::kClosed) {
resolver->Reject(
DOMException::Create(DOMExceptionCode::kInvalidStateError,
"cannot resume a closed offline context"));
return promise;
}
// If the context is already running, resolve the promise without altering
// the current state or starting the rendering loop.
if (ContextState() == AudioContextState::kRunning) {
resolver->Resolve();
return promise;
}
DCHECK_EQ(ContextState(), AudioContextState::kSuspended);
// If the context is suspended, resume rendering by setting the state to
// "Running". and calling startRendering(). Note that resuming is possible
// only after the rendering started.
SetContextState(kRunning);
DestinationHandler().StartRendering();
// Resolve the promise immediately.
resolver->Resolve();
return promise;
}
void OfflineAudioContext::FireCompletionEvent() {
DCHECK(IsMainThread());
// Context is finished, so remove any tail processing nodes; there's nowhere
// for the output to go.
GetDeferredTaskHandler().FinishTailProcessing();
// We set the state to closed here so that the oncomplete event handler sees
// that the context has been closed.
SetContextState(kClosed);
// Avoid firing the event if the document has already gone away.
if (GetExecutionContext()) {
AudioBuffer* rendered_buffer = DestinationHandler().RenderTarget();
DCHECK(rendered_buffer);
if (!rendered_buffer)
return;
// Call the offline rendering completion event listener and resolve the
// promise too.
DispatchEvent(*OfflineAudioCompletionEvent::Create(rendered_buffer));
complete_resolver_->Resolve(rendered_buffer);
} else {
// The resolver should be rejected when the execution context is gone.
complete_resolver_->Reject(
DOMException::Create(DOMExceptionCode::kInvalidStateError,
"the execution context does not exist"));
}
is_rendering_started_ = false;
PerformCleanupOnMainThread();
}
bool OfflineAudioContext::HandlePreOfflineRenderTasks() {
DCHECK(IsAudioThread());
// OfflineGraphAutoLocker here locks the audio graph for this scope. Note
// that this locker does not use tryLock() inside because the timing of
// suspension MUST NOT be delayed.
OfflineGraphAutoLocker locker(this);
// Update the dirty state of the listener.
listener()->UpdateState();
GetDeferredTaskHandler().HandleDeferredTasks();
HandleStoppableSourceNodes();
return ShouldSuspend();
}
void OfflineAudioContext::HandlePostOfflineRenderTasks() {
DCHECK(IsAudioThread());
// OfflineGraphAutoLocker here locks the audio graph for the same reason
// above in |handlePreOfflineRenderTasks|.
{
OfflineGraphAutoLocker locker(this);
GetDeferredTaskHandler().BreakConnections();
GetDeferredTaskHandler().HandleDeferredTasks();
GetDeferredTaskHandler().RequestToDeleteHandlersOnMainThread();
}
}
OfflineAudioDestinationHandler& OfflineAudioContext::DestinationHandler() {
return static_cast<OfflineAudioDestinationHandler&>(
destination()->GetAudioDestinationHandler());
}
void OfflineAudioContext::ResolveSuspendOnMainThread(size_t frame) {
DCHECK(IsMainThread());
// Suspend the context first. This will fire onstatechange event.
SetContextState(kSuspended);
// Wait until the suspend map is available for the removal.
GraphAutoLocker locker(this);
// If the context is going away, m_scheduledSuspends could have had all its
// entries removed. Check for that here.
if (scheduled_suspends_.size()) {
// |frame| must exist in the map.
DCHECK(scheduled_suspends_.Contains(frame));
SuspendMap::iterator it = scheduled_suspends_.find(frame);
it->value->Resolve();
scheduled_suspends_.erase(it);
}
}
void OfflineAudioContext::RejectPendingResolvers() {
DCHECK(IsMainThread());
// Wait until the suspend map is available for removal.
GraphAutoLocker locker(this);
// Offline context is going away so reject any promises that are still
// pending.
for (auto& pending_suspend_resolver : scheduled_suspends_) {
pending_suspend_resolver.value->Reject(DOMException::Create(
DOMExceptionCode::kInvalidStateError, "Audio context is going away"));
}
scheduled_suspends_.clear();
DCHECK_EQ(resume_resolvers_.size(), 0u);
RejectPendingDecodeAudioDataResolvers();
}
bool OfflineAudioContext::ShouldSuspend() {
DCHECK(IsAudioThread());
// Note that the GraphLock is required before this check. Since this needs
// to run on the audio thread, OfflineGraphAutoLocker must be used.
if (scheduled_suspends_.Contains(CurrentSampleFrame()))
return true;
return false;
}
bool OfflineAudioContext::HasPendingActivity() const {
return is_rendering_started_;
}
} // namespace blink