|
| 1 | +const MIN_AUDIO_GAIN = 0.0001; |
| 2 | + |
| 3 | +/** |
| 4 | + * Computes fade durations from Sample Settings percentages for any sample-like |
| 5 | + * playback window. The same shaping is used for pattern voices and playlist |
| 6 | + * audio clips so UI In/Out controls sound consistent everywhere. |
| 7 | + * |
| 8 | + * @param {number} sourcePlayDuration |
| 9 | + * @param {object} settings |
| 10 | + * @returns {{fadeInSec:number,shapedFadeOutPct:number,fadeOutSec:number,fadeTotal:number,fadeScale:number,finalFadeIn:number,finalFadeOut:number}} |
| 11 | + */ |
| 12 | +export function computeSampleFadeParams(sourcePlayDuration, settings) { |
| 13 | + const safeDuration = Math.max(0.0001, Number(sourcePlayDuration || 0)); |
| 14 | + const fadeInPct = Math.max(0, Math.min(95, Number(settings?.fadeInPct || 0))); |
| 15 | + const fadeOutPct = Math.max(0, Math.min(95, Number(settings?.fadeOutPct || 0))); |
| 16 | + const fadeInSec = safeDuration * (fadeInPct / 100); |
| 17 | + const shapedFadeOutPct = Math.pow(fadeOutPct / 100, 0.7) * 100; |
| 18 | + const fadeOutSec = safeDuration * (shapedFadeOutPct / 100); |
| 19 | + const fadeTotal = fadeInSec + fadeOutSec; |
| 20 | + const fadeScale = |
| 21 | + fadeTotal > safeDuration * 0.98 |
| 22 | + ? (safeDuration * 0.98) / Math.max(0.0001, fadeTotal) |
| 23 | + : 1; |
| 24 | + |
| 25 | + return { |
| 26 | + fadeInSec, |
| 27 | + shapedFadeOutPct, |
| 28 | + fadeOutSec, |
| 29 | + fadeTotal, |
| 30 | + fadeScale, |
| 31 | + finalFadeIn: fadeInSec * fadeScale, |
| 32 | + finalFadeOut: fadeOutSec * fadeScale, |
| 33 | + }; |
| 34 | +} |
| 35 | + |
| 36 | +/** |
| 37 | + * Applies sample gain automation for fade-in and fade-out. This intentionally |
| 38 | + * mirrors the old sample-note node behavior while making it reusable by |
| 39 | + * playlist audio clips and offline render. |
| 40 | + * |
| 41 | + * @param {AudioParam} gainParam |
| 42 | + * @param {number} startTime |
| 43 | + * @param {number} sourcePlayDuration |
| 44 | + * @param {number} finalGain |
| 45 | + * @param {object} fadeParams |
| 46 | + * @param {object} [options] |
| 47 | + * @param {number} [options.retriggerFadeInSec] |
| 48 | + */ |
| 49 | +export function applySampleGainAutomation( |
| 50 | + gainParam, |
| 51 | + startTime, |
| 52 | + sourcePlayDuration, |
| 53 | + finalGain, |
| 54 | + fadeParams, |
| 55 | + options = {}, |
| 56 | +) { |
| 57 | + const safeDuration = Math.max(0.0001, Number(sourcePlayDuration || 0)); |
| 58 | + const stopAt = startTime + safeDuration; |
| 59 | + const finalFadeIn = Math.max(0, Number(fadeParams?.finalFadeIn || 0)); |
| 60 | + const finalFadeOut = Math.max(0, Number(fadeParams?.finalFadeOut || 0)); |
| 61 | + const fadeOutStart = Math.max(startTime, stopAt - finalFadeOut); |
| 62 | + const fadeIn = |
| 63 | + Number(options.retriggerFadeInSec || 0) > 0.001 |
| 64 | + ? Math.max(finalFadeIn, Number(options.retriggerFadeInSec || 0)) |
| 65 | + : finalFadeIn; |
| 66 | + |
| 67 | + if (fadeIn > 0.001) { |
| 68 | + gainParam.setValueAtTime(MIN_AUDIO_GAIN, startTime); |
| 69 | + gainParam.linearRampToValueAtTime(finalGain, startTime + fadeIn); |
| 70 | + } else { |
| 71 | + gainParam.setValueAtTime(finalGain, startTime); |
| 72 | + } |
| 73 | + |
| 74 | + gainParam.setValueAtTime(finalGain, fadeOutStart); |
| 75 | + if (finalFadeOut > 0.001) { |
| 76 | + gainParam.exponentialRampToValueAtTime(MIN_AUDIO_GAIN, stopAt); |
| 77 | + } else { |
| 78 | + gainParam.setValueAtTime(MIN_AUDIO_GAIN, stopAt); |
| 79 | + } |
| 80 | +} |
0 commit comments