
Hacker News · Mar 2, 2026 · Collected from RSS
Hey HN, I just released v4 of my gapless playback library that I first built in 2017 for https://relisten.net. We stream concert recordings, where gapless playback is paramount. It's built from scratch, backed by a rigid state machine (the sole dependency is xstate) and is already running in production over at Relisten. The way it works is by preloading future tracks as raw buffers and scheduling them via the web audio API. It seamlessly transitions between HTML5 and web audio. We've used this technique for the last 9 years and it works fairly well. Occasionally it will blip slightly from HTML5->web audio, but there's not much to be done to avoid that (just when to do it - lotta nuance here). Once you get on web audio, everything should be clean. Unfortunately web audio support still lacks on mobile, in which case you can just disable web audio and it'll fallback to full HTML5 playback (sans gapless). But if you drive a largely desktop experience, this is fine. On mobile, most people use our native app. You can view a demo of the project at https://gapless.saewitz.com - just click on "Scarlet Begonias", seek halfway in the track (as it won't preload until >15s) and wait for "decoding" on "Fire on the Mountain" to switch to "ready". Then tap "skip to -2s and hear the buttery smooth segue. Comments URL: https://news.ycombinator.com/item?id=47222271 Points: 10 # Comments: 4
gapless.js Gapless audio player for the web. Takes an array of audio tracks and uses HTML5 audio with the Web Audio API to enable seamless, gapless transitions between tracks. Though the earnest goal is not bundle-size driven, it has only one production dependency (xstate) so it operates in a rigid manner according to a well-designed state machine. It has a dead simple API and is easy to get up and running. Built for Relisten.net, where playing back gapless live tracks is paramount. Live Demo Install pnpm install gapless Quick Start import Queue from 'gapless'; const player = new Queue({ tracks: [ 'https://example.com/track1.mp3', 'https://example.com/track2.mp3', 'https://example.com/track3.mp3', ], onProgress: (track) => { console.log(`${track.currentTime} / ${track.duration}`); }, onEnded: () => { console.log('Queue finished'); }, }); player.play(); API Constructor Options (GaplessOptions) const player = new Queue({ tracks: [], // Initial list of track URLs onProgress: (info) => {}, // Called at ~60fps while playing onEnded: () => {}, // Called when the last track ends onPlayNextTrack: (info) => {}, // Called when advancing to next track onPlayPreviousTrack: (info) => {},// Called when going to previous track onStartNewTrack: (info) => {}, // Called whenever a new track becomes current onError: (error) => {}, // Called on audio errors onPlayBlocked: () => {}, // Called when autoplay is blocked by the browser onDebug: (msg) => {}, // Internal debug messages (development only) webAudioIsDisabled: false, // Disable Web Audio API (disables gapless playback) trackMetadata: [], // Per-track metadata (aligned by index) volume: 1, // Initial volume, 0.0–1.0 }); Methods Method Description play() Start or resume playback pause() Pause playback togglePlayPause() Toggle between play and pause next() Advance to the next track previous() Go to previous track (restarts current track if > 8s in) gotoTrack(index, playImmediately?) Jump to a track by index seek(time) Seek to a position in seconds setVolume(volume) Set volume (0.0–1.0) addTrack(url, options?) Add a track to the end of the queue removeTrack(index) Remove a track by index resumeAudioContext() Resume the AudioContext (for browsers that require user gesture) destroy() Clean up all resources Getters Getter Type Description currentTrack TrackInfo | undefined Snapshot of the current track currentTrackIndex number Index of the current track tracks readonly TrackInfo[] Snapshot of all tracks isPlaying boolean Whether the queue is playing isPaused boolean Whether the queue is paused volume number Current volume TrackInfo All callbacks and getters return TrackInfo objects — plain data snapshots with no methods: interface TrackInfo { index: number; // Position in the queue currentTime: number; // Playback position in seconds duration: number; // Total duration (NaN until loaded) isPlaying: boolean; isPaused: boolean; volume: number; trackUrl: string; // Resolved audio URL playbackType: 'HTML5' | 'WEBAUDIO'; webAudioLoadingState: 'NONE' | 'LOADING' | 'LOADED' | 'ERROR'; metadata?: TrackMetadata; machineState: string; // Internal state machine state } AddTrackOptions player.addTrack('https://example.com/track.mp3', { skipHEAD: true, // Skip HEAD request for URL resolution metadata: { title: 'Track Title', artist: 'Artist', album: 'Album', artwork: [{ src: 'https://example.com/art.jpg', sizes: '512x512', type: 'image/jpeg' }], }, }); TrackMetadata Metadata is used for the Media Session API (lock screen controls, browser media UI) and can contain arbitrary additional fields: interface TrackMetadata { title?: string; artist?: string; album?: string; artwork?: MediaImage[]; [key: string]: unknown; } Migration from v3 v4 is a complete rewrite. The public API has changed: v3 v4 import GaplessQueue from 'gapless.js' import Queue from 'gapless' (or import { Queue }) player.playNext() player.next() player.playPrevious() player.previous() player.resetCurrentTrack() player.seek(0) player.disableWebAudio() Pass webAudioIsDisabled: true in constructor player.nextTrack player.tracks[player.currentTrackIndex + 1] track.completeState Callbacks now receive TrackInfo objects Callbacks receive Track instances Callbacks receive plain TrackInfo data snapshots Key differences State machines: Internally uses XState for queue and track state management. XState is bundled — no extra dependency needed. ESM only: Published as ES module only. No CommonJS build. TrackInfo: All callbacks and getters return plain data objects (TrackInfo) instead of Track class instances. Media Session: Built-in support for the Media Session API via trackMetadata. Volume: Volume is now set via setVolume(n) and readable via the volume getter. License MIT