@@ -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,改为显示媒体信息 */
}
/* 关闭按钮 */