track-fade.jsjavascript
/**
* @file Fade-scheduling logic behind scheduleTrackFade(), extracted so it's testable in
* plain Node (test/track-fade.test.js). UMD-lite, same as automix.js. Computes the Web
* Audio gain-automation events that fade a track to silence over the video's final
* moments.
* @module track-fade
*/
(function (root, factory) {
const api = factory();
if (typeof module === 'object' && module.exports) {
module.exports = api;
} else {
root.TrackFade = api;
}
})(typeof self !== 'undefined' ? self : this, function () {
/** Length of the end-of-video fade-out, in seconds. @type {number} */
const TRACK_FADE_SEC = 0.5;
/**
* Build the ordered list of Web Audio gain-automation events for a track fade-out.
*
* The schedule anchors the gain at `now`: without that anchor a looping track inherits
* the previous loop's leftover gain (0 after a completed fade) and plays silent for the
* whole next loop. When `rampSec > 0` the anchor glides to `currentGain` via
* setTargetAtTime instead of jumping — used when the schedule is being rebuilt because
* the user just MOVED a volume slider, so the level change itself stays smooth (matching
* the setTargetAtTime the slider handler would otherwise have done alone). The glide
* only happens when there's room before the hold: at fadeStart === 0 the two events
* would collide at the same instant and their ordering is not worth relying on, so it
* jumps instead.
*
* The hold event (at now + fadeStart) must carry the CURRENT gain — a stale value there
* is what made Auto-Mix jump the level back the moment the fade began (issue #342).
* @param {Object} args
* @param {number} args.currentGain Track gain to hold until the fade, linear amplitude.
* @param {number} args.videoCurrentTime Video playback position, in seconds.
* @param {number} args.videoDuration Total video duration, in seconds.
* @param {number} args.now AudioContext.currentTime, in seconds.
* @param {number} [args.fadeSec=TRACK_FADE_SEC] Fade length, in seconds.
* @param {number} [args.rampSec=0] Time constant for gliding to `currentGain`, in
* seconds; 0 jumps immediately.
* @returns {Array<{method: string, value: number, time: number, timeConstant: number}>}
* Gain-automation events in application order: cancel, anchor, hold, linear ramp to 0
* ending exactly at the video's last frame.
*/
function computeFadeSchedule({ currentGain, videoCurrentTime, videoDuration, now, fadeSec = TRACK_FADE_SEC, rampSec = 0 }) {
const timeLeft = videoDuration - videoCurrentTime;
const fadeStart = Math.max(0, timeLeft - fadeSec);
const anchor = rampSec > 0 && fadeStart > 0
? { method: 'setTargetAtTime', value: currentGain, time: now, timeConstant: rampSec }
: { method: 'setValueAtTime', value: currentGain, time: now };
return [
{ method: 'cancelScheduledValues', time: now },
anchor,
{ method: 'setValueAtTime', value: currentGain, time: now + fadeStart },
{ method: 'linearRampToValueAtTime', value: 0, time: now + timeLeft },
];
}
return { TRACK_FADE_SEC, computeFadeSchedule };
});