1001 lines
44 KiB
C++
1001 lines
44 KiB
C++
/*
|
|
* Copyright (C) 2011 Google Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
*
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
|
|
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
* DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
|
|
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
|
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
|
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
#include "config.h"
|
|
|
|
#if ENABLE(WEB_AUDIO)
|
|
|
|
#include "AudioParamTimeline.h"
|
|
|
|
#include "AudioUtilities.h"
|
|
#include "FloatConversion.h"
|
|
#include "VectorMath.h"
|
|
#include <algorithm>
|
|
#include <wtf/MathExtras.h>
|
|
|
|
namespace WebCore {
|
|
|
|
static void fillWithValue(float* values, float value, unsigned endFrame, unsigned& writeIndex)
|
|
{
|
|
if (writeIndex < endFrame) {
|
|
std::fill_n(values + writeIndex, endFrame - writeIndex, value);
|
|
writeIndex = endFrame;
|
|
}
|
|
}
|
|
|
|
// Test that for a SetTarget event, the current value is close enough to the target value that
|
|
// we can consider the event to have converged to the target.
|
|
static bool hasSetTargetConverged(float value, float target, Seconds currentTime, Seconds startTime, double timeConstant)
|
|
{
|
|
// For a SetTarget event, we want the event to terminate eventually so that we can stop using
|
|
// the timeline to compute the values.
|
|
constexpr float timeConstantsToConverge = 10;
|
|
constexpr float setTargetThreshold = 4.539992976248485e-05;
|
|
|
|
// Converged if enough time constants (|timeConstantsToConverge|) have passed since the start
|
|
// of the event.
|
|
if (currentTime.value() > startTime.value() + timeConstantsToConverge * timeConstant)
|
|
return true;
|
|
|
|
// If |target| is 0, converged if |value| is less than |setTargetThreshold|.
|
|
if (!target && std::abs(value) < setTargetThreshold)
|
|
return true;
|
|
|
|
// If |target| is not zero, converged if relative difference between |value|
|
|
// and |target| is small. That is |target - value| / |target| < |setTargetThreshold|.
|
|
if (target && std::abs(target - value) < setTargetThreshold * std::abs(value))
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
ExceptionOr<void> AudioParamTimeline::setValueAtTime(float value, Seconds time)
|
|
{
|
|
Locker locker { m_eventsLock };
|
|
return insertEvent(ParamEvent::createSetValueEvent(value, time));
|
|
}
|
|
|
|
ExceptionOr<void> AudioParamTimeline::linearRampToValueAtTime(float targetValue, Seconds endTime, float currentValue, Seconds currentTime)
|
|
{
|
|
Locker locker { m_eventsLock };
|
|
|
|
// Linear ramp events need a preceding event so that they have an initial value.
|
|
if (m_events.isEmpty())
|
|
insertEvent(ParamEvent::createSetValueEvent(currentValue, currentTime));
|
|
|
|
return insertEvent(ParamEvent::createLinearRampEvent(targetValue, endTime));
|
|
}
|
|
|
|
ExceptionOr<void> AudioParamTimeline::exponentialRampToValueAtTime(float targetValue, Seconds endTime, float currentValue, Seconds currentTime)
|
|
{
|
|
Locker locker { m_eventsLock };
|
|
|
|
// Exponential ramp events need a preceding event so that they have an initial value.
|
|
if (m_events.isEmpty())
|
|
insertEvent(ParamEvent::createSetValueEvent(currentValue, currentTime));
|
|
|
|
return insertEvent(ParamEvent::createExponentialRampEvent(targetValue, endTime));
|
|
}
|
|
|
|
ExceptionOr<void> AudioParamTimeline::setTargetAtTime(float target, Seconds time, float timeConstant)
|
|
{
|
|
Locker locker { m_eventsLock };
|
|
// If timeConstant is 0, we instantly jump to the target value, so insert a SetValueEvent instead of SetTargetEvent.
|
|
if (!timeConstant)
|
|
return insertEvent(ParamEvent::createSetValueEvent(target, time));
|
|
return insertEvent(ParamEvent::createSetTargetEvent(target, time, timeConstant));
|
|
}
|
|
|
|
ExceptionOr<void> AudioParamTimeline::setValueCurveAtTime(Vector<float>&& curve, Seconds time, Seconds duration)
|
|
{
|
|
Locker locker { m_eventsLock };
|
|
|
|
float curveEndValue = curve.last();
|
|
auto result = insertEvent(ParamEvent::createSetValueCurveEvent(WTFMove(curve), time, duration));
|
|
if (result.hasException())
|
|
return result.releaseException();
|
|
|
|
// The specification says an implicit call to setValueAtTime() is made at time T0+TD with value V[N-1]
|
|
// so that following automations will start from the end of the setValueCurveAtTime() event.
|
|
return insertEvent(ParamEvent::createSetValueEvent(curveEndValue, time + duration));
|
|
}
|
|
|
|
static bool isValidNumber(float x)
|
|
{
|
|
return !std::isnan(x) && !std::isinf(x);
|
|
}
|
|
|
|
static bool isValidNumber(Seconds s)
|
|
{
|
|
return !std::isnan(s.value()) && !std::isinf(s.value());
|
|
}
|
|
|
|
ExceptionOr<void> AudioParamTimeline::insertEvent(ParamEvent&& event)
|
|
{
|
|
// Sanity check the event. Be super careful we're not getting infected with NaN or Inf.
|
|
bool isValid = event.type() < ParamEvent::LastType
|
|
&& isValidNumber(event.value())
|
|
&& isValidNumber(event.time())
|
|
&& isValidNumber(event.timeConstant())
|
|
&& isValidNumber(event.duration())
|
|
&& event.duration() >= 0_s;
|
|
|
|
if (!isValid)
|
|
return { };
|
|
|
|
ASSERT(m_eventsLock.isLocked());
|
|
|
|
unsigned i = 0;
|
|
auto insertTime = event.time();
|
|
|
|
for (auto& paramEvent : m_events) {
|
|
if (event.type() == ParamEvent::SetValueCurve) {
|
|
if (paramEvent.type() != ParamEvent::CancelValues) {
|
|
// If this event is a SetValueCurve, make sure it doesn't overlap any existing event.
|
|
// It's ok if the SetValueCurve starts at the same time as the end of some other duration.
|
|
auto endTime = event.time() + event.duration();
|
|
if (paramEvent.type() == ParamEvent::SetValueCurve) {
|
|
auto paramEventEndTime = paramEvent.time() + paramEvent.duration();
|
|
if ((paramEvent.time() >= event.time() && paramEvent.time() < endTime)
|
|
|| (paramEventEndTime > event.time() && paramEventEndTime < endTime)
|
|
|| (event.time() >= paramEvent.time() && event.time() < paramEventEndTime)
|
|
|| (endTime >= paramEvent.time() && endTime < paramEventEndTime)) {
|
|
return Exception { NotSupportedError, "Events are overlapping"_s };
|
|
}
|
|
} else if (paramEvent.time() > event.time() && paramEvent.time() < endTime)
|
|
return Exception { NotSupportedError, "Events are overlapping"_s };
|
|
}
|
|
} else if (paramEvent.type() == ParamEvent::SetValueCurve) {
|
|
// Otherwise, make sure this event doesn't overlap any existing SetValueCurve event.
|
|
auto parentEventEndTime = paramEvent.time() + paramEvent.duration();
|
|
if (event.time() >= paramEvent.time() && event.time() < parentEventEndTime)
|
|
return Exception { NotSupportedError, "Events are overlapping" };
|
|
}
|
|
|
|
if (paramEvent.time() > insertTime)
|
|
break;
|
|
|
|
++i;
|
|
}
|
|
|
|
m_events.insert(i, WTFMove(event));
|
|
return { };
|
|
}
|
|
|
|
void AudioParamTimeline::cancelScheduledValues(Seconds cancelTime)
|
|
{
|
|
Locker locker { m_eventsLock };
|
|
|
|
// Remove all events whose start time is greater than or equal to the cancel time.
|
|
// Also handle the special case where the cancel time lies in the middle of a
|
|
// SetValueCurve event.
|
|
//
|
|
// This critically depends on the fact that no event can be scheduled in the middle
|
|
// of the curve or at the same start time. Then removing the SetValueCurve doesn't
|
|
// remove any events that shouldn't have been.
|
|
auto isAfter = [](const ParamEvent& event, Seconds cancelTime) {
|
|
auto eventTime = event.time();
|
|
if (eventTime >= cancelTime)
|
|
return true;
|
|
return event.type() == ParamEvent::SetValueCurve
|
|
&& eventTime <= cancelTime
|
|
&& (eventTime + event.duration() > cancelTime);
|
|
};
|
|
|
|
// Remove all events starting at cancelTime.
|
|
for (unsigned i = 0; i < m_events.size(); ++i) {
|
|
if (isAfter(m_events[i], cancelTime)) {
|
|
m_events.remove(i, m_events.size() - i);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
ExceptionOr<void> AudioParamTimeline::cancelAndHoldAtTime(Seconds cancelTime)
|
|
{
|
|
Locker locker { m_eventsLock };
|
|
|
|
// Find the first event at or just past cancelTime.
|
|
size_t i = m_events.findMatching([&](auto& event) {
|
|
return event.time() > cancelTime;
|
|
});
|
|
i = (i == notFound) ? m_events.size() : i;
|
|
|
|
// The event that is being cancelled. This is the event just past cancelTime, if any.
|
|
size_t cancelledEventIndex = i;
|
|
|
|
// If the event just before cancelTime is a SetTarget or SetValueCurve event, we need
|
|
// to handle that event specially instead of the event after.
|
|
if (i > 0 && ((m_events[i - 1].type() == ParamEvent::SetTarget) || (m_events[i - 1].type() == ParamEvent::SetValueCurve)))
|
|
cancelledEventIndex = i - 1;
|
|
else if (i >= m_events.size()) {
|
|
// If there were no events occurring after |cancelTime| (and the
|
|
// previous event is not SetTarget or SetValueCurve, we're done.
|
|
return { };
|
|
}
|
|
|
|
// cancelledEvent is the event that is being cancelled.
|
|
auto& cancelledEvent = m_events[cancelledEventIndex];
|
|
auto eventType = cancelledEvent.type();
|
|
|
|
// New event to be inserted, if any, and a SetValueEvent if needed.
|
|
std::optional<ParamEvent> newEvent;
|
|
std::optional<ParamEvent> newSetValueEvent;
|
|
|
|
switch (eventType) {
|
|
case ParamEvent::LinearRampToValue:
|
|
case ParamEvent::ExponentialRampToValue: {
|
|
// For these events we need to remember the parameters of the event
|
|
// for a CancelValues event so that we can properly cancel the event
|
|
// and hold the value.
|
|
auto savedEvent = ParamEvent::SavedEvent { eventType, cancelledEvent.value(), cancelledEvent.time() };
|
|
newEvent = ParamEvent::createCancelValuesEvent(cancelTime, WTFMove(savedEvent));
|
|
break;
|
|
}
|
|
case ParamEvent::SetTarget: {
|
|
if (cancelledEvent.time() < cancelTime) {
|
|
// Don't want to remove the SetTarget event if it started before the
|
|
// cancel time, so bump the index. But we do want to insert a
|
|
// cancelEvent so that we stop this automation and hold the value when
|
|
// we get there.
|
|
++cancelledEventIndex;
|
|
|
|
newEvent = ParamEvent::createCancelValuesEvent(cancelTime, std::nullopt);
|
|
}
|
|
break;
|
|
}
|
|
case ParamEvent::SetValueCurve: {
|
|
// If the setValueCurve event started strictly before the cancel time,
|
|
// there might be something to do....
|
|
if (cancelledEvent.time() < cancelTime) {
|
|
if (cancelTime > cancelledEvent.time() + cancelledEvent.duration()) {
|
|
// If the cancellation time is past the end of the curve there's
|
|
// nothing to do except remove the following events.
|
|
++cancelledEventIndex;
|
|
} else {
|
|
// Cancellation time is in the middle of the curve. Therefore,
|
|
// create a new SetValueCurve event with the appropriate new
|
|
// parameters to cancel this event properly. Since it's illegal
|
|
// to insert any event within a SetValueCurve event, we can
|
|
// compute the new end value now instead of doing when running
|
|
// the timeline.
|
|
auto newDuration = cancelTime - cancelledEvent.time();
|
|
float endValue = valueCurveAtTime(cancelTime, cancelledEvent.time(), cancelledEvent.duration(), cancelledEvent.curve().data(), cancelledEvent.curve().size());
|
|
|
|
// Replace the existing SetValueCurve with this new one that is identical except for the duration.
|
|
newEvent = ParamEvent { eventType, cancelledEvent.value(), cancelledEvent.time(), cancelledEvent.timeConstant(), newDuration, Vector<float> { cancelledEvent.curve() }, cancelledEvent.curvePointsPerSecond(), endValue, std::nullopt };
|
|
newSetValueEvent = ParamEvent::createSetValueEvent(endValue, cancelledEvent.time() + newDuration);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case ParamEvent::SetValue:
|
|
case ParamEvent::CancelValues:
|
|
// Nothing needs to be done for a SetValue or CancelValues event.
|
|
break;
|
|
case ParamEvent::LastType:
|
|
ASSERT_NOT_REACHED();
|
|
break;
|
|
}
|
|
|
|
// Now remove all the following events from the timeline.
|
|
if (cancelledEventIndex < m_events.size())
|
|
removeCancelledEvents(cancelledEventIndex);
|
|
|
|
// Insert the new event, if any.
|
|
if (newEvent) {
|
|
auto result = insertEvent(WTFMove(*newEvent));
|
|
if (result.hasException())
|
|
return result.releaseException();
|
|
if (newSetValueEvent) {
|
|
insertEvent(WTFMove(*newSetValueEvent));
|
|
if (result.hasException())
|
|
return result.releaseException();
|
|
}
|
|
}
|
|
|
|
return { };
|
|
}
|
|
|
|
void AudioParamTimeline::removeCancelledEvents(size_t firstEventToRemove)
|
|
{
|
|
m_events.remove(firstEventToRemove, m_events.size() - firstEventToRemove);
|
|
}
|
|
|
|
std::optional<float> AudioParamTimeline::valueForContextTime(BaseAudioContext& context, float defaultValue, float minValue, float maxValue)
|
|
{
|
|
{
|
|
if (!m_eventsLock.tryLock())
|
|
return std::nullopt;
|
|
Locker locker { AdoptLock, m_eventsLock };
|
|
if (!m_events.size() || Seconds { context.currentTime() } < m_events[0].time())
|
|
return std::nullopt;
|
|
}
|
|
|
|
// Ask for just a single value.
|
|
float value;
|
|
double sampleRate = context.sampleRate();
|
|
size_t startFrame = context.currentSampleFrame();
|
|
size_t endFrame = startFrame + 1;
|
|
double controlRate = sampleRate / AudioUtilities::renderQuantumSize; // one parameter change per render quantum
|
|
value = valuesForFrameRange(startFrame, endFrame, defaultValue, minValue, maxValue, &value, 1, sampleRate, controlRate);
|
|
return value;
|
|
}
|
|
|
|
float AudioParamTimeline::valuesForFrameRange(size_t startFrame, size_t endFrame, float defaultValue, float minValue, float maxValue, float* values, unsigned numberOfValues, double sampleRate, double controlRate)
|
|
{
|
|
// We can't contend the lock in the realtime audio thread.
|
|
if (!m_eventsLock.tryLock()) {
|
|
std::fill_n(values, numberOfValues, defaultValue);
|
|
return defaultValue;
|
|
}
|
|
Locker locker { AdoptLock, m_eventsLock };
|
|
|
|
float value = valuesForFrameRangeImpl(startFrame, endFrame, defaultValue, values, numberOfValues, sampleRate, controlRate);
|
|
|
|
// Clamp values based on range allowed by AudioParam's min and max values.
|
|
VectorMath::clamp(values, minValue, maxValue, values, numberOfValues);
|
|
|
|
return value;
|
|
}
|
|
|
|
float AudioParamTimeline::valuesForFrameRangeImpl(size_t startFrame, size_t endFrame, float defaultValue, float* values, unsigned numberOfValues, double sampleRate, double controlRate)
|
|
{
|
|
ASSERT(values);
|
|
if (!values)
|
|
return defaultValue;
|
|
|
|
double samplingPeriod = 1. / sampleRate;
|
|
|
|
// Return default value if there are no events matching the desired time range.
|
|
if (!m_events.size() || endFrame * samplingPeriod <= m_events[0].time().value()) {
|
|
std::fill_n(values, numberOfValues, defaultValue);
|
|
return defaultValue;
|
|
}
|
|
|
|
// Maintain a running time and index for writing the values buffer.
|
|
size_t currentFrame = startFrame;
|
|
unsigned writeIndex = 0;
|
|
|
|
// If first event is after startTime then fill initial part of values buffer with defaultValue
|
|
// until we reach the first event time.
|
|
auto firstEventTime = m_events[0].time();
|
|
if (firstEventTime.value() > startFrame * samplingPeriod) {
|
|
size_t fillToEndFrame = endFrame;
|
|
double firstEventFrame = ceil(firstEventTime.value() * sampleRate);
|
|
if (endFrame > firstEventFrame)
|
|
fillToEndFrame = firstEventFrame;
|
|
ASSERT(fillToEndFrame >= startFrame);
|
|
|
|
unsigned fillToFrame = static_cast<unsigned>(fillToEndFrame - startFrame);
|
|
fillToFrame = std::min(fillToFrame, numberOfValues);
|
|
fillWithValue(values, defaultValue, fillToFrame, writeIndex);
|
|
|
|
currentFrame += fillToFrame;
|
|
}
|
|
|
|
float value = defaultValue;
|
|
|
|
// Go through each event and render the value buffer where the times overlap,
|
|
// stopping when we've rendered all the requested values.
|
|
// FIXME: could try to optimize by avoiding having to iterate starting from the very first event
|
|
// and keeping track of a "current" event index.
|
|
int n = m_events.size();
|
|
for (int i = 0; i < n && writeIndex < numberOfValues; ++i) {
|
|
auto* event = &m_events[i];
|
|
auto* nextEvent = i < n - 1 ? &m_events[i + 1] : nullptr;
|
|
|
|
// Wait until we get a more recent event.
|
|
if (!isEventCurrent(*event, nextEvent, currentFrame, sampleRate))
|
|
continue;
|
|
|
|
auto nextEventType = nextEvent ? static_cast<ParamEvent::Type>(nextEvent->type()) : ParamEvent::LastType /* unknown */;
|
|
|
|
processSetTargetFollowedByRamp(i, event, nextEventType, currentFrame, sampleRate, controlRate, value);
|
|
|
|
float value1 = event->value();
|
|
auto time1 = event->time();
|
|
float value2 = nextEvent ? nextEvent->value() : value1;
|
|
auto time2 = nextEvent ? nextEvent->time() : Seconds { endFrame * samplingPeriod + 1 };
|
|
|
|
ASSERT(time2 >= time1);
|
|
|
|
handleCancelValues(*event, nextEvent, value2, time2, nextEventType);
|
|
|
|
size_t fillToEndFrame = endFrame;
|
|
if (endFrame > time2.value() * sampleRate)
|
|
fillToEndFrame = static_cast<size_t>(ceil(time2.value() * sampleRate));
|
|
|
|
ASSERT(fillToEndFrame >= startFrame);
|
|
unsigned fillToFrame = static_cast<unsigned>(fillToEndFrame - startFrame);
|
|
fillToFrame = std::min(fillToFrame, numberOfValues);
|
|
|
|
const AutomationState currentState = {
|
|
numberOfValues,
|
|
startFrame,
|
|
endFrame,
|
|
sampleRate,
|
|
controlRate,
|
|
samplingPeriod,
|
|
fillToFrame,
|
|
fillToEndFrame,
|
|
value1,
|
|
time1,
|
|
value2,
|
|
time2,
|
|
event,
|
|
i,
|
|
};
|
|
|
|
// First handle linear and exponential ramps which require looking ahead to the next event.
|
|
if (nextEventType == ParamEvent::LinearRampToValue)
|
|
processLinearRamp(currentState, values, currentFrame, value, writeIndex);
|
|
else if (nextEventType == ParamEvent::ExponentialRampToValue)
|
|
processExponentialRamp(currentState, values, currentFrame, value, writeIndex);
|
|
else {
|
|
// Handle event types not requiring looking ahead to the next event.
|
|
switch (event->type()) {
|
|
case ParamEvent::SetValue:
|
|
case ParamEvent::LinearRampToValue:
|
|
case ParamEvent::ExponentialRampToValue:
|
|
currentFrame = fillToEndFrame;
|
|
|
|
// Simply stay at a constant value.
|
|
value = event->value();
|
|
fillWithValue(values, value, fillToFrame, writeIndex);
|
|
break;
|
|
case ParamEvent::CancelValues:
|
|
processCancelValues(currentState, values, currentFrame, value, writeIndex);
|
|
break;
|
|
case ParamEvent::SetTarget:
|
|
processSetTarget(currentState, values, currentFrame, value, writeIndex);
|
|
break;
|
|
case ParamEvent::SetValueCurve:
|
|
processSetValueCurve(currentState, values, currentFrame, value, writeIndex);
|
|
break;
|
|
case ParamEvent::LastType:
|
|
ASSERT_NOT_REACHED();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// If there's any time left after processing the last event then just propagate the last value
|
|
// to the end of the values buffer.
|
|
fillWithValue(values, value, numberOfValues, writeIndex);
|
|
|
|
return value;
|
|
}
|
|
|
|
void AudioParamTimeline::processLinearRamp(const AutomationState& currentState, float* values, size_t& currentFrame, float& value, unsigned& writeIndex)
|
|
{
|
|
auto deltaTime = currentState.time2 - currentState.time1;
|
|
float valueDelta = currentState.value2 - currentState.value1;
|
|
|
|
// Since deltaTime is a double, 1/deltaTime can easily overflow a float. Thus, if deltaTime
|
|
// is close enough to zero (less than float min), treat it as zero.
|
|
float k = deltaTime.value() <= std::numeric_limits<float>::min() ? 0 : 1 / deltaTime.value();
|
|
|
|
unsigned fillToFrameTrunc = writeIndex + ((currentState.fillToFrame - writeIndex) / 4) * 4;
|
|
if (fillToFrameTrunc > writeIndex) {
|
|
// Minimize in-loop operations. Calculate starting value and increment.
|
|
// Next step: value += inc.
|
|
// value = value1 + (currentFrame/sampleRate - time1) * k * (value2 - value1);
|
|
// inc = 4 / sampleRate * k * (value2 - value1);
|
|
// Resolve recursion by expanding constants to achieve a 4-step loop unrolling.
|
|
// value = value1 + ((currentFrame/sampleRate - time1) + i * sampleFrameTimeIncr) * k * (value2 - value1), i in 0..3
|
|
values[writeIndex] = 0;
|
|
values[writeIndex + 1] = 1;
|
|
values[writeIndex + 2] = 2;
|
|
values[writeIndex + 3] = 3;
|
|
VectorMath::multiplyByScalar(values + writeIndex, currentState.samplingPeriod, values + writeIndex, 4);
|
|
VectorMath::addScalar(values + writeIndex, currentFrame * currentState.samplingPeriod - currentState.time1.value(), values + writeIndex, 4);
|
|
VectorMath::multiplyByScalar(values + writeIndex, k * valueDelta, values + writeIndex, 4);
|
|
VectorMath::addScalar(values + writeIndex, currentState.value1, values + writeIndex, 4);
|
|
|
|
float inc = 4 * currentState.samplingPeriod * k * valueDelta;
|
|
|
|
// Truncate loop steps to multiple of 4.
|
|
unsigned fillToFrameTrunc = writeIndex + ((currentState.fillToFrame - writeIndex) / 4) * 4;
|
|
// Compute final frame.
|
|
currentFrame += fillToFrameTrunc - writeIndex;
|
|
|
|
// Process 4 loop steps.
|
|
writeIndex += 4;
|
|
for (; writeIndex < fillToFrameTrunc; writeIndex += 4)
|
|
VectorMath::addScalar(values + writeIndex - 4, inc, values + writeIndex, 4);
|
|
}
|
|
// Update |value| with the last value computed so that the .value attribute of the AudioParam gets
|
|
// the correct linear ramp value, in case the following loop doesn't execute.
|
|
if (writeIndex >= 1)
|
|
value = values[writeIndex - 1];
|
|
|
|
// Serially process remaining values.
|
|
for (; writeIndex < currentState.fillToFrame; ++writeIndex) {
|
|
float x = (currentFrame * currentState.samplingPeriod - currentState.time1.value()) * k;
|
|
value = currentState.value1 + valueDelta * x;
|
|
values[writeIndex] = value;
|
|
++currentFrame;
|
|
}
|
|
}
|
|
|
|
void AudioParamTimeline::processExponentialRamp(const AutomationState& currentState, float* values, size_t& currentFrame, float& value, unsigned& writeIndex)
|
|
{
|
|
if (!currentState.value1 || currentState.value1 * currentState.value2 < 0) {
|
|
// Per the specification:
|
|
// If value1 and value2 have opposite signs or if value1 is zero, then v(t) = value1 for T0 <= t < T1.
|
|
value = currentState.value1;
|
|
fillWithValue(values, value, currentState.fillToFrame, writeIndex);
|
|
return;
|
|
}
|
|
|
|
auto deltaTime = currentState.time2 - currentState.time1;
|
|
float numSampleFrames = deltaTime.value() * currentState.sampleRate;
|
|
// The value goes exponentially from value1 to value2 in a duration of deltaTime seconds (corresponding to numSampleFrames).
|
|
// Compute the per-sample multiplier.
|
|
float multiplier = powf(currentState.value2 / currentState.value1, 1 / numSampleFrames);
|
|
|
|
// Set the starting value of the exponential ramp.
|
|
value = currentState.value1 * pow(currentState.value2 / static_cast<double>(currentState.value1), (currentFrame * currentState.samplingPeriod - currentState.time1.value()) / deltaTime.value());
|
|
|
|
for (; writeIndex < currentState.fillToFrame; ++writeIndex) {
|
|
values[writeIndex] = value;
|
|
value *= multiplier;
|
|
++currentFrame;
|
|
}
|
|
|
|
// |value| got updated one extra time in the above loop. Restore it to the last computed value.
|
|
if (writeIndex >= 1)
|
|
value /= multiplier;
|
|
}
|
|
|
|
void AudioParamTimeline::processCancelValues(const AutomationState& currentState, float* values, size_t& currentFrame, float& value, unsigned& writeIndex)
|
|
{
|
|
// If the previous event was a SetTarget or ExponentialRamp
|
|
// event, the current value is one sample behind. Update
|
|
// the sample value by one sample, but only at the start of
|
|
// this CancelValues event.
|
|
if (currentState.event->hasDefaultCancelledValue())
|
|
value = currentState.event->value();
|
|
else {
|
|
double cancelFrame = currentState.time1.value() * currentState.sampleRate;
|
|
if (currentState.eventIndex >= 1 && cancelFrame <= currentFrame && currentFrame < cancelFrame + 1) {
|
|
auto lastEventType = m_events[currentState.eventIndex - 1].type();
|
|
if (lastEventType == ParamEvent::SetTarget) {
|
|
float target = m_events[currentState.eventIndex - 1].value();
|
|
float timeConstant = m_events[currentState.eventIndex - 1].timeConstant();
|
|
float discreteTimeConstant = static_cast<float>(AudioUtilities::discreteTimeConstantForSampleRate(timeConstant, currentState.controlRate));
|
|
value += (target - value) * discreteTimeConstant;
|
|
}
|
|
}
|
|
}
|
|
|
|
fillWithValue(values, value, currentState.fillToFrame, writeIndex);
|
|
|
|
currentFrame = currentState.fillToEndFrame;
|
|
}
|
|
|
|
void AudioParamTimeline::processSetTarget(const AutomationState& currentState, float* values, size_t& currentFrame, float& value, unsigned& writeIndex)
|
|
{
|
|
// Exponential approach to target value with given time constant.
|
|
float target = currentState.event->value();
|
|
float timeConstant = currentState.event->timeConstant();
|
|
float discreteTimeConstant = static_cast<float>(AudioUtilities::discreteTimeConstantForSampleRate(timeConstant, currentState.controlRate));
|
|
|
|
// Set the starting value correctly. This is only needed when the
|
|
// current time is "equal" to the start time of this event. This is
|
|
// to get the sampling correct if the start time of this automation
|
|
// isn't on a frame boundary. Otherwise, we can just continue from
|
|
// where we left off from the previous rendering quantum.
|
|
double rampStartFrame = currentState.time1.value() * currentState.sampleRate;
|
|
// Condition is c - 1 < r <= c where c = currentFrame and r =
|
|
// rampStartFrame. Compute it this way because currentFrame is
|
|
// unsigned and could be 0.
|
|
if (rampStartFrame <= currentFrame && currentFrame < rampStartFrame + 1)
|
|
value = target + (value - target) * exp(-(currentFrame * currentState.samplingPeriod - currentState.time1.value()) / timeConstant);
|
|
else {
|
|
// Otherwise, need to compute a new value because |value| is the
|
|
// last computed value of SetTarget. Time has progressed by one
|
|
// frame, so we need to update the value for the new frame.
|
|
value += (target - value) * discreteTimeConstant;
|
|
}
|
|
|
|
// If the value is close enough to the target, just fill in the data
|
|
// with the target value.
|
|
if (hasSetTargetConverged(value, target, Seconds { currentFrame * currentState.samplingPeriod }, currentState.time1, timeConstant)) {
|
|
currentFrame += currentState.fillToFrame - writeIndex;
|
|
fillWithValue(values, target, currentState.fillToFrame, writeIndex);
|
|
value = target;
|
|
return;
|
|
}
|
|
|
|
if (currentState.fillToFrame > writeIndex) {
|
|
// Resolve recursion by expanding constants to achieve a 4-step loop unrolling.
|
|
//
|
|
// v1 = v0 + (t - v0) * c
|
|
// v2 = v1 + (t - v1) * c
|
|
// v2 = v0 + (t - v0) * c + (t - (v0 + (t - v0) * c)) * c
|
|
// v2 = v0 + (t - v0) * c + (t - v0) * c - (t - v0) * c * c
|
|
// v2 = v0 + (t - v0) * c * (2 - c)
|
|
// Thus c0 = c, c1 = c*(2-c). The same logic applies to c2 and c3.
|
|
const float c0 = discreteTimeConstant;
|
|
const float c1 = c0 * (2 - c0);
|
|
const float c2 = c0 * ((c0 - 3) * c0 + 3);
|
|
const float c3 = c0 * (c0 * ((4 - c0) * c0 - 6) + 4);
|
|
float delta;
|
|
|
|
// Process 4 loop steps.
|
|
unsigned fillToFrameTrunc = writeIndex + ((currentState.fillToFrame - writeIndex) / 4) * 4;
|
|
const float cVector[4] = { 0, c0, c1, c2 };
|
|
|
|
for (; writeIndex < fillToFrameTrunc; writeIndex += 4) {
|
|
delta = target - value;
|
|
|
|
VectorMath::multiplyByScalar(&cVector[0], delta, &values[writeIndex], 4);
|
|
VectorMath::addScalar(&values[writeIndex], value, &values[writeIndex], 4);
|
|
|
|
value += delta * c3;
|
|
}
|
|
}
|
|
|
|
// Serially process remaining values.
|
|
for (; writeIndex < currentState.fillToFrame; ++writeIndex) {
|
|
values[writeIndex] = value;
|
|
value += (target - value) * discreteTimeConstant;
|
|
}
|
|
|
|
// The previous loops may have updated |value| one extra time.
|
|
// Reset it to the last computed value.
|
|
if (writeIndex >= 1)
|
|
value = values[writeIndex - 1];
|
|
|
|
currentFrame = currentState.fillToEndFrame;
|
|
}
|
|
|
|
void AudioParamTimeline::processSetValueCurve(const AutomationState& currentState, float* values, size_t& currentFrame, float& value, unsigned& writeIndex)
|
|
{
|
|
auto* curveData = currentState.event->curve().data();
|
|
unsigned numberOfCurvePoints = currentState.event->curve().size();
|
|
float curveEndValue = currentState.event->curveEndValue();
|
|
size_t fillToEndFrame = currentState.fillToEndFrame;
|
|
unsigned fillToFrame = currentState.fillToFrame;
|
|
|
|
// Curve events have duration, so don't just use next event time.
|
|
auto duration = currentState.event->duration();
|
|
double curvePointsPerFrame = currentState.event->curvePointsPerSecond() * currentState.samplingPeriod;
|
|
|
|
if (!curveData || !numberOfCurvePoints || duration <= 0_s || currentState.sampleRate <= 0) {
|
|
// Error condition - simply propagate previous value.
|
|
currentFrame = fillToEndFrame;
|
|
fillWithValue(values, value, fillToFrame, writeIndex);
|
|
return;
|
|
}
|
|
|
|
// Save old values and recalculate information based on the curve's duration
|
|
// instead of the next event time.
|
|
unsigned nextEventFillToFrame = fillToFrame;
|
|
|
|
double curveEndFrame = ceil(currentState.sampleRate * (currentState.time1 + duration).value());
|
|
if (currentState.endFrame > curveEndFrame)
|
|
fillToEndFrame = static_cast<size_t>(curveEndFrame);
|
|
else
|
|
fillToEndFrame = currentState.endFrame;
|
|
|
|
fillToFrame = (fillToEndFrame < currentState.startFrame) ? 0 : static_cast<unsigned>(fillToEndFrame - currentState.startFrame);
|
|
fillToFrame = std::min(fillToFrame, currentState.numberOfValues);
|
|
|
|
// Index into the curve data using a floating-point value.
|
|
// We're scaling the number of curve points by the duration (see curvePointsPerFrame).
|
|
double curveVirtualIndex = 0;
|
|
if (currentState.time1.value() < currentFrame * currentState.samplingPeriod) {
|
|
// Index somewhere in the middle of the curve data.
|
|
// Don't use timeToSampleFrame() since we want the exact floating-point frame.
|
|
double frameOffset = currentFrame - currentState.time1.value() * currentState.sampleRate;
|
|
curveVirtualIndex = curvePointsPerFrame * frameOffset;
|
|
}
|
|
|
|
// Set the default value in case fillToFrame is 0.
|
|
value = curveEndValue;
|
|
|
|
// Render the stretched curve data using nearest neighbor sampling.
|
|
// Oversampled curve data can be provided if smoothness is desired.
|
|
int k = 0;
|
|
for (; writeIndex < fillToFrame; ++writeIndex, ++k) {
|
|
// Compute current index this way to minimize round-off that would
|
|
// have occurred by incrementing the index by curvePointsPerFrame.
|
|
double currentVirtualIndex = curveVirtualIndex + k * curvePointsPerFrame;
|
|
unsigned curveIndex0;
|
|
|
|
// Clamp index to the last element of the array.
|
|
if (currentVirtualIndex < numberOfCurvePoints)
|
|
curveIndex0 = static_cast<unsigned>(currentVirtualIndex);
|
|
else
|
|
curveIndex0 = numberOfCurvePoints - 1;
|
|
|
|
unsigned curveIndex1 = std::min(curveIndex0 + 1, numberOfCurvePoints - 1);
|
|
|
|
// Linearly interpolate between the two nearest curve points.
|
|
// |delta| is clamped to 1 because currentVirtualIndex can exceed
|
|
// curveIndex0 by more than one. This can happen when we reached
|
|
// the end of the curve but still need values to fill out the
|
|
// current rendering quantum.
|
|
ASSERT(curveIndex0 < numberOfCurvePoints);
|
|
ASSERT(curveIndex1 < numberOfCurvePoints);
|
|
float c0 = curveData[curveIndex0];
|
|
float c1 = curveData[curveIndex1];
|
|
double delta = std::min(currentVirtualIndex - curveIndex0, 1.0);
|
|
|
|
value = c0 + (c1 - c0) * delta;
|
|
|
|
values[writeIndex] = value;
|
|
}
|
|
|
|
// If there's any time left after the duration of this event and the start
|
|
// of the next, then just propagate the last value.
|
|
if (writeIndex < nextEventFillToFrame) {
|
|
value = curveEndValue;
|
|
fillWithValue(values, value, nextEventFillToFrame, writeIndex);
|
|
}
|
|
|
|
// Re-adjust current time
|
|
currentFrame += nextEventFillToFrame;
|
|
}
|
|
|
|
void AudioParamTimeline::processSetTargetFollowedByRamp(int eventIndex, ParamEvent*& event, ParamEvent::Type nextEventType, size_t currentFrame, double sampleRate, double controlRate, float& value)
|
|
{
|
|
// If the current event is SetTarget and the next event is a LinearRampToValue or ExponentialRampToValue,
|
|
// special handling is needed. In this case, the linear and exponential ramp should start at wherever
|
|
// the SetTarget processing has reached.
|
|
if (event->type() != ParamEvent::SetTarget)
|
|
return;
|
|
|
|
if (nextEventType != ParamEvent::LinearRampToValue && nextEventType != ParamEvent::ExponentialRampToValue)
|
|
return;
|
|
|
|
// Replace the SetTarget with a SetValue to set the starting time and value for the ramp using the
|
|
// current frame. We need to update |value| appropriately depending on whether the ramp has started
|
|
// or not.
|
|
//
|
|
// If SetTarget starts somewhere between currentFrame - 1 and currentFrame, we directly compute the
|
|
// value it would have at currentFrame. If not, we update the value from the value from currentFrame - 1.
|
|
//
|
|
// Can't use the condition currentFrame - 1 <= t0 * sampleRate <= currentFrame because currentFrame
|
|
// is unsigned and could be 0. Instead, compute the condition this way, where f = currentFrame and
|
|
// Fs = sampleRate:
|
|
//
|
|
// f - 1 <= t0 * Fs <= f
|
|
// 2 * f - 2 <= 2 * Fs * t0 <= 2 * f
|
|
// -2 <= 2 * Fs * t0 - 2 * f <= 0
|
|
// -1 <= 2 * Fs * t0 - 2 * f + 1 <= 1
|
|
// abs(2 * Fs * t0 - 2 * f + 1) <= 1
|
|
|
|
if (fabs(2 * sampleRate * event->time().value() - 2 * currentFrame + 1) <= 1) {
|
|
// SetTarget is starting somewhere between currentFrame - 1 and currentFrame. Compute the value
|
|
// the SetTarget would have at the currentFrame.
|
|
value = event->value() + (value - event->value()) * exp(-(currentFrame / sampleRate - event->time().value()) / event->timeConstant());
|
|
} else {
|
|
// SetTarget has already started. Update |value| one frame because it's the value from the previous frame.
|
|
float discreteTimeConstant = static_cast<float>(AudioUtilities::discreteTimeConstantForSampleRate(event->timeConstant(), controlRate));
|
|
value += (event->value() - value) * discreteTimeConstant;
|
|
}
|
|
// Insert a SetValueEvent to mark the starting value and time.
|
|
// Clear the clamp check because this doesn't need it.
|
|
m_events[eventIndex] = ParamEvent::createSetValueEvent(value, Seconds { currentFrame / sampleRate });
|
|
|
|
// Update our pointer to the current event because we just changed it.
|
|
event = &m_events[eventIndex];
|
|
}
|
|
|
|
|
|
float AudioParamTimeline::linearRampAtTime(Seconds t, float value1, Seconds time1, float value2, Seconds time2)
|
|
{
|
|
return value1 + (value2 - value1) * (t - time1).value() / (time2 - time1).value();
|
|
}
|
|
|
|
float AudioParamTimeline::exponentialRampAtTime(Seconds t, float value1, Seconds time1, float value2, Seconds time2)
|
|
{
|
|
return value1 * pow(value2 / value1, (t - time1).value() / (time2 - time1).value());
|
|
}
|
|
|
|
float AudioParamTimeline::valueCurveAtTime(Seconds t, Seconds time1, Seconds duration, const float* curveData, size_t curveLength)
|
|
{
|
|
double curveIndex = (curveLength - 1) / duration.value() * (t - time1).value();
|
|
size_t k = std::min(static_cast<size_t>(curveIndex), curveLength - 1);
|
|
size_t k1 = std::min(k + 1, curveLength - 1);
|
|
float c0 = curveData[k];
|
|
float c1 = curveData[k1];
|
|
float delta = std::min(curveIndex - k, 1.0);
|
|
|
|
return c0 + (c1 - c0) * delta;
|
|
}
|
|
|
|
void AudioParamTimeline::handleCancelValues(ParamEvent& event, ParamEvent* nextEvent, float& value2, Seconds& time2, ParamEvent::Type& nextEventType)
|
|
{
|
|
if (!nextEvent || nextEvent->type() != ParamEvent::CancelValues || !nextEvent->savedEvent())
|
|
return;
|
|
|
|
float value1 = event.value();
|
|
auto time1 = event.time();
|
|
|
|
switch (event.type()) {
|
|
case ParamEvent::CancelValues:
|
|
case ParamEvent::LinearRampToValue:
|
|
case ParamEvent::ExponentialRampToValue:
|
|
case ParamEvent::SetValue: {
|
|
// These three events potentially establish a starting value for
|
|
// the following event, so we need to examine the cancelled
|
|
// event to see what to do.
|
|
auto* savedEvent = nextEvent->savedEvent();
|
|
|
|
// Update the end time and type to pretend that we're running
|
|
// this saved event type.
|
|
time2 = nextEvent->time();
|
|
nextEventType = savedEvent->type;
|
|
|
|
if (nextEvent->hasDefaultCancelledValue()) {
|
|
// We've already established a value for the cancelled
|
|
// event, so just return it.
|
|
value2 = nextEvent->value();
|
|
} else {
|
|
// If the next event would have been a LinearRamp or
|
|
// ExponentialRamp, we need to compute a new end value for
|
|
// the event so that the curve works continues as if it were
|
|
// not cancelled.
|
|
switch (savedEvent->type) {
|
|
case ParamEvent::LinearRampToValue:
|
|
value2 = linearRampAtTime(nextEvent->time(), value1, time1, savedEvent->value, savedEvent->time);
|
|
break;
|
|
case ParamEvent::ExponentialRampToValue:
|
|
value2 = exponentialRampAtTime(nextEvent->time(), value1, time1, savedEvent->value, savedEvent->time);
|
|
break;
|
|
case ParamEvent::SetValueCurve:
|
|
case ParamEvent::SetValue:
|
|
case ParamEvent::SetTarget:
|
|
case ParamEvent::CancelValues:
|
|
// These cannot be possible types for the saved event because they can't be created.
|
|
// createCancelValuesEvent doesn't allow them (SetValue, SetTarget, CancelValues) or
|
|
// cancelScheduledValues() doesn't create such an event (SetValueCurve).
|
|
ASSERT_NOT_REACHED();
|
|
break;
|
|
case ParamEvent::LastType:
|
|
ASSERT_NOT_REACHED();
|
|
break;
|
|
}
|
|
|
|
// Cache the new value so we don't keep computing it over and over.
|
|
nextEvent->setCancelledValue(value2);
|
|
}
|
|
} break;
|
|
case ParamEvent::SetValueCurve:
|
|
// Everything needed for this was handled when cancelling was
|
|
// done.
|
|
break;
|
|
case ParamEvent::SetTarget:
|
|
// Nothing special needs to be done for SetTarget
|
|
// followed by CancelValues.
|
|
break;
|
|
case ParamEvent::LastType:
|
|
ASSERT_NOT_REACHED();
|
|
break;
|
|
}
|
|
}
|
|
|
|
auto AudioParamTimeline::ParamEvent::createSetValueEvent(float value, Seconds time) -> ParamEvent
|
|
{
|
|
return ParamEvent { ParamEvent::SetValue, value, time, 0, Seconds { }, Vector<float> { }, 0, 0, std::nullopt };
|
|
}
|
|
|
|
auto AudioParamTimeline::ParamEvent::createLinearRampEvent(float value, Seconds time) -> ParamEvent
|
|
{
|
|
return { ParamEvent::LinearRampToValue, value, time, 0, Seconds { }, Vector<float> { }, 0, 0, std::nullopt };
|
|
}
|
|
|
|
auto AudioParamTimeline::ParamEvent::createExponentialRampEvent(float value, Seconds time) -> ParamEvent
|
|
{
|
|
return { ParamEvent::ExponentialRampToValue, value, time, 0, Seconds { }, Vector<float> { }, 0, 0, std::nullopt };
|
|
}
|
|
|
|
auto AudioParamTimeline::ParamEvent::createSetTargetEvent(float target, Seconds time, float timeConstant) -> ParamEvent
|
|
{
|
|
// The time line code does not expect a timeConstant of 0. (It returns NaN or Infinity due to division by zero. The caller
|
|
// should have converted this to a SetValueEvent.
|
|
ASSERT(!!timeConstant);
|
|
return { ParamEvent::SetTarget, target, time, timeConstant, Seconds { }, Vector<float> { }, 0, 0, std::nullopt };
|
|
}
|
|
|
|
auto AudioParamTimeline::ParamEvent::createSetValueCurveEvent(Vector<float>&& curve, Seconds time, Seconds duration) -> ParamEvent
|
|
{
|
|
double curvePointsPerSecond = (curve.size() - 1) / duration.value();
|
|
float curveEndValue = curve.last();
|
|
return { ParamEvent::SetValueCurve, 0, time, 0, duration, WTFMove(curve), curvePointsPerSecond, curveEndValue, std::nullopt };
|
|
}
|
|
|
|
auto AudioParamTimeline::ParamEvent::createCancelValuesEvent(Seconds cancelTime, std::optional<SavedEvent>&& savedEvent) -> ParamEvent
|
|
{
|
|
#if ASSERT_ENABLED
|
|
if (savedEvent) {
|
|
// The savedEvent can only have certain event types. Verify that.
|
|
auto savedEventType = savedEvent->type;
|
|
|
|
ASSERT(savedEventType != ParamEvent::LastType);
|
|
ASSERT(savedEventType == ParamEvent::LinearRampToValue
|
|
|| savedEventType == ParamEvent::ExponentialRampToValue
|
|
|| savedEventType == ParamEvent::SetValueCurve);
|
|
}
|
|
#endif
|
|
return { ParamEvent::CancelValues, 0, cancelTime, 0, Seconds { }, Vector<float> { }, 0, 0, WTFMove(savedEvent) };
|
|
}
|
|
|
|
bool AudioParamTimeline::isEventCurrent(const ParamEvent& event, const ParamEvent* nextEvent, size_t currentFrame, double sampleRate) const
|
|
{
|
|
// WARNING: due to round-off it might happen that nextEvent->time() is
|
|
// just larger than currentFrame/sampleRate. This means that we will end
|
|
// up running the |event| again. The code below had better be prepared
|
|
// for this case! What should happen is the fillToFrame should be 0 so
|
|
// that while the event is actually run again, nothing actually gets
|
|
// computed, and we move on to the next event.
|
|
//
|
|
// An example of this case is setValueCurveAtTime. The time at which
|
|
// setValueCurveAtTime ends (and the setValueAtTime begins) might be
|
|
// just past currentTime/sampleRate. Then setValueCurveAtTime will be
|
|
// processed again before advancing to setValueAtTime. The number of
|
|
// frames to be processed should be zero in this case.
|
|
if (nextEvent && nextEvent->time().value() < currentFrame / sampleRate) {
|
|
// But if the current event is a SetValue event and the event time is
|
|
// between currentFrame - 1 and curentFrame (in time). we don't want to
|
|
// skip it. If we do skip it, the SetValue event is completely skipped
|
|
// and not applied, which is wrong. Other events don't have this problem.
|
|
// (Because currentFrame is unsigned, we do the time check in this funny,
|
|
// but equivalent way.)
|
|
double eventFrame = event.time().value() * sampleRate;
|
|
|
|
// Condition is currentFrame - 1 < eventFrame <= currentFrame, but
|
|
// currentFrame is unsigned and could be 0, so use
|
|
// currentFrame < eventFrame + 1 instead.
|
|
if (!((event.type() == ParamEvent::SetValue && (eventFrame <= currentFrame) && (currentFrame < eventFrame + 1)))) {
|
|
// This is not the special SetValue event case, and nextEvent is
|
|
// in the past. We can skip processing of this event since it's
|
|
// in past.
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool AudioParamTimeline::hasValues(size_t startFrame, double sampleRate) const
|
|
{
|
|
if (!m_eventsLock.tryLock())
|
|
return true;
|
|
Locker locker { AdoptLock, m_eventsLock };
|
|
|
|
// Return false if there are no events in the time range.
|
|
auto endFrame = startFrame + AudioUtilities::renderQuantumSize;
|
|
return !m_events.isEmpty() && endFrame / sampleRate > m_events[0].time().value();
|
|
}
|
|
|
|
} // namespace WebCore
|
|
|
|
#endif // ENABLE(WEB_AUDIO)
|