Created
October 28, 2015 01:19
-
-
Save mrkishi/5ea32350a5bd0c52bf33 to your computer and use it in GitHub Desktop.
AudioParam Automation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(() => { | |
'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