Core Player
[1.17.0] — 2026-07-01
Section titled “[1.17.0] — 2026-07-01”- Seekbar drag-to-scrub. Press and drag on the waveform/seekbar to scrub; the seek commits on release, so audio keeps playing during the drag instead of re-seeking on every move.
- Opt-in circular seek handle on the
seekbarstyle —seekHandle(defaultfalse, alsodata-seek-handle). A draggable handle that expands on hover; the bar turns it on.
Changed
Section titled “Changed”- Hardened peak extraction. Replaced
Math.max(...peaks)with a loop-based max (noRangeErroron very large arrays) and centralised the default sample count in a sharedDEFAULT_SAMPLES(1800) constant so live extraction andDEFAULT_OPTIONScan’t drift. - Playback-speed menu accessibility. The speed toggle now sets
aria-haspopup/aria-expanded(so the open/closed state is announced), closes onEscape, and returns focus to the trigger after a rate is chosen. Kept as a lightweight disclosure — the options remain plain buttons in the tab order, no ARIA-menu machinery.
- Activating a control no longer steals keyboard focus onto the player wrapper. Clicking/activating the play button — or any interactive control (slider, link, input) — now keeps focus on that control; the container only takes focus when the click lands on a non-interactive area. (Reported in #10.)
[1.16.1] — 2026-06-30
Section titled “[1.16.1] — 2026-06-30”Changed
Section titled “Changed”- Active-marker label now flashes then fades instead of staying visible the
whole time the playhead is past a marker. When the playhead reaches a marker
its label appears for ~2.5s and then fades out, while the highlight stays.
Driven by a
.show-labelclass the player adds/removes, so it’s CSS-overridable: keep it on with.waveform-marker.active .waveform-marker-tooltip { opacity: 1 }or hide it with{ opacity: 0 !important }.
[1.16.0] — 2026-06-30
Section titled “[1.16.0] — 2026-06-30”- Hover-time tooltip. With
showHoverTime: true(data-show-hover-time), a tooltip follows the pointer over the waveform showing the time at that position. The option was previously parsed but did nothing — it’s now wired up (works in both self and external modes). - Active markers. As playback passes a marker, the player highlights it and
reveals its label automatically — it drives the existing
setActiveMarker()from the progress loop (self and external modes), so chaptered tracks and DJ cues light up as you reach them. - Artwork fallback. When the
artworkimage fails to load (404 / broken URL), the player now shows a muted music-note placeholder tile instead of the browser’s broken-image icon.
index.d.ts@defaultJSDoc synced with the runtime. The hand-written type file still annotatedheightas@default 60andbarRadiusas@default 0;DEFAULT_OPTIONSis64and1. Corrected so IDE tooltips match actual behaviour. Documentation-only — no type or runtime change.
[1.15.0] — 2026-06-30
Section titled “[1.15.0] — 2026-06-30”- Live-decoded waveforms are now accurate.
extractPeaksinspected only ~1 in 10 frames per window (a real-time speed shortcut), which missed transients and made the waveform shape shift noticeably when the sample count changed. It now scans every frame — matching WaveformGen’s offline output exactly (same shape, not just same amplitude), so a live decode and a pre-generated.jsonrender identically.decodeAudioDatadominates the cost, so the full scan is effectively free.
Changed
Section titled “Changed”samplesdefault raised 256 → 1800 (the SoundCloud / WaveformGen figure). It is the source peak resolution for live decode only (ignored whenwaveformpeaks are supplied), resampled down to the visible bar count. At 256, any waveform wider than ~256 bars — common on wide or high-DPI displays — was upsampled and looked blurry; 1800 keeps it crisp. Paired with the every-frame scan it costs no extra extraction time. Override withsamples/data-samples.
[1.14.0] — 2026-06-28
Section titled “[1.14.0] — 2026-06-28”buttonSizeoption (+data-button-size) to size the play/pause button. A number is treated as px; a string (e.g.'4rem') is used verbatim. It sets a new--wfp-btn-sizeCSS variable that scales both button styles —circleandminimal, box and glyph — proportionally from a single value, so there are no per-style magic numbers left to drift. The default reproduces the prior 36px sizing exactly. Forwarded by the Astro/React wrappers.
- Minimal play button glyph size now actually applies.
.waveform-btn svg(16px) had equal specificity but later source order than the minimal-button rule, so it silently won — the glyph was pinned at 16px no matter what the minimal rule said (1.13.0/1.13.1 changes had no visible effect). The rule is now.waveform-btn.waveform-btn-minimal svg(higher specificity), and both button styles derive their size from--wfp-btn-size.
[1.13.1] — 2026-06-28
Section titled “[1.13.1] — 2026-06-28”Changed
Section titled “Changed”- Minimal play button (
buttonStyle: 'minimal') now renders the bare play/pause glyph at the same visual size as the default circle button — the glyph was noticeably smaller than the ring it replaces. Larger fixed box (2.5rem) + 36px glyph; still a fixed width so the waveform never shifts on play/pause.
[1.13.0] — 2026-06-28
Section titled “[1.13.0] — 2026-06-28”- Live theme switching. Auto-themed players (no explicit
colorPresetor hand-set colours) now re-detect the page theme and redraw on a runtime light/dark switch — a class/attribute flip on<html>/<body>(Tailwinddark,data-theme,data-color-scheme) or an OSprefers-color-schemechange. Previously the palette was fixed at load, so a toggle left the player stuck in the old theme (invisible waveform/buttons). Event-driven (one sharedMutationObserver+matchMedia, no polling), with a publicrefreshTheme()to trigger it manually. Explicit presets/colours are never overridden.
[1.12.1] — 2026-06-28
Section titled “[1.12.1] — 2026-06-28”Changed
Section titled “Changed”- Larger
minimalbutton glyph. The bare play/pause glyph read too small; bumped the fixed button box to 2.25rem and the icon to 28px (still a fixed box, so it doesn’t shift the waveform on play/pause).
[1.12.0] — 2026-06-28
Section titled “[1.12.0] — 2026-06-28”bpmoption (data-bpm). Show a known BPM in the badge (withshowBPM) without decoding the audio — ideal when peaks are pre-generated but the tempo is known (e.g. sample-pack previews). A supplied value wins over detection.
- Minimal button no longer shifts the waveform on play/pause. The
buttonStyle: 'minimal'button was auto-width, so swapping the play glyph (optically nudged 1px) for the pause glyph changed its width — moving the adjacent waveform and re-sampling its bars on every toggle. It’s now a fixed box, so the canvas stays put.
[1.11.0] — 2026-06-28
Section titled “[1.11.0] — 2026-06-28”buttonStyleoption ('circle' | 'minimal', plusdata-button-style).'minimal'renders a bare play/pause glyph with no circle — the look sample-pack and beat stores use in their preview grids. Composes with thepreviewlayout.
Changed
Section titled “Changed”- Default look refresh. The
mirrorstyle now usesbarSpacing: 2(distinct thin bars rather than a solid fill); the defaultbarRadiusis1(soft caps), defaultsamplesis256(more source fidelity — resampled to fit the bar pitch), and defaultheightis64. Players that set these explicitly are unaffected.
[1.10.0] — 2026-06-28
Section titled “[1.10.0] — 2026-06-28”previewlayout (layout: 'preview'/data-layout="preview") — centers the title under the waveform and trims the meta row, for sample-pack previews and dense grids.
Changed
Section titled “Changed”- Monochrome by default (shadcn). The keyboard-focus ring and speed-menu
active state are now neutral, driven by a new
--wfp-accentCSS variable (default zinc). Set--wfp-accentto a brand hue to re-tint.
[1.9.0] — 2026-06-28
Section titled “[1.9.0] — 2026-06-28”WaveformPlayer.utils.parseDataAttributesexposed on the utils bridge, so wrappers (waveform-bar, waveform-playlist) can read the player’s fulldata-*option surface off a host element without re-implementing — and drifting from — the contract.
[1.8.0] — 2026-06-27
Section titled “[1.8.0] — 2026-06-27”-
TypeScript definitions — the package now ships a hand-authored
index.d.ts(wired through atypesexport condition), so bundler/IDE users get full IntelliSense and type-checking with zero runtime bytes. It is the single source of truth for the option surface, theWaveformPlayerclass API, and a typed custom-event map (addEventListener('waveformplayer:timeupdate', …)now typese.detail). The React/Astro wrappers can re-export from it instead of re-declaring the option list. -
Dual ESM + CommonJS build — added a
dist/waveform-player.cjsbundle and arequireexport condition, sorequire('@arraypress/waveform-player')works under Node CJS (previously ESM-only). External sourcemaps now ship for the ESM, CJS, and minified IIFE bundles. New./styles.cssexport alias. -
Accessibility polish (near-zero footprint): a
@media (prefers-reduced-motion: reduce)guard that neutralizes transitions/animations,role="alert"on the error node, andaria-busytoggled on the seek slider while loading. -
CSS custom properties for spacing —
--waveform-line-height(1.4),--waveform-body-gap(8px), and--waveform-track-gap(12px). Lets embedders (e.g.@arraypress/waveform-bar) retune layout without!importantoverrides of internal classes. Defaults are unchanged. -
Expressive bar styling (bundle-neutral):
barRadiusfor rounded bar caps (bars/mirror; falls back to square whereroundRectis unavailable), and gradient fills —waveformColor/progressColornow also accept an array of CSS colour stops (e.g.['#fafafa', '#71717a']) rendered as a vertical canvas gradient. Both work via constructor options,data-*attributes (gradients as a JSON array), and the React/Astro wrappers. -
Lifecycle + event-contract completeness (so controllers/analytics stop reaching into internals):
waveformplayer:destroyevent — the symmetric counterpart to:ready; lets listeners release references on teardown.loadTrack(url, title, subtitle, { autoplay: false })— load / restore / enqueue without forcing playback.waveformplayer:endednow carries{ currentTime, duration }, and is synthesized inexternalmode when progress reaches the end (fires once).request-play/request-pause/request-seekdetail now includesmarkersandwaveform, so controllers don’t re-fetch them.parseDataAttributesnow readsdata-audio-mode,data-show-markers,data-accessible-seek,data-seek-label,data-play-icon,data-pause-icon(previously silently inert on every auto-init path).- Shorthand aliases —
style/data-styleforwaveformStyle, andsrc/data-srcforurl(e.g.<div data-waveform-player data-src="t.mp3" data-style="bars">). The canonical names still work and win if both are set.
-
Controller-support helpers (so
@arraypress/waveform-barand other external controllers stop reaching into internals / shipping divergent copies):player.setActiveMarker(index | null)— highlight the current marker via a core-owned.waveform-marker.activeclass instead of poking the marker DOM.WaveformPlayer.utils— a static bridge exposingformatTime,extractTitleFromUrl,escapeHtml, andisSafeHref(allow-listshttp/https/relative URLs; rejectsjavascript:/data:script schemes).setVolume()now coerces + guards non-finite input (no moreNaNreachingaudio.volume).- The
request-*event detail’sartistnow falls back tosubtitle, so the published contract is self-consistent.
-
errorTextoption (default'Unable to load audio') — customize/localize the message shown when audio fails to load. Escaped before render. Also settable viadata-error-text.
Fixed (additional)
Section titled “Fixed (additional)”- Type hygiene in
audio.js: cast thewebkitAudioContextfallback (silences ts2568) and dropped a no-opawaiton the synchronousdetectBPM().
- AudioContext leak on failed decode —
generateWaveform()now closes the context in afinallyblock. Browsers hard-cap live AudioContexts (~6 in Chrome), so leaking one per failed load could break every later player on a catalogue page. destroy()listener leak — all document/container/seek listeners are now registered against anAbortControllerand torn down ondestroy(). The old teardown left the outside-click and container listeners attached.- Constructor crash in external mode —
audioMode: 'external'combined withshowPlaybackSpeed: truethrew on init (dereferencing the null<audio>);updateSpeedUI()now no-ops without audio. onTimeUpdateargument order — external mode previously fired(player, currentTime, duration); it now matches self mode’s documented(currentTime, duration, player)so one handler works in both modes. ⚠️ Behavior change for external-mode consumers relying on the old order.- Markers in external mode —
renderMarkers()now uses a mode-agnostic duration, so chapter markers render when audio is delegated. - Accessible seek in external mode — the duration is now published
unconditionally, so keyboard seeking / the ARIA slider work even when
showTimeis off. generateId()— hashes the full URL (+ a counter) instead of a 10-charbtoa()prefix: no more collisions for same-host tracks and no throw on non-Latin1 / Unicode URLs.formatTime()— addsH:MM:SSrollover past one hour and clamps negatives (also fixesaria-valuetext).- Autoplay — the autoplay
play()no longer emits an unhandled promise rejection when the browser blocks it.
[1.7.2] — 2026-06-27
Section titled “[1.7.2] — 2026-06-27”-
Accessible seek slider — the waveform surface (
.waveform-container) is now exposed as a keyboard-operable ARIA slider:role="slider", focusable in the tab order, witharia-valuemin/aria-valuemax/aria-valuenowand a readablearia-valuetext(e.g."0:30 of 2:00") kept in sync on metadata load,timeupdate, and externalsetProgress(). When focused it handles the standard slider keys — ←/↓ and →/↑ (±5s), Page Up/Page Down (±10s), Home/End — callingpreventDefault()(no page scroll). Works in bothself(callsseekTo()) andexternalmode (dispatcheswaveformplayer:request-seek, exactly like click-to-seek). Addresses WCAG 2.1.1 (Keyboard) and 4.1.2 (Name, Role, Value). Resolves #8; upstreamed from WordPress/Gutenberg’s external accessibility layer (#9).Two new options:
accessibleSeek(boolean, defaulttrue— setfalseto opt out and keep the prior markup) andseekLabel(string, defaultnull— accessible name for the slider, falls back to the track title, then'Seek').The existing container-level keyboard shortcuts (number-key seek, space, volume on ↑/↓, mute) are untouched; they live on the outer container and only run when it is the active element, so they don’t collide with the slider on the inner element.
[1.7.1] — Unreleased
Section titled “[1.7.1] — Unreleased”-
package.jsonexports field — added"type": "module", pointedmainatdist/waveform-player.esm.js(was IIFE bundle with no exports), and added a properexportsmap so SSR / Node consumers canimport { WaveformPlayer } from '@arraypress/waveform-player'without “does not provide an export named ‘WaveformPlayer’” errors.Browser direct-script usage via the
unpkgfield ordist/waveform-player.js?urlimports is unaffected — the IIFE bundle stays on disk for<script>tag and Vite asset-URL loading.
[1.7.0] — 2026-05-23
Section titled “[1.7.0] — 2026-05-23”-
WaveformPlayer.getPeaksUrl(audioUrl)— static helper that derives a peaks-JSON URL from an audio URL by swapping the extension (.mp3 / .wav / .ogg / .flac / .m4a / .aac→.json). Pair with@arraypress/waveform-gento pre-generate peaks at build time and skip the Web Audio decode pass at runtime — big perf win on catalogues with many tracks.Preserves query strings and URL fragments. Returns
undefinedfor empty input or unrecognised extensions so callers can pass through unconditionally:new WaveformPlayer('#el', {url: track.audioUrl,waveform: WaveformPlayer.getPeaksUrl(track.audioUrl),});
[1.6.0] - 2026-05-13
Section titled “[1.6.0] - 2026-05-13”New Features
Section titled “New Features”-
External audio mode (
audioMode: 'external') — turns a WaveformPlayer instance into a visualization-only surface that delegates playback to an external controller (e.g.@arraypress/waveform-bar). The player renders the canvas + scrubber + play button as usual, but skips creating its own<audio>element. Play / pause / seek interactions dispatch cancelablewaveformplayer:request-play,waveformplayer:request-pause, andwaveformplayer:request-seekevents on the container; the controller listens and routes the action to its own audio source.Pair with WaveformBar 1.3+: any
[data-waveform-player][data-audio-mode="external"]element with a matching URL becomes a synced visual mirror of bar playback (canvas scrubs in real time, play/pause icon reflects bar state).<div data-waveform-playerdata-audio-mode="external"data-url="song.mp3"data-waveform-style="bars"></div>new WaveformPlayer(el, { audioMode: 'external' });
player.setPlayingState(playing)— external-state pump for the play/pause visual state. Toggles the play/pause icon, starts/stops the smooth-update RAF, dispatcheswaveformplayer:play/:pauseso existing listeners still fire.player.setProgress(currentTime, duration)— external-state pump for the scrubber + canvas + time displays. Drives the visualization from an external clock without touching audio.
Backward Compatibility
Section titled “Backward Compatibility”Fully additive. audioMode defaults to 'self' — every existing instance behaves exactly as before. The new event dispatchers and setPlayingState / setProgress methods only fire in external mode, so they can’t disturb self-mode callers.
[1.5.2] - 2026-03-22
Section titled “[1.5.2] - 2026-03-22”- JSON waveform files can now include
markers— automatically loaded if no markers are set via data attributes
[1.5.1] - 2026-03-22
Section titled “[1.5.1] - 2026-03-22”- Drawing style aliases:
bar,block,dotnow accepted alongsidebars,blocks,dotsforwaveformStyle
[1.5.0] - 2026-03-22
Section titled “[1.5.0] - 2026-03-22”New Features
Section titled “New Features”- JSON Waveform Loading —
data-waveformnow accepts a URL to a JSON file ending in.json- Fetches the file and reads peaks from the response (supports
[...]array or{ peaks: [...] }object) - Falls back gracefully if fetch fails (player generates waveform from audio as before)
- No changes to constructor,
load(), orloadTrack()— fully backwards compatible
- Fetches the file and reads peaks from the response (supports
<!-- JSON file instead of inline peaks --><div data-waveform-player data-url="song.mp3" data-waveform="waveforms/song.json"></div>
<!-- WaveformBar — same attribute --><div data-wb-play data-url="song.mp3" data-wb-waveform="waveforms/song.json"></div>
<!-- Inline peaks still work exactly as before --><div data-waveform-player data-url="song.mp3" data-waveform="[0.2, 0.37, 0.41, ...]"></div>[1.4.1] - 2026-03-22
Section titled “[1.4.1] - 2026-03-22”Reverted
Section titled “Reverted”- Reverted JSON config file feature (
data-config) introduced in 1.4.0 due to breaking changes with WaveformBar integration. The feature caused layout and sizing issues when the player was used inside WaveformBar. Will be revisited in a future release with proper integration testing.
[1.4.0] - 2026-03-22 [YANKED]
Section titled “[1.4.0] - 2026-03-22 [YANKED]”New Features
Section titled “New Features”- JSON Config Files — Load track configuration from external JSON files via
data-configattribute- Single attribute setup:
<div data-waveform-player data-config="waveforms/track.json"></div> - JSON supports
url,title,subtitle,artwork,album,samples,peaks,markers, andmeta - Priority order: JSON config (base) → data attributes (override) → JS options (override)
- Config files are cached in memory — subsequent loads of the same file are instant
- Works with
loadTrack()viaoptions.configfor dynamic track loading metaobject passes through for use by extensions (e.g. WaveformBar readsmeta.bpm,meta.key)
- Single attribute setup:
JSON Config Format
Section titled “JSON Config Format”{ "url": "audio/track.mp3", "title": "Track Title", "subtitle": "Artist Name", "artwork": "covers/artwork.webp", "samples": 200, "peaks": [ 0.2, 0.37, 0.41, ... ], "markers": [], "meta": { "bpm": "128", "key": "Am" }}Generate config files with @arraypress/waveform-gen:
npx @arraypress/waveform-gen ./audio/*.mp3 --output ./waveforms/[1.3.5] - 2026-03-17
Section titled “[1.3.5] - 2026-03-17”Bug Fixes
Section titled “Bug Fixes”- Fixed marker elements from previous track persisting when loading a new track without markers
[1.3.4] - 2026-03-17
Section titled “[1.3.4] - 2026-03-17”Bug Fixes
Section titled “Bug Fixes”- Fixed markers from previous track persisting when loading a new track without markers via
loadTrack()
[1.3.3] - 2026-03-17
Section titled “[1.3.3] - 2026-03-17”Bug Fixes
Section titled “Bug Fixes”- Removed inline
style.heighton canvas element that prevented it from filling available width in flex containers. Container div still controls height.
[1.3.2] - 2026-03-17
Section titled “[1.3.2] - 2026-03-17”Bug Fixes
Section titled “Bug Fixes”- Fixed waveform canvas not filling available width when embedded in flex containers (e.g. persistent bottom bars).
resizeCanvas()now reads width from the container div instead of the canvas element.
[1.3.1] - 2026-03-17
Section titled “[1.3.1] - 2026-03-17”Bug Fixes
Section titled “Bug Fixes”- Fixed uncaught
NotAllowedErrorwhenloadTrack()triggers autoplay before user interaction (e.g. on session restore). Theplay()promise returned since v1.2.2 was not being caught internally.
[1.3.0] - 2026-03-16
Section titled “[1.3.0] - 2026-03-16”Features
Section titled “Features”showControlsoption - Hide the play/pause button for custom UI implementations (data-show-controls="false")showInfooption - Hide the title, subtitle, time, and metadata bar (data-show-info="false")- Both options work via HTML data attributes or JavaScript API
- Waveform automatically fills the full width when controls are hidden
<!-- Waveform only, no button or info --><div data-waveform-player data-url="song.mp3" data-show-controls="false" data-show-info="false"></div>[1.2.2] - 2026-02-28
Section titled “[1.2.2] - 2026-02-28”Bug Fixes
Section titled “Bug Fixes”play()now returns the Promise fromHTMLMediaElement.play(), allowing callers to handle errors likeAbortError
Thanks to @scruffian for the contribution.
[1.2.1] - 2026-02-14
Section titled “[1.2.1] - 2026-02-14”Bug Fixes
Section titled “Bug Fixes”- Fixed null reference error when
destroy()is called during resize events - Cleaned up window resize listener on destroy to prevent memory leaks
- Added destruction guards to all event handlers to prevent race conditions
- Added
bubbles: trueto all custom events for better framework integration
Thanks to @scruffian for contributing these fixes.
[1.2.0] - 2025-10-19
Section titled “[1.2.0] - 2025-10-19”Features
Section titled “Features”- Automatic Theme Detection - Player now automatically adapts to your website’s color scheme
- Detects light/dark themes automatically
- Checks background brightness, theme classes, and system preferences
- Works seamlessly on WordPress, Shopify, and all platforms
- Override with explicit
data-color-preset="light"or"dark"if needed
[1.1.0] - 2025-09-15
Section titled “[1.1.0] - 2025-09-15”Features
Section titled “Features”- 6 visual styles: bars, mirror, line, blocks, dots, seekbar
- BPM detection
- Waveform caching with pre-generated data
- Keyboard controls
- Media Session API integration
- Speed control
- Chapter markers
- Dynamic track loading
[1.0.1] - 2025-09-15
Section titled “[1.0.1] - 2025-09-15”Bug Fixes
Section titled “Bug Fixes”- Initial patch release
[1.0.0] - 2025-09-15
Section titled “[1.0.0] - 2025-09-15”Initial Release
Section titled “Initial Release”- Zero-config audio player with waveform visualization
- HTML data attribute API
- JavaScript API
- ~8KB gzipped, zero dependencies
- Framework agnostic (React, Vue, Angular, vanilla JS)