diff options
Diffstat (limited to 'dom/media/webaudio/OscillatorNode.cpp')
-rw-r--r-- | dom/media/webaudio/OscillatorNode.cpp | 542 |
1 files changed, 542 insertions, 0 deletions
diff --git a/dom/media/webaudio/OscillatorNode.cpp b/dom/media/webaudio/OscillatorNode.cpp new file mode 100644 index 0000000000..b18b0a0fe1 --- /dev/null +++ b/dom/media/webaudio/OscillatorNode.cpp @@ -0,0 +1,542 @@ +/* -*- 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, AudioTimelineEvent& aEvent) override { + mRecomputeParameters = true; + + MOZ_ASSERT(mDestination); + + WebAudioUtils::ConvertAudioTimelineEventToTicks(aEvent, 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(TrackTime ticks, size_t count) { + double frequency, detune; + + // Shortcut if frequency-related AudioParam are not automated, and we + // already have computed the frequency information and related parameters. + if (!ParametersMayNeedUpdate()) { + return false; + } + + bool simpleFrequency = mFrequency.HasSimpleValue(); + bool simpleDetune = mDetune.HasSimpleValue(); + + if (simpleFrequency) { + frequency = mFrequency.GetValue(); + } else { + frequency = mFrequency.GetValueAtTime(ticks, count); + } + if (simpleDetune) { + detune = mDetune.GetValue(); + } else { + detune = mDetune.GetValueAtTime(ticks, count); + } + + float finalFrequency = frequency * exp2(detune / 1200.); + float signalPeriod = mSource->mSampleRate / finalFrequency; + mRecomputeParameters = false; + + mPhaseIncrement = 2 * M_PI / signalPeriod; + + if (finalFrequency != mFinalFrequency) { + mFinalFrequency = finalFrequency; + return true; + } + return false; + } + + 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, TrackTime ticks, uint32_t aStart, + uint32_t aEnd) { + for (uint32_t i = aStart; i < aEnd; ++i) { + // We ignore the return value, changing the frequency has no impact on + // performances here. + UpdateParametersIfNeeded(ticks, i); + + aOutput[i] = sin(mPhase); + + IncrementPhase(); + } + } + + bool ParametersMayNeedUpdate() { + return !mDetune.HasSimpleValue() || !mFrequency.HasSimpleValue() || + mRecomputeParameters; + } + + void ComputeCustom(float* aOutput, TrackTime ticks, uint32_t aStart, + uint32_t aEnd) { + 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(ticks, aStart); + + 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(ticks, i); + } + // 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) { + ComputeSilence(aOutput); + + } else { + aOutput->AllocateChannels(1); + float* output = aOutput->ChannelFloatsForWrite(0); + + uint32_t start, end; + FillBounds(output, ticks, start, end); + + // Synthesize the correct waveform. + switch (mType) { + case OscillatorType::Sine: + ComputeSine(output, ticks, start, end); + break; + case OscillatorType::Square: + case OscillatorType::Triangle: + case OscillatorType::Sawtooth: + case OscillatorType::Custom: + ComputeCustom(output, ticks, start, end); + break; + default: + ComputeSilence(aOutput); + }; + } + + 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; + 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 |