Skip to content

Instantly share code, notes, and snippets.

@mrkishi
Created October 28, 2015 01:19
Show Gist options
  • Save mrkishi/5ea32350a5bd0c52bf33 to your computer and use it in GitHub Desktop.
Save mrkishi/5ea32350a5bd0c52bf33 to your computer and use it in GitHub Desktop.
AudioParam Automation
(() => {
'use strict';
class AutomationEvent {
constructor(proxy, time) {
this.proxy = proxy;
this.time = time;
}
getValueAtTime(t) {
// Implemented by subclasses;
//
// Only valid for startTime <= t <= endTime.
// t < startTime === undefined
// t > endTime === last valid value
return undefined;
}
}
class Ramp extends AutomationEvent {
constructor(proxy, value, endTime) {
super(proxy, endTime);
this.value = value;
this.endTime = endTime;
}
getStartValue() {
let prev = this.proxy.eventBeforeTime(this.endTime);
let value;
if (prev) {
if (prev instanceof Target) {
// Spec unclear: On Chrome, setTargetAtTime acts like
// setValueAtTime when followed by a ramp
value = prev.value;
} else {
value = prev.getValueAtTime(this.endTime);
}
} else {
// Spec unclear: There's no ramp on Chrome
value = this.value;
}
return value;
}
getStartTime() {
let prev = this.proxy.eventBeforeTime(this.endTime);
let value;
if (prev) {
value = prev.time;
} else {
// Spec unclear: There's no ramp on Chrome
value = this.endTime;
}
return value;
}
getValueAtTime(t) {
let v0 = this.getStartValue(),
v1 = this.value,
t0 = this.getStartTime(),
t1 = this.endTime,
value;
if (t < t0) {
value = undefined;
} else if (t < this.endTime) {
value = this.ramp(t0, t1, v0, v1, t);
} else {
value = v1;
}
return value;
}
ramp(t0, t1, v0, v1, t) {
return v1;
}
}
class LinearRamp extends Ramp {
ramp(t0, t1, v0, v1, t) {
return v0 + (v1 - v0) * ((t - t0) / (t1 - t0));
}
}
class ExponentialRamp extends Ramp {
ramp(t0, t1, v0, v1, t) {
return v0 * Math.pow(v1 / v0, (t - t0) / (t1 - t0));
}
}
class Value extends AutomationEvent {
constructor(proxy, value, startTime) {
super(proxy, startTime);
this.value = value;
this.startTime = startTime;
}
getValueAtTime(t) {
let value = undefined;
if (t >= this.startTime) {
value = this.value;
}
return value;
}
}
class Target extends AutomationEvent {
constructor(proxy, target, startTime, timeConstant) {
super(proxy, startTime);
this.target = target;
this.startTime = startTime;
this.timeConstant = timeConstant;
}
getStartValue() {
let prev = this.proxy.eventBeforeTime(this.startTime);
let value;
if (prev) {
value = prev.getValueAtTime(this.startTime);
} else {
value = this.proxy.param.directValue;
}
return value;
}
getValueAtTime(t) {
let v0 = this.getStartValue(),
v1 = this.target,
t0 = this.startTime,
tau = this.timeConstant,
value = undefined;
if (t >= t0) {
value = v1 + (v0 - v1) * Math.pow(Math.E, -((t - t0) / tau));
}
return value;
}
}
class ValueCurve extends AutomationEvent {
constructor(proxy, values, startTime, duration) {
super(proxy, startTime);
this.values = values;
this.startTime = startTime;
this.duration = duration;
}
getValueAtTime(t) {
let v = this.values,
n = this.values.length - 1,
t0 = this.startTime,
td = this.duration,
value;
if (t < t0) {
value = undefined;
} else if (t < t0 + td) {
let i = n / td * (t - t0);
let k = Math.floor(i);
let kt = i - k;
value = (1-kt) * v[k] + kt * v[k+1];
} else {
value = v[n];
}
return value;
}
}
class AudioParamProxy {
constructor(param) {
this.events = [];
this.param = param;
}
setValueAtTime(value, startTime) {
this.events.push(new Value(this, value, startTime));
this.sort();
}
setValueCurveAtTime(values, startTime, duration) {
this.events.push(new ValueCurve(this, value, startTime, duration));
this.sort();
}
setTargetAtTime(target, startTime, timeConstant) {
this.events.push(new Target(this, target, startTime, timeConstant));
this.sort();
}
linearRampToValueAtTime(value, endTime) {
this.events.push(new LinearRamp(this, value, endTime));
this.sort();
}
exponentialRampToValueAtTime(value, endTime) {
this.events.push(new ExponentialRamp(this, value, endTime));
this.sort();
}
cancelScheduledValues(startTime) {
// Spec unclear about setValueCurveAtTime and setTargetAtTime
this.events = this.events.filter(event => event.time < startTime);
}
empty() {
return this.events.length === 0;
}
sort() {
this.events.sort((a, b) => a.time - b.time);
}
eventAfterTime(time) {
for (let i = 0; i < this.events.length; ++i) {
if (this.events[i].time >= time) {
return this.events[i];
}
}
}
eventBeforeTime(time) {
for (let i = this.events.length - 1; i >= 0; --i) {
if (this.events[i].time < time) {
return this.events[i];
}
}
}
// Unused, here for documentation
getValueAtTime(t) {
let value = this.param.directValue;
let prev = this.eventBeforeTime(t);
let next = this.eventAfterTime(t);
if (next instanceof Ramp) {
value = next.getValueAtTime(t);
} else if (prev) {
value = prev.getValueAtTime(t);
}
return value;
}
}
// Monkey-patching AudioParam to keep track of the automation events
// and also add a getter for the direct value (ie. before intrinsic computation),
// as spec is currently unclear about the correct behavior of the getter
AudioParam.prototype._value = Object.getOwnPropertyDescriptor(AudioParam.prototype, 'value');
Object.defineProperty(AudioParam.prototype, 'value', {
get: function () {
return this._value.get.call(this);
},
set: function (value) {
this._set = true;
this._directValue = value;
this._value.set.call(this, value);
}
});
Object.defineProperty(AudioParam.prototype, 'directValue', {
get: function () {
if (this._set) {
return this._directValue;
} else {
return this.defaultValue;
}
}
});
Object.defineProperty(AudioParam.prototype, 'proxy', {
get: function () {
this._proxy = this._proxy || new AudioParamProxy(this);
return this._proxy;
}
});
// AudioParam.fn -> AudioParamProxy.fn
[
AudioParam.prototype.setValueAtTime,
AudioParam.prototype.setValueCurveAtTime,
AudioParam.prototype.setTargetAtTime,
AudioParam.prototype.linearRampToValueAtTime,
AudioParam.prototype.exponentialRampToValueAtTime,
AudioParam.prototype.cancelScheduledValues,
].forEach(fn => {
AudioParam.prototype[fn.name] = function () {
this.proxy[fn.name].apply(this.proxy, arguments);
fn.apply(this, arguments);
};
});
// The scheduled "cancel-and-hold"
AudioParam.prototype.cancelAutomationAfterTime = function (startTime) {
if (this.proxy.empty()) return;
let prev = this.proxy.eventBeforeTime(startTime);
let next = this.proxy.eventAfterTime(startTime);
if (next instanceof Ramp) {
let value = next.getValueAtTime(startTime);
if (next instanceof LinearRamp) {
this.cancelScheduledValues(startTime);
this.linearRampToValueAtTime(value, startTime);
} else if (next instanceof ExponentialRamp) {
this.cancelScheduledValues(startTime);
this.exponentialRampToValueAtTime(value, startTime);
}
} else if (prev) {
let value = prev.getValueAtTime(startTime);
this.cancelScheduledValues(startTime);
this.setValueAtTime(value, startTime);
}
};
let ctx = new AudioContext();
let osc = ctx.createOscillator();
osc.connect(ctx.destination);
osc.start();
osc.frequency.setValueAtTime(440, ctx.currentTime);
//osc.frequency.setValueCurveAtTime(new Float32Array([440, 880]), ctx.currentTime, 5);
//osc.frequency.setTargetAtTime(880, ctx.currentTime, 5);
osc.frequency.linearRampToValueAtTime(880, ctx.currentTime + 5);
//osc.frequency.exponentialRampToValueAtTime(880, ctx.currentTime + 5);
osc.frequency.cancelAutomationAfterTime(ctx.currentTime + 2.5);
osc.stop(ctx.currentTime + 5);
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment