Skip to content

Crossfade and Gapless

Smooth music transitions: a volume-faded crossfade or a preloaded gapless handoff. This applies to the music player only.

Prerequisites: Quick Start. For the full crossfade API see Crossfade.

Crossfade vs gapless, when to choose

FeatureCrossfadeGapless
OverlapBoth tracks play simultaneouslyTracks play sequentially
VolumeOutgoing fades down, incoming fades upConstant
Use caseAlbum transitions, radio, DJ-style mixesContinuous music (prog rock, live albums)
ConfigcrossfadeDefaults.duration + AutoAdvancePluginOmit crossfade; AutoAdvancePlugin preloads next

Enabling crossfade

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

const player = nmMPlayer('main')
.addPlugin(AutoAdvancePlugin, {
crossfade: true, // hand off to crossfadeTo on trackEndingSoon
crossfadeDuration: 5, // seconds
})
.setup({
baseUrl: 'https://raw.githubusercontent.com/NoMercy-Entertainment/nomercy-media/master/Music',
crossfadeDefaults: {
duration: 5, // seconds
curve: 'equal-power',
},
trackEndingSoonThreshold: 8, // fire trackEndingSoon with 8s remaining
playlist: [
{
id: 'kjc-01',
name: 'Thaw You Out',
url: '/D/Derek%20Clegg/%5B2010%5D%20KJC/01%20Thaw%20You%20Out.mp3',
artist: 'Derek Clegg',
},
{
id: 'kjc-02',
name: 'Hope',
url: '/D/Derek%20Clegg/%5B2010%5D%20KJC/02%20Hope.mp3',
artist: 'Derek Clegg',
},
],
});

player.on('ready', () => {
player.item(0, { autoplay: true });
});

AutoAdvancePlugin with crossfade: true calls player.crossfadeTo(next, { duration: crossfadeDuration }) when trackEndingSoon fires. Set crossfadeDuration to match or be less than trackEndingSoonThreshold.

Crossfade curve comparison

'equal-power' keeps perceived loudness stable:

Code
equal-power:
outgoing gain: cos(t × π/2)
incoming gain: sin(t × π/2)
sum: constant at 1.0 throughout

linear:
outgoing gain: 1 − t
incoming gain: t
sum: dips to ~0.71 at t = 0.5

Use 'equal-power' for all standard music. 'linear' creates an audible dip at the midpoint, which is almost never what you want.

Manual crossfade trigger

To crossfade on an explicit user action (next button, click on a playlist item):

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

async function crossfadeToTrack(nextTrack: MusicPlaylistItem): Promise<void> {
if (player.isTransitioning()) return; // ignore if already fading

await player.crossfadeTo(nextTrack, {
duration: 3,
curve: 'equal-power',
});
}

Nested crossfades are rejected by the player, so crossfadeTo is a no-op when isTransitioning() returns true.

Monitoring crossfade progress

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

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

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

Gapless playback

Gapless preloads the next track so it can begin immediately when the current one ends:

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

const player = nmMPlayer('main')
.addPlugin(AutoAdvancePlugin, {
preloadNextOnEnding: true, // preload next track on trackEndingSoon
crossfade: false, // no volume fade, hard cut
})
.setup({
trackEndingSoonThreshold: 30, // preload 30 s before end
playlist: [track1, track2, track3],
});

AutoAdvancePlugin with preloadNextOnEnding: true calls player.load(next, { slot: 'next' }) when trackEndingSoon fires, so the next track’s audio is buffered before ended.

Backend choice and accuracy

BackendCrossfade mechanismGapless accuracy
audio-elementRAF loop (~50 fps, ±20 ms)Browser-dependent. Chrome/Safari good, Firefox may gap
webaudiolinearRampToValueAtTime (sample-accurate)Sample-accurate on all browsers with Web Audio

Switch to webaudio for production DJ-style crossfades:

TypeScript
player.setup({
backend: 'webaudio',
crossfadeDefaults: { duration: 6, curve: 'equal-power' },
});

Timing pitfalls

The crossfade duration (seconds) must be less than or equal to trackEndingSoonThreshold:

TypeScript
// Correct: threshold gives 8s to run a 5s fade:
trackEndingSoonThreshold: 8,
crossfadeDefaults: { duration: 5 }

// Wrong: 6s fade but only 3s warning; fade clips at end:
trackEndingSoonThreshold: 3,
crossfadeDefaults: { duration: 6 }

See also