// Copyright 2015 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "modules/webaudio/AudioContext.h" #include "bindings/core/v8/ExceptionMessages.h" #include "bindings/core/v8/ExceptionState.h" #include "bindings/core/v8/ScriptPromiseResolver.h" #include "core/dom/DOMException.h" #include "core/dom/ExceptionCode.h" #include "modules/webaudio/AudioBufferCallback.h" #include "platform/audio/AudioUtilities.h" #if DEBUG_AUDIONODE_REFERENCES #include #endif namespace blink { // Don't allow more than this number of simultaneous AudioContexts // talking to hardware. const unsigned MaxHardwareContexts = 6; static unsigned s_hardwareContextCount = 0; static unsigned s_contextId = 0; AbstractAudioContext* AudioContext::create(Document& document, ExceptionState& exceptionState) { ASSERT(isMainThread()); if (s_hardwareContextCount >= MaxHardwareContexts) { exceptionState.throwDOMException( NotSupportedError, ExceptionMessages::indexExceedsMaximumBound( "number of hardware contexts", s_hardwareContextCount, MaxHardwareContexts)); return nullptr; } AudioContext* audioContext = new AudioContext(document); audioContext->suspendIfNeeded(); if (!AudioUtilities::isValidAudioBufferSampleRate(audioContext->sampleRate())) { exceptionState.throwDOMException( NotSupportedError, ExceptionMessages::indexOutsideRange( "hardware sample rate", audioContext->sampleRate(), AudioUtilities::minAudioBufferSampleRate(), ExceptionMessages::InclusiveBound, AudioUtilities::maxAudioBufferSampleRate(), ExceptionMessages::InclusiveBound)); return audioContext; } // This starts the audio thread. The destination node's // provideInput() method will now be called repeatedly to render // audio. Each time provideInput() is called, a portion of the // audio stream is rendered. Let's call this time period a "render // quantum". NOTE: for now AudioContext does not need an explicit // startRendering() call from JavaScript. We may want to consider // requiring it for symmetry with OfflineAudioContext. audioContext->startRendering(); ++s_hardwareContextCount; #if DEBUG_AUDIONODE_REFERENCES fprintf(stderr, "%p: AudioContext::AudioContext(): %u #%u\n", audioContext, audioContext->m_contextId, s_hardwareContextCount); #endif return audioContext; } AudioContext::AudioContext(Document& document) : AbstractAudioContext(&document) , m_contextId(s_contextId++) { } AudioContext::~AudioContext() { #if DEBUG_AUDIONODE_REFERENCES fprintf(stderr, "%p: AudioContext::~AudioContext(): %u\n", this, m_contextId); #endif } DEFINE_TRACE(AudioContext) { visitor->trace(m_closeResolver); AbstractAudioContext::trace(visitor); } ScriptPromise AudioContext::suspendContext(ScriptState* scriptState) { ASSERT(isMainThread()); AutoLocker locker(this); ScriptPromiseResolver* resolver = ScriptPromiseResolver::create(scriptState); ScriptPromise promise = resolver->promise(); if (contextState() == Closed) { resolver->reject( DOMException::create(InvalidStateError, "Cannot suspend a context that has been closed")); } else { // Stop rendering now. if (destination()) stopRendering(); // Since we don't have any way of knowing when the hardware actually stops, we'll just // resolve the promise now. resolver->resolve(); } return promise; } ScriptPromise AudioContext::resumeContext(ScriptState* scriptState) { ASSERT(isMainThread()); if (isContextClosed()) { return ScriptPromise::rejectWithDOMException( scriptState, DOMException::create( InvalidAccessError, "cannot resume a closed AudioContext")); } ScriptPromiseResolver* resolver = ScriptPromiseResolver::create(scriptState); ScriptPromise promise = resolver->promise(); // Restart the destination node to pull on the audio graph. if (destination()) startRendering(); // Save the resolver which will get resolved when the destination node starts pulling on the // graph again. { AutoLocker locker(this); m_resumeResolvers.append(resolver); } return promise; } ScriptPromise AudioContext::closeContext(ScriptState* scriptState) { if (isContextClosed()) { // We've already closed the context previously, but it hasn't yet been resolved, so just // create a new promise and reject it. return ScriptPromise::rejectWithDOMException( scriptState, DOMException::create(InvalidStateError, "Cannot close a context that is being closed or has already been closed.")); } // Save the current sample rate for any subsequent decodeAudioData calls. setClosedContextSampleRate(sampleRate()); m_closeResolver = ScriptPromiseResolver::create(scriptState); ScriptPromise promise = m_closeResolver->promise(); // Stop the audio context. This will stop the destination node from pulling audio anymore. And // since we have disconnected the destination from the audio graph, and thus has no references, // the destination node can GCed if JS has no references. uninitialize() will also resolve the Promise // created here. uninitialize(); return promise; } void AudioContext::didClose() { // This is specific to AudioContexts. OfflineAudioContexts // are closed in their completion event. setContextState(Closed); ASSERT(s_hardwareContextCount); --s_hardwareContextCount; if (m_closeResolver) m_closeResolver->resolve(); } bool AudioContext::isContextClosed() const { return m_closeResolver || AbstractAudioContext::isContextClosed(); } void AudioContext::stopRendering() { ASSERT(isMainThread()); ASSERT(destination()); if (contextState() == Running) { destination()->audioDestinationHandler().stopRendering(); setContextState(Suspended); deferredTaskHandler().clearHandlersToBeDeleted(); } } } // namespace blink