源代码如下:
DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MP3元数据解析器title>
<style>
body {
font-family: 'Arial', sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
}
h1 {
color: #333;
text-align: center;
}
.container {
background-color: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.upload-area {
border: 2px dashed #ccc;
padding: 20px;
text-align: center;
margin-bottom: 20px;
border-radius: 5px;
cursor: pointer;
}
.upload-area:hover {
border-color: #007bff;
}
.upload-area.active {
border-color: #28a745;
background-color: rgba(40, 167, 69, 0.1);
}
.btn {
background-color: #007bff;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
margin-top: 10px;
}
.btn:hover {
background-color: #0069d9;
}
.btn:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
.btn-success {
background-color: #28a745;
}
.btn-success:hover {
background-color: #218838;
}
.btn-container {
display: flex;
justify-content: center;
gap: 10px;
}
.metadata {
margin-top: 20px;
border-top: 1px solid #eee;
padding-top: 20px;
}
.metadata-item {
margin-bottom: 10px;
display: flex;
}
.metadata-label {
font-weight: bold;
width: 150px;
}
.metadata-value {
flex: 1;
}
.hidden {
display: none;
}
#file-name {
margin-top: 10px;
font-style: italic;
}
.error {
color: #dc3545;
margin-top: 10px;
}
.player-container {
margin-top: 20px;
padding: 15px;
border: 1px solid #eee;
border-radius: 5px;
background-color: #f9f9f9;
}
.audio-controls {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 10px;
}
.progress-container {
flex-grow: 1;
height: 8px;
background-color: #ddd;
border-radius: 4px;
margin: 0 15px;
cursor: pointer;
position: relative;
}
.progress-bar {
height: 100%;
background-color: #007bff;
border-radius: 4px;
width: 0;
}
.time-display {
font-size: 14px;
color: #666;
min-width: 45px;
}
.volume-container {
display: flex;
align-items: center;
margin-left: 15px;
}
.volume-slider {
width: 80px;
margin-left: 10px;
}
/* 频谱分析器样式 */
.visualizer-container {
margin-top: 15px;
width: 100%;
height: 150px;
background-color: #000;
border-radius: 5px;
overflow: hidden;
position: relative;
}
#visualizer {
width: 100%;
height: 100%;
display: block;
}
.visualizer-controls {
display: flex;
justify-content: center;
margin-top: 10px;
gap: 15px;
}
.visualizer-btn {
background-color: #6c757d;
color: white;
border: none;
padding: 5px 10px;
border-radius: 3px;
cursor: pointer;
font-size: 14px;
}
.visualizer-btn:hover {
background-color: #5a6268;
}
.visualizer-btn.active {
background-color: #007bff;
}
style>
head>
<body>
<div class="container">
<h1>MP3元数据解析器h1>
<div class="upload-area" id="drop-area">
<p>拖放MP3文件到这里,或点击选择文件p>
<input type="file" id="file-input" accept=".mp3" class="hidden">
<div id="file-name">div>
div>
<div class="btn-container">
<button id="parse-btn" class="btn" disabled>解析元数据button>
<button id="play-btn" class="btn btn-success hidden">播放音乐button>
div>
<div id="player-container" class="player-container hidden">
<div class="audio-controls">
<span class="time-display" id="current-time">0:00span>
<div class="progress-container" id="progress-container">
<div class="progress-bar" id="progress-bar">div>
div>
<span class="time-display" id="duration">0:00span>
<div class="volume-container">
<i class="volume-icon">i>
<input type="range" class="volume-slider" id="volume-slider" min="0" max="1" step="0.1" value="1">
div>
div>
<div class="visualizer-container">
<canvas id="visualizer">canvas>
div>
<div class="visualizer-controls">
<button class="visualizer-btn active" id="bars-btn">柱状图button>
<button class="visualizer-btn" id="wave-btn">波形图button>
<button class="visualizer-btn" id="circle-btn">环形图button>
div>
div>
<div id="metadata-container" class="metadata hidden">
<h2>文件元数据h2>
<div id="metadata-content">div>
div>
<div id="error-message" class="error hidden">div>
div>
<script src="jsmediatags.min.js">script>
<script>
document.addEventListener('DOMContentLoaded', function() {
const dropArea = document.getElementById('drop-area');
const fileInput = document.getElementById('file-input');
const fileName = document.getElementById('file-name');
const parseBtn = document.getElementById('parse-btn');
const playBtn = document.getElementById('play-btn');
const playerContainer = document.getElementById('player-container');
const metadataContainer = document.getElementById('metadata-container');
const metadataContent = document.getElementById('metadata-content');
const errorMessage = document.getElementById('error-message');
const progressBar = document.getElementById('progress-bar');
const progressContainer = document.getElementById('progress-container');
const currentTimeDisplay = document.getElementById('current-time');
const durationDisplay = document.getElementById('duration');
const volumeSlider = document.getElementById('volume-slider');
const visualizer = document.getElementById('visualizer');
const barsBtn = document.getElementById('bars-btn');
const waveBtn = document.getElementById('wave-btn');
const circleBtn = document.getElementById('circle-btn');
let selectedFile = null;
let audioElement = null;
let audioContext = null;
let audioSource = null;
let analyser = null;
let isPlaying = false;
let animationId = null;
let visualizerType = 'bars'; // 默认可视化类型
let audioContextInitialized = false;
// 设置Canvas
const canvas = visualizer;
const canvasCtx = canvas.getContext('2d');
// 调整Canvas大小以匹配容器
function resizeCanvas() {
const container = canvas.parentElement;
canvas.width = container.clientWidth;
canvas.height = container.clientHeight;
}
// 初始调整Canvas大小
resizeCanvas();
// 窗口大小改变时调整Canvas大小
window.addEventListener('resize', resizeCanvas);
// 可视化类型切换按钮
barsBtn.addEventListener('click', () => {
setVisualizerType('bars');
setActiveButton(barsBtn);
});
waveBtn.addEventListener('click', () => {
setVisualizerType('wave');
setActiveButton(waveBtn);
});
circleBtn.addEventListener('click', () => {
setVisualizerType('circle');
setActiveButton(circleBtn);
});
function setVisualizerType(type) {
visualizerType = type;
}
function setActiveButton(button) {
[barsBtn, waveBtn, circleBtn].forEach(btn => {
btn.classList.remove('active');
});
button.classList.add('active');
}
// 点击上传区域触发文件选择
dropArea.addEventListener('click', () => {
fileInput.click();
});
// 处理文件选择
fileInput.addEventListener('change', handleFileSelect);
// 拖放事件处理
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
dropArea.addEventListener(eventName, preventDefaults, false);
});
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
['dragenter', 'dragover'].forEach(eventName => {
dropArea.addEventListener(eventName, () => {
dropArea.classList.add('active');
}, false);
});
['dragleave', 'drop'].forEach(eventName => {
dropArea.addEventListener(eventName, () => {
dropArea.classList.remove('active');
}, false);
});
dropArea.addEventListener('drop', (e) => {
const dt = e.dataTransfer;
const files = dt.files;
if (files.length) {
fileInput.files = files;
handleFileSelect(e);
}
}, false);
// 处理文件选择
function handleFileSelect(e) {
resetUI();
const files = e.target.files || e.dataTransfer.files;
if (!files.length) return;
const file = files[0];
// 检查文件类型
if (!file.type.match('audio/mp3') && !file.name.endsWith('.mp3')) {
showError('请选择MP3文件');
return;
}
selectedFile = file;
fileName.textContent = `已选择: ${file.name}`;
parseBtn.disabled = false;
// 创建音频元素
createAudioElement(file);
}
// 创建音频元素
function createAudioElement(file) {
// 如果已存在音频元素,先清除
if (audioElement) {
audioElement.pause();
audioElement.src = '';
audioElement = null;
}
// 停止之前的动画
if (animationId) {
cancelAnimationFrame(animationId);
animationId = null;
}
// 创建新的音频元素
audioElement = new Audio();
audioElement.src = URL.createObjectURL(file);
// 音频加载完成后显示播放按钮
audioElement.addEventListener('loadedmetadata', () => {
playBtn.classList.remove('hidden');
durationDisplay.textContent = formatTime(audioElement.duration);
});
// 音频播放时更新进度条
audioElement.addEventListener('timeupdate', updateProgress);
// 音频播放结束时重置
audioElement.addEventListener('ended', () => {
isPlaying = false;
playBtn.textContent = '播放音乐';
progressBar.style.width = '0%';
currentTimeDisplay.textContent = '0:00';
// 停止可视化
if (animationId) {
cancelAnimationFrame(animationId);
animationId = null;
}
});
// 音频错误处理
audioElement.addEventListener('error', () => {
showError('音频文件加载失败');
});
}
// 初始化音频上下文和分析器
function initAudioContext() {
// 如果已存在音频上下文,先清除
if (audioContext) {
audioContext.close().catch(e => console.error(e));
audioContext = null;
}
try {
// 创建新的音频上下文
audioContext = new (window.AudioContext || window.webkitAudioContext)();
// 创建分析器节点
analyser = audioContext.createAnalyser();
analyser.fftSize = 2048;
analyser.smoothingTimeConstant = 0.8;
// 连接音频源到分析器
audioSource = audioContext.createMediaElementSource(audioElement);
audioSource.connect(analyser);
analyser.connect(audioContext.destination);
audioContextInitialized = true;
console.log('音频上下文初始化成功');
} catch (error) {
console.error('初始化音频上下文失败:', error);
showError('初始化音频上下文失败: ' + error.message);
}
}
// 播放按钮点击事件
playBtn.addEventListener('click', togglePlay);
// 切换播放/暂停
function togglePlay() {
if (!audioElement) return;
// 确保音频上下文已初始化
if (!audioContextInitialized) {
initAudioContext();
}
if (isPlaying) {
audioElement.pause();
playBtn.textContent = '播放音乐';
// 停止可视化
if (animationId) {
cancelAnimationFrame(animationId);
animationId = null;
}
} else {
// 如果音频上下文被挂起,恢复它
if (audioContext && audioContext.state === 'suspended') {
audioContext.resume();
}
audioElement.play().then(() => {
playBtn.textContent = '暂停';
playerContainer.classList.remove('hidden');
// 开始可视化
visualize();
}).catch(error => {
console.error('播放失败:', error);
showError('播放失败: ' + error.message);
});
}
isPlaying = !isPlaying;
}
// 更新进度条
function updateProgress() {
if (!audioElement) return;
const currentTime = audioElement.currentTime;
const duration = audioElement.duration;
const progressPercent = (currentTime / duration) * 100;
progressBar.style.width = `${progressPercent}%`;
currentTimeDisplay.textContent = formatTime(currentTime);
}
// 点击进度条跳转
progressContainer.addEventListener('click', setProgress);
// 设置播放进度
function setProgress(e) {
if (!audioElement) return;
const width = this.clientWidth;
const clickX = e.offsetX;
const duration = audioElement.duration;
audioElement.currentTime = (clickX / width) * duration;
}
// 音量控制
volumeSlider.addEventListener('input', () => {
if (!audioElement) return;
audioElement.volume = volumeSlider.value;
});
// 可视化音频
function visualize() {
if (!analyser || !isPlaying) return;
// 获取频域数据
const bufferLength = analyser.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);
// 清除Canvas
canvasCtx.clearRect(0, 0, canvas.width, canvas.height);
// 根据可视化类型绘制
switch (visualizerType) {
case 'bars':
drawBars(bufferLength, dataArray);
break;
case 'wave':
drawWave(bufferLength, dataArray);
break;
case 'circle':
drawCircle(bufferLength, dataArray);
break;
}
// 循环动画
animationId = requestAnimationFrame(visualize);
}
// 绘制柱状图
function drawBars(bufferLength, dataArray) {
analyser.getByteFrequencyData(dataArray);
const barWidth = (canvas.width / bufferLength) * 2.5;
let barHeight;
let x = 0;
for (let i = 0; i < bufferLength; i++) {
barHeight = dataArray[i] / 2;
// 根据频率创建渐变色
const hue = i / bufferLength * 360;
canvasCtx.fillStyle = `hsl(${hue}, 100%, 50%)`;
canvasCtx.fillRect(x, canvas.height - barHeight, barWidth, barHeight);
x += barWidth + 1;
if (x > canvas.width) break;
}
}
// 绘制波形图
function drawWave(bufferLength, dataArray) {
analyser.getByteTimeDomainData(dataArray);
canvasCtx.lineWidth = 2;
canvasCtx.strokeStyle = 'rgb(0, 255, 0)';
canvasCtx.beginPath();
const sliceWidth = canvas.width / bufferLength;
let x = 0;
for (let i = 0; i < bufferLength; i++) {
const v = dataArray[i] / 128.0;
const y = v * canvas.height / 2;
if (i === 0) {
canvasCtx.moveTo(x, y);
} else {
canvasCtx.lineTo(x, y);
}
x += sliceWidth;
}
canvasCtx.lineTo(canvas.width, canvas.height / 2);
canvasCtx.stroke();
}
// 绘制环形图
function drawCircle(bufferLength, dataArray) {
analyser.getByteFrequencyData(dataArray);
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
const radius = Math.min(centerX, centerY) - 10;
canvasCtx.beginPath();
canvasCtx.arc(centerX, centerY, radius / 4, 0, 2 * Math.PI);
canvasCtx.fillStyle = 'rgba(0, 0, 0, 0.3)';
canvasCtx.fill();
for (let i = 0; i < bufferLength; i++) {
const barHeight = dataArray[i] / 256 * radius;
const angle = i * 2 * Math.PI / bufferLength;
const x = centerX + Math.cos(angle) * (radius - barHeight / 2);
const y = centerY + Math.sin(angle) * (radius - barHeight / 2);
const x2 = centerX + Math.cos(angle) * (radius + barHeight / 2);
const y2 = centerY + Math.sin(angle) * (radius + barHeight / 2);
const gradient = canvasCtx.createLinearGradient(x, y, x2, y2);
gradient.addColorStop(0, `hsl(${i / bufferLength * 360}, 100%, 50%)`);
gradient.addColorStop(1, `hsl(${i / bufferLength * 360}, 100%, 80%)`);
canvasCtx.beginPath();
canvasCtx.moveTo(x, y);
canvasCtx.lineTo(x2, y2);
canvasCtx.lineWidth = 2;
canvasCtx.strokeStyle = gradient;
canvasCtx.stroke();
}
}
// 解析按钮点击事件
parseBtn.addEventListener('click', () => {
if (!selectedFile) return;
metadataContainer.classList.add('hidden');
errorMessage.classList.add('hidden');
// 使用jsmediatags解析MP3元数据
jsmediatags.read(selectedFile, {
onSuccess: function(tag) {
displayMetadata(tag, selectedFile);
},
onError: function(error) {
console.error('Error reading tags:', error.type, error.info);
showError('无法读取文件元数据: ' + error.info);
// 尝试使用Web Audio API获取基本信息
getAudioDuration(selectedFile);
}
});
});
// 显示元数据
function displayMetadata(tag, file) {
metadataContent.innerHTML = '';
// 添加文件基本信息
addMetadataItem('文件名', file.name);
addMetadataItem('文件大小', formatFileSize(file.size));
addMetadataItem('文件类型', file.type || 'audio/mp3');
// 获取音频时长
getAudioDuration(file);
// 添加ID3标签信息
const tags = tag.tags;
if (tags.title) addMetadataItem('标题', tags.title);
if (tags.artist) addMetadataItem('艺术家', tags.artist);
if (tags.album) addMetadataItem('专辑', tags.album);
if (tags.year) addMetadataItem('年份', tags.year);
if (tags.genre) addMetadataItem('流派', tags.genre);
if (tags.track) addMetadataItem('音轨', tags.track);
// 显示专辑封面
if (tags.picture) {
const picture = tags.picture;
const base64String = arrayBufferToBase64(picture.data);
const imgFormat = picture.format || 'image/jpeg';
const imgSrc = `data:${imgFormat};base64,${base64String}`;
const imgContainer = document.createElement('div');
imgContainer.className = 'metadata-item';
const label = document.createElement('div');
label.className = 'metadata-label';
label.textContent = '专辑封面';
const value = document.createElement('div');
value.className = 'metadata-value';
const img = document.createElement('img');
img.src = imgSrc;
img.style.maxWidth = '200px';
img.style.maxHeight = '200px';
value.appendChild(img);
imgContainer.appendChild(label);
imgContainer.appendChild(value);
metadataContent.appendChild(imgContainer);
}
// 显示其他可用标签
for (const key in tags) {
if (['title', 'artist', 'album', 'year', 'genre', 'track', 'picture'].includes(key)) continue;
if (typeof tags[key] === 'object') {
// 跳过复杂对象
continue;
}
addMetadataItem(key, tags[key]);
}
metadataContainer.classList.remove('hidden');
}
// 获取音频时长
function getAudioDuration(file) {
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const reader = new FileReader();
reader.onload = function(e) {
audioContext.decodeAudioData(e.target.result, function(buffer) {
const duration = buffer.duration;
addMetadataItem('时长', formatTime(duration));
// 添加其他音频信息
addMetadataItem('采样率', buffer.sampleRate + ' Hz');
addMetadataItem('声道数', buffer.numberOfChannels);
metadataContainer.classList.remove('hidden');
}, function(error) {
console.error('Error decoding audio data', error);
showError('无法解码音频数据');
});
};
reader.onerror = function() {
showError('读取文件时出错');
};
reader.readAsArrayBuffer(file);
}
// 添加元数据项
function addMetadataItem(label, value) {
if (value === undefined || value === null || value === '') return;
const item = document.createElement('div');
item.className = 'metadata-item';
const labelEl = document.createElement('div');
labelEl.className = 'metadata-label';
labelEl.textContent = label;
const valueEl = document.createElement('div');
valueEl.className = 'metadata-value';
valueEl.textContent = value;
item.appendChild(labelEl);
item.appendChild(valueEl);
metadataContent.appendChild(item);
}
// 显示错误信息
function showError(message) {
errorMessage.textContent = message;
errorMessage.classList.remove('hidden');
}
// 重置UI
function resetUI() {
fileName.textContent = '';
parseBtn.disabled = true;
playBtn.classList.add('hidden');
playerContainer.classList.add('hidden');
metadataContainer.classList.add('hidden');
errorMessage.classList.add('hidden');
metadataContent.innerHTML = '';
// 停止可视化
if (animationId) {
cancelAnimationFrame(animationId);
animationId = null;
}
// 清除Canvas
if (canvasCtx) {
canvasCtx.clearRect(0, 0, canvas.width, canvas.height);
}
// 重置播放状态
if (audioElement) {
audioElement.pause();
audioElement.src = '';
audioElement = null;
}
// 关闭音频上下文
if (audioContext) {
audioContext.close().catch(e => console.error(e));
audioContext = null;
}
audioContextInitialized = false;
isPlaying = false;
playBtn.textContent = '播放音乐';
}
// 格式化文件大小
function formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
// 格式化时间
function formatTime(seconds) {
const minutes = Math.floor(seconds / 60);
const remainingSeconds = Math.floor(seconds % 60);
return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
}
// ArrayBuffer转Base64
function arrayBufferToBase64(buffer) {
let binary = '';
const bytes = new Uint8Array(buffer);
const len = bytes.byteLength;
for (let i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
return window.btoa(binary);
}
});
script>
body>
html>