/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim:set ts=2 sw=2 sts=2 et cindent: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "AudioContext.h" #include "nsPIDOMWindow.h" #include "mozilla/ErrorResult.h" #include "mozilla/dom/AnalyserNode.h" #include "mozilla/dom/AudioContextBinding.h" #include "mozilla/dom/HTMLMediaElement.h" #include "mozilla/dom/OfflineAudioContextBinding.h" #include "mozilla/dom/OwningNonNull.h" #include "MediaStreamGraph.h" #include "AudioDestinationNode.h" #include "AudioBufferSourceNode.h" #include "AudioBuffer.h" #include "GainNode.h" #include "MediaElementAudioSourceNode.h" #include "MediaStreamAudioSourceNode.h" #include "DelayNode.h" #include "PannerNode.h" #include "AudioListener.h" #include "DynamicsCompressorNode.h" #include "BiquadFilterNode.h" #include "ScriptProcessorNode.h" #include "ChannelMergerNode.h" #include "ChannelSplitterNode.h" #include "MediaStreamAudioDestinationNode.h" #include "WaveShaperNode.h" #include "PeriodicWave.h" #include "ConvolverNode.h" #include "OscillatorNode.h" #include "nsNetUtil.h" #include "AudioStream.h" namespace mozilla { namespace dom { NS_IMPL_CYCLE_COLLECTION_CLASS(AudioContext) NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(AudioContext) NS_IMPL_CYCLE_COLLECTION_UNLINK(mDestination) NS_IMPL_CYCLE_COLLECTION_UNLINK(mListener) if (!tmp->mIsStarted) { NS_IMPL_CYCLE_COLLECTION_UNLINK(mActiveNodes) } NS_IMPL_CYCLE_COLLECTION_UNLINK_END_INHERITED(nsDOMEventTargetHelper) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(AudioContext, nsDOMEventTargetHelper) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDestination) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mListener) if (!tmp->mIsStarted) { MOZ_ASSERT(tmp->mIsOffline, "Online AudioContexts should always be started"); NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mActiveNodes) } NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END NS_IMPL_ADDREF_INHERITED(AudioContext, nsDOMEventTargetHelper) NS_IMPL_RELEASE_INHERITED(AudioContext, nsDOMEventTargetHelper) NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(AudioContext) NS_INTERFACE_MAP_END_INHERITING(nsDOMEventTargetHelper) static uint8_t gWebAudioOutputKey; float GetSampleRateForAudioContext(bool aIsOffline, float aSampleRate) { if (aIsOffline) { return aSampleRate; } else { AudioStream::InitPreferredSampleRate(); return static_cast(AudioStream::PreferredSampleRate()); } } AudioContext::AudioContext(nsPIDOMWindow* aWindow, bool aIsOffline, uint32_t aNumberOfChannels, uint32_t aLength, float aSampleRate) : nsDOMEventTargetHelper(aWindow) , mSampleRate(GetSampleRateForAudioContext(aIsOffline, aSampleRate)) , mNumberOfChannels(aNumberOfChannels) , mNodeCount(0) , mIsOffline(aIsOffline) , mIsStarted(!aIsOffline) , mIsShutDown(false) { aWindow->AddAudioContext(this); // Note: AudioDestinationNode needs an AudioContext that must already be // bound to the window. mDestination = new AudioDestinationNode(this, aIsOffline, aNumberOfChannels, aLength, aSampleRate); mDestination->Stream()->AddAudioOutput(&gWebAudioOutputKey); // We skip calling SetIsOnlyNodeForContext during mDestination's constructor, // because we can only call SetIsOnlyNodeForContext after mDestination has // been set up. mDestination->SetIsOnlyNodeForContext(true); } AudioContext::~AudioContext() { nsPIDOMWindow* window = GetOwner(); if (window) { window->RemoveAudioContext(this); } UnregisterWeakMemoryReporter(this); } JSObject* AudioContext::WrapObject(JSContext* aCx, JS::Handle aScope) { if (mIsOffline) { return OfflineAudioContextBinding::Wrap(aCx, aScope, this); } else { return AudioContextBinding::Wrap(aCx, aScope, this); } } /* static */ already_AddRefed AudioContext::Constructor(const GlobalObject& aGlobal, ErrorResult& aRv) { nsCOMPtr window = do_QueryInterface(aGlobal.GetAsSupports()); if (!window) { aRv.Throw(NS_ERROR_FAILURE); return nullptr; } nsRefPtr object = new AudioContext(window, false); RegisterWeakMemoryReporter(object); return object.forget(); } /* static */ already_AddRefed AudioContext::Constructor(const GlobalObject& aGlobal, uint32_t aNumberOfChannels, uint32_t aLength, float aSampleRate, ErrorResult& aRv) { nsCOMPtr window = do_QueryInterface(aGlobal.GetAsSupports()); if (!window) { aRv.Throw(NS_ERROR_FAILURE); return nullptr; } if (aNumberOfChannels == 0 || aNumberOfChannels > WebAudioUtils::MaxChannelCount || aLength == 0 || aSampleRate <= 1.0f || aSampleRate >= TRACK_RATE_MAX) { // The DOM binding protects us against infinity and NaN aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR); return nullptr; } nsRefPtr object = new AudioContext(window, true, aNumberOfChannels, aLength, aSampleRate); RegisterWeakMemoryReporter(object); return object.forget(); } already_AddRefed AudioContext::CreateBufferSource() { nsRefPtr bufferNode = new AudioBufferSourceNode(this); return bufferNode.forget(); } already_AddRefed AudioContext::CreateBuffer(JSContext* aJSContext, uint32_t aNumberOfChannels, uint32_t aLength, float aSampleRate, ErrorResult& aRv) { if (aSampleRate < 8000 || aSampleRate > 192000 || !aLength || !aNumberOfChannels) { aRv.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR); return nullptr; } if (aLength > INT32_MAX) { aRv.Throw(NS_ERROR_OUT_OF_MEMORY); return nullptr; } nsRefPtr buffer = new AudioBuffer(this, int32_t(aLength), aSampleRate); if (!buffer->InitializeBuffers(aNumberOfChannels, aJSContext)) { aRv.Throw(NS_ERROR_OUT_OF_MEMORY); return nullptr; } return buffer.forget(); } already_AddRefed AudioContext::CreateBuffer(JSContext* aJSContext, const ArrayBuffer& aBuffer, bool aMixToMono, ErrorResult& aRv) { // Do not accept this method unless the legacy pref has been set. if (!Preferences::GetBool("media.webaudio.legacy.AudioContext")) { aRv.ThrowNotEnoughArgsError(); return nullptr; } // Sniff the content of the media. // Failed type sniffing will be handled by SyncDecodeMedia. nsAutoCString contentType; NS_SniffContent(NS_DATA_SNIFFER_CATEGORY, nullptr, aBuffer.Data(), aBuffer.Length(), contentType); nsRefPtr job = new WebAudioDecodeJob(contentType, this, aBuffer); if (mDecoder.SyncDecodeMedia(contentType.get(), aBuffer.Data(), aBuffer.Length(), *job) && job->mOutput) { nsRefPtr buffer = job->mOutput.forget(); if (aMixToMono) { buffer->MixToMono(aJSContext); } return buffer.forget(); } return nullptr; } namespace { bool IsValidBufferSize(uint32_t aBufferSize) { switch (aBufferSize) { case 0: // let the implementation choose the buffer size case 256: case 512: case 1024: case 2048: case 4096: case 8192: case 16384: return true; default: return false; } } } already_AddRefed AudioContext::CreateMediaStreamDestination(ErrorResult& aRv) { if (mIsOffline) { aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR); return nullptr; } nsRefPtr node = new MediaStreamAudioDestinationNode(this); return node.forget(); } already_AddRefed AudioContext::CreateScriptProcessor(uint32_t aBufferSize, uint32_t aNumberOfInputChannels, uint32_t aNumberOfOutputChannels, ErrorResult& aRv) { if ((aNumberOfInputChannels == 0 && aNumberOfOutputChannels == 0) || aNumberOfInputChannels > WebAudioUtils::MaxChannelCount || aNumberOfOutputChannels > WebAudioUtils::MaxChannelCount || !IsValidBufferSize(aBufferSize)) { aRv.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR); return nullptr; } nsRefPtr scriptProcessor = new ScriptProcessorNode(this, aBufferSize, aNumberOfInputChannels, aNumberOfOutputChannels); return scriptProcessor.forget(); } already_AddRefed AudioContext::CreateAnalyser() { nsRefPtr analyserNode = new AnalyserNode(this); return analyserNode.forget(); } already_AddRefed AudioContext::CreateMediaElementSource(HTMLMediaElement& aMediaElement, ErrorResult& aRv) { if (mIsOffline) { aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR); return nullptr; } nsRefPtr stream = aMediaElement.MozCaptureStream(aRv); if (aRv.Failed()) { return nullptr; } nsRefPtr mediaElementAudioSourceNode = new MediaElementAudioSourceNode(this, stream); return mediaElementAudioSourceNode.forget(); } already_AddRefed AudioContext::CreateMediaStreamSource(DOMMediaStream& aMediaStream, ErrorResult& aRv) { if (mIsOffline) { aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR); return nullptr; } nsRefPtr mediaStreamAudioSourceNode = new MediaStreamAudioSourceNode(this, &aMediaStream); return mediaStreamAudioSourceNode.forget(); } already_AddRefed AudioContext::CreateGain() { nsRefPtr gainNode = new GainNode(this); return gainNode.forget(); } already_AddRefed AudioContext::CreateWaveShaper() { nsRefPtr waveShaperNode = new WaveShaperNode(this); return waveShaperNode.forget(); } already_AddRefed AudioContext::CreateDelay(double aMaxDelayTime, ErrorResult& aRv) { if (aMaxDelayTime > 0. && aMaxDelayTime < 180.) { nsRefPtr delayNode = new DelayNode(this, aMaxDelayTime); return delayNode.forget(); } aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR); return nullptr; } already_AddRefed AudioContext::CreatePanner() { nsRefPtr pannerNode = new PannerNode(this); mPannerNodes.PutEntry(pannerNode); return pannerNode.forget(); } already_AddRefed AudioContext::CreateConvolver() { nsRefPtr convolverNode = new ConvolverNode(this); return convolverNode.forget(); } already_AddRefed AudioContext::CreateChannelSplitter(uint32_t aNumberOfOutputs, ErrorResult& aRv) { if (aNumberOfOutputs == 0 || aNumberOfOutputs > WebAudioUtils::MaxChannelCount) { aRv.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR); return nullptr; } nsRefPtr splitterNode = new ChannelSplitterNode(this, aNumberOfOutputs); return splitterNode.forget(); } already_AddRefed AudioContext::CreateChannelMerger(uint32_t aNumberOfInputs, ErrorResult& aRv) { if (aNumberOfInputs == 0 || aNumberOfInputs > WebAudioUtils::MaxChannelCount) { aRv.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR); return nullptr; } nsRefPtr mergerNode = new ChannelMergerNode(this, aNumberOfInputs); return mergerNode.forget(); } already_AddRefed AudioContext::CreateDynamicsCompressor() { nsRefPtr compressorNode = new DynamicsCompressorNode(this); return compressorNode.forget(); } already_AddRefed AudioContext::CreateBiquadFilter() { nsRefPtr filterNode = new BiquadFilterNode(this); return filterNode.forget(); } already_AddRefed AudioContext::CreateOscillator() { nsRefPtr oscillatorNode = new OscillatorNode(this); return oscillatorNode.forget(); } already_AddRefed AudioContext::CreatePeriodicWave(const Float32Array& aRealData, const Float32Array& aImagData, ErrorResult& aRv) { if (aRealData.Length() != aImagData.Length() || aRealData.Length() == 0 || aRealData.Length() > 4096) { aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR); return nullptr; } nsRefPtr periodicWave = new PeriodicWave(this, aRealData.Data(), aImagData.Data(), aImagData.Length(), aRv); if (aRv.Failed()) { return nullptr; } return periodicWave.forget(); } AudioListener* AudioContext::Listener() { if (!mListener) { mListener = new AudioListener(this); } return mListener; } void AudioContext::DecodeAudioData(const ArrayBuffer& aBuffer, DecodeSuccessCallback& aSuccessCallback, const Optional >& aFailureCallback) { // Sniff the content of the media. // Failed type sniffing will be handled by AsyncDecodeMedia. nsAutoCString contentType; NS_SniffContent(NS_DATA_SNIFFER_CATEGORY, nullptr, aBuffer.Data(), aBuffer.Length(), contentType); nsCOMPtr failureCallback; if (aFailureCallback.WasPassed()) { failureCallback = &aFailureCallback.Value(); } nsRefPtr job( new WebAudioDecodeJob(contentType, this, aBuffer, &aSuccessCallback, failureCallback)); mDecoder.AsyncDecodeMedia(contentType.get(), aBuffer.Data(), aBuffer.Length(), *job); // Transfer the ownership to mDecodeJobs mDecodeJobs.AppendElement(job.forget()); } void AudioContext::RemoveFromDecodeQueue(WebAudioDecodeJob* aDecodeJob) { mDecodeJobs.RemoveElement(aDecodeJob); } void AudioContext::RegisterActiveNode(AudioNode* aNode) { if (!mIsShutDown) { mActiveNodes.PutEntry(aNode); } } void AudioContext::UnregisterActiveNode(AudioNode* aNode) { mActiveNodes.RemoveEntry(aNode); } void AudioContext::UnregisterAudioBufferSourceNode(AudioBufferSourceNode* aNode) { UpdatePannerSource(); } void AudioContext::UnregisterPannerNode(PannerNode* aNode) { mPannerNodes.RemoveEntry(aNode); if (mListener) { mListener->UnregisterPannerNode(aNode); } } static PLDHashOperator FindConnectedSourcesOn(nsPtrHashKey* aEntry, void* aData) { aEntry->GetKey()->FindConnectedSources(); return PL_DHASH_NEXT; } void AudioContext::UpdatePannerSource() { mPannerNodes.EnumerateEntries(FindConnectedSourcesOn, nullptr); } uint32_t AudioContext::MaxChannelCount() const { return mIsOffline ? mNumberOfChannels : AudioStream::MaxNumberOfChannels(); } MediaStreamGraph* AudioContext::Graph() const { return Destination()->Stream()->Graph(); } MediaStream* AudioContext::DestinationStream() const { if (Destination()) { return Destination()->Stream(); } return nullptr; } double AudioContext::CurrentTime() const { return MediaTimeToSeconds(Destination()->Stream()->GetCurrentTime()) + Destination()->ExtraCurrentTime(); } void AudioContext::Shutdown() { mIsShutDown = true; // We mute rather than suspending, because the delay between the ::Shutdown // call and the CC would make us overbuffer in the MediaStreamGraph. // See bug 936784 for details. if (!mIsOffline) { Mute(); } mDecoder.Shutdown(); // Release references to active nodes. // Active AudioNodes don't unregister in destructors, at which point the // Node is already unregistered. mActiveNodes.Clear(); // For offline contexts, we can destroy the MediaStreamGraph at this point. if (mIsOffline && mDestination) { mDestination->OfflineShutdown(); } } void AudioContext::Suspend() { MediaStream* ds = DestinationStream(); if (ds) { ds->ChangeExplicitBlockerCount(1); } } void AudioContext::Resume() { MediaStream* ds = DestinationStream(); if (ds) { ds->ChangeExplicitBlockerCount(-1); } } void AudioContext::UpdateNodeCount(int32_t aDelta) { bool firstNode = mNodeCount == 0; mNodeCount += aDelta; MOZ_ASSERT(mNodeCount >= 0); // mDestinationNode may be null when we're destroying nodes unlinked by CC if (!firstNode && mDestination) { mDestination->SetIsOnlyNodeForContext(mNodeCount == 1); } } JSContext* AudioContext::GetJSContext() const { MOZ_ASSERT(NS_IsMainThread()); nsCOMPtr scriptGlobal = do_QueryInterface(GetParentObject()); if (!scriptGlobal) { return nullptr; } nsIScriptContext* scriptContext = scriptGlobal->GetContext(); if (!scriptContext) { return nullptr; } return scriptContext->GetNativeContext(); } void AudioContext::StartRendering(ErrorResult& aRv) { MOZ_ASSERT(mIsOffline, "This should only be called on OfflineAudioContext"); if (mIsStarted) { aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); return; } mIsStarted = true; mDestination->StartRendering(); } void AudioContext::Mute() const { MOZ_ASSERT(!mIsOffline); if (mDestination) { mDestination->Mute(); } } void AudioContext::Unmute() const { MOZ_ASSERT(!mIsOffline); if (mDestination) { mDestination->Unmute(); } } AudioChannel AudioContext::MozAudioChannelType() const { return mDestination->MozAudioChannelType(); } void AudioContext::SetMozAudioChannelType(AudioChannel aValue, ErrorResult& aRv) { mDestination->SetMozAudioChannelType(aValue, aRv); } size_t AudioContext::SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const { // AudioNodes are tracked separately because we do not want the AudioContext // to track all of the AudioNodes it creates, so we wouldn't be able to // traverse them from here. size_t amount = aMallocSizeOf(this); if (mListener) { amount += mListener->SizeOfIncludingThis(aMallocSizeOf); } amount += mDecoder.SizeOfExcludingThis(aMallocSizeOf); amount += mDecodeJobs.SizeOfExcludingThis(aMallocSizeOf); for (uint32_t i = 0; i < mDecodeJobs.Length(); ++i) { amount += mDecodeJobs[i]->SizeOfExcludingThis(aMallocSizeOf); } amount += mActiveNodes.SizeOfExcludingThis(nullptr, aMallocSizeOf); amount += mPannerNodes.SizeOfExcludingThis(nullptr, aMallocSizeOf); return amount; } NS_IMETHODIMP AudioContext::CollectReports(nsIHandleReportCallback* aHandleReport, nsISupports* aData) { int64_t amount = SizeOfIncludingThis(MallocSizeOf); return MOZ_COLLECT_REPORT("explicit/webaudio/audiocontext", KIND_HEAP, UNITS_BYTES, amount, "Memory used by AudioContext objects (Web Audio)."); } double AudioContext::ExtraCurrentTime() const { return mDestination->ExtraCurrentTime(); } } }