初始提交

This commit is contained in:
陈乾 2026-01-30 14:18:58 +08:00
parent a93f7e4ff5
commit 38b1c613b5
11 changed files with 2824 additions and 0 deletions

24
.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

77
index.html Normal file
View File

@ -0,0 +1,77 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>音乐播放器 - 波形图可视化</title>
</head>
<body>
<div id="app">
<div class="audio-player">
<h1>音乐播放器</h1>
<!-- 文件选择 -->
<div class="file-selector">
<input type="file" id="audioFile" accept="audio/*" />
<label for="audioFile" class="file-label">选择音乐文件</label>
<input type="file" id="audioFolder" webkitdirectory directory multiple accept="audio/*" style="display: none;" />
<label for="audioFolder" class="file-label folder-label">选择音乐文件夹</label>
</div>
<!-- 当前播放信息 -->
<div class="track-info">
<p id="trackName">请选择音乐文件</p>
</div>
<!-- 波形图显示 -->
<div class="waveform-container">
<canvas id="waveform" width="800" height="300"></canvas>
</div>
<!-- 音频元素(隐藏,只用于音频播放) -->
<audio id="audio" style="display: none;"></audio>
<!-- 播放控制和进度条 -->
<div class="playback-progress-container">
<button id="prevBtn" class="control-btn nav-btn" title="上一首 (P)">⏮️</button>
<button id="playPauseBtn" class="control-btn play-pause-btn">▶️</button>
<button id="nextBtn" class="control-btn nav-btn" title="下一首 (N)">⏭️</button>
<div class="progress-container">
<div class="progress-bar" id="progressBar">
<div id="progress" class="progress"></div>
<div class="progress-handle" id="progressHandle"></div>
</div>
<div class="time-display">
<span id="currentTime">0:00</span> / <span id="duration">0:00</span>
</div>
</div>
<button id="stopBtn" class="control-btn stop-btn">⏹️</button>
</div>
<!-- 底部控制栏 -->
<div class="bottom-controls">
<!-- 播放模式控制 -->
<div class="playback-mode-control">
<button id="playbackModeBtn" class="control-btn" title="切换播放模式">🔁</button>
<span id="playbackModeText">列表循环</span>
</div>
<!-- 可视化控制 -->
<div class="visualization-control">
<button id="visualizationBtn" class="control-btn">🎨 切换可视化</button>
<span id="visualizationMode">条形图</span>
</div>
<!-- 音量控制 -->
<div class="volume-control">
<button id="muteBtn" class="control-btn volume-btn">🔊</button>
<input type="range" id="volumeSlider" min="0" max="100" value="50" />
<span id="volumeValue">50%</span>
</div>
</div>
</div>
</div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

1105
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

14
package.json Normal file
View File

@ -0,0 +1,14 @@
{
"name": "audio-player",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"vite": "^7.2.4"
}
}

1
public/vite.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

9
src/counter.js Normal file
View File

@ -0,0 +1,9 @@
export function setupCounter(element) {
let counter = 0
const setCounter = (count) => {
counter = count
element.innerHTML = `count is ${counter}`
}
element.addEventListener('click', () => setCounter(counter + 1))
setCounter(0)
}

1
src/javascript.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="32" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path fill="#F7DF1E" d="M0 0h256v256H0V0Z"></path><path d="m67.312 213.932l19.59-11.856c3.78 6.701 7.218 12.371 15.465 12.371c7.905 0 12.89-3.092 12.89-15.12v-81.798h24.057v82.138c0 24.917-14.606 36.259-35.916 36.259c-19.245 0-30.416-9.967-36.087-21.996m85.07-2.576l19.588-11.341c5.157 8.421 11.859 14.607 23.715 14.607c9.969 0 16.325-4.984 16.325-11.858c0-8.248-6.53-11.17-17.528-15.98l-6.013-2.58c-17.357-7.387-28.87-16.667-28.87-36.257c0-18.044 13.747-31.792 35.228-31.792c15.294 0 26.292 5.328 34.196 19.247l-18.732 12.03c-4.125-7.389-8.591-10.31-15.465-10.31c-7.046 0-11.514 4.468-11.514 10.31c0 7.217 4.468 10.14 14.778 14.608l6.014 2.577c20.45 8.765 31.963 17.7 31.963 37.804c0 21.654-17.012 33.51-39.867 33.51c-22.339 0-36.774-10.654-43.819-24.574"></path></svg>

After

Width:  |  Height:  |  Size: 995 B

773
src/main.js Normal file
View File

