diff options
Diffstat (limited to '')
430 files changed, 49011 insertions, 0 deletions
diff --git a/dom/media/webaudio/AlignedTArray.h b/dom/media/webaudio/AlignedTArray.h new file mode 100644 index 0000000000..28c0f094ef --- /dev/null +++ b/dom/media/webaudio/AlignedTArray.h @@ -0,0 +1,115 @@ +/* -*- 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/. */ + +#ifndef AlignedTArray_h__ +#define AlignedTArray_h__ + +#include "mozilla/Alignment.h" +#include "nsTArray.h" + +/** + * E: element type, must be a POD type. + * N: N bytes alignment for the first element, defaults to 32 + * S: S bytes of inline storage + */ +template <typename E, int S, int N = 32> +class AlignedAutoTArray : private AutoTArray<E, S + N> { + static_assert((N & (N - 1)) == 0, "N must be power of 2"); + typedef AutoTArray<E, S + N> base_type; + + public: + typedef E value_type; + typedef typename base_type::size_type size_type; + typedef typename base_type::index_type index_type; + + AlignedAutoTArray() = default; + explicit AlignedAutoTArray(size_type capacity) + : base_type(capacity + sExtra) {} + value_type* Elements() { return getAligned(base_type::Elements()); } + const value_type* Elements() const { + return getAligned(base_type::Elements()); + } + value_type& operator[](index_type i) { return Elements()[i]; } + const value_type& operator[](index_type i) const { return Elements()[i]; } + + void SetLength(size_type newLen) { base_type::SetLength(newLen + sExtra); } + + [[nodiscard]] bool SetLength(size_type newLen, const mozilla::fallible_t&) { + return base_type::SetLength(newLen + sExtra, mozilla::fallible); + } + + size_type Length() const { + return base_type::Length() <= sExtra ? 0 : base_type::Length() - sExtra; + } + + using base_type::ShallowSizeOfExcludingThis; + using base_type::ShallowSizeOfIncludingThis; + + private: + AlignedAutoTArray(const AlignedAutoTArray& other) = delete; + void operator=(const AlignedAutoTArray& other) = delete; + + static const size_type sPadding = + N <= MOZ_ALIGNOF(E) ? 0 : N - MOZ_ALIGNOF(E); + static const size_type sExtra = (sPadding + sizeof(E) - 1) / sizeof(E); + + template <typename U> + static U* getAligned(U* p) { + return reinterpret_cast<U*>(((uintptr_t)p + N - 1) & ~(N - 1)); + } +}; + +/** + * E: element type, must be a POD type. + * N: N bytes alignment for the first element, defaults to 32 + */ +template <typename E, int N = 32> +class AlignedTArray : private nsTArray_Impl<E, nsTArrayInfallibleAllocator> { + static_assert((N & (N - 1)) == 0, "N must be power of 2"); + typedef nsTArray_Impl<E, nsTArrayInfallibleAllocator> base_type; + + public: + typedef E value_type; + typedef typename base_type::size_type size_type; + typedef typename base_type::index_type index_type; + + AlignedTArray() = default; + explicit AlignedTArray(size_type capacity) : base_type(capacity + sExtra) {} + value_type* Elements() { return getAligned(base_type::Elements()); } + const value_type* Elements() const { + return getAligned(base_type::Elements()); + } + value_type& operator[](index_type i) { return Elements()[i]; } + const value_type& operator[](index_type i) const { return Elements()[i]; } + + void SetLength(size_type newLen) { base_type::SetLength(newLen + sExtra); } + + [[nodiscard]] bool SetLength(size_type newLen, const mozilla::fallible_t&) { + return base_type::SetLength(newLen + sExtra, mozilla::fallible); + } + + size_type Length() const { + return base_type::Length() <= sExtra ? 0 : base_type::Length() - sExtra; + } + + using base_type::ShallowSizeOfExcludingThis; + using base_type::ShallowSizeOfIncludingThis; + + private: + AlignedTArray(const AlignedTArray& other) = delete; + void operator=(const AlignedTArray& other) = delete; + + static const size_type sPadding = + N <= MOZ_ALIGNOF(E) ? 0 : N - MOZ_ALIGNOF(E); + static const size_type sExtra = (sPadding + sizeof(E) - 1) / sizeof(E); + + template <typename U> + static U* getAligned(U* p) { + return reinterpret_cast<U*>(((uintptr_t)p + N - 1) & ~(N - 1)); + } +}; + +#endif // AlignedTArray_h__ diff --git a/dom/media/webaudio/AlignmentUtils.h b/dom/media/webaudio/AlignmentUtils.h new file mode 100644 index 0000000000..c22dd4a58b --- /dev/null +++ b/dom/media/webaudio/AlignmentUtils.h @@ -0,0 +1,29 @@ +/* -*- 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/. */ + +#ifndef AlignmentUtils_h__ +#define AlignmentUtils_h__ + +#define IS_ALIGNED16(ptr) ((((uintptr_t)ptr + 15) & ~0x0F) == (uintptr_t)ptr) + +#ifdef DEBUG +# define ASSERT_ALIGNED16(ptr) \ + MOZ_ASSERT(IS_ALIGNED16(ptr), \ + #ptr " has to be aligned to a 16 byte boundary"); +#else +# define ASSERT_ALIGNED16(ptr) +#endif + +#ifdef DEBUG +# define ASSERT_MULTIPLE16(v) \ + MOZ_ASSERT(v % 16 == 0, #v " has to be a a multiple of 16"); +#else +# define ASSERT_MULTIPLE16(v) +#endif + +#define ALIGNED16(ptr) (float*)(((uintptr_t)ptr + 15) & ~0x0F); + +#endif diff --git a/dom/media/webaudio/AnalyserNode.cpp b/dom/media/webaudio/AnalyserNode.cpp new file mode 100644 index 0000000000..d8b547477e --- /dev/null +++ b/dom/media/webaudio/AnalyserNode.cpp @@ -0,0 +1,388 @@ +/* -*- 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 "mozilla/dom/AnalyserNode.h" +#include "mozilla/dom/AnalyserNodeBinding.h" +#include "AudioNodeEngine.h" +#include "AudioNodeTrack.h" +#include "mozilla/Mutex.h" +#include "mozilla/PodOperations.h" +#include "nsMathUtils.h" +#include "Tracing.h" + +namespace mozilla { + +static const uint32_t MAX_FFT_SIZE = 32768; +static const size_t CHUNK_COUNT = MAX_FFT_SIZE >> WEBAUDIO_BLOCK_SIZE_BITS; +static_assert(MAX_FFT_SIZE == CHUNK_COUNT * WEBAUDIO_BLOCK_SIZE, + "MAX_FFT_SIZE must be a multiple of WEBAUDIO_BLOCK_SIZE"); +static_assert((CHUNK_COUNT & (CHUNK_COUNT - 1)) == 0, + "CHUNK_COUNT must be power of 2 for remainder behavior"); + +namespace dom { + +class AnalyserNodeEngine final : public AudioNodeEngine { + class TransferBuffer final : public Runnable { + public: + TransferBuffer(AudioNodeTrack* aTrack, const AudioChunk& aChunk) + : Runnable("dom::AnalyserNodeEngine::TransferBuffer"), + mTrack(aTrack), + mChunk(aChunk) {} + + NS_IMETHOD Run() override { + RefPtr<AnalyserNode> node = + static_cast<AnalyserNode*>(mTrack->Engine()->NodeMainThread()); + if (node) { + node->AppendChunk(mChunk); + } + return NS_OK; + } + + private: + RefPtr<AudioNodeTrack> mTrack; + AudioChunk mChunk; + }; + + public: + explicit AnalyserNodeEngine(AnalyserNode* aNode) : AudioNodeEngine(aNode) { + MOZ_ASSERT(NS_IsMainThread()); + } + + virtual void ProcessBlock(AudioNodeTrack* aTrack, GraphTime aFrom, + const AudioBlock& aInput, AudioBlock* aOutput, + bool* aFinished) override { + TRACE("AnalyserNodeEngine::ProcessBlock"); + *aOutput = aInput; + + if (aInput.IsNull()) { + // If AnalyserNode::mChunks has only null chunks, then there is no need + // to send further null chunks. + if (mChunksToProcess == 0) { + return; + } + + --mChunksToProcess; + if (mChunksToProcess == 0) { + aTrack->ScheduleCheckForInactive(); + } + + } else { + // This many null chunks will be required to empty AnalyserNode::mChunks. + mChunksToProcess = CHUNK_COUNT; + } + + RefPtr<TransferBuffer> transfer = + new TransferBuffer(aTrack, aInput.AsAudioChunk()); + AbstractThread::MainThread()->Dispatch(transfer.forget()); + } + + virtual bool IsActive() const override { return mChunksToProcess != 0; } + + virtual size_t SizeOfIncludingThis( + MallocSizeOf aMallocSizeOf) const override { + return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf); + } + + uint32_t mChunksToProcess = 0; +}; + +/* static */ +already_AddRefed<AnalyserNode> AnalyserNode::Create( + AudioContext& aAudioContext, const AnalyserOptions& aOptions, + ErrorResult& aRv) { + RefPtr<AnalyserNode> analyserNode = new AnalyserNode(&aAudioContext); + + analyserNode->Initialize(aOptions, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + analyserNode->SetFftSize(aOptions.mFftSize, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + analyserNode->SetMinAndMaxDecibels(aOptions.mMinDecibels, + aOptions.mMaxDecibels, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + analyserNode->SetSmoothingTimeConstant(aOptions.mSmoothingTimeConstant, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + return analyserNode.forget(); +} + +AnalyserNode::AnalyserNode(AudioContext* aContext) + : AudioNode(aContext, 2, ChannelCountMode::Max, + ChannelInterpretation::Speakers), + mAnalysisBlock(2048), + mMinDecibels(-100.), + mMaxDecibels(-30.), + mSmoothingTimeConstant(.8) { + mTrack = + AudioNodeTrack::Create(aContext, new AnalyserNodeEngine(this), + AudioNodeTrack::NO_TRACK_FLAGS, aContext->Graph()); + + // Enough chunks must be recorded to handle the case of fftSize being + // increased to maximum immediately before getFloatTimeDomainData() is + // called, for example. + Unused << mChunks.SetLength(CHUNK_COUNT, fallible); + + AllocateBuffer(); +} + +size_t AnalyserNode::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const { + size_t amount = AudioNode::SizeOfExcludingThis(aMallocSizeOf); + amount += mAnalysisBlock.SizeOfExcludingThis(aMallocSizeOf); + amount += mChunks.ShallowSizeOfExcludingThis(aMallocSizeOf); + amount += mOutputBuffer.ShallowSizeOfExcludingThis(aMallocSizeOf); + return amount; +} + +size_t AnalyserNode::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const { + return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf); +} + +JSObject* AnalyserNode::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return AnalyserNode_Binding::Wrap(aCx, this, aGivenProto); +} + +void AnalyserNode::SetFftSize(uint32_t aValue, ErrorResult& aRv) { + // Disallow values that are not a power of 2 and outside the [32,32768] range + if (aValue < 32 || aValue > MAX_FFT_SIZE || (aValue & (aValue - 1)) != 0) { + aRv.ThrowIndexSizeError(nsPrintfCString( + "FFT size %u is not a power of two in the range 32 to 32768", aValue)); + return; + } + if (FftSize() != aValue) { + mAnalysisBlock.SetFFTSize(aValue); + AllocateBuffer(); + } +} + +void AnalyserNode::SetMinDecibels(double aValue, ErrorResult& aRv) { + if (aValue >= mMaxDecibels) { + aRv.ThrowIndexSizeError(nsPrintfCString( + "%g is not strictly smaller than current maxDecibels (%g)", aValue, + mMaxDecibels)); + return; + } + mMinDecibels = aValue; +} + +void AnalyserNode::SetMaxDecibels(double aValue, ErrorResult& aRv) { + if (aValue <= mMinDecibels) { + aRv.ThrowIndexSizeError(nsPrintfCString( + "%g is not strictly larger than current minDecibels (%g)", aValue, + mMinDecibels)); + return; + } + mMaxDecibels = aValue; +} + +void AnalyserNode::SetMinAndMaxDecibels(double aMinValue, double aMaxValue, + ErrorResult& aRv) { + if (aMinValue >= aMaxValue) { + aRv.ThrowIndexSizeError(nsPrintfCString( + "minDecibels value (%g) must be smaller than maxDecibels value (%g)", + aMinValue, aMaxValue)); + return; + } + mMinDecibels = aMinValue; + mMaxDecibels = aMaxValue; +} + +void AnalyserNode::SetSmoothingTimeConstant(double aValue, ErrorResult& aRv) { + if (aValue < 0 || aValue > 1) { + aRv.ThrowIndexSizeError( + nsPrintfCString("%g is not in the range [0, 1]", aValue)); + return; + } + mSmoothingTimeConstant = aValue; +} + +void AnalyserNode::GetFloatFrequencyData(const Float32Array& aArray) { + if (!FFTAnalysis()) { + // Might fail to allocate memory + return; + } + + aArray.ProcessData([&](const Span<float>& aData, JS::AutoCheckCannotGC&&) { + size_t length = std::min(size_t(aData.Length()), mOutputBuffer.Length()); + + for (size_t i = 0; i < length; ++i) { + aData[i] = WebAudioUtils::ConvertLinearToDecibels( + mOutputBuffer[i], -std::numeric_limits<float>::infinity()); + } + }); +} + +void AnalyserNode::GetByteFrequencyData(const Uint8Array& aArray) { + if (!FFTAnalysis()) { + // Might fail to allocate memory + return; + } + + const double rangeScaleFactor = 1.0 / (mMaxDecibels - mMinDecibels); + + aArray.ProcessData([&](const Span<uint8_t>& aData, JS::AutoCheckCannotGC&&) { + size_t length = std::min(size_t(aData.Length()), mOutputBuffer.Length()); + + for (size_t i = 0; i < length; ++i) { + const double decibels = WebAudioUtils::ConvertLinearToDecibels( + mOutputBuffer[i], mMinDecibels); + // scale down the value to the range of [0, UCHAR_MAX] + const double scaled = std::max( + 0.0, + std::min(double(UCHAR_MAX), + UCHAR_MAX * (decibels - mMinDecibels) * rangeScaleFactor)); + aData[i] = static_cast<unsigned char>(scaled); + } + }); +} + +void AnalyserNode::GetFloatTimeDomainData(const Float32Array& aArray) { + aArray.ProcessData([&](const Span<float>& aData, JS::AutoCheckCannotGC&&) { + size_t length = std::min(aData.Length(), size_t(FftSize())); + + GetTimeDomainData(aData.Elements(), length); + }); +} + +void AnalyserNode::GetByteTimeDomainData(const Uint8Array& aArray) { + aArray.ProcessData([&](const Span<uint8_t>& aData, JS::AutoCheckCannotGC&&) { + size_t length = std::min(aData.Length(), size_t(FftSize())); + + AlignedTArray<float> tmpBuffer; + if (!tmpBuffer.SetLength(length, fallible)) { + return; + } + + GetTimeDomainData(tmpBuffer.Elements(), length); + + unsigned char* buffer = aData.Elements(); + for (size_t i = 0; i < length; ++i) { + const float value = tmpBuffer[i]; + // scale the value to the range of [0, UCHAR_MAX] + const float scaled = + std::max(0.0f, std::min(float(UCHAR_MAX), 128.0f * (value + 1.0f))); + buffer[i] = static_cast<unsigned char>(scaled); + } + }); +} + +bool AnalyserNode::FFTAnalysis() { + AlignedTArray<float> tmpBuffer; + size_t fftSize = FftSize(); + if (!tmpBuffer.SetLength(fftSize, fallible)) { + return false; + } + + float* inputBuffer = tmpBuffer.Elements(); + GetTimeDomainData(inputBuffer, fftSize); + ApplyBlackmanWindow(inputBuffer, fftSize); + mAnalysisBlock.PerformFFT(inputBuffer); + + // Normalize so than an input sine wave at 0dBfs registers as 0dBfs (undo FFT + // scaling factor). + const double magnitudeScale = 1.0 / fftSize; + + for (uint32_t i = 0; i < mOutputBuffer.Length(); ++i) { + double scalarMagnitude = + fdlibm_hypot(mAnalysisBlock.RealData(i), mAnalysisBlock.ImagData(i)) * + magnitudeScale; + mOutputBuffer[i] = mSmoothingTimeConstant * mOutputBuffer[i] + + (1.0 - mSmoothingTimeConstant) * scalarMagnitude; + } + + return true; +} + +void AnalyserNode::ApplyBlackmanWindow(float* aBuffer, uint32_t aSize) { + double alpha = 0.16; + double a0 = 0.5 * (1.0 - alpha); + double a1 = 0.5; + double a2 = 0.5 * alpha; + + for (uint32_t i = 0; i < aSize; ++i) { + double x = double(i) / aSize; + double window = + a0 - a1 * fdlibm_cos(2 * M_PI * x) + a2 * fdlibm_cos(4 * M_PI * x); + aBuffer[i] *= window; + } +} + +bool AnalyserNode::AllocateBuffer() { + bool result = true; + if (mOutputBuffer.Length() != FrequencyBinCount()) { + if (!mOutputBuffer.SetLength(FrequencyBinCount(), fallible)) { + return false; + } + memset(mOutputBuffer.Elements(), 0, sizeof(float) * FrequencyBinCount()); + } + return result; +} + +void AnalyserNode::AppendChunk(const AudioChunk& aChunk) { + if (mChunks.Length() == 0) { + return; + } + + ++mCurrentChunk; + mChunks[mCurrentChunk & (CHUNK_COUNT - 1)] = aChunk; +} + +// Reads into aData the oldest aLength samples of the fftSize most recent +// samples. +void AnalyserNode::GetTimeDomainData(float* aData, size_t aLength) { + size_t fftSize = FftSize(); + MOZ_ASSERT(aLength <= fftSize); + + if (mChunks.Length() == 0) { + PodZero(aData, aLength); + return; + } + + size_t readChunk = + mCurrentChunk - ((fftSize - 1) >> WEBAUDIO_BLOCK_SIZE_BITS); + size_t readIndex = (0 - fftSize) & (WEBAUDIO_BLOCK_SIZE - 1); + MOZ_ASSERT(readIndex == 0 || readIndex + fftSize == WEBAUDIO_BLOCK_SIZE); + + for (size_t writeIndex = 0; writeIndex < aLength;) { + const AudioChunk& chunk = mChunks[readChunk & (CHUNK_COUNT - 1)]; + const size_t channelCount = chunk.ChannelCount(); + size_t copyLength = + std::min<size_t>(aLength - writeIndex, WEBAUDIO_BLOCK_SIZE); + float* dataOut = &aData[writeIndex]; + + if (channelCount == 0) { + PodZero(dataOut, copyLength); + } else { + float scale = chunk.mVolume / channelCount; + { // channel 0 + auto channelData = + static_cast<const float*>(chunk.mChannelData[0]) + readIndex; + AudioBufferCopyWithScale(channelData, scale, dataOut, copyLength); + } + for (uint32_t i = 1; i < channelCount; ++i) { + auto channelData = + static_cast<const float*>(chunk.mChannelData[i]) + readIndex; + AudioBufferAddWithScale(channelData, scale, dataOut, copyLength); + } + } + + readChunk++; + writeIndex += copyLength; + } +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/media/webaudio/AnalyserNode.h b/dom/media/webaudio/AnalyserNode.h new file mode 100644 index 0000000000..bdec13f6b9 --- /dev/null +++ b/dom/media/webaudio/AnalyserNode.h @@ -0,0 +1,82 @@ +/* -*- 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/. */ + +#ifndef AnalyserNode_h_ +#define AnalyserNode_h_ + +#include "AudioNode.h" +#include "FFTBlock.h" +#include "AlignedTArray.h" + +namespace mozilla::dom { + +class AudioContext; +struct AnalyserOptions; + +class AnalyserNode final : public AudioNode { + public: + static already_AddRefed<AnalyserNode> Create(AudioContext& aAudioContext, + const AnalyserOptions& aOptions, + ErrorResult& aRv); + + NS_INLINE_DECL_REFCOUNTING_INHERITED(AnalyserNode, AudioNode) + + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + static already_AddRefed<AnalyserNode> Constructor( + const GlobalObject& aGlobal, AudioContext& aAudioContext, + const AnalyserOptions& aOptions, ErrorResult& aRv) { + return Create(aAudioContext, aOptions, aRv); + } + + void GetFloatFrequencyData(const Float32Array& aArray); + void GetByteFrequencyData(const Uint8Array& aArray); + void GetFloatTimeDomainData(const Float32Array& aArray); + void GetByteTimeDomainData(const Uint8Array& aArray); + uint32_t FftSize() const { return mAnalysisBlock.FFTSize(); } + void SetFftSize(uint32_t aValue, ErrorResult& aRv); + uint32_t FrequencyBinCount() const { return FftSize() / 2; } + double MinDecibels() const { return mMinDecibels; } + void SetMinDecibels(double aValue, ErrorResult& aRv); + double MaxDecibels() const { return mMaxDecibels; } + void SetMaxDecibels(double aValue, ErrorResult& aRv); + double SmoothingTimeConstant() const { return mSmoothingTimeConstant; } + void SetSmoothingTimeConstant(double aValue, ErrorResult& aRv); + + virtual const char* NodeType() const override { return "AnalyserNode"; } + + virtual size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override; + virtual size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override; + + void SetMinAndMaxDecibels(double aMinValue, double aMaxValue, + ErrorResult& aRv); + + private: + ~AnalyserNode() = default; + + friend class AnalyserNodeEngine; + void AppendChunk(const AudioChunk& aChunk); + bool AllocateBuffer(); + bool FFTAnalysis(); + void ApplyBlackmanWindow(float* aBuffer, uint32_t aSize); + void GetTimeDomainData(float* aData, size_t aLength); + + private: + explicit AnalyserNode(AudioContext* aContext); + + FFTBlock mAnalysisBlock; + nsTArray<AudioChunk> mChunks; + double mMinDecibels; + double mMaxDecibels; + double mSmoothingTimeConstant; + size_t mCurrentChunk = 0; + AlignedTArray<float> mOutputBuffer; +}; + +} // namespace mozilla::dom + +#endif diff --git a/dom/media/webaudio/AudioBlock.cpp b/dom/media/webaudio/AudioBlock.cpp new file mode 100644 index 0000000000..09bbd7f70a --- /dev/null +++ b/dom/media/webaudio/AudioBlock.cpp @@ -0,0 +1,166 @@ +/* -*- 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 "AudioBlock.h" +#include "AlignmentUtils.h" + +namespace mozilla { + +/** + * Heap-allocated buffer of channels of 128-sample float arrays, with + * threadsafe refcounting. Typically you would allocate one of these, fill it + * in, and then treat it as immutable while it's shared. + * + * Downstream references are accounted specially so that the creator of the + * buffer can reuse and modify its contents next iteration if other references + * are all downstream temporary references held by AudioBlock. + * + * We guarantee 16 byte alignment of the channel data. + */ +class AudioBlockBuffer final : public ThreadSharedObject { + public: + virtual AudioBlockBuffer* AsAudioBlockBuffer() override { return this; }; + + uint32_t ChannelsAllocated() const { return mChannelsAllocated; } + float* ChannelData(uint32_t aChannel) const { + float* base = + reinterpret_cast<float*>(((uintptr_t)(this + 1) + 15) & ~0x0F); + ASSERT_ALIGNED16(base); + return base + aChannel * WEBAUDIO_BLOCK_SIZE; + } + + static already_AddRefed<AudioBlockBuffer> Create(uint32_t aChannelCount) { + CheckedInt<size_t> size = WEBAUDIO_BLOCK_SIZE; + size *= aChannelCount; + size *= sizeof(float); + size += sizeof(AudioBlockBuffer); + size += 15; // padding for alignment + if (!size.isValid()) { + MOZ_CRASH(); + } + + void* m = operator new(size.value()); + RefPtr<AudioBlockBuffer> p = new (m) AudioBlockBuffer(aChannelCount); + NS_ASSERTION((reinterpret_cast<char*>(p.get() + 1) - + reinterpret_cast<char*>(p.get())) % + 4 == + 0, + "AudioBlockBuffers should be at least 4-byte aligned"); + return p.forget(); + } + + // Graph thread only. + void DownstreamRefAdded() { ++mDownstreamRefCount; } + void DownstreamRefRemoved() { + MOZ_ASSERT(mDownstreamRefCount > 0); + --mDownstreamRefCount; + } + // Whether this is shared by any owners that are not downstream. + // Called only from owners with a reference that is not a downstream + // reference. Graph thread only. + bool HasLastingShares() const { + // mRefCnt is atomic and so reading its value is defined even when + // modifications may happen on other threads. mDownstreamRefCount is + // not modified on any other thread. + // + // If all other references are downstream references (managed on this, the + // graph thread), then other threads are not using this buffer and cannot + // add further references. This method can safely return false. The + // buffer contents can be modified. + // + // If there are other references that are not downstream references, then + // this method will return true. The buffer will be assumed to be still + // in use and so will not be reused. + nsrefcnt count = mRefCnt; + // This test is strictly less than because the caller has a reference + // that is not a downstream reference. + MOZ_ASSERT(mDownstreamRefCount < count); + return count != mDownstreamRefCount + 1; + } + + virtual size_t SizeOfIncludingThis( + MallocSizeOf aMallocSizeOf) const override { + return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf); + } + + private: + explicit AudioBlockBuffer(uint32_t aChannelsAllocated) + : mChannelsAllocated(aChannelsAllocated) {} + ~AudioBlockBuffer() override { MOZ_ASSERT(mDownstreamRefCount == 0); } + + nsAutoRefCnt mDownstreamRefCount; + const uint32_t mChannelsAllocated; +}; + +AudioBlock::~AudioBlock() { ClearDownstreamMark(); } + +void AudioBlock::SetBuffer(ThreadSharedObject* aNewBuffer) { + if (aNewBuffer == mBuffer) { + return; + } + + ClearDownstreamMark(); + + mBuffer = aNewBuffer; + + if (!aNewBuffer) { + return; + } + + AudioBlockBuffer* buffer = aNewBuffer->AsAudioBlockBuffer(); + if (buffer) { + buffer->DownstreamRefAdded(); + mBufferIsDownstreamRef = true; + } +} + +void AudioBlock::ClearDownstreamMark() { + if (mBufferIsDownstreamRef) { + mBuffer->AsAudioBlockBuffer()->DownstreamRefRemoved(); + mBufferIsDownstreamRef = false; + } +} + +bool AudioBlock::CanWrite() { + // If mBufferIsDownstreamRef is set then the buffer is not ours to use. + // It may be in use by another node which is not downstream. + return !mBufferIsDownstreamRef && + !mBuffer->AsAudioBlockBuffer()->HasLastingShares(); +} + +void AudioBlock::AllocateChannels(uint32_t aChannelCount) { + MOZ_ASSERT(mDuration == WEBAUDIO_BLOCK_SIZE); + + if (mBufferIsDownstreamRef) { + // This is not our buffer to re-use. + ClearDownstreamMark(); + } else if (mBuffer) { + AudioBlockBuffer* buffer = mBuffer->AsAudioBlockBuffer(); + if (buffer && !buffer->HasLastingShares() && + buffer->ChannelsAllocated() >= aChannelCount) { + MOZ_ASSERT(mBufferFormat == AUDIO_FORMAT_FLOAT32); + // No need to allocate again. + uint32_t previousChannelCount = ChannelCount(); + mChannelData.SetLength(aChannelCount); + for (uint32_t i = previousChannelCount; i < aChannelCount; ++i) { + mChannelData[i] = buffer->ChannelData(i); + } + mVolume = 1.0f; + return; + } + } + + RefPtr<AudioBlockBuffer> buffer = AudioBlockBuffer::Create(aChannelCount); + mChannelData.SetLength(aChannelCount); + for (uint32_t i = 0; i < aChannelCount; ++i) { + mChannelData[i] = buffer->ChannelData(i); + } + mBuffer = std::move(buffer); + mVolume = 1.0f; + mBufferFormat = AUDIO_FORMAT_FLOAT32; +} + +} // namespace mozilla diff --git a/dom/media/webaudio/AudioBlock.h b/dom/media/webaudio/AudioBlock.h new file mode 100644 index 0000000000..b5178c9d8a --- /dev/null +++ b/dom/media/webaudio/AudioBlock.h @@ -0,0 +1,134 @@ +/* -*- 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/. */ +#ifndef MOZILLA_AUDIOBLOCK_H_ +#define MOZILLA_AUDIOBLOCK_H_ + +#include "AudioSegment.h" + +namespace mozilla { + +/** + * An AudioChunk whose buffer contents need to be valid only for one + * processing block iteration, after which contents can be overwritten if the + * buffer has not been passed to longer term storage or to another thread, + * which may happen though AsAudioChunk() or AsMutableChunk(). + * + * Use on graph thread only. + */ +class AudioBlock : private AudioChunk { + public: + AudioBlock() { + mDuration = WEBAUDIO_BLOCK_SIZE; + mBufferFormat = AUDIO_FORMAT_SILENCE; + } + // No effort is made in constructors to ensure that mBufferIsDownstreamRef + // is set because the block is expected to be a temporary and so the + // reference will be released before the next iteration. + // The custom copy constructor is required so as not to set + // mBufferIsDownstreamRef without notifying AudioBlockBuffer. + AudioBlock(const AudioBlock& aBlock) : AudioChunk(aBlock.AsAudioChunk()) {} + explicit AudioBlock(const AudioChunk& aChunk) : AudioChunk(aChunk) { + MOZ_ASSERT(aChunk.mDuration == WEBAUDIO_BLOCK_SIZE); + } + ~AudioBlock(); + + using AudioChunk::ChannelCount; + using AudioChunk::ChannelData; + using AudioChunk::GetDuration; + using AudioChunk::IsNull; + using AudioChunk::SizeOfExcludingThis; + using AudioChunk::SizeOfExcludingThisIfUnshared; + // mDuration is not exposed. Use GetDuration(). + // mBuffer is not exposed. Use Get/SetBuffer(). + using AudioChunk::mBufferFormat; + using AudioChunk::mChannelData; + using AudioChunk::mVolume; + + const AudioChunk& AsAudioChunk() const { return *this; } + AudioChunk* AsMutableChunk() { + ClearDownstreamMark(); + return this; + } + + /** + * Allocates, if necessary, aChannelCount buffers of WEBAUDIO_BLOCK_SIZE float + * samples for writing. + */ + void AllocateChannels(uint32_t aChannelCount); + + /** + * ChannelFloatsForWrite() should only be used when the buffers have been + * created with AllocateChannels(). + */ + float* ChannelFloatsForWrite(size_t aChannel) { + MOZ_ASSERT(mBufferFormat == AUDIO_FORMAT_FLOAT32); + MOZ_ASSERT(CanWrite()); + return static_cast<float*>(const_cast<void*>(mChannelData[aChannel])); + } + + ThreadSharedObject* GetBuffer() const { return mBuffer; } + void SetBuffer(ThreadSharedObject* aNewBuffer); + void SetNull(TrackTime aDuration) { + MOZ_ASSERT(aDuration == WEBAUDIO_BLOCK_SIZE); + SetBuffer(nullptr); + mChannelData.Clear(); + mVolume = 1.0f; + mBufferFormat = AUDIO_FORMAT_SILENCE; + } + + AudioBlock& operator=(const AudioBlock& aBlock) { + // Instead of just copying, mBufferIsDownstreamRef must be first cleared + // if set. It is set again for the new mBuffer if possible. This happens + // in SetBuffer(). + return *this = aBlock.AsAudioChunk(); + } + AudioBlock& operator=(const AudioChunk& aChunk) { + MOZ_ASSERT(aChunk.mDuration == WEBAUDIO_BLOCK_SIZE); + SetBuffer(aChunk.mBuffer); + mChannelData = aChunk.mChannelData; + mVolume = aChunk.mVolume; + mBufferFormat = aChunk.mBufferFormat; + return *this; + } + + bool IsMuted() const { return mVolume == 0.0f; } + + bool IsSilentOrSubnormal() const { + if (!mBuffer) { + return true; + } + + for (uint32_t i = 0, length = mChannelData.Length(); i < length; ++i) { + const float* channel = static_cast<const float*>(mChannelData[i]); + for (TrackTime frame = 0; frame < mDuration; ++frame) { + if (fabs(channel[frame]) >= FLT_MIN) { + return false; + } + } + } + + return true; + } + + private: + void ClearDownstreamMark(); + bool CanWrite(); + + // mBufferIsDownstreamRef is set only when mBuffer references an + // AudioBlockBuffer created in a different AudioBlock. That can happen when + // this AudioBlock is on a node downstream from the node which created the + // buffer. When this is set, the AudioBlockBuffer is notified that this + // reference does not prevent the upstream node from re-using the buffer next + // iteration and modifying its contents. The AudioBlockBuffer is also + // notified when mBuffer releases this reference. + bool mBufferIsDownstreamRef = false; +}; + +} // namespace mozilla + +MOZ_DECLARE_RELOCATE_USING_MOVE_CONSTRUCTOR(mozilla::AudioBlock) + +#endif // MOZILLA_AUDIOBLOCK_H_ diff --git a/dom/media/webaudio/AudioBuffer.cpp b/dom/media/webaudio/AudioBuffer.cpp new file mode 100644 index 0000000000..8c2a36fe45 --- /dev/null +++ b/dom/media/webaudio/AudioBuffer.cpp @@ -0,0 +1,498 @@ +/* -*- 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 "AudioBuffer.h" +#include "mozilla/dom/AudioBufferBinding.h" +#include "jsfriendapi.h" +#include "js/ArrayBuffer.h" // JS::StealArrayBufferContents +#include "js/experimental/TypedData.h" // JS_NewFloat32Array, JS_GetFloat32ArrayData, JS_GetTypedArrayLength, JS_GetArrayBufferViewBuffer +#include "mozilla/ErrorResult.h" +#include "AudioSegment.h" +#include "AudioChannelFormat.h" +#include "mozilla/PodOperations.h" +#include "mozilla/CheckedInt.h" +#include "mozilla/HoldDropJSObjects.h" +#include "mozilla/MemoryReporting.h" +#include "AudioNodeEngine.h" +#include "nsPrintfCString.h" +#include "nsTHashSet.h" +#include <numeric> + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_CLASS(AudioBuffer) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(AudioBuffer) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mJSChannels) + NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER + tmp->ClearJSChannels(); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(AudioBuffer) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(AudioBuffer) + NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER + for (uint32_t i = 0; i < tmp->mJSChannels.Length(); ++i) { + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mJSChannels[i]) + } +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +/** + * AudioBuffers can be shared between AudioContexts, so we need a separate + * mechanism to track their memory usage. This thread-safe class keeps track of + * all the AudioBuffers, and gets called back by the memory reporting system + * when a memory report is needed, reporting how much memory is used by the + * buffers backing AudioBuffer objects. */ +class AudioBufferMemoryTracker : public nsIMemoryReporter { + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIMEMORYREPORTER + + private: + AudioBufferMemoryTracker(); + virtual ~AudioBufferMemoryTracker(); + + public: + /* Those methods can be called on any thread. */ + static void RegisterAudioBuffer(const AudioBuffer* aAudioBuffer); + static void UnregisterAudioBuffer(const AudioBuffer* aAudioBuffer); + + private: + static AudioBufferMemoryTracker* GetInstance(); + /* Those methods must be called with the lock held. */ + void RegisterAudioBufferInternal(const AudioBuffer* aAudioBuffer); + /* Returns the number of buffers still present in the hash table. */ + uint32_t UnregisterAudioBufferInternal(const AudioBuffer* aAudioBuffer); + void Init(); + + /* This protects all members of this class. */ + static StaticMutex sMutex MOZ_UNANNOTATED; + static StaticRefPtr<AudioBufferMemoryTracker> sSingleton; + nsTHashSet<const AudioBuffer*> mBuffers; +}; + +StaticRefPtr<AudioBufferMemoryTracker> AudioBufferMemoryTracker::sSingleton; +StaticMutex AudioBufferMemoryTracker::sMutex; + +NS_IMPL_ISUPPORTS(AudioBufferMemoryTracker, nsIMemoryReporter); + +AudioBufferMemoryTracker* AudioBufferMemoryTracker::GetInstance() { + sMutex.AssertCurrentThreadOwns(); + if (!sSingleton) { + sSingleton = new AudioBufferMemoryTracker(); + sSingleton->Init(); + } + return sSingleton; +} + +AudioBufferMemoryTracker::AudioBufferMemoryTracker() = default; + +void AudioBufferMemoryTracker::Init() { RegisterWeakMemoryReporter(this); } + +AudioBufferMemoryTracker::~AudioBufferMemoryTracker() { + UnregisterWeakMemoryReporter(this); +} + +void AudioBufferMemoryTracker::RegisterAudioBuffer( + const AudioBuffer* aAudioBuffer) { + StaticMutexAutoLock lock(sMutex); + AudioBufferMemoryTracker* tracker = AudioBufferMemoryTracker::GetInstance(); + tracker->RegisterAudioBufferInternal(aAudioBuffer); +} + +void AudioBufferMemoryTracker::UnregisterAudioBuffer( + const AudioBuffer* aAudioBuffer) { + StaticMutexAutoLock lock(sMutex); + AudioBufferMemoryTracker* tracker = AudioBufferMemoryTracker::GetInstance(); + uint32_t count; + count = tracker->UnregisterAudioBufferInternal(aAudioBuffer); + if (count == 0) { + sSingleton = nullptr; + } +} + +void AudioBufferMemoryTracker::RegisterAudioBufferInternal( + const AudioBuffer* aAudioBuffer) { + sMutex.AssertCurrentThreadOwns(); + mBuffers.Insert(aAudioBuffer); +} + +uint32_t AudioBufferMemoryTracker::UnregisterAudioBufferInternal( + const AudioBuffer* aAudioBuffer) { + sMutex.AssertCurrentThreadOwns(); + mBuffers.Remove(aAudioBuffer); + return mBuffers.Count(); +} + +MOZ_DEFINE_MALLOC_SIZE_OF(AudioBufferMemoryTrackerMallocSizeOf) + +NS_IMETHODIMP +AudioBufferMemoryTracker::CollectReports(nsIHandleReportCallback* aHandleReport, + nsISupports* aData, bool) { + StaticMutexAutoLock lock(sMutex); + const size_t amount = + std::accumulate(mBuffers.cbegin(), mBuffers.cend(), size_t(0), + [](size_t val, const AudioBuffer* buffer) { + return val + buffer->SizeOfIncludingThis( + AudioBufferMemoryTrackerMallocSizeOf); + }); + + MOZ_COLLECT_REPORT("explicit/webaudio/audiobuffer", KIND_HEAP, UNITS_BYTES, + amount, "Memory used by AudioBuffer objects (Web Audio)."); + + return NS_OK; +} + +AudioBuffer::AudioBuffer(nsPIDOMWindowInner* aWindow, + uint32_t aNumberOfChannels, uint32_t aLength, + float aSampleRate, ErrorResult& aRv) + : mOwnerWindow(do_GetWeakReference(aWindow)), mSampleRate(aSampleRate) { + // Note that a buffer with zero channels is permitted here for the sake of + // AudioProcessingEvent, where channel counts must match parameters passed + // to createScriptProcessor(), one of which may be zero. + if (aSampleRate < WebAudioUtils::MinSampleRate || + aSampleRate > WebAudioUtils::MaxSampleRate) { + aRv.ThrowNotSupportedError( + nsPrintfCString("Sample rate (%g) is out of range", aSampleRate)); + return; + } + + if (aNumberOfChannels > WebAudioUtils::MaxChannelCount) { + aRv.ThrowNotSupportedError(nsPrintfCString( + "Number of channels (%u) is out of range", aNumberOfChannels)); + return; + } + + if (!aLength || aLength > INT32_MAX) { + aRv.ThrowNotSupportedError( + nsPrintfCString("Length (%u) is out of range", aLength)); + return; + } + + mSharedChannels.mDuration = aLength; + mJSChannels.SetLength(aNumberOfChannels); + mozilla::HoldJSObjects(this); + AudioBufferMemoryTracker::RegisterAudioBuffer(this); +} + +AudioBuffer::~AudioBuffer() { + AudioBufferMemoryTracker::UnregisterAudioBuffer(this); + ClearJSChannels(); + mozilla::DropJSObjects(this); +} + +/* static */ +already_AddRefed<AudioBuffer> AudioBuffer::Constructor( + const GlobalObject& aGlobal, const AudioBufferOptions& aOptions, + ErrorResult& aRv) { + if (!aOptions.mNumberOfChannels) { + aRv.ThrowNotSupportedError("Must have nonzero number of channels"); + return nullptr; + } + + nsCOMPtr<nsPIDOMWindowInner> window = + do_QueryInterface(aGlobal.GetAsSupports()); + + return Create(window, aOptions.mNumberOfChannels, aOptions.mLength, + aOptions.mSampleRate, aRv); +} + +void AudioBuffer::ClearJSChannels() { mJSChannels.Clear(); } + +void AudioBuffer::SetSharedChannels( + already_AddRefed<ThreadSharedFloatArrayBufferList> aBuffer) { + RefPtr<ThreadSharedFloatArrayBufferList> buffer = aBuffer; + uint32_t channelCount = buffer->GetChannels(); + mSharedChannels.mChannelData.SetLength(channelCount); + for (uint32_t i = 0; i < channelCount; ++i) { + mSharedChannels.mChannelData[i] = buffer->GetData(i); + } + mSharedChannels.mBuffer = std::move(buffer); + mSharedChannels.mBufferFormat = AUDIO_FORMAT_FLOAT32; +} + +/* static */ +already_AddRefed<AudioBuffer> AudioBuffer::Create( + nsPIDOMWindowInner* aWindow, uint32_t aNumberOfChannels, uint32_t aLength, + float aSampleRate, + already_AddRefed<ThreadSharedFloatArrayBufferList> aInitialContents, + ErrorResult& aRv) { + RefPtr<ThreadSharedFloatArrayBufferList> initialContents = aInitialContents; + RefPtr<AudioBuffer> buffer = + new AudioBuffer(aWindow, aNumberOfChannels, aLength, aSampleRate, aRv); + if (aRv.Failed()) { + return nullptr; + } + + if (initialContents) { + MOZ_ASSERT(initialContents->GetChannels() == aNumberOfChannels); + buffer->SetSharedChannels(initialContents.forget()); + } + + return buffer.forget(); +} + +/* static */ +already_AddRefed<AudioBuffer> AudioBuffer::Create( + nsPIDOMWindowInner* aWindow, float aSampleRate, + AudioChunk&& aInitialContents) { + AudioChunk initialContents = aInitialContents; + ErrorResult rv; + RefPtr<AudioBuffer> buffer = + new AudioBuffer(aWindow, initialContents.ChannelCount(), + initialContents.mDuration, aSampleRate, rv); + if (rv.Failed()) { + return nullptr; + } + buffer->mSharedChannels = std::move(aInitialContents); + + return buffer.forget(); +} + +JSObject* AudioBuffer::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return AudioBuffer_Binding::Wrap(aCx, this, aGivenProto); +} + +static void CopyChannelDataToFloat(const AudioChunk& aChunk, uint32_t aChannel, + uint32_t aSrcOffset, float* aOutput, + uint32_t aLength) { + MOZ_ASSERT(aChunk.mVolume == 1.0f); + if (aChunk.mBufferFormat == AUDIO_FORMAT_FLOAT32) { + mozilla::PodCopy( + aOutput, aChunk.ChannelData<float>()[aChannel] + aSrcOffset, aLength); + } else { + MOZ_ASSERT(aChunk.mBufferFormat == AUDIO_FORMAT_S16); + ConvertAudioSamples(aChunk.ChannelData<int16_t>()[aChannel] + aSrcOffset, + aOutput, aLength); + } +} + +bool AudioBuffer::RestoreJSChannelData(JSContext* aJSContext) { + nsPIDOMWindowInner* global = GetParentObject(); + if (!global || !global->AsGlobal()->HasJSGlobal()) { + return false; + } + + JSAutoRealm ar(aJSContext, global->AsGlobal()->GetGlobalJSObject()); + + for (uint32_t i = 0; i < mJSChannels.Length(); ++i) { + if (mJSChannels[i]) { + // Already have data in JS array. + continue; + } + + // The following code first zeroes the array and then copies our data + // into it. We could avoid this with additional JS APIs to construct + // an array (or ArrayBuffer) containing initial data. + JS::Rooted<JSObject*> array(aJSContext, + JS_NewFloat32Array(aJSContext, Length())); + if (!array) { + return false; + } + if (!mSharedChannels.IsNull()) { + // "4. Attach ArrayBuffers containing copies of the data to the + // AudioBuffer, to be returned by the next call to getChannelData." + JS::AutoCheckCannotGC nogc; + bool isShared; + float* jsData = JS_GetFloat32ArrayData(array, &isShared, nogc); + MOZ_ASSERT(!isShared); // Was created as unshared above + CopyChannelDataToFloat(mSharedChannels, i, 0, jsData, Length()); + } + mJSChannels[i] = array; + } + + mSharedChannels.SetNull(Length()); + + return true; +} + +void AudioBuffer::CopyFromChannel(const Float32Array& aDestination, + uint32_t aChannelNumber, + uint32_t aBufferOffset, ErrorResult& aRv) { + if (aChannelNumber >= NumberOfChannels()) { + aRv.ThrowIndexSizeError( + nsPrintfCString("Channel number (%u) is out of range", aChannelNumber)); + return; + } + uint32_t length = Length(); + if (aBufferOffset >= length) { + return; + } + JS::AutoCheckCannotGC nogc; + MOZ_RELEASE_ASSERT(!JS_GetTypedArraySharedness(aDestination.Obj())); + auto calculateCount = [=](uint32_t aLength) -> uint32_t { + return std::min(length - aBufferOffset, aLength); + }; + + JSObject* channelArray = mJSChannels[aChannelNumber]; + if (channelArray) { + if (JS_GetTypedArrayLength(channelArray) != length) { + // The array's buffer was detached. + return; + } + bool isShared = false; + const float* sourceData = + JS_GetFloat32ArrayData(channelArray, &isShared, nogc); + // The sourceData arrays should all have originated in + // RestoreJSChannelData, where they are created unshared. + MOZ_ASSERT(!isShared); + aDestination.ProcessData( + [&](const Span<float>& aData, JS::AutoCheckCannotGC&&) { + PodMove(aData.Elements(), sourceData + aBufferOffset, + calculateCount(aData.Length())); + }); + return; + } + + if (!mSharedChannels.IsNull()) { + aDestination.ProcessData([&](const Span<float>& aData, + JS::AutoCheckCannotGC&&) { + CopyChannelDataToFloat(mSharedChannels, aChannelNumber, aBufferOffset, + aData.Elements(), calculateCount(aData.Length())); + }); + return; + } + + aDestination.ProcessData( + [&](const Span<float>& aData, JS::AutoCheckCannotGC&&) { + PodZero(aData.Elements(), calculateCount(aData.Length())); + }); +} + +void AudioBuffer::CopyToChannel(JSContext* aJSContext, + const Float32Array& aSource, + uint32_t aChannelNumber, uint32_t aBufferOffset, + ErrorResult& aRv) { + if (aChannelNumber >= NumberOfChannels()) { + aRv.ThrowIndexSizeError( + nsPrintfCString("Channel number (%u) is out of range", aChannelNumber)); + return; + } + + if (!RestoreJSChannelData(aJSContext)) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + JS::AutoCheckCannotGC nogc; + JSObject* channelArray = mJSChannels[aChannelNumber]; + // This may differ from Length() if the buffer has been detached. + uint32_t length = JS_GetTypedArrayLength(channelArray); + if (aBufferOffset >= length) { + return; + } + + int64_t offset = aBufferOffset; + aSource.ProcessData([&](const Span<float>& aData, JS::AutoCheckCannotGC&&) { + MOZ_ASSERT_IF(std::numeric_limits<decltype(aData.Length())>::max() > + std::numeric_limits<int64_t>::max(), + aData.Length() <= std::numeric_limits<int64_t>::max()); + int64_t srcLength = int64_t(aData.Length()); + size_t count = std::max(int64_t(0), std::min(length - offset, srcLength)); + bool isShared = false; + float* channelData = JS_GetFloat32ArrayData(channelArray, &isShared, nogc); + // The channelData arrays should all have originated in + // RestoreJSChannelData, where they are created unshared. + MOZ_ASSERT(!isShared); + PodMove(channelData + aBufferOffset, aData.Elements(), count); + }); +} + +void AudioBuffer::GetChannelData(JSContext* aJSContext, uint32_t aChannel, + JS::MutableHandle<JSObject*> aRetval, + ErrorResult& aRv) { + if (aChannel >= NumberOfChannels()) { + aRv.ThrowIndexSizeError( + nsPrintfCString("Channel number (%u) is out of range", aChannel)); + return; + } + + if (!RestoreJSChannelData(aJSContext)) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + aRetval.set(mJSChannels[aChannel]); +} + +already_AddRefed<ThreadSharedFloatArrayBufferList> +AudioBuffer::StealJSArrayDataIntoSharedChannels(JSContext* aJSContext) { + nsPIDOMWindowInner* global = GetParentObject(); + if (!global || !global->AsGlobal()->HasJSGlobal()) { + return nullptr; + } + + JSAutoRealm ar(aJSContext, global->AsGlobal()->GetGlobalJSObject()); + + // "1. If any of the AudioBuffer's ArrayBuffer have been detached, abort + // these steps, and return a zero-length channel data buffers to the + // invoker." + for (uint32_t i = 0; i < mJSChannels.Length(); ++i) { + JSObject* channelArray = mJSChannels[i]; + if (!channelArray || Length() != JS_GetTypedArrayLength(channelArray)) { + // Either empty buffer or one of the arrays' buffers was detached. + return nullptr; + } + } + + // "2. Detach all ArrayBuffers for arrays previously returned by + // getChannelData on this AudioBuffer." + // "3. Retain the underlying data buffers from those ArrayBuffers and return + // references to them to the invoker." + RefPtr<ThreadSharedFloatArrayBufferList> result = + new ThreadSharedFloatArrayBufferList(mJSChannels.Length()); + for (uint32_t i = 0; i < mJSChannels.Length(); ++i) { + JS::Rooted<JSObject*> arrayBufferView(aJSContext, mJSChannels[i]); + bool isSharedMemory; + JS::Rooted<JSObject*> arrayBuffer( + aJSContext, JS_GetArrayBufferViewBuffer(aJSContext, arrayBufferView, + &isSharedMemory)); + // The channel data arrays should all have originated in + // RestoreJSChannelData, where they are created unshared. + MOZ_ASSERT(!isSharedMemory); + auto stolenData = arrayBuffer + ? static_cast<float*>(JS::StealArrayBufferContents( + aJSContext, arrayBuffer)) + : nullptr; + if (stolenData) { + result->SetData(i, stolenData, js_free, stolenData); + } else { + NS_ASSERTION(i == 0, "some channels lost when contents not acquired"); + return nullptr; + } + } + + for (uint32_t i = 0; i < mJSChannels.Length(); ++i) { + mJSChannels[i] = nullptr; + } + + return result.forget(); +} + +const AudioChunk& AudioBuffer::GetThreadSharedChannelsForRate( + JSContext* aJSContext) { + if (mSharedChannels.IsNull()) { + // mDuration is set in constructor + RefPtr<ThreadSharedFloatArrayBufferList> buffer = + StealJSArrayDataIntoSharedChannels(aJSContext); + + if (buffer) { + SetSharedChannels(buffer.forget()); + } + } + + return mSharedChannels; +} + +size_t AudioBuffer::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const { + size_t amount = aMallocSizeOf(this); + amount += mJSChannels.ShallowSizeOfExcludingThis(aMallocSizeOf); + amount += mSharedChannels.SizeOfExcludingThis(aMallocSizeOf, false); + return amount; +} + +} // namespace mozilla::dom diff --git a/dom/media/webaudio/AudioBuffer.h b/dom/media/webaudio/AudioBuffer.h new file mode 100644 index 0000000000..e313c5c944 --- /dev/null +++ b/dom/media/webaudio/AudioBuffer.h @@ -0,0 +1,138 @@ +/* -*- 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/. */ + +#ifndef AudioBuffer_h_ +#define AudioBuffer_h_ + +#include "AudioSegment.h" +#include "nsWrapperCache.h" +#include "nsCycleCollectionParticipant.h" +#include "mozilla/Attributes.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/StaticMutex.h" +#include "nsTArray.h" +#include "js/TypeDecls.h" +#include "mozilla/MemoryReporting.h" +#include "mozilla/dom/TypedArray.h" +#include "nsPIDOMWindow.h" +#include "nsIWeakReferenceUtils.h" + +namespace mozilla { + +class ErrorResult; +class ThreadSharedFloatArrayBufferList; + +namespace dom { + +struct AudioBufferOptions; + +/** + * An AudioBuffer keeps its data either in the mJSChannels objects, which + * are Float32Arrays, or in mSharedChannels if the mJSChannels objects' buffers + * are detached. + */ +class AudioBuffer final : public nsWrapperCache { + public: + // If non-null, aInitialContents must have number of channels equal to + // aNumberOfChannels and their lengths must be at least aLength. + static already_AddRefed<AudioBuffer> Create( + nsPIDOMWindowInner* aWindow, uint32_t aNumberOfChannels, uint32_t aLength, + float aSampleRate, + already_AddRefed<ThreadSharedFloatArrayBufferList> aInitialContents, + ErrorResult& aRv); + + static already_AddRefed<AudioBuffer> Create(nsPIDOMWindowInner* aWindow, + uint32_t aNumberOfChannels, + uint32_t aLength, + float aSampleRate, + ErrorResult& aRv) { + return Create(aWindow, aNumberOfChannels, aLength, aSampleRate, nullptr, + aRv); + } + + // Non-unit AudioChunk::mVolume is not supported + static already_AddRefed<AudioBuffer> Create(nsPIDOMWindowInner* aWindow, + float aSampleRate, + AudioChunk&& aInitialContents); + + size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const; + + NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(AudioBuffer) + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_NATIVE_CLASS(AudioBuffer) + + static already_AddRefed<AudioBuffer> Constructor( + const GlobalObject& aGlobal, const AudioBufferOptions& aOptions, + ErrorResult& aRv); + + nsPIDOMWindowInner* GetParentObject() const { + nsCOMPtr<nsPIDOMWindowInner> parentObject = do_QueryReferent(mOwnerWindow); + return parentObject; + } + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + float SampleRate() const { return mSampleRate; } + + uint32_t Length() const { return mSharedChannels.mDuration; } + + double Duration() const { + return Length() / static_cast<double>(mSampleRate); + } + + uint32_t NumberOfChannels() const { return mJSChannels.Length(); } + + /** + * If mSharedChannels is non-null, copies its contents to + * new Float32Arrays in mJSChannels. Returns a Float32Array. + */ + void GetChannelData(JSContext* aJSContext, uint32_t aChannel, + JS::MutableHandle<JSObject*> aRetval, ErrorResult& aRv); + + void CopyFromChannel(const Float32Array& aDestination, + uint32_t aChannelNumber, uint32_t aBufferOffset, + ErrorResult& aRv); + void CopyToChannel(JSContext* aJSContext, const Float32Array& aSource, + uint32_t aChannelNumber, uint32_t aBufferOffset, + ErrorResult& aRv); + + /** + * Returns a reference to an AudioChunk containing the sample data. + * The AudioChunk can have a null buffer if there is no data. + */ + const AudioChunk& GetThreadSharedChannelsForRate(JSContext* aContext); + + protected: + AudioBuffer(nsPIDOMWindowInner* aWindow, uint32_t aNumberOfChannels, + uint32_t aLength, float aSampleRate, ErrorResult& aRv); + ~AudioBuffer(); + + void SetSharedChannels( + already_AddRefed<ThreadSharedFloatArrayBufferList> aBuffer); + + bool RestoreJSChannelData(JSContext* aJSContext); + + already_AddRefed<ThreadSharedFloatArrayBufferList> + StealJSArrayDataIntoSharedChannels(JSContext* aJSContext); + + void ClearJSChannels(); + + // Float32Arrays + AutoTArray<JS::Heap<JSObject*>, 2> mJSChannels; + // mSharedChannels aggregates the data from mJSChannels. This is non-null + // if and only if the mJSChannels' buffers are detached, but its mDuration + // member keeps the buffer length regardless of whether the buffer is + // provided by mJSChannels or mSharedChannels. + AudioChunk mSharedChannels; + + nsWeakPtr mOwnerWindow; + float mSampleRate; +}; + +} // namespace dom +} // namespace mozilla + +#endif diff --git a/dom/media/webaudio/AudioBufferSourceNode.cpp b/dom/media/webaudio/AudioBufferSourceNode.cpp new file mode 100644 index 0000000000..db34c49f48 --- /dev/null +++ b/dom/media/webaudio/AudioBufferSourceNode.cpp @@ -0,0 +1,853 @@ +/* -*- 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 "AudioBufferSourceNode.h" +#include "nsDebug.h" +#include "mozilla/dom/AudioBufferSourceNodeBinding.h" +#include "mozilla/dom/AudioParam.h" +#include "mozilla/FloatingPoint.h" +#include "nsContentUtils.h" +#include "nsMathUtils.h" +#include "AlignmentUtils.h" +#include "AudioNodeEngine.h" +#include "AudioNodeTrack.h" +#include "AudioDestinationNode.h" +#include "AudioParamTimeline.h" +#include <limits> +#include <algorithm> +#include "Tracing.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_INHERITED(AudioBufferSourceNode, + AudioScheduledSourceNode, mBuffer, + mPlaybackRate, mDetune) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(AudioBufferSourceNode) +NS_INTERFACE_MAP_END_INHERITING(AudioScheduledSourceNode) + +NS_IMPL_ADDREF_INHERITED(AudioBufferSourceNode, AudioScheduledSourceNode) +NS_IMPL_RELEASE_INHERITED(AudioBufferSourceNode, AudioScheduledSourceNode) + +/** + * Media-thread playback engine for AudioBufferSourceNode. + * Nothing is played until a non-null buffer has been set (via + * AudioNodeTrack::SetBuffer) and a non-zero mBufferSampleRate has been set + * (via AudioNodeTrack::SetInt32Parameter) + */ +class AudioBufferSourceNodeEngine final : public AudioNodeEngine { + public: + AudioBufferSourceNodeEngine(AudioNode* aNode, + AudioDestinationNode* aDestination) + : AudioNodeEngine(aNode), + mStart(0.0), + mBeginProcessing(0), + mStop(TRACK_TIME_MAX), + mResampler(nullptr), + mRemainingResamplerTail(0), + mRemainingFrames(TRACK_TICKS_MAX), + mLoopStart(0), + mLoopEnd(0), + mBufferPosition(0), + mBufferSampleRate(0), + // mResamplerOutRate is initialized in UpdateResampler(). + mChannels(0), + mDestination(aDestination->Track()), + mPlaybackRateTimeline(1.0f), + mDetuneTimeline(0.0f), + mLoop(false) {} + + ~AudioBufferSourceNodeEngine() { + if (mResampler) { + speex_resampler_destroy(mResampler); + } + } + + void SetSourceTrack(AudioNodeTrack* aSource) { mSource = aSource; } + + void RecvTimelineEvent(uint32_t aIndex, AudioParamEvent& aEvent) override { + MOZ_ASSERT(mDestination); + aEvent.ConvertToTicks(mDestination); + mRecomputeOutRate = true; + + switch (aIndex) { + case AudioBufferSourceNode::PLAYBACKRATE: + mPlaybackRateTimeline.InsertEvent<int64_t>(aEvent); + break; + case AudioBufferSourceNode::DETUNE: + mDetuneTimeline.InsertEvent<int64_t>(aEvent); + break; + default: + NS_ERROR("Bad AudioBufferSourceNodeEngine TimelineParameter"); + } + } + void SetTrackTimeParameter(uint32_t aIndex, TrackTime aParam) override { + switch (aIndex) { + case AudioBufferSourceNode::STOP: + mStop = aParam; + break; + default: + NS_ERROR("Bad AudioBufferSourceNodeEngine TrackTimeParameter"); + } + } + void SetDoubleParameter(uint32_t aIndex, double aParam) override { + switch (aIndex) { + case AudioBufferSourceNode::START: + MOZ_ASSERT(!mStart, "Another START?"); + mStart = aParam * mDestination->mSampleRate; + // Round to nearest + mBeginProcessing = llround(mStart); + break; + case AudioBufferSourceNode::DURATION: + MOZ_ASSERT(aParam >= 0); + mRemainingFrames = llround(aParam * mBufferSampleRate); + break; + default: + NS_ERROR("Bad AudioBufferSourceNodeEngine double parameter."); + }; + } + void SetInt32Parameter(uint32_t aIndex, int32_t aParam) override { + switch (aIndex) { + case AudioBufferSourceNode::SAMPLE_RATE: + MOZ_ASSERT(aParam > 0); + MOZ_ASSERT(mRecomputeOutRate); + mBufferSampleRate = aParam; + mSource->SetActive(); + break; + case AudioBufferSourceNode::BUFFERSTART: + MOZ_ASSERT(aParam >= 0); + if (mBufferPosition == 0) { + mBufferPosition = aParam; + } + break; + case AudioBufferSourceNode::LOOP: + mLoop = !!aParam; + break; + case AudioBufferSourceNode::LOOPSTART: + MOZ_ASSERT(aParam >= 0); + mLoopStart = aParam; + break; + case AudioBufferSourceNode::LOOPEND: + MOZ_ASSERT(aParam >= 0); + mLoopEnd = aParam; + break; + default: + NS_ERROR("Bad AudioBufferSourceNodeEngine Int32Parameter"); + } + } + void SetBuffer(AudioChunk&& aBuffer) override { mBuffer = aBuffer; } + + bool BegunResampling() { return mBeginProcessing == -TRACK_TIME_MAX; } + + void UpdateResampler(int32_t aOutRate, uint32_t aChannels) { + if (mResampler && + (aChannels != mChannels || + // If the resampler has begun, then it will have moved + // mBufferPosition to after the samples it has read, but it hasn't + // output its buffered samples. Keep using the resampler, even if + // the rates now match, so that this latent segment is output. + (aOutRate == mBufferSampleRate && !BegunResampling()))) { + speex_resampler_destroy(mResampler); + mResampler = nullptr; + mRemainingResamplerTail = 0; + mBeginProcessing = llround(mStart); + } + + if (aChannels == 0 || (aOutRate == mBufferSampleRate && !mResampler)) { + mResamplerOutRate = aOutRate; + return; + } + + if (!mResampler) { + mChannels = aChannels; + mResampler = speex_resampler_init(mChannels, mBufferSampleRate, aOutRate, + SPEEX_RESAMPLER_QUALITY_MIN, nullptr); + } else { + if (mResamplerOutRate == aOutRate) { + return; + } + if (speex_resampler_set_rate(mResampler, mBufferSampleRate, aOutRate) != + RESAMPLER_ERR_SUCCESS) { + NS_ASSERTION(false, "speex_resampler_set_rate failed"); + return; + } + } + + mResamplerOutRate = aOutRate; + + if (!BegunResampling()) { + // Low pass filter effects from the resampler mean that samples before + // the start time are influenced by resampling the buffer. The input + // latency indicates half the filter width. + int64_t inputLatency = speex_resampler_get_input_latency(mResampler); + uint32_t ratioNum, ratioDen; + speex_resampler_get_ratio(mResampler, &ratioNum, &ratioDen); + // The output subsample resolution supported in aligning the resampler + // is ratioNum. First round the start time to the nearest subsample. + int64_t subsample = llround(mStart * ratioNum); + // Now include the leading effects of the filter, and round *up* to the + // next whole tick, because there is no effect on samples outside the + // filter width. + mBeginProcessing = + (subsample - inputLatency * ratioDen + ratioNum - 1) / ratioNum; + } + } + + // Borrow a full buffer of size WEBAUDIO_BLOCK_SIZE from the source buffer + // at offset aSourceOffset. This avoids copying memory. + void BorrowFromInputBuffer(AudioBlock* aOutput, uint32_t aChannels) { + aOutput->SetBuffer(mBuffer.mBuffer); + aOutput->mChannelData.SetLength(aChannels); + for (uint32_t i = 0; i < aChannels; ++i) { + aOutput->mChannelData[i] = + mBuffer.ChannelData<float>()[i] + mBufferPosition; + } + aOutput->mVolume = mBuffer.mVolume; + aOutput->mBufferFormat = AUDIO_FORMAT_FLOAT32; + } + + // Copy aNumberOfFrames frames from the source buffer at offset aSourceOffset + // and put it at offset aBufferOffset in the destination buffer. + template <typename T> + void CopyFromInputBuffer(AudioBlock* aOutput, uint32_t aChannels, + uintptr_t aOffsetWithinBlock, + uint32_t aNumberOfFrames) { + MOZ_ASSERT(mBuffer.mVolume == 1.0f); + for (uint32_t i = 0; i < aChannels; ++i) { + float* baseChannelData = aOutput->ChannelFloatsForWrite(i); + ConvertAudioSamples(mBuffer.ChannelData<T>()[i] + mBufferPosition, + baseChannelData + aOffsetWithinBlock, + aNumberOfFrames); + } + } + + // Resamples input data to an output buffer, according to |mBufferSampleRate| + // and the playbackRate/detune. The number of frames consumed/produced depends + // on the amount of space remaining in both the input and output buffer, and + // the playback rate (that is, the ratio between the output samplerate and the + // input samplerate). + void CopyFromInputBufferWithResampling(AudioBlock* aOutput, + uint32_t aChannels, + uint32_t* aOffsetWithinBlock, + uint32_t aAvailableInOutput, + TrackTime* aCurrentPosition, + uint32_t aBufferMax) { + if (*aOffsetWithinBlock == 0) { + aOutput->AllocateChannels(aChannels); + } + SpeexResamplerState* resampler = mResampler; + MOZ_ASSERT(aChannels > 0); + + if (mBufferPosition < aBufferMax) { + uint32_t availableInInputBuffer = aBufferMax - mBufferPosition; + uint32_t ratioNum, ratioDen; + speex_resampler_get_ratio(resampler, &ratioNum, &ratioDen); + // Limit the number of input samples copied and possibly + // format-converted for resampling by estimating how many will be used. + // This may be a little small if still filling the resampler with + // initial data, but we'll get called again and it will work out. + uint32_t inputLimit = aAvailableInOutput * ratioNum / ratioDen + 10; + if (!BegunResampling()) { + // First time the resampler is used. + uint32_t inputLatency = speex_resampler_get_input_latency(resampler); + inputLimit += inputLatency; + // If starting after mStart, then play from the beginning of the + // buffer, but correct for input latency. If starting before mStart, + // then align the resampler so that the time corresponding to the + // first input sample is mStart. + int64_t skipFracNum = static_cast<int64_t>(inputLatency) * ratioDen; + double leadTicks = mStart - *aCurrentPosition; + if (leadTicks > 0.0) { + // Round to nearest output subsample supported by the resampler at + // these rates. + int64_t leadSubsamples = llround(leadTicks * ratioNum); + MOZ_ASSERT(leadSubsamples <= skipFracNum, + "mBeginProcessing is wrong?"); + skipFracNum -= leadSubsamples; + } + speex_resampler_set_skip_frac_num( + resampler, std::min<int64_t>(skipFracNum, UINT32_MAX)); + + mBeginProcessing = -TRACK_TIME_MAX; + } + inputLimit = std::min(inputLimit, availableInInputBuffer); + + MOZ_ASSERT(mBuffer.mVolume == 1.0f); + for (uint32_t i = 0; true;) { + uint32_t inSamples = inputLimit; + + uint32_t outSamples = aAvailableInOutput; + float* outputData = + aOutput->ChannelFloatsForWrite(i) + *aOffsetWithinBlock; + + if (mBuffer.mBufferFormat == AUDIO_FORMAT_FLOAT32) { + const float* inputData = + mBuffer.ChannelData<float>()[i] + mBufferPosition; + WebAudioUtils::SpeexResamplerProcess( + resampler, i, inputData, &inSamples, outputData, &outSamples); + } else { + MOZ_ASSERT(mBuffer.mBufferFormat == AUDIO_FORMAT_S16); + const int16_t* inputData = + mBuffer.ChannelData<int16_t>()[i] + mBufferPosition; + WebAudioUtils::SpeexResamplerProcess( + resampler, i, inputData, &inSamples, outputData, &outSamples); + } + if (++i == aChannels) { + mBufferPosition += inSamples; + mRemainingFrames -= inSamples; + MOZ_ASSERT(mBufferPosition <= mBuffer.GetDuration()); + MOZ_ASSERT(mRemainingFrames >= 0); + *aOffsetWithinBlock += outSamples; + *aCurrentPosition += outSamples; + if ((!mLoop && inSamples == availableInInputBuffer) || + mRemainingFrames == 0) { + // We'll feed in enough zeros to empty out the resampler's memory. + // This handles the output latency as well as capturing the low + // pass effects of the resample filter. + mRemainingResamplerTail = + 2 * speex_resampler_get_input_latency(resampler) - 1; + } + return; + } + } + } else { + for (uint32_t i = 0; true;) { + uint32_t inSamples = mRemainingResamplerTail; + uint32_t outSamples = aAvailableInOutput; + float* outputData = + aOutput->ChannelFloatsForWrite(i) + *aOffsetWithinBlock; + + // AudioDataValue* for aIn selects the function that does not try to + // copy and format-convert input data. + WebAudioUtils::SpeexResamplerProcess( + resampler, i, static_cast<AudioDataValue*>(nullptr), &inSamples, + outputData, &outSamples); + if (++i == aChannels) { + MOZ_ASSERT(inSamples <= mRemainingResamplerTail); + mRemainingResamplerTail -= inSamples; + *aOffsetWithinBlock += outSamples; + *aCurrentPosition += outSamples; + break; + } + } + } + } + + /** + * Fill aOutput with as many zero frames as we can, and advance + * aOffsetWithinBlock and aCurrentPosition based on how many frames we write. + * This will never advance aOffsetWithinBlock past WEBAUDIO_BLOCK_SIZE or + * aCurrentPosition past aMaxPos. This function knows when it needs to + * allocate the output buffer, and also optimizes the case where it can avoid + * memory allocations. + */ + void FillWithZeroes(AudioBlock* aOutput, uint32_t aChannels, + uint32_t* aOffsetWithinBlock, TrackTime* aCurrentPosition, + TrackTime aMaxPos) { + MOZ_ASSERT(*aCurrentPosition < aMaxPos); + uint32_t numFrames = std::min<TrackTime>( + WEBAUDIO_BLOCK_SIZE - *aOffsetWithinBlock, aMaxPos - *aCurrentPosition); + if (numFrames == WEBAUDIO_BLOCK_SIZE || !aChannels) { + aOutput->SetNull(WEBAUDIO_BLOCK_SIZE); + } else { + if (*aOffsetWithinBlock == 0) { + aOutput->AllocateChannels(aChannels); + } + WriteZeroesToAudioBlock(aOutput, *aOffsetWithinBlock, numFrames); + } + *aOffsetWithinBlock += numFrames; + *aCurrentPosition += numFrames; + } + + /** + * Copy as many frames as possible from the source buffer to aOutput, and + * advance aOffsetWithinBlock and aCurrentPosition based on how many frames + * we write. This will never advance aOffsetWithinBlock past + * WEBAUDIO_BLOCK_SIZE, or aCurrentPosition past mStop. It takes data from + * the buffer at aBufferOffset, and never takes more data than aBufferMax. + * This function knows when it needs to allocate the output buffer, and also + * optimizes the case where it can avoid memory allocations. + */ + void CopyFromBuffer(AudioBlock* aOutput, uint32_t aChannels, + uint32_t* aOffsetWithinBlock, TrackTime* aCurrentPosition, + uint32_t aBufferMax) { + MOZ_ASSERT(*aCurrentPosition < mStop); + uint32_t availableInOutput = std::min<TrackTime>( + WEBAUDIO_BLOCK_SIZE - *aOffsetWithinBlock, mStop - *aCurrentPosition); + if (mResampler) { + CopyFromInputBufferWithResampling(aOutput, aChannels, aOffsetWithinBlock, + availableInOutput, aCurrentPosition, + aBufferMax); + return; + } + + if (aChannels == 0) { + aOutput->SetNull(WEBAUDIO_BLOCK_SIZE); + // There is no attempt here to limit advance so that mBufferPosition is + // limited to aBufferMax. The only observable affect of skipping the + // check would be in the precise timing of the ended event if the loop + // attribute is reset after playback has looped. + *aOffsetWithinBlock += availableInOutput; + *aCurrentPosition += availableInOutput; + // Rounding at the start and end of the period means that fractional + // increments essentially accumulate if outRate remains constant. If + // outRate is varying, then accumulation happens on average but not + // precisely. + TrackTicks start = + *aCurrentPosition * mBufferSampleRate / mResamplerOutRate; + TrackTicks end = (*aCurrentPosition + availableInOutput) * + mBufferSampleRate / mResamplerOutRate; + mBufferPosition += end - start; + return; + } + + uint32_t numFrames = + std::min(aBufferMax - mBufferPosition, availableInOutput); + + bool shouldBorrow = false; + if (numFrames == WEBAUDIO_BLOCK_SIZE && + mBuffer.mBufferFormat == AUDIO_FORMAT_FLOAT32) { + shouldBorrow = true; + for (uint32_t i = 0; i < aChannels; ++i) { + if (!IS_ALIGNED16(mBuffer.ChannelData<float>()[i] + mBufferPosition)) { + shouldBorrow = false; + break; + } + } + } + MOZ_ASSERT(mBufferPosition < aBufferMax); + if (shouldBorrow) { + BorrowFromInputBuffer(aOutput, aChannels); + } else { + if (*aOffsetWithinBlock == 0) { + aOutput->AllocateChannels(aChannels); + } + if (mBuffer.mBufferFormat == AUDIO_FORMAT_FLOAT32) { + CopyFromInputBuffer<float>(aOutput, aChannels, *aOffsetWithinBlock, + numFrames); + } else { + MOZ_ASSERT(mBuffer.mBufferFormat == AUDIO_FORMAT_S16); + CopyFromInputBuffer<int16_t>(aOutput, aChannels, *aOffsetWithinBlock, + numFrames); + } + } + *aOffsetWithinBlock += numFrames; + *aCurrentPosition += numFrames; + mBufferPosition += numFrames; + mRemainingFrames -= numFrames; + } + + int32_t ComputeFinalOutSampleRate(float aPlaybackRate, float aDetune) { + float computedPlaybackRate = aPlaybackRate * fdlibm_exp2f(aDetune / 1200.f); + // Make sure the playback rate is something our resampler can work with. + int32_t rate = WebAudioUtils::TruncateFloatToInt<int32_t>( + mSource->mSampleRate / computedPlaybackRate); + return rate ? rate : mBufferSampleRate; + } + + void UpdateSampleRateIfNeeded(uint32_t aChannels, TrackTime aTrackPosition) { + bool simplePlaybackRate = mPlaybackRateTimeline.HasSimpleValue(); + bool simpleDetune = mDetuneTimeline.HasSimpleValue(); + + if (simplePlaybackRate && simpleDetune && !mRecomputeOutRate) { + return; // skipping the slow exp2f() for the detune + } + mRecomputeOutRate = false; + + float playbackRate; + float detune; + if (simplePlaybackRate) { + playbackRate = mPlaybackRateTimeline.GetValue(); + } else { + playbackRate = + mPlaybackRateTimeline.GetComplexValueAtTime(aTrackPosition); + } + if (simpleDetune) { + detune = mDetuneTimeline.GetValue(); + } else { + detune = mDetuneTimeline.GetComplexValueAtTime(aTrackPosition); + } + if (playbackRate <= 0 || std::isnan(playbackRate)) { + playbackRate = 1.0f; + } + + int32_t outRate = ComputeFinalOutSampleRate(playbackRate, detune); + UpdateResampler(outRate, aChannels); + } + + void ProcessBlock(AudioNodeTrack* aTrack, GraphTime aFrom, + const AudioBlock& aInput, AudioBlock* aOutput, + bool* aFinished) override { + TRACE("AudioBufferSourceNodeEngine::ProcessBlock"); + if (mBufferSampleRate == 0) { + // start() has not yet been called or no buffer has yet been set + aOutput->SetNull(WEBAUDIO_BLOCK_SIZE); + return; + } + + TrackTime streamPosition = mDestination->GraphTimeToTrackTime(aFrom); + uint32_t channels = mBuffer.ChannelCount(); + + UpdateSampleRateIfNeeded(channels, streamPosition); + + uint32_t written = 0; + while (true) { + if ((mStop != TRACK_TIME_MAX && streamPosition >= mStop) || + (!mRemainingResamplerTail && + ((mBufferPosition >= mBuffer.GetDuration() && !mLoop) || + mRemainingFrames <= 0))) { + if (written != WEBAUDIO_BLOCK_SIZE) { + FillWithZeroes(aOutput, channels, &written, &streamPosition, + TRACK_TIME_MAX); + } + *aFinished = true; + break; + } + if (written == WEBAUDIO_BLOCK_SIZE) { + break; + } + if (streamPosition < mBeginProcessing) { + FillWithZeroes(aOutput, channels, &written, &streamPosition, + mBeginProcessing); + continue; + } + + TrackTicks bufferLeft; + if (mLoop) { + // mLoopEnd can become less than mBufferPosition when a LOOPEND engine + // parameter is received after "loopend" is changed on the node or a + // new buffer with lower samplerate is set. + if (mBufferPosition >= mLoopEnd) { + mBufferPosition = mLoopStart; + } + bufferLeft = + std::min<TrackTicks>(mRemainingFrames, mLoopEnd - mBufferPosition); + } else { + bufferLeft = + std::min(mRemainingFrames, mBuffer.GetDuration() - mBufferPosition); + } + + CopyFromBuffer(aOutput, channels, &written, &streamPosition, + bufferLeft + mBufferPosition); + } + } + + bool IsActive() const override { + // Whether buffer has been set and start() has been called. + return mBufferSampleRate != 0; + } + + size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override { + // Not owned: + // - mBuffer - shared w/ AudioNode + // - mPlaybackRateTimeline - shared w/ AudioNode + // - mDetuneTimeline - shared w/ AudioNode + + size_t amount = AudioNodeEngine::SizeOfExcludingThis(aMallocSizeOf); + + // NB: We need to modify speex if we want the full memory picture, internal + // fields that need measuring noted below. + // - mResampler->mem + // - mResampler->sinc_table + // - mResampler->last_sample + // - mResampler->magic_samples + // - mResampler->samp_frac_num + amount += aMallocSizeOf(mResampler); + + return amount; + } + + size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override { + return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf); + } + + double mStart; // including the fractional position between ticks + // Low pass filter effects from the resampler mean that samples before the + // start time are influenced by resampling the buffer. mBeginProcessing + // includes the extent of this filter. The special value of -TRACK_TIME_MAX + // indicates that the resampler has begun processing. + TrackTime mBeginProcessing; + TrackTime mStop; + AudioChunk mBuffer; + SpeexResamplerState* mResampler; + // mRemainingResamplerTail, like mBufferPosition + // is measured in input buffer samples. + uint32_t mRemainingResamplerTail; + TrackTicks mRemainingFrames; + uint32_t mLoopStart; + uint32_t mLoopEnd; + uint32_t mBufferPosition; + int32_t mBufferSampleRate; + int32_t mResamplerOutRate; + uint32_t mChannels; + RefPtr<AudioNodeTrack> mDestination; + + // mSource deletes the engine in its destructor. + AudioNodeTrack* MOZ_NON_OWNING_REF mSource; + AudioParamTimeline mPlaybackRateTimeline; + AudioParamTimeline mDetuneTimeline; + bool mLoop; + bool mRecomputeOutRate = true; +}; + +AudioBufferSourceNode::AudioBufferSourceNode(AudioContext* aContext) + : AudioScheduledSourceNode(aContext, 2, ChannelCountMode::Max, + ChannelInterpretation::Speakers), + mLoopStart(0.0), + mLoopEnd(0.0), + // mOffset and mDuration are initialized in Start(). + mLoop(false), + mStartCalled(false), + mBufferSet(false) { + mPlaybackRate = CreateAudioParam(PLAYBACKRATE, u"playbackRate"_ns, 1.0f); + mDetune = CreateAudioParam(DETUNE, u"detune"_ns, 0.0f); + AudioBufferSourceNodeEngine* engine = + new AudioBufferSourceNodeEngine(this, aContext->Destination()); + mTrack = AudioNodeTrack::Create(aContext, engine, + AudioNodeTrack::NEED_MAIN_THREAD_ENDED, + aContext->Graph()); + engine->SetSourceTrack(mTrack); + mTrack->AddMainThreadListener(this); +} + +/* static */ +already_AddRefed<AudioBufferSourceNode> AudioBufferSourceNode::Create( + JSContext* aCx, AudioContext& aAudioContext, + const AudioBufferSourceOptions& aOptions) { + RefPtr<AudioBufferSourceNode> audioNode = + new AudioBufferSourceNode(&aAudioContext); + + if (aOptions.mBuffer.WasPassed()) { + ErrorResult ignored; + MOZ_ASSERT(aCx); + audioNode->SetBuffer(aCx, aOptions.mBuffer.Value(), ignored); + } + + audioNode->Detune()->SetInitialValue(aOptions.mDetune); + audioNode->SetLoop(aOptions.mLoop); + audioNode->SetLoopEnd(aOptions.mLoopEnd); + audioNode->SetLoopStart(aOptions.mLoopStart); + audioNode->PlaybackRate()->SetInitialValue(aOptions.mPlaybackRate); + + return audioNode.forget(); +} +void AudioBufferSourceNode::DestroyMediaTrack() { + bool hadTrack = mTrack; + if (hadTrack) { + mTrack->RemoveMainThreadListener(this); + } + AudioNode::DestroyMediaTrack(); +} + +size_t AudioBufferSourceNode::SizeOfExcludingThis( + MallocSizeOf aMallocSizeOf) const { + size_t amount = AudioNode::SizeOfExcludingThis(aMallocSizeOf); + + /* mBuffer can be shared and is accounted for separately. */ + + amount += mPlaybackRate->SizeOfIncludingThis(aMallocSizeOf); + amount += mDetune->SizeOfIncludingThis(aMallocSizeOf); + return amount; +} + +size_t AudioBufferSourceNode::SizeOfIncludingThis( + MallocSizeOf aMallocSizeOf) const { + return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf); +} + +JSObject* AudioBufferSourceNode::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return AudioBufferSourceNode_Binding::Wrap(aCx, this, aGivenProto); +} + +void AudioBufferSourceNode::Start(double aWhen, double aOffset, + const Optional<double>& aDuration, + ErrorResult& aRv) { + if (!WebAudioUtils::IsTimeValid(aWhen)) { + aRv.ThrowRangeError<MSG_VALUE_OUT_OF_RANGE>("start time"); + return; + } + if (aOffset < 0) { + aRv.ThrowRangeError<MSG_VALUE_OUT_OF_RANGE>("offset"); + return; + } + if (aDuration.WasPassed() && !WebAudioUtils::IsTimeValid(aDuration.Value())) { + aRv.ThrowRangeError<MSG_VALUE_OUT_OF_RANGE>("duration"); + return; + } + + if (mStartCalled) { + aRv.ThrowInvalidStateError( + "Start has already been called on this AudioBufferSourceNode."); + return; + } + mStartCalled = true; + + AudioNodeTrack* ns = mTrack; + if (!ns) { + // Nothing to play, or we're already dead for some reason + return; + } + + // Remember our arguments so that we can use them when we get a new buffer. + mOffset = aOffset; + mDuration = aDuration.WasPassed() ? aDuration.Value() + : std::numeric_limits<double>::min(); + + WEB_AUDIO_API_LOG("%f: %s %u Start(%f, %g, %g)", Context()->CurrentTime(), + NodeType(), Id(), aWhen, aOffset, mDuration); + + // We can't send these parameters without a buffer because we don't know the + // buffer's sample rate or length. + if (mBuffer) { + SendOffsetAndDurationParametersToTrack(ns); + } + + // Don't set parameter unnecessarily + if (aWhen > 0.0) { + ns->SetDoubleParameter(START, aWhen); + } + + Context()->StartBlockedAudioContextIfAllowed(); +} + +void AudioBufferSourceNode::Start(double aWhen, ErrorResult& aRv) { + Start(aWhen, 0 /* offset */, Optional<double>(), aRv); +} + +void AudioBufferSourceNode::SendBufferParameterToTrack(JSContext* aCx) { + AudioNodeTrack* ns = mTrack; + if (!ns) { + return; + } + + if (mBuffer) { + AudioChunk data = mBuffer->GetThreadSharedChannelsForRate(aCx); + ns->SetBuffer(std::move(data)); + + if (mStartCalled) { + SendOffsetAndDurationParametersToTrack(ns); + } + } else { + ns->SetBuffer(AudioChunk()); + + MarkInactive(); + } +} + +void AudioBufferSourceNode::SendOffsetAndDurationParametersToTrack( + AudioNodeTrack* aTrack) { + NS_ASSERTION( + mBuffer && mStartCalled, + "Only call this when we have a buffer and start() has been called"); + + float rate = mBuffer->SampleRate(); + aTrack->SetInt32Parameter(SAMPLE_RATE, rate); + + int32_t offsetSamples = std::max(0, NS_lround(mOffset * rate)); + + // Don't set parameter unnecessarily + if (offsetSamples > 0) { + aTrack->SetInt32Parameter(BUFFERSTART, offsetSamples); + } + + if (mDuration != std::numeric_limits<double>::min()) { + MOZ_ASSERT(mDuration >= 0.0); // provided by Start() + MOZ_ASSERT(rate >= 0.0f); // provided by AudioBuffer::Create() + aTrack->SetDoubleParameter(DURATION, mDuration); + } + MarkActive(); +} + +void AudioBufferSourceNode::Stop(double aWhen, ErrorResult& aRv) { + if (!WebAudioUtils::IsTimeValid(aWhen)) { + aRv.ThrowRangeError<MSG_VALUE_OUT_OF_RANGE>("stop time"); + return; + } + + if (!mStartCalled) { + aRv.ThrowInvalidStateError( + "Start has not been called on this AudioBufferSourceNode."); + return; + } + + WEB_AUDIO_API_LOG("%f: %s %u Stop(%f)", Context()->CurrentTime(), NodeType(), + Id(), aWhen); + + AudioNodeTrack* ns = mTrack; + if (!ns || !Context()) { + // We've already stopped and had our track shut down + return; + } + + ns->SetTrackTimeParameter(STOP, Context(), std::max(0.0, aWhen)); +} + +void AudioBufferSourceNode::NotifyMainThreadTrackEnded() { + MOZ_ASSERT(mTrack->IsEnded()); + + class EndedEventDispatcher final : public Runnable { + public: + explicit EndedEventDispatcher(AudioBufferSourceNode* aNode) + : mozilla::Runnable("EndedEventDispatcher"), mNode(aNode) {} + NS_IMETHOD Run() override { + // If it's not safe to run scripts right now, schedule this to run later + if (!nsContentUtils::IsSafeToRunScript()) { + nsContentUtils::AddScriptRunner(this); + return NS_OK; + } + + mNode->DispatchTrustedEvent(u"ended"_ns); + // Release track resources. + mNode->DestroyMediaTrack(); + return NS_OK; + } + + private: + RefPtr<AudioBufferSourceNode> mNode; + }; + + Context()->Dispatch(do_AddRef(new EndedEventDispatcher(this))); + + // Drop the playing reference + // Warning: The below line might delete this. + MarkInactive(); +} + +void AudioBufferSourceNode::SendLoopParametersToTrack() { + if (!mTrack) { + return; + } + // Don't compute and set the loop parameters unnecessarily + if (mLoop && mBuffer) { + float rate = mBuffer->SampleRate(); + double length = (double(mBuffer->Length()) / mBuffer->SampleRate()); + double actualLoopStart, actualLoopEnd; + if (mLoopStart >= 0.0 && mLoopEnd > 0.0 && mLoopStart < mLoopEnd) { + MOZ_ASSERT(mLoopStart != 0.0 || mLoopEnd != 0.0); + actualLoopStart = (mLoopStart > length) ? 0.0 : mLoopStart; + actualLoopEnd = std::min(mLoopEnd, length); + } else { + actualLoopStart = 0.0; + actualLoopEnd = length; + } + int32_t loopStartTicks = NS_lround(actualLoopStart * rate); + int32_t loopEndTicks = NS_lround(actualLoopEnd * rate); + if (loopStartTicks < loopEndTicks) { + SendInt32ParameterToTrack(LOOPSTART, loopStartTicks); + SendInt32ParameterToTrack(LOOPEND, loopEndTicks); + SendInt32ParameterToTrack(LOOP, 1); + } else { + // Be explicit about looping not happening if the offsets make + // looping impossible. + SendInt32ParameterToTrack(LOOP, 0); + } + } else { + SendInt32ParameterToTrack(LOOP, 0); + } +} + +} // namespace mozilla::dom diff --git a/dom/media/webaudio/AudioBufferSourceNode.h b/dom/media/webaudio/AudioBufferSourceNode.h new file mode 100644 index 0000000000..9a3861555c --- /dev/null +++ b/dom/media/webaudio/AudioBufferSourceNode.h @@ -0,0 +1,129 @@ +/* -*- 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/. */ + +#ifndef AudioBufferSourceNode_h_ +#define AudioBufferSourceNode_h_ + +#include "AudioScheduledSourceNode.h" +#include "AudioBuffer.h" + +namespace mozilla::dom { + +struct AudioBufferSourceOptions; +class AudioParam; + +class AudioBufferSourceNode final : public AudioScheduledSourceNode, + public MainThreadMediaTrackListener { + public: + static already_AddRefed<AudioBufferSourceNode> Create( + JSContext* aCx, AudioContext& aAudioContext, + const AudioBufferSourceOptions& aOptions); + + void DestroyMediaTrack() override; + + uint16_t NumberOfInputs() const final { return 0; } + AudioBufferSourceNode* AsAudioBufferSourceNode() override { return this; } + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(AudioBufferSourceNode, + AudioScheduledSourceNode) + + static already_AddRefed<AudioBufferSourceNode> Constructor( + const GlobalObject& aGlobal, AudioContext& aAudioContext, + const AudioBufferSourceOptions& aOptions) { + return Create(aGlobal.Context(), aAudioContext, aOptions); + } + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + void Start(double aWhen, double aOffset, const Optional<double>& aDuration, + ErrorResult& aRv); + + void Start(double aWhen, ErrorResult& aRv) override; + void Stop(double aWhen, ErrorResult& aRv) override; + + AudioBuffer* GetBuffer(JSContext* aCx) const { return mBuffer; } + void SetBuffer(JSContext* aCx, AudioBuffer* aBuffer, ErrorResult& aRv) { + if (aBuffer && mBufferSet) { + aRv.ThrowInvalidStateError( + "Cannot set the buffer attribute of an AudioBufferSourceNode " + "with an AudioBuffer more than once"); + return; + } + if (aBuffer) { + mBufferSet = true; + } + mBuffer = aBuffer; + SendBufferParameterToTrack(aCx); + SendLoopParametersToTrack(); + } + AudioParam* PlaybackRate() const { return mPlaybackRate; } + AudioParam* Detune() const { return mDetune; } + bool Loop() const { return mLoop; } + void SetLoop(bool aLoop) { + mLoop = aLoop; + SendLoopParametersToTrack(); + } + double LoopStart() const { return mLoopStart; } + void SetLoopStart(double aStart) { + mLoopStart = aStart; + SendLoopParametersToTrack(); + } + double LoopEnd() const { return mLoopEnd; } + void SetLoopEnd(double aEnd) { + mLoopEnd = aEnd; + SendLoopParametersToTrack(); + } + void NotifyMainThreadTrackEnded() override; + + const char* NodeType() const override { return "AudioBufferSourceNode"; } + + size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override; + size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override; + + private: + explicit AudioBufferSourceNode(AudioContext* aContext); + ~AudioBufferSourceNode() = default; + + friend class AudioBufferSourceNodeEngine; + // START is sent during Start(). + // STOP is sent during Stop(). + // BUFFERSTART and DURATION are sent when SetBuffer() and Start() have + // been called (along with sending the buffer). + enum EngineParameters { + SAMPLE_RATE, + START, + STOP, + // BUFFERSTART is the "offset" passed to start(), multiplied by + // buffer.sampleRate. + BUFFERSTART, + DURATION, + LOOP, + LOOPSTART, + LOOPEND, + PLAYBACKRATE, + DETUNE + }; + + void SendLoopParametersToTrack(); + void SendBufferParameterToTrack(JSContext* aCx); + void SendOffsetAndDurationParametersToTrack(AudioNodeTrack* aTrack); + + double mLoopStart; + double mLoopEnd; + double mOffset; + double mDuration; + RefPtr<AudioBuffer> mBuffer; + RefPtr<AudioParam> mPlaybackRate; + RefPtr<AudioParam> mDetune; + bool mLoop; + bool mStartCalled; + bool mBufferSet; +}; + +} // namespace mozilla::dom + +#endif diff --git a/dom/media/webaudio/AudioContext.cpp b/dom/media/webaudio/AudioContext.cpp new file mode 100644 index 0000000000..2ef5fdd65b --- /dev/null +++ b/dom/media/webaudio/AudioContext.cpp @@ -0,0 +1,1405 @@ +/* -*- 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 "blink/PeriodicWave.h" + +#include "mozilla/ErrorResult.h" +#include "mozilla/NotNull.h" +#include "mozilla/OwningNonNull.h" +#include "mozilla/RefPtr.h" +#include "mozilla/Preferences.h" +#include "mozilla/StaticPrefs_media.h" + +#include "mozilla/dom/AnalyserNode.h" +#include "mozilla/dom/AnalyserNodeBinding.h" +#include "mozilla/dom/AudioBufferSourceNodeBinding.h" +#include "mozilla/dom/AudioContextBinding.h" +#include "mozilla/dom/BaseAudioContextBinding.h" +#include "mozilla/dom/BiquadFilterNodeBinding.h" +#include "mozilla/dom/BrowsingContext.h" +#include "mozilla/dom/CanonicalBrowsingContext.h" +#include "mozilla/dom/ChannelMergerNodeBinding.h" +#include "mozilla/dom/ChannelSplitterNodeBinding.h" +#include "mozilla/dom/ContentChild.h" +#include "mozilla/dom/ConvolverNodeBinding.h" +#include "mozilla/dom/DelayNodeBinding.h" +#include "mozilla/dom/DynamicsCompressorNodeBinding.h" +#include "mozilla/dom/GainNodeBinding.h" +#include "mozilla/dom/IIRFilterNodeBinding.h" +#include "mozilla/dom/HTMLMediaElement.h" +#include "mozilla/dom/MediaElementAudioSourceNodeBinding.h" +#include "mozilla/dom/MediaStreamAudioSourceNodeBinding.h" +#include "mozilla/dom/MediaStreamTrackAudioSourceNodeBinding.h" +#include "mozilla/dom/OfflineAudioContextBinding.h" +#include "mozilla/dom/OscillatorNodeBinding.h" +#include "mozilla/dom/PannerNodeBinding.h" +#include "mozilla/dom/PeriodicWaveBinding.h" +#include "mozilla/dom/Performance.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/StereoPannerNodeBinding.h" +#include "mozilla/dom/WaveShaperNodeBinding.h" +#include "mozilla/dom/Worklet.h" + +#include "AudioBuffer.h" +#include "AudioBufferSourceNode.h" +#include "AudioChannelService.h" +#include "AudioDestinationNode.h" +#include "AudioListener.h" +#include "AudioNodeTrack.h" +#include "AudioStream.h" +#include "AudioWorkletImpl.h" +#include "AutoplayPolicy.h" +#include "BiquadFilterNode.h" +#include "ChannelMergerNode.h" +#include "ChannelSplitterNode.h" +#include "ConstantSourceNode.h" +#include "ConvolverNode.h" +#include "DelayNode.h" +#include "DynamicsCompressorNode.h" +#include "GainNode.h" +#include "IIRFilterNode.h" +#include "js/ArrayBuffer.h" // JS::StealArrayBufferContents +#include "MediaElementAudioSourceNode.h" +#include "MediaStreamAudioDestinationNode.h" +#include "MediaStreamAudioSourceNode.h" +#include "MediaTrackGraph.h" +#include "MediaStreamTrackAudioSourceNode.h" +#include "nsContentUtils.h" +#include "nsIScriptError.h" +#include "nsNetCID.h" +#include "nsNetUtil.h" +#include "nsPIDOMWindow.h" +#include "nsPrintfCString.h" +#include "nsRFPService.h" +#include "OscillatorNode.h" +#include "PannerNode.h" +#include "PeriodicWave.h" +#include "ScriptProcessorNode.h" +#include "StereoPannerNode.h" +#include "WaveShaperNode.h" +#include "Tracing.h" + +extern mozilla::LazyLogModule gAutoplayPermissionLog; + +#define AUTOPLAY_LOG(msg, ...) \ + MOZ_LOG(gAutoplayPermissionLog, LogLevel::Debug, (msg, ##__VA_ARGS__)) + +namespace mozilla::dom { + +// 0 is a special value that MediaTracks use to denote they are not part of a +// AudioContext. +static dom::AudioContext::AudioContextId gAudioContextId = 1; + +NS_IMPL_CYCLE_COLLECTION_CLASS(AudioContext) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(AudioContext) + // The destination node and AudioContext form a cycle and so the destination + // track will be destroyed. mWorklet must be shut down before the track + // is destroyed. Do this before clearing mWorklet. + tmp->ShutdownWorklet(); + NS_IMPL_CYCLE_COLLECTION_UNLINK(mDestination) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mListener) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mWorklet) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mPromiseGripArray) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mPendingResumePromises) + if (tmp->mTracksAreSuspended || !tmp->mIsStarted) { + NS_IMPL_CYCLE_COLLECTION_UNLINK(mActiveNodes) + } + // mDecodeJobs owns the WebAudioDecodeJob objects whose lifetime is managed + // explicitly. mAllNodes is an array of weak pointers, ignore it here. + // mBasicWaveFormCache cannot participate in cycles, ignore it here. + + // Remove weak reference on the global window as the context is not usable + // without mDestination. + tmp->DisconnectFromWindow(); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END_INHERITED(DOMEventTargetHelper) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(AudioContext, + DOMEventTargetHelper) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDestination) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mListener) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mWorklet) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPromiseGripArray) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPendingResumePromises) + if (tmp->mTracksAreSuspended || !tmp->mIsStarted) { + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mActiveNodes) + } + // mDecodeJobs owns the WebAudioDecodeJob objects whose lifetime is managed + // explicitly. mAllNodes is an array of weak pointers, ignore it here. + // mBasicWaveFormCache cannot participate in cycles, ignore it here. +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_ADDREF_INHERITED(AudioContext, DOMEventTargetHelper) +NS_IMPL_RELEASE_INHERITED(AudioContext, DOMEventTargetHelper) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(AudioContext) + NS_INTERFACE_MAP_ENTRY(nsIMemoryReporter) +NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper) + +static float GetSampleRateForAudioContext(bool aIsOffline, float aSampleRate, + bool aShouldResistFingerprinting) { + if (aIsOffline || aSampleRate != 0.0) { + return aSampleRate; + } else { + return static_cast<float>( + CubebUtils::PreferredSampleRate(aShouldResistFingerprinting)); + } +} + +AudioContext::AudioContext(nsPIDOMWindowInner* aWindow, bool aIsOffline, + uint32_t aNumberOfChannels, uint32_t aLength, + float aSampleRate) + : DOMEventTargetHelper(aWindow), + mId(gAudioContextId++), + mSampleRate(GetSampleRateForAudioContext( + aIsOffline, aSampleRate, + aWindow->AsGlobal()->ShouldResistFingerprinting( + RFPTarget::AudioSampleRate))), + mAudioContextState(AudioContextState::Suspended), + mNumberOfChannels(aNumberOfChannels), + mRTPCallerType(aWindow->AsGlobal()->GetRTPCallerType()), + mShouldResistFingerprinting( + aWindow->AsGlobal()->ShouldResistFingerprinting( + RFPTarget::AudioContext)), + mIsOffline(aIsOffline), + mIsStarted(!aIsOffline), + mIsShutDown(false), + mIsDisconnecting(false), + mCloseCalled(false), + // Realtime contexts start with suspended tracks until an + // AudioCallbackDriver is running. + mTracksAreSuspended(!aIsOffline), + mWasAllowedToStart(true), + mSuspendedByContent(false), + mSuspendedByChrome(aWindow->IsSuspended()), + mWasEverAllowedToStart(false), + mWasEverBlockedToStart(false), + mWouldBeAllowedToStart(true) { + bool mute = aWindow->AddAudioContext(this); + + // Note: AudioDestinationNode needs an AudioContext that must already be + // bound to the window. + const bool allowedToStart = media::AutoplayPolicy::IsAllowedToPlay(*this); + mDestination = + new AudioDestinationNode(this, aIsOffline, aNumberOfChannels, aLength); + mDestination->Init(); + // If an AudioContext is not allowed to start, we would postpone its state + // transition from `suspended` to `running` until sites explicitly call + // AudioContext.resume() or AudioScheduledSourceNode.start(). + if (!allowedToStart) { + MOZ_ASSERT(!mIsOffline); + AUTOPLAY_LOG("AudioContext %p is not allowed to start", this); + ReportBlocked(); + } else if (!mIsOffline) { + ResumeInternal(); + } + + // The context can't be muted until it has a destination. + if (mute) { + Mute(); + } + + UpdateAutoplayAssumptionStatus(); + + FFTBlock::MainThreadInit(); +} + +void AudioContext::StartBlockedAudioContextIfAllowed() { + MOZ_ASSERT(NS_IsMainThread()); + MaybeUpdateAutoplayTelemetry(); + // Only try to start AudioContext when AudioContext was not allowed to start. + if (mWasAllowedToStart) { + return; + } + + const bool isAllowedToPlay = media::AutoplayPolicy::IsAllowedToPlay(*this); + AUTOPLAY_LOG("Trying to start AudioContext %p, IsAllowedToPlay=%d", this, + isAllowedToPlay); + + // Only start the AudioContext if this resume() call was initiated by content, + // not if it was a result of the AudioContext starting after having been + // blocked because of the auto-play policy. + if (isAllowedToPlay && !mSuspendedByContent) { + ResumeInternal(); + } else { + ReportBlocked(); + } +} + +void AudioContext::DisconnectFromWindow() { + MaybeClearPageAwakeRequest(); + nsPIDOMWindowInner* window = GetOwner(); + if (window) { + window->RemoveAudioContext(this); + } +} + +AudioContext::~AudioContext() { + DisconnectFromWindow(); + UnregisterWeakMemoryReporter(this); + MOZ_ASSERT(!mSetPageAwakeRequest, "forgot to revoke for page awake?"); +} + +JSObject* AudioContext::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + if (mIsOffline) { + return OfflineAudioContext_Binding::Wrap(aCx, this, aGivenProto); + } else { + return AudioContext_Binding::Wrap(aCx, this, aGivenProto); + } +} + +static bool CheckFullyActive(nsPIDOMWindowInner* aWindow, ErrorResult& aRv) { + if (!aWindow->IsFullyActive()) { + aRv.ThrowInvalidStateError("The document is not fully active."); + return false; + } + return true; +} + +/* static */ +already_AddRefed<AudioContext> AudioContext::Constructor( + const GlobalObject& aGlobal, const AudioContextOptions& aOptions, + ErrorResult& aRv) { + nsCOMPtr<nsPIDOMWindowInner> window = + do_QueryInterface(aGlobal.GetAsSupports()); + if (!window) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + /** + * If the current settings object’s responsible document is NOT fully + * active, throw an InvalidStateError and abort these steps. + */ + if (!CheckFullyActive(window, aRv)) { + return nullptr; + } + + if (aOptions.mSampleRate.WasPassed() && + (aOptions.mSampleRate.Value() < WebAudioUtils::MinSampleRate || + aOptions.mSampleRate.Value() > WebAudioUtils::MaxSampleRate)) { + aRv.ThrowNotSupportedError(nsPrintfCString( + "Sample rate %g is not in the range [%u, %u]", + aOptions.mSampleRate.Value(), WebAudioUtils::MinSampleRate, + WebAudioUtils::MaxSampleRate)); + return nullptr; + } + float sampleRate = aOptions.mSampleRate.WasPassed() + ? aOptions.mSampleRate.Value() + : MediaTrackGraph::REQUEST_DEFAULT_SAMPLE_RATE; + + RefPtr<AudioContext> object = + new AudioContext(window, false, 2, 0, sampleRate); + + RegisterWeakMemoryReporter(object); + + return object.forget(); +} + +/* static */ +already_AddRefed<AudioContext> AudioContext::Constructor( + const GlobalObject& aGlobal, const OfflineAudioContextOptions& aOptions, + ErrorResult& aRv) { + return Constructor(aGlobal, aOptions.mNumberOfChannels, aOptions.mLength, + aOptions.mSampleRate, aRv); +} + +/* static */ +already_AddRefed<AudioContext> AudioContext::Constructor( + const GlobalObject& aGlobal, uint32_t aNumberOfChannels, uint32_t aLength, + float aSampleRate, ErrorResult& aRv) { + nsCOMPtr<nsPIDOMWindowInner> window = + do_QueryInterface(aGlobal.GetAsSupports()); + if (!window) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + /** + * If the current settings object’s responsible document is NOT fully + * active, throw an InvalidStateError and abort these steps. + */ + if (!CheckFullyActive(window, aRv)) { + return nullptr; + } + + if (aNumberOfChannels == 0 || + aNumberOfChannels > WebAudioUtils::MaxChannelCount) { + aRv.ThrowNotSupportedError( + nsPrintfCString("%u is not a valid channel count", aNumberOfChannels)); + return nullptr; + } + + if (aLength == 0) { + aRv.ThrowNotSupportedError("Length must be nonzero"); + return nullptr; + } + + if (aSampleRate < WebAudioUtils::MinSampleRate || + aSampleRate > WebAudioUtils::MaxSampleRate) { + // The DOM binding protects us against infinity and NaN + aRv.ThrowNotSupportedError(nsPrintfCString( + "Sample rate %g is not in the range [%u, %u]", aSampleRate, + WebAudioUtils::MinSampleRate, WebAudioUtils::MaxSampleRate)); + return nullptr; + } + + RefPtr<AudioContext> object = + new AudioContext(window, true, aNumberOfChannels, aLength, aSampleRate); + + RegisterWeakMemoryReporter(object); + + return object.forget(); +} + +already_AddRefed<AudioBufferSourceNode> AudioContext::CreateBufferSource() { + return AudioBufferSourceNode::Create(nullptr, *this, + AudioBufferSourceOptions()); +} + +already_AddRefed<ConstantSourceNode> AudioContext::CreateConstantSource() { + RefPtr<ConstantSourceNode> constantSourceNode = new ConstantSourceNode(this); + return constantSourceNode.forget(); +} + +already_AddRefed<AudioBuffer> AudioContext::CreateBuffer( + uint32_t aNumberOfChannels, uint32_t aLength, float aSampleRate, + ErrorResult& aRv) { + if (!aNumberOfChannels) { + aRv.ThrowNotSupportedError("Number of channels must be nonzero"); + return nullptr; + } + + return AudioBuffer::Create(GetOwner(), aNumberOfChannels, aLength, + aSampleRate, aRv); +} + +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; + } +} + +} // namespace + +already_AddRefed<MediaStreamAudioDestinationNode> +AudioContext::CreateMediaStreamDestination(ErrorResult& aRv) { + return MediaStreamAudioDestinationNode::Create(*this, AudioNodeOptions(), + aRv); +} + +already_AddRefed<ScriptProcessorNode> AudioContext::CreateScriptProcessor( + uint32_t aBufferSize, uint32_t aNumberOfInputChannels, + uint32_t aNumberOfOutputChannels, ErrorResult& aRv) { + if (aNumberOfInputChannels == 0 && aNumberOfOutputChannels == 0) { + aRv.ThrowIndexSizeError( + "At least one of numberOfInputChannels and numberOfOutputChannels must " + "be nonzero"); + return nullptr; + } + + if (aNumberOfInputChannels > WebAudioUtils::MaxChannelCount) { + aRv.ThrowIndexSizeError(nsPrintfCString( + "%u is not a valid number of input channels", aNumberOfInputChannels)); + return nullptr; + } + + if (aNumberOfOutputChannels > WebAudioUtils::MaxChannelCount) { + aRv.ThrowIndexSizeError( + nsPrintfCString("%u is not a valid number of output channels", + aNumberOfOutputChannels)); + return nullptr; + } + + if (!IsValidBufferSize(aBufferSize)) { + aRv.ThrowIndexSizeError( + nsPrintfCString("%u is not a valid bufferSize", aBufferSize)); + return nullptr; + } + + RefPtr<ScriptProcessorNode> scriptProcessor = new ScriptProcessorNode( + this, aBufferSize, aNumberOfInputChannels, aNumberOfOutputChannels); + return scriptProcessor.forget(); +} + +already_AddRefed<AnalyserNode> AudioContext::CreateAnalyser(ErrorResult& aRv) { + return AnalyserNode::Create(*this, AnalyserOptions(), aRv); +} + +already_AddRefed<StereoPannerNode> AudioContext::CreateStereoPanner( + ErrorResult& aRv) { + return StereoPannerNode::Create(*this, StereoPannerOptions(), aRv); +} + +already_AddRefed<MediaElementAudioSourceNode> +AudioContext::CreateMediaElementSource(HTMLMediaElement& aMediaElement, + ErrorResult& aRv) { + MediaElementAudioSourceOptions options; + options.mMediaElement = aMediaElement; + + return MediaElementAudioSourceNode::Create(*this, options, aRv); +} + +already_AddRefed<MediaStreamAudioSourceNode> +AudioContext::CreateMediaStreamSource(DOMMediaStream& aMediaStream, + ErrorResult& aRv) { + MediaStreamAudioSourceOptions options; + options.mMediaStream = aMediaStream; + + return MediaStreamAudioSourceNode::Create(*this, options, aRv); +} + +already_AddRefed<MediaStreamTrackAudioSourceNode> +AudioContext::CreateMediaStreamTrackSource(MediaStreamTrack& aMediaStreamTrack, + ErrorResult& aRv) { + MediaStreamTrackAudioSourceOptions options; + options.mMediaStreamTrack = aMediaStreamTrack; + + return MediaStreamTrackAudioSourceNode::Create(*this, options, aRv); +} + +already_AddRefed<GainNode> AudioContext::CreateGain(ErrorResult& aRv) { + return GainNode::Create(*this, GainOptions(), aRv); +} + +already_AddRefed<WaveShaperNode> AudioContext::CreateWaveShaper( + ErrorResult& aRv) { + return WaveShaperNode::Create(*this, WaveShaperOptions(), aRv); +} + +already_AddRefed<DelayNode> AudioContext::CreateDelay(double aMaxDelayTime, + ErrorResult& aRv) { + DelayOptions options; + options.mMaxDelayTime = aMaxDelayTime; + return DelayNode::Create(*this, options, aRv); +} + +already_AddRefed<PannerNode> AudioContext::CreatePanner(ErrorResult& aRv) { + return PannerNode::Create(*this, PannerOptions(), aRv); +} + +already_AddRefed<ConvolverNode> AudioContext::CreateConvolver( + ErrorResult& aRv) { + return ConvolverNode::Create(nullptr, *this, ConvolverOptions(), aRv); +} + +already_AddRefed<ChannelSplitterNode> AudioContext::CreateChannelSplitter( + uint32_t aNumberOfOutputs, ErrorResult& aRv) { + ChannelSplitterOptions options; + options.mNumberOfOutputs = aNumberOfOutputs; + return ChannelSplitterNode::Create(*this, options, aRv); +} + +already_AddRefed<ChannelMergerNode> AudioContext::CreateChannelMerger( + uint32_t aNumberOfInputs, ErrorResult& aRv) { + ChannelMergerOptions options; + options.mNumberOfInputs = aNumberOfInputs; + return ChannelMergerNode::Create(*this, options, aRv); +} + +already_AddRefed<DynamicsCompressorNode> AudioContext::CreateDynamicsCompressor( + ErrorResult& aRv) { + return DynamicsCompressorNode::Create(*this, DynamicsCompressorOptions(), + aRv); +} + +already_AddRefed<BiquadFilterNode> AudioContext::CreateBiquadFilter( + ErrorResult& aRv) { + return BiquadFilterNode::Create(*this, BiquadFilterOptions(), aRv); +} + +already_AddRefed<IIRFilterNode> AudioContext::CreateIIRFilter( + const Sequence<double>& aFeedforward, const Sequence<double>& aFeedback, + mozilla::ErrorResult& aRv) { + IIRFilterOptions options; + options.mFeedforward = aFeedforward; + options.mFeedback = aFeedback; + return IIRFilterNode::Create(*this, options, aRv); +} + +already_AddRefed<OscillatorNode> AudioContext::CreateOscillator( + ErrorResult& aRv) { + return OscillatorNode::Create(*this, OscillatorOptions(), aRv); +} + +already_AddRefed<PeriodicWave> AudioContext::CreatePeriodicWave( + const Sequence<float>& aRealData, const Sequence<float>& aImagData, + const PeriodicWaveConstraints& aConstraints, ErrorResult& aRv) { + RefPtr<PeriodicWave> periodicWave = new PeriodicWave( + this, aRealData.Elements(), aRealData.Length(), aImagData.Elements(), + aImagData.Length(), aConstraints.mDisableNormalization, aRv); + if (aRv.Failed()) { + return nullptr; + } + return periodicWave.forget(); +} + +AudioListener* AudioContext::Listener() { + if (!mListener) { + mListener = new AudioListener(this); + } + return mListener; +} + +double AudioContext::OutputLatency() { + if (mIsShutDown) { + return 0.0; + } + // When reduceFingerprinting is enabled, return a latency figure that is + // fixed, but plausible for the platform. + double latency_s = 0.0; + if (mShouldResistFingerprinting) { +#ifdef XP_MACOSX + latency_s = 512. / mSampleRate; +#elif MOZ_WIDGET_ANDROID + latency_s = 0.020; +#elif XP_WIN + latency_s = 0.04; +#else // Catchall for other OSes, including Linux. + latency_s = 0.025; +#endif + } else { + return Graph()->AudioOutputLatency(); + } + return latency_s; +} + +void AudioContext::GetOutputTimestamp(AudioTimestamp& aTimeStamp) { + if (!Destination()) { + aTimeStamp.mContextTime.Construct(0.0); + aTimeStamp.mPerformanceTime.Construct(0.0); + return; + } + + // The currentTime currently being output is the currentTime minus the audio + // output latency. The resolution of CurrentTime() is already reduced. + aTimeStamp.mContextTime.Construct( + std::max(0.0, CurrentTime() - OutputLatency())); + nsPIDOMWindowInner* parent = GetParentObject(); + Performance* perf = parent ? parent->GetPerformance() : nullptr; + if (perf) { + // perf->Now() already has reduced resolution here, no need to do it again. + aTimeStamp.mPerformanceTime.Construct( + std::max(0., perf->Now() - (OutputLatency() * 1000.))); + } else { + aTimeStamp.mPerformanceTime.Construct(0.0); + } +} + +Worklet* AudioContext::GetAudioWorklet(ErrorResult& aRv) { + if (!mWorklet) { + mWorklet = AudioWorkletImpl::CreateWorklet(this, aRv); + } + + return mWorklet; +} +bool AudioContext::IsRunning() const { + return mAudioContextState == AudioContextState::Running; +} + +already_AddRefed<Promise> AudioContext::CreatePromise(ErrorResult& aRv) { + // Get the relevant global for the promise from the wrapper cache because + // DOMEventTargetHelper::GetOwner() returns null if the document is unloaded. + // We know the wrapper exists because it is being used for |this| from JS. + // See https://github.com/heycam/webidl/issues/932 for why the relevant + // global is used instead of the current global. + nsCOMPtr<nsIGlobalObject> global = xpc::NativeGlobal(GetWrapper()); + RefPtr<Promise> promise = Promise::Create(global, aRv); + if (aRv.Failed()) { + return nullptr; + } + /** + * If this's relevant global object's associated Document is not fully + * active then return a promise rejected with "InvalidStateError" + * DOMException. + */ + nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(global); + if (!window->IsFullyActive()) { + promise->MaybeRejectWithInvalidStateError( + "The document is not fully active."); + } + return promise.forget(); +} + +already_AddRefed<Promise> AudioContext::DecodeAudioData( + const ArrayBuffer& aBuffer, + const Optional<OwningNonNull<DecodeSuccessCallback>>& aSuccessCallback, + const Optional<OwningNonNull<DecodeErrorCallback>>& aFailureCallback, + ErrorResult& aRv) { + AutoJSAPI jsapi; + jsapi.Init(); + JSContext* cx = jsapi.cx(); + + // CheckedUnwrapStatic is OK, since we know we have an ArrayBuffer. + JS::Rooted<JSObject*> obj(cx, js::CheckedUnwrapStatic(aBuffer.Obj())); + if (!obj) { + aRv.ThrowSecurityError("Can't get audio data from cross-origin object"); + return nullptr; + } + + RefPtr<Promise> promise = CreatePromise(aRv); + if (aRv.Failed() || promise->State() == Promise::PromiseState::Rejected) { + return promise.forget(); + } + + JSAutoRealm ar(cx, obj); + + // Detach the array buffer + size_t length = JS::GetArrayBufferByteLength(obj); + uint8_t* data = static_cast<uint8_t*>(JS::StealArrayBufferContents(cx, obj)); + if (!data) { + JS_ClearPendingException(cx); + + // Throw if the buffer is detached + aRv.ThrowTypeError("Buffer argument can't be a detached buffer"); + return nullptr; + } + + // Sniff the content of the media. + // Failed type sniffing will be handled by AsyncDecodeWebAudio. + nsAutoCString contentType; + NS_SniffContent(NS_DATA_SNIFFER_CATEGORY, nullptr, data, length, contentType); + + RefPtr<DecodeErrorCallback> failureCallback; + RefPtr<DecodeSuccessCallback> successCallback; + if (aFailureCallback.WasPassed()) { + failureCallback = &aFailureCallback.Value(); + } + if (aSuccessCallback.WasPassed()) { + successCallback = &aSuccessCallback.Value(); + } + UniquePtr<WebAudioDecodeJob> job( + new WebAudioDecodeJob(this, promise, successCallback, failureCallback)); + AsyncDecodeWebAudio(contentType.get(), data, length, *job); + // Transfer the ownership to mDecodeJobs + mDecodeJobs.AppendElement(std::move(job)); + + return promise.forget(); +} + +void AudioContext::RemoveFromDecodeQueue(WebAudioDecodeJob* aDecodeJob) { + // Since UniquePtr doesn't provide an operator== which allows you to compare + // against raw pointers, we need to iterate manually. + for (uint32_t i = 0; i < mDecodeJobs.Length(); ++i) { + if (mDecodeJobs[i].get() == aDecodeJob) { + mDecodeJobs.RemoveElementAt(i); + break; + } + } +} + +void AudioContext::RegisterActiveNode(AudioNode* aNode) { + if (!mCloseCalled) { + mActiveNodes.Insert(aNode); + } +} + +void AudioContext::UnregisterActiveNode(AudioNode* aNode) { + mActiveNodes.Remove(aNode); +} + +uint32_t AudioContext::MaxChannelCount() const { + if (mShouldResistFingerprinting) { + return 2; + } + return std::min<uint32_t>( + WebAudioUtils::MaxChannelCount, + mIsOffline ? mNumberOfChannels : CubebUtils::MaxNumberOfChannels()); +} + +uint32_t AudioContext::ActiveNodeCount() const { return mActiveNodes.Count(); } + +MediaTrackGraph* AudioContext::Graph() const { + return Destination()->Track()->Graph(); +} + +AudioNodeTrack* AudioContext::DestinationTrack() const { + if (Destination()) { + return Destination()->Track(); + } + return nullptr; +} + +void AudioContext::ShutdownWorklet() { + if (mWorklet) { + mWorklet->Impl()->NotifyWorkletFinished(); + } +} + +double AudioContext::CurrentTime() { + mozilla::MediaTrack* track = Destination()->Track(); + + double rawTime = track->TrackTimeToSeconds(track->GetCurrentTime()); + + // CurrentTime increments in intervals of 128/sampleRate. If the Timer + // Precision Reduction is smaller than this interval, the jittered time + // can always be reversed to the raw step of the interval. In that case + // we can simply return the un-reduced time; and avoid breaking tests. + // We have to convert each variable into a common magnitude, we choose ms. + if ((128 / mSampleRate) * 1000.0 > + nsRFPService::TimerResolution(mRTPCallerType) / 1000.0) { + return rawTime; + } + + // The value of a MediaTrack's CurrentTime will always advance forward; it + // will never reset (even if one rewinds a video.) Therefore we can use a + // single Random Seed initialized at the same time as the object. + return nsRFPService::ReduceTimePrecisionAsSecs( + rawTime, GetRandomTimelineSeed(), mRTPCallerType); +} + +nsISerialEventTarget* AudioContext::GetMainThread() const { + if (nsPIDOMWindowInner* window = GetParentObject()) { + return window->AsGlobal()->SerialEventTarget(); + } + return GetCurrentSerialEventTarget(); +} + +void AudioContext::DisconnectFromOwner() { + mIsDisconnecting = true; + MaybeClearPageAwakeRequest(); + OnWindowDestroy(); + DOMEventTargetHelper::DisconnectFromOwner(); +} + +void AudioContext::OnWindowDestroy() { + // Avoid resend the Telemetry data. + if (!mIsShutDown) { + MaybeUpdateAutoplayTelemetryWhenShutdown(); + } + mIsShutDown = true; + + CloseInternal(nullptr, AudioContextOperationFlags::None); + + // We don't want to touch promises if the global is going away soon. + if (!mIsDisconnecting) { + for (auto p : mPromiseGripArray) { + p->MaybeRejectWithInvalidStateError("Navigated away from page"); + } + + mPromiseGripArray.Clear(); + + for (const auto& p : mPendingResumePromises) { + p->MaybeRejectWithInvalidStateError("Navigated away from page"); + } + mPendingResumePromises.Clear(); + } + + // On process shutdown, the MTG thread shuts down before the destination + // track is destroyed, but AudioWorklet needs to release objects on the MTG + // thread. AudioContext::Shutdown() is invoked on processing the + // PBrowser::Destroy() message before xpcom shutdown begins. + ShutdownWorklet(); + + if (mDestination) { + // We can destroy the MediaTrackGraph at this point. + // Although there may be other clients using the graph, this graph is used + // only for clients in the same window and this window is going away. + // This will also interrupt any worklet script still running on the graph + // thread. + Graph()->ForceShutDown(); + // AudioDestinationNodes on rendering offline contexts have a + // self-reference which needs removal. + if (mIsOffline) { + mDestination->OfflineShutdown(); + } + } +} + +/* This runnable allows to fire the "statechange" event */ +class OnStateChangeTask final : public Runnable { + public: + explicit OnStateChangeTask(AudioContext* aAudioContext) + : Runnable("dom::OnStateChangeTask"), mAudioContext(aAudioContext) {} + + NS_IMETHODIMP + Run() override { + nsPIDOMWindowInner* parent = mAudioContext->GetParentObject(); + if (!parent) { + return NS_ERROR_FAILURE; + } + + Document* doc = parent->GetExtantDoc(); + if (!doc) { + return NS_ERROR_FAILURE; + } + + return nsContentUtils::DispatchTrustedEvent( + doc, mAudioContext, u"statechange"_ns, CanBubble::eNo, Cancelable::eNo); + } + + private: + RefPtr<AudioContext> mAudioContext; +}; + +void AudioContext::Dispatch(already_AddRefed<nsIRunnable>&& aRunnable) { + MOZ_ASSERT(NS_IsMainThread()); + // It can happen that this runnable took a long time to reach the main thread, + // and the global is not valid anymore. + if (GetParentObject()) { + AbstractThread::MainThread()->Dispatch(std::move(aRunnable)); + } else { + RefPtr<nsIRunnable> runnable(aRunnable); + runnable = nullptr; + } +} + +void AudioContext::OnStateChanged(void* aPromise, AudioContextState aNewState) { + MOZ_ASSERT(NS_IsMainThread()); + + if (mAudioContextState == AudioContextState::Closed) { + fprintf(stderr, + "Invalid transition: mAudioContextState: %d -> aNewState %d\n", + static_cast<int>(mAudioContextState), static_cast<int>(aNewState)); + MOZ_ASSERT(false); + } + + if (aPromise) { + Promise* promise = reinterpret_cast<Promise*>(aPromise); + // It is possible for the promise to have been removed from + // mPromiseGripArray if the cycle collector has severed our connections. DO + // NOT dereference the promise pointer in that case since it may point to + // already freed memory. + if (mPromiseGripArray.Contains(promise)) { + promise->MaybeResolveWithUndefined(); + DebugOnly<bool> rv = mPromiseGripArray.RemoveElement(promise); + MOZ_ASSERT(rv, "Promise wasn't in the grip array?"); + } + } + + // Resolve all pending promises once the audio context has been allowed to + // start. + if (aNewState == AudioContextState::Running) { + for (const auto& p : mPendingResumePromises) { + p->MaybeResolveWithUndefined(); + } + mPendingResumePromises.Clear(); + } + + if (mAudioContextState != aNewState) { + RefPtr<OnStateChangeTask> task = new OnStateChangeTask(this); + Dispatch(task.forget()); + } + + mAudioContextState = aNewState; + Destination()->NotifyAudioContextStateChanged(); + MaybeUpdatePageAwakeRequest(); +} + +BrowsingContext* AudioContext::GetTopLevelBrowsingContext() { + nsCOMPtr<nsPIDOMWindowInner> window = GetParentObject(); + if (!window) { + return nullptr; + } + BrowsingContext* bc = window->GetBrowsingContext(); + if (!bc || bc->IsDiscarded()) { + return nullptr; + } + return bc->Top(); +} + +void AudioContext::MaybeUpdatePageAwakeRequest() { + // No need to keep page awake for offline context. + if (IsOffline()) { + return; + } + + if (mIsShutDown) { + return; + } + + if (IsRunning() && !mSetPageAwakeRequest) { + SetPageAwakeRequest(true); + } else if (!IsRunning() && mSetPageAwakeRequest) { + SetPageAwakeRequest(false); + } +} + +void AudioContext::SetPageAwakeRequest(bool aShouldSet) { + mSetPageAwakeRequest = aShouldSet; + BrowsingContext* bc = GetTopLevelBrowsingContext(); + if (!bc) { + return; + } + if (XRE_IsContentProcess()) { + ContentChild* contentChild = ContentChild::GetSingleton(); + Unused << contentChild->SendAddOrRemovePageAwakeRequest(bc, aShouldSet); + return; + } + if (aShouldSet) { + bc->Canonical()->AddPageAwakeRequest(); + } else { + bc->Canonical()->RemovePageAwakeRequest(); + } +} + +void AudioContext::MaybeClearPageAwakeRequest() { + if (mSetPageAwakeRequest) { + SetPageAwakeRequest(false); + } +} + +nsTArray<RefPtr<mozilla::MediaTrack>> AudioContext::GetAllTracks() const { + nsTArray<RefPtr<mozilla::MediaTrack>> tracks; + for (AudioNode* node : mAllNodes) { + mozilla::MediaTrack* t = node->GetTrack(); + if (t) { + tracks.AppendElement(t); + } + // Add the tracks of AudioParam. + const nsTArray<RefPtr<AudioParam>>& audioParams = node->GetAudioParams(); + if (!audioParams.IsEmpty()) { + for (auto& param : audioParams) { + t = param->GetTrack(); + if (t && !tracks.Contains(t)) { + tracks.AppendElement(t); + } + } + } + } + return tracks; +} + +already_AddRefed<Promise> AudioContext::Suspend(ErrorResult& aRv) { + TRACE("AudioContext::Suspend"); + RefPtr<Promise> promise = CreatePromise(aRv); + if (aRv.Failed() || promise->State() == Promise::PromiseState::Rejected) { + return promise.forget(); + } + if (mIsOffline) { + // XXXbz This is not reachable, since we don't implement this + // method on OfflineAudioContext at all! + promise->MaybeRejectWithNotSupportedError( + "Can't suspend OfflineAudioContext yet"); + return promise.forget(); + } + + if (mCloseCalled) { + promise->MaybeRejectWithInvalidStateError( + "Can't suspend if the control thread state is \"closed\""); + return promise.forget(); + } + + mSuspendedByContent = true; + mPromiseGripArray.AppendElement(promise); + SuspendInternal(promise, AudioContextOperationFlags::SendStateChange); + return promise.forget(); +} + +void AudioContext::SuspendFromChrome() { + if (mIsOffline || mIsShutDown) { + return; + } + MOZ_ASSERT(!mSuspendedByChrome); + mSuspendedByChrome = true; + SuspendInternal(nullptr, Preferences::GetBool("dom.audiocontext.testing") + ? AudioContextOperationFlags::SendStateChange + : AudioContextOperationFlags::None); +} + +void AudioContext::SuspendInternal(void* aPromise, + AudioContextOperationFlags aFlags) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!mIsOffline); + Destination()->Suspend(); + + nsTArray<RefPtr<mozilla::MediaTrack>> tracks; + // If mTracksAreSuspended is true then we already suspended all our tracks, + // so don't suspend them again (since suspend(); suspend(); resume(); should + // cancel both suspends). But we still need to do ApplyAudioContextOperation + // to ensure our new promise is resolved. + if (!mTracksAreSuspended) { + mTracksAreSuspended = true; + tracks = GetAllTracks(); + } + auto promise = Graph()->ApplyAudioContextOperation( + DestinationTrack(), std::move(tracks), AudioContextOperation::Suspend); + if ((aFlags & AudioContextOperationFlags::SendStateChange)) { + promise->Then( + GetMainThread(), "AudioContext::OnStateChanged", + [self = RefPtr<AudioContext>(this), + aPromise](AudioContextState aNewState) { + self->OnStateChanged(aPromise, aNewState); + }, + [] { MOZ_CRASH("Unexpected rejection"); }); + } +} + +void AudioContext::ResumeFromChrome() { + if (mIsOffline || mIsShutDown) { + return; + } + MOZ_ASSERT(mSuspendedByChrome); + mSuspendedByChrome = false; + if (!mWasAllowedToStart) { + return; + } + ResumeInternal(); +} + +already_AddRefed<Promise> AudioContext::Resume(ErrorResult& aRv) { + TRACE("AudioContext::Resume"); + RefPtr<Promise> promise = CreatePromise(aRv); + if (aRv.Failed() || promise->State() == Promise::PromiseState::Rejected) { + return promise.forget(); + } + + if (mIsOffline) { + promise->MaybeRejectWithNotSupportedError( + "Can't resume OfflineAudioContext"); + return promise.forget(); + } + + if (mCloseCalled) { + promise->MaybeRejectWithInvalidStateError( + "Can't resume if the control thread state is \"closed\""); + return promise.forget(); + } + + mSuspendedByContent = false; + mPendingResumePromises.AppendElement(promise); + + const bool isAllowedToPlay = media::AutoplayPolicy::IsAllowedToPlay(*this); + AUTOPLAY_LOG("Trying to resume AudioContext %p, IsAllowedToPlay=%d", this, + isAllowedToPlay); + if (isAllowedToPlay) { + ResumeInternal(); + } else { + ReportBlocked(); + } + + MaybeUpdateAutoplayTelemetry(); + + return promise.forget(); +} + +void AudioContext::ResumeInternal() { + MOZ_ASSERT(!mIsOffline); + AUTOPLAY_LOG("Allow to resume AudioContext %p", this); + mWasAllowedToStart = true; + + if (mSuspendedByChrome || mSuspendedByContent || mCloseCalled) { + MOZ_ASSERT(mTracksAreSuspended); + return; + } + + Destination()->Resume(); + + nsTArray<RefPtr<mozilla::MediaTrack>> tracks; + // If mTracksAreSuspended is false then we already resumed all our tracks, + // so don't resume them again (since suspend(); resume(); resume(); should + // be OK). But we still need to do ApplyAudioContextOperation + // to ensure our new promise is resolved. + if (mTracksAreSuspended) { + mTracksAreSuspended = false; + tracks = GetAllTracks(); + } + // Check for statechange even when resumed from chrome because content may + // have called Resume() before chrome resumed the window. + Graph() + ->ApplyAudioContextOperation(DestinationTrack(), std::move(tracks), + AudioContextOperation::Resume) + ->Then( + GetMainThread(), "AudioContext::OnStateChanged", + [self = RefPtr<AudioContext>(this)](AudioContextState aNewState) { + self->OnStateChanged(nullptr, aNewState); + }, + [] {}); // Promise may be rejected after graph shutdown. +} + +void AudioContext::UpdateAutoplayAssumptionStatus() { + if (media::AutoplayPolicyTelemetryUtils:: + WouldBeAllowedToPlayIfAutoplayDisabled(*this)) { + mWasEverAllowedToStart |= true; + mWouldBeAllowedToStart = true; + } else { + mWasEverBlockedToStart |= true; + mWouldBeAllowedToStart = false; + } +} + +void AudioContext::MaybeUpdateAutoplayTelemetry() { + // Exclude offline AudioContext because it's always allowed to start. + if (mIsOffline) { + return; + } + + if (media::AutoplayPolicyTelemetryUtils:: + WouldBeAllowedToPlayIfAutoplayDisabled(*this) && + !mWouldBeAllowedToStart) { + AccumulateCategorical( + mozilla::Telemetry::LABELS_WEB_AUDIO_AUTOPLAY::AllowedAfterBlocked); + } + UpdateAutoplayAssumptionStatus(); +} + +void AudioContext::MaybeUpdateAutoplayTelemetryWhenShutdown() { + // Exclude offline AudioContext because it's always allowed to start. + if (mIsOffline) { + return; + } + + if (mWasEverAllowedToStart && !mWasEverBlockedToStart) { + AccumulateCategorical( + mozilla::Telemetry::LABELS_WEB_AUDIO_AUTOPLAY::NeverBlocked); + } else if (!mWasEverAllowedToStart && mWasEverBlockedToStart) { + AccumulateCategorical( + mozilla::Telemetry::LABELS_WEB_AUDIO_AUTOPLAY::NeverAllowed); + } +} + +void AudioContext::ReportBlocked() { + ReportToConsole(nsIScriptError::warningFlag, + "BlockAutoplayWebAudioStartError"); + mWasAllowedToStart = false; + + if (!StaticPrefs::media_autoplay_block_event_enabled()) { + return; + } + + RefPtr<AudioContext> self = this; + RefPtr<nsIRunnable> r = + NS_NewRunnableFunction("AudioContext::AutoplayBlocked", [self]() { + nsPIDOMWindowInner* parent = self->GetParentObject(); + if (!parent) { + return; + } + + Document* doc = parent->GetExtantDoc(); + if (!doc) { + return; + } + + AUTOPLAY_LOG("Dispatch `blocked` event for AudioContext %p", + self.get()); + nsContentUtils::DispatchTrustedEvent(doc, self, u"blocked"_ns, + CanBubble::eNo, Cancelable::eNo); + }); + Dispatch(r.forget()); +} + +already_AddRefed<Promise> AudioContext::Close(ErrorResult& aRv) { + TRACE("AudioContext::Close"); + RefPtr<Promise> promise = CreatePromise(aRv); + if (aRv.Failed() || promise->State() == Promise::PromiseState::Rejected) { + return promise.forget(); + } + + if (mIsOffline) { + // XXXbz This is not reachable, since we don't implement this + // method on OfflineAudioContext at all! + promise->MaybeRejectWithNotSupportedError( + "Can't close OfflineAudioContext yet"); + return promise.forget(); + } + + if (mCloseCalled) { + promise->MaybeRejectWithInvalidStateError( + "Can't close an AudioContext twice"); + return promise.forget(); + } + + mPromiseGripArray.AppendElement(promise); + + CloseInternal(promise, AudioContextOperationFlags::SendStateChange); + + return promise.forget(); +} + +void AudioContext::OfflineClose() { + CloseInternal(nullptr, AudioContextOperationFlags::None); +} + +void AudioContext::CloseInternal(void* aPromise, + AudioContextOperationFlags aFlags) { + // This can be called when freeing a document, and the tracks are dead at + // this point, so we need extra null-checks. + AudioNodeTrack* ds = DestinationTrack(); + if (ds && !mIsOffline) { + Destination()->Close(); + + nsTArray<RefPtr<mozilla::MediaTrack>> tracks; + // If mTracksAreSuspended or mCloseCalled are true then we already suspended + // all our tracks, so don't suspend them again. But we still need to do + // ApplyAudioContextOperation to ensure our new promise is resolved. + if (!mTracksAreSuspended && !mCloseCalled) { + tracks = GetAllTracks(); + } + auto promise = Graph()->ApplyAudioContextOperation( + ds, std::move(tracks), AudioContextOperation::Close); + if ((aFlags & AudioContextOperationFlags::SendStateChange)) { + promise->Then( + GetMainThread(), "AudioContext::OnStateChanged", + [self = RefPtr<AudioContext>(this), + aPromise](AudioContextState aNewState) { + self->OnStateChanged(aPromise, aNewState); + }, + [] {}); // Promise may be rejected after graph shutdown. + } + } + mCloseCalled = true; + // Release references to active nodes. + // Active AudioNodes don't unregister in destructors, at which point the + // Node is already unregistered. + mActiveNodes.Clear(); +} + +void AudioContext::RegisterNode(AudioNode* aNode) { + MOZ_ASSERT(!mAllNodes.Contains(aNode)); + mAllNodes.Insert(aNode); +} + +void AudioContext::UnregisterNode(AudioNode* aNode) { + MOZ_ASSERT(mAllNodes.Contains(aNode)); + mAllNodes.Remove(aNode); +} + +already_AddRefed<Promise> AudioContext::StartRendering(ErrorResult& aRv) { + MOZ_ASSERT(mIsOffline, "This should only be called on OfflineAudioContext"); + RefPtr<Promise> promise = CreatePromise(aRv); + if (aRv.Failed() || promise->State() == Promise::PromiseState::Rejected) { + return promise.forget(); + } + if (mIsStarted) { + aRv.ThrowInvalidStateError("Rendering already started"); + return nullptr; + } + + mIsStarted = true; + mDestination->StartRendering(promise); + + OnStateChanged(nullptr, AudioContextState::Running); + + return promise.forget(); +} + +unsigned long AudioContext::Length() { + MOZ_ASSERT(mIsOffline); + return mDestination->Length(); +} + +void AudioContext::Mute() const { + MOZ_ASSERT(!mIsOffline); + if (mDestination) { + mDestination->Mute(); + } +} + +void AudioContext::Unmute() const { + MOZ_ASSERT(!mIsOffline); + if (mDestination) { + mDestination->Unmute(); + } +} + +void AudioContext::SetParamMapForWorkletName( + const nsAString& aName, AudioParamDescriptorMap* aParamMap) { + MOZ_ASSERT(!mWorkletParamDescriptors.Contains(aName)); + Unused << mWorkletParamDescriptors.InsertOrUpdate( + aName, std::move(*aParamMap), fallible); +} + +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 += mDecodeJobs.ShallowSizeOfExcludingThis(aMallocSizeOf); + for (uint32_t i = 0; i < mDecodeJobs.Length(); ++i) { + amount += mDecodeJobs[i]->SizeOfIncludingThis(aMallocSizeOf); + } + amount += mActiveNodes.ShallowSizeOfExcludingThis(aMallocSizeOf); + return amount; +} + +NS_IMETHODIMP +AudioContext::CollectReports(nsIHandleReportCallback* aHandleReport, + nsISupports* aData, bool aAnonymize) { + const nsLiteralCString nodeDescription( + "Memory used by AudioNode DOM objects (Web Audio)."); + for (AudioNode* node : mAllNodes) { + int64_t amount = node->SizeOfIncludingThis(MallocSizeOf); + nsPrintfCString domNodePath("explicit/webaudio/audio-node/%s/dom-nodes", + node->NodeType()); + aHandleReport->Callback(""_ns, domNodePath, KIND_HEAP, UNITS_BYTES, amount, + nodeDescription, aData); + } + + int64_t amount = SizeOfIncludingThis(MallocSizeOf); + MOZ_COLLECT_REPORT("explicit/webaudio/audiocontext", KIND_HEAP, UNITS_BYTES, + amount, + "Memory used by AudioContext objects (Web Audio)."); + + return NS_OK; +} + +BasicWaveFormCache* AudioContext::GetBasicWaveFormCache() { + MOZ_ASSERT(NS_IsMainThread()); + if (!mBasicWaveFormCache) { + mBasicWaveFormCache = new BasicWaveFormCache(SampleRate()); + } + return mBasicWaveFormCache; +} + +void AudioContext::ReportToConsole(uint32_t aErrorFlags, + const char* aMsg) const { + MOZ_ASSERT(aMsg); + Document* doc = + GetParentObject() ? GetParentObject()->GetExtantDoc() : nullptr; + nsContentUtils::ReportToConsole(aErrorFlags, "Media"_ns, doc, + nsContentUtils::eDOM_PROPERTIES, aMsg); +} + +BasicWaveFormCache::BasicWaveFormCache(uint32_t aSampleRate) + : mSampleRate(aSampleRate) { + MOZ_ASSERT(NS_IsMainThread()); +} +BasicWaveFormCache::~BasicWaveFormCache() = default; + +WebCore::PeriodicWave* BasicWaveFormCache::GetBasicWaveForm( + OscillatorType aType) { + MOZ_ASSERT(!NS_IsMainThread()); + if (aType == OscillatorType::Sawtooth) { + if (!mSawtooth) { + mSawtooth = WebCore::PeriodicWave::createSawtooth(mSampleRate); + } + return mSawtooth; + } + if (aType == OscillatorType::Square) { + if (!mSquare) { + mSquare = WebCore::PeriodicWave::createSquare(mSampleRate); + } + return mSquare; + } + if (aType == OscillatorType::Triangle) { + if (!mTriangle) { + mTriangle = WebCore::PeriodicWave::createTriangle(mSampleRate); + } + return mTriangle; + } + MOZ_ASSERT(false, "Not reached"); + return nullptr; +} + +} // namespace mozilla::dom diff --git a/dom/media/webaudio/AudioContext.h b/dom/media/webaudio/AudioContext.h new file mode 100644 index 0000000000..81a64a18bf --- /dev/null +++ b/dom/media/webaudio/AudioContext.h @@ -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/. */ + +#ifndef AudioContext_h_ +#define AudioContext_h_ + +#include "X11UndefineNone.h" +#include "AudioParamDescriptorMap.h" +#include "mozilla/dom/OfflineAudioContextBinding.h" +#include "mozilla/dom/AudioContextBinding.h" +#include "MediaBufferDecoder.h" +#include "mozilla/Attributes.h" +#include "mozilla/DOMEventTargetHelper.h" +#include "mozilla/MemoryReporting.h" +#include "mozilla/dom/TypedArray.h" +#include "mozilla/RelativeTimeline.h" +#include "mozilla/TypedEnumBits.h" +#include "mozilla/UniquePtr.h" +#include "nsCOMPtr.h" +#include "nsCycleCollectionParticipant.h" +#include "nsTHashMap.h" +#include "nsHashKeys.h" +#include "nsTHashSet.h" +#include "js/TypeDecls.h" +#include "nsIMemoryReporter.h" + +namespace WebCore { +class PeriodicWave; +} // namespace WebCore + +class nsPIDOMWindowInner; + +namespace mozilla { + +class DOMMediaStream; +class ErrorResult; +class MediaTrack; +class MediaTrackGraph; +class AudioNodeTrack; + +namespace dom { + +enum class AudioContextState : uint8_t; +class AnalyserNode; +class AudioBuffer; +class AudioBufferSourceNode; +class AudioDestinationNode; +class AudioListener; +class AudioNode; +class BiquadFilterNode; +class BrowsingContext; +class ChannelMergerNode; +class ChannelSplitterNode; +class ConstantSourceNode; +class ConvolverNode; +class DelayNode; +class DynamicsCompressorNode; +class GainNode; +class GlobalObject; +class HTMLMediaElement; +class IIRFilterNode; +class MediaElementAudioSourceNode; +class MediaStreamAudioDestinationNode; +class MediaStreamAudioSourceNode; +class MediaStreamTrack; +class MediaStreamTrackAudioSourceNode; +class OscillatorNode; +class PannerNode; +class ScriptProcessorNode; +class StereoPannerNode; +class WaveShaperNode; +class Worklet; +class PeriodicWave; +struct PeriodicWaveConstraints; +class Promise; +enum class OscillatorType : uint8_t; + +// This is addrefed by the OscillatorNodeEngine on the main thread +// and then used from the MTG thread. +// It can be released either from the graph thread or the main thread. +class BasicWaveFormCache { + public: + explicit BasicWaveFormCache(uint32_t aSampleRate); + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(BasicWaveFormCache) + WebCore::PeriodicWave* GetBasicWaveForm(OscillatorType aType); + + private: + ~BasicWaveFormCache(); + RefPtr<WebCore::PeriodicWave> mSawtooth; + RefPtr<WebCore::PeriodicWave> mSquare; + RefPtr<WebCore::PeriodicWave> mTriangle; + uint32_t mSampleRate; +}; + +/* This runnable allows the MTG to notify the main thread when audio is actually + * flowing */ +class StateChangeTask final : public Runnable { + public: + /* This constructor should be used when this event is sent from the main + * thread. */ + StateChangeTask(AudioContext* aAudioContext, void* aPromise, + AudioContextState aNewState); + + /* This constructor should be used when this event is sent from the audio + * thread. */ + StateChangeTask(AudioNodeTrack* aTrack, void* aPromise, + AudioContextState aNewState); + + NS_IMETHOD Run() override; + + private: + RefPtr<AudioContext> mAudioContext; + void* mPromise; + RefPtr<AudioNodeTrack> mAudioNodeTrack; + AudioContextState mNewState; +}; + +enum class AudioContextOperation : uint8_t { Suspend, Resume, Close }; +static const char* const kAudioContextOptionsStrings[] = {"Suspend", "Resume", + "Close"}; +// When suspending or resuming an AudioContext, some operations have to notify +// the main thread, so that the Promise is resolved, the state is modified, and +// the statechanged event is sent. Some other operations don't go back to the +// main thread, for example when the AudioContext is paused by something that is +// not caused by the page itself: opening a debugger, breaking on a breakpoint, +// reloading a document. +enum class AudioContextOperationFlags { None, SendStateChange }; +MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(AudioContextOperationFlags); + +struct AudioContextOptions; + +class AudioContext final : public DOMEventTargetHelper, + public nsIMemoryReporter, + public RelativeTimeline { + AudioContext(nsPIDOMWindowInner* aParentWindow, bool aIsOffline, + uint32_t aNumberOfChannels = 0, uint32_t aLength = 0, + float aSampleRate = 0.0f); + ~AudioContext(); + + public: + typedef uint64_t AudioContextId; + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(AudioContext, DOMEventTargetHelper) + MOZ_DEFINE_MALLOC_SIZE_OF(MallocSizeOf) + + nsPIDOMWindowInner* GetParentObject() const { return GetOwner(); } + + nsISerialEventTarget* GetMainThread() const; + + virtual void DisconnectFromOwner() override; + + void OnWindowDestroy(); // idempotent + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + using DOMEventTargetHelper::DispatchTrustedEvent; + + // Constructor for regular AudioContext + static already_AddRefed<AudioContext> Constructor( + const GlobalObject& aGlobal, const AudioContextOptions& aOptions, + ErrorResult& aRv); + + // Constructor for offline AudioContext with options object + static already_AddRefed<AudioContext> Constructor( + const GlobalObject& aGlobal, const OfflineAudioContextOptions& aOptions, + ErrorResult& aRv); + + // Constructor for offline AudioContext + static already_AddRefed<AudioContext> Constructor(const GlobalObject& aGlobal, + uint32_t aNumberOfChannels, + uint32_t aLength, + float aSampleRate, + ErrorResult& aRv); + + // AudioContext methods + + AudioDestinationNode* Destination() const { return mDestination; } + + float SampleRate() const { return mSampleRate; } + + bool ShouldSuspendNewTrack() const { + return mTracksAreSuspended || mCloseCalled; + } + double CurrentTime(); + + AudioListener* Listener(); + + AudioContextState State() const { return mAudioContextState; } + + double BaseLatency() const { + // Gecko does not do any buffering between rendering the audio and sending + // it to the audio subsystem. + return 0.0; + } + + double OutputLatency(); + + void GetOutputTimestamp(AudioTimestamp& aTimeStamp); + + Worklet* GetAudioWorklet(ErrorResult& aRv); + + bool IsRunning() const; + + // Called when an AudioScheduledSourceNode started or the source node starts, + // this method might resume the AudioContext if it was not allowed to start. + void StartBlockedAudioContextIfAllowed(); + + // Those three methods return a promise to content, that is resolved when an + // (possibly long) operation is completed on the MTG (and possibly other) + // thread(s). To avoid having to match the calls and asychronous result when + // the operation is completed, we keep a reference to the promises on the main + // thread, and then send the promises pointers down the MTG thread, as a void* + // (to make it very clear that the pointer is to merely be treated as an ID). + // When back on the main thread, we can resolve or reject the promise, by + // casting it back to a `Promise*` while asserting we're back on the main + // thread and removing the reference we added. + already_AddRefed<Promise> Suspend(ErrorResult& aRv); + already_AddRefed<Promise> Resume(ErrorResult& aRv); + already_AddRefed<Promise> Close(ErrorResult& aRv); + IMPL_EVENT_HANDLER(statechange) + + // These two functions are similar with Suspend() and Resume(), the difference + // is they are designed for calling from chrome side, not content side. eg. + // calling from inner window, so we won't need to return promise for caller. + void SuspendFromChrome(); + void ResumeFromChrome(); + // Called on completion of offline rendering: + void OfflineClose(); + + already_AddRefed<AudioBufferSourceNode> CreateBufferSource(); + + already_AddRefed<ConstantSourceNode> CreateConstantSource(); + + already_AddRefed<AudioBuffer> CreateBuffer(uint32_t aNumberOfChannels, + uint32_t aLength, + float aSampleRate, + ErrorResult& aRv); + + already_AddRefed<MediaStreamAudioDestinationNode> + CreateMediaStreamDestination(ErrorResult& aRv); + + already_AddRefed<ScriptProcessorNode> CreateScriptProcessor( + uint32_t aBufferSize, uint32_t aNumberOfInputChannels, + uint32_t aNumberOfOutputChannels, ErrorResult& aRv); + + already_AddRefed<StereoPannerNode> CreateStereoPanner(ErrorResult& aRv); + + already_AddRefed<AnalyserNode> CreateAnalyser(ErrorResult& aRv); + + already_AddRefed<GainNode> CreateGain(ErrorResult& aRv); + + already_AddRefed<WaveShaperNode> CreateWaveShaper(ErrorResult& aRv); + + already_AddRefed<MediaElementAudioSourceNode> CreateMediaElementSource( + HTMLMediaElement& aMediaElement, ErrorResult& aRv); + already_AddRefed<MediaStreamAudioSourceNode> CreateMediaStreamSource( + DOMMediaStream& aMediaStream, ErrorResult& aRv); + already_AddRefed<MediaStreamTrackAudioSourceNode> + CreateMediaStreamTrackSource(MediaStreamTrack& aMediaStreamTrack, + ErrorResult& aRv); + + already_AddRefed<DelayNode> CreateDelay(double aMaxDelayTime, + ErrorResult& aRv); + + already_AddRefed<PannerNode> CreatePanner(ErrorResult& aRv); + + already_AddRefed<ConvolverNode> CreateConvolver(ErrorResult& aRv); + + already_AddRefed<ChannelSplitterNode> CreateChannelSplitter( + uint32_t aNumberOfOutputs, ErrorResult& aRv); + + already_AddRefed<ChannelMergerNode> CreateChannelMerger( + uint32_t aNumberOfInputs, ErrorResult& aRv); + + already_AddRefed<DynamicsCompressorNode> CreateDynamicsCompressor( + ErrorResult& aRv); + + already_AddRefed<BiquadFilterNode> CreateBiquadFilter(ErrorResult& aRv); + + already_AddRefed<IIRFilterNode> CreateIIRFilter( + const Sequence<double>& aFeedforward, const Sequence<double>& aFeedback, + mozilla::ErrorResult& aRv); + + already_AddRefed<OscillatorNode> CreateOscillator(ErrorResult& aRv); + + already_AddRefed<PeriodicWave> CreatePeriodicWave( + const Sequence<float>& aRealData, const Sequence<float>& aImagData, + const PeriodicWaveConstraints& aConstraints, ErrorResult& aRv); + + already_AddRefed<Promise> DecodeAudioData( + const ArrayBuffer& aBuffer, + const Optional<OwningNonNull<DecodeSuccessCallback>>& aSuccessCallback, + const Optional<OwningNonNull<DecodeErrorCallback>>& aFailureCallback, + ErrorResult& aRv); + + // OfflineAudioContext methods + already_AddRefed<Promise> StartRendering(ErrorResult& aRv); + IMPL_EVENT_HANDLER(complete) + unsigned long Length(); + + bool IsOffline() const { return mIsOffline; } + + bool ShouldResistFingerprinting() const { + return mShouldResistFingerprinting; + } + + MediaTrackGraph* Graph() const; + AudioNodeTrack* DestinationTrack() const; + + // Nodes register here if they will produce sound even if they have silent + // or no input connections. The AudioContext will keep registered nodes + // alive until the context is collected. This takes care of "playing" + // references and "tail-time" references. + void RegisterActiveNode(AudioNode* aNode); + // Nodes unregister when they have finished producing sound for the + // foreseeable future. + // Do NOT call UnregisterActiveNode from an AudioNode destructor. + // If the destructor is called, then the Node has already been unregistered. + // The destructor may be called during hashtable enumeration, during which + // unregistering would not be safe. + void UnregisterActiveNode(AudioNode* aNode); + + uint32_t MaxChannelCount() const; + + uint32_t ActiveNodeCount() const; + + void Mute() const; + void Unmute() const; + + void RegisterNode(AudioNode* aNode); + void UnregisterNode(AudioNode* aNode); + + void OnStateChanged(void* aPromise, AudioContextState aNewState); + + BasicWaveFormCache* GetBasicWaveFormCache(); + + void ShutdownWorklet(); + // Steals from |aParamMap| + void SetParamMapForWorkletName(const nsAString& aName, + AudioParamDescriptorMap* aParamMap); + const AudioParamDescriptorMap* GetParamMapForWorkletName( + const nsAString& aName) { + return mWorkletParamDescriptors.Lookup(aName).DataPtrOrNull(); + } + + void Dispatch(already_AddRefed<nsIRunnable>&& aRunnable); + + private: + void DisconnectFromWindow(); + already_AddRefed<Promise> CreatePromise(ErrorResult& aRv); + void RemoveFromDecodeQueue(WebAudioDecodeJob* aDecodeJob); + void ShutdownDecoder(); + + size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const; + NS_DECL_NSIMEMORYREPORTER + + friend struct ::mozilla::WebAudioDecodeJob; + + nsTArray<RefPtr<mozilla::MediaTrack>> GetAllTracks() const; + + void ResumeInternal(); + void SuspendInternal(void* aPromise, AudioContextOperationFlags aFlags); + void CloseInternal(void* aPromise, AudioContextOperationFlags aFlags); + + // Will report error message to console and dispatch testing event if needed + // when AudioContext is blocked by autoplay policy. + void ReportBlocked(); + + void ReportToConsole(uint32_t aErrorFlags, const char* aMsg) const; + + // This function should be called everytime we decide whether allow to start + // audio context, it's used to update Telemetry related variables. + void UpdateAutoplayAssumptionStatus(); + + // These functions are used for updating Telemetry. + // - MaybeUpdateAutoplayTelemetry: update category 'AllowedAfterBlocked' + // - MaybeUpdateAutoplayTelemetryWhenShutdown: update category 'NeverBlocked' + // and 'NeverAllowed', so we need to call it when shutdown AudioContext + void MaybeUpdateAutoplayTelemetry(); + void MaybeUpdateAutoplayTelemetryWhenShutdown(); + + // If the pref `dom.suspend_inactive.enabled` is enabled, the dom window will + // be suspended when the window becomes inactive. In order to keep audio + // context running still, we will ask pages to keep awake in that situation. + void MaybeUpdatePageAwakeRequest(); + void MaybeClearPageAwakeRequest(); + void SetPageAwakeRequest(bool aShouldSet); + + BrowsingContext* GetTopLevelBrowsingContext(); + + private: + // Each AudioContext has an id, that is passed down the MediaTracks that + // back the AudioNodes, so we can easily compute the set of all the + // MediaTracks for a given context, on the MediasTrackGraph side. + const AudioContextId mId; + // Note that it's important for mSampleRate to be initialized before + // mDestination, as mDestination's constructor needs to access it! + const float mSampleRate; + AudioContextState mAudioContextState; + RefPtr<AudioDestinationNode> mDestination; + RefPtr<AudioListener> mListener; + RefPtr<Worklet> mWorklet; + nsTArray<UniquePtr<WebAudioDecodeJob>> mDecodeJobs; + // This array is used to keep the suspend/close promises alive until + // they are resolved, so we can safely pass them accross threads. + nsTArray<RefPtr<Promise>> mPromiseGripArray; + // This array is used to onlly keep the resume promises alive until they are + // resolved, so we can safely pass them accross threads. If the audio context + // is not allowed to play, the promise would be pending in this array and be + // resolved until audio context has been allowed and user call resume() again. + nsTArray<RefPtr<Promise>> mPendingResumePromises; + // See RegisterActiveNode. These will keep the AudioContext alive while it + // is rendering and the window remains alive. + nsTHashSet<RefPtr<AudioNode>> mActiveNodes; + // Raw (non-owning) references to all AudioNodes for this AudioContext. + nsTHashSet<AudioNode*> mAllNodes; + nsTHashMap<nsStringHashKey, AudioParamDescriptorMap> mWorkletParamDescriptors; + // Cache to avoid recomputing basic waveforms all the time. + RefPtr<BasicWaveFormCache> mBasicWaveFormCache; + // Number of channels passed in the OfflineAudioContext ctor. + uint32_t mNumberOfChannels; + const RTPCallerType mRTPCallerType; + const bool mShouldResistFingerprinting; + const bool mIsOffline; + // true iff realtime or startRendering() has been called. + bool mIsStarted; + bool mIsShutDown; + bool mIsDisconnecting; + // Close has been called; reject suspend and resume calls. + bool mCloseCalled; + // Whether the MediaTracks are suspended, due to one or more of + // !mWasAllowedToStart, mSuspendedByContent, or mSuspendedByChrome. + // false if offline. + bool mTracksAreSuspended; + // This flag stores the value of previous status of `allowed-to-start`. + // true if offline. + bool mWasAllowedToStart; + // Whether this AudioContext is suspended because the page called suspend(). + // Unused if offline. + bool mSuspendedByContent; + // Whether this AudioContext is suspended because the Window is suspended. + // Unused if offline. + bool mSuspendedByChrome; + + // These variables are used for telemetry, they're not reflect the actual + // status of AudioContext, they are based on the "assumption" of enabling + // blocking web audio. Because we want to record Telemetry no matter user + // enable blocking autoplay or not. + // - 'mWasEverAllowedToStart' would be true when AudioContext had ever been + // allowed to start if we enable blocking web audio. + // - 'mWasEverBlockedToStart' would be true when AudioContext had ever been + // blocked to start if we enable blocking web audio. + // - 'mWouldBeAllowedToStart' stores the value of previous status of + // `allowed-to-start` if we enable blocking web audio. + bool mWasEverAllowedToStart; + bool mWasEverBlockedToStart; + bool mWouldBeAllowedToStart; + + // Whether we have set the page awake reqeust when non-offline audio context + // is running. That will keep the audio context being able to continue running + // even if the window is inactive. + bool mSetPageAwakeRequest = false; +}; + +static const dom::AudioContext::AudioContextId NO_AUDIO_CONTEXT = 0; + +} // namespace dom +} // namespace mozilla + +inline nsISupports* ToSupports(mozilla::dom::AudioContext* p) { + return NS_CYCLE_COLLECTION_CLASSNAME(mozilla::dom::AudioContext)::Upcast(p); +} + +#endif diff --git a/dom/media/webaudio/AudioDestinationNode.cpp b/dom/media/webaudio/AudioDestinationNode.cpp new file mode 100644 index 0000000000..1a9c3c0f7b --- /dev/null +++ b/dom/media/webaudio/AudioDestinationNode.cpp @@ -0,0 +1,675 @@ +/* -*- 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 "AudioDestinationNode.h" + +#include "AlignmentUtils.h" +#include "AudibilityMonitor.h" +#include "AudioChannelService.h" +#include "AudioContext.h" +#include "AudioNodeEngine.h" +#include "AudioNodeTrack.h" +#include "CubebUtils.h" +#include "MediaTrackGraph.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/dom/AudioDestinationNodeBinding.h" +#include "mozilla/dom/BaseAudioContextBinding.h" +#include "mozilla/dom/OfflineAudioCompletionEvent.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/dom/WakeLock.h" +#include "mozilla/dom/power/PowerManagerService.h" +#include "mozilla/Telemetry.h" +#include "mozilla/TelemetryHistogramEnums.h" +#include "nsContentUtils.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsIScriptObjectPrincipal.h" +#include "nsServiceManagerUtils.h" +#include "Tracing.h" + +extern mozilla::LazyLogModule gAudioChannelLog; + +#define AUDIO_CHANNEL_LOG(msg, ...) \ + MOZ_LOG(gAudioChannelLog, LogLevel::Debug, (msg, ##__VA_ARGS__)) + +namespace mozilla::dom { + +namespace { +class OnCompleteTask final : public Runnable { + public: + OnCompleteTask(AudioContext* aAudioContext, AudioBuffer* aRenderedBuffer) + : Runnable("dom::OfflineDestinationNodeEngine::OnCompleteTask"), + mAudioContext(aAudioContext), + mRenderedBuffer(aRenderedBuffer) {} + + NS_IMETHOD Run() override { + OfflineAudioCompletionEventInit param; + param.mRenderedBuffer = mRenderedBuffer; + + RefPtr<OfflineAudioCompletionEvent> event = + OfflineAudioCompletionEvent::Constructor(mAudioContext, u"complete"_ns, + param); + mAudioContext->DispatchTrustedEvent(event); + + return NS_OK; + } + + private: + RefPtr<AudioContext> mAudioContext; + RefPtr<AudioBuffer> mRenderedBuffer; +}; +} // anonymous namespace + +class OfflineDestinationNodeEngine final : public AudioNodeEngine { + public: + explicit OfflineDestinationNodeEngine(AudioDestinationNode* aNode) + : AudioNodeEngine(aNode), + mWriteIndex(0), + mNumberOfChannels(aNode->ChannelCount()), + mLength(aNode->Length()), + mSampleRate(aNode->Context()->SampleRate()), + mBufferAllocated(false) {} + + void ProcessBlock(AudioNodeTrack* aTrack, GraphTime aFrom, + const AudioBlock& aInput, AudioBlock* aOutput, + bool* aFinished) override { + TRACE("OfflineDestinationNodeEngine::ProcessBlock"); + // Do this just for the sake of political correctness; this output + // will not go anywhere. + *aOutput = aInput; + + // The output buffer is allocated lazily, on the rendering thread, when + // non-null input is received. + if (!mBufferAllocated && !aInput.IsNull()) { + // These allocations might fail if content provides a huge number of + // channels or size, but it's OK since we'll deal with the failure + // gracefully. + mBuffer = ThreadSharedFloatArrayBufferList::Create(mNumberOfChannels, + mLength, fallible); + if (mBuffer && mWriteIndex) { + // Zero leading for any null chunks that were skipped. + for (uint32_t i = 0; i < mNumberOfChannels; ++i) { + float* channelData = mBuffer->GetDataForWrite(i); + PodZero(channelData, mWriteIndex); + } + } + + mBufferAllocated = true; + } + + // Skip copying if there is no buffer. + uint32_t outputChannelCount = mBuffer ? mNumberOfChannels : 0; + + // Record our input buffer + MOZ_ASSERT(mWriteIndex < mLength, "How did this happen?"); + const uint32_t duration = + std::min(WEBAUDIO_BLOCK_SIZE, mLength - mWriteIndex); + const uint32_t inputChannelCount = aInput.ChannelCount(); + for (uint32_t i = 0; i < outputChannelCount; ++i) { + float* outputData = mBuffer->GetDataForWrite(i) + mWriteIndex; + if (aInput.IsNull() || i >= inputChannelCount) { + PodZero(outputData, duration); + } else { + const float* inputBuffer = + static_cast<const float*>(aInput.mChannelData[i]); + if (duration == WEBAUDIO_BLOCK_SIZE && IS_ALIGNED16(inputBuffer)) { + // Use the optimized version of the copy with scale operation + AudioBlockCopyChannelWithScale(inputBuffer, aInput.mVolume, + outputData); + } else { + if (aInput.mVolume == 1.0f) { + PodCopy(outputData, inputBuffer, duration); + } else { + for (uint32_t j = 0; j < duration; ++j) { + outputData[j] = aInput.mVolume * inputBuffer[j]; + } + } + } + } + } + mWriteIndex += duration; + + if (mWriteIndex >= mLength) { + NS_ASSERTION(mWriteIndex == mLength, "Overshot length"); + // Go to finished state. When the graph's current time eventually reaches + // the end of the track, then the main thread will be notified and we'll + // shut down the AudioContext. + *aFinished = true; + } + } + + bool IsActive() const override { + // Keep processing to track track time, which is used for all timelines + // associated with the same AudioContext. + return true; + } + + already_AddRefed<AudioBuffer> CreateAudioBuffer(AudioContext* aContext) { + MOZ_ASSERT(NS_IsMainThread()); + // Create the input buffer + ErrorResult rv; + RefPtr<AudioBuffer> renderedBuffer = + AudioBuffer::Create(aContext->GetOwner(), mNumberOfChannels, mLength, + mSampleRate, mBuffer.forget(), rv); + if (rv.Failed()) { + rv.SuppressException(); + return nullptr; + } + + return renderedBuffer.forget(); + } + + size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override { + size_t amount = AudioNodeEngine::SizeOfExcludingThis(aMallocSizeOf); + if (mBuffer) { + amount += mBuffer->SizeOfIncludingThis(aMallocSizeOf); + } + return amount; + } + + size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override { + return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf); + } + + private: + // The input to the destination node is recorded in mBuffer. + // When this buffer fills up with mLength frames, the buffered input is sent + // to the main thread in order to dispatch OfflineAudioCompletionEvent. + RefPtr<ThreadSharedFloatArrayBufferList> mBuffer; + // An index representing the next offset in mBuffer to be written to. + uint32_t mWriteIndex; + uint32_t mNumberOfChannels; + // How many frames the OfflineAudioContext intends to produce. + uint32_t mLength; + float mSampleRate; + bool mBufferAllocated; +}; + +class DestinationNodeEngine final : public AudioNodeEngine { + public: + explicit DestinationNodeEngine(AudioDestinationNode* aNode) + : AudioNodeEngine(aNode), + mSampleRate(CubebUtils::PreferredSampleRate( + aNode->Context()->ShouldResistFingerprinting())), + mVolume(1.0f), + mAudibilityMonitor( + mSampleRate, + StaticPrefs::dom_media_silence_duration_for_audibility()), + mSuspended(false), + mIsAudible(false) { + MOZ_ASSERT(aNode); + } + + void ProcessBlock(AudioNodeTrack* aTrack, GraphTime aFrom, + const AudioBlock& aInput, AudioBlock* aOutput, + bool* aFinished) override { + TRACE("DestinationNodeEngine::ProcessBlock"); + *aOutput = aInput; + aOutput->mVolume *= mVolume; + + if (mSuspended) { + return; + } + + mAudibilityMonitor.Process(aInput); + bool isAudible = + mAudibilityMonitor.RecentlyAudible() && aOutput->mVolume > 0.0; + if (isAudible != mIsAudible) { + mIsAudible = isAudible; + RefPtr<AudioNodeTrack> track = aTrack; + auto r = [track, isAudible]() -> void { + MOZ_ASSERT(NS_IsMainThread()); + RefPtr<AudioNode> node = track->Engine()->NodeMainThread(); + if (node) { + RefPtr<AudioDestinationNode> destinationNode = + static_cast<AudioDestinationNode*>(node.get()); + destinationNode->NotifyDataAudibleStateChanged(isAudible); + } + }; + + aTrack->Graph()->DispatchToMainThreadStableState(NS_NewRunnableFunction( + "dom::WebAudioAudibleStateChangedRunnable", r)); + } + } + + bool IsActive() const override { + // Keep processing to track track time, which is used for all timelines + // associated with the same AudioContext. If there are no other engines + // for the AudioContext, then this could return false to suspend the + // track, but the track is blocked anyway through + // AudioDestinationNode::SetIsOnlyNodeForContext(). + return true; + } + + void SetDoubleParameter(uint32_t aIndex, double aParam) override { + if (aIndex == VOLUME) { + mVolume = static_cast<float>(aParam); + } + } + + void SetInt32Parameter(uint32_t aIndex, int32_t aParam) override { + if (aIndex == SUSPENDED) { + mSuspended = !!aParam; + if (mSuspended) { + mIsAudible = false; + } + } + } + + enum Parameters { + VOLUME, + SUSPENDED, + }; + + size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override { + return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf); + } + + private: + int mSampleRate; + float mVolume; + AudibilityMonitor mAudibilityMonitor; + bool mSuspended; + bool mIsAudible; +}; + +NS_IMPL_CYCLE_COLLECTION_INHERITED(AudioDestinationNode, AudioNode, + mAudioChannelAgent, mOfflineRenderingPromise) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(AudioDestinationNode) + NS_INTERFACE_MAP_ENTRY(nsIAudioChannelAgentCallback) +NS_INTERFACE_MAP_END_INHERITING(AudioNode) + +NS_IMPL_ADDREF_INHERITED(AudioDestinationNode, AudioNode) +NS_IMPL_RELEASE_INHERITED(AudioDestinationNode, AudioNode) + +const AudioNodeTrack::Flags kTrackFlags = + AudioNodeTrack::NEED_MAIN_THREAD_CURRENT_TIME | + AudioNodeTrack::NEED_MAIN_THREAD_ENDED | AudioNodeTrack::EXTERNAL_OUTPUT; + +AudioDestinationNode::AudioDestinationNode(AudioContext* aContext, + bool aIsOffline, + uint32_t aNumberOfChannels, + uint32_t aLength) + : AudioNode(aContext, aNumberOfChannels, ChannelCountMode::Explicit, + ChannelInterpretation::Speakers), + mFramesToProduce(aLength), + mIsOffline(aIsOffline), + mCreatedTime(TimeStamp::Now()) { + if (aIsOffline) { + // The track is created on demand to avoid creating a graph thread that + // may not be used. + return; + } + + // GetParentObject can return nullptr here. This will end up creating another + // MediaTrackGraph + MediaTrackGraph* graph = MediaTrackGraph::GetInstance( + MediaTrackGraph::AUDIO_THREAD_DRIVER, aContext->GetParentObject(), + aContext->SampleRate(), MediaTrackGraph::DEFAULT_OUTPUT_DEVICE); + AudioNodeEngine* engine = new DestinationNodeEngine(this); + + mTrack = AudioNodeTrack::Create(aContext, engine, kTrackFlags, graph); + mTrack->AddMainThreadListener(this); + // null key is fine: only one output per mTrack + mTrack->AddAudioOutput(nullptr, nullptr); +} + +void AudioDestinationNode::Init() { + // The reason we don't do that in ctor is because we have to keep AudioContext + // holding a strong reference to the destination node first. If we don't do + // that, initializing the agent would cause an unexpected destroy of the + // destination node when destroying the local weak reference inside + // `InitWithWeakCallback()`. + if (!mIsOffline) { + CreateAndStartAudioChannelAgent(); + } +} + +void AudioDestinationNode::Close() { + DestroyAudioChannelAgentIfExists(); + ReleaseAudioWakeLockIfExists(); +} + +void AudioDestinationNode::CreateAndStartAudioChannelAgent() { + MOZ_ASSERT(!mIsOffline); + MOZ_ASSERT(!mAudioChannelAgent); + + AudioChannelAgent* agent = new AudioChannelAgent(); + nsresult rv = agent->InitWithWeakCallback(GetOwner(), this); + if (NS_WARN_IF(NS_FAILED(rv))) { + AUDIO_CHANNEL_LOG("Failed to init audio channel agent"); + return; + } + + AudibleState state = + IsAudible() ? AudibleState::eAudible : AudibleState::eNotAudible; + rv = agent->NotifyStartedPlaying(state); + if (NS_WARN_IF(NS_FAILED(rv))) { + AUDIO_CHANNEL_LOG("Failed to start audio channel agent"); + return; + } + + mAudioChannelAgent = agent; + mAudioChannelAgent->PullInitialUpdate(); +} + +AudioDestinationNode::~AudioDestinationNode() { + MOZ_ASSERT(!mAudioChannelAgent); + MOZ_ASSERT(!mWakeLock); + MOZ_ASSERT(!mCaptureTrackPort); +} + +size_t AudioDestinationNode::SizeOfExcludingThis( + MallocSizeOf aMallocSizeOf) const { + size_t amount = AudioNode::SizeOfExcludingThis(aMallocSizeOf); + // Might be useful in the future: + // - mAudioChannelAgent + return amount; +} + +size_t AudioDestinationNode::SizeOfIncludingThis( + MallocSizeOf aMallocSizeOf) const { + return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf); +} + +AudioNodeTrack* AudioDestinationNode::Track() { + if (mTrack) { + return mTrack; + } + + AudioContext* context = Context(); + if (!context) { // This node has been unlinked. + return nullptr; + } + + MOZ_ASSERT(mIsOffline, "Realtime tracks are created in constructor"); + + // GetParentObject can return nullptr here when the document has been + // unlinked. + MediaTrackGraph* graph = + MediaTrackGraph::CreateNonRealtimeInstance(context->SampleRate()); + AudioNodeEngine* engine = new OfflineDestinationNodeEngine(this); + + mTrack = AudioNodeTrack::Create(context, engine, kTrackFlags, graph); + mTrack->AddMainThreadListener(this); + + return mTrack; +} + +void AudioDestinationNode::DestroyAudioChannelAgentIfExists() { + if (mAudioChannelAgent) { + mAudioChannelAgent->NotifyStoppedPlaying(); + mAudioChannelAgent = nullptr; + if (IsCapturingAudio()) { + StopAudioCapturingTrack(); + } + } +} + +void AudioDestinationNode::DestroyMediaTrack() { + Close(); + if (!mTrack) { + return; + } + + Context()->ShutdownWorklet(); + + mTrack->RemoveMainThreadListener(this); + AudioNode::DestroyMediaTrack(); +} + +void AudioDestinationNode::NotifyMainThreadTrackEnded() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mTrack->IsEnded()); + + if (mIsOffline) { + AbstractThread::MainThread()->Dispatch(NewRunnableMethod( + "dom::AudioDestinationNode::FireOfflineCompletionEvent", this, + &AudioDestinationNode::FireOfflineCompletionEvent)); + } +} + +void AudioDestinationNode::FireOfflineCompletionEvent() { + AudioContext* context = Context(); + context->OfflineClose(); + + OfflineDestinationNodeEngine* engine = + static_cast<OfflineDestinationNodeEngine*>(Track()->Engine()); + RefPtr<AudioBuffer> renderedBuffer = engine->CreateAudioBuffer(context); + if (!renderedBuffer) { + return; + } + ResolvePromise(renderedBuffer); + + context->Dispatch(do_AddRef(new OnCompleteTask(context, renderedBuffer))); + + context->OnStateChanged(nullptr, AudioContextState::Closed); + + mOfflineRenderingRef.Drop(this); +} + +void AudioDestinationNode::ResolvePromise(AudioBuffer* aRenderedBuffer) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mIsOffline); + mOfflineRenderingPromise->MaybeResolve(aRenderedBuffer); +} + +uint32_t AudioDestinationNode::MaxChannelCount() const { + return Context()->MaxChannelCount(); +} + +void AudioDestinationNode::SetChannelCount(uint32_t aChannelCount, + ErrorResult& aRv) { + if (aChannelCount > MaxChannelCount()) { + aRv.ThrowIndexSizeError( + nsPrintfCString("%u is larger than maxChannelCount", aChannelCount)); + return; + } + + if (aChannelCount == ChannelCount()) { + return; + } + + AudioNode::SetChannelCount(aChannelCount, aRv); +} + +void AudioDestinationNode::Mute() { + MOZ_ASSERT(Context() && !Context()->IsOffline()); + SendDoubleParameterToTrack(DestinationNodeEngine::VOLUME, 0.0f); +} + +void AudioDestinationNode::Unmute() { + MOZ_ASSERT(Context() && !Context()->IsOffline()); + SendDoubleParameterToTrack(DestinationNodeEngine::VOLUME, 1.0f); +} + +void AudioDestinationNode::Suspend() { + SendInt32ParameterToTrack(DestinationNodeEngine::SUSPENDED, 1); +} + +void AudioDestinationNode::Resume() { + SendInt32ParameterToTrack(DestinationNodeEngine::SUSPENDED, 0); +} + +void AudioDestinationNode::NotifyAudioContextStateChanged() { + UpdateFinalAudibleStateIfNeeded(AudibleChangedReasons::ePauseStateChanged); +} + +void AudioDestinationNode::OfflineShutdown() { + MOZ_ASSERT(Context() && Context()->IsOffline(), + "Should only be called on a valid OfflineAudioContext"); + + mOfflineRenderingRef.Drop(this); +} + +JSObject* AudioDestinationNode::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return AudioDestinationNode_Binding::Wrap(aCx, this, aGivenProto); +} + +void AudioDestinationNode::StartRendering(Promise* aPromise) { + mOfflineRenderingPromise = aPromise; + mOfflineRenderingRef.Take(this); + Track()->Graph()->StartNonRealtimeProcessing(mFramesToProduce); +} + +NS_IMETHODIMP +AudioDestinationNode::WindowVolumeChanged(float aVolume, bool aMuted) { + MOZ_ASSERT(mAudioChannelAgent); + if (!mTrack) { + return NS_OK; + } + + AUDIO_CHANNEL_LOG( + "AudioDestinationNode %p WindowVolumeChanged, " + "aVolume = %f, aMuted = %s\n", + this, aVolume, aMuted ? "true" : "false"); + + mAudioChannelVolume = aMuted ? 0.0f : aVolume; + mTrack->SetAudioOutputVolume(nullptr, mAudioChannelVolume); + UpdateFinalAudibleStateIfNeeded(AudibleChangedReasons::eVolumeChanged); + return NS_OK; +} + +NS_IMETHODIMP +AudioDestinationNode::WindowSuspendChanged(nsSuspendedTypes aSuspend) { + MOZ_ASSERT(mAudioChannelAgent); + if (!mTrack) { + return NS_OK; + } + + const bool shouldDisable = aSuspend == nsISuspendedTypes::SUSPENDED_BLOCK; + if (mAudioChannelDisabled == shouldDisable) { + return NS_OK; + } + mAudioChannelDisabled = shouldDisable; + + AUDIO_CHANNEL_LOG( + "AudioDestinationNode %p WindowSuspendChanged, shouldDisable = %d\n", + this, mAudioChannelDisabled); + + DisabledTrackMode disabledMode = mAudioChannelDisabled + ? DisabledTrackMode::SILENCE_BLACK + : DisabledTrackMode::ENABLED; + mTrack->SetDisabledTrackMode(disabledMode); + UpdateFinalAudibleStateIfNeeded(AudibleChangedReasons::ePauseStateChanged); + return NS_OK; +} + +NS_IMETHODIMP +AudioDestinationNode::WindowAudioCaptureChanged(bool aCapture) { + MOZ_ASSERT(mAudioChannelAgent); + if (!mTrack) { + return NS_OK; + } + + nsCOMPtr<nsPIDOMWindowInner> ownerWindow = GetOwner(); + if (!ownerWindow) { + return NS_OK; + } + + if (aCapture == IsCapturingAudio()) { + return NS_OK; + } + + if (aCapture) { + StartAudioCapturingTrack(); + } else { + StopAudioCapturingTrack(); + } + + return NS_OK; +} + +bool AudioDestinationNode::IsCapturingAudio() const { + return mCaptureTrackPort != nullptr; +} + +void AudioDestinationNode::StartAudioCapturingTrack() { + MOZ_ASSERT(!IsCapturingAudio()); + nsCOMPtr<nsPIDOMWindowInner> window = Context()->GetParentObject(); + uint64_t id = window->WindowID(); + mCaptureTrackPort = mTrack->Graph()->ConnectToCaptureTrack(id, mTrack); +} + +void AudioDestinationNode::StopAudioCapturingTrack() { + MOZ_ASSERT(IsCapturingAudio()); + mCaptureTrackPort->Destroy(); + mCaptureTrackPort = nullptr; +} + +void AudioDestinationNode::CreateAudioWakeLockIfNeeded() { + if (!mWakeLock && IsAudible()) { + RefPtr<power::PowerManagerService> pmService = + power::PowerManagerService::GetInstance(); + NS_ENSURE_TRUE_VOID(pmService); + + ErrorResult rv; + mWakeLock = pmService->NewWakeLock(u"audio-playing"_ns, GetOwner(), rv); + } +} + +void AudioDestinationNode::ReleaseAudioWakeLockIfExists() { + if (mWakeLock) { + IgnoredErrorResult rv; + mWakeLock->Unlock(rv); + mWakeLock = nullptr; + } +} + +void AudioDestinationNode::NotifyDataAudibleStateChanged(bool aAudible) { + MOZ_ASSERT(!mIsOffline); + + AUDIO_CHANNEL_LOG( + "AudioDestinationNode %p NotifyDataAudibleStateChanged, audible=%d", this, + aAudible); + + if (mDurationBeforeFirstTimeAudible.IsZero()) { + MOZ_ASSERT(aAudible); + mDurationBeforeFirstTimeAudible = TimeStamp::Now() - mCreatedTime; + Telemetry::Accumulate(Telemetry::WEB_AUDIO_BECOMES_AUDIBLE_TIME, + mDurationBeforeFirstTimeAudible.ToSeconds()); + } + + mIsDataAudible = aAudible; + UpdateFinalAudibleStateIfNeeded(AudibleChangedReasons::eDataAudibleChanged); +} + +void AudioDestinationNode::UpdateFinalAudibleStateIfNeeded( + AudibleChangedReasons aReason) { + // The audio context has been closed and we've destroyed the agent. + if (!mAudioChannelAgent) { + return; + } + const bool newAudibleState = IsAudible(); + if (mFinalAudibleState == newAudibleState) { + return; + } + AUDIO_CHANNEL_LOG("AudioDestinationNode %p Final audible state=%d", this, + newAudibleState); + mFinalAudibleState = newAudibleState; + AudibleState state = + mFinalAudibleState ? AudibleState::eAudible : AudibleState::eNotAudible; + mAudioChannelAgent->NotifyStartedAudible(state, aReason); + if (mFinalAudibleState) { + CreateAudioWakeLockIfNeeded(); + } else { + ReleaseAudioWakeLockIfExists(); + } +} + +bool AudioDestinationNode::IsAudible() const { + // The desitionation node will be regarded as audible if all following + // conditions are true. + // (1) data audible state : both audio input and output are audible + // (2) window audible state : the tab isn't muted by tab sound indicator + // (3) audio context state : audio context should be running + return Context()->State() == AudioContextState::Running && mIsDataAudible && + mAudioChannelVolume != 0.0; +} + +} // namespace mozilla::dom diff --git a/dom/media/webaudio/AudioDestinationNode.h b/dom/media/webaudio/AudioDestinationNode.h new file mode 100644 index 0000000000..df4a9b16ec --- /dev/null +++ b/dom/media/webaudio/AudioDestinationNode.h @@ -0,0 +1,133 @@ +/* -*- 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/. */ + +#ifndef AudioDestinationNode_h_ +#define AudioDestinationNode_h_ + +#include "AudioChannelService.h" +#include "AudioNode.h" +#include "AudioChannelAgent.h" +#include "mozilla/TimeStamp.h" + +namespace mozilla::dom { + +class AudioContext; +class WakeLock; + +class AudioDestinationNode final : public AudioNode, + public nsIAudioChannelAgentCallback, + public MainThreadMediaTrackListener { + public: + // This node type knows what MediaTrackGraph to use based on + // whether it's in offline mode. + AudioDestinationNode(AudioContext* aContext, bool aIsOffline, + uint32_t aNumberOfChannels, uint32_t aLength); + + void DestroyMediaTrack() override; + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(AudioDestinationNode, AudioNode) + NS_DECL_NSIAUDIOCHANNELAGENTCALLBACK + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + uint16_t NumberOfOutputs() const final { return 0; } + + uint32_t MaxChannelCount() const; + void SetChannelCount(uint32_t aChannelCount, ErrorResult& aRv) override; + + void Init(); + void Close(); + + // Returns the track or null after unlink. + AudioNodeTrack* Track(); + + void Mute(); + void Unmute(); + + void Suspend(); + void Resume(); + + void StartRendering(Promise* aPromise); + + void OfflineShutdown(); + + void NotifyMainThreadTrackEnded() override; + void FireOfflineCompletionEvent(); + + const char* NodeType() const override { return "AudioDestinationNode"; } + + size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override; + size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override; + + void NotifyDataAudibleStateChanged(bool aAudible); + void ResolvePromise(AudioBuffer* aRenderedBuffer); + + unsigned long Length() { + MOZ_ASSERT(mIsOffline); + return mFramesToProduce; + } + + void NotifyAudioContextStateChanged(); + + protected: + virtual ~AudioDestinationNode(); + + private: + // This would be created for non-offline audio context in order to receive + // tab's mute/suspend/audio capture state change and update the audible state + // to the tab. + void CreateAndStartAudioChannelAgent(); + void DestroyAudioChannelAgentIfExists(); + RefPtr<AudioChannelAgent> mAudioChannelAgent; + + // These members are related to audio capturing. We would start capturing + // audio if we're starting capturing audio from whole window, and MUST stop + // capturing explicitly when we don't need to capture audio any more, because + // we have to release the resource we allocated before. + bool IsCapturingAudio() const; + void StartAudioCapturingTrack(); + void StopAudioCapturingTrack(); + RefPtr<MediaInputPort> mCaptureTrackPort; + + // These members are used to determine if the destination node is actual + // audible and `mFinalAudibleState` represents the final result. + using AudibleChangedReasons = AudioChannelService::AudibleChangedReasons; + using AudibleState = AudioChannelService::AudibleState; + void UpdateFinalAudibleStateIfNeeded(AudibleChangedReasons aReason); + bool IsAudible() const; + bool mFinalAudibleState = false; + bool mIsDataAudible = false; + float mAudioChannelVolume = 1.0; + + // True if the audio channel disables the track for unvisited tab, and the + // track will be enabled again when the tab gets first visited, or a user + // presses the tab play icon. + bool mAudioChannelDisabled = false; + + // When the destination node is audible, we would request a wakelock to + // prevent computer from sleeping in order to keep audio playing. + void CreateAudioWakeLockIfNeeded(); + void ReleaseAudioWakeLockIfExists(); + RefPtr<WakeLock> mWakeLock; + + SelfReference<AudioDestinationNode> mOfflineRenderingRef; + uint32_t mFramesToProduce; + + RefPtr<Promise> mOfflineRenderingPromise; + + bool mIsOffline; + + // These varaibles are used to know how long AudioContext would become audible + // since it was created. + TimeStamp mCreatedTime; + TimeDuration mDurationBeforeFirstTimeAudible; +}; + +} // namespace mozilla::dom + +#endif diff --git a/dom/media/webaudio/AudioEventTimeline.cpp b/dom/media/webaudio/AudioEventTimeline.cpp new file mode 100644 index 0000000000..f960cadd62 --- /dev/null +++ b/dom/media/webaudio/AudioEventTimeline.cpp @@ -0,0 +1,513 @@ +/* -*- 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 "AudioEventTimeline.h" +#include "AudioNodeTrack.h" + +#include "mozilla/ErrorResult.h" + +using mozilla::Span; + +// v1 and v0 are passed from float variables but converted to double for +// double precision interpolation. +static void FillLinearRamp(double aBufferStartTime, Span<float> aBuffer, + double t0, double v0, double t1, double v1) { + double bufferStartDelta = aBufferStartTime - t0; + double gradient = (v1 - v0) / (t1 - t0); + for (size_t i = 0; i < aBuffer.Length(); ++i) { + double v = v0 + (bufferStartDelta + static_cast<double>(i)) * gradient; + aBuffer[i] = static_cast<float>(v); + } +} + +static void FillExponentialRamp(double aBufferStartTime, Span<float> aBuffer, + double t0, float v0, double t1, float v1) { + MOZ_ASSERT(aBuffer.Length() >= 1); + double fullRatio = static_cast<double>(v1) / v0; + if (v0 == 0.f || fullRatio < 0.0) { + std::fill_n(aBuffer.Elements(), aBuffer.Length(), v0); + return; + } + + double tDelta = t1 - t0; + // Calculate the value for the first tick from the curve initial value. + // v(t) = v0 * (v1/v0)^((t-t0)/(t1-t0)) + double exponent = (aBufferStartTime - t0) / tDelta; + // The power function can amplify rounding error in the exponent by + // ((t−t0)/(t1−t0)) ln (v1/v0). The single precision exponent argument for + // powf() would be sufficient when max(v1/v0,v0/v1) <= e, where e is Euler's + // number, but fdlibm's single precision powf() is not expected to provide + // speed advantages over double precision pow(). + double v = v0 * fdlibm_pow(fullRatio, exponent); + aBuffer[0] = static_cast<float>(v); + if (aBuffer.Length() == 1) { + return; + } + + // Use the inter-tick ratio to calculate values at other ticks. + // v(t+1) = (v1/v0)^(1/(t1-t0)) * v(t) + // Double precision is used so that accumulation of rounding error is not + // significant. + double tickRatio = fdlibm_pow(fullRatio, 1.0 / tDelta); + for (size_t i = 1; i < aBuffer.Length(); ++i) { + v *= tickRatio; + aBuffer[i] = static_cast<float>(v); + } +} + +template <typename TimeType, typename DurationType> +static size_t LimitedCountForDuration(size_t aMax, DurationType aDuration); + +template <> +size_t LimitedCountForDuration<double>(size_t aMax, double aDuration) { + // aDuration is in seconds, so tick arithmetic is inappropriate, + // and unnecessary. + // GetValuesAtTime() is not available, so at most one value is fetched. + MOZ_ASSERT(aMax <= 1); + return aMax; +} +template <> +size_t LimitedCountForDuration<int64_t>(size_t aMax, int64_t aDuration) { + MOZ_ASSERT(aDuration >= 0); + // int64_t aDuration is in ticks. + // On 32-bit systems, aDuration may be larger than SIZE_MAX. + // Determine the larger with int64_t to avoid truncating before the + // comparison. + return static_cast<int64_t>(aMax) <= aDuration + ? aMax + : static_cast<size_t>(aDuration); +} +template <> +size_t LimitedCountForDuration<int64_t>(size_t aMax, double aDuration) { + MOZ_ASSERT(aDuration >= 0); + // double aDuration is in ticks. + // AudioTimelineEvent::mDuration may be larger than INT64_MAX. + // On 32-bit systems, mDuration may be larger than SIZE_MAX. + // Determine the larger with double to avoid truncating before the + // comparison. + return static_cast<double>(aMax) <= aDuration + ? aMax + : static_cast<size_t>(aDuration); +} + +static float* NewCurveCopy(Span<const float> aCurve) { + if (aCurve.Length() == 0) { + return nullptr; + } + + float* curve = new float[aCurve.Length()]; + mozilla::PodCopy(curve, aCurve.Elements(), aCurve.Length()); + return curve; +} + +namespace mozilla::dom { + +AudioTimelineEvent::AudioTimelineEvent(Type aType, double aTime, float aValue, + double aTimeConstant) + : mType(aType), + mValue(aValue), + mTimeConstant(aTimeConstant), + mPerTickRatio(std::numeric_limits<double>::quiet_NaN()), + mTime(aTime) {} + +AudioTimelineEvent::AudioTimelineEvent(Type aType, + const nsTArray<float>& aValues, + double aStartTime, double aDuration) + : mType(aType), + mCurveLength(aValues.Length()), + mCurve(NewCurveCopy(aValues)), + mDuration(aDuration), + mTime(aStartTime) { + MOZ_ASSERT(aType == AudioTimelineEvent::SetValueCurve); +} + +// cppcoreguidelines-pro-type-member-init does not know PodCopy(). +// NOLINTNEXTLINE(cppcoreguidelines-pro-type-member-init) +AudioTimelineEvent::AudioTimelineEvent(const AudioTimelineEvent& rhs) + : mType(rhs.mType) { + PodCopy(this, &rhs, 1); + + if (rhs.mType == AudioTimelineEvent::SetValueCurve) { + mCurve = NewCurveCopy(Span(rhs.mCurve, rhs.mCurveLength)); + } +} + +AudioTimelineEvent::~AudioTimelineEvent() { + if (mType == AudioTimelineEvent::SetValueCurve) { + delete[] mCurve; + } +} + +template <class TimeType> +double AudioTimelineEvent::EndTime() const { + MOZ_ASSERT(mType != AudioTimelineEvent::SetTarget); + if (mType == AudioTimelineEvent::SetValueCurve) { + return Time<TimeType>() + mDuration; + } + return Time<TimeType>(); +}; + +float AudioTimelineEvent::EndValue() const { + if (mType == AudioTimelineEvent::SetValueCurve) { + return mCurve[mCurveLength - 1]; + } + return mValue; +}; + +void AudioTimelineEvent::ConvertToTicks(AudioNodeTrack* aDestination) { + mTime = aDestination->SecondsToNearestTrackTime(mTime.Get<double>()); + switch (mType) { + case SetTarget: + mTimeConstant *= aDestination->mSampleRate; + // exp(-1/timeConstant) is usually very close to 1, but its effect + // depends on the difference from 1 and rounding errors would + // accumulate, so use double precision to retain precision in the + // difference. Single precision expm1f() would be sufficient, but the + // arithmetic in AudioTimelineEvent::FillTargetApproach() is simpler + // with exp(). + mPerTickRatio = + mTimeConstant == 0.0 ? 0.0 : fdlibm_exp(-1.0 / mTimeConstant); + + break; + case SetValueCurve: + mDuration *= aDestination->mSampleRate; + break; + default: + break; + } +} + +template <class TimeType> +void AudioTimelineEvent::FillTargetApproach(TimeType aBufferStartTime, + Span<float> aBuffer, + double v0) const { + MOZ_ASSERT(mType == SetTarget); + MOZ_ASSERT(aBuffer.Length() >= 1); + double v1 = mValue; + double vDelta = v0 - v1; + if (vDelta == 0.0 || mTimeConstant == 0.0) { + std::fill_n(aBuffer.Elements(), aBuffer.Length(), mValue); + return; + } + + // v(t) = v1 + vDelta(t) where vDelta(t) = (v0-v1) * e^(-(t-t0)/timeConstant). + // Calculate the value for the first element in the buffer using this + // formulation. + vDelta *= fdlibm_expf(-(aBufferStartTime - Time<TimeType>()) / mTimeConstant); + for (size_t i = 0; true;) { + aBuffer[i] = static_cast<float>(v1 + vDelta); + ++i; + if (i == aBuffer.Length()) { + return; + } + // For other buffer elements, use the pre-computed exp(-1/timeConstant) + // for the inter-tick ratio of the difference from the target. + // vDelta(t+1) = vDelta(t) * e^(-1/timeConstant) + vDelta *= mPerTickRatio; + } +} + +static_assert(TRACK_TIME_MAX >> FloatingPoint<double>::kSignificandWidth == 0, + "double precision must be exact for integer tick counts"); +template <class TimeType> +void AudioTimelineEvent::FillFromValueCurve(TimeType aBufferStartTime, + Span<float> aBuffer) const { + MOZ_ASSERT(mType == SetValueCurve); + double curveStartTime = Time<TimeType>(); + MOZ_ASSERT(aBufferStartTime >= curveStartTime); + MOZ_ASSERT(aBufferStartTime - curveStartTime <= mDuration); + MOZ_ASSERT((std::is_same<TimeType, int64_t>::value) || aBuffer.Length() == 1); + MOZ_ASSERT((!std::is_same<TimeType, int64_t>::value) || + aBufferStartTime - curveStartTime + aBuffer.Length() - 1 <= + mDuration); + uint32_t stepCount = mCurveLength - 1; + double timeStep = mDuration / stepCount; + + for (size_t fillStart = 0; fillStart < aBuffer.Length();) { + // Find the curve sample index, spec'd as `k`, corresponding to a time less + // than or equal to the first buffer element to be filled. + double stepPos = + (aBufferStartTime + fillStart - curveStartTime) / mDuration * stepCount; + // GetValuesAtTimeHelperInternal() calls this only when + // aBufferStartTime + fillStart - curveStartTime <= mDuration. + MOZ_ASSERT(stepPos >= 0 && stepPos <= UINT32_MAX - 1); + uint32_t currentNode = floor(stepPos); + if (currentNode >= stepCount) { + auto remaining = aBuffer.From(fillStart); + std::fill_n(remaining.Elements(), remaining.Length(), mCurve[stepCount]); + return; + } + + // Linearly interpolate to fill the buffer elements for any ticks between + // curve samples k and k + 1 inclusive. + double tCurrent = curveStartTime + currentNode * timeStep; + uint32_t nextNode = currentNode + 1; + double tNext = curveStartTime + nextNode * timeStep; + // The first buffer index that cannot be filled with these curve samples + size_t fillEnd = LimitedCountForDuration<TimeType>( + aBuffer.Length(), + // This parameter is used only when time is in ticks: + // If tNext aligns exactly with a tick then fill to tNext, thus + // ensuring that fillStart is advanced even when timeStep is so small + // that tNext == tCurrent. + floor(tNext - aBufferStartTime) + 1.0); + TimeType fillStartTime = + aBufferStartTime + static_cast<TimeType>(fillStart); + FillLinearRamp(fillStartTime, aBuffer.FromTo(fillStart, fillEnd), tCurrent, + mCurve[currentNode], tNext, mCurve[nextNode]); + fillStart = fillEnd; + } +} + +template <class TimeType> +float AudioEventTimeline::ComputeSetTargetStartValue( + const AudioTimelineEvent* aPreviousEvent, TimeType aTime) { + mSetTargetStartTime = aTime; + GetValuesAtTimeHelperInternal(aTime, Span(&mSetTargetStartValue, 1), + aPreviousEvent, nullptr); + return mSetTargetStartValue; +} + +template void AudioEventTimeline::CleanupEventsOlderThan(double); +template void AudioEventTimeline::CleanupEventsOlderThan(int64_t); +template <class TimeType> +void AudioEventTimeline::CleanupEventsOlderThan(TimeType aTime) { + auto TimeOf = + [](const decltype(mEvents)::const_iterator& aEvent) -> TimeType { + return aEvent->Time<TimeType>(); + }; + + if (mSimpleValue.isSome()) { + return; // already only a single event + } + + // Find first event to keep. Keep one event prior to aTime. + auto begin = mEvents.cbegin(); + auto end = mEvents.cend(); + auto event = begin + 1; + for (; event < end && aTime > TimeOf(event); ++event) { + } + auto firstToKeep = event - 1; + + if (firstToKeep->mType != AudioTimelineEvent::SetTarget) { + // The value is constant if there is a single remaining non-SetTarget event + // that has already passed. + if (end - firstToKeep == 1 && aTime >= firstToKeep->EndTime<TimeType>()) { + mSimpleValue.emplace(firstToKeep->EndValue()); + } + } else { + // The firstToKeep event is a SetTarget. Set its initial value if + // not already set. First find the most recent event where the value at + // the end time of the event is known, either from the event or for + // SetTarget events because it has already been calculated. This may not + // have been calculated if GetValuesAtTime() was not called for the start + // time of the SetTarget event. + for (event = firstToKeep; + event > begin && event->mType == AudioTimelineEvent::SetTarget && + TimeOf(event) > mSetTargetStartTime.Get<TimeType>(); + --event) { + } + // Compute SetTarget start times. + for (; event < firstToKeep; ++event) { + MOZ_ASSERT((event + 1)->mType == AudioTimelineEvent::SetTarget); + ComputeSetTargetStartValue(&*event, TimeOf(event + 1)); + } + } + if (firstToKeep == begin) { + return; + } + + mEvents.RemoveElementsRange(begin, firstToKeep); +} + +// This method computes the AudioParam value at a given time based on the event +// timeline +template <class TimeType> +void AudioEventTimeline::GetValuesAtTimeHelper(TimeType aTime, float* aBuffer, + const size_t aSize) { + MOZ_ASSERT(aBuffer); + MOZ_ASSERT(aSize); + + auto TimeOf = [](const AudioTimelineEvent& aEvent) -> TimeType { + return aEvent.Time<TimeType>(); + }; + + size_t eventIndex = 0; + const AudioTimelineEvent* previous = nullptr; + + // Let's remove old events except the last one: we need it to calculate some + // curves. + CleanupEventsOlderThan(aTime); + + for (size_t bufferIndex = 0; bufferIndex < aSize;) { + bool timeMatchesEventIndex = false; + const AudioTimelineEvent* next; + for (;; ++eventIndex) { + if (eventIndex >= mEvents.Length()) { + next = nullptr; + break; + } + + next = &mEvents[eventIndex]; + if (aTime < TimeOf(*next)) { + break; + } + +#ifdef DEBUG + MOZ_ASSERT(next->mType == AudioTimelineEvent::SetValueAtTime || + next->mType == AudioTimelineEvent::SetTarget || + next->mType == AudioTimelineEvent::LinearRamp || + next->mType == AudioTimelineEvent::ExponentialRamp || + next->mType == AudioTimelineEvent::SetValueCurve); +#endif + + if (TimesEqual(aTime, TimeOf(*next))) { + timeMatchesEventIndex = true; + aBuffer[bufferIndex] = GetValueAtTimeOfEvent<TimeType>(next, previous); + // Advance to next event, which may or may not have the same time. + } + previous = next; + } + + if (timeMatchesEventIndex) { + // The time matches one of the events exactly. + MOZ_ASSERT(TimesEqual(aTime, TimeOf(mEvents[eventIndex - 1]))); + ++bufferIndex; + ++aTime; + } else { + size_t count = aSize - bufferIndex; + if (next) { + count = LimitedCountForDuration<TimeType>(count, TimeOf(*next) - aTime); + } + GetValuesAtTimeHelperInternal(aTime, Span(aBuffer + bufferIndex, count), + previous, next); + bufferIndex += count; + aTime += static_cast<TimeType>(count); + } + } +} +template void AudioEventTimeline::GetValuesAtTimeHelper(double aTime, + float* aBuffer, + const size_t aSize); +template void AudioEventTimeline::GetValuesAtTimeHelper(int64_t aTime, + float* aBuffer, + const size_t aSize); + +template <class TimeType> +float AudioEventTimeline::GetValueAtTimeOfEvent( + const AudioTimelineEvent* aEvent, const AudioTimelineEvent* aPrevious) { + TimeType time = aEvent->Time<TimeType>(); + switch (aEvent->mType) { + case AudioTimelineEvent::SetTarget: + // Start the curve, from the last value of the previous event. + return ComputeSetTargetStartValue(aPrevious, time); + case AudioTimelineEvent::SetValueCurve: + return aEvent->StartValue(); + default: + // For other event types + return aEvent->NominalValue(); + } +} + +template <class TimeType> +void AudioEventTimeline::GetValuesAtTimeHelperInternal( + TimeType aStartTime, Span<float> aBuffer, + const AudioTimelineEvent* aPrevious, const AudioTimelineEvent* aNext) { + MOZ_ASSERT(aBuffer.Length() >= 1); + MOZ_ASSERT((std::is_same<TimeType, int64_t>::value) || aBuffer.Length() == 1); + + // If the requested time is before all of the existing events + if (!aPrevious) { + std::fill_n(aBuffer.Elements(), aBuffer.Length(), mDefaultValue); + return; + } + + auto TimeOf = [](const AudioTimelineEvent* aEvent) -> TimeType { + return aEvent->Time<TimeType>(); + }; + auto EndTimeOf = [](const AudioTimelineEvent* aEvent) -> double { + return aEvent->EndTime<TimeType>(); + }; + + // SetTarget nodes can be handled no matter what their next node is (if + // they have one) + if (aPrevious->mType == AudioTimelineEvent::SetTarget) { + aPrevious->FillTargetApproach(aStartTime, aBuffer, mSetTargetStartValue); + return; + } + + // SetValueCurve events can be handled no matter what their next node is + // (if they have one), when aStartTime is in the curve region. + if (aPrevious->mType == AudioTimelineEvent::SetValueCurve) { + double remainingDuration = + TimeOf(aPrevious) - aStartTime + aPrevious->Duration(); + if (remainingDuration >= 0.0) { + // aBuffer.Length() is 1 if remainingDuration is not in ticks. + size_t count = LimitedCountForDuration<TimeType>( + aBuffer.Length(), + // This parameter is used only when time is in ticks: + // Fill the last tick in the curve before possible ramps below. + floor(remainingDuration) + 1.0); + // GetValueAtTimeOfEvent() will set the value at the end of the curve if + // another event immediately follows. + MOZ_ASSERT(!aNext || + aStartTime + static_cast<TimeType>(count - 1) < TimeOf(aNext)); + aPrevious->FillFromValueCurve(aStartTime, + Span(aBuffer.Elements(), count)); + aBuffer = aBuffer.From(count); + if (aBuffer.Length() == 0) { + return; + } + aStartTime += static_cast<TimeType>(count); + } + } + + // Handle the cases where our range ends up in a ramp event + if (aNext) { + switch (aNext->mType) { + case AudioTimelineEvent::LinearRamp: + FillLinearRamp(aStartTime, aBuffer, EndTimeOf(aPrevious), + aPrevious->EndValue(), TimeOf(aNext), + aNext->NominalValue()); + return; + case AudioTimelineEvent::ExponentialRamp: + FillExponentialRamp(aStartTime, aBuffer, EndTimeOf(aPrevious), + aPrevious->EndValue(), TimeOf(aNext), + aNext->NominalValue()); + return; + case AudioTimelineEvent::SetValueAtTime: + case AudioTimelineEvent::SetTarget: + case AudioTimelineEvent::SetValueCurve: + break; + case AudioTimelineEvent::SetValue: + case AudioTimelineEvent::Cancel: + case AudioTimelineEvent::Track: + MOZ_ASSERT(false, "Should have been handled earlier."); + } + } + + // Now handle all other cases + switch (aPrevious->mType) { + case AudioTimelineEvent::SetValueAtTime: + case AudioTimelineEvent::LinearRamp: + case AudioTimelineEvent::ExponentialRamp: + break; + case AudioTimelineEvent::SetValueCurve: + MOZ_ASSERT(aStartTime - TimeOf(aPrevious) >= aPrevious->Duration()); + break; + case AudioTimelineEvent::SetTarget: + MOZ_FALLTHROUGH_ASSERT("AudioTimelineEvent::SetTarget"); + case AudioTimelineEvent::SetValue: + case AudioTimelineEvent::Cancel: + case AudioTimelineEvent::Track: + MOZ_ASSERT(false, "Should have been handled earlier."); + } + // If the next event type is neither linear or exponential ramp, the + // value is constant. + std::fill_n(aBuffer.Elements(), aBuffer.Length(), aPrevious->EndValue()); +} + +} // namespace mozilla::dom diff --git a/dom/media/webaudio/AudioEventTimeline.h b/dom/media/webaudio/AudioEventTimeline.h new file mode 100644 index 0000000000..9284fc5b6d --- /dev/null +++ b/dom/media/webaudio/AudioEventTimeline.h @@ -0,0 +1,418 @@ +/* -*- 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/. */ + +#ifndef AudioEventTimeline_h_ +#define AudioEventTimeline_h_ + +#include <algorithm> +#include "mozilla/Assertions.h" +#include "mozilla/FloatingPoint.h" +#include "mozilla/PodOperations.h" +#include "mozilla/ErrorResult.h" + +#include "MainThreadUtils.h" +#include "nsTArray.h" +#include "math.h" +#include "WebAudioUtils.h" + +// XXX Avoid including this here by moving function bodies to the cpp file +#include "js/GCAPI.h" + +namespace mozilla { + +class AudioNodeTrack; + +namespace dom { + +struct AudioTimelineEvent { + enum Type : uint32_t { + SetValue, + SetValueAtTime, + LinearRamp, + ExponentialRamp, + SetTarget, + SetValueCurve, + Track, + Cancel + }; + + class TimeUnion { + public: + // double 0.0 is bit-identical to int64_t 0. + TimeUnion() + : mSeconds() +#if DEBUG + , + mIsInSeconds(true), + mIsInTicks(true) +#endif + { + } + explicit TimeUnion(double aTime) + : mSeconds(aTime) +#if DEBUG + , + mIsInSeconds(true), + mIsInTicks(false) +#endif + { + } + explicit TimeUnion(int64_t aTime) + : mTicks(aTime) +#if DEBUG + , + mIsInSeconds(false), + mIsInTicks(true) +#endif + { + } + + double operator=(double aTime) { +#if DEBUG + mIsInSeconds = true; + mIsInTicks = true; +#endif + return mSeconds = aTime; + } + int64_t operator=(int64_t aTime) { +#if DEBUG + mIsInSeconds = true; + mIsInTicks = true; +#endif + return mTicks = aTime; + } + + template <class TimeType> + TimeType Get() const; + + private: + union { + double mSeconds; + int64_t mTicks; + }; +#ifdef DEBUG + bool mIsInSeconds; + bool mIsInTicks; + + public: + bool IsInTicks() const { return mIsInTicks; }; +#endif + }; + + AudioTimelineEvent(Type aType, double aTime, float aValue, + double aTimeConstant = 0.0); + // For SetValueCurve + AudioTimelineEvent(Type aType, const nsTArray<float>& aValues, + double aStartTime, double aDuration); + AudioTimelineEvent(const AudioTimelineEvent& rhs); + ~AudioTimelineEvent(); + + template <class TimeType> + TimeType Time() const { + return mTime.Get<TimeType>(); + } + // If this event is a curve event, this returns the end time of the curve. + // Otherwise, this returns the time of the event. + template <class TimeType> + double EndTime() const; + + float NominalValue() const { + MOZ_ASSERT(mType != SetValueCurve); + return mValue; + } + float StartValue() const { + MOZ_ASSERT(mType == SetValueCurve); + return mCurve[0]; + } + // Value for an event, or for a ValueCurve event, this is the value of the + // last element of the curve. + float EndValue() const; + + double TimeConstant() const { + MOZ_ASSERT(mType == SetTarget); + return mTimeConstant; + } + uint32_t CurveLength() const { + MOZ_ASSERT(mType == SetValueCurve); + return mCurveLength; + } + double Duration() const { + MOZ_ASSERT(mType == SetValueCurve); + return mDuration; + } + /** + * Converts an AudioTimelineEvent's floating point time members to tick + * values with respect to a destination AudioNodeTrack. + * + * This needs to be called for each AudioTimelineEvent that gets sent to an + * AudioNodeEngine, on the engine side where the AudioTimlineEvent is + * received. This means that such engines need to be aware of their + * destination tracks as well. + */ + void ConvertToTicks(AudioNodeTrack* aDestination); + + template <class TimeType> + void FillTargetApproach(TimeType aBufferStartTime, Span<float> aBuffer, + double v0) const; + template <class TimeType> + void FillFromValueCurve(TimeType aBufferStartTime, Span<float> aBuffer) const; + + const Type mType; + + private: + union { + float mValue; + uint32_t mCurveLength; // for SetValueCurve + }; + union { + double mTimeConstant; + // mCurve contains a buffer of SetValueCurve samples. We sample the + // values in the buffer depending on how far along we are in time. + // If we're at time T and the event has started as time T0 and has a + // duration of D, we sample the buffer at floor(mCurveLength*(T-T0)/D) + // if T<T0+D, and just take the last sample in the buffer otherwise. + float* mCurve; + }; + union { + // mPerTickRatio is used only with SetTarget and int64_t TimeType. + double mPerTickRatio; + double mDuration; // for SetValueCurve + }; + + // This member is accessed using the `Time` method. + // + // The time for an event can either be in seconds or in ticks. + // Initially the time of the event is always in seconds. + // In order to convert it to ticks, call SetTimeInTicks. Once this + // method has been called for an event, the time cannot be converted + // back to seconds. + TimeUnion mTime; +}; + +template <> +inline double AudioTimelineEvent::TimeUnion::Get<double>() const { + MOZ_ASSERT(mIsInSeconds); + return mSeconds; +} +template <> +inline int64_t AudioTimelineEvent::TimeUnion::Get<int64_t>() const { + MOZ_ASSERT(mIsInTicks); + return mTicks; +} + +class AudioEventTimeline { + public: + explicit AudioEventTimeline(float aDefaultValue) + : mDefaultValue(aDefaultValue), + mSetTargetStartValue(aDefaultValue), + mSimpleValue(Some(aDefaultValue)) {} + + bool ValidateEvent(const AudioTimelineEvent& aEvent, ErrorResult& aRv) const { + MOZ_ASSERT(NS_IsMainThread()); + + auto TimeOf = [](const AudioTimelineEvent& aEvent) -> double { + return aEvent.Time<double>(); + }; + + // Validate the event itself + if (!WebAudioUtils::IsTimeValid(TimeOf(aEvent))) { + aRv.ThrowRangeError<MSG_INVALID_AUDIOPARAM_METHOD_START_TIME_ERROR>(); + return false; + } + + switch (aEvent.mType) { + case AudioTimelineEvent::SetValueCurve: + if (aEvent.CurveLength() < 2) { + aRv.ThrowInvalidStateError("Curve length must be at least 2"); + return false; + } + if (aEvent.Duration() <= 0) { + aRv.ThrowRangeError( + "The curve duration for setValueCurveAtTime must be strictly " + "positive."); + return false; + } + MOZ_ASSERT(IsValid(aEvent.Duration())); + break; + case AudioTimelineEvent::SetTarget: + if (!WebAudioUtils::IsTimeValid(aEvent.TimeConstant())) { + aRv.ThrowRangeError( + "The exponential constant passed to setTargetAtTime must be " + "non-negative."); + return false; + } + [[fallthrough]]; + default: + MOZ_ASSERT(IsValid(aEvent.NominalValue())); + } + + // Make sure that new events don't fall within the duration of a + // curve event. + for (unsigned i = 0; i < mEvents.Length(); ++i) { + if (mEvents[i].mType == AudioTimelineEvent::SetValueCurve && + TimeOf(mEvents[i]) <= TimeOf(aEvent) && + TimeOf(mEvents[i]) + mEvents[i].Duration() > TimeOf(aEvent)) { + aRv.ThrowNotSupportedError("Can't add events during a curve event"); + return false; + } + } + + // Make sure that new curve events don't fall in a range which includes + // other events. + if (aEvent.mType == AudioTimelineEvent::SetValueCurve) { + for (unsigned i = 0; i < mEvents.Length(); ++i) { + if (TimeOf(aEvent) < TimeOf(mEvents[i]) && + TimeOf(aEvent) + aEvent.Duration() > TimeOf(mEvents[i])) { + aRv.ThrowNotSupportedError( + "Can't add curve events that overlap other events"); + return false; + } + } + } + + // Make sure that invalid values are not used for exponential curves + if (aEvent.mType == AudioTimelineEvent::ExponentialRamp) { + if (aEvent.NominalValue() == 0.f) { + aRv.ThrowRangeError( + "The value passed to exponentialRampToValueAtTime must be " + "non-zero."); + return false; + } + } + return true; + } + + template <typename TimeType> + void InsertEvent(const AudioTimelineEvent& aEvent) { + mSimpleValue.reset(); + for (unsigned i = 0; i < mEvents.Length(); ++i) { + if (aEvent.Time<TimeType>() == mEvents[i].Time<TimeType>()) { + // If two events happen at the same time, have them in chronological + // order of insertion. + do { + ++i; + } while (i < mEvents.Length() && + aEvent.Time<TimeType>() == mEvents[i].Time<TimeType>()); + mEvents.InsertElementAt(i, aEvent); + return; + } + // Otherwise, place the event right after the latest existing event + if (aEvent.Time<TimeType>() < mEvents[i].Time<TimeType>()) { + mEvents.InsertElementAt(i, aEvent); + return; + } + } + + // If we couldn't find a place for the event, just append it to the list + mEvents.AppendElement(aEvent); + } + + bool HasSimpleValue() const { return mSimpleValue.isSome(); } + + float GetValue() const { + // This method should only be called if HasSimpleValue() returns true + MOZ_ASSERT(HasSimpleValue()); + return mSimpleValue.value(); + } + + void SetValue(float aValue) { + // FIXME: bug 1308435 + // A spec change means this should instead behave like setValueAtTime(). + + // Silently don't change anything if there are any events + if (mEvents.IsEmpty()) { + mSetTargetStartValue = mDefaultValue = aValue; + mSimpleValue = Some(aValue); + } + } + + template <typename TimeType> + void CancelScheduledValues(TimeType aStartTime) { + for (unsigned i = 0; i < mEvents.Length(); ++i) { + if (mEvents[i].Time<TimeType>() >= aStartTime) { +#ifdef DEBUG + // Sanity check: the array should be sorted, so all of the following + // events should have a time greater than aStartTime too. + for (unsigned j = i + 1; j < mEvents.Length(); ++j) { + MOZ_ASSERT(mEvents[j].Time<TimeType>() >= aStartTime); + } +#endif + mEvents.TruncateLength(i); + break; + } + } + if (mEvents.IsEmpty()) { + mSimpleValue = Some(mDefaultValue); + } + } + + static bool TimesEqual(int64_t aLhs, int64_t aRhs) { return aLhs == aRhs; } + + // Since we are going to accumulate error by adding 0.01 multiple time in a + // loop, we want to fuzz the equality check in GetValueAtTime. + static bool TimesEqual(double aLhs, double aRhs) { + const float kEpsilon = 0.0000000001f; + return fabs(aLhs - aRhs) < kEpsilon; + } + + template <class TimeType> + float GetValueAtTime(TimeType aTime) { + float result; + GetValuesAtTimeHelper(aTime, &result, 1); + return result; + } + + void GetValuesAtTime(int64_t aTime, float* aBuffer, const size_t aSize) { + MOZ_ASSERT(aBuffer); + GetValuesAtTimeHelper(aTime, aBuffer, aSize); + } + void GetValuesAtTime(double aTime, float* aBuffer, + const size_t aSize) = delete; + + // Return the number of events scheduled + uint32_t GetEventCount() const { return mEvents.Length(); } + + template <class TimeType> + void CleanupEventsOlderThan(TimeType aTime); + + private: + template <class TimeType> + void GetValuesAtTimeHelper(TimeType aTime, float* aBuffer, + const size_t aSize); + + template <class TimeType> + float GetValueAtTimeOfEvent(const AudioTimelineEvent* aEvent, + const AudioTimelineEvent* aPrevious); + + template <class TimeType> + void GetValuesAtTimeHelperInternal(TimeType aStartTime, Span<float> aBuffer, + const AudioTimelineEvent* aPrevious, + const AudioTimelineEvent* aNext); + + static bool IsValid(double value) { return std::isfinite(value); } + + template <class TimeType> + float ComputeSetTargetStartValue(const AudioTimelineEvent* aPreviousEvent, + TimeType aTime); + + // This is a sorted array of the events in the timeline. Queries of this + // data structure should probably be more frequent than modifications to it, + // and that is the reason why we're using a simple array as the data + // structure. We can optimize this in the future if the performance of the + // array ends up being a bottleneck. + nsTArray<AudioTimelineEvent> mEvents; + float mDefaultValue; + // This is the value of this AudioParam at the end of the previous + // event for SetTarget curves. + float mSetTargetStartValue; + AudioTimelineEvent::TimeUnion mSetTargetStartTime; + Maybe<float> mSimpleValue; +}; + +} // namespace dom +} // namespace mozilla + +#endif diff --git a/dom/media/webaudio/AudioListener.cpp b/dom/media/webaudio/AudioListener.cpp new file mode 100644 index 0000000000..b8c4e9bc65 --- /dev/null +++ b/dom/media/webaudio/AudioListener.cpp @@ -0,0 +1,121 @@ +/* -*- 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 "AudioListener.h" +#include "MediaTrackGraph.h" +#include "Tracing.h" +#include "mozilla/dom/AudioListenerBinding.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(AudioListener, mContext) + +AudioListenerEngine::AudioListenerEngine() + : mFrontVector(0., 0., -1.), mRightVector(1., 0., 0.) {} + +void AudioListenerEngine::RecvListenerEngineEvent( + AudioListenerEngine::AudioListenerParameter aParameter, + const ThreeDPoint& aValue) { + switch (aParameter) { + case AudioListenerParameter::POSITION: + mPosition = aValue; + break; + case AudioListenerParameter::FRONT: + mFrontVector = aValue; + break; + case AudioListenerParameter::RIGHT: + mRightVector = aValue; + break; + default: + MOZ_CRASH("Not handled"); + } +} + +const ThreeDPoint& AudioListenerEngine::Position() const { return mPosition; } +const ThreeDPoint& AudioListenerEngine::FrontVector() const { + return mFrontVector; +} +const ThreeDPoint& AudioListenerEngine::RightVector() const { + return mRightVector; +} + +AudioListener::AudioListener(AudioContext* aContext) + : mContext(aContext), + mEngine(new AudioListenerEngine()), + + mFrontVector(0., 0., -1.), + mRightVector(1., 0., 0.) { + MOZ_ASSERT(aContext); +} + +JSObject* AudioListener::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return AudioListener_Binding::Wrap(aCx, this, aGivenProto); +} + +void AudioListener::SetOrientation(double aX, double aY, double aZ, double aXUp, + double aYUp, double aZUp) { + ThreeDPoint front(aX, aY, aZ); + // The panning effect and the azimuth and elevation calculation in the Web + // Audio spec becomes undefined with linearly dependent vectors, so keep + // existing state in these situations. + if (front.IsZero()) { + return; + } + // Normalize before using CrossProduct() to avoid overflow. + front.Normalize(); + ThreeDPoint up(aXUp, aYUp, aZUp); + if (up.IsZero()) { + return; + } + up.Normalize(); + ThreeDPoint right = front.CrossProduct(up); + if (right.IsZero()) { + return; + } + right.Normalize(); + + if (!mFrontVector.FuzzyEqual(front)) { + mFrontVector = front; + SendListenerEngineEvent(AudioListenerEngine::AudioListenerParameter::FRONT, + mFrontVector); + } + if (!mRightVector.FuzzyEqual(right)) { + mRightVector = right; + SendListenerEngineEvent(AudioListenerEngine::AudioListenerParameter::RIGHT, + mRightVector); + } +} + +void AudioListener::SetPosition(double aX, double aY, double aZ) { + if (WebAudioUtils::FuzzyEqual(mPosition.x, aX) && + WebAudioUtils::FuzzyEqual(mPosition.y, aY) && + WebAudioUtils::FuzzyEqual(mPosition.z, aZ)) { + return; + } + mPosition.x = aX; + mPosition.y = aY; + mPosition.z = aZ; + SendListenerEngineEvent(AudioListenerEngine::AudioListenerParameter::POSITION, + mPosition); +} + +void AudioListener::SendListenerEngineEvent( + AudioListenerEngine::AudioListenerParameter aParameter, + const ThreeDPoint& aValue) { + mContext->DestinationTrack()->QueueControlMessageWithNoShutdown( + [engine = RefPtr(Engine()), aParameter, aValue] { + TRACE("AudioListener::RecvListenerEngineEvent"); + engine->RecvListenerEngineEvent(aParameter, aValue); + }); +} + +size_t AudioListener::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const { + return aMallocSizeOf(this); +} + +} // namespace mozilla::dom diff --git a/dom/media/webaudio/AudioListener.h b/dom/media/webaudio/AudioListener.h new file mode 100644 index 0000000000..7d7fad3bfb --- /dev/null +++ b/dom/media/webaudio/AudioListener.h @@ -0,0 +1,85 @@ +/* -*- 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/. */ + +#ifndef AudioListener_h_ +#define AudioListener_h_ + +#include "nsWrapperCache.h" +#include "nsCycleCollectionParticipant.h" +#include "mozilla/Attributes.h" +#include "ThreeDPoint.h" +#include "AudioContext.h" +#include "PannerNode.h" +#include "WebAudioUtils.h" +#include "js/TypeDecls.h" +#include "mozilla/MemoryReporting.h" + +namespace mozilla::dom { + +class AudioListenerEngine final { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(AudioListenerEngine) + + AudioListenerEngine(); + + enum class AudioListenerParameter { + POSITION, + FRONT, // unit length + RIGHT // unit length, orthogonal to FRONT + }; + void RecvListenerEngineEvent( + AudioListenerEngine::AudioListenerParameter aParameter, + const ThreeDPoint& aValue); + const ThreeDPoint& Position() const; + const ThreeDPoint& FrontVector() const; + const ThreeDPoint& RightVector() const; + + private: + ~AudioListenerEngine() = default; + + ThreeDPoint mPosition; + ThreeDPoint mFrontVector; + ThreeDPoint mRightVector; +}; + +class AudioListener final : public nsWrapperCache { + public: + explicit AudioListener(AudioContext* aContext); + + NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(AudioListener) + NS_DECL_CYCLE_COLLECTION_NATIVE_WRAPPERCACHE_CLASS(AudioListener) + + size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const; + + AudioContext* GetParentObject() const { return mContext; } + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + void SetPosition(double aX, double aY, double aZ); + void SetOrientation(double aX, double aY, double aZ, double aXUp, double aYUp, + double aZUp); + + AudioListenerEngine* Engine() { return mEngine.get(); } + + private: + void SendListenerEngineEvent( + AudioListenerEngine::AudioListenerParameter aParameter, + const ThreeDPoint& aValue); + + ~AudioListener() = default; + + private: + RefPtr<AudioContext> mContext; + RefPtr<AudioListenerEngine> mEngine; + ThreeDPoint mPosition; + ThreeDPoint mFrontVector; + ThreeDPoint mRightVector; +}; + +} // namespace mozilla::dom + +#endif diff --git a/dom/media/webaudio/AudioNode.cpp b/dom/media/webaudio/AudioNode.cpp new file mode 100644 index 0000000000..f5c56d0da6 --- /dev/null +++ b/dom/media/webaudio/AudioNode.cpp @@ -0,0 +1,608 @@ +/* -*- 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 "AudioNode.h" +#include "mozilla/ErrorResult.h" +#include "AudioNodeTrack.h" +#include "AudioNodeEngine.h" +#include "mozilla/dom/AudioParam.h" +#include "mozilla/Services.h" +#include "nsIObserverService.h" + +namespace mozilla::dom { + +static const uint32_t INVALID_PORT = 0xffffffff; +static uint32_t gId = 0; + +NS_IMPL_CYCLE_COLLECTION_CLASS(AudioNode) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(AudioNode, DOMEventTargetHelper) + tmp->DisconnectFromGraph(); + if (tmp->mContext) { + tmp->mContext->UnregisterNode(tmp); + } + NS_IMPL_CYCLE_COLLECTION_UNLINK(mContext) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mParams) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mOutputNodes) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mOutputParams) + NS_IMPL_CYCLE_COLLECTION_UNLINK_WEAK_REFERENCE +NS_IMPL_CYCLE_COLLECTION_UNLINK_END +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(AudioNode, + DOMEventTargetHelper) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mContext) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mParams) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOutputNodes) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOutputParams) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_ADDREF_INHERITED(AudioNode, DOMEventTargetHelper) +NS_IMPL_RELEASE_INHERITED(AudioNode, DOMEventTargetHelper) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(AudioNode) + NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) +NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper) + +AudioNode::AudioNode(AudioContext* aContext, uint32_t aChannelCount, + ChannelCountMode aChannelCountMode, + ChannelInterpretation aChannelInterpretation) + : DOMEventTargetHelper(aContext->GetParentObject()), + mContext(aContext), + mChannelCount(aChannelCount), + mChannelCountMode(aChannelCountMode), + mChannelInterpretation(aChannelInterpretation), + mId(gId++), + mPassThrough(false) { + MOZ_ASSERT(aContext); + aContext->RegisterNode(this); +} + +AudioNode::~AudioNode() { + MOZ_ASSERT(mInputNodes.IsEmpty()); + MOZ_ASSERT(mOutputNodes.IsEmpty()); + MOZ_ASSERT(mOutputParams.IsEmpty()); + MOZ_ASSERT(!mTrack, + "The webaudio-node-demise notification must have been sent"); + if (mContext) { + mContext->UnregisterNode(this); + } +} + +void AudioNode::Initialize(const AudioNodeOptions& aOptions, ErrorResult& aRv) { + if (aOptions.mChannelCount.WasPassed()) { + SetChannelCount(aOptions.mChannelCount.Value(), aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + } + + if (aOptions.mChannelCountMode.WasPassed()) { + SetChannelCountModeValue(aOptions.mChannelCountMode.Value(), aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + } + + if (aOptions.mChannelInterpretation.WasPassed()) { + SetChannelInterpretationValue(aOptions.mChannelInterpretation.Value(), aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + } +} + +size_t AudioNode::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const { + // Not owned: + // - mContext + // - mTrack + size_t amount = 0; + + amount += mInputNodes.ShallowSizeOfExcludingThis(aMallocSizeOf); + for (size_t i = 0; i < mInputNodes.Length(); i++) { + amount += mInputNodes[i].SizeOfExcludingThis(aMallocSizeOf); + } + + // Just measure the array. The entire audio node graph is measured via the + // MediaTrackGraph's tracks, so we don't want to double-count the elements. + amount += mOutputNodes.ShallowSizeOfExcludingThis(aMallocSizeOf); + + amount += mOutputParams.ShallowSizeOfExcludingThis(aMallocSizeOf); + for (size_t i = 0; i < mOutputParams.Length(); i++) { + amount += mOutputParams[i]->SizeOfIncludingThis(aMallocSizeOf); + } + + return amount; +} + +size_t AudioNode::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const { + return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf); +} + +template <class InputNode> +static size_t FindIndexOfNode(const nsTArray<InputNode>& aInputNodes, + const AudioNode* aNode) { + for (size_t i = 0; i < aInputNodes.Length(); ++i) { + if (aInputNodes[i].mInputNode == aNode) { + return i; + } + } + return nsTArray<InputNode>::NoIndex; +} + +template <class InputNode> +static size_t FindIndexOfNodeWithPorts(const nsTArray<InputNode>& aInputNodes, + const AudioNode* aNode, + uint32_t aInputPort, + uint32_t aOutputPort) { + for (size_t i = 0; i < aInputNodes.Length(); ++i) { + if (aInputNodes[i].mInputNode == aNode && + aInputNodes[i].mInputPort == aInputPort && + aInputNodes[i].mOutputPort == aOutputPort) { + return i; + } + } + return nsTArray<InputNode>::NoIndex; +} + +void AudioNode::DisconnectFromGraph() { + MOZ_ASSERT(mRefCnt.get() > mInputNodes.Length(), + "Caller should be holding a reference"); + + // The idea here is that we remove connections one by one, and at each step + // the graph is in a valid state. + + // Disconnect inputs. We don't need them anymore. + while (!mInputNodes.IsEmpty()) { + InputNode inputNode = mInputNodes.PopLastElement(); + inputNode.mInputNode->mOutputNodes.RemoveElement(this); + } + + while (!mOutputNodes.IsEmpty()) { + RefPtr<AudioNode> output = mOutputNodes.PopLastElement(); + size_t inputIndex = FindIndexOfNode(output->mInputNodes, this); + // It doesn't matter which one we remove, since we're going to remove all + // entries for this node anyway. + output->mInputNodes.RemoveElementAt(inputIndex); + // This effects of this connection will remain. + output->NotifyHasPhantomInput(); + } + + while (!mOutputParams.IsEmpty()) { + RefPtr<AudioParam> output = mOutputParams.PopLastElement(); + size_t inputIndex = FindIndexOfNode(output->InputNodes(), this); + // It doesn't matter which one we remove, since we're going to remove all + // entries for this node anyway. + output->RemoveInputNode(inputIndex); + } + + DestroyMediaTrack(); +} + +AudioNode* AudioNode::Connect(AudioNode& aDestination, uint32_t aOutput, + uint32_t aInput, ErrorResult& aRv) { + if (aOutput >= NumberOfOutputs()) { + aRv.ThrowIndexSizeError( + nsPrintfCString("Output index %u is out of bounds", aOutput)); + return nullptr; + } + + if (aInput >= aDestination.NumberOfInputs()) { + aRv.ThrowIndexSizeError( + nsPrintfCString("Input index %u is out of bounds", aInput)); + return nullptr; + } + + if (Context() != aDestination.Context()) { + aRv.ThrowInvalidAccessError( + "Can't connect nodes from different AudioContexts"); + return nullptr; + } + + if (FindIndexOfNodeWithPorts(aDestination.mInputNodes, this, aInput, + aOutput) != + nsTArray<AudioNode::InputNode>::NoIndex) { + // connection already exists. + return &aDestination; + } + + WEB_AUDIO_API_LOG("%f: %s %u Connect() to %s %u", Context()->CurrentTime(), + NodeType(), Id(), aDestination.NodeType(), + aDestination.Id()); + + // The MediaTrackGraph will handle cycle detection. We don't need to do it + // here. + + mOutputNodes.AppendElement(&aDestination); + InputNode* input = aDestination.mInputNodes.AppendElement(); + input->mInputNode = this; + input->mInputPort = aInput; + input->mOutputPort = aOutput; + AudioNodeTrack* destinationTrack = aDestination.mTrack; + if (mTrack && destinationTrack) { + // Connect tracks in the MediaTrackGraph + MOZ_ASSERT(aInput <= UINT16_MAX, "Unexpected large input port number"); + MOZ_ASSERT(aOutput <= UINT16_MAX, "Unexpected large output port number"); + input->mTrackPort = destinationTrack->AllocateInputPort( + mTrack, static_cast<uint16_t>(aInput), static_cast<uint16_t>(aOutput)); + } + aDestination.NotifyInputsChanged(); + + return &aDestination; +} + +void AudioNode::Connect(AudioParam& aDestination, uint32_t aOutput, + ErrorResult& aRv) { + if (aOutput >= NumberOfOutputs()) { + aRv.ThrowIndexSizeError( + nsPrintfCString("Output index %u is out of bounds", aOutput)); + return; + } + + if (Context() != aDestination.GetParentObject()) { + aRv.ThrowInvalidAccessError( + "Can't connect a node to an AudioParam from a different AudioContext"); + return; + } + + if (FindIndexOfNodeWithPorts(aDestination.InputNodes(), this, INVALID_PORT, + aOutput) != + nsTArray<AudioNode::InputNode>::NoIndex) { + // connection already exists. + return; + } + + mOutputParams.AppendElement(&aDestination); + InputNode* input = aDestination.AppendInputNode(); + input->mInputNode = this; + input->mInputPort = INVALID_PORT; + input->mOutputPort = aOutput; + + mozilla::MediaTrack* track = aDestination.Track(); + MOZ_ASSERT(track->AsProcessedTrack()); + ProcessedMediaTrack* ps = static_cast<ProcessedMediaTrack*>(track); + if (mTrack) { + // Setup our track as an input to the AudioParam's track + MOZ_ASSERT(aOutput <= UINT16_MAX, "Unexpected large output port number"); + input->mTrackPort = + ps->AllocateInputPort(mTrack, 0, static_cast<uint16_t>(aOutput)); + } +} + +void AudioNode::SendDoubleParameterToTrack(uint32_t aIndex, double aValue) { + MOZ_ASSERT(mTrack, "How come we don't have a track here?"); + mTrack->SetDoubleParameter(aIndex, aValue); +} + +void AudioNode::SendInt32ParameterToTrack(uint32_t aIndex, int32_t aValue) { + MOZ_ASSERT(mTrack, "How come we don't have a track here?"); + mTrack->SetInt32Parameter(aIndex, aValue); +} + +void AudioNode::SendChannelMixingParametersToTrack() { + if (mTrack) { + mTrack->SetChannelMixingParameters(mChannelCount, mChannelCountMode, + mChannelInterpretation); + } +} + +template <> +bool AudioNode::DisconnectFromOutputIfConnected<AudioNode>( + uint32_t aOutputNodeIndex, uint32_t aInputIndex) { + WEB_AUDIO_API_LOG("%f: %s %u Disconnect()", Context()->CurrentTime(), + NodeType(), Id()); + + AudioNode* destination = mOutputNodes[aOutputNodeIndex]; + + MOZ_ASSERT(aOutputNodeIndex < mOutputNodes.Length()); + MOZ_ASSERT(aInputIndex < destination->InputNodes().Length()); + + // An upstream node may be starting to play on the graph thread, and the + // engine for a downstream node may be sending a PlayingRefChangeHandler + // ADDREF message to this (main) thread. Wait for a round trip before + // releasing nodes, to give engines receiving sound now time to keep their + // nodes alive. + class RunnableRelease final : public Runnable { + public: + explicit RunnableRelease(already_AddRefed<AudioNode> aNode) + : mozilla::Runnable("RunnableRelease"), mNode(aNode) {} + + NS_IMETHOD Run() override { + mNode = nullptr; + return NS_OK; + } + + private: + RefPtr<AudioNode> mNode; + }; + + InputNode& input = destination->mInputNodes[aInputIndex]; + if (input.mInputNode != this) { + return false; + } + + // Remove one instance of 'dest' from mOutputNodes. There could be + // others, and it's not correct to remove them all since some of them + // could be for different output ports. + RefPtr<AudioNode> output = std::move(mOutputNodes[aOutputNodeIndex]); + mOutputNodes.RemoveElementAt(aOutputNodeIndex); + // Destroying the InputNode here sends a message to the graph thread + // to disconnect the tracks, which should be sent before the + // RunAfterPendingUpdates() call below. + destination->mInputNodes.RemoveElementAt(aInputIndex); + output->NotifyInputsChanged(); + if (mTrack) { + nsCOMPtr<nsIRunnable> runnable = new RunnableRelease(output.forget()); + mTrack->RunAfterPendingUpdates(runnable.forget()); + } + return true; +} + +template <> +bool AudioNode::DisconnectFromOutputIfConnected<AudioParam>( + uint32_t aOutputParamIndex, uint32_t aInputIndex) { + MOZ_ASSERT(aOutputParamIndex < mOutputParams.Length()); + + AudioParam* destination = mOutputParams[aOutputParamIndex]; + + MOZ_ASSERT(aInputIndex < destination->InputNodes().Length()); + + const InputNode& input = destination->InputNodes()[aInputIndex]; + if (input.mInputNode != this) { + return false; + } + destination->RemoveInputNode(aInputIndex); + // Remove one instance of 'dest' from mOutputParams. There could be + // others, and it's not correct to remove them all since some of them + // could be for different output ports. + mOutputParams.RemoveElementAt(aOutputParamIndex); + return true; +} + +template <> +const nsTArray<AudioNode::InputNode>& +AudioNode::InputsForDestination<AudioNode>(uint32_t aOutputNodeIndex) const { + return mOutputNodes[aOutputNodeIndex]->InputNodes(); +} + +template <> +const nsTArray<AudioNode::InputNode>& +AudioNode::InputsForDestination<AudioParam>(uint32_t aOutputNodeIndex) const { + return mOutputParams[aOutputNodeIndex]->InputNodes(); +} + +template <typename DestinationType, typename Predicate> +bool AudioNode::DisconnectMatchingDestinationInputs(uint32_t aDestinationIndex, + Predicate aPredicate) { + bool wasConnected = false; + uint32_t inputCount = + InputsForDestination<DestinationType>(aDestinationIndex).Length(); + + for (int32_t inputIndex = inputCount - 1; inputIndex >= 0; --inputIndex) { + const InputNode& input = + InputsForDestination<DestinationType>(aDestinationIndex)[inputIndex]; + if (aPredicate(input)) { + if (DisconnectFromOutputIfConnected<DestinationType>(aDestinationIndex, + inputIndex)) { + wasConnected = true; + break; + } + } + } + return wasConnected; +} + +void AudioNode::Disconnect(ErrorResult& aRv) { + for (int32_t outputIndex = mOutputNodes.Length() - 1; outputIndex >= 0; + --outputIndex) { + DisconnectMatchingDestinationInputs<AudioNode>( + outputIndex, [](const InputNode&) { return true; }); + } + + for (int32_t outputIndex = mOutputParams.Length() - 1; outputIndex >= 0; + --outputIndex) { + DisconnectMatchingDestinationInputs<AudioParam>( + outputIndex, [](const InputNode&) { return true; }); + } +} + +void AudioNode::Disconnect(uint32_t aOutput, ErrorResult& aRv) { + if (aOutput >= NumberOfOutputs()) { + aRv.ThrowIndexSizeError( + nsPrintfCString("Output index %u is out of bounds", aOutput)); + return; + } + + for (int32_t outputIndex = mOutputNodes.Length() - 1; outputIndex >= 0; + --outputIndex) { + DisconnectMatchingDestinationInputs<AudioNode>( + outputIndex, [aOutput](const InputNode& aInputNode) { + return aInputNode.mOutputPort == aOutput; + }); + } + + for (int32_t outputIndex = mOutputParams.Length() - 1; outputIndex >= 0; + --outputIndex) { + DisconnectMatchingDestinationInputs<AudioParam>( + outputIndex, [aOutput](const InputNode& aInputNode) { + return aInputNode.mOutputPort == aOutput; + }); + } +} + +void AudioNode::Disconnect(AudioNode& aDestination, ErrorResult& aRv) { + bool wasConnected = false; + + for (int32_t outputIndex = mOutputNodes.Length() - 1; outputIndex >= 0; + --outputIndex) { + if (mOutputNodes[outputIndex] != &aDestination) { + continue; + } + wasConnected |= DisconnectMatchingDestinationInputs<AudioNode>( + outputIndex, [](const InputNode&) { return true; }); + } + + if (!wasConnected) { + aRv.ThrowInvalidAccessError( + "Trying to disconnect from a node we're not connected to"); + return; + } +} + +void AudioNode::Disconnect(AudioNode& aDestination, uint32_t aOutput, + ErrorResult& aRv) { + if (aOutput >= NumberOfOutputs()) { + aRv.ThrowIndexSizeError( + nsPrintfCString("Output index %u is out of bounds", aOutput)); + return; + } + + bool wasConnected = false; + + for (int32_t outputIndex = mOutputNodes.Length() - 1; outputIndex >= 0; + --outputIndex) { + if (mOutputNodes[outputIndex] != &aDestination) { + continue; + } + wasConnected |= DisconnectMatchingDestinationInputs<AudioNode>( + outputIndex, [aOutput](const InputNode& aInputNode) { + return aInputNode.mOutputPort == aOutput; + }); + } + + if (!wasConnected) { + aRv.ThrowInvalidAccessError( + "Trying to disconnect from a node we're not connected to"); + return; + } +} + +void AudioNode::Disconnect(AudioNode& aDestination, uint32_t aOutput, + uint32_t aInput, ErrorResult& aRv) { + if (aOutput >= NumberOfOutputs()) { + aRv.ThrowIndexSizeError( + nsPrintfCString("Output index %u is out of bounds", aOutput)); + return; + } + + if (aInput >= aDestination.NumberOfInputs()) { + aRv.ThrowIndexSizeError( + nsPrintfCString("Input index %u is out of bounds", aInput)); + return; + } + + bool wasConnected = false; + + for (int32_t outputIndex = mOutputNodes.Length() - 1; outputIndex >= 0; + --outputIndex) { + if (mOutputNodes[outputIndex] != &aDestination) { + continue; + } + wasConnected |= DisconnectMatchingDestinationInputs<AudioNode>( + outputIndex, [aOutput, aInput](const InputNode& aInputNode) { + return aInputNode.mOutputPort == aOutput && + aInputNode.mInputPort == aInput; + }); + } + + if (!wasConnected) { + aRv.ThrowInvalidAccessError( + "Trying to disconnect from a node we're not connected to"); + return; + } +} + +void AudioNode::Disconnect(AudioParam& aDestination, ErrorResult& aRv) { + bool wasConnected = false; + + for (int32_t outputIndex = mOutputParams.Length() - 1; outputIndex >= 0; + --outputIndex) { + if (mOutputParams[outputIndex] != &aDestination) { + continue; + } + wasConnected |= DisconnectMatchingDestinationInputs<AudioParam>( + outputIndex, [](const InputNode&) { return true; }); + } + + if (!wasConnected) { + aRv.ThrowInvalidAccessError( + "Trying to disconnect from an AudioParam we're not connected to"); + return; + } +} + +void AudioNode::Disconnect(AudioParam& aDestination, uint32_t aOutput, + ErrorResult& aRv) { + if (aOutput >= NumberOfOutputs()) { + aRv.ThrowIndexSizeError( + nsPrintfCString("Output index %u is out of bounds", aOutput)); + return; + } + + bool wasConnected = false; + + for (int32_t outputIndex = mOutputParams.Length() - 1; outputIndex >= 0; + --outputIndex) { + if (mOutputParams[outputIndex] != &aDestination) { + continue; + } + wasConnected |= DisconnectMatchingDestinationInputs<AudioParam>( + outputIndex, [aOutput](const InputNode& aInputNode) { + return aInputNode.mOutputPort == aOutput; + }); + } + + if (!wasConnected) { + aRv.ThrowInvalidAccessError( + "Trying to disconnect from an AudioParam we're not connected to"); + return; + } +} + +void AudioNode::DestroyMediaTrack() { + if (mTrack) { + // Remove the node pointer on the engine. + AudioNodeTrack* ns = mTrack; + MOZ_ASSERT(ns, "How come we don't have a track here?"); + MOZ_ASSERT(ns->Engine()->NodeMainThread() == this, + "Invalid node reference"); + ns->Engine()->ClearNode(); + + mTrack->Destroy(); + mTrack = nullptr; + + nsCOMPtr<nsIObserverService> obs = services::GetObserverService(); + if (obs) { + nsAutoString id; + id.AppendPrintf("%u", mId); + obs->NotifyObservers(nullptr, "webaudio-node-demise", id.get()); + } + } +} + +void AudioNode::RemoveOutputParam(AudioParam* aParam) { + mOutputParams.RemoveElement(aParam); +} + +bool AudioNode::PassThrough() const { + MOZ_ASSERT(NumberOfInputs() <= 1 && NumberOfOutputs() == 1); + return mPassThrough; +} + +void AudioNode::SetPassThrough(bool aPassThrough) { + MOZ_ASSERT(NumberOfInputs() <= 1 && NumberOfOutputs() == 1); + mPassThrough = aPassThrough; + if (mTrack) { + mTrack->SetPassThrough(mPassThrough); + } +} + +AudioParam* AudioNode::CreateAudioParam(uint32_t aIndex, const nsAString& aName, + float aDefaultValue, float aMinValue, + float aMaxValue) { + return *mParams.AppendElement( + new AudioParam(this, aIndex, aName, aDefaultValue, aMinValue, aMaxValue)); +} + +} // namespace mozilla::dom diff --git a/dom/media/webaudio/AudioNode.h b/dom/media/webaudio/AudioNode.h new file mode 100644 index 0000000000..561749fd61 --- /dev/null +++ b/dom/media/webaudio/AudioNode.h @@ -0,0 +1,290 @@ +/* -*- 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/. */ + +#ifndef AudioNode_h_ +#define AudioNode_h_ + +#include "mozilla/DOMEventTargetHelper.h" +#include "mozilla/dom/AudioNodeBinding.h" +#include "nsCycleCollectionParticipant.h" +#include "nsTArray.h" +#include "AudioContext.h" +#include "MediaTrackGraph.h" +#include "WebAudioUtils.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/MemoryReporting.h" +#include "nsPrintfCString.h" +#include "nsWeakReference.h" +#include "SelfRef.h" + +namespace mozilla { + +class AbstractThread; + +namespace dom { + +class AudioContext; +class AudioBufferSourceNode; +class AudioParam; +class AudioParamTimeline; +struct ThreeDPoint; + +/** + * The DOM object representing a Web Audio AudioNode. + * + * Each AudioNode has a MediaTrack representing the actual + * real-time processing and output of this AudioNode. + * + * We track the incoming and outgoing connections to other AudioNodes. + * Outgoing connections have strong ownership. Also, AudioNodes that will + * produce sound on their output even when they have silent or no input ask + * the AudioContext to keep playing or tail-time references to keep them alive + * until the context is finished. + * + * Explicit disconnections will only remove references from output nodes after + * the graph is notified and the main thread receives a reply. Similarly, + * nodes with playing or tail-time references release these references only + * after receiving notification from their engine on the graph thread that + * playing has stopped. Engines notifying the main thread that they have + * finished do so strictly *after* producing and returning their last block. + * In this way, an engine that receives non-null input knows that the input + * comes from nodes that are still alive and will keep their output nodes + * alive for at least as long as it takes to process messages from the graph + * thread. i.e. the engine receiving non-null input knows that its node is + * still alive, and will still be alive when it receives a message from the + * engine. + */ +class AudioNode : public DOMEventTargetHelper, public nsSupportsWeakReference { + protected: + // You can only use refcounting to delete this object + virtual ~AudioNode(); + + public: + AudioNode(AudioContext* aContext, uint32_t aChannelCount, + ChannelCountMode aChannelCountMode, + ChannelInterpretation aChannelInterpretation); + + // This should be idempotent (safe to call multiple times). + virtual void DestroyMediaTrack(); + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(AudioNode, DOMEventTargetHelper) + + virtual AudioBufferSourceNode* AsAudioBufferSourceNode() { return nullptr; } + + AudioContext* GetParentObject() const { return mContext; } + + AudioContext* Context() const { return mContext; } + + virtual AudioNode* Connect(AudioNode& aDestination, uint32_t aOutput, + uint32_t aInput, ErrorResult& aRv); + + virtual void Connect(AudioParam& aDestination, uint32_t aOutput, + ErrorResult& aRv); + + virtual void Disconnect(ErrorResult& aRv); + virtual void Disconnect(uint32_t aOutput, ErrorResult& aRv); + virtual void Disconnect(AudioNode& aDestination, ErrorResult& aRv); + virtual void Disconnect(AudioNode& aDestination, uint32_t aOutput, + ErrorResult& aRv); + virtual void Disconnect(AudioNode& aDestination, uint32_t aOutput, + uint32_t aInput, ErrorResult& aRv); + virtual void Disconnect(AudioParam& aDestination, ErrorResult& aRv); + virtual void Disconnect(AudioParam& aDestination, uint32_t aOutput, + ErrorResult& aRv); + + // Called after input nodes have been explicitly added or removed through + // the Connect() or Disconnect() methods. + virtual void NotifyInputsChanged() {} + // Indicate that the node should continue indefinitely to behave as if an + // input is connected, even though there is no longer a corresponding entry + // in mInputNodes. Called after an input node has been removed because it + // is being garbage collected. + virtual void NotifyHasPhantomInput() {} + + // The following two virtual methods must be implemented by each node type + // to provide their number of input and output ports. These numbers are + // constant for the lifetime of the node. Both default to 1. + virtual uint16_t NumberOfInputs() const { return 1; } + virtual uint16_t NumberOfOutputs() const { return 1; } + + uint32_t Id() const { return mId; } + + bool PassThrough() const; + void SetPassThrough(bool aPassThrough); + + uint32_t ChannelCount() const { return mChannelCount; } + virtual void SetChannelCount(uint32_t aChannelCount, ErrorResult& aRv) { + if (aChannelCount == 0 || aChannelCount > WebAudioUtils::MaxChannelCount) { + aRv.ThrowNotSupportedError( + nsPrintfCString("Channel count (%u) must be in the range [1, max " + "supported channel count]", + aChannelCount)); + return; + } + mChannelCount = aChannelCount; + SendChannelMixingParametersToTrack(); + } + ChannelCountMode ChannelCountModeValue() const { return mChannelCountMode; } + virtual void SetChannelCountModeValue(ChannelCountMode aMode, + ErrorResult& aRv) { + mChannelCountMode = aMode; + SendChannelMixingParametersToTrack(); + } + ChannelInterpretation ChannelInterpretationValue() const { + return mChannelInterpretation; + } + virtual void SetChannelInterpretationValue(ChannelInterpretation aMode, + ErrorResult& aRv) { + mChannelInterpretation = aMode; + SendChannelMixingParametersToTrack(); + } + + struct InputNode final { + InputNode() = default; + InputNode(const InputNode&) = delete; + InputNode(InputNode&&) = default; + + ~InputNode() { + if (mTrackPort) { + mTrackPort->Destroy(); + } + } + + size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const { + size_t amount = 0; + if (mTrackPort) { + amount += mTrackPort->SizeOfIncludingThis(aMallocSizeOf); + } + + return amount; + } + + // The InputNode is destroyed when mInputNode is disconnected. + AudioNode* MOZ_NON_OWNING_REF mInputNode; + RefPtr<MediaInputPort> mTrackPort; + // The index of the input port this node feeds into. + // This is not used for connections to AudioParams. + uint32_t mInputPort; + // The index of the output port this node comes out of. + uint32_t mOutputPort; + }; + + // Returns the track, if any. + AudioNodeTrack* GetTrack() const { return mTrack; } + + const nsTArray<InputNode>& InputNodes() const { return mInputNodes; } + const nsTArray<RefPtr<AudioNode>>& OutputNodes() const { + return mOutputNodes; + } + const nsTArray<RefPtr<AudioParam>>& OutputParams() const { + return mOutputParams; + } + + template <typename T> + const nsTArray<InputNode>& InputsForDestination(uint32_t aOutputIndex) const; + + void RemoveOutputParam(AudioParam* aParam); + + // MarkActive() asks the context to keep the AudioNode alive until the + // context is finished. This takes care of "playing" references and + // "tail-time" references. + void MarkActive() { Context()->RegisterActiveNode(this); } + // Active nodes call MarkInactive() when they have finished producing sound + // for the foreseeable future. + // Do not call MarkInactive from a node destructor. If the destructor is + // called, then the node is already inactive. + // MarkInactive() may delete |this|. + void MarkInactive() { Context()->UnregisterActiveNode(this); } + + virtual size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const; + virtual size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const; + + // Returns a string from constant static storage identifying the dom node + // type. + virtual const char* NodeType() const = 0; + + const nsTArray<RefPtr<AudioParam>>& GetAudioParams() const { return mParams; } + + private: + // Given: + // + // - a DestinationType, that can be an AudioNode or an AudioParam ; + // - a Predicate, a function that takes an InputNode& and returns a bool ; + // + // This method iterates on the InputNodes() of the node at the index + // aDestinationIndex, and calls `DisconnectFromOutputIfConnected` with this + // input node, if aPredicate returns true. + template <typename DestinationType, typename Predicate> + bool DisconnectMatchingDestinationInputs(uint32_t aDestinationIndex, + Predicate aPredicate); + + virtual void LastRelease() override { + // We are about to be deleted, disconnect the object from the graph before + // the derived type is destroyed. + DisconnectFromGraph(); + } + // Callers must hold a reference to 'this'. + void DisconnectFromGraph(); + + template <typename DestinationType> + bool DisconnectFromOutputIfConnected(uint32_t aOutputIndex, + uint32_t aInputIndex); + + protected: + // Helper for the Constructors for nodes. + void Initialize(const AudioNodeOptions& aOptions, ErrorResult& aRv); + + // Helpers for sending different value types to tracks + void SendDoubleParameterToTrack(uint32_t aIndex, double aValue); + void SendInt32ParameterToTrack(uint32_t aIndex, int32_t aValue); + void SendChannelMixingParametersToTrack(); + + private: + RefPtr<AudioContext> mContext; + + protected: + // Set in the constructor of all nodes except offline AudioDestinationNode. + // Must not become null until finished. + RefPtr<AudioNodeTrack> mTrack; + + // The reference pointing out all audio params which belong to this node. + nsTArray<RefPtr<AudioParam>> mParams; + // Use this function to create an AudioParam, so as to automatically add + // the new AudioParam to `mParams`. + AudioParam* CreateAudioParam( + uint32_t aIndex, const nsAString& aName, float aDefaultValue, + float aMinValue = std::numeric_limits<float>::lowest(), + float aMaxValue = std::numeric_limits<float>::max()); + + private: + // For every InputNode, there is a corresponding entry in mOutputNodes of the + // InputNode's mInputNode. + nsTArray<InputNode> mInputNodes; + // For every mOutputNode entry, there is a corresponding entry in mInputNodes + // of the mOutputNode entry. We won't necessarily be able to identify the + // exact matching entry, since mOutputNodes doesn't include the port + // identifiers and the same node could be connected on multiple ports. + nsTArray<RefPtr<AudioNode>> mOutputNodes; + // For every mOutputParams entry, there is a corresponding entry in + // AudioParam::mInputNodes of the mOutputParams entry. We won't necessarily be + // able to identify the exact matching entry, since mOutputParams doesn't + // include the port identifiers and the same node could be connected on + // multiple ports. + nsTArray<RefPtr<AudioParam>> mOutputParams; + uint32_t mChannelCount; + ChannelCountMode mChannelCountMode; + ChannelInterpretation mChannelInterpretation; + const uint32_t mId; + // Whether the node just passes through its input. This is a devtools API + // that only works for some node types. + bool mPassThrough; +}; + +} // namespace dom +} // namespace mozilla + +#endif diff --git a/dom/media/webaudio/AudioNodeEngine.cpp b/dom/media/webaudio/AudioNodeEngine.cpp new file mode 100644 index 0000000000..3fba688b8c --- /dev/null +++ b/dom/media/webaudio/AudioNodeEngine.cpp @@ -0,0 +1,437 @@ +/* -*- 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 "AudioNodeEngine.h" + +#include "mozilla/AbstractThread.h" +#ifdef USE_NEON +# include "mozilla/arm.h" +# include "AudioNodeEngineGeneric.h" +#endif +#ifdef USE_SSE2 +# include "mozilla/SSE.h" +# include "AudioNodeEngineGeneric.h" +#endif +#if defined(USE_SSE42) && defined(USE_FMA3) +# include "mozilla/SSE.h" +# include "AudioNodeEngineGeneric.h" +#endif +#include "AudioBlock.h" +#include "Tracing.h" + +namespace mozilla { + +already_AddRefed<ThreadSharedFloatArrayBufferList> +ThreadSharedFloatArrayBufferList::Create(uint32_t aChannelCount, size_t aLength, + const mozilla::fallible_t&) { + RefPtr<ThreadSharedFloatArrayBufferList> buffer = + new ThreadSharedFloatArrayBufferList(aChannelCount); + + for (uint32_t i = 0; i < aChannelCount; ++i) { + float* channelData = js_pod_malloc<float>(aLength); + if (!channelData) { + return nullptr; + } + + buffer->SetData(i, channelData, js_free, channelData); + } + + return buffer.forget(); +} + +void WriteZeroesToAudioBlock(AudioBlock* aChunk, uint32_t aStart, + uint32_t aLength) { + MOZ_ASSERT(aStart + aLength <= WEBAUDIO_BLOCK_SIZE); + MOZ_ASSERT(!aChunk->IsNull(), "You should pass a non-null chunk"); + if (aLength == 0) { + return; + } + + for (uint32_t i = 0; i < aChunk->ChannelCount(); ++i) { + PodZero(aChunk->ChannelFloatsForWrite(i) + aStart, aLength); + } +} + +void AudioBufferCopyWithScale(const float* aInput, float aScale, float* aOutput, + uint32_t aSize) { + if (aScale == 1.0f) { + PodCopy(aOutput, aInput, aSize); + } else { + for (uint32_t i = 0; i < aSize; ++i) { + aOutput[i] = aInput[i] * aScale; + } + } +} + +void AudioBufferAddWithScale(const float* aInput, float aScale, float* aOutput, + uint32_t aSize) { +#ifdef USE_NEON + if (mozilla::supports_neon()) { + Engine<xsimd::neon>::AudioBufferAddWithScale(aInput, aScale, aOutput, + aSize); + return; + } +#endif + +#ifdef USE_SSE2 + if (mozilla::supports_sse2()) { +# if defined(USE_SSE42) && defined(USE_FMA3) + if (mozilla::supports_fma3() && mozilla::supports_sse4_2()) { + Engine<xsimd::fma3<xsimd::sse4_2>>::AudioBufferAddWithScale( + aInput, aScale, aOutput, aSize); + } else +# endif + { + Engine<xsimd::sse2>::AudioBufferAddWithScale(aInput, aScale, aOutput, + aSize); + } + return; + } +#endif + + if (aScale == 1.0f) { + for (uint32_t i = 0; i < aSize; ++i) { + aOutput[i] += aInput[i]; + } + } else { + for (uint32_t i = 0; i < aSize; ++i) { + aOutput[i] += aInput[i] * aScale; + } + } +} + +void AudioBlockAddChannelWithScale(const float aInput[WEBAUDIO_BLOCK_SIZE], + float aScale, + float aOutput[WEBAUDIO_BLOCK_SIZE]) { + AudioBufferAddWithScale(aInput, aScale, aOutput, WEBAUDIO_BLOCK_SIZE); +} + +void AudioBlockCopyChannelWithScale(const float* aInput, float aScale, + float* aOutput) { + if (aScale == 1.0f) { + memcpy(aOutput, aInput, WEBAUDIO_BLOCK_SIZE * sizeof(float)); + } else { +#ifdef USE_NEON + if (mozilla::supports_neon()) { + Engine<xsimd::neon>::AudioBlockCopyChannelWithScale(aInput, aScale, + aOutput); + return; + } +#endif + +#ifdef USE_SSE2 + if (mozilla::supports_sse2()) { + Engine<xsimd::sse2>::AudioBlockCopyChannelWithScale(aInput, aScale, + aOutput); + return; + } +#endif + + for (uint32_t i = 0; i < WEBAUDIO_BLOCK_SIZE; ++i) { + aOutput[i] = aInput[i] * aScale; + } + } +} + +void BufferComplexMultiply(const float* aInput, const float* aScale, + float* aOutput, uint32_t aSize) { +#ifdef USE_NEON + if (mozilla::supports_neon()) { + Engine<xsimd::neon>::BufferComplexMultiply(aInput, aScale, aOutput, aSize); + return; + } +#endif +#ifdef USE_SSE2 + if (mozilla::supports_sse()) { +# if defined(USE_SSE42) && defined(USE_FMA3) + if (mozilla::supports_fma3() && mozilla::supports_sse4_2()) { + Engine<xsimd::fma3<xsimd::sse4_2>>::BufferComplexMultiply(aInput, aScale, + aOutput, aSize); + } else +# endif + { + Engine<xsimd::sse2>::BufferComplexMultiply(aInput, aScale, aOutput, + aSize); + } + return; + } +#endif + + for (uint32_t i = 0; i < aSize * 2; i += 2) { + float real1 = aInput[i]; + float imag1 = aInput[i + 1]; + float real2 = aScale[i]; + float imag2 = aScale[i + 1]; + float realResult = real1 * real2 - imag1 * imag2; + float imagResult = real1 * imag2 + imag1 * real2; + aOutput[i] = realResult; + aOutput[i + 1] = imagResult; + } +} + +float AudioBufferPeakValue(const float* aInput, uint32_t aSize) { + float max = 0.0f; + for (uint32_t i = 0; i < aSize; i++) { + float mag = fabs(aInput[i]); + if (mag > max) { + max = mag; + } + } + return max; +} + +void AudioBlockCopyChannelWithScale(const float aInput[WEBAUDIO_BLOCK_SIZE], + const float aScale[WEBAUDIO_BLOCK_SIZE], + float aOutput[WEBAUDIO_BLOCK_SIZE]) { +#ifdef USE_NEON + if (mozilla::supports_neon()) { + Engine<xsimd::neon>::AudioBlockCopyChannelWithScale(aInput, aScale, + aOutput); + return; + } +#endif + +#ifdef USE_SSE2 + if (mozilla::supports_sse2()) { + Engine<xsimd::sse2>::AudioBlockCopyChannelWithScale(aInput, aScale, + aOutput); + return; + } +#endif + + for (uint32_t i = 0; i < WEBAUDIO_BLOCK_SIZE; ++i) { + aOutput[i] = aInput[i] * aScale[i]; + } +} + +void AudioBlockInPlaceScale(float aBlock[WEBAUDIO_BLOCK_SIZE], float aScale) { + AudioBufferInPlaceScale(aBlock, aScale, WEBAUDIO_BLOCK_SIZE); +} + +void AudioBlockInPlaceScale(float aBlock[WEBAUDIO_BLOCK_SIZE], + float aScale[WEBAUDIO_BLOCK_SIZE]) { + AudioBufferInPlaceScale(aBlock, aScale, WEBAUDIO_BLOCK_SIZE); +} + +void AudioBufferInPlaceScale(float* aBlock, float aScale, uint32_t aSize) { + if (aScale == 1.0f) { + return; + } +#ifdef USE_NEON + if (mozilla::supports_neon()) { + Engine<xsimd::neon>::AudioBufferInPlaceScale(aBlock, aScale, aSize); + return; + } +#endif + +#ifdef USE_SSE2 + if (mozilla::supports_sse2()) { + Engine<xsimd::sse2>::AudioBufferInPlaceScale(aBlock, aScale, aSize); + return; + } +#endif + + for (uint32_t i = 0; i < aSize; ++i) { + *aBlock++ *= aScale; + } +} + +void AudioBufferInPlaceScale(float* aBlock, float* aScale, uint32_t aSize) { +#ifdef USE_NEON + if (mozilla::supports_neon()) { + Engine<xsimd::neon>::AudioBufferInPlaceScale(aBlock, aScale, aSize); + return; + } +#endif + +#ifdef USE_SSE2 + if (mozilla::supports_sse2()) { + Engine<xsimd::sse2>::AudioBufferInPlaceScale(aBlock, aScale, aSize); + return; + } +#endif + + for (uint32_t i = 0; i < aSize; ++i) { + *aBlock++ *= *aScale++; + } +} + +void AudioBlockPanMonoToStereo(const float aInput[WEBAUDIO_BLOCK_SIZE], + float aGainL[WEBAUDIO_BLOCK_SIZE], + float aGainR[WEBAUDIO_BLOCK_SIZE], + float aOutputL[WEBAUDIO_BLOCK_SIZE], + float aOutputR[WEBAUDIO_BLOCK_SIZE]) { + AudioBlockCopyChannelWithScale(aInput, aGainL, aOutputL); + AudioBlockCopyChannelWithScale(aInput, aGainR, aOutputR); +} + +void AudioBlockPanMonoToStereo(const float aInput[WEBAUDIO_BLOCK_SIZE], + float aGainL, float aGainR, + float aOutputL[WEBAUDIO_BLOCK_SIZE], + float aOutputR[WEBAUDIO_BLOCK_SIZE]) { + AudioBlockCopyChannelWithScale(aInput, aGainL, aOutputL); + AudioBlockCopyChannelWithScale(aInput, aGainR, aOutputR); +} + +void AudioBlockPanStereoToStereo(const float aInputL[WEBAUDIO_BLOCK_SIZE], + const float aInputR[WEBAUDIO_BLOCK_SIZE], + float aGainL, float aGainR, bool aIsOnTheLeft, + float aOutputL[WEBAUDIO_BLOCK_SIZE], + float aOutputR[WEBAUDIO_BLOCK_SIZE]) { +#ifdef USE_NEON + if (mozilla::supports_neon()) { + Engine<xsimd::neon>::AudioBlockPanStereoToStereo( + aInputL, aInputR, aGainL, aGainR, aIsOnTheLeft, aOutputL, aOutputR); + return; + } +#endif + +#ifdef USE_SSE2 + if (mozilla::supports_sse2()) { +# if defined(USE_SSE42) && defined(USE_FMA3) + if (mozilla::supports_fma3() && mozilla::supports_sse4_2()) { + Engine<xsimd::fma3<xsimd::sse4_2>>::AudioBlockPanStereoToStereo( + aInputL, aInputR, aGainL, aGainR, aIsOnTheLeft, aOutputL, aOutputR); + } else +# endif + { + Engine<xsimd::sse2>::AudioBlockPanStereoToStereo( + aInputL, aInputR, aGainL, aGainR, aIsOnTheLeft, aOutputL, aOutputR); + } + return; + } +#endif + + uint32_t i; + + if (aIsOnTheLeft) { + for (i = 0; i < WEBAUDIO_BLOCK_SIZE; ++i) { + aOutputL[i] = aInputL[i] + aInputR[i] * aGainL; + aOutputR[i] = aInputR[i] * aGainR; + } + } else { + for (i = 0; i < WEBAUDIO_BLOCK_SIZE; ++i) { + aOutputL[i] = aInputL[i] * aGainL; + aOutputR[i] = aInputR[i] + aInputL[i] * aGainR; + } + } +} + +void AudioBlockPanStereoToStereo(const float aInputL[WEBAUDIO_BLOCK_SIZE], + const float aInputR[WEBAUDIO_BLOCK_SIZE], + const float aGainL[WEBAUDIO_BLOCK_SIZE], + const float aGainR[WEBAUDIO_BLOCK_SIZE], + const bool aIsOnTheLeft[WEBAUDIO_BLOCK_SIZE], + float aOutputL[WEBAUDIO_BLOCK_SIZE], + float aOutputR[WEBAUDIO_BLOCK_SIZE]) { +#ifdef USE_NEON + if (mozilla::supports_neon()) { + Engine<xsimd::neon>::AudioBlockPanStereoToStereo( + aInputL, aInputR, aGainL, aGainR, aIsOnTheLeft, aOutputL, aOutputR); + return; + } +#endif + +#ifdef USE_SSE2 + if (mozilla::supports_sse2()) { +# if defined(USE_SSE42) && defined(USE_FMA3) + if (mozilla::supports_fma3() && mozilla::supports_sse4_2()) { + Engine<xsimd::fma3<xsimd::sse2>>::AudioBlockPanStereoToStereo( + aInputL, aInputR, aGainL, aGainR, aIsOnTheLeft, aOutputL, aOutputR); + } else +# endif + { + Engine<xsimd::sse2>::AudioBlockPanStereoToStereo( + aInputL, aInputR, aGainL, aGainR, aIsOnTheLeft, aOutputL, aOutputR); + } + return; + } +#endif + + uint32_t i; + for (i = 0; i < WEBAUDIO_BLOCK_SIZE; i++) { + if (aIsOnTheLeft[i]) { + aOutputL[i] = aInputL[i] + aInputR[i] * aGainL[i]; + aOutputR[i] = aInputR[i] * aGainR[i]; + } else { + aOutputL[i] = aInputL[i] * aGainL[i]; + aOutputR[i] = aInputR[i] + aInputL[i] * aGainR[i]; + } + } +} + +float AudioBufferSumOfSquares(const float* aInput, uint32_t aLength) { +#ifdef USE_NEON + if (mozilla::supports_neon()) { + return Engine<xsimd::neon>::AudioBufferSumOfSquares(aInput, aLength); + } +#endif + +#ifdef USE_SSE2 + if (mozilla::supports_sse()) { +# if defined(USE_SSE42) && defined(USE_FMA3) + if (mozilla::supports_fma3() && mozilla::supports_sse4_2()) { + return Engine<xsimd::fma3<xsimd::sse4_2>>::AudioBufferSumOfSquares( + aInput, aLength); + } else +# endif + { + return Engine<xsimd::sse2>::AudioBufferSumOfSquares(aInput, aLength); + } + } +#endif + + float sum = 0.f; + while (aLength--) { + sum += *aInput * *aInput; + ++aInput; + } + return sum; +} + +void NaNToZeroInPlace(float* aSamples, size_t aCount) { +#ifdef USE_SSE2 + if (mozilla::supports_sse2()) { + Engine<xsimd::sse2>::NaNToZeroInPlace(aSamples, aCount); + return; + } +#endif + for (size_t i = 0; i < aCount; i++) { + if (aSamples[i] != aSamples[i]) { + aSamples[i] = 0.0; + } + } +} + +AudioNodeEngine::AudioNodeEngine(dom::AudioNode* aNode) + : mNode(aNode), + mNodeType(aNode ? aNode->NodeType() : nullptr), + mInputCount(aNode ? aNode->NumberOfInputs() : 1), + mOutputCount(aNode ? aNode->NumberOfOutputs() : 0) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_COUNT_CTOR(AudioNodeEngine); +} + +void AudioNodeEngine::ProcessBlock(AudioNodeTrack* aTrack, GraphTime aFrom, + const AudioBlock& aInput, + AudioBlock* aOutput, bool* aFinished) { + MOZ_ASSERT(mInputCount <= 1 && mOutputCount <= 1); + TRACE("AudioNodeEngine::ProcessBlock"); + *aOutput = aInput; +} + +void AudioNodeEngine::ProcessBlocksOnPorts(AudioNodeTrack* aTrack, + GraphTime aFrom, + Span<const AudioBlock> aInput, + Span<AudioBlock> aOutput, + bool* aFinished) { + MOZ_ASSERT(mInputCount > 1 || mOutputCount > 1); + TRACE("AudioNodeEngine::ProcessBlocksOnPorts"); + // Only produce one output port, and drop all other input ports. + aOutput[0] = aInput[0]; +} + +} // namespace mozilla diff --git a/dom/media/webaudio/AudioNodeEngine.h b/dom/media/webaudio/AudioNodeEngine.h new file mode 100644 index 0000000000..7a30e1b4cc --- /dev/null +++ b/dom/media/webaudio/AudioNodeEngine.h @@ -0,0 +1,389 @@ +/* -*- 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/. */ +#ifndef MOZILLA_AUDIONODEENGINE_H_ +#define MOZILLA_AUDIONODEENGINE_H_ + +#include "AudioSegment.h" +#include "mozilla/dom/AudioNode.h" +#include "mozilla/MemoryReporting.h" +#include "mozilla/Mutex.h" + +namespace WebCore { +class Reverb; +} // namespace WebCore + +namespace mozilla { + +namespace dom { +struct ThreeDPoint; +class AudioParamTimeline; +class DelayNodeEngine; +struct AudioParamEvent; +} // namespace dom + +class AbstractThread; +class AudioBlock; +class AudioNodeTrack; + +/** + * This class holds onto a set of immutable channel buffers. The storage + * for the buffers must be malloced, but the buffer pointers and the malloc + * pointers can be different (e.g. if the buffers are contained inside + * some malloced object). + */ +class ThreadSharedFloatArrayBufferList final : public ThreadSharedObject { + public: + /** + * Construct with null channel data pointers. + */ + explicit ThreadSharedFloatArrayBufferList(uint32_t aCount) { + mContents.SetLength(aCount); + } + /** + * Create with buffers suitable for transfer to + * JS::NewArrayBufferWithContents(). The buffer contents are uninitialized + * and so should be set using GetDataForWrite(). + */ + static already_AddRefed<ThreadSharedFloatArrayBufferList> Create( + uint32_t aChannelCount, size_t aLength, const mozilla::fallible_t&); + + ThreadSharedFloatArrayBufferList* AsThreadSharedFloatArrayBufferList() + override { + return this; + }; + + struct Storage final { + Storage() : mDataToFree(nullptr), mFree(nullptr), mSampleData(nullptr) {} + ~Storage() { + if (mFree) { + mFree(mDataToFree); + } else { + MOZ_ASSERT(!mDataToFree); + } + } + size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const { + // NB: mSampleData might not be owned, if it is it just points to + // mDataToFree. + return aMallocSizeOf(mDataToFree); + } + void* mDataToFree; + void (*mFree)(void*); + float* mSampleData; + }; + + /** + * This can be called on any thread. + */ + uint32_t GetChannels() const { return mContents.Length(); } + /** + * This can be called on any thread. + */ + const float* GetData(uint32_t aIndex) const { + return mContents[aIndex].mSampleData; + } + /** + * This can be called on any thread, but only when the calling thread is the + * only owner. + */ + float* GetDataForWrite(uint32_t aIndex) { + MOZ_ASSERT(!IsShared()); + return mContents[aIndex].mSampleData; + } + + /** + * Call this only during initialization, before the object is handed to + * any other thread. + */ + void SetData(uint32_t aIndex, void* aDataToFree, void (*aFreeFunc)(void*), + float* aData) { + Storage* s = &mContents[aIndex]; + if (s->mFree) { + s->mFree(s->mDataToFree); + } else { + MOZ_ASSERT(!s->mDataToFree); + } + + s->mDataToFree = aDataToFree; + s->mFree = aFreeFunc; + s->mSampleData = aData; + } + + /** + * Put this object into an error state where there are no channels. + */ + void Clear() { mContents.Clear(); } + + size_t SizeOfExcludingThis( + mozilla::MallocSizeOf aMallocSizeOf) const override { + size_t amount = ThreadSharedObject::SizeOfExcludingThis(aMallocSizeOf); + amount += mContents.ShallowSizeOfExcludingThis(aMallocSizeOf); + for (size_t i = 0; i < mContents.Length(); i++) { + amount += mContents[i].SizeOfExcludingThis(aMallocSizeOf); + } + + return amount; + } + + size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override { + return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf); + } + + private: + AutoTArray<Storage, 2> mContents; +}; + +/** + * aChunk must have been allocated by AllocateAudioBlock. + */ +void WriteZeroesToAudioBlock(AudioBlock* aChunk, uint32_t aStart, + uint32_t aLength); + +/** + * Copy with scale. aScale == 1.0f should be optimized. + */ +void AudioBufferCopyWithScale(const float* aInput, float aScale, float* aOutput, + uint32_t aSize); + +/** + * Pointwise multiply-add operation. aScale == 1.0f should be optimized. + */ +void AudioBufferAddWithScale(const float* aInput, float aScale, float* aOutput, + uint32_t aSize); + +/** + * Pointwise multiply-add operation. aScale == 1.0f should be optimized. + */ +void AudioBlockAddChannelWithScale(const float aInput[WEBAUDIO_BLOCK_SIZE], + float aScale, + float aOutput[WEBAUDIO_BLOCK_SIZE]); + +/** + * Pointwise copy-scaled operation. aScale == 1.0f should be optimized. + * + * Buffer size is implicitly assumed to be WEBAUDIO_BLOCK_SIZE. + */ +void AudioBlockCopyChannelWithScale(const float* aInput, float aScale, + float* aOutput); + +/** + * Vector copy-scaled operation. + */ +void AudioBlockCopyChannelWithScale(const float aInput[WEBAUDIO_BLOCK_SIZE], + const float aScale[WEBAUDIO_BLOCK_SIZE], + float aOutput[WEBAUDIO_BLOCK_SIZE]); + +/** + * Vector complex multiplication on arbitrary sized buffers. + */ +void BufferComplexMultiply(const float* aInput, const float* aScale, + float* aOutput, uint32_t aSize); + +/** + * Vector maximum element magnitude ( max(abs(aInput)) ). + */ +float AudioBufferPeakValue(const float* aInput, uint32_t aSize); + +/** + * In place gain. aScale == 1.0f should be optimized. + */ +void AudioBlockInPlaceScale(float aBlock[WEBAUDIO_BLOCK_SIZE], float aScale); + +/** + * In place gain. aScale == 1.0f should be optimized. + */ +void AudioBufferInPlaceScale(float* aBlock, float aScale, uint32_t aSize); + +/** + * a-rate in place gain. + */ +void AudioBlockInPlaceScale(float aBlock[WEBAUDIO_BLOCK_SIZE], + float aScale[WEBAUDIO_BLOCK_SIZE]); +/** + * a-rate in place gain. + */ +void AudioBufferInPlaceScale(float* aBlock, float* aScale, uint32_t aSize); + +/** + * Upmix a mono input to a stereo output, scaling the two output channels by two + * different gain value. + * This algorithm is specified in the WebAudio spec. + */ +void AudioBlockPanMonoToStereo(const float aInput[WEBAUDIO_BLOCK_SIZE], + float aGainL, float aGainR, + float aOutputL[WEBAUDIO_BLOCK_SIZE], + float aOutputR[WEBAUDIO_BLOCK_SIZE]); + +void AudioBlockPanMonoToStereo(const float aInput[WEBAUDIO_BLOCK_SIZE], + float aGainL[WEBAUDIO_BLOCK_SIZE], + float aGainR[WEBAUDIO_BLOCK_SIZE], + float aOutputL[WEBAUDIO_BLOCK_SIZE], + float aOutputR[WEBAUDIO_BLOCK_SIZE]); +/** + * Pan a stereo source according to right and left gain, and the position + * (whether the listener is on the left of the source or not). + * This algorithm is specified in the WebAudio spec. + */ +void AudioBlockPanStereoToStereo(const float aInputL[WEBAUDIO_BLOCK_SIZE], + const float aInputR[WEBAUDIO_BLOCK_SIZE], + float aGainL, float aGainR, bool aIsOnTheLeft, + float aOutputL[WEBAUDIO_BLOCK_SIZE], + float aOutputR[WEBAUDIO_BLOCK_SIZE]); +void AudioBlockPanStereoToStereo(const float aInputL[WEBAUDIO_BLOCK_SIZE], + const float aInputR[WEBAUDIO_BLOCK_SIZE], + const float aGainL[WEBAUDIO_BLOCK_SIZE], + const float aGainR[WEBAUDIO_BLOCK_SIZE], + const bool aIsOnTheLeft[WEBAUDIO_BLOCK_SIZE], + float aOutputL[WEBAUDIO_BLOCK_SIZE], + float aOutputR[WEBAUDIO_BLOCK_SIZE]); + +/** + * Replace NaN by zeros in aSamples. + */ +void NaNToZeroInPlace(float* aSamples, size_t aCount); + +/** + * Return the sum of squares of all of the samples in the input. + */ +float AudioBufferSumOfSquares(const float* aInput, uint32_t aLength); + +/** + * All methods of this class and its subclasses are called on the + * MediaTrackGraph thread. + */ +class AudioNodeEngine { + public: + // This should be compatible with AudioNodeTrack::OutputChunks. + typedef AutoTArray<AudioBlock, 1> OutputChunks; + + explicit AudioNodeEngine(dom::AudioNode* aNode); + + virtual ~AudioNodeEngine() { + MOZ_ASSERT(!mNode, "The node reference must be already cleared"); + MOZ_COUNT_DTOR(AudioNodeEngine); + } + + virtual dom::DelayNodeEngine* AsDelayNodeEngine() { return nullptr; } + + virtual void SetTrackTimeParameter(uint32_t aIndex, TrackTime aParam) { + NS_ERROR("Invalid SetTrackTimeParameter index"); + } + virtual void SetDoubleParameter(uint32_t aIndex, double aParam) { + NS_ERROR("Invalid SetDoubleParameter index"); + } + virtual void SetInt32Parameter(uint32_t aIndex, int32_t aParam) { + NS_ERROR("Invalid SetInt32Parameter index"); + } + virtual void RecvTimelineEvent(uint32_t aIndex, + dom::AudioParamEvent& aValue) { + NS_ERROR("Invalid RecvTimelineEvent index"); + } + virtual void SetBuffer(AudioChunk&& aBuffer) { + NS_ERROR("SetBuffer called on engine that doesn't support it"); + } + // This consumes the contents of aData. aData will be emptied after this + // returns. + virtual void SetRawArrayData(nsTArray<float>&& aData) { + NS_ERROR("SetRawArrayData called on an engine that doesn't support it"); + } + + virtual void SetReverb(WebCore::Reverb* aBuffer, + uint32_t aImpulseChannelCount) { + NS_ERROR("SetReverb called on engine that doesn't support it"); + } + + /** + * Produce the next block of audio samples, given input samples aInput + * (the mixed data for input 0). + * aInput is guaranteed to have float sample format (if it has samples at all) + * and to have been resampled to the sampling rate for the track, and to have + * exactly WEBAUDIO_BLOCK_SIZE samples. + * *aFinished is set to false by the caller. The callee must not set this to + * true unless silent output is produced. If set to true, we'll finish the + * track, consider this input inactive on any downstream nodes, and not + * call this again. + */ + virtual void ProcessBlock(AudioNodeTrack* aTrack, GraphTime aFrom, + const AudioBlock& aInput, AudioBlock* aOutput, + bool* aFinished); + /** + * Produce the next block of audio samples, before input is provided. + * ProcessBlock() will be called later, and it then should not change + * aOutput. This is used only for DelayNodeEngine in a feedback loop. + */ + virtual void ProduceBlockBeforeInput(AudioNodeTrack* aTrack, GraphTime aFrom, + AudioBlock* aOutput) { + MOZ_ASSERT_UNREACHABLE("ProduceBlockBeforeInput called on wrong engine"); + } + + /** + * Produce the next block of audio samples, given input samples in the aInput + * array. There is one input sample per port in aInput, in order. + * This is the multi-input/output version of ProcessBlock. Only one kind + * of ProcessBlock is called on each node. ProcessBlocksOnPorts() is called + * instead of ProcessBlock() if either the number of inputs or the number of + * outputs is greater than 1. + * + * The numbers of AudioBlocks in aInput and aOutput are always guaranteed to + * match the numbers of inputs and outputs for the node. + */ + virtual void ProcessBlocksOnPorts(AudioNodeTrack* aTrack, GraphTime aFrom, + Span<const AudioBlock> aInput, + Span<AudioBlock> aOutput, bool* aFinished); + + // IsActive() returns true if the engine needs to continue processing an + // unfinished track even when it has silent or no input connections. This + // includes tail-times and when sources have been scheduled to start. If + // returning false, then the track can be suspended. + virtual bool IsActive() const { return false; } + + // Called on graph thread when the engine will not be used again. + virtual void OnGraphThreadDone() {} + + bool HasNode() const { + MOZ_ASSERT(NS_IsMainThread()); + return !!mNode; + } + + dom::AudioNode* NodeMainThread() const { + MOZ_ASSERT(NS_IsMainThread()); + return mNode; + } + + void ClearNode() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mNode != nullptr); + mNode = nullptr; + } + + uint16_t InputCount() const { return mInputCount; } + uint16_t OutputCount() const { return mOutputCount; } + + virtual size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const { + // NB: |mNode| is tracked separately so it is excluded here. + return 0; + } + + virtual size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const { + return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf); + } + + void SizeOfIncludingThis(MallocSizeOf aMallocSizeOf, + AudioNodeSizes& aUsage) const { + aUsage.mEngine = SizeOfIncludingThis(aMallocSizeOf); + aUsage.mNodeType = mNodeType; + } + + private: + // This is cleared from AudioNode::DestroyMediaTrack() + dom::AudioNode* MOZ_NON_OWNING_REF mNode; // main thread only + const char* const mNodeType; + const uint16_t mInputCount; + const uint16_t mOutputCount; +}; + +} // namespace mozilla + +#endif /* MOZILLA_AUDIONODEENGINE_H_ */ diff --git a/dom/media/webaudio/AudioNodeEngineGeneric.h b/dom/media/webaudio/AudioNodeEngineGeneric.h new file mode 100644 index 0000000000..de436ebc8d --- /dev/null +++ b/dom/media/webaudio/AudioNodeEngineGeneric.h @@ -0,0 +1,58 @@ +/* -*- mode: c++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef MOZILLA_AUDIONODEENGINEGENERIC_H_ +#define MOZILLA_AUDIONODEENGINEGENERIC_H_ + +#include "AudioNodeEngine.h" + +#include "xsimd/xsimd.hpp" + +namespace mozilla { + +template <class Arch> +struct Engine { + static void AudioBufferAddWithScale(const float* aInput, float aScale, + float* aOutput, uint32_t aSize); + + static void AudioBlockCopyChannelWithScale(const float* aInput, float aScale, + float* aOutput); + + static void AudioBlockCopyChannelWithScale( + const float aInput[WEBAUDIO_BLOCK_SIZE], + const float aScale[WEBAUDIO_BLOCK_SIZE], + float aOutput[WEBAUDIO_BLOCK_SIZE]); + + static void AudioBufferInPlaceScale(float* aBlock, float aScale, + uint32_t aSize); + + static void AudioBufferInPlaceScale(float* aBlock, float* aScale, + uint32_t aSize); + + static void AudioBlockPanStereoToStereo( + const float aInputL[WEBAUDIO_BLOCK_SIZE], + const float aInputR[WEBAUDIO_BLOCK_SIZE], float aGainL, float aGainR, + bool aIsOnTheLeft, float aOutputL[WEBAUDIO_BLOCK_SIZE], + float aOutputR[WEBAUDIO_BLOCK_SIZE]); + + static void BufferComplexMultiply(const float* aInput, const float* aScale, + float* aOutput, uint32_t aSize); + + static float AudioBufferSumOfSquares(const float* aInput, uint32_t aLength); + + static void NaNToZeroInPlace(float* aSamples, size_t aCount); + + static void AudioBlockPanStereoToStereo( + const float aInputL[WEBAUDIO_BLOCK_SIZE], + const float aInputR[WEBAUDIO_BLOCK_SIZE], + const float aGainL[WEBAUDIO_BLOCK_SIZE], + const float aGainR[WEBAUDIO_BLOCK_SIZE], + const bool aIsOnTheLeft[WEBAUDIO_BLOCK_SIZE], + float aOutputL[WEBAUDIO_BLOCK_SIZE], float aOutputR[WEBAUDIO_BLOCK_SIZE]); +}; + +} // namespace mozilla + +#endif diff --git a/dom/media/webaudio/AudioNodeEngineGenericImpl.h b/dom/media/webaudio/AudioNodeEngineGenericImpl.h new file mode 100644 index 0000000000..b7fabc7a0b --- /dev/null +++ b/dom/media/webaudio/AudioNodeEngineGenericImpl.h @@ -0,0 +1,341 @@ +/* -*- mode: c++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#ifndef MOZILLA_AUDIONODEENGINEGENERICIMPL_H_ +#define MOZILLA_AUDIONODEENGINEGENERICIMPL_H_ + +#include "AudioNodeEngineGeneric.h" +#include "AlignmentUtils.h" + +#if defined(__GNUC__) && __GNUC__ > 7 +# define MOZ_PRAGMA(tokens) _Pragma(#tokens) +# define MOZ_UNROLL(factor) MOZ_PRAGMA(GCC unroll factor) +#elif defined(__INTEL_COMPILER) || (defined(__clang__) && __clang_major__ > 3) +# define MOZ_PRAGMA(tokens) _Pragma(#tokens) +# define MOZ_UNROLL(factor) MOZ_PRAGMA(unroll factor) +#else +# define MOZ_UNROLL(_) +#endif + +namespace mozilla { + +template <class Arch> +static bool is_aligned(const void* ptr) { + return (reinterpret_cast<uintptr_t>(ptr) & + ~(static_cast<uintptr_t>(Arch::alignment()) - 1)) == + reinterpret_cast<uintptr_t>(ptr); +}; + +template <class Arch> +void Engine<Arch>::AudioBufferAddWithScale(const float* aInput, float aScale, + float* aOutput, uint32_t aSize) { + if constexpr (Arch::requires_alignment()) { + if (aScale == 1.0f) { + while (!is_aligned<Arch>(aInput) || !is_aligned<Arch>(aOutput)) { + if (!aSize) return; + *aOutput += *aInput; + ++aOutput; + ++aInput; + --aSize; + } + } else { + while (!is_aligned<Arch>(aInput) || !is_aligned<Arch>(aOutput)) { + if (!aSize) return; + *aOutput += *aInput * aScale; + ++aOutput; + ++aInput; + --aSize; + } + } + } + MOZ_ASSERT(is_aligned<Arch>(aInput), "aInput is aligned"); + MOZ_ASSERT(is_aligned<Arch>(aOutput), "aOutput is aligned"); + + xsimd::batch<float, Arch> vgain(aScale); + + uint32_t aVSize = aSize & ~(xsimd::batch<float, Arch>::size - 1); + MOZ_UNROLL(4) + for (unsigned i = 0; i < aVSize; i += xsimd::batch<float, Arch>::size) { + auto vin1 = xsimd::batch<float, Arch>::load_aligned(&aInput[i]); + auto vin2 = xsimd::batch<float, Arch>::load_aligned(&aOutput[i]); + auto vout = xsimd::fma(vin1, vgain, vin2); + vout.store_aligned(&aOutput[i]); + } + + for (unsigned i = aVSize; i < aSize; ++i) { + aOutput[i] += aInput[i] * aScale; + } +} + +template <class Arch> +void Engine<Arch>::AudioBlockCopyChannelWithScale(const float* aInput, + float aScale, + float* aOutput) { + MOZ_ASSERT(is_aligned<Arch>(aInput), "aInput is aligned"); + MOZ_ASSERT(is_aligned<Arch>(aOutput), "aOutput is aligned"); + + MOZ_ASSERT((WEBAUDIO_BLOCK_SIZE % xsimd::batch<float, Arch>::size == 0), + "requires tail processing"); + + xsimd::batch<float, Arch> vgain = (aScale); + + MOZ_UNROLL(4) + for (unsigned i = 0; i < WEBAUDIO_BLOCK_SIZE; + i += xsimd::batch<float, Arch>::size) { + auto vin = xsimd::batch<float, Arch>::load_aligned(&aInput[i]); + auto vout = vin * vgain; + vout.store_aligned(&aOutput[i]); + } +}; + +template <class Arch> +void Engine<Arch>::AudioBlockCopyChannelWithScale( + const float aInput[WEBAUDIO_BLOCK_SIZE], + const float aScale[WEBAUDIO_BLOCK_SIZE], + float aOutput[WEBAUDIO_BLOCK_SIZE]) { + MOZ_ASSERT(is_aligned<Arch>(aInput), "aInput is aligned"); + MOZ_ASSERT(is_aligned<Arch>(aOutput), "aOutput is aligned"); + MOZ_ASSERT(is_aligned<Arch>(aScale), "aScale is aligned"); + + MOZ_ASSERT((WEBAUDIO_BLOCK_SIZE % xsimd::batch<float, Arch>::size == 0), + "requires tail processing"); + + MOZ_UNROLL(4) + for (unsigned i = 0; i < WEBAUDIO_BLOCK_SIZE; + i += xsimd::batch<float, Arch>::size) { + auto vscaled = xsimd::batch<float, Arch>::load_aligned(&aScale[i]); + auto vin = xsimd::batch<float, Arch>::load_aligned(&aInput[i]); + auto vout = vin * vscaled; + vout.store_aligned(&aOutput[i]); + } +}; + +template <class Arch> +void Engine<Arch>::AudioBufferInPlaceScale(float* aBlock, float aScale, + uint32_t aSize) { + MOZ_ASSERT(is_aligned<Arch>(aBlock), "aBlock is aligned"); + + xsimd::batch<float, Arch> vgain(aScale); + + uint32_t aVSize = aSize & ~(xsimd::batch<float, Arch>::size - 1); + MOZ_UNROLL(4) + for (unsigned i = 0; i < aVSize; i += xsimd::batch<float, Arch>::size) { + auto vin = xsimd::batch<float, Arch>::load_aligned(&aBlock[i]); + auto vout = vin * vgain; + vout.store_aligned(&aBlock[i]); + } + for (unsigned i = aVSize; i < aSize; ++i) aBlock[i] *= aScale; +}; + +template <class Arch> +void Engine<Arch>::AudioBufferInPlaceScale(float* aBlock, float* aScale, + uint32_t aSize) { + MOZ_ASSERT(is_aligned<Arch>(aBlock), "aBlock is aligned"); + MOZ_ASSERT(is_aligned<Arch>(aScale), "aScale is aligned"); + + uint32_t aVSize = aSize & ~(xsimd::batch<float, Arch>::size - 1); + MOZ_UNROLL(4) + for (unsigned i = 0; i < aVSize; i += xsimd::batch<float, Arch>::size) { + auto vin = xsimd::batch<float, Arch>::load_aligned(&aBlock[i]); + auto vgain = xsimd::batch<float, Arch>::load_aligned(&aScale[i]); + auto vout = vin * vgain; + vout.store_aligned(&aBlock[i]); + } + for (uint32_t i = aVSize; i < aSize; ++i) { + *aBlock++ *= *aScale++; + } +}; + +template <class Arch> +void Engine<Arch>::AudioBlockPanStereoToStereo( + const float aInputL[WEBAUDIO_BLOCK_SIZE], + const float aInputR[WEBAUDIO_BLOCK_SIZE], float aGainL, float aGainR, + bool aIsOnTheLeft, float aOutputL[WEBAUDIO_BLOCK_SIZE], + float aOutputR[WEBAUDIO_BLOCK_SIZE]) { + MOZ_ASSERT(is_aligned<Arch>(aInputL), "aInputL is aligned"); + MOZ_ASSERT(is_aligned<Arch>(aInputR), "aInputR is aligned"); + MOZ_ASSERT(is_aligned<Arch>(aOutputL), "aOutputL is aligned"); + MOZ_ASSERT(is_aligned<Arch>(aOutputR), "aOutputR is aligned"); + + MOZ_ASSERT((WEBAUDIO_BLOCK_SIZE % xsimd::batch<float, Arch>::size == 0), + "requires tail processing"); + + xsimd::batch<float, Arch> vgainl(aGainL); + xsimd::batch<float, Arch> vgainr(aGainR); + + if (aIsOnTheLeft) { + MOZ_UNROLL(2) + for (unsigned i = 0; i < WEBAUDIO_BLOCK_SIZE; + i += xsimd::batch<float, Arch>::size) { + auto vinl = xsimd::batch<float, Arch>::load_aligned(&aInputL[i]); + auto vinr = xsimd::batch<float, Arch>::load_aligned(&aInputR[i]); + + /* left channel : aOutputL = aInputL + aInputR * gainL */ + auto vout = xsimd::fma(vinr, vgainl, vinl); + vout.store_aligned(&aOutputL[i]); + + /* right channel : aOutputR = aInputR * gainR */ + auto vscaled = vinr * vgainr; + vscaled.store_aligned(&aOutputR[i]); + } + } else { + MOZ_UNROLL(2) + for (unsigned i = 0; i < WEBAUDIO_BLOCK_SIZE; + i += xsimd::batch<float, Arch>::size) { + auto vinl = xsimd::batch<float, Arch>::load_aligned(&aInputL[i]); + auto vinr = xsimd::batch<float, Arch>::load_aligned(&aInputR[i]); + + /* left channel : aInputL * gainL */ + auto vscaled = vinl * vgainl; + vscaled.store_aligned(&aOutputL[i]); + + /* right channel: aOutputR = aInputR + aInputL * gainR */ + auto vout = xsimd::fma(vinl, vgainr, vinr); + vout.store_aligned(&aOutputR[i]); + } + } +}; + +template <class Arch> +void Engine<Arch>::BufferComplexMultiply(const float* aInput, + const float* aScale, float* aOutput, + uint32_t aSize) { + MOZ_ASSERT(is_aligned<Arch>(aInput), "aInput is aligned"); + MOZ_ASSERT(is_aligned<Arch>(aOutput), "aOutput is aligned"); + MOZ_ASSERT(is_aligned<Arch>(aScale), "aScale is aligned"); + MOZ_ASSERT((aSize % xsimd::batch<float, Arch>::size == 0), + "requires tail processing"); + + MOZ_UNROLL(2) + for (unsigned i = 0; i < aSize * 2; + i += 2 * xsimd::batch<std::complex<float>, Arch>::size) { + auto in1 = xsimd::batch<std::complex<float>, Arch>::load_aligned( + reinterpret_cast<const std::complex<float>*>(&aInput[i])); + auto in2 = xsimd::batch<std::complex<float>, Arch>::load_aligned( + reinterpret_cast<const std::complex<float>*>(&aScale[i])); + auto out = in1 * in2; + out.store_aligned(reinterpret_cast<std::complex<float>*>(&aOutput[i])); + } +}; + +template <class Arch> +float Engine<Arch>::AudioBufferSumOfSquares(const float* aInput, + uint32_t aLength) { + float sum = 0.f; + + if constexpr (Arch::requires_alignment()) { + while (!is_aligned<Arch>(aInput)) { + if (!aLength) { + return sum; + } + sum += *aInput * *aInput; + ++aInput; + --aLength; + } + } + + MOZ_ASSERT(is_aligned<Arch>(aInput), "aInput is aligned"); + + constexpr uint32_t unroll_factor = 4; + xsimd::batch<float, Arch> accs[unroll_factor] = {0.f, 0.f, 0.f, 0.f}; + + uint32_t vLength = + aLength & ~(unroll_factor * xsimd::batch<float, Arch>::size - 1); + + for (uint32_t i = 0; i < vLength; + i += unroll_factor * xsimd::batch<float, Arch>::size) { + MOZ_UNROLL(4) + for (uint32_t j = 0; j < unroll_factor; ++j) { + auto in = xsimd::batch<float, Arch>::load_aligned( + &aInput[i + xsimd::batch<float, Arch>::size * j]); + accs[j] = xsimd::fma(in, in, accs[j]); + } + } + + sum += reduce_add((accs[0] + accs[1]) + (accs[2] + accs[3])); + for (uint32_t i = vLength; i < aLength; ++i) sum += aInput[i] * aInput[i]; + return sum; +}; + +template <class Arch> +void Engine<Arch>::NaNToZeroInPlace(float* aSamples, size_t aCount) { + if constexpr (Arch::requires_alignment()) { + while (!is_aligned<Arch>(aSamples)) { + if (!aCount) { + return; + } + if (*aSamples != *aSamples) { + *aSamples = 0.0; + } + ++aSamples; + --aCount; + } + } + + MOZ_ASSERT(is_aligned<Arch>(aSamples), "aSamples is aligned"); + + uint32_t vCount = aCount & ~(xsimd::batch<float, Arch>::size - 1); + + MOZ_UNROLL(4) + for (uint32_t i = 0; i < vCount; i += xsimd::batch<float, Arch>::size) { + auto vin = xsimd::batch<float, Arch>::load_aligned(&aSamples[i]); + auto vout = + xsimd::select(xsimd::isnan(vin), xsimd::batch<float, Arch>(0.f), vin); + vout.store_aligned(&aSamples[i]); + } + + for (uint32_t i = vCount; i < aCount; i++) { + if (aSamples[i] != aSamples[i]) { + aSamples[i] = 0.0; + } + } +}; + +template <class Arch> +void Engine<Arch>::AudioBlockPanStereoToStereo( + const float aInputL[WEBAUDIO_BLOCK_SIZE], + const float aInputR[WEBAUDIO_BLOCK_SIZE], + const float aGainL[WEBAUDIO_BLOCK_SIZE], + const float aGainR[WEBAUDIO_BLOCK_SIZE], + const bool aIsOnTheLeft[WEBAUDIO_BLOCK_SIZE], + float aOutputL[WEBAUDIO_BLOCK_SIZE], float aOutputR[WEBAUDIO_BLOCK_SIZE]) { + MOZ_ASSERT(is_aligned<Arch>(aInputL), "aInputL is aligned"); + MOZ_ASSERT(is_aligned<Arch>(aInputR), "aInputR is aligned"); + MOZ_ASSERT(is_aligned<Arch>(aGainL), "aGainL is aligned"); + MOZ_ASSERT(is_aligned<Arch>(aGainR), "aGainR is aligned"); + MOZ_ASSERT(is_aligned<Arch>(aIsOnTheLeft), "aIsOnTheLeft is aligned"); + MOZ_ASSERT(is_aligned<Arch>(aOutputL), "aOutputL is aligned"); + MOZ_ASSERT(is_aligned<Arch>(aOutputR), "aOutputR is aligned"); + + MOZ_ASSERT((WEBAUDIO_BLOCK_SIZE % xsimd::batch<float, Arch>::size == 0), + "requires tail processing"); + + MOZ_UNROLL(2) + for (uint32_t i = 0; i < WEBAUDIO_BLOCK_SIZE; + i += xsimd::batch<float, Arch>::size) { + auto mask = xsimd::batch_bool<float, Arch>::load_aligned(&aIsOnTheLeft[i]); + + auto inputL = xsimd::batch<float, Arch>::load_aligned(&aInputL[i]); + auto inputR = xsimd::batch<float, Arch>::load_aligned(&aInputR[i]); + auto gainL = xsimd::batch<float, Arch>::load_aligned(&aGainL[i]); + auto gainR = xsimd::batch<float, Arch>::load_aligned(&aGainR[i]); + + auto outL_true = xsimd::fma(inputR, gainL, inputL); + auto outR_true = inputR * gainR; + + auto outL_false = inputL * gainL; + auto outR_false = xsimd::fma(inputL, gainR, inputR); + + auto outL = xsimd::select(mask, outL_true, outL_false); + auto outR = xsimd::select(mask, outR_true, outR_false); + + outL.store_aligned(&aOutputL[i]); + outR.store_aligned(&aOutputR[i]); + } +} + +} // namespace mozilla + +#endif diff --git a/dom/media/webaudio/AudioNodeEngineNEON.cpp b/dom/media/webaudio/AudioNodeEngineNEON.cpp new file mode 100644 index 0000000000..93f5c85285 --- /dev/null +++ b/dom/media/webaudio/AudioNodeEngineNEON.cpp @@ -0,0 +1,9 @@ +/* -*- mode: c++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 "AudioNodeEngineGenericImpl.h" +namespace mozilla { +template struct Engine<xsimd::neon>; +} // namespace mozilla diff --git a/dom/media/webaudio/AudioNodeEngineSSE2.cpp b/dom/media/webaudio/AudioNodeEngineSSE2.cpp new file mode 100644 index 0000000000..558cd866fd --- /dev/null +++ b/dom/media/webaudio/AudioNodeEngineSSE2.cpp @@ -0,0 +1,10 @@ +/* -*- mode: c++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 "AudioNodeEngineGenericImpl.h" + +namespace mozilla { +template struct Engine<xsimd::sse2>; +} // namespace mozilla diff --git a/dom/media/webaudio/AudioNodeEngineSSE4_2_FMA3.cpp b/dom/media/webaudio/AudioNodeEngineSSE4_2_FMA3.cpp new file mode 100644 index 0000000000..ce0134ee4b --- /dev/null +++ b/dom/media/webaudio/AudioNodeEngineSSE4_2_FMA3.cpp @@ -0,0 +1,10 @@ +/* -*- mode: c++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 "AudioNodeEngineGenericImpl.h" + +namespace mozilla { +template struct Engine<xsimd::fma3<xsimd::sse4_2>>; +} // namespace mozilla diff --git a/dom/media/webaudio/AudioNodeExternalInputTrack.cpp b/dom/media/webaudio/AudioNodeExternalInputTrack.cpp new file mode 100644 index 0000000000..8867a2dbf2 --- /dev/null +++ b/dom/media/webaudio/AudioNodeExternalInputTrack.cpp @@ -0,0 +1,225 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/ +/* 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 "AlignedTArray.h" +#include "AlignmentUtils.h" +#include "AudioNodeEngine.h" +#include "AudioNodeExternalInputTrack.h" +#include "AudioChannelFormat.h" +#include "mozilla/dom/MediaStreamAudioSourceNode.h" + +using namespace mozilla::dom; + +namespace mozilla { + +AudioNodeExternalInputTrack::AudioNodeExternalInputTrack( + AudioNodeEngine* aEngine, TrackRate aSampleRate) + : AudioNodeTrack(aEngine, NO_TRACK_FLAGS, aSampleRate) { + MOZ_COUNT_CTOR(AudioNodeExternalInputTrack); +} + +AudioNodeExternalInputTrack::~AudioNodeExternalInputTrack() { + MOZ_COUNT_DTOR(AudioNodeExternalInputTrack); +} + +/* static */ +already_AddRefed<AudioNodeExternalInputTrack> +AudioNodeExternalInputTrack::Create(MediaTrackGraph* aGraph, + AudioNodeEngine* aEngine) { + AudioContext* ctx = aEngine->NodeMainThread()->Context(); + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aGraph == ctx->Graph()); + + RefPtr<AudioNodeExternalInputTrack> track = + new AudioNodeExternalInputTrack(aEngine, aGraph->GraphRate()); + track->mSuspendedCount += ctx->ShouldSuspendNewTrack(); + aGraph->AddTrack(track); + return track.forget(); +} + +/** + * Copies the data in aInput to aOffsetInBlock within aBlock. + * aBlock must have been allocated with AllocateInputBlock and have a channel + * count that's a superset of the channels in aInput. + */ +template <typename T> +static void CopyChunkToBlock(AudioChunk& aInput, AudioBlock* aBlock, + uint32_t aOffsetInBlock) { + uint32_t blockChannels = aBlock->ChannelCount(); + AutoTArray<const T*, 2> channels; + if (aInput.IsNull()) { + channels.SetLength(blockChannels); + PodZero(channels.Elements(), blockChannels); + } else { + Span inputChannels = aInput.ChannelData<T>(); + channels.SetLength(inputChannels.Length()); + PodCopy(channels.Elements(), inputChannels.Elements(), channels.Length()); + if (channels.Length() != blockChannels) { + // We only need to upmix here because aBlock's channel count has been + // chosen to be a superset of the channel count of every chunk. + AudioChannelsUpMix(&channels, blockChannels, static_cast<T*>(nullptr)); + } + } + + for (uint32_t c = 0; c < blockChannels; ++c) { + float* outputData = aBlock->ChannelFloatsForWrite(c) + aOffsetInBlock; + if (channels[c]) { + ConvertAudioSamplesWithScale(channels[c], outputData, + aInput.GetDuration(), aInput.mVolume); + } else { + PodZero(outputData, aInput.GetDuration()); + } + } +} + +/** + * Converts the data in aSegment to a single chunk aBlock. aSegment must have + * duration WEBAUDIO_BLOCK_SIZE. aFallbackChannelCount is a superset of the + * channels in every chunk of aSegment. aBlock must be float format or null. + */ +static void ConvertSegmentToAudioBlock(AudioSegment* aSegment, + AudioBlock* aBlock, + int32_t aFallbackChannelCount) { + NS_ASSERTION(aSegment->GetDuration() == WEBAUDIO_BLOCK_SIZE, + "Bad segment duration"); + + { + AudioSegment::ChunkIterator ci(*aSegment); + NS_ASSERTION(!ci.IsEnded(), "Should be at least one chunk!"); + if (ci->GetDuration() == WEBAUDIO_BLOCK_SIZE && + (ci->IsNull() || ci->mBufferFormat == AUDIO_FORMAT_FLOAT32)) { + bool aligned = true; + for (size_t i = 0; i < ci->mChannelData.Length(); ++i) { + if (!IS_ALIGNED16(ci->mChannelData[i])) { + aligned = false; + break; + } + } + + // Return this chunk directly to avoid copying data. + if (aligned) { + *aBlock = *ci; + return; + } + } + } + + aBlock->AllocateChannels(aFallbackChannelCount); + + uint32_t duration = 0; + for (AudioSegment::ChunkIterator ci(*aSegment); !ci.IsEnded(); ci.Next()) { + switch (ci->mBufferFormat) { + case AUDIO_FORMAT_S16: { + CopyChunkToBlock<int16_t>(*ci, aBlock, duration); + break; + } + case AUDIO_FORMAT_FLOAT32: { + CopyChunkToBlock<float>(*ci, aBlock, duration); + break; + } + case AUDIO_FORMAT_SILENCE: { + // The actual type of the sample does not matter here, but we still need + // to send some audio to the graph. + CopyChunkToBlock<float>(*ci, aBlock, duration); + break; + } + } + duration += ci->GetDuration(); + } +} + +void AudioNodeExternalInputTrack::ProcessInput(GraphTime aFrom, GraphTime aTo, + uint32_t aFlags) { + // According to spec, number of outputs is always 1. + MOZ_ASSERT(mLastChunks.Length() == 1); + + // GC stuff can result in our input track being destroyed before this track. + // Handle that. + if (!IsEnabled() || mInputs.IsEmpty() || mPassThrough) { + mLastChunks[0].SetNull(WEBAUDIO_BLOCK_SIZE); + return; + } + + MOZ_ASSERT(mInputs.Length() == 1); + + MediaTrack* source = mInputs[0]->GetSource(); + AutoTArray<AudioSegment, 1> audioSegments; + uint32_t inputChannels = 0; + + MOZ_ASSERT(source->GetData()->GetType() == MediaSegment::AUDIO, + "AudioNodeExternalInputTrack shouldn't have a video input"); + + const AudioSegment& inputSegment = + *mInputs[0]->GetSource()->GetData<AudioSegment>(); + if (!inputSegment.IsNull()) { + AudioSegment& segment = *audioSegments.AppendElement(); + GraphTime next; + for (GraphTime t = aFrom; t < aTo; t = next) { + MediaInputPort::InputInterval interval = + MediaInputPort::GetNextInputInterval(mInputs[0], t); + interval.mEnd = std::min(interval.mEnd, aTo); + if (interval.mStart >= interval.mEnd) { + break; + } + next = interval.mEnd; + + // We know this track does not block during the processing interval --- + // we're not finished, we don't underrun, and we're not suspended. + TrackTime outputStart = GraphTimeToTrackTime(interval.mStart); + TrackTime outputEnd = GraphTimeToTrackTime(interval.mEnd); + TrackTime ticks = outputEnd - outputStart; + + if (interval.mInputIsBlocked) { + segment.AppendNullData(ticks); + } else { + // The input track is not blocked in this interval, so no need to call + // GraphTimeToTrackTimeWithBlocking. + TrackTime inputStart = + std::min(inputSegment.GetDuration(), + source->GraphTimeToTrackTime(interval.mStart)); + TrackTime inputEnd = + std::min(inputSegment.GetDuration(), + source->GraphTimeToTrackTime(interval.mEnd)); + + segment.AppendSlice(inputSegment, inputStart, inputEnd); + // Pad if we're looking past the end of the track + segment.AppendNullData(ticks - (inputEnd - inputStart)); + } + } + + for (AudioSegment::ChunkIterator iter(segment); !iter.IsEnded(); + iter.Next()) { + inputChannels = + GetAudioChannelsSuperset(inputChannels, iter->ChannelCount()); + } + } + + uint32_t accumulateIndex = 0; + if (inputChannels) { + DownmixBufferType downmixBuffer; + ASSERT_ALIGNED16(downmixBuffer.Elements()); + for (auto& audioSegment : audioSegments) { + AudioBlock tmpChunk; + ConvertSegmentToAudioBlock(&audioSegment, &tmpChunk, inputChannels); + if (!tmpChunk.IsNull()) { + if (accumulateIndex == 0) { + mLastChunks[0].AllocateChannels(inputChannels); + } + AccumulateInputChunk(accumulateIndex, tmpChunk, &mLastChunks[0], + &downmixBuffer); + accumulateIndex++; + } + } + } + if (accumulateIndex == 0) { + mLastChunks[0].SetNull(WEBAUDIO_BLOCK_SIZE); + } +} + +bool AudioNodeExternalInputTrack::IsEnabled() { + return ((MediaStreamAudioSourceNodeEngine*)Engine())->IsEnabled(); +} + +} // namespace mozilla diff --git a/dom/media/webaudio/AudioNodeExternalInputTrack.h b/dom/media/webaudio/AudioNodeExternalInputTrack.h new file mode 100644 index 0000000000..f273fee696 --- /dev/null +++ b/dom/media/webaudio/AudioNodeExternalInputTrack.h @@ -0,0 +1,46 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/ +/* 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/. */ + +#ifndef MOZILLA_AUDIONODEEXTERNALINPUTTRACK_H_ +#define MOZILLA_AUDIONODEEXTERNALINPUTTRACK_H_ + +#include "MediaTrackGraph.h" +#include "AudioNodeTrack.h" +#include "mozilla/Atomics.h" + +namespace mozilla { + +class AbstractThread; + +/** + * This is a MediaTrack implementation that acts for a Web Audio node but + * unlike other AudioNodeTracks, supports any kind of MediaTrack as an + * input --- handling any number of audio tracks and handling blocking of + * the input MediaTrack. + */ +class AudioNodeExternalInputTrack final : public AudioNodeTrack { + public: + static already_AddRefed<AudioNodeExternalInputTrack> Create( + MediaTrackGraph* aGraph, AudioNodeEngine* aEngine); + + protected: + AudioNodeExternalInputTrack(AudioNodeEngine* aEngine, TrackRate aSampleRate); + ~AudioNodeExternalInputTrack(); + + public: + void ProcessInput(GraphTime aFrom, GraphTime aTo, uint32_t aFlags) override; + + private: + /** + * Determines if this is enabled or not. Disabled nodes produce silence. + * This node becomes disabled if the document principal does not subsume the + * DOMMediaStream principal. + */ + bool IsEnabled(); +}; + +} // namespace mozilla + +#endif /* MOZILLA_AUDIONODEEXTERNALINPUTTRACK_H_ */ diff --git a/dom/media/webaudio/AudioNodeTrack.cpp b/dom/media/webaudio/AudioNodeTrack.cpp new file mode 100644 index 0000000000..ea411a16ac --- /dev/null +++ b/dom/media/webaudio/AudioNodeTrack.cpp @@ -0,0 +1,594 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/ +/* 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 "AudioNodeTrack.h" + +#include "MediaTrackGraph.h" +#include "MediaTrackListener.h" +#include "AudioNodeEngine.h" +#include "ThreeDPoint.h" +#include "Tracing.h" +#include "AudioChannelFormat.h" +#include "AudioParamTimeline.h" +#include "AudioContext.h" +#include "nsMathUtils.h" +#include "AlignmentUtils.h" +#include "blink/Reverb.h" + +using namespace mozilla::dom; + +namespace mozilla { + +/** + * An AudioNodeTrack produces a single audio track with ID + * AUDIO_TRACK. This track has rate AudioContext::sIdealAudioRate + * for regular audio contexts, and the rate requested by the web content + * for offline audio contexts. + * Each chunk in the track is a single block of WEBAUDIO_BLOCK_SIZE samples. + * Note: This must be a different value than MEDIA_STREAM_DEST_TRACK_ID + */ + +AudioNodeTrack::AudioNodeTrack(AudioNodeEngine* aEngine, Flags aFlags, + TrackRate aSampleRate) + : ProcessedMediaTrack( + aSampleRate, MediaSegment::AUDIO, + (aFlags & EXTERNAL_OUTPUT) ? new AudioSegment() : nullptr), + mEngine(aEngine), + mFlags(aFlags), + mNumberOfInputChannels(2), + mIsActive(aEngine->IsActive()), + mMarkAsEndedAfterThisBlock(false), + mAudioParamTrack(false), + mPassThrough(false) { + MOZ_ASSERT(NS_IsMainThread()); + mSuspendedCount = !(mIsActive || mFlags & EXTERNAL_OUTPUT); + mChannelCountMode = ChannelCountMode::Max; + mChannelInterpretation = ChannelInterpretation::Speakers; + mLastChunks.SetLength(std::max(uint16_t(1), mEngine->OutputCount())); + MOZ_COUNT_CTOR(AudioNodeTrack); +} + +AudioNodeTrack::~AudioNodeTrack() { + MOZ_ASSERT(mActiveInputCount == 0); + MOZ_COUNT_DTOR(AudioNodeTrack); +} + +void AudioNodeTrack::OnGraphThreadDone() { mEngine->OnGraphThreadDone(); } + +void AudioNodeTrack::DestroyImpl() { + // These are graph thread objects, so clean up on graph thread. + mInputChunks.Clear(); + mLastChunks.Clear(); + + ProcessedMediaTrack::DestroyImpl(); +} + +/* static */ +already_AddRefed<AudioNodeTrack> AudioNodeTrack::Create( + AudioContext* aCtx, AudioNodeEngine* aEngine, Flags aFlags, + MediaTrackGraph* aGraph) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_RELEASE_ASSERT(aGraph); + + // MediaRecorders use an AudioNodeTrack, but no AudioNode + AudioNode* node = aEngine->NodeMainThread(); + + RefPtr<AudioNodeTrack> track = + new AudioNodeTrack(aEngine, aFlags, aGraph->GraphRate()); + if (node) { + track->SetChannelMixingParametersImpl(node->ChannelCount(), + node->ChannelCountModeValue(), + node->ChannelInterpretationValue()); + } + // All realtime tracks are initially suspended. + // ApplyAudioContextOperation() is used to start tracks so that a new track + // will not be started before the existing tracks, which may be awaiting an + // AudioCallbackDriver to resume. + bool isRealtime = !aCtx->IsOffline(); + track->mSuspendedCount += isRealtime; + aGraph->AddTrack(track); + if (isRealtime && !aCtx->ShouldSuspendNewTrack()) { + nsTArray<RefPtr<mozilla::MediaTrack>> tracks; + tracks.AppendElement(track); + aGraph->ApplyAudioContextOperation(aCtx->DestinationTrack(), + std::move(tracks), + AudioContextOperation::Resume); + } + return track.forget(); +} + +size_t AudioNodeTrack::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const { + size_t amount = 0; + + // Not reported: + // - mEngine + + amount += ProcessedMediaTrack::SizeOfExcludingThis(aMallocSizeOf); + amount += mLastChunks.ShallowSizeOfExcludingThis(aMallocSizeOf); + for (size_t i = 0; i < mLastChunks.Length(); i++) { + // NB: This is currently unshared only as there are instances of + // double reporting in DMD otherwise. + amount += mLastChunks[i].SizeOfExcludingThisIfUnshared(aMallocSizeOf); + } + + return amount; +} + +size_t AudioNodeTrack::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const { + return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf); +} + +void AudioNodeTrack::SizeOfAudioNodesIncludingThis( + MallocSizeOf aMallocSizeOf, AudioNodeSizes& aUsage) const { + // Explicitly separate out the track memory. + aUsage.mTrack = SizeOfIncludingThis(aMallocSizeOf); + + if (mEngine) { + // This will fill out the rest of |aUsage|. + mEngine->SizeOfIncludingThis(aMallocSizeOf, aUsage); + } +} + +void AudioNodeTrack::SetTrackTimeParameter(uint32_t aIndex, + AudioContext* aContext, + double aTrackTime) { + QueueControlMessageWithNoShutdown( + [self = RefPtr{this}, this, aIndex, + relativeToTrack = RefPtr{aContext->DestinationTrack()}, aTrackTime] { + TRACE("AudioNodeTrack::SetTrackTimeParameterImpl"); + SetTrackTimeParameterImpl(aIndex, relativeToTrack, aTrackTime); + }); +} + +void AudioNodeTrack::SetTrackTimeParameterImpl(uint32_t aIndex, + MediaTrack* aRelativeToTrack, + double aTrackTime) { + TrackTime ticks = aRelativeToTrack->SecondsToNearestTrackTime(aTrackTime); + mEngine->SetTrackTimeParameter(aIndex, ticks); +} + +void AudioNodeTrack::SetDoubleParameter(uint32_t aIndex, double aValue) { + QueueControlMessageWithNoShutdown( + [self = RefPtr{this}, this, aIndex, aValue] { + TRACE("AudioNodeTrack::SetDoubleParameter"); + Engine()->SetDoubleParameter(aIndex, aValue); + }); +} + +void AudioNodeTrack::SetInt32Parameter(uint32_t aIndex, int32_t aValue) { + QueueControlMessageWithNoShutdown( + [self = RefPtr{this}, this, aIndex, aValue] { + TRACE("AudioNodeTrack::SetInt32Parameter"); + Engine()->SetInt32Parameter(aIndex, aValue); + }); +} + +void AudioNodeTrack::SendTimelineEvent(uint32_t aIndex, + const AudioParamEvent& aEvent) { + QueueControlMessageWithNoShutdown( + [self = RefPtr{this}, this, aIndex, event = aEvent]() mutable { + TRACE("AudioNodeTrack::RecvTimelineEvent"); + Engine()->RecvTimelineEvent(aIndex, event); + }); +} + +void AudioNodeTrack::SetBuffer(AudioChunk&& aBuffer) { + QueueControlMessageWithNoShutdown( + [self = RefPtr{this}, this, buffer = std::move(aBuffer)]() mutable { + TRACE("AudioNodeTrack::SetBuffer"); + Engine()->SetBuffer(std::move(buffer)); + }); +} + +void AudioNodeTrack::SetReverb(WebCore::Reverb* aReverb, + uint32_t aImpulseChannelCount) { + QueueControlMessageWithNoShutdown([self = RefPtr{this}, this, + reverb = WrapUnique(aReverb), + aImpulseChannelCount]() mutable { + TRACE("AudioNodeTrack::SetReverb"); + Engine()->SetReverb(reverb.release(), aImpulseChannelCount); + }); +} + +void AudioNodeTrack::SetRawArrayData(nsTArray<float>&& aData) { + QueueControlMessageWithNoShutdown( + [self = RefPtr{this}, this, data = std::move(aData)]() mutable { + TRACE("AudioNodeTrack::SetRawArrayData"); + Engine()->SetRawArrayData(std::move(data)); + }); +} + +void AudioNodeTrack::SetChannelMixingParameters( + uint32_t aNumberOfChannels, ChannelCountMode aChannelCountMode, + ChannelInterpretation aChannelInterpretation) { + QueueControlMessageWithNoShutdown([self = RefPtr{this}, this, + aNumberOfChannels, aChannelCountMode, + aChannelInterpretation] { + TRACE("AudioNodeTrack::SetChannelMixingParameters"); + SetChannelMixingParametersImpl(aNumberOfChannels, aChannelCountMode, + aChannelInterpretation); + }); +} + +void AudioNodeTrack::SetPassThrough(bool aPassThrough) { + QueueControlMessageWithNoShutdown([self = RefPtr{this}, this, aPassThrough] { + TRACE("AudioNodeTrack::SetPassThrough"); + mPassThrough = aPassThrough; + }); +} + +void AudioNodeTrack::SendRunnable(already_AddRefed<nsIRunnable> aRunnable) { + QueueControlMessageWithNoShutdown([runnable = nsCOMPtr{aRunnable}] { + TRACE("AudioNodeTrack::SendRunnable"); + runnable->Run(); + }); +} + +void AudioNodeTrack::SetChannelMixingParametersImpl( + uint32_t aNumberOfChannels, ChannelCountMode aChannelCountMode, + ChannelInterpretation aChannelInterpretation) { + mNumberOfInputChannels = aNumberOfChannels; + mChannelCountMode = aChannelCountMode; + mChannelInterpretation = aChannelInterpretation; +} + +uint32_t AudioNodeTrack::ComputedNumberOfChannels(uint32_t aInputChannelCount) { + switch (mChannelCountMode) { + case ChannelCountMode::Explicit: + // Disregard the channel count we've calculated from inputs, and just use + // mNumberOfInputChannels. + return mNumberOfInputChannels; + case ChannelCountMode::Clamped_max: + // Clamp the computed output channel count to mNumberOfInputChannels. + return std::min(aInputChannelCount, mNumberOfInputChannels); + default: + case ChannelCountMode::Max: + // Nothing to do here, just shut up the compiler warning. + return aInputChannelCount; + } +} + +uint32_t AudioNodeTrack::NumberOfChannels() const { + AssertOnGraphThread(); + + return mNumberOfInputChannels; +} + +void AudioNodeTrack::AdvanceAndResume(TrackTime aAdvance) { + mMainThreadCurrentTime += aAdvance; + QueueControlMessageWithNoShutdown([self = RefPtr{this}, this, aAdvance] { + TRACE("AudioNodeTrack::AdvanceAndResumeMessage"); + mStartTime -= aAdvance; + mSegment->AppendNullData(aAdvance); + DecrementSuspendCount(); + }); +} + +void AudioNodeTrack::ObtainInputBlock(AudioBlock& aTmpChunk, + uint32_t aPortIndex) { + uint32_t inputCount = mInputs.Length(); + uint32_t outputChannelCount = 1; + AutoTArray<const AudioBlock*, 250> inputChunks; + for (uint32_t i = 0; i < inputCount; ++i) { + if (aPortIndex != mInputs[i]->InputNumber()) { + // This input is connected to a different port + continue; + } + MediaTrack* t = mInputs[i]->GetSource(); + AudioNodeTrack* a = static_cast<AudioNodeTrack*>(t); + MOZ_ASSERT(a == t->AsAudioNodeTrack()); + if (a->IsAudioParamTrack()) { + continue; + } + + const AudioBlock* chunk = &a->mLastChunks[mInputs[i]->OutputNumber()]; + MOZ_ASSERT(chunk); + if (chunk->IsNull() || chunk->mChannelData.IsEmpty()) { + continue; + } + + inputChunks.AppendElement(chunk); + outputChannelCount = + GetAudioChannelsSuperset(outputChannelCount, chunk->ChannelCount()); + } + + outputChannelCount = ComputedNumberOfChannels(outputChannelCount); + + uint32_t inputChunkCount = inputChunks.Length(); + if (inputChunkCount == 0 || + (inputChunkCount == 1 && inputChunks[0]->ChannelCount() == 0)) { + aTmpChunk.SetNull(WEBAUDIO_BLOCK_SIZE); + return; + } + + if (inputChunkCount == 1 && + inputChunks[0]->ChannelCount() == outputChannelCount) { + aTmpChunk = *inputChunks[0]; + return; + } + + if (outputChannelCount == 0) { + aTmpChunk.SetNull(WEBAUDIO_BLOCK_SIZE); + return; + } + + aTmpChunk.AllocateChannels(outputChannelCount); + DownmixBufferType downmixBuffer; + ASSERT_ALIGNED16(downmixBuffer.Elements()); + + for (uint32_t i = 0; i < inputChunkCount; ++i) { + AccumulateInputChunk(i, *inputChunks[i], &aTmpChunk, &downmixBuffer); + } +} + +void AudioNodeTrack::AccumulateInputChunk(uint32_t aInputIndex, + const AudioBlock& aChunk, + AudioBlock* aBlock, + DownmixBufferType* aDownmixBuffer) { + AutoTArray<const float*, GUESS_AUDIO_CHANNELS> channels; + UpMixDownMixChunk(&aChunk, aBlock->ChannelCount(), channels, *aDownmixBuffer); + + for (uint32_t c = 0; c < channels.Length(); ++c) { + const float* inputData = static_cast<const float*>(channels[c]); + float* outputData = aBlock->ChannelFloatsForWrite(c); + if (inputData) { + if (aInputIndex == 0) { + AudioBlockCopyChannelWithScale(inputData, aChunk.mVolume, outputData); + } else { + AudioBlockAddChannelWithScale(inputData, aChunk.mVolume, outputData); + } + } else { + if (aInputIndex == 0) { + PodZero(outputData, WEBAUDIO_BLOCK_SIZE); + } + } + } +} + +void AudioNodeTrack::UpMixDownMixChunk(const AudioBlock* aChunk, + uint32_t aOutputChannelCount, + nsTArray<const float*>& aOutputChannels, + DownmixBufferType& aDownmixBuffer) { + for (uint32_t i = 0; i < aChunk->ChannelCount(); i++) { + aOutputChannels.AppendElement( + static_cast<const float*>(aChunk->mChannelData[i])); + } + if (aOutputChannels.Length() < aOutputChannelCount) { + if (mChannelInterpretation == ChannelInterpretation::Speakers) { + AudioChannelsUpMix<float>(&aOutputChannels, aOutputChannelCount, nullptr); + NS_ASSERTION(aOutputChannelCount == aOutputChannels.Length(), + "We called GetAudioChannelsSuperset to avoid this"); + } else { + // Fill up the remaining aOutputChannels by zeros + for (uint32_t j = aOutputChannels.Length(); j < aOutputChannelCount; + ++j) { + aOutputChannels.AppendElement(nullptr); + } + } + } else if (aOutputChannels.Length() > aOutputChannelCount) { + if (mChannelInterpretation == ChannelInterpretation::Speakers) { + AutoTArray<float*, GUESS_AUDIO_CHANNELS> outputChannels; + outputChannels.SetLength(aOutputChannelCount); + aDownmixBuffer.SetLength(aOutputChannelCount * WEBAUDIO_BLOCK_SIZE); + for (uint32_t j = 0; j < aOutputChannelCount; ++j) { + outputChannels[j] = &aDownmixBuffer[j * WEBAUDIO_BLOCK_SIZE]; + } + + AudioChannelsDownMix<float, float>(aOutputChannels, outputChannels, + WEBAUDIO_BLOCK_SIZE); + + aOutputChannels.SetLength(aOutputChannelCount); + for (uint32_t j = 0; j < aOutputChannels.Length(); ++j) { + aOutputChannels[j] = outputChannels[j]; + } + } else { + // Drop the remaining aOutputChannels + aOutputChannels.RemoveLastElements(aOutputChannels.Length() - + aOutputChannelCount); + } + } +} + +// The MediaTrackGraph guarantees that this is actually one block, for +// AudioNodeTracks. +void AudioNodeTrack::ProcessInput(GraphTime aFrom, GraphTime aTo, + uint32_t aFlags) { + uint16_t outputCount = mLastChunks.Length(); + MOZ_ASSERT(outputCount == std::max(uint16_t(1), mEngine->OutputCount())); + + if (!mIsActive) { + // mLastChunks are already null. +#ifdef DEBUG + for (const auto& chunk : mLastChunks) { + MOZ_ASSERT(chunk.IsNull()); + } +#endif + } else if (InMutedCycle()) { + mInputChunks.Clear(); + for (uint16_t i = 0; i < outputCount; ++i) { + mLastChunks[i].SetNull(WEBAUDIO_BLOCK_SIZE); + } + } else { + // We need to generate at least one input + uint16_t maxInputs = std::max(uint16_t(1), mEngine->InputCount()); + mInputChunks.SetLength(maxInputs); + for (uint16_t i = 0; i < maxInputs; ++i) { + ObtainInputBlock(mInputChunks[i], i); + } + bool finished = false; + if (mPassThrough) { + MOZ_ASSERT(outputCount == 1, + "For now, we only support nodes that have one output port"); + mLastChunks[0] = mInputChunks[0]; + } else { + if (maxInputs <= 1 && outputCount <= 1) { + mEngine->ProcessBlock(this, aFrom, mInputChunks[0], &mLastChunks[0], + &finished); + } else { + mEngine->ProcessBlocksOnPorts( + this, aFrom, Span(mInputChunks.Elements(), mEngine->InputCount()), + Span(mLastChunks.Elements(), mEngine->OutputCount()), &finished); + } + } + for (uint16_t i = 0; i < outputCount; ++i) { + NS_ASSERTION(mLastChunks[i].GetDuration() == WEBAUDIO_BLOCK_SIZE, + "Invalid WebAudio chunk size"); + } + if (finished) { + mMarkAsEndedAfterThisBlock = true; + if (mIsActive) { + ScheduleCheckForInactive(); + } + } + + if (mDisabledMode != DisabledTrackMode::ENABLED) { + for (uint32_t i = 0; i < outputCount; ++i) { + mLastChunks[i].SetNull(WEBAUDIO_BLOCK_SIZE); + } + } + } + + if (!mEnded) { + // Don't output anything while finished + if (mFlags & EXTERNAL_OUTPUT) { + AdvanceOutputSegment(); + } + if (mMarkAsEndedAfterThisBlock && (aFlags & ALLOW_END)) { + // This track was ended the last time that we looked at it, and all + // of the depending tracks have ended their output as well, so now + // it's time to mark this track as ended. + mEnded = true; + } + } +} + +void AudioNodeTrack::ProduceOutputBeforeInput(GraphTime aFrom) { + MOZ_ASSERT(mEngine->AsDelayNodeEngine()); + MOZ_ASSERT(mEngine->OutputCount() == 1, + "DelayNodeEngine output count should be 1"); + MOZ_ASSERT(!InMutedCycle(), "DelayNodes should break cycles"); + MOZ_ASSERT(mLastChunks.Length() == 1); + + if (!mIsActive) { + mLastChunks[0].SetNull(WEBAUDIO_BLOCK_SIZE); + } else { + mEngine->ProduceBlockBeforeInput(this, aFrom, &mLastChunks[0]); + NS_ASSERTION(mLastChunks[0].GetDuration() == WEBAUDIO_BLOCK_SIZE, + "Invalid WebAudio chunk size"); + if (mDisabledMode != DisabledTrackMode::ENABLED) { + mLastChunks[0].SetNull(WEBAUDIO_BLOCK_SIZE); + } + } +} + +void AudioNodeTrack::AdvanceOutputSegment() { + AudioSegment* segment = GetData<AudioSegment>(); + + AudioChunk copyChunk = *mLastChunks[0].AsMutableChunk(); + AudioSegment tmpSegment; + tmpSegment.AppendAndConsumeChunk(std::move(copyChunk)); + + for (const auto& l : mTrackListeners) { + // Notify MediaTrackListeners. + l->NotifyQueuedChanges(Graph(), segment->GetDuration(), tmpSegment); + } + + if (mLastChunks[0].IsNull()) { + segment->AppendNullData(tmpSegment.GetDuration()); + } else { + segment->AppendFrom(&tmpSegment); + } +} + +void AudioNodeTrack::AddInput(MediaInputPort* aPort) { + ProcessedMediaTrack::AddInput(aPort); + AudioNodeTrack* ns = aPort->GetSource()->AsAudioNodeTrack(); + // Tracks that are not AudioNodeTracks are considered active. + if (!ns || (ns->mIsActive && !ns->IsAudioParamTrack())) { + IncrementActiveInputCount(); + } +} +void AudioNodeTrack::RemoveInput(MediaInputPort* aPort) { + ProcessedMediaTrack::RemoveInput(aPort); + AudioNodeTrack* ns = aPort->GetSource()->AsAudioNodeTrack(); + // Tracks that are not AudioNodeTracks are considered active. + if (!ns || (ns->mIsActive && !ns->IsAudioParamTrack())) { + DecrementActiveInputCount(); + } +} + +void AudioNodeTrack::SetActive() { + if (mIsActive || mMarkAsEndedAfterThisBlock) { + return; + } + + mIsActive = true; + if (!(mFlags & EXTERNAL_OUTPUT)) { + DecrementSuspendCount(); + } + if (IsAudioParamTrack()) { + // Consumers merely influence track order. + // They do not read from the track. + return; + } + + for (const auto& consumer : mConsumers) { + AudioNodeTrack* ns = consumer->GetDestination()->AsAudioNodeTrack(); + if (ns) { + ns->IncrementActiveInputCount(); + } + } +} + +void AudioNodeTrack::ScheduleCheckForInactive() { + if (mActiveInputCount > 0 && !mMarkAsEndedAfterThisBlock) { + return; + } + + RunAfterProcessing([self = RefPtr{this}, this] { + TRACE("AudioNodeTrack::CheckForInactive"); + CheckForInactive(); + }); +} + +void AudioNodeTrack::CheckForInactive() { + if (((mActiveInputCount > 0 || mEngine->IsActive()) && + !mMarkAsEndedAfterThisBlock) || + !mIsActive) { + return; + } + + mIsActive = false; + mInputChunks.Clear(); // not required for foreseeable future + for (auto& chunk : mLastChunks) { + chunk.SetNull(WEBAUDIO_BLOCK_SIZE); + } + if (!(mFlags & EXTERNAL_OUTPUT)) { + IncrementSuspendCount(); + } + if (IsAudioParamTrack()) { + return; + } + + for (const auto& consumer : mConsumers) { + AudioNodeTrack* ns = consumer->GetDestination()->AsAudioNodeTrack(); + if (ns) { + ns->DecrementActiveInputCount(); + } + } +} + +void AudioNodeTrack::IncrementActiveInputCount() { + ++mActiveInputCount; + SetActive(); +} + +void AudioNodeTrack::DecrementActiveInputCount() { + MOZ_ASSERT(mActiveInputCount > 0); + --mActiveInputCount; + CheckForInactive(); +} + +} // namespace mozilla diff --git a/dom/media/webaudio/AudioNodeTrack.h b/dom/media/webaudio/AudioNodeTrack.h new file mode 100644 index 0000000000..505ea96718 --- /dev/null +++ b/dom/media/webaudio/AudioNodeTrack.h @@ -0,0 +1,233 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/ +/* 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/. */ + +#ifndef MOZILLA_AUDIONODETRACK_H_ +#define MOZILLA_AUDIONODETRACK_H_ + +#include "MediaTrackGraph.h" +#include "mozilla/dom/AudioNodeBinding.h" +#include "AlignedTArray.h" +#include "AudioBlock.h" +#include "AudioSegment.h" + +namespace WebCore { +class Reverb; +} // namespace WebCore + +namespace mozilla { + +namespace dom { +struct ThreeDPoint; +struct AudioParamEvent; +class AudioContext; +} // namespace dom + +class AbstractThread; +class ThreadSharedFloatArrayBufferList; +class AudioNodeEngine; + +typedef AlignedAutoTArray<float, GUESS_AUDIO_CHANNELS * WEBAUDIO_BLOCK_SIZE, 16> + DownmixBufferType; + +/** + * An AudioNodeTrack produces one audio track with ID AUDIO_TRACK. + * The start time of the AudioTrack is aligned to the start time of the + * AudioContext's destination node track, plus some multiple of BLOCK_SIZE + * samples. + * + * An AudioNodeTrack has an AudioNodeEngine plugged into it that does the + * actual audio processing. AudioNodeTrack contains the glue code that + * integrates audio processing with the MediaTrackGraph. + */ +class AudioNodeTrack : public ProcessedMediaTrack { + typedef dom::ChannelCountMode ChannelCountMode; + typedef dom::ChannelInterpretation ChannelInterpretation; + + public: + typedef mozilla::dom::AudioContext AudioContext; + + enum { AUDIO_TRACK = 1 }; + + typedef AutoTArray<AudioBlock, 1> OutputChunks; + + // Flags re main thread updates and track output. + typedef unsigned Flags; + enum : Flags { + NO_TRACK_FLAGS = 0U, + NEED_MAIN_THREAD_ENDED = 1U << 0, + NEED_MAIN_THREAD_CURRENT_TIME = 1U << 1, + // Internal AudioNodeTracks can only pass their output to another + // AudioNode, whereas external AudioNodeTracks can pass their output + // to other ProcessedMediaTracks or hardware audio output. + EXTERNAL_OUTPUT = 1U << 2, + }; + /** + * Create a track that will process audio for an AudioNode. + * Takes ownership of aEngine. + * aGraph is required and equals the graph of aCtx in most cases. An exception + * is AudioDestinationNode where the context's graph hasn't been set up yet. + */ + static already_AddRefed<AudioNodeTrack> Create(AudioContext* aCtx, + AudioNodeEngine* aEngine, + Flags aKind, + MediaTrackGraph* aGraph); + + protected: + /** + * Transfers ownership of aEngine to the new AudioNodeTrack. + */ + AudioNodeTrack(AudioNodeEngine* aEngine, Flags aFlags, TrackRate aSampleRate); + + ~AudioNodeTrack(); + + public: + // Control API + /** + * Sets a parameter that's a time relative to some track's played time. + * This time is converted to a time relative to this track when it's set. + */ + void SetTrackTimeParameter(uint32_t aIndex, AudioContext* aContext, + double aTrackTime); + void SetDoubleParameter(uint32_t aIndex, double aValue); + void SetInt32Parameter(uint32_t aIndex, int32_t aValue); + void SetThreeDPointParameter(uint32_t aIndex, const dom::ThreeDPoint& aValue); + void SetBuffer(AudioChunk&& aBuffer); + void SetReverb(WebCore::Reverb* aReverb, uint32_t aImpulseChannelCount); + // This sends a single event to the timeline on the MTG thread side. + void SendTimelineEvent(uint32_t aIndex, const dom::AudioParamEvent& aEvent); + // This consumes the contents of aData. aData will be emptied after this + // returns. + void SetRawArrayData(nsTArray<float>&& aData); + void SetChannelMixingParameters(uint32_t aNumberOfChannels, + ChannelCountMode aChannelCountMoe, + ChannelInterpretation aChannelInterpretation); + void SetPassThrough(bool aPassThrough); + void SendRunnable(already_AddRefed<nsIRunnable> aRunnable); + ChannelInterpretation GetChannelInterpretation() { + return mChannelInterpretation; + } + + void SetAudioParamHelperTrack() { + MOZ_ASSERT(!mAudioParamTrack, "Can only do this once"); + mAudioParamTrack = true; + } + // The value for channelCount on an AudioNode, but on the audio thread side. + uint32_t NumberOfChannels() const override; + + /* + * Resume track after updating its concept of current time by aAdvance. + * Main thread. Used only from AudioDestinationNode when resuming a track + * suspended to save running the MediaTrackGraph when there are no other + * nodes in the AudioContext. + */ + void AdvanceAndResume(TrackTime aAdvance); + + AudioNodeTrack* AsAudioNodeTrack() override { return this; } + void AddInput(MediaInputPort* aPort) override; + void RemoveInput(MediaInputPort* aPort) override; + + // Graph thread only + void SetTrackTimeParameterImpl(uint32_t aIndex, MediaTrack* aRelativeToTrack, + double aTrackTime); + void SetChannelMixingParametersImpl( + uint32_t aNumberOfChannels, ChannelCountMode aChannelCountMoe, + ChannelInterpretation aChannelInterpretation); + void ProcessInput(GraphTime aFrom, GraphTime aTo, uint32_t aFlags) override; + /** + * Produce the next block of output, before input is provided. + * ProcessInput() will be called later, and it then should not change + * the output. This is used only for DelayNodeEngine in a feedback loop. + */ + void ProduceOutputBeforeInput(GraphTime aFrom); + bool IsAudioParamTrack() const { return mAudioParamTrack; } + + const OutputChunks& LastChunks() const { return mLastChunks; } + bool MainThreadNeedsUpdates() const override { + return ((mFlags & NEED_MAIN_THREAD_ENDED) && mEnded) || + (mFlags & NEED_MAIN_THREAD_CURRENT_TIME); + } + + // Any thread + AudioNodeEngine* Engine() { return mEngine.get(); } + + size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override; + size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override; + + void SizeOfAudioNodesIncludingThis(MallocSizeOf aMallocSizeOf, + AudioNodeSizes& aUsage) const; + + /* + * SetActive() is called when either an active input is added or the engine + * for a source node transitions from inactive to active. This is not + * called from engines for processing nodes because they only become active + * when there are active input tracks, in which case this track is already + * active. + */ + void SetActive(); + /* + * ScheduleCheckForInactive() is called during track processing when the + * engine transitions from active to inactive, or the track finishes. It + * schedules a call to CheckForInactive() after track processing. + */ + void ScheduleCheckForInactive(); + + protected: + void OnGraphThreadDone() override; + void DestroyImpl() override; + + /* + * CheckForInactive() is called when the engine transitions from active to + * inactive, or an active input is removed, or the track finishes. If the + * track is now inactive, then mInputChunks will be cleared and mLastChunks + * will be set to null. ProcessBlock() will not be called on the engine + * again until SetActive() is called. + */ + void CheckForInactive(); + + void AdvanceOutputSegment(); + void FinishOutput(); + void AccumulateInputChunk(uint32_t aInputIndex, const AudioBlock& aChunk, + AudioBlock* aBlock, + DownmixBufferType* aDownmixBuffer); + void UpMixDownMixChunk(const AudioBlock* aChunk, uint32_t aOutputChannelCount, + nsTArray<const float*>& aOutputChannels, + DownmixBufferType& aDownmixBuffer); + + uint32_t ComputedNumberOfChannels(uint32_t aInputChannelCount); + void ObtainInputBlock(AudioBlock& aTmpChunk, uint32_t aPortIndex); + void IncrementActiveInputCount(); + void DecrementActiveInputCount(); + + // The engine that will generate output for this node. + const UniquePtr<AudioNodeEngine> mEngine; + // The mixed input blocks are kept from iteration to iteration to avoid + // reallocating channel data arrays and any buffers for mixing. + OutputChunks mInputChunks; + // The last block produced by this node. + OutputChunks mLastChunks; + // Whether this is an internal or external track + const Flags mFlags; + // The number of input tracks that may provide non-silent input. + uint32_t mActiveInputCount = 0; + // The number of input channels that this track requires. 0 means don't care. + uint32_t mNumberOfInputChannels; + // The mixing modes + ChannelCountMode mChannelCountMode; + ChannelInterpretation mChannelInterpretation; + // Tracks are considered active if the track has not finished and either + // the engine is active or there are active input tracks. + bool mIsActive; + // Whether the track should be marked as ended as soon + // as the current time range has been computed block by block. + bool mMarkAsEndedAfterThisBlock; + // Whether the track is an AudioParamHelper track. + bool mAudioParamTrack; + // Whether the track just passes its input through. + bool mPassThrough; +}; + +} // namespace mozilla + +#endif /* MOZILLA_AUDIONODETRACK_H_ */ diff --git a/dom/media/webaudio/AudioParam.cpp b/dom/media/webaudio/AudioParam.cpp new file mode 100644 index 0000000000..58d3b225c5 --- /dev/null +++ b/dom/media/webaudio/AudioParam.cpp @@ -0,0 +1,170 @@ +/* -*- 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 "AudioParam.h" +#include "mozilla/dom/AudioParamBinding.h" +#include "AudioNodeEngine.h" +#include "AudioNodeTrack.h" +#include "AudioContext.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(AudioParam) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(AudioParam) + tmp->DisconnectFromGraphAndDestroyTrack(); + NS_IMPL_CYCLE_COLLECTION_UNLINK(mNode) + NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER +NS_IMPL_CYCLE_COLLECTION_UNLINK_END +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(AudioParam) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mNode) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTING_NATIVE_ADDREF(AudioParam) +NS_IMPL_CYCLE_COLLECTING_NATIVE_RELEASE(AudioParam) + +AudioParam::AudioParam(AudioNode* aNode, uint32_t aIndex, + const nsAString& aName, float aDefaultValue, + float aMinValue, float aMaxValue) + : AudioParamTimeline(aDefaultValue), + mNode(aNode), + mName(aName), + mIndex(aIndex), + mDefaultValue(aDefaultValue), + mMinValue(aMinValue), + mMaxValue(aMaxValue) {} + +AudioParam::~AudioParam() { DisconnectFromGraphAndDestroyTrack(); } + +JSObject* AudioParam::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return AudioParam_Binding::Wrap(aCx, this, aGivenProto); +} + +void AudioParam::DisconnectFromGraphAndDestroyTrack() { + MOZ_ASSERT(mRefCnt.get() > mInputNodes.Length(), + "Caller should be holding a reference or have called " + "mRefCnt.stabilizeForDeletion()"); + + while (!mInputNodes.IsEmpty()) { + RefPtr<AudioNode> input = mInputNodes.PopLastElement().mInputNode; + input->RemoveOutputParam(this); + } + + if (mNodeTrackPort) { + mNodeTrackPort->Destroy(); + mNodeTrackPort = nullptr; + } + + if (mTrack) { + mTrack->Destroy(); + mTrack = nullptr; + } +} + +mozilla::MediaTrack* AudioParam::GetTrack() const { return mTrack; } + +mozilla::MediaTrack* AudioParam::Track() { + if (mTrack) { + return mTrack; + } + + AudioNodeEngine* engine = new AudioNodeEngine(nullptr); + mTrack = AudioNodeTrack::Create(mNode->Context(), engine, + AudioNodeTrack::NO_TRACK_FLAGS, + mNode->Context()->Graph()); + + // Force the input to have only one channel, and make it down-mix using + // the speaker rules if needed. + mTrack->SetChannelMixingParametersImpl(1, ChannelCountMode::Explicit, + ChannelInterpretation::Speakers); + // Mark as an AudioParam helper track + mTrack->SetAudioParamHelperTrack(); + + // Setup the AudioParam's track as an input to the owner AudioNode's track + AudioNodeTrack* nodeTrack = mNode->GetTrack(); + if (nodeTrack) { + mNodeTrackPort = nodeTrack->AllocateInputPort(mTrack); + } + + // Send the track to the timeline on the MTG side. + AudioParamEvent event(mTrack); + SendEventToEngine(event); + + return mTrack; +} + +static const char* ToString(AudioTimelineEvent::Type aType) { + switch (aType) { + case AudioTimelineEvent::SetValue: + return "SetValue"; + case AudioTimelineEvent::SetValueAtTime: + return "SetValueAtTime"; + case AudioTimelineEvent::LinearRamp: + return "LinearRamp"; + case AudioTimelineEvent::ExponentialRamp: + return "ExponentialRamp"; + case AudioTimelineEvent::SetTarget: + return "SetTarget"; + case AudioTimelineEvent::SetValueCurve: + return "SetValueCurve"; + case AudioTimelineEvent::Track: + return "Track"; + case AudioTimelineEvent::Cancel: + return "Cancel"; + default: + return "unknown AudioTimelineEvent"; + } +} + +void AudioParam::SendEventToEngine(const AudioParamEvent& aEvent) { + WEB_AUDIO_API_LOG( + "%f: %s for %u %s %s=%g time=%f %s=%g", GetParentObject()->CurrentTime(), + NS_ConvertUTF16toUTF8(mName).get(), ParentNodeId(), + ToString(aEvent.mType), + aEvent.mType == AudioTimelineEvent::SetValueCurve ? "length" : "value", + aEvent.mType == AudioTimelineEvent::SetValueCurve + ? static_cast<double>(aEvent.CurveLength()) + : static_cast<double>(aEvent.NominalValue()), + aEvent.Time<double>(), + aEvent.mType == AudioTimelineEvent::SetValueCurve ? "duration" + : "constant", + aEvent.mType == AudioTimelineEvent::SetValueCurve + ? aEvent.Duration() + : aEvent.TimeConstant()); + + AudioNodeTrack* track = mNode->GetTrack(); + if (track) { + track->SendTimelineEvent(mIndex, aEvent); + } +} + +void AudioParam::CleanupOldEvents() { + MOZ_ASSERT(NS_IsMainThread()); + double currentTime = mNode->Context()->CurrentTime(); + + CleanupEventsOlderThan(currentTime); +} + +float AudioParamTimeline::AudioNodeInputValue(size_t aCounter) const { + MOZ_ASSERT(mTrack); + + // If we have a chunk produced by the AudioNode inputs to the AudioParam, + // get its value now. We use aCounter to tell us which frame of the last + // AudioChunk to look at. + float audioNodeInputValue = 0.0f; + const AudioBlock& lastAudioNodeChunk = mTrack->LastChunks()[0]; + if (!lastAudioNodeChunk.IsNull()) { + MOZ_ASSERT(lastAudioNodeChunk.GetDuration() == WEBAUDIO_BLOCK_SIZE); + audioNodeInputValue = + static_cast<const float*>(lastAudioNodeChunk.mChannelData[0])[aCounter]; + audioNodeInputValue *= lastAudioNodeChunk.mVolume; + } + + return audioNodeInputValue; +} + +} // namespace mozilla::dom diff --git a/dom/media/webaudio/AudioParam.h b/dom/media/webaudio/AudioParam.h new file mode 100644 index 0000000000..2365857a5b --- /dev/null +++ b/dom/media/webaudio/AudioParam.h @@ -0,0 +1,239 @@ +/* -*- 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/. */ + +#ifndef AudioParam_h_ +#define AudioParam_h_ + +#include "AudioParamTimeline.h" +#include "nsWrapperCache.h" +#include "nsCycleCollectionParticipant.h" +#include "AudioNode.h" +#include "mozilla/dom/TypedArray.h" +#include "WebAudioUtils.h" +#include "js/TypeDecls.h" + +namespace mozilla::dom { + +class AudioParam final : public nsWrapperCache, public AudioParamTimeline { + virtual ~AudioParam(); + + public: + AudioParam(AudioNode* aNode, uint32_t aIndex, const nsAString& aName, + float aDefaultValue, + float aMinValue = std::numeric_limits<float>::lowest(), + float aMaxValue = std::numeric_limits<float>::max()); + + NS_IMETHOD_(MozExternalRefCountType) AddRef(void); + NS_IMETHOD_(MozExternalRefCountType) Release(void); + NS_DECL_CYCLE_COLLECTION_NATIVE_WRAPPERCACHE_CLASS(AudioParam) + + AudioContext* GetParentObject() const { return mNode->Context(); } + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + float Value() { + return AudioParamTimeline::GetValueAtTime<double>( + GetParentObject()->CurrentTime()); + } + + // We override SetValueCurveAtTime to convert the Float32Array to the wrapper + // object. + AudioParam* SetValueCurveAtTime(const nsTArray<float>& aValues, + double aStartTime, double aDuration, + ErrorResult& aRv) { + if (!WebAudioUtils::IsTimeValid(aStartTime)) { + aRv.ThrowRangeError<MSG_INVALID_AUDIOPARAM_METHOD_START_TIME_ERROR>(); + return this; + } + aStartTime = std::max(aStartTime, GetParentObject()->CurrentTime()); + AudioParamEvent event(AudioTimelineEvent::SetValueCurve, aValues, + aStartTime, aDuration); + ValidateAndInsertEvent(event, aRv); + return this; + } + + // Intended for use in AudioNode creation, when the setter should not throw. + void SetInitialValue(float aValue) { + MOZ_ASSERT(HasSimpleValue(), "Existing events unexpected"); + AudioParamEvent event(AudioTimelineEvent::SetValue, 0.0f, aValue); + + DebugOnly<ErrorResult> rv; + MOZ_ASSERT(ValidateEvent(event, rv), "This event should be valid"); + + AudioParamTimeline::SetValue(aValue); + + SendEventToEngine(event); + } + + void SetValue(float aValue, ErrorResult& aRv) { + AudioParamEvent event(AudioTimelineEvent::SetValue, 0.0f, aValue); + + if (!ValidateEvent(event, aRv)) { + return; + } + + AudioParamTimeline::SetValue(aValue); + + SendEventToEngine(event); + } + + AudioParam* SetValueAtTime(float aValue, double aStartTime, + ErrorResult& aRv) { + if (!WebAudioUtils::IsTimeValid(aStartTime)) { + aRv.ThrowRangeError<MSG_INVALID_AUDIOPARAM_METHOD_START_TIME_ERROR>(); + return this; + } + aStartTime = std::max(aStartTime, GetParentObject()->CurrentTime()); + AudioParamEvent event(AudioTimelineEvent::SetValueAtTime, aStartTime, + aValue); + ValidateAndInsertEvent(event, aRv); + return this; + } + + AudioParam* LinearRampToValueAtTime(float aValue, double aEndTime, + ErrorResult& aRv) { + if (!WebAudioUtils::IsTimeValid(aEndTime)) { + aRv.ThrowRangeError<MSG_INVALID_AUDIOPARAM_METHOD_END_TIME_ERROR>(); + return this; + } + aEndTime = std::max(aEndTime, GetParentObject()->CurrentTime()); + AudioParamEvent event(AudioTimelineEvent::LinearRamp, aEndTime, aValue); + ValidateAndInsertEvent(event, aRv); + return this; + } + + AudioParam* ExponentialRampToValueAtTime(float aValue, double aEndTime, + ErrorResult& aRv) { + if (!WebAudioUtils::IsTimeValid(aEndTime)) { + aRv.ThrowRangeError<MSG_INVALID_AUDIOPARAM_METHOD_END_TIME_ERROR>(); + return this; + } + aEndTime = std::max(aEndTime, GetParentObject()->CurrentTime()); + AudioParamEvent event(AudioTimelineEvent::ExponentialRamp, aEndTime, + aValue); + ValidateAndInsertEvent(event, aRv); + return this; + } + + AudioParam* SetTargetAtTime(float aTarget, double aStartTime, + double aTimeConstant, ErrorResult& aRv) { + if (!WebAudioUtils::IsTimeValid(aStartTime) || + !WebAudioUtils::IsTimeValid(aTimeConstant)) { + aRv.ThrowRangeError<MSG_INVALID_AUDIOPARAM_METHOD_START_TIME_ERROR>(); + return this; + } + aStartTime = std::max(aStartTime, GetParentObject()->CurrentTime()); + AudioParamEvent event(AudioTimelineEvent::SetTarget, aStartTime, aTarget, + aTimeConstant); + ValidateAndInsertEvent(event, aRv); + return this; + } + + AudioParam* CancelScheduledValues(double aStartTime, ErrorResult& aRv) { + if (!WebAudioUtils::IsTimeValid(aStartTime)) { + aRv.ThrowRangeError<MSG_INVALID_AUDIOPARAM_METHOD_START_TIME_ERROR>(); + return this; + } + + aStartTime = std::max(aStartTime, GetParentObject()->CurrentTime()); + + // Remove some events on the main thread copy. + AudioEventTimeline::CancelScheduledValues(aStartTime); + + AudioParamEvent event(AudioTimelineEvent::Cancel, aStartTime, 0.0f); + + SendEventToEngine(event); + + return this; + } + + uint32_t ParentNodeId() { return mNode->Id(); } + + void GetName(nsAString& aName) { aName.Assign(mName); } + + float DefaultValue() const { return mDefaultValue; } + + float MinValue() const { return mMinValue; } + + float MaxValue() const { return mMaxValue; } + + bool IsTrackSuspended() const { + return mTrack ? mTrack->IsSuspended() : false; + } + + const nsTArray<AudioNode::InputNode>& InputNodes() const { + return mInputNodes; + } + + void RemoveInputNode(uint32_t aIndex) { mInputNodes.RemoveElementAt(aIndex); } + + AudioNode::InputNode* AppendInputNode() { + return mInputNodes.AppendElement(); + } + + // May create the track if it doesn't exist + mozilla::MediaTrack* Track(); + + // Return nullptr if track doesn't exist. + mozilla::MediaTrack* GetTrack() const; + + size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override { + size_t amount = AudioParamTimeline::SizeOfExcludingThis(aMallocSizeOf); + // Not owned: + // - mNode + + // Just count the array, actual nodes are counted in mNode. + amount += mInputNodes.ShallowSizeOfExcludingThis(aMallocSizeOf); + + if (mNodeTrackPort) { + amount += mNodeTrackPort->SizeOfIncludingThis(aMallocSizeOf); + } + + return amount; + } + + size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override { + return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf); + } + + private: + void ValidateAndInsertEvent(const AudioParamEvent& aEvent, ErrorResult& aRv) { + if (!ValidateEvent(aEvent, aRv)) { + return; + } + + AudioEventTimeline::InsertEvent<double>(aEvent); + + SendEventToEngine(aEvent); + + CleanupOldEvents(); + } + + void CleanupOldEvents(); + + void SendEventToEngine(const AudioParamEvent& aEvent); + + void DisconnectFromGraphAndDestroyTrack(); + + nsCycleCollectingAutoRefCnt mRefCnt; + NS_DECL_OWNINGTHREAD + RefPtr<AudioNode> mNode; + // For every InputNode, there is a corresponding entry in mOutputParams of the + // InputNode's mInputNode. + nsTArray<AudioNode::InputNode> mInputNodes; + const nsString mName; + // The input port used to connect the AudioParam's track to its node's track + RefPtr<MediaInputPort> mNodeTrackPort; + const uint32_t mIndex; + const float mDefaultValue; + const float mMinValue; + const float mMaxValue; +}; + +} // namespace mozilla::dom + +#endif diff --git a/dom/media/webaudio/AudioParamDescriptorMap.h b/dom/media/webaudio/AudioParamDescriptorMap.h new file mode 100644 index 0000000000..978e8bc90c --- /dev/null +++ b/dom/media/webaudio/AudioParamDescriptorMap.h @@ -0,0 +1,22 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 https://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_AudioParamDescriptor_h +#define mozilla_dom_AudioParamDescriptor_h + +#include "mozilla/dom/AudioParamDescriptorBinding.h" +#include "nsTArray.h" + +namespace mozilla::dom { + +// Note: we call this "map" to match the spec, but we store audio param +// descriptors in an array, because we need ordered access, and don't need the +// map functionalities. +typedef nsTArray<AudioParamDescriptor> AudioParamDescriptorMap; + +} // namespace mozilla::dom + +#endif // mozilla_dom_AudioParamDescriptor_h diff --git a/dom/media/webaudio/AudioParamMap.cpp b/dom/media/webaudio/AudioParamMap.cpp new file mode 100644 index 0000000000..2d4f936361 --- /dev/null +++ b/dom/media/webaudio/AudioParamMap.cpp @@ -0,0 +1,21 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 https://mozilla.org/MPL/2.0/. */ + +#include "AudioParamMap.h" +#include "mozilla/dom/AudioParamMapBinding.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(AudioParamMap, mParent) + +AudioParamMap::AudioParamMap(AudioWorkletNode* aParent) : mParent(aParent) {} + +JSObject* AudioParamMap::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return AudioParamMap_Binding::Wrap(aCx, this, aGivenProto); +} + +} // namespace mozilla::dom diff --git a/dom/media/webaudio/AudioParamMap.h b/dom/media/webaudio/AudioParamMap.h new file mode 100644 index 0000000000..c7bb0bdbea --- /dev/null +++ b/dom/media/webaudio/AudioParamMap.h @@ -0,0 +1,34 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 https://mozilla.org/MPL/2.0/. */ + +#ifndef AudioParamMap_h_ +#define AudioParamMap_h_ + +#include "nsWrapperCache.h" +#include "AudioWorkletNode.h" + +namespace mozilla::dom { + +class AudioParamMap final : public nsWrapperCache { + public: + NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(AudioParamMap) + NS_DECL_CYCLE_COLLECTION_NATIVE_WRAPPERCACHE_CLASS(AudioParamMap) + + explicit AudioParamMap(AudioWorkletNode* aParent); + + AudioWorkletNode* GetParentObject() const { return mParent; } + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + private: + ~AudioParamMap() = default; + RefPtr<AudioWorkletNode> mParent; +}; + +} // namespace mozilla::dom + +#endif // AudioParamMap_h_ diff --git a/dom/media/webaudio/AudioParamTimeline.h b/dom/media/webaudio/AudioParamTimeline.h new file mode 100644 index 0000000000..bd0ba99ffe --- /dev/null +++ b/dom/media/webaudio/AudioParamTimeline.h @@ -0,0 +1,144 @@ +/* -*- 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/. */ + +#ifndef AudioParamTimeline_h_ +#define AudioParamTimeline_h_ + +#include "AudioEventTimeline.h" +#include "AudioNodeTrack.h" +#include "AudioSegment.h" + +namespace mozilla::dom { + +struct AudioParamEvent final : public AudioTimelineEvent { + AudioParamEvent(Type aType, double aTime, float aValue, + double aTimeConstant = 0.0) + : AudioTimelineEvent(aType, aTime, aValue, aTimeConstant) {} + AudioParamEvent(Type aType, const nsTArray<float>& aValues, double aStartTime, + double aDuration) + : AudioTimelineEvent(aType, aValues, aStartTime, aDuration) {} + explicit AudioParamEvent(AudioNodeTrack* aTrack) + : AudioTimelineEvent(Track, 0.0, 0.f), mTrack(aTrack) {} + + RefPtr<AudioNodeTrack> mTrack; +}; + +// This helper class is used to represent the part of the AudioParam +// class that gets sent to AudioNodeEngine instances. In addition to +// AudioEventTimeline methods, it holds a pointer to an optional +// AudioNodeTrack which represents the AudioNode inputs to the AudioParam. +// This AudioNodeTrack is managed by the AudioParam subclass on the main +// thread, and can only be obtained from the AudioNodeEngine instances +// consuming this class. +class AudioParamTimeline : public AudioEventTimeline { + typedef AudioEventTimeline BaseClass; + + public: + explicit AudioParamTimeline(float aDefaultValue) : BaseClass(aDefaultValue) {} + + AudioNodeTrack* Track() const { return mTrack; } + + bool HasSimpleValue() const { + return BaseClass::HasSimpleValue() && + (!mTrack || mTrack->LastChunks()[0].IsNull()); + } + + template <class TimeType> + float GetValueAtTime(TimeType aTime); + + // Prefer this method over GetValueAtTime() only if HasSimpleValue() is + // known false. + float GetComplexValueAtTime(int64_t aTime); + float GetComplexValueAtTime(double aTime) = delete; + + template <typename TimeType> + void InsertEvent(const AudioParamEvent& aEvent) { + if (aEvent.mType == AudioTimelineEvent::Cancel) { + CancelScheduledValues(aEvent.Time<TimeType>()); + return; + } + if (aEvent.mType == AudioTimelineEvent::Track) { + mTrack = aEvent.mTrack; + return; + } + if (aEvent.mType == AudioTimelineEvent::SetValue) { + AudioEventTimeline::SetValue(aEvent.NominalValue()); + return; + } + AudioEventTimeline::InsertEvent<TimeType>(aEvent); + } + + // Get the values of the AudioParam at time aTime + (0 to aSize). + // aBuffer must have the correct aSize. + // aSize here is an offset to aTime if we try to get the value in ticks, + // otherwise it should always be zero. aSize is meant to be used when + // getting the value of an a-rate AudioParam for each tick inside an + // AudioNodeEngine implementation. + void GetValuesAtTime(int64_t aTime, float* aBuffer, const size_t aSize); + void GetValuesAtTime(double aTime, float* aBuffer, + const size_t aSize) = delete; + + virtual size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const { + return mTrack ? mTrack->SizeOfIncludingThis(aMallocSizeOf) : 0; + } + + virtual size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const { + return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf); + } + + private: + float AudioNodeInputValue(size_t aCounter) const; + + protected: + // This is created lazily when needed. + RefPtr<AudioNodeTrack> mTrack; +}; + +template <> +inline float AudioParamTimeline::GetValueAtTime(double aTime) { + // Getting an AudioParam value on an AudioNode does not consider input from + // other AudioNodes, which is managed only on the graph thread. + return BaseClass::GetValueAtTime(aTime); +} + +template <> +inline float AudioParamTimeline::GetValueAtTime(int64_t aTime) { + // Use GetValuesAtTime() for a-rate parameters. + MOZ_ASSERT(aTime % WEBAUDIO_BLOCK_SIZE == 0); + if (HasSimpleValue()) { + return GetValue(); + } + return GetComplexValueAtTime(aTime); +} + +inline float AudioParamTimeline::GetComplexValueAtTime(int64_t aTime) { + MOZ_ASSERT(aTime % WEBAUDIO_BLOCK_SIZE == 0); + + // Mix the value of the AudioParam itself with that of the AudioNode inputs. + return BaseClass::GetValueAtTime(aTime) + + (mTrack ? AudioNodeInputValue(0) : 0.0f); +} + +inline void AudioParamTimeline::GetValuesAtTime(int64_t aTime, float* aBuffer, + const size_t aSize) { + MOZ_ASSERT(aBuffer); + MOZ_ASSERT(aSize <= WEBAUDIO_BLOCK_SIZE); + MOZ_ASSERT(aSize == 1 || !HasSimpleValue()); + + // Mix the value of the AudioParam itself with that of the AudioNode inputs. + BaseClass::GetValuesAtTime(aTime, aBuffer, aSize); + if (mTrack) { + uint32_t blockOffset = aTime % WEBAUDIO_BLOCK_SIZE; + MOZ_ASSERT(blockOffset + aSize <= WEBAUDIO_BLOCK_SIZE); + for (size_t i = 0; i < aSize; ++i) { + aBuffer[i] += AudioNodeInputValue(blockOffset + i); + } + } +} + +} // namespace mozilla::dom + +#endif diff --git a/dom/media/webaudio/AudioProcessingEvent.cpp b/dom/media/webaudio/AudioProcessingEvent.cpp new file mode 100644 index 0000000000..7d9987db07 --- /dev/null +++ b/dom/media/webaudio/AudioProcessingEvent.cpp @@ -0,0 +1,44 @@ +/* -*- 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 "AudioProcessingEvent.h" +#include "mozilla/dom/AudioProcessingEventBinding.h" +#include "mozilla/dom/ScriptSettings.h" +#include "AudioContext.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_INHERITED(AudioProcessingEvent, Event, mInputBuffer, + mOutputBuffer, mNode) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(AudioProcessingEvent) +NS_INTERFACE_MAP_END_INHERITING(Event) + +NS_IMPL_ADDREF_INHERITED(AudioProcessingEvent, Event) +NS_IMPL_RELEASE_INHERITED(AudioProcessingEvent, Event) + +AudioProcessingEvent::AudioProcessingEvent(ScriptProcessorNode* aOwner, + nsPresContext* aPresContext, + WidgetEvent* aEvent) + : Event(aOwner, aPresContext, aEvent), mPlaybackTime(0.0), mNode(aOwner) {} + +AudioProcessingEvent::~AudioProcessingEvent() = default; + +JSObject* AudioProcessingEvent::WrapObjectInternal( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) { + return AudioProcessingEvent_Binding::Wrap(aCx, this, aGivenProto); +} + +already_AddRefed<AudioBuffer> AudioProcessingEvent::LazilyCreateBuffer( + uint32_t aNumberOfChannels, ErrorResult& aRv) { + RefPtr<AudioBuffer> buffer = AudioBuffer::Create( + mNode->Context()->GetOwner(), aNumberOfChannels, mNode->BufferSize(), + mNode->Context()->SampleRate(), aRv); + MOZ_ASSERT(buffer || aRv.ErrorCodeIs(NS_ERROR_OUT_OF_MEMORY)); + return buffer.forget(); +} + +} // namespace mozilla::dom diff --git a/dom/media/webaudio/AudioProcessingEvent.h b/dom/media/webaudio/AudioProcessingEvent.h new file mode 100644 index 0000000000..3b8b9cf008 --- /dev/null +++ b/dom/media/webaudio/AudioProcessingEvent.h @@ -0,0 +1,71 @@ +/* -*- 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/. */ + +#ifndef AudioProcessingEvent_h_ +#define AudioProcessingEvent_h_ + +#include "AudioBuffer.h" +#include "ScriptProcessorNode.h" +#include "mozilla/dom/Event.h" + +namespace mozilla::dom { + +class AudioProcessingEvent final : public Event { + public: + AudioProcessingEvent(ScriptProcessorNode* aOwner, nsPresContext* aPresContext, + WidgetEvent* aEvent); + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(AudioProcessingEvent, Event) + + JSObject* WrapObjectInternal(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + using Event::InitEvent; + void InitEvent(AudioBuffer* aInputBuffer, uint32_t aNumberOfInputChannels, + double aPlaybackTime) { + InitEvent(u"audioprocess"_ns, CanBubble::eNo, Cancelable::eNo); + mInputBuffer = aInputBuffer; + mNumberOfInputChannels = aNumberOfInputChannels; + mPlaybackTime = aPlaybackTime; + } + + double PlaybackTime() const { return mPlaybackTime; } + + AudioBuffer* GetInputBuffer(ErrorResult& aRv) { + if (!mInputBuffer) { + mInputBuffer = LazilyCreateBuffer(mNumberOfInputChannels, aRv); + } + return mInputBuffer; + } + + AudioBuffer* GetOutputBuffer(ErrorResult& aRv) { + if (!mOutputBuffer) { + mOutputBuffer = LazilyCreateBuffer(mNode->NumberOfOutputChannels(), aRv); + } + return mOutputBuffer; + } + + bool HasOutputBuffer() const { return !!mOutputBuffer; } + + protected: + virtual ~AudioProcessingEvent(); + + private: + already_AddRefed<AudioBuffer> LazilyCreateBuffer(uint32_t aNumberOfChannels, + ErrorResult& rv); + + private: + double mPlaybackTime; + RefPtr<AudioBuffer> mInputBuffer; + RefPtr<AudioBuffer> mOutputBuffer; + RefPtr<ScriptProcessorNode> mNode; + uint32_t mNumberOfInputChannels; +}; + +} // namespace mozilla::dom + +#endif diff --git a/dom/media/webaudio/AudioScheduledSourceNode.cpp b/dom/media/webaudio/AudioScheduledSourceNode.cpp new file mode 100644 index 0000000000..c6609def51 --- /dev/null +++ b/dom/media/webaudio/AudioScheduledSourceNode.cpp @@ -0,0 +1,17 @@ +/* -*- 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 "AudioScheduledSourceNode.h" + +namespace mozilla::dom { + +AudioScheduledSourceNode::AudioScheduledSourceNode( + AudioContext* aContext, uint32_t aChannelCount, + ChannelCountMode aChannelCountMode, + ChannelInterpretation aChannelInterpretation) + : AudioNode(aContext, aChannelCount, aChannelCountMode, + aChannelInterpretation) {} +} // namespace mozilla::dom diff --git a/dom/media/webaudio/AudioScheduledSourceNode.h b/dom/media/webaudio/AudioScheduledSourceNode.h new file mode 100644 index 0000000000..0102d4a6e8 --- /dev/null +++ b/dom/media/webaudio/AudioScheduledSourceNode.h @@ -0,0 +1,33 @@ +/* -*- 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/. */ + +#ifndef AudioScheduledSourceNode_h_ +#define AudioScheduledSourceNode_h_ + +#include "AudioNode.h" +#include "mozilla/dom/AudioScheduledSourceNodeBinding.h" + +namespace mozilla::dom { + +class AudioContext; + +class AudioScheduledSourceNode : public AudioNode { + public: + virtual void Start(double aWhen, ErrorResult& aRv) = 0; + virtual void Stop(double aWhen, ErrorResult& aRv) = 0; + + IMPL_EVENT_HANDLER(ended) + + protected: + AudioScheduledSourceNode(AudioContext* aContext, uint32_t aChannelCount, + ChannelCountMode aChannelCountMode, + ChannelInterpretation aChannelInterpretation); + virtual ~AudioScheduledSourceNode() = default; +}; + +} // namespace mozilla::dom + +#endif diff --git a/dom/media/webaudio/AudioWorkletGlobalScope.cpp b/dom/media/webaudio/AudioWorkletGlobalScope.cpp new file mode 100644 index 0000000000..18b0b4fa75 --- /dev/null +++ b/dom/media/webaudio/AudioWorkletGlobalScope.cpp @@ -0,0 +1,358 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 "AudioWorkletGlobalScope.h" + +#include "AudioNodeEngine.h" +#include "AudioNodeTrack.h" +#include "AudioWorkletImpl.h" +#include "jsapi.h" +#include "js/ForOfIterator.h" +#include "js/PropertyAndElement.h" // JS_GetProperty +#include "mozilla/BasePrincipal.h" +#include "mozilla/dom/AudioWorkletGlobalScopeBinding.h" +#include "mozilla/dom/AudioWorkletProcessor.h" +#include "mozilla/dom/BindingCallContext.h" +#include "mozilla/dom/MessagePort.h" +#include "mozilla/dom/StructuredCloneHolder.h" +#include "mozilla/dom/AudioParamDescriptorBinding.h" +#include "nsPrintfCString.h" +#include "nsTHashSet.h" +#include "Tracing.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_INHERITED(AudioWorkletGlobalScope, WorkletGlobalScope, + mNameToProcessorMap); + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(AudioWorkletGlobalScope) +NS_INTERFACE_MAP_END_INHERITING(WorkletGlobalScope) + +NS_IMPL_ADDREF_INHERITED(AudioWorkletGlobalScope, WorkletGlobalScope) +NS_IMPL_RELEASE_INHERITED(AudioWorkletGlobalScope, WorkletGlobalScope) + +AudioWorkletGlobalScope::AudioWorkletGlobalScope(AudioWorkletImpl* aImpl) + : WorkletGlobalScope(aImpl) {} + +AudioWorkletImpl* AudioWorkletGlobalScope::Impl() const { + return static_cast<AudioWorkletImpl*>(mImpl.get()); +} + +bool AudioWorkletGlobalScope::WrapGlobalObject( + JSContext* aCx, JS::MutableHandle<JSObject*> aReflector) { + // |this| is being exposed to JS and content script will soon be running. + // The graph needs a handle on the JSContext so it can interrupt JS. + Impl()->DestinationTrack()->Graph()->NotifyJSContext(aCx); + + JS::RealmOptions options = CreateRealmOptions(); + return AudioWorkletGlobalScope_Binding::Wrap( + aCx, this, this, options, BasePrincipal::Cast(mImpl->Principal()), + aReflector); +} + +void AudioWorkletGlobalScope::RegisterProcessor( + JSContext* aCx, const nsAString& aName, + AudioWorkletProcessorConstructor& aProcessorCtor, ErrorResult& aRv) { + TRACE_COMMENT("AudioWorkletGlobalScope::RegisterProcessor", "%s", + NS_ConvertUTF16toUTF8(aName).get()); + + JS::Rooted<JSObject*> processorConstructor(aCx, + aProcessorCtor.CallableOrNull()); + + /** + * 1. If the name is the empty string, throw a NotSupportedError + * exception and abort these steps because the empty string is not + * a valid key. + */ + if (aName.IsEmpty()) { + aRv.ThrowNotSupportedError("Argument 1 should not be an empty string."); + return; + } + + /** + * 2. If the name exists as a key in the node name to processor + * definition map, throw a NotSupportedError exception and abort + * these steps because registering a definition with a duplicated + * key is not allowed. + */ + if (mNameToProcessorMap.GetWeak(aName)) { + // Duplicate names are not allowed + aRv.ThrowNotSupportedError( + "Argument 1 is invalid: a class with the same name is already " + "registered."); + return; + } + + // We know processorConstructor is callable, so not a WindowProxy or Location. + JS::Rooted<JSObject*> constructorUnwrapped( + aCx, js::CheckedUnwrapStatic(processorConstructor)); + if (!constructorUnwrapped) { + // If the caller's compartment does not have permission to access the + // unwrapped constructor then throw. + aRv.ThrowSecurityError("Constructor cannot be called"); + return; + } + + /** + * 3. If the result of IsConstructor(argument=processorCtor) is false, + * throw a TypeError and abort these steps. + */ + if (!JS::IsConstructor(constructorUnwrapped)) { + aRv.ThrowTypeError<MSG_NOT_CONSTRUCTOR>("Argument 2"); + return; + } + + /** + * 4. Let prototype be the result of Get(O=processorCtor, P="prototype"). + */ + // The .prototype on the constructor passed could be an "expando" of a + // wrapper. So we should get it from wrapper instead of the underlying + // object. + JS::Rooted<JS::Value> prototype(aCx); + if (!JS_GetProperty(aCx, processorConstructor, "prototype", &prototype)) { + aRv.NoteJSContextException(aCx); + return; + } + + /** + * 5. If the result of Type(argument=prototype) is not Object, throw a + * TypeError and abort all these steps. + */ + if (!prototype.isObject()) { + aRv.ThrowTypeError<MSG_NOT_OBJECT>("processorCtor.prototype"); + return; + } + /** + * 6. Let parameterDescriptorsValue be the result of Get(O=processorCtor, + * P="parameterDescriptors"). + */ + JS::Rooted<JS::Value> descriptors(aCx); + if (!JS_GetProperty(aCx, processorConstructor, "parameterDescriptors", + &descriptors)) { + aRv.NoteJSContextException(aCx); + return; + } + + AudioParamDescriptorMap map; + /* + * 7. If parameterDescriptorsValue is not undefined + */ + if (!descriptors.isUndefined()) { + /* + * 7.1. Let parameterDescriptorSequence be the result of the conversion + * from parameterDescriptorsValue to an IDL value of type + * sequence<AudioParamDescriptor>. + */ + JS::Rooted<JS::Value> objectValue(aCx, descriptors); + JS::ForOfIterator iter(aCx); + if (!iter.init(objectValue, JS::ForOfIterator::AllowNonIterable)) { + aRv.NoteJSContextException(aCx); + return; + } + if (!iter.valueIsIterable()) { + aRv.ThrowTypeError<MSG_CONVERSION_ERROR>( + "AudioWorkletProcessor.parameterDescriptors", "sequence"); + return; + } + /* + * 7.2 and 7.3 (and substeps) + */ + map = DescriptorsFromJS(aCx, &iter, aRv); + if (aRv.Failed()) { + return; + } + } + + /** + * 8. Append the key-value pair name → processorCtor to node name to processor + * constructor map of the associated AudioWorkletGlobalScope. + */ + if (!mNameToProcessorMap.InsertOrUpdate(aName, RefPtr{&aProcessorCtor}, + fallible)) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + /** + * 9. Queue a task to the control thread to add the key-value pair + * (name - descriptors) to the node name to parameter descriptor + * map of the associated BaseAudioContext. + */ + NS_DispatchToMainThread(NS_NewRunnableFunction( + "AudioWorkletGlobalScope: parameter descriptors", + [impl = RefPtr{Impl()}, name = nsString(aName), + map = std::move(map)]() mutable { + AudioNode* destinationNode = + impl->DestinationTrack()->Engine()->NodeMainThread(); + if (!destinationNode) { + return; + } + destinationNode->Context()->SetParamMapForWorkletName(name, &map); + })); +} + +uint64_t AudioWorkletGlobalScope::CurrentFrame() const { + AudioNodeTrack* destinationTrack = Impl()->DestinationTrack(); + GraphTime processedTime = destinationTrack->Graph()->ProcessedTime(); + return destinationTrack->GraphTimeToTrackTime(processedTime); +} + +double AudioWorkletGlobalScope::CurrentTime() const { + return static_cast<double>(CurrentFrame()) / SampleRate(); +} + +float AudioWorkletGlobalScope::SampleRate() const { + return static_cast<float>(Impl()->DestinationTrack()->mSampleRate); +} + +AudioParamDescriptorMap AudioWorkletGlobalScope::DescriptorsFromJS( + JSContext* aCx, JS::ForOfIterator* aIter, ErrorResult& aRv) { + AudioParamDescriptorMap res; + // To check for duplicates + nsTHashSet<nsString> namesSet; + + JS::Rooted<JS::Value> nextValue(aCx); + bool done = false; + size_t i = 0; + while (true) { + if (!aIter->next(&nextValue, &done)) { + aRv.NoteJSContextException(aCx); + return AudioParamDescriptorMap(); + } + if (done) { + break; + } + + BindingCallContext callCx(aCx, "AudioWorkletGlobalScope.registerProcessor"); + nsPrintfCString sourceDescription("Element %zu in parameterDescriptors", i); + i++; + AudioParamDescriptor* descriptor = res.AppendElement(fallible); + if (!descriptor) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return AudioParamDescriptorMap(); + } + if (!descriptor->Init(callCx, nextValue, sourceDescription.get())) { + aRv.NoteJSContextException(aCx); + return AudioParamDescriptorMap(); + } + } + + for (const auto& descriptor : res) { + if (namesSet.Contains(descriptor.mName)) { + aRv.ThrowNotSupportedError("Duplicated name \""_ns + + NS_ConvertUTF16toUTF8(descriptor.mName) + + "\" in parameterDescriptors."_ns); + return AudioParamDescriptorMap(); + } + + if (!namesSet.Insert(descriptor.mName, fallible)) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return AudioParamDescriptorMap(); + } + + if (descriptor.mMinValue > descriptor.mMaxValue) { + aRv.ThrowInvalidStateError( + "In parameterDescriptors, "_ns + + NS_ConvertUTF16toUTF8(descriptor.mName) + + " minValue should be smaller than maxValue."_ns); + return AudioParamDescriptorMap(); + } + + if (descriptor.mDefaultValue < descriptor.mMinValue || + descriptor.mDefaultValue > descriptor.mMaxValue) { + aRv.ThrowInvalidStateError( + "In parameterDescriptors, "_ns + + NS_ConvertUTF16toUTF8(descriptor.mName) + + nsLiteralCString(" defaultValue is out of the range defined by " + "minValue and maxValue.")); + return AudioParamDescriptorMap(); + } + } + + return res; +} + +bool AudioWorkletGlobalScope::ConstructProcessor( + JSContext* aCx, const nsAString& aName, + NotNull<StructuredCloneHolder*> aSerializedOptions, + UniqueMessagePortId& aPortIdentifier, + JS::MutableHandle<JSObject*> aRetProcessor) { + TRACE_COMMENT("AudioWorkletProcessor::ConstructProcessor", "%s", + NS_ConvertUTF16toUTF8(aName).get()); + /** + * See + * https://webaudio.github.io/web-audio-api/#AudioWorkletProcessor-instantiation + */ + ErrorResult rv; + /** + * 4. Let deserializedPort be the result of + * StructuredDeserialize(serializedPort, the current Realm). + */ + RefPtr<MessagePort> deserializedPort = + MessagePort::Create(this, aPortIdentifier, rv); + if (NS_WARN_IF(rv.MaybeSetPendingException(aCx))) { + return false; + } + /** + * 5. Let deserializedOptions be the result of + * StructuredDeserialize(serializedOptions, the current Realm). + */ + JS::CloneDataPolicy cloneDataPolicy; + cloneDataPolicy.allowIntraClusterClonableSharedObjects(); + cloneDataPolicy.allowSharedMemoryObjects(); + + JS::Rooted<JS::Value> deserializedOptions(aCx); + aSerializedOptions->Read(this, aCx, &deserializedOptions, cloneDataPolicy, + rv); + if (rv.MaybeSetPendingException(aCx)) { + return false; + } + /** + * 6. Let processorCtor be the result of looking up processorName on the + * AudioWorkletGlobalScope's node name to processor definition map. + */ + RefPtr<AudioWorkletProcessorConstructor> processorCtor = + mNameToProcessorMap.Get(aName); + // AudioWorkletNode has already checked the definition exists. + // See also https://github.com/WebAudio/web-audio-api/issues/1854 + MOZ_ASSERT(processorCtor); + /** + * 7. Store nodeReference and deserializedPort to node reference and + * transferred port of this AudioWorkletGlobalScope's pending processor + * construction data respectively. + */ + // |nodeReference| is not required here because the "processorerror" event + // is thrown by WorkletNodeEngine::ConstructProcessor(). + mPortForProcessor = std::move(deserializedPort); + /** + * 8. Construct a callback function from processorCtor with the argument + * of deserializedOptions. + */ + // The options were an object before serialization and so will be an object + // if deserialization succeeded above. toObject() asserts. + JS::Rooted<JSObject*> options(aCx, &deserializedOptions.toObject()); + RefPtr<AudioWorkletProcessor> processor = processorCtor->Construct( + options, rv, "AudioWorkletProcessor construction", + CallbackFunction::eRethrowExceptions); + // https://github.com/WebAudio/web-audio-api/issues/2096 + mPortForProcessor = nullptr; + if (rv.MaybeSetPendingException(aCx)) { + return false; + } + JS::Rooted<JS::Value> processorVal(aCx); + if (NS_WARN_IF(!ToJSValue(aCx, processor, &processorVal))) { + return false; + } + MOZ_ASSERT(processorVal.isObject()); + aRetProcessor.set(&processorVal.toObject()); + return true; +} + +RefPtr<MessagePort> AudioWorkletGlobalScope::TakePortForProcessorCtor() { + return std::move(mPortForProcessor); +} + +} // namespace mozilla::dom diff --git a/dom/media/webaudio/AudioWorkletGlobalScope.h b/dom/media/webaudio/AudioWorkletGlobalScope.h new file mode 100644 index 0000000000..faaa731fa6 --- /dev/null +++ b/dom/media/webaudio/AudioWorkletGlobalScope.h @@ -0,0 +1,84 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_dom_AudioWorkletGlobalScope_h +#define mozilla_dom_AudioWorkletGlobalScope_h + +#include "mozilla/dom/AudioParamDescriptorMap.h" +#include "mozilla/dom/FunctionBinding.h" +#include "mozilla/dom/WorkletGlobalScope.h" +#include "js/ForOfIterator.h" +#include "nsRefPtrHashtable.h" + +namespace mozilla { + +class AudioWorkletImpl; + +namespace dom { + +class AudioWorkletProcessorConstructor; +class MessagePort; +class StructuredCloneHolder; +class UniqueMessagePortId; + +class AudioWorkletGlobalScope final : public WorkletGlobalScope { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(AudioWorkletGlobalScope, + WorkletGlobalScope); + + explicit AudioWorkletGlobalScope(AudioWorkletImpl* aImpl); + + bool WrapGlobalObject(JSContext* aCx, + JS::MutableHandle<JSObject*> aReflector) override; + + void RegisterProcessor(JSContext* aCx, const nsAString& aName, + AudioWorkletProcessorConstructor& aProcessorCtor, + ErrorResult& aRv); + + AudioWorkletImpl* Impl() const; + + uint64_t CurrentFrame() const; + + double CurrentTime() const; + + float SampleRate() const; + + // If successful, returns true and sets aRetProcessor, which will be in the + // compartment for the realm of this global. Returns false on failure. + MOZ_CAN_RUN_SCRIPT + bool ConstructProcessor(JSContext* aCx, const nsAString& aName, + NotNull<StructuredCloneHolder*> aSerializedOptions, + UniqueMessagePortId& aPortIdentifier, + JS::MutableHandle<JSObject*> aRetProcessor); + + // Returns null if not called during ConstructProcessor() or if the port has + // already been taken. + RefPtr<MessagePort> TakePortForProcessorCtor(); + + private: + ~AudioWorkletGlobalScope() = default; + + // Returns an AudioParamDescriptorMap filled with AudioParamDescriptor + // objects, extracted from JS. Returns an empty map in case of error and set + // aRv accordingly. + AudioParamDescriptorMap DescriptorsFromJS(JSContext* aCx, + JS::ForOfIterator* aIter, + ErrorResult& aRv); + + typedef nsRefPtrHashtable<nsStringHashKey, AudioWorkletProcessorConstructor> + NodeNameToProcessorDefinitionMap; + NodeNameToProcessorDefinitionMap mNameToProcessorMap; + // https://webaudio.github.io/web-audio-api/#pending-processor-construction-data-transferred-port + // This does not need to be traversed during cycle-collection because it is + // only set while this AudioWorkletGlobalScope is on the stack. + RefPtr<MessagePort> mPortForProcessor; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_AudioWorkletGlobalScope_h diff --git a/dom/media/webaudio/AudioWorkletImpl.cpp b/dom/media/webaudio/AudioWorkletImpl.cpp new file mode 100644 index 0000000000..5eaa128c8a --- /dev/null +++ b/dom/media/webaudio/AudioWorkletImpl.cpp @@ -0,0 +1,88 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 https://mozilla.org/MPL/2.0/. */ + +#include "AudioWorkletImpl.h" + +#include "AudioContext.h" +#include "AudioNodeTrack.h" +#include "GeckoProfiler.h" +#include "mozilla/dom/AudioWorkletBinding.h" +#include "mozilla/dom/AudioWorkletGlobalScope.h" +#include "mozilla/dom/Worklet.h" +#include "mozilla/dom/WorkletThread.h" +#include "nsGlobalWindowInner.h" + +namespace mozilla { + +/* static */ already_AddRefed<dom::Worklet> AudioWorkletImpl::CreateWorklet( + dom::AudioContext* aContext, ErrorResult& aRv) { + MOZ_ASSERT(NS_IsMainThread()); + + nsCOMPtr<nsPIDOMWindowInner> window = aContext->GetOwner(); + if (NS_WARN_IF(!window)) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + nsCOMPtr<nsIPrincipal> principal = + nsGlobalWindowInner::Cast(window)->GetPrincipal(); + if (NS_WARN_IF(!principal)) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + RefPtr<AudioWorkletImpl> impl = + new AudioWorkletImpl(window, principal, aContext->DestinationTrack()); + + // The Worklet owns a reference to the AudioContext so as to keep the graph + // thread running as long as the Worklet is alive by keeping the + // AudioDestinationNode alive. + return MakeAndAddRef<dom::Worklet>(window, std::move(impl), + ToSupports(aContext)); +} + +AudioWorkletImpl::AudioWorkletImpl(nsPIDOMWindowInner* aWindow, + nsIPrincipal* aPrincipal, + AudioNodeTrack* aDestinationTrack) + : WorkletImpl(aWindow, aPrincipal), mDestinationTrack(aDestinationTrack) {} + +AudioWorkletImpl::~AudioWorkletImpl() = default; + +JSObject* AudioWorkletImpl::WrapWorklet(JSContext* aCx, dom::Worklet* aWorklet, + JS::Handle<JSObject*> aGivenProto) { + MOZ_ASSERT(NS_IsMainThread()); + return dom::AudioWorklet_Binding::Wrap(aCx, aWorklet, aGivenProto); +} + +nsresult AudioWorkletImpl::SendControlMessage( + already_AddRefed<nsIRunnable> aRunnable) { + mDestinationTrack->SendRunnable(std::move(aRunnable)); + return NS_OK; +} + +void AudioWorkletImpl::OnAddModuleStarted() const { +#ifdef MOZ_GECKO_PROFILER + profiler_add_marker(ProfilerStringView("AudioWorklet.addModule"), + geckoprofiler::category::MEDIA_RT, + {MarkerTiming::IntervalStart()}); +#endif +} + +void AudioWorkletImpl::OnAddModulePromiseSettled() const { +#ifdef MOZ_GECKO_PROFILER + profiler_add_marker(ProfilerStringView("AudioWorklet.addModule"), + geckoprofiler::category::MEDIA_RT, + {MarkerTiming::IntervalEnd()}); +#endif +} + +already_AddRefed<dom::WorkletGlobalScope> +AudioWorkletImpl::ConstructGlobalScope() { + dom::WorkletThread::AssertIsOnWorkletThread(); + + return MakeAndAddRef<dom::AudioWorkletGlobalScope>(this); +} + +} // namespace mozilla diff --git a/dom/media/webaudio/AudioWorkletImpl.h b/dom/media/webaudio/AudioWorkletImpl.h new file mode 100644 index 0000000000..39f347fdb2 --- /dev/null +++ b/dom/media/webaudio/AudioWorkletImpl.h @@ -0,0 +1,63 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 https://mozilla.org/MPL/2.0/. */ + +#ifndef AudioWorkletImpl_h +#define AudioWorkletImpl_h + +#include "mozilla/dom/WorkletImpl.h" +#include "mozilla/dom/AudioWorkletGlobalScope.h" + +namespace mozilla { + +class AudioNodeTrack; + +namespace dom { +class AudioContext; +} + +class AudioWorkletImpl final : public WorkletImpl { + public: + // Methods for parent thread only: + + static already_AddRefed<dom::Worklet> CreateWorklet( + dom::AudioContext* aContext, ErrorResult& aRv); + + JSObject* WrapWorklet(JSContext* aCx, dom::Worklet* aWorklet, + JS::Handle<JSObject*> aGivenProto) override; + + nsresult SendControlMessage(already_AddRefed<nsIRunnable> aRunnable) override; + + nsContentPolicyType ContentPolicyType() const override { + return nsIContentPolicy::TYPE_INTERNAL_AUDIOWORKLET; + } + + // Execution thread only. + dom::AudioWorkletGlobalScope* GetGlobalScope() { + return static_cast<dom::AudioWorkletGlobalScope*>( + WorkletImpl::GetGlobalScope()); + } + + // Any thread: + AudioNodeTrack* DestinationTrack() const { return mDestinationTrack; } + + void OnAddModuleStarted() const override; + void OnAddModulePromiseSettled() const override; + + protected: + // Execution thread only. + already_AddRefed<dom::WorkletGlobalScope> ConstructGlobalScope() override; + + private: + AudioWorkletImpl(nsPIDOMWindowInner* aWindow, nsIPrincipal* aPrincipal, + AudioNodeTrack* aDestinationTrack); + ~AudioWorkletImpl(); + + const RefPtr<AudioNodeTrack> mDestinationTrack; +}; + +} // namespace mozilla + +#endif // AudioWorkletImpl_h diff --git a/dom/media/webaudio/AudioWorkletNode.cpp b/dom/media/webaudio/AudioWorkletNode.cpp new file mode 100644 index 0000000000..76a3185f2c --- /dev/null +++ b/dom/media/webaudio/AudioWorkletNode.cpp @@ -0,0 +1,899 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 https://mozilla.org/MPL/2.0/. */ + +#include "AudioWorkletNode.h" + +#include "AudioNodeEngine.h" +#include "AudioParamMap.h" +#include "AudioWorkletImpl.h" +#include "js/Array.h" // JS::{Get,Set}ArrayLength, JS::NewArrayLength +#include "js/CallAndConstruct.h" // JS::Call, JS::IsCallable +#include "js/Exception.h" +#include "js/experimental/TypedData.h" // JS_NewFloat32Array, JS_GetFloat32ArrayData, JS_GetTypedArrayLength, JS_GetArrayBufferViewBuffer +#include "js/PropertyAndElement.h" // JS_DefineElement, JS_DefineUCProperty, JS_GetProperty +#include "mozilla/dom/AudioWorkletNodeBinding.h" +#include "mozilla/dom/AudioParamMapBinding.h" +#include "mozilla/dom/AutoEntryScript.h" +#include "mozilla/dom/RootedDictionary.h" +#include "mozilla/dom/ErrorEvent.h" +#include "mozilla/dom/Worklet.h" +#include "nsIScriptGlobalObject.h" +#include "AudioParam.h" +#include "AudioDestinationNode.h" +#include "mozilla/dom/MessageChannel.h" +#include "mozilla/dom/MessagePort.h" +#include "mozilla/ScopeExit.h" +#include "nsReadableUtils.h" +#include "mozilla/Span.h" +#include "PlayingRefChangeHandler.h" +#include "nsPrintfCString.h" +#include "Tracing.h" + +namespace mozilla::dom { + +NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(AudioWorkletNode, AudioNode) +NS_IMPL_CYCLE_COLLECTION_INHERITED(AudioWorkletNode, AudioNode, mPort, + mParameters) + +struct NamedAudioParamTimeline { + explicit NamedAudioParamTimeline(const AudioParamDescriptor& aParamDescriptor) + : mName(aParamDescriptor.mName), + mTimeline(aParamDescriptor.mDefaultValue) {} + const nsString mName; + AudioParamTimeline mTimeline; +}; + +struct ProcessorErrorDetails { + ProcessorErrorDetails() : mLineno(0), mColno(0) {} + // Line number (1-origin). + unsigned mLineno; + // Column number in UTF-16 code units (1-origin). + unsigned mColno; + nsString mFilename; + nsString mMessage; +}; + +class WorkletNodeEngine final : public AudioNodeEngine { + public: + WorkletNodeEngine(AudioWorkletNode* aNode, + AudioDestinationNode* aDestinationNode, + nsTArray<NamedAudioParamTimeline>&& aParamTimelines, + const Optional<Sequence<uint32_t>>& aOutputChannelCount) + : AudioNodeEngine(aNode), + mDestination(aDestinationNode->Track()), + mParamTimelines(std::move(aParamTimelines)) { + if (aOutputChannelCount.WasPassed()) { + mOutputChannelCount = aOutputChannelCount.Value(); + } + } + + MOZ_CAN_RUN_SCRIPT + void ConstructProcessor(AudioWorkletImpl* aWorkletImpl, + const nsAString& aName, + NotNull<StructuredCloneHolder*> aSerializedOptions, + UniqueMessagePortId& aPortIdentifier, + AudioNodeTrack* aTrack); + + void RecvTimelineEvent(uint32_t aIndex, AudioParamEvent& aEvent) override { + MOZ_ASSERT(mDestination); + aEvent.ConvertToTicks(mDestination); + + if (aIndex < mParamTimelines.Length()) { + mParamTimelines[aIndex].mTimeline.InsertEvent<int64_t>(aEvent); + } else { + NS_ERROR("Bad WorkletNodeEngine timeline event index"); + } + } + + void ProcessBlock(AudioNodeTrack* aTrack, GraphTime aFrom, + const AudioBlock& aInput, AudioBlock* aOutput, + bool* aFinished) override { + MOZ_ASSERT(InputCount() <= 1); + MOZ_ASSERT(OutputCount() <= 1); + TRACE("WorkletNodeEngine::ProcessBlock"); + ProcessBlocksOnPorts(aTrack, aFrom, Span(&aInput, InputCount()), + Span(aOutput, OutputCount()), aFinished); + } + + void ProcessBlocksOnPorts(AudioNodeTrack* aTrack, GraphTime aFrom, + Span<const AudioBlock> aInput, + Span<AudioBlock> aOutput, bool* aFinished) override; + + void OnGraphThreadDone() override { ReleaseJSResources(); } + + bool IsActive() const override { return mKeepEngineActive; } + + // Vector<T> supports non-memmovable types such as PersistentRooted + // (without any need to jump through hoops like + // MOZ_DECLARE_RELOCATE_USING_MOVE_CONSTRUCTOR_FOR_TEMPLATE for nsTArray). + // PersistentRooted is used because these AudioWorkletGlobalScope scope + // objects may be kept alive as long as the AudioWorkletNode in the + // main-thread global. + struct Channels { + Vector<JS::PersistentRooted<JSObject*>, GUESS_AUDIO_CHANNELS> + mFloat32Arrays; + JS::PersistentRooted<JSObject*> mJSArray; + // For SetArrayElements(): + operator JS::Handle<JSObject*>() const { return mJSArray; } + }; + struct Ports { + Vector<Channels, 1> mPorts; + JS::PersistentRooted<JSObject*> mJSArray; + }; + struct ParameterValues { + Vector<JS::PersistentRooted<JSObject*>> mFloat32Arrays; + JS::PersistentRooted<JSObject*> mJSObject; + }; + + private: + size_t ParameterCount() { return mParamTimelines.Length(); } + void SendProcessorError(AudioNodeTrack* aTrack, JSContext* aCx); + bool CallProcess(AudioNodeTrack* aTrack, JSContext* aCx, + JS::Handle<JS::Value> aCallable); + void ProduceSilence(AudioNodeTrack* aTrack, Span<AudioBlock> aOutput); + void SendErrorToMainThread(AudioNodeTrack* aTrack, + const ProcessorErrorDetails& aDetails); + + void ReleaseJSResources() { + mInputs.mPorts.clearAndFree(); + mOutputs.mPorts.clearAndFree(); + mParameters.mFloat32Arrays.clearAndFree(); + mInputs.mJSArray.reset(); + mOutputs.mJSArray.reset(); + mParameters.mJSObject.reset(); + mGlobal = nullptr; + // This is equivalent to setting [[callable process]] to false. + mProcessor.reset(); + } + + nsCString mProcessorName; + RefPtr<AudioNodeTrack> mDestination; + nsTArray<uint32_t> mOutputChannelCount; + nsTArray<NamedAudioParamTimeline> mParamTimelines; + // The AudioWorkletGlobalScope-associated objects referenced from + // WorkletNodeEngine are typically kept alive as long as the + // AudioWorkletNode in the main-thread global. The objects must be released + // on the rendering thread, which usually happens simply because + // AudioWorkletNode is such that the last AudioNodeTrack reference is + // released by the MTG. That occurs on the rendering thread except during + // process shutdown, in which case NotifyForcedShutdown() is called on the + // rendering thread. + // + // mInputs, mOutputs and mParameters keep references to all objects passed to + // process(), for reuse of the same objects. The JS objects are all in the + // compartment of the realm of mGlobal. Properties on the objects may be + // replaced by script, so don't assume that getting indexed properties on the + // JS arrays will return the same objects. Only objects and buffers created + // by the implementation are modified or read by the implementation. + Ports mInputs; + Ports mOutputs; + ParameterValues mParameters; + + RefPtr<AudioWorkletGlobalScope> mGlobal; + JS::PersistentRooted<JSObject*> mProcessor; + + // mProcessorIsActive is named [[active source]] in the spec. + // It is initially true and so at least the first process() + // call will not be skipped when there are no active inputs. + bool mProcessorIsActive = true; + // mKeepEngineActive ensures another call to ProcessBlocksOnPorts(), even if + // there are no active inputs. Its transitions to false lag those of + // mProcessorIsActive by one call to ProcessBlocksOnPorts() so that + // downstream engines can addref their nodes before this engine's node is + // released. + bool mKeepEngineActive = true; +}; + +void WorkletNodeEngine::SendErrorToMainThread( + AudioNodeTrack* aTrack, const ProcessorErrorDetails& aDetails) { + RefPtr<AudioNodeTrack> track = aTrack; + NS_DispatchToMainThread(NS_NewRunnableFunction( + "WorkletNodeEngine::SendProcessorError", + [track = std::move(track), aDetails]() mutable { + AudioWorkletNode* node = + static_cast<AudioWorkletNode*>(track->Engine()->NodeMainThread()); + if (!node) { + return; + } + node->DispatchProcessorErrorEvent(aDetails); + })); +} + +void WorkletNodeEngine::SendProcessorError(AudioNodeTrack* aTrack, + JSContext* aCx) { + // Note that once an exception is thrown, the processor will output silence + // throughout its lifetime. + ReleaseJSResources(); + // The processor errored out while getting a context, try to tell the node + // anyways. + if (!aCx || !JS_IsExceptionPending(aCx)) { + ProcessorErrorDetails details; + details.mMessage.Assign(u"Unknown processor error"); + SendErrorToMainThread(aTrack, details); + return; + } + + JS::ExceptionStack exnStack(aCx); + if (JS::StealPendingExceptionStack(aCx, &exnStack)) { + JS::ErrorReportBuilder jsReport(aCx); + if (!jsReport.init(aCx, exnStack, + JS::ErrorReportBuilder::WithSideEffects)) { + ProcessorErrorDetails details; + details.mMessage.Assign(u"Unknown processor error"); + SendErrorToMainThread(aTrack, details); + // Set the exception and stack back to have it in the console with a stack + // trace. + JS::SetPendingExceptionStack(aCx, exnStack); + return; + } + + ProcessorErrorDetails details; + + CopyUTF8toUTF16( + mozilla::MakeStringSpan(jsReport.report()->filename.c_str()), + details.mFilename); + + xpc::ErrorReport::ErrorReportToMessageString(jsReport.report(), + details.mMessage); + details.mLineno = jsReport.report()->lineno; + details.mColno = jsReport.report()->column.oneOriginValue(); + MOZ_ASSERT(!jsReport.report()->isMuted); + + SendErrorToMainThread(aTrack, details); + + // Set the exception and stack back to have it in the console with a stack + // trace. + JS::SetPendingExceptionStack(aCx, exnStack); + } else { + NS_WARNING("No exception, but processor errored out?"); + } +} + +void WorkletNodeEngine::ConstructProcessor( + AudioWorkletImpl* aWorkletImpl, const nsAString& aName, + NotNull<StructuredCloneHolder*> aSerializedOptions, + UniqueMessagePortId& aPortIdentifier, AudioNodeTrack* aTrack) { + MOZ_ASSERT(mInputs.mPorts.empty() && mOutputs.mPorts.empty()); + RefPtr<AudioWorkletGlobalScope> global = aWorkletImpl->GetGlobalScope(); + if (!global) { + // A global was previously used to register this kind of processor. If it + // no longer exists now, that is because the document is going away and so + // there is no need to send an error. + return; + } + AutoJSAPI api; + if (NS_WARN_IF(!api.Init(global))) { + SendProcessorError(aTrack, nullptr); + return; + } + mProcessorName = NS_ConvertUTF16toUTF8(aName); + JSContext* cx = api.cx(); + mProcessor.init(cx); + if (!global->ConstructProcessor(cx, aName, aSerializedOptions, + aPortIdentifier, &mProcessor) || + // mInputs and mOutputs outer arrays are fixed length and so much of the + // initialization need only be performed once (i.e. here). + NS_WARN_IF(!mInputs.mPorts.growBy(InputCount())) || + NS_WARN_IF(!mOutputs.mPorts.growBy(OutputCount()))) { + SendProcessorError(aTrack, cx); + return; + } + mGlobal = std::move(global); + mInputs.mJSArray.init(cx); + mOutputs.mJSArray.init(cx); + for (auto& port : mInputs.mPorts) { + port.mJSArray.init(cx); + } + for (auto& port : mOutputs.mPorts) { + port.mJSArray.init(cx); + } + JSObject* object = JS_NewPlainObject(cx); + if (NS_WARN_IF(!object)) { + SendProcessorError(aTrack, cx); + return; + } + + mParameters.mJSObject.init(cx, object); + if (NS_WARN_IF(!mParameters.mFloat32Arrays.growBy(ParameterCount()))) { + SendProcessorError(aTrack, cx); + return; + } + for (size_t i = 0; i < mParamTimelines.Length(); i++) { + auto& float32ArraysRef = mParameters.mFloat32Arrays; + float32ArraysRef[i].init(cx); + JSObject* array = JS_NewFloat32Array(cx, WEBAUDIO_BLOCK_SIZE); + if (NS_WARN_IF(!array)) { + SendProcessorError(aTrack, cx); + return; + } + + float32ArraysRef[i] = array; + if (NS_WARN_IF(!JS_DefineUCProperty( + cx, mParameters.mJSObject, mParamTimelines[i].mName.get(), + mParamTimelines[i].mName.Length(), float32ArraysRef[i], + JSPROP_ENUMERATE))) { + SendProcessorError(aTrack, cx); + return; + } + } + if (NS_WARN_IF(!JS_FreezeObject(cx, mParameters.mJSObject))) { + SendProcessorError(aTrack, cx); + return; + } +} + +// Type T should support the length() and operator[]() methods and the return +// type of |operator[]() const| should support conversion to Handle<JSObject*>. +template <typename T> +static bool SetArrayElements(JSContext* aCx, const T& aElements, + JS::Handle<JSObject*> aArray) { + for (size_t i = 0; i < aElements.length(); ++i) { + if (!JS_DefineElement(aCx, aArray, i, aElements[i], JSPROP_ENUMERATE)) { + return false; + } + } + + return true; +} + +template <typename T> +static bool PrepareArray(JSContext* aCx, const T& aElements, + JS::MutableHandle<JSObject*> aArray) { + size_t length = aElements.length(); + if (aArray) { + // Attempt to reuse. + uint32_t oldLength; + if (JS::GetArrayLength(aCx, aArray, &oldLength) && + (oldLength == length || JS::SetArrayLength(aCx, aArray, length)) && + SetArrayElements(aCx, aElements, aArray)) { + return true; + } + // Script may have frozen the array. Try again with a new Array. + JS_ClearPendingException(aCx); + } + JSObject* array = JS::NewArrayObject(aCx, length); + if (NS_WARN_IF(!array)) { + return false; + } + aArray.set(array); + return SetArrayElements(aCx, aElements, aArray); +} + +enum class ArrayElementInit { None, Zero }; + +// Exactly when to create new Float32Array and Array objects is not specified. +// This approach aims to minimize garbage creation, while continuing to +// function after objects are modified by content. +// See https://github.com/WebAudio/web-audio-api/issues/1934 and +// https://github.com/WebAudio/web-audio-api/issues/1933 +static bool PrepareBufferArrays(JSContext* aCx, Span<const AudioBlock> aBlocks, + WorkletNodeEngine::Ports* aPorts, + ArrayElementInit aInit) { + MOZ_ASSERT(aBlocks.Length() == aPorts->mPorts.length()); + for (size_t i = 0; i < aBlocks.Length(); ++i) { + size_t channelCount = aBlocks[i].ChannelCount(); + WorkletNodeEngine::Channels& portRef = aPorts->mPorts[i]; + + auto& float32ArraysRef = portRef.mFloat32Arrays; + for (auto& channelRef : float32ArraysRef) { + size_t length = JS_GetTypedArrayLength(channelRef); + if (length != WEBAUDIO_BLOCK_SIZE) { + // Script has detached array buffers. Create new objects. + JSObject* array = JS_NewFloat32Array(aCx, WEBAUDIO_BLOCK_SIZE); + if (NS_WARN_IF(!array)) { + return false; + } + channelRef = array; + } else if (aInit == ArrayElementInit::Zero) { + // Need only zero existing arrays as new arrays are already zeroed. + JS::AutoCheckCannotGC nogc; + bool isShared; + float* elementData = + JS_GetFloat32ArrayData(channelRef, &isShared, nogc); + MOZ_ASSERT(!isShared); // Was created as unshared + std::fill_n(elementData, WEBAUDIO_BLOCK_SIZE, 0.0f); + } + } + // Enlarge if necessary... + if (NS_WARN_IF(!float32ArraysRef.reserve(channelCount))) { + return false; + } + while (float32ArraysRef.length() < channelCount) { + JSObject* array = JS_NewFloat32Array(aCx, WEBAUDIO_BLOCK_SIZE); + if (NS_WARN_IF(!array)) { + return false; + } + float32ArraysRef.infallibleEmplaceBack(aCx, array); + } + // ... or shrink if necessary. + float32ArraysRef.shrinkTo(channelCount); + + if (NS_WARN_IF(!PrepareArray(aCx, float32ArraysRef, &portRef.mJSArray))) { + return false; + } + } + + return !(NS_WARN_IF(!PrepareArray(aCx, aPorts->mPorts, &aPorts->mJSArray))); +} + +// This runs JS script. MediaTrackGraph control messages, which would +// potentially destroy the WorkletNodeEngine and its AudioNodeTrack, cannot +// be triggered by script. They are not run from an nsIThread event loop and +// do not run until after ProcessBlocksOnPorts() has returned. +bool WorkletNodeEngine::CallProcess(AudioNodeTrack* aTrack, JSContext* aCx, + JS::Handle<JS::Value> aCallable) { + TRACE_COMMENT("AudioWorkletNodeEngine::CallProcess", mProcessorName.get()); + + JS::RootedVector<JS::Value> argv(aCx); + if (NS_WARN_IF(!argv.resize(3))) { + return false; + } + argv[0].setObject(*mInputs.mJSArray); + argv[1].setObject(*mOutputs.mJSArray); + argv[2].setObject(*mParameters.mJSObject); + JS::Rooted<JS::Value> rval(aCx); + if (!JS::Call(aCx, mProcessor, aCallable, argv, &rval)) { + return false; + } + + mProcessorIsActive = JS::ToBoolean(rval); + // Transitions of mProcessorIsActive to false do not trigger + // PlayingRefChangeHandler::RELEASE until silence is produced in the next + // block. This allows downstream engines receiving this non-silence block + // to take a reference to their nodes before this engine's node releases its + // down node references. + if (mProcessorIsActive && !mKeepEngineActive) { + mKeepEngineActive = true; + RefPtr<PlayingRefChangeHandler> refchanged = + new PlayingRefChangeHandler(aTrack, PlayingRefChangeHandler::ADDREF); + aTrack->Graph()->DispatchToMainThreadStableState(refchanged.forget()); + } + return true; +} + +void WorkletNodeEngine::ProduceSilence(AudioNodeTrack* aTrack, + Span<AudioBlock> aOutput) { + if (mKeepEngineActive) { + mKeepEngineActive = false; + aTrack->ScheduleCheckForInactive(); + RefPtr<PlayingRefChangeHandler> refchanged = + new PlayingRefChangeHandler(aTrack, PlayingRefChangeHandler::RELEASE); + aTrack->Graph()->DispatchToMainThreadStableState(refchanged.forget()); + } + for (AudioBlock& output : aOutput) { + output.SetNull(WEBAUDIO_BLOCK_SIZE); + } +} + +void WorkletNodeEngine::ProcessBlocksOnPorts(AudioNodeTrack* aTrack, + GraphTime aFrom, + Span<const AudioBlock> aInput, + Span<AudioBlock> aOutput, + bool* aFinished) { + MOZ_ASSERT(aInput.Length() == InputCount()); + MOZ_ASSERT(aOutput.Length() == OutputCount()); + TRACE("WorkletNodeEngine::ProcessBlocksOnPorts"); + + bool isSilent = true; + if (mProcessor) { + if (mProcessorIsActive) { + isSilent = false; // call process() + } else { // [[active source]] is false. + // Call process() only if an input is actively processing. + for (const AudioBlock& input : aInput) { + if (!input.IsNull()) { + isSilent = false; + break; + } + } + } + } + if (isSilent) { + ProduceSilence(aTrack, aOutput); + return; + } + + if (!mOutputChannelCount.IsEmpty()) { + MOZ_ASSERT(mOutputChannelCount.Length() == aOutput.Length()); + for (size_t o = 0; o < aOutput.Length(); ++o) { + aOutput[o].AllocateChannels(mOutputChannelCount[o]); + } + } else if (aInput.Length() == 1 && aOutput.Length() == 1) { + uint32_t channelCount = std::max(aInput[0].ChannelCount(), 1U); + aOutput[0].AllocateChannels(channelCount); + } else { + for (AudioBlock& output : aOutput) { + output.AllocateChannels(1); + } + } + + AutoEntryScript aes(mGlobal, "Worklet Process"); + JSContext* cx = aes.cx(); + auto produceSilenceWithError = MakeScopeExit([this, aTrack, cx, &aOutput] { + SendProcessorError(aTrack, cx); + ProduceSilence(aTrack, aOutput); + }); + + JS::Rooted<JS::Value> process(cx); + if (!JS_GetProperty(cx, mProcessor, "process", &process) || + !process.isObject() || !JS::IsCallable(&process.toObject()) || + !PrepareBufferArrays(cx, aInput, &mInputs, ArrayElementInit::None) || + !PrepareBufferArrays(cx, aOutput, &mOutputs, ArrayElementInit::Zero)) { + // process() not callable or OOM. + return; + } + + // Copy input values to JS objects. + for (size_t i = 0; i < aInput.Length(); ++i) { + const AudioBlock& input = aInput[i]; + size_t channelCount = input.ChannelCount(); + if (channelCount == 0) { + // Null blocks have AUDIO_FORMAT_SILENCE. + // Don't call ChannelData<float>(). + continue; + } + float volume = input.mVolume; + const auto& channelData = input.ChannelData<float>(); + const auto& float32Arrays = mInputs.mPorts[i].mFloat32Arrays; + JS::AutoCheckCannotGC nogc; + for (size_t c = 0; c < channelCount; ++c) { + bool isShared; + float* dest = JS_GetFloat32ArrayData(float32Arrays[c], &isShared, nogc); + MOZ_ASSERT(!isShared); // Was created as unshared + AudioBlockCopyChannelWithScale(channelData[c], volume, dest); + } + } + + TrackTime tick = mDestination->GraphTimeToTrackTime(aFrom); + // Compute and copy parameter values to JS objects. + for (size_t i = 0; i < mParamTimelines.Length(); ++i) { + const auto& float32Arrays = mParameters.mFloat32Arrays[i]; + size_t length = JS_GetTypedArrayLength(float32Arrays); + + // If the Float32Array that is supposed to hold the values for a particular + // AudioParam has been detached, error out. This is being worked on in + // https://github.com/WebAudio/web-audio-api/issues/1933 and + // https://bugzilla.mozilla.org/show_bug.cgi?id=1619486 + if (length != WEBAUDIO_BLOCK_SIZE) { + return; + } + JS::AutoCheckCannotGC nogc; + bool isShared; + float* dest = JS_GetFloat32ArrayData(float32Arrays, &isShared, nogc); + MOZ_ASSERT(!isShared); // Was created as unshared + + size_t frames = + mParamTimelines[i].mTimeline.HasSimpleValue() ? 1 : WEBAUDIO_BLOCK_SIZE; + mParamTimelines[i].mTimeline.GetValuesAtTime(tick, dest, frames); + // https://bugzilla.mozilla.org/show_bug.cgi?id=1616599 + if (frames == 1) { + std::fill_n(dest + 1, WEBAUDIO_BLOCK_SIZE - 1, dest[0]); + } + } + + if (!CallProcess(aTrack, cx, process)) { + // An exception occurred. + /** + * https://webaudio.github.io/web-audio-api/#dom-audioworkletnode-onprocessorerror + * Note that once an exception is thrown, the processor will output silence + * throughout its lifetime. + */ + return; + } + + // Copy output values from JS objects. + for (size_t o = 0; o < aOutput.Length(); ++o) { + AudioBlock* output = &aOutput[o]; + size_t channelCount = output->ChannelCount(); + const auto& float32Arrays = mOutputs.mPorts[o].mFloat32Arrays; + for (size_t c = 0; c < channelCount; ++c) { + size_t length = JS_GetTypedArrayLength(float32Arrays[c]); + if (length != WEBAUDIO_BLOCK_SIZE) { + // ArrayBuffer has been detached. Behavior is unspecified. + // https://github.com/WebAudio/web-audio-api/issues/1933 and + // https://bugzilla.mozilla.org/show_bug.cgi?id=1619486 + return; + } + JS::AutoCheckCannotGC nogc; + bool isShared; + const float* src = + JS_GetFloat32ArrayData(float32Arrays[c], &isShared, nogc); + MOZ_ASSERT(!isShared); // Was created as unshared + PodCopy(output->ChannelFloatsForWrite(c), src, WEBAUDIO_BLOCK_SIZE); + } + } + + produceSilenceWithError.release(); // have output and no error +} + +AudioWorkletNode::AudioWorkletNode(AudioContext* aAudioContext, + const nsAString& aName, + const AudioWorkletNodeOptions& aOptions) + : AudioNode(aAudioContext, 2, ChannelCountMode::Max, + ChannelInterpretation::Speakers), + mNodeName(aName), + mInputCount(aOptions.mNumberOfInputs), + mOutputCount(aOptions.mNumberOfOutputs) {} + +void AudioWorkletNode::InitializeParameters( + nsTArray<NamedAudioParamTimeline>* aParamTimelines, ErrorResult& aRv) { + MOZ_ASSERT(!mParameters, "Only initialize the `parameters` attribute once."); + MOZ_ASSERT(aParamTimelines); + + AudioContext* context = Context(); + const AudioParamDescriptorMap* parameterDescriptors = + context->GetParamMapForWorkletName(mNodeName); + MOZ_ASSERT(parameterDescriptors); + + size_t audioParamIndex = 0; + aParamTimelines->SetCapacity(parameterDescriptors->Length()); + + for (size_t i = 0; i < parameterDescriptors->Length(); i++) { + auto& paramEntry = (*parameterDescriptors)[i]; + CreateAudioParam(audioParamIndex++, paramEntry.mName, + paramEntry.mDefaultValue, paramEntry.mMinValue, + paramEntry.mMaxValue); + aParamTimelines->AppendElement(paramEntry); + } +} + +void AudioWorkletNode::SendParameterData( + const Optional<Record<nsString, double>>& aParameterData) { + MOZ_ASSERT(mTrack, "This method only works if the track has been created."); + nsAutoString name; + if (aParameterData.WasPassed()) { + const auto& paramData = aParameterData.Value(); + for (const auto& paramDataEntry : paramData.Entries()) { + for (auto& audioParam : mParams) { + audioParam->GetName(name); + if (paramDataEntry.mKey.Equals(name)) { + audioParam->SetInitialValue(paramDataEntry.mValue); + } + } + } + } +} + +/* static */ +already_AddRefed<AudioWorkletNode> AudioWorkletNode::Constructor( + const GlobalObject& aGlobal, AudioContext& aAudioContext, + const nsAString& aName, const AudioWorkletNodeOptions& aOptions, + ErrorResult& aRv) { + TRACE_COMMENT("AudioWorkletNode::Constructor", "%s", + NS_ConvertUTF16toUTF8(aName).get()); + /** + * 1. If nodeName does not exist as a key in the BaseAudioContext’s node + * name to parameter descriptor map, throw a InvalidStateError exception + * and abort these steps. + */ + const AudioParamDescriptorMap* parameterDescriptors = + aAudioContext.GetParamMapForWorkletName(aName); + if (!parameterDescriptors) { + // Not using nsPrintfCString in case aName has embedded nulls. + aRv.ThrowInvalidStateError("Unknown AudioWorklet name '"_ns + + NS_ConvertUTF16toUTF8(aName) + "'"_ns); + return nullptr; + } + + // See https://github.com/WebAudio/web-audio-api/issues/2074 for ordering. + RefPtr<AudioWorkletNode> audioWorkletNode = + new AudioWorkletNode(&aAudioContext, aName, aOptions); + audioWorkletNode->Initialize(aOptions, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + /** + * 3. Configure input, output and output channels of node with options. + */ + if (aOptions.mNumberOfInputs == 0 && aOptions.mNumberOfOutputs == 0) { + aRv.ThrowNotSupportedError( + "Must have nonzero numbers of inputs or outputs"); + return nullptr; + } + + if (aOptions.mOutputChannelCount.WasPassed()) { + /** + * 1. If any value in outputChannelCount is zero or greater than the + * implementation’s maximum number of channels, throw a + * NotSupportedError and abort the remaining steps. + */ + for (uint32_t channelCount : aOptions.mOutputChannelCount.Value()) { + if (channelCount == 0 || channelCount > WebAudioUtils::MaxChannelCount) { + aRv.ThrowNotSupportedError( + nsPrintfCString("Channel count (%u) must be in the range [1, max " + "supported channel count]", + channelCount)); + return nullptr; + } + } + /** + * 2. If the length of outputChannelCount does not equal numberOfOutputs, + * throw an IndexSizeError and abort the remaining steps. + */ + if (aOptions.mOutputChannelCount.Value().Length() != + aOptions.mNumberOfOutputs) { + aRv.ThrowIndexSizeError( + nsPrintfCString("Length of outputChannelCount (%zu) does not match " + "numberOfOutputs (%u)", + aOptions.mOutputChannelCount.Value().Length(), + aOptions.mNumberOfOutputs)); + return nullptr; + } + } + // MTG does not support more than UINT16_MAX inputs or outputs. + if (aOptions.mNumberOfInputs > UINT16_MAX) { + aRv.ThrowRangeError<MSG_VALUE_OUT_OF_RANGE>("numberOfInputs"); + return nullptr; + } + if (aOptions.mNumberOfOutputs > UINT16_MAX) { + aRv.ThrowRangeError<MSG_VALUE_OUT_OF_RANGE>("numberOfOutputs"); + return nullptr; + } + + /** + * 4. Let messageChannel be a new MessageChannel. + */ + RefPtr<MessageChannel> messageChannel = + MessageChannel::Constructor(aGlobal, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + /* 5. Let nodePort be the value of messageChannel’s port1 attribute. + * 6. Let processorPortOnThisSide be the value of messageChannel’s port2 + * attribute. + * 7. Let serializedProcessorPort be the result of + * StructuredSerializeWithTransfer(processorPortOnThisSide, + * « processorPortOnThisSide »). + */ + UniqueMessagePortId processorPortId; + messageChannel->Port2()->CloneAndDisentangle(processorPortId); + /** + * 8. Convert options dictionary to optionsObject. + */ + JSContext* cx = aGlobal.Context(); + JS::Rooted<JS::Value> optionsVal(cx); + if (NS_WARN_IF(!ToJSValue(cx, aOptions, &optionsVal))) { + aRv.NoteJSContextException(cx); + return nullptr; + } + + /** + * 9. Let serializedOptions be the result of + * StructuredSerialize(optionsObject). + */ + + // This context and the worklet are part of the same agent cluster and they + // can share memory. + JS::CloneDataPolicy cloneDataPolicy; + cloneDataPolicy.allowIntraClusterClonableSharedObjects(); + cloneDataPolicy.allowSharedMemoryObjects(); + + // StructuredCloneHolder does not have a move constructor. Instead allocate + // memory so that the pointer can be passed to the rendering thread. + UniquePtr<StructuredCloneHolder> serializedOptions = + MakeUnique<StructuredCloneHolder>( + StructuredCloneHolder::CloningSupported, + StructuredCloneHolder::TransferringNotSupported, + JS::StructuredCloneScope::SameProcess); + serializedOptions->Write(cx, optionsVal, JS::UndefinedHandleValue, + cloneDataPolicy, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + /** + * 10. Set node’s port to nodePort. + */ + audioWorkletNode->mPort = messageChannel->Port1(); + + /** + * 11. Let parameterDescriptors be the result of retrieval of nodeName from + * node name to parameter descriptor map. + */ + nsTArray<NamedAudioParamTimeline> paramTimelines; + audioWorkletNode->InitializeParameters(¶mTimelines, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + auto engine = new WorkletNodeEngine( + audioWorkletNode, aAudioContext.Destination(), std::move(paramTimelines), + aOptions.mOutputChannelCount); + audioWorkletNode->mTrack = AudioNodeTrack::Create( + &aAudioContext, engine, AudioNodeTrack::NO_TRACK_FLAGS, + aAudioContext.Graph()); + + audioWorkletNode->SendParameterData(aOptions.mParameterData); + + /** + * 12. Queue a control message to invoke the constructor of the + * corresponding AudioWorkletProcessor with the processor construction + * data that consists of: nodeName, node, serializedOptions, and + * serializedProcessorPort. + */ + Worklet* worklet = aAudioContext.GetAudioWorklet(aRv); + MOZ_ASSERT(worklet, "Worklet already existed and so getter shouldn't fail."); + auto workletImpl = static_cast<AudioWorkletImpl*>(worklet->Impl()); + audioWorkletNode->mTrack->SendRunnable(NS_NewRunnableFunction( + "WorkletNodeEngine::ConstructProcessor", + // MOZ_CAN_RUN_SCRIPT_BOUNDARY until Runnable::Run is MOZ_CAN_RUN_SCRIPT. + // See bug 1535398. + // + // Note that clang and gcc have mutually incompatible rules about whether + // attributes should come before or after the `mutable` keyword here, so + // use a compatibility hack until we can switch to the standardized + // [[attr]] syntax (bug 1627007). +#ifdef __clang__ +# define AND_MUTABLE(macro) macro mutable +#else +# define AND_MUTABLE(macro) mutable macro +#endif + [track = audioWorkletNode->mTrack, + workletImpl = RefPtr<AudioWorkletImpl>(workletImpl), + name = nsString(aName), options = std::move(serializedOptions), + portId = std::move(processorPortId)]() + AND_MUTABLE(MOZ_CAN_RUN_SCRIPT_BOUNDARY) { + auto engine = static_cast<WorkletNodeEngine*>(track->Engine()); + engine->ConstructProcessor( + workletImpl, name, WrapNotNull(options.get()), portId, track); + })); +#undef AND_MUTABLE + + // [[active source]] is initially true and so at least the first process() + // call will not be skipped when there are no active inputs. + audioWorkletNode->MarkActive(); + + return audioWorkletNode.forget(); +} + +AudioParamMap* AudioWorkletNode::GetParameters(ErrorResult& aRv) { + if (!mParameters) { + RefPtr<AudioParamMap> parameters = new AudioParamMap(this); + nsAutoString name; + for (const auto& audioParam : mParams) { + audioParam->GetName(name); + AudioParamMap_Binding::MaplikeHelpers::Set(parameters, name, *audioParam, + aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + } + mParameters = std::move(parameters); + } + return mParameters.get(); +} + +void AudioWorkletNode::DispatchProcessorErrorEvent( + const ProcessorErrorDetails& aDetails) { + TRACE("AudioWorkletNode::DispatchProcessorErrorEvent"); + if (HasListenersFor(nsGkAtoms::onprocessorerror)) { + RootedDictionary<ErrorEventInit> init(RootingCx()); + init.mMessage = aDetails.mMessage; + init.mFilename = aDetails.mFilename; + init.mLineno = aDetails.mLineno; + init.mColno = aDetails.mColno; + RefPtr<ErrorEvent> errorEvent = + ErrorEvent::Constructor(this, u"processorerror"_ns, init); + MOZ_ASSERT(errorEvent); + DispatchTrustedEvent(errorEvent); + } +} + +JSObject* AudioWorkletNode::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return AudioWorkletNode_Binding::Wrap(aCx, this, aGivenProto); +} + +size_t AudioWorkletNode::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const { + size_t amount = AudioNode::SizeOfExcludingThis(aMallocSizeOf); + return amount; +} + +size_t AudioWorkletNode::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const { + return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf); +} + +} // namespace mozilla::dom diff --git a/dom/media/webaudio/AudioWorkletNode.h b/dom/media/webaudio/AudioWorkletNode.h new file mode 100644 index 0000000000..22e089f8cd --- /dev/null +++ b/dom/media/webaudio/AudioWorkletNode.h @@ -0,0 +1,68 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 https://mozilla.org/MPL/2.0/. */ + +#ifndef AudioWorkletNode_h_ +#define AudioWorkletNode_h_ + +#include "AudioNode.h" + +namespace mozilla::dom { + +class AudioParamMap; +struct AudioWorkletNodeOptions; +class MessagePort; +struct NamedAudioParamTimeline; +struct ProcessorErrorDetails; +template <typename KeyType, typename ValueType> +class Record; + +class AudioWorkletNode : public AudioNode { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(AudioWorkletNode, AudioNode) + + IMPL_EVENT_HANDLER(processorerror) + + static already_AddRefed<AudioWorkletNode> Constructor( + const GlobalObject& aGlobal, AudioContext& aAudioContext, + const nsAString& aName, const AudioWorkletNodeOptions& aOptions, + ErrorResult& aRv); + + AudioParamMap* GetParameters(ErrorResult& aRv); + + MessagePort* Port() const { return mPort; }; + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + // AudioNode methods + uint16_t NumberOfInputs() const override { return mInputCount; } + uint16_t NumberOfOutputs() const override { return mOutputCount; } + const char* NodeType() const override { return "AudioWorkletNode"; } + void DispatchProcessorErrorEvent(const ProcessorErrorDetails& aDetails); + + size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override; + size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override; + + private: + AudioWorkletNode(AudioContext* aAudioContext, const nsAString& aName, + const AudioWorkletNodeOptions& aOptions); + ~AudioWorkletNode() = default; + void InitializeParameters(nsTArray<NamedAudioParamTimeline>* aParamTimelines, + ErrorResult& aRv); + void SendParameterData( + const Optional<Record<nsString, double>>& aParameterData); + + nsString mNodeName; + RefPtr<MessagePort> mPort; + RefPtr<AudioParamMap> mParameters; + uint16_t mInputCount; + uint16_t mOutputCount; +}; + +} // namespace mozilla::dom + +#endif // AudioWorkletNode_h_ diff --git a/dom/media/webaudio/AudioWorkletProcessor.cpp b/dom/media/webaudio/AudioWorkletProcessor.cpp new file mode 100644 index 0000000000..194cc5bf97 --- /dev/null +++ b/dom/media/webaudio/AudioWorkletProcessor.cpp @@ -0,0 +1,49 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 https://mozilla.org/MPL/2.0/. */ + +#include "AudioWorkletProcessor.h" + +#include "mozilla/dom/AudioWorkletNodeBinding.h" +#include "mozilla/dom/AudioWorkletProcessorBinding.h" +#include "mozilla/dom/AudioWorkletGlobalScope.h" +#include "mozilla/dom/MessagePort.h" +#include "mozilla/dom/WorkletGlobalScope.h" +#include "nsIGlobalObject.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(AudioWorkletProcessor, mParent, mPort) + +AudioWorkletProcessor::AudioWorkletProcessor(nsIGlobalObject* aParent, + MessagePort* aPort) + : mParent(aParent), mPort(aPort) {} + +AudioWorkletProcessor::~AudioWorkletProcessor() = default; + +/* static */ +already_AddRefed<AudioWorkletProcessor> AudioWorkletProcessor::Constructor( + const GlobalObject& aGlobal, ErrorResult& aRv) { + nsCOMPtr<WorkletGlobalScope> global = + do_QueryInterface(aGlobal.GetAsSupports()); + MOZ_ASSERT(global); + RefPtr<MessagePort> port = static_cast<AudioWorkletGlobalScope*>(global.get()) + ->TakePortForProcessorCtor(); + if (!port) { + aRv.ThrowTypeError<MSG_ILLEGAL_CONSTRUCTOR>(); + return nullptr; + } + RefPtr<AudioWorkletProcessor> audioWorkletProcessor = + new AudioWorkletProcessor(global, port); + + return audioWorkletProcessor.forget(); +} + +JSObject* AudioWorkletProcessor::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return AudioWorkletProcessor_Binding::Wrap(aCx, this, aGivenProto); +} + +} // namespace mozilla::dom diff --git a/dom/media/webaudio/AudioWorkletProcessor.h b/dom/media/webaudio/AudioWorkletProcessor.h new file mode 100644 index 0000000000..3d37d61498 --- /dev/null +++ b/dom/media/webaudio/AudioWorkletProcessor.h @@ -0,0 +1,50 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 https://mozilla.org/MPL/2.0/. */ + +#ifndef AudioWorkletProcessor_h_ +#define AudioWorkletProcessor_h_ + +#include "nsCOMPtr.h" +#include "nsWrapperCache.h" + +class nsIGlobalObject; + +namespace mozilla { + +class ErrorResult; + +namespace dom { + +struct AudioWorkletNodeOptions; +class GlobalObject; +class MessagePort; + +class AudioWorkletProcessor final : public nsWrapperCache { + public: + NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(AudioWorkletProcessor) + NS_DECL_CYCLE_COLLECTION_NATIVE_WRAPPERCACHE_CLASS(AudioWorkletProcessor) + + static already_AddRefed<AudioWorkletProcessor> Constructor( + const GlobalObject& aGlobal, ErrorResult& aRv); + + nsIGlobalObject* GetParentObject() const { return mParent; } + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + MessagePort* Port() const { return mPort; }; + + private: + explicit AudioWorkletProcessor(nsIGlobalObject* aParent, MessagePort* aPort); + ~AudioWorkletProcessor(); + nsCOMPtr<nsIGlobalObject> mParent; + RefPtr<MessagePort> mPort; +}; + +} // namespace dom +} // namespace mozilla + +#endif // AudioWorkletProcessor_h_ diff --git a/dom/media/webaudio/BiquadFilterNode.cpp b/dom/media/webaudio/BiquadFilterNode.cpp new file mode 100644 index 0000000000..602dbf9cc1 --- /dev/null +++ b/dom/media/webaudio/BiquadFilterNode.cpp @@ -0,0 +1,358 @@ +/* -*- 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 "BiquadFilterNode.h" +#include <algorithm> +#include "AlignmentUtils.h" +#include "AudioNodeEngine.h" +#include "AudioNodeTrack.h" +#include "AudioDestinationNode.h" +#include "PlayingRefChangeHandler.h" +#include "WebAudioUtils.h" +#include "blink/Biquad.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/ErrorResult.h" +#include "AudioParamTimeline.h" +#include "Tracing.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_INHERITED(BiquadFilterNode, AudioNode, mFrequency, + mDetune, mQ, mGain) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(BiquadFilterNode) +NS_INTERFACE_MAP_END_INHERITING(AudioNode) + +NS_IMPL_ADDREF_INHERITED(BiquadFilterNode, AudioNode) +NS_IMPL_RELEASE_INHERITED(BiquadFilterNode, AudioNode) + +static void SetParamsOnBiquad(WebCore::Biquad& aBiquad, float aSampleRate, + BiquadFilterType aType, double aFrequency, + double aQ, double aGain, double aDetune) { + const double nyquist = aSampleRate * 0.5; + double normalizedFrequency = aFrequency / nyquist; + + if (aDetune) { + normalizedFrequency *= fdlibm_exp2(aDetune / 1200); + } + + switch (aType) { + case BiquadFilterType::Lowpass: + aBiquad.setLowpassParams(normalizedFrequency, aQ); + break; + case BiquadFilterType::Highpass: + aBiquad.setHighpassParams(normalizedFrequency, aQ); + break; + case BiquadFilterType::Bandpass: + aBiquad.setBandpassParams(normalizedFrequency, aQ); + break; + case BiquadFilterType::Lowshelf: + aBiquad.setLowShelfParams(normalizedFrequency, aGain); + break; + case BiquadFilterType::Highshelf: + aBiquad.setHighShelfParams(normalizedFrequency, aGain); + break; + case BiquadFilterType::Peaking: + aBiquad.setPeakingParams(normalizedFrequency, aQ, aGain); + break; + case BiquadFilterType::Notch: + aBiquad.setNotchParams(normalizedFrequency, aQ); + break; + case BiquadFilterType::Allpass: + aBiquad.setAllpassParams(normalizedFrequency, aQ); + break; + default: + MOZ_ASSERT_UNREACHABLE("We should never see the alternate names here"); + break; + } +} + +class BiquadFilterNodeEngine final : public AudioNodeEngine { + public: + BiquadFilterNodeEngine(AudioNode* aNode, AudioDestinationNode* aDestination, + uint64_t aWindowID) + : AudioNodeEngine(aNode), + mDestination(aDestination->Track()) + // Keep the default values in sync with the default values in + // BiquadFilterNode::BiquadFilterNode + , + mType(BiquadFilterType::Lowpass), + mFrequency(350.f), + mDetune(0.f), + mQ(1.f), + mGain(0.f), + mWindowID(aWindowID) {} + + enum Parameters { TYPE, FREQUENCY, DETUNE, Q, GAIN }; + void SetInt32Parameter(uint32_t aIndex, int32_t aValue) override { + switch (aIndex) { + case TYPE: + mType = static_cast<BiquadFilterType>(aValue); + break; + default: + NS_ERROR("Bad BiquadFilterNode Int32Parameter"); + } + } + void RecvTimelineEvent(uint32_t aIndex, AudioParamEvent& aEvent) override { + MOZ_ASSERT(mDestination); + + aEvent.ConvertToTicks(mDestination); + + switch (aIndex) { + case FREQUENCY: + mFrequency.InsertEvent<int64_t>(aEvent); + break; + case DETUNE: + mDetune.InsertEvent<int64_t>(aEvent); + break; + case Q: + mQ.InsertEvent<int64_t>(aEvent); + break; + case GAIN: + mGain.InsertEvent<int64_t>(aEvent); + break; + default: + NS_ERROR("Bad BiquadFilterNodeEngine TimelineParameter"); + } + } + + void ProcessBlock(AudioNodeTrack* aTrack, GraphTime aFrom, + const AudioBlock& aInput, AudioBlock* aOutput, + bool* aFinished) override { + TRACE("BiquadFilterNode::ProcessBlock"); + float inputBuffer[WEBAUDIO_BLOCK_SIZE + 4]; + float* alignedInputBuffer = ALIGNED16(inputBuffer); + ASSERT_ALIGNED16(alignedInputBuffer); + + if (aInput.IsNull()) { + bool hasTail = false; + for (uint32_t i = 0; i < mBiquads.Length(); ++i) { + if (mBiquads[i].hasTail()) { + hasTail = true; + break; + } + } + if (!hasTail) { + if (!mBiquads.IsEmpty()) { + mBiquads.Clear(); + aTrack->ScheduleCheckForInactive(); + + RefPtr<PlayingRefChangeHandler> refchanged = + new PlayingRefChangeHandler(aTrack, + PlayingRefChangeHandler::RELEASE); + aTrack->Graph()->DispatchToMainThreadStableState(refchanged.forget()); + } + + aOutput->SetNull(WEBAUDIO_BLOCK_SIZE); + return; + } + + PodArrayZero(inputBuffer); + + } else if (mBiquads.Length() != aInput.ChannelCount()) { + if (mBiquads.IsEmpty()) { + RefPtr<PlayingRefChangeHandler> refchanged = + new PlayingRefChangeHandler(aTrack, + PlayingRefChangeHandler::ADDREF); + aTrack->Graph()->DispatchToMainThreadStableState(refchanged.forget()); + } else { // Help people diagnose bug 924718 + WebAudioUtils::LogToDeveloperConsole( + mWindowID, "BiquadFilterChannelCountChangeWarning"); + } + + // Adjust the number of biquads based on the number of channels + mBiquads.SetLength(aInput.ChannelCount()); + } + + uint32_t numberOfChannels = mBiquads.Length(); + aOutput->AllocateChannels(numberOfChannels); + + TrackTime pos = mDestination->GraphTimeToTrackTime(aFrom); + + double freq = mFrequency.GetValueAtTime(pos); + double q = mQ.GetValueAtTime(pos); + double gain = mGain.GetValueAtTime(pos); + double detune = mDetune.GetValueAtTime(pos); + + for (uint32_t i = 0; i < numberOfChannels; ++i) { + const float* input; + if (aInput.IsNull()) { + input = alignedInputBuffer; + } else { + input = static_cast<const float*>(aInput.mChannelData[i]); + if (aInput.mVolume != 1.0) { + AudioBlockCopyChannelWithScale(input, aInput.mVolume, + alignedInputBuffer); + input = alignedInputBuffer; + } + } + SetParamsOnBiquad(mBiquads[i], aTrack->mSampleRate, mType, freq, q, gain, + detune); + + mBiquads[i].process(input, aOutput->ChannelFloatsForWrite(i), + aInput.GetDuration()); + } + } + + bool IsActive() const override { return !mBiquads.IsEmpty(); } + + size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override { + // Not owned: + // - mDestination - probably not owned + // - AudioParamTimelines - counted in the AudioNode + size_t amount = AudioNodeEngine::SizeOfExcludingThis(aMallocSizeOf); + amount += mBiquads.ShallowSizeOfExcludingThis(aMallocSizeOf); + return amount; + } + + size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override { + return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf); + } + + private: + RefPtr<AudioNodeTrack> mDestination; + BiquadFilterType mType; + AudioParamTimeline mFrequency; + AudioParamTimeline mDetune; + AudioParamTimeline mQ; + AudioParamTimeline mGain; + nsTArray<WebCore::Biquad> mBiquads; + uint64_t mWindowID; +}; + +BiquadFilterNode::BiquadFilterNode(AudioContext* aContext) + : AudioNode(aContext, 2, ChannelCountMode::Max, + ChannelInterpretation::Speakers), + mType(BiquadFilterType::Lowpass) { + mFrequency = CreateAudioParam( + BiquadFilterNodeEngine::FREQUENCY, u"frequency"_ns, 350.f, + -(aContext->SampleRate() / 2), aContext->SampleRate() / 2); + mDetune = CreateAudioParam(BiquadFilterNodeEngine::DETUNE, u"detune"_ns, 0.f); + mQ = CreateAudioParam(BiquadFilterNodeEngine::Q, u"Q"_ns, 1.f); + mGain = CreateAudioParam(BiquadFilterNodeEngine::GAIN, u"gain"_ns, 0.f); + + uint64_t windowID = 0; + if (aContext->GetParentObject()) { + windowID = aContext->GetParentObject()->WindowID(); + } + BiquadFilterNodeEngine* engine = + new BiquadFilterNodeEngine(this, aContext->Destination(), windowID); + mTrack = AudioNodeTrack::Create( + aContext, engine, AudioNodeTrack::NO_TRACK_FLAGS, aContext->Graph()); +} + +/* static */ +already_AddRefed<BiquadFilterNode> BiquadFilterNode::Create( + AudioContext& aAudioContext, const BiquadFilterOptions& aOptions, + ErrorResult& aRv) { + RefPtr<BiquadFilterNode> audioNode = new BiquadFilterNode(&aAudioContext); + + audioNode->Initialize(aOptions, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + audioNode->SetType(aOptions.mType); + audioNode->Q()->SetInitialValue(aOptions.mQ); + audioNode->Detune()->SetInitialValue(aOptions.mDetune); + audioNode->Frequency()->SetInitialValue(aOptions.mFrequency); + audioNode->Gain()->SetInitialValue(aOptions.mGain); + + return audioNode.forget(); +} + +size_t BiquadFilterNode::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const { + size_t amount = AudioNode::SizeOfExcludingThis(aMallocSizeOf); + + if (mFrequency) { + amount += mFrequency->SizeOfIncludingThis(aMallocSizeOf); + } + + if (mDetune) { + amount += mDetune->SizeOfIncludingThis(aMallocSizeOf); + } + + if (mQ) { + amount += mQ->SizeOfIncludingThis(aMallocSizeOf); + } + + if (mGain) { + amount += mGain->SizeOfIncludingThis(aMallocSizeOf); + } + + return amount; +} + +size_t BiquadFilterNode::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const { + return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf); +} + +JSObject* BiquadFilterNode::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return BiquadFilterNode_Binding::Wrap(aCx, this, aGivenProto); +} + +void BiquadFilterNode::SetType(BiquadFilterType aType) { + mType = aType; + SendInt32ParameterToTrack(BiquadFilterNodeEngine::TYPE, + static_cast<int32_t>(aType)); +} + +void BiquadFilterNode::GetFrequencyResponse(const Float32Array& aFrequencyHz, + const Float32Array& aMagResponse, + const Float32Array& aPhaseResponse, + ErrorResult& aRv) { + UniquePtr<float[]> frequencies; + size_t length; + const double currentTime = Context()->CurrentTime(); + aFrequencyHz.ProcessData([&](const Span<float>& aFrequencyData, + JS::AutoCheckCannotGC&&) { + aMagResponse.ProcessData([&](const Span<float>& aMagData, + JS::AutoCheckCannotGC&&) { + aPhaseResponse.ProcessData([&](const Span<float>& aPhaseData, + JS::AutoCheckCannotGC&&) { + length = aFrequencyData.Length(); + if (length != aMagData.Length() || length != aPhaseData.Length()) { + aRv.ThrowInvalidAccessError("Parameter lengths must match"); + return; + } + + if (length == 0) { + return; + } + + frequencies = MakeUniqueForOverwriteFallible<float[]>(length); + if (!frequencies) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + const double nyquist = Context()->SampleRate() * 0.5; + std::transform(aFrequencyData.begin(), aFrequencyData.end(), + frequencies.get(), [&](float aFrequency) { + if (aFrequency >= 0 && aFrequency <= nyquist) { + return static_cast<float>(aFrequency / nyquist); + } + + return std::numeric_limits<float>::quiet_NaN(); + }); + + double freq = mFrequency->GetValueAtTime(currentTime); + double q = mQ->GetValueAtTime(currentTime); + double gain = mGain->GetValueAtTime(currentTime); + double detune = mDetune->GetValueAtTime(currentTime); + + WebCore::Biquad biquad; + SetParamsOnBiquad(biquad, Context()->SampleRate(), mType, freq, q, gain, + detune); + biquad.getFrequencyResponse(int(length), frequencies.get(), + aMagData.Elements(), aPhaseData.Elements()); + }); + }); + }); +} + +} // namespace mozilla::dom diff --git a/dom/media/webaudio/BiquadFilterNode.h b/dom/media/webaudio/BiquadFilterNode.h new file mode 100644 index 0000000000..31b08c00c8 --- /dev/null +++ b/dom/media/webaudio/BiquadFilterNode.h @@ -0,0 +1,71 @@ +/* -*- 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/. */ + +#ifndef BiquadFilterNode_h_ +#define BiquadFilterNode_h_ + +#include "AudioNode.h" +#include "AudioParam.h" +#include "mozilla/dom/BiquadFilterNodeBinding.h" + +namespace mozilla::dom { + +class AudioContext; +struct BiquadFilterOptions; + +class BiquadFilterNode final : public AudioNode { + public: + static already_AddRefed<BiquadFilterNode> Create( + AudioContext& aAudioContext, const BiquadFilterOptions& aOptions, + ErrorResult& aRv); + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(BiquadFilterNode, AudioNode) + + static already_AddRefed<BiquadFilterNode> Constructor( + const GlobalObject& aGlobal, AudioContext& aAudioContext, + const BiquadFilterOptions& aOptions, ErrorResult& aRv) { + return Create(aAudioContext, aOptions, aRv); + } + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + BiquadFilterType Type() const { return mType; } + void SetType(BiquadFilterType aType); + + AudioParam* Frequency() const { return mFrequency; } + + AudioParam* Detune() const { return mDetune; } + + AudioParam* Q() const { return mQ; } + + AudioParam* Gain() const { return mGain; } + + void GetFrequencyResponse(const Float32Array& aFrequencyHz, + const Float32Array& aMagResponse, + const Float32Array& aPhaseResponse, + ErrorResult& aRv); + + const char* NodeType() const override { return "BiquadFilterNode"; } + + size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override; + size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override; + + private: + explicit BiquadFilterNode(AudioContext* aContext); + ~BiquadFilterNode() = default; + + BiquadFilterType mType; + RefPtr<AudioParam> mFrequency; + RefPtr<AudioParam> mDetune; + RefPtr<AudioParam> mQ; + RefPtr<AudioParam> mGain; +}; + +} // namespace mozilla::dom + +#endif diff --git a/dom/media/webaudio/ChannelMergerNode.cpp b/dom/media/webaudio/ChannelMergerNode.cpp new file mode 100644 index 0000000000..4ffae02591 --- /dev/null +++ b/dom/media/webaudio/ChannelMergerNode.cpp @@ -0,0 +1,100 @@ +/* -*- 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 "mozilla/dom/ChannelMergerNode.h" +#include "mozilla/dom/ChannelMergerNodeBinding.h" +#include "AudioNodeEngine.h" +#include "AudioNodeTrack.h" +#include "nsPrintfCString.h" +#include "Tracing.h" + +namespace mozilla::dom { + +class ChannelMergerNodeEngine final : public AudioNodeEngine { + public: + explicit ChannelMergerNodeEngine(ChannelMergerNode* aNode) + : AudioNodeEngine(aNode) { + MOZ_ASSERT(NS_IsMainThread()); + } + + void ProcessBlocksOnPorts(AudioNodeTrack* aTrack, GraphTime aFrom, + Span<const AudioBlock> aInput, + Span<AudioBlock> aOutput, + bool* aFinished) override { + MOZ_ASSERT(aInput.Length() == InputCount()); + MOZ_ASSERT(aOutput.Length() == 1, "Should have only one output port"); + TRACE("ChannelMergerNodeEngine::ProcessBlocksOnPorts"); + + // Get the number of output channels, and allocate it + size_t channelCount = InputCount(); + bool allNull = true; + for (size_t i = 0; i < channelCount; ++i) { + allNull &= aInput[i].IsNull(); + } + if (allNull) { + aOutput[0].SetNull(WEBAUDIO_BLOCK_SIZE); + return; + } + + aOutput[0].AllocateChannels(channelCount); + + for (size_t i = 0; i < channelCount; ++i) { + float* output = aOutput[0].ChannelFloatsForWrite(i); + if (aInput[i].IsNull()) { + PodZero(output, WEBAUDIO_BLOCK_SIZE); + } else { + AudioBlockCopyChannelWithScale( + static_cast<const float*>(aInput[i].mChannelData[0]), + aInput[i].mVolume, output); + } + } + } + + size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override { + return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf); + } +}; + +ChannelMergerNode::ChannelMergerNode(AudioContext* aContext, + uint16_t aInputCount) + : AudioNode(aContext, 1, ChannelCountMode::Explicit, + ChannelInterpretation::Speakers), + mInputCount(aInputCount) { + mTrack = + AudioNodeTrack::Create(aContext, new ChannelMergerNodeEngine(this), + AudioNodeTrack::NO_TRACK_FLAGS, aContext->Graph()); +} + +/* static */ +already_AddRefed<ChannelMergerNode> ChannelMergerNode::Create( + AudioContext& aAudioContext, const ChannelMergerOptions& aOptions, + ErrorResult& aRv) { + if (aOptions.mNumberOfInputs == 0 || + aOptions.mNumberOfInputs > WebAudioUtils::MaxChannelCount) { + aRv.ThrowIndexSizeError( + nsPrintfCString("Number of inputs (%u) must be in the range [1, number " + "of supported channels]", + aOptions.mNumberOfInputs)); + return nullptr; + } + + RefPtr<ChannelMergerNode> audioNode = + new ChannelMergerNode(&aAudioContext, aOptions.mNumberOfInputs); + + audioNode->Initialize(aOptions, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + return audioNode.forget(); +} + +JSObject* ChannelMergerNode::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return ChannelMergerNode_Binding::Wrap(aCx, this, aGivenProto); +} + +} // namespace mozilla::dom diff --git a/dom/media/webaudio/ChannelMergerNode.h b/dom/media/webaudio/ChannelMergerNode.h new file mode 100644 index 0000000000..92327c8b1e --- /dev/null +++ b/dom/media/webaudio/ChannelMergerNode.h @@ -0,0 +1,67 @@ +/* -*- 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/. */ + +#ifndef ChannelMergerNode_h_ +#define ChannelMergerNode_h_ + +#include "AudioNode.h" + +namespace mozilla::dom { + +class AudioContext; +struct ChannelMergerOptions; + +class ChannelMergerNode final : public AudioNode { + public: + static already_AddRefed<ChannelMergerNode> Create( + AudioContext& aAudioContext, const ChannelMergerOptions& aOptions, + ErrorResult& aRv); + + NS_INLINE_DECL_REFCOUNTING_INHERITED(ChannelMergerNode, AudioNode) + + static already_AddRefed<ChannelMergerNode> Constructor( + const GlobalObject& aGlobal, AudioContext& aAudioContext, + const ChannelMergerOptions& aOptions, ErrorResult& aRv) { + return Create(aAudioContext, aOptions, aRv); + } + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + uint16_t NumberOfInputs() const override { return mInputCount; } + + const char* NodeType() const override { return "ChannelMergerNode"; } + + virtual void SetChannelCount(uint32_t aChannelCount, + ErrorResult& aRv) override { + if (aChannelCount != 1) { + aRv.ThrowInvalidStateError( + "Cannot change channel count of ChannelMergerNode"); + } + } + + virtual void SetChannelCountModeValue(ChannelCountMode aMode, + ErrorResult& aRv) override { + if (aMode != ChannelCountMode::Explicit) { + aRv.ThrowInvalidStateError( + "Cannot change channel count mode of ChannelMergerNode"); + } + } + + size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override { + return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf); + } + + private: + ChannelMergerNode(AudioContext* aContext, uint16_t aInputCount); + ~ChannelMergerNode() = default; + + const uint16_t mInputCount; +}; + +} // namespace mozilla::dom + +#endif diff --git a/dom/media/webaudio/ChannelSplitterNode.cpp b/dom/media/webaudio/ChannelSplitterNode.cpp new file mode 100644 index 0000000000..52d3ae948f --- /dev/null +++ b/dom/media/webaudio/ChannelSplitterNode.cpp @@ -0,0 +1,105 @@ +/* -*- 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 "mozilla/dom/ChannelSplitterNode.h" +#include "mozilla/dom/ChannelSplitterNodeBinding.h" +#include "AudioNodeEngine.h" +#include "AudioNodeTrack.h" +#include "Tracing.h" + +namespace mozilla::dom { + +class ChannelSplitterNodeEngine final : public AudioNodeEngine { + public: + explicit ChannelSplitterNodeEngine(ChannelSplitterNode* aNode) + : AudioNodeEngine(aNode) { + MOZ_ASSERT(NS_IsMainThread()); + } + + void ProcessBlocksOnPorts(AudioNodeTrack* aTrack, GraphTime aFrom, + Span<const AudioBlock> aInput, + Span<AudioBlock> aOutput, + bool* aFinished) override { + MOZ_ASSERT(aInput.Length() == 1, "Should only have one input port"); + MOZ_ASSERT(aOutput.Length() == OutputCount()); + TRACE("ChannelSplitterNodeEngine::ProcessBlocksOnPorts"); + + for (uint16_t i = 0; i < OutputCount(); ++i) { + if (i < aInput[0].ChannelCount()) { + // Split out existing channels + aOutput[i].AllocateChannels(1); + AudioBlockCopyChannelWithScale( + static_cast<const float*>(aInput[0].mChannelData[i]), + aInput[0].mVolume, aOutput[i].ChannelFloatsForWrite(0)); + } else { + // Pad with silent channels if needed + aOutput[i].SetNull(WEBAUDIO_BLOCK_SIZE); + } + } + } + + size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override { + return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf); + } +}; + +ChannelSplitterNode::ChannelSplitterNode(AudioContext* aContext, + uint16_t aOutputCount) + : AudioNode(aContext, aOutputCount, ChannelCountMode::Explicit, + ChannelInterpretation::Discrete), + mOutputCount(aOutputCount) { + mTrack = + AudioNodeTrack::Create(aContext, new ChannelSplitterNodeEngine(this), + AudioNodeTrack::NO_TRACK_FLAGS, aContext->Graph()); +} + +/* static */ +already_AddRefed<ChannelSplitterNode> ChannelSplitterNode::Create( + AudioContext& aAudioContext, const ChannelSplitterOptions& aOptions, + ErrorResult& aRv) { + if (aOptions.mNumberOfOutputs == 0 || + aOptions.mNumberOfOutputs > WebAudioUtils::MaxChannelCount) { + aRv.ThrowIndexSizeError(nsPrintfCString( + "%u is not a valid number of outputs", aOptions.mNumberOfOutputs)); + return nullptr; + } + + RefPtr<ChannelSplitterNode> audioNode = + new ChannelSplitterNode(&aAudioContext, aOptions.mNumberOfOutputs); + + // Manually check that the other options are valid, this node has + // channelCount, channelCountMode and channelInterpretation constraints: they + // cannot be changed from the default. + if (aOptions.mChannelCount.WasPassed()) { + audioNode->SetChannelCount(aOptions.mChannelCount.Value(), aRv); + if (aRv.Failed()) { + return nullptr; + } + } + if (aOptions.mChannelInterpretation.WasPassed()) { + audioNode->SetChannelInterpretationValue( + aOptions.mChannelInterpretation.Value(), aRv); + if (aRv.Failed()) { + return nullptr; + } + } + if (aOptions.mChannelCountMode.WasPassed()) { + audioNode->SetChannelCountModeValue(aOptions.mChannelCountMode.Value(), + aRv); + if (aRv.Failed()) { + return nullptr; + } + } + + return audioNode.forget(); +} + +JSObject* ChannelSplitterNode::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return ChannelSplitterNode_Binding::Wrap(aCx, this, aGivenProto); +} + +} // namespace mozilla::dom diff --git a/dom/media/webaudio/ChannelSplitterNode.h b/dom/media/webaudio/ChannelSplitterNode.h new file mode 100644 index 0000000000..c13eaa8f22 --- /dev/null +++ b/dom/media/webaudio/ChannelSplitterNode.h @@ -0,0 +1,72 @@ +/* -*- 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/. */ + +#ifndef ChannelSplitterNode_h_ +#define ChannelSplitterNode_h_ + +#include "AudioNode.h" + +namespace mozilla::dom { + +class AudioContext; +struct ChannelSplitterOptions; + +class ChannelSplitterNode final : public AudioNode { + public: + static already_AddRefed<ChannelSplitterNode> Create( + AudioContext& aAudioContext, const ChannelSplitterOptions& aOptions, + ErrorResult& aRv); + + NS_INLINE_DECL_REFCOUNTING_INHERITED(ChannelSplitterNode, AudioNode) + + static already_AddRefed<ChannelSplitterNode> Constructor( + const GlobalObject& aGlobal, AudioContext& aAudioContext, + const ChannelSplitterOptions& aOptions, ErrorResult& aRv) { + return Create(aAudioContext, aOptions, aRv); + } + + void SetChannelCount(uint32_t aChannelCount, ErrorResult& aRv) override { + if (aChannelCount != ChannelCount()) { + aRv.ThrowInvalidStateError( + "Cannot change channel count of ChannelSplitterNode"); + } + } + void SetChannelCountModeValue(ChannelCountMode aMode, + ErrorResult& aRv) override { + if (aMode != ChannelCountModeValue()) { + aRv.ThrowInvalidStateError( + "Cannot change channel count mode of ChannelSplitterNode"); + } + } + void SetChannelInterpretationValue(ChannelInterpretation aMode, + ErrorResult& aRv) override { + if (aMode != ChannelInterpretationValue()) { + aRv.ThrowInvalidStateError( + "Cannot change channel interpretation of ChannelSplitterNode"); + } + } + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + uint16_t NumberOfOutputs() const override { return mOutputCount; } + + const char* NodeType() const override { return "ChannelSplitterNode"; } + + size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override { + return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf); + } + + private: + ChannelSplitterNode(AudioContext* aContext, uint16_t aOutputCount); + ~ChannelSplitterNode() = default; + + const uint16_t mOutputCount; +}; + +} // namespace mozilla::dom + +#endif diff --git a/dom/media/webaudio/ConstantSourceNode.cpp b/dom/media/webaudio/ConstantSourceNode.cpp new file mode 100644 index 0000000000..6e74c3e219 --- /dev/null +++ b/dom/media/webaudio/ConstantSourceNode.cpp @@ -0,0 +1,279 @@ +/* -*- 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 "ConstantSourceNode.h" + +#include "AudioDestinationNode.h" +#include "nsContentUtils.h" +#include "AudioNodeEngine.h" +#include "AudioNodeTrack.h" +#include "Tracing.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_INHERITED(ConstantSourceNode, AudioScheduledSourceNode, + mOffset) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ConstantSourceNode) +NS_INTERFACE_MAP_END_INHERITING(AudioScheduledSourceNode) + +NS_IMPL_ADDREF_INHERITED(ConstantSourceNode, AudioScheduledSourceNode) +NS_IMPL_RELEASE_INHERITED(ConstantSourceNode, AudioScheduledSourceNode) + +class ConstantSourceNodeEngine final : public AudioNodeEngine { + public: + ConstantSourceNodeEngine(AudioNode* aNode, AudioDestinationNode* aDestination) + : AudioNodeEngine(aNode), + mSource(nullptr), + mDestination(aDestination->Track()), + mStart(-1), + mStop(TRACK_TIME_MAX) + // Keep the default values in sync with + // ConstantSourceNode::ConstantSourceNode. + , + mOffset(1.0f) { + MOZ_ASSERT(NS_IsMainThread()); + } + + void SetSourceTrack(AudioNodeTrack* aSource) { mSource = aSource; } + + enum Parameters { + OFFSET, + START, + STOP, + }; + void RecvTimelineEvent(uint32_t aIndex, AudioParamEvent& aEvent) override { + MOZ_ASSERT(mDestination); + + aEvent.ConvertToTicks(mDestination); + + switch (aIndex) { + case OFFSET: + mOffset.InsertEvent<int64_t>(aEvent); + break; + default: + NS_ERROR("Bad ConstantSourceNodeEngine TimelineParameter"); + } + } + + void SetTrackTimeParameter(uint32_t aIndex, TrackTime aParam) override { + switch (aIndex) { + case START: + mStart = aParam; + mSource->SetActive(); + break; + case STOP: + mStop = aParam; + break; + default: + NS_ERROR("Bad ConstantSourceNodeEngine TrackTimeParameter"); + } + } + + void ProcessBlock(AudioNodeTrack* aTrack, GraphTime aFrom, + const AudioBlock& aInput, AudioBlock* aOutput, + bool* aFinished) override { + MOZ_ASSERT(mSource == aTrack, "Invalid source track"); + TRACE("ConstantSourceNodeEngine::ProcessBlock"); + + TrackTime ticks = mDestination->GraphTimeToTrackTime(aFrom); + if (mStart == -1) { + aOutput->SetNull(WEBAUDIO_BLOCK_SIZE); + return; + } + + if (ticks + WEBAUDIO_BLOCK_SIZE <= mStart || ticks >= mStop || + mStop <= mStart) { + aOutput->SetNull(WEBAUDIO_BLOCK_SIZE); + } else { + aOutput->AllocateChannels(1); + float* output = aOutput->ChannelFloatsForWrite(0); + uint32_t writeOffset = 0; + + if (ticks < mStart) { + MOZ_ASSERT(mStart - ticks <= WEBAUDIO_BLOCK_SIZE); + uint32_t count = mStart - ticks; + std::fill_n(output, count, 0.0f); + writeOffset += count; + } + + MOZ_ASSERT(ticks + writeOffset >= mStart); + MOZ_ASSERT(mStop - ticks >= writeOffset); + uint32_t count = + std::min<TrackTime>(WEBAUDIO_BLOCK_SIZE, mStop - ticks) - writeOffset; + + if (mOffset.HasSimpleValue()) { + float value = mOffset.GetValue(); + std::fill_n(output + writeOffset, count, value); + } else { + mOffset.GetValuesAtTime(ticks + writeOffset, output + writeOffset, + count); + } + + writeOffset += count; + + std::fill_n(output + writeOffset, WEBAUDIO_BLOCK_SIZE - writeOffset, + 0.0f); + } + + if (ticks + WEBAUDIO_BLOCK_SIZE >= mStop) { + // We've finished playing. + *aFinished = true; + } + } + + bool IsActive() const override { + // start() has been called. + return mStart != -1; + } + + size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override { + size_t amount = AudioNodeEngine::SizeOfExcludingThis(aMallocSizeOf); + + // Not owned: + // - mSource + // - mDestination + // - mOffset (internal ref owned by node) + + return amount; + } + + size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override { + return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf); + } + + // mSource deletes the engine in its destructor. + AudioNodeTrack* MOZ_NON_OWNING_REF mSource; + RefPtr<AudioNodeTrack> mDestination; + TrackTime mStart; + TrackTime mStop; + AudioParamTimeline mOffset; +}; + +ConstantSourceNode::ConstantSourceNode(AudioContext* aContext) + : AudioScheduledSourceNode(aContext, 2, ChannelCountMode::Max, + ChannelInterpretation::Speakers), + mStartCalled(false) { + mOffset = + CreateAudioParam(ConstantSourceNodeEngine::OFFSET, u"offset"_ns, 1.0f); + ConstantSourceNodeEngine* engine = + new ConstantSourceNodeEngine(this, aContext->Destination()); + mTrack = AudioNodeTrack::Create(aContext, engine, + AudioNodeTrack::NEED_MAIN_THREAD_ENDED, + aContext->Graph()); + engine->SetSourceTrack(mTrack); + mTrack->AddMainThreadListener(this); +} + +ConstantSourceNode::~ConstantSourceNode() = default; + +size_t ConstantSourceNode::SizeOfExcludingThis( + MallocSizeOf aMallocSizeOf) const { + size_t amount = AudioNode::SizeOfExcludingThis(aMallocSizeOf); + + amount += mOffset->SizeOfIncludingThis(aMallocSizeOf); + return amount; +} + +size_t ConstantSourceNode::SizeOfIncludingThis( + MallocSizeOf aMallocSizeOf) const { + return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf); +} + +JSObject* ConstantSourceNode::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return ConstantSourceNode_Binding::Wrap(aCx, this, aGivenProto); +} + +already_AddRefed<ConstantSourceNode> ConstantSourceNode::Constructor( + const GlobalObject& aGlobal, AudioContext& aContext, + const ConstantSourceOptions& aOptions) { + RefPtr<ConstantSourceNode> object = new ConstantSourceNode(&aContext); + object->mOffset->SetInitialValue(aOptions.mOffset); + return object.forget(); +} + +void ConstantSourceNode::DestroyMediaTrack() { + if (mTrack) { + mTrack->RemoveMainThreadListener(this); + } + AudioNode::DestroyMediaTrack(); +} + +void ConstantSourceNode::Start(double aWhen, ErrorResult& aRv) { + if (!WebAudioUtils::IsTimeValid(aWhen)) { + aRv.ThrowRangeError<MSG_VALUE_OUT_OF_RANGE>("start time"); + return; + } + + if (mStartCalled) { + aRv.ThrowInvalidStateError("Can't call start() more than once"); + return; + } + mStartCalled = true; + + if (!mTrack) { + return; + } + + mTrack->SetTrackTimeParameter(ConstantSourceNodeEngine::START, Context(), + aWhen); + + MarkActive(); + Context()->StartBlockedAudioContextIfAllowed(); +} + +void ConstantSourceNode::Stop(double aWhen, ErrorResult& aRv) { + if (!WebAudioUtils::IsTimeValid(aWhen)) { + aRv.ThrowRangeError<MSG_VALUE_OUT_OF_RANGE>("stop time"); + return; + } + + if (!mStartCalled) { + aRv.ThrowInvalidStateError("Can't call stop() without calling start()"); + return; + } + + if (!mTrack || !Context()) { + return; + } + + mTrack->SetTrackTimeParameter(ConstantSourceNodeEngine::STOP, Context(), + std::max(0.0, aWhen)); +} + +void ConstantSourceNode::NotifyMainThreadTrackEnded() { + MOZ_ASSERT(mTrack->IsEnded()); + + class EndedEventDispatcher final : public Runnable { + public: + explicit EndedEventDispatcher(ConstantSourceNode* aNode) + : mozilla::Runnable("EndedEventDispatcher"), mNode(aNode) {} + NS_IMETHOD Run() override { + // If it's not safe to run scripts right now, schedule this to run later + if (!nsContentUtils::IsSafeToRunScript()) { + nsContentUtils::AddScriptRunner(this); + return NS_OK; + } + + mNode->DispatchTrustedEvent(u"ended"_ns); + // Release track resources. + mNode->DestroyMediaTrack(); + return NS_OK; + } + + private: + RefPtr<ConstantSourceNode> mNode; + }; + + Context()->Dispatch(do_AddRef(new EndedEventDispatcher(this))); + + // Drop the playing reference + // Warning: The below line might delete this. + MarkInactive(); +} + +} // namespace mozilla::dom diff --git a/dom/media/webaudio/ConstantSourceNode.h b/dom/media/webaudio/ConstantSourceNode.h new file mode 100644 index 0000000000..61f939a7e6 --- /dev/null +++ b/dom/media/webaudio/ConstantSourceNode.h @@ -0,0 +1,60 @@ +/* -*- 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/. */ + +#ifndef ConstantSourceNode_h_ +#define ConstantSourceNode_h_ + +#include "AudioScheduledSourceNode.h" +#include "AudioParam.h" +#include "mozilla/dom/ConstantSourceNodeBinding.h" + +namespace mozilla::dom { + +class AudioContext; + +class ConstantSourceNode final : public AudioScheduledSourceNode, + public MainThreadMediaTrackListener { + public: + explicit ConstantSourceNode(AudioContext* aContext); + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(ConstantSourceNode, + AudioScheduledSourceNode) + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + static already_AddRefed<ConstantSourceNode> Constructor( + const GlobalObject& aGlobal, AudioContext& aContext, + const ConstantSourceOptions& aOptions); + + void DestroyMediaTrack() override; + + uint16_t NumberOfInputs() const final { return 0; } + + AudioParam* Offset() const { return mOffset; } + + void Start(double aWhen, ErrorResult& rv) override; + void Stop(double aWhen, ErrorResult& rv) override; + + void NotifyMainThreadTrackEnded() override; + + const char* NodeType() const override { return "ConstantSourceNode"; } + + size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override; + size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override; + + protected: + virtual ~ConstantSourceNode(); + + private: + RefPtr<AudioParam> mOffset; + bool mStartCalled; +}; + +} // namespace mozilla::dom + +#endif 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 diff --git a/dom/media/webaudio/ConvolverNode.h b/dom/media/webaudio/ConvolverNode.h new file mode 100644 index 0000000000..897f8d6547 --- /dev/null +++ b/dom/media/webaudio/ConvolverNode.h @@ -0,0 +1,77 @@ +/* -*- 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/. */ + +#ifndef ConvolverNode_h_ +#define ConvolverNode_h_ + +#include "AudioNode.h" +#include "AudioBuffer.h" +#include "nsPrintfCString.h" + +namespace mozilla::dom { + +class AudioContext; +struct ConvolverOptions; + +class ConvolverNode final : public AudioNode { + public: + static already_AddRefed<ConvolverNode> Create( + JSContext* aCx, AudioContext& aAudioContext, + const ConvolverOptions& aOptions, ErrorResult& aRv); + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(ConvolverNode, AudioNode); + + static already_AddRefed<ConvolverNode> Constructor( + const GlobalObject& aGlobal, AudioContext& aAudioContext, + const ConvolverOptions& aOptions, ErrorResult& aRv) { + return Create(aGlobal.Context(), aAudioContext, aOptions, aRv); + } + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + AudioBuffer* GetBuffer(JSContext* aCx) const { return mBuffer; } + + void SetBuffer(JSContext* aCx, AudioBuffer* aBuffer, ErrorResult& aRv); + + bool Normalize() const { return mNormalize; } + + void SetNormalize(bool aNormal); + + void SetChannelCount(uint32_t aChannelCount, ErrorResult& aRv) override { + if (aChannelCount > 2) { + aRv.ThrowNotSupportedError( + nsPrintfCString("%u is greater than 2", aChannelCount)); + return; + } + AudioNode::SetChannelCount(aChannelCount, aRv); + } + void SetChannelCountModeValue(ChannelCountMode aMode, + ErrorResult& aRv) override { + if (aMode == ChannelCountMode::Max) { + aRv.ThrowNotSupportedError("Cannot set channel count mode to \"max\""); + return; + } + AudioNode::SetChannelCountModeValue(aMode, aRv); + } + + const char* NodeType() const override { return "ConvolverNode"; } + + size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override; + size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override; + + private: + explicit ConvolverNode(AudioContext* aContext); + ~ConvolverNode() = default; + + RefPtr<AudioBuffer> mBuffer; + bool mNormalize; +}; + +} // namespace mozilla::dom + +#endif diff --git a/dom/media/webaudio/DelayBuffer.cpp b/dom/media/webaudio/DelayBuffer.cpp new file mode 100644 index 0000000000..52c8d4184b --- /dev/null +++ b/dom/media/webaudio/DelayBuffer.cpp @@ -0,0 +1,236 @@ +/* -*- 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 "DelayBuffer.h" + +#include "mozilla/PodOperations.h" +#include "AudioChannelFormat.h" +#include "AudioNodeEngine.h" + +namespace mozilla { + +size_t DelayBuffer::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const { + size_t amount = 0; + amount += mChunks.ShallowSizeOfExcludingThis(aMallocSizeOf); + for (size_t i = 0; i < mChunks.Length(); i++) { + amount += mChunks[i].SizeOfExcludingThis(aMallocSizeOf, false); + } + + amount += mUpmixChannels.ShallowSizeOfExcludingThis(aMallocSizeOf); + return amount; +} + +void DelayBuffer::Write(const AudioBlock& aInputChunk) { + // We must have a reference to the buffer if there are channels + MOZ_ASSERT(aInputChunk.IsNull() == !aInputChunk.ChannelCount()); +#ifdef DEBUG + MOZ_ASSERT(!mHaveWrittenBlock); + mHaveWrittenBlock = true; +#endif + + if (!EnsureBuffer()) { + return; + } + + if (mCurrentChunk == mLastReadChunk) { + mLastReadChunk = -1; // invalidate cache + } + mChunks[mCurrentChunk] = aInputChunk.AsAudioChunk(); +} + +void DelayBuffer::Read(const float aPerFrameDelays[WEBAUDIO_BLOCK_SIZE], + AudioBlock* aOutputChunk, + ChannelInterpretation aChannelInterpretation) { + int chunkCount = mChunks.Length(); + if (!chunkCount) { + aOutputChunk->SetNull(WEBAUDIO_BLOCK_SIZE); + return; + } + + // Find the maximum number of contributing channels to determine the output + // channel count that retains all signal information. Buffered blocks will + // be upmixed if necessary. + // + // First find the range of "delay" offsets backwards from the current + // position. Note that these may be negative for frames that are after the + // current position (including i). + float minDelay = aPerFrameDelays[0]; + float maxDelay = minDelay; + for (unsigned i = 1; i < WEBAUDIO_BLOCK_SIZE; ++i) { + minDelay = std::min(minDelay, aPerFrameDelays[i] - i); + maxDelay = std::max(maxDelay, aPerFrameDelays[i] - i); + } + + // Now find the chunks touched by this range and check their channel counts. + int oldestChunk = ChunkForDelay(std::ceil(maxDelay)); + int youngestChunk = ChunkForDelay(std::floor(minDelay)); + + uint32_t channelCount = 0; + for (int i = oldestChunk; true; i = (i + 1) % chunkCount) { + channelCount = + GetAudioChannelsSuperset(channelCount, mChunks[i].ChannelCount()); + if (i == youngestChunk) { + break; + } + } + + if (channelCount) { + aOutputChunk->AllocateChannels(channelCount); + ReadChannels(aPerFrameDelays, aOutputChunk, 0, channelCount, + aChannelInterpretation); + } else { + aOutputChunk->SetNull(WEBAUDIO_BLOCK_SIZE); + } +} + +void DelayBuffer::ReadChannel(const float aPerFrameDelays[WEBAUDIO_BLOCK_SIZE], + AudioBlock* aOutputChunk, uint32_t aChannel, + ChannelInterpretation aChannelInterpretation) { + if (!mChunks.Length()) { + float* outputChannel = aOutputChunk->ChannelFloatsForWrite(aChannel); + PodZero(outputChannel, WEBAUDIO_BLOCK_SIZE); + return; + } + + ReadChannels(aPerFrameDelays, aOutputChunk, aChannel, 1, + aChannelInterpretation); +} + +void DelayBuffer::ReadChannels(const float aPerFrameDelays[WEBAUDIO_BLOCK_SIZE], + AudioBlock* aOutputChunk, uint32_t aFirstChannel, + uint32_t aNumChannelsToRead, + ChannelInterpretation aChannelInterpretation) { + uint32_t totalChannelCount = aOutputChunk->ChannelCount(); + uint32_t readChannelsEnd = aFirstChannel + aNumChannelsToRead; + MOZ_ASSERT(readChannelsEnd <= totalChannelCount); + + if (mUpmixChannels.Length() != totalChannelCount) { + mLastReadChunk = -1; // invalidate cache + } + + for (uint32_t channel = aFirstChannel; channel < readChannelsEnd; ++channel) { + PodZero(aOutputChunk->ChannelFloatsForWrite(channel), WEBAUDIO_BLOCK_SIZE); + } + + for (unsigned i = 0; i < WEBAUDIO_BLOCK_SIZE; ++i) { + float currentDelay = aPerFrameDelays[i]; + MOZ_ASSERT(currentDelay >= 0.0f); + MOZ_ASSERT(currentDelay <= (mChunks.Length() - 1) * WEBAUDIO_BLOCK_SIZE); + + // Interpolate two input frames in case the read position does not match + // an integer index. + // Use the larger delay, for the older frame, first, as this is more + // likely to use the cached upmixed channel arrays. + int floorDelay = int(currentDelay); + float interpolationFactor = currentDelay - floorDelay; + int positions[2]; + positions[1] = PositionForDelay(floorDelay) + i; + positions[0] = positions[1] - 1; + + for (unsigned tick = 0; tick < ArrayLength(positions); ++tick) { + int readChunk = ChunkForPosition(positions[tick]); + // The zero check on interpolationFactor is important because, when + // currentDelay is integer, positions[0] may be outside the range + // considered for determining totalChannelCount. + // mVolume is not set on default initialized chunks so also handle null + // chunks specially. + if (interpolationFactor != 0.0f && !mChunks[readChunk].IsNull()) { + int readOffset = OffsetForPosition(positions[tick]); + UpdateUpmixChannels(readChunk, totalChannelCount, + aChannelInterpretation); + float multiplier = interpolationFactor * mChunks[readChunk].mVolume; + for (uint32_t channel = aFirstChannel; channel < readChannelsEnd; + ++channel) { + aOutputChunk->ChannelFloatsForWrite(channel)[i] += + multiplier * mUpmixChannels[channel][readOffset]; + } + } + + interpolationFactor = 1.0f - interpolationFactor; + } + } +} + +void DelayBuffer::Read(float aDelayTicks, AudioBlock* aOutputChunk, + ChannelInterpretation aChannelInterpretation) { + float computedDelay[WEBAUDIO_BLOCK_SIZE]; + + for (unsigned i = 0; i < WEBAUDIO_BLOCK_SIZE; ++i) { + computedDelay[i] = aDelayTicks; + } + + Read(computedDelay, aOutputChunk, aChannelInterpretation); +} + +bool DelayBuffer::EnsureBuffer() { + if (mChunks.Length() == 0) { + // The length of the buffer is at least one block greater than the maximum + // delay so that writing an input block does not overwrite the block that + // would subsequently be read at maximum delay. Also round up to the next + // block size, so that no block of writes will need to wrap. + const int chunkCount = (mMaxDelayTicks + 2 * WEBAUDIO_BLOCK_SIZE - 1) >> + WEBAUDIO_BLOCK_SIZE_BITS; + if (!mChunks.SetLength(chunkCount, fallible)) { + return false; + } + + mLastReadChunk = -1; + } + return true; +} + +int DelayBuffer::PositionForDelay(int aDelay) { + // Adding mChunks.Length() keeps integers positive for defined and + // appropriate bitshift, remainder, and bitwise operations. + return ((mCurrentChunk + mChunks.Length()) * WEBAUDIO_BLOCK_SIZE) - aDelay; +} + +int DelayBuffer::ChunkForPosition(int aPosition) { + MOZ_ASSERT(aPosition >= 0); + return (aPosition >> WEBAUDIO_BLOCK_SIZE_BITS) % mChunks.Length(); +} + +int DelayBuffer::OffsetForPosition(int aPosition) { + MOZ_ASSERT(aPosition >= 0); + return aPosition & (WEBAUDIO_BLOCK_SIZE - 1); +} + +int DelayBuffer::ChunkForDelay(int aDelay) { + return ChunkForPosition(PositionForDelay(aDelay)); +} + +void DelayBuffer::UpdateUpmixChannels( + int aNewReadChunk, uint32_t aChannelCount, + ChannelInterpretation aChannelInterpretation) { + if (aNewReadChunk == mLastReadChunk) { + MOZ_ASSERT(mUpmixChannels.Length() == aChannelCount); + return; + } + + NS_WARNING_ASSERTION(mHaveWrittenBlock || aNewReadChunk != mCurrentChunk, + "Smoothing is making feedback delay too small."); + + mLastReadChunk = aNewReadChunk; + mUpmixChannels.ClearAndRetainStorage(); + mUpmixChannels.AppendElements(mChunks[aNewReadChunk].ChannelData<float>()); + MOZ_ASSERT(mUpmixChannels.Length() <= aChannelCount); + if (mUpmixChannels.Length() < aChannelCount) { + if (aChannelInterpretation == ChannelInterpretation::Speakers) { + AudioChannelsUpMix(&mUpmixChannels, aChannelCount, + SilentChannel::ZeroChannel<float>()); + MOZ_ASSERT(mUpmixChannels.Length() == aChannelCount, + "We called GetAudioChannelsSuperset to avoid this"); + } else { + // Fill up the remaining channels with zeros + for (uint32_t channel = mUpmixChannels.Length(); channel < aChannelCount; + ++channel) { + mUpmixChannels.AppendElement(SilentChannel::ZeroChannel<float>()); + } + } + } +} + +} // namespace mozilla diff --git a/dom/media/webaudio/DelayBuffer.h b/dom/media/webaudio/DelayBuffer.h new file mode 100644 index 0000000000..4b1135fa56 --- /dev/null +++ b/dom/media/webaudio/DelayBuffer.h @@ -0,0 +1,105 @@ +/* -*- 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/. */ + +#ifndef DelayBuffer_h_ +#define DelayBuffer_h_ + +#include "nsTArray.h" +#include "AudioBlock.h" +#include "AudioSegment.h" +#include "mozilla/dom/AudioNodeBinding.h" // for ChannelInterpretation + +namespace mozilla { + +class DelayBuffer final { + typedef dom::ChannelInterpretation ChannelInterpretation; + + public: + explicit DelayBuffer(float aMaxDelayTicks) + // Round the maximum delay up to the next tick. + : mMaxDelayTicks(std::ceil(aMaxDelayTicks)), + mCurrentChunk(0) + // mLastReadChunk is initialized in EnsureBuffer +#ifdef DEBUG + , + mHaveWrittenBlock(false) +#endif + { + // The 180 second limit in AudioContext::CreateDelay() and the + // 1 << MEDIA_TIME_FRAC_BITS limit on sample rate provide a limit on the + // maximum delay. + MOZ_ASSERT(aMaxDelayTicks <= + float(std::numeric_limits<decltype(mMaxDelayTicks)>::max())); + } + + // Write a WEBAUDIO_BLOCK_SIZE block for aChannelCount channels. + void Write(const AudioBlock& aInputChunk); + + // Read a block with an array of delays, in ticks, for each sample frame. + // Each delay should be >= 0 and <= MaxDelayTicks(). + void Read(const float aPerFrameDelays[WEBAUDIO_BLOCK_SIZE], + AudioBlock* aOutputChunk, + ChannelInterpretation aChannelInterpretation); + // Read a block with a constant delay. The delay should be >= 0 and + // <= MaxDelayTicks(). + void Read(float aDelayTicks, AudioBlock* aOutputChunk, + ChannelInterpretation aChannelInterpretation); + + // Read into one of the channels of aOutputChunk, given an array of + // delays in ticks. This is useful when delays are different on different + // channels. aOutputChunk must have already been allocated with at least as + // many channels as were in any of the blocks passed to Write(). + void ReadChannel(const float aPerFrameDelays[WEBAUDIO_BLOCK_SIZE], + AudioBlock* aOutputChunk, uint32_t aChannel, + ChannelInterpretation aChannelInterpretation); + + // Advance the buffer pointer + void NextBlock() { + mCurrentChunk = (mCurrentChunk + 1) % mChunks.Length(); +#ifdef DEBUG + MOZ_ASSERT(mHaveWrittenBlock); + mHaveWrittenBlock = false; +#endif + } + + void Reset() { mChunks.Clear(); }; + + int MaxDelayTicks() const { return mMaxDelayTicks; } + + size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const; + + private: + void ReadChannels(const float aPerFrameDelays[WEBAUDIO_BLOCK_SIZE], + AudioBlock* aOutputChunk, uint32_t aFirstChannel, + uint32_t aNumChannelsToRead, + ChannelInterpretation aChannelInterpretation); + bool EnsureBuffer(); + int PositionForDelay(int aDelay); + int ChunkForPosition(int aPosition); + int OffsetForPosition(int aPosition); + int ChunkForDelay(int aDelay); + void UpdateUpmixChannels(int aNewReadChunk, uint32_t channelCount, + ChannelInterpretation aChannelInterpretation); + + // Circular buffer for capturing delayed samples. + FallibleTArray<AudioChunk> mChunks; + // Cached upmixed channel arrays, to avoid repeated allocations. + CopyableAutoTArray<const float*, GUESS_AUDIO_CHANNELS> mUpmixChannels; + // Maximum delay, in ticks + int mMaxDelayTicks; + // The current position in the circular buffer. The next write will be to + // this chunk, and the next read may begin before this chunk. + int mCurrentChunk; + // The chunk owning the pointers in mUpmixChannels + int mLastReadChunk; +#ifdef DEBUG + bool mHaveWrittenBlock; +#endif +}; + +} // namespace mozilla + +#endif // DelayBuffer_h_ diff --git a/dom/media/webaudio/DelayNode.cpp b/dom/media/webaudio/DelayNode.cpp new file mode 100644 index 0000000000..c531756794 --- /dev/null +++ b/dom/media/webaudio/DelayNode.cpp @@ -0,0 +1,223 @@ +/* -*- 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 "DelayNode.h" +#include "mozilla/dom/DelayNodeBinding.h" +#include "AudioNodeEngine.h" +#include "AudioNodeTrack.h" +#include "AudioDestinationNode.h" +#include "WebAudioUtils.h" +#include "DelayBuffer.h" +#include "PlayingRefChangeHandler.h" +#include "Tracing.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_INHERITED(DelayNode, AudioNode, mDelay) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DelayNode) +NS_INTERFACE_MAP_END_INHERITING(AudioNode) + +NS_IMPL_ADDREF_INHERITED(DelayNode, AudioNode) +NS_IMPL_RELEASE_INHERITED(DelayNode, AudioNode) + +class DelayNodeEngine final : public AudioNodeEngine { + typedef PlayingRefChangeHandler PlayingRefChanged; + + public: + DelayNodeEngine(AudioNode* aNode, AudioDestinationNode* aDestination, + float aMaxDelayTicks) + : AudioNodeEngine(aNode), + mDestination(aDestination->Track()) + // Keep the default value in sync with the default value in + // DelayNode::DelayNode. + , + mDelay(0.f) + // Use a smoothing range of 20ms + , + mBuffer( + std::max(aMaxDelayTicks, static_cast<float>(WEBAUDIO_BLOCK_SIZE))), + mMaxDelay(aMaxDelayTicks), + mHaveProducedBeforeInput(false), + mLeftOverData(INT32_MIN) {} + + DelayNodeEngine* AsDelayNodeEngine() override { return this; } + + enum Parameters { + DELAY, + }; + void RecvTimelineEvent(uint32_t aIndex, AudioParamEvent& aEvent) override { + MOZ_ASSERT(mDestination); + aEvent.ConvertToTicks(mDestination); + + switch (aIndex) { + case DELAY: + mDelay.InsertEvent<int64_t>(aEvent); + break; + default: + NS_ERROR("Bad DelayNodeEngine TimelineParameter"); + } + } + + void ProcessBlock(AudioNodeTrack* aTrack, GraphTime aFrom, + const AudioBlock& aInput, AudioBlock* aOutput, + bool* aFinished) override { + MOZ_ASSERT(aTrack->mSampleRate == mDestination->mSampleRate); + TRACE("DelayNodeEngine::ProcessBlock"); + + if (!aInput.IsSilentOrSubnormal()) { + if (mLeftOverData <= 0) { + RefPtr<PlayingRefChanged> refchanged = + new PlayingRefChanged(aTrack, PlayingRefChanged::ADDREF); + aTrack->Graph()->DispatchToMainThreadStableState(refchanged.forget()); + } + mLeftOverData = mBuffer.MaxDelayTicks(); + } else if (mLeftOverData > 0) { + mLeftOverData -= WEBAUDIO_BLOCK_SIZE; + } else { + if (mLeftOverData != INT32_MIN) { + mLeftOverData = INT32_MIN; + aTrack->ScheduleCheckForInactive(); + + // Delete our buffered data now we no longer need it + mBuffer.Reset(); + + RefPtr<PlayingRefChanged> refchanged = + new PlayingRefChanged(aTrack, PlayingRefChanged::RELEASE); + aTrack->Graph()->DispatchToMainThreadStableState(refchanged.forget()); + } + aOutput->SetNull(WEBAUDIO_BLOCK_SIZE); + return; + } + + mBuffer.Write(aInput); + + // Skip output update if mLastChunks has already been set by + // ProduceBlockBeforeInput() when in a cycle. + if (!mHaveProducedBeforeInput) { + UpdateOutputBlock(aTrack, aFrom, aOutput, 0.0); + } + mHaveProducedBeforeInput = false; + mBuffer.NextBlock(); + } + + void UpdateOutputBlock(AudioNodeTrack* aTrack, GraphTime aFrom, + AudioBlock* aOutput, float minDelay) { + float maxDelay = mMaxDelay; + float sampleRate = aTrack->mSampleRate; + ChannelInterpretation channelInterpretation = + aTrack->GetChannelInterpretation(); + if (mDelay.HasSimpleValue()) { + // If this DelayNode is in a cycle, make sure the delay value is at least + // one block, even if that is greater than maxDelay. + float delayFrames = mDelay.GetValue() * sampleRate; + float delayFramesClamped = + std::max(minDelay, std::min(delayFrames, maxDelay)); + mBuffer.Read(delayFramesClamped, aOutput, channelInterpretation); + } else { + // Compute the delay values for the duration of the input AudioChunk + // If this DelayNode is in a cycle, make sure the delay value is at least + // one block. + TrackTime tick = mDestination->GraphTimeToTrackTime(aFrom); + float values[WEBAUDIO_BLOCK_SIZE]; + mDelay.GetValuesAtTime(tick, values, WEBAUDIO_BLOCK_SIZE); + + float computedDelay[WEBAUDIO_BLOCK_SIZE]; + for (size_t counter = 0; counter < WEBAUDIO_BLOCK_SIZE; ++counter) { + float delayAtTick = values[counter] * sampleRate; + float delayAtTickClamped = + std::max(minDelay, std::min(delayAtTick, maxDelay)); + computedDelay[counter] = delayAtTickClamped; + } + mBuffer.Read(computedDelay, aOutput, channelInterpretation); + } + } + + void ProduceBlockBeforeInput(AudioNodeTrack* aTrack, GraphTime aFrom, + AudioBlock* aOutput) override { + if (mLeftOverData <= 0) { + aOutput->SetNull(WEBAUDIO_BLOCK_SIZE); + } else { + UpdateOutputBlock(aTrack, aFrom, aOutput, WEBAUDIO_BLOCK_SIZE); + } + mHaveProducedBeforeInput = true; + } + + bool IsActive() const override { return mLeftOverData != INT32_MIN; } + + size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override { + size_t amount = AudioNodeEngine::SizeOfExcludingThis(aMallocSizeOf); + // Not owned: + // - mDestination - probably not owned + // - mDelay - shares ref with AudioNode, don't count + amount += mBuffer.SizeOfExcludingThis(aMallocSizeOf); + return amount; + } + + size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override { + return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf); + } + + RefPtr<AudioNodeTrack> mDestination; + AudioParamTimeline mDelay; + DelayBuffer mBuffer; + float mMaxDelay; + bool mHaveProducedBeforeInput; + // How much data we have in our buffer which needs to be flushed out when our + // inputs finish. + int32_t mLeftOverData; +}; + +DelayNode::DelayNode(AudioContext* aContext, double aMaxDelay) + : AudioNode(aContext, 2, ChannelCountMode::Max, + ChannelInterpretation::Speakers) { + mDelay = CreateAudioParam(DelayNodeEngine::DELAY, u"delayTime"_ns, 0.0f, 0.f, + aMaxDelay); + DelayNodeEngine* engine = new DelayNodeEngine( + this, aContext->Destination(), aContext->SampleRate() * aMaxDelay); + mTrack = AudioNodeTrack::Create( + aContext, engine, AudioNodeTrack::NO_TRACK_FLAGS, aContext->Graph()); +} + +/* static */ +already_AddRefed<DelayNode> DelayNode::Create(AudioContext& aAudioContext, + const DelayOptions& aOptions, + ErrorResult& aRv) { + if (aOptions.mMaxDelayTime <= 0. || aOptions.mMaxDelayTime >= 180.) { + aRv.ThrowNotSupportedError( + nsPrintfCString("\"maxDelayTime\" (%g) is not in the range (0,180)", + aOptions.mMaxDelayTime)); + return nullptr; + } + + RefPtr<DelayNode> audioNode = + new DelayNode(&aAudioContext, aOptions.mMaxDelayTime); + + audioNode->Initialize(aOptions, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + audioNode->DelayTime()->SetInitialValue(aOptions.mDelayTime); + return audioNode.forget(); +} + +size_t DelayNode::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const { + size_t amount = AudioNode::SizeOfExcludingThis(aMallocSizeOf); + amount += mDelay->SizeOfIncludingThis(aMallocSizeOf); + return amount; +} + +size_t DelayNode::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const { + return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf); +} + +JSObject* DelayNode::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return DelayNode_Binding::Wrap(aCx, this, aGivenProto); +} + +} // namespace mozilla::dom diff --git a/dom/media/webaudio/DelayNode.h b/dom/media/webaudio/DelayNode.h new file mode 100644 index 0000000000..bdfe9eba68 --- /dev/null +++ b/dom/media/webaudio/DelayNode.h @@ -0,0 +1,55 @@ +/* -*- 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/. */ + +#ifndef DelayNode_h_ +#define DelayNode_h_ + +#include "AudioNode.h" +#include "AudioParam.h" + +namespace mozilla::dom { + +class AudioContext; +struct DelayOptions; + +class DelayNode final : public AudioNode { + public: + static already_AddRefed<DelayNode> Create(AudioContext& aAudioContext, + const DelayOptions& aOptions, + ErrorResult& aRv); + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(DelayNode, AudioNode) + + static already_AddRefed<DelayNode> Constructor(const GlobalObject& aGlobal, + AudioContext& aAudioContext, + const DelayOptions& aOptions, + ErrorResult& aRv) { + return Create(aAudioContext, aOptions, aRv); + } + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + AudioParam* DelayTime() const { return mDelay; } + + const char* NodeType() const override { return "DelayNode"; } + + size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override; + size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override; + + private: + DelayNode(AudioContext* aContext, double aMaxDelay); + ~DelayNode() = default; + + friend class DelayNodeEngine; + + RefPtr<AudioParam> mDelay; +}; + +} // namespace mozilla::dom + +#endif diff --git a/dom/media/webaudio/DynamicsCompressorNode.cpp b/dom/media/webaudio/DynamicsCompressorNode.cpp new file mode 100644 index 0000000000..7ec27d9495 --- /dev/null +++ b/dom/media/webaudio/DynamicsCompressorNode.cpp @@ -0,0 +1,226 @@ +/* -*- 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 "DynamicsCompressorNode.h" +#include "mozilla/dom/DynamicsCompressorNodeBinding.h" +#include "AudioNodeEngine.h" +#include "AudioNodeTrack.h" +#include "AudioDestinationNode.h" +#include "WebAudioUtils.h" +#include "blink/DynamicsCompressor.h" +#include "Tracing.h" + +using WebCore::DynamicsCompressor; + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_INHERITED(DynamicsCompressorNode, AudioNode, + mThreshold, mKnee, mRatio, mAttack, mRelease) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DynamicsCompressorNode) +NS_INTERFACE_MAP_END_INHERITING(AudioNode) + +NS_IMPL_ADDREF_INHERITED(DynamicsCompressorNode, AudioNode) +NS_IMPL_RELEASE_INHERITED(DynamicsCompressorNode, AudioNode) + +class DynamicsCompressorNodeEngine final : public AudioNodeEngine { + public: + explicit DynamicsCompressorNodeEngine(AudioNode* aNode, + AudioDestinationNode* aDestination) + : AudioNodeEngine(aNode), + mDestination(aDestination->Track()) + // Keep the default value in sync with the default value in + // DynamicsCompressorNode::DynamicsCompressorNode. + , + mThreshold(-24.f), + mKnee(30.f), + mRatio(12.f), + mAttack(0.003f), + mRelease(0.25f), + mCompressor(new DynamicsCompressor(mDestination->mSampleRate, 2)) {} + + enum Parameters { THRESHOLD, KNEE, RATIO, ATTACK, RELEASE }; + void RecvTimelineEvent(uint32_t aIndex, AudioParamEvent& aEvent) override { + MOZ_ASSERT(mDestination); + + aEvent.ConvertToTicks(mDestination); + + switch (aIndex) { + case THRESHOLD: + mThreshold.InsertEvent<int64_t>(aEvent); + break; + case KNEE: + mKnee.InsertEvent<int64_t>(aEvent); + break; + case RATIO: + mRatio.InsertEvent<int64_t>(aEvent); + break; + case ATTACK: + mAttack.InsertEvent<int64_t>(aEvent); + break; + case RELEASE: + mRelease.InsertEvent<int64_t>(aEvent); + break; + default: + NS_ERROR("Bad DynamicsCompresssorNodeEngine TimelineParameter"); + } + } + + void ProcessBlock(AudioNodeTrack* aTrack, GraphTime aFrom, + const AudioBlock& aInput, AudioBlock* aOutput, + bool* aFinished) override { + TRACE("DynamicsCompressorNodeEngine::ProcessBlock"); + if (aInput.IsNull()) { + // Just output silence + *aOutput = aInput; + return; + } + + const uint32_t channelCount = aInput.ChannelCount(); + if (mCompressor->numberOfChannels() != channelCount) { + // Create a new compressor object with a new channel count + mCompressor = MakeUnique<WebCore::DynamicsCompressor>( + aTrack->mSampleRate, aInput.ChannelCount()); + } + + TrackTime pos = mDestination->GraphTimeToTrackTime(aFrom); + mCompressor->setParameterValue(DynamicsCompressor::ParamThreshold, + mThreshold.GetValueAtTime(pos)); + mCompressor->setParameterValue(DynamicsCompressor::ParamKnee, + mKnee.GetValueAtTime(pos)); + mCompressor->setParameterValue(DynamicsCompressor::ParamRatio, + mRatio.GetValueAtTime(pos)); + mCompressor->setParameterValue(DynamicsCompressor::ParamAttack, + mAttack.GetValueAtTime(pos)); + mCompressor->setParameterValue(DynamicsCompressor::ParamRelease, + mRelease.GetValueAtTime(pos)); + + aOutput->AllocateChannels(channelCount); + mCompressor->process(&aInput, aOutput, aInput.GetDuration()); + + SendReductionParamToMainThread( + aTrack, + mCompressor->parameterValue(DynamicsCompressor::ParamReduction)); + } + + size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override { + // Not owned: + // - mDestination (probably) + // - Don't count the AudioParamTimelines, their inner refs are owned by the + // AudioNode. + size_t amount = AudioNodeEngine::SizeOfExcludingThis(aMallocSizeOf); + amount += mCompressor->sizeOfIncludingThis(aMallocSizeOf); + return amount; + } + + size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override { + return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf); + } + + private: + void SendReductionParamToMainThread(AudioNodeTrack* aTrack, + float aReduction) { + MOZ_ASSERT(!NS_IsMainThread()); + + class Command final : public Runnable { + public: + Command(AudioNodeTrack* aTrack, float aReduction) + : mozilla::Runnable("Command"), + mTrack(aTrack), + mReduction(aReduction) {} + + NS_IMETHOD Run() override { + RefPtr<DynamicsCompressorNode> node = + static_cast<DynamicsCompressorNode*>( + mTrack->Engine()->NodeMainThread()); + if (node) { + node->SetReduction(mReduction); + } + return NS_OK; + } + + private: + RefPtr<AudioNodeTrack> mTrack; + float mReduction; + }; + + AbstractThread::MainThread()->Dispatch( + do_AddRef(new Command(aTrack, aReduction))); + } + + private: + RefPtr<AudioNodeTrack> mDestination; + AudioParamTimeline mThreshold; + AudioParamTimeline mKnee; + AudioParamTimeline mRatio; + AudioParamTimeline mAttack; + AudioParamTimeline mRelease; + UniquePtr<DynamicsCompressor> mCompressor; +}; + +DynamicsCompressorNode::DynamicsCompressorNode(AudioContext* aContext) + : AudioNode(aContext, 2, ChannelCountMode::Clamped_max, + ChannelInterpretation::Speakers), + mReduction(0) { + mThreshold = CreateAudioParam(DynamicsCompressorNodeEngine::THRESHOLD, + u"threshold"_ns, -24.f, -100.f, 0.f); + mKnee = CreateAudioParam(DynamicsCompressorNodeEngine::KNEE, u"knee"_ns, 30.f, + 0.f, 40.f); + mRatio = CreateAudioParam(DynamicsCompressorNodeEngine::RATIO, u"ratio"_ns, + 12.f, 1.f, 20.f); + mAttack = CreateAudioParam(DynamicsCompressorNodeEngine::ATTACK, u"attack"_ns, + 0.003f, 0.f, 1.f); + mRelease = CreateAudioParam(DynamicsCompressorNodeEngine::RELEASE, + u"release"_ns, 0.25f, 0.f, 1.f); + DynamicsCompressorNodeEngine* engine = + new DynamicsCompressorNodeEngine(this, aContext->Destination()); + mTrack = AudioNodeTrack::Create( + aContext, engine, AudioNodeTrack::NO_TRACK_FLAGS, aContext->Graph()); +} + +/* static */ +already_AddRefed<DynamicsCompressorNode> DynamicsCompressorNode::Create( + AudioContext& aAudioContext, const DynamicsCompressorOptions& aOptions, + ErrorResult& aRv) { + RefPtr<DynamicsCompressorNode> audioNode = + new DynamicsCompressorNode(&aAudioContext); + + audioNode->Initialize(aOptions, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + audioNode->Attack()->SetInitialValue(aOptions.mAttack); + audioNode->Knee()->SetInitialValue(aOptions.mKnee); + audioNode->Ratio()->SetInitialValue(aOptions.mRatio); + audioNode->GetRelease()->SetInitialValue(aOptions.mRelease); + audioNode->Threshold()->SetInitialValue(aOptions.mThreshold); + + return audioNode.forget(); +} + +size_t DynamicsCompressorNode::SizeOfExcludingThis( + MallocSizeOf aMallocSizeOf) const { + size_t amount = AudioNode::SizeOfExcludingThis(aMallocSizeOf); + amount += mThreshold->SizeOfIncludingThis(aMallocSizeOf); + amount += mKnee->SizeOfIncludingThis(aMallocSizeOf); + amount += mRatio->SizeOfIncludingThis(aMallocSizeOf); + amount += mAttack->SizeOfIncludingThis(aMallocSizeOf); + amount += mRelease->SizeOfIncludingThis(aMallocSizeOf); + return amount; +} + +size_t DynamicsCompressorNode::SizeOfIncludingThis( + MallocSizeOf aMallocSizeOf) const { + return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf); +} + +JSObject* DynamicsCompressorNode::WrapObject( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) { + return DynamicsCompressorNode_Binding::Wrap(aCx, this, aGivenProto); +} + +} // namespace mozilla::dom diff --git a/dom/media/webaudio/DynamicsCompressorNode.h b/dom/media/webaudio/DynamicsCompressorNode.h new file mode 100644 index 0000000000..1e0f9cda62 --- /dev/null +++ b/dom/media/webaudio/DynamicsCompressorNode.h @@ -0,0 +1,91 @@ +/* -*- 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/. */ + +#ifndef DynamicsCompressorNode_h_ +#define DynamicsCompressorNode_h_ + +#include "AudioNode.h" +#include "AudioParam.h" + +namespace mozilla::dom { + +class AudioContext; +struct DynamicsCompressorOptions; + +class DynamicsCompressorNode final : public AudioNode { + public: + static already_AddRefed<DynamicsCompressorNode> Create( + AudioContext& aAudioContext, const DynamicsCompressorOptions& aOptions, + ErrorResult& aRv); + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(DynamicsCompressorNode, AudioNode) + + static already_AddRefed<DynamicsCompressorNode> Constructor( + const GlobalObject& aGlobal, AudioContext& aAudioContext, + const DynamicsCompressorOptions& aOptions, ErrorResult& aRv) { + return Create(aAudioContext, aOptions, aRv); + } + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + AudioParam* Threshold() const { return mThreshold; } + + AudioParam* Knee() const { return mKnee; } + + AudioParam* Ratio() const { return mRatio; } + + AudioParam* Attack() const { return mAttack; } + + // Called GetRelease to prevent clashing with the nsISupports::Release name + AudioParam* GetRelease() const { return mRelease; } + + float Reduction() const { return mReduction; } + + const char* NodeType() const override { return "DynamicsCompressorNode"; } + + size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override; + size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override; + + void SetReduction(float aReduction) { + MOZ_ASSERT(NS_IsMainThread()); + mReduction = aReduction; + } + + void SetChannelCountModeValue(ChannelCountMode aMode, + ErrorResult& aRv) override { + if (aMode == ChannelCountMode::Max) { + aRv.ThrowNotSupportedError("Cannot set channel count mode to \"max\""); + return; + } + AudioNode::SetChannelCountModeValue(aMode, aRv); + } + + void SetChannelCount(uint32_t aChannelCount, ErrorResult& aRv) override { + if (aChannelCount > 2) { + aRv.ThrowNotSupportedError( + nsPrintfCString("%u is greater than 2", aChannelCount)); + return; + } + AudioNode::SetChannelCount(aChannelCount, aRv); + } + + private: + explicit DynamicsCompressorNode(AudioContext* aContext); + ~DynamicsCompressorNode() = default; + + RefPtr<AudioParam> mThreshold; + RefPtr<AudioParam> mKnee; + RefPtr<AudioParam> mRatio; + float mReduction; + RefPtr<AudioParam> mAttack; + RefPtr<AudioParam> mRelease; +}; + +} // namespace mozilla::dom + +#endif diff --git a/dom/media/webaudio/FFTBlock.cpp b/dom/media/webaudio/FFTBlock.cpp new file mode 100644 index 0000000000..6789ca0264 --- /dev/null +++ b/dom/media/webaudio/FFTBlock.cpp @@ -0,0 +1,231 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=4 sw=2 sts=2 et cindent: */ +/* + * Copyright (C) 2010 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. + * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of + * its contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY APPLE 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 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 "FFTBlock.h" + +#include <complex> + +namespace mozilla { + +typedef std::complex<double> Complex; + +#ifdef MOZ_LIBAV_FFT +FFmpegRDFTFuncs FFTBlock::sRDFTFuncs; +#endif + +static double fdlibm_cabs(const Complex& z) { + return fdlibm_hypot(real(z), imag(z)); +} + +static double fdlibm_carg(const Complex& z) { + return fdlibm_atan2(imag(z), real(z)); +} + +FFTBlock* FFTBlock::CreateInterpolatedBlock(const FFTBlock& block0, + const FFTBlock& block1, + double interp) { + FFTBlock* newBlock = new FFTBlock(block0.FFTSize()); + + newBlock->InterpolateFrequencyComponents(block0, block1, interp); + + // In the time-domain, the 2nd half of the response must be zero, to avoid + // circular convolution aliasing... + int fftSize = newBlock->FFTSize(); + AlignedTArray<float> buffer(fftSize); + newBlock->GetInverseWithoutScaling(buffer.Elements()); + AudioBufferInPlaceScale(buffer.Elements(), 1.0f / fftSize, fftSize / 2); + PodZero(buffer.Elements() + fftSize / 2, fftSize / 2); + + // Put back into frequency domain. + newBlock->PerformFFT(buffer.Elements()); + + return newBlock; +} + +void FFTBlock::InterpolateFrequencyComponents(const FFTBlock& block0, + const FFTBlock& block1, + double interp) { + // FIXME : with some work, this method could be optimized + + ComplexU* dft = mOutputBuffer.Elements(); + + const ComplexU* dft1 = block0.mOutputBuffer.Elements(); + const ComplexU* dft2 = block1.mOutputBuffer.Elements(); + + MOZ_ASSERT(mFFTSize == block0.FFTSize()); + MOZ_ASSERT(mFFTSize == block1.FFTSize()); + double s1base = (1.0 - interp); + double s2base = interp; + + double phaseAccum = 0.0; + double lastPhase1 = 0.0; + double lastPhase2 = 0.0; + + int n = mFFTSize / 2; + + dft[0].r = static_cast<float>(s1base * dft1[0].r + s2base * dft2[0].r); + dft[n].r = static_cast<float>(s1base * dft1[n].r + s2base * dft2[n].r); + + for (int i = 1; i < n; ++i) { + Complex c1(dft1[i].r, dft1[i].i); + Complex c2(dft2[i].r, dft2[i].i); + + double mag1 = fdlibm_cabs(c1); + double mag2 = fdlibm_cabs(c2); + + // Interpolate magnitudes in decibels + double mag1db = 20.0 * fdlibm_log10(mag1); + double mag2db = 20.0 * fdlibm_log10(mag2); + + double s1 = s1base; + double s2 = s2base; + + double magdbdiff = mag1db - mag2db; + + // Empirical tweak to retain higher-frequency zeroes + double threshold = (i > 16) ? 5.0 : 2.0; + + if (magdbdiff < -threshold && mag1db < 0.0) { + s1 = fdlibm_pow(s1, 0.75); + s2 = 1.0 - s1; + } else if (magdbdiff > threshold && mag2db < 0.0) { + s2 = fdlibm_pow(s2, 0.75); + s1 = 1.0 - s2; + } + + // Average magnitude by decibels instead of linearly + double magdb = s1 * mag1db + s2 * mag2db; + double mag = fdlibm_pow(10.0, 0.05 * magdb); + + // Now, deal with phase + double phase1 = fdlibm_carg(c1); + double phase2 = fdlibm_carg(c2); + + double deltaPhase1 = phase1 - lastPhase1; + double deltaPhase2 = phase2 - lastPhase2; + lastPhase1 = phase1; + lastPhase2 = phase2; + + // Unwrap phase deltas + if (deltaPhase1 > M_PI) deltaPhase1 -= 2.0 * M_PI; + if (deltaPhase1 < -M_PI) deltaPhase1 += 2.0 * M_PI; + if (deltaPhase2 > M_PI) deltaPhase2 -= 2.0 * M_PI; + if (deltaPhase2 < -M_PI) deltaPhase2 += 2.0 * M_PI; + + // Blend group-delays + double deltaPhaseBlend; + + if (deltaPhase1 - deltaPhase2 > M_PI) + deltaPhaseBlend = s1 * deltaPhase1 + s2 * (2.0 * M_PI + deltaPhase2); + else if (deltaPhase2 - deltaPhase1 > M_PI) + deltaPhaseBlend = s1 * (2.0 * M_PI + deltaPhase1) + s2 * deltaPhase2; + else + deltaPhaseBlend = s1 * deltaPhase1 + s2 * deltaPhase2; + + phaseAccum += deltaPhaseBlend; + + // Unwrap + if (phaseAccum > M_PI) phaseAccum -= 2.0 * M_PI; + if (phaseAccum < -M_PI) phaseAccum += 2.0 * M_PI; + + dft[i].r = static_cast<float>(mag * fdlibm_cos(phaseAccum)); + dft[i].i = static_cast<float>(mag * fdlibm_sin(phaseAccum)); + } +} + +double FFTBlock::ExtractAverageGroupDelay() { + ComplexU* dft = mOutputBuffer.Elements(); + + double aveSum = 0.0; + double weightSum = 0.0; + double lastPhase = 0.0; + + int halfSize = FFTSize() / 2; + + const double kSamplePhaseDelay = (2.0 * M_PI) / double(FFTSize()); + + // Remove DC offset + dft[0].r = 0.0f; + + // Calculate weighted average group delay + for (int i = 1; i < halfSize; i++) { + Complex c(dft[i].r, dft[i].i); + double mag = fdlibm_cabs(c); + double phase = fdlibm_carg(c); + + double deltaPhase = phase - lastPhase; + lastPhase = phase; + + // Unwrap + if (deltaPhase < -M_PI) deltaPhase += 2.0 * M_PI; + if (deltaPhase > M_PI) deltaPhase -= 2.0 * M_PI; + + aveSum += mag * deltaPhase; + weightSum += mag; + } + + // Note how we invert the phase delta wrt frequency since this is how group + // delay is defined + double ave = aveSum / weightSum; + double aveSampleDelay = -ave / kSamplePhaseDelay; + + // Leave 20 sample headroom (for leading edge of impulse) + aveSampleDelay -= 20.0; + if (aveSampleDelay <= 0.0) return 0.0; + + // Remove average group delay (minus 20 samples for headroom) + AddConstantGroupDelay(-aveSampleDelay); + + return aveSampleDelay; +} + +void FFTBlock::AddConstantGroupDelay(double sampleFrameDelay) { + int halfSize = FFTSize() / 2; + + ComplexU* dft = mOutputBuffer.Elements(); + + const double kSamplePhaseDelay = (2.0 * M_PI) / double(FFTSize()); + + double phaseAdj = -sampleFrameDelay * kSamplePhaseDelay; + + // Add constant group delay + for (int i = 1; i < halfSize; i++) { + Complex c(dft[i].r, dft[i].i); + double mag = fdlibm_cabs(c); + double phase = fdlibm_carg(c); + + phase += i * phaseAdj; + + dft[i].r = static_cast<float>(mag * fdlibm_cos(phase)); + dft[i].i = static_cast<float>(mag * fdlibm_sin(phase)); + } +} + +} // namespace mozilla diff --git a/dom/media/webaudio/FFTBlock.h b/dom/media/webaudio/FFTBlock.h new file mode 100644 index 0000000000..6af882f9af --- /dev/null +++ b/dom/media/webaudio/FFTBlock.h @@ -0,0 +1,348 @@ +/* -*- 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/. */ + +#ifndef FFTBlock_h_ +#define FFTBlock_h_ + +#ifdef BUILD_ARM_NEON +# include <cmath> +# include "mozilla/arm.h" +# include "dl/sp/api/omxSP.h" +#endif + +#include "AlignedTArray.h" +#include "AudioNodeEngine.h" +#if defined(MOZ_LIBAV_FFT) +# include "FFmpegRDFTTypes.h" +# include "FFVPXRuntimeLinker.h" +#else +# include "kiss_fft/kiss_fftr.h" +#endif + +namespace mozilla { + +// This class defines an FFT block, loosely modeled after Blink's FFTFrame +// class to make sharing code with Blink easy. +// Currently it's implemented on top of KissFFT on all platforms. +class FFTBlock final { + union ComplexU { +#if !defined(MOZ_LIBAV_FFT) + kiss_fft_cpx c; +#endif + float f[2]; + struct { + float r; + float i; + }; + }; + + public: + static void MainThreadInit() { +#ifdef MOZ_LIBAV_FFT + FFVPXRuntimeLinker::Init(); + if (!sRDFTFuncs.init) { + FFVPXRuntimeLinker::GetRDFTFuncs(&sRDFTFuncs); + } +#endif + } + + explicit FFTBlock(uint32_t aFFTSize) +#if defined(MOZ_LIBAV_FFT) + : mAvRDFT(nullptr), + mAvIRDFT(nullptr) +#else + : mKissFFT(nullptr), + mKissIFFT(nullptr) +# ifdef BUILD_ARM_NEON + , + mOmxFFT(nullptr), + mOmxIFFT(nullptr) +# endif +#endif + { + MOZ_COUNT_CTOR(FFTBlock); + SetFFTSize(aFFTSize); + } + ~FFTBlock() { + MOZ_COUNT_DTOR(FFTBlock); + Clear(); + } + + // Return a new FFTBlock with frequency components interpolated between + // |block0| and |block1| with |interp| between 0.0 and 1.0. + static FFTBlock* CreateInterpolatedBlock(const FFTBlock& block0, + const FFTBlock& block1, + double interp); + + // Transform FFTSize() points of aData and store the result internally. + void PerformFFT(const float* aData) { + if (!EnsureFFT()) { + return; + } + +#if defined(MOZ_LIBAV_FFT) + PodCopy(mOutputBuffer.Elements()->f, aData, mFFTSize); + sRDFTFuncs.calc(mAvRDFT, mOutputBuffer.Elements()->f); + // Recover packed Nyquist. + mOutputBuffer[mFFTSize / 2].r = mOutputBuffer[0].i; + mOutputBuffer[0].i = 0.0f; +#else +# ifdef BUILD_ARM_NEON + if (mozilla::supports_neon()) { + omxSP_FFTFwd_RToCCS_F32_Sfs(aData, mOutputBuffer.Elements()->f, mOmxFFT); + } else +# endif + { + kiss_fftr(mKissFFT, aData, &(mOutputBuffer.Elements()->c)); + } +#endif + } + // Inverse-transform internal data and store the resulting FFTSize() + // points in aDataOut. + void GetInverse(float* aDataOut) { + GetInverseWithoutScaling(aDataOut); + AudioBufferInPlaceScale(aDataOut, 1.0f / mFFTSize, mFFTSize); + } + + // Inverse-transform internal frequency data and store the resulting + // FFTSize() points in |aDataOut|. If frequency data has not already been + // scaled, then the output will need scaling by 1/FFTSize(). + void GetInverseWithoutScaling(float* aDataOut) { + if (!EnsureIFFT()) { + std::fill_n(aDataOut, mFFTSize, 0.0f); + return; + }; + +#if defined(MOZ_LIBAV_FFT) + { + // Even though this function doesn't scale, the libav forward transform + // gives a value that needs scaling by 2 in order for things to turn out + // similar to how we expect from kissfft/openmax. + AudioBufferCopyWithScale(mOutputBuffer.Elements()->f, 2.0f, aDataOut, + mFFTSize); + aDataOut[1] = 2.0f * mOutputBuffer[mFFTSize / 2].r; // Packed Nyquist + sRDFTFuncs.calc(mAvIRDFT, aDataOut); + } +#else +# ifdef BUILD_ARM_NEON + if (mozilla::supports_neon()) { + omxSP_FFTInv_CCSToR_F32_Sfs_unscaled(mOutputBuffer.Elements()->f, + aDataOut, mOmxIFFT); + } else +# endif + { + kiss_fftri(mKissIFFT, &(mOutputBuffer.Elements()->c), aDataOut); + } +#endif + } + + void Multiply(const FFTBlock& aFrame) { + uint32_t halfSize = mFFTSize / 2; + // DFTs are not packed. + MOZ_ASSERT(mOutputBuffer[0].i == 0); + MOZ_ASSERT(aFrame.mOutputBuffer[0].i == 0); + + BufferComplexMultiply(mOutputBuffer.Elements()->f, + aFrame.mOutputBuffer.Elements()->f, + mOutputBuffer.Elements()->f, halfSize); + mOutputBuffer[halfSize].r *= aFrame.mOutputBuffer[halfSize].r; + // This would have been set to NaN if either real component was NaN. + mOutputBuffer[0].i = 0.0f; + } + + // Perform a forward FFT on |aData|, assuming zeros after dataSize samples, + // and pre-scale the generated internal frequency domain coefficients so + // that GetInverseWithoutScaling() can be used to transform to the time + // domain. This is useful for convolution kernels. + void PadAndMakeScaledDFT(const float* aData, size_t dataSize) { + MOZ_ASSERT(dataSize <= FFTSize()); + AlignedTArray<float> paddedData; + paddedData.SetLength(FFTSize()); + AudioBufferCopyWithScale(aData, 1.0f / FFTSize(), paddedData.Elements(), + dataSize); + PodZero(paddedData.Elements() + dataSize, mFFTSize - dataSize); + PerformFFT(paddedData.Elements()); + } + + // aSize must be a power of 2 + void SetFFTSize(uint32_t aSize) { + MOZ_ASSERT(CountPopulation32(aSize) == 1); + mFFTSize = aSize; + mOutputBuffer.SetLength(aSize / 2 + 1); + PodZero(mOutputBuffer.Elements(), aSize / 2 + 1); + Clear(); + } + + // Return the average group delay and removes this from the frequency data. + double ExtractAverageGroupDelay(); + + uint32_t FFTSize() const { return mFFTSize; } + float RealData(uint32_t aIndex) const { return mOutputBuffer[aIndex].r; } + float& RealData(uint32_t aIndex) { return mOutputBuffer[aIndex].r; } + float ImagData(uint32_t aIndex) const { return mOutputBuffer[aIndex].i; } + float& ImagData(uint32_t aIndex) { return mOutputBuffer[aIndex].i; } + + size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const { + size_t amount = 0; + +#if defined(MOZ_LIBAV_FFT) + auto ComputedSizeOfContextIfSet = [this](void* aContext) -> size_t { + if (!aContext) { + return 0; + } + // RDFTContext is only forward declared in public headers, but this is + // an estimate based on a value of 231 seen requested from + // _aligned_alloc on Win64. Don't use malloc_usable_size() because the + // context pointer is not necessarily from malloc. + size_t amount = 232; + // Add size of allocations performed in ff_fft_init(). + // The maximum FFT size used is 32768 = 2^15 and so revtab32 is not + // allocated. + MOZ_ASSERT(mFFTSize <= 32768); + amount += mFFTSize * (sizeof(uint16_t) + 2 * sizeof(float)); + + return amount; + }; + + amount += ComputedSizeOfContextIfSet(mAvRDFT); + amount += ComputedSizeOfContextIfSet(mAvIRDFT); +#else +# ifdef BUILD_ARM_NEON + amount += aMallocSizeOf(mOmxFFT); + amount += aMallocSizeOf(mOmxIFFT); +# endif +# ifdef USE_SIMD +# error kiss fft uses malloc only when USE_SIMD is not defined +# endif + amount += aMallocSizeOf(mKissFFT); + amount += aMallocSizeOf(mKissIFFT); +#endif + amount += mOutputBuffer.ShallowSizeOfExcludingThis(aMallocSizeOf); + return amount; + } + + size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const { + return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf); + } + + private: + FFTBlock(const FFTBlock& other) = delete; + void operator=(const FFTBlock& other) = delete; + + bool EnsureFFT() { +#if defined(MOZ_LIBAV_FFT) + if (!mAvRDFT) { + if (!sRDFTFuncs.init) { + return false; + } + + mAvRDFT = sRDFTFuncs.init(FloorLog2(mFFTSize), DFT_R2C); + } +#else +# ifdef BUILD_ARM_NEON + if (mozilla::supports_neon()) { + if (!mOmxFFT) { + mOmxFFT = createOmxFFT(mFFTSize); + } + } else +# endif + { + if (!mKissFFT) { + mKissFFT = kiss_fftr_alloc(mFFTSize, 0, nullptr, nullptr); + } + } +#endif + return true; + } + + bool EnsureIFFT() { +#if defined(MOZ_LIBAV_FFT) + if (!mAvIRDFT) { + if (!sRDFTFuncs.init) { + return false; + } + + mAvIRDFT = sRDFTFuncs.init(FloorLog2(mFFTSize), IDFT_C2R); + } +#else +# ifdef BUILD_ARM_NEON + if (mozilla::supports_neon()) { + if (!mOmxIFFT) { + mOmxIFFT = createOmxFFT(mFFTSize); + } + } else +# endif + { + if (!mKissIFFT) { + mKissIFFT = kiss_fftr_alloc(mFFTSize, 1, nullptr, nullptr); + } + } +#endif + return true; + } + +#ifdef BUILD_ARM_NEON + static OMXFFTSpec_R_F32* createOmxFFT(uint32_t aFFTSize) { + MOZ_ASSERT((aFFTSize & (aFFTSize - 1)) == 0); + OMX_INT bufSize; + OMX_INT order = FloorLog2(aFFTSize); + MOZ_ASSERT(aFFTSize >> order == 1); + OMXResult status = omxSP_FFTGetBufSize_R_F32(order, &bufSize); + if (status == OMX_Sts_NoErr) { + OMXFFTSpec_R_F32* context = + static_cast<OMXFFTSpec_R_F32*>(malloc(bufSize)); + if (omxSP_FFTInit_R_F32(context, order) != OMX_Sts_NoErr) { + return nullptr; + } + return context; + } + return nullptr; + } +#endif + + void Clear() { +#if defined(MOZ_LIBAV_FFT) + if (mAvRDFT) { + sRDFTFuncs.end(mAvRDFT); + mAvRDFT = nullptr; + } + if (mAvIRDFT) { + sRDFTFuncs.end(mAvIRDFT); + mAvIRDFT = nullptr; + } +#else +# ifdef BUILD_ARM_NEON + free(mOmxFFT); + free(mOmxIFFT); + mOmxFFT = mOmxIFFT = nullptr; +# endif + free(mKissFFT); + free(mKissIFFT); + mKissFFT = mKissIFFT = nullptr; +#endif + } + void AddConstantGroupDelay(double sampleFrameDelay); + void InterpolateFrequencyComponents(const FFTBlock& block0, + const FFTBlock& block1, double interp); +#if defined(MOZ_LIBAV_FFT) + static FFmpegRDFTFuncs sRDFTFuncs; + RDFTContext* mAvRDFT; + RDFTContext* mAvIRDFT; +#else + kiss_fftr_cfg mKissFFT; + kiss_fftr_cfg mKissIFFT; +# ifdef BUILD_ARM_NEON + OMXFFTSpec_R_F32* mOmxFFT; + OMXFFTSpec_R_F32* mOmxIFFT; +# endif +#endif + AlignedTArray<ComplexU> mOutputBuffer; + uint32_t mFFTSize; +}; + +} // namespace mozilla + +#endif diff --git a/dom/media/webaudio/GainNode.cpp b/dom/media/webaudio/GainNode.cpp new file mode 100644 index 0000000000..ed998ee2a9 --- /dev/null +++ b/dom/media/webaudio/GainNode.cpp @@ -0,0 +1,149 @@ +/* -*- 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 "GainNode.h" +#include "mozilla/dom/GainNodeBinding.h" +#include "AlignmentUtils.h" +#include "AudioNodeEngine.h" +#include "AudioNodeTrack.h" +#include "AudioDestinationNode.h" +#include "WebAudioUtils.h" +#include "Tracing.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_INHERITED(GainNode, AudioNode, mGain) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(GainNode) +NS_INTERFACE_MAP_END_INHERITING(AudioNode) + +NS_IMPL_ADDREF_INHERITED(GainNode, AudioNode) +NS_IMPL_RELEASE_INHERITED(GainNode, AudioNode) + +class GainNodeEngine final : public AudioNodeEngine { + public: + GainNodeEngine(AudioNode* aNode, AudioDestinationNode* aDestination) + : AudioNodeEngine(aNode), + mDestination(aDestination->Track()) + // Keep the default value in sync with the default value in + // GainNode::GainNode. + , + mGain(1.f) {} + + enum Parameters { GAIN }; + void RecvTimelineEvent(uint32_t aIndex, AudioParamEvent& aEvent) override { + MOZ_ASSERT(mDestination); + aEvent.ConvertToTicks(mDestination); + + switch (aIndex) { + case GAIN: + mGain.InsertEvent<int64_t>(aEvent); + break; + default: + NS_ERROR("Bad GainNodeEngine TimelineParameter"); + } + } + + void ProcessBlock(AudioNodeTrack* aTrack, GraphTime aFrom, + const AudioBlock& aInput, AudioBlock* aOutput, + bool* aFinished) override { + TRACE("GainNodeEngine::ProcessBlock"); + if (aInput.IsNull()) { + // If input is silent, so is the output + aOutput->SetNull(WEBAUDIO_BLOCK_SIZE); + } else if (mGain.HasSimpleValue()) { + // Optimize the case where we only have a single value set as the volume + float gain = mGain.GetValue(); + if (gain == 0.0f) { + aOutput->SetNull(WEBAUDIO_BLOCK_SIZE); + } else { + *aOutput = aInput; + aOutput->mVolume *= gain; + } + } else { + // First, compute a vector of gains for each track tick based on the + // timeline at hand, and then for each channel, multiply the values + // in the buffer with the gain vector. + aOutput->AllocateChannels(aInput.ChannelCount()); + + // Compute the gain values for the duration of the input AudioChunk + TrackTime tick = mDestination->GraphTimeToTrackTime(aFrom); + float computedGain[WEBAUDIO_BLOCK_SIZE + 4]; + float* alignedComputedGain = ALIGNED16(computedGain); + ASSERT_ALIGNED16(alignedComputedGain); + mGain.GetValuesAtTime(tick, alignedComputedGain, WEBAUDIO_BLOCK_SIZE); + + for (size_t counter = 0; counter < WEBAUDIO_BLOCK_SIZE; ++counter) { + alignedComputedGain[counter] *= aInput.mVolume; + } + + // Apply the gain to the output buffer + for (size_t channel = 0; channel < aOutput->ChannelCount(); ++channel) { + const float* inputBuffer = + static_cast<const float*>(aInput.mChannelData[channel]); + float* buffer = aOutput->ChannelFloatsForWrite(channel); + AudioBlockCopyChannelWithScale(inputBuffer, alignedComputedGain, + buffer); + } + } + } + + size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override { + // Not owned: + // - mDestination - MediaTrackGraphImpl::CollectSizesForMemoryReport() + // accounts for mDestination. + // - mGain - Internal ref owned by AudioNode + return AudioNodeEngine::SizeOfExcludingThis(aMallocSizeOf); + } + + size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override { + return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf); + } + + RefPtr<AudioNodeTrack> mDestination; + AudioParamTimeline mGain; +}; + +GainNode::GainNode(AudioContext* aContext) + : AudioNode(aContext, 2, ChannelCountMode::Max, + ChannelInterpretation::Speakers) { + mGain = CreateAudioParam(GainNodeEngine::GAIN, u"gain"_ns, 1.0f); + GainNodeEngine* engine = new GainNodeEngine(this, aContext->Destination()); + mTrack = AudioNodeTrack::Create( + aContext, engine, AudioNodeTrack::NO_TRACK_FLAGS, aContext->Graph()); +} + +/* static */ +already_AddRefed<GainNode> GainNode::Create(AudioContext& aAudioContext, + const GainOptions& aOptions, + ErrorResult& aRv) { + RefPtr<GainNode> audioNode = new GainNode(&aAudioContext); + + audioNode->Initialize(aOptions, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + audioNode->Gain()->SetInitialValue(aOptions.mGain); + return audioNode.forget(); +} + +size_t GainNode::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const { + size_t amount = AudioNode::SizeOfExcludingThis(aMallocSizeOf); + amount += mGain->SizeOfIncludingThis(aMallocSizeOf); + return amount; +} + +size_t GainNode::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const { + return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf); +} + +JSObject* GainNode::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return GainNode_Binding::Wrap(aCx, this, aGivenProto); +} + +} // namespace mozilla::dom diff --git a/dom/media/webaudio/GainNode.h b/dom/media/webaudio/GainNode.h new file mode 100644 index 0000000000..d2790af746 --- /dev/null +++ b/dom/media/webaudio/GainNode.h @@ -0,0 +1,53 @@ +/* -*- 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/. */ + +#ifndef GainNode_h_ +#define GainNode_h_ + +#include "AudioNode.h" +#include "AudioParam.h" + +namespace mozilla::dom { + +class AudioContext; +struct GainOptions; + +class GainNode final : public AudioNode { + public: + static already_AddRefed<GainNode> Create(AudioContext& aAudioContext, + const GainOptions& aOptions, + ErrorResult& aRv); + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(GainNode, AudioNode) + + static already_AddRefed<GainNode> Constructor(const GlobalObject& aGlobal, + AudioContext& aAudioContext, + const GainOptions& aOptions, + ErrorResult& aRv) { + return Create(aAudioContext, aOptions, aRv); + } + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + AudioParam* Gain() const { return mGain; } + + const char* NodeType() const override { return "GainNode"; } + + size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override; + size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override; + + private: + explicit GainNode(AudioContext* aContext); + ~GainNode() = default; + + RefPtr<AudioParam> mGain; +}; + +} // namespace mozilla::dom + +#endif diff --git a/dom/media/webaudio/IIRFilterNode.cpp b/dom/media/webaudio/IIRFilterNode.cpp new file mode 100644 index 0000000000..fe87744d4d --- /dev/null +++ b/dom/media/webaudio/IIRFilterNode.cpp @@ -0,0 +1,266 @@ +/* -*- 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 "IIRFilterNode.h" +#include "AudioNodeEngine.h" +#include "AudioDestinationNode.h" +#include "blink/IIRFilter.h" +#include "PlayingRefChangeHandler.h" +#include "AlignmentUtils.h" +#include "nsPrintfCString.h" + +#include "nsGkAtoms.h" +#include "Tracing.h" + +namespace mozilla::dom { + +class IIRFilterNodeEngine final : public AudioNodeEngine { + public: + IIRFilterNodeEngine(AudioNode* aNode, AudioDestinationNode* aDestination, + const AudioDoubleArray& aFeedforward, + const AudioDoubleArray& aFeedback, uint64_t aWindowID) + : AudioNodeEngine(aNode), + mDestination(aDestination->Track()), + mFeedforward(aFeedforward.Clone()), + mFeedback(aFeedback.Clone()), + mWindowID(aWindowID) {} + + void ProcessBlock(AudioNodeTrack* aTrack, GraphTime aFrom, + const AudioBlock& aInput, AudioBlock* aOutput, + bool* aFinished) override { + TRACE("IIRFilterNodeEngine::ProcessBlock"); + float inputBuffer[WEBAUDIO_BLOCK_SIZE + 4]; + float* alignedInputBuffer = ALIGNED16(inputBuffer); + ASSERT_ALIGNED16(alignedInputBuffer); + + if (aInput.IsNull()) { + if (!mIIRFilters.IsEmpty()) { + bool allZero = true; + for (uint32_t i = 0; i < mIIRFilters.Length(); ++i) { + allZero &= mIIRFilters[i]->buffersAreZero(); + } + + // all filter buffer values are zero, so the output will be zero + // as well. + if (allZero) { + mIIRFilters.Clear(); + aTrack->ScheduleCheckForInactive(); + + RefPtr<PlayingRefChangeHandler> refchanged = + new PlayingRefChangeHandler(aTrack, + PlayingRefChangeHandler::RELEASE); + aTrack->Graph()->DispatchToMainThreadStableState(refchanged.forget()); + + aOutput->SetNull(WEBAUDIO_BLOCK_SIZE); + return; + } + + PodZero(alignedInputBuffer, WEBAUDIO_BLOCK_SIZE); + } + } else if (mIIRFilters.Length() != aInput.ChannelCount()) { + if (mIIRFilters.IsEmpty()) { + RefPtr<PlayingRefChangeHandler> refchanged = + new PlayingRefChangeHandler(aTrack, + PlayingRefChangeHandler::ADDREF); + aTrack->Graph()->DispatchToMainThreadStableState(refchanged.forget()); + } else { + WebAudioUtils::LogToDeveloperConsole( + mWindowID, "IIRFilterChannelCountChangeWarning"); + } + + // Adjust the number of filters based on the number of channels + mIIRFilters.SetLength(aInput.ChannelCount()); + for (size_t i = 0; i < aInput.ChannelCount(); ++i) { + mIIRFilters[i] = + MakeUnique<blink::IIRFilter>(&mFeedforward, &mFeedback); + } + } + + uint32_t numberOfChannels = mIIRFilters.Length(); + aOutput->AllocateChannels(numberOfChannels); + + for (uint32_t i = 0; i < numberOfChannels; ++i) { + const float* input; + if (aInput.IsNull()) { + input = alignedInputBuffer; + } else { + input = static_cast<const float*>(aInput.mChannelData[i]); + if (aInput.mVolume != 1.0) { + AudioBlockCopyChannelWithScale(input, aInput.mVolume, + alignedInputBuffer); + input = alignedInputBuffer; + } + } + + mIIRFilters[i]->process(input, aOutput->ChannelFloatsForWrite(i), + aInput.GetDuration()); + } + } + + bool IsActive() const override { return !mIIRFilters.IsEmpty(); } + + size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override { + // Not owned: + // - mDestination - probably not owned + // - AudioParamTimelines - counted in the AudioNode + size_t amount = AudioNodeEngine::SizeOfExcludingThis(aMallocSizeOf); + amount += mIIRFilters.ShallowSizeOfExcludingThis(aMallocSizeOf); + return amount; + } + + size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override { + return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf); + } + + private: + RefPtr<AudioNodeTrack> mDestination; + nsTArray<UniquePtr<blink::IIRFilter>> mIIRFilters; + AudioDoubleArray mFeedforward; + AudioDoubleArray mFeedback; + uint64_t mWindowID; +}; + +IIRFilterNode::IIRFilterNode(AudioContext* aContext, + const Sequence<double>& aFeedforward, + const Sequence<double>& aFeedback) + : AudioNode(aContext, 2, ChannelCountMode::Max, + ChannelInterpretation::Speakers) { + mFeedforward.SetLength(aFeedforward.Length()); + PodCopy(mFeedforward.Elements(), aFeedforward.Elements(), + aFeedforward.Length()); + mFeedback.SetLength(aFeedback.Length()); + PodCopy(mFeedback.Elements(), aFeedback.Elements(), aFeedback.Length()); + + // Scale coefficients -- we guarantee that mFeedback != 0 when creating + // the IIRFilterNode. + double scale = mFeedback[0]; + double* elements = mFeedforward.Elements(); + for (size_t i = 0; i < mFeedforward.Length(); ++i) { + elements[i] /= scale; + } + + elements = mFeedback.Elements(); + for (size_t i = 0; i < mFeedback.Length(); ++i) { + elements[i] /= scale; + } + + // We check that this is exactly equal to one later in blink/IIRFilter.cpp + elements[0] = 1.0; + + uint64_t windowID = 0; + if (aContext->GetParentObject()) { + windowID = aContext->GetParentObject()->WindowID(); + } + IIRFilterNodeEngine* engine = new IIRFilterNodeEngine( + this, aContext->Destination(), mFeedforward, mFeedback, windowID); + mTrack = AudioNodeTrack::Create( + aContext, engine, AudioNodeTrack::NO_TRACK_FLAGS, aContext->Graph()); +} + +/* static */ +already_AddRefed<IIRFilterNode> IIRFilterNode::Create( + AudioContext& aAudioContext, const IIRFilterOptions& aOptions, + ErrorResult& aRv) { + if (aOptions.mFeedforward.Length() == 0 || + aOptions.mFeedforward.Length() > 20) { + aRv.ThrowNotSupportedError( + nsPrintfCString("\"feedforward\" length %zu is not in the range [1,20]", + aOptions.mFeedforward.Length())); + return nullptr; + } + + if (aOptions.mFeedback.Length() == 0 || aOptions.mFeedback.Length() > 20) { + aRv.ThrowNotSupportedError( + nsPrintfCString("\"feedback\" length %zu is not in the range [1,20]", + aOptions.mFeedback.Length())); + return nullptr; + } + + bool feedforwardAllZeros = true; + for (size_t i = 0; i < aOptions.mFeedforward.Length(); ++i) { + if (aOptions.mFeedforward.Elements()[i] != 0.0) { + feedforwardAllZeros = false; + break; + } + } + + if (feedforwardAllZeros) { + aRv.ThrowInvalidStateError( + "\"feedforward\" must contain some nonzero values"); + return nullptr; + } + + if (aOptions.mFeedback[0] == 0.0) { + aRv.ThrowInvalidStateError("First value in \"feedback\" must be nonzero"); + return nullptr; + } + + RefPtr<IIRFilterNode> audioNode = new IIRFilterNode( + &aAudioContext, aOptions.mFeedforward, aOptions.mFeedback); + + audioNode->Initialize(aOptions, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + return audioNode.forget(); +} + +size_t IIRFilterNode::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const { + size_t amount = AudioNode::SizeOfExcludingThis(aMallocSizeOf); + return amount; +} + +size_t IIRFilterNode::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const { + return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf); +} + +JSObject* IIRFilterNode::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return IIRFilterNode_Binding::Wrap(aCx, this, aGivenProto); +} + +void IIRFilterNode::GetFrequencyResponse(const Float32Array& aFrequencyHz, + const Float32Array& aMagResponse, + const Float32Array& aPhaseResponse) { + aFrequencyHz.ProcessData([&](const Span<float>& aFrequencyData, + JS::AutoCheckCannotGC&&) { + aMagResponse.ProcessData([&](const Span<float>& aMagData, + JS::AutoCheckCannotGC&&) { + aPhaseResponse.ProcessData([&](const Span<float>& aPhaseData, + JS::AutoCheckCannotGC&&) { + uint32_t length = std::min( + {aFrequencyData.Length(), aMagData.Length(), aPhaseData.Length()}); + if (!length) { + return; + } + + auto frequencies = MakeUniqueForOverwriteFallible<float[]>(length); + if (!frequencies) { + return; + } + + const double nyquist = Context()->SampleRate() * 0.5; + + // Normalize the frequencies + std::transform(aFrequencyData.begin(), aFrequencyData.begin() + length, + frequencies.get(), [&](float aFrequency) { + if (aFrequency >= 0 && aFrequency <= nyquist) { + return static_cast<float>(aFrequency / nyquist); + } + + return std::numeric_limits<float>::quiet_NaN(); + }); + + blink::IIRFilter filter(&mFeedforward, &mFeedback); + filter.getFrequencyResponse(int(length), frequencies.get(), + aMagData.Elements(), aPhaseData.Elements()); + }); + }); + }); +} + +} // namespace mozilla::dom diff --git a/dom/media/webaudio/IIRFilterNode.h b/dom/media/webaudio/IIRFilterNode.h new file mode 100644 index 0000000000..d79f87f632 --- /dev/null +++ b/dom/media/webaudio/IIRFilterNode.h @@ -0,0 +1,56 @@ +/* -*- 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/. */ + +#ifndef IIRFilterNode_h_ +#define IIRFilterNode_h_ + +#include "AudioNode.h" +#include "AudioParam.h" +#include "mozilla/dom/IIRFilterNodeBinding.h" + +namespace mozilla::dom { + +class AudioContext; +struct IIRFilterOptions; + +class IIRFilterNode final : public AudioNode { + public: + static already_AddRefed<IIRFilterNode> Create( + AudioContext& aAudioContext, const IIRFilterOptions& aOptions, + ErrorResult& aRv); + + NS_INLINE_DECL_REFCOUNTING_INHERITED(IIRFilterNode, AudioNode) + + static already_AddRefed<IIRFilterNode> Constructor( + const GlobalObject& aGlobal, AudioContext& aAudioContext, + const IIRFilterOptions& aOptions, ErrorResult& aRv) { + return Create(aAudioContext, aOptions, aRv); + } + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + void GetFrequencyResponse(const Float32Array& aFrequencyHz, + const Float32Array& aMagResponse, + const Float32Array& aPhaseResponse); + + const char* NodeType() const override { return "IIRFilterNode"; } + + size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override; + size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override; + + private: + IIRFilterNode(AudioContext* aContext, const Sequence<double>& aFeedforward, + const Sequence<double>& aFeedback); + ~IIRFilterNode() = default; + + nsTArray<double> mFeedback; + nsTArray<double> mFeedforward; +}; + +} // namespace mozilla::dom + +#endif diff --git a/dom/media/webaudio/MediaBufferDecoder.cpp b/dom/media/webaudio/MediaBufferDecoder.cpp new file mode 100644 index 0000000000..425c0639b5 --- /dev/null +++ b/dom/media/webaudio/MediaBufferDecoder.cpp @@ -0,0 +1,764 @@ +/* -*- 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 "MediaBufferDecoder.h" + +#include <speex/speex_resampler.h> + +#include "AudioBuffer.h" +#include "AudioContext.h" +#include "AudioNodeEngine.h" +#include "BufferMediaResource.h" +#include "DecoderTraits.h" +#include "MediaContainerType.h" +#include "MediaDataDecoderProxy.h" +#include "MediaDataDemuxer.h" +#include "MediaQueue.h" +#include "PDMFactory.h" +#include "VideoUtils.h" +#include "WebAudioUtils.h" +#include "js/MemoryFunctions.h" +#include "mozilla/AbstractThread.h" +#include "mozilla/Logging.h" +#include "mozilla/StaticPrefs_media.h" +#include "mozilla/Telemetry.h" +#include "mozilla/dom/AudioContextBinding.h" +#include "mozilla/dom/BaseAudioContextBinding.h" +#include "mozilla/dom/DOMException.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/ScriptSettings.h" +#include "nsComponentManagerUtils.h" +#include "nsContentUtils.h" +#include "nsIScriptError.h" +#include "nsIScriptObjectPrincipal.h" +#include "nsMimeTypes.h" +#include "nsPrintfCString.h" +#include "nsXPCOMCIDInternal.h" + +namespace mozilla { + +extern LazyLogModule gMediaDecoderLog; + +#define LOG(x, ...) \ + MOZ_LOG(gMediaDecoderLog, LogLevel::Debug, (x, ##__VA_ARGS__)) + +using namespace dom; + +class ReportResultTask final : public Runnable { + public: + ReportResultTask(WebAudioDecodeJob& aDecodeJob, + WebAudioDecodeJob::ResultFn aFunction, + WebAudioDecodeJob::ErrorCode aErrorCode) + : Runnable("ReportResultTask"), + mDecodeJob(aDecodeJob), + mFunction(aFunction), + mErrorCode(aErrorCode) { + MOZ_ASSERT(aFunction); + } + + NS_IMETHOD Run() override { + MOZ_ASSERT(NS_IsMainThread()); + + (mDecodeJob.*mFunction)(mErrorCode); + + return NS_OK; + } + + private: + // Note that the mDecodeJob member will probably die when mFunction is run. + // Therefore, it is not safe to do anything fancy with it in this class. + // Really, this class is only used because nsRunnableMethod doesn't support + // methods accepting arguments. + WebAudioDecodeJob& mDecodeJob; + WebAudioDecodeJob::ResultFn mFunction; + WebAudioDecodeJob::ErrorCode mErrorCode; +}; + +enum class PhaseEnum : int { Decode, AllocateBuffer, Done }; + +class MediaDecodeTask final : public Runnable { + public: + MediaDecodeTask(const MediaContainerType& aContainerType, uint8_t* aBuffer, + uint32_t aLength, WebAudioDecodeJob& aDecodeJob) + : Runnable("MediaDecodeTask"), + mContainerType(aContainerType), + mBuffer(aBuffer), + mLength(aLength), + mBatchSize(StaticPrefs::media_rdd_webaudio_batch_size()), + mDecodeJob(aDecodeJob), + mPhase(PhaseEnum::Decode) { + MOZ_ASSERT(aBuffer); + MOZ_ASSERT(NS_IsMainThread()); + } + + // MOZ_CAN_RUN_SCRIPT_BOUNDARY until Runnable::Run is MOZ_CAN_RUN_SCRIPT. See + // bug 1535398. + MOZ_CAN_RUN_SCRIPT_BOUNDARY + NS_IMETHOD Run() override; + bool Init(); + TaskQueue* PSupervisorTaskQueue() { return mPSupervisorTaskQueue; } + bool OnPSupervisorTaskQueue() const { + return mPSupervisorTaskQueue->IsCurrentThreadIn(); + } + + private: + MOZ_CAN_RUN_SCRIPT + void ReportFailureOnMainThread(WebAudioDecodeJob::ErrorCode aErrorCode) { + if (NS_IsMainThread()) { + Cleanup(); + mDecodeJob.OnFailure(aErrorCode); + } else { + // Take extra care to cleanup on the main thread + mMainThread->Dispatch(NewRunnableMethod("MediaDecodeTask::Cleanup", this, + &MediaDecodeTask::Cleanup)); + + nsCOMPtr<nsIRunnable> event = new ReportResultTask( + mDecodeJob, &WebAudioDecodeJob::OnFailure, aErrorCode); + mMainThread->Dispatch(event.forget()); + } + } + + void Decode(); + + void OnCreateDecoderCompleted(RefPtr<MediaDataDecoder> aDecoder); + MOZ_CAN_RUN_SCRIPT void OnCreateDecoderFailed(const MediaResult& aError); + + MOZ_CAN_RUN_SCRIPT void OnInitDemuxerCompleted(); + MOZ_CAN_RUN_SCRIPT void OnInitDemuxerFailed(const MediaResult& aError); + + void InitDecoder(); + void OnInitDecoderCompleted(); + MOZ_CAN_RUN_SCRIPT void OnInitDecoderFailed(); + + void DoDemux(); + void OnAudioDemuxCompleted(RefPtr<MediaTrackDemuxer::SamplesHolder> aSamples); + MOZ_CAN_RUN_SCRIPT void OnAudioDemuxFailed(const MediaResult& aError); + + void DoDecode(); + void OnAudioDecodeCompleted(MediaDataDecoder::DecodedData&& aResults); + MOZ_CAN_RUN_SCRIPT void OnAudioDecodeFailed(const MediaResult& aError); + + void DoDrain(); + MOZ_CAN_RUN_SCRIPT void OnAudioDrainCompleted( + MediaDataDecoder::DecodedData&& aResults); + MOZ_CAN_RUN_SCRIPT void OnAudioDrainFailed(const MediaResult& aError); + + void ShutdownDecoder(); + + MOZ_CAN_RUN_SCRIPT void FinishDecode(); + MOZ_CAN_RUN_SCRIPT void AllocateBuffer(); + MOZ_CAN_RUN_SCRIPT void CallbackTheResult(); + + void Cleanup() { + MOZ_ASSERT(NS_IsMainThread()); + JS_free(nullptr, mBuffer); + if (mTrackDemuxer) { + mTrackDemuxer->BreakCycles(); + } + mTrackDemuxer = nullptr; + mDemuxer = nullptr; + mPSupervisorTaskQueue = nullptr; + mPDecoderTaskQueue = nullptr; + } + + private: + MediaContainerType mContainerType; + uint8_t* mBuffer; + const uint32_t mLength; + const uint32_t mBatchSize; + WebAudioDecodeJob& mDecodeJob; + PhaseEnum mPhase; + RefPtr<TaskQueue> mPSupervisorTaskQueue; + RefPtr<TaskQueue> mPDecoderTaskQueue; + RefPtr<MediaDataDemuxer> mDemuxer; + RefPtr<MediaTrackDemuxer> mTrackDemuxer; + RefPtr<MediaDataDecoder> mDecoder; + nsTArray<RefPtr<MediaRawData>> mRawSamples; + MediaInfo mMediaInfo; + MediaQueue<AudioData> mAudioQueue; + RefPtr<AbstractThread> mMainThread; +}; + +NS_IMETHODIMP +MediaDecodeTask::Run() { + switch (mPhase) { + case PhaseEnum::Decode: + Decode(); + break; + case PhaseEnum::AllocateBuffer: + AllocateBuffer(); + break; + case PhaseEnum::Done: + break; + } + + return NS_OK; +} + +bool MediaDecodeTask::Init() { + MOZ_ASSERT(NS_IsMainThread()); + + RefPtr<BufferMediaResource> resource = + new BufferMediaResource(static_cast<uint8_t*>(mBuffer), mLength); + + mMainThread = AbstractThread::MainThread(); + + mPSupervisorTaskQueue = + TaskQueue::Create(GetMediaThreadPool(MediaThreadType::SUPERVISOR), + "MediaBufferDecoder::mPSupervisorTaskQueue"); + mPDecoderTaskQueue = + TaskQueue::Create(GetMediaThreadPool(MediaThreadType::PLATFORM_DECODER), + "MediaBufferDecoder::mPDecoderTaskQueue"); + + // If you change this list to add support for new decoders, please consider + // updating HTMLMediaElement::CreateDecoder as well. + mDemuxer = DecoderTraits::CreateDemuxer(mContainerType, resource); + if (!mDemuxer) { + return false; + } + + return true; +} + +class AutoResampler final { + public: + AutoResampler() : mResampler(nullptr) {} + ~AutoResampler() { + if (mResampler) { + speex_resampler_destroy(mResampler); + } + } + operator SpeexResamplerState*() const { + MOZ_ASSERT(mResampler); + return mResampler; + } + void operator=(SpeexResamplerState* aResampler) { mResampler = aResampler; } + + private: + SpeexResamplerState* mResampler; +}; + +void MediaDecodeTask::Decode() { + MOZ_ASSERT(OnPSupervisorTaskQueue()); + + mDemuxer->Init()->Then(PSupervisorTaskQueue(), __func__, this, + &MediaDecodeTask::OnInitDemuxerCompleted, + &MediaDecodeTask::OnInitDemuxerFailed); +} + +void MediaDecodeTask::OnInitDemuxerCompleted() { + MOZ_ASSERT(OnPSupervisorTaskQueue()); + + if (!!mDemuxer->GetNumberTracks(TrackInfo::kAudioTrack)) { + mTrackDemuxer = mDemuxer->GetTrackDemuxer(TrackInfo::kAudioTrack, 0); + if (!mTrackDemuxer) { + LOG("MediaDecodeTask: Could not get a track demuxer."); + ReportFailureOnMainThread(WebAudioDecodeJob::UnknownContent); + return; + } + + RefPtr<PDMFactory> platform = new PDMFactory(); + UniquePtr<TrackInfo> audioInfo = mTrackDemuxer->GetInfo(); + // We actively ignore audio tracks that we know we can't play. + if (audioInfo && audioInfo->IsValid() && + !platform->SupportsMimeType(audioInfo->mMimeType).isEmpty()) { + mMediaInfo.mAudio = *audioInfo->GetAsAudioInfo(); + } + } + + RefPtr<PDMFactory> pdm = new PDMFactory(); + pdm->CreateDecoder( + {*mMediaInfo.mAudio.GetAsAudioInfo(), TrackInfo::kAudioTrack}) + ->Then(PSupervisorTaskQueue(), __func__, this, + &MediaDecodeTask::OnCreateDecoderCompleted, + &MediaDecodeTask::OnCreateDecoderFailed); +} + +void MediaDecodeTask::OnCreateDecoderCompleted( + RefPtr<MediaDataDecoder> aDecoder) { + MOZ_ASSERT(OnPSupervisorTaskQueue()); + + mDecoder = new MediaDataDecoderProxy(aDecoder.forget(), + do_AddRef(mPDecoderTaskQueue.get())); + InitDecoder(); +} + +void MediaDecodeTask::OnCreateDecoderFailed(const MediaResult& aError) { + MOZ_ASSERT(OnPSupervisorTaskQueue()); + + LOG("MediaDecodeTask: Could not create a decoder."); + ReportFailureOnMainThread(WebAudioDecodeJob::UnknownContent); +} + +void MediaDecodeTask::OnInitDemuxerFailed(const MediaResult& aError) { + MOZ_ASSERT(OnPSupervisorTaskQueue()); + + LOG("MediaDecodeTask: Could not initialize the demuxer."); + ReportFailureOnMainThread(WebAudioDecodeJob::InvalidContent); +} + +void MediaDecodeTask::InitDecoder() { + MOZ_ASSERT(OnPSupervisorTaskQueue()); + + mDecoder->Init()->Then(PSupervisorTaskQueue(), __func__, this, + &MediaDecodeTask::OnInitDecoderCompleted, + &MediaDecodeTask::OnInitDecoderFailed); +} + +void MediaDecodeTask::OnInitDecoderCompleted() { + MOZ_ASSERT(OnPSupervisorTaskQueue()); + + DoDemux(); +} + +void MediaDecodeTask::OnInitDecoderFailed() { + MOZ_ASSERT(OnPSupervisorTaskQueue()); + + ShutdownDecoder(); + LOG("MediaDecodeTask: Could not initialize the decoder"); + ReportFailureOnMainThread(WebAudioDecodeJob::InvalidContent); +} + +void MediaDecodeTask::DoDemux() { + MOZ_ASSERT(OnPSupervisorTaskQueue()); + + mTrackDemuxer->GetSamples(mBatchSize) + ->Then(PSupervisorTaskQueue(), __func__, this, + &MediaDecodeTask::OnAudioDemuxCompleted, + &MediaDecodeTask::OnAudioDemuxFailed); +} + +void MediaDecodeTask::OnAudioDemuxCompleted( + RefPtr<MediaTrackDemuxer::SamplesHolder> aSamples) { + MOZ_ASSERT(OnPSupervisorTaskQueue()); + + mRawSamples.AppendElements(aSamples->GetSamples()); + + DoDemux(); +} + +void MediaDecodeTask::OnAudioDemuxFailed(const MediaResult& aError) { + MOZ_ASSERT(OnPSupervisorTaskQueue()); + + if (aError.Code() == NS_ERROR_DOM_MEDIA_END_OF_STREAM) { + DoDecode(); + } else { + ShutdownDecoder(); + LOG("MediaDecodeTask: Audio demux failed"); + ReportFailureOnMainThread(WebAudioDecodeJob::InvalidContent); + } +} + +void MediaDecodeTask::DoDecode() { + MOZ_ASSERT(OnPSupervisorTaskQueue()); + + if (mRawSamples.IsEmpty()) { + DoDrain(); + return; + } + + if (mBatchSize > 1 && mDecoder->CanDecodeBatch()) { + nsTArray<RefPtr<MediaRawData>> rawSampleBatch; + const int batchSize = std::min((unsigned long)mBatchSize, + (unsigned long)mRawSamples.Length()); + for (int i = 0; i < batchSize; ++i) { + rawSampleBatch.AppendElement(std::move(mRawSamples[i])); + } + + mDecoder->DecodeBatch(std::move(rawSampleBatch)) + ->Then(PSupervisorTaskQueue(), __func__, this, + &MediaDecodeTask::OnAudioDecodeCompleted, + &MediaDecodeTask::OnAudioDecodeFailed); + + mRawSamples.RemoveElementsAt(0, batchSize); + } else { + RefPtr<MediaRawData> sample = std::move(mRawSamples[0]); + + mDecoder->Decode(sample)->Then(PSupervisorTaskQueue(), __func__, this, + &MediaDecodeTask::OnAudioDecodeCompleted, + &MediaDecodeTask::OnAudioDecodeFailed); + + mRawSamples.RemoveElementAt(0); + } +} + +void MediaDecodeTask::OnAudioDecodeCompleted( + MediaDataDecoder::DecodedData&& aResults) { + MOZ_ASSERT(OnPSupervisorTaskQueue()); + + for (auto&& sample : aResults) { + MOZ_ASSERT(sample->mType == MediaData::Type::AUDIO_DATA); + RefPtr<AudioData> audioData = sample->As<AudioData>(); + + mMediaInfo.mAudio.mRate = audioData->mRate; + mMediaInfo.mAudio.mChannels = audioData->mChannels; + + mAudioQueue.Push(audioData.forget()); + } + + DoDecode(); +} + +void MediaDecodeTask::OnAudioDecodeFailed(const MediaResult& aError) { + MOZ_ASSERT(OnPSupervisorTaskQueue()); + + ShutdownDecoder(); + LOG("MediaDecodeTask: decode audio failed."); + ReportFailureOnMainThread(WebAudioDecodeJob::InvalidContent); +} + +void MediaDecodeTask::DoDrain() { + MOZ_ASSERT(OnPSupervisorTaskQueue()); + + mDecoder->Drain()->Then(PSupervisorTaskQueue(), __func__, this, + &MediaDecodeTask::OnAudioDrainCompleted, + &MediaDecodeTask::OnAudioDrainFailed); +} + +void MediaDecodeTask::OnAudioDrainCompleted( + MediaDataDecoder::DecodedData&& aResults) { + MOZ_ASSERT(OnPSupervisorTaskQueue()); + + if (aResults.IsEmpty()) { + FinishDecode(); + return; + } + + for (auto&& sample : aResults) { + MOZ_ASSERT(sample->mType == MediaData::Type::AUDIO_DATA); + RefPtr<AudioData> audioData = sample->As<AudioData>(); + + mAudioQueue.Push(audioData.forget()); + } + DoDrain(); +} + +void MediaDecodeTask::OnAudioDrainFailed(const MediaResult& aError) { + MOZ_ASSERT(OnPSupervisorTaskQueue()); + + ShutdownDecoder(); + LOG("MediaDecodeTask: Drain audio failed"); + ReportFailureOnMainThread(WebAudioDecodeJob::InvalidContent); +} + +void MediaDecodeTask::ShutdownDecoder() { + MOZ_ASSERT(OnPSupervisorTaskQueue()); + + if (!mDecoder) { + return; + } + + RefPtr<MediaDecodeTask> self = this; + mDecoder->Shutdown(); + mDecoder = nullptr; +} + +void MediaDecodeTask::FinishDecode() { + MOZ_ASSERT(OnPSupervisorTaskQueue()); + + ShutdownDecoder(); + + uint32_t frameCount = mAudioQueue.AudioFramesCount(); + uint32_t channelCount = mMediaInfo.mAudio.mChannels; + uint32_t sampleRate = mMediaInfo.mAudio.mRate; + + if (!frameCount || !channelCount || !sampleRate) { + LOG("MediaDecodeTask: invalid content frame count, channel count or " + "sample-rate"); + ReportFailureOnMainThread(WebAudioDecodeJob::InvalidContent); + return; + } + + const uint32_t destSampleRate = mDecodeJob.mContext->SampleRate(); + AutoResampler resampler; + + uint32_t resampledFrames = frameCount; + if (sampleRate != destSampleRate) { + resampledFrames = static_cast<uint32_t>( + static_cast<uint64_t>(destSampleRate) * + static_cast<uint64_t>(frameCount) / static_cast<uint64_t>(sampleRate)); + + resampler = speex_resampler_init(channelCount, sampleRate, destSampleRate, + SPEEX_RESAMPLER_QUALITY_DEFAULT, nullptr); + speex_resampler_skip_zeros(resampler); + resampledFrames += speex_resampler_get_output_latency(resampler); + } + + // Allocate contiguous channel buffers. Note that if we end up resampling, + // we may write fewer bytes than mResampledFrames to the output buffer, in + // which case writeIndex will tell us how many valid samples we have. + mDecodeJob.mBuffer.mChannelData.SetLength(channelCount); +#if AUDIO_OUTPUT_FORMAT == AUDIO_FORMAT_FLOAT32 + // This buffer has separate channel arrays that could be transferred to + // JS::NewArrayBufferWithContents(), but AudioBuffer::RestoreJSChannelData() + // does not yet take advantage of this. + RefPtr<ThreadSharedFloatArrayBufferList> buffer = + ThreadSharedFloatArrayBufferList::Create(channelCount, resampledFrames, + fallible); + if (!buffer) { + LOG("MediaDecodeTask: Could not create final buffer (f32)"); + ReportFailureOnMainThread(WebAudioDecodeJob::UnknownError); + return; + } + for (uint32_t i = 0; i < channelCount; ++i) { + mDecodeJob.mBuffer.mChannelData[i] = buffer->GetData(i); + } +#else + CheckedInt<size_t> bufferSize(sizeof(AudioDataValue)); + bufferSize *= resampledFrames; + bufferSize *= channelCount; + RefPtr<SharedBuffer> buffer = SharedBuffer::Create(bufferSize); + if (!buffer) { + LOG("MediaDecodeTask: Could not create final buffer (i16)"); + ReportFailureOnMainThread(WebAudioDecodeJob::UnknownError); + return; + } + auto data = static_cast<AudioDataValue*>(floatBuffer->Data()); + for (uint32_t i = 0; i < channelCount; ++i) { + mDecodeJob.mBuffer.mChannelData[i] = data; + data += resampledFrames; + } +#endif + mDecodeJob.mBuffer.mBuffer = std::move(buffer); + mDecodeJob.mBuffer.mVolume = 1.0f; + mDecodeJob.mBuffer.mBufferFormat = AUDIO_OUTPUT_FORMAT; + + uint32_t writeIndex = 0; + RefPtr<AudioData> audioData; + while ((audioData = mAudioQueue.PopFront())) { + if (!audioData->Frames()) { + // The packet contains no audio frames, skip it. + continue; + } + audioData->EnsureAudioBuffer(); // could lead to a copy :( + const AudioDataValue* bufferData = + static_cast<AudioDataValue*>(audioData->mAudioBuffer->Data()); + + if (sampleRate != destSampleRate) { + const uint32_t maxOutSamples = resampledFrames - writeIndex; + + for (uint32_t i = 0; i < audioData->mChannels; ++i) { + uint32_t inSamples = audioData->Frames(); + uint32_t outSamples = maxOutSamples; + AudioDataValue* outData = + mDecodeJob.mBuffer.ChannelDataForWrite<AudioDataValue>(i) + + writeIndex; + + WebAudioUtils::SpeexResamplerProcess( + resampler, i, &bufferData[i * audioData->Frames()], &inSamples, + outData, &outSamples); + + if (i == audioData->mChannels - 1) { + writeIndex += outSamples; + MOZ_ASSERT(writeIndex <= resampledFrames); + MOZ_ASSERT(inSamples == audioData->Frames()); + } + } + } else { + for (uint32_t i = 0; i < audioData->mChannels; ++i) { + AudioDataValue* outData = + mDecodeJob.mBuffer.ChannelDataForWrite<AudioDataValue>(i) + + writeIndex; + PodCopy(outData, &bufferData[i * audioData->Frames()], + audioData->Frames()); + + if (i == audioData->mChannels - 1) { + writeIndex += audioData->Frames(); + } + } + } + } + + if (sampleRate != destSampleRate) { + uint32_t inputLatency = speex_resampler_get_input_latency(resampler); + const uint32_t maxOutSamples = resampledFrames - writeIndex; + for (uint32_t i = 0; i < channelCount; ++i) { + uint32_t inSamples = inputLatency; + uint32_t outSamples = maxOutSamples; + AudioDataValue* outData = + mDecodeJob.mBuffer.ChannelDataForWrite<AudioDataValue>(i) + + writeIndex; + + WebAudioUtils::SpeexResamplerProcess(resampler, i, + (AudioDataValue*)nullptr, &inSamples, + outData, &outSamples); + + if (i == channelCount - 1) { + writeIndex += outSamples; + MOZ_ASSERT(writeIndex <= resampledFrames); + MOZ_ASSERT(inSamples == inputLatency); + } + } + } + + mDecodeJob.mBuffer.mDuration = writeIndex; + mPhase = PhaseEnum::AllocateBuffer; + mMainThread->Dispatch(do_AddRef(this)); +} + +void MediaDecodeTask::AllocateBuffer() { + MOZ_ASSERT(NS_IsMainThread()); + + if (!mDecodeJob.AllocateBuffer()) { + LOG("MediaDecodeTask: Could not allocate final buffer"); + ReportFailureOnMainThread(WebAudioDecodeJob::UnknownError); + return; + } + + mPhase = PhaseEnum::Done; + CallbackTheResult(); +} + +void MediaDecodeTask::CallbackTheResult() { + MOZ_ASSERT(NS_IsMainThread()); + + Cleanup(); + + // Now, we're ready to call the script back with the resulting buffer + mDecodeJob.OnSuccess(WebAudioDecodeJob::NoError); +} + +bool WebAudioDecodeJob::AllocateBuffer() { + MOZ_ASSERT(!mOutput); + MOZ_ASSERT(NS_IsMainThread()); + + // Now create the AudioBuffer + mOutput = AudioBuffer::Create(mContext->GetOwner(), mContext->SampleRate(), + std::move(mBuffer)); + return mOutput != nullptr; +} + +void AsyncDecodeWebAudio(const char* aContentType, uint8_t* aBuffer, + uint32_t aLength, WebAudioDecodeJob& aDecodeJob) { + Maybe<MediaContainerType> containerType = + MakeMediaContainerType(aContentType); + // Do not attempt to decode the media if we were not successful at sniffing + // the container type. + if (!*aContentType || strcmp(aContentType, APPLICATION_OCTET_STREAM) == 0 || + !containerType) { + nsCOMPtr<nsIRunnable> event = + new ReportResultTask(aDecodeJob, &WebAudioDecodeJob::OnFailure, + WebAudioDecodeJob::UnknownContent); + JS_free(nullptr, aBuffer); + aDecodeJob.mContext->Dispatch(event.forget()); + return; + } + + RefPtr<MediaDecodeTask> task = + new MediaDecodeTask(*containerType, aBuffer, aLength, aDecodeJob); + if (!task->Init()) { + nsCOMPtr<nsIRunnable> event = + new ReportResultTask(aDecodeJob, &WebAudioDecodeJob::OnFailure, + WebAudioDecodeJob::UnknownError); + aDecodeJob.mContext->Dispatch(event.forget()); + } else { + nsresult rv = task->PSupervisorTaskQueue()->Dispatch(task.forget()); + MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv)); + Unused << rv; + } +} + +WebAudioDecodeJob::WebAudioDecodeJob(AudioContext* aContext, Promise* aPromise, + DecodeSuccessCallback* aSuccessCallback, + DecodeErrorCallback* aFailureCallback) + : mContext(aContext), + mPromise(aPromise), + mSuccessCallback(aSuccessCallback), + mFailureCallback(aFailureCallback) { + MOZ_ASSERT(aContext); + MOZ_ASSERT(NS_IsMainThread()); + MOZ_COUNT_CTOR(WebAudioDecodeJob); +} + +WebAudioDecodeJob::~WebAudioDecodeJob() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_COUNT_DTOR(WebAudioDecodeJob); +} + +void WebAudioDecodeJob::OnSuccess(ErrorCode aErrorCode) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aErrorCode == NoError); + + RefPtr<AudioBuffer> output(mOutput); + if (mSuccessCallback) { + RefPtr<DecodeSuccessCallback> callback(mSuccessCallback); + // Ignore errors in calling the callback, since there is not much that we + // can do about it here. + callback->Call(*output); + } + mPromise->MaybeResolve(output); + + mContext->RemoveFromDecodeQueue(this); +} + +void WebAudioDecodeJob::OnFailure(ErrorCode aErrorCode) { + MOZ_ASSERT(NS_IsMainThread()); + + const char* errorMessage; + switch (aErrorCode) { + case UnknownContent: + errorMessage = + "The buffer passed to decodeAudioData contains an unknown content " + "type."; + break; + case InvalidContent: + errorMessage = + "The buffer passed to decodeAudioData contains invalid content which " + "cannot be decoded successfully."; + break; + case NoAudio: + errorMessage = + "The buffer passed to decodeAudioData does not contain any audio."; + break; + case NoError: + MOZ_FALLTHROUGH_ASSERT("Who passed NoError to OnFailure?"); + // Fall through to get some sort of a sane error message if this actually + // happens at runtime. + case UnknownError: + [[fallthrough]]; + default: + errorMessage = + "An unknown error occurred while processing decodeAudioData."; + break; + } + + // Ignore errors in calling the callback, since there is not much that we can + // do about it here. + nsAutoCString errorString(errorMessage); + if (mFailureCallback) { + RefPtr<DOMException> exception = DOMException::Create( + NS_ERROR_DOM_ENCODING_NOT_SUPPORTED_ERR, errorString); + RefPtr<DecodeErrorCallback> callback(mFailureCallback); + callback->Call(*exception); + } + + mPromise->MaybeRejectWithEncodingError(errorString); + + mContext->RemoveFromDecodeQueue(this); +} + +size_t WebAudioDecodeJob::SizeOfExcludingThis( + MallocSizeOf aMallocSizeOf) const { + size_t amount = 0; + if (mSuccessCallback) { + amount += mSuccessCallback->SizeOfIncludingThis(aMallocSizeOf); + } + if (mFailureCallback) { + amount += mFailureCallback->SizeOfIncludingThis(aMallocSizeOf); + } + if (mOutput) { + amount += mOutput->SizeOfIncludingThis(aMallocSizeOf); + } + amount += mBuffer.SizeOfExcludingThis(aMallocSizeOf, false); + return amount; +} + +size_t WebAudioDecodeJob::SizeOfIncludingThis( + MallocSizeOf aMallocSizeOf) const { + return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf); +} + +} // namespace mozilla diff --git a/dom/media/webaudio/MediaBufferDecoder.h b/dom/media/webaudio/MediaBufferDecoder.h new file mode 100644 index 0000000000..7af1acfd06 --- /dev/null +++ b/dom/media/webaudio/MediaBufferDecoder.h @@ -0,0 +1,70 @@ +/* -*- 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/. */ + +#ifndef MediaBufferDecoder_h_ +#define MediaBufferDecoder_h_ + +#include "AudioSegment.h" +#include "nsWrapperCache.h" +#include "nsCOMPtr.h" +#include "nsString.h" +#include "nsTArray.h" +#include "mozilla/Attributes.h" +#include "mozilla/dom/TypedArray.h" +#include "mozilla/MemoryReporting.h" + +namespace mozilla { + +class ThreadSharedFloatArrayBufferList; + +namespace dom { +class AudioBuffer; +class AudioContext; +class DecodeErrorCallback; +class DecodeSuccessCallback; +class Promise; +} // namespace dom + +struct WebAudioDecodeJob final { + // You may omit both the success and failure callback, or you must pass both. + // The callbacks are only necessary for asynchronous operation. + WebAudioDecodeJob(dom::AudioContext* aContext, dom::Promise* aPromise, + dom::DecodeSuccessCallback* aSuccessCallback = nullptr, + dom::DecodeErrorCallback* aFailureCallback = nullptr); + ~WebAudioDecodeJob(); + + enum ErrorCode { + NoError, + UnknownContent, + UnknownError, + InvalidContent, + NoAudio + }; + + typedef void (WebAudioDecodeJob::*ResultFn)(ErrorCode); + + MOZ_CAN_RUN_SCRIPT void OnSuccess(ErrorCode /* ignored */); + MOZ_CAN_RUN_SCRIPT void OnFailure(ErrorCode aErrorCode); + + bool AllocateBuffer(); + + size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const; + size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const; + + AudioChunk mBuffer; + RefPtr<dom::AudioContext> mContext; + RefPtr<dom::Promise> mPromise; + RefPtr<dom::DecodeSuccessCallback> mSuccessCallback; + RefPtr<dom::DecodeErrorCallback> mFailureCallback; // can be null + RefPtr<dom::AudioBuffer> mOutput; +}; + +void AsyncDecodeWebAudio(const char* aContentType, uint8_t* aBuffer, + uint32_t aLength, WebAudioDecodeJob& aDecodeJob); + +} // namespace mozilla + +#endif diff --git a/dom/media/webaudio/MediaElementAudioSourceNode.cpp b/dom/media/webaudio/MediaElementAudioSourceNode.cpp new file mode 100644 index 0000000000..953fee66d9 --- /dev/null +++ b/dom/media/webaudio/MediaElementAudioSourceNode.cpp @@ -0,0 +1,100 @@ +/* -*- 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 "MediaElementAudioSourceNode.h" +#include "mozilla/dom/MediaElementAudioSourceNodeBinding.h" +#include "AudioDestinationNode.h" +#include "AudioNodeTrack.h" +#include "MediaStreamTrack.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_CLASS(MediaElementAudioSourceNode) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(MediaElementAudioSourceNode) + tmp->Destroy(); + NS_IMPL_CYCLE_COLLECTION_UNLINK(mElement) +NS_IMPL_CYCLE_COLLECTION_UNLINK_END_INHERITED(AudioNode) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(MediaElementAudioSourceNode, + MediaStreamAudioSourceNode) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mElement) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MediaElementAudioSourceNode) +NS_INTERFACE_MAP_END_INHERITING(MediaStreamAudioSourceNode) + +NS_IMPL_ADDREF_INHERITED(MediaElementAudioSourceNode, + MediaStreamAudioSourceNode) +NS_IMPL_RELEASE_INHERITED(MediaElementAudioSourceNode, + MediaStreamAudioSourceNode) + +MediaElementAudioSourceNode::MediaElementAudioSourceNode( + AudioContext* aContext, HTMLMediaElement* aElement) + : MediaStreamAudioSourceNode(aContext, TrackChangeBehavior::FollowChanges), + mElement(aElement) { + MOZ_ASSERT(aElement); +} + +/* static */ +already_AddRefed<MediaElementAudioSourceNode> +MediaElementAudioSourceNode::Create( + AudioContext& aAudioContext, const MediaElementAudioSourceOptions& aOptions, + ErrorResult& aRv) { + // The spec has a pointless check here. See + // https://github.com/WebAudio/web-audio-api/issues/2149 + MOZ_RELEASE_ASSERT(!aAudioContext.IsOffline(), "Bindings messed up?"); + + RefPtr<MediaElementAudioSourceNode> node = + new MediaElementAudioSourceNode(&aAudioContext, aOptions.mMediaElement); + + RefPtr<DOMMediaStream> stream = aOptions.mMediaElement->CaptureAudio( + aRv, aAudioContext.Destination()->Track()->Graph()); + if (aRv.Failed()) { + return nullptr; + } + MOZ_ASSERT(stream, "CaptureAudio should report failure via aRv!"); + + node->Init(*stream, aRv); + if (aRv.Failed()) { + return nullptr; + } + + node->ListenForAllowedToPlay(aOptions); + return node.forget(); +} + +JSObject* MediaElementAudioSourceNode::WrapObject( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) { + return MediaElementAudioSourceNode_Binding::Wrap(aCx, this, aGivenProto); +} + +void MediaElementAudioSourceNode::ListenForAllowedToPlay( + const MediaElementAudioSourceOptions& aOptions) { + aOptions.mMediaElement->GetAllowedToPlayPromise() + ->Then( + AbstractThread::MainThread(), __func__, + // Capture by reference to bypass the mozilla-refcounted-inside-lambda + // static analysis. We capture a non-owning reference so as to allow + // cycle collection of the node. The reference is cleared via + // DisconnectIfExists() from Destroy() when the node is collected. + [&self = *this]() { + self.Context()->StartBlockedAudioContextIfAllowed(); + self.mAllowedToPlayRequest.Complete(); + }) + ->Track(mAllowedToPlayRequest); +} + +void MediaElementAudioSourceNode::Destroy() { + mAllowedToPlayRequest.DisconnectIfExists(); + MediaStreamAudioSourceNode::Destroy(); +} + +HTMLMediaElement* MediaElementAudioSourceNode::MediaElement() { + return mElement; +} + +} // namespace mozilla::dom diff --git a/dom/media/webaudio/MediaElementAudioSourceNode.h b/dom/media/webaudio/MediaElementAudioSourceNode.h new file mode 100644 index 0000000000..6ba21279fe --- /dev/null +++ b/dom/media/webaudio/MediaElementAudioSourceNode.h @@ -0,0 +1,67 @@ +/* -*- 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/. */ + +#ifndef MediaElementAudioSourceNode_h_ +#define MediaElementAudioSourceNode_h_ + +#include "MediaStreamAudioSourceNode.h" + +namespace mozilla::dom { + +class AudioContext; +struct MediaElementAudioSourceOptions; + +class MediaElementAudioSourceNode final : public MediaStreamAudioSourceNode { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(MediaElementAudioSourceNode, + MediaStreamAudioSourceNode) + static already_AddRefed<MediaElementAudioSourceNode> Create( + AudioContext& aAudioContext, + const MediaElementAudioSourceOptions& aOptions, ErrorResult& aRv); + + static already_AddRefed<MediaElementAudioSourceNode> Constructor( + const GlobalObject& aGlobal, AudioContext& aAudioContext, + const MediaElementAudioSourceOptions& aOptions, ErrorResult& aRv) { + return Create(aAudioContext, aOptions, aRv); + } + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + const char* NodeType() const override { + return "MediaElementAudioSourceNode"; + } + + const char* CrossOriginErrorString() const override { + return "MediaElementAudioSourceNodeCrossOrigin"; + } + + size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override { + return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf); + } + + HTMLMediaElement* MediaElement(); + + private: + explicit MediaElementAudioSourceNode(AudioContext* aContext, + HTMLMediaElement* aElement); + ~MediaElementAudioSourceNode() = default; + + void Destroy() override; + + // If AudioContext was not allowed to start, we would try to start it when + // source starts. + void ListenForAllowedToPlay(const MediaElementAudioSourceOptions& aOptions); + + MozPromiseRequestHolder<GenericNonExclusivePromise> mAllowedToPlayRequest; + + RefPtr<HTMLMediaElement> mElement; +}; + +} // namespace mozilla::dom + +#endif diff --git a/dom/media/webaudio/MediaStreamAudioDestinationNode.cpp b/dom/media/webaudio/MediaStreamAudioDestinationNode.cpp new file mode 100644 index 0000000000..704adaa304 --- /dev/null +++ b/dom/media/webaudio/MediaStreamAudioDestinationNode.cpp @@ -0,0 +1,150 @@ +/* -*- 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 "MediaStreamAudioDestinationNode.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/MediaStreamAudioDestinationNodeBinding.h" +#include "AudioNodeEngine.h" +#include "AudioNodeTrack.h" +#include "AudioStreamTrack.h" +#include "DOMMediaStream.h" +#include "ForwardedInputTrack.h" + +namespace mozilla::dom { + +class AudioDestinationTrackSource final : public MediaStreamTrackSource { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(AudioDestinationTrackSource, + MediaStreamTrackSource) + + AudioDestinationTrackSource(MediaStreamAudioDestinationNode* aNode, + mozilla::MediaTrack* aInputTrack, + ProcessedMediaTrack* aTrack, + nsIPrincipal* aPrincipal) + : MediaStreamTrackSource( + aPrincipal, nsString(), + // We pass 0 here because tracking ids are video only for now. + TrackingId(TrackingId::Source::AudioDestinationNode, 0)), + mTrack(aTrack), + mPort(mTrack->AllocateInputPort(aInputTrack)), + mNode(aNode) {} + + void Destroy() override { + if (!mTrack->IsDestroyed()) { + mTrack->Destroy(); + mPort->Destroy(); + } + if (mNode) { + mNode->DestroyMediaTrack(); + mNode = nullptr; + } + } + + MediaSourceEnum GetMediaSource() const override { + return MediaSourceEnum::AudioCapture; + } + + void Stop() override { Destroy(); } + + void Disable() override {} + + void Enable() override {} + + const RefPtr<ProcessedMediaTrack> mTrack; + const RefPtr<MediaInputPort> mPort; + + private: + ~AudioDestinationTrackSource() = default; + + RefPtr<MediaStreamAudioDestinationNode> mNode; +}; + +NS_IMPL_ADDREF_INHERITED(AudioDestinationTrackSource, MediaStreamTrackSource) +NS_IMPL_RELEASE_INHERITED(AudioDestinationTrackSource, MediaStreamTrackSource) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(AudioDestinationTrackSource) +NS_INTERFACE_MAP_END_INHERITING(MediaStreamTrackSource) +NS_IMPL_CYCLE_COLLECTION_INHERITED(AudioDestinationTrackSource, + MediaStreamTrackSource, mNode) + +NS_IMPL_CYCLE_COLLECTION_INHERITED(MediaStreamAudioDestinationNode, AudioNode, + mDOMStream) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MediaStreamAudioDestinationNode) +NS_INTERFACE_MAP_END_INHERITING(AudioNode) + +NS_IMPL_ADDREF_INHERITED(MediaStreamAudioDestinationNode, AudioNode) +NS_IMPL_RELEASE_INHERITED(MediaStreamAudioDestinationNode, AudioNode) + +MediaStreamAudioDestinationNode::MediaStreamAudioDestinationNode( + AudioContext* aContext) + : AudioNode(aContext, 2, ChannelCountMode::Explicit, + ChannelInterpretation::Speakers), + mDOMStream(MakeAndAddRef<DOMMediaStream>(GetOwner())) { + // Ensure an audio track with the correct ID is exposed to JS. If we can't get + // a principal here because the document is not available, pass in a null + // principal. This happens in edge cases when the document is being unloaded + // and it does not matter too much to have something working as long as it's + // not dangerous. + nsCOMPtr<nsIPrincipal> principal = nullptr; + if (aContext->GetParentObject()) { + Document* doc = aContext->GetParentObject()->GetExtantDoc(); + principal = doc->NodePrincipal(); + } + mTrack = AudioNodeTrack::Create(aContext, new AudioNodeEngine(this), + AudioNodeTrack::EXTERNAL_OUTPUT, + aContext->Graph()); + auto source = MakeRefPtr<AudioDestinationTrackSource>( + this, mTrack, + aContext->Graph()->CreateForwardedInputTrack(MediaSegment::AUDIO), + principal); + auto track = MakeRefPtr<AudioStreamTrack>(GetOwner(), source->mTrack, source); + mDOMStream->AddTrackInternal(track); +} + +/* static */ +already_AddRefed<MediaStreamAudioDestinationNode> +MediaStreamAudioDestinationNode::Create(AudioContext& aAudioContext, + const AudioNodeOptions& aOptions, + ErrorResult& aRv) { + // The spec has a pointless check here. See + // https://github.com/WebAudio/web-audio-api/issues/2149 + MOZ_RELEASE_ASSERT(!aAudioContext.IsOffline(), "Bindings messed up?"); + + RefPtr<MediaStreamAudioDestinationNode> audioNode = + new MediaStreamAudioDestinationNode(&aAudioContext); + + audioNode->Initialize(aOptions, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + return audioNode.forget(); +} + +size_t MediaStreamAudioDestinationNode::SizeOfExcludingThis( + MallocSizeOf aMallocSizeOf) const { + // Future: + // - mDOMStream + size_t amount = AudioNode::SizeOfExcludingThis(aMallocSizeOf); + return amount; +} + +size_t MediaStreamAudioDestinationNode::SizeOfIncludingThis( + MallocSizeOf aMallocSizeOf) const { + return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf); +} + +void MediaStreamAudioDestinationNode::DestroyMediaTrack() { + AudioNode::DestroyMediaTrack(); +} + +JSObject* MediaStreamAudioDestinationNode::WrapObject( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) { + return MediaStreamAudioDestinationNode_Binding::Wrap(aCx, this, aGivenProto); +} + +} // namespace mozilla::dom diff --git a/dom/media/webaudio/MediaStreamAudioDestinationNode.h b/dom/media/webaudio/MediaStreamAudioDestinationNode.h new file mode 100644 index 0000000000..24923955df --- /dev/null +++ b/dom/media/webaudio/MediaStreamAudioDestinationNode.h @@ -0,0 +1,58 @@ +/* -*- 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/. */ + +#ifndef MediaStreamAudioDestinationNode_h_ +#define MediaStreamAudioDestinationNode_h_ + +#include "AudioNode.h" + +namespace mozilla::dom { + +class AudioContext; +struct AudioNodeOptions; + +class MediaStreamAudioDestinationNode final : public AudioNode { + public: + static already_AddRefed<MediaStreamAudioDestinationNode> Create( + AudioContext& aAudioContext, const AudioNodeOptions& aOptions, + ErrorResult& aRv); + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(MediaStreamAudioDestinationNode, + AudioNode) + + static already_AddRefed<MediaStreamAudioDestinationNode> Constructor( + const GlobalObject& aGlobal, AudioContext& aAudioContext, + const AudioNodeOptions& aOptions, ErrorResult& aRv) { + return Create(aAudioContext, aOptions, aRv); + } + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + uint16_t NumberOfOutputs() const final { return 0; } + + void DestroyMediaTrack() override; + + DOMMediaStream* DOMStream() const { return mDOMStream; } + + const char* NodeType() const override { + return "MediaStreamAudioDestinationNode"; + } + + size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override; + size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override; + + private: + explicit MediaStreamAudioDestinationNode(AudioContext* aContext); + ~MediaStreamAudioDestinationNode() = default; + + RefPtr<DOMMediaStream> mDOMStream; +}; + +} // namespace mozilla::dom + +#endif diff --git a/dom/media/webaudio/MediaStreamAudioSourceNode.cpp b/dom/media/webaudio/MediaStreamAudioSourceNode.cpp new file mode 100644 index 0000000000..a915e78859 --- /dev/null +++ b/dom/media/webaudio/MediaStreamAudioSourceNode.cpp @@ -0,0 +1,278 @@ +/* -*- 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 "MediaStreamAudioSourceNode.h" +#include "mozilla/dom/MediaStreamAudioSourceNodeBinding.h" +#include "AudioNodeEngine.h" +#include "AudioNodeExternalInputTrack.h" +#include "AudioStreamTrack.h" +#include "mozilla/dom/Document.h" +#include "nsContentUtils.h" +#include "nsIScriptError.h" +#include "nsID.h" +#include "Tracing.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_CLASS(MediaStreamAudioSourceNode) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(MediaStreamAudioSourceNode) + tmp->Destroy(); + NS_IMPL_CYCLE_COLLECTION_UNLINK(mInputStream) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mInputTrack) +NS_IMPL_CYCLE_COLLECTION_UNLINK_END_INHERITED(AudioNode) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(MediaStreamAudioSourceNode, + AudioNode) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mInputStream) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mInputTrack) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MediaStreamAudioSourceNode) +NS_INTERFACE_MAP_END_INHERITING(AudioNode) + +NS_IMPL_ADDREF_INHERITED(MediaStreamAudioSourceNode, AudioNode) +NS_IMPL_RELEASE_INHERITED(MediaStreamAudioSourceNode, AudioNode) + +MediaStreamAudioSourceNode::MediaStreamAudioSourceNode( + AudioContext* aContext, TrackChangeBehavior aBehavior) + : AudioNode(aContext, 2, ChannelCountMode::Max, + ChannelInterpretation::Speakers), + mBehavior(aBehavior) {} + +/* static */ +already_AddRefed<MediaStreamAudioSourceNode> MediaStreamAudioSourceNode::Create( + AudioContext& aAudioContext, const MediaStreamAudioSourceOptions& aOptions, + ErrorResult& aRv) { + // The spec has a pointless check here. See + // https://github.com/WebAudio/web-audio-api/issues/2149 + MOZ_RELEASE_ASSERT(!aAudioContext.IsOffline(), "Bindings messed up?"); + + RefPtr<MediaStreamAudioSourceNode> node = + new MediaStreamAudioSourceNode(&aAudioContext, LockOnTrackPicked); + + // aOptions.mMediaStream is not nullable. + node->Init(*aOptions.mMediaStream, aRv); + if (aRv.Failed()) { + return nullptr; + } + + return node.forget(); +} + +void MediaStreamAudioSourceNode::Init(DOMMediaStream& aMediaStream, + ErrorResult& aRv) { + mInputStream = &aMediaStream; + AudioNodeEngine* engine = new MediaStreamAudioSourceNodeEngine(this); + mTrack = AudioNodeExternalInputTrack::Create(Context()->Graph(), engine); + mInputStream->AddConsumerToKeepAlive(ToSupports(this)); + + mInputStream->RegisterTrackListener(this); + if (mInputStream->Audible()) { + NotifyAudible(); + } + AttachToRightTrack(mInputStream, aRv); +} + +void MediaStreamAudioSourceNode::Destroy() { + if (mInputStream) { + mInputStream->UnregisterTrackListener(this); + mInputStream = nullptr; + } + DetachFromTrack(); +} + +MediaStreamAudioSourceNode::~MediaStreamAudioSourceNode() { Destroy(); } + +void MediaStreamAudioSourceNode::AttachToTrack( + const RefPtr<MediaStreamTrack>& aTrack, ErrorResult& aRv) { + MOZ_ASSERT(!mInputTrack); + MOZ_ASSERT(aTrack->AsAudioStreamTrack()); + MOZ_DIAGNOSTIC_ASSERT(!aTrack->Ended()); + + if (!mTrack) { + return; + } + + if (NS_WARN_IF(Context()->Graph() != aTrack->Graph())) { + nsCOMPtr<nsPIDOMWindowInner> pWindow = Context()->GetParentObject(); + Document* document = pWindow ? pWindow->GetExtantDoc() : nullptr; + nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, "Web Audio"_ns, + document, nsContentUtils::eDOM_PROPERTIES, + "MediaStreamAudioSourceNodeDifferentRate"); + // This is not a spec-required exception, just a limitation of our + // implementation. + aRv.ThrowNotSupportedError( + "Connecting AudioNodes from AudioContexts with different sample-rate " + "is currently not supported."); + return; + } + + mInputTrack = aTrack; + ProcessedMediaTrack* outputTrack = + static_cast<ProcessedMediaTrack*>(mTrack.get()); + mInputPort = mInputTrack->ForwardTrackContentsTo(outputTrack); + PrincipalChanged(mInputTrack); // trigger enabling/disabling of the connector + mInputTrack->AddPrincipalChangeObserver(this); + MarkActive(); +} + +void MediaStreamAudioSourceNode::DetachFromTrack() { + if (mInputTrack) { + mInputTrack->RemovePrincipalChangeObserver(this); + mInputTrack = nullptr; + } + if (mInputPort) { + mInputPort->Destroy(); + mInputPort = nullptr; + } +} + +static int AudioTrackCompare(const RefPtr<AudioStreamTrack>& aLhs, + const RefPtr<AudioStreamTrack>& aRhs) { + nsAutoStringN<NSID_LENGTH> IDLhs; + nsAutoStringN<NSID_LENGTH> IDRhs; + aLhs->GetId(IDLhs); + aRhs->GetId(IDRhs); + return Compare(NS_ConvertUTF16toUTF8(IDLhs), NS_ConvertUTF16toUTF8(IDRhs)); +} + +void MediaStreamAudioSourceNode::AttachToRightTrack( + const RefPtr<DOMMediaStream>& aMediaStream, ErrorResult& aRv) { + nsTArray<RefPtr<AudioStreamTrack>> tracks; + aMediaStream->GetAudioTracks(tracks); + + if (tracks.IsEmpty() && mBehavior == LockOnTrackPicked) { + aRv.ThrowInvalidStateError("No audio tracks in MediaStream"); + return; + } + + // Sort the track to have a stable order, on their ID by lexicographic + // ordering on sequences of code unit values. + tracks.Sort(AudioTrackCompare); + + for (const RefPtr<AudioStreamTrack>& track : tracks) { + if (mBehavior == FollowChanges) { + if (track->Ended()) { + continue; + } + } + + if (!track->Ended()) { + AttachToTrack(track, aRv); + } + return; + } + + // There was no track available. We'll allow the node to be garbage collected. + MarkInactive(); +} + +void MediaStreamAudioSourceNode::NotifyTrackAdded( + const RefPtr<MediaStreamTrack>& aTrack) { + if (mBehavior != FollowChanges) { + return; + } + if (mInputTrack) { + return; + } + + if (!aTrack->AsAudioStreamTrack()) { + return; + } + + AttachToTrack(aTrack, IgnoreErrors()); +} + +void MediaStreamAudioSourceNode::NotifyTrackRemoved( + const RefPtr<MediaStreamTrack>& aTrack) { + if (mBehavior == FollowChanges) { + if (aTrack != mInputTrack) { + return; + } + + DetachFromTrack(); + AttachToRightTrack(mInputStream, IgnoreErrors()); + } +} + +void MediaStreamAudioSourceNode::NotifyAudible() { + MOZ_ASSERT(mInputStream); + Context()->StartBlockedAudioContextIfAllowed(); +} + +/** + * Changes the principal. Note that this will be called on the main thread, but + * changes will be enacted on the MediaTrackGraph thread. If the principal + * change results in the document principal losing access to the stream, then + * there needs to be other measures in place to ensure that any media that is + * governed by the new stream principal is not available to the MediaTrackGraph + * before this change completes. Otherwise, a site could get access to + * media that they are not authorized to receive. + * + * One solution is to block the altered content, call this method, then dispatch + * another change request to the MediaTrackGraph thread that allows the content + * under the new principal to flow. This might be unnecessary if the principal + * change is changing to be the document principal. + */ +void MediaStreamAudioSourceNode::PrincipalChanged( + MediaStreamTrack* aMediaStreamTrack) { + MOZ_ASSERT(aMediaStreamTrack == mInputTrack); + + bool subsumes = false; + Document* doc = nullptr; + if (nsPIDOMWindowInner* parent = Context()->GetParentObject()) { + doc = parent->GetExtantDoc(); + if (doc) { + nsIPrincipal* docPrincipal = doc->NodePrincipal(); + nsIPrincipal* trackPrincipal = aMediaStreamTrack->GetPrincipal(); + if (!trackPrincipal || + NS_FAILED(docPrincipal->Subsumes(trackPrincipal, &subsumes))) { + subsumes = false; + } + } + } + auto track = static_cast<AudioNodeExternalInputTrack*>(mTrack.get()); + bool enabled = subsumes; + track->SetInt32Parameter(MediaStreamAudioSourceNodeEngine::ENABLE, enabled); + + if (!enabled && doc) { + nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, "Web Audio"_ns, + doc, nsContentUtils::eDOM_PROPERTIES, + CrossOriginErrorString()); + } +} + +size_t MediaStreamAudioSourceNode::SizeOfExcludingThis( + MallocSizeOf aMallocSizeOf) const { + // Future: + // - mInputStream + size_t amount = AudioNode::SizeOfExcludingThis(aMallocSizeOf); + if (mInputPort) { + amount += mInputPort->SizeOfIncludingThis(aMallocSizeOf); + } + return amount; +} + +size_t MediaStreamAudioSourceNode::SizeOfIncludingThis( + MallocSizeOf aMallocSizeOf) const { + return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf); +} + +void MediaStreamAudioSourceNode::DestroyMediaTrack() { + if (mInputPort) { + mInputPort->Destroy(); + mInputPort = nullptr; + } + AudioNode::DestroyMediaTrack(); +} + +JSObject* MediaStreamAudioSourceNode::WrapObject( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) { + return MediaStreamAudioSourceNode_Binding::Wrap(aCx, this, aGivenProto); +} + +} // namespace mozilla::dom diff --git a/dom/media/webaudio/MediaStreamAudioSourceNode.h b/dom/media/webaudio/MediaStreamAudioSourceNode.h new file mode 100644 index 0000000000..1875fc2e83 --- /dev/null +++ b/dom/media/webaudio/MediaStreamAudioSourceNode.h @@ -0,0 +1,127 @@ +/* -*- 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/. */ + +#ifndef MediaStreamAudioSourceNode_h_ +#define MediaStreamAudioSourceNode_h_ + +#include "AudioNode.h" +#include "AudioNodeEngine.h" +#include "DOMMediaStream.h" +#include "PrincipalChangeObserver.h" + +namespace mozilla::dom { + +class AudioContext; +struct MediaStreamAudioSourceOptions; + +class MediaStreamAudioSourceNodeEngine final : public AudioNodeEngine { + public: + explicit MediaStreamAudioSourceNodeEngine(AudioNode* aNode) + : AudioNodeEngine(aNode), mEnabled(false) {} + + bool IsEnabled() const { return mEnabled; } + enum Parameters { ENABLE }; + void SetInt32Parameter(uint32_t aIndex, int32_t aValue) override { + switch (aIndex) { + case ENABLE: + mEnabled = !!aValue; + break; + default: + NS_ERROR("MediaStreamAudioSourceNodeEngine bad parameter index"); + } + } + + private: + bool mEnabled; +}; + +class MediaStreamAudioSourceNode + : public AudioNode, + public DOMMediaStream::TrackListener, + public PrincipalChangeObserver<MediaStreamTrack> { + public: + static already_AddRefed<MediaStreamAudioSourceNode> Create( + AudioContext& aContext, const MediaStreamAudioSourceOptions& aOptions, + ErrorResult& aRv); + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(MediaStreamAudioSourceNode, + AudioNode) + + static already_AddRefed<MediaStreamAudioSourceNode> Constructor( + const GlobalObject& aGlobal, AudioContext& aAudioContext, + const MediaStreamAudioSourceOptions& aOptions, ErrorResult& aRv) { + return Create(aAudioContext, aOptions, aRv); + } + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + void DestroyMediaTrack() override; + + uint16_t NumberOfInputs() const override { return 0; } + + DOMMediaStream* GetMediaStream() { return mInputStream; } + + const char* NodeType() const override { return "MediaStreamAudioSourceNode"; } + + virtual const char* CrossOriginErrorString() const { + return "MediaStreamAudioSourceNodeCrossOrigin"; + } + + size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override; + size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override; + + // Attaches to aTrack so that its audio content will be used as input. + void AttachToTrack(const RefPtr<MediaStreamTrack>& aTrack, ErrorResult& aRv); + + // Detaches from the currently attached track if there is one. + void DetachFromTrack(); + + // Attaches to the first audio track in the MediaStream, when the tracks are + // ordered by id. + void AttachToRightTrack(const RefPtr<DOMMediaStream>& aMediaStream, + ErrorResult& aRv); + + // From DOMMediaStream::TrackListener. + void NotifyTrackAdded(const RefPtr<MediaStreamTrack>& aTrack) override; + void NotifyTrackRemoved(const RefPtr<MediaStreamTrack>& aTrack) override; + void NotifyAudible() override; + + // From PrincipalChangeObserver<MediaStreamTrack>. + void PrincipalChanged(MediaStreamTrack* aMediaStreamTrack) override; + + // This allows implementing the correct behaviour for both + // MediaElementAudioSourceNode and MediaStreamAudioSourceNode, that have most + // of their behaviour shared. + enum TrackChangeBehavior { + // MediaStreamAudioSourceNode locks on the track it picked, and never + // changes. + LockOnTrackPicked, + // MediaElementAudioSourceNode can change track, depending on what the + // HTMLMediaElement does. + FollowChanges + }; + + protected: + MediaStreamAudioSourceNode(AudioContext* aContext, + TrackChangeBehavior aBehavior); + void Init(DOMMediaStream& aMediaStream, ErrorResult& aRv); + virtual void Destroy(); + virtual ~MediaStreamAudioSourceNode(); + + private: + const TrackChangeBehavior mBehavior; + RefPtr<MediaInputPort> mInputPort; + RefPtr<DOMMediaStream> mInputStream; + + // On construction we set this to the first audio track of mInputStream. + RefPtr<MediaStreamTrack> mInputTrack; +}; + +} // namespace mozilla::dom + +#endif diff --git a/dom/media/webaudio/MediaStreamTrackAudioSourceNode.cpp b/dom/media/webaudio/MediaStreamTrackAudioSourceNode.cpp new file mode 100644 index 0000000000..f3948a33ad --- /dev/null +++ b/dom/media/webaudio/MediaStreamTrackAudioSourceNode.cpp @@ -0,0 +1,200 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 "MediaStreamTrackAudioSourceNode.h" +#include "mozilla/dom/MediaStreamTrackAudioSourceNodeBinding.h" +#include "AudioNodeEngine.h" +#include "AudioNodeExternalInputTrack.h" +#include "AudioStreamTrack.h" +#include "mozilla/dom/Document.h" +#include "nsContentUtils.h" +#include "nsIScriptError.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_CLASS(MediaStreamTrackAudioSourceNode) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(MediaStreamTrackAudioSourceNode) + tmp->Destroy(); + NS_IMPL_CYCLE_COLLECTION_UNLINK(mInputTrack) + NS_IMPL_CYCLE_COLLECTION_UNLINK_WEAK_PTR +NS_IMPL_CYCLE_COLLECTION_UNLINK_END_INHERITED(AudioNode) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED( + MediaStreamTrackAudioSourceNode, AudioNode) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mInputTrack) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MediaStreamTrackAudioSourceNode) +NS_INTERFACE_MAP_END_INHERITING(AudioNode) + +NS_IMPL_ADDREF_INHERITED(MediaStreamTrackAudioSourceNode, AudioNode) +NS_IMPL_RELEASE_INHERITED(MediaStreamTrackAudioSourceNode, AudioNode) + +MediaStreamTrackAudioSourceNode::MediaStreamTrackAudioSourceNode( + AudioContext* aContext) + : AudioNode(aContext, 2, ChannelCountMode::Max, + ChannelInterpretation::Speakers), + mTrackListener(this) {} + +/* static */ already_AddRefed<MediaStreamTrackAudioSourceNode> +MediaStreamTrackAudioSourceNode::Create( + AudioContext& aAudioContext, + const MediaStreamTrackAudioSourceOptions& aOptions, ErrorResult& aRv) { + // The spec has a pointless check here. See + // https://github.com/WebAudio/web-audio-api/issues/2149 + MOZ_RELEASE_ASSERT(!aAudioContext.IsOffline(), "Bindings messed up?"); + + if (!aOptions.mMediaStreamTrack->Ended() && + aAudioContext.Graph() != aOptions.mMediaStreamTrack->Graph()) { + nsCOMPtr<nsPIDOMWindowInner> pWindow = aAudioContext.GetParentObject(); + Document* document = pWindow ? pWindow->GetExtantDoc() : nullptr; + nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, "Web Audio"_ns, + document, nsContentUtils::eDOM_PROPERTIES, + "MediaStreamAudioSourceNodeDifferentRate"); + // This is not a spec-required exception, just a limitation of our + // implementation. + aRv.ThrowNotSupportedError( + "Connecting AudioNodes from AudioContexts with different sample-rate " + "is currently not supported."); + return nullptr; + } + + RefPtr<MediaStreamTrackAudioSourceNode> node = + new MediaStreamTrackAudioSourceNode(&aAudioContext); + + node->Init(aOptions.mMediaStreamTrack, aRv); + if (aRv.Failed()) { + return nullptr; + } + + return node.forget(); +} + +void MediaStreamTrackAudioSourceNode::Init(MediaStreamTrack* aMediaStreamTrack, + ErrorResult& aRv) { + MOZ_ASSERT(aMediaStreamTrack); + + if (!aMediaStreamTrack->AsAudioStreamTrack()) { + aRv.ThrowInvalidStateError("\"mediaStreamTrack\" must be an audio track"); + return; + } + + if (aMediaStreamTrack->Ended()) { + // The track is ended and will never produce any data. Pretend like this is + // fine. + return; + } + + MarkActive(); + + MediaTrackGraph* graph = Context()->Graph(); + + AudioNodeEngine* engine = new MediaStreamTrackAudioSourceNodeEngine(this); + mTrack = AudioNodeExternalInputTrack::Create(graph, engine); + + MOZ_ASSERT(mTrack); + + mInputTrack = aMediaStreamTrack; + ProcessedMediaTrack* outputTrack = + static_cast<ProcessedMediaTrack*>(mTrack.get()); + mInputPort = mInputTrack->ForwardTrackContentsTo(outputTrack); + PrincipalChanged(mInputTrack); // trigger enabling/disabling of the connector + mInputTrack->AddPrincipalChangeObserver(this); + + mInputTrack->AddConsumer(&mTrackListener); +} + +void MediaStreamTrackAudioSourceNode::Destroy() { + if (mInputTrack) { + mTrackListener.NotifyEnded(mInputTrack); + mInputTrack->RemovePrincipalChangeObserver(this); + mInputTrack->RemoveConsumer(&mTrackListener); + mInputTrack = nullptr; + } + + if (mInputPort) { + mInputPort->Destroy(); + mInputPort = nullptr; + } +} + +MediaStreamTrackAudioSourceNode::~MediaStreamTrackAudioSourceNode() { + Destroy(); +} + +/** + * Changes the principal. Note that this will be called on the main thread, but + * changes will be enacted on the MediaTrackGraph thread. If the principal + * change results in the document principal losing access to the track, then + * there needs to be other measures in place to ensure that any media that is + * governed by the new track principal is not available to the MediaTrackGraph + * before this change completes. Otherwise, a site could get access to + * media that they are not authorized to receive. + * + * One solution is to block the altered content, call this method, then dispatch + * another change request to the MediaTrackGraph thread that allows the content + * under the new principal to flow. This might be unnecessary if the principal + * change is changing to be the document principal. + */ +void MediaStreamTrackAudioSourceNode::PrincipalChanged( + MediaStreamTrack* aMediaStreamTrack) { + MOZ_ASSERT(aMediaStreamTrack == mInputTrack); + + bool subsumes = false; + Document* doc = nullptr; + if (nsPIDOMWindowInner* parent = Context()->GetParentObject()) { + doc = parent->GetExtantDoc(); + if (doc) { + nsIPrincipal* docPrincipal = doc->NodePrincipal(); + nsIPrincipal* trackPrincipal = aMediaStreamTrack->GetPrincipal(); + if (!trackPrincipal || + NS_FAILED(docPrincipal->Subsumes(trackPrincipal, &subsumes))) { + subsumes = false; + } + } + } + auto track = static_cast<AudioNodeExternalInputTrack*>(mTrack.get()); + bool enabled = subsumes; + track->SetInt32Parameter(MediaStreamTrackAudioSourceNodeEngine::ENABLE, + enabled); + fprintf(stderr, "NOW: %s", enabled ? "enabled" : "disabled"); + + if (!enabled && doc) { + nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, "Web Audio"_ns, + doc, nsContentUtils::eDOM_PROPERTIES, + CrossOriginErrorString()); + } +} + +size_t MediaStreamTrackAudioSourceNode::SizeOfExcludingThis( + MallocSizeOf aMallocSizeOf) const { + size_t amount = AudioNode::SizeOfExcludingThis(aMallocSizeOf); + if (mInputPort) { + amount += mInputPort->SizeOfIncludingThis(aMallocSizeOf); + } + return amount; +} + +size_t MediaStreamTrackAudioSourceNode::SizeOfIncludingThis( + MallocSizeOf aMallocSizeOf) const { + return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf); +} + +void MediaStreamTrackAudioSourceNode::DestroyMediaTrack() { + if (mInputPort) { + mInputPort->Destroy(); + mInputPort = nullptr; + } + AudioNode::DestroyMediaTrack(); +} + +JSObject* MediaStreamTrackAudioSourceNode::WrapObject( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) { + return MediaStreamTrackAudioSourceNode_Binding::Wrap(aCx, this, aGivenProto); +} + +} // namespace mozilla::dom diff --git a/dom/media/webaudio/MediaStreamTrackAudioSourceNode.h b/dom/media/webaudio/MediaStreamTrackAudioSourceNode.h new file mode 100644 index 0000000000..bb3d5d6c94 --- /dev/null +++ b/dom/media/webaudio/MediaStreamTrackAudioSourceNode.h @@ -0,0 +1,113 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef MediaStreamTrackAudioSourceNode_h_ +#define MediaStreamTrackAudioSourceNode_h_ + +#include "AudioNode.h" +#include "AudioNodeEngine.h" +#include "mozilla/dom/MediaStreamTrack.h" +#include "mozilla/WeakPtr.h" +#include "PrincipalChangeObserver.h" + +namespace mozilla::dom { + +class AudioContext; +struct MediaStreamTrackAudioSourceOptions; + +class MediaStreamTrackAudioSourceNodeEngine final : public AudioNodeEngine { + public: + explicit MediaStreamTrackAudioSourceNodeEngine(AudioNode* aNode) + : AudioNodeEngine(aNode), mEnabled(false) {} + + bool IsEnabled() const { return mEnabled; } + enum Parameters { ENABLE }; + void SetInt32Parameter(uint32_t aIndex, int32_t aValue) override { + switch (aIndex) { + case ENABLE: + mEnabled = !!aValue; + break; + default: + NS_ERROR("MediaStreamTrackAudioSourceNodeEngine bad parameter index"); + } + } + + private: + bool mEnabled; +}; + +class MediaStreamTrackAudioSourceNode + : public AudioNode, + public PrincipalChangeObserver<MediaStreamTrack>, + public SupportsWeakPtr { + public: + static already_AddRefed<MediaStreamTrackAudioSourceNode> Create( + AudioContext& aContext, + const MediaStreamTrackAudioSourceOptions& aOptions, ErrorResult& aRv); + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(MediaStreamTrackAudioSourceNode, + AudioNode) + + static already_AddRefed<MediaStreamTrackAudioSourceNode> Constructor( + const GlobalObject& aGlobal, AudioContext& aAudioContext, + const MediaStreamTrackAudioSourceOptions& aOptions, ErrorResult& aRv) { + return Create(aAudioContext, aOptions, aRv); + } + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + void DestroyMediaTrack() override; + + uint16_t NumberOfInputs() const override { return 0; } + + const char* NodeType() const override { + return "MediaStreamTrackAudioSourceNode"; + } + + virtual const char* CrossOriginErrorString() const { + return "MediaStreamTrackAudioSourceNodeCrossOrigin"; + } + + size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override; + size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override; + + // From PrincipalChangeObserver<MediaStreamTrack>. + void PrincipalChanged(MediaStreamTrack* aMediaStreamTrack) override; + + protected: + explicit MediaStreamTrackAudioSourceNode(AudioContext* aContext); + void Init(MediaStreamTrack* aMediaStreamTrack, ErrorResult& aRv); + void Destroy(); + virtual ~MediaStreamTrackAudioSourceNode(); + + class TrackListener : public MediaStreamTrackConsumer { + public: + explicit TrackListener(MediaStreamTrackAudioSourceNode* aNode) + : mNode(aNode) {} + + void NotifyEnded(MediaStreamTrack* aTrack) override { + if (mNode) { + mNode->MarkInactive(); + mNode->DestroyMediaTrack(); + mNode = nullptr; + } + } + + private: + WeakPtr<MediaStreamTrackAudioSourceNode> mNode; + }; + + private: + RefPtr<MediaInputPort> mInputPort; + RefPtr<MediaStreamTrack> mInputTrack; + TrackListener mTrackListener; +}; + +} // namespace mozilla::dom + +#endif diff --git a/dom/media/webaudio/OscillatorNode.cpp b/dom/media/webaudio/OscillatorNode.cpp new file mode 100644 index 0000000000..70592f6e96 --- /dev/null +++ b/dom/media/webaudio/OscillatorNode.cpp @@ -0,0 +1,560 @@ +/* -*- 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 "OscillatorNode.h" +#include "AudioNodeEngine.h" +#include "AudioNodeTrack.h" +#include "AudioDestinationNode.h" +#include "nsContentUtils.h" +#include "WebAudioUtils.h" +#include "blink/PeriodicWave.h" +#include "Tracing.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_INHERITED(OscillatorNode, AudioScheduledSourceNode, + mPeriodicWave, mFrequency, mDetune) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(OscillatorNode) +NS_INTERFACE_MAP_END_INHERITING(AudioScheduledSourceNode) + +NS_IMPL_ADDREF_INHERITED(OscillatorNode, AudioScheduledSourceNode) +NS_IMPL_RELEASE_INHERITED(OscillatorNode, AudioScheduledSourceNode) + +class OscillatorNodeEngine final : public AudioNodeEngine { + public: + OscillatorNodeEngine(AudioNode* aNode, AudioDestinationNode* aDestination) + : AudioNodeEngine(aNode), + mSource(nullptr), + mDestination(aDestination->Track()), + mStart(-1), + mStop(TRACK_TIME_MAX) + // Keep the default values in sync with OscillatorNode::OscillatorNode. + , + mFrequency(440.f), + mDetune(0.f), + mType(OscillatorType::Sine), + mPhase(0.), + mFinalFrequency(0.), + mPhaseIncrement(0.), + mRecomputeParameters(true), + mCustomDisableNormalization(false) { + MOZ_ASSERT(NS_IsMainThread()); + mBasicWaveFormCache = aDestination->Context()->GetBasicWaveFormCache(); + } + + void SetSourceTrack(AudioNodeTrack* aSource) { mSource = aSource; } + + enum Parameters { + FREQUENCY, + DETUNE, + TYPE, + DISABLE_NORMALIZATION, + START, + STOP, + }; + void RecvTimelineEvent(uint32_t aIndex, AudioParamEvent& aEvent) override { + mRecomputeParameters = true; + + MOZ_ASSERT(mDestination); + + aEvent.ConvertToTicks(mDestination); + + switch (aIndex) { + case FREQUENCY: + mFrequency.InsertEvent<int64_t>(aEvent); + break; + case DETUNE: + mDetune.InsertEvent<int64_t>(aEvent); + break; + default: + NS_ERROR("Bad OscillatorNodeEngine TimelineParameter"); + } + } + + void SetTrackTimeParameter(uint32_t aIndex, TrackTime aParam) override { + switch (aIndex) { + case START: + mStart = aParam; + mSource->SetActive(); + break; + case STOP: + mStop = aParam; + break; + default: + NS_ERROR("Bad OscillatorNodeEngine TrackTimeParameter"); + } + } + + void SetInt32Parameter(uint32_t aIndex, int32_t aParam) override { + switch (aIndex) { + case TYPE: + // Set the new type. + mType = static_cast<OscillatorType>(aParam); + if (mType == OscillatorType::Sine) { + // Forget any previous custom data. + mCustomDisableNormalization = false; + mPeriodicWave = nullptr; + mRecomputeParameters = true; + } + switch (mType) { + case OscillatorType::Sine: + mPhase = 0.0; + break; + case OscillatorType::Square: + case OscillatorType::Triangle: + case OscillatorType::Sawtooth: + mPeriodicWave = mBasicWaveFormCache->GetBasicWaveForm(mType); + break; + case OscillatorType::Custom: + break; + default: + NS_ERROR("Bad OscillatorNodeEngine type parameter."); + } + // End type switch. + break; + case DISABLE_NORMALIZATION: + MOZ_ASSERT(aParam >= 0, "negative custom array length"); + mCustomDisableNormalization = static_cast<uint32_t>(aParam); + break; + default: + NS_ERROR("Bad OscillatorNodeEngine Int32Parameter."); + } + // End index switch. + } + + void SetBuffer(AudioChunk&& aBuffer) override { + MOZ_ASSERT(aBuffer.ChannelCount() == 2, + "PeriodicWave should have sent two channels"); + MOZ_ASSERT(aBuffer.mVolume == 1.0f); + mPeriodicWave = WebCore::PeriodicWave::create( + mSource->mSampleRate, aBuffer.ChannelData<float>()[0], + aBuffer.ChannelData<float>()[1], aBuffer.mDuration, + mCustomDisableNormalization); + } + + void IncrementPhase() { + const float twoPiFloat = float(2 * M_PI); + mPhase += mPhaseIncrement; + if (mPhase > twoPiFloat) { + mPhase -= twoPiFloat; + } else if (mPhase < -twoPiFloat) { + mPhase += twoPiFloat; + } + } + + // Returns true if the final frequency (and thus the phase increment) changed, + // false otherwise. This allow some optimizations at callsite. + bool UpdateParametersIfNeeded(size_t aIndexInBlock, + const float aFrequency[WEBAUDIO_BLOCK_SIZE], + const float aDetune[WEBAUDIO_BLOCK_SIZE]) { + // Shortcut if frequency-related AudioParam are not automated, and we + // already have computed the frequency information and related parameters. + if (!ParametersMayNeedUpdate()) { + return false; + } + + float detune = aDetune[aIndexInBlock]; + if (detune != mLastDetune) { + mLastDetune = detune; + // Single-precision fdlibm_exp2f() would sometimes amplify rounding + // error in the division for large detune. + // https://bugzilla.mozilla.org/show_bug.cgi?id=1849806#c4 + mDetuneRatio = fdlibm_exp2(detune / 1200.); + } + float finalFrequency = aFrequency[aIndexInBlock] * mDetuneRatio; + mRecomputeParameters = false; + + if (finalFrequency == mFinalFrequency) { + return false; + } + + mFinalFrequency = finalFrequency; + mPhaseIncrement = 2 * M_PI * finalFrequency / mSource->mSampleRate; + return true; + } + + void FillBounds(float* output, TrackTime ticks, uint32_t& start, + uint32_t& end) { + MOZ_ASSERT(output); + static_assert(TrackTime(WEBAUDIO_BLOCK_SIZE) < UINT_MAX, + "WEBAUDIO_BLOCK_SIZE overflows interator bounds."); + start = 0; + if (ticks < mStart) { + start = mStart - ticks; + for (uint32_t i = 0; i < start; ++i) { + output[i] = 0.0; + } + } + end = WEBAUDIO_BLOCK_SIZE; + if (ticks + end > mStop) { + end = mStop - ticks; + for (uint32_t i = end; i < WEBAUDIO_BLOCK_SIZE; ++i) { + output[i] = 0.0; + } + } + } + + void ComputeSine(float* aOutput, uint32_t aStart, uint32_t aEnd, + const float aFrequency[WEBAUDIO_BLOCK_SIZE], + const float aDetune[WEBAUDIO_BLOCK_SIZE]) { + for (uint32_t i = aStart; i < aEnd; ++i) { + // We ignore the return value, changing the frequency has no impact on + // performances here. + UpdateParametersIfNeeded(i, aFrequency, aDetune); + + aOutput[i] = fdlibm_sinf(mPhase); + + IncrementPhase(); + } + } + + bool ParametersMayNeedUpdate() { + return !mDetune.HasSimpleValue() || !mFrequency.HasSimpleValue() || + mRecomputeParameters; + } + + void ComputeCustom(float* aOutput, uint32_t aStart, uint32_t aEnd, + const float aFrequency[WEBAUDIO_BLOCK_SIZE], + const float aDetune[WEBAUDIO_BLOCK_SIZE]) { + MOZ_ASSERT(mPeriodicWave, "No custom waveform data"); + + uint32_t periodicWaveSize = mPeriodicWave->periodicWaveSize(); + // Mask to wrap wave data indices into the range [0,periodicWaveSize). + uint32_t indexMask = periodicWaveSize - 1; + MOZ_ASSERT(periodicWaveSize && (periodicWaveSize & indexMask) == 0, + "periodicWaveSize must be power of 2"); + float* higherWaveData = nullptr; + float* lowerWaveData = nullptr; + float tableInterpolationFactor; + // Phase increment at frequency of 1 Hz. + // mPhase runs [0,periodicWaveSize) here instead of [0,2*M_PI). + float basePhaseIncrement = mPeriodicWave->rateScale(); + + bool needToFetchWaveData = + UpdateParametersIfNeeded(aStart, aFrequency, aDetune); + + bool parametersMayNeedUpdate = ParametersMayNeedUpdate(); + mPeriodicWave->waveDataForFundamentalFrequency( + mFinalFrequency, lowerWaveData, higherWaveData, + tableInterpolationFactor); + + for (uint32_t i = aStart; i < aEnd; ++i) { + if (parametersMayNeedUpdate) { + if (needToFetchWaveData) { + mPeriodicWave->waveDataForFundamentalFrequency( + mFinalFrequency, lowerWaveData, higherWaveData, + tableInterpolationFactor); + } + needToFetchWaveData = UpdateParametersIfNeeded(i, aFrequency, aDetune); + } + // Bilinear interpolation between adjacent samples in each table. + float floorPhase = floorf(mPhase); + int j1Signed = static_cast<int>(floorPhase); + uint32_t j1 = j1Signed & indexMask; + uint32_t j2 = j1 + 1; + j2 &= indexMask; + + float sampleInterpolationFactor = mPhase - floorPhase; + + float lower = (1.0f - sampleInterpolationFactor) * lowerWaveData[j1] + + sampleInterpolationFactor * lowerWaveData[j2]; + float higher = (1.0f - sampleInterpolationFactor) * higherWaveData[j1] + + sampleInterpolationFactor * higherWaveData[j2]; + aOutput[i] = (1.0f - tableInterpolationFactor) * lower + + tableInterpolationFactor * higher; + + // Calculate next phase position from wrapped value j1 to avoid loss of + // precision at large values. + mPhase = + j1 + sampleInterpolationFactor + basePhaseIncrement * mFinalFrequency; + } + } + + void ComputeSilence(AudioBlock* aOutput) { + aOutput->SetNull(WEBAUDIO_BLOCK_SIZE); + } + + void ProcessBlock(AudioNodeTrack* aTrack, GraphTime aFrom, + const AudioBlock& aInput, AudioBlock* aOutput, + bool* aFinished) override { + MOZ_ASSERT(mSource == aTrack, "Invalid source track"); + TRACE("OscillatorNodeEngine::ProcessBlock"); + + TrackTime ticks = mDestination->GraphTimeToTrackTime(aFrom); + if (mStart == -1) { + ComputeSilence(aOutput); + return; + } + + if (ticks + WEBAUDIO_BLOCK_SIZE <= mStart || ticks >= mStop || + mStop <= mStart) { + ComputeSilence(aOutput); + + } else { + aOutput->AllocateChannels(1); + float* output = aOutput->ChannelFloatsForWrite(0); + + uint32_t start, end; + FillBounds(output, ticks, start, end); + MOZ_ASSERT(start < end); + + float frequency[WEBAUDIO_BLOCK_SIZE]; + float detune[WEBAUDIO_BLOCK_SIZE]; + if (ParametersMayNeedUpdate()) { + if (mFrequency.HasSimpleValue()) { + std::fill_n(frequency, WEBAUDIO_BLOCK_SIZE, mFrequency.GetValue()); + } else { + mFrequency.GetValuesAtTime(ticks + start, frequency + start, + end - start); + } + if (mDetune.HasSimpleValue()) { + std::fill_n(detune, WEBAUDIO_BLOCK_SIZE, mDetune.GetValue()); + } else { + mDetune.GetValuesAtTime(ticks + start, detune + start, end - start); + } + } + + // Synthesize the correct waveform. + switch (mType) { + case OscillatorType::Sine: + ComputeSine(output, start, end, frequency, detune); + break; + case OscillatorType::Square: + case OscillatorType::Triangle: + case OscillatorType::Sawtooth: + case OscillatorType::Custom: + ComputeCustom(output, start, end, frequency, detune); + break; + case OscillatorType::EndGuard_: + MOZ_ASSERT_UNREACHABLE("end guard"); + // Avoid `default:` so that `-Wswitch` catches missing enumerators + // at compile time. + }; + } + + if (ticks + WEBAUDIO_BLOCK_SIZE >= mStop) { + // We've finished playing. + *aFinished = true; + } + } + + bool IsActive() const override { + // start() has been called. + return mStart != -1; + } + + size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override { + size_t amount = AudioNodeEngine::SizeOfExcludingThis(aMallocSizeOf); + + // Not owned: + // - mSource + // - mDestination + // - mFrequency (internal ref owned by node) + // - mDetune (internal ref owned by node) + + if (mPeriodicWave) { + amount += mPeriodicWave->sizeOfIncludingThis(aMallocSizeOf); + } + + return amount; + } + + size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override { + return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf); + } + + // mSource deletes this engine in its destructor + AudioNodeTrack* MOZ_NON_OWNING_REF mSource; + RefPtr<AudioNodeTrack> mDestination; + TrackTime mStart; + TrackTime mStop; + AudioParamTimeline mFrequency; + AudioParamTimeline mDetune; + OscillatorType mType; + float mPhase; + float mFinalFrequency; + float mPhaseIncrement; + float mLastDetune = 0.f; + float mDetuneRatio = 1.f; // 2^(mLastDetune/1200) + bool mRecomputeParameters; + RefPtr<BasicWaveFormCache> mBasicWaveFormCache; + bool mCustomDisableNormalization; + RefPtr<WebCore::PeriodicWave> mPeriodicWave; +}; + +OscillatorNode::OscillatorNode(AudioContext* aContext) + : AudioScheduledSourceNode(aContext, 2, ChannelCountMode::Max, + ChannelInterpretation::Speakers), + mType(OscillatorType::Sine), + mStartCalled(false) { + mFrequency = CreateAudioParam( + OscillatorNodeEngine::FREQUENCY, u"frequency"_ns, 440.0f, + -(aContext->SampleRate() / 2), aContext->SampleRate() / 2); + mDetune = CreateAudioParam(OscillatorNodeEngine::DETUNE, u"detune"_ns, 0.0f); + OscillatorNodeEngine* engine = + new OscillatorNodeEngine(this, aContext->Destination()); + mTrack = AudioNodeTrack::Create(aContext, engine, + AudioNodeTrack::NEED_MAIN_THREAD_ENDED, + aContext->Graph()); + engine->SetSourceTrack(mTrack); + mTrack->AddMainThreadListener(this); +} + +/* static */ +already_AddRefed<OscillatorNode> OscillatorNode::Create( + AudioContext& aAudioContext, const OscillatorOptions& aOptions, + ErrorResult& aRv) { + RefPtr<OscillatorNode> audioNode = new OscillatorNode(&aAudioContext); + + audioNode->Initialize(aOptions, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + audioNode->Frequency()->SetInitialValue(aOptions.mFrequency); + audioNode->Detune()->SetInitialValue(aOptions.mDetune); + + if (aOptions.mPeriodicWave.WasPassed()) { + audioNode->SetPeriodicWave(aOptions.mPeriodicWave.Value()); + } else { + audioNode->SetType(aOptions.mType, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + } + + return audioNode.forget(); +} + +size_t OscillatorNode::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const { + size_t amount = AudioNode::SizeOfExcludingThis(aMallocSizeOf); + + // For now only report if we know for sure that it's not shared. + if (mPeriodicWave) { + amount += mPeriodicWave->SizeOfIncludingThisIfNotShared(aMallocSizeOf); + } + amount += mFrequency->SizeOfIncludingThis(aMallocSizeOf); + amount += mDetune->SizeOfIncludingThis(aMallocSizeOf); + return amount; +} + +size_t OscillatorNode::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const { + return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf); +} + +JSObject* OscillatorNode::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return OscillatorNode_Binding::Wrap(aCx, this, aGivenProto); +} + +void OscillatorNode::DestroyMediaTrack() { + if (mTrack) { + mTrack->RemoveMainThreadListener(this); + } + AudioNode::DestroyMediaTrack(); +} + +void OscillatorNode::SendTypeToTrack() { + if (!mTrack) { + return; + } + if (mType == OscillatorType::Custom) { + // The engine assumes we'll send the custom data before updating the type. + SendPeriodicWaveToTrack(); + } + SendInt32ParameterToTrack(OscillatorNodeEngine::TYPE, + static_cast<int32_t>(mType)); +} + +void OscillatorNode::SendPeriodicWaveToTrack() { + NS_ASSERTION(mType == OscillatorType::Custom, + "Sending custom waveform to engine thread with non-custom type"); + MOZ_ASSERT(mTrack, "Missing node track."); + MOZ_ASSERT(mPeriodicWave, "Send called without PeriodicWave object."); + SendInt32ParameterToTrack(OscillatorNodeEngine::DISABLE_NORMALIZATION, + mPeriodicWave->DisableNormalization()); + AudioChunk data = mPeriodicWave->GetThreadSharedBuffer(); + mTrack->SetBuffer(std::move(data)); +} + +void OscillatorNode::Start(double aWhen, ErrorResult& aRv) { + if (!WebAudioUtils::IsTimeValid(aWhen)) { + aRv.ThrowRangeError<MSG_VALUE_OUT_OF_RANGE>("start time"); + return; + } + + if (mStartCalled) { + aRv.ThrowInvalidStateError("Can't call start() more than once"); + return; + } + mStartCalled = true; + + if (!mTrack) { + // Nothing to play, or we're already dead for some reason + return; + } + + // TODO: Perhaps we need to do more here. + mTrack->SetTrackTimeParameter(OscillatorNodeEngine::START, Context(), aWhen); + + MarkActive(); + Context()->StartBlockedAudioContextIfAllowed(); +} + +void OscillatorNode::Stop(double aWhen, ErrorResult& aRv) { + if (!WebAudioUtils::IsTimeValid(aWhen)) { + aRv.ThrowRangeError<MSG_VALUE_OUT_OF_RANGE>("stop time"); + return; + } + + if (!mStartCalled) { + aRv.ThrowInvalidStateError("Can't call stop() without calling start()"); + return; + } + + if (!mTrack || !Context()) { + // We've already stopped and had our track shut down + return; + } + + // TODO: Perhaps we need to do more here. + mTrack->SetTrackTimeParameter(OscillatorNodeEngine::STOP, Context(), + std::max(0.0, aWhen)); +} + +void OscillatorNode::NotifyMainThreadTrackEnded() { + MOZ_ASSERT(mTrack->IsEnded()); + + class EndedEventDispatcher final : public Runnable { + public: + explicit EndedEventDispatcher(OscillatorNode* aNode) + : mozilla::Runnable("EndedEventDispatcher"), mNode(aNode) {} + NS_IMETHOD Run() override { + // If it's not safe to run scripts right now, schedule this to run later + if (!nsContentUtils::IsSafeToRunScript()) { + nsContentUtils::AddScriptRunner(this); + return NS_OK; + } + + mNode->DispatchTrustedEvent(u"ended"_ns); + // Release track resources. + mNode->DestroyMediaTrack(); + return NS_OK; + } + + private: + RefPtr<OscillatorNode> mNode; + }; + + Context()->Dispatch(do_AddRef(new EndedEventDispatcher(this))); + + // Drop the playing reference + // Warning: The below line might delete this. + MarkInactive(); +} + +} // namespace mozilla::dom diff --git a/dom/media/webaudio/OscillatorNode.h b/dom/media/webaudio/OscillatorNode.h new file mode 100644 index 0000000000..1e207b9d67 --- /dev/null +++ b/dom/media/webaudio/OscillatorNode.h @@ -0,0 +1,92 @@ +/* -*- 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/. */ + +#ifndef OscillatorNode_h_ +#define OscillatorNode_h_ + +#include "AudioScheduledSourceNode.h" +#include "AudioParam.h" +#include "PeriodicWave.h" +#include "mozilla/dom/OscillatorNodeBinding.h" + +namespace mozilla::dom { + +class AudioContext; +struct OscillatorOptions; + +class OscillatorNode final : public AudioScheduledSourceNode, + public MainThreadMediaTrackListener { + public: + static already_AddRefed<OscillatorNode> Create( + AudioContext& aAudioContext, const OscillatorOptions& aOptions, + ErrorResult& aRv); + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(OscillatorNode, + AudioScheduledSourceNode) + + static already_AddRefed<OscillatorNode> Constructor( + const GlobalObject& aGlobal, AudioContext& aAudioContext, + const OscillatorOptions& aOptions, ErrorResult& aRv) { + return Create(aAudioContext, aOptions, aRv); + } + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + void DestroyMediaTrack() override; + + uint16_t NumberOfInputs() const final { return 0; } + + OscillatorType Type() const { return mType; } + void SetType(OscillatorType aType, ErrorResult& aRv) { + if (aType == OscillatorType::Custom) { + // ::Custom can only be set by setPeriodicWave(). + // https://github.com/WebAudio/web-audio-api/issues/105 for exception. + aRv.ThrowInvalidStateError("Can't set type to \"custom\""); + return; + } + mType = aType; + SendTypeToTrack(); + } + + AudioParam* Frequency() const { return mFrequency; } + AudioParam* Detune() const { return mDetune; } + + void Start(double aWhen, ErrorResult& aRv) override; + void Stop(double aWhen, ErrorResult& aRv) override; + + void SetPeriodicWave(PeriodicWave& aPeriodicWave) { + mPeriodicWave = &aPeriodicWave; + // SendTypeToTrack will call SendPeriodicWaveToTrack for us. + mType = OscillatorType::Custom; + SendTypeToTrack(); + } + + void NotifyMainThreadTrackEnded() override; + + const char* NodeType() const override { return "OscillatorNode"; } + + size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override; + size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override; + + private: + explicit OscillatorNode(AudioContext* aContext); + ~OscillatorNode() = default; + + void SendTypeToTrack(); + void SendPeriodicWaveToTrack(); + + OscillatorType mType; + RefPtr<PeriodicWave> mPeriodicWave; + RefPtr<AudioParam> mFrequency; + RefPtr<AudioParam> mDetune; + bool mStartCalled; +}; + +} // namespace mozilla::dom + +#endif diff --git a/dom/media/webaudio/PannerNode.cpp b/dom/media/webaudio/PannerNode.cpp new file mode 100644 index 0000000000..c6a17e5943 --- /dev/null +++ b/dom/media/webaudio/PannerNode.cpp @@ -0,0 +1,730 @@ +/* -*- 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 "PannerNode.h" +#include "AlignmentUtils.h" +#include "AudioDestinationNode.h" +#include "AudioNodeEngine.h" +#include "AudioNodeTrack.h" +#include "AudioListener.h" +#include "PanningUtils.h" +#include "AudioBufferSourceNode.h" +#include "PlayingRefChangeHandler.h" +#include "blink/HRTFPanner.h" +#include "blink/HRTFDatabaseLoader.h" +#include "Tracing.h" + +using WebCore::HRTFDatabaseLoader; +using WebCore::HRTFPanner; + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_CLASS(PannerNode) +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(PannerNode, AudioNode) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mPositionX, mPositionY, mPositionZ, + mOrientationX, mOrientationY, mOrientationZ) +NS_IMPL_CYCLE_COLLECTION_UNLINK_END +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(PannerNode, AudioNode) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPositionX, mPositionY, mPositionZ, + mOrientationX, mOrientationY, mOrientationZ) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PannerNode) +NS_INTERFACE_MAP_END_INHERITING(AudioNode) + +NS_IMPL_ADDREF_INHERITED(PannerNode, AudioNode) +NS_IMPL_RELEASE_INHERITED(PannerNode, AudioNode) + +class PannerNodeEngine final : public AudioNodeEngine { + public: + explicit PannerNodeEngine(AudioNode* aNode, + AudioDestinationNode* aDestination, + AudioListenerEngine* aListenerEngine) + : AudioNodeEngine(aNode), + mDestination(aDestination->Track()), + mListenerEngine(aListenerEngine) + // Please keep these default values consistent with + // PannerNode::PannerNode below. + , + mPanningModelFunction(&PannerNodeEngine::EqualPowerPanningFunction), + mDistanceModelFunction(&PannerNodeEngine::InverseGainFunction), + mPositionX(0.), + mPositionY(0.), + mPositionZ(0.), + mOrientationX(1.), + mOrientationY(0.), + mOrientationZ(0.), + mRefDistance(1.), + mMaxDistance(10000.), + mRolloffFactor(1.), + mConeInnerAngle(360.), + mConeOuterAngle(360.), + mConeOuterGain(0.), + mLeftOverData(INT_MIN) {} + + void RecvTimelineEvent(uint32_t aIndex, AudioParamEvent& aEvent) override { + MOZ_ASSERT(mDestination); + aEvent.ConvertToTicks(mDestination); + switch (aIndex) { + case PannerNode::POSITIONX: + mPositionX.InsertEvent<int64_t>(aEvent); + break; + case PannerNode::POSITIONY: + mPositionY.InsertEvent<int64_t>(aEvent); + break; + case PannerNode::POSITIONZ: + mPositionZ.InsertEvent<int64_t>(aEvent); + break; + case PannerNode::ORIENTATIONX: + mOrientationX.InsertEvent<int64_t>(aEvent); + break; + case PannerNode::ORIENTATIONY: + mOrientationY.InsertEvent<int64_t>(aEvent); + break; + case PannerNode::ORIENTATIONZ: + mOrientationZ.InsertEvent<int64_t>(aEvent); + break; + default: + NS_ERROR("Bad PannerNode TimelineParameter"); + } + } + + void CreateHRTFPanner() { + MOZ_ASSERT(NS_IsMainThread()); + if (mHRTFPanner) { + return; + } + // HRTFDatabaseLoader needs to be fetched on the main thread. + RefPtr<HRTFDatabaseLoader> loader = + HRTFDatabaseLoader::createAndLoadAsynchronouslyIfNecessary( + NodeMainThread()->Context()->SampleRate()); + mHRTFPanner = MakeUnique<HRTFPanner>( + NodeMainThread()->Context()->SampleRate(), loader.forget()); + } + + void SetInt32Parameter(uint32_t aIndex, int32_t aParam) override { + switch (aIndex) { + case PannerNode::PANNING_MODEL: + switch (PanningModelType(aParam)) { + case PanningModelType::Equalpower: + mPanningModelFunction = + &PannerNodeEngine::EqualPowerPanningFunction; + break; + case PanningModelType::HRTF: + mPanningModelFunction = &PannerNodeEngine::HRTFPanningFunction; + break; + default: + MOZ_ASSERT_UNREACHABLE("We should never see alternate names here"); + break; + } + break; + case PannerNode::DISTANCE_MODEL: + switch (DistanceModelType(aParam)) { + case DistanceModelType::Inverse: + mDistanceModelFunction = &PannerNodeEngine::InverseGainFunction; + break; + case DistanceModelType::Linear: + mDistanceModelFunction = &PannerNodeEngine::LinearGainFunction; + break; + case DistanceModelType::Exponential: + mDistanceModelFunction = &PannerNodeEngine::ExponentialGainFunction; + break; + default: + MOZ_ASSERT_UNREACHABLE("We should never see alternate names here"); + break; + } + break; + default: + NS_ERROR("Bad PannerNodeEngine Int32Parameter"); + } + } + void SetDoubleParameter(uint32_t aIndex, double aParam) override { + switch (aIndex) { + case PannerNode::REF_DISTANCE: + mRefDistance = aParam; + break; + case PannerNode::MAX_DISTANCE: + mMaxDistance = aParam; + break; + case PannerNode::ROLLOFF_FACTOR: + mRolloffFactor = aParam; + break; + case PannerNode::CONE_INNER_ANGLE: + mConeInnerAngle = aParam; + break; + case PannerNode::CONE_OUTER_ANGLE: + mConeOuterAngle = aParam; + break; + case PannerNode::CONE_OUTER_GAIN: + mConeOuterGain = aParam; + break; + default: + NS_ERROR("Bad PannerNodeEngine DoubleParameter"); + } + } + + void ProcessBlock(AudioNodeTrack* aTrack, GraphTime aFrom, + const AudioBlock& aInput, AudioBlock* aOutput, + bool* aFinished) override { + TRACE("PannerNodeEngine::ProcessBlock"); + + if (aInput.IsNull()) { + // mLeftOverData != INT_MIN means that the panning model was HRTF and a + // tail-time reference was added. Even if the model is now equalpower, + // the reference will need to be removed. + if (mLeftOverData > 0 && + mPanningModelFunction == &PannerNodeEngine::HRTFPanningFunction) { + mLeftOverData -= WEBAUDIO_BLOCK_SIZE; + } else { + if (mLeftOverData != INT_MIN) { + mLeftOverData = INT_MIN; + aTrack->ScheduleCheckForInactive(); + mHRTFPanner->reset(); + + RefPtr<PlayingRefChangeHandler> refchanged = + new PlayingRefChangeHandler(aTrack, + PlayingRefChangeHandler::RELEASE); + aTrack->Graph()->DispatchToMainThreadStableState(refchanged.forget()); + } + aOutput->SetNull(WEBAUDIO_BLOCK_SIZE); + return; + } + } else if (mPanningModelFunction == + &PannerNodeEngine::HRTFPanningFunction) { + if (mLeftOverData == INT_MIN) { + RefPtr<PlayingRefChangeHandler> refchanged = + new PlayingRefChangeHandler(aTrack, + PlayingRefChangeHandler::ADDREF); + aTrack->Graph()->DispatchToMainThreadStableState(refchanged.forget()); + } + mLeftOverData = mHRTFPanner->maxTailFrames(); + } + + TrackTime tick = mDestination->GraphTimeToTrackTime(aFrom); + (this->*mPanningModelFunction)(aInput, aOutput, tick); + } + + bool IsActive() const override { return mLeftOverData != INT_MIN; } + + void ComputeAzimuthAndElevation(const ThreeDPoint& position, float& aAzimuth, + float& aElevation); + float ComputeConeGain(const ThreeDPoint& position, + const ThreeDPoint& orientation); + // Compute how much the distance contributes to the gain reduction. + double ComputeDistanceGain(const ThreeDPoint& position); + + void EqualPowerPanningFunction(const AudioBlock& aInput, AudioBlock* aOutput, + TrackTime tick); + void HRTFPanningFunction(const AudioBlock& aInput, AudioBlock* aOutput, + TrackTime tick); + + float LinearGainFunction(double aDistance); + float InverseGainFunction(double aDistance); + float ExponentialGainFunction(double aDistance); + + ThreeDPoint ConvertAudioParamTimelineTo3DP(AudioParamTimeline& aX, + AudioParamTimeline& aY, + AudioParamTimeline& aZ, + TrackTime& tick); + + size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override { + size_t amount = AudioNodeEngine::SizeOfExcludingThis(aMallocSizeOf); + if (mHRTFPanner) { + amount += mHRTFPanner->sizeOfIncludingThis(aMallocSizeOf); + } + + return amount; + } + + size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override { + return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf); + } + + RefPtr<AudioNodeTrack> mDestination; + // This member is set on the main thread, but is not accessed on the rendering + // thread untile mPanningModelFunction has changed, and this happens strictly + // later, via a MediaTrackGraph ControlMessage. + UniquePtr<HRTFPanner> mHRTFPanner; + RefPtr<AudioListenerEngine> mListenerEngine; + using PanningModelFunction = void (PannerNodeEngine::*)(const AudioBlock&, + AudioBlock*, + TrackTime); + PanningModelFunction mPanningModelFunction; + using DistanceModelFunction = float (PannerNodeEngine::*)(double); + DistanceModelFunction mDistanceModelFunction; + AudioParamTimeline mPositionX; + AudioParamTimeline mPositionY; + AudioParamTimeline mPositionZ; + AudioParamTimeline mOrientationX; + AudioParamTimeline mOrientationY; + AudioParamTimeline mOrientationZ; + double mRefDistance; + double mMaxDistance; + double mRolloffFactor; + double mConeInnerAngle; + double mConeOuterAngle; + double mConeOuterGain; + int mLeftOverData; +}; + +PannerNode::PannerNode(AudioContext* aContext) + : AudioNode(aContext, 2, ChannelCountMode::Clamped_max, + ChannelInterpretation::Speakers) + // Please keep these default values consistent with + // PannerNodeEngine::PannerNodeEngine above. + , + mPanningModel(PanningModelType::Equalpower), + mDistanceModel(DistanceModelType::Inverse), + mRefDistance(1.), + mMaxDistance(10000.), + mRolloffFactor(1.), + mConeInnerAngle(360.), + mConeOuterAngle(360.), + mConeOuterGain(0.) { + mPositionX = CreateAudioParam(PannerNode::POSITIONX, u"PositionX"_ns, 0.f); + mPositionY = CreateAudioParam(PannerNode::POSITIONY, u"PositionY"_ns, 0.f); + mPositionZ = CreateAudioParam(PannerNode::POSITIONZ, u"PositionZ"_ns, 0.f); + mOrientationX = + CreateAudioParam(PannerNode::ORIENTATIONX, u"OrientationX"_ns, 1.0f); + mOrientationY = + CreateAudioParam(PannerNode::ORIENTATIONY, u"OrientationY"_ns, 0.f); + mOrientationZ = + CreateAudioParam(PannerNode::ORIENTATIONZ, u"OrientationZ"_ns, 0.f); + mTrack = AudioNodeTrack::Create( + aContext, + new PannerNodeEngine(this, aContext->Destination(), + aContext->Listener()->Engine()), + AudioNodeTrack::NO_TRACK_FLAGS, aContext->Graph()); +} + +/* static */ +already_AddRefed<PannerNode> PannerNode::Create(AudioContext& aAudioContext, + const PannerOptions& aOptions, + ErrorResult& aRv) { + RefPtr<PannerNode> audioNode = new PannerNode(&aAudioContext); + + audioNode->Initialize(aOptions, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + audioNode->SetPanningModel(aOptions.mPanningModel); + audioNode->SetDistanceModel(aOptions.mDistanceModel); + audioNode->mPositionX->SetInitialValue(aOptions.mPositionX); + audioNode->mPositionY->SetInitialValue(aOptions.mPositionY); + audioNode->mPositionZ->SetInitialValue(aOptions.mPositionZ); + audioNode->mOrientationX->SetInitialValue(aOptions.mOrientationX); + audioNode->mOrientationY->SetInitialValue(aOptions.mOrientationY); + audioNode->mOrientationZ->SetInitialValue(aOptions.mOrientationZ); + audioNode->SetRefDistance(aOptions.mRefDistance, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + audioNode->SetMaxDistance(aOptions.mMaxDistance, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + audioNode->SetRolloffFactor(aOptions.mRolloffFactor, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + audioNode->SetConeInnerAngle(aOptions.mConeInnerAngle); + audioNode->SetConeOuterAngle(aOptions.mConeOuterAngle); + audioNode->SetConeOuterGain(aOptions.mConeOuterGain, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + return audioNode.forget(); +} + +void PannerNode::SetPanningModel(PanningModelType aPanningModel) { + mPanningModel = aPanningModel; + if (mPanningModel == PanningModelType::HRTF) { + // We can set the engine's `mHRTFPanner` member here from the main thread, + // because the engine will not touch it from the MediaTrackGraph + // thread until the PANNING_MODEL message sent below is received. + static_cast<PannerNodeEngine*>(mTrack->Engine())->CreateHRTFPanner(); + } + SendInt32ParameterToTrack(PANNING_MODEL, int32_t(mPanningModel)); +} + +static bool SetParamFromDouble(AudioParam* aParam, double aValue, + const char (&aParamName)[2], ErrorResult& aRv) { + float value = static_cast<float>(aValue); + if (!std::isfinite(value)) { + aRv.ThrowTypeError<MSG_NOT_FINITE>(aParamName); + return false; + } + aParam->SetValue(value, aRv); + return !aRv.Failed(); +} + +void PannerNode::SetPosition(double aX, double aY, double aZ, + ErrorResult& aRv) { + if (!SetParamFromDouble(mPositionX, aX, "x", aRv)) { + return; + } + if (!SetParamFromDouble(mPositionY, aY, "y", aRv)) { + return; + } + SetParamFromDouble(mPositionZ, aZ, "z", aRv); +} + +void PannerNode::SetOrientation(double aX, double aY, double aZ, + ErrorResult& aRv) { + if (!SetParamFromDouble(mOrientationX, aX, "x", aRv)) { + return; + } + if (!SetParamFromDouble(mOrientationY, aY, "y", aRv)) { + return; + } + SetParamFromDouble(mOrientationZ, aZ, "z", aRv); +} + +size_t PannerNode::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const { + return AudioNode::SizeOfExcludingThis(aMallocSizeOf); +} + +size_t PannerNode::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const { + return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf); +} + +JSObject* PannerNode::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return PannerNode_Binding::Wrap(aCx, this, aGivenProto); +} + +// Those three functions are described in the spec. +float PannerNodeEngine::LinearGainFunction(double aDistance) { + double clampedRollof = std::clamp(mRolloffFactor, 0.0, 1.0); + return AssertedCast<float>( + 1.0 - clampedRollof * + (std::max(std::min(aDistance, mMaxDistance), mRefDistance) - + mRefDistance) / + (mMaxDistance - mRefDistance)); +} + +float PannerNodeEngine::InverseGainFunction(double aDistance) { + return mRefDistance / + (mRefDistance + + mRolloffFactor * (std::max(aDistance, mRefDistance) - mRefDistance)); +} + +float PannerNodeEngine::ExponentialGainFunction(double aDistance) { + return fdlibm_pow(std::max(aDistance, mRefDistance) / mRefDistance, + -mRolloffFactor); +} + +void PannerNodeEngine::HRTFPanningFunction(const AudioBlock& aInput, + AudioBlock* aOutput, + TrackTime tick) { + // The output of this node is always stereo, no matter what the inputs are. + aOutput->AllocateChannels(2); + + float azimuth, elevation; + + ThreeDPoint position = + ConvertAudioParamTimelineTo3DP(mPositionX, mPositionY, mPositionZ, tick); + ThreeDPoint orientation = ConvertAudioParamTimelineTo3DP( + mOrientationX, mOrientationY, mOrientationZ, tick); + if (!orientation.IsZero()) { + orientation.Normalize(); + } + ComputeAzimuthAndElevation(position, azimuth, elevation); + + AudioBlock input = aInput; + // Gain is applied before the delay and convolution of the HRTF. + input.mVolume *= + ComputeConeGain(position, orientation) * ComputeDistanceGain(position); + + mHRTFPanner->pan(azimuth, elevation, &input, aOutput); +} + +ThreeDPoint PannerNodeEngine::ConvertAudioParamTimelineTo3DP( + AudioParamTimeline& aX, AudioParamTimeline& aY, AudioParamTimeline& aZ, + TrackTime& tick) { + return ThreeDPoint(aX.GetValueAtTime(tick), aY.GetValueAtTime(tick), + aZ.GetValueAtTime(tick)); +} + +void PannerNodeEngine::EqualPowerPanningFunction(const AudioBlock& aInput, + AudioBlock* aOutput, + TrackTime tick) { + float azimuth, elevation, gainL, gainR, normalizedAzimuth, distanceGain, + coneGain; + int inputChannels = aInput.ChannelCount(); + + // Optimize the case where the position and orientation is constant for this + // processing block: we can just apply a constant gain on the left and right + // channel + if (mPositionX.HasSimpleValue() && mPositionY.HasSimpleValue() && + mPositionZ.HasSimpleValue() && mOrientationX.HasSimpleValue() && + mOrientationY.HasSimpleValue() && mOrientationZ.HasSimpleValue()) { + ThreeDPoint position(mPositionX.GetValue(), mPositionY.GetValue(), + mPositionZ.GetValue()); + ThreeDPoint orientation(mOrientationX.GetValue(), mOrientationY.GetValue(), + mOrientationZ.GetValue()); + if (!orientation.IsZero()) { + orientation.Normalize(); + } + + // For a stereo source, when both the listener and the panner are in + // the same spot, and no cone gain is specified, this node is noop. + if (inputChannels == 2 && mListenerEngine->Position() == position && + mConeInnerAngle == 360 && mConeOuterAngle == 360) { + *aOutput = aInput; + return; + } + + ComputeAzimuthAndElevation(position, azimuth, elevation); + coneGain = ComputeConeGain(position, orientation); + + // The following algorithm is described in the spec. + // Clamp azimuth in the [-90, 90] range. + azimuth = std::min(180.f, std::max(-180.f, azimuth)); + + // Wrap around + if (azimuth < -90.f) { + azimuth = -180.f - azimuth; + } else if (azimuth > 90) { + azimuth = 180.f - azimuth; + } + + // Normalize the value in the [0, 1] range. + if (inputChannels == 1) { + normalizedAzimuth = (azimuth + 90.f) / 180.f; + } else { + if (azimuth <= 0) { + normalizedAzimuth = (azimuth + 90.f) / 90.f; + } else { + normalizedAzimuth = azimuth / 90.f; + } + } + + distanceGain = ComputeDistanceGain(position); + + // Actually compute the left and right gain. + gainL = fdlibm_cos(0.5 * M_PI * normalizedAzimuth); + gainR = fdlibm_sin(0.5 * M_PI * normalizedAzimuth); + + // Compute the output. + ApplyStereoPanning(aInput, aOutput, gainL, gainR, azimuth <= 0); + + aOutput->mVolume *= distanceGain * coneGain; + } else { + float positionX[WEBAUDIO_BLOCK_SIZE]; + float positionY[WEBAUDIO_BLOCK_SIZE]; + float positionZ[WEBAUDIO_BLOCK_SIZE]; + float orientationX[WEBAUDIO_BLOCK_SIZE]; + float orientationY[WEBAUDIO_BLOCK_SIZE]; + float orientationZ[WEBAUDIO_BLOCK_SIZE]; + + if (!mPositionX.HasSimpleValue()) { + mPositionX.GetValuesAtTime(tick, positionX, WEBAUDIO_BLOCK_SIZE); + } else { + positionX[0] = mPositionX.GetValue(); + } + if (!mPositionY.HasSimpleValue()) { + mPositionY.GetValuesAtTime(tick, positionY, WEBAUDIO_BLOCK_SIZE); + } else { + positionY[0] = mPositionY.GetValue(); + } + if (!mPositionZ.HasSimpleValue()) { + mPositionZ.GetValuesAtTime(tick, positionZ, WEBAUDIO_BLOCK_SIZE); + } else { + positionZ[0] = mPositionZ.GetValue(); + } + if (!mOrientationX.HasSimpleValue()) { + mOrientationX.GetValuesAtTime(tick, orientationX, WEBAUDIO_BLOCK_SIZE); + } else { + orientationX[0] = mOrientationX.GetValue(); + } + if (!mOrientationY.HasSimpleValue()) { + mOrientationY.GetValuesAtTime(tick, orientationY, WEBAUDIO_BLOCK_SIZE); + } else { + orientationY[0] = mOrientationY.GetValue(); + } + if (!mOrientationZ.HasSimpleValue()) { + mOrientationZ.GetValuesAtTime(tick, orientationZ, WEBAUDIO_BLOCK_SIZE); + } else { + orientationZ[0] = mOrientationZ.GetValue(); + } + + float buffer[3 * WEBAUDIO_BLOCK_SIZE + 4]; + bool onLeft[WEBAUDIO_BLOCK_SIZE]; + + float* alignedPanningL = ALIGNED16(buffer); + float* alignedPanningR = alignedPanningL + WEBAUDIO_BLOCK_SIZE; + float* alignedGain = alignedPanningR + WEBAUDIO_BLOCK_SIZE; + ASSERT_ALIGNED16(alignedPanningL); + ASSERT_ALIGNED16(alignedPanningR); + ASSERT_ALIGNED16(alignedGain); + + for (size_t counter = 0; counter < WEBAUDIO_BLOCK_SIZE; ++counter) { + ThreeDPoint position( + mPositionX.HasSimpleValue() ? positionX[0] : positionX[counter], + mPositionY.HasSimpleValue() ? positionY[0] : positionY[counter], + mPositionZ.HasSimpleValue() ? positionZ[0] : positionZ[counter]); + ThreeDPoint orientation( + mOrientationX.HasSimpleValue() ? orientationX[0] + : orientationX[counter], + mOrientationY.HasSimpleValue() ? orientationY[0] + : orientationY[counter], + mOrientationZ.HasSimpleValue() ? orientationZ[0] + : orientationZ[counter]); + if (!orientation.IsZero()) { + orientation.Normalize(); + } + + ComputeAzimuthAndElevation(position, azimuth, elevation); + coneGain = ComputeConeGain(position, orientation); + + // The following algorithm is described in the spec. + // Clamp azimuth in the [-90, 90] range. + azimuth = std::min(180.f, std::max(-180.f, azimuth)); + + // Wrap around + if (azimuth < -90.f) { + azimuth = -180.f - azimuth; + } else if (azimuth > 90) { + azimuth = 180.f - azimuth; + } + + // Normalize the value in the [0, 1] range. + if (inputChannels == 1) { + normalizedAzimuth = (azimuth + 90.f) / 180.f; + } else { + if (azimuth <= 0) { + normalizedAzimuth = (azimuth + 90.f) / 90.f; + } else { + normalizedAzimuth = azimuth / 90.f; + } + } + + distanceGain = ComputeDistanceGain(position); + + // Actually compute the left and right gain. + float gainL = fdlibm_cos(0.5 * M_PI * normalizedAzimuth); + float gainR = fdlibm_sin(0.5 * M_PI * normalizedAzimuth); + + alignedPanningL[counter] = gainL; + alignedPanningR[counter] = gainR; + alignedGain[counter] = distanceGain * coneGain; + onLeft[counter] = azimuth <= 0; + } + + // Apply the panning to the output buffer + ApplyStereoPanning(aInput, aOutput, alignedPanningL, alignedPanningR, + onLeft); + + // Apply the input volume, cone and distance gain to the output buffer. + float* outputL = aOutput->ChannelFloatsForWrite(0); + float* outputR = aOutput->ChannelFloatsForWrite(1); + AudioBlockInPlaceScale(outputL, alignedGain); + AudioBlockInPlaceScale(outputR, alignedGain); + } +} + +// This algorithm is specified in the webaudio spec. +void PannerNodeEngine::ComputeAzimuthAndElevation(const ThreeDPoint& position, + float& aAzimuth, + float& aElevation) { + ThreeDPoint sourceListener = position - mListenerEngine->Position(); + if (sourceListener.IsZero()) { + aAzimuth = 0.0; + aElevation = 0.0; + return; + } + + sourceListener.Normalize(); + + // Project the source-listener vector on the x-z plane. + const ThreeDPoint& listenerFront = mListenerEngine->FrontVector(); + const ThreeDPoint& listenerRight = mListenerEngine->RightVector(); + ThreeDPoint up = listenerRight.CrossProduct(listenerFront); + + double upProjection = sourceListener.DotProduct(up); + aElevation = 90 - 180 * fdlibm_acos(upProjection) / M_PI; + + if (aElevation > 90) { + aElevation = 180 - aElevation; + } else if (aElevation < -90) { + aElevation = -180 - aElevation; + } + + ThreeDPoint projectedSource = sourceListener - up * upProjection; + if (projectedSource.IsZero()) { + // source - listener direction is up or down. + aAzimuth = 0.0; + return; + } + projectedSource.Normalize(); + + // Actually compute the angle, and convert to degrees + double projection = projectedSource.DotProduct(listenerRight); + aAzimuth = 180 * fdlibm_acos(projection) / M_PI; + + // Compute whether the source is in front or behind the listener. + double frontBack = projectedSource.DotProduct(listenerFront); + if (frontBack < 0) { + aAzimuth = 360 - aAzimuth; + } + // Rotate the azimuth so it is relative to the listener front vector instead + // of the right vector. + if ((aAzimuth >= 0) && (aAzimuth <= 270)) { + aAzimuth = 90 - aAzimuth; + } else { + aAzimuth = 450 - aAzimuth; + } +} + +// This algorithm is described in the WebAudio spec. +float PannerNodeEngine::ComputeConeGain(const ThreeDPoint& position, + const ThreeDPoint& orientation) { + // Omnidirectional source + if (orientation.IsZero() || + ((mConeInnerAngle == 360) && (mConeOuterAngle == 360))) { + return 1; + } + + // Normalized source-listener vector + ThreeDPoint sourceToListener = mListenerEngine->Position() - position; + sourceToListener.Normalize(); + + // Angle between the source orientation vector and the source-listener vector + double dotProduct = sourceToListener.DotProduct(orientation); + double angle = 180 * fdlibm_acos(dotProduct) / M_PI; + double absAngle = fabs(angle); + + // Divide by 2 here since API is entire angle (not half-angle) + double absInnerAngle = fabs(mConeInnerAngle) / 2; + double absOuterAngle = fabs(mConeOuterAngle) / 2; + double gain = 1; + + if (absAngle <= absInnerAngle) { + // No attenuation + gain = 1; + } else if (absAngle >= absOuterAngle) { + // Max attenuation + gain = mConeOuterGain; + } else { + // Between inner and outer cones + // inner -> outer, x goes from 0 -> 1 + double x = (absAngle - absInnerAngle) / (absOuterAngle - absInnerAngle); + gain = (1 - x) + mConeOuterGain * x; + } + + return gain; +} + +double PannerNodeEngine::ComputeDistanceGain(const ThreeDPoint& position) { + ThreeDPoint distanceVec = position - mListenerEngine->Position(); + float distance = sqrt(distanceVec.DotProduct(distanceVec)); + return std::max(0.0f, (this->*mDistanceModelFunction)(distance)); +} + +} // namespace mozilla::dom diff --git a/dom/media/webaudio/PannerNode.h b/dom/media/webaudio/PannerNode.h new file mode 100644 index 0000000000..94876c371c --- /dev/null +++ b/dom/media/webaudio/PannerNode.h @@ -0,0 +1,221 @@ +/* -*- 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/. */ + +#ifndef PannerNode_h_ +#define PannerNode_h_ + +#include "AudioNode.h" +#include "AudioParam.h" +#include "nsPrintfCString.h" +#include "mozilla/dom/PannerNodeBinding.h" +#include "ThreeDPoint.h" +#include <limits> +#include <set> + +namespace mozilla::dom { + +class AudioContext; +class AudioBufferSourceNode; +struct PannerOptions; + +class PannerNode final : public AudioNode { + public: + static already_AddRefed<PannerNode> Create(AudioContext& aAudioContext, + const PannerOptions& aOptions, + ErrorResult& aRv); + + static already_AddRefed<PannerNode> Constructor(const GlobalObject& aGlobal, + AudioContext& aAudioContext, + const PannerOptions& aOptions, + ErrorResult& aRv) { + return Create(aAudioContext, aOptions, aRv); + } + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + void SetChannelCount(uint32_t aChannelCount, ErrorResult& aRv) override { + if (aChannelCount > 2) { + aRv.ThrowNotSupportedError( + nsPrintfCString("%u is greater than 2", aChannelCount)); + return; + } + AudioNode::SetChannelCount(aChannelCount, aRv); + } + void SetChannelCountModeValue(ChannelCountMode aMode, + ErrorResult& aRv) override { + if (aMode == ChannelCountMode::Max) { + aRv.ThrowNotSupportedError("Cannot set channel count mode to \"max\""); + return; + } + AudioNode::SetChannelCountModeValue(aMode, aRv); + } + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(PannerNode, AudioNode) + + PanningModelType PanningModel() const { return mPanningModel; } + void SetPanningModel(PanningModelType aPanningModel); + + DistanceModelType DistanceModel() const { return mDistanceModel; } + void SetDistanceModel(DistanceModelType aDistanceModel) { + mDistanceModel = aDistanceModel; + SendInt32ParameterToTrack(DISTANCE_MODEL, int32_t(mDistanceModel)); + } + + void SetPosition(double aX, double aY, double aZ, ErrorResult& aRv); + void SetOrientation(double aX, double aY, double aZ, ErrorResult& aRv); + + double RefDistance() const { return mRefDistance; } + void SetRefDistance(double aRefDistance, ErrorResult& aRv) { + if (WebAudioUtils::FuzzyEqual(mRefDistance, aRefDistance)) { + return; + } + + if (aRefDistance < 0) { + aRv.ThrowRangeError( + "The refDistance value passed to PannerNode must not be negative."); + return; + } + + mRefDistance = aRefDistance; + SendDoubleParameterToTrack(REF_DISTANCE, mRefDistance); + } + + double MaxDistance() const { return mMaxDistance; } + void SetMaxDistance(double aMaxDistance, ErrorResult& aRv) { + if (WebAudioUtils::FuzzyEqual(mMaxDistance, aMaxDistance)) { + return; + } + + if (aMaxDistance <= 0) { + aRv.ThrowRangeError( + "The maxDistance value passed to PannerNode must be positive."); + return; + } + + mMaxDistance = aMaxDistance; + SendDoubleParameterToTrack(MAX_DISTANCE, mMaxDistance); + } + + double RolloffFactor() const { return mRolloffFactor; } + void SetRolloffFactor(double aRolloffFactor, ErrorResult& aRv) { + if (WebAudioUtils::FuzzyEqual(mRolloffFactor, aRolloffFactor)) { + return; + } + + if (aRolloffFactor < 0) { + aRv.ThrowRangeError( + "The rolloffFactor value passed to PannerNode must not be negative."); + return; + } + + mRolloffFactor = aRolloffFactor; + SendDoubleParameterToTrack(ROLLOFF_FACTOR, mRolloffFactor); + } + + double ConeInnerAngle() const { return mConeInnerAngle; } + void SetConeInnerAngle(double aConeInnerAngle) { + if (WebAudioUtils::FuzzyEqual(mConeInnerAngle, aConeInnerAngle)) { + return; + } + mConeInnerAngle = aConeInnerAngle; + SendDoubleParameterToTrack(CONE_INNER_ANGLE, mConeInnerAngle); + } + + double ConeOuterAngle() const { return mConeOuterAngle; } + void SetConeOuterAngle(double aConeOuterAngle) { + if (WebAudioUtils::FuzzyEqual(mConeOuterAngle, aConeOuterAngle)) { + return; + } + mConeOuterAngle = aConeOuterAngle; + SendDoubleParameterToTrack(CONE_OUTER_ANGLE, mConeOuterAngle); + } + + double ConeOuterGain() const { return mConeOuterGain; } + void SetConeOuterGain(double aConeOuterGain, ErrorResult& aRv) { + if (WebAudioUtils::FuzzyEqual(mConeOuterGain, aConeOuterGain)) { + return; + } + + if (aConeOuterGain < 0 || aConeOuterGain > 1) { + aRv.ThrowInvalidStateError( + nsPrintfCString("%g is not in the range [0, 1]", aConeOuterGain)); + return; + } + + mConeOuterGain = aConeOuterGain; + SendDoubleParameterToTrack(CONE_OUTER_GAIN, mConeOuterGain); + } + + AudioParam* PositionX() { return mPositionX; } + + AudioParam* PositionY() { return mPositionY; } + + AudioParam* PositionZ() { return mPositionZ; } + + AudioParam* OrientationX() { return mOrientationX; } + + AudioParam* OrientationY() { return mOrientationY; } + + AudioParam* OrientationZ() { return mOrientationZ; } + + const char* NodeType() const override { return "PannerNode"; } + + size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override; + size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override; + + private: + explicit PannerNode(AudioContext* aContext); + ~PannerNode() = default; + + friend class AudioListener; + friend class PannerNodeEngine; + enum EngineParameters { + PANNING_MODEL, + DISTANCE_MODEL, + POSITION, + POSITIONX, + POSITIONY, + POSITIONZ, + ORIENTATION, // unit length or zero + ORIENTATIONX, + ORIENTATIONY, + ORIENTATIONZ, + REF_DISTANCE, + MAX_DISTANCE, + ROLLOFF_FACTOR, + CONE_INNER_ANGLE, + CONE_OUTER_ANGLE, + CONE_OUTER_GAIN + }; + + ThreeDPoint ConvertAudioParamTo3DP(RefPtr<AudioParam> aX, + RefPtr<AudioParam> aY, + RefPtr<AudioParam> aZ) { + return ThreeDPoint(aX->GetValue(), aY->GetValue(), aZ->GetValue()); + } + + PanningModelType mPanningModel; + DistanceModelType mDistanceModel; + RefPtr<AudioParam> mPositionX; + RefPtr<AudioParam> mPositionY; + RefPtr<AudioParam> mPositionZ; + RefPtr<AudioParam> mOrientationX; + RefPtr<AudioParam> mOrientationY; + RefPtr<AudioParam> mOrientationZ; + + double mRefDistance; + double mMaxDistance; + double mRolloffFactor; + double mConeInnerAngle; + double mConeOuterAngle; + double mConeOuterGain; +}; + +} // namespace mozilla::dom + +#endif diff --git a/dom/media/webaudio/PanningUtils.h b/dom/media/webaudio/PanningUtils.h new file mode 100644 index 0000000000..eef8ae78c9 --- /dev/null +++ b/dom/media/webaudio/PanningUtils.h @@ -0,0 +1,62 @@ +/* -*- 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/. */ + +#ifndef PANNING_UTILS_H +#define PANNING_UTILS_H + +#include "AudioSegment.h" +#include "AudioNodeEngine.h" + +namespace mozilla::dom { + +template <typename T> +void GainMonoToStereo(const AudioBlock& aInput, AudioBlock* aOutput, T aGainL, + T aGainR) { + float* outputL = aOutput->ChannelFloatsForWrite(0); + float* outputR = aOutput->ChannelFloatsForWrite(1); + const float* input = static_cast<const float*>(aInput.mChannelData[0]); + + MOZ_ASSERT(aInput.ChannelCount() == 1); + MOZ_ASSERT(aOutput->ChannelCount() == 2); + + AudioBlockPanMonoToStereo(input, aGainL, aGainR, outputL, outputR); +} + +// T can be float or an array of float, and U can be bool or an array of bool, +// depending if the value of the parameters are constant for this block. +template <typename T, typename U> +void GainStereoToStereo(const AudioBlock& aInput, AudioBlock* aOutput, T aGainL, + T aGainR, U aOnLeft) { + float* outputL = aOutput->ChannelFloatsForWrite(0); + float* outputR = aOutput->ChannelFloatsForWrite(1); + const float* inputL = static_cast<const float*>(aInput.mChannelData[0]); + const float* inputR = static_cast<const float*>(aInput.mChannelData[1]); + + MOZ_ASSERT(aInput.ChannelCount() == 2); + MOZ_ASSERT(aOutput->ChannelCount() == 2); + + AudioBlockPanStereoToStereo(inputL, inputR, aGainL, aGainR, aOnLeft, outputL, + outputR); +} + +// T can be float or an array of float, and U can be bool or an array of bool, +// depending if the value of the parameters are constant for this block. +template <typename T, typename U> +void ApplyStereoPanning(const AudioBlock& aInput, AudioBlock* aOutput, T aGainL, + T aGainR, U aOnLeft) { + aOutput->AllocateChannels(2); + + if (aInput.ChannelCount() == 1) { + GainMonoToStereo(aInput, aOutput, aGainL, aGainR); + } else { + GainStereoToStereo(aInput, aOutput, aGainL, aGainR, aOnLeft); + } + aOutput->mVolume = aInput.mVolume; +} + +} // namespace mozilla::dom + +#endif // PANNING_UTILS_H diff --git a/dom/media/webaudio/PeriodicWave.cpp b/dom/media/webaudio/PeriodicWave.cpp new file mode 100644 index 0000000000..e036161724 --- /dev/null +++ b/dom/media/webaudio/PeriodicWave.cpp @@ -0,0 +1,146 @@ +/* -*- 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 "PeriodicWave.h" +#include "AudioContext.h" +#include "mozilla/dom/PeriodicWaveBinding.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(PeriodicWave, mContext) + +PeriodicWave::PeriodicWave(AudioContext* aContext, const float* aRealData, + const uint32_t aRealSize, const float* aImagData, + const uint32_t aImagSize, + const bool aDisableNormalization, ErrorResult& aRv) + : mContext(aContext), mDisableNormalization(aDisableNormalization) { + if (aRealData && aImagData && aRealSize != aImagSize) { + aRv.ThrowIndexSizeError("\"real\" and \"imag\" are different in length"); + return; + } + + uint32_t length = 0; + if (aRealData) { + length = aRealSize; + } else if (aImagData) { + length = aImagSize; + } else { + // If nothing has been passed, this PeriodicWave will be a sine wave: 2 + // elements for each array, the second imaginary component set to 1.0. + length = 2; + } + + if (length < 2) { + aRv.ThrowIndexSizeError( + "\"real\" and \"imag\" must have a length of " + "at least 2"); + return; + } + + MOZ_ASSERT(aContext); + MOZ_ASSERT((aRealData || aImagData) || length == 2); + + // Caller should have checked this and thrown. + MOZ_ASSERT(length >= 2); + mCoefficients.mDuration = length; + + // Copy coefficient data. + // The SharedBuffer and two arrays share a single allocation. + CheckedInt<size_t> bufferSize(sizeof(float)); + bufferSize *= length; + bufferSize *= 2; + RefPtr<SharedBuffer> buffer = SharedBuffer::Create(bufferSize, fallible); + if (!buffer) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + auto data = static_cast<float*>(buffer->Data()); + mCoefficients.mBuffer = std::move(buffer); + + if (!aRealData && !aImagData) { + PodZero(data, length); + mCoefficients.mChannelData.AppendElement(data); + data += length; + data[0] = 0.0f; + data[1] = 1.0f; + mCoefficients.mChannelData.AppendElement(data); + } else { + if (aRealData) { + PodCopy(data, aRealData, length); + } else { + PodZero(data, length); + } + mCoefficients.mChannelData.AppendElement(data); + + data += length; + if (aImagData) { + PodCopy(data, aImagData, length); + } else { + PodZero(data, length); + } + mCoefficients.mChannelData.AppendElement(data); + } + mCoefficients.mVolume = 1.0f; + mCoefficients.mBufferFormat = AUDIO_FORMAT_FLOAT32; +} + +/* static */ +already_AddRefed<PeriodicWave> PeriodicWave::Constructor( + const GlobalObject& aGlobal, AudioContext& aAudioContext, + const PeriodicWaveOptions& aOptions, ErrorResult& aRv) { + const float* realData; + const float* imagData; + uint32_t realSize; + uint32_t imagSize; + + if (aOptions.mReal.WasPassed()) { + realData = aOptions.mReal.Value().Elements(); + realSize = aOptions.mReal.Value().Length(); + } else { + realData = nullptr; + realSize = 0; + } + + if (aOptions.mImag.WasPassed()) { + imagData = aOptions.mImag.Value().Elements(); + imagSize = aOptions.mImag.Value().Length(); + } else { + imagData = nullptr; + imagSize = 0; + } + + RefPtr<PeriodicWave> wave = + new PeriodicWave(&aAudioContext, realData, realSize, imagData, imagSize, + aOptions.mDisableNormalization, aRv); + if (aRv.Failed()) { + return nullptr; + } + + return wave.forget(); +} + +size_t PeriodicWave::SizeOfExcludingThisIfNotShared( + MallocSizeOf aMallocSizeOf) const { + // Not owned: + // - mContext + size_t amount = 0; + amount += mCoefficients.SizeOfExcludingThisIfUnshared(aMallocSizeOf); + + return amount; +} + +size_t PeriodicWave::SizeOfIncludingThisIfNotShared( + MallocSizeOf aMallocSizeOf) const { + return aMallocSizeOf(this) + SizeOfExcludingThisIfNotShared(aMallocSizeOf); +} + +JSObject* PeriodicWave::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return PeriodicWave_Binding::Wrap(aCx, this, aGivenProto); +} + +} // namespace mozilla::dom diff --git a/dom/media/webaudio/PeriodicWave.h b/dom/media/webaudio/PeriodicWave.h new file mode 100644 index 0000000000..18408789a9 --- /dev/null +++ b/dom/media/webaudio/PeriodicWave.h @@ -0,0 +1,59 @@ +/* -*- 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/. */ + +#ifndef PeriodicWave_h_ +#define PeriodicWave_h_ + +#include "nsWrapperCache.h" +#include "nsCycleCollectionParticipant.h" +#include "mozilla/Attributes.h" +#include "AudioContext.h" +#include "AudioNodeEngine.h" + +namespace mozilla::dom { + +class AudioContext; +struct PeriodicWaveOptions; + +class PeriodicWave final : public nsWrapperCache { + public: + PeriodicWave(AudioContext* aContext, const float* aRealData, + const uint32_t aRealSize, const float* aImagData, + const uint32_t aImagSize, const bool aDisableNormalization, + ErrorResult& aRv); + + NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(PeriodicWave) + NS_DECL_CYCLE_COLLECTION_NATIVE_WRAPPERCACHE_CLASS(PeriodicWave) + + static already_AddRefed<PeriodicWave> Constructor( + const GlobalObject& aGlobal, AudioContext& aAudioContext, + const PeriodicWaveOptions& aOptions, ErrorResult& aRv); + + AudioContext* GetParentObject() const { return mContext; } + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + uint32_t DataLength() const { return mCoefficients.mDuration; } + + bool DisableNormalization() const { return mDisableNormalization; } + + const AudioChunk& GetThreadSharedBuffer() const { return mCoefficients; } + + size_t SizeOfExcludingThisIfNotShared(MallocSizeOf aMallocSizeOf) const; + size_t SizeOfIncludingThisIfNotShared(MallocSizeOf aMallocSizeOf) const; + + private: + ~PeriodicWave() = default; + + AudioChunk mCoefficients; + RefPtr<AudioContext> mContext; + bool mDisableNormalization; +}; + +} // namespace mozilla::dom + +#endif diff --git a/dom/media/webaudio/PlayingRefChangeHandler.h b/dom/media/webaudio/PlayingRefChangeHandler.h new file mode 100644 index 0000000000..cda01fdcd3 --- /dev/null +++ b/dom/media/webaudio/PlayingRefChangeHandler.h @@ -0,0 +1,42 @@ +/* -*- 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/. */ + +#ifndef PlayingRefChangeHandler_h__ +#define PlayingRefChangeHandler_h__ + +#include "nsThreadUtils.h" +#include "AudioNodeTrack.h" + +namespace mozilla::dom { + +class PlayingRefChangeHandler final : public Runnable { + public: + enum ChangeType { ADDREF, RELEASE }; + PlayingRefChangeHandler(AudioNodeTrack* aTrack, ChangeType aChange) + : Runnable("dom::PlayingRefChangeHandler"), + mTrack(aTrack), + mChange(aChange) {} + + NS_IMETHOD Run() override { + RefPtr<AudioNode> node = mTrack->Engine()->NodeMainThread(); + if (node) { + if (mChange == ADDREF) { + node->MarkActive(); + } else if (mChange == RELEASE) { + node->MarkInactive(); + } + } + return NS_OK; + } + + private: + RefPtr<AudioNodeTrack> mTrack; + ChangeType mChange; +}; + +} // namespace mozilla::dom + +#endif diff --git a/dom/media/webaudio/ReportDecodeResultTask.h b/dom/media/webaudio/ReportDecodeResultTask.h new file mode 100644 index 0000000000..49b7e39e04 --- /dev/null +++ b/dom/media/webaudio/ReportDecodeResultTask.h @@ -0,0 +1,37 @@ +/* -*- 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/. */ + +#ifndef ReportDecodeResultTask_h_ +#define ReportDecodeResultTask_h_ + +#include "mozilla/Attributes.h" +#include "MediaBufferDecoder.h" + +namespace mozilla { + +class ReportDecodeResultTask final : public Runnable { + public: + ReportDecodeResultTask(DecodeJob& aDecodeJob, DecodeJob::ResultFn aFunction) + : mDecodeJob(aDecodeJob), mFunction(aFunction) { + MOZ_ASSERT(aFunction); + } + + NS_IMETHOD Run() override { + MOZ_ASSERT(NS_IsMainThread()); + + (mDecodeJob.*mFunction)(); + + return NS_OK; + } + + private: + DecodeJob& mDecodeJob; + DecodeJob::ResultFn mFunction; +}; + +} // namespace mozilla + +#endif diff --git a/dom/media/webaudio/ScriptProcessorNode.cpp b/dom/media/webaudio/ScriptProcessorNode.cpp new file mode 100644 index 0000000000..1e645a2b42 --- /dev/null +++ b/dom/media/webaudio/ScriptProcessorNode.cpp @@ -0,0 +1,549 @@ +/* -*- 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 "ScriptProcessorNode.h" +#include "mozilla/dom/ScriptProcessorNodeBinding.h" +#include "AudioBuffer.h" +#include "AudioDestinationNode.h" +#include "AudioNodeEngine.h" +#include "AudioNodeTrack.h" +#include "AudioProcessingEvent.h" +#include "WebAudioUtils.h" +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/Mutex.h" +#include "mozilla/PodOperations.h" +#include <deque> +#include "Tracing.h" + +namespace mozilla::dom { + +// The maximum latency, in seconds, that we can live with before dropping +// buffers. +static const float MAX_LATENCY_S = 0.5; + +// This class manages a queue of output buffers shared between +// the main thread and the Media Track Graph thread. +class SharedBuffers final { + private: + class OutputQueue final { + public: + explicit OutputQueue(const char* aName) : mMutex(aName) {} + + size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const + MOZ_REQUIRES(mMutex) { + mMutex.AssertCurrentThreadOwns(); + + size_t amount = 0; + for (size_t i = 0; i < mBufferList.size(); i++) { + amount += mBufferList[i].SizeOfExcludingThis(aMallocSizeOf, false); + } + + return amount; + } + + Mutex& Lock() const MOZ_RETURN_CAPABILITY(mMutex) { + return const_cast<OutputQueue*>(this)->mMutex; + } + + size_t ReadyToConsume() const MOZ_REQUIRES(mMutex) { + // Accessed on both main thread and media graph thread. + mMutex.AssertCurrentThreadOwns(); + return mBufferList.size(); + } + + // Produce one buffer + AudioChunk& Produce() MOZ_REQUIRES(mMutex) { + mMutex.AssertCurrentThreadOwns(); + MOZ_ASSERT(NS_IsMainThread()); + mBufferList.push_back(AudioChunk()); + return mBufferList.back(); + } + + // Consumes one buffer. + AudioChunk Consume() MOZ_REQUIRES(mMutex) { + mMutex.AssertCurrentThreadOwns(); + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(ReadyToConsume() > 0); + AudioChunk front = mBufferList.front(); + mBufferList.pop_front(); + return front; + } + + // Empties the buffer queue. + void Clear() MOZ_REQUIRES(mMutex) { + mMutex.AssertCurrentThreadOwns(); + mBufferList.clear(); + } + + private: + typedef std::deque<AudioChunk> BufferList; + + // Synchronizes access to mBufferList. Note that it's the responsibility + // of the callers to perform the required locking, and we assert that every + // time we access mBufferList. + Mutex mMutex MOZ_UNANNOTATED; + // The list representing the queue. + BufferList mBufferList; + }; + + public: + explicit SharedBuffers(float aSampleRate) + : mOutputQueue("SharedBuffers::outputQueue"), + mDelaySoFar(TRACK_TIME_MAX), + mSampleRate(aSampleRate), + mLatency(0.0), + mDroppingBuffers(false) {} + + size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const { + size_t amount = aMallocSizeOf(this); + + { + MutexAutoLock lock(mOutputQueue.Lock()); + amount += mOutputQueue.SizeOfExcludingThis(aMallocSizeOf); + } + + return amount; + } + + // main thread + + // NotifyNodeIsConnected() may be called even when the state has not + // changed. + void NotifyNodeIsConnected(bool aIsConnected) { + MOZ_ASSERT(NS_IsMainThread()); + if (!aIsConnected) { + // Reset main thread state for FinishProducingOutputBuffer(). + mLatency = 0.0f; + mLastEventTime = TimeStamp(); + mDroppingBuffers = false; + // Don't flush the output buffer here because the graph thread may be + // using it now. The graph thread will flush when it knows it is + // disconnected. + } + mNodeIsConnected = aIsConnected; + } + + void FinishProducingOutputBuffer(const AudioChunk& aBuffer) { + MOZ_ASSERT(NS_IsMainThread()); + + if (!mNodeIsConnected) { + // The output buffer is not used, and mLastEventTime will not be + // initialized until the node is re-connected. + return; + } + + TimeStamp now = TimeStamp::Now(); + + if (mLastEventTime.IsNull()) { + mLastEventTime = now; + } else { + // When main thread blocking has built up enough so + // |mLatency > MAX_LATENCY_S|, frame dropping starts. It continues until + // the output buffer is completely empty, at which point the accumulated + // latency is also reset to 0. + // It could happen that the output queue becomes empty before the input + // node has fully caught up. In this case there will be events where + // |(now - mLastEventTime)| is very short, making mLatency negative. + // As this happens and the size of |mLatency| becomes greater than + // MAX_LATENCY_S, frame dropping starts again to maintain an as short + // output queue as possible. + float latency = (now - mLastEventTime).ToSeconds(); + float bufferDuration = aBuffer.mDuration / mSampleRate; + mLatency += latency - bufferDuration; + mLastEventTime = now; + if (fabs(mLatency) > MAX_LATENCY_S) { + mDroppingBuffers = true; + } + } + + MutexAutoLock lock(mOutputQueue.Lock()); + if (mDroppingBuffers) { + if (mOutputQueue.ReadyToConsume()) { + return; + } + mDroppingBuffers = false; + mLatency = 0; + } + + for (uint32_t offset = 0; offset < aBuffer.mDuration; + offset += WEBAUDIO_BLOCK_SIZE) { + AudioChunk& chunk = mOutputQueue.Produce(); + chunk = aBuffer; + chunk.SliceTo(offset, offset + WEBAUDIO_BLOCK_SIZE); + } + } + + // graph thread + + AudioChunk GetOutputBuffer() { + MOZ_ASSERT(!NS_IsMainThread()); + AudioChunk buffer; + + { + MutexAutoLock lock(mOutputQueue.Lock()); + if (mOutputQueue.ReadyToConsume() > 0) { + if (mDelaySoFar == TRACK_TIME_MAX) { + mDelaySoFar = 0; + } + buffer = mOutputQueue.Consume(); + } else { + // If we're out of buffers to consume, just output silence + buffer.SetNull(WEBAUDIO_BLOCK_SIZE); + if (mDelaySoFar != TRACK_TIME_MAX) { + // Remember the delay that we just hit + mDelaySoFar += WEBAUDIO_BLOCK_SIZE; + } + } + } + + return buffer; + } + + TrackTime DelaySoFar() const { + MOZ_ASSERT(!NS_IsMainThread()); + return mDelaySoFar == TRACK_TIME_MAX ? 0 : mDelaySoFar; + } + + void Flush() { + MOZ_ASSERT(!NS_IsMainThread()); + mDelaySoFar = TRACK_TIME_MAX; + { + MutexAutoLock lock(mOutputQueue.Lock()); + mOutputQueue.Clear(); + } + } + + private: + OutputQueue mOutputQueue; + // How much delay we've seen so far. This measures the amount of delay + // caused by the main thread lagging behind in producing output buffers. + // TRACK_TIME_MAX means that we have not received our first buffer yet. + // Graph thread only. + TrackTime mDelaySoFar; + // The samplerate of the context. + const float mSampleRate; + // The remaining members are main thread only. + // This is the latency caused by the buffering. If this grows too high, we + // will drop buffers until it is acceptable. + float mLatency; + // This is the time at which we last produced a buffer, to detect if the main + // thread has been blocked. + TimeStamp mLastEventTime; + // True if we should be dropping buffers. + bool mDroppingBuffers; + // True iff the AudioNode has at least one input or output connected. + bool mNodeIsConnected; +}; + +class ScriptProcessorNodeEngine final : public AudioNodeEngine { + public: + ScriptProcessorNodeEngine(ScriptProcessorNode* aNode, + AudioDestinationNode* aDestination, + uint32_t aBufferSize, + uint32_t aNumberOfInputChannels) + : AudioNodeEngine(aNode), + mDestination(aDestination->Track()), + mSharedBuffers(new SharedBuffers(mDestination->mSampleRate)), + mBufferSize(aBufferSize), + mInputChannelCount(aNumberOfInputChannels), + mInputWriteIndex(0) {} + + SharedBuffers* GetSharedBuffers() const { return mSharedBuffers.get(); } + + enum { + IS_CONNECTED, + }; + + void SetInt32Parameter(uint32_t aIndex, int32_t aParam) override { + switch (aIndex) { + case IS_CONNECTED: + mIsConnected = aParam; + break; + default: + NS_ERROR("Bad Int32Parameter"); + } // End index switch. + } + + void ProcessBlock(AudioNodeTrack* aTrack, GraphTime aFrom, + const AudioBlock& aInput, AudioBlock* aOutput, + bool* aFinished) override { + TRACE("ScriptProcessorNodeEngine::ProcessBlock"); + + // This node is not connected to anything. Per spec, we don't fire the + // onaudioprocess event. We also want to clear out the input and output + // buffer queue, and output a null buffer. + if (!mIsConnected) { + aOutput->SetNull(WEBAUDIO_BLOCK_SIZE); + mSharedBuffers->Flush(); + mInputWriteIndex = 0; + return; + } + + // The input buffer is allocated lazily when non-null input is received. + if (!aInput.IsNull() && !mInputBuffer) { + mInputBuffer = ThreadSharedFloatArrayBufferList::Create( + mInputChannelCount, mBufferSize, fallible); + if (mInputBuffer && mInputWriteIndex) { + // Zero leading for null chunks that were skipped. + for (uint32_t i = 0; i < mInputChannelCount; ++i) { + float* channelData = mInputBuffer->GetDataForWrite(i); + PodZero(channelData, mInputWriteIndex); + } + } + } + + // First, record our input buffer, if its allocation succeeded. + uint32_t inputChannelCount = mInputBuffer ? mInputBuffer->GetChannels() : 0; + for (uint32_t i = 0; i < inputChannelCount; ++i) { + float* writeData = mInputBuffer->GetDataForWrite(i) + mInputWriteIndex; + if (aInput.IsNull()) { + PodZero(writeData, aInput.GetDuration()); + } else { + MOZ_ASSERT(aInput.GetDuration() == WEBAUDIO_BLOCK_SIZE, "sanity check"); + MOZ_ASSERT(aInput.ChannelCount() == inputChannelCount); + AudioBlockCopyChannelWithScale( + static_cast<const float*>(aInput.mChannelData[i]), aInput.mVolume, + writeData); + } + } + mInputWriteIndex += aInput.GetDuration(); + + // Now, see if we have data to output + // Note that we need to do this before sending the buffer to the main + // thread so that our delay time is updated. + *aOutput = mSharedBuffers->GetOutputBuffer(); + + if (mInputWriteIndex >= mBufferSize) { + SendBuffersToMainThread(aTrack, aFrom); + mInputWriteIndex -= mBufferSize; + } + } + + bool IsActive() const override { + // Could return false when !mIsConnected after all output chunks produced + // by main thread events calling + // SharedBuffers::FinishProducingOutputBuffer() have been processed. + return true; + } + + size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override { + // Not owned: + // - mDestination (probably) + size_t amount = AudioNodeEngine::SizeOfExcludingThis(aMallocSizeOf); + amount += mSharedBuffers->SizeOfIncludingThis(aMallocSizeOf); + if (mInputBuffer) { + amount += mInputBuffer->SizeOfIncludingThis(aMallocSizeOf); + } + + return amount; + } + + size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override { + return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf); + } + + private: + void SendBuffersToMainThread(AudioNodeTrack* aTrack, GraphTime aFrom) { + MOZ_ASSERT(!NS_IsMainThread()); + + // we now have a full input buffer ready to be sent to the main thread. + TrackTime playbackTick = mDestination->GraphTimeToTrackTime(aFrom); + // Add the duration of the current sample + playbackTick += WEBAUDIO_BLOCK_SIZE; + // Add the delay caused by the main thread + playbackTick += mSharedBuffers->DelaySoFar(); + // Compute the playback time in the coordinate system of the destination + double playbackTime = mDestination->TrackTimeToSeconds(playbackTick); + + class Command final : public Runnable { + public: + Command(AudioNodeTrack* aTrack, + already_AddRefed<ThreadSharedFloatArrayBufferList> aInputBuffer, + double aPlaybackTime) + : mozilla::Runnable("Command"), + mTrack(aTrack), + mInputBuffer(aInputBuffer), + mPlaybackTime(aPlaybackTime) {} + + NS_IMETHOD Run() override { + auto engine = static_cast<ScriptProcessorNodeEngine*>(mTrack->Engine()); + AudioChunk output; + output.SetNull(engine->mBufferSize); + { + auto node = + static_cast<ScriptProcessorNode*>(engine->NodeMainThread()); + if (!node) { + return NS_OK; + } + + if (node->HasListenersFor(nsGkAtoms::onaudioprocess)) { + DispatchAudioProcessEvent(node, &output); + } + // The node may have been destroyed during event dispatch. + } + + // Append it to our output buffer queue + engine->GetSharedBuffers()->FinishProducingOutputBuffer(output); + + return NS_OK; + } + + // Sets up |output| iff buffers are set in event handlers. + void DispatchAudioProcessEvent(ScriptProcessorNode* aNode, + AudioChunk* aOutput) { + AudioContext* context = aNode->Context(); + if (!context) { + return; + } + + AutoJSAPI jsapi; + if (NS_WARN_IF(!jsapi.Init(aNode->GetOwner()))) { + return; + } + JSContext* cx = jsapi.cx(); + uint32_t inputChannelCount = aNode->ChannelCount(); + + // Create the input buffer + RefPtr<AudioBuffer> inputBuffer; + if (mInputBuffer) { + ErrorResult rv; + inputBuffer = AudioBuffer::Create( + context->GetOwner(), inputChannelCount, aNode->BufferSize(), + context->SampleRate(), mInputBuffer.forget(), rv); + if (rv.Failed()) { + rv.SuppressException(); + return; + } + } + + // Ask content to produce data in the output buffer + // Note that we always avoid creating the output buffer here, and we try + // to avoid creating the input buffer as well. The AudioProcessingEvent + // class knows how to lazily create them if needed once the script tries + // to access them. Otherwise, we may be able to get away without + // creating them! + RefPtr<AudioProcessingEvent> event = + new AudioProcessingEvent(aNode, nullptr, nullptr); + event->InitEvent(inputBuffer, inputChannelCount, mPlaybackTime); + aNode->DispatchTrustedEvent(event); + + // Steal the output buffers if they have been set. + // Don't create a buffer if it hasn't been used to return output; + // FinishProducingOutputBuffer() will optimize output = null. + // GetThreadSharedChannelsForRate() may also return null after OOM. + if (event->HasOutputBuffer()) { + ErrorResult rv; + AudioBuffer* buffer = event->GetOutputBuffer(rv); + // HasOutputBuffer() returning true means that GetOutputBuffer() + // will not fail. + MOZ_ASSERT(!rv.Failed()); + *aOutput = buffer->GetThreadSharedChannelsForRate(cx); + MOZ_ASSERT(aOutput->IsNull() || + aOutput->mBufferFormat == AUDIO_FORMAT_FLOAT32, + "AudioBuffers initialized from JS have float data"); + } + } + + private: + RefPtr<AudioNodeTrack> mTrack; + RefPtr<ThreadSharedFloatArrayBufferList> mInputBuffer; + double mPlaybackTime; + }; + + RefPtr<Command> command = + new Command(aTrack, mInputBuffer.forget(), playbackTime); + AbstractThread::MainThread()->Dispatch(command.forget()); + } + + friend class ScriptProcessorNode; + + RefPtr<AudioNodeTrack> mDestination; + UniquePtr<SharedBuffers> mSharedBuffers; + RefPtr<ThreadSharedFloatArrayBufferList> mInputBuffer; + const uint32_t mBufferSize; + const uint32_t mInputChannelCount; + // The write index into the current input buffer + uint32_t mInputWriteIndex; + bool mIsConnected = false; +}; + +ScriptProcessorNode::ScriptProcessorNode(AudioContext* aContext, + uint32_t aBufferSize, + uint32_t aNumberOfInputChannels, + uint32_t aNumberOfOutputChannels) + : AudioNode(aContext, aNumberOfInputChannels, + mozilla::dom::ChannelCountMode::Explicit, + mozilla::dom::ChannelInterpretation::Speakers), + mBufferSize(aBufferSize ? aBufferSize + : // respect what the web developer requested + 4096) // choose our own buffer size -- 4KB for now + , + mNumberOfOutputChannels(aNumberOfOutputChannels) { + MOZ_ASSERT(BufferSize() % WEBAUDIO_BLOCK_SIZE == 0, "Invalid buffer size"); + ScriptProcessorNodeEngine* engine = new ScriptProcessorNodeEngine( + this, aContext->Destination(), BufferSize(), aNumberOfInputChannels); + mTrack = AudioNodeTrack::Create( + aContext, engine, AudioNodeTrack::NO_TRACK_FLAGS, aContext->Graph()); +} + +ScriptProcessorNode::~ScriptProcessorNode() = default; + +size_t ScriptProcessorNode::SizeOfExcludingThis( + MallocSizeOf aMallocSizeOf) const { + size_t amount = AudioNode::SizeOfExcludingThis(aMallocSizeOf); + return amount; +} + +size_t ScriptProcessorNode::SizeOfIncludingThis( + MallocSizeOf aMallocSizeOf) const { + return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf); +} + +void ScriptProcessorNode::EventListenerAdded(nsAtom* aType) { + AudioNode::EventListenerAdded(aType); + if (aType == nsGkAtoms::onaudioprocess) { + UpdateConnectedStatus(); + } +} + +void ScriptProcessorNode::EventListenerRemoved(nsAtom* aType) { + AudioNode::EventListenerRemoved(aType); + if (aType == nsGkAtoms::onaudioprocess && mTrack) { + UpdateConnectedStatus(); + } +} + +JSObject* ScriptProcessorNode::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return ScriptProcessorNode_Binding::Wrap(aCx, this, aGivenProto); +} + +void ScriptProcessorNode::UpdateConnectedStatus() { + bool isConnected = + mHasPhantomInput || !(OutputNodes().IsEmpty() && + OutputParams().IsEmpty() && InputNodes().IsEmpty()); + + // Events are queued even when there is no listener because a listener + // may be added while events are in the queue. + SendInt32ParameterToTrack(ScriptProcessorNodeEngine::IS_CONNECTED, + isConnected); + + if (isConnected && HasListenersFor(nsGkAtoms::onaudioprocess)) { + MarkActive(); + } else { + MarkInactive(); + } + + // MarkInactive above might have released this node, check if it has a track. + if (!mTrack) { + return; + } + + auto engine = static_cast<ScriptProcessorNodeEngine*>(mTrack->Engine()); + engine->GetSharedBuffers()->NotifyNodeIsConnected(isConnected); +} + +} // namespace mozilla::dom diff --git a/dom/media/webaudio/ScriptProcessorNode.h b/dom/media/webaudio/ScriptProcessorNode.h new file mode 100644 index 0000000000..c8f023cb17 --- /dev/null +++ b/dom/media/webaudio/ScriptProcessorNode.h @@ -0,0 +1,131 @@ +/* -*- 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/. */ + +#ifndef ScriptProcessorNode_h_ +#define ScriptProcessorNode_h_ + +#include "AudioNode.h" +#include "mozilla/ErrorResult.h" + +namespace mozilla::dom { + +class AudioContext; +class SharedBuffers; + +class ScriptProcessorNode final : public AudioNode { + public: + ScriptProcessorNode(AudioContext* aContext, uint32_t aBufferSize, + uint32_t aNumberOfInputChannels, + uint32_t aNumberOfOutputChannels); + + NS_INLINE_DECL_REFCOUNTING_INHERITED(ScriptProcessorNode, AudioNode) + + IMPL_EVENT_HANDLER(audioprocess) + + using EventTarget::EventListenerAdded; + void EventListenerAdded(nsAtom* aType) override; + + using EventTarget::EventListenerRemoved; + void EventListenerRemoved(nsAtom* aType) override; + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + AudioNode* Connect(AudioNode& aDestination, uint32_t aOutput, uint32_t aInput, + ErrorResult& aRv) override { + AudioNode* node = AudioNode::Connect(aDestination, aOutput, aInput, aRv); + if (!aRv.Failed()) { + UpdateConnectedStatus(); + } + return node; + } + + void Connect(AudioParam& aDestination, uint32_t aOutput, + ErrorResult& aRv) override { + AudioNode::Connect(aDestination, aOutput, aRv); + if (!aRv.Failed()) { + UpdateConnectedStatus(); + } + } + void Disconnect(ErrorResult& aRv) override { + AudioNode::Disconnect(aRv); + UpdateConnectedStatus(); + } + void Disconnect(uint32_t aOutput, ErrorResult& aRv) override { + AudioNode::Disconnect(aOutput, aRv); + UpdateConnectedStatus(); + } + void NotifyInputsChanged() override { UpdateConnectedStatus(); } + void NotifyHasPhantomInput() override { + mHasPhantomInput = true; + // No need to UpdateConnectedStatus() because there was previously an + // input in InputNodes(). + } + void Disconnect(AudioNode& aDestination, ErrorResult& aRv) override { + AudioNode::Disconnect(aDestination, aRv); + UpdateConnectedStatus(); + } + void Disconnect(AudioNode& aDestination, uint32_t aOutput, + ErrorResult& aRv) override { + AudioNode::Disconnect(aDestination, aOutput, aRv); + UpdateConnectedStatus(); + } + void Disconnect(AudioNode& aDestination, uint32_t aOutput, uint32_t aInput, + ErrorResult& aRv) override { + AudioNode::Disconnect(aDestination, aOutput, aInput, aRv); + UpdateConnectedStatus(); + } + void Disconnect(AudioParam& aDestination, ErrorResult& aRv) override { + AudioNode::Disconnect(aDestination, aRv); + UpdateConnectedStatus(); + } + void Disconnect(AudioParam& aDestination, uint32_t aOutput, + ErrorResult& aRv) override { + AudioNode::Disconnect(aDestination, aOutput, aRv); + UpdateConnectedStatus(); + } + void SetChannelCount(uint32_t aChannelCount, ErrorResult& aRv) override { + if (aChannelCount != ChannelCount()) { + // Spec says to throw InvalidStateError, but see + // https://github.com/WebAudio/web-audio-api/issues/2153 + aRv.ThrowNotSupportedError( + "Cannot change channel count of ScriptProcessorNode"); + } + } + void SetChannelCountModeValue(ChannelCountMode aMode, + ErrorResult& aRv) override { + if (aMode != ChannelCountMode::Explicit) { + // Spec says to throw InvalidStateError, but see + // https://github.com/WebAudio/web-audio-api/issues/2154 + aRv.ThrowNotSupportedError( + "Cannot change channel count mode of ScriptProcessorNode"); + } + } + + uint32_t BufferSize() const { return mBufferSize; } + + uint32_t NumberOfOutputChannels() const { return mNumberOfOutputChannels; } + + using DOMEventTargetHelper::DispatchTrustedEvent; + + const char* NodeType() const override { return "ScriptProcessorNode"; } + + size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override; + size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override; + + private: + virtual ~ScriptProcessorNode(); + + void UpdateConnectedStatus(); + + const uint32_t mBufferSize; + const uint32_t mNumberOfOutputChannels; + bool mHasPhantomInput = false; +}; + +} // namespace mozilla::dom + +#endif diff --git a/dom/media/webaudio/StereoPannerNode.cpp b/dom/media/webaudio/StereoPannerNode.cpp new file mode 100644 index 0000000000..758a92511a --- /dev/null +++ b/dom/media/webaudio/StereoPannerNode.cpp @@ -0,0 +1,197 @@ +/* -*- 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 "StereoPannerNode.h" +#include "mozilla/dom/StereoPannerNodeBinding.h" +#include "AudioNodeEngine.h" +#include "AudioNodeTrack.h" +#include "AudioDestinationNode.h" +#include "AlignmentUtils.h" +#include "WebAudioUtils.h" +#include "PanningUtils.h" +#include "AudioParamTimeline.h" +#include "AudioParam.h" +#include "Tracing.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_INHERITED(StereoPannerNode, AudioNode, mPan) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(StereoPannerNode) +NS_INTERFACE_MAP_END_INHERITING(AudioNode) + +NS_IMPL_ADDREF_INHERITED(StereoPannerNode, AudioNode) +NS_IMPL_RELEASE_INHERITED(StereoPannerNode, AudioNode) + +class StereoPannerNodeEngine final : public AudioNodeEngine { + public: + StereoPannerNodeEngine(AudioNode* aNode, AudioDestinationNode* aDestination) + : AudioNodeEngine(aNode), + mDestination(aDestination->Track()) + // Keep the default value in sync with the default value in + // StereoPannerNode::StereoPannerNode. + , + mPan(0.f) {} + + enum Parameters { PAN }; + void RecvTimelineEvent(uint32_t aIndex, AudioParamEvent& aEvent) override { + MOZ_ASSERT(mDestination); + aEvent.ConvertToTicks(mDestination); + + switch (aIndex) { + case PAN: + mPan.InsertEvent<int64_t>(aEvent); + break; + default: + NS_ERROR("Bad StereoPannerNode TimelineParameter"); + } + } + + void GetGainValuesForPanning(float aPanning, bool aMonoToStereo, + float& aLeftGain, float& aRightGain) { + // Clamp and normalize the panning in [0; 1] + aPanning = std::min(std::max(aPanning, -1.f), 1.f); + + if (aMonoToStereo) { + aPanning += 1; + aPanning /= 2; + } else if (aPanning <= 0) { + aPanning += 1; + } + + aLeftGain = fdlibm_cos(0.5 * M_PI * aPanning); + aRightGain = fdlibm_sin(0.5 * M_PI * aPanning); + } + + void SetToSilentStereoBlock(AudioBlock* aChunk) { + for (uint32_t channel = 0; channel < 2; channel++) { + float* samples = aChunk->ChannelFloatsForWrite(channel); + for (uint32_t i = 0; i < WEBAUDIO_BLOCK_SIZE; i++) { + samples[i] = 0.f; + } + } + } + + void UpmixToStereoIfNeeded(const AudioBlock& aInput, AudioBlock* aOutput) { + if (aInput.ChannelCount() == 2) { + *aOutput = aInput; + } else { + MOZ_ASSERT(aInput.ChannelCount() == 1); + aOutput->SetBuffer(aInput.GetBuffer()); + aOutput->mChannelData.SetLength(2); + for (uint32_t i = 0; i < 2; ++i) { + aOutput->mChannelData[i] = aInput.ChannelData<float>()[0]; + } + // 1/sqrt(2) multiplier is because StereoPanner up-mixing differs from + // input up-mixing. + aOutput->mVolume = M_SQRT1_2 * aInput.mVolume; + aOutput->mBufferFormat = AUDIO_FORMAT_FLOAT32; + } + } + + virtual void ProcessBlock(AudioNodeTrack* aTrack, GraphTime aFrom, + const AudioBlock& aInput, AudioBlock* aOutput, + bool* aFinished) override { + // The output of this node is always stereo, no matter what the inputs are. + MOZ_ASSERT(aInput.ChannelCount() <= 2); + TRACE("StereoPannerNodeEngine::ProcessBlock"); + + bool monoToStereo = aInput.ChannelCount() == 1; + + if (aInput.IsNull()) { + // If input is silent, so is the output + aOutput->SetNull(WEBAUDIO_BLOCK_SIZE); + } else if (mPan.HasSimpleValue()) { + float panning = mPan.GetValue(); + // If the panning is 0.0, we can simply copy the input to the + // output with gain applied, up-mixing to stereo if needed. + if (panning == 0.0f) { + UpmixToStereoIfNeeded(aInput, aOutput); + } else { + // Optimize the case where the panning is constant for this processing + // block: we can just apply a constant gain on the left and right + // channel + float gainL, gainR; + + GetGainValuesForPanning(panning, monoToStereo, gainL, gainR); + ApplyStereoPanning(aInput, aOutput, gainL, gainR, panning <= 0); + } + } else { + float computedGain[2 * WEBAUDIO_BLOCK_SIZE + 4]; + bool onLeft[WEBAUDIO_BLOCK_SIZE]; + + float values[WEBAUDIO_BLOCK_SIZE]; + TrackTime tick = mDestination->GraphTimeToTrackTime(aFrom); + mPan.GetValuesAtTime(tick, values, WEBAUDIO_BLOCK_SIZE); + + float* alignedComputedGain = ALIGNED16(computedGain); + ASSERT_ALIGNED16(alignedComputedGain); + for (size_t counter = 0; counter < WEBAUDIO_BLOCK_SIZE; ++counter) { + float left, right; + GetGainValuesForPanning(values[counter], monoToStereo, left, right); + + alignedComputedGain[counter] = left; + alignedComputedGain[WEBAUDIO_BLOCK_SIZE + counter] = right; + onLeft[counter] = values[counter] <= 0; + } + + // Apply the gain to the output buffer + ApplyStereoPanning(aInput, aOutput, alignedComputedGain, + &alignedComputedGain[WEBAUDIO_BLOCK_SIZE], onLeft); + } + } + + virtual size_t SizeOfIncludingThis( + MallocSizeOf aMallocSizeOf) const override { + return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf); + } + + RefPtr<AudioNodeTrack> mDestination; + AudioParamTimeline mPan; +}; + +StereoPannerNode::StereoPannerNode(AudioContext* aContext) + : AudioNode(aContext, 2, ChannelCountMode::Clamped_max, + ChannelInterpretation::Speakers) { + mPan = + CreateAudioParam(StereoPannerNodeEngine::PAN, u"pan"_ns, 0.f, -1.f, 1.f); + StereoPannerNodeEngine* engine = + new StereoPannerNodeEngine(this, aContext->Destination()); + mTrack = AudioNodeTrack::Create( + aContext, engine, AudioNodeTrack::NO_TRACK_FLAGS, aContext->Graph()); +} + +/* static */ +already_AddRefed<StereoPannerNode> StereoPannerNode::Create( + AudioContext& aAudioContext, const StereoPannerOptions& aOptions, + ErrorResult& aRv) { + RefPtr<StereoPannerNode> audioNode = new StereoPannerNode(&aAudioContext); + + audioNode->Initialize(aOptions, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + audioNode->Pan()->SetInitialValue(aOptions.mPan); + return audioNode.forget(); +} + +size_t StereoPannerNode::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const { + size_t amount = AudioNode::SizeOfExcludingThis(aMallocSizeOf); + amount += mPan->SizeOfIncludingThis(aMallocSizeOf); + return amount; +} + +size_t StereoPannerNode::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const { + return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf); +} + +JSObject* StereoPannerNode::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return StereoPannerNode_Binding::Wrap(aCx, this, aGivenProto); +} + +} // namespace mozilla::dom diff --git a/dom/media/webaudio/StereoPannerNode.h b/dom/media/webaudio/StereoPannerNode.h new file mode 100644 index 0000000000..9dc7370fd1 --- /dev/null +++ b/dom/media/webaudio/StereoPannerNode.h @@ -0,0 +1,74 @@ +/* -*- 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/. */ + +#ifndef StereoPannerNode_h_ +#define StereoPannerNode_h_ + +#include "AudioNode.h" +#include "nsPrintfCString.h" +#include "mozilla/RefCounted.h" +#include "mozilla/dom/StereoPannerNodeBinding.h" + +namespace mozilla::dom { + +class AudioContext; +struct StereoPannerOptions; + +class StereoPannerNode final : public AudioNode { + public: + static already_AddRefed<StereoPannerNode> Create( + AudioContext& aAudioContext, const StereoPannerOptions& aOptions, + ErrorResult& aRv); + + MOZ_DECLARE_REFCOUNTED_TYPENAME(StereoPannerNode) + + static already_AddRefed<StereoPannerNode> Constructor( + const GlobalObject& aGlobal, AudioContext& aAudioContext, + const StereoPannerOptions& aOptions, ErrorResult& aRv) { + return Create(aAudioContext, aOptions, aRv); + } + + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + virtual void SetChannelCount(uint32_t aChannelCount, + ErrorResult& aRv) override { + if (aChannelCount > 2) { + aRv.ThrowNotSupportedError( + nsPrintfCString("%u is greater than 2", aChannelCount)); + return; + } + AudioNode::SetChannelCount(aChannelCount, aRv); + } + virtual void SetChannelCountModeValue(ChannelCountMode aMode, + ErrorResult& aRv) override { + if (aMode == ChannelCountMode::Max) { + aRv.ThrowNotSupportedError("Cannot set channel count mode to \"max\""); + return; + } + AudioNode::SetChannelCountModeValue(aMode, aRv); + } + + AudioParam* Pan() const { return mPan; } + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(StereoPannerNode, AudioNode) + + virtual const char* NodeType() const override { return "StereoPannerNode"; } + + virtual size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override; + virtual size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override; + + private: + explicit StereoPannerNode(AudioContext* aContext); + ~StereoPannerNode() = default; + + RefPtr<AudioParam> mPan; +}; + +} // namespace mozilla::dom + +#endif diff --git a/dom/media/webaudio/ThreeDPoint.cpp b/dom/media/webaudio/ThreeDPoint.cpp new file mode 100644 index 0000000000..d54d03e24c --- /dev/null +++ b/dom/media/webaudio/ThreeDPoint.cpp @@ -0,0 +1,38 @@ +/* -*- 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/. */ + +/** + * Other similar methods can be added if needed. + */ + +#include "ThreeDPoint.h" +#include "WebAudioUtils.h" + +namespace mozilla::dom { + +bool ThreeDPoint::FuzzyEqual(const ThreeDPoint& other) { + return WebAudioUtils::FuzzyEqual(x, other.x) && + WebAudioUtils::FuzzyEqual(y, other.y) && + WebAudioUtils::FuzzyEqual(z, other.z); +} + +ThreeDPoint operator-(const ThreeDPoint& lhs, const ThreeDPoint& rhs) { + return ThreeDPoint(lhs.x - rhs.x, lhs.y - rhs.y, lhs.z - rhs.z); +} + +ThreeDPoint operator*(const ThreeDPoint& lhs, const ThreeDPoint& rhs) { + return ThreeDPoint(lhs.x * rhs.x, lhs.y * rhs.y, lhs.z * rhs.z); +} + +ThreeDPoint operator*(const ThreeDPoint& lhs, const double rhs) { + return ThreeDPoint(lhs.x * rhs, lhs.y * rhs, lhs.z * rhs); +} + +bool operator==(const ThreeDPoint& lhs, const ThreeDPoint& rhs) { + return lhs.x == rhs.x && lhs.y == rhs.y && lhs.z == rhs.z; +} + +} // namespace mozilla::dom diff --git a/dom/media/webaudio/ThreeDPoint.h b/dom/media/webaudio/ThreeDPoint.h new file mode 100644 index 0000000000..4e2c7358f8 --- /dev/null +++ b/dom/media/webaudio/ThreeDPoint.h @@ -0,0 +1,68 @@ +/* -*- 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/. */ + +#ifndef ThreeDPoint_h_ +#define ThreeDPoint_h_ + +#include <cmath> +#include <algorithm> + +namespace mozilla::dom { + +struct ThreeDPoint final { + ThreeDPoint() : x(0.), y(0.), z(0.) {} + ThreeDPoint(double aX, double aY, double aZ) : x(aX), y(aY), z(aZ) {} + + double Magnitude() const { return sqrt(x * x + y * y + z * z); } + + void Normalize() { + // Zero vectors cannot be normalized. For our purpose, normalizing a zero + // vector results in a zero vector. + if (IsZero()) { + return; + } + // Normalize with the maximum norm first to avoid overflow and underflow. + double invMax = 1 / MaxNorm(); + x *= invMax; + y *= invMax; + z *= invMax; + + double invDistance = 1 / Magnitude(); + x *= invDistance; + y *= invDistance; + z *= invDistance; + } + + ThreeDPoint CrossProduct(const ThreeDPoint& rhs) const { + return ThreeDPoint(y * rhs.z - z * rhs.y, z * rhs.x - x * rhs.z, + x * rhs.y - y * rhs.x); + } + + double DotProduct(const ThreeDPoint& rhs) { + return x * rhs.x + y * rhs.y + z * rhs.z; + } + + bool IsZero() const { return x == 0 && y == 0 && z == 0; } + + // For comparing two vectors of close to unit magnitude. + bool FuzzyEqual(const ThreeDPoint& other); + + double x, y, z; + + private: + double MaxNorm() const { + return std::max(fabs(x), std::max(fabs(y), fabs(z))); + } +}; + +ThreeDPoint operator-(const ThreeDPoint& lhs, const ThreeDPoint& rhs); +ThreeDPoint operator*(const ThreeDPoint& lhs, const ThreeDPoint& rhs); +ThreeDPoint operator*(const ThreeDPoint& lhs, const double rhs); +bool operator==(const ThreeDPoint& lhs, const ThreeDPoint& rhs); + +} // namespace mozilla::dom + +#endif diff --git a/dom/media/webaudio/WaveShaperNode.cpp b/dom/media/webaudio/WaveShaperNode.cpp new file mode 100644 index 0000000000..f01b613039 --- /dev/null +++ b/dom/media/webaudio/WaveShaperNode.cpp @@ -0,0 +1,389 @@ +/* -*- 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 "WaveShaperNode.h" +#include "mozilla/dom/WaveShaperNodeBinding.h" +#include "AlignmentUtils.h" +#include "AudioNode.h" +#include "AudioNodeEngine.h" +#include "AudioNodeTrack.h" +#include "mozilla/PodOperations.h" +#include "Tracing.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_CLASS(WaveShaperNode) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(WaveShaperNode, AudioNode) + NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(WaveShaperNode, AudioNode) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(WaveShaperNode) + NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(WaveShaperNode) +NS_INTERFACE_MAP_END_INHERITING(AudioNode) + +NS_IMPL_ADDREF_INHERITED(WaveShaperNode, AudioNode) +NS_IMPL_RELEASE_INHERITED(WaveShaperNode, AudioNode) + +static uint32_t ValueOf(OverSampleType aType) { + switch (aType) { + case OverSampleType::None: + return 1; + case OverSampleType::_2x: + return 2; + case OverSampleType::_4x: + return 4; + default: + MOZ_ASSERT_UNREACHABLE("We should never reach here"); + return 1; + } +} + +class Resampler final { + public: + Resampler() + : mType(OverSampleType::None), + mUpSampler(nullptr), + mDownSampler(nullptr), + mChannels(0), + mSampleRate(0) {} + + ~Resampler() { Destroy(); } + + void Reset(uint32_t aChannels, TrackRate aSampleRate, OverSampleType aType) { + if (aChannels == mChannels && aSampleRate == mSampleRate && + aType == mType) { + return; + } + + mChannels = aChannels; + mSampleRate = aSampleRate; + mType = aType; + + Destroy(); + + if (aType == OverSampleType::None) { + mBuffer.Clear(); + return; + } + + mUpSampler = speex_resampler_init(aChannels, aSampleRate, + aSampleRate * ValueOf(aType), + SPEEX_RESAMPLER_QUALITY_MIN, nullptr); + mDownSampler = + speex_resampler_init(aChannels, aSampleRate * ValueOf(aType), + aSampleRate, SPEEX_RESAMPLER_QUALITY_MIN, nullptr); + mBuffer.SetLength(WEBAUDIO_BLOCK_SIZE * ValueOf(aType)); + } + + float* UpSample(uint32_t aChannel, const float* aInputData, + uint32_t aBlocks) { + uint32_t inSamples = WEBAUDIO_BLOCK_SIZE; + uint32_t outSamples = WEBAUDIO_BLOCK_SIZE * aBlocks; + float* outputData = mBuffer.Elements(); + + MOZ_ASSERT(mBuffer.Length() == outSamples); + + WebAudioUtils::SpeexResamplerProcess(mUpSampler, aChannel, aInputData, + &inSamples, outputData, &outSamples); + + MOZ_ASSERT(inSamples == WEBAUDIO_BLOCK_SIZE && + outSamples == WEBAUDIO_BLOCK_SIZE * aBlocks); + + return outputData; + } + + void DownSample(uint32_t aChannel, float* aOutputData, uint32_t aBlocks) { + uint32_t inSamples = WEBAUDIO_BLOCK_SIZE * aBlocks; + uint32_t outSamples = WEBAUDIO_BLOCK_SIZE; + const float* inputData = mBuffer.Elements(); + + MOZ_ASSERT(mBuffer.Length() == inSamples); + + WebAudioUtils::SpeexResamplerProcess(mDownSampler, aChannel, inputData, + &inSamples, aOutputData, &outSamples); + + MOZ_ASSERT(inSamples == WEBAUDIO_BLOCK_SIZE * aBlocks && + outSamples == WEBAUDIO_BLOCK_SIZE); + } + + size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const { + size_t amount = 0; + // Future: properly measure speex memory + amount += aMallocSizeOf(mUpSampler); + amount += aMallocSizeOf(mDownSampler); + amount += mBuffer.ShallowSizeOfExcludingThis(aMallocSizeOf); + return amount; + } + + private: + void Destroy() { + if (mUpSampler) { + speex_resampler_destroy(mUpSampler); + mUpSampler = nullptr; + } + if (mDownSampler) { + speex_resampler_destroy(mDownSampler); + mDownSampler = nullptr; + } + } + + private: + OverSampleType mType; + SpeexResamplerState* mUpSampler; + SpeexResamplerState* mDownSampler; + uint32_t mChannels; + TrackRate mSampleRate; + nsTArray<float> mBuffer; +}; + +class WaveShaperNodeEngine final : public AudioNodeEngine { + public: + explicit WaveShaperNodeEngine(AudioNode* aNode) + : AudioNodeEngine(aNode), mType(OverSampleType::None) {} + + enum Parameters { TYPE }; + + void SetRawArrayData(nsTArray<float>&& aCurve) override { + mCurve = std::move(aCurve); + } + + void SetInt32Parameter(uint32_t aIndex, int32_t aValue) override { + switch (aIndex) { + case TYPE: + mType = static_cast<OverSampleType>(aValue); + break; + default: + NS_ERROR("Bad WaveShaperNode Int32Parameter"); + } + } + + template <uint32_t blocks> + void ProcessCurve(const float* aInputBuffer, float* aOutputBuffer) { + for (uint32_t j = 0; j < WEBAUDIO_BLOCK_SIZE * blocks; ++j) { + // Index into the curve array based on the amplitude of the + // incoming signal by using an amplitude range of [-1, 1] and + // performing a linear interpolation of the neighbor values. + float index = (mCurve.Length() - 1) * (aInputBuffer[j] + 1.0f) / 2.0f; + if (index < 0.0f) { + aOutputBuffer[j] = mCurve[0]; + } else { + int32_t indexLower = index; + if (static_cast<uint32_t>(indexLower) >= mCurve.Length() - 1) { + aOutputBuffer[j] = mCurve[mCurve.Length() - 1]; + } else { + uint32_t indexHigher = indexLower + 1; + float interpolationFactor = index - indexLower; + aOutputBuffer[j] = (1.0f - interpolationFactor) * mCurve[indexLower] + + interpolationFactor * mCurve[indexHigher]; + } + } + } + } + + void ProcessBlock(AudioNodeTrack* aTrack, GraphTime aFrom, + const AudioBlock& aInput, AudioBlock* aOutput, + bool* aFinished) override { + TRACE("WaveShaperNodeEngine::ProcessBlock"); + + uint32_t channelCount = aInput.ChannelCount(); + if (!mCurve.Length()) { + // Optimize the case where we don't have a curve buffer + *aOutput = aInput; + return; + } + + // If the input is null, check to see if non-null output will be produced + bool nullInput = false; + if (channelCount == 0) { + float index = (mCurve.Length() - 1) * 0.5; + uint32_t indexLower = index; + uint32_t indexHigher = indexLower + 1; + float interpolationFactor = index - indexLower; + if ((1.0f - interpolationFactor) * mCurve[indexLower] + + interpolationFactor * mCurve[indexHigher] == + 0.0) { + *aOutput = aInput; + return; + } + nullInput = true; + channelCount = 1; + } + + aOutput->AllocateChannels(channelCount); + for (uint32_t i = 0; i < channelCount; ++i) { + const float* inputSamples; + float scaledInput[WEBAUDIO_BLOCK_SIZE + 4]; + float* alignedScaledInput = ALIGNED16(scaledInput); + ASSERT_ALIGNED16(alignedScaledInput); + if (!nullInput) { + if (aInput.mVolume != 1.0f) { + AudioBlockCopyChannelWithScale( + static_cast<const float*>(aInput.mChannelData[i]), aInput.mVolume, + alignedScaledInput); + inputSamples = alignedScaledInput; + } else { + inputSamples = static_cast<const float*>(aInput.mChannelData[i]); + } + } else { + PodZero(alignedScaledInput, WEBAUDIO_BLOCK_SIZE); + inputSamples = alignedScaledInput; + } + float* outputBuffer = aOutput->ChannelFloatsForWrite(i); + float* sampleBuffer; + + switch (mType) { + case OverSampleType::None: + mResampler.Reset(channelCount, aTrack->mSampleRate, + OverSampleType::None); + ProcessCurve<1>(inputSamples, outputBuffer); + break; + case OverSampleType::_2x: + mResampler.Reset(channelCount, aTrack->mSampleRate, + OverSampleType::_2x); + sampleBuffer = mResampler.UpSample(i, inputSamples, 2); + ProcessCurve<2>(sampleBuffer, sampleBuffer); + mResampler.DownSample(i, outputBuffer, 2); + break; + case OverSampleType::_4x: + mResampler.Reset(channelCount, aTrack->mSampleRate, + OverSampleType::_4x); + sampleBuffer = mResampler.UpSample(i, inputSamples, 4); + ProcessCurve<4>(sampleBuffer, sampleBuffer); + mResampler.DownSample(i, outputBuffer, 4); + break; + default: + MOZ_ASSERT_UNREACHABLE("We should never reach here"); + } + } + } + + size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override { + size_t amount = AudioNodeEngine::SizeOfExcludingThis(aMallocSizeOf); + amount += mCurve.ShallowSizeOfExcludingThis(aMallocSizeOf); + amount += mResampler.SizeOfExcludingThis(aMallocSizeOf); + return amount; + } + + size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override { + return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf); + } + + private: + nsTArray<float> mCurve; + OverSampleType mType; + Resampler mResampler; +}; + +WaveShaperNode::WaveShaperNode(AudioContext* aContext) + : AudioNode(aContext, 2, ChannelCountMode::Max, + ChannelInterpretation::Speakers), + mType(OverSampleType::None) { + WaveShaperNodeEngine* engine = new WaveShaperNodeEngine(this); + mTrack = AudioNodeTrack::Create( + aContext, engine, AudioNodeTrack::NO_TRACK_FLAGS, aContext->Graph()); +} + +/* static */ +already_AddRefed<WaveShaperNode> WaveShaperNode::Create( + AudioContext& aAudioContext, const WaveShaperOptions& aOptions, + ErrorResult& aRv) { + RefPtr<WaveShaperNode> audioNode = new WaveShaperNode(&aAudioContext); + + audioNode->Initialize(aOptions, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + if (aOptions.mCurve.WasPassed()) { + audioNode->SetCurveInternal(aOptions.mCurve.Value(), aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + } + + audioNode->SetOversample(aOptions.mOversample); + return audioNode.forget(); +} + +JSObject* WaveShaperNode::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return WaveShaperNode_Binding::Wrap(aCx, this, aGivenProto); +} + +void WaveShaperNode::SetCurve(const Nullable<Float32Array>& aCurve, + ErrorResult& aRv) { + // Let's purge the cached value for the curve attribute. + WaveShaperNode_Binding::ClearCachedCurveValue(this); + + if (aCurve.IsNull()) { + CleanCurveInternal(); + return; + } + + nsTArray<float> curve; + if (!aCurve.Value().AppendDataTo(curve)) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + SetCurveInternal(curve, aRv); +} + +void WaveShaperNode::SetCurveInternal(const nsTArray<float>& aCurve, + ErrorResult& aRv) { + if (aCurve.Length() < 2) { + aRv.ThrowInvalidStateError("Must have at least two entries"); + return; + } + + mCurve = aCurve.Clone(); + SendCurveToTrack(); +} + +void WaveShaperNode::CleanCurveInternal() { + mCurve.Clear(); + SendCurveToTrack(); +} + +void WaveShaperNode::SendCurveToTrack() { + AudioNodeTrack* ns = mTrack; + MOZ_ASSERT(ns, "Why don't we have a track here?"); + + nsTArray<float> copyCurve(mCurve.Clone()); + ns->SetRawArrayData(std::move(copyCurve)); +} + +void WaveShaperNode::GetCurve(JSContext* aCx, + JS::MutableHandle<JSObject*> aRetval, + ErrorResult& aError) { + // Let's return a null value if the list is empty. + if (mCurve.IsEmpty()) { + aRetval.set(nullptr); + return; + } + + MOZ_ASSERT(mCurve.Length() >= 2); + JSObject* curve = Float32Array::Create(aCx, this, mCurve, aError); + if (aError.Failed()) { + return; + } + aRetval.set(curve); +} + +void WaveShaperNode::SetOversample(OverSampleType aType) { + mType = aType; + SendInt32ParameterToTrack(WaveShaperNodeEngine::TYPE, + static_cast<int32_t>(aType)); +} + +} // namespace mozilla::dom diff --git a/dom/media/webaudio/WaveShaperNode.h b/dom/media/webaudio/WaveShaperNode.h new file mode 100644 index 0000000000..5c91960af9 --- /dev/null +++ b/dom/media/webaudio/WaveShaperNode.h @@ -0,0 +1,72 @@ +/* -*- 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/. */ + +#ifndef WaveShaperNode_h_ +#define WaveShaperNode_h_ + +#include "AudioNode.h" +#include "mozilla/dom/WaveShaperNodeBinding.h" +#include "mozilla/dom/TypedArray.h" + +namespace mozilla::dom { + +class AudioContext; +struct WaveShaperOptions; + +class WaveShaperNode final : public AudioNode { + public: + static already_AddRefed<WaveShaperNode> Create( + AudioContext& aAudioContext, const WaveShaperOptions& aOptions, + ErrorResult& aRv); + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(WaveShaperNode, + AudioNode) + + static already_AddRefed<WaveShaperNode> Constructor( + const GlobalObject& aGlobal, AudioContext& aAudioContext, + const WaveShaperOptions& aOptions, ErrorResult& aRv) { + return Create(aAudioContext, aOptions, aRv); + } + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + void GetCurve(JSContext* aCx, JS::MutableHandle<JSObject*> aRetval, + ErrorResult& aRv); + void SetCurve(const Nullable<Float32Array>& aData, ErrorResult& aRv); + + OverSampleType Oversample() const { return mType; } + void SetOversample(OverSampleType aType); + + size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override { + // Possibly track in the future: + // - mCurve + return AudioNode::SizeOfExcludingThis(aMallocSizeOf); + } + + const char* NodeType() const override { return "WaveShaperNode"; } + + size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override { + return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf); + } + + private: + explicit WaveShaperNode(AudioContext* aContext); + ~WaveShaperNode() = default; + + void SetCurveInternal(const nsTArray<float>& aCurve, ErrorResult& aRv); + void CleanCurveInternal(); + + void SendCurveToTrack(); + + nsTArray<float> mCurve; + OverSampleType mType; +}; + +} // namespace mozilla::dom + +#endif diff --git a/dom/media/webaudio/WebAudioUtils.cpp b/dom/media/webaudio/WebAudioUtils.cpp new file mode 100644 index 0000000000..a1338650e0 --- /dev/null +++ b/dom/media/webaudio/WebAudioUtils.cpp @@ -0,0 +1,114 @@ +/* -*- 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 "WebAudioUtils.h" +#include "blink/HRTFDatabaseLoader.h" + +#include "nsComponentManagerUtils.h" +#include "nsContentUtils.h" +#include "nsIConsoleService.h" +#include "nsIScriptError.h" +#include "nsJSUtils.h" +#include "nsServiceManagerUtils.h" + +#include "mozilla/SchedulerGroup.h" + +namespace mozilla { + +LazyLogModule gWebAudioAPILog("WebAudioAPI"); + +namespace dom { + +void WebAudioUtils::Shutdown() { WebCore::HRTFDatabaseLoader::shutdown(); } + +int WebAudioUtils::SpeexResamplerProcess(SpeexResamplerState* aResampler, + uint32_t aChannel, const float* aIn, + uint32_t* aInLen, float* aOut, + uint32_t* aOutLen) { + return speex_resampler_process_float(aResampler, aChannel, aIn, aInLen, aOut, + aOutLen); +} + +int WebAudioUtils::SpeexResamplerProcess(SpeexResamplerState* aResampler, + uint32_t aChannel, const int16_t* aIn, + uint32_t* aInLen, float* aOut, + uint32_t* aOutLen) { + AutoTArray<AudioDataValue, WEBAUDIO_BLOCK_SIZE * 4> tmp; + tmp.SetLength(*aInLen); + ConvertAudioSamples(aIn, tmp.Elements(), *aInLen); + int result = speex_resampler_process_float( + aResampler, aChannel, tmp.Elements(), aInLen, aOut, aOutLen); + return result; +} + +int WebAudioUtils::SpeexResamplerProcess(SpeexResamplerState* aResampler, + uint32_t aChannel, const int16_t* aIn, + uint32_t* aInLen, int16_t* aOut, + uint32_t* aOutLen) { + AutoTArray<AudioDataValue, WEBAUDIO_BLOCK_SIZE * 4> tmp1; + AutoTArray<AudioDataValue, WEBAUDIO_BLOCK_SIZE * 4> tmp2; + tmp1.SetLength(*aInLen); + tmp2.SetLength(*aOutLen); + ConvertAudioSamples(aIn, tmp1.Elements(), *aInLen); + int result = speex_resampler_process_float( + aResampler, aChannel, tmp1.Elements(), aInLen, tmp2.Elements(), aOutLen); + ConvertAudioSamples(tmp2.Elements(), aOut, *aOutLen); + return result; +} + +void WebAudioUtils::LogToDeveloperConsole(uint64_t aWindowID, + const char* aKey) { + // This implementation is derived from dom/media/VideoUtils.cpp, but we + // use a windowID so that the message is delivered to the developer console. + // It is similar to ContentUtils::ReportToConsole, but also works off main + // thread. + if (!NS_IsMainThread()) { + nsCOMPtr<nsIRunnable> task = NS_NewRunnableFunction( + "dom::WebAudioUtils::LogToDeveloperConsole", + [aWindowID, aKey] { LogToDeveloperConsole(aWindowID, aKey); }); + SchedulerGroup::Dispatch(task.forget()); + return; + } + + nsCOMPtr<nsIConsoleService> console( + do_GetService("@mozilla.org/consoleservice;1")); + if (!console) { + NS_WARNING("Failed to log message to console."); + return; + } + + nsAutoString spec; + uint32_t aLineNumber = 0, aColumnNumber = 1; + JSContext* cx = nsContentUtils::GetCurrentJSContext(); + if (cx) { + nsJSUtils::GetCallingLocation(cx, spec, &aLineNumber, &aColumnNumber); + } + + nsresult rv; + nsCOMPtr<nsIScriptError> errorObject = + do_CreateInstance(NS_SCRIPTERROR_CONTRACTID, &rv); + if (!errorObject) { + NS_WARNING("Failed to log message to console."); + return; + } + + nsAutoString result; + rv = nsContentUtils::GetLocalizedString(nsContentUtils::eDOM_PROPERTIES, aKey, + result); + + if (NS_FAILED(rv)) { + NS_WARNING("Failed to log message to console."); + return; + } + + errorObject->InitWithWindowID(result, spec, u""_ns, aLineNumber, + aColumnNumber, nsIScriptError::warningFlag, + "Web Audio", aWindowID); + console->LogMessage(errorObject); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/media/webaudio/WebAudioUtils.h b/dom/media/webaudio/WebAudioUtils.h new file mode 100644 index 0000000000..08e4974771 --- /dev/null +++ b/dom/media/webaudio/WebAudioUtils.h @@ -0,0 +1,184 @@ +/* -*- 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/. */ + +#ifndef WebAudioUtils_h_ +#define WebAudioUtils_h_ + +#include <cmath> +#include <limits> +#include <type_traits> +#include "fdlibm.h" +#include "mozilla/FloatingPoint.h" +#include "mozilla/Logging.h" +#include "MediaSegment.h" + +// Forward declaration +typedef struct SpeexResamplerState_ SpeexResamplerState; + +namespace mozilla { + +extern LazyLogModule gWebAudioAPILog; +#define WEB_AUDIO_API_LOG(...) \ + MOZ_LOG(gWebAudioAPILog, LogLevel::Debug, (__VA_ARGS__)) + +namespace dom::WebAudioUtils { + +// 32 is the minimum required by the spec for createBuffer() and +// createScriptProcessor() and matches what is used by Blink. The limit +// protects against large memory allocations. +const size_t MaxChannelCount = 32; +// AudioContext::CreateBuffer() "must support sample-rates in at least the +// range 22050 to 96000." +const uint32_t MinSampleRate = 8000; +const uint32_t MaxSampleRate = 192000; + +inline bool FuzzyEqual(float v1, float v2) { return fabsf(v1 - v2) < 1e-7f; } +inline bool FuzzyEqual(double v1, double v2) { return fabs(v1 - v2) < 1e-7; } + +/** + * Converts a linear value to decibels. Returns aMinDecibels if the linear + * value is 0. + */ +inline float ConvertLinearToDecibels(float aLinearValue, float aMinDecibels) { + return aLinearValue ? 20.0f * fdlibm_log10f(aLinearValue) : aMinDecibels; +} + +/** + * Converts a decibel value to a linear value. + */ +inline float ConvertDecibelsToLinear(float aDecibels) { + return fdlibm_powf(10.0f, 0.05f * aDecibels); +} + +inline void FixNaN(double& aDouble) { + if (std::isnan(aDouble) || std::isinf(aDouble)) { + aDouble = 0.0; + } +} + +inline double DiscreteTimeConstantForSampleRate(double timeConstant, + double sampleRate) { + return 1.0 - fdlibm_exp(-1.0 / (sampleRate * timeConstant)); +} + +inline bool IsTimeValid(double aTime) { + return aTime >= 0 && aTime <= (MEDIA_TIME_MAX >> TRACK_RATE_MAX_BITS); +} + +/** + * Converts a floating point value to an integral type in a safe and + * platform agnostic way. The following program demonstrates the kinds + * of ways things can go wrong depending on the CPU architecture you're + * compiling for: + * + * #include <stdio.h> + * volatile float r; + * int main() + * { + * unsigned int q; + * r = 1e100; + * q = r; + * printf("%f %d\n", r, q); + * r = -1e100; + * q = r; + * printf("%f %d\n", r, q); + * r = 1e15; + * q = r; + * printf("%f %x\n", r, q); + * r = 0/0.; + * q = r; + * printf("%f %d\n", r, q); + * } + * + * This program, when compiled for unsigned int, generates the following + * results depending on the architecture: + * + * x86 and x86-64 + * --- + * inf 0 + * -inf 0 + * 999999995904.000000 -727384064 d4a50000 + * nan 0 + * + * ARM + * --- + * inf -1 + * -inf 0 + * 999999995904.000000 -1 + * nan 0 + * + * When compiled for int, this program generates the following results: + * + * x86 and x86-64 + * --- + * inf -2147483648 + * -inf -2147483648 + * 999999995904.000000 -2147483648 + * nan -2147483648 + * + * ARM + * --- + * inf 2147483647 + * -inf -2147483648 + * 999999995904.000000 2147483647 + * nan 0 + * + * Note that the caller is responsible to make sure that the value + * passed to this function is not a NaN. This function will abort if + * it sees a NaN. + */ +template <typename IntType, typename FloatType> +IntType TruncateFloatToInt(FloatType f) { + using std::numeric_limits; + static_assert(std::is_integral_v<IntType> == true, + "IntType must be an integral type"); + static_assert(std::is_floating_point_v<FloatType> == true, + "FloatType must be a floating point type"); + + if (std::isnan(f)) { + // It is the responsibility of the caller to deal with NaN values. + // If we ever get to this point, we have a serious bug to fix. + MOZ_CRASH("We should never see a NaN here"); + } + + // If the floating point value is outside of the range of maximum + // integral value for this type, just clamp to the maximum value. + // The equality case must also return max() due to loss of precision when + // converting max() to float. + if (f >= FloatType(numeric_limits<IntType>::max())) { + return numeric_limits<IntType>::max(); + } + + if (f <= FloatType(numeric_limits<IntType>::min())) { + // If the floating point value is outside of the range of minimum + // integral value for this type, just clamp to the minimum value. + return numeric_limits<IntType>::min(); + } + + // Otherwise, this conversion must be well defined. + return IntType(f); +} + +void Shutdown(); + +int SpeexResamplerProcess(SpeexResamplerState* aResampler, uint32_t aChannel, + const float* aIn, uint32_t* aInLen, float* aOut, + uint32_t* aOutLen); + +int SpeexResamplerProcess(SpeexResamplerState* aResampler, uint32_t aChannel, + const int16_t* aIn, uint32_t* aInLen, float* aOut, + uint32_t* aOutLen); + +int SpeexResamplerProcess(SpeexResamplerState* aResampler, uint32_t aChannel, + const int16_t* aIn, uint32_t* aInLen, int16_t* aOut, + uint32_t* aOutLen); + +void LogToDeveloperConsole(uint64_t aWindowID, const char* aKey); + +} // namespace dom::WebAudioUtils +} // namespace mozilla + +#endif diff --git a/dom/media/webaudio/blink/Biquad.cpp b/dom/media/webaudio/blink/Biquad.cpp new file mode 100644 index 0000000000..f8ce08f9bb --- /dev/null +++ b/dom/media/webaudio/blink/Biquad.cpp @@ -0,0 +1,442 @@ +/* + * Copyright (C) 2010 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. + * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of + * its contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY APPLE 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 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 "Biquad.h" + +#include "DenormalDisabler.h" + +#include <float.h> +#include <algorithm> +#include <math.h> + +#include "fdlibm.h" + +namespace WebCore { + +Biquad::Biquad() { + // Initialize as pass-thru (straight-wire, no filter effect) + setNormalizedCoefficients(1, 0, 0, 1, 0, 0); + + reset(); // clear filter memory +} + +Biquad::~Biquad() = default; + +void Biquad::process(const float* sourceP, float* destP, + size_t framesToProcess) { + // Create local copies of member variables + double x1 = m_x1; + double x2 = m_x2; + double y1 = m_y1; + double y2 = m_y2; + + double b0 = m_b0; + double b1 = m_b1; + double b2 = m_b2; + double a1 = m_a1; + double a2 = m_a2; + + for (size_t i = 0; i < framesToProcess; ++i) { + // FIXME: this can be optimized by pipelining the multiply adds... + double x = sourceP[i]; + double y = b0 * x + b1 * x1 + b2 * x2 - a1 * y1 - a2 * y2; + + destP[i] = y; + + // Update state variables + x2 = x1; + x1 = x; + y2 = y1; + y1 = y; + } + + // Avoid introducing a stream of subnormals when input is silent and the + // tail approaches zero. + if (x1 == 0.0 && x2 == 0.0 && (y1 != 0.0 || y2 != 0.0) && + fabs(y1) < FLT_MIN && fabs(y2) < FLT_MIN) { + // Flush future values to zero (until there is new input). + y1 = y2 = 0.0; +// Flush calculated values. +#ifndef HAVE_DENORMAL + for (int i = framesToProcess; i-- && fabsf(destP[i]) < FLT_MIN;) { + destP[i] = 0.0f; + } +#endif + } + // Local variables back to member. + m_x1 = x1; + m_x2 = x2; + m_y1 = y1; + m_y2 = y2; +} + +void Biquad::reset() { m_x1 = m_x2 = m_y1 = m_y2 = 0; } + +void Biquad::setLowpassParams(double cutoff, double resonance) { + // Limit cutoff to 0 to 1. + cutoff = std::max(0.0, std::min(cutoff, 1.0)); + + if (cutoff == 1) { + // When cutoff is 1, the z-transform is 1. + setNormalizedCoefficients(1, 0, 0, 1, 0, 0); + } else if (cutoff > 0) { + // Compute biquad coefficients for lowpass filter + double g = fdlibm_pow(10.0, -0.05 * resonance); + double w0 = M_PI * cutoff; + double cos_w0 = fdlibm_cos(w0); + double alpha = 0.5 * fdlibm_sin(w0) * g; + + double b1 = 1.0 - cos_w0; + double b0 = 0.5 * b1; + double b2 = b0; + double a0 = 1.0 + alpha; + double a1 = -2.0 * cos_w0; + double a2 = 1.0 - alpha; + + setNormalizedCoefficients(b0, b1, b2, a0, a1, a2); + } else { + // When cutoff is zero, nothing gets through the filter, so set + // coefficients up correctly. + setNormalizedCoefficients(0, 0, 0, 1, 0, 0); + } +} + +void Biquad::setHighpassParams(double cutoff, double resonance) { + // Limit cutoff to 0 to 1. + cutoff = std::max(0.0, std::min(cutoff, 1.0)); + + if (cutoff == 1) { + // The z-transform is 0. + setNormalizedCoefficients(0, 0, 0, 1, 0, 0); + } else if (cutoff > 0) { + // Compute biquad coefficients for highpass filter + double g = fdlibm_pow(10.0, -0.05 * resonance); + double w0 = M_PI * cutoff; + double cos_w0 = fdlibm_cos(w0); + double alpha = 0.5 * fdlibm_sin(w0) * g; + + double b1 = -1.0 - cos_w0; + double b0 = -0.5 * b1; + double b2 = b0; + double a0 = 1.0 + alpha; + double a1 = -2.0 * cos_w0; + double a2 = 1.0 - alpha; + + setNormalizedCoefficients(b0, b1, b2, a0, a1, a2); + } else { + // When cutoff is zero, we need to be careful because the above + // gives a quadratic divided by the same quadratic, with poles + // and zeros on the unit circle in the same place. When cutoff + // is zero, the z-transform is 1. + setNormalizedCoefficients(1, 0, 0, 1, 0, 0); + } +} + +void Biquad::setNormalizedCoefficients(double b0, double b1, double b2, + double a0, double a1, double a2) { + double a0Inverse = 1 / a0; + + m_b0 = b0 * a0Inverse; + m_b1 = b1 * a0Inverse; + m_b2 = b2 * a0Inverse; + m_a1 = a1 * a0Inverse; + m_a2 = a2 * a0Inverse; +} + +void Biquad::setLowShelfParams(double frequency, double dbGain) { + // Clip frequencies to between 0 and 1, inclusive. + frequency = std::max(0.0, std::min(frequency, 1.0)); + + double A = fdlibm_pow(10.0, dbGain / 40); + + if (frequency == 1) { + // The z-transform is a constant gain. + setNormalizedCoefficients(A * A, 0, 0, 1, 0, 0); + } else if (frequency > 0) { + double w0 = M_PI * frequency; + double S = 1; // filter slope (1 is max value) + double alpha = 0.5 * fdlibm_sin(w0) * sqrt((A + 1 / A) * (1 / S - 1) + 2); + double k = fdlibm_cos(w0); + double k2 = 2 * sqrt(A) * alpha; + double aPlusOne = A + 1; + double aMinusOne = A - 1; + + double b0 = A * (aPlusOne - aMinusOne * k + k2); + double b1 = 2 * A * (aMinusOne - aPlusOne * k); + double b2 = A * (aPlusOne - aMinusOne * k - k2); + double a0 = aPlusOne + aMinusOne * k + k2; + double a1 = -2 * (aMinusOne + aPlusOne * k); + double a2 = aPlusOne + aMinusOne * k - k2; + + setNormalizedCoefficients(b0, b1, b2, a0, a1, a2); + } else { + // When frequency is 0, the z-transform is 1. + setNormalizedCoefficients(1, 0, 0, 1, 0, 0); + } +} + +void Biquad::setHighShelfParams(double frequency, double dbGain) { + // Clip frequencies to between 0 and 1, inclusive. + frequency = std::max(0.0, std::min(frequency, 1.0)); + + double A = fdlibm_pow(10.0, dbGain / 40); + + if (frequency == 1) { + // The z-transform is 1. + setNormalizedCoefficients(1, 0, 0, 1, 0, 0); + } else if (frequency > 0) { + double w0 = M_PI * frequency; + double S = 1; // filter slope (1 is max value) + double alpha = 0.5 * fdlibm_sin(w0) * sqrt((A + 1 / A) * (1 / S - 1) + 2); + double k = fdlibm_cos(w0); + double k2 = 2 * sqrt(A) * alpha; + double aPlusOne = A + 1; + double aMinusOne = A - 1; + + double b0 = A * (aPlusOne + aMinusOne * k + k2); + double b1 = -2 * A * (aMinusOne + aPlusOne * k); + double b2 = A * (aPlusOne + aMinusOne * k - k2); + double a0 = aPlusOne - aMinusOne * k + k2; + double a1 = 2 * (aMinusOne - aPlusOne * k); + double a2 = aPlusOne - aMinusOne * k - k2; + + setNormalizedCoefficients(b0, b1, b2, a0, a1, a2); + } else { + // When frequency = 0, the filter is just a gain, A^2. + setNormalizedCoefficients(A * A, 0, 0, 1, 0, 0); + } +} + +void Biquad::setPeakingParams(double frequency, double Q, double dbGain) { + // Clip frequencies to between 0 and 1, inclusive. + frequency = std::max(0.0, std::min(frequency, 1.0)); + + // Don't let Q go negative, which causes an unstable filter. + Q = std::max(0.0, Q); + + double A = fdlibm_pow(10.0, dbGain / 40); + + if (frequency > 0 && frequency < 1) { + if (Q > 0) { + double w0 = M_PI * frequency; + double alpha = fdlibm_sin(w0) / (2 * Q); + double k = fdlibm_cos(w0); + + double b0 = 1 + alpha * A; + double b1 = -2 * k; + double b2 = 1 - alpha * A; + double a0 = 1 + alpha / A; + double a1 = -2 * k; + double a2 = 1 - alpha / A; + + setNormalizedCoefficients(b0, b1, b2, a0, a1, a2); + } else { + // When Q = 0, the above formulas have problems. If we look at + // the z-transform, we can see that the limit as Q->0 is A^2, so + // set the filter that way. + setNormalizedCoefficients(A * A, 0, 0, 1, 0, 0); + } + } else { + // When frequency is 0 or 1, the z-transform is 1. + setNormalizedCoefficients(1, 0, 0, 1, 0, 0); + } +} + +void Biquad::setAllpassParams(double frequency, double Q) { + // Clip frequencies to between 0 and 1, inclusive. + frequency = std::max(0.0, std::min(frequency, 1.0)); + + // Don't let Q go negative, which causes an unstable filter. + Q = std::max(0.0, Q); + + if (frequency > 0 && frequency < 1) { + if (Q > 0) { + double w0 = M_PI * frequency; + double alpha = fdlibm_sin(w0) / (2 * Q); + double k = fdlibm_cos(w0); + + double b0 = 1 - alpha; + double b1 = -2 * k; + double b2 = 1 + alpha; + double a0 = 1 + alpha; + double a1 = -2 * k; + double a2 = 1 - alpha; + + setNormalizedCoefficients(b0, b1, b2, a0, a1, a2); + } else { + // When Q = 0, the above formulas have problems. If we look at + // the z-transform, we can see that the limit as Q->0 is -1, so + // set the filter that way. + setNormalizedCoefficients(-1, 0, 0, 1, 0, 0); + } + } else { + // When frequency is 0 or 1, the z-transform is 1. + setNormalizedCoefficients(1, 0, 0, 1, 0, 0); + } +} + +void Biquad::setNotchParams(double frequency, double Q) { + // Clip frequencies to between 0 and 1, inclusive. + frequency = std::max(0.0, std::min(frequency, 1.0)); + + // Don't let Q go negative, which causes an unstable filter. + Q = std::max(0.0, Q); + + if (frequency > 0 && frequency < 1) { + if (Q > 0) { + double w0 = M_PI * frequency; + double alpha = fdlibm_sin(w0) / (2 * Q); + double k = fdlibm_cos(w0); + + double b0 = 1; + double b1 = -2 * k; + double b2 = 1; + double a0 = 1 + alpha; + double a1 = -2 * k; + double a2 = 1 - alpha; + + setNormalizedCoefficients(b0, b1, b2, a0, a1, a2); + } else { + // When Q = 0, the above formulas have problems. If we look at + // the z-transform, we can see that the limit as Q->0 is 0, so + // set the filter that way. + setNormalizedCoefficients(0, 0, 0, 1, 0, 0); + } + } else { + // When frequency is 0 or 1, the z-transform is 1. + setNormalizedCoefficients(1, 0, 0, 1, 0, 0); + } +} + +void Biquad::setBandpassParams(double frequency, double Q) { + // No negative frequencies allowed. + frequency = std::max(0.0, frequency); + + // Don't let Q go negative, which causes an unstable filter. + Q = std::max(0.0, Q); + + if (frequency > 0 && frequency < 1) { + double w0 = M_PI * frequency; + if (Q > 0) { + double alpha = fdlibm_sin(w0) / (2 * Q); + double k = fdlibm_cos(w0); + + double b0 = alpha; + double b1 = 0; + double b2 = -alpha; + double a0 = 1 + alpha; + double a1 = -2 * k; + double a2 = 1 - alpha; + + setNormalizedCoefficients(b0, b1, b2, a0, a1, a2); + } else { + // When Q = 0, the above formulas have problems. If we look at + // the z-transform, we can see that the limit as Q->0 is 1, so + // set the filter that way. + setNormalizedCoefficients(1, 0, 0, 1, 0, 0); + } + } else { + // When the cutoff is zero, the z-transform approaches 0, if Q + // > 0. When both Q and cutoff are zero, the z-transform is + // pretty much undefined. What should we do in this case? + // For now, just make the filter 0. When the cutoff is 1, the + // z-transform also approaches 0. + setNormalizedCoefficients(0, 0, 0, 1, 0, 0); + } +} + +void Biquad::setZeroPolePairs(const Complex& zero, const Complex& pole) { + double b0 = 1; + double b1 = -2 * zero.real(); + + double zeroMag = abs(zero); + double b2 = zeroMag * zeroMag; + + double a1 = -2 * pole.real(); + + double poleMag = abs(pole); + double a2 = poleMag * poleMag; + setNormalizedCoefficients(b0, b1, b2, 1, a1, a2); +} + +void Biquad::setAllpassPole(const Complex& pole) { + Complex zero = Complex(1, 0) / pole; + setZeroPolePairs(zero, pole); +} + +void Biquad::getFrequencyResponse(int nFrequencies, const float* frequency, + float* magResponse, float* phaseResponse) { + // Evaluate the Z-transform of the filter at given normalized + // frequency from 0 to 1. (1 corresponds to the Nyquist + // frequency.) + // + // The z-transform of the filter is + // + // H(z) = (b0 + b1*z^(-1) + b2*z^(-2))/(1 + a1*z^(-1) + a2*z^(-2)) + // + // Evaluate as + // + // b0 + (b1 + b2*z1)*z1 + // -------------------- + // 1 + (a1 + a2*z1)*z1 + // + // with z1 = 1/z and z = exp(j*pi*frequency). Hence z1 = exp(-j*pi*frequency) + + // Make local copies of the coefficients as a micro-optimization. + double b0 = m_b0; + double b1 = m_b1; + double b2 = m_b2; + double a1 = m_a1; + double a2 = m_a2; + + for (int k = 0; k < nFrequencies; ++k) { + double omega = -M_PI * frequency[k]; + Complex z = Complex(fdlibm_cos(omega), fdlibm_sin(omega)); + Complex numerator = b0 + (b1 + b2 * z) * z; + Complex denominator = Complex(1, 0) + (a1 + a2 * z) * z; + // Strangely enough, using complex division: + // e.g. Complex response = numerator / denominator; + // fails on our test machines, yielding infinities and NaNs, so we do + // things the long way here. + double n = norm(denominator); + double r = (real(numerator) * real(denominator) + + imag(numerator) * imag(denominator)) / + n; + double i = (imag(numerator) * real(denominator) - + real(numerator) * imag(denominator)) / + n; + std::complex<double> response = std::complex<double>(r, i); + + magResponse[k] = + static_cast<float>(fdlibm_hypot(real(response), imag(response))); + phaseResponse[k] = + static_cast<float>(fdlibm_atan2(imag(response), real(response))); + } +} + +} // namespace WebCore diff --git a/dom/media/webaudio/blink/Biquad.h b/dom/media/webaudio/blink/Biquad.h new file mode 100644 index 0000000000..d64ed72b50 --- /dev/null +++ b/dom/media/webaudio/blink/Biquad.h @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2010 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. + * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of + * its contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY APPLE 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 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. + */ + +#ifndef Biquad_h +#define Biquad_h + +#include <complex> + +namespace WebCore { + +typedef std::complex<double> Complex; + +// A basic biquad (two-zero / two-pole digital filter) +// +// It can be configured to a number of common and very useful filters: +// lowpass, highpass, shelving, parameteric, notch, allpass, ... + +class Biquad { + public: + Biquad(); + ~Biquad(); + + void process(const float* sourceP, float* destP, size_t framesToProcess); + + // frequency is 0 - 1 normalized, resonance and dbGain are in decibels. + // Q is a unitless quality factor. + void setLowpassParams(double frequency, double resonance); + void setHighpassParams(double frequency, double resonance); + void setBandpassParams(double frequency, double Q); + void setLowShelfParams(double frequency, double dbGain); + void setHighShelfParams(double frequency, double dbGain); + void setPeakingParams(double frequency, double Q, double dbGain); + void setAllpassParams(double frequency, double Q); + void setNotchParams(double frequency, double Q); + + // Set the biquad coefficients given a single zero (other zero will be + // conjugate) and a single pole (other pole will be conjugate) + void setZeroPolePairs(const Complex& zero, const Complex& pole); + + // Set the biquad coefficients given a single pole (other pole will be + // conjugate) (The zeroes will be the inverse of the poles) + void setAllpassPole(const Complex& pole); + + // Return true iff the next output block will contain sound even with + // silent input. + bool hasTail() const { return m_y1 || m_y2 || m_x1 || m_x2; } + + // Resets filter state + void reset(); + + // Filter response at a set of n frequencies. The magnitude and + // phase response are returned in magResponse and phaseResponse. + // The phase response is in radians. + void getFrequencyResponse(int nFrequencies, const float* frequency, + float* magResponse, float* phaseResponse); + + private: + void setNormalizedCoefficients(double b0, double b1, double b2, double a0, + double a1, double a2); + + // Filter coefficients. The filter is defined as + // + // y[n] + m_a1*y[n-1] + m_a2*y[n-2] = m_b0*x[n] + m_b1*x[n-1] + m_b2*x[n-2]. + double m_b0; + double m_b1; + double m_b2; + double m_a1; + double m_a2; + + // Filter memory + // + // Double precision for the output values is valuable because errors can + // accumulate. Input values are also stored as double so they need not be + // converted again for computation. + double m_x1; // input delayed by 1 sample + double m_x2; // input delayed by 2 samples + double m_y1; // output delayed by 1 sample + double m_y2; // output delayed by 2 samples +}; + +} // namespace WebCore + +#endif // Biquad_h diff --git a/dom/media/webaudio/blink/DenormalDisabler.h b/dom/media/webaudio/blink/DenormalDisabler.h new file mode 100644 index 0000000000..646482b74f --- /dev/null +++ b/dom/media/webaudio/blink/DenormalDisabler.h @@ -0,0 +1,170 @@ +/* + * Copyright (C) 2011, 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. + */ + +#ifndef DenormalDisabler_h +#define DenormalDisabler_h + +#include <cmath> +#include <cinttypes> +#include <cstring> +#include <float.h> + +namespace WebCore { + +// Deal with denormals. They can very seriously impact performance on x86. + +// Define HAVE_DENORMAL if we support flushing denormals to zero. + +#if defined(XP_WIN) && defined(_MSC_VER) +// Windows compiled using MSVC with SSE2 +# define HAVE_DENORMAL 1 +#endif + +#if defined(__GNUC__) && defined(__SSE__) +# define HAVE_DENORMAL 1 +#endif + +#if defined(__arm__) || defined(__aarch64__) +# define HAVE_DENORMAL 1 +#endif + +#ifdef HAVE_DENORMAL +class DenormalDisabler { + public: + DenormalDisabler() : m_savedCSR(0) { disableDenormals(); } + + ~DenormalDisabler() { restoreState(); } + + // This is a nop if we can flush denormals to zero in hardware. + static inline float flushDenormalFloatToZero(float f) { return f; } + + private: + unsigned m_savedCSR; + +# if defined(__GNUC__) && defined(__SSE__) + static inline bool isDAZSupported() { +# if defined(__x86_64__) + return true; +# else + static bool s_isInited = false; + static bool s_isSupported = false; + if (s_isInited) { + return s_isSupported; + } + + struct fxsaveResult { + uint8_t before[28]; + uint32_t CSRMask; + uint8_t after[480]; + } __attribute__((aligned(16))); + + fxsaveResult registerData; + memset(®isterData, 0, sizeof(fxsaveResult)); + asm volatile("fxsave %0" : "=m"(registerData)); + s_isSupported = registerData.CSRMask & 0x0040; + s_isInited = true; + return s_isSupported; +# endif + } + + inline void disableDenormals() { + m_savedCSR = getCSR(); + setCSR(m_savedCSR | (isDAZSupported() ? 0x8040 : 0x8000)); + } + + inline void restoreState() { setCSR(m_savedCSR); } + + inline int getCSR() { + int result; + asm volatile("stmxcsr %0" : "=m"(result)); + return result; + } + + inline void setCSR(int a) { + int temp = a; + asm volatile("ldmxcsr %0" : : "m"(temp)); + } + +# elif defined(XP_WIN) && defined(_MSC_VER) + inline void disableDenormals() { + // Save the current state, and set mode to flush denormals. + // + // http://stackoverflow.com/questions/637175/possible-bug-in-controlfp-s-may-not-restore-control-word-correctly + _controlfp_s(&m_savedCSR, 0, 0); + unsigned unused; + _controlfp_s(&unused, _DN_FLUSH, _MCW_DN); + } + + inline void restoreState() { + unsigned unused; + _controlfp_s(&unused, m_savedCSR, _MCW_DN); + } +# elif defined(__arm__) || defined(__aarch64__) + inline void disableDenormals() { + m_savedCSR = getStatusWord(); + // Bit 24 is the flush-to-zero mode control bit. Setting it to 1 flushes + // denormals to 0. + setStatusWord(m_savedCSR | (1 << 24)); + } + + inline void restoreState() { setStatusWord(m_savedCSR); } + + inline int getStatusWord() { + int result; +# if defined(__aarch64__) + asm volatile("mrs %x[result], FPCR" : [result] "=r"(result)); +# else + asm volatile("vmrs %[result], FPSCR" : [result] "=r"(result)); +# endif + return result; + } + + inline void setStatusWord(int a) { +# if defined(__aarch64__) + asm volatile("msr FPCR, %x[src]" : : [src] "r"(a)); +# else + asm volatile("vmsr FPSCR, %[src]" : : [src] "r"(a)); +# endif + } + +# endif +}; + +#else +// FIXME: add implementations for other architectures and compilers +class DenormalDisabler { + public: + DenormalDisabler() {} + + // Assume the worst case that other architectures and compilers + // need to flush denormals to zero manually. + static inline float flushDenormalFloatToZero(float f) { + return (fabs(f) < FLT_MIN) ? 0.0f : f; + } +}; + +#endif + +} // namespace WebCore +#endif // DenormalDisabler_h diff --git a/dom/media/webaudio/blink/DynamicsCompressor.cpp b/dom/media/webaudio/blink/DynamicsCompressor.cpp new file mode 100644 index 0000000000..0ee9426692 --- /dev/null +++ b/dom/media/webaudio/blink/DynamicsCompressor.cpp @@ -0,0 +1,315 @@ +/* + * Copyright (C) 2011 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. + * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of + * its contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY APPLE 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 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 "DynamicsCompressor.h" +#include "AlignmentUtils.h" +#include "AudioBlock.h" + +#include <cmath> +#include "AudioNodeEngine.h" +#include "nsDebug.h" + +using mozilla::AudioBlockCopyChannelWithScale; +using mozilla::WEBAUDIO_BLOCK_SIZE; + +namespace WebCore { + +DynamicsCompressor::DynamicsCompressor(float sampleRate, + unsigned numberOfChannels) + : m_numberOfChannels(numberOfChannels), + m_sampleRate(sampleRate), + m_compressor(sampleRate, numberOfChannels) { + // Uninitialized state - for parameter recalculation. + m_lastFilterStageRatio = -1; + m_lastAnchor = -1; + m_lastFilterStageGain = -1; + + setNumberOfChannels(numberOfChannels); + initializeParameters(); +} + +size_t DynamicsCompressor::sizeOfIncludingThis( + mozilla::MallocSizeOf aMallocSizeOf) const { + size_t amount = aMallocSizeOf(this); + amount += m_preFilterPacks.ShallowSizeOfExcludingThis(aMallocSizeOf); + for (size_t i = 0; i < m_preFilterPacks.Length(); i++) { + if (m_preFilterPacks[i]) { + amount += m_preFilterPacks[i]->sizeOfIncludingThis(aMallocSizeOf); + } + } + + amount += m_postFilterPacks.ShallowSizeOfExcludingThis(aMallocSizeOf); + for (size_t i = 0; i < m_postFilterPacks.Length(); i++) { + if (m_postFilterPacks[i]) { + amount += m_postFilterPacks[i]->sizeOfIncludingThis(aMallocSizeOf); + } + } + + amount += aMallocSizeOf(m_sourceChannels.get()); + amount += aMallocSizeOf(m_destinationChannels.get()); + amount += m_compressor.sizeOfExcludingThis(aMallocSizeOf); + return amount; +} + +void DynamicsCompressor::setParameterValue(unsigned parameterID, float value) { + MOZ_ASSERT(parameterID < ParamLast); + if (parameterID < ParamLast) m_parameters[parameterID] = value; +} + +void DynamicsCompressor::initializeParameters() { + // Initializes compressor to default values. + + m_parameters[ParamThreshold] = -24; // dB + m_parameters[ParamKnee] = 30; // dB + m_parameters[ParamRatio] = 12; // unit-less + m_parameters[ParamAttack] = 0.003f; // seconds + m_parameters[ParamRelease] = 0.250f; // seconds + m_parameters[ParamPreDelay] = 0.006f; // seconds + + // Release zone values 0 -> 1. + m_parameters[ParamReleaseZone1] = 0.09f; + m_parameters[ParamReleaseZone2] = 0.16f; + m_parameters[ParamReleaseZone3] = 0.42f; + m_parameters[ParamReleaseZone4] = 0.98f; + + m_parameters[ParamFilterStageGain] = 4.4f; // dB + m_parameters[ParamFilterStageRatio] = 2; + m_parameters[ParamFilterAnchor] = 15000 / nyquist(); + + m_parameters[ParamPostGain] = 0; // dB + m_parameters[ParamReduction] = 0; // dB + + // Linear crossfade (0 -> 1). + m_parameters[ParamEffectBlend] = 1; +} + +float DynamicsCompressor::parameterValue(unsigned parameterID) { + MOZ_ASSERT(parameterID < ParamLast); + return m_parameters[parameterID]; +} + +void DynamicsCompressor::setEmphasisStageParameters( + unsigned stageIndex, float gain, float normalizedFrequency /* 0 -> 1 */) { + float gk = 1 - gain / 20; + float f1 = normalizedFrequency * gk; + float f2 = normalizedFrequency / gk; + float r1 = fdlibm_expf(-f1 * M_PI); + float r2 = fdlibm_expf(-f2 * M_PI); + + MOZ_ASSERT(m_numberOfChannels == m_preFilterPacks.Length()); + + for (unsigned i = 0; i < m_numberOfChannels; ++i) { + // Set pre-filter zero and pole to create an emphasis filter. + ZeroPole& preFilter = m_preFilterPacks[i]->filters[stageIndex]; + preFilter.setZero(r1); + preFilter.setPole(r2); + + // Set post-filter with zero and pole reversed to create the de-emphasis + // filter. If there were no compressor kernel in between, they would cancel + // each other out (allpass filter). + ZeroPole& postFilter = m_postFilterPacks[i]->filters[stageIndex]; + postFilter.setZero(r2); + postFilter.setPole(r1); + } +} + +void DynamicsCompressor::setEmphasisParameters(float gain, float anchorFreq, + float filterStageRatio) { + setEmphasisStageParameters(0, gain, anchorFreq); + setEmphasisStageParameters(1, gain, anchorFreq / filterStageRatio); + setEmphasisStageParameters( + 2, gain, anchorFreq / (filterStageRatio * filterStageRatio)); + setEmphasisStageParameters( + 3, gain, + anchorFreq / (filterStageRatio * filterStageRatio * filterStageRatio)); +} + +void DynamicsCompressor::process(const AudioBlock* sourceChunk, + AudioBlock* destinationChunk, + unsigned framesToProcess) { + // Though numberOfChannels is retrived from destinationBus, we still name it + // numberOfChannels instead of numberOfDestinationChannels. It's because we + // internally match sourceChannels's size to destinationBus by channel up/down + // mix. Thus we need numberOfChannels to do the loop work for both + // m_sourceChannels and m_destinationChannels. + + unsigned numberOfChannels = destinationChunk->ChannelCount(); + unsigned numberOfSourceChannels = sourceChunk->ChannelCount(); + + MOZ_ASSERT(numberOfChannels == m_numberOfChannels && numberOfSourceChannels); + + if (numberOfChannels != m_numberOfChannels || !numberOfSourceChannels) { + destinationChunk->SetNull(WEBAUDIO_BLOCK_SIZE); + return; + } + + switch (numberOfChannels) { + case 2: // stereo + m_sourceChannels[0] = + static_cast<const float*>(sourceChunk->mChannelData[0]); + + if (numberOfSourceChannels > 1) + m_sourceChannels[1] = + static_cast<const float*>(sourceChunk->mChannelData[1]); + else + // Simply duplicate mono channel input data to right channel for stereo + // processing. + m_sourceChannels[1] = m_sourceChannels[0]; + + break; + case 1: + m_sourceChannels[0] = + static_cast<const float*>(sourceChunk->mChannelData[0]); + break; + default: + MOZ_CRASH("not supported."); + } + + for (unsigned i = 0; i < numberOfChannels; ++i) + m_destinationChannels[i] = const_cast<float*>( + static_cast<const float*>(destinationChunk->mChannelData[i])); + + float filterStageGain = parameterValue(ParamFilterStageGain); + float filterStageRatio = parameterValue(ParamFilterStageRatio); + float anchor = parameterValue(ParamFilterAnchor); + + if (filterStageGain != m_lastFilterStageGain || + filterStageRatio != m_lastFilterStageRatio || anchor != m_lastAnchor) { + m_lastFilterStageGain = filterStageGain; + m_lastFilterStageRatio = filterStageRatio; + m_lastAnchor = anchor; + + setEmphasisParameters(filterStageGain, anchor, filterStageRatio); + } + + float sourceWithVolume[WEBAUDIO_BLOCK_SIZE + 4]; + float* alignedSourceWithVolume = ALIGNED16(sourceWithVolume); + ASSERT_ALIGNED16(alignedSourceWithVolume); + + // Apply pre-emphasis filter. + // Note that the final three stages are computed in-place in the destination + // buffer. + for (unsigned i = 0; i < numberOfChannels; ++i) { + const float* sourceData; + if (sourceChunk->mVolume == 1.0f) { + // Fast path, the volume scale doesn't need to get taken into account + sourceData = m_sourceChannels[i]; + } else { + AudioBlockCopyChannelWithScale(m_sourceChannels[i], sourceChunk->mVolume, + alignedSourceWithVolume); + sourceData = alignedSourceWithVolume; + } + + float* destinationData = m_destinationChannels[i]; + ZeroPole* preFilters = m_preFilterPacks[i]->filters; + + preFilters[0].process(sourceData, destinationData, framesToProcess); + preFilters[1].process(destinationData, destinationData, framesToProcess); + preFilters[2].process(destinationData, destinationData, framesToProcess); + preFilters[3].process(destinationData, destinationData, framesToProcess); + } + + float dbThreshold = parameterValue(ParamThreshold); + float dbKnee = parameterValue(ParamKnee); + float ratio = parameterValue(ParamRatio); + float attackTime = parameterValue(ParamAttack); + float releaseTime = parameterValue(ParamRelease); + float preDelayTime = parameterValue(ParamPreDelay); + + // This is effectively a master volume on the compressed signal + // (pre-blending). + float dbPostGain = parameterValue(ParamPostGain); + + // Linear blending value from dry to completely processed (0 -> 1) + // 0 means the signal is completely unprocessed. + // 1 mixes in only the compressed signal. + float effectBlend = parameterValue(ParamEffectBlend); + + float releaseZone1 = parameterValue(ParamReleaseZone1); + float releaseZone2 = parameterValue(ParamReleaseZone2); + float releaseZone3 = parameterValue(ParamReleaseZone3); + float releaseZone4 = parameterValue(ParamReleaseZone4); + + // Apply compression to the pre-filtered signal. + // The processing is performed in place. + m_compressor.process(m_destinationChannels.get(), m_destinationChannels.get(), + numberOfChannels, framesToProcess, + + dbThreshold, dbKnee, ratio, attackTime, releaseTime, + preDelayTime, dbPostGain, effectBlend, + + releaseZone1, releaseZone2, releaseZone3, releaseZone4); + + // Update the compression amount. + setParameterValue(ParamReduction, m_compressor.meteringGain()); + + // Apply de-emphasis filter. + for (unsigned i = 0; i < numberOfChannels; ++i) { + float* destinationData = m_destinationChannels[i]; + ZeroPole* postFilters = m_postFilterPacks[i]->filters; + + postFilters[0].process(destinationData, destinationData, framesToProcess); + postFilters[1].process(destinationData, destinationData, framesToProcess); + postFilters[2].process(destinationData, destinationData, framesToProcess); + postFilters[3].process(destinationData, destinationData, framesToProcess); + } +} + +void DynamicsCompressor::reset() { + m_lastFilterStageRatio = -1; // for recalc + m_lastAnchor = -1; + m_lastFilterStageGain = -1; + + for (unsigned channel = 0; channel < m_numberOfChannels; ++channel) { + for (unsigned stageIndex = 0; stageIndex < 4; ++stageIndex) { + m_preFilterPacks[channel]->filters[stageIndex].reset(); + m_postFilterPacks[channel]->filters[stageIndex].reset(); + } + } + + m_compressor.reset(); +} + +void DynamicsCompressor::setNumberOfChannels(unsigned numberOfChannels) { + if (m_preFilterPacks.Length() == numberOfChannels) return; + + m_preFilterPacks.Clear(); + m_postFilterPacks.Clear(); + for (unsigned i = 0; i < numberOfChannels; ++i) { + m_preFilterPacks.AppendElement(new ZeroPoleFilterPack4()); + m_postFilterPacks.AppendElement(new ZeroPoleFilterPack4()); + } + + m_sourceChannels = mozilla::MakeUnique<const float*[]>(numberOfChannels); + m_destinationChannels = mozilla::MakeUnique<float*[]>(numberOfChannels); + + m_compressor.setNumberOfChannels(numberOfChannels); + m_numberOfChannels = numberOfChannels; +} + +} // namespace WebCore diff --git a/dom/media/webaudio/blink/DynamicsCompressor.h b/dom/media/webaudio/blink/DynamicsCompressor.h new file mode 100644 index 0000000000..1838e268fe --- /dev/null +++ b/dom/media/webaudio/blink/DynamicsCompressor.h @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2011 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. + * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of + * its contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY APPLE 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 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. + */ + +#ifndef DynamicsCompressor_h +#define DynamicsCompressor_h + +#include "DynamicsCompressorKernel.h" +#include "ZeroPole.h" + +#include "nsTArray.h" +#include "mozilla/MemoryReporting.h" +#include "mozilla/UniquePtr.h" + +namespace mozilla { +class AudioBlock; +} // namespace mozilla + +namespace WebCore { + +using mozilla::AudioBlock; +using mozilla::UniquePtr; + +// DynamicsCompressor implements a flexible audio dynamics compression effect +// such as is commonly used in musical production and game audio. It lowers the +// volume of the loudest parts of the signal and raises the volume of the +// softest parts, making the sound richer, fuller, and more controlled. + +class DynamicsCompressor { + public: + enum { + ParamThreshold, + ParamKnee, + ParamRatio, + ParamAttack, + ParamRelease, + ParamPreDelay, + ParamReleaseZone1, + ParamReleaseZone2, + ParamReleaseZone3, + ParamReleaseZone4, + ParamPostGain, + ParamFilterStageGain, + ParamFilterStageRatio, + ParamFilterAnchor, + ParamEffectBlend, + ParamReduction, + ParamLast + }; + + DynamicsCompressor(float sampleRate, unsigned numberOfChannels); + + void process(const AudioBlock* sourceChunk, AudioBlock* destinationChunk, + unsigned framesToProcess); + void reset(); + void setNumberOfChannels(unsigned); + unsigned numberOfChannels() const { return m_numberOfChannels; } + + void setParameterValue(unsigned parameterID, float value); + float parameterValue(unsigned parameterID); + + float sampleRate() const { return m_sampleRate; } + float nyquist() const { return m_sampleRate / 2; } + + double tailTime() const { return 0; } + double latencyTime() const { + return m_compressor.latencyFrames() / static_cast<double>(sampleRate()); + } + + size_t sizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const; + + protected: + unsigned m_numberOfChannels; + + // m_parameters holds the tweakable compressor parameters. + float m_parameters[ParamLast]; + void initializeParameters(); + + float m_sampleRate; + + // Emphasis filter controls. + float m_lastFilterStageRatio; + float m_lastAnchor; + float m_lastFilterStageGain; + + struct ZeroPoleFilterPack4 { + ZeroPole filters[4]; + size_t sizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const { + return aMallocSizeOf(this); + } + }; + + // Per-channel emphasis filters. + nsTArray<UniquePtr<ZeroPoleFilterPack4> > m_preFilterPacks; + nsTArray<UniquePtr<ZeroPoleFilterPack4> > m_postFilterPacks; + + mozilla::UniquePtr<const float*[]> m_sourceChannels; + mozilla::UniquePtr<float*[]> m_destinationChannels; + + void setEmphasisStageParameters(unsigned stageIndex, float gain, + float normalizedFrequency /* 0 -> 1 */); + void setEmphasisParameters(float gain, float anchorFreq, + float filterStageRatio); + + // The core compressor. + DynamicsCompressorKernel m_compressor; +}; + +} // namespace WebCore + +#endif // DynamicsCompressor_h diff --git a/dom/media/webaudio/blink/DynamicsCompressorKernel.cpp b/dom/media/webaudio/blink/DynamicsCompressorKernel.cpp new file mode 100644 index 0000000000..7fac8b78a3 --- /dev/null +++ b/dom/media/webaudio/blink/DynamicsCompressorKernel.cpp @@ -0,0 +1,496 @@ +/* + * Copyright (C) 2011 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. + * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of + * its contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY APPLE 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 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 "DynamicsCompressorKernel.h" + +#include "DenormalDisabler.h" +#include <algorithm> +#include <cmath> + +#include "mozilla/FloatingPoint.h" +#include "WebAudioUtils.h" + +using namespace mozilla::dom; // for WebAudioUtils +using mozilla::MakeUnique; +using mozilla::PositiveInfinity; + +namespace WebCore { + +// Metering hits peaks instantly, but releases this fast (in seconds). +const float meteringReleaseTimeConstant = 0.325f; + +const float uninitializedValue = -1; + +DynamicsCompressorKernel::DynamicsCompressorKernel(float sampleRate, + unsigned numberOfChannels) + : m_sampleRate(sampleRate), + m_lastPreDelayFrames(DefaultPreDelayFrames), + m_preDelayReadIndex(0), + m_preDelayWriteIndex(DefaultPreDelayFrames), + m_ratio(uninitializedValue), + m_slope(uninitializedValue), + m_linearThreshold(uninitializedValue), + m_dbThreshold(uninitializedValue), + m_dbKnee(uninitializedValue), + m_kneeThreshold(uninitializedValue), + m_kneeThresholdDb(uninitializedValue), + m_ykneeThresholdDb(uninitializedValue), + m_K(uninitializedValue) { + setNumberOfChannels(numberOfChannels); + + // Initializes most member variables + reset(); + + m_meteringReleaseK = + static_cast<float>(WebAudioUtils::DiscreteTimeConstantForSampleRate( + meteringReleaseTimeConstant, sampleRate)); +} + +size_t DynamicsCompressorKernel::sizeOfExcludingThis( + mozilla::MallocSizeOf aMallocSizeOf) const { + size_t amount = 0; + amount += m_preDelayBuffers.ShallowSizeOfExcludingThis(aMallocSizeOf); + for (size_t i = 0; i < m_preDelayBuffers.Length(); i++) { + amount += aMallocSizeOf(m_preDelayBuffers[i].get()); + } + + return amount; +} + +void DynamicsCompressorKernel::setNumberOfChannels(unsigned numberOfChannels) { + if (m_preDelayBuffers.Length() == numberOfChannels) return; + + m_preDelayBuffers.Clear(); + for (unsigned i = 0; i < numberOfChannels; ++i) + m_preDelayBuffers.AppendElement(MakeUnique<float[]>(MaxPreDelayFrames)); +} + +void DynamicsCompressorKernel::setPreDelayTime(float preDelayTime) { + // Re-configure look-ahead section pre-delay if delay time has changed. + unsigned preDelayFrames = preDelayTime * sampleRate(); + if (preDelayFrames > MaxPreDelayFrames - 1) + preDelayFrames = MaxPreDelayFrames - 1; + + if (m_lastPreDelayFrames != preDelayFrames) { + m_lastPreDelayFrames = preDelayFrames; + for (unsigned i = 0; i < m_preDelayBuffers.Length(); ++i) + memset(m_preDelayBuffers[i].get(), 0, sizeof(float) * MaxPreDelayFrames); + + m_preDelayReadIndex = 0; + m_preDelayWriteIndex = preDelayFrames; + } +} + +// Exponential curve for the knee. +// It is 1st derivative matched at m_linearThreshold and asymptotically +// approaches the value m_linearThreshold + 1 / k. +float DynamicsCompressorKernel::kneeCurve(float x, float k) { + // Linear up to threshold. + if (x < m_linearThreshold) return x; + + return m_linearThreshold + + (1 - fdlibm_expf(-k * (x - m_linearThreshold))) / k; +} + +// Full compression curve with constant ratio after knee. +float DynamicsCompressorKernel::saturate(float x, float k) { + float y; + + if (x < m_kneeThreshold) + y = kneeCurve(x, k); + else { + // Constant ratio after knee. + float xDb = WebAudioUtils::ConvertLinearToDecibels(x, -1000.0f); + float yDb = m_ykneeThresholdDb + m_slope * (xDb - m_kneeThresholdDb); + + y = WebAudioUtils::ConvertDecibelsToLinear(yDb); + } + + return y; +} + +// Approximate 1st derivative with input and output expressed in dB. +// This slope is equal to the inverse of the compression "ratio". +// In other words, a compression ratio of 20 would be a slope of 1/20. +float DynamicsCompressorKernel::slopeAt(float x, float k) { + if (x < m_linearThreshold) return 1; + + float x2 = x * 1.001; + + float xDb = WebAudioUtils::ConvertLinearToDecibels(x, -1000.0f); + float x2Db = WebAudioUtils::ConvertLinearToDecibels(x2, -1000.0f); + + float yDb = WebAudioUtils::ConvertLinearToDecibels(kneeCurve(x, k), -1000.0f); + float y2Db = + WebAudioUtils::ConvertLinearToDecibels(kneeCurve(x2, k), -1000.0f); + + float m = (y2Db - yDb) / (x2Db - xDb); + + return m; +} + +float DynamicsCompressorKernel::kAtSlope(float desiredSlope) { + float xDb = m_dbThreshold + m_dbKnee; + float x = WebAudioUtils::ConvertDecibelsToLinear(xDb); + + // Approximate k given initial values. + float minK = 0.1f; + float maxK = 10000; + float k = 5; + + for (int i = 0; i < 15; ++i) { + // A high value for k will more quickly asymptotically approach a slope of + // 0. + float slope = slopeAt(x, k); + + if (slope < desiredSlope) { + // k is too high. + maxK = k; + } else { + // k is too low. + minK = k; + } + + // Re-calculate based on geometric mean. + k = sqrtf(minK * maxK); + } + + return k; +} + +float DynamicsCompressorKernel::updateStaticCurveParameters(float dbThreshold, + float dbKnee, + float ratio) { + if (dbThreshold != m_dbThreshold || dbKnee != m_dbKnee || ratio != m_ratio) { + // Threshold and knee. + m_dbThreshold = dbThreshold; + m_linearThreshold = WebAudioUtils::ConvertDecibelsToLinear(dbThreshold); + m_dbKnee = dbKnee; + + // Compute knee parameters. + m_ratio = ratio; + m_slope = 1 / m_ratio; + + float k = kAtSlope(1 / m_ratio); + + m_kneeThresholdDb = dbThreshold + dbKnee; + m_kneeThreshold = WebAudioUtils::ConvertDecibelsToLinear(m_kneeThresholdDb); + + m_ykneeThresholdDb = WebAudioUtils::ConvertLinearToDecibels( + kneeCurve(m_kneeThreshold, k), -1000.0f); + + m_K = k; + } + return m_K; +} + +void DynamicsCompressorKernel::process( + float* sourceChannels[], float* destinationChannels[], + unsigned numberOfChannels, unsigned framesToProcess, + + float dbThreshold, float dbKnee, float ratio, float attackTime, + float releaseTime, float preDelayTime, float dbPostGain, + float effectBlend, /* equal power crossfade */ + + float releaseZone1, float releaseZone2, float releaseZone3, + float releaseZone4) { + MOZ_ASSERT(m_preDelayBuffers.Length() == numberOfChannels); + + float sampleRate = this->sampleRate(); + + float dryMix = 1 - effectBlend; + float wetMix = effectBlend; + + float k = updateStaticCurveParameters(dbThreshold, dbKnee, ratio); + + // Makeup gain. + float fullRangeGain = saturate(1, k); + float fullRangeMakeupGain = 1 / fullRangeGain; + + // Empirical/perceptual tuning. + fullRangeMakeupGain = fdlibm_powf(fullRangeMakeupGain, 0.6f); + + float masterLinearGain = + WebAudioUtils::ConvertDecibelsToLinear(dbPostGain) * fullRangeMakeupGain; + + // Attack parameters. + attackTime = std::max(0.001f, attackTime); + float attackFrames = attackTime * sampleRate; + + // Release parameters. + float releaseFrames = sampleRate * releaseTime; + + // Detector release time. + float satReleaseTime = 0.0025f; + float satReleaseFrames = satReleaseTime * sampleRate; + + // Create a smooth function which passes through four points. + + // Polynomial of the form + // y = a + b*x + c*x^2 + d*x^3 + e*x^4; + + float y1 = releaseFrames * releaseZone1; + float y2 = releaseFrames * releaseZone2; + float y3 = releaseFrames * releaseZone3; + float y4 = releaseFrames * releaseZone4; + + // All of these coefficients were derived for 4th order polynomial curve + // fitting where the y values match the evenly spaced x values as follows: + // (y1 : x == 0, y2 : x == 1, y3 : x == 2, y4 : x == 3) + float kA = 0.9999999999999998f * y1 + 1.8432219684323923e-16f * y2 - + 1.9373394351676423e-16f * y3 + 8.824516011816245e-18f * y4; + float kB = -1.5788320352845888f * y1 + 2.3305837032074286f * y2 - + 0.9141194204840429f * y3 + 0.1623677525612032f * y4; + float kC = 0.5334142869106424f * y1 - 1.272736789213631f * y2 + + 0.9258856042207512f * y3 - 0.18656310191776226f * y4; + float kD = 0.08783463138207234f * y1 - 0.1694162967925622f * y2 + + 0.08588057951595272f * y3 - 0.00429891410546283f * y4; + float kE = -0.042416883008123074f * y1 + 0.1115693827987602f * y2 - + 0.09764676325265872f * y3 + 0.028494263462021576f * y4; + + // x ranges from 0 -> 3 0 1 2 3 + // -15 -10 -5 0db + + // y calculates adaptive release frames depending on the amount of + // compression. + + setPreDelayTime(preDelayTime); + + const int nDivisionFrames = 32; + + const int nDivisions = framesToProcess / nDivisionFrames; + + unsigned frameIndex = 0; + for (int i = 0; i < nDivisions; ++i) { + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Calculate desired gain + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + // Fix gremlins. + if (std::isnan(m_detectorAverage)) m_detectorAverage = 1; + if (std::isinf(m_detectorAverage)) m_detectorAverage = 1; + + float desiredGain = m_detectorAverage; + + // Pre-warp so we get desiredGain after sin() warp below. + float scaledDesiredGain = fdlibm_asinf(desiredGain) / (0.5f * M_PI); + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Deal with envelopes + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + // envelopeRate is the rate we slew from current compressor level to the + // desired level. The exact rate depends on if we're attacking or releasing + // and by how much. + float envelopeRate; + + bool isReleasing = scaledDesiredGain > m_compressorGain; + + // compressionDiffDb is the difference between current compression level and + // the desired level. + float compressionDiffDb; + if (scaledDesiredGain == 0.0) { + compressionDiffDb = PositiveInfinity<float>(); + } else { + compressionDiffDb = WebAudioUtils::ConvertLinearToDecibels( + m_compressorGain / scaledDesiredGain, -1000.0f); + } + + if (isReleasing) { + // Release mode - compressionDiffDb should be negative dB + m_maxAttackCompressionDiffDb = -1; + + // Fix gremlins. + if (std::isnan(compressionDiffDb)) compressionDiffDb = -1; + if (std::isinf(compressionDiffDb)) compressionDiffDb = -1; + + // Adaptive release - higher compression (lower compressionDiffDb) + // releases faster. + + // Contain within range: -12 -> 0 then scale to go from 0 -> 3 + float x = compressionDiffDb; + x = std::max(-12.0f, x); + x = std::min(0.0f, x); + x = 0.25f * (x + 12); + + // Compute adaptive release curve using 4th order polynomial. + // Normal values for the polynomial coefficients would create a + // monotonically increasing function. + float x2 = x * x; + float x3 = x2 * x; + float x4 = x2 * x2; + float releaseFrames = kA + kB * x + kC * x2 + kD * x3 + kE * x4; + +#define kSpacingDb 5 + float dbPerFrame = kSpacingDb / releaseFrames; + + envelopeRate = WebAudioUtils::ConvertDecibelsToLinear(dbPerFrame); + } else { + // Attack mode - compressionDiffDb should be positive dB + + // Fix gremlins. + if (std::isnan(compressionDiffDb)) compressionDiffDb = 1; + if (std::isinf(compressionDiffDb)) compressionDiffDb = 1; + + // As long as we're still in attack mode, use a rate based off + // the largest compressionDiffDb we've encountered so far. + if (m_maxAttackCompressionDiffDb == -1 || + m_maxAttackCompressionDiffDb < compressionDiffDb) + m_maxAttackCompressionDiffDb = compressionDiffDb; + + float effAttenDiffDb = std::max(0.5f, m_maxAttackCompressionDiffDb); + + float x = 0.25f / effAttenDiffDb; + envelopeRate = 1 - fdlibm_powf(x, 1 / attackFrames); + } + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // Inner loop - calculate shaped power average - apply compression. + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + { + int preDelayReadIndex = m_preDelayReadIndex; + int preDelayWriteIndex = m_preDelayWriteIndex; + float detectorAverage = m_detectorAverage; + float compressorGain = m_compressorGain; + + int loopFrames = nDivisionFrames; + while (loopFrames--) { + float compressorInput = 0; + + // Predelay signal, computing compression amount from un-delayed + // version. + for (unsigned i = 0; i < numberOfChannels; ++i) { + float* delayBuffer = m_preDelayBuffers[i].get(); + float undelayedSource = sourceChannels[i][frameIndex]; + delayBuffer[preDelayWriteIndex] = undelayedSource; + + float absUndelayedSource = + undelayedSource > 0 ? undelayedSource : -undelayedSource; + if (compressorInput < absUndelayedSource) + compressorInput = absUndelayedSource; + } + + // Calculate shaped power on undelayed input. + + float scaledInput = compressorInput; + float absInput = scaledInput > 0 ? scaledInput : -scaledInput; + + // Put through shaping curve. + // This is linear up to the threshold, then enters a "knee" portion + // followed by the "ratio" portion. The transition from the threshold to + // the knee is smooth (1st derivative matched). The transition from the + // knee to the ratio portion is smooth (1st derivative matched). + float shapedInput = saturate(absInput, k); + + float attenuation = absInput <= 0.0001f ? 1 : shapedInput / absInput; + + float attenuationDb = + -WebAudioUtils::ConvertLinearToDecibels(attenuation, -1000.0f); + attenuationDb = std::max(2.0f, attenuationDb); + + float dbPerFrame = attenuationDb / satReleaseFrames; + + float satReleaseRate = + WebAudioUtils::ConvertDecibelsToLinear(dbPerFrame) - 1; + + bool isRelease = (attenuation > detectorAverage); + float rate = isRelease ? satReleaseRate : 1; + + detectorAverage += (attenuation - detectorAverage) * rate; + detectorAverage = std::min(1.0f, detectorAverage); + + // Fix gremlins. + if (std::isnan(detectorAverage)) detectorAverage = 1; + if (std::isinf(detectorAverage)) detectorAverage = 1; + + // Exponential approach to desired gain. + if (envelopeRate < 1) { + // Attack - reduce gain to desired. + compressorGain += (scaledDesiredGain - compressorGain) * envelopeRate; + } else { + // Release - exponentially increase gain to 1.0 + compressorGain *= envelopeRate; + compressorGain = std::min(1.0f, compressorGain); + } + + // Warp pre-compression gain to smooth out sharp exponential transition + // points. + float postWarpCompressorGain = + fdlibm_sinf(0.5f * M_PI * compressorGain); + + // Calculate total gain using master gain and effect blend. + float totalGain = + dryMix + wetMix * masterLinearGain * postWarpCompressorGain; + + // Calculate metering. + float dbRealGain = 20 * fdlibm_log10f(postWarpCompressorGain); + if (dbRealGain < m_meteringGain) + m_meteringGain = dbRealGain; + else + m_meteringGain += (dbRealGain - m_meteringGain) * m_meteringReleaseK; + + // Apply final gain. + for (unsigned i = 0; i < numberOfChannels; ++i) { + float* delayBuffer = m_preDelayBuffers[i].get(); + destinationChannels[i][frameIndex] = + delayBuffer[preDelayReadIndex] * totalGain; + } + + frameIndex++; + preDelayReadIndex = (preDelayReadIndex + 1) & MaxPreDelayFramesMask; + preDelayWriteIndex = (preDelayWriteIndex + 1) & MaxPreDelayFramesMask; + } + + // Locals back to member variables. + m_preDelayReadIndex = preDelayReadIndex; + m_preDelayWriteIndex = preDelayWriteIndex; + m_detectorAverage = + DenormalDisabler::flushDenormalFloatToZero(detectorAverage); + m_compressorGain = + DenormalDisabler::flushDenormalFloatToZero(compressorGain); + } + } +} + +void DynamicsCompressorKernel::reset() { + m_detectorAverage = 0; + m_compressorGain = 1; + m_meteringGain = 1; + + // Predelay section. + for (unsigned i = 0; i < m_preDelayBuffers.Length(); ++i) + memset(m_preDelayBuffers[i].get(), 0, sizeof(float) * MaxPreDelayFrames); + + m_preDelayReadIndex = 0; + m_preDelayWriteIndex = DefaultPreDelayFrames; + + m_maxAttackCompressionDiffDb = -1; // uninitialized state +} + +} // namespace WebCore diff --git a/dom/media/webaudio/blink/DynamicsCompressorKernel.h b/dom/media/webaudio/blink/DynamicsCompressorKernel.h new file mode 100644 index 0000000000..bb7defbd7e --- /dev/null +++ b/dom/media/webaudio/blink/DynamicsCompressorKernel.h @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2011 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. + * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of + * its contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY APPLE 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 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. + */ + +#ifndef DynamicsCompressorKernel_h +#define DynamicsCompressorKernel_h + +#include "nsTArray.h" +#include "mozilla/MemoryReporting.h" +#include "mozilla/UniquePtr.h" + +namespace WebCore { + +class DynamicsCompressorKernel { + public: + DynamicsCompressorKernel(float sampleRate, unsigned numberOfChannels); + + void setNumberOfChannels(unsigned); + + // Performs stereo-linked compression. + void process(float* sourceChannels[], float* destinationChannels[], + unsigned numberOfChannels, unsigned framesToProcess, + + float dbThreshold, float dbKnee, float ratio, float attackTime, + float releaseTime, float preDelayTime, float dbPostGain, + float effectBlend, + + float releaseZone1, float releaseZone2, float releaseZone3, + float releaseZone4); + + void reset(); + + unsigned latencyFrames() const { return m_lastPreDelayFrames; } + + float sampleRate() const { return m_sampleRate; } + + float meteringGain() const { return m_meteringGain; } + + size_t sizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const; + + protected: + float m_sampleRate; + + float m_detectorAverage; + float m_compressorGain; + + // Metering + float m_meteringReleaseK; + float m_meteringGain; + + // Lookahead section. + enum { MaxPreDelayFrames = 1024 }; + enum { MaxPreDelayFramesMask = MaxPreDelayFrames - 1 }; + enum { + DefaultPreDelayFrames = 256 + }; // setPreDelayTime() will override this initial value + unsigned m_lastPreDelayFrames; + void setPreDelayTime(float); + + nsTArray<mozilla::UniquePtr<float[]>> m_preDelayBuffers; + int m_preDelayReadIndex; + int m_preDelayWriteIndex; + + float m_maxAttackCompressionDiffDb; + + // Static compression curve. + float kneeCurve(float x, float k); + float saturate(float x, float k); + float slopeAt(float x, float k); + float kAtSlope(float desiredSlope); + + float updateStaticCurveParameters(float dbThreshold, float dbKnee, + float ratio); + + // Amount of input change in dB required for 1 dB of output change. + // This applies to the portion of the curve above m_kneeThresholdDb (see + // below). + float m_ratio; + float m_slope; // Inverse ratio. + + // The input to output change below the threshold is linear 1:1. + float m_linearThreshold; + float m_dbThreshold; + + // m_dbKnee is the number of dB above the threshold before we enter the + // "ratio" portion of the curve. m_kneeThresholdDb = m_dbThreshold + m_dbKnee + // The portion between m_dbThreshold and m_kneeThresholdDb is the "soft knee" + // portion of the curve which transitions smoothly from the linear portion to + // the ratio portion. + float m_dbKnee; + float m_kneeThreshold; + float m_kneeThresholdDb; + float m_ykneeThresholdDb; + + // Internal parameter for the knee portion of the curve. + float m_K; +}; + +} // namespace WebCore + +#endif // DynamicsCompressorKernel_h diff --git a/dom/media/webaudio/blink/FFTConvolver.cpp b/dom/media/webaudio/blink/FFTConvolver.cpp new file mode 100644 index 0000000000..2ade9031ce --- /dev/null +++ b/dom/media/webaudio/blink/FFTConvolver.cpp @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2010 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. + * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of + * its contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY APPLE 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 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 "FFTConvolver.h" +#include "mozilla/PodOperations.h" + +using namespace mozilla; + +namespace WebCore { + +FFTConvolver::FFTConvolver(size_t fftSize, size_t renderPhase) + : m_frame(fftSize), m_readWriteIndex(renderPhase % (fftSize / 2)) { + MOZ_ASSERT(fftSize >= 2 * WEBAUDIO_BLOCK_SIZE); + m_inputBuffer.SetLength(fftSize); + PodZero(m_inputBuffer.Elements(), fftSize); + m_outputBuffer.SetLength(fftSize); + PodZero(m_outputBuffer.Elements(), fftSize); + m_lastOverlapBuffer.SetLength(fftSize / 2); + PodZero(m_lastOverlapBuffer.Elements(), fftSize / 2); +} + +size_t FFTConvolver::sizeOfExcludingThis( + mozilla::MallocSizeOf aMallocSizeOf) const { + size_t amount = 0; + amount += m_frame.SizeOfExcludingThis(aMallocSizeOf); + amount += m_inputBuffer.ShallowSizeOfExcludingThis(aMallocSizeOf); + amount += m_outputBuffer.ShallowSizeOfExcludingThis(aMallocSizeOf); + amount += m_lastOverlapBuffer.ShallowSizeOfExcludingThis(aMallocSizeOf); + return amount; +} + +size_t FFTConvolver::sizeOfIncludingThis( + mozilla::MallocSizeOf aMallocSizeOf) const { + return aMallocSizeOf(this) + sizeOfExcludingThis(aMallocSizeOf); +} + +const float* FFTConvolver::process(FFTBlock* fftKernel, const float* sourceP) { + size_t halfSize = fftSize() / 2; + + // WEBAUDIO_BLOCK_SIZE must be an exact multiple of halfSize, + // halfSize must be a multiple of WEBAUDIO_BLOCK_SIZE + // and > WEBAUDIO_BLOCK_SIZE. + MOZ_ASSERT(halfSize % WEBAUDIO_BLOCK_SIZE == 0 && + WEBAUDIO_BLOCK_SIZE <= halfSize); + + // Copy samples to input buffer (note contraint above!) + float* inputP = m_inputBuffer.Elements(); + + MOZ_ASSERT(sourceP && inputP && + m_readWriteIndex + WEBAUDIO_BLOCK_SIZE <= m_inputBuffer.Length()); + + memcpy(inputP + m_readWriteIndex, sourceP, + sizeof(float) * WEBAUDIO_BLOCK_SIZE); + + float* outputP = m_outputBuffer.Elements(); + m_readWriteIndex += WEBAUDIO_BLOCK_SIZE; + + // Check if it's time to perform the next FFT + if (m_readWriteIndex == halfSize) { + // The input buffer is now filled (get frequency-domain version) + m_frame.PerformFFT(m_inputBuffer.Elements()); + m_frame.Multiply(*fftKernel); + m_frame.GetInverseWithoutScaling(m_outputBuffer.Elements()); + + // Overlap-add 1st half from previous time + AudioBufferAddWithScale(m_lastOverlapBuffer.Elements(), 1.0f, + m_outputBuffer.Elements(), halfSize); + + // Finally, save 2nd half of result + MOZ_ASSERT(m_outputBuffer.Length() == 2 * halfSize && + m_lastOverlapBuffer.Length() == halfSize); + + memcpy(m_lastOverlapBuffer.Elements(), m_outputBuffer.Elements() + halfSize, + sizeof(float) * halfSize); + + // Reset index back to start for next time + m_readWriteIndex = 0; + } + + return outputP + m_readWriteIndex; +} + +void FFTConvolver::reset() { + PodZero(m_lastOverlapBuffer.Elements(), m_lastOverlapBuffer.Length()); + m_readWriteIndex = 0; +} + +size_t FFTConvolver::latencyFrames() const { + return std::max<size_t>(fftSize() / 2, WEBAUDIO_BLOCK_SIZE) - + WEBAUDIO_BLOCK_SIZE; +} + +} // namespace WebCore diff --git a/dom/media/webaudio/blink/FFTConvolver.h b/dom/media/webaudio/blink/FFTConvolver.h new file mode 100644 index 0000000000..d54b07f7a4 --- /dev/null +++ b/dom/media/webaudio/blink/FFTConvolver.h @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2010 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. + * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of + * its contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY APPLE 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 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. + */ + +#ifndef FFTConvolver_h +#define FFTConvolver_h + +#include "nsTArray.h" +#include "mozilla/FFTBlock.h" +#include "mozilla/MemoryReporting.h" + +namespace WebCore { + +typedef AlignedTArray<float> AlignedAudioFloatArray; +using mozilla::FFTBlock; + +class FFTConvolver { + public: + // |fftSize| must be a power of two. + // + // |renderPhase| is the initial offset in the initially zero input buffer. + // It is coordinated with the other stages, so they don't all do their + // FFTs at the same time. + explicit FFTConvolver(size_t fftSize, size_t renderPhase = 0); + + // Process WEBAUDIO_BLOCK_SIZE elements of array |sourceP| and return a + // pointer to an output array of the same size. + // + // |fftKernel| must be pre-scaled for FFTBlock::GetInverseWithoutScaling(). + // + // FIXME: Later, we can do more sophisticated buffering to relax this + // requirement... + const float* process(FFTBlock* fftKernel, const float* sourceP); + + void reset(); + + size_t fftSize() const { return m_frame.FFTSize(); } + + // The input to output latency is up to fftSize / 2, but alignment of the + // FFTs with the blocks reduces this by one block. + size_t latencyFrames() const; + + size_t sizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const; + size_t sizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const; + + private: + FFTBlock m_frame; + + // Buffer input until we get fftSize / 2 samples then do an FFT + size_t m_readWriteIndex; + AlignedAudioFloatArray m_inputBuffer; + + // Stores output which we read a little at a time + AlignedAudioFloatArray m_outputBuffer; + + // Saves the 2nd half of the FFT buffer, so we can do an overlap-add with the + // 1st half of the next one + AlignedAudioFloatArray m_lastOverlapBuffer; +}; + +} // namespace WebCore + +#endif // FFTConvolver_h diff --git a/dom/media/webaudio/blink/HRTFDatabase.cpp b/dom/media/webaudio/blink/HRTFDatabase.cpp new file mode 100644 index 0000000000..54be5ea958 --- /dev/null +++ b/dom/media/webaudio/blink/HRTFDatabase.cpp @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2010 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. + * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of + * its contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY APPLE 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 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 "HRTFDatabase.h" + +#include "HRTFElevation.h" + +namespace WebCore { + +const int HRTFDatabase::MinElevation = -45; +const int HRTFDatabase::MaxElevation = 90; +const unsigned HRTFDatabase::RawElevationAngleSpacing = 15; +const unsigned HRTFDatabase::NumberOfRawElevations = + 10; // -45 -> +90 (each 15 degrees) +const unsigned HRTFDatabase::InterpolationFactor = 1; +const unsigned HRTFDatabase::NumberOfTotalElevations = + NumberOfRawElevations * InterpolationFactor; + +nsReturnRef<HRTFDatabase> HRTFDatabase::create(float sampleRate) { + return nsReturnRef<HRTFDatabase>(new HRTFDatabase(sampleRate)); +} + +HRTFDatabase::HRTFDatabase(float sampleRate) : m_sampleRate(sampleRate) { + m_elevations.SetLength(NumberOfTotalElevations); + + unsigned elevationIndex = 0; + for (int elevation = MinElevation; elevation <= MaxElevation; + elevation += RawElevationAngleSpacing) { + nsAutoRef<HRTFElevation> hrtfElevation( + HRTFElevation::createBuiltin(elevation, sampleRate)); + MOZ_ASSERT(hrtfElevation.get()); + if (!hrtfElevation.get()) return; + + m_elevations[elevationIndex] = hrtfElevation.out(); + elevationIndex += InterpolationFactor; + } + + // Now, go back and interpolate elevations. + if (InterpolationFactor > 1) { + for (unsigned i = 0; i < NumberOfTotalElevations; + i += InterpolationFactor) { + unsigned j = (i + InterpolationFactor); + if (j >= NumberOfTotalElevations) + j = i; // for last elevation interpolate with itself + + // Create the interpolated convolution kernels and delays. + for (unsigned jj = 1; jj < InterpolationFactor; ++jj) { + float x = + static_cast<float>(jj) / static_cast<float>(InterpolationFactor); + m_elevations[i + jj] = HRTFElevation::createByInterpolatingSlices( + m_elevations[i].get(), m_elevations[j].get(), x, sampleRate); + MOZ_ASSERT(m_elevations[i + jj].get()); + } + } + } +} + +size_t HRTFDatabase::sizeOfIncludingThis( + mozilla::MallocSizeOf aMallocSizeOf) const { + size_t amount = aMallocSizeOf(this); + amount += m_elevations.ShallowSizeOfExcludingThis(aMallocSizeOf); + for (size_t i = 0; i < m_elevations.Length(); i++) { + amount += m_elevations[i]->sizeOfIncludingThis(aMallocSizeOf); + } + + return amount; +} + +void HRTFDatabase::getKernelsFromAzimuthElevation( + double azimuthBlend, unsigned azimuthIndex, double elevationAngle, + HRTFKernel*& kernelL, HRTFKernel*& kernelR, double& frameDelayL, + double& frameDelayR) { + unsigned elevationIndex = indexFromElevationAngle(elevationAngle); + MOZ_ASSERT(elevationIndex < m_elevations.Length() && + m_elevations.Length() > 0); + + if (!m_elevations.Length()) { + kernelL = 0; + kernelR = 0; + return; + } + + if (elevationIndex > m_elevations.Length() - 1) + elevationIndex = m_elevations.Length() - 1; + + HRTFElevation* hrtfElevation = m_elevations[elevationIndex].get(); + MOZ_ASSERT(hrtfElevation); + if (!hrtfElevation) { + kernelL = 0; + kernelR = 0; + return; + } + + hrtfElevation->getKernelsFromAzimuth(azimuthBlend, azimuthIndex, kernelL, + kernelR, frameDelayL, frameDelayR); +} + +unsigned HRTFDatabase::indexFromElevationAngle(double elevationAngle) { + // Clamp to allowed range. + elevationAngle = + mozilla::clamped(elevationAngle, static_cast<double>(MinElevation), + static_cast<double>(MaxElevation)); + + unsigned elevationIndex = + static_cast<int>(InterpolationFactor * (elevationAngle - MinElevation) / + RawElevationAngleSpacing); + return elevationIndex; +} + +} // namespace WebCore diff --git a/dom/media/webaudio/blink/HRTFDatabase.h b/dom/media/webaudio/blink/HRTFDatabase.h new file mode 100644 index 0000000000..50cf2958bd --- /dev/null +++ b/dom/media/webaudio/blink/HRTFDatabase.h @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2010 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. + * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of + * its contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY APPLE 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 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. + */ + +#ifndef HRTFDatabase_h +#define HRTFDatabase_h + +#include "HRTFElevation.h" +#include "nsAutoRef.h" +#include "nsTArray.h" +#include "mozilla/MemoryReporting.h" + +namespace WebCore { + +class HRTFKernel; + +class HRTFDatabase { + public: + static nsReturnRef<HRTFDatabase> create(float sampleRate); + + // clang-format off + // getKernelsFromAzimuthElevation() returns a left and right ear kernel, and an interpolated left and right frame delay for the given azimuth and elevation. + // azimuthBlend must be in the range 0 -> 1. + // Valid values for azimuthIndex are 0 -> HRTFElevation::NumberOfTotalAzimuths - 1 (corresponding to angles of 0 -> 360). + // Valid values for elevationAngle are MinElevation -> MaxElevation. + // clang-format on + void getKernelsFromAzimuthElevation(double azimuthBlend, + unsigned azimuthIndex, + double elevationAngle, + HRTFKernel*& kernelL, + HRTFKernel*& kernelR, double& frameDelayL, + double& frameDelayR); + + // Returns the number of different azimuth angles. + static unsigned numberOfAzimuths() { + return HRTFElevation::NumberOfTotalAzimuths; + } + + float sampleRate() const { return m_sampleRate; } + + // Number of elevations loaded from resource. + static const unsigned NumberOfRawElevations; + + size_t sizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const; + + private: + HRTFDatabase(const HRTFDatabase& other) = delete; + void operator=(const HRTFDatabase& other) = delete; + + explicit HRTFDatabase(float sampleRate); + + // Minimum and maximum elevation angles (inclusive) for a HRTFDatabase. + static const int MinElevation; + static const int MaxElevation; + static const unsigned RawElevationAngleSpacing; + + // Interpolates by this factor to get the total number of elevations from + // every elevation loaded from resource. + static const unsigned InterpolationFactor; + + // Total number of elevations after interpolation. + static const unsigned NumberOfTotalElevations; + + // Returns the index for the correct HRTFElevation given the elevation angle. + static unsigned indexFromElevationAngle(double); + + nsTArray<nsAutoRef<HRTFElevation> > m_elevations; + float m_sampleRate; +}; + +} // namespace WebCore + +template <> +class nsAutoRefTraits<WebCore::HRTFDatabase> + : public nsPointerRefTraits<WebCore::HRTFDatabase> { + public: + static void Release(WebCore::HRTFDatabase* ptr) { delete (ptr); } +}; + +#endif // HRTFDatabase_h diff --git a/dom/media/webaudio/blink/HRTFDatabaseLoader.cpp b/dom/media/webaudio/blink/HRTFDatabaseLoader.cpp new file mode 100644 index 0000000000..3db455545e --- /dev/null +++ b/dom/media/webaudio/blink/HRTFDatabaseLoader.cpp @@ -0,0 +1,215 @@ +/* + * Copyright (C) 2010 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. + * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of + * its contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY APPLE 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 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 "HRTFDatabaseLoader.h" +#include "HRTFDatabase.h" +#include "GeckoProfiler.h" +#include "nsThreadUtils.h" + +using namespace mozilla; + +namespace WebCore { + +// Singleton +nsTHashtable<HRTFDatabaseLoader::LoaderByRateEntry>* + HRTFDatabaseLoader::s_loaderMap = nullptr; + +size_t HRTFDatabaseLoader::sizeOfLoaders(mozilla::MallocSizeOf aMallocSizeOf) { + return s_loaderMap ? s_loaderMap->SizeOfIncludingThis(aMallocSizeOf) : 0; +} + +already_AddRefed<HRTFDatabaseLoader> +HRTFDatabaseLoader::createAndLoadAsynchronouslyIfNecessary(float sampleRate) { + MOZ_ASSERT(NS_IsMainThread()); + + RefPtr<HRTFDatabaseLoader> loader; + + if (!s_loaderMap) { + s_loaderMap = new nsTHashtable<LoaderByRateEntry>(); + } + + LoaderByRateEntry* entry = s_loaderMap->PutEntry(sampleRate); + loader = entry->mLoader; + if (loader) { // existing entry + MOZ_ASSERT(sampleRate == loader->databaseSampleRate()); + return loader.forget(); + } + + loader = new HRTFDatabaseLoader(sampleRate); + entry->mLoader = loader; + + loader->loadAsynchronously(); + + return loader.forget(); +} + +HRTFDatabaseLoader::HRTFDatabaseLoader(float sampleRate) + : m_refCnt(0), + m_threadLock("HRTFDatabaseLoader"), + m_databaseLoaderThread(nullptr), + m_databaseSampleRate(sampleRate), + m_databaseLoaded(false) { + MOZ_ASSERT(NS_IsMainThread()); +} + +HRTFDatabaseLoader::~HRTFDatabaseLoader() { + MOZ_ASSERT(NS_IsMainThread()); + + waitForLoaderThreadCompletion(); + m_hrtfDatabase.reset(); + + if (s_loaderMap) { + // Remove ourself from the map. + s_loaderMap->RemoveEntry(m_databaseSampleRate); + if (s_loaderMap->Count() == 0) { + delete s_loaderMap; + s_loaderMap = nullptr; + } + } +} + +size_t HRTFDatabaseLoader::sizeOfIncludingThis( + mozilla::MallocSizeOf aMallocSizeOf) const { + size_t amount = aMallocSizeOf(this); + + // NB: Need to make sure we're not competing with the loader thread. + const_cast<HRTFDatabaseLoader*>(this)->waitForLoaderThreadCompletion(); + + if (m_hrtfDatabase) { + amount += m_hrtfDatabase->sizeOfIncludingThis(aMallocSizeOf); + } + + return amount; +} + +class HRTFDatabaseLoader::ProxyReleaseEvent final : public Runnable { + public: + explicit ProxyReleaseEvent(HRTFDatabaseLoader* loader) + : mozilla::Runnable("WebCore::HRTFDatabaseLoader::ProxyReleaseEvent"), + mLoader(loader) {} + NS_IMETHOD Run() override { + mLoader->MainThreadRelease(); + return NS_OK; + } + + private: + // Ownership transferred by ProxyRelease + HRTFDatabaseLoader* MOZ_OWNING_REF mLoader; +}; + +void HRTFDatabaseLoader::ProxyRelease() { + nsCOMPtr<nsIEventTarget> mainTarget = GetMainThreadSerialEventTarget(); + if (MOZ_LIKELY(mainTarget)) { + RefPtr<ProxyReleaseEvent> event = new ProxyReleaseEvent(this); + DebugOnly<nsresult> rv = mainTarget->Dispatch(event, NS_DISPATCH_NORMAL); + MOZ_ASSERT(NS_SUCCEEDED(rv), "Failed to dispatch release event"); + } else { + // Should be in XPCOM shutdown. + MOZ_ASSERT(NS_IsMainThread(), "Main thread is not available for dispatch."); + MainThreadRelease(); + } +} + +void HRTFDatabaseLoader::MainThreadRelease() { + MOZ_ASSERT(NS_IsMainThread()); + int count = --m_refCnt; + MOZ_ASSERT(count >= 0, "extra release"); + NS_LOG_RELEASE(this, count, "HRTFDatabaseLoader"); + if (count == 0) { + // It is safe to delete here as the first reference can only be added + // on this (main) thread. + delete this; + } +} + +// Asynchronously load the database in this thread. +static void databaseLoaderEntry(void* threadData) { + AUTO_PROFILER_REGISTER_THREAD("HRTFDatabaseLdr"); + NS_SetCurrentThreadName("HRTFDatabaseLdr"); + + HRTFDatabaseLoader* loader = + reinterpret_cast<HRTFDatabaseLoader*>(threadData); + MOZ_ASSERT(loader); + loader->load(); +} + +void HRTFDatabaseLoader::load() { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(!m_hrtfDatabase.get(), "Called twice"); + // Load the default HRTF database. + m_hrtfDatabase = HRTFDatabase::create(m_databaseSampleRate); + m_databaseLoaded = true; + // Notifies the main thread of completion. See loadAsynchronously(). + Release(); +} + +void HRTFDatabaseLoader::loadAsynchronously() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(m_refCnt, "Must not be called before a reference is added"); + + // Add a reference so that the destructor won't run and wait for the + // loader thread, until load() has completed. + AddRef(); + + MutexAutoLock locker(m_threadLock); + + MOZ_ASSERT(!m_hrtfDatabase.get() && !m_databaseLoaderThread, "Called twice"); + // Start the asynchronous database loading process. + m_databaseLoaderThread = PR_CreateThread( + PR_USER_THREAD, databaseLoaderEntry, this, PR_PRIORITY_NORMAL, + PR_GLOBAL_THREAD, PR_JOINABLE_THREAD, 0); +} + +bool HRTFDatabaseLoader::isLoaded() const { return m_hrtfDatabase.get(); } + +void HRTFDatabaseLoader::waitForLoaderThreadCompletion() { + MutexAutoLock locker(m_threadLock); + + // waitForThreadCompletion() should not be called twice for the same thread. + if (m_databaseLoaderThread) { + DebugOnly<PRStatus> status = PR_JoinThread(m_databaseLoaderThread); + MOZ_ASSERT(status == PR_SUCCESS, "PR_JoinThread failed"); + } + m_databaseLoaderThread = nullptr; +} + +void HRTFDatabaseLoader::shutdown() { + MOZ_ASSERT(NS_IsMainThread()); + if (s_loaderMap) { + // Set s_loaderMap to nullptr so that the hashtable is not modified on + // reference release during enumeration. + nsTHashtable<LoaderByRateEntry>* loaderMap = s_loaderMap; + s_loaderMap = nullptr; + for (auto iter = loaderMap->Iter(); !iter.Done(); iter.Next()) { + iter.Get()->mLoader->waitForLoaderThreadCompletion(); + } + delete loaderMap; + } +} + +} // namespace WebCore diff --git a/dom/media/webaudio/blink/HRTFDatabaseLoader.h b/dom/media/webaudio/blink/HRTFDatabaseLoader.h new file mode 100644 index 0000000000..5d4548d247 --- /dev/null +++ b/dom/media/webaudio/blink/HRTFDatabaseLoader.h @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2010 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. + * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of + * its contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY APPLE 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 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. + */ + +#ifndef HRTFDatabaseLoader_h +#define HRTFDatabaseLoader_h + +#include "nsHashKeys.h" +#include "mozilla/RefPtr.h" +#include "mozilla/MemoryReporting.h" +#include "mozilla/Mutex.h" +#include "HRTFDatabase.h" + +template <class EntryType> +class nsTHashtable; +template <class T> +class nsAutoRef; + +namespace WebCore { + +// HRTFDatabaseLoader will asynchronously load the default HRTFDatabase in a new +// thread. + +class HRTFDatabaseLoader { + public: + // Lazily creates a HRTFDatabaseLoader (if not already created) for the given + // sample-rate and starts loading asynchronously (when created the first + // time). Returns the HRTFDatabaseLoader. Must be called from the main thread. + static already_AddRefed<HRTFDatabaseLoader> + createAndLoadAsynchronouslyIfNecessary(float sampleRate); + + // AddRef and Release may be called from any thread. + void AddRef() { +#if defined(DEBUG) || defined(NS_BUILD_REFCNT_LOGGING) + int count = +#endif + ++m_refCnt; + MOZ_ASSERT(count > 0, "invalid ref count"); + NS_LOG_ADDREF(this, count, "HRTFDatabaseLoader", sizeof(*this)); + } + + void Release() { + // The last reference can't be removed on a non-main thread because + // the object can be accessed on the main thread from the hash + // table via createAndLoadAsynchronouslyIfNecessary(). + int count = m_refCnt; + MOZ_ASSERT(count > 0, "extra release"); + // Optimization attempt to possibly skip proxying the release to the + // main thread. + if (count != 1 && m_refCnt.compareExchange(count, count - 1)) { + NS_LOG_RELEASE(this, count - 1, "HRTFDatabaseLoader"); + return; + } + + ProxyRelease(); + } + + // Returns true once the default database has been completely loaded. + bool isLoaded() const; + + // waitForLoaderThreadCompletion() may be called more than once, + // on any thread except m_databaseLoaderThread. + void waitForLoaderThreadCompletion(); + + HRTFDatabase* database() { + if (!m_databaseLoaded) { + return nullptr; + } + return m_hrtfDatabase.get(); + } + + float databaseSampleRate() const { return m_databaseSampleRate; } + + static void shutdown(); + + // Called in asynchronous loading thread. + void load(); + + // Sums the size of all cached database loaders. + static size_t sizeOfLoaders(mozilla::MallocSizeOf aMallocSizeOf); + + private: + // Both constructor and destructor must be called from the main thread. + explicit HRTFDatabaseLoader(float sampleRate); + ~HRTFDatabaseLoader(); + + size_t sizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const; + + void ProxyRelease(); // any thread + void MainThreadRelease(); // main thread only + class ProxyReleaseEvent; + + // If it hasn't already been loaded, creates a new thread and initiates + // asynchronous loading of the default database. This must be called from the + // main thread. + void loadAsynchronously(); + + // Map from sample-rate to loader. + class LoaderByRateEntry : public nsFloatHashKey { + public: + explicit LoaderByRateEntry(KeyTypePointer aKey) + : nsFloatHashKey(aKey), + mLoader() // so PutEntry() will zero-initialize + {} + + size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const { + return mLoader ? mLoader->sizeOfIncludingThis(aMallocSizeOf) : 0; + } + + // The HRTFDatabaseLoader removes itself from s_loaderMap on destruction. + HRTFDatabaseLoader* MOZ_NON_OWNING_REF mLoader; + }; + + // Keeps track of loaders on a per-sample-rate basis. + static nsTHashtable<LoaderByRateEntry>* s_loaderMap; // singleton + + mozilla::Atomic<int> m_refCnt; + + nsAutoRef<HRTFDatabase> m_hrtfDatabase; + + // Holding a m_threadLock is required when accessing m_databaseLoaderThread. + mozilla::Mutex m_threadLock; + PRThread* m_databaseLoaderThread MOZ_GUARDED_BY(m_threadLock); + + float m_databaseSampleRate; + mozilla::Atomic<bool> m_databaseLoaded; +}; + +} // namespace WebCore + +#endif // HRTFDatabaseLoader_h diff --git a/dom/media/webaudio/blink/HRTFElevation.cpp b/dom/media/webaudio/blink/HRTFElevation.cpp new file mode 100644 index 0000000000..38ac0f8614 --- /dev/null +++ b/dom/media/webaudio/blink/HRTFElevation.cpp @@ -0,0 +1,317 @@ +/* + * Copyright (C) 2010 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. + * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of + * its contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY APPLE 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 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 "HRTFElevation.h" + +#include <speex/speex_resampler.h> +#include "mozilla/PodOperations.h" +#include "AudioSampleFormat.h" + +#include "IRC_Composite_C_R0195-incl.cpp" + +using namespace mozilla; + +namespace WebCore { + +const int elevationSpacing = irc_composite_c_r0195_elevation_interval; +const int firstElevation = irc_composite_c_r0195_first_elevation; +const int numberOfElevations = MOZ_ARRAY_LENGTH(irc_composite_c_r0195); + +const unsigned HRTFElevation::NumberOfTotalAzimuths = 360 / 15 * 8; + +const int rawSampleRate = irc_composite_c_r0195_sample_rate; + +// Number of frames in an individual impulse response. +const size_t ResponseFrameSize = 256; + +size_t HRTFElevation::sizeOfIncludingThis( + mozilla::MallocSizeOf aMallocSizeOf) const { + size_t amount = aMallocSizeOf(this); + + amount += m_kernelListL.ShallowSizeOfExcludingThis(aMallocSizeOf); + for (size_t i = 0; i < m_kernelListL.Length(); i++) { + amount += m_kernelListL[i]->sizeOfIncludingThis(aMallocSizeOf); + } + + return amount; +} + +size_t HRTFElevation::fftSizeForSampleRate(float sampleRate) { + // The IRCAM HRTF impulse responses were 512 sample-frames @44.1KHz, + // but these have been truncated to 256 samples. + // An FFT-size of twice impulse response size is used (for convolution). + // So for sample rates of 44.1KHz an FFT size of 512 is good. + // We double the FFT-size only for sample rates at least double this. + // If the FFT size is too large then the impulse response will be padded + // with zeros without the fade-out provided by HRTFKernel. + MOZ_ASSERT(sampleRate > 1.0 && sampleRate < 1048576.0); + + // This is the size if we were to use all raw response samples. + unsigned resampledLength = + floorf(ResponseFrameSize * sampleRate / rawSampleRate); + // Keep things semi-sane, with max FFT size of 1024. + unsigned size = std::min(resampledLength, 1023U); + // Ensure a minimum of 2 * WEBAUDIO_BLOCK_SIZE (with the size++ below) for + // FFTConvolver and set the 8 least significant bits for rounding up to + // the next power of 2 below. + size |= 2 * WEBAUDIO_BLOCK_SIZE - 1; + // Round up to the next power of 2, making the FFT size no more than twice + // the impulse response length. This doubles size for values that are + // already powers of 2. This works by filling in alls bit to right of the + // most significant bit. The most significant bit is no greater than + // 1 << 9, and the least significant 8 bits were already set above, so + // there is at most one bit to add. + size |= (size >> 1); + size++; + MOZ_ASSERT((size & (size - 1)) == 0); + + return size; +} + +nsReturnRef<HRTFKernel> HRTFElevation::calculateKernelForAzimuthElevation( + int azimuth, int elevation, SpeexResamplerState* resampler, + float sampleRate) { + int elevationIndex = (elevation - firstElevation) / elevationSpacing; + MOZ_ASSERT(elevationIndex >= 0 && elevationIndex <= numberOfElevations); + + int numberOfAzimuths = irc_composite_c_r0195[elevationIndex].count; + int azimuthSpacing = 360 / numberOfAzimuths; + MOZ_ASSERT(numberOfAzimuths * azimuthSpacing == 360); + + int azimuthIndex = azimuth / azimuthSpacing; + MOZ_ASSERT(azimuthIndex * azimuthSpacing == azimuth); + + const int16_t(&impulse_response_data)[ResponseFrameSize] = + irc_composite_c_r0195[elevationIndex].azimuths[azimuthIndex]; + + float response[ResponseFrameSize]; + ConvertAudioSamples(impulse_response_data, response, ResponseFrameSize); + float* resampledResponse; + + // Note that depending on the fftSize returned by the panner, we may be + // truncating the impulse response. + const size_t resampledResponseLength = fftSizeForSampleRate(sampleRate) / 2; + + AutoTArray<AudioDataValue, 2 * ResponseFrameSize> resampled; + if (sampleRate == rawSampleRate) { + resampledResponse = response; + MOZ_ASSERT(resampledResponseLength == ResponseFrameSize); + } else { + resampled.SetLength(resampledResponseLength); + resampledResponse = resampled.Elements(); + speex_resampler_skip_zeros(resampler); + + // Feed the input buffer into the resampler. + spx_uint32_t in_len = ResponseFrameSize; + spx_uint32_t out_len = resampled.Length(); + speex_resampler_process_float(resampler, 0, response, &in_len, + resampled.Elements(), &out_len); + + if (out_len < resampled.Length()) { + // The input should have all been processed. + MOZ_ASSERT(in_len == ResponseFrameSize); + // Feed in zeros get the data remaining in the resampler. + spx_uint32_t out_index = out_len; + in_len = speex_resampler_get_input_latency(resampler); + out_len = resampled.Length() - out_index; + speex_resampler_process_float(resampler, 0, nullptr, &in_len, + resampled.Elements() + out_index, &out_len); + out_index += out_len; + // There may be some uninitialized samples remaining for very low + // sample rates. + PodZero(resampled.Elements() + out_index, resampled.Length() - out_index); + } + + speex_resampler_reset_mem(resampler); + } + + return HRTFKernel::create(resampledResponse, resampledResponseLength, + sampleRate); +} + +// The range of elevations for the IRCAM impulse responses varies depending on +// azimuth, but the minimum elevation appears to always be -45. +// +// Here's how it goes: +static int maxElevations[] = { + // Azimuth + // + 90, // 0 + 45, // 15 + 60, // 30 + 45, // 45 + 75, // 60 + 45, // 75 + 60, // 90 + 45, // 105 + 75, // 120 + 45, // 135 + 60, // 150 + 45, // 165 + 75, // 180 + 45, // 195 + 60, // 210 + 45, // 225 + 75, // 240 + 45, // 255 + 60, // 270 + 45, // 285 + 75, // 300 + 45, // 315 + 60, // 330 + 45 // 345 +}; + +nsReturnRef<HRTFElevation> HRTFElevation::createBuiltin(int elevation, + float sampleRate) { + if (elevation < firstElevation || + elevation > firstElevation + numberOfElevations * elevationSpacing || + (elevation / elevationSpacing) * elevationSpacing != elevation) + return nsReturnRef<HRTFElevation>(); + + // Spacing, in degrees, between every azimuth loaded from resource. + // Some elevations do not have data for all these intervals. + // See maxElevations. + static const unsigned AzimuthSpacing = 15; + static const unsigned NumberOfRawAzimuths = 360 / AzimuthSpacing; + static_assert(AzimuthSpacing * NumberOfRawAzimuths == 360, "Not a multiple"); + static const unsigned InterpolationFactor = + NumberOfTotalAzimuths / NumberOfRawAzimuths; + static_assert( + NumberOfTotalAzimuths == NumberOfRawAzimuths * InterpolationFactor, + "Not a multiple"); + + HRTFKernelList kernelListL; + kernelListL.SetLength(NumberOfTotalAzimuths); + + SpeexResamplerState* resampler = + sampleRate == rawSampleRate + ? nullptr + : speex_resampler_init(1, rawSampleRate, sampleRate, + SPEEX_RESAMPLER_QUALITY_MIN, nullptr); + + // Load convolution kernels from HRTF files. + int interpolatedIndex = 0; + for (unsigned rawIndex = 0; rawIndex < NumberOfRawAzimuths; ++rawIndex) { + // Don't let elevation exceed maximum for this azimuth. + int maxElevation = maxElevations[rawIndex]; + int actualElevation = std::min(elevation, maxElevation); + + kernelListL[interpolatedIndex] = calculateKernelForAzimuthElevation( + rawIndex * AzimuthSpacing, actualElevation, resampler, sampleRate); + + interpolatedIndex += InterpolationFactor; + } + + if (resampler) speex_resampler_destroy(resampler); + + // Now go back and interpolate intermediate azimuth values. + for (unsigned i = 0; i < NumberOfTotalAzimuths; i += InterpolationFactor) { + int j = (i + InterpolationFactor) % NumberOfTotalAzimuths; + + // Create the interpolated convolution kernels and delays. + for (unsigned jj = 1; jj < InterpolationFactor; ++jj) { + float x = + float(jj) / float(InterpolationFactor); // interpolate from 0 -> 1 + + kernelListL[i + jj] = HRTFKernel::createInterpolatedKernel( + kernelListL[i], kernelListL[j], x); + } + } + + return nsReturnRef<HRTFElevation>( + new HRTFElevation(std::move(kernelListL), elevation, sampleRate)); +} + +nsReturnRef<HRTFElevation> HRTFElevation::createByInterpolatingSlices( + HRTFElevation* hrtfElevation1, HRTFElevation* hrtfElevation2, float x, + float sampleRate) { + MOZ_ASSERT(hrtfElevation1 && hrtfElevation2); + if (!hrtfElevation1 || !hrtfElevation2) return nsReturnRef<HRTFElevation>(); + + MOZ_ASSERT(x >= 0.0 && x < 1.0); + + HRTFKernelList kernelListL; + kernelListL.SetLength(NumberOfTotalAzimuths); + + const HRTFKernelList& kernelListL1 = hrtfElevation1->kernelListL(); + const HRTFKernelList& kernelListL2 = hrtfElevation2->kernelListL(); + + // Interpolate kernels of corresponding azimuths of the two elevations. + for (unsigned i = 0; i < NumberOfTotalAzimuths; ++i) { + kernelListL[i] = HRTFKernel::createInterpolatedKernel(kernelListL1[i], + kernelListL2[i], x); + } + + // Interpolate elevation angle. + double angle = (1.0 - x) * hrtfElevation1->elevationAngle() + + x * hrtfElevation2->elevationAngle(); + + return nsReturnRef<HRTFElevation>(new HRTFElevation( + std::move(kernelListL), static_cast<int>(angle), sampleRate)); +} + +void HRTFElevation::getKernelsFromAzimuth( + double azimuthBlend, unsigned azimuthIndex, HRTFKernel*& kernelL, + HRTFKernel*& kernelR, double& frameDelayL, double& frameDelayR) { + bool checkAzimuthBlend = azimuthBlend >= 0.0 && azimuthBlend < 1.0; + MOZ_ASSERT(checkAzimuthBlend); + if (!checkAzimuthBlend) azimuthBlend = 0.0; + + unsigned numKernels = m_kernelListL.Length(); + + bool isIndexGood = azimuthIndex < numKernels; + MOZ_ASSERT(isIndexGood); + if (!isIndexGood) { + kernelL = 0; + kernelR = 0; + return; + } + + // Return the left and right kernels, + // using symmetry to produce the right kernel. + kernelL = m_kernelListL[azimuthIndex]; + int azimuthIndexR = (numKernels - azimuthIndex) % numKernels; + kernelR = m_kernelListL[azimuthIndexR]; + + frameDelayL = kernelL->frameDelay(); + frameDelayR = kernelR->frameDelay(); + + int azimuthIndex2L = (azimuthIndex + 1) % numKernels; + double frameDelay2L = m_kernelListL[azimuthIndex2L]->frameDelay(); + int azimuthIndex2R = (numKernels - azimuthIndex2L) % numKernels; + double frameDelay2R = m_kernelListL[azimuthIndex2R]->frameDelay(); + + // Linearly interpolate delays. + frameDelayL = + (1.0 - azimuthBlend) * frameDelayL + azimuthBlend * frameDelay2L; + frameDelayR = + (1.0 - azimuthBlend) * frameDelayR + azimuthBlend * frameDelay2R; +} + +} // namespace WebCore diff --git a/dom/media/webaudio/blink/HRTFElevation.h b/dom/media/webaudio/blink/HRTFElevation.h new file mode 100644 index 0000000000..cd54bbc04a --- /dev/null +++ b/dom/media/webaudio/blink/HRTFElevation.h @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2010 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. + * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of + * its contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY APPLE 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 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. + */ + +#ifndef HRTFElevation_h +#define HRTFElevation_h + +#include "HRTFKernel.h" +#include "nsAutoRef.h" +#include "mozilla/MemoryReporting.h" + +struct SpeexResamplerState_; +typedef struct SpeexResamplerState_ SpeexResamplerState; + +namespace WebCore { + +// HRTFElevation contains all of the HRTFKernels (one left ear and one right ear +// per azimuth angle) for a particular elevation. + +class HRTFElevation { + public: + // Loads and returns an HRTFElevation with the given HRTF database subject + // name and elevation from browser (or WebKit.framework) resources. Normally, + // there will only be a single HRTF database set, but this API supports the + // possibility of multiple ones with different names. Interpolated azimuths + // will be generated based on InterpolationFactor. Valid values for elevation + // are -45 -> +90 in 15 degree increments. + static nsReturnRef<HRTFElevation> createBuiltin(int elevation, + float sampleRate); + + // Given two HRTFElevations, and an interpolation factor x: 0 -> 1, returns an + // interpolated HRTFElevation. + static nsReturnRef<HRTFElevation> createByInterpolatingSlices( + HRTFElevation* hrtfElevation1, HRTFElevation* hrtfElevation2, float x, + float sampleRate); + + double elevationAngle() const { return m_elevationAngle; } + unsigned numberOfAzimuths() const { return NumberOfTotalAzimuths; } + float sampleRate() const { return m_sampleRate; } + + // Returns the left and right kernels for the given azimuth index. + // The interpolated delays based on azimuthBlend: 0 -> 1 are returned in + // frameDelayL and frameDelayR. + void getKernelsFromAzimuth(double azimuthBlend, unsigned azimuthIndex, + HRTFKernel*& kernelL, HRTFKernel*& kernelR, + double& frameDelayL, double& frameDelayR); + + // Total number of azimuths after interpolation. + static const unsigned NumberOfTotalAzimuths; + + static size_t fftSizeForSampleRate(float sampleRate); + + size_t sizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const; + + private: + HRTFElevation(const HRTFElevation& other) = delete; + void operator=(const HRTFElevation& other) = delete; + + HRTFElevation(HRTFKernelList&& kernelListL, int elevation, float sampleRate) + : m_kernelListL(std::move(kernelListL)), + m_elevationAngle(elevation), + m_sampleRate(sampleRate) {} + + // Returns the list of left ear HRTFKernels for all the azimuths going from 0 + // to 360 degrees. + const HRTFKernelList& kernelListL() { return m_kernelListL; } + + // Given a specific azimuth and elevation angle, returns the left HRTFKernel. + // Values for azimuth must be multiples of 15 in 0 -> 345, + // but not all azimuths are available for elevations > +45. + // Valid values for elevation are -45 -> +90 in 15 degree increments. + static nsReturnRef<HRTFKernel> calculateKernelForAzimuthElevation( + int azimuth, int elevation, SpeexResamplerState* resampler, + float sampleRate); + + HRTFKernelList m_kernelListL; + double m_elevationAngle; + float m_sampleRate; +}; + +} // namespace WebCore + +template <> +class nsAutoRefTraits<WebCore::HRTFElevation> + : public nsPointerRefTraits<WebCore::HRTFElevation> { + public: + static void Release(WebCore::HRTFElevation* ptr) { delete (ptr); } +}; + +#endif // HRTFElevation_h diff --git a/dom/media/webaudio/blink/HRTFKernel.cpp b/dom/media/webaudio/blink/HRTFKernel.cpp new file mode 100644 index 0000000000..ecaa846a66 --- /dev/null +++ b/dom/media/webaudio/blink/HRTFKernel.cpp @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2010 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. + * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of + * its contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY APPLE 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 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 "HRTFKernel.h" +namespace WebCore { + +// Takes the input audio channel |impulseP| as an input impulse response and +// calculates the average group delay. This represents the initial delay before +// the most energetic part of the impulse response. The sample-frame delay is +// removed from the |impulseP| impulse response, and this value is returned. +// The |length| of the passed in |impulseP| must be must be a power of 2. +static float extractAverageGroupDelay(float* impulseP, size_t length) { + // Check for power-of-2. + MOZ_ASSERT(length && (length & (length - 1)) == 0); + + FFTBlock estimationFrame(length); + estimationFrame.PerformFFT(impulseP); + + float frameDelay = + static_cast<float>(estimationFrame.ExtractAverageGroupDelay()); + estimationFrame.GetInverse(impulseP); + + return frameDelay; +} + +HRTFKernel::HRTFKernel(float* impulseResponse, size_t length, float sampleRate) + : m_frameDelay(0), m_sampleRate(sampleRate) { + AlignedTArray<float> buffer; + // copy to a 32-byte aligned buffer + if (((uintptr_t)impulseResponse & 31) != 0) { + buffer.SetLength(length); + mozilla::PodCopy(buffer.Elements(), impulseResponse, length); + impulseResponse = buffer.Elements(); + } + + // Determine the leading delay (average group delay) for the response. + m_frameDelay = extractAverageGroupDelay(impulseResponse, length); + + // The FFT size (with zero padding) needs to be twice the response length + // in order to do proper convolution. + unsigned fftSize = 2 * length; + + // Quick fade-out (apply window) at truncation point + // because the impulse response has been truncated. + unsigned numberOfFadeOutFrames = static_cast<unsigned>( + sampleRate / 4410); // 10 sample-frames @44.1KHz sample-rate + MOZ_ASSERT(numberOfFadeOutFrames < length); + if (numberOfFadeOutFrames < length) { + for (unsigned i = length - numberOfFadeOutFrames; i < length; ++i) { + float x = + 1.0f - static_cast<float>(i - (length - numberOfFadeOutFrames)) / + numberOfFadeOutFrames; + impulseResponse[i] *= x; + } + } + + m_fftFrame = mozilla::MakeUnique<FFTBlock>(fftSize); + m_fftFrame->PadAndMakeScaledDFT(impulseResponse, length); +} + +// Interpolates two kernels with x: 0 -> 1 and returns the result. +nsReturnRef<HRTFKernel> HRTFKernel::createInterpolatedKernel( + HRTFKernel* kernel1, HRTFKernel* kernel2, float x) { + MOZ_ASSERT(kernel1 && kernel2); + if (!kernel1 || !kernel2) return nsReturnRef<HRTFKernel>(); + + MOZ_ASSERT(x >= 0.0 && x < 1.0); + x = mozilla::clamped(x, 0.0f, 1.0f); + + float sampleRate1 = kernel1->sampleRate(); + float sampleRate2 = kernel2->sampleRate(); + MOZ_ASSERT(sampleRate1 == sampleRate2); + if (sampleRate1 != sampleRate2) return nsReturnRef<HRTFKernel>(); + + float frameDelay = + (1 - x) * kernel1->frameDelay() + x * kernel2->frameDelay(); + + UniquePtr<FFTBlock> interpolatedFrame(FFTBlock::CreateInterpolatedBlock( + *kernel1->fftFrame(), *kernel2->fftFrame(), x)); + return HRTFKernel::create(std::move(interpolatedFrame), frameDelay, + sampleRate1); +} + +} // namespace WebCore diff --git a/dom/media/webaudio/blink/HRTFKernel.h b/dom/media/webaudio/blink/HRTFKernel.h new file mode 100644 index 0000000000..34479f6301 --- /dev/null +++ b/dom/media/webaudio/blink/HRTFKernel.h @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2010 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. + * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of + * its contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY APPLE 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 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. + */ + +#ifndef HRTFKernel_h +#define HRTFKernel_h + +#include "nsAutoRef.h" +#include "nsTArray.h" +#include "mozilla/FFTBlock.h" +#include "mozilla/MemoryReporting.h" +#include "mozilla/UniquePtr.h" + +namespace WebCore { + +using mozilla::FFTBlock; +using mozilla::UniquePtr; + +// HRTF stands for Head-Related Transfer Function. +// HRTFKernel is a frequency-domain representation of an impulse-response used +// as part of the spatialized panning system. For a given azimuth / elevation +// angle there will be one HRTFKernel for the left ear transfer function, and +// one for the right ear. The leading delay (average group delay) for each +// impulse response is extracted: +// m_fftFrame is the frequency-domain representation of the impulse +// response with the delay removed +// m_frameDelay is the leading delay of the original impulse response. +class HRTFKernel { + public: + // Note: this is destructive on the passed in |impulseResponse|. + // The |length| of |impulseResponse| must be a power of two. + // The size of the DFT will be |2 * length|. + static nsReturnRef<HRTFKernel> create(float* impulseResponse, size_t length, + float sampleRate); + + static nsReturnRef<HRTFKernel> create(UniquePtr<FFTBlock> fftFrame, + float frameDelay, float sampleRate); + + // Given two HRTFKernels, and an interpolation factor x: 0 -> 1, returns an + // interpolated HRTFKernel. + static nsReturnRef<HRTFKernel> createInterpolatedKernel(HRTFKernel* kernel1, + HRTFKernel* kernel2, + float x); + + FFTBlock* fftFrame() { return m_fftFrame.get(); } + + size_t fftSize() const { return m_fftFrame->FFTSize(); } + float frameDelay() const { return m_frameDelay; } + + float sampleRate() const { return m_sampleRate; } + double nyquist() const { return 0.5 * sampleRate(); } + + size_t sizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const { + size_t amount = aMallocSizeOf(this); + amount += m_fftFrame->SizeOfIncludingThis(aMallocSizeOf); + return amount; + } + + private: + HRTFKernel(const HRTFKernel& other) = delete; + void operator=(const HRTFKernel& other) = delete; + + // Note: this is destructive on the passed in |impulseResponse|. + HRTFKernel(float* impulseResponse, size_t fftSize, float sampleRate); + + HRTFKernel(UniquePtr<FFTBlock> fftFrame, float frameDelay, float sampleRate) + : m_fftFrame(std::move(fftFrame)), + m_frameDelay(frameDelay), + m_sampleRate(sampleRate) {} + + UniquePtr<FFTBlock> m_fftFrame; + float m_frameDelay; + float m_sampleRate; +}; + +typedef nsTArray<nsAutoRef<HRTFKernel> > HRTFKernelList; + +} // namespace WebCore + +template <> +class nsAutoRefTraits<WebCore::HRTFKernel> + : public nsPointerRefTraits<WebCore::HRTFKernel> { + public: + static void Release(WebCore::HRTFKernel* ptr) { delete (ptr); } +}; + +namespace WebCore { + +inline nsReturnRef<HRTFKernel> HRTFKernel::create(float* impulseResponse, + size_t length, + float sampleRate) { + return nsReturnRef<HRTFKernel>( + new HRTFKernel(impulseResponse, length, sampleRate)); +} + +inline nsReturnRef<HRTFKernel> HRTFKernel::create(UniquePtr<FFTBlock> fftFrame, + float frameDelay, + float sampleRate) { + return nsReturnRef<HRTFKernel>( + new HRTFKernel(std::move(fftFrame), frameDelay, sampleRate)); +} + +} // namespace WebCore + +#endif // HRTFKernel_h diff --git a/dom/media/webaudio/blink/HRTFPanner.cpp b/dom/media/webaudio/blink/HRTFPanner.cpp new file mode 100644 index 0000000000..3d01fb5b39 --- /dev/null +++ b/dom/media/webaudio/blink/HRTFPanner.cpp @@ -0,0 +1,328 @@ +/* + * Copyright (C) 2010, 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 "HRTFPanner.h" +#include "HRTFDatabaseLoader.h" + +#include "FFTConvolver.h" +#include "HRTFDatabase.h" +#include "AudioBlock.h" + +using namespace mozilla; +using dom::ChannelInterpretation; + +namespace WebCore { + +// The value of 2 milliseconds is larger than the largest delay which exists in +// any HRTFKernel from the default HRTFDatabase (0.0136 seconds). We ASSERT the +// delay values used in process() with this value. +const float MaxDelayTimeSeconds = 0.002f; + +const int UninitializedAzimuth = -1; + +HRTFPanner::HRTFPanner(float sampleRate, + already_AddRefed<HRTFDatabaseLoader> databaseLoader) + : m_databaseLoader(databaseLoader), + m_sampleRate(sampleRate), + m_crossfadeSelection(CrossfadeSelection1), + m_azimuthIndex1(UninitializedAzimuth), + m_azimuthIndex2(UninitializedAzimuth) + // m_elevation1 and m_elevation2 are initialized in pan() + , + m_crossfadeX(0), + m_crossfadeIncr(0), + m_convolverL1(HRTFElevation::fftSizeForSampleRate(sampleRate)), + m_convolverR1(m_convolverL1.fftSize()), + m_convolverL2(m_convolverL1.fftSize()), + m_convolverR2(m_convolverL1.fftSize()), + m_delayLine(MaxDelayTimeSeconds * sampleRate) { + MOZ_ASSERT(m_databaseLoader); + MOZ_COUNT_CTOR(HRTFPanner); +} + +HRTFPanner::~HRTFPanner() { MOZ_COUNT_DTOR(HRTFPanner); } + +size_t HRTFPanner::sizeOfIncludingThis( + mozilla::MallocSizeOf aMallocSizeOf) const { + size_t amount = aMallocSizeOf(this); + + // NB: m_databaseLoader can be shared, so it is not measured here + amount += m_convolverL1.sizeOfExcludingThis(aMallocSizeOf); + amount += m_convolverR1.sizeOfExcludingThis(aMallocSizeOf); + amount += m_convolverL2.sizeOfExcludingThis(aMallocSizeOf); + amount += m_convolverR2.sizeOfExcludingThis(aMallocSizeOf); + amount += m_delayLine.SizeOfExcludingThis(aMallocSizeOf); + + return amount; +} + +void HRTFPanner::reset() { + m_azimuthIndex1 = UninitializedAzimuth; + m_azimuthIndex2 = UninitializedAzimuth; + // m_elevation1 and m_elevation2 are initialized in pan() + m_crossfadeSelection = CrossfadeSelection1; + m_crossfadeX = 0.0f; + m_crossfadeIncr = 0.0f; + m_convolverL1.reset(); + m_convolverR1.reset(); + m_convolverL2.reset(); + m_convolverR2.reset(); + m_delayLine.Reset(); +} + +int HRTFPanner::calculateDesiredAzimuthIndexAndBlend(double azimuth, + double& azimuthBlend) { + // Convert the azimuth angle from the range -180 -> +180 into the range 0 -> + // 360. The azimuth index may then be calculated from this positive value. + if (azimuth < 0) azimuth += 360.0; + + int numberOfAzimuths = HRTFDatabase::numberOfAzimuths(); + const double angleBetweenAzimuths = 360.0 / numberOfAzimuths; + + // Calculate the azimuth index and the blend (0 -> 1) for interpolation. + double desiredAzimuthIndexFloat = azimuth / angleBetweenAzimuths; + int desiredAzimuthIndex = static_cast<int>(desiredAzimuthIndexFloat); + azimuthBlend = + desiredAzimuthIndexFloat - static_cast<double>(desiredAzimuthIndex); + + // We don't immediately start using this azimuth index, but instead approach + // this index from the last index we rendered at. This minimizes the clicks + // and graininess for moving sources which occur otherwise. + desiredAzimuthIndex = std::max(0, desiredAzimuthIndex); + desiredAzimuthIndex = std::min(numberOfAzimuths - 1, desiredAzimuthIndex); + return desiredAzimuthIndex; +} + +void HRTFPanner::pan(double desiredAzimuth, double elevation, + const AudioBlock* inputBus, AudioBlock* outputBus) { +#ifdef DEBUG + unsigned numInputChannels = inputBus->IsNull() ? 0 : inputBus->ChannelCount(); + + MOZ_ASSERT(numInputChannels <= 2); + MOZ_ASSERT(inputBus->GetDuration() == WEBAUDIO_BLOCK_SIZE); +#endif + + bool isOutputGood = outputBus && outputBus->ChannelCount() == 2 && + outputBus->GetDuration() == WEBAUDIO_BLOCK_SIZE; + MOZ_ASSERT(isOutputGood); + + if (!isOutputGood) { + if (outputBus) outputBus->SetNull(outputBus->GetDuration()); + return; + } + + HRTFDatabase* database = m_databaseLoader->database(); + if (!database) { // not yet loaded + outputBus->SetNull(outputBus->GetDuration()); + return; + } + + // IRCAM HRTF azimuths values from the loaded database is reversed from the + // panner's notion of azimuth. + double azimuth = -desiredAzimuth; + + bool isAzimuthGood = azimuth >= -180.0 && azimuth <= 180.0; + MOZ_ASSERT(isAzimuthGood); + if (!isAzimuthGood) { + outputBus->SetNull(outputBus->GetDuration()); + return; + } + + // Normally, we'll just be dealing with mono sources. + // If we have a stereo input, implement stereo panning with left source + // processed by left HRTF, and right source by right HRTF. + + // Get destination pointers. + float* destinationL = + static_cast<float*>(const_cast<void*>(outputBus->mChannelData[0])); + float* destinationR = + static_cast<float*>(const_cast<void*>(outputBus->mChannelData[1])); + + double azimuthBlend; + int desiredAzimuthIndex = + calculateDesiredAzimuthIndexAndBlend(azimuth, azimuthBlend); + + // Initially snap azimuth and elevation values to first values encountered. + if (m_azimuthIndex1 == UninitializedAzimuth) { + m_azimuthIndex1 = desiredAzimuthIndex; + m_elevation1 = elevation; + } + if (m_azimuthIndex2 == UninitializedAzimuth) { + m_azimuthIndex2 = desiredAzimuthIndex; + m_elevation2 = elevation; + } + + // Cross-fade / transition over a period of around 45 milliseconds. + // This is an empirical value tuned to be a reasonable trade-off between + // smoothness and speed. + const double fadeFrames = sampleRate() <= 48000 ? 2048 : 4096; + + // Check for azimuth and elevation changes, initiating a cross-fade if needed. + if (!m_crossfadeX && m_crossfadeSelection == CrossfadeSelection1) { + if (desiredAzimuthIndex != m_azimuthIndex1 || elevation != m_elevation1) { + // Cross-fade from 1 -> 2 + m_crossfadeIncr = 1 / fadeFrames; + m_azimuthIndex2 = desiredAzimuthIndex; + m_elevation2 = elevation; + } + } + if (m_crossfadeX == 1 && m_crossfadeSelection == CrossfadeSelection2) { + if (desiredAzimuthIndex != m_azimuthIndex2 || elevation != m_elevation2) { + // Cross-fade from 2 -> 1 + m_crossfadeIncr = -1 / fadeFrames; + m_azimuthIndex1 = desiredAzimuthIndex; + m_elevation1 = elevation; + } + } + + // Get the HRTFKernels and interpolated delays. + HRTFKernel* kernelL1; + HRTFKernel* kernelR1; + HRTFKernel* kernelL2; + HRTFKernel* kernelR2; + double frameDelayL1; + double frameDelayR1; + double frameDelayL2; + double frameDelayR2; + database->getKernelsFromAzimuthElevation(azimuthBlend, m_azimuthIndex1, + m_elevation1, kernelL1, kernelR1, + frameDelayL1, frameDelayR1); + database->getKernelsFromAzimuthElevation(azimuthBlend, m_azimuthIndex2, + m_elevation2, kernelL2, kernelR2, + frameDelayL2, frameDelayR2); + + bool areKernelsGood = kernelL1 && kernelR1 && kernelL2 && kernelR2; + MOZ_ASSERT(areKernelsGood); + if (!areKernelsGood) { + outputBus->SetNull(outputBus->GetDuration()); + return; + } + + MOZ_ASSERT(frameDelayL1 / sampleRate() < MaxDelayTimeSeconds && + frameDelayR1 / sampleRate() < MaxDelayTimeSeconds); + MOZ_ASSERT(frameDelayL2 / sampleRate() < MaxDelayTimeSeconds && + frameDelayR2 / sampleRate() < MaxDelayTimeSeconds); + + // Crossfade inter-aural delays based on transitions. + float frameDelaysL[WEBAUDIO_BLOCK_SIZE]; + float frameDelaysR[WEBAUDIO_BLOCK_SIZE]; + { + float x = m_crossfadeX; + float incr = m_crossfadeIncr; + for (unsigned i = 0; i < WEBAUDIO_BLOCK_SIZE; ++i) { + frameDelaysL[i] = (1 - x) * frameDelayL1 + x * frameDelayL2; + frameDelaysR[i] = (1 - x) * frameDelayR1 + x * frameDelayR2; + x += incr; + } + } + + // First run through delay lines for inter-aural time difference. + m_delayLine.Write(*inputBus); + // "Speakers" means a mono input is read into both outputs (with possibly + // different delays). + m_delayLine.ReadChannel(frameDelaysL, outputBus, 0, + ChannelInterpretation::Speakers); + m_delayLine.ReadChannel(frameDelaysR, outputBus, 1, + ChannelInterpretation::Speakers); + m_delayLine.NextBlock(); + + bool needsCrossfading = m_crossfadeIncr; + + const float* convolutionDestinationL1; + const float* convolutionDestinationR1; + const float* convolutionDestinationL2; + const float* convolutionDestinationR2; + + // Now do the convolutions. + // Note that we avoid doing convolutions on both sets of convolvers if we're + // not currently cross-fading. + + if (m_crossfadeSelection == CrossfadeSelection1 || needsCrossfading) { + convolutionDestinationL1 = + m_convolverL1.process(kernelL1->fftFrame(), destinationL); + convolutionDestinationR1 = + m_convolverR1.process(kernelR1->fftFrame(), destinationR); + } + + if (m_crossfadeSelection == CrossfadeSelection2 || needsCrossfading) { + convolutionDestinationL2 = + m_convolverL2.process(kernelL2->fftFrame(), destinationL); + convolutionDestinationR2 = + m_convolverR2.process(kernelR2->fftFrame(), destinationR); + } + + if (needsCrossfading) { + // Apply linear cross-fade. + float x = m_crossfadeX; + float incr = m_crossfadeIncr; + for (unsigned i = 0; i < WEBAUDIO_BLOCK_SIZE; ++i) { + destinationL[i] = (1 - x) * convolutionDestinationL1[i] + + x * convolutionDestinationL2[i]; + destinationR[i] = (1 - x) * convolutionDestinationR1[i] + + x * convolutionDestinationR2[i]; + x += incr; + } + // Update cross-fade value from local. + m_crossfadeX = x; + + if (m_crossfadeIncr > 0 && fabs(m_crossfadeX - 1) < m_crossfadeIncr) { + // We've fully made the crossfade transition from 1 -> 2. + m_crossfadeSelection = CrossfadeSelection2; + m_crossfadeX = 1; + m_crossfadeIncr = 0; + } else if (m_crossfadeIncr < 0 && fabs(m_crossfadeX) < -m_crossfadeIncr) { + // We've fully made the crossfade transition from 2 -> 1. + m_crossfadeSelection = CrossfadeSelection1; + m_crossfadeX = 0; + m_crossfadeIncr = 0; + } + } else { + const float* sourceL; + const float* sourceR; + if (m_crossfadeSelection == CrossfadeSelection1) { + sourceL = convolutionDestinationL1; + sourceR = convolutionDestinationR1; + } else { + sourceL = convolutionDestinationL2; + sourceR = convolutionDestinationR2; + } + PodCopy(destinationL, sourceL, WEBAUDIO_BLOCK_SIZE); + PodCopy(destinationR, sourceR, WEBAUDIO_BLOCK_SIZE); + } +} + +int HRTFPanner::maxTailFrames() const { + // Although the ideal tail time would be the length of the impulse + // response, there is additional tail time from the approximations in the + // implementation. Because HRTFPanner is implemented with a DelayKernel + // and a FFTConvolver, the tailTime of the HRTFPanner is the sum of the + // tailTime of the DelayKernel and the tailTime of the FFTConvolver. The + // FFTs of the convolver are fftSize(), half of which is latency, but this + // is aligned with blocks and so is reduced by the one block which is + // processed immediately. + return m_delayLine.MaxDelayTicks() + m_convolverL1.fftSize() / 2 + + m_convolverL1.latencyFrames(); +} + +} // namespace WebCore diff --git a/dom/media/webaudio/blink/HRTFPanner.h b/dom/media/webaudio/blink/HRTFPanner.h new file mode 100644 index 0000000000..b30689557b --- /dev/null +++ b/dom/media/webaudio/blink/HRTFPanner.h @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2010, 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. + */ + +#ifndef HRTFPanner_h +#define HRTFPanner_h + +#include "FFTConvolver.h" +#include "DelayBuffer.h" +#include "mozilla/MemoryReporting.h" + +namespace mozilla { +class AudioBlock; +} // namespace mozilla + +namespace WebCore { + +typedef nsTArray<float> AudioFloatArray; + +class HRTFDatabaseLoader; + +using mozilla::AudioBlock; + +class HRTFPanner { + public: + HRTFPanner(float sampleRate, + already_AddRefed<HRTFDatabaseLoader> databaseLoader); + ~HRTFPanner(); + + // chunk durations must be 128 + void pan(double azimuth, double elevation, const AudioBlock* inputBus, + AudioBlock* outputBus); + void reset(); + + size_t fftSize() const { return m_convolverL1.fftSize(); } + + float sampleRate() const { return m_sampleRate; } + + int maxTailFrames() const; + + size_t sizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const; + + private: + // Given an azimuth angle in the range -180 -> +180, returns the corresponding + // azimuth index for the database, and azimuthBlend which is an interpolation + // value from 0 -> 1. + int calculateDesiredAzimuthIndexAndBlend(double azimuth, + double& azimuthBlend); + + RefPtr<HRTFDatabaseLoader> m_databaseLoader; + + float m_sampleRate; + + // We maintain two sets of convolvers for smooth cross-faded interpolations + // when then azimuth and elevation are dynamically changing. When the azimuth + // and elevation are not changing, we simply process with one of the two sets. + // Initially we use CrossfadeSelection1 corresponding to m_convolverL1 and + // m_convolverR1. Whenever the azimuth or elevation changes, a crossfade is + // initiated to transition to the new position. So if we're currently + // processing with CrossfadeSelection1, then we transition to + // CrossfadeSelection2 (and vice versa). If we're in the middle of a + // transition, then we wait until it is complete before initiating a new + // transition. + + // Selects either the convolver set (m_convolverL1, m_convolverR1) or + // (m_convolverL2, m_convolverR2). + enum CrossfadeSelection { CrossfadeSelection1, CrossfadeSelection2 }; + + CrossfadeSelection m_crossfadeSelection; + + // azimuth/elevation for CrossfadeSelection1. + int m_azimuthIndex1; + double m_elevation1; + + // azimuth/elevation for CrossfadeSelection2. + int m_azimuthIndex2; + double m_elevation2; + + // A crossfade value 0 <= m_crossfadeX <= 1. + float m_crossfadeX; + + // Per-sample-frame crossfade value increment. + float m_crossfadeIncr; + + FFTConvolver m_convolverL1; + FFTConvolver m_convolverR1; + FFTConvolver m_convolverL2; + FFTConvolver m_convolverR2; + + mozilla::DelayBuffer m_delayLine; + + AudioFloatArray m_tempL1; + AudioFloatArray m_tempR1; + AudioFloatArray m_tempL2; + AudioFloatArray m_tempR2; +}; + +} // namespace WebCore + +#endif // HRTFPanner_h diff --git a/dom/media/webaudio/blink/IIRFilter.cpp b/dom/media/webaudio/blink/IIRFilter.cpp new file mode 100644 index 0000000000..5ea027e83a --- /dev/null +++ b/dom/media/webaudio/blink/IIRFilter.cpp @@ -0,0 +1,181 @@ +// Copyright 2016 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 "IIRFilter.h" + +#include "DenormalDisabler.h" +#include "fdlibm.h" +#include "mozilla/FloatingPoint.h" + +#include <mozilla/Assertions.h> + +#include <complex> + +namespace blink { + +// The length of the memory buffers for the IIR filter. This MUST be a power of +// two and must be greater than the possible length of the filter coefficients. +const int kBufferLength = 32; +static_assert(kBufferLength >= IIRFilter::kMaxOrder + 1, + "Internal IIR buffer length must be greater than maximum IIR " + "Filter order."); + +IIRFilter::IIRFilter(const AudioDoubleArray* feedforwardCoef, + const AudioDoubleArray* feedbackCoef) + : m_bufferIndex(0), + m_feedback(feedbackCoef), + m_feedforward(feedforwardCoef) { + m_xBuffer.SetLength(kBufferLength); + m_yBuffer.SetLength(kBufferLength); + reset(); +} + +IIRFilter::~IIRFilter() = default; + +void IIRFilter::reset() { + memset(m_xBuffer.Elements(), 0, m_xBuffer.Length() * sizeof(double)); + memset(m_yBuffer.Elements(), 0, m_yBuffer.Length() * sizeof(double)); +} + +static std::complex<double> evaluatePolynomial(const double* coef, + std::complex<double> z, + int order) { + // Use Horner's method to evaluate the polynomial P(z) = sum(coef[k]*z^k, k, + // 0, order); + std::complex<double> result = 0; + + for (int k = order; k >= 0; --k) + result = result * z + std::complex<double>(coef[k]); + + return result; +} + +void IIRFilter::process(const float* sourceP, float* destP, + size_t framesToProcess) { + // Compute + // + // y[n] = sum(b[k] * x[n - k], k = 0, M) - sum(a[k] * y[n - k], k = 1, N) + // + // where b[k] are the feedforward coefficients and a[k] are the feedback + // coefficients of the filter. + + // This is a Direct Form I implementation of an IIR Filter. Should we + // consider doing a different implementation such as Transposed Direct Form + // II? + const double* feedback = m_feedback->Elements(); + const double* feedforward = m_feedforward->Elements(); + + MOZ_ASSERT(feedback); + MOZ_ASSERT(feedforward); + + // Sanity check to see if the feedback coefficients have been scaled + // appropriately. It must be EXACTLY 1! + MOZ_ASSERT(feedback[0] == 1); + + int feedbackLength = m_feedback->Length(); + int feedforwardLength = m_feedforward->Length(); + int minLength = std::min(feedbackLength, feedforwardLength); + + double* xBuffer = m_xBuffer.Elements(); + double* yBuffer = m_yBuffer.Elements(); + + for (size_t n = 0; n < framesToProcess; ++n) { + // To help minimize roundoff, we compute using double's, even though the + // filter coefficients only have single precision values. + double yn = feedforward[0] * sourceP[n]; + + // Run both the feedforward and feedback terms together, when possible. + for (int k = 1; k < minLength; ++k) { + int n = (m_bufferIndex - k) & (kBufferLength - 1); + yn += feedforward[k] * xBuffer[n]; + yn -= feedback[k] * yBuffer[n]; + } + + // Handle any remaining feedforward or feedback terms. + for (int k = minLength; k < feedforwardLength; ++k) + yn += feedforward[k] * xBuffer[(m_bufferIndex - k) & (kBufferLength - 1)]; + + for (int k = minLength; k < feedbackLength; ++k) + yn -= feedback[k] * yBuffer[(m_bufferIndex - k) & (kBufferLength - 1)]; + + // Save the current input and output values in the memory buffers for the + // next output. + m_xBuffer[m_bufferIndex] = sourceP[n]; + m_yBuffer[m_bufferIndex] = yn; + + m_bufferIndex = (m_bufferIndex + 1) & (kBufferLength - 1); + + // Avoid introducing a stream of subnormals + destP[n] = WebCore::DenormalDisabler::flushDenormalFloatToZero(yn); + MOZ_ASSERT(destP[n] == 0.0 || std::fabs(destP[n]) > FLT_MIN || + std::isnan(destP[n]), + "output should not be subnormal, but can be NaN"); + } +} + +void IIRFilter::getFrequencyResponse(int nFrequencies, const float* frequency, + float* magResponse, float* phaseResponse) { + // Evaluate the z-transform of the filter at the given normalized frequencies + // from 0 to 1. (One corresponds to the Nyquist frequency.) + // + // The z-tranform of the filter is + // + // H(z) = sum(b[k]*z^(-k), k, 0, M) / sum(a[k]*z^(-k), k, 0, N); + // + // The desired frequency response is H(exp(j*omega)), where omega is in + // [0, 1). + // + // Let P(x) = sum(c[k]*x^k, k, 0, P) be a polynomial of order P. Then each of + // the sums in H(z) is equivalent to evaluating a polynomial at the point 1/z. + + for (int k = 0; k < nFrequencies; ++k) { + // zRecip = 1/z = exp(-j*frequency) + double omega = -M_PI * frequency[k]; + std::complex<double> zRecip = + std::complex<double>(fdlibm_cos(omega), fdlibm_sin(omega)); + + std::complex<double> numerator = evaluatePolynomial( + m_feedforward->Elements(), zRecip, m_feedforward->Length() - 1); + std::complex<double> denominator = evaluatePolynomial( + m_feedback->Elements(), zRecip, m_feedback->Length() - 1); + // Strangely enough, using complex division: + // e.g. Complex response = numerator / denominator; + // fails on our test machines, yielding infinities and NaNs, so we do + // things the long way here. + double n = norm(denominator); + double r = (real(numerator) * real(denominator) + + imag(numerator) * imag(denominator)) / + n; + double i = (imag(numerator) * real(denominator) - + real(numerator) * imag(denominator)) / + n; + std::complex<double> response = std::complex<double>(r, i); + + magResponse[k] = + static_cast<float>(fdlibm_hypot(real(response), imag(response))); + phaseResponse[k] = + static_cast<float>(fdlibm_atan2(imag(response), real(response))); + } +} + +bool IIRFilter::buffersAreZero() { + double* xBuffer = m_xBuffer.Elements(); + double* yBuffer = m_yBuffer.Elements(); + + for (size_t k = 0; k < m_feedforward->Length(); ++k) { + if (xBuffer[(m_bufferIndex - k) & (kBufferLength - 1)] != 0.0) { + return false; + } + } + + for (size_t k = 0; k < m_feedback->Length(); ++k) { + if (fabs(yBuffer[(m_bufferIndex - k) & (kBufferLength - 1)]) >= FLT_MIN) { + return false; + } + } + + return true; +} + +} // namespace blink diff --git a/dom/media/webaudio/blink/IIRFilter.h b/dom/media/webaudio/blink/IIRFilter.h new file mode 100644 index 0000000000..87522add06 --- /dev/null +++ b/dom/media/webaudio/blink/IIRFilter.h @@ -0,0 +1,62 @@ +// Copyright 2016 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. + +#ifndef IIRFilter_h +#define IIRFilter_h + +#include "nsTArray.h" + +typedef nsTArray<double> AudioDoubleArray; + +namespace blink { + +class IIRFilter final { + public: + // The maximum IIR filter order. This also limits the number of feedforward + // coefficients. The maximum number of coefficients is 20 according to the + // spec. + const static size_t kMaxOrder = 19; + IIRFilter(const AudioDoubleArray* feedforwardCoef, + const AudioDoubleArray* feedbackCoef); + ~IIRFilter(); + + void process(const float* sourceP, float* destP, size_t framesToProcess); + + void reset(); + + void getFrequencyResponse(int nFrequencies, const float* frequency, + float* magResponse, float* phaseResponse); + + bool buffersAreZero(); + + private: + // Filter memory + // + // For simplicity, we assume |m_xBuffer| and |m_yBuffer| have the same length, + // and the length is a power of two. Since the number of coefficients has a + // fixed upper length, the size of xBuffer and yBuffer is fixed. |m_xBuffer| + // holds the old input values and |m_yBuffer| holds the old output values + // needed to compute the new output value. + // + // m_yBuffer[m_bufferIndex] holds the most recent output value, say, y[n]. + // Then m_yBuffer[m_bufferIndex - k] is y[n - k]. Similarly for m_xBuffer. + // + // To minimize roundoff, these arrays are double's instead of floats. + AudioDoubleArray m_xBuffer; + AudioDoubleArray m_yBuffer; + + // Index into the xBuffer and yBuffer arrays where the most current x and y + // values should be stored. xBuffer[bufferIndex] corresponds to x[n], the + // current x input value and yBuffer[bufferIndex] is where y[n], the current + // output value. + int m_bufferIndex; + + // Coefficients of the IIR filter. + const AudioDoubleArray* m_feedback; + const AudioDoubleArray* m_feedforward; +}; + +} // namespace blink + +#endif // IIRFilter_h diff --git a/dom/media/webaudio/blink/IRC_Composite_C_R0195-incl.cpp b/dom/media/webaudio/blink/IRC_Composite_C_R0195-incl.cpp new file mode 100644 index 0000000000..37d731d3a2 --- /dev/null +++ b/dom/media/webaudio/blink/IRC_Composite_C_R0195-incl.cpp @@ -0,0 +1,4571 @@ +/* -*- 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/. */ + +/** + * The sample data in the arrays here was derived for Webkit by Chris Rogers + * through averaging of impulse responses from the IRCAM Listen HRTF Database. + * The responses here are half the length of the Listen responses. This + * sample data has been granted to the Public Domain. + * + * This file is intended to be included in compilation of a single + * implementation file. + * + * Each elevation (p) contains impulse responses at a varying number of + * equally spaced azimuths for the left ear, ordered clockwise from in front + * the listener. + */ + +#include "mozilla/ArrayUtils.h" + +using mozilla::ArrayLength; + +const int16_t irc_composite_c_r0195_p315[][256] = + {/* IRC_Composite_C_R0195_T000_P315.wav */ + {-37, 37, -38, 39, -39, 40, -41, 42, -42, 43, -43, + 44, -44, 44, -44, 44, -43, 42, -39, 36, -31, 23, + -10, -10, 5, -655, -410, -552, -353, -474, -1525, -758, 656, + -263, 70, 1414, -1528, -731, -1811, 1646, 1312, -1501, -407, 8893, + -1543, 7696, 8084, 2629, -2452, -234, 3799, 1676, 177, -1077, -474, + -1325, 3527, 985, 265, -884, 373, 971, 1024, -412, 507, -173, + 259, -799, 635, -628, 507, -344, 394, -359, 178, -276, 349, + -201, 137, -249, 377, -311, 263, -404, 284, -244, 173, -243, + 330, -320, 112, -150, 164, -142, 174, -300, 158, -197, 13, + -141, 85, -190, 64, -122, 41, -122, 60, -195, 125, -163, + 10, -67, -6, -122, 77, -133, 26, -71, -42, -156, 48, + -152, -12, -89, -120, -104, -37, -154, -57, -139, -80, -165, + -95, -242, -81, -146, -111, -178, -109, -208, -48, -178, -131, + -163, -68, -169, -94, -190, -139, -190, -118, -204, -160, -220, + -140, -204, -171, -238, -126, -203, -114, -209, -138, -177, -124, + -184, -130, -175, -170, -185, -180, -231, -189, -233, -210, -236, + -245, -288, -208, -329, -246, -274, -199, -273, -189, -267, -208, + -215, -199, -187, -209, -206, -210, -123, -197, -156, -173, -142, + -97, -123, -97, -107, -73, -84, -39, -50, -66, -11, -50, + -12, -51, 8, -27, 19, -48, -9, -18, 5, -42, -15, + -35, -31, -27, -27, -64, -33, -54, -1, -98, -47, -56, + -7, -76, -47, -70, -42, -54, -65, -76, -43, -57, -9, + -61, -39, -58, 33, -39, 3, -34, 20, -19, 4, -71, + 61, -22, 10}, + /* IRC_Composite_C_R0195_T015_P315.wav */ + {81, -82, 83, -84, 84, -85, 86, -87, 87, -87, 87, + -87, 87, -86, 84, -82, 78, -72, 63, -49, 23, 33, + -344, -481, -563, -443, -265, -1527, -713, 251, -1453, 939, 2510, + -1221, -1282, -1307, 806, 585, -990, -82, 9029, -4621, 9096, 11230, + 4611, -3051, 400, 2105, 749, 1644, 165, -1556, -1521, 3009, 1430, + 723, -902, 933, 187, 28, 480, 951, -214, 122, -730, -95, + -137, 573, -593, 558, -692, -62, 28, 16, -505, 426, -283, + 227, -320, 210, -374, 303, -435, 127, -128, 76, -349, 106, + -364, 139, -348, 184, -425, -36, -441, 91, -413, 93, -444, + 33, -257, -74, -414, 93, -379, 19, -327, -43, -366, 79, + -293, 20, -199, -76, -207, 149, -239, 68, -247, 66, -219, + 61, -232, 38, -212, 36, -209, -27, -209, -58, -227, -6, + -309, -12, -225, -60, -199, -10, -277, 19, -207, -69, -211, + -30, -261, -24, -233, -102, -217, -131, -247, -144, -229, -111, + -256, -104, -254, -130, -241, -66, -249, -88, -223, -144, -199, + -148, -229, -101, -242, -189, -240, -163, -308, -147, -285, -217, + -239, -224, -291, -173, -269, -220, -209, -208, -240, -180, -212, + -201, -217, -169, -184, -174, -178, -146, -209, -108, -186, -96, + -108, -90, -120, -44, -101, -49, -39, -22, -44, 1, -58, + 3, -19, -21, -8, -6, -22, -9, 18, -15, -7, -23, + -12, -24, -11, -43, -33, -33, -31, -57, -5, -64, -22, + -45, -16, -87, -8, -40, -45, -57, -14, -53, -5, -47, + -21, -45, -8, -33, 13, -16, 3, -29, 6, -18, 23, + -31, 24, -41}, + /* IRC_Composite_C_R0195_T030_P315.wav */ + {9, -9, 8, -8, 8, -8, 8, -7, 7, -6, 5, + -4, 3, -2, 0, 2, -5, 9, -14, 23, -127, -380, + -922, -742, -548, 257, -2362, 972, -1653, 1079, 1193, 1855, -423, + -3573, -1945, 3974, 274, -2796, 11656, -988, 5085, 9362, 10293, -750, + -3105, -1864, 2111, 2283, -318, -221, -1158, -110, 2923, 410, -981, + 900, 122, -303, 833, 532, -152, 20, -462, 17, -48, 336, + 235, 63, -647, 577, -362, -19, -259, 102, 168, -103, -338, + -333, 24, -9, -458, 287, -470, -273, -216, 46, -484, 13, + -220, -143, -392, -290, -340, -198, -301, -237, -241, -176, -489, + -201, -271, -252, -335, -60, -395, -225, -412, -191, -198, -164, + -259, -64, -380, -75, -287, -9, -159, -126, -100, -116, -155, + -96, -113, -23, -100, -117, -115, -164, -180, 8, -173, -57, + -208, -89, -129, -73, -188, -102, -184, -93, -204, -81, -217, + -128, -180, -66, -279, -127, -244, -206, -146, -178, -190, -135, + -228, -154, -205, -100, -196, -143, -185, -140, -235, -135, -209, + -144, -158, -250, -202, -234, -227, -157, -214, -201, -260, -180, + -244, -184, -234, -151, -192, -198, -223, -199, -199, -202, -189, + -164, -233, -143, -220, -175, -186, -107, -156, -84, -181, -106, + -116, -72, -63, -29, -76, -11, -83, 15, -65, 30, -2, + -4, -41, 4, -49, 38, -44, 57, -2, 9, -61, 11, + -29, 35, -27, 2, -50, -23, -17, 1, -57, 2, -26, + -14, -67, -39, -9, -30, -30, -21, -44, -39, -75, -12, + -27, -13, -32, 13, -22, -16, 5, -2, 23, -25, 48, + -24, -32, -30}, + /* IRC_Composite_C_R0195_T045_P315.wav */ + {52, -54, 57, -59, 62, -64, 67, -71, 74, -78, 82, + -86, 91, -95, 100, -104, 107, -105, 80, -502, -332, -993, + -162, -1907, 1505, -2021, -314, 756, -842, 5057, -1148, -2263, -3462, + 3639, -1435, 4503, -272, 9367, 494, 8006, 14353, 933, -5033, -2453, + 2315, 1073, 2918, -1236, -3010, 2416, 371, 1956, -531, -1294, -177, + 691, 1104, -312, 402, -1300, 693, -586, 315, -26, 538, -594, + 294, -267, -119, -580, 378, 28, 261, -1010, 385, -342, 8, + -224, 217, -399, 43, -603, -1, -295, -252, 167, -340, -419, + -328, -348, -86, -497, -57, -294, -381, -499, -201, -454, -232, + -518, -17, -717, -310, -542, -84, -458, -96, -313, -154, -496, + -112, -226, -165, -242, -163, -234, -306, -270, -70, -238, -108, + -139, -101, -251, -218, -175, -17, -215, -23, -205, -109, -221, + -12, -131, -27, -205, -45, -264, -158, -151, -4, -169, -113, + -251, -102, -247, -100, -151, -158, -212, -170, -190, -217, -220, + -105, -182, -191, -216, -210, -208, -188, -144, -93, -288, -132, + -248, -124, -281, -112, -217, -92, -321, -80, -343, -94, -222, + -100, -231, -229, -216, -139, -209, -164, -138, -158, -204, -180, + -171, -145, -125, -68, -150, -139, -133, -97, -43, -133, -83, + -49, -109, -53, -101, -20, -84, 23, -64, -10, -116, 38, + -50, 15, -23, -2, -3, -27, -28, 3, 14, 36, -13, + -21, -18, -38, 28, -26, 9, -12, -28, -46, -29, -8, + 8, 18, -21, -12, -47, 16, -23, -21, -22, -28, -21, + -46, -32, 37, -14, -1, -31, 11, -4, -17, 51, -42, + 23, -30, 55}, + /* IRC_Composite_C_R0195_T060_P315.wav */ + {-9, 10, -10, 10, -11, 11, -11, 12, -12, 13, -13, + 14, -14, 14, -15, 15, -14, 14, -237, -853, -211, -839, + -537, -1171, 1035, -1099, 1039, 294, -1596, 6549, -2739, -2660, -4050, + 4749, 2134, 7195, 4024, 6882, -1377, 13010, 8996, -6905, -3319, -3088, + 4606, 2892, 2461, -4423, -1310, 1787, 1273, 1672, -1868, -79, 1190, + 70, -141, -131, 222, -677, 570, -820, 675, -811, 128, -382, + 165, -353, -183, -560, 68, -440, -382, -163, -67, -467, -152, + -570, 177, -472, -46, -374, -58, -324, -179, -380, -114, -308, + -223, -231, -266, -228, -188, -382, -93, -468, -172, -439, -277, + -467, -233, -484, -158, -431, -177, -375, -153, -360, -208, -377, + -90, -359, -252, -321, -261, -288, -236, -352, -117, -397, -126, + -318, -103, -229, -203, -302, -132, -152, -113, -159, -188, -103, + -160, -141, -121, -237, -158, -186, -215, -126, -180, -203, -129, + -258, -131, -232, -90, -192, -139, -222, -58, -238, -81, -214, + -111, -210, -119, -232, -97, -257, -79, -254, -124, -244, -169, + -224, -170, -218, -187, -194, -208, -142, -212, -136, -216, -224, + -142, -210, -181, -238, -144, -243, -81, -233, -107, -193, -82, + -124, -113, -114, -86, -61, -72, -88, -74, -42, -62, -68, + -57, -28, -20, -32, -42, -4, -55, -25, 11, -34, -17, + -21, 12, -38, -17, -4, -21, -19, -51, -19, -40, -1, + -58, -23, -65, -46, -5, -23, -26, -24, -22, 36, -42, + 4, -20, 73, -50, 19, -61, 15, -46, 1, -36, -37, + -6, -9, 13, -34, 18, -10, 58, -13, 38, 32, 29, + 4, 16, -6}, + /* IRC_Composite_C_R0195_T075_P315.wav */ + {-13, 13, -14, 14, -14, 14, -14, 14, -14, 13, -13, + 11, -9, 5, 2, -16, 50, -741, 214, -932, 242, -1432, + -236, -662, 1347, -571, -1196, 4105, -1805, 3633, -3512, -1059, -3340, + 6144, 8904, 2949, -3056, 13761, 4639, 6581, 1016, -7994, -537, 2069, + 9498, -3772, -2314, -3272, 3945, 434, 437, -1200, -83, 923, 138, + 258, 258, -7, 455, -141, 284, -478, 398, -1090, 528, -789, + -29, -665, -287, -554, -73, -808, -317, -229, -409, -754, -201, + -562, 103, -767, -233, -393, 90, -725, -54, -341, -112, -375, + -320, -304, -39, -501, -232, -150, -220, -432, -164, -401, -81, + -462, -293, -278, -139, -468, -172, -335, -180, -318, -233, -226, + -103, -361, -214, -194, -168, -399, -129, -254, -92, -285, -156, + -134, -28, -353, -136, -218, -68, -245, -164, -257, -128, -294, + -188, -259, -264, -283, -216, -266, -192, -249, -243, -221, -193, + -297, -159, -234, -247, -176, -224, -169, -180, -199, -220, -117, + -192, -112, -197, -146, -176, -92, -190, -108, -218, -108, -210, + -116, -237, -105, -215, -104, -222, -112, -224, -93, -273, -105, + -266, -128, -293, -88, -283, -37, -279, -49, -211, -28, -227, + 20, -197, 2, -189, 32, -205, 58, -176, 59, -144, 57, + -167, 132, -119, 93, -104, 104, -78, 87, -109, 101, -75, + 67, -98, 89, -87, 61, -85, 56, -78, 56, -103, 66, + -107, 43, -101, 53, -122, 26, -89, 31, -77, 5, -71, + 25, -79, 9, -93, 21, -74, 23, -83, 51, -92, 20, + -72, 56, -59, 56, -36, 38, 19, 39, 4, 62, -17, + 56, -23, 68}, + /* IRC_Composite_C_R0195_T090_P315.wav */ + {87, -92, 97, -103, 109, -116, 123, -131, 140, -150, 162, + -174, 189, -205, 224, -244, 266, -286, 51, -859, 535, -1364, + 139, -1195, 1589, -390, 911, -1048, 4305, 473, -5108, 558, 766, + 2725, 10227, -95, 2100, 7357, 10939, 4034, -9033, 1273, -876, 7923, + 448, -413, -5710, 1509, 967, 2067, -1395, -1318, 853, 624, -242, + 51, 284, 401, 299, 341, -114, 602, -538, 101, -460, -168, + -515, -276, -591, -395, -537, -566, -206, -633, -470, -595, -421, + -387, -497, -528, -554, -277, -415, -451, -399, -376, -198, -441, + -287, -406, -190, -420, -196, -316, -218, -371, -230, -316, -251, + -337, -304, -180, -217, -314, -203, -175, -197, -285, -220, -238, + -109, -305, -167, -234, -146, -316, -92, -192, -70, -236, -136, + -80, -80, -227, -175, -115, -77, -157, -162, -216, -61, -249, + -168, -273, -182, -316, -207, -288, -178, -282, -287, -286, -206, + -333, -257, -356, -226, -357, -253, -372, -144, -341, -201, -306, + -120, -225, -145, -256, -69, -151, -103, -191, -78, -159, -72, + -231, -86, -209, -102, -254, -75, -225, -131, -246, -122, -189, + -129, -238, -123, -178, -85, -185, -80, -172, -78, -165, -42, + -131, -62, -158, -35, -137, -15, -123, -23, -84, 14, -68, + 23, -29, 8, 12, -7, 21, 17, -7, 6, -12, 18, + -60, 33, -67, 51, -69, 41, -34, 41, -45, 33, -37, + 31, -56, 11, -29, -2, -76, 4, -43, 17, -73, -1, + -63, 19, -62, -23, -71, -23, -50, -30, -62, 24, -59, + 23, -75, 63, -53, 35, -32, 38, -9, 16, 4, 53, + -2, 34, -15}, + /* IRC_Composite_C_R0195_T105_P315.wav */ + {-4, 4, -4, 4, -4, 4, -4, 4, -5, 5, -5, + 6, -7, 9, -11, 17, -28, 67, 21, -444, -141, -224, + -503, -492, 56, 581, 659, -254, 970, 3243, -1085, -4435, 2584, + 328, 6386, 5564, -809, 5990, 6762, 10583, -5626, -2592, 332, 5216, + 4379, -2326, -2369, -2841, 2364, 794, 414, -2336, 841, 434, 185, + -245, 467, 54, 226, 66, 291, 109, -14, -250, -37, -434, + -55, -468, -301, -501, -528, -390, -260, -361, -561, -460, -370, + -387, -354, -670, -414, -508, -325, -671, -240, -470, -287, -539, + -267, -403, -309, -523, -407, -423, -444, -437, -347, -265, -394, + -345, -296, -231, -221, -203, -113, -208, -173, -209, -71, -127, + -120, -168, -119, -150, -164, -160, -171, -155, -129, -132, -133, + -114, -84, -152, -145, -150, -50, -140, -100, -199, -121, -188, + -145, -209, -221, -240, -266, -216, -273, -254, -311, -241, -309, + -303, -306, -288, -304, -324, -325, -280, -286, -254, -285, -213, + -282, -199, -235, -168, -198, -151, -152, -157, -138, -157, -155, + -189, -176, -212, -179, -192, -145, -184, -171, -174, -110, -150, + -113, -170, -84, -115, -76, -135, -71, -161, -62, -153, -49, + -144, -73, -126, -80, -100, -82, -63, -82, -57, -74, 14, + -40, -10, -30, 25, -21, 31, -22, 45, -26, 43, -15, + 48, -18, 20, -19, 34, -32, 26, -30, 33, -39, 33, + -38, 25, -62, 21, -45, -12, -65, -6, -49, -13, -61, + 8, -71, 9, -80, 15, -87, 37, -62, 49, -58, 33, + -30, 20, -31, -30, -19, -18, -22, 12, -19, 18, -18, + 62, -6, 19}, + /* IRC_Composite_C_R0195_T120_P315.wav */ + {-8, 8, -9, 10, -10, 11, -12, 13, -14, 15, -17, + 18, -20, 22, -24, 27, -30, 33, -36, -6, -353, -241, + 59, -307, -656, 81, 435, 931, -462, 1098, 2228, 1185, -5297, + 2389, 1039, 4366, 5054, 563, 5052, 5065, 10308, -3912, -1912, -658, + 6384, 3010, -2420, -2503, -2288, 1665, 1300, -465, -1526, 865, 689, + -244, 192, 664, -256, -73, 265, -57, -312, -7, -322, -127, + -354, -230, -494, -275, -378, -331, -253, -267, -433, -173, -259, + -320, -294, -344, -567, -293, -505, -543, -515, -293, -398, -306, + -402, -421, -360, -360, -466, -547, -514, -519, -327, -326, -417, + -471, -219, -314, -177, -232, -161, -243, -135, -249, -154, -136, + -89, -210, -64, -203, -123, -169, -90, -247, -100, -141, -68, + -108, -41, -101, -97, -95, -145, -67, -56, -122, -193, -178, + -180, -188, -181, -281, -271, -252, -234, -263, -302, -242, -343, + -214, -391, -204, -407, -189, -457, -208, -383, -206, -352, -186, + -336, -183, -268, -158, -243, -145, -229, -99, -205, -110, -223, + -83, -256, -120, -241, -124, -258, -103, -238, -73, -205, -48, + -194, -12, -168, -14, -161, -15, -200, -2, -184, -29, -168, + -39, -203, -35, -143, -59, -138, -57, -120, -39, -78, -38, + -71, 4, -85, 25, -41, 42, -65, 60, -35, 71, -28, + 56, -41, 66, -43, 40, -26, -6, -23, -11, -1, -28, + 3, -16, -8, -25, -4, -23, -19, -21, -43, 6, -38, + -12, -49, 4, -21, -1, -32, 9, -8, 22, -23, -1, + -11, -14, -32, -12, -40, -9, -31, 23, -39, 25, -13, + 20, -8, 9}, + /* IRC_Composite_C_R0195_T135_P315.wav */ + {-23, 24, -24, 24, -24, 24, -24, 24, -24, 24, -24, + 24, -24, 23, -23, 22, -21, 19, -17, 12, 2, -321, + 142, -352, -116, -71, -511, 212, 1241, -690, 1132, 1447, 1818, + -2325, -1459, 2842, 2322, 3950, -401, 7294, 1799, 9821, 384, -3014, + -458, 4980, 4067, -3265, -1817, -2557, 1536, 1050, 141, -1408, 467, + 945, 477, -318, 841, -225, -162, -191, 333, -541, -289, -847, + 48, -224, -228, -453, -42, -332, -101, -393, -171, -405, -103, + -444, -70, -591, -149, -459, -69, -570, -117, -584, -95, -396, + -42, -496, -210, -651, -204, -618, -333, -581, -255, -540, -313, + -471, -202, -369, -230, -385, -202, -278, -178, -259, -149, -205, + -123, -195, -100, -150, -73, -188, -126, -225, -124, -193, -92, + -182, -65, -172, -74, -168, -103, -186, -57, -178, -89, -280, + -140, -220, -97, -251, -226, -268, -204, -268, -271, -286, -293, + -290, -360, -269, -322, -274, -307, -261, -331, -275, -290, -250, + -261, -307, -242, -239, -199, -225, -211, -141, -180, -148, -189, + -147, -163, -178, -155, -195, -114, -193, -96, -201, -69, -171, + -54, -158, -66, -130, -90, -112, -83, -108, -98, -108, -90, + -89, -94, -103, -90, -117, -87, -105, -51, -97, -64, -70, + -56, -23, -79, 7, -88, 34, -80, 43, -77, 63, -50, + 59, -68, 43, -51, 41, -54, 60, -92, 29, -102, 48, + -85, 28, -79, 9, -67, 13, -66, 37, -57, 38, -64, + 47, -47, 53, -38, 54, -42, 32, -33, 27, -15, 13, + -24, -26, -34, -6, -41, 23, -23, 1, -31, 23, -1, + 22, -48, -14}, + /* IRC_Composite_C_R0195_T150_P315.wav */ + {-14, 14, -14, 14, -14, 14, -14, 14, -14, 14, -15, + 15, -15, 15, -15, 15, -15, 15, -15, 15, -14, 13, + -9, -299, 105, -109, -149, -174, -101, 123, 928, 331, 409, + 1091, 1957, -1219, -1800, 2020, 2641, 4225, -365, 6765, 1276, 7468, + 2095, -1421, -1941, 2847, 4183, -2838, -2013, -2585, 1301, 1369, 1048, + -1018, 299, 712, 620, -358, 419, -160, -391, -365, 85, -244, + -278, -735, 101, -159, -46, -323, -112, -300, -73, -375, -177, + -408, -241, -349, -232, -359, -171, -320, -131, -293, -2, -318, + -62, -331, -78, -385, -166, -449, -212, -441, -266, -408, -251, + -382, -246, -414, -254, -434, -274, -407, -251, -444, -252, -399, + -189, -349, -176, -289, -151, -238, -106, -181, -45, -201, -17, + -170, -8, -151, 12, -150, -68, -196, -148, -187, -145, -232, + -247, -286, -284, -266, -250, -295, -275, -287, -281, -302, -308, + -288, -299, -267, -330, -280, -301, -267, -269, -255, -300, -276, + -267, -240, -267, -229, -279, -186, -258, -159, -229, -176, -170, + -148, -137, -189, -140, -182, -128, -193, -123, -144, -95, -99, + -87, -75, -90, -47, -89, -71, -115, -103, -102, -112, -110, + -131, -87, -120, -99, -104, -130, -68, -115, -50, -116, -33, + -115, -23, -91, -20, -77, -32, -46, -36, -20, -19, -1, + -1, -16, 2, -30, -4, -10, 2, -12, -25, -22, -46, + -39, -40, -29, -51, -40, -76, -30, -47, -17, -48, -11, + -52, 8, -41, 31, -26, 41, 1, 54, -33, 53, -2, + 35, -28, 34, -23, 25, -31, 33, -25, -2, -7, 10, + -26, -60, -40}, + /* IRC_Composite_C_R0195_T165_P315.wav */ + {-17, 17, -18, 19, -19, 20, -21, 21, -22, 23, -24, + 25, -26, 27, -29, 30, -32, 33, -35, 37, -39, 42, + -45, 48, -52, 67, -40, 152, 75, -119, -31, 837, 302, + 348, 1063, 1535, 471, -104, -785, 1551, 1039, 5803, 545, 3679, + 3043, 5986, 50, -1878, 1830, -1451, 2642, -2294, -1114, -2432, 2146, + 1209, -135, -693, 285, 141, 308, -50, -77, -237, 196, -409, + 80, -447, -119, -467, -15, -415, -52, -429, -63, -216, -215, + -423, -211, -250, -99, -343, -232, -231, -78, -380, -66, -225, + 10, -172, -61, -149, -128, -331, -281, -369, -348, -420, -336, + -400, -303, -402, -320, -360, -377, -402, -338, -363, -356, -382, + -392, -353, -371, -352, -344, -280, -280, -250, -233, -166, -154, + -160, -136, -102, -71, -93, -69, -118, -111, -164, -179, -223, + -230, -272, -255, -285, -312, -330, -270, -347, -306, -400, -338, + -356, -324, -353, -315, -327, -313, -301, -321, -289, -309, -258, + -317, -227, -285, -196, -252, -189, -237, -155, -182, -95, -165, + -128, -123, -87, -127, -113, -150, -118, -122, -92, -114, -79, + -90, -44, -70, -19, -82, -37, -76, -88, -84, -86, -94, + -117, -109, -92, -108, -75, -136, -74, -119, -62, -120, -51, + -126, -38, -97, -20, -110, -3, -105, 29, -72, 41, -66, + 43, -69, 58, -54, 55, -37, 38, -45, 18, -47, 10, + -73, -1, -104, 15, -120, 0, -103, 12, -106, 19, -73, + 20, -41, 43, -9, 42, -5, 48, -4, 46, 3, 29, + -8, 24, 11, 13, -8, 11, 55, 13, 32, 16, 31, + 9, -51, -42}, + /* IRC_Composite_C_R0195_T180_P315.wav */ + {24, -25, 26, -27, 28, -30, 31, -33, 34, -36, 38, -40, + 42, -45, 47, -50, 53, -57, 61, -65, 70, -76, 82, -90, + 100, -116, 158, -120, 110, -47, 143, -200, 288, -83, 509, 566, + 678, 584, 1717, -752, -218, -645, 3620, 1547, 2553, 2618, 2850, 3932, + 1154, 2478, -3553, 2655, -1012, 521, -1943, -220, 418, 465, 358, -445, + 32, 572, 410, 189, 111, 242, -109, 240, 99, 120, -178, -44, + -146, -12, -251, -169, -110, -64, -56, -37, -65, 38, 137, -75, + -42, -159, -127, -127, -11, -39, -7, -121, -97, -173, -207, -322, + -357, -395, -311, -334, -329, -369, -303, -285, -259, -281, -323, -274, + -364, -264, -415, -314, -459, -317, -463, -346, -399, -298, -323, -239, + -217, -185, -167, -162, -146, -115, -142, -125, -194, -139, -196, -150, + -224, -208, -224, -201, -206, -219, -254, -241, -239, -289, -306, -358, + -320, -362, -312, -398, -353, -429, -362, -421, -352, -421, -332, -364, + -272, -351, -188, -256, -150, -222, -109, -165, -57, -142, -72, -111, + -54, -116, -66, -134, -87, -132, -39, -149, -55, -144, -3, -111, + -37, -108, -57, -98, -89, -106, -109, -135, -117, -147, -114, -137, + -99, -140, -85, -140, -56, -119, -68, -119, -57, -94, -53, -95, + -61, -85, -42, -76, -22, -61, -39, -37, -44, -27, -31, -14, + -37, -23, -16, -39, 0, -62, -11, -92, 2, -64, -27, -61, + -33, -39, -34, -35, -26, -27, 5, -20, 14, -33, 30, -28, + 12, -27, 25, -7, 7, 29, -15, 24, -9, 67, 32, 52, + -7, 19, -10, -34}, + /* IRC_Composite_C_R0195_T195_P315.wav */ + {-3, 3, -4, 4, -4, 4, -4, 4, -4, 4, -4, 4, + -5, 5, -5, 5, -5, 5, -6, 6, -6, 7, -7, 8, + -10, 16, -53, -21, 71, -52, 123, 66, -67, 138, 16, 149, + 471, 552, 491, 879, 834, 185, -864, 886, 1624, 2747, 1377, 2335, + 2675, 2476, 2941, -204, -424, -902, 1080, -594, -652, -972, -15, 737, + 158, 15, -103, 673, 669, 117, 134, 496, 369, 6, 174, 211, + 204, 39, 24, 7, -16, 8, -20, 185, 63, 80, 52, 87, + -18, -50, -107, -83, -58, -90, -149, -191, -215, -179, -280, -228, + -426, -325, -403, -260, -377, -309, -454, -294, -358, -263, -350, -288, + -312, -298, -376, -336, -392, -351, -440, -368, -423, -300, -356, -221, + -309, -156, -237, -133, -233, -159, -209, -147, -233, -211, -247, -210, + -236, -217, -261, -238, -270, -208, -309, -227, -327, -269, -364, -336, + -391, -375, -368, -412, -407, -388, -417, -354, -411, -328, -386, -273, + -319, -198, -244, -162, -205, -101, -178, -129, -177, -104, -161, -77, + -156, -48, -139, -49, -168, -54, -132, -40, -129, -49, -113, -43, + -97, -66, -144, -76, -174, -72, -177, -115, -186, -100, -175, -109, + -149, -106, -124, -101, -86, -76, -56, -81, -27, -107, 3, -87, + -27, -99, -52, -77, -57, -80, -68, -65, -62, -77, -17, -71, + -16, -67, -12, -47, -11, -33, -14, -33, -31, -4, -26, -4, + -79, 6, -73, -6, -74, -4, -61, -7, -62, 10, -50, 33, + -27, 0, -4, 51, 21, 29, 16, 43, 17, 21, 38, 16, + 70, -26, 47, -23}, + /* IRC_Composite_C_R0195_T210_P315.wav */ + {6, -6, 6, -6, 6, -6, 6, -6, 6, -6, 6, -7, + 7, -7, 7, -7, 7, -7, 8, -8, 8, -8, 9, -9, + 9, -9, 9, -9, 9, -6, -16, -81, -27, -64, -11, -116, + 37, -7, 308, 224, 339, 624, 595, 47, -450, 456, 1108, 1630, + 1528, 1730, 2147, 2381, 2098, 1287, -731, 283, 215, 213, -551, -518, + 308, 203, 729, -194, 652, 579, 759, 441, 763, 571, 528, 360, + 442, 343, 364, 229, 431, 125, 293, 81, 275, 88, 238, 45, + 215, 155, 198, 82, 61, 12, 5, -51, -58, -119, -156, -200, + -208, -255, -244, -261, -244, -232, -236, -252, -249, -226, -258, -256, + -283, -248, -281, -296, -308, -266, -304, -256, -333, -220, -281, -198, + -262, -186, -220, -164, -184, -148, -198, -168, -234, -150, -270, -190, + -317, -221, -358, -246, -358, -268, -356, -277, -357, -276, -375, -272, + -357, -257, -384, -239, -370, -230, -374, -258, -374, -262, -363, -281, + -344, -263, -317, -224, -292, -194, -243, -172, -216, -149, -196, -138, + -151, -112, -138, -121, -140, -121, -158, -136, -149, -135, -124, -140, + -124, -136, -125, -160, -126, -165, -139, -151, -144, -151, -125, -132, + -116, -99, -80, -92, -84, -64, -97, -68, -84, -76, -83, -93, + -75, -65, -93, -87, -89, -70, -103, -55, -102, -68, -112, -68, + -70, -61, -55, -62, -34, -41, -24, -58, -48, -63, -63, -52, + -80, -56, -81, -54, -68, -55, -48, -48, -24, -69, -21, -60, + -6, -21, -10, -12, -6, 14, -8, 19, 1, 57, 22, 38, + 15, 27, 32, 33}, + /* IRC_Composite_C_R0195_T225_P315.wav */ + {-7, 7, -8, 8, -8, 8, -8, 8, -8, 8, -8, 8, + -8, 8, -8, 8, -8, 8, -8, 9, -9, 9, -9, 9, + -9, 9, -9, 9, -6, 33, 28, 58, 51, 52, 71, 102, + 56, 94, 137, 218, 326, 455, 483, 632, 249, 92, 428, 904, + 1132, 1053, 1398, 1725, 1892, 1761, 996, 419, -35, 393, 230, -426, + 83, 187, 561, 443, 405, 624, 567, 778, 322, 714, 220, 555, + 133, 272, 57, 175, 33, -14, 10, 34, 86, 73, 113, 24, + -32, -47, -92, -107, -159, -198, -236, -286, -255, -307, -290, -357, + -323, -402, -335, -423, -329, -409, -335, -383, -288, -312, -274, -328, + -286, -276, -227, -303, -252, -323, -243, -382, -269, -374, -281, -400, + -300, -362, -289, -363, -292, -337, -287, -341, -275, -329, -275, -353, + -270, -350, -272, -365, -283, -346, -284, -356, -310, -344, -298, -351, + -295, -331, -289, -350, -307, -364, -293, -334, -305, -333, -280, -301, + -259, -287, -211, -253, -202, -233, -185, -196, -178, -176, -188, -164, + -153, -127, -135, -117, -145, -120, -134, -127, -143, -116, -126, -80, + -113, -89, -119, -102, -89, -86, -92, -78, -108, -66, -94, -63, + -86, -58, -73, -44, -52, -66, -55, -76, -50, -92, -33, -91, + -19, -106, -44, -97, -52, -76, -62, -65, -52, -53, -56, -64, + -38, -57, -27, -67, -32, -61, -26, -56, -44, -40, -36, -27, + -22, -5, -13, -6, 12, -15, 8, -1, 18, 1, -3, 6, + -13, 24, 6, 33, 17, 44, 11, 63, 31, 57, 47, 73, + 42, 34, 14, 5}, + /* IRC_Composite_C_R0195_T240_P315.wav */ + {0, 0, 0, -1, 1, -1, 1, -1, 1, -1, 1, -1, + 1, -1, 1, -1, 1, -1, 1, -1, 1, -1, 1, -1, + 1, -1, 2, 58, 78, 16, 22, 51, 114, 84, 41, 96, + 70, 113, 111, 119, 154, 190, 332, 340, 437, 381, 393, 351, + 291, 690, 869, 1203, 1249, 1386, 1308, 1507, 1378, 1185, 595, 424, + 365, 626, 420, 458, 252, 382, 637, 446, 207, 255, 488, 305, + 321, 49, 221, 53, 63, 79, -40, 108, -82, 101, -101, -13, + -203, -152, -221, -220, -235, -289, -282, -281, -296, -224, -344, -241, + -368, -252, -366, -268, -359, -308, -318, -288, -298, -317, -264, -285, + -261, -303, -269, -298, -270, -320, -331, -336, -351, -327, -351, -348, + -331, -347, -276, -351, -246, -372, -243, -370, -280, -365, -300, -410, + -295, -413, -330, -447, -345, -440, -333, -430, -333, -415, -326, -403, + -318, -379, -309, -372, -303, -357, -286, -300, -274, -299, -260, -274, + -222, -258, -207, -249, -178, -228, -179, -202, -181, -164, -194, -132, + -181, -100, -149, -105, -124, -85, -117, -92, -115, -104, -129, -96, + -119, -72, -137, -65, -104, -51, -87, -75, -80, -56, -63, -66, + -73, -81, -74, -88, -75, -87, -74, -68, -80, -63, -77, -50, + -74, -36, -76, -42, -77, -46, -59, -47, -48, -54, -32, -62, + -21, -50, -24, -43, -33, -23, -40, -10, -7, -13, -16, -4, + 2, 7, 17, -10, 37, -7, 35, 11, 27, -4, 32, -1, + 15, -21, -1, -28, 7, -9, 16, 21, 30, 38, 52, 37, + 48, 38, 34, 3}, + /* IRC_Composite_C_R0195_T255_P315.wav */ + {-2, 2, -2, 2, -2, 2, -2, 2, -2, 2, -2, 2, + -2, 2, -2, 2, -2, 2, -2, 2, -2, 2, -2, 2, + -3, 3, -3, 3, -3, 4, -6, 74, 75, 79, 61, 128, + 72, 79, 70, 94, 189, 182, 258, 204, 285, 366, 406, 480, + 409, 503, 603, 776, 875, 1073, 1366, 1703, 1601, 1585, 1344, 1253, + 1054, 708, 209, 184, 245, 471, 220, 82, 328, 234, 143, -43, + 229, 63, 146, 52, 46, 143, -26, 108, -162, -81, -337, -290, + -357, -257, -274, -266, -282, -295, -253, -242, -177, -217, -241, -242, + -180, -187, -261, -282, -286, -285, -278, -318, -290, -350, -241, -329, + -216, -356, -250, -344, -253, -301, -333, -307, -381, -310, -406, -310, + -398, -327, -389, -328, -383, -321, -397, -364, -416, -386, -447, -400, + -469, -437, -477, -423, -470, -435, -472, -439, -441, -374, -396, -361, + -359, -340, -300, -318, -278, -293, -236, -257, -203, -214, -171, -197, + -164, -178, -159, -160, -150, -162, -167, -187, -166, -177, -176, -193, + -156, -159, -125, -174, -110, -158, -92, -129, -96, -113, -107, -94, + -103, -101, -73, -98, -45, -93, -27, -108, -14, -122, -36, -134, + -52, -131, -66, -121, -83, -114, -73, -107, -51, -91, -32, -79, + 2, -74, 2, -68, 4, -68, 20, -45, 9, -63, -6, -50, + 8, -15, -2, -22, -9, 6, 4, 25, -11, 21, -12, 18, + -6, -1, -16, -16, -15, -8, 7, 8, -10, 14, -14, 21, + -9, 18, -34, -11, 0, 0, 38, 9, 28, 24, 33, 41, + 32, 56, 10, 45}, + /* IRC_Composite_C_R0195_T270_P315.wav */ + {5, -5, 5, -5, 5, -5, 5, -6, 6, -6, 6, -6, + 6, -6, 7, -7, 7, -7, 8, -8, 8, -9, 9, -10, + 10, -11, 12, -13, 14, -16, 19, -24, 35, -112, -87, -172, + -147, -119, -127, -152, -113, -103, -89, -113, -68, -116, -46, 22, + 42, 151, 321, 392, 548, 700, 898, 1192, 1367, 1289, 1342, 1432, + 1348, 1134, 1055, 823, 716, 745, 988, 819, 628, 418, 468, 438, + 382, 415, 357, 462, 457, 454, 331, 457, 286, 349, 199, 216, + 110, 272, 103, 141, 96, 107, 157, 39, 151, 90, 143, 187, + 135, 145, 132, 119, 90, 29, 67, 9, 33, -99, -4, -78, + -31, -119, -118, -125, -133, -39, -116, 6, -53, -9, -2, -28, + -8, -68, -109, -175, -231, -243, -320, -296, -388, -312, -439, -323, + -431, -308, -403, -341, -379, -320, -333, -304, -317, -322, -321, -302, + -340, -277, -367, -233, -348, -216, -344, -197, -304, -195, -255, -199, + -226, -173, -179, -167, -168, -175, -177, -169, -197, -168, -238, -156, + -231, -167, -240, -168, -219, -192, -218, -202, -205, -205, -186, -197, + -157, -172, -143, -163, -152, -143, -150, -149, -177, -148, -159, -149, + -150, -163, -154, -183, -160, -189, -132, -190, -124, -156, -96, -125, + -92, -99, -99, -74, -85, -61, -83, -67, -68, -74, -62, -91, + -54, -91, -46, -108, -22, -86, -15, -70, -6, -63, -3, -43, + -35, -76, -52, -91, -99, -89, -83, -72, -67, -70, -42, -53, + -31, -59, -32, -49, -39, -55, -65, -43, -43, -36, -50, -25, + -39, -34, -26, -31}, + /* IRC_Composite_C_R0195_T285_P315.wav */ + {0, 0, -1, 1, -1, 1, -1, 1, -1, 1, -1, 1, + -1, 1, -1, 1, -1, 1, -1, 1, -1, 2, -2, 2, + -2, 3, -3, 0, -53, -54, -56, -85, -47, -114, -135, -181, + -153, -193, -267, -244, -55, -96, 1, 65, -104, -149, -27, 77, + 318, 544, 620, 776, 1096, 1388, 1457, 1232, 925, 941, 734, 587, + 918, 901, 616, 762, 669, 803, 637, 587, 517, 641, 585, 459, + 366, 454, 473, 472, 377, 381, 369, 477, 434, 303, 311, 258, + 424, 217, 330, 203, 348, 239, 206, 163, 147, 190, 93, 131, + 3, 88, 29, 61, 25, -4, 30, -29, -32, -79, -68, -48, + -73, -75, -119, -13, -30, 17, -48, 5, -35, 5, -39, -80, + -142, -105, -167, -166, -226, -200, -281, -233, -317, -276, -334, -290, + -353, -338, -366, -316, -342, -297, -343, -306, -273, -252, -266, -278, + -281, -276, -239, -265, -258, -270, -226, -250, -191, -215, -181, -217, + -149, -202, -131, -195, -119, -225, -148, -263, -146, -220, -148, -221, + -170, -215, -156, -215, -188, -262, -202, -269, -194, -235, -202, -241, + -188, -209, -153, -204, -154, -203, -167, -217, -151, -214, -172, -225, + -158, -182, -145, -170, -150, -164, -121, -136, -87, -134, -90, -102, + -41, -87, -63, -73, -53, -73, -60, -73, -67, -76, -98, -68, + -64, -72, -69, -72, -35, -71, -32, -55, -37, -56, -47, -39, + -69, -58, -90, -53, -99, -57, -83, -34, -50, -55, -58, -67, + -57, -63, -70, -62, -74, -53, -70, -30, -49, -56, -37, -35, + -21, -10, -19, -29}, + /* IRC_Composite_C_R0195_T300_P315.wav */ + {2, -2, 2, -2, 2, -2, 2, -2, 2, -2, 2, -2, + 2, -2, 2, -2, 2, -2, 2, -2, 2, -2, 2, -2, + 2, -2, 2, -3, 4, -10, -133, -133, -170, -242, -249, -433, + -299, -139, -159, 192, 133, -70, 22, -303, 18, -19, 635, 218, + 706, 1507, 1430, 1383, 2387, 1796, 1075, 392, 368, 909, 611, 309, + -168, 353, 2, 566, 420, 570, 464, 600, 533, 643, 640, 611, + 592, 536, 352, 641, 435, 676, 299, 466, 377, 487, 301, 349, + 330, 262, 326, 234, 228, 152, 89, 163, 56, 54, -10, 7, + -78, -45, -66, -85, -68, -104, -18, 4, -26, 41, 2, 31, + 0, 79, 5, 36, 26, 46, 69, 42, -19, -67, -82, -80, + -133, -166, -222, -198, -222, -228, -274, -302, -279, -261, -261, -282, + -253, -280, -237, -241, -240, -230, -249, -229, -231, -221, -230, -219, + -245, -228, -234, -188, -202, -201, -222, -167, -182, -137, -203, -165, + -172, -150, -180, -163, -208, -169, -194, -164, -229, -193, -245, -204, + -252, -223, -246, -236, -257, -266, -272, -274, -271, -280, -267, -251, + -243, -225, -207, -200, -174, -184, -165, -180, -160, -164, -160, -158, + -167, -156, -169, -151, -138, -125, -125, -108, -104, -80, -94, -71, + -82, -75, -85, -57, -68, -57, -49, -58, -52, -65, -51, -59, + -53, -74, -44, -57, -44, -43, -49, -55, -42, -57, -55, -65, + -60, -62, -53, -54, -78, -70, -64, -55, -66, -73, -66, -48, + -68, -87, -49, -57, -32, -77, -35, -66, -33, -71, -35, -70, + 15, -17, -11, -33}, + /* IRC_Composite_C_R0195_T315_P315.wav */ + {-3, 3, -4, 4, -4, 4, -4, 4, -4, 4, -4, 5, + -5, 5, -5, 5, -5, 5, -6, 6, -6, 6, -6, 6, + -6, 6, -5, 3, 2, -152, -177, -483, -235, -110, -653, -348, + -150, -38, 208, 331, 33, -385, -315, -27, 364, 811, -94, 2504, + 888, 2086, 3574, 3328, 531, 314, 789, 729, 404, -337, 56, 36, + 119, 405, 418, 656, 613, 503, 327, 557, 477, 459, 703, 343, + 212, 405, 546, 432, 317, 280, 459, 304, 364, 271, 334, 99, + 204, 199, 155, 38, 34, 17, 94, 27, 75, -28, 51, -48, + 69, -29, 44, -56, 45, -30, 20, 5, 41, -28, 41, 31, + 56, 13, 68, 19, 90, -37, 31, -65, 21, -87, -51, -142, + -103, -155, -98, -191, -137, -221, -116, -207, -139, -209, -136, -203, + -147, -207, -168, -209, -164, -226, -176, -243, -198, -258, -194, -267, + -197, -246, -196, -229, -191, -208, -190, -182, -197, -201, -171, -193, + -170, -221, -151, -220, -141, -245, -167, -251, -184, -255, -210, -246, + -238, -245, -223, -258, -247, -284, -254, -278, -249, -311, -255, -297, + -217, -263, -193, -236, -176, -198, -176, -178, -155, -172, -140, -181, + -132, -195, -91, -184, -92, -165, -90, -121, -68, -117, -88, -97, + -47, -73, -55, -70, -16, -46, -13, -61, -26, -38, -50, -48, + -41, -43, -40, -60, -49, -55, -58, -57, -47, -68, -57, -81, + -49, -74, -61, -74, -29, -70, -35, -79, -39, -87, -35, -94, + -49, -86, -55, -65, -49, -58, -43, -30, -32, -23, -33, -5, + -23, 19, -13, -17}, + /* IRC_Composite_C_R0195_T330_P315.wav */ + {-7, 7, -7, 7, -7, 7, -7, 7, -7, 7, -7, 7, + -7, 7, -7, 7, -7, 8, -8, 8, -8, 9, -9, 9, + -10, 11, -11, -55, -296, -386, -360, -162, -486, -689, -594, 323, + -279, 489, 253, -225, -799, -208, -290, 1653, -875, 1421, 3185, 831, + 3972, 4758, 3089, -835, 674, 981, 1685, -351, -114, -17, 248, 390, + 777, 359, 517, 535, 503, 290, 712, 58, 681, 346, 142, -203, + 198, 199, 331, -307, 34, 132, 139, 70, 114, 91, 13, 123, + 184, 191, 4, 23, 205, 163, 125, 72, 141, -40, 14, 124, + -28, 33, -51, 40, -5, 35, -58, 73, -52, -16, 56, 78, + -40, -37, 45, 32, 27, 55, -7, -15, -18, 29, -29, 22, + -117, 6, -57, -69, -79, -66, -162, -105, -133, -121, -164, -165, + -210, -152, -236, -167, -217, -215, -247, -204, -244, -201, -239, -194, + -273, -180, -249, -181, -223, -192, -224, -139, -212, -188, -198, -169, + -226, -134, -219, -153, -208, -148, -204, -149, -219, -168, -214, -162, + -245, -160, -257, -219, -229, -208, -279, -218, -292, -266, -282, -268, + -275, -227, -298, -216, -236, -164, -238, -151, -226, -142, -217, -147, + -203, -145, -177, -146, -138, -122, -131, -73, -151, -51, -110, -31, + -107, -6, -86, -9, -40, -10, -36, -1, -49, -9, -42, -8, + -74, 2, -73, 3, -80, -26, -79, 6, -86, -19, -76, -29, + -75, -32, -78, -38, -65, -46, -77, -46, -88, -55, -75, -74, + -82, -34, -77, -58, -74, -44, -61, -16, -21, -21, -1, -32, + 25, -5, -9, -4}, + /* IRC_Composite_C_R0195_T345_P315.wav */ + {-3, 3, -3, 3, -3, 3, -3, 3, -3, 3, -2, 2, + -2, 2, -1, 1, 0, 0, 1, -2, 3, -4, 6, -9, + 13, -23, -205, -456, -570, -178, -609, -516, -1049, -349, 515, 157, + -280, 1141, -1675, -135, -1096, 1502, 703, -567, 2409, 4580, 1615, 5569, + 6660, -925, 461, 21, 3064, -94, 275, -939, 176, 137, 1125, 718, + -145, 338, 911, 169, 330, 365, 552, 148, 369, -393, 179, -127, + 204, 80, -84, -339, 8, 81, -65, -288, 145, 50, -22, -24, + 61, 62, 45, -71, 194, 159, 54, -19, 226, -56, 24, 48, + 99, -31, -91, -8, 53, 24, -79, 12, 113, -69, -3, 43, + 52, -53, 25, 22, 42, -13, -5, -1, 64, -132, -15, -42, + -12, -176, -19, -103, -113, -146, -76, -99, -119, -177, -139, -96, + -193, -229, -107, -210, -181, -214, -106, -219, -211, -156, -157, -151, + -246, -153, -144, -151, -254, -173, -147, -195, -202, -155, -196, -154, + -199, -151, -187, -147, -192, -144, -157, -216, -170, -139, -200, -196, + -193, -158, -269, -172, -256, -190, -293, -226, -252, -229, -318, -238, + -250, -265, -306, -166, -254, -191, -300, -137, -229, -129, -286, -136, + -186, -156, -221, -108, -162, -109, -143, -76, -137, -51, -129, -13, + -99, -24, -56, 17, -62, -20, -41, 40, -86, 44, -52, 39, + -123, 42, -65, 39, -115, -16, -46, 0, -101, -28, -69, -10, + -115, -27, -101, 20, -128, 3, -110, 13, -133, -3, -101, -28, + -103, -3, -79, -10, -82, 10, -56, 27, -77, 25, -50, 57, + -33, 20, -49, 39}}; + +const int16_t irc_composite_c_r0195_p330[][256] = + {/* IRC_Composite_C_R0195_T000_P330.wav */ + {-59, 61, -63, 65, -67, 69, -71, 73, -76, 78, -81, + 83, -86, 89, -92, 95, -98, 100, -102, 103, -103, 99, + -90, 66, 27, -206, -447, -812, -1040, -532, -532, -221, -2282, + 2737, -1254, 634, -40, 577, -3131, -820, -289, 5598, -4456, 4514, + 11627, -957, 5804, 7393, 108, -5260, 1410, 3318, 1621, -1983, 2287, + 284, 1686, 1238, 2060, -1196, 399, 489, 1551, 51, 439, -136, + 620, 290, -51, 26, 548, -418, 535, -429, 119, -693, 256, + -317, 191, -412, 18, -258, -48, -331, 42, -245, -151, -84, + -244, -168, -112, -248, 56, -243, -130, -346, 3, -311, -140, + -201, -205, -208, -164, -257, -153, -216, -142, -212, -32, -240, + -169, -200, -80, -259, -56, -266, -49, -259, -153, -247, -131, + -186, -180, -157, -174, -170, -168, -133, -130, -144, -118, -156, + -112, -157, -115, -125, -52, -108, -95, -100, -113, -69, -143, + -89, -121, -105, -124, -163, -161, -185, -102, -218, -93, -213, + -136, -158, -144, -182, -122, -122, -161, -147, -153, -151, -116, + -162, -100, -155, -94, -176, -98, -138, -94, -127, -119, -122, + -158, -125, -151, -130, -178, -168, -144, -200, -186, -199, -181, + -157, -192, -201, -216, -176, -230, -170, -197, -219, -207, -194, + -203, -187, -195, -174, -204, -175, -205, -170, -207, -182, -206, + -156, -195, -149, -193, -165, -200, -125, -166, -120, -182, -142, + -113, -73, -137, -86, -97, -75, -88, -72, -83, -37, -82, + -24, -72, -40, -53, 24, -38, -23, -48, 32, 10, 7, + -19, 18, 7, 43, 7, 34, 32, 65, 53, 56, 21, + 94, 55, 42}, + /* IRC_Composite_C_R0195_T015_P330.wav */ + {-33, 32, -32, 31, -31, 30, -28, 27, -25, 22, -19, + 15, -10, 4, 5, -16, 31, -53, 84, -136, 240, -673, + 177, -174, -1030, -1321, -76, -1250, -372, 289, -2348, 2418, 1442, + -314, -2260, 351, -1076, 96, -4201, 12653, -2793, 350, 16253, 7416, + -1734, 373, 1945, -1722, 32, 2367, 1990, -2906, 2200, 2573, 955, + -397, 1580, -424, 684, 371, 1473, 401, 8, -1088, 1156, -110, + 164, -526, 361, -233, -337, -706, 160, -330, -24, -130, 132, + -810, 170, -491, 242, -434, 33, -622, 355, -505, -308, -68, + -61, -264, -71, -244, -294, -284, -179, -295, 41, -462, -220, + -369, -36, -507, -206, -424, -65, -416, -180, -392, -246, -316, + -59, -245, -204, -339, -224, -205, -78, -425, -107, -254, -66, + -399, -77, -350, 46, -239, -96, -256, -101, -268, -138, -142, + -190, -179, -136, -172, -84, -261, -40, -173, 25, -283, -32, + -282, 9, -234, -87, -198, -101, -238, -154, -207, -159, -166, + -106, -164, -136, -199, -109, -121, -73, -175, -65, -149, -73, + -162, -108, -135, -78, -99, -124, -126, -179, -91, -133, -84, + -188, -122, -167, -148, -155, -182, -183, -160, -158, -219, -207, + -224, -181, -161, -192, -206, -207, -174, -243, -140, -189, -173, + -194, -138, -203, -173, -192, -143, -155, -154, -216, -158, -170, + -138, -202, -110, -182, -132, -154, -83, -201, -104, -130, -44, + -129, -80, -139, -19, -72, -80, -88, -16, -110, -20, -46, + -35, -88, 15, -32, 6, -67, -9, -16, 52, -18, 27, + 27, 26, -2, 60, 51, 76, 28, 98, 40, 77, 33, + 94, 11, 62}, + /* IRC_Composite_C_R0195_T030_P330.wav */ + {15, -16, 18, -19, 21, -23, 26, -29, 32, -36, + 41, -47, 54, -62, 73, -86, 104, -126, 155, -102, + 601, -1085, -197, -1634, 218, -1892, 836, -1472, 627, 332, + 2248, 1679, -4388, -1076, 2778, -5228, 3611, 10385, -5976, 10952, + 11266, 9110, -4453, 651, -313, -354, 33, 2898, -224, -1072, + 2571, 1824, -115, 539, 940, -84, 401, 1263, 337, 3, + -171, -426, 685, -69, -27, -94, -916, -1001, 131, -752, + -458, -101, -74, -99, -305, -309, -166, -102, 238, -512, + 98, -429, -43, -482, 424, -183, -138, -249, -301, -240, + -13, -418, -203, -184, -296, -341, -269, -474, -99, -447, + -210, -601, -403, -560, -148, -418, -397, -430, -241, -495, + -140, -391, -94, -358, -170, -431, -28, -368, -72, -337, + -69, -376, -11, -267, -144, -348, -177, -200, -156, -317, + -161, -307, -97, -238, -69, -237, -166, -271, -82, -187, + -69, -237, -35, -248, -105, -213, -38, -207, -108, -284, + -91, -256, -64, -223, -92, -227, -52, -205, -48, -178, + -94, -174, -26, -226, -71, -202, -71, -184, -34, -207, + -88, -154, -79, -180, -41, -222, -49, -215, -56, -245, + -90, -251, -51, -260, -109, -246, -150, -229, -109, -239, + -125, -253, -146, -228, -85, -219, -106, -252, -82, -250, + -58, -194, -116, -232, -74, -193, -81, -184, -110, -195, + -65, -162, -92, -193, -65, -199, -29, -203, 0, -193, + 28, -151, -16, -159, -4, -109, -9, -90, -21, -99, + 36, -92, 27, -82, 30, -74, 71, -74, 64, -43, + 102, -28, 93, 18, 88, 22, 98, 29, 116, 21, + 126, -20, 133, -40, 143, -66}, + /* IRC_Composite_C_R0195_T045_P330.wav */ + {60, -60, 60, -60, 60, -60, 59, -57, 55, -51, 47, + -40, 30, -15, -9, 50, -138, 582, -538, -783, -136, 149, + -1756, -848, -253, -449, 1379, -797, 1611, 1075, 2352, -3846, -4473, + 1829, 1877, 4778, 13625, -4798, 10293, 11126, 4895, -5087, -2315, -1117, + -112, 3354, 2968, -606, -2642, 2599, 370, 2356, -155, 634, -118, + 50, -430, 371, -366, 83, 83, 846, -691, 615, -552, -124, + -1033, -101, -1482, 300, -868, 142, -849, -166, -356, 234, -620, + -26, -586, 234, -687, 38, -602, 184, -537, -8, -502, 109, + -408, 41, -516, 144, -611, 54, -513, -33, -440, -242, -381, + -176, -498, -276, -575, -140, -578, -79, -706, -148, -475, -195, + -498, -214, -378, -197, -385, -257, -315, -80, -443, -59, -376, + -102, -363, -123, -440, -39, -442, -136, -304, -144, -309, -84, + -271, -165, -242, -238, -200, -117, -273, -150, -258, -130, -283, + -92, -309, -42, -296, -47, -253, -76, -218, 8, -252, -78, + -168, -84, -181, -86, -207, -54, -197, -48, -239, -19, -220, + -19, -245, -20, -221, -34, -247, -31, -246, -93, -181, -96, + -211, -94, -210, -85, -235, -118, -230, -101, -229, -107, -245, + -109, -215, -112, -220, -113, -204, -100, -188, -108, -185, -89, + -182, -62, -197, -34, -195, -28, -194, -18, -182, -55, -157, + -81, -141, -90, -141, -91, -129, -74, -112, -85, -107, -64, + -103, -64, -86, -50, -93, -43, -62, -35, -57, -57, -38, + -24, -28, -39, -24, -20, -16, 22, 2, 35, 17, 56, + 35, 75, 47, 56, 59, 88, 45, 61, 91, 80, 67, + 62, 68, 59}, + /* IRC_Composite_C_R0195_T060_P330.wav */ + {-16, 18, -21, 24, -27, 30, -34, 39, -45, 51, -59, + 68, -79, 93, -109, 120, -488, -209, -843, -446, 581, -2538, + 150, -1048, 1674, -298, 921, -422, 4633, -1379, -3456, -5235, 5897, + -232, 13802, 4771, -1871, 13790, 9150, 509, -7321, -2062, 426, 4376, + 2825, -66, -4120, 2160, 1014, 1965, -182, 10, -66, 1012, -1092, + 605, -694, -103, -630, 876, -1446, 848, -616, 147, -237, 208, + -605, 430, -846, 156, -639, -33, -577, -169, -800, -107, -681, + -261, -491, -122, -613, -72, -646, -89, -523, -258, -835, -111, + -630, -192, -684, -120, -546, -114, -589, -198, -484, -123, -592, + -31, -441, -137, -356, -160, -262, -88, -337, -230, -206, -197, + -286, -158, -371, -90, -386, -164, -357, -120, -396, -123, -371, + -159, -394, -268, -385, -156, -411, -140, -387, -213, -351, -179, + -344, -165, -326, -182, -213, -154, -216, -130, -224, -133, -226, + -137, -212, -88, -294, -83, -283, -112, -241, -166, -252, -129, + -207, -171, -148, -189, -146, -134, -197, -75, -135, -114, -111, + -109, -99, -40, -137, -112, -65, -123, -140, -104, -208, -112, + -191, -189, -188, -164, -205, -119, -255, -149, -161, -150, -183, + -173, -162, -174, -126, -195, -97, -145, -155, -118, -129, -86, + -145, -98, -141, -61, -119, -118, -63, -133, -28, -155, 2, + -161, -3, -128, -39, -100, -78, -65, -82, -72, -98, -47, + -85, -70, -46, -75, -12, -101, 21, -108, 22, -100, 40, + -92, 45, -75, 34, -63, 31, -40, 44, -41, 58, -23, + 74, -14, 74, -19, 131, -21, 113, 30, 101, 49, 110, + 57, 108, 51}, + /* IRC_Composite_C_R0195_T075_P330.wav */ + {19, -20, 21, -23, 25, -26, 28, -31, 33, -36, 39, + -42, 45, -49, 49, -225, -118, -411, -201, -767, -125, -1198, + -1086, 1587, 699, -114, 366, 4108, -2207, 1045, -7292, 822, 4055, + 16836, -2188, 1091, 15052, 6204, 1571, -5363, -5126, 3112, 6714, 709, + -1466, -3132, 1975, 682, 2154, -1147, 580, -537, 152, -77, -200, + -771, 391, -354, -354, -457, 389, -921, 324, -902, 497, -455, + -10, -125, 532, -709, 91, -249, -171, -384, -149, -591, -267, + -649, -63, -714, -477, -400, -204, -742, -437, -554, -445, -510, + -517, -561, -379, -611, -347, -592, -417, -611, -301, -482, -385, + -503, -245, -302, -310, -258, -221, -225, -194, -239, -127, -248, + -118, -185, -116, -277, -78, -132, -151, -167, -94, -212, -171, + -287, -162, -301, -224, -259, -202, -317, -238, -263, -235, -226, + -328, -285, -227, -274, -225, -257, -270, -272, -203, -280, -198, + -229, -168, -195, -197, -135, -134, -188, -159, -143, -143, -184, + -122, -175, -115, -188, -85, -144, -140, -133, -147, -139, -167, + -127, -183, -140, -187, -127, -176, -160, -172, -135, -190, -179, + -159, -144, -201, -142, -175, -140, -206, -117, -148, -146, -179, + -121, -175, -129, -135, -92, -156, -126, -159, -68, -176, -108, + -129, -91, -133, -75, -91, -67, -116, -63, -52, -31, -122, + -11, -115, 3, -129, 15, -110, -22, -119, -11, -83, -62, + -34, -39, -48, -27, -43, -19, -79, 23, -48, -1, -86, + 40, -64, 16, -37, 23, -6, -6, 9, 18, 18, 23, + 24, 45, 17, 82, 40, 87, 28, 105, 40, 116, 77, + 81, 80, 85}, + /* IRC_Composite_C_R0195_T090_P330.wav */ + {6, -4, 3, -1, 0, 3, -5, 8, -11, 16, + -21, 29, -39, 57, -101, -332, 89, -835, -16, 8, + -1022, -629, -474, 1050, 924, -984, 2683, 3640, -2207, -5863, + 2261, -3476, 15168, 2953, -3266, 10727, 14390, 3174, -8455, -1966, + 1449, 7129, 3075, -3700, -2692, 1997, 423, 1234, -1087, 216, + -471, 1061, -1107, 458, -1402, 142, 319, -88, -425, 76, + -1043, 226, -230, 12, -536, -207, -663, 129, -455, 253, + -222, -284, -472, 86, -636, 265, -368, -261, -539, -160, + -407, 43, -778, -256, -683, -475, -586, -306, -628, -455, + -647, -525, -513, -416, -500, -430, -812, -419, -489, -274, + -452, -386, -597, -258, -363, -159, -236, -327, -241, -259, + -214, -139, -145, -206, -216, -123, -79, -29, -96, -65, + -217, -263, -177, -100, -202, -186, -272, -223, -240, -138, + -180, -221, -352, -224, -206, -195, -213, -254, -233, -244, + -227, -156, -224, -178, -249, -190, -199, -133, -154, -164, + -183, -191, -143, -185, -101, -190, -166, -201, -98, -150, + -133, -119, -165, -142, -178, -120, -115, -180, -149, -197, + -152, -187, -75, -218, -143, -264, -128, -205, -139, -221, + -148, -269, -159, -215, -126, -214, -123, -256, -105, -233, + -71, -203, -97, -215, -79, -182, -53, -120, -82, -122, + -49, -119, 20, -120, 24, -141, 6, -133, 24, -115, + 14, -116, -39, -71, -8, -71, 6, -93, -11, -108, + 40, -59, 43, -147, 22, -98, 28, -64, 17, -87, + -13, -67, 63, -36, 44, -53, 62, -77, 123, -52, + 145, -81, 82, -33, 141, -4, 116, 8, 98, 14, + 137, 56, 139, -7, 138, 4}, + /* IRC_Composite_C_R0195_T105_P330.wav */ + {61, -61, 61, -61, 62, -62, 62, -62, 63, -63, 63, + -64, 64, -65, 67, -301, 294, -741, -246, -201, 457, -1576, + -144, -73, 2001, -1275, 1763, 1153, 6179, -10039, 184, 1999, 2771, + 10917, -378, 522, 13686, 11704, -7376, -5609, 1748, 7382, 4547, -1426, + -5565, -24, 2068, 630, -441, -1191, 186, 450, -243, 220, -909, + -111, -78, 770, -727, 124, -899, -156, -79, 202, -685, -336, + -650, -314, -474, -53, -350, -261, -756, 191, -463, 128, -417, + -139, -484, -53, -226, -113, -375, -280, -491, -342, -457, -195, + -589, -428, -623, -328, -572, -393, -551, -458, -577, -399, -453, + -401, -526, -392, -496, -278, -442, -319, -518, -259, -377, -158, + -346, -171, -366, -187, -308, -53, -156, -44, -233, -111, -203, + -9, -156, -124, -286, -249, -248, -172, -147, -210, -273, -296, + -215, -151, -154, -166, -209, -170, -207, -76, -118, -174, -253, + -208, -154, -172, -145, -172, -242, -211, -169, -70, -176, -103, + -198, -93, -133, -36, -100, -118, -191, -149, -134, -127, -138, + -220, -229, -254, -179, -191, -192, -230, -211, -235, -161, -158, + -161, -230, -195, -206, -174, -165, -144, -216, -190, -173, -127, + -136, -156, -157, -179, -150, -128, -122, -138, -124, -152, -83, + -109, -30, -73, -66, -103, -55, -77, -32, -75, -113, -125, + -86, -47, -60, -39, -89, -29, -60, 20, -18, -21, -32, + -8, -50, 24, -43, -1, -69, -6, -57, 11, -53, 21, + -58, -2, -13, 35, -10, 30, 5, 44, 12, 60, 31, + 79, -4, 79, 46, 97, 47, 97, 36, 128, 43, 132, + 30, 81, 38}, + /* IRC_Composite_C_R0195_T120_P330.wav */ + {1, 4, -8, 14, -20, 27, -35, 44, -54, 66, -79, + 95, -114, 135, -159, 177, -87, 615, -830, 163, -909, 1304, + -1626, -325, 297, 1206, 69, -817, 4405, 1017, -2560, -616, -3770, + 4387, 14092, -5772, 3858, 10124, 9882, -2074, -2959, -3337, 7410, 7437, + -1780, -6636, -2212, 2639, 1700, -1078, -1371, -262, 1153, -566, 417, + -320, 27, -39, -111, 485, -441, -792, -288, -146, -108, -616, + -333, -464, -419, -367, -303, -262, -308, -516, -133, -270, -200, + -382, -324, -225, -347, -205, -250, -254, -201, -400, -249, -330, + -242, -364, -366, -273, -444, -377, -352, -336, -480, -405, -375, + -496, -436, -362, -461, -436, -395, -354, -445, -362, -274, -397, + -353, -222, -328, -290, -351, -239, -329, -117, -69, -109, -182, + -35, -73, -135, -239, -228, -363, -234, -203, -243, -333, -218, + -233, -231, -229, -116, -190, -172, -177, -158, -144, -144, -172, + -173, -215, -124, -180, -106, -203, -150, -185, -121, -123, -149, + -106, -93, -132, -84, -116, -33, -138, -72, -186, -81, -187, + -113, -205, -194, -255, -235, -238, -195, -263, -202, -264, -198, + -268, -158, -241, -175, -267, -183, -249, -160, -199, -167, -203, + -153, -184, -118, -137, -101, -164, -93, -143, -66, -128, -30, + -127, -42, -113, -31, -78, -23, -59, -78, -78, -48, -79, + -53, -70, -40, -106, -22, -46, -9, -51, -12, -37, -31, + -54, -23, -49, -43, -68, -43, -55, -39, -14, -29, -16, + -13, -8, 32, 0, 28, 5, 31, 33, 68, 33, 69, + 42, 71, 38, 89, 64, 82, 64, 81, 94, 57, 103, + 62, 51, 87}, + /* IRC_Composite_C_R0195_T135_P330.wav */ + {13, -11, 8, -5, 2, 2, -7, 12, -18, 26, -34, + 45, -58, 75, -96, 126, -173, 263, -756, 239, -382, 327, + -504, 374, -988, 304, -762, 1963, 32, 1243, 612, 772, 3408, + -6680, -2100, 10509, 4890, -1044, 6633, 2426, 10000, 1888, -3554, -3952, + 7154, 5846, -3202, -5792, -2133, 1379, 2382, -1044, -683, -846, 1056, + -24, 511, 18, 303, -710, -25, -39, -85, -861, -206, -321, + -34, -428, -245, -428, -30, -639, -166, -459, -198, -463, -354, + -339, -255, -329, -348, -224, -286, -255, -287, -204, -181, -232, + -210, -296, -199, -313, -262, -347, -268, -316, -307, -365, -310, + -360, -308, -316, -324, -335, -371, -355, -377, -361, -310, -401, + -283, -402, -244, -368, -238, -331, -274, -247, -165, -162, -178, + -185, -197, -205, -193, -250, -203, -288, -187, -284, -187, -262, + -183, -219, -206, -232, -196, -178, -177, -216, -242, -166, -214, + -147, -219, -161, -195, -148, -178, -168, -190, -170, -141, -148, + -164, -119, -116, -35, -121, -54, -95, -56, -120, -107, -112, + -149, -152, -172, -185, -195, -227, -175, -253, -211, -280, -223, + -250, -252, -243, -251, -235, -239, -213, -184, -218, -176, -225, + -139, -205, -123, -184, -114, -154, -113, -98, -100, -89, -89, + -91, -67, -61, -32, -65, -41, -68, -21, -70, -18, -87, + -13, -97, -10, -79, -5, -41, -42, -23, -67, 8, -74, + 15, -75, -43, -63, -41, -18, -81, -7, -69, 6, -49, + 2, -9, -7, 21, -8, 24, -11, 44, 32, 44, 38, + 36, 66, 51, 91, 52, 103, 44, 90, 55, 87, 41, + 60, 28, 49}, + /* IRC_Composite_C_R0195_T150_P330.wav */ + {15, -18, 20, -22, 25, -28, 31, -35, 38, -42, 47, + -52, 57, -63, 70, -76, 84, -91, 97, -95, 51, -395, + 348, -546, 384, -515, 230, -347, 78, 797, 314, 1274, 475, + 2177, 1227, -4216, -2348, 10076, 258, 3084, 3609, 6149, 5472, 3566, + -4049, -1019, 5212, 3739, -5321, -3637, -1580, 2270, 1066, -633, -1395, + -136, 1340, -32, 75, -243, 507, -628, 158, 108, -226, -702, + -291, -112, -126, -404, -330, -254, -212, -362, -151, -436, -14, + -424, -215, -583, -44, -508, -144, -379, -62, -360, -158, -295, + -27, -305, -69, -339, -66, -367, -167, -326, -112, -361, -177, + -468, -151, -402, -103, -407, -145, -397, -208, -387, -142, -366, + -207, -368, -232, -411, -224, -349, -159, -358, -152, -366, -101, + -313, -105, -307, -196, -327, -268, -341, -250, -319, -245, -350, + -228, -325, -151, -267, -108, -294, -109, -251, -84, -251, -104, + -207, -91, -253, -103, -244, -91, -252, -85, -272, -130, -311, + -129, -241, -94, -173, -90, -147, -80, -140, -35, -126, -79, + -186, -118, -197, -120, -210, -155, -272, -176, -269, -156, -268, + -151, -277, -170, -275, -153, -283, -181, -265, -186, -257, -194, + -217, -154, -209, -128, -189, -106, -201, -80, -156, -34, -154, + -17, -151, 12, -97, 27, -70, 9, -39, -39, -3, -46, + -29, -50, -61, -28, -93, -9, -89, -8, -103, 1, -74, + -1, -70, -9, -35, -44, -51, -51, -29, -31, -30, -13, + -35, 10, 8, 7, -4, 11, 13, 35, 12, 38, 37, + 49, 36, 56, 47, 69, 81, 52, 69, 15, 90, 11, + 52, -17, 56}, + /* IRC_Composite_C_R0195_T165_P330.wav */ + {-38, 38, -39, 39, -39, 40, -40, 41, -42, 43, -44, + 46, -47, 49, -52, 55, -59, 64, -70, 79, -90, 108, + -143, 381, -197, 374, -312, 378, -424, 566, -428, 1134, 331, + 1309, 501, 1274, 2077, -920, -3821, 4175, 5452, 953, 5098, 2533, + 5399, 3026, 943, -4554, 1848, 1742, -912, -4176, -875, -267, 1882, + -418, -1283, -863, 1021, 217, -28, -198, 445, -494, -277, -183, + 13, -551, -347, -206, -115, -218, -241, -334, -209, -134, -196, + -318, -365, -189, -242, -376, -315, -159, -285, -247, -217, -119, + -295, -192, -233, -63, -298, -72, -221, -78, -309, -142, -336, + -207, -384, -220, -389, -223, -371, -217, -392, -165, -378, -175, + -375, -156, -391, -200, -346, -196, -386, -169, -293, -171, -325, + -127, -312, -180, -413, -244, -413, -275, -437, -268, -434, -258, + -381, -194, -377, -183, -352, -126, -339, -126, -286, -92, -262, + -50, -229, -59, -226, -70, -231, -92, -226, -106, -251, -108, + -219, -79, -217, -73, -199, -48, -168, -54, -223, -113, -224, + -121, -228, -134, -243, -180, -267, -148, -265, -176, -290, -166, + -274, -160, -279, -151, -277, -159, -262, -146, -233, -125, -202, + -129, -198, -90, -162, -74, -165, -69, -153, -33, -141, -13, + -136, 2, -114, 40, -123, 47, -102, 43, -95, 48, -108, + 50, -112, 44, -100, 32, -92, 18, -95, 22, -82, 17, + -82, 23, -102, 19, -84, 22, -128, 24, -87, 44, -69, + 35, -66, 63, -29, 79, -26, 54, -19, 109, -9, 110, + -5, 137, -8, 111, 4, 120, 34, 92, -13, 96, -16, + 93, -54, 92}, + /* IRC_Composite_C_R0195_T180_P330.wav */ + {-26, 26, -26, 25, -25, 25, -24, 23, -23, 22, -21, + 20, -19, 17, -16, 14, -11, 8, -4, -1, 7, -15, + 27, -46, 89, 61, -145, 147, -71, 284, -270, 217, -379, + 526, 177, 1579, 111, 1304, -624, 3003, -1570, -1854, 2371, 5215, + 2766, 3717, 2594, 2071, 4289, 53, -1269, -2397, 2282, -1207, -426, + -1536, -96, 41, 749, -948, -416, 181, 838, -208, 144, -15, + -109, -323, 76, 15, 16, -276, 4, -8, -1, -123, -108, + -209, -91, -130, 71, -58, -162, -230, -119, -177, -166, -207, + -161, -106, -120, -36, -71, -88, -115, -141, -23, -102, -161, + -225, -242, -194, -306, -250, -347, -260, -382, -276, -340, -235, + -319, -231, -283, -196, -267, -199, -293, -242, -296, -261, -227, + -164, -163, -202, -268, -256, -325, -304, -440, -389, -451, -306, + -367, -292, -374, -282, -281, -248, -233, -251, -211, -225, -190, + -161, -223, -149, -241, -144, -229, -124, -173, -155, -158, -194, + -95, -175, -83, -151, -123, -82, -138, -35, -195, -70, -203, + -140, -178, -222, -181, -287, -200, -285, -259, -281, -289, -246, + -302, -246, -260, -227, -227, -242, -189, -224, -180, -220, -169, + -184, -146, -121, -121, -86, -121, -59, -86, -47, -78, -63, + -71, -90, -66, -82, -53, -82, -82, -54, -72, -16, -90, + -24, -88, -44, -31, -45, -32, -70, -43, -25, -59, -33, + -76, -24, -59, -46, -55, -45, -56, -56, -64, -32, -54, + -6, -57, -1, -22, 23, 2, 9, -2, 35, 54, 34, + 72, 22, 84, 15, 94, 10, 75, 34, 67, 47, 21, + 57, 14, 36}, + /* IRC_Composite_C_R0195_T195_P330.wav */ + {19, -19, 19, -20, 20, -21, 21, -22, 23, -23, 24, -24, + 25, -26, 26, -27, 27, -28, 29, -29, 30, -30, 30, -29, + 28, -25, 18, 14, 179, 64, -45, 11, 282, 14, -136, -276, + 1166, 711, 738, -60, 1508, 162, 166, 165, 449, 1532, 5355, 3392, + 931, 2171, 2302, 2446, -1456, -644, -2020, 1267, 216, -603, -1662, -126, + 427, -150, -158, 310, 15, 459, -99, 240, 14, 137, -140, 123, + 54, 92, -77, 39, 37, 33, -11, 20, 88, 136, -9, 51, + -50, -40, -148, -156, -244, -108, -193, -138, -117, -78, -113, -124, + -103, -88, -193, -190, -214, -191, -267, -312, -314, -321, -363, -318, + -371, -313, -329, -304, -279, -293, -244, -274, -270, -284, -268, -270, + -226, -229, -218, -220, -244, -283, -323, -321, -343, -387, -412, -395, + -369, -338, -385, -339, -361, -243, -279, -233, -263, -196, -226, -166, + -214, -179, -210, -188, -205, -188, -194, -186, -176, -189, -187, -188, + -151, -148, -132, -148, -138, -144, -161, -155, -165, -177, -165, -222, + -217, -269, -237, -294, -251, -318, -235, -297, -234, -275, -203, -229, + -195, -221, -176, -219, -151, -204, -120, -189, -136, -166, -97, -148, + -79, -121, -68, -94, -95, -78, -72, -67, -80, -75, -79, -69, + -70, -61, -76, -83, -71, -84, -52, -102, -40, -87, -65, -74, + -59, -59, -44, -53, -35, -32, -38, -24, -30, -33, -41, -31, + -32, -32, -53, -10, -53, 7, -39, 3, 11, -3, 30, 29, + 44, 32, 63, 46, 79, 39, 81, 48, 51, 38, 73, 36, + 50, 22, 77, -11}, + /* IRC_Composite_C_R0195_T210_P330.wav */ + {4, -4, 4, -4, 4, -4, 4, -4, 4, -4, 4, -4, + 4, -4, 4, -4, 4, -4, 4, -3, 3, -3, 2, -2, + 1, 0, -1, 4, -7, 14, -37, -66, 67, -14, -33, -186, + 91, -96, 90, 124, 958, 676, 187, 107, -283, 310, 1299, 1265, + 1017, 1358, 3762, 3924, 898, 885, 85, 1188, 235, 180, -1266, -91, + 662, -183, -222, -323, 535, 278, 681, 120, 382, 366, 409, 299, + 413, 216, 314, 49, 299, 205, 244, 15, 349, 134, 369, -25, + 355, 37, 155, -122, 149, -90, 17, -126, 0, -119, -101, -98, + -45, -101, -108, -137, -41, -165, -128, -190, -90, -220, -248, -213, + -218, -183, -289, -234, -287, -213, -287, -181, -291, -252, -334, -216, + -249, -215, -305, -231, -259, -234, -279, -267, -321, -303, -381, -272, + -334, -278, -372, -241, -327, -217, -356, -191, -320, -190, -311, -163, + -271, -146, -273, -141, -249, -181, -245, -176, -249, -177, -262, -171, + -258, -201, -269, -174, -249, -165, -249, -157, -236, -157, -222, -150, + -201, -185, -232, -186, -231, -212, -272, -199, -268, -197, -269, -185, + -248, -203, -236, -188, -226, -202, -206, -202, -200, -198, -175, -174, + -139, -154, -129, -144, -119, -116, -115, -116, -101, -118, -88, -110, + -115, -124, -128, -104, -139, -117, -161, -110, -128, -96, -116, -100, + -91, -78, -46, -54, -38, -54, -28, -28, -31, -22, -47, -22, + -45, -45, -34, -47, -26, -63, -19, -69, 15, -46, 17, -34, + 36, -7, 28, 11, 12, 21, 18, 27, 33, 12, 47, 14, + 27, 2, 38, -2}, + /* IRC_Composite_C_R0195_T225_P330.wav */ + {-1, 1, -1, 1, -1, 1, -1, 1, -1, 1, -1, 1, + -1, 1, -1, 1, -1, 1, -1, 1, -1, 1, -1, 1, + -1, 1, -1, 1, -1, 1, 2, -68, -59, -5, -96, 48, + -93, -175, -114, 61, 113, 32, 173, 565, 369, -5, 6, 42, + 629, 908, 1232, 1079, 1509, 2842, 2279, 709, 505, 658, 652, 456, + 31, -132, 16, 714, 22, 392, 243, 724, 581, 610, 769, 414, + 780, 295, 589, 257, 440, 139, 342, 210, 326, 138, 213, 205, + 269, 65, 206, 39, 192, -75, 77, -72, 119, -93, 32, -98, + -25, -124, -61, -115, -124, -100, -152, -32, -169, -66, -171, -60, + -174, -142, -204, -153, -209, -250, -258, -282, -284, -268, -319, -270, + -333, -243, -293, -243, -293, -258, -255, -244, -253, -268, -246, -247, + -251, -255, -254, -239, -252, -237, -244, -241, -235, -232, -243, -208, + -257, -193, -268, -204, -268, -227, -248, -235, -265, -257, -230, -233, + -218, -240, -222, -215, -193, -210, -173, -175, -183, -176, -191, -180, + -202, -196, -223, -223, -252, -229, -258, -254, -257, -250, -252, -220, + -262, -227, -250, -215, -243, -223, -230, -208, -202, -158, -170, -134, + -160, -123, -166, -123, -164, -100, -174, -109, -180, -112, -164, -109, + -168, -131, -171, -106, -158, -104, -162, -121, -153, -114, -132, -112, + -125, -95, -82, -74, -52, -73, -50, -54, -44, -32, -38, -20, + -57, -17, -36, -14, -46, -36, -11, -10, -23, -34, -7, -16, + 7, -20, -30, -21, -13, -19, -3, -32, -10, -25, 9, -16, + 17, 18, -2, 23}, + /* IRC_Composite_C_R0195_T240_P330.wav */ + {6, -6, 6, -6, 6, -6, 6, -6, 7, -7, 7, -7, + 7, -7, 7, -7, 7, -7, 7, -7, 8, -8, 8, -8, + 8, -8, 8, -8, 1, -47, -37, -124, -93, -95, -58, -38, + -70, -58, -105, -121, -151, -76, 14, 175, 104, 51, 147, 291, + 191, -40, 158, 429, 1090, 1311, 1373, 1241, 1326, 1526, 1181, 419, + 583, 828, 657, 621, 656, 466, 551, 572, 739, 747, 685, 612, + 423, 667, 583, 601, 619, 480, 483, 323, 428, 299, 276, 192, + 162, 105, 83, 89, 89, 51, 98, -46, 97, -2, 117, -65, + 34, -69, 1, -69, -69, -127, -170, -131, -173, -121, -145, -147, + -134, -204, -102, -232, -83, -273, -123, -325, -159, -309, -183, -275, + -249, -249, -252, -185, -266, -178, -245, -201, -259, -208, -248, -243, + -270, -238, -253, -227, -254, -209, -249, -203, -269, -184, -271, -141, + -258, -121, -230, -113, -188, -120, -198, -167, -178, -200, -214, -242, + -233, -264, -244, -276, -244, -267, -267, -261, -267, -239, -274, -233, + -272, -226, -277, -260, -268, -247, -282, -253, -289, -226, -270, -213, + -255, -189, -247, -158, -192, -146, -173, -170, -171, -177, -143, -184, + -151, -187, -146, -147, -141, -154, -121, -150, -139, -166, -139, -155, + -135, -176, -141, -174, -144, -174, -160, -177, -162, -156, -152, -157, + -114, -126, -89, -103, -86, -98, -78, -68, -62, -53, -84, -35, + -53, -7, -51, -15, -38, -6, -34, -13, -34, -15, -32, -28, + -35, -19, -32, 1, -19, 7, -27, 9, 3, 4, -14, 3, + -5, 36, 2, 41}, + /* IRC_Composite_C_R0195_T255_P330.wav */ + {-1, 1, -1, 1, -1, 1, -2, 2, -2, 2, -2, 2, + -2, 2, -2, 2, -2, 3, -3, 3, -3, 3, -3, 4, + -4, 4, -5, 5, -6, 7, -8, 10, -13, 22, -83, -97, + -106, -152, -99, -59, -90, -92, -128, -74, -117, -122, -12, 68, + 86, 137, 156, 199, 190, 329, 533, 767, 1036, 1233, 1574, 1651, + 1491, 1169, 954, 781, 906, 1168, 950, 370, 704, 786, 629, 605, + 494, 653, 561, 698, 550, 738, 501, 619, 385, 441, 218, 218, + 189, 132, 100, 39, 147, 27, 91, -38, 77, -24, 27, -36, + -30, -17, -41, -95, -49, -122, -68, -169, -78, -186, -103, -225, + -135, -197, -120, -153, -145, -183, -142, -167, -150, -231, -159, -244, + -153, -214, -165, -219, -193, -224, -173, -215, -157, -238, -162, -231, + -149, -238, -155, -243, -185, -203, -182, -175, -206, -184, -214, -165, + -184, -162, -168, -165, -138, -160, -133, -192, -158, -236, -216, -272, + -273, -290, -295, -306, -316, -279, -301, -261, -303, -259, -299, -287, + -289, -292, -273, -302, -247, -283, -207, -253, -202, -230, -201, -189, + -205, -172, -192, -162, -193, -165, -169, -166, -191, -174, -179, -167, + -155, -172, -157, -151, -142, -148, -140, -156, -161, -162, -168, -167, + -172, -156, -177, -164, -174, -152, -168, -156, -171, -153, -173, -152, + -171, -139, -162, -124, -144, -85, -111, -53, -108, -40, -71, -33, + -75, -21, -62, -20, -68, 0, -50, 5, -37, -3, -30, 28, + -23, 3, -23, 12, -19, 28, -12, 24, -5, 41, 17, 24, + -7, 17, -24, 23}, + /* IRC_Composite_C_R0195_T270_P330.wav */ + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 1, -1, 1, -1, 1, -1, 1, -2, 3, + -127, -152, -188, -165, -160, -178, -195, -131, -72, -37, -53, -57, + -34, 48, 106, 135, 127, 188, 385, 765, 884, 1201, 1389, 1575, + 1756, 1541, 1264, 1282, 908, 615, 982, 1053, 498, 403, 667, 706, + 647, 681, 663, 732, 712, 700, 422, 527, 505, 426, 401, 192, + 156, 184, 190, 100, 26, -34, 55, -50, 19, -119, 28, -59, + -54, -94, -103, -24, -139, -24, -165, -13, -154, -5, -159, -81, + -104, -118, -99, -136, -98, -118, -106, -89, -142, -101, -168, -112, + -178, -178, -148, -180, -114, -217, -134, -255, -104, -252, -140, -238, + -148, -235, -187, -236, -202, -242, -197, -183, -140, -137, -108, -143, + -129, -142, -128, -211, -166, -246, -182, -269, -212, -271, -233, -292, + -267, -287, -263, -282, -269, -312, -249, -304, -257, -293, -240, -287, + -230, -257, -242, -248, -234, -224, -236, -219, -241, -188, -227, -176, + -221, -206, -210, -185, -185, -166, -179, -163, -176, -164, -168, -164, + -186, -168, -180, -154, -159, -141, -142, -150, -142, -147, -152, -145, + -159, -150, -168, -167, -187, -177, -186, -176, -192, -163, -197, -167, + -186, -164, -170, -159, -148, -137, -112, -102, -104, -88, -70, -56, + -50, -28, -30, -11, -12, -16, -10, -21, -31, -4, -26, 2, + 15, 8, 10, -3, -2, 5, 10, 12, -9, 0, -5, 9, + -13, -19, -29, -16}, + /* IRC_Composite_C_R0195_T285_P330.wav */ + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 1, -1, 1, -1, 1, + -1, 1, -1, 2, -2, 3, -6, 13, -111, -184, -215, -184, + -140, -139, -179, -220, -168, -247, -124, 1, -91, -84, 42, -80, + 17, -54, 56, 218, 634, 881, 967, 1181, 1587, 1636, 1273, 1643, + 1650, 702, 357, 915, 823, 759, 711, 555, 754, 615, 798, 685, + 720, 707, 793, 638, 630, 510, 595, 460, 429, 211, 188, 178, + 148, 102, 16, 162, 0, 142, -40, 140, -32, 103, 24, -7, + -30, 11, -24, -1, -87, 12, -168, 51, -103, 64, -105, 39, + -118, 10, -91, -91, -119, -125, -90, -149, -75, -130, -138, -153, + -195, -185, -254, -205, -313, -223, -303, -243, -266, -224, -223, -230, + -188, -216, -155, -167, -129, -140, -137, -102, -110, -74, -146, -82, + -177, -89, -192, -110, -215, -160, -231, -232, -220, -242, -237, -262, + -238, -271, -237, -259, -263, -234, -261, -228, -278, -222, -281, -252, + -268, -270, -265, -263, -228, -244, -187, -230, -176, -214, -164, -194, + -141, -175, -152, -173, -143, -161, -171, -179, -174, -184, -157, -194, + -163, -211, -148, -196, -144, -205, -158, -219, -155, -190, -162, -178, + -166, -174, -156, -186, -169, -200, -161, -200, -167, -208, -167, -218, + -158, -186, -143, -155, -119, -123, -104, -117, -86, -101, -54, -82, + -39, -73, -25, -68, -7, -55, -18, -42, 23, -14, 19, -21, + 7, -24, 19, -4, 2, -14, 7, -10, 12, -14, 13, -7, + 26, -23, 8, -14}, + /* IRC_Composite_C_R0195_T300_P330.wav */ + {-3, 3, -3, 3, -3, 3, -3, 3, -3, 3, -3, 3, + -3, 3, -3, 3, -3, 3, -3, 3, -3, 3, -3, 4, + -4, 4, -4, 4, -4, 4, -5, -62, -157, -83, -247, -284, + -202, -303, -294, -235, -54, -305, 15, 156, 241, -112, -479, -226, + 774, -232, 495, 1765, 1236, 1265, 2221, 2705, 1763, 850, 505, 963, + 731, 757, 546, 519, 206, 795, 441, 708, 577, 742, 596, 545, + 491, 526, 666, 413, 377, 307, 300, 457, 188, 260, 34, 250, + 133, 251, 58, 87, 249, 132, 194, -1, 200, 55, 156, 95, + 139, 122, 50, 62, 57, 84, -3, -71, -62, -99, -113, -176, + -122, -254, -180, -227, -166, -230, -202, -293, -196, -279, -248, -326, + -256, -320, -217, -260, -211, -244, -154, -191, -125, -171, -113, -160, + -81, -135, -55, -102, -70, -95, -24, -42, -53, -78, -73, -93, + -89, -149, -139, -195, -180, -235, -192, -245, -233, -268, -234, -245, + -224, -256, -221, -249, -225, -220, -233, -215, -250, -226, -218, -236, + -221, -224, -181, -198, -176, -191, -173, -185, -156, -184, -162, -178, + -171, -172, -156, -184, -183, -174, -192, -186, -179, -189, -180, -212, + -184, -191, -167, -199, -217, -182, -203, -172, -210, -178, -224, -155, + -220, -185, -224, -199, -194, -203, -201, -206, -192, -196, -188, -165, + -158, -177, -147, -145, -112, -128, -98, -113, -74, -77, -54, -65, + -67, -32, -44, -10, -54, -13, -29, -7, -15, -1, -23, 19, + -18, 10, -27, 11, -16, 8, 2, 12, -12, 4, -1, 37, + 18, 19, 17, 36}, + /* IRC_Composite_C_R0195_T315_P330.wav */ + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, -1, 1, -1, 1, -1, 1, -1, 1, -1, + 1, -1, 1, -1, 2, -3, -227, -204, -310, -344, -179, -311, + -505, -298, -88, -402, 115, 712, -76, -577, -211, 9, 558, -404, + 914, 2627, 1047, 2118, 4245, 2542, 908, 788, 1382, 865, 422, 674, + -11, 248, 192, 1314, 191, 817, 366, 966, 306, 467, 192, 289, + 334, 100, 222, 7, 227, 236, 315, 254, 209, 305, 479, 300, + 240, 335, 445, 260, 199, 112, 202, 84, 42, 63, 85, -45, + -100, -80, -73, -157, -181, -120, -209, -167, -259, -161, -262, -234, + -281, -209, -243, -208, -221, -245, -173, -195, -197, -241, -151, -201, + -166, -240, -132, -182, -104, -150, -85, -104, -75, -148, -59, -141, + -36, -144, -33, -133, -71, -89, -7, -47, -109, -70, -108, -44, + -152, -113, -164, -122, -182, -216, -170, -212, -172, -263, -155, -228, + -178, -252, -176, -207, -200, -242, -206, -220, -204, -208, -195, -218, + -188, -193, -156, -217, -159, -200, -119, -211, -133, -181, -131, -177, + -169, -158, -166, -171, -195, -175, -181, -214, -193, -224, -184, -218, + -204, -219, -183, -222, -197, -222, -197, -226, -215, -204, -205, -207, + -216, -212, -210, -224, -222, -213, -210, -205, -202, -194, -174, -155, + -148, -134, -144, -126, -89, -101, -75, -94, -84, -69, -47, -37, + -69, -67, -26, -35, -13, -29, -17, -23, -3, 13, -18, -8, + -26, 22, -30, 20, -15, 30, -13, 8, -11, 23, 49, 30, + 39, 22, 36, 16}, + /* IRC_Composite_C_R0195_T330_P330.wav */ + {9, -9, 9, -10, 10, -10, 10, -11, 11, -12, 12, -12, + 13, -13, 14, -15, 15, -16, 17, -17, 18, -19, 20, -22, + 23, -25, 30, -50, -332, -388, -131, -617, -214, -313, -733, -659, + 255, -155, -66, 699, -77, -372, -1255, -242, 2041, -707, -6, 4736, + 2036, 2809, 3545, 4509, 22, -72, 1207, 1973, -173, 508, 959, 302, + 542, 540, 656, 123, 461, 665, 400, 314, 251, 504, 190, 265, + 35, 373, 467, 91, 154, 128, 447, 203, 170, 90, 232, 80, + 149, 193, 21, -6, -90, -101, -59, -80, -255, -144, -154, -358, + -29, -238, -96, -237, -126, -195, 15, -288, -107, -147, -89, -276, + -120, -223, -134, -174, -181, -152, -158, -179, -71, -138, -116, -177, + -93, -189, -60, -206, -162, -159, -113, -111, -150, -123, -93, -106, + -83, -101, -52, -113, -43, -99, -10, -99, -66, -88, -58, -127, + -64, -152, -99, -152, -144, -200, -127, -259, -116, -227, -129, -245, + -98, -258, -94, -239, -152, -227, -154, -225, -147, -242, -169, -202, + -172, -212, -140, -232, -141, -182, -139, -168, -130, -205, -127, -137, + -140, -176, -175, -160, -166, -171, -205, -188, -193, -212, -199, -238, + -185, -248, -173, -246, -186, -227, -206, -245, -184, -218, -210, -235, + -210, -228, -175, -238, -206, -213, -189, -196, -171, -200, -159, -123, + -146, -114, -128, -128, -81, -88, -83, -80, -52, -124, -70, -58, + -40, -45, -69, -50, -10, -13, -16, -42, -4, -25, 50, -37, + 11, -37, 37, -4, 24, -11, 12, -7, 33, 10, 69, 29, + 60, 20, 87, -1}, + /* IRC_Composite_C_R0195_T345_P330.wav */ + {1, 0, 0, 0, -1, 1, -2, 2, -3, 4, -5, 6, + -7, 8, -9, 11, -13, 15, -17, 20, -24, 28, -33, 40, + -47, 52, -211, -235, -688, -550, -297, -690, -704, -444, 713, -1568, + 989, 470, -39, -1205, 323, -714, 276, -2380, 5748, 2929, 602, 8080, + 5302, -453, -722, 3812, 715, -74, -365, 1626, 369, 1163, 1544, 781, + -4, 229, 668, 441, 364, 676, 214, 441, 36, 409, 174, 565, + -210, 423, -90, 172, -132, 46, -99, 78, -186, 159, -181, 96, + -244, 13, -280, -20, -156, -41, -291, -109, -365, -55, -322, -117, + -274, -136, -169, -159, -189, -140, -191, 12, -178, 0, -295, -70, + -206, -88, -236, -75, -169, -78, -191, -95, -204, -87, -146, -66, + -185, -107, -238, -113, -249, -119, -185, -98, -187, -131, -176, -95, + -137, -31, -142, -17, -163, -50, -120, -40, -114, -19, -100, -43, + -142, -64, -163, -81, -167, -107, -184, -140, -220, -122, -200, -146, + -203, -155, -165, -178, -177, -198, -166, -175, -153, -160, -161, -178, + -163, -171, -143, -138, -136, -139, -146, -131, -151, -132, -167, -143, + -153, -146, -165, -157, -219, -159, -215, -151, -222, -190, -225, -204, + -218, -208, -214, -189, -218, -191, -221, -190, -214, -185, -216, -196, + -215, -178, -200, -217, -218, -194, -162, -182, -170, -189, -185, -152, + -157, -123, -144, -122, -111, -99, -99, -89, -76, -63, -81, -56, + -61, -31, -49, -85, -23, -30, -14, -9, -21, -45, -18, -1, + 16, 3, 5, -5, 5, 8, -5, 64, 39, 55, 37, 73, + 42, 56, 31, 84}}; + +const int16_t irc_composite_c_r0195_p345[][256] = + {/* IRC_Composite_C_R0195_T000_P345.wav */ + {2, -1, 1, -1, 1, -1, 1, -1, 0, 0, 0, + 0, -1, 1, -2, 2, -3, 4, -5, 7, -9, 11, + -15, 21, -203, -88, -927, -196, -604, -22, -2141, -496, -650, + 1585, -548, 803, -263, 412, -5131, 5346, -2103, -7267, 12288, 1672, + 1403, 10338, 7456, -3182, -874, 2680, 1965, -5296, 1356, 1865, 68, + -578, 2523, 1092, 1554, 2056, 2530, 1058, 1432, 379, 1062, -82, + -380, -971, 369, -376, 62, -56, 669, -390, 190, 379, 537, + 98, -46, -35, 315, 348, 110, 183, 491, -38, 19, 211, + 170, -22, -231, -125, -221, 138, -512, -336, -126, 10, -387, + -311, -283, -176, -241, -345, -432, -207, -330, -489, -386, -392, + -351, -423, -274, -457, -352, -361, -311, -213, -391, -323, -302, + -143, -325, -378, -206, -229, -190, -319, -334, -196, -236, -126, + -387, -117, -339, -105, -264, -135, -207, -172, -176, -254, -160, + -183, -177, -200, -218, -201, -189, -116, -205, -120, -224, -113, + -159, -119, -241, -118, -127, -119, -188, -91, -119, -15, -186, + -30, -132, 28, -213, -19, -166, 21, -190, -46, -185, -49, + -159, -65, -168, -65, -190, -59, -218, -46, -217, -24, -231, + -74, -204, -52, -181, -89, -211, -94, -196, -70, -219, -104, + -200, -55, -181, -86, -186, -39, -155, -66, -178, -1, -187, + -47, -188, 6, -142, -11, -161, -5, -114, -9, -113, 6, + -145, -7, -123, -21, -161, -34, -116, -47, -173, -82, -145, + -76, -157, -72, -137, -75, -186, -79, -163, -31, -131, -17, + -156, -51, -99, 26, -74, -5, -101, 37, -82, 48, -83, + 35, -130, 20}, + /* IRC_Composite_C_R0195_T015_P345.wav */ + {-8, 8, -9, 9, -9, 10, -10, 11, -11, 12, -13, + 14, -15, 17, -19, 21, -23, 26, -30, 34, -31, -453, + -1036, -33, -665, 328, -1202, -749, -1695, -16, 154, 1160, 807, + 430, -250, 202, -9659, 9217, 1210, -9352, 17225, 6331, 1506, 7124, + 7939, -4308, -4730, 1188, 3176, -2475, 1424, -48, -469, 790, 3944, + 1309, 680, 1752, 2484, 1048, 341, -282, 654, -429, -824, -317, + 110, -114, -77, 514, 87, -336, 314, 152, 794, -136, 169, + -632, 188, -172, 280, -21, -59, -628, -366, -701, 125, -342, + 195, -484, 14, -385, 142, -203, -25, -63, -111, -438, -237, + -144, -19, -603, -408, -283, -225, -622, -635, -157, -385, -331, + -577, -30, -389, -404, -427, -106, -226, -443, -405, -162, -392, + -396, -469, 17, -462, -322, -529, -39, -465, -308, -396, -19, + -390, -307, -364, 11, -326, -213, -317, -16, -354, -213, -313, + -60, -343, -187, -297, -56, -344, -144, -249, -9, -231, -112, + -162, -24, -173, -102, -136, 27, -150, -25, -210, 56, -219, + 64, -254, 58, -220, 23, -244, -12, -203, -8, -244, -39, + -226, -35, -259, -51, -231, -34, -263, -76, -247, -60, -275, + -44, -227, -27, -295, -46, -242, -22, -251, -17, -192, -18, + -252, -5, -165, 49, -233, 62, -170, 73, -217, 68, -124, + 45, -174, 23, -102, 4, -167, 36, -98, 28, -187, 19, + -138, 2, -165, -15, -133, -54, -155, -42, -139, -76, -178, + -15, -149, -36, -176, 14, -154, -30, -174, 22, -136, -10, + -157, 29, -146, 8, -148, 67, -147, 48, -150, 87, -144, + 95, -120, 68}, + /* IRC_Composite_C_R0195_T030_P345.wav */ + {-5, 4, -2, 1, 1, -3, 5, -8, 11, -14, + 18, -22, 28, -33, 40, -47, 52, -40, -408, -1246, + -239, -636, 412, -1166, -641, -1047, -627, -510, 85, 1903, + 1495, 1980, -2101, -8149, 8335, -7570, 2561, 15248, -2261, 7441, + 12350, 8152, -6960, -897, -1312, -80, 35, 3139, -1223, -111, + 782, 2694, 1229, 2248, 830, 1733, -78, 616, 211, 239, + -1106, 180, -577, 174, -80, 366, -414, -235, 48, 899, + -101, -343, 251, 6, -139, -259, 84, -149, -439, -392, + -377, -353, -641, -180, -347, -645, -512, 18, -444, -412, + -353, -372, 29, -540, -286, -340, 91, -270, 81, -387, + -257, -287, -105, -191, -258, -436, 11, -476, -269, -411, + -52, -449, -147, -462, -317, -370, -394, -207, -298, -370, + -445, -321, -432, -282, -273, -429, -392, -579, -276, -340, + -267, -422, -237, -397, -288, -363, -183, -311, -252, -339, + -167, -369, -181, -301, -85, -308, -243, -297, -206, -198, + -141, -225, -133, -188, -51, -81, -5, -156, 39, -131, + -42, -137, -12, -105, -59, -165, -33, -91, -103, -131, + -61, -132, -15, -166, -70, -196, -74, -136, -77, -240, + -119, -181, -167, -207, -153, -210, -116, -267, -128, -236, + -136, -218, -68, -191, -86, -215, -66, -87, -41, -115, + -57, -149, 39, -73, 32, -134, 21, -60, 58, -96, + 16, -33, 13, -37, -10, -59, 0, -62, -11, -108, + -26, -126, 5, -173, -25, -133, -22, -188, -62, -152, + -27, -144, -75, -133, -61, -105, -49, -114, -43, -128, + -43, -94, -30, -105, -18, -125, -14, -97, 30, -123, + 23, -65, 42, -66, 35, -56}, + /* IRC_Composite_C_R0195_T045_P345.wav */ + {-8, 9, -9, 10, -11, 12, -14, 15, -17, 18, -20, + 23, -26, 29, -32, 27, -914, -992, 248, -1147, 746, -600, + -1411, -744, -565, -282, 187, 2409, 2683, -221, -1093, -6934, 4033, + -4276, 11276, -297, 4328, 20553, 5592, -1738, -1835, 48, -2700, 3273, + 333, -1462, -612, 3497, 663, 1061, 1222, 2492, 754, 1168, -36, + 273, -1174, -272, -1054, 891, -844, 132, -415, 315, -117, 257, + -335, 440, -662, 37, -141, 164, -657, 106, -514, -245, -612, + -181, -587, -119, -354, -272, -370, 70, -460, -182, -631, -316, + -708, -110, -803, -99, -670, -498, -427, -54, -498, -454, -369, + -295, -361, -311, -286, -279, -189, -173, -337, -51, -132, -118, + -321, -115, -360, -197, -296, -134, -359, -223, -370, -154, -278, + -322, -354, -297, -436, -355, -372, -287, -465, -352, -358, -269, + -478, -297, -336, -244, -461, -251, -336, -244, -377, -295, -277, + -241, -357, -246, -212, -210, -283, -149, -224, -96, -233, -70, + -163, -39, -261, -32, -98, -58, -219, -32, -96, -67, -134, + 7, -101, -50, -139, 18, -115, -50, -164, -70, -125, -101, + -200, -71, -185, -130, -181, -136, -176, -132, -203, -147, -173, + -138, -199, -159, -176, -127, -141, -96, -163, -71, -143, -38, + -126, -27, -77, -59, -103, 5, -43, -32, -25, -36, -5, + -42, 40, -34, 18, -45, 1, -59, 7, -47, -28, -49, + -55, -63, -33, -55, -79, -59, -82, -109, -94, -40, -81, + -89, -121, -82, -83, -49, -111, -62, -79, -97, -59, -52, + -74, -97, -50, -98, -31, -116, 11, -85, 32, -106, 43, + -88, 79, -70}, + /* IRC_Composite_C_R0195_T060_P345.wav */ + {2, 0, -1, 2, -4, 7, -10, 13, -18, 24, -33, + 46, -69, 124, -1066, 18, -1103, -58, -112, -921, -201, 357, + -2572, -292, 84, 3712, 705, 1846, -3182, 733, -1997, -2694, 2428, + -595, 23701, 7754, -2253, 5078, 3945, -1381, 793, -3491, -1163, 3714, + 3613, -2506, 296, 680, 1730, 1310, 1740, 569, 508, 99, -722, + -392, -453, -871, 66, -328, -669, -71, -484, -147, -35, 146, + 49, -532, -397, -139, -139, -522, -373, -116, -571, -415, -445, + -69, -274, -275, -429, -422, -271, -360, -287, -382, -22, -407, + -385, -428, -388, -334, -422, -213, -696, -500, -450, -310, -488, + -360, -442, -369, -341, -387, -436, -315, -312, -227, -524, -284, + -360, -145, -448, -103, -329, -255, -335, -170, -226, -169, -275, + -186, -277, -175, -313, -153, -328, -106, -340, -187, -359, -196, + -395, -186, -410, -204, -401, -242, -445, -212, -399, -269, -333, + -214, -369, -275, -249, -157, -224, -209, -240, -93, -196, -87, + -134, -136, -170, -118, -141, -152, -108, -143, -171, -122, -121, + -75, -167, -118, -171, -34, -161, -63, -166, -73, -182, -54, + -115, -102, -170, -130, -176, -78, -151, -106, -187, -132, -212, + -115, -143, -110, -184, -158, -155, -100, -103, -72, -126, -49, + -120, -26, -63, 20, -83, 6, -50, 25, -17, 26, -29, + 39, -31, 8, -37, 34, -77, 20, -77, 4, -80, -26, + -93, -19, -70, -86, -71, -70, -82, -94, -51, -114, -61, + -80, -78, -73, -90, -68, -70, -42, -80, -51, -78, -111, + -16, -79, -18, -107, -18, -114, -3, -27, -1, -36, -19, + -21, -10, 39}, + /* IRC_Composite_C_R0195_T075_P345.wav */ + {13, -13, 13, -13, 13, -13, 13, -14, 14, -14, 14, + -14, 13, -149, -486, -602, -335, -455, -212, -518, 0, -1171, + -540, -311, 1579, 3683, 3360, -5772, -426, -1232, -715, 5798, -5995, + 21017, 15656, -2523, -3222, 5350, 1893, 1313, -2953, -885, 4326, 1209, + -279, -1161, 869, 861, 1767, 953, -85, 210, -139, -663, -17, + -553, 268, -673, -215, -760, -236, -646, -139, -393, -427, -462, + -55, -562, -131, -287, -193, -567, -273, -625, -345, -491, -150, + -487, -111, -512, -283, -603, -50, -418, -328, -443, -111, -346, + -355, -349, -35, -305, -248, -516, -491, -547, -144, -443, -470, + -467, -394, -484, -216, -365, -370, -505, -259, -440, -318, -386, + -196, -476, -376, -474, -304, -362, -195, -334, -380, -413, -292, + -310, -213, -289, -263, -333, -155, -255, -179, -252, -180, -265, + -153, -285, -190, -264, -147, -236, -237, -304, -248, -194, -182, + -204, -186, -250, -173, -172, -144, -229, -114, -172, -167, -183, + -136, -161, -106, -122, -161, -186, -139, -156, -91, -182, -139, + -216, -134, -193, -139, -180, -115, -198, -155, -201, -90, -175, + -115, -207, -160, -200, -119, -149, -119, -195, -93, -130, -96, + -135, -84, -130, -121, -106, -85, -107, -61, -110, -53, -77, + 2, -45, -15, -51, -20, 4, 33, -23, -40, -36, 1, + -53, 16, -46, -46, -69, 1, -46, -34, -47, -35, -46, + -45, -73, -11, -76, -35, -107, -21, -105, -57, -70, -68, + -101, -73, -74, -59, -95, -48, -78, -69, -89, -82, -50, + -61, -44, -75, -23, -52, -44, 10, -43, -23, -47, 63, + -13, 31, 19}, + /* IRC_Composite_C_R0195_T090_P345.wav */ + {-40, 45, -52, 60, -69, 79, -92, 108, -129, 156, -197, + 267, -462, -149, 208, -635, -826, 268, -390, -920, 244, -62, + -1260, -320, 2937, 4521, -564, -4829, 2706, -6796, 9962, -6783, 6356, + 23745, 4526, -4606, -155, 5413, 3814, -1343, -3207, 4343, 2939, -733, + -2665, 998, 738, 1892, 332, -416, -415, 112, -618, 265, -680, + -48, -750, 405, -729, 216, -673, -99, -730, -121, -855, -310, + -693, -146, -680, -198, -695, -260, -522, -283, -523, -143, -597, + -333, -173, -342, -516, -211, -519, -364, -344, -136, -671, -213, + -411, -202, -398, -103, -578, -290, -294, -138, -306, -230, -402, + -283, -321, -289, -337, -289, -473, -414, -279, -358, -413, -275, + -431, -453, -313, -307, -448, -301, -341, -461, -354, -322, -417, + -358, -269, -383, -284, -242, -302, -305, -257, -285, -308, -182, + -268, -314, -270, -212, -228, -190, -249, -276, -158, -168, -143, + -161, -146, -207, -91, -103, -158, -110, -126, -88, -73, -13, + -110, -35, -46, -106, -57, -116, -140, -215, -98, -197, -197, + -174, -246, -265, -202, -185, -227, -181, -218, -214, -125, -146, + -175, -178, -153, -217, -80, -87, -137, -151, -98, -122, -94, + -88, -136, -141, -168, -115, -121, -70, -143, -102, -56, -76, + -36, 6, -27, -49, 15, 48, -26, 23, -19, 19, -21, + 34, -59, 8, -22, -47, 2, -38, -69, -64, -5, -74, + -105, -59, -66, -97, -94, -96, -78, -97, -52, -113, -64, + -142, -56, -66, -39, -129, -40, -66, -56, -70, 5, -96, + -30, -55, 4, -41, 13, -55, 20, -26, 32, -22, 71, + -18, 17, 30}, + /* IRC_Composite_C_R0195_T105_P345.wav */ + {-67, 70, -74, 77, -81, 86, -90, 96, -101, 108, + -115, 124, -133, 69, -792, -387, 271, -596, -717, 859, + -864, -953, -174, 2178, -1005, 1282, 2249, 6443, -10856, 1162, + -3444, 7735, 12493, -6470, 12789, 13090, -1110, -7098, 1720, 6128, + 6522, -3157, -2418, 823, 2649, -1545, -82, 75, 412, 176, + -227, -1102, -27, 27, -108, -705, 430, -492, -144, -623, + 458, -659, -400, -488, -145, -709, -561, -722, -446, -592, + -392, -773, -320, -376, -219, -435, -230, -433, -344, -257, + -282, -422, -405, -416, -298, -443, -368, -500, -217, -519, + -166, -433, -279, -366, -130, -440, -244, -224, -238, -388, + -222, -308, -238, -178, -169, -397, -297, -264, -256, -355, + -298, -417, -426, -330, -305, -333, -364, -344, -434, -356, + -283, -355, -382, -364, -318, -364, -236, -292, -356, -334, + -239, -297, -263, -218, -274, -335, -226, -222, -269, -272, + -264, -249, -207, -170, -177, -194, -173, -129, -105, -104, + -107, -136, -46, -4, 64, 55, 23, -30, -30, -40, + -113, -234, -240, -226, -142, -197, -242, -304, -222, -141, + -127, -177, -202, -148, -92, -95, -154, -222, -191, -141, + -114, -142, -174, -129, -161, -67, -128, -34, -186, -89, + -116, -46, -141, -77, -118, -122, -94, -41, -91, -117, + -59, -17, -48, -43, -49, -43, -26, 26, 1, -33, + -44, -8, 1, 24, -53, -52, -67, -16, -53, -12, + -77, -55, -119, -25, -52, -50, -119, -50, -99, -48, + -93, -38, -125, -90, -97, -23, -111, -77, -96, -23, + -117, -28, -57, -25, -82, -14, -37, -3, -37, 48, + -10, 30, -29, 94, 17, 67}, + /* IRC_Composite_C_R0195_T120_P345.wav */ + {-3, 4, -6, 7, -9, 12, -14, 18, -21, 26, -32, + 40, -51, 68, -105, -237, -115, -196, -240, -710, 503, -404, + -299, -610, 1573, -1052, 1538, 3156, 4734, -10274, 2428, 47, 3883, + 7208, -4454, 16464, 10228, -4264, -5316, 5989, 6995, 2607, -3203, -668, + 1640, 904, -2642, -469, 203, 1050, -729, -1586, -33, 324, -227, + 138, -165, -76, 50, -374, -903, 385, -637, -751, -371, 7, + -958, -172, -379, -347, -638, -287, -600, -251, -397, -596, -514, + -222, -363, -323, -357, -509, -371, -223, -433, -356, -378, -472, + -223, -149, -381, -362, -256, -360, -249, -245, -414, -343, -215, + -318, -296, -253, -281, -260, -186, -267, -334, -184, -218, -221, + -298, -323, -280, -235, -266, -346, -291, -364, -250, -269, -330, + -407, -337, -288, -297, -291, -367, -331, -313, -227, -277, -310, + -324, -354, -249, -294, -307, -326, -246, -243, -266, -239, -219, + -134, -170, -168, -181, -178, -179, -104, -105, -182, -117, -16, + 21, -1, -16, -123, -97, -70, -125, -190, -191, -224, -174, + -191, -187, -240, -111, -186, -142, -146, -92, -134, -84, -176, + -166, -110, -109, -146, -139, -231, -119, -73, -70, -193, -83, + -143, -72, -63, -63, -163, -69, -90, -96, -97, -84, -157, + -37, -91, -88, -116, -29, -99, -31, -73, -68, -76, 24, + -65, -8, -90, -71, -32, 33, -119, -46, -74, -20, -92, + 37, -144, -45, -100, 37, -86, 3, -132, 2, -42, -29, + -104, -4, -152, -16, -85, -19, -175, 14, -94, -9, -106, + -24, -72, 42, -77, -27, -22, 2, -33, 78, -29, -21, + 21, 111, -5}, + /* IRC_Composite_C_R0195_T135_P345.wav */ + {-8, 8, -9, 9, -9, 10, -10, 11, -11, 11, -12, + 12, -12, 13, -12, 12, -14, -199, -345, 194, -292, -20, + -363, 219, -924, 441, 900, 798, -558, 3720, 1651, -3208, -514, + -4991, 13814, 1350, -1342, 10873, 8019, 1098, -3160, 1031, 6880, 5706, + -3917, -4208, 1661, 708, -1088, -1854, -201, 229, -177, -273, -648, + -82, 475, 5, -734, 169, -151, -581, -653, 471, -491, -236, + -587, -109, -475, -248, -581, -317, -480, -514, -555, -293, -524, + -197, -571, -212, -393, -202, -577, -116, -375, -344, -418, -218, + -355, -263, -348, -305, -348, -210, -371, -214, -394, -179, -361, + -184, -347, -189, -348, -251, -234, -293, -267, -213, -252, -242, + -269, -147, -370, -183, -325, -201, -333, -191, -269, -230, -243, + -283, -228, -273, -293, -252, -342, -267, -359, -215, -428, -224, + -365, -239, -330, -269, -348, -257, -251, -286, -250, -246, -272, + -177, -242, -155, -277, -116, -236, -94, -218, -96, -109, -104, + -71, -63, -76, -143, -84, -111, -151, -139, -218, -149, -229, + -159, -185, -165, -192, -165, -156, -158, -137, -140, -159, -114, + -167, -123, -164, -97, -148, -122, -112, -140, -90, -115, -78, + -111, -104, -86, -114, -49, -142, 12, -166, -28, -168, -17, + -113, -58, -89, -81, -116, -67, -58, -65, -96, -68, -92, + -33, -99, -37, -87, -67, -90, -37, -68, -114, -65, -87, + -50, -124, -30, -83, -1, -96, -4, -55, -19, -87, 13, + -80, -10, -90, -5, -84, -42, -79, 4, -67, -63, -38, + -27, -36, -23, -7, 11, -34, 14, -18, 51, -74, 41, + 9, 19, -7}, + /* IRC_Composite_C_R0195_T150_P345.wav */ + {9, -10, 11, -12, 13, -14, 16, -18, 20, -22, 25, + -28, 32, -36, 41, -48, 57, -69, 92, -187, -93, -65, + 148, -471, 61, 62, -108, -513, 1146, 660, 197, 907, 4643, + -2481, -3069, 2384, -436, 10331, -1444, 3529, 10790, 4133, -3114, 1101, + 5334, 2231, 175, -4639, -668, 1364, -942, -1832, -179, 106, -201, + -621, -288, 256, -14, 7, -70, 10, -62, -163, -538, -397, + 250, -662, -393, -549, -98, -597, -308, -388, -216, -452, -393, + -291, -335, -411, -150, -445, -160, -290, -89, -416, -142, -299, + -403, -360, -239, -271, -373, -246, -311, -338, -233, -221, -259, + -317, -192, -311, -255, -207, -237, -276, -272, -275, -277, -233, + -252, -250, -181, -293, -170, -245, -207, -274, -163, -258, -249, + -201, -205, -239, -243, -229, -292, -303, -216, -288, -278, -312, + -241, -345, -256, -284, -302, -346, -258, -265, -246, -271, -236, + -265, -215, -277, -207, -240, -196, -255, -168, -178, -155, -144, + -107, -152, -110, -104, -74, -180, -121, -180, -122, -196, -144, + -245, -129, -207, -127, -182, -107, -225, -82, -192, -124, -217, + -97, -227, -156, -196, -108, -174, -119, -141, -77, -130, -30, + -126, -49, -162, -24, -161, -3, -167, -53, -117, -53, -120, + -68, -81, -70, -85, -45, -100, -52, -108, -40, -95, -39, + -123, -50, -123, -64, -117, -58, -114, -101, -106, -61, -84, + -90, -69, -81, -96, -40, -52, -52, -64, -50, -43, -17, + -37, -37, -34, -28, -67, -18, -49, -26, -48, -2, -23, + -34, -24, 7, 4, -14, 13, 9, -1, -21, 7, 5, + 3, -11, -9}, + /* IRC_Composite_C_R0195_T165_P345.wav */ + {-16, 16, -16, 16, -16, 17, -17, 17, -17, 18, -18, + 18, -18, 19, -19, 19, -19, 19, -19, 19, -18, 15, + -25, -180, 39, 144, -199, 84, -114, -19, -188, 1254, 741, + 544, 587, 3703, -1657, -2617, 2350, 59, 8499, 1392, 3068, 7765, + 4121, -1476, -112, 3753, -281, 908, -3882, -912, -13, 232, -1645, + 225, -231, 30, -284, 242, 364, 101, -416, -192, -215, -224, + -211, -588, -413, 94, -532, -410, -203, -224, -476, -149, -162, + -206, -299, -157, -323, -203, -258, -155, -285, -195, -259, -160, + -310, -161, -188, -219, -325, -228, -252, -329, -249, -318, -288, + -289, -215, -279, -273, -237, -265, -253, -240, -166, -308, -223, + -268, -204, -257, -185, -200, -214, -215, -169, -225, -233, -270, + -224, -251, -229, -254, -245, -245, -239, -242, -271, -250, -255, + -257, -319, -229, -320, -280, -334, -233, -335, -241, -307, -225, + -319, -212, -265, -239, -293, -245, -235, -213, -218, -190, -158, + -146, -114, -164, -138, -176, -126, -194, -163, -222, -185, -202, + -181, -221, -210, -185, -167, -169, -169, -149, -149, -121, -122, + -91, -166, -97, -138, -90, -161, -94, -134, -93, -167, -101, + -146, -89, -171, -96, -130, -90, -122, -75, -116, -77, -102, + -73, -86, -102, -91, -92, -79, -122, -82, -99, -73, -113, + -89, -86, -78, -89, -91, -110, -85, -90, -61, -78, -110, + -68, -88, -12, -123, -18, -120, 12, -117, 12, -119, 3, + -74, 28, -74, -4, -63, 7, -46, 0, -67, 7, -52, + 21, -52, 5, -33, 17, 5, -5, -18, -6, 3, -14, + -17, -13, -5}, + /* IRC_Composite_C_R0195_T180_P345.wav */ + {14, -14, 15, -15, 15, -16, 16, -17, 18, -18, 19, -20, + 20, -21, 22, -23, 25, -26, 28, -31, 33, -37, 43, -53, + 84, -38, 163, -71, -158, 103, 194, -92, 53, 482, 91, 607, + 1972, 2332, -1572, -66, -260, 1180, 4049, -804, 6508, 7047, 3077, -817, + 1693, 1744, 560, 329, -3303, 313, -242, -704, -831, 249, -87, -229, + 183, 93, 110, 39, -28, -382, -360, -250, -254, -387, -156, -146, + -568, -151, -222, 7, -192, -113, -168, -1, -224, -13, -332, -98, + -241, -68, -295, -42, -210, -57, -298, -57, -251, -128, -317, -192, + -299, -254, -305, -255, -341, -262, -251, -237, -260, -298, -266, -251, + -226, -202, -261, -186, -262, -164, -227, -165, -247, -142, -218, -142, + -250, -192, -303, -264, -330, -282, -325, -281, -325, -258, -295, -215, + -312, -214, -308, -229, -274, -251, -309, -306, -294, -311, -271, -299, + -256, -284, -250, -220, -205, -223, -287, -266, -239, -195, -135, -147, + -134, -149, -99, -176, -199, -275, -263, -263, -286, -264, -295, -223, + -247, -187, -176, -146, -128, -120, -103, -115, -67, -85, -89, -116, + -143, -100, -117, -73, -127, -58, -97, -32, -121, -65, -147, -77, + -157, -103, -172, -126, -140, -135, -140, -152, -146, -132, -154, -119, + -152, -105, -161, -118, -130, -105, -117, -95, -88, -108, -66, -84, + -63, -104, -55, -48, -36, -55, -20, -46, -8, -40, 5, -55, + -25, -61, -13, -59, -44, -51, -47, -33, -43, -29, -37, -41, + -40, -27, -26, -28, -32, -23, -2, -7, 24, -14, -21, -16, + -7, -26, -52, -44}, + /* IRC_Composite_C_R0195_T195_P345.wav */ + {6, -6, 6, -7, 7, -7, 7, -7, 7, -7, 8, -8, + 8, -8, 9, -9, 9, -10, 10, -11, 11, -12, 14, -15, + 17, -21, 38, -16, 75, -220, -59, 106, 197, -177, 5, -30, + -104, 721, 1540, 1146, -1007, 1149, 558, -528, 530, 189, 5406, 5771, + 1722, 1227, 2989, 2251, -549, 199, -1093, 649, 351, -1192, -797, 86, + 474, -276, 161, -98, 513, -55, 324, -259, 35, -250, 203, -294, + -48, -254, -20, -107, 43, -102, 74, 7, 58, 5, 31, 16, + -81, -113, -13, -32, -61, -116, -90, -161, -57, -141, -149, -200, + -221, -127, -211, -175, -301, -256, -303, -211, -249, -283, -281, -255, + -186, -218, -164, -228, -189, -224, -141, -185, -133, -235, -194, -211, + -160, -216, -234, -256, -252, -243, -263, -310, -294, -327, -270, -308, + -298, -307, -298, -251, -283, -236, -306, -260, -308, -239, -300, -271, + -297, -271, -265, -262, -234, -272, -265, -257, -272, -222, -275, -166, + -216, -116, -201, -132, -209, -165, -253, -247, -300, -289, -266, -286, + -274, -274, -215, -180, -166, -157, -142, -89, -78, -66, -105, -65, + -95, -58, -92, -79, -99, -64, -63, -57, -120, -89, -112, -118, + -168, -135, -190, -134, -199, -134, -197, -155, -189, -166, -196, -156, + -193, -139, -163, -148, -164, -142, -130, -130, -110, -101, -105, -93, + -75, -95, -30, -101, -23, -80, -12, -54, -3, -54, -21, -40, + -15, -43, -40, -44, -53, -61, -41, -51, -45, -65, -44, -54, + -43, -56, -70, -49, -64, -4, -42, 4, -66, 5, -19, 28, + -23, -34, -32, -54}, + /* IRC_Composite_C_R0195_T210_P345.wav */ + {-2, 2, -2, 2, -2, 2, -2, 1, -1, 1, -1, 1, + -1, 0, 0, 0, 1, -1, 1, -2, 3, -3, 4, -6, + 7, -9, 13, -20, 43, -89, -222, 10, 103, -107, -34, -36, + 142, -400, -30, 464, 582, 998, 189, -102, 357, 928, -496, 3, + 2407, 3014, 4127, 2156, 97, 2572, 2367, 192, -1077, 553, 225, 383, + -471, -511, -191, 662, 222, 22, 339, 294, 362, 131, 306, 286, + 107, 75, 262, 201, 100, 126, 189, 137, 172, 10, 156, 70, + 103, -4, -19, -25, -76, -86, -32, -105, -27, -153, -34, -185, + -5, -211, -79, -248, -114, -227, -127, -231, -155, -259, -155, -256, + -173, -269, -198, -265, -178, -210, -163, -209, -150, -214, -137, -236, + -119, -268, -155, -295, -185, -296, -225, -287, -248, -275, -270, -274, + -255, -294, -292, -305, -257, -323, -287, -335, -263, -321, -272, -309, + -253, -288, -250, -283, -256, -298, -279, -264, -275, -235, -286, -206, + -259, -185, -242, -173, -250, -179, -242, -182, -246, -189, -235, -187, + -222, -180, -217, -154, -198, -159, -175, -140, -165, -133, -123, -114, + -152, -125, -126, -116, -132, -139, -111, -130, -90, -128, -110, -154, + -128, -148, -134, -149, -163, -162, -154, -157, -161, -181, -160, -162, + -162, -166, -161, -175, -145, -160, -146, -183, -159, -151, -121, -142, + -111, -108, -118, -82, -70, -76, -70, -81, -45, -58, -32, -55, + -61, -50, -56, -13, -58, -19, -65, -23, -55, -34, -79, -47, + -53, -75, -77, -59, -49, -49, -45, -23, -65, -15, -50, -1, + -35, -44, -51, -18}, + /* IRC_Composite_C_R0195_T225_P345.wav */ + {4, -4, 4, -4, 5, -5, 5, -5, 5, -5, 5, -5, + 5, -5, 5, -6, 6, -6, 6, -6, 6, -6, 7, -7, + 7, -7, 8, -8, 8, -8, 9, -22, -68, -102, -39, -33, + -2, -125, -43, -65, -81, -88, 504, 491, 271, 136, 50, 242, + 413, 575, 246, 1154, 3270, 2343, 1173, 1360, 1386, 1335, 693, 41, + -16, 626, 497, -208, 385, -141, 534, 407, 815, 196, 540, 538, + 608, 534, 497, 553, 348, 395, 314, 320, 191, 167, 89, 75, + -22, 80, -26, 77, -33, 113, -103, 86, -85, 66, -164, -34, + -138, -32, -150, -66, -143, -81, -162, -134, -151, -126, -180, -176, + -206, -176, -215, -184, -242, -174, -257, -171, -240, -144, -234, -156, + -238, -145, -213, -186, -241, -198, -236, -200, -304, -237, -309, -223, + -315, -256, -320, -273, -306, -276, -282, -301, -281, -293, -281, -297, + -281, -281, -278, -300, -296, -308, -282, -258, -291, -257, -276, -228, + -231, -194, -203, -185, -194, -181, -173, -165, -183, -152, -208, -152, + -227, -158, -213, -156, -222, -165, -206, -169, -192, -153, -170, -166, + -179, -180, -153, -149, -139, -152, -153, -126, -111, -90, -107, -114, + -128, -119, -123, -144, -150, -167, -156, -182, -180, -204, -160, -191, + -173, -198, -185, -178, -167, -163, -174, -180, -175, -186, -163, -152, + -140, -147, -132, -122, -102, -97, -72, -79, -75, -66, -54, -46, + -62, -28, -64, -26, -69, -23, -69, -28, -68, -48, -80, -55, + -84, -64, -71, -62, -66, -82, -71, -59, -36, -53, -39, -61, + -45, -48, -32, -64}, + /* IRC_Composite_C_R0195_T240_P345.wav */ + {1, -1, 1, -1, 1, -1, 1, -2, 2, -2, 2, -2, + 2, -2, 2, -2, 2, -2, 2, -2, 2, -2, 3, -3, + 3, -3, 4, -4, 5, -7, 11, -87, -91, -69, -34, -17, + -83, -130, -72, -103, -104, -134, 114, -4, -17, 85, 373, 202, + -49, -101, 219, 365, 876, 782, 785, 1628, 2056, 1505, 913, 728, + 668, 1183, 655, 491, 465, 893, 831, 430, 584, 712, 1020, 649, + 708, 723, 730, 613, 504, 566, 414, 315, 187, 278, 133, 226, + 116, 259, 129, 186, 116, 185, 45, 88, -32, 30, -121, -4, + -138, -59, -242, -77, -196, -88, -164, -95, -154, -158, -144, -123, + -166, -155, -131, -163, -193, -126, -158, -197, -221, -177, -191, -199, + -207, -207, -232, -259, -213, -257, -223, -245, -210, -232, -214, -199, + -250, -237, -259, -286, -308, -300, -276, -316, -306, -323, -315, -283, + -322, -259, -334, -262, -308, -246, -279, -237, -252, -210, -235, -204, + -211, -204, -216, -219, -187, -208, -184, -188, -172, -182, -147, -178, + -171, -209, -148, -207, -140, -214, -153, -202, -132, -178, -141, -158, + -129, -140, -132, -148, -104, -170, -94, -160, -103, -179, -127, -186, + -148, -195, -164, -196, -178, -176, -178, -191, -206, -200, -201, -208, + -201, -208, -200, -195, -166, -167, -160, -164, -138, -176, -171, -170, + -145, -157, -125, -127, -91, -104, -77, -96, -65, -75, -71, -96, + -65, -84, -60, -84, -57, -72, -49, -85, -51, -85, -52, -82, + -51, -93, -48, -69, -42, -81, -54, -69, -69, -69, -62, -61, + -61, -54, -82, -67}, + /* IRC_Composite_C_R0195_T255_P345.wav */ + {1, -1, 1, -1, 1, -1, 1, -1, 1, -1, 1, -1, + 1, -1, 1, -1, 1, -1, 1, -1, 1, -1, 1, -1, + 1, -1, 2, -2, 2, -2, 2, -2, 3, -3, 5, -29, + -81, -145, -126, -102, -79, -99, -83, -32, 26, -25, -104, -85, + -9, -133, -67, -57, 165, 465, 613, 590, 447, 814, 1074, 1050, + 1231, 1480, 1384, 1691, 1545, 721, 693, 1027, 967, 911, 897, 961, + 823, 595, 850, 817, 872, 522, 568, 476, 335, 222, 147, 215, + 189, 255, 142, 248, 142, 175, 30, 4, 8, -59, -17, -77, + -20, -108, -60, -97, -148, -132, -187, -115, -138, -127, -111, -127, + -113, -156, -101, -162, -102, -180, -108, -179, -120, -179, -195, -199, + -229, -185, -270, -215, -278, -237, -258, -241, -261, -210, -244, -198, + -260, -213, -287, -247, -340, -255, -344, -240, -343, -221, -330, -201, + -299, -199, -288, -250, -262, -234, -258, -219, -245, -213, -243, -209, + -226, -210, -206, -229, -185, -199, -171, -204, -168, -186, -151, -175, + -154, -197, -147, -168, -130, -180, -149, -171, -122, -158, -129, -152, + -132, -156, -139, -160, -164, -170, -174, -154, -162, -126, -151, -134, + -162, -148, -177, -191, -199, -212, -200, -209, -220, -215, -220, -217, + -209, -214, -187, -188, -163, -169, -145, -176, -134, -160, -141, -157, + -132, -139, -118, -136, -115, -131, -104, -132, -107, -134, -96, -102, + -76, -89, -80, -83, -80, -72, -65, -58, -64, -74, -61, -66, + -46, -79, -66, -94, -77, -85, -74, -86, -77, -80, -67, -80, + -78, -81, -61, -58}, + /* IRC_Composite_C_R0195_T270_P345.wav */ + {1, -1, 1, -1, 1, -1, 1, -1, 1, -1, 1, -1, + 1, -1, 1, -1, 2, -2, 2, -2, 2, -2, 2, -2, + 2, -3, 3, -3, 3, -3, 4, -4, 5, -5, 7, -13, + -9, 25, 107, 159, 235, 223, 228, 197, 135, 148, 180, 104, + 249, 332, 345, 533, 679, 812, 953, 1029, 929, 1077, 1230, 1424, + 1718, 1696, 1725, 1631, 997, 986, 1012, 146, -166, 111, 353, 347, + -6, -43, 73, 193, 90, 62, -93, -92, -338, -310, -405, -336, + -407, -441, -401, -511, -347, -531, -317, -432, -349, -454, -400, -327, + -440, -322, -442, -318, -433, -382, -424, -384, -375, -393, -396, -458, + -428, -480, -383, -434, -382, -464, -371, -432, -337, -394, -307, -399, + -335, -417, -322, -442, -324, -436, -303, -449, -304, -409, -298, -363, + -284, -317, -307, -308, -323, -288, -319, -293, -322, -285, -281, -248, + -239, -242, -269, -244, -286, -239, -275, -228, -281, -236, -270, -215, + -230, -183, -189, -147, -168, -142, -140, -138, -148, -145, -122, -112, + -122, -100, -101, -92, -130, -111, -155, -125, -154, -111, -187, -117, + -190, -95, -171, -106, -163, -109, -148, -117, -153, -132, -173, -135, + -163, -114, -185, -122, -191, -116, -168, -92, -150, -108, -141, -94, + -122, -95, -150, -97, -145, -88, -132, -65, -125, -53, -97, -33, + -83, -12, -48, -5, -27, 7, -11, 35, -15, 26, -18, 26, + -29, 3, -32, -9, -39, 15, 11, 7, 13, 12, 6, 12, + 11, 20, 5, 2, 4, 3, -12, -24, -23, -5, -3, -22, + 4, 5, 1, 4}, + /* IRC_Composite_C_R0195_T285_P345.wav */ + {0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 1, -1, + 1, -1, 1, -1, 1, -1, 1, -1, 1, -2, 2, -2, + 2, -2, 3, -3, 3, -4, 5, -6, 8, -11, 22, -196, + -173, -113, -87, -175, -179, -137, -214, -236, -209, -197, -203, -89, + 115, 94, 63, 76, -21, -123, -58, 550, 837, 1139, 1631, 2133, + 1768, 1607, 1992, 1501, 200, 482, 1172, 950, 840, 796, 772, 845, + 1106, 940, 1271, 764, 758, 561, 601, 592, 456, 196, 274, 22, + 269, 50, 133, 94, 119, 18, 4, 153, 30, 97, 24, 85, + 20, 33, 14, -38, -26, -152, -46, -172, -60, -155, -115, -148, + -198, -89, -195, -112, -261, -100, -214, -212, -183, -197, -150, -219, + -140, -179, -164, -230, -178, -187, -230, -226, -257, -158, -281, -201, + -228, -189, -261, -210, -220, -259, -267, -256, -233, -276, -267, -249, + -227, -294, -254, -243, -276, -267, -297, -257, -292, -260, -305, -245, + -279, -264, -243, -230, -212, -214, -151, -148, -130, -127, -88, -74, + -103, -75, -88, -67, -122, -69, -96, -106, -123, -96, -146, -128, + -172, -140, -152, -152, -190, -154, -179, -153, -191, -155, -209, -127, + -197, -114, -197, -134, -193, -131, -195, -130, -172, -126, -176, -158, + -191, -169, -206, -154, -203, -183, -230, -172, -205, -185, -204, -179, + -196, -157, -166, -116, -150, -115, -126, -82, -123, -71, -85, -83, + -83, -68, -72, -63, -78, -79, -49, -75, -79, -92, -70, -98, + -66, -95, -75, -105, -104, -85, -82, -92, -105, -73, -107, -87, + -82, -75, -79, -91}, + /* IRC_Composite_C_R0195_T300_P345.wav */ + {-1, 1, -1, 1, -1, 1, -1, 1, -1, 2, -2, 2, + -2, 2, -2, 2, -2, 2, -2, 2, -2, 3, -3, 3, + -3, 4, -4, 5, -6, 8, -10, 16, -33, 4, 109, 115, + 258, 119, 96, 128, 286, 309, 258, 544, 795, 778, 461, 339, + 836, 601, 406, 1682, 1975, 1154, 2276, 2834, 1223, 701, 931, 69, + -668, -254, 489, 635, -539, 173, 339, 531, 396, 94, 191, -102, + 150, -127, -75, -251, -176, -409, -489, -562, -686, -623, -666, -571, + -644, -520, -569, -434, -437, -442, -489, -355, -376, -438, -545, -399, + -497, -467, -538, -479, -549, -412, -479, -420, -458, -466, -435, -392, + -372, -360, -342, -413, -353, -363, -348, -419, -344, -451, -370, -415, + -367, -468, -389, -500, -406, -491, -367, -474, -360, -481, -350, -435, + -296, -421, -285, -402, -267, -363, -222, -350, -221, -334, -215, -270, + -177, -235, -146, -214, -118, -175, -89, -140, -81, -133, -96, -92, + -54, -91, -56, -86, -58, -64, -32, -51, -40, -76, -57, -55, + -36, -113, -96, -83, -105, -91, -129, -152, -109, -131, -124, -142, + -150, -156, -119, -136, -143, -152, -125, -140, -130, -126, -129, -99, + -125, -119, -110, -121, -109, -118, -102, -117, -98, -117, -101, -93, + -114, -105, -94, -69, -112, -75, -86, -60, -49, -25, -59, 6, + -33, -1, -8, 8, -2, 17, 23, 0, 29, 13, 17, 24, + 18, 21, 18, 23, -13, 11, -8, 15, 5, 10, -10, 8, + -24, 7, 1, -11, 1, 27, -4, 48, 5, 53, 24, 86, + 43, 68, 50, 99}, + /* IRC_Composite_C_R0195_T315_P345.wav */ + {1, -1, 1, -1, 1, -1, 1, -1, 1, -1, 1, -2, + 2, -2, 2, -2, 2, -2, 2, -2, 2, -2, 2, -2, + 2, -2, 2, -2, 2, -1, 0, 6, -137, -110, -314, -483, + -264, -184, -467, -482, 89, -336, 204, 277, 141, -93, -770, -71, + 819, -837, 1577, 3127, 1312, 2383, 3512, 2495, -80, 350, 890, 699, + 71, 561, 954, 294, 1113, 1117, 1259, 495, 984, 849, 877, 553, + 746, 524, 537, 393, 441, 428, 542, 429, 296, 210, 204, 231, + 133, -55, 33, -7, -111, -11, -91, 34, -190, -56, -131, 39, + -14, 66, -79, 58, -96, 105, -60, 169, -64, 84, -143, 118, + -100, -84, -157, -91, -304, -233, -224, -201, -251, -259, -309, -306, + -307, -286, -299, -265, -407, -357, -301, -345, -348, -313, -294, -342, + -334, -338, -298, -266, -288, -261, -298, -254, -268, -227, -236, -260, + -244, -223, -227, -260, -233, -195, -202, -171, -228, -156, -170, -114, + -170, -107, -146, -105, -89, -96, -83, -40, -73, -59, -66, -65, + -71, -53, -107, -74, -75, -92, -122, -102, -105, -87, -130, -86, + -139, -115, -142, -131, -148, -147, -160, -144, -151, -139, -173, -139, + -186, -126, -170, -147, -194, -137, -189, -141, -184, -162, -186, -151, + -208, -180, -195, -172, -186, -165, -204, -165, -157, -155, -139, -132, + -141, -108, -126, -89, -110, -98, -101, -113, -94, -111, -133, -99, + -126, -121, -126, -97, -115, -103, -122, -84, -114, -58, -120, -106, + -109, -97, -103, -75, -128, -91, -104, -73, -91, -53, -59, -46, + -63, -33, -22, -14}, + /* IRC_Composite_C_R0195_T330_P345.wav */ + {-9, 9, -9, 9, -9, 9, -9, 9, -9, 9, -9, 9, + -9, 9, -9, 9, -9, 9, -9, 10, -10, 10, -10, 10, + -10, 10, -10, 10, -7, -378, -458, 118, -356, -539, -320, -251, + -977, -150, 272, 171, 262, 290, -552, -354, -2031, 2393, -54, -667, + 4888, 3245, 1575, 3792, 4173, -210, -907, 432, 1658, -334, 338, 623, + 940, 723, 1598, 1314, 1216, 764, 1038, 976, 401, 116, 250, 473, + 239, 334, 528, 294, 340, 65, 340, 194, 348, 152, 304, 92, + 279, 123, 93, -24, 89, -192, -125, -56, -130, 68, 194, -86, + 77, 174, 220, 128, 98, -234, -216, -168, -160, -251, -283, -410, + -399, -240, -227, -363, -327, -361, -302, -370, -271, -312, -309, -428, + -402, -318, -223, -266, -304, -253, -264, -272, -246, -203, -278, -299, + -319, -254, -236, -260, -281, -188, -203, -212, -220, -185, -220, -178, + -230, -207, -215, -227, -248, -182, -197, -163, -179, -101, -143, -112, + -138, -84, -115, -82, -111, -83, -102, -83, -51, -69, -49, -89, + -18, -67, -73, -108, -72, -63, -79, -111, -167, -94, -130, -99, + -166, -109, -181, -121, -156, -124, -154, -131, -162, -169, -165, -170, + -136, -151, -159, -185, -163, -122, -139, -141, -175, -141, -148, -148, + -172, -157, -130, -154, -150, -148, -142, -153, -141, -116, -159, -97, + -155, -91, -162, -92, -141, -53, -134, -97, -132, -68, -111, -97, + -127, -98, -128, -105, -137, -81, -141, -76, -120, -73, -136, -97, + -105, -48, -102, -73, -108, -56, -97, -41, -75, -56, -70, -20, + -33, -9, -51, -32}, + /* IRC_Composite_C_R0195_T345_P345.wav */ + {1, -2, 2, -3, 4, -4, 5, -6, 7, -8, 9, + -10, 11, -13, 14, -16, 17, -19, 21, -22, 24, -25, + 25, -22, 13, 15, -152, -764, 140, -602, 18, -900, -519, + -1092, -545, 489, 261, 706, -1198, 1895, -1912, -2689, 2900, 30, + -3177, 8188, 3071, 2405, 4243, 6073, 615, -3233, 507, 1858, -510, + -537, 1688, 110, 230, 1724, 2185, 1547, 1263, 1871, 1145, 573, + 342, -23, 261, -614, -9, -121, 805, -137, 38, 285, 272, + -29, 312, 280, 184, 234, 346, -32, 603, 80, 194, 153, + 323, -216, 458, 198, 10, -275, 28, -275, -40, -248, -266, + -416, -305, -349, -213, -336, -338, -460, -267, -400, -247, -403, + -303, -379, -264, -460, -298, -347, -227, -420, -309, -405, -246, + -238, -250, -319, -267, -250, -266, -221, -240, -318, -184, -236, + -175, -305, -161, -225, -161, -213, -190, -219, -158, -206, -155, + -198, -165, -211, -169, -178, -141, -180, -148, -191, -154, -191, + -120, -222, -141, -171, -68, -131, -104, -142, -95, -72, -93, + -71, -91, -94, -90, -92, -83, -97, -91, -117, -108, -109, + -102, -101, -101, -116, -95, -82, -119, -115, -158, -119, -137, + -108, -151, -151, -163, -123, -125, -148, -167, -118, -168, -149, + -196, -146, -169, -130, -179, -147, -164, -153, -114, -109, -119, + -128, -121, -82, -111, -89, -98, -81, -102, -105, -71, -85, + -85, -116, -85, -98, -76, -124, -102, -118, -107, -84, -126, + -141, -114, -78, -80, -110, -127, -121, -93, -112, -96, -109, + -98, -90, -55, -53, -66, -54, -41, -26, -18, -19, -5, + -33, -57, -33}}; + +const int16_t irc_composite_c_r0195_p000[][256] = + {/* IRC_Composite_C_R0195_T000_P000.wav */ + {1, -1, 1, -1, 0, 0, 0, 0, 0, 1, -1, + 2, -2, 2, -3, 4, -5, 6, -7, 9, -11, 14, + -20, 33, -756, -534, -348, 102, -1032, -582, -1506, 65, -959, + 133, -210, 1928, 576, -291, -5368, 5774, -6962, -2265, 14308, -3483, + 7090, 10599, 2076, -2731, 3149, -192, -3412, -1652, 3866, 548, -1226, + -1088, 2012, 2077, 1960, -209, 1087, 1404, 1871, 1150, 1665, 536, + 626, 1200, 1268, 592, -114, -160, -95, 133, -37, -246, 158, + -144, -190, -548, 421, 96, 105, -192, 181, 129, 158, 143, + 73, -164, -261, -64, -147, -128, -55, -166, -60, 98, 349, + -253, 126, -172, 110, -391, -118, 265, -317, 256, -97, 416, + -62, 274, -244, -355, -104, -427, -357, -741, -368, -489, -365, + -418, -498, -150, -384, -361, -530, -257, -297, -381, -491, -511, + -393, -429, -294, -395, -373, -408, -292, -342, -260, -297, -286, + -427, -324, -291, -291, -276, -319, -244, -386, -221, -241, -141, + -260, -273, -231, -153, -86, -203, -202, -152, -95, -140, -176, + -127, -110, -156, -237, -148, -139, -94, -248, -190, -202, -85, + -155, -205, -223, -169, -158, -183, -216, -167, -164, -146, -219, + -103, -162, -71, -220, -10, -169, 7, -134, 32, -76, -42, + -100, -66, -50, -44, -71, -88, -122, -40, -75, 21, -78, + -18, -125, -33, -69, 53, -77, -22, -138, -41, -62, -55, + -92, -83, -99, -115, -150, -17, -53, -12, -184, -50, -96, + 28, -81, -1, -78, -41, -118, -14, -66, 38, -82, -29, + -104, 29, 5, 34, -62, 14, 7, 47, -61, 67, -43, + 51, -145, 61}, + /* IRC_Composite_C_R0195_T015_P000.wav */ + {123, -125, 127, -129, 131, -133, 135, -137, 138, -140, + 141, -141, 141, -140, 137, -131, 122, -106, 76, -10, + -238, -830, -319, -1069, 17, -859, 165, -1424, -309, -2002, + 526, 695, 3378, -155, -423, -6705, 9134, -12025, 1719, 14823, + -7846, 16119, 11510, -1243, -4202, 5454, -1293, -2996, -1156, 2894, + -853, -440, -59, 2319, 814, 827, 428, 2620, 1829, 1181, + 817, 1748, 469, 1133, 599, 1385, -490, 316, -1523, 369, + -621, 391, -835, 160, -740, 77, -149, 751, -195, -103, + -622, 191, -100, 342, -303, -90, -465, 82, 62, 259, + -409, -385, 44, 87, -50, 21, 54, -467, -207, -79, + -324, -108, -90, -152, -620, 71, -110, 47, -749, -161, + -523, -153, -446, -157, -336, 40, -137, -86, -507, -177, + -322, -14, -430, -277, -557, -360, -326, -296, -274, -463, + -359, -502, -263, -388, -316, -369, -353, -459, -464, -335, + -293, -324, -333, -369, -324, -380, -288, -268, -287, -294, + -317, -270, -395, -181, -204, -149, -383, -214, -212, -113, + -188, -174, -211, -188, -99, -166, -173, -203, -111, -134, + -148, -226, -166, -126, -86, -216, -199, -234, -119, -193, + -118, -250, -172, -245, -101, -156, -114, -172, -153, -162, + -98, -70, -71, -106, -70, -81, -14, -14, 37, -38, + -36, -107, -3, -9, 52, -91, -63, -116, 20, -50, + 18, -151, -37, -125, 57, -97, 31, -149, 5, -133, + 4, -141, 9, -132, -7, -168, 9, -124, 33, -144, + 22, -166, 15, -117, 46, -156, 11, -127, 36, -124, + 23, -58, 69, -65, 71, -34, 110, -36, 130, -45, + 96, -68, 96, -59, 38, -71}, + /* IRC_Composite_C_R0195_T030_P000.wav */ + {-63, 66, -69, 73, -76, 80, -84, 89, -93, 98, + -102, 107, -110, 111, -108, 95, -54, -154, -785, -302, + -1153, 268, -1139, 240, -1422, 320, -2283, 272, -629, 4046, + 237, 3112, -7137, 2285, 231, -13166, 20036, -5068, 11914, 16533, + 2581, -5753, 2916, 1267, -3077, -1961, 1642, -123, 210, 665, + 1080, 1069, 485, 1122, 1846, 1948, 901, 1486, 921, 1132, + 470, 865, -186, -57, -358, -153, -489, -118, -724, -365, + -517, -156, -335, 155, -255, 96, -315, 376, -169, 22, + -435, 5, -544, -35, -484, -12, -457, 280, -453, -177, + -399, 435, -423, -188, -484, 161, -217, -38, -732, -416, + -157, 73, -480, -440, -302, -140, -260, -335, -490, -420, + -470, -291, -617, -112, -315, 20, -559, -210, -412, 154, + -162, -102, -453, -225, -205, -23, -247, -203, -409, -147, + -366, -147, -367, -150, -477, -326, -502, -222, -390, -294, + -498, -249, -423, -313, -510, -239, -382, -176, -516, -351, + -496, -92, -337, -150, -462, -212, -366, -74, -288, -94, + -322, -110, -334, -53, -240, 11, -306, -108, -332, -45, + -271, -52, -295, -76, -308, -99, -319, -57, -226, -50, + -303, -130, -254, -54, -183, -89, -261, -98, -193, -6, + -184, -12, -164, 45, -175, 27, -120, 129, -69, 82, + -89, 120, -26, 113, -48, 59, -77, 18, -85, -3, + -107, 0, -78, -29, -159, 0, -62, 67, -139, -9, + -138, 19, -136, -3, -124, -27, -149, -30, -136, -11, + -97, -7, -137, -19, -96, 20, -84, 15, -64, 28, + -92, 9, 15, 121, -12, 24, -32, 74, 36, 98, + -9, 3, -30, 36, -42, 4}, + /* IRC_Composite_C_R0195_T045_P000.wav */ + {72, -74, 77, -79, 82, -86, 91, -96, 102, -111, 121, + -134, 152, -179, 228, -635, 490, -1252, -114, -1202, 742, -1779, + 495, -1563, 532, -2895, 2675, 1167, 4718, -4709, 4646, -5559, -6673, + 7608, -8298, 21201, 11442, 4596, 920, 5308, -1818, -1353, -1082, -283, + -725, 2349, -984, -140, 614, 1696, 1859, 2003, 557, 2080, 1067, + 416, 1112, 525, 38, -87, -851, -336, 38, -375, -495, -291, + -403, -90, -299, -393, -130, -647, -37, -270, -227, -182, -2, + -485, -21, -380, -221, -179, -364, -244, -343, -247, -161, -213, + -443, -201, -386, -349, -198, -453, -401, -479, -187, -349, -331, + -486, -40, -343, -132, -248, -346, -150, -185, -498, -200, -280, + -512, -271, -152, -472, -405, -421, -235, -444, -272, -369, -299, + -285, -209, -483, -192, -140, -287, -241, -120, -328, 36, -232, + -275, -345, -26, -488, -78, -319, -205, -488, 22, -390, -294, + -391, -131, -450, -198, -416, -347, -391, -95, -530, -266, -359, + -162, -403, -129, -438, -172, -313, -124, -327, -146, -338, -102, + -245, -121, -328, -124, -244, -79, -277, -128, -320, -47, -241, + -144, -306, -70, -274, -85, -274, -147, -219, -71, -212, -78, + -214, -60, -127, 0, -182, -21, -106, 67, -97, 35, -113, + 84, 16, 65, -67, 99, -24, 79, 7, 29, -85, 99, + -35, 17, -91, 57, -100, 58, -112, -18, -130, 73, -160, + -25, -148, 6, -136, 45, -217, 2, -142, 79, -180, 64, + -198, 81, -156, 114, -200, 98, -162, 123, -120, 152, -170, + 176, -85, 130, -107, 189, -99, 158, -113, 138, -127, 161, + -140, 111, -221}, + /* IRC_Composite_C_R0195_T060_P000.wav */ + {123, -132, 142, -153, 166, -180, 196, -215, 238, -264, 296, + -338, 397, -545, -26, -1070, 548, -1189, 312, -1132, -540, -593, + 703, -1932, 23, 1834, 2821, 3650, -4896, 2244, -8990, 3547, -3667, + 11688, 19991, 4002, -67, 5265, -1066, -618, 257, -237, -93, 2490, + -1554, -1096, 1601, 944, 1501, 1048, 1374, 1754, 1792, 566, 335, + -350, 932, -271, -1525, -771, -646, -466, 146, -610, 113, -1035, + 510, -222, -23, -608, -443, -725, 203, -887, -434, -589, -19, + -438, -40, -610, 14, -467, -137, -372, -214, -629, -55, -580, + -263, -517, -350, -563, -317, -499, -281, -660, -233, -369, 6, + -533, -180, -457, -90, -331, -140, -395, -150, -246, -124, -225, + -134, -388, -151, -322, -330, -363, -280, -394, -241, -503, -138, + -532, -169, -402, -263, -433, -255, -363, -244, -372, -271, -364, + -268, -337, -126, -472, -58, -441, 6, -373, -75, -345, -91, + -290, -171, -369, -128, -311, -189, -312, -183, -241, -230, -309, + -78, -303, -168, -337, -178, -236, -164, -270, -238, -313, -113, + -261, -195, -270, -208, -224, -136, -312, -153, -332, -79, -287, + -193, -257, -149, -227, -124, -297, -82, -209, -107, -181, -87, + -173, -18, -146, 9, -126, 28, -74, -22, -25, 22, -50, + 40, -41, 25, -17, -14, 5, 9, -67, 23, -34, 57, + -46, 5, 3, 37, -39, 6, -36, 11, -54, -39, -69, + -61, -79, -35, -99, -38, -98, -29, -88, -12, -86, -19, + -71, -19, -37, -12, -58, 7, -41, 34, -37, 47, -22, + 42, -26, 97, 2, 68, -13, 74, 7, 35, -6, 34, + -1, 0, -38}, + /* IRC_Composite_C_R0195_T075_P000.wav */ + {30, -35, 41, -47, 55, -63, 72, -84, 97, -113, 134, + -162, 202, -472, -304, -372, 274, -1176, -371, -433, -535, -54, + 364, -1802, 1909, 3073, 5773, -6217, -2323, -1631, -4047, 10163, -533, + 23315, 9574, -7064, 764, 4451, 279, -686, 938, 2910, -1594, -781, + 31, 2302, -241, 1013, 1336, 1127, 1162, 699, 28, 761, 380, + -426, -314, -917, -750, -202, -732, -632, -472, -517, -73, -213, + -19, -699, -215, -440, -368, -595, -354, -716, -262, -604, -448, + -306, -492, -226, -481, -404, -393, -315, -472, -290, -442, -401, + -227, -459, -350, -606, -194, -594, -308, -438, -436, -423, -217, + -461, -220, -298, -300, -185, -218, -342, -120, -282, -142, -260, + -82, -276, -46, -282, -219, -258, -93, -447, -193, -418, -296, + -377, -249, -479, -177, -358, -304, -416, -185, -366, -179, -486, + -287, -406, -134, -512, -167, -454, -201, -352, -195, -386, -169, + -289, -223, -334, -186, -280, -123, -302, -167, -268, -36, -301, + -139, -264, -133, -195, -111, -276, -131, -175, -123, -195, -148, + -226, -77, -211, -165, -231, -123, -257, -132, -285, -135, -289, + -147, -278, -141, -274, -166, -238, -154, -189, -134, -212, -101, + -127, -6, -142, -21, -106, 41, -81, 41, -105, 54, -54, + 6, -41, -3, -62, 27, -57, -25, -42, 29, -39, 10, + -90, 56, -38, 43, -106, 38, -63, 27, -58, -21, -110, + -19, -82, -36, -124, -57, -83, 23, -113, 9, -73, 55, + -109, 34, -55, 45, -13, 62, -54, 27, -33, 52, 12, + 35, -34, 72, -19, 33, -32, 74, -59, 39, -25, 53, + 1, 10, -3}, + /* IRC_Composite_C_R0195_T090_P000.wav */ + {-3, 3, -4, 5, -6, 7, -9, 11, -13, 17, + -23, 36, -967, 38, -432, 430, -1179, 327, -1275, 279, + -1297, 1913, -1800, 360, 826, 8131, -4106, 182, -6660, -1308, + 8763, -6147, 21004, 13661, -1493, -3341, 815, 6070, 1330, -1696, + 3689, 487, -871, -2812, 2923, 1220, 319, -441, 2019, 161, + 1147, -214, 819, -1063, 179, -1133, 125, -1120, 701, -1109, + -441, -308, -3, -1227, 135, -661, -529, -162, -375, -951, + -262, -371, -533, -415, -491, -644, -459, -451, -508, -551, + -360, -465, -434, -399, -416, -458, -204, -423, -358, -363, + -290, -434, -346, -447, -174, -475, -424, -400, -239, -439, + -175, -468, -418, -188, -90, -409, -214, -233, -225, -147, + -140, -439, -299, -104, -157, -229, -145, -201, -232, -109, + -193, -310, -284, -271, -306, -318, -342, -330, -320, -362, + -208, -295, -346, -295, -212, -394, -231, -297, -357, -348, + -177, -356, -301, -275, -291, -319, -184, -255, -307, -178, + -245, -228, -206, -194, -280, -137, -218, -191, -183, -203, + -186, -105, -193, -151, -164, -132, -164, -77, -203, -113, + -167, -192, -164, -75, -239, -194, -160, -166, -186, -77, + -232, -182, -148, -63, -204, -92, -164, -69, -54, 4, + -140, -18, -43, 36, -61, 41, -116, -2, -92, 42, + -126, -61, -150, -8, -117, -15, -62, -69, -114, 61, + -14, 10, -74, 8, -25, 119, -37, 0, -69, 10, + -56, 37, -94, -111, -86, 30, -107, -71, -119, 4, + -92, 38, -125, 29, -50, 58, -57, 64, -84, 92, + -4, 67, -73, 54, -8, 88, -67, 71, 21, 64, + -36, 123, -7, 23, -36, 115}, + /* IRC_Composite_C_R0195_T105_P000.wav */ + {67, -63, 58, -51, 43, -33, 19, -1, -24, 61, -121, + 244, -927, 183, -686, 327, -496, -330, -486, -340, -314, 396, + 182, -1212, 1259, 6063, -556, -2378, -2582, -3838, 9614, -8512, 18557, + 14219, -782, -2750, 529, 5714, 4180, -1314, 2811, 874, -1413, -2611, + 2334, 2590, -537, -811, 436, 1284, 515, -112, -879, -106, -221, + -558, -955, -184, -84, -312, -553, -219, -379, -172, -456, -790, + -576, -350, -708, -606, -553, -357, -404, -422, -537, -414, -291, + -515, -518, -625, -263, -417, -492, -719, -360, -360, -368, -573, + -481, -304, -142, -306, -536, -356, -164, -154, -398, -429, -388, + -147, -288, -315, -532, -144, -267, -143, -527, -186, -329, 6, + -447, -226, -375, 20, -329, -159, -444, -77, -329, -143, -450, + -93, -327, 2, -390, -87, -374, 55, -418, -96, -445, -37, + -436, -129, -505, -118, -444, -152, -487, -172, -395, -135, -432, + -170, -368, -96, -414, -145, -394, -32, -326, -123, -434, -83, + -276, -94, -355, -149, -242, -56, -256, -166, -269, -75, -228, + -119, -313, -87, -207, -58, -304, -97, -234, -23, -285, -126, + -259, -11, -242, -91, -247, -1, -204, -10, -242, -5, -194, + 74, -169, 102, -92, 170, -62, 135, -93, 108, -134, 5, + -190, -8, -203, -47, -223, 16, -187, 1, -174, 58, -131, + 78, -113, 75, -130, 98, -75, 44, -121, 101, -58, 53, + -146, 47, -70, 70, -128, -19, -108, 21, -89, -23, -119, + -27, -39, 31, -124, -51, -37, 65, -45, 0, 4, 66, + 6, 17, 11, 14, 30, 64, 44, 27, -16, 79, -9, + -11, -60, 55}, + /* IRC_Composite_C_R0195_T120_P000.wav */ + {-76, 75, -75, 74, -72, 69, -65, 59, -49, 35, + -14, -21, 85, -263, 347, 27, -533, -67, -385, -42, + -335, 645, -2065, 1305, 1033, 1922, -2255, 6706, -3555, 1838, + -9048, 1510, 15767, 522, 6485, 7416, 2582, -1635, -1123, 7704, + 6890, -3746, -2349, -815, 4058, -1305, 633, -1081, 2060, -1086, + 490, -1435, 1038, -465, -103, -1558, 594, -576, -246, -797, + -411, -729, 216, -627, -444, -642, 122, -670, -147, -796, + -255, -906, 76, -717, -449, -584, -119, -685, -337, -606, + -352, -326, -478, -502, -420, -468, -445, -428, -466, -351, + -293, -513, -155, -443, -243, -371, -82, -573, -58, -388, + -209, -303, -195, -394, -165, -220, -376, -192, -262, -255, + -289, -234, -278, -284, -271, -328, -227, -325, -185, -358, + -162, -289, -168, -301, -148, -278, -221, -237, -227, -212, + -236, -164, -236, -223, -147, -209, -256, -222, -209, -360, + -125, -328, -206, -254, -219, -307, -162, -296, -276, -174, + -324, -187, -268, -155, -286, -159, -317, -134, -252, -195, + -181, -236, -204, -162, -148, -226, -141, -227, -132, -141, + -218, -189, -165, -114, -288, -111, -256, -76, -252, -50, + -308, -80, -158, -63, -217, -126, -102, 12, -10, -77, + 13, 74, 33, 49, -22, 50, -56, 57, -177, -13, + -201, 19, -144, -101, -181, 59, -32, -49, -45, -72, + -16, 32, -40, -48, -33, -42, -45, 64, -145, 8, + -69, 33, -144, 44, -92, 56, -118, -2, -60, 8, + -77, -3, -91, -37, -73, -11, -53, 0, -49, 56, + -45, 17, -73, 100, 6, 64, -7, 125, -47, 94, + 22, 33, -55, 12, -30, 7}, + /* IRC_Composite_C_R0195_T135_P000.wav */ + {-47, 46, -46, 44, -43, 40, -37, 33, -28, 21, -12, + -1, 20, -48, 94, -206, 95, -290, -258, 153, -592, 203, + -293, 297, -1153, 1348, 644, 1791, -1433, 6094, -3856, -260, -5383, + 6042, 11744, -1699, 5991, 8208, 1865, -2201, 2510, 6226, 4590, -5126, + -1361, 782, 2433, -2030, 854, -833, 853, -1117, 568, -1205, 680, + -1070, -374, -797, 196, -738, -381, -460, 54, -509, 80, -809, + -117, -538, -17, -916, 203, -876, -170, -815, -257, -782, -178, + -520, -324, -433, -344, -353, -383, -382, -403, -522, -240, -512, + -312, -523, -189, -533, -114, -477, -247, -366, -173, -363, -247, + -204, -351, -273, -220, -239, -329, -247, -257, -330, -207, -331, + -212, -342, -200, -298, -246, -261, -277, -228, -264, -213, -328, + -212, -273, -219, -256, -244, -234, -265, -159, -264, -191, -244, + -157, -229, -190, -221, -203, -203, -265, -198, -238, -189, -235, + -164, -189, -220, -183, -216, -189, -257, -194, -227, -199, -234, + -209, -214, -214, -195, -212, -179, -255, -200, -207, -151, -257, + -182, -194, -172, -200, -169, -219, -215, -208, -180, -207, -192, + -215, -173, -203, -137, -219, -101, -232, -121, -175, -4, -148, + -45, -92, 5, -35, 0, -5, -45, 0, 8, 10, -53, + -107, -5, -81, 6, -157, 3, -130, 19, -128, 39, -138, + 5, -65, 57, -90, 7, -52, 45, -79, 13, -78, 29, + -109, 14, -79, 25, -126, 7, -61, -11, -75, 1, -51, + -46, -64, -35, -74, -40, -48, 36, -46, 3, -64, 53, + -59, 33, -26, 105, 4, 63, 13, 64, -28, 24, -1, + 3, -80, -21}, + /* IRC_Composite_C_R0195_T150_P000.wav */ + {-5, 6, -7, 7, -8, 9, -10, 11, -12, 13, -15, + 16, -17, 19, -20, 20, -17, 1, -234, 227, -421, 117, + -50, -175, -40, 33, -222, 178, 1388, 586, 910, 1027, 4228, + -7050, 1223, 2017, 6153, 4669, 1121, 8628, 6102, -3597, 1339, 6488, + 2001, -1618, -2670, 715, 665, -9, -1202, 163, -501, 22, -977, + 268, -483, 330, -922, 182, -469, 62, -791, -223, -334, -215, + -485, -347, -491, -176, -429, -178, -696, -63, -705, -191, -727, + -100, -715, -18, -605, -161, -531, -137, -475, -197, -485, -201, + -433, -135, -408, -232, -404, -136, -441, -131, -469, -151, -475, + -144, -469, -155, -431, -165, -416, -207, -381, -199, -342, -208, + -344, -172, -301, -189, -339, -193, -297, -159, -303, -169, -283, + -144, -295, -190, -323, -198, -312, -172, -324, -214, -279, -169, + -264, -159, -230, -157, -232, -160, -206, -167, -240, -200, -232, + -191, -234, -175, -238, -189, -225, -142, -206, -214, -206, -200, + -152, -232, -176, -232, -184, -217, -152, -205, -194, -188, -192, + -160, -199, -163, -211, -186, -234, -194, -218, -212, -270, -194, + -202, -181, -219, -182, -202, -169, -194, -142, -192, -142, -138, + -58, -121, -63, -104, -7, -62, -12, -40, 0, -55, -16, + -33, -41, -87, -38, -71, -48, -89, -7, -81, -22, -59, + 5, -41, -28, -47, -20, -14, -23, -20, -10, -31, -22, + -25, -11, -55, -34, -32, -33, -44, -53, 1, -30, -56, + -58, -29, -61, -47, -27, -44, -54, -52, -32, -37, -63, + -16, 18, 1, -3, 18, 72, 0, 21, 9, 42, -23, + -7, -40, -42}, + /* IRC_Composite_C_R0195_T165_P000.wav */ + {-31, 31, -31, 31, -30, 30, -30, 29, -28, 27, -25, 23, + -20, 16, -12, 5, 5, -18, 40, -78, 187, -74, -49, 55, + -110, 112, -199, 218, -425, 530, 199, 1304, 62, 2352, 168, 2823, + -5315, 837, 4953, 2252, 4624, 4623, 6361, 3213, -1540, 938, 5698, -601, + -1840, -1321, 415, -514, -55, -892, 3, -75, 186, -753, 471, -260, + 192, -752, 21, -345, -220, -374, -258, -349, -282, -436, -427, -398, + -253, -570, -291, -449, -37, -524, -88, -564, -148, -493, -85, -435, + -73, -317, -105, -359, -181, -335, -176, -321, -195, -365, -206, -391, + -180, -383, -187, -437, -200, -456, -199, -434, -221, -404, -185, -335, + -239, -304, -221, -288, -247, -317, -222, -256, -192, -283, -227, -290, + -193, -268, -185, -298, -185, -261, -149, -305, -194, -283, -162, -272, + -207, -271, -209, -214, -185, -206, -201, -195, -156, -200, -178, -267, + -179, -273, -180, -271, -149, -245, -175, -193, -143, -182, -204, -213, + -175, -213, -191, -252, -204, -249, -193, -210, -188, -202, -194, -140, + -186, -146, -188, -134, -188, -186, -187, -199, -210, -191, -202, -211, + -250, -183, -225, -176, -254, -154, -188, -96, -150, -85, -102, -69, + -80, -63, -71, -81, -74, -55, -81, -81, -69, -57, -79, -51, + -37, -34, -57, -26, -18, 6, -12, 5, -5, 22, -15, 19, + -13, 6, -49, 9, -38, -32, -76, -25, -65, -43, -75, -1, + -82, -14, -96, -21, -95, -25, -47, -4, -67, -76, -41, -67, + -39, -53, -5, -25, -37, 30, 14, 61, -21, 72, -39, 30, + -59, 25, -48, -21}, + /* IRC_Composite_C_R0195_T180_P000.wav */ + {9, -9, 9, -10, 10, -11, 11, -12, 13, -13, 14, -15, + 16, -17, 18, -19, 20, -21, 22, -24, 25, -26, 25, -17, + 104, -114, 196, -98, 54, -126, 232, -41, 337, 459, 418, 1258, + 2024, 1306, -3081, 2067, -322, 1935, 2599, 4829, 7530, 3384, -1993, 2649, + 3619, -256, -653, -863, -84, -381, -1124, -459, 826, -56, -554, -375, + 247, 362, 403, -767, -54, -307, 253, -528, -72, -499, -251, -538, + -224, -365, -429, -387, -270, -98, -240, -189, -323, -221, -114, -200, + -172, -265, -34, -233, -42, -325, -228, -392, -171, -360, -298, -341, + -265, -231, -220, -299, -332, -289, -282, -307, -286, -295, -293, -265, + -194, -288, -280, -341, -219, -319, -258, -367, -240, -308, -190, -294, + -215, -308, -212, -271, -223, -283, -231, -230, -192, -212, -231, -187, + -195, -184, -197, -212, -220, -239, -198, -224, -201, -254, -209, -229, + -205, -256, -239, -265, -231, -229, -194, -202, -206, -179, -140, -169, + -205, -189, -208, -194, -220, -219, -227, -219, -190, -210, -179, -230, + -143, -194, -136, -216, -131, -181, -147, -210, -176, -226, -209, -222, + -193, -214, -206, -190, -190, -205, -213, -200, -179, -148, -109, -85, + -53, -73, -40, -100, -69, -150, -91, -144, -106, -152, -99, -85, + -54, -38, -16, 3, 2, 19, 74, 35, 28, 11, 42, 13, + 8, -18, -32, -36, -29, -19, -47, -86, -72, -66, -42, -83, + -71, -83, -83, -72, -54, -20, -67, -68, -58, -74, -64, -79, + -16, -60, -46, -42, -7, 11, 22, 53, 51, 32, -13, 33, + 28, 3, -22, -46}, + /* IRC_Composite_C_R0195_T195_P000.wav */ + {12, -12, 12, -12, 12, -12, 12, -12, 12, -12, 12, -12, + 12, -12, 12, -12, 11, -11, 10, -9, 8, -6, 4, -1, + -5, 16, -58, -48, -46, 77, 37, -65, -49, -7, -27, 197, + 332, 255, 848, 1561, 1608, -2167, 729, -55, 1458, 1975, 2964, 6210, + 4289, 99, 1163, 3083, 300, -110, -1053, 97, -72, -273, -635, 580, + 371, -146, -5, 150, 225, 128, 22, 74, -75, 62, -60, -62, + -162, -184, -359, -146, -99, 33, -264, -53, -91, -68, -97, -68, + -107, -168, -116, -102, -205, -156, -272, -180, -287, -225, -284, -228, + -198, -203, -177, -240, -212, -239, -265, -245, -245, -239, -269, -232, + -261, -248, -259, -262, -292, -271, -282, -251, -272, -265, -298, -265, + -285, -267, -290, -263, -263, -239, -279, -219, -264, -196, -232, -191, + -206, -170, -181, -179, -220, -200, -244, -184, -248, -231, -259, -234, + -254, -249, -268, -279, -250, -255, -219, -236, -226, -171, -197, -163, + -222, -176, -205, -187, -189, -200, -188, -195, -184, -172, -221, -174, + -245, -154, -259, -156, -238, -169, -237, -202, -213, -230, -237, -242, + -223, -236, -210, -202, -201, -217, -219, -206, -204, -158, -180, -128, + -112, -64, -38, -58, -71, -95, -94, -116, -117, -160, -118, -117, + -105, -102, -88, -47, -31, -21, -9, 0, 24, -11, 6, -19, + 6, 1, -16, -22, -27, -22, -10, -73, -58, -79, -51, -76, + -65, -82, -99, -73, -70, -36, -82, -58, -110, -76, -105, -83, + -46, -122, -54, -86, 17, -33, 15, 14, -1, 0, 13, 44, + 23, -6, 2, -42}, + /* IRC_Composite_C_R0195_T210_P000.wav */ + {6, -6, 6, -6, 7, -7, 7, -7, 7, -7, 7, -7, + 8, -8, 8, -8, 8, -9, 9, -9, 10, -10, 10, -11, + 12, -12, 13, -13, -3, -140, -62, 16, 74, -75, -81, -9, + -154, 52, 75, 214, 374, 933, 1063, -50, -424, 1215, -28, 199, + 1671, 2854, 5292, 2238, 1086, 1897, 2061, -301, -116, -17, 89, 220, + -368, -4, 403, 517, 47, 242, 331, 363, 284, 327, 289, 188, + 152, 291, 134, 170, 136, 286, 56, 186, 15, 53, -103, 44, + -142, -179, -241, -86, -171, -94, -229, -133, -165, -58, -222, -146, + -225, -113, -173, -105, -202, -191, -235, -184, -216, -202, -258, -257, + -241, -211, -214, -256, -271, -278, -250, -239, -276, -254, -285, -245, + -277, -265, -277, -285, -295, -284, -275, -264, -248, -242, -249, -215, + -225, -190, -221, -208, -231, -195, -210, -191, -211, -203, -222, -199, + -222, -228, -234, -247, -238, -233, -235, -232, -214, -216, -211, -201, + -215, -188, -250, -191, -224, -183, -218, -210, -195, -212, -196, -217, + -202, -221, -224, -225, -263, -219, -268, -220, -277, -225, -254, -223, + -243, -232, -228, -229, -221, -217, -194, -205, -169, -170, -122, -124, + -95, -89, -83, -76, -91, -74, -103, -96, -96, -80, -91, -101, + -107, -123, -107, -115, -88, -113, -70, -93, -25, -70, -18, -56, + -11, -38, -15, -23, -20, -23, -29, -31, -55, -70, -58, -57, + -52, -93, -74, -95, -46, -80, -63, -97, -83, -108, -85, -90, + -88, -102, -101, -88, -79, -54, -29, -30, -33, -9, -5, -3, + 24, -9, 7, -23}, + /* IRC_Composite_C_R0195_T225_P000.wav */ + {-1, 1, -1, 1, -1, 1, -1, 1, -1, 1, -1, 1, + -1, 1, -1, 1, -1, 1, -1, 1, -2, 2, -2, 2, + -3, 4, -4, 6, -9, 17, -16, -98, -60, -77, 48, 3, + -81, -121, -43, -55, 1, 19, 93, 305, 537, 631, 131, -260, + 545, 567, 27, 1207, 2128, 2827, 2558, 1188, 1325, 1915, 744, 102, + 304, 255, 211, 393, 327, 58, 643, 377, 590, 456, 611, 727, + 425, 704, 395, 620, 277, 569, 148, 252, 218, 128, 12, -134, + -32, -106, -54, -94, -7, 3, -19, -31, -59, -78, -49, -114, + -79, -187, -72, -177, -103, -203, -112, -200, -151, -256, -183, -276, + -182, -258, -181, -256, -202, -239, -241, -240, -281, -205, -284, -252, + -257, -288, -259, -319, -259, -309, -239, -302, -232, -305, -231, -282, + -235, -251, -227, -244, -183, -227, -177, -216, -169, -213, -156, -216, + -165, -230, -183, -214, -199, -211, -207, -215, -203, -236, -236, -213, + -269, -197, -284, -219, -271, -227, -256, -210, -245, -209, -247, -203, + -231, -195, -235, -194, -248, -194, -245, -212, -237, -203, -241, -198, + -226, -200, -216, -221, -205, -209, -210, -188, -216, -146, -202, -108, + -186, -110, -157, -108, -129, -121, -125, -129, -118, -125, -104, -120, + -104, -95, -101, -78, -84, -57, -71, -28, -65, -16, -83, -25, + -68, -41, -63, -42, -59, -29, -46, -33, -54, -48, -58, -50, + -85, -80, -93, -89, -117, -98, -103, -82, -104, -94, -118, -105, + -87, -86, -85, -96, -100, -64, -69, -48, -63, -65, -45, -15, + -39, -27, -55, -31}, + /* IRC_Composite_C_R0195_T240_P000.wav */ + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 1, -1, + 1, -1, 1, -1, 2, -2, 2, -3, 5, -9, -97, -133, + -196, -147, -157, -47, -93, 37, -29, 1, 0, 59, -116, -55, + 150, 274, 295, 91, 361, 1004, 1112, 1011, 1324, 1700, 2156, 1559, + 331, 574, 1032, 523, 614, 629, 882, 990, 1087, 1066, 943, 977, + 621, 691, 470, 536, 382, 281, 280, 209, 295, 117, 203, 196, + 393, 300, 250, 185, 141, 127, -15, -42, -136, -73, -208, -111, + -261, -80, -157, -42, -115, -38, -121, -105, -155, -167, -199, -209, + -235, -206, -227, -259, -253, -267, -251, -281, -214, -260, -213, -271, + -234, -259, -192, -277, -204, -294, -201, -341, -226, -327, -238, -309, + -206, -263, -189, -240, -185, -208, -186, -211, -192, -209, -158, -223, + -155, -231, -165, -251, -159, -233, -168, -259, -189, -266, -206, -255, + -200, -268, -211, -290, -218, -286, -233, -302, -227, -283, -216, -289, + -207, -262, -193, -248, -196, -236, -182, -219, -173, -221, -200, -230, + -207, -207, -205, -216, -209, -217, -217, -212, -207, -184, -207, -178, + -189, -131, -147, -115, -129, -101, -99, -95, -93, -92, -108, -91, + -105, -77, -114, -73, -104, -68, -90, -52, -74, -36, -70, -50, + -80, -45, -81, -54, -78, -26, -78, -29, -94, -58, -114, -94, + -144, -115, -134, -101, -99, -82, -78, -81, -65, -58, -71, -110, + -125, -130, -133, -109, -102, -88, -107, -78, -90, -39, -58, -31, + -64, -17, -39, -13}, + /* IRC_Composite_C_R0195_T255_P000.wav */ + {2, -2, 2, -2, 2, -2, 2, -2, 2, -2, 2, -2, + 2, -2, 2, -2, 2, -2, 2, -2, 2, -3, 3, -3, + 3, -3, 3, -3, 3, -3, 4, -4, 4, -5, 5, -6, + 9, -19, -71, -126, -111, -131, -223, -213, -179, -73, -136, -101, + -19, 30, 101, 69, 89, 95, 323, 406, 515, 721, 768, 1140, + 1389, 925, 919, 1504, 1350, 1027, 1508, 1800, 2083, 1870, 1354, 950, + 539, 490, 494, 392, 378, 293, 344, 461, 473, 522, 375, 344, + 271, 237, 162, 56, 42, 11, 83, -5, 55, -30, 81, -3, + 71, -18, -50, 0, -151, -66, -240, -143, -251, -198, -256, -250, + -192, -239, -205, -271, -195, -240, -195, -267, -238, -257, -227, -242, + -241, -251, -226, -256, -185, -246, -212, -252, -224, -227, -235, -216, + -249, -192, -222, -170, -209, -167, -231, -184, -255, -188, -250, -181, + -283, -185, -279, -146, -264, -164, -273, -161, -261, -181, -298, -216, + -272, -226, -251, -181, -242, -222, -248, -216, -260, -249, -302, -271, + -289, -255, -276, -256, -246, -225, -218, -198, -202, -222, -210, -223, + -219, -210, -230, -209, -218, -149, -175, -141, -182, -152, -139, -146, + -160, -169, -152, -140, -120, -129, -130, -102, -120, -59, -92, -67, + -121, -84, -93, -62, -69, -69, -85, -101, -60, -94, -80, -123, + -92, -126, -73, -83, -80, -90, -79, -71, -42, -42, -54, -59, + -77, -88, -83, -86, -100, -126, -130, -119, -130, -124, -145, -127, + -148, -111, -124, -92, -85, -75, -85, -70, -68, -66, -66, -59, + -78, -67, -59, -65}, + /* IRC_Composite_C_R0195_T270_P000.wav */ + {-1, 1, -1, 1, -1, 1, -1, 1, -1, 1, -1, 1, + -1, 1, -1, 1, -1, 1, -1, 1, -1, 1, -1, 1, + -1, 1, -1, 1, -1, 1, -1, 2, -2, 2, -2, 3, + -4, 9, -91, -159, -163, -111, -130, -163, -90, -102, -89, -163, + -184, -188, -171, -83, 30, 36, 169, 298, 65, 214, 405, 559, + 898, 896, 1271, 2043, 2157, 2404, 2912, 2261, 1344, 1103, 1268, 1220, + 756, 484, 263, 283, 379, 505, 398, 413, 455, 527, 338, 482, + 334, 406, 255, 187, 211, 119, 189, 4, 182, -16, 145, -56, + 26, -74, -83, -100, -148, -133, -197, -139, -191, -198, -217, -194, + -192, -215, -224, -197, -182, -180, -242, -199, -243, -153, -261, -180, + -264, -151, -250, -164, -239, -176, -235, -178, -231, -191, -258, -227, + -259, -218, -280, -188, -268, -159, -259, -154, -260, -137, -244, -185, + -245, -228, -251, -260, -250, -247, -239, -233, -209, -205, -224, -262, + -274, -268, -248, -262, -263, -277, -234, -255, -222, -233, -226, -262, + -221, -257, -211, -267, -218, -251, -186, -221, -179, -224, -195, -213, + -161, -226, -193, -212, -178, -219, -191, -204, -183, -190, -175, -182, + -165, -164, -140, -131, -97, -107, -80, -102, -45, -85, -60, -81, + -66, -74, -59, -82, -82, -91, -88, -102, -100, -120, -85, -122, + -72, -129, -98, -137, -55, -101, -62, -69, -41, -72, -56, -83, + -69, -90, -53, -98, -81, -105, -80, -107, -83, -112, -102, -115, + -97, -125, -111, -136, -123, -145, -118, -131, -95, -114, -62, -70, + -59, -73, -48, -57}, + /* IRC_Composite_C_R0195_T285_P000.wav */ + {-1, 1, -1, 1, -1, 1, -1, 1, -1, 1, -1, 1, + -1, 1, -1, 1, -1, 1, -1, 2, -2, 2, -2, 2, + -2, 2, -2, 2, -2, 2, -2, 2, -3, 3, -4, 6, + -9, 19, -100, -184, -163, -233, -161, -194, -211, -190, -252, -368, + -121, 58, 37, -12, 35, 111, -96, -223, -19, 464, 559, 1425, + 2382, 1927, 1709, 2204, 1586, 984, 1062, 1267, 1442, 1113, 1078, 1182, + 1130, 1148, 848, 575, 534, 584, 541, 405, 456, 300, 346, 294, + 405, 279, 183, 240, 258, 176, 160, 100, -27, 100, -24, 93, + -139, 0, -270, 60, -268, 20, -249, -123, -250, -178, -127, -185, + -84, -288, -109, -237, -116, -161, -179, -137, -193, -134, -189, -164, + -211, -174, -211, -244, -187, -259, -227, -263, -160, -220, -206, -234, + -213, -214, -205, -225, -212, -266, -182, -254, -229, -238, -166, -272, + -223, -282, -263, -305, -287, -272, -261, -288, -239, -247, -227, -247, + -224, -236, -226, -259, -230, -256, -220, -217, -226, -224, -232, -214, + -225, -205, -261, -251, -252, -219, -235, -230, -230, -236, -236, -209, + -203, -226, -221, -222, -180, -160, -190, -161, -159, -154, -156, -97, + -111, -129, -127, -107, -66, -95, -69, -92, -64, -55, -64, -43, + -78, -65, -103, -71, -87, -110, -101, -114, -93, -117, -90, -112, + -88, -109, -72, -82, -69, -68, -63, -65, -90, -69, -67, -77, + -62, -56, -47, -91, -59, -73, -34, -101, -70, -91, -61, -105, + -91, -128, -154, -149, -128, -150, -140, -142, -93, -111, -108, -78, + -62, -77, -59, -62}, + /* IRC_Composite_C_R0195_T300_P000.wav */ + {4, -4, 4, -4, 4, -4, 4, -4, 4, -5, 5, -5, + 5, -5, 5, -5, 5, -6, 6, -6, 6, -6, 6, -7, + 7, -7, 8, -8, 8, -9, 10, -11, 13, -16, 24, -75, + -192, -243, -206, -121, -200, -413, -335, -275, -203, -85, -38, 193, + -95, -382, 283, -596, -531, 1085, 630, 1024, 2620, 2194, 1518, 1499, + 1392, 607, 112, 846, 1164, 950, 675, 1005, 1305, 1174, 1521, 1142, + 1208, 859, 1074, 771, 735, 552, 498, 679, 274, 454, 160, 457, + 127, 176, 172, 280, 101, 64, 67, -27, 98, 11, 3, -150, + -68, -72, -105, -163, -160, -139, -135, -141, -133, -136, -55, -184, + -109, -200, -81, -104, -78, -213, -147, -100, -188, -136, -214, -162, + -281, -139, -265, -190, -228, -184, -320, -138, -205, -130, -238, -158, + -209, -100, -297, -175, -328, -233, -318, -199, -385, -256, -355, -254, + -336, -269, -310, -262, -286, -237, -272, -206, -279, -217, -262, -212, + -280, -260, -296, -269, -321, -249, -293, -297, -294, -263, -252, -259, + -248, -231, -189, -223, -204, -205, -214, -202, -172, -186, -170, -216, + -162, -160, -149, -167, -137, -119, -139, -138, -127, -91, -90, -111, + -123, -105, -94, -88, -92, -101, -104, -94, -87, -104, -82, -106, + -68, -106, -75, -113, -53, -76, -65, -86, -59, -75, -81, -73, + -84, -59, -75, -52, -59, -73, -55, -69, -45, -87, -49, -62, + -47, -76, -53, -57, -57, -74, -83, -72, -109, -94, -135, -114, + -127, -112, -128, -125, -121, -104, -114, -94, -97, -102, -114, -98, + -73, -61, -46, -72}, + /* IRC_Composite_C_R0195_T315_P000.wav */ + {-3, 3, -3, 3, -3, 3, -3, 3, -3, 3, -3, 3, + -3, 3, -3, 3, -3, 3, -4, 4, -4, 4, -4, 4, + -5, 5, -5, 6, -7, 8, -10, 14, -29, -234, -230, -266, + -204, -305, -247, -524, -359, -150, 55, 142, 71, 572, -570, -1004, + 861, -803, -374, 3189, 1120, 2120, 3144, 2428, 636, 1026, 1269, 59, + -187, 571, 1369, 181, 332, 532, 1375, 1240, 1554, 1033, 1045, 866, + 1172, 1026, 1022, 606, 720, 597, 671, 349, 376, 201, 259, 47, + 222, 146, 93, -124, 20, -35, -174, -23, 6, -91, -108, -18, + 6, 38, -2, -118, 67, -77, -79, -121, -96, -206, -171, -74, + -193, -141, -167, -54, -168, -65, -180, -137, -281, -209, -164, -172, + -178, -158, -127, -188, -174, -11, -152, -142, -222, -182, -348, -203, + -338, -287, -380, -316, -332, -319, -347, -297, -281, -364, -362, -314, + -304, -376, -347, -321, -343, -330, -367, -276, -306, -278, -290, -268, + -295, -267, -229, -264, -248, -290, -244, -263, -201, -223, -197, -193, + -189, -154, -159, -137, -178, -126, -217, -115, -163, -129, -166, -130, + -150, -147, -107, -147, -129, -143, -117, -141, -126, -115, -135, -99, + -146, -90, -112, -81, -119, -52, -80, -79, -84, -83, -64, -59, + -53, -89, -91, -74, -101, -29, -121, -49, -139, -64, -94, -23, + -57, -36, -29, -38, -50, -50, -47, -30, -53, -56, -81, -58, + -97, -62, -69, -56, -82, -74, -103, -94, -111, -122, -150, -117, + -148, -135, -117, -104, -87, -95, -77, -107, -55, -89, -56, -82, + -65, -70, -36, -13}, + /* IRC_Composite_C_R0195_T330_P000.wav */ + {-15, 15, -15, 15, -15, 15, -15, 16, -16, 16, -16, 16, + -16, 16, -16, 16, -16, 16, -16, 16, -16, 15, -15, 14, + -13, 11, -8, 5, 0, -7, 12, -293, -526, -71, -505, -401, + -1003, -83, -138, 277, -121, 450, 638, -529, -2538, 1974, -1021, -928, + 6018, 1862, 2117, 4170, 3177, -540, 80, 773, 465, -382, 760, 707, + 287, -96, 895, 1260, 1490, 749, 709, 1138, 1758, 1405, 855, 490, + 661, 833, 888, 382, 366, 310, 491, 203, 204, 155, 246, 122, + -32, -122, -37, 50, 94, -114, -242, -173, 94, -97, -233, -239, + 44, 3, 91, -64, -78, -19, 148, 9, 2, -196, 62, -119, + -154, -277, 128, -102, -103, -182, -189, -136, -49, 51, -260, -285, + -17, -46, -344, -260, -264, -310, -481, -356, -385, -299, -442, -338, + -388, -433, -413, -352, -281, -444, -379, -419, -361, -361, -316, -320, + -407, -371, -354, -283, -325, -263, -281, -264, -300, -242, -200, -218, + -231, -208, -283, -185, -218, -137, -254, -173, -210, -124, -178, -189, + -155, -197, -150, -148, -83, -183, -134, -157, -104, -154, -134, -133, + -118, -172, -161, -120, -119, -156, -145, -96, -143, -172, -117, -71, + -107, -120, -113, -53, -111, -85, -65, -51, -97, -63, -15, -77, + -94, -67, -18, -45, -126, -60, -101, -21, -115, 3, -114, -45, + -90, -5, -69, -55, -89, -42, -99, -38, -100, -24, -95, -21, + -116, -37, -113, -13, -85, -64, -143, -57, -122, -61, -126, -85, + -123, -78, -94, -71, -113, -55, -72, -37, -93, -41, -42, -10, + -34, -37, -13, -34}, + /* IRC_Composite_C_R0195_T345_P000.wav */ + {-1, 1, -1, 1, -1, 1, -1, 1, -1, 1, -1, + 1, -1, 1, -1, 1, -1, 1, -1, 1, -1, 1, + -1, 1, -2, 2, -3, -196, -727, -30, -506, -136, -1125, + -612, -692, -135, 283, 619, 802, -51, -2221, 1398, -964, -5308, + 8059, 961, 2013, 8066, 4451, -858, 1438, 1808, -955, -2183, 475, + 1387, 742, 458, 140, 184, 1030, 1768, 1316, 965, 1001, 1155, + 1147, 1222, 1301, 1124, 461, 289, 297, 592, 383, 273, -220, + -259, -53, 323, 264, -32, -200, 42, 191, 421, 77, -195, + -257, 18, -154, -38, -140, -135, -290, -47, 107, 49, -29, + -98, -94, -50, 49, 264, 36, 153, -148, 86, -24, 287, + -156, 101, -86, 125, -186, -182, -278, -236, -501, -636, -484, + -401, -330, -428, -463, -554, -283, -366, -318, -494, -441, -375, + -318, -358, -465, -416, -438, -337, -400, -344, -393, -247, -308, + -313, -356, -181, -229, -204, -374, -214, -320, -200, -307, -179, + -339, -189, -252, -152, -246, -143, -210, -166, -196, -69, -139, + -172, -216, -103, -73, -127, -214, -116, -118, -96, -247, -135, + -187, -78, -264, -166, -235, -95, -173, -142, -167, -169, -129, + -117, -114, -125, -125, -103, -137, -47, -110, -26, -121, -21, + -72, -15, -71, -68, -34, -53, -38, -72, -96, -96, -90, + -11, -126, -33, -166, -22, -92, 18, -75, -35, -69, -60, + -67, -82, -72, -61, -96, -68, -103, -26, -100, -19, -108, + 0, -110, -34, -106, -39, -54, -75, -76, -83, -35, -42, + -69, -51, -59, -38, -55, 19, -49, -3, -43, 27, -14, + -5, -46, -24}}; + +const int16_t irc_composite_c_r0195_p015[][256] = + {/* IRC_Composite_C_R0195_T000_P015.wav */ + {4, -4, 4, -4, 4, -4, 4, -4, 4, -4, 4, + -5, 5, -5, 4, -4, 4, -4, 3, -2, 1, 2, + -5, -101, -665, -424, -897, -170, -507, -460, -1001, -828, -356, + 47, 474, 980, 404, 2103, -3700, -5968, 7765, -9133, 9741, 10867, + 71, 6925, 3456, -1455, -2458, 2718, -170, -1201, -1751, 1809, 507, + 632, -1165, 1945, 1684, 1705, -450, 1152, 360, 1527, 269, 952, + 644, 931, 727, 1576, 725, 835, 741, 915, 186, 162, 327, + 384, 20, 24, -311, 60, 225, 484, -123, -28, -210, 118, + 15, 76, -335, -153, -249, 49, -120, 24, -200, -169, -213, + -175, -235, -158, -280, -399, -410, -304, -104, -355, -319, -311, + -79, -366, -75, -79, -120, -487, 147, -42, -198, -175, 135, + -242, 55, 233, 123, -314, -58, -179, -96, -352, -26, 7, + 11, -105, -129, 125, -110, -293, -276, -461, -507, -406, -422, + -497, -481, -446, -459, -325, -336, -443, -420, -320, -391, -360, + -309, -407, -375, -288, -289, -385, -315, -272, -292, -271, -256, + -318, -173, -220, -185, -329, -142, -239, -76, -219, -117, -277, + -107, -204, -117, -233, -199, -220, -177, -238, -171, -170, -187, + -285, -154, -168, -145, -192, -131, -239, -194, -170, -109, -205, + -155, -164, -163, -187, -126, -85, -138, -195, -140, -189, -84, + -134, -22, -177, -51, -128, -26, -105, 26, -144, -65, -123, + -28, -136, 19, -69, -43, -112, 40, -43, 59, -67, 32, + -35, 97, -21, 99, -27, 90, 23, 141, 21, 38, 39, + 61, 12, 10, -1, 37, -3, 7, 2, 41, -19, -12, + -12, 16, -53}, + /* IRC_Composite_C_R0195_T015_P015.wav */ + {100, -101, 102, -102, 102, -102, 102, -102, 101, -99, 96, + -93, 88, -81, 71, -58, 37, -6, -46, 154, -587, -280, + -52, -712, -530, -842, -228, -637, -217, -1535, -463, 274, 2688, + 1298, -390, -2760, 4349, -16532, 13572, -2117, 2580, 22789, -850, 559, + 1346, 2618, -4202, 397, 316, 686, -1654, 1356, -848, 1484, 164, + 1873, 376, 1720, 39, 1069, 309, 1623, 748, 807, 488, 1192, + 547, 1364, 679, 594, 48, 334, -179, 362, 98, -149, -587, + -275, -81, 371, -430, -372, -369, 412, -126, -39, -294, 169, + -117, 61, -209, -100, -271, -40, -262, -244, -406, 12, -243, + -253, -484, 54, -243, -283, -575, -138, -358, -262, -307, -61, + -567, -206, 25, 51, -696, -10, -132, -245, -388, 187, -132, + -194, -316, -92, -71, -291, -374, 27, -295, -84, -4, 145, + -576, 122, -217, -153, -724, 53, -337, -126, -502, -305, -401, + -138, -372, -296, -502, -318, -375, -140, -645, -283, -375, -217, + -529, -172, -316, -324, -287, -239, -309, -305, -230, -228, -310, + -332, -289, -168, -302, -286, -241, -197, -266, -181, -156, -260, + -272, -185, -148, -225, -221, -239, -178, -223, -142, -203, -226, + -260, -124, -148, -200, -259, -124, -203, -182, -207, -78, -186, + -163, -136, -74, -179, -123, -45, -87, -180, -103, -28, -117, + -135, -64, -29, -148, -90, -13, -28, -111, -40, 7, -42, + -98, -26, -4, -57, -22, 22, -38, -72, 76, 67, 1, + -21, 80, 49, 39, 63, 66, 26, 23, 62, 97, 29, + 39, 49, 28, -1, 52, 74, -6, -4, 19, 15, -15, + -12, 17, -50}, + /* IRC_Composite_C_R0195_T030_P015.wav */ + {-101, 102, -103, 105, -105, 106, -106, 106, -104, 102, -97, + 90, -79, 62, -35, -12, 109, -431, -649, 311, -951, 117, + -1193, 10, -1439, 943, -1996, 57, -1866, 3892, -160, 3817, -4940, + 7695, -14389, 320, 6811, -6312, 29122, 1548, 2820, 2754, 2735, -5703, + -20, 838, 313, -2387, 2394, -1371, 1731, 262, 1937, 34, 1456, + -294, 1713, 221, 997, 660, 1535, 549, 1179, 928, 928, 293, + 321, -76, 273, -164, -356, -622, 24, -278, -26, -520, -116, + -436, -61, -412, -21, -436, -263, -534, -25, -24, 127, -328, + -77, -189, 107, -200, -313, -487, -278, -245, -413, -269, -216, + -100, -434, -149, -238, -270, -527, -94, -144, -244, -529, -94, + -120, -125, -544, -205, -477, -214, -375, -52, -404, -184, -361, + -14, -189, -53, -346, -89, -276, -127, -233, -126, -417, -169, + -225, -263, -470, -227, -238, -276, -329, -339, -304, -226, -209, + -212, -255, -261, -275, -96, -168, -289, -298, -134, -296, -334, + -291, -248, -272, -268, -362, -278, -286, -108, -360, -215, -330, + -76, -394, -179, -365, -104, -449, -206, -420, -128, -409, -167, + -434, -175, -416, -105, -318, -125, -408, -157, -270, -45, -277, + -160, -326, -56, -220, -114, -280, -69, -199, -100, -242, -24, + -143, -39, -214, 3, -104, 20, -184, 20, -140, -7, -183, + 31, -135, -21, -164, 13, -128, 42, -188, 49, -113, 71, + -189, 90, -97, 102, -87, 84, -61, 147, -63, 149, -94, + 170, -108, 183, -108, 208, -150, 179, -163, 250, -98, 215, + -137, 235, -107, 214, -135, 246, -126, 201, -171, 186, -149, + 135, -194, 121}, + /* IRC_Composite_C_R0195_T045_P015.wav */ + {-146, 153, -161, 170, -179, 189, -200, 212, -224, 238, -252, + 266, -278, 280, -241, -262, -1208, 369, -877, 618, -1865, 344, + -1624, 1098, -2418, 1166, -2204, 3338, -287, 6351, -5670, 4490, -9471, + -5595, 10415, -3658, 26422, 6163, 571, 640, 3020, -3458, 345, 262, + -17, -2379, 1504, -915, 2155, 96, 1778, -36, 1408, 800, 1533, + 117, 618, 355, 1940, 474, 1443, 116, 436, -13, 320, -248, + -589, -590, -422, -164, -365, -343, -251, -474, 20, -343, -59, + -619, -421, -460, -72, -443, -399, -435, -238, -311, -325, -256, + -210, -285, -282, -222, -283, -212, -374, -178, -386, -213, -343, + -35, -317, -276, -390, -79, -244, -300, -445, -222, -296, -233, + -458, -263, -403, -259, -462, -188, -423, -283, -371, -78, -225, + -208, -380, -203, -156, -140, -307, -202, -208, -86, -293, -179, + -233, -185, -390, -305, -381, -170, -416, -344, -426, -229, -331, + -201, -370, -183, -273, -88, -257, -116, -206, -60, -298, -72, + -233, -115, -304, -131, -336, -187, -271, -44, -375, -190, -355, + -35, -321, -189, -400, -136, -365, -198, -383, -200, -395, -244, + -401, -188, -398, -184, -393, -179, -365, -150, -314, -118, -327, + -118, -291, -73, -272, -87, -247, -52, -258, -27, -226, 25, + -210, 8, -202, 51, -136, 62, -162, 35, -135, 61, -156, + -12, -167, 55, -165, 15, -174, 47, -131, 28, -125, 76, + -91, 69, -71, 126, -60, 104, -40, 141, -51, 117, -33, + 141, -58, 103, -38, 158, -48, 95, -52, 149, -45, 86, + -47, 120, -68, 88, -39, 116, -45, 74, -35, 89, -68, + 62, -90, 53}, + /* IRC_Composite_C_R0195_T060_P015.wav */ + {29, -34, 39, -45, 52, -59, 69, -80, 93, -110, 131, + -160, 205, -308, -459, -920, -171, 388, -674, -719, -1064, 350, + -1149, 552, -1891, 2109, -28, 5773, -3032, 5413, -11840, 194, -1388, + 1646, 23561, 8535, 2547, 1107, -66, 257, 913, 517, -131, -1162, + -856, 344, 2136, -879, 1332, -592, 2048, 385, 1977, 174, 2217, + -131, 2091, -69, 1390, -1343, 963, -905, 515, -1176, -81, -1557, + 250, -301, -119, -467, -479, -337, 138, -523, -373, -525, -22, + -558, -74, -857, -193, -765, -208, -1034, -15, -947, -96, -803, + 45, -665, 15, -523, -1, -312, 49, -446, -141, -311, -126, + -246, -344, -467, -27, -388, -328, -427, -198, -495, -118, -356, + -277, -493, -134, -525, -71, -411, -262, -445, -175, -367, -96, + -400, -267, -383, -49, -313, -260, -205, -179, -166, -56, -272, + -194, -146, -166, -310, -204, -405, -207, -321, -211, -468, -223, + -383, -150, -326, -281, -312, -146, -220, -208, -241, -286, -189, + -157, -134, -243, -213, -136, -145, -92, -188, -205, -244, -73, + -253, -131, -321, -224, -277, -194, -364, -238, -342, -254, -284, + -301, -272, -246, -239, -245, -226, -236, -207, -220, -191, -251, + -189, -226, -188, -154, -195, -171, -163, -122, -121, -97, -111, + -91, -108, -20, -120, -59, -33, -136, -42, -55, -62, -49, + -97, -100, 16, -97, -13, -104, 7, -74, -20, -54, 27, + -6, 45, -12, 56, 35, 24, 86, 33, 48, 24, 68, + 72, -3, 50, 22, 52, 12, 43, -32, 109, -68, 106, + -66, 49, -61, 102, -74, 44, -72, 28, -60, 21, -71, + -4, -76, 16}, + /* IRC_Composite_C_R0195_T075_P015.wav */ + {19, -16, 12, -7, 2, 4, -11, 18, -27, 36, -47, + 59, -68, 37, -553, 36, -916, -89, -13, -916, -389, -1224, + 572, -284, 1453, 853, 1354, 3254, 1573, -10498, -1005, -5881, 20609, + 10333, 4130, 9878, -3085, -1702, 1001, 5191, 773, -2104, -1269, -41, + 1645, -349, 309, 387, 447, 1304, 620, 1533, 360, 1697, 867, + 1545, -94, 149, -359, 473, -1422, -432, -1225, -246, -886, 186, + -914, -144, -892, 309, -602, 60, -603, -112, -520, -150, -750, + -486, -681, -262, -736, -448, -1017, -103, -734, -305, -903, -61, + -752, 39, -644, -19, -565, 33, -324, -70, -458, -205, -229, + -182, -403, -322, -245, -211, -302, -350, -295, -222, -298, -293, + -385, -263, -357, -235, -379, -252, -470, -139, -429, -84, -457, + -178, -385, -120, -380, -232, -339, -192, -273, -205, -196, -146, + -164, -174, -129, -218, -203, -145, -229, -290, -273, -262, -283, + -222, -302, -215, -314, -150, -291, -151, -311, -154, -281, -177, + -260, -179, -190, -177, -222, -159, -209, -215, -215, -185, -191, + -224, -214, -221, -182, -209, -269, -260, -254, -234, -284, -241, + -293, -201, -269, -218, -264, -188, -203, -199, -218, -207, -118, + -151, -146, -184, -126, -92, -92, -129, -108, -109, -42, -120, + -57, -144, -45, -102, -23, -164, -63, -98, -36, -106, -81, + -3, -74, -55, -79, -49, -60, -59, -60, -31, -36, -35, + 57, -37, 99, -7, 135, -26, 115, 14, 154, -6, 78, + 28, 119, -3, 53, -48, 80, -38, 55, -56, 13, -65, + 74, -45, 19, -79, 50, -39, 24, -95, 40, -41, 15, + -61, 55, -129}, + /* IRC_Composite_C_R0195_T090_P015.wav */ + {106, -108, 111, -113, 116, -119, 121, -123, 125, -125, 121, + -102, -150, -386, -21, -846, -117, 203, -762, -759, -425, -20, + 26, 535, 183, 3678, 866, 180, 877, -12284, 4742, -888, 21444, + 10158, -2546, 4612, -145, -630, 3778, 3819, 1940, -4361, -1372, 2229, + 1116, -1596, -423, 1713, 840, 536, 1040, 2123, 618, 888, -302, + 898, -618, 52, -845, 240, -1279, -435, -1143, 105, -929, -50, + -1077, -180, -541, 98, -930, -161, -671, -158, -656, -338, -774, + -263, -620, -260, -792, -238, -787, -172, -874, -235, -797, -199, + -767, -178, -702, -101, -607, -63, -479, -72, -504, -49, -400, + -118, -387, -113, -404, -73, -442, -100, -429, -15, -496, -79, + -480, -58, -494, -80, -498, -112, -411, -138, -447, -136, -406, + -145, -485, -166, -440, -131, -485, -87, -417, -104, -413, -102, + -426, -31, -350, -62, -299, 27, -272, -36, -302, -22, -320, + -120, -364, -124, -307, -111, -382, -185, -231, -108, -275, -163, + -225, -123, -157, -158, -254, -183, -208, -146, -274, -183, -229, + -148, -301, -148, -275, -212, -388, -177, -304, -193, -358, -180, + -265, -195, -243, -171, -184, -212, -200, -212, -156, -173, -149, + -232, -145, -162, -100, -147, -119, -102, -57, -53, -87, -18, + -36, -11, -87, -46, -46, 17, -89, -82, -57, 6, -100, + -61, -101, -38, -60, -46, -108, -40, -41, 19, -104, -21, + -14, 77, -24, 51, 22, 136, 25, 103, 19, 102, 3, + 92, -42, 26, -51, 25, -38, -15, -65, -12, -35, -8, + 28, 24, -31, -1, 36, 92, 2, 6, 8, 52, -33, + -7, -25, -27}, + /* IRC_Composite_C_R0195_T105_P015.wav */ + {16, -19, 23, -28, 34, -40, 48, -57, 68, -82, 100, + -121, 33, -823, 199, -561, -96, -481, 741, -1752, -67, -480, + 1840, -966, 591, 1601, 5280, -3652, 770, -7901, 214, 10553, 2446, + 18816, 1377, -3359, -1411, 6633, 5595, -115, -232, 1823, -2575, -717, + -242, 2719, -2255, 509, 511, 2747, 225, 1585, 549, 951, -478, + -34, -731, -13, -1246, -512, -881, -341, -1127, 207, -442, -495, + -702, -517, -566, 35, -620, -983, -450, -179, -571, -441, -658, + -297, -211, -340, -730, -514, -334, -479, -562, -662, -463, -524, + -336, -537, -522, -336, -226, -483, -343, -332, -364, -181, -306, + -365, -271, -175, -316, -213, -283, -326, -115, -221, -216, -290, + -146, -290, -98, -334, -301, -279, -138, -349, -278, -318, -250, + -285, -237, -388, -326, -257, -206, -416, -256, -293, -207, -350, + -156, -397, -149, -314, -125, -323, -153, -327, -64, -258, -145, + -241, -10, -249, -51, -197, -108, -234, 33, -286, -138, -267, + -26, -273, -117, -347, -77, -229, -117, -301, -119, -229, -104, + -247, -175, -311, -162, -227, -248, -287, -181, -293, -170, -318, + -206, -285, -159, -334, -167, -232, -161, -235, -76, -246, -154, + -204, -10, -221, -106, -229, 14, -182, -12, -251, 30, -154, + 32, -190, 61, -152, 90, -145, 48, -122, 71, -96, 29, + -121, 6, -57, 66, -80, 40, -49, 59, -112, -8, -129, + 54, -18, 76, -21, 97, 37, 171, 23, 59, -61, 75, + -37, 21, -127, 1, -154, 68, -112, -12, -180, 86, -58, + 91, -120, 128, -28, 129, -100, 167, -17, 106, -84, 94, + -49, 76, -37}, + /* IRC_Composite_C_R0195_T120_P015.wav */ + {39, -39, 39, -39, 39, -38, 38, -37, 35, -32, 27, + -19, 0, -223, 40, -675, 329, -841, 457, -348, -274, -1647, + 1770, 432, -494, 3, 4260, 289, 887, -4454, -3947, 7446, -2378, + 15614, 7773, 95, -2028, 2409, 7171, 2953, 950, 1439, -1677, -1118, + -1770, 1575, 491, 137, -1226, 1181, 2451, 1325, -149, 862, 100, + -196, -613, -302, -1148, -1325, -632, -731, -1011, -688, 174, -67, + -430, -378, -168, -383, -445, -570, -934, -576, -71, -333, -939, + -576, -180, -299, -551, -542, -536, -312, -421, -506, -398, -463, + -360, -423, -342, -463, -390, -485, -299, -416, -291, -519, -227, + -313, -272, -376, -199, -241, -176, -354, -259, -245, -94, -289, + -288, -266, -79, -203, -210, -332, -200, -224, -227, -331, -258, + -315, -219, -293, -232, -358, -231, -306, -201, -298, -234, -413, + -216, -295, -220, -417, -228, -272, -126, -326, -197, -254, -104, + -275, -161, -280, -75, -215, -110, -276, -33, -130, -19, -261, + -8, -171, 30, -242, -55, -229, -65, -254, -74, -218, -134, + -301, -99, -211, -155, -300, -156, -234, -131, -322, -164, -311, + -142, -349, -229, -334, -148, -297, -165, -289, -133, -215, -100, + -254, -101, -185, -51, -234, -38, -186, -6, -168, 0, -111, + -29, -125, 29, -96, -23, -153, 22, -59, -32, -103, 28, + -118, 11, -64, 83, -103, 41, -112, 28, -109, 99, -41, + 39, -86, 157, 43, 121, -15, 157, -11, 121, -30, 90, + -113, 25, -109, 38, -172, -47, -151, 15, -102, 21, -97, + 16, -33, 104, 12, 59, -55, 121, -5, 63, -116, 114, + -30, 115, -85}, + /* IRC_Composite_C_R0195_T135_P015.wav */ + {15, -17, 19, -21, 24, -28, 32, -37, 43, -50, 60, + -73, 90, -115, 157, -297, 361, -673, 287, -116, -598, -544, + 1021, -902, -263, 1113, 1563, -848, 3395, 810, -1816, -1445, -4755, + 10984, 3295, 5362, 6262, 2589, -529, 1444, 7698, 4758, -2233, -2138, + -1622, 980, -226, 100, -517, 871, 952, 682, 983, 1138, -542, + -341, -164, -377, -526, -992, -910, -828, -413, -830, -380, -392, + -124, -316, -225, -315, -25, -678, -691, -468, -412, -545, -395, + -676, -301, -467, -356, -548, -300, -516, -260, -553, -295, -485, + -232, -525, -220, -421, -217, -488, -227, -455, -268, -426, -269, + -474, -232, -395, -199, -378, -219, -372, -176, -401, -202, -354, + -114, -340, -124, -267, -86, -322, -166, -298, -181, -316, -179, + -307, -163, -290, -175, -321, -186, -325, -234, -372, -200, -335, + -202, -352, -199, -268, -180, -319, -236, -296, -200, -331, -209, + -300, -142, -273, -118, -226, -91, -203, -97, -250, -91, -176, + -72, -201, -65, -156, -22, -171, -59, -200, -36, -210, -93, + -201, -48, -197, -73, -185, -73, -195, -95, -266, -166, -294, + -169, -299, -216, -315, -187, -265, -167, -287, -146, -275, -175, + -265, -144, -282, -127, -249, -66, -222, -39, -146, -32, -126, + -20, -76, 5, -92, 18, -56, 21, -91, 13, -119, -23, + -127, 37, -71, 16, -116, 10, -105, 12, -96, 55, -69, + 43, -76, 137, -20, 86, -3, 123, 28, 100, 8, 49, + -58, 60, -31, -10, -117, -15, -44, -19, -89, 2, -51, + -18, -51, 62, -39, 31, -46, 59, -59, 67, -57, 112, + -72, 62, -22}, + /* IRC_Composite_C_R0195_T150_P015.wav */ + {22, -23, 24, -24, 25, -26, 27, -27, 28, -28, 28, + -28, 26, -24, 19, -9, -13, 134, -205, 5, 10, -146, + -122, -75, -78, -152, 345, -100, 1711, 198, 1882, 367, 2730, + -6333, 870, 5933, 2367, 6468, 4276, 4587, 970, 719, 5822, 3692, + -1207, -2971, -1058, 797, 482, -202, -740, 507, 536, 875, 480, + 668, -204, -211, -690, 500, -1107, -874, -841, -223, -910, -122, + -465, -407, -494, -192, -244, -202, -337, -294, -451, -519, -512, + -353, -580, -345, -407, -307, -608, -176, -450, -303, -475, -205, + -453, -277, -342, -237, -284, -212, -314, -272, -380, -283, -368, + -307, -417, -297, -337, -321, -374, -329, -283, -379, -283, -310, + -317, -249, -232, -217, -269, -190, -252, -183, -270, -238, -227, + -187, -286, -191, -260, -230, -245, -233, -289, -230, -246, -218, + -241, -208, -260, -204, -276, -211, -286, -243, -284, -211, -281, + -227, -258, -199, -238, -185, -241, -133, -220, -133, -189, -80, + -190, -65, -146, -88, -167, -80, -129, -110, -166, -124, -142, + -139, -178, -111, -146, -116, -143, -102, -118, -138, -137, -152, + -173, -190, -174, -190, -193, -230, -208, -208, -216, -246, -203, + -231, -209, -216, -167, -210, -170, -158, -128, -148, -142, -127, + -104, -88, -98, -82, -75, -58, -53, -46, -85, -53, -56, + -1, -14, 17, -80, -58, -41, -57, -6, -70, 6, -78, + 37, -14, 58, 10, 69, 35, 20, 45, 42, 27, 3, + 3, -29, -5, -10, 26, -54, -21, -21, 39, -58, -58, + -23, 10, -16, -35, 6, -22, 2, -8, 54, -13, 7, + -2, 51, 6}, + /* IRC_Composite_C_R0195_T165_P015.wav */ + {20, -21, 21, -22, 23, -23, 24, -25, 26, -26, 27, -28, + 29, -29, 30, -30, 29, -28, 22, -5, 163, -209, 156, -276, + 329, -325, 157, -151, 120, 93, 1138, 522, 872, 2060, 255, 503, + -4749, 3937, 3841, 3248, 5654, 4851, 3110, 1024, 1230, 4380, 981, -2433, + -2102, 345, 461, 292, -696, 50, 937, 951, -195, 227, 100, -111, + -682, -242, -295, -695, -542, -371, -167, -471, -237, -441, -313, -257, + -355, -428, -338, -330, -347, -256, -340, -455, -298, -395, -419, -365, + -319, -336, -313, -224, -306, -223, -245, -277, -258, -289, -268, -318, + -289, -331, -299, -363, -319, -390, -363, -384, -327, -342, -339, -372, + -297, -284, -259, -319, -264, -296, -233, -297, -258, -289, -255, -235, + -239, -231, -238, -221, -219, -255, -237, -261, -205, -265, -219, -253, + -180, -250, -196, -250, -200, -248, -207, -278, -194, -276, -219, -261, + -199, -283, -219, -241, -187, -217, -180, -196, -141, -180, -121, -175, + -110, -181, -79, -171, -84, -195, -62, -164, -99, -185, -115, -180, + -164, -155, -140, -152, -147, -164, -113, -154, -117, -178, -132, -201, + -137, -183, -162, -227, -171, -200, -151, -199, -165, -161, -150, -178, + -144, -157, -158, -179, -144, -176, -163, -168, -98, -168, -122, -152, + -52, -148, -86, -125, -9, -56, -37, -107, -79, -44, -26, -30, + -92, -55, -14, 29, 14, 24, 62, 67, 48, 30, 48, 10, + 57, -9, 40, -55, 25, -77, 17, -96, 25, -64, 21, -101, + -1, 4, 58, -21, 25, 24, 70, 3, 30, 9, 36, -16, + 6, -21, -39, -15}, + /* IRC_Composite_C_R0195_T180_P015.wav */ + {-9, 9, -9, 9, -10, 10, -10, 10, -11, 11, -11, 11, + -12, 12, -13, 13, -14, 15, -15, 16, -17, 19, -21, 50, + -66, 25, 121, 28, -120, 155, -168, 335, 416, 671, 222, 2210, + 806, 1236, -2882, 612, 2593, 898, 4860, 6139, 4465, 870, -196, 3886, + 2840, -2197, -1283, -67, 115, -435, 2, 221, 731, -37, -52, 179, + 744, -513, -450, -510, 328, -437, -225, -503, 1, -204, -76, -530, + -331, -379, -166, -435, -279, -284, -151, -266, -225, -384, -293, -305, + -154, -293, -219, -273, -134, -244, -264, -371, -311, -317, -245, -345, + -336, -343, -237, -329, -370, -441, -342, -336, -321, -387, -336, -319, + -260, -281, -304, -299, -268, -249, -331, -318, -306, -253, -254, -267, + -280, -263, -244, -262, -278, -297, -251, -213, -235, -246, -260, -197, + -223, -206, -272, -188, -228, -176, -268, -213, -279, -202, -229, -233, + -258, -216, -166, -186, -223, -208, -202, -136, -198, -161, -225, -147, + -174, -123, -192, -165, -169, -119, -164, -157, -169, -110, -156, -134, + -190, -131, -196, -117, -175, -127, -176, -95, -111, -96, -149, -112, + -153, -144, -212, -161, -218, -171, -217, -170, -191, -155, -154, -114, + -156, -141, -134, -77, -145, -152, -168, -124, -167, -164, -158, -156, + -173, -143, -139, -75, -112, -48, -151, -85, -139, -30, -81, -70, + -106, -105, -40, -41, 29, 15, 57, 54, 103, 128, 65, 65, + 35, 45, -4, -23, -56, -53, -39, -44, -11, -66, 11, -23, + 53, -3, 44, 26, 45, 23, 36, 66, 32, 13, 3, 39, + 11, -14, 8, -12}, + /* IRC_Composite_C_R0195_T195_P015.wav */ + {3, -3, 4, -4, 4, -5, 5, -5, 6, -6, 7, -7, + 8, -9, 9, -10, 11, -13, 14, -16, 19, -22, 27, -33, + 44, -87, 114, -132, 102, -154, 155, -97, -20, -37, -29, 363, + 412, 474, 853, 1336, 1092, -788, -1724, 2098, 1099, 2640, 4844, 4905, + 2895, -39, 1681, 2632, 175, -1400, -248, 325, 22, -206, 196, 419, + 561, 41, 192, 277, 401, -173, 2, -114, 106, -178, -24, -176, + 30, -91, 54, -173, -183, -124, -65, -136, -127, -106, -163, -59, + -115, -115, -267, -191, -291, -145, -314, -229, -397, -205, -300, -229, + -373, -215, -360, -218, -346, -234, -397, -220, -360, -257, -382, -254, + -389, -213, -322, -234, -324, -214, -345, -227, -381, -255, -363, -174, + -354, -207, -318, -230, -299, -263, -316, -271, -243, -219, -239, -195, + -263, -183, -262, -181, -310, -210, -258, -227, -229, -243, -215, -263, + -194, -269, -182, -235, -184, -209, -147, -196, -148, -196, -147, -207, + -155, -221, -187, -223, -184, -221, -208, -205, -186, -173, -165, -148, + -132, -141, -127, -151, -120, -154, -131, -136, -104, -119, -115, -117, + -108, -145, -140, -188, -146, -212, -174, -199, -183, -203, -176, -190, + -174, -178, -155, -183, -156, -192, -139, -178, -158, -185, -162, -172, + -198, -158, -136, -126, -116, -127, -125, -157, -109, -154, -85, -152, + -60, -135, -26, -117, 10, -26, 16, -15, 36, 69, 64, 91, + 41, 52, -5, 31, -40, -16, -60, -3, -25, -20, -13, 18, + 23, 6, 15, 15, 42, 21, 45, 37, 32, 17, 30, 46, + 26, 13, 23, -19}, + /* IRC_Composite_C_R0195_T210_P015.wav */ + {5, -5, 6, -6, 6, -6, 6, -6, 6, -6, 7, -7, + 7, -7, 7, -8, 8, -8, 8, -9, 9, -9, 9, -9, + 9, -9, 7, -1, -45, -76, 21, -111, 31, -35, -9, -141, + 17, -81, 104, 351, 414, 314, 1167, 1284, -698, -334, -392, 1578, + 2039, 2332, 5286, 3230, 840, 735, 2013, 886, -250, -668, 388, 293, + -154, 194, 485, 539, 373, 247, 362, 554, 379, 14, 200, 206, + 243, 111, 161, 197, 127, 193, 183, 265, -4, 123, -60, 64, + -188, -26, -316, -89, -293, -150, -287, -153, -256, -136, -244, -221, + -246, -144, -274, -195, -276, -224, -307, -223, -340, -289, -302, -287, + -323, -301, -317, -303, -243, -292, -298, -294, -312, -280, -306, -243, + -309, -226, -276, -214, -262, -251, -287, -252, -261, -239, -268, -241, + -251, -220, -263, -226, -289, -222, -266, -235, -299, -243, -268, -249, + -232, -227, -224, -212, -188, -201, -205, -192, -215, -147, -197, -162, + -188, -158, -182, -181, -170, -210, -185, -204, -179, -201, -174, -187, + -176, -157, -173, -157, -158, -147, -138, -131, -123, -128, -126, -106, + -147, -126, -173, -126, -184, -147, -199, -168, -203, -173, -188, -192, + -200, -201, -192, -187, -199, -212, -196, -185, -179, -192, -178, -197, + -173, -188, -148, -169, -132, -157, -149, -165, -140, -138, -136, -123, + -135, -83, -122, -54, -79, -22, -60, -30, -15, -6, 29, -8, + 28, -22, 45, -13, 39, -33, 30, 5, 20, -21, 0, 10, + 30, -5, 10, -9, 35, 17, 55, 16, 50, 34, 50, 43, + 21, 55, 18, 41}, + /* IRC_Composite_C_R0195_T225_P015.wav */ + {3, -3, 3, -3, 3, -3, 3, -3, 3, -3, 3, -3, + 3, -3, 3, -3, 3, -3, 3, -3, 3, -3, 3, -2, + 2, -2, 2, -1, 1, 0, -5, -60, -132, -64, -20, -85, + -7, -176, -5, -52, -74, -72, 84, 430, 320, 474, 759, 338, + -353, -244, 418, 1855, 1977, 2351, 3573, 2234, 1049, 638, 1145, 830, + 98, -47, 162, 541, 334, 496, 426, 623, 595, 701, 586, 611, + 652, 468, 517, 289, 478, 320, 343, 117, 277, 121, 176, -24, + 22, -90, -30, -74, -66, -98, -115, -117, -110, -81, -130, -60, + -133, -100, -189, -113, -212, -176, -266, -224, -323, -264, -344, -286, + -346, -280, -345, -296, -322, -294, -296, -283, -287, -282, -269, -247, + -271, -247, -275, -227, -292, -244, -283, -265, -275, -259, -269, -255, + -263, -256, -260, -233, -264, -234, -273, -263, -267, -260, -261, -252, + -268, -235, -241, -202, -245, -183, -233, -168, -240, -165, -221, -162, + -203, -155, -192, -162, -177, -157, -183, -162, -190, -153, -205, -157, + -215, -152, -221, -147, -199, -142, -182, -144, -171, -142, -156, -148, + -173, -154, -163, -157, -167, -170, -162, -167, -179, -174, -184, -178, + -180, -174, -206, -178, -198, -188, -207, -204, -201, -199, -190, -214, + -206, -229, -200, -212, -173, -192, -170, -215, -166, -189, -131, -174, + -137, -168, -105, -105, -64, -83, -43, -48, 2, -25, -6, -28, + 14, -21, 8, -16, 16, 3, 33, 17, 37, 28, 49, 34, + 62, 28, 60, 13, 34, 6, 35, -8, 13, -2, 6, 11, + 8, 6, -3, 12}, + /* IRC_Composite_C_R0195_T240_P015.wav */ + {0, 0, 0, 0, 1, -1, 1, -1, 1, -1, 1, -1, + 1, -1, 1, -1, 2, -2, 2, -2, 2, -2, 3, -3, + 3, -4, 4, -5, 6, -7, 9, -12, 19, -47, -65, -42, + -103, -56, -86, -61, -84, -136, -84, -125, -76, 46, 139, 205, + 243, 514, 311, -296, -329, 431, 1034, 1553, 1739, 2266, 2575, 1097, + 709, 1340, 1199, 680, 407, 592, 719, 747, 950, 891, 924, 905, + 835, 742, 679, 646, 246, 443, 284, 396, 76, 270, 79, 360, + 141, 234, 132, 176, 91, 44, 66, -33, 37, -24, -26, -52, + -140, -134, -171, -129, -209, -205, -236, -225, -189, -236, -238, -284, + -228, -319, -232, -313, -237, -314, -261, -304, -270, -301, -338, -291, + -284, -273, -275, -283, -246, -253, -222, -299, -253, -305, -231, -286, + -251, -299, -224, -259, -244, -287, -247, -273, -229, -277, -233, -278, + -222, -256, -210, -244, -213, -226, -197, -200, -180, -187, -177, -191, + -166, -184, -157, -198, -181, -196, -166, -179, -163, -184, -173, -192, + -155, -187, -160, -198, -161, -183, -151, -173, -150, -191, -183, -210, + -175, -205, -187, -208, -163, -185, -168, -170, -162, -180, -159, -180, + -156, -158, -160, -186, -181, -225, -214, -217, -218, -234, -238, -231, + -223, -221, -238, -230, -217, -206, -178, -170, -207, -185, -183, -154, + -152, -154, -148, -132, -105, -76, -64, -31, -49, -9, -20, 6, + -12, 13, 19, 25, 27, 32, 34, 31, 31, 36, 25, 9, + 26, 16, 20, -7, 0, -5, 22, 12, 4, 3, 18, 14, + 9, -1, -12, -43}, + /* IRC_Composite_C_R0195_T255_P015.wav */ + {1, -1, 2, -2, 2, -2, 2, -2, 2, -2, 2, -2, + 2, -2, 2, -2, 2, -2, 2, -2, 2, -2, 2, -2, + 2, -2, 3, -3, 3, -3, 3, -4, 5, -6, 10, -30, + -120, -103, -93, -24, -84, -108, -106, -140, -67, -76, -73, -121, + -133, -119, 68, 29, 241, 313, 294, 61, 72, 517, 913, 1238, + 1676, 2099, 2348, 2019, 1198, 861, 1666, 1655, 972, 1040, 933, 753, + 835, 866, 579, 456, 471, 608, 472, 520, 250, 352, 304, 298, + 249, 242, 223, 199, 145, 120, 235, 93, 122, 49, 3, -39, + -15, -81, -71, -213, -81, -243, -130, -266, -181, -248, -213, -210, + -258, -180, -292, -166, -311, -258, -301, -258, -288, -304, -246, -283, + -255, -296, -284, -260, -336, -252, -351, -234, -370, -214, -328, -219, + -303, -234, -296, -242, -261, -235, -272, -252, -263, -194, -230, -174, + -222, -179, -253, -165, -264, -178, -236, -165, -217, -158, -210, -155, + -187, -164, -190, -169, -218, -178, -196, -162, -199, -188, -233, -170, + -224, -161, -189, -146, -207, -166, -198, -168, -188, -182, -169, -203, + -189, -199, -180, -192, -203, -183, -200, -212, -208, -215, -216, -240, + -188, -209, -178, -223, -162, -198, -167, -209, -211, -239, -196, -203, + -168, -214, -197, -239, -145, -209, -149, -231, -167, -214, -131, -185, + -129, -161, -129, -137, -105, -119, -97, -124, -84, -109, -56, -30, + 29, 13, 51, 35, 66, 39, 42, 23, 8, -3, 12, 8, + 29, 13, 32, 3, 26, 10, 21, -16, -7, -1, 7, 2, + -5, -3, -3, -1}, + /* IRC_Composite_C_R0195_T270_P015.wav */ + {-1, 1, -1, 1, -1, 1, -1, 1, -1, 1, -1, 1, + -1, 1, -1, 1, -1, 1, -1, 1, -1, 1, -1, 1, + -1, 1, -1, 1, -1, 1, -1, 1, -1, 1, -2, 2, + -2, 3, -5, 38, 41, 81, 155, 185, 109, 43, 168, 195, + 276, 233, 321, 381, 601, 818, 813, 1005, 694, 582, 706, 813, + 1380, 2408, 2859, 3417, 3192, 1234, 164, 601, 379, 145, -151, -609, + -543, -195, 49, -120, -111, 136, 61, -67, -105, -152, -114, -346, + -267, -397, -310, -240, -454, -334, -493, -338, -595, -418, -579, -472, + -506, -514, -489, -592, -416, -592, -471, -574, -491, -531, -515, -529, + -526, -514, -446, -512, -519, -497, -434, -454, -430, -467, -415, -434, + -400, -422, -405, -387, -422, -391, -428, -343, -360, -326, -389, -310, + -335, -241, -318, -246, -341, -202, -264, -207, -278, -221, -234, -239, + -210, -224, -186, -240, -226, -220, -207, -184, -222, -220, -245, -206, + -189, -231, -234, -238, -195, -237, -129, -227, -188, -216, -145, -224, + -225, -241, -252, -274, -214, -216, -202, -212, -164, -190, -171, -187, + -134, -175, -141, -168, -164, -167, -150, -174, -138, -128, -101, -135, + -76, -112, -70, -116, -70, -102, -92, -120, -98, -123, -112, -146, + -92, -145, -88, -135, -96, -115, -84, -87, -67, -61, -82, -42, + -48, 11, -13, 5, 25, 35, 52, 70, 95, 98, 101, 116, + 108, 116, 94, 137, 109, 102, 83, 89, 88, 89, 64, 59, + 40, 43, 14, 53, 3, 16, -3, 7, 12, -13, 16, -2, + 28, 6, 37, 18}, + /* IRC_Composite_C_R0195_T285_P015.wav */ + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 1, -1, 1, -1, 1, -1, + 2, -3, 6, -109, -159, -182, -252, -225, -252, -209, -220, -323, + -249, -123, 27, 108, 237, 39, -227, -176, -141, 26, 983, 1563, + 1989, 2256, 2042, 1443, 1643, 1525, 1088, 1602, 1474, 1170, 1150, 824, + 708, 924, 1176, 1036, 269, 443, 781, 769, 425, 308, 405, 367, + 433, 461, 448, 234, 194, 163, 218, 246, 100, -62, 56, -52, + 109, -114, 68, -271, 33, -231, -10, -251, -107, -211, -189, -193, + -271, -173, -258, -220, -344, -246, -222, -245, -241, -310, -276, -282, + -207, -275, -244, -325, -317, -257, -224, -252, -269, -249, -255, -262, + -227, -233, -233, -271, -256, -189, -188, -164, -249, -173, -198, -150, + -210, -124, -172, -127, -154, -156, -239, -193, -248, -200, -220, -169, + -212, -123, -176, -157, -249, -249, -297, -272, -295, -303, -316, -296, + -263, -235, -217, -173, -234, -208, -250, -182, -255, -227, -234, -247, + -244, -205, -187, -219, -199, -160, -156, -146, -178, -163, -199, -166, + -183, -164, -214, -208, -196, -160, -168, -196, -193, -160, -204, -162, + -188, -175, -206, -218, -198, -177, -203, -194, -169, -169, -172, -168, + -160, -149, -149, -113, -128, -93, -92, -23, -69, -13, -56, -10, + -44, 8, -28, 9, -21, 0, 13, 41, 16, 24, 3, 43, + 28, 54, 11, 12, -13, 29, -17, 4, -27, 25, -15, 29, + -5, 31, 21, 23}, + /* IRC_Composite_C_R0195_T300_P015.wav */ + {3, -3, 3, -3, 3, -3, 3, -3, 4, -4, 4, -4, + 4, -4, 4, -4, 4, -4, 5, -5, 5, -5, 5, -6, + 6, -6, 6, -7, 7, -8, 9, -10, 13, -17, 28, -93, + -234, -180, -207, -251, -293, -262, -250, -346, -376, -130, 67, -6, + 108, -151, -710, -124, 223, 55, 1941, 2161, 1520, 2079, 1404, 552, + 750, 680, 497, 384, 1277, 1177, 733, 896, 1394, 1657, 1268, 1283, + 1309, 1208, 892, 805, 954, 655, 670, 296, 482, 320, 570, 596, + 507, 445, 118, 348, 106, 469, 16, 25, -157, 191, 5, 47, + -157, -78, -270, -124, -129, -61, -219, -206, -188, -118, -157, -193, + -216, -244, -222, -155, -190, -248, -263, -189, -260, -255, -305, -285, + -272, -179, -250, -320, -353, -177, -207, -158, -311, -279, -294, -136, + -181, -176, -231, -183, -184, -86, -72, -138, -172, -154, -185, -208, + -132, -194, -245, -197, -155, -169, -170, -257, -250, -273, -291, -308, + -267, -292, -322, -261, -262, -223, -282, -280, -302, -284, -249, -260, + -293, -276, -203, -199, -209, -219, -251, -230, -225, -236, -239, -254, + -234, -253, -179, -216, -170, -211, -150, -212, -180, -190, -146, -198, + -192, -173, -159, -174, -166, -187, -188, -211, -146, -166, -163, -201, + -184, -171, -129, -130, -154, -144, -145, -167, -132, -161, -149, -160, + -132, -127, -113, -104, -89, -73, -95, -76, -83, -55, -53, -22, + -5, -13, -3, 5, 22, 0, 15, 5, -11, -15, 15, -13, + -5, -8, 7, -15, 26, 37, 33, 12, 52, 50, 45, 34, + 80, 35, 79, 31}, + /* IRC_Composite_C_R0195_T315_P015.wav */ + {-3, 3, -3, 3, -3, 3, -3, 3, -3, 3, -3, 3, + -3, 3, -3, 3, -3, 3, -3, 3, -3, 3, -3, 3, + -3, 3, -3, 3, -3, 2, -2, 2, -3, -145, -419, -273, + -234, -320, -494, -368, -332, -137, 23, 435, 141, -274, 189, -707, + -1152, 1545, 415, 1656, 4094, 1604, 1580, 2053, 473, -525, 351, 1046, + 431, 4, 613, 555, 843, 494, 1071, 1048, 1375, 1139, 994, 1431, + 1283, 1617, 838, 1011, 491, 1146, 716, 701, 358, 388, 443, 310, + 457, 32, -5, -93, 169, 269, -27, 91, -165, 86, -189, 135, + -120, -103, -270, -74, -56, -217, -55, -238, -119, -288, -120, -263, + -264, -223, -279, -179, -302, -173, -211, -262, -189, -255, -177, -382, + -172, -263, -206, -302, -155, -222, -165, -156, -91, -226, -129, -208, + -104, -116, -7, -182, -57, -43, -59, -165, -192, -254, -334, -329, + -222, -307, -218, -299, -183, -230, -153, -307, -262, -341, -352, -360, + -306, -320, -349, -329, -284, -313, -262, -287, -276, -348, -332, -282, + -281, -235, -311, -213, -232, -205, -232, -209, -189, -252, -237, -224, + -214, -199, -229, -157, -205, -196, -185, -173, -156, -212, -157, -188, + -156, -172, -131, -151, -175, -157, -139, -172, -126, -151, -157, -177, + -123, -115, -119, -116, -103, -151, -157, -157, -103, -111, -83, -143, + -134, -106, -78, -57, -83, -76, -143, -96, -110, -30, -81, -37, + -72, -10, -21, -9, 18, 1, -1, -42, 21, 2, 14, 26, + 52, 25, 26, 34, 53, 32, 44, 40, 74, 42, 66, 49, + 90, 62, 69, 50}, + /* IRC_Composite_C_R0195_T330_P015.wav */ + {1, -1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, + -1, 1, -2, 2, -2, 3, -3, 4, -5, 6, -7, 9, + -11, 14, -18, 25, -38, 72, -494, -377, -163, -458, -490, -5, + -649, -505, -448, 737, 42, 720, -5, -1466, 655, -1957, -272, 3638, + 1401, 3790, 5414, 320, 1005, 1351, 716, -1658, 736, 755, 629, -241, + 367, 775, 655, 1313, 522, 711, 607, 1017, 814, 1153, 1014, 727, + 1072, 1193, 1369, 911, 877, 665, 747, 788, 602, 389, -2, 301, + 79, 299, 235, 78, -171, -171, -12, -204, -96, -124, -285, -390, + -109, 136, -127, -79, -244, -125, -236, -21, -257, -313, -264, -266, + -248, -188, -112, -345, -304, -50, -265, -235, -235, -38, -345, -176, + -119, -52, -156, -49, -98, -184, -269, -46, -139, -85, -206, -15, + -64, -75, -25, 28, -69, -306, -216, -145, -321, -127, -396, -359, + -364, -161, -402, -370, -313, -329, -419, -337, -409, -317, -386, -319, + -505, -396, -337, -244, -368, -365, -380, -286, -289, -230, -330, -297, + -331, -175, -233, -184, -243, -195, -204, -144, -176, -187, -238, -158, + -239, -170, -186, -124, -214, -122, -133, -166, -196, -122, -151, -204, + -200, -147, -209, -159, -148, -140, -151, -108, -106, -148, -137, -107, + -109, -157, -138, -152, -159, -152, -143, -132, -169, -84, -121, -99, + -110, -53, -57, -49, -65, -56, -59, -72, -39, -80, -76, -50, + -24, -78, -31, -4, 10, -54, 43, 12, 18, 43, 54, 13, + -6, 86, 38, 22, 50, 62, 50, 38, 99, 31, 62, 60, + 58, 29, 11, 38}, + /* IRC_Composite_C_R0195_T345_P015.wav */ + {-11, 11, -12, 12, -13, 13, -14, 15, -16, 16, -17, + 18, -19, 20, -22, 23, -24, 26, -27, 29, -32, 34, + -38, 43, -52, 76, -328, -363, -214, -611, -509, -335, -643, + -447, -869, 31, 57, 1040, 355, -374, 1188, -2297, -4332, 5821, + -1811, 4636, 8422, 1800, 2573, 2446, 333, -2543, 348, 441, 1038, + -1221, -120, 503, 1705, 557, 994, 621, 1108, 529, 1387, 553, + 713, 268, 987, 1168, 1373, 742, 1063, 633, 1079, 800, 733, + 478, 430, -8, 448, 243, 389, -34, 240, -156, 40, -68, + 208, -262, -163, -284, -85, -119, 4, -292, -169, -173, -9, + -177, -185, -284, -261, -239, -151, -333, -444, -278, -212, -269, + -320, -207, -353, -167, -122, 5, -101, -146, -236, 26, 158, + -193, -6, 31, -110, -13, 194, -336, -291, 166, -187, -339, + 71, 83, -256, -36, 64, -291, -253, -258, -423, -483, -404, + -542, -450, -398, -404, -517, -415, -395, -300, -370, -455, -406, + -370, -334, -365, -318, -452, -367, -267, -228, -288, -345, -283, + -288, -195, -237, -211, -198, -242, -243, -148, -120, -215, -182, + -175, -171, -178, -118, -143, -239, -153, -169, -160, -247, -180, + -208, -203, -145, -188, -199, -188, -106, -162, -137, -157, -131, + -109, -183, -154, -130, -109, -197, -150, -64, -168, -155, -207, + -78, -176, 15, -176, -113, -192, -34, -58, -13, -144, -90, + -50, -81, -89, 24, -35, -100, -56, -23, -9, 29, 37, + -35, -51, 58, 55, 55, -8, 24, 51, 71, 28, 39, + 46, 49, 51, 27, -11, 48, 41, 22, -3, 23, -14, + 20, -19, -13}}; + +const int16_t irc_composite_c_r0195_p030[][256] = + {/* IRC_Composite_C_R0195_T000_P030.wav */ + {-54, 55, -55, 56, -57, 57, -58, 58, -58, 59, -58, + 58, -58, 56, -55, 52, -48, 42, -34, 21, 1, -38, + 103, -854, -293, -223, -413, -662, -699, -525, -649, -153, -821, + -339, 218, 2022, 2054, -2330, -2792, 1747, -9777, 10764, 1443, 5425, + 12121, 258, -1458, 190, 2417, 73, -781, 460, -747, -560, 1297, + 150, 875, 435, 1456, 292, 1300, -209, 948, 616, 962, -100, + 649, 571, 794, 577, 979, 597, 904, 611, 978, 804, 976, + 113, 890, 241, 591, 176, 615, -381, 92, -14, 208, -196, + 144, -25, -87, -290, -51, -201, -11, -376, 13, -401, 6, + -326, 16, -348, -73, -269, -196, -276, -77, -266, -298, -332, + -42, -326, -208, -519, -147, -423, -93, -432, -257, -451, -109, + -270, -192, -375, -151, -315, -188, -206, -37, -459, -168, -241, + -10, -278, -73, -257, 87, -155, -18, -234, -126, -305, -82, + -294, -405, -294, -172, -409, -47, -89, -22, -89, 128, -65, + 30, 48, -38, -108, -174, -371, -254, -219, -255, -366, -375, + -485, -192, -333, -204, -374, -171, -415, -254, -437, -213, -353, + -257, -320, -265, -229, -230, -199, -344, -342, -208, -291, -328, + -415, -224, -366, -172, -311, -156, -289, -178, -259, -84, -212, + -136, -195, -136, -204, -23, -192, -135, -219, -38, -190, -41, + -200, -34, -195, -41, -117, -47, -160, -96, -111, -97, -131, + -73, -186, -78, -137, -70, -202, -93, -119, -55, -149, -67, + -139, -48, -119, -34, -122, -39, -119, -31, -62, -1, -61, + -28, -48, 31, 6, 17, -8, 75, 29, 40, 45, 65, + 21, 45, 14}, + /* IRC_Composite_C_R0195_T015_P030.wav */ + {45, -49, 52, -57, 61, -66, 71, -77, 83, -90, + 98, -107, 116, -126, 138, -150, 164, -177, 188, -179, + -73, -966, -102, -989, 560, -1173, -327, -1684, 597, -1115, + 259, -1628, 2183, 1183, 3440, -5302, 3131, -10502, -529, 12738, + -6827, 21598, 5258, -7, -3187, 1963, 495, 327, 502, -665, + -1769, 1964, -371, 1009, 596, 896, 151, 1819, -137, 401, + 915, 974, 249, 114, 559, 962, 751, 584, 371, 576, + 838, 1123, 906, 351, -23, 332, 521, 427, -138, -265, + -100, 55, -54, 59, -205, -261, -77, 2, -107, -229, + -219, -403, -235, -134, -143, -470, -234, -177, -61, -236, + -83, -258, -197, -192, -101, -300, -331, -254, -216, -242, + -271, -289, -310, -261, -262, -333, -321, -293, -270, -333, + -255, -243, -275, -316, -256, -252, -176, -243, -182, -212, + -172, -289, -118, -228, -303, -307, -77, -225, -298, -230, + -122, -403, -282, -152, -387, -451, 8, 13, -160, -49, + 124, -112, -104, 87, -45, -358, -217, -135, -206, -167, + -172, -229, -317, -197, -162, -307, -272, -363, -290, -325, + -168, -360, -289, -359, -212, -286, -239, -278, -241, -270, + -234, -333, -266, -308, -234, -344, -218, -357, -274, -331, + -208, -244, -205, -284, -219, -226, -149, -177, -181, -201, + -159, -198, -109, -172, -113, -218, -95, -145, -67, -151, + -73, -148, -84, -129, -52, -118, -51, -142, -54, -113, + -29, -150, -66, -118, -44, -150, -8, -105, -59, -133, + -10, -103, -51, -102, -26, -114, 2, -62, -14, -78, + 24, -62, 35, -36, 28, -33, 74, -22, 57, 13, + 101, -23, 94, 36, 50, 32}, + /* IRC_Composite_C_R0195_T030_P030.wav */ + {22, -22, 22, -22, 22, -22, 21, -21, 20, -19, 18, + -16, 13, -9, 3, 6, -23, 62, -927, -68, -994, 540, + -922, -246, -1369, 328, -1205, -194, -1442, 1439, 570, 2758, -885, + 3393, -4925, -8851, 7444, -11725, 26992, 7407, 2376, 4575, -342, -2312, + -724, 2866, -938, -1366, 86, -585, 1150, 1637, 627, 500, 364, + 713, 726, 1042, -231, 616, 243, 1337, 315, 722, 212, 833, + 509, 1128, 741, 763, -23, 706, -315, 392, -241, 100, -771, + -188, -212, 298, -422, -413, -205, 37, -281, -130, -523, -88, + -503, -112, -573, -211, -454, -80, -504, -132, -294, -80, -239, + -22, -204, -163, -320, -82, -296, -297, -476, -149, -313, -195, + -414, -189, -328, -173, -333, -197, -351, -236, -353, -212, -335, + -289, -358, -212, -232, -235, -269, -127, -247, -255, -385, -167, + -387, -267, -392, -201, -373, -245, -334, -180, -331, -245, -322, + -210, -238, -125, -198, -59, -138, -44, -137, -3, -58, 9, + -199, -115, -324, -112, -304, -122, -334, -195, -342, -88, -186, + -146, -296, -157, -238, -181, -256, -164, -321, -198, -320, -193, + -346, -189, -318, -169, -297, -240, -369, -178, -273, -261, -370, + -222, -299, -226, -327, -186, -311, -192, -283, -157, -277, -144, + -267, -140, -249, -109, -220, -95, -203, -109, -172, -51, -152, + -88, -149, -49, -133, -83, -123, -30, -107, -62, -145, -55, + -94, -23, -81, -49, -86, -1, -42, -24, -119, -38, -58, + -5, -105, -50, -71, 1, -68, -36, -53, -6, -19, 20, + -14, 18, -29, 55, -17, 56, -9, 73, 3, 81, 35, + 97, 32, 73}, + /* IRC_Composite_C_R0195_T045_P030.wav */ + {9, -7, 5, -3, 0, 3, -7, 12, -18, 25, -34, + 45, -60, 79, -107, 151, -363, -677, -240, -871, 518, -941, + -269, -1276, 265, -1859, 1187, -1350, 2149, 1, 5698, -4523, 4866, + -12427, -2128, 5167, 3666, 25146, 2435, 401, 417, 1825, -3164, 2605, + 613, -1851, -1719, 979, -329, 2301, -426, 1606, -126, 1675, 119, + 1149, -57, 595, -80, 1644, 19, 793, -129, 1326, 678, 1045, + -17, 856, -22, 267, -380, 190, -844, -146, -664, 199, -735, + -167, -428, 32, -402, -217, -411, -308, -427, -284, -543, -438, + -454, -307, -561, -259, -430, -199, -381, -181, -382, -148, -228, + -87, -405, -169, -303, -91, -361, -175, -378, -291, -318, -127, + -301, -269, -260, -171, -316, -164, -268, -241, -361, -178, -341, + -229, -322, -201, -335, -221, -314, -343, -328, -221, -321, -372, + -308, -210, -400, -380, -322, -269, -386, -304, -356, -223, -292, + -166, -317, -192, -91, -100, -158, -51, -73, -116, -23, -56, + -150, -243, -92, -175, -169, -181, -177, -294, -169, -187, -210, + -286, -130, -227, -213, -253, -159, -238, -150, -211, -249, -260, + -144, -232, -247, -283, -227, -289, -178, -293, -252, -299, -148, + -285, -211, -271, -197, -276, -200, -222, -204, -252, -176, -235, + -205, -223, -107, -218, -119, -198, -109, -182, -35, -149, -101, + -171, -62, -123, -44, -128, -75, -141, -37, -172, -54, -143, + -11, -144, -27, -104, 26, -103, 23, -105, 37, -73, 18, + -74, 15, -63, 4, -83, 16, -68, -5, -50, 47, -28, + 24, -35, 41, -27, 78, -21, 77, -17, 105, 16, 96, + 7, 122, 24}, + /* IRC_Composite_C_R0195_T060_P030.wav */ + {8, -7, 7, -6, 5, -5, 4, -3, 3, -3, 3, + -3, 5, -9, 20, 86, -790, 103, -872, 55, -678, -196, + -962, -387, -893, 162, 722, 1609, 608, 2929, 1713, -6837, -667, + -12691, 15713, 13906, 9242, 5982, 83, -1809, 1076, 1596, 1326, -1529, + -1392, -366, 731, 78, 1368, -246, 850, 592, 521, 699, 1115, + 310, 930, 973, 592, 664, 159, 708, 710, 489, -74, 485, + -697, 374, -695, 249, -1252, 204, -915, 137, -777, -91, -241, + -403, -350, -466, -269, -521, -499, -540, -380, -320, -709, -233, + -517, -228, -634, -244, -652, -290, -447, -147, -545, -241, -352, + 5, -488, -69, -437, 53, -465, -6, -353, -78, -525, -24, + -335, -157, -418, -27, -367, -103, -287, -241, -397, -158, -276, + -304, -351, -181, -369, -225, -401, -209, -512, -184, -447, -153, + -476, -152, -484, -76, -492, -243, -498, -127, -515, -186, -392, + -135, -408, -53, -239, -82, -191, 13, -138, -32, -118, -6, + -177, -44, -187, -143, -246, -50, -262, -108, -264, -71, -256, + -48, -325, -90, -349, -63, -352, -110, -280, -159, -311, -153, + -350, -187, -295, -152, -318, -137, -192, -178, -252, -142, -284, + -157, -277, -190, -286, -151, -282, -119, -257, -91, -310, -54, + -224, -93, -207, -75, -245, -9, -203, -48, -237, 12, -182, + -40, -201, 33, -146, -26, -200, -37, -156, -43, -159, -28, + -130, -32, -114, 20, -143, 6, -113, 12, -126, 58, -103, + 21, -121, 59, -91, 44, -108, 59, -43, 47, -58, 60, + -38, 52, -61, 83, -36, 62, 3, 66, 27, 84, 84, + 65, 66, 103}, + /* IRC_Composite_C_R0195_T075_P030.wav */ + {100, -103, 106, -109, 112, -115, 118, -120, 122, -122, 119, + -110, 88, -25, 53, -874, 378, -897, 369, -1226, 537, -1263, + 224, -1798, 1274, 98, 2195, -462, 4170, 361, -3989, -3149, -11328, + 17722, 9812, 11683, 3944, -511, -808, 1931, 2611, 1751, -2704, -605, + -430, 728, -349, 908, 612, 941, -687, 931, 759, -66, 894, + 2123, 310, 1489, 449, 746, 475, 494, -493, -725, 213, -1074, + 381, -1100, -205, -670, -115, -645, -170, -469, -431, 31, -824, + -248, -707, -458, -476, -267, -901, -228, -228, -601, -420, -335, + -514, -545, -367, -632, -446, -566, -351, -391, -306, -444, -134, + -388, -144, -333, -243, -194, -98, -434, -105, -173, -206, -249, + -121, -377, -102, -345, -159, -256, -146, -470, -106, -327, -152, + -330, -205, -368, -192, -409, -197, -444, -310, -359, -229, -461, + -161, -393, -318, -351, -227, -387, -261, -314, -291, -270, -249, + -242, -213, -299, -192, -269, -108, -253, -73, -278, 80, -171, + -11, -111, 7, -198, -30, -155, -148, -117, -164, -168, -181, + -155, -172, -221, -156, -207, -176, -229, -130, -292, -171, -280, + -233, -288, -132, -237, -242, -209, -166, -245, -147, -263, -160, + -240, -126, -261, -109, -276, -150, -274, -92, -247, -135, -225, + -43, -207, -57, -174, -33, -157, -10, -172, -20, -116, -56, + -146, 22, -132, -61, -119, -53, -166, -22, -107, -81, -99, + -40, -65, -89, -70, -32, -77, -31, -75, -25, -75, 36, + -146, 3, -52, 3, -100, 13, -55, -5, -17, 13, -41, + 5, -12, 59, -13, 32, 3, 61, 44, 107, 53, 129, + 85, 118, 95}, + /* IRC_Composite_C_R0195_T090_P030.wav */ + {-85, 89, -94, 98, -102, 107, -112, 116, -119, 121, + -119, 106, -58, -455, 60, 99, -1054, -36, -88, 242, + -1635, 433, -1255, 1196, -1210, 2540, -840, 5344, -2798, 4670, + -10472, -874, 1281, 9198, 19443, 266, 2404, -1705, 3816, 1662, + 3038, 83, -1499, -2490, 1368, -37, 500, -334, 1490, -626, + 909, -15, 1075, 678, 1886, 658, 852, 386, 1418, 146, + 400, -1073, -338, -633, -165, -1024, -383, -1251, -264, -407, + -25, -1062, -140, -559, 70, -716, -459, -642, -96, -725, + -472, -412, -384, -647, -281, -605, -395, -572, -399, -706, + -351, -583, -297, -495, -434, -423, -201, -406, -407, -323, + -268, -341, -186, -360, -201, -241, -141, -313, -94, -257, + -169, -208, -194, -224, -147, -275, -276, -255, -215, -224, + -195, -256, -265, -271, -252, -352, -290, -430, -350, -309, + -268, -387, -267, -289, -271, -262, -303, -332, -237, -287, + -328, -292, -220, -307, -211, -297, -184, -216, -183, -248, + -116, -180, -163, -187, -112, -160, -80, -117, -46, -81, + -25, -91, -53, -114, -120, -176, -181, -173, -184, -205, + -224, -154, -233, -229, -178, -204, -243, -197, -188, -194, + -206, -154, -204, -205, -213, -116, -191, -127, -210, -145, + -216, -173, -183, -177, -263, -209, -141, -116, -148, -107, + -124, -68, -87, -45, -121, -68, -134, -77, -87, -29, + -115, -67, -117, -46, -114, -27, -115, -45, -100, -16, + -82, -32, -85, -17, -74, -28, -41, -27, -85, -23, + -68, -40, -47, -15, -96, -13, -38, 11, -39, -38, + -19, 1, -19, -3, -8, -15, -22, 28, 17, 70, + 76, 128, 80, 124, 112, 151}, + /* IRC_Composite_C_R0195_T105_P030.wav */ + {104, -111, 117, -125, 133, -143, 153, -166, 180, -197, 218, + -245, 288, -401, 132, -429, -287, -538, 503, -701, -185, -1083, + 867, -1151, 1236, -192, 2589, 704, 2171, -548, -4634, -1245, -3979, + 18483, 6281, 7009, -196, 501, 1762, 4063, 3838, -1, -2732, -1085, + -409, 884, 444, -393, -9, 733, 362, 138, 1179, 1594, 1300, + 947, 317, 734, 700, 55, -900, -943, -310, -712, -521, -933, + -377, -1554, -143, -410, 48, -924, -152, -440, -212, -554, -286, + -601, -527, -371, -454, -653, -533, -426, -450, -579, -486, -483, + -369, -609, -354, -543, -201, -457, -293, -586, -146, -430, -339, + -522, -196, -439, -324, -340, -141, -423, -157, -344, -39, -390, + -88, -363, -66, -244, -152, -284, -176, -153, -268, -264, -245, + -148, -256, -290, -221, -325, -190, -340, -211, -490, -165, -333, + -238, -471, -203, -235, -228, -397, -218, -264, -147, -365, -267, + -367, -142, -319, -211, -356, -158, -290, -141, -243, -127, -217, + -86, -135, -64, -128, -75, -153, -67, -155, -25, -190, -45, + -193, -18, -259, 1, -235, -77, -284, -88, -227, -151, -250, + -186, -245, -111, -256, -126, -274, -46, -272, -88, -254, -32, + -214, -71, -201, -144, -187, -98, -154, -182, -196, -78, -154, + -85, -159, -36, -221, -26, -121, -4, -188, -47, -127, -22, + -132, -24, -158, -49, -155, -2, -158, -21, -135, -30, -121, + 10, -87, -16, -118, 10, -48, -22, -86, -12, -80, 14, + -80, 13, -86, 50, -103, 29, -96, 64, -85, 45, -104, + 50, -45, 64, -67, -11, -39, 117, -14, 158, 27, 234, + 13, 225, 68}, + /* IRC_Composite_C_R0195_T120_P030.wav */ + {28, -28, 29, -29, 29, -29, 29, -27, 25, -21, 15, + -5, -12, 43, -140, 88, -559, -189, 139, -567, 198, -314, + -739, -383, 1538, -450, 794, 642, 4608, -2302, 1277, -7331, 2563, + 5148, 5379, 12280, 2713, -323, -1693, 7682, 3305, 1850, -1530, -2098, + -1411, 1431, -392, 275, -715, 741, -246, 1255, 618, 1870, 987, + 1146, 368, 730, -169, 18, -891, -805, -1522, -439, -421, -616, + -1006, -319, -441, -263, -511, -164, -630, -92, -636, -184, -746, + -202, -725, -323, -658, -347, -529, -471, -673, -383, -415, -310, + -627, -358, -521, -225, -567, -258, -424, -264, -432, -273, -312, + -333, -413, -308, -330, -340, -367, -280, -273, -329, -276, -223, + -220, -254, -266, -200, -177, -199, -276, -232, -217, -244, -204, + -238, -182, -299, -214, -297, -195, -311, -235, -299, -177, -299, + -241, -285, -222, -309, -277, -284, -244, -293, -235, -236, -241, + -258, -206, -250, -223, -292, -255, -245, -224, -254, -179, -205, + -152, -237, -116, -146, -78, -191, -75, -140, -27, -176, -63, + -134, -82, -199, -79, -164, -84, -202, -123, -164, -105, -201, + -110, -207, -105, -211, -126, -202, -134, -203, -129, -204, -131, + -154, -100, -194, -94, -163, -74, -166, -82, -216, -107, -149, + -27, -161, -56, -129, -40, -130, -55, -114, -53, -102, -63, + -77, -31, -89, -106, -127, -83, -123, -74, -94, -24, -173, + -31, -94, 11, -133, -8, -83, -23, -88, 12, -65, -46, + -111, 25, -94, 13, -117, 28, -88, 37, -79, 24, -35, + 51, -27, 22, 7, 50, -3, 53, 7, 103, 86, 163, + 91, 162, 75}, + /* IRC_Composite_C_R0195_T135_P030.wav */ + {6, -6, 7, -7, 8, -8, 9, -9, 10, -10, 11, -12, + 13, -14, 16, -18, -114, -234, -98, -110, -115, -173, -170, -232, + -274, 363, 317, 1353, 133, 2229, 1323, 703, -7423, 4287, 1521, 5913, + 8831, 4880, 804, -872, 5713, 3941, 3130, -2817, -2290, -315, 304, -50, + -26, -209, 494, 295, 1100, 751, 1288, 533, 716, 964, 54, -449, + -399, -528, -808, -934, -947, -951, -457, -538, -157, -625, -308, -385, + -95, -571, -310, -374, -278, -494, -514, -429, -461, -556, -433, -529, + -432, -535, -327, -580, -236, -492, -287, -555, -216, -430, -280, -451, + -246, -351, -270, -341, -290, -325, -254, -404, -265, -383, -232, -420, + -232, -398, -198, -399, -199, -300, -210, -291, -213, -230, -263, -273, + -261, -215, -220, -262, -203, -257, -189, -275, -199, -298, -243, -292, + -198, -258, -246, -263, -220, -238, -243, -271, -259, -242, -227, -263, + -220, -247, -166, -281, -180, -274, -165, -251, -184, -245, -176, -216, + -217, -207, -158, -156, -155, -166, -106, -127, -108, -139, -81, -152, + -98, -167, -88, -170, -109, -150, -125, -150, -119, -119, -124, -135, + -111, -142, -113, -144, -112, -168, -76, -193, -154, -199, -103, -124, + -149, -137, -125, -90, -105, -96, -120, -90, -84, -94, -99, -96, + -69, -90, -69, -82, -94, -80, -67, -60, -94, -70, -110, -76, + -102, -51, -103, -55, -74, -38, -105, -57, -91, -76, -96, -51, + -36, -59, -52, -57, -56, -61, -46, -70, -31, -32, -25, -47, + 6, -6, 17, -33, -1, 20, 49, 32, 43, 24, 63, 112, + 138, 114, 43, 112}, + /* IRC_Composite_C_R0195_T150_P030.wav */ + {40, -42, 44, -47, 49, -53, 56, -60, 64, -69, 75, -82, + 90, -99, 110, -125, 148, -207, 85, -316, 199, -301, 148, -341, + 137, -377, 122, 76, 967, 463, 1542, 593, 2935, -2335, -3746, 3892, + 1126, 7018, 5853, 6116, -1287, 2364, 4148, 3574, 181, -2393, -1125, -178, + 512, -398, -17, -462, 310, 1083, 929, 1228, 538, 479, 431, 450, + -124, -446, -926, -681, -484, -854, -858, -886, -425, -380, -163, -187, + -475, -333, -267, -193, -398, -286, -470, -331, -472, -511, -390, -535, + -398, -443, -447, -458, -316, -355, -323, -348, -312, -366, -331, -282, + -304, -410, -276, -324, -270, -334, -288, -347, -263, -345, -283, -348, + -294, -324, -322, -384, -264, -346, -287, -352, -258, -326, -246, -345, + -247, -332, -220, -276, -243, -276, -171, -257, -208, -277, -184, -237, + -184, -265, -186, -245, -165, -259, -201, -253, -180, -238, -198, -272, + -176, -246, -187, -277, -161, -252, -150, -275, -137, -267, -126, -254, + -141, -241, -149, -227, -151, -206, -151, -203, -126, -181, -101, -170, + -85, -185, -87, -146, -87, -170, -106, -170, -90, -160, -111, -150, + -107, -140, -90, -144, -63, -132, -90, -238, -87, -160, -61, -203, + -110, -165, -57, -102, -36, -116, -53, -98, -41, -96, -62, -105, + -61, -102, -70, -120, -86, -109, -103, -103, -70, -67, -72, -85, + -74, -124, -64, -94, -56, -139, -102, -99, -75, -97, -58, -61, + -30, -94, -25, -74, -43, -104, -48, -76, -56, -75, -27, -39, + -24, -3, 21, -10, -1, 6, 97, 22, 44, 1, 101, 93, + 134, 63, 82, 45}, + /* IRC_Composite_C_R0195_T165_P030.wav */ + {-5, 5, -6, 6, -6, 6, -6, 7, -7, 7, -8, 8, + -9, 9, -10, 11, -12, 14, -17, 25, -163, 17, -80, 12, + 2, -73, 14, -200, 92, 163, 755, 471, 1371, 723, 2348, -678, + -3403, 2150, 1314, 5172, 5430, 6608, 508, 1066, 3048, 2691, 1177, -1954, + -1312, -864, 1114, -99, -395, -170, 755, 800, 840, 925, 689, -7, + -2, -92, -86, -565, -703, -538, -415, -580, -306, -607, -337, -594, + -235, -197, -71, -393, -276, -461, -131, -367, -319, -372, -331, -590, + -309, -448, -316, -391, -370, -330, -202, -345, -331, -285, -252, -333, + -332, -343, -285, -341, -299, -355, -341, -319, -335, -324, -306, -321, + -304, -329, -296, -343, -321, -372, -317, -381, -294, -371, -299, -341, + -289, -334, -273, -311, -239, -276, -234, -285, -217, -256, -202, -250, + -169, -221, -150, -205, -167, -217, -204, -247, -225, -236, -214, -247, + -214, -207, -155, -197, -183, -192, -155, -160, -191, -236, -192, -219, + -188, -240, -195, -229, -141, -216, -140, -210, -108, -198, -110, -186, + -131, -164, -118, -161, -128, -132, -117, -113, -132, -130, -137, -165, + -120, -157, -110, -125, -107, -149, -159, -131, -114, -123, -178, -132, + -136, -75, -82, -32, -23, -71, -33, -49, -17, -94, -64, -87, + -53, -94, -80, -79, -100, -87, -113, -66, -94, -109, -102, -113, + -90, -94, -92, -105, -128, -124, -129, -89, -94, -67, -93, -83, + -95, -80, -94, -87, -89, -91, -53, -81, -31, -57, 18, -49, + -13, -37, 25, -7, 9, -13, -1, 53, 58, 59, 79, 109, + 133, 124, 84, 60}, + /* IRC_Composite_C_R0195_T180_P030.wav */ + {11, -12, 12, -12, 13, -13, 14, -14, 15, -15, 16, -17, + 18, -19, 20, -21, 23, -25, 28, -33, 39, -51, 85, 16, + -87, 129, -81, 94, 33, -96, -40, 382, 84, 940, 857, 674, + 1733, 496, -329, -2997, 2450, 4178, 3824, 5230, 3832, 1334, 27, 2852, + 1755, 841, -2811, -750, 709, 798, -492, -489, 295, 1105, 873, 238, + 383, 361, 52, -483, -293, -277, -430, -468, -128, -283, -308, -272, + -363, -267, -297, -189, -289, -143, -258, -242, -373, -291, -355, -331, + -315, -246, -385, -325, -276, -197, -314, -270, -313, -267, -313, -307, + -364, -269, -366, -341, -361, -328, -371, -363, -339, -357, -363, -329, + -335, -334, -353, -302, -358, -291, -419, -330, -381, -332, -384, -319, + -331, -309, -321, -276, -296, -256, -286, -256, -306, -268, -277, -214, + -200, -211, -199, -179, -152, -213, -196, -243, -196, -273, -196, -241, + -183, -226, -151, -195, -173, -198, -169, -171, -172, -166, -179, -179, + -198, -192, -224, -204, -227, -203, -207, -198, -181, -177, -142, -148, + -122, -136, -110, -124, -113, -120, -142, -140, -137, -157, -161, -213, + -116, -179, -81, -164, -112, -182, -121, -113, -114, -132, -189, -130, + -138, -73, -121, -47, -93, -7, -49, 37, -29, -7, -77, -47, + -80, -70, -85, -99, -107, -91, -49, -61, -81, -124, -69, -122, + -37, -160, -57, -192, -64, -193, -77, -181, -87, -158, -111, -159, + -105, -113, -89, -109, -88, -83, -83, -88, -75, -44, -73, -39, + -53, -1, -6, 52, 20, 54, 20, 68, 72, 99, 107, 107, + 128, 102, 141, 66}, + /* IRC_Composite_C_R0195_T195_P030.wav */ + {-2, 2, -2, 2, -2, 2, -2, 2, -2, 2, -2, 2, + -2, 2, -2, 1, -1, 1, -1, 0, 0, -1, 2, -3, + 4, -1, 20, -103, 88, -97, 77, -98, 0, -92, 123, 108, + 628, 541, 868, 946, 950, -323, -2426, 1960, 1868, 3655, 4432, 4145, + 1520, 425, 2257, 2243, 324, -1505, -101, 360, 566, -340, -97, 521, + 852, 598, 427, 177, 417, 31, 149, -102, -24, -395, -11, -98, + -149, -157, -72, -31, -41, -10, -27, -82, -183, -287, -120, -245, + -255, -309, -131, -235, -200, -246, -222, -253, -210, -246, -249, -280, + -281, -300, -278, -328, -316, -325, -352, -332, -302, -334, -406, -331, + -345, -314, -353, -298, -355, -316, -331, -341, -377, -390, -356, -375, + -325, -347, -301, -303, -263, -297, -274, -288, -262, -288, -280, -284, + -231, -214, -205, -233, -199, -221, -205, -231, -218, -253, -209, -218, + -203, -211, -199, -179, -191, -186, -199, -201, -193, -181, -187, -195, + -182, -193, -188, -207, -197, -194, -197, -186, -192, -163, -178, -160, + -165, -142, -165, -124, -155, -125, -159, -112, -160, -116, -190, -111, + -165, -107, -172, -122, -190, -155, -169, -127, -164, -144, -157, -122, + -135, -85, -113, -68, -116, -46, -67, -29, -54, -47, -65, -70, + -92, -108, -92, -118, -99, -92, -87, -77, -99, -78, -106, -86, + -118, -92, -123, -105, -136, -119, -126, -128, -143, -162, -171, -163, + -154, -125, -147, -120, -134, -91, -125, -91, -103, -88, -80, -77, + -45, -46, -3, 16, 24, 24, 27, 58, 42, 84, 85, 129, + 127, 141, 127, 125}, + /* IRC_Composite_C_R0195_T210_P030.wav */ + {-3, 3, -3, 3, -3, 3, -3, 3, -3, 3, -3, 3, + -3, 3, -3, 4, -4, 4, -4, 4, -4, 5, -5, 6, + -6, 7, -9, 12, -23, -83, -41, -51, -44, -54, -36, -72, + -62, 13, 62, 362, 515, 745, 618, 964, 123, -1474, 242, 1605, + 2935, 3779, 3927, 2051, 644, 1570, 1770, 938, -679, -243, 210, 595, + 104, 204, 284, 657, 698, 453, 338, 507, 437, 72, 227, 224, + 63, -55, 88, 141, 215, 131, 225, 97, 158, 60, 38, -69, + -107, -265, -85, -124, -202, -245, -131, -257, -191, -192, -178, -241, + -145, -269, -233, -270, -235, -338, -254, -338, -292, -359, -271, -388, + -290, -377, -344, -357, -293, -419, -370, -380, -349, -397, -346, -388, + -357, -344, -302, -335, -306, -279, -276, -246, -273, -284, -282, -237, + -264, -250, -226, -225, -248, -218, -230, -247, -240, -246, -259, -217, + -216, -201, -233, -193, -207, -183, -205, -202, -228, -190, -207, -213, + -222, -215, -237, -186, -220, -176, -235, -136, -217, -116, -201, -133, + -200, -133, -159, -165, -150, -164, -152, -134, -165, -144, -175, -119, + -164, -116, -139, -114, -152, -126, -132, -128, -147, -152, -173, -154, + -145, -152, -115, -135, -100, -99, -65, -75, -79, -71, -101, -104, + -126, -96, -135, -132, -139, -125, -80, -131, -108, -168, -90, -142, + -81, -142, -146, -133, -121, -109, -142, -113, -121, -117, -148, -144, + -150, -144, -150, -171, -140, -133, -106, -126, -118, -107, -114, -61, + -79, -59, -59, -5, -31, 13, -2, 37, 21, 52, 67, 76, + 116, 109, 131, 125}, + /* IRC_Composite_C_R0195_T225_P030.wav */ + {-1, 1, -1, 1, -1, 1, -1, 1, -1, 1, -1, 1, + -1, 1, -1, 2, -2, 2, -2, 2, -2, 2, -2, 3, + -3, 3, -4, 5, -7, 14, -133, -89, -52, -68, -90, -53, + -98, -77, -65, -141, -33, -38, 144, 467, 407, 586, 730, 265, + -702, -561, 1147, 2053, 3022, 3625, 2128, 1090, 1205, 1750, 1123, 130, + -65, 370, 545, 406, 83, 446, 636, 787, 529, 661, 549, 526, + 605, 368, 384, 101, 455, 156, 304, 149, 290, 238, 156, 207, + 48, 113, -86, -57, -36, -176, -93, -129, -107, -209, -107, -181, + -195, -158, -169, -157, -239, -205, -240, -206, -253, -259, -282, -324, + -296, -377, -347, -379, -340, -400, -365, -377, -398, -363, -379, -335, + -398, -330, -337, -283, -328, -250, -335, -246, -279, -228, -301, -284, + -299, -253, -272, -251, -277, -231, -269, -213, -274, -207, -256, -219, + -248, -212, -205, -234, -191, -244, -197, -238, -194, -226, -213, -226, + -214, -216, -219, -201, -233, -193, -216, -184, -183, -195, -171, -198, + -136, -183, -121, -177, -126, -151, -131, -156, -138, -160, -140, -152, + -130, -143, -109, -146, -113, -168, -114, -165, -123, -191, -139, -186, + -143, -163, -101, -126, -91, -147, -80, -140, -85, -158, -107, -181, + -100, -149, -100, -152, -116, -152, -120, -144, -111, -159, -127, -155, + -129, -148, -146, -173, -157, -145, -134, -136, -128, -146, -120, -128, + -115, -125, -133, -149, -144, -153, -151, -147, -139, -128, -113, -110, + -89, -78, -59, -67, -27, -39, -8, -19, 9, 9, 34, 44, + 63, 65, 98, 113}, + /* IRC_Composite_C_R0195_T240_P030.wav */ + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 1, -1, 1, -1, 1, -1, + 1, -1, 1, -1, 1, -1, 1, -1, 1, 1, -97, -166, + -68, -82, -98, -145, 6, -178, -148, -79, -61, -4, 226, 289, + 466, 695, 150, -363, -564, 127, 1778, 2371, 2730, 2418, 1314, 1333, + 1958, 1573, 362, 208, 618, 691, 672, 358, 520, 767, 841, 691, + 635, 738, 675, 472, 476, 447, 387, 278, 358, 303, 322, 169, + 236, 146, 167, 153, 93, 161, -50, 82, -137, 22, -215, -48, + -220, -102, -183, -156, -150, -189, -134, -246, -183, -293, -242, -320, + -276, -333, -344, -324, -341, -336, -359, -336, -345, -340, -353, -365, + -331, -325, -327, -353, -302, -293, -288, -330, -303, -271, -296, -272, + -313, -253, -304, -231, -298, -228, -279, -211, -267, -230, -276, -219, + -263, -229, -267, -208, -254, -187, -265, -150, -261, -152, -276, -148, + -255, -165, -256, -184, -237, -198, -227, -197, -220, -180, -224, -157, + -210, -127, -200, -118, -181, -83, -171, -100, -162, -99, -139, -124, + -137, -122, -137, -107, -131, -113, -152, -146, -173, -133, -190, -141, + -182, -105, -142, -132, -151, -164, -146, -160, -134, -193, -153, -203, + -128, -188, -144, -178, -164, -173, -166, -129, -130, -161, -160, -179, + -136, -174, -125, -201, -162, -176, -100, -152, -132, -174, -152, -170, + -137, -167, -127, -157, -111, -151, -85, -120, -73, -114, -88, -97, + -72, -75, -82, -80, -55, -47, -53, -51, -32, -31, 18, -8, + 35, 22, 73, 65}, + /* IRC_Composite_C_R0195_T255_P030.wav */ + {1, -1, 1, -1, 1, -1, 1, -1, 1, -1, 1, -1, + 1, -1, 1, -1, 1, -1, 1, -1, 2, -2, 2, -2, + 2, -2, 3, -3, 4, -4, 5, -6, 8, -12, 21, -67, + -186, -86, -78, -102, -151, -102, -59, -120, -188, -201, -77, -93, + -133, 190, 315, 388, 642, 206, -429, -627, -41, 1512, 2532, 2996, + 2313, 1394, 1764, 1723, 1247, 1241, 976, 437, 321, 813, 834, 517, + 405, 589, 1007, 744, 553, 490, 641, 609, 429, 381, 224, 460, + 347, 308, 191, 277, 332, 175, 143, 27, 143, -12, 2, -104, + -86, -53, -114, -79, -254, -137, -211, -109, -349, -252, -297, -195, + -348, -341, -330, -262, -295, -325, -316, -289, -220, -305, -292, -333, + -256, -294, -316, -394, -312, -314, -286, -387, -317, -323, -233, -326, + -302, -311, -250, -238, -292, -290, -296, -199, -259, -241, -292, -230, + -245, -240, -270, -264, -223, -199, -230, -230, -203, -176, -179, -217, + -226, -180, -203, -190, -239, -188, -230, -173, -226, -190, -237, -149, + -184, -187, -190, -158, -126, -139, -130, -127, -103, -111, -83, -113, + -124, -148, -136, -140, -136, -138, -118, -132, -127, -165, -140, -179, + -161, -224, -175, -209, -159, -182, -149, -200, -170, -196, -150, -218, + -152, -222, -177, -254, -168, -210, -186, -215, -137, -166, -128, -166, + -128, -184, -106, -195, -174, -233, -137, -162, -153, -164, -155, -113, + -146, -99, -141, -92, -113, -93, -86, -120, -80, -115, -69, -118, + -75, -88, -73, -78, -77, -48, -78, -41, -54, -26, -50, -11, + -21, 13, 14, 31}, + /* IRC_Composite_C_R0195_T270_P030.wav */ + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 1, -1, 2, -2, 4, + -6, 13, -118, -205, -234, -125, -147, -202, -210, -154, -149, -302, + -174, -138, 74, 139, 366, 494, 501, -30, -788, -660, 656, 2336, + 3548, 2933, 1466, 1698, 2087, 1596, 1331, 660, 745, 348, 609, 649, + 510, 592, 471, 837, 634, 881, 692, 658, 657, 499, 658, 390, + 588, 291, 406, 283, 323, 211, 84, 205, 87, 193, 12, 79, + -62, 28, -174, -101, -266, -102, -288, -156, -317, -186, -313, -245, + -324, -271, -272, -274, -260, -294, -238, -276, -248, -278, -258, -319, + -271, -317, -281, -337, -312, -324, -337, -313, -331, -296, -324, -287, + -323, -272, -287, -250, -300, -247, -285, -236, -292, -228, -244, -210, + -272, -214, -253, -227, -239, -213, -239, -229, -229, -214, -226, -214, + -215, -199, -227, -177, -203, -180, -193, -178, -164, -152, -146, -149, + -125, -151, -141, -143, -150, -155, -139, -130, -168, -120, -114, -108, + -126, -125, -154, -199, -182, -165, -181, -236, -178, -180, -150, -191, + -167, -205, -200, -201, -200, -203, -212, -213, -204, -217, -180, -215, + -180, -171, -178, -161, -169, -169, -194, -215, -198, -197, -181, -198, + -150, -180, -147, -175, -139, -160, -138, -133, -143, -113, -144, -115, + -133, -126, -110, -112, -106, -129, -97, -110, -96, -107, -98, -88, + -83, -69, -93, -79, -84, -51, -65, -56, -47, -41, -18, -38, + 4, -20, 19, 6}, + /* IRC_Composite_C_R0195_T285_P030.wav */ + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, + 1, -1, 1, -1, 1, -1, 2, -2, 3, -3, 4, -7, + 13, -190, -183, -235, -156, -218, -208, -304, -188, -211, -199, -139, + -161, 153, 336, 233, -123, -164, -557, -685, 1147, 1908, 2035, 3160, + 1492, 785, 1242, 1667, 1506, 818, 1143, 817, 1178, 780, 1057, 657, + 807, 713, 683, 790, 563, 729, 595, 855, 599, 704, 615, 614, + 540, 520, 362, 304, 245, 337, 219, 250, 133, 197, 59, 126, + -65, -70, -160, -89, -159, -231, -254, -291, -203, -274, -185, -304, + -208, -285, -189, -264, -256, -331, -243, -311, -262, -308, -280, -304, + -246, -335, -250, -318, -269, -309, -282, -300, -260, -340, -268, -307, + -236, -284, -269, -325, -251, -300, -247, -310, -244, -277, -183, -239, + -198, -269, -229, -228, -237, -229, -245, -249, -264, -220, -228, -201, + -208, -179, -162, -96, -70, -76, -134, -147, -121, -145, -142, -175, + -192, -136, -108, -69, -153, -156, -210, -206, -236, -198, -320, -233, + -263, -173, -277, -125, -224, -144, -166, -131, -224, -164, -197, -216, + -260, -240, -244, -177, -196, -187, -157, -155, -171, -202, -195, -206, + -203, -187, -195, -236, -200, -188, -186, -176, -232, -177, -163, -133, + -189, -164, -179, -140, -153, -146, -152, -146, -135, -108, -114, -110, + -119, -88, -135, -96, -118, -89, -127, -122, -110, -95, -72, -108, + -98, -91, -59, -48, -71, -44, -52, -26, -30, -20, -36, -34, + -3, -13, 9, 16}, + /* IRC_Composite_C_R0195_T300_P030.wav */ + {1, -1, 2, -2, 2, -2, 2, -2, 2, -2, 2, -2, + 2, -2, 2, -3, 3, -3, 3, -3, 3, -4, 4, -4, + 5, -5, 5, -6, 7, -8, 9, -10, 12, -16, 27, -117, + -227, -314, -248, -219, -228, -232, -272, -389, -204, -111, 300, 56, + 366, 86, -1011, -298, 44, -215, 2477, 2714, 2032, 2711, 645, 499, + 536, 1654, 449, 374, 611, 764, 837, 821, 1350, 980, 1002, 808, + 1149, 967, 761, 841, 707, 633, 745, 777, 758, 579, 642, 512, + 570, 444, 429, 371, 457, 296, 276, 181, 191, 36, 17, 10, + -72, -108, -107, -232, -151, -205, -165, -300, -163, -265, -253, -269, + -203, -258, -301, -275, -269, -292, -272, -260, -302, -297, -289, -229, + -300, -264, -305, -243, -283, -264, -269, -279, -280, -287, -259, -269, + -276, -247, -263, -262, -247, -231, -256, -218, -280, -241, -295, -239, + -252, -218, -256, -230, -258, -241, -251, -148, -189, -159, -149, -45, + -91, -37, -119, -83, -194, -47, -124, -58, -134, -130, -231, -259, + -239, -276, -269, -285, -258, -248, -198, -188, -240, -206, -250, -202, + -249, -205, -247, -163, -226, -240, -217, -219, -189, -209, -212, -237, + -257, -142, -184, -180, -176, -167, -213, -202, -187, -188, -191, -235, + -229, -241, -179, -197, -185, -210, -199, -189, -160, -163, -174, -186, + -151, -189, -138, -133, -147, -130, -137, -107, -118, -101, -102, -120, + -113, -110, -78, -102, -64, -110, -74, -76, -59, -92, -76, -85, + -71, -79, -43, -88, -39, -85, -19, -81, 13, -60, 8, -26, + 31, 8, 16, 23}, + /* IRC_Composite_C_R0195_T315_P030.wav */ + {-3, 3, -3, 3, -3, 3, -3, 3, -3, 3, -3, 3, + -3, 3, -3, 3, -3, 3, -3, 3, -4, 4, -4, 4, + -4, 5, -5, 6, -7, 10, -15, 27, -80, -353, -110, -370, + -400, -195, -345, -309, -394, -97, -188, 193, 754, 56, -290, -194, + -1922, 214, 1653, 1159, 4327, 2754, 1527, 1212, 743, 317, 539, 1293, + -56, -21, 321, 1262, 295, 442, 708, 1213, 856, 813, 872, 1000, + 917, 1073, 730, 897, 944, 834, 864, 775, 610, 537, 692, 383, + 605, 370, 536, 245, 348, 155, 280, 101, 83, 31, 90, -127, + -63, -179, -119, -272, -116, -304, -232, -233, -212, -345, -176, -295, + -247, -334, -204, -342, -233, -195, -284, -324, -229, -236, -255, -304, + -258, -323, -258, -297, -224, -323, -252, -263, -224, -317, -252, -224, + -255, -281, -221, -218, -205, -288, -209, -253, -193, -244, -249, -201, + -218, -210, -200, -172, -180, -181, -84, -203, -75, -54, 19, -108, + -2, -46, -171, -209, -203, -294, -294, -286, -264, -295, -154, -187, + -222, -182, -252, -197, -249, -211, -305, -232, -289, -224, -284, -267, + -256, -184, -221, -260, -295, -281, -237, -233, -228, -269, -251, -203, + -162, -193, -192, -189, -166, -237, -188, -201, -208, -209, -181, -224, + -228, -151, -192, -200, -209, -131, -196, -159, -162, -159, -166, -116, + -160, -151, -131, -47, -142, -119, -122, -73, -93, -108, -96, -133, + -90, -84, -86, -83, -90, -56, -96, -65, -65, -48, -80, -67, + -79, -50, -60, -46, -63, -39, -44, -12, -27, 9, -35, 23, + -3, 24, 6, 41}, + /* IRC_Composite_C_R0195_T330_P030.wav */ + {-5, 5, -5, 5, -5, 5, -5, 5, -5, 6, -6, 6, + -6, 6, -6, 6, -6, 6, -7, 7, -7, 7, -8, 8, + -9, 9, -11, 14, -26, -447, -195, -107, -505, -457, -480, -118, + -514, -260, -345, 82, 416, 511, 1319, -1763, -994, 103, -1768, 3014, + 4385, 2231, 5234, 1274, 290, -49, 1679, 111, -604, 148, 524, 573, + -122, 973, 360, 1000, 586, 1035, 552, 578, 788, 885, 854, 618, + 749, 894, 854, 1021, 930, 968, 502, 1006, 765, 768, 438, 460, + 339, 388, 236, 299, 159, 177, -65, 162, -6, -74, -134, -51, + -230, -132, -193, -105, -327, -152, -257, -191, -291, -227, -315, -280, + -252, -192, -306, -253, -281, -182, -326, -252, -283, -225, -353, -255, + -282, -258, -322, -236, -271, -288, -246, -248, -299, -265, -254, -312, + -330, -129, -295, -141, -204, -129, -190, -22, -154, -203, -125, -172, + -156, -188, -111, -223, -112, -106, -241, -86, -37, -262, -238, -193, + -103, -105, -7, -250, -241, -39, -63, -184, -238, -261, -364, -243, + -263, -310, -361, -286, -407, -227, -301, -289, -311, -319, -263, -215, + -201, -304, -287, -339, -226, -246, -239, -335, -276, -276, -181, -234, + -181, -257, -180, -234, -151, -165, -203, -213, -194, -170, -205, -157, + -164, -191, -168, -154, -86, -133, -128, -162, -96, -147, -81, -165, + -68, -173, -90, -164, -64, -127, -68, -162, -97, -99, -50, -115, + -84, -111, -102, -87, -74, -80, -123, -76, -57, -95, -59, -42, + -45, -74, -38, -27, -9, -30, 0, -55, 36, -23, 58, -21, + 61, -7, 59, 29}, + /* IRC_Composite_C_R0195_T345_P030.wav */ + {0, 0, 0, 0, 0, 0, 0, 1, -1, 1, -1, + 2, -2, 2, -3, 3, -4, 5, -5, 6, -7, 8, + -9, 10, -10, -1, -563, -328, -175, -595, -424, -853, 130, + -981, 36, -843, 608, 167, 1104, 1422, -2010, -2282, 1073, -3818, + 6278, 4848, 2400, 7303, 1229, -443, -1130, 2494, 95, -616, -743, + 335, 108, 968, 417, 599, 684, 1073, 630, 810, 221, 839, + 469, 1136, 218, 739, 427, 945, 692, 942, 955, 757, 852, + 1008, 762, 614, 598, 745, 200, 403, 51, 357, 29, 333, + -147, -65, 9, 35, -114, -161, -231, -52, -197, -139, -202, + -61, -310, -211, -214, -165, -307, -207, -293, -261, -259, -204, + -201, -310, -323, -244, -250, -260, -335, -226, -421, -206, -333, + -191, -401, -188, -244, -264, -283, -225, -208, -338, -233, -69, + -304, -181, -213, -18, -265, 39, -68, -82, -113, -107, 16, + -329, -80, -324, -205, -399, -184, -328, -391, -87, -9, 36, + 1, 59, -85, 124, -60, -143, -307, -231, -406, -296, -456, + -311, -297, -269, -433, -350, -189, -283, -277, -303, -343, -325, + -287, -213, -328, -275, -279, -293, -296, -268, -217, -284, -286, + -294, -277, -236, -247, -201, -305, -195, -224, -135, -244, -185, + -128, -225, -126, -186, -74, -235, -93, -209, -95, -133, -90, + -131, -165, -80, -89, -69, -148, -104, -72, -157, -77, -134, + -88, -171, -67, -113, -124, -110, -76, -110, -130, -59, -96, + -88, -119, -61, -109, -81, -81, -67, -82, -54, -22, -49, + -19, -18, 2, -15, 25, 13, 24, 8, 45, 57, 4, + 20, 23, 60}}; + +const int16_t irc_composite_c_r0195_p045[][256] = + {/* IRC_Composite_C_R0195_T000_P045.wav */ + {-30, 30, -31, 32, -33, 34, -35, 36, -37, 38, -39, + 40, -41, 42, -43, 43, -44, 44, -42, 38, -28, -9, + -568, -169, -659, -127, -629, -131, -879, -529, -702, -207, -686, + -146, 757, 1500, 46, 1169, -4599, -1790, 163, -4363, 15423, 4884, + 3940, 951, 722, -172, 1158, 1933, -227, -750, 303, -613, 1137, + 856, 493, 193, 1077, 347, 1002, 300, 420, 882, 521, 444, + 512, 395, 442, 484, 453, 404, 610, 620, 839, 826, 758, + 649, 736, 767, 608, 484, 493, 147, 169, 427, 10, 204, + -311, 19, -245, 98, -241, 0, -167, 16, -127, -26, -105, + -158, -191, -79, -246, -206, -268, -177, -349, -216, -328, -216, + -366, -178, -332, -214, -308, -168, -352, -204, -312, -132, -301, + -215, -309, -152, -304, -202, -336, -200, -308, -229, -340, -205, + -331, -196, -326, -240, -274, -198, -310, -200, -321, -159, -282, + -190, -264, -176, -207, -74, -246, -157, -167, -77, -245, 18, + -155, -133, -274, -40, -289, -273, -267, -139, -520, -214, -265, + -252, -317, -46, -133, -17, 38, 121, -153, 151, 27, -13, + -114, 13, -297, -74, -188, -147, -332, -197, -268, -297, -199, + -221, -310, -309, -263, -206, -258, -237, -414, -155, -291, -200, + -367, -189, -287, -139, -241, -261, -247, -142, -190, -233, -239, + -158, -249, -110, -242, -173, -207, -82, -217, -136, -180, -136, + -164, -112, -177, -97, -134, -40, -164, -32, -115, -16, -91, + -12, -112, 4, -81, -19, -100, -20, -70, -21, -87, -6, + -59, 22, -68, -7, -66, 36, -20, 13, -47, -2, -27, + 28, -4, 13}, + /* IRC_Composite_C_R0195_T015_P045.wav */ + {16, -14, 12, -9, 7, -4, 1, 3, -7, 12, -17, + 22, -29, 36, -45, 56, -69, 85, -107, 143, -247, -403, + -616, -230, -517, -270, -663, -670, -583, -556, -570, -300, 850, + 1495, 298, 1490, -1579, -5393, 1137, -9580, 17573, 8715, 3526, 4288, + -490, -1083, 280, 3534, -555, -1021, -341, -114, 804, 1521, 139, + 355, 844, 673, 325, 735, -35, 1245, 91, 909, -18, 912, + 280, 465, 82, 792, 554, 703, 257, 849, 594, 952, 381, + 794, 169, 438, 96, 219, 18, 183, -220, -11, -303, 284, + -345, -44, -429, -44, -355, -58, -224, -155, -342, -30, -264, + -140, -360, -95, -355, -133, -396, -161, -354, -174, -312, -65, + -314, -194, -357, -142, -289, -170, -390, -213, -337, -152, -369, + -215, -364, -191, -332, -194, -330, -206, -298, -234, -318, -198, + -344, -225, -368, -198, -333, -240, -308, -215, -230, -223, -291, + -224, -258, -133, -220, -212, -260, -84, -207, -164, -294, -157, + -248, -263, -285, -134, -218, -267, -295, -223, -237, -192, -163, + -155, -68, 86, -74, -67, -51, -46, -49, -45, -144, -181, + -126, -51, -106, -136, -213, -189, -163, -189, -238, -230, -238, + -276, -288, -300, -274, -264, -268, -228, -288, -202, -201, -180, + -232, -217, -238, -174, -222, -220, -242, -187, -203, -190, -211, + -143, -223, -148, -187, -110, -202, -179, -175, -124, -140, -207, + -165, -148, -117, -138, -129, -91, -111, -52, -96, -31, -72, + -57, -38, -41, -39, -63, -12, -65, -2, -67, 12, -64, + 37, -48, 34, -49, 20, -4, 26, -16, 4, -9, 34, + -12, 34, -23}, + /* IRC_Composite_C_R0195_T030_P045.wav */ + {53, -54, 56, -58, 59, -62, 64, -67, 70, -73, 77, + -81, 87, -93, 100, -110, 122, -140, 176, -592, -266, -620, + -93, -807, 111, -1033, -244, -1451, 661, -1378, 1038, 309, 3279, + -1940, 4376, -7187, -1452, -4658, 38, 22380, 4913, 3629, 1106, -370, + -1337, 2958, 848, -390, -2691, 1295, -1155, 2599, -36, 956, 635, + 787, 53, 821, 200, 581, 67, 983, 274, 565, 247, 646, + 211, 751, 653, 273, 546, 759, 707, 776, 230, 704, 98, + 258, -440, 41, -44, -99, -570, -276, 125, -89, -316, -287, + -67, -256, -409, -175, -243, -300, -369, -371, -102, -291, -350, + -382, -109, -199, -343, -360, -210, -168, -316, -283, -194, -249, + -97, -252, -166, -374, -161, -245, -317, -297, -364, -202, -337, + -267, -324, -305, -165, -347, -151, -302, -171, -371, -213, -207, + -334, -383, -313, -191, -297, -398, -235, -273, -140, -391, -214, + -292, -119, -316, -254, -221, -194, -227, -232, -269, -229, -362, + -187, -364, -206, -335, -144, -247, -146, -237, -158, -174, -97, + -117, 50, -80, 66, -91, 178, -80, -68, -202, -96, -198, + -132, -223, -177, -186, -153, -217, -140, -249, -182, -298, -91, + -285, -230, -334, -153, -250, -218, -295, -154, -260, -140, -294, + -141, -233, -87, -273, -133, -222, -68, -214, -169, -225, -107, + -209, -114, -264, -92, -242, -49, -295, -99, -226, -66, -252, + -85, -193, -65, -189, -57, -171, -14, -172, -4, -179, 85, + -157, 3, -134, 92, -113, -4, -109, 69, -51, 13, -79, + 68, -27, 61, -58, 66, -43, 79, -23, 54, -24, 30, + -27, 66, -41}, + /* IRC_Composite_C_R0195_T045_P045.wav */ + {-71, 74, -76, 79, -82, 84, -87, 90, -93, 96, -99, 101, + -102, 102, -97, 85, -52, -97, -607, -227, -812, 438, -1024, -204, + -760, 95, -1636, 140, -766, 2279, -763, 3056, 150, 2120, -5760, -6480, + 1711, -14, 24602, 5276, 1036, -813, 1031, 1166, 848, 674, -478, -1487, + -945, 687, 1855, -292, 799, 1002, 786, 207, 559, 505, 363, 364, + 343, 530, 502, 11, 500, 785, 837, 217, 463, 907, 599, 564, + 357, 380, -197, 84, -367, 169, -530, -810, -282, -180, -171, -519, + -179, -442, -72, -160, -239, -494, -294, -88, -553, -268, -512, -278, + -503, -198, -473, -229, -317, -213, -370, -187, -331, -249, -344, -154, + -381, -135, -317, -147, -279, -176, -355, -133, -274, -305, -330, -235, + -314, -312, -303, -290, -257, -159, -269, -234, -243, -173, -350, -204, + -383, -239, -309, -233, -404, -183, -289, -321, -344, -212, -319, -251, + -253, -168, -303, -204, -277, -189, -284, -216, -333, -204, -348, -208, + -298, -166, -339, -189, -186, -273, -152, -87, -184, -166, 10, 48, + -40, 2, 0, -18, -97, -3, -134, -124, -166, -216, -161, -149, + -234, -162, -175, -239, -198, -150, -294, -241, -224, -190, -303, -189, + -150, -236, -232, -162, -159, -199, -136, -231, -166, -169, -136, -155, + -152, -125, -146, -133, -100, -123, -156, -121, -135, -145, -164, -174, + -157, -151, -189, -118, -198, -112, -121, -108, -138, -101, -93, -63, + -79, -82, -83, -26, -44, -58, -96, 20, -91, -15, -43, 22, + -69, 29, 5, 6, -19, 24, 3, 1, 25, 26, 26, 17, + 31, 13, 22, 22}, + /* IRC_Composite_C_R0195_T060_P045.wav */ + {59, -58, 56, -55, 52, -49, 45, -40, 34, -25, 14, + 1, -23, 55, -108, 214, -619, -300, -123, -537, -172, -393, + -415, -586, -104, -1306, 69, -335, 1375, 535, 2761, -686, 3824, + -8072, -1237, -7735, 11382, 19138, 4865, 2658, -2047, 1916, 571, 2806, + -624, -694, -1625, 70, 232, 950, -64, 1404, -414, 1196, 34, + 1403, -261, 1089, 343, 834, -188, 587, -251, 1086, 518, 827, + 135, 641, 494, 842, -31, 161, -360, -291, -317, 171, -783, + -241, -1031, 130, -617, -201, -704, -152, -418, -199, -454, -247, + -285, -342, -410, -378, -429, -306, -467, -327, -419, -184, -471, + -188, -474, -157, -446, -174, -469, -146, -371, -239, -326, -267, + -268, -265, -279, -218, -230, -216, -236, -148, -289, -212, -358, + -216, -330, -124, -259, -173, -298, -166, -287, -241, -319, -291, + -323, -261, -320, -293, -286, -298, -318, -332, -305, -269, -323, + -212, -266, -206, -301, -143, -304, -236, -268, -212, -344, -239, + -265, -225, -364, -157, -292, -187, -206, -129, -241, -186, -152, + -141, -106, -103, -15, -115, 56, -68, 26, -47, -92, -125, + -141, -154, -176, -196, -181, -250, -197, -136, -112, -240, -89, + -180, -145, -235, -146, -223, -248, -178, -229, -152, -219, -112, + -250, -98, -141, -152, -155, -116, -137, -150, -110, -137, -128, + -94, -113, -72, -189, -29, -176, -93, -125, -129, -123, -190, + -65, -169, -92, -169, -68, -159, -48, -109, -46, -109, -25, + -81, -31, -72, -27, -71, 7, -65, -16, -30, -30, -44, + 30, -39, 8, -56, 53, -36, 81, -51, 97, -64, 95, + -35, 60, -30}, + /* IRC_Composite_C_R0195_T075_P045.wav */ + {7, -7, 6, -6, 5, -5, 4, -3, 2, 0, -2, + 5, -9, 14, -23, 45, -729, -86, -463, -18, -765, 398, + -1336, 518, -1262, -130, -544, 1878, -781, 3861, -694, 2694, -2479, + -6040, -3168, 1816, 21907, 5475, 5383, -2274, 1082, 1645, 3196, -180, + -862, -1506, -288, -182, 870, -91, 1493, -388, 779, 238, 870, + 64, 614, 1007, 850, 122, 95, 413, 1397, 214, 480, 297, + 844, -36, 272, 27, -433, -791, -467, 60, -441, -446, -697, + -441, -417, -402, -239, -587, -268, -547, -343, -464, -296, -435, + -388, -347, -416, -430, -420, -403, -296, -406, -376, -408, -228, + -422, -330, -353, -273, -416, -325, -234, -301, -372, -327, -219, + -336, -296, -307, -215, -240, -202, -250, -161, -199, -207, -315, + -154, -247, -142, -267, -158, -266, -221, -313, -201, -303, -302, + -339, -264, -278, -318, -314, -301, -319, -312, -315, -204, -334, + -223, -340, -127, -300, -165, -343, -198, -307, -246, -285, -207, + -293, -254, -293, -183, -246, -202, -252, -100, -213, -78, -138, + -60, -197, -164, -160, -109, -141, -50, -142, 2, -155, 50, + -176, 0, -217, -67, -265, -39, -241, -180, -198, -196, -186, + -182, -111, -135, -134, -125, -176, -98, -184, -100, -212, -114, + -228, -113, -95, -140, -143, -141, -78, -133, -100, -67, -178, + -75, -161, -2, -319, 35, -236, -7, -269, 53, -223, -47, + -143, -20, -108, -80, -80, -54, -96, -67, -96, -24, -164, + -15, -108, -24, -125, -8, -47, -26, -57, 4, -8, -39, + -35, 32, -69, 30, -49, 58, -82, 89, -80, 107, -80, + 104, -56, 112}, + /* IRC_Composite_C_R0195_T090_P045.wav */ + {82, -84, 86, -88, 90, -91, 93, -94, 95, -95, 94, + -92, 87, -78, 61, -33, -240, -426, 56, -683, 113, -744, + 373, -1306, 284, -1194, 1644, -667, 2652, -291, 4245, -2423, -2014, + -4070, -4168, 13696, 10251, 10475, -3031, 1940, 668, 3850, 1809, -625, + -1492, -994, -456, 184, 563, 568, -96, 1027, 330, 539, 32, + 648, 1068, 571, 102, 392, 437, 1555, 535, 636, 312, 223, + -470, 382, -426, -676, -1277, -176, -342, -251, -706, -452, -477, + -459, -100, -511, -346, -481, -454, -509, -470, -397, -448, -337, + -587, -233, -611, -143, -563, -280, -521, -364, -455, -244, -466, + -275, -326, -321, -388, -219, -441, -210, -415, -297, -415, -244, + -390, -213, -373, -184, -325, -193, -152, -257, -243, -230, -170, + -254, -69, -214, -166, -228, -242, -227, -305, -281, -292, -232, + -343, -218, -305, -280, -254, -331, -243, -363, -249, -287, -245, + -320, -255, -262, -272, -215, -275, -191, -269, -200, -292, -200, + -287, -217, -267, -214, -217, -218, -189, -189, -148, -203, -120, + -154, -118, -165, -102, -132, -161, -134, -144, -132, -168, -94, + -134, -87, -100, -96, -67, -104, -58, -178, -114, -154, -134, + -226, -145, -224, -215, -130, -174, -117, -174, -71, -88, -63, + -109, -58, -78, -120, -50, -84, -79, -101, -63, -97, -110, + -65, -129, -123, -160, -83, -162, -109, -152, -92, -143, -76, + -108, -73, -110, -42, -74, -64, -99, -35, -69, -11, -79, + -57, -78, -51, -41, -69, -60, -68, -23, -64, 48, -114, + 27, -96, 75, -109, 79, -84, 99, -121, 109, -64, 69, + -52, 115, -98}, + /* IRC_Composite_C_R0195_T105_P045.wav */ + {28, -30, 31, -33, 35, -38, 40, -43, 47, -51, 55, + -60, 67, -75, 85, -99, 111, -72, -376, -88, -540, 307, + -524, -278, -931, 669, 115, 481, 924, 2601, 65, 2397, -5614, + -1324, -2647, 10799, 8614, 10099, 118, -2294, 4895, 2386, 2569, -875, + -924, -2723, 797, 264, 147, -195, 968, 521, 465, -376, 1032, + 858, 1253, -135, 495, 389, 792, 755, 975, 0, 305, -239, + -39, -629, -469, -1320, -587, -582, -54, -751, -376, -721, -115, + -425, -260, -335, -443, -566, -430, -612, -353, -563, -240, -666, + -197, -569, -261, -523, -199, -719, -213, -574, -181, -663, -112, + -511, -96, -492, -191, -414, -208, -419, -314, -403, -269, -455, + -207, -442, -146, -453, -78, -424, -85, -318, -146, -327, -204, + -254, -94, -214, -119, -280, -105, -318, -172, -355, -253, -350, + -232, -337, -213, -346, -174, -337, -196, -365, -187, -307, -214, + -347, -231, -304, -233, -293, -202, -273, -150, -270, -125, -278, + -116, -278, -165, -297, -205, -241, -198, -221, -169, -202, -83, + -240, -100, -225, -103, -190, -146, -171, -133, -169, -147, -152, + -62, -157, -83, -150, -63, -116, -82, -115, -86, -88, -91, + -126, -129, -161, -111, -196, -127, -178, -79, -197, -117, -103, + -73, -136, -26, -65, -11, -114, 16, -69, 41, -165, 27, + -83, -32, -83, -83, -89, -127, -138, -85, -115, -105, -151, + -76, -90, -77, -130, -45, -81, -85, -105, -60, -35, -80, + -62, -86, -27, -81, -18, -56, -46, -45, -2, 5, -61, + -31, -63, -14, -64, -36, 8, -66, 36, -43, 73, -62, + 61, -61, 80}, + /* IRC_Composite_C_R0195_T120_P045.wav */ + {-12, 12, -13, 13, -14, 14, -15, 16, -17, 18, -20, + 22, -25, 29, -34, 41, -54, 124, -315, 231, -915, 409, + -381, 76, -1011, 336, -106, 1161, -312, 2462, 834, 1649, -538, + -4939, -381, 2968, 10089, 6155, 8410, -2705, 920, 4167, 3787, 745, + -2352, -1802, -28, 292, -651, 645, 868, -434, 709, 424, 388, + 1070, 1285, -64, 322, 849, 520, 234, 673, 260, -252, -169, + -219, -404, -898, -522, -1000, -760, -659, -37, -489, -625, -480, + -117, -279, -318, -554, -444, -479, -476, -447, -397, -506, -426, + -374, -367, -517, -341, -428, -392, -509, -321, -429, -316, -424, + -291, -372, -230, -336, -266, -337, -314, -357, -245, -417, -326, + -393, -223, -400, -276, -346, -207, -347, -196, -289, -214, -324, + -195, -253, -149, -240, -179, -253, -165, -279, -187, -327, -224, + -367, -235, -338, -247, -316, -185, -311, -218, -272, -162, -301, + -187, -304, -196, -322, -148, -322, -157, -312, -104, -274, -142, + -240, -141, -246, -193, -235, -171, -192, -186, -214, -189, -212, + -181, -191, -123, -229, -156, -208, -159, -193, -117, -178, -146, + -179, -127, -164, -97, -166, -64, -139, -51, -149, -26, -150, + -13, -111, -43, -117, -85, -121, -102, -128, -113, -107, -143, + -80, -75, -82, -86, -55, -52, -89, -61, -20, -13, -63, + 41, -66, -14, -65, -25, -99, -66, -90, -83, -119, -73, + -60, -96, -94, -16, -118, -43, -110, -43, -94, -27, -153, + -76, -126, -21, -94, -45, -92, -51, -33, 3, -63, -11, + -52, -9, -73, 9, -41, -20, -40, 3, -44, -34, 0, + 0, -32, 16}, + /* IRC_Composite_C_R0195_T135_P045.wav */ + {15, -16, 17, -18, 19, -20, 21, -22, 23, -24, 25, + -27, 28, -29, 29, -28, 24, -11, -117, -145, -90, -121, + -109, -173, -39, -435, 83, -128, 1107, 344, 1609, 548, 3339, + -3005, -2346, -79, 2825, 6596, 9304, 4876, -1449, 2789, 1884, 4671, + 84, -2193, -1608, -211, -70, 580, 333, 233, 35, 298, -56, + 1401, 612, 725, 299, 829, 803, 425, -150, 290, -261, -139, + -438, -491, -740, -322, -692, -659, -1013, -400, -489, -130, -431, + -461, -332, -161, -302, -454, -443, -489, -403, -443, -472, -442, + -454, -357, -427, -305, -456, -382, -411, -333, -418, -365, -303, + -376, -334, -316, -292, -311, -265, -339, -352, -281, -323, -329, + -379, -310, -346, -316, -320, -334, -291, -274, -280, -297, -290, + -272, -270, -230, -233, -229, -239, -263, -224, -254, -270, -293, + -279, -264, -289, -263, -287, -260, -261, -232, -231, -207, -225, + -198, -204, -183, -223, -189, -221, -183, -247, -188, -250, -189, + -248, -190, -209, -183, -176, -163, -144, -170, -178, -173, -185, + -158, -195, -164, -234, -222, -222, -206, -165, -198, -160, -175, + -170, -167, -137, -130, -154, -126, -141, -104, -142, -91, -116, + -80, -71, -71, -88, -42, -93, -59, -74, -48, -70, -66, + -85, -79, -76, -90, -60, -94, -51, -65, -11, -46, -25, + -16, -13, -19, -24, -51, -74, -67, -91, -21, -96, -77, + -66, 2, -73, -36, -56, -44, -134, -42, -35, -70, -118, + -47, -68, -116, -72, -82, -75, -136, -24, -85, -36, -90, + 1, -62, -4, -20, -33, -14, 1, -17, -33, -28, -35, + -37, -9, -16}, + /* IRC_Composite_C_R0195_T150_P045.wav */ + {13, -14, 16, -17, 19, -20, 22, -25, 27, -30, 34, -38, + 42, -48, 55, -63, 75, -91, 119, -208, 14, -179, 43, -143, + 14, -237, 127, -445, 347, 318, 882, 1092, 671, 2337, -2, -2406, + -2033, 3403, 2686, 9080, 6065, 1654, -385, 2378, 3537, 1656, -964, -2514, + -65, -61, 604, 313, -548, 8, 506, 751, 592, 750, 799, 256, + 788, 738, 691, -486, -503, -169, -52, -560, -771, -513, -275, -418, + -623, -527, -620, -550, -389, -73, -175, -357, -452, -335, -134, -523, + -381, -574, -280, -554, -346, -501, -295, -395, -265, -376, -302, -406, + -287, -380, -314, -400, -273, -369, -283, -407, -233, -368, -270, -408, + -280, -353, -282, -379, -324, -362, -292, -349, -288, -317, -293, -299, + -273, -329, -303, -342, -223, -328, -247, -359, -217, -332, -232, -362, + -243, -293, -224, -266, -223, -255, -188, -248, -204, -263, -170, -234, + -148, -228, -150, -221, -123, -200, -152, -223, -169, -222, -177, -244, + -174, -210, -124, -206, -129, -206, -127, -177, -96, -151, -166, -237, + -192, -200, -169, -204, -186, -233, -213, -217, -169, -178, -153, -156, + -130, -156, -129, -136, -88, -131, -105, -128, -101, -129, -90, -83, + -84, -106, -52, -43, -49, -65, -44, -66, -38, -70, 26, -76, + 6, -88, -13, -82, -34, -41, -4, -34, -1, -14, 9, -50, + -23, -69, -16, -101, -10, -110, -17, -119, -37, -74, -19, -102, + -24, -41, -56, -111, -39, -111, -124, -147, -53, -65, -109, -46, + -41, -68, -52, -7, -29, -64, -43, -18, -36, -49, -47, 1, + -26, -14, -36, -17}, + /* IRC_Composite_C_R0195_T165_P045.wav */ + {-5, 6, -6, 7, -7, 8, -9, 9, -10, 11, -13, 14, + -16, 18, -20, 23, -27, 31, -37, 45, -57, 83, -131, 98, + -140, 102, -154, 88, -172, 82, 24, 745, 704, 1159, 942, 1377, + 685, -3350, -75, 2310, 5071, 6483, 6504, -342, 276, 2310, 3436, 1307, + -1968, -1879, -504, 1149, 580, -184, -486, 250, 728, 683, 585, 651, + 265, 901, 267, 694, -240, -105, -607, -269, -676, -500, -479, -378, + -278, -303, -345, -367, -327, -471, -422, -217, -111, -233, -480, -413, + -399, -332, -479, -354, -471, -387, -344, -260, -374, -267, -230, -298, + -347, -353, -301, -344, -328, -357, -343, -372, -361, -346, -368, -366, + -338, -358, -344, -365, -318, -353, -318, -336, -351, -319, -335, -289, + -334, -309, -321, -326, -336, -348, -335, -354, -321, -327, -304, -329, + -269, -270, -235, -245, -249, -208, -236, -185, -237, -214, -241, -196, + -206, -169, -200, -166, -161, -150, -185, -187, -205, -193, -210, -166, + -170, -138, -171, -155, -201, -170, -152, -109, -129, -189, -178, -185, + -130, -167, -150, -206, -233, -222, -196, -166, -206, -190, -201, -159, + -173, -138, -175, -147, -160, -114, -124, -105, -119, -77, -100, -117, + -136, -90, -111, -91, -103, -87, -79, -69, -28, -22, -44, -41, + -25, -6, -27, -13, -21, -10, -27, 9, 1, -13, 14, -2, + 20, 6, -17, -7, -23, -45, -82, -88, -89, -86, -91, -67, + -101, -82, -96, -74, -71, -81, -64, -98, -60, -94, -66, -105, + -103, -119, -65, -33, -28, -21, -24, -24, -66, -22, -25, -29, + -63, -45, -61, -77}, + /* IRC_Composite_C_R0195_T180_P045.wav */ + {1, -1, 1, -1, 2, -2, 2, -2, 2, -2, 2, -2, + 3, -3, 3, -3, 3, -4, 4, -4, 4, -4, 3, -2, + 9, -9, 70, -59, 121, -126, 127, -45, 222, 619, 653, 1289, + 587, 1432, 300, -1888, -1240, 3381, 3947, 6141, 5035, 479, -148, 2113, + 2998, 773, -1353, -1664, 690, 1041, -58, -664, -159, 711, 746, 445, + 284, 522, 787, 289, 291, 216, 96, -622, -296, -616, -684, -562, + -153, -193, -156, -160, -230, -111, -119, -291, -584, -298, -144, -240, + -398, -579, -379, -509, -208, -402, -311, -292, -227, -267, -259, -266, + -213, -263, -327, -375, -294, -371, -362, -434, -364, -436, -381, -431, + -375, -404, -373, -398, -329, -365, -326, -409, -345, -385, -333, -367, + -288, -357, -297, -388, -309, -398, -314, -430, -330, -408, -271, -349, + -243, -310, -230, -234, -197, -214, -201, -187, -203, -238, -232, -238, + -212, -225, -205, -211, -176, -179, -201, -211, -228, -202, -224, -150, + -202, -129, -214, -123, -184, -94, -133, -120, -193, -166, -163, -113, + -144, -135, -197, -168, -225, -183, -213, -177, -199, -157, -207, -155, + -195, -92, -173, -121, -200, -130, -155, -103, -148, -111, -166, -85, + -143, -56, -147, -56, -154, -59, -141, -36, -120, -42, -95, -13, + -67, 16, -28, 44, -44, 24, -33, 43, -27, 82, -21, 103, + -17, 122, -2, 81, -69, 27, -35, -19, -94, -112, -111, -109, + -91, -94, -119, -134, -85, -80, -83, -129, -79, -97, -58, -112, + -41, -119, -24, -104, -33, -99, -41, -79, -41, -68, -44, -36, + -44, -71, -84, -70}, + /* IRC_Composite_C_R0195_T195_P045.wav */ + {-10, 11, -11, 11, -12, 12, -12, 13, -13, 14, -14, 15, + -15, 16, -17, 17, -18, 19, -20, 21, -22, 23, -24, 26, + -28, 29, -70, 1, 21, -50, -12, 7, -98, 118, -221, 432, + 295, 945, 530, 962, 1220, -497, -1827, 293, 2669, 3717, 6133, 2421, + 667, 647, 2517, 2286, 876, -1019, -811, 715, 567, 146, -552, 68, + 761, 748, 406, 192, 794, 584, 428, 293, 114, -104, -113, -153, + -376, -408, -234, -64, -66, -15, 75, -43, -32, -84, -58, -289, + -255, -381, -227, -301, -267, -362, -351, -257, -250, -265, -197, -240, + -267, -205, -195, -288, -234, -313, -331, -391, -314, -376, -403, -414, + -376, -388, -386, -415, -381, -386, -367, -392, -359, -382, -358, -395, + -338, -337, -339, -329, -314, -348, -381, -383, -383, -385, -396, -353, + -328, -277, -291, -244, -249, -220, -237, -207, -212, -209, -222, -227, + -219, -186, -196, -201, -196, -190, -194, -210, -219, -243, -209, -201, + -163, -195, -186, -186, -145, -123, -130, -154, -175, -167, -170, -122, + -126, -124, -185, -179, -196, -160, -179, -161, -199, -183, -176, -134, + -131, -147, -153, -166, -151, -161, -167, -166, -166, -160, -162, -139, + -166, -135, -153, -143, -142, -125, -105, -102, -94, -90, -78, -72, + -57, -50, -51, -21, -44, -9, -31, -16, -46, -3, -13, 24, + 10, 27, 32, 25, 66, -13, -17, -41, -15, -51, -100, -128, + -126, -123, -107, -116, -127, -132, -96, -99, -88, -127, -87, -121, + -70, -130, -87, -96, -70, -95, -94, -64, -80, -73, -64, -54, + -55, -52, -44, -57}, + /* IRC_Composite_C_R0195_T210_P045.wav */ + {-1, 1, -1, 1, -1, 1, -1, 1, -2, 2, -2, 2, + -2, 2, -2, 3, -3, 3, -3, 4, -4, 4, -5, 5, + -6, 6, -7, 9, -41, -44, -65, -30, -65, -8, -83, -60, + -92, 30, 110, 507, 445, 762, 568, 1174, -1167, -1019, 628, 2221, + 3797, 4340, 2039, 369, 1655, 2128, 2575, -74, -532, -44, 828, 327, + -261, -60, 394, 940, 522, 463, 465, 701, 615, 239, 186, 177, + 226, 24, 55, -107, -121, -144, 122, 80, 101, 60, 168, 75, + 69, -42, -103, -208, -202, -275, -257, -213, -175, -274, -214, -201, + -239, -203, -186, -264, -269, -249, -233, -294, -292, -329, -314, -341, + -364, -402, -341, -398, -395, -448, -334, -429, -332, -431, -329, -423, + -331, -395, -337, -340, -332, -347, -337, -368, -357, -389, -353, -376, + -312, -331, -265, -307, -238, -251, -202, -250, -204, -228, -195, -243, + -216, -235, -186, -234, -184, -216, -198, -218, -214, -243, -226, -238, + -184, -210, -192, -215, -154, -146, -144, -168, -181, -177, -171, -140, + -114, -169, -137, -193, -142, -178, -151, -186, -152, -183, -144, -144, + -116, -143, -141, -149, -130, -174, -129, -185, -122, -193, -143, -180, + -134, -169, -148, -166, -136, -141, -122, -130, -103, -114, -106, -102, + -72, -91, -60, -67, -53, -70, -51, -40, -61, -20, -57, 16, + -56, 22, -58, 21, -49, -10, -48, 3, -48, -36, -55, -46, + -67, -83, -96, -101, -127, -82, -139, -122, -145, -101, -121, -109, + -139, -130, -124, -118, -116, -120, -95, -97, -71, -73, -66, -58, + -48, -24, -31, -11}, + /* IRC_Composite_C_R0195_T225_P045.wav */ + {-3, 3, -3, 3, -3, 3, -3, 4, -4, 4, -4, 4, + -4, 4, -4, 5, -5, 5, -5, 5, -6, 6, -6, 7, + -7, 8, -9, 10, -13, 20, -75, -97, -66, -47, -85, -74, + -101, -34, -123, -132, -22, 222, 292, 490, 466, 903, 275, -1049, + -389, 584, 2358, 3669, 3152, 1258, 1261, 1738, 2331, 1624, 37, -28, + 186, 674, 40, 219, 313, 646, 713, 656, 528, 603, 598, 473, + 348, 444, 144, 373, 151, 223, 11, 95, 112, 186, 158, 153, + 282, 115, 221, 30, 18, -91, -32, -200, -106, -202, -164, -212, + -141, -234, -274, -248, -205, -234, -252, -279, -220, -289, -259, -337, + -286, -358, -337, -398, -361, -383, -377, -397, -403, -387, -416, -383, + -391, -375, -383, -384, -351, -350, -337, -371, -325, -339, -314, -362, + -307, -327, -273, -302, -260, -293, -265, -270, -253, -272, -249, -242, + -222, -243, -203, -221, -193, -215, -188, -249, -219, -244, -222, -251, + -227, -225, -219, -214, -206, -186, -149, -158, -170, -204, -178, -182, + -146, -152, -156, -169, -175, -163, -157, -142, -159, -167, -156, -155, + -165, -170, -148, -148, -131, -118, -114, -117, -133, -119, -145, -148, + -151, -147, -145, -167, -157, -166, -139, -171, -158, -145, -135, -121, + -133, -97, -119, -63, -84, -67, -101, -69, -67, -57, -75, -59, + -62, -49, -63, -41, -77, -33, -68, -14, -83, -22, -64, -11, + -77, -48, -83, -73, -129, -98, -117, -128, -152, -116, -144, -123, + -154, -107, -170, -138, -138, -97, -115, -107, -82, -89, -65, -72, + -83, -58, -72, -30}, + /* IRC_Composite_C_R0195_T240_P045.wav */ + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, + -1, 1, -1, 1, -1, 2, -5, -83, -84, -72, -135, -73, + -91, -109, -87, -133, -84, -173, -91, -36, 199, 307, 354, 685, + 469, -284, -594, -516, 1131, 3046, 2851, 2215, 1571, 1578, 1884, 1712, + 836, 241, 420, 708, 320, 266, 499, 693, 588, 631, 732, 645, + 553, 384, 505, 486, 415, 335, 428, 309, 250, 327, 166, 248, + 139, 364, 147, 249, 184, 201, 90, 89, -4, -84, -57, -140, + -149, -268, -170, -229, -181, -241, -235, -260, -225, -272, -259, -355, + -270, -344, -221, -429, -254, -394, -305, -443, -348, -406, -390, -405, + -383, -420, -382, -385, -345, -366, -308, -315, -295, -317, -301, -307, + -307, -319, -316, -337, -273, -310, -260, -332, -260, -287, -238, -267, + -235, -236, -259, -223, -218, -231, -226, -221, -205, -245, -216, -236, + -203, -244, -208, -224, -221, -211, -210, -180, -186, -192, -188, -204, + -183, -150, -161, -144, -182, -151, -199, -152, -176, -159, -170, -168, + -142, -147, -118, -110, -117, -97, -125, -84, -116, -88, -125, -124, + -122, -160, -117, -177, -157, -199, -179, -171, -156, -148, -174, -137, + -139, -117, -130, -135, -112, -124, -104, -131, -80, -123, -82, -99, + -80, -101, -46, -83, -75, -90, -80, -63, -59, -51, -122, -65, + -82, -66, -92, -66, -83, -92, -88, -117, -122, -158, -126, -150, + -152, -142, -139, -83, -139, -101, -146, -84, -121, -95, -110, -91, + -88, -79, -72, -47}, + /* IRC_Composite_C_R0195_T255_P045.wav */ + {-1, 1, -1, 1, -1, 1, -1, 1, -2, 2, -2, 2, + -2, 2, -2, 2, -3, 3, -3, 3, -4, 4, -4, 5, + -5, 6, -6, 7, -8, 10, -12, 15, -21, 35, -138, -127, + -91, -119, -131, -100, -136, -154, -123, -162, -225, -17, 208, 157, + 422, 539, 315, -186, -874, -513, 1317, 2834, 3212, 2035, 1165, 1380, + 1985, 1825, 760, 470, 698, 708, 299, 840, 390, 551, 566, 771, + 555, 435, 745, 529, 606, 365, 682, 399, 459, 406, 443, 291, + 358, 302, 320, 305, 286, 249, 233, 146, 69, 90, 0, -26, + -85, -106, -54, -197, -179, -210, -226, -306, -252, -328, -293, -273, + -273, -342, -265, -302, -315, -353, -309, -387, -325, -457, -297, -442, + -355, -408, -318, -407, -303, -346, -296, -308, -269, -329, -219, -337, + -263, -338, -282, -364, -295, -326, -281, -306, -274, -302, -265, -294, + -241, -286, -262, -250, -230, -264, -201, -225, -215, -219, -198, -213, + -212, -205, -209, -201, -251, -202, -242, -212, -196, -186, -200, -172, + -211, -195, -206, -149, -217, -164, -212, -147, -189, -117, -143, -88, + -129, -55, -121, -125, -117, -90, -114, -92, -138, -75, -136, -123, + -181, -119, -197, -132, -134, -131, -172, -162, -136, -187, -159, -156, + -181, -172, -159, -163, -167, -116, -199, -172, -135, -102, -126, -131, + -78, -101, -129, -76, -94, -120, -143, -86, -127, -74, -98, -46, + -113, -79, -97, -44, -125, -131, -113, -86, -115, -124, -120, -129, + -130, -91, -134, -118, -138, -78, -131, -110, -118, -103, -103, -97, + -94, -71, -62, -45}, + /* IRC_Composite_C_R0195_T270_P045.wav */ + {-2, 2, -2, 2, -2, 2, -2, 2, -2, 2, -2, 2, + -2, 2, -2, 2, -2, 2, -2, 2, -2, 2, -2, 2, + -2, 2, -2, 3, -3, 4, -4, 6, -9, 16, -64, -161, + -143, -126, -145, -185, -145, -161, -193, -254, -120, -230, -52, 146, + 316, 329, 329, 253, -502, -967, -275, 2022, 3347, 2710, 947, 1504, + 1884, 1375, 1901, 809, 618, 609, 671, 737, 376, 610, 640, 652, + 766, 573, 669, 526, 783, 441, 547, 504, 550, 598, 531, 667, + 314, 551, 275, 554, 115, 507, 96, 221, 103, 170, 93, 44, + 90, -71, 5, -109, -137, -246, -250, -241, -295, -289, -315, -295, + -325, -258, -316, -295, -327, -300, -348, -332, -372, -354, -355, -374, + -324, -355, -324, -341, -316, -283, -312, -268, -304, -257, -336, -239, + -313, -303, -322, -321, -295, -351, -234, -324, -242, -342, -262, -303, + -265, -288, -272, -254, -289, -193, -250, -195, -234, -188, -219, -212, + -194, -217, -186, -231, -179, -233, -211, -209, -215, -227, -234, -228, + -223, -193, -202, -196, -175, -156, -144, -145, -108, -127, -113, -111, + -50, -105, -58, -119, -95, -117, -140, -166, -165, -130, -150, -114, + -120, -117, -146, -129, -172, -175, -166, -189, -168, -160, -197, -193, + -208, -220, -186, -186, -158, -220, -160, -145, -123, -177, -155, -188, + -132, -141, -109, -129, -71, -127, -88, -107, -92, -141, -124, -132, + -136, -104, -99, -93, -109, -90, -81, -97, -101, -102, -111, -126, + -107, -115, -121, -123, -88, -123, -117, -104, -75, -79, -90, -92, + -70, -77, -68, -59}, + /* IRC_Composite_C_R0195_T285_P045.wav */ + {5, -5, 5, -5, 5, -5, 5, -5, 5, -5, 5, -5, + 5, -5, 5, -5, 5, -5, 5, -5, 5, -5, 5, -5, + 5, -5, 6, -6, 6, -7, 8, -12, 29, 101, 50, 50, + 117, 188, 58, 104, 258, 239, 166, 306, 686, 455, 833, 834, + 700, 494, 424, 49, 743, 2627, 3265, 3156, 1306, -112, 1114, 559, + 356, 246, 113, -51, -113, 695, 8, 344, 126, 384, -46, 304, + 134, 58, 125, -42, 60, -117, 150, -115, 60, -66, -74, -290, + -174, -222, -336, -410, -393, -454, -500, -436, -457, -579, -566, -638, + -618, -691, -652, -777, -695, -736, -701, -743, -633, -655, -650, -600, + -573, -602, -585, -524, -544, -563, -496, -527, -483, -513, -414, -513, + -387, -442, -351, -441, -335, -395, -360, -389, -346, -380, -350, -337, + -339, -350, -286, -303, -303, -289, -278, -317, -289, -279, -256, -274, + -224, -223, -201, -206, -165, -204, -151, -205, -147, -195, -148, -200, + -177, -178, -170, -179, -162, -156, -149, -146, -135, -109, -105, -142, + -94, -76, -93, -61, -56, -26, -52, -21, -71, -105, -113, -130, + -140, -154, -74, -110, -107, -129, -114, -156, -108, -147, -143, -197, + -121, -103, -164, -177, -160, -133, -135, -137, -138, -104, -105, -75, + -105, -83, -126, -87, -67, -44, -97, -18, -59, -13, -45, -23, + -53, -56, -43, -12, -14, -6, -40, 1, -18, 27, -19, 35, + -53, 6, -9, 7, 11, 2, -8, -3, 22, 16, -2, 32, + 44, 18, -5, 28, 17, 14, 19, 9, 27, 26, 56, 44, + 54, 64, 65, 88}, + /* IRC_Composite_C_R0195_T300_P045.wav */ + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 1, -2, 6, -170, -102, -259, -234, + -199, -268, -51, -528, -137, -354, -180, 92, -180, 340, 604, 11, + -371, -583, -984, 10, 2699, 3142, 3080, 1412, 327, 1987, 956, 830, + 764, 347, 196, 382, 1091, 294, 859, 889, 945, 523, 1199, 597, + 582, 854, 625, 694, 469, 918, 446, 675, 763, 646, 482, 552, + 534, 540, 563, 394, 300, 434, 396, 321, 144, 359, -94, 142, + -43, 165, -138, 6, -183, -179, -221, -161, -328, -285, -267, -402, + -333, -327, -294, -327, -363, -296, -325, -259, -291, -320, -304, -304, + -298, -291, -297, -314, -280, -329, -297, -314, -281, -234, -352, -241, + -283, -217, -315, -280, -263, -279, -310, -268, -299, -296, -261, -277, + -280, -277, -225, -216, -261, -203, -214, -192, -239, -227, -233, -213, + -266, -208, -195, -200, -197, -191, -188, -186, -205, -150, -205, -153, + -112, -151, -87, -35, -120, -124, -179, -137, -157, -174, -117, -144, + -171, -153, -152, -138, -131, -174, -135, -128, -150, -114, -214, -174, + -206, -228, -215, -210, -194, -196, -208, -219, -189, -187, -156, -164, + -222, -168, -262, -148, -197, -176, -237, -214, -212, -100, -129, -146, + -213, -165, -123, -124, -133, -162, -159, -151, -116, -118, -122, -155, + -138, -137, -123, -99, -108, -104, -101, -84, -78, -75, -64, -83, + -84, -92, -41, -94, -60, -107, -53, -86, -49, -66, -60, -68, + -31, -65, -31, -29}, + /* IRC_Composite_C_R0195_T315_P045.wav */ + {-1, 1, -1, 1, -1, 1, -1, 1, -1, 1, -1, 1, + -1, 1, -1, 1, -1, 1, -1, 1, -1, 1, -1, 1, + -1, 2, -2, 4, -7, 17, -100, -204, -340, -371, -160, -344, + -275, -415, -419, -249, 41, -182, 77, 891, 131, 2, -1052, -1535, + 924, 162, 4003, 4715, 1544, 1179, 621, 1424, 674, 1203, -197, 55, + 273, 545, 402, 710, 753, 673, 897, 715, 647, 832, 758, 686, + 732, 814, 620, 688, 779, 690, 604, 649, 586, 620, 586, 617, + 478, 512, 430, 467, 385, 389, 275, 162, 250, 131, 159, 97, + 100, -42, -92, -107, -156, -193, -270, -280, -274, -295, -312, -336, + -301, -367, -310, -306, -332, -327, -275, -299, -308, -302, -264, -320, + -240, -345, -250, -325, -245, -353, -231, -309, -234, -308, -240, -335, + -236, -284, -266, -330, -242, -316, -259, -281, -275, -309, -222, -254, + -279, -277, -236, -254, -242, -235, -247, -199, -188, -240, -203, -141, + -139, -214, -131, -200, -170, -111, -70, -193, -145, -148, -101, -173, + -117, -218, -163, -214, -197, -208, -111, -206, -113, -191, -104, -100, + -70, -142, -89, -169, -106, -184, -156, -187, -185, -255, -195, -151, + -192, -223, -180, -149, -215, -245, -216, -248, -232, -230, -274, -292, + -203, -223, -171, -218, -208, -215, -141, -139, -194, -204, -163, -184, + -155, -159, -154, -164, -134, -151, -130, -134, -130, -152, -122, -129, + -133, -120, -106, -118, -102, -72, -84, -69, -84, -65, -68, -55, + -55, -56, -72, -45, -57, -43, -50, -64, -34, -44, -40, -36, + -22, -14, -32, 2}, + /* IRC_Composite_C_R0195_T330_P045.wav */ + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 1, -1, 1, -1, 1, -1, 1, -2, 2, -2, + 3, -4, 5, -8, -246, -348, -392, -294, -392, -112, -819, -204, + -558, 205, -381, 275, 768, 476, 319, -1186, -2693, 1397, 173, 2904, + 7126, 2191, 1840, -141, 1440, 643, 1148, 280, -721, 280, 227, 474, + 413, 704, 690, 753, 889, 309, 920, 476, 802, 503, 682, 628, + 623, 869, 602, 783, 626, 695, 707, 691, 850, 635, 757, 447, + 558, 418, 394, 227, 133, 146, 217, 152, 239, 143, 20, 23, + 13, -113, -130, -110, -235, -231, -231, -223, -290, -311, -279, -339, + -297, -335, -258, -358, -326, -332, -275, -283, -293, -287, -240, -279, + -296, -244, -248, -261, -257, -280, -251, -271, -271, -291, -272, -276, + -292, -293, -287, -273, -297, -261, -259, -291, -282, -238, -254, -285, + -269, -242, -239, -234, -237, -211, -232, -160, -167, -153, -54, -121, + -133, -68, -127, -55, -93, -170, -253, -307, -209, -255, -315, -307, + -329, -320, -102, -172, -106, -41, -45, 45, -125, 12, -25, -98, + -177, -185, -220, -166, -179, -213, -164, -210, -152, -240, -217, -183, + -300, -182, -230, -224, -316, -238, -276, -217, -287, -209, -290, -207, + -191, -176, -237, -198, -251, -191, -184, -201, -245, -147, -177, -164, + -190, -112, -183, -108, -171, -100, -163, -85, -153, -101, -137, -74, + -146, -57, -120, -58, -110, -43, -90, -50, -94, -38, -79, -64, + -64, -54, -43, -59, -47, -30, -23, -30, -38, -10, -18, 12, + -13, 10, -24, 5}, + /* IRC_Composite_C_R0195_T345_P045.wav */ + {16, -16, 16, -16, 16, -16, 16, -16, 16, -15, 15, -14, + 14, -13, 11, -9, 7, -4, 0, 5, -13, 25, -45, 84, + -233, -338, -363, -485, -200, -447, -145, -898, -330, -612, 95, -690, + 294, 625, 1287, -151, 69, -4722, 1521, -1345, 1556, 10396, 3052, 2742, + 68, 1282, 287, 1583, 813, -614, -309, 335, 28, 768, 552, 558, + 584, 902, 480, 734, 438, 738, 500, 576, 491, 546, 493, 681, + 435, 611, 561, 752, 683, 864, 812, 738, 819, 537, 703, 581, + 367, 218, 0, 254, 20, 344, -127, 37, -166, 211, -28, -62, + -54, -160, -27, -117, -166, -287, -249, -103, -237, -262, -426, -232, + -222, -305, -355, -320, -241, -298, -264, -311, -247, -264, -248, -253, + -288, -211, -240, -212, -321, -248, -261, -248, -306, -335, -270, -295, + -235, -360, -279, -274, -198, -287, -320, -253, -257, -194, -270, -236, + -243, -201, -255, -169, -230, -203, -38, -204, -189, 64, -47, -32, + -96, -24, -149, -249, -211, -363, -445, -322, -199, -451, -374, -212, + -206, -56, -101, 52, -41, 90, 144, 4, -84, 38, -111, -234, + -118, -217, -227, -246, -157, -274, -235, -253, -192, -341, -200, -287, + -253, -258, -299, -242, -317, -247, -273, -208, -264, -246, -273, -176, + -209, -205, -204, -238, -186, -184, -158, -218, -216, -193, -143, -174, + -141, -179, -144, -152, -78, -161, -124, -122, -54, -126, -102, -95, + -62, -64, -102, -54, -75, -51, -32, -78, -64, -53, -39, -81, + -33, -75, -14, -32, -27, -68, 2, -35, 2, -22, 5, -24, + 24, -12, -6, 0}}; + +const int16_t irc_composite_c_r0195_p060[][256] = + {/* IRC_Composite_C_R0195_T000_P060.wav */ + {6, -6, 6, -6, 6, -6, 6, -6, 6, -6, 6, + -6, 6, -6, 5, -5, 5, -6, 7, -9, 18, -62, + -477, -304, -336, -458, -335, -491, -402, -829, -409, -707, -293, + -66, 983, 571, 504, 539, -3665, -1218, -4247, 2405, 12687, 3792, + 3387, 222, 562, 745, 2239, 1323, -741, -286, -83, 813, 783, + 665, -6, 1300, 432, 669, 373, 824, 513, 741, 332, 740, + 326, 679, 126, 625, 66, 777, 213, 737, 590, 515, 621, + 819, 662, 597, 612, 571, 221, 378, 180, 456, -17, 193, + -19, 201, -103, 188, -114, -50, -223, 41, -272, -7, -204, + -73, -274, -27, -179, -118, -266, -52, -236, -150, -298, -185, + -328, -211, -330, -359, -321, -288, -288, -390, -233, -301, -238, + -287, -186, -257, -179, -231, -185, -249, -207, -244, -242, -257, + -261, -239, -326, -213, -312, -252, -311, -214, -308, -230, -270, + -210, -269, -227, -231, -256, -281, -231, -249, -231, -210, -238, + -174, -214, -204, -218, -156, -225, -179, -158, -114, -151, -171, + -128, -150, -184, -87, -97, -145, -171, -201, -199, -179, -259, + -239, -309, -310, -314, -227, -196, -170, -192, -26, -115, 43, + -61, 74, -180, -52, -139, -83, -192, -18, -187, -104, -111, + -67, -175, -118, -149, -164, -160, -118, -201, -219, -161, -187, + -241, -189, -199, -195, -205, -163, -179, -154, -122, -150, -171, + -167, -152, -190, -183, -192, -193, -159, -180, -150, -160, -87, + -151, -131, -124, -50, -117, -92, -81, -58, -70, -16, -38, + -34, -12, 39, -10, 47, 38, 87, 17, 83, 31, 82, + 15, 52, 1}, + /* IRC_Composite_C_R0195_T030_P060.wav */ + {20, -21, 22, -23, 24, -25, 26, -28, 29, -31, 33, + -35, 37, -40, 44, -49, 57, -69, 99, -268, -386, -296, + -341, -306, -439, -461, -219, -926, -450, -515, -185, -151, 2064, + -248, 2208, -557, -2551, -2664, -6482, 6173, 13499, 9000, -215, 137, + 1397, 897, 1847, 1331, -989, -1152, -31, 458, 894, 968, 314, + 922, 758, 306, 312, 862, 265, 458, 604, 466, 114, 555, + 424, 349, 321, 475, 497, 583, 497, 442, 464, 481, 505, + 592, 441, 239, 76, 20, 163, 45, -273, -262, -336, -166, + -64, -152, -189, -166, -140, -223, -230, -243, -348, -277, -328, + -243, -252, -223, -247, -223, -225, -225, -211, -310, -229, -259, + -253, -320, -259, -356, -330, -309, -289, -302, -271, -256, -257, + -245, -269, -281, -283, -267, -256, -268, -267, -280, -263, -261, + -261, -284, -289, -275, -272, -247, -274, -279, -271, -276, -305, + -292, -268, -282, -266, -248, -248, -229, -247, -221, -202, -217, + -233, -213, -204, -239, -209, -216, -194, -227, -155, -201, -185, + -243, -159, -264, -210, -189, -201, -282, -175, -151, -178, -186, + -73, -151, -201, -182, -71, -254, -132, -94, -52, -235, -22, + -27, -101, -159, -17, -130, -166, -55, -23, -206, -132, -126, + -159, -211, -63, -187, -189, -148, -84, -174, -142, -124, -126, + -185, -149, -155, -156, -187, -149, -205, -136, -153, -105, -164, + -138, -137, -75, -158, -128, -135, -115, -148, -94, -125, -133, + -123, -51, -120, -102, -85, -18, -84, -52, -42, 8, -51, + 10, 0, 9, -20, 40, 16, 14, 24, 69, 21, 25, + 23, 33, 2}, + /* IRC_Composite_C_R0195_T060_P060.wav */ + {49, -49, 50, -50, 49, -49, 48, -47, 45, -43, 39, + -34, 27, -17, 1, 24, -67, 158, -573, 27, -273, -533, + -199, -358, -57, -1237, 277, -801, -585, 81, 1678, -52, 2584, + -635, 2281, -6550, -1907, -4733, 12287, 16138, 2134, 560, -1141, 3390, + 1047, 2071, -850, -288, -1730, 541, 499, 654, 282, 1081, 13, + 1053, -59, 982, 296, 639, 439, 387, 97, 735, -102, 457, + 298, 541, 229, 951, 144, 642, -123, 725, 239, 371, -407, + 173, -217, 5, -418, 64, -709, -238, -708, -195, -402, -221, + -414, -199, -238, -175, -279, -243, -447, -227, -531, -270, -429, + -178, -455, -173, -350, -170, -312, -238, -284, -268, -355, -248, + -402, -300, -358, -251, -472, -244, -350, -274, -353, -195, -309, + -279, -276, -243, -232, -248, -185, -198, -344, -177, -263, -274, + -355, -198, -341, -222, -358, -178, -344, -238, -289, -271, -338, + -286, -218, -316, -206, -256, -230, -264, -214, -239, -277, -261, + -259, -225, -268, -221, -134, -246, -182, -192, -154, -260, -116, + -218, -234, -234, -227, -254, -254, -224, -164, -220, -284, -56, + -257, -45, -289, -17, -299, 22, -225, 103, -264, -36, -33, + -158, -37, -147, 39, -238, -62, -117, -69, -262, 42, -153, + -162, -86, -22, -214, -31, -193, -19, -225, -87, -128, -143, + -84, -102, -114, -95, -106, -146, -143, -5, -308, -86, -140, + -102, -254, 41, -68, -208, 37, -26, -47, -124, 31, -99, + -131, -35, -104, -59, -141, 32, -115, 3, -48, 1, -54, + 32, -67, -4, -31, -40, -50, 45, -30, -17, 43, 3, + 8, 10, 37}, + /* IRC_Composite_C_R0195_T090_P060.wav */ + {20, -22, 23, -25, 26, -28, 30, -33, 35, -38, 41, + -45, 49, -54, 60, -66, 75, -87, -10, -413, -67, -233, + -277, -289, -247, -282, -617, -150, -183, 967, 613, 1964, 132, + 3132, -4890, -30, -7492, 6867, 13211, 9263, 1898, -2424, 3417, 1010, + 3400, -250, -877, -1856, 320, -121, 560, 520, 754, 353, 491, + -65, 931, 362, 494, 135, 792, 604, 460, -39, 664, 258, + 476, 82, 671, 151, 141, 64, 562, -256, -248, -550, -170, + -357, -396, -602, -158, -390, -415, -610, -298, -495, -302, -712, + -205, -411, -237, -411, -197, -452, -250, -501, -239, -421, -284, + -481, -170, -438, -181, -420, -184, -419, -262, -417, -218, -376, + -336, -394, -333, -356, -335, -390, -304, -355, -247, -352, -237, + -395, -250, -384, -156, -227, -147, -266, -148, -197, -207, -288, + -280, -286, -312, -317, -266, -316, -238, -306, -223, -320, -187, + -342, -177, -339, -198, -318, -212, -319, -219, -304, -241, -273, + -207, -279, -190, -248, -107, -255, -126, -251, -112, -272, -95, + -249, -163, -250, -133, -247, -177, -254, -198, -235, -194, -229, + -212, -194, -191, -142, -191, -92, -153, -45, -145, -65, -138, + -64, -93, -82, -65, -113, -49, -140, -78, -118, -32, -137, + -37, -107, -30, -59, -142, -106, -132, -11, -240, 49, -159, + 20, -179, 55, -117, -17, -81, -27, -68, -101, -72, -98, + -48, -107, -136, -19, -93, -74, -119, 94, -211, 28, -129, + 49, -157, 78, -96, 21, -143, 105, -58, -15, -28, 13, + -42, 14, -40, -16, -41, -38, -39, 21, -40, -15, -19, + -23, -62, 3}, + /* IRC_Composite_C_R0195_T120_P060.wav */ + {-11, 11, -11, 11, -11, 10, -10, 10, -9, 9, -8, + 7, -6, 4, -1, -4, 11, -23, 54, -588, 228, -347, + 10, -393, 78, -279, -133, -619, 373, 107, 880, 1033, 1066, + 2024, -241, -1833, -4036, 1812, 3742, 13338, 5090, 1325, -1146, 2733, + 2814, 1516, -118, -2246, -488, 96, 262, 192, 815, 13, 476, + 529, 495, 181, 475, 265, 780, 210, 341, 81, 498, 784, + 541, 47, -30, -8, 36, -29, -17, -592, -609, -210, -251, + -604, -616, -485, -190, -407, -234, -662, -290, -550, -301, -639, + -361, -458, -250, -462, -279, -431, -205, -390, -287, -423, -271, + -396, -265, -443, -263, -432, -176, -477, -278, -447, -188, -475, + -218, -453, -216, -462, -223, -420, -282, -431, -248, -385, -293, + -403, -276, -425, -204, -367, -74, -400, -116, -375, -65, -372, + -120, -338, -171, -350, -192, -321, -226, -351, -196, -350, -234, + -344, -164, -341, -158, -306, -138, -338, -156, -268, -116, -265, + -176, -243, -204, -212, -198, -229, -226, -192, -144, -137, -153, + -181, -152, -194, -135, -178, -195, -187, -227, -175, -208, -170, + -244, -159, -254, -137, -245, -163, -226, -152, -162, -107, -156, + -107, -123, -75, -95, -2, -107, -23, -62, -12, -74, -60, + -54, -88, -41, -86, -11, -107, -13, -74, -29, -98, -23, + -52, -89, -64, -61, -71, -89, -73, 19, -96, 13, -86, + 41, -106, 92, -5, 1, -25, -7, -37, -56, 34, -91, + -24, -86, 10, -49, -33, -31, -56, 10, -83, 56, -62, + 45, -72, 47, -53, 7, -38, 12, -40, -11, -4, -41, + -34, -33, -33}, + /* IRC_Composite_C_R0195_T150_P060.wav */ + {-16, 16, -17, 17, -18, 18, -19, 20, -20, 21, -21, 22, + -22, 23, -23, 23, -23, 22, -20, 15, -1, -129, 61, -83, + -118, -17, -51, -30, -188, -95, 109, 510, 730, 1005, 940, 2137, + -1039, -410, -2971, 1165, 5406, 9365, 3937, 2137, -649, 991, 4216, 933, + -931, -1885, 270, 39, 471, -2, 92, 175, 614, 97, 357, 338, + 829, 636, 315, -32, 510, 435, 434, 61, 257, -175, -293, -260, + -101, -474, -492, -360, -265, -315, -367, -291, -300, -567, -400, -366, + -215, -495, -451, -522, -369, -523, -275, -402, -310, -403, -263, -346, + -267, -319, -256, -370, -300, -367, -298, -401, -295, -443, -304, -417, + -322, -444, -298, -381, -312, -439, -329, -390, -317, -384, -301, -362, + -302, -383, -295, -368, -322, -392, -272, -324, -246, -326, -211, -303, + -225, -323, -240, -326, -214, -295, -235, -325, -259, -307, -231, -265, + -204, -266, -221, -283, -193, -226, -150, -215, -156, -235, -140, -226, + -173, -226, -138, -181, -139, -211, -149, -228, -146, -230, -122, -202, + -104, -157, -89, -169, -122, -180, -141, -172, -119, -242, -182, -247, + -146, -221, -160, -209, -160, -222, -134, -194, -158, -172, -105, -115, + -90, -104, -68, -78, -70, -76, -8, -32, 19, -27, 16, -5, + -14, -32, 0, -60, -11, -71, 10, -79, -60, -81, -34, -99, + -28, -10, -16, 0, 23, 9, -66, -19, -7, -4, -45, 41, + 7, -5, -25, -4, 7, -31, -3, 22, -19, -42, 6, -62, + -7, -36, 56, -32, 47, -21, 39, -84, -6, -47, -14, -45, + 33, -19, -3, -57}, + /* IRC_Composite_C_R0195_T180_P060.wav */ + {10, -10, 10, -11, 11, -10, 10, -10, 10, -10, 10, -10, + 9, -9, 8, -7, 6, -5, 3, -1, -2, 6, -12, 19, + -22, 153, -20, -54, 110, 44, 10, -80, 172, 36, 605, 639, + 998, 719, 1758, -623, -249, -1810, 1268, 4590, 6942, 3540, 1131, 148, + 816, 4013, 533, -755, -1438, 271, -13, 605, 142, 55, 202, 291, + -131, 465, 665, 844, 217, 120, 213, 663, 92, -57, -100, 67, + -191, -270, -528, -381, -572, -177, -319, -6, -278, -198, -211, 58, + -313, -463, -592, -248, -465, -368, -496, -344, -514, -270, -361, -242, + -341, -263, -349, -193, -335, -278, -416, -272, -416, -335, -439, -313, + -451, -348, -453, -349, -443, -392, -472, -363, -430, -386, -408, -312, + -392, -320, -379, -310, -370, -331, -403, -335, -385, -348, -366, -269, + -311, -276, -302, -234, -290, -316, -341, -275, -241, -217, -208, -227, + -228, -238, -222, -184, -224, -208, -241, -186, -232, -205, -245, -203, + -214, -163, -151, -128, -135, -145, -180, -187, -204, -171, -188, -145, + -158, -112, -157, -75, -116, -69, -149, -129, -160, -136, -115, -133, + -152, -184, -177, -167, -165, -197, -218, -169, -194, -138, -205, -133, + -162, -112, -132, -87, -117, -57, -79, -38, -55, -39, -50, -29, + -58, -18, 2, 13, -3, 31, 17, 9, 8, 29, -11, -10, + -3, 19, -49, -35, -14, -39, -21, -64, -32, -53, -24, -15, + -14, -23, 27, 17, 51, -9, -15, -18, 18, -37, 5, -74, + -13, -18, 63, 0, 46, 7, 63, -15, 19, -10, 7, -30, + -32, -44, -79, -107}, + /* IRC_Composite_C_R0195_T210_P060.wav */ + {-5, 5, -5, 5, -5, 5, -5, 5, -5, 5, -5, 5, + -5, 5, -5, 5, -5, 5, -5, 5, -5, 5, -4, 4, + -4, 4, -4, -36, -70, 2, -43, -45, -73, 17, -83, -76, + -100, 274, 192, 709, 499, 1049, 474, 192, -1587, -214, 1449, 4357, + 5043, 2092, 784, 976, 2790, 1402, 1131, -633, 70, 310, 385, 217, + 300, 87, 393, 309, 640, 644, 573, 357, 469, 403, 414, 393, + 291, 95, 9, 177, 41, -38, -147, -145, -178, -91, 51, -23, + 82, 6, 81, -66, -34, -205, -207, -298, -223, -348, -275, -305, + -257, -377, -308, -322, -230, -324, -269, -300, -305, -334, -286, -346, + -334, -384, -351, -411, -405, -389, -413, -415, -387, -382, -401, -384, + -393, -405, -388, -417, -364, -399, -357, -398, -374, -392, -376, -343, + -359, -316, -314, -243, -324, -280, -326, -283, -293, -262, -262, -275, + -249, -231, -206, -195, -215, -224, -241, -210, -220, -242, -276, -255, + -264, -198, -192, -190, -189, -210, -164, -172, -169, -172, -189, -153, + -193, -127, -166, -117, -158, -112, -128, -133, -107, -136, -117, -161, + -108, -110, -142, -127, -184, -138, -172, -147, -192, -188, -193, -158, + -153, -183, -168, -189, -142, -164, -152, -151, -135, -105, -91, -78, + -46, -70, -9, -22, -10, 6, -3, 11, 9, 0, -18, -34, + -49, -47, -78, -56, -80, -88, -84, -41, -76, -89, -89, -34, + -62, 21, -11, 29, -63, 38, 13, 32, -26, -6, -18, -15, + -14, -11, -43, -37, -18, 40, -10, 3, -52, 3, -65, -18, + -66, -33, -83, -53}, + /* IRC_Composite_C_R0195_T240_P060.wav */ + {-5, 5, -5, 5, -5, 6, -6, 6, -6, 6, -6, 6, + -6, 6, -6, 6, -6, 6, -6, 6, -6, 7, -7, 7, + -6, 6, -6, 4, 1, -68, -141, -43, -120, -98, -71, -122, + -111, -111, -115, -151, -61, 253, 299, 522, 567, 502, 242, -1198, + -647, 857, 3187, 4265, 2111, 1152, 1068, 2588, 1288, 1124, 171, 189, + 245, 613, 495, 306, 631, 410, 664, 513, 726, 614, 455, 361, + 493, 538, 415, 305, 397, 398, 328, 162, 355, 127, 167, 95, + 91, 92, 127, 159, 137, 126, 136, 112, 42, -54, -59, -214, + -99, -334, -210, -340, -229, -357, -264, -345, -232, -312, -253, -294, + -304, -352, -288, -347, -349, -364, -352, -373, -394, -376, -368, -434, + -376, -402, -393, -434, -406, -412, -357, -430, -351, -354, -374, -337, + -289, -323, -276, -287, -233, -304, -270, -311, -283, -321, -267, -321, + -256, -305, -231, -272, -202, -256, -217, -272, -229, -238, -234, -247, + -261, -261, -253, -218, -198, -218, -190, -226, -148, -209, -150, -201, + -174, -181, -174, -172, -158, -155, -141, -131, -122, -138, -117, -118, + -119, -130, -144, -143, -130, -146, -135, -155, -149, -172, -147, -158, + -167, -175, -169, -144, -176, -150, -108, -129, -129, -136, -93, -143, + -97, -99, -87, -71, -74, -15, -38, -48, -75, -66, -95, -101, + -91, -93, -103, -154, -122, -128, -116, -84, -70, -79, -65, -51, + -28, -23, -20, -21, -53, -15, -9, 15, -49, 5, -40, -18, + -42, -33, -30, -86, -81, -68, -62, -85, -44, -26, -89, -49, + -55, -24, -61, -16}, + /* IRC_Composite_C_R0195_T270_P060.wav */ + {5, -5, 6, -6, 6, -6, 6, -6, 7, -7, 7, -7, + 8, -8, 8, -9, 9, -10, 10, -11, 12, -12, 13, -15, + 16, -19, 22, -27, 38, -79, -217, -116, -99, -179, -211, -36, + -231, -204, -130, -219, -231, -133, 80, 277, 396, 430, 429, -88, + -768, -1080, 327, 3345, 4354, 1751, 849, 1821, 1217, 1828, 885, 517, + 269, 311, 1017, 365, 820, 357, 716, 549, 590, 621, 516, 736, + 326, 780, 263, 770, 281, 705, 378, 636, 371, 475, 311, 429, + 255, 409, 238, 273, 140, 314, 213, 208, 138, 105, 115, -23, + 74, -205, -25, -248, -155, -287, -280, -252, -297, -307, -288, -293, + -267, -333, -245, -352, -304, -348, -348, -351, -433, -337, -451, -334, + -472, -324, -423, -375, -381, -372, -314, -375, -276, -267, -321, -301, + -279, -273, -291, -287, -302, -304, -304, -290, -236, -296, -289, -263, + -294, -284, -324, -265, -281, -292, -245, -245, -266, -260, -214, -224, + -254, -200, -260, -209, -252, -220, -200, -254, -196, -233, -158, -228, + -136, -185, -148, -205, -143, -175, -148, -149, -166, -120, -148, -104, + -128, -140, -125, -152, -101, -130, -77, -199, -88, -178, -108, -197, + -126, -198, -117, -194, -113, -172, -133, -128, -87, -131, -57, -112, + -60, -136, -71, -150, -125, -162, -106, -205, -139, -165, -138, -149, + -118, -134, -84, -134, -83, -120, -64, -109, -79, -116, -53, -90, + -64, -49, -97, -69, -54, -86, -38, -63, -120, -64, -43, -89, + -78, -45, -32, -110, -9, -49, -30, -91, -36, -87, -34, -90, + -12, -62, -20, -67}, + /* IRC_Composite_C_R0195_T300_P060.wav */ + {5, -5, 5, -5, 6, -6, 6, -6, 6, -6, 6, -6, + 6, -7, 7, -7, 7, -7, 7, -8, 8, -8, 8, -9, + 9, -9, 10, -11, 16, -103, -316, -221, -172, -255, -229, -364, + -48, -533, -236, -67, -300, 259, 642, 185, 100, 427, -1196, -1745, + 476, 2467, 5309, 2589, 680, 1682, 925, 1569, 1041, 949, -237, 234, + 798, 421, 371, 569, 750, 604, 969, 544, 826, 529, 786, 297, + 628, 582, 490, 514, 782, 585, 536, 518, 533, 446, 484, 585, + 264, 478, 477, 367, 328, 417, 65, 219, 65, 246, -91, 156, + -64, 72, -138, 50, -297, -48, -254, -210, -356, -206, -256, -287, + -317, -214, -367, -254, -373, -339, -401, -317, -430, -376, -368, -350, + -418, -316, -298, -353, -273, -305, -281, -287, -227, -371, -293, -252, + -310, -323, -260, -272, -324, -243, -257, -310, -290, -276, -297, -255, + -201, -324, -280, -270, -230, -282, -221, -248, -298, -222, -268, -241, + -260, -255, -277, -235, -187, -236, -210, -172, -224, -173, -145, -191, + -157, -155, -157, -126, -186, -84, -150, -177, -153, -72, -128, -133, + -127, -100, -229, -197, -160, -241, -236, -171, -176, -120, -75, -47, + -120, -105, -128, -76, -160, -120, -127, -109, -139, -135, -144, -192, + -179, -152, -203, -173, -143, -161, -61, -179, -143, -104, -126, -147, + -78, -176, -138, -177, -87, -160, -110, -137, -186, -155, -86, -135, + -127, -105, -97, -124, -34, -111, -50, -117, -72, -123, -75, -65, + -94, -51, -83, -75, -56, -47, -45, -13, -39, -55, 20, -56, + 36, -41, 12, -24}, + /* IRC_Composite_C_R0195_T330_P060.wav */ + {3, -3, 4, -4, 4, -4, 5, -5, 5, -6, 6, -6, + 7, -7, 8, -9, 10, -11, 13, -15, 18, -22, 28, -41, + 71, -233, -373, -96, -345, -318, -371, -265, -252, -594, -439, -312, + -197, 197, 323, 301, 1318, -757, -1616, -868, -2081, 2422, 6337, 5378, + 1528, 381, 1195, 1007, 1545, 810, 158, -165, 224, 315, 652, 555, + 370, 728, 945, 600, 540, 730, 606, 560, 567, 567, 493, 662, + 473, 574, 547, 639, 468, 638, 527, 625, 475, 584, 476, 643, + 403, 465, 284, 372, 243, 235, 43, 108, 49, 102, 20, -28, + -86, -48, -49, -120, -149, -133, -123, -173, -198, -229, -246, -291, + -242, -300, -328, -373, -296, -357, -299, -377, -351, -356, -276, -311, + -324, -289, -282, -268, -271, -255, -256, -273, -249, -285, -230, -270, + -219, -299, -282, -269, -275, -287, -307, -267, -290, -268, -297, -284, + -263, -248, -257, -244, -243, -257, -290, -273, -259, -265, -237, -261, + -238, -273, -209, -234, -171, -171, -157, -118, -120, -137, -80, -115, + -96, -166, -45, -140, -79, -126, -114, -259, -243, -265, -228, -274, + -267, -264, -220, -203, -117, -137, -118, -112, -33, -82, -41, -93, + -102, -121, -120, -145, -175, -177, -147, -143, -65, -127, -138, -136, + -94, -127, -130, -217, -183, -159, -168, -202, -184, -203, -240, -202, + -146, -195, -160, -147, -169, -143, -72, -128, -191, -139, -128, -170, + -166, -129, -160, -152, -97, -112, -123, -68, -72, -87, -76, -27, + -63, -48, -50, -16, -43, 16, -15, -10, -1, 57, 47, 30, + 49, 50, 35, 14}}; + +const int16_t irc_composite_c_r0195_p075[][256] = + {/* IRC_Composite_C_R0195_T000_P075.wav */ + {12, -12, 13, -13, 13, -13, 13, -13, 14, -14, 14, -14, + 14, -14, 15, -15, 14, -14, 13, -12, 7, 8, -224, -367, + -189, -595, -344, -165, -801, -581, -302, -836, -618, 407, 530, 134, + 1455, -966, -265, -4329, -4113, 4525, 6992, 8542, 304, -199, 2762, 1323, + 1544, 1359, 195, -1272, 815, 548, 454, 489, 1188, 303, 1217, 433, + 606, 486, 940, 200, 865, 394, 415, 452, 439, 167, 687, 262, + 388, 668, 618, 250, 776, 505, 518, 525, 594, 287, 552, 461, + 358, 276, 304, 160, 239, 30, 35, -21, -62, -125, -17, -31, + -371, 39, -211, -122, -197, 40, -391, 72, -166, -93, -262, -84, + -337, -76, -325, -174, -301, -196, -378, -120, -367, -324, -293, -268, + -402, -208, -271, -377, -322, -212, -314, -262, -200, -245, -294, -186, + -222, -275, -226, -280, -246, -300, -262, -182, -254, -272, -206, -217, + -307, -182, -214, -248, -250, -184, -258, -303, -196, -229, -312, -240, + -213, -256, -268, -167, -202, -256, -145, -174, -141, -306, -39, -235, + -192, -132, -161, -142, -205, -78, -192, -93, -268, -65, -239, -166, + -229, -88, -215, -227, -134, -262, -99, -230, -94, -297, -147, -247, + -15, -245, -197, -51, -171, -94, -46, -60, -97, -14, -100, 14, + -99, -80, -92, -24, -178, 55, -112, -23, -104, 47, -121, -73, + -80, -27, -196, -98, -74, -188, -162, -54, -110, -182, -133, -30, + -121, -83, -72, -116, -145, -93, -89, -136, -159, -163, -75, -145, + -145, -85, -61, -103, -107, 5, -74, -43, -3, -22, -72, -26, + -24, 6, -85, 5}, + /* IRC_Composite_C_R0195_T060_P075.wav */ + {-14, 15, -15, 16, -17, 18, -19, 20, -21, 22, -23, + 24, -25, 27, -28, 30, -32, 35, -42, 65, -281, -389, + 123, -583, -268, -119, -384, -436, -660, 282, -1424, 931, -191, + 1870, -113, 1526, 243, -2974, -2922, -3038, 6766, 14120, 3000, -177, + 427, 2482, 2472, 607, 1128, -2092, 683, -982, 2038, -839, 1456, + -30, 1050, 188, 720, 669, 391, 864, 257, 483, 214, 493, + 238, 321, 378, 114, 592, 45, 886, -9, 646, 103, 701, + 13, 345, 57, 116, 121, -88, 171, -386, 97, -335, -262, + -369, -373, -308, -409, 33, -533, 5, -484, 24, -435, -237, + -319, -208, -233, -416, -146, -317, -166, -309, -232, -304, -340, + -158, -439, -169, -482, -113, -521, -200, -450, -258, -409, -296, + -298, -425, -296, -402, -280, -434, -185, -317, -227, -279, -194, + -241, -311, -232, -320, -314, -306, -233, -329, -276, -236, -245, + -297, -256, -187, -267, -262, -210, -246, -287, -248, -205, -331, + -278, -229, -232, -260, -191, -174, -217, -208, -141, -202, -243, + -151, -110, -284, -132, -132, -134, -277, -115, -207, -229, -254, + -153, -213, -329, -111, -236, -248, -245, -82, -281, -102, -144, + -93, -133, -52, -82, -139, -23, -121, -40, -135, 7, -127, + -34, -127, -9, -67, -119, -37, -34, -134, 4, -70, -77, + -81, -62, -64, -127, -18, -109, 11, -156, 60, -224, 27, + -77, -130, 6, -101, -38, -151, 93, -175, -123, 0, -32, + -129, -111, 115, -229, -48, 12, -46, -196, -37, -14, -163, + -83, -1, -137, -48, -101, 91, -141, 15, -60, 28, -35, + -33, -10, -28}, + /* IRC_Composite_C_R0195_T120_P075.wav */ + {-13, 13, -14, 14, -15, 15, -16, 16, -17, 17, -18, + 18, -19, 19, -20, 20, -19, 19, -16, 12, -1, -57, + -48, -189, -132, -75, -227, -56, -343, 28, -501, 199, 201, + 917, 964, 1130, 545, 1226, -3782, -981, -1166, 8252, 10379, 3341, + -521, -669, 4572, 428, 2096, -1386, -405, -1098, 1383, -2, 236, + 525, 510, 124, 361, 340, 604, 403, 459, 20, 703, 550, + 361, 4, 507, 76, 379, 200, 87, 66, -36, 226, 133, + 30, -418, -183, -144, -119, -262, -333, -294, -550, -263, -353, + -297, -518, -525, -481, -405, -172, -382, -282, -383, -195, -329, + -205, -358, -340, -304, -296, -417, -305, -321, -319, -400, -254, + -454, -283, -418, -294, -482, -262, -427, -329, -482, -271, -419, + -319, -403, -345, -351, -373, -409, -381, -310, -297, -225, -271, + -290, -302, -223, -252, -250, -249, -220, -293, -267, -259, -270, + -267, -281, -238, -321, -208, -300, -203, -357, -197, -295, -169, + -277, -177, -273, -210, -234, -196, -252, -188, -188, -144, -221, + -121, -160, -150, -180, -111, -124, -171, -110, -188, -133, -213, + -124, -220, -168, -203, -176, -182, -229, -156, -255, -117, -221, + -99, -240, -61, -158, -122, -129, -39, -92, -133, 4, -74, + -53, -77, 35, -129, 8, -50, 5, -61, -24, -33, -46, + 31, -56, 58, -35, 39, -37, 57, -74, 2, -108, -10, + -136, 18, -141, 8, -114, 4, -132, -22, -37, -31, -20, + -71, 79, -149, 14, -32, 50, -85, -47, 94, -59, 21, + -39, 82, -145, 47, -47, -14, -82, -16, -3, -58, -48, + -44, -22, -40}, + /* IRC_Composite_C_R0195_T180_P075.wav */ + {-17, 17, -17, 17, -16, 16, -16, 16, -15, 15, -15, 14, + -14, 14, -13, 12, -12, 11, -10, 9, -7, 5, -2, -2, + 11, 164, -118, 156, -74, 171, -166, 281, -259, 331, -163, 647, + 483, 1342, 96, 2125, -652, 369, -2506, 1012, 3788, 6928, 5436, -854, + 927, 1011, 2973, 419, 642, -1377, -224, 371, 309, -128, 78, 268, + 228, 233, 434, 427, 397, -91, 692, 541, 379, -60, 222, -23, + 131, 178, 60, -172, -56, -82, -145, -311, -297, -431, -248, -412, + -107, -371, -72, -353, -277, -448, -332, -408, -362, -461, -379, -530, + -388, -407, -290, -364, -334, -409, -288, -419, -285, -384, -338, -447, + -343, -386, -344, -453, -378, -390, -377, -502, -386, -429, -361, -457, + -343, -463, -441, -462, -348, -398, -369, -416, -375, -435, -312, -336, + -244, -309, -307, -321, -263, -272, -272, -262, -236, -196, -217, -232, + -215, -261, -261, -304, -256, -296, -220, -241, -218, -248, -199, -190, + -206, -213, -223, -195, -209, -182, -150, -161, -155, -165, -145, -155, + -129, -127, -102, -102, -126, -107, -145, -133, -143, -141, -131, -140, + -117, -119, -86, -121, -100, -131, -130, -130, -139, -142, -162, -155, + -148, -128, -171, -136, -140, -117, -101, -76, -112, -44, -78, -20, + -37, -38, -18, 15, -6, 2, 39, 44, 55, -20, 46, 41, + 30, 8, 37, 20, -14, 0, -10, 0, -57, -38, -55, -62, + -113, -27, -88, -33, -69, -21, -80, 1, -86, 62, -11, 16, + 23, 128, 6, 102, -12, 152, 3, 46, 29, 36, -97, 2, + -15, 22, -109, -54}, + /* IRC_Composite_C_R0195_T240_P075.wav */ + {3, -3, 3, -3, 3, -3, 3, -2, 2, -2, 2, -2, + 1, -1, 1, 0, 0, 1, -2, 3, -4, 5, -7, 10, + -13, 20, -42, -111, -129, -71, -115, -137, -61, -159, -123, -192, + -19, -219, 406, 390, 680, 549, 886, -353, -736, -1471, 543, 4694, + 6064, 1476, 785, 1081, 2341, 1848, 567, 793, -1054, 878, -19, 694, + -27, 624, 453, 574, 713, 561, 524, 289, 447, 341, 517, 436, + 227, 504, 311, 411, 259, 396, 204, 319, 114, 96, 38, -78, + 39, -23, 9, 123, -104, 210, -61, 44, -245, -60, -225, -163, + -284, -292, -366, -364, -318, -284, -326, -284, -257, -269, -279, -348, + -246, -376, -367, -329, -388, -355, -428, -316, -418, -375, -426, -318, + -396, -436, -393, -407, -383, -405, -383, -442, -422, -393, -344, -375, + -309, -340, -270, -311, -270, -269, -256, -268, -233, -283, -233, -230, + -236, -283, -256, -265, -301, -274, -317, -261, -298, -265, -239, -262, + -221, -248, -188, -287, -192, -249, -152, -246, -140, -231, -148, -218, + -130, -170, -165, -154, -171, -127, -181, -99, -138, -109, -120, -99, + -88, -129, -61, -150, -63, -199, -44, -206, -106, -201, -113, -189, + -182, -146, -180, -174, -177, -146, -150, -167, -103, -131, -76, -138, + -14, -119, -32, -66, -17, -34, -48, 8, -80, -30, -22, -34, + -12, -52, 1, -29, -7, 12, -34, -30, -36, -43, -95, -29, + -80, -102, -17, -119, -24, -119, 10, -78, -16, -105, -38, -66, + -45, -15, 9, -36, -32, 33, -12, -9, -66, -12, -59, 8, + -23, -10, -6, -22}, + /* IRC_Composite_C_R0195_T300_P075.wav */ + {5, -5, 5, -6, 6, -6, 6, -7, 7, -7, 8, -8, + 9, -9, 10, -10, 11, -12, 13, -14, 16, -19, 23, -30, + 49, -174, -216, -220, -266, -146, -276, -310, -167, -398, -257, -446, + -119, 2, 150, 904, 282, 399, -249, -994, -2605, 51, 5569, 5353, + 3304, -294, 1237, 2394, 748, 1912, 84, -494, 511, 465, 372, 641, + 761, 120, 1277, 373, 764, 283, 1087, -27, 731, 558, 213, 625, + 490, 578, 331, 755, 261, 576, 308, 533, 270, 531, 241, 337, + 345, 292, 249, 254, 130, 252, 95, 124, 0, -94, 22, -124, + -166, -120, -222, -195, -188, -102, -288, -138, -247, -177, -285, -135, + -368, -230, -298, -370, -301, -355, -365, -394, -255, -460, -300, -338, + -376, -356, -289, -388, -412, -303, -449, -224, -403, -214, -327, -319, + -233, -280, -268, -283, -281, -308, -227, -300, -251, -210, -330, -195, + -309, -196, -294, -197, -342, -211, -310, -249, -268, -272, -295, -243, + -275, -237, -249, -206, -274, -172, -248, -191, -211, -154, -258, -145, + -194, -183, -134, -143, -154, -131, -111, -163, -86, -129, -136, -111, + -159, -135, -153, -128, -194, -126, -204, -111, -239, -106, -205, -215, + -163, -175, -185, -155, -144, -130, -163, -92, -105, -142, -4, -111, + -109, -19, -118, 13, -189, 9, -76, -118, -8, -6, -126, -54, + 46, -138, -64, 43, -92, -25, -132, 48, -164, -57, -43, -117, + -199, -11, -164, -58, -105, -75, -100, -150, -59, -62, -137, -80, + -148, -65, -126, -49, -62, -33, -77, -10, -18, -11, -21, -15, + -36, -38, -38, 9}}; + +const int16_t irc_composite_c_r0195_p090[][256] = + {/* IRC_Composite_C_R0195_T000_P090.wav */ + {-1, 1, -1, 1, -1, 1, -1, 2, -2, 2, -3, 3, + -3, 4, -5, 5, -7, 8, -10, 14, -25, -385, -133, -356, + -344, -390, -312, -438, -400, -852, -107, -995, -245, -186, 1358, -771, + 1473, 28, -1710, -3762, -3025, 4119, 8200, 5927, -785, 1558, 2250, 1837, + 2098, 631, -318, 150, -77, 1215, -23, 792, 865, 600, 981, 659, + 366, 950, 511, 766, 502, 516, 589, 299, 331, 571, 105, 609, + 462, 485, 538, 594, 470, 640, 393, 432, 325, 604, 259, 355, + 336, 454, 33, 263, 182, -24, -124, 121, -231, -50, -93, 49, + -230, -101, 25, -196, -123, -21, -160, -161, -132, -136, -155, -268, + -121, -146, -311, -112, -328, -201, -220, -280, -214, -304, -233, -227, + -250, -242, -334, -256, -283, -393, -267, -285, -332, -331, -224, -295, + -279, -171, -151, -361, -199, -239, -227, -317, -168, -211, -370, -182, + -150, -343, -204, -192, -224, -319, -203, -137, -357, -146, -223, -237, + -276, -162, -201, -262, -186, -209, -147, -267, -46, -252, -162, -146, + -182, -199, -143, -218, -204, -108, -250, -163, -90, -241, -137, -125, + -122, -272, -111, -188, -211, -160, -160, -164, -201, -231, -114, -175, + -180, -212, -172, -181, -129, -130, -77, -150, -114, -39, 12, -122, + -9, -11, -46, -21, -4, 7, -73, -20, -50, -21, -80, -102, + 2, -103, -66, -67, -8, -41, -119, -26, 29, -208, 3, -108, + -34, -228, 114, -196, -84, -28, 1, -109, -92, 77, -169, -27, + 48, -12, -79, 3, -100, -44, -53, 13, -103, -90, -14, -91, + -23, -64, -21, -116}}; + +struct Elevation { + /** + * An array of |count| impulse responses of 256 samples for the left ear. + * The impulse responses in each elevation are at equally spaced azimuths + * for a full 360 degree revolution, ordered clockwise from in front the + * listener. + */ + const int16_t (*azimuths)[256]; + int count; +}; + +/** + * irc_composite_c_r0195 is an array with each element containing data for one + * elevation. + */ +const Elevation irc_composite_c_r0195[] = { + {irc_composite_c_r0195_p315, MOZ_ARRAY_LENGTH(irc_composite_c_r0195_p315)}, + {irc_composite_c_r0195_p330, MOZ_ARRAY_LENGTH(irc_composite_c_r0195_p330)}, + {irc_composite_c_r0195_p345, MOZ_ARRAY_LENGTH(irc_composite_c_r0195_p345)}, + {irc_composite_c_r0195_p000, MOZ_ARRAY_LENGTH(irc_composite_c_r0195_p000)}, + {irc_composite_c_r0195_p015, MOZ_ARRAY_LENGTH(irc_composite_c_r0195_p015)}, + {irc_composite_c_r0195_p030, MOZ_ARRAY_LENGTH(irc_composite_c_r0195_p030)}, + {irc_composite_c_r0195_p045, MOZ_ARRAY_LENGTH(irc_composite_c_r0195_p045)}, + {irc_composite_c_r0195_p060, MOZ_ARRAY_LENGTH(irc_composite_c_r0195_p060)}, + {irc_composite_c_r0195_p075, MOZ_ARRAY_LENGTH(irc_composite_c_r0195_p075)}, + {irc_composite_c_r0195_p090, MOZ_ARRAY_LENGTH(irc_composite_c_r0195_p090)}}; + +const int irc_composite_c_r0195_first_elevation = -45; /* degrees */ +const int irc_composite_c_r0195_elevation_interval = 15; /* degrees */ +const int irc_composite_c_r0195_sample_rate = 44100; /* Hz */ diff --git a/dom/media/webaudio/blink/PeriodicWave.cpp b/dom/media/webaudio/blink/PeriodicWave.cpp new file mode 100644 index 0000000000..6b1d173008 --- /dev/null +++ b/dom/media/webaudio/blink/PeriodicWave.cpp @@ -0,0 +1,355 @@ +/* + * 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. + * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of + * its contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY APPLE 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 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 "PeriodicWave.h" +#include <algorithm> +#include <cmath> +#include <limits> +#include "mozilla/FFTBlock.h" + +const unsigned MinPeriodicWaveSize = 4096; // This must be a power of two. +const unsigned MaxPeriodicWaveSize = 8192; // This must be a power of two. +const float CentsPerRange = 1200 / 3; // 1/3 Octave. + +using namespace mozilla; +using mozilla::dom::OscillatorType; + +namespace WebCore { + +already_AddRefed<PeriodicWave> PeriodicWave::create(float sampleRate, + const float* real, + const float* imag, + size_t numberOfComponents, + bool disableNormalization) { + bool isGood = real && imag && numberOfComponents > 0; + MOZ_ASSERT(isGood); + if (isGood) { + RefPtr<PeriodicWave> periodicWave = + new PeriodicWave(sampleRate, numberOfComponents, disableNormalization); + + // Limit the number of components used to those for frequencies below the + // Nyquist of the fixed length inverse FFT. + size_t halfSize = periodicWave->m_periodicWaveSize / 2; + numberOfComponents = std::min(numberOfComponents, halfSize); + periodicWave->m_numberOfComponents = numberOfComponents; + periodicWave->m_realComponents = + MakeUnique<AudioFloatArray>(numberOfComponents); + periodicWave->m_imagComponents = + MakeUnique<AudioFloatArray>(numberOfComponents); + memcpy(periodicWave->m_realComponents->Elements(), real, + numberOfComponents * sizeof(float)); + memcpy(periodicWave->m_imagComponents->Elements(), imag, + numberOfComponents * sizeof(float)); + + return periodicWave.forget(); + } + return nullptr; +} + +already_AddRefed<PeriodicWave> PeriodicWave::createSine(float sampleRate) { + RefPtr<PeriodicWave> periodicWave = + new PeriodicWave(sampleRate, MinPeriodicWaveSize, false); + periodicWave->generateBasicWaveform(OscillatorType::Sine); + return periodicWave.forget(); +} + +already_AddRefed<PeriodicWave> PeriodicWave::createSquare(float sampleRate) { + RefPtr<PeriodicWave> periodicWave = + new PeriodicWave(sampleRate, MinPeriodicWaveSize, false); + periodicWave->generateBasicWaveform(OscillatorType::Square); + return periodicWave.forget(); +} + +already_AddRefed<PeriodicWave> PeriodicWave::createSawtooth(float sampleRate) { + RefPtr<PeriodicWave> periodicWave = + new PeriodicWave(sampleRate, MinPeriodicWaveSize, false); + periodicWave->generateBasicWaveform(OscillatorType::Sawtooth); + return periodicWave.forget(); +} + +already_AddRefed<PeriodicWave> PeriodicWave::createTriangle(float sampleRate) { + RefPtr<PeriodicWave> periodicWave = + new PeriodicWave(sampleRate, MinPeriodicWaveSize, false); + periodicWave->generateBasicWaveform(OscillatorType::Triangle); + return periodicWave.forget(); +} + +PeriodicWave::PeriodicWave(float sampleRate, size_t numberOfComponents, + bool disableNormalization) + : m_sampleRate(sampleRate), + m_centsPerRange(CentsPerRange), + m_maxPartialsInBandLimitedTable(0), + m_normalizationScale(1.0f), + m_disableNormalization(disableNormalization) { + float nyquist = 0.5 * m_sampleRate; + + if (numberOfComponents <= MinPeriodicWaveSize) { + m_periodicWaveSize = MinPeriodicWaveSize; + } else { + unsigned npow2 = fdlibm_exp2f(floorf( + fdlibm_logf(numberOfComponents - 1.0) / fdlibm_logf(2.0f) + 1.0f)); + m_periodicWaveSize = std::min(MaxPeriodicWaveSize, npow2); + } + + m_numberOfRanges = + (unsigned)(3.0f * fdlibm_logf(m_periodicWaveSize) / fdlibm_logf(2.0f)); + m_bandLimitedTables.SetLength(m_numberOfRanges); + m_lowestFundamentalFrequency = nyquist / maxNumberOfPartials(); + m_rateScale = m_periodicWaveSize / m_sampleRate; +} + +size_t PeriodicWave::sizeOfIncludingThis( + mozilla::MallocSizeOf aMallocSizeOf) const { + size_t amount = aMallocSizeOf(this); + + amount += m_bandLimitedTables.ShallowSizeOfExcludingThis(aMallocSizeOf); + for (size_t i = 0; i < m_bandLimitedTables.Length(); i++) { + if (m_bandLimitedTables[i]) { + amount += + m_bandLimitedTables[i]->ShallowSizeOfIncludingThis(aMallocSizeOf); + } + } + + return amount; +} + +void PeriodicWave::waveDataForFundamentalFrequency( + float fundamentalFrequency, float*& lowerWaveData, float*& higherWaveData, + float& tableInterpolationFactor) { + // Negative frequencies are allowed, in which case we alias + // to the positive frequency. + fundamentalFrequency = fabsf(fundamentalFrequency); + + // We only need to rebuild to the tables if the new fundamental + // frequency is low enough to allow for more partials below the + // Nyquist frequency. + unsigned numberOfPartials = numberOfPartialsForRange(0); + float nyquist = 0.5 * m_sampleRate; + if (fundamentalFrequency != 0.0) { + numberOfPartials = + std::min(numberOfPartials, (unsigned)(nyquist / fundamentalFrequency)); + } + if (numberOfPartials > m_maxPartialsInBandLimitedTable) { + for (unsigned rangeIndex = 0; rangeIndex < m_numberOfRanges; ++rangeIndex) { + m_bandLimitedTables[rangeIndex] = 0; + } + + // We need to create the first table to determine the normalization + // constant. + createBandLimitedTables(fundamentalFrequency, 0); + m_maxPartialsInBandLimitedTable = numberOfPartials; + } + + // Calculate the pitch range. + float ratio = fundamentalFrequency > 0 + ? fundamentalFrequency / m_lowestFundamentalFrequency + : 0.5; + float centsAboveLowestFrequency = + fdlibm_logf(ratio) / fdlibm_logf(2.0f) * 1200; + + // Add one to round-up to the next range just in time to truncate + // partials before aliasing occurs. + float pitchRange = 1 + centsAboveLowestFrequency / m_centsPerRange; + + pitchRange = std::max(pitchRange, 0.0f); + pitchRange = std::min(pitchRange, static_cast<float>(m_numberOfRanges - 1)); + + // The words "lower" and "higher" refer to the table data having + // the lower and higher numbers of partials. It's a little confusing + // since the range index gets larger the more partials we cull out. + // So the lower table data will have a larger range index. + unsigned rangeIndex1 = static_cast<unsigned>(pitchRange); + unsigned rangeIndex2 = + rangeIndex1 < m_numberOfRanges - 1 ? rangeIndex1 + 1 : rangeIndex1; + + if (!m_bandLimitedTables[rangeIndex1].get()) + createBandLimitedTables(fundamentalFrequency, rangeIndex1); + + if (!m_bandLimitedTables[rangeIndex2].get()) + createBandLimitedTables(fundamentalFrequency, rangeIndex2); + + lowerWaveData = m_bandLimitedTables[rangeIndex2]->Elements(); + higherWaveData = m_bandLimitedTables[rangeIndex1]->Elements(); + + // Ranges from 0 -> 1 to interpolate between lower -> higher. + tableInterpolationFactor = rangeIndex2 - pitchRange; +} + +unsigned PeriodicWave::maxNumberOfPartials() const { + return m_periodicWaveSize / 2; +} + +unsigned PeriodicWave::numberOfPartialsForRange(unsigned rangeIndex) const { + // Number of cents below nyquist where we cull partials. + float centsToCull = rangeIndex * m_centsPerRange; + + // A value from 0 -> 1 representing what fraction of the partials to keep. + float cullingScale = fdlibm_exp2f(-centsToCull / 1200); + + // The very top range will have all the partials culled. + unsigned numberOfPartials = cullingScale * maxNumberOfPartials(); + + return numberOfPartials; +} + +// Convert into time-domain wave buffers. +// One table is created for each range for non-aliasing playback +// at different playback rates. Thus, higher ranges have more +// high-frequency partials culled out. +void PeriodicWave::createBandLimitedTables(float fundamentalFrequency, + unsigned rangeIndex) { + unsigned fftSize = m_periodicWaveSize; + unsigned i; + + const float* realData = m_realComponents->Elements(); + const float* imagData = m_imagComponents->Elements(); + + // This FFTBlock is used to cull partials (represented by frequency bins). + FFTBlock frame(fftSize); + + // Find the starting bin where we should start culling the aliasing + // partials for this pitch range. We need to clear out the highest + // frequencies to band-limit the waveform. + unsigned numberOfPartials = numberOfPartialsForRange(rangeIndex); + // Also limit to the number of components that are provided. + numberOfPartials = std::min(numberOfPartials, m_numberOfComponents - 1); + + // Limit number of partials to those below Nyquist frequency + float nyquist = 0.5 * m_sampleRate; + if (fundamentalFrequency != 0.0) { + numberOfPartials = + std::min(numberOfPartials, (unsigned)(nyquist / fundamentalFrequency)); + } + + // Copy from loaded frequency data and generate complex conjugate + // because of the way the inverse FFT is defined. + // The coefficients of higher partials remain zero, as initialized in + // the FFTBlock constructor. + for (i = 0; i < numberOfPartials + 1; ++i) { + frame.RealData(i) = realData[i]; + frame.ImagData(i) = -imagData[i]; + } + + // Clear any DC-offset. + frame.RealData(0) = 0; + // Clear value which has no effect. + frame.ImagData(0) = 0; + + // Create the band-limited table. + m_bandLimitedTables[rangeIndex] = + MakeUnique<AlignedAudioFloatArray>(m_periodicWaveSize); + + // Apply an inverse FFT to generate the time-domain table data. + float* data = m_bandLimitedTables[rangeIndex]->Elements(); + frame.GetInverseWithoutScaling(data); + + // For the first range (which has the highest power), calculate + // its peak value then compute normalization scale. + if (m_disableNormalization) { + // See Bug 1424906, results need to be scaled by 0.5 even + // when normalization is disabled. + m_normalizationScale = 0.5; + } else if (!rangeIndex) { + float maxValue; + maxValue = AudioBufferPeakValue(data, m_periodicWaveSize); + + if (maxValue) m_normalizationScale = 1.0f / maxValue; + } + + // Apply normalization scale. + AudioBufferInPlaceScale(data, m_normalizationScale, m_periodicWaveSize); +} + +void PeriodicWave::generateBasicWaveform(OscillatorType shape) { + const float piFloat = float(M_PI); + unsigned fftSize = periodicWaveSize(); + unsigned halfSize = fftSize / 2; + + m_numberOfComponents = halfSize; + m_realComponents = MakeUnique<AudioFloatArray>(halfSize); + m_imagComponents = MakeUnique<AudioFloatArray>(halfSize); + float* realP = m_realComponents->Elements(); + float* imagP = m_imagComponents->Elements(); + + // Clear DC and imag value which is ignored. + realP[0] = 0; + imagP[0] = 0; + + for (unsigned n = 1; n < halfSize; ++n) { + float omega = 2 * piFloat * n; + float invOmega = 1 / omega; + + // Fourier coefficients according to standard definition. + float a; // Coefficient for cos(). + float b; // Coefficient for sin(). + + // Calculate Fourier coefficients depending on the shape. + // Note that the overall scaling (magnitude) of the waveforms + // is normalized in createBandLimitedTables(). + switch (shape) { + case OscillatorType::Sine: + // Standard sine wave function. + a = 0; + b = (n == 1) ? 1 : 0; + break; + case OscillatorType::Square: + // Square-shaped waveform with the first half its maximum value + // and the second half its minimum value. + a = 0; + b = invOmega * ((n & 1) ? 2 : 0); + break; + case OscillatorType::Sawtooth: + // Sawtooth-shaped waveform with the first half ramping from + // zero to maximum and the second half from minimum to zero. + a = 0; + b = -invOmega * fdlibm_cos(0.5 * omega); + break; + case OscillatorType::Triangle: + // Triangle-shaped waveform going from its maximum value to + // its minimum value then back to the maximum value. + a = 0; + if (n & 1) { + b = 2 * (2 / (n * piFloat) * 2 / (n * piFloat)) * + ((((n - 1) >> 1) & 1) ? -1 : 1); + } else { + b = 0; + } + break; + default: + MOZ_ASSERT_UNREACHABLE("invalid oscillator type"); + a = 0; + b = 0; + break; + } + + realP[n] = a; + imagP[n] = b; + } +} + +} // namespace WebCore diff --git a/dom/media/webaudio/blink/PeriodicWave.h b/dom/media/webaudio/blink/PeriodicWave.h new file mode 100644 index 0000000000..fd4e50287c --- /dev/null +++ b/dom/media/webaudio/blink/PeriodicWave.h @@ -0,0 +1,123 @@ +/* + * 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. + * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of + * its contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY APPLE 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 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. + */ + +#ifndef PeriodicWave_h +#define PeriodicWave_h + +#include "mozilla/dom/OscillatorNodeBinding.h" +#include "mozilla/UniquePtr.h" +#include <nsTArray.h> +#include "AlignedTArray.h" +#include "mozilla/MemoryReporting.h" + +namespace WebCore { + +typedef AlignedTArray<float> AlignedAudioFloatArray; +typedef nsTArray<float> AudioFloatArray; + +using mozilla::UniquePtr; + +class PeriodicWave { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(WebCore::PeriodicWave); + + static already_AddRefed<PeriodicWave> createSine(float sampleRate); + static already_AddRefed<PeriodicWave> createSquare(float sampleRate); + static already_AddRefed<PeriodicWave> createSawtooth(float sampleRate); + static already_AddRefed<PeriodicWave> createTriangle(float sampleRate); + + // Creates an arbitrary periodic wave given the frequency components + // (Fourier coefficients). + static already_AddRefed<PeriodicWave> create(float sampleRate, + const float* real, + const float* imag, + size_t numberOfComponents, + bool disableNormalization); + + // Returns pointers to the lower and higher wave data for the pitch range + // containing the given fundamental frequency. These two tables are in + // adjacent "pitch" ranges where the higher table will have the maximum + // number of partials which won't alias when played back at this + // fundamental frequency. The lower wave is the next range containing fewer + // partials than the higher wave. Interpolation between these two tables + // can be made according to tableInterpolationFactor. Where values + // from 0 -> 1 interpolate between lower -> higher. + void waveDataForFundamentalFrequency(float, float*& lowerWaveData, + float*& higherWaveData, + float& tableInterpolationFactor); + + // Returns the scalar multiplier to the oscillator frequency to calculate + // wave buffer phase increment. + float rateScale() const { return m_rateScale; } + + unsigned periodicWaveSize() const { return m_periodicWaveSize; } + float sampleRate() const { return m_sampleRate; } + + size_t sizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const; + + private: + explicit PeriodicWave(float sampleRate, size_t numberOfComponents, + bool disableNormalization); + ~PeriodicWave() = default; + + void generateBasicWaveform(mozilla::dom::OscillatorType); + + float m_sampleRate; + unsigned m_periodicWaveSize; + unsigned m_numberOfRanges; + float m_centsPerRange; + unsigned m_numberOfComponents; + UniquePtr<AudioFloatArray> m_realComponents; + UniquePtr<AudioFloatArray> m_imagComponents; + + // The lowest frequency (in Hertz) where playback will include all of the + // partials. Playing back lower than this frequency will gradually lose + // more high-frequency information. + // This frequency is quite low (~10Hz @ // 44.1KHz) + float m_lowestFundamentalFrequency; + + float m_rateScale; + + unsigned numberOfRanges() const { return m_numberOfRanges; } + + // Maximum possible number of partials (before culling). + unsigned maxNumberOfPartials() const; + + unsigned numberOfPartialsForRange(unsigned rangeIndex) const; + + // Creates table for specified index based on fundamental frequency. + void createBandLimitedTables(float fundamentalFrequency, unsigned rangeIndex); + unsigned m_maxPartialsInBandLimitedTable; + float m_normalizationScale; + bool m_disableNormalization; + nsTArray<UniquePtr<AlignedAudioFloatArray> > m_bandLimitedTables; +}; + +} // namespace WebCore + +#endif // PeriodicWave_h diff --git a/dom/media/webaudio/blink/README b/dom/media/webaudio/blink/README new file mode 100644 index 0000000000..96d209dfc8 --- /dev/null +++ b/dom/media/webaudio/blink/README @@ -0,0 +1,24 @@ +This directory contains the code originally borrowed from the Blink Web Audio +implementation. We are forking the code here because in many cases the burden +of adopting Blink specific utilities is too large compared to the prospect of +importing upstream fixes by just copying newer versions of the code in the +future. + +The process of borrowing code from Blink is as follows: + +* Try to borrow utility classes only, and avoid borrowing code which depends + too much on the Blink specific utilities. +* First, import the pristine files from the Blink repository before adding + them to the build system, noting the SVN revision of Blink from which the + original files were copied in the commit message. +* In a separate commit, add the imported source files to the build system, + and apply the necessary changes to make it build successfully. +* Use the code in a separate commit. +* Never add headers as exported headers. All headers should be included + using the following convention: #include "blink/Header.h". +* Leave the imported code in the WebCore namespace, and import the needed + names into the Mozilla code via `using'. +* Cherry-pick upsteam fixes manually when needed. In case you fix a problem + that is not Mozilla specific locally, try to upstream your changes into + Blink. +* Ping ehsan for any questions. diff --git a/dom/media/webaudio/blink/Reverb.cpp b/dom/media/webaudio/blink/Reverb.cpp new file mode 100644 index 0000000000..dfce966ca9 --- /dev/null +++ b/dom/media/webaudio/blink/Reverb.cpp @@ -0,0 +1,277 @@ +/* + * Copyright (C) 2010 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. + * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of + * its contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY APPLE 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 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 "Reverb.h" +#include "ReverbConvolverStage.h" + +#include <math.h> +#include "ReverbConvolver.h" +#include "mozilla/FloatingPoint.h" + +using namespace mozilla; + +namespace WebCore { + +// Empirical gain calibration tested across many impulse responses to ensure +// perceived volume is same as dry (unprocessed) signal +const float GainCalibration = 0.00125f; +const float GainCalibrationSampleRate = 44100; + +// A minimum power value to when normalizing a silent (or very quiet) impulse +// response +const float MinPower = 0.000125f; + +static float calculateNormalizationScale(const nsTArray<const float*>& response, + size_t aLength, float sampleRate) { + // Normalize by RMS power + size_t numberOfChannels = response.Length(); + + float power = 0; + + for (size_t i = 0; i < numberOfChannels; ++i) { + float channelPower = AudioBufferSumOfSquares(response[i], aLength); + power += channelPower; + } + + power = sqrt(power / (numberOfChannels * aLength)); + + // Protect against accidental overload + if (!std::isfinite(power) || std::isnan(power) || power < MinPower) + power = MinPower; + + float scale = 1 / power; + + scale *= GainCalibration; // calibrate to make perceived volume same as + // unprocessed + + // Scale depends on sample-rate. + if (sampleRate) scale *= GainCalibrationSampleRate / sampleRate; + + // True-stereo compensation + if (numberOfChannels == 4) scale *= 0.5f; + + return scale; +} + +Reverb::Reverb(const AudioChunk& impulseResponse, size_t maxFFTSize, + bool useBackgroundThreads, bool normalize, float sampleRate, + bool* aAllocationFailure) { + MOZ_ASSERT(aAllocationFailure); + size_t impulseResponseBufferLength = impulseResponse.mDuration; + float scale = impulseResponse.mVolume; + + CopyableAutoTArray<const float*, 4> irChannels; + irChannels.AppendElements(impulseResponse.ChannelData<float>()); + AutoTArray<float, 1024> tempBuf; + + if (normalize) { + scale = calculateNormalizationScale(irChannels, impulseResponseBufferLength, + sampleRate); + } + + if (scale != 1.0f) { + bool rv = tempBuf.SetLength( + irChannels.Length() * impulseResponseBufferLength, mozilla::fallible); + *aAllocationFailure = !rv; + if (*aAllocationFailure) { + return; + } + + for (uint32_t i = 0; i < irChannels.Length(); ++i) { + float* buf = &tempBuf[i * impulseResponseBufferLength]; + AudioBufferCopyWithScale(irChannels[i], scale, buf, + impulseResponseBufferLength); + irChannels[i] = buf; + } + } + + *aAllocationFailure = !initialize(irChannels, impulseResponseBufferLength, + maxFFTSize, useBackgroundThreads); +} + +size_t Reverb::sizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const { + size_t amount = aMallocSizeOf(this); + amount += m_convolvers.ShallowSizeOfExcludingThis(aMallocSizeOf); + for (size_t i = 0; i < m_convolvers.Length(); i++) { + if (m_convolvers[i]) { + amount += m_convolvers[i]->sizeOfIncludingThis(aMallocSizeOf); + } + } + + amount += m_tempBuffer.SizeOfExcludingThis(aMallocSizeOf, false); + return amount; +} + +bool Reverb::initialize(const nsTArray<const float*>& impulseResponseBuffer, + size_t impulseResponseBufferLength, size_t maxFFTSize, + bool useBackgroundThreads) { + m_impulseResponseLength = impulseResponseBufferLength; + + // The reverb can handle a mono impulse response and still do stereo + // processing + size_t numResponseChannels = impulseResponseBuffer.Length(); + MOZ_ASSERT(numResponseChannels > 0); + // The number of convolvers required is at least the number of audio + // channels. Even if there is initially only one audio channel, another + // may be added later, and so a second convolver is created now while the + // impulse response is available. + size_t numConvolvers = std::max<size_t>(numResponseChannels, 2); + m_convolvers.SetCapacity(numConvolvers); + + int convolverRenderPhase = 0; + for (size_t i = 0; i < numConvolvers; ++i) { + size_t channelIndex = i < numResponseChannels ? i : 0; + const float* channel = impulseResponseBuffer[channelIndex]; + size_t length = impulseResponseBufferLength; + + bool allocationFailure; + UniquePtr<ReverbConvolver> convolver( + new ReverbConvolver(channel, length, maxFFTSize, convolverRenderPhase, + useBackgroundThreads, &allocationFailure)); + if (allocationFailure) { + return false; + } + m_convolvers.AppendElement(std::move(convolver)); + + convolverRenderPhase += WEBAUDIO_BLOCK_SIZE; + } + + // For "True" stereo processing we allocate a temporary buffer to avoid + // repeatedly allocating it in the process() method. It can be bad to allocate + // memory in a real-time thread. + if (numResponseChannels == 4) { + m_tempBuffer.AllocateChannels(2); + WriteZeroesToAudioBlock(&m_tempBuffer, 0, WEBAUDIO_BLOCK_SIZE); + } + return true; +} + +void Reverb::process(const AudioBlock* sourceBus, AudioBlock* destinationBus) { + // Do a fairly comprehensive sanity check. + // If these conditions are satisfied, all of the source and destination + // pointers will be valid for the various matrixing cases. + bool isSafeToProcess = + sourceBus && destinationBus && sourceBus->ChannelCount() > 0 && + destinationBus->mChannelData.Length() > 0 && + WEBAUDIO_BLOCK_SIZE <= MaxFrameSize && + WEBAUDIO_BLOCK_SIZE <= size_t(sourceBus->GetDuration()) && + WEBAUDIO_BLOCK_SIZE <= size_t(destinationBus->GetDuration()); + + MOZ_ASSERT(isSafeToProcess); + if (!isSafeToProcess) return; + + // For now only handle mono or stereo output + MOZ_ASSERT(destinationBus->ChannelCount() <= 2); + + float* destinationChannelL = + static_cast<float*>(const_cast<void*>(destinationBus->mChannelData[0])); + const float* sourceBusL = + static_cast<const float*>(sourceBus->mChannelData[0]); + + // Handle input -> output matrixing... + size_t numInputChannels = sourceBus->ChannelCount(); + size_t numOutputChannels = destinationBus->ChannelCount(); + size_t numReverbChannels = m_convolvers.Length(); + + if (numInputChannels == 2 && numReverbChannels == 2 && + numOutputChannels == 2) { + // 2 -> 2 -> 2 + const float* sourceBusR = + static_cast<const float*>(sourceBus->mChannelData[1]); + float* destinationChannelR = + static_cast<float*>(const_cast<void*>(destinationBus->mChannelData[1])); + m_convolvers[0]->process(sourceBusL, destinationChannelL); + m_convolvers[1]->process(sourceBusR, destinationChannelR); + } else if (numInputChannels == 1 && numOutputChannels == 2 && + numReverbChannels == 2) { + // 1 -> 2 -> 2 + for (int i = 0; i < 2; ++i) { + float* destinationChannel = static_cast<float*>( + const_cast<void*>(destinationBus->mChannelData[i])); + m_convolvers[i]->process(sourceBusL, destinationChannel); + } + } else if (numInputChannels == 1 && numOutputChannels == 1) { + // 1 -> 1 -> 1 (Only one of the convolvers is used.) + m_convolvers[0]->process(sourceBusL, destinationChannelL); + } else if (numInputChannels == 2 && numReverbChannels == 4 && + numOutputChannels == 2) { + // 2 -> 4 -> 2 ("True" stereo) + const float* sourceBusR = + static_cast<const float*>(sourceBus->mChannelData[1]); + float* destinationChannelR = + static_cast<float*>(const_cast<void*>(destinationBus->mChannelData[1])); + + float* tempChannelL = + static_cast<float*>(const_cast<void*>(m_tempBuffer.mChannelData[0])); + float* tempChannelR = + static_cast<float*>(const_cast<void*>(m_tempBuffer.mChannelData[1])); + + // Process left virtual source + m_convolvers[0]->process(sourceBusL, destinationChannelL); + m_convolvers[1]->process(sourceBusL, destinationChannelR); + + // Process right virtual source + m_convolvers[2]->process(sourceBusR, tempChannelL); + m_convolvers[3]->process(sourceBusR, tempChannelR); + + AudioBufferAddWithScale(tempChannelL, 1.0f, destinationChannelL, + sourceBus->GetDuration()); + AudioBufferAddWithScale(tempChannelR, 1.0f, destinationChannelR, + sourceBus->GetDuration()); + } else if (numInputChannels == 1 && numReverbChannels == 4 && + numOutputChannels == 2) { + // 1 -> 4 -> 2 (Processing mono with "True" stereo impulse response) + // This is an inefficient use of a four-channel impulse response, but we + // should handle the case. + float* destinationChannelR = + static_cast<float*>(const_cast<void*>(destinationBus->mChannelData[1])); + + float* tempChannelL = + static_cast<float*>(const_cast<void*>(m_tempBuffer.mChannelData[0])); + float* tempChannelR = + static_cast<float*>(const_cast<void*>(m_tempBuffer.mChannelData[1])); + + // Process left virtual source + m_convolvers[0]->process(sourceBusL, destinationChannelL); + m_convolvers[1]->process(sourceBusL, destinationChannelR); + + // Process right virtual source + m_convolvers[2]->process(sourceBusL, tempChannelL); + m_convolvers[3]->process(sourceBusL, tempChannelR); + + AudioBufferAddWithScale(tempChannelL, 1.0f, destinationChannelL, + sourceBus->GetDuration()); + AudioBufferAddWithScale(tempChannelR, 1.0f, destinationChannelR, + sourceBus->GetDuration()); + } else { + MOZ_ASSERT_UNREACHABLE("Unexpected Reverb configuration"); + destinationBus->SetNull(destinationBus->GetDuration()); + } +} + +} // namespace WebCore diff --git a/dom/media/webaudio/blink/Reverb.h b/dom/media/webaudio/blink/Reverb.h new file mode 100644 index 0000000000..16459532a9 --- /dev/null +++ b/dom/media/webaudio/blink/Reverb.h @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2010 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. + * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of + * its contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY APPLE 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 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. + */ + +#ifndef Reverb_h +#define Reverb_h + +#include "ReverbConvolver.h" +#include "nsTArray.h" +#include "AudioBlock.h" +#include "mozilla/MemoryReporting.h" +#include "mozilla/UniquePtr.h" + +namespace WebCore { + +using mozilla::UniquePtr; + +// Multi-channel convolution reverb with channel matrixing - one or more +// ReverbConvolver objects are used internally. + +class Reverb { + public: + enum { MaxFrameSize = 256 }; + + // renderSliceSize is a rendering hint, so the FFTs can be optimized to not + // all occur at the same time (very bad when rendering on a real-time thread). + // aAllocation failure is to be checked by the caller. If false, internal + // memory could not be allocated, and this Reverb instance is not to be + // used. + Reverb(const mozilla::AudioChunk& impulseResponseBuffer, size_t maxFFTSize, + bool useBackgroundThreads, bool normalize, float sampleRate, + bool* aAllocationFailure); + + void process(const mozilla::AudioBlock* sourceBus, + mozilla::AudioBlock* destinationBus); + + size_t impulseResponseLength() const { return m_impulseResponseLength; } + + size_t sizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const; + + private: + bool initialize(const nsTArray<const float*>& impulseResponseBuffer, + size_t impulseResponseBufferLength, size_t maxFFTSize, + bool useBackgroundThreads); + + size_t m_impulseResponseLength; + + nsTArray<UniquePtr<ReverbConvolver> > m_convolvers; + + // For "True" stereo processing + mozilla::AudioBlock m_tempBuffer; +}; + +} // namespace WebCore + +#endif // Reverb_h diff --git a/dom/media/webaudio/blink/ReverbAccumulationBuffer.cpp b/dom/media/webaudio/blink/ReverbAccumulationBuffer.cpp new file mode 100644 index 0000000000..ec745c294f --- /dev/null +++ b/dom/media/webaudio/blink/ReverbAccumulationBuffer.cpp @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2010 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. + * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of + * its contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY APPLE 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 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 "ReverbAccumulationBuffer.h" +#include "AudioNodeEngine.h" +#include "mozilla/PodOperations.h" +#include <algorithm> + +using namespace mozilla; + +namespace WebCore { + +ReverbAccumulationBuffer::ReverbAccumulationBuffer() + : m_readIndex(0), m_readTimeFrame(0) {} + +bool ReverbAccumulationBuffer::allocate(size_t length) { + if (!m_buffer.SetLength(length, fallible)) { + return false; + } + PodZero(m_buffer.Elements(), length); + return true; +} + +void ReverbAccumulationBuffer::readAndClear(float* destination, + size_t numberOfFrames) { + size_t bufferLength = m_buffer.Length(); + bool isCopySafe = + m_readIndex <= bufferLength && numberOfFrames <= bufferLength; + + MOZ_ASSERT(isCopySafe); + if (!isCopySafe) return; + + size_t framesAvailable = bufferLength - m_readIndex; + size_t numberOfFrames1 = std::min(numberOfFrames, framesAvailable); + size_t numberOfFrames2 = numberOfFrames - numberOfFrames1; + + float* source = m_buffer.Elements(); + memcpy(destination, source + m_readIndex, sizeof(float) * numberOfFrames1); + memset(source + m_readIndex, 0, sizeof(float) * numberOfFrames1); + + // Handle wrap-around if necessary + if (numberOfFrames2 > 0) { + memcpy(destination + numberOfFrames1, source, + sizeof(float) * numberOfFrames2); + memset(source, 0, sizeof(float) * numberOfFrames2); + } + + m_readIndex = (m_readIndex + numberOfFrames) % bufferLength; + m_readTimeFrame += numberOfFrames; +} + +void ReverbAccumulationBuffer::accumulate(const float* source, + size_t numberOfFrames, + size_t* readIndex, + size_t delayFrames) { + size_t bufferLength = m_buffer.Length(); + + size_t writeIndex = (*readIndex + delayFrames) % bufferLength; + + // Update caller's readIndex + *readIndex = (*readIndex + numberOfFrames) % bufferLength; + + size_t framesAvailable = bufferLength - writeIndex; + size_t numberOfFrames1 = std::min(numberOfFrames, framesAvailable); + size_t numberOfFrames2 = numberOfFrames - numberOfFrames1; + + float* destination = m_buffer.Elements(); + + bool isSafe = writeIndex <= bufferLength && + numberOfFrames1 + writeIndex <= bufferLength && + numberOfFrames2 <= bufferLength; + MOZ_ASSERT(isSafe); + if (!isSafe) return; + + AudioBufferAddWithScale(source, 1.0f, destination + writeIndex, + numberOfFrames1); + if (numberOfFrames2 > 0) { + AudioBufferAddWithScale(source + numberOfFrames1, 1.0f, destination, + numberOfFrames2); + } +} + +void ReverbAccumulationBuffer::reset() { + PodZero(m_buffer.Elements(), m_buffer.Length()); + m_readIndex = 0; + m_readTimeFrame = 0; +} + +} // namespace WebCore diff --git a/dom/media/webaudio/blink/ReverbAccumulationBuffer.h b/dom/media/webaudio/blink/ReverbAccumulationBuffer.h new file mode 100644 index 0000000000..8883a56a73 --- /dev/null +++ b/dom/media/webaudio/blink/ReverbAccumulationBuffer.h @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2010 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. + * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of + * its contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY APPLE 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 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. + */ + +#ifndef ReverbAccumulationBuffer_h +#define ReverbAccumulationBuffer_h + +#include "AlignedTArray.h" +#include "mozilla/MemoryReporting.h" + +namespace WebCore { + +// ReverbAccumulationBuffer is a circular delay buffer with one client reading +// from it and multiple clients writing/accumulating to it at different delay +// offsets from the read position. The read operation will zero the memory just +// read from the buffer, so it will be ready for accumulation the next time +// around. +class ReverbAccumulationBuffer { + public: + ReverbAccumulationBuffer(); + // Returns false on failure. + bool allocate(size_t length); + + // This will read from, then clear-out numberOfFrames + void readAndClear(float* destination, size_t numberOfFrames); + + // Each ReverbConvolverStage will accumulate its output at the appropriate + // delay from the read position. We need to pass in and update readIndex here, + // since each ReverbConvolverStage may be running in a different thread than + // the realtime thread calling ReadAndClear() and maintaining m_readIndex + void accumulate(const float* source, size_t numberOfFrames, size_t* readIndex, + size_t delayFrames); + + size_t readIndex() const { return m_readIndex; } + + size_t readTimeFrame() const { return m_readTimeFrame; } + + void reset(); + + size_t sizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const { + return m_buffer.ShallowSizeOfExcludingThis(aMallocSizeOf); + } + + private: + AlignedTArray<float, 16> m_buffer; + size_t m_readIndex; + size_t m_readTimeFrame; // for debugging (frame on continuous timeline) +}; + +} // namespace WebCore + +#endif // ReverbAccumulationBuffer_h diff --git a/dom/media/webaudio/blink/ReverbConvolver.cpp b/dom/media/webaudio/blink/ReverbConvolver.cpp new file mode 100644 index 0000000000..dcaf6e7467 --- /dev/null +++ b/dom/media/webaudio/blink/ReverbConvolver.cpp @@ -0,0 +1,272 @@ +/* + * Copyright (C) 2010 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. + * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of + * its contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY APPLE 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 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 "ReverbConvolver.h" +#include "ReverbConvolverStage.h" + +using namespace mozilla; + +namespace WebCore { + +const int InputBufferSize = 8 * 16384; + +// We only process the leading portion of the impulse response in the real-time +// thread. We don't exceed this length. It turns out then, that the background +// thread has about 278msec of scheduling slop. Empirically, this has been found +// to be a good compromise between giving enough time for scheduling slop, while +// still minimizing the amount of processing done in the primary (high-priority) +// thread. This was found to be a good value on Mac OS X, and may work well on +// other platforms as well, assuming the very rough scheduling latencies are +// similar on these time-scales. Of course, this code may need to be tuned for +// individual platforms if this assumption is found to be incorrect. +const size_t RealtimeFrameLimit = 8192 + 4096 // ~278msec @ 44.1KHz + - WEBAUDIO_BLOCK_SIZE; +// First stage will have size MinFFTSize - successive stages will double in +// size each time until we hit the maximum size. +const size_t MinFFTSize = 256; +// If we are using background threads then don't exceed this FFT size for the +// stages which run in the real-time thread. This avoids having only one or +// two large stages (size 16384 or so) at the end which take a lot of time +// every several processing slices. This way we amortize the cost over more +// processing slices. +const size_t MaxRealtimeFFTSize = 4096; + +ReverbConvolver::ReverbConvolver(const float* impulseResponseData, + size_t impulseResponseLength, + size_t maxFFTSize, size_t convolverRenderPhase, + bool useBackgroundThreads, + bool* aAllocationFailure) + : m_impulseResponseLength(impulseResponseLength), + m_inputBuffer(InputBufferSize), + m_backgroundThread("ConvolverWorker"), + m_backgroundThreadMonitor("ConvolverMonitor"), + m_useBackgroundThreads(useBackgroundThreads), + m_wantsToExit(false), + m_moreInputBuffered(false) { + *aAllocationFailure = !m_accumulationBuffer.allocate(impulseResponseLength + + WEBAUDIO_BLOCK_SIZE); + if (*aAllocationFailure) { + return; + } + // For the moment, a good way to know if we have real-time constraint is to + // check if we're using background threads. Otherwise, assume we're being run + // from a command-line tool. + bool hasRealtimeConstraint = useBackgroundThreads; + + const float* response = impulseResponseData; + size_t totalResponseLength = impulseResponseLength; + + // The total latency is zero because the first FFT stage is small enough + // to return output in the first block. + size_t reverbTotalLatency = 0; + + size_t stageOffset = 0; + size_t stagePhase = 0; + size_t fftSize = MinFFTSize; + while (stageOffset < totalResponseLength) { + size_t stageSize = fftSize / 2; + + // For the last stage, it's possible that stageOffset is such that we're + // straddling the end of the impulse response buffer (if we use stageSize), + // so reduce the last stage's length... + if (stageSize + stageOffset > totalResponseLength) { + stageSize = totalResponseLength - stageOffset; + // Use smallest FFT that is large enough to cover the last stage. + fftSize = MinFFTSize; + while (stageSize * 2 > fftSize) { + fftSize *= 2; + } + } + + // This "staggers" the time when each FFT happens so they don't all happen + // at the same time + int renderPhase = convolverRenderPhase + stagePhase; + + UniquePtr<ReverbConvolverStage> stage(new ReverbConvolverStage( + response, totalResponseLength, reverbTotalLatency, stageOffset, + stageSize, fftSize, renderPhase, &m_accumulationBuffer)); + + bool isBackgroundStage = false; + + if (this->useBackgroundThreads() && stageOffset > RealtimeFrameLimit) { + m_backgroundStages.AppendElement(std::move(stage)); + isBackgroundStage = true; + } else + m_stages.AppendElement(std::move(stage)); + + // Figure out next FFT size + fftSize *= 2; + + stageOffset += stageSize; + + if (hasRealtimeConstraint && !isBackgroundStage && + fftSize > MaxRealtimeFFTSize) { + fftSize = MaxRealtimeFFTSize; + // Custom phase positions for all but the first of the realtime + // stages of largest size. These spread out the work of the + // larger realtime stages. None of the FFTs of size 1024, 2048 or + // 4096 are performed when processing the same block. The first + // MaxRealtimeFFTSize = 4096 stage, at the end of the doubling, + // performs its FFT at block 7. The FFTs of size 2048 are + // performed in blocks 3 + 8 * n and size 1024 at 1 + 4 * n. + const uint32_t phaseLookup[] = {14, 0, 10, 4}; + stagePhase = WEBAUDIO_BLOCK_SIZE * + phaseLookup[m_stages.Length() % ArrayLength(phaseLookup)]; + } else if (fftSize > maxFFTSize) { + fftSize = maxFFTSize; + // A prime offset spreads out FFTs in a way that all + // available phase positions will be used if there are sufficient + // stages. + stagePhase += 5 * WEBAUDIO_BLOCK_SIZE; + } else if (stageSize > WEBAUDIO_BLOCK_SIZE) { + // As the stages are doubling in size, the next FFT will occur + // mid-way between FFTs for this stage. + stagePhase = stageSize - WEBAUDIO_BLOCK_SIZE; + } + } + + // Start up background thread + // FIXME: would be better to up the thread priority here. It doesn't need to + // be real-time, but higher than the default... + if (this->useBackgroundThreads() && m_backgroundStages.Length() > 0) { + if (!m_backgroundThread.Start()) { + NS_WARNING("Cannot start convolver thread."); + return; + } + m_backgroundThread.message_loop()->PostTask(NewNonOwningRunnableMethod( + "WebCore::ReverbConvolver::backgroundThreadEntry", this, + &ReverbConvolver::backgroundThreadEntry)); + } +} + +ReverbConvolver::~ReverbConvolver() { + // Wait for background thread to stop + if (useBackgroundThreads() && m_backgroundThread.IsRunning()) { + m_wantsToExit = true; + + // Wake up thread so it can return + { + MonitorAutoLock locker(m_backgroundThreadMonitor); + m_moreInputBuffered = true; + m_backgroundThreadMonitor.Notify(); + } + + m_backgroundThread.Stop(); + } +} + +size_t ReverbConvolver::sizeOfIncludingThis( + mozilla::MallocSizeOf aMallocSizeOf) const { + size_t amount = aMallocSizeOf(this); + amount += m_stages.ShallowSizeOfExcludingThis(aMallocSizeOf); + for (size_t i = 0; i < m_stages.Length(); i++) { + if (m_stages[i]) { + amount += m_stages[i]->sizeOfIncludingThis(aMallocSizeOf); + } + } + + amount += m_backgroundStages.ShallowSizeOfExcludingThis(aMallocSizeOf); + for (size_t i = 0; i < m_backgroundStages.Length(); i++) { + if (m_backgroundStages[i]) { + amount += m_backgroundStages[i]->sizeOfIncludingThis(aMallocSizeOf); + } + } + + // NB: The buffer sizes are static, so even though they might be accessed + // in another thread it's safe to measure them. + amount += m_accumulationBuffer.sizeOfExcludingThis(aMallocSizeOf); + amount += m_inputBuffer.sizeOfExcludingThis(aMallocSizeOf); + + // Possible future measurements: + // - m_backgroundThread + // - m_backgroundThreadMonitor + return amount; +} + +void ReverbConvolver::backgroundThreadEntry() { + while (!m_wantsToExit) { + // Wait for realtime thread to give us more input + m_moreInputBuffered = false; + { + MonitorAutoLock locker(m_backgroundThreadMonitor); + while (!m_moreInputBuffered && !m_wantsToExit) + m_backgroundThreadMonitor.Wait(); + } + + // Process all of the stages until their read indices reach the input + // buffer's write index + int writeIndex = m_inputBuffer.writeIndex(); + + // Even though it doesn't seem like every stage needs to maintain its own + // version of readIndex we do this in case we want to run in more than one + // background thread. + int readIndex; + + while ((readIndex = m_backgroundStages[0]->inputReadIndex()) != + writeIndex) { // FIXME: do better to detect buffer overrun... + // Accumulate contributions from each stage + for (size_t i = 0; i < m_backgroundStages.Length(); ++i) + m_backgroundStages[i]->processInBackground(this); + } + } +} + +void ReverbConvolver::process(const float* sourceChannelData, + float* destinationChannelData) { + const float* source = sourceChannelData; + float* destination = destinationChannelData; + bool isDataSafe = source && destination; + MOZ_ASSERT(isDataSafe); + if (!isDataSafe) return; + + // Feed input buffer (read by all threads) + m_inputBuffer.write(source, WEBAUDIO_BLOCK_SIZE); + + // Accumulate contributions from each stage + for (size_t i = 0; i < m_stages.Length(); ++i) m_stages[i]->process(source); + + // Finally read from accumulation buffer + m_accumulationBuffer.readAndClear(destination, WEBAUDIO_BLOCK_SIZE); + + // Now that we've buffered more input, wake up our background thread. + + // Not using a MonitorAutoLock looks strange, but we use a TryLock() instead + // because this is run on the real-time thread where it is a disaster for the + // lock to be contended (causes audio glitching). It's OK if we fail to + // signal from time to time, since we'll get to it the next time we're called. + // We're called repeatedly and frequently (around every 3ms). The background + // thread is processing well into the future and has a considerable amount of + // leeway here... + if (m_backgroundThreadMonitor.TryLock()) { + m_moreInputBuffered = true; + m_backgroundThreadMonitor.Notify(); + m_backgroundThreadMonitor.Unlock(); + } +} + +} // namespace WebCore diff --git a/dom/media/webaudio/blink/ReverbConvolver.h b/dom/media/webaudio/blink/ReverbConvolver.h new file mode 100644 index 0000000000..8000ba811e --- /dev/null +++ b/dom/media/webaudio/blink/ReverbConvolver.h @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2010 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. + * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of + * its contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY APPLE 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 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. + */ + +#ifndef ReverbConvolver_h +#define ReverbConvolver_h + +#include "ReverbAccumulationBuffer.h" +#include "ReverbInputBuffer.h" +#include "mozilla/MemoryReporting.h" +#include "mozilla/Monitor.h" +#include "mozilla/UniquePtr.h" +#ifdef LOG +# undef LOG +#endif +#include "base/thread.h" +#include <atomic> + +namespace WebCore { + +using mozilla::UniquePtr; + +class ReverbConvolverStage; + +class ReverbConvolver { + public: + // maxFFTSize can be adjusted (from say 2048 to 32768) depending on how much + // precision is necessary. For certain tweaky de-convolving applications the + // phase errors add up quickly and lead to non-sensical results with larger + // FFT sizes and single-precision floats. In these cases 2048 is a good size. + // If not doing multi-threaded convolution, then should not go > 8192. + ReverbConvolver(const float* impulseResponseData, + size_t impulseResponseLength, size_t maxFFTSize, + size_t convolverRenderPhase, bool useBackgroundThreads, + bool* aAllocationFailure); + ~ReverbConvolver(); + + void process(const float* sourceChannelData, float* destinationChannelData); + + size_t impulseResponseLength() const { return m_impulseResponseLength; } + + ReverbInputBuffer* inputBuffer() { return &m_inputBuffer; } + + bool useBackgroundThreads() const { return m_useBackgroundThreads; } + void backgroundThreadEntry(); + + size_t sizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const; + + private: + nsTArray<UniquePtr<ReverbConvolverStage> > m_stages; + nsTArray<UniquePtr<ReverbConvolverStage> > m_backgroundStages; + size_t m_impulseResponseLength; + + ReverbAccumulationBuffer m_accumulationBuffer; + + // One or more background threads read from this input buffer which is fed + // from the realtime thread. + ReverbInputBuffer m_inputBuffer; + + // Background thread and synchronization + base::Thread m_backgroundThread; + mozilla::Monitor m_backgroundThreadMonitor MOZ_UNANNOTATED; + bool m_useBackgroundThreads; + std::atomic<bool> m_wantsToExit; + std::atomic<bool> m_moreInputBuffered; +}; + +} // namespace WebCore + +#endif // ReverbConvolver_h diff --git a/dom/media/webaudio/blink/ReverbConvolverStage.cpp b/dom/media/webaudio/blink/ReverbConvolverStage.cpp new file mode 100644 index 0000000000..56354f0271 --- /dev/null +++ b/dom/media/webaudio/blink/ReverbConvolverStage.cpp @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2010 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. + * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of + * its contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY APPLE 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 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 "ReverbConvolverStage.h" + +#include "ReverbAccumulationBuffer.h" +#include "ReverbConvolver.h" +#include "ReverbInputBuffer.h" +#include "mozilla/PodOperations.h" + +using namespace mozilla; + +namespace WebCore { + +ReverbConvolverStage::ReverbConvolverStage( + const float* impulseResponse, size_t, size_t reverbTotalLatency, + size_t stageOffset, size_t stageLength, size_t fftSize, size_t renderPhase, + ReverbAccumulationBuffer* accumulationBuffer) + : m_accumulationBuffer(accumulationBuffer), + m_accumulationReadIndex(0), + m_inputReadIndex(0) { + MOZ_ASSERT(impulseResponse); + MOZ_ASSERT(accumulationBuffer); + + m_fftKernel = MakeUnique<FFTBlock>(fftSize); + m_fftKernel->PadAndMakeScaledDFT(impulseResponse + stageOffset, stageLength); + m_fftConvolver = MakeUnique<FFTConvolver>(fftSize, renderPhase); + + // The convolution stage at offset stageOffset needs to have a corresponding + // delay to cancel out the offset. + size_t totalDelay = stageOffset + reverbTotalLatency; + + // But, the FFT convolution itself incurs latency, so subtract this out... + size_t fftLatency = m_fftConvolver->latencyFrames(); + MOZ_ASSERT(totalDelay >= fftLatency); + totalDelay -= fftLatency; + + m_postDelayLength = totalDelay; +} + +size_t ReverbConvolverStage::sizeOfIncludingThis( + mozilla::MallocSizeOf aMallocSizeOf) const { + size_t amount = aMallocSizeOf(this); + + if (m_fftKernel) { + amount += m_fftKernel->SizeOfIncludingThis(aMallocSizeOf); + } + + if (m_fftConvolver) { + amount += m_fftConvolver->sizeOfIncludingThis(aMallocSizeOf); + } + + return amount; +} + +void ReverbConvolverStage::processInBackground(ReverbConvolver* convolver) { + ReverbInputBuffer* inputBuffer = convolver->inputBuffer(); + float* source = + inputBuffer->directReadFrom(&m_inputReadIndex, WEBAUDIO_BLOCK_SIZE); + process(source); +} + +void ReverbConvolverStage::process(const float* source) { + MOZ_ASSERT(source); + if (!source) return; + + // Now, run the convolution (into the delay buffer). + // An expensive FFT will happen every fftSize / 2 frames. + const float* output = m_fftConvolver->process(m_fftKernel.get(), source); + + // Now accumulate into reverb's accumulation buffer. + m_accumulationBuffer->accumulate(output, WEBAUDIO_BLOCK_SIZE, + &m_accumulationReadIndex, m_postDelayLength); +} + +} // namespace WebCore diff --git a/dom/media/webaudio/blink/ReverbConvolverStage.h b/dom/media/webaudio/blink/ReverbConvolverStage.h new file mode 100644 index 0000000000..f770e76d45 --- /dev/null +++ b/dom/media/webaudio/blink/ReverbConvolverStage.h @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2010 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. + * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of + * its contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY APPLE 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 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. + */ + +#ifndef ReverbConvolverStage_h +#define ReverbConvolverStage_h + +#include "FFTConvolver.h" + +#include "nsTArray.h" +#include "mozilla/FFTBlock.h" +#include "mozilla/MemoryReporting.h" +#include "mozilla/UniquePtr.h" + +namespace WebCore { + +using mozilla::FFTBlock; + +class ReverbAccumulationBuffer; +class ReverbConvolver; + +// A ReverbConvolverStage represents the convolution associated with a +// sub-section of a large impulse response. It incorporates a delay line to +// account for the offset of the sub-section within the larger impulse response. +class ReverbConvolverStage { + public: + // renderPhase is useful to know so that we can manipulate the pre versus post + // delay so that stages will perform their heavy work (FFT processing) on + // different slices to balance the load in a real-time thread. + ReverbConvolverStage(const float* impulseResponse, size_t responseLength, + size_t reverbTotalLatency, size_t stageOffset, + size_t stageLength, size_t fftSize, size_t renderPhase, + ReverbAccumulationBuffer*); + + // |source| must point to an array of WEBAUDIO_BLOCK_SIZE elements. + void process(const float* source); + + void processInBackground(ReverbConvolver* convolver); + + // Useful for background processing + int inputReadIndex() const { return m_inputReadIndex; } + + size_t sizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const; + + private: + mozilla::UniquePtr<FFTBlock> m_fftKernel; + mozilla::UniquePtr<FFTConvolver> m_fftConvolver; + + ReverbAccumulationBuffer* m_accumulationBuffer; + size_t m_accumulationReadIndex; + int m_inputReadIndex; + + size_t m_postDelayLength; + + nsTArray<float> m_temporaryBuffer; +}; + +} // namespace WebCore + +#endif // ReverbConvolverStage_h diff --git a/dom/media/webaudio/blink/ReverbInputBuffer.cpp b/dom/media/webaudio/blink/ReverbInputBuffer.cpp new file mode 100644 index 0000000000..d6a69263c9 --- /dev/null +++ b/dom/media/webaudio/blink/ReverbInputBuffer.cpp @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2010 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. + * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of + * its contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY APPLE 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 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 "ReverbInputBuffer.h" +#include "mozilla/PodOperations.h" + +using namespace mozilla; + +namespace WebCore { + +ReverbInputBuffer::ReverbInputBuffer(size_t length) : m_writeIndex(0) { + m_buffer.SetLength(length); + PodZero(m_buffer.Elements(), length); +} + +void ReverbInputBuffer::write(const float* sourceP, size_t numberOfFrames) { + // m_writeIndex is atomic and checked by other threads, so only touch + // it at the start and end. + size_t bufferLength = m_buffer.Length(); + size_t index = m_writeIndex; + size_t newIndex = index + numberOfFrames; + + MOZ_RELEASE_ASSERT(newIndex <= bufferLength); + + memcpy(m_buffer.Elements() + index, sourceP, sizeof(float) * numberOfFrames); + + if (newIndex >= bufferLength) { + newIndex = 0; + } + m_writeIndex = newIndex; +} + +float* ReverbInputBuffer::directReadFrom(int* readIndex, + size_t numberOfFrames) { + size_t bufferLength = m_buffer.Length(); + bool isPointerGood = readIndex && *readIndex >= 0 && + *readIndex + numberOfFrames <= bufferLength; + MOZ_ASSERT(isPointerGood); + if (!isPointerGood) { + // Should never happen in practice but return pointer to start of buffer + // (avoid crash) + if (readIndex) *readIndex = 0; + return m_buffer.Elements(); + } + + float* sourceP = m_buffer.Elements(); + float* p = sourceP + *readIndex; + + // Update readIndex + *readIndex = (*readIndex + numberOfFrames) % bufferLength; + + return p; +} + +void ReverbInputBuffer::reset() { + PodZero(m_buffer.Elements(), m_buffer.Length()); + m_writeIndex = 0; +} + +} // namespace WebCore diff --git a/dom/media/webaudio/blink/ReverbInputBuffer.h b/dom/media/webaudio/blink/ReverbInputBuffer.h new file mode 100644 index 0000000000..93ef97a010 --- /dev/null +++ b/dom/media/webaudio/blink/ReverbInputBuffer.h @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2010 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. + * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of + * its contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY APPLE 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 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. + */ + +#ifndef ReverbInputBuffer_h +#define ReverbInputBuffer_h + +#include "nsTArray.h" +#include "mozilla/Atomics.h" +#include "mozilla/MemoryReporting.h" + +namespace WebCore { + +// ReverbInputBuffer is used to buffer input samples for deferred processing by +// the background threads. +class ReverbInputBuffer { + public: + explicit ReverbInputBuffer(size_t length); + + // The realtime audio thread keeps writing samples here. + // The assumption is that the buffer's length is evenly divisible by + // numberOfFrames (for nearly all cases this will be fine). + // FIXME: remove numberOfFrames restriction... + void write(const float* sourceP, size_t numberOfFrames); + + // Background threads can call this to check if there's anything to read... + size_t writeIndex() const { return m_writeIndex; } + + // The individual background threads read here (and hope that they can keep up + // with the buffer writing). readIndex is updated with the next readIndex to + // read from... The assumption is that the buffer's length is evenly divisible + // by numberOfFrames. + // FIXME: remove numberOfFrames restriction... + float* directReadFrom(int* readIndex, size_t numberOfFrames); + + void reset(); + + size_t sizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const { + return m_buffer.ShallowSizeOfExcludingThis(aMallocSizeOf); + } + + private: + nsTArray<float> m_buffer; + mozilla::Atomic<size_t, mozilla::ReleaseAcquire> m_writeIndex; +}; + +} // namespace WebCore + +#endif // ReverbInputBuffer_h diff --git a/dom/media/webaudio/blink/ZeroPole.cpp b/dom/media/webaudio/blink/ZeroPole.cpp new file mode 100644 index 0000000000..4d0888d758 --- /dev/null +++ b/dom/media/webaudio/blink/ZeroPole.cpp @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2011 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. + * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of + * its contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY APPLE 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 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 "ZeroPole.h" + +#include "DenormalDisabler.h" + +#include <cmath> +#include <float.h> + +namespace WebCore { + +void ZeroPole::process(const float* source, float* destination, + int framesToProcess) { + float zero = m_zero; + float pole = m_pole; + + // Gain compensation to make 0dB @ 0Hz + const float k1 = 1 / (1 - zero); + const float k2 = 1 - pole; + + // Member variables to locals. + float lastX = m_lastX; + float lastY = m_lastY; + + for (int i = 0; i < framesToProcess; ++i) { + float input = source[i]; + + // Zero + float output1 = k1 * (input - zero * lastX); + lastX = input; + + // Pole + float output2 = k2 * output1 + pole * lastY; + lastY = output2; + + destination[i] = output2; + } + +// Locals to member variables. Flush denormals here so we don't +// slow down the inner loop above. +#ifndef HAVE_DENORMAL + if (lastX == 0.0f && lastY != 0.0f && fabsf(lastY) < FLT_MIN) { + // Flush future values to zero (until there is new input). + lastY = 0.0; + + // Flush calculated values. + for (int i = framesToProcess; i-- && fabsf(destination[i]) < FLT_MIN;) { + destination[i] = 0.0f; + } + } +#endif + + m_lastX = lastX; + m_lastY = lastY; +} + +} // namespace WebCore diff --git a/dom/media/webaudio/blink/ZeroPole.h b/dom/media/webaudio/blink/ZeroPole.h new file mode 100644 index 0000000000..07f3f04f53 --- /dev/null +++ b/dom/media/webaudio/blink/ZeroPole.h @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2011 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. + * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of + * its contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY APPLE 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 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. + */ + +#ifndef ZeroPole_h +#define ZeroPole_h + +namespace WebCore { + +// ZeroPole is a simple filter with one zero and one pole. + +class ZeroPole { + public: + ZeroPole() : m_zero(0), m_pole(0), m_lastX(0), m_lastY(0) {} + + void process(const float* source, float* destination, int framesToProcess); + + // Reset filter state. + void reset() { + m_lastX = 0; + m_lastY = 0; + } + + void setZero(float zero) { m_zero = zero; } + void setPole(float pole) { m_pole = pole; } + + float zero() const { return m_zero; } + float pole() const { return m_pole; } + + private: + float m_zero; + float m_pole; + float m_lastX; + float m_lastY; +}; + +} // namespace WebCore + +#endif // ZeroPole_h diff --git a/dom/media/webaudio/blink/moz.build b/dom/media/webaudio/blink/moz.build new file mode 100644 index 0000000000..5f00d01ed3 --- /dev/null +++ b/dom/media/webaudio/blink/moz.build @@ -0,0 +1,36 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +UNIFIED_SOURCES += [ + "Biquad.cpp", + "DynamicsCompressor.cpp", + "DynamicsCompressorKernel.cpp", + "FFTConvolver.cpp", + "HRTFDatabase.cpp", + "HRTFDatabaseLoader.cpp", + "HRTFElevation.cpp", + "HRTFKernel.cpp", + "HRTFPanner.cpp", + "IIRFilter.cpp", + "PeriodicWave.cpp", + "Reverb.cpp", + "ReverbAccumulationBuffer.cpp", + "ReverbConvolver.cpp", + "ReverbConvolverStage.cpp", + "ReverbInputBuffer.cpp", + "ZeroPole.cpp", +] + +# Are we targeting x86 or x64? If so, build SSE2 files. +if CONFIG["INTEL_ARCHITECTURE"]: + DEFINES["USE_SSE2"] = True + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" +LOCAL_INCLUDES += [ + "/dom/media/webaudio", +] diff --git a/dom/media/webaudio/moz.build b/dom/media/webaudio/moz.build new file mode 100644 index 0000000000..2e82d3daa8 --- /dev/null +++ b/dom/media/webaudio/moz.build @@ -0,0 +1,153 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +with Files("*"): + BUG_COMPONENT = ("Core", "Web Audio") + +DIRS += ["blink"] + +MOCHITEST_MANIFESTS += [ + "test/blink/mochitest.toml", + "test/mochitest.toml", + "test/mochitest_audio.toml", + "test/mochitest_bugs.toml", + "test/mochitest_media.toml", +] + +TEST_HARNESS_FILES.testing.mochitest.tests.dom.media.webaudio.test.blink += [ + "test/blink/audio-testing.js", + "test/blink/convolution-testing.js", + "test/blink/panner-model-testing.js", +] + +EXPORTS += [ + "AlignedTArray.h", + "AudioBlock.h", + "AudioEventTimeline.h", + "AudioNodeEngine.h", + "AudioNodeExternalInputTrack.h", + "AudioNodeTrack.h", + "AudioParamTimeline.h", + "MediaBufferDecoder.h", + "ThreeDPoint.h", + "WebAudioUtils.h", +] + +EXPORTS.mozilla += [ + "FFTBlock.h", + "MediaStreamAudioDestinationNode.h", +] + +EXPORTS.mozilla.dom += [ + "AnalyserNode.h", + "AudioBuffer.h", + "AudioBufferSourceNode.h", + "AudioContext.h", + "AudioDestinationNode.h", + "AudioListener.h", + "AudioNode.h", + "AudioParam.h", + "AudioParamDescriptorMap.h", + "AudioParamMap.h", + "AudioProcessingEvent.h", + "AudioScheduledSourceNode.h", + "AudioWorkletGlobalScope.h", + "AudioWorkletNode.h", + "AudioWorkletProcessor.h", + "BiquadFilterNode.h", + "ChannelMergerNode.h", + "ChannelSplitterNode.h", + "ConstantSourceNode.h", + "ConvolverNode.h", + "DelayNode.h", + "DynamicsCompressorNode.h", + "GainNode.h", + "IIRFilterNode.h", + "MediaElementAudioSourceNode.h", + "MediaStreamAudioDestinationNode.h", + "MediaStreamAudioSourceNode.h", + "MediaStreamTrackAudioSourceNode.h", + "OscillatorNode.h", + "PannerNode.h", + "PeriodicWave.h", + "ScriptProcessorNode.h", + "StereoPannerNode.h", + "WaveShaperNode.h", +] + +UNIFIED_SOURCES += [ + "AnalyserNode.cpp", + "AudioBlock.cpp", + "AudioBuffer.cpp", + "AudioBufferSourceNode.cpp", + "AudioContext.cpp", + "AudioDestinationNode.cpp", + "AudioEventTimeline.cpp", + "AudioListener.cpp", + "AudioNode.cpp", + "AudioNodeEngine.cpp", + "AudioNodeExternalInputTrack.cpp", + "AudioNodeTrack.cpp", + "AudioParam.cpp", + "AudioParamMap.cpp", + "AudioProcessingEvent.cpp", + "AudioScheduledSourceNode.cpp", + "AudioWorkletGlobalScope.cpp", + "AudioWorkletImpl.cpp", + "AudioWorkletNode.cpp", + "AudioWorkletProcessor.cpp", + "BiquadFilterNode.cpp", + "ChannelMergerNode.cpp", + "ChannelSplitterNode.cpp", + "ConstantSourceNode.cpp", + "ConvolverNode.cpp", + "DelayBuffer.cpp", + "DelayNode.cpp", + "DynamicsCompressorNode.cpp", + "FFTBlock.cpp", + "GainNode.cpp", + "IIRFilterNode.cpp", + "MediaBufferDecoder.cpp", + "MediaElementAudioSourceNode.cpp", + "MediaStreamAudioDestinationNode.cpp", + "MediaStreamAudioSourceNode.cpp", + "MediaStreamTrackAudioSourceNode.cpp", + "OscillatorNode.cpp", + "PannerNode.cpp", + "PeriodicWave.cpp", + "ScriptProcessorNode.cpp", + "StereoPannerNode.cpp", + "ThreeDPoint.cpp", + "WaveShaperNode.cpp", + "WebAudioUtils.cpp", +] + +if CONFIG["TARGET_CPU"] == "aarch64" or CONFIG["BUILD_ARM_NEON"]: + DEFINES["USE_NEON"] = True + LOCAL_INCLUDES += ["/third_party/xsimd/include"] + SOURCES += ["AudioNodeEngineNEON.cpp"] + SOURCES["AudioNodeEngineNEON.cpp"].flags += CONFIG["NEON_FLAGS"] + if CONFIG["BUILD_ARM_NEON"]: + LOCAL_INCLUDES += ["/media/openmax_dl/dl/api/"] + +# Are we targeting x86 or x64? If so, build SSEX files. +if CONFIG["INTEL_ARCHITECTURE"]: + DEFINES["USE_SSE2"] = True + SOURCES += ["AudioNodeEngineSSE2.cpp"] + LOCAL_INCLUDES += ["/third_party/xsimd/include"] + SOURCES["AudioNodeEngineSSE2.cpp"].flags += CONFIG["SSE2_FLAGS"] + if CONFIG["SSE4_2_FLAGS"] and CONFIG["FMA_FLAGS"]: + DEFINES["USE_SSE4_2"] = True + DEFINES["USE_FMA3"] = True + SOURCES += ["AudioNodeEngineSSE4_2_FMA3.cpp"] + SOURCES["AudioNodeEngineSSE4_2_FMA3.cpp"].flags += ( + CONFIG["SSE4_2_FLAGS"] + CONFIG["FMA_FLAGS"] + ) + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" +LOCAL_INCLUDES += [".."] diff --git a/dom/media/webaudio/test/1856145.ogg b/dom/media/webaudio/test/1856145.ogg Binary files differnew file mode 100644 index 0000000000..771fad7899 --- /dev/null +++ b/dom/media/webaudio/test/1856145.ogg diff --git a/dom/media/webaudio/test/8kHz-320kbps-6ch.aac b/dom/media/webaudio/test/8kHz-320kbps-6ch.aac Binary files differnew file mode 100644 index 0000000000..8981d40dfd --- /dev/null +++ b/dom/media/webaudio/test/8kHz-320kbps-6ch.aac diff --git a/dom/media/webaudio/test/audio-expected.wav b/dom/media/webaudio/test/audio-expected.wav Binary files differnew file mode 100644 index 0000000000..384f76318a --- /dev/null +++ b/dom/media/webaudio/test/audio-expected.wav diff --git a/dom/media/webaudio/test/audio-mono-expected-2.wav b/dom/media/webaudio/test/audio-mono-expected-2.wav Binary files differnew file mode 100644 index 0000000000..68c90dfa1e --- /dev/null +++ b/dom/media/webaudio/test/audio-mono-expected-2.wav diff --git a/dom/media/webaudio/test/audio-mono-expected.wav b/dom/media/webaudio/test/audio-mono-expected.wav Binary files differnew file mode 100644 index 0000000000..bf00e5cdf2 --- /dev/null +++ b/dom/media/webaudio/test/audio-mono-expected.wav diff --git a/dom/media/webaudio/test/audio-quad.wav b/dom/media/webaudio/test/audio-quad.wav Binary files differnew file mode 100644 index 0000000000..093f0197ae --- /dev/null +++ b/dom/media/webaudio/test/audio-quad.wav diff --git a/dom/media/webaudio/test/audio.ogv b/dom/media/webaudio/test/audio.ogv Binary files differnew file mode 100644 index 0000000000..68dee3cf2b --- /dev/null +++ b/dom/media/webaudio/test/audio.ogv diff --git a/dom/media/webaudio/test/audioBufferSourceNodeDetached_worker.js b/dom/media/webaudio/test/audioBufferSourceNodeDetached_worker.js new file mode 100644 index 0000000000..2a5a4bff89 --- /dev/null +++ b/dom/media/webaudio/test/audioBufferSourceNodeDetached_worker.js @@ -0,0 +1,3 @@ +onmessage = function (event) { + postMessage("Pong"); +}; diff --git a/dom/media/webaudio/test/audiovideo.mp4 b/dom/media/webaudio/test/audiovideo.mp4 Binary files differnew file mode 100644 index 0000000000..fe93122d29 --- /dev/null +++ b/dom/media/webaudio/test/audiovideo.mp4 diff --git a/dom/media/webaudio/test/blink/README b/dom/media/webaudio/test/blink/README new file mode 100644 index 0000000000..1d819221fd --- /dev/null +++ b/dom/media/webaudio/test/blink/README @@ -0,0 +1,9 @@ +This directory contains tests originally borrowed from the Blink Web Audio test +suite. + +The process of borrowing tests from Blink is as follows: + +* Import the pristine file from the Blink repo, noting the revision in the + commit message. +* Modify the test files to turn the LayoutTest into a mochitest-plain and add +* them to the test suite in a separate commit. diff --git a/dom/media/webaudio/test/blink/audio-testing.js b/dom/media/webaudio/test/blink/audio-testing.js new file mode 100644 index 0000000000..c66d32c7f2 --- /dev/null +++ b/dom/media/webaudio/test/blink/audio-testing.js @@ -0,0 +1,192 @@ +if (window.testRunner) + testRunner.overridePreference("WebKitWebAudioEnabled", "1"); + +function writeString(s, a, offset) { + for (var i = 0; i < s.length; ++i) { + a[offset + i] = s.charCodeAt(i); + } +} + +function writeInt16(n, a, offset) { + n = Math.floor(n); + + var b1 = n & 255; + var b2 = (n >> 8) & 255; + + a[offset + 0] = b1; + a[offset + 1] = b2; +} + +function writeInt32(n, a, offset) { + n = Math.floor(n); + var b1 = n & 255; + var b2 = (n >> 8) & 255; + var b3 = (n >> 16) & 255; + var b4 = (n >> 24) & 255; + + a[offset + 0] = b1; + a[offset + 1] = b2; + a[offset + 2] = b3; + a[offset + 3] = b4; +} + +function writeAudioBuffer(audioBuffer, a, offset) { + var n = audioBuffer.length; + var channels = audioBuffer.numberOfChannels; + + for (var i = 0; i < n; ++i) { + for (var k = 0; k < channels; ++k) { + var buffer = audioBuffer.getChannelData(k); + var sample = buffer[i] * 32768.0; + + // Clip samples to the limitations of 16-bit. + // If we don't do this then we'll get nasty wrap-around distortion. + if (sample < -32768) + sample = -32768; + if (sample > 32767) + sample = 32767; + + writeInt16(sample, a, offset); + offset += 2; + } + } +} + +function createWaveFileData(audioBuffer) { + var frameLength = audioBuffer.length; + var numberOfChannels = audioBuffer.numberOfChannels; + var sampleRate = audioBuffer.sampleRate; + var bitsPerSample = 16; + var byteRate = sampleRate * numberOfChannels * bitsPerSample/8; + var blockAlign = numberOfChannels * bitsPerSample/8; + var wavDataByteLength = frameLength * numberOfChannels * 2; // 16-bit audio + var headerByteLength = 44; + var totalLength = headerByteLength + wavDataByteLength; + + var waveFileData = new Uint8Array(totalLength); + + var subChunk1Size = 16; // for linear PCM + var subChunk2Size = wavDataByteLength; + var chunkSize = 4 + (8 + subChunk1Size) + (8 + subChunk2Size); + + writeString("RIFF", waveFileData, 0); + writeInt32(chunkSize, waveFileData, 4); + writeString("WAVE", waveFileData, 8); + writeString("fmt ", waveFileData, 12); + + writeInt32(subChunk1Size, waveFileData, 16); // SubChunk1Size (4) + writeInt16(1, waveFileData, 20); // AudioFormat (2) + writeInt16(numberOfChannels, waveFileData, 22); // NumChannels (2) + writeInt32(sampleRate, waveFileData, 24); // SampleRate (4) + writeInt32(byteRate, waveFileData, 28); // ByteRate (4) + writeInt16(blockAlign, waveFileData, 32); // BlockAlign (2) + writeInt32(bitsPerSample, waveFileData, 34); // BitsPerSample (4) + + writeString("data", waveFileData, 36); + writeInt32(subChunk2Size, waveFileData, 40); // SubChunk2Size (4) + + // Write actual audio data starting at offset 44. + writeAudioBuffer(audioBuffer, waveFileData, 44); + + return waveFileData; +} + +function createAudioData(audioBuffer) { + return createWaveFileData(audioBuffer); +} + +function finishAudioTest(event) { + var audioData = createAudioData(event.renderedBuffer); + testRunner.setAudioData(audioData); + testRunner.notifyDone(); +} + +// Create an impulse in a buffer of length sampleFrameLength +function createImpulseBuffer(context, sampleFrameLength) { + var audioBuffer = context.createBuffer(1, sampleFrameLength, context.sampleRate); + var n = audioBuffer.length; + var dataL = audioBuffer.getChannelData(0); + + for (var k = 0; k < n; ++k) { + dataL[k] = 0; + } + dataL[0] = 1; + + return audioBuffer; +} + +// Create a buffer of the given length with a linear ramp having values 0 <= x < 1. +function createLinearRampBuffer(context, sampleFrameLength) { + var audioBuffer = context.createBuffer(1, sampleFrameLength, context.sampleRate); + var n = audioBuffer.length; + var dataL = audioBuffer.getChannelData(0); + + for (var i = 0; i < n; ++i) + dataL[i] = i / n; + + return audioBuffer; +} + +// Create a buffer of the given length having a constant value. +function createConstantBuffer(context, sampleFrameLength, constantValue) { + var audioBuffer = context.createBuffer(1, sampleFrameLength, context.sampleRate); + var n = audioBuffer.length; + var dataL = audioBuffer.getChannelData(0); + + for (var i = 0; i < n; ++i) + dataL[i] = constantValue; + + return audioBuffer; +} + +// Create a stereo impulse in a buffer of length sampleFrameLength +function createStereoImpulseBuffer(context, sampleFrameLength) { + var audioBuffer = context.createBuffer(2, sampleFrameLength, context.sampleRate); + var n = audioBuffer.length; + var dataL = audioBuffer.getChannelData(0); + var dataR = audioBuffer.getChannelData(1); + + for (var k = 0; k < n; ++k) { + dataL[k] = 0; + dataR[k] = 0; + } + dataL[0] = 1; + dataR[0] = 1; + + return audioBuffer; +} + +// Convert time (in seconds) to sample frames. +function timeToSampleFrame(time, sampleRate) { + return Math.floor(0.5 + time * sampleRate); +} + +// Compute the number of sample frames consumed by start with +// the specified |grainOffset|, |duration|, and |sampleRate|. +function grainLengthInSampleFrames(grainOffset, duration, sampleRate) { + var startFrame = timeToSampleFrame(grainOffset, sampleRate); + var endFrame = timeToSampleFrame(grainOffset + duration, sampleRate); + + return endFrame - startFrame; +} + +// True if the number is not an infinity or NaN +function isValidNumber(x) { + return !isNaN(x) && (x != Infinity) && (x != -Infinity); +} + +function shouldThrowTypeError(func, text) { + var ok = false; + try { + func(); + } catch (e) { + if (e instanceof TypeError) { + ok = true; + } + } + if (ok) { + testPassed(text + " threw TypeError."); + } else { + testFailed(text + " should throw TypeError."); + } +} diff --git a/dom/media/webaudio/test/blink/biquad-filters.js b/dom/media/webaudio/test/blink/biquad-filters.js new file mode 100644 index 0000000000..06fff98b18 --- /dev/null +++ b/dom/media/webaudio/test/blink/biquad-filters.js @@ -0,0 +1,368 @@ +// Taken from WebKit/LayoutTests/webaudio/resources/biquad-filters.js + +// A biquad filter has a z-transform of +// H(z) = (b0 + b1 / z + b2 / z^2) / (1 + a1 / z + a2 / z^2) +// +// The formulas for the various filters were taken from +// http://www.musicdsp.org/files/Audio-EQ-Cookbook.txt. + + +// Lowpass filter. +function createLowpassFilter(freq, q, gain) { + var b0; + var b1; + var b2; + var a0; + var a1; + var a2; + + if (freq == 1) { + // The formula below works, except for roundoff. When freq = 1, + // the filter is just a wire, so hardwire the coefficients. + b0 = 1; + b1 = 0; + b2 = 0; + a0 = 1; + a1 = 0; + a2 = 0; + } else { + var w0 = Math.PI * freq; + var alpha = 0.5 * Math.sin(w0) / Math.pow(10, q / 20); + var cos_w0 = Math.cos(w0); + + b0 = 0.5 * (1 - cos_w0); + b1 = 1 - cos_w0; + b2 = b0; + a0 = 1 + alpha; + a1 = -2.0 * cos_w0; + a2 = 1 - alpha; + } + + return normalizeFilterCoefficients(b0, b1, b2, a0, a1, a2); +} + +function createHighpassFilter(freq, q, gain) { + var b0; + var b1; + var b2; + var a1; + var a2; + + if (freq == 1) { + // The filter is 0 + b0 = 0; + b1 = 0; + b2 = 0; + a0 = 1; + a1 = 0; + a2 = 0; + } else if (freq == 0) { + // The filter is 1. Computation of coefficients below is ok, but + // there's a pole at 1 and a zero at 1, so round-off could make + // the filter unstable. + b0 = 1; + b1 = 0; + b2 = 0; + a0 = 1; + a1 = 0; + a2 = 0; + } else { + var w0 = Math.PI * freq; + var alpha = 0.5 * Math.sin(w0) / Math.pow(10, q / 20); + var cos_w0 = Math.cos(w0); + + b0 = 0.5 * (1 + cos_w0); + b1 = -1 - cos_w0; + b2 = b0; + a0 = 1 + alpha; + a1 = -2.0 * cos_w0; + a2 = 1 - alpha; + } + + return normalizeFilterCoefficients(b0, b1, b2, a0, a1, a2); +} + +function normalizeFilterCoefficients(b0, b1, b2, a0, a1, a2) { + var scale = 1 / a0; + + return {b0 : b0 * scale, + b1 : b1 * scale, + b2 : b2 * scale, + a1 : a1 * scale, + a2 : a2 * scale}; +} + +function createBandpassFilter(freq, q, gain) { + var b0; + var b1; + var b2; + var a0; + var a1; + var a2; + var coef; + + if (freq > 0 && freq < 1) { + var w0 = Math.PI * freq; + if (q > 0) { + var alpha = Math.sin(w0) / (2 * q); + var k = Math.cos(w0); + + b0 = alpha; + b1 = 0; + b2 = -alpha; + a0 = 1 + alpha; + a1 = -2 * k; + a2 = 1 - alpha; + + coef = normalizeFilterCoefficients(b0, b1, b2, a0, a1, a2); + } else { + // q = 0, and frequency is not 0 or 1. The above formula has a + // divide by zero problem. The limit of the z-transform as q + // approaches 0 is 1, so set the filter that way. + coef = {b0 : 1, b1 : 0, b2 : 0, a1 : 0, a2 : 0}; + } + } else { + // When freq = 0 or 1, the z-transform is identically 0, + // independent of q. + coef = {b0 : 0, b1 : 0, b2 : 0, a1 : 0, a2 : 0} + } + + return coef; +} + +function createLowShelfFilter(freq, q, gain) { + // q not used + var b0; + var b1; + var b2; + var a0; + var a1; + var a2; + var coef; + + var S = 1; + var A = Math.pow(10, gain / 40); + + if (freq == 1) { + // The filter is just a constant gain + coef = {b0 : A * A, b1 : 0, b2 : 0, a1 : 0, a2 : 0}; + } else if (freq == 0) { + // The filter is 1 + coef = {b0 : 1, b1 : 0, b2 : 0, a1 : 0, a2 : 0}; + } else { + var w0 = Math.PI * freq; + var alpha = 1 / 2 * Math.sin(w0) * Math.sqrt((A + 1 / A) * (1 / S - 1) + 2); + var k = Math.cos(w0); + var k2 = 2 * Math.sqrt(A) * alpha; + var Ap1 = A + 1; + var Am1 = A - 1; + + b0 = A * (Ap1 - Am1 * k + k2); + b1 = 2 * A * (Am1 - Ap1 * k); + b2 = A * (Ap1 - Am1 * k - k2); + a0 = Ap1 + Am1 * k + k2; + a1 = -2 * (Am1 + Ap1 * k); + a2 = Ap1 + Am1 * k - k2; + coef = normalizeFilterCoefficients(b0, b1, b2, a0, a1, a2); + } + + return coef; +} + +function createHighShelfFilter(freq, q, gain) { + // q not used + var b0; + var b1; + var b2; + var a0; + var a1; + var a2; + var coef; + + var A = Math.pow(10, gain / 40); + + if (freq == 1) { + // When freq = 1, the z-transform is 1 + coef = {b0 : 1, b1 : 0, b2 : 0, a1 : 0, a2 : 0}; + } else if (freq > 0) { + var w0 = Math.PI * freq; + var S = 1; + var alpha = 0.5 * Math.sin(w0) * Math.sqrt((A + 1 / A) * (1 / S - 1) + 2); + var k = Math.cos(w0); + var k2 = 2 * Math.sqrt(A) * alpha; + var Ap1 = A + 1; + var Am1 = A - 1; + + b0 = A * (Ap1 + Am1 * k + k2); + b1 = -2 * A * (Am1 + Ap1 * k); + b2 = A * (Ap1 + Am1 * k - k2); + a0 = Ap1 - Am1 * k + k2; + a1 = 2 * (Am1 - Ap1*k); + a2 = Ap1 - Am1 * k-k2; + + coef = normalizeFilterCoefficients(b0, b1, b2, a0, a1, a2); + } else { + // When freq = 0, the filter is just a gain + coef = {b0 : A * A, b1 : 0, b2 : 0, a1 : 0, a2 : 0}; + } + + return coef; +} + +function createPeakingFilter(freq, q, gain) { + var b0; + var b1; + var b2; + var a0; + var a1; + var a2; + var coef; + + var A = Math.pow(10, gain / 40); + + if (freq > 0 && freq < 1) { + if (q > 0) { + var w0 = Math.PI * freq; + var alpha = Math.sin(w0) / (2 * q); + var k = Math.cos(w0); + + b0 = 1 + alpha * A; + b1 = -2 * k; + b2 = 1 - alpha * A; + a0 = 1 + alpha / A; + a1 = -2 * k; + a2 = 1 - alpha / A; + + coef = normalizeFilterCoefficients(b0, b1, b2, a0, a1, a2); + } else { + // q = 0, we have a divide by zero problem in the formulas + // above. But if we look at the z-transform, we see that the + // limit as q approaches 0 is A^2. + coef = {b0 : A * A, b1 : 0, b2 : 0, a1 : 0, a2 : 0}; + } + } else { + // freq = 0 or 1, the z-transform is 1 + coef = {b0 : 1, b1 : 0, b2 : 0, a1 : 0, a2 : 0}; + } + + return coef; +} + +function createNotchFilter(freq, q, gain) { + var b0; + var b1; + var b2; + var a0; + var a1; + var a2; + var coef; + + if (freq > 0 && freq < 1) { + if (q > 0) { + var w0 = Math.PI * freq; + var alpha = Math.sin(w0) / (2 * q); + var k = Math.cos(w0); + + b0 = 1; + b1 = -2 * k; + b2 = 1; + a0 = 1 + alpha; + a1 = -2 * k; + a2 = 1 - alpha; + coef = normalizeFilterCoefficients(b0, b1, b2, a0, a1, a2); + } else { + // When q = 0, we get a divide by zero above. The limit of the + // z-transform as q approaches 0 is 0, so set the coefficients + // appropriately. + coef = {b0 : 0, b1 : 0, b2 : 0, a1 : 0, a2 : 0}; + } + } else { + // When freq = 0 or 1, the z-transform is 1 + coef = {b0 : 1, b1 : 0, b2 : 0, a1 : 0, a2 : 0}; + } + + return coef; +} + +function createAllpassFilter(freq, q, gain) { + var b0; + var b1; + var b2; + var a0; + var a1; + var a2; + var coef; + + if (freq > 0 && freq < 1) { + if (q > 0) { + var w0 = Math.PI * freq; + var alpha = Math.sin(w0) / (2 * q); + var k = Math.cos(w0); + + b0 = 1 - alpha; + b1 = -2 * k; + b2 = 1 + alpha; + a0 = 1 + alpha; + a1 = -2 * k; + a2 = 1 - alpha; + coef = normalizeFilterCoefficients(b0, b1, b2, a0, a1, a2); + } else { + // q = 0 + coef = {b0 : -1, b1 : 0, b2 : 0, a1 : 0, a2 : 0}; + } + } else { + coef = {b0 : 1, b1 : 0, b2 : 0, a1 : 0, a2 : 0}; + } + + return coef; +} + +function filterData(filterCoef, signal, len) { + var y = new Array(len); + var b0 = filterCoef.b0; + var b1 = filterCoef.b1; + var b2 = filterCoef.b2; + var a1 = filterCoef.a1; + var a2 = filterCoef.a2; + + // Prime the pump. (Assumes the signal has length >= 2!) + y[0] = b0 * signal[0]; + y[1] = b0 * signal[1] + b1 * signal[0] - a1 * y[0]; + + // Filter all of the signal that we have. + for (var k = 2; k < Math.min(signal.length, len); ++k) { + y[k] = b0 * signal[k] + b1 * signal[k-1] + b2 * signal[k-2] - a1 * y[k-1] - a2 * y[k-2]; + } + + // If we need to filter more, but don't have any signal left, + // assume the signal is zero. + for (var k = signal.length; k < len; ++k) { + y[k] = - a1 * y[k-1] - a2 * y[k-2]; + } + + return y; +} + +// Map the filter type name to a function that computes the filter coefficents for the given filter +// type. +var filterCreatorFunction = {"lowpass": createLowpassFilter, + "highpass": createHighpassFilter, + "bandpass": createBandpassFilter, + "lowshelf": createLowShelfFilter, + "highshelf": createHighShelfFilter, + "peaking": createPeakingFilter, + "notch": createNotchFilter, + "allpass": createAllpassFilter}; + +var filterTypeName = {"lowpass": "Lowpass filter", + "highpass": "Highpass filter", + "bandpass": "Bandpass filter", + "lowshelf": "Lowshelf filter", + "highshelf": "Highshelf filter", + "peaking": "Peaking filter", + "notch": "Notch filter", + "allpass": "Allpass filter"}; + +function createFilter(filterType, freq, q, gain) { + return filterCreatorFunction[filterType](freq, q, gain); +} diff --git a/dom/media/webaudio/test/blink/biquad-testing.js b/dom/media/webaudio/test/blink/biquad-testing.js new file mode 100644 index 0000000000..795adf6012 --- /dev/null +++ b/dom/media/webaudio/test/blink/biquad-testing.js @@ -0,0 +1,153 @@ +// Globals, to make testing and debugging easier. +var context; +var filter; +var signal; +var renderedBuffer; +var renderedData; + +var sampleRate = 44100.0; +var pulseLengthFrames = .1 * sampleRate; + +// Maximum allowed error for the test to succeed. Experimentally determined. +var maxAllowedError = 5.9e-8; + +// This must be large enough so that the filtered result is +// essentially zero. See comments for createTestAndRun. +var timeStep = .1; + +// Maximum number of filters we can process (mostly for setting the +// render length correctly.) +var maxFilters = 5; + +// How long to render. Must be long enough for all of the filters we +// want to test. +var renderLengthSeconds = timeStep * (maxFilters + 1) ; + +var renderLengthSamples = Math.round(renderLengthSeconds * sampleRate); + +// Number of filters that will be processed. +var nFilters; + +function createImpulseBuffer(context, length) { + var impulse = context.createBuffer(1, length, context.sampleRate); + var data = impulse.getChannelData(0); + for (var k = 1; k < data.length; ++k) { + data[k] = 0; + } + data[0] = 1; + + return impulse; +} + + +function createTestAndRun(context, filterType, filterParameters) { + // To test the filters, we apply a signal (an impulse) to each of + // the specified filters, with each signal starting at a different + // time. The output of the filters is summed together at the + // output. Thus for filter k, the signal input to the filter + // starts at time k * timeStep. For this to work well, timeStep + // must be large enough for the output of each filter to have + // decayed to zero with timeStep seconds. That way the filter + // outputs don't interfere with each other. + + nFilters = Math.min(filterParameters.length, maxFilters); + + signal = new Array(nFilters); + filter = new Array(nFilters); + + impulse = createImpulseBuffer(context, pulseLengthFrames); + + // Create all of the signal sources and filters that we need. + for (var k = 0; k < nFilters; ++k) { + signal[k] = context.createBufferSource(); + signal[k].buffer = impulse; + + filter[k] = context.createBiquadFilter(); + filter[k].type = filterType; + filter[k].frequency.value = context.sampleRate / 2 * filterParameters[k].cutoff; + filter[k].detune.value = (filterParameters[k].detune === undefined) ? 0 : filterParameters[k].detune; + filter[k].Q.value = filterParameters[k].q; + filter[k].gain.value = filterParameters[k].gain; + + signal[k].connect(filter[k]); + filter[k].connect(context.destination); + + signal[k].start(timeStep * k); + } + + context.oncomplete = checkFilterResponse(filterType, filterParameters); + context.startRendering(); +} + +function addSignal(dest, src, destOffset) { + // Add src to dest at the given dest offset. + for (var k = destOffset, j = 0; k < dest.length, j < src.length; ++k, ++j) { + dest[k] += src[j]; + } +} + +function generateReference(filterType, filterParameters) { + var result = new Array(renderLengthSamples); + var data = new Array(renderLengthSamples); + // Initialize the result array and data. + for (var k = 0; k < result.length; ++k) { + result[k] = 0; + data[k] = 0; + } + // Make data an impulse. + data[0] = 1; + + for (var k = 0; k < nFilters; ++k) { + // Filter an impulse + var detune = (filterParameters[k].detune === undefined) ? 0 : filterParameters[k].detune; + var frequency = filterParameters[k].cutoff * Math.pow(2, detune / 1200); // Apply detune, converting from Cents. + + var filterCoef = createFilter(filterType, + frequency, + filterParameters[k].q, + filterParameters[k].gain); + var y = filterData(filterCoef, data, renderLengthSamples); + + // Accumulate this filtered data into the final output at the desired offset. + addSignal(result, y, timeToSampleFrame(timeStep * k, sampleRate)); + } + + return result; +} + +function checkFilterResponse(filterType, filterParameters) { + return function(event) { + renderedBuffer = event.renderedBuffer; + renderedData = renderedBuffer.getChannelData(0); + + reference = generateReference(filterType, filterParameters); + + var len = Math.min(renderedData.length, reference.length); + + var success = true; + + // Maximum error between rendered data and expected data + var maxError = 0; + + // Sample offset where the maximum error occurred. + var maxPosition = 0; + + // Number of infinities or NaNs that occurred in the rendered data. + var invalidNumberCount = 0; + + ok(nFilters == filterParameters.length, "Test wanted " + filterParameters.length + " filters but only " + maxFilters + " allowed."); + + compareChannels(renderedData, reference, len, 0, 0, true); + + // Check for bad numbers in the rendered output too. + // There shouldn't be any. + for (var k = 0; k < len; ++k) { + if (!isValidNumber(renderedData[k])) { + ++invalidNumberCount; + } + } + + ok(invalidNumberCount == 0, "Rendered output has " + invalidNumberCount + " infinities or NaNs."); + SimpleTest.finish(); + } +} diff --git a/dom/media/webaudio/test/blink/convolution-testing.js b/dom/media/webaudio/test/blink/convolution-testing.js new file mode 100644 index 0000000000..98ff0c7756 --- /dev/null +++ b/dom/media/webaudio/test/blink/convolution-testing.js @@ -0,0 +1,182 @@ +var sampleRate = 44100.0; + +var renderLengthSeconds = 8; +var pulseLengthSeconds = 1; +var pulseLengthFrames = pulseLengthSeconds * sampleRate; + +function createSquarePulseBuffer(context, sampleFrameLength) { + var audioBuffer = context.createBuffer(1, sampleFrameLength, context.sampleRate); + + var n = audioBuffer.length; + var data = audioBuffer.getChannelData(0); + + for (var i = 0; i < n; ++i) + data[i] = 1; + + return audioBuffer; +} + +// The triangle buffer holds the expected result of the convolution. +// It linearly ramps up from 0 to its maximum value (at the center) +// then linearly ramps down to 0. The center value corresponds to the +// point where the two square pulses overlap the most. +function createTrianglePulseBuffer(context, sampleFrameLength) { + var audioBuffer = context.createBuffer(1, sampleFrameLength, context.sampleRate); + + var n = audioBuffer.length; + var halfLength = n / 2; + var data = audioBuffer.getChannelData(0); + + for (var i = 0; i < halfLength; ++i) + data[i] = i + 1; + + for (var i = halfLength; i < n; ++i) + data[i] = n - i - 1; + + return audioBuffer; +} + +function log10(x) { + return Math.log(x)/Math.LN10; +} + +function linearToDecibel(x) { + return 20*log10(x); +} + +// Verify that the rendered result is very close to the reference +// triangular pulse. +function checkTriangularPulse(rendered, reference) { + var match = true; + var maxDelta = 0; + var valueAtMaxDelta = 0; + var maxDeltaIndex = 0; + + for (var i = 0; i < reference.length; ++i) { + var diff = rendered[i] - reference[i]; + var x = Math.abs(diff); + if (x > maxDelta) { + maxDelta = x; + valueAtMaxDelta = reference[i]; + maxDeltaIndex = i; + } + } + + // allowedDeviationFraction was determined experimentally. It + // is the threshold of the relative error at the maximum + // difference between the true triangular pulse and the + // rendered pulse. + var allowedDeviationDecibels = -129.4; + var maxDeviationDecibels = linearToDecibel(maxDelta / valueAtMaxDelta); + + if (maxDeviationDecibels <= allowedDeviationDecibels) { + testPassed("Triangular portion of convolution is correct."); + } else { + testFailed("Triangular portion of convolution is not correct. Max deviation = " + maxDeviationDecibels + " dB at " + maxDeltaIndex); + match = false; + } + + return match; +} + +// Verify that the rendered data is close to zero for the first part +// of the tail. +function checkTail1(data, reference, breakpoint) { + var isZero = true; + var tail1Max = 0; + + for (var i = reference.length; i < reference.length + breakpoint; ++i) { + var mag = Math.abs(data[i]); + if (mag > tail1Max) { + tail1Max = mag; + } + } + + // Let's find the peak of the reference (even though we know a + // priori what it is). + var refMax = 0; + for (var i = 0; i < reference.length; ++i) { + refMax = Math.max(refMax, Math.abs(reference[i])); + } + + // This threshold is experimentally determined by examining the + // value of tail1MaxDecibels. + var threshold1 = -129.7; + + var tail1MaxDecibels = linearToDecibel(tail1Max/refMax); + if (tail1MaxDecibels <= threshold1) { + testPassed("First part of tail of convolution is sufficiently small."); + } else { + testFailed("First part of tail of convolution is not sufficiently small: " + tail1MaxDecibels + " dB"); + isZero = false; + } + + return isZero; +} + +// Verify that the second part of the tail of the convolution is +// exactly zero. +function checkTail2(data, reference, breakpoint) { + var isZero = true; + var tail2Max = 0; + // For the second part of the tail, the maximum value should be + // exactly zero. + var threshold2 = 0; + for (var i = reference.length + breakpoint; i < data.length; ++i) { + if (Math.abs(data[i]) > 0) { + isZero = false; + break; + } + } + + if (isZero) { + testPassed("Rendered signal after tail of convolution is silent."); + } else { + testFailed("Rendered signal after tail of convolution should be silent."); + } + + return isZero; +} + +function checkConvolvedResult(trianglePulse) { + return function(event) { + var renderedBuffer = event.renderedBuffer; + + var referenceData = trianglePulse.getChannelData(0); + var renderedData = renderedBuffer.getChannelData(0); + + var success = true; + + // Verify the triangular pulse is actually triangular. + + success = success && checkTriangularPulse(renderedData, referenceData); + + // Make sure that portion after convolved portion is totally + // silent. But round-off prevents this from being completely + // true. At the end of the triangle, it should be close to + // zero. If we go farther out, it should be even closer and + // eventually zero. + + // For the tail of the convolution (where the result would be + // theoretically zero), we partition the tail into two + // parts. The first is the at the beginning of the tail, + // where we tolerate a small but non-zero value. The second part is + // farther along the tail where the result should be zero. + + // breakpoint is the point dividing the first two tail parts + // we're looking at. Experimentally determined. + var breakpoint = 12800; + + success = success && checkTail1(renderedData, referenceData, breakpoint); + + success = success && checkTail2(renderedData, referenceData, breakpoint); + + if (success) { + testPassed("Test signal was correctly convolved."); + } else { + testFailed("Test signal was not correctly convolved."); + } + + finishJSTest(); + } +} diff --git a/dom/media/webaudio/test/blink/mochitest.toml b/dom/media/webaudio/test/blink/mochitest.toml new file mode 100644 index 0000000000..2b176d1225 --- /dev/null +++ b/dom/media/webaudio/test/blink/mochitest.toml @@ -0,0 +1,36 @@ +[DEFAULT] +tags = "mtg webaudio" +subsuite = "media" +support-files = [ + "biquad-filters.js", + "biquad-testing.js", + "../webaudio.js", +] + +["test_biquadFilterNodeAllPass.html"] + +["test_biquadFilterNodeAutomation.html"] +skip-if = ["true"] # Known problems with Biquad automation, e.g. Bug 1155709 + +["test_biquadFilterNodeBandPass.html"] + +["test_biquadFilterNodeGetFrequencyResponse.html"] + +["test_biquadFilterNodeHighPass.html"] + +["test_biquadFilterNodeHighShelf.html"] + +["test_biquadFilterNodeLowPass.html"] + +["test_biquadFilterNodeLowShelf.html"] + +["test_biquadFilterNodeNotch.html"] + +["test_biquadFilterNodePeaking.html"] + +["test_biquadFilterNodeTail.html"] + +["test_iirFilterNode.html"] + +["test_iirFilterNodeGetFrequencyResponse.html"] + diff --git a/dom/media/webaudio/test/blink/panner-model-testing.js b/dom/media/webaudio/test/blink/panner-model-testing.js new file mode 100644 index 0000000000..45460e2768 --- /dev/null +++ b/dom/media/webaudio/test/blink/panner-model-testing.js @@ -0,0 +1,210 @@ +var sampleRate = 48000.0; + +var numberOfChannels = 1; + +// Time step when each panner node starts. +var timeStep = 0.001; + +// Length of the impulse signal. +var pulseLengthFrames = Math.round(timeStep * sampleRate); + +// How many panner nodes to create for the test +var nodesToCreate = 100; + +// Be sure we render long enough for all of our nodes. +var renderLengthSeconds = timeStep * (nodesToCreate + 1); + +// These are global mostly for debugging. +var context; +var impulse; +var bufferSource; +var panner; +var position; +var time; + +var renderedBuffer; +var renderedLeft; +var renderedRight; + +function createGraph(context, nodeCount) { + bufferSource = new Array(nodeCount); + panner = new Array(nodeCount); + position = new Array(nodeCount); + time = new Array(nodeCount); + // Angle between panner locations. (nodeCount - 1 because we want + // to include both 0 and 180 deg. + var angleStep = Math.PI / (nodeCount - 1); + + if (numberOfChannels == 2) { + impulse = createStereoImpulseBuffer(context, pulseLengthFrames); + } + else + impulse = createImpulseBuffer(context, pulseLengthFrames); + + for (var k = 0; k < nodeCount; ++k) { + bufferSource[k] = context.createBufferSource(); + bufferSource[k].buffer = impulse; + + panner[k] = context.createPanner(); + panner[k].panningModel = "equalpower"; + panner[k].distanceModel = "linear"; + + var angle = angleStep * k; + position[k] = {angle : angle, x : Math.cos(angle), z : Math.sin(angle)}; + panner[k].positionX.value = position[k].x; + panner[k].positionZ.value = position[k].z; + + bufferSource[k].connect(panner[k]); + panner[k].connect(context.destination); + + // Start the source + time[k] = k * timeStep; + bufferSource[k].start(time[k]); + } +} + +function createTestAndRun(context, nodeCount, numberOfSourceChannels) { + numberOfChannels = numberOfSourceChannels; + + createGraph(context, nodeCount); + + context.oncomplete = checkResult; + context.startRendering(); +} + +// Map our position angle to the azimuth angle (in degrees). +// +// An angle of 0 corresponds to an azimuth of 90 deg; pi, to -90 deg. +function angleToAzimuth(angle) { + return 90 - angle * 180 / Math.PI; +} + +// The gain caused by the EQUALPOWER panning model +function equalPowerGain(angle) { + var azimuth = angleToAzimuth(angle); + + if (numberOfChannels == 1) { + var panPosition = (azimuth + 90) / 180; + + var gainL = Math.cos(0.5 * Math.PI * panPosition); + var gainR = Math.sin(0.5 * Math.PI * panPosition); + + return { left : gainL, right : gainR }; + } else { + if (azimuth <= 0) { + var panPosition = (azimuth + 90) / 90; + + var gainL = 1 + Math.cos(0.5 * Math.PI * panPosition); + var gainR = Math.sin(0.5 * Math.PI * panPosition); + + return { left : gainL, right : gainR }; + } else { + var panPosition = azimuth / 90; + + var gainL = Math.cos(0.5 * Math.PI * panPosition); + var gainR = 1 + Math.sin(0.5 * Math.PI * panPosition); + + return { left : gainL, right : gainR }; + } + } +} + +function checkResult(event) { + renderedBuffer = event.renderedBuffer; + renderedLeft = renderedBuffer.getChannelData(0); + renderedRight = renderedBuffer.getChannelData(1); + + // The max error we allow between the rendered impulse and the + // expected value. This value is experimentally determined. Set + // to 0 to make the test fail to see what the actual error is. + var maxAllowedError = 1.3e-6; + + var success = true; + + // Number of impulses found in the rendered result. + var impulseCount = 0; + + // Max (relative) error and the index of the maxima for the left + // and right channels. + var maxErrorL = 0; + var maxErrorIndexL = 0; + var maxErrorR = 0; + var maxErrorIndexR = 0; + + // Number of impulses that don't match our expected locations. + var timeCount = 0; + + // Locations of where the impulses aren't at the expected locations. + var timeErrors = new Array(); + + for (var k = 0; k < renderedLeft.length; ++k) { + // We assume that the left and right channels start at the same instant. + if (renderedLeft[k] != 0 || renderedRight[k] != 0) { + // The expected gain for the left and right channels. + var pannerGain = equalPowerGain(position[impulseCount].angle); + var expectedL = pannerGain.left; + var expectedR = pannerGain.right; + + // Absolute error in the gain. + var errorL = Math.abs(renderedLeft[k] - expectedL); + var errorR = Math.abs(renderedRight[k] - expectedR); + + if (Math.abs(errorL) > maxErrorL) { + maxErrorL = Math.abs(errorL); + maxErrorIndexL = impulseCount; + } + if (Math.abs(errorR) > maxErrorR) { + maxErrorR = Math.abs(errorR); + maxErrorIndexR = impulseCount; + } + + // Keep track of the impulses that didn't show up where we + // expected them to be. + var expectedOffset = timeToSampleFrame(time[impulseCount], sampleRate); + if (k != expectedOffset) { + timeErrors[timeCount] = { actual : k, expected : expectedOffset}; + ++timeCount; + } + ++impulseCount; + } + } + + if (impulseCount == nodesToCreate) { + testPassed("Number of impulses matches the number of panner nodes."); + } else { + testFailed("Number of impulses is incorrect. (Found " + impulseCount + " but expected " + nodesToCreate + ")"); + success = false; + } + + if (timeErrors.length > 0) { + success = false; + testFailed(timeErrors.length + " timing errors found in " + nodesToCreate + " panner nodes."); + for (var k = 0; k < timeErrors.length; ++k) { + testFailed("Impulse at sample " + timeErrors[k].actual + " but expected " + timeErrors[k].expected); + } + } else { + testPassed("All impulses at expected offsets."); + } + + if (maxErrorL <= maxAllowedError) { + testPassed("Left channel gain values are correct."); + } else { + testFailed("Left channel gain values are incorrect. Max error = " + maxErrorL + " at time " + time[maxErrorIndexL] + " (threshold = " + maxAllowedError + ")"); + success = false; + } + + if (maxErrorR <= maxAllowedError) { + testPassed("Right channel gain values are correct."); + } else { + testFailed("Right channel gain values are incorrect. Max error = " + maxErrorR + " at time " + time[maxErrorIndexR] + " (threshold = " + maxAllowedError + ")"); + success = false; + } + + if (success) { + testPassed("EqualPower panner test passed"); + } else { + testFailed("EqualPower panner test failed"); + } + + finishJSTest(); +} diff --git a/dom/media/webaudio/test/blink/test_biquadFilterNodeAllPass.html b/dom/media/webaudio/test/blink/test_biquadFilterNodeAllPass.html new file mode 100644 index 0000000000..024c2f50df --- /dev/null +++ b/dom/media/webaudio/test/blink/test_biquadFilterNodeAllPass.html @@ -0,0 +1,32 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test BiquadFilterNode All Pass Filter</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script src="audio-testing.js"></script> +<script src="biquad-filters.js"></script> +<script src="biquad-testing.js"></script> +<script src="webaudio.js" type="text/javascript"></script> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); + +addLoadEvent(function() { + // Create offline audio context. + var context = new OfflineAudioContext(2, sampleRate * renderLengthSeconds, sampleRate); + + var filterParameters = [{cutoff : 0, q : 10, gain : 1 }, + {cutoff : 1, q : 10, gain : 1 }, + {cutoff : .5, q : 0, gain : 1 }, + {cutoff : 0.25, q : 10, gain : 1 }, + ]; + createTestAndRun(context, "allpass", filterParameters); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/blink/test_biquadFilterNodeAutomation.html b/dom/media/webaudio/test/blink/test_biquadFilterNodeAutomation.html new file mode 100644 index 0000000000..5a71ce46e5 --- /dev/null +++ b/dom/media/webaudio/test/blink/test_biquadFilterNodeAutomation.html @@ -0,0 +1,351 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test BiquadFilterNode All Pass Filter</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script src="audio-testing.js"></script> +<script src="biquad-filters.js"></script> +<script src="biquad-testing.js"></script> +<script src="webaudio.js" type="text/javascript"></script> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); + +addLoadEvent(function() { + // Don't need to run these tests at high sampling rate, so just use a low one to reduce memory + // usage and complexity. + var sampleRate = 16000; + + // How long to render for each test. + var renderDuration = 1; + + // The definition of the linear ramp automation function. + function linearRamp(t, v0, v1, t0, t1) { + return v0 + (v1 - v0) * (t - t0) / (t1 - t0); + } + + // Generate the filter coefficients for the specified filter using the given parameters for + // the given duration. |filterTypeFunction| is a function that returns the filter + // coefficients for one set of parameters. |parameters| is a property bag that contains the + // start and end values (as an array) for each of the biquad attributes. The properties are + // |freq|, |Q|, |gain|, and |detune|. |duration| is the number of seconds for which the + // coefficients are generated. + // + // A property bag with properties |b0|, |b1|, |b2|, |a1|, |a2|. Each propery is an array + // consisting of the coefficients for the time-varying biquad filter. + function generateFilterCoefficients(filterTypeFunction, parameters, duration) { + var endFrame = Math.ceil(duration * sampleRate); + var nCoef = endFrame; + var b0 = new Float64Array(nCoef); + var b1 = new Float64Array(nCoef); + var b2 = new Float64Array(nCoef); + var a1 = new Float64Array(nCoef); + var a2 = new Float64Array(nCoef); + + var k = 0; + // If the property is not given, use the defaults. + var freqs = parameters.freq || [350, 350]; + var qs = parameters.Q || [1, 1]; + var gains = parameters.gain || [0, 0]; + var detunes = parameters.detune || [0, 0]; + + for (var frame = 0; frame < endFrame; ++frame) { + // Apply linear ramp at frame |frame|. + var f = linearRamp(frame / sampleRate, freqs[0], freqs[1], 0, duration); + var q = linearRamp(frame / sampleRate, qs[0], qs[1], 0, duration); + var g = linearRamp(frame / sampleRate, gains[0], gains[1], 0, duration); + var d = linearRamp(frame / sampleRate, detunes[0], detunes[1], 0, duration); + + // Compute actual frequency parameter + f = f * Math.pow(2, d / 1200); + + // Compute filter coefficients + var coef = filterTypeFunction(f / (sampleRate / 2), q, g); + b0[k] = coef.b0; + b1[k] = coef.b1; + b2[k] = coef.b2; + a1[k] = coef.a1; + a2[k] = coef.a2; + ++k; + } + + return {b0: b0, b1: b1, b2: b2, a1: a1, a2: a2}; + } + + // Apply the given time-varying biquad filter to the given signal, |signal|. |coef| should be + // the time-varying coefficients of the filter, as returned by |generateFilterCoefficients|. + function timeVaryingFilter(signal, coef) { + var length = signal.length; + // Use double precision for the internal computations. + var y = new Float64Array(length); + + // Prime the pump. (Assumes the signal has length >= 2!) + y[0] = coef.b0[0] * signal[0]; + y[1] = coef.b0[1] * signal[1] + coef.b1[1] * signal[0] - coef.a1[1] * y[0]; + + for (var n = 2; n < length; ++n) { + y[n] = coef.b0[n] * signal[n] + coef.b1[n] * signal[n-1] + coef.b2[n] * signal[n-2]; + y[n] -= coef.a1[n] * y[n-1] + coef.a2[n] * y[n-2]; + } + + // But convert the result to single precision for comparison. + return y.map(Math.fround); + } + + // Configure the audio graph using |context|. Returns the biquad filter node and the + // AudioBuffer used for the source. + function configureGraph(context, toneFrequency) { + // The source is just a simple sine wave. + var src = context.createBufferSource(); + var b = context.createBuffer(1, renderDuration * sampleRate, sampleRate); + var data = b.getChannelData(0); + var omega = 2 * Math.PI * toneFrequency / sampleRate; + for (var k = 0; k < data.length; ++k) { + data[k] = Math.sin(omega * k); + } + src.buffer = b; + var f = context.createBiquadFilter(); + src.connect(f); + f.connect(context.destination); + + src.start(); + + return {filter: f, source: b}; + } + + function createFilterVerifier(filterCreator, threshold, parameters, input, message) { + return function (resultBuffer) { + var actual = resultBuffer.getChannelData(0); + var coefs = generateFilterCoefficients(filterCreator, parameters, renderDuration); + + reference = timeVaryingFilter(input, coefs); + + compareChannels(actual, reference); + }; + } + + var testPromises = []; + + // Automate just the frequency parameter. A bandpass filter is used where the center + // frequency is swept across the source (which is a simple tone). + testPromises.push(function () { + var context = new OfflineAudioContext(1, renderDuration * sampleRate, sampleRate); + + // Center frequency of bandpass filter and also the frequency of the test tone. + var centerFreq = 10*440; + + // Sweep the frequency +/- 9*440 Hz from the center. This should cause the output to low at + // the beginning and end of the test where the done is outside the pass band of the filter, + // but high in the center where the tone is near the center of the pass band. + var parameters = { + freq: [centerFreq - 9*440, centerFreq + 9*440] + } + var graph = configureGraph(context, centerFreq); + var f = graph.filter; + var b = graph.source; + + f.type = "bandpass"; + f.frequency.setValueAtTime(parameters.freq[0], 0); + f.frequency.linearRampToValueAtTime(parameters.freq[1], renderDuration); + + return context.startRendering() + .then(createFilterVerifier(createBandpassFilter, 5e-5, parameters, b.getChannelData(0), + "Output of bandpass filter with frequency automation")); + }()); + + // Automate just the Q parameter. A bandpass filter is used where the Q of the filter is + // swept. + testPromises.push(function() { + var context = new OfflineAudioContext(1, renderDuration * sampleRate, sampleRate); + + // The frequency of the test tone. + var centerFreq = 440; + + // Sweep the Q paramter between 1 and 200. This will cause the output of the filter to pass + // most of the tone at the beginning to passing less of the tone at the end. This is + // because we set center frequency of the bandpass filter to be slightly off from the actual + // tone. + var parameters = { + Q: [1, 200], + // Center frequency of the bandpass filter is just 25 Hz above the tone frequency. + freq: [centerFreq + 25, centerFreq + 25] + }; + var graph = configureGraph(context, centerFreq); + var f = graph.filter; + var b = graph.source; + + f.type = "bandpass"; + f.frequency.value = parameters.freq[0]; + f.Q.setValueAtTime(parameters.Q[0], 0); + f.Q.linearRampToValueAtTime(parameters.Q[1], renderDuration); + + return context.startRendering() + .then(createFilterVerifier(createBandpassFilter, 1.4e-6, parameters, b.getChannelData(0), + "Output of bandpass filter with Q automation")); + }()); + + // Automate just the gain of the lowshelf filter. A test tone will be in the lowshelf part of + // the filter. The output will vary as the gain of the lowshelf is changed. + testPromises.push(function() { + var context = new OfflineAudioContext(1, renderDuration * sampleRate, sampleRate); + + // Frequency of the test tone. + var centerFreq = 440; + + // Set the cutoff frequency of the lowshelf to be significantly higher than the test tone. + // Sweep the gain from 20 dB to -20 dB. (We go from 20 to -20 to easily verify that the + // filter didn't go unstable.) + var parameters = { + freq: [3500, 3500], + gain: [20, -20] + } + var graph = configureGraph(context, centerFreq); + var f = graph.filter; + var b = graph.source; + + f.type = "lowshelf"; + f.frequency.value = parameters.freq[0]; + f.gain.setValueAtTime(parameters.gain[0], 0); + f.gain.linearRampToValueAtTime(parameters.gain[1], renderDuration); + + context.startRendering() + .then(createFilterVerifier(createLowShelfFilter, 8e-6, parameters, b.getChannelData(0), + "Output of lowshelf filter with gain automation")); + }()); + + // Automate just the detune parameter. Basically the same test as for the frequncy parameter + // but we just use the detune parameter to modulate the frequency parameter. + testPromises.push(function() { + var context = new OfflineAudioContext(1, renderDuration * sampleRate, sampleRate); + var centerFreq = 10*440; + var parameters = { + freq: [centerFreq, centerFreq], + detune: [-10*1200, 10*1200] + }; + var graph = configureGraph(context, centerFreq); + var f = graph.filter; + var b = graph.source; + + f.type = "bandpass"; + f.frequency.value = parameters.freq[0]; + f.detune.setValueAtTime(parameters.detune[0], 0); + f.detune.linearRampToValueAtTime(parameters.detune[1], renderDuration); + + context.startRendering() + .then(createFilterVerifier(createBandpassFilter, 5e-6, parameters, b.getChannelData(0), + "Output of bandpass filter with detune automation")); + }()); + + // Automate all of the filter parameters at once. This is a basic check that everything is + // working. A peaking filter is used because it uses all of the parameters. + testPromises.push(function() { + var context = new OfflineAudioContext(1, renderDuration * sampleRate, sampleRate); + var graph = configureGraph(context, 10*440); + var f = graph.filter; + var b = graph.source; + + // Sweep all of the filter parameters. These are pretty much arbitrary. + var parameters = { + freq: [10000, 100], + Q: [f.Q.value, .0001], + gain: [f.gain.value, 20], + detune: [2400, -2400] + }; + + f.type = "peaking"; + // Set starting points for all parameters of the filter. Start at 10 kHz for the center + // frequency, and the defaults for Q and gain. + f.frequency.setValueAtTime(parameters.freq[0], 0); + f.Q.setValueAtTime(parameters.Q[0], 0); + f.gain.setValueAtTime(parameters.gain[0], 0); + f.detune.setValueAtTime(parameters.detune[0], 0); + + // Linear ramp each parameter + f.frequency.linearRampToValueAtTime(parameters.freq[1], renderDuration); + f.Q.linearRampToValueAtTime(parameters.Q[1], renderDuration); + f.gain.linearRampToValueAtTime(parameters.gain[1], renderDuration); + f.detune.linearRampToValueAtTime(parameters.detune[1], renderDuration); + + context.startRendering() + .then(createFilterVerifier(createPeakingFilter, 3.3e-4, parameters, b.getChannelData(0), + "Output of peaking filter with automation of all parameters")); + }()); + + // Test that modulation of the frequency parameter of the filter works. A sinusoid of 440 Hz + // is the test signal that is applied to a bandpass biquad filter. The frequency parameter of + // the filter is modulated by a sinusoid at 103 Hz, and the frequency modulation varies from + // 116 to 412 Hz. (This test was taken from the description in + // https://github.com/WebAudio/web-audio-api/issues/509#issuecomment-94731355) + testPromises.push(function() { + var context = new OfflineAudioContext(1, renderDuration * sampleRate, sampleRate); + + // Create a graph with the sinusoidal source at 440 Hz as the input to a biquad filter. + var graph = configureGraph(context, 440); + var f = graph.filter; + var b = graph.source; + + f.type = "bandpass"; + f.Q.value = 5; + f.frequency.value = 264; + + // Create the modulation source, a sinusoid with frequency 103 Hz and amplitude 148. (The + // amplitude of 148 is added to the filter's frequency value of 264 to produce a sinusoidal + // modulation of the frequency parameter from 116 to 412 Hz.) + var mod = context.createBufferSource(); + var mbuffer = context.createBuffer(1, renderDuration * sampleRate, sampleRate); + var d = mbuffer.getChannelData(0); + var omega = 2 * Math.PI * 103 / sampleRate; + for (var k = 0; k < d.length; ++k) { + d[k] = 148 * Math.sin(omega * k); + } + mod.buffer = mbuffer; + + mod.connect(f.frequency); + + mod.start(); + return context.startRendering() + .then(function (resultBuffer) { + var actual = resultBuffer.getChannelData(0); + // Compute the filter coefficients using the mod sine wave + + var endFrame = Math.ceil(renderDuration * sampleRate); + var nCoef = endFrame; + var b0 = new Float64Array(nCoef); + var b1 = new Float64Array(nCoef); + var b2 = new Float64Array(nCoef); + var a1 = new Float64Array(nCoef); + var a2 = new Float64Array(nCoef); + + // Generate the filter coefficients when the frequency varies from 116 to 248 Hz using + // the 103 Hz sinusoid. + for (var k = 0; k < nCoef; ++k) { + var freq = f.frequency.value + d[k]; + var c = createBandpassFilter(freq / (sampleRate / 2), f.Q.value, f.gain.value); + b0[k] = c.b0; + b1[k] = c.b1; + b2[k] = c.b2; + a1[k] = c.a1; + a2[k] = c.a2; + } + reference = timeVaryingFilter(b.getChannelData(0), + {b0: b0, b1: b1, b2: b2, a1: a1, a2: a2}); + + compareChannels(actual, reference); + }); + }()); + + // Wait for all tests + Promise.all(testPromises).then(function () { + SimpleTest.finish(); + }, function () { + SimpleTest.finish(); + }); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/blink/test_biquadFilterNodeBandPass.html b/dom/media/webaudio/test/blink/test_biquadFilterNodeBandPass.html new file mode 100644 index 0000000000..05c4c3a265 --- /dev/null +++ b/dom/media/webaudio/test/blink/test_biquadFilterNodeBandPass.html @@ -0,0 +1,34 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test BiquadFilterNode Band Pass Filter</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script src="audio-testing.js"></script> +<script src="biquad-filters.js"></script> +<script src="biquad-testing.js"></script> +<script src="webaudio.js" type="text/javascript"></script> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); + +addLoadEvent(function() { + // Create offline audio context. + var context = new OfflineAudioContext(2, sampleRate * renderLengthSeconds, sampleRate); + + // The filters we want to test. + var filterParameters = [{cutoff : 0, q : 0, gain : 1 }, + {cutoff : 1, q : 0, gain : 1 }, + {cutoff : 0.5, q : 0, gain : 1 }, + {cutoff : 0.25, q : 1, gain : 1 }, + ]; + + createTestAndRun(context, "bandpass", filterParameters); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/blink/test_biquadFilterNodeGetFrequencyResponse.html b/dom/media/webaudio/test/blink/test_biquadFilterNodeGetFrequencyResponse.html new file mode 100644 index 0000000000..c2b6612034 --- /dev/null +++ b/dom/media/webaudio/test/blink/test_biquadFilterNodeGetFrequencyResponse.html @@ -0,0 +1,261 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test BiquadFilterNode All Pass Filter</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script src="audio-testing.js"></script> +<script src="biquad-filters.js"></script> +<script src="biquad-testing.js"></script> +<script src="webaudio.js" type="text/javascript"></script> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); + +addLoadEvent(function() { +// Test the frequency response of a biquad filter. We compute the frequency response for a simple +// peaking biquad filter and compare it with the expected frequency response. The actual filter +// used doesn't matter since we're testing getFrequencyResponse and not the actual filter output. +// The filters are extensively tested in other biquad tests. + +var context; + +// The biquad filter node. +var filter; + +// The magnitude response of the biquad filter. +var magResponse; + +// The phase response of the biquad filter. +var phaseResponse; + +// Number of frequency samples to take. +var numberOfFrequencies = 1000; + +// The filter parameters. +var filterCutoff = 1000; // Hz. +var filterQ = 1; +var filterGain = 5; // Decibels. + +// The maximum allowed error in the magnitude response. +var maxAllowedMagError = 5.7e-7; + +// The maximum allowed error in the phase response. +var maxAllowedPhaseError = 4.7e-8; + +// The magnitudes and phases of the reference frequency response. +var magResponse; +var phaseResponse; + +// The magnitudes and phases of the reference frequency response. +var expectedMagnitudes; +var expectedPhases; + +// Convert frequency in Hz to a normalized frequency between 0 to 1 with 1 corresponding to the +// Nyquist frequency. +function normalizedFrequency(freqHz, sampleRate) +{ + var nyquist = sampleRate / 2; + return freqHz / nyquist; +} + +// Get the filter response at a (normalized) frequency |f| for the filter with coefficients |coef|. +function getResponseAt(coef, f) +{ + var b0 = coef.b0; + var b1 = coef.b1; + var b2 = coef.b2; + var a1 = coef.a1; + var a2 = coef.a2; + + // H(z) = (b0 + b1 / z + b2 / z^2) / (1 + a1 / z + a2 / z^2) + // + // Compute H(exp(i * pi * f)). No native complex numbers in javascript, so break H(exp(i * pi * // f)) + // in to the real and imaginary parts of the numerator and denominator. Let omega = pi * f. + // Then the numerator is + // + // b0 + b1 * cos(omega) + b2 * cos(2 * omega) - i * (b1 * sin(omega) + b2 * sin(2 * omega)) + // + // and the denominator is + // + // 1 + a1 * cos(omega) + a2 * cos(2 * omega) - i * (a1 * sin(omega) + a2 * sin(2 * omega)) + // + // Compute the magnitude and phase from the real and imaginary parts. + + var omega = Math.PI * f; + var numeratorReal = b0 + b1 * Math.cos(omega) + b2 * Math.cos(2 * omega); + var numeratorImag = -(b1 * Math.sin(omega) + b2 * Math.sin(2 * omega)); + var denominatorReal = 1 + a1 * Math.cos(omega) + a2 * Math.cos(2 * omega); + var denominatorImag = -(a1 * Math.sin(omega) + a2 * Math.sin(2 * omega)); + + var magnitude = Math.sqrt((numeratorReal * numeratorReal + numeratorImag * numeratorImag) + / (denominatorReal * denominatorReal + denominatorImag * denominatorImag)); + var phase = Math.atan2(numeratorImag, numeratorReal) - Math.atan2(denominatorImag, denominatorReal); + + if (phase >= Math.PI) { + phase -= 2 * Math.PI; + } else if (phase <= -Math.PI) { + phase += 2 * Math.PI; + } + + return {magnitude : magnitude, phase : phase}; +} + +// Compute the reference frequency response for the biquad filter |filter| at the frequency samples +// given by |frequencies|. +function frequencyResponseReference(filter, frequencies) +{ + var sampleRate = filter.context.sampleRate; + var normalizedFreq = normalizedFrequency(filter.frequency.value, sampleRate); + var filterCoefficients = createFilter(filter.type, normalizedFreq, filter.Q.value, filter.gain.value); + + var magnitudes = []; + var phases = []; + + for (var k = 0; k < frequencies.length; ++k) { + var response = getResponseAt(filterCoefficients, normalizedFrequency(frequencies[k], sampleRate)); + magnitudes.push(response.magnitude); + phases.push(response.phase); + } + + return {magnitudes : magnitudes, phases : phases}; +} + +// Compute a set of linearly spaced frequencies. +function createFrequencies(nFrequencies, sampleRate) +{ + var frequencies = new Float32Array(nFrequencies); + var nyquist = sampleRate / 2; + var freqDelta = nyquist / nFrequencies; + + for (var k = 0; k < nFrequencies; ++k) { + frequencies[k] = k * freqDelta; + } + + return frequencies; +} + +function linearToDecibels(x) +{ + if (x) { + return 20 * Math.log(x) / Math.LN10; + } else { + return -1000; + } +} + +// Look through the array and find any NaN or infinity. Returns the index of the first occurence or +// -1 if none. +function findBadNumber(signal) +{ + for (var k = 0; k < signal.length; ++k) { + if (!isValidNumber(signal[k])) { + return k; + } + } + return -1; +} + +// Compute absolute value of the difference between phase angles, taking into account the wrapping +// of phases. +function absolutePhaseDifference(x, y) +{ + var diff = Math.abs(x - y); + + if (diff > Math.PI) { + diff = 2 * Math.PI - diff; + } + return diff; +} + +// Compare the frequency response with our expected response. +function compareResponses(filter, frequencies, magResponse, phaseResponse) +{ + var expectedResponse = frequencyResponseReference(filter, frequencies); + + expectedMagnitudes = expectedResponse.magnitudes; + expectedPhases = expectedResponse.phases; + + var n = magResponse.length; + var success = true; + var badResponse = false; + + var maxMagError = -1; + var maxMagErrorIndex = -1; + + var k; + var hasBadNumber; + + hasBadNumber = findBadNumber(magResponse); + ok (hasBadNumber < 0, "Magnitude response has NaN or infinity at " + hasBadNumber); + + hasBadNumber = findBadNumber(phaseResponse); + ok (hasBadNumber < 0, "Phase response has NaN or infinity at " + hasBadNumber); + + // These aren't testing the implementation itself. Instead, these are sanity checks on the + // reference. Failure here does not imply an error in the implementation. + hasBadNumber = findBadNumber(expectedMagnitudes); + ok (hasBadNumber < 0, "Expected magnitude response has NaN or infinity at " + hasBadNumber); + + hasBadNumber = findBadNumber(expectedPhases); + ok (hasBadNumber < 0, "Expected phase response has NaN or infinity at " + hasBadNumber); + + for (k = 0; k < n; ++k) { + var error = Math.abs(linearToDecibels(magResponse[k]) - linearToDecibels(expectedMagnitudes[k])); + if (error > maxMagError) { + maxMagError = error; + maxMagErrorIndex = k; + } + } + + var message = "Magnitude error (" + maxMagError + " dB)"; + message += " exceeded threshold at " + frequencies[maxMagErrorIndex]; + message += " Hz. Actual: " + linearToDecibels(magResponse[maxMagErrorIndex]); + message += " dB, expected: " + linearToDecibels(expectedMagnitudes[maxMagErrorIndex]) + " dB."; + ok(maxMagError < maxAllowedMagError, message); + + var maxPhaseError = -1; + var maxPhaseErrorIndex = -1; + + for (k = 0; k < n; ++k) { + var error = absolutePhaseDifference(phaseResponse[k], expectedPhases[k]); + if (error > maxPhaseError) { + maxPhaseError = error; + maxPhaseErrorIndex = k; + } + } + + message = "Phase error (radians) (" + maxPhaseError; + message += ") exceeded threshold at " + frequencies[maxPhaseErrorIndex]; + message += " Hz. Actual: " + phaseResponse[maxPhaseErrorIndex]; + message += " expected: " + expectedPhases[maxPhaseErrorIndex]; + + ok(maxPhaseError < maxAllowedPhaseError, message); +} + +context = new AudioContext(); + +filter = context.createBiquadFilter(); + +// Arbitrarily test a peaking filter, but any kind of filter can be tested. +filter.type = "peaking"; +filter.frequency.value = filterCutoff; +filter.Q.value = filterQ; +filter.gain.value = filterGain; + +var frequencies = createFrequencies(numberOfFrequencies, context.sampleRate); +magResponse = new Float32Array(numberOfFrequencies); +phaseResponse = new Float32Array(numberOfFrequencies); + +filter.getFrequencyResponse(frequencies, magResponse, phaseResponse); +compareResponses(filter, frequencies, magResponse, phaseResponse); + +SimpleTest.finish(); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/blink/test_biquadFilterNodeHighPass.html b/dom/media/webaudio/test/blink/test_biquadFilterNodeHighPass.html new file mode 100644 index 0000000000..a615574c2d --- /dev/null +++ b/dom/media/webaudio/test/blink/test_biquadFilterNodeHighPass.html @@ -0,0 +1,33 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test BiquadFilterNode High Pass Filter</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script src="audio-testing.js"></script> +<script src="biquad-filters.js"></script> +<script src="biquad-testing.js"></script> +<script src="webaudio.js" type="text/javascript"></script> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); + +addLoadEvent(function() { + // Create offline audio context. + var context = new OfflineAudioContext(2, sampleRate * renderLengthSeconds, sampleRate); + + // The filters we want to test. + var filterParameters = [{cutoff : 0, q : 1, gain : 1 }, + {cutoff : 1, q : 1, gain : 1 }, + {cutoff : 0.25, q : 1, gain : 1 }, + ]; + + createTestAndRun(context, "highpass", filterParameters); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/blink/test_biquadFilterNodeHighShelf.html b/dom/media/webaudio/test/blink/test_biquadFilterNodeHighShelf.html new file mode 100644 index 0000000000..c8f6815930 --- /dev/null +++ b/dom/media/webaudio/test/blink/test_biquadFilterNodeHighShelf.html @@ -0,0 +1,33 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test BiquadFilterNode High Shelf Filter</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script src="audio-testing.js"></script> +<script src="biquad-filters.js"></script> +<script src="biquad-testing.js"></script> +<script src="webaudio.js" type="text/javascript"></script> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); + +addLoadEvent(function() { + // Create offline audio context. + var context = new OfflineAudioContext(2, sampleRate * renderLengthSeconds, sampleRate); + + // The filters we want to test. + var filterParameters = [{cutoff : 0, q : 10, gain : 10 }, + {cutoff : 1, q : 10, gain : 10 }, + {cutoff : 0.25, q : 10, gain : 10 }, + ]; + + createTestAndRun(context, "highshelf", filterParameters); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/blink/test_biquadFilterNodeLowPass.html b/dom/media/webaudio/test/blink/test_biquadFilterNodeLowPass.html new file mode 100644 index 0000000000..dcea18551a --- /dev/null +++ b/dom/media/webaudio/test/blink/test_biquadFilterNodeLowPass.html @@ -0,0 +1,34 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test BiquadFilterNode Low Pass Filter</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script src="audio-testing.js"></script> +<script src="biquad-filters.js"></script> +<script src="biquad-testing.js"></script> +<script src="webaudio.js" type="text/javascript"></script> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); + +addLoadEvent(function() { + // Create offline audio context. + var context = new OfflineAudioContext(2, sampleRate * renderLengthSeconds, sampleRate); + + // The filters we want to test. + var filterParameters = [{cutoff : 0, q : 1, gain : 1 }, + {cutoff : 1, q : 1, gain : 1 }, + {cutoff : 0.25, q : 1, gain : 1 }, + {cutoff : 0.25, q : 1, gain : 1, detune : 100 }, + {cutoff : 0.01, q : 1, gain : 1, detune : -200 }, + ]; + createTestAndRun(context, "lowpass", filterParameters); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/blink/test_biquadFilterNodeLowShelf.html b/dom/media/webaudio/test/blink/test_biquadFilterNodeLowShelf.html new file mode 100644 index 0000000000..c1349f8e7e --- /dev/null +++ b/dom/media/webaudio/test/blink/test_biquadFilterNodeLowShelf.html @@ -0,0 +1,34 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test BiquadFilterNode Low Shelf Filter</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script src="audio-testing.js"></script> +<script src="biquad-filters.js"></script> +<script src="biquad-testing.js"></script> +<script src="webaudio.js" type="text/javascript"></script> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); + +addLoadEvent(function() { + + // Create offline audio context. + var context = new OfflineAudioContext(2, sampleRate * renderLengthSeconds, sampleRate); + + // The filters we want to test. + var filterParameters = [{cutoff : 0, q : 10, gain : 10 }, + {cutoff : 1, q : 10, gain : 10 }, + {cutoff : 0.25, q : 10, gain : 10 }, + ]; + + createTestAndRun(context, "lowshelf", filterParameters); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/blink/test_biquadFilterNodeNotch.html b/dom/media/webaudio/test/blink/test_biquadFilterNodeNotch.html new file mode 100644 index 0000000000..0fcbc5546e --- /dev/null +++ b/dom/media/webaudio/test/blink/test_biquadFilterNodeNotch.html @@ -0,0 +1,33 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test BiquadFilterNode Notch Filter</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script src="audio-testing.js"></script> +<script src="biquad-filters.js"></script> +<script src="biquad-testing.js"></script> +<script src="webaudio.js" type="text/javascript"></script> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); + +addLoadEvent(function() { + // Create offline audio context. + var context = new OfflineAudioContext(2, sampleRate * renderLengthSeconds, sampleRate); + + var filterParameters = [{cutoff : 0, q : 10, gain : 1 }, + {cutoff : 1, q : 10, gain : 1 }, + {cutoff : .5, q : 0, gain : 1 }, + {cutoff : 0.25, q : 10, gain : 1 }, + ]; + + createTestAndRun(context, "notch", filterParameters); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/blink/test_biquadFilterNodePeaking.html b/dom/media/webaudio/test/blink/test_biquadFilterNodePeaking.html new file mode 100644 index 0000000000..8e4727a37e --- /dev/null +++ b/dom/media/webaudio/test/blink/test_biquadFilterNodePeaking.html @@ -0,0 +1,34 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test BiquadFilterNode Low Pass Filter</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script src="audio-testing.js"></script> +<script src="biquad-filters.js"></script> +<script src="biquad-testing.js"></script> +<script src="webaudio.js" type="text/javascript"></script> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); + +addLoadEvent(function() { + // Create offline audio context. + var context = new OfflineAudioContext(2, sampleRate * renderLengthSeconds, sampleRate); + + // The filters we want to test. + var filterParameters = [{cutoff : 0, q : 10, gain : 10 }, + {cutoff : 1, q : 10, gain : 10 }, + {cutoff : .5, q : 0, gain : 10 }, + {cutoff : 0.25, q : 10, gain : 10 }, + ]; + + createTestAndRun(context, "peaking", filterParameters); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/blink/test_biquadFilterNodeTail.html b/dom/media/webaudio/test/blink/test_biquadFilterNodeTail.html new file mode 100644 index 0000000000..cc170df2a5 --- /dev/null +++ b/dom/media/webaudio/test/blink/test_biquadFilterNodeTail.html @@ -0,0 +1,76 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test BiquadFilterNode All Pass Filter</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script src="audio-testing.js"></script> +<script src="biquad-filters.js"></script> +<script src="biquad-testing.js"></script> +<script src="webaudio.js" type="text/javascript"></script> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); + +addLoadEvent(function() { + // A high sample rate shows the issue more clearly. + var sampleRate = 192000; + // Some short duration because we don't need to run the test for very long. + var testDurationSec = 0.5; + var testDurationFrames = testDurationSec * sampleRate; + + // Amplitude experimentally determined to give a biquad output close to 1. (No attempt was + // made to produce exactly 1; it's not needed.) + var sourceAmplitude = 100; + + // The output of the biquad filter should not change by more than this much between output + // samples. Threshold was determined experimentally. + var glitchThreshold = 0.01292; + + // Test that a Biquad filter doesn't have it's output terminated because the input has gone + // away. Generally, when a source node is finished, it disconnects itself from any downstream + // nodes. This is the correct behavior. Nodes that have no inputs (disconnected) are + // generally assumed to output zeroes. This is also desired behavior. However, biquad + // filters have memory so they should not suddenly output zeroes when the input is + // disconnected. This test checks to see if the output doesn't suddenly change to zero. + var context = new OfflineAudioContext(1, testDurationFrames, sampleRate); + + // Create an impulse source. + var buffer = context.createBuffer(1, 1, context.sampleRate); + buffer.getChannelData(0)[0] = sourceAmplitude; + var source = context.createBufferSource(); + source.buffer = buffer; + + // Create the biquad filter. It doesn't really matter what kind, so the default filter type + // and parameters is fine. Connect the source to it. + var biquad = context.createBiquadFilter(); + source.connect(biquad); + biquad.connect(context.destination); + + source.start(); + + context.startRendering().then(function(result) { + // There should be no large discontinuities in the output + var buffer = result.getChannelData(0); + var maxGlitchIndex = 0; + var maxGlitchValue = 0.0; + for (var i = 1; i < buffer.length; i++) { + var diff = Math.abs(buffer[i-1] - buffer[i]); + if (diff >= glitchThreshold) { + if (diff > maxGlitchValue) { + maxGlitchIndex = i; + maxGlitchValue = diff; + } + } + } + ok(maxGlitchIndex == 0, 'glitches detected in biquad output: maximum glitch at ' + maxGlitchIndex + ' with diff of ' + maxGlitchValue); + SimpleTest.finish(); + }) +}); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/blink/test_iirFilterNode.html b/dom/media/webaudio/test/blink/test_iirFilterNode.html new file mode 100644 index 0000000000..0ef7a37e3b --- /dev/null +++ b/dom/media/webaudio/test/blink/test_iirFilterNode.html @@ -0,0 +1,467 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test IIRFilterNode GetFrequencyResponse</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <script type="text/javascript" src="biquad-filters.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); + +addLoadEvent(function() { + var sampleRate = 48000; + var testDurationSec = 1; + var testFrames = testDurationSec * sampleRate; + + var testPromises = [] + testPromises.push(function () { + // Test that the feedback coefficients are normalized. Do this be creating two + // IIRFilterNodes. One has normalized coefficients, and one doesn't. Compute the + // difference and make sure they're the same. + var context = new OfflineAudioContext(2, testFrames, sampleRate); + + // Use a simple impulse as the source. + var buffer = context.createBuffer(1, 1, sampleRate); + buffer.getChannelData(0)[0] = 1; + var source = context.createBufferSource(); + source.buffer = buffer; + + // Gain node for computing the difference between the filters. + var gain = context.createGain(); + gain.gain.value = -1; + + // The IIR filters. Use a common feedforward array. + var ff = [1]; + + var fb1 = [1, .9]; + + var fb2 = new Float64Array(2); + // Scale the feedback coefficients by an arbitrary factor. + var coefScaleFactor = 2; + for (var k = 0; k < fb2.length; ++k) { + fb2[k] = coefScaleFactor * fb1[k]; + } + + var iir1 = context.createIIRFilter(ff, fb1); + var iir2 = context.createIIRFilter(ff, fb2); + + // Create the graph. The output of iir1 (normalized coefficients) is channel 0, and the + // output of iir2 (unnormalized coefficients), with appropriate scaling, is channel 1. + var merger = context.createChannelMerger(2); + source.connect(iir1); + source.connect(iir2); + iir1.connect(merger, 0, 0); + iir2.connect(gain); + + // The gain for the gain node should be set to compensate for the scaling of the + // coefficients. Since iir2 has scaled the coefficients by coefScaleFactor, the output is + // reduced by the same factor, so adjust the gain to scale the output of iir2 back up. + gain.gain.value = coefScaleFactor; + gain.connect(merger, 0, 1); + + merger.connect(context.destination); + + source.start(); + + // Rock and roll! + + return context.startRendering().then(function (result) { + // Find the max amplitude of the result, which should be near zero. + var iir1Data = result.getChannelData(0); + var iir2Data = result.getChannelData(1); + + // Threshold isn't exactly zero because the arithmetic is done differently between the + // IIRFilterNode and the BiquadFilterNode. + compareChannels(iir1Data, iir2Data); + }); + }()); + + testPromises.push(function () { + // Create a simple 1-zero filter and compare with the expected output. + var context = new OfflineAudioContext(1, testFrames, sampleRate); + + // Use a simple impulse as the source + var buffer = context.createBuffer(1, 1, sampleRate); + buffer.getChannelData(0)[0] = 1; + var source = context.createBufferSource(); + source.buffer = buffer; + + // The filter is y(n) = 0.5*(x(n) + x(n-1)), a simple 2-point moving average. This is + // rather arbitrary; keep it simple. + + var iir = context.createIIRFilter([0.5, 0.5], [1]); + + // Create the graph + source.connect(iir); + iir.connect(context.destination); + + // Rock and roll! + source.start(); + + return context.startRendering().then(function (result) { + var actual = result.getChannelData(0); + var expected = new Float64Array(testFrames); + // The filter is a simple 2-point moving average of an impulse, so the first two values + // are non-zero and the rest are zero. + expected[0] = 0.5; + expected[1] = 0.5; + compareChannels(actual, expected); + }); + }()); + + testPromises.push(function () { + // Create a simple 1-pole filter and compare with the expected output. + + // The filter is y(n) + c*y(n-1)= x(n). The analytical response is (-c)^n, so choose a + // suitable number of frames to run the test for where the output isn't flushed to zero. + var c = 0.9; + var eps = 1e-20; + var duration = Math.floor(Math.log(eps) / Math.log(Math.abs(c))); + var context = new OfflineAudioContext(1, duration, sampleRate); + + // Use a simple impulse as the source + var buffer = context.createBuffer(1, 1, sampleRate); + buffer.getChannelData(0)[0] = 1; + var source = context.createBufferSource(); + source.buffer = buffer; + + var iir = context.createIIRFilter([1], [1, c]); + + // Create the graph + source.connect(iir); + iir.connect(context.destination); + + // Rock and roll! + source.start(); + + return context.startRendering().then(function (result) { + var actual = result.getChannelData(0); + var expected = new Float64Array(actual.length); + + // The filter is a simple 1-pole filter: y(n) = -c*y(n-k)+x(n), with an impulse as the + // input. + expected[0] = 1; + for (k = 1; k < testFrames; ++k) { + expected[k] = -c * expected[k-1]; + } + + compareChannels(actual, expected); + }); + }()); + + // This function creates an IIRFilterNode equivalent to the specified + // BiquadFilterNode and compares the outputs. The + // outputs from the two filters should be virtually identical. + function testWithBiquadFilter(filterType) { + var context = new OfflineAudioContext(2, testFrames, sampleRate); + + // Use a constant (step function) as the source + var buffer = context.createBuffer(1, testFrames, context.sampleRate); + for (var i = 0; i < testFrames; ++i) { + buffer.getChannelData(0)[i] = 1; + } + var source = context.createBufferSource(); + source.buffer = buffer; + + // Create the biquad. Choose some rather arbitrary values for Q and gain for the biquad + // so that the shelf filters aren't identical. + var biquad = context.createBiquadFilter(); + biquad.type = filterType; + biquad.Q.value = 10; + biquad.gain.value = 10; + + // Create the equivalent IIR Filter node by computing the coefficients of the given biquad + // filter type. + var nyquist = sampleRate / 2; + var coef = createFilter(filterType, + biquad.frequency.value / nyquist, + biquad.Q.value, + biquad.gain.value); + + var iir = context.createIIRFilter([coef.b0, coef.b1, coef.b2], [1, coef.a1, coef.a2]); + + var merger = context.createChannelMerger(2); + // Create the graph + source.connect(biquad); + source.connect(iir); + + biquad.connect(merger, 0, 0); + iir.connect(merger, 0, 1); + + merger.connect(context.destination); + + // Rock and roll! + source.start(); + + return context.startRendering().then(function (result) { + // Find the max amplitude of the result, which should be near zero. + var expected = result.getChannelData(0); + var actual = result.getChannelData(1); + compareChannels(actual, expected); + }); + } + + biquadFilterTypes = ["lowpass", "highpass", "bandpass", "notch", + "allpass", "lowshelf", "highshelf", "peaking"]; + + // Create a set of tasks based on biquadTestConfigs. + for (var i = 0; i < biquadFilterTypes.length; ++i) { + testPromises.push(testWithBiquadFilter(biquadFilterTypes[i])); + } + + testPromises.push(function () { + // Multi-channel test. Create a biquad filter and the equivalent IIR filter. Filter the + // same multichannel signal and compare the results. + var nChannels = 3; + var context = new OfflineAudioContext(nChannels, testFrames, sampleRate); + + // Create a set of oscillators as the multi-channel source. + var source = []; + + for (k = 0; k < nChannels; ++k) { + source[k] = context.createOscillator(); + source[k].type = "sawtooth"; + // The frequency of the oscillator is pretty arbitrary, but each oscillator should have a + // different frequency. + source[k].frequency.value = 100 + k * 100; + } + + var merger = context.createChannelMerger(3); + + var biquad = context.createBiquadFilter(); + + // Create the equivalent IIR Filter node. + var nyquist = sampleRate / 2; + var coef = createFilter(biquad.type, + biquad.frequency.value / nyquist, + biquad.Q.value, + biquad.gain.value); + var fb = [1, coef.a1, coef.a2]; + var ff = [coef.b0, coef.b1, coef.b2]; + + var iir = context.createIIRFilter(ff, fb); + // Gain node to compute the difference between the IIR and biquad filter. + var gain = context.createGain(); + gain.gain.value = -1; + + // Create the graph. + for (k = 0; k < nChannels; ++k) + source[k].connect(merger, 0, k); + + merger.connect(biquad); + merger.connect(iir); + iir.connect(gain); + biquad.connect(context.destination); + gain.connect(context.destination); + + for (k = 0; k < nChannels; ++k) + source[k].start(); + + return context.startRendering().then(function (result) { + var errorThresholds = [3.7671e-5, 3.0071e-5, 2.6241e-5]; + + // Check the difference signal on each channel + for (channel = 0; channel < result.numberOfChannels; ++channel) { + // Find the max amplitude of the result, which should be near zero. + var data = result.getChannelData(channel); + var maxError = data.reduce(function(reducedValue, currentValue) { + return Math.max(reducedValue, Math.abs(currentValue)); + }); + + ok(maxError <= errorThresholds[channel], "Max difference between IIR and Biquad on channel " + channel); + } + }); + }()); + + testPromises.push(function () { + // Apply an IIRFilter to the given input signal. + // + // IIR filter in the time domain is + // + // y[n] = sum(ff[k]*x[n-k], k, 0, M) - sum(fb[k]*y[n-k], k, 1, N) + // + function iirFilter(input, feedforward, feedback) { + // For simplicity, create an x buffer that contains the input, and a y buffer that contains + // the output. Both of these buffers have an initial work space to implement the initial + // memory of the filter. + var workSize = Math.max(feedforward.length, feedback.length); + var x = new Float32Array(input.length + workSize); + + // Float64 because we want to match the implementation that uses doubles to minimize + // roundoff. + var y = new Float64Array(input.length + workSize); + + // Copy the input over. + for (var k = 0; k < input.length; ++k) + x[k + feedforward.length] = input[k]; + + // Run the filter + for (var n = 0; n < input.length; ++n) { + var index = n + workSize; + var yn = 0; + for (var k = 0; k < feedforward.length; ++k) + yn += feedforward[k] * x[index - k]; + for (var k = 0; k < feedback.length; ++k) + yn -= feedback[k] * y[index - k]; + + y[index] = yn; + } + + return y.slice(workSize).map(Math.fround); + } + + // Cascade the two given biquad filters to create one IIR filter. + function cascadeBiquads(f1Coef, f2Coef) { + // The biquad filters are: + // + // f1 = (b10 + b11/z + b12/z^2)/(1 + a11/z + a12/z^2); + // f2 = (b20 + b21/z + b22/z^2)/(1 + a21/z + a22/z^2); + // + // To cascade them, multiply the two transforms together to get a fourth order IIR filter. + + var numProduct = [f1Coef.b0 * f2Coef.b0, + f1Coef.b0 * f2Coef.b1 + f1Coef.b1 * f2Coef.b0, + f1Coef.b0 * f2Coef.b2 + f1Coef.b1 * f2Coef.b1 + f1Coef.b2 * f2Coef.b0, + f1Coef.b1 * f2Coef.b2 + f1Coef.b2 * f2Coef.b1, + f1Coef.b2 * f2Coef.b2 + ]; + + var denProduct = [1, + f2Coef.a1 + f1Coef.a1, + f2Coef.a2 + f1Coef.a1 * f2Coef.a1 + f1Coef.a2, + f1Coef.a1 * f2Coef.a2 + f1Coef.a2 * f2Coef.a1, + f1Coef.a2 * f2Coef.a2 + ]; + + return { + ff: numProduct, + fb: denProduct + } + } + + // Find the magnitude of the root of the quadratic that has the maximum magnitude. + // + // The quadratic is z^2 + a1 * z + a2 and we want the root z that has the largest magnitude. + function largestRootMagnitude(a1, a2) { + var discriminant = a1 * a1 - 4 * a2; + if (discriminant < 0) { + // Complex roots: -a1/2 +/- i*sqrt(-d)/2. Thus the magnitude of each root is the same + // and is sqrt(a1^2/4 + |d|/4) + var d = Math.sqrt(-discriminant); + return Math.hypot(a1 / 2, d / 2); + } else { + // Real roots + var d = Math.sqrt(discriminant); + return Math.max(Math.abs((-a1 + d) / 2), Math.abs((-a1 - d) / 2)); + } + } + + // Cascade 2 lowpass biquad filters and compare that with the equivalent 4th order IIR + // filter. + + var nyquist = sampleRate / 2; + // Compute the coefficients of a lowpass filter. + + // First some preliminary stuff. Compute the coefficients of the biquad. This is used to + // figure out how frames to use in the test. + var biquadType = "lowpass"; + var biquadCutoff = 350; + var biquadQ = 5; + var biquadGain = 1; + + var coef = createFilter(biquadType, + biquadCutoff / nyquist, + biquadQ, + biquadGain); + + // Cascade the biquads together to create an equivalent IIR filter. + var cascade = cascadeBiquads(coef, coef); + + // Since we're cascading two identical biquads, the root of denominator of the IIR filter is + // repeated, so the root of the denominator with the largest magnitude occurs twice. The + // impulse response of the IIR filter will be roughly c*(r*r)^n at time n, where r is the + // root of largest magnitude. This approximation gets better as n increases. We can use + // this to get a rough idea of when the response has died down to a small value. + + // This is the value we will use to determine how many frames to render. Rendering too many + // is a waste of time and also makes it hard to compare the actual result to the expected + // because the magnitudes are so small that they could be mostly round-off noise. + // + // Find magnitude of the root with largest magnitude + var rootMagnitude = largestRootMagnitude(coef.a1, coef.a2); + + // Find n such that |r|^(2*n) <= eps. That is, n = log(eps)/(2*log(r)). Somewhat + // arbitrarily choose eps = 1e-20; + var eps = 1e-20; + var framesForTest = Math.floor(Math.log(eps) / (2 * Math.log(rootMagnitude))); + + // We're ready to create the graph for the test. The offline context has two channels: + // channel 0 is the expected (cascaded biquad) result and channel 1 is the actual IIR filter + // result. + var context = new OfflineAudioContext(2, framesForTest, sampleRate); + + // Use a simple impulse with a large (arbitrary) amplitude as the source + var amplitude = 1; + var buffer = context.createBuffer(1, testFrames, sampleRate); + buffer.getChannelData(0)[0] = amplitude; + var source = context.createBufferSource(); + source.buffer = buffer; + + // Create the two biquad filters. Doesn't really matter what, but for simplicity we choose + // identical lowpass filters with the same parameters. + var biquad1 = context.createBiquadFilter(); + biquad1.type = biquadType; + biquad1.frequency.value = biquadCutoff; + biquad1.Q.value = biquadQ; + + var biquad2 = context.createBiquadFilter(); + biquad2.type = biquadType; + biquad2.frequency.value = biquadCutoff; + biquad2.Q.value = biquadQ; + + var iir = context.createIIRFilter(cascade.ff, cascade.fb); + + // Create the merger to get the signals into multiple channels + var merger = context.createChannelMerger(2); + + // Create the graph, filtering the source through two biquads. + source.connect(biquad1); + biquad1.connect(biquad2); + biquad2.connect(merger, 0, 0); + + source.connect(iir); + iir.connect(merger, 0, 1); + + merger.connect(context.destination); + + // Now filter the source through the IIR filter. + var y = iirFilter(buffer.getChannelData(0), cascade.ff, cascade.fb); + + // Rock and roll! + source.start(); + + return context.startRendering().then(function(result) { + var expected = result.getChannelData(0); + var actual = result.getChannelData(1); + + compareChannels(actual, expected); + + }); + }()); + + // Wait for all tests + Promise.all(testPromises).then(function () { + SimpleTest.finish(); + }, function () { + SimpleTest.finish(); + }); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/blink/test_iirFilterNodeGetFrequencyResponse.html b/dom/media/webaudio/test/blink/test_iirFilterNodeGetFrequencyResponse.html new file mode 100644 index 0000000000..09782f73be --- /dev/null +++ b/dom/media/webaudio/test/blink/test_iirFilterNodeGetFrequencyResponse.html @@ -0,0 +1,97 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test IIRFilterNode GetFrequencyResponse</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <script type="text/javascript" src="biquad-filters.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + // Modified from WebKit/LayoutTests/webaudio/iirfilter-getFrequencyResponse.html + var sampleRate = 48000; + var testDurationSec = 0.01; + + // Compute a set of linearly spaced frequencies. + function createFrequencies(nFrequencies, sampleRate) + { + var frequencies = new Float32Array(nFrequencies); + var nyquist = sampleRate / 2; + var freqDelta = nyquist / nFrequencies; + + for (var k = 0; k < nFrequencies; ++k) { + frequencies[k] = k * freqDelta; + } + + return frequencies; + } + + // Number of frequency samples to take. + var numberOfFrequencies = 1000; + + var context = new OfflineAudioContext(1, testDurationSec * sampleRate, sampleRate); + + var frequencies = createFrequencies(numberOfFrequencies, context.sampleRate); + + // 1-Pole IIR Filter + var iir = context.createIIRFilter([1], [1, -0.9]); + + var iirMag = new Float32Array(numberOfFrequencies); + var iirPhase = new Float32Array(numberOfFrequencies); + var trueMag = new Float32Array(numberOfFrequencies); + var truePhase = new Float32Array(numberOfFrequencies); + + // The IIR filter is + // H(z) = 1/(1 - 0.9*z^(-1)). + // + // The frequency response is + // H(exp(j*w)) = 1/(1 - 0.9*exp(-j*w)). + // + // Thus, the magnitude is + // |H(exp(j*w))| = 1/sqrt(1.81-1.8*cos(w)). + // + // The phase is + // arg(H(exp(j*w)) = atan(0.9*sin(w)/(.9*cos(w)-1)) + + var frequencyScale = Math.PI / (sampleRate / 2); + + for (var k = 0; k < frequencies.length; ++k) { + var omega = frequencyScale * frequencies[k]; + trueMag[k] = 1/Math.sqrt(1.81-1.8*Math.cos(omega)); + truePhase[k] = Math.atan(0.9 * Math.sin(omega) / (0.9 * Math.cos(omega) - 1)); + } + + iir.getFrequencyResponse(frequencies, iirMag, iirPhase); + compareChannels(iirMag, trueMag); + compareChannels(iirPhase, truePhase); + + // Compare IIR and Biquad Filter + // Create an IIR filter equivalent to the biquad filter. Compute the frequency response for both and verify that they are the same. + var biquad = context.createBiquadFilter(); + var coef = createFilter(biquad.type, + biquad.frequency.value / (context.sampleRate / 2), + biquad.Q.value, + biquad.gain.value); + + var iir = context.createIIRFilter([coef.b0, coef.b1, coef.b2], [1, coef.a1, coef.a2]); + + var biquadMag = new Float32Array(numberOfFrequencies); + var biquadPhase = new Float32Array(numberOfFrequencies); + var iirMag = new Float32Array(numberOfFrequencies); + var iirPhase = new Float32Array(numberOfFrequencies); + + biquad.getFrequencyResponse(frequencies, biquadMag, biquadPhase); + iir.getFrequencyResponse(frequencies, iirMag, iirPhase); + compareChannels(iirMag, biquadMag); + compareChannels(iirPhase, biquadPhase); + + SimpleTest.finish(); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/corsServer.sjs b/dom/media/webaudio/test/corsServer.sjs new file mode 100644 index 0000000000..de77ef71ed --- /dev/null +++ b/dom/media/webaudio/test/corsServer.sjs @@ -0,0 +1,26 @@ +function handleRequest(request, response) { + var file = Cc["@mozilla.org/file/directory_service;1"] + .getService(Ci.nsIProperties) + .get("CurWorkD", Ci.nsIFile); + var fis = Cc["@mozilla.org/network/file-input-stream;1"].createInstance( + Ci.nsIFileInputStream + ); + var bis = Cc["@mozilla.org/binaryinputstream;1"].createInstance( + Ci.nsIBinaryInputStream + ); + var paths = "tests/dom/media/webaudio/test/small-shot.ogg"; + var split = paths.split("/"); + for (var i = 0; i < split.length; ++i) { + file.append(split[i]); + } + fis.init(file, -1, -1, false); + bis.setInputStream(fis); + var bytes = bis.readBytes(bis.available()); + response.setHeader("Content-Type", "video/ogg", false); + response.setHeader("Content-Length", "" + bytes.length, false); + response.setHeader("Access-Control-Allow-Origin", "*", false); + response.write(bytes, bytes.length); + response.processAsync(); + response.finish(); + bis.close(); +} diff --git a/dom/media/webaudio/test/file_nodeCreationDocumentGone.html b/dom/media/webaudio/test/file_nodeCreationDocumentGone.html new file mode 100644 index 0000000000..aedf16702f --- /dev/null +++ b/dom/media/webaudio/test/file_nodeCreationDocumentGone.html @@ -0,0 +1,4 @@ +<!DOCTYPE html> +<html><script> +var context = new AudioContext(); +setTimeout(function(){window.close();},1000);</script></html> diff --git a/dom/media/webaudio/test/generate-test-files.py b/dom/media/webaudio/test/generate-test-files.py new file mode 100755 index 0000000000..af4bc3bb49 --- /dev/null +++ b/dom/media/webaudio/test/generate-test-files.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 + +import os + +rates = [44100, 48000] +channels = [1, 2] +duration = "0.5" +frequency = "1000" +volume = "-3dB" +name = "half-a-second" +formats = { + "aac-in-adts": [{"codec": "aac", "extension": "aac"}], + "mp3": [{"codec": "libmp3lame", "extension": "mp3"}], + "mp4": [ + { + "codec": "libopus", + "extension": "mp4", + }, + {"codec": "aac", "extension": "mp4"}, + ], + "ogg": [ + {"codec": "libvorbis", "extension": "ogg"}, + {"codec": "libopus", "extension": "opus"}, + ], + "flac": [ + {"codec": "flac", "extension": "flac"}, + ], + "webm": [ + {"codec": "libopus", "extension": "webm"}, + {"codec": "libvorbis", "extension": "webm"}, + ], +} + +for rate in rates: + for channel_count in channels: + wav_filename = "{}-{}ch-{}.wav".format(name, channel_count, rate) + wav_command = "sox -V -r {} -n -b 16 -c {} {} synth {} sin {} vol {}".format( + rate, channel_count, wav_filename, duration, frequency, volume + ) + print(wav_command) + os.system(wav_command) + for container, codecs in formats.items(): + for codec in codecs: + encoded_filename = "{}-{}ch-{}-{}.{}".format( + name, channel_count, rate, codec["codec"], codec["extension"] + ) + print(encoded_filename) + encoded_command = "ffmpeg -y -i {} -acodec {} {}".format( + wav_filename, codec["codec"], encoded_filename + ) + print(encoded_command) + os.system(encoded_command) diff --git a/dom/media/webaudio/test/half-a-second-1ch-44100-aac-afconvert.mp4 b/dom/media/webaudio/test/half-a-second-1ch-44100-aac-afconvert.mp4 Binary files differnew file mode 100644 index 0000000000..7e3b008376 --- /dev/null +++ b/dom/media/webaudio/test/half-a-second-1ch-44100-aac-afconvert.mp4 diff --git a/dom/media/webaudio/test/half-a-second-1ch-44100-aac.aac b/dom/media/webaudio/test/half-a-second-1ch-44100-aac.aac Binary files differnew file mode 100644 index 0000000000..28435589ae --- /dev/null +++ b/dom/media/webaudio/test/half-a-second-1ch-44100-aac.aac diff --git a/dom/media/webaudio/test/half-a-second-1ch-44100-aac.mp4 b/dom/media/webaudio/test/half-a-second-1ch-44100-aac.mp4 Binary files differnew file mode 100644 index 0000000000..c603635f6e --- /dev/null +++ b/dom/media/webaudio/test/half-a-second-1ch-44100-aac.mp4 diff --git a/dom/media/webaudio/test/half-a-second-1ch-44100-alaw.wav b/dom/media/webaudio/test/half-a-second-1ch-44100-alaw.wav Binary files differnew file mode 100644 index 0000000000..4e8b6e7d45 --- /dev/null +++ b/dom/media/webaudio/test/half-a-second-1ch-44100-alaw.wav diff --git a/dom/media/webaudio/test/half-a-second-1ch-44100-flac.flac b/dom/media/webaudio/test/half-a-second-1ch-44100-flac.flac Binary files differnew file mode 100644 index 0000000000..49f71674f5 --- /dev/null +++ b/dom/media/webaudio/test/half-a-second-1ch-44100-flac.flac diff --git a/dom/media/webaudio/test/half-a-second-1ch-44100-libmp3lame.mp3 b/dom/media/webaudio/test/half-a-second-1ch-44100-libmp3lame.mp3 Binary files differnew file mode 100644 index 0000000000..fb1ec45af2 --- /dev/null +++ b/dom/media/webaudio/test/half-a-second-1ch-44100-libmp3lame.mp3 diff --git a/dom/media/webaudio/test/half-a-second-1ch-44100-libopus.mp4 b/dom/media/webaudio/test/half-a-second-1ch-44100-libopus.mp4 Binary files differnew file mode 100644 index 0000000000..3a7df17582 --- /dev/null +++ b/dom/media/webaudio/test/half-a-second-1ch-44100-libopus.mp4 diff --git a/dom/media/webaudio/test/half-a-second-1ch-44100-libopus.opus b/dom/media/webaudio/test/half-a-second-1ch-44100-libopus.opus Binary files differnew file mode 100644 index 0000000000..304b9f9d1d --- /dev/null +++ b/dom/media/webaudio/test/half-a-second-1ch-44100-libopus.opus diff --git a/dom/media/webaudio/test/half-a-second-1ch-44100-libopus.webm b/dom/media/webaudio/test/half-a-second-1ch-44100-libopus.webm Binary files differnew file mode 100644 index 0000000000..71be30de9c --- /dev/null +++ b/dom/media/webaudio/test/half-a-second-1ch-44100-libopus.webm diff --git a/dom/media/webaudio/test/half-a-second-1ch-44100-libvorbis.ogg b/dom/media/webaudio/test/half-a-second-1ch-44100-libvorbis.ogg Binary files differnew file mode 100644 index 0000000000..ab5ec06e50 --- /dev/null +++ b/dom/media/webaudio/test/half-a-second-1ch-44100-libvorbis.ogg diff --git a/dom/media/webaudio/test/half-a-second-1ch-44100-libvorbis.webm b/dom/media/webaudio/test/half-a-second-1ch-44100-libvorbis.webm Binary files differnew file mode 100644 index 0000000000..b5142703ba --- /dev/null +++ b/dom/media/webaudio/test/half-a-second-1ch-44100-libvorbis.webm diff --git a/dom/media/webaudio/test/half-a-second-1ch-44100-mulaw.wav b/dom/media/webaudio/test/half-a-second-1ch-44100-mulaw.wav Binary files differnew file mode 100644 index 0000000000..5b6c1edcb4 --- /dev/null +++ b/dom/media/webaudio/test/half-a-second-1ch-44100-mulaw.wav diff --git a/dom/media/webaudio/test/half-a-second-1ch-44100.wav b/dom/media/webaudio/test/half-a-second-1ch-44100.wav Binary files differnew file mode 100644 index 0000000000..0028a66007 --- /dev/null +++ b/dom/media/webaudio/test/half-a-second-1ch-44100.wav diff --git a/dom/media/webaudio/test/half-a-second-1ch-48000-aac.aac b/dom/media/webaudio/test/half-a-second-1ch-48000-aac.aac Binary files differnew file mode 100644 index 0000000000..e1c5ba4631 --- /dev/null +++ b/dom/media/webaudio/test/half-a-second-1ch-48000-aac.aac diff --git a/dom/media/webaudio/test/half-a-second-1ch-48000-aac.mp4 b/dom/media/webaudio/test/half-a-second-1ch-48000-aac.mp4 Binary files differnew file mode 100644 index 0000000000..089d2a93e1 --- /dev/null +++ b/dom/media/webaudio/test/half-a-second-1ch-48000-aac.mp4 diff --git a/dom/media/webaudio/test/half-a-second-1ch-48000-flac.flac b/dom/media/webaudio/test/half-a-second-1ch-48000-flac.flac Binary files differnew file mode 100644 index 0000000000..783bbf2c97 --- /dev/null +++ b/dom/media/webaudio/test/half-a-second-1ch-48000-flac.flac diff --git a/dom/media/webaudio/test/half-a-second-1ch-48000-libmp3lame.mp3 b/dom/media/webaudio/test/half-a-second-1ch-48000-libmp3lame.mp3 Binary files differnew file mode 100644 index 0000000000..f9dfe29a89 --- /dev/null +++ b/dom/media/webaudio/test/half-a-second-1ch-48000-libmp3lame.mp3 diff --git a/dom/media/webaudio/test/half-a-second-1ch-48000-libopus.mp4 b/dom/media/webaudio/test/half-a-second-1ch-48000-libopus.mp4 Binary files differnew file mode 100644 index 0000000000..eb48fdac1b --- /dev/null +++ b/dom/media/webaudio/test/half-a-second-1ch-48000-libopus.mp4 diff --git a/dom/media/webaudio/test/half-a-second-1ch-48000-libopus.opus b/dom/media/webaudio/test/half-a-second-1ch-48000-libopus.opus Binary files differnew file mode 100644 index 0000000000..1b7cefcb3f --- /dev/null +++ b/dom/media/webaudio/test/half-a-second-1ch-48000-libopus.opus diff --git a/dom/media/webaudio/test/half-a-second-1ch-48000-libopus.webm b/dom/media/webaudio/test/half-a-second-1ch-48000-libopus.webm Binary files differnew file mode 100644 index 0000000000..c06e5d7583 --- /dev/null +++ b/dom/media/webaudio/test/half-a-second-1ch-48000-libopus.webm diff --git a/dom/media/webaudio/test/half-a-second-1ch-48000-libvorbis.ogg b/dom/media/webaudio/test/half-a-second-1ch-48000-libvorbis.ogg Binary files differnew file mode 100644 index 0000000000..ad88da968c --- /dev/null +++ b/dom/media/webaudio/test/half-a-second-1ch-48000-libvorbis.ogg diff --git a/dom/media/webaudio/test/half-a-second-1ch-48000-libvorbis.webm b/dom/media/webaudio/test/half-a-second-1ch-48000-libvorbis.webm Binary files differnew file mode 100644 index 0000000000..d63e2c31d3 --- /dev/null +++ b/dom/media/webaudio/test/half-a-second-1ch-48000-libvorbis.webm diff --git a/dom/media/webaudio/test/half-a-second-1ch-48000.wav b/dom/media/webaudio/test/half-a-second-1ch-48000.wav Binary files differnew file mode 100644 index 0000000000..d1fcb21134 --- /dev/null +++ b/dom/media/webaudio/test/half-a-second-1ch-48000.wav diff --git a/dom/media/webaudio/test/half-a-second-2ch-44100-aac.aac b/dom/media/webaudio/test/half-a-second-2ch-44100-aac.aac Binary files differnew file mode 100644 index 0000000000..d2255e982b --- /dev/null +++ b/dom/media/webaudio/test/half-a-second-2ch-44100-aac.aac diff --git a/dom/media/webaudio/test/half-a-second-2ch-44100-aac.mp4 b/dom/media/webaudio/test/half-a-second-2ch-44100-aac.mp4 Binary files differnew file mode 100644 index 0000000000..fbdd17e416 --- /dev/null +++ b/dom/media/webaudio/test/half-a-second-2ch-44100-aac.mp4 diff --git a/dom/media/webaudio/test/half-a-second-2ch-44100-flac.flac b/dom/media/webaudio/test/half-a-second-2ch-44100-flac.flac Binary files differnew file mode 100644 index 0000000000..9cc57c24bd --- /dev/null +++ b/dom/media/webaudio/test/half-a-second-2ch-44100-flac.flac diff --git a/dom/media/webaudio/test/half-a-second-2ch-44100-libmp3lame.mp3 b/dom/media/webaudio/test/half-a-second-2ch-44100-libmp3lame.mp3 Binary files differnew file mode 100644 index 0000000000..399df50839 --- /dev/null +++ b/dom/media/webaudio/test/half-a-second-2ch-44100-libmp3lame.mp3 diff --git a/dom/media/webaudio/test/half-a-second-2ch-44100-libopus.mp4 b/dom/media/webaudio/test/half-a-second-2ch-44100-libopus.mp4 Binary files differnew file mode 100644 index 0000000000..242fb3e12e --- /dev/null +++ b/dom/media/webaudio/test/half-a-second-2ch-44100-libopus.mp4 diff --git a/dom/media/webaudio/test/half-a-second-2ch-44100-libopus.opus b/dom/media/webaudio/test/half-a-second-2ch-44100-libopus.opus Binary files differnew file mode 100644 index 0000000000..a9311b5f9c --- /dev/null +++ b/dom/media/webaudio/test/half-a-second-2ch-44100-libopus.opus diff --git a/dom/media/webaudio/test/half-a-second-2ch-44100-libopus.webm b/dom/media/webaudio/test/half-a-second-2ch-44100-libopus.webm Binary files differnew file mode 100644 index 0000000000..ca8876d5e1 --- /dev/null +++ b/dom/media/webaudio/test/half-a-second-2ch-44100-libopus.webm diff --git a/dom/media/webaudio/test/half-a-second-2ch-44100-libvorbis.ogg b/dom/media/webaudio/test/half-a-second-2ch-44100-libvorbis.ogg Binary files differnew file mode 100644 index 0000000000..edf76edf89 --- /dev/null +++ b/dom/media/webaudio/test/half-a-second-2ch-44100-libvorbis.ogg diff --git a/dom/media/webaudio/test/half-a-second-2ch-44100-libvorbis.webm b/dom/media/webaudio/test/half-a-second-2ch-44100-libvorbis.webm Binary files differnew file mode 100644 index 0000000000..b01575c526 --- /dev/null +++ b/dom/media/webaudio/test/half-a-second-2ch-44100-libvorbis.webm diff --git a/dom/media/webaudio/test/half-a-second-2ch-44100.wav b/dom/media/webaudio/test/half-a-second-2ch-44100.wav Binary files differnew file mode 100644 index 0000000000..ae37e12813 --- /dev/null +++ b/dom/media/webaudio/test/half-a-second-2ch-44100.wav diff --git a/dom/media/webaudio/test/half-a-second-2ch-48000-aac.aac b/dom/media/webaudio/test/half-a-second-2ch-48000-aac.aac Binary files differnew file mode 100644 index 0000000000..d26803d76f --- /dev/null +++ b/dom/media/webaudio/test/half-a-second-2ch-48000-aac.aac diff --git a/dom/media/webaudio/test/half-a-second-2ch-48000-aac.mp4 b/dom/media/webaudio/test/half-a-second-2ch-48000-aac.mp4 Binary files differnew file mode 100644 index 0000000000..d7e3140580 --- /dev/null +++ b/dom/media/webaudio/test/half-a-second-2ch-48000-aac.mp4 diff --git a/dom/media/webaudio/test/half-a-second-2ch-48000-flac.flac b/dom/media/webaudio/test/half-a-second-2ch-48000-flac.flac Binary files differnew file mode 100644 index 0000000000..624e5280ff --- /dev/null +++ b/dom/media/webaudio/test/half-a-second-2ch-48000-flac.flac diff --git a/dom/media/webaudio/test/half-a-second-2ch-48000-libmp3lame.mp3 b/dom/media/webaudio/test/half-a-second-2ch-48000-libmp3lame.mp3 Binary files differnew file mode 100644 index 0000000000..bd009ebfb4 --- /dev/null +++ b/dom/media/webaudio/test/half-a-second-2ch-48000-libmp3lame.mp3 diff --git a/dom/media/webaudio/test/half-a-second-2ch-48000-libopus.mp4 b/dom/media/webaudio/test/half-a-second-2ch-48000-libopus.mp4 Binary files differnew file mode 100644 index 0000000000..89e2b19256 --- /dev/null +++ b/dom/media/webaudio/test/half-a-second-2ch-48000-libopus.mp4 diff --git a/dom/media/webaudio/test/half-a-second-2ch-48000-libopus.opus b/dom/media/webaudio/test/half-a-second-2ch-48000-libopus.opus Binary files differnew file mode 100644 index 0000000000..1e3c72b7b2 --- /dev/null +++ b/dom/media/webaudio/test/half-a-second-2ch-48000-libopus.opus diff --git a/dom/media/webaudio/test/half-a-second-2ch-48000-libopus.webm b/dom/media/webaudio/test/half-a-second-2ch-48000-libopus.webm Binary files differnew file mode 100644 index 0000000000..c7306df2ef --- /dev/null +++ b/dom/media/webaudio/test/half-a-second-2ch-48000-libopus.webm diff --git a/dom/media/webaudio/test/half-a-second-2ch-48000-libvorbis.ogg b/dom/media/webaudio/test/half-a-second-2ch-48000-libvorbis.ogg Binary files differnew file mode 100644 index 0000000000..63e6d2bb87 --- /dev/null +++ b/dom/media/webaudio/test/half-a-second-2ch-48000-libvorbis.ogg diff --git a/dom/media/webaudio/test/half-a-second-2ch-48000-libvorbis.webm b/dom/media/webaudio/test/half-a-second-2ch-48000-libvorbis.webm Binary files differnew file mode 100644 index 0000000000..589370d6e2 --- /dev/null +++ b/dom/media/webaudio/test/half-a-second-2ch-48000-libvorbis.webm diff --git a/dom/media/webaudio/test/half-a-second-2ch-48000.wav b/dom/media/webaudio/test/half-a-second-2ch-48000.wav Binary files differnew file mode 100644 index 0000000000..e9fc21e30b --- /dev/null +++ b/dom/media/webaudio/test/half-a-second-2ch-48000.wav diff --git a/dom/media/webaudio/test/invalid.txt b/dom/media/webaudio/test/invalid.txt new file mode 100644 index 0000000000..c44840faf1 --- /dev/null +++ b/dom/media/webaudio/test/invalid.txt @@ -0,0 +1 @@ +Surely this is not an audio file! diff --git a/dom/media/webaudio/test/invalidContent.flac b/dom/media/webaudio/test/invalidContent.flac new file mode 100644 index 0000000000..b2f4e1ff7a --- /dev/null +++ b/dom/media/webaudio/test/invalidContent.flac @@ -0,0 +1 @@ +fLaC diff --git a/dom/media/webaudio/test/layouttest-glue.js b/dom/media/webaudio/test/layouttest-glue.js new file mode 100644 index 0000000000..0ed0b9dc90 --- /dev/null +++ b/dom/media/webaudio/test/layouttest-glue.js @@ -0,0 +1,18 @@ +// Reimplementation of the LayoutTest API from Blink so we can easily port +// WebAudio tests to Simpletest, without touching the internals of the test. + +function testFailed(msg) { + ok(false, msg); +} + +function testPassed(msg) { + ok(true, msg); +} + +function finishJSTest() { + SimpleTest.finish(); +} + +function description(str) { + info(str); +} diff --git a/dom/media/webaudio/test/mochitest.toml b/dom/media/webaudio/test/mochitest.toml new file mode 100644 index 0000000000..74d17d4fc9 --- /dev/null +++ b/dom/media/webaudio/test/mochitest.toml @@ -0,0 +1,332 @@ +[DEFAULT] +tags = "mtg webaudio" +subsuite = "media" +support-files = [ + "8kHz-320kbps-6ch.aac", + "audio-expected.wav", + "audio-mono-expected-2.wav", + "audio-mono-expected.wav", + "audio-quad.wav", + "audio.ogv", + "audiovideo.mp4", + "audioBufferSourceNodeDetached_worker.js", + "corsServer.sjs", + "!/dom/events/test/event_leak_utils.js", + "file_nodeCreationDocumentGone.html", + "invalid.txt", + "invalidContent.flac", + "layouttest-glue.js", + "nil-packet.ogg", + "noaudio.webm", + "small-shot-expected.wav", + "small-shot-mono-expected.wav", + "small-shot.ogg", + "small-shot.mp3", + "sweep-300-330-1sec.opus", + "ting-44.1k-1ch.ogg", + "ting-44.1k-2ch.ogg", + "ting-48k-1ch.ogg", + "ting-48k-2ch.ogg", + "ting-44.1k-1ch.wav", + "ting-44.1k-2ch.wav", + "ting-48k-1ch.wav", + "ting-48k-2ch.wav", + "sine-440-10s.opus", + "webaudio.js", + # See ./generate-test-files.py + "half-a-second-1ch-44100-aac.aac", + "half-a-second-1ch-44100-aac.mp4", + "half-a-second-1ch-44100-alaw.wav", + "half-a-second-1ch-44100-flac.flac", + "half-a-second-1ch-44100-libmp3lame.mp3", + "half-a-second-1ch-44100-libopus.mp4", + "half-a-second-1ch-44100-libopus.opus", + "half-a-second-1ch-44100-libopus.webm", + "half-a-second-1ch-44100-libvorbis.ogg", + "half-a-second-1ch-44100-libvorbis.webm", + "half-a-second-1ch-44100-mulaw.wav", + "half-a-second-1ch-44100.wav", + "half-a-second-1ch-48000-aac.aac", + "half-a-second-1ch-48000-aac.mp4", + "half-a-second-1ch-48000-flac.flac", + "half-a-second-1ch-48000-libmp3lame.mp3", + "half-a-second-1ch-48000-libopus.mp4", + "half-a-second-1ch-48000-libopus.opus", + "half-a-second-1ch-48000-libopus.webm", + "half-a-second-1ch-48000-libvorbis.ogg", + "half-a-second-1ch-48000-libvorbis.webm", + "half-a-second-1ch-48000.wav", + "half-a-second-2ch-44100-aac.aac", + "half-a-second-2ch-44100-aac.mp4", + "half-a-second-2ch-44100-flac.flac", + "half-a-second-2ch-44100-libmp3lame.mp3", + "half-a-second-2ch-44100-libopus.mp4", + "half-a-second-2ch-44100-libopus.opus", + "half-a-second-2ch-44100-libopus.webm", + "half-a-second-2ch-44100-libvorbis.ogg", + "half-a-second-2ch-44100-libvorbis.webm", + "half-a-second-2ch-44100.wav", + "half-a-second-2ch-48000-aac.aac", + "half-a-second-2ch-48000-aac.mp4", + "half-a-second-2ch-48000-flac.flac", + "half-a-second-2ch-48000-libmp3lame.mp3", + "half-a-second-2ch-48000-libopus.mp4", + "half-a-second-2ch-48000-libopus.opus", + "half-a-second-2ch-48000-libopus.webm", + "half-a-second-2ch-48000-libvorbis.ogg", + "half-a-second-2ch-48000-libvorbis.webm", + "half-a-second-2ch-48000.wav", + "half-a-second-1ch-44100-aac-afconvert.mp4", + "waveformatextensible.wav", + "waveformatextensiblebadmask.wav", + "sixteen-frames.mp3", # only 16 frames of valid audio + "../../webrtc/tests/mochitests/mediaStreamPlayback.js", + "../../webrtc/tests/mochitests/head.js", +] + +["test_OfflineAudioContext.html"] + +["test_ScriptProcessorCollected1.html"] + +["test_WebAudioMemoryReporting.html"] + +["test_analyserNode.html"] +skip-if = ["!asan && os != android"] # These are tested in web-platform-tests, except on ASan and Android which don't run WPT. + +["test_analyserNodeMinimum.html"] +skip-if = ["!asan && os != android"] # These are tested in web-platform-tests, except on ASan and Android which don't run WPT. + +["test_analyserNodeOutput.html"] +skip-if = ["!asan && os != android"] # These are tested in web-platform-tests, except on ASan and Android which don't run WPT. + +["test_analyserNodePassThrough.html"] + +["test_analyserNodeWithGain.html"] +skip-if = ["!asan && os != android"] # These are tested in web-platform-tests, except on ASan and Android which don't run WPT. + +["test_analyserScale.html"] +skip-if = ["!asan && os != android"] # These are tested in web-platform-tests, except on ASan and Android which don't run WPT. + +["test_audioContextParams_recordNonDefaultSampleRate.html"] + +["test_audioContextParams_sampleRate.html"] + +["test_channelMergerNode.html"] + +["test_channelMergerNodeWithVolume.html"] + +["test_channelSplitterNode.html"] + +["test_channelSplitterNodeWithVolume.html"] + +["test_convolver-upmixing-1-channel-response.html"] +# This is a copy of +# testing/web-platform/tests/webaudio/the-audio-api/the-convolvernode-interface/convolver-upmixing-1-channel-response.html, +# but WPT are not run with ASan or Android builds. +skip-if = ["!asan && os != android"] + +["test_convolverNode.html"] + +["test_convolverNodeChannelCount.html"] + +["test_convolverNodeChannelInterpretationChanges.html"] + +["test_convolverNodeDelay.html"] + +["test_convolverNodeFiniteInfluence.html"] + +["test_convolverNodeNormalization.html"] + +["test_convolverNodeOOM.html"] +skip-if = [ + "asan", + "tsan", # 1672869 +] + +["test_convolverNodePassThrough.html"] + +["test_convolverNodeWithGain.html"] + +["test_convolverNode_mono_mono.html"] + +["test_currentTime.html"] + +["test_decodeAudioDataOnDetachedBuffer.html"] + +["test_decodeAudioDataPromise.html"] + +["test_decodeAudioError.html"] + +["test_decodeMultichannel.html"] + +["test_decodeOpusTail.html"] + +["test_decoderDelay.html"] + +["test_delayNode.html"] + +["test_delayNodeAtMax.html"] + +["test_delayNodeChannelChanges.html"] + +["test_delayNodeCycles.html"] + +["test_delayNodePassThrough.html"] + +["test_delayNodeSmallMaxDelay.html"] + +["test_delayNodeTailIncrease.html"] + +["test_delayNodeTailWithDisconnect.html"] + +["test_delayNodeTailWithGain.html"] + +["test_delayNodeTailWithReconnect.html"] + +["test_delayNodeWithGain.html"] + +["test_delaynode-channel-count-1.html"] +# This is a copy of +# testing/web-platform/tests/webaudio/the-audio-api/the-delaynode-interface/delaynode-channel-count-1.html +# but WPT are not run with ASan or Android builds. +skip-if = ["!asan && os != android"] + +["test_disconnectAll.html"] + +["test_disconnectAudioParam.html"] + +["test_disconnectAudioParamFromOutput.html"] + +["test_disconnectExceptions.html"] + +["test_disconnectFromAudioNode.html"] + +["test_disconnectFromAudioNodeAndOutput.html"] + +["test_disconnectFromAudioNodeAndOutputAndInput.html"] + +["test_disconnectFromAudioNodeMultipleConnection.html"] + +["test_disconnectFromOutput.html"] + +["test_dynamicsCompressorNode.html"] + +["test_dynamicsCompressorNodePassThrough.html"] + +["test_dynamicsCompressorNodeWithGain.html"] + +["test_event_listener_leaks.html"] + +["test_gainNode.html"] + +["test_gainNodeInLoop.html"] + +["test_gainNodePassThrough.html"] + +["test_iirFilterNodePassThrough.html"] + +["test_maxChannelCount.html"] + +["test_mixingRules.html"] + +["test_nodeCreationDocumentGone.html"] + +["test_nodeToParamConnection.html"] + +["test_notAllowedToStartAudioContextGC.html"] + +["test_offlineDestinationChannelCountLess.html"] + +["test_offlineDestinationChannelCountMore.html"] + +["test_oscillatorNode.html"] + +["test_oscillatorNode2.html"] + +["test_oscillatorNodeNegativeFrequency.html"] + +["test_oscillatorNodePassThrough.html"] + +["test_oscillatorNodeStart.html"] + +["test_oscillatorTypeChange.html"] + +["test_pannerNode.html"] + +["test_pannerNodeAbove.html"] + +["test_pannerNodeAtZeroDistance.html"] + +["test_pannerNodeChannelCount.html"] + +["test_pannerNodeHRTFSymmetry.html"] + +["test_pannerNodeTail.html"] + +["test_pannerNode_audioparam_distance.html"] + +["test_pannerNode_equalPower.html"] + +["test_pannerNode_maxDistance.html"] + +["test_periodicWave.html"] + +["test_periodicWaveBandLimiting.html"] + +["test_periodicWaveDisableNormalization.html"] + +["test_retrospective-exponentialRampToValueAtTime.html"] + +["test_retrospective-linearRampToValueAtTime.html"] + +["test_retrospective-setTargetAtTime.html"] + +["test_retrospective-setValueAtTime.html"] + +["test_retrospective-setValueCurveAtTime.html"] + +["test_scriptProcessorNode.html"] + +["test_scriptProcessorNodeChannelCount.html"] + +["test_scriptProcessorNodeNotConnected.html"] + +["test_scriptProcessorNodePassThrough.html"] + +["test_scriptProcessorNodeZeroInputOutput.html"] + +["test_scriptProcessorNode_playbackTime1.html"] + +["test_sequentialBufferSourceWithResampling.html"] + +["test_setValueCurveWithNonFiniteElements.html"] + +["test_singleSourceDest.html"] + +["test_slowStart.html"] + +["test_stereoPannerNode.html"] + +["test_stereoPannerNodePassThrough.html"] + +["test_stereoPanningWithGain.html"] + +["test_waveDecoder.html"] + +["test_waveShaper.html"] + +["test_waveShaperGain.html"] + +["test_waveShaperInvalidLengthCurve.html"] + +["test_waveShaperNoCurve.html"] + +["test_waveShaperPassThrough.html"] + +["test_webAudio_muteTab.html"] +scheme = "https" +skip-if = [ + "os == 'mac'", + "os == 'win'", + "os == 'android'", # Bug 1404995, no loopback devices on some platforms +] diff --git a/dom/media/webaudio/test/mochitest_audio.toml b/dom/media/webaudio/test/mochitest_audio.toml new file mode 100644 index 0000000000..56e612b102 --- /dev/null +++ b/dom/media/webaudio/test/mochitest_audio.toml @@ -0,0 +1,100 @@ +[DEFAULT] +tags = "mtg webaudio" +subsuite = "media" +support-files = [ + "audio-expected.wav", + "audio-mono-expected-2.wav", + "audio-mono-expected.wav", + "audio-quad.wav", + "audio.ogv", + "audiovideo.mp4", + "audioBufferSourceNodeDetached_worker.js", + "corsServer.sjs", + "!/dom/events/test/event_leak_utils.js", + "file_nodeCreationDocumentGone.html", + "invalid.txt", + "invalidContent.flac", + "layouttest-glue.js", + "nil-packet.ogg", + "noaudio.webm", + "small-shot-expected.wav", + "small-shot-mono-expected.wav", + "small-shot.ogg", + "small-shot.mp3", + "sweep-300-330-1sec.opus", + "ting-44.1k-1ch.ogg", + "ting-44.1k-2ch.ogg", + "ting-48k-1ch.ogg", + "ting-48k-2ch.ogg", + "ting-44.1k-1ch.wav", + "ting-44.1k-2ch.wav", + "ting-48k-1ch.wav", + "ting-48k-2ch.wav", + "sine-440-10s.opus", + "1856145.ogg", + "webaudio.js", + "../../webrtc/tests/mochitests/mediaStreamPlayback.js", + "../../webrtc/tests/mochitests/head.js", +] + +["test_AudioBuffer.html"] + +["test_AudioContext.html"] + +["test_AudioContext_disabled.html"] + +["test_AudioListener.html"] + +["test_AudioNodeDevtoolsAPI.html"] + +["test_AudioParamDevtoolsAPI.html"] + +["test_audioBufferSourceNode.html"] + +["test_audioBufferSourceNodeDetached.html"] + +["test_audioBufferSourceNodeEnded.html"] + +["test_audioBufferSourceNodeLazyLoopParam.html"] + +["test_audioBufferSourceNodeLoop.html"] + +["test_audioBufferSourceNodeLoopStartEnd.html"] + +["test_audioBufferSourceNodeLoopStartEndSame.html"] + +["test_audioBufferSourceNodeNoStart.html"] + +["test_audioBufferSourceNodeNullBuffer.html"] + +["test_audioBufferSourceNodeOffset.html"] + +["test_audioBufferSourceNodePassThrough.html"] + +["test_audioBufferSourceNodeRate.html"] + +["test_audioContextGC.html"] + +["test_audioContextSuspendResumeClose.html"] +tags = "capturestream" + +["test_audioDestinationNode.html"] + +["test_audioParamChaining.html"] + +["test_audioParamExponentialRamp.html"] + +["test_audioParamGain.html"] + +["test_audioParamLinearRamp.html"] + +["test_audioParamSetCurveAtTime.html"] + +["test_audioParamSetTargetAtTime.html"] + +["test_audioParamSetTargetAtTimeZeroTimeConstant.html"] + +["test_audioParamSetValueAtTime.html"] + +["test_audioParamTimelineDestinationOffset.html"] + diff --git a/dom/media/webaudio/test/mochitest_bugs.toml b/dom/media/webaudio/test/mochitest_bugs.toml new file mode 100644 index 0000000000..8e987d6065 --- /dev/null +++ b/dom/media/webaudio/test/mochitest_bugs.toml @@ -0,0 +1,89 @@ +[DEFAULT] +tags = "mtg webaudio" +subsuite = "media" +support-files = [ + "audio-expected.wav", + "audio-mono-expected-2.wav", + "audio-mono-expected.wav", + "audio-quad.wav", + "audio.ogv", + "audiovideo.mp4", + "audioBufferSourceNodeDetached_worker.js", + "corsServer.sjs", + "!/dom/events/test/event_leak_utils.js", + "file_nodeCreationDocumentGone.html", + "invalid.txt", + "invalidContent.flac", + "layouttest-glue.js", + "nil-packet.ogg", + "noaudio.webm", + "small-shot-expected.wav", + "small-shot-mono-expected.wav", + "small-shot.ogg", + "small-shot.mp3", + "sweep-300-330-1sec.opus", + "ting-44.1k-1ch.ogg", + "ting-44.1k-2ch.ogg", + "ting-48k-1ch.ogg", + "ting-48k-2ch.ogg", + "ting-44.1k-1ch.wav", + "ting-44.1k-2ch.wav", + "ting-48k-1ch.wav", + "ting-48k-2ch.wav", + "sine-440-10s.opus", + "webaudio.js", + "../../webrtc/tests/mochitests/mediaStreamPlayback.js", + "../../webrtc/tests/mochitests/head.js", +] + +["test_bug808374.html"] + +["test_bug827541.html"] + +["test_bug839753.html"] + +["test_bug845960.html"] + +["test_bug856771.html"] + +["test_bug866570.html"] + +["test_bug866737.html"] + +["test_bug867089.html"] + +["test_bug867174.html"] + +["test_bug873335.html"] + +["test_bug875221.html"] + +["test_bug875402.html"] + +["test_bug894150.html"] + +["test_bug956489.html"] + +["test_bug964376.html"] + +["test_bug966247.html"] +tags = "capturestream" + +["test_bug972678.html"] + +["test_bug1027864.html"] +skip-if = ["true"] #Bug 1650930 + +["test_bug1056032.html"] + +["test_bug1113634.html"] + +["test_bug1118372.html"] + +["test_bug1255618.html"] + +["test_bug1267579.html"] + +["test_bug1355798.html"] + +["test_bug1447273.html"] diff --git a/dom/media/webaudio/test/mochitest_media.toml b/dom/media/webaudio/test/mochitest_media.toml new file mode 100644 index 0000000000..c369afd942 --- /dev/null +++ b/dom/media/webaudio/test/mochitest_media.toml @@ -0,0 +1,76 @@ +[DEFAULT] +tags = "mtg webaudio" +subsuite = "media" +support-files = [ + "audio-expected.wav", + "audio-mono-expected-2.wav", + "audio-mono-expected.wav", + "audio-quad.wav", + "audio.ogv", + "audiovideo.mp4", + "audioBufferSourceNodeDetached_worker.js", + "corsServer.sjs", + "!/dom/events/test/event_leak_utils.js", + "file_nodeCreationDocumentGone.html", + "invalid.txt", + "invalidContent.flac", + "layouttest-glue.js", + "nil-packet.ogg", + "noaudio.webm", + "small-shot-expected.wav", + "small-shot-mono-expected.wav", + "small-shot.ogg", + "small-shot.mp3", + "sweep-300-330-1sec.opus", + "ting-44.1k-1ch.ogg", + "ting-44.1k-2ch.ogg", + "ting-48k-1ch.ogg", + "ting-48k-2ch.ogg", + "ting-44.1k-1ch.wav", + "ting-44.1k-2ch.wav", + "ting-48k-1ch.wav", + "ting-48k-2ch.wav", + "sine-440-10s.opus", + "webaudio.js", + "../../webrtc/tests/mochitests/mediaStreamPlayback.js", + "../../webrtc/tests/mochitests/head.js", +] + +["test_mediaDecoding.html"] + +["test_mediaElementAudioSourceNode.html"] +tags = "capturestream" + +["test_mediaElementAudioSourceNodeCrossOrigin.html"] +tags = "capturestream" + +["test_mediaElementAudioSourceNodeFidelity.html"] +tags = "capturestream" + +["test_mediaElementAudioSourceNodePassThrough.html"] +tags = "capturestream" + +["test_mediaElementAudioSourceNodeVideo.html"] +tags = "capturestream" + +["test_mediaStreamAudioDestinationNode.html"] + +["test_mediaStreamAudioSourceNode.html"] + +["test_mediaStreamAudioSourceNodeCrossOrigin.html"] +tags = "capturestream" + +["test_mediaStreamAudioSourceNodeNoGC.html"] +skip-if = ["os == 'mac' && debug"] # Bug 1756880 - lower frequency shutdown hangs +scheme = "https" + +["test_mediaStreamAudioSourceNodePassThrough.html"] + +["test_mediaStreamAudioSourceNodeResampling.html"] +tags = "capturestream" + +["test_mediaStreamTrackAudioSourceNode.html"] + +["test_mediaStreamTrackAudioSourceNodeCrossOrigin.html"] + +["test_mediaStreamTrackAudioSourceNodeVideo.html"] diff --git a/dom/media/webaudio/test/nil-packet.ogg b/dom/media/webaudio/test/nil-packet.ogg Binary files differnew file mode 100644 index 0000000000..7b00b5a63e --- /dev/null +++ b/dom/media/webaudio/test/nil-packet.ogg diff --git a/dom/media/webaudio/test/noaudio.webm b/dom/media/webaudio/test/noaudio.webm Binary files differnew file mode 100644 index 0000000000..9207017fb6 --- /dev/null +++ b/dom/media/webaudio/test/noaudio.webm diff --git a/dom/media/webaudio/test/sine-440-10s.opus b/dom/media/webaudio/test/sine-440-10s.opus Binary files differnew file mode 100644 index 0000000000..eb91020168 --- /dev/null +++ b/dom/media/webaudio/test/sine-440-10s.opus diff --git a/dom/media/webaudio/test/sixteen-frames.mp3 b/dom/media/webaudio/test/sixteen-frames.mp3 Binary files differnew file mode 100644 index 0000000000..1d15dcad59 --- /dev/null +++ b/dom/media/webaudio/test/sixteen-frames.mp3 diff --git a/dom/media/webaudio/test/small-shot-expected.wav b/dom/media/webaudio/test/small-shot-expected.wav Binary files differnew file mode 100644 index 0000000000..2faaa8258b --- /dev/null +++ b/dom/media/webaudio/test/small-shot-expected.wav diff --git a/dom/media/webaudio/test/small-shot-mono-expected.wav b/dom/media/webaudio/test/small-shot-mono-expected.wav Binary files differnew file mode 100644 index 0000000000..d4e2647e42 --- /dev/null +++ b/dom/media/webaudio/test/small-shot-mono-expected.wav diff --git a/dom/media/webaudio/test/small-shot.mp3 b/dom/media/webaudio/test/small-shot.mp3 Binary files differnew file mode 100644 index 0000000000..f9397a5106 --- /dev/null +++ b/dom/media/webaudio/test/small-shot.mp3 diff --git a/dom/media/webaudio/test/small-shot.ogg b/dom/media/webaudio/test/small-shot.ogg Binary files differnew file mode 100644 index 0000000000..1a41623f81 --- /dev/null +++ b/dom/media/webaudio/test/small-shot.ogg diff --git a/dom/media/webaudio/test/sweep-300-330-1sec.opus b/dom/media/webaudio/test/sweep-300-330-1sec.opus Binary files differnew file mode 100644 index 0000000000..619d1e0844 --- /dev/null +++ b/dom/media/webaudio/test/sweep-300-330-1sec.opus diff --git a/dom/media/webaudio/test/test_AudioBuffer.html b/dom/media/webaudio/test/test_AudioBuffer.html new file mode 100644 index 0000000000..05957f679e --- /dev/null +++ b/dom/media/webaudio/test/test_AudioBuffer.html @@ -0,0 +1,104 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test whether we can create an AudioContext interface</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script src="webaudio.js" type="text/javascript"></script> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + var context = new AudioContext(); + var buffer = context.createBuffer(2, 2048, context.sampleRate); + SpecialPowers.gc(); // Make sure that our channels are accessible after GC + ok(buffer, "Buffer was allocated successfully"); + is(buffer.sampleRate, context.sampleRate, "Correct sample rate"); + is(buffer.length, 2048, "Correct length"); + ok(Math.abs(buffer.duration - 2048 / context.sampleRate) < 0.0001, "Correct duration"); + is(buffer.numberOfChannels, 2, "Correct number of channels"); + for (var i = 0; i < buffer.numberOfChannels; ++i) { + var buf = buffer.getChannelData(i); + ok(buf, "Buffer index " + i + " exists"); + ok(buf instanceof Float32Array, "Result is a typed array"); + is(buf.length, buffer.length, "Correct length"); + var foundNonZero = false; + for (var j = 0; j < buf.length; ++j) { + if (buf[j] != 0) { + foundNonZero = true; + break; + } + buf[j] = j; + } + ok(!foundNonZero, "Buffer " + i + " should be initialized to 0"); + } + + // Now test copying the channel data out of a normal buffer + var copy = new Float32Array(100); + buffer.copyFromChannel(copy, 0, 1024); + for (var i = 0; i < copy.length; ++i) { + is(copy[i], 1024 + i, "Correct sample"); + } + // Test copying the channel data out of a playing buffer + var srcNode = context.createBufferSource(); + srcNode.buffer = buffer; + srcNode.start(0); + copy = new Float32Array(100); + buffer.copyFromChannel(copy, 0, 1024); + for (var i = 0; i < copy.length; ++i) { + is(copy[i], 1024 + i, "Correct sample"); + } + + // Test copying to the channel data + var newData = new Float32Array(200); + buffer.copyToChannel(newData, 0, 100); + var changedData = buffer.getChannelData(0); + for (var i = 0; i < changedData.length; ++i) { + if (i < 100 || i >= 300) { + is(changedData[i], i, "Untouched sample"); + } else { + is(changedData[i], 0, "Correct sample"); + } + } + + // Now, detach the array buffer + var worker = new Worker("audioBufferSourceNodeDetached_worker.js"); + var data = buffer.getChannelData(0).buffer; + worker.postMessage(data, [data]); + SpecialPowers.gc(); + + expectNoException(function() { + buffer.copyFromChannel(copy, 0, 1024); + }); + + expectNoException(function() { + buffer.copyToChannel(newData, 0, 100); + }); + + expectException(function() { + context.createBuffer(2, 2048, 7999); + }, DOMException.NOT_SUPPORTED_ERR); + expectException(function() { + context.createBuffer(2, 2048, 192001); + }, DOMException.NOT_SUPPORTED_ERR); + context.createBuffer(2, 2048, 8000); // no exception + context.createBuffer(2, 2048, 192000); // no exception + context.createBuffer(32, 2048, 48000); // no exception + // Null length + expectException(function() { + context.createBuffer(2, 0, 48000); + }, DOMException.NOT_SUPPORTED_ERR); + // Null number of channels + expectException(function() { + context.createBuffer(0, 2048, 48000); + }, DOMException.NOT_SUPPORTED_ERR); + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_AudioContext.html b/dom/media/webaudio/test/test_AudioContext.html new file mode 100644 index 0000000000..50aeee489e --- /dev/null +++ b/dom/media/webaudio/test/test_AudioContext.html @@ -0,0 +1,23 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test whether we can create an AudioContext interface</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + var ac = new AudioContext(); + ok(ac, "Create a AudioContext object"); + ok(ac instanceof EventTarget, "AudioContexts must be EventTargets"); + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_AudioContext_disabled.html b/dom/media/webaudio/test/test_AudioContext_disabled.html new file mode 100644 index 0000000000..9c3651a883 --- /dev/null +++ b/dom/media/webaudio/test/test_AudioContext_disabled.html @@ -0,0 +1,56 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test whether we can disable the AudioContext interface</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +const webaudio_interfaces = [ + "AudioContext", + "OfflineAudioContext", + "AudioContext", + "OfflineAudioCompletionEvent", + "AudioNode", + "AudioDestinationNode", + "AudioParam", + "GainNode", + "DelayNode", + "AudioBuffer", + "AudioBufferSourceNode", + "MediaElementAudioSourceNode", + "ScriptProcessorNode", + "AudioProcessingEvent", + "PannerNode", + "AudioListener", + "StereoPannerNode", + "ConvolverNode", + "AnalyserNode", + "ChannelSplitterNode", + "ChannelMergerNode", + "DynamicsCompressorNode", + "BiquadFilterNode", + "IIRFilterNode", + "WaveShaperNode", + "OscillatorNode", + "PeriodicWave", + "MediaStreamAudioSourceNode", + "MediaStreamAudioDestinationNode" +]; + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + SpecialPowers.pushPrefEnv({"set": [["dom.webaudio.enabled", false]]}, function() { + webaudio_interfaces.forEach((e) => ok(!window[e], e + " must be disabled when the Web Audio API is disabled")); + SimpleTest.finish(); + }); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_AudioListener.html b/dom/media/webaudio/test/test_AudioListener.html new file mode 100644 index 0000000000..e3d605cbcc --- /dev/null +++ b/dom/media/webaudio/test/test_AudioListener.html @@ -0,0 +1,26 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test AudioContext.listener and the AudioListener interface</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + var context = new AudioContext(); + ok("listener" in context, "AudioContext.listener should exist"); + // The values set by the following cannot be read from script, but the + // implementation is simple enough, so we just make sure that nothing throws. + context.listener.setPosition(1.0, 1.0, 1.0); + context.listener.setOrientation(1.0, 1.0, 1.0, 1.0, 1.0, 1.0); + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_AudioNodeDevtoolsAPI.html b/dom/media/webaudio/test/test_AudioNodeDevtoolsAPI.html new file mode 100644 index 0000000000..f032ed88f0 --- /dev/null +++ b/dom/media/webaudio/test/test_AudioNodeDevtoolsAPI.html @@ -0,0 +1,59 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test the devtool AudioNode API</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + + SimpleTest.waitForExplicitFinish(); + + function id(node) { + return SpecialPowers.getPrivilegedProps(node, "id"); + } + + var ac = new AudioContext(); + var ids; + var weak; + (function() { + var src1 = ac.createBufferSource(); + var src2 = ac.createBufferSource(); + ok(id(src2) > id(src1), "The ID should be monotonic"); + ok(id(src1) > id(ac.destination), "The ID of the destination node should be the lowest"); + ids = [id(src1), id(src2)]; + weak = SpecialPowers.Cu.getWeakReference(src1); + is(SpecialPowers.unwrap(weak.get()), src1, "The node should support a weak reference"); + })(); + function observer(subject, topic, data) { + var id = parseInt(data); + var index = ids.indexOf(id); + if (index != -1) { + info("Dropping id " + id + " at index " + index); + ids.splice(index, 1); + if (!ids.length) { + SimpleTest.executeSoon(function() { + is(weak.get(), null, "The weak reference must be dropped now"); + SpecialPowers.removeObserver(observer, "webaudio-node-demise"); + SimpleTest.finish(); + }); + } + } + } + SpecialPowers.addObserver(observer, "webaudio-node-demise"); + + forceCC(); + forceCC(); + + function forceCC() { + SpecialPowers.DOMWindowUtils.cycleCollect(); + SpecialPowers.DOMWindowUtils.garbageCollect(); + SpecialPowers.DOMWindowUtils.garbageCollect(); + } + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_AudioParamDevtoolsAPI.html b/dom/media/webaudio/test/test_AudioParamDevtoolsAPI.html new file mode 100644 index 0000000000..81fdd357c9 --- /dev/null +++ b/dom/media/webaudio/test/test_AudioParamDevtoolsAPI.html @@ -0,0 +1,49 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test the devtool AudioParam API</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + + function checkIdAndName(node, name) { + is(SpecialPowers.getPrivilegedProps(node, "id"), + SpecialPowers.getPrivilegedProps(node[name], "parentNodeId"), + "The parent id should be correct"); + is(SpecialPowers.getPrivilegedProps(node[name], "name"), name, + "The name of the AudioParam should be correct."); + } + + var ac = new AudioContext(), + gain = ac.createGain(), + osc = ac.createOscillator(), + del = ac.createDelay(), + source = ac.createBufferSource(), + stereoPanner = ac.createStereoPanner(), + comp = ac.createDynamicsCompressor(), + biquad = ac.createBiquadFilter(); + + checkIdAndName(gain, "gain"); + checkIdAndName(osc, "frequency"); + checkIdAndName(osc, "detune"); + checkIdAndName(del, "delayTime"); + checkIdAndName(source, "playbackRate"); + checkIdAndName(source, "detune"); + checkIdAndName(stereoPanner, "pan"); + checkIdAndName(comp, "threshold"); + checkIdAndName(comp, "knee"); + checkIdAndName(comp, "ratio"); + checkIdAndName(comp, "attack"); + checkIdAndName(comp, "release"); + checkIdAndName(biquad, "frequency"); + checkIdAndName(biquad, "detune"); + checkIdAndName(biquad, "Q"); + checkIdAndName(biquad, "gain"); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_OfflineAudioContext.html b/dom/media/webaudio/test/test_OfflineAudioContext.html new file mode 100644 index 0000000000..d9403566ae --- /dev/null +++ b/dom/media/webaudio/test/test_OfflineAudioContext.html @@ -0,0 +1,118 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test OfflineAudioContext</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +var renderedBuffer = null; +var finished = 0; + +function finish() { + finished++; + if (finished == 2) { + SimpleTest.finish(); + } +} + +function setOrCompareRenderedBuffer(aRenderedBuffer) { + if (renderedBuffer) { + is(renderedBuffer, aRenderedBuffer, "Rendered buffers from the event and the promise should be the same"); + finish(); + } else { + renderedBuffer = aRenderedBuffer; + } +} + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + let ctxs = [ + new OfflineAudioContext(2, 100, 22050), + new OfflineAudioContext({length: 100, sampleRate: 22050}), + new OfflineAudioContext({channels: 2, length: 100, sampleRate: 22050}), + ]; + + for (let ctx of ctxs) { + ok(ctx instanceof EventTarget, "OfflineAudioContexts must be EventTargets"); + is(ctx.length, 100, "OfflineAudioContext.length is equal to the value passed to the ctor."); + + var buf = ctx.createBuffer(2, 100, ctx.sampleRate); + for (var i = 0; i < 2; ++i) { + for (var j = 0; j < 100; ++j) { + buf.getChannelData(i)[j] = Math.sin(2 * Math.PI * 200 * j / ctx.sampleRate); + } + } + } + + is(ctxs[1].destination.channelCount, 1, "OfflineAudioContext defaults to to correct channelCount."); + + let ctx = ctxs[0]; + + expectException(function() { + new OfflineAudioContext(2, 100, 0); + }, DOMException.NOT_SUPPORTED_ERR); + expectException(function() { + new OfflineAudioContext(2, 100, -1); + }, DOMException.NOT_SUPPORTED_ERR); + expectException(function() { + new OfflineAudioContext(0, 100, 44100); + }, DOMException.NOT_SUPPORTED_ERR); + new OfflineAudioContext(32, 100, 44100); + expectException(function() { + new OfflineAudioContext(33, 100, 44100); + }, DOMException.NOT_SUPPORTED_ERR); + expectException(function() { + new OfflineAudioContext(2, 0, 44100); + }, DOMException.NOT_SUPPORTED_ERR); + expectTypeError(function() { + new OfflineAudioContext({}); + }); + expectTypeError(function() { + new OfflineAudioContext({sampleRate: 44100}); + }); + expectTypeError(function() { + new OfflineAudioContext({length: 44100*40}); + }); + + var src = ctx.createBufferSource(); + src.buffer = buf; + src.start(0); + src.connect(ctx.destination); + + ctx.addEventListener("complete", function(e) { + ok(e instanceof OfflineAudioCompletionEvent, "Correct event received"); + is(e.renderedBuffer.numberOfChannels, 2, "Correct expected number of buffers"); + ok(renderedBuffer != null, "The event should be fired after the promise callback."); + expectNoException(function() { + ctx.startRendering().then(function() { + ok(false, "Promise should not resolve when startRendering is called a second time on an OfflineAudioContext") + finish(); + }).catch(function(err) { + ok(true, "Promise should reject when startRendering is called a second time on an OfflineAudioContext") + finish(); + }); + }); + compareBuffers(e.renderedBuffer, buf); + setOrCompareRenderedBuffer(e.renderedBuffer); + + }); + + expectNoException(function() { + ctx.startRendering().then(function(b) { + is(renderedBuffer, null, "The promise callback should be called first."); + setOrCompareRenderedBuffer(b); + }).catch(function (error) { + ok(false, "The promise from OfflineAudioContext.startRendering should never be rejected"); + }); + }); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_ScriptProcessorCollected1.html b/dom/media/webaudio/test/test_ScriptProcessorCollected1.html new file mode 100644 index 0000000000..8f05889d26 --- /dev/null +++ b/dom/media/webaudio/test/test_ScriptProcessorCollected1.html @@ -0,0 +1,77 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test ScriptProcessorNode in cycle with no listener is collected</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); + +var observer = function(subject, topic, data) { + var id = parseInt(data); + var index = ids.indexOf(id); + if (index != -1) { + ok(true, "Collected AudioNode id " + id + " at index " + index); + ids.splice(index, 1); + } +} + +SpecialPowers.addObserver(observer, "webaudio-node-demise"); + +SimpleTest.registerCleanupFunction(function() { + if (observer) { + SpecialPowers.removeObserver(observer, "webaudio-node-demise"); + } +}); + +var ac = new AudioContext(); + +var testProcessor = ac.createScriptProcessor(256, 1, 0); +var delay = ac.createDelay(); +testProcessor.connect(delay); +delay.connect(testProcessor); + +var referenceProcessor = ac.createScriptProcessor(256, 1, 0); +var gain = ac.createGain(); +gain.connect(referenceProcessor); + +var processCount = 0; +testProcessor.onaudioprocess = function(event) { + ++processCount; + switch (processCount) { + case 1: + // Switch to listening to referenceProcessor; + referenceProcessor.onaudioprocess = event.target.onaudioprocess; + referenceProcessor = null; + event.target.onaudioprocess = null; + break; + case 2: + // There are no references to testProcessor and so GC can begin. + SpecialPowers.exactGC(function() { + SpecialPowers.removeObserver(observer, "webaudio-node-demise"); + observer = null; + event.target.onaudioprocess = null; + ok(!ids.length, "All expected nodes should be collected"); + SimpleTest.finish(); + }); + break; + } +}; + +function id(node) { + return SpecialPowers.getPrivilegedProps(node, "id"); +} + +// Nodes with these ids should be collected. +var ids = [ id(testProcessor), id(delay), id(gain) ]; +testProcessor = null; +delay = null; +gain = null; + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_WebAudioMemoryReporting.html b/dom/media/webaudio/test/test_WebAudioMemoryReporting.html new file mode 100644 index 0000000000..693e558304 --- /dev/null +++ b/dom/media/webaudio/test/test_WebAudioMemoryReporting.html @@ -0,0 +1,54 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Web Audio memory reporting</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); + +var ac = new AudioContext(); +var sp = ac.createScriptProcessor(4096, 1, 1); +sp.connect(ac.destination); + +// Not started so as to test +// https://bugzilla.mozilla.org/show_bug.cgi?id=1225003#c2 +var oac = new OfflineAudioContext(1, 1, 48000); + +var nodeTypes = ["ScriptProcessorNode", "AudioDestinationNode"]; +var objectTypes = ["dom-nodes", "engine-objects", "track-objects"]; + +var usages = { "explicit/webaudio/audiocontext": 0 }; + +for (var i = 0; i < nodeTypes.length; ++i) { + for (var j = 0; j < objectTypes.length; ++j) { + usages["explicit/webaudio/audio-node/" + + nodeTypes[i] + "/" + objectTypes[j]] = 0; + } +} + +var handleReport = function(aProcess, aPath, aKind, aUnits, aAmount, aDesc) { + if (aPath in usages) { + usages[aPath] += aAmount; + } +} + +var finished = function () { + ok(true, "Yay didn't crash!"); + for (var resource in usages) { + ok(usages[resource] > 0, "Non-zero usage for " + resource); + }; + SimpleTest.finish(); +} + +SpecialPowers.Cc["@mozilla.org/memory-reporter-manager;1"]. + getService(SpecialPowers.Ci.nsIMemoryReporterManager). + getReports(handleReport, null, finished, null, /* anonymized = */ false); + +// To test bug 1225003, run a failing decodeAudioData() job over a time when +// the tasks from getReports() are expected to run. +ac.decodeAudioData(new ArrayBuffer(4), function(){}, function(){}); +</script> +</html> diff --git a/dom/media/webaudio/test/test_analyserNode.html b/dom/media/webaudio/test/test_analyserNode.html new file mode 100644 index 0000000000..0793eeb2cb --- /dev/null +++ b/dom/media/webaudio/test/test_analyserNode.html @@ -0,0 +1,178 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test AnalyserNode</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +function testNode() { + var context = new AudioContext(); + var buffer = context.createBuffer(1, 2048, context.sampleRate); + for (var i = 0; i < 2048; ++i) { + buffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate); + } + + var destination = context.destination; + + var source = context.createBufferSource(); + + var analyser = context.createAnalyser(); + + source.buffer = buffer; + + source.connect(analyser); + analyser.connect(destination); + + is(analyser.channelCount, 2, "analyser node has 2 input channels by default"); + is(analyser.channelCountMode, "max", "Correct channelCountMode for the analyser node"); + is(analyser.channelInterpretation, "speakers", "Correct channelCountInterpretation for the analyser node"); + + is(analyser.fftSize, 2048, "Correct default value for fftSize"); + is(analyser.frequencyBinCount, 1024, "Correct default value for frequencyBinCount"); + expectException(function() { + analyser.fftSize = 0; + }, DOMException.INDEX_SIZE_ERR); + expectException(function() { + analyser.fftSize = 1; + }, DOMException.INDEX_SIZE_ERR); + expectException(function() { + analyser.fftSize = 8; + }, DOMException.INDEX_SIZE_ERR); + expectException(function() { + analyser.fftSize = 100; // non-power of two + }, DOMException.INDEX_SIZE_ERR); + expectException(function() { + analyser.fftSize = 2049; + }, DOMException.INDEX_SIZE_ERR); + expectException(function() { + analyser.fftSize = 4097; + }, DOMException.INDEX_SIZE_ERR); + expectException(function() { + analyser.fftSize = 8193; + }, DOMException.INDEX_SIZE_ERR); + expectException(function() { + analyser.fftSize = 16385; + }, DOMException.INDEX_SIZE_ERR); + expectException(function() { + analyser.fftSize = 32769; + }, DOMException.INDEX_SIZE_ERR); + expectException(function() { + analyser.fftSize = 65536; + }, DOMException.INDEX_SIZE_ERR); + analyser.fftSize = 1024; + is(analyser.frequencyBinCount, 512, "Correct new value for frequencyBinCount"); + + is(analyser.minDecibels, -100, "Correct default value for minDecibels"); + is(analyser.maxDecibels, -30, "Correct default value for maxDecibels"); + expectException(function() { + analyser.minDecibels = -30; + }, DOMException.INDEX_SIZE_ERR); + expectException(function() { + analyser.minDecibels = -29; + }, DOMException.INDEX_SIZE_ERR); + expectException(function() { + analyser.maxDecibels = -100; + }, DOMException.INDEX_SIZE_ERR); + expectException(function() { + analyser.maxDecibels = -101; + }, DOMException.INDEX_SIZE_ERR); + + ok(Math.abs(analyser.smoothingTimeConstant - 0.8) < 0.001, "Correct default value for smoothingTimeConstant"); + expectException(function() { + analyser.smoothingTimeConstant = -0.1; + }, DOMException.INDEX_SIZE_ERR); + expectException(function() { + analyser.smoothingTimeConstant = 1.1; + }, DOMException.INDEX_SIZE_ERR); + analyser.smoothingTimeConstant = 0; + analyser.smoothingTimeConstant = 1; +} + +function testConstructor() { + var context = new AudioContext(); + + var analyser = new AnalyserNode(context); + is(analyser.channelCount, 2, "analyser node has 2 input channels by default"); + is(analyser.channelCountMode, "max", "Correct channelCountMode for the analyser node"); + is(analyser.channelInterpretation, "speakers", "Correct channelCountInterpretation for the analyser node"); + + is(analyser.fftSize, 2048, "Correct default value for fftSize"); + is(analyser.frequencyBinCount, 1024, "Correct default value for frequencyBinCount"); + is(analyser.minDecibels, -100, "Correct default value for minDecibels"); + is(analyser.maxDecibels, -30, "Correct default value for maxDecibels"); + ok(Math.abs(analyser.smoothingTimeConstant - 0.8) < 0.001, "Correct default value for smoothingTimeConstant"); + + expectException(function() { + analyser = new AnalyserNode(context, { fftSize: 0 }); + }, DOMException.INDEX_SIZE_ERR); + expectException(function() { + analyser = new AnalyserNode(context, { fftSize: 1 }); + }, DOMException.INDEX_SIZE_ERR); + expectException(function() { + analyser = new AnalyserNode(context, { fftSize: 8 }); + }, DOMException.INDEX_SIZE_ERR); + expectException(function() { + analyser = new AnalyserNode(context, { fftSize: 100 }); // non-power of two + }, DOMException.INDEX_SIZE_ERR); + expectException(function() { + analyser = new AnalyserNode(context, { fftSize: 2049 }); + }, DOMException.INDEX_SIZE_ERR); + expectException(function() { + analyser = new AnalyserNode(context, { fftSize: 4097 }); + }, DOMException.INDEX_SIZE_ERR); + expectException(function() { + analyser = new AnalyserNode(context, { fftSize: 8193 }); + }, DOMException.INDEX_SIZE_ERR); + expectException(function() { + analyser = new AnalyserNode(context, { fftSize: 16385 }); + }, DOMException.INDEX_SIZE_ERR); + expectException(function() { + analyser = new AnalyserNode(context, { fftSize: 32769 }); + }, DOMException.INDEX_SIZE_ERR); + expectException(function() { + analyser = new AnalyserNode(context, { fftSize: 65536 }); + }, DOMException.INDEX_SIZE_ERR); + analyser = new AnalyserNode(context, { fftSize: 1024 }); + is(analyser.frequencyBinCount, 512, "Correct new value for frequencyBinCount"); + + expectException(function() { + analyser = new AnalyserNode(context, { minDecibels: -30 }); + }, DOMException.INDEX_SIZE_ERR); + expectException(function() { + analyser = new AnalyserNode(context, { minDecibels: -29 }); + }, DOMException.INDEX_SIZE_ERR); + expectException(function() { + analyser = new AnalyserNode(context, { maxDecibels: -100 }); + }, DOMException.INDEX_SIZE_ERR); + expectException(function() { + analyser = new AnalyserNode(context, { maxDecibels: -101 }); + }, DOMException.INDEX_SIZE_ERR); + + expectException(function() { + analyser = new AnalyserNode(context, { smoothingTimeConstant: -0.1 }); + }, DOMException.INDEX_SIZE_ERR); + expectException(function() { + analyser = new AnalyserNode(context, { smoothingTimeConstant: -1.1 }); + }, DOMException.INDEX_SIZE_ERR); + analyser = new AnalyserNode(context, { smoothingTimeConstant: 0 }); + analyser = new AnalyserNode(context, { smoothingTimeConstant: 1 }); +} + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + + testNode(); + testConstructor(); + + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_analyserNodeMinimum.html b/dom/media/webaudio/test/test_analyserNodeMinimum.html new file mode 100644 index 0000000000..950fd1812b --- /dev/null +++ b/dom/media/webaudio/test/test_analyserNodeMinimum.html @@ -0,0 +1,51 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test AnalyserNode when the input is silent</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + + var ac = new AudioContext(); + var analyser = ac.createAnalyser(); + var constant = ac.createConstantSource(); + var sp = ac.createScriptProcessor(2048, 1, 0); + + constant.offset.value = 0.0; + + constant.connect(analyser) + .connect(ac.destination); + + constant.connect(sp); + + var buf = new Float32Array(analyser.frequencyBinCount); + var iteration_count = 10; + sp.onaudioprocess = function() { + analyser.getFloatFrequencyData(buf); + var correct = true; + for (var i = 0; i < buf.length; i++) { + correct &= buf[i] == -Infinity; + } + ok(correct, "silent input process -Infinity in decibel bins"); + if(!(iteration_count--)) { + sp.onaudioprocess = null; + constant.stop(); + ac.close(); + SimpleTest.finish(); + } + } + + constant.start(); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_analyserNodeOutput.html b/dom/media/webaudio/test/test_analyserNodeOutput.html new file mode 100644 index 0000000000..27b354e92d --- /dev/null +++ b/dom/media/webaudio/test/test_analyserNodeOutput.html @@ -0,0 +1,43 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test AnalyserNode</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +var gTest = { + length: 2048, + numberOfChannels: 1, + createGraph(context) { + var source = context.createBufferSource(); + + var analyser = context.createAnalyser(); + + source.buffer = this.buffer; + + source.connect(analyser); + + source.start(0); + return analyser; + }, + createExpectedBuffers(context) { + this.buffer = context.createBuffer(1, 2048, context.sampleRate); + for (var i = 0; i < 2048; ++i) { + this.buffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate); + } + + return [this.buffer]; + }, +}; + +runTest(); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_analyserNodePassThrough.html b/dom/media/webaudio/test/test_analyserNodePassThrough.html new file mode 100644 index 0000000000..50ff94a8c7 --- /dev/null +++ b/dom/media/webaudio/test/test_analyserNodePassThrough.html @@ -0,0 +1,47 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test AnalyserNode with passthrough</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +var gTest = { + length: 2048, + numberOfChannels: 1, + createGraph(context) { + var source = context.createBufferSource(); + + var analyser = context.createAnalyser(); + + source.buffer = this.buffer; + + source.connect(analyser); + + var analyserWrapped = SpecialPowers.wrap(analyser); + ok("passThrough" in analyserWrapped, "AnalyserNode should support the passThrough API"); + analyserWrapped.passThrough = true; + + source.start(0); + return analyser; + }, + createExpectedBuffers(context) { + this.buffer = context.createBuffer(1, 2048, context.sampleRate); + for (var i = 0; i < 2048; ++i) { + this.buffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate); + } + + return [this.buffer]; + }, +}; + +runTest(); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_analyserNodeWithGain.html b/dom/media/webaudio/test/test_analyserNodeWithGain.html new file mode 100644 index 0000000000..fa0a2caa75 --- /dev/null +++ b/dom/media/webaudio/test/test_analyserNodeWithGain.html @@ -0,0 +1,47 @@ +<!DOCTYPE html> +<title>Test effect of AnalyserNode on GainNode output</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script> +promise_test(function() { + // fftSize <= bufferSize so that the time domain data is full of input after + // processing the buffer. + const fftSize = 32; + const bufferSize = 128; + + var context = new OfflineAudioContext(1, bufferSize, 48000); + + var analyser1 = context.createAnalyser(); + analyser1.fftSize = fftSize; + analyser1.connect(context.destination); + var analyser2 = context.createAnalyser(); + analyser2.fftSize = fftSize; + + var gain = context.createGain(); + gain.gain.value = 2.0; + gain.connect(analyser1); + gain.connect(analyser2); + + // Create a DC input to make getFloatTimeDomainData() output consistent at + // any time. + var buffer = context.createBuffer(1, 1, context.sampleRate); + buffer.getChannelData(0)[0] = 1.0 / gain.gain.value; + var source = context.createBufferSource(); + source.buffer = buffer; + source.loop = true; + source.connect(gain); + source.start(); + + return context.startRendering(). + then(function(buffer) { + assert_equals(buffer.getChannelData(0)[0], 1.0, + "analyser1 output"); + + var data = new Float32Array(1); + analyser1.getFloatTimeDomainData(data); + assert_equals(data[0], 1.0, "analyser1 time domain data"); + analyser2.getFloatTimeDomainData(data); + assert_equals(data[0], 1.0, "analyser2 time domain data"); + }); +}); +</script> diff --git a/dom/media/webaudio/test/test_analyserScale.html b/dom/media/webaudio/test/test_analyserScale.html new file mode 100644 index 0000000000..dc4260c47a --- /dev/null +++ b/dom/media/webaudio/test/test_analyserScale.html @@ -0,0 +1,58 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test AnalyserNode when the input is scaled </title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + + var context = new AudioContext(); + + var gain = context.createGain(); + var analyser = context.createAnalyser(); + var osc = context.createOscillator(); + + + osc.connect(gain); + gain.connect(analyser); + + osc.start(); + + var array = new Uint8Array(analyser.frequencyBinCount); + + function getAnalyserData() { + gain.gain.setValueAtTime(currentGain, context.currentTime); + analyser.getByteTimeDomainData(array); + var max = -1; + for (var i = 0; i < array.length; i++) { + if (array[i] > max) { + max = Math.abs(array[i] - 128); + } + } + if (max <= currentGain * 128) { + ok(true, "Analyser got scaled data for " + currentGain); + currentGain = tests.shift(); + if (currentGain == undefined) { + SimpleTest.finish(); + return; + } + } + requestAnimationFrame(getAnalyserData); + } + + var tests = [1.0, 0.5, 0.0]; + var currentGain = tests.shift(); + requestAnimationFrame(getAnalyserData); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_audioBufferSourceNode.html b/dom/media/webaudio/test/test_audioBufferSourceNode.html new file mode 100644 index 0000000000..fc7c0b48d1 --- /dev/null +++ b/dom/media/webaudio/test/test_audioBufferSourceNode.html @@ -0,0 +1,44 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test AudioBufferSourceNode</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +var gTest = { + length: 4096, + createGraph(context) { + var buffer = context.createBuffer(1, 2048, context.sampleRate); + for (var i = 0; i < 2048; ++i) { + buffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate); + } + + var source = context.createBufferSource(); + source.start(0); + source.buffer = buffer; + return source; + }, + createExpectedBuffers(context) { + var buffers = []; + var buffer = context.createBuffer(2, 2048, context.sampleRate); + for (var i = 0; i < 2048; ++i) { + buffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate); + buffer.getChannelData(1)[i] = buffer.getChannelData(0)[i]; + } + buffers.push(buffer); + buffers.push(getEmptyBuffer(context, 2048)); + return buffers; + }, +}; + +runTest(); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_audioBufferSourceNodeDetached.html b/dom/media/webaudio/test/test_audioBufferSourceNodeDetached.html new file mode 100644 index 0000000000..e84c33e585 --- /dev/null +++ b/dom/media/webaudio/test/test_audioBufferSourceNodeDetached.html @@ -0,0 +1,58 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test AudioBufferSourceNode when an AudioBuffer's getChanneData buffer is detached</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +function createGarbage() { + var s = []; + for (var i = 0; i < 10000000; ++i) { + s.push(i); + } + var sum = 0; + for (var i = 0; i < s.length; ++i) { + sum += s[i]; + } + return sum; +} + +var worker = new Worker("audioBufferSourceNodeDetached_worker.js"); + +var gTest = { + length: 2048, + numberOfChannels: 1, + createGraph(context) { + var buffer = context.createBuffer(1, 10000000, context.sampleRate); + var data = buffer.getChannelData(0); + for (var i = 0; i < data.length; ++i) { + data[i] = (i%100)/100 - 0.5; + } + + // Detach the buffer now + var data = buffer.getChannelData(0).buffer; + worker.postMessage(data, [data]); + // Create garbage and GC to replace the buffer data with garbage + SpecialPowers.gc(); + createGarbage(); + SpecialPowers.gc(); + + var source = context.createBufferSource(); + source.buffer = buffer; + source.start(); + // This should play silence + return source; + }, +}; + +runTest(); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_audioBufferSourceNodeEnded.html b/dom/media/webaudio/test/test_audioBufferSourceNodeEnded.html new file mode 100644 index 0000000000..a11bb880a2 --- /dev/null +++ b/dom/media/webaudio/test/test_audioBufferSourceNodeEnded.html @@ -0,0 +1,36 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test ended event on AudioBufferSourceNode</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + var context = new AudioContext(); + var buffer = context.createBuffer(1, 2048, context.sampleRate); + for (var i = 0; i < 2048; ++i) { + buffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate); + } + + var source = context.createBufferSource(); + + source.onended = function(e) { + is(e.target, source, "Correct target for the ended event"); + SimpleTest.finish(); + }; + + source.start(0); + source.buffer = buffer; + source.connect(context.destination); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_audioBufferSourceNodeLazyLoopParam.html b/dom/media/webaudio/test/test_audioBufferSourceNodeLazyLoopParam.html new file mode 100644 index 0000000000..757d4487c4 --- /dev/null +++ b/dom/media/webaudio/test/test_audioBufferSourceNodeLazyLoopParam.html @@ -0,0 +1,47 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test AudioBufferSourceNode</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +var gTest = { + length: 4096, + numberOfChannels: 1, + createGraph(context) { + // silence for half of the buffer, ones after that. + var buffer = context.createBuffer(1, 2048, context.sampleRate); + for (var i = 1024; i < 2048; i++) { + buffer.getChannelData(0)[i] = 1; + } + + var source = context.createBufferSource(); + + // we start at the 1024 frames, we should only have ones. + source.loop = true; + source.loopStart = 1024 / context.sampleRate; + source.loopEnd = 2048 / context.sampleRate; + source.buffer = buffer; + source.start(0, 1024 / context.sampleRate, 2048 / context.sampleRate); + return source; + }, + createExpectedBuffers(context) { + var expectedBuffer = context.createBuffer(1, 4096, context.sampleRate); + for (var i = 0; i < 2048; i++) { + expectedBuffer.getChannelData(0)[i] = 1; + } + return expectedBuffer; + }, +}; + +runTest(); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_audioBufferSourceNodeLoop.html b/dom/media/webaudio/test/test_audioBufferSourceNodeLoop.html new file mode 100644 index 0000000000..10d5d99108 --- /dev/null +++ b/dom/media/webaudio/test/test_audioBufferSourceNodeLoop.html @@ -0,0 +1,45 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test AudioBufferSourceNode looping</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +var gTest = { + length: 2048 * 4, + numberOfChannels: 1, + createGraph(context) { + var buffer = context.createBuffer(1, 2048, context.sampleRate); + for (var i = 0; i < 2048; ++i) { + buffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate); + } + + var source = context.createBufferSource(); + source.buffer = buffer; + + source.start(0); + source.loop = true; + return source; + }, + createExpectedBuffers(context) { + var expectedBuffer = context.createBuffer(1, 2048 * 4, context.sampleRate); + for (var i = 0; i < 4; ++i) { + for (var j = 0; j < 2048; ++j) { + expectedBuffer.getChannelData(0)[i * 2048 + j] = Math.sin(440 * 2 * Math.PI * j / context.sampleRate); + } + } + return expectedBuffer; + }, +}; + +runTest(); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_audioBufferSourceNodeLoopStartEnd.html b/dom/media/webaudio/test/test_audioBufferSourceNodeLoopStartEnd.html new file mode 100644 index 0000000000..1ef08e0b83 --- /dev/null +++ b/dom/media/webaudio/test/test_audioBufferSourceNodeLoopStartEnd.html @@ -0,0 +1,48 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test AudioBufferSourceNode looping</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +var gTest = { + length: 2048 * 4, + numberOfChannels: 1, + createGraph(context) { + var buffer = context.createBuffer(1, 2048, context.sampleRate); + for (var i = 0; i < 2048; ++i) { + buffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate); + } + + var source = new AudioBufferSourceNode(context, {buffer, loop: true, loopStart: buffer.duration * 0.25, loopEnd: buffer.duration * 0.75 }); + source.start(0); + return source; + }, + createExpectedBuffers(context) { + var expectedBuffer = context.createBuffer(1, 2048 * 4, context.sampleRate); + for (var i = 0; i < 1536; ++i) { + expectedBuffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate); + } + for (var i = 0; i < 6; ++i) { + for (var j = 512; j < 1536; ++j) { + expectedBuffer.getChannelData(0)[1536 + i * 1024 + j - 512] = Math.sin(440 * 2 * Math.PI * j / context.sampleRate); + } + } + for (var j = 7680; j < 2048 * 4; ++j) { + expectedBuffer.getChannelData(0)[j] = Math.sin(440 * 2 * Math.PI * (j - 7168) / context.sampleRate); + } + return expectedBuffer; + }, +}; + +runTest(); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_audioBufferSourceNodeLoopStartEndSame.html b/dom/media/webaudio/test/test_audioBufferSourceNodeLoopStartEndSame.html new file mode 100644 index 0000000000..cfe054f838 --- /dev/null +++ b/dom/media/webaudio/test/test_audioBufferSourceNodeLoopStartEndSame.html @@ -0,0 +1,44 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test AudioBufferSourceNode looping</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +var gTest = { + length: 2048, + numberOfChannels: 1, + createGraph(context) { + var buffer = context.createBuffer(1, 2048, context.sampleRate); + for (var i = 0; i < 2048; ++i) { + buffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate); + } + + var source = context.createBufferSource(); + source.buffer = buffer; + + source.loop = true; + source.loopStart = source.loopEnd = 1 / context.sampleRate; + source.start(0); + return source; + }, + createExpectedBuffers(context) { + var buffer = context.createBuffer(1, 2048, context.sampleRate); + for (var i = 0; i < 2048; ++i) { + buffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate); + } + return buffer; + }, +}; + +runTest(); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_audioBufferSourceNodeNoStart.html b/dom/media/webaudio/test/test_audioBufferSourceNodeNoStart.html new file mode 100644 index 0000000000..e9a0472e2a --- /dev/null +++ b/dom/media/webaudio/test/test_audioBufferSourceNodeNoStart.html @@ -0,0 +1,33 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test AudioBufferSourceNode when start() is not called</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +var gTest = { + length: 2048, + numberOfChannels: 1, + createGraph(context) { + var buffer = context.createBuffer(1, 2048, context.sampleRate); + var data = buffer.getChannelData(0); + for (var i = 0; i < data.length; ++i) { + data[i] = (i%100)/100 - 0.5; + } + var source = context.createBufferSource(); + source.buffer = buffer; + return source; + }, +}; + +runTest(); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_audioBufferSourceNodeNullBuffer.html b/dom/media/webaudio/test/test_audioBufferSourceNodeNullBuffer.html new file mode 100644 index 0000000000..b0b405b366 --- /dev/null +++ b/dom/media/webaudio/test/test_audioBufferSourceNodeNullBuffer.html @@ -0,0 +1,31 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test AudioBufferSourceNode</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +var gTest = { + length: 2048, + numberOfChannels: 1, + createGraph(context) { + var source = context.createBufferSource(); + + source.start(0); + source.buffer = null; + is(source.buffer, null, "Try playing back a null buffer"); + return source; + }, +}; + +runTest(); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_audioBufferSourceNodeOffset.html b/dom/media/webaudio/test/test_audioBufferSourceNodeOffset.html new file mode 100644 index 0000000000..0411b74ce5 --- /dev/null +++ b/dom/media/webaudio/test/test_audioBufferSourceNodeOffset.html @@ -0,0 +1,55 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test the offset property on AudioBufferSourceNode</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +var fuzz = 0.3; + +if (navigator.platform.startsWith("Mac")) { + // bug 895720 + fuzz = 0.6; +} + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + var samplesFromSource = 0; + var context = new AudioContext(); + var sp = context.createScriptProcessor(256); + + sp.onaudioprocess = function(e) { + samplesFromSource += e.inputBuffer.length; + } + + var buffer = context.createBuffer(1, context.sampleRate, context.sampleRate); + for (var i = 0; i < context.sampleRate; ++i) { + buffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate); + } + + var source = context.createBufferSource(); + + source.onended = function(e) { + // The timing at which the audioprocess and ended listeners are called can + // change, hence the fuzzy equal here. + var errorRatio = samplesFromSource / (0.5 * context.sampleRate); + ok(errorRatio > (1.0 - fuzz) && errorRatio < (1.0 + fuzz), + "Correct number of samples received (expected: " + + (0.5 * context.sampleRate) + ", actual: " + samplesFromSource + ")."); + SimpleTest.finish(); + }; + + source.buffer = buffer; + source.connect(sp); + source.start(0, 0.5); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_audioBufferSourceNodePassThrough.html b/dom/media/webaudio/test/test_audioBufferSourceNodePassThrough.html new file mode 100644 index 0000000000..6cb0cccf99 --- /dev/null +++ b/dom/media/webaudio/test/test_audioBufferSourceNodePassThrough.html @@ -0,0 +1,45 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test AudioBufferSourceNode with passthrough</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +var gTest = { + length: 2048, + numberOfChannels: 1, + createGraph(context) { + var buffer = context.createBuffer(1, 2048, context.sampleRate); + for (var i = 0; i < 2048; ++i) { + buffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate); + } + + var source = context.createBufferSource(); + + source.buffer = buffer; + + var srcWrapped = SpecialPowers.wrap(source); + ok("passThrough" in srcWrapped, "AudioBufferSourceNode should support the passThrough API"); + srcWrapped.passThrough = true; + + source.start(0); + return source; + }, + createExpectedBuffers(context) { + var expectedBuffer = context.createBuffer(1, 2048, context.sampleRate); + + return [expectedBuffer]; + }, +}; + +runTest(); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_audioBufferSourceNodeRate.html b/dom/media/webaudio/test/test_audioBufferSourceNodeRate.html new file mode 100644 index 0000000000..60a065c2a9 --- /dev/null +++ b/dom/media/webaudio/test/test_audioBufferSourceNodeRate.html @@ -0,0 +1,58 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test AudioBufferSourceNode</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); + +var rate = 44100; +var off = new OfflineAudioContext(1, rate, rate); +var off2 = new OfflineAudioContext(1, rate, rate); + +var source = off.createBufferSource(); +var source2 = off2.createBufferSource(); + +// a buffer of a 440Hz at half the length. If we detune by -1200 or set the +// playbackRate to 0.5, we should get 44100 samples back with a sine at 220Hz. +var buf = off.createBuffer(1, rate / 2, rate); +var bufarray = buf.getChannelData(0); +for (var i = 0; i < bufarray.length; i++) { + bufarray[i] = Math.sin(i * 440 * 2 * Math.PI / rate); +} + +source.buffer = buf; +source.playbackRate.value = 0.25; // slowdown to 25% speed +source.connect(off.destination); +source.start(0); + +source2.buffer = buf; +source2.detune.value = -2400; // two octaves -> slowdown to 25% speed +source2.connect(off2.destination); +source2.start(0); + +off.startRendering().then((renderedPlaybackRate) => { + // we don't care about comparing the value here, we just want to know whether + // the second part is noisy. + var rmsValue = rms(renderedPlaybackRate, 0, 22050); + ok(rmsValue != 0, "Resampling happened (rms of the second part " + rmsValue + ")"); + + off2.startRendering().then((renderedDetune) => { + var rmsValue = rms(renderedDetune, 0, 22050); + ok(rmsValue != 0, "Resampling happened (rms of the second part " + rmsValue + ")"); + // The two buffers should be the same: detune of -2400 is a slowdown to 25% speed + compareBuffers(renderedPlaybackRate, renderedDetune); + SimpleTest.finish(); + }); +}); + +</script> +</pre> +</body> +</html>
\ No newline at end of file diff --git a/dom/media/webaudio/test/test_audioContextGC.html b/dom/media/webaudio/test/test_audioContextGC.html new file mode 100644 index 0000000000..be9990cfad --- /dev/null +++ b/dom/media/webaudio/test/test_audioContextGC.html @@ -0,0 +1,162 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test inactive AudioContext is garbage collected</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); + +let ids; + +const observer = (subject, topic, data) => { + const id = parseInt(data); + if (ids) { + ok(ids.delete(id), "Collected AudioNode id " + id); + } +} +SpecialPowers.addObserver(observer, "webaudio-node-demise"); + +SimpleTest.registerCleanupFunction(function() { + if (observer) { + SpecialPowers.removeObserver(observer, "webaudio-node-demise"); + } +}); + +function id(node) { + return SpecialPowers.getPrivilegedProps(node, "id"); +} + +let tests = [{ + name: "Bare running AudioContext", setup: () => { + const ac = new AudioContext(); + ids.add(id(ac.destination)); + // Await state change notification before collection. + return new Promise((resolve) => { + ac.onstatechange = () => { + is(ac.state, "running", "ac.state"); + resolve(); + }; + }); + } +}, { + name: "Stopped source", setup: () => { + const ac = new AudioContext(); + ids.add(id(ac.destination)); + const source = new ConstantSourceNode(ac); + ids.add(id(source)); + source.start(); + source.stop(); + // Await ended notification before collection. + return new Promise((resolve) => { + source.onended = () => { + is(ac.state, "running", "ac.state"); + resolve(); + }; + }); + } +}, { + name: "OfflineAudioContext not started", setup: () => { + const ac = new OfflineAudioContext({ + numberOfChannels: 1, length: 1, sampleRate: 48000 + }); + ids.add(id(ac.destination)); + const source = new ConstantSourceNode(ac); + ids.add(id(source)); + source.start(); + } +}, { + name: "Completed OfflineAudioContext", setup: async () => { + const ac = new OfflineAudioContext({ + numberOfChannels: 1, length: 1, sampleRate: 48000 + }); + ids.add(id(ac.destination)); + const sourceBeforeStart = new ConstantSourceNode(ac); + ids.add(id(sourceBeforeStart)); + sourceBeforeStart.start(); + ac.startRendering(); + await new Promise((resolve) => { + ac.oncomplete = () => { + resolve(); + }; + }); + const sourceAfterComplete = new ConstantSourceNode(ac); + ids.add(id(sourceAfterComplete)); + sourceAfterComplete.start(); + } +}, { + name: "suspended AudioContext", setup: async () => { + const ac = new AudioContext(); + ids.add(id(ac.destination)); + const sourceBeforeSuspend = new ConstantSourceNode(ac); + ids.add(id(sourceBeforeSuspend)); + sourceBeforeSuspend.start(); + ac.suspend(); + const sourceAfterSuspend = new ConstantSourceNode(ac); + ids.add(id(sourceAfterSuspend)); + sourceAfterSuspend.start(); + await new Promise((resolve) => { + ac.onstatechange = () => { + if (ac.state == "suspended") { + resolve(); + } + }; + }); + const sourceAfterSuspended = new ConstantSourceNode(ac); + ids.add(id(sourceAfterSuspended)); + sourceAfterSuspended.start(); + } +}, { + name: "closed AudioContext", setup: async () => { + const ac = new AudioContext(); + ids.add(id(ac.destination)); + const sourceBeforeClose = new ConstantSourceNode(ac); + ids.add(id(sourceBeforeClose)); + sourceBeforeClose.start(); + ac.close(); + const sourceAfterClose = new ConstantSourceNode(ac); + ids.add(id(sourceAfterClose)); + sourceAfterClose.start(); + await new Promise((resolve) => { + ac.onstatechange = () => { + if (ac.state == "closed") { + resolve(); + } + }; + }); + const sourceAfterClosed = new ConstantSourceNode(ac); + ids.add(id(sourceAfterClosed)); + sourceAfterClosed.start(); + } +}]; + +const start_next_test = async () => { + const test = tests.shift(); + if (!test) { + SimpleTest.finish(); + return; + } + // Collect all audio nodes from previous tests. + if (!ids) { + await new Promise(resolve => { + SpecialPowers.exactGC(resolve); + }); + } + ids = new Set(); + await test.setup(); + SpecialPowers.exactGC(() => { + is(ids.size, 0, + `All expected nodes for "${test.name}" should be collected`); + start_next_test(); + }); +} + +start_next_test(); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_audioContextParams_recordNonDefaultSampleRate.html b/dom/media/webaudio/test/test_audioContextParams_recordNonDefaultSampleRate.html new file mode 100644 index 0000000000..9f2e6c9116 --- /dev/null +++ b/dom/media/webaudio/test/test_audioContextParams_recordNonDefaultSampleRate.html @@ -0,0 +1,48 @@ +<!DOCTYPE HTML> +<html> +<head> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <script type="text/javascript" src="manifest.js"></script> +</head> +<body> +<pre id="test"> + +<script class="testbody" type="text/javascript"> +function startTest() { + let ctx = new AudioContext({sampleRate: 32000}); + let oscillator = ctx.createOscillator(); + let dest = ctx.createMediaStreamDestination(); + oscillator.connect(dest); + oscillator.start(); + let stream = dest.stream; + + let recorder = new MediaRecorder(stream); + recorder.ondataavailable = (e) => { + ok(true, 'recorder ondataavailable event'); + if (recorder.state == 'recording') { + ok(e.data.size > 0, 'check blob has data'); + recorder.stop(); + } + } + + recorder.onstop = () => { + ok(true, 'recorder stop event'); + SimpleTest.finish(); + } + + try { + recorder.start(1000); + ok(true, 'recorder started'); + is('recording', recorder.state, 'check record state recording'); + } catch (e) { + ok(false, 'Can t record audio context'); + } +} + +startTest(); +SimpleTest.waitForExplicitFinish(); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_audioContextParams_sampleRate.html b/dom/media/webaudio/test/test_audioContextParams_sampleRate.html new file mode 100644 index 0000000000..65cee61243 --- /dev/null +++ b/dom/media/webaudio/test/test_audioContextParams_sampleRate.html @@ -0,0 +1,82 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="mediaStreamPlayback.js"></script> +</head> +<body> +<pre id="test"> + +<script> +/* import-globals-from ../../webrtc/tests/mochitests/mediaStreamPlayback.js */ +createHTML({ + title: "Parallel MTG by setting AudioContextParam sample rate", + bug: "1387454", + visible: true +}); + +runTest(async () => { + // Test an AudioContext of specific sample rate. + // Verify that the oscillator produces a tone. + const rate1 = 500; + const ac1 = new AudioContext({sampleRate: 44100}); + const dest_ac1 = ac1.createMediaStreamDestination(); + const osc_ac1 = ac1.createOscillator(); + osc_ac1.frequency.value = rate1; + osc_ac1.connect(dest_ac1); + osc_ac1.start(0); + + const analyser = new AudioStreamAnalyser(ac1, dest_ac1.stream); + analyser.enableDebugCanvas(); + await analyser.waitForAnalysisSuccess( array => { + const freg_50Hz = array[analyser.binIndexForFrequency(50)]; + const freq_rate1 = array[analyser.binIndexForFrequency(rate1)]; + const freq_4000Hz = array[analyser.binIndexForFrequency(4000)]; + + info("Analysing audio frequency - low:target1:high = " + + freg_50Hz + ':' + freq_rate1 + ':' + freq_4000Hz); + return freg_50Hz < 50 && freq_rate1 > 200 && freq_4000Hz < 50; + }) + osc_ac1.stop(); + + // Same test using a new AudioContext of different sample rate. + const rate2 = 1500; + const ac2 = new AudioContext({sampleRate: 48000}); + const dest_ac2 = ac2.createMediaStreamDestination(); + const osc_ac2 = ac2.createOscillator(); + osc_ac2.frequency.value = rate2; + osc_ac2.connect(dest_ac2); + osc_ac2.start(0); + + const analyser2 = new AudioStreamAnalyser(ac2, dest_ac2.stream); + analyser2.enableDebugCanvas(); + await analyser2.waitForAnalysisSuccess( array => { + const freg_50Hz = array[analyser2.binIndexForFrequency(50)]; + const freq_rate2 = array[analyser2.binIndexForFrequency(rate2)]; + const freq_4000Hz = array[analyser2.binIndexForFrequency(4000)]; + + info("Analysing audio frequency - low:target2:high = " + + freg_50Hz + ':' + freq_rate2 + ':' + freq_4000Hz); + return freg_50Hz < 50 && freq_rate2 > 200 && freq_4000Hz < 50; + }) + osc_ac2.stop(); + + // Two AudioContexts with different sample rate cannot communicate. + mustThrowWith("Connect nodes with different sample rate", "NotSupportedError", + () => ac2.createMediaStreamSource(dest_ac1.stream)); + + // Two AudioContext with the same sample rate can communicate. + const ac3 = new AudioContext({sampleRate: 48000}); + const dest_ac3 = ac3.createMediaStreamDestination(); + ac2.createMediaStreamSource(dest_ac3.stream); + ok(true, "Connect nodes with the same sample rate is ok"); + + mustThrowWith("Invalid zero samplerate", "NotSupportedError", + () => new AudioContext({sampleRate: 0})); + + mustThrowWith("Invalid negative samplerate", "NotSupportedError", + () => new AudioContext({sampleRate: -1})); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_audioContextSuspendResumeClose.html b/dom/media/webaudio/test/test_audioContextSuspendResumeClose.html new file mode 100644 index 0000000000..36cf8f720c --- /dev/null +++ b/dom/media/webaudio/test/test_audioContextSuspendResumeClose.html @@ -0,0 +1,419 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test suspend, resume and close method of the AudioContext</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +function tryToCreateNodeOnClosedContext(ctx) { + is(ctx.state, "closed", "The context is in closed state"); + + [ { name: "createBufferSource" }, + { name: "createMediaStreamDestination", + onOfflineAudioContext: false}, + { name: "createScriptProcessor" }, + { name: "createStereoPanner" }, + { name: "createAnalyser" }, + { name: "createGain" }, + { name: "createDelay" }, + { name: "createBiquadFilter" }, + { name: "createWaveShaper" }, + { name: "createPanner" }, + { name: "createConvolver" }, + { name: "createChannelSplitter" }, + { name: "createChannelMerger" }, + { name: "createDynamicsCompressor" }, + { name: "createOscillator" }, + { name: "createMediaElementSource", + args: [new Audio()], + onOfflineAudioContext: false }, + { name: "createMediaStreamSource", + args: [(new AudioContext()).createMediaStreamDestination().stream], + onOfflineAudioContext: false } ].forEach(function(e) { + + if (e.onOfflineAudioContext == false && + ctx instanceof OfflineAudioContext) { + return; + } + + expectNoException(function() { + ctx[e.name].apply(ctx, e.args); + }, DOMException.INVALID_STATE_ERR); + }); +} + +function loadFile(url, callback) { + var xhr = new XMLHttpRequest(); + xhr.open("GET", url, true); + xhr.responseType = "arraybuffer"; + xhr.onload = function() { + callback(xhr.response); + }; + xhr.send(); +} + +// createBuffer, createPeriodicWave and decodeAudioData should work on a context +// that has `state` == "closed" +function tryLegalOpeerationsOnClosedContext(ctx) { + is(ctx.state, "closed", "The context is in closed state"); + + [ { name: "createBuffer", + args: [1, 44100, 44100] }, + { name: "createPeriodicWave", + args: [new Float32Array(10), new Float32Array(10)] } + ].forEach(function(e) { + expectNoException(function() { + ctx[e.name].apply(ctx, e.args); + }); + }); + loadFile("ting-44.1k-1ch.ogg", function(buf) { + ctx.decodeAudioData(buf).then(function(decodedBuf) { + ok(true, "decodeAudioData on a closed context should work, it did.") + finish(); + }).catch(function(e){ + ok(false, "decodeAudioData on a closed context should work, it did not"); + finish(); + }); + }); +} + +// Test that MediaStreams that are the output of a suspended AudioContext are +// producing silence +// ac1 produce a sine fed to a MediaStreamAudioDestinationNode +// ac2 is connected to ac1 with a MediaStreamAudioSourceNode, and check that +// there is silence when ac1 is suspended +function testMultiContextOutput() { + var ac1 = new AudioContext(), + ac2 = new AudioContext(); + + ac1.onstatechange = function() { + ac1.onstatechange = null; + + var osc1 = ac1.createOscillator(), + mediaStreamDestination1 = ac1.createMediaStreamDestination(); + + var mediaStreamAudioSourceNode2 = + ac2.createMediaStreamSource(mediaStreamDestination1.stream), + sp2 = ac2.createScriptProcessor(), + silentBuffersInARow = 0; + + + sp2.onaudioprocess = function(e) { + ac1.suspend().then(function() { + is(ac1.state, "suspended", "ac1 is suspended"); + sp2.onaudioprocess = checkSilence; + }); + sp2.onaudioprocess = null; + } + + function checkSilence(e) { + var input = e.inputBuffer.getChannelData(0); + var silent = true; + for (var i = 0; i < input.length; i++) { + if (input[i] != 0.0) { + silent = false; + } + } + + if (silent) { + silentBuffersInARow++; + if (silentBuffersInARow == 10) { + ok(true, + "MediaStreams produce silence when their input is blocked."); + sp2.onaudioprocess = null; + ac1.close(); + ac2.close(); + finish(); + } + } else { + is(silentBuffersInARow, 0, + "No non silent buffer inbetween silent buffers."); + } + } + + osc1.connect(mediaStreamDestination1); + + mediaStreamAudioSourceNode2.connect(sp2); + osc1.start(); + } +} + + +// Test that there is no buffering between contexts when connecting a running +// AudioContext to a suspended AudioContext. Our ScriptProcessorNode does some +// buffering internally, so we ensure this by using a very very low frequency +// on a sine, and oberve that the phase has changed by a big enough margin. +function testMultiContextInput() { + var ac1 = new AudioContext(), + ac2 = new AudioContext(); + + ac1.onstatechange = function() { + ac1.onstatechange = null; + + var osc1 = ac1.createOscillator(), + mediaStreamDestination1 = ac1.createMediaStreamDestination(), + sp1 = ac1.createScriptProcessor(); + + var mediaStreamAudioSourceNode2 = + ac2.createMediaStreamSource(mediaStreamDestination1.stream), + sp2 = ac2.createScriptProcessor(), + eventReceived = 0; + + + osc1.frequency.value = 0.0001; + + function checkDiscontinuity(e) { + var inputBuffer = e.inputBuffer.getChannelData(0); + if (eventReceived++ == 3) { + var delta = Math.abs(inputBuffer[1] - sp2.value), + theoreticalIncrement = 2048 * 3 * Math.PI * 2 * osc1.frequency.value / ac1.sampleRate; + ok(delta >= theoreticalIncrement, + "Buffering did not occur when the context was suspended (delta:" + delta + " increment: " + theoreticalIncrement+")"); + ac1.close(); + ac2.close(); + sp1.onaudioprocess = null; + sp2.onaudioprocess = null; + finish(); + } + } + + sp2.onaudioprocess = function(e) { + var inputBuffer = e.inputBuffer.getChannelData(0); + sp2.value = inputBuffer[inputBuffer.length - 1]; + ac2.suspend().then(function() { + ac2.resume().then(function() { + sp2.onaudioprocess = checkDiscontinuity; + }); + }); + } + + osc1.connect(mediaStreamDestination1); + osc1.connect(sp1); + + mediaStreamAudioSourceNode2.connect(sp2); + osc1.start(); + } +} + +// Test that ScriptProcessorNode's onaudioprocess don't get called while the +// context is suspended/closed. It is possible that we get the handler called +// exactly once after suspend, because the event has already been sent to the +// event loop. +function testScriptProcessNodeSuspended() { + var ac = new AudioContext(); + var sp = ac.createScriptProcessor(); + var remainingIterations = 30; + var afterResume = false; + ac.onstatechange = function() { + ac.onstatechange = null; + sp.onaudioprocess = function() { + ok(ac.state == "running", "If onaudioprocess is called, the context" + + " must be running (was " + ac.state + ", remainingIterations:" + remainingIterations +")"); + remainingIterations--; + if (!afterResume) { + if (remainingIterations == 0) { + ac.suspend().then(function() { + ac.resume().then(function() { + remainingIterations = 30; + afterResume = true; + }); + }); + } + } else { + sp.onaudioprocess = null; + finish(); + } + } + } + sp.connect(ac.destination); +} + +// Take an AudioContext, make sure it switches to running when the audio starts +// flowing, and then, call suspend, resume and close on it, tracking its state. +function testAudioContext() { + var ac = new AudioContext(); + is(ac.state, "suspended", "AudioContext should start in suspended state."); + var stateTracker = { + previous: ac.state, + // no promise for the initial suspended -> running + initial: { handler: false }, + suspend: { promise: false, handler: false }, + resume: { promise: false, handler: false }, + close: { promise: false, handler: false } + }; + + function initialSuspendToRunning() { + ok(stateTracker.previous == "suspended" && + ac.state == "running", + "AudioContext should switch to \"running\" when the audio hardware is" + + " ready."); + + stateTracker.previous = ac.state; + ac.onstatechange = afterSuspend; + stateTracker.initial.handler = true; + + ac.suspend().then(function() { + ok(!stateTracker.suspend.promise && !stateTracker.suspend.handler, + "Promise should be resolved before the callback, and only once.") + stateTracker.suspend.promise = true; + }); + } + + function afterSuspend() { + ok(stateTracker.previous == "running" && + ac.state == "suspended", + "AudioContext should switch to \"suspend\" when the audio stream is" + + "suspended."); + ok(stateTracker.suspend.promise && !stateTracker.suspend.handler, + "Handler should be called after the callback, and only once"); + + stateTracker.suspend.handler = true; + stateTracker.previous = ac.state; + ac.onstatechange = afterResume; + + ac.resume().then(function() { + ok(!stateTracker.resume.promise && !stateTracker.resume.handler, + "Promise should be called before the callback, and only once"); + stateTracker.resume.promise = true; + }); + } + + function afterResume() { + ok(stateTracker.previous == "suspended" && + ac.state == "running", + "AudioContext should switch to \"running\" when the audio stream resumes."); + + ok(stateTracker.resume.promise && !stateTracker.resume.handler, + "Handler should be called after the callback, and only once"); + + stateTracker.resume.handler = true; + stateTracker.previous = ac.state; + ac.onstatechange = afterClose; + + ac.close().then(function() { + ok(!stateTracker.close.promise && !stateTracker.close.handler, + "Promise should be called before the callback, and only once"); + stateTracker.close.promise = true; + tryToCreateNodeOnClosedContext(ac); + tryLegalOpeerationsOnClosedContext(ac); + }); + } + + function afterClose() { + ok(stateTracker.previous == "running" && + ac.state == "closed", + "AudioContext should switch to \"closed\" when the audio stream is" + + " closed."); + ok(stateTracker.close.promise && !stateTracker.close.handler, + "Handler should be called after the callback, and only once"); + } + + ac.onstatechange = initialSuspendToRunning; +} + +function testOfflineAudioContext() { + var o = new OfflineAudioContext(1, 44100, 44100); + is(o.state, "suspended", "OfflineAudioContext should start in suspended state."); + + expectRejectedPromise(o, "resume", "NotSupportedError"); + + var previousState = o.state, + finishedRendering = false; + function beforeStartRendering() { + ok(previousState == "suspended" && o.state == "running", "onstatechanged" + + "handler is called on state changed, and the new state is running"); + previousState = o.state; + o.onstatechange = onRenderingFinished; + } + + function onRenderingFinished() { + ok(previousState == "running" && o.state == "closed", + "onstatechanged handler is called when rendering finishes, " + + "and the new state is closed"); + ok(finishedRendering, "The Promise that is resolved when the rendering is" + + "done should be resolved earlier than the state change."); + previousState = o.state; + o.onstatechange = afterRenderingFinished; + + tryToCreateNodeOnClosedContext(o); + tryLegalOpeerationsOnClosedContext(o); + } + + function afterRenderingFinished() { + ok(false, "There should be no transition out of the closed state."); + } + + o.onstatechange = beforeStartRendering; + + o.startRendering().then(function(buffer) { + finishedRendering = true; + }); +} + +function testSuspendResumeEventLoop() { + var ac = new AudioContext(); + var source = ac.createBufferSource(); + source.buffer = ac.createBuffer(1, 44100, 44100); + source.onended = function() { + ok(true, "The AudioContext did resume."); + finish(); + } + ac.onstatechange = function() { + ac.onstatechange = null; + + ok(ac.state == "running", "initial state is running"); + ac.suspend(); + source.start(); + ac.resume(); + } +} + +function testResumeInStateChangeForResumeCallback() { + // Regression test for bug 1468085. + var ac = new AudioContext; + ac.onstatechange = function() { + ac.resume().then(() => { + ok(true, "resume promise resolved as expected."); + finish(); + }); + } +} + +var remaining = 0; +function finish() { + remaining--; + if (remaining == 0) { + SimpleTest.finish(); + } +} + + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + var tests = [ + testOfflineAudioContext, + testScriptProcessNodeSuspended, + testMultiContextOutput, + testMultiContextInput, + testSuspendResumeEventLoop, + testResumeInStateChangeForResumeCallback + ]; + + // See Bug 1305136, many intermittent failures on Linux + if (!navigator.platform.startsWith("Linux")) { + tests.push(testAudioContext); + } + + remaining = tests.length; + tests.forEach(function(f) { f() }); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_audioDestinationNode.html b/dom/media/webaudio/test/test_audioDestinationNode.html new file mode 100644 index 0000000000..e7a8b091ba --- /dev/null +++ b/dom/media/webaudio/test/test_audioDestinationNode.html @@ -0,0 +1,26 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test AudioDestinationNode as EventTarget</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); + +var ac = new AudioContext() +ac.destination.addEventListener("foo", function() { + ok(true, "Event received!"); + SimpleTest.finish(); +}); +ac.destination.dispatchEvent(new CustomEvent("foo")); + +</script> +</pre> +</body> +</html> + diff --git a/dom/media/webaudio/test/test_audioParamChaining.html b/dom/media/webaudio/test/test_audioParamChaining.html new file mode 100644 index 0000000000..85b8099e2e --- /dev/null +++ b/dom/media/webaudio/test/test_audioParamChaining.html @@ -0,0 +1,77 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test whether we can create an AudioContext interface</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish() + +function frameToTime(frame, rate) +{ + return frame / rate; +} + +const RATE = 44100; + +var oc = new OfflineAudioContext(1, 44100, RATE); +// This allows us to have a source that is simply a DC offset. +var source = oc.createBufferSource(); +var buf = oc.createBuffer(1, 1, RATE); +buf.getChannelData(0)[0] = 1; +source.loop = true; +source.buffer = buf; + +source.start(0); + +var gain = oc.createGain(); + +source.connect(gain).connect(oc.destination); + +var gain2 = oc.createGain(); +var rv2 = gain2.gain.linearRampToValueAtTime(0.1, 0.5); +ok(rv2 instanceof AudioParam, "linearRampToValueAtTime returns an AudioParam."); +ok(rv2 == gain2.gain, "linearRampToValueAtTime returns the right AudioParam."); + +rv2 = gain2.gain.exponentialRampToValueAtTime(0.01, 1.0); +ok(rv2 instanceof AudioParam, + "exponentialRampToValueAtTime returns an AudioParam."); +ok(rv2 == gain2.gain, + "exponentialRampToValueAtTime returns the right AudioParam."); + +rv2 = gain2.gain.setTargetAtTime(1.0, 2.0, 0.1); +ok(rv2 instanceof AudioParam, "setTargetAtTime returns an AudioParam."); +ok(rv2 == gain2.gain, "setTargetAtTime returns the right AudioParam."); + +var array = new Float32Array(10); +rv2 = gain2.gain.setValueCurveAtTime(array, 10, 11); +ok(rv2 instanceof AudioParam, "setValueCurveAtTime returns an AudioParam."); +ok(rv2 == gain2.gain, "setValueCurveAtTime returns the right AudioParam."); + +// We chain three automation methods, making a gain step. +var rv = gain.gain.setValueAtTime(0, frameToTime(0, RATE)) + .setValueAtTime(0.5, frameToTime(22000, RATE)) + .setValueAtTime(1, frameToTime(44000, RATE)); + +ok(rv instanceof AudioParam, "setValueAtTime returns an AudioParam."); +ok(rv == gain.gain, "setValueAtTime returns the right AudioParam."); + +oc.startRendering().then(function(rendered) { + console.log(rendered.getChannelData(0)); + is(rendered.getChannelData(0)[0], 0, + "The value of the first step is correct."); + is(rendered.getChannelData(0)[22050], 0.5, + "The value of the second step is correct"); + is(rendered.getChannelData(0)[44099], 1, + "The value of the third step is correct."); + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_audioParamExponentialRamp.html b/dom/media/webaudio/test/test_audioParamExponentialRamp.html new file mode 100644 index 0000000000..2416b5de14 --- /dev/null +++ b/dom/media/webaudio/test/test_audioParamExponentialRamp.html @@ -0,0 +1,58 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test AudioParam.exponentialRampToValue</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +var V0 = 0.1; +var V1 = 0.9; +var T0 = 0; + +var gTest = { + length: 2048, + numberOfChannels: 1, + createGraph(context) { + var sourceBuffer = context.createBuffer(1, 2048, context.sampleRate); + for (var i = 0; i < 2048; ++i) { + sourceBuffer.getChannelData(0)[i] = 1; + } + + var source = context.createBufferSource(); + source.buffer = sourceBuffer; + + var gain = context.createGain(); + gain.gain.setValueAtTime(V0, 0); + gain.gain.exponentialRampToValueAtTime(V1, 2048/context.sampleRate); + + source.connect(gain); + + source.start(0); + return gain; + }, + createExpectedBuffers(context) { + var T1 = 2048 / context.sampleRate; + var expectedBuffer = context.createBuffer(1, 2048, context.sampleRate); + for (var i = 0; i < 2048; ++i) { + var t = i / context.sampleRate; + expectedBuffer.getChannelData(0)[i] = V0 * Math.pow(V1 / V0, (t - T0) / (T1 - T0)); + } + return expectedBuffer; + }, +}; + + +SimpleTest.waitForExplicitFinish(); +// Comparing different AudioContexts may result in different timing reated information being reported +// when we jitter time, as they are on different Relative Timelines. +SpecialPowers.pushPrefEnv({"set": [["privacy.resistFingerprinting.reduceTimerPrecision.jitter", false]]}, runTest); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_audioParamGain.html b/dom/media/webaudio/test/test_audioParamGain.html new file mode 100644 index 0000000000..3977b94703 --- /dev/null +++ b/dom/media/webaudio/test/test_audioParamGain.html @@ -0,0 +1,61 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test AudioParam with pre-gain </title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); + +var ctx = new AudioContext(); +var source = ctx.createOscillator(); +var lfo = ctx.createOscillator(); +var lfoIntensity = ctx.createGain(); +var effect = ctx.createGain(); +var sp = ctx.createScriptProcessor(2048, 1); + +source.frequency.value = 440; +lfo.frequency.value = 2; +// Very low gain, so the LFO should have very little influence +// on the source, its RMS value should be close to the nominal value +// for a sine wave. +lfoIntensity.gain.value = 0.0001; + +lfo.connect(lfoIntensity); +lfoIntensity.connect(effect.gain); +source.connect(effect); +effect.connect(sp); + +sp.onaudioprocess = function(e) { + var buffer = e.inputBuffer.getChannelData(0); + var rms = 0; + for (var i = 0; i < buffer.length; i++) { + rms += buffer[i] * buffer[i]; + } + + rms /= buffer.length; + rms = Math.sqrt(rms); + + // 1 / Math.sqrt(2) is the theoretical RMS value for a sine wave. + ok(fuzzyCompare(rms, 1 / Math.sqrt(2)), + "Gain correctly applied to the AudioParam."); + + ctx = null; + sp.onaudioprocess = null; + lfo.stop(0); + source.stop(0); + + SimpleTest.finish(); +} + +lfo.start(0); +source.start(0); + +</script> +</pre> +</body> diff --git a/dom/media/webaudio/test/test_audioParamLinearRamp.html b/dom/media/webaudio/test/test_audioParamLinearRamp.html new file mode 100644 index 0000000000..5ec26467e8 --- /dev/null +++ b/dom/media/webaudio/test/test_audioParamLinearRamp.html @@ -0,0 +1,54 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test AudioParam.linearRampToValue</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +var V0 = 0.1; +var V1 = 0.9; +var T0 = 0; + +var gTest = { + length: 2048, + numberOfChannels: 1, + createGraph(context) { + var sourceBuffer = context.createBuffer(1, 2048, context.sampleRate); + for (var i = 0; i < 2048; ++i) { + sourceBuffer.getChannelData(0)[i] = 1; + } + + var source = context.createBufferSource(); + source.buffer = sourceBuffer; + + var gain = context.createGain(); + gain.gain.setValueAtTime(V0, 0); + gain.gain.linearRampToValueAtTime(V1, 2048/context.sampleRate); + + source.connect(gain); + + source.start(0); + return gain; + }, + createExpectedBuffers(context) { + var T1 = 2048 / context.sampleRate; + var expectedBuffer = context.createBuffer(1, 2048, context.sampleRate); + for (var i = 0; i < 2048; ++i) { + var t = i / context.sampleRate; + expectedBuffer.getChannelData(0)[i] = V0 + (V1 - V0) * ((t - T0) / (T1 - T0)); + } + return expectedBuffer; + }, +}; + +runTest(); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_audioParamSetCurveAtTime.html b/dom/media/webaudio/test/test_audioParamSetCurveAtTime.html new file mode 100644 index 0000000000..e21b58bb19 --- /dev/null +++ b/dom/media/webaudio/test/test_audioParamSetCurveAtTime.html @@ -0,0 +1,54 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test AudioParam.linearRampToValue</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +var T0 = 0; + +var gTest = { + length: 2048, + numberOfChannels: 1, + createGraph(context) { + var source = context.createConstantSource(); + + var gain = context.createGain(); + gain.gain.setValueCurveAtTime(this.curve, T0, this.duration); + source.connect(gain); + + source.start(0); + return gain; + }, + createExpectedBuffers(context) { + this.duration = 1024 / context.sampleRate; + this.curve = new Float32Array([1.0, 0.5, 0.75, 0.25]); + var expectedBuffer = context.createBuffer(1, 2048, context.sampleRate); + var data = expectedBuffer.getChannelData(0); + var step = 1024 / 3; + for (var i = 0; i < 2048; ++i) { + if (i < step) { + data[i] = 1.0 - 0.5*i/step; + } else if (i < 2*step) { + data[i] = 0.5 + 0.25*(i - step)/step; + } else if (i < 3*step) { + data[i] = 0.75 - 0.5*(i - 2*step)/step; + } else { + data[i] = 0.25; + } + } + return expectedBuffer; + }, +}; + +runTest(); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_audioParamSetTargetAtTime.html b/dom/media/webaudio/test/test_audioParamSetTargetAtTime.html new file mode 100644 index 0000000000..442e209be6 --- /dev/null +++ b/dom/media/webaudio/test/test_audioParamSetTargetAtTime.html @@ -0,0 +1,54 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test AudioParam.setTargetAtTime</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +var V0 = 0.9; +var V1 = 0.1; +var T0 = 0; +var TimeConstant = 10; + +var gTest = { + length: 2048, + numberOfChannels: 1, + createGraph(context) { + var sourceBuffer = context.createBuffer(1, 2048, context.sampleRate); + for (var i = 0; i < 2048; ++i) { + sourceBuffer.getChannelData(0)[i] = 1; + } + + var source = context.createBufferSource(); + source.buffer = sourceBuffer; + + var gain = context.createGain(); + gain.gain.value = V0; + gain.gain.setTargetAtTime(V1, T0, TimeConstant); + + source.connect(gain); + + source.start(0); + return gain; + }, + createExpectedBuffers(context) { + var expectedBuffer = context.createBuffer(1, 2048, context.sampleRate); + for (var i = 0; i < 2048; ++i) { + var t = i / context.sampleRate; + expectedBuffer.getChannelData(0)[i] = V1 + (V0 - V1) * Math.exp(-(t - T0) / TimeConstant); + } + return expectedBuffer; + }, +}; + +runTest(); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_audioParamSetTargetAtTimeZeroTimeConstant.html b/dom/media/webaudio/test/test_audioParamSetTargetAtTimeZeroTimeConstant.html new file mode 100644 index 0000000000..2d18447a0d --- /dev/null +++ b/dom/media/webaudio/test/test_audioParamSetTargetAtTimeZeroTimeConstant.html @@ -0,0 +1,57 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test AudioParam.setTargetAtTime with zero time constant</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +var V0 = 0.9; +var V1 = 0.1; +var T0 = 0; +var TimeConstant = 0; + +var gTest = { + length: 2048, + numberOfChannels: 1, + createGraph(context) { + var sourceBuffer = context.createBuffer(1, 2048, context.sampleRate); + for (var i = 0; i < 2048; ++i) { + sourceBuffer.getChannelData(0)[i] = 1; + } + + var source = context.createBufferSource(); + source.buffer = sourceBuffer; + + var gain = context.createGain(); + gain.gain.value = V0; + gain.gain.setTargetAtTime(V1, T0, TimeConstant); + + source.connect(gain); + + source.start(0); + return gain; + }, + createExpectedBuffers(context) { + var expectedBuffer = context.createBuffer(1, 2048, context.sampleRate); + expectedBuffer.getChannelData(0)[0] = V0; + for (var i = 1; i < 2048; ++i) { + expectedBuffer.getChannelData(0)[i] = V1; + } + return expectedBuffer; + }, +}; + +SimpleTest.waitForExplicitFinish(); +// Comparing different AudioContexts may result in different timing reated information being reported +// when we jitter time, as they are on different Relative Timelines. +SpecialPowers.pushPrefEnv({"set": [["privacy.resistFingerprinting.reduceTimerPrecision.jitter", false]]}, runTest); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_audioParamSetValueAtTime.html b/dom/media/webaudio/test/test_audioParamSetValueAtTime.html new file mode 100644 index 0000000000..18c02837e6 --- /dev/null +++ b/dom/media/webaudio/test/test_audioParamSetValueAtTime.html @@ -0,0 +1,52 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test AudioParam.linearRampToValue</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +var V0 = 0.1; +var V1 = 0.9; +var T0 = 0; + +var gTest = { + length: 2048, + numberOfChannels: 1, + createGraph(context) { + var sourceBuffer = context.createBuffer(1, 2048, context.sampleRate); + for (var i = 0; i < 2048; ++i) { + sourceBuffer.getChannelData(0)[i] = 1; + } + + var source = context.createBufferSource(); + source.buffer = sourceBuffer; + + var gain = context.createGain(); + gain.gain.value = 0; + gain.gain.setValueAtTime(V0, 1024/context.sampleRate); + + source.connect(gain); + + source.start(0); + return gain; + }, + createExpectedBuffers(context) { + var expectedBuffer = context.createBuffer(1, 2048, context.sampleRate); + for (var i = 1024; i < 2048; ++i) { + expectedBuffer.getChannelData(0)[i] = 0.1; + } + return expectedBuffer; + }, +}; + +runTest(); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_audioParamTimelineDestinationOffset.html b/dom/media/webaudio/test/test_audioParamTimelineDestinationOffset.html new file mode 100644 index 0000000000..76db12f88a --- /dev/null +++ b/dom/media/webaudio/test/test_audioParamTimelineDestinationOffset.html @@ -0,0 +1,45 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test AudioParam timeline events scheduled after the destination stream has started playback</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +SimpleTest.requestFlakyTimeout("This test needs to wait until the AudioDestinationNode's stream's timer starts."); + +var gTest = { + length: 16384, + numberOfChannels: 1, + createGraphAsync(context, callback) { + var sourceBuffer = context.createBuffer(1, 2048, context.sampleRate); + for (var i = 0; i < 2048; ++i) { + sourceBuffer.getChannelData(0)[i] = 1; + } + + setTimeout(function() { + var source = context.createBufferSource(); + source.buffer = sourceBuffer; + source.start(context.currentTime); + source.stop(context.currentTime + sourceBuffer.duration); + + var gain = context.createGain(); + gain.gain.setValueAtTime(0, context.currentTime); + gain.gain.setTargetAtTime(0, context.currentTime + sourceBuffer.duration, 1); + source.connect(gain); + + callback(gain); + }, 100); + }, +}; + +runTest(); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_badConnect.html b/dom/media/webaudio/test/test_badConnect.html new file mode 100644 index 0000000000..51e83f6088 --- /dev/null +++ b/dom/media/webaudio/test/test_badConnect.html @@ -0,0 +1,52 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test whether we can create an AudioContext interface</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script src="webaudio.js" type="text/javascript"></script> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + var context1 = new OfflineAudioContext(1, 128, 44100); + var context2 = new OfflineAudioContext(1, 128, 44100); + + var destination1 = context1.destination; + var destination2 = context2.destination; + var gain1 = new GainNode(context2); + + isnot(destination1, destination2, "Destination nodes should not be the same"); + isnot(destination1.context, destination2.context, "Destination nodes should not have the same context"); + + var source1 = context1.createBufferSource(); + + expectException(function() { + source1.connect(destination1, 1); + }, DOMException.INDEX_SIZE_ERR); + expectException(function() { + source1.connect(destination1, 0, 1); + }, DOMException.INDEX_SIZE_ERR); + expectException(function() { + source1.connect(destination2); + }, DOMException.INVALID_ACCESS_ERR); + expectException(function() { + source1.connect(gain1.gain); + }, DOMException.INVALID_ACCESS_ERR); + + source1.connect(destination1); + + expectException(function() { + source1.disconnect(1); + }, DOMException.INDEX_SIZE_ERR); + + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_biquadFilterNode.html b/dom/media/webaudio/test/test_biquadFilterNode.html new file mode 100644 index 0000000000..561198c491 --- /dev/null +++ b/dom/media/webaudio/test/test_biquadFilterNode.html @@ -0,0 +1,86 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test BiquadFilterNode</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script src="webaudio.js" type="text/javascript"></script> +<script class="testbody" type="text/javascript"> + +function near(a, b, msg) { + ok(Math.abs(a - b) < 1e-3, msg); +} + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + var context = new AudioContext(); + var buffer = context.createBuffer(1, 2048, context.sampleRate); + for (var i = 0; i < 2048; ++i) { + buffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate); + } + + var destination = context.destination; + + var source = context.createBufferSource(); + + var filter = context.createBiquadFilter(); + + source.buffer = buffer; + + source.connect(filter); + filter.connect(destination); + + // Verify default values + is(filter.type, "lowpass", "Correct default value for type"); + near(filter.frequency.defaultValue, 350, "Correct default value for filter frequency"); + near(filter.detune.defaultValue, 0, "Correct default value for filter detune"); + near(filter.Q.defaultValue, 1, "Correct default value for filter Q"); + near(filter.gain.defaultValue, 0, "Correct default value for filter gain"); + is(filter.channelCount, 2, "Biquad filter node has 2 input channels by default"); + is(filter.channelCountMode, "max", "Correct channelCountMode for the biquad filter node"); + is(filter.channelInterpretation, "speakers", "Correct channelCountInterpretation for the biquad filter node"); + + // Make sure that we can set all of the valid type values + var types = [ + "lowpass", + "highpass", + "bandpass", + "lowshelf", + "highshelf", + "peaking", + "notch", + "allpass", + ]; + for (var i = 0; i < types.length; ++i) { + filter.type = types[i]; + } + + // Make sure getFrequencyResponse handles invalid frequencies properly + var frequencies = new Float32Array([-1.0, context.sampleRate*0.5 - 1.0, context.sampleRate]); + var magResults = new Float32Array(3); + var phaseResults = new Float32Array(3); + filter.getFrequencyResponse(frequencies, magResults, phaseResults); + ok(isNaN(magResults[0]), "Invalid input frequency should give NaN magnitude response"); + ok(!isNaN(magResults[1]), "Valid input frequency should not give NaN magnitude response"); + ok(isNaN(magResults[2]), "Invalid input frequency should give NaN magnitude response"); + ok(isNaN(phaseResults[0]), "Invalid input frquency should give NaN phase response"); + ok(!isNaN(phaseResults[1]), "Valid input frquency should not give NaN phase response"); + ok(isNaN(phaseResults[2]), "Invalid input frquency should give NaN phase response"); + + source.start(0); + SimpleTest.executeSoon(function() { + source.stop(0); + source.disconnect(); + filter.disconnect(); + + SimpleTest.finish(); + }); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_biquadFilterNodePassThrough.html b/dom/media/webaudio/test/test_biquadFilterNodePassThrough.html new file mode 100644 index 0000000000..4db01b0b14 --- /dev/null +++ b/dom/media/webaudio/test/test_biquadFilterNodePassThrough.html @@ -0,0 +1,47 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test BiquadFilterNode with passthrough</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +var gTest = { + length: 2048, + numberOfChannels: 1, + createGraph(context) { + var source = context.createBufferSource(); + + var filter = context.createBiquadFilter(); + + source.buffer = this.buffer; + + source.connect(filter); + + var filterWrapped = SpecialPowers.wrap(filter); + ok("passThrough" in filterWrapped, "BiquadFilterNode should support the passThrough API"); + filterWrapped.passThrough = true; + + source.start(0); + return filter; + }, + createExpectedBuffers(context) { + this.buffer = context.createBuffer(1, 2048, context.sampleRate); + for (var i = 0; i < 2048; ++i) { + this.buffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate); + } + + return [this.buffer]; + }, +}; + +runTest(); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_biquadFilterNodeWithGain.html b/dom/media/webaudio/test/test_biquadFilterNodeWithGain.html new file mode 100644 index 0000000000..d97a753de9 --- /dev/null +++ b/dom/media/webaudio/test/test_biquadFilterNodeWithGain.html @@ -0,0 +1,61 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test BiquadFilterNode after a GainNode and tail - Bugs 924286 and 924288</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script src="webaudio.js" type="text/javascript"></script> +<script class="testbody" type="text/javascript"> + +const signalLength = 2048; + +var gTest = { + length: signalLength, + numberOfChannels: 1, + createGraph(context) { + // Two oscillators scheduled sequentially + var signalDuration = signalLength / context.sampleRate; + var osc1 = context.createOscillator(); + osc1.type = "square"; + osc1.start(0); + osc1.stop(signalDuration / 2); + var osc2 = context.createOscillator(); + osc2.start(signalDuration / 2); + osc2.stop(signalDuration); + + // Comparing a biquad on each source with one on both sources checks that + // the biquad on the first source doesn't shut down early. + var biquad1 = context.createBiquadFilter(); + osc1.connect(biquad1); + var biquad2 = context.createBiquadFilter(); + osc2.connect(biquad2); + + var gain = context.createGain(); + gain.gain.value = -1; + osc1.connect(gain); + osc2.connect(gain); + + var biquadWithGain = context.createBiquadFilter(); + gain.connect(biquadWithGain); + + // The output of biquadWithGain should be the inverse of the sum of the + // outputs of biquad1 and biquad2, so blend them together and expect + // silence. + var blend = context.createGain(); + biquad1.connect(blend); + biquad2.connect(blend); + biquadWithGain.connect(blend); + + return blend; + }, +}; + +runTest(); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_bug1027864.html b/dom/media/webaudio/test/test_bug1027864.html new file mode 100644 index 0000000000..847485ff88 --- /dev/null +++ b/dom/media/webaudio/test/test_bug1027864.html @@ -0,0 +1,74 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test bug 1027864</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); + +function observer(subject, topic, data) { + var id = parseInt(data); + var index = ids.indexOf(id); + if (index != -1) { + ok(true, "Dropping id " + id + " at index " + index); + ids.splice(index, 1); + if (!ids.length) { + SimpleTest.executeSoon(function() { + SimpleTest.finish(); + }); + } + } +} + +function id(node) { + return SpecialPowers.getPrivilegedProps(node, "id"); +} + +SpecialPowers.addAsyncObserver(observer, "webaudio-node-demise", false); + +SimpleTest.registerCleanupFunction(function() { + SpecialPowers.removeAsyncObserver(observer, "webaudio-node-demise"); +}); + +var ac = new AudioContext(); +var ids; + +(function() { + var delay = ac.createDelay(); + delay.delayTime.value = 0.03; + + var gain = ac.createGain(); + gain.gain.value = 0.6; + + delay.connect(gain); + gain.connect(delay); + + gain.connect(ac.destination); + + var source = ac.createOscillator(); + + source.connect(gain); + source.start(ac.currentTime); + source.stop(ac.currentTime + 0.1); + + ids = [ id(delay), id(gain), id(source) ]; +})(); + +setInterval(function() { + forceCC(); +}, 200); + +function forceCC() { + SpecialPowers.DOMWindowUtils.cycleCollect(); + SpecialPowers.DOMWindowUtils.garbageCollect(); +} + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_bug1056032.html b/dom/media/webaudio/test/test_bug1056032.html new file mode 100644 index 0000000000..ba38267e19 --- /dev/null +++ b/dom/media/webaudio/test/test_bug1056032.html @@ -0,0 +1,35 @@ +<!DOCTYPE HTML> +<html> +<meta charset=utf-8> +<head> + <title>Test that we can decode an mp3 (bug 1056032)</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> +var filename = "small-shot.mp3"; + +SimpleTest.waitForExplicitFinish(); + +addLoadEvent(function() { + var xhr = new XMLHttpRequest(); + xhr.open("GET", filename); + xhr.responseType = "arraybuffer"; + xhr.onload = function() { + var context = new AudioContext(); + context.decodeAudioData(xhr.response, function(b) { + ok(true, "We can decode an mp3 using decodeAudioData"); + SimpleTest.finish(); + }, function() { + ok(false, "We should be able to decode an mp3 using decodeAudioData but couldn't"); + SimpleTest.finish(); + }); + }; + xhr.send(null); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_bug1113634.html b/dom/media/webaudio/test/test_bug1113634.html new file mode 100644 index 0000000000..acdcba7c25 --- /dev/null +++ b/dom/media/webaudio/test/test_bug1113634.html @@ -0,0 +1,58 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test AudioParam.setTargetAtTime where the target time is the same as the time of a previous event</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +var V0 = 0.9; +var V1 = 0.1; +var T0 = 0; +var TimeConstant = 0.1; + +var gTest = { + length: 2048, + numberOfChannels: 1, + createGraph(context) { + var sourceBuffer = context.createBuffer(1, 2048, context.sampleRate); + for (var i = 0; i < 2048; ++i) { + sourceBuffer.getChannelData(0)[i] = 1; + } + + var source = context.createBufferSource(); + source.buffer = sourceBuffer; + + var gain = context.createGain(); + gain.gain.setValueAtTime(V0, T0); + gain.gain.setTargetAtTime(V1, T0, TimeConstant); + + source.connect(gain); + + source.start(0); + return gain; + }, + createExpectedBuffers(context) { + var expectedBuffer = context.createBuffer(1, 2048, context.sampleRate); + for (var i = 0; i < 2048; ++i) { + var t = i / context.sampleRate; + expectedBuffer.getChannelData(0)[i] = V1 + (V0 - V1) * Math.exp(-(t - T0) / TimeConstant); + } + return expectedBuffer; + }, +}; + +SimpleTest.waitForExplicitFinish(); +// Comparing different AudioContexts may result in different timing reated information being reported +// when we jitter time, as they are on different Relative Timelines. +SpecialPowers.pushPrefEnv({"set": [["privacy.resistFingerprinting.reduceTimerPrecision.jitter", false]]}, runTest); + + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_bug1118372.html b/dom/media/webaudio/test/test_bug1118372.html new file mode 100644 index 0000000000..f049b221e8 --- /dev/null +++ b/dom/media/webaudio/test/test_bug1118372.html @@ -0,0 +1,46 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test WaveShaperNode with no curve</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); + +addLoadEvent(function() { + var context = new OfflineAudioContext(1, 2048, 44100); + + var osc=context.createOscillator(); + var gain=context.createGain(); + var shaper=context.createWaveShaper(); + gain.gain.value=0.1; + shaper.curve=new Float32Array([-0.5,-0.5,1,1]); + + osc.connect(gain); + gain.connect(shaper); + shaper.connect(context.destination); + osc.start(0); + + context.startRendering().then(function(buffer) { + var samples = buffer.getChannelData(0); + // the signal should be scaled + var failures = 0; + for (var i = 0; i < 2048; ++i) { + if (samples[i] > 0.5) { + failures = failures + 1; + } + } + ok(failures == 0, "signal should have been rescaled by gain: found " + failures + " points too loud."); + SimpleTest.finish(); + }); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_bug1255618.html b/dom/media/webaudio/test/test_bug1255618.html new file mode 100644 index 0000000000..15e7351995 --- /dev/null +++ b/dom/media/webaudio/test/test_bug1255618.html @@ -0,0 +1,41 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test sync XHR does not crash unlinked AudioContext</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script> +SimpleTest.waitForExplicitFinish(); + +const filename = "test_bug1255618.html"; + +function collect_and_fetch() { + SpecialPowers.forceGC(); + SpecialPowers.forceCC(); + + var xhr = new XMLHttpRequest(); + xhr.open("GET", filename, false); + var ended = false; + xhr.onloadend = function() { ended = true; } + // Sync XHR will suspend timeouts, which involves any AudioContexts still + // registered with the window. + // See https://bugzilla.mozilla.org/show_bug.cgi?id=1255618#c0 + xhr.send(null); + + ok(ended, "No crash during fetch"); + SimpleTest.finish(); +} + +var ac = new AudioContext(); + +ac.onstatechange = function () { + ac.onstatechange = null; + is(ac.state, "running", "statechange to running"); + ac = null; + SimpleTest.executeSoon(collect_and_fetch); +} + +</script> +</body> diff --git a/dom/media/webaudio/test/test_bug1267579.html b/dom/media/webaudio/test/test_bug1267579.html new file mode 100644 index 0000000000..7003b345f5 --- /dev/null +++ b/dom/media/webaudio/test/test_bug1267579.html @@ -0,0 +1,46 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test that PeriodicWave handles fundamental fequency of zero</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +// This is the smallest value that the test framework will accept +const testLength = 256; + +SimpleTest.waitForExplicitFinish(); + +addLoadEvent(function() { + runTest(); +}); + +var gTest = { + numberOfChannels: 1, + createGraph(context) { + var osc = context.createOscillator(); + osc.setPeriodicWave(context. + createPeriodicWave(new Float32Array([0.0, 1.0]), + new Float32Array(2))); + osc.frequency.value = 0.0; + osc.start(); + return osc; + }, + createExpectedBuffers(context) { + var buffer = context.createBuffer(1, testLength, context.sampleRate); + + for (var i = 0; i < buffer.length; ++i) { + buffer.getChannelData(0)[i] = 1.0; + } + return buffer; + }, +}; + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_bug1355798.html b/dom/media/webaudio/test/test_bug1355798.html new file mode 100644 index 0000000000..9b46322bbc --- /dev/null +++ b/dom/media/webaudio/test/test_bug1355798.html @@ -0,0 +1,30 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test PannerNode produces output even when the even when the distance is + from the listener is zero, and the cone gain is present, regression test for + bug 1355798.</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> +SimpleTest.waitForExplicitFinish(); +var off = new OfflineAudioContext(1, 128, 44100); +var panner = off.createPanner(); +var osc = off.createOscillator(); +panner.setPosition(1, 1, 1); +off.listener.setPosition(1, 1, 1); +osc.connect(panner).connect(off.destination); +panner.coneOuterAngle = 359; +osc.start(); +off.startRendering().then(function(b) { + is(b.getChannelData(0).filter(x => isNaN(x)).length, 0); + SimpleTest.finish(); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_bug1447273.html b/dom/media/webaudio/test/test_bug1447273.html new file mode 100644 index 0000000000..f4b473d8ab --- /dev/null +++ b/dom/media/webaudio/test/test_bug1447273.html @@ -0,0 +1,175 @@ +<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test bug 1447273 - GainNode with a stereo input and changing volume</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+/**
+ * Sets up a stereo BufferSource and plumbs it through different gain node
+ * configurations. A control gain path with no changes to gain is used along
+ * with 2 other paths which should increase their gain. The result should be
+ * that audio travelling along the increased gain paths is louder than the
+ * control path.
+ */
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout(
+ "This test uses a live audio context and uses a setTimeout to schedule a " +
+ "change to the graph.");
+addLoadEvent(function() {
+ let context = new AudioContext();
+
+ let numChannels = 2;
+ let sampleRate = context.sampleRate;
+ // 60 seconds to mitigate timing issues on slow test machines
+ let recordingLength = 60;
+ let bufferLength = sampleRate * recordingLength;
+ let gainExplicitlyIncreased = false;
+ let sourceFinished = false;
+
+ // Create source buffer
+ let sourceBuffer = context.createBuffer(numChannels, bufferLength, sampleRate);
+ for (let i = 0; i < bufferLength; ++i) {
+ sourceBuffer.getChannelData(0)[i] = 1;
+ sourceBuffer.getChannelData(1)[i] = 1;
+ }
+ let source = context.createBufferSource();
+ source.buffer = sourceBuffer;
+
+ let gainNoChange = context.createGain();
+ let gainExplicitAssignment = context.createGain();
+ let gainSetValueAtTime = context.createGain();
+
+ // All gain nodes start of with the same gain
+ gainNoChange.gain.value = 0.25;
+ gainExplicitAssignment.gain.value = 0.25;
+ gainSetValueAtTime.gain.value = 0.25;
+
+ // Connect source to gain nodes:
+ // source--> gainNoChange
+ // |-> gainExplicitAssignment
+ // \-> gainSetValueAtTime
+ source.connect(gainNoChange);
+ source.connect(gainExplicitAssignment);
+ source.connect(gainSetValueAtTime);
+
+ // Create intermediate media streams (required to repro bug 1447273)
+ let destNoChange = context.createMediaStreamDestination();
+ let destExplicitAssignement = context.createMediaStreamDestination();
+ let destSetValueAtTime = context.createMediaStreamDestination();
+
+ let sourceNoChange = context.createMediaStreamSource(destNoChange.stream);
+ let sourceExplicitAssignement = context.createMediaStreamSource(destExplicitAssignement.stream);
+ let sourceSetValueAtTime = context.createMediaStreamSource(destSetValueAtTime.stream);
+
+ // Connect gain nodes to our intermediate streams:
+ // source--> gainNoChange -> destNoChange -> sourceNoChange
+ // |-> gainExplicitAssignment -> destExplicitAssignement -> sourceExplicitAssignement
+ // \-> gainSetValueAtTime -> destSetValueAtTime -> sourceSetValueAtTime
+ gainNoChange.connect(destNoChange);
+ gainExplicitAssignment.connect(destExplicitAssignement);
+ gainSetValueAtTime.connect(destSetValueAtTime);
+
+ // Create analysers for each path
+ let analyserNoChange = context.createAnalyser();
+ let analyserExplicitAssignment = context.createAnalyser();
+ let analyserSetValueAtTime = context.createAnalyser();
+
+ // Connect our intermediate media streams to analysers:
+ // source--> gainNoChange -> destNoChange -> sourceNoChange -> analyserNoChange
+ // |-> gainExplicitAssignment -> destExplicitAssignement -> sourceExplicitAssignement -> analyserExplicitAssignment
+ // \-> gainSetValueAtTime -> destSetValueAtTime -> sourceSetValueAtTime -> analyserSetValueAtTime
+ sourceNoChange.connect(analyserNoChange);
+ sourceExplicitAssignement.connect(analyserExplicitAssignment);
+ sourceSetValueAtTime.connect(analyserSetValueAtTime);
+
+ // Two seconds in, increase gain for setValueAt path
+ gainSetValueAtTime.gain.setValueAtTime(0.5, 2);
+
+ // Maximum values seen at each analyser node, will be updated by
+ // checkAnalysersForMaxValues() during test.
+ let maxNoGainChange = 0;
+ let maxExplicitAssignment = 0;
+ let maxSetValueAtTime = 0;
+
+ // Poll analysers and check for max values
+ function checkAnalysersForMaxValues() {
+ let findMaxValue =
+ (array) => array.reduce((a, b) => Math.max(Math.abs(a), Math.abs(b)));
+
+ let dataArray = new Float32Array(analyserNoChange.fftSize);
+ analyserNoChange.getFloatTimeDomainData(dataArray);
+ maxNoGainChange = Math.max(maxNoGainChange, findMaxValue(dataArray));
+
+ analyserExplicitAssignment.getFloatTimeDomainData(dataArray);
+ maxExplicitAssignment = Math.max(maxExplicitAssignment, findMaxValue(dataArray));
+
+ analyserSetValueAtTime.getFloatTimeDomainData(dataArray);
+ maxSetValueAtTime = Math.max(maxSetValueAtTime, findMaxValue(dataArray));
+
+ // End test if we've met our conditions
+ // Add a small amount to initial gain to make sure we're not getting
+ // passes due to rounding errors.
+ let epsilon = 0.01;
+ if (maxExplicitAssignment > (maxNoGainChange + epsilon) &&
+ maxSetValueAtTime > (maxNoGainChange + epsilon)) {
+ source.stop();
+ }
+ }
+
+ source.onended = () => {
+ sourceFinished = true;
+ info(`maxNoGainChange: ${maxNoGainChange}`);
+ info(`maxExplicitAssignment: ${maxExplicitAssignment}`);
+ info(`maxSetValueAtTime: ${maxSetValueAtTime}`);
+ ok(gainExplicitlyIncreased,
+ "Gain should be explicitly assinged during test!");
+ // Add a small amount to initial gain to make sure we're not getting
+ // passes due to rounding errors.
+ let epsilon = 0.01;
+ ok(maxExplicitAssignment > (maxNoGainChange + epsilon),
+ "Volume should increase due to explicit assignment to gain.value");
+ ok(maxSetValueAtTime > (maxNoGainChange + epsilon),
+ "Volume should increase due to setValueAtTime on gain.value");
+ SimpleTest.finish();
+ };
+
+ // Start the graph
+ source.start(0);
+
+ // We'll use this callback to check our analysers for gain
+ function animationFrameCb() {
+ if (sourceFinished) {
+ return;
+ }
+ requestAnimationFrame(animationFrameCb);
+ checkAnalysersForMaxValues();
+ }
+
+ // Using timers is gross, but as of writing there doesn't appear to be a
+ // nicer way to perform gain.value = 0.5 on our node. When/if we support
+ // suspend(time) on offline contexts we could potentially use that instead.
+
+ // Roughly 2 seconds through our source buffer (setTimeout flakiness) increase
+ // our gain on gainExplicitAssignment path.
+ window.setTimeout(() => {
+ gainExplicitAssignment.gain.value = 0.5;
+ // Make debugging flaky timeouts in test easier
+ info("Gain explicitly set!")
+ gainExplicitlyIncreased = true;
+ // Start checking analysers, we do this only after changing volume to avoid
+ // possible starvation of this timeout from requestAnimationFrame calls.
+ animationFrameCb();
+ }, 2000);
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_bug808374.html b/dom/media/webaudio/test/test_bug808374.html new file mode 100644 index 0000000000..b255c0b9c3 --- /dev/null +++ b/dom/media/webaudio/test/test_bug808374.html @@ -0,0 +1,22 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Crashtest for bug 808374</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +try { + var ctx = new AudioContext(); + ctx.createBuffer(0, 1, ctx.sampleRate); +} catch (e) { + ok(true, "The test should not crash during CC"); +} + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_bug827541.html b/dom/media/webaudio/test/test_bug827541.html new file mode 100644 index 0000000000..f205c7edf9 --- /dev/null +++ b/dom/media/webaudio/test/test_bug827541.html @@ -0,0 +1,24 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Tell the cycle collector about the audio contexts owned by nsGlobalWindow</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + var iframe = document.createElementNS("http://www.w3.org/1999/xhtml", "iframe"); + document.body.appendChild(iframe); + var frameWin = iframe.contentWindow; + new frameWin.AudioContext(); + document.body.removeChild(iframe); + expectException(() => new frameWin.AudioContext(), + DOMException.INVALID_STATE_ERR); + + // This test should not leak. +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_bug839753.html b/dom/media/webaudio/test/test_bug839753.html new file mode 100644 index 0000000000..f3e6598116 --- /dev/null +++ b/dom/media/webaudio/test/test_bug839753.html @@ -0,0 +1,18 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Crashtest for bug 839753</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +(new AudioContext()).destination.expando = null; +ok(true, "The test should not trigger wrapper cache assertions"); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_bug845960.html b/dom/media/webaudio/test/test_bug845960.html new file mode 100644 index 0000000000..17bf9a5700 --- /dev/null +++ b/dom/media/webaudio/test/test_bug845960.html @@ -0,0 +1,18 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Crashtest for bug 845960</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +(new AudioContext()).decodeAudioData(new ArrayBuffer(0), function() {}); +ok(true, "Should not crash when the optional failure callback is not specified"); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_bug856771.html b/dom/media/webaudio/test/test_bug856771.html new file mode 100644 index 0000000000..24a527ccd5 --- /dev/null +++ b/dom/media/webaudio/test/test_bug856771.html @@ -0,0 +1,26 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for bug 856771</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + var context = new AudioContext(); + + var source = context.createBufferSource(); + source.connect(context.destination); + ok(true, "Nothing should leak"); + + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_bug866570.html b/dom/media/webaudio/test/test_bug866570.html new file mode 100644 index 0000000000..90bd8f6985 --- /dev/null +++ b/dom/media/webaudio/test/test_bug866570.html @@ -0,0 +1,18 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Crashtest for bug 859600</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +(new AudioContext()).foo = null; +ok(true, "The test should not fatally assert"); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_bug866737.html b/dom/media/webaudio/test/test_bug866737.html new file mode 100644 index 0000000000..e8db6b76e8 --- /dev/null +++ b/dom/media/webaudio/test/test_bug866737.html @@ -0,0 +1,36 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for bug 866737</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +var context = new AudioContext(); + +(function() { + var d = context.createDelay(); + var panner = context.createPanner(); + d.connect(panner); + var gain = context.createGain(); + panner.connect(gain); + gain.connect(context.destination); + gain.disconnect(0); +})(); + +SpecialPowers.forceGC(); +SpecialPowers.forceCC(); + +var gain = context.createGain(); +gain.connect(context.destination); +gain.disconnect(0); + +ok(true, "No crashes should happen!"); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_bug867089.html b/dom/media/webaudio/test/test_bug867089.html new file mode 100644 index 0000000000..e5a5179530 --- /dev/null +++ b/dom/media/webaudio/test/test_bug867089.html @@ -0,0 +1,43 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Crashtest for bug 867089</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + var ctx = new AudioContext(); + + // Test invalid playbackRate values for AudioBufferSourceNode. + var source = ctx.createBufferSource(); + var buffer = ctx.createBuffer(2, 2048, 8000); + source.buffer = buffer; + source.playbackRate.value = 0.0; + source.connect(ctx.destination); + source.start(0); + + var source2 = ctx.createBufferSource(); + source2.buffer = buffer; + source2.playbackRate.value = -1.0; + source2.connect(ctx.destination); + source2.start(0); + + var source3 = ctx.createBufferSource(); + source3.buffer = buffer; + source3.playbackRate.value = 3000000.0; + source3.connect(ctx.destination); + source3.start(0); + ok(true, "We did not crash."); + SimpleTest.finish(); +}); + + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_bug867174.html b/dom/media/webaudio/test/test_bug867174.html new file mode 100644 index 0000000000..e949bcec41 --- /dev/null +++ b/dom/media/webaudio/test/test_bug867174.html @@ -0,0 +1,38 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Crashtest for bug 867174</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + var ctx = new AudioContext(); + + var source = ctx.createBufferSource(); + var buffer = ctx.createBuffer(2, 2048, 8000); + source.playbackRate.setTargetAtTime(0, 2, 3); + var sp = ctx.createScriptProcessor(); + source.connect(sp); + sp.connect(ctx.destination); + source.start(0); + + sp.onaudioprocess = function(e) { + // Now set the buffer + source.buffer = buffer; + + ok(true, "We did not crash."); + sp.onaudioprocess = null; + SimpleTest.finish(); + }; +}); + + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_bug873335.html b/dom/media/webaudio/test/test_bug873335.html new file mode 100644 index 0000000000..19fd6d4dae --- /dev/null +++ b/dom/media/webaudio/test/test_bug873335.html @@ -0,0 +1,22 @@ +<html> +<head> +<meta charset="UTF-8"> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script> + +function boom() +{ + (new AudioContext()).createScriptProcessor().hamster = {}; + SpecialPowers.forceCC(); + SpecialPowers.forceGC(); + ok(true, "test finished"); + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); + +</script> +</head> + +<body onload="boom();"></body> +</html> diff --git a/dom/media/webaudio/test/test_bug875221.html b/dom/media/webaudio/test/test_bug875221.html new file mode 100644 index 0000000000..134316f9ae --- /dev/null +++ b/dom/media/webaudio/test/test_bug875221.html @@ -0,0 +1,243 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Crashtest for bug 875221</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> +/* eslint-disable no-implied-eval */ + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestFlakyTimeout("This test is generated by a fuzzer, so we leave these setTimeouts untouched."); + +let o0, o1, o2, o3, o4, o5, o6, o7, o8, o9, o10; +let o11, o12, o13, o14, o15, o16, o17, o18, o19; + +try { o0 = document.createElement('audio'); } catch(e) { } +try { (document.body || document.documentElement).appendChild(o0); } catch(e) { } +try { o1 = new OfflineAudioContext(1, 10, (new AudioContext()).sampleRate); } catch(e) { } +try { o1.startRendering(); } catch(e) { } +try { o1.listener.dopplerFactor = 1; } catch(e) { } +try { o2 = o1.createScriptProcessor(); } catch(e) { } +try { o3 = o1.createChannelMerger(4); } catch(e) { } +try { o1.listener.dopplerFactor = 3; } catch(e) { } +try { o1.listener.setPosition(0, 134217728, 64) } catch(e) { } +try { o1.listener.dopplerFactor = 15; } catch(e) { } +try { o1.startRendering(); } catch(e) { } +try { o4 = new OfflineAudioContext(1, 10, (new AudioContext()).sampleRate); } catch(e) { } +try { o4.listener.speedOfSound = 2048; } catch(e) { } +try { o4.listener.setPosition(32768, 1, 1) } catch(e) { } +try { o5 = o1.createChannelSplitter(4); } catch(e) { } +try { o4.listener.setVelocity(4, 1, 0) } catch(e) { } +try { o4.startRendering(); } catch(e) { } +try { o4.startRendering(); } catch(e) { } +try { o4.listener.setPosition(64, 1, 0) } catch(e) { } +try { o1.listener.setOrientation(4194304, 15, 8388608, 15, 1, 1) } catch(e) { } +try { o1.listener.dopplerFactor = 256; } catch(e) { } +try { o6 = o4.createDelay(16); } catch(e) { } +try { o4.startRendering(); } catch(e) { } +try { o4.listener.setOrientation(0, 1, 0, 0, 31, 1073741824) } catch(e) { } +try { o4.listener.speedOfSound = 1; } catch(e) { } +try { o1.listener.speedOfSound = 0; } catch(e) { } +try { o1.startRendering(); } catch(e) { } +try { o6.connect(o3, 1, 0) } catch(e) { } +try { o1.listener.setPosition(4294967296, 32, 1) } catch(e) { } +try { o1.listener.speedOfSound = 0; } catch(e) { } +try { o1.listener.speedOfSound = 0; } catch(e) { } +try { o1.listener.setVelocity(1, 256, 0) } catch(e) { } +try { o4.startRendering(); } catch(e) { } +try { o3.disconnect() } catch(e) { } +setTimeout("try { o4.startRendering(); } catch(e) { }",50) +try { o4.listener.setOrientation(0, 0, 2048, 128, 16384, 127) } catch(e) { } +try { o4.listener.setVelocity(0, 4, 1) } catch(e) { } +try { o7 = o4.createScriptProcessor(1024, 4, 1); } catch(e) { } +try { o8 = o4.createDynamicsCompressor(); } catch(e) { } +try { o1.startRendering(); } catch(e) { } +try { o2.connect(o7); } catch(e) { } +try { o7.connect(o4); } catch(e) { } +try { o2.connect(o7); } catch(e) { } +try { o7.connect(o4); } catch(e) { } +SpecialPowers.forceCC(); +SpecialPowers.forceGC(); +try { o4.listener.setOrientation(8192, 1, 1, 512, 0, 15) } catch(e) { } +setTimeout("try { o7.onaudioprocess = function() {}; } catch(e) { }",50) +try { o1.startRendering(); } catch(e) { } +try { o1.listener.speedOfSound = 1073741824; } catch(e) { } +try { o2.connect(o7); } catch(e) { } +try { o7.connect(o4); } catch(e) { } +try { o9 = o4.createScriptProcessor(1024, 1, 4); } catch(e) { } +try { o10 = o4.createAnalyser(); } catch(e) { } +try { o4.listener.speedOfSound = 0; } catch(e) { } +try { o2.connect(o7); } catch(e) { } +try { o9.connect(o1); } catch(e) { } +try { o4.listener.setVelocity(524288, 1, 65536) } catch(e) { } +setTimeout("try { o2.connect(o9); } catch(e) { } setTimeout(done, 0);",1000) +try { o7.connect(o4); } catch(e) { } +try { o1.listener.setVelocity(1, 127, 31) } catch(e) { } +try { o2.connect(o7); } catch(e) { } +try { o7.connect(o1); } catch(e) { } +setTimeout("try { o5.disconnect() } catch(e) { }",100) +try { o2.connect(o9); } catch(e) { } +try { o7.connect(o4); } catch(e) { } +try { o4.startRendering(); } catch(e) { } +setTimeout("try { o1.listener.dopplerFactor = 1; } catch(e) { }",100) +try { o5.disconnect() } catch(e) { } +try { o1.startRendering(); } catch(e) { } +try { o1.startRendering(); } catch(e) { } +try { o10.disconnect() } catch(e) { } +try { o1.startRendering(); } catch(e) { } +try { o11 = o1.createGain(); } catch(e) { } +try { o2.connect(o7); } catch(e) { } +try { o9.connect(o4); } catch(e) { } +try { o4.listener.setOrientation(31, 0, 15, 0, 33554432, 1) } catch(e) { } +try { o4.listener.dopplerFactor = 1; } catch(e) { } +try { o2.connect(o7); } catch(e) { } +try { o7.connect(o4); } catch(e) { } +try { o2.connect(o7); } catch(e) { } +setTimeout("try { o9.connect(o4); } catch(e) { }",50) +try { o2.connect(o9); } catch(e) { } +setTimeout("try { o9.connect(o1); } catch(e) { }",200) +try { o2.connect(o7); } catch(e) { } +try { o7.connect(o1); } catch(e) { } +try { o12 = o4.createDynamicsCompressor(); } catch(e) { } +try { o2.connect(o7); } catch(e) { } +try { o9.connect(o1); } catch(e) { } +try { o9.onaudioprocess = function() {}; } catch(e) { } +try { o1.listener.speedOfSound = 262144; } catch(e) { } +try { o2.connect(o7); } catch(e) { } +try { o7.connect(o4); } catch(e) { } +try { o2.connect(o9); } catch(e) { } +setTimeout("try { o7.connect(o4); } catch(e) { }",50) +try { o2.connect(o9); } catch(e) { } +try { o7.connect(o4); } catch(e) { } +try { o13 = o4.createGain(); } catch(e) { } +try { o4.listener.dopplerFactor = 31; } catch(e) { } +try { o11.gain.value = 268435456; } catch(e) { } +try { o1.listener.setOrientation(63, 3, 1, 63, 1, 2147483648) } catch(e) { } +try { o2.connect(o9); } catch(e) { } +try { o7.connect(o4); } catch(e) { } +try { o4.listener.setVelocity(1, 0, 1) } catch(e) { } +try { o11.gain.value = 65536; } catch(e) { } +try { o2.connect(o9); } catch(e) { } +setTimeout("try { o7.connect(o4); } catch(e) { }",200) +try { o14 = o4.createDynamicsCompressor(); } catch(e) { } +setTimeout("try { o2.connect(o9); } catch(e) { }",50) +try { o7.connect(o1); } catch(e) { } +try { o15 = o1.createWaveShaper(); } catch(e) { } +try { o2.connect(o7); } catch(e) { } +try { o7.connect(o1); } catch(e) { } +try { o16 = o1.createWaveShaper(); } catch(e) { } +try { o11.gain.value = 1; } catch(e) { } +try { o1.listener.speedOfSound = 16; } catch(e) { } +try { o4.listener.setVelocity(0, 127, 15) } catch(e) { } +try { o1.listener.setVelocity(0, 2048, 16777216) } catch(e) { } +try { o13.gain.value = 0; } catch(e) { } +try { o2.connect(o9); } catch(e) { } +try { o7.connect(o4); } catch(e) { } +try { o2.connect(o9); } catch(e) { } +try { o9.connect(o1); } catch(e) { } +try { o17 = document.createElement('audio'); } catch(e) { } +try { (document.body || document.documentElement).appendChild(o0); } catch(e) { } +try { o4.listener.setVelocity(3, 1, 256) } catch(e) { } +try { o11.gain.cancelScheduledValues(1) } catch(e) { } +try { o1.listener.dopplerFactor = 524288; } catch(e) { } +try { o9.onaudioprocess = function() {}; } catch(e) { } +setTimeout("try { o7.connect(o13, 0, 0) } catch(e) { }",50) +try { o1.listener.speedOfSound = 0; } catch(e) { } +try { o10.disconnect() } catch(e) { } +try { o2.connect(o9); } catch(e) { } +try { o9.connect(o4); } catch(e) { } +try { o2.connect(o7); } catch(e) { } +try { o9.connect(o4); } catch(e) { } +try { o1.listener.speedOfSound = 1; } catch(e) { } +try { o15.disconnect() } catch(e) { } +try { o11.gain.exponentialRampToValueAtTime(0, 15) } catch(e) { } +try { o15.curve = new Float32Array(15); } catch(e) { } +try { o4.listener.setVelocity(1, 1, 1) } catch(e) { } +try { o14.connect(o6, 0, 0) } catch(e) { } +try { o2.connect(o9); } catch(e) { } +try { o9.connect(o1); } catch(e) { } +try { o2.connect(o9); } catch(e) { } +try { o7.connect(o4); } catch(e) { } +try { o2.connect(o7); } catch(e) { } +setTimeout("try { o7.connect(o1); } catch(e) { }",100) +try { o4.listener.setVelocity(1, 7, 1) } catch(e) { } +try { o18 = document.createElement('audio'); } catch(e) { } +try { (document.body || document.documentElement).appendChild(o18); } catch(e) { } +try { o19 = o4.createGain(); } catch(e) { } +try { o2.connect(o7); } catch(e) { } +try { o7.connect(o1); } catch(e) { } +try { o4.listener.dopplerFactor = 0; } catch(e) { } +try { o1.listener.setPosition(256, 16, 1) } catch(e) { } +setTimeout("try { o2.connect(o9); } catch(e) { }",50) +try { o7.connect(o1); } catch(e) { } +try { o4.listener.speedOfSound = 31; } catch(e) { } +try { o2.connect(o7); } catch(e) { } +setTimeout("try { o9.connect(o4); } catch(e) { }",1000) +try { o11.gain.value = 127; } catch(e) { } +try { o7.connect(o7, 0, 0) } catch(e) { } +try { o4.listener.speedOfSound = 63; } catch(e) { } +try { o11.gain.value = 33554432; } catch(e) { } +try { o2.connect(o7); } catch(e) { } +try { o9.connect(o4); } catch(e) { } +try { o4.listener.speedOfSound = 16; } catch(e) { } +try { o4.listener.setVelocity(1048576, 0, 127) } catch(e) { } +try { o1.listener.dopplerFactor = 0; } catch(e) { } +try { o6.connect(o2, 0, 1) } catch(e) { } +try { o5.disconnect() } catch(e) { } +try { o3.disconnect() } catch(e) { } +try { o2.connect(o9); } catch(e) { } +try { o7.connect(o1); } catch(e) { } +try { o16.disconnect() } catch(e) { } +try { o2.connect(o9); } catch(e) { } +try { o7.connect(o1); } catch(e) { } +try { o9.disconnect() } catch(e) { } +try { o4.listener.speedOfSound = 1; } catch(e) { } +try { o2.connect(o7); } catch(e) { } +try { o7.connect(o4); } catch(e) { } +try { o11.gain.setValueCurveAtTime(new Float32Array(3), 2048, 3) } catch(e) { } +try { o13.gain.value = 8; } catch(e) { } +try { o2.connect(o7); } catch(e) { } +try { o9.connect(o4); } catch(e) { } +try { o4.listener.setOrientation(1, 2048, 1, 1, 0, 31) } catch(e) { } +try { o2.connect(o9); } catch(e) { } +try { o7.connect(o1); } catch(e) { } +try { o1.listener.speedOfSound = 256; } catch(e) { } +try { o2.connect(o7); } catch(e) { } +try { o9.connect(o4); } catch(e) { } +try { o4.listener.setVelocity(1, 67108864, 128) } catch(e) { } +setTimeout("try { o1.listener.setVelocity(0, 1, 1) } catch(e) { }",100) +try { o2.connect(o9); } catch(e) { } +try { o9.connect(o1); } catch(e) { } +setTimeout("try { o20 = o1.createBiquadFilter(); } catch(e) { }",200) +try { o13.gain.value = 4096; } catch(e) { } +try { o1.listener.dopplerFactor = 0; } catch(e) { } +try { o2.connect(o7); } catch(e) { } +try { o9.connect(o1); } catch(e) { } +try { o2.connect(o9); } catch(e) { } +try { o7.connect(o4); } catch(e) { } +setTimeout("try { o2.connect(o9); } catch(e) { }",200) +try { o7.connect(o1); } catch(e) { } +try { o3.connect(o15, 1, 1) } catch(e) { } +try { o2.connect(o12, 0, 0) } catch(e) { } +try { o19.gain.exponentialRampToValueAtTime(1, 0) } catch(e) { } +try { o2.connect(o7); } catch(e) { } +try { o7.connect(o4); } catch(e) { } +try { o2.connect(o7); } catch(e) { } +try { o9.connect(o1); } catch(e) { } +try { o2.connect(o7); } catch(e) { } +try { o7.connect(o4); } catch(e) { } + +function done() { + ok(true, "We did not crash."); + SimpleTest.finish(); +} + + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_bug875402.html b/dom/media/webaudio/test/test_bug875402.html new file mode 100644 index 0000000000..bb14c82037 --- /dev/null +++ b/dom/media/webaudio/test/test_bug875402.html @@ -0,0 +1,50 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Crashtest for bug 875402</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> +/* eslint-disable no-implied-eval */ + +SimpleTest.waitForExplicitFinish(); + +SimpleTest.requestFlakyTimeout("This test is generated by a fuzzer, so we leave these setTimeouts untouched."); + +let o1, o2, o4, o5, o7, o9, o13; + +try { o1 = new OfflineAudioContext(1, 10, (new AudioContext()).sampleRate); } catch(e) { } +try { o2 = o1.createScriptProcessor(); } catch(e) { } +try { o4 = new OfflineAudioContext(1, 10, (new AudioContext()).sampleRate); } catch(e) { } +try { o5 = o1.createChannelSplitter(4); } catch(e) { } +try { o7 = o4.createScriptProcessor(1024, 4, 1); } catch(e) { } +SpecialPowers.forceCC(); +SpecialPowers.forceGC(); +try { o1.startRendering(); } catch(e) { } +try { o2.connect(o7); } catch(e) { } +try { o7.connect(o4); } catch(e) { } +try { o9 = o4.createScriptProcessor(1024, 1, 4); } catch(e) { } +try { o2.connect(o7); } catch(e) { } +try { o9.connect(o1); } catch(e) { } +setTimeout("try { o2.connect(o9); } catch(e) { } done();",1000) +try { o7.connect(o4); } catch(e) { } +setTimeout("try { o5.disconnect() } catch(e) { }",100) +try { o2.connect(o9); } catch(e) { } +try { o4.startRendering(); } catch(e) { } +try { o2.connect(o9); } catch(e) { } +setTimeout("try { o7.connect(o4); } catch(e) { }",50) +try { o13 = o4.createGain(); } catch(e) { } +setTimeout("try { o7.connect(o13, 0, 0) } catch(e) { }",50) + +function done() { + ok(true, "We did not crash."); + SimpleTest.finish(); +} + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_bug894150.html b/dom/media/webaudio/test/test_bug894150.html new file mode 100644 index 0000000000..4577232d71 --- /dev/null +++ b/dom/media/webaudio/test/test_bug894150.html @@ -0,0 +1,21 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test whether we can create an AudioContext interface</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script> + +var ac = new AudioContext(); +ac.createPanner(); +var listener = ac.listener; +SpecialPowers.forceGC(); +SpecialPowers.forceCC(); +listener.setOrientation(0, 0, -1, 0, 0, 0); + +ok(true, "No crashes should happen!"); + +</script> +</body> diff --git a/dom/media/webaudio/test/test_bug956489.html b/dom/media/webaudio/test/test_bug956489.html new file mode 100644 index 0000000000..f0ae559b05 --- /dev/null +++ b/dom/media/webaudio/test/test_bug956489.html @@ -0,0 +1,56 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test when and currentTime are in the same coordinate system</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestFlakyTimeout("This test needs to wait a while for the AudioContext's timer to start."); +addLoadEvent(function() { + var freq = 330; + + var context = new AudioContext(); + + var buffer = context.createBuffer(1, context.sampleRate / freq, context.sampleRate); + for (var i = 0; i < buffer.length; ++i) { + buffer.getChannelData(0)[i] = Math.sin(2 * Math.PI * i / buffer.length); + } + + var source = context.createBufferSource(); + source.loop = true; + source.buffer = buffer; + + setTimeout(function () { + var finished = false; + + source.start(context.currentTime); + var processor = context.createScriptProcessor(256, 1, 1); + processor.onaudioprocess = function (e) { + if (finished) return; + var c = e.inputBuffer.getChannelData(0); + var result = true; + + for (var i = 0; i < buffer.length; ++i) { + if (Math.abs(c[i] - buffer.getChannelData(0)[i]) > 1e-9) { + result = false; + break; + } + } + finished = true; + ok(result, "when and currentTime are in same time coordinate system"); + SimpleTest.finish(); + } + processor.connect(context.destination); + source.connect(processor); + }, 500); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_bug964376.html b/dom/media/webaudio/test/test_bug964376.html new file mode 100644 index 0000000000..bc9d167dcd --- /dev/null +++ b/dom/media/webaudio/test/test_bug964376.html @@ -0,0 +1,64 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test repeating audio is not distorted</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +function gcd(a, b) { + if (b === 0) { + return a; + } + return gcd(b, a % b); +} + +var SAMPLE_PLACEMENT = 128; + +var gTest = { + length: 2048, + numberOfChannels: 1, + + createGraph(context) { + var freq = Math.round(context.sampleRate / SAMPLE_PLACEMENT); + var dur = context.sampleRate / gcd(freq, context.sampleRate); + var buffer = context.createBuffer(1, dur, context.sampleRate); + + for (var i = 0; i < context.sampleRate; ++i) { + buffer.getChannelData(0)[i] = Math.sin(freq * 2 * Math.PI * i / context.sampleRate); + } + + var source = context.createBufferSource(); + source.buffer = buffer; + source.loop = true; + source.playbackRate.setValueAtTime(0.5, SAMPLE_PLACEMENT / context.sampleRate); + source.start(0); + + return source; + }, + + createExpectedBuffers(context) { + var freq = Math.round(context.sampleRate / SAMPLE_PLACEMENT); + var expectedBuffer = context.createBuffer(1, 2048, context.sampleRate); + var c = expectedBuffer.getChannelData(0); + for (var i = 0; i < c.length; ++i) { + if (i < SAMPLE_PLACEMENT) { + c[i] = Math.sin(freq * 2 * Math.PI * i / context.sampleRate); + } else { + c[i] = Math.sin(freq / 2 * 2 * Math.PI * (i + SAMPLE_PLACEMENT) / context.sampleRate); + } + } + + return expectedBuffer; + }, +}; + +runTest(); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_bug966247.html b/dom/media/webaudio/test/test_bug966247.html new file mode 100644 index 0000000000..69831c33b6 --- /dev/null +++ b/dom/media/webaudio/test/test_bug966247.html @@ -0,0 +1,46 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test whether an audio file played with a volume set to 0 plays silence</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<audio preload=none src="ting-48k-1ch.ogg" controls> </audio> +<script> + SimpleTest.waitForExplicitFinish(); + + var count = 20; + + function isSilent(b) { + for (var i = 0; i < b.length; i++) { + if (b[i] != 0.0) { + return false; + } + } + return true; + } + + var a = document.getElementsByTagName("audio")[0]; + a.volume = 0.0; + var ac = new AudioContext(); + var measn = ac.createMediaElementSource(a); + var sp = ac.createScriptProcessor(); + + sp.onaudioprocess = function(e) { + var inputBuffer = e.inputBuffer.getChannelData(0); + ok(isSilent(inputBuffer), "The volume is set to 0, so all the elements of the buffer are supposed to be equal to 0.0"); + } + // Connect the MediaElementAudioSourceNode to the ScriptProcessorNode to check + // the audio volume. + measn.connect(sp); + a.play(); + + a.addEventListener("ended", function() { + sp.onaudioprocess = null; + SimpleTest.finish(); + }); + +</script> +</body> +</html> diff --git a/dom/media/webaudio/test/test_bug972678.html b/dom/media/webaudio/test/test_bug972678.html new file mode 100644 index 0000000000..ac96b4ac3b --- /dev/null +++ b/dom/media/webaudio/test/test_bug972678.html @@ -0,0 +1,62 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test buffers do not interfere when scheduled in sequence</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> +var OFFSETS = [0.005, 0.01, 0.02, 0.03]; +var LENGTH = 128; + +var gTest = { + length: 128 * OFFSETS.length, + numberOfChannels: 1, + + createGraph(context) { + var gain = context.createGain(); + + // create a repeating sample + var repeatingSample = context.createBuffer(1, 2, context.sampleRate); + var c = repeatingSample.getChannelData(0); + for (var i = 0; i < repeatingSample.length; ++i) { + c[i] = i % 2 == 0 ? 1 : -1; + } + + OFFSETS.forEach(function (offset, offsetIdx) { + // Schedule a set of nodes to repeat the sample. + for (var i = 0; i < LENGTH; i += repeatingSample.length) { + var source = context.createBufferSource(); + source.buffer = repeatingSample; + source.connect(gain); + source.start((offsetIdx * LENGTH + i + offset) / context.sampleRate); + } + + let buffer = context.createBuffer(1, LENGTH, context.sampleRate); + c = buffer.getChannelData(0); + for (var i = 0; i < buffer.length; ++i) { + c[i] = i % 2 == 0 ? -1 : 1; + } + + var source = context.createBufferSource(); + source.buffer = buffer; + source.connect(gain); + source.start((offsetIdx * LENGTH + offset) / context.sampleRate); + }); + + return gain; + }, + + createExpectedBuffers(context) { + return context.createBuffer(1, gTest.length, context.sampleRate); + }, +}; + +runTest(); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_channelMergerNode.html b/dom/media/webaudio/test/test_channelMergerNode.html new file mode 100644 index 0000000000..b62d34d6ba --- /dev/null +++ b/dom/media/webaudio/test/test_channelMergerNode.html @@ -0,0 +1,57 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test ChannelMergerNode</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +var gTest = { + length: 2048, + numberOfChannels: 6, + createGraph(context) { + var buffers = []; + for (var j = 0; j < 6; ++j) { + var buffer = context.createBuffer(2, 2048, context.sampleRate); + for (var i = 0; i < 2048; ++i) { + buffer.getChannelData(0)[i] = Math.sin(440 * 2 * (j + 1) * Math.PI * i / context.sampleRate); + // Second channel is silent + } + buffers.push(buffer); + } + + var merger = new ChannelMergerNode(context); + is(merger.channelCount, 1, "merger node has 1 input channels"); + is(merger.channelCountMode, "explicit", "Correct channelCountMode for the merger node"); + is(merger.channelInterpretation, "speakers", "Correct channelCountInterpretation for the merger node"); + + for (var i = 0; i < 6; ++i) { + var source = context.createBufferSource(); + source.buffer = buffers[i]; + source.connect(merger, 0, i); + source.start(0); + } + + return merger; + }, + createExpectedBuffers(context) { + var expectedBuffer = context.createBuffer(6, 2048, context.sampleRate); + for (var i = 0; i < 6; ++i) { + for (var j = 0; j < 2048; ++j) { + expectedBuffer.getChannelData(i)[j] = 0.5 * Math.sin(440 * 2 * (i + 1) * Math.PI * j / context.sampleRate); + } + } + return expectedBuffer; + }, +}; + +runTest(); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_channelMergerNodeWithVolume.html b/dom/media/webaudio/test/test_channelMergerNodeWithVolume.html new file mode 100644 index 0000000000..55b9ec0c0b --- /dev/null +++ b/dom/media/webaudio/test/test_channelMergerNodeWithVolume.html @@ -0,0 +1,60 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test ChannelMergerNode</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +var gTest = { + length: 2048, + numberOfChannels: 6, + createGraph(context) { + var buffers = []; + for (var j = 0; j < 6; ++j) { + var buffer = context.createBuffer(2, 2048, context.sampleRate); + for (var i = 0; i < 2048; ++i) { + buffer.getChannelData(0)[i] = Math.sin(440 * 2 * (j + 1) * Math.PI * i / context.sampleRate); + // Second channel is silent + } + buffers.push(buffer); + } + + var merger = context.createChannelMerger(); + is(merger.channelCount, 1, "merger node has 1 input channels"); + is(merger.channelCountMode, "explicit", "Correct channelCountMode for the merger node"); + is(merger.channelInterpretation, "speakers", "Correct channelCountInterpretation for the merger node"); + + for (var i = 0; i < 6; ++i) { + var source = context.createBufferSource(); + source.buffer = buffers[i]; + var gain = context.createGain(); + gain.gain.value = 0.5; + source.connect(gain); + gain.connect(merger, 0, i); + source.start(0); + } + + return merger; + }, + createExpectedBuffers(context) { + var expectedBuffer = context.createBuffer(6, 2048, context.sampleRate); + for (var i = 0; i < 6; ++i) { + for (var j = 0; j < 2048; ++j) { + expectedBuffer.getChannelData(i)[j] = 0.5 * 0.5 * Math.sin(440 * 2 * (i + 1) * Math.PI * j / context.sampleRate); + } + } + return expectedBuffer; + }, +}; + +runTest(); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_channelSplitterNode.html b/dom/media/webaudio/test/test_channelSplitterNode.html new file mode 100644 index 0000000000..d74845f821 --- /dev/null +++ b/dom/media/webaudio/test/test_channelSplitterNode.html @@ -0,0 +1,71 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test ChannelSplitterNode</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +// We do not use our generic graph test framework here because +// the splitter node is special in that it creates multiple +// output ports. + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + var context = new AudioContext(); + var buffer = context.createBuffer(4, 2048, context.sampleRate); + for (var j = 0; j < 4; ++j) { + for (var i = 0; i < 2048; ++i) { + buffer.getChannelData(j)[i] = Math.sin(440 * 2 * (j + 1) * Math.PI * i / context.sampleRate); + } + } + var emptyBuffer = context.createBuffer(1, 2048, context.sampleRate); + + var destination = context.destination; + + var source = context.createBufferSource(); + + var splitter = new ChannelSplitterNode(context); + is(splitter.channelCount, 6, "splitter node has 2 input channels by default"); + is(splitter.channelCountMode, "explicit", "Correct channelCountMode for the splitter node"); + is(splitter.channelInterpretation, "discrete", "Correct channelCountInterpretation for the splitter node"); + + source.buffer = buffer; + source.connect(splitter); + + var channelsSeen = 0; + function createHandler(i) { + return function(e) { + is(e.inputBuffer.numberOfChannels, 1, "Correct input channel count"); + if (i < 4) { + compareChannels(e.inputBuffer.getChannelData(0), buffer.getChannelData(i)); + } else { + compareChannels(e.inputBuffer.getChannelData(0), emptyBuffer.getChannelData(0)); + } + e.target.onaudioprocess = null; + ++channelsSeen; + + if (channelsSeen == 6) { + SimpleTest.finish(); + } + }; + } + + for (var i = 0; i < 6; ++i) { + var sp = context.createScriptProcessor(2048, 1); + splitter.connect(sp, i); + sp.onaudioprocess = createHandler(i); + sp.connect(destination); + } + + source.start(0); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_channelSplitterNodeWithVolume.html b/dom/media/webaudio/test/test_channelSplitterNodeWithVolume.html new file mode 100644 index 0000000000..c03f6deeaf --- /dev/null +++ b/dom/media/webaudio/test/test_channelSplitterNodeWithVolume.html @@ -0,0 +1,76 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test ChannelSplitterNode</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +// We do not use our generic graph test framework here because +// the splitter node is special in that it creates multiple +// output ports. + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + var context = new AudioContext(); + var buffer = context.createBuffer(4, 2048, context.sampleRate); + var expectedBuffer = context.createBuffer(4, 2048, context.sampleRate); + for (var j = 0; j < 4; ++j) { + for (var i = 0; i < 2048; ++i) { + buffer.getChannelData(j)[i] = Math.sin(440 * 2 * (j + 1) * Math.PI * i / context.sampleRate); + expectedBuffer.getChannelData(j)[i] = buffer.getChannelData(j)[i] / 2; + } + } + var emptyBuffer = context.createBuffer(1, 2048, context.sampleRate); + + var destination = context.destination; + + var source = context.createBufferSource(); + + var splitter = context.createChannelSplitter(); + is(splitter.channelCount, 6, "splitter node has 2 input channels by default"); + is(splitter.channelCountMode, "explicit", "Correct channelCountMode for the splitter node"); + is(splitter.channelInterpretation, "discrete", "Correct channelCountInterpretation for the splitter node"); + + source.buffer = buffer; + var gain = context.createGain(); + gain.gain.value = 0.5; + source.connect(gain); + gain.connect(splitter); + + var channelsSeen = 0; + function createHandler(i) { + return function(e) { + is(e.inputBuffer.numberOfChannels, 1, "Correct input channel count"); + if (i < 4) { + compareBuffers(e.inputBuffer.getChannelData(0), expectedBuffer.getChannelData(i)); + } else { + compareBuffers(e.inputBuffer.getChannelData(0), emptyBuffer.getChannelData(0)); + } + e.target.onaudioprocess = null; + ++channelsSeen; + + if (channelsSeen == 6) { + SimpleTest.finish(); + } + }; + } + + for (var i = 0; i < 6; ++i) { + var sp = context.createScriptProcessor(2048, 1); + splitter.connect(sp, i); + sp.onaudioprocess = createHandler(i); + sp.connect(destination); + } + + source.start(0); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_convolver-upmixing-1-channel-response.html b/dom/media/webaudio/test/test_convolver-upmixing-1-channel-response.html new file mode 100644 index 0000000000..50bd594821 --- /dev/null +++ b/dom/media/webaudio/test/test_convolver-upmixing-1-channel-response.html @@ -0,0 +1,143 @@ +<!DOCTYPE html> +<title>Test that up-mixing signals in ConvolverNode processing is linear</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script> +const EPSILON = 3.0 * Math.pow(2, -22); +// sampleRate is a power of two so that delay times are exact in base-2 +// floating point arithmetic. +const SAMPLE_RATE = 32768; +// Length of stereo convolver input in frames (arbitrary): +const STEREO_FRAMES = 256; +// Length of mono signal in frames. This is more than two blocks to ensure +// that at least one block will be mono, even if interpolation in the +// DelayNode means that stereo is output one block earlier and later than +// if frames are delayed without interpolation. +const MONO_FRAMES = 384; +// Length of response buffer: +const RESPONSE_FRAMES = 256; + +function test_linear_upmixing(channelInterpretation, initial_mono_frames) +{ + let stereo_input_end = initial_mono_frames + STEREO_FRAMES; + // Total length: + let length = stereo_input_end + RESPONSE_FRAMES + MONO_FRAMES + STEREO_FRAMES; + // The first two channels contain signal where some up-mixing occurs + // internally to a ConvolverNode when a stereo signal is added and removed. + // The last two channels are expected to contain the same signal, but mono + // and stereo signals are convolved independently before up-mixing the mono + // output to mix with the stereo output. + let context = new OfflineAudioContext({numberOfChannels: 4, + length, + sampleRate: SAMPLE_RATE}); + + let response = new AudioBuffer({numberOfChannels: 1, + length: RESPONSE_FRAMES, + sampleRate: context.sampleRate}); + + // Two stereo channel splitters will collect test and reference outputs. + let destinationMerger = new ChannelMergerNode(context, {numberOfInputs: 4}); + destinationMerger.connect(context.destination); + let testSplitter = + new ChannelSplitterNode(context, {numberOfOutputs: 2}); + let referenceSplitter = + new ChannelSplitterNode(context, {numberOfOutputs: 2}); + testSplitter.connect(destinationMerger, 0, 0); + testSplitter.connect(destinationMerger, 1, 1); + referenceSplitter.connect(destinationMerger, 0, 2); + referenceSplitter.connect(destinationMerger, 1, 3); + + // A GainNode mixes reference stereo and mono signals because up-mixing + // cannot be performed at a channel splitter. + let referenceGain = new GainNode(context); + referenceGain.connect(referenceSplitter); + referenceGain.channelInterpretation = channelInterpretation; + + // The impulse response for convolution contains two impulses so as to test + // effects in at least two processing blocks. + response.getChannelData(0)[0] = 0.5; + response.getChannelData(0)[response.length - 1] = 0.5; + + let testConvolver = new ConvolverNode(context, {disableNormalization: true, + buffer: response}); + testConvolver.channelInterpretation = channelInterpretation; + let referenceMonoConvolver = new ConvolverNode(context, + {disableNormalization: true, + buffer: response}); + let referenceStereoConvolver = new ConvolverNode(context, + {disableNormalization: true, + buffer: response}); + // No need to set referenceStereoConvolver.channelInterpretation because + // input is either silent or stereo. + testConvolver.connect(testSplitter); + // Mix reference convolver output. + referenceMonoConvolver.connect(referenceGain); + referenceStereoConvolver.connect(referenceGain); + + // The DelayNode initially has a single channel of silence, which is used to + // switch the stereo signal in and out. The output of the delay node is + // first mono silence (if there is a non-zero initial_mono_frames), then + // stereo, then mono silence, and finally stereo again. maxDelayTime is + // used to generate the middle mono silence period from the initial silence + // in the DelayNode and then generate the final period of stereo from its + // initial input. + let maxDelayTime = (length - STEREO_FRAMES) / context.sampleRate; + let delay = + new DelayNode(context, + {maxDelayTime, + delayTime: initial_mono_frames / context.sampleRate}); + // Schedule an increase in the delay to return to mono silence. + delay.delayTime.setValueAtTime(maxDelayTime, + stereo_input_end / context.sampleRate); + delay.connect(testConvolver); + delay.connect(referenceStereoConvolver); + + let stereoMerger = new ChannelMergerNode(context, {numberOfInputs: 2}); + stereoMerger.connect(delay); + + // Three independent signals + let monoSignal = new OscillatorNode(context, {frequency: 440}); + let leftSignal = new OscillatorNode(context, {frequency: 450}); + let rightSignal = new OscillatorNode(context, {frequency: 460}); + monoSignal.connect(testConvolver); + monoSignal.connect(referenceMonoConvolver); + leftSignal.connect(stereoMerger, 0, 0); + rightSignal.connect(stereoMerger, 0, 1); + monoSignal.start(); + leftSignal.start(); + rightSignal.start(); + + return context.startRendering(). + then((buffer) => { + let maxDiff = -1.0; + let frameIndex = 0; + let channelIndex = 0; + for (let c = 0; c < 2; ++c) { + let testOutput = buffer.getChannelData(0 + c); + let referenceOutput = buffer.getChannelData(2 + c); + for (var i = 0; i < buffer.length; ++i) { + var diff = Math.abs(testOutput[i] - referenceOutput[i]); + if (diff > maxDiff) { + maxDiff = diff; + frameIndex = i; + channelIndex = c; + } + } + } + assert_approx_equals(buffer.getChannelData(0 + channelIndex)[frameIndex], + buffer.getChannelData(2 + channelIndex)[frameIndex], + EPSILON, + `output at ${frameIndex} ` + + `in channel ${channelIndex}` ); + }); +} + +promise_test(() => test_linear_upmixing("speakers", MONO_FRAMES), + "speakers, initially mono"); +promise_test(() => test_linear_upmixing("discrete", MONO_FRAMES), + "discrete"); +// Gecko uses a separate path for "speakers" up-mixing when the convolver's +// first input is stereo, so test that separately. +promise_test(() => test_linear_upmixing("speakers", 0), + "speakers, initially stereo"); +</script> diff --git a/dom/media/webaudio/test/test_convolverNode.html b/dom/media/webaudio/test/test_convolverNode.html new file mode 100644 index 0000000000..c1677aafab --- /dev/null +++ b/dom/media/webaudio/test/test_convolverNode.html @@ -0,0 +1,31 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test the ConvolverNode interface</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + var context = new AudioContext(); + var conv = new ConvolverNode(context); + + is(conv.channelCount, 2, "Convolver node has 2 input channels by default"); + is(conv.channelCountMode, "clamped-max", "Correct channelCountMode for the Convolver node"); + is(conv.channelInterpretation, "speakers", "Correct channelCountInterpretation for the Convolver node"); + + is(conv.buffer, null, "Default buffer value"); + is(conv.normalize, true, "Default normalize value"); + + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_convolverNodeChannelCount.html b/dom/media/webaudio/test/test_convolverNodeChannelCount.html new file mode 100644 index 0000000000..03824578ea --- /dev/null +++ b/dom/media/webaudio/test/test_convolverNodeChannelCount.html @@ -0,0 +1,61 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test ConvolverNode channel count</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script src="webaudio.js" type="text/javascript"></script> +<script class="testbody" type="text/javascript"> + +const signalLength = 2048; +const responseLength = 1000; +const outputLength = 2048; // < signalLength + responseLength to test bug 910171 + +var gTest = { + length: outputLength, + numberOfChannels: 2, + createGraph(context) { + var buffer = context.createBuffer(2, signalLength, context.sampleRate); + for (var i = 0; i < signalLength; ++i) { + var sample = Math.sin(440 * 2 * Math.PI * i / context.sampleRate); + // When mixed into a single channel, this produces silence + buffer.getChannelData(0)[i] = sample; + buffer.getChannelData(1)[i] = -sample; + } + + var response = context.createBuffer(2, responseLength, context.sampleRate); + for (var i = 0; i < responseLength; ++i) { + response.getChannelData(0)[i] = i / responseLength; + response.getChannelData(1)[i] = 1 - (i / responseLength); + } + + var convolver = context.createConvolver(); + convolver.buffer = response; + convolver.channelCount = 1; + + expectException(function() { convolver.channelCount = 3; }, + DOMException.NOT_SUPPORTED_ERR); + convolver.channelCountMode = "explicit"; + expectException(function() { convolver.channelCountMode = "max"; }, + DOMException.NOT_SUPPORTED_ERR); + convolver.channelInterpretation = "discrete"; + convolver.channelInterpretation = "speakers"; + + var source = context.createBufferSource(); + source.buffer = buffer; + source.connect(convolver); + source.start(0); + + return convolver; + }, +}; + +runTest(); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_convolverNodeChannelInterpretationChanges.html b/dom/media/webaudio/test/test_convolverNodeChannelInterpretationChanges.html new file mode 100644 index 0000000000..bede517b2e --- /dev/null +++ b/dom/media/webaudio/test/test_convolverNodeChannelInterpretationChanges.html @@ -0,0 +1,169 @@ +<!DOCTYPE html> +<title>Test up-mixing in ConvolverNode after ChannelInterpretation change</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script> +// This test is not in wpt because it requires that multiple changes to the +// nodes in an AudioContext during a single event will be processed by the +// audio thread in a single transaction. Gecko provides that, but this is not +// currently required by the Web Audio API. + +const EPSILON = Math.pow(2, -23); +// sampleRate is a power of two so that delay times are exact in base-2 +// floating point arithmetic. +const SAMPLE_RATE = 32768; +// Length of initial mono signal in frames, if the test has an initial mono +// signal. This is more than one block to ensure that at least one block +// will be mono, even if interpolation in the DelayNode means that stereo is +// output one block earlier than if frames are delayed without interpolation. +const MONO_FRAMES = 256; +// Length of response buffer. This is greater than 1 to ensure that the +// convolver has stereo output at least one block after stereo input is +// disconnected. +const RESPONSE_FRAMES = 2; + +function test_interpretation_change(t, initialInterpretation, initialMonoFrames) +{ + let context = new AudioContext({sampleRate: SAMPLE_RATE}); + + // Three independent signals. These are constant so that results are + // independent of the timing of the `ended` event. + let monoOffset = 0.25 + let monoSource = new ConstantSourceNode(context, {offset: monoOffset}); + let leftOffset = 0.125; + let rightOffset = 0.5; + let leftSource = new ConstantSourceNode(context, {offset: leftOffset}); + let rightSource = new ConstantSourceNode(context, {offset: rightOffset}); + monoSource.start(); + leftSource.start(); + rightSource.start(); + + let stereoMerger = new ChannelMergerNode(context, {numberOfInputs: 2}); + leftSource.connect(stereoMerger, 0, 0); + rightSource.connect(stereoMerger, 0, 1); + + // The DelayNode initially has a single channel of silence, and so the + // output of the delay node is first mono silence (if there is a non-zero + // initialMonoFrames), then stereo. In Gecko, this triggers a convolver + // configuration that is different for different channelInterpretations. + let delay = + new DelayNode(context, + {maxDelayTime: MONO_FRAMES / context.sampleRate, + delayTime: initialMonoFrames / context.sampleRate}); + stereoMerger.connect(delay); + + // Two convolvers with the same impulse response. The test convolver will + // process a mix of stereo and mono signals. The reference convolver will + // always process stereo, including the up-mixed mono signal. + let response = new AudioBuffer({numberOfChannels: 1, + length: RESPONSE_FRAMES, + sampleRate: context.sampleRate}); + response.getChannelData(0)[response.length - 1] = 1; + + let testConvolver = new ConvolverNode(context, + {disableNormalization: true, + buffer: response}); + testConvolver.channelInterpretation = initialInterpretation; + let referenceConvolver = new ConvolverNode(context, + {disableNormalization: true, + buffer: response}); + // No need to set referenceConvolver.channelInterpretation because + // input is always stereo, due to up-mixing at gain node. + let referenceMixer = new GainNode(context); + referenceMixer.channelCount = 2; + referenceMixer.channelCountMode = "explicit"; + referenceMixer.channelInterpretation = initialInterpretation; + referenceMixer.connect(referenceConvolver); + + delay.connect(testConvolver); + delay.connect(referenceMixer); + + monoSource.connect(testConvolver); + monoSource.connect(referenceMixer); + + // A timer sends 'ended' when the convolvers are known to be processing + // stereo. + let timer = new ConstantSourceNode(context); + timer.start(); + timer.stop((initialMonoFrames + 1) / context.sampleRate); + + timer.onended = t.step_func(() => { + let changedInterpretation = + initialInterpretation == "speakers" ? "discrete" : "speakers"; + + // Switch channelInterpretation in test and reference paths. + testConvolver.channelInterpretation = changedInterpretation; + referenceMixer.channelInterpretation = changedInterpretation; + + // Disconnect the stereo input from both test and reference convolvers. + // The disconnected convolvers will continue to output stereo for at least + // one frame. The test convolver will up-mix its mono input into its two + // buffers. + delay.disconnect(); + + // Capture the outputs in a script processor. + // + // The first two channels contain signal where some up-mixing occurs + // internally to the test convolver. + // + // The last two channels are expected to contain the same signal, but + // up-mixing was performed at a GainNode prior to convolution. + // + // Two stereo splitters will collect test and reference outputs. + let testSplitter = + new ChannelSplitterNode(context, {numberOfOutputs: 2}); + let referenceSplitter = + new ChannelSplitterNode(context, {numberOfOutputs: 2}); + testConvolver.connect(testSplitter); + referenceConvolver.connect(referenceSplitter); + + let outputMerger = new ChannelMergerNode(context, {numberOfInputs: 4}); + testSplitter.connect(outputMerger, 0, 0); + testSplitter.connect(outputMerger, 1, 1); + referenceSplitter.connect(outputMerger, 0, 2); + referenceSplitter.connect(outputMerger, 1, 3); + + let processor = context.createScriptProcessor(256, 4, 0); + outputMerger.connect(processor); + + processor.onaudioprocess = t.step_func_done((e) => { + e.target.onaudioprocess = null; + outputMerger.disconnect(); + + // The test convolver output is stereo for the first block. + let length = 128; + + let buffer = e.inputBuffer; + let maxDiff = -1.0; + let frameIndex = 0; + let channelIndex = 0; + for (let c = 0; c < 2; ++c) { + let testOutput = buffer.getChannelData(0 + c); + let referenceOutput = buffer.getChannelData(2 + c); + for (var i = 0; i < length; ++i) { + var diff = Math.abs(testOutput[i] - referenceOutput[i]); + if (diff > maxDiff) { + maxDiff = diff; + frameIndex = i; + channelIndex = c; + } + } + } + assert_approx_equals(buffer.getChannelData(0 + channelIndex)[frameIndex], + buffer.getChannelData(2 + channelIndex)[frameIndex], + EPSILON, + `output at ${frameIndex} ` + + `in channel ${channelIndex}` ); + }); + }); +} + +async_test((t) => test_interpretation_change(t, "speakers", MONO_FRAMES), + "speakers to discrete, initially mono"); +async_test((t) => test_interpretation_change(t, "discrete", MONO_FRAMES), + "discrete to speakers"); +// Gecko uses a separate path for "speakers" initial up-mixing when the +// convolver's first input is stereo, so test that separately. +async_test((t) => test_interpretation_change(t, "speakers", 0), + "speakers to discrete, initially stereo"); +</script> diff --git a/dom/media/webaudio/test/test_convolverNodeDelay.html b/dom/media/webaudio/test/test_convolverNodeDelay.html new file mode 100644 index 0000000000..2e8caf8027 --- /dev/null +++ b/dom/media/webaudio/test/test_convolverNodeDelay.html @@ -0,0 +1,72 @@ +<!DOCTYPE html> +<title>Test convolution to delay a triangle pulse</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script> +const sampleRate = 48000; +const LENGTH = 12800; +// tolerate 16-bit math. +const EPSILON = 1.0 / Math.pow(2, 15); + +// Triangle pulse +var sourceBuffer = new OfflineAudioContext(1, 1, sampleRate). + createBuffer(1, 2 * 128, sampleRate); +var channelData = sourceBuffer.getChannelData(0); +for (var i = 0; i < 128; ++i) { + channelData[i] = i/128; + channelData[128 + i] = 1.0 - i/128; +} + +function test_delay_index(delayIndex) { + + var context = new OfflineAudioContext(2, LENGTH, sampleRate); + + var merger = context.createChannelMerger(2); + merger.connect(context.destination); + + var impulse = context.createBuffer(1, delayIndex + 1, sampleRate); + impulse.getChannelData(0)[delayIndex] = 1.0; + var convolver = context.createConvolver(); + convolver.normalize = false; + convolver.buffer = impulse; + convolver.connect(merger, 0, 0); + + var delayTime = delayIndex/sampleRate; + var delay = context.createDelay(delayTime || 1/sampleRate); + delay.delayTime.value = delayTime; + delay.connect(merger, 0, 1); + + var source = context.createBufferSource(); + source.buffer = sourceBuffer; + source.connect(convolver); + source.connect(delay); + source.start(0); + + return context.startRendering(). + then((buffer) => { + var convolverOutput = buffer.getChannelData(0); + var delayOutput = buffer.getChannelData(1); + var maxDiff = 0.0; + var maxIndex = 0; + for (var i = 0; i < buffer.length; ++i) { + var diff = Math.abs(convolverOutput[i] - delayOutput[i]); + if (diff > maxDiff) { + maxDiff = diff; + maxIndex = i; + } + } + // The convolver should produce similar output to the delay. + assert_approx_equals(convolverOutput[maxIndex], delayOutput[maxIndex], + EPSILON, "output at " + maxIndex); + }); +} + +// The 5/4 ratio provides sampling across a range of delays and offsets within +// blocks. +for (var delayIndex = 0; + delayIndex < LENGTH; + delayIndex = Math.floor((5 * (delayIndex + 1)) / 4)) { + promise_test(test_delay_index.bind(null, delayIndex), + "Delay " + delayIndex); +} +</script> diff --git a/dom/media/webaudio/test/test_convolverNodeFiniteInfluence.html b/dom/media/webaudio/test/test_convolverNodeFiniteInfluence.html new file mode 100644 index 0000000000..1cfb51ce8a --- /dev/null +++ b/dom/media/webaudio/test/test_convolverNodeFiniteInfluence.html @@ -0,0 +1,44 @@ +<!DOCTYPE html> +<title>Test convolution effect has finite duration</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script> +promise_test(function() { + + const responseLength = 256; + // Accept an influence period of twice the responseLength to accept FFT + // implementations. + const tolerancePeriod = 2 * responseLength; + const totalSize = tolerancePeriod + responseLength; + + var context = new OfflineAudioContext(1, totalSize, 48000); + + var responseBuffer = + context.createBuffer(1, responseLength, context.sampleRate); + var responseChannelData = responseBuffer.getChannelData(0); + responseChannelData[0] = 1; + responseChannelData[responseLength - 1] = 1; + var convolver = context.createConvolver(); + convolver.buffer = responseBuffer; + convolver.connect(context.destination); + + var sourceBuffer = context.createBuffer(1, totalSize, context.sampleRate); + sourceBuffer.getChannelData(0)[0] = NaN; + var source = context.createBufferSource(); + source.buffer = sourceBuffer; + source.connect(convolver); + source.start(); + + return context.startRendering(). + then((buffer) => { + var convolverOutput = buffer.getChannelData(0); + // There should be no non-zeros after the tolerance period. + var testIndex = tolerancePeriod; + for (; + testIndex < buffer.length - 1 && convolverOutput[testIndex] == 0; + ++testIndex) { + } + assert_equals(convolverOutput[testIndex], 0, "output at " + testIndex); + }); +}); +</script> diff --git a/dom/media/webaudio/test/test_convolverNodeNormalization.html b/dom/media/webaudio/test/test_convolverNodeNormalization.html new file mode 100644 index 0000000000..24cb7d1670 --- /dev/null +++ b/dom/media/webaudio/test/test_convolverNodeNormalization.html @@ -0,0 +1,83 @@ +<!DOCTYPE html> +<title>Test normalization of convolution buffers</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script> +// Constants from +// https://www.w3.org/TR/2015/WD-webaudio-20151208/#widl-ConvolverNode-normalize +const GainCalibration = 0.00125; +const GainCalibrationSampleRate = 44100; + +const sampleRate = GainCalibrationSampleRate; +const LENGTH = 12800; +// tolerate 16-bit math. +const EPSILON = 1.0 / Math.pow(2, 15); + +function test_normalization_via_response_concat(delayIndex) +{ + var context = new OfflineAudioContext(1, LENGTH, sampleRate); + + var impulse = context.createBuffer(1, 1, sampleRate); + impulse.getChannelData(0)[0] = 1.0; + var source = context.createBufferSource(); + source.buffer = impulse; + source.start(0); + + // Construct a set of adjacent responses in such a way that, when each is + // convolved with the impulse, they can be merged to produce a constant. + + // The 5/4 ratio provides a range of lengths with different offsets within + // blocks. + var i = 0; + for (var responseEnd = 1; + i < LENGTH; + responseEnd = Math.floor((5 * responseEnd) / 4) + 1) { + var responseBuffer = context.createBuffer(1, responseEnd, sampleRate); + var response = responseBuffer.getChannelData(0); + var responseStart = i; + // The values in the response should be normalized, and so the output + // should not be dependent on the value. Pick a changing value to test + // this. + var value = responseStart + 1; + for (; i < responseEnd; ++i) { + response[i] = value; + } + var convolver = context.createConvolver(); + convolver.normalize = true; + convolver.buffer = responseBuffer; + convolver.connect(context.destination); + // Undo the normalization calibration by scaling the impulse so as to + // expect unit pulse output from the convolver. + var gain = context.createGain(); + gain.gain.value = + Math.sqrt((responseEnd - responseStart) / responseEnd) / GainCalibration; + gain.connect(convolver); + source.connect(gain); + } + + return context.startRendering(). + then((buffer) => { + var output = buffer.getChannelData(0); + var max = output[0]; + var maxIndex = 0; + var min = max; + var minIndex = 0; + for (var i = 1; i < buffer.length; ++i) { + if (output[i] > max) { + max = output[i]; + maxIndex = i; + } else if (output[i] < min) { + min = output[i]; + minIndex = i; + } + } + assert_approx_equals(output[maxIndex], 1.0, EPSILON, + "max output at " + maxIndex); + assert_approx_equals(output[minIndex], 1.0, EPSILON, + "min output at " + minIndex); + }); +} + +promise_test(test_normalization_via_response_concat, + "via response concatenation"); +</script> diff --git a/dom/media/webaudio/test/test_convolverNodeOOM.html b/dom/media/webaudio/test/test_convolverNodeOOM.html new file mode 100644 index 0000000000..80260267a8 --- /dev/null +++ b/dom/media/webaudio/test/test_convolverNodeOOM.html @@ -0,0 +1,46 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test ConvolverNode with very large buffer that triggers an OOM</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +var gTest = { + length: 2048, + numberOfChannels: 1, + skipOfflineContextTests: true, + createGraph(context) { + var source = context.createOscillator(); + var convolver = context.createConvolver(); + // Very big buffer that results in an OOM + try { + var buffer = context.createBuffer(2, 300000000, context.sampleRate) + buffer.getChannelData(0); + } catch(e) { + // OOM when attempting to create the buffer, this can happen on 32bits + // OSes. Simply return here. + return convolver; + } + source.connect(convolver); + try { + convolver.buffer = buffer; + } catch (e) { + // This can also OOM. + return convolver; + } + source.start(); + return convolver; + } +}; + +runTest(); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_convolverNodePassThrough.html b/dom/media/webaudio/test/test_convolverNodePassThrough.html new file mode 100644 index 0000000000..54682aee0c --- /dev/null +++ b/dom/media/webaudio/test/test_convolverNodePassThrough.html @@ -0,0 +1,48 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test ConvolverNode with passthrough</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +var gTest = { + length: 2048, + numberOfChannels: 1, + createGraph(context) { + var source = context.createBufferSource(); + + var convolver = context.createConvolver(); + + source.buffer = this.buffer; + convolver.buffer = this.buffer; + + source.connect(convolver); + + var convolverWrapped = SpecialPowers.wrap(convolver); + ok("passThrough" in convolverWrapped, "ConvolverNode should support the passThrough API"); + convolverWrapped.passThrough = true; + + source.start(0); + return convolver; + }, + createExpectedBuffers(context) { + this.buffer = context.createBuffer(1, 2048, context.sampleRate); + for (var i = 0; i < 2048; ++i) { + this.buffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate); + } + + return [this.buffer]; + }, +}; + +runTest(); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_convolverNodeWithGain.html b/dom/media/webaudio/test/test_convolverNodeWithGain.html new file mode 100644 index 0000000000..0762f16329 --- /dev/null +++ b/dom/media/webaudio/test/test_convolverNodeWithGain.html @@ -0,0 +1,62 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test ConvolverNode after a GainNode - Bug 891254 </title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script src="webaudio.js" type="text/javascript"></script> +<script class="testbody" type="text/javascript"> + +const signalLength = 2048; +const responseLength = 100; +const outputLength = 4096; // > signalLength + responseLength + +var gTest = { + length: outputLength, + numberOfChannels: 1, + createGraph(context) { + var buffer = context.createBuffer(1, signalLength, context.sampleRate); + for (var i = 0; i < signalLength; ++i) { + buffer.getChannelData(0)[i] = Math.sin(2 * Math.PI * i / signalLength); + } + + var source = context.createBufferSource(); + source.buffer = buffer; + source.start(0); + + var response = context.createBuffer(1, responseLength, context.sampleRate); + for (var i = 0; i < responseLength; ++i) { + response.getChannelData(0)[i] = i / responseLength; + } + + var gain = context.createGain(); + gain.gain.value = -1; + source.connect(gain); + + var convolver1 = context.createConvolver(); + convolver1.buffer = response; + gain.connect(convolver1); + + var convolver2 = context.createConvolver(); + convolver2.buffer = response; + source.connect(convolver2); + + // The output of convolver1 should be the inverse of convolver2, so blend + // them together and expect silence. + var blend = context.createGain(); + convolver1.connect(blend); + convolver2.connect(blend); + + return blend; + }, +}; + +runTest(); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_convolverNode_mono_mono.html b/dom/media/webaudio/test/test_convolverNode_mono_mono.html new file mode 100644 index 0000000000..f3bac8d5c3 --- /dev/null +++ b/dom/media/webaudio/test/test_convolverNode_mono_mono.html @@ -0,0 +1,72 @@ +<!DOCTYPE html> + +<html> +<head> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script type="text/javascript" src="webaudio.js"></script> +<script type="text/javascript" src="layouttest-glue.js"></script> +<script type="text/javascript" src="blink/audio-testing.js"></script> +<script type="text/javascript" src="blink/convolution-testing.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> + +<body> + +<div id="description"></div> +<div id="console"></div> + +<script> +description("Tests ConvolverNode processing a mono channel with mono impulse response."); +SimpleTest.waitForExplicitFinish(); + +// To test the convolver, we convolve two square pulses together to +// produce a triangular pulse. To verify the result is correct we +// check several parts of the result. First, we make sure the initial +// part of the result is zero (due to the latency in the convolver). +// Next, the triangular pulse should match the theoretical result to +// within some roundoff. After the triangular pulse, the result +// should be exactly zero, but round-off prevents that. We make sure +// the part after the pulse is sufficiently close to zero. Finally, +// the result should be exactly zero because the inputs are exactly +// zero. +function runTest() { + if (window.testRunner) { + window.testRunner.dumpAsText(); + window.testRunner.waitUntilDone(); + } + + window.jsTestIsAsync = true; + + // Create offline audio context. + var context = new OfflineAudioContext(2, sampleRate * renderLengthSeconds, sampleRate); + + var squarePulse = createSquarePulseBuffer(context, pulseLengthFrames); + var trianglePulse = createTrianglePulseBuffer(context, 2 * pulseLengthFrames); + + var bufferSource = context.createBufferSource(); + bufferSource.buffer = squarePulse; + + var convolver = context.createConvolver(); + convolver.normalize = false; + convolver.buffer = squarePulse; + + bufferSource.connect(convolver); + convolver.connect(context.destination); + + bufferSource.start(0); + + context.oncomplete = checkConvolvedResult(trianglePulse); + context.startRendering(); +} + +function finishJSTest() { + SimpleTest.finish(); +} + +runTest(); + +</script> + +<script src="../fast/js/resources/js-test-post.js"></script> +</body> +</html> diff --git a/dom/media/webaudio/test/test_currentTime.html b/dom/media/webaudio/test/test_currentTime.html new file mode 100644 index 0000000000..66fdf42653 --- /dev/null +++ b/dom/media/webaudio/test/test_currentTime.html @@ -0,0 +1,27 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test AudioContext.currentTime</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestFlakyTimeout("This test needs to wait a while for the AudioContext's timer to start."); +addLoadEvent(function() { + var ac = new AudioContext(); + is(ac.currentTime, 0, "AudioContext.currentTime should be 0 initially"); + ac.onstatechange = function () { + ok(ac.state == "running", "AudioContext.currentTime should eventually start"); + ok(ac.currentTime > 0, "AudioContext.currentTime should have increased by now"); + SimpleTest.finish(); + } +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_decodeAudioDataOnDetachedBuffer.html b/dom/media/webaudio/test/test_decodeAudioDataOnDetachedBuffer.html new file mode 100644 index 0000000000..e7c6d2db0c --- /dev/null +++ b/dom/media/webaudio/test/test_decodeAudioDataOnDetachedBuffer.html @@ -0,0 +1,50 @@ +<!DOCTYPE HTML> +<html> + <meta charset=utf-8> +<head> + <title>Bug 1308434 - Test DecodeAudioData on detached buffer</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script type="text/javascript"> +var testDecodeAudioDataOnDetachedBuffer = function(buffer) { + var context = new AudioContext(); + + // make the buffer detached + context.decodeAudioData(buffer); + + // check that the buffer is detached + is(buffer.byteLength, 0, "Buffer should be detached"); + + // call decodeAudioData on detached buffer + context.decodeAudioData(buffer).then(function(b) { + ok(false, "We should not be able to decode the detached buffer but we do"); + SimpleTest.finish(); + }, function(r) { + SimpleTest.isa(r, TypeError); + is(r.message, "BaseAudioContext.decodeAudioData: Buffer argument can't be a detached buffer", "Incorrect message"); + SimpleTest.finish(); + }); +}; + +var filename = "small-shot.mp3"; + +SimpleTest.waitForExplicitFinish(); + +addLoadEvent(function() { + var xhr = new XMLHttpRequest(); + xhr.open("GET", filename); + xhr.responseType = "arraybuffer"; + + xhr.onload = function() { + testDecodeAudioDataOnDetachedBuffer(xhr.response); + }; + + xhr.send(); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_decodeAudioDataPromise.html b/dom/media/webaudio/test/test_decodeAudioDataPromise.html new file mode 100644 index 0000000000..139a686db1 --- /dev/null +++ b/dom/media/webaudio/test/test_decodeAudioDataPromise.html @@ -0,0 +1,62 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test the decodeAudioData API with Promise</title> + + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <script src="webaudio.js"></script> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + +var finished = 0; + +function finish() { + if (++finished == 2) { + SimpleTest.finish(); + } +} + +var ac = new AudioContext(); +// Test that a the promise is rejected with an invalid source buffer. +expectNoException(function() { + var p = ac.decodeAudioData(" "); + ok(p instanceof Promise, "AudioContext.decodeAudioData should return a Promise"); + p.then(function(data) { + ok(false, "Promise should not resolve with an invalid source buffer."); + finish(); + }).catch(function(e) { + ok(true, "Promise should be rejected with an invalid source buffer."); + ok(e.name == "TypeError", "The error should be TypeError"); + finish(); + }) +}); + +// Test that a the promise is resolved with a valid source buffer. +var xhr = new XMLHttpRequest(); +xhr.open("GET", "ting-44.1k-1ch.ogg", true); +xhr.responseType = "arraybuffer"; +xhr.onload = function() { + var p = ac.decodeAudioData(xhr.response); + ok(p instanceof Promise, "AudioContext.decodeAudioData should return a Promise"); + p.then(function(data) { + ok(data instanceof AudioBuffer, "Promise should resolve, passing an AudioBuffer"); + ok(true, "Promise should resolve with a valid source buffer."); + finish(); + }).catch(function() { + ok(false, "Promise should not be rejected with a valid source buffer."); + finish(); + }); +}; +xhr.send(); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_decodeAudioError.html b/dom/media/webaudio/test/test_decodeAudioError.html new file mode 100644 index 0000000000..f18b971ac4 --- /dev/null +++ b/dom/media/webaudio/test/test_decodeAudioError.html @@ -0,0 +1,74 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test the decodeAudioData Errors</title> + + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <script src="webaudio.js"></script> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + +var finished = 0; + +var ctx = new AudioContext(); + +function errorExpectedWithFile(file, errorMsg) { + var xhr = new XMLHttpRequest(); + function test(e) { + ok(e instanceof DOMException, + "The exception should be an instance of DOMException"); + ok(e.name == "EncodingError", + "The exception name should be EncodingError"); + ok(e.message == errorMsg, + "The exception message is not the one intended.\n" + + "\tExpected : " + errorMsg + "\n" + + "\tGot : " + e.message ); + finish(); + } + xhr.open("GET", file, true); + xhr.responseType = "arraybuffer"; + xhr.onload = function() { + ctx.decodeAudioData(xhr.response, buffer => { + ok(false, "You should not be able to decode that"); + finish(); + }, e => test(e)) + .then(buffer => { + ok(false, "You should not be able to decode that"); + finish(); + }) + .catch(e => test(e)); + }; + xhr.send(); +} + +function finish() { + if (++finished == 4) { + SimpleTest.finish(); + } +} + +// Unknown Content +errorExpectedWithFile("404", "The buffer passed to decodeAudioData contains an unknown content type."); + +// Invalid Content +errorExpectedWithFile("invalidContent.flac", "The buffer passed to decodeAudioData contains invalid content which cannot be decoded successfully."); + +// No Audio +// # Bug 1656032 +// Think about increasing the finish counter to 6 when activating this line +// errorExpectedWithFile("noaudio.webm", "The buffer passed to decodeAudioData does not contain any audio."); + +// Unknown Error +// errorExpectedWithFile("There is no file we can't handle", "An unknown error occurred while processing decodeAudioData."); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_decodeMultichannel.html b/dom/media/webaudio/test/test_decodeMultichannel.html new file mode 100644 index 0000000000..a799c641ee --- /dev/null +++ b/dom/media/webaudio/test/test_decodeMultichannel.html @@ -0,0 +1,75 @@ +<!DOCTYPE HTML> +<html> +<meta charset=utf-8> +<head> + <title>Test that we can decode multichannel file with webaudio and <audio></title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> +var testcases = [ + { + filename: "audio-quad.wav", + channels: 4 + }, + { + filename: "8kHz-320kbps-6ch.aac", + channels: 6 + } +]; + +SimpleTest.waitForExplicitFinish(); + +function decodeUsingAudioElement(filename, resolve) { + var a = new Audio(); + a.addEventListener("error", function() { + ok(false, "Error loading metadata"); + resolve(); + }); + a.addEventListener("loadedmetadata", function() { + ok(true, "Metadata Loaded"); + resolve(); + }); + + a.src = filename; + a.load(); +} + +function testOne({filename, channels}) { + return new Promise(resolve => { + var xhr = new XMLHttpRequest(); + xhr.open("GET", filename); + xhr.responseType = "arraybuffer"; + xhr.onload = function() { + var context = new AudioContext(); + context.decodeAudioData(xhr.response, function(b) { + ok(true, "Decoding of a wave file with four channels succeded."); + is(b.numberOfChannels, + channels, + `The AudioBuffer decoded from ${filename} should have ${channels} channels.`); + decodeUsingAudioElement(filename, resolve); + }, function() { + ok(false, `Decoding ${filename} failed)`); + decodeUsingAudioElement(filename, resolve); + }); + }; + xhr.send(null); + }); +} + +async function runTest() { + for (var testcase of testcases) { + await testOne(testcase); + } + + SimpleTest.finish(); +} + +addLoadEvent(runTest); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_decodeOpusTail.html b/dom/media/webaudio/test/test_decodeOpusTail.html new file mode 100644 index 0000000000..451b2b6a23 --- /dev/null +++ b/dom/media/webaudio/test/test_decodeOpusTail.html @@ -0,0 +1,28 @@ +<!DOCTYPE HTML> +<html> +<meta charset="utf-8"> +<head> + <title>Regression test to check that opus files don't have a tail at the end.</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> +SimpleTest.waitForExplicitFinish(); + +// This gets a 1 second Opus file and decodes it to a buffer. The opus file is +// decoded at 48kHz, and the OfflineAudioContext is also at 48kHz, no resampling +// is taking place. +fetch('sweep-300-330-1sec.opus') +.then(function(response) { return response.arrayBuffer(); }) +.then(function(buffer) { + var off = new OfflineAudioContext(1, 128, 48000); + off.decodeAudioData(buffer, function(decoded) { + var pcm = decoded.getChannelData(0); + is(pcm.length, 48000, "The length of the decoded file is correct."); + SimpleTest.finish(); + }); +}); + +</script> diff --git a/dom/media/webaudio/test/test_decoderDelay.html b/dom/media/webaudio/test/test_decoderDelay.html new file mode 100644 index 0000000000..4ed379dec5 --- /dev/null +++ b/dom/media/webaudio/test/test_decoderDelay.html @@ -0,0 +1,154 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8" /> + <title>Test that decoder delay is handled</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + <script class="testbody" type="text/javascript"> + SimpleTest.waitForExplicitFinish(); + const {AppConstants} = SpecialPowers.ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" + ); + + var tests_half_a_second = [ + "half-a-second-1ch-44100-aac.mp4", + "half-a-second-1ch-44100-flac.flac", + "half-a-second-1ch-44100-libmp3lame.mp3", + "half-a-second-1ch-44100-libopus.opus", + "half-a-second-1ch-44100-libopus.webm", + "half-a-second-1ch-44100-libvorbis.ogg", + "half-a-second-1ch-44100.wav", + "half-a-second-1ch-48000-aac.mp4", + "half-a-second-1ch-48000-flac.flac", + "half-a-second-1ch-48000-libmp3lame.mp3", + "half-a-second-1ch-48000-libopus.opus", + "half-a-second-1ch-48000-libopus.webm", + "half-a-second-1ch-48000-libvorbis.ogg", + "half-a-second-1ch-48000.wav", + "half-a-second-2ch-44100-aac.mp4", + "half-a-second-2ch-44100-flac.flac", + "half-a-second-2ch-44100-libmp3lame.mp3", + "half-a-second-2ch-44100-libopus.opus", + "half-a-second-2ch-44100-libopus.webm", + "half-a-second-2ch-44100-libvorbis.ogg", + "half-a-second-2ch-44100.wav", + "half-a-second-2ch-48000-aac.mp4", + "half-a-second-2ch-48000-flac.flac", + "half-a-second-2ch-48000-libmp3lame.mp3", + "half-a-second-2ch-48000-libopus.opus", + "half-a-second-2ch-48000-libopus.webm", + "half-a-second-2ch-48000-libvorbis.ogg", + "half-a-second-2ch-48000.wav", + ]; + + // Those files are almost exactly half a second, but don't have enough pre-roll/padding + // information in the container, or the container isn't parsed properly, so + // aren't trimmed appropriately. + // vorbis webm, opus mp4, aac adts + var tests_adts = [ + "half-a-second-1ch-44100-aac.aac", + "half-a-second-1ch-44100-libopus.mp4", + "half-a-second-1ch-44100-libvorbis.webm", + "half-a-second-1ch-48000-aac.aac", + "half-a-second-1ch-48000-libopus.mp4", + "half-a-second-1ch-48000-libvorbis.webm", + "half-a-second-2ch-44100-aac.aac", + "half-a-second-2ch-44100-libopus.mp4", + "half-a-second-2ch-44100-libvorbis.webm", + "half-a-second-2ch-48000-aac.aac", + "half-a-second-2ch-48000-libopus.mp4", + "half-a-second-2ch-48000-libvorbis.webm", + ]; + + // Other files that have interesting characteristics. + var tests_others = [ + { + // Very short VBR file, 16 frames of audio at 44100. Padding spanning two + // packets. + "path": "sixteen-frames.mp3", + "frameCount": 16, + "samplerate": 44100, + "fuzz": {} + }, + { + // This is incorrect (the duration should be 0.5s exactly) + // This is tracked in https://github.com/mozilla/mp4parse-rust/issues/404 + "path":"half-a-second-1ch-44100-aac-afconvert.mp4", + "frameCount": 22464, + "samplerate": 44100, + "fuzz": { + "android": 2 + } + }, + { + // Bug 1856145 - Invalid OGG file with busted granulepos in the + // bytestrem, but we should be able to recover and play it properly. + // This triggers a bug in the decoder delay trimming logic. + "path": "1856145.ogg", + "samplerate": 8000, + "frameCount": 8192, + "fuzz" : {} + } + ]; + + var all_tests = [tests_half_a_second, tests_adts, tests_others].flat(); + + var count = 0; + function checkDone() { + if (++count == all_tests.length) { + SimpleTest.finish(); + } + } + + async function doit() { + var context = new OfflineAudioContext(1, 128, 48000); + tests_half_a_second.forEach(async testfile => { + var response = await fetch(testfile); + var buffer = await response.arrayBuffer(); + var decoded = await context.decodeAudioData(buffer); + is( + decoded.duration, + 0.5, + "The file " + testfile + " is half a second." + ); + // Value found empirically after looking at the files. The initial + // amplitude should be 0 at phase 0 because those files are sine wave. + // The compression is sometimes lossy and the first sample is not always + // exactly 0.0. + ok( + Math.abs(decoded.getChannelData(0)[0]) <= 0.022, + `The start point for ${testfile} is correct ${ decoded.getChannelData(0)[0] }` + ); + checkDone(); + }); + tests_adts.forEach(async testfile => { + var response = await fetch(testfile); + var buffer = await response.arrayBuffer(); + var decoded = await context.decodeAudioData(buffer); + // Value found empirically after looking at the files. ADTS containers + // don't have encoder delay / padding info so we can't trim correctly. + ok( + Math.abs(decoded.duration - 0.5) < 0.02, + `The ADTS file ${testfile} is about half a second (${decoded.duration}, error: ${Math.abs(decoded.duration-0.5)}).` + ); + checkDone(); + }); + tests_others.forEach(async test => { + // Get an context at a specific rate to avoid duration changes due to resampling. + var contextAtRate = new OfflineAudioContext(1, 128, test.samplerate); + var response = await fetch(test.path); + var buffer = await response.arrayBuffer(); + var decoded = await contextAtRate.decodeAudioData(buffer); + const fuzz = test.fuzz[AppConstants.platform] ?? 0; + ok(Math.abs(decoded.length - test.frameCount) <= fuzz, `${test.path} is ${decoded.length} frames long`); + checkDone(); + }); + } + + doit(); + </script> +</body> +</html> diff --git a/dom/media/webaudio/test/test_delayNode.html b/dom/media/webaudio/test/test_delayNode.html new file mode 100644 index 0000000000..89172aa86f --- /dev/null +++ b/dom/media/webaudio/test/test_delayNode.html @@ -0,0 +1,101 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test DelayNode</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script src="webaudio.js" type="text/javascript"></script> +<script class="testbody" type="text/javascript"> + +var gTest = { + length: 4096, + numberOfChannels: 1, + createGraph(context) { + var delay = new DelayNode(context); + ok(delay.delayTime, "The audioparam member must exist"); + is(delay.delayTime.value, 0, "Correct initial value"); + is(delay.delayTime.defaultValue, 0, "Correct default value"); + delay.delayTime.value = 0.5; + is(delay.delayTime.value, 0.5, "Correct initial value"); + is(delay.delayTime.defaultValue, 0, "Correct default value"); + + delay = new DelayNode(context, { delayTime: 0.5 }); + ok(delay.delayTime, "The audioparam member must exist"); + is(delay.delayTime.value, 0.5, "Correct initial value"); + is(delay.delayTime.defaultValue, 0, "Correct default value"); + + var buffer = context.createBuffer(1, 2048, context.sampleRate); + for (var i = 0; i < 2048; ++i) { + buffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate); + } + + var source = context.createBufferSource(); + + delay = context.createDelay(); + + source.buffer = buffer; + + source.connect(delay); + + ok(delay.delayTime, "The audioparam member must exist"); + is(delay.delayTime.value, 0, "Correct initial value"); + is(delay.delayTime.defaultValue, 0, "Correct default value"); + delay.delayTime.value = 0.5; + is(delay.delayTime.value, 0.5, "Correct initial value"); + is(delay.delayTime.defaultValue, 0, "Correct default value"); + is(delay.channelCount, 2, "delay node has 2 input channels by default"); + is(delay.channelCountMode, "max", "Correct channelCountMode for the delay node"); + is(delay.channelInterpretation, "speakers", "Correct channelCountInterpretation for the delay node"); + + expectException(function() { + context.createDelay(0); + }, DOMException.NOT_SUPPORTED_ERR); + expectException(function() { + context.createDelay(180); + }, DOMException.NOT_SUPPORTED_ERR); + expectTypeError(function() { + context.createDelay(NaN); + }, DOMException.NOT_SUPPORTED_ERR); + expectException(function() { + context.createDelay(-1); + }, DOMException.NOT_SUPPORTED_ERR); + + expectException(function() { + new DelayNode(context, { maxDelayTime: 0 }); + }, DOMException.NOT_SUPPORTED_ERR); + expectException(function() { + new DelayNode(context, { maxDelayTime: 180 }); + }, DOMException.NOT_SUPPORTED_ERR); + expectTypeError(function() { + new DelayNode(context, { maxDelayTime: NaN }); + }, DOMException.NOT_SUPPORTED_ERR); + expectException(function() { + new DelayNode(context, { maxDelayTime: -1 }); + }, DOMException.NOT_SUPPORTED_ERR); + + context.createDelay(1); // should not throw + + // Delay the source stream by 2048 frames + delay.delayTime.value = 2048 / context.sampleRate; + + source.start(0); + return delay; + }, + createExpectedBuffers(context) { + var expectedBuffer = context.createBuffer(1, 2048 * 2, context.sampleRate); + for (var i = 2048; i < 2048 * 2; ++i) { + expectedBuffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * (i - 2048) / context.sampleRate); + } + return expectedBuffer; + }, +}; + +runTest(); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_delayNodeAtMax.html b/dom/media/webaudio/test/test_delayNodeAtMax.html new file mode 100644 index 0000000000..3d0afba0ac --- /dev/null +++ b/dom/media/webaudio/test/test_delayNodeAtMax.html @@ -0,0 +1,53 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test DelayNode with maxDelayTime delay - bug 890528</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script src="webaudio.js" type="text/javascript"></script> +<script class="testbody" type="text/javascript"> + +const signalLength = 2048; +const delayLength = 1000; // Not on a block boundary +const outputLength = 4096 // > signalLength + 2 * delayLength; + +function applySignal(buffer, offset) { + for (var i = 0; i < signalLength; ++i) { + buffer.getChannelData(0)[offset + i] = Math.cos(Math.PI * i / signalLength); + } +} + +var gTest = { + numberOfChannels: 1, + createGraph(context) { + var buffer = context.createBuffer(1, signalLength, context.sampleRate); + applySignal(buffer, 0); + + var source = context.createBufferSource(); + source.buffer = buffer; + + const delayTime = delayLength / context.sampleRate; + var delay = context.createDelay(delayTime); + delay.delayTime.value = delayTime; + + source.connect(delay); + + source.start(0); + return delay; + }, + createExpectedBuffers(context) { + var expectedBuffer = context.createBuffer(1, outputLength, context.sampleRate); + applySignal(expectedBuffer, delayLength); + return expectedBuffer; + }, +}; + +runTest(); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_delayNodeChannelChanges.html b/dom/media/webaudio/test/test_delayNodeChannelChanges.html new file mode 100644 index 0000000000..c58d81f467 --- /dev/null +++ b/dom/media/webaudio/test/test_delayNodeChannelChanges.html @@ -0,0 +1,98 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>test DelayNode channel count changes</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestCompleteLog(); + +const bufferSize = 4096; + +var ctx; +var testDelay; +var stereoDelay; +var invertor; + +function compareOutputs(callback) { + var processor = ctx.createScriptProcessor(bufferSize, 2, 0); + testDelay.connect(processor); + invertor.connect(processor); + processor.onaudioprocess = + function(e) { + compareBuffers(e.inputBuffer, + ctx.createBuffer(2, bufferSize, ctx.sampleRate)); + e.target.onaudioprocess = null; + callback(); + } +} + +function startTest() { + // And a two-channel signal + var merger = ctx.createChannelMerger(); + merger.connect(testDelay); + merger.connect(stereoDelay); + var oscL = ctx.createOscillator(); + oscL.connect(merger, 0, 0); + oscL.start(0); + var oscR = ctx.createOscillator(); + oscR.type = "sawtooth"; + oscR.connect(merger, 0, 1); + oscR.start(0); + + compareOutputs( + function () { + // Disconnect the two-channel signal and test again + merger.disconnect(); + compareOutputs(SimpleTest.finish); + }); +} + +function prepareTest() { + ctx = new AudioContext(); + + // The output of a test delay node with mono and stereo input will be + // compared with that of separate mono and stereo delay nodes. + const delayTime = 0.3 * bufferSize / ctx.sampleRate; + testDelay = ctx.createDelay(delayTime); + testDelay.delayTime.value = delayTime; + let monoDelay = ctx.createDelay(delayTime); + monoDelay.delayTime.value = delayTime; + stereoDelay = ctx.createDelay(delayTime); + stereoDelay.delayTime.value = delayTime; + + // Create a one-channel signal and connect to the delay nodes + var monoOsc = ctx.createOscillator(); + monoOsc.frequency.value = 110; + monoOsc.connect(testDelay); + monoOsc.connect(monoDelay); + monoOsc.start(0); + + // Invert the expected so that mixing with the test will find the difference. + invertor = ctx.createGain(); + invertor.gain.value = -1.0; + monoDelay.connect(invertor); + stereoDelay.connect(invertor); + + // Start the test after the delay nodes have begun processing. + var processor = ctx.createScriptProcessor(bufferSize, 1, 0); + processor.connect(ctx.destination); + + processor.onaudioprocess = + function(e) { + e.target.onaudioprocess = null; + processor.disconnect(); + startTest(); + }; +} +prepareTest(); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_delayNodeCycles.html b/dom/media/webaudio/test/test_delayNodeCycles.html new file mode 100644 index 0000000000..fb4b6c5202 --- /dev/null +++ b/dom/media/webaudio/test/test_delayNodeCycles.html @@ -0,0 +1,156 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test the support of cycles.</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); + +const sampleRate = 48000; +const inputLength = 2048; + +addLoadEvent(function() { + function addSine(b) { + for (var i = 0; i < b.length; i++) { + b[i] += Math.sin(440 * 2 * Math.PI * i / sampleRate); + } + } + + function getSineBuffer(ctx) { + var buffer = ctx.createBuffer(1, inputLength, ctx.sampleRate); + addSine(buffer.getChannelData(0)); + return buffer; + } + + function createAndPlayWithCycleAndDelayNode(ctx, delayFrames) { + var source = ctx.createBufferSource(); + source.buffer = getSineBuffer(ctx); + + var gain = ctx.createGain(); + var delay = ctx.createDelay(); + delay.delayTime.value = delayFrames/ctx.sampleRate; + + source.connect(gain); + gain.connect(delay); + delay.connect(ctx.destination); + // cycle + delay.connect(gain); + + source.start(0); + } + + function createAndPlayWithCycleAndNoDelayNode(ctx) { + var source = ctx.createBufferSource(); + source.loop = true; + source.buffer = getSineBuffer(ctx); + + var gain = ctx.createGain(); + var gain2 = ctx.createGain(); + + source.connect(gain); + gain.connect(gain2); + // cycle + gain2.connect(gain); + gain2.connect(ctx.destination); + + source.start(0); + } + + function createAndPlayWithCycleAndNoDelayNodeInCycle(ctx) { + var source = ctx.createBufferSource(); + source.loop = true; + source.buffer = getSineBuffer(ctx); + + var delay = ctx.createDelay(); + var gain = ctx.createGain(); + var gain2 = ctx.createGain(); + + // Their is a cycle, a delay, but the delay is not in the cycle. + source.connect(delay); + delay.connect(gain); + gain.connect(gain2); + // cycle + gain2.connect(gain); + gain2.connect(ctx.destination); + + source.start(0); + } + + var remainingTests = 0; + function finish() { + if (--remainingTests == 0) { + SimpleTest.finish(); + } + } + + function getOfflineContext(oncomplete) { + var ctx = new OfflineAudioContext(1, sampleRate, sampleRate); + ctx.oncomplete = oncomplete; + return ctx; + } + + function checkSilentBuffer(e) { + var buffer = e.renderedBuffer.getChannelData(0); + for (var i = 0; i < buffer.length; i++) { + if (buffer[i] != 0.0) { + ok(false, "buffer should be silent."); + finish(); + return; + } + } + ok(true, "buffer should be silent."); + finish(); + } + + function checkNoisyBuffer(e, aDelayFrames) { + let delayFrames = Math.max(128, aDelayFrames); + + var expected = new Float32Array(e.renderedBuffer.length); + for (var i = delayFrames; i < expected.length; i += delayFrames) { + addSine(expected.subarray(i, i + inputLength)); + } + + compareChannels(e.renderedBuffer.getChannelData(0), expected); + finish(); + } + + function expectSilentOutput(f) { + remainingTests++; + var ctx = getOfflineContext(checkSilentBuffer); + f(ctx); + ctx.startRendering(); + } + + function expectNoisyOutput(delayFrames) { + remainingTests++; + var ctx = getOfflineContext(); + ctx.oncomplete = function(e) { checkNoisyBuffer(e, delayFrames); }; + createAndPlayWithCycleAndDelayNode(ctx, delayFrames); + ctx.startRendering(); + } + + // This is trying to make a graph with a cycle and no DelayNode in the graph. + // The cycle subgraph should be muted, in this graph the output should be silent. + expectSilentOutput(createAndPlayWithCycleAndNoDelayNode); + // This is trying to make a graph with a cycle and a DelayNode in the graph, but + // not part of the cycle. + // The cycle subgraph should be muted, in this graph the output should be silent. + expectSilentOutput(createAndPlayWithCycleAndNoDelayNodeInCycle); + // Those are making legal graphs, with at least one DelayNode in the cycle. + // There should be some non-silent output. + expectNoisyOutput(sampleRate/4); + // DelayNode.delayTime will be clamped to 128/ctx.sampleRate. + // There should be some non-silent output. + expectNoisyOutput(0); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_delayNodePassThrough.html b/dom/media/webaudio/test/test_delayNodePassThrough.html new file mode 100644 index 0000000000..0c2d1db30a --- /dev/null +++ b/dom/media/webaudio/test/test_delayNodePassThrough.html @@ -0,0 +1,53 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test DelayNode</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script src="webaudio.js" type="text/javascript"></script> +<script class="testbody" type="text/javascript"> + +var gTest = { + length: 4096, + numberOfChannels: 1, + createGraph(context) { + var source = context.createBufferSource(); + + var delay = context.createDelay(); + + source.buffer = this.buffer; + + source.connect(delay); + + delay.delayTime.value = 0.5; + + // Delay the source stream by 2048 frames + delay.delayTime.value = 2048 / context.sampleRate; + + var delayWrapped = SpecialPowers.wrap(delay); + ok("passThrough" in delayWrapped, "DelayNode should support the passThrough API"); + delayWrapped.passThrough = true; + + source.start(0); + return delay; + }, + createExpectedBuffers(context) { + this.buffer = context.createBuffer(1, 2048, context.sampleRate); + for (var i = 0; i < 2048; ++i) { + this.buffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate); + } + var silence = context.createBuffer(1, 2048, context.sampleRate); + + return [this.buffer, silence]; + }, +}; + +runTest(); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_delayNodeSmallMaxDelay.html b/dom/media/webaudio/test/test_delayNodeSmallMaxDelay.html new file mode 100644 index 0000000000..235a55a4c8 --- /dev/null +++ b/dom/media/webaudio/test/test_delayNodeSmallMaxDelay.html @@ -0,0 +1,43 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test DelayNode</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script src="webaudio.js" type="text/javascript"></script> +<script class="testbody" type="text/javascript"> + +var gTest = { + length: 2048, + numberOfChannels: 1, + createGraph(context) { + var source = context.createBufferSource(); + + var delay = context.createDelay(0.02); + + source.buffer = this.buffer; + + source.connect(delay); + + source.start(0); + return delay; + }, + createExpectedBuffers(context) { + var expectedBuffer = context.createBuffer(1, 2048, context.sampleRate); + for (var i = 0; i < 2048; ++i) { + expectedBuffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate); + } + this.buffer = expectedBuffer; + return expectedBuffer; + }, +}; + +runTest(); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_delayNodeTailIncrease.html b/dom/media/webaudio/test/test_delayNodeTailIncrease.html new file mode 100644 index 0000000000..a511a4a3ff --- /dev/null +++ b/dom/media/webaudio/test/test_delayNodeTailIncrease.html @@ -0,0 +1,71 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test increasing delay of DelayNode after input finishes</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); + +const signalLength = 100; +const bufferSize = 1024; +// Delay should be long enough to allow CC to run +const delayBufferCount = 50; +const delayLength = delayBufferCount * bufferSize + 700; + +var count = 0; + +function applySignal(buffer, offset) { + for (var i = 0; i < signalLength; ++i) { + buffer.getChannelData(0)[offset + i] = Math.cos(Math.PI * i / signalLength); + } +} + +function onAudioProcess(e) { + switch(count) { + case 5: + SpecialPowers.forceGC(); + SpecialPowers.forceCC(); + break; + case delayBufferCount: + var offset = delayLength - count * bufferSize; + var ctx = e.target.context; + var expected = ctx.createBuffer(1, bufferSize, ctx.sampleRate); + applySignal(expected, offset); + compareBuffers(e.inputBuffer, expected); + SimpleTest.finish(); + } + count++; +} + +function startTest() { + var ctx = new AudioContext(); + var processor = ctx.createScriptProcessor(bufferSize, 1, 0); + processor.onaudioprocess = onAudioProcess; + + // Switch on delay at a time in the future. + var delayDuration = delayLength / ctx.sampleRate; + var delayStartTime = (delayLength - bufferSize) / ctx.sampleRate; + var delay = ctx.createDelay(delayDuration); + delay.delayTime.setValueAtTime(delayDuration, delayStartTime); + delay.connect(processor); + + // Short signal that finishes before switching to long delay + var buffer = ctx.createBuffer(1, signalLength, ctx.sampleRate); + applySignal(buffer, 0); + var source = ctx.createBufferSource(); + source.buffer = buffer; + source.start(); + source.connect(delay); +}; + +startTest(); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_delayNodeTailWithDisconnect.html b/dom/media/webaudio/test/test_delayNodeTailWithDisconnect.html new file mode 100644 index 0000000000..c6723f643d --- /dev/null +++ b/dom/media/webaudio/test/test_delayNodeTailWithDisconnect.html @@ -0,0 +1,95 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test tail time lifetime of DelayNode after input is disconnected</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +// Web Audio doesn't provide a means to precisely time disconnect()s but we +// can test that the output of delay nodes matches the output from their +// sources before they are disconnected. + +SimpleTest.waitForExplicitFinish(); + +const signalLength = 128; +const bufferSize = 4096; +const sourceCount = bufferSize / signalLength; +// Delay should be long enough to allow CC to run +var delayBufferCount = 20; +const delayLength = delayBufferCount * bufferSize; + +var sourceOutput = new Float32Array(bufferSize); +var delayOutputCount = 0; +var sources = []; + +function onDelayOutput(e) { + if (delayOutputCount < delayBufferCount) { + delayOutputCount++; + return; + } + + compareChannels(e.inputBuffer.getChannelData(0), sourceOutput); + e.target.onaudioprocess = null; + SimpleTest.finish(); +} + +function onSourceOutput(e) { + // Record the first buffer + e.inputBuffer.copyFromChannel(sourceOutput, 0); + e.target.onaudioprocess = null; +} + +function disconnectSources() { + for (var i = 0; i < sourceCount; ++i) { + sources[i].disconnect(); + } + + SpecialPowers.forceGC(); + SpecialPowers.forceCC(); +} + +function startTest() { + var ctx = new AudioContext(); + + var sourceProcessor = ctx.createScriptProcessor(bufferSize, 1, 0); + sourceProcessor.onaudioprocess = onSourceOutput; + // Keep audioprocess events going after source disconnect. + sourceProcessor.connect(ctx.destination); + + var delayProcessor = ctx.createScriptProcessor(bufferSize, 1, 0); + delayProcessor.onaudioprocess = onDelayOutput; + + var delayDuration = delayLength / ctx.sampleRate; + for (var i = 0; i < sourceCount; ++i) { + var delay = ctx.createDelay(delayDuration); + delay.delayTime.value = delayDuration; + delay.connect(delayProcessor); + + var source = ctx.createOscillator(); + source.frequency.value = 440 + 10 * i + source.start(i * signalLength / ctx.sampleRate); + source.stop((i + 1) * signalLength / ctx.sampleRate); + source.connect(delay); + source.connect(sourceProcessor); + + sources[i] = source; + } + + // Assuming the above Web Audio operations have already scheduled an event + // to run in stable state and start the graph thread, schedule a subsequent + // event to disconnect the sources, which will remove main thread connection + // references before it knows the graph thread has started using the source + // streams. + SimpleTest.executeSoon(disconnectSources); +}; + +startTest(); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_delayNodeTailWithGain.html b/dom/media/webaudio/test/test_delayNodeTailWithGain.html new file mode 100644 index 0000000000..60cca276c0 --- /dev/null +++ b/dom/media/webaudio/test/test_delayNodeTailWithGain.html @@ -0,0 +1,72 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test tail time lifetime of DelayNode indirectly connected to source</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); + +const signalLength = 130; +const bufferSize = 1024; +// Delay should be long enough to allow CC to run +const delayBufferCount = 50; +const delayLength = delayBufferCount * bufferSize + 700; + +var count = 0; + +function applySignal(buffer, offset) { + for (var i = 0; i < signalLength; ++i) { + buffer.getChannelData(0)[offset + i] = Math.cos(Math.PI * i / signalLength); + } +} + +function onAudioProcess(e) { + switch(count) { + case 5: + SpecialPowers.forceGC(); + SpecialPowers.forceCC(); + break; + case delayBufferCount: + var offset = delayLength - count * bufferSize; + var ctx = e.target.context; + var expected = ctx.createBuffer(1, bufferSize, ctx.sampleRate); + applySignal(expected, offset); + compareBuffers(e.inputBuffer, expected); + SimpleTest.finish(); + } + count++; +} + +function startTest() { + var ctx = new AudioContext(); + var processor = ctx.createScriptProcessor(bufferSize, 1, 0); + processor.onaudioprocess = onAudioProcess; + + var delayDuration = delayLength / ctx.sampleRate; + var delay = ctx.createDelay(delayDuration); + delay.delayTime.value = delayDuration; + delay.connect(processor); + + var gain = ctx.createGain(); + gain.connect(delay); + + // Short signal that finishes before garbage collection + var buffer = ctx.createBuffer(1, signalLength, ctx.sampleRate); + applySignal(buffer, 0); + var source = ctx.createBufferSource(); + source.buffer = buffer; + source.start(); + source.connect(gain); +}; + +startTest(); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_delayNodeTailWithReconnect.html b/dom/media/webaudio/test/test_delayNodeTailWithReconnect.html new file mode 100644 index 0000000000..da5f02b052 --- /dev/null +++ b/dom/media/webaudio/test/test_delayNodeTailWithReconnect.html @@ -0,0 +1,136 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test tail time lifetime of DelayNode after input finishes and new input added</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); + +// The buffer source will start on a block boundary, so keeping the signal +// within one block ensures that it will not cross AudioProcessingEvent buffer +// boundaries. +const signalLength = 128; +const bufferSize = 1024; +// Delay should be long enough to allow CC to run +var delayBufferCount = 50; +var delayBufferOffset; +const delayLength = delayBufferCount * bufferSize; + +var phase = "initial"; +var sourceCount = 0; +var delayCount = 0; +var oscillator; +var delay; +var source; + +function applySignal(buffer, offset) { + for (var i = 0; i < signalLength; ++i) { + buffer.getChannelData(0)[offset + i] = Math.cos(Math.PI * i / signalLength); + } +} + +function bufferIsSilent(buffer, out) { + for (var i = 0; i < buffer.length; ++i) { + if (buffer.getChannelData(0)[i] != 0) { + if (out) { + out.soundOffset = i; + } + return false; + } + } + return true; +} + +function onDelayOutput(e) { + switch(phase) { + + case "initial": + // Wait for oscillator sound to exit delay + if (bufferIsSilent(e.inputBuffer)) + break; + + phase = "played oscillator"; + break; + + case "played oscillator": + // First tail time has expired. Start second source and remove references + // to the delay and connected second source. + oscillator.disconnect(); + source.connect(delay); + source.start(); + source = null; + delay = null; + phase = "started second source"; + break; + + case "second tail time": + if (delayCount == delayBufferCount) { + var ctx = e.target.context; + var expected = ctx.createBuffer(1, bufferSize, ctx.sampleRate); + applySignal(expected, delayBufferOffset); + compareBuffers(e.inputBuffer, expected); + e.target.onaudioprocess = null; + SimpleTest.finish(); + } + } + + delayCount++; +} + +function onSourceOutput(e) { + switch(phase) { + case "started second source": + var out = {}; + if (!bufferIsSilent(e.inputBuffer, out)) { + delayBufferCount += sourceCount; + delayBufferOffset = out.soundOffset; + phase = "played second source"; + } + break; + case "played second source": + SpecialPowers.forceGC(); + SpecialPowers.forceCC(); + phase = "second tail time"; + e.target.onaudioprocess = null; + } + + sourceCount++; +} + +function startTest() { + var ctx = new AudioContext(); + var delayDuration = delayLength / ctx.sampleRate; + delay = ctx.createDelay(delayDuration); + delay.delayTime.value = delayDuration; + var processor1 = ctx.createScriptProcessor(bufferSize, 1, 0); + delay.connect(processor1); + processor1.onaudioprocess = onDelayOutput; + + // Signal to trigger initial tail time reference + oscillator = ctx.createOscillator(); + oscillator.start(0); + oscillator.stop(100/ctx.sampleRate); + oscillator.connect(delay); + + // Short signal, not started yet, with a ScriptProcessor to detect when it + // starts. It should finish before garbage collection. + var buffer = ctx.createBuffer(1, signalLength, ctx.sampleRate); + applySignal(buffer, 0); + source = ctx.createBufferSource(); + source.buffer = buffer; + var processor2 = ctx.createScriptProcessor(bufferSize, 1, 0); + source.connect(processor2); + processor2.onaudioprocess = onSourceOutput; +}; + +startTest(); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_delayNodeWithGain.html b/dom/media/webaudio/test/test_delayNodeWithGain.html new file mode 100644 index 0000000000..af075c7439 --- /dev/null +++ b/dom/media/webaudio/test/test_delayNodeWithGain.html @@ -0,0 +1,54 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test DelayNode with a GainNode</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script src="webaudio.js" type="text/javascript"></script> +<script class="testbody" type="text/javascript"> + +var gTest = { + length: 4096, + numberOfChannels: 1, + createGraph(context) { + var buffer = context.createBuffer(1, 2048, context.sampleRate); + for (var i = 0; i < 2048; ++i) { + buffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate); + } + + var source = context.createBufferSource(); + + var delay = context.createDelay(); + + source.buffer = buffer; + + var gain = context.createGain(); + gain.gain.value = 0.5; + + source.connect(gain); + gain.connect(delay); + + // Delay the source stream by 2048 frames + delay.delayTime.value = 2048 / context.sampleRate; + + source.start(0); + return delay; + }, + createExpectedBuffers(context) { + var expectedBuffer = context.createBuffer(1, 2048 * 2, context.sampleRate); + for (var i = 2048; i < 2048 * 2; ++i) { + expectedBuffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * (i - 2048) / context.sampleRate) / 2; + } + return expectedBuffer; + }, +}; + +runTest(); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_delaynode-channel-count-1.html b/dom/media/webaudio/test/test_delaynode-channel-count-1.html new file mode 100644 index 0000000000..dd964ef9e3 --- /dev/null +++ b/dom/media/webaudio/test/test_delaynode-channel-count-1.html @@ -0,0 +1,104 @@ +<!DOCTYPE html> +<title>Test that DelayNode output channelCount matches that of the delayed input</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script> +// See https://github.com/WebAudio/web-audio-api/issues/25 + +// sampleRate is a power of two so that delay times are exact in base-2 +// floating point arithmetic. +const SAMPLE_RATE = 32768; +// Arbitrary delay time in frames (but this is assumed a multiple of block +// size below): +const DELAY_FRAMES = 3 * 128; +// Implementations may apply interpolation to input samples, which can spread +// the effect of input with larger channel counts over neighbouring blocks. +// This test ignores enough neighbouring blocks to ignore the effects of +// filter radius of up to this number of frames: +const INTERPOLATION_GRACE = 128; +// Number of frames of DelayNode output that are known to be stereo: +const STEREO_FRAMES = 128; +// The delay will be increased at this frame to switch DelayNode output back +// to mono. +const MONO_OUTPUT_START_FRAME = + DELAY_FRAMES + INTERPOLATION_GRACE + STEREO_FRAMES; +// Number of frames of output that are known to be mono after the known stereo +// and interpolation grace. +const MONO_FRAMES = 128; +// Total length allows for interpolation after effects of stereo input are +// finished and one block to test return to mono output: +const TOTAL_LENGTH = + MONO_OUTPUT_START_FRAME + INTERPOLATION_GRACE + MONO_FRAMES; +// maxDelayTime, is a multiple of block size, because the Gecko implementation +// once had a bug with delayTime = maxDelayTime in this situation: +const MAX_DELAY_FRAMES = TOTAL_LENGTH + INTERPOLATION_GRACE; + +promise_test(() => { + let context = new OfflineAudioContext({numberOfChannels: 1, + length: TOTAL_LENGTH, + sampleRate: SAMPLE_RATE}); + + // Only channel 1 of the splitter is connected to the destination. + let splitter = new ChannelSplitterNode(context, {numberOfOutputs: 2}); + splitter.connect(context.destination, 1); + + // A gain node has channelCountMode "max" and channelInterpretation + // "speakers", and so will up-mix a mono input when there is stereo input. + let gain = new GainNode(context); + gain.connect(splitter); + + // The delay node initially outputs a single channel of silence, when it + // does not have enough signal in its history to output what it has + // previously received. After the delay period, it will then output the + // stereo signal it received. + let delay = + new DelayNode(context, + {maxDelayTime: MAX_DELAY_FRAMES / context.sampleRate, + delayTime: DELAY_FRAMES / context.sampleRate}); + // Schedule an increase in the delay to return to mono silent output from + // the unfilled portion of the DelayNode's buffer. + delay.delayTime.setValueAtTime(MAX_DELAY_FRAMES / context.sampleRate, + MONO_OUTPUT_START_FRAME / context.sampleRate); + delay.connect(gain); + + let stereoMerger = new ChannelMergerNode(context, {numberOfInputs: 2}); + stereoMerger.connect(delay); + + let leftOffset = 0.125; + let rightOffset = 0.5; + let leftSource = new ConstantSourceNode(context, {offset: leftOffset}); + let rightSource = new ConstantSourceNode(context, {offset: rightOffset}); + leftSource.start(); + rightSource.start(); + leftSource.connect(stereoMerger, 0, 0); + rightSource.connect(stereoMerger, 0, 1); + // Connect a mono source directly to the gain, so that even stereo silence + // will be detected in channel 1 of the gain output because it will cause + // the mono source to be up-mixed. + let monoOffset = 0.25 + let monoSource = new ConstantSourceNode(context, {offset: monoOffset}); + monoSource.start(); + monoSource.connect(gain); + + return context.startRendering(). + then((buffer) => { + let output = buffer.getChannelData(0); + + function assert_samples_equal(startIndex, length, expected, description) + { + for (let i = startIndex; i < startIndex + length; ++i) { + assert_equals(output[i], expected, description + ` at ${i}`); + } + } + + assert_samples_equal(0, DELAY_FRAMES - INTERPOLATION_GRACE, + 0, "Initial mono"); + assert_samples_equal(DELAY_FRAMES + INTERPOLATION_GRACE, STEREO_FRAMES, + monoOffset + rightOffset, "Stereo"); + assert_samples_equal(MONO_OUTPUT_START_FRAME + INTERPOLATION_GRACE, + MONO_FRAMES, + 0, "Final mono"); + }); +}); + +</script> diff --git a/dom/media/webaudio/test/test_disconnectAll.html b/dom/media/webaudio/test/test_disconnectAll.html new file mode 100644 index 0000000000..f67b969949 --- /dev/null +++ b/dom/media/webaudio/test/test_disconnectAll.html @@ -0,0 +1,51 @@ +<!DOCTYPE HTML> +<html> + <head> + <title>Test whether we can disconnect an AudioNode</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + </head> + <body> + <pre id="test"> + <script class="testbody" type="text/javascript"> + var gTest = { + length: 256, + numberOfChannels: 1, + createGraph(context) { + var sourceBuffer = context.createBuffer(1, 256, context.sampleRate); + var data = sourceBuffer.getChannelData(0); + for (var j = 0; j < data.length; j++) { + data[j] = 1; + } + + var source = context.createBufferSource(); + source.buffer = sourceBuffer; + + var source = context.createBufferSource(); + source.buffer = sourceBuffer; + + var gain1 = context.createGain(); + var gain2 = context.createGain(); + var gain3 = context.createGain(); + var merger = context.createChannelMerger(3); + + source.connect(gain1); + source.connect(gain2); + source.connect(gain3); + gain1.connect(merger); + gain2.connect(merger); + gain3.connect(merger); + source.start(); + + source.disconnect(); + + return merger; + } + }; + + runTest(); + </script> + </pre> + </body> +</html>
\ No newline at end of file diff --git a/dom/media/webaudio/test/test_disconnectAudioParam.html b/dom/media/webaudio/test/test_disconnectAudioParam.html new file mode 100644 index 0000000000..87df902b8e --- /dev/null +++ b/dom/media/webaudio/test/test_disconnectAudioParam.html @@ -0,0 +1,58 @@ +<!DOCTYPE HTML> +<html> + <head> + <title>Test whether we can disconnect an AudioParam</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + </head> + <body> + <pre id="test"> + <script class="testbody" type="text/javascript"> + var gTest = { + length: 256, + numberOfChannels: 1, + createGraph(context) { + var sourceBuffer = context.createBuffer(1, 256, context.sampleRate); + var data = sourceBuffer.getChannelData(0); + for (var j = 0; j < data.length; j++) { + data[j] = 1; + } + + var source = context.createBufferSource(); + source.buffer = sourceBuffer; + + var half = context.createGain(); + var gain1 = context.createGain(); + var gain2 = context.createGain(); + + half.gain.value = 0.5; + + source.connect(gain1); + gain1.connect(gain2); + source.connect(half); + + half.connect(gain1.gain); + half.connect(gain2.gain); + + half.disconnect(gain2.gain); + + source.start(); + + return gain2; + }, + createExpectedBuffers(context) { + let expectedBuffer = context.createBuffer(1, 256, context.sampleRate); + for (var i = 0; i < 256; ++i) { + expectedBuffer.getChannelData(0)[i] = 1.5; + } + + return expectedBuffer; + } + }; + + runTest(); + </script> + </pre> + </body> +</html> diff --git a/dom/media/webaudio/test/test_disconnectAudioParamFromOutput.html b/dom/media/webaudio/test/test_disconnectAudioParamFromOutput.html new file mode 100644 index 0000000000..533091f920 --- /dev/null +++ b/dom/media/webaudio/test/test_disconnectAudioParamFromOutput.html @@ -0,0 +1,67 @@ +<!DOCTYPE HTML> +<html> + <head> + <title>Test whether we can disconnect an AudioParam</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + </head> + <body> + <pre id="test"> + <script class="testbody" type="text/javascript"> + var gTest = { + length: 256, + numberOfChannels: 2, + createGraph(context) { + var sourceBuffer = context.createBuffer(2, 256, context.sampleRate); + for (var i = 1; i <= 2; i++) { + var data = sourceBuffer.getChannelData(i-1); + for (var j = 0; j < data.length; j++) { + data[j] = i; + } + } + + var source = context.createBufferSource(); + source.buffer = sourceBuffer; + + var source = context.createBufferSource(); + source.buffer = sourceBuffer; + + var half = context.createGain(); + var gain1 = context.createGain(); + var gain2 = context.createGain(); + var splitter = context.createChannelSplitter(2); + + half.gain.value = 0.5; + + source.connect(gain1); + gain1.connect(gain2); + source.connect(half); + half.connect(splitter); + splitter.connect(gain1.gain, 0); + splitter.connect(gain2.gain, 1); + + splitter.disconnect(gain2.gain, 1); + + source.start(); + + return gain2; + }, + createExpectedBuffers(context) { + var expectedBuffer = context.createBuffer(2, 256, context.sampleRate); + for (var i = 1; i <= 2; i++) { + var data = expectedBuffer.getChannelData(i-1); + for (var j = 0; j < data.length; j++) { + data[j] = (i == 1) ? 1.5 : 3.0; + } + } + + return expectedBuffer; + } + }; + + runTest(); + </script> + </pre> + </body> +</html> diff --git a/dom/media/webaudio/test/test_disconnectExceptions.html b/dom/media/webaudio/test/test_disconnectExceptions.html new file mode 100644 index 0000000000..54fde4df8d --- /dev/null +++ b/dom/media/webaudio/test/test_disconnectExceptions.html @@ -0,0 +1,75 @@ +<!DOCTYPE HTML> +<html> + <head> + <title>Test whether we can disconnect an AudioNode</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + </head> + <body> + <pre id="test"> + <script class="testbody" type="text/javascript"> + var ctx = new AudioContext(); + var sourceBuffer = ctx.createBuffer(2, 256, ctx.sampleRate); + for (var i = 1; i <= 2; i++) { + var data = sourceBuffer.getChannelData(i-1); + for (var j = 0; j < data.length; j++) { + data[j] = i; + } + } + + var source = ctx.createBufferSource(); + source.buffer = sourceBuffer; + + var gain1 = ctx.createGain(); + var splitter = ctx.createChannelSplitter(2); + var merger = ctx.createChannelMerger(2); + var gain2 = ctx.createGain(); + var gain3 = ctx.createGain(); + + gain1.connect(splitter); + splitter.connect(gain2, 0); + splitter.connect(gain3, 1); + splitter.connect(merger, 0, 0); + splitter.connect(merger, 1, 1); + gain2.connect(gain3); + gain3.connect(ctx.destination); + merger.connect(ctx.destination); + + expectException(function() { + splitter.disconnect(2); + }, DOMException.INDEX_SIZE_ERR); + + expectNoException(function() { + splitter.disconnect(1); + splitter.disconnect(1); + }); + + expectException(function() { + gain1.disconnect(gain2); + }, DOMException.INVALID_ACCESS_ERR); + + expectException(function() { + gain1.disconnect(gain3); + ok(false, 'Should get InvalidAccessError exception'); + }, DOMException.INVALID_ACCESS_ERR); + + expectException(function() { + splitter.disconnect(gain2, 2); + }, DOMException.INDEX_SIZE_ERR); + + expectException(function() { + splitter.disconnect(gain1, 0); + }, DOMException.INVALID_ACCESS_ERR); + + expectException(function() { + splitter.disconnect(gain3, 0, 0); + }, DOMException.INVALID_ACCESS_ERR); + + expectException(function() { + splitter.disconnect(merger, 3, 0); + }, DOMException.INDEX_SIZE_ERR); + </script> + </pre> + </body> +</html> diff --git a/dom/media/webaudio/test/test_disconnectFromAudioNode.html b/dom/media/webaudio/test/test_disconnectFromAudioNode.html new file mode 100644 index 0000000000..5f80bbfbe8 --- /dev/null +++ b/dom/media/webaudio/test/test_disconnectFromAudioNode.html @@ -0,0 +1,55 @@ +<!DOCTYPE HTML> +<html> + <head> + <title>Test whether we can disconnect an AudioNode</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + </head> + <body> + <pre id="test"> + <script class="testbody" type="text/javascript"> + var gTest = { + length: 256, + numberOfChannels: 1, + createGraph(context) { + var sourceBuffer = context.createBuffer(1, 256, context.sampleRate); + var data = sourceBuffer.getChannelData(0); + for (var j = 0; j < data.length; j++) { + data[j] = 1; + } + + var source = context.createBufferSource(); + source.buffer = sourceBuffer; + + var gain1 = context.createGain(); + var gain2 = context.createGain(); + var gain3 = context.createGain(); + + source.connect(gain1); + source.connect(gain2); + + gain1.connect(gain3); + gain2.connect(gain3); + + source.start(); + + source.disconnect(gain2); + + return gain3; + }, + createExpectedBuffers(context) { + let expectedBuffer = context.createBuffer(1, 256, context.sampleRate); + for (var i = 0; i < 256; ++i) { + expectedBuffer.getChannelData(0)[i] = 1.0; + } + + return expectedBuffer; + } + }; + + runTest(); + </script> + </pre> + </body> +</html> diff --git a/dom/media/webaudio/test/test_disconnectFromAudioNodeAndOutput.html b/dom/media/webaudio/test/test_disconnectFromAudioNodeAndOutput.html new file mode 100644 index 0000000000..a0812bdb09 --- /dev/null +++ b/dom/media/webaudio/test/test_disconnectFromAudioNodeAndOutput.html @@ -0,0 +1,59 @@ +<!DOCTYPE HTML> +<html> + <head> + <title>Test whether we can disconnect an AudioNode</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + </head> + <body> + <pre id="test"> + <script class="testbody" type="text/javascript"> + var gTest = { + length: 256, + numberOfChannels: 2, + createGraph(context) { + var sourceBuffer = context.createBuffer(2, 256, context.sampleRate); + for (var i = 1; i <= 2; i++) { + var data = sourceBuffer.getChannelData(i-1); + for (var j = 0; j < data.length; j++) { + data[j] = i; + } + } + + var source = context.createBufferSource(); + source.buffer = sourceBuffer; + + var splitter = context.createChannelSplitter(2); + var gain1 = context.createGain(); + var gain2 = context.createGain(); + var merger = context.createChannelMerger(2); + + source.connect(splitter); + splitter.connect(gain1, 0); + splitter.connect(gain2, 0); + splitter.connect(gain2, 1); + gain1.connect(merger, 0, 1); + gain2.connect(merger, 0, 1); + source.start(); + + splitter.disconnect(gain2, 0); + + return merger; + }, + createExpectedBuffers(context) { + let expectedBuffer = context.createBuffer(2, 256, context.sampleRate); + for (var i = 0; i < 256; ++i) { + expectedBuffer.getChannelData(0)[i] = 0; + expectedBuffer.getChannelData(1)[i] = 3; + } + + return expectedBuffer; + } + }; + + runTest(); + </script> + </pre> + </body> +</html> diff --git a/dom/media/webaudio/test/test_disconnectFromAudioNodeAndOutputAndInput.html b/dom/media/webaudio/test/test_disconnectFromAudioNodeAndOutputAndInput.html new file mode 100644 index 0000000000..478768c62d --- /dev/null +++ b/dom/media/webaudio/test/test_disconnectFromAudioNodeAndOutputAndInput.html @@ -0,0 +1,57 @@ +<!DOCTYPE HTML> +<html> + <head> + <title>Test whether we can disconnect an AudioNode</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + </head> + <body> + <pre id="test"> + <script class="testbody" type="text/javascript"> + var gTest = { + length: 256, + numberOfChannels: 3, + createGraph(context) { + var sourceBuffer = context.createBuffer(3, 256, context.sampleRate); + for (var i = 1; i <= 3; i++) { + var data = sourceBuffer.getChannelData(i-1); + for (var j = 0; j < data.length; j++) { + data[j] = i; + } + } + + var source = context.createBufferSource(); + source.buffer = sourceBuffer; + + var splitter = context.createChannelSplitter(3); + var merger = context.createChannelMerger(3); + + source.connect(splitter); + splitter.connect(merger, 0, 0); + splitter.connect(merger, 1, 1); + splitter.connect(merger, 2, 2); + source.start(); + + splitter.disconnect(merger, 2, 2); + + return merger; + }, + createExpectedBuffers(context) { + var expectedBuffer = context.createBuffer(3, 256, context.sampleRate); + for (var i = 1; i <= 3; i++) { + var data = expectedBuffer.getChannelData(i-1); + for (var j = 0; j < data.length; j++) { + data[j] = (i == 3) ? 0 : i; + } + } + + return expectedBuffer; + } + }; + + runTest(); + </script> + </pre> + </body> +</html>
\ No newline at end of file diff --git a/dom/media/webaudio/test/test_disconnectFromAudioNodeMultipleConnection.html b/dom/media/webaudio/test/test_disconnectFromAudioNodeMultipleConnection.html new file mode 100644 index 0000000000..9bd8e4d6f6 --- /dev/null +++ b/dom/media/webaudio/test/test_disconnectFromAudioNodeMultipleConnection.html @@ -0,0 +1,56 @@ +<!DOCTYPE HTML> +<html> + <head> + <title> + Test whether we can disconnect all outbound connection of an AudioNode + </title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + </head> + <body> + <pre id="test"> + <script class="testbody" type="text/javascript"> + var gTest = { + length: 256, + numberOfChannels: 2, + createGraph(context) { + var sourceBuffer = context.createBuffer(1, 256, context.sampleRate); + var data = sourceBuffer.getChannelData(0); + for (var j = 0; j < data.length; j++) { + data[j] = 1; + } + + var source = context.createBufferSource(); + source.buffer = sourceBuffer; + + var merger = context.createChannelMerger(2); + var gain = context.createGain(); + + source.connect(merger, 0, 0); + source.connect(gain); + source.connect(merger, 0, 1); + + source.disconnect(merger); + + source.start(); + + return merger; + }, + createExpectedBuffers(context) { + let expectedBuffer = context.createBuffer(2, 256, context.sampleRate); + for (var channel = 0; channel < 2; channel++) { + for (var i = 0; i < 256; ++i) { + expectedBuffer.getChannelData(0)[i] = 0; + } + } + + return expectedBuffer; + } + }; + + runTest(); + </script> + </pre> + </body> +</html> diff --git a/dom/media/webaudio/test/test_disconnectFromOutput.html b/dom/media/webaudio/test/test_disconnectFromOutput.html new file mode 100644 index 0000000000..35080bf609 --- /dev/null +++ b/dom/media/webaudio/test/test_disconnectFromOutput.html @@ -0,0 +1,54 @@ +<!DOCTYPE HTML> +<html> + <head> + <title>Test whether we can disconnect an AudioNode</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + </head> + <body> + <pre id="test"> + <script class="testbody" type="text/javascript"> + var gTest = { + length: 256, + numberOfChannels: 1, + createGraph(context) { + var sourceBuffer = context.createBuffer(3, 256, context.sampleRate); + for (var i = 1; i <= 3; i++) { + var data = sourceBuffer.getChannelData(i-1); + for (var j = 0; j < data.length; j++) { + data[j] = i; + } + } + + var source = context.createBufferSource(); + source.buffer = sourceBuffer; + + var splitter = context.createChannelSplitter(3); + var sum = context.createGain(); + + source.connect(splitter); + splitter.connect(sum, 0); + splitter.connect(sum, 1); + splitter.connect(sum, 2); + source.start(); + + splitter.disconnect(1); + + return sum; + }, + createExpectedBuffers(context) { + let expectedBuffer = context.createBuffer(1, 256, context.sampleRate); + for (var i = 0; i < 256; ++i) { + expectedBuffer.getChannelData(0)[i] = 4; + } + + return expectedBuffer; + }, + }; + + runTest(); + </script> + </pre> + </body> +</html> diff --git a/dom/media/webaudio/test/test_dynamicsCompressorNode.html b/dom/media/webaudio/test/test_dynamicsCompressorNode.html new file mode 100644 index 0000000000..05b6887a53 --- /dev/null +++ b/dom/media/webaudio/test/test_dynamicsCompressorNode.html @@ -0,0 +1,68 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test DynamicsCompressorNode</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +function near(a, b, msg) { + ok(Math.abs(a - b) < 1e-4, msg); +} + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + var context = new AudioContext(); + + var osc = context.createOscillator(); + var sp = context.createScriptProcessor(); + + var compressor = new DynamicsCompressorNode(context); + + osc.connect(compressor); + osc.connect(sp); + compressor.connect(context.destination); + + is(compressor.channelCount, 2, "compressor node has 2 input channels by default"); + is(compressor.channelCountMode, "clamped-max", "Correct channelCountMode for the compressor node"); + is(compressor.channelInterpretation, "speakers", "Correct channelCountInterpretation for the compressor node"); + + // Verify default values + ok(compressor.threshold instanceof AudioParam, "treshold is an AudioParam"); + near(compressor.threshold.defaultValue, -24, "Correct default value for threshold"); + ok(compressor.knee instanceof AudioParam, "knee is an AudioParam"); + near(compressor.knee.defaultValue, 30, "Correct default value for knee"); + ok(compressor.ratio instanceof AudioParam, "knee is an AudioParam"); + near(compressor.ratio.defaultValue, 12, "Correct default value for ratio"); + is(typeof compressor.reduction, "number", "reduction is a number"); + near(compressor.reduction, 0, "Correct default value for reduction"); + ok(compressor.attack instanceof AudioParam, "attack is an AudioParam"); + near(compressor.attack.defaultValue, 0.003, "Correct default value for attack"); + ok(compressor.release instanceof AudioParam, "release is an AudioParam"); + near(compressor.release.defaultValue, 0.25, "Correct default value for release"); + + compressor.threshold.value = -80; + + osc.start(); + var iteration = 0; + sp.onaudioprocess = function(e) { + if (iteration > 10) { + ok(compressor.reduction < 0, + "Feeding a full-scale sine to a compressor should result in an db" + + "reduction."); + sp.onaudioprocess = null; + osc.stop(0); + + SimpleTest.finish(); + } + iteration++; + } +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_dynamicsCompressorNodePassThrough.html b/dom/media/webaudio/test/test_dynamicsCompressorNodePassThrough.html new file mode 100644 index 0000000000..9e8d794547 --- /dev/null +++ b/dom/media/webaudio/test/test_dynamicsCompressorNodePassThrough.html @@ -0,0 +1,47 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test DynamicsCompressorNode with passthrough</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +var gTest = { + length: 2048, + numberOfChannels: 1, + createGraph(context) { + var source = context.createBufferSource(); + + var compressor = context.createDynamicsCompressor(); + + source.buffer = this.buffer; + + source.connect(compressor); + + var compressorWrapped = SpecialPowers.wrap(compressor); + ok("passThrough" in compressorWrapped, "DynamicsCompressorNode should support the passThrough API"); + compressorWrapped.passThrough = true; + + source.start(0); + return compressor; + }, + createExpectedBuffers(context) { + this.buffer = context.createBuffer(1, 2048, context.sampleRate); + for (var i = 0; i < 2048; ++i) { + this.buffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate); + } + + return [this.buffer]; + }, +}; + +runTest(); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_dynamicsCompressorNodeWithGain.html b/dom/media/webaudio/test/test_dynamicsCompressorNodeWithGain.html new file mode 100644 index 0000000000..7e6487e3f8 --- /dev/null +++ b/dom/media/webaudio/test/test_dynamicsCompressorNodeWithGain.html @@ -0,0 +1,51 @@ +<!DOCTYPE HTML> +<html> +<head> +<meta charset="utf-8"> + <title>Test DynamicsCompressor with Gain</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script class="testbody" type="text/javascript"> +SimpleTest.waitForExplicitFinish(); + +addLoadEvent(function() { + var samplerate = 44100; + var context = new OfflineAudioContext(1, samplerate/100, samplerate); + + var osc = context.createOscillator(); + osc.frequency.value = 2400; + + var gain = context.createGain(); + gain.gain.value = 1.5; + + // These numbers are borrowed from the example code on MDN + // https://developer.mozilla.org/en-US/docs/Web/API/DynamicsCompressorNode + var compressor = context.createDynamicsCompressor(); + compressor.threshold.value = -50; + compressor.knee.value = 40; + compressor.ratio.value = 12; + compressor.reduction.value = -20; + compressor.attack.value = 0; + compressor.release.value = 0.25; + + osc.connect(gain); + gain.connect(compressor); + compressor.connect(context.destination); + osc.start(); + + context.startRendering().then(buffer => { + var peak = Math.max(...buffer.getChannelData(0)); + console.log(peak); + // These values are experimentally determined. Without dynamics compression + // the peak should be just under 1.5. We also check for a minimum value + // to make sure we are not getting all zeros. + ok(peak >= 0.2 && peak < 1.0, "Peak value should be greater than 0.25 and less than 1.0"); + SimpleTest.finish(); + }); +}); +</script> +<pre> +</pre> +</body> diff --git a/dom/media/webaudio/test/test_event_listener_leaks.html b/dom/media/webaudio/test/test_event_listener_leaks.html new file mode 100644 index 0000000000..a3bcc9259e --- /dev/null +++ b/dom/media/webaudio/test/test_event_listener_leaks.html @@ -0,0 +1,47 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1450358 - Test AudioContext event listener leak conditions</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/dom/events/test/event_leak_utils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script class="testbody" type="text/javascript"> +// Manipulate AudioContext objects in the frame's context. +// Its important here that we create a listener callback from +// the DOM objects back to the frame's global in order to +// exercise the leak condition. +async function useAudioContext(contentWindow) { + let ctx = new contentWindow.AudioContext(); + ctx.onstatechange = e => { + contentWindow.stateChangeCount += 1; + }; + + let osc = ctx.createOscillator(); + osc.type = "sine"; + osc.frequency.value = 440; + osc.start(); +} + +async function runTest() { + try { + await checkForEventListenerLeaks("AudioContext", useAudioContext); + } catch (e) { + ok(false, e); + } finally { + SimpleTest.finish(); + } +} + +SimpleTest.waitForExplicitFinish(); +addEventListener("load", runTest, { once: true }); +</script> +</pre> +</body> +</html> + diff --git a/dom/media/webaudio/test/test_gainNode.html b/dom/media/webaudio/test/test_gainNode.html new file mode 100644 index 0000000000..77b0ae88b0 --- /dev/null +++ b/dom/media/webaudio/test/test_gainNode.html @@ -0,0 +1,72 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test GainNode</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +var gTest = { + length: 2048, + numberOfChannels: 1, + createGraph(context) { + + var gain = new GainNode(context); + ok(gain.gain, "The audioparam member must exist"); + is(gain.gain.value, 1.0, "Correct initial value"); + is(gain.gain.defaultValue, 1.0, "Correct default value"); + gain.gain.value = 0.5; + is(gain.gain.value, 0.5, "Correct initial value"); + is(gain.gain.defaultValue, 1.0, "Correct default value"); + + gain = new GainNode(context, { gain: 0.5 }); + ok(gain.gain, "The audioparam member must exist"); + is(gain.gain.value, 0.5, "Correct initial value"); + is(gain.gain.defaultValue, 1.0, "Correct default value"); + gain.gain.value = 0.5; + is(gain.gain.value, 0.5, "Correct initial value"); + is(gain.gain.defaultValue, 1.0, "Correct default value"); + + var buffer = context.createBuffer(1, 2048, context.sampleRate); + for (var i = 0; i < 2048; ++i) { + buffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate); + } + + var source = context.createBufferSource(); + source.buffer = buffer; + + gain = context.createGain(); + source.connect(gain); + + ok(gain.gain, "The audioparam member must exist"); + is(gain.gain.value, 1.0, "Correct initial value"); + is(gain.gain.defaultValue, 1.0, "Correct default value"); + gain.gain.value = 0.5; + is(gain.gain.value, 0.5, "Correct initial value"); + is(gain.gain.defaultValue, 1.0, "Correct default value"); + is(gain.channelCount, 2, "gain node has 2 input channels by default"); + is(gain.channelCountMode, "max", "Correct channelCountMode for the gain node"); + is(gain.channelInterpretation, "speakers", "Correct channelCountInterpretation for the gain node"); + + source.start(0); + return gain; + }, + createExpectedBuffers(context) { + var expectedBuffer = context.createBuffer(1, 2048, context.sampleRate); + for (var i = 0; i < 2048; ++i) { + expectedBuffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate) / 2; + } + return expectedBuffer; + }, +}; + +runTest(); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_gainNodeInLoop.html b/dom/media/webaudio/test/test_gainNodeInLoop.html new file mode 100644 index 0000000000..90dedc0ef4 --- /dev/null +++ b/dom/media/webaudio/test/test_gainNodeInLoop.html @@ -0,0 +1,48 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test GainNode in presence of loops</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +var gTest = { + length: 4096, + numberOfChannels: 1, + createGraph(context) { + var sourceBuffer = context.createBuffer(1, 2048, context.sampleRate); + for (var i = 0; i < 2048; ++i) { + sourceBuffer.getChannelData(0)[i] = 1; + } + + var source = context.createBufferSource(); + source.buffer = sourceBuffer; + source.loop = true; + source.start(0); + source.stop(sourceBuffer.duration * 2); + + var gain = context.createGain(); + // Adjust the gain in a way that we don't just end up modifying AudioChunk::mVolume + gain.gain.setValueAtTime(0.5, 0); + source.connect(gain); + return gain; + }, + createExpectedBuffers(context) { + var expectedBuffer = context.createBuffer(1, 4096, context.sampleRate); + for (var i = 0; i < 4096; ++i) { + expectedBuffer.getChannelData(0)[i] = 0.5; + } + return expectedBuffer; + }, +}; + +runTest(); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_gainNodePassThrough.html b/dom/media/webaudio/test/test_gainNodePassThrough.html new file mode 100644 index 0000000000..5a973ccaa2 --- /dev/null +++ b/dom/media/webaudio/test/test_gainNodePassThrough.html @@ -0,0 +1,49 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test GainNode with passthrough</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +var gTest = { + length: 2048, + numberOfChannels: 1, + createGraph(context) { + var source = context.createBufferSource(); + + var gain = context.createGain(); + + source.buffer = this.buffer; + + source.connect(gain); + + gain.gain.value = 0.5; + + var gainWrapped = SpecialPowers.wrap(gain); + ok("passThrough" in gainWrapped, "GainNode should support the passThrough API"); + gainWrapped.passThrough = true; + + source.start(0); + return gain; + }, + createExpectedBuffers(context) { + this.buffer = context.createBuffer(1, 2048, context.sampleRate); + for (var i = 0; i < 2048; ++i) { + this.buffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate); + } + + return [this.buffer]; + }, +}; + +runTest(); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_iirFilterNodePassThrough.html b/dom/media/webaudio/test/test_iirFilterNodePassThrough.html new file mode 100644 index 0000000000..ab54499e04 --- /dev/null +++ b/dom/media/webaudio/test/test_iirFilterNodePassThrough.html @@ -0,0 +1,47 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test IIRFilterNode with passthrough</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +var gTest = { + length: 2048, + numberOfChannels: 1, + createGraph(context) { + var source = context.createBufferSource(); + + var filter = context.createIIRFilter([0.5, 0.5], [1.0]); + + source.buffer = this.buffer; + + source.connect(filter); + + var filterWrapped = SpecialPowers.wrap(filter); + ok("passThrough" in filterWrapped, "BiquadFilterNode should support the passThrough API"); + filterWrapped.passThrough = true; + + source.start(0); + return filter; + }, + createExpectedBuffers(context) { + this.buffer = context.createBuffer(1, 2048, context.sampleRate); + for (var i = 0; i < 2048; ++i) { + this.buffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate); + } + + return [this.buffer]; + }, +}; + +runTest(); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_maxChannelCount.html b/dom/media/webaudio/test/test_maxChannelCount.html new file mode 100644 index 0000000000..1a4e5c856e --- /dev/null +++ b/dom/media/webaudio/test/test_maxChannelCount.html @@ -0,0 +1,38 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test the AudioContext.destination interface</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +// Work around bug 911777 +SpecialPowers.forceGC(); +SpecialPowers.forceCC(); + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + var ac = new AudioContext(); + ok(ac.destination.maxChannelCount > 0, "We can query the maximum number of channels"); + + var oac = new OfflineAudioContext(2, 1024, 48000); + is(oac.destination.maxChannelCount, 2, "This OfflineAudioContext should have 2 max channels."); + + oac = new OfflineAudioContext(6, 1024, 48000); + is(oac.destination.maxChannelCount, 6, "This OfflineAudioContext should have 6 max channels."); + + expectException(function() { + oac.destination.channelCount = oac.destination.channelCount + 1; + }, DOMException.INDEX_SIZE_ERR); + + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_mediaDecoding.html b/dom/media/webaudio/test/test_mediaDecoding.html new file mode 100644 index 0000000000..e76a533e4a --- /dev/null +++ b/dom/media/webaudio/test/test_mediaDecoding.html @@ -0,0 +1,436 @@ +<!DOCTYPE html> +<html> + <head> + <title>Test the decodeAudioData API and Resampling</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + </head> + + <body> + <pre id="test"> +<script src="webaudio.js" type="text/javascript"></script> +<script type="text/javascript"> + + // These routines have been copied verbatim from WebKit, and are used in order + // to convert a memory buffer into a wave buffer. + function writeString(s, a, offset) { + for (var i = 0; i < s.length; ++i) { + a[offset + i] = s.charCodeAt(i); + } + } + + function writeInt16(n, a, offset) { + n = Math.floor(n); + + var b1 = n & 255; + var b2 = (n >> 8) & 255; + + a[offset + 0] = b1; + a[offset + 1] = b2; + } + + function writeInt32(n, a, offset) { + n = Math.floor(n); + var b1 = n & 255; + var b2 = (n >> 8) & 255; + var b3 = (n >> 16) & 255; + var b4 = (n >> 24) & 255; + + a[offset + 0] = b1; + a[offset + 1] = b2; + a[offset + 2] = b3; + a[offset + 3] = b4; + } + + function writeAudioBuffer(audioBuffer, a, offset) { + var n = audioBuffer.length; + var channels = audioBuffer.numberOfChannels; + + for (var i = 0; i < n; ++i) { + for (var k = 0; k < channels; ++k) { + var buffer = audioBuffer.getChannelData(k); + var sample = buffer[i] * 32768.0; + + // Clip samples to the limitations of 16-bit. + // If we don't do this then we'll get nasty wrap-around distortion. + if (sample < -32768) + sample = -32768; + if (sample > 32767) + sample = 32767; + + writeInt16(sample, a, offset); + offset += 2; + } + } + } + + function createWaveFileData(audioBuffer) { + var frameLength = audioBuffer.length; + var numberOfChannels = audioBuffer.numberOfChannels; + var sampleRate = audioBuffer.sampleRate; + var bitsPerSample = 16; + var byteRate = sampleRate * numberOfChannels * bitsPerSample / 8; + var blockAlign = numberOfChannels * bitsPerSample / 8; + var wavDataByteLength = frameLength * numberOfChannels * 2; // 16-bit audio + var headerByteLength = 44; + var totalLength = headerByteLength + wavDataByteLength; + + var waveFileData = new Uint8Array(totalLength); + + var subChunk1Size = 16; // for linear PCM + var subChunk2Size = wavDataByteLength; + var chunkSize = 4 + (8 + subChunk1Size) + (8 + subChunk2Size); + + writeString("RIFF", waveFileData, 0); + writeInt32(chunkSize, waveFileData, 4); + writeString("WAVE", waveFileData, 8); + writeString("fmt ", waveFileData, 12); + + writeInt32(subChunk1Size, waveFileData, 16); // SubChunk1Size (4) + writeInt16(1, waveFileData, 20); // AudioFormat (2) + writeInt16(numberOfChannels, waveFileData, 22); // NumChannels (2) + writeInt32(sampleRate, waveFileData, 24); // SampleRate (4) + writeInt32(byteRate, waveFileData, 28); // ByteRate (4) + writeInt16(blockAlign, waveFileData, 32); // BlockAlign (2) + writeInt32(bitsPerSample, waveFileData, 34); // BitsPerSample (4) + + writeString("data", waveFileData, 36); + writeInt32(subChunk2Size, waveFileData, 40); // SubChunk2Size (4) + + // Write actual audio data starting at offset 44. + writeAudioBuffer(audioBuffer, waveFileData, 44); + + return waveFileData; + } + +</script> +<script class="testbody" type="text/javascript"> + + SimpleTest.waitForExplicitFinish(); + + // fuzzTolerance and fuzzToleranceMobile are used to determine fuzziness + // thresholds. They're needed to make sure that we can deal with neglibible + // differences in the binary buffer caused as a result of resampling the + // audio. fuzzToleranceMobile is typically larger on mobile platforms since + // we do fixed-point resampling as opposed to floating-point resampling on + // those platforms. + // If fuzzMagnitude, is present, is the maximum magnitude difference, per + // sample, to consider two samples are identical. It is multiplied by the + // maximum value a sample, in our case INT16_MAX. This allows checking files + // that should be identical except one has e.g. a higher quantization noise. + var files = [ + // An ogg file, 44.1khz, mono + { + url: "ting-44.1k-1ch.ogg", + valid: true, + expectedUrl: "ting-44.1k-1ch.wav", + numberOfChannels: 1, + frames: 30592, + sampleRate: 44100, + duration: 0.693, + fuzzTolerance: 5, + fuzzToleranceMobile: 1284 + }, + // An ogg file, 44.1khz, stereo + { + url: "ting-44.1k-2ch.ogg", + valid: true, + expectedUrl: "ting-44.1k-2ch.wav", + numberOfChannels: 2, + frames: 30592, + sampleRate: 44100, + duration: 0.693, + fuzzTolerance: 6, + fuzzToleranceMobile: 2544 + }, + // An ogg file, 48khz, mono + { + url: "ting-48k-1ch.ogg", + valid: true, + expectedUrl: "ting-48k-1ch.wav", + numberOfChannels: 1, + frames: 33297, + sampleRate: 48000, + duration: 0.693, + fuzzTolerance: 5, + fuzzToleranceMobile: 1388 + }, + // An ogg file, 48khz, stereo + { + url: "ting-48k-2ch.ogg", + valid: true, + expectedUrl: "ting-48k-2ch.wav", + numberOfChannels: 2, + frames: 33297, + sampleRate: 48000, + duration: 0.693, + fuzzTolerance: 14, + fuzzToleranceMobile: 2752 + }, + // Make sure decoding a wave file results in the same buffer (for both the + // resampling and non-resampling cases) + { + url: "ting-44.1k-1ch.wav", + valid: true, + expectedUrl: "ting-44.1k-1ch.wav", + numberOfChannels: 1, + frames: 30592, + sampleRate: 44100, + duration: 0.693, + fuzzTolerance: 0, + fuzzToleranceMobile: 0 + }, + { + url: "ting-48k-1ch.wav", + valid: true, + expectedUrl: "ting-48k-1ch.wav", + numberOfChannels: 1, + frames: 33297, + sampleRate: 48000, + duration: 0.693, + fuzzTolerance: 0, + fuzzToleranceMobile: 0 + }, + // // A wave file + // //{ url: "24bit-44khz.wav", valid: true, expectedUrl: "24bit-44khz-expected.wav" }, + // A non-audio file + { url: "invalid.txt", valid: false, sampleRate: 44100 }, + // A webm file with no audio + { url: "noaudio.webm", valid: false, sampleRate: 48000 }, + // A video ogg file with audio + { + url: "audio.ogv", + valid: true, + expectedUrl: "audio-expected.wav", + numberOfChannels: 2, + sampleRate: 44100, + frames: 47680, + duration: 1.0807, + fuzzTolerance: 106, + fuzzToleranceMobile: 3482 + }, + { + url: "nil-packet.ogg", + expectedUrl: null, + valid: true, + numberOfChannels: 2, + sampleRate: 48000, + frames: 18600, + duration: 0.3874, + }, + { + url: "half-a-second-1ch-44100-mulaw.wav", + // It is expected that mulaw and linear are similar enough at 16-bits + expectedUrl: "half-a-second-1ch-44100.wav", + valid: true, + numberOfChannels: 1, + sampleRate: 44100, + frames: 22050, + duration: 0.5, + fuzzMagnitude: 0.04, + }, + { + url: "half-a-second-1ch-44100-alaw.wav", + // It is expected that alaw and linear are similar enough at 16-bits + expectedUrl: "half-a-second-1ch-44100.wav", + valid: true, + numberOfChannels: 1, + sampleRate: 44100, + frames: 22050, + duration: 0.5, + fuzzMagnitude: 0.04, + }, + { + url: "waveformatextensible.wav", + valid: true, + numberOfChannels: 1, + sampleRate: 44100, + frames: 472, + duration: 0.01 + }, + { + // A wav file that has 8 channel, but has a channel mask that doesn't + // match the channel count. + url: "waveformatextensiblebadmask.wav", + valid: true, + numberOfChannels: 8, + sampleRate: 8000, + frames: 80, + duration: 0.01 + } + ]; + + // Returns true if the memory buffers are less different that |fuzz| bytes + function fuzzyMemcmp(buf1, buf2, fuzz) { + var difference = 0; + is(buf1.length, buf2.length, "same length"); + for (var i = 0; i < buf1.length; ++i) { + if (Math.abs(buf1[i] - buf2[i]) > fuzz.magnitude * (2 << 15)) { + ++difference; + } + } + if (difference > fuzz.count) { + ok(false, "Expected at most " + fuzz + " bytes difference, found " + difference + " bytes"); + } + console.log(difference, fuzz.count); + return difference <= fuzz.count; + } + + function getFuzzTolerance(test) { + var kIsMobile = + navigator.userAgent.includes("Mobile") || // b2g + navigator.userAgent.includes("Android"); // android + return { + magnitude: test.fuzzMagnitude ?? 0, + count: kIsMobile ? test.fuzzToleranceMobile ?? 0 : test.fuzzTolerance ?? 0 + }; + } + + function bufferIsSilent(buffer) { + for (var i = 0; i < buffer.length; ++i) { + if (buffer.getChannelData(0)[i] != 0) { + return false; + } + } + return true; + } + + function checkAudioBuffer(buffer, test) { + if (buffer.numberOfChannels != test.numberOfChannels) { + is(buffer.numberOfChannels, test.numberOfChannels, "Correct number of channels"); + return; + } + ok(Math.abs(buffer.duration - test.duration) < 1e-3, `Correct duration expected ${test.duration} got ${buffer.duration}`); + if (Math.abs(buffer.duration - test.duration) >= 1e-3) { + ok(false, "got: " + buffer.duration + ", expected: " + test.duration); + } + is(buffer.sampleRate, test.sampleRate, "Correct sample rate"); + is(buffer.length, test.frames, "Correct length"); + + var wave = createWaveFileData(buffer); + if (test.expectedWaveData) { + ok(fuzzyMemcmp(wave, test.expectedWaveData, getFuzzTolerance(test)), "Received expected decoded data for " + test.url); + } + } + + function checkResampledBuffer(buffer, test, callback) { + if (buffer.numberOfChannels != test.numberOfChannels) { + is(buffer.numberOfChannels, test.numberOfChannels, "Correct number of channels"); + return; + } + ok(Math.abs(buffer.duration - test.duration) < 1e-3, "Correct duration"); + if (Math.abs(buffer.duration - test.duration) >= 1e-3) { + ok(false, "got: " + buffer.duration + ", expected: " + test.duration); + } + // Take into account the resampling when checking the size + var expectedLength = test.frames * buffer.sampleRate / test.sampleRate; + SimpleTest.ok( + Math.abs(buffer.length - expectedLength) < 1.0, + "Correct length - got " + buffer.length + + ", expected about " + expectedLength + ); + + // Playback the buffer in the original context, to resample back to the + // original rate and compare with the decoded buffer without resampling. + let cx = test.nativeContext; + var expected = cx.createBufferSource(); + expected.buffer = test.expectedBuffer; + expected.start(); + var inverse = cx.createGain(); + inverse.gain.value = -1; + expected.connect(inverse); + inverse.connect(cx.destination); + var resampled = cx.createBufferSource(); + resampled.buffer = buffer; + resampled.start(); + // This stop should do nothing, but it tests for bug 937475 + resampled.stop(test.frames / cx.sampleRate); + resampled.connect(cx.destination); + cx.oncomplete = function (e) { + ok(!bufferIsSilent(e.renderedBuffer), "Expect buffer not silent"); + // Resampling will lose the highest frequency components, so we should + // pass the difference through a low pass filter. However, either the + // input files don't have significant high frequency components or the + // tolerance in compareBuffers() is too high to detect them. + compareBuffers(e.renderedBuffer, + cx.createBuffer(test.numberOfChannels, + test.frames, test.sampleRate)); + callback(); + } + cx.startRendering(); + } + + function runResampling(test, response, callback) { + var sampleRate = test.sampleRate == 44100 ? 48000 : 44100; + var cx = new OfflineAudioContext(1, 1, sampleRate); + cx.decodeAudioData(response, function onSuccess(asyncResult) { + is(asyncResult.sampleRate, sampleRate, "Correct sample rate"); + + checkResampledBuffer(asyncResult, test, callback); + }, function onFailure() { + ok(false, "Expected successful decode with resample"); + callback(); + }); + } + + function runTest(test, response, callback) { + // We need to copy the array here, because decodeAudioData will detach the + // array's buffer. + var compressedAudio = response.slice(0); + var expectCallback = false; + var cx = new OfflineAudioContext(test.numberOfChannels || 1, + test.frames || 1, test.sampleRate); + cx.decodeAudioData(response, function onSuccess(asyncResult) { + ok(expectCallback, "Success callback should fire asynchronously"); + ok(test.valid, "Did expect success for test " + test.url); + + checkAudioBuffer(asyncResult, test); + + test.expectedBuffer = asyncResult; + test.nativeContext = cx; + runResampling(test, compressedAudio, callback); + }, function onFailure(e) { + ok(e instanceof DOMException, "We want to see an exception here"); + is(e.name, "EncodingError", "Exception name matches"); + ok(expectCallback, "Failure callback should fire asynchronously"); + ok(!test.valid, "Did expect failure for test " + test.url); + callback(); + }); + expectCallback = true; + } + + function loadTest(test, callback) { + var xhr = new XMLHttpRequest(); + xhr.open("GET", test.url, true); + xhr.responseType = "arraybuffer"; + xhr.onload = function () { + if (!test.expectedUrl) { + runTest(test, xhr.response, callback); + return; + } + var getExpected = new XMLHttpRequest(); + getExpected.open("GET", test.expectedUrl, true); + getExpected.responseType = "arraybuffer"; + getExpected.onload = function () { + test.expectedWaveData = new Uint8Array(getExpected.response); + runTest(test, xhr.response, callback); + }; + getExpected.send(); + }; + xhr.send(); + } + + function loadNextTest() { + if (files.length) { + loadTest(files.shift(), loadNextTest); + } else { + SimpleTest.finish(); + } + } + + loadNextTest(); + +</script> +</pre> + </body> +</html> diff --git a/dom/media/webaudio/test/test_mediaElementAudioSourceNode.html b/dom/media/webaudio/test/test_mediaElementAudioSourceNode.html new file mode 100644 index 0000000000..36e1e9f2cc --- /dev/null +++ b/dom/media/webaudio/test/test_mediaElementAudioSourceNode.html @@ -0,0 +1,74 @@ +<!DOCTYPE HTML> +<html> +<meta charset="utf-8"> +<head> + <title>Test MediaElementAudioSourceNode</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> +SimpleTest.waitForExplicitFinish(); + +function test() { + var audio = new Audio("small-shot.ogg"); + var context = new AudioContext(); + var expectedMinNonzeroSampleCount; + var expectedMaxNonzeroSampleCount; + var nonzeroSampleCount = 0; + var complete = false; + var iterationCount = 0; + + // This test ensures we receive at least expectedSampleCount nonzero samples + function processSamples(e) { + if (complete) { + return; + } + + if (iterationCount == 0) { + // Don't start playing the audio until the AudioContext stuff is connected + // and running. + audio.play(); + } + ++iterationCount; + + var buf = e.inputBuffer.getChannelData(0); + var nonzeroSamplesThisBuffer = 0; + for (var i = 0; i < buf.length; ++i) { + if (buf[i] != 0) { + ++nonzeroSamplesThisBuffer; + } + } + nonzeroSampleCount += nonzeroSamplesThisBuffer; + is(e.inputBuffer.numberOfChannels, 1, + "Checking data channel count (nonzeroSamplesThisBuffer=" + + nonzeroSamplesThisBuffer + ")"); + ok(nonzeroSampleCount <= expectedMaxNonzeroSampleCount, + "Too many nonzero samples (got " + nonzeroSampleCount + ", expected max " + expectedMaxNonzeroSampleCount + ")"); + if (nonzeroSampleCount >= expectedMinNonzeroSampleCount && + nonzeroSamplesThisBuffer == 0) { + ok(true, + "Check received enough nonzero samples (got " + nonzeroSampleCount + ", expected min " + expectedMinNonzeroSampleCount + ")"); + SimpleTest.finish(); + complete = true; + } + } + + audio.onloadedmetadata = function() { + var node = new MediaElementAudioSourceNode(context, { mediaElement: audio }); + var sp = context.createScriptProcessor(2048, 1); + node.connect(sp); + // Use a fuzz factor of 100 to account for samples that just happen to be zero + expectedMinNonzeroSampleCount = Math.floor(audio.duration*context.sampleRate) - 100; + expectedMaxNonzeroSampleCount = Math.floor(audio.duration*context.sampleRate) + 500; + sp.onaudioprocess = processSamples; + }; +} + +SpecialPowers.pushPrefEnv({"set": [["media.preload.default", 2], ["media.preload.auto", 3]]}, test); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_mediaElementAudioSourceNodeCrossOrigin.html b/dom/media/webaudio/test/test_mediaElementAudioSourceNodeCrossOrigin.html new file mode 100644 index 0000000000..778658b332 --- /dev/null +++ b/dom/media/webaudio/test/test_mediaElementAudioSourceNodeCrossOrigin.html @@ -0,0 +1,94 @@ +<!DOCTYPE HTML> +<html> +<meta charset="utf-8"> +<head> + <title>Test MediaStreamAudioSourceNode doesn't get data from cross-origin media resources</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> +SimpleTest.waitForExplicitFinish(); + +// Turn off the authentication dialog blocking for this test. +SpecialPowers.setIntPref("network.auth.subresource-http-auth-allow", 2) + +var tests = [ + // Not the same origin no CORS asked for, should have silence + { url: "http://example.org:80/tests/dom/media/webaudio/test/small-shot.ogg", + cors: null, + expectSilence: true }, + // Same origin, should have sound + { url: "small-shot.ogg", + cors: null, + expectSilence: false }, + // Cross-origin but we asked for CORS and the server answered with the right + // header, should have + { url: "http://example.org:80/tests/dom/media/webaudio/test/corsServer.sjs", + cors: "anonymous", + expectSilence: false } +]; + +var testsRemaining = tests.length; + +tests.forEach(function(e) { + e.ac = new AudioContext(); + var a = new Audio(); + if (e.cors) { + a.crossOrigin = e.cors; + } + a.src = e.url; + document.body.appendChild(a); + + a.onloadedmetadata = () => { + // Wait for "loadedmetadata" before capturing since tracks are then known + // directly. If we set up the capture before "loadedmetadata" we + // (internally) have to wait an extra async jump for tracks to become known + // to main thread, before setting up audio data forwarding to the node. + // As that happens, the audio resource may have already ended on slow test + // machines, causing failures. + a.onloadedmetadata = null; + var measn = e.ac.createMediaElementSource(a); + var sp = e.ac.createScriptProcessor(2048, 1); + sp.seenSound = false; + sp.onaudioprocess = checkBufferSilent; + + measn.connect(sp); + a.play(); + }; + + function checkFinished(sp) { + if (a.ended) { + sp.onaudioprocess = null; + var not = e.expectSilence ? "" : "not"; + is(e.expectSilence, !sp.seenSound, + "Buffer is " + not + " silent as expected, for " + + e.url + " (cors: " + e.cors + ")"); + if (--testsRemaining == 0) { + SimpleTest.finish(); + } + } + } + + function checkBufferSilent(e) { + var inputArrayBuffer = e.inputBuffer.getChannelData(0); + var silent = true; + for (var i = 0; i < inputArrayBuffer.length; i++) { + if (inputArrayBuffer[i] != 0.0) { + silent = false; + break; + } + } + // It is acceptable to find a full buffer of silence here, even if we expect + // sound, because Gecko's looping on media elements is not seamless and we + // can underrun. We are looking for at least one buffer of non-silent data. + e.target.seenSound = !silent || e.target.seenSound; + checkFinished(e.target); + return silent; + } +}); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_mediaElementAudioSourceNodeFidelity.html b/dom/media/webaudio/test/test_mediaElementAudioSourceNodeFidelity.html new file mode 100644 index 0000000000..4714172607 --- /dev/null +++ b/dom/media/webaudio/test/test_mediaElementAudioSourceNodeFidelity.html @@ -0,0 +1,137 @@ +<!DOCTYPE HTML> +<html> +<meta charset="utf-8"> +<head> + <title>Test MediaStreamAudioSourceNode doesn't get data from cross-origin media resources</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> +SimpleTest.waitForExplicitFinish(); + +function binIndexForFrequency(frequency, analyser) { + return 1 + Math.round(frequency * + analyser.fftSize / + analyser.context.sampleRate); +} + +function debugCanvas(analyser) { + var cvs = document.createElement("canvas"); + document.body.appendChild(cvs); + + // Easy: 1px per bin + cvs.width = analyser.frequencyBinCount; + cvs.height = 256; + cvs.style.border = "1px solid red"; + + var c = cvs.getContext('2d'); + var buf = new Uint8Array(analyser.frequencyBinCount); + + function render() { + c.clearRect(0, 0, cvs.width, cvs.height); + analyser.getByteFrequencyData(buf); + for (var i = 0; i < buf.length; i++) { + c.fillRect(i, (256 - (buf[i])), 1, 256); + } + requestAnimationFrame(render); + } + requestAnimationFrame(render); +} + + +function checkFrequency(an) { + an.getFloatFrequencyData(frequencyArray); + // We should have no energy when checking the data largely outside the index + // for 440Hz (the frequency of the sine wave), start checking an octave above, + // the Opus compression can add some harmonics to the pure since wave. + var maxNoiseIndex = binIndexForFrequency(880, an); + for (var i = maxNoiseIndex + 1; i < frequencyArray.length; i++) { + if (frequencyArray[i] > frequencyArray[maxNoiseIndex]) { + maxNoiseIndex = i; + } + } + + // On the other hand, we should find a peak at 440Hz. Our sine wave is not + // attenuated, we're expecting the peak to reach 0dBFs. + var index = binIndexForFrequency(440, an); + info("energy at 440: " + frequencyArray[index] + + ", threshold " + (an.maxDecibels - 10) + + "; max noise at index " + maxNoiseIndex + + ": " + frequencyArray[maxNoiseIndex] ); + if (frequencyArray[index] < (an.maxDecibels - 10)) { + return false; + } + // Let some slack, there might be some noise here because of int -> float + // conversion or the Opus encoding. + if (frequencyArray[maxNoiseIndex] > an.minDecibels + 40) { + return false; + } + + return true; +} + +var audioElement = new Audio(); +audioElement.src = 'sine-440-10s.opus' +audioElement.loop = true; +var ac = new AudioContext(); +var mediaElementSource = ac.createMediaElementSource(audioElement); +var an = ac.createAnalyser(); +// Use no smoothing as this would just average with previous +// getFloatFrequencyData() calls. Non-seamless looping would introduce noise, +// and smoothing would spread this into calls after the loop point. +an.smoothingTimeConstant = 0; +let frequencyArray = new Float32Array(an.frequencyBinCount); + +// Uncomment this to check what the analyser is doing. +// debugCanvas(an); + +mediaElementSource.connect(an) + +audioElement.play(); +// We want to check the we have the expected audio for at least two loop of +// the HTMLMediaElement, piped into an AudioContext. The file is ten seconds, +// and we use the default FFT size. +var lastCurrentTime = 0; +var loopCount = 0; +audioElement.onplaying = function() { + audioElement.ontimeupdate = function() { + // We don't run the analysis when close to loop point or at the + // beginning, since looping is not seamless, there could be an + // unpredictable amount of silence + var rv = checkFrequency(an); + info("currentTime: " + audioElement.currentTime); + if (audioElement.currentTime < 4 || + audioElement.currentTime > 8){ + return; + } + if (!rv) { + ok(false, "Found unexpected noise during analysis."); + audioElement.ontimeupdate = null; + audioElement.onplaying = null; + ac.close(); + audioElement.src = ''; + SimpleTest.finish() + return; + } + ok(true, "Found correct audio signal during analysis"); + info(lastCurrentTime + " " + audioElement.currentTime); + if (lastCurrentTime > audioElement.currentTime) { + info("loopCount: " + loopCount); + if (loopCount > 1) { + audioElement.ontimeupdate = null; + audioElement.onplaying = null; + ac.close(); + audioElement.src = ''; + SimpleTest.finish(); + } + lastCurrentTime = audioElement.currentTime; + loopCount++; + } else { + lastCurrentTime = audioElement.currentTime; + } + } +} + +</script> diff --git a/dom/media/webaudio/test/test_mediaElementAudioSourceNodePassThrough.html b/dom/media/webaudio/test/test_mediaElementAudioSourceNodePassThrough.html new file mode 100644 index 0000000000..5ee12b16ad --- /dev/null +++ b/dom/media/webaudio/test/test_mediaElementAudioSourceNodePassThrough.html @@ -0,0 +1,66 @@ +<!DOCTYPE HTML> +<html> +<meta charset="utf-8"> +<head> + <title>Test MediaElementAudioSourceNode with passthrough</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> +SimpleTest.waitForExplicitFinish(); + +function test() { + var audio = new Audio("small-shot.ogg"); + var context = new AudioContext(); + var node = context.createMediaElementSource(audio); + var sp = context.createScriptProcessor(2048, 1); + node.connect(sp); + var nonzeroSampleCount = 0; + var complete = false; + var iterationCount = 0; + + var srcWrapped = SpecialPowers.wrap(node); + ok("passThrough" in srcWrapped, "MediaElementAudioSourceNode should support the passThrough API"); + srcWrapped.passThrough = true; + + // This test ensures we receive at least expectedSampleCount nonzero samples + function processSamples(e) { + if (complete) { + return; + } + + if (iterationCount == 0) { + // Don't start playing the audio until the AudioContext stuff is connected + // and running. + audio.play(); + } + ++iterationCount; + + var buf = e.inputBuffer.getChannelData(0); + var nonzeroSamplesThisBuffer = 0; + for (var i = 0; i < buf.length; ++i) { + if (buf[i] != 0) { + ++nonzeroSamplesThisBuffer; + } + } + nonzeroSampleCount += nonzeroSamplesThisBuffer; + if (iterationCount == 10) { + is(nonzeroSampleCount, 0, "The input must be silence"); + SimpleTest.finish(); + complete = true; + } + } + + audio.oncanplaythrough = function() { + sp.onaudioprocess = processSamples; + }; +} + +SpecialPowers.pushPrefEnv({"set": [["media.preload.default", 2], ["media.preload.auto", 3]]}, test); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_mediaElementAudioSourceNodeVideo.html b/dom/media/webaudio/test/test_mediaElementAudioSourceNodeVideo.html new file mode 100644 index 0000000000..dcb85f12cb --- /dev/null +++ b/dom/media/webaudio/test/test_mediaElementAudioSourceNodeVideo.html @@ -0,0 +1,70 @@ +<!DOCTYPE HTML> +<html> +<meta charset="utf-8"> +<head> + <title>Test MediaElementAudioSourceNode before "loadedmetadata"</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> +SimpleTest.waitForExplicitFinish(); + +var video = document.createElement("video"); +function test() { + video.src = "audiovideo.mp4"; + + var context = new AudioContext(); + var complete = false; + + video.onended = () => { + if (complete) { + return; + } + + complete = true; + ok(false, "Video ended without any samples seen"); + SimpleTest.finish(); + }; + + video.ontimeupdate = () => { + info("Timeupdate: " + video.currentTime); + }; + + var node = context.createMediaElementSource(video); + var sp = context.createScriptProcessor(2048, 1); + node.connect(sp); + + // This test ensures we receive some nonzero samples when we capture to + // WebAudio before "loadedmetadata". + sp.onaudioprocess = e => { + if (complete) { + return; + } + + var buf = e.inputBuffer.getChannelData(0); + for (var i = 0; i < buf.length; ++i) { + if (buf[i] != 0) { + complete = true; + ok(true, "Got non-zero samples"); + SimpleTest.finish(); + return; + } + } + }; + + video.play(); +} + +if (video.canPlayType("video/mp4")) { + test(); +} else { + ok(true, "MP4 not supported. Skipping."); + SimpleTest.finish(); +} + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_mediaStreamAudioDestinationNode.html b/dom/media/webaudio/test/test_mediaStreamAudioDestinationNode.html new file mode 100644 index 0000000000..fd0ce8141b --- /dev/null +++ b/dom/media/webaudio/test/test_mediaStreamAudioDestinationNode.html @@ -0,0 +1,50 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test MediaStreamAudioDestinationNode</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<audio id="audioelem"></audio> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestFlakyTimeout("This test uses a live media element so it needs to wait for the media stack to do some work."); +addLoadEvent(function() { + var context = new AudioContext(); + var buffer = context.createBuffer(1, 2048, context.sampleRate); + for (var i = 0; i < 2048; ++i) { + buffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate); + } + + var source = context.createBufferSource(); + source.buffer = buffer; + + var dest = new MediaStreamAudioDestinationNode(context); + source.connect(dest); + + var elem = document.getElementById('audioelem'); + elem.srcObject = dest.stream; + elem.onloadedmetadata = function() { + ok(true, "got metadata event"); + setTimeout(function() { + is(elem.played.length, 1, "should have a played interval"); + is(elem.played.start(0), 0, "should have played immediately"); + isnot(elem.played.end(0), 0, "should have played for a non-zero interval"); + + // This will end the media element. + dest.stream.getTracks()[0].stop(); + }, 2000); + }; + elem.onended = function() { + ok(true, "media element ended after destination track.stop()"); + SimpleTest.finish(); + }; + + source.start(0); + elem.play(); +}); +</script> diff --git a/dom/media/webaudio/test/test_mediaStreamAudioSourceNode.html b/dom/media/webaudio/test/test_mediaStreamAudioSourceNode.html new file mode 100644 index 0000000000..eaa8a564b9 --- /dev/null +++ b/dom/media/webaudio/test/test_mediaStreamAudioSourceNode.html @@ -0,0 +1,50 @@ +<!DOCTYPE HTML> +<html> +<meta charset="utf-8"> +<head> + <title>Test MediaStreamAudioSourceNode processing is correct</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +function createBuffer(context) { + var buffer = context.createBuffer(2, 2048, context.sampleRate); + for (var i = 0; i < 2048; ++i) { + buffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate); + buffer.getChannelData(1)[i] = -buffer.getChannelData(0)[i]; + } + return buffer; +} + +var gTest = { + length: 2048, + skipOfflineContextTests: true, + createGraph(context) { + var sourceGraph = new AudioContext(); + var source = sourceGraph.createBufferSource(); + source.buffer = createBuffer(context); + var dest = sourceGraph.createMediaStreamDestination(); + source.connect(dest); + source.start(0); + + var mediaStreamSource = new MediaStreamAudioSourceNode(context, { mediaStream: dest.stream }); + // channelCount and channelCountMode should have no effect + mediaStreamSource.channelCount = 1; + mediaStreamSource.channelCountMode = "explicit"; + return mediaStreamSource; + }, + createExpectedBuffers(context) { + return createBuffer(context); + }, +}; + +runTest(); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_mediaStreamAudioSourceNodeCrossOrigin.html b/dom/media/webaudio/test/test_mediaStreamAudioSourceNodeCrossOrigin.html new file mode 100644 index 0000000000..aaeaab3159 --- /dev/null +++ b/dom/media/webaudio/test/test_mediaStreamAudioSourceNodeCrossOrigin.html @@ -0,0 +1,59 @@ +<!DOCTYPE HTML> +<html> +<meta charset="utf-8"> +<head> + <title>Test MediaStreamAudioSourceNode doesn't get data from cross-origin media resources</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> +SimpleTest.waitForExplicitFinish(); + +var audio = new Audio("http://example.org:80/tests/dom/media/webaudio/test/small-shot.ogg"); +audio.load(); +var context = new AudioContext(); +audio.onloadedmetadata = function() { + var node = context.createMediaStreamSource(audio.mozCaptureStreamUntilEnded()); + var sp = context.createScriptProcessor(2048, 1); + node.connect(sp); + var complete = false; + var iterationCount = 0; + + // This test ensures we receive at least expectedSampleCount nonzero samples + function processSamples(e) { + if (complete) { + return; + } + + if (iterationCount == 0) { + // Don't start playing the audio until the AudioContext stuff is connected + // and running. + audio.play(); + } + ++iterationCount; + + var buf = e.inputBuffer.getChannelData(0); + var nonzeroSamplesThisBuffer = 0; + for (var i = 0; i < buf.length; ++i) { + if (buf[i] != 0) { + ++nonzeroSamplesThisBuffer; + } + } + is(nonzeroSamplesThisBuffer, 0, + "Checking all samples are zero"); + if (iterationCount >= 20) { + SimpleTest.finish(); + complete = true; + } + } + + audio.oncanplaythrough = function() { + sp.onaudioprocess = processSamples; + }; +} +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_mediaStreamAudioSourceNodeNoGC.html b/dom/media/webaudio/test/test_mediaStreamAudioSourceNodeNoGC.html new file mode 100644 index 0000000000..7920af9f7b --- /dev/null +++ b/dom/media/webaudio/test/test_mediaStreamAudioSourceNodeNoGC.html @@ -0,0 +1,116 @@ +<!DOCTYPE HTML> +<html> +<meta charset="utf-8"> +<head> + <title>Test that MediaStreamAudioSourceNode and its input MediaStream stays alive while there are active tracks</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestFlakyTimeout("gUM and WebAudio data is async to main thread. " + + "We need a timeout to see that something does " + + "NOT happen to data."); + +let context = new AudioContext(); +let analyser = context.createAnalyser(); + +function wait(millis, resolveWithThis) { + return new Promise(resolve => setTimeout(() => resolve(resolveWithThis), millis)); +} + +function binIndexForFrequency(frequency) { + return 1 + Math.round(frequency * analyser.fftSize / context.sampleRate); +} + +function waitForAudio(analysisFunction, cancelPromise) { + let data = new Uint8Array(analyser.frequencyBinCount); + let cancelled = false; + let cancelledMsg = ""; + cancelPromise.then(msg => { + cancelled = true; + cancelledMsg = msg; + }); + return new Promise((resolve, reject) => { + let loop = () => { + analyser.getByteFrequencyData(data); + if (cancelled) { + reject(new Error("waitForAudio cancelled: " + cancelledMsg)); + return; + } + if (analysisFunction(data)) { + resolve(); + return; + } + requestAnimationFrame(loop); + }; + loop(); + }); +} + +async function test(sourceNode) { + try { + await analyser.connect(context.destination); + + ok(true, "Waiting for audio to pass through the analyser") + await waitForAudio(arr => arr[binIndexForFrequency(1000)] > 200, + wait(60000, "Timeout waiting for audio")); + + ok(true, "Audio was detected by the analyser. Forcing CC."); + SpecialPowers.forceCC(); + SpecialPowers.forceGC(); + SpecialPowers.forceCC(); + SpecialPowers.forceGC(); + + info("Checking that GC didn't destroy the stream or source node"); + await waitForAudio(arr => arr[binIndexForFrequency(1000)] < 50, + wait(5000, "Timeout waiting for GC (timeout OK)")) + .then(() => Promise.reject("Audio stopped unexpectedly"), + () => Promise.resolve()); + + ok(true, "Audio is still flowing"); + } catch(e) { + ok(false, "Error executing test: " + e + (e.stack ? "\n" + e.stack : "")); + SimpleTest.finish(); + } +} + +(async function() { + try { + await SpecialPowers.pushPrefEnv({ + set: [ + // This test expects the fake audio device, specifically for the tones + // it outputs. Explicitly disable the audio loopback device and enable + // fake streams. + ['media.audio_loopback_dev', ''], + ['media.navigator.streams.fake', true], + ['media.navigator.permission.disabled', true] + ] + }); + + // Test stream source GC + let stream = await navigator.mediaDevices.getUserMedia({audio: true}); + let source = context.createMediaStreamSource(stream); + stream = null; + source.connect(analyser); + await test(source); + + // Test track source GC + stream = await navigator.mediaDevices.getUserMedia({audio: true}); + source = context.createMediaStreamTrackSource(stream.getAudioTracks()[0]); + stream = null; + source.connect(analyser); + await test(source); + } catch(e) { + ok(false, `Error executing test: ${e}${e.stack ? "\n" + e.stack : ""}`); + } finally { + context.close(); + SimpleTest.finish(); + } +})(); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_mediaStreamAudioSourceNodePassThrough.html b/dom/media/webaudio/test/test_mediaStreamAudioSourceNodePassThrough.html new file mode 100644 index 0000000000..379bfdbc6a --- /dev/null +++ b/dom/media/webaudio/test/test_mediaStreamAudioSourceNodePassThrough.html @@ -0,0 +1,55 @@ +<!DOCTYPE HTML> +<html> +<meta charset="utf-8"> +<head> + <title>Test MediaStreamAudioSourceNode passthrough</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +function createBuffer(context, delay) { + var buffer = context.createBuffer(2, 2048, context.sampleRate); + for (var i = 0; i < 2048 - delay; ++i) { + buffer.getChannelData(0)[i + delay] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate); + buffer.getChannelData(1)[i + delay] = -buffer.getChannelData(0)[i + delay]; + } + return buffer; +} + +var gTest = { + length: 2048, + skipOfflineContextTests: true, + createGraph(context) { + var sourceGraph = new AudioContext(); + var source = sourceGraph.createBufferSource(); + source.buffer = createBuffer(context, 0); + var dest = sourceGraph.createMediaStreamDestination(); + source.connect(dest); + source.start(0); + + var mediaStreamSource = context.createMediaStreamSource(dest.stream); + // channelCount and channelCountMode should have no effect + mediaStreamSource.channelCount = 1; + mediaStreamSource.channelCountMode = "explicit"; + + var srcWrapped = SpecialPowers.wrap(mediaStreamSource); + ok("passThrough" in srcWrapped, "MediaStreamAudioSourceNode should support the passThrough API"); + srcWrapped.passThrough = true; + + return mediaStreamSource; + }, + createExpectedBuffers(context) { + return context.createBuffer(2, 2048, context.sampleRate); + }, +}; + +runTest(); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_mediaStreamAudioSourceNodeResampling.html b/dom/media/webaudio/test/test_mediaStreamAudioSourceNodeResampling.html new file mode 100644 index 0000000000..efacf1ecc5 --- /dev/null +++ b/dom/media/webaudio/test/test_mediaStreamAudioSourceNodeResampling.html @@ -0,0 +1,74 @@ +<!DOCTYPE HTML> +<html> +<meta charset="utf-8"> +<head> + <title>Test MediaStreamAudioSourceNode processing is correct</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> +SimpleTest.waitForExplicitFinish(); + +function test() { + var audio = new Audio("small-shot.ogg"); + var context = new AudioContext(); + var expectedMinNonzeroSampleCount; + var expectedMaxNonzeroSampleCount; + var nonzeroSampleCount = 0; + var complete = false; + var iterationCount = 0; + + // This test ensures we receive at least expectedSampleCount nonzero samples + function processSamples(e) { + if (complete) { + return; + } + + if (iterationCount == 0) { + // Don't start playing the audio until the AudioContext stuff is connected + // and running. + audio.play(); + } + ++iterationCount; + + var buf = e.inputBuffer.getChannelData(0); + var nonzeroSamplesThisBuffer = 0; + for (var i = 0; i < buf.length; ++i) { + if (buf[i] != 0) { + ++nonzeroSamplesThisBuffer; + } + } + nonzeroSampleCount += nonzeroSamplesThisBuffer; + is(e.inputBuffer.numberOfChannels, 1, + "Checking data channel count (nonzeroSamplesThisBuffer=" + + nonzeroSamplesThisBuffer + ")"); + ok(nonzeroSampleCount <= expectedMaxNonzeroSampleCount, + "Too many nonzero samples (got " + nonzeroSampleCount + ", expected max " + expectedMaxNonzeroSampleCount + ")"); + if (nonzeroSampleCount >= expectedMinNonzeroSampleCount && + nonzeroSamplesThisBuffer == 0) { + ok(true, + "Check received enough nonzero samples (got " + nonzeroSampleCount + ", expected min " + expectedMinNonzeroSampleCount + ")"); + SimpleTest.finish(); + complete = true; + } + } + + audio.onloadedmetadata = function() { + var node = context.createMediaStreamSource(audio.mozCaptureStreamUntilEnded()); + var sp = context.createScriptProcessor(2048, 1, 0); + node.connect(sp); + // Use a fuzz factor of 100 to account for samples that just happen to be zero + expectedMinNonzeroSampleCount = Math.floor(audio.duration*context.sampleRate) - 100; + expectedMaxNonzeroSampleCount = Math.floor(audio.duration*context.sampleRate) + 500; + sp.onaudioprocess = processSamples; + }; +} + +SpecialPowers.pushPrefEnv({"set": [["media.preload.default", 2], ["media.preload.auto", 3]]}, test); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_mediaStreamTrackAudioSourceNode.html b/dom/media/webaudio/test/test_mediaStreamTrackAudioSourceNode.html new file mode 100644 index 0000000000..350e9e0fab --- /dev/null +++ b/dom/media/webaudio/test/test_mediaStreamTrackAudioSourceNode.html @@ -0,0 +1,54 @@ +<!DOCTYPE HTML> +<html> +<meta charset="utf-8"> +<head> + <title>Test MediaStreamTrackAudioSourceNode processing is correct</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +function createBuffer(context) { + let buffer = context.createBuffer(2, 2048, context.sampleRate); + for (let i = 0; i < 2048; ++i) { + buffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate); + buffer.getChannelData(1)[i] = -buffer.getChannelData(0)[i]; + } + return buffer; +} + +let gTest = { + length: 2048, + skipOfflineContextTests: true, + createGraph(context) { + let sourceGraph = new AudioContext(); + let source = sourceGraph.createBufferSource(); + source.buffer = createBuffer(context); + let dest = sourceGraph.createMediaStreamDestination(); + source.connect(dest); + + // Extract first audio track from dest.stream + let track = dest.stream.getAudioTracks()[0]; + + source.start(0); + + let mediaStreamTrackSource = new MediaStreamTrackAudioSourceNode(context, { mediaStreamTrack: track }); + // channelCount and channelCountMode should have no effect + mediaStreamTrackSource.channelCount = 1; + mediaStreamTrackSource.channelCountMode = "explicit"; + return mediaStreamTrackSource; + }, + createExpectedBuffers(context) { + return createBuffer(context); + }, +}; + +runTest(); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_mediaStreamTrackAudioSourceNodeCrossOrigin.html b/dom/media/webaudio/test/test_mediaStreamTrackAudioSourceNodeCrossOrigin.html new file mode 100644 index 0000000000..243534de32 --- /dev/null +++ b/dom/media/webaudio/test/test_mediaStreamTrackAudioSourceNodeCrossOrigin.html @@ -0,0 +1,54 @@ +<!DOCTYPE HTML> +<html> +<meta charset="utf-8"> +<head> +<title>Test MediaStreamTrackAudioSourceNode doesn't get data from cross-origin media resources</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> +SimpleTest.waitForExplicitFinish(); + +const CROSS_ORIGIN_URL = "http://example.org:80/tests/dom/media/webaudio/test/sine-440-10s.opus" +let iterationCount = 0; +let context = null; +let sp; + +function processSamples(e) { + ++iterationCount; + + let buf = e.inputBuffer.getChannelData(0); + let nonzeroSamplesThisBuffer = 0; + for (let i = 0; i < buf.length; ++i) { + if (buf[i] != 0) { + ++nonzeroSamplesThisBuffer; + } + } + is(nonzeroSamplesThisBuffer, 0, + "a source that is cross origin cannot be inspected by Web Audio"); + + if (iterationCount == 40) { + sp.onaudioprocess = null; + context.close(); + SimpleTest.finish(); + } +} + +let audio = new Audio(); +audio.src = CROSS_ORIGIN_URL; +audio.onloadedmetadata = function () { + context = new AudioContext(); + let stream = audio.mozCaptureStream(); + let track = stream.getAudioTracks()[0]; + let node = context.createMediaStreamTrackSource(track); + node.connect(context.destination); + sp = context.createScriptProcessor(2048, 1); + sp.onaudioprocess = processSamples; + node.connect(sp); +} + +</script> +</pre> +</body> diff --git a/dom/media/webaudio/test/test_mediaStreamTrackAudioSourceNodeVideo.html b/dom/media/webaudio/test/test_mediaStreamTrackAudioSourceNodeVideo.html new file mode 100644 index 0000000000..772b7f8d5f --- /dev/null +++ b/dom/media/webaudio/test/test_mediaStreamTrackAudioSourceNodeVideo.html @@ -0,0 +1,27 @@ +<!DOCTYPE HTML> +<html> +<meta charset="utf-8"> +<head> + <title>Test MediaStreamTrackAudioSourceNode throw video track</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/dom/media/webaudio/test/webaudio.js"></script> + <script type="text/javascript" src="/tests/dom/media/webrtc/tests/mochitests/head.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + let context = new AudioContext(); + let canvas = document.createElement("canvas"); + canvas.getContext("2d"); + let track = canvas.captureStream().getTracks()[0]; + + expectException(() => { + new MediaStreamTrackAudioSourceNode( + context, + { mediaStreamTrack: track }); + }, DOMException.INVALID_STATE_ERR); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_mixingRules.html b/dom/media/webaudio/test/test_mixingRules.html new file mode 100644 index 0000000000..719175fbfb --- /dev/null +++ b/dom/media/webaudio/test/test_mixingRules.html @@ -0,0 +1,402 @@ +<!DOCTYPE html> +<html> +<head> + <title>Testcase for AudioNode channel up-mix/down-mix rules</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> + +<body> + +<script> + +// This test is based on http://src.chromium.org/viewvc/blink/trunk/LayoutTests/webaudio/audionode-channel-rules.html + +var context = null; +var sp = null; +var renderNumberOfChannels = 8; +var singleTestFrameLength = 8; +var testBuffers; + +// A list of connections to an AudioNode input, each of which is to be used in one or more specific test cases. +// Each element in the list is a string, with the number of connections corresponding to the length of the string, +// and each character in the string is from '1' to '8' representing a 1 to 8 channel connection (from an AudioNode output). +// For example, the string "128" means 3 connections, having 1, 2, and 8 channels respectively. +var connectionsList = []; +for (var i = 1; i <= 8; ++i) { + connectionsList.push(i.toString()); + for (var j = 1; j <= 8; ++j) { + connectionsList.push(i.toString() + j.toString()); + } +} + +// A list of mixing rules, each of which will be tested against all of the connections in connectionsList. +var mixingRulesList = [ + {channelCount: 1, channelCountMode: "max", channelInterpretation: "speakers"}, + {channelCount: 2, channelCountMode: "clamped-max", channelInterpretation: "speakers"}, + {channelCount: 3, channelCountMode: "clamped-max", channelInterpretation: "speakers"}, + {channelCount: 4, channelCountMode: "clamped-max", channelInterpretation: "speakers"}, + {channelCount: 5, channelCountMode: "clamped-max", channelInterpretation: "speakers"}, + {channelCount: 6, channelCountMode: "clamped-max", channelInterpretation: "speakers"}, + {channelCount: 7, channelCountMode: "clamped-max", channelInterpretation: "speakers"}, + {channelCount: 2, channelCountMode: "explicit", channelInterpretation: "speakers"}, + {channelCount: 3, channelCountMode: "explicit", channelInterpretation: "speakers"}, + {channelCount: 4, channelCountMode: "explicit", channelInterpretation: "speakers"}, + {channelCount: 5, channelCountMode: "explicit", channelInterpretation: "speakers"}, + {channelCount: 6, channelCountMode: "explicit", channelInterpretation: "speakers"}, + {channelCount: 7, channelCountMode: "explicit", channelInterpretation: "speakers"}, + {channelCount: 8, channelCountMode: "explicit", channelInterpretation: "speakers"}, + {channelCount: 1, channelCountMode: "max", channelInterpretation: "discrete"}, + {channelCount: 2, channelCountMode: "clamped-max", channelInterpretation: "discrete"}, + {channelCount: 3, channelCountMode: "clamped-max", channelInterpretation: "discrete"}, + {channelCount: 4, channelCountMode: "clamped-max", channelInterpretation: "discrete"}, + {channelCount: 5, channelCountMode: "clamped-max", channelInterpretation: "discrete"}, + {channelCount: 6, channelCountMode: "clamped-max", channelInterpretation: "discrete"}, + {channelCount: 3, channelCountMode: "explicit", channelInterpretation: "discrete"}, + {channelCount: 4, channelCountMode: "explicit", channelInterpretation: "discrete"}, + {channelCount: 5, channelCountMode: "explicit", channelInterpretation: "discrete"}, + {channelCount: 6, channelCountMode: "explicit", channelInterpretation: "discrete"}, + {channelCount: 7, channelCountMode: "explicit", channelInterpretation: "discrete"}, + {channelCount: 8, channelCountMode: "explicit", channelInterpretation: "discrete"}, +]; + +var numberOfTests = mixingRulesList.length * connectionsList.length; + +// Create an n-channel buffer, with all sample data zero except for a shifted impulse. +// The impulse position depends on the channel index. +// For example, for a 4-channel buffer: +// channel0: 1 0 0 0 0 0 0 0 +// channel1: 0 1 0 0 0 0 0 0 +// channel2: 0 0 1 0 0 0 0 0 +// channel3: 0 0 0 1 0 0 0 0 +function createTestBuffer(numberOfChannels) { + var buffer = context.createBuffer(numberOfChannels, singleTestFrameLength, context.sampleRate); + for (var i = 0; i < numberOfChannels; ++i) { + var data = buffer.getChannelData(i); + data[i] = 1; + } + return buffer; +} + +// Discrete channel interpretation mixing: +// https://dvcs.w3.org/hg/audio/raw-file/tip/webaudio/specification.html#UpMix +// up-mix by filling channels until they run out then ignore remaining dest channels. +// down-mix by filling as many channels as possible, then dropping remaining source channels. +function discreteSum(sourceBuffer, destBuffer) { + if (sourceBuffer.length != destBuffer.length) { + is(sourceBuffer.length, destBuffer.length, "source and destination buffers should have the same length"); + } + + var numberOfChannels = Math.min(sourceBuffer.numberOfChannels, destBuffer.numberOfChannels); + var length = sourceBuffer.length; + + for (var c = 0; c < numberOfChannels; ++c) { + var source = sourceBuffer.getChannelData(c); + var dest = destBuffer.getChannelData(c); + for (var i = 0; i < length; ++i) { + dest[i] += source[i]; + } + } +} + +// Speaker channel interpretation mixing: +// https://dvcs.w3.org/hg/audio/raw-file/tip/webaudio/specification.html#UpMix +// eslint-disable-next-line complexity +function speakersSum(sourceBuffer, destBuffer) +{ + var numberOfSourceChannels = sourceBuffer.numberOfChannels; + var numberOfDestinationChannels = destBuffer.numberOfChannels; + var length = destBuffer.length; + + if ((numberOfDestinationChannels == 2 && numberOfSourceChannels == 1) || + (numberOfDestinationChannels == 4 && numberOfSourceChannels == 1)) { + // Handle mono -> stereo/Quad case (summing mono channel into both left and right). + var source = sourceBuffer.getChannelData(0); + var destL = destBuffer.getChannelData(0); + var destR = destBuffer.getChannelData(1); + + for (var i = 0; i < length; ++i) { + destL[i] += source[i]; + destR[i] += source[i]; + } + } else if ((numberOfDestinationChannels == 4 && numberOfSourceChannels == 2) || + (numberOfDestinationChannels == 6 && numberOfSourceChannels == 2)) { + // Handle stereo -> Quad/5.1 case (summing left and right channels into the output's left and right). + var sourceL = sourceBuffer.getChannelData(0); + var sourceR = sourceBuffer.getChannelData(1); + var destL = destBuffer.getChannelData(0); + var destR = destBuffer.getChannelData(1); + + for (var i = 0; i < length; ++i) { + destL[i] += sourceL[i]; + destR[i] += sourceR[i]; + } + } else if (numberOfDestinationChannels == 1 && numberOfSourceChannels == 2) { + // Handle stereo -> mono case. output += 0.5 * (input.L + input.R). + var sourceL = sourceBuffer.getChannelData(0); + var sourceR = sourceBuffer.getChannelData(1); + var dest = destBuffer.getChannelData(0); + + for (var i = 0; i < length; ++i) { + dest[i] += 0.5 * (sourceL[i] + sourceR[i]); + } + } else if (numberOfDestinationChannels == 1 && numberOfSourceChannels == 4) { + // Handle Quad -> mono case. output += 0.25 * (input.L + input.R + input.SL + input.SR). + var sourceL = sourceBuffer.getChannelData(0); + var sourceR = sourceBuffer.getChannelData(1); + var sourceSL = sourceBuffer.getChannelData(2); + var sourceSR = sourceBuffer.getChannelData(3); + var dest = destBuffer.getChannelData(0); + + for (var i = 0; i < length; ++i) { + dest[i] += 0.25 * (sourceL[i] + sourceR[i] + sourceSL[i] + sourceSR[i]); + } + } else if (numberOfDestinationChannels == 2 && numberOfSourceChannels == 4) { + // Handle Quad -> stereo case. outputLeft += 0.5 * (input.L + input.SL), + // outputRight += 0.5 * (input.R + input.SR). + var sourceL = sourceBuffer.getChannelData(0); + var sourceR = sourceBuffer.getChannelData(1); + var sourceSL = sourceBuffer.getChannelData(2); + var sourceSR = sourceBuffer.getChannelData(3); + var destL = destBuffer.getChannelData(0); + var destR = destBuffer.getChannelData(1); + + for (var i = 0; i < length; ++i) { + destL[i] += 0.5 * (sourceL[i] + sourceSL[i]); + destR[i] += 0.5 * (sourceR[i] + sourceSR[i]); + } + } else if (numberOfDestinationChannels == 6 && numberOfSourceChannels == 4) { + // Handle Quad -> 5.1 case. outputLeft += (inputL, inputR, 0, 0, inputSL, inputSR) + var sourceL = sourceBuffer.getChannelData(0); + var sourceR = sourceBuffer.getChannelData(1); + var sourceSL = sourceBuffer.getChannelData(2); + var sourceSR = sourceBuffer.getChannelData(3); + var destL = destBuffer.getChannelData(0); + var destR = destBuffer.getChannelData(1); + var destSL = destBuffer.getChannelData(4); + var destSR = destBuffer.getChannelData(5); + + for (var i = 0; i < length; ++i) { + destL[i] += sourceL[i]; + destR[i] += sourceR[i]; + destSL[i] += sourceSL[i]; + destSR[i] += sourceSR[i]; + } + } else if (numberOfDestinationChannels == 6 && numberOfSourceChannels == 1) { + // Handle mono -> 5.1 case, sum mono channel into center. + var source = sourceBuffer.getChannelData(0); + var dest = destBuffer.getChannelData(2); + + for (var i = 0; i < length; ++i) { + dest[i] += source[i]; + } + } else if (numberOfDestinationChannels == 1 && numberOfSourceChannels == 6) { + // Handle 5.1 -> mono. + var sourceL = sourceBuffer.getChannelData(0); + var sourceR = sourceBuffer.getChannelData(1); + var sourceC = sourceBuffer.getChannelData(2); + // skip LFE for now, according to current spec. + var sourceSL = sourceBuffer.getChannelData(4); + var sourceSR = sourceBuffer.getChannelData(5); + var dest = destBuffer.getChannelData(0); + + for (var i = 0; i < length; ++i) { + dest[i] += 0.7071 * (sourceL[i] + sourceR[i]) + sourceC[i] + 0.5 * (sourceSL[i] + sourceSR[i]); + } + } else if (numberOfDestinationChannels == 2 && numberOfSourceChannels == 6) { + // Handle 5.1 -> stereo. + var sourceL = sourceBuffer.getChannelData(0); + var sourceR = sourceBuffer.getChannelData(1); + var sourceC = sourceBuffer.getChannelData(2); + // skip LFE for now, according to current spec. + var sourceSL = sourceBuffer.getChannelData(4); + var sourceSR = sourceBuffer.getChannelData(5); + var destL = destBuffer.getChannelData(0); + var destR = destBuffer.getChannelData(1); + + for (var i = 0; i < length; ++i) { + destL[i] += sourceL[i] + 0.7071 * (sourceC[i] + sourceSL[i]); + destR[i] += sourceR[i] + 0.7071 * (sourceC[i] + sourceSR[i]); + } + } else if (numberOfDestinationChannels == 4 && numberOfSourceChannels == 6) { + // Handle 5.1 -> Quad. + var sourceL = sourceBuffer.getChannelData(0); + var sourceR = sourceBuffer.getChannelData(1); + var sourceC = sourceBuffer.getChannelData(2); + // skip LFE for now, according to current spec. + var sourceSL = sourceBuffer.getChannelData(4); + var sourceSR = sourceBuffer.getChannelData(5); + var destL = destBuffer.getChannelData(0); + var destR = destBuffer.getChannelData(1); + var destSL = destBuffer.getChannelData(2); + var destSR = destBuffer.getChannelData(3); + + for (var i = 0; i < length; ++i) { + destL[i] += sourceL[i] + 0.7071 * sourceC[i]; + destR[i] += sourceR[i] + 0.7071 * sourceC[i]; + destSL[i] += sourceSL[i]; + destSR[i] += sourceSR[i]; + } + } else { + // Fallback for unknown combinations. + discreteSum(sourceBuffer, destBuffer); + } +} + +function scheduleTest(testNumber, connections, channelCount, channelCountMode, channelInterpretation) { + var mixNode = context.createGain(); + mixNode.channelCount = channelCount; + mixNode.channelCountMode = channelCountMode; + mixNode.channelInterpretation = channelInterpretation; + mixNode.connect(sp); + + for (var i = 0; i < connections.length; ++i) { + var connectionNumberOfChannels = connections.charCodeAt(i) - "0".charCodeAt(0); + + var source = context.createBufferSource(); + // Get a buffer with the right number of channels, converting from 1-based to 0-based index. + var buffer = testBuffers[connectionNumberOfChannels - 1]; + source.buffer = buffer; + source.connect(mixNode); + + // Start at the right offset. + var sampleFrameOffset = testNumber * singleTestFrameLength; + var time = sampleFrameOffset / context.sampleRate; + source.start(time); + } +} + +function computeNumberOfChannels(connections, channelCount, channelCountMode) { + if (channelCountMode == "explicit") + return channelCount; + + var computedNumberOfChannels = 1; // Must have at least one channel. + + // Compute "computedNumberOfChannels" based on all the connections. + for (var i = 0; i < connections.length; ++i) { + var connectionNumberOfChannels = connections.charCodeAt(i) - "0".charCodeAt(0); + computedNumberOfChannels = Math.max(computedNumberOfChannels, connectionNumberOfChannels); + } + + if (channelCountMode == "clamped-max") + computedNumberOfChannels = Math.min(computedNumberOfChannels, channelCount); + + return computedNumberOfChannels; +} + +function checkTestResult(renderedBuffer, testNumber, connections, channelCount, channelCountMode, channelInterpretation) { + var computedNumberOfChannels = computeNumberOfChannels(connections, channelCount, channelCountMode); + + // Create a zero-initialized silent AudioBuffer with computedNumberOfChannels. + var destBuffer = context.createBuffer(computedNumberOfChannels, singleTestFrameLength, context.sampleRate); + + // Mix all of the connections into the destination buffer. + for (var i = 0; i < connections.length; ++i) { + var connectionNumberOfChannels = connections.charCodeAt(i) - "0".charCodeAt(0); + var sourceBuffer = testBuffers[connectionNumberOfChannels - 1]; // convert from 1-based to 0-based index + + if (channelInterpretation == "speakers") { + speakersSum(sourceBuffer, destBuffer); + } else if (channelInterpretation == "discrete") { + discreteSum(sourceBuffer, destBuffer); + } else { + ok(false, "Invalid channel interpretation!"); + } + } + + // Validate that destBuffer matches the rendered output. + // We need to check the rendered output at a specific sample-frame-offset corresponding + // to the specific test case we're checking for based on testNumber. + + var sampleFrameOffset = testNumber * singleTestFrameLength; + for (var c = 0; c < renderNumberOfChannels; ++c) { + var renderedData = renderedBuffer.getChannelData(c); + for (var frame = 0; frame < singleTestFrameLength; ++frame) { + var renderedValue = renderedData[frame + sampleFrameOffset]; + + var expectedValue = 0; + if (c < destBuffer.numberOfChannels) { + var expectedData = destBuffer.getChannelData(c); + expectedValue = expectedData[frame]; + } + + if (Math.abs(renderedValue - expectedValue) > 1e-4) { + var s = "connections: " + connections + ", " + channelCountMode; + + // channelCount is ignored in "max" mode. + if (channelCountMode == "clamped-max" || channelCountMode == "explicit") { + s += "(" + channelCount + ")"; + } + + s += ", " + channelInterpretation + ". "; + + var message = s + "rendered: " + renderedValue + " expected: " + expectedValue + " channel: " + c + " frame: " + frame; + is(renderedValue, expectedValue, message); + } + } + } +} + +function checkResult(event) { + var buffer = event.inputBuffer; + + // Sanity check result. + ok(buffer.length != numberOfTests * singleTestFrameLength || + buffer.numberOfChannels != renderNumberOfChannels, "Sanity check"); + + // Check all the tests. + var testNumber = 0; + for (var m = 0; m < mixingRulesList.length; ++m) { + var mixingRules = mixingRulesList[m]; + for (var i = 0; i < connectionsList.length; ++i, ++testNumber) { + checkTestResult(buffer, testNumber, connectionsList[i], mixingRules.channelCount, mixingRules.channelCountMode, mixingRules.channelInterpretation); + } + } + + sp.onaudioprocess = null; + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +function runTest() { + // Create 8-channel offline audio context. + // Each test will render 8 sample-frames starting at sample-frame position testNumber * 8. + var totalFrameLength = numberOfTests * singleTestFrameLength; + context = new AudioContext(); + var nextPowerOfTwo = 256; + while (nextPowerOfTwo < totalFrameLength) { + nextPowerOfTwo *= 2; + } + sp = context.createScriptProcessor(nextPowerOfTwo, renderNumberOfChannels); + + // Set destination to discrete mixing. + sp.channelCount = renderNumberOfChannels; + sp.channelCountMode = "explicit"; + sp.channelInterpretation = "discrete"; + + // Create test buffers from 1 to 8 channels. + testBuffers = new Array(); + for (var i = 0; i < renderNumberOfChannels; ++i) { + testBuffers[i] = createTestBuffer(i + 1); + } + + // Schedule all the tests. + var testNumber = 0; + for (var m = 0; m < mixingRulesList.length; ++m) { + var mixingRules = mixingRulesList[m]; + for (var i = 0; i < connectionsList.length; ++i, ++testNumber) { + scheduleTest(testNumber, connectionsList[i], mixingRules.channelCount, mixingRules.channelCountMode, mixingRules.channelInterpretation); + } + } + + // Render then check results. + sp.onaudioprocess = checkResult; +} + +runTest(); + +</script> + +</body> +</html> diff --git a/dom/media/webaudio/test/test_nodeCreationDocumentGone.html b/dom/media/webaudio/test/test_nodeCreationDocumentGone.html new file mode 100644 index 0000000000..07a4f7a97d --- /dev/null +++ b/dom/media/webaudio/test/test_nodeCreationDocumentGone.html @@ -0,0 +1,34 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test whether we can create an AudioContext interface</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +SimpleTest.requestCompleteLog(); +SimpleTest.waitForExplicitFinish(); + +var a = window.open("file_nodeCreationDocumentGone.html"); +a.onbeforeunload = function() { + setTimeout(function(){ + try { + a.context.createScriptProcessor(512, 1, 1); + } catch(e) { + ok (true,"got exception"); + } + setTimeout(function() { + ok (true,"no crash"); + SimpleTest.finish(); + }, 0); + }, 0); +} + + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_nodeToParamConnection.html b/dom/media/webaudio/test/test_nodeToParamConnection.html new file mode 100644 index 0000000000..49098eaffe --- /dev/null +++ b/dom/media/webaudio/test/test_nodeToParamConnection.html @@ -0,0 +1,58 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test connecting an AudioNode to an AudioParam</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +var gTest = { + length: 2048, + createGraph(context) { + var sourceBuffer = context.createBuffer(2, 2048, context.sampleRate); + for (var i = 0; i < 2048; ++i) { + sourceBuffer.getChannelData(0)[i] = 1; + sourceBuffer.getChannelData(1)[i] = -1; + } + + var paramSource = context.createBufferSource(); + paramSource.buffer = this.buffer; + + var source = context.createBufferSource(); + source.buffer = sourceBuffer; + + var gain = context.createGain(); + + paramSource.connect(gain.gain); + source.connect(gain); + + paramSource.start(0); + source.start(0); + return gain; + }, + createExpectedBuffers(context) { + this.buffer = context.createBuffer(2, 2048, context.sampleRate); + for (var i = 0; i < 2048; ++i) { + for (var j = 0; j < 2; ++j) { + this.buffer.getChannelData(j)[i] = Math.sin(440 * 2 * (j + 1) * Math.PI * i / context.sampleRate); + } + } + var expectedBuffer = context.createBuffer(2, 2048, context.sampleRate); + for (var i = 0; i < 2048; ++i) { + expectedBuffer.getChannelData(0)[i] = 1 + (this.buffer.getChannelData(0)[i] + this.buffer.getChannelData(1)[i]) / 2; + expectedBuffer.getChannelData(1)[i] = -(1 + (this.buffer.getChannelData(0)[i] + this.buffer.getChannelData(1)[i]) / 2); + } + return expectedBuffer; + }, +}; + +runTest(); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_notAllowedToStartAudioContextGC.html b/dom/media/webaudio/test/test_notAllowedToStartAudioContextGC.html new file mode 100644 index 0000000000..b8715c1644 --- /dev/null +++ b/dom/media/webaudio/test/test_notAllowedToStartAudioContextGC.html @@ -0,0 +1,57 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test GC for not-allow-to-start audio context</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +SimpleTest.requestFlakyTimeout(`Checking that something does not happen`); + +SimpleTest.waitForExplicitFinish(); + +var destId; + +function observer(subject, topic, data) { + let id = parseInt(data); + ok(id != destId, "dropping another node, not the context's destination"); +} + +SpecialPowers.addAsyncObserver(observer, "webaudio-node-demise", false); +SimpleTest.registerCleanupFunction(function() { + SpecialPowers.removeAsyncObserver(observer, "webaudio-node-demise"); +}); + +SpecialPowers.pushPrefEnv({"set": [["media.autoplay.default", SpecialPowers.Ci.nsIAutoplay.BLOCKED], + ["media.autoplay.blocking_policy", 0]]}, + startTest); + +function startTest() { + info("- create audio context -"); + let ac = new AudioContext(); + + info("- get node Id -"); + destId = SpecialPowers.getPrivilegedProps(ac.destination, "id"); + + info("- trigger GCs -"); + SpecialPowers.forceGC(); + SpecialPowers.forceCC(); + SpecialPowers.forceGC(); + + info("- after three GCs -"); + + // We're doing this async so that we can receive observerservice messages. + setTimeout(function() { + ok(true, `AudioContext that has been prevented + from starting has correctly survived GC`) + SimpleTest.finish(); + }, 1); +} + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_offlineDestinationChannelCountLess.html b/dom/media/webaudio/test/test_offlineDestinationChannelCountLess.html new file mode 100644 index 0000000000..8ff1deac4b --- /dev/null +++ b/dom/media/webaudio/test/test_offlineDestinationChannelCountLess.html @@ -0,0 +1,42 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test OfflineAudioContext with a channel count less than the specified number</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + var ctx = new OfflineAudioContext(2, 100, 22050); + + var buf = ctx.createBuffer(6, 100, ctx.sampleRate); + for (var i = 0; i < 6; ++i) { + for (var j = 0; j < 100; ++j) { + buf.getChannelData(i)[j] = Math.sin(2 * Math.PI * 200 * j / ctx.sampleRate); + } + } + + var src = ctx.createBufferSource(); + src.buffer = buf; + src.start(0); + src.connect(ctx.destination); + ctx.destination.channelCountMode = "max"; + ctx.startRendering(); + ctx.oncomplete = function(e) { + is(e.renderedBuffer.numberOfChannels, 2, "Correct expected number of buffers"); + compareChannels(e.renderedBuffer.getChannelData(0), buf.getChannelData(0)); + compareChannels(e.renderedBuffer.getChannelData(1), buf.getChannelData(1)); + + SimpleTest.finish(); + }; +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_offlineDestinationChannelCountMore.html b/dom/media/webaudio/test/test_offlineDestinationChannelCountMore.html new file mode 100644 index 0000000000..fa38114e2b --- /dev/null +++ b/dom/media/webaudio/test/test_offlineDestinationChannelCountMore.html @@ -0,0 +1,46 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test OfflineAudioContext with a channel count less than the specified number</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + var ctx = new OfflineAudioContext(6, 100, 22050); + + var buf = ctx.createBuffer(2, 100, ctx.sampleRate); + for (var i = 0; i < 2; ++i) { + for (var j = 0; j < 100; ++j) { + buf.getChannelData(i)[j] = Math.sin(2 * Math.PI * 200 * j / ctx.sampleRate); + } + } + var emptyBuffer = ctx.createBuffer(1, 100, ctx.sampleRate); + + var src = ctx.createBufferSource(); + src.buffer = buf; + src.start(0); + src.connect(ctx.destination); + ctx.destination.channelCountMode = "max"; + ctx.startRendering(); + ctx.oncomplete = function(e) { + is(e.renderedBuffer.numberOfChannels, 6, "Correct expected number of buffers"); + compareChannels(e.renderedBuffer.getChannelData(0), buf.getChannelData(0)); + compareChannels(e.renderedBuffer.getChannelData(1), buf.getChannelData(1)); + for (var i = 2; i < 6; ++i) { + compareChannels(e.renderedBuffer.getChannelData(i), emptyBuffer.getChannelData(0)); + } + + SimpleTest.finish(); + }; +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_oscillatorNode.html b/dom/media/webaudio/test/test_oscillatorNode.html new file mode 100644 index 0000000000..e2a47a4e1e --- /dev/null +++ b/dom/media/webaudio/test/test_oscillatorNode.html @@ -0,0 +1,60 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test the OscillatorNode interface</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + + var context = new AudioContext(); + var osc = new OscillatorNode(context); + + is(osc.channelCount, 2, "Oscillator node has 2 input channels by default"); + is(osc.channelCountMode, "max", "Correct channelCountMode for the Oscillator node"); + is(osc.channelInterpretation, "speakers", "Correct channelCountInterpretation for the Oscillator node"); + is(osc.type, "sine", "Correct default type"); + expectException(function() { + osc.type = "custom"; + }, DOMException.INVALID_STATE_ERR); + is(osc.type, "sine", "Cannot set the type to custom"); + is(osc.frequency.value, 440, "Correct default frequency value"); + is(osc.detune.value, 0, "Correct default detine value"); + + // Make sure that we can set all of the valid type values + var types = [ + "sine", + "square", + "sawtooth", + "triangle", + ]; + for (var i = 0; i < types.length; ++i) { + osc.type = types[i]; + } + + // Verify setPeriodicWave() + var real = new Float32Array([1.0, 0.5, 0.25, 0.125]); + var imag = new Float32Array([1.0, 0.7, -1.0, 0.5]); + osc.setPeriodicWave(context.createPeriodicWave(real, imag)); + is(osc.type, "custom", "Failed to set custom waveform"); + + expectNoException(function() { + osc.start(); + }); + expectNoException(function() { + osc.stop(); + }); + + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_oscillatorNode2.html b/dom/media/webaudio/test/test_oscillatorNode2.html new file mode 100644 index 0000000000..69a6655ff1 --- /dev/null +++ b/dom/media/webaudio/test/test_oscillatorNode2.html @@ -0,0 +1,53 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test OscillatorNode lifetime and sine phase</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script src="webaudio.js" type="text/javascript"></script> +<script class="testbody" type="text/javascript"> + +const signalLength = 2048; + +function createOscillator(context) { + var osc = context.createOscillator(); + osc.start(0); + osc.stop(signalLength/context.sampleRate); + return osc; +} + +function connectUnreferencedOscillator(context, destination) { + var osc = createOscillator(context); + osc.connect(destination); +} + +var gTest = { + length: signalLength, + numberOfChannels: 1, + createGraph(context) { + var blend = context.createGain(); + + connectUnreferencedOscillator(context, blend); + // Test that the unreferenced oscillator remains alive until it has finished. + SpecialPowers.forceGC(); + SpecialPowers.forceCC(); + + // Create another sine wave oscillator in negative time, which should + // cancel when mixed with the unreferenced oscillator. + var oscillator = createOscillator(context); + oscillator.frequency.value = -440; + oscillator.connect(blend); + + return blend; + }, +}; + +runTest(); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_oscillatorNodeNegativeFrequency.html b/dom/media/webaudio/test/test_oscillatorNodeNegativeFrequency.html new file mode 100644 index 0000000000..c46c0fea13 --- /dev/null +++ b/dom/media/webaudio/test/test_oscillatorNodeNegativeFrequency.html @@ -0,0 +1,50 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test the OscillatorNode when the frequency is negative</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + + var types = ["sine", + "square", + "sawtooth", + "triangle"]; + + var finished = 0; + function finish() { + if (++finished == types.length) { + SimpleTest.finish(); + } + } + + types.forEach(function(t) { + var context = new OfflineAudioContext(1, 256, 44100); + var osc = context.createOscillator(); + + osc.frequency.value = -440; + osc.type = t; + + osc.connect(context.destination); + osc.start(); + context.startRendering().then(function(buffer) { + var samples = buffer.getChannelData(0); + // This samples the wave form in the middle of the first period, the value + // should be negative. + ok(samples[Math.floor(44100 / 440 / 4)] < 0., "Phase should be inverted when using a " + t + " waveform"); + finish(); + }); + }); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_oscillatorNodePassThrough.html b/dom/media/webaudio/test/test_oscillatorNodePassThrough.html new file mode 100644 index 0000000000..63c0848d06 --- /dev/null +++ b/dom/media/webaudio/test/test_oscillatorNodePassThrough.html @@ -0,0 +1,43 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test Oscillator with passthrough</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +var gTest = { + length: 2048, + numberOfChannels: 1, + createGraph(context) { + var buffer = context.createBuffer(1, 2048, context.sampleRate); + for (var i = 0; i < 2048; ++i) { + buffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate); + } + + var source = context.createOscillator(); + + var srcWrapped = SpecialPowers.wrap(source); + ok("passThrough" in srcWrapped, "OscillatorNode should support the passThrough API"); + srcWrapped.passThrough = true; + + source.start(0); + return source; + }, + createExpectedBuffers(context) { + var expectedBuffer = context.createBuffer(1, 2048, context.sampleRate); + + return [expectedBuffer]; + }, +}; + +runTest(); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_oscillatorNodeStart.html b/dom/media/webaudio/test/test_oscillatorNodeStart.html new file mode 100644 index 0000000000..4df129170f --- /dev/null +++ b/dom/media/webaudio/test/test_oscillatorNodeStart.html @@ -0,0 +1,38 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test the OscillatorNode interface</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + + var context = new AudioContext(); + var osc = context.createOscillator(); + var sp = context.createScriptProcessor(0, 1, 0); + + osc.connect(sp); + + sp.onaudioprocess = function (e) { + var input = e.inputBuffer.getChannelData(0); + var isSilent = true; + for (var i = 0; i < input.length; i++) { + if (input[i] != 0.0) { + isSilent = false; + } + } + sp.onaudioprocess = null; + ok(isSilent, "OscillatorNode should be silent before calling start."); + SimpleTest.finish(); + } +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_oscillatorTypeChange.html b/dom/media/webaudio/test/test_oscillatorTypeChange.html new file mode 100644 index 0000000000..e4b4944703 --- /dev/null +++ b/dom/media/webaudio/test/test_oscillatorTypeChange.html @@ -0,0 +1,58 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test OscillatorNode type change after it has started and triangle phase</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); + +const bufferSize = 1024; + +function startTest() { + var ctx = new AudioContext(); + + var oscillator1 = ctx.createOscillator(); + oscillator1.connect(ctx.destination); + oscillator1.start(0); + + // Assuming the above Web Audio operations have already scheduled an event + // to run in stable state and start the graph thread, schedule a subsequent + // event to change the type of oscillator1. + SimpleTest.executeSoon(function() { + oscillator1.type = "triangle"; + + // Another triangle wave with -1 gain should cancel the first. This is + // starting at the same time as the type change, assuming that the phase + // is reset on type change. A negative frequency should achieve the same + // as the -1 gain but for bug 916285. + var oscillator2 = ctx.createOscillator(); + oscillator2.type = "triangle"; + oscillator2.start(0); + + var processor = ctx.createScriptProcessor(bufferSize, 1, 0); + oscillator1.connect(processor); + var gain = ctx.createGain(); + gain.gain.value = -1; + gain.connect(processor); + oscillator2.connect(gain); + + processor.onaudioprocess = function(e) { + compareChannels(e.inputBuffer.getChannelData(0), + new Float32Array(bufferSize)); + e.target.onaudioprocess = null; + SimpleTest.finish(); + } + }); +}; + +startTest(); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_pannerNode.html b/dom/media/webaudio/test/test_pannerNode.html new file mode 100644 index 0000000000..7f4d3ea915 --- /dev/null +++ b/dom/media/webaudio/test/test_pannerNode.html @@ -0,0 +1,71 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test PannerNode</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +function near(a, b, msg) { + ok(Math.abs(a - b) < 1e-4, msg); +} + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + var context = new AudioContext(); + var buffer = context.createBuffer(1, 2048, context.sampleRate); + for (var i = 0; i < 2048; ++i) { + buffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate); + } + + var destination = context.destination; + + var source = context.createBufferSource(); + + var panner = new PannerNode(context); + + source.buffer = buffer; + + source.connect(panner); + panner.connect(destination); + + // Verify default values + is(panner.panningModel, "equalpower", "Correct default value for panning model"); + is(panner.distanceModel, "inverse", "Correct default value for distance model"); + near(panner.refDistance, 1, "Correct default value for ref distance"); + near(panner.maxDistance, 10000, "Correct default value for max distance"); + near(panner.rolloffFactor, 1, "Correct default value for rolloff factor"); + near(panner.coneInnerAngle, 360, "Correct default value for cone inner angle"); + near(panner.coneOuterAngle, 360, "Correct default value for cone outer angle"); + near(panner.coneOuterGain, 0, "Correct default value for cone outer gain"); + is(panner.channelCount, 2, "panner node has 2 input channels by default"); + is(panner.channelCountMode, "clamped-max", "Correct channelCountMode for the panner node"); + is(panner.channelInterpretation, "speakers", "Correct channelCountInterpretation for the panner node"); + + panner.setPosition(1, 1, 1); + near(panner.positionX.value, 1, "setPosition sets AudioParam properly"); + near(panner.positionY.value, 1, "setPosition sets AudioParam properly"); + near(panner.positionZ.value, 1, "setPosition sets AudioParam properly"); + + panner.setOrientation(0, 1, 0); + near(panner.orientationX.value, 0, "setOrientation sets AudioParam properly"); + near(panner.orientationY.value, 1, "setOrientation sets AudioParam properly"); + near(panner.orientationZ.value, 0, "setOrientation sets AudioParam properly"); + + source.start(0); + SimpleTest.executeSoon(function() { + source.stop(0); + source.disconnect(); + panner.disconnect(); + + SimpleTest.finish(); + }); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_pannerNodeAbove.html b/dom/media/webaudio/test/test_pannerNodeAbove.html new file mode 100644 index 0000000000..5931fa04de --- /dev/null +++ b/dom/media/webaudio/test/test_pannerNodeAbove.html @@ -0,0 +1,50 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test PannerNode directly above</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script src="webaudio.js" type="text/javascript"></script> +<script class="testbody" type="text/javascript"> + +var gTest = { + numberOfChannels: 2, + createGraph(context) { + // An up vector will be made perpendicular to the front vector, in the + // front-up plane. + context.listener.setOrientation(0, 6.311749985202524e+307, 0, 0.1, 1000, 0); + // Linearly dependent vectors are ignored. + context.listener.setOrientation(0, 0, -6.311749985202524e+307, 0, 0, 6.311749985202524e+307); + var panner = context.createPanner(); + panner.positionX.value = 2; // directly above + panner.rolloffFactor = 0; // no distance gain + panner.panningModel = "equalpower"; // no effect when directly above + + var source = context.createBufferSource(); + source.buffer = this.buffer; + source.connect(panner); + source.start(0); + + return panner; + }, + createExpectedBuffers(context) { + var expectedBuffer = context.createBuffer(2, 2048, context.sampleRate); + for (var i = 0; i < 2048; ++i) { + // Different signals in left and right buffers + expectedBuffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate); + expectedBuffer.getChannelData(1)[i] = Math.sin(220 * 2 * Math.PI * i / context.sampleRate); + } + this.buffer = expectedBuffer; + return expectedBuffer; + }, +}; + +runTest(); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_pannerNodeAtZeroDistance.html b/dom/media/webaudio/test/test_pannerNodeAtZeroDistance.html new file mode 100644 index 0000000000..b5aff589ee --- /dev/null +++ b/dom/media/webaudio/test/test_pannerNodeAtZeroDistance.html @@ -0,0 +1,140 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test PannerNode produces output even when the even when the distance is from the listener is zero</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +const BUF_SIZE = 1024; +const sampleRate = 44100; +var types = [ + "equalpower", + "HRTF" +] + +async function testMono(panningModel) { + var ac = new OfflineAudioContext(2, BUF_SIZE, sampleRate); + + // A sine to be used to fill the buffers + function sine(t) { + return Math.sin(440 * 2 * Math.PI * t / ac.sampleRate); + } + + var monoBuffer = ac.createBuffer(1, BUF_SIZE, ac.sampleRate); + for (var i = 0; i < BUF_SIZE; ++i) { + monoBuffer.getChannelData(0)[i] = sine(i); + } + + var monoSource = ac.createBufferSource(); + monoSource.buffer = monoBuffer; + monoSource.start(0); + + var panner = new PannerNode(ac, { + panningModel, + distanceModel: "linear", + }); + monoSource.connect(panner); + + var panner2 = new PannerNode(ac, { + panningModel, + distanceModel: "inverse", + }); + panner.connect(panner2); + + var panner3 = new PannerNode(ac, { + panningModel, + distanceModel: "exponential" + }); + panner2.connect(panner3); + + panner3.connect(ac.destination); + + const buffer = await ac.startRendering(); + + if (panningModel == "equalpower") { + // Use the input buffer to compare the output. According to the spec, + // mono input at zero distance will apply gain = cos(0.5 * Math.PI / 2) + // https://webaudio.github.io/web-audio-api/#Spatialzation-equal-power-panning + const gain = Math.cos(0.5 * Math.PI / 2); + for (var i = 0; i < BUF_SIZE; ++i) { + monoBuffer.getChannelData(0)[i] = gain * monoBuffer.getChannelData(0)[i]; + } + compareChannels(buffer.getChannelData(0), monoBuffer.getChannelData(0)); + } else { + ok(!isChannelSilent(buffer.getChannelData(0)), + "mono panning: expect non-zero left channel"); + } + // Check symmetry + compareChannels(buffer.getChannelData(0), buffer.getChannelData(1)); +} + +async function testStereo(panningModel) { + var ac = new OfflineAudioContext(2, BUF_SIZE, sampleRate); + + // A sine to be used to fill the buffers + function sine(t) { + return Math.sin(440 * 2 * Math.PI * t / ac.sampleRate); + } + + var stereoBuffer = ac.createBuffer(2, BUF_SIZE, ac.sampleRate); + for (var i = 0; i < BUF_SIZE; ++i) { + stereoBuffer.getChannelData(0)[i] = sine(i); + stereoBuffer.getChannelData(1)[i] = sine(i); + } + + var stereoSource = ac.createBufferSource(); + stereoSource.buffer = stereoBuffer; + stereoSource.start(0); + + var panner = new PannerNode(ac, { + panningModel, + distanceModel: "linear", + }); + stereoSource.connect(panner); + + var panner2 = new PannerNode(ac, { + panningModel, + distanceModel: "inverse", + }); + panner.connect(panner2); + + var panner3 = new PannerNode(ac, { + panningModel, + distanceModel: "exponential", + }); + panner2.connect(panner3); + + panner3.connect(ac.destination); + + const buffer = await ac.startRendering(); + if (panningModel == "equalpower") { + compareBuffers(buffer, stereoBuffer); + } else { + ok(!isChannelSilent(buffer.getChannelData(0)), + "stereo panning: expect non-zero left channel"); + // Check symmetry + compareChannels(buffer.getChannelData(0), buffer.getChannelData(1)); + } +} + +async function test(type) { + await promiseHRTFReady(sampleRate); + await testMono(type) + await testStereo(type); +} + +add_task(async function() { + for (const panningModel of types) { + await test(panningModel); + } +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_pannerNodeChannelCount.html b/dom/media/webaudio/test/test_pannerNodeChannelCount.html new file mode 100644 index 0000000000..9cb90f32da --- /dev/null +++ b/dom/media/webaudio/test/test_pannerNodeChannelCount.html @@ -0,0 +1,52 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test PannerNode directly above</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script src="webaudio.js" type="text/javascript"></script> +<script class="testbody" type="text/javascript"> + +var gTest = { + length: 2048, + numberOfChannels: 2, + createGraph(context) { + var buffer = context.createBuffer(2, 2048, context.sampleRate); + for (var i = 0; i < 2048; ++i) { + var sample = Math.sin(440 * 2 * Math.PI * i / context.sampleRate); + // When mixed into a single channel, this produces silence + buffer.getChannelData(0)[i] = sample; + buffer.getChannelData(1)[i] = -sample; + } + + var panner = context.createPanner(); + panner.positionX.value = 1; + panner.positionY.value = 2; + panner.positionZ.value = 3; + panner.channelCount = 1; + expectException(function() { panner.channelCount = 3; }, + DOMException.NOT_SUPPORTED_ERR); + panner.channelCountMode = "explicit"; + expectException(function() { panner.channelCountMode = "max"; }, + DOMException.NOT_SUPPORTED_ERR); + panner.channelInterpretation = "discrete"; + panner.channelInterpretation = "speakers"; + + var source = context.createBufferSource(); + source.buffer = buffer; + source.connect(panner); + source.start(0); + + return panner; + }, +}; + +runTest(); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_pannerNodeHRTFSymmetry.html b/dom/media/webaudio/test/test_pannerNodeHRTFSymmetry.html new file mode 100644 index 0000000000..cd7b623e41 --- /dev/null +++ b/dom/media/webaudio/test/test_pannerNodeHRTFSymmetry.html @@ -0,0 +1,76 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test left/right symmetry and block-offset invariance of HRTF panner</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); + +const blockSize = 128; +const bufferSize = 4096; // > HRTF panner latency + +var ctx = new AudioContext(); + +function startTest() { + var leftPanner = ctx.createPanner(); + var rightPanner = ctx.createPanner(); + leftPanner.panningModel = "HRTF"; + rightPanner.panningModel = "HRTF"; + leftPanner.positionX.value = -1; + rightPanner.positionX.value = 1; + + // Test that PannerNode processes the signal consistently irrespective of + // the offset in the processing block. This is done by inserting a delay of + // less than a block size before one panner. + const delayTime = 0.7 * blockSize / ctx.sampleRate; + var leftDelay = ctx.createDelay(delayTime); + leftDelay.delayTime.value = delayTime; + leftDelay.connect(leftPanner); + // and compensating for the delay after the other. + var rightDelay = ctx.createDelay(delayTime); + rightDelay.delayTime.value = delayTime; + rightPanner.connect(rightDelay); + + // Feed the panners with a signal having some harmonics to fill the spectrum. + var oscillator = ctx.createOscillator(); + oscillator.frequency.value = 110; + oscillator.type = "sawtooth"; + oscillator.connect(leftDelay); + oscillator.connect(rightPanner); + oscillator.start(0); + + // Switch the channels on one panner output, and it should match the other. + var splitter = ctx.createChannelSplitter(); + leftPanner.connect(splitter); + var merger = ctx.createChannelMerger(); + splitter.connect(merger, 0, 1); + splitter.connect(merger, 1, 0); + + // Invert one signal so that mixing with the other will find the difference. + var gain = ctx.createGain(); + gain.gain.value = -1.0; + merger.connect(gain); + + var processor = ctx.createScriptProcessor(bufferSize, 2, 0); + gain.connect(processor); + rightDelay.connect(processor); + processor.onaudioprocess = + function(e) { + compareBuffers(e.inputBuffer, + ctx.createBuffer(2, bufferSize, ctx.sampleRate)); + e.target.onaudioprocess = null; + SimpleTest.finish(); + } +} + +promiseHRTFReady(ctx.sampleRate).then(startTest); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_pannerNodePassThrough.html b/dom/media/webaudio/test/test_pannerNodePassThrough.html new file mode 100644 index 0000000000..d8c809a2e2 --- /dev/null +++ b/dom/media/webaudio/test/test_pannerNodePassThrough.html @@ -0,0 +1,53 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test PannerNode with passthrough</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +var gTest = { + length: 2048, + numberOfChannels: 1, + createGraph(context) { + var source = context.createBufferSource(); + + var panner = context.createPanner(); + + source.buffer = this.buffer; + + source.connect(panner); + + context.listener.setOrientation(0, 6.311749985202524e+307, 0, 0.1, 1000, 0); + context.listener.setOrientation(0, 0, -6.311749985202524e+307, 0, 0, 6.311749985202524e+307); + panner.positionX = 2; + panner.rolloffFactor = 0; + panner.panningModel = "equalpower"; + + var pannerWrapped = SpecialPowers.wrap(panner); + ok("passThrough" in pannerWrapped, "PannerNode should support the passThrough API"); + pannerWrapped.passThrough = true; + + source.start(0); + return panner; + }, + createExpectedBuffers(context) { + this.buffer = context.createBuffer(1, 2048, context.sampleRate); + for (var i = 0; i < 2048; ++i) { + this.buffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate); + } + + return [this.buffer]; + }, +}; + +runTest(); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_pannerNodeTail.html b/dom/media/webaudio/test/test_pannerNodeTail.html new file mode 100644 index 0000000000..7035780bf2 --- /dev/null +++ b/dom/media/webaudio/test/test_pannerNodeTail.html @@ -0,0 +1,200 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test tail time lifetime of PannerNode</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +// This tests that a PannerNode does not release its reference before +// it finishes emitting sound. +// +// The PannerNode tail time is short, so, when a PannerNode is destroyed on +// the main thread, it is unlikely to notify the graph thread before the tail +// time expires. However, by adding DelayNodes downstream from the +// PannerNodes, the graph thread can have enough time to notice that a +// DelayNode has been destroyed. +// +// In the current implementation, DelayNodes will take a tail-time reference +// immediately when they receive the first block of sound from an upstream +// node, so this test connects the downstream DelayNodes while the upstream +// nodes are finishing, and then runs GC (on the main thread) before the +// DelayNodes receive any input (on the graph thread). +// +// Web Audio doesn't provide a means to precisely time connect()s but we can +// test that the output of delay nodes matches the output from a reference +// PannerNode that we know will not be GCed. +// +// Another set of delay nodes is added upstream to ensure that the source node +// has removed its self-reference after dispatching its "ended" event. + +SimpleTest.waitForExplicitFinish(); + +const blockSize = 128; +// bufferSize should be long enough that to allow an audioprocess event to be +// sent to the main thread and a connect message to return to the graph +// thread. +const bufferSize = 4096; +const pannerCount = bufferSize / blockSize; +// sourceDelayBufferCount should be long enough to allow the source node +// onended to finish and remove the source self-reference. +const sourceDelayBufferCount = 3; +var gotEnded = false; +// ccDelayLength should be long enough to allow CC to run +var ccDelayBufferCount = 20; +const ccDelayLength = ccDelayBufferCount * bufferSize; + +var ctx = new AudioContext(); +var testPanners = []; +var referencePanner = new PannerNode(ctx, {panningModel: "HRTF"}); +var referenceProcessCount = 0; +var referenceOutput = [new Float32Array(bufferSize), + new Float32Array(bufferSize)]; +var testProcessor; +var testProcessCount = 0; + +function onReferenceOutput(e) { + switch(referenceProcessCount) { + + case sourceDelayBufferCount - 1: + // The panners are about to finish. + if (!gotEnded) { + todo(false, "Source hasn't ended. Increase sourceDelayBufferCount?"); + } + + // Connect each PannerNode output to a downstream DelayNode, + // and connect ScriptProcessors to compare test and reference panners. + var delayDuration = ccDelayLength / ctx.sampleRate; + for (var i = 0; i < pannerCount; ++i) { + var delay = ctx.createDelay(delayDuration); + delay.delayTime.value = delayDuration; + delay.connect(testProcessor); + testPanners[i].connect(delay); + } + testProcessor = null; + testPanners = null; + + // The panning effect is linear so only one reference panner is required. + // This also checks that the individual panners don't chop their output + // too soon. + referencePanner.connect(e.target); + + // Assuming the above operations have already scheduled an event to run in + // stable state and ask the graph thread to make connections, schedule a + // subsequent event to run cycle collection, which should not collect + // panners that are still producing sound. + SimpleTest.executeSoon(function() { + SpecialPowers.forceGC(); + SpecialPowers.forceCC(); + }); + + break; + + case sourceDelayBufferCount: + // Record this buffer during which PannerNode outputs were connected. + for (var i = 0; i < 2; ++i) { + e.inputBuffer.copyFromChannel(referenceOutput[i], i); + } + e.target.onaudioprocess = null; + e.target.disconnect(); + + // If the buffer is silent, there is probably not much point just + // increasing the buffer size, because, with the buffer size already + // significantly larger than panner tail time, it demonstrates that the + // lag between threads is much greater than the tail time. + if (isChannelSilent(referenceOutput[0])) { + todo(false, "Connections not detected."); + } + } + + referenceProcessCount++; +} + +function onTestOutput(e) { + if (testProcessCount < sourceDelayBufferCount + ccDelayBufferCount) { + testProcessCount++; + return; + } + + for (var i = 0; i < 2; ++i) { + compareChannels(e.inputBuffer.getChannelData(i), referenceOutput[i]); + } + e.target.onaudioprocess = null; + e.target.disconnect(); + SimpleTest.finish(); +} + +function startTest() { + // 0.002 is MaxDelayTimeSeconds in HRTFpanner.cpp + // and 512 is fftSize() at 48 kHz. + const expectedPannerTailTime = 0.002 * ctx.sampleRate + 512; + + // Place the listener to the side of the origin, where the panners are + // positioned, to maximize delay in one ear. + ctx.listener.setPosition(1,0,0); + + // Create some PannerNodes downstream from DelayNodes with delays long + // enough for their source to finish, dispatch its "ended" event + // and release its playing reference. The DelayNodes should expire their + // tail-time references before the PannerNodes and so only the PannerNode + // lifetimes depends on their tail-time references. Many DelayNodes are + // created and timed to finish at different times so that one PannerNode + // will be finishing the block processed immediately after the connect is + // received. + var source = ctx.createBufferSource(); + // Just short of blockSize here to avoid rounding into the next block + var buffer = ctx.createBuffer(1, blockSize - 1, ctx.sampleRate); + for (var i = 0; i < buffer.length; ++i) { + buffer.getChannelData(0)[i] = Math.cos(Math.PI * i / buffer.length); + } + source.buffer = buffer; + source.start(0); + source.onended = function(e) { + gotEnded = true; + }; + + // Time the first test panner to finish just before downstream DelayNodes + // are about the be connected. Note that DelayNode lifetime depends on + // maxDelayTime so set that equal to the delay. + var delayDuration = + (sourceDelayBufferCount * bufferSize + - expectedPannerTailTime - 2 * blockSize) / ctx.sampleRate; + + for (var i = 0; i < pannerCount; ++i) { + var delay = ctx.createDelay(delayDuration); + delay.delayTime.value = delayDuration; + source.connect(delay); + delay.connect(referencePanner) + + var panner = ctx.createPanner(); + panner.panningModel = "HRTF"; + delay.connect(panner); + testPanners[i] = panner; + + delayDuration += blockSize / ctx.sampleRate; + } + + // Create a ScriptProcessor now to use as a timer to trigger connection of + // downstream nodes. It will also be used to record reference output. + var referenceProcessor = ctx.createScriptProcessor(bufferSize, 2, 0); + referenceProcessor.onaudioprocess = onReferenceOutput; + // Start audioprocess events before source delays are connected. + referenceProcessor.connect(ctx.destination); + + // The test ScriptProcessor will record output of testPanners. + // Create it now so that it is synchronized with the referenceProcessor. + testProcessor = ctx.createScriptProcessor(bufferSize, 2, 0); + testProcessor.onaudioprocess = onTestOutput; + // Start audioprocess events before source delays are connected. + testProcessor.connect(ctx.destination); +} + +promiseHRTFReady(ctx.sampleRate).then(startTest); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_pannerNode_audioparam_distance.html b/dom/media/webaudio/test/test_pannerNode_audioparam_distance.html new file mode 100644 index 0000000000..2d955de19d --- /dev/null +++ b/dom/media/webaudio/test/test_pannerNode_audioparam_distance.html @@ -0,0 +1,43 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Distance effect of a PannerNode with the position set via AudioParams (Bug 1472550)</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + SimpleTest.waitForExplicitFinish(); + var o = new OfflineAudioContext(2, 256, 44100); + + // We want a stereo constant source. + var b = o.createBuffer(2, 1, 44100); + b.getChannelData(0)[0] = 1; + b.getChannelData(1)[0] = 1; + var c = o.createBufferSource(); + c.buffer = b; + c.loop = true; + + var p = o.createPanner(); + p.positionY.setValueAtTime(1, 0); + p.positionX.setValueAtTime(1, 0); + p.positionZ.setValueAtTime(1, 0); + + // Set the listener somewhere far + o.listener.setPosition(20, 2, 20); + + c.start(); + c.connect(p).connect(o.destination); + + o.startRendering().then((ab) => { + // Check that the distance attenuates the sound. + ok(ab.getChannelData(0)[0] < 0.1, "left channel must be very quiet"); + ok(ab.getChannelData(1)[0] < 0.1, "right channel must be very quiet"); + SimpleTest.finish(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_pannerNode_equalPower.html b/dom/media/webaudio/test/test_pannerNode_equalPower.html new file mode 100644 index 0000000000..127a87b254 --- /dev/null +++ b/dom/media/webaudio/test/test_pannerNode_equalPower.html @@ -0,0 +1,26 @@ +<!DOCTYPE HTML> +<html> +<head> +<title>Test PannerNode</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script type="text/javascript" src="webaudio.js"></script> +<script type="text/javascript" src="layouttest-glue.js"></script> +<script type="text/javascript" src="blink/audio-testing.js"></script> +<script type="text/javascript" src="blink/panner-model-testing.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + function checkFinished() { + SimpleTest.finish(); + } + var ctx = new OfflineAudioContext(2, sampleRate * renderLengthSeconds, sampleRate); + createTestAndRun(ctx, nodesToCreate, 2, checkFinished); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_pannerNode_maxDistance.html b/dom/media/webaudio/test/test_pannerNode_maxDistance.html new file mode 100644 index 0000000000..bb70b0a0ec --- /dev/null +++ b/dom/media/webaudio/test/test_pannerNode_maxDistance.html @@ -0,0 +1,61 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test PannerNode outputs silence when the distance is greater than maxDist</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +// BUF_SIZE must be greater than HRTF delay. +const BUF_SIZE = 1024; +const sampleRate = 44100; +var types = [ + "equalpower", + "HRTF" +] + +async function test(panningModel) { + var ac = new OfflineAudioContext(1, 1024, sampleRate); + var osc = ac.createOscillator(); + var panner = new PannerNode(ac, { + panningModel, + distanceModel: "linear", + maxDistance: 100, + positionY: 200, + }); + + ac.listener.setPosition(0, 0, 0); + + osc.connect(panner); + panner.connect(ac.destination); + + osc.start(); + + const buffer = await ac.startRendering() + + var silence = true; + var array = buffer.getChannelData(0); + for (var i = 0; i < buffer.length; i++) { + if (array[i] != 0) { + ok(false, "Found noise in the buffer."); + silence = false; + } + } + ok(silence, "The buffer is silent."); +} + +add_task(async function() { + await promiseHRTFReady(sampleRate); + for (const panningModel of types) { + await test(panningModel); + } +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_periodicWave.html b/dom/media/webaudio/test/test_periodicWave.html new file mode 100644 index 0000000000..7b8a6ab12c --- /dev/null +++ b/dom/media/webaudio/test/test_periodicWave.html @@ -0,0 +1,130 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test the PeriodicWave interface</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); + +// real and imag are used in separate PeriodicWaves to make their peak values +// easy to determine. +const realMax = 99; +var real = new Float32Array(realMax + 1); +real[1] = 2.0; // fundamental +real[realMax] = 3.0; +const realPeak = real[1] + real[realMax]; +const realFundamental = 19.0; +var imag = new Float32Array(4); +imag[0] = 6.0; // should be ignored. +imag[3] = 0.5; +const imagPeak = imag[3]; +const imagFundamental = 551.0; + +const testLength = 4096; + +addLoadEvent(function() { + var ac = new AudioContext(); + ac.createPeriodicWave(new Float32Array(4096), new Float32Array(4096)); + expectException(function() { + ac.createPeriodicWave(new Float32Array(512), imag); + }, DOMException.INDEX_SIZE_ERR); + expectException(function() { + ac.createPeriodicWave(new Float32Array(0), new Float32Array(0)); + }, DOMException.INDEX_SIZE_ERR); + expectException(function() { + ac.createPeriodicWave(new Float32Array(1), new Float32Array(1)); + }, DOMException.INDEX_SIZE_ERR); + expectNoException(function() { + ac.createPeriodicWave(new Float32Array(4097), new Float32Array(4097)); + }); + + expectNoException(function() { + new PeriodicWave(ac, {}); + }); + + // real.size == imag.size + expectException(function() { + new PeriodicWave(ac, {real: new Float32Array(10), imag: new Float32Array(9)}); + }, DOMException.INDEX_SIZE_ERR); + + // size lower than 2 is not allowed + expectException(function() { + new PeriodicWave(ac, {real: new Float32Array(0)}); + }, DOMException.INDEX_SIZE_ERR); + expectException(function() { + new PeriodicWave(ac, {imag: new Float32Array(0)}); + }, DOMException.INDEX_SIZE_ERR); + expectException(function() { + new PeriodicWave(ac, {real: new Float32Array(1)}); + }, DOMException.INDEX_SIZE_ERR); + expectException(function() { + new PeriodicWave(ac, {imag: new Float32Array(1)}); + }, DOMException.INDEX_SIZE_ERR); + expectException(function() { + new PeriodicWave(ac, {real: new Float32Array(0), imag: new Float32Array(0)}); + }, DOMException.INDEX_SIZE_ERR); + expectException(function() { + new PeriodicWave(ac, {real: new Float32Array(1), imag: new Float32Array(1)}); + }, DOMException.INDEX_SIZE_ERR); + + new PeriodicWave(ac, {real: new Float32Array(4096), imag: new Float32Array(4096)}); + new PeriodicWave(ac, {real: new Float32Array(4096) }); + new PeriodicWave(ac, {imag: new Float32Array(4096) }); + + runTest(); +}); + +var gTest = { + createGraph(context) { + var merger = context.createChannelMerger(); + + var osc0 = context.createOscillator(); + var osc1 = context.createOscillator(); + + osc0.setPeriodicWave(context. + createPeriodicWave(real, + new Float32Array(real.length))); + osc1.setPeriodicWave(context. + createPeriodicWave(new Float32Array(imag.length), + imag)); + + osc0.frequency.value = realFundamental; + osc1.frequency.value = imagFundamental; + + osc0.start(); + osc1.start(); + + osc0.connect(merger, 0, 0); + osc1.connect(merger, 0, 1); + + return merger; + }, + createExpectedBuffers(context) { + var buffer = context.createBuffer(2, testLength, context.sampleRate); + + for (var i = 0; i < buffer.length; ++i) { + + buffer.getChannelData(0)[i] = 1.0 / realPeak * + (real[1] * Math.cos(2 * Math.PI * realFundamental * i / + context.sampleRate) + + real[realMax] * Math.cos(2 * Math.PI * realMax * realFundamental * i / + context.sampleRate)); + + buffer.getChannelData(1)[i] = 1.0 / imagPeak * + imag[3] * Math.sin(2 * Math.PI * 3 * imagFundamental * i / + context.sampleRate); + } + return buffer; + }, +}; + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_periodicWaveBandLimiting.html b/dom/media/webaudio/test/test_periodicWaveBandLimiting.html new file mode 100644 index 0000000000..a17328b965 --- /dev/null +++ b/dom/media/webaudio/test/test_periodicWaveBandLimiting.html @@ -0,0 +1,86 @@ +<!DOCTYPE html> +<title>Test effect of band limiting on PeriodicWave signals</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script> +const sampleRate = 48000; +const bufferSize = 12800; +const epsilon = 0.01; + +// "All implementations must support arrays up to at least 8192", but the +// linear interpolation of the current implementation distorts the higher +// frequency components too much to pass this test. +const frequencyIndexMax = 200; + +// A set of oscillators are created near the Nyquist frequency. +// These are factors giving each oscillator frequency relative to the Nyquist. +// The first is an octave below Nyquist and the last is just above. +const OCTAVE_BELOW = 0; +const HALF_BELOW = 1; +const NEAR_BELOW = 2; +const ABOVE = 3; +const oscillatorFactors = [0.5, Math.sqrt(0.5), 0.99, 1.01]; +const oscillatorCount = oscillatorFactors.length; + +// Return magnitude relative to unit sine wave +function magnitude(array) { + var mag = 0 + for (var i = 0; i < array.length; ++i) { + let sample = array[i]; + mag += sample * sample; + } + return Math.sqrt(2 * mag / array.length); +} + +function test_frequency_index(frequencyIndex) { + + var context = + new OfflineAudioContext(oscillatorCount, bufferSize, sampleRate); + + var merger = context.createChannelMerger(oscillatorCount); + merger.connect(context.destination); + + var real = new Float32Array(frequencyIndex + 1); + real[frequencyIndex] = 1; + var image = new Float32Array(real.length); + var wave = context.createPeriodicWave(real, image); + + for (var i = 0; i < oscillatorCount; ++i) { + var oscillator = context.createOscillator(); + oscillator.frequency.value = + oscillatorFactors[i] * sampleRate / (2 * frequencyIndex); + oscillator.connect(merger, 0, i); + oscillator.setPeriodicWave(wave); + oscillator.start(0); + } + + return context.startRendering(). + then((buffer) => { + assert_equals(buffer.numberOfChannels, oscillatorCount); + var magnitudes = []; + for (var i = 0; i < oscillatorCount; ++i) { + magnitudes[i] = magnitude(buffer.getChannelData(i)); + } + // Unaffected by band-limiting one octave below Nyquist. + assert_approx_equals(magnitudes[OCTAVE_BELOW], 1, epsilon, + "magnitude with frequency octave below Nyquist"); + // Still at least half the amplitude at half octave below Nyquist. + assert_greater_than(magnitudes[HALF_BELOW], 0.5 * (1 - epsilon), + "magnitude with frequency half octave below Nyquist"); + // Approaching zero or zero near Nyquist. + assert_less_than(magnitudes[NEAR_BELOW], 0.1, + "magnitude with frequency near Nyquist"); + assert_equals(magnitudes[ABOVE], 0, + "magnitude with frequency above Nyquist"); + }); +} + +// The 5/4 ratio with rounding up provides sampling across a range of +// octaves and offsets within octaves. +for (var frequencyIndex = 1; + frequencyIndex < frequencyIndexMax; + frequencyIndex = Math.floor((5 * frequencyIndex + 3) / 4)) { + promise_test(test_frequency_index.bind(null, frequencyIndex), + "Frequency " + frequencyIndex); +} +</script> diff --git a/dom/media/webaudio/test/test_periodicWaveDisableNormalization.html b/dom/media/webaudio/test/test_periodicWaveDisableNormalization.html new file mode 100644 index 0000000000..229d48282e --- /dev/null +++ b/dom/media/webaudio/test/test_periodicWaveDisableNormalization.html @@ -0,0 +1,98 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test PeriodicWave disableNormalization Parameter</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); + +// We create PerodicWave instances containing two tones and compare it to +// buffers created directly in JavaScript by adding the two waves together. +// Two of the PeriodicWaves are normalized, the other is not. This test is +// a modification of test_periodicWave.html. +// +// These constants are borrowed from test_periodicWave.html and modified +// so that the realPeak (which is the normalization factor) will be small +// enough that the errors are within the bounds for the test. +const realMax = 99; +var real = new Float32Array(realMax + 1); +real[1] = 2.0; // fundamental +real[realMax] = 0.25; + +const realPeak = real[1] + real[realMax]; +const realFundamental = 19.0; + +const testLength = 4096; + +addLoadEvent(function() { + runTest(); +}); + +var gTest = { + createGraph(context) { + var merger = context.createChannelMerger(); + + var osc0 = context.createOscillator(); + var osc1 = context.createOscillator(); + var osc2 = context.createOscillator(); + + osc0.setPeriodicWave(context. + createPeriodicWave(real, + new Float32Array(real.length), + {disableNormalization: false})); + osc1.setPeriodicWave(context. + createPeriodicWave(real, + new Float32Array(real.length))); + osc2.setPeriodicWave(context. + createPeriodicWave(real, + new Float32Array(real.length), + {disableNormalization: true})); + + osc0.frequency.value = realFundamental; + osc1.frequency.value = realFundamental; + osc2.frequency.value = realFundamental; + + osc0.start(); + osc1.start(); + osc2.start(); + + osc0.connect(merger, 0, 0); + osc1.connect(merger, 0, 1); + osc2.connect(merger, 0, 2); + + return merger; + }, + createExpectedBuffers(context) { + var buffer = context.createBuffer(3, testLength, context.sampleRate); + + for (var i = 0; i < buffer.length; ++i) { + + buffer.getChannelData(0)[i] = 1.0 / realPeak * + (real[1] * Math.cos(2 * Math.PI * realFundamental * i / + context.sampleRate) + + real[realMax] * Math.cos(2 * Math.PI * realMax * realFundamental * i / + context.sampleRate)); + + buffer.getChannelData(1)[i] = buffer.getChannelData(0)[i]; + + buffer.getChannelData(2)[i] = + (real[1] * Math.cos(2 * Math.PI * realFundamental * i / + context.sampleRate) + + real[realMax] * Math.cos(2 * Math.PI * realMax * realFundamental * i / + context.sampleRate)); + } + return buffer; + }, + 'numberOfChannels': 3, +}; + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_retrospective-exponentialRampToValueAtTime.html b/dom/media/webaudio/test/test_retrospective-exponentialRampToValueAtTime.html new file mode 100644 index 0000000000..20d3d59faf --- /dev/null +++ b/dom/media/webaudio/test/test_retrospective-exponentialRampToValueAtTime.html @@ -0,0 +1,51 @@ +<!doctype html> +<meta charset=utf-8> +<title>Test exponentialRampToValue with end time in the past</title> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script> +function do_test(t, context) { + var source = context.createConstantSource(); + source.start(); + + var test = context.createGain(); + test.gain.exponentialRampToValueAtTime(0.1, 0.5*context.currentTime); + test.gain.exponentialRampToValueAtTime(0.9, 2.0); + + var reference = context.createGain(); + reference.gain.exponentialRampToValueAtTime(0.1, context.currentTime); + reference.gain.exponentialRampToValueAtTime(0.9, 2.0); + + source.connect(test); + source.connect(reference); + + var merger = context.createChannelMerger(); + test.connect(merger, 0, 0); + reference.connect(merger, 0, 1); + + var processor = context.createScriptProcessor(0, 2, 0); + merger.connect(processor); + processor.onaudioprocess = + t.step_func_done((e) => { + source.stop(); + processor.onaudioprocess = null; + + var testValue = e.inputBuffer.getChannelData(0)[0]; + var referenceValue = e.inputBuffer.getChannelData(1)[0]; + + assert_equals(testValue, referenceValue, + "value matches expected"); + }); +} + +async_test(function(t) { + var context = new AudioContext; + (function waitForTimeAdvance() { + if (context.currentTime == 0) { + t.step_timeout(waitForTimeAdvance, 0); + } else { + do_test(t, context); + } + })(); +}); +</script> diff --git a/dom/media/webaudio/test/test_retrospective-linearRampToValueAtTime.html b/dom/media/webaudio/test/test_retrospective-linearRampToValueAtTime.html new file mode 100644 index 0000000000..1594a30bd1 --- /dev/null +++ b/dom/media/webaudio/test/test_retrospective-linearRampToValueAtTime.html @@ -0,0 +1,51 @@ +<!doctype html> +<meta charset=utf-8> +<title>Test linearRampToValue with end time in the past</title> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script> +function do_test(t, context) { + var source = context.createConstantSource(); + source.start(); + + var test = context.createGain(); + test.gain.linearRampToValueAtTime(0.1, 0.5*context.currentTime); + test.gain.linearRampToValueAtTime(0.9, 2.0); + + var reference = context.createGain(); + reference.gain.linearRampToValueAtTime(0.1, context.currentTime); + reference.gain.linearRampToValueAtTime(0.9, 2.0); + + source.connect(test); + source.connect(reference); + + var merger = context.createChannelMerger(); + test.connect(merger, 0, 0); + reference.connect(merger, 0, 1); + + var processor = context.createScriptProcessor(0, 2, 0); + merger.connect(processor); + processor.onaudioprocess = + t.step_func_done((e) => { + source.stop(); + processor.onaudioprocess = null; + + var testValue = e.inputBuffer.getChannelData(0)[0]; + var referenceValue = e.inputBuffer.getChannelData(1)[0]; + + assert_equals(testValue, referenceValue, + "value matches expected"); + }); +} + +async_test(function(t) { + var context = new AudioContext; + (function waitForTimeAdvance() { + if (context.currentTime == 0) { + t.step_timeout(waitForTimeAdvance, 0); + } else { + do_test(t, context); + } + })(); +}); +</script> diff --git a/dom/media/webaudio/test/test_retrospective-setTargetAtTime.html b/dom/media/webaudio/test/test_retrospective-setTargetAtTime.html new file mode 100644 index 0000000000..9b04fe22bb --- /dev/null +++ b/dom/media/webaudio/test/test_retrospective-setTargetAtTime.html @@ -0,0 +1,51 @@ +<!doctype html> +<meta charset=utf-8> +<title>Test setTargetAtTime with start time in the past</title> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script> +function do_test(t, context) { + var source = context.createConstantSource(); + source.start(); + + var test = context.createGain(); + test.gain.setTargetAtTime(0.1, 0.5*context.currentTime, 0.1); + test.gain.linearRampToValueAtTime(0.9, 2.0); + + var reference = context.createGain(); + reference.gain.setTargetAtTime(0.1, context.currentTime, 0.1); + reference.gain.linearRampToValueAtTime(0.9, 2.0); + + source.connect(test); + source.connect(reference); + + var merger = context.createChannelMerger(); + test.connect(merger, 0, 0); + reference.connect(merger, 0, 1); + + var processor = context.createScriptProcessor(0, 2, 0); + merger.connect(processor); + processor.onaudioprocess = + t.step_func_done((e) => { + source.stop(); + processor.onaudioprocess = null; + + var testValue = e.inputBuffer.getChannelData(0)[0]; + var referenceValue = e.inputBuffer.getChannelData(1)[0]; + + assert_equals(testValue, referenceValue, + "value matches expected"); + }); +} + +async_test(function(t) { + var context = new AudioContext; + (function waitForTimeAdvance() { + if (context.currentTime == 0) { + t.step_timeout(waitForTimeAdvance, 0); + } else { + do_test(t, context); + } + })(); +}); +</script> diff --git a/dom/media/webaudio/test/test_retrospective-setValueAtTime.html b/dom/media/webaudio/test/test_retrospective-setValueAtTime.html new file mode 100644 index 0000000000..b9657ef211 --- /dev/null +++ b/dom/media/webaudio/test/test_retrospective-setValueAtTime.html @@ -0,0 +1,54 @@ +<!DOCTYPE html> +<title>Test setValueAtTime with startTime in the past</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script> +function do_test(t, context) { + var source = context.createConstantSource(); + source.start(); + + // Use a ramp of slope 1/sample to measure time. + // The end value is the extent of exact precision in single precision float. + const rampEnd = Math.pow(2, 24); + const rampEndSeconds = rampEnd / context.sampleRate; + var test = context.createGain(); + test.gain.setValueAtTime(0.0, 0.5*context.currentTime); + test.gain.linearRampToValueAtTime(rampEnd, rampEndSeconds); + + var reference = context.createGain(); + reference.gain.setValueAtTime(0.0, context.currentTime); + reference.gain.linearRampToValueAtTime(rampEnd, rampEndSeconds); + + source.connect(test); + source.connect(reference); + + var merger = context.createChannelMerger(); + test.connect(merger, 0, 0); + reference.connect(merger, 0, 1); + + var processor = context.createScriptProcessor(0, 2, 0); + merger.connect(processor); + processor.onaudioprocess = + t.step_func_done((e) => { + source.stop(); + processor.onaudioprocess = null; + + var testValue = e.inputBuffer.getChannelData(0)[0]; + var referenceValue = e.inputBuffer.getChannelData(1)[0]; + + assert_equals(testValue, referenceValue, + "ramp value matches expected"); + }); +} + +async_test(function(t) { + var context = new AudioContext; + (function waitForTimeAdvance() { + if (context.currentTime == 0) { + t.step_timeout(waitForTimeAdvance, 0); + } else { + do_test(t, context); + } + })(); +}); +</script> diff --git a/dom/media/webaudio/test/test_retrospective-setValueCurveAtTime.html b/dom/media/webaudio/test/test_retrospective-setValueCurveAtTime.html new file mode 100644 index 0000000000..008b240129 --- /dev/null +++ b/dom/media/webaudio/test/test_retrospective-setValueCurveAtTime.html @@ -0,0 +1,49 @@ +<!doctype html> +<meta charset=utf-8> +<title>Test SetValueCurve with start time in the past</title> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script> +function do_test(t, context) { + var source = context.createConstantSource(); + source.start(); + + var test = context.createGain(); + test.gain.setValueCurveAtTime(new Float32Array([1.0, 0.1]), 0.0, 1.0); + + var reference = context.createGain(); + reference.gain.setValueCurveAtTime(new Float32Array([1.0, 0.1]), 0.5*context.currentTime, 1.0); + + source.connect(test); + source.connect(reference); + + var merger = context.createChannelMerger(); + test.connect(merger, 0, 0); + reference.connect(merger, 0, 1); + + var processor = context.createScriptProcessor(0, 2, 0); + merger.connect(processor); + processor.onaudioprocess = + t.step_func_done((e) => { + source.stop(); + processor.onaudioprocess = null; + + var testValue = e.inputBuffer.getChannelData(0)[0]; + var referenceValue = e.inputBuffer.getChannelData(1)[0]; + + assert_equals(testValue, referenceValue, + "value matches expected"); + }); +} + +async_test(function(t) { + var context = new AudioContext; + (function waitForTimeAdvance() { + if (context.currentTime == 0) { + t.step_timeout(waitForTimeAdvance, 0); + } else { + do_test(t, context); + } + })(); +}); +</script> diff --git a/dom/media/webaudio/test/test_scriptProcessorNode.html b/dom/media/webaudio/test/test_scriptProcessorNode.html new file mode 100644 index 0000000000..ec263755cb --- /dev/null +++ b/dom/media/webaudio/test/test_scriptProcessorNode.html @@ -0,0 +1,132 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test ScriptProcessorNode</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +// We do not use our generic graph test framework here because +// the testing logic here is sort of complicated, and would +// not be easy to map to OfflineAudioContext, as ScriptProcessorNodes +// can experience delays. + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + var context = new AudioContext(); + var buffer = null; + + var sourceSP = context.createScriptProcessor(2048); + sourceSP.addEventListener("audioprocess", function(e) { + // generate the audio + for (var i = 0; i < 2048; ++i) { + // Make sure our first sample won't be zero + e.outputBuffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * (i + 1) / context.sampleRate); + e.outputBuffer.getChannelData(1)[i] = Math.sin(880 * 2 * Math.PI * (i + 1) / context.sampleRate); + } + // Remember our generated audio + buffer = e.outputBuffer; + + sourceSP.removeEventListener("audioprocess", arguments.callee); + }); + + expectException(function() { + context.createScriptProcessor(1); + }, DOMException.INDEX_SIZE_ERR); + expectException(function() { + context.createScriptProcessor(2); + }, DOMException.INDEX_SIZE_ERR); + expectException(function() { + context.createScriptProcessor(128); + }, DOMException.INDEX_SIZE_ERR); + expectException(function() { + context.createScriptProcessor(255); + }, DOMException.INDEX_SIZE_ERR); + + is(sourceSP.channelCount, 2, "script processor node has 2 input channels by default"); + is(sourceSP.channelCountMode, "explicit", "Correct channelCountMode for the script processor node"); + is(sourceSP.channelInterpretation, "speakers", "Correct channelCountInterpretation for the script processor node"); + + function findFirstNonZeroSample(buffer) { + for (var i = 0; i < buffer.length; ++i) { + if (buffer.getChannelData(0)[i] != 0) { + return i; + } + } + return buffer.length; + } + + var sp = context.createScriptProcessor(2048); + sourceSP.connect(sp); + sp.connect(context.destination); + var lastPlaybackTime = 0; + + var emptyBuffer = context.createBuffer(1, 2048, context.sampleRate); + + function checkAudioProcessingEvent(e) { + is(e.target, sp, "Correct event target"); + ok(e.playbackTime > lastPlaybackTime, "playbackTime correctly set"); + lastPlaybackTime = e.playbackTime; + is(e.inputBuffer.numberOfChannels, 2, "Correct number of channels for the input buffer"); + is(e.inputBuffer.length, 2048, "Correct length for the input buffer"); + is(e.inputBuffer.sampleRate, context.sampleRate, "Correct sample rate for the input buffer"); + is(e.outputBuffer.numberOfChannels, 2, "Correct number of channels for the output buffer"); + is(e.outputBuffer.length, 2048, "Correct length for the output buffer"); + is(e.outputBuffer.sampleRate, context.sampleRate, "Correct sample rate for the output buffer"); + + compareChannels(e.outputBuffer.getChannelData(0), emptyBuffer.getChannelData(0)); + compareChannels(e.outputBuffer.getChannelData(1), emptyBuffer.getChannelData(0)); + } + + sp.onaudioprocess = function(e) { + isnot(buffer, null, "The audioprocess handler for sourceSP must be run at this point"); + checkAudioProcessingEvent(e); + + // Because of the initial latency added by the second script processor node, + // we will never see any generated audio frames in the first callback. + compareChannels(e.inputBuffer.getChannelData(0), emptyBuffer.getChannelData(0)); + compareChannels(e.inputBuffer.getChannelData(1), emptyBuffer.getChannelData(0)); + + sp.onaudioprocess = function(e) { + checkAudioProcessingEvent(e); + + var firstNonZero = findFirstNonZeroSample(e.inputBuffer); + ok(firstNonZero <= 2048, "First non-zero sample within range"); + + compareChannels(e.inputBuffer.getChannelData(0), emptyBuffer.getChannelData(0), firstNonZero); + compareChannels(e.inputBuffer.getChannelData(1), emptyBuffer.getChannelData(0), firstNonZero); + compareChannels(e.inputBuffer.getChannelData(0), buffer.getChannelData(0), 2048 - firstNonZero, firstNonZero, 0); + compareChannels(e.inputBuffer.getChannelData(1), buffer.getChannelData(1), 2048 - firstNonZero, firstNonZero, 0); + + if (firstNonZero == 0) { + // If we did not experience any delays, the test is done! + sp.onaudioprocess = null; + + SimpleTest.finish(); + } else if (firstNonZero != 2048) { + // In case we just saw a zero buffer this time, wait one more round + sp.onaudioprocess = function(e) { + checkAudioProcessingEvent(e); + + compareChannels(e.inputBuffer.getChannelData(0), buffer.getChannelData(0), firstNonZero, 0, 2048 - firstNonZero); + compareChannels(e.inputBuffer.getChannelData(1), buffer.getChannelData(1), firstNonZero, 0, 2048 - firstNonZero); + compareChannels(e.inputBuffer.getChannelData(0), emptyBuffer.getChannelData(0), undefined, firstNonZero); + compareChannels(e.inputBuffer.getChannelData(1), emptyBuffer.getChannelData(0), undefined, firstNonZero); + + sp.onaudioprocess = null; + + SimpleTest.finish(); + }; + } + }; + }; +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_scriptProcessorNodeChannelCount.html b/dom/media/webaudio/test/test_scriptProcessorNodeChannelCount.html new file mode 100644 index 0000000000..5e9a9960b7 --- /dev/null +++ b/dom/media/webaudio/test/test_scriptProcessorNodeChannelCount.html @@ -0,0 +1,80 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test AudioBufferSourceNode</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +// We do not use our generic graph test framework here because +// the testing logic here is sort of complicated, and would +// not be easy to map to OfflineAudioContext, as ScriptProcessorNodes +// can experience delays. + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + var context = new AudioContext(); + var buffer = context.createBuffer(6, 2048, context.sampleRate); + for (var i = 0; i < 2048; ++i) { + for (var j = 0; j < 6; ++j) { + buffer.getChannelData(0)[i] = Math.sin(440 * j * Math.PI * i / context.sampleRate); + } + } + + var monoBuffer = context.createBuffer(1, 2048, context.sampleRate); + for (var i = 0; i < 2048; ++i) { + monoBuffer.getChannelData(0)[i] = 1; + } + + var source = context.createBufferSource(); + + var sp = context.createScriptProcessor(2048, 3); + expectException(function() { sp.channelCount = 2; }, + DOMException.NOT_SUPPORTED_ERR); + sp.channelCountMode = "explicit"; + expectException(function() { sp.channelCountMode = "max"; }, + DOMException.NOT_SUPPORTED_ERR); + expectException(function() { sp.channelCountMode = "clamped-max"; }, + DOMException.NOT_SUPPORTED_ERR); + sp.channelInterpretation = "discrete"; + source.start(0); + source.buffer = buffer; + source.connect(sp); + sp.connect(context.destination); + + var monoSource = context.createBufferSource(); + monoSource.buffer = monoBuffer; + monoSource.connect(sp); + monoSource.start(2048 / context.sampleRate); + + sp.onaudioprocess = function(e) { + is(e.inputBuffer.numberOfChannels, 3, "Should be correctly down-mixed to three channels"); + for (var i = 0; i < 3; ++i) { + compareChannels(e.inputBuffer.getChannelData(i), buffer.getChannelData(i)); + } + + // On the next iteration, we'll get a silence buffer + sp.onaudioprocess = function(e) { + var emptyBuffer = context.createBuffer(1, 2048, context.sampleRate); + is(e.inputBuffer.numberOfChannels, 3, "Should be correctly up-mixed to three channels"); + compareChannels(e.inputBuffer.getChannelData(0), monoBuffer.getChannelData(0)); + for (var i = 1; i < 3; ++i) { + compareChannels(e.inputBuffer.getChannelData(i), emptyBuffer.getChannelData(0)); + } + + sp.onaudioprocess = null; + sp.disconnect(context.destination); + + SimpleTest.finish(); + }; + }; +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_scriptProcessorNodeNotConnected.html b/dom/media/webaudio/test/test_scriptProcessorNodeNotConnected.html new file mode 100644 index 0000000000..fb45895380 --- /dev/null +++ b/dom/media/webaudio/test/test_scriptProcessorNodeNotConnected.html @@ -0,0 +1,34 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test AudioBufferSourceNode: should not fire audioprocess if not connected.</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestFlakyTimeout("This test needs to wait a while to ensure that a given event does not happen."); +addLoadEvent(function() { + var context = new AudioContext(); + + var sp = context.createScriptProcessor(2048, 2, 2); + sp.onaudioprocess = function(e) { + ok(false, "Should not call onaudioprocess if the node is not connected."); + sp.onaudioprocess = null; + SimpleTest.finish(); + }; + setTimeout(function() { + console.log(sp.onaudioprocess); + if (sp.onaudioprocess) { + ok(true, "onaudioprocess not fired."); + SimpleTest.finish(); + } + }, 4000); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_scriptProcessorNodePassThrough.html b/dom/media/webaudio/test/test_scriptProcessorNodePassThrough.html new file mode 100644 index 0000000000..5d2d8170e2 --- /dev/null +++ b/dom/media/webaudio/test/test_scriptProcessorNodePassThrough.html @@ -0,0 +1,103 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test ScriptProcessorNode with passthrough</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +// We do not use our generic graph test framework here because +// the testing logic here is sort of complicated, and would +// not be easy to map to OfflineAudioContext, as ScriptProcessorNodes +// can experience delays. + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + var context = new AudioContext(); + var buffer = null; + + var sourceSP = context.createScriptProcessor(2048); + sourceSP.addEventListener("audioprocess", function(e) { + // generate the audio + for (var i = 0; i < 2048; ++i) { + // Make sure our first sample won't be zero + e.outputBuffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * (i + 1) / context.sampleRate); + e.outputBuffer.getChannelData(1)[i] = Math.sin(880 * 2 * Math.PI * (i + 1) / context.sampleRate); + } + // Remember our generated audio + buffer = e.outputBuffer; + + sourceSP.removeEventListener("audioprocess", arguments.callee); + }); + + function findFirstNonZeroSample(buffer) { + for (var i = 0; i < buffer.length; ++i) { + if (buffer.getChannelData(0)[i] != 0) { + return i; + } + } + return buffer.length; + } + + var sp = context.createScriptProcessor(2048); + sourceSP.connect(sp); + + var spWrapped = SpecialPowers.wrap(sp); + ok("passThrough" in spWrapped, "ScriptProcessorNode should support the passThrough API"); + spWrapped.passThrough = true; + + sp.onaudioprocess = function() { + ok(false, "The audioprocess event must never be dispatched on the passthrough ScriptProcessorNode"); + }; + + var sp2 = context.createScriptProcessor(2048); + sp.connect(sp2); + sp2.connect(context.destination); + + var emptyBuffer = context.createBuffer(1, 2048, context.sampleRate); + + sp2.onaudioprocess = function(e) { + // Because of the initial latency added by the second script processor node, + // we will never see any generated audio frames in the first callback. + compareChannels(e.inputBuffer.getChannelData(0), emptyBuffer.getChannelData(0)); + compareChannels(e.inputBuffer.getChannelData(1), emptyBuffer.getChannelData(0)); + + sp2.onaudioprocess = function(e) { + var firstNonZero = findFirstNonZeroSample(e.inputBuffer); + ok(firstNonZero <= 2048, "First non-zero sample within range"); + + compareChannels(e.inputBuffer.getChannelData(0), emptyBuffer.getChannelData(0), firstNonZero); + compareChannels(e.inputBuffer.getChannelData(1), emptyBuffer.getChannelData(0), firstNonZero); + compareChannels(e.inputBuffer.getChannelData(0), buffer.getChannelData(0), 2048 - firstNonZero, firstNonZero, 0); + compareChannels(e.inputBuffer.getChannelData(1), buffer.getChannelData(1), 2048 - firstNonZero, firstNonZero, 0); + + if (firstNonZero == 0) { + // If we did not experience any delays, the test is done! + sp2.onaudioprocess = null; + + SimpleTest.finish(); + } else if (firstNonZero != 2048) { + // In case we just saw a zero buffer this time, wait one more round + sp2.onaudioprocess = function(e) { + compareChannels(e.inputBuffer.getChannelData(0), buffer.getChannelData(0), firstNonZero, 0, 2048 - firstNonZero); + compareChannels(e.inputBuffer.getChannelData(1), buffer.getChannelData(1), firstNonZero, 0, 2048 - firstNonZero); + compareChannels(e.inputBuffer.getChannelData(0), emptyBuffer.getChannelData(0), undefined, firstNonZero); + compareChannels(e.inputBuffer.getChannelData(1), emptyBuffer.getChannelData(0), undefined, firstNonZero); + + sp2.onaudioprocess = null; + + SimpleTest.finish(); + }; + } + }; + }; +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_scriptProcessorNodeZeroInputOutput.html b/dom/media/webaudio/test/test_scriptProcessorNodeZeroInputOutput.html new file mode 100644 index 0000000000..f4b25d49dd --- /dev/null +++ b/dom/media/webaudio/test/test_scriptProcessorNodeZeroInputOutput.html @@ -0,0 +1,39 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test AudioBufferSourceNode</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + var context = new AudioContext(); + + var sp = context.createScriptProcessor(2048, 0, 2); + sp.onaudioprocess = function(e) { + is(e.inputBuffer.numberOfChannels, 0, "Should have 0 input channels"); + is(e.outputBuffer.numberOfChannels, 2, "Should have 2 output channels"); + sp.onaudioprocess = null; + + sp = context.createScriptProcessor(2048, 2, 0); + sp.onaudioprocess = function(e) { + is(e.inputBuffer.numberOfChannels, 2, "Should have 2 input channels"); + is(e.outputBuffer.numberOfChannels, 0, "Should have 0 output channels"); + sp.onaudioprocess = null; + + SimpleTest.finish(); + }; + sp.connect(context.destination); + }; + sp.connect(context.destination); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_scriptProcessorNode_playbackTime1.html b/dom/media/webaudio/test/test_scriptProcessorNode_playbackTime1.html new file mode 100644 index 0000000000..ec695f952b --- /dev/null +++ b/dom/media/webaudio/test/test_scriptProcessorNode_playbackTime1.html @@ -0,0 +1,52 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test ScriptProcessorNode playbackTime for bug 970773</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); + +var context = new AudioContext(); +const delay = 0.1; + +function doTest() { + const processorBufferLength = 256; + // |currentTime| may include double precision floating point + // rounding errors, so round to nearest integer sample to ignore these. + var minimumPlaybackSample = + Math.round(context.currentTime * context.sampleRate) + + processorBufferLength; + var sp = context.createScriptProcessor(processorBufferLength); + sp.connect(context.destination); + sp.onaudioprocess = + function(e) { + is(e.inputBuffer.length, processorBufferLength, + "expected buffer length"); + var playbackSample = Math.round(e.playbackTime * context.sampleRate) + ok(playbackSample >= minimumPlaybackSample, + "playbackSample " + playbackSample + + " beyond expected minimum " + minimumPlaybackSample); + sp.onaudioprocess = null; + SimpleTest.finish(); + }; +} + +// Wait until AudioDestinationNode has accumulated enough 'extra' time so that +// a failure would be easily detected. +(function waitForExtraTime() { + if (context.currentTime < delay) { + SimpleTest.executeSoon(waitForExtraTime); + } else { + doTest(); + } +})(); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_sequentialBufferSourceWithResampling.html b/dom/media/webaudio/test/test_sequentialBufferSourceWithResampling.html new file mode 100644 index 0000000000..218bf462c5 --- /dev/null +++ b/dom/media/webaudio/test/test_sequentialBufferSourceWithResampling.html @@ -0,0 +1,72 @@ +<!DOCTYPE html> +<title>Test seamless playback of a series of resampled buffers</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script> +// Permitting some accumulation of rounding to int16_t. +// 64/2^15 would be only just small enough to detect off-by-one-subsample +// scheduling errors with the frequencies here. +const EPSILON = 4.0 / Math.pow(2, 15); +// Offsets test for rounding to nearest rather than up or down. +const OFFSETS = [EPSILON, 1.0 - EPSILON]; +// The ratio of resampling is 147:160, so 256 start points is enough to cover +// every fractional offset. +const LENGTH = 256; + +function do_test(context_rate, buffer_rate, start_offset) { + + var context = + new OfflineAudioContext(2, LENGTH, context_rate); + + var merger = context.createChannelMerger(context.destination.channelCount); + merger.connect(context.destination); + + // Create an audio signal that will be repeated + var repeating_signal = context.createBuffer(1, 1, buffer_rate); + repeating_signal.getChannelData(0)[0] = 0.5; + + // Schedule a series of nodes to repeat the signal. + for (var i = 0; i < LENGTH; ++i) { + var source = context.createBufferSource(); + source.buffer = repeating_signal; + source.connect(merger, 0, 0); + source.start((i + start_offset) / buffer_rate); + } + + // A single long signal should produce the same result. + var long_signal = context.createBuffer(1, LENGTH, buffer_rate); + var c = long_signal.getChannelData(0); + for (var i = 0; i < c.length; ++i) { + c[i] = 0.5; + } + + var source = context.createBufferSource(); + source.buffer = long_signal; + source.connect(merger, 0, 1); + source.start(start_offset / buffer_rate); + + return context.startRendering(). + then((buffer) => { + let series_output = buffer.getChannelData(0); + let expected = buffer.getChannelData(1); + + for (var i = 0; i < buffer.length; ++i) { + assert_approx_equals(series_output[i], expected[i], EPSILON, + "series output at " + i); + } + }); +} + +function start_tests(context_rate, buffer_rate) { + OFFSETS.forEach((start_offset) => { + promise_test(() => do_test(context_rate, buffer_rate, start_offset), + "" + context_rate + " context, " + + buffer_rate + " buffer, " + + start_offset + " start"); + }); +} + +start_tests(48000, 44100); +start_tests(44100, 48000); + +</script> diff --git a/dom/media/webaudio/test/test_setValueCurveWithNonFiniteElements.html b/dom/media/webaudio/test/test_setValueCurveWithNonFiniteElements.html new file mode 100644 index 0000000000..28829e1ec2 --- /dev/null +++ b/dom/media/webaudio/test/test_setValueCurveWithNonFiniteElements.html @@ -0,0 +1,60 @@ +<!DOCTYPE HTML> +<html> +<meta charset=utf-8> +<head> + <title>Bug 1308437 - setValueCurve should throw on non-finite elements</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> +SimpleTest.waitForExplicitFinish(); + +function testInfiniteElement(audioContext, audioParam) { + // create value curve with infinite element + var arr = new Float32Array(5); + arr[0] = 0.5; + arr[1] = 1; + arr[2] = Infinity; + arr[3] = 1; + arr[4] = 0.5; + + try { + audioParam.setValueCurveAtTime(arr, audioContext.currentTime(), 2); + ok(false, "We shouldn't be able to call setValueCurve with Infinity but we can"); + } catch(e) { + ok(e instanceof TypeError, "TypeError is thrown"); + } +}; + +function testNanElement(audioContext, audioParam) { + // create value curve with infinite element + var arr = new Float32Array(5); + arr[0] = 0.5; + arr[1] = 1; + arr[2] = NaN; + arr[3] = 1; + arr[4] = 0.5; + + try { + audioParam.setValueCurveAtTime(arr, audioContext.currentTime(), 2); + ok(false, "We shouldn't be able to call setValueCurve with NaN but we can"); + } catch(e) { + ok(e instanceof TypeError, "TypeError is thrown"); + } +}; + +addLoadEvent(function() { + var audioContext = new AudioContext(); + var gainNode = audioContext.createGain(); + + testInfiniteElement(audioContext, gainNode.gain); + testNanElement(audioContext, gainNode.gain); + + SimpleTest.finish(); +}); +</script> +</pre> +</body> +</html>
\ No newline at end of file diff --git a/dom/media/webaudio/test/test_singleSourceDest.html b/dom/media/webaudio/test/test_singleSourceDest.html new file mode 100644 index 0000000000..fd4de50f5d --- /dev/null +++ b/dom/media/webaudio/test/test_singleSourceDest.html @@ -0,0 +1,70 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test whether we can create an AudioContext interface</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + var context = new AudioContext(); + var buffer = context.createBuffer(1, 2048, context.sampleRate); + for (var i = 0; i < 2048; ++i) { + buffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate); + } + + var destination = context.destination; + is(destination.context, context, "Destination node has proper context"); + is(destination.context, context, "Destination node has proper context"); + is(destination.numberOfInputs, 1, "Destination node has 1 inputs"); + is(destination.numberOfOutputs, 0, "Destination node has 0 outputs"); + is(destination.channelCount, 2, "Destination node has 2 input channels by default"); + is(destination.channelCountMode, "explicit", "Correct channelCountMode for the destination node"); + is(destination.channelInterpretation, "speakers", "Correct channelCountInterpretation for the destination node"); + ok(destination instanceof EventTarget, "AudioNodes must be EventTargets"); + + var source = context.createBufferSource(); + is(source.context, context, "Source node has proper context"); + is(source.numberOfInputs, 0, "Source node has 0 inputs"); + is(source.numberOfOutputs, 1, "Source node has 1 outputs"); + is(source.loop, false, "Source node is not looping"); + is(source.loopStart, 0, "Correct default value for loopStart"); + is(source.loopEnd, 0, "Correct default value for loopEnd"); + ok(!source.buffer, "Source node should not have a buffer when it's created"); + is(source.channelCount, 2, "source node has 2 input channels by default"); + is(source.channelCountMode, "max", "Correct channelCountMode for the source node"); + is(source.channelInterpretation, "speakers", "Correct channelCountInterpretation for the source node"); + + expectException(function() { + source.channelCount = 0; + }, DOMException.NOT_SUPPORTED_ERR); + + source.buffer = buffer; + ok(source.buffer, "Source node should have a buffer now"); + + source.connect(destination); + + is(source.numberOfInputs, 0, "Source node has 0 inputs"); + is(source.numberOfOutputs, 1, "Source node has 0 outputs"); + is(destination.numberOfInputs, 1, "Destination node has 0 inputs"); + is(destination.numberOfOutputs, 0, "Destination node has 0 outputs"); + + source.start(0); + SimpleTest.executeSoon(function() { + source.stop(0); + source.disconnect(); + + SpecialPowers.clearUserPref("media.webaudio.enabled"); + SimpleTest.finish(); + }); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_slowStart.html b/dom/media/webaudio/test/test_slowStart.html new file mode 100644 index 0000000000..17de7351c1 --- /dev/null +++ b/dom/media/webaudio/test/test_slowStart.html @@ -0,0 +1,48 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test AudioContext.currentTime</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestFlakyTimeout("This test needs to periodically query the AudioContext's position."); +const CUBEB_INIT_DELAY = 5000; +// Delay audio stream start by a good 5 seconds +SpecialPowers.pushPrefEnv({"set": [["media.cubeb.slow_stream_init_ms", + CUBEB_INIT_DELAY]]}, runTest); + + +function runTest() { + let ac = new AudioContext(); + let notStartedYetCount = 0; + let startWallClockTime = performance.now(); + is(ac.currentTime, 0, "AudioContext.currentTime should be 0 initially"); + is(ac.state, "suspended", "AudioContext.currentTime is initially suspended"); + let intervalHandle = setInterval(function() { + if (ac.state == "running" || ac.currentTime > 0) { + clearInterval(intervalHandle); + return; + } + is(ac.currentTime, 0, "AudioContext.currentTime is still 0"); + is(ac.state, "suspended", "AudioContext.currentTime is still suspended"); + notStartedYetCount++; + }); + ac.onstatechange = function() { + is(ac.state, "running", "The AudioContext eventually started."); + var startDuration = performance.now() - startWallClockTime; + info(`AudioContext start time with a delay of ${CUBEB_INIT_DELAY}): ${startDuration}`); + ok(notStartedYetCount > 0, "We should have observed the AudioContext in \"suspended\" state"); + ok(startDuration >= CUBEB_INIT_DELAY, "The AudioContext state transition was correct."); + SimpleTest.finish(); + } +} + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_stereoPannerNode.html b/dom/media/webaudio/test/test_stereoPannerNode.html new file mode 100644 index 0000000000..d08e1640b2 --- /dev/null +++ b/dom/media/webaudio/test/test_stereoPannerNode.html @@ -0,0 +1,295 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test StereoPannerNode</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +var SR = 44100; +var BUF_SIZE = 128; +var PANNING = 0.1; +var GAIN = 0.5; + +// Cheap reimplementation of some bits of the spec +function gainForPanningMonoToStereo(panning) { + panning += 1; + panning /= 2; + return [ Math.cos(0.5 * Math.PI * panning), + Math.sin(0.5 * Math.PI * panning) ]; +} + +function gainForPanningStereoToStereo(panning) { + if (panning <= 0) { + panning += 1.; + } + return [ Math.cos(0.5 * Math.PI * panning), + Math.sin(0.5 * Math.PI * panning) ]; +} + +function applyStereoToStereoPanning(l, r, panningValues, panning) { + var outL, outR; + if (panning <= 0) { + outL = l + r * panningValues[0]; + outR = r * panningValues[1]; + } else { + outL = l * panningValues[0]; + outR = r + l * panningValues[1]; + } + return [outL,outR]; +} + +function applyMonoToStereoPanning(c, panning) { + return [c * panning[0], c * panning[1]]; +} + +// Test the DOM interface +var context = new OfflineAudioContext(1, 1, SR); +var stereoPanner = new StereoPannerNode(context); +ok(stereoPanner.pan, "The AudioParam member must exist"); +is(stereoPanner.pan.value, 0.0, "Correct initial value"); +is(stereoPanner.pan.defaultValue, 0.0, "Correct default value"); +is(stereoPanner.channelCount, 2, "StereoPannerNode node has 2 input channels by default"); +is(stereoPanner.channelCountMode, "clamped-max", "Correct channelCountMode for the StereoPannerNode"); +is(stereoPanner.channelInterpretation, "speakers", "Correct channelCountInterpretation for the StereoPannerNode"); +expectException(function() { + stereoPanner.channelCount = 3; +}, DOMException.NOT_SUPPORTED_ERR); +expectException(function() { + stereoPanner.channelCountMode = "max"; +}, DOMException.NOT_SUPPORTED_ERR); + +// A sine to be used to fill the buffers +function sine(t) { + return Math.sin(440 * 2 * Math.PI * t / context.sampleRate); +} + +// A couple mono and stereo buffers: the StereoPannerNode equation is different +// if the input is mono or stereo +var stereoBuffer = new AudioBuffer({ numberOfChannels: 2, + length: BUF_SIZE, + sampleRate: context.sampleRate }); +var monoBuffer = new AudioBuffer({ numberOfChannels: 1, + length: BUF_SIZE, + sampleRate: context.sampleRate }); +for (var i = 0; i < BUF_SIZE; ++i) { + monoBuffer.getChannelData(0)[i] = + stereoBuffer.getChannelData(0)[i] = + stereoBuffer.getChannelData(1)[i] = sine(i); +} + +// Expected test vectors +function expectedBufferNoop(gain) { + gain = gain || 1.0; + var expectedBuffer = new AudioBuffer({ numberOfChannels: 2, + length: BUF_SIZE, + sampleRate: SR }); + for (var i = 0; i < BUF_SIZE; i++) { + expectedBuffer.getChannelData(0)[i] = gain * sine(i); + expectedBuffer.getChannelData(1)[i] = gain * sine(i); + } + return expectedBuffer; +} + +function expectedBufferForStereo(panning, gain) { + gain = gain || 1.0; + var expectedBuffer = new AudioBuffer({ numberOfChannels: 2, + length: BUF_SIZE, + sampleRate: SR }); + var gainPanning = gainForPanningStereoToStereo(panning); + for (var i = 0; i < BUF_SIZE; i++) { + var values = [ gain * sine(i), gain * sine(i) ]; + var processed = applyStereoToStereoPanning(values[0], values[1], gainPanning, PANNING); + expectedBuffer.getChannelData(0)[i] = processed[0]; + expectedBuffer.getChannelData(1)[i] = processed[1]; + } + return expectedBuffer; +} + +function expectedBufferForMono(panning, gain) { + gain = gain || 1.0; + var expectedBuffer = new AudioBuffer({ numberOfChannels: 2, + length: BUF_SIZE, + sampleRate: SR }); + var gainPanning = gainForPanningMonoToStereo(panning); + gainPanning[0] *= gain; + gainPanning[1] *= gain; + for (var i = 0; i < BUF_SIZE; i++) { + var value = sine(i); + var processed = applyMonoToStereoPanning(value, gainPanning); + expectedBuffer.getChannelData(0)[i] = processed[0]; + expectedBuffer.getChannelData(1)[i] = processed[1]; + } + return expectedBuffer; +} + +// Actual test cases +var tests = [ + function monoPanningNoop(ctx, panner) { + var monoSource = ctx.createBufferSource(); + monoSource.connect(panner); + monoSource.buffer = monoBuffer; + monoSource.start(0); + return expectedBufferForMono(0); + }, + function stereoPanningNoop(ctx, panner) { + var stereoSource = ctx.createBufferSource(); + stereoSource.connect(panner); + stereoSource.buffer = stereoBuffer; + stereoSource.start(0); + return expectedBufferNoop(); + }, + function monoPanningNoopWithGain(ctx, panner) { + var monoSource = ctx.createBufferSource(); + var gain = ctx.createGain(); + gain.gain.value = GAIN; + monoSource.connect(gain); + gain.connect(panner); + monoSource.buffer = monoBuffer; + monoSource.start(0); + return expectedBufferForMono(0, GAIN); + }, + function stereoPanningNoopWithGain(ctx, panner) { + var stereoSource = ctx.createBufferSource(); + var gain = ctx.createGain(); + gain.gain.value = GAIN; + stereoSource.connect(gain); + gain.connect(panner); + stereoSource.buffer = stereoBuffer; + stereoSource.start(0); + return expectedBufferNoop(GAIN); + }, + function stereoPanningAutomation(ctx, panner) { + var stereoSource = ctx.createBufferSource(); + stereoSource.connect(panner); + stereoSource.buffer = stereoBuffer; + panner.pan.setValueAtTime(0.1, 0.0); + stereoSource.start(0); + return expectedBufferForStereo(PANNING); + }, + function stereoPanning(ctx, panner) { + var stereoSource = ctx.createBufferSource(); + stereoSource.buffer = stereoBuffer; + stereoSource.connect(panner); + panner.pan.value = 0.1; + stereoSource.start(0); + return expectedBufferForStereo(PANNING); + }, + function monoPanningAutomation(ctx, panner) { + var monoSource = ctx.createBufferSource(); + monoSource.connect(panner); + monoSource.buffer = monoBuffer; + panner.pan.setValueAtTime(PANNING, 0.0); + monoSource.start(0); + return expectedBufferForMono(PANNING); + }, + function monoPanning(ctx, panner) { + var monoSource = ctx.createBufferSource(); + monoSource.connect(panner); + monoSource.buffer = monoBuffer; + panner.pan.value = 0.1; + monoSource.start(0); + return expectedBufferForMono(PANNING); + }, + function monoPanningWithGain(ctx, panner) { + var monoSource = ctx.createBufferSource(); + var gain = ctx.createGain(); + gain.gain.value = GAIN; + monoSource.connect(gain); + gain.connect(panner); + monoSource.buffer = monoBuffer; + panner.pan.value = 0.1; + monoSource.start(0); + return expectedBufferForMono(PANNING, GAIN); + }, + function stereoPanningWithGain(ctx, panner) { + var stereoSource = ctx.createBufferSource(); + var gain = ctx.createGain(); + gain.gain.value = GAIN; + stereoSource.connect(gain); + gain.connect(panner); + stereoSource.buffer = stereoBuffer; + panner.pan.value = 0.1; + stereoSource.start(0); + return expectedBufferForStereo(PANNING, GAIN); + }, + function monoPanningWithGainAndAutomation(ctx, panner) { + var monoSource = ctx.createBufferSource(); + var gain = ctx.createGain(); + gain.gain.value = GAIN; + monoSource.connect(gain); + gain.connect(panner); + monoSource.buffer = monoBuffer; + panner.pan.setValueAtTime(PANNING, 0); + monoSource.start(0); + return expectedBufferForMono(PANNING, GAIN); + }, + function stereoPanningWithGainAndAutomation(ctx, panner) { + var stereoSource = ctx.createBufferSource(); + var gain = ctx.createGain(); + gain.gain.value = GAIN; + stereoSource.connect(gain); + gain.connect(panner); + stereoSource.buffer = stereoBuffer; + panner.pan.setValueAtTime(PANNING, 0); + stereoSource.start(0); + return expectedBufferForStereo(PANNING, GAIN); + }, + function bug_1783181(ctx, panner) { + const length = 128; + const buffer = new AudioBuffer({ length, numberOfChannels: 2, sampleRate: ctx.sampleRate }); + + buffer.copyToChannel(new Float32Array([1, 0.5, 0, -0.5, -1]), 0); + buffer.copyToChannel(new Float32Array([-0.5, -0.25, 0, 0.25, 0.5]), 1); + + const audioBufferSourceNode = new AudioBufferSourceNode(ctx, { buffer }); + + audioBufferSourceNode.connect(panner); + + panner.pan.setValueAtTime(0.5, 0); + panner.pan.setValueAtTime(0, 2 / ctx.sampleRate); + panner.pan.linearRampToValueAtTime(1, 5 / ctx.sampleRate); + panner.pan.cancelScheduledValues(3 / ctx.sampleRate); + + audioBufferSourceNode.start(0); + + const expected = new AudioBuffer({ length, numberOfChannels: 2, sampleRate: ctx.sampleRate }); + expected.copyToChannel(new Float32Array([ 0.7071067690849304, 0.3535533845424652, 0, -0.5, -1 ]), 0); + expected.copyToChannel(new Float32Array([ 0.20710676908493042, 0.10355338454246521, 0, 0.25, 0.5 ]), 1); + + return expected; + } +]; + +var finished = 0; +function finish() { + if (++finished == tests.length) { + SimpleTest.finish(); + } +} + +tests.forEach(function(f) { + var ac = new OfflineAudioContext(2, BUF_SIZE, SR); + var panner = ac.createStereoPanner(); + panner.connect(ac.destination); + var expected = f(ac, panner); + ac.oncomplete = function(e) { + info(f.name); + compareBuffers(e.renderedBuffer, expected); + finish(); + }; + ac.startRendering() +}); + +SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +<pre id=dump> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_stereoPannerNodePassThrough.html b/dom/media/webaudio/test/test_stereoPannerNodePassThrough.html new file mode 100644 index 0000000000..2d774d366b --- /dev/null +++ b/dom/media/webaudio/test/test_stereoPannerNodePassThrough.html @@ -0,0 +1,47 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test StereoPanerNode with passthrough</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +var gTest = { + length: 2048, + numberOfChannels: 1, + createGraph(context) { + var source = context.createBufferSource(); + + var stereoPanner = context.createStereoPanner(); + + source.buffer = this.buffer; + + source.connect(stereoPanner); + + var stereoPannerWrapped = SpecialPowers.wrap(stereoPanner); + ok("passThrough" in stereoPannerWrapped, "StereoPannerNode should support the passThrough API"); + stereoPannerWrapped.passThrough = true; + + source.start(0); + return stereoPanner; + }, + createExpectedBuffers(context) { + this.buffer = context.createBuffer(1, 2048, context.sampleRate); + for (var i = 0; i < 2048; ++i) { + this.buffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate); + } + + return [this.buffer]; + }, +}; + +runTest(); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_stereoPanningWithGain.html b/dom/media/webaudio/test/test_stereoPanningWithGain.html new file mode 100644 index 0000000000..94dfa3bb92 --- /dev/null +++ b/dom/media/webaudio/test/test_stereoPanningWithGain.html @@ -0,0 +1,49 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test stereo equalpower panning with a GainNode</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script src="webaudio.js" type="text/javascript"></script> +<script class="testbody" type="text/javascript"> + +const size = 256; + +var gTest = { + numberOfChannels: 2, + createGraph(context) { + var panner = context.createPanner(); + panner.setPosition(1.0, 0.0, 0.0); // reference distance the right + panner.panningModel = "equalpower"; + + var gain = context.createGain(); + gain.gain.value = -0.5; + gain.connect(panner); + + var buffer = context.createBuffer(2, 2, context.sampleRate); + buffer.getChannelData(0)[0] = 1.0; + buffer.getChannelData(1)[1] = 1.0; + var source = context.createBufferSource(); + source.buffer = buffer; + source.connect(gain); + source.start(0); + + return panner; + }, + createExpectedBuffers(context) { + var expectedBuffer = context.createBuffer(2, size, context.sampleRate); + expectedBuffer.getChannelData(1)[0] = -0.5; + expectedBuffer.getChannelData(1)[1] = -0.5; + return expectedBuffer; + }, +}; + +runTest(); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_waveDecoder.html b/dom/media/webaudio/test/test_waveDecoder.html new file mode 100644 index 0000000000..65c429f2ba --- /dev/null +++ b/dom/media/webaudio/test/test_waveDecoder.html @@ -0,0 +1,69 @@ +<!DOCTYPE HTML> +<html> +<meta charset=utf-8> +<head> + <title>Test that we decode uint8 and sint16 wave files with correct conversion to float64</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> +var testsDone = 0; +var tests = ["UklGRjUrAABXQVZFZm10IBAAAAABAAEAESsAABErAAABAAgAZGF0YQMAAAD/AIA=", + "UklGRkZWAABXQVZFZm10IBAAAAABAAEAESsAACJWAAACABAAZGF0YQYAAAD/fwCAAAA="]; + +SimpleTest.waitForExplicitFinish(); + +function base64ToUint8Buffer(b64) { + var str = atob(b64) + var u8 = new Uint8Array(str.length); + for (var i = 0; i < str.length; ++i) { + u8[i] = str.charCodeAt(i); + } + return u8; +} + +function fixupBufferSampleRate(u8, rate) { + u8[24] = (rate & 0x000000ff) >> 0; + u8[25] = (rate & 0x0000ff00) >> 8; + u8[26] = (rate & 0x00ff0000) >> 16; + u8[27] = (rate & 0xff000000) >> 24; +} + +function finishTest() { + testsDone += 1; + if (testsDone == tests.length) { + SimpleTest.finish(); + } +} + +function decodeComplete(b) { + ok(true, "Decoding succeeded."); + is(b.numberOfChannels, 1, "Should have 1 channel."); + is(b.length, 3, "Should have three samples."); + var samples = b.getChannelData(0); + ok(samples[0] > 0.99 && samples[0] < 1.01, "Check near 1.0. Got " + samples[0]); + ok(samples[1] > -1.01 && samples[1] < -0.99, "Check near -1.0. Got " + samples[1]); + ok(samples[2] > -0.01 && samples[2] < 0.01, "Check near 0.0. Got " + samples[2]); + finishTest(); +} + +function decodeFailed() { + ok(false, "Decoding failed."); + finishTest(); +} + +addLoadEvent(function() { + var context = new AudioContext(); + + for (var i = 0; i < tests.length; ++i) { + var u8 = base64ToUint8Buffer(tests[i]); + fixupBufferSampleRate(u8, context.sampleRate); + context.decodeAudioData(u8.buffer, decodeComplete, decodeFailed); + } +}); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_waveShaper.html b/dom/media/webaudio/test/test_waveShaper.html new file mode 100644 index 0000000000..9d2f1b3fa2 --- /dev/null +++ b/dom/media/webaudio/test/test_waveShaper.html @@ -0,0 +1,60 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test WaveShaperNode with no curve</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +var gTest = { + length: 4096, + numberOfChannels: 1, + createGraph(context) { + var source = context.createBufferSource(); + source.buffer = this.buffer; + + var shaper = new WaveShaperNode(context); + shaper.curve = this.curve; + + source.connect(shaper); + + source.start(0); + return shaper; + }, + createExpectedBuffers(context) { + this.buffer = context.createBuffer(1, 4096, context.sampleRate); + for (var i = 1; i < 4095; ++i) { + this.buffer.getChannelData(0)[i] = 2 * (i / 4096) - 1; + } + // Two out of range values + this.buffer.getChannelData(0)[0] = -2; + this.buffer.getChannelData(0)[4095] = 2; + + this.curve = new Float32Array(2048); + for (var i = 0; i < 2048; ++i) { + this.curve[i] = Math.sin(100 * Math.PI * (i + 1) / context.sampleRate); + } + + var expectedBuffer = context.createBuffer(1, 4096, context.sampleRate); + for (var i = 1; i < 4095; ++i) { + var input = this.buffer.getChannelData(0)[i]; + var index = Math.floor(this.curve.length * (input + 1) / 2); + index = Math.max(0, Math.min(this.curve.length - 1, index)); + expectedBuffer.getChannelData(0)[i] = this.curve[index]; + } + expectedBuffer.getChannelData(0)[0] = this.curve[0]; + expectedBuffer.getChannelData(0)[4095] = this.curve[2047]; + return expectedBuffer; + }, +}; + +runTest(); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_waveShaperGain.html b/dom/media/webaudio/test/test_waveShaperGain.html new file mode 100644 index 0000000000..45411eca02 --- /dev/null +++ b/dom/media/webaudio/test/test_waveShaperGain.html @@ -0,0 +1,73 @@ +<!DOCTYPE HTML> +<html> +<head> +<meta charset="utf-8"> + <title>Test that WaveShaperNode doesn't corrupt its inputs when the gain is != + 1.0 (bug 1203616)</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre> +</pre> +<script class="testbody" type="text/javascript"> +SimpleTest.waitForExplicitFinish(); +var samplerate = 44100; +var context = new OfflineAudioContext(1, 44100, samplerate); + +var dc = context.createBufferSource(); + +var buffer = context.createBuffer(1, 1, samplerate); +buffer.getChannelData(0)[0] = 1.0; +dc.buffer = buffer; + +var gain = context.createGain(); +var ws2 = context.createWaveShaper(); +var ws = []; + +// No-op waveshaper curves. +for (var i = 0; i < 2; i++) { + ws[i] = context.createWaveShaper(); + var curve = new Float32Array(2); + curve[0] = -1.0; + curve[1] = 1.0; + ws[i].curve = curve; + ws[i].connect(context.destination); + gain.connect(ws[i]); +} + +dc.connect(gain); +dc.start(); + +gain.gain.value = 0.5; + +context.startRendering().then(buffer => { + document.querySelector("pre").innerHTML = buffer.getChannelData(0)[0]; + ok(buffer.getChannelData(0)[0] == 1.0, "Volume was handled properly"); + + context = new OfflineAudioContext(1, 100, samplerate); + var oscillator = context.createOscillator(); + var gain = context.createGain(); + var waveShaper = context.createWaveShaper(); + + oscillator.start(0); + oscillator.connect(gain); + + // to silence + gain.gain.value = 0; + gain.connect(waveShaper); + + // convert all signal into 1.0. The non unity values are to detect the use + // of uninitialized buffers (see Bug 1283910). + waveShaper.curve = new Float32Array([ 0.5, 0.5, 0.5, 0.5, 0.5, 1, 1, 0.5, 0.5, 0.5, 0.5, 0.5 ]); + waveShaper.connect(context.destination); + + context.startRendering().then((buffer) => { + var result = buffer.getChannelData(0); + ok(result.every(x => x === 1), "WaveShaper handles zero gain properly"); + SimpleTest.finish(); + }); +}); +</script> +</body> + diff --git a/dom/media/webaudio/test/test_waveShaperInvalidLengthCurve.html b/dom/media/webaudio/test/test_waveShaperInvalidLengthCurve.html new file mode 100644 index 0000000000..0901521a7b --- /dev/null +++ b/dom/media/webaudio/test/test_waveShaperInvalidLengthCurve.html @@ -0,0 +1,66 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test WaveShaperNode with an invalid curve</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +var gTest = { + length: 2048, + numberOfChannels: 1, + createGraph(context) { + var source = context.createBufferSource(); + source.buffer = this.buffer; + + var shaper = context.createWaveShaper(); + + expectException(() => { + shaper.curve = new Float32Array(0); + }, DOMException.INVALID_STATE_ERR); + + is(shaper.curve, null, "The curve mustn't have been set"); + + expectException(() => { + shaper.curve = new Float32Array(1); + }, DOMException.INVALID_STATE_ERR); + + is(shaper.curve, null, "The curve mustn't have been set"); + + expectNoException(() => { + shaper.curve = new Float32Array(2); + }); + + isnot(shaper.curve, null, "The curve must have been set"); + + expectNoException(() => { + shaper.curve = null; + }); + + is(shaper.curve, null, "The curve must be null by default"); + + source.connect(shaper); + + source.start(0); + return shaper; + }, + createExpectedBuffers(context) { + var expectedBuffer = context.createBuffer(1, 2048, context.sampleRate); + for (var i = 0; i < 2048; ++i) { + expectedBuffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate); + } + this.buffer = expectedBuffer; + return expectedBuffer; + }, +}; + +runTest(); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_waveShaperNoCurve.html b/dom/media/webaudio/test/test_waveShaperNoCurve.html new file mode 100644 index 0000000000..2da0b511af --- /dev/null +++ b/dom/media/webaudio/test/test_waveShaperNoCurve.html @@ -0,0 +1,43 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test WaveShaperNode with no curve</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +var gTest = { + length: 2048, + numberOfChannels: 1, + createGraph(context) { + var source = context.createBufferSource(); + source.buffer = this.buffer; + + var shaper = context.createWaveShaper(); + is(shaper.curve, null, "The shaper curve must be null by default"); + + source.connect(shaper); + + source.start(0); + return shaper; + }, + createExpectedBuffers(context) { + var expectedBuffer = context.createBuffer(1, 2048, context.sampleRate); + for (var i = 0; i < 2048; ++i) { + expectedBuffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate); + } + this.buffer = expectedBuffer; + return expectedBuffer; + }, +}; + +runTest(); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_waveShaperPassThrough.html b/dom/media/webaudio/test/test_waveShaperPassThrough.html new file mode 100644 index 0000000000..d34add9c90 --- /dev/null +++ b/dom/media/webaudio/test/test_waveShaperPassThrough.html @@ -0,0 +1,55 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test WaveShaperNode with passthrough</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="webaudio.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +var gTest = { + length: 4096, + numberOfChannels: 1, + createGraph(context) { + var source = context.createBufferSource(); + source.buffer = this.buffer; + + var shaper = context.createWaveShaper(); + shaper.curve = this.curve; + + var shaperWrapped = SpecialPowers.wrap(shaper); + ok("passThrough" in shaperWrapped, "WaveShaperNode should support the passThrough API"); + shaperWrapped.passThrough = true; + + source.connect(shaper); + + source.start(0); + return shaper; + }, + createExpectedBuffers(context) { + this.buffer = context.createBuffer(1, 4096, context.sampleRate); + for (var i = 1; i < 4095; ++i) { + this.buffer.getChannelData(0)[i] = 2 * (i / 4096) - 1; + } + // Two out of range values + this.buffer.getChannelData(0)[0] = -2; + this.buffer.getChannelData(0)[4095] = 2; + + this.curve = new Float32Array(2048); + for (var i = 0; i < 2048; ++i) { + this.curve[i] = Math.sin(100 * Math.PI * (i + 1) / context.sampleRate); + } + + return [this.buffer]; + }, +}; + +runTest(); + +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/test_webAudio_muteTab.html b/dom/media/webaudio/test/test_webAudio_muteTab.html new file mode 100644 index 0000000000..3a3d60eea7 --- /dev/null +++ b/dom/media/webaudio/test/test_webAudio_muteTab.html @@ -0,0 +1,93 @@ +<!DOCTYPE HTML> +<html> +<head> + <script type="application/javascript" src="mediaStreamPlayback.js"></script> +</head> +<body> +<pre id="test"> + +<script> +/* import-globals-from ../../webrtc/tests/mochitests/mediaStreamPlayback.js */ +createHTML({ + title: "Check tab muting when the tab plays audio via the Web Audio API", + bug: "1346880", + visible: false +}); + +/** + * Check that muting a tab results in no audible audio: mute a tab, in which + * an OscillatorNode is playing. The default audio output device is a + * pulseaudio null-sink. Simulateously, record the other side of the null + * sink, and check that no audio has been written to the sink, because the tab + * was muted. Then, umute the tab and check that audio is being sent to the + * null-sink. */ +runTest(async () => { + if (!SpecialPowers.getCharPref("media.audio_loopback_dev", "")) { + todo(false, "No loopback device set by framework. Try --use-test-media-devices"); + return; + } + + // Mute the tab + await SpecialPowers.toggleMuteState(true, window.top); + + const stream = await getUserMedia({audio: { + noiseSuppression: false, + echoCancellation: false, + autoGainControl: false, + }}); + try { + const ac = new AudioContext(); + const osc = new OscillatorNode(ac); + osc.connect(ac.destination); + osc.start(); + + const analyser = new AudioStreamAnalyser(ac, stream); + // Wait for some time, checking there is only ever silent audio in the + // loopback stream. `waitForAnalysisSuccess` runs off requestAnimationFrame + let silenceFor = 3 / (1 / 60); + await analyser.waitForAnalysisSuccess(array => { + // `array` has values between 0 and 255, 0 being silence. + const sum = array.reduce((acc, v) => { return acc + v; }); + if (sum == 0) { + silenceFor--; + } else { + info(`Sum of the array values ${sum}`); + ok(false, `Found non-silent data in the loopback stream while the tab was muted.`); + return true; + } + if (silenceFor == 0) { + ok(true, "Muting the tab was effective"); + } + return silenceFor == 0; + }); + + // Unmute the tab + await SpecialPowers.toggleMuteState(false, window.top); + + await analyser.waitForAnalysisSuccess(array => { + // `array` has values between 0 and 255, 0 being silence. + const sum = array.reduce((acc, v) => { return acc + v; }); + if (sum != 0) { + info(`Sum after unmuting ${sum}`); + ok(true, "Unmuting the tab was effective"); + return true; + } else { + // Increment again if we find silence. + silenceFor++; + if (silenceFor > 100) { + ok(false, "Unmuting wasn't effective") + return true; + } + return false; + } + }); + } finally { + for (let t of stream.getTracks()) { + t.stop(); + } + } +}); +</script> +</pre> +</body> +</html> diff --git a/dom/media/webaudio/test/ting-44.1k-1ch.ogg b/dom/media/webaudio/test/ting-44.1k-1ch.ogg Binary files differnew file mode 100644 index 0000000000..a11aaf1cbf --- /dev/null +++ b/dom/media/webaudio/test/ting-44.1k-1ch.ogg diff --git a/dom/media/webaudio/test/ting-44.1k-1ch.wav b/dom/media/webaudio/test/ting-44.1k-1ch.wav Binary files differnew file mode 100644 index 0000000000..dd731e354c --- /dev/null +++ b/dom/media/webaudio/test/ting-44.1k-1ch.wav diff --git a/dom/media/webaudio/test/ting-44.1k-2ch.ogg b/dom/media/webaudio/test/ting-44.1k-2ch.ogg Binary files differnew file mode 100644 index 0000000000..94e0014858 --- /dev/null +++ b/dom/media/webaudio/test/ting-44.1k-2ch.ogg diff --git a/dom/media/webaudio/test/ting-44.1k-2ch.wav b/dom/media/webaudio/test/ting-44.1k-2ch.wav Binary files differnew file mode 100644 index 0000000000..b2ef11ecbe --- /dev/null +++ b/dom/media/webaudio/test/ting-44.1k-2ch.wav diff --git a/dom/media/webaudio/test/ting-48k-1ch.ogg b/dom/media/webaudio/test/ting-48k-1ch.ogg Binary files differnew file mode 100644 index 0000000000..f45ce33a58 --- /dev/null +++ b/dom/media/webaudio/test/ting-48k-1ch.ogg diff --git a/dom/media/webaudio/test/ting-48k-1ch.wav b/dom/media/webaudio/test/ting-48k-1ch.wav Binary files differnew file mode 100644 index 0000000000..91874dfbb2 --- /dev/null +++ b/dom/media/webaudio/test/ting-48k-1ch.wav diff --git a/dom/media/webaudio/test/ting-48k-2ch.ogg b/dom/media/webaudio/test/ting-48k-2ch.ogg Binary files differnew file mode 100644 index 0000000000..e4c564abbd --- /dev/null +++ b/dom/media/webaudio/test/ting-48k-2ch.ogg diff --git a/dom/media/webaudio/test/ting-48k-2ch.wav b/dom/media/webaudio/test/ting-48k-2ch.wav Binary files differnew file mode 100644 index 0000000000..6adb8d3e5d --- /dev/null +++ b/dom/media/webaudio/test/ting-48k-2ch.wav diff --git a/dom/media/webaudio/test/ting-dualchannel44.1.wav b/dom/media/webaudio/test/ting-dualchannel44.1.wav Binary files differnew file mode 100644 index 0000000000..62954394d3 --- /dev/null +++ b/dom/media/webaudio/test/ting-dualchannel44.1.wav diff --git a/dom/media/webaudio/test/ting-dualchannel48.wav b/dom/media/webaudio/test/ting-dualchannel48.wav Binary files differnew file mode 100644 index 0000000000..a0b8247888 --- /dev/null +++ b/dom/media/webaudio/test/ting-dualchannel48.wav diff --git a/dom/media/webaudio/test/waveformatextensible.wav b/dom/media/webaudio/test/waveformatextensible.wav Binary files differnew file mode 100644 index 0000000000..f16e588021 --- /dev/null +++ b/dom/media/webaudio/test/waveformatextensible.wav diff --git a/dom/media/webaudio/test/waveformatextensiblebadmask.wav b/dom/media/webaudio/test/waveformatextensiblebadmask.wav Binary files differnew file mode 100644 index 0000000000..3aa55df98e --- /dev/null +++ b/dom/media/webaudio/test/waveformatextensiblebadmask.wav diff --git a/dom/media/webaudio/test/webaudio.js b/dom/media/webaudio/test/webaudio.js new file mode 100644 index 0000000000..049e0e5af3 --- /dev/null +++ b/dom/media/webaudio/test/webaudio.js @@ -0,0 +1,367 @@ +// Helpers for Web Audio tests + +// It is expected that the test defines this. +/* global gTest */ + +function expectException(func, exceptionCode) { + var threw = false; + try { + func(); + } catch (ex) { + threw = true; + is(ex.constructor.name, "DOMException", "Expect a DOM exception"); + is(ex.code, exceptionCode, "Expect the correct exception code"); + } + ok(threw, "The exception was thrown"); +} + +function expectNoException(func) { + var threw = false; + try { + func(); + } catch (ex) { + threw = true; + } + ok(!threw, "An exception was not thrown"); +} + +function expectTypeError(func) { + var threw = false; + try { + func(); + } catch (ex) { + threw = true; + ok(ex instanceof TypeError, "Expect a TypeError"); + } + ok(threw, "The exception was thrown"); +} + +function expectRejectedPromise(that, func, exceptionName) { + var promise = that[func](); + + ok(promise instanceof Promise, "Expect a Promise"); + + promise + .then(function (res) { + ok(false, "Promise resolved when it should have been rejected."); + }) + .catch(function (err) { + is( + err.name, + exceptionName, + "Promise correctly reject with " + exceptionName + ); + }); +} + +function fuzzyCompare(a, b) { + return Math.abs(a - b) < 9e-3; +} + +function compareChannels( + buf1, + buf2, + /*optional*/ length, + /*optional*/ sourceOffset, + /*optional*/ destOffset, + /*optional*/ skipLengthCheck +) { + if (!skipLengthCheck) { + is(buf1.length, buf2.length, "Channels must have the same length"); + } + sourceOffset = sourceOffset || 0; + destOffset = destOffset || 0; + if (length == undefined) { + length = buf1.length - sourceOffset; + } + var difference = 0; + var maxDifference = 0; + var firstBadIndex = -1; + for (var i = 0; i < length; ++i) { + if (!fuzzyCompare(buf1[i + sourceOffset], buf2[i + destOffset])) { + difference++; + maxDifference = Math.max( + maxDifference, + Math.abs(buf1[i + sourceOffset] - buf2[i + destOffset]) + ); + if (firstBadIndex == -1) { + firstBadIndex = i; + } + } + } + + is( + difference, + 0, + "maxDifference: " + + maxDifference + + ", first bad index: " + + firstBadIndex + + " with test-data offset " + + sourceOffset + + " and expected-data offset " + + destOffset + + "; corresponding values " + + buf1[firstBadIndex + sourceOffset] + + " and " + + buf2[firstBadIndex + destOffset] + + " --- differences" + ); +} + +function compareBuffers(got, expected) { + if (got.numberOfChannels != expected.numberOfChannels) { + is( + got.numberOfChannels, + expected.numberOfChannels, + "Correct number of buffer channels" + ); + return; + } + if (got.length != expected.length) { + is(got.length, expected.length, "Correct buffer length"); + return; + } + if (got.sampleRate != expected.sampleRate) { + is(got.sampleRate, expected.sampleRate, "Correct sample rate"); + return; + } + + for (var i = 0; i < got.numberOfChannels; ++i) { + compareChannels( + got.getChannelData(i), + expected.getChannelData(i), + got.length, + 0, + 0, + true + ); + } +} + +/** + * Compute the root mean square (RMS, + * <http://en.wikipedia.org/wiki/Root_mean_square>) of a channel of a slice + * (defined by `start` and `end`) of an AudioBuffer. + * + * This is useful to detect that a buffer is noisy or silent. + */ +function rms(audiobuffer, channel = 0, start = 0, end = audiobuffer.length) { + var buffer = audiobuffer.getChannelData(channel); + var rms = 0; + for (var i = start; i < end; i++) { + rms += buffer[i] * buffer[i]; + } + + rms /= buffer.length; + rms = Math.sqrt(rms); + return rms; +} + +function getEmptyBuffer(context, length) { + return context.createBuffer( + gTest.numberOfChannels, + length, + context.sampleRate + ); +} + +function isChannelSilent(channel) { + for (var i = 0; i < channel.length; ++i) { + if (channel[i] != 0.0) { + return false; + } + } + return true; +} + +const HRTFPannersByRate = new Map(); +/** + * Return a promise that resolves when PannerNodes with HRTF panningModel in + * an AudioContext of the specified sample rate will be ready to produce + * non-zero output. Before the HRIR database is loaded, such PannerNodes + * produce zero output. + */ +async function promiseHRTFReady(sampleRate) { + if (HRTFPannersByRate.has(sampleRate)) { + return; + } + + const ctx = new AudioContext({ sampleRate }); + const processor = ctx.createScriptProcessor(4096, 2, 0); + const panner = new PannerNode(ctx, { panningModel: "HRTF" }); + panner.connect(processor); + const oscillator = ctx.createOscillator(); + oscillator.connect(panner); + oscillator.start(0); + + await new Promise(r => { + processor.onaudioprocess = e => { + if (!isChannelSilent(e.inputBuffer.getChannelData(0))) { + r(); + } + }; + }); + + ctx.suspend(); + oscillator.disconnect(); + panner.disconnect(); + processor.onaudioprocess = null; + // Keep a reference to the panner so that the database is not unloaded. + HRTFPannersByRate.set(sampleRate, panner); +} + +/** + * This function assumes that the test file defines a single gTest variable with + * the following properties and methods: + * + * + numberOfChannels: optional property which specifies the number of channels + * in the output. The default value is 2. + * + createGraph: mandatory method which takes a context object and does + * everything needed in order to set up the Web Audio graph. + * This function returns the node to be inspected. + * + createGraphAsync: async version of createGraph. This function takes + * a callback which should be called with an argument + * set to the node to be inspected when the callee is + * ready to proceed with the test. Either this function + * or createGraph must be provided. + * + createExpectedBuffers: optional method which takes a context object and + * returns either one expected buffer or an array of + * them, designating what is expected to be observed + * in the output. If omitted, the output is expected + * to be silence. All buffers must have the same + * length, which must be a bufferSize supported by + * ScriptProcessorNode. This function is guaranteed + * to be called before createGraph. + * + length: property equal to the total number of frames which we are waiting + * to see in the output, mandatory if createExpectedBuffers is not + * provided, in which case it must be a bufferSize supported by + * ScriptProcessorNode (256, 512, 1024, 2048, 4096, 8192, or 16384). + * If createExpectedBuffers is provided then this must be equal to + * the number of expected buffers * the expected buffer length. + * + * + skipOfflineContextTests: optional. when true, skips running tests on an offline + * context by circumventing testOnOfflineContext. + */ +function runTest() { + function done() { + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + function runTestFunction() { + if (!gTest.numberOfChannels) { + gTest.numberOfChannels = 2; // default + } + + var testLength; + + function runTestOnContext(context, callback, testOutput) { + if (!gTest.createExpectedBuffers) { + // Assume that the output is silence + var expectedBuffers = getEmptyBuffer(context, gTest.length); + } else { + var expectedBuffers = gTest.createExpectedBuffers(context); + } + if (!(expectedBuffers instanceof Array)) { + expectedBuffers = [expectedBuffers]; + } + var expectedFrames = 0; + for (var i = 0; i < expectedBuffers.length; ++i) { + is( + expectedBuffers[i].numberOfChannels, + gTest.numberOfChannels, + "Correct number of channels for expected buffer " + i + ); + expectedFrames += expectedBuffers[i].length; + } + if (gTest.length && gTest.createExpectedBuffers) { + is(expectedFrames, gTest.length, "Correct number of expected frames"); + } + + if (gTest.createGraphAsync) { + gTest.createGraphAsync(context, function (nodeToInspect) { + testOutput(nodeToInspect, expectedBuffers, callback); + }); + } else { + testOutput(gTest.createGraph(context), expectedBuffers, callback); + } + } + + function testOnNormalContext(callback) { + function testOutput(nodeToInspect, expectedBuffers, callback) { + testLength = 0; + var sp = context.createScriptProcessor( + expectedBuffers[0].length, + gTest.numberOfChannels, + 0 + ); + nodeToInspect.connect(sp); + sp.onaudioprocess = function (e) { + var expectedBuffer = expectedBuffers.shift(); + testLength += expectedBuffer.length; + compareBuffers(e.inputBuffer, expectedBuffer); + if (!expectedBuffers.length) { + sp.onaudioprocess = null; + callback(); + } + }; + } + var context = new AudioContext(); + runTestOnContext(context, callback, testOutput); + } + + function testOnOfflineContext(callback, sampleRate) { + function testOutput(nodeToInspect, expectedBuffers, callback) { + nodeToInspect.connect(context.destination); + context.oncomplete = function (e) { + var samplesSeen = 0; + while (expectedBuffers.length) { + var expectedBuffer = expectedBuffers.shift(); + is( + e.renderedBuffer.numberOfChannels, + expectedBuffer.numberOfChannels, + "Correct number of input buffer channels" + ); + for (var i = 0; i < e.renderedBuffer.numberOfChannels; ++i) { + compareChannels( + e.renderedBuffer.getChannelData(i), + expectedBuffer.getChannelData(i), + expectedBuffer.length, + samplesSeen, + undefined, + true + ); + } + samplesSeen += expectedBuffer.length; + } + callback(); + }; + context.startRendering(); + } + + var context = new OfflineAudioContext( + gTest.numberOfChannels, + testLength, + sampleRate + ); + runTestOnContext(context, callback, testOutput); + } + + testOnNormalContext(function () { + if (!gTest.skipOfflineContextTests) { + testOnOfflineContext(function () { + testOnOfflineContext(done, 44100); + }, 48000); + } else { + done(); + } + }); + } + + if (document.readyState !== "complete") { + addLoadEvent(runTestFunction); + } else { + runTestFunction(); + } +} |