diff --git a/src/AudioPlayerComponent.js b/src/AudioPlayerComponent.js index 9f3ceb1..3ea3843 100644 --- a/src/AudioPlayerComponent.js +++ b/src/AudioPlayerComponent.js @@ -29,7 +29,6 @@ export class AudioPlayerComponent { this.createPlayerHTML(); this.initializeElements(); this.setupEventListeners(); - this.setupCanvas(); this.initializeAudioContext(); } @@ -69,7 +68,7 @@ export class AudioPlayerComponent {
- +
@@ -171,20 +170,6 @@ export class AudioPlayerComponent { }); } - setupCanvas() { - if (!this.canvas) return; - - const resizeCanvas = () => { - const container = this.canvas.parentElement; - const rect = container.getBoundingClientRect(); - this.canvas.width = Math.min(600, rect.width - 40); - this.canvas.height = 300; - }; - - resizeCanvas(); - window.addEventListener('resize', resizeCanvas); - } - initializeAudioContext() { try { this.audioContext = new (window.AudioContext || window.webkitAudioContext)(); @@ -655,11 +640,38 @@ export class AudioPlayerComponent { }, 500); } - // 更新歌曲标题的方法 + // 更新歌曲标题的方法 - 从媒体信息获取 updateSongTitle() { if (!this.currentAudioUrl) return; - // 从URL中提取歌曲信息 + // 尝试从音频文件的媒体信息中获取歌曲信息 + this.extractAudioMetadata(this.currentAudioUrl).then(metadata => { + // 触发自定义事件,通知外部更新标题 + const event = new CustomEvent('songLoaded', { + detail: { + title: metadata.title || '未知歌曲', + artist: metadata.artist || '未知艺术家', + album: metadata.album || '', + url: this.currentAudioUrl + } + }); + + // 从播放器容器向上冒泡事件 + if (this.container && this.container.parentNode) { + this.container.parentNode.dispatchEvent(event); + } + }).catch(error => { + console.error('Failed to extract audio metadata:', error); + // 失败时回退到从URL提取 + this.extractTitleFromUrl(); + }); + } + + // 从URL提取歌曲信息的回退方法 + extractTitleFromUrl() { + if (!this.currentAudioUrl) return; + + // 从URL中提取歌曲信息作为回退 let title = '未知歌曲'; try { const urlParts = this.currentAudioUrl.split('/'); @@ -668,13 +680,15 @@ export class AudioPlayerComponent { const nameWithoutQuery = decodedFilename.split('?')[0]; title = nameWithoutQuery.split('.')[0].replace(/[-_]/g, ' ').trim(); } catch (e) { - console.error('Failed to extract song title:', e); + console.error('Failed to extract song title from URL:', e); } // 触发自定义事件,通知外部更新标题 const event = new CustomEvent('songLoaded', { detail: { title: title, + artist: '', + album: '', url: this.currentAudioUrl } }); @@ -685,6 +699,133 @@ export class AudioPlayerComponent { } } + // 从音频文件提取元数据 + async extractAudioMetadata(url) { + try { + // 创建AudioContext + const audioContext = new (window.AudioContext || window.webkitAudioContext)(); + + // 获取音频文件 + const response = await fetch(url, { mode: 'cors' }); + const arrayBuffer = await response.arrayBuffer(); + + // 解码音频数据 + const audioBuffer = await audioContext.decodeAudioData(arrayBuffer); + + // 尝试提取ID3标签信息 + const metadata = this.parseID3Tags(arrayBuffer); + + // 关闭AudioContext + audioContext.close(); + + return metadata; + } catch (error) { + console.error('Error extracting audio metadata:', error); + return { title: '', artist: '', album: '' }; + } + } + + // 解析ID3标签 + parseID3Tags(arrayBuffer) { + const metadata = { title: '', artist: '', album: '' }; + + try { + const dv = new DataView(arrayBuffer); + let offset = 0; + let header = ''; + + // 检查是否有ID3标签 + if (dv.getUint8(0) === 0x49 && dv.getUint8(1) === 0x44 && dv.getUint8(2) === 0x33) { + // 跳过ID3头 + offset += 10; + + // 读取帧 + while (offset < arrayBuffer.byteLength) { + // 读取帧头 + const frameId = String.fromCharCode( + dv.getUint8(offset), + dv.getUint8(offset + 1), + dv.getUint8(offset + 2), + dv.getUint8(offset + 3) + ); + + // 读取帧大小 + const frameSize = + (dv.getUint8(offset + 4) << 21) + + (dv.getUint8(offset + 5) << 14) + + (dv.getUint8(offset + 6) << 7) + + dv.getUint8(offset + 7); + + // 读取帧标志 + offset += 10; + + // 读取帧内容 + if (frameId === 'TIT2') { + metadata.title = this.decodeID3Text(dv, offset, frameSize); + } else if (frameId === 'TPE1') { + metadata.artist = this.decodeID3Text(dv, offset, frameSize); + } else if (frameId === 'TALB') { + metadata.album = this.decodeID3Text(dv, offset, frameSize); + } + + offset += frameSize; + } + } + } catch (error) { + console.error('Error parsing ID3 tags:', error); + } + + return metadata; + } + + // 解码ID3文本 + decodeID3Text(dv, offset, length) { + try { + // 检查编码方式 (0: ISO-8859-1, 1: UTF-16 with BOM, 2: UTF-16BE, 3: UTF-8) + const encoding = dv.getUint8(offset); + offset += 1; + length -= 1; + + let text = ''; + + if (encoding === 0) { + // ISO-8859-1 + for (let i = 0; i < length; i++) { + const charCode = dv.getUint8(offset + i); + if (charCode === 0) break; // 结束符 + text += String.fromCharCode(charCode); + } + } else if (encoding === 1 || encoding === 2) { + // UTF-16 + const littleEndian = encoding === 1; + if (littleEndian) { + // 跳过BOM + offset += 2; + length -= 2; + } + + for (let i = 0; i < length; i += 2) { + const charCode = littleEndian ? + (dv.getUint8(offset + i + 1) << 8) + dv.getUint8(offset + i) : + (dv.getUint8(offset + i) << 8) + dv.getUint8(offset + i + 1); + if (charCode === 0) break; // 结束符 + text += String.fromCharCode(charCode); + } + } else if (encoding === 3) { + // UTF-8 + const uint8Array = new Uint8Array(dv.buffer, offset, length); + text = new TextDecoder('utf-8').decode(uint8Array); + // 移除可能的结束符 + text = text.replace(/\u0000.*$/, ''); + } + + return text.trim(); + } catch (error) { + console.error('Error decoding ID3 text:', error); + return ''; + } + } + handleAudioError(error) { console.error('音频加载错误详情:', { error: error, @@ -872,33 +1013,63 @@ export class AudioPlayerComponent { drawCircle() { if (!this.ctx || !this.canvas) return; - this.ctx.fillStyle = 'rgba(0, 0, 0, 0.2)'; + this.ctx.fillStyle = 'rgb(0, 0, 0)'; this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); - const centerX = this.canvas.width / 2; - const centerY = this.canvas.height / 2; - const radius = Math.min(centerX, centerY) - 50; + // 获取画布的实际显示尺寸(考虑设备像素比) + const displayWidth = this.canvas.width / window.devicePixelRatio; + const displayHeight = this.canvas.height / window.devicePixelRatio; - for (let i = 0; i < this.bufferLength; i++) { + // 圆心位置在画布中央 + const centerX = displayWidth / 2 + Math.min(displayWidth,displayHeight) / 4; //增加一点偏移量 + const centerY = displayHeight / 2; + + // 缩小圆形图大小,使用较小的半径 + const baseRadius = Math.min(centerX, centerY) * 0.5; // 从0.8改为0.5,缩小一半 + const maxAmplitude = Math.min(centerX, centerY) * 0.5; // 最大振幅也相应缩小 + + // 绘制内部基础圆形(可选,增加层次感) + this.ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)'; + this.ctx.lineWidth = 1; + this.ctx.beginPath(); + this.ctx.arc(centerX, centerY, baseRadius, 0, 2 * Math.PI); + this.ctx.stroke(); + + // 绘制频谱线条 + const step = Math.max(1, Math.floor(this.bufferLength / 128)); // 减少线条数量,提高性能 + + for (let i = 0; i < this.bufferLength; i += step) { const angle = (i / this.bufferLength) * Math.PI * 2; - const barHeight = (this.dataArray[i] / 255) * radius; + const amplitude = (this.dataArray[i] / 255) * maxAmplitude; - const x1 = centerX + Math.cos(angle) * radius; - const y1 = centerY + Math.sin(angle) * radius; - const x2 = centerX + Math.cos(angle) * (radius + barHeight); - const y2 = centerY + Math.sin(angle) * (radius + barHeight); + // 起始点(基础圆上) + const x1 = centerX + Math.cos(angle) * baseRadius; + const y1 = centerY + Math.sin(angle) * baseRadius; - const gradient = this.ctx.createLinearGradient(x1, y1, x2, y2); - gradient.addColorStop(0, '#667eea'); - gradient.addColorStop(1, '#764ba2'); + // 结束点(根据频率数据延伸) + const x2 = centerX + Math.cos(angle) * (baseRadius + amplitude); + const y2 = centerY + Math.sin(angle) * (baseRadius + amplitude); - this.ctx.strokeStyle = gradient; - this.ctx.lineWidth = 3; + // 根据频率数据计算颜色 + const intensity = this.dataArray[i] / 255; + const hue = (i / this.bufferLength) * 360; // 彩虹色调 + const saturation = 80 + (intensity * 20); // 饱和度随强度变化 + const lightness = 40 + (intensity * 40); // 亮度随强度变化 + + this.ctx.strokeStyle = `hsl(${hue}, ${saturation}%, ${lightness}%)`; + this.ctx.lineWidth = 1.5 + (intensity * 1.5); // 线条宽度随强度变化 this.ctx.beginPath(); this.ctx.moveTo(x1, y1); this.ctx.lineTo(x2, y2); this.ctx.stroke(); } + + // 绘制中心点(可选,增加视觉焦点) + this.ctx.fillStyle = 'rgba(255, 255, 255, 0.8)'; + this.ctx.beginPath(); + this.ctx.arc(centerX, centerY, 2, 0, 2 * Math.PI); + this.ctx.fill(); + } // 工具方法 @@ -998,10 +1169,16 @@ export function createAudioPlayerDialog(options = {}) { titleBar.className = 'dialog-title-bar'; // 创建歌曲信息区域 - const songInfo = document.createElement('div'); - songInfo.className = 'song-info'; - songInfo.innerHTML = '
正在播放
'; - titleBar.appendChild(songInfo); + const songInfo = document.createElement('div'); + songInfo.className = 'song-info'; + songInfo.innerHTML = ` +
+ 正在播放 + + +
+ `; + titleBar.appendChild(songInfo); // 创建关闭按钮 const closeBtn = document.createElement('button'); @@ -1013,6 +1190,7 @@ export function createAudioPlayerDialog(options = {}) { // 创建播放器容器 const playerContainer = document.createElement('div'); + playerContainer.className="player-dialog-component"; dialog.appendChild(playerContainer); document.body.appendChild(dialog); @@ -1021,9 +1199,10 @@ export function createAudioPlayerDialog(options = {}) { const player = new AudioPlayerComponent(playerContainer, mergedOptions); // 更新歌曲信息的方法 - const updateSongInfo = (url, title = null) => { + const updateSongInfo = (url, title = null, artist = null, album = null) => { const songTitle = songInfo.querySelector('.song-title'); - const songUrl = songInfo.querySelector('.song-url'); + const songArtist = songInfo.querySelector('.song-artist'); + const songAlbum = songInfo.querySelector('.song-album'); // 从URL中提取歌曲名称(如果可能) let finalTitle = title || '未知歌曲'; @@ -1039,8 +1218,10 @@ export function createAudioPlayerDialog(options = {}) { } } + // 更新标题和艺术家信息 songTitle.textContent = finalTitle; - songUrl.textContent = url || ''; + songArtist.textContent = artist ? ` - ${artist}` : ''; + songAlbum.textContent = album ? ` | ${album}` : ''; }; // 监听加载事件更新歌曲信息 @@ -1052,8 +1233,8 @@ export function createAudioPlayerDialog(options = {}) { // 监听歌曲加载完成事件,更新更准确的标题信息 dialog.addEventListener('songLoaded', (e) => { - const { title, url } = e.detail; - updateSongInfo(url, title); + const { title, artist, album, url } = e.detail; + updateSongInfo(url, title, artist, album); }); closeBtn.addEventListener('click', () => { diff --git a/src/style.css b/src/style.css index b457473..a784472 100644 --- a/src/style.css +++ b/src/style.css @@ -84,20 +84,6 @@ body { transform: translateY(-2px); } -/* 可视化区域 */ -.visualization-section { - margin-bottom: 30px; - text-align: center; -} - -#visualizer { - border: 2px solid #e0e0e0; - border-radius: 15px; - background: #000; - width: 100%; - height: 320px; -} - .visualization-controls { margin-top: 15px; display: flex; @@ -1123,6 +1109,11 @@ body.loading-active { animation: pulse 2s infinite; } +.player-dialog-component{ + width: 100%; + overflow: hidden; +} + /* 音频播放器组件样式 */ .audio-player-component { background: rgba(255, 255, 255, 0.95); @@ -1136,9 +1127,15 @@ body.loading-active { /* 可视化区域 */ .visualization-section { margin-bottom: 30px; + margin-left: auto; + margin-right: auto; + overflow: hidden; + text-align: center; + position: relative; } #visualizer { + margin: 0 0; width: 100%; height: 300px; border-radius: 15px; @@ -1552,6 +1549,24 @@ button:focus-visible { min-width: 320px; } +/* 优化滚动条样式 */ +.audio-player-dialog > div::-webkit-scrollbar { + width: 6px; +} + +.audio-player-dialog > div::-webkit-scrollbar-track { + background: #f1f1f1; +} + +.audio-player-dialog > div::-webkit-scrollbar-thumb { + background: #c1c1c1; + border-radius: 3px; +} + +.audio-player-dialog > div::-webkit-scrollbar-thumb:hover { + background: #a8a8a8; +} + .audio-player-dialog::backdrop { background: rgba(0, 0, 0, 0.5); backdrop-filter: blur(5px); @@ -1575,21 +1590,39 @@ button:focus-visible { overflow: hidden; } +.song-main-info { + display: flex; + align-items: center; + gap: 5px; + flex-wrap: wrap; +} + .song-title { font-size: 16px; font-weight: bold; - margin-bottom: 3px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.song-artist { + font-size: 14px; + opacity: 0.9; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.song-album { + font-size: 12px; + opacity: 0.8; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .song-url { - font-size: 12px; - opacity: 0.9; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + display: none; /* 隐藏URL,改为显示媒体信息 */ } /* 关闭按钮 */