Skip to content

Commit

Permalink
Fix bugs/ improve automation() & buildOptions()
Browse files Browse the repository at this point in the history
- Set the final options ramp only if the given ramp is not undefined
- Add `skipImmediate` parameter to automation(), which will force it to run the full automation even if the change appears redundant. Using a number[] for ramp will do nothing if the difference between the current value of the audioParam is the same as the destination value, even when using `skipImmediate`. This may be a permanent limitation.
- Re-order setValueAtTime and cancelAndHoldAtTime, as this is the order the events should happen in. In practice, this doesn't make a meaningful difference.
- If an exponential ramp is requested, but either the current value or the destination value is close to zero, the ramp is changed to a natural ramp. See included comment for explaination. TL;DR: exponential isn't intuitive, so use natural.
- Fix bug with chrome's implementation of Web Audio API. In theory, calling cancelAndHoldAtTime(time) on an active setTargetAtTime() is supposed to insert an implicit setValueAtTime(time). Chrome seemingly implements the spec incorrectly, or I don't understand the spec.
- Modify natural ramping to use a local constant for timesteps, which can be more easily changed in the future by other developers
- Add warning log when calling automation() without a ramp type
  • Loading branch information
almic committed Apr 7, 2024
1 parent e906827 commit abf8a40
Show file tree
Hide file tree
Showing 2 changed files with 55 additions and 9 deletions.
59 changes: 51 additions & 8 deletions src/automation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,26 +46,38 @@ export type AudioAdjustmentOptions = {

/**
* Automation function for AudioParam
*
* Set `skipImmediate` to `true` if you want the automation to play out in its entirety, even if
* the current value of the audioParam is at `value`. By default, the automation will cancel active
* automations, so in many cases playing the full duration of the automation is not needed.
*
* @param audioContext the audioContext from which to use as the time system
* @param audioParam the audioParam to automate
* @param value the value to automate towards
* @param options the method of automation
* @param skipImmediate if true, dont short the automation if the current value is already at the
* given value, allow the automation to play out.
*/
export default function automation(
audioContext: AudioContext,
audioParam: AudioParam,
value: number,
options: Required<AudioAdjustmentOptions>,
skipImmediate: boolean = false,
): void {
const currentValue = audioParam.value;
const difference = value - currentValue;

// Stop automations and immediately ramp.
if (Math.abs(difference) < Number.EPSILON) {
audioParam.cancelAndHoldAtTime(audioContext.currentTime);
if (!skipImmediate && Math.abs(difference) < Number.EPSILON) {
audioParam.setValueAtTime(currentValue, audioContext.currentTime);
audioParam.cancelAndHoldAtTime(audioContext.currentTime);
audioParam.linearRampToValueAtTime(value, options.delay + audioContext.currentTime);
return;
}

audioParam.cancelAndHoldAtTime(options.delay + audioContext.currentTime);
audioParam.setValueAtTime(currentValue, options.delay + audioContext.currentTime);
audioParam.cancelAndHoldAtTime(options.delay + audioContext.currentTime);
if (Array.isArray(options.ramp)) {
const valueCurve = [];
for (const markiplier of options.ramp) {
Expand All @@ -79,6 +91,29 @@ export default function automation(
return;
}

/**
* It is necessary to explain the function of exponential ramping:
*
* - Ramping from zero to any value is the same as using setValueAtTime()
* - Ramping from any value to zero is undefined
* - Ramping to values near zero have an instantaneous effect
*
* The only way to "exponentially" ramp to or away from zero is to use
* natural ramping; `setTargetAtTime()`. Therefore, an exponential ramp is
* converted to a natural ramp when the start or end is near zero.
*
* This conversion is done with the goal of being intuitive, as it may not
* be well understood that normal exponential ramping has these limitations.
*/

if (
options.ramp == AudioRampType.EXPONENTIAL &&
(Math.abs(currentValue) < Number.EPSILON || Math.abs(value) < Number.EPSILON)
) {
options = structuredClone(options);
options.ramp = AudioRampType.NATURAL;
}

switch (options.ramp) {
case AudioRampType.EXPONENTIAL: {
audioParam.exponentialRampToValueAtTime(
Expand All @@ -96,20 +131,28 @@ export default function automation(
}
case AudioRampType.NATURAL: {
// Logarithmic approach to value, it is 95% the way there after 3 timeConstant, so we linearly ramp at that point
const timeConstant = options.duration / 4;
const timeSteps = 4;
const timeConstant = options.duration / timeSteps;
audioParam.setTargetAtTime(value, options.delay + audioContext.currentTime, timeConstant);
audioParam.cancelAndHoldAtTime(options.delay + timeConstant * 3 + audioContext.currentTime);
// The following event is implicitly added, per WebAudio spec.
audioParam.cancelAndHoldAtTime(
timeConstant * (timeSteps - 1) + options.delay + audioContext.currentTime,
);
// ThE fOlLoWiNg EvEnT iS iMpLiCiTlY aDdEd, PeR wEbAuDiO SpEc.
// https://webaudio.github.io/web-audio-api/#dom-audioparam-cancelandholdattime
// this.gainNode.gain.setValueAtTime(currentValue + (difference * (1 - Math.pow(Math.E, -3))), timeConstant * 3 + this.currentTime);
// https://www.youtube.com/watch?v=EzWNBmjyv7Y
audioParam.setValueAtTime(
currentValue + difference * (1 - Math.pow(Math.E, -(timeSteps - 1))),
timeConstant * (timeSteps - 1) + audioContext.currentTime,
);
audioParam.linearRampToValueAtTime(
value,
options.delay + options.duration + audioContext.currentTime,
);
break;
}
default: {
audioParam.setValueAtTime(value, options.delay);
console.warn(`Automation function received unknown ramp type ${options.ramp}`);
audioParam.setValueAtTime(value, options.delay + audioContext.currentTime);
break;
}
}
Expand Down
5 changes: 4 additions & 1 deletion src/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,10 @@ function buildOptions(

const fullOptions = structuredClone(defaultOptions);

fullOptions.ramp = structuredClone(options.ramp);
// while required in typescript, javascript will let this be undefined
if (options.ramp) {
fullOptions.ramp = structuredClone(options.ramp);
}

if (options.delay) {
fullOptions.delay = options.delay;
Expand Down

0 comments on commit abf8a40

Please sign in to comment.