2013-06-11 00:07:55 +04:00
|
|
|
/* -*- 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"
|
2016-06-07 23:10:18 +03:00
|
|
|
#include "nsAutoPtr.h"
|
2016-04-13 22:31:50 +03:00
|
|
|
#include "AlignmentUtils.h"
|
2013-06-11 00:07:55 +04:00
|
|
|
#include "AudioNodeEngine.h"
|
|
|
|
#include "AudioNodeStream.h"
|
2013-06-11 00:09:12 +04:00
|
|
|
#include "blink/Reverb.h"
|
2013-08-15 23:44:14 +04:00
|
|
|
#include "PlayingRefChangeHandler.h"
|
2013-06-11 00:07:55 +04:00
|
|
|
|
|
|
|
namespace mozilla {
|
|
|
|
namespace dom {
|
|
|
|
|
2014-04-25 20:49:00 +04:00
|
|
|
NS_IMPL_CYCLE_COLLECTION_INHERITED(ConvolverNode, AudioNode, mBuffer)
|
2013-06-11 00:07:55 +04:00
|
|
|
|
2017-08-30 02:02:48 +03:00
|
|
|
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ConvolverNode)
|
2013-06-11 00:07:55 +04:00
|
|
|
NS_INTERFACE_MAP_END_INHERITING(AudioNode)
|
|
|
|
|
|
|
|
NS_IMPL_ADDREF_INHERITED(ConvolverNode, AudioNode)
|
|
|
|
NS_IMPL_RELEASE_INHERITED(ConvolverNode, AudioNode)
|
|
|
|
|
2015-04-28 09:42:00 +03:00
|
|
|
class ConvolverNodeEngine final : public AudioNodeEngine
|
2013-06-11 00:07:55 +04:00
|
|
|
{
|
2013-10-25 03:12:12 +04:00
|
|
|
typedef PlayingRefChangeHandler PlayingRefChanged;
|
2013-06-11 00:07:55 +04:00
|
|
|
public:
|
|
|
|
ConvolverNodeEngine(AudioNode* aNode, bool aNormalize)
|
|
|
|
: AudioNodeEngine(aNode)
|
2017-08-29 06:40:11 +03:00
|
|
|
, mBufferLength(0)
|
2013-07-04 05:38:31 +04:00
|
|
|
, mLeftOverData(INT32_MIN)
|
2013-06-11 00:09:12 +04:00
|
|
|
, mSampleRate(0.0f)
|
|
|
|
, mUseBackgroundThreads(!aNode->Context()->IsOffline())
|
2013-06-11 00:07:55 +04:00
|
|
|
, mNormalize(aNormalize)
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
enum Parameters {
|
2017-08-29 06:40:11 +03:00
|
|
|
BUFFER_LENGTH,
|
2013-06-11 00:09:12 +04:00
|
|
|
SAMPLE_RATE,
|
2013-06-11 00:07:55 +04:00
|
|
|
NORMALIZE
|
|
|
|
};
|
2016-01-18 06:22:51 +03:00
|
|
|
void SetInt32Parameter(uint32_t aIndex, int32_t aParam) override
|
2013-06-11 00:07:55 +04:00
|
|
|
{
|
|
|
|
switch (aIndex) {
|
2017-08-29 06:40:11 +03:00
|
|
|
case BUFFER_LENGTH:
|
|
|
|
// BUFFER_LENGTH is the first parameter that we set when setting a new buffer,
|
|
|
|
// so we should be careful to invalidate the rest of our state here.
|
|
|
|
mSampleRate = 0.0f;
|
|
|
|
mBufferLength = aParam;
|
|
|
|
mLeftOverData = INT32_MIN;
|
|
|
|
break;
|
2013-06-11 00:07:55 +04:00
|
|
|
case NORMALIZE:
|
|
|
|
mNormalize = !!aParam;
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
NS_ERROR("Bad ConvolverNodeEngine Int32Parameter");
|
|
|
|
}
|
|
|
|
}
|
2016-01-18 06:22:51 +03:00
|
|
|
void SetDoubleParameter(uint32_t aIndex, double aParam) override
|
2013-06-11 00:09:12 +04:00
|
|
|
{
|
|
|
|
switch (aIndex) {
|
|
|
|
case SAMPLE_RATE:
|
|
|
|
mSampleRate = aParam;
|
2017-08-08 07:01:57 +03:00
|
|
|
// The buffer is passed after the sample rate.
|
|
|
|
// mReverb will be set using this sample rate when the buffer is received.
|
2013-06-11 00:09:12 +04:00
|
|
|
break;
|
|
|
|
default:
|
|
|
|
NS_ERROR("Bad ConvolverNodeEngine DoubleParameter");
|
|
|
|
}
|
|
|
|
}
|
2017-08-29 06:40:11 +03:00
|
|
|
void SetBuffer(already_AddRefed<ThreadSharedFloatArrayBufferList> aBuffer) override
|
2013-06-11 00:07:55 +04:00
|
|
|
{
|
2017-08-29 06:40:11 +03:00
|
|
|
RefPtr<ThreadSharedFloatArrayBufferList> buffer = aBuffer;
|
|
|
|
|
2013-06-11 00:09:12 +04:00
|
|
|
// 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;
|
|
|
|
|
2017-08-29 06:40:11 +03:00
|
|
|
if (!buffer || !mBufferLength || !mSampleRate) {
|
2013-06-11 00:09:12 +04:00
|
|
|
mReverb = nullptr;
|
2017-08-29 06:40:11 +03:00
|
|
|
mLeftOverData = INT32_MIN;
|
2013-06-11 00:09:12 +04:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2017-08-29 06:40:11 +03:00
|
|
|
mReverb = new WebCore::Reverb(buffer, mBufferLength,
|
|
|
|
MaxFFTSize, mUseBackgroundThreads,
|
2013-06-11 00:09:12 +04:00
|
|
|
mNormalize, mSampleRate);
|
2013-06-11 00:07:55 +04:00
|
|
|
}
|
|
|
|
|
2016-01-18 06:22:51 +03:00
|
|
|
void ProcessBlock(AudioNodeStream* aStream,
|
|
|
|
GraphTime aFrom,
|
|
|
|
const AudioBlock& aInput,
|
|
|
|
AudioBlock* aOutput,
|
|
|
|
bool* aFinished) override
|
2013-06-11 00:07:55 +04:00
|
|
|
{
|
2013-06-11 00:09:12 +04:00
|
|
|
if (!mReverb) {
|
2015-08-21 08:14:14 +03:00
|
|
|
aOutput->SetNull(WEBAUDIO_BLOCK_SIZE);
|
2013-06-11 00:09:12 +04:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2015-09-03 10:01:50 +03:00
|
|
|
AudioBlock input = aInput;
|
2013-06-11 00:09:12 +04:00
|
|
|
if (aInput.IsNull()) {
|
2013-10-25 03:12:13 +04:00
|
|
|
if (mLeftOverData > 0) {
|
|
|
|
mLeftOverData -= WEBAUDIO_BLOCK_SIZE;
|
2015-09-03 10:01:50 +03:00
|
|
|
input.AllocateChannels(1);
|
2013-10-25 03:12:13 +04:00
|
|
|
WriteZeroesToAudioBlock(&input, 0, WEBAUDIO_BLOCK_SIZE);
|
|
|
|
} else {
|
|
|
|
if (mLeftOverData != INT32_MIN) {
|
|
|
|
mLeftOverData = INT32_MIN;
|
2015-10-22 23:37:45 +03:00
|
|
|
aStream->ScheduleCheckForInactive();
|
2015-10-18 08:24:48 +03:00
|
|
|
RefPtr<PlayingRefChanged> refchanged =
|
2013-10-25 03:12:13 +04:00
|
|
|
new PlayingRefChanged(aStream, PlayingRefChanged::RELEASE);
|
2017-06-29 21:30:57 +03:00
|
|
|
aStream->Graph()->DispatchToMainThreadAfterStreamStateUpdate(
|
|
|
|
refchanged.forget());
|
2013-10-25 03:12:13 +04:00
|
|
|
}
|
|
|
|
aOutput->SetNull(WEBAUDIO_BLOCK_SIZE);
|
|
|
|
return;
|
2013-07-04 05:38:31 +04:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if (aInput.mVolume != 1.0f) {
|
|
|
|
// Pre-multiply the input's volume
|
2015-09-03 08:30:16 +03:00
|
|
|
uint32_t numChannels = aInput.ChannelCount();
|
2015-09-03 10:01:50 +03:00
|
|
|
input.AllocateChannels(numChannels);
|
2013-07-04 05:38:31 +04:00
|
|
|
for (uint32_t i = 0; i < numChannels; ++i) {
|
|
|
|
const float* src = static_cast<const float*>(aInput.mChannelData[i]);
|
2015-07-22 08:59:21 +03:00
|
|
|
float* dest = input.ChannelFloatsForWrite(i);
|
2013-07-18 18:22:47 +04:00
|
|
|
AudioBlockCopyChannelWithScale(src, aInput.mVolume, dest);
|
2013-07-04 05:38:31 +04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (mLeftOverData <= 0) {
|
2015-10-18 08:24:48 +03:00
|
|
|
RefPtr<PlayingRefChanged> refchanged =
|
2013-07-04 05:38:31 +04:00
|
|
|
new PlayingRefChanged(aStream, PlayingRefChanged::ADDREF);
|
2017-06-29 21:30:57 +03:00
|
|
|
aStream->Graph()->DispatchToMainThreadAfterStreamStateUpdate(
|
|
|
|
refchanged.forget());
|
2013-06-11 00:09:12 +04:00
|
|
|
}
|
2017-08-29 06:40:11 +03:00
|
|
|
mLeftOverData = mBufferLength;
|
2013-07-04 05:38:31 +04:00
|
|
|
MOZ_ASSERT(mLeftOverData > 0);
|
2013-06-11 00:09:12 +04:00
|
|
|
}
|
2015-09-03 10:01:50 +03:00
|
|
|
aOutput->AllocateChannels(2);
|
2013-06-11 00:09:12 +04:00
|
|
|
|
2015-11-03 06:35:32 +03:00
|
|
|
mReverb->process(&input, aOutput);
|
2013-06-11 00:07:55 +04:00
|
|
|
}
|
|
|
|
|
2016-01-18 06:22:51 +03:00
|
|
|
bool IsActive() const override
|
2015-09-08 23:54:03 +03:00
|
|
|
{
|
|
|
|
return mLeftOverData != INT32_MIN;
|
|
|
|
}
|
|
|
|
|
2016-01-18 06:22:51 +03:00
|
|
|
size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override
|
2014-04-13 22:08:10 +04:00
|
|
|
{
|
|
|
|
size_t amount = AudioNodeEngine::SizeOfExcludingThis(aMallocSizeOf);
|
|
|
|
|
|
|
|
if (mReverb) {
|
|
|
|
amount += mReverb->sizeOfIncludingThis(aMallocSizeOf);
|
|
|
|
}
|
|
|
|
|
|
|
|
return amount;
|
|
|
|
}
|
|
|
|
|
2016-01-18 06:22:51 +03:00
|
|
|
size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override
|
2014-04-13 22:08:10 +04:00
|
|
|
{
|
|
|
|
return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
|
|
|
|
}
|
|
|
|
|
2013-06-11 00:07:55 +04:00
|
|
|
private:
|
2013-06-11 00:09:12 +04:00
|
|
|
nsAutoPtr<WebCore::Reverb> mReverb;
|
2017-08-29 06:40:11 +03:00
|
|
|
int32_t mBufferLength;
|
2013-07-04 05:38:31 +04:00
|
|
|
int32_t mLeftOverData;
|
2013-06-11 00:09:12 +04:00
|
|
|
float mSampleRate;
|
|
|
|
bool mUseBackgroundThreads;
|
2013-06-11 00:07:55 +04:00
|
|
|
bool mNormalize;
|
|
|
|
};
|
|
|
|
|
|
|
|
ConvolverNode::ConvolverNode(AudioContext* aContext)
|
|
|
|
: AudioNode(aContext,
|
|
|
|
2,
|
|
|
|
ChannelCountMode::Clamped_max,
|
|
|
|
ChannelInterpretation::Speakers)
|
|
|
|
, mNormalize(true)
|
|
|
|
{
|
|
|
|
ConvolverNodeEngine* engine = new ConvolverNodeEngine(this, mNormalize);
|
2015-09-08 16:22:16 +03:00
|
|
|
mStream = AudioNodeStream::Create(aContext, engine,
|
2016-09-05 18:25:41 +03:00
|
|
|
AudioNodeStream::NO_STREAM_FLAGS,
|
|
|
|
aContext->Graph());
|
2013-06-11 00:07:55 +04:00
|
|
|
}
|
|
|
|
|
2016-12-15 21:24:42 +03:00
|
|
|
/* static */ already_AddRefed<ConvolverNode>
|
|
|
|
ConvolverNode::Create(JSContext* aCx, AudioContext& aAudioContext,
|
|
|
|
const ConvolverOptions& aOptions,
|
|
|
|
ErrorResult& aRv)
|
2014-07-09 01:23:17 +04:00
|
|
|
{
|
2016-12-15 21:24:42 +03:00
|
|
|
if (aAudioContext.CheckClosed(aRv)) {
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
|
|
|
|
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();
|
2014-07-09 01:23:17 +04:00
|
|
|
}
|
|
|
|
|
2014-04-13 22:08:10 +04:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2013-06-11 00:07:55 +04:00
|
|
|
JSObject*
|
Bug 1117172 part 3. Change the wrappercached WrapObject methods to allow passing in aGivenProto. r=peterv
The only manual changes here are to BindingUtils.h, BindingUtils.cpp,
Codegen.py, Element.cpp, IDBFileRequest.cpp, IDBObjectStore.cpp,
dom/workers/Navigator.cpp, WorkerPrivate.cpp, DeviceStorageRequestChild.cpp,
Notification.cpp, nsGlobalWindow.cpp, MessagePort.cpp, nsJSEnvironment.cpp,
Sandbox.cpp, XPCConvert.cpp, ExportHelpers.cpp, and DataStoreService.cpp. The
rest of this diff was generated by running the following commands:
find . -name "*.h" -o -name "*.cpp" | xargs perl -pi -e 'BEGIN { $/ = undef } s/(WrapObjectInternal\(JSContext *\* *(?:aCx|cx|aContext|aCtx|js))\)/\1, JS::Handle<JSObject*> aGivenProto)/g'
find . -name "*.h" -o -name "*.cpp" | xargs perl -pi -e 'BEGIN { $/ = undef } s/(WrapObjectInternal\((?:aCx|cx|aContext|aCtx|js))\)/\1, aGivenProto)/g'
find . -name "*.h" -o -name "*.cpp" | xargs perl -pi -e 'BEGIN { $/ = undef } s/(WrapNode\(JSContext *\* *(?:aCx|cx|aContext|aCtx|js))\)/\1, JS::Handle<JSObject*> aGivenProto)/g'
find . -name "*.h" -o -name "*.cpp" | xargs perl -pi -e 'BEGIN { $/ = undef } s/(WrapNode\((?:aCx|cx|aContext|aCtx|js))\)/\1, aGivenProto)/g'
find . -name "*.h" -o -name "*.cpp" | xargs perl -pi -e 'BEGIN { $/ = undef } s/(WrapObject\(JSContext *\* *(?:aCx|cx|aContext|aCtx|js))\)/\1, JS::Handle<JSObject*> aGivenProto)/g'
find . -name "*.h" -o -name "*.cpp" | xargs perl -pi -e 'BEGIN { $/ = undef } s/(Binding(?:_workers)?::Wrap\((?:aCx|cx|aContext|aCtx|js), [^,)]+)\)/\1, aGivenProto)/g'
2015-03-19 17:13:33 +03:00
|
|
|
ConvolverNode::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
|
2013-06-11 00:07:55 +04:00
|
|
|
{
|
Bug 1117172 part 3. Change the wrappercached WrapObject methods to allow passing in aGivenProto. r=peterv
The only manual changes here are to BindingUtils.h, BindingUtils.cpp,
Codegen.py, Element.cpp, IDBFileRequest.cpp, IDBObjectStore.cpp,
dom/workers/Navigator.cpp, WorkerPrivate.cpp, DeviceStorageRequestChild.cpp,
Notification.cpp, nsGlobalWindow.cpp, MessagePort.cpp, nsJSEnvironment.cpp,
Sandbox.cpp, XPCConvert.cpp, ExportHelpers.cpp, and DataStoreService.cpp. The
rest of this diff was generated by running the following commands:
find . -name "*.h" -o -name "*.cpp" | xargs perl -pi -e 'BEGIN { $/ = undef } s/(WrapObjectInternal\(JSContext *\* *(?:aCx|cx|aContext|aCtx|js))\)/\1, JS::Handle<JSObject*> aGivenProto)/g'
find . -name "*.h" -o -name "*.cpp" | xargs perl -pi -e 'BEGIN { $/ = undef } s/(WrapObjectInternal\((?:aCx|cx|aContext|aCtx|js))\)/\1, aGivenProto)/g'
find . -name "*.h" -o -name "*.cpp" | xargs perl -pi -e 'BEGIN { $/ = undef } s/(WrapNode\(JSContext *\* *(?:aCx|cx|aContext|aCtx|js))\)/\1, JS::Handle<JSObject*> aGivenProto)/g'
find . -name "*.h" -o -name "*.cpp" | xargs perl -pi -e 'BEGIN { $/ = undef } s/(WrapNode\((?:aCx|cx|aContext|aCtx|js))\)/\1, aGivenProto)/g'
find . -name "*.h" -o -name "*.cpp" | xargs perl -pi -e 'BEGIN { $/ = undef } s/(WrapObject\(JSContext *\* *(?:aCx|cx|aContext|aCtx|js))\)/\1, JS::Handle<JSObject*> aGivenProto)/g'
find . -name "*.h" -o -name "*.cpp" | xargs perl -pi -e 'BEGIN { $/ = undef } s/(Binding(?:_workers)?::Wrap\((?:aCx|cx|aContext|aCtx|js), [^,)]+)\)/\1, aGivenProto)/g'
2015-03-19 17:13:33 +03:00
|
|
|
return ConvolverNodeBinding::Wrap(aCx, this, aGivenProto);
|
2013-06-11 00:07:55 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
ConvolverNode::SetBuffer(JSContext* aCx, AudioBuffer* aBuffer, ErrorResult& aRv)
|
|
|
|
{
|
2013-07-02 22:15:32 +04:00
|
|
|
if (aBuffer) {
|
|
|
|
switch (aBuffer->NumberOfChannels()) {
|
|
|
|
case 1:
|
|
|
|
case 2:
|
|
|
|
case 4:
|
|
|
|
// Supported number of channels
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
aRv.Throw(NS_ERROR_DOM_SYNTAX_ERR);
|
|
|
|
return;
|
|
|
|
}
|
2013-06-11 00:07:55 +04:00
|
|
|
}
|
|
|
|
|
2017-08-29 06:40:11 +03:00
|
|
|
mBuffer = aBuffer;
|
|
|
|
|
2013-06-11 00:07:55 +04:00
|
|
|
// Send the buffer to the stream
|
2015-07-02 08:36:07 +03:00
|
|
|
AudioNodeStream* ns = mStream;
|
2013-06-11 00:07:55 +04:00
|
|
|
MOZ_ASSERT(ns, "Why don't we have a stream here?");
|
2017-08-29 06:40:11 +03:00
|
|
|
if (mBuffer) {
|
|
|
|
uint32_t length = mBuffer->Length();
|
|
|
|
RefPtr<ThreadSharedFloatArrayBufferList> data =
|
|
|
|
mBuffer->GetThreadSharedChannelsForRate(aCx);
|
|
|
|
SendInt32ParameterToStream(ConvolverNodeEngine::BUFFER_LENGTH, length);
|
2013-06-11 00:09:12 +04:00
|
|
|
SendDoubleParameterToStream(ConvolverNodeEngine::SAMPLE_RATE,
|
2017-08-29 06:40:11 +03:00
|
|
|
mBuffer->SampleRate());
|
|
|
|
ns->SetBuffer(data.forget());
|
2013-06-11 00:07:55 +04:00
|
|
|
} else {
|
2017-08-29 06:40:11 +03:00
|
|
|
ns->SetBuffer(nullptr);
|
2013-06-11 00:07:55 +04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
ConvolverNode::SetNormalize(bool aNormalize)
|
|
|
|
{
|
|
|
|
mNormalize = aNormalize;
|
|
|
|
SendInt32ParameterToStream(ConvolverNodeEngine::NORMALIZE, aNormalize);
|
|
|
|
}
|
|
|
|
|
2015-07-13 18:25:42 +03:00
|
|
|
} // namespace dom
|
|
|
|
} // namespace mozilla
|