文章说明
这是一个基于 JavaScript 和 Web Audio API 实现的音频可视化小工具。通过上传音频文件,工具可以实时展示音频的可视化效果,包括条形频谱、圆形频谱等多种模式。代码由 GPT 生成,适合用于学习、演示或嵌入到网页中。
功能特点
-
音频可视化:
- 支持条形频谱(Bar Mode)、圆形频谱(Circle Mode)和扇形频谱(Arc Mode)三种可视化模式。
- 实时动态展示音频的频率数据。
-
音频播放控制:
- 支持播放、暂停功能。
- 上传音频文件后自动播放。
-
元数据展示:
- 自动提取音频文件的元数据(如歌曲名称、艺术家、封面图片)。
- 如果元数据不存在,则显示文件名作为歌曲名称。
-
交互设计:
- 提供上传按钮、模式切换按钮和暂停按钮,操作简单直观。
- 界面美观,支持浅蓝色主题和毛玻璃效果。
使用说明
-
上传音频文件:
- 点击 Upload Audio File 按钮,选择本地音频文件(支持 MP3、WAV 等格式)。
- 上传后,音频会自动播放,并显示可视化效果。
-
切换可视化模式:
- 点击 Bar Mode、Circle Mode 或 Arc Mode 按钮,切换不同的可视化效果。
-
播放/暂停音频:
- 点击 Pause 按钮可以暂停音频,再次点击可以继续播放。
-
查看元数据:
- 上传音频后,工具会自动显示歌曲名称、艺术家和封面图片(如果音频文件包含元数据)。
技术细节
-
核心技术:
- 使用 Web Audio API 分析音频数据。
- 通过 Canvas 绘制实时可视化效果。
- 使用 jsmediatags 库提取音频文件的元数据(如 ID3 标签)。
-
可视化模式:
- 条形频谱:将音频频率数据绘制为条形图。
- 圆形频谱:将音频频率数据围绕中心点绘制为圆形。
- 扇形频谱:将音频频率数据围绕中心点绘制为扇形。
-
界面设计:
- 使用 CSS 实现毛玻璃效果和浅蓝色主题。
- 动态切换按钮状态,提升用户体验。
核心代码
一个HTML页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Audio Visualization</title>
<style>
body {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background: linear-gradient(135deg, #1e3c72, #2a5298);
color: #fff;
font-family: 'Arial', sans-serif;
overflow: hidden;
}
.container {
display: flex;
flex-direction: column;
align-items: center;
background: rgba(255, 255, 255, 0.1);
border-radius: 20px;
padding: 20px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
backdrop-filter: blur(10px);
width: 90%;
max-width: 800px;
}
canvas {
background: rgba(255, 255, 255, 0.1);
border-radius: 15px;
margin-top: 20px;
width: 100%;
height: 300px;
}
#upload-label {
background: rgba(255, 255, 255, 0.2);
padding: 10px 20px;
border-radius: 25px;
cursor: pointer;
font-size: 16px;
color: #fff;
transition: background 0.3s ease;
margin-bottom: 20px;
}
#upload-label:hover {
background: rgba(255, 255, 255, 0.3);
}
#audioFile {
display: none;
}
.music-info {
display: flex;
align-items: center;
width: 100%;
margin-bottom: 20px;
}
.music-cover {
width: 100px;
height: 100px;
border-radius: 10px;
margin-right: 20px;
background: rgba(255, 255, 255, 0.1);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
object-fit: cover;
opacity: 0;
/* Initially hidden */
transition: opacity 0.5s ease;
}
.music-cover.visible {
opacity: 1;
/* Show when cover is loaded */
}
.music-details {
display: flex;
flex-direction: column;
justify-content: center;
}
.music-title {
font-size: 20px;
font-weight: bold;
margin: 0;
}
.music-artist {
font-size: 16px;
color: rgba(255, 255, 255, 0.8);
margin: 5px 0 0 0;
}
.mode-switcher {
display: flex;
gap: 10px;
margin-top: 20px;
}
.mode-switcher button {
background: rgba(255, 255, 255, 0.2);
border: none;
padding: 10px 20px;
border-radius: 25px;
color: #fff;
font-size: 14px;
cursor: pointer;
transition: background 0.3s ease;
}
.mode-switcher button:hover {
background: rgba(255, 255, 255, 0.3);
}
.mode-switcher button.active {
background: #00b4db;
}
#pause-button {
background: rgba(255, 255, 255, 0.2);
border: none;
padding: 10px 20px;
border-radius: 25px;
color: #fff;
font-size: 14px;
cursor: pointer;
transition: background 0.3s ease;
margin-top: 20px;
}
#pause-button:hover {
background: rgba(255, 255, 255, 0.3);
}
</style>
</head>
<body>
<div class="container">
<label for="audioFile" id="upload-label">Upload Audio File</label>
<input type="file" id="audioFile" accept="audio/*"/>
<div class="music-info">
<div class="music-cover" id="music-cover"></div>
<div class="music-details">
<p class="music-title" id="music-title">Song Title</p>
<p class="music-artist" id="music-artist">Artist</p>
</div>
</div>
<canvas id="visualizer" width="800" height="300"></canvas>
<div class="mode-switcher">
<button id="bar-mode" class="active">Bar Mode</button>
<button id="circle-mode">Circle Mode</button>
<button id="arc-mode">Arc Mode</button>
</div>
<button id="pause-button">Pause</button>
</div>
<!-- Include jsmediatags library -->
<script src="https://unpkg.com/[email protected]/dist/jsmediatags.min.js"></script>
<script>
const canvas = document.getElementById('visualizer');
const ctx = canvas.getContext('2d');
const audioFileInput = document.getElementById('audioFile');
const musicCover = document.getElementById('music-cover');
const musicTitle = document.getElementById('music-title');
const musicArtist = document.getElementById('music-artist');
const barModeButton = document.getElementById('bar-mode');
const circleModeButton = document.getElementById('circle-mode');
const arcModeButton = document.getElementById('arc-mode');
const pauseButton = document.getElementById('pause-button');
let audioContext;
let analyser;
let source;
let dataArray;
let bufferLength;
let currentMode = 'bar'; // Default mode
let audio; // Audio element
let isPlaying = false; // Track playback state
// Event listeners for mode switching
barModeButton.addEventListener('click', () => switchMode('bar'));
circleModeButton.addEventListener('click', () => switchMode('circle'));
arcModeButton.addEventListener('click', () => switchMode('arc'));
// Event listener for pause button
pauseButton.addEventListener('click', togglePlayback);
function switchMode(mode) {
currentMode = mode;
barModeButton.classList.remove('active');
circleModeButton.classList.remove('active');
arcModeButton.classList.remove('active');
if (mode === 'bar') barModeButton.classList.add('active');
if (mode === 'circle') circleModeButton.classList.add('active');
if (mode === 'arc') arcModeButton.classList.add('active');
}
function togglePlayback() {
if (isPlaying) {
audio.pause();
pauseButton.textContent = 'Play';
} else {
audio.play();
pauseButton.textContent = 'Pause';
}
isPlaying = !isPlaying;
}
audioFileInput.addEventListener('change', function (event) {
const file = event.target.files[0];
if (file) {
const fileURL = URL.createObjectURL(file);
audio = new Audio(fileURL);
setupAudioContext(audio);
audio.play();
isPlaying = true;
pauseButton.textContent = 'Pause';
// Extract metadata using jsmediatags
jsmediatags.read(file, {
onSuccess: function (tag) {
const tags = tag.tags;
// Update song title
if (tags.title) {
musicTitle.textContent = tags.title;
} else {
musicTitle.textContent = file.name.replace(/\.[^/.]+$/, ""); // Fallback to file name
}
// Update artist
if (tags.artist) {
musicArtist.textContent = tags.artist;
} else {
musicArtist.textContent = "Unknown Artist";
}
// Update cover image
if (tags.picture) {
const picture = tags.picture;
const base64String = Array.from(picture.data)
.map(byte => String.fromCharCode(byte))
.join('');
const base64 = `data:${picture.format};base64,${window.btoa(base64String)}`;
musicCover.style.backgroundImage = `url(${base64})`;
musicCover.classList.add('visible'); // Show cover
} else {
musicCover.style.backgroundImage = ''; // No cover
musicCover.classList.remove('visible'); // Hide cover
}
},
onError: function (error) {
console.error("Error reading metadata:", error);
// Fallback to file name if metadata cannot be read
musicTitle.textContent = file.name.replace(/\.[^/.]+$/, "");
musicArtist.textContent = "Unknown Artist";
musicCover.style.backgroundImage = ''; // No cover
musicCover.classList.remove('visible'); // Hide cover
}
});
}
});
function setupAudioContext(audio) {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
analyser = audioContext.createAnalyser();
source = audioContext.createMediaElementSource(audio);
source.connect(analyser);
analyser.connect(audioContext.destination);
analyser.fftSize = 512;
bufferLength = analyser.frequencyBinCount;
dataArray = new Uint8Array(bufferLength);
draw();
}
function draw() {
requestAnimationFrame(draw);
analyser.getByteFrequencyData(dataArray);
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (currentMode === 'bar') {
drawBars();
} else if (currentMode === 'circle') {
drawCircle();
} else if (currentMode === 'arc') {
drawArc();
}
}
function drawBars() {
const barWidth = (canvas.width / bufferLength) * 1.5; // Thinner bars
let barHeight;
let x = 0;
for (let i = 0; i < bufferLength; i++) {
barHeight = dataArray[i] / 2;
// Create a gradient for each bar
const gradient = ctx.createLinearGradient(x, canvas.height, x + barWidth, canvas.height - barHeight);
gradient.addColorStop(0, '#00b4db');
gradient.addColorStop(1, '#0083b0');
ctx.fillStyle = gradient;
ctx.fillRect(x, canvas.height - barHeight, barWidth, barHeight);
// Add glow effect
ctx.shadowBlur = 10;
ctx.shadowColor = 'rgba(0, 180, 219, 0.7)';
x += barWidth + 1;
}
}
function drawCircle() {
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
const radius = 100;
const barWidth = 2; // Thinner bars
const angleStep = (Math.PI * 2) / bufferLength;
for (let i = 0; i < bufferLength; i++) {
const barHeight = dataArray[i] / 2;
const angle = i * angleStep;
const x = centerX + Math.cos(angle) * radius;
const y = centerY + Math.sin(angle) * radius;
const endX = centerX + Math.cos(angle) * (radius + barHeight);
const endY = centerY + Math.sin(angle) * (radius + barHeight);
// Create a gradient for each bar
const gradient = ctx.createLinearGradient(x, y, endX, endY);
gradient.addColorStop(0, '#00b4db');
gradient.addColorStop(1, '#0083b0');
ctx.strokeStyle = gradient;
ctx.lineWidth = barWidth;
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(endX, endY);
ctx.stroke();
// Add glow effect
ctx.shadowBlur = 10;
ctx.shadowColor = 'rgba(0, 180, 219, 0.7)';
}
}
function drawArc() {
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
const radius = 100;
const barWidth = 2; // Thinner bars
const angleStep = (Math.PI * 2) / bufferLength;
for (let i = 0; i < bufferLength; i++) {
const barHeight = dataArray[i] / 2;
const startAngle = i * angleStep;
const endAngle = startAngle + angleStep;
// Create a gradient for each bar
const gradient = ctx.createRadialGradient(
centerX, centerY, radius,
centerX, centerY, radius + barHeight
);
gradient.addColorStop(0, '#00b4db');
gradient.addColorStop(1, '#0083b0');
ctx.strokeStyle = gradient;
ctx.lineWidth = barWidth;
ctx.beginPath();
ctx.arc(centerX, centerY, radius + barHeight, startAngle, endAngle);
ctx.stroke();
// Add glow effect
ctx.shadowBlur = 10;
ctx.shadowColor = 'rgba(0, 180, 219, 0.7)';
}
}
</script>
</body>
</html>
效果展示
条形频谱模式
圆形频谱模式