From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- dom/media/webaudio/AudioEventTimeline.h | 387 ++++++++++++++++++++++++++++++++ 1 file changed, 387 insertions(+) create mode 100644 dom/media/webaudio/AudioEventTimeline.h (limited to 'dom/media/webaudio/AudioEventTimeline.h') 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 +#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 + 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 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() const { + MOZ_ASSERT(!mTimeIsInTicks); + return mTime; +} + +template <> +inline int64_t AudioTimelineEvent::Time() 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(); + }; + + // Validate the event itself + if (!WebAudioUtils::IsTimeValid(TimeOf(aEvent))) { + aRv.ThrowRangeError(); + 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 + void InsertEvent(const AudioTimelineEvent& aEvent) { + for (unsigned i = 0; i < mEvents.Length(); ++i) { + if (aEvent.Time() == mEvents[i].Time()) { + // If two events happen at the same time, have them in chronological + // order of insertion. + do { + ++i; + } while (i < mEvents.Length() && + aEvent.Time() == mEvents[i].Time()); + mEvents.InsertElementAt(i, aEvent); + return; + } + // Otherwise, place the event right after the latest existing event + if (aEvent.Time() < mEvents[i].Time()) { + 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(event); + } + } + + void LinearRampToValueAtTime(float aValue, double aEndTime, + ErrorResult& aRv) { + AudioTimelineEvent event(AudioTimelineEvent::LinearRamp, aEndTime, aValue); + + if (ValidateEvent(event, aRv)) { + InsertEvent(event); + } + } + + void ExponentialRampToValueAtTime(float aValue, double aEndTime, + ErrorResult& aRv) { + AudioTimelineEvent event(AudioTimelineEvent::ExponentialRamp, aEndTime, + aValue); + + if (ValidateEvent(event, aRv)) { + InsertEvent(event); + } + } + + void SetTargetAtTime(float aTarget, double aStartTime, double aTimeConstant, + ErrorResult& aRv) { + AudioTimelineEvent event(AudioTimelineEvent::SetTarget, aStartTime, aTarget, + aTimeConstant); + + if (ValidateEvent(event, aRv)) { + InsertEvent(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(event); + } + } + + template + void CancelScheduledValues(TimeType aStartTime) { + for (unsigned i = 0; i < mEvents.Length(); ++i) { + if (mEvents[i].Time() >= 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() >= 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 + float GetValueAtTime(TimeType aTime) { + float result; + GetValuesAtTimeHelper(aTime, &result, 1); + return result; + } + + template + 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 + void CleanupEventsOlderThan(TimeType aTime) { + while (mEvents.Length() > 1 && aTime > mEvents[1].Time()) { + if (mEvents[1].mType == AudioTimelineEvent::SetTarget) { + mLastComputedValue = GetValuesAtTimeHelperInternal( + mEvents[1].Time(), &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 + void GetValuesAtTimeHelper(TimeType aTime, float* aBuffer, + const size_t aSize); + + template + float GetValueAtTimeOfEvent(const AudioTimelineEvent* aNext); + + template + 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 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 -- cgit v1.2.3