在这个数字时代,音乐不仅是听觉的享受,更可以成为视觉的盛宴!本文用 HTML + JavaScript 实现了一个音频可视化播放器,它不仅能播放本地音乐、控制进度和音量,还能通过 Canvas 绘制炫酷的音频频谱图,让你"听见色彩,看见旋律"。
效果演示


核心功能
本项目主要包含以下核心功能:
- 音频播放控制:支持播放、暂停、上一首、下一首等基本操作。
- 进度控制:显示当前播放时间和总时长,并支持点击进度条跳转。
- 音量调节:提供滑动条调节播放音量。
- 播放列表管理:支持动态添加本地音乐文件,显示播放列表并高亮当前播放曲目。
- 音频可视化:通过Canvas实时绘制音频频谱图,增强用户体验。
页面结构
音频可视化容器
使用 HTML5 的 canvas
元素来绘制动态的音频频谱图。
html
<div class="visualizer">
<canvas id="visualizer"></canvas>
</div>
操作控制区域
整个音乐播放器的主要控制区域,包含播放进度条与时间显示、播放控制按钮、音量调节滑块、文件上传控件。
html
<div class="controls">
<div class="progress-container" id="progress-container">
<div class="progress-bar" id="progress-bar"></div>
</div>
<div class="time-display">
<span id="current-time">0:00</span>
<span id="total-time">0:00</span>
</div>
<div class="control-row">
<div class="left"></div>
<div class="buttons">
<button id="prev-btn">上一首</button>
<button id="play-btn">播放</button>
<button id="next-btn">下一首</button>
</div>
<div class="volume-control">
<span>音量</span>
<input type="range" min="0" max="1" step="0.01" value="0.7" class="volume-slider" id="volume-control">
</div>
</div>
<div class="file-upload">
<input type="file" id="file-input" accept="audio/*" multiple>
<label for="file-input">添加音乐文件</label>
</div>
</div>
播放列表区域
该区域用于展示用户上传的音频文件列表,并提供一个初始为空时的提示信息。
html
<div class="playlist">
<h2>播放列表</h2>
<div id="playlist-items">
<div class="empty-playlist">暂无音乐,请添加音乐文件</div>
</div>
</div>
核心功能实现
添加本地音乐文件
使用 URL.createObjectURL 创建本地文件链接供 audio
播放。
js
function addMusicFiles(files) {
for (let i = 0; i < files.length; i++) {
const file = files[i];
const url = URL.createObjectURL(file);
playlist.push({
name: file.name.replace(/\.[^/.]+$/, ""), // 移除扩展名
url: url
});
}
// 如果是第一次添加音乐,自动加载第一首
if (playlist.length === files.length) {
currentTrack = 0;
loadTrack();
}
renderPlaylist();
}
加载当前曲目
js
function loadTrack() {
if (playlist.length === 0) return;
const track = playlist[currentTrack];
audio.src = track.url;
audio.load();
updatePlaylistHighlight();
if (isPlaying) {
audio.play().catch(e => console.log('播放错误:', e));
}
}
播放/暂停控制
判断播放列表是否为空,控制播放状态切换,并更新按钮文本,如果播放失败尝试下一首。
js
function togglePlay() {
if (playlist.length === 0) {
alert('播放列表为空,请先添加音乐');
return;
}
if (isPlaying) {
audio.pause();
playBtn.textContent = '播放';
} else {
initAudioContext(); // 首次播放时才初始化音频上下文
audio.play().catch(e => {
console.log('播放错误:', e);
nextTrack(); // 播放失败自动下一首
});
playBtn.textContent = '暂停';
}
isPlaying = !isPlaying;
}
音频上下文初始化
初始化 AudioContext,创建音频分析节点,将音频元素通过 createMediaElementSource 接入分析器,dataArray 用于后续可视化绘制。
js
function initAudioContext() {
if (!audioContext) {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
analyser = audioContext.createAnalyser();
analyser.fftSize = 256;
source = audioContext.createMediaElementSource(audio);
source.connect(analyser);
analyser.connect(audioContext.destination);
dataArray = new Uint8Array(analyser.frequencyBinCount);
}
}
音频可视化
使用 requestAnimationFrame 实现动画帧循环,使用 getByteFrequencyData() 获取实时音频数据,使用 HSL 颜色绘制彩色柱状图,形成"跳舞"的视觉效果。
js
function visualize() {
if (!isPlaying || playlist.length === 0) {
canvasCtx.clearRect(0, 0, canvas.width, canvas.height);
return;
}
requestAnimationFrame(visualize);
analyser.getByteFrequencyData(dataArray);
canvasCtx.clearRect(0, 0, canvas.width, canvas.height);
const barWidth = (canvas.width / analyser.frequencyBinCount) * 2.5;
let x = 0;
for (let i = 0; i < analyser.frequencyBinCount; i++) {
const barHeight = dataArray[i] / 2;
const hue = i * 360 / analyser.frequencyBinCount;
canvasCtx.fillStyle = `hsl(${hue}, 100%, 50%)`;
canvasCtx.fillRect(
x,
canvas.height - barHeight,
barWidth,
barHeight
);
x += barWidth + 1;
}
}
扩展建议
- 支持播放模式:单曲循环、随机播放、顺序播放。
- 增加歌词同步功能:解析 LRC 歌词文件,并根据当前播放时间匹配对应歌词行,在页面中展示滚动歌词。
- 支持拖拽排序播放列表,使用户可以自定义播放顺序。
- 添加缓存机制:缓存播放历史、播放列表等信息,避免刷新后丢失
- 支持在线音乐资源加载
完整代码
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>可视化音乐播放器</title>
<style>
body {
font-family: 'Arial', sans-serif;
margin: 0;
padding: 20px;
background-color: #f5f5f5;
color: #333;
}
.player-container {
max-width: 800px;
margin: 0 auto;
background-color: white;
border-radius: 10px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
padding: 20px;
}
h1 {
text-align: center;
color: #2c3e50;
}
.visualizer {
width: 100%;
height: 200px;
background-color: #2c3e50;
margin-bottom: 20px;
border-radius: 5px;
position: relative;
overflow: hidden;
}
canvas {
width: 100%;
height: 100%;
}
.controls {
display: flex;
flex-direction: column;
gap: 15px;
}
.progress-container {
width: 100%;
height: 10px;
background-color: #ecf0f1;
border-radius: 5px;
cursor: pointer;
}
.progress-bar {
height: 100%;
background-color: #3498db;
border-radius: 5px;
width: 0%;
}
.time-display {
display: flex;
justify-content: space-between;
font-size: 14px;
color: #7f8c8d;
}
.control-row {
display: flex;
justify-content: center;
align-items: center;
gap: 20px;
}
.control-row>div {
flex: 1;
}
.buttons {
display: flex;
gap: 10px;
}
button {
background-color: #3498db;
color: white;
border: none;
border-radius: 5px;
padding: 8px 15px;
font-size: 14px;
cursor: pointer;
transition: all 0.3s;
}
button:hover {
background-color: #2980b9;
transform: scale(1.05);
}
button:active {
transform: scale(0.95);
}
.playlist {
margin-top: 30px;
}
.playlist h2 {
border-bottom: 1px solid #ecf0f1;
padding-bottom: 10px;
margin-bottom: 15px;
}
.playlist-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.2s;
}
.playlist-item:hover {
background-color: #ecf0f1;
}
.playlist-item.active {
background-color: #3498db;
color: white;
}
.playlist-item-actions {
display: flex;
gap: 10px;
}
.delete-btn {
background-color: #e74c3c;
padding: 2px 8px;
font-size: 12px;
border-radius: 3px;
}
.delete-btn:hover {
background-color: #c0392b;
}
.volume-control {
display: flex;
align-items: center;
gap: 10px;
}
.volume-slider {
width: 100px;
}
.file-upload {
margin-top: 20px;
text-align: center;
}
.file-upload input {
display: none;
}
.file-upload label {
background-color: #2ecc71;
color: white;
padding: 10px 15px;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s;
}
.file-upload label:hover {
background-color: #27ae60;
}
.empty-playlist {
text-align: center;
color: #7f8c8d;
padding: 20px;
}
</style>
</head>
<body>
<div class="player-container">
<h1>可视化音乐播放器</h1>
<div class="visualizer">
<canvas id="visualizer"></canvas>
</div>
<div class="controls">
<div class="progress-container" id="progress-container">
<div class="progress-bar" id="progress-bar"></div>
</div>
<div class="time-display">
<span id="current-time">0:00</span>
<span id="total-time">0:00</span>
</div>
<div class="control-row">
<div class="left"></div>
<div class="buttons">
<button id="prev-btn">上一首</button>
<button id="play-btn">播放</button>
<button id="next-btn">下一首</button>
</div>
<div class="volume-control">
<span>音量</span>
<input type="range" min="0" max="1" step="0.01" value="0.7" class="volume-slider" id="volume-control">
</div>
</div>
<div class="file-upload">
<input type="file" id="file-input" accept="audio/*" multiple>
<label for="file-input">添加音乐文件</label>
</div>
</div>
<div class="playlist">
<h2>播放列表</h2>
<div id="playlist-items">
<div class="empty-playlist">暂无音乐,请添加音乐文件</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// 音频上下文和分析器
let audioContext;
let analyser;
let dataArray;
let source;
// 播放器状态
let currentTrack = 0;
let isPlaying = false;
let audio = new Audio();
// 播放列表 - 初始为空
let playlist = [];
// DOM 元素
const playBtn = document.getElementById('play-btn');
const prevBtn = document.getElementById('prev-btn');
const nextBtn = document.getElementById('next-btn');
const progressContainer = document.getElementById('progress-container');
const progressBar = document.getElementById('progress-bar');
const currentTimeDisplay = document.getElementById('current-time');
const totalTimeDisplay = document.getElementById('total-time');
const playlistItems = document.getElementById('playlist-items');
const volumeControl = document.getElementById('volume-control');
const fileInput = document.getElementById('file-input');
const canvas = document.getElementById('visualizer');
const canvasCtx = canvas.getContext('2d');
// 初始化音频上下文
function initAudioContext() {
if (!audioContext) {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
analyser = audioContext.createAnalyser();
analyser.fftSize = 256;
source = audioContext.createMediaElementSource(audio);
source.connect(analyser);
analyser.connect(audioContext.destination);
dataArray = new Uint8Array(analyser.frequencyBinCount);
}
}
// 加载当前曲目
function loadTrack() {
if (playlist.length === 0) {
audio.src = '';
return;
}
// 确保当前曲目索引有效
if (currentTrack >= playlist.length) {
currentTrack = playlist.length - 1;
}
if (currentTrack < 0) {
currentTrack = 0;
}
const track = playlist[currentTrack];
audio.src = track.url;
audio.load();
// 更新播放列表高亮
updatePlaylistHighlight();
// 如果正在播放,继续播放
if (isPlaying) {
audio.play().catch(e => console.log('播放错误:', e));
}
}
// 播放/暂停
function togglePlay() {
if (playlist.length === 0) {
alert('播放列表为空,请先添加音乐');
return;
}
if (isPlaying) {
audio.pause();
playBtn.textContent = '播放';
} else {
initAudioContext();
audio.play().catch(e => {
console.log('播放错误:', e);
// 如果播放失败,尝试下一首
nextTrack();
});
playBtn.textContent = '暂停';
}
isPlaying = !isPlaying;
}
// 下一曲
function nextTrack() {
if (playlist.length === 0) return;
currentTrack = (currentTrack + 1) % playlist.length;
loadTrack();
if (isPlaying) {
audio.play().catch(e => console.log('播放错误:', e));
}
}
// 上一曲
function prevTrack() {
if (playlist.length === 0) return;
currentTrack = (currentTrack - 1 + playlist.length) % playlist.length;
loadTrack();
if (isPlaying) {
audio.play().catch(e => console.log('播放错误:', e));
}
}
// 删除曲目
function deleteTrack(index) {
// 如果删除的是当前正在播放的曲目
if (index === currentTrack && isPlaying) {
audio.pause();
isPlaying = false;
playBtn.textContent = '播放';
}
// 调整当前曲目索引
if (index < currentTrack || (currentTrack === playlist.length - 1 && currentTrack > 0)) {
currentTrack--;
}
// 从播放列表中移除
playlist.splice(index, 1);
// 重新渲染播放列表
renderPlaylist();
// 如果播放列表不为空,加载当前曲目
if (playlist.length > 0) {
loadTrack();
} else {
audio.src = '';
currentTimeDisplay.textContent = '0:00';
totalTimeDisplay.textContent = '0:00';
progressBar.style.width = '0%';
}
}
// 更新进度条
function updateProgress() {
const { currentTime, duration } = audio;
const progressPercent = (currentTime / duration) * 100;
progressBar.style.width = `${progressPercent}%`;
// 更新时间显示
currentTimeDisplay.textContent = formatTime(currentTime);
totalTimeDisplay.textContent = formatTime(duration);
}
// 设置进度
function setProgress(e) {
if (playlist.length === 0) return;
const width = this.clientWidth;
const clickX = e.offsetX;
const duration = audio.duration;
audio.currentTime = (clickX / width) * duration;
}
// 格式化时间 (秒 -> MM:SS)
function formatTime(seconds) {
if (isNaN(seconds)) return '0:00';
const minutes = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${minutes}:${secs < 10 ? '0' : ''}${secs}`;
}
// 更新播放列表高亮
function updatePlaylistHighlight() {
const items = playlistItems.querySelectorAll('.playlist-item');
items.forEach((item, index) => {
item.classList.toggle('active', index === currentTrack);
});
}
// 渲染播放列表
function renderPlaylist() {
if (playlist.length === 0) {
playlistItems.innerHTML = '<div class="empty-playlist">暂无音乐,请添加音乐文件</div>';
return;
}
playlistItems.innerHTML = '';
playlist.forEach((track, index) => {
console.log(index, currentTrack, isPlaying)
const item = document.createElement('div');
item.className = `playlist-item ${index === currentTrack ? 'active' : ''}`;
item.innerHTML = `<span>${track.name}</span>
<div class="playlist-item-actions">
<button class="delete-btn">删除</button>
</div>`;
// 点击曲目切换播放
item.addEventListener('click', (e) => {
// 防止点击删除按钮时触发
if (e.target.classList.contains('delete-btn')) return;
currentTrack = index;
loadTrack();
if (!isPlaying) {
togglePlay();
}
});
// 删除按钮事件
const deleteBtn = item.querySelector('.delete-btn');
deleteBtn.addEventListener('click', (e) => {
e.stopPropagation(); // 阻止事件冒泡
deleteTrack(index);
});
playlistItems.appendChild(item);
});
}
// 可视化音频
function visualize() {
if (!isPlaying || playlist.length === 0) {
canvasCtx.clearRect(0, 0, canvas.width, canvas.height);
return;
}
requestAnimationFrame(visualize);
analyser.getByteFrequencyData(dataArray);
canvasCtx.clearRect(0, 0, canvas.width, canvas.height);
const barWidth = (canvas.width / analyser.frequencyBinCount) * 2.5;
let x = 0;
for (let i = 0; i < analyser.frequencyBinCount; i++) {
const barHeight = dataArray[i] / 2;
const hue = i * 360 / analyser.frequencyBinCount;
canvasCtx.fillStyle = `hsl(${hue}, 100%, 50%)`;
canvasCtx.fillRect(
x,
canvas.height - barHeight,
barWidth,
barHeight
);
x += barWidth + 1;
}
}
// 添加音乐文件
function addMusicFiles(files) {
for (let i = 0; i < files.length; i++) {
const file = files[i];
const url = URL.createObjectURL(file);
playlist.push({
name: file.name.replace(/\.[^/.]+$/, ""), // 移除扩展名
url: url
});
}
// 如果是第一次添加音乐,自动加载第一首
if (playlist.length === files.length) {
currentTrack = 0;
loadTrack();
}
renderPlaylist();
}
// 事件监听
playBtn.addEventListener('click', togglePlay);
nextBtn.addEventListener('click', nextTrack);
prevBtn.addEventListener('click', prevTrack);
audio.addEventListener('timeupdate', updateProgress);
audio.addEventListener('ended', nextTrack);
audio.addEventListener('loadedmetadata', updateProgress);
progressContainer.addEventListener('click', setProgress);
volumeControl.addEventListener('input', () => {
audio.volume = volumeControl.value;
});
fileInput.addEventListener('change', (e) => {
addMusicFiles(e.target.files);
fileInput.value = ''; // 重置输入,允许重复选择相同文件
});
// 初始化
renderPlaylist();
audio.volume = volumeControl.value;
// 设置canvas尺寸
function resizeCanvas() {
canvas.width = canvas.offsetWidth;
canvas.height = canvas.offsetHeight;
}
window.addEventListener('resize', resizeCanvas);
resizeCanvas();
// 开始可视化
setInterval(visualize, 30);
});
</script>
</body>
</html>