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 arequestAnimationFrameloop (~50 fps, ±20 ms accuracy) - WebAudioBackend: two
GainNoderamps scheduled withlinearRampToValueAtTime(sample-accurate)
When a crossfade begins:
- The backend loads the incoming track URL into a secondary slot via
loadSecondary(url) - The secondary is pre-rolled via
primeSecondary(startAt) crossfade(durationMs)ramps primary → 0 and secondary → volume simultaneously- At completion, secondary becomes primary; old primary is discarded
- The queue cursor advances and
currentevent fires
Configuration
Set defaults that apply when no per-call options are provided:
player.setup({
crossfadeDefaults: {
duration: 5, // seconds
curve: 'equal-power',
},
trackEndingSoonThreshold: 8, // fire trackEndingSoon 8s before end
});
Manual crossfade
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
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:
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
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:
player.setup({ backend: 'webaudio' });
Check the active backend by .kind, not by string comparison with the return value of player.backend():
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
- Crossfade API:
crossfadeTo(),isTransitioning(),CrossfadeOptions - AutoAdvancePlugin:
crossfade,crossfadeDurationoptions - Events:
crossfadeStart,crossfadeComplete,trackEndingSoon - Advanced: Crossfade Tuning: duration, curve, and timing guide
- Recipes: Crossfade and Gapless: practical walkthrough