| /* |
| * Copyright (C) 2010 Google Inc. All rights reserved. |
| * Copyright (C) 2016 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 "AudioContext.h" |
| |
| #include "AnalyserNode.h" |
| #include "AsyncAudioDecoder.h" |
| #include "AudioBuffer.h" |
| #include "AudioBufferCallback.h" |
| #include "AudioBufferSourceNode.h" |
| #include "AudioListener.h" |
| #include "AudioNodeInput.h" |
| #include "AudioNodeOutput.h" |
| #include "AudioSession.h" |
| #include "BiquadFilterNode.h" |
| #include "ChannelMergerNode.h" |
| #include "ChannelSplitterNode.h" |
| #include "ConvolverNode.h" |
| #include "DefaultAudioDestinationNode.h" |
| #include "DelayNode.h" |
| #include "Document.h" |
| #include "DynamicsCompressorNode.h" |
| #include "EventNames.h" |
| #include "FFTFrame.h" |
| #include "Frame.h" |
| #include "FrameLoader.h" |
| #include "GainNode.h" |
| #include "GenericEventQueue.h" |
| #include "HRTFDatabaseLoader.h" |
| #include "HRTFPanner.h" |
| #include "JSDOMPromiseDeferred.h" |
| #include "Logging.h" |
| #include "NetworkingContext.h" |
| #include "OfflineAudioCompletionEvent.h" |
| #include "OfflineAudioDestinationNode.h" |
| #include "OscillatorNode.h" |
| #include "Page.h" |
| #include "PannerNode.h" |
| #include "PeriodicWave.h" |
| #include "ScriptController.h" |
| #include "ScriptProcessorNode.h" |
| #include "WaveShaperNode.h" |
| #include <JavaScriptCore/ScriptCallStack.h> |
| |
| #if ENABLE(MEDIA_STREAM) |
| #include "MediaStream.h" |
| #include "MediaStreamAudioDestinationNode.h" |
| #include "MediaStreamAudioSource.h" |
| #include "MediaStreamAudioSourceNode.h" |
| #endif |
| |
| #if ENABLE(VIDEO) |
| #include "HTMLMediaElement.h" |
| #include "MediaElementAudioSourceNode.h" |
| #endif |
| |
| #if DEBUG_AUDIONODE_REFERENCES |
| #include <stdio.h> |
| #endif |
| |
| #if USE(GSTREAMER) |
| #include "GStreamerCommon.h" |
| #endif |
| |
| #if PLATFORM(IOS_FAMILY) |
| #include "ScriptController.h" |
| #include "Settings.h" |
| #endif |
| |
| #include <JavaScriptCore/ArrayBuffer.h> |
| #include <wtf/Atomics.h> |
| #include <wtf/IsoMallocInlines.h> |
| #include <wtf/MainThread.h> |
| #include <wtf/Ref.h> |
| #include <wtf/RefCounted.h> |
| #include <wtf/Scope.h> |
| #include <wtf/text/WTFString.h> |
| |
| const unsigned MaxPeriodicWaveLength = 4096; |
| |
| namespace WebCore { |
| |
| WTF_MAKE_ISO_ALLOCATED_IMPL(AudioContext); |
| |
| #define RELEASE_LOG_IF_ALLOWED(fmt, ...) RELEASE_LOG_IF(document() && document()->page() && document()->page()->isAlwaysOnLoggingAllowed(), Media, "%p - AudioContext::" fmt, this, ##__VA_ARGS__) |
| |
| bool AudioContext::isSampleRateRangeGood(float sampleRate) |
| { |
| // FIXME: It would be nice if the minimum sample-rate could be less than 44.1KHz, |
| // but that will require some fixes in HRTFPanner::fftSizeForSampleRate(), and some testing there. |
| return sampleRate >= 44100 && sampleRate <= 96000; |
| } |
| |
| // Don't allow more than this number of simultaneous AudioContexts talking to hardware. |
| const unsigned MaxHardwareContexts = 4; |
| unsigned AudioContext::s_hardwareContextCount = 0; |
| |
| RefPtr<AudioContext> AudioContext::create(Document& document) |
| { |
| ASSERT(isMainThread()); |
| if (s_hardwareContextCount >= MaxHardwareContexts) |
| return nullptr; |
| |
| RefPtr<AudioContext> audioContext(adoptRef(new AudioContext(document))); |
| audioContext->suspendIfNeeded(); |
| return audioContext; |
| } |
| |
| // Constructor for rendering to the audio hardware. |
| AudioContext::AudioContext(Document& document) |
| : ActiveDOMObject(document) |
| #if !RELEASE_LOG_DISABLED |
| , m_logger(document.logger()) |
| , m_logIdentifier(uniqueLogIdentifier()) |
| #endif |
| , m_mediaSession(PlatformMediaSession::create(*this)) |
| , m_eventQueue(MainThreadGenericEventQueue::create(*this)) |
| { |
| // According to spec AudioContext must die only after page navigate. |
| // Lets mark it as ActiveDOMObject with pending activity and unmark it in clear method. |
| makePendingActivity(); |
| |
| constructCommon(); |
| |
| m_destinationNode = DefaultAudioDestinationNode::create(*this); |
| |
| // Initialize the destination node's muted state to match the page's current muted state. |
| pageMutedStateDidChange(); |
| |
| document.addAudioProducer(*this); |
| document.registerForVisibilityStateChangedCallbacks(*this); |
| } |
| |
| // Constructor for offline (non-realtime) rendering. |
| AudioContext::AudioContext(Document& document, AudioBuffer* renderTarget) |
| : ActiveDOMObject(document) |
| #if !RELEASE_LOG_DISABLED |
| , m_logger(document.logger()) |
| , m_logIdentifier(uniqueLogIdentifier()) |
| #endif |
| , m_isOfflineContext(true) |
| , m_mediaSession(PlatformMediaSession::create(*this)) |
| , m_eventQueue(MainThreadGenericEventQueue::create(*this)) |
| , m_renderTarget(renderTarget) |
| { |
| constructCommon(); |
| |
| // Create a new destination for offline rendering. |
| m_destinationNode = OfflineAudioDestinationNode::create(*this, m_renderTarget.get()); |
| } |
| |
| void AudioContext::constructCommon() |
| { |
| FFTFrame::initialize(); |
| |
| m_listener = AudioListener::create(); |
| |
| ASSERT(document()); |
| if (document()->audioPlaybackRequiresUserGesture()) |
| addBehaviorRestriction(RequireUserGestureForAudioStartRestriction); |
| else |
| m_restrictions = NoRestrictions; |
| |
| #if PLATFORM(COCOA) |
| addBehaviorRestriction(RequirePageConsentForAudioStartRestriction); |
| #endif |
| } |
| |
| AudioContext::~AudioContext() |
| { |
| #if DEBUG_AUDIONODE_REFERENCES |
| fprintf(stderr, "%p: AudioContext::~AudioContext()\n", this); |
| #endif |
| ASSERT(!m_isInitialized); |
| ASSERT(m_isStopScheduled); |
| ASSERT(m_nodesToDelete.isEmpty()); |
| ASSERT(m_referencedNodes.isEmpty()); |
| ASSERT(m_finishedNodes.isEmpty()); // FIXME (bug 105870): This assertion fails on tests sometimes. |
| ASSERT(m_automaticPullNodes.isEmpty()); |
| if (m_automaticPullNodesNeedUpdating) |
| m_renderingAutomaticPullNodes.resize(m_automaticPullNodes.size()); |
| ASSERT(m_renderingAutomaticPullNodes.isEmpty()); |
| // FIXME: Can we assert that m_deferredFinishDerefList is empty? |
| |
| if (!isOfflineContext() && scriptExecutionContext()) { |
| document()->removeAudioProducer(*this); |
| document()->unregisterForVisibilityStateChangedCallbacks(*this); |
| } |
| } |
| |
| void AudioContext::lazyInitialize() |
| { |
| ASSERT(!m_isStopScheduled); |
| |
| if (m_isInitialized) |
| return; |
| |
| // Don't allow the context to initialize a second time after it's already been explicitly uninitialized. |
| ASSERT(!m_isAudioThreadFinished); |
| if (m_isAudioThreadFinished) |
| return; |
| |
| if (m_destinationNode) { |
| m_destinationNode->initialize(); |
| |
| if (!isOfflineContext()) { |
| // 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 default AudioContext does not need an explicit startRendering() call from JavaScript. |
| // We may want to consider requiring it for symmetry with OfflineAudioContext. |
| startRendering(); |
| ++s_hardwareContextCount; |
| } |
| } |
| m_isInitialized = true; |
| } |
| |
| void AudioContext::clear() |
| { |
| Ref<AudioContext> protectedThis(*this); |
| |
| // We have to release our reference to the destination node before the context will ever be deleted since the destination node holds a reference to the context. |
| if (m_destinationNode) |
| m_destinationNode = nullptr; |
| |
| // Audio thread is dead. Nobody will schedule node deletion action. Let's do it ourselves. |
| do { |
| deleteMarkedNodes(); |
| m_nodesToDelete.appendVector(m_nodesMarkedForDeletion); |
| m_nodesMarkedForDeletion.clear(); |
| } while (m_nodesToDelete.size()); |
| |
| clearPendingActivity(); |
| } |
| |
| void AudioContext::uninitialize() |
| { |
| ALWAYS_LOG(LOGIDENTIFIER); |
| |
| ASSERT(isMainThread()); |
| |
| if (!m_isInitialized) |
| return; |
| |
| // This stops the audio thread and all audio rendering. |
| if (m_destinationNode) |
| m_destinationNode->uninitialize(); |
| |
| // Don't allow the context to initialize a second time after it's already been explicitly uninitialized. |
| m_isAudioThreadFinished = true; |
| |
| if (!isOfflineContext()) { |
| ASSERT(s_hardwareContextCount); |
| --s_hardwareContextCount; |
| |
| // Offline contexts move to 'Closed' state when dispatching the completion event. |
| setState(State::Closed); |
| } |
| |
| // Get rid of the sources which may still be playing. |
| derefUnfinishedSourceNodes(); |
| |
| m_isInitialized = false; |
| } |
| |
| bool AudioContext::isInitialized() const |
| { |
| return m_isInitialized; |
| } |
| |
| void AudioContext::addReaction(State state, DOMPromiseDeferred<void>&& promise) |
| { |
| size_t stateIndex = static_cast<size_t>(state); |
| if (stateIndex >= m_stateReactions.size()) |
| m_stateReactions.grow(stateIndex + 1); |
| |
| m_stateReactions[stateIndex].append(WTFMove(promise)); |
| } |
| |
| void AudioContext::setState(State state) |
| { |
| if (m_state == state) |
| return; |
| |
| m_state = state; |
| m_eventQueue->enqueueEvent(Event::create(eventNames().statechangeEvent, Event::CanBubble::Yes, Event::IsCancelable::No)); |
| |
| size_t stateIndex = static_cast<size_t>(state); |
| if (stateIndex >= m_stateReactions.size()) |
| return; |
| |
| Vector<DOMPromiseDeferred<void>> reactions; |
| m_stateReactions[stateIndex].swap(reactions); |
| |
| for (auto& promise : reactions) |
| promise.resolve(); |
| } |
| |
| void AudioContext::stop() |
| { |
| ALWAYS_LOG(LOGIDENTIFIER); |
| |
| ASSERT(isMainThread()); |
| |
| // Usually ScriptExecutionContext calls stop twice. |
| if (m_isStopScheduled) |
| return; |
| m_isStopScheduled = true; |
| |
| ASSERT(document()); |
| document()->updateIsPlayingMedia(); |
| |
| uninitialize(); |
| clear(); |
| } |
| |
| bool AudioContext::shouldPreventEnteringBackForwardCache_DEPRECATED() const |
| { |
| // FIXME: We should be able to suspend while rendering as well with some more code. |
| return m_state != State::Suspended && m_state != State::Closed; |
| } |
| |
| const char* AudioContext::activeDOMObjectName() const |
| { |
| return "AudioContext"; |
| } |
| |
| Document* AudioContext::document() const |
| { |
| return downcast<Document>(m_scriptExecutionContext); |
| } |
| |
| Document* AudioContext::hostingDocument() const |
| { |
| return downcast<Document>(m_scriptExecutionContext); |
| } |
| |
| String AudioContext::sourceApplicationIdentifier() const |
| { |
| Document* document = this->document(); |
| if (Frame* frame = document ? document->frame() : nullptr) { |
| if (NetworkingContext* networkingContext = frame->loader().networkingContext()) |
| return networkingContext->sourceApplicationIdentifier(); |
| } |
| return emptyString(); |
| } |
| |
| bool AudioContext::processingUserGestureForMedia() const |
| { |
| return document() ? document()->processingUserGestureForMedia() : false; |
| } |
| |
| bool AudioContext::isSuspended() const |
| { |
| return !document() || document()->activeDOMObjectsAreSuspended() || document()->activeDOMObjectsAreStopped(); |
| } |
| |
| void AudioContext::visibilityStateChanged() |
| { |
| // Do not suspend if audio is audible. |
| if (!document() || mediaState() == MediaProducer::IsPlayingAudio || m_isStopScheduled) |
| return; |
| |
| if (document()->hidden()) { |
| if (state() == State::Running) { |
| RELEASE_LOG_IF_ALLOWED("visibilityStateChanged() Suspending playback after going to the background"); |
| m_mediaSession->beginInterruption(PlatformMediaSession::EnteringBackground); |
| } |
| } else { |
| if (state() == State::Interrupted) { |
| RELEASE_LOG_IF_ALLOWED("visibilityStateChanged() Resuming playback after entering foreground"); |
| m_mediaSession->endInterruption(PlatformMediaSession::MayResumePlaying); |
| } |
| } |
| } |
| |
| bool AudioContext::wouldTaintOrigin(const URL& url) const |
| { |
| if (url.protocolIsData()) |
| return false; |
| |
| if (auto* document = this->document()) |
| return !document->securityOrigin().canRequest(url); |
| |
| return false; |
| } |
| |
| ExceptionOr<Ref<AudioBuffer>> AudioContext::createBuffer(unsigned numberOfChannels, size_t numberOfFrames, float sampleRate) |
| { |
| auto audioBuffer = AudioBuffer::create(numberOfChannels, numberOfFrames, sampleRate); |
| if (!audioBuffer) |
| return Exception { NotSupportedError }; |
| return audioBuffer.releaseNonNull(); |
| } |
| |
| ExceptionOr<Ref<AudioBuffer>> AudioContext::createBuffer(ArrayBuffer& arrayBuffer, bool mixToMono) |
| { |
| auto audioBuffer = AudioBuffer::createFromAudioFileData(arrayBuffer.data(), arrayBuffer.byteLength(), mixToMono, sampleRate()); |
| if (!audioBuffer) |
| return Exception { SyntaxError }; |
| return audioBuffer.releaseNonNull(); |
| } |
| |
| void AudioContext::decodeAudioData(Ref<ArrayBuffer>&& audioData, RefPtr<AudioBufferCallback>&& successCallback, RefPtr<AudioBufferCallback>&& errorCallback) |
| { |
| if (!m_audioDecoder) |
| m_audioDecoder = makeUnique<AsyncAudioDecoder>(); |
| m_audioDecoder->decodeAsync(WTFMove(audioData), sampleRate(), WTFMove(successCallback), WTFMove(errorCallback)); |
| } |
| |
| ExceptionOr<Ref<AudioBufferSourceNode>> AudioContext::createBufferSource() |
| { |
| ALWAYS_LOG(LOGIDENTIFIER); |
| |
| ASSERT(isMainThread()); |
| |
| if (m_isStopScheduled) |
| return Exception { InvalidStateError }; |
| |
| lazyInitialize(); |
| Ref<AudioBufferSourceNode> node = AudioBufferSourceNode::create(*this, sampleRate()); |
| |
| // Because this is an AudioScheduledSourceNode, the context keeps a reference until it has finished playing. |
| // When this happens, AudioScheduledSourceNode::finish() calls AudioContext::notifyNodeFinishedProcessing(). |
| refNode(node); |
| |
| return node; |
| } |
| |
| #if ENABLE(VIDEO) |
| |
| ExceptionOr<Ref<MediaElementAudioSourceNode>> AudioContext::createMediaElementSource(HTMLMediaElement& mediaElement) |
| { |
| ALWAYS_LOG(LOGIDENTIFIER); |
| |
| ASSERT(isMainThread()); |
| |
| if (m_isStopScheduled || mediaElement.audioSourceNode()) |
| return Exception { InvalidStateError }; |
| |
| lazyInitialize(); |
| |
| auto node = MediaElementAudioSourceNode::create(*this, mediaElement); |
| |
| mediaElement.setAudioSourceNode(node.ptr()); |
| |
| refNode(node.get()); // context keeps reference until node is disconnected |
| return node; |
| } |
| |
| #endif |
| |
| #if ENABLE(MEDIA_STREAM) |
| |
| ExceptionOr<Ref<MediaStreamAudioSourceNode>> AudioContext::createMediaStreamSource(MediaStream& mediaStream) |
| { |
| ALWAYS_LOG(LOGIDENTIFIER); |
| |
| ASSERT(isMainThread()); |
| |
| if (m_isStopScheduled) |
| return Exception { InvalidStateError }; |
| |
| auto audioTracks = mediaStream.getAudioTracks(); |
| if (audioTracks.isEmpty()) |
| return Exception { InvalidStateError }; |
| |
| MediaStreamTrack* providerTrack = nullptr; |
| for (auto& track : audioTracks) { |
| if (track->audioSourceProvider()) { |
| providerTrack = track.get(); |
| break; |
| } |
| } |
| if (!providerTrack) |
| return Exception { InvalidStateError }; |
| |
| lazyInitialize(); |
| |
| auto node = MediaStreamAudioSourceNode::create(*this, mediaStream, *providerTrack); |
| node->setFormat(2, sampleRate()); |
| |
| refNode(node); // context keeps reference until node is disconnected |
| return node; |
| } |
| |
| ExceptionOr<Ref<MediaStreamAudioDestinationNode>> AudioContext::createMediaStreamDestination() |
| { |
| if (m_isStopScheduled) |
| return Exception { InvalidStateError }; |
| |
| // FIXME: Add support for an optional argument which specifies the number of channels. |
| // FIXME: The default should probably be stereo instead of mono. |
| return MediaStreamAudioDestinationNode::create(*this, 1); |
| } |
| |
| #endif |
| |
| ExceptionOr<Ref<ScriptProcessorNode>> AudioContext::createScriptProcessor(size_t bufferSize, size_t numberOfInputChannels, size_t numberOfOutputChannels) |
| { |
| ALWAYS_LOG(LOGIDENTIFIER); |
| |
| ASSERT(isMainThread()); |
| |
| if (m_isStopScheduled) |
| return Exception { InvalidStateError }; |
| |
| lazyInitialize(); |
| |
| // W3C Editor's Draft 06 June 2017 |
| // https://webaudio.github.io/web-audio-api/#widl-BaseAudioContext-createScriptProcessor-ScriptProcessorNode-unsigned-long-bufferSize-unsigned-long-numberOfInputChannels-unsigned-long-numberOfOutputChannels |
| |
| // The bufferSize parameter determines the buffer size in units of sample-frames. If it's not passed in, |
| // or if the value is 0, then the implementation will choose the best buffer size for the given environment, |
| // which will be constant power of 2 throughout the lifetime of the node. ... If the value of this parameter |
| // is not one of the allowed power-of-2 values listed above, an IndexSizeError must be thrown. |
| switch (bufferSize) { |
| case 0: |
| #if USE(AUDIO_SESSION) |
| // Pick a value between 256 (2^8) and 16384 (2^14), based on the buffer size of the current AudioSession: |
| bufferSize = 1 << std::max<size_t>(8, std::min<size_t>(14, std::log2(AudioSession::sharedSession().bufferSize()))); |
| #else |
| bufferSize = 2048; |
| #endif |
| break; |
| case 256: |
| case 512: |
| case 1024: |
| case 2048: |
| case 4096: |
| case 8192: |
| case 16384: |
| break; |
| default: |
| return Exception { IndexSizeError }; |
| } |
| |
| // An IndexSizeError exception must be thrown if bufferSize or numberOfInputChannels or numberOfOutputChannels |
| // are outside the valid range. It is invalid for both numberOfInputChannels and numberOfOutputChannels to be zero. |
| // In this case an IndexSizeError must be thrown. |
| |
| if (!numberOfInputChannels && !numberOfOutputChannels) |
| return Exception { NotSupportedError }; |
| |
| // This parameter [numberOfInputChannels] determines the number of channels for this node's input. Values of |
| // up to 32 must be supported. A NotSupportedError must be thrown if the number of channels is not supported. |
| |
| if (numberOfInputChannels > maxNumberOfChannels()) |
| return Exception { NotSupportedError }; |
| |
| // This parameter [numberOfOutputChannels] determines the number of channels for this node's output. Values of |
| // up to 32 must be supported. A NotSupportedError must be thrown if the number of channels is not supported. |
| |
| if (numberOfOutputChannels > maxNumberOfChannels()) |
| return Exception { NotSupportedError }; |
| |
| auto node = ScriptProcessorNode::create(*this, sampleRate(), bufferSize, numberOfInputChannels, numberOfOutputChannels); |
| |
| refNode(node); // context keeps reference until we stop making javascript rendering callbacks |
| return node; |
| } |
| |
| ExceptionOr<Ref<BiquadFilterNode>> AudioContext::createBiquadFilter() |
| { |
| ALWAYS_LOG(LOGIDENTIFIER); |
| |
| ASSERT(isMainThread()); |
| if (m_isStopScheduled) |
| return Exception { InvalidStateError }; |
| |
| lazyInitialize(); |
| |
| return BiquadFilterNode::create(*this, sampleRate()); |
| } |
| |
| ExceptionOr<Ref<WaveShaperNode>> AudioContext::createWaveShaper() |
| { |
| ALWAYS_LOG(LOGIDENTIFIER); |
| |
| ASSERT(isMainThread()); |
| if (m_isStopScheduled) |
| return Exception { InvalidStateError }; |
| |
| lazyInitialize(); |
| return WaveShaperNode::create(*this); |
| } |
| |
| ExceptionOr<Ref<PannerNode>> AudioContext::createPanner() |
| { |
| ALWAYS_LOG(LOGIDENTIFIER); |
| |
| ASSERT(isMainThread()); |
| if (m_isStopScheduled) |
| return Exception { InvalidStateError }; |
| |
| lazyInitialize(); |
| return PannerNode::create(*this, sampleRate()); |
| } |
| |
| ExceptionOr<Ref<ConvolverNode>> AudioContext::createConvolver() |
| { |
| ALWAYS_LOG(LOGIDENTIFIER); |
| |
| ASSERT(isMainThread()); |
| if (m_isStopScheduled) |
| return Exception { InvalidStateError }; |
| |
| lazyInitialize(); |
| return ConvolverNode::create(*this, sampleRate()); |
| } |
| |
| ExceptionOr<Ref<DynamicsCompressorNode>> AudioContext::createDynamicsCompressor() |
| { |
| ALWAYS_LOG(LOGIDENTIFIER); |
| |
| ASSERT(isMainThread()); |
| if (m_isStopScheduled) |
| return Exception { InvalidStateError }; |
| |
| lazyInitialize(); |
| return DynamicsCompressorNode::create(*this, sampleRate()); |
| } |
| |
| ExceptionOr<Ref<AnalyserNode>> AudioContext::createAnalyser() |
| { |
| ALWAYS_LOG(LOGIDENTIFIER); |
| |
| ASSERT(isMainThread()); |
| if (m_isStopScheduled) |
| return Exception { InvalidStateError }; |
| |
| lazyInitialize(); |
| return AnalyserNode::create(*this, sampleRate()); |
| } |
| |
| ExceptionOr<Ref<GainNode>> AudioContext::createGain() |
| { |
| ALWAYS_LOG(LOGIDENTIFIER); |
| |
| ASSERT(isMainThread()); |
| if (m_isStopScheduled) |
| return Exception { InvalidStateError }; |
| |
| lazyInitialize(); |
| return GainNode::create(*this, sampleRate()); |
| } |
| |
| ExceptionOr<Ref<DelayNode>> AudioContext::createDelay(double maxDelayTime) |
| { |
| ALWAYS_LOG(LOGIDENTIFIER); |
| |
| ASSERT(isMainThread()); |
| if (m_isStopScheduled) |
| return Exception { InvalidStateError }; |
| |
| lazyInitialize(); |
| return DelayNode::create(*this, sampleRate(), maxDelayTime); |
| } |
| |
| ExceptionOr<Ref<ChannelSplitterNode>> AudioContext::createChannelSplitter(size_t numberOfOutputs) |
| { |
| ALWAYS_LOG(LOGIDENTIFIER); |
| |
| ASSERT(isMainThread()); |
| if (m_isStopScheduled) |
| return Exception { InvalidStateError }; |
| |
| lazyInitialize(); |
| auto node = ChannelSplitterNode::create(*this, sampleRate(), numberOfOutputs); |
| if (!node) |
| return Exception { IndexSizeError }; |
| return node.releaseNonNull(); |
| } |
| |
| ExceptionOr<Ref<ChannelMergerNode>> AudioContext::createChannelMerger(size_t numberOfInputs) |
| { |
| ALWAYS_LOG(LOGIDENTIFIER); |
| |
| ASSERT(isMainThread()); |
| if (m_isStopScheduled) |
| return Exception { InvalidStateError }; |
| |
| lazyInitialize(); |
| auto node = ChannelMergerNode::create(*this, sampleRate(), numberOfInputs); |
| if (!node) |
| return Exception { IndexSizeError }; |
| return node.releaseNonNull(); |
| } |
| |
| ExceptionOr<Ref<OscillatorNode>> AudioContext::createOscillator() |
| { |
| ALWAYS_LOG(LOGIDENTIFIER); |
| |
| ASSERT(isMainThread()); |
| if (m_isStopScheduled) |
| return Exception { InvalidStateError }; |
| |
| lazyInitialize(); |
| |
| Ref<OscillatorNode> node = OscillatorNode::create(*this, sampleRate()); |
| |
| // Because this is an AudioScheduledSourceNode, the context keeps a reference until it has finished playing. |
| // When this happens, AudioScheduledSourceNode::finish() calls AudioContext::notifyNodeFinishedProcessing(). |
| refNode(node); |
| |
| return node; |
| } |
| |
| ExceptionOr<Ref<PeriodicWave>> AudioContext::createPeriodicWave(Float32Array& real, Float32Array& imaginary) |
| { |
| ALWAYS_LOG(LOGIDENTIFIER); |
| |
| ASSERT(isMainThread()); |
| if (m_isStopScheduled) |
| return Exception { InvalidStateError }; |
| |
| if (real.length() != imaginary.length() || (real.length() > MaxPeriodicWaveLength) || !real.length()) |
| return Exception { IndexSizeError }; |
| lazyInitialize(); |
| return PeriodicWave::create(sampleRate(), real, imaginary); |
| } |
| |
| void AudioContext::notifyNodeFinishedProcessing(AudioNode* node) |
| { |
| ASSERT(isAudioThread()); |
| m_finishedNodes.append(node); |
| } |
| |
| void AudioContext::derefFinishedSourceNodes() |
| { |
| ASSERT(isGraphOwner()); |
| ASSERT(isAudioThread() || isAudioThreadFinished()); |
| for (auto& node : m_finishedNodes) |
| derefNode(*node); |
| |
| m_finishedNodes.clear(); |
| } |
| |
| void AudioContext::refNode(AudioNode& node) |
| { |
| ASSERT(isMainThread()); |
| AutoLocker locker(*this); |
| |
| node.ref(AudioNode::RefTypeConnection); |
| m_referencedNodes.append(&node); |
| } |
| |
| void AudioContext::derefNode(AudioNode& node) |
| { |
| ASSERT(isGraphOwner()); |
| |
| node.deref(AudioNode::RefTypeConnection); |
| |
| ASSERT(m_referencedNodes.contains(&node)); |
| m_referencedNodes.removeFirst(&node); |
| } |
| |
| void AudioContext::derefUnfinishedSourceNodes() |
| { |
| ASSERT(isMainThread() && isAudioThreadFinished()); |
| for (auto& node : m_referencedNodes) |
| node->deref(AudioNode::RefTypeConnection); |
| |
| m_referencedNodes.clear(); |
| } |
| |
| void AudioContext::lock(bool& mustReleaseLock) |
| { |
| // Don't allow regular lock in real-time audio thread. |
| ASSERT(isMainThread()); |
| |
| Thread& thisThread = Thread::current(); |
| |
| if (&thisThread == m_graphOwnerThread) { |
| // We already have the lock. |
| mustReleaseLock = false; |
| } else { |
| // Acquire the lock. |
| m_contextGraphMutex.lock(); |
| m_graphOwnerThread = &thisThread; |
| mustReleaseLock = true; |
| } |
| } |
| |
| bool AudioContext::tryLock(bool& mustReleaseLock) |
| { |
| Thread& thisThread = Thread::current(); |
| bool isAudioThread = &thisThread == audioThread(); |
| |
| // Try to catch cases of using try lock on main thread - it should use regular lock. |
| ASSERT(isAudioThread || isAudioThreadFinished()); |
| |
| if (!isAudioThread) { |
| // In release build treat tryLock() as lock() (since above ASSERT(isAudioThread) never fires) - this is the best we can do. |
| lock(mustReleaseLock); |
| return true; |
| } |
| |
| bool hasLock; |
| |
| if (&thisThread == m_graphOwnerThread) { |
| // Thread already has the lock. |
| hasLock = true; |
| mustReleaseLock = false; |
| } else { |
| // Don't already have the lock - try to acquire it. |
| hasLock = m_contextGraphMutex.tryLock(); |
| |
| if (hasLock) |
| m_graphOwnerThread = &thisThread; |
| |
| mustReleaseLock = hasLock; |
| } |
| |
| return hasLock; |
| } |
| |
| void AudioContext::unlock() |
| { |
| ASSERT(m_graphOwnerThread == &Thread::current()); |
| |
| m_graphOwnerThread = nullptr; |
| m_contextGraphMutex.unlock(); |
| } |
| |
| bool AudioContext::isAudioThread() const |
| { |
| return m_audioThread == &Thread::current(); |
| } |
| |
| bool AudioContext::isGraphOwner() const |
| { |
| return m_graphOwnerThread == &Thread::current(); |
| } |
| |
| void AudioContext::addDeferredFinishDeref(AudioNode* node) |
| { |
| ASSERT(isAudioThread()); |
| m_deferredFinishDerefList.append(node); |
| } |
| |
| void AudioContext::handlePreRenderTasks() |
| { |
| ASSERT(isAudioThread()); |
| |
| // At the beginning of every render quantum, try to update the internal rendering graph state (from main thread changes). |
| // It's OK if the tryLock() fails, we'll just take slightly longer to pick up the changes. |
| bool mustReleaseLock; |
| if (tryLock(mustReleaseLock)) { |
| // Fixup the state of any dirty AudioSummingJunctions and AudioNodeOutputs. |
| handleDirtyAudioSummingJunctions(); |
| handleDirtyAudioNodeOutputs(); |
| |
| updateAutomaticPullNodes(); |
| |
| if (mustReleaseLock) |
| unlock(); |
| } |
| } |
| |
| void AudioContext::handlePostRenderTasks() |
| { |
| ASSERT(isAudioThread()); |
| |
| // Must use a tryLock() here too. Don't worry, the lock will very rarely be contended and this method is called frequently. |
| // The worst that can happen is that there will be some nodes which will take slightly longer than usual to be deleted or removed |
| // from the render graph (in which case they'll render silence). |
| bool mustReleaseLock; |
| if (tryLock(mustReleaseLock)) { |
| // Take care of finishing any derefs where the tryLock() failed previously. |
| handleDeferredFinishDerefs(); |
| |
| // Dynamically clean up nodes which are no longer needed. |
| derefFinishedSourceNodes(); |
| |
| // Don't delete in the real-time thread. Let the main thread do it. |
| // Ref-counted objects held by certain AudioNodes may not be thread-safe. |
| scheduleNodeDeletion(); |
| |
| // Fixup the state of any dirty AudioSummingJunctions and AudioNodeOutputs. |
| handleDirtyAudioSummingJunctions(); |
| handleDirtyAudioNodeOutputs(); |
| |
| updateAutomaticPullNodes(); |
| |
| if (mustReleaseLock) |
| unlock(); |
| } |
| } |
| |
| void AudioContext::handleDeferredFinishDerefs() |
| { |
| ASSERT(isAudioThread() && isGraphOwner()); |
| for (auto& node : m_deferredFinishDerefList) |
| node->finishDeref(AudioNode::RefTypeConnection); |
| |
| m_deferredFinishDerefList.clear(); |
| } |
| |
| void AudioContext::markForDeletion(AudioNode& node) |
| { |
| ASSERT(isGraphOwner()); |
| |
| if (isAudioThreadFinished()) |
| m_nodesToDelete.append(&node); |
| else |
| m_nodesMarkedForDeletion.append(&node); |
| |
| // This is probably the best time for us to remove the node from automatic pull list, |
| // since all connections are gone and we hold the graph lock. Then when handlePostRenderTasks() |
| // gets a chance to schedule the deletion work, updateAutomaticPullNodes() also gets a chance to |
| // modify m_renderingAutomaticPullNodes. |
| removeAutomaticPullNode(node); |
| } |
| |
| void AudioContext::scheduleNodeDeletion() |
| { |
| bool isGood = m_isInitialized && isGraphOwner(); |
| ASSERT(isGood); |
| if (!isGood) |
| return; |
| |
| // Make sure to call deleteMarkedNodes() on main thread. |
| if (m_nodesMarkedForDeletion.size() && !m_isDeletionScheduled) { |
| m_nodesToDelete.appendVector(m_nodesMarkedForDeletion); |
| m_nodesMarkedForDeletion.clear(); |
| |
| m_isDeletionScheduled = true; |
| |
| callOnMainThread([protectedThis = makeRef(*this)]() mutable { |
| protectedThis->deleteMarkedNodes(); |
| }); |
| } |
| } |
| |
| void AudioContext::deleteMarkedNodes() |
| { |
| ASSERT(isMainThread()); |
| |
| // Protect this object from being deleted before we release the mutex locked by AutoLocker. |
| Ref<AudioContext> protectedThis(*this); |
| { |
| AutoLocker locker(*this); |
| |
| while (m_nodesToDelete.size()) { |
| AudioNode* node = m_nodesToDelete.takeLast(); |
| |
| // Before deleting the node, clear out any AudioNodeInputs from m_dirtySummingJunctions. |
| unsigned numberOfInputs = node->numberOfInputs(); |
| for (unsigned i = 0; i < numberOfInputs; ++i) |
| m_dirtySummingJunctions.remove(node->input(i)); |
| |
| // Before deleting the node, clear out any AudioNodeOutputs from m_dirtyAudioNodeOutputs. |
| unsigned numberOfOutputs = node->numberOfOutputs(); |
| for (unsigned i = 0; i < numberOfOutputs; ++i) |
| m_dirtyAudioNodeOutputs.remove(node->output(i)); |
| |
| // Finally, delete it. |
| delete node; |
| } |
| m_isDeletionScheduled = false; |
| } |
| } |
| |
| void AudioContext::markSummingJunctionDirty(AudioSummingJunction* summingJunction) |
| { |
| ASSERT(isGraphOwner()); |
| m_dirtySummingJunctions.add(summingJunction); |
| } |
| |
| void AudioContext::removeMarkedSummingJunction(AudioSummingJunction* summingJunction) |
| { |
| ASSERT(isMainThread()); |
| AutoLocker locker(*this); |
| m_dirtySummingJunctions.remove(summingJunction); |
| } |
| |
| void AudioContext::markAudioNodeOutputDirty(AudioNodeOutput* output) |
| { |
| ASSERT(isGraphOwner()); |
| m_dirtyAudioNodeOutputs.add(output); |
| } |
| |
| void AudioContext::handleDirtyAudioSummingJunctions() |
| { |
| ASSERT(isGraphOwner()); |
| |
| for (auto& junction : m_dirtySummingJunctions) |
| junction->updateRenderingState(); |
| |
| m_dirtySummingJunctions.clear(); |
| } |
| |
| void AudioContext::handleDirtyAudioNodeOutputs() |
| { |
| ASSERT(isGraphOwner()); |
| |
| for (auto& output : m_dirtyAudioNodeOutputs) |
| output->updateRenderingState(); |
| |
| m_dirtyAudioNodeOutputs.clear(); |
| } |
| |
| void AudioContext::addAutomaticPullNode(AudioNode& node) |
| { |
| ASSERT(isGraphOwner()); |
| |
| if (m_automaticPullNodes.add(&node).isNewEntry) |
| m_automaticPullNodesNeedUpdating = true; |
| } |
| |
| void AudioContext::removeAutomaticPullNode(AudioNode& node) |
| { |
| ASSERT(isGraphOwner()); |
| |
| if (m_automaticPullNodes.remove(&node)) |
| m_automaticPullNodesNeedUpdating = true; |
| } |
| |
| void AudioContext::updateAutomaticPullNodes() |
| { |
| ASSERT(isGraphOwner()); |
| |
| if (m_automaticPullNodesNeedUpdating) { |
| // Copy from m_automaticPullNodes to m_renderingAutomaticPullNodes. |
| m_renderingAutomaticPullNodes.resize(m_automaticPullNodes.size()); |
| |
| unsigned i = 0; |
| for (auto& output : m_automaticPullNodes) |
| m_renderingAutomaticPullNodes[i++] = output; |
| |
| m_automaticPullNodesNeedUpdating = false; |
| } |
| } |
| |
| void AudioContext::processAutomaticPullNodes(size_t framesToProcess) |
| { |
| ASSERT(isAudioThread()); |
| |
| for (auto& node : m_renderingAutomaticPullNodes) |
| node->processIfNecessary(framesToProcess); |
| } |
| |
| ScriptExecutionContext* AudioContext::scriptExecutionContext() const |
| { |
| return ActiveDOMObject::scriptExecutionContext(); |
| } |
| |
| void AudioContext::nodeWillBeginPlayback() |
| { |
| // Called by scheduled AudioNodes when clients schedule their start times. |
| // Prior to the introduction of suspend(), resume(), and stop(), starting |
| // a scheduled AudioNode would remove the user-gesture restriction, if present, |
| // and would thus unmute the context. Now that AudioContext stays in the |
| // "suspended" state if a user-gesture restriction is present, starting a |
| // schedule AudioNode should set the state to "running", but only if the |
| // user-gesture restriction is set. |
| if (userGestureRequiredForAudioStart()) |
| startRendering(); |
| } |
| |
| bool AudioContext::willBeginPlayback() |
| { |
| if (!document()) |
| return false; |
| |
| if (userGestureRequiredForAudioStart()) { |
| if (!processingUserGestureForMedia() && !document()->isCapturing()) { |
| ALWAYS_LOG(LOGIDENTIFIER, "returning false, not processing user gesture or capturing"); |
| return false; |
| } |
| removeBehaviorRestriction(AudioContext::RequireUserGestureForAudioStartRestriction); |
| } |
| |
| if (pageConsentRequiredForAudioStart()) { |
| Page* page = document()->page(); |
| if (page && !page->canStartMedia()) { |
| document()->addMediaCanStartListener(*this); |
| ALWAYS_LOG(LOGIDENTIFIER, "returning false, page doesn't allow media to start"); |
| return false; |
| } |
| removeBehaviorRestriction(AudioContext::RequirePageConsentForAudioStartRestriction); |
| } |
| |
| auto willBegin = m_mediaSession->clientWillBeginPlayback(); |
| ALWAYS_LOG(LOGIDENTIFIER, "returning ", willBegin); |
| |
| return willBegin; |
| } |
| |
| bool AudioContext::willPausePlayback() |
| { |
| if (!document()) |
| return false; |
| |
| if (userGestureRequiredForAudioStart()) { |
| if (!processingUserGestureForMedia()) |
| return false; |
| removeBehaviorRestriction(AudioContext::RequireUserGestureForAudioStartRestriction); |
| } |
| |
| if (pageConsentRequiredForAudioStart()) { |
| Page* page = document()->page(); |
| if (page && !page->canStartMedia()) { |
| document()->addMediaCanStartListener(*this); |
| return false; |
| } |
| removeBehaviorRestriction(AudioContext::RequirePageConsentForAudioStartRestriction); |
| } |
| |
| return m_mediaSession->clientWillPausePlayback(); |
| } |
| |
| void AudioContext::startRendering() |
| { |
| ALWAYS_LOG(LOGIDENTIFIER); |
| if (m_isStopScheduled || !willBeginPlayback()) |
| return; |
| |
| makePendingActivity(); |
| |
| destination()->startRendering(); |
| setState(State::Running); |
| } |
| |
| void AudioContext::mediaCanStart(Document& document) |
| { |
| ASSERT_UNUSED(document, &document == this->document()); |
| removeBehaviorRestriction(AudioContext::RequirePageConsentForAudioStartRestriction); |
| mayResumePlayback(true); |
| } |
| |
| MediaProducer::MediaStateFlags AudioContext::mediaState() const |
| { |
| if (!m_isStopScheduled && m_destinationNode && m_destinationNode->isPlayingAudio()) |
| return MediaProducer::IsPlayingAudio; |
| |
| return MediaProducer::IsNotPlaying; |
| } |
| |
| void AudioContext::pageMutedStateDidChange() |
| { |
| if (m_destinationNode && document() && document()->page()) |
| m_destinationNode->setMuted(document()->page()->isAudioMuted()); |
| } |
| |
| void AudioContext::isPlayingAudioDidChange() |
| { |
| // Make sure to call Document::updateIsPlayingMedia() on the main thread, since |
| // we could be on the audio I/O thread here and the call into WebCore could block. |
| callOnMainThread([protectedThis = makeRef(*this)] { |
| if (protectedThis->document()) |
| protectedThis->document()->updateIsPlayingMedia(); |
| }); |
| } |
| |
| void AudioContext::finishedRendering(bool didRendering) |
| { |
| ASSERT(isOfflineContext()); |
| ASSERT(isMainThread()); |
| if (!isMainThread()) |
| return; |
| |
| auto clearPendingActivityIfExitEarly = WTF::makeScopeExit([this] { |
| clearPendingActivity(); |
| }); |
| |
| |
| ALWAYS_LOG(LOGIDENTIFIER); |
| |
| if (!didRendering) |
| return; |
| |
| AudioBuffer* renderedBuffer = m_renderTarget.get(); |
| setState(State::Closed); |
| |
| ASSERT(renderedBuffer); |
| if (!renderedBuffer) |
| return; |
| |
| // Avoid firing the event if the document has already gone away. |
| if (m_isStopScheduled) |
| return; |
| |
| clearPendingActivityIfExitEarly.release(); |
| m_eventQueue->enqueueEvent(OfflineAudioCompletionEvent::create(renderedBuffer)); |
| } |
| |
| void AudioContext::dispatchEvent(Event& event) |
| { |
| EventTarget::dispatchEvent(event); |
| if (event.eventInterface() == OfflineAudioCompletionEventInterfaceType) |
| clearPendingActivity(); |
| } |
| |
| void AudioContext::incrementActiveSourceCount() |
| { |
| ++m_activeSourceCount; |
| } |
| |
| void AudioContext::decrementActiveSourceCount() |
| { |
| --m_activeSourceCount; |
| } |
| |
| void AudioContext::suspend(DOMPromiseDeferred<void>&& promise) |
| { |
| if (isOfflineContext() || m_isStopScheduled) { |
| promise.reject(InvalidStateError); |
| return; |
| } |
| |
| if (m_state == State::Suspended) { |
| promise.resolve(); |
| return; |
| } |
| |
| if (m_state == State::Closed || m_state == State::Interrupted || !m_destinationNode) { |
| promise.reject(); |
| return; |
| } |
| |
| addReaction(State::Suspended, WTFMove(promise)); |
| |
| if (!willPausePlayback()) |
| return; |
| |
| lazyInitialize(); |
| |
| m_destinationNode->suspend([this, protectedThis = makeRef(*this)] { |
| setState(State::Suspended); |
| }); |
| } |
| |
| void AudioContext::resume(DOMPromiseDeferred<void>&& promise) |
| { |
| if (isOfflineContext() || m_isStopScheduled) { |
| promise.reject(InvalidStateError); |
| return; |
| } |
| |
| if (m_state == State::Running) { |
| promise.resolve(); |
| return; |
| } |
| |
| if (m_state == State::Closed || !m_destinationNode) { |
| promise.reject(); |
| return; |
| } |
| |
| addReaction(State::Running, WTFMove(promise)); |
| |
| if (!willBeginPlayback()) |
| return; |
| |
| lazyInitialize(); |
| |
| m_destinationNode->resume([this, protectedThis = makeRef(*this)] { |
| setState(State::Running); |
| }); |
| } |
| |
| void AudioContext::close(DOMPromiseDeferred<void>&& promise) |
| { |
| if (isOfflineContext() || m_isStopScheduled) { |
| promise.reject(InvalidStateError); |
| return; |
| } |
| |
| if (m_state == State::Closed || !m_destinationNode) { |
| promise.resolve(); |
| return; |
| } |
| |
| addReaction(State::Closed, WTFMove(promise)); |
| |
| lazyInitialize(); |
| |
| m_destinationNode->close([this, protectedThis = makeRef(*this)] { |
| setState(State::Closed); |
| uninitialize(); |
| }); |
| } |
| |
| |
| void AudioContext::suspendPlayback() |
| { |
| if (!m_destinationNode || m_state == State::Closed) |
| return; |
| |
| if (m_state == State::Suspended) { |
| if (m_mediaSession->state() == PlatformMediaSession::Interrupted) |
| setState(State::Interrupted); |
| return; |
| } |
| |
| lazyInitialize(); |
| |
| m_destinationNode->suspend([this, protectedThis = makeRef(*this)] { |
| bool interrupted = m_mediaSession->state() == PlatformMediaSession::Interrupted; |
| setState(interrupted ? State::Interrupted : State::Suspended); |
| }); |
| } |
| |
| void AudioContext::mayResumePlayback(bool shouldResume) |
| { |
| if (!m_destinationNode || m_state == State::Closed || m_state == State::Running) |
| return; |
| |
| if (!shouldResume) { |
| setState(State::Suspended); |
| return; |
| } |
| |
| if (!willBeginPlayback()) |
| return; |
| |
| lazyInitialize(); |
| |
| m_destinationNode->resume([this, protectedThis = makeRef(*this)] { |
| setState(State::Running); |
| }); |
| } |
| |
| void AudioContext::postTask(WTF::Function<void()>&& task) |
| { |
| if (m_isStopScheduled) |
| return; |
| |
| m_scriptExecutionContext->postTask(WTFMove(task)); |
| } |
| |
| const SecurityOrigin* AudioContext::origin() const |
| { |
| return m_scriptExecutionContext ? m_scriptExecutionContext->securityOrigin() : nullptr; |
| } |
| |
| void AudioContext::addConsoleMessage(MessageSource source, MessageLevel level, const String& message) |
| { |
| if (m_scriptExecutionContext) |
| m_scriptExecutionContext->addConsoleMessage(source, level, message); |
| } |
| |
| void AudioContext::clearPendingActivity() |
| { |
| if (!m_pendingActivity) |
| return; |
| m_pendingActivity = nullptr; |
| // FIXME: Remove this specific deref() and ref() call in makePendingActivity(). |
| deref(); |
| } |
| |
| void AudioContext::makePendingActivity() |
| { |
| if (m_pendingActivity) |
| return; |
| m_pendingActivity = ActiveDOMObject::makePendingActivity(*this); |
| ref(); |
| } |
| |
| #if !RELEASE_LOG_DISABLED |
| WTFLogChannel& AudioContext::logChannel() const |
| { |
| return LogMedia; |
| } |
| #endif |
| |
| } // namespace WebCore |
| |
| #endif // ENABLE(WEB_AUDIO) |