Skip to content

Commit

Permalink
Insert initial event if needed for linear or exponential ramp automation
Browse files Browse the repository at this point in the history
When there is no event preceding the linear or exponential ramp,
insert a new event (setValueAtTime) to give the starting point for the
ramp.  This fixes the unintuitive behavior where the ramp basically
did nothing except cause a jump to the final value at the end time of
the ramp.

WebAudio issue: WebAudio/web-audio-api#130
Resolution: WebAudio/web-audio-api#659

BUG=552072
TEST=audioparam-initial-event.html

Review URL: https://codereview.chromium.org/1432813003

Cr-Commit-Position: refs/heads/master@{#388242}
  • Loading branch information
rtoy authored and Commit bot committed Apr 19, 2016
1 parent 862ec97 commit f8198c3
Show file tree
Hide file tree
Showing 6 changed files with 224 additions and 16 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Test Automation Ramps without Initial Event

On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".


PASS Linear ramp equals [0.5,0.4998958333333333,0.4997916666666667,0.4996875,0.4995833333333333,0.49947916666666664,0.499375,0.49927083333333333,0.49916666666666665,0.4990625,0.49895833333333334,0.49885416666666665,0.49875,0.49864583333333334,0.49854166666666666,0.4984375,...] with an element-wise tolerance of 6.0003e-8.
PASS Exponential ramp equals [0.5,0.5001444220542908,0.5002889037132263,0.5004333853721619,0.5005779266357422,0.5007225275039673,0.5008671879768372,0.501011848449707,0.5011565685272217,0.5013013482093811,0.5014461278915405,0.5015909671783447,0.5017358660697937,0.5018808245658875,0.5020257830619812,0.5021708011627197,...] with an element-wise tolerance of 0.0000023842.
PASS Delayed linear ramp equals [0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,...] with an element-wise tolerance of 9.87968e-8.
PASS Delayed exponential ramp equals [0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5,...] with an element-wise tolerance of 0.000013948.
PASS successfullyParsed is true

TEST COMPLETE

175 changes: 175 additions & 0 deletions third_party/WebKit/LayoutTests/webaudio/audioparam-initial-event.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
<!doctype html>
<html>

<head>
<script src="../resources/js-test.js"></script>
<script src="resources/compatibility.js"></script>
<script src="resources/audio-testing.js"></script>
<script src="resources/audioparam-testing.js"></script>
<title>AudioParam Initial Events </title>
</head>

<body>
<script>
description("Test Automation Ramps without Initial Event");
window.jsTestIsAsync = true;

var sampleRate = 48000;
// Number of frames in a rendering quantum.
var quantumFrames = 128;
// Test doesn't need to run for very long.
var renderDuration = 0.2;
var renderFrames = renderDuration * sampleRate;
var automationEndTime = 0.1;

var audit = Audit.createTaskRunner();

// The following tests start a ramp automation without an initial event. This should cause an
// initial event to be added implicitly to give a starting point.
audit.defineTask("linear-ramp", function (done) {
runTest("Linear ramp", {
automationFunction: function (node, value, time) {
node.gain.linearRampToValueAtTime(value, time);
},
referenceFunction: linearRamp,
// Experimentally determined threshold
threshold: 6.0003e-8,
// The starting value of the gain node
v0: 0.5,
// The target value of the automation
v1: 0,
})
.then(done);
});

audit.defineTask("exponential-ramp", function (done) {
runTest("Exponential ramp", {
automationFunction: function (node, value, time) {
node.gain.exponentialRampToValueAtTime(value, time);
},
referenceFunction: exponentialRamp,
threshold: 2.3842e-6,
v0: 0.5,
v1: 2,
})
.then(done);
});

// Same tests as above, but we delay the call to the automation function. This is to verify that
// the we still do the right thing after the context has started.
audit.defineTask("delayed-linear-ramp", function (done) {
runTest("Delayed linear ramp", {
automationFunction: function (node, value, time) {
node.gain.linearRampToValueAtTime(value, time);
},
referenceFunction: linearRamp,
// Experimentally determined threshold
threshold: 9.87968e-8,
// The starting value of the gain node
v0: 0.5,
// The target value of the automation
v1: 0,
delay: quantumFrames / sampleRate
})
.then(done);
});

audit.defineTask("delayed-exponential-ramp", function (done) {
runTest("Delayed exponential ramp", {
automationFunction: function (node, value, time) {
node.gain.exponentialRampToValueAtTime(value, time);
},
referenceFunction: exponentialRamp,
// Experimentally determined threshold
threshold: 1.3948e-5,
// The starting value of the gain node
v0: 0.5,
// The target value of the automation
v1: 2,
delay: quantumFrames / sampleRate
})
.then(done);
});

audit.defineTask("finish", function (done) {
finishJSTest();
done();
});

audit.runTasks();

// Generate the expected waveform for a linear ramp starting from the value |v0|, ramping to
// |v1| at time |endTime|. The time of |v0| is assumed to be 0. |nFrames| is how many frames
// to generate.
function linearRamp(v0, v1, startTime, endTime, nFrames) {
var expected = createLinearRampArray(startTime, endTime, v0, v1, sampleRate);
var preFiller = new Array(Math.floor(startTime * sampleRate));
var postFiller = new Array(nFrames - Math.ceil(endTime * sampleRate));
preFiller.fill(v0);
return preFiller.concat(expected.concat(postFiller.fill(v1)));
}

// Generate the expected waveform for an exponential ramp starting from the value |v0|,
// ramping to |v1| at time |endTime|. The time of |v0| is assumed to be 0. |nFrames| is how
// many frames to generate.
function exponentialRamp(v0, v1, startTime, endTime, nFrames) {
var expected = createExponentialRampArray(startTime, endTime, v0, v1, sampleRate);
var preFiller = new Array(Math.floor(startTime * sampleRate));
preFiller.fill(v0);
var postFiller = new Array(nFrames - Math.ceil(endTime * sampleRate));
return preFiller.concat(expected.concat(postFiller.fill(v1)));
}

// Run an automation test. |message| is the message to use for printing the results. |options|
// is a property bag containing the configuration of the test including the following:
//
// automationFunction - automation function to use,
// referenceFunction - function generating the expected result
// threshold - comparison threshold
// v0 - starting value
// v1 - end value for automation
function runTest(message, options) {
var automationFunction = options.automationFunction;
var referenceFunction = options.referenceFunction;
var threshold = options.threshold;
var v0 = options.v0;
var v1 = options.v1;
var delay = options.delay;

var context = new OfflineAudioContext(1, renderFrames, sampleRate);

// A constant source of amplitude 1.
var source = context.createBufferSource();
source.buffer = createConstantBuffer(context, 1, 1);
source.loop = true;

// Automation is applied to a gain node
var gain = context.createGain();
gain.gain.value = v0;

// Delay start of automation, if requested
if (delay) {
context.suspend(options.delay).then(function () {
automationFunction(gain, v1, automationEndTime);
context.resume();
});
} else {
automationFunction(gain, v1, automationEndTime);
}

source.connect(gain);
gain.connect(context.destination);

source.start();

return context.startRendering()
.then(function (resultBuffer) {
var result = resultBuffer.getChannelData(0);
var expected = referenceFunction(v0, v1, delay ? delay : 0, automationEndTime, renderFrames);
Should(message, result).beCloseToArray(expected, threshold);
});
}
</script>
</body>

</html>
5 changes: 3 additions & 2 deletions third_party/WebKit/Source/modules/webaudio/AudioParam.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -237,13 +237,14 @@ AudioParam* AudioParam::setValueAtTime(float value, double time, ExceptionState&

AudioParam* AudioParam::linearRampToValueAtTime(float value, double time, ExceptionState& exceptionState)
{
handler().timeline().linearRampToValueAtTime(value, time, exceptionState);
handler().timeline().linearRampToValueAtTime(
value, time, handler().intrinsicValue(), context()->currentTime(), exceptionState);
return this;
}

AudioParam* AudioParam::exponentialRampToValueAtTime(float value, double time, ExceptionState& exceptionState)
{
handler().timeline().exponentialRampToValueAtTime(value, time, exceptionState);
handler().timeline().exponentialRampToValueAtTime(value, time, handler().intrinsicValue(), context()->currentTime(), exceptionState);
return this;
}

Expand Down
2 changes: 1 addition & 1 deletion third_party/WebKit/Source/modules/webaudio/AudioParam.h
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ class AudioParamHandler final : public ThreadSafeRefCounted<AudioParamHandler>,
void connect(AudioNodeOutput&);
void disconnect(AudioNodeOutput&);

float intrinsicValue() const { return noBarrierLoad(&m_intrinsicValue); }
private:
AudioParamHandler(AbstractAudioContext& context, double defaultValue)
: AudioSummingJunction(context.deferredTaskHandler())
Expand All @@ -114,7 +115,6 @@ class AudioParamHandler final : public ThreadSafeRefCounted<AudioParamHandler>,

// Intrinsic value
float m_intrinsicValue;
float intrinsicValue() const { return noBarrierLoad(&m_intrinsicValue); }
void setIntrinsicValue(float newValue) { noBarrierStore(&m_intrinsicValue, newValue); }

float m_defaultValue;
Expand Down
25 changes: 17 additions & 8 deletions third_party/WebKit/Source/modules/webaudio/AudioParamTimeline.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -114,14 +114,14 @@ AudioParamTimeline::ParamEvent AudioParamTimeline::ParamEvent::createSetValueEve
return ParamEvent(ParamEvent::SetValue, value, time, 0, 0, nullptr);
}

AudioParamTimeline::ParamEvent AudioParamTimeline::ParamEvent::createLinearRampEvent(float value, double time)
AudioParamTimeline::ParamEvent AudioParamTimeline::ParamEvent::createLinearRampEvent(float value, double time, float initialValue, double callTime)
{
return ParamEvent(ParamEvent::LinearRampToValue, value, time, 0, 0, nullptr);
return ParamEvent(ParamEvent::LinearRampToValue, value, time, 0, 0, nullptr, initialValue, callTime);
}

AudioParamTimeline::ParamEvent AudioParamTimeline::ParamEvent::createExponentialRampEvent(float value, double time)
AudioParamTimeline::ParamEvent AudioParamTimeline::ParamEvent::createExponentialRampEvent(float value, double time, float initialValue, double callTime)
{
return ParamEvent(ParamEvent::ExponentialRampToValue, value, time, 0, 0, nullptr);
return ParamEvent(ParamEvent::ExponentialRampToValue, value, time, 0, 0, nullptr, initialValue, callTime);
}

AudioParamTimeline::ParamEvent AudioParamTimeline::ParamEvent::createSetTargetEvent(float value, double time, double timeConstant)
Expand All @@ -144,17 +144,17 @@ void AudioParamTimeline::setValueAtTime(float value, double time, ExceptionState
insertEvent(ParamEvent::createSetValueEvent(value, time), exceptionState);
}

void AudioParamTimeline::linearRampToValueAtTime(float value, double time, ExceptionState& exceptionState)
void AudioParamTimeline::linearRampToValueAtTime(float value, double time, float initialValue, double callTime, ExceptionState& exceptionState)
{
ASSERT(isMainThread());

if (!isNonNegativeAudioParamTime(time, exceptionState))
return;

insertEvent(ParamEvent::createLinearRampEvent(value, time), exceptionState);
insertEvent(ParamEvent::createLinearRampEvent(value, time, initialValue, callTime), exceptionState);
}

void AudioParamTimeline::exponentialRampToValueAtTime(float value, double time, ExceptionState& exceptionState)
void AudioParamTimeline::exponentialRampToValueAtTime(float value, double time, float initialValue, double callTime, ExceptionState& exceptionState)
{
ASSERT(isMainThread());

Expand All @@ -171,7 +171,7 @@ void AudioParamTimeline::exponentialRampToValueAtTime(float value, double time,
return;
}

insertEvent(ParamEvent::createExponentialRampEvent(value, time), exceptionState);
insertEvent(ParamEvent::createExponentialRampEvent(value, time, initialValue, callTime), exceptionState);
}

void AudioParamTimeline::setTargetAtTime(float target, double time, double timeConstant, ExceptionState& exceptionState)
Expand Down Expand Up @@ -219,6 +219,15 @@ void AudioParamTimeline::insertEvent(const ParamEvent& event, ExceptionState& ex
unsigned i = 0;
double insertTime = event.time();

if (!m_events.size()
&& (event.getType() == ParamEvent::LinearRampToValue
|| event.getType() == ParamEvent::ExponentialRampToValue)) {
// There are no events preceding these ramps. Insert a new setValueAtTime event to set the
// starting point for these events.
m_events.insert(0,
AudioParamTimeline::ParamEvent::createSetValueEvent(event.initialValue(), event.callTime()));
}

for (i = 0; i < m_events.size(); ++i) {
if (event.getType() == ParamEvent::SetValueCurve) {
// If this event is a SetValueCurve, make sure it doesn't overlap any existing
Expand Down
20 changes: 15 additions & 5 deletions third_party/WebKit/Source/modules/webaudio/AudioParamTimeline.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ class AudioParamTimeline {
}

void setValueAtTime(float value, double time, ExceptionState&);
void linearRampToValueAtTime(float value, double time, ExceptionState&);
void exponentialRampToValueAtTime(float value, double time, ExceptionState&);
void linearRampToValueAtTime(float value, double time, float initialValue, double callTime, ExceptionState&);
void exponentialRampToValueAtTime(float value, double time, float initialValue, double callTime, ExceptionState&);
void setTargetAtTime(float target, double time, double timeConstant, ExceptionState&);
void setValueCurveAtTime(DOMFloat32Array* curve, double time, double duration, ExceptionState&);
void cancelScheduledValues(double startTime, ExceptionState&);
Expand Down Expand Up @@ -78,8 +78,8 @@ class AudioParamTimeline {
LastType
};

static ParamEvent createLinearRampEvent(float value, double time);
static ParamEvent createExponentialRampEvent(float value, double time);
static ParamEvent createLinearRampEvent(float value, double time, float initialValue, double callTime);
static ParamEvent createExponentialRampEvent(float value, double time, float initialValue, double callTime);
static ParamEvent createSetValueEvent(float value, double time);
static ParamEvent createSetTargetEvent(float value, double time, double timeConstant);
static ParamEvent createSetValueCurveEvent(DOMFloat32Array* curve, double time, double duration);
Expand All @@ -90,15 +90,21 @@ class AudioParamTimeline {
double timeConstant() const { return m_timeConstant; }
double duration() const { return m_duration; }
DOMFloat32Array* curve() { return m_curve.get(); }
float initialValue() const { return m_initialValue; }
double callTime() const { return m_callTime; }

private:
ParamEvent(Type type, float value, double time, double timeConstant, double duration, DOMFloat32Array* curve)
ParamEvent(Type type, float value, double time,
double timeConstant, double duration, DOMFloat32Array* curve,
float initialValue = 0, double callTime = 0)
: m_type(type)
, m_value(value)
, m_time(time)
, m_timeConstant(timeConstant)
, m_duration(duration)
, m_curve(curve)
, m_initialValue(initialValue)
, m_callTime(callTime)
{
}

Expand All @@ -110,6 +116,10 @@ class AudioParamTimeline {
// Only used for SetValueCurve events.
double m_duration;
CrossThreadPersistent<DOMFloat32Array> m_curve;
// Initial value and time to use for linear and exponential ramps that don't have a
// preceding event.
float m_initialValue;
double m_callTime;
};

void insertEvent(const ParamEvent&, ExceptionState&);
Expand Down

0 comments on commit f8198c3

Please sign in to comment.