michael@0: /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* vim:set ts=2 sw=2 sts=2 et cindent: */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: #ifndef AudioEventTimeline_h_ michael@0: #define AudioEventTimeline_h_ michael@0: michael@0: #include michael@0: #include "mozilla/Assertions.h" michael@0: #include "mozilla/FloatingPoint.h" michael@0: #include "mozilla/TypedEnum.h" michael@0: #include "mozilla/PodOperations.h" michael@0: michael@0: #include "nsTArray.h" michael@0: #include "math.h" michael@0: michael@0: namespace mozilla { michael@0: michael@0: namespace dom { michael@0: michael@0: // This is an internal helper class and should not be used outside of this header. michael@0: struct AudioTimelineEvent { michael@0: enum Type MOZ_ENUM_TYPE(uint32_t) { michael@0: SetValue, michael@0: LinearRamp, michael@0: ExponentialRamp, michael@0: SetTarget, michael@0: SetValueCurve michael@0: }; michael@0: michael@0: AudioTimelineEvent(Type aType, double aTime, float aValue, double aTimeConstant = 0.0, michael@0: float aDuration = 0.0, const float* aCurve = nullptr, uint32_t aCurveLength = 0) michael@0: : mType(aType) michael@0: , mTimeConstant(aTimeConstant) michael@0: , mDuration(aDuration) michael@0: #ifdef DEBUG michael@0: , mTimeIsInTicks(false) michael@0: #endif michael@0: { michael@0: mTime = aTime; michael@0: if (aType == AudioTimelineEvent::SetValueCurve) { michael@0: SetCurveParams(aCurve, aCurveLength); michael@0: } else { michael@0: mValue = aValue; michael@0: } michael@0: } michael@0: michael@0: AudioTimelineEvent(const AudioTimelineEvent& rhs) michael@0: { michael@0: PodCopy(this, &rhs, 1); michael@0: if (rhs.mType == AudioTimelineEvent::SetValueCurve) { michael@0: SetCurveParams(rhs.mCurve, rhs.mCurveLength); michael@0: } michael@0: } michael@0: michael@0: ~AudioTimelineEvent() michael@0: { michael@0: if (mType == AudioTimelineEvent::SetValueCurve) { michael@0: delete[] mCurve; michael@0: } michael@0: } michael@0: michael@0: bool IsValid() const michael@0: { michael@0: if (mType == AudioTimelineEvent::SetValueCurve) { michael@0: if (!mCurve || !mCurveLength) { michael@0: return false; michael@0: } michael@0: for (uint32_t i = 0; i < mCurveLength; ++i) { michael@0: if (!IsValid(mCurve[i])) { michael@0: return false; michael@0: } michael@0: } michael@0: } michael@0: michael@0: return IsValid(mTime) && michael@0: IsValid(mValue) && michael@0: IsValid(mTimeConstant) && michael@0: IsValid(mDuration); michael@0: } michael@0: michael@0: template michael@0: TimeType Time() const; michael@0: michael@0: void SetTimeInTicks(int64_t aTimeInTicks) michael@0: { michael@0: mTimeInTicks = aTimeInTicks; michael@0: #ifdef DEBUG michael@0: mTimeIsInTicks = true; michael@0: #endif michael@0: } michael@0: michael@0: void SetCurveParams(const float* aCurve, uint32_t aCurveLength) { michael@0: mCurveLength = aCurveLength; michael@0: if (aCurveLength) { michael@0: mCurve = new float[aCurveLength]; michael@0: PodCopy(mCurve, aCurve, aCurveLength); michael@0: } else { michael@0: mCurve = nullptr; michael@0: } michael@0: } michael@0: michael@0: Type mType; michael@0: union { michael@0: float mValue; michael@0: uint32_t mCurveLength; michael@0: }; michael@0: // The time for an event can either be in absolute value or in ticks. michael@0: // Initially the time of the event is always in absolute value. michael@0: // In order to convert it to ticks, call SetTimeInTicks. Once this michael@0: // method has been called for an event, the time cannot be converted michael@0: // back to absolute value. michael@0: union { michael@0: double mTime; michael@0: int64_t mTimeInTicks; michael@0: }; michael@0: // mCurve contains a buffer of SetValueCurve samples. We sample the michael@0: // values in the buffer depending on how far along we are in time. michael@0: // If we're at time T and the event has started as time T0 and has a michael@0: // duration of D, we sample the buffer at floor(mCurveLength*(T-T0)/D) michael@0: // if T michael@0: inline double AudioTimelineEvent::Time() const michael@0: { michael@0: MOZ_ASSERT(!mTimeIsInTicks); michael@0: return mTime; michael@0: } michael@0: michael@0: template <> michael@0: inline int64_t AudioTimelineEvent::Time() const michael@0: { michael@0: MOZ_ASSERT(mTimeIsInTicks); michael@0: return mTimeInTicks; michael@0: } michael@0: michael@0: /** michael@0: * This class will be instantiated with different template arguments for testing and michael@0: * production code. michael@0: * michael@0: * ErrorResult is a type which satisfies the following: michael@0: * - Implements a Throw() method taking an nsresult argument, representing an error code. michael@0: */ michael@0: template michael@0: class AudioEventTimeline michael@0: { michael@0: public: michael@0: explicit AudioEventTimeline(float aDefaultValue) michael@0: : mValue(aDefaultValue), michael@0: mComputedValue(aDefaultValue), michael@0: mLastComputedValue(aDefaultValue) michael@0: { michael@0: } michael@0: michael@0: bool HasSimpleValue() const michael@0: { michael@0: return mEvents.IsEmpty(); michael@0: } michael@0: michael@0: float GetValue() const michael@0: { michael@0: // This method should only be called if HasSimpleValue() returns true michael@0: MOZ_ASSERT(HasSimpleValue()); michael@0: return mValue; michael@0: } michael@0: michael@0: float Value() const michael@0: { michael@0: // TODO: Return the current value based on the timeline of the AudioContext michael@0: return mValue; michael@0: } michael@0: michael@0: void SetValue(float aValue) michael@0: { michael@0: // Silently don't change anything if there are any events michael@0: if (mEvents.IsEmpty()) { michael@0: mLastComputedValue = mComputedValue = mValue = aValue; michael@0: } michael@0: } michael@0: michael@0: void SetValueAtTime(float aValue, double aStartTime, ErrorResult& aRv) michael@0: { michael@0: InsertEvent(AudioTimelineEvent(AudioTimelineEvent::SetValue, aStartTime, aValue), aRv); michael@0: } michael@0: michael@0: void LinearRampToValueAtTime(float aValue, double aEndTime, ErrorResult& aRv) michael@0: { michael@0: InsertEvent(AudioTimelineEvent(AudioTimelineEvent::LinearRamp, aEndTime, aValue), aRv); michael@0: } michael@0: michael@0: void ExponentialRampToValueAtTime(float aValue, double aEndTime, ErrorResult& aRv) michael@0: { michael@0: InsertEvent(AudioTimelineEvent(AudioTimelineEvent::ExponentialRamp, aEndTime, aValue), aRv); michael@0: } michael@0: michael@0: void SetTargetAtTime(float aTarget, double aStartTime, double aTimeConstant, ErrorResult& aRv) michael@0: { michael@0: InsertEvent(AudioTimelineEvent(AudioTimelineEvent::SetTarget, aStartTime, aTarget, aTimeConstant), aRv); michael@0: } michael@0: michael@0: void SetValueCurveAtTime(const float* aValues, uint32_t aValuesLength, double aStartTime, double aDuration, ErrorResult& aRv) michael@0: { michael@0: InsertEvent(AudioTimelineEvent(AudioTimelineEvent::SetValueCurve, aStartTime, 0.0f, 0.0f, aDuration, aValues, aValuesLength), aRv); michael@0: } michael@0: michael@0: void CancelScheduledValues(double aStartTime) michael@0: { michael@0: for (unsigned i = 0; i < mEvents.Length(); ++i) { michael@0: if (mEvents[i].mTime >= aStartTime) { michael@0: #ifdef DEBUG michael@0: // Sanity check: the array should be sorted, so all of the following michael@0: // events should have a time greater than aStartTime too. michael@0: for (unsigned j = i + 1; j < mEvents.Length(); ++j) { michael@0: MOZ_ASSERT(mEvents[j].mTime >= aStartTime); michael@0: } michael@0: #endif michael@0: mEvents.TruncateLength(i); michael@0: break; michael@0: } michael@0: } michael@0: } michael@0: michael@0: void CancelAllEvents() michael@0: { michael@0: mEvents.Clear(); michael@0: } michael@0: michael@0: static bool TimesEqual(int64_t aLhs, int64_t aRhs) michael@0: { michael@0: return aLhs == aRhs; michael@0: } michael@0: michael@0: // Since we are going to accumulate error by adding 0.01 multiple time in a michael@0: // loop, we want to fuzz the equality check in GetValueAtTime. michael@0: static bool TimesEqual(double aLhs, double aRhs) michael@0: { michael@0: const float kEpsilon = 0.0000000001f; michael@0: return fabs(aLhs - aRhs) < kEpsilon; michael@0: } michael@0: michael@0: template michael@0: float GetValueAtTime(TimeType aTime) michael@0: { michael@0: mComputedValue = GetValueAtTimeHelper(aTime); michael@0: return mComputedValue; michael@0: } michael@0: michael@0: // This method computes the AudioParam value at a given time based on the event timeline michael@0: template michael@0: float GetValueAtTimeHelper(TimeType aTime) michael@0: { michael@0: const AudioTimelineEvent* previous = nullptr; michael@0: const AudioTimelineEvent* next = nullptr; michael@0: michael@0: bool bailOut = false; michael@0: for (unsigned i = 0; !bailOut && i < mEvents.Length(); ++i) { michael@0: switch (mEvents[i].mType) { michael@0: case AudioTimelineEvent::SetValue: michael@0: case AudioTimelineEvent::SetTarget: michael@0: case AudioTimelineEvent::LinearRamp: michael@0: case AudioTimelineEvent::ExponentialRamp: michael@0: case AudioTimelineEvent::SetValueCurve: michael@0: if (TimesEqual(aTime, mEvents[i].template Time())) { michael@0: mLastComputedValue = mComputedValue; michael@0: // Find the last event with the same time michael@0: do { michael@0: ++i; michael@0: } while (i < mEvents.Length() && michael@0: aTime == mEvents[i].template Time()); michael@0: michael@0: // SetTarget nodes can be handled no matter what their next node is (if they have one) michael@0: if (mEvents[i - 1].mType == AudioTimelineEvent::SetTarget) { michael@0: // Follow the curve, without regard to the next event, starting at michael@0: // the last value of the last event. michael@0: return ExponentialApproach(mEvents[i - 1].template Time(), michael@0: mLastComputedValue, mEvents[i - 1].mValue, michael@0: mEvents[i - 1].mTimeConstant, aTime); michael@0: } michael@0: michael@0: // SetValueCurve events can be handled no matter what their event node is (if they have one) michael@0: if (mEvents[i - 1].mType == AudioTimelineEvent::SetValueCurve) { michael@0: return ExtractValueFromCurve(mEvents[i - 1].template Time(), michael@0: mEvents[i - 1].mCurve, michael@0: mEvents[i - 1].mCurveLength, michael@0: mEvents[i - 1].mDuration, aTime); michael@0: } michael@0: michael@0: // For other event types michael@0: return mEvents[i - 1].mValue; michael@0: } michael@0: previous = next; michael@0: next = &mEvents[i]; michael@0: if (aTime < mEvents[i].template Time()) { michael@0: bailOut = true; michael@0: } michael@0: break; michael@0: default: michael@0: MOZ_ASSERT(false, "unreached"); michael@0: } michael@0: } michael@0: // Handle the case where the time is past all of the events michael@0: if (!bailOut) { michael@0: previous = next; michael@0: next = nullptr; michael@0: } michael@0: michael@0: // Just return the default value if we did not find anything michael@0: if (!previous && !next) { michael@0: return mValue; michael@0: } michael@0: michael@0: // If the requested time is before all of the existing events michael@0: if (!previous) { michael@0: return mValue; michael@0: } michael@0: michael@0: // SetTarget nodes can be handled no matter what their next node is (if they have one) michael@0: if (previous->mType == AudioTimelineEvent::SetTarget) { michael@0: return ExponentialApproach(previous->template Time(), michael@0: mLastComputedValue, previous->mValue, michael@0: previous->mTimeConstant, aTime); michael@0: } michael@0: michael@0: // SetValueCurve events can be handled no mattar what their next node is (if they have one) michael@0: if (previous->mType == AudioTimelineEvent::SetValueCurve) { michael@0: return ExtractValueFromCurve(previous->template Time(), michael@0: previous->mCurve, previous->mCurveLength, michael@0: previous->mDuration, aTime); michael@0: } michael@0: michael@0: // If the requested time is after all of the existing events michael@0: if (!next) { michael@0: switch (previous->mType) { michael@0: case AudioTimelineEvent::SetValue: michael@0: case AudioTimelineEvent::LinearRamp: michael@0: case AudioTimelineEvent::ExponentialRamp: michael@0: // The value will be constant after the last event michael@0: return previous->mValue; michael@0: case AudioTimelineEvent::SetValueCurve: michael@0: return ExtractValueFromCurve(previous->template Time(), michael@0: previous->mCurve, previous->mCurveLength, michael@0: previous->mDuration, aTime); michael@0: case AudioTimelineEvent::SetTarget: michael@0: MOZ_ASSERT(false, "unreached"); michael@0: } michael@0: MOZ_ASSERT(false, "unreached"); michael@0: } michael@0: michael@0: // Finally, handle the case where we have both a previous and a next event michael@0: michael@0: // First, handle the case where our range ends up in a ramp event michael@0: switch (next->mType) { michael@0: case AudioTimelineEvent::LinearRamp: michael@0: return LinearInterpolate(previous->template Time(), previous->mValue, next->template Time(), next->mValue, aTime); michael@0: case AudioTimelineEvent::ExponentialRamp: michael@0: return ExponentialInterpolate(previous->template Time(), previous->mValue, next->template Time(), next->mValue, aTime); michael@0: case AudioTimelineEvent::SetValue: michael@0: case AudioTimelineEvent::SetTarget: michael@0: case AudioTimelineEvent::SetValueCurve: michael@0: break; michael@0: } michael@0: michael@0: // Now handle all other cases michael@0: switch (previous->mType) { michael@0: case AudioTimelineEvent::SetValue: michael@0: case AudioTimelineEvent::LinearRamp: michael@0: case AudioTimelineEvent::ExponentialRamp: michael@0: // If the next event type is neither linear or exponential ramp, the michael@0: // value is constant. michael@0: return previous->mValue; michael@0: case AudioTimelineEvent::SetValueCurve: michael@0: return ExtractValueFromCurve(previous->template Time(), michael@0: previous->mCurve, previous->mCurveLength, michael@0: previous->mDuration, aTime); michael@0: case AudioTimelineEvent::SetTarget: michael@0: MOZ_ASSERT(false, "unreached"); michael@0: } michael@0: michael@0: MOZ_ASSERT(false, "unreached"); michael@0: return 0.0f; michael@0: } michael@0: michael@0: // Return the number of events scheduled michael@0: uint32_t GetEventCount() const michael@0: { michael@0: return mEvents.Length(); michael@0: } michael@0: michael@0: static float LinearInterpolate(double t0, float v0, double t1, float v1, double t) michael@0: { michael@0: return v0 + (v1 - v0) * ((t - t0) / (t1 - t0)); michael@0: } michael@0: michael@0: static float ExponentialInterpolate(double t0, float v0, double t1, float v1, double t) michael@0: { michael@0: return v0 * powf(v1 / v0, (t - t0) / (t1 - t0)); michael@0: } michael@0: michael@0: static float ExponentialApproach(double t0, double v0, float v1, double timeConstant, double t) michael@0: { michael@0: return v1 + (v0 - v1) * expf(-(t - t0) / timeConstant); michael@0: } michael@0: michael@0: static float ExtractValueFromCurve(double startTime, float* aCurve, uint32_t aCurveLength, double duration, double t) michael@0: { michael@0: if (t >= startTime + duration) { michael@0: // After the duration, return the last curve value michael@0: return aCurve[aCurveLength - 1]; michael@0: } michael@0: double ratio = std::max((t - startTime) / duration, 0.0); michael@0: if (ratio >= 1.0) { michael@0: return aCurve[aCurveLength - 1]; michael@0: } michael@0: return aCurve[uint32_t(aCurveLength * ratio)]; michael@0: } michael@0: michael@0: void ConvertEventTimesToTicks(int64_t (*aConvertor)(double aTime, void* aClosure), void* aClosure, michael@0: int32_t aSampleRate) michael@0: { michael@0: for (unsigned i = 0; i < mEvents.Length(); ++i) { michael@0: mEvents[i].SetTimeInTicks(aConvertor(mEvents[i].template Time(), aClosure)); michael@0: mEvents[i].mTimeConstant *= aSampleRate; michael@0: mEvents[i].mDuration *= aSampleRate; michael@0: } michael@0: } michael@0: michael@0: private: michael@0: const AudioTimelineEvent* GetPreviousEvent(double aTime) const michael@0: { michael@0: const AudioTimelineEvent* previous = nullptr; michael@0: const AudioTimelineEvent* next = nullptr; michael@0: michael@0: bool bailOut = false; michael@0: for (unsigned i = 0; !bailOut && i < mEvents.Length(); ++i) { michael@0: switch (mEvents[i].mType) { michael@0: case AudioTimelineEvent::SetValue: michael@0: case AudioTimelineEvent::SetTarget: michael@0: case AudioTimelineEvent::LinearRamp: michael@0: case AudioTimelineEvent::ExponentialRamp: michael@0: case AudioTimelineEvent::SetValueCurve: michael@0: if (aTime == mEvents[i].mTime) { michael@0: // Find the last event with the same time michael@0: do { michael@0: ++i; michael@0: } while (i < mEvents.Length() && michael@0: aTime == mEvents[i].mTime); michael@0: return &mEvents[i - 1]; michael@0: } michael@0: previous = next; michael@0: next = &mEvents[i]; michael@0: if (aTime < mEvents[i].mTime) { michael@0: bailOut = true; michael@0: } michael@0: break; michael@0: default: michael@0: MOZ_ASSERT(false, "unreached"); michael@0: } michael@0: } michael@0: // Handle the case where the time is past all of the events michael@0: if (!bailOut) { michael@0: previous = next; michael@0: } michael@0: michael@0: return previous; michael@0: } michael@0: michael@0: void InsertEvent(const AudioTimelineEvent& aEvent, ErrorResult& aRv) michael@0: { michael@0: if (!aEvent.IsValid()) { michael@0: aRv.Throw(NS_ERROR_DOM_SYNTAX_ERR); michael@0: return; michael@0: } michael@0: michael@0: // Make sure that non-curve events don't fall within the duration of a michael@0: // curve event. michael@0: for (unsigned i = 0; i < mEvents.Length(); ++i) { michael@0: if (mEvents[i].mType == AudioTimelineEvent::SetValueCurve && michael@0: mEvents[i].mTime <= aEvent.mTime && michael@0: (mEvents[i].mTime + mEvents[i].mDuration) >= aEvent.mTime) { michael@0: aRv.Throw(NS_ERROR_DOM_SYNTAX_ERR); michael@0: return; michael@0: } michael@0: } michael@0: michael@0: // Make sure that curve events don't fall in a range which includes other michael@0: // events. michael@0: if (aEvent.mType == AudioTimelineEvent::SetValueCurve) { michael@0: for (unsigned i = 0; i < mEvents.Length(); ++i) { michael@0: if (mEvents[i].mTime > aEvent.mTime && michael@0: mEvents[i].mTime < (aEvent.mTime + aEvent.mDuration)) { michael@0: aRv.Throw(NS_ERROR_DOM_SYNTAX_ERR); michael@0: return; michael@0: } michael@0: } michael@0: } michael@0: michael@0: // Make sure that invalid values are not used for exponential curves michael@0: if (aEvent.mType == AudioTimelineEvent::ExponentialRamp) { michael@0: if (aEvent.mValue <= 0.f) { michael@0: aRv.Throw(NS_ERROR_DOM_SYNTAX_ERR); michael@0: return; michael@0: } michael@0: const AudioTimelineEvent* previousEvent = GetPreviousEvent(aEvent.mTime); michael@0: if (previousEvent) { michael@0: if (previousEvent->mValue <= 0.f) { michael@0: aRv.Throw(NS_ERROR_DOM_SYNTAX_ERR); michael@0: return; michael@0: } michael@0: } else { michael@0: if (mValue <= 0.f) { michael@0: aRv.Throw(NS_ERROR_DOM_SYNTAX_ERR); michael@0: return; michael@0: } michael@0: } michael@0: } michael@0: michael@0: for (unsigned i = 0; i < mEvents.Length(); ++i) { michael@0: if (aEvent.mTime == mEvents[i].mTime) { michael@0: if (aEvent.mType == mEvents[i].mType) { michael@0: // If times and types are equal, replace the event michael@0: mEvents.ReplaceElementAt(i, aEvent); michael@0: } else { michael@0: // Otherwise, place the element after the last event of another type michael@0: do { michael@0: ++i; michael@0: } while (i < mEvents.Length() && michael@0: aEvent.mType != mEvents[i].mType && michael@0: aEvent.mTime == mEvents[i].mTime); michael@0: mEvents.InsertElementAt(i, aEvent); michael@0: } michael@0: return; michael@0: } michael@0: // Otherwise, place the event right after the latest existing event michael@0: if (aEvent.mTime < mEvents[i].mTime) { michael@0: mEvents.InsertElementAt(i, aEvent); michael@0: return; michael@0: } michael@0: } michael@0: michael@0: // If we couldn't find a place for the event, just append it to the list michael@0: mEvents.AppendElement(aEvent); michael@0: } michael@0: michael@0: private: michael@0: // This is a sorted array of the events in the timeline. Queries of this michael@0: // data structure should probably be more frequent than modifications to it, michael@0: // and that is the reason why we're using a simple array as the data structure. michael@0: // We can optimize this in the future if the performance of the array ends up michael@0: // being a bottleneck. michael@0: nsTArray mEvents; michael@0: float mValue; michael@0: // This is the value of this AudioParam we computed at the last call. michael@0: float mComputedValue; michael@0: // This is the value of this AudioParam at the last tick of the previous event. michael@0: float mLastComputedValue; michael@0: }; michael@0: michael@0: } michael@0: } michael@0: michael@0: #endif michael@0: