diff --git a/eslint.config.js b/eslint.config.js index adf0e61..f9bfcbe 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -37,6 +37,7 @@ export default defineConfig([ // Variables 'no-var': 'error', 'prefer-const': 'error', + '@typescript-eslint/no-unused-vars': ['error', { varsIgnorePattern: '^_', argsIgnorePattern: '^_' }], // Naming conventions '@typescript-eslint/naming-convention': [ @@ -44,7 +45,7 @@ export default defineConfig([ { selector: 'default', format: ['camelCase'], - leadingUnderscore: 'forbid', + leadingUnderscore: 'allow', trailingUnderscore: 'forbid', }, { diff --git a/src/audio/PlaylistManager.ts b/src/audio/PlaylistManager.ts index eb01c90..033a53a 100644 --- a/src/audio/PlaylistManager.ts +++ b/src/audio/PlaylistManager.ts @@ -1,4 +1,5 @@ import type { Fade } from '../../types/Fade'; +import { FadeType } from '../../types/Fade'; import type { ID, Seconds, PlaylistEntry, Transition, ScheduledEntry } from './types'; /** @@ -118,7 +119,7 @@ export class PlaylistManager { if (i > 0) { const prevEntry = this.entries[i - 1]; const incomingTransition = this.getTransitionBetween(prevEntry.id, entry.id); - if (incomingTransition) { + if (incomingTransition && incomingTransition.fadeInType !== FadeType.NONE) { fades.push({ type: incomingTransition.fadeInType, startOffset: 0, @@ -133,7 +134,7 @@ export class PlaylistManager { if (i < this.entries.length - 1) { const nextEntry = this.entries[i + 1]; const outgoingTransition = this.getTransitionBetween(entry.id, nextEntry.id); - if (outgoingTransition) { + if (outgoingTransition && outgoingTransition.fadeOutType !== FadeType.NONE) { fades.push({ type: outgoingTransition.fadeOutType, startOffset: entry.duration - outgoingTransition.duration, diff --git a/src/audio/Scheduler.ts b/src/audio/Scheduler.ts index 10a89ff..f9ee640 100644 --- a/src/audio/Scheduler.ts +++ b/src/audio/Scheduler.ts @@ -11,6 +11,17 @@ const SCHEDULE_AHEAD_S: Seconds = 0.2; /** grace period, nodes within this window of ctx.currentTime are considered "audible" */ const AUDIBLE_GRACE_S = 0.05; +const EQUAL_POWER_CURVE_LENGTH = 128; + +// fade-out: cos(t * pi/2), 1 -> 0 +const EQUAL_POWER_FADE_OUT = new Float32Array(EQUAL_POWER_CURVE_LENGTH).map((_, i) => + Math.cos((i / (EQUAL_POWER_CURVE_LENGTH - 1)) * (Math.PI / 2)), +); +// fade-in: sin(t * pi/2), 0 -> 1 +const EQUAL_POWER_FADE_IN = new Float32Array(EQUAL_POWER_CURVE_LENGTH).map((_, i) => + Math.sin((i / (EQUAL_POWER_CURVE_LENGTH - 1)) * (Math.PI / 2)), +); + /** * Look-ahead scheduler * @@ -385,6 +396,13 @@ export class Scheduler { if (fade.type === FadeType.LINEAR) { gainNode.gain.linearRampToValueAtTime(Math.max(fade.endGain, 0.0001), fadeEndCtx); + } else if (fade.type === FadeType.EQUAL_POWER) { + const isFadeIn = fade.endGain > fade.startGain; + const fullCurve = isFadeIn ? EQUAL_POWER_FADE_IN : EQUAL_POWER_FADE_OUT; + // slice from the fraction we've already passed (mid-fade seek) + const startIdx = Math.round(fadeFraction * (EQUAL_POWER_CURVE_LENGTH - 1)); + const curve = startIdx > 0 ? fullCurve.slice(startIdx) : fullCurve; + gainNode.gain.setValueCurveAtTime(curve, fadeStartCtx, fadeEndCtx - fadeStartCtx); } else { // exponentialRamp can't reach 0, so we use an epsilon gainNode.gain.exponentialRampToValueAtTime(Math.max(fade.endGain, 0.0001), fadeEndCtx); diff --git a/types/Fade.ts b/types/Fade.ts index 6bd7e00..5e28ec7 100644 --- a/types/Fade.ts +++ b/types/Fade.ts @@ -1,8 +1,10 @@ import type {Seconds, Gain} from "./UtilTypes" export const FadeType = { - LINEAR: 0, - EXPONENTIAL: 1, + NONE: 0, + LINEAR: 1, + EXPONENTIAL: 2, + EQUAL_POWER: 3, } as const; export type FadeType = (typeof FadeType)[keyof typeof FadeType];