summaryrefslogtreecommitdiffstats
path: root/dom/media/webaudio/AudioEventTimeline.h
diff options
context:
space:
mode:
Diffstat (limited to 'dom/media/webaudio/AudioEventTimeline.h')
-rw-r--r--dom/media/webaudio/AudioEventTimeline.h387
1 files changed, 387 insertions, 0 deletions
diff --git a/dom/media/webaudio/AudioEventTimeline.h b/dom/media/webaudio/AudioEventTimeline.h
new file mode 100644
index 0000000000..7a136df245
--- /dev/null
+++ b/dom/media/webaudio/AudioEventTimeline.h
@@ -0,0 +1,387 @@
+/* -*- 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 final {
+ enum Type : uint32_t {
+ SetValue,
+ SetValueAtTime,
+ LinearRamp,
+ ExponentialRamp,
+ SetTarget,
+ SetValueCurve,
+ Track,
+ Cancel
+ };
+
+ AudioTimelineEvent(Type aType, double aTime, float aValue,
+ double aTimeConstant = 0.0, double aDuration = 0.0,
+ const float* aCurve = nullptr, uint32_t aCurveLength = 0);
+ explicit AudioTimelineEvent(AudioNodeTrack* aTrack);
+ AudioTimelineEvent(const AudioTimelineEvent& rhs);
+ ~AudioTimelineEvent();
+
+ template <class TimeType>
+ TimeType Time() const;
+
+ void SetTimeInTicks(int64_t aTimeInTicks) {
+ mTimeInTicks = aTimeInTicks;
+#ifdef DEBUG
+ mTimeIsInTicks = true;
+#endif
+ }
+
+ void SetCurveParams(const float* aCurve, uint32_t aCurveLength) {
+ mCurveLength = aCurveLength;
+ if (aCurveLength) {
+ mCurve = new float[aCurveLength];
+ PodCopy(mCurve, aCurve, aCurveLength);
+ } else {
+ mCurve = nullptr;
+ }
+ }
+
+ Type mType;
+ union {
+ float mValue;
+ uint32_t mCurveLength;
+ };
+ // 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;
+ RefPtr<AudioNodeTrack> mTrack;
+ double mTimeConstant;
+ double mDuration;
+#ifdef DEBUG
+ bool mTimeIsInTicks;
+#endif
+
+ private:
+ // This member is accessed using the `Time` method, for safety.
+ //
+ // The time for an event can either be in absolute value or in ticks.
+ // Initially the time of the event is always in absolute value.
+ // 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 absolute value.
+ union {
+ double mTime;
+ int64_t mTimeInTicks;
+ };
+};
+
+template <>
+inline double AudioTimelineEvent::Time<double>() const {
+ MOZ_ASSERT(!mTimeIsInTicks);
+ return mTime;
+}
+
+template <>
+inline int64_t AudioTimelineEvent::Time<int64_t>() const {
+ MOZ_ASSERT(!NS_IsMainThread());
+ MOZ_ASSERT(mTimeIsInTicks);
+ return mTimeInTicks;
+}
+
+class AudioEventTimeline {
+ public:
+ explicit AudioEventTimeline(float aDefaultValue)
+ : mValue(aDefaultValue),
+ mComputedValue(aDefaultValue),
+ mLastComputedValue(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;
+ }
+ if (!WebAudioUtils::IsTimeValid(aEvent.mTimeConstant)) {
+ aRv.ThrowRangeError(
+ "The exponential constant passed to setTargetAtTime must be "
+ "non-negative.");
+ return false;
+ }
+
+ if (aEvent.mType == AudioTimelineEvent::SetValueCurve) {
+ if (!aEvent.mCurve || aEvent.mCurveLength < 2) {
+ aRv.ThrowInvalidStateError("Curve length must be at least 2");
+ return false;
+ }
+ if (aEvent.mDuration <= 0) {
+ aRv.ThrowRangeError(
+ "The curve duration for setValueCurveAtTime must be strictly "
+ "positive.");
+ return false;
+ }
+ }
+
+ MOZ_ASSERT(IsValid(aEvent.mValue) && IsValid(aEvent.mDuration));
+
+ // 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].mDuration > 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.mDuration > 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.mValue <= 0.f) {
+ aRv.ThrowRangeError(
+ "The value passed to exponentialRampToValueAtTime must be "
+ "positive.");
+ return false;
+ }
+ const AudioTimelineEvent* previousEvent =
+ GetPreviousEvent(TimeOf(aEvent));
+ if (previousEvent) {
+ if (previousEvent->mValue <= 0.f) {
+ // XXXbz I see no mention of SyntaxError in the Web Audio API spec
+ aRv.ThrowSyntaxError("Previous event value must be positive");
+ return false;
+ }
+ } else {
+ if (mValue <= 0.f) {
+ // XXXbz I see no mention of SyntaxError in the Web Audio API spec
+ aRv.ThrowSyntaxError("Our value must be positive");
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ template <typename TimeType>
+ void InsertEvent(const AudioTimelineEvent& aEvent) {
+ 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 mEvents.IsEmpty(); }
+
+ float GetValue() const {
+ // This method should only be called if HasSimpleValue() returns true
+ MOZ_ASSERT(HasSimpleValue());
+ return mValue;
+ }
+
+ void SetValue(float aValue) {
+ // Silently don't change anything if there are any events
+ if (mEvents.IsEmpty()) {
+ mLastComputedValue = mComputedValue = mValue = aValue;
+ }
+ }
+
+ void SetValueAtTime(float aValue, double aStartTime, ErrorResult& aRv) {
+ AudioTimelineEvent event(AudioTimelineEvent::SetValueAtTime, aStartTime,
+ aValue);
+
+ if (ValidateEvent(event, aRv)) {
+ InsertEvent<double>(event);
+ }
+ }
+
+ void LinearRampToValueAtTime(float aValue, double aEndTime,
+ ErrorResult& aRv) {
+ AudioTimelineEvent event(AudioTimelineEvent::LinearRamp, aEndTime, aValue);
+
+ if (ValidateEvent(event, aRv)) {
+ InsertEvent<double>(event);
+ }
+ }
+
+ void ExponentialRampToValueAtTime(float aValue, double aEndTime,
+ ErrorResult& aRv) {
+ AudioTimelineEvent event(AudioTimelineEvent::ExponentialRamp, aEndTime,
+ aValue);
+
+ if (ValidateEvent(event, aRv)) {
+ InsertEvent<double>(event);
+ }
+ }
+
+ void SetTargetAtTime(float aTarget, double aStartTime, double aTimeConstant,
+ ErrorResult& aRv) {
+ AudioTimelineEvent event(AudioTimelineEvent::SetTarget, aStartTime, aTarget,
+ aTimeConstant);
+
+ if (ValidateEvent(event, aRv)) {
+ InsertEvent<double>(event);
+ }
+ }
+
+ void SetValueCurveAtTime(const float* aValues, uint32_t aValuesLength,
+ double aStartTime, double aDuration,
+ ErrorResult& aRv) {
+ AudioTimelineEvent event(AudioTimelineEvent::SetValueCurve, aStartTime,
+ 0.0f, 0.0f, aDuration, aValues, aValuesLength);
+ if (ValidateEvent(event, aRv)) {
+ InsertEvent<double>(event);
+ }
+ }
+
+ 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;
+ }
+ }
+ }
+
+ void CancelAllEvents() { mEvents.Clear(); }
+
+ 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;
+ }
+
+ template <class TimeType>
+ void GetValuesAtTime(TimeType aTime, float* aBuffer, const size_t aSize) {
+ MOZ_ASSERT(aBuffer);
+ GetValuesAtTimeHelper(aTime, aBuffer, aSize);
+ }
+
+ // Return the number of events scheduled
+ uint32_t GetEventCount() const { return mEvents.Length(); }
+
+ template <class TimeType>
+ void CleanupEventsOlderThan(TimeType aTime) {
+ while (mEvents.Length() > 1 && aTime > mEvents[1].Time<TimeType>()) {
+ if (mEvents[1].mType == AudioTimelineEvent::SetTarget) {
+ mLastComputedValue = GetValuesAtTimeHelperInternal(
+ mEvents[1].Time<TimeType>(), &mEvents[0], nullptr);
+ }
+
+ MOZ_ASSERT(!mEvents[0].mTrack,
+ "AudioParam tracks should never be destroyed on the real-time "
+ "thread.");
+ JS::AutoSuppressGCAnalysis suppress;
+ mEvents.RemoveElementAt(0);
+ }
+ }
+
+ private:
+ template <class TimeType>
+ void GetValuesAtTimeHelper(TimeType aTime, float* aBuffer,
+ const size_t aSize);
+
+ template <class TimeType>
+ float GetValueAtTimeOfEvent(const AudioTimelineEvent* aNext);
+
+ template <class TimeType>
+ float GetValuesAtTimeHelperInternal(TimeType aTime,
+ const AudioTimelineEvent* aPrevious,
+ const AudioTimelineEvent* aNext);
+
+ const AudioTimelineEvent* GetPreviousEvent(double aTime) const;
+
+ static bool IsValid(double value) { return std::isfinite(value); }
+
+ // 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 mValue;
+ // This is the value of this AudioParam we computed at the last tick.
+ float mComputedValue;
+ // This is the value of this AudioParam at the last tick of the previous
+ // event.
+ float mLastComputedValue;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif