diff options
Diffstat (limited to 'dom/media/webaudio/ConvolverNode.cpp')
-rw-r--r-- | dom/media/webaudio/ConvolverNode.cpp | 479 |
1 files changed, 479 insertions, 0 deletions
diff --git a/dom/media/webaudio/ConvolverNode.cpp b/dom/media/webaudio/ConvolverNode.cpp new file mode 100644 index 0000000000..65562ae6d0 --- /dev/null +++ b/dom/media/webaudio/ConvolverNode.cpp @@ -0,0 +1,479 @@ +/* -*- 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 "ConvolverNode.h" +#include "mozilla/dom/ConvolverNodeBinding.h" +#include "AlignmentUtils.h" +#include "AudioNodeEngine.h" +#include "AudioNodeTrack.h" +#include "blink/Reverb.h" +#include "PlayingRefChangeHandler.h" +#include "Tracing.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_INHERITED(ConvolverNode, AudioNode, mBuffer) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ConvolverNode) +NS_INTERFACE_MAP_END_INHERITING(AudioNode) + +NS_IMPL_ADDREF_INHERITED(ConvolverNode, AudioNode) +NS_IMPL_RELEASE_INHERITED(ConvolverNode, AudioNode) + +class ConvolverNodeEngine final : public AudioNodeEngine { + typedef PlayingRefChangeHandler PlayingRefChanged; + + public: + ConvolverNodeEngine(AudioNode* aNode, bool aNormalize) + : AudioNodeEngine(aNode) {} + + // Indicates how the right output channel is generated. + enum class RightConvolverMode { + // A right convolver is always used when there is more than one impulse + // response channel. + Always, + // With a single response channel, the mode may be either Direct or + // Difference. The decision on which to use is made when stereo input is + // received. Once the right convolver is in use, convolver state is + // suitable only for the selected mode, and so the mode cannot change + // until the right convolver contains only silent history. + // + // With Direct mode, each convolver processes a corresponding channel. + // This mode is selected when input is initially stereo or + // channelInterpretation is "discrete" at the time or starting the right + // convolver when input changes from non-silent mono to stereo. + Direct, + // Difference mode is selected if channelInterpretation is "speakers" at + // the time starting the right convolver when the input changes from mono + // to stereo. + // + // When non-silent input is initially mono, with a single response + // channel, the right output channel is not produced until input becomes + // stereo. Only a single convolver is used for mono processing. When + // stereo input arrives after mono input, output must be as if the mono + // signal remaining in the left convolver is up-mixed, but the right + // convolver has not been initialized with the history of the mono input. + // Copying the state of the left convolver into the right convolver is not + // desirable, because there is considerable state to copy, and the + // different convolvers are intended to process out of phase, which means + // that state from one convolver would not directly map to state in + // another convolver. + // + // Instead the distributive property of convolution is used to generate + // the right output channel using information in the left output channel. + // Using l and r to denote the left and right channel input signals, g the + // impulse response, and * convolution, the convolution of the right + // channel can be given by + // + // r * g = (l + (r - l)) * g + // = l * g + (r - l) * g + // + // The left convolver continues to process the left channel l to produce + // l * g. The right convolver processes the difference of input channel + // signals r - l to produce (r - l) * g. The outputs of the two + // convolvers are added to generate the right channel output r * g. + // + // The benefit of doing this is that the history of the r - l input for a + // "speakers" up-mixed mono signal is zero, and so an empty convolver + // already has exactly the right history for mixing the previous mono + // signal with the new stereo signal. + Difference + }; + + void SetReverb(WebCore::Reverb* aReverb, + uint32_t aImpulseChannelCount) override { + mRemainingLeftOutput = INT32_MIN; + mRemainingRightOutput = 0; + mRemainingRightHistory = 0; + + // Assume for now that convolution of channel difference is not required. + // Direct may change to Difference during processing. + if (aReverb) { + mRightConvolverMode = aImpulseChannelCount == 1 + ? RightConvolverMode::Direct + : RightConvolverMode::Always; + } else { + mRightConvolverMode = RightConvolverMode::Always; + } + + mReverb.reset(aReverb); + } + + void AllocateReverbInput(const AudioBlock& aInput, + uint32_t aTotalChannelCount) { + uint32_t inputChannelCount = aInput.ChannelCount(); + MOZ_ASSERT(inputChannelCount <= aTotalChannelCount); + mReverbInput.AllocateChannels(aTotalChannelCount); + // Pre-multiply the input's volume + for (uint32_t i = 0; i < inputChannelCount; ++i) { + const float* src = static_cast<const float*>(aInput.mChannelData[i]); + float* dest = mReverbInput.ChannelFloatsForWrite(i); + AudioBlockCopyChannelWithScale(src, aInput.mVolume, dest); + } + // Fill remaining channels with silence + for (uint32_t i = inputChannelCount; i < aTotalChannelCount; ++i) { + float* dest = mReverbInput.ChannelFloatsForWrite(i); + std::fill_n(dest, WEBAUDIO_BLOCK_SIZE, 0.0f); + } + } + + void ProcessBlock(AudioNodeTrack* aTrack, GraphTime aFrom, + const AudioBlock& aInput, AudioBlock* aOutput, + bool* aFinished) override; + + bool IsActive() const override { return mRemainingLeftOutput != INT32_MIN; } + + size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override { + size_t amount = AudioNodeEngine::SizeOfExcludingThis(aMallocSizeOf); + + amount += mReverbInput.SizeOfExcludingThis(aMallocSizeOf, false); + + if (mReverb) { + amount += mReverb->sizeOfIncludingThis(aMallocSizeOf); + } + + return amount; + } + + size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override { + return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf); + } + + private: + // Keeping mReverbInput across process calls avoids unnecessary reallocation. + AudioBlock mReverbInput; + UniquePtr<WebCore::Reverb> mReverb; + // Tracks samples of the tail remaining to be output. INT32_MIN is a + // special value to indicate that the end of any previous tail has been + // handled. + int32_t mRemainingLeftOutput = INT32_MIN; + // mRemainingRightOutput and mRemainingRightHistory are only used when + // mRightOutputMode != Always. There is no special handling required at the + // end of tail times and so INT32_MIN is not used. + // mRemainingRightOutput tracks how much longer this node needs to continue + // to produce a right output channel. + int32_t mRemainingRightOutput = 0; + // mRemainingRightHistory tracks how much silent input would be required to + // drain the right convolver, which may sometimes be longer than the period + // a right output channel is required. + int32_t mRemainingRightHistory = 0; + RightConvolverMode mRightConvolverMode = RightConvolverMode::Always; +}; + +static void AddScaledLeftToRight(AudioBlock* aBlock, float aScale) { + const float* left = static_cast<const float*>(aBlock->mChannelData[0]); + float* right = aBlock->ChannelFloatsForWrite(1); + AudioBlockAddChannelWithScale(left, aScale, right); +} + +void ConvolverNodeEngine::ProcessBlock(AudioNodeTrack* aTrack, GraphTime aFrom, + const AudioBlock& aInput, + AudioBlock* aOutput, bool* aFinished) { + TRACE("ConvolverNodeEngine::ProcessBlock"); + if (!mReverb) { + aOutput->SetNull(WEBAUDIO_BLOCK_SIZE); + return; + } + + uint32_t inputChannelCount = aInput.ChannelCount(); + if (aInput.IsNull()) { + if (mRemainingLeftOutput > 0) { + mRemainingLeftOutput -= WEBAUDIO_BLOCK_SIZE; + AllocateReverbInput(aInput, 1); // floats for silence + } else { + if (mRemainingLeftOutput != INT32_MIN) { + mRemainingLeftOutput = INT32_MIN; + MOZ_ASSERT(mRemainingRightOutput <= 0); + MOZ_ASSERT(mRemainingRightHistory <= 0); + aTrack->ScheduleCheckForInactive(); + RefPtr<PlayingRefChanged> refchanged = + new PlayingRefChanged(aTrack, PlayingRefChanged::RELEASE); + aTrack->Graph()->DispatchToMainThreadStableState(refchanged.forget()); + } + aOutput->SetNull(WEBAUDIO_BLOCK_SIZE); + return; + } + } else { + if (mRemainingLeftOutput <= 0) { + RefPtr<PlayingRefChanged> refchanged = + new PlayingRefChanged(aTrack, PlayingRefChanged::ADDREF); + aTrack->Graph()->DispatchToMainThreadStableState(refchanged.forget()); + } + + // Use mVolume as a flag to detect whether AllocateReverbInput() gets + // called. + mReverbInput.mVolume = 0.0f; + + // Special handling of input channel count changes is used when there is + // only a single impulse response channel. See RightConvolverMode. + if (mRightConvolverMode != RightConvolverMode::Always) { + ChannelInterpretation channelInterpretation = + aTrack->GetChannelInterpretation(); + if (inputChannelCount == 2) { + if (mRemainingRightHistory <= 0) { + // Will start the second convolver. Choose to convolve the right + // channel directly if there is no left tail to up-mix or up-mixing + // is "discrete". + mRightConvolverMode = + (mRemainingLeftOutput <= 0 || + channelInterpretation == ChannelInterpretation::Discrete) + ? RightConvolverMode::Direct + : RightConvolverMode::Difference; + } + // The extra WEBAUDIO_BLOCK_SIZE is subtracted below. + mRemainingRightOutput = + mReverb->impulseResponseLength() + WEBAUDIO_BLOCK_SIZE; + mRemainingRightHistory = mRemainingRightOutput; + if (mRightConvolverMode == RightConvolverMode::Difference) { + AllocateReverbInput(aInput, 2); + // Subtract left from right. + AddScaledLeftToRight(&mReverbInput, -1.0f); + } + } else if (mRemainingRightHistory > 0) { + // There is one channel of input, but a second convolver also + // requires input. Up-mix appropriately for the second convolver. + if ((mRightConvolverMode == RightConvolverMode::Difference) ^ + (channelInterpretation == ChannelInterpretation::Discrete)) { + MOZ_ASSERT( + (mRightConvolverMode == RightConvolverMode::Difference && + channelInterpretation == ChannelInterpretation::Speakers) || + (mRightConvolverMode == RightConvolverMode::Direct && + channelInterpretation == ChannelInterpretation::Discrete)); + // The state is one of the following combinations: + // 1) Difference and speakers. + // Up-mixing gives r = l. + // The input to the second convolver is r - l. + // 2) Direct and discrete. + // Up-mixing gives r = 0. + // The input to the second convolver is r. + // + // In each case the input for the second convolver is silence, which + // will drain the convolver. + AllocateReverbInput(aInput, 2); + } else { + if (channelInterpretation == ChannelInterpretation::Discrete) { + MOZ_ASSERT(mRightConvolverMode == RightConvolverMode::Difference); + // channelInterpretation has changed since the second convolver + // was added. "discrete" up-mixing of input would produce a + // silent right channel r = 0, but the second convolver needs + // r - l for RightConvolverMode::Difference. + AllocateReverbInput(aInput, 2); + AddScaledLeftToRight(&mReverbInput, -1.0f); + } else { + MOZ_ASSERT(channelInterpretation == + ChannelInterpretation::Speakers); + MOZ_ASSERT(mRightConvolverMode == RightConvolverMode::Direct); + // The Reverb will essentially up-mix the single input channel by + // feeding it into both convolvers. + } + // The second convolver does not have silent input, and so it will + // not drain. It will need to continue processing up-mixed input + // because the next input block may be stereo, which would be mixed + // with the signal remaining in the convolvers. + // The extra WEBAUDIO_BLOCK_SIZE is subtracted below. + mRemainingRightHistory = + mReverb->impulseResponseLength() + WEBAUDIO_BLOCK_SIZE; + } + } + } + + if (mReverbInput.mVolume == 0.0f) { // not yet set + if (aInput.mVolume != 1.0f) { + AllocateReverbInput(aInput, inputChannelCount); // pre-multiply + } else { + mReverbInput = aInput; + } + } + + mRemainingLeftOutput = mReverb->impulseResponseLength(); + MOZ_ASSERT(mRemainingLeftOutput > 0); + } + + // "The ConvolverNode produces a mono output only in the single case where + // there is a single input channel and a single-channel buffer." + uint32_t outputChannelCount = 2; + uint32_t reverbOutputChannelCount = 2; + if (mRightConvolverMode != RightConvolverMode::Always) { + // When the input changes from stereo to mono, the output continues to be + // stereo for the length of the tail time, during which the two channels + // may differ. + if (mRemainingRightOutput > 0) { + MOZ_ASSERT(mRemainingRightHistory > 0); + mRemainingRightOutput -= WEBAUDIO_BLOCK_SIZE; + } else { + outputChannelCount = 1; + } + // The second convolver keeps processing until it drains. + if (mRemainingRightHistory > 0) { + mRemainingRightHistory -= WEBAUDIO_BLOCK_SIZE; + } else { + reverbOutputChannelCount = 1; + } + } + + // If there are two convolvers, then they each need an output buffer, even + // if the second convolver is only processing to keep history of up-mixed + // input. + aOutput->AllocateChannels(reverbOutputChannelCount); + + mReverb->process(&mReverbInput, aOutput); + + if (mRightConvolverMode == RightConvolverMode::Difference && + outputChannelCount == 2) { + // Add left to right. + AddScaledLeftToRight(aOutput, 1.0f); + } else { + // Trim if outputChannelCount < reverbOutputChannelCount + aOutput->mChannelData.TruncateLength(outputChannelCount); + } +} + +ConvolverNode::ConvolverNode(AudioContext* aContext) + : AudioNode(aContext, 2, ChannelCountMode::Clamped_max, + ChannelInterpretation::Speakers), + mNormalize(true) { + ConvolverNodeEngine* engine = new ConvolverNodeEngine(this, mNormalize); + mTrack = AudioNodeTrack::Create( + aContext, engine, AudioNodeTrack::NO_TRACK_FLAGS, aContext->Graph()); +} + +/* static */ +already_AddRefed<ConvolverNode> ConvolverNode::Create( + JSContext* aCx, AudioContext& aAudioContext, + const ConvolverOptions& aOptions, ErrorResult& aRv) { + RefPtr<ConvolverNode> audioNode = new ConvolverNode(&aAudioContext); + + audioNode->Initialize(aOptions, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + // This must be done before setting the buffer. + audioNode->SetNormalize(!aOptions.mDisableNormalization); + + if (aOptions.mBuffer.WasPassed()) { + MOZ_ASSERT(aCx); + audioNode->SetBuffer(aCx, aOptions.mBuffer.Value(), aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + } + + return audioNode.forget(); +} + +size_t ConvolverNode::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const { + size_t amount = AudioNode::SizeOfExcludingThis(aMallocSizeOf); + if (mBuffer) { + // NB: mBuffer might be shared with the associated engine, by convention + // the AudioNode will report. + amount += mBuffer->SizeOfIncludingThis(aMallocSizeOf); + } + return amount; +} + +size_t ConvolverNode::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const { + return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf); +} + +JSObject* ConvolverNode::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return ConvolverNode_Binding::Wrap(aCx, this, aGivenProto); +} + +void ConvolverNode::SetBuffer(JSContext* aCx, AudioBuffer* aBuffer, + ErrorResult& aRv) { + if (aBuffer) { + switch (aBuffer->NumberOfChannels()) { + case 1: + case 2: + case 4: + // Supported number of channels + break; + default: + aRv.ThrowNotSupportedError( + nsPrintfCString("%u is not a supported number of channels", + aBuffer->NumberOfChannels())); + return; + } + } + + if (aBuffer && (aBuffer->SampleRate() != Context()->SampleRate())) { + aRv.ThrowNotSupportedError(nsPrintfCString( + "Buffer sample rate (%g) does not match AudioContext sample rate (%g)", + aBuffer->SampleRate(), Context()->SampleRate())); + return; + } + + // Send the buffer to the track + AudioNodeTrack* ns = mTrack; + MOZ_ASSERT(ns, "Why don't we have a track here?"); + if (aBuffer) { + AudioChunk data = aBuffer->GetThreadSharedChannelsForRate(aCx); + if (data.mBufferFormat == AUDIO_FORMAT_S16) { + // Reverb expects data in float format. + // Convert on the main thread so as to minimize allocations on the audio + // thread. + // Reverb will dispose of the buffer once initialized, so convert here + // and leave the smaller arrays in the AudioBuffer. + // There is currently no value in providing 16/32-byte aligned data + // because PadAndMakeScaledDFT() will copy the data (without SIMD + // instructions) to aligned arrays for the FFT. + CheckedInt<size_t> bufferSize(sizeof(float)); + bufferSize *= data.mDuration; + bufferSize *= data.ChannelCount(); + RefPtr<SharedBuffer> floatBuffer = + SharedBuffer::Create(bufferSize, fallible); + if (!floatBuffer) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + auto floatData = static_cast<float*>(floatBuffer->Data()); + for (size_t i = 0; i < data.ChannelCount(); ++i) { + ConvertAudioSamples(data.ChannelData<int16_t>()[i], floatData, + data.mDuration); + data.mChannelData[i] = floatData; + floatData += data.mDuration; + } + data.mBuffer = std::move(floatBuffer); + data.mBufferFormat = AUDIO_FORMAT_FLOAT32; + } else if (data.mBufferFormat == AUDIO_FORMAT_SILENCE) { + // This is valid, but a signal convolved by a silent signal is silent, set + // the reverb to nullptr and return. + ns->SetReverb(nullptr, 0); + mBuffer = aBuffer; + return; + } + + // Note about empirical tuning (this is copied from Blink) + // The maximum FFT size affects reverb performance and accuracy. + // If the reverb is single-threaded and processes entirely in the real-time + // audio thread, it's important not to make this too high. In this case + // 8192 is a good value. But, the Reverb object is multi-threaded, so we + // want this as high as possible without losing too much accuracy. Very + // large FFTs will have worse phase errors. Given these constraints 32768 is + // a good compromise. + const size_t MaxFFTSize = 32768; + + bool allocationFailure = false; + UniquePtr<WebCore::Reverb> reverb(new WebCore::Reverb( + data, MaxFFTSize, !Context()->IsOffline(), mNormalize, + aBuffer->SampleRate(), &allocationFailure)); + if (!allocationFailure) { + ns->SetReverb(reverb.release(), data.ChannelCount()); + } else { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + } else { + ns->SetReverb(nullptr, 0); + } + mBuffer = aBuffer; +} + +void ConvolverNode::SetNormalize(bool aNormalize) { mNormalize = aNormalize; } + +} // namespace mozilla::dom |