dom/media/webaudio/OscillatorNode.cpp
author Mozilla Releng Treescript <release+treescript@mozilla.org>
Thu, 11 Aug 2022 19:04:36 +0000
changeset 626815 17df1e9794687c0d56a85bc975e6b650c911b69f
parent 578391 70e615eb295d5ebe006aa15e2a07f3ec8f4c1958
permissions -rw-r--r--
no bug - Bumping Firefox l10n changesets r=release a=l10n-bump DONTBUILD be -> 6daa5b6b8fd1fa77bccf62322096327dccd066e0 el -> bcd4d4b244dfa8172b68b41accd737fd36ed7dbd fi -> f037943462ab2b512cb269433de93c9344a5345e fr -> 1b2091cc79db85a931b133b75d22e257f1f940fc gn -> 6cfc80f9de35eac5dfed526526b794a4a4c32028 it -> 009103209ccf4d86f65499afebe7ef92068aaf61 nb-NO -> 74148db8148b6771b80691f557bfe84c352c2355 nn-NO -> 5da86b9cec5a3f94dccc7410559a3fca0f223cdf pt-PT -> 9b5667ea3729055fe486763b7834c2982efddaed sl -> c321d54e13ec17376bc27de22e149ca56a97115c zh-CN -> 89082f3a883b48bf9c0497416ee3013dd0797bf0

/* -*- 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"

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");

    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