@arraypress/waveform-player 1.11.0 → 1.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/waveform-player.cjs +8 -2
- package/dist/waveform-player.cjs.map +2 -2
- package/dist/waveform-player.css +1 -1
- package/dist/waveform-player.esm.js +3 -3
- package/dist/waveform-player.esm.js.map +3 -3
- package/dist/waveform-player.js +8 -2
- package/dist/waveform-player.min.js +3 -3
- package/dist/waveform-player.min.js.map +3 -3
- package/index.d.ts +2 -0
- package/package.json +1 -1
- package/src/css/waveform-player.css +9 -5
- package/src/js/core.js +9 -2
- package/src/js/themes.js +3 -0
- package/src/js/utils.js +1 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/js/utils.js", "../src/js/drawing.js", "../src/js/bpm.js", "../src/js/audio.js", "../src/js/themes.js", "../src/js/core.js", "../src/js/index.js"],
|
|
4
|
-
"sourcesContent": ["/**\n * @module utils\n * @description Utility functions for WaveformPlayer\n */\n\n/**\n * Escape a string for safe interpolation into HTML, preventing injection when\n * building markup with template strings. `null`/`undefined` become `''`.\n * @param {*} str - Value to escape.\n * @returns {string} HTML-escaped string.\n */\nexport function escapeHtml(str) {\n return String(str == null ? '' : str)\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n\n/**\n * Whether a URL is safe to navigate to (assign to `location.href`): allows only\n * `http`/`https` and relative URLs, rejecting `javascript:`, `data:`, `blob:`,\n * `vbscript:` and other script-bearing schemes.\n * @param {string} url - Candidate URL.\n * @returns {boolean} True if the URL uses a safe scheme.\n */\nexport function isSafeHref(url) {\n if (typeof url !== 'string' || url === '') return false;\n try {\n // Resolve relative URLs against a dummy http base; only the scheme matters.\n const u = new URL(url, 'http://localhost/');\n return u.protocol === 'http:' || u.protocol === 'https:';\n } catch (e) {\n return false;\n }\n}\n\n/**\n * Clamp a number to an inclusive range.\n * @param {number} value - Value to constrain.\n * @param {number} [min=0] - Lower bound.\n * @param {number} [max=1] - Upper bound.\n * @returns {number} `value` constrained to `[min, max]`.\n */\nexport function clamp(value, min = 0, max = 1) {\n return Math.max(min, Math.min(value, max));\n}\n\n/**\n * Read a boolean `data-*` flag. Returns `undefined` when the attribute is\n * absent (preserving the sparse-options contract) and otherwise compares the\n * raw value against the literal string `'true'`.\n * @param {string|undefined} value - Raw `dataset` value.\n * @returns {boolean|undefined} `true`/`false` when present, else `undefined`.\n */\nexport function parseBoolAttr(value) {\n return value === undefined ? undefined : value === 'true';\n}\n\n/**\n * A colour data-attribute may be a CSS colour string OR a JSON array of\n * gradient stops (e.g. '[\"#fafafa\",\"#71717a\"]'). Parse the array form;\n * otherwise pass the string straight through.\n * @param {string} value\n * @returns {string|string[]}\n */\nfunction parseColorValue(value) {\n if (typeof value === 'string' && value.trim().startsWith('[')) {\n try { return JSON.parse(value); } catch (e) { /* fall through to string */ }\n }\n return value;\n}\n\n/**\n * Read every recognised `data-*` attribute off a host element and translate it\n * into a plain options object suitable for `mergeOptions`.\n *\n * Only attributes that are actually present are copied, so the returned object\n * is sparse and never overrides defaults with `undefined`. Numeric attributes\n * are coerced with `parseInt`/`parseFloat`, boolean flags are compared against\n * the literal string `'true'`, and JSON-valued attributes (`markers`,\n * `playbackRates`) are parsed defensively \u2014 a parse failure is warned about and\n * the attribute is skipped rather than thrown.\n *\n * Several attributes are shorthand aliases of a canonical long form: `data-src`\n * \u2192 `url`, `data-style` \u2192 `waveformStyle`. When both are present the canonical\n * long form is applied last and therefore wins. `data-color` and `data-theme`\n * are retained as legacy aliases for `waveformColor` and `colorPreset`.\n * Colour attributes that accept gradients (`waveformColor`, `progressColor`)\n * are passed through {@link parseColorValue} so a JSON stop array is expanded.\n *\n * @param {HTMLElement} element - Host element whose `dataset` is inspected.\n * @returns {Object} Sparse options object containing only the attributes found.\n */\nexport function parseDataAttributes(element) {\n const options = {};\n\n // Set a boolean option only when its `data-*` attribute is present, so the\n // returned object stays sparse and never overrides a default with a value\n // the author didn't set. (`dataKey` differs from `optKey` only for showBPM.)\n const setBool = (optKey, dataKey = optKey) => {\n const v = parseBoolAttr(element.dataset[dataKey]);\n if (v !== undefined) options[optKey] = v;\n };\n\n // Read a present (non-empty) numeric attribute as an int (or float).\n const setNum = (optKey, dataKey = optKey, float = false) => {\n const raw = element.dataset[dataKey];\n if (raw) options[optKey] = float ? parseFloat(raw) : parseInt(raw, 10);\n };\n\n // Parse a JSON-valued attribute defensively \u2014 warn and skip on bad JSON.\n const setJson = (optKey, dataKey = optKey) => {\n const raw = element.dataset[dataKey];\n if (!raw) return;\n try { options[optKey] = JSON.parse(raw); }\n catch (e) { console.warn(`[WaveformPlayer] Invalid ${dataKey} JSON:`, e); }\n };\n\n // Core attributes. `data-src` is a shorthand alias for `data-url`;\n // the canonical long form wins if both are set.\n if (element.dataset.src) options.url = element.dataset.src;\n if (element.dataset.url) options.url = element.dataset.url;\n setNum('height');\n setNum('samples');\n if (element.dataset.preload) {\n options.preload = element.dataset.preload;\n }\n if (element.dataset.audioMode) options.audioMode = element.dataset.audioMode;\n\n // Waveform style attributes. `data-style` is a shorthand alias for\n // `data-waveform-style`; the canonical long form wins if both are set.\n if (element.dataset.style) options.waveformStyle = element.dataset.style;\n if (element.dataset.waveformStyle) options.waveformStyle = element.dataset.waveformStyle;\n setNum('barWidth');\n setNum('barSpacing');\n setNum('barRadius');\n if (element.dataset.buttonAlign) options.buttonAlign = element.dataset.buttonAlign;\n if (element.dataset.layout) options.layout = element.dataset.layout;\n if (element.dataset.buttonStyle) options.buttonStyle = element.dataset.buttonStyle;\n\n // Color preset\n if (element.dataset.colorPreset) options.colorPreset = element.dataset.colorPreset;\n\n // Individual color customization\n if (element.dataset.waveformColor) options.waveformColor = parseColorValue(element.dataset.waveformColor);\n if (element.dataset.progressColor) options.progressColor = parseColorValue(element.dataset.progressColor);\n if (element.dataset.buttonColor) options.buttonColor = element.dataset.buttonColor;\n if (element.dataset.buttonHoverColor) options.buttonHoverColor = element.dataset.buttonHoverColor;\n if (element.dataset.textColor) options.textColor = element.dataset.textColor;\n if (element.dataset.textSecondaryColor) options.textSecondaryColor = element.dataset.textSecondaryColor;\n if (element.dataset.backgroundColor) options.backgroundColor = element.dataset.backgroundColor;\n if (element.dataset.borderColor) options.borderColor = element.dataset.borderColor;\n\n // Legacy support for old attribute names\n if (element.dataset.color) options.waveformColor = element.dataset.color;\n if (element.dataset.theme) options.colorPreset = element.dataset.theme;\n\n // Feature flags\n setBool('autoplay');\n setBool('showControls');\n setBool('showInfo');\n setBool('showTime');\n setBool('showHoverTime');\n setBool('showBPM', 'showBpm');\n setBool('singlePlay');\n setBool('playOnSeek');\n\n // Content and metadata\n if (element.dataset.title) options.title = element.dataset.title;\n if (element.dataset.subtitle) options.subtitle = element.dataset.subtitle;\n if (element.dataset.album) options.album = element.dataset.album;\n if (element.dataset.artwork) options.artwork = element.dataset.artwork;\n\n // Waveform data\n if (element.dataset.waveform) options.waveform = element.dataset.waveform;\n\n // Markers\n setJson('markers');\n\n // Playback controls\n setNum('playbackRate', 'playbackRate', true);\n setBool('showPlaybackSpeed');\n setJson('playbackRates');\n\n // Media Session API\n setBool('enableMediaSession');\n\n // Markers visibility\n setBool('showMarkers');\n\n // Accessibility\n setBool('accessibleSeek');\n if (element.dataset.seekLabel) options.seekLabel = element.dataset.seekLabel;\n if (element.dataset.errorText) options.errorText = element.dataset.errorText;\n\n // Custom icons (raw SVG markup)\n if (element.dataset.playIcon) options.playIcon = element.dataset.playIcon;\n if (element.dataset.pauseIcon) options.pauseIcon = element.dataset.pauseIcon;\n\n return options;\n}\n\n/**\n * Format a duration as a clock string.\n *\n * Renders `M:SS` for durations under an hour and `H:MM:SS` for longer ones,\n * zero-padding the minutes and seconds. Falsy, `NaN`, or negative inputs are\n * treated as zero and return `'0:00'`.\n * @param {number} seconds - Time in seconds.\n * @returns {string} Formatted time, e.g. `'3:07'` or `'1:02:09'`.\n */\nexport function formatTime(seconds) {\n if (!seconds || isNaN(seconds) || seconds < 0) return '0:00';\n\n const hrs = Math.floor(seconds / 3600);\n const mins = Math.floor((seconds % 3600) / 60);\n const secs = Math.floor(seconds % 60);\n\n if (hrs > 0) {\n return `${hrs}:${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;\n }\n\n return `${mins}:${secs.toString().padStart(2, '0')}`;\n}\n\n/**\n * Monotonic per-process counter appended to every generated id to guarantee\n * uniqueness even when two ids hash from the same URL.\n * @type {number}\n * @private\n */\nlet idCounter = 0;\n\n/**\n * Generate a unique, DOM-safe ID from a URL.\n *\n * Uses a DJB2 hash of the FULL url (not a 10-char prefix) plus a process\n * counter, so same-host tracks don't collide in the instances map and\n * non-Latin1 / Unicode URLs don't throw (the old btoa() approach did both).\n * @param {string} url - Audio URL\n * @returns {string} Unique element-id-safe string\n */\nexport function generateId(url) {\n const str = url || 'audio';\n let hash = 5381;\n for (let i = 0; i < str.length; i++) {\n hash = ((hash << 5) + hash + str.charCodeAt(i)) | 0;\n }\n return `wp_${(hash >>> 0).toString(36)}_${(idCounter++).toString(36)}`;\n}\n\n/**\n * Derive a human-readable title from an audio URL's filename.\n *\n * Takes the last path segment, drops the extension, replaces `-`/`_`\n * separators with spaces, and title-cases the first letter of each word.\n * Returns `'Audio'` for an empty or missing URL.\n * @param {string} url - Audio URL.\n * @returns {string} Extracted, prettified title.\n * @example\n * extractTitleFromUrl('https://cdn.example.com/my-cool_track.mp3'); // 'My Cool Track'\n */\nexport function extractTitleFromUrl(url) {\n if (!url) return 'Audio';\n\n const parts = url.split('/');\n const filename = parts[parts.length - 1];\n const name = filename.split('.')[0];\n\n // Clean up common separators\n return name\n .replace(/[-_]/g, ' ')\n .replace(/\\b\\w/g, l => l.toUpperCase());\n}\n\n/**\n * Perceived brightness (0\u2013255) of a CSS colour, via the luminance formula.\n * Pulls the numeric channels out of an `rgb()`/`rgba()` string.\n * @param {string} color - CSS colour string, e.g. `\"rgb(34, 34, 34)\"`.\n * @returns {number|null} Brightness 0\u2013255, or `null` if it can't be parsed.\n */\nexport function perceivedBrightness(color) {\n const rgb = typeof color === 'string' ? color.match(/\\d+/g) : null;\n if (!rgb || rgb.length < 3) return null;\n const [r, g, b] = rgb.map(Number);\n return (r * 299 + g * 587 + b * 114) / 1000;\n}\n\n/**\n * Shallow-merge option objects into a new object, last source winning.\n *\n * Keys whose value is `null` or `undefined` are skipped, so a later source can\n * leave an earlier value untouched by passing a nullish entry rather than\n * clobbering it. The inputs are never mutated.\n * @param {...Object} sources - Option objects merged left-to-right.\n * @returns {Object} A fresh object containing the merged, defined keys.\n */\nexport function mergeOptions(...sources) {\n const result = {};\n\n for (const source of sources) {\n for (const key in source) {\n if (source[key] !== null && source[key] !== undefined) {\n result[key] = source[key];\n }\n }\n }\n\n return result;\n}\n\n/**\n * Wrap a function so it only runs once calls stop arriving for `wait` ms.\n *\n * Each invocation resets the pending timer, so rapid bursts collapse into a\n * single trailing-edge call that receives the most recent arguments. The\n * wrapper itself returns nothing.\n * @param {Function} func - Function to debounce.\n * @param {number} wait - Idle period in milliseconds before `func` fires.\n * @returns {Function} Debounced wrapper forwarding its arguments to `func`.\n */\nexport function debounce(func, wait) {\n let timeout;\n\n return function executedFunction(...args) {\n const later = () => {\n clearTimeout(timeout);\n func(...args);\n };\n\n clearTimeout(timeout);\n timeout = setTimeout(later, wait);\n };\n}\n\n/**\n * Resize a waveform amplitude array to a target number of bars.\n *\n * Returns the original array unchanged when lengths already match, and an empty\n * array when either side is empty. When upsampling (target larger than source)\n * it linearly interpolates between neighbouring samples for a smooth result.\n * When downsampling it splits the source into evenly sized buckets and keeps\n * the peak (maximum) of each so transients survive the reduction; an empty\n * bucket falls back to its nearest-neighbour sample.\n * @param {number[]} data - Original amplitude samples.\n * @param {number} targetLength - Desired number of output bars.\n * @returns {number[]} Resampled amplitude array of length `targetLength`.\n */\nexport function resampleData(data, targetLength) {\n if (data.length === targetLength) return data;\n if (data.length === 0 || targetLength === 0) return [];\n\n const result = [];\n\n // If upsampling (target is larger than source)\n if (targetLength > data.length) {\n const ratio = (data.length - 1) / (targetLength - 1);\n\n for (let i = 0; i < targetLength; i++) {\n const index = i * ratio;\n const lower = Math.floor(index);\n const upper = Math.ceil(index);\n const fraction = index - lower;\n\n // Linear interpolation between samples\n if (upper >= data.length) {\n result.push(data[data.length - 1]);\n } else if (lower === upper) {\n result.push(data[lower]);\n } else {\n const value = data[lower] * (1 - fraction) + data[upper] * fraction;\n result.push(value);\n }\n }\n } else {\n // Downsampling (target is smaller than source)\n const bucketSize = data.length / targetLength;\n\n for (let i = 0; i < targetLength; i++) {\n const start = Math.floor(i * bucketSize);\n const end = Math.floor((i + 1) * bucketSize);\n\n // Find the maximum value in this bucket\n let max = 0;\n let count = 0;\n\n for (let j = start; j <= end && j < data.length; j++) {\n if (data[j] > max) {\n max = data[j];\n }\n count++;\n }\n\n // If no samples were found in this bucket, use nearest neighbor\n if (count === 0) {\n const nearestIndex = Math.min(Math.round(i * bucketSize), data.length - 1);\n max = data[nearestIndex];\n }\n\n result.push(max);\n }\n }\n\n return result;\n}", "/**\n * @module drawing\n * @description Core waveform drawing styles optimized for visual distinction at all sizes\n */\n\nimport {resampleData, clamp} from './utils.js';\n\n/**\n * Resolve a fill value that may be a CSS colour string OR an array of colour\n * stops (rendered as a vertical canvas gradient). Bundle-light gradient\n * support: pass e.g. `waveformColor: ['#fafafa', '#71717a']`.\n * A single-element array collapses to that one colour; a multi-element array\n * is spread evenly from top (y=0) to bottom (y=height).\n * @private\n * @param {CanvasRenderingContext2D} ctx - Canvas context used to build the gradient.\n * @param {string|string[]} value - A CSS colour string, or an array of colour stops.\n * @param {number} height - Canvas height in device pixels (gradient span).\n * @returns {string|CanvasGradient} The original string, or a vertical linear gradient.\n */\nfunction makeFill(ctx, value, height) {\n if (!Array.isArray(value)) return value;\n if (value.length === 1) return value[0];\n const grad = ctx.createLinearGradient(0, 0, 0, height);\n value.forEach((c, i) => grad.addColorStop(i / (value.length - 1), c));\n return grad;\n}\n\n/**\n * Fill a bar rect, optionally with rounded caps (`barRadius`). Falls back to\n * a plain fillRect where `roundRect` is unavailable (older Safari) \u2014 square\n * bars, no error. Radii are clamped to half the rect's width/height so a\n * large `barRadius` never overflows a thin or short bar.\n * @private\n * @param {CanvasRenderingContext2D} ctx - Canvas context (current fillStyle is used).\n * @param {number} x - Left edge of the bar in device pixels.\n * @param {number} y - Top edge of the bar in device pixels.\n * @param {number} w - Bar width in device pixels.\n * @param {number} h - Bar height in device pixels (may be negative for upward fills).\n * @param {number|number[]} radii - Corner radius (number, or [tl, tr, br, bl]).\n * @returns {void}\n */\nfunction fillBar(ctx, x, y, w, h, radii) {\n const any = Array.isArray(radii) ? radii.some(r => r > 0) : radii > 0;\n if (any && typeof ctx.roundRect === 'function') {\n const max = Math.min(w / 2, Math.abs(h) / 2);\n const clampR = (r) => clamp(r, 0, max);\n ctx.beginPath();\n ctx.roundRect(x, y, w, h, Array.isArray(radii) ? radii.map(clampR) : clampR(radii));\n ctx.fill();\n } else {\n ctx.fillRect(x, y, w, h);\n }\n}\n\n/**\n * Scale the configured `barRadius` into device pixels (scalar).\n * @private\n * @param {Object} options - Drawing options (`barRadius` in CSS pixels, defaults to 0).\n * @param {number} dpr - Device pixel ratio multiplier.\n * @returns {number} The bar corner radius in device pixels.\n */\nfunction barRadiusPx(options, dpr) {\n return (options.barRadius || 0) * dpr;\n}\n\n/**\n * Top-rounded corner radii for bottom-anchored bars: [tl, tr, br, bl].\n * Only the top two corners are rounded so bars sit flush on the baseline.\n * @private\n * @param {Object} options - Drawing options (supplies `barRadius`).\n * @param {number} dpr - Device pixel ratio multiplier.\n * @returns {number[]} Corner radii in device pixels as [tl, tr, br, bl].\n */\nfunction barRadii(options, dpr) {\n const r = barRadiusPx(options, dpr);\n return [r, r, 0, 0];\n}\n\n/**\n * Trace a horizontal rounded-capsule (stadium) path from `startX` to `endX`,\n * ready to fill. The end caps are semicircles of radius `barHeight / 2`.\n * @private\n * @param {CanvasRenderingContext2D} ctx - Canvas context.\n * @param {number} startX - Left edge x (also the left cap centre).\n * @param {number} endX - Right edge x.\n * @param {number} centerY - Vertical centre of the capsule.\n * @param {number} barHeight - Capsule thickness in pixels.\n * @returns {void}\n */\nfunction capsulePath(ctx, startX, endX, centerY, barHeight) {\n const r = barHeight / 2;\n ctx.beginPath();\n ctx.moveTo(startX, centerY - r);\n ctx.lineTo(endX - r, centerY - r);\n ctx.arc(endX - r, centerY, r, -Math.PI / 2, Math.PI / 2);\n ctx.lineTo(startX, centerY + r);\n ctx.arc(startX, centerY, r, Math.PI / 2, -Math.PI / 2);\n ctx.closePath();\n}\n\n/**\n * Draw standard bars waveform - classic vertical bars anchored to the baseline.\n * Peaks are resampled to fit the available bar slots, drawn at 90% of canvas\n * height, then the progress portion is repainted in `progressColor` via a\n * left-anchored clip rect.\n * @param {CanvasRenderingContext2D} ctx - Canvas 2D context to draw into.\n * @param {HTMLCanvasElement} canvas - Canvas element (provides device-pixel dimensions).\n * @param {number[]} peaks - Normalised waveform peak values (0-1).\n * @param {number} progress - Playback progress (0-1) that drives the colour overlay.\n * @param {Object} options - Drawing options: `barWidth`, `barSpacing`, `barRadius`,\n * `color`, `progressColor` (colour strings or gradient stop arrays).\n * @returns {void}\n */\nexport function drawBars(ctx, canvas, peaks, progress, options) {\n const dpr = window.devicePixelRatio || 1;\n const barWidth = options.barWidth * dpr;\n const barSpacing = options.barSpacing * dpr;\n const barCount = Math.floor(canvas.width / (barWidth + barSpacing));\n const resampledPeaks = resampleData(peaks, barCount);\n const height = canvas.height;\n const progressWidth = progress * canvas.width;\n const radii = barRadii(options, dpr);\n const baseFill = makeFill(ctx, options.color, height);\n const progFill = makeFill(ctx, options.progressColor, height);\n\n ctx.clearRect(0, 0, canvas.width, canvas.height);\n\n // Draw all bars first\n ctx.fillStyle = baseFill;\n for (let i = 0; i < resampledPeaks.length; i++) {\n const x = i * (barWidth + barSpacing);\n if (x + barWidth > canvas.width) break;\n\n const peakHeight = resampledPeaks[i] * height * 0.9;\n // Draw from bottom up, not centered\n const y = height - peakHeight;\n\n fillBar(ctx, x, y, barWidth, peakHeight, radii);\n }\n\n // Progress overlay\n ctx.save();\n ctx.beginPath();\n ctx.rect(0, 0, progressWidth, height);\n ctx.clip();\n\n ctx.fillStyle = progFill;\n for (let i = 0; i < resampledPeaks.length; i++) {\n const x = i * (barWidth + barSpacing);\n if (x > progressWidth) break;\n\n const peakHeight = resampledPeaks[i] * height * 0.9;\n // Draw from bottom up, not centered\n const y = height - peakHeight;\n\n fillBar(ctx, x, y, barWidth, peakHeight, radii);\n }\n\n ctx.restore();\n}\n\n/**\n * Draw mirror/SoundCloud style waveform - symmetrical bars about the centre line.\n * Each peak is drawn twice (45% of height up and down) with the upper cap rounded\n * on top and the lower cap rounded on the bottom; the progress portion is then\n * repainted in `progressColor` through a left-anchored clip rect.\n * @param {CanvasRenderingContext2D} ctx - Canvas 2D context to draw into.\n * @param {HTMLCanvasElement} canvas - Canvas element (provides device-pixel dimensions).\n * @param {number[]} peaks - Normalised waveform peak values (0-1).\n * @param {number} progress - Playback progress (0-1) that drives the colour overlay.\n * @param {Object} options - Drawing options: `barWidth`, `barSpacing`, `barRadius`,\n * `color`, `progressColor`.\n * @returns {void}\n */\nexport function drawMirror(ctx, canvas, peaks, progress, options) {\n const dpr = window.devicePixelRatio || 1;\n const barWidth = options.barWidth * dpr;\n const barSpacing = options.barSpacing * dpr;\n const barCount = Math.floor(canvas.width / (barWidth + barSpacing));\n const resampledPeaks = resampleData(peaks, barCount);\n const height = canvas.height;\n const centerY = height / 2;\n const progressWidth = progress * canvas.width;\n const r = barRadiusPx(options, dpr);\n const topRadii = [r, r, 0, 0]; // round the upper cap\n const botRadii = [0, 0, r, r]; // round the lower cap\n const baseFill = makeFill(ctx, options.color, height);\n const progFill = makeFill(ctx, options.progressColor, height);\n\n ctx.clearRect(0, 0, canvas.width, canvas.height);\n\n // Draw all bars\n ctx.fillStyle = baseFill;\n for (let i = 0; i < resampledPeaks.length; i++) {\n const x = i * (barWidth + barSpacing);\n if (x + barWidth > canvas.width) break;\n\n const peakHeight = resampledPeaks[i] * height * 0.45;\n\n fillBar(ctx, x, centerY - peakHeight, barWidth, peakHeight, topRadii);\n fillBar(ctx, x, centerY, barWidth, peakHeight, botRadii);\n }\n\n // Progress overlay\n ctx.save();\n ctx.beginPath();\n ctx.rect(0, 0, progressWidth, height);\n ctx.clip();\n\n ctx.fillStyle = progFill;\n for (let i = 0; i < resampledPeaks.length; i++) {\n const x = i * (barWidth + barSpacing);\n if (x > progressWidth) break;\n\n const peakHeight = resampledPeaks[i] * height * 0.45;\n\n fillBar(ctx, x, centerY - peakHeight, barWidth, peakHeight, topRadii);\n fillBar(ctx, x, centerY, barWidth, peakHeight, botRadii);\n }\n\n ctx.restore();\n}\n\n/**\n * Draw line/oscilloscope style waveform - smooth flowing wave with glow.\n * Renders a faint oscilloscope grid (centre line + 10 vertical divisions), the\n * full waveform as a bezier-smoothed curve, then the played portion on top with\n * a coloured shadow glow. Peaks are modulated by a sine term so the line undulates\n * rather than reading as static bars.\n * @param {CanvasRenderingContext2D} ctx - Canvas 2D context to draw into.\n * @param {HTMLCanvasElement} canvas - Canvas element (provides device-pixel dimensions).\n * @param {number[]} peaks - Normalised waveform peak values (0-1).\n * @param {number} progress - Playback progress (0-1); the glowing curve is only drawn when > 0.\n * @param {Object} options - Drawing options: `color` (base wave), `progressColor` (played wave).\n * @returns {void}\n */\nexport function drawLine(ctx, canvas, peaks, progress, options) {\n const width = canvas.width;\n const height = canvas.height;\n const centerY = height / 2;\n const amplitude = height * 0.35;\n\n ctx.clearRect(0, 0, width, height);\n\n /**\n * Stroke a bezier-smoothed curve through the (optionally sine-modulated) peaks.\n * @private\n * @param {string} color - Stroke colour (and shadow colour when glowing).\n * @param {number} lineWidth - Stroke width in pixels.\n * @param {number} [endProgress=1] - Fraction (0-1) of the peaks to draw, left to right.\n * @param {boolean} [addGlow=false] - When true, applies a coloured shadow blur for a glow effect.\n * @returns {void}\n */\n const drawCurve = (color, lineWidth, endProgress = 1, addGlow = false) => {\n if (addGlow) {\n ctx.shadowBlur = 12;\n ctx.shadowColor = color;\n }\n\n ctx.strokeStyle = color;\n ctx.lineWidth = lineWidth;\n ctx.lineCap = 'round';\n ctx.lineJoin = 'round';\n\n ctx.beginPath();\n ctx.moveTo(0, centerY);\n\n const points = [];\n const samples = Math.floor(peaks.length * endProgress);\n\n // Calculate smoothed points\n for (let i = 0; i < samples; i++) {\n const x = (i / (peaks.length - 1)) * width;\n const peakValue = peaks[i];\n\n // Create a smooth wave motion\n const waveOffset = Math.sin(i * 0.1) * peakValue;\n const y = centerY + (waveOffset * amplitude);\n\n points.push({x, y});\n }\n\n // Draw smooth curve through points using bezier curves\n for (let i = 0; i < points.length - 1; i++) {\n const cp1x = points[i].x + (points[i + 1].x - points[i].x) * 0.5;\n const cp1y = points[i].y;\n const cp2x = points[i + 1].x - (points[i + 1].x - points[i].x) * 0.5;\n const cp2y = points[i + 1].y;\n\n ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, points[i + 1].x, points[i + 1].y);\n }\n\n ctx.stroke();\n\n if (addGlow) {\n ctx.shadowBlur = 0;\n }\n };\n\n // Draw subtle grid for oscilloscope feel\n ctx.strokeStyle = 'rgba(255, 255, 255, 0.03)';\n ctx.lineWidth = 0.5;\n\n // Horizontal center line\n ctx.beginPath();\n ctx.moveTo(0, centerY);\n ctx.lineTo(width, centerY);\n ctx.stroke();\n\n // Vertical grid lines\n for (let i = 0; i <= 10; i++) {\n const x = (width / 10) * i;\n ctx.beginPath();\n ctx.moveTo(x, 0);\n ctx.lineTo(x, height);\n ctx.stroke();\n }\n\n // Draw background wave\n drawCurve(options.color, 2, 1, false);\n\n // Draw progress with glow\n if (progress > 0) {\n drawCurve(options.progressColor, 3, progress, true);\n }\n}\n\n/**\n * Draw blocks/LED meter style waveform - segmented blocks growing from the centre.\n * Each bar's height is quantised into fixed-size blocks separated by gaps, drawn\n * symmetrically up and down from the centre line (the shared centre block is not\n * duplicated downward). Per-bar colour is chosen by comparing the bar's x against\n * the played width \u2014 there is no clip overlay here.\n * @param {CanvasRenderingContext2D} ctx - Canvas 2D context to draw into.\n * @param {HTMLCanvasElement} canvas - Canvas element (provides device-pixel dimensions).\n * @param {number[]} peaks - Normalised waveform peak values (0-1).\n * @param {number} progress - Playback progress (0-1) used to pick each bar's colour.\n * @param {Object} options - Drawing options: `barWidth` (default 3), `barSpacing` (default 1),\n * `color`, `progressColor`.\n * @returns {void}\n */\nexport function drawBlocks(ctx, canvas, peaks, progress, options) {\n const dpr = window.devicePixelRatio || 1;\n const barWidth = (options.barWidth || 3) * dpr;\n const barSpacing = (options.barSpacing || 1) * dpr;\n const barCount = Math.floor(canvas.width / (barWidth + barSpacing));\n const resampledPeaks = resampleData(peaks, barCount);\n const height = canvas.height;\n const blockSize = 4 * dpr;\n const blockGap = 2 * dpr;\n const progressWidth = progress * canvas.width;\n const centerY = height / 2;\n const baseFill = makeFill(ctx, options.color, height);\n const progFill = makeFill(ctx, options.progressColor, height);\n\n ctx.clearRect(0, 0, canvas.width, canvas.height);\n\n for (let i = 0; i < resampledPeaks.length; i++) {\n const x = i * (barWidth + barSpacing);\n if (x + barWidth > canvas.width) break;\n\n const peakHeight = resampledPeaks[i] * height * 0.9;\n const blockCount = Math.floor(peakHeight / (blockSize + blockGap));\n\n ctx.fillStyle = x < progressWidth ? progFill : baseFill;\n\n // Draw blocks from center outward\n for (let j = 0; j < blockCount; j++) {\n const blockOffset = j * (blockSize + blockGap);\n\n // Upper blocks\n ctx.fillRect(x, centerY - blockOffset - blockSize, barWidth, blockSize);\n\n // Lower blocks (skip the center block)\n if (j > 0) {\n ctx.fillRect(x, centerY + blockOffset, barWidth, blockSize);\n }\n }\n }\n}\n\n/**\n * Draw dots style waveform - pairs of circular points mirrored about the centre.\n * For each sample a dot is drawn above and below the centre line at half the peak\n * height; dot radius scales with bar width but is floored at 1.5 device pixels.\n * Per-dot colour is chosen by comparing x against the played width (no clip overlay).\n * @param {CanvasRenderingContext2D} ctx - Canvas 2D context to draw into.\n * @param {HTMLCanvasElement} canvas - Canvas element (provides device-pixel dimensions).\n * @param {number[]} peaks - Normalised waveform peak values (0-1).\n * @param {number} progress - Playback progress (0-1) used to pick each dot's colour.\n * @param {Object} options - Drawing options: `barWidth` (default 2), `barSpacing` (default 3),\n * `color`, `progressColor`.\n * @returns {void}\n */\nexport function drawDots(ctx, canvas, peaks, progress, options) {\n const dpr = window.devicePixelRatio || 1;\n const barWidth = (options.barWidth || 2) * dpr;\n const barSpacing = (options.barSpacing || 3) * dpr;\n const barCount = Math.floor(canvas.width / (barWidth + barSpacing));\n const resampledPeaks = resampleData(peaks, barCount);\n const height = canvas.height;\n const dotRadius = Math.max(1.5 * dpr, barWidth / 2);\n const progressWidth = progress * canvas.width;\n const centerY = height / 2;\n const baseFill = makeFill(ctx, options.color, height);\n const progFill = makeFill(ctx, options.progressColor, height);\n\n ctx.clearRect(0, 0, canvas.width, canvas.height);\n\n for (let i = 0; i < resampledPeaks.length; i++) {\n const x = i * (barWidth + barSpacing) + barWidth / 2;\n if (x > canvas.width) break;\n\n const peakHeight = resampledPeaks[i] * height * 0.9;\n\n ctx.fillStyle = x < progressWidth ? progFill : baseFill;\n\n // Draw upper dot\n ctx.beginPath();\n ctx.arc(x, centerY - peakHeight / 2, dotRadius, 0, Math.PI * 2);\n ctx.fill();\n\n // Draw lower dot\n ctx.beginPath();\n ctx.arc(x, centerY + peakHeight / 2, dotRadius, 0, Math.PI * 2);\n ctx.fill();\n }\n}\n\n/**\n * Draw seekbar style - a simple rounded progress bar with no waveform.\n * Renders a pill-shaped background track, a glowing pill-shaped filled portion\n * (clamped to at least one full pill width so it never collapses), and a draggable\n * circular handle/thumb at the playhead with a drop shadow and inner accent dot.\n * The `peaks` argument is accepted for signature parity but is unused by this style.\n * @param {CanvasRenderingContext2D} ctx - Canvas 2D context to draw into.\n * @param {HTMLCanvasElement} canvas - Canvas element (provides device-pixel dimensions).\n * @param {number[]} peaks - Ignored; present to match the shared draw-function signature.\n * @param {number} progress - Playback progress (0-1); the fill and handle are only drawn when > 0.\n * @param {Object} options - Drawing options: `color` (track), `progressColor` (fill/glow/accent).\n * @returns {void}\n */\nexport function drawSeekbar(ctx, canvas, peaks, progress, options) {\n const width = canvas.width;\n const height = canvas.height;\n const centerY = height / 2;\n const barHeight = 4; // Height of the seekbar in pixels\n const borderRadius = barHeight / 2;\n\n ctx.clearRect(0, 0, width, height);\n\n // Draw background track\n ctx.fillStyle = options.color || 'rgba(255, 255, 255, 0.2)';\n\n // Rounded background track\n capsulePath(ctx, borderRadius, width, centerY, barHeight);\n ctx.fill();\n\n // Draw progress\n if (progress > 0) {\n const progressWidth = Math.max(borderRadius * 2, progress * width);\n\n // Add subtle glow effect\n ctx.shadowBlur = 8;\n ctx.shadowColor = options.progressColor;\n\n ctx.fillStyle = options.progressColor || 'rgba(255, 255, 255, 0.9)';\n\n // Rounded progress fill\n capsulePath(ctx, borderRadius, progressWidth, centerY, barHeight);\n ctx.fill();\n\n ctx.shadowBlur = 0;\n\n // Draw progress handle/thumb\n const handleRadius = 8;\n const handleX = progressWidth;\n\n // Handle shadow\n ctx.shadowBlur = 4;\n ctx.shadowColor = 'rgba(0, 0, 0, 0.3)';\n ctx.shadowOffsetY = 2;\n\n // Handle circle\n ctx.fillStyle = '#ffffff';\n ctx.beginPath();\n ctx.arc(handleX, centerY, handleRadius, 0, Math.PI * 2);\n ctx.fill();\n\n // Handle inner circle (for depth)\n ctx.shadowBlur = 0;\n ctx.shadowOffsetY = 0;\n ctx.fillStyle = options.progressColor || 'rgba(255, 255, 255, 0.9)';\n ctx.beginPath();\n ctx.arc(handleX, centerY, handleRadius * 0.4, 0, Math.PI * 2);\n ctx.fill();\n }\n}\n\n/**\n * Map of style names (and singular aliases) to their drawing functions.\n * Six visually distinct styles including a simple seekbar; keys are matched\n * against `options.waveformStyle` by {@link draw}.\n * @type {Object.<string, function(CanvasRenderingContext2D, HTMLCanvasElement, number[], number, Object): void>}\n */\nexport const DRAWING_STYLES = {\n 'bars': drawBars, // Classic vertical bars\n 'bar': drawBars,\n 'mirror': drawMirror, // SoundCloud-style symmetrical\n 'line': drawLine, // Smooth oscilloscope wave\n 'blocks': drawBlocks, // LED meter segmented\n 'block': drawBlocks,\n 'dots': drawDots, // Circular points\n 'dot': drawDots,\n 'seekbar': drawSeekbar // Simple progress bar (no waveform)\n};\n\n/**\n * Main drawing entry point that delegates to the style named by\n * `options.waveformStyle`, falling back to {@link drawBars} for unknown styles.\n * @param {CanvasRenderingContext2D} ctx - Canvas context\n * @param {HTMLCanvasElement} canvas - Canvas element\n * @param {number[]} peaks - Waveform peak data (0-1)\n * @param {number} progress - Progress (0-1)\n * @param {Object} options - Drawing options, including `waveformStyle` plus the\n * per-style fields (`barWidth`, `barSpacing`, `barRadius`, `color`, `progressColor`).\n * @returns {void}\n */\nexport function draw(ctx, canvas, peaks, progress, options) {\n const drawFunc = DRAWING_STYLES[options.waveformStyle] || drawBars;\n drawFunc(ctx, canvas, peaks, progress, options);\n}", "/**\n * @module bpm\n * @description BPM detection for audio analysis\n */\n\n/**\n * Estimate the tempo (beats per minute) of an audio buffer.\n *\n * Analyses the first (left/mono) channel by detecting onsets, measuring the\n * time between successive onsets, converting each interval to a tempo, and\n * histogramming those tempos into 3-BPM buckets (60-200 BPM) to find the most\n * common one. Octave errors are corrected by doubling very slow results and\n * halving very fast ones when a strong half/double bucket also exists, then a\n * fixed -1 BPM calibration offset is applied. Returns a 120 BPM fallback when\n * too few onsets are found, and null if analysis throws.\n *\n * @param {AudioBuffer} buffer - Decoded audio buffer to analyse; only channel 0 is read.\n * @returns {number|null} Detected tempo in BPM, 120 as a fallback when onsets are insufficient, or null on error.\n */\nexport function detectBPM(buffer) {\n try {\n const channelData = buffer.getChannelData(0);\n const sampleRate = buffer.sampleRate;\n const onsets = detectOnsets(channelData, sampleRate);\n\n if (onsets.length < 2) return 120;\n\n // Calculate intervals\n const intervals = [];\n for (let i = 1; i < onsets.length; i++) {\n intervals.push((onsets[i] - onsets[i - 1]) / sampleRate);\n }\n\n // Convert to tempos and group\n const tempoGroups = {};\n intervals.forEach(interval => {\n const tempo = 60 / interval;\n const bucket = Math.round(tempo / 3) * 3;\n if (bucket > 60 && bucket < 200) {\n tempoGroups[bucket] = (tempoGroups[bucket] || 0) + 1;\n }\n });\n\n // Find most common\n let maxCount = 0;\n let detectedBPM = 120;\n for (const [tempo, count] of Object.entries(tempoGroups)) {\n if (count > maxCount) {\n maxCount = count;\n detectedBPM = parseInt(tempo);\n }\n }\n\n // Handle tempo ambiguity\n if (detectedBPM < 70 && tempoGroups[detectedBPM * 2]) {\n detectedBPM *= 2;\n } else if (detectedBPM > 160 && tempoGroups[Math.round(detectedBPM / 2)]) {\n detectedBPM = Math.round(detectedBPM / 2);\n }\n\n return detectedBPM - 1; // Calibration offset\n } catch (e) {\n console.warn('[WaveformPlayer] BPM detection failed:', e);\n return null;\n }\n}\n\n/**\n * Detect onset sample positions (transients/beats) within a channel of audio.\n *\n * Slides a 2048-sample window (50% overlap via a half-window hop) across the\n * signal, computing the mean squared energy of each window. An onset is flagged\n * when the energy rise over the previous (smoothed) energy exceeds an adaptive\n * threshold and the window energy is above a noise floor, subject to a minimum\n * spacing of 150 ms so a single transient is not counted twice. The running\n * previousEnergy is exponentially smoothed (0.8 new / 0.2 old) to track the\n * local energy envelope.\n *\n * @param {Float32Array} channelData - PCM samples (normalised -1..1) for a single channel.\n * @param {number} sampleRate - Sample rate in Hz, used to derive the minimum onset spacing.\n * @returns {number[]} Ascending sample indices at which onsets were detected.\n * @private\n */\nfunction detectOnsets(channelData, sampleRate) {\n const windowSize = 2048;\n const hopSize = windowSize / 2;\n const onsets = [];\n let previousEnergy = 0;\n\n for (let i = 0; i < channelData.length - windowSize; i += hopSize) {\n let energy = 0;\n for (let j = i; j < i + windowSize; j++) {\n energy += channelData[j] * channelData[j];\n }\n energy = energy / windowSize;\n\n const energyDiff = energy - previousEnergy;\n const threshold = previousEnergy * 1.8 + 0.01;\n\n if (energyDiff > threshold && energy > 0.01) {\n const lastOnset = onsets[onsets.length - 1] || 0;\n const minDistance = sampleRate * 0.15;\n\n if (i - lastOnset > minDistance) {\n onsets.push(i);\n }\n }\n\n previousEnergy = energy * 0.8 + previousEnergy * 0.2;\n }\n\n return onsets;\n}", "/**\n * @module audio\n * @description Audio processing for WaveformPlayer\n */\n\nimport {detectBPM} from './bpm.js';\nimport {clamp} from './utils.js';\n\n/**\n * Extract peaks from a decoded audio buffer for waveform visualization.\n *\n * Divides the buffer into `samples` equal-width windows and, within each\n * window, finds the largest absolute amplitude. To keep large files fast the\n * inner loop strides through every 10th frame (`sampleStep`) rather than\n * inspecting every frame. Across multiple channels the per-window peaks are\n * merged by taking the loudest channel, then the whole array is normalized so\n * the maximum peak becomes 1 (a silent buffer is returned unscaled).\n *\n * @param {AudioBuffer} buffer - Decoded audio buffer to analyse.\n * @param {number} [samples=200] - Number of peak windows (output array length).\n * @returns {number[]} Array of `samples` normalized peak values in the 0-1 range.\n */\nexport function extractPeaks(buffer, samples = 200) {\n const sampleSize = buffer.length / samples;\n const sampleStep = ~~(sampleSize / 10) || 1;\n const channels = buffer.numberOfChannels;\n const peaks = [];\n\n for (let c = 0; c < channels; c++) {\n const chan = buffer.getChannelData(c);\n\n for (let i = 0; i < samples; i++) {\n const start = ~~(i * sampleSize);\n const end = ~~(start + sampleSize);\n\n let min = 0;\n let max = 0;\n\n for (let j = start; j < end; j += sampleStep) {\n const value = chan[j];\n if (value > max) max = value;\n if (value < min) min = value;\n }\n\n const peak = Math.max(Math.abs(max), Math.abs(min));\n\n if (c === 0 || peak > peaks[i]) {\n peaks[i] = peak;\n }\n }\n }\n\n // Normalize peaks\n const maxPeak = Math.max(...peaks);\n return maxPeak > 0 ? peaks.map(peak => peak / maxPeak) : peaks;\n}\n\n/**\n * Generate waveform data by fetching and decoding an audio file at a URL.\n *\n * Fetches the URL, decodes it through a short-lived AudioContext, runs\n * {@link extractPeaks} followed by {@link normalizePeaks}, and optionally\n * detects the track's BPM. The AudioContext is created lazily and always\n * closed in the `finally` block so failed decodes never leak one (browsers\n * hard-cap the number of live contexts). Errors are logged and re-thrown so\n * callers can fall back to a placeholder waveform.\n *\n * @param {string} url - Audio file URL to fetch and decode.\n * @param {number} [samples=200] - Number of peak windows to extract.\n * @param {boolean} [shouldDetectBPM=false] - Whether to run BPM detection on the decoded buffer.\n * @returns {Promise<{peaks: number[], bpm: (number|null)}>} Resolves with the\n * normalized peaks and the detected BPM (`null` when detection is disabled or fails).\n * @throws {Error} Re-throws any fetch/decode error after logging it.\n */\nexport async function generateWaveform(url, samples = 200, shouldDetectBPM = false) {\n // Created lazily so the finally block can always close it \u2014 browsers\n // hard-cap live AudioContexts (~6 in Chrome), so leaking one per failed\n // decode would break every subsequent player on the page.\n let audioContext;\n try {\n const AudioCtx = window.AudioContext || /** @type {any} */ (window).webkitAudioContext;\n audioContext = new AudioCtx();\n const response = await fetch(url);\n const arrayBuffer = await response.arrayBuffer();\n const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);\n\n let peaks = extractPeaks(audioBuffer, samples);\n\n // Normalize peaks for consistent visualization\n peaks = normalizePeaks(peaks);\n\n let bpm = null;\n if (shouldDetectBPM) {\n bpm = detectBPM(audioBuffer); // synchronous \u2014 returns number|null\n }\n\n return {peaks, bpm};\n } finally {\n // Error (if any) propagates to the caller, which decides how to log /\n // recover; the context is always closed either way.\n if (audioContext) audioContext.close();\n }\n}\n\n/**\n * Generate a synthetic placeholder waveform for use before (or instead of)\n * real peak data is available.\n *\n * Each bar combines a random base height (0.3-0.8) with a slow sinusoidal\n * variation across the array, clamped to the 0.1-1 range so the result always\n * looks like a plausible waveform rather than pure noise.\n *\n * @param {number} [samples=200] - Number of bars (output array length).\n * @returns {number[]} Array of `samples` pseudo-random peak values in the 0.1-1 range.\n */\nexport function generatePlaceholderWaveform(samples = 200) {\n const data = [];\n for (let i = 0; i < samples; i++) {\n const base = Math.random() * 0.5 + 0.3;\n const variation = Math.sin(i / samples * Math.PI * 4) * 0.2;\n data.push(clamp(base + variation, 0.1, 1));\n }\n return data;\n}\n\n/**\n * Scale peak values so quiet tracks fill the available height consistently.\n *\n * Finds the loudest peak and, only when it is non-zero yet below `targetMax`,\n * scales every peak proportionally so the maximum lands on `targetMax`. Silent\n * arrays (max 0) and already-loud arrays (max above `targetMax`) are returned\n * untouched, so the function never amplifies clipping or divides by zero.\n *\n * @param {number[]} peaks - Peak values, typically in the 0-1 range.\n * @param {number} [targetMax=0.95] - Desired maximum peak after scaling.\n * @returns {number[]} The normalized peak array (the original array when no scaling is applied).\n * @private\n */\nfunction normalizePeaks(peaks, targetMax = 0.95) {\n const maxPeak = Math.max(...peaks);\n\n // Don't normalize if already loud enough or silent\n if (maxPeak === 0 || maxPeak > targetMax) return peaks;\n\n // Scale all peaks proportionally\n const scaleFactor = targetMax / maxPeak;\n return peaks.map(peak => peak * scaleFactor);\n}", "/**\n * @module themes\n * @description Color presets and default options for WaveformPlayer\n */\n\nimport {perceivedBrightness} from './utils.js';\n\n/**\n * Does `<html>` or `<body>` explicitly signal the given colour scheme via a\n * known class name (`dark`, `dark-mode`, `theme-dark`) or theme attribute\n * (`data-theme`, and `data-color-scheme` on the root)?\n * @param {'dark'|'light'} scheme - Scheme to look for.\n * @returns {boolean} True if the page explicitly hints at `scheme`.\n * @private\n */\nfunction hasThemeHint(scheme) {\n const root = document.documentElement;\n const body = document.body;\n return (\n root.classList.contains(scheme) ||\n root.classList.contains(`${scheme}-mode`) ||\n root.classList.contains(`theme-${scheme}`) ||\n root.getAttribute('data-theme') === scheme ||\n root.getAttribute('data-color-scheme') === scheme ||\n body.classList.contains(scheme) ||\n body.classList.contains(`${scheme}-mode`) ||\n body.getAttribute('data-theme') === scheme\n );\n}\n\n/**\n * Detect the appropriate color scheme for the player from the surrounding page.\n *\n * Resolution order, first match wins:\n * 1. Explicit theme hints on `<html>`/`<body>` \u2014 class names\n * (`dark`, `dark-mode`, `theme-dark`, light equivalents) and data\n * attributes (`data-theme`, `data-color-scheme`).\n * 2. The page's computed `<body>` background colour, classified via\n * {@link perceivedBrightness} (>128 = light, <128 = dark; exactly 128\n * or unparseable is treated as ambiguous and falls through).\n * 3. The OS/browser `prefers-color-scheme` media query.\n * 4. Default fallback of `'dark'` (most audio players are dark).\n *\n * @returns {string} The detected scheme, either `'dark'` or `'light'`.\n */\nexport function detectColorScheme() {\n // 1. Explicit theme class names / data attributes win.\n if (hasThemeHint('dark')) return 'dark';\n if (hasThemeHint('light')) return 'light';\n\n // 2. Try to detect website's theme from background color\n try {\n const bodyBg = getComputedStyle(document.body).backgroundColor;\n const brightness = perceivedBrightness(bodyBg);\n\n // Clear determination: bright background = light theme. Exactly 128\n // (or unparseable) is ambiguous \u2014 fall through to the next method.\n if (brightness !== null) {\n if (brightness > 128) return 'light';\n if (brightness < 128) return 'dark';\n }\n } catch (e) {\n // If background detection fails, continue to next method\n }\n\n // 3. Check system preference\n if (window.matchMedia) {\n if (window.matchMedia('(prefers-color-scheme: dark)').matches) {\n return 'dark';\n }\n if (window.matchMedia('(prefers-color-scheme: light)').matches) {\n return 'light';\n }\n }\n\n // 4. Default fallback (most audio players are dark)\n return 'dark';\n}\n\n/**\n * Built-in colour presets keyed by scheme name.\n *\n * Each preset is a flat map of the player's themeable colour tokens\n * (waveform, progress, button, text, background, border). They are deliberately\n * simple translucent black/white values so they sit on any host background, and\n * any individual token can be overridden per-instance via the matching\n * `*Color` option in {@link DEFAULT_OPTIONS}.\n *\n * @type {Object<string, Object<string, string>>}\n * @property {Object<string, string>} dark Light-on-dark token set.\n * @property {Object<string, string>} light Dark-on-light token set.\n */\nexport const COLOR_PRESETS = {\n dark: {\n waveformColor: 'rgba(255, 255, 255, 0.3)',\n progressColor: 'rgba(255, 255, 255, 0.9)',\n buttonColor: 'rgba(255, 255, 255, 0.9)',\n buttonHoverColor: 'rgba(255, 255, 255, 1)',\n textColor: '#ffffff',\n textSecondaryColor: 'rgba(255, 255, 255, 0.6)',\n backgroundColor: 'rgba(255, 255, 255, 0.03)',\n borderColor: 'rgba(255, 255, 255, 0.1)'\n },\n light: {\n waveformColor: 'rgba(0, 0, 0, 0.2)',\n progressColor: 'rgba(0, 0, 0, 0.8)',\n buttonColor: 'rgba(0, 0, 0, 0.8)',\n buttonHoverColor: 'rgba(0, 0, 0, 0.9)',\n textColor: '#333333',\n textSecondaryColor: 'rgba(0, 0, 0, 0.6)',\n backgroundColor: 'rgba(0, 0, 0, 0.02)',\n borderColor: 'rgba(0, 0, 0, 0.1)'\n }\n};\n\n/**\n * Resolve a colour preset by name, falling back to auto-detection.\n *\n * When `presetName` names a known preset it is returned as-is; otherwise\n * (null, undefined, or an unrecognised name) the scheme is auto-detected via\n * {@link detectColorScheme} and the corresponding preset is returned.\n *\n * @param {string|null} presetName - Preset name (`'dark'` or `'light'`), or\n * null/invalid to trigger auto-detection.\n * @returns {Object<string, string>} The matching colour token map from\n * {@link COLOR_PRESETS}.\n */\nexport function getColorPreset(presetName) {\n // If explicitly set to a valid preset, use it\n if (presetName && COLOR_PRESETS[presetName]) {\n return COLOR_PRESETS[presetName];\n }\n\n // Auto-detect if not specified or invalid\n const detected = detectColorScheme();\n return COLOR_PRESETS[detected];\n}\n\n/**\n * Default option set for a {@link WaveformPlayer} instance.\n *\n * User-supplied options are merged over this object, so every supported option\n * is enumerated here with its baseline value. `null` colour tokens mean \"inherit\n * from the resolved {@link COLOR_PRESETS} preset\"; `null` content/callback\n * fields mean \"unset\". See the grouped inline comments for per-field notes,\n * notably the `audioMode` self/external distinction and the `accessibleSeek`\n * keyboard slider.\n *\n * @type {Object}\n */\nexport const DEFAULT_OPTIONS = {\n // Core settings\n url: '',\n height: 64,\n // Source peak resolution. The drawer resamples these to fit\n // canvasWidth / (barWidth + barSpacing) bars, so this is fidelity headroom,\n // not the visible bar count.\n samples: 256,\n preload: 'metadata',\n\n // Audio mode \u2014 'self' = player owns the <audio> element (default, current\n // behavior). 'external' = player is a visualization-only surface; no audio\n // element is created, play() dispatches `waveformplayer:request-play`\n // instead of calling audio.play(), and setPlayingState/setProgress are\n // expected to be driven by an external controller (e.g. WaveformBar).\n audioMode: 'self',\n\n // Playback\n playbackRate: 1,\n showPlaybackSpeed: false,\n playbackRates: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2],\n\n // Layout Options\n buttonAlign: 'auto',\n // Player layout. 'default' = play button + waveform with a left-aligned\n // info row below. 'preview' = compact: the title is centered under the\n // waveform and the meta row (time / speed / BPM) is trimmed \u2014 ideal for\n // sample-pack sample previews and dense grids.\n layout: 'default',\n // Play/pause button style. 'circle' = bordered circle (default).\n // 'minimal' = a bare play/pause glyph with no circle \u2014 the look sample-pack\n // and beat stores use in their preview grids.\n buttonStyle: 'circle',\n\n // Default waveform style\n waveformStyle: 'mirror',\n barWidth: 2,\n barSpacing: 0,\n // Rounded bar caps (px). 0 = square; 1 = soft caps (default). Applies to bars/mirror.\n barRadius: 1,\n\n // Color preset: null = auto-detect, 'dark' = force dark, 'light' = force light\n colorPreset: null,\n\n // Individual color overrides (null means use preset)\n waveformColor: null,\n progressColor: null,\n buttonColor: null,\n buttonHoverColor: null,\n textColor: null,\n textSecondaryColor: null,\n backgroundColor: null,\n borderColor: null,\n\n // Features\n autoplay: false,\n showControls: true,\n showInfo: true,\n showTime: true,\n showHoverTime: false,\n showBPM: false,\n singlePlay: true,\n playOnSeek: true,\n enableMediaSession: true,\n\n // Markers\n markers: [],\n showMarkers: true,\n\n // Accessibility \u2014 expose the waveform as a keyboard-operable slider\n // (role=\"slider\" + ARIA value attributes + arrow/page/home/end seeking).\n // seekLabel sets the slider's accessible name; when null it falls back\n // to the track title, then 'Seek'.\n accessibleSeek: true,\n seekLabel: null,\n\n // Content\n title: null,\n subtitle: null,\n artwork: null,\n album: '',\n\n // Message shown in the error state when audio fails to load.\n errorText: 'Unable to load audio',\n\n // Icons (SVG)\n playIcon: '<svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\"><path d=\"M8 5v14l11-7z\"/></svg>',\n pauseIcon: '<svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\"><path d=\"M6 4h4v16H6zM14 4h4v16h-4z\"/></svg>',\n\n // Callbacks\n onLoad: null,\n onPlay: null,\n onPause: null,\n onEnd: null,\n onError: null,\n onTimeUpdate: null\n};\n\n/**\n * Per-waveform-style geometry defaults.\n *\n * Maps each supported `waveformStyle` to its natural `barWidth`/`barSpacing`\n * (in px), used to seed bar geometry when the caller has not explicitly set\n * those options so each style renders at sensible proportions.\n *\n * @type {Object<string, {barWidth: number, barSpacing: number}>}\n */\nexport const STYLE_DEFAULTS = {\n bars: {barWidth: 3, barSpacing: 1},\n mirror: {barWidth: 2, barSpacing: 2},\n line: {barWidth: 2, barSpacing: 0},\n blocks: {barWidth: 4, barSpacing: 2},\n dots: {barWidth: 3, barSpacing: 3},\n seekbar: {barWidth: 1, barSpacing: 0}\n};", "/**\n * @module core\n * @description Main WaveformPlayer class\n */\n\nimport {draw} from './drawing.js';\nimport {generateWaveform, generatePlaceholderWaveform} from './audio.js';\nimport {\n formatTime,\n extractTitleFromUrl,\n generateId,\n parseDataAttributes,\n mergeOptions,\n debounce,\n clamp,\n escapeHtml\n} from './utils.js';\n\nimport {DEFAULT_OPTIONS, STYLE_DEFAULTS, getColorPreset} from './themes.js';\n\n// Keyboard seek steps (seconds) for the accessible slider.\nconst SEEK_STEP_SECONDS = 5;\nconst SEEK_PAGE_SECONDS = 10;\n\n/**\n * WaveformPlayer - Modern audio player with waveform visualization\n * @class\n */\nexport class WaveformPlayer {\n /** @type {Map<string, WaveformPlayer>} */\n static instances = new Map();\n\n /** @type {WaveformPlayer|null} */\n static currentlyPlaying = null;\n\n /**\n * Create a new WaveformPlayer instance.\n *\n * Resolves the container, merges options (defaults < `data-*` attributes <\n * constructor options), applies the colour preset and style-specific\n * defaults, registers the instance in the static map, and kicks off\n * {@link WaveformPlayer#init}. A `waveformplayer:ready` event is dispatched\n * ~100ms later, once initialization has settled.\n *\n * @param {string|HTMLElement} container - Container element, or a CSS\n * selector resolved with `document.querySelector`.\n * @param {Object} [options={}] - Player options. Accepts the shorthand\n * aliases `style` (\u2192 `waveformStyle`) and `src` (\u2192 `url`); the canonical\n * names win if both are supplied.\n * @throws {Error} If the container element cannot be found.\n * @fires WaveformPlayer#waveformplayer:ready\n */\n constructor(container, options = {}) {\n // Resolve container\n this.container = typeof container === 'string'\n ? document.querySelector(container)\n : container;\n\n if (!this.container) {\n throw new Error('[WaveformPlayer] Container element not found');\n }\n\n // Parse data attributes if present\n const dataOptions = parseDataAttributes(this.container);\n\n // Shorthand option aliases \u2014 `style` -> `waveformStyle`, `src` -> `url`.\n // The canonical names still work and win if both are supplied.\n const userOptions = { ...options };\n if (userOptions.style && !userOptions.waveformStyle) userOptions.waveformStyle = userOptions.style;\n if (userOptions.src && !userOptions.url) userOptions.url = userOptions.src;\n\n // Merge options: defaults < data attributes < constructor options\n this.options = mergeOptions(DEFAULT_OPTIONS, dataOptions, userOptions);\n\n // Apply color preset (auto-detect if not specified)\n const preset = getColorPreset(this.options.colorPreset);\n\n // Apply preset colors only if individual colors aren't explicitly set\n for (const [key, value] of Object.entries(preset)) {\n if (this.options[key] === null || this.options[key] === undefined) {\n this.options[key] = value;\n }\n }\n\n // Apply style-specific defaults if not explicitly set\n const styleDefaults = STYLE_DEFAULTS[this.options.waveformStyle];\n if (styleDefaults) {\n if (dataOptions.barWidth === undefined && options.barWidth === undefined) {\n this.options.barWidth = styleDefaults.barWidth;\n }\n if (dataOptions.barSpacing === undefined && options.barSpacing === undefined) {\n this.options.barSpacing = styleDefaults.barSpacing;\n }\n }\n\n // Initialize state\n this.audio = null;\n this.canvas = null;\n this.ctx = null;\n this.waveformData = [];\n this.progress = 0;\n this.isPlaying = false;\n this.isLoading = false;\n this.hasError = false;\n this.updateTimer = null;\n this.resizeObserver = null;\n\n // All DOM/document listeners are registered with this signal so a\n // single abort() in destroy() tears every one of them down (the old\n // destroy left the document-click and container listeners attached).\n this._ac = new AbortController();\n\n // Generate unique ID\n this.id = this.container.id || generateId(this.options.url);\n\n // Add to instances\n WaveformPlayer.instances.set(this.id, this);\n\n // Initialize\n this.init();\n\n // Dispatch ready event after initialization\n setTimeout(() => {\n this._emit('waveformplayer:ready', {player: this, url: this.options.url});\n }, 100);\n }\n\n /**\n * Build and dispatch a bubbling `waveformplayer:*` CustomEvent on the\n * container, returning the event so cancelable (request-*) events can have\n * their `defaultPrevented` checked. Single source of truth for the event\n * shape \u2014 every player event bubbles and carries the supplied detail.\n * @param {string} type - Full event type, e.g. `'waveformplayer:play'`.\n * @param {Object} detail - Event detail payload.\n * @param {boolean} [cancelable=false] - Whether the event is cancelable.\n * @returns {CustomEvent} The dispatched event.\n * @private\n */\n _emit(type, detail, cancelable = false) {\n const event = new CustomEvent(type, { bubbles: true, cancelable, detail });\n this.container.dispatchEvent(event);\n return event;\n }\n\n /**\n * External-mode seek request: dispatch a cancelable\n * `waveformplayer:request-seek` and, unless the controller calls\n * `preventDefault()`, optimistically advance the local progress overlay so\n * the canvas repaints at once. Shared by the keyboard slider and canvas click.\n * @param {number} percent - Target position as a 0..1 fraction.\n * @private\n * @fires WaveformPlayer#waveformplayer:request-seek\n */\n _requestSeek(percent) {\n const evt = this._emit('waveformplayer:request-seek', { ...this._buildTrackDetail(), percent }, true);\n if (!evt.defaultPrevented) {\n this.progress = percent;\n this.drawWaveform?.();\n }\n }\n\n // ============================================\n // Initialization\n // ============================================\n\n /**\n * Initialize the player: build the DOM, create the audio element (self\n * mode only), wire up the feature controls (speed, keyboard, accessible\n * seek), bind events, attach the resize observer, then size the canvas and\n * \u2014 if a `url` option was given \u2014 load it and optionally autoplay.\n * @private\n */\n init() {\n this.createDOM();\n this.createAudio();\n this.initPlaybackSpeed();\n this.initKeyboardControls();\n this.initSeekControl();\n this.bindEvents();\n this.setupResizeObserver();\n\n // Ensure proper sizing after DOM is ready\n requestAnimationFrame(() => {\n this.resizeCanvas();\n\n // Load audio if URL provided\n if (this.options.url) {\n this.load(this.options.url).then(() => {\n if (this.options.autoplay) {\n this.play()?.catch(() => {});\n }\n }).catch(error => {\n console.error('[WaveformPlayer] Failed to load audio:', error);\n });\n }\n });\n }\n\n /**\n * Build the player's DOM tree inside the container and cache element\n * references.\n *\n * Clears the container, resolves button alignment (`auto` \u2192 `bottom` for\n * the `bars` style, `center` otherwise), and conditionally renders the play\n * button, info row (artwork/title/subtitle), BPM badge, playback-speed\n * menu, and time display based on the relevant `show*` options. Caches the\n * canvas, controls, and text elements onto `this`, then sizes the canvas.\n * @private\n */\n createDOM() {\n // Clear container\n this.container.innerHTML = '';\n this.container.className = 'waveform-player';\n\n // Determine button alignment\n let buttonAlign = this.options.buttonAlign;\n if (buttonAlign === 'auto') {\n // Auto-align based on waveform style\n const style = this.options.waveformStyle;\n if (style === 'bars') {\n buttonAlign = 'bottom';\n } else {\n buttonAlign = 'center'; // blocks, mirror, line, dots, seekbar all center\n }\n }\n\n // Compact 'preview' layout: centered title under the waveform with the\n // meta row trimmed. Set via the `layout` option / data-layout=\"preview\".\n const isPreview = this.options.layout === 'preview';\n if (isPreview) {\n this.container.classList.add('waveform-layout-preview');\n }\n\n // Build play button HTML (conditional)\n const buttonHTML = this.options.showControls ? `\n <button class=\"waveform-btn${this.options.buttonStyle === 'minimal' ? ' waveform-btn-minimal' : ''}\" aria-label=\"Play/Pause\" style=\"\n border-color: ${this.options.buttonColor};\n color: ${this.options.buttonColor};\n \">\n <span class=\"waveform-icon-play\">${this.options.playIcon}</span>\n <span class=\"waveform-icon-pause\" style=\"display:none;\">${this.options.pauseIcon}</span>\n </button>\n ` : '';\n\n // Build info section HTML (conditional)\n const infoHTML = this.options.showInfo ? `\n <div class=\"waveform-info\">\n ${this.options.artwork ? `\n <img class=\"waveform-artwork\" src=\"${this.options.artwork}\" alt=\"Album artwork\" style=\"\n width: 40px;\n height: 40px;\n border-radius: 4px;\n object-fit: cover;\n flex-shrink: 0;\n \">\n ` : ''}\n <div class=\"waveform-text\">\n <span class=\"waveform-title\" style=\"color: ${this.options.textColor};\"></span>\n ${this.options.subtitle ? `<span class=\"waveform-subtitle\" style=\"color: ${this.options.textSecondaryColor};\">${this.options.subtitle}</span>` : ''}\n </div>\n <div class=\"waveform-meta\" style=\"display: flex; align-items: center; gap: 1rem;\">\n ${this.options.showBPM ? `\n <span class=\"waveform-bpm\" style=\"color: ${this.options.textSecondaryColor}; display: none;\">\n <span class=\"bpm-value\">--</span> BPM\n </span>\n ` : ''}\n ${this.options.showPlaybackSpeed ? `\n <div class=\"waveform-speed\">\n <button class=\"speed-btn\" aria-label=\"Playback speed\">\n <span class=\"speed-value\">1x</span>\n </button>\n <div class=\"speed-menu\" style=\"display: none;\">\n ${this.options.playbackRates.map(rate =>\n `<button class=\"speed-option\" data-rate=\"${rate}\">${rate}x</button>`\n ).join('')}\n </div>\n </div>\n ` : ''}\n ${this.options.showTime ? `\n <span class=\"waveform-time\" style=\"color: ${this.options.textSecondaryColor};\">\n <span class=\"time-current\">0:00</span> / <span class=\"time-total\">0:00</span>\n </span>\n ` : ''}\n </div>\n </div>\n ` : '';\n\n // Create HTML structure\n this.container.innerHTML = `\n <div class=\"waveform-player-inner\">\n <div class=\"waveform-body\">\n <div class=\"waveform-track waveform-align-${buttonAlign}\">\n ${buttonHTML}\n \n <div class=\"waveform-container\">\n <canvas></canvas>\n <div class=\"waveform-markers\"></div>\n <div class=\"waveform-loading\" style=\"display:none;\"></div>\n <div class=\"waveform-error\" style=\"display:none;\" role=\"alert\">\n <span class=\"waveform-error-text\">${escapeHtml(this.options.errorText)}</span>\n </div>\n </div>\n </div>\n \n ${infoHTML}\n </div>\n </div>\n`;\n\n // Get references\n this.playBtn = this.container.querySelector('.waveform-btn');\n this.canvas = this.container.querySelector('canvas');\n this.ctx = this.canvas.getContext('2d');\n this.titleEl = this.container.querySelector('.waveform-title');\n this.subtitleEl = this.container.querySelector('.waveform-subtitle');\n this.artworkEl = this.container.querySelector('.waveform-artwork');\n this.currentTimeEl = this.container.querySelector('.time-current');\n this.totalTimeEl = this.container.querySelector('.time-total');\n this.bpmEl = this.container.querySelector('.waveform-bpm');\n this.bpmValueEl = this.container.querySelector('.bpm-value');\n this.loadingEl = this.container.querySelector('.waveform-loading');\n this.errorEl = this.container.querySelector('.waveform-error');\n this.markersContainer = this.container.querySelector('.waveform-markers');\n this.speedBtn = this.container.querySelector('.speed-btn');\n this.speedMenu = this.container.querySelector('.speed-menu');\n\n // Set canvas size\n this.resizeCanvas();\n }\n\n /**\n * Create audio element\n * @private\n *\n * No-op in `audioMode: 'external'` \u2014 the player has no audio of its\n * own; an external controller (e.g. WaveformBar) owns playback and\n * pushes state in via setPlayingState() / setProgress(). The\n * `this.audio` field stays null in that mode; downstream code must\n * null-check it.\n */\n createAudio() {\n if (this.options.audioMode === 'external') {\n this.audio = null;\n return;\n }\n this.audio = new Audio();\n this.audio.preload = this.options.preload || 'metadata';\n this.audio.crossOrigin = 'anonymous';\n }\n\n // ============================================\n // Feature Initialization\n // ============================================\n\n /**\n * Apply the configured initial playback rate to the audio element (self\n * mode only) and, when `showPlaybackSpeed` is enabled, wire up the speed\n * menu UI via {@link WaveformPlayer#initSpeedControls}.\n * @private\n */\n initPlaybackSpeed() {\n // External mode has no <audio> element, so the speed control\n // doesn't apply locally \u2014 the external controller (e.g.\n // WaveformBar) owns playback rate. Skip the audio init but\n // still bind the speed control UI in case the controller\n // wants to mirror rate changes via events later.\n if (this.audio && this.options.playbackRate && this.options.playbackRate !== 1) {\n this.audio.playbackRate = this.options.playbackRate;\n }\n\n // Initialize speed control UI if enabled\n if (this.options.showPlaybackSpeed) {\n this.initSpeedControls();\n }\n }\n\n /**\n * Wire up the playback-speed menu: toggle it open on the speed button,\n * close it on any outside click, and apply the chosen rate when a\n * `.speed-option` is clicked. All listeners are registered against the\n * instance `AbortController` signal so {@link WaveformPlayer#destroy} tears\n * them down. No-op if the speed elements are absent.\n * @private\n */\n initSpeedControls() {\n const speedBtn = this.container.querySelector('.speed-btn');\n const speedMenu = this.container.querySelector('.speed-menu');\n\n if (!speedBtn || !speedMenu) return;\n\n // Toggle menu\n speedBtn.addEventListener('click', (e) => {\n e.stopPropagation();\n speedMenu.style.display = speedMenu.style.display === 'none' ? 'block' : 'none';\n }, {signal: this._ac.signal});\n\n // Close menu when clicking outside\n document.addEventListener('click', () => {\n speedMenu.style.display = 'none';\n }, {signal: this._ac.signal});\n\n // Handle speed selection\n speedMenu.addEventListener('click', (e) => {\n e.stopPropagation();\n if (e.target.classList.contains('speed-option')) {\n const rate = parseFloat(e.target.dataset.rate);\n this.setPlaybackRate(rate);\n speedMenu.style.display = 'none';\n }\n }, {signal: this._ac.signal});\n\n // Set initial UI state\n this.updateSpeedUI();\n }\n\n /**\n * Enable keyboard transport controls on the container.\n *\n * The container is focusable only after it is clicked (it carries\n * `tabindex=\"-1\"` until then, and clicking steals focus from sibling\n * players). While focused it handles: digits 0-9 (seek to that tenth of\n * the track), Space (toggle play), and \u2014 in self mode only, since\n * `this.audio` is null in external mode \u2014 arrow keys (seek \u00B15s, volume\n * \u00B10.1) and `m`/`M` (mute). Listeners use the instance abort signal.\n * @private\n */\n initKeyboardControls() {\n // Make container focusable but not in tab order by default\n this.container.setAttribute('tabindex', '-1');\n\n // Only activate keyboard controls when explicitly focused (clicked)\n this.container.addEventListener('click', () => {\n // Remove focus from all other players\n WaveformPlayer.getAllInstances().forEach(player => {\n if (player !== this) {\n player.container.setAttribute('tabindex', '-1');\n }\n });\n // Make this one focusable\n this.container.setAttribute('tabindex', '0');\n this.container.focus();\n }, {signal: this._ac.signal});\n\n // Keyboard events. In external mode `this.audio` is null, so\n // seek/volume/mute keys are no-ops (the external controller\n // owns those). Space (togglePlay) still works because togglePlay\n // routes through the request-play/pause events.\n this.container.addEventListener('keydown', (e) => {\n if (document.activeElement !== this.container) return;\n\n const key = e.key;\n const hasAudio = !!this.audio;\n const currentTime = hasAudio ? this.audio.currentTime : 0;\n\n // Handle number keys 0-9 for seeking\n if (hasAudio && key >= '0' && key <= '9') {\n e.preventDefault();\n this.seekToPercent(parseInt(key) / 10);\n return;\n }\n\n // Handle other keys. Space always works (dispatches\n // request-play in external mode); audio-bound keys only\n // when we own the <audio> element.\n const actions = {\n ' ': () => this.togglePlay(),\n };\n if (hasAudio) {\n actions['ArrowLeft'] = () => this.seekTo(clamp(currentTime - 5, 0, this.audio.duration));\n actions['ArrowRight'] = () => this.seekTo(clamp(currentTime + 5, 0, this.audio.duration));\n actions['ArrowUp'] = () => this.setVolume(clamp(this.audio.volume + 0.1));\n actions['ArrowDown'] = () => this.setVolume(clamp(this.audio.volume - 0.1));\n actions['m'] = actions['M'] = () => this.audio.muted = !this.audio.muted;\n }\n\n if (actions[key]) {\n e.preventDefault();\n actions[key]();\n }\n }, {signal: this._ac.signal});\n }\n\n /**\n * Expose the waveform as an accessible, keyboard-operable slider.\n *\n * Adds role=\"slider\" + ARIA value attributes to the waveform surface,\n * makes it focusable in the tab order, and handles the standard slider\n * keys (arrows, Page Up/Down, Home/End) to seek. Works in both self and\n * external audio modes. Opt out with `accessibleSeek: false`.\n * @private\n */\n initSeekControl() {\n if (!this.options.accessibleSeek) return;\n\n this.seekEl = this.container.querySelector('.waveform-container');\n if (!this.seekEl) return;\n\n this.seekEl.setAttribute('role', 'slider');\n this.seekEl.setAttribute('tabindex', '0');\n this.seekEl.setAttribute('aria-valuemin', '0');\n this.applySeekLabel();\n this.updateSeekAccessibility();\n\n this.seekEl.addEventListener('keydown', (e) => {\n const duration = this.getSeekDuration();\n if (!duration) return;\n\n const current = this.getSeekCurrentTime();\n let target;\n switch (e.key) {\n case 'ArrowLeft':\n case 'ArrowDown':\n target = current - SEEK_STEP_SECONDS;\n break;\n case 'ArrowRight':\n case 'ArrowUp':\n target = current + SEEK_STEP_SECONDS;\n break;\n case 'PageDown':\n target = current - SEEK_PAGE_SECONDS;\n break;\n case 'PageUp':\n target = current + SEEK_PAGE_SECONDS;\n break;\n case 'Home':\n target = 0;\n break;\n case 'End':\n target = duration;\n break;\n default:\n return;\n }\n\n // Prevent page scroll and stop the container-level keydown\n // handler from also seeking (it would double-fire / change\n // volume on the vertical arrows).\n e.preventDefault();\n e.stopPropagation();\n this.seekToSeconds(target);\n }, {signal: this._ac.signal});\n }\n\n /**\n * Total seekable duration in seconds, regardless of audio mode.\n * @returns {number}\n * @private\n */\n getSeekDuration() {\n if (this.options.audioMode === 'external') {\n return this._extDuration || 0;\n }\n return this.audio && Number.isFinite(this.audio.duration)\n ? this.audio.duration\n : 0;\n }\n\n /**\n * Current playback position in seconds, regardless of audio mode.\n * @returns {number}\n * @private\n */\n getSeekCurrentTime() {\n if (this.options.audioMode === 'external') {\n return this.progress * (this._extDuration || 0);\n }\n return this.audio && Number.isFinite(this.audio.currentTime)\n ? this.audio.currentTime\n : 0;\n }\n\n /**\n * Seek the slider to an absolute time, clamped to the track length.\n *\n * In self mode this defers to {@link WaveformPlayer#seekTo}. In external\n * mode it dispatches a cancelable `waveformplayer:request-seek` event with\n * the target percentage; if the controller doesn't `preventDefault()`, the\n * local progress/visual is updated optimistically. Either way the ARIA\n * slider values are refreshed.\n * @param {number} seconds - Target time in seconds.\n * @private\n * @fires WaveformPlayer#waveformplayer:request-seek\n */\n seekToSeconds(seconds) {\n const duration = this.getSeekDuration();\n if (!duration) return;\n\n const clamped = clamp(seconds, 0, duration);\n\n if (this.options.audioMode === 'external') {\n this._requestSeek(clamped / duration);\n this.updateSeekAccessibility();\n return;\n }\n\n // seekTo() calls updateProgress(), which refreshes the ARIA values.\n this.seekTo(clamped);\n }\n\n /**\n * Set the slider's accessible name from `seekLabel`, falling back to the\n * track title, then a generic 'Seek'. No-op if the slider isn't present.\n * @param {string} [title=this.options.title] - Track title to fall back to\n * when `seekLabel` is not set.\n * @private\n */\n applySeekLabel(title = this.options.title) {\n if (!this.seekEl) return;\n const label = this.options.seekLabel || title || 'Seek';\n this.seekEl.setAttribute('aria-label', label);\n }\n\n /**\n * Keep the slider's ARIA value attributes in sync with playback.\n * @private\n */\n updateSeekAccessibility() {\n if (!this.seekEl) return;\n\n const duration = this.getSeekDuration();\n const current = Math.min(this.getSeekCurrentTime(), duration);\n\n this.seekEl.setAttribute('aria-valuemax', String(Math.round(duration)));\n this.seekEl.setAttribute('aria-valuenow', String(Math.round(current)));\n this.seekEl.setAttribute(\n 'aria-valuetext',\n `${formatTime(current)} of ${formatTime(duration)}`\n );\n }\n\n /**\n * Initialize Media Session API for system media controls\n * @private\n */\n initMediaSession() {\n if (!('mediaSession' in navigator) || !this.options.enableMediaSession) return;\n // Skip Media Session in external mode \u2014 the controller (e.g.\n // WaveformBar) owns audio playback and registers its own Media\n // Session handlers; ours would conflict with its.\n if (!this.audio) return;\n\n // Set metadata\n navigator.mediaSession.metadata = new MediaMetadata({\n title: this.options.title || 'Unknown Track',\n artist: this.options.subtitle || '',\n album: this.options.album || '',\n artwork: this.options.artwork ? [\n {src: this.options.artwork, sizes: '512x512', type: 'image/jpeg'}\n ] : []\n });\n\n // Set up action handlers\n navigator.mediaSession.setActionHandler('play', () => this.play());\n navigator.mediaSession.setActionHandler('pause', () => this.pause());\n navigator.mediaSession.setActionHandler('seekbackward', () => {\n this.seekTo(clamp(this.audio.currentTime - 10, 0, this.audio.duration));\n });\n navigator.mediaSession.setActionHandler('seekforward', () => {\n this.seekTo(clamp(this.audio.currentTime + 10, 0, this.audio.duration));\n });\n navigator.mediaSession.setActionHandler('seekto', (details) => {\n if (details.seekTime !== null) {\n this.seekTo(details.seekTime);\n }\n });\n }\n\n // ============================================\n // Event Binding\n // ============================================\n\n /**\n * Bind the core interaction listeners: play-button click, the `<audio>`\n * media events (self mode only \u2014 external mode is fed state via\n * {@link WaveformPlayer#setPlayingState}/{@link WaveformPlayer#setProgress}),\n * canvas click-to-seek, and a debounced window-resize redraw.\n * @private\n */\n bindEvents() {\n // Play button (only if controls are shown). In external mode\n // togglePlay() dispatches the request-play/pause events so the\n // controller can decide what to do; the click still goes through\n // here.\n if (this.playBtn) {\n this.playBtn.addEventListener('click', () => this.togglePlay());\n }\n\n // Audio events \u2014 only when we own an <audio> element. External\n // mode receives state via setPlayingState() / setProgress() from\n // the controller, so we have nothing to listen to here.\n if (this.audio) {\n this.audio.addEventListener('loadstart', () => this.setLoading(true));\n this.audio.addEventListener('loadedmetadata', () => this.onMetadataLoaded());\n this.audio.addEventListener('canplay', () => this.setLoading(false));\n this.audio.addEventListener('play', () => this.onPlay());\n this.audio.addEventListener('pause', () => this.onPause());\n this.audio.addEventListener('ended', () => this.onEnded());\n this.audio.addEventListener('error', (e) => this.onError(e));\n }\n\n // Canvas interactions \u2014 seek-on-click. In external mode the\n // canvas click dispatches a `waveformplayer:request-seek` event\n // so the controller can position its own audio element.\n this.canvas.addEventListener('click', (e) => this.handleCanvasClick(e));\n\n // Window resize - store handler for cleanup\n this.resizeHandler = debounce(() => this.resizeCanvas(), 100);\n window.addEventListener('resize', this.resizeHandler);\n }\n\n /**\n * Observe the canvas's parent element for size changes and re-fit the\n * canvas on each one. No-op where `ResizeObserver` is unavailable.\n * @private\n */\n setupResizeObserver() {\n if ('ResizeObserver' in window) {\n this.resizeObserver = new ResizeObserver(() => {\n this.resizeCanvas();\n });\n\n if (this.canvas?.parentElement) {\n this.resizeObserver.observe(this.canvas.parentElement);\n }\n }\n }\n\n // ============================================\n // Audio Loading\n // ============================================\n\n /**\n * Load an audio source: set the title, fetch/generate the waveform peaks,\n * draw them, render markers, and initialise Media Session.\n *\n * In self mode the `<audio>` src is assigned and the method awaits\n * `loadedmetadata` before proceeding. In external mode there is no audio\n * element, so the src/metadata step is skipped and only the visualization\n * is built (duration/time come from the controller via\n * {@link WaveformPlayer#setProgress}). Peaks come from the `waveform`\n * option when provided, otherwise they are decoded from the audio; a\n * decode failure falls back to a placeholder waveform. The `onLoad`\n * callback fires on success.\n * @param {string} url - Audio URL.\n * @returns {Promise<void>} Resolves once loading settles (errors are caught\n * internally and surfaced through {@link WaveformPlayer#onError}).\n */\n async load(url) {\n try {\n this.setLoading(true);\n this.progress = 0;\n this.hasError = false;\n\n // In external mode we don't own an <audio> element \u2014 skip\n // src assignment + metadata-wait, but still generate the\n // waveform peaks so the canvas can render the visualization.\n // Duration / current time come from the external controller\n // via setProgress().\n if (this.audio) {\n // Set audio source\n this.audio.src = url;\n\n // Wait for metadata to load\n await new Promise((resolve, reject) => {\n const metadataHandler = () => {\n this.audio.removeEventListener('loadedmetadata', metadataHandler);\n this.audio.removeEventListener('error', errorHandler);\n resolve();\n };\n const errorHandler = (e) => {\n this.audio.removeEventListener('loadedmetadata', metadataHandler);\n this.audio.removeEventListener('error', errorHandler);\n reject(e);\n };\n this.audio.addEventListener('loadedmetadata', metadataHandler);\n this.audio.addEventListener('error', errorHandler);\n });\n }\n\n // Set title\n const title = this.options.title || extractTitleFromUrl(url);\n if (this.titleEl) {\n this.titleEl.textContent = title;\n }\n // Keep the seek slider's accessible name in sync with the track.\n this.applySeekLabel(title);\n\n // Load or generate waveform\n if (this.options.waveform) {\n this.setWaveformData(this.options.waveform);\n } else {\n // Generate waveform\n try {\n const result = await generateWaveform(url, this.options.samples, this.options.showBPM);\n this.waveformData = result.peaks;\n\n // Store BPM if detected\n if (result.bpm) {\n this.detectedBPM = result.bpm;\n this.updateBPMDisplay();\n }\n } catch (error) {\n console.warn('[WaveformPlayer] Using placeholder waveform:', error);\n this.waveformData = generatePlaceholderWaveform(this.options.samples);\n }\n }\n\n this.drawWaveform();\n this.renderMarkers();\n this.initMediaSession();\n\n // Fire callback\n if (this.options.onLoad) {\n this.options.onLoad(this);\n }\n } catch (error) {\n // onError() is the single funnel for surfacing + logging errors.\n this.onError(error);\n } finally {\n this.setLoading(false);\n }\n }\n\n /**\n * Swap the player to a new track at runtime.\n *\n * Pauses any current playback, fully resets the audio element (self mode),\n * clears error/marker/progress state, merges the new metadata into\n * `this.options`, updates the subtitle/artwork DOM, then calls\n * {@link WaveformPlayer#load}. Auto-plays the new track unless\n * `options.autoplay === false`.\n * @param {string} url - Audio URL.\n * @param {string|null} [title=null] - Track title; keeps the existing\n * title when null.\n * @param {string|null} [subtitle=null] - Track subtitle; pass `''` to hide\n * the subtitle row, or null to keep the existing one.\n * @param {Object} [options={}] - Additional options to merge (e.g.\n * `preload`, `artwork`, `markers`, `autoplay`).\n * @returns {Promise<void>}\n */\n async loadTrack(url, title = null, subtitle = null, options = {}) {\n // Stop current playback and clear state\n if (this.isPlaying) {\n this.pause();\n }\n\n // Reset audio element completely (only when we own one)\n if (this.audio) {\n this.audio.src = '';\n this.audio.load();\n }\n\n // Clear any errors\n this.hasError = false;\n if (this.errorEl) {\n this.errorEl.style.display = 'none';\n }\n if (this.canvas) {\n this.canvas.style.opacity = '1';\n }\n if (this.playBtn) {\n this.playBtn.disabled = false;\n }\n\n // Reset state\n this.progress = 0;\n this.waveformData = [];\n\n // Update options (including preload if specified)\n this.options = mergeOptions(this.options, {\n url,\n title: title || this.options.title,\n subtitle: subtitle || this.options.subtitle,\n ...options\n });\n\n // Apply preload setting if it was changed\n if (options.preload && this.audio) {\n this.audio.preload = options.preload;\n }\n\n // Update UI elements\n if (this.subtitleEl) {\n if (subtitle) {\n this.subtitleEl.textContent = subtitle;\n this.subtitleEl.style.display = '';\n } else if (subtitle === '') {\n this.subtitleEl.style.display = 'none';\n }\n }\n\n // Update artwork if provided\n if (options.artwork && this.artworkEl) {\n this.artworkEl.src = options.artwork;\n }\n\n // Clear or update markers\n this.options.markers = options.markers || [];\n\n // Reset the waveform to the NEW track's peaks, or null to regenerate\n // from the URL. mergeOptions() above keeps the previous track's\n // this.options.waveform when the caller passes none, and load() does\n // `if (this.options.waveform) setWaveformData(...)` \u2014 so without this\n // reset a track loaded without peaks would redraw the PREVIOUS track's\n // waveform (audio changes, visualization doesn't).\n this.options.waveform = options.waveform || null;\n\n // Load the new track\n await this.load(url);\n\n // Auto-play the new track unless the caller opted out \u2014 lets a\n // controller load/restore/enqueue without forcing playback.\n if (options.autoplay !== false) {\n this.play()?.catch(() => {});\n }\n }\n\n // ============================================\n // Visualization\n // ============================================\n\n /**\n * Normalise externally-supplied waveform data into `this.waveformData` and\n * redraw.\n *\n * Accepts several shapes: a `.json` URL (fetched async; peaks and any\n * embedded `markers` are applied on resolve), a JSON-encoded array string,\n * a comma-separated number string, or a plain number array. Malformed\n * input degrades to an empty array rather than throwing.\n * @param {string|number[]} data - Peaks as an array, a JSON/CSV string, or\n * a URL to a `.json` peaks file.\n * @private\n */\n setWaveformData(data) {\n // URL to JSON file \u2014 fetch peaks and maybe markers\n if (typeof data === 'string' && data.trim().endsWith('.json')) {\n fetch(data.trim())\n .then(r => r.json())\n .then(json => {\n this.waveformData = Array.isArray(json) ? json : (json.peaks || []);\n if (json.markers && !this.options.markers?.length) {\n this.options.markers = json.markers;\n this.renderMarkers();\n }\n this.drawWaveform();\n })\n .catch(() => {});\n return;\n }\n\n if (typeof data === 'string') {\n try {\n const parsed = JSON.parse(data);\n this.waveformData = Array.isArray(parsed) ? parsed : [];\n } catch {\n this.waveformData = data.split(',').map(Number);\n }\n } else {\n this.waveformData = Array.isArray(data) ? data : [];\n }\n this.drawWaveform();\n }\n\n /**\n * Render the current waveform + progress to the canvas via the shared\n * {@link draw} routine, passing the resolved style and colours. No-op\n * before the context exists or while there is no peak data.\n * @private\n */\n drawWaveform() {\n if (!this.ctx || this.waveformData.length === 0) return;\n\n draw(this.ctx, this.canvas, this.waveformData, this.progress, {\n ...this.options,\n waveformStyle: this.options.waveformStyle || 'bars',\n color: this.options.waveformColor,\n progressColor: this.options.progressColor\n });\n }\n\n /**\n * Re-fit the canvas backing store to its parent's width and the configured\n * height, scaled by the device pixel ratio for crisp rendering, then\n * redraw. Guards against running after destruction.\n * @private\n */\n resizeCanvas() {\n // Guard against calls after destruction\n if (!this.canvas || this.isDestroying) {\n return;\n }\n\n const dpr = window.devicePixelRatio || 1;\n const rect = this.canvas.parentElement.getBoundingClientRect();\n\n this.canvas.width = rect.width * dpr;\n this.canvas.height = this.options.height * dpr;\n this.canvas.parentElement.style.height = this.options.height + 'px';\n\n this.drawWaveform();\n }\n\n /**\n * Render the configured cue markers as positioned, clickable buttons over\n * the waveform.\n *\n * Clears any existing markers first, then bails out unless `showMarkers` is\n * on, markers exist, and a duration is known (via the mode-agnostic\n * {@link WaveformPlayer#getSeekDuration}). Each marker is placed by its\n * time-as-percentage, carries a tooltip and ARIA label, and seeks on click\n * (also starting playback when `playOnSeek` is set and currently paused).\n * Markers past the track duration are skipped with a warning.\n * @private\n */\n renderMarkers() {\n if (!this.markersContainer) return;\n\n // Always clear existing markers first\n this.markersContainer.innerHTML = '';\n\n if (!this.options.showMarkers || !this.options.markers?.length) return;\n\n // Duration may come from the <audio> (self mode) or the external\n // controller (external mode) \u2014 use the mode-agnostic accessor.\n const duration = this.getSeekDuration();\n if (!duration) {\n return;\n }\n\n // Add each marker\n this.options.markers.forEach((marker, index) => {\n // Skip markers that are beyond the audio duration\n if (marker.time > duration) {\n console.warn(`[WaveformPlayer] Marker \"${marker.label}\" at ${marker.time}s exceeds audio duration of ${duration}s`);\n return;\n }\n\n const position = (marker.time / duration) * 100;\n\n const markerEl = document.createElement('button');\n markerEl.className = 'waveform-marker';\n markerEl.style.left = `${position}%`;\n markerEl.style.backgroundColor = marker.color || 'rgba(255, 255, 255, 0.5)';\n markerEl.setAttribute('aria-label', marker.label);\n markerEl.setAttribute('data-time', marker.time);\n\n // Tooltip\n const tooltip = document.createElement('span');\n tooltip.className = 'waveform-marker-tooltip';\n tooltip.textContent = marker.label;\n markerEl.appendChild(tooltip);\n\n // Click to seek\n markerEl.addEventListener('click', (e) => {\n e.stopPropagation();\n this.seekTo(marker.time);\n if (this.options.playOnSeek && !this.isPlaying) {\n this.play();\n }\n });\n\n this.markersContainer.appendChild(markerEl);\n });\n }\n\n /**\n * Highlight the marker at `index` (toggling an `active` class) and clear\n * the rest. Pass `null` to clear all. Lets an external controller (e.g. a\n * DJ bar) reflect the current section without reaching into the player's\n * private marker DOM.\n * @param {number|null} index - Marker index to activate, or `null` to clear.\n */\n setActiveMarker(index) {\n if (!this.markersContainer) return;\n const markers = this.markersContainer.querySelectorAll('.waveform-marker');\n markers.forEach((el, i) => el.classList.toggle('active', i === index));\n }\n\n // ============================================\n // Event Handlers\n // ============================================\n\n /**\n * Seek to the clicked horizontal position on the waveform canvas.\n *\n * Converts the click X into a 0..1 percentage. In external mode it\n * dispatches a cancelable `waveformplayer:request-seek` event (updating the\n * local visual optimistically unless the controller vetoes it); in self\n * mode it seeks the owned `<audio>` via\n * {@link WaveformPlayer#seekToPercent}.\n * @param {MouseEvent} event - The canvas click event.\n * @private\n * @fires WaveformPlayer#waveformplayer:request-seek\n */\n handleCanvasClick(event) {\n // In external mode the player has no audio of its own \u2014\n // dispatch a cancelable `waveformplayer:request-seek` event\n // with the target percentage so the controller can seek its\n // own audio. Locally we just update the visual progress so\n // the canvas paints the new position immediately (the\n // controller's progress event will reconcile shortly after).\n const rect = this.canvas.getBoundingClientRect();\n const x = event.clientX - rect.left;\n const targetPercent = clamp(x / rect.width);\n\n if (this.options.audioMode === 'external') {\n this._requestSeek(targetPercent);\n return;\n }\n\n if (!this.audio || !this.audio.duration) return;\n this.seekToPercent(targetPercent);\n }\n\n /**\n * Toggle the loading state: show/hide the spinner overlay and set\n * `aria-busy` on the accessible seek slider so assistive tech knows the\n * player is fetching/decoding.\n * @param {boolean} loading - True while audio is loading.\n * @private\n */\n setLoading(loading) {\n this.isLoading = loading;\n if (this.loadingEl) {\n this.loadingEl.style.display = loading ? 'block' : 'none';\n }\n // Let assistive tech know the player is busy fetching/decoding.\n if (this.seekEl) {\n this.seekEl.setAttribute('aria-busy', loading ? 'true' : 'false');\n }\n }\n\n /**\n * `loadedmetadata` handler (self mode): write the total-time display, now\n * that duration is known re-render markers, and publish duration to the\n * accessible seek slider. No-op during destruction.\n * @private\n */\n onMetadataLoaded() {\n // Ignore during destruction\n if (this.isDestroying) return;\n\n if (this.totalTimeEl) {\n this.totalTimeEl.textContent = formatTime(this.audio.duration);\n }\n // Re-render markers when duration is known\n this.renderMarkers();\n // Duration is now known \u2014 publish it to the accessible slider.\n this.updateSeekAccessibility();\n }\n\n /**\n * Reflect play/pause state on the transport button: toggle the `playing`\n * class and swap the play/pause icon visibility. The single source of\n * truth shared by `onPlay`, `onPause`, and the external-mode\n * `setPlayingState` pump so they can't drift. No-op without a button.\n * @param {boolean} isPlaying - Whether playback is active.\n * @private\n */\n setPlayButtonState(isPlaying) {\n if (!this.playBtn) return;\n this.playBtn.classList.toggle('playing', isPlaying);\n const playIcon = this.playBtn.querySelector('.waveform-icon-play');\n const pauseIcon = this.playBtn.querySelector('.waveform-icon-pause');\n if (playIcon) playIcon.style.display = isPlaying ? 'none' : 'flex';\n if (pauseIcon) pauseIcon.style.display = isPlaying ? 'flex' : 'none';\n }\n\n /**\n * `play` handler (self mode): set the playing flag, swap the button to its\n * pause icon, start the smooth progress loop, dispatch\n * `waveformplayer:play`, and fire the `onPlay` callback. No-op during\n * destruction.\n * @private\n * @fires WaveformPlayer#waveformplayer:play\n */\n onPlay() {\n // Ignore during destruction\n if (this.isDestroying) return;\n\n this.isPlaying = true;\n\n this.setPlayButtonState(true);\n\n this.startSmoothUpdate();\n\n // Dispatch play event\n this._emit('waveformplayer:play', {player: this, url: this.options.url});\n\n if (this.options.onPlay) {\n this.options.onPlay(this);\n }\n }\n\n /**\n * `pause` handler (self mode): clear the playing flag, swap the button back\n * to its play icon, stop the smooth progress loop, dispatch\n * `waveformplayer:pause`, and fire the `onPause` callback. No-op during\n * destruction.\n * @private\n * @fires WaveformPlayer#waveformplayer:pause\n */\n onPause() {\n // Ignore during destruction\n if (this.isDestroying) return;\n\n this.isPlaying = false;\n\n this.setPlayButtonState(false);\n\n this.stopSmoothUpdate();\n\n // Dispatch pause event\n this._emit('waveformplayer:pause', {player: this, url: this.options.url});\n\n if (this.options.onPause) {\n this.options.onPause(this);\n }\n }\n\n /**\n * `ended` handler (self mode): reset progress and `currentTime` to the\n * start, redraw, reset the time display, dispatch `waveformplayer:ended`\n * (carrying the final time), run {@link WaveformPlayer#onPause}, and fire\n * the `onEnd` callback. No-op during destruction.\n * @private\n * @fires WaveformPlayer#waveformplayer:ended\n */\n onEnded() {\n // Ignore during destruction\n if (this.isDestroying) return;\n\n const duration = this.audio.duration;\n\n this.progress = 0;\n this.audio.currentTime = 0;\n this.drawWaveform();\n\n // Reset time display\n if (this.currentTimeEl) {\n this.currentTimeEl.textContent = '0:00';\n }\n\n // Dispatch ended event \u2014 carries the final time so listeners (e.g.\n // analytics) don't have to reach into player.audio.\n this._emit('waveformplayer:ended', {player: this, url: this.options.url, currentTime: duration, duration});\n\n this.onPause();\n\n if (this.options.onEnd) {\n this.options.onEnd(this);\n }\n }\n\n /**\n * `error` handler: set the error flag, hide the spinner, reveal the error\n * overlay, dim the canvas, disable the play button, and fire the `onError`\n * callback. No-op during destruction.\n * @param {Event|Error} error - The audio error event, or an Error thrown\n * during loading.\n * @private\n */\n onError(error) {\n // Ignore errors during destruction\n if (this.isDestroying) return;\n\n console.error('[WaveformPlayer] Audio error:', error);\n this.hasError = true;\n this.setLoading(false);\n\n if (this.errorEl) {\n this.errorEl.style.display = 'flex';\n }\n\n if (this.canvas) {\n this.canvas.style.opacity = '0.2';\n }\n\n if (this.playBtn) {\n this.playBtn.disabled = true;\n }\n\n if (this.options.onError) {\n this.options.onError(error, this);\n }\n }\n\n // ============================================\n // Progress Updates\n // ============================================\n\n /**\n * Start the `requestAnimationFrame` loop that drives smooth progress\n * updates while playing (self mode only \u2014 external mode is redrawn by\n * controller {@link WaveformPlayer#setProgress} pushes). Cancels any\n * existing loop first so it's safe to call repeatedly.\n * @private\n */\n startSmoothUpdate() {\n this.stopSmoothUpdate();\n\n const update = () => {\n // In external mode the canvas redraws are driven by\n // setProgress() pushes from the controller \u2014 no internal\n // RAF needed. Self-mode keeps the smooth-update loop.\n if (this.isPlaying && this.audio && this.audio.duration) {\n this.updateProgress();\n this.updateTimer = requestAnimationFrame(update);\n }\n };\n\n this.updateTimer = requestAnimationFrame(update);\n }\n\n /**\n * Cancel the smooth-update animation frame, if one is scheduled.\n * @private\n */\n stopSmoothUpdate() {\n if (this.updateTimer) {\n cancelAnimationFrame(this.updateTimer);\n this.updateTimer = null;\n }\n }\n\n /**\n * Recompute progress from the owned `<audio>` clock and reflect it\n * everywhere (self mode only \u2014 external mode uses\n * {@link WaveformPlayer#setProgress}).\n *\n * Redraws the canvas when progress moves meaningfully, updates the\n * current-time display, dispatches `waveformplayer:timeupdate`, fires the\n * `onTimeUpdate` callback, and refreshes the accessible slider values.\n * @private\n * @fires WaveformPlayer#waveformplayer:timeupdate\n */\n updateProgress() {\n // Self-mode only \u2014 external mode receives progress via\n // setProgress() from the controller and never calls this.\n if (!this.audio || !this.audio.duration) return;\n\n const newProgress = this.audio.currentTime / this.audio.duration;\n\n if (Math.abs(newProgress - this.progress) > 0.001) {\n this.progress = newProgress;\n this.drawWaveform();\n }\n\n if (this.currentTimeEl) {\n this.currentTimeEl.textContent = formatTime(this.audio.currentTime);\n }\n\n // Dispatch timeupdate event\n this._emit('waveformplayer:timeupdate', {\n player: this,\n currentTime: this.audio.currentTime,\n duration: this.audio.duration,\n progress: this.progress,\n url: this.options.url\n });\n\n if (this.options.onTimeUpdate) {\n this.options.onTimeUpdate(this.audio.currentTime, this.audio.duration, this);\n }\n\n this.updateSeekAccessibility();\n }\n\n // ============================================\n // UI Updates\n // ============================================\n\n /**\n * Show the detected BPM in the badge, once a value has been detected.\n * @private\n */\n updateBPMDisplay() {\n if (this.bpmEl && this.bpmValueEl && this.detectedBPM) {\n this.bpmValueEl.textContent = Math.round(this.detectedBPM);\n this.bpmEl.style.display = 'inline-flex';\n }\n }\n\n /**\n * Sync the speed control's label and the menu's active-option highlight to\n * the audio element's current `playbackRate`. No-op in external mode (no\n * owned `<audio>`), which also avoids reading `playbackRate` before the\n * element exists.\n * @private\n */\n updateSpeedUI() {\n // External mode owns no <audio>; nothing to reflect (and reading\n // this.audio.playbackRate here would throw during construction).\n if (!this.audio) return;\n\n const speedValue = this.container.querySelector('.speed-value');\n if (speedValue) {\n const rate = this.audio.playbackRate;\n speedValue.textContent = rate === 1 ? '1x' : `${rate}x`;\n }\n\n // Update active state in menu\n this.container.querySelectorAll('.speed-option').forEach(btn => {\n btn.classList.toggle('active', parseFloat(btn.dataset.rate) === this.audio.playbackRate);\n });\n }\n\n // ============================================\n // Public API\n // ============================================\n\n /**\n * Play audio.\n *\n * In `audioMode: 'self'` (default): calls the underlying <audio>\n * element's play(). Returns the promise from HTMLMediaElement.play().\n *\n * In `audioMode: 'external'`: dispatches a cancelable\n * `waveformplayer:request-play` event with the track metadata and\n * does NOT touch any audio element. Returns `undefined`. An external\n * controller (e.g. WaveformBar) listens for this event and starts\n * playback on its own audio source, then pushes state back via\n * setPlayingState() / setProgress(). Calling preventDefault() on\n * the event lets the controller veto the play (state is unchanged).\n *\n * When `singlePlay` is enabled, any other currently-playing instance is\n * paused first.\n *\n * @return {Promise|undefined} The promise from `HTMLMediaElement.play()` in\n * self mode; `undefined` in external mode.\n * @fires WaveformPlayer#waveformplayer:request-play\n */\n play() {\n if (this.options.singlePlay && WaveformPlayer.currentlyPlaying &&\n WaveformPlayer.currentlyPlaying !== this) {\n WaveformPlayer.currentlyPlaying.pause();\n }\n\n if (this.options.audioMode === 'external') {\n const evt = this._emit('waveformplayer:request-play', this._buildTrackDetail(), true);\n // If the controller cancels (preventDefault), don't claim\n // \"currentlyPlaying\" \u2014 the controller didn't accept the play.\n if (!evt.defaultPrevented) {\n WaveformPlayer.currentlyPlaying = this;\n }\n return undefined;\n }\n\n WaveformPlayer.currentlyPlaying = this;\n return this.audio.play();\n }\n\n /**\n * Pause audio.\n *\n * In `audioMode: 'external'`, dispatches `waveformplayer:request-pause`\n * (cancelable) and does NOT touch any audio element. See play().\n *\n * @fires WaveformPlayer#waveformplayer:request-pause\n */\n pause() {\n if (WaveformPlayer.currentlyPlaying === this) {\n WaveformPlayer.currentlyPlaying = null;\n }\n if (this.options.audioMode === 'external') {\n this._emit('waveformplayer:request-pause', this._buildTrackDetail(), true);\n return;\n }\n this.audio.pause();\n }\n\n /**\n * Build the track detail object dispatched by request-play /\n * request-pause events in external audio mode. Mirrors the shape\n * WaveformBar.play() accepts so a controller can forward it\n * directly: `WaveformBar.play(event.detail)`.\n *\n * @private\n * @return {{url:string,title:?string,subtitle:?string,artist:?string,artwork:?string,player:WaveformPlayer}}\n */\n _buildTrackDetail() {\n return {\n url: this.options.url,\n title: this.options.title,\n subtitle: this.options.subtitle,\n // Core has no separate `artist` option; mirror subtitle so the\n // published event detail is self-consistent for controllers.\n artist: this.options.artist || this.options.subtitle,\n artwork: this.options.artwork,\n markers: this.options.markers,\n waveform: this.options.waveform,\n id: this.id,\n player: this\n };\n }\n\n /**\n * External-mode state pump: flip the play/pause visual state without\n * touching audio. Mirrors what onPlay()/onPause() do but skips the\n * audio-element interactions. Safe to call repeatedly \u2014 idempotent.\n *\n * Only dispatches `waveformplayer:play`/`waveformplayer:pause` (and runs\n * the matching callback) on an actual transition, starting/stopping the\n * smooth-update loop accordingly.\n *\n * @param {boolean} playing - True to enter the playing state, false to\n * enter the paused state.\n * @fires WaveformPlayer#waveformplayer:play\n * @fires WaveformPlayer#waveformplayer:pause\n */\n setPlayingState(playing) {\n const wasPlaying = this.isPlaying;\n this.isPlaying = !!playing;\n this.setPlayButtonState(this.isPlaying);\n if (this.isPlaying && !wasPlaying) {\n this.startSmoothUpdate?.();\n this._emit('waveformplayer:play', {player: this, url: this.options.url});\n if (this.options.onPlay) this.options.onPlay(this);\n } else if (!this.isPlaying && wasPlaying) {\n this.stopSmoothUpdate?.();\n this._emit('waveformplayer:pause', {player: this, url: this.options.url});\n if (this.options.onPause) this.options.onPause(this);\n }\n }\n\n /**\n * External-mode state pump: update the visualization's progress\n * from an external clock (e.g. WaveformBar's audio element's\n * timeupdate). Drives the canvas redraw + the time displays.\n *\n * Redraws the canvas, updates the current/total time displays, stores the\n * external duration for the accessible slider, dispatches\n * `waveformplayer:timeupdate`, runs `onTimeUpdate`, and synthesizes a\n * one-shot `waveformplayer:ended` (with `onEnd`) when progress reaches the\n * end. No-op for a non-positive duration.\n *\n * @param {number} currentTime - Current playback position in seconds.\n * @param {number} duration - Total track duration in seconds.\n * @fires WaveformPlayer#waveformplayer:timeupdate\n * @fires WaveformPlayer#waveformplayer:ended\n */\n setProgress(currentTime, duration) {\n if (!duration || duration <= 0) return;\n this.progress = clamp(currentTime / duration);\n // Mirror the existing display update code so callers don't have\n // to know which DOM elements live where.\n if (this.currentTimeEl) this.currentTimeEl.textContent = formatTime(currentTime);\n // Publish the duration unconditionally \u2014 the accessible seek slider\n // and keyboard seeking read getSeekDuration()/_extDuration even when\n // there's no time display to update.\n this._extDuration = duration;\n if (this.totalTimeEl && (!this.totalTimeEl.dataset._extSet || this.totalTimeEl.dataset._extDur !== String(duration))) {\n this.totalTimeEl.textContent = formatTime(duration);\n this.totalTimeEl.dataset._extSet = '1';\n this.totalTimeEl.dataset._extDur = String(duration);\n }\n this.drawWaveform?.();\n this._emit('waveformplayer:timeupdate', {player: this, currentTime, duration, progress: this.progress, url: this.options.url});\n // Same (currentTime, duration, player) signature as self mode \u2014 the\n // arg order used to be swapped here, which made one shared handler\n // impossible across audioModes.\n if (this.options.onTimeUpdate) this.options.onTimeUpdate(currentTime, duration, this);\n\n // External mode has no <audio> 'ended' event \u2014 synthesize one when the\n // controller's progress reaches the end (fires once per playthrough).\n if (this.progress >= 1) {\n if (!this._extEnded) {\n this._extEnded = true;\n this._emit('waveformplayer:ended', {player: this, url: this.options.url, currentTime: duration, duration});\n if (this.options.onEnd) this.options.onEnd(this);\n }\n } else {\n this._extEnded = false;\n }\n\n this.updateSeekAccessibility();\n }\n\n /**\n * Toggle between play and pause based on the current `isPlaying` state.\n * Works in both audio modes (in external mode it routes through the\n * request-play/pause events).\n */\n togglePlay() {\n if (this.isPlaying) {\n this.pause();\n } else {\n this.play();\n }\n }\n\n /**\n * Seek the owned `<audio>` element to an absolute time, clamped to\n * `[0, duration]`, and refresh progress. Self mode only \u2014 a no-op when\n * there is no audio element or duration. External-mode keyboard/click\n * seeks go through {@link WaveformPlayer#seekToSeconds} instead.\n * @param {number} seconds - Target time in seconds.\n */\n seekTo(seconds) {\n if (this.audio && this.audio.duration) {\n this.audio.currentTime = clamp(seconds, 0, this.audio.duration);\n this.updateProgress();\n }\n }\n\n /**\n * Seek the owned `<audio>` element to a fraction of the track, clamped to\n * `[0, 1]`, and refresh progress. Self mode only \u2014 a no-op without an audio\n * element or duration.\n * @param {number} percent - Position as a fraction from 0 to 1.\n */\n seekToPercent(percent) {\n if (this.audio && this.audio.duration) {\n this.audio.currentTime = this.audio.duration * clamp(percent);\n this.updateProgress();\n }\n }\n\n /**\n * Set the owned `<audio>` element's volume, clamped to `[0, 1]`. Self mode\n * only \u2014 a no-op in external mode where the controller owns volume.\n * @param {number} volume - Volume from 0 (silent) to 1 (full).\n */\n setVolume(volume) {\n // Coerce + guard: a non-finite value (e.g. from a bad config or stale\n // storage) must not propagate NaN into audio.volume (which throws).\n const v = Number(volume);\n if (this.audio && Number.isFinite(v)) {\n this.audio.volume = clamp(v);\n }\n }\n\n /**\n * Set the owned `<audio>` element's playback rate (clamped to 0.5\u20132),\n * persist it onto `this.options.playbackRate`, and refresh the speed UI.\n * Self mode only \u2014 a no-op in external mode.\n * @param {number} rate - Desired playback rate; clamped to the 0.5\u20132 range.\n */\n setPlaybackRate(rate) {\n if (!this.audio) return;\n\n const clampedRate = clamp(rate, 0.5, 2);\n this.audio.playbackRate = clampedRate;\n this.options.playbackRate = clampedRate;\n\n this.updateSpeedUI();\n }\n\n /**\n * Tear down the player and release all resources.\n *\n * Flags destruction (so in-flight handlers bail), dispatches\n * `waveformplayer:destroy`, stops playback and the animation loop, aborts\n * every listener registered on the instance signal, disconnects the resize\n * observer, removes the window-resize handler, drops the instance from the\n * static map and `currentlyPlaying`, resets/releases the audio element, and\n * empties the container.\n * @fires WaveformPlayer#waveformplayer:destroy\n */\n destroy() {\n // Set a flag to indicate we're destroying\n this.isDestroying = true;\n\n // Let listeners (analytics, controllers) release their references\n // before teardown \u2014 the symmetric counterpart to waveformplayer:ready.\n this._emit('waveformplayer:destroy', {player: this, url: this.options.url});\n\n // Stop playback and animations\n this.pause();\n this.stopSmoothUpdate();\n\n // Tear down every document/container/seek listener in one shot.\n this._ac?.abort();\n\n // Disconnect observer\n if (this.resizeObserver) {\n this.resizeObserver.disconnect();\n this.resizeObserver = null;\n }\n\n // Remove window resize listener\n if (this.resizeHandler) {\n window.removeEventListener('resize', this.resizeHandler);\n this.resizeHandler = null;\n }\n\n // Remove from instances map\n WaveformPlayer.instances.delete(this.id);\n\n // Clear current playing reference if it's this instance\n if (WaveformPlayer.currentlyPlaying === this) {\n WaveformPlayer.currentlyPlaying = null;\n }\n\n // Properly clean up audio element\n if (this.audio) {\n this.audio.pause();\n this.audio.src = '';\n this.audio.load(); // Reset the audio element\n this.audio = null;\n }\n\n // Clear the container\n this.container.innerHTML = '';\n\n // Clear all references\n this.canvas = null;\n this.ctx = null;\n this.playBtn = null;\n this.waveformData = [];\n }\n\n // ============================================\n // Static Methods\n // ============================================\n\n /**\n * Get player instance by ID, element, or element ID\n * @param {string|HTMLElement} idOrElement - Player ID, element, or element ID\n * @returns {WaveformPlayer|undefined}\n */\n static getInstance(idOrElement) {\n if (typeof idOrElement === 'string') {\n const instance = this.instances.get(idOrElement);\n if (instance) return instance;\n\n const element = document.getElementById(idOrElement);\n if (element) {\n return Array.from(this.instances.values()).find(p => p.container === element);\n }\n }\n\n if (idOrElement instanceof HTMLElement) {\n return Array.from(this.instances.values()).find(p => p.container === idOrElement);\n }\n\n return undefined;\n }\n\n /**\n * Get all player instances\n * @returns {WaveformPlayer[]}\n */\n static getAllInstances() {\n return Array.from(this.instances.values());\n }\n\n /**\n * Destroy all player instances\n */\n static destroyAll() {\n this.instances.forEach(player => player.destroy());\n this.instances.clear();\n }\n\n /**\n * Generate waveform data from audio URL\n * @static\n * @param {string} url - Audio URL\n * @param {number} samples - Number of samples\n * @returns {Promise<number[]>} Waveform peak data\n */\n static async generateWaveformData(url, samples = 200) {\n try {\n const result = await generateWaveform(url, samples);\n return result.peaks;\n } catch (error) {\n console.error('[WaveformPlayer] Failed to generate waveform:', error);\n throw error;\n }\n }\n\n /**\n * Derive a peaks-JSON URL from an audio URL by swapping the\n * extension. Strict counterpart to `generateWaveformData()`:\n * `generateWaveformData` decodes the audio at runtime,\n * `getPeaksUrl` assumes you generated the peaks at build time\n * (e.g. with `@arraypress/waveform-gen`) and stored the JSON\n * alongside the audio file.\n *\n * Use the result as the `waveform` option \u2014 the player detects\n * the `.json` suffix, `fetch()`es the file, and skips the Web\n * Audio decode pass entirely. Big perf win on catalogues with\n * many tracks (saves ~1-5s decode per file on slow connections).\n *\n * Recognised extensions: mp3, wav, ogg, flac, m4a, aac.\n * Preserves query strings + URL fragments. Returns `undefined`\n * for unrecognised inputs so callers can pass through\n * unconditionally:\n *\n * new WaveformPlayer('#el', {\n * url: track.audioUrl,\n * waveform: WaveformPlayer.getPeaksUrl(track.audioUrl),\n * });\n *\n * @static\n * @param {string|undefined|null} audioUrl - Audio file URL.\n * @returns {string|undefined} Peaks JSON URL, or `undefined`\n * when the input is empty / has no recognised audio extension.\n *\n * @example\n * WaveformPlayer.getPeaksUrl('/audio/track.mp3')\n * // '/audio/track.json'\n *\n * WaveformPlayer.getPeaksUrl('/audio/track.wav?v=2')\n * // '/audio/track.json?v=2'\n *\n * WaveformPlayer.getPeaksUrl(undefined)\n * // undefined\n */\n static getPeaksUrl(audioUrl) {\n if (!audioUrl) return undefined;\n const swapped = audioUrl.replace(\n /\\.(mp3|wav|ogg|flac|m4a|aac)(\\?[^#]*)?(#.*)?$/i,\n '.json$2$3'\n );\n /* Nothing changed \u2192 unrecognised extension, return undefined\n * so callers know to fall back to live decoding. */\n return swapped === audioUrl ? undefined : swapped;\n }\n\n}", "/**\n * @module index\n * @description Public entry point for the WaveformPlayer library.\n *\n * Wires together the runtime surfaces for the player: it re-exports the\n * {@link WaveformPlayer} class (default and named), exposes a static\n * `WaveformPlayer.init` hook, scans the DOM for declarative `[data-waveform-player]`\n * markup and auto-instantiates a player for each match, and attaches the class\n * to `window` for plain `<script>`/CDN usage. Loading this module is enough to\n * make any markup-driven players on the page come alive once the DOM is ready.\n */\n\n// Import the main class\nimport {WaveformPlayer} from './core.js';\nimport {formatTime, extractTitleFromUrl, escapeHtml, isSafeHref, parseDataAttributes} from './utils.js';\n\n// Expose a small set of pure helpers as a single source of truth so consumers\n// (e.g. @arraypress/waveform-bar, @arraypress/waveform-playlist) can reuse them\n// instead of shipping divergent copies. `parseDataAttributes` lets wrappers read\n// the player's full `data-*` option surface off a host element without\n// re-implementing (and drifting from) the contract. Attached to the class so\n// it's reachable from the IIFE global too.\nWaveformPlayer.utils = {formatTime, extractTitleFromUrl, escapeHtml, isSafeHref, parseDataAttributes};\n\n/**\n * Whether we're running in a browser (vs. SSR / Node), where `window` and\n * `document` are available. Guards the auto-init and global-attach steps.\n * @returns {boolean}\n */\nconst isBrowser = () => typeof window !== 'undefined' && typeof document !== 'undefined';\n\n/**\n * Scan the document for declarative player markup and instantiate one\n * {@link WaveformPlayer} per matching element.\n *\n * Finds every element carrying the `data-waveform-player` attribute and, for\n * each one not already initialized, constructs a player from it (the constructor\n * reads the element's `data-*` attributes for configuration). Each successfully\n * initialized element is flagged with `data-waveform-initialized=\"true\"` so\n * repeat calls are idempotent and never double-initialize the same element.\n * Construction errors are caught and logged so one broken element cannot abort\n * the rest of the scan. A no-op in non-DOM environments (e.g. SSR).\n *\n * @returns {void}\n */\nfunction autoInit() {\n if (!isBrowser()) return;\n\n const elements = document.querySelectorAll('[data-waveform-player]');\n\n elements.forEach(element => {\n if (element.dataset.waveformInitialized === 'true') return;\n\n try {\n new WaveformPlayer(element);\n element.dataset.waveformInitialized = 'true';\n } catch (error) {\n console.error('[WaveformPlayer] Failed to initialize:', error, element);\n }\n });\n}\n\n// Initialize when DOM is ready: defer until DOMContentLoaded if the document is\n// still parsing, otherwise run the scan immediately on import.\nif (isBrowser()) {\n if (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', autoInit);\n } else {\n autoInit();\n }\n}\n\n/**\n * Static re-scan hook.\n *\n * Exposes {@link autoInit} as `WaveformPlayer.init` so callers can manually\n * (re-)scan the DOM after dynamically injecting `[data-waveform-player]` markup.\n * Already-initialized elements are skipped on subsequent calls.\n *\n * @type {typeof autoInit}\n */\nWaveformPlayer.init = autoInit;\n\n// For CDN/browser usage: expose the class as a global so it is reachable from a\n// plain <script> tag without an ES module loader.\nif (isBrowser()) {\n window.WaveformPlayer = WaveformPlayer;\n}\n\n/**\n * The {@link WaveformPlayer} class.\n * @type {typeof WaveformPlayer}\n */\nexport default WaveformPlayer;\n\n// Named exports\nexport {WaveformPlayer};"],
|
|
5
|
-
"mappings": "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",
|
|
6
|
-
"names": ["escapeHtml", "str", "isSafeHref", "url", "u", "clamp", "value", "min", "max", "parseBoolAttr", "parseColorValue", "parseDataAttributes", "element", "options", "setBool", "optKey", "dataKey", "v", "setNum", "float", "raw", "setJson", "e", "formatTime", "seconds", "hrs", "mins", "secs", "idCounter", "generateId", "hash", "i", "extractTitleFromUrl", "parts", "l", "perceivedBrightness", "color", "rgb", "r", "g", "b", "mergeOptions", "sources", "result", "source", "key", "debounce", "func", "wait", "timeout", "args", "later", "resampleData", "data", "targetLength", "ratio", "index", "lower", "upper", "fraction", "bucketSize", "start", "end", "count", "j", "nearestIndex", "makeFill", "ctx", "value", "height", "grad", "c", "i", "fillBar", "x", "y", "w", "h", "radii", "r", "max", "clampR", "clamp", "barRadiusPx", "options", "dpr", "barRadii", "capsulePath", "startX", "endX", "centerY", "barHeight", "drawBars", "canvas", "peaks", "progress", "barWidth", "barSpacing", "barCount", "resampledPeaks", "resampleData", "progressWidth", "baseFill", "progFill", "peakHeight", "drawMirror", "topRadii", "botRadii", "drawLine", "width", "amplitude", "drawCurve", "color", "lineWidth", "endProgress", "addGlow", "points", "samples", "peakValue", "waveOffset", "cp1x", "cp1y", "cp2x", "cp2y", "drawBlocks", "blockSize", "blockGap", "blockCount", "j", "blockOffset", "drawDots", "dotRadius", "drawSeekbar", "borderRadius", "handleRadius", "handleX", "DRAWING_STYLES", "draw", "detectBPM", "buffer", "channelData", "sampleRate", "onsets", "detectOnsets", "intervals", "i", "tempoGroups", "interval", "tempo", "bucket", "maxCount", "detectedBPM", "count", "previousEnergy", "energy", "j", "energyDiff", "threshold", "lastOnset", "minDistance", "extractPeaks", "buffer", "samples", "sampleSize", "sampleStep", "channels", "peaks", "c", "chan", "i", "start", "end", "min", "max", "j", "value", "peak", "maxPeak", "generateWaveform", "url", "shouldDetectBPM", "audioContext", "AudioCtx", "arrayBuffer", "audioBuffer", "normalizePeaks", "bpm", "detectBPM", "generatePlaceholderWaveform", "data", "base", "variation", "clamp", "targetMax", "scaleFactor", "hasThemeHint", "scheme", "root", "body", "detectColorScheme", "bodyBg", "brightness", "perceivedBrightness", "COLOR_PRESETS", "getColorPreset", "presetName", "detected", "DEFAULT_OPTIONS", "STYLE_DEFAULTS", "SEEK_STEP_SECONDS", "SEEK_PAGE_SECONDS", "WaveformPlayer", "_WaveformPlayer", "container", "options", "dataOptions", "parseDataAttributes", "userOptions", "mergeOptions", "DEFAULT_OPTIONS", "preset", "getColorPreset", "key", "value", "styleDefaults", "STYLE_DEFAULTS", "generateId", "type", "detail", "cancelable", "event", "percent", "error", "buttonAlign", "buttonHTML", "infoHTML", "rate", "escapeHtml", "speedBtn", "speedMenu", "e", "player", "hasAudio", "currentTime", "actions", "clamp", "duration", "current", "target", "seconds", "clamped", "title", "label", "formatTime", "details", "debounce", "url", "resolve", "reject", "metadataHandler", "errorHandler", "extractTitleFromUrl", "result", "generateWaveform", "generatePlaceholderWaveform", "subtitle", "data", "r", "json", "parsed", "draw", "dpr", "rect", "marker", "index", "position", "markerEl", "tooltip", "el", "i", "x", "targetPercent", "loading", "isPlaying", "playIcon", "pauseIcon", "update", "newProgress", "speedValue", "btn", "playing", "wasPlaying", "volume", "v", "clampedRate", "idOrElement", "instance", "element", "p", "samples", "audioUrl", "swapped", "WaveformPlayer", "formatTime", "extractTitleFromUrl", "escapeHtml", "isSafeHref", "parseDataAttributes", "isBrowser", "autoInit", "element", "error", "index_default"]
|
|
4
|
+
"sourcesContent": ["/**\n * @module utils\n * @description Utility functions for WaveformPlayer\n */\n\n/**\n * Escape a string for safe interpolation into HTML, preventing injection when\n * building markup with template strings. `null`/`undefined` become `''`.\n * @param {*} str - Value to escape.\n * @returns {string} HTML-escaped string.\n */\nexport function escapeHtml(str) {\n return String(str == null ? '' : str)\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n\n/**\n * Whether a URL is safe to navigate to (assign to `location.href`): allows only\n * `http`/`https` and relative URLs, rejecting `javascript:`, `data:`, `blob:`,\n * `vbscript:` and other script-bearing schemes.\n * @param {string} url - Candidate URL.\n * @returns {boolean} True if the URL uses a safe scheme.\n */\nexport function isSafeHref(url) {\n if (typeof url !== 'string' || url === '') return false;\n try {\n // Resolve relative URLs against a dummy http base; only the scheme matters.\n const u = new URL(url, 'http://localhost/');\n return u.protocol === 'http:' || u.protocol === 'https:';\n } catch (e) {\n return false;\n }\n}\n\n/**\n * Clamp a number to an inclusive range.\n * @param {number} value - Value to constrain.\n * @param {number} [min=0] - Lower bound.\n * @param {number} [max=1] - Upper bound.\n * @returns {number} `value` constrained to `[min, max]`.\n */\nexport function clamp(value, min = 0, max = 1) {\n return Math.max(min, Math.min(value, max));\n}\n\n/**\n * Read a boolean `data-*` flag. Returns `undefined` when the attribute is\n * absent (preserving the sparse-options contract) and otherwise compares the\n * raw value against the literal string `'true'`.\n * @param {string|undefined} value - Raw `dataset` value.\n * @returns {boolean|undefined} `true`/`false` when present, else `undefined`.\n */\nexport function parseBoolAttr(value) {\n return value === undefined ? undefined : value === 'true';\n}\n\n/**\n * A colour data-attribute may be a CSS colour string OR a JSON array of\n * gradient stops (e.g. '[\"#fafafa\",\"#71717a\"]'). Parse the array form;\n * otherwise pass the string straight through.\n * @param {string} value\n * @returns {string|string[]}\n */\nfunction parseColorValue(value) {\n if (typeof value === 'string' && value.trim().startsWith('[')) {\n try { return JSON.parse(value); } catch (e) { /* fall through to string */ }\n }\n return value;\n}\n\n/**\n * Read every recognised `data-*` attribute off a host element and translate it\n * into a plain options object suitable for `mergeOptions`.\n *\n * Only attributes that are actually present are copied, so the returned object\n * is sparse and never overrides defaults with `undefined`. Numeric attributes\n * are coerced with `parseInt`/`parseFloat`, boolean flags are compared against\n * the literal string `'true'`, and JSON-valued attributes (`markers`,\n * `playbackRates`) are parsed defensively \u2014 a parse failure is warned about and\n * the attribute is skipped rather than thrown.\n *\n * Several attributes are shorthand aliases of a canonical long form: `data-src`\n * \u2192 `url`, `data-style` \u2192 `waveformStyle`. When both are present the canonical\n * long form is applied last and therefore wins. `data-color` and `data-theme`\n * are retained as legacy aliases for `waveformColor` and `colorPreset`.\n * Colour attributes that accept gradients (`waveformColor`, `progressColor`)\n * are passed through {@link parseColorValue} so a JSON stop array is expanded.\n *\n * @param {HTMLElement} element - Host element whose `dataset` is inspected.\n * @returns {Object} Sparse options object containing only the attributes found.\n */\nexport function parseDataAttributes(element) {\n const options = {};\n\n // Set a boolean option only when its `data-*` attribute is present, so the\n // returned object stays sparse and never overrides a default with a value\n // the author didn't set. (`dataKey` differs from `optKey` only for showBPM.)\n const setBool = (optKey, dataKey = optKey) => {\n const v = parseBoolAttr(element.dataset[dataKey]);\n if (v !== undefined) options[optKey] = v;\n };\n\n // Read a present (non-empty) numeric attribute as an int (or float).\n const setNum = (optKey, dataKey = optKey, float = false) => {\n const raw = element.dataset[dataKey];\n if (raw) options[optKey] = float ? parseFloat(raw) : parseInt(raw, 10);\n };\n\n // Parse a JSON-valued attribute defensively \u2014 warn and skip on bad JSON.\n const setJson = (optKey, dataKey = optKey) => {\n const raw = element.dataset[dataKey];\n if (!raw) return;\n try { options[optKey] = JSON.parse(raw); }\n catch (e) { console.warn(`[WaveformPlayer] Invalid ${dataKey} JSON:`, e); }\n };\n\n // Core attributes. `data-src` is a shorthand alias for `data-url`;\n // the canonical long form wins if both are set.\n if (element.dataset.src) options.url = element.dataset.src;\n if (element.dataset.url) options.url = element.dataset.url;\n setNum('height');\n setNum('samples');\n if (element.dataset.preload) {\n options.preload = element.dataset.preload;\n }\n if (element.dataset.audioMode) options.audioMode = element.dataset.audioMode;\n\n // Waveform style attributes. `data-style` is a shorthand alias for\n // `data-waveform-style`; the canonical long form wins if both are set.\n if (element.dataset.style) options.waveformStyle = element.dataset.style;\n if (element.dataset.waveformStyle) options.waveformStyle = element.dataset.waveformStyle;\n setNum('barWidth');\n setNum('barSpacing');\n setNum('barRadius');\n if (element.dataset.buttonAlign) options.buttonAlign = element.dataset.buttonAlign;\n if (element.dataset.layout) options.layout = element.dataset.layout;\n if (element.dataset.buttonStyle) options.buttonStyle = element.dataset.buttonStyle;\n\n // Color preset\n if (element.dataset.colorPreset) options.colorPreset = element.dataset.colorPreset;\n\n // Individual color customization\n if (element.dataset.waveformColor) options.waveformColor = parseColorValue(element.dataset.waveformColor);\n if (element.dataset.progressColor) options.progressColor = parseColorValue(element.dataset.progressColor);\n if (element.dataset.buttonColor) options.buttonColor = element.dataset.buttonColor;\n if (element.dataset.buttonHoverColor) options.buttonHoverColor = element.dataset.buttonHoverColor;\n if (element.dataset.textColor) options.textColor = element.dataset.textColor;\n if (element.dataset.textSecondaryColor) options.textSecondaryColor = element.dataset.textSecondaryColor;\n if (element.dataset.backgroundColor) options.backgroundColor = element.dataset.backgroundColor;\n if (element.dataset.borderColor) options.borderColor = element.dataset.borderColor;\n\n // Legacy support for old attribute names\n if (element.dataset.color) options.waveformColor = element.dataset.color;\n if (element.dataset.theme) options.colorPreset = element.dataset.theme;\n\n // Feature flags\n setBool('autoplay');\n setBool('showControls');\n setBool('showInfo');\n setBool('showTime');\n setBool('showHoverTime');\n setBool('showBPM', 'showBpm');\n setNum('bpm');\n setBool('singlePlay');\n setBool('playOnSeek');\n\n // Content and metadata\n if (element.dataset.title) options.title = element.dataset.title;\n if (element.dataset.subtitle) options.subtitle = element.dataset.subtitle;\n if (element.dataset.album) options.album = element.dataset.album;\n if (element.dataset.artwork) options.artwork = element.dataset.artwork;\n\n // Waveform data\n if (element.dataset.waveform) options.waveform = element.dataset.waveform;\n\n // Markers\n setJson('markers');\n\n // Playback controls\n setNum('playbackRate', 'playbackRate', true);\n setBool('showPlaybackSpeed');\n setJson('playbackRates');\n\n // Media Session API\n setBool('enableMediaSession');\n\n // Markers visibility\n setBool('showMarkers');\n\n // Accessibility\n setBool('accessibleSeek');\n if (element.dataset.seekLabel) options.seekLabel = element.dataset.seekLabel;\n if (element.dataset.errorText) options.errorText = element.dataset.errorText;\n\n // Custom icons (raw SVG markup)\n if (element.dataset.playIcon) options.playIcon = element.dataset.playIcon;\n if (element.dataset.pauseIcon) options.pauseIcon = element.dataset.pauseIcon;\n\n return options;\n}\n\n/**\n * Format a duration as a clock string.\n *\n * Renders `M:SS` for durations under an hour and `H:MM:SS` for longer ones,\n * zero-padding the minutes and seconds. Falsy, `NaN`, or negative inputs are\n * treated as zero and return `'0:00'`.\n * @param {number} seconds - Time in seconds.\n * @returns {string} Formatted time, e.g. `'3:07'` or `'1:02:09'`.\n */\nexport function formatTime(seconds) {\n if (!seconds || isNaN(seconds) || seconds < 0) return '0:00';\n\n const hrs = Math.floor(seconds / 3600);\n const mins = Math.floor((seconds % 3600) / 60);\n const secs = Math.floor(seconds % 60);\n\n if (hrs > 0) {\n return `${hrs}:${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;\n }\n\n return `${mins}:${secs.toString().padStart(2, '0')}`;\n}\n\n/**\n * Monotonic per-process counter appended to every generated id to guarantee\n * uniqueness even when two ids hash from the same URL.\n * @type {number}\n * @private\n */\nlet idCounter = 0;\n\n/**\n * Generate a unique, DOM-safe ID from a URL.\n *\n * Uses a DJB2 hash of the FULL url (not a 10-char prefix) plus a process\n * counter, so same-host tracks don't collide in the instances map and\n * non-Latin1 / Unicode URLs don't throw (the old btoa() approach did both).\n * @param {string} url - Audio URL\n * @returns {string} Unique element-id-safe string\n */\nexport function generateId(url) {\n const str = url || 'audio';\n let hash = 5381;\n for (let i = 0; i < str.length; i++) {\n hash = ((hash << 5) + hash + str.charCodeAt(i)) | 0;\n }\n return `wp_${(hash >>> 0).toString(36)}_${(idCounter++).toString(36)}`;\n}\n\n/**\n * Derive a human-readable title from an audio URL's filename.\n *\n * Takes the last path segment, drops the extension, replaces `-`/`_`\n * separators with spaces, and title-cases the first letter of each word.\n * Returns `'Audio'` for an empty or missing URL.\n * @param {string} url - Audio URL.\n * @returns {string} Extracted, prettified title.\n * @example\n * extractTitleFromUrl('https://cdn.example.com/my-cool_track.mp3'); // 'My Cool Track'\n */\nexport function extractTitleFromUrl(url) {\n if (!url) return 'Audio';\n\n const parts = url.split('/');\n const filename = parts[parts.length - 1];\n const name = filename.split('.')[0];\n\n // Clean up common separators\n return name\n .replace(/[-_]/g, ' ')\n .replace(/\\b\\w/g, l => l.toUpperCase());\n}\n\n/**\n * Perceived brightness (0\u2013255) of a CSS colour, via the luminance formula.\n * Pulls the numeric channels out of an `rgb()`/`rgba()` string.\n * @param {string} color - CSS colour string, e.g. `\"rgb(34, 34, 34)\"`.\n * @returns {number|null} Brightness 0\u2013255, or `null` if it can't be parsed.\n */\nexport function perceivedBrightness(color) {\n const rgb = typeof color === 'string' ? color.match(/\\d+/g) : null;\n if (!rgb || rgb.length < 3) return null;\n const [r, g, b] = rgb.map(Number);\n return (r * 299 + g * 587 + b * 114) / 1000;\n}\n\n/**\n * Shallow-merge option objects into a new object, last source winning.\n *\n * Keys whose value is `null` or `undefined` are skipped, so a later source can\n * leave an earlier value untouched by passing a nullish entry rather than\n * clobbering it. The inputs are never mutated.\n * @param {...Object} sources - Option objects merged left-to-right.\n * @returns {Object} A fresh object containing the merged, defined keys.\n */\nexport function mergeOptions(...sources) {\n const result = {};\n\n for (const source of sources) {\n for (const key in source) {\n if (source[key] !== null && source[key] !== undefined) {\n result[key] = source[key];\n }\n }\n }\n\n return result;\n}\n\n/**\n * Wrap a function so it only runs once calls stop arriving for `wait` ms.\n *\n * Each invocation resets the pending timer, so rapid bursts collapse into a\n * single trailing-edge call that receives the most recent arguments. The\n * wrapper itself returns nothing.\n * @param {Function} func - Function to debounce.\n * @param {number} wait - Idle period in milliseconds before `func` fires.\n * @returns {Function} Debounced wrapper forwarding its arguments to `func`.\n */\nexport function debounce(func, wait) {\n let timeout;\n\n return function executedFunction(...args) {\n const later = () => {\n clearTimeout(timeout);\n func(...args);\n };\n\n clearTimeout(timeout);\n timeout = setTimeout(later, wait);\n };\n}\n\n/**\n * Resize a waveform amplitude array to a target number of bars.\n *\n * Returns the original array unchanged when lengths already match, and an empty\n * array when either side is empty. When upsampling (target larger than source)\n * it linearly interpolates between neighbouring samples for a smooth result.\n * When downsampling it splits the source into evenly sized buckets and keeps\n * the peak (maximum) of each so transients survive the reduction; an empty\n * bucket falls back to its nearest-neighbour sample.\n * @param {number[]} data - Original amplitude samples.\n * @param {number} targetLength - Desired number of output bars.\n * @returns {number[]} Resampled amplitude array of length `targetLength`.\n */\nexport function resampleData(data, targetLength) {\n if (data.length === targetLength) return data;\n if (data.length === 0 || targetLength === 0) return [];\n\n const result = [];\n\n // If upsampling (target is larger than source)\n if (targetLength > data.length) {\n const ratio = (data.length - 1) / (targetLength - 1);\n\n for (let i = 0; i < targetLength; i++) {\n const index = i * ratio;\n const lower = Math.floor(index);\n const upper = Math.ceil(index);\n const fraction = index - lower;\n\n // Linear interpolation between samples\n if (upper >= data.length) {\n result.push(data[data.length - 1]);\n } else if (lower === upper) {\n result.push(data[lower]);\n } else {\n const value = data[lower] * (1 - fraction) + data[upper] * fraction;\n result.push(value);\n }\n }\n } else {\n // Downsampling (target is smaller than source)\n const bucketSize = data.length / targetLength;\n\n for (let i = 0; i < targetLength; i++) {\n const start = Math.floor(i * bucketSize);\n const end = Math.floor((i + 1) * bucketSize);\n\n // Find the maximum value in this bucket\n let max = 0;\n let count = 0;\n\n for (let j = start; j <= end && j < data.length; j++) {\n if (data[j] > max) {\n max = data[j];\n }\n count++;\n }\n\n // If no samples were found in this bucket, use nearest neighbor\n if (count === 0) {\n const nearestIndex = Math.min(Math.round(i * bucketSize), data.length - 1);\n max = data[nearestIndex];\n }\n\n result.push(max);\n }\n }\n\n return result;\n}", "/**\n * @module drawing\n * @description Core waveform drawing styles optimized for visual distinction at all sizes\n */\n\nimport {resampleData, clamp} from './utils.js';\n\n/**\n * Resolve a fill value that may be a CSS colour string OR an array of colour\n * stops (rendered as a vertical canvas gradient). Bundle-light gradient\n * support: pass e.g. `waveformColor: ['#fafafa', '#71717a']`.\n * A single-element array collapses to that one colour; a multi-element array\n * is spread evenly from top (y=0) to bottom (y=height).\n * @private\n * @param {CanvasRenderingContext2D} ctx - Canvas context used to build the gradient.\n * @param {string|string[]} value - A CSS colour string, or an array of colour stops.\n * @param {number} height - Canvas height in device pixels (gradient span).\n * @returns {string|CanvasGradient} The original string, or a vertical linear gradient.\n */\nfunction makeFill(ctx, value, height) {\n if (!Array.isArray(value)) return value;\n if (value.length === 1) return value[0];\n const grad = ctx.createLinearGradient(0, 0, 0, height);\n value.forEach((c, i) => grad.addColorStop(i / (value.length - 1), c));\n return grad;\n}\n\n/**\n * Fill a bar rect, optionally with rounded caps (`barRadius`). Falls back to\n * a plain fillRect where `roundRect` is unavailable (older Safari) \u2014 square\n * bars, no error. Radii are clamped to half the rect's width/height so a\n * large `barRadius` never overflows a thin or short bar.\n * @private\n * @param {CanvasRenderingContext2D} ctx - Canvas context (current fillStyle is used).\n * @param {number} x - Left edge of the bar in device pixels.\n * @param {number} y - Top edge of the bar in device pixels.\n * @param {number} w - Bar width in device pixels.\n * @param {number} h - Bar height in device pixels (may be negative for upward fills).\n * @param {number|number[]} radii - Corner radius (number, or [tl, tr, br, bl]).\n * @returns {void}\n */\nfunction fillBar(ctx, x, y, w, h, radii) {\n const any = Array.isArray(radii) ? radii.some(r => r > 0) : radii > 0;\n if (any && typeof ctx.roundRect === 'function') {\n const max = Math.min(w / 2, Math.abs(h) / 2);\n const clampR = (r) => clamp(r, 0, max);\n ctx.beginPath();\n ctx.roundRect(x, y, w, h, Array.isArray(radii) ? radii.map(clampR) : clampR(radii));\n ctx.fill();\n } else {\n ctx.fillRect(x, y, w, h);\n }\n}\n\n/**\n * Scale the configured `barRadius` into device pixels (scalar).\n * @private\n * @param {Object} options - Drawing options (`barRadius` in CSS pixels, defaults to 0).\n * @param {number} dpr - Device pixel ratio multiplier.\n * @returns {number} The bar corner radius in device pixels.\n */\nfunction barRadiusPx(options, dpr) {\n return (options.barRadius || 0) * dpr;\n}\n\n/**\n * Top-rounded corner radii for bottom-anchored bars: [tl, tr, br, bl].\n * Only the top two corners are rounded so bars sit flush on the baseline.\n * @private\n * @param {Object} options - Drawing options (supplies `barRadius`).\n * @param {number} dpr - Device pixel ratio multiplier.\n * @returns {number[]} Corner radii in device pixels as [tl, tr, br, bl].\n */\nfunction barRadii(options, dpr) {\n const r = barRadiusPx(options, dpr);\n return [r, r, 0, 0];\n}\n\n/**\n * Trace a horizontal rounded-capsule (stadium) path from `startX` to `endX`,\n * ready to fill. The end caps are semicircles of radius `barHeight / 2`.\n * @private\n * @param {CanvasRenderingContext2D} ctx - Canvas context.\n * @param {number} startX - Left edge x (also the left cap centre).\n * @param {number} endX - Right edge x.\n * @param {number} centerY - Vertical centre of the capsule.\n * @param {number} barHeight - Capsule thickness in pixels.\n * @returns {void}\n */\nfunction capsulePath(ctx, startX, endX, centerY, barHeight) {\n const r = barHeight / 2;\n ctx.beginPath();\n ctx.moveTo(startX, centerY - r);\n ctx.lineTo(endX - r, centerY - r);\n ctx.arc(endX - r, centerY, r, -Math.PI / 2, Math.PI / 2);\n ctx.lineTo(startX, centerY + r);\n ctx.arc(startX, centerY, r, Math.PI / 2, -Math.PI / 2);\n ctx.closePath();\n}\n\n/**\n * Draw standard bars waveform - classic vertical bars anchored to the baseline.\n * Peaks are resampled to fit the available bar slots, drawn at 90% of canvas\n * height, then the progress portion is repainted in `progressColor` via a\n * left-anchored clip rect.\n * @param {CanvasRenderingContext2D} ctx - Canvas 2D context to draw into.\n * @param {HTMLCanvasElement} canvas - Canvas element (provides device-pixel dimensions).\n * @param {number[]} peaks - Normalised waveform peak values (0-1).\n * @param {number} progress - Playback progress (0-1) that drives the colour overlay.\n * @param {Object} options - Drawing options: `barWidth`, `barSpacing`, `barRadius`,\n * `color`, `progressColor` (colour strings or gradient stop arrays).\n * @returns {void}\n */\nexport function drawBars(ctx, canvas, peaks, progress, options) {\n const dpr = window.devicePixelRatio || 1;\n const barWidth = options.barWidth * dpr;\n const barSpacing = options.barSpacing * dpr;\n const barCount = Math.floor(canvas.width / (barWidth + barSpacing));\n const resampledPeaks = resampleData(peaks, barCount);\n const height = canvas.height;\n const progressWidth = progress * canvas.width;\n const radii = barRadii(options, dpr);\n const baseFill = makeFill(ctx, options.color, height);\n const progFill = makeFill(ctx, options.progressColor, height);\n\n ctx.clearRect(0, 0, canvas.width, canvas.height);\n\n // Draw all bars first\n ctx.fillStyle = baseFill;\n for (let i = 0; i < resampledPeaks.length; i++) {\n const x = i * (barWidth + barSpacing);\n if (x + barWidth > canvas.width) break;\n\n const peakHeight = resampledPeaks[i] * height * 0.9;\n // Draw from bottom up, not centered\n const y = height - peakHeight;\n\n fillBar(ctx, x, y, barWidth, peakHeight, radii);\n }\n\n // Progress overlay\n ctx.save();\n ctx.beginPath();\n ctx.rect(0, 0, progressWidth, height);\n ctx.clip();\n\n ctx.fillStyle = progFill;\n for (let i = 0; i < resampledPeaks.length; i++) {\n const x = i * (barWidth + barSpacing);\n if (x > progressWidth) break;\n\n const peakHeight = resampledPeaks[i] * height * 0.9;\n // Draw from bottom up, not centered\n const y = height - peakHeight;\n\n fillBar(ctx, x, y, barWidth, peakHeight, radii);\n }\n\n ctx.restore();\n}\n\n/**\n * Draw mirror/SoundCloud style waveform - symmetrical bars about the centre line.\n * Each peak is drawn twice (45% of height up and down) with the upper cap rounded\n * on top and the lower cap rounded on the bottom; the progress portion is then\n * repainted in `progressColor` through a left-anchored clip rect.\n * @param {CanvasRenderingContext2D} ctx - Canvas 2D context to draw into.\n * @param {HTMLCanvasElement} canvas - Canvas element (provides device-pixel dimensions).\n * @param {number[]} peaks - Normalised waveform peak values (0-1).\n * @param {number} progress - Playback progress (0-1) that drives the colour overlay.\n * @param {Object} options - Drawing options: `barWidth`, `barSpacing`, `barRadius`,\n * `color`, `progressColor`.\n * @returns {void}\n */\nexport function drawMirror(ctx, canvas, peaks, progress, options) {\n const dpr = window.devicePixelRatio || 1;\n const barWidth = options.barWidth * dpr;\n const barSpacing = options.barSpacing * dpr;\n const barCount = Math.floor(canvas.width / (barWidth + barSpacing));\n const resampledPeaks = resampleData(peaks, barCount);\n const height = canvas.height;\n const centerY = height / 2;\n const progressWidth = progress * canvas.width;\n const r = barRadiusPx(options, dpr);\n const topRadii = [r, r, 0, 0]; // round the upper cap\n const botRadii = [0, 0, r, r]; // round the lower cap\n const baseFill = makeFill(ctx, options.color, height);\n const progFill = makeFill(ctx, options.progressColor, height);\n\n ctx.clearRect(0, 0, canvas.width, canvas.height);\n\n // Draw all bars\n ctx.fillStyle = baseFill;\n for (let i = 0; i < resampledPeaks.length; i++) {\n const x = i * (barWidth + barSpacing);\n if (x + barWidth > canvas.width) break;\n\n const peakHeight = resampledPeaks[i] * height * 0.45;\n\n fillBar(ctx, x, centerY - peakHeight, barWidth, peakHeight, topRadii);\n fillBar(ctx, x, centerY, barWidth, peakHeight, botRadii);\n }\n\n // Progress overlay\n ctx.save();\n ctx.beginPath();\n ctx.rect(0, 0, progressWidth, height);\n ctx.clip();\n\n ctx.fillStyle = progFill;\n for (let i = 0; i < resampledPeaks.length; i++) {\n const x = i * (barWidth + barSpacing);\n if (x > progressWidth) break;\n\n const peakHeight = resampledPeaks[i] * height * 0.45;\n\n fillBar(ctx, x, centerY - peakHeight, barWidth, peakHeight, topRadii);\n fillBar(ctx, x, centerY, barWidth, peakHeight, botRadii);\n }\n\n ctx.restore();\n}\n\n/**\n * Draw line/oscilloscope style waveform - smooth flowing wave with glow.\n * Renders a faint oscilloscope grid (centre line + 10 vertical divisions), the\n * full waveform as a bezier-smoothed curve, then the played portion on top with\n * a coloured shadow glow. Peaks are modulated by a sine term so the line undulates\n * rather than reading as static bars.\n * @param {CanvasRenderingContext2D} ctx - Canvas 2D context to draw into.\n * @param {HTMLCanvasElement} canvas - Canvas element (provides device-pixel dimensions).\n * @param {number[]} peaks - Normalised waveform peak values (0-1).\n * @param {number} progress - Playback progress (0-1); the glowing curve is only drawn when > 0.\n * @param {Object} options - Drawing options: `color` (base wave), `progressColor` (played wave).\n * @returns {void}\n */\nexport function drawLine(ctx, canvas, peaks, progress, options) {\n const width = canvas.width;\n const height = canvas.height;\n const centerY = height / 2;\n const amplitude = height * 0.35;\n\n ctx.clearRect(0, 0, width, height);\n\n /**\n * Stroke a bezier-smoothed curve through the (optionally sine-modulated) peaks.\n * @private\n * @param {string} color - Stroke colour (and shadow colour when glowing).\n * @param {number} lineWidth - Stroke width in pixels.\n * @param {number} [endProgress=1] - Fraction (0-1) of the peaks to draw, left to right.\n * @param {boolean} [addGlow=false] - When true, applies a coloured shadow blur for a glow effect.\n * @returns {void}\n */\n const drawCurve = (color, lineWidth, endProgress = 1, addGlow = false) => {\n if (addGlow) {\n ctx.shadowBlur = 12;\n ctx.shadowColor = color;\n }\n\n ctx.strokeStyle = color;\n ctx.lineWidth = lineWidth;\n ctx.lineCap = 'round';\n ctx.lineJoin = 'round';\n\n ctx.beginPath();\n ctx.moveTo(0, centerY);\n\n const points = [];\n const samples = Math.floor(peaks.length * endProgress);\n\n // Calculate smoothed points\n for (let i = 0; i < samples; i++) {\n const x = (i / (peaks.length - 1)) * width;\n const peakValue = peaks[i];\n\n // Create a smooth wave motion\n const waveOffset = Math.sin(i * 0.1) * peakValue;\n const y = centerY + (waveOffset * amplitude);\n\n points.push({x, y});\n }\n\n // Draw smooth curve through points using bezier curves\n for (let i = 0; i < points.length - 1; i++) {\n const cp1x = points[i].x + (points[i + 1].x - points[i].x) * 0.5;\n const cp1y = points[i].y;\n const cp2x = points[i + 1].x - (points[i + 1].x - points[i].x) * 0.5;\n const cp2y = points[i + 1].y;\n\n ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, points[i + 1].x, points[i + 1].y);\n }\n\n ctx.stroke();\n\n if (addGlow) {\n ctx.shadowBlur = 0;\n }\n };\n\n // Draw subtle grid for oscilloscope feel\n ctx.strokeStyle = 'rgba(255, 255, 255, 0.03)';\n ctx.lineWidth = 0.5;\n\n // Horizontal center line\n ctx.beginPath();\n ctx.moveTo(0, centerY);\n ctx.lineTo(width, centerY);\n ctx.stroke();\n\n // Vertical grid lines\n for (let i = 0; i <= 10; i++) {\n const x = (width / 10) * i;\n ctx.beginPath();\n ctx.moveTo(x, 0);\n ctx.lineTo(x, height);\n ctx.stroke();\n }\n\n // Draw background wave\n drawCurve(options.color, 2, 1, false);\n\n // Draw progress with glow\n if (progress > 0) {\n drawCurve(options.progressColor, 3, progress, true);\n }\n}\n\n/**\n * Draw blocks/LED meter style waveform - segmented blocks growing from the centre.\n * Each bar's height is quantised into fixed-size blocks separated by gaps, drawn\n * symmetrically up and down from the centre line (the shared centre block is not\n * duplicated downward). Per-bar colour is chosen by comparing the bar's x against\n * the played width \u2014 there is no clip overlay here.\n * @param {CanvasRenderingContext2D} ctx - Canvas 2D context to draw into.\n * @param {HTMLCanvasElement} canvas - Canvas element (provides device-pixel dimensions).\n * @param {number[]} peaks - Normalised waveform peak values (0-1).\n * @param {number} progress - Playback progress (0-1) used to pick each bar's colour.\n * @param {Object} options - Drawing options: `barWidth` (default 3), `barSpacing` (default 1),\n * `color`, `progressColor`.\n * @returns {void}\n */\nexport function drawBlocks(ctx, canvas, peaks, progress, options) {\n const dpr = window.devicePixelRatio || 1;\n const barWidth = (options.barWidth || 3) * dpr;\n const barSpacing = (options.barSpacing || 1) * dpr;\n const barCount = Math.floor(canvas.width / (barWidth + barSpacing));\n const resampledPeaks = resampleData(peaks, barCount);\n const height = canvas.height;\n const blockSize = 4 * dpr;\n const blockGap = 2 * dpr;\n const progressWidth = progress * canvas.width;\n const centerY = height / 2;\n const baseFill = makeFill(ctx, options.color, height);\n const progFill = makeFill(ctx, options.progressColor, height);\n\n ctx.clearRect(0, 0, canvas.width, canvas.height);\n\n for (let i = 0; i < resampledPeaks.length; i++) {\n const x = i * (barWidth + barSpacing);\n if (x + barWidth > canvas.width) break;\n\n const peakHeight = resampledPeaks[i] * height * 0.9;\n const blockCount = Math.floor(peakHeight / (blockSize + blockGap));\n\n ctx.fillStyle = x < progressWidth ? progFill : baseFill;\n\n // Draw blocks from center outward\n for (let j = 0; j < blockCount; j++) {\n const blockOffset = j * (blockSize + blockGap);\n\n // Upper blocks\n ctx.fillRect(x, centerY - blockOffset - blockSize, barWidth, blockSize);\n\n // Lower blocks (skip the center block)\n if (j > 0) {\n ctx.fillRect(x, centerY + blockOffset, barWidth, blockSize);\n }\n }\n }\n}\n\n/**\n * Draw dots style waveform - pairs of circular points mirrored about the centre.\n * For each sample a dot is drawn above and below the centre line at half the peak\n * height; dot radius scales with bar width but is floored at 1.5 device pixels.\n * Per-dot colour is chosen by comparing x against the played width (no clip overlay).\n * @param {CanvasRenderingContext2D} ctx - Canvas 2D context to draw into.\n * @param {HTMLCanvasElement} canvas - Canvas element (provides device-pixel dimensions).\n * @param {number[]} peaks - Normalised waveform peak values (0-1).\n * @param {number} progress - Playback progress (0-1) used to pick each dot's colour.\n * @param {Object} options - Drawing options: `barWidth` (default 2), `barSpacing` (default 3),\n * `color`, `progressColor`.\n * @returns {void}\n */\nexport function drawDots(ctx, canvas, peaks, progress, options) {\n const dpr = window.devicePixelRatio || 1;\n const barWidth = (options.barWidth || 2) * dpr;\n const barSpacing = (options.barSpacing || 3) * dpr;\n const barCount = Math.floor(canvas.width / (barWidth + barSpacing));\n const resampledPeaks = resampleData(peaks, barCount);\n const height = canvas.height;\n const dotRadius = Math.max(1.5 * dpr, barWidth / 2);\n const progressWidth = progress * canvas.width;\n const centerY = height / 2;\n const baseFill = makeFill(ctx, options.color, height);\n const progFill = makeFill(ctx, options.progressColor, height);\n\n ctx.clearRect(0, 0, canvas.width, canvas.height);\n\n for (let i = 0; i < resampledPeaks.length; i++) {\n const x = i * (barWidth + barSpacing) + barWidth / 2;\n if (x > canvas.width) break;\n\n const peakHeight = resampledPeaks[i] * height * 0.9;\n\n ctx.fillStyle = x < progressWidth ? progFill : baseFill;\n\n // Draw upper dot\n ctx.beginPath();\n ctx.arc(x, centerY - peakHeight / 2, dotRadius, 0, Math.PI * 2);\n ctx.fill();\n\n // Draw lower dot\n ctx.beginPath();\n ctx.arc(x, centerY + peakHeight / 2, dotRadius, 0, Math.PI * 2);\n ctx.fill();\n }\n}\n\n/**\n * Draw seekbar style - a simple rounded progress bar with no waveform.\n * Renders a pill-shaped background track, a glowing pill-shaped filled portion\n * (clamped to at least one full pill width so it never collapses), and a draggable\n * circular handle/thumb at the playhead with a drop shadow and inner accent dot.\n * The `peaks` argument is accepted for signature parity but is unused by this style.\n * @param {CanvasRenderingContext2D} ctx - Canvas 2D context to draw into.\n * @param {HTMLCanvasElement} canvas - Canvas element (provides device-pixel dimensions).\n * @param {number[]} peaks - Ignored; present to match the shared draw-function signature.\n * @param {number} progress - Playback progress (0-1); the fill and handle are only drawn when > 0.\n * @param {Object} options - Drawing options: `color` (track), `progressColor` (fill/glow/accent).\n * @returns {void}\n */\nexport function drawSeekbar(ctx, canvas, peaks, progress, options) {\n const width = canvas.width;\n const height = canvas.height;\n const centerY = height / 2;\n const barHeight = 4; // Height of the seekbar in pixels\n const borderRadius = barHeight / 2;\n\n ctx.clearRect(0, 0, width, height);\n\n // Draw background track\n ctx.fillStyle = options.color || 'rgba(255, 255, 255, 0.2)';\n\n // Rounded background track\n capsulePath(ctx, borderRadius, width, centerY, barHeight);\n ctx.fill();\n\n // Draw progress\n if (progress > 0) {\n const progressWidth = Math.max(borderRadius * 2, progress * width);\n\n // Add subtle glow effect\n ctx.shadowBlur = 8;\n ctx.shadowColor = options.progressColor;\n\n ctx.fillStyle = options.progressColor || 'rgba(255, 255, 255, 0.9)';\n\n // Rounded progress fill\n capsulePath(ctx, borderRadius, progressWidth, centerY, barHeight);\n ctx.fill();\n\n ctx.shadowBlur = 0;\n\n // Draw progress handle/thumb\n const handleRadius = 8;\n const handleX = progressWidth;\n\n // Handle shadow\n ctx.shadowBlur = 4;\n ctx.shadowColor = 'rgba(0, 0, 0, 0.3)';\n ctx.shadowOffsetY = 2;\n\n // Handle circle\n ctx.fillStyle = '#ffffff';\n ctx.beginPath();\n ctx.arc(handleX, centerY, handleRadius, 0, Math.PI * 2);\n ctx.fill();\n\n // Handle inner circle (for depth)\n ctx.shadowBlur = 0;\n ctx.shadowOffsetY = 0;\n ctx.fillStyle = options.progressColor || 'rgba(255, 255, 255, 0.9)';\n ctx.beginPath();\n ctx.arc(handleX, centerY, handleRadius * 0.4, 0, Math.PI * 2);\n ctx.fill();\n }\n}\n\n/**\n * Map of style names (and singular aliases) to their drawing functions.\n * Six visually distinct styles including a simple seekbar; keys are matched\n * against `options.waveformStyle` by {@link draw}.\n * @type {Object.<string, function(CanvasRenderingContext2D, HTMLCanvasElement, number[], number, Object): void>}\n */\nexport const DRAWING_STYLES = {\n 'bars': drawBars, // Classic vertical bars\n 'bar': drawBars,\n 'mirror': drawMirror, // SoundCloud-style symmetrical\n 'line': drawLine, // Smooth oscilloscope wave\n 'blocks': drawBlocks, // LED meter segmented\n 'block': drawBlocks,\n 'dots': drawDots, // Circular points\n 'dot': drawDots,\n 'seekbar': drawSeekbar // Simple progress bar (no waveform)\n};\n\n/**\n * Main drawing entry point that delegates to the style named by\n * `options.waveformStyle`, falling back to {@link drawBars} for unknown styles.\n * @param {CanvasRenderingContext2D} ctx - Canvas context\n * @param {HTMLCanvasElement} canvas - Canvas element\n * @param {number[]} peaks - Waveform peak data (0-1)\n * @param {number} progress - Progress (0-1)\n * @param {Object} options - Drawing options, including `waveformStyle` plus the\n * per-style fields (`barWidth`, `barSpacing`, `barRadius`, `color`, `progressColor`).\n * @returns {void}\n */\nexport function draw(ctx, canvas, peaks, progress, options) {\n const drawFunc = DRAWING_STYLES[options.waveformStyle] || drawBars;\n drawFunc(ctx, canvas, peaks, progress, options);\n}", "/**\n * @module bpm\n * @description BPM detection for audio analysis\n */\n\n/**\n * Estimate the tempo (beats per minute) of an audio buffer.\n *\n * Analyses the first (left/mono) channel by detecting onsets, measuring the\n * time between successive onsets, converting each interval to a tempo, and\n * histogramming those tempos into 3-BPM buckets (60-200 BPM) to find the most\n * common one. Octave errors are corrected by doubling very slow results and\n * halving very fast ones when a strong half/double bucket also exists, then a\n * fixed -1 BPM calibration offset is applied. Returns a 120 BPM fallback when\n * too few onsets are found, and null if analysis throws.\n *\n * @param {AudioBuffer} buffer - Decoded audio buffer to analyse; only channel 0 is read.\n * @returns {number|null} Detected tempo in BPM, 120 as a fallback when onsets are insufficient, or null on error.\n */\nexport function detectBPM(buffer) {\n try {\n const channelData = buffer.getChannelData(0);\n const sampleRate = buffer.sampleRate;\n const onsets = detectOnsets(channelData, sampleRate);\n\n if (onsets.length < 2) return 120;\n\n // Calculate intervals\n const intervals = [];\n for (let i = 1; i < onsets.length; i++) {\n intervals.push((onsets[i] - onsets[i - 1]) / sampleRate);\n }\n\n // Convert to tempos and group\n const tempoGroups = {};\n intervals.forEach(interval => {\n const tempo = 60 / interval;\n const bucket = Math.round(tempo / 3) * 3;\n if (bucket > 60 && bucket < 200) {\n tempoGroups[bucket] = (tempoGroups[bucket] || 0) + 1;\n }\n });\n\n // Find most common\n let maxCount = 0;\n let detectedBPM = 120;\n for (const [tempo, count] of Object.entries(tempoGroups)) {\n if (count > maxCount) {\n maxCount = count;\n detectedBPM = parseInt(tempo);\n }\n }\n\n // Handle tempo ambiguity\n if (detectedBPM < 70 && tempoGroups[detectedBPM * 2]) {\n detectedBPM *= 2;\n } else if (detectedBPM > 160 && tempoGroups[Math.round(detectedBPM / 2)]) {\n detectedBPM = Math.round(detectedBPM / 2);\n }\n\n return detectedBPM - 1; // Calibration offset\n } catch (e) {\n console.warn('[WaveformPlayer] BPM detection failed:', e);\n return null;\n }\n}\n\n/**\n * Detect onset sample positions (transients/beats) within a channel of audio.\n *\n * Slides a 2048-sample window (50% overlap via a half-window hop) across the\n * signal, computing the mean squared energy of each window. An onset is flagged\n * when the energy rise over the previous (smoothed) energy exceeds an adaptive\n * threshold and the window energy is above a noise floor, subject to a minimum\n * spacing of 150 ms so a single transient is not counted twice. The running\n * previousEnergy is exponentially smoothed (0.8 new / 0.2 old) to track the\n * local energy envelope.\n *\n * @param {Float32Array} channelData - PCM samples (normalised -1..1) for a single channel.\n * @param {number} sampleRate - Sample rate in Hz, used to derive the minimum onset spacing.\n * @returns {number[]} Ascending sample indices at which onsets were detected.\n * @private\n */\nfunction detectOnsets(channelData, sampleRate) {\n const windowSize = 2048;\n const hopSize = windowSize / 2;\n const onsets = [];\n let previousEnergy = 0;\n\n for (let i = 0; i < channelData.length - windowSize; i += hopSize) {\n let energy = 0;\n for (let j = i; j < i + windowSize; j++) {\n energy += channelData[j] * channelData[j];\n }\n energy = energy / windowSize;\n\n const energyDiff = energy - previousEnergy;\n const threshold = previousEnergy * 1.8 + 0.01;\n\n if (energyDiff > threshold && energy > 0.01) {\n const lastOnset = onsets[onsets.length - 1] || 0;\n const minDistance = sampleRate * 0.15;\n\n if (i - lastOnset > minDistance) {\n onsets.push(i);\n }\n }\n\n previousEnergy = energy * 0.8 + previousEnergy * 0.2;\n }\n\n return onsets;\n}", "/**\n * @module audio\n * @description Audio processing for WaveformPlayer\n */\n\nimport {detectBPM} from './bpm.js';\nimport {clamp} from './utils.js';\n\n/**\n * Extract peaks from a decoded audio buffer for waveform visualization.\n *\n * Divides the buffer into `samples` equal-width windows and, within each\n * window, finds the largest absolute amplitude. To keep large files fast the\n * inner loop strides through every 10th frame (`sampleStep`) rather than\n * inspecting every frame. Across multiple channels the per-window peaks are\n * merged by taking the loudest channel, then the whole array is normalized so\n * the maximum peak becomes 1 (a silent buffer is returned unscaled).\n *\n * @param {AudioBuffer} buffer - Decoded audio buffer to analyse.\n * @param {number} [samples=200] - Number of peak windows (output array length).\n * @returns {number[]} Array of `samples` normalized peak values in the 0-1 range.\n */\nexport function extractPeaks(buffer, samples = 200) {\n const sampleSize = buffer.length / samples;\n const sampleStep = ~~(sampleSize / 10) || 1;\n const channels = buffer.numberOfChannels;\n const peaks = [];\n\n for (let c = 0; c < channels; c++) {\n const chan = buffer.getChannelData(c);\n\n for (let i = 0; i < samples; i++) {\n const start = ~~(i * sampleSize);\n const end = ~~(start + sampleSize);\n\n let min = 0;\n let max = 0;\n\n for (let j = start; j < end; j += sampleStep) {\n const value = chan[j];\n if (value > max) max = value;\n if (value < min) min = value;\n }\n\n const peak = Math.max(Math.abs(max), Math.abs(min));\n\n if (c === 0 || peak > peaks[i]) {\n peaks[i] = peak;\n }\n }\n }\n\n // Normalize peaks\n const maxPeak = Math.max(...peaks);\n return maxPeak > 0 ? peaks.map(peak => peak / maxPeak) : peaks;\n}\n\n/**\n * Generate waveform data by fetching and decoding an audio file at a URL.\n *\n * Fetches the URL, decodes it through a short-lived AudioContext, runs\n * {@link extractPeaks} followed by {@link normalizePeaks}, and optionally\n * detects the track's BPM. The AudioContext is created lazily and always\n * closed in the `finally` block so failed decodes never leak one (browsers\n * hard-cap the number of live contexts). Errors are logged and re-thrown so\n * callers can fall back to a placeholder waveform.\n *\n * @param {string} url - Audio file URL to fetch and decode.\n * @param {number} [samples=200] - Number of peak windows to extract.\n * @param {boolean} [shouldDetectBPM=false] - Whether to run BPM detection on the decoded buffer.\n * @returns {Promise<{peaks: number[], bpm: (number|null)}>} Resolves with the\n * normalized peaks and the detected BPM (`null` when detection is disabled or fails).\n * @throws {Error} Re-throws any fetch/decode error after logging it.\n */\nexport async function generateWaveform(url, samples = 200, shouldDetectBPM = false) {\n // Created lazily so the finally block can always close it \u2014 browsers\n // hard-cap live AudioContexts (~6 in Chrome), so leaking one per failed\n // decode would break every subsequent player on the page.\n let audioContext;\n try {\n const AudioCtx = window.AudioContext || /** @type {any} */ (window).webkitAudioContext;\n audioContext = new AudioCtx();\n const response = await fetch(url);\n const arrayBuffer = await response.arrayBuffer();\n const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);\n\n let peaks = extractPeaks(audioBuffer, samples);\n\n // Normalize peaks for consistent visualization\n peaks = normalizePeaks(peaks);\n\n let bpm = null;\n if (shouldDetectBPM) {\n bpm = detectBPM(audioBuffer); // synchronous \u2014 returns number|null\n }\n\n return {peaks, bpm};\n } finally {\n // Error (if any) propagates to the caller, which decides how to log /\n // recover; the context is always closed either way.\n if (audioContext) audioContext.close();\n }\n}\n\n/**\n * Generate a synthetic placeholder waveform for use before (or instead of)\n * real peak data is available.\n *\n * Each bar combines a random base height (0.3-0.8) with a slow sinusoidal\n * variation across the array, clamped to the 0.1-1 range so the result always\n * looks like a plausible waveform rather than pure noise.\n *\n * @param {number} [samples=200] - Number of bars (output array length).\n * @returns {number[]} Array of `samples` pseudo-random peak values in the 0.1-1 range.\n */\nexport function generatePlaceholderWaveform(samples = 200) {\n const data = [];\n for (let i = 0; i < samples; i++) {\n const base = Math.random() * 0.5 + 0.3;\n const variation = Math.sin(i / samples * Math.PI * 4) * 0.2;\n data.push(clamp(base + variation, 0.1, 1));\n }\n return data;\n}\n\n/**\n * Scale peak values so quiet tracks fill the available height consistently.\n *\n * Finds the loudest peak and, only when it is non-zero yet below `targetMax`,\n * scales every peak proportionally so the maximum lands on `targetMax`. Silent\n * arrays (max 0) and already-loud arrays (max above `targetMax`) are returned\n * untouched, so the function never amplifies clipping or divides by zero.\n *\n * @param {number[]} peaks - Peak values, typically in the 0-1 range.\n * @param {number} [targetMax=0.95] - Desired maximum peak after scaling.\n * @returns {number[]} The normalized peak array (the original array when no scaling is applied).\n * @private\n */\nfunction normalizePeaks(peaks, targetMax = 0.95) {\n const maxPeak = Math.max(...peaks);\n\n // Don't normalize if already loud enough or silent\n if (maxPeak === 0 || maxPeak > targetMax) return peaks;\n\n // Scale all peaks proportionally\n const scaleFactor = targetMax / maxPeak;\n return peaks.map(peak => peak * scaleFactor);\n}", "/**\n * @module themes\n * @description Color presets and default options for WaveformPlayer\n */\n\nimport {perceivedBrightness} from './utils.js';\n\n/**\n * Does `<html>` or `<body>` explicitly signal the given colour scheme via a\n * known class name (`dark`, `dark-mode`, `theme-dark`) or theme attribute\n * (`data-theme`, and `data-color-scheme` on the root)?\n * @param {'dark'|'light'} scheme - Scheme to look for.\n * @returns {boolean} True if the page explicitly hints at `scheme`.\n * @private\n */\nfunction hasThemeHint(scheme) {\n const root = document.documentElement;\n const body = document.body;\n return (\n root.classList.contains(scheme) ||\n root.classList.contains(`${scheme}-mode`) ||\n root.classList.contains(`theme-${scheme}`) ||\n root.getAttribute('data-theme') === scheme ||\n root.getAttribute('data-color-scheme') === scheme ||\n body.classList.contains(scheme) ||\n body.classList.contains(`${scheme}-mode`) ||\n body.getAttribute('data-theme') === scheme\n );\n}\n\n/**\n * Detect the appropriate color scheme for the player from the surrounding page.\n *\n * Resolution order, first match wins:\n * 1. Explicit theme hints on `<html>`/`<body>` \u2014 class names\n * (`dark`, `dark-mode`, `theme-dark`, light equivalents) and data\n * attributes (`data-theme`, `data-color-scheme`).\n * 2. The page's computed `<body>` background colour, classified via\n * {@link perceivedBrightness} (>128 = light, <128 = dark; exactly 128\n * or unparseable is treated as ambiguous and falls through).\n * 3. The OS/browser `prefers-color-scheme` media query.\n * 4. Default fallback of `'dark'` (most audio players are dark).\n *\n * @returns {string} The detected scheme, either `'dark'` or `'light'`.\n */\nexport function detectColorScheme() {\n // 1. Explicit theme class names / data attributes win.\n if (hasThemeHint('dark')) return 'dark';\n if (hasThemeHint('light')) return 'light';\n\n // 2. Try to detect website's theme from background color\n try {\n const bodyBg = getComputedStyle(document.body).backgroundColor;\n const brightness = perceivedBrightness(bodyBg);\n\n // Clear determination: bright background = light theme. Exactly 128\n // (or unparseable) is ambiguous \u2014 fall through to the next method.\n if (brightness !== null) {\n if (brightness > 128) return 'light';\n if (brightness < 128) return 'dark';\n }\n } catch (e) {\n // If background detection fails, continue to next method\n }\n\n // 3. Check system preference\n if (window.matchMedia) {\n if (window.matchMedia('(prefers-color-scheme: dark)').matches) {\n return 'dark';\n }\n if (window.matchMedia('(prefers-color-scheme: light)').matches) {\n return 'light';\n }\n }\n\n // 4. Default fallback (most audio players are dark)\n return 'dark';\n}\n\n/**\n * Built-in colour presets keyed by scheme name.\n *\n * Each preset is a flat map of the player's themeable colour tokens\n * (waveform, progress, button, text, background, border). They are deliberately\n * simple translucent black/white values so they sit on any host background, and\n * any individual token can be overridden per-instance via the matching\n * `*Color` option in {@link DEFAULT_OPTIONS}.\n *\n * @type {Object<string, Object<string, string>>}\n * @property {Object<string, string>} dark Light-on-dark token set.\n * @property {Object<string, string>} light Dark-on-light token set.\n */\nexport const COLOR_PRESETS = {\n dark: {\n waveformColor: 'rgba(255, 255, 255, 0.3)',\n progressColor: 'rgba(255, 255, 255, 0.9)',\n buttonColor: 'rgba(255, 255, 255, 0.9)',\n buttonHoverColor: 'rgba(255, 255, 255, 1)',\n textColor: '#ffffff',\n textSecondaryColor: 'rgba(255, 255, 255, 0.6)',\n backgroundColor: 'rgba(255, 255, 255, 0.03)',\n borderColor: 'rgba(255, 255, 255, 0.1)'\n },\n light: {\n waveformColor: 'rgba(0, 0, 0, 0.2)',\n progressColor: 'rgba(0, 0, 0, 0.8)',\n buttonColor: 'rgba(0, 0, 0, 0.8)',\n buttonHoverColor: 'rgba(0, 0, 0, 0.9)',\n textColor: '#333333',\n textSecondaryColor: 'rgba(0, 0, 0, 0.6)',\n backgroundColor: 'rgba(0, 0, 0, 0.02)',\n borderColor: 'rgba(0, 0, 0, 0.1)'\n }\n};\n\n/**\n * Resolve a colour preset by name, falling back to auto-detection.\n *\n * When `presetName` names a known preset it is returned as-is; otherwise\n * (null, undefined, or an unrecognised name) the scheme is auto-detected via\n * {@link detectColorScheme} and the corresponding preset is returned.\n *\n * @param {string|null} presetName - Preset name (`'dark'` or `'light'`), or\n * null/invalid to trigger auto-detection.\n * @returns {Object<string, string>} The matching colour token map from\n * {@link COLOR_PRESETS}.\n */\nexport function getColorPreset(presetName) {\n // If explicitly set to a valid preset, use it\n if (presetName && COLOR_PRESETS[presetName]) {\n return COLOR_PRESETS[presetName];\n }\n\n // Auto-detect if not specified or invalid\n const detected = detectColorScheme();\n return COLOR_PRESETS[detected];\n}\n\n/**\n * Default option set for a {@link WaveformPlayer} instance.\n *\n * User-supplied options are merged over this object, so every supported option\n * is enumerated here with its baseline value. `null` colour tokens mean \"inherit\n * from the resolved {@link COLOR_PRESETS} preset\"; `null` content/callback\n * fields mean \"unset\". See the grouped inline comments for per-field notes,\n * notably the `audioMode` self/external distinction and the `accessibleSeek`\n * keyboard slider.\n *\n * @type {Object}\n */\nexport const DEFAULT_OPTIONS = {\n // Core settings\n url: '',\n height: 64,\n // Source peak resolution. The drawer resamples these to fit\n // canvasWidth / (barWidth + barSpacing) bars, so this is fidelity headroom,\n // not the visible bar count.\n samples: 256,\n preload: 'metadata',\n\n // Audio mode \u2014 'self' = player owns the <audio> element (default, current\n // behavior). 'external' = player is a visualization-only surface; no audio\n // element is created, play() dispatches `waveformplayer:request-play`\n // instead of calling audio.play(), and setPlayingState/setProgress are\n // expected to be driven by an external controller (e.g. WaveformBar).\n audioMode: 'self',\n\n // Playback\n playbackRate: 1,\n showPlaybackSpeed: false,\n playbackRates: [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2],\n\n // Layout Options\n buttonAlign: 'auto',\n // Player layout. 'default' = play button + waveform with a left-aligned\n // info row below. 'preview' = compact: the title is centered under the\n // waveform and the meta row (time / speed / BPM) is trimmed \u2014 ideal for\n // sample-pack sample previews and dense grids.\n layout: 'default',\n // Play/pause button style. 'circle' = bordered circle (default).\n // 'minimal' = a bare play/pause glyph with no circle \u2014 the look sample-pack\n // and beat stores use in their preview grids.\n buttonStyle: 'circle',\n\n // Default waveform style\n waveformStyle: 'mirror',\n barWidth: 2,\n barSpacing: 0,\n // Rounded bar caps (px). 0 = square; 1 = soft caps (default). Applies to bars/mirror.\n barRadius: 1,\n\n // Color preset: null = auto-detect, 'dark' = force dark, 'light' = force light\n colorPreset: null,\n\n // Individual color overrides (null means use preset)\n waveformColor: null,\n progressColor: null,\n buttonColor: null,\n buttonHoverColor: null,\n textColor: null,\n textSecondaryColor: null,\n backgroundColor: null,\n borderColor: null,\n\n // Features\n autoplay: false,\n showControls: true,\n showInfo: true,\n showTime: true,\n showHoverTime: false,\n showBPM: false,\n // Known BPM to display in the badge (with showBPM). Wins over auto-detection\n // \u2014 set it when peaks are pre-generated so the tempo still shows. null = auto.\n bpm: null,\n singlePlay: true,\n playOnSeek: true,\n enableMediaSession: true,\n\n // Markers\n markers: [],\n showMarkers: true,\n\n // Accessibility \u2014 expose the waveform as a keyboard-operable slider\n // (role=\"slider\" + ARIA value attributes + arrow/page/home/end seeking).\n // seekLabel sets the slider's accessible name; when null it falls back\n // to the track title, then 'Seek'.\n accessibleSeek: true,\n seekLabel: null,\n\n // Content\n title: null,\n subtitle: null,\n artwork: null,\n album: '',\n\n // Message shown in the error state when audio fails to load.\n errorText: 'Unable to load audio',\n\n // Icons (SVG)\n playIcon: '<svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\"><path d=\"M8 5v14l11-7z\"/></svg>',\n pauseIcon: '<svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\"><path d=\"M6 4h4v16H6zM14 4h4v16h-4z\"/></svg>',\n\n // Callbacks\n onLoad: null,\n onPlay: null,\n onPause: null,\n onEnd: null,\n onError: null,\n onTimeUpdate: null\n};\n\n/**\n * Per-waveform-style geometry defaults.\n *\n * Maps each supported `waveformStyle` to its natural `barWidth`/`barSpacing`\n * (in px), used to seed bar geometry when the caller has not explicitly set\n * those options so each style renders at sensible proportions.\n *\n * @type {Object<string, {barWidth: number, barSpacing: number}>}\n */\nexport const STYLE_DEFAULTS = {\n bars: {barWidth: 3, barSpacing: 1},\n mirror: {barWidth: 2, barSpacing: 2},\n line: {barWidth: 2, barSpacing: 0},\n blocks: {barWidth: 4, barSpacing: 2},\n dots: {barWidth: 3, barSpacing: 3},\n seekbar: {barWidth: 1, barSpacing: 0}\n};", "/**\n * @module core\n * @description Main WaveformPlayer class\n */\n\nimport {draw} from './drawing.js';\nimport {generateWaveform, generatePlaceholderWaveform} from './audio.js';\nimport {\n formatTime,\n extractTitleFromUrl,\n generateId,\n parseDataAttributes,\n mergeOptions,\n debounce,\n clamp,\n escapeHtml\n} from './utils.js';\n\nimport {DEFAULT_OPTIONS, STYLE_DEFAULTS, getColorPreset} from './themes.js';\n\n// Keyboard seek steps (seconds) for the accessible slider.\nconst SEEK_STEP_SECONDS = 5;\nconst SEEK_PAGE_SECONDS = 10;\n\n/**\n * WaveformPlayer - Modern audio player with waveform visualization\n * @class\n */\nexport class WaveformPlayer {\n /** @type {Map<string, WaveformPlayer>} */\n static instances = new Map();\n\n /** @type {WaveformPlayer|null} */\n static currentlyPlaying = null;\n\n /**\n * Create a new WaveformPlayer instance.\n *\n * Resolves the container, merges options (defaults < `data-*` attributes <\n * constructor options), applies the colour preset and style-specific\n * defaults, registers the instance in the static map, and kicks off\n * {@link WaveformPlayer#init}. A `waveformplayer:ready` event is dispatched\n * ~100ms later, once initialization has settled.\n *\n * @param {string|HTMLElement} container - Container element, or a CSS\n * selector resolved with `document.querySelector`.\n * @param {Object} [options={}] - Player options. Accepts the shorthand\n * aliases `style` (\u2192 `waveformStyle`) and `src` (\u2192 `url`); the canonical\n * names win if both are supplied.\n * @throws {Error} If the container element cannot be found.\n * @fires WaveformPlayer#waveformplayer:ready\n */\n constructor(container, options = {}) {\n // Resolve container\n this.container = typeof container === 'string'\n ? document.querySelector(container)\n : container;\n\n if (!this.container) {\n throw new Error('[WaveformPlayer] Container element not found');\n }\n\n // Parse data attributes if present\n const dataOptions = parseDataAttributes(this.container);\n\n // Shorthand option aliases \u2014 `style` -> `waveformStyle`, `src` -> `url`.\n // The canonical names still work and win if both are supplied.\n const userOptions = { ...options };\n if (userOptions.style && !userOptions.waveformStyle) userOptions.waveformStyle = userOptions.style;\n if (userOptions.src && !userOptions.url) userOptions.url = userOptions.src;\n\n // Merge options: defaults < data attributes < constructor options\n this.options = mergeOptions(DEFAULT_OPTIONS, dataOptions, userOptions);\n\n // Apply color preset (auto-detect if not specified)\n const preset = getColorPreset(this.options.colorPreset);\n\n // Apply preset colors only if individual colors aren't explicitly set\n for (const [key, value] of Object.entries(preset)) {\n if (this.options[key] === null || this.options[key] === undefined) {\n this.options[key] = value;\n }\n }\n\n // Apply style-specific defaults if not explicitly set\n const styleDefaults = STYLE_DEFAULTS[this.options.waveformStyle];\n if (styleDefaults) {\n if (dataOptions.barWidth === undefined && options.barWidth === undefined) {\n this.options.barWidth = styleDefaults.barWidth;\n }\n if (dataOptions.barSpacing === undefined && options.barSpacing === undefined) {\n this.options.barSpacing = styleDefaults.barSpacing;\n }\n }\n\n // Initialize state\n this.audio = null;\n this.canvas = null;\n this.ctx = null;\n this.waveformData = [];\n this.progress = 0;\n this.isPlaying = false;\n this.isLoading = false;\n this.hasError = false;\n this.updateTimer = null;\n this.resizeObserver = null;\n\n // All DOM/document listeners are registered with this signal so a\n // single abort() in destroy() tears every one of them down (the old\n // destroy left the document-click and container listeners attached).\n this._ac = new AbortController();\n\n // Generate unique ID\n this.id = this.container.id || generateId(this.options.url);\n\n // Add to instances\n WaveformPlayer.instances.set(this.id, this);\n\n // Initialize\n this.init();\n\n // Dispatch ready event after initialization\n setTimeout(() => {\n this._emit('waveformplayer:ready', {player: this, url: this.options.url});\n }, 100);\n }\n\n /**\n * Build and dispatch a bubbling `waveformplayer:*` CustomEvent on the\n * container, returning the event so cancelable (request-*) events can have\n * their `defaultPrevented` checked. Single source of truth for the event\n * shape \u2014 every player event bubbles and carries the supplied detail.\n * @param {string} type - Full event type, e.g. `'waveformplayer:play'`.\n * @param {Object} detail - Event detail payload.\n * @param {boolean} [cancelable=false] - Whether the event is cancelable.\n * @returns {CustomEvent} The dispatched event.\n * @private\n */\n _emit(type, detail, cancelable = false) {\n const event = new CustomEvent(type, { bubbles: true, cancelable, detail });\n this.container.dispatchEvent(event);\n return event;\n }\n\n /**\n * External-mode seek request: dispatch a cancelable\n * `waveformplayer:request-seek` and, unless the controller calls\n * `preventDefault()`, optimistically advance the local progress overlay so\n * the canvas repaints at once. Shared by the keyboard slider and canvas click.\n * @param {number} percent - Target position as a 0..1 fraction.\n * @private\n * @fires WaveformPlayer#waveformplayer:request-seek\n */\n _requestSeek(percent) {\n const evt = this._emit('waveformplayer:request-seek', { ...this._buildTrackDetail(), percent }, true);\n if (!evt.defaultPrevented) {\n this.progress = percent;\n this.drawWaveform?.();\n }\n }\n\n // ============================================\n // Initialization\n // ============================================\n\n /**\n * Initialize the player: build the DOM, create the audio element (self\n * mode only), wire up the feature controls (speed, keyboard, accessible\n * seek), bind events, attach the resize observer, then size the canvas and\n * \u2014 if a `url` option was given \u2014 load it and optionally autoplay.\n * @private\n */\n init() {\n this.createDOM();\n this.createAudio();\n this.initPlaybackSpeed();\n this.initKeyboardControls();\n this.initSeekControl();\n this.bindEvents();\n this.setupResizeObserver();\n\n // Ensure proper sizing after DOM is ready\n requestAnimationFrame(() => {\n this.resizeCanvas();\n\n // Load audio if URL provided\n if (this.options.url) {\n this.load(this.options.url).then(() => {\n if (this.options.autoplay) {\n this.play()?.catch(() => {});\n }\n }).catch(error => {\n console.error('[WaveformPlayer] Failed to load audio:', error);\n });\n }\n });\n }\n\n /**\n * Build the player's DOM tree inside the container and cache element\n * references.\n *\n * Clears the container, resolves button alignment (`auto` \u2192 `bottom` for\n * the `bars` style, `center` otherwise), and conditionally renders the play\n * button, info row (artwork/title/subtitle), BPM badge, playback-speed\n * menu, and time display based on the relevant `show*` options. Caches the\n * canvas, controls, and text elements onto `this`, then sizes the canvas.\n * @private\n */\n createDOM() {\n // Clear container\n this.container.innerHTML = '';\n this.container.className = 'waveform-player';\n\n // Determine button alignment\n let buttonAlign = this.options.buttonAlign;\n if (buttonAlign === 'auto') {\n // Auto-align based on waveform style\n const style = this.options.waveformStyle;\n if (style === 'bars') {\n buttonAlign = 'bottom';\n } else {\n buttonAlign = 'center'; // blocks, mirror, line, dots, seekbar all center\n }\n }\n\n // Compact 'preview' layout: centered title under the waveform with the\n // meta row trimmed. Set via the `layout` option / data-layout=\"preview\".\n const isPreview = this.options.layout === 'preview';\n if (isPreview) {\n this.container.classList.add('waveform-layout-preview');\n }\n\n // Build play button HTML (conditional)\n const buttonHTML = this.options.showControls ? `\n <button class=\"waveform-btn${this.options.buttonStyle === 'minimal' ? ' waveform-btn-minimal' : ''}\" aria-label=\"Play/Pause\" style=\"\n border-color: ${this.options.buttonColor};\n color: ${this.options.buttonColor};\n \">\n <span class=\"waveform-icon-play\">${this.options.playIcon}</span>\n <span class=\"waveform-icon-pause\" style=\"display:none;\">${this.options.pauseIcon}</span>\n </button>\n ` : '';\n\n // Build info section HTML (conditional)\n const infoHTML = this.options.showInfo ? `\n <div class=\"waveform-info\">\n ${this.options.artwork ? `\n <img class=\"waveform-artwork\" src=\"${this.options.artwork}\" alt=\"Album artwork\" style=\"\n width: 40px;\n height: 40px;\n border-radius: 4px;\n object-fit: cover;\n flex-shrink: 0;\n \">\n ` : ''}\n <div class=\"waveform-text\">\n <span class=\"waveform-title\" style=\"color: ${this.options.textColor};\"></span>\n ${this.options.subtitle ? `<span class=\"waveform-subtitle\" style=\"color: ${this.options.textSecondaryColor};\">${this.options.subtitle}</span>` : ''}\n </div>\n <div class=\"waveform-meta\" style=\"display: flex; align-items: center; gap: 1rem;\">\n ${this.options.showBPM ? `\n <span class=\"waveform-bpm\" style=\"color: ${this.options.textSecondaryColor}; display: none;\">\n <span class=\"bpm-value\">--</span> BPM\n </span>\n ` : ''}\n ${this.options.showPlaybackSpeed ? `\n <div class=\"waveform-speed\">\n <button class=\"speed-btn\" aria-label=\"Playback speed\">\n <span class=\"speed-value\">1x</span>\n </button>\n <div class=\"speed-menu\" style=\"display: none;\">\n ${this.options.playbackRates.map(rate =>\n `<button class=\"speed-option\" data-rate=\"${rate}\">${rate}x</button>`\n ).join('')}\n </div>\n </div>\n ` : ''}\n ${this.options.showTime ? `\n <span class=\"waveform-time\" style=\"color: ${this.options.textSecondaryColor};\">\n <span class=\"time-current\">0:00</span> / <span class=\"time-total\">0:00</span>\n </span>\n ` : ''}\n </div>\n </div>\n ` : '';\n\n // Create HTML structure\n this.container.innerHTML = `\n <div class=\"waveform-player-inner\">\n <div class=\"waveform-body\">\n <div class=\"waveform-track waveform-align-${buttonAlign}\">\n ${buttonHTML}\n \n <div class=\"waveform-container\">\n <canvas></canvas>\n <div class=\"waveform-markers\"></div>\n <div class=\"waveform-loading\" style=\"display:none;\"></div>\n <div class=\"waveform-error\" style=\"display:none;\" role=\"alert\">\n <span class=\"waveform-error-text\">${escapeHtml(this.options.errorText)}</span>\n </div>\n </div>\n </div>\n \n ${infoHTML}\n </div>\n </div>\n`;\n\n // Get references\n this.playBtn = this.container.querySelector('.waveform-btn');\n this.canvas = this.container.querySelector('canvas');\n this.ctx = this.canvas.getContext('2d');\n this.titleEl = this.container.querySelector('.waveform-title');\n this.subtitleEl = this.container.querySelector('.waveform-subtitle');\n this.artworkEl = this.container.querySelector('.waveform-artwork');\n this.currentTimeEl = this.container.querySelector('.time-current');\n this.totalTimeEl = this.container.querySelector('.time-total');\n this.bpmEl = this.container.querySelector('.waveform-bpm');\n this.bpmValueEl = this.container.querySelector('.bpm-value');\n this.loadingEl = this.container.querySelector('.waveform-loading');\n this.errorEl = this.container.querySelector('.waveform-error');\n this.markersContainer = this.container.querySelector('.waveform-markers');\n this.speedBtn = this.container.querySelector('.speed-btn');\n this.speedMenu = this.container.querySelector('.speed-menu');\n\n // Set canvas size\n this.resizeCanvas();\n\n // Show a caller-supplied BPM immediately (no audio decode required).\n this.updateBPMDisplay();\n }\n\n /**\n * Create audio element\n * @private\n *\n * No-op in `audioMode: 'external'` \u2014 the player has no audio of its\n * own; an external controller (e.g. WaveformBar) owns playback and\n * pushes state in via setPlayingState() / setProgress(). The\n * `this.audio` field stays null in that mode; downstream code must\n * null-check it.\n */\n createAudio() {\n if (this.options.audioMode === 'external') {\n this.audio = null;\n return;\n }\n this.audio = new Audio();\n this.audio.preload = this.options.preload || 'metadata';\n this.audio.crossOrigin = 'anonymous';\n }\n\n // ============================================\n // Feature Initialization\n // ============================================\n\n /**\n * Apply the configured initial playback rate to the audio element (self\n * mode only) and, when `showPlaybackSpeed` is enabled, wire up the speed\n * menu UI via {@link WaveformPlayer#initSpeedControls}.\n * @private\n */\n initPlaybackSpeed() {\n // External mode has no <audio> element, so the speed control\n // doesn't apply locally \u2014 the external controller (e.g.\n // WaveformBar) owns playback rate. Skip the audio init but\n // still bind the speed control UI in case the controller\n // wants to mirror rate changes via events later.\n if (this.audio && this.options.playbackRate && this.options.playbackRate !== 1) {\n this.audio.playbackRate = this.options.playbackRate;\n }\n\n // Initialize speed control UI if enabled\n if (this.options.showPlaybackSpeed) {\n this.initSpeedControls();\n }\n }\n\n /**\n * Wire up the playback-speed menu: toggle it open on the speed button,\n * close it on any outside click, and apply the chosen rate when a\n * `.speed-option` is clicked. All listeners are registered against the\n * instance `AbortController` signal so {@link WaveformPlayer#destroy} tears\n * them down. No-op if the speed elements are absent.\n * @private\n */\n initSpeedControls() {\n const speedBtn = this.container.querySelector('.speed-btn');\n const speedMenu = this.container.querySelector('.speed-menu');\n\n if (!speedBtn || !speedMenu) return;\n\n // Toggle menu\n speedBtn.addEventListener('click', (e) => {\n e.stopPropagation();\n speedMenu.style.display = speedMenu.style.display === 'none' ? 'block' : 'none';\n }, {signal: this._ac.signal});\n\n // Close menu when clicking outside\n document.addEventListener('click', () => {\n speedMenu.style.display = 'none';\n }, {signal: this._ac.signal});\n\n // Handle speed selection\n speedMenu.addEventListener('click', (e) => {\n e.stopPropagation();\n if (e.target.classList.contains('speed-option')) {\n const rate = parseFloat(e.target.dataset.rate);\n this.setPlaybackRate(rate);\n speedMenu.style.display = 'none';\n }\n }, {signal: this._ac.signal});\n\n // Set initial UI state\n this.updateSpeedUI();\n }\n\n /**\n * Enable keyboard transport controls on the container.\n *\n * The container is focusable only after it is clicked (it carries\n * `tabindex=\"-1\"` until then, and clicking steals focus from sibling\n * players). While focused it handles: digits 0-9 (seek to that tenth of\n * the track), Space (toggle play), and \u2014 in self mode only, since\n * `this.audio` is null in external mode \u2014 arrow keys (seek \u00B15s, volume\n * \u00B10.1) and `m`/`M` (mute). Listeners use the instance abort signal.\n * @private\n */\n initKeyboardControls() {\n // Make container focusable but not in tab order by default\n this.container.setAttribute('tabindex', '-1');\n\n // Only activate keyboard controls when explicitly focused (clicked)\n this.container.addEventListener('click', () => {\n // Remove focus from all other players\n WaveformPlayer.getAllInstances().forEach(player => {\n if (player !== this) {\n player.container.setAttribute('tabindex', '-1');\n }\n });\n // Make this one focusable\n this.container.setAttribute('tabindex', '0');\n this.container.focus();\n }, {signal: this._ac.signal});\n\n // Keyboard events. In external mode `this.audio` is null, so\n // seek/volume/mute keys are no-ops (the external controller\n // owns those). Space (togglePlay) still works because togglePlay\n // routes through the request-play/pause events.\n this.container.addEventListener('keydown', (e) => {\n if (document.activeElement !== this.container) return;\n\n const key = e.key;\n const hasAudio = !!this.audio;\n const currentTime = hasAudio ? this.audio.currentTime : 0;\n\n // Handle number keys 0-9 for seeking\n if (hasAudio && key >= '0' && key <= '9') {\n e.preventDefault();\n this.seekToPercent(parseInt(key) / 10);\n return;\n }\n\n // Handle other keys. Space always works (dispatches\n // request-play in external mode); audio-bound keys only\n // when we own the <audio> element.\n const actions = {\n ' ': () => this.togglePlay(),\n };\n if (hasAudio) {\n actions['ArrowLeft'] = () => this.seekTo(clamp(currentTime - 5, 0, this.audio.duration));\n actions['ArrowRight'] = () => this.seekTo(clamp(currentTime + 5, 0, this.audio.duration));\n actions['ArrowUp'] = () => this.setVolume(clamp(this.audio.volume + 0.1));\n actions['ArrowDown'] = () => this.setVolume(clamp(this.audio.volume - 0.1));\n actions['m'] = actions['M'] = () => this.audio.muted = !this.audio.muted;\n }\n\n if (actions[key]) {\n e.preventDefault();\n actions[key]();\n }\n }, {signal: this._ac.signal});\n }\n\n /**\n * Expose the waveform as an accessible, keyboard-operable slider.\n *\n * Adds role=\"slider\" + ARIA value attributes to the waveform surface,\n * makes it focusable in the tab order, and handles the standard slider\n * keys (arrows, Page Up/Down, Home/End) to seek. Works in both self and\n * external audio modes. Opt out with `accessibleSeek: false`.\n * @private\n */\n initSeekControl() {\n if (!this.options.accessibleSeek) return;\n\n this.seekEl = this.container.querySelector('.waveform-container');\n if (!this.seekEl) return;\n\n this.seekEl.setAttribute('role', 'slider');\n this.seekEl.setAttribute('tabindex', '0');\n this.seekEl.setAttribute('aria-valuemin', '0');\n this.applySeekLabel();\n this.updateSeekAccessibility();\n\n this.seekEl.addEventListener('keydown', (e) => {\n const duration = this.getSeekDuration();\n if (!duration) return;\n\n const current = this.getSeekCurrentTime();\n let target;\n switch (e.key) {\n case 'ArrowLeft':\n case 'ArrowDown':\n target = current - SEEK_STEP_SECONDS;\n break;\n case 'ArrowRight':\n case 'ArrowUp':\n target = current + SEEK_STEP_SECONDS;\n break;\n case 'PageDown':\n target = current - SEEK_PAGE_SECONDS;\n break;\n case 'PageUp':\n target = current + SEEK_PAGE_SECONDS;\n break;\n case 'Home':\n target = 0;\n break;\n case 'End':\n target = duration;\n break;\n default:\n return;\n }\n\n // Prevent page scroll and stop the container-level keydown\n // handler from also seeking (it would double-fire / change\n // volume on the vertical arrows).\n e.preventDefault();\n e.stopPropagation();\n this.seekToSeconds(target);\n }, {signal: this._ac.signal});\n }\n\n /**\n * Total seekable duration in seconds, regardless of audio mode.\n * @returns {number}\n * @private\n */\n getSeekDuration() {\n if (this.options.audioMode === 'external') {\n return this._extDuration || 0;\n }\n return this.audio && Number.isFinite(this.audio.duration)\n ? this.audio.duration\n : 0;\n }\n\n /**\n * Current playback position in seconds, regardless of audio mode.\n * @returns {number}\n * @private\n */\n getSeekCurrentTime() {\n if (this.options.audioMode === 'external') {\n return this.progress * (this._extDuration || 0);\n }\n return this.audio && Number.isFinite(this.audio.currentTime)\n ? this.audio.currentTime\n : 0;\n }\n\n /**\n * Seek the slider to an absolute time, clamped to the track length.\n *\n * In self mode this defers to {@link WaveformPlayer#seekTo}. In external\n * mode it dispatches a cancelable `waveformplayer:request-seek` event with\n * the target percentage; if the controller doesn't `preventDefault()`, the\n * local progress/visual is updated optimistically. Either way the ARIA\n * slider values are refreshed.\n * @param {number} seconds - Target time in seconds.\n * @private\n * @fires WaveformPlayer#waveformplayer:request-seek\n */\n seekToSeconds(seconds) {\n const duration = this.getSeekDuration();\n if (!duration) return;\n\n const clamped = clamp(seconds, 0, duration);\n\n if (this.options.audioMode === 'external') {\n this._requestSeek(clamped / duration);\n this.updateSeekAccessibility();\n return;\n }\n\n // seekTo() calls updateProgress(), which refreshes the ARIA values.\n this.seekTo(clamped);\n }\n\n /**\n * Set the slider's accessible name from `seekLabel`, falling back to the\n * track title, then a generic 'Seek'. No-op if the slider isn't present.\n * @param {string} [title=this.options.title] - Track title to fall back to\n * when `seekLabel` is not set.\n * @private\n */\n applySeekLabel(title = this.options.title) {\n if (!this.seekEl) return;\n const label = this.options.seekLabel || title || 'Seek';\n this.seekEl.setAttribute('aria-label', label);\n }\n\n /**\n * Keep the slider's ARIA value attributes in sync with playback.\n * @private\n */\n updateSeekAccessibility() {\n if (!this.seekEl) return;\n\n const duration = this.getSeekDuration();\n const current = Math.min(this.getSeekCurrentTime(), duration);\n\n this.seekEl.setAttribute('aria-valuemax', String(Math.round(duration)));\n this.seekEl.setAttribute('aria-valuenow', String(Math.round(current)));\n this.seekEl.setAttribute(\n 'aria-valuetext',\n `${formatTime(current)} of ${formatTime(duration)}`\n );\n }\n\n /**\n * Initialize Media Session API for system media controls\n * @private\n */\n initMediaSession() {\n if (!('mediaSession' in navigator) || !this.options.enableMediaSession) return;\n // Skip Media Session in external mode \u2014 the controller (e.g.\n // WaveformBar) owns audio playback and registers its own Media\n // Session handlers; ours would conflict with its.\n if (!this.audio) return;\n\n // Set metadata\n navigator.mediaSession.metadata = new MediaMetadata({\n title: this.options.title || 'Unknown Track',\n artist: this.options.subtitle || '',\n album: this.options.album || '',\n artwork: this.options.artwork ? [\n {src: this.options.artwork, sizes: '512x512', type: 'image/jpeg'}\n ] : []\n });\n\n // Set up action handlers\n navigator.mediaSession.setActionHandler('play', () => this.play());\n navigator.mediaSession.setActionHandler('pause', () => this.pause());\n navigator.mediaSession.setActionHandler('seekbackward', () => {\n this.seekTo(clamp(this.audio.currentTime - 10, 0, this.audio.duration));\n });\n navigator.mediaSession.setActionHandler('seekforward', () => {\n this.seekTo(clamp(this.audio.currentTime + 10, 0, this.audio.duration));\n });\n navigator.mediaSession.setActionHandler('seekto', (details) => {\n if (details.seekTime !== null) {\n this.seekTo(details.seekTime);\n }\n });\n }\n\n // ============================================\n // Event Binding\n // ============================================\n\n /**\n * Bind the core interaction listeners: play-button click, the `<audio>`\n * media events (self mode only \u2014 external mode is fed state via\n * {@link WaveformPlayer#setPlayingState}/{@link WaveformPlayer#setProgress}),\n * canvas click-to-seek, and a debounced window-resize redraw.\n * @private\n */\n bindEvents() {\n // Play button (only if controls are shown). In external mode\n // togglePlay() dispatches the request-play/pause events so the\n // controller can decide what to do; the click still goes through\n // here.\n if (this.playBtn) {\n this.playBtn.addEventListener('click', () => this.togglePlay());\n }\n\n // Audio events \u2014 only when we own an <audio> element. External\n // mode receives state via setPlayingState() / setProgress() from\n // the controller, so we have nothing to listen to here.\n if (this.audio) {\n this.audio.addEventListener('loadstart', () => this.setLoading(true));\n this.audio.addEventListener('loadedmetadata', () => this.onMetadataLoaded());\n this.audio.addEventListener('canplay', () => this.setLoading(false));\n this.audio.addEventListener('play', () => this.onPlay());\n this.audio.addEventListener('pause', () => this.onPause());\n this.audio.addEventListener('ended', () => this.onEnded());\n this.audio.addEventListener('error', (e) => this.onError(e));\n }\n\n // Canvas interactions \u2014 seek-on-click. In external mode the\n // canvas click dispatches a `waveformplayer:request-seek` event\n // so the controller can position its own audio element.\n this.canvas.addEventListener('click', (e) => this.handleCanvasClick(e));\n\n // Window resize - store handler for cleanup\n this.resizeHandler = debounce(() => this.resizeCanvas(), 100);\n window.addEventListener('resize', this.resizeHandler);\n }\n\n /**\n * Observe the canvas's parent element for size changes and re-fit the\n * canvas on each one. No-op where `ResizeObserver` is unavailable.\n * @private\n */\n setupResizeObserver() {\n if ('ResizeObserver' in window) {\n this.resizeObserver = new ResizeObserver(() => {\n this.resizeCanvas();\n });\n\n if (this.canvas?.parentElement) {\n this.resizeObserver.observe(this.canvas.parentElement);\n }\n }\n }\n\n // ============================================\n // Audio Loading\n // ============================================\n\n /**\n * Load an audio source: set the title, fetch/generate the waveform peaks,\n * draw them, render markers, and initialise Media Session.\n *\n * In self mode the `<audio>` src is assigned and the method awaits\n * `loadedmetadata` before proceeding. In external mode there is no audio\n * element, so the src/metadata step is skipped and only the visualization\n * is built (duration/time come from the controller via\n * {@link WaveformPlayer#setProgress}). Peaks come from the `waveform`\n * option when provided, otherwise they are decoded from the audio; a\n * decode failure falls back to a placeholder waveform. The `onLoad`\n * callback fires on success.\n * @param {string} url - Audio URL.\n * @returns {Promise<void>} Resolves once loading settles (errors are caught\n * internally and surfaced through {@link WaveformPlayer#onError}).\n */\n async load(url) {\n try {\n this.setLoading(true);\n this.progress = 0;\n this.hasError = false;\n\n // In external mode we don't own an <audio> element \u2014 skip\n // src assignment + metadata-wait, but still generate the\n // waveform peaks so the canvas can render the visualization.\n // Duration / current time come from the external controller\n // via setProgress().\n if (this.audio) {\n // Set audio source\n this.audio.src = url;\n\n // Wait for metadata to load\n await new Promise((resolve, reject) => {\n const metadataHandler = () => {\n this.audio.removeEventListener('loadedmetadata', metadataHandler);\n this.audio.removeEventListener('error', errorHandler);\n resolve();\n };\n const errorHandler = (e) => {\n this.audio.removeEventListener('loadedmetadata', metadataHandler);\n this.audio.removeEventListener('error', errorHandler);\n reject(e);\n };\n this.audio.addEventListener('loadedmetadata', metadataHandler);\n this.audio.addEventListener('error', errorHandler);\n });\n }\n\n // Set title\n const title = this.options.title || extractTitleFromUrl(url);\n if (this.titleEl) {\n this.titleEl.textContent = title;\n }\n // Keep the seek slider's accessible name in sync with the track.\n this.applySeekLabel(title);\n\n // Load or generate waveform\n if (this.options.waveform) {\n this.setWaveformData(this.options.waveform);\n } else {\n // Generate waveform\n try {\n const result = await generateWaveform(url, this.options.samples, this.options.showBPM);\n this.waveformData = result.peaks;\n\n // Store BPM if detected\n if (result.bpm) {\n this.detectedBPM = result.bpm;\n this.updateBPMDisplay();\n }\n } catch (error) {\n console.warn('[WaveformPlayer] Using placeholder waveform:', error);\n this.waveformData = generatePlaceholderWaveform(this.options.samples);\n }\n }\n\n this.drawWaveform();\n this.renderMarkers();\n this.initMediaSession();\n\n // Fire callback\n if (this.options.onLoad) {\n this.options.onLoad(this);\n }\n } catch (error) {\n // onError() is the single funnel for surfacing + logging errors.\n this.onError(error);\n } finally {\n this.setLoading(false);\n }\n }\n\n /**\n * Swap the player to a new track at runtime.\n *\n * Pauses any current playback, fully resets the audio element (self mode),\n * clears error/marker/progress state, merges the new metadata into\n * `this.options`, updates the subtitle/artwork DOM, then calls\n * {@link WaveformPlayer#load}. Auto-plays the new track unless\n * `options.autoplay === false`.\n * @param {string} url - Audio URL.\n * @param {string|null} [title=null] - Track title; keeps the existing\n * title when null.\n * @param {string|null} [subtitle=null] - Track subtitle; pass `''` to hide\n * the subtitle row, or null to keep the existing one.\n * @param {Object} [options={}] - Additional options to merge (e.g.\n * `preload`, `artwork`, `markers`, `autoplay`).\n * @returns {Promise<void>}\n */\n async loadTrack(url, title = null, subtitle = null, options = {}) {\n // Stop current playback and clear state\n if (this.isPlaying) {\n this.pause();\n }\n\n // Reset audio element completely (only when we own one)\n if (this.audio) {\n this.audio.src = '';\n this.audio.load();\n }\n\n // Clear any errors\n this.hasError = false;\n if (this.errorEl) {\n this.errorEl.style.display = 'none';\n }\n if (this.canvas) {\n this.canvas.style.opacity = '1';\n }\n if (this.playBtn) {\n this.playBtn.disabled = false;\n }\n\n // Reset state\n this.progress = 0;\n this.waveformData = [];\n\n // Update options (including preload if specified)\n this.options = mergeOptions(this.options, {\n url,\n title: title || this.options.title,\n subtitle: subtitle || this.options.subtitle,\n ...options\n });\n\n // Apply preload setting if it was changed\n if (options.preload && this.audio) {\n this.audio.preload = options.preload;\n }\n\n // Update UI elements\n if (this.subtitleEl) {\n if (subtitle) {\n this.subtitleEl.textContent = subtitle;\n this.subtitleEl.style.display = '';\n } else if (subtitle === '') {\n this.subtitleEl.style.display = 'none';\n }\n }\n\n // Update artwork if provided\n if (options.artwork && this.artworkEl) {\n this.artworkEl.src = options.artwork;\n }\n\n // Clear or update markers\n this.options.markers = options.markers || [];\n\n // Reset the waveform to the NEW track's peaks, or null to regenerate\n // from the URL. mergeOptions() above keeps the previous track's\n // this.options.waveform when the caller passes none, and load() does\n // `if (this.options.waveform) setWaveformData(...)` \u2014 so without this\n // reset a track loaded without peaks would redraw the PREVIOUS track's\n // waveform (audio changes, visualization doesn't).\n this.options.waveform = options.waveform || null;\n\n // Load the new track\n await this.load(url);\n\n // Auto-play the new track unless the caller opted out \u2014 lets a\n // controller load/restore/enqueue without forcing playback.\n if (options.autoplay !== false) {\n this.play()?.catch(() => {});\n }\n }\n\n // ============================================\n // Visualization\n // ============================================\n\n /**\n * Normalise externally-supplied waveform data into `this.waveformData` and\n * redraw.\n *\n * Accepts several shapes: a `.json` URL (fetched async; peaks and any\n * embedded `markers` are applied on resolve), a JSON-encoded array string,\n * a comma-separated number string, or a plain number array. Malformed\n * input degrades to an empty array rather than throwing.\n * @param {string|number[]} data - Peaks as an array, a JSON/CSV string, or\n * a URL to a `.json` peaks file.\n * @private\n */\n setWaveformData(data) {\n // URL to JSON file \u2014 fetch peaks and maybe markers\n if (typeof data === 'string' && data.trim().endsWith('.json')) {\n fetch(data.trim())\n .then(r => r.json())\n .then(json => {\n this.waveformData = Array.isArray(json) ? json : (json.peaks || []);\n if (json.markers && !this.options.markers?.length) {\n this.options.markers = json.markers;\n this.renderMarkers();\n }\n this.drawWaveform();\n })\n .catch(() => {});\n return;\n }\n\n if (typeof data === 'string') {\n try {\n const parsed = JSON.parse(data);\n this.waveformData = Array.isArray(parsed) ? parsed : [];\n } catch {\n this.waveformData = data.split(',').map(Number);\n }\n } else {\n this.waveformData = Array.isArray(data) ? data : [];\n }\n this.drawWaveform();\n }\n\n /**\n * Render the current waveform + progress to the canvas via the shared\n * {@link draw} routine, passing the resolved style and colours. No-op\n * before the context exists or while there is no peak data.\n * @private\n */\n drawWaveform() {\n if (!this.ctx || this.waveformData.length === 0) return;\n\n draw(this.ctx, this.canvas, this.waveformData, this.progress, {\n ...this.options,\n waveformStyle: this.options.waveformStyle || 'bars',\n color: this.options.waveformColor,\n progressColor: this.options.progressColor\n });\n }\n\n /**\n * Re-fit the canvas backing store to its parent's width and the configured\n * height, scaled by the device pixel ratio for crisp rendering, then\n * redraw. Guards against running after destruction.\n * @private\n */\n resizeCanvas() {\n // Guard against calls after destruction\n if (!this.canvas || this.isDestroying) {\n return;\n }\n\n const dpr = window.devicePixelRatio || 1;\n const rect = this.canvas.parentElement.getBoundingClientRect();\n\n this.canvas.width = rect.width * dpr;\n this.canvas.height = this.options.height * dpr;\n this.canvas.parentElement.style.height = this.options.height + 'px';\n\n this.drawWaveform();\n }\n\n /**\n * Render the configured cue markers as positioned, clickable buttons over\n * the waveform.\n *\n * Clears any existing markers first, then bails out unless `showMarkers` is\n * on, markers exist, and a duration is known (via the mode-agnostic\n * {@link WaveformPlayer#getSeekDuration}). Each marker is placed by its\n * time-as-percentage, carries a tooltip and ARIA label, and seeks on click\n * (also starting playback when `playOnSeek` is set and currently paused).\n * Markers past the track duration are skipped with a warning.\n * @private\n */\n renderMarkers() {\n if (!this.markersContainer) return;\n\n // Always clear existing markers first\n this.markersContainer.innerHTML = '';\n\n if (!this.options.showMarkers || !this.options.markers?.length) return;\n\n // Duration may come from the <audio> (self mode) or the external\n // controller (external mode) \u2014 use the mode-agnostic accessor.\n const duration = this.getSeekDuration();\n if (!duration) {\n return;\n }\n\n // Add each marker\n this.options.markers.forEach((marker, index) => {\n // Skip markers that are beyond the audio duration\n if (marker.time > duration) {\n console.warn(`[WaveformPlayer] Marker \"${marker.label}\" at ${marker.time}s exceeds audio duration of ${duration}s`);\n return;\n }\n\n const position = (marker.time / duration) * 100;\n\n const markerEl = document.createElement('button');\n markerEl.className = 'waveform-marker';\n markerEl.style.left = `${position}%`;\n markerEl.style.backgroundColor = marker.color || 'rgba(255, 255, 255, 0.5)';\n markerEl.setAttribute('aria-label', marker.label);\n markerEl.setAttribute('data-time', marker.time);\n\n // Tooltip\n const tooltip = document.createElement('span');\n tooltip.className = 'waveform-marker-tooltip';\n tooltip.textContent = marker.label;\n markerEl.appendChild(tooltip);\n\n // Click to seek\n markerEl.addEventListener('click', (e) => {\n e.stopPropagation();\n this.seekTo(marker.time);\n if (this.options.playOnSeek && !this.isPlaying) {\n this.play();\n }\n });\n\n this.markersContainer.appendChild(markerEl);\n });\n }\n\n /**\n * Highlight the marker at `index` (toggling an `active` class) and clear\n * the rest. Pass `null` to clear all. Lets an external controller (e.g. a\n * DJ bar) reflect the current section without reaching into the player's\n * private marker DOM.\n * @param {number|null} index - Marker index to activate, or `null` to clear.\n */\n setActiveMarker(index) {\n if (!this.markersContainer) return;\n const markers = this.markersContainer.querySelectorAll('.waveform-marker');\n markers.forEach((el, i) => el.classList.toggle('active', i === index));\n }\n\n // ============================================\n // Event Handlers\n // ============================================\n\n /**\n * Seek to the clicked horizontal position on the waveform canvas.\n *\n * Converts the click X into a 0..1 percentage. In external mode it\n * dispatches a cancelable `waveformplayer:request-seek` event (updating the\n * local visual optimistically unless the controller vetoes it); in self\n * mode it seeks the owned `<audio>` via\n * {@link WaveformPlayer#seekToPercent}.\n * @param {MouseEvent} event - The canvas click event.\n * @private\n * @fires WaveformPlayer#waveformplayer:request-seek\n */\n handleCanvasClick(event) {\n // In external mode the player has no audio of its own \u2014\n // dispatch a cancelable `waveformplayer:request-seek` event\n // with the target percentage so the controller can seek its\n // own audio. Locally we just update the visual progress so\n // the canvas paints the new position immediately (the\n // controller's progress event will reconcile shortly after).\n const rect = this.canvas.getBoundingClientRect();\n const x = event.clientX - rect.left;\n const targetPercent = clamp(x / rect.width);\n\n if (this.options.audioMode === 'external') {\n this._requestSeek(targetPercent);\n return;\n }\n\n if (!this.audio || !this.audio.duration) return;\n this.seekToPercent(targetPercent);\n }\n\n /**\n * Toggle the loading state: show/hide the spinner overlay and set\n * `aria-busy` on the accessible seek slider so assistive tech knows the\n * player is fetching/decoding.\n * @param {boolean} loading - True while audio is loading.\n * @private\n */\n setLoading(loading) {\n this.isLoading = loading;\n if (this.loadingEl) {\n this.loadingEl.style.display = loading ? 'block' : 'none';\n }\n // Let assistive tech know the player is busy fetching/decoding.\n if (this.seekEl) {\n this.seekEl.setAttribute('aria-busy', loading ? 'true' : 'false');\n }\n }\n\n /**\n * `loadedmetadata` handler (self mode): write the total-time display, now\n * that duration is known re-render markers, and publish duration to the\n * accessible seek slider. No-op during destruction.\n * @private\n */\n onMetadataLoaded() {\n // Ignore during destruction\n if (this.isDestroying) return;\n\n if (this.totalTimeEl) {\n this.totalTimeEl.textContent = formatTime(this.audio.duration);\n }\n // Re-render markers when duration is known\n this.renderMarkers();\n // Duration is now known \u2014 publish it to the accessible slider.\n this.updateSeekAccessibility();\n }\n\n /**\n * Reflect play/pause state on the transport button: toggle the `playing`\n * class and swap the play/pause icon visibility. The single source of\n * truth shared by `onPlay`, `onPause`, and the external-mode\n * `setPlayingState` pump so they can't drift. No-op without a button.\n * @param {boolean} isPlaying - Whether playback is active.\n * @private\n */\n setPlayButtonState(isPlaying) {\n if (!this.playBtn) return;\n this.playBtn.classList.toggle('playing', isPlaying);\n const playIcon = this.playBtn.querySelector('.waveform-icon-play');\n const pauseIcon = this.playBtn.querySelector('.waveform-icon-pause');\n if (playIcon) playIcon.style.display = isPlaying ? 'none' : 'flex';\n if (pauseIcon) pauseIcon.style.display = isPlaying ? 'flex' : 'none';\n }\n\n /**\n * `play` handler (self mode): set the playing flag, swap the button to its\n * pause icon, start the smooth progress loop, dispatch\n * `waveformplayer:play`, and fire the `onPlay` callback. No-op during\n * destruction.\n * @private\n * @fires WaveformPlayer#waveformplayer:play\n */\n onPlay() {\n // Ignore during destruction\n if (this.isDestroying) return;\n\n this.isPlaying = true;\n\n this.setPlayButtonState(true);\n\n this.startSmoothUpdate();\n\n // Dispatch play event\n this._emit('waveformplayer:play', {player: this, url: this.options.url});\n\n if (this.options.onPlay) {\n this.options.onPlay(this);\n }\n }\n\n /**\n * `pause` handler (self mode): clear the playing flag, swap the button back\n * to its play icon, stop the smooth progress loop, dispatch\n * `waveformplayer:pause`, and fire the `onPause` callback. No-op during\n * destruction.\n * @private\n * @fires WaveformPlayer#waveformplayer:pause\n */\n onPause() {\n // Ignore during destruction\n if (this.isDestroying) return;\n\n this.isPlaying = false;\n\n this.setPlayButtonState(false);\n\n this.stopSmoothUpdate();\n\n // Dispatch pause event\n this._emit('waveformplayer:pause', {player: this, url: this.options.url});\n\n if (this.options.onPause) {\n this.options.onPause(this);\n }\n }\n\n /**\n * `ended` handler (self mode): reset progress and `currentTime` to the\n * start, redraw, reset the time display, dispatch `waveformplayer:ended`\n * (carrying the final time), run {@link WaveformPlayer#onPause}, and fire\n * the `onEnd` callback. No-op during destruction.\n * @private\n * @fires WaveformPlayer#waveformplayer:ended\n */\n onEnded() {\n // Ignore during destruction\n if (this.isDestroying) return;\n\n const duration = this.audio.duration;\n\n this.progress = 0;\n this.audio.currentTime = 0;\n this.drawWaveform();\n\n // Reset time display\n if (this.currentTimeEl) {\n this.currentTimeEl.textContent = '0:00';\n }\n\n // Dispatch ended event \u2014 carries the final time so listeners (e.g.\n // analytics) don't have to reach into player.audio.\n this._emit('waveformplayer:ended', {player: this, url: this.options.url, currentTime: duration, duration});\n\n this.onPause();\n\n if (this.options.onEnd) {\n this.options.onEnd(this);\n }\n }\n\n /**\n * `error` handler: set the error flag, hide the spinner, reveal the error\n * overlay, dim the canvas, disable the play button, and fire the `onError`\n * callback. No-op during destruction.\n * @param {Event|Error} error - The audio error event, or an Error thrown\n * during loading.\n * @private\n */\n onError(error) {\n // Ignore errors during destruction\n if (this.isDestroying) return;\n\n console.error('[WaveformPlayer] Audio error:', error);\n this.hasError = true;\n this.setLoading(false);\n\n if (this.errorEl) {\n this.errorEl.style.display = 'flex';\n }\n\n if (this.canvas) {\n this.canvas.style.opacity = '0.2';\n }\n\n if (this.playBtn) {\n this.playBtn.disabled = true;\n }\n\n if (this.options.onError) {\n this.options.onError(error, this);\n }\n }\n\n // ============================================\n // Progress Updates\n // ============================================\n\n /**\n * Start the `requestAnimationFrame` loop that drives smooth progress\n * updates while playing (self mode only \u2014 external mode is redrawn by\n * controller {@link WaveformPlayer#setProgress} pushes). Cancels any\n * existing loop first so it's safe to call repeatedly.\n * @private\n */\n startSmoothUpdate() {\n this.stopSmoothUpdate();\n\n const update = () => {\n // In external mode the canvas redraws are driven by\n // setProgress() pushes from the controller \u2014 no internal\n // RAF needed. Self-mode keeps the smooth-update loop.\n if (this.isPlaying && this.audio && this.audio.duration) {\n this.updateProgress();\n this.updateTimer = requestAnimationFrame(update);\n }\n };\n\n this.updateTimer = requestAnimationFrame(update);\n }\n\n /**\n * Cancel the smooth-update animation frame, if one is scheduled.\n * @private\n */\n stopSmoothUpdate() {\n if (this.updateTimer) {\n cancelAnimationFrame(this.updateTimer);\n this.updateTimer = null;\n }\n }\n\n /**\n * Recompute progress from the owned `<audio>` clock and reflect it\n * everywhere (self mode only \u2014 external mode uses\n * {@link WaveformPlayer#setProgress}).\n *\n * Redraws the canvas when progress moves meaningfully, updates the\n * current-time display, dispatches `waveformplayer:timeupdate`, fires the\n * `onTimeUpdate` callback, and refreshes the accessible slider values.\n * @private\n * @fires WaveformPlayer#waveformplayer:timeupdate\n */\n updateProgress() {\n // Self-mode only \u2014 external mode receives progress via\n // setProgress() from the controller and never calls this.\n if (!this.audio || !this.audio.duration) return;\n\n const newProgress = this.audio.currentTime / this.audio.duration;\n\n if (Math.abs(newProgress - this.progress) > 0.001) {\n this.progress = newProgress;\n this.drawWaveform();\n }\n\n if (this.currentTimeEl) {\n this.currentTimeEl.textContent = formatTime(this.audio.currentTime);\n }\n\n // Dispatch timeupdate event\n this._emit('waveformplayer:timeupdate', {\n player: this,\n currentTime: this.audio.currentTime,\n duration: this.audio.duration,\n progress: this.progress,\n url: this.options.url\n });\n\n if (this.options.onTimeUpdate) {\n this.options.onTimeUpdate(this.audio.currentTime, this.audio.duration, this);\n }\n\n this.updateSeekAccessibility();\n }\n\n // ============================================\n // UI Updates\n // ============================================\n\n /**\n * Show the detected BPM in the badge, once a value has been detected.\n * @private\n */\n updateBPMDisplay() {\n // A caller-supplied `bpm` wins over auto-detection \u2014 useful when peaks\n // are pre-generated (so the audio is never decoded) but the BPM is known\n // anyway, e.g. sample-pack previews where the tempo is in the metadata.\n const bpm = this.options.bpm || this.detectedBPM;\n if (this.bpmEl && this.bpmValueEl && bpm) {\n this.bpmValueEl.textContent = Math.round(bpm);\n this.bpmEl.style.display = 'inline-flex';\n }\n }\n\n /**\n * Sync the speed control's label and the menu's active-option highlight to\n * the audio element's current `playbackRate`. No-op in external mode (no\n * owned `<audio>`), which also avoids reading `playbackRate` before the\n * element exists.\n * @private\n */\n updateSpeedUI() {\n // External mode owns no <audio>; nothing to reflect (and reading\n // this.audio.playbackRate here would throw during construction).\n if (!this.audio) return;\n\n const speedValue = this.container.querySelector('.speed-value');\n if (speedValue) {\n const rate = this.audio.playbackRate;\n speedValue.textContent = rate === 1 ? '1x' : `${rate}x`;\n }\n\n // Update active state in menu\n this.container.querySelectorAll('.speed-option').forEach(btn => {\n btn.classList.toggle('active', parseFloat(btn.dataset.rate) === this.audio.playbackRate);\n });\n }\n\n // ============================================\n // Public API\n // ============================================\n\n /**\n * Play audio.\n *\n * In `audioMode: 'self'` (default): calls the underlying <audio>\n * element's play(). Returns the promise from HTMLMediaElement.play().\n *\n * In `audioMode: 'external'`: dispatches a cancelable\n * `waveformplayer:request-play` event with the track metadata and\n * does NOT touch any audio element. Returns `undefined`. An external\n * controller (e.g. WaveformBar) listens for this event and starts\n * playback on its own audio source, then pushes state back via\n * setPlayingState() / setProgress(). Calling preventDefault() on\n * the event lets the controller veto the play (state is unchanged).\n *\n * When `singlePlay` is enabled, any other currently-playing instance is\n * paused first.\n *\n * @return {Promise|undefined} The promise from `HTMLMediaElement.play()` in\n * self mode; `undefined` in external mode.\n * @fires WaveformPlayer#waveformplayer:request-play\n */\n play() {\n if (this.options.singlePlay && WaveformPlayer.currentlyPlaying &&\n WaveformPlayer.currentlyPlaying !== this) {\n WaveformPlayer.currentlyPlaying.pause();\n }\n\n if (this.options.audioMode === 'external') {\n const evt = this._emit('waveformplayer:request-play', this._buildTrackDetail(), true);\n // If the controller cancels (preventDefault), don't claim\n // \"currentlyPlaying\" \u2014 the controller didn't accept the play.\n if (!evt.defaultPrevented) {\n WaveformPlayer.currentlyPlaying = this;\n }\n return undefined;\n }\n\n WaveformPlayer.currentlyPlaying = this;\n return this.audio.play();\n }\n\n /**\n * Pause audio.\n *\n * In `audioMode: 'external'`, dispatches `waveformplayer:request-pause`\n * (cancelable) and does NOT touch any audio element. See play().\n *\n * @fires WaveformPlayer#waveformplayer:request-pause\n */\n pause() {\n if (WaveformPlayer.currentlyPlaying === this) {\n WaveformPlayer.currentlyPlaying = null;\n }\n if (this.options.audioMode === 'external') {\n this._emit('waveformplayer:request-pause', this._buildTrackDetail(), true);\n return;\n }\n this.audio.pause();\n }\n\n /**\n * Build the track detail object dispatched by request-play /\n * request-pause events in external audio mode. Mirrors the shape\n * WaveformBar.play() accepts so a controller can forward it\n * directly: `WaveformBar.play(event.detail)`.\n *\n * @private\n * @return {{url:string,title:?string,subtitle:?string,artist:?string,artwork:?string,player:WaveformPlayer}}\n */\n _buildTrackDetail() {\n return {\n url: this.options.url,\n title: this.options.title,\n subtitle: this.options.subtitle,\n // Core has no separate `artist` option; mirror subtitle so the\n // published event detail is self-consistent for controllers.\n artist: this.options.artist || this.options.subtitle,\n artwork: this.options.artwork,\n markers: this.options.markers,\n waveform: this.options.waveform,\n id: this.id,\n player: this\n };\n }\n\n /**\n * External-mode state pump: flip the play/pause visual state without\n * touching audio. Mirrors what onPlay()/onPause() do but skips the\n * audio-element interactions. Safe to call repeatedly \u2014 idempotent.\n *\n * Only dispatches `waveformplayer:play`/`waveformplayer:pause` (and runs\n * the matching callback) on an actual transition, starting/stopping the\n * smooth-update loop accordingly.\n *\n * @param {boolean} playing - True to enter the playing state, false to\n * enter the paused state.\n * @fires WaveformPlayer#waveformplayer:play\n * @fires WaveformPlayer#waveformplayer:pause\n */\n setPlayingState(playing) {\n const wasPlaying = this.isPlaying;\n this.isPlaying = !!playing;\n this.setPlayButtonState(this.isPlaying);\n if (this.isPlaying && !wasPlaying) {\n this.startSmoothUpdate?.();\n this._emit('waveformplayer:play', {player: this, url: this.options.url});\n if (this.options.onPlay) this.options.onPlay(this);\n } else if (!this.isPlaying && wasPlaying) {\n this.stopSmoothUpdate?.();\n this._emit('waveformplayer:pause', {player: this, url: this.options.url});\n if (this.options.onPause) this.options.onPause(this);\n }\n }\n\n /**\n * External-mode state pump: update the visualization's progress\n * from an external clock (e.g. WaveformBar's audio element's\n * timeupdate). Drives the canvas redraw + the time displays.\n *\n * Redraws the canvas, updates the current/total time displays, stores the\n * external duration for the accessible slider, dispatches\n * `waveformplayer:timeupdate`, runs `onTimeUpdate`, and synthesizes a\n * one-shot `waveformplayer:ended` (with `onEnd`) when progress reaches the\n * end. No-op for a non-positive duration.\n *\n * @param {number} currentTime - Current playback position in seconds.\n * @param {number} duration - Total track duration in seconds.\n * @fires WaveformPlayer#waveformplayer:timeupdate\n * @fires WaveformPlayer#waveformplayer:ended\n */\n setProgress(currentTime, duration) {\n if (!duration || duration <= 0) return;\n this.progress = clamp(currentTime / duration);\n // Mirror the existing display update code so callers don't have\n // to know which DOM elements live where.\n if (this.currentTimeEl) this.currentTimeEl.textContent = formatTime(currentTime);\n // Publish the duration unconditionally \u2014 the accessible seek slider\n // and keyboard seeking read getSeekDuration()/_extDuration even when\n // there's no time display to update.\n this._extDuration = duration;\n if (this.totalTimeEl && (!this.totalTimeEl.dataset._extSet || this.totalTimeEl.dataset._extDur !== String(duration))) {\n this.totalTimeEl.textContent = formatTime(duration);\n this.totalTimeEl.dataset._extSet = '1';\n this.totalTimeEl.dataset._extDur = String(duration);\n }\n this.drawWaveform?.();\n this._emit('waveformplayer:timeupdate', {player: this, currentTime, duration, progress: this.progress, url: this.options.url});\n // Same (currentTime, duration, player) signature as self mode \u2014 the\n // arg order used to be swapped here, which made one shared handler\n // impossible across audioModes.\n if (this.options.onTimeUpdate) this.options.onTimeUpdate(currentTime, duration, this);\n\n // External mode has no <audio> 'ended' event \u2014 synthesize one when the\n // controller's progress reaches the end (fires once per playthrough).\n if (this.progress >= 1) {\n if (!this._extEnded) {\n this._extEnded = true;\n this._emit('waveformplayer:ended', {player: this, url: this.options.url, currentTime: duration, duration});\n if (this.options.onEnd) this.options.onEnd(this);\n }\n } else {\n this._extEnded = false;\n }\n\n this.updateSeekAccessibility();\n }\n\n /**\n * Toggle between play and pause based on the current `isPlaying` state.\n * Works in both audio modes (in external mode it routes through the\n * request-play/pause events).\n */\n togglePlay() {\n if (this.isPlaying) {\n this.pause();\n } else {\n this.play();\n }\n }\n\n /**\n * Seek the owned `<audio>` element to an absolute time, clamped to\n * `[0, duration]`, and refresh progress. Self mode only \u2014 a no-op when\n * there is no audio element or duration. External-mode keyboard/click\n * seeks go through {@link WaveformPlayer#seekToSeconds} instead.\n * @param {number} seconds - Target time in seconds.\n */\n seekTo(seconds) {\n if (this.audio && this.audio.duration) {\n this.audio.currentTime = clamp(seconds, 0, this.audio.duration);\n this.updateProgress();\n }\n }\n\n /**\n * Seek the owned `<audio>` element to a fraction of the track, clamped to\n * `[0, 1]`, and refresh progress. Self mode only \u2014 a no-op without an audio\n * element or duration.\n * @param {number} percent - Position as a fraction from 0 to 1.\n */\n seekToPercent(percent) {\n if (this.audio && this.audio.duration) {\n this.audio.currentTime = this.audio.duration * clamp(percent);\n this.updateProgress();\n }\n }\n\n /**\n * Set the owned `<audio>` element's volume, clamped to `[0, 1]`. Self mode\n * only \u2014 a no-op in external mode where the controller owns volume.\n * @param {number} volume - Volume from 0 (silent) to 1 (full).\n */\n setVolume(volume) {\n // Coerce + guard: a non-finite value (e.g. from a bad config or stale\n // storage) must not propagate NaN into audio.volume (which throws).\n const v = Number(volume);\n if (this.audio && Number.isFinite(v)) {\n this.audio.volume = clamp(v);\n }\n }\n\n /**\n * Set the owned `<audio>` element's playback rate (clamped to 0.5\u20132),\n * persist it onto `this.options.playbackRate`, and refresh the speed UI.\n * Self mode only \u2014 a no-op in external mode.\n * @param {number} rate - Desired playback rate; clamped to the 0.5\u20132 range.\n */\n setPlaybackRate(rate) {\n if (!this.audio) return;\n\n const clampedRate = clamp(rate, 0.5, 2);\n this.audio.playbackRate = clampedRate;\n this.options.playbackRate = clampedRate;\n\n this.updateSpeedUI();\n }\n\n /**\n * Tear down the player and release all resources.\n *\n * Flags destruction (so in-flight handlers bail), dispatches\n * `waveformplayer:destroy`, stops playback and the animation loop, aborts\n * every listener registered on the instance signal, disconnects the resize\n * observer, removes the window-resize handler, drops the instance from the\n * static map and `currentlyPlaying`, resets/releases the audio element, and\n * empties the container.\n * @fires WaveformPlayer#waveformplayer:destroy\n */\n destroy() {\n // Set a flag to indicate we're destroying\n this.isDestroying = true;\n\n // Let listeners (analytics, controllers) release their references\n // before teardown \u2014 the symmetric counterpart to waveformplayer:ready.\n this._emit('waveformplayer:destroy', {player: this, url: this.options.url});\n\n // Stop playback and animations\n this.pause();\n this.stopSmoothUpdate();\n\n // Tear down every document/container/seek listener in one shot.\n this._ac?.abort();\n\n // Disconnect observer\n if (this.resizeObserver) {\n this.resizeObserver.disconnect();\n this.resizeObserver = null;\n }\n\n // Remove window resize listener\n if (this.resizeHandler) {\n window.removeEventListener('resize', this.resizeHandler);\n this.resizeHandler = null;\n }\n\n // Remove from instances map\n WaveformPlayer.instances.delete(this.id);\n\n // Clear current playing reference if it's this instance\n if (WaveformPlayer.currentlyPlaying === this) {\n WaveformPlayer.currentlyPlaying = null;\n }\n\n // Properly clean up audio element\n if (this.audio) {\n this.audio.pause();\n this.audio.src = '';\n this.audio.load(); // Reset the audio element\n this.audio = null;\n }\n\n // Clear the container\n this.container.innerHTML = '';\n\n // Clear all references\n this.canvas = null;\n this.ctx = null;\n this.playBtn = null;\n this.waveformData = [];\n }\n\n // ============================================\n // Static Methods\n // ============================================\n\n /**\n * Get player instance by ID, element, or element ID\n * @param {string|HTMLElement} idOrElement - Player ID, element, or element ID\n * @returns {WaveformPlayer|undefined}\n */\n static getInstance(idOrElement) {\n if (typeof idOrElement === 'string') {\n const instance = this.instances.get(idOrElement);\n if (instance) return instance;\n\n const element = document.getElementById(idOrElement);\n if (element) {\n return Array.from(this.instances.values()).find(p => p.container === element);\n }\n }\n\n if (idOrElement instanceof HTMLElement) {\n return Array.from(this.instances.values()).find(p => p.container === idOrElement);\n }\n\n return undefined;\n }\n\n /**\n * Get all player instances\n * @returns {WaveformPlayer[]}\n */\n static getAllInstances() {\n return Array.from(this.instances.values());\n }\n\n /**\n * Destroy all player instances\n */\n static destroyAll() {\n this.instances.forEach(player => player.destroy());\n this.instances.clear();\n }\n\n /**\n * Generate waveform data from audio URL\n * @static\n * @param {string} url - Audio URL\n * @param {number} samples - Number of samples\n * @returns {Promise<number[]>} Waveform peak data\n */\n static async generateWaveformData(url, samples = 200) {\n try {\n const result = await generateWaveform(url, samples);\n return result.peaks;\n } catch (error) {\n console.error('[WaveformPlayer] Failed to generate waveform:', error);\n throw error;\n }\n }\n\n /**\n * Derive a peaks-JSON URL from an audio URL by swapping the\n * extension. Strict counterpart to `generateWaveformData()`:\n * `generateWaveformData` decodes the audio at runtime,\n * `getPeaksUrl` assumes you generated the peaks at build time\n * (e.g. with `@arraypress/waveform-gen`) and stored the JSON\n * alongside the audio file.\n *\n * Use the result as the `waveform` option \u2014 the player detects\n * the `.json` suffix, `fetch()`es the file, and skips the Web\n * Audio decode pass entirely. Big perf win on catalogues with\n * many tracks (saves ~1-5s decode per file on slow connections).\n *\n * Recognised extensions: mp3, wav, ogg, flac, m4a, aac.\n * Preserves query strings + URL fragments. Returns `undefined`\n * for unrecognised inputs so callers can pass through\n * unconditionally:\n *\n * new WaveformPlayer('#el', {\n * url: track.audioUrl,\n * waveform: WaveformPlayer.getPeaksUrl(track.audioUrl),\n * });\n *\n * @static\n * @param {string|undefined|null} audioUrl - Audio file URL.\n * @returns {string|undefined} Peaks JSON URL, or `undefined`\n * when the input is empty / has no recognised audio extension.\n *\n * @example\n * WaveformPlayer.getPeaksUrl('/audio/track.mp3')\n * // '/audio/track.json'\n *\n * WaveformPlayer.getPeaksUrl('/audio/track.wav?v=2')\n * // '/audio/track.json?v=2'\n *\n * WaveformPlayer.getPeaksUrl(undefined)\n * // undefined\n */\n static getPeaksUrl(audioUrl) {\n if (!audioUrl) return undefined;\n const swapped = audioUrl.replace(\n /\\.(mp3|wav|ogg|flac|m4a|aac)(\\?[^#]*)?(#.*)?$/i,\n '.json$2$3'\n );\n /* Nothing changed \u2192 unrecognised extension, return undefined\n * so callers know to fall back to live decoding. */\n return swapped === audioUrl ? undefined : swapped;\n }\n\n}", "/**\n * @module index\n * @description Public entry point for the WaveformPlayer library.\n *\n * Wires together the runtime surfaces for the player: it re-exports the\n * {@link WaveformPlayer} class (default and named), exposes a static\n * `WaveformPlayer.init` hook, scans the DOM for declarative `[data-waveform-player]`\n * markup and auto-instantiates a player for each match, and attaches the class\n * to `window` for plain `<script>`/CDN usage. Loading this module is enough to\n * make any markup-driven players on the page come alive once the DOM is ready.\n */\n\n// Import the main class\nimport {WaveformPlayer} from './core.js';\nimport {formatTime, extractTitleFromUrl, escapeHtml, isSafeHref, parseDataAttributes} from './utils.js';\n\n// Expose a small set of pure helpers as a single source of truth so consumers\n// (e.g. @arraypress/waveform-bar, @arraypress/waveform-playlist) can reuse them\n// instead of shipping divergent copies. `parseDataAttributes` lets wrappers read\n// the player's full `data-*` option surface off a host element without\n// re-implementing (and drifting from) the contract. Attached to the class so\n// it's reachable from the IIFE global too.\nWaveformPlayer.utils = {formatTime, extractTitleFromUrl, escapeHtml, isSafeHref, parseDataAttributes};\n\n/**\n * Whether we're running in a browser (vs. SSR / Node), where `window` and\n * `document` are available. Guards the auto-init and global-attach steps.\n * @returns {boolean}\n */\nconst isBrowser = () => typeof window !== 'undefined' && typeof document !== 'undefined';\n\n/**\n * Scan the document for declarative player markup and instantiate one\n * {@link WaveformPlayer} per matching element.\n *\n * Finds every element carrying the `data-waveform-player` attribute and, for\n * each one not already initialized, constructs a player from it (the constructor\n * reads the element's `data-*` attributes for configuration). Each successfully\n * initialized element is flagged with `data-waveform-initialized=\"true\"` so\n * repeat calls are idempotent and never double-initialize the same element.\n * Construction errors are caught and logged so one broken element cannot abort\n * the rest of the scan. A no-op in non-DOM environments (e.g. SSR).\n *\n * @returns {void}\n */\nfunction autoInit() {\n if (!isBrowser()) return;\n\n const elements = document.querySelectorAll('[data-waveform-player]');\n\n elements.forEach(element => {\n if (element.dataset.waveformInitialized === 'true') return;\n\n try {\n new WaveformPlayer(element);\n element.dataset.waveformInitialized = 'true';\n } catch (error) {\n console.error('[WaveformPlayer] Failed to initialize:', error, element);\n }\n });\n}\n\n// Initialize when DOM is ready: defer until DOMContentLoaded if the document is\n// still parsing, otherwise run the scan immediately on import.\nif (isBrowser()) {\n if (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', autoInit);\n } else {\n autoInit();\n }\n}\n\n/**\n * Static re-scan hook.\n *\n * Exposes {@link autoInit} as `WaveformPlayer.init` so callers can manually\n * (re-)scan the DOM after dynamically injecting `[data-waveform-player]` markup.\n * Already-initialized elements are skipped on subsequent calls.\n *\n * @type {typeof autoInit}\n */\nWaveformPlayer.init = autoInit;\n\n// For CDN/browser usage: expose the class as a global so it is reachable from a\n// plain <script> tag without an ES module loader.\nif (isBrowser()) {\n window.WaveformPlayer = WaveformPlayer;\n}\n\n/**\n * The {@link WaveformPlayer} class.\n * @type {typeof WaveformPlayer}\n */\nexport default WaveformPlayer;\n\n// Named exports\nexport {WaveformPlayer};"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["escapeHtml", "str", "isSafeHref", "url", "u", "clamp", "value", "min", "max", "parseBoolAttr", "parseColorValue", "parseDataAttributes", "element", "options", "setBool", "optKey", "dataKey", "v", "setNum", "float", "raw", "setJson", "e", "formatTime", "seconds", "hrs", "mins", "secs", "idCounter", "generateId", "hash", "i", "extractTitleFromUrl", "parts", "l", "perceivedBrightness", "color", "rgb", "r", "g", "b", "mergeOptions", "sources", "result", "source", "key", "debounce", "func", "wait", "timeout", "args", "later", "resampleData", "data", "targetLength", "ratio", "index", "lower", "upper", "fraction", "bucketSize", "start", "end", "count", "j", "nearestIndex", "makeFill", "ctx", "value", "height", "grad", "c", "i", "fillBar", "x", "y", "w", "h", "radii", "r", "max", "clampR", "clamp", "barRadiusPx", "options", "dpr", "barRadii", "capsulePath", "startX", "endX", "centerY", "barHeight", "drawBars", "canvas", "peaks", "progress", "barWidth", "barSpacing", "barCount", "resampledPeaks", "resampleData", "progressWidth", "baseFill", "progFill", "peakHeight", "drawMirror", "topRadii", "botRadii", "drawLine", "width", "amplitude", "drawCurve", "color", "lineWidth", "endProgress", "addGlow", "points", "samples", "peakValue", "waveOffset", "cp1x", "cp1y", "cp2x", "cp2y", "drawBlocks", "blockSize", "blockGap", "blockCount", "j", "blockOffset", "drawDots", "dotRadius", "drawSeekbar", "borderRadius", "handleRadius", "handleX", "DRAWING_STYLES", "draw", "detectBPM", "buffer", "channelData", "sampleRate", "onsets", "detectOnsets", "intervals", "i", "tempoGroups", "interval", "tempo", "bucket", "maxCount", "detectedBPM", "count", "e", "previousEnergy", "energy", "j", "energyDiff", "threshold", "lastOnset", "minDistance", "extractPeaks", "buffer", "samples", "sampleSize", "sampleStep", "channels", "peaks", "c", "chan", "i", "start", "end", "min", "max", "j", "value", "peak", "maxPeak", "generateWaveform", "url", "shouldDetectBPM", "audioContext", "AudioCtx", "arrayBuffer", "audioBuffer", "normalizePeaks", "bpm", "detectBPM", "generatePlaceholderWaveform", "data", "base", "variation", "clamp", "targetMax", "scaleFactor", "hasThemeHint", "scheme", "root", "body", "detectColorScheme", "bodyBg", "brightness", "perceivedBrightness", "COLOR_PRESETS", "getColorPreset", "presetName", "detected", "DEFAULT_OPTIONS", "STYLE_DEFAULTS", "SEEK_STEP_SECONDS", "SEEK_PAGE_SECONDS", "WaveformPlayer", "_WaveformPlayer", "container", "options", "dataOptions", "parseDataAttributes", "userOptions", "mergeOptions", "DEFAULT_OPTIONS", "preset", "getColorPreset", "key", "value", "styleDefaults", "STYLE_DEFAULTS", "generateId", "type", "detail", "cancelable", "event", "percent", "error", "buttonAlign", "buttonHTML", "infoHTML", "rate", "escapeHtml", "speedBtn", "speedMenu", "e", "player", "hasAudio", "currentTime", "actions", "clamp", "duration", "current", "target", "seconds", "clamped", "title", "label", "formatTime", "details", "debounce", "url", "resolve", "reject", "metadataHandler", "errorHandler", "extractTitleFromUrl", "result", "generateWaveform", "generatePlaceholderWaveform", "subtitle", "data", "r", "json", "parsed", "draw", "dpr", "rect", "marker", "index", "position", "markerEl", "tooltip", "el", "i", "x", "targetPercent", "loading", "isPlaying", "playIcon", "pauseIcon", "update", "newProgress", "bpm", "speedValue", "btn", "playing", "wasPlaying", "volume", "v", "clampedRate", "idOrElement", "instance", "element", "p", "samples", "audioUrl", "swapped", "WaveformPlayer", "formatTime", "extractTitleFromUrl", "escapeHtml", "isSafeHref", "parseDataAttributes", "isBrowser", "autoInit", "element", "error", "index_default"]
|
|
7
7
|
}
|