@arraypress/waveform-player
Version:
Lightweight, customizable audio player with waveform visualization
237 lines (200 loc) • 8.09 kB
JavaScript
/**
* @module utils
* @description Utility functions for WaveformPlayer
*/
/**
* Parse data attributes from element
* @param {HTMLElement} element - Element with data attributes
* @returns {Object} Parsed options
*/
export function parseDataAttributes(element) {
const options = {};
// Core attributes
if (element.dataset.url) options.url = element.dataset.url;
if (element.dataset.height) options.height = parseInt(element.dataset.height);
if (element.dataset.samples) options.samples = parseInt(element.dataset.samples);
if (element.dataset.preload) {
options.preload = element.dataset.preload;
}
// Waveform style attributes
if (element.dataset.waveformStyle) options.waveformStyle = element.dataset.waveformStyle;
if (element.dataset.barWidth) options.barWidth = parseInt(element.dataset.barWidth);
if (element.dataset.barSpacing) options.barSpacing = parseInt(element.dataset.barSpacing);
if (element.dataset.buttonAlign) options.buttonAlign = element.dataset.buttonAlign;
// Color preset
if (element.dataset.colorPreset) options.colorPreset = element.dataset.colorPreset;
// Individual color customization
if (element.dataset.waveformColor) options.waveformColor = element.dataset.waveformColor;
if (element.dataset.progressColor) options.progressColor = element.dataset.progressColor;
if (element.dataset.buttonColor) options.buttonColor = element.dataset.buttonColor;
if (element.dataset.buttonHoverColor) options.buttonHoverColor = element.dataset.buttonHoverColor;
if (element.dataset.textColor) options.textColor = element.dataset.textColor;
if (element.dataset.textSecondaryColor) options.textSecondaryColor = element.dataset.textSecondaryColor;
if (element.dataset.backgroundColor) options.backgroundColor = element.dataset.backgroundColor;
if (element.dataset.borderColor) options.borderColor = element.dataset.borderColor;
// Legacy support for old attribute names
if (element.dataset.color) options.waveformColor = element.dataset.color;
if (element.dataset.theme) options.colorPreset = element.dataset.theme;
// Feature flags
if (element.dataset.autoplay) options.autoplay = element.dataset.autoplay === 'true';
if (element.dataset.showTime) options.showTime = element.dataset.showTime === 'true';
if (element.dataset.showHoverTime) options.showHoverTime = element.dataset.showHoverTime === 'true';
if (element.dataset.showBpm) options.showBPM = element.dataset.showBpm === 'true';
if (element.dataset.singlePlay) options.singlePlay = element.dataset.singlePlay === 'true';
if (element.dataset.playOnSeek) options.playOnSeek = element.dataset.playOnSeek === 'true';
// Content and metadata
if (element.dataset.title) options.title = element.dataset.title;
if (element.dataset.subtitle) options.subtitle = element.dataset.subtitle;
if (element.dataset.album) options.album = element.dataset.album;
if (element.dataset.artwork) options.artwork = element.dataset.artwork;
// Waveform data
if (element.dataset.waveform) options.waveform = element.dataset.waveform;
// Markers
if (element.dataset.markers) {
try {
options.markers = JSON.parse(element.dataset.markers);
} catch (e) {
console.warn('Invalid markers JSON:', e);
}
}
// Playback controls
if (element.dataset.playbackRate) {
options.playbackRate = parseFloat(element.dataset.playbackRate);
}
if (element.dataset.showPlaybackSpeed !== undefined) {
options.showPlaybackSpeed = element.dataset.showPlaybackSpeed === 'true';
}
if (element.dataset.playbackRates) {
try {
options.playbackRates = JSON.parse(element.dataset.playbackRates);
} catch (e) {
console.warn('Invalid playbackRates JSON:', e);
}
}
// Media Session API
if (element.dataset.enableMediaSession !== undefined) {
options.enableMediaSession = element.dataset.enableMediaSession === 'true';
}
return options;
}
/**
* Format time in MM:SS format
* @param {number} seconds - Time in seconds
* @returns {string} Formatted time
*/
export function formatTime(seconds) {
if (!seconds || isNaN(seconds)) return '0:00';
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs.toString().padStart(2, '0')}`;
}
/**
* Generate unique ID from URL
* @param {string} url - Audio URL
* @returns {string} Base64 encoded ID
*/
export function generateId(url) {
const str = url || Math.random().toString();
return btoa(str.substring(0, 10)).replace(/[^a-zA-Z0-9]/g, '');
}
/**
* Extract title from URL
* @param {string} url - Audio URL
* @returns {string} Extracted title
*/
export function extractTitleFromUrl(url) {
if (!url) return 'Audio';
const parts = url.split('/');
const filename = parts[parts.length - 1];
const name = filename.split('.')[0];
// Clean up common separators
return name
.replace(/[-_]/g, ' ')
.replace(/\b\w/g, l => l.toUpperCase());
}
/**
* Merge multiple option objects
* @param {...Object} sources - Option objects to merge
* @returns {Object} Merged options
*/
export function mergeOptions(...sources) {
const result = {};
for (const source of sources) {
for (const key in source) {
if (source[key] !== null && source[key] !== undefined) {
result[key] = source[key];
}
}
}
return result;
}
/**
* Debounce function
* @param {Function} func - Function to debounce
* @param {number} wait - Wait time in ms
* @returns {Function} Debounced function
*/
export function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
/**
* Resample array data
* @param {number[]} data - Original data
* @param {number} targetLength - Target length
* @returns {number[]} Resampled data
*/
export function resampleData(data, targetLength) {
if (data.length === targetLength) return data;
if (data.length === 0 || targetLength === 0) return [];
const result = [];
// If upsampling (target is larger than source)
if (targetLength > data.length) {
const ratio = (data.length - 1) / (targetLength - 1);
for (let i = 0; i < targetLength; i++) {
const index = i * ratio;
const lower = Math.floor(index);
const upper = Math.ceil(index);
const fraction = index - lower;
// Linear interpolation between samples
if (upper >= data.length) {
result.push(data[data.length - 1]);
} else if (lower === upper) {
result.push(data[lower]);
} else {
const value = data[lower] * (1 - fraction) + data[upper] * fraction;
result.push(value);
}
}
} else {
// Downsampling (target is smaller than source)
const bucketSize = data.length / targetLength;
for (let i = 0; i < targetLength; i++) {
const start = Math.floor(i * bucketSize);
const end = Math.floor((i + 1) * bucketSize);
// Find the maximum value in this bucket
let max = 0;
let count = 0;
for (let j = start; j <= end && j < data.length; j++) {
if (data[j] > max) {
max = data[j];
}
count++;
}
// If no samples were found in this bucket, use nearest neighbor
if (count === 0) {
const nearestIndex = Math.min(Math.round(i * bucketSize), data.length - 1);
max = data[nearestIndex];
}
result.push(max);
}
}
return result;
}