Skip to content

Crossfade

Crossfade transitions smoothly between two tracks by fading one out while fading the other in, using a dual-buffer architecture in both built-in backends.

For the full method reference see Crossfade Methods.

How it works

Both backends support crossfade:

  • AudioElementBackend: two <audio> elements; gain ramps driven by a requestAnimationFrame loop (~50 fps, ±20 ms accuracy)
  • WebAudioBackend: two GainNode ramps scheduled with linearRampToValueAtTime (sample-accurate)

When a crossfade begins:

  1. The backend loads the incoming track URL into a secondary slot via loadSecondary(url)
  2. The secondary is pre-rolled via primeSecondary(startAt)
  3. crossfade(durationMs) ramps primary → 0 and secondary → volume simultaneously
  4. At completion, secondary becomes primary; old primary is discarded
  5. The queue cursor advances and current event fires

Configuration

Set defaults that apply when no per-call options are provided:

TypeScript
player.setup({
crossfadeDefaults: {
duration: 5, // seconds
curve: 'equal-power',
},
trackEndingSoonThreshold: 8, // fire trackEndingSoon 8s before end
});

Manual crossfade

TypeScript
import type { MusicPlaylistItem } from '@nomercy-entertainment/nomercy-music-player';

async function playWithFade(track: MusicPlaylistItem): Promise<void> {
if (player.isTransitioning()) return;

await player.crossfadeTo(track, {
duration: 5, // seconds
});
// A manual crossfadeTo() always uses a linear gain ramp. The fade curve
// (default 'equal-power') applies only to the automatic AutoAdvance transition.
}

Automatic crossfade via AutoAdvancePlugin

TypeScript
import { AutoAdvancePlugin } from '@nomercy-entertainment/nomercy-music-player/plugins';

player.addPlugin(AutoAdvancePlugin, {
crossfade: true, // hand off to crossfadeTo on trackEndingSoon
crossfadeDuration: 5, // seconds, must be ≤ trackEndingSoonThreshold
});

AutoAdvancePlugin calls player.crossfadeTo(next, { duration: crossfadeDuration }) when trackEndingSoon fires.

Server-triggered crossfade

For radio mode, group listening, or any server-orchestrated transition:

TypeScript
signalRConnection.on('CrossfadeNow', ({ nextTrackId, durationSeconds }) => {
const queue = player.queue();
const track = queue.find((item) => item.id === nextTrackId);
if (track && !player.isTransitioning()) {
void player.crossfadeTo(track, { duration: durationSeconds });
}
});

Monitoring transitions

TypeScript
player.on('crossfadeStart', ({ from, to, duration }) => {
// duration is in milliseconds here (converted from seconds in CrossfadeOptions)
console.log(`Fading "${from?.name}" → "${to.name}" over ${duration}ms`);
});

player.on('crossfadeComplete', ({ track }) => {
console.log(`Now playing: ${track.name}`);
});

// Poll mid-transition:
if (player.isTransitioning()) {
showFadeIndicator();
}

Backend and accuracy

Crossfade works with both backends. For sample-accurate transitions, use webaudio:

TypeScript
player.setup({ backend: 'webaudio' });

Check the active backend by .kind, not by string comparison with the return value of player.backend():

TypeScript
const backend = player.backend();
if (backend.kind === 'webaudio') {
console.log('Sample-accurate crossfade available');
} else {
console.log('RAF-based crossfade (~50 fps)');
}

EQ through crossfade

During a crossfade only the outgoing track is routed through the EQ graph; the incoming track plays un-equalized until it becomes the primary at the end of the fade, after which EQ applies to it as normal.

See also