diff options
Diffstat (limited to 'dom/media/webaudio/AudioEventTimeline.h')
-rw-r--r-- | dom/media/webaudio/AudioEventTimeline.h | 418 |
1 files changed, 418 insertions, 0 deletions
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 |