Architecture
The music player sits on top of the NoMercy Player Core (@nomercy-entertainment/nomercy-player-core).
The core owns:
- Transport lifecycle (play, pause, stop, seek)
- Queue and backlog management
- Plugin system and event bus
- Auth pipeline (Bearer tokens, request signing, 401 refresh)
- HLS stream registration
- Cue parser registry (LRC, WebVTT, custom)
- i18n
- Storage adapter interface
The music player adds on top of the core:
- Audio backends:
AudioElementBackend(default) andWebAudioBackend - Crossfade: dual-buffer crossfade with
equal-power/linearcurves - Music-typed events:
currentnarrowed toMusicPlaylistItem,trackEndingSoon,crossfadeStart,crossfadeComplete,repeat,shuffle,backend:changed - Music plugins:
LyricsPlugin,AutoAdvancePlugin,MediaSessionPlugin,CastSenderPlugin,KeyHandlerPlugin,MusicUiPlugin,TabLeaderPlugin - Music adapters:
IAudioBackend,ILyricSource,IScrobbler,INowPlayingArt,IPlaylistGenerator,ISimilarityEngine
Layer diagram
Your consumer app holds a NMMusicPlayer<T> instance, which is composed of:
| Part | What it provides |
|---|---|
| Audio backends | AudioElementBackend or WebAudioBackend |
| Music plugins | LyricsPlugin, AutoAdvancePlugin, and others |
| Core methods | transport, queue, auth, storage, and others |
| Core plugins | AudioGraphPlugin, EqualizerPlugin, and others |
Key design decisions
No UI in core. NMMusicPlayer renders nothing.
The MusicUiPlugin is optional and produces a configurable DOM overlay.
Consumers who want full control render their own UI from player events.
Generic playlist item. NMMusicPlayer<T>, where T defaults to MusicPlaylistItem but consumers extend it with their own fields without wrapping or adapting.
Plugin-everything. Lyrics, media session, cast, EQ, key handling are all plugins. Register only what you need. Plugins are removed from the bundle via tree-shaking when not imported.
Backend swappable at runtime. Switch between audio-element and webaudio with await player.backend('webaudio').
Custom backends inject via backendFactory.
What it is not
- Not a UI framework component (no Vue/React component, see Framework Integration)
- Not a video player (no subtitle tracks, no quality levels, see nomercy-video-player)
- Not server-side, runs in the browser only
Core component responsibilities
Three concerns share the work of playing audio:
Transport and queue — owned by the Player Core mixin. It drives the lifecycle state machine (idle, loading, playing, paused, ended), manages the ordered playlist and backlog, handles shuffle (Fisher-Yates) and repeat modes, and serialises every operation through the event bus so plugins see consistent state.
Audio backends — AudioElementBackend (default) wraps a single <audio> element and bridges its DOM events onto the player event bus. WebAudioBackend does the same but additionally opens an AudioContext, attaches a MediaElementAudioSourceNode, and exposes graph mount points (outputNode, analyserSource) for plugins. Both backends implement the same IAudioBackend contract so higher-level code never branches on which one is active.
Web Audio signal chain — assembled on demand by AudioGraphPlugin (a core plugin re-exported from the music package). Nothing allocates an AudioContext until that plugin is registered, so playback without visualisation or EQ has zero Web Audio overhead.
Web Audio signal chain
The baseline chain when only WebAudioBackend is active (no AudioGraphPlugin):
<audio> element
└─ MediaElementAudioSourceNode
└─ GainNode (volume, crossfade ramps)
└─ AnalyserNode (fftSize 2048 — tap point for visualisers)
└─ AudioContext.destination
When AudioGraphPlugin is registered, it takes ownership of the chain from the backend’s output node outward and manages insertion of effect nodes:
backend outputNode (GainNode)
├─ (parallel tap) → shared AnalyserNode (read by SpectrumPlugin)
├─ preEffects[0..n] (e.g. custom pre-processing nodes)
└─ postEffects[0..n] (e.g. EQ pre-gain GainNode → 10 BiquadFilterNodes, MixerPlugin gain/pan)
└─ AudioContext.destination
The AnalyserNode is wired in parallel to the source, not in series, so it reads the signal without interrupting the path to the speakers.
Note:
AudioContextstarts in'suspended'state per the browser autoplay policy.AudioGraphPluginresumes it automatically on the firstplayevent, which always fires inside a user-gesture callback.
Equalizer
EqualizerPlugin depends on AudioGraphPlugin and inserts a pre-gain GainNode followed by ten peaking BiquadFilterNodes as 'post' effects. The band centre frequencies are:
| Band | Frequency |
|---|---|
| 1 | 70 Hz |
| 2 | 180 Hz |
| 3 | 320 Hz |
| 4 | 600 Hz |
| 5 | 1,000 Hz |
| 6 | 3,000 Hz |
| 7 | 6,000 Hz |
| 8 | 12,000 Hz |
| 9 | 14,000 Hz |
| 10 | 16,000 Hz |
Band gains are in the range -12 dB to +12 dB. Index 0 is always the pre-gain pseudo-band (frequency: 'Pre'), which maps to a GainNode at unity when set to 0.
Registration order matters: AudioGraphPlugin must be added before EqualizerPlugin.
import nmMusicPlayer from '@nomercy-entertainment/nomercy-music-player';
import {
AudioGraphPlugin,
EqualizerPlugin,
SpectrumPlugin,
} from '@nomercy-entertainment/nomercy-music-player/plugins';
const player = nmMusicPlayer('my-player')
.setup({
baseUrl: 'https://protected.cdn.your-domain.com',
auth: { bearerToken: () => getToken() },
})
.addPlugin(AudioGraphPlugin)
.addPlugin(EqualizerPlugin, { persistKey: 'nm-eq' })
.addPlugin(SpectrumPlugin, { fftSize: 2048 });
const eq = player.getPlugin(EqualizerPlugin);
eq.preset('Rock');
eq.band({ frequency: 70, gain: 4 });
Spectrum analyser
SpectrumPlugin depends on AudioGraphPlugin and reads from the shared AnalyserNode on every requestAnimationFrame tick. Each tick it calls getByteFrequencyData and getByteTimeDomainData, computes coarse band energies (bass 20-250 Hz, mid 250-4000 Hz, treble 4000-16000 Hz), and emits a frame event carrying the full VisualizationFrame buffer.
Visualisation plugins (VisualizationPlugin subclasses) consume the frame event. For custom energy meters, bandEnergy(loHz, hiHz) returns a 0-1 normalised value for any arbitrary frequency range without subscribing to the full frame stream.
See also
- Audio Backend: the
IAudioBackendadapter and the two built-in implementations - Equalizer: using the EQ from your UI
- Crossfade: the transition engine
- Player Core: the shared infrastructure