@ -0,0 +1,773 @@
import './style.css'
import WaveformVisualizer from './waveformVisualizer.js'
// 音频播放器类
class AudioPlayer {
constructor() {
this.audio = document.getElementById('audio');
this.canvas = document.getElementById('waveform');
this.ctx = this.canvas.getContext('2d');
this.audioContext = null;
this.analyser = null;
this.source = null;
this.dataArray = null;
this.animationId = null;
this.waveformVisualizer = null;
// 播放列表相关
this.playlist = [];
this.currentTrackIndex = 0;
this.isPlayingFolder = false;
this.playbackMode = 'loop'; // 默认列表循环模式:'single', 'loop', 'random'
// Web Audio API警告标志
this.webAudioApiWarningShown = false;
// 自动播放状态跟踪
this.shouldAutoPlay = false;
this.initializeElements();
this.setupEventListeners();
this.setupCanvas();
this.setupKeyboardShortcuts();
this.initializePlayerState();
}
initializeElements() {
// 获取所有DOM元素
this.fileInput = document.getElementById('audioFile');
this.folderInput = document.getElementById('audioFolder');
this.playPauseBtn = document.getElementById('playPauseBtn');
this.prevBtn = document.getElementById('prevBtn');
this.nextBtn = document.getElementById('nextBtn');
this.stopBtn = document.getElementById('stopBtn');
this.progress = document.getElementById('progress');
this.currentTime = document.getElementById('currentTime');
this.duration = document.getElementById('duration');
this.volumeSlider = document.getElementById('volumeSlider');
this.volumeValue = document.getElementById('volumeValue');
this.trackName = document.getElementById('trackName');
this.muteBtn = document.getElementById('muteBtn');
this.visualizationBtn = document.getElementById('visualizationBtn');
this.visualizationMode = document.getElementById('visualizationMode');
this.playbackModeBtn = document.getElementById('playbackModeBtn');
this.playbackModeText = document.getElementById('playbackModeText');
this.progressBar = document.getElementById('progressBar');
this.progressHandle = document.getElementById('progressHandle');
// 初始化音频音量和状态
this.audio.volume = 0.5; // 设置默认音量为50%
this.previousVolume = 0.5; // 用于静音前的音量记忆
this.isMuted = false;
this.isDraggingProgress = false; // 进度条拖动状态
}
setupCanvas() {
// 设置canvas尺寸
const rect = this.canvas.getBoundingClientRect();
this.canvas.width = rect.width * window.devicePixelRatio;
this.canvas.height = rect.height * window.devicePixelRatio;
this.ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
this.canvas.style.width = rect.width + 'px';
this.canvas.style.height = rect.height + 'px';
}
setupEventListeners() {
// 文件选择
this.fileInput.addEventListener('change', (e) => this.handleFileSelect(e));
this.folderInput.addEventListener('change', (e) => this.handleFolderSelect(e));
// 音频结束事件(用于自动播放下一首)
this.audio.addEventListener('ended', () => this.handleTrackEnd());
// 播放控制
this.playPauseBtn.addEventListener('click', () => this.togglePlayPause());
this.prevBtn.addEventListener('click', () => this.previousTrack());
this.nextBtn.addEventListener('click', () => this.nextTrack());
this.stopBtn.addEventListener('click', () => this.stop());
// 播放模式切换
this.playbackModeBtn.addEventListener('click', () => this.togglePlaybackMode());
// 音频事件
this.audio.addEventListener('loadedmetadata', () => this.updateDuration());
this.audio.addEventListener('timeupdate', () => this.updateProgress());
this.audio.addEventListener('ended', () => this.stop());
this.audio.addEventListener('volumechange', () => this.syncVolumeSlider());
// 音量控制 - 双向联动
this.volumeSlider.addEventListener('input', (e) => this.updateVolume(e));
// 静音按钮
this.muteBtn.addEventListener('click', () => this.toggleMute());
// Canvas点击切换可视化模式不再用于跳转进度
this.canvas.addEventListener('click', () => this.switchVisualization());
// 可视化模式切换按钮
this.visualizationBtn.addEventListener('click', () => this.switchVisualization());
// 进度条拖动功能
this.setupProgressDrag();
}
setupProgressDrag() {
// 进度条拖动功能
let isDragging = false;
const handleProgressDrag = (e) => {
if (!this.audio.duration) return;
const rect = this.progressBar.getBoundingClientRect();
const x = e.clientX - rect.left;
const percentage = Math.max(0, Math.min(1, x / rect.width));
const newTime = percentage * this.audio.duration;
this.audio.currentTime = newTime;
this.updateProgressBar(percentage);
};
const startDrag = (e) => {
isDragging = true;
this.isDraggingProgress = true;
document.addEventListener('mousemove', handleProgressDrag);
document.addEventListener('mouseup', stopDrag);
handleProgressDrag(e);
};
const stopDrag = () => {
isDragging = false;
this.isDraggingProgress = false;
document.removeEventListener('mousemove', handleProgressDrag);
document.removeEventListener('mouseup', stopDrag);
};
// 进度条点击
this.progressBar.addEventListener('click', handleProgressDrag);
// 拖动手柄
this.progressHandle.addEventListener('mousedown', startDrag);
// 防止拖动时选中文本
this.progressHandle.addEventListener('selectstart', (e) => e.preventDefault());
}
updateProgressBar(percentage) {
// 更新进度条显示不依赖于timeupdate事件
this.progress.style.width = (percentage * 100) + '%';
// 更新手柄位置
if (this.progressHandle && this.progressBar) {
const handleX = percentage * this.progressBar.offsetWidth - 8; // 8是手柄宽度的一半
this.progressHandle.style.left = Math.max(-8, Math.min(this.progressBar.offsetWidth - 8, handleX)) + 'px';
}
}
handleFileSelect(event) {
const file = event.target.files[0];
if (file) {
// 单个文件播放模式
this.isPlayingFolder = false;
this.playlist = [];
this.currentTrackIndex = 0;
// 清理旧的URL
if (this.audio.src) {
URL.revokeObjectURL(this.audio.src);
}
const url = URL.createObjectURL(file);
this.audio.src = url;
this.trackName.textContent = file.name;
// 确保音量设置保持一致
const currentVolume = this.volumeSlider.value / 100;
this.audio.volume = currentVolume;
// 设置音频上下文(如果尚未初始化)
this.setupAudioContext();
}
}
handleFolderSelect(event) {
const files = Array.from(event.target.files);
if (files.length > 0) {
// 筛选音频文件 - 支持更多格式
const audioExtensions = ['mp3', 'wav', 'ogg', 'm4a', 'flac', 'aac', 'wma', 'opus', 'webm'];
const audioFiles = files.filter(file => {
const extension = file.name.split('.').pop().toLowerCase();
return file.type.startsWith('audio/') || audioExtensions.includes(extension);
});
if (audioFiles.length > 0) {
// 按文件名排序
audioFiles.sort((a, b) => a.name.localeCompare(b.name));
this.playlist = audioFiles;
this.currentTrackIndex = 0;
this.isPlayingFolder = true;
// 确保播放模式显示正确(默认列表循环)
this.playbackMode = 'loop';
this.updatePlaybackModeDisplay();
// 确保音频上下文已初始化
this.setupAudioContext();
// 播放第一首
this.loadTrack(0);
console.log(`已加载文件夹,共${audioFiles.length}首音乐`);
// 显示成功提示
this.showFolderLoadedFeedback(audioFiles.length);
} else {
alert('文件夹中没有找到音频文件');
}
}
}
showFolderLoadedFeedback(count) {
const feedback = document.createElement('div');
feedback.textContent = `已加载 ${count} 首音乐`;
feedback.className = 'visualization-feedback';
feedback.style.background = 'rgba(76, 175, 80, 0.9)';
document.body.appendChild(feedback);
setTimeout(() => {
if (feedback.parentNode) {
feedback.parentNode.removeChild(feedback);
}
}, 2000);
}
loadTrack(index, shouldAutoPlay = true) {
if (index < 0 || index >= this.playlist.length) return;
this.currentTrackIndex = index;
const file = this.playlist[index];
// 停止当前播放和可视化
this.stopVisualization();
// 清理旧的URL
if (this.audio.src) {
URL.revokeObjectURL(this.audio.src);
}
const url = URL.createObjectURL(file);
this.audio.src = url;
this.trackName.textContent = `${index + 1}/${this.playlist.length}: ${file.name}`;
// 确保音量设置保持一致
const currentVolume = this.volumeSlider.value / 100;
this.audio.volume = currentVolume;
// 设置音频上下文(如果尚未初始化)
this.setupAudioContext();
this.updateNavigationButtons();
// 设置自动播放状态
this.shouldAutoPlay = shouldAutoPlay;
// 等待音频数据加载完成后播放
const tryAutoPlay = () => {
if (this.audio.readyState >= 2 && (!this.audio.paused || this.shouldAutoPlay)) {
this.play();
}
};
this.audio.addEventListener('loadedmetadata', tryAutoPlay, { once: true });
this.audio.addEventListener('canplay', tryAutoPlay, { once: true });
this.audio.addEventListener('loadeddata', tryAutoPlay, { once: true });
}
handleTrackEnd() {
if (!this.isPlayingFolder || this.playlist.length === 0) return;
let nextIndex;
switch (this.playbackMode) {
case 'single':
// 单曲循环
nextIndex = this.currentTrackIndex;
break;
case 'random':
// 随机播放
nextIndex = Math.floor(Math.random() * this.playlist.length);
break;
case 'loop':
default:
// 列表循环
nextIndex = (this.currentTrackIndex + 1) % this.playlist.length;
break;
}
// 自动播放下一首
this.loadTrack(nextIndex, true);
}
nextTrack() {
if (!this.isPlayingFolder || this.playlist.length === 0) return;
const nextIndex = (this.currentTrackIndex + 1) % this.playlist.length;
this.loadTrack(nextIndex, true); // 自动播放
}
previousTrack() {
if (!this.isPlayingFolder || this.playlist.length === 0) return;
const prevIndex = this.currentTrackIndex === 0 ? this.playlist.length - 1 : this.currentTrackIndex - 1;
this.loadTrack(prevIndex, true); // 自动播放
}
setupAudioContext() {
try {
// 首次创建音频上下文
if (!this.audioContext) {
this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
this.analyser = this.audioContext.createAnalyser();
this.analyser.fftSize = 2048;
const bufferLength = this.analyser.frequencyBinCount;
this.dataArray = new Uint8Array(bufferLength);
// 初始化高级可视化器
this.waveformVisualizer = new WaveformVisualizer(this.canvas, this.audioContext, this.analyser);
console.log('音频上下文和可视化器初始化成功');
}
// 连接音频源(只在首次或需要重新连接时)
if (this.audioContext && this.analyser && !this.source) {
this.source = this.audioContext.createMediaElementSource(this.audio);
this.source.connect(this.analyser);
this.analyser.connect(this.audioContext.destination);
console.log('音频源连接成功');
}
} catch (error) {
console.error('音频上下文初始化失败:', error);
if (!this.webAudioApiWarningShown) {
alert('您的浏览器不支持Web Audio API部分功能可能无法正常使用');
this.webAudioApiWarningShown = true;
}
}
}
play() {
if (this.audio.src) {
// 确保音频上下文处于运行状态
if (this.audioContext && this.audioContext.state === 'suspended') {
this.audioContext.resume();
}
this.audio.play().then(() => {
this.startVisualization();
this.updatePlayPauseButton(true);
document.querySelector('.audio-player').classList.add('playing');
}).catch(error => {
console.error('播放失败:', error);
alert('播放失败,请检查音频文件');
});
}
}
pause() {
this.audio.pause();
this.stopVisualization();
this.updatePlayPauseButton(false);
document.querySelector('.audio-player').classList.remove('playing');
}
// 移除旧的togglePlayPause方法因为已经移到了上面
updatePlayPauseButton(isPlaying) {
if (this.playPauseBtn) {
this.playPauseBtn.textContent = isPlaying ? '⏸️' : '▶️';
}
}
stop() {
this.audio.pause();
this.audio.currentTime = 0;
this.stopVisualization();
this.clearCanvas();
this.updatePlayPauseButton(false);
document.querySelector('.audio-player').classList.remove('playing');
}
updateDuration() {
const duration = this.audio.duration;
this.duration.textContent = this.formatTime(duration);
}
updateProgress() {
// 只在不拖动时更新进度条
if (this.isDraggingProgress) return;
const currentTime = this.audio.currentTime;
const duration = this.audio.duration;
const progressPercent = (currentTime / duration) * 100;
this.progress.style.width = progressPercent + '%';
this.currentTime.textContent = this.formatTime(currentTime);
// 更新手柄位置
if (this.progressHandle && this.progressBar) {
const handleX = (currentTime / duration) * this.progressBar.offsetWidth - 8;
this.progressHandle.style.left = Math.max(-8, Math.min(this.progressBar.offsetWidth - 8, handleX)) + 'px';
}
}
updateVolume(event) {
const volume = event.target.value / 100;
this.audio.volume = volume;
this.volumeValue.textContent = event.target.value + '%';
// 更新静音状态
if (volume > 0 && this.isMuted) {
this.isMuted = false;
} else if (volume === 0 && !this.isMuted) {
this.isMuted = true;
}
// 如果音量大于0且不是静音状态保存当前音量
if (volume > 0 && !this.isMuted) {
this.previousVolume = volume;
}
this.updateVolumeIcon(Math.round(volume * 100));
console.log('音量设置为:', volume, '静音状态:', this.isMuted);
}
syncVolumeSlider() {
// 同步滑块位置与音频实际音量(处理音频元素自身的音量变化)
const currentVolume = Math.round(this.audio.volume * 100);
this.volumeSlider.value = currentVolume;
this.volumeValue.textContent = currentVolume + '%';
this.updateVolumeIcon(currentVolume);
}
updateVolumeIcon(volume) {
if (this.muteBtn) {
if (volume === 0 || this.isMuted) {
this.muteBtn.textContent = '🔇';
this.muteBtn.classList.add('muted');
} else if (volume < 30) {
this.muteBtn.textContent = '🔈';
this.muteBtn.classList.remove('muted');
} else if (volume < 70) {
this.muteBtn.textContent = '🔉';
this.muteBtn.classList.remove('muted');
} else {
this.muteBtn.textContent = '🔊';
this.muteBtn.classList.remove('muted');
}
}
}
toggleMute() {
if (this.isMuted) {
// 取消静音,恢复到之前的音量
this.audio.volume = this.previousVolume;
this.volumeSlider.value = Math.round(this.previousVolume * 100);
this.volumeValue.textContent = Math.round(this.previousVolume * 100) + '%';
this.isMuted = false;
} else {
// 静音,保存当前音量
this.previousVolume = this.audio.volume;
this.audio.volume = 0;
this.volumeSlider.value = 0;
this.volumeValue.textContent = '0%';
this.isMuted = true;
}
this.updateVolumeIcon(this.audio.volume);
console.log(this.isMuted ? '已静音' : '取消静音', '音量:', this.audio.volume);
}
formatTime(seconds) {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs.toString().padStart(2, '0')}`;
}
startVisualization() {
if (!this.waveformVisualizer) {
console.warn('可视化器未初始化,使用备用方案');
this.startBasicVisualization();
return;
}
const draw = () => {
this.animationId = requestAnimationFrame(draw);
this.waveformVisualizer.draw();
};
draw();
}
startBasicVisualization() {
// 备用基础可视化方案
if (!this.analyser) return;
const draw = () => {
this.animationId = requestAnimationFrame(draw);
this.analyser.getByteFrequencyData(this.dataArray);
this.ctx.fillStyle = 'rgb(0, 0, 0)';
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
const barWidth = (this.canvas.width / this.dataArray.length) * 2.5;
let barHeight;
let x = 0;
for (let i = 0; i < this.dataArray.length; i++) {
barHeight = (this.dataArray[i] / 255) * this.canvas.height;
const r = barHeight + (25 * (i / this.dataArray.length));
const g = 250 * (i / this.dataArray.length);
const b = 50;
this.ctx.fillStyle = `rgb(${r},${g},${b})`;
this.ctx.fillRect(x, this.canvas.height - barHeight, barWidth, barHeight);
x += barWidth + 1;
}
};
draw();
}
stopVisualization() {
if (this.animationId) {
cancelAnimationFrame(this.animationId);
this.animationId = null;
}
}
clearCanvas() {
if (this.waveformVisualizer) {
this.ctx.fillStyle = 'rgb(0, 0, 0)';
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
}
}
switchVisualization() {
if (this.waveformVisualizer) {
this.waveformVisualizer.nextVisualization();
const currentMode = this.waveformVisualizer.getCurrentVisualization();
const modeNames = {
'bars': '条形图',
'wave': '波形图',
'circular': '圆形图'
};
if (this.visualizationMode) {
this.visualizationMode.textContent = modeNames[currentMode] || '条形图';
console.log('切换到可视化模式:', currentMode);
}
// 添加视觉反馈
this.showVisualizationFeedback(currentMode);
} else {
console.warn('可视化器未初始化');
}
}
showVisualizationFeedback(mode) {
// 创建切换反馈效果
const feedback = document.createElement('div');
feedback.textContent = this.getModeDisplayName(mode);
feedback.className = 'visualization-feedback';
document.body.appendChild(feedback);
// 1.5秒后移除反馈元素
setTimeout(() => {
if (feedback.parentNode) {
feedback.parentNode.removeChild(feedback);
}
}, 1500);
}
getModeDisplayName(mode) {
const modeNames = {
'bars': '🔊 条形图模式',
'wave': '〰️ 波形图模式',
'circular': '⭕ 圆形图模式'
};
return modeNames[mode] || '条形图模式';
}
// 键盘快捷键相关方法
setupKeyboardShortcuts() {
// 键盘快捷键支持
document.addEventListener('keydown', (e) => {
// 防止在输入框中触发快捷键
if (e.target.tagName === 'INPUT') return;
switch (e.key.toLowerCase()) {
case ' ':
e.preventDefault();
this.togglePlayPause();
break;
case 'm':
this.toggleMute();
break;
case 'arrowup':
e.preventDefault();
this.adjustVolume(5);
break;
case 'arrowdown':
e.preventDefault();
this.adjustVolume(-5);
break;
case 'arrowleft':
e.preventDefault();
if (e.ctrlKey && this.isPlayingFolder) {
this.previousTrack();
} else {
this.skipTime(-5);
}
break;
case 'arrowright':
e.preventDefault();
if (e.ctrlKey && this.isPlayingFolder) {
this.nextTrack();
} else {
this.skipTime(5);
}
break;
case 'n':
if (this.isPlayingFolder) {
this.nextTrack();
}
break;
case 'p':
if (this.isPlayingFolder) {
this.previousTrack();
}
break;
}
});
}
initializePlayerState() {
// 初始化播放器状态
this.updatePlayPauseButton(false);
document.querySelector('.audio-player').classList.remove('playing');
// 初始化播放模式显示(直接设置为列表循环)
this.playbackMode = 'loop';
this.updatePlaybackModeDisplay();
// 初始状态下禁用导航按钮
this.updateNavigationButtons();
}
updateNavigationButtons() {
const hasPlaylist = this.isPlayingFolder && this.playlist.length > 0;
this.prevBtn.disabled = !hasPlaylist;
this.nextBtn.disabled = !hasPlaylist;
this.playbackModeBtn.disabled = !hasPlaylist;
if (!hasPlaylist) {
this.prevBtn.style.opacity = '0.5';
this.nextBtn.style.opacity = '0.5';
this.playbackModeBtn.style.opacity = '0.5';
} else {
this.prevBtn.style.opacity = '1';
this.nextBtn.style.opacity = '1';
this.playbackModeBtn.style.opacity = '1';
}
}
togglePlayPause() {
if (this.audio.paused) {
this.play();
} else {
this.pause();
}
}
adjustVolume(change) {
const currentVolume = Math.round(this.audio.volume * 100);
const newVolume = Math.max(0, Math.min(100, currentVolume + change));
this.volumeSlider.value = newVolume;
this.audio.volume = newVolume / 100;
this.volumeValue.textContent = newVolume + '%';
// 更新静音状态
if (newVolume > 0 && this.isMuted) {
this.isMuted = false;
} else if (newVolume === 0 && !this.isMuted) {
this.isMuted = true;
}
if (newVolume > 0 && !this.isMuted) {
this.previousVolume = newVolume / 100;
}
this.updateVolumeIcon(newVolume);
console.log('键盘调节音量:', newVolume);
}
skipTime(seconds) {
if (this.audio.duration) {
const newTime = Math.max(0, Math.min(this.audio.duration, this.audio.currentTime + seconds));
this.audio.currentTime = newTime;
}
}
updatePlaybackModeDisplay() {
const modeIcons = {
'loop': '🔁',
'single': '🔂',
'random': '🔀'
};
const modeTexts = {
'loop': '列表循环',
'single': '单曲循环',
'random': '随机播放'
};
if (this.playbackModeBtn && this.playbackModeText) {
this.playbackModeBtn.textContent = modeIcons[this.playbackMode];
this.playbackModeText.textContent = modeTexts[this.playbackMode];
}
}
togglePlaybackMode() {
const modes = ['loop', 'single', 'random'];
const currentIndex = modes.indexOf(this.playbackMode);
this.playbackMode = modes[(currentIndex + 1) % modes.length];
this.updatePlaybackModeDisplay();
const modeTexts = {
'loop': '列表循环',
'single': '单曲循环',
'random': '随机播放'
};
// 添加视觉反馈
this.showPlaybackModeFeedback(modeTexts[this.playbackMode]);
}
showPlaybackModeFeedback(modeText) {
const feedback = document.createElement('div');
feedback.textContent = `播放模式: ${modeText}`;
feedback.className = 'visualization-feedback';
feedback.style.background = 'rgba(102, 126, 234, 0.9)';
document.body.appendChild(feedback);
setTimeout(() => {
if (feedback.parentNode) {
feedback.parentNode.removeChild(feedback);
}
}, 1500);
}
}
// 初始化播放器
document.addEventListener('DOMContentLoaded', () => {
new AudioPlayer();
});

583
src/style.css Normal file
View File

@ -0,0 +1,583 @@
/* 音乐播放器样式 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
.audio-player {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 20px;
padding: 30px;
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
border: 1px solid rgba(255, 255, 255, 0.18);
width: 100%;
max-width: 900px;
color: white;
}
.audio-player h1 {
text-align: center;
margin-bottom: 20px;
font-size: 2.5em;
font-weight: 300;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
}
/* 文件选择器 */
.file-selector {
margin-bottom: 15px;
text-align: center;
display: flex;
justify-content: center;
gap: 15px;
flex-wrap: wrap;
}
.folder-label {
background: linear-gradient(45deg, #667eea, #764ba2);
}
.file-selector input[type="file"] {
display: none;
}
.file-label {
display: inline-block;
padding: 12px 30px;
background: linear-gradient(45deg, #ff6b6b, #ee5a24);
color: white;
border-radius: 25px;
cursor: pointer;
transition: all 0.3s ease;
font-weight: 500;
box-shadow: 0 4px 15px rgba(238, 90, 36, 0.4);
}
.file-label:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(238, 90, 36, 0.6);
}
/* 波形图容器 */
.waveform-container {
margin: 20px 0;
text-align: center;
border-radius: 15px;
overflow: hidden;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
height: 320px;
}
#waveform {
width: 100%;
height: 100%;
background: #000;
border-radius: 15px;
cursor: pointer;
}
#waveform:hover {
transform: scale(1.01);
}
/* 播放控制和进度条容器 */
.playback-progress-container {
display: flex;
align-items: center;
gap: 15px;
margin: 20px 0;
}
.control-btn {
padding: 12px 25px;
border: none;
border-radius: 25px;
background: linear-gradient(45deg, #4facfe, #00f2fe);
color: white;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(79, 172, 254, 0.4);
white-space: nowrap;
}
/* 播放/暂停按钮 */
.play-pause-btn {
padding: 12px;
border-radius: 50%;
width: 40px;
height: 40px;
font-size: 20px;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(90deg, #ff6b6b, #4ecdc4);
box-shadow: 0 3px 12px rgba(255, 107, 107, 0.4);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border: 2px solid rgba(255, 255, 255, 0.3);
}
.play-pause-btn:hover {
box-shadow: 0 5px 18px rgba(255, 107, 107, 0.6);
transform: scale(1.08);
border-color: rgba(255, 255, 255, 0.5);
}
.play-pause-btn:active {
transform: scale(0.95);
}
/* 停止按钮 */
.stop-btn {
padding: 10px;
border-radius: 50%;
width: 40px;
height: 40px;
font-size: 16px;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(90deg, #667eea, #764ba2);
box-shadow: 0 2px 10px rgba(102, 126, 234, 0.4);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border: 2px solid rgba(255, 255, 255, 0.2);
}
.stop-btn:hover {
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.6);
transform: scale(1.08);
border-color: rgba(255, 255, 255, 0.4);
}
.stop-btn:active {
transform: scale(0.95);
}
/* 导航按钮(上一首/下一首) */
.nav-btn {
padding: 10px;
border-radius: 50%;
width: 40px;
height: 40px;
font-size: 16px;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(45deg, #4facfe, #00f2fe);
box-shadow: 0 2px 10px rgba(79, 172, 254, 0.4);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border: 2px solid rgba(255, 255, 255, 0.2);
}
.nav-btn:hover {
box-shadow: 0 4px 15px rgba(79, 172, 254, 0.6);
transform: scale(1.08);
border-color: rgba(255, 255, 255, 0.4);
}
.nav-btn:active {
transform: scale(0.95);
}
.nav-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none !important;
}
.control-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(79, 172, 254, 0.6);
}
.control-btn:active {
transform: translateY(0);
}
/* 进度条 */
.progress-container {
flex: 1;
user-select: none; /* 防止拖动时选中文本 */
margin-top: 15px;
}
.progress-bar {
width: 100%;
height: 6px;
background: rgba(255, 255, 255, 0.15);
border-radius: 3px;
position: relative;
cursor: pointer;
margin-bottom: 8px;
transition: all 0.3s ease;
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.2);
}
.progress-bar:hover {
height: 8px;
box-shadow: 0 2px 8px rgba(255, 255, 255, 0.2), inset 0 1px 3px rgba(0, 0, 0, 0.2);
}
.progress {
height: 100%;
background: linear-gradient(90deg, #ff6b6b, #4ecdc4);
width: 0%;
transition: width 0.1s ease;
border-radius: 3px;
position: relative;
box-shadow: 0 1px 3px rgba(255, 107, 107, 0.4);
}
.progress-handle {
position: absolute;
top: 50%;
right: -6px;
transform: translateY(-50%);
width: 12px;
height: 12px;
background: linear-gradient(45deg, #ff6b6b, #4ecdc4);
border-radius: 50%;
cursor: grab;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
transition: all 0.2s ease;
z-index: 10;
border: 1px solid rgba(255, 255, 255, 0.8);
}
.progress-handle:hover {
transform: translateY(-50%) scale(1.4);
box-shadow: 0 3px 8px rgba(255, 107, 107, 0.4);
border-width: 2px;
}
.progress-handle:active {
cursor: grabbing;
transform: translateY(-50%) scale(1.2);
box-shadow: 0 2px 6px rgba(255, 107, 107, 0.6);
}
.time-display {
text-align: center;
font-size: 14px;
color: rgba(255, 255, 255, 0.8);
}
/* 底部控制栏 */
.bottom-controls {
display: flex;
justify-content: space-between;
align-items: center;
gap: 20px;
margin: 20px 0;
}
/* 播放模式控制 */
.playback-mode-control {
display: flex;
align-items: center;
gap: 10px;
background: rgba(255, 255, 255, 0.05);
padding: 8px 12px;
border-radius: 20px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
#playbackModeBtn {
background: linear-gradient(45deg, #ff6b6b, #ee5a24);
padding: 8px 12px;
border-radius: 50%;
width: 36px;
height: 36px;
font-size: 16px;
display: flex;
align-items: center;
justify-content: center;
border: none;
min-width: 36px;
}
#playbackModeText {
font-size: 14px;
font-weight: 500;
white-space: nowrap;
}
/* 可视化控制 */
.visualization-control {
display: flex;
align-items: center;
gap: 15px;
}
#visualizationBtn {
background: linear-gradient(45deg, #667eea, #764ba2);
}
#visualizationMode {
font-weight: 500;
color: rgba(255, 255, 255, 0.9);
}
/* 音量控制 */
.volume-control {
display: flex;
align-items: center;
gap: 10px;
}
.volume-control label {
font-weight: 500;
white-space: nowrap;
}
#volumeSlider {
width: 120px;
height: 5px;
background: rgba(255, 255, 255, 0.3);
border-radius: 5px;
outline: none;
-webkit-appearance: none;
}
#volumeSlider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 20px;
height: 20px;
background: linear-gradient(45deg, #ff6b6b, #4ecdc4);
border-radius: 50%;
cursor: pointer;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
}
#volumeSlider::-moz-range-thumb {
width: 20px;
height: 20px;
background: linear-gradient(45deg, #ff6b6b, #4ecdc4);
border-radius: 50%;
cursor: pointer;
border: none;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
}
#volumeValue {
min-width: 40px;
font-weight: 500;
}
/* 音量按钮 */
.volume-btn {
background: linear-gradient(45deg, #667eea, #764ba2);
padding: 8px;
font-size: 16px;
border-radius: 50%;
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
border: none;
min-width: 36px;
}
.volume-btn:hover {
transform: scale(1.1);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.volume-btn:active {
transform: scale(0.95);
}
.volume-btn.muted {
background: linear-gradient(45deg, #ff6b6b, #ee5a24);
}
.volume-btn.muted:hover {
box-shadow: 0 4px 12px rgba(255, 107, 107, 0.4);
}
/* 曲目信息 */
.track-info {
text-align: center;
margin: 15px 0;
padding: 10px;
background: rgba(255, 255, 255, 0.1);
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.2);
}
#trackName {
font-size: 16px;
font-weight: 500;
color: rgba(255, 255, 255, 0.9);
margin: 0;
}
/* 响应式设计 */
@media (max-width: 768px) {
.audio-player {
padding: 20px;
margin: 10px;
}
.audio-player h1 {
font-size: 2em;
}
.playback-progress-container {
flex-direction: row;
align-items: center;
gap: 10px;
}
.play-pause-btn {
width: 42px;
height: 42px;
font-size: 16px;
}
.stop-btn {
width: 32px;
height: 32px;
font-size: 12px;
}
.waveform-container {
height: 250px;
}
.bottom-controls {
flex-direction: column;
gap: 15px;
}
.playback-mode-control {
justify-content: center;
width: 100%;
}
.visualization-control,
.volume-control {
justify-content: center;
width: 100%;
}
#volumeSlider {
width: 150px;
}
.control-btn {
font-size: 14px;
padding: 10px 20px;
}
}
@media (max-width: 480px) {
.audio-player {
padding: 15px;
}
.audio-player h1 {
font-size: 1.8em;
}
.waveform-container {
height: 200px;
}
.play-pause-btn {
width: 45px;
height: 45px;
font-size: 18px;
}
.stop-btn {
width: 35px;
height: 35px;
font-size: 14px;
}
#volumeSlider {
width: 120px;
}
}
/* 动画效果 */
@keyframes pulse {
0% {
transform: scale(1);
}
50% {
transform: scale(1.05);
}
100% {
transform: scale(1);
}
}
@keyframes glow {
0% {
box-shadow: 0 0 5px rgba(255, 107, 107, 0.5);
}
50% {
box-shadow: 0 0 20px rgba(255, 107, 107, 0.8), 0 0 30px rgba(255, 107, 107, 0.6);
}
100% {
box-shadow: 0 0 5px rgba(255, 107, 107, 0.5);
}
}
.audio-player.playing .play-pause-btn {
animation: glow 2s infinite;
background: linear-gradient(90deg, #ff6b6b, #ff8e53);
}
@keyframes fadeInOut {
0% {
opacity: 0;
transform: translate(-50%, -50%) scale(0.8);
}
20% {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
80% {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
100% {
opacity: 0;
transform: translate(-50%, -50%) scale(0.8);
}
}
.visualization-feedback {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 15px 25px;
border-radius: 25px;
font-size: 18px;
font-weight: bold;
z-index: 1000;
pointer-events: none;
animation: fadeInOut 1.5s ease-in-out;
}

224
src/waveformVisualizer.js Normal file
View File

@ -0,0 +1,224 @@
// 高级波形图可视化器
class WaveformVisualizer {
constructor(canvas, audioContext, analyser) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
this.audioContext = audioContext;
this.analyser = analyser;
this.bufferLength = this.analyser.frequencyBinCount;
this.dataArray = new Uint8Array(this.bufferLength);
this.setupCanvas();
this.visualizationTypes = ['bars', 'wave', 'circular'];
this.currentVisualization = 0;
}
setupCanvas() {
const rect = this.canvas.getBoundingClientRect();
this.canvas.width = rect.width * window.devicePixelRatio;
this.canvas.height = rect.height * window.devicePixelRatio;
this.ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
this.canvas.style.width = rect.width + 'px';
this.canvas.style.height = rect.height + 'px';
}
drawBars() {
this.analyser.getByteFrequencyData(this.dataArray);
this.ctx.fillStyle = 'rgb(0, 0, 0)';
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
const barWidth = (this.canvas.width / this.bufferLength) * 2.5;
let barHeight;
let x = 0;
for (let i = 0; i < this.bufferLength; i++) {
barHeight = (this.dataArray[i] / 255) * this.canvas.height;
// 创建渐变效果
const gradient = this.ctx.createLinearGradient(0, this.canvas.height - barHeight, 0, this.canvas.height);
const r = barHeight + (25 * (i / this.bufferLength));
const g = 250 * (i / this.bufferLength);
const b = 50;
gradient.addColorStop(0, `rgb(${r}, ${g}, ${b})`);
gradient.addColorStop(1, `rgb(${Math.floor(r/2)}, ${Math.floor(g/2)}, ${Math.floor(b/2)})`);
this.ctx.fillStyle = gradient;
this.ctx.fillRect(x, this.canvas.height - barHeight, barWidth, barHeight);
x += barWidth + 1;
}
}
drawWave() {
this.analyser.getByteTimeDomainData(this.dataArray);
this.ctx.fillStyle = 'rgb(0, 0, 0)';
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
this.ctx.lineWidth = 2;
this.ctx.strokeStyle = 'rgb(0, 255, 0)';
this.ctx.beginPath();
const sliceWidth = this.canvas.width * 1.0 / this.bufferLength;
let x = 0;
for (let i = 0; i < this.bufferLength; i++) {
const v = this.dataArray[i] / 128.0;
const y = v * this.canvas.height / 2;
if (i === 0) {
this.ctx.moveTo(x, y);
} else {
this.ctx.lineTo(x, y);
}
x += sliceWidth;
}
this.ctx.lineTo(this.canvas.width, this.canvas.height / 2);
this.ctx.stroke();
}
drawCircular() {
this.drawCompactCircular();
}
drawCompactCircular() {
// 更紧凑的圆形图版本 - 圆心在画布中央,大小适中
this.analyser.getByteFrequencyData(this.dataArray);
this.ctx.fillStyle = 'rgb(0, 0, 0)';
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
// 获取画布的实际显示尺寸
const displayWidth = this.canvas.width / window.devicePixelRatio;
const displayHeight = this.canvas.height / window.devicePixelRatio;
// 圆心位置在画布中央
const centerX = displayWidth / 2;
const centerY = displayHeight / 2;
// 适中的圆形图大小 - 占画布最小尺寸的30-35%
const baseRadius = Math.min(centerX, centerY) * 0.32;
const maxAmplitude = Math.min(centerX, centerY) * 0.22;
// 绘制频谱线条 - 平衡视觉效果和性能
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 amplitude = (this.dataArray[i] / 255) * maxAmplitude;
// 起始点(基础圆上)
const x1 = centerX + Math.cos(angle) * baseRadius;
const y1 = centerY + Math.sin(angle) * baseRadius;
// 结束点(根据频率数据延伸)
const x2 = centerX + Math.cos(angle) * (baseRadius + amplitude);
const y2 = centerY + Math.sin(angle) * (baseRadius + amplitude);
// 根据频率强度计算颜色
const intensity = this.dataArray[i] / 255;
const hue = (i / this.bufferLength) * 360;
const saturation = 75 + (intensity * 25);
const lightness = 50 + (intensity * 30);
this.ctx.strokeStyle = `hsl(${hue}, ${saturation}%, ${lightness}%)`;
this.ctx.lineWidth = 1 + (intensity * 1.5);
this.ctx.beginPath();
this.ctx.moveTo(x1, y1);
this.ctx.lineTo(x2, y2);
this.ctx.stroke();
}
}
drawCircular() {
this.analyser.getByteFrequencyData(this.dataArray);
this.ctx.fillStyle = 'rgb(0, 0, 0)';
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
// 获取画布的实际显示尺寸(考虑设备像素比)
const displayWidth = this.canvas.width / window.devicePixelRatio;
const displayHeight = this.canvas.height / window.devicePixelRatio;
// 圆心位置在画布中央
const centerX = displayWidth / 2;
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 amplitude = (this.dataArray[i] / 255) * maxAmplitude;
// 起始点(基础圆上)
const x1 = centerX + Math.cos(angle) * baseRadius;
const y1 = centerY + Math.sin(angle) * baseRadius;
// 结束点(根据频率数据延伸)
const x2 = centerX + Math.cos(angle) * (baseRadius + amplitude);
const y2 = centerY + Math.sin(angle) * (baseRadius + amplitude);
// 根据频率数据计算颜色
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();
}
draw() {
switch (this.visualizationTypes[this.currentVisualization]) {
case 'bars':
this.drawBars();
break;
case 'wave':
this.drawWave();
break;
case 'circular':
this.drawCircular();
break;
default:
this.drawBars();
}
}
nextVisualization() {
this.currentVisualization = (this.currentVisualization + 1) % this.visualizationTypes.length;
}
getCurrentVisualization() {
return this.visualizationTypes[this.currentVisualization];
}
}
export default WaveformVisualizer;

13
vite.config.js Normal file
View File

@ -0,0 +1,13 @@
import { defineConfig } from 'vite'
export default defineConfig({
// Vite配置选项
server: {
port: 3000,
open: true
},
build: {
outDir: 'dist',
assetsDir: 'assets'
}
})