summaryrefslogtreecommitdiffstats
path: root/dom/media/webaudio/OscillatorNode.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'dom/media/webaudio/OscillatorNode.cpp')
-rw-r--r--dom/media/webaudio/OscillatorNode.cpp542
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