一个纯 html 手搓的音乐播放器,支持频谱显示

源代码如下:

一个纯 html 手搓的音乐播放器,支持频谱显示_第1张图片

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>

你可能感兴趣的:(前端开发相关,html,前端,javascript)