
提示词:
“制作一个可以上传wav音频的html,并可以播放声音,播放声音时有动感炫酷的声波跳动,动感炫酷的声波跳动波纹看着非常有高级质感,使用Flex布局,将声波条纹做成横向居中,旋律上下跳动效果,,色调为醒目的灰黑。单个文件其中包含CSS和JavaScript。
将声波展示加一个全屏按钮,点了全屏按钮后将声波展示全屏显示,再点一次全屏按钮后则恢复初始状态的声波展示。”
视频:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WAV 音频播放器与声波可视化</title>
<style>
/* --- 基本样式与布局 --- */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: sans-serif;
background-color: #1a1a1a; /* 深灰色背景 */
color: #e0e0e0; /* 浅灰色文字 */
display: flex;
flex-direction: column; /* 垂直排列元素 */
justify-content: center; /* 垂直居中(主轴) */
align-items: center; /* 水平居中(交叉轴) */
min-height: 100vh; /* 占据至少整个视口高度 */
padding: 20px;
overflow: hidden; /* 防止页面滚动条在全屏时出现问题 */
}
/* --- 主要容器 --- */
.container {
display: flex;
flex-direction: column;
align-items: center;
gap: 20px; /* 元素间距 */
width: 100%;
max-width: 800px; /* 限制最大宽度 */
}
/* --- 文件上传与播放器 --- */
.controls {
display: flex;
flex-direction: column;
align-items: center;
gap: 15px;
background-color: #2a2a2a;
padding: 20px;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
}
label {
font-size: 1.1em;
cursor: pointer;
padding: 10px 20px;
background-color: #444;
color: #fff;
border-radius: 5px;
transition: background-color 0.3s ease;
}
label:hover {
background-color: #555;
}
input[type="file"] {
display: none; /* 隐藏默认的文件输入 */
}
#audioPlayer {
width: 100%;
margin-top: 10px;
filter: contrast(1.5) brightness(0.8); /* 给播放器一点质感 */
}
/* --- 可视化区域容器 --- */
#visualizer-container {
width: 100%;
position: relative; /* 用于定位全屏按钮 */
background-color: #222; /* 可视化区域背景 */
border: 1px solid #444;
border-radius: 5px;
display: flex; /* 使用Flex居中Canvas */
justify-content: center;
align-items: center;
padding: 10px 0; /* 上下留空 */
overflow: hidden; /* 隐藏可能溢出的内容 */
transition: all 0.3s ease-in-out; /* 平滑过渡效果 */
}
/* --- Canvas 画布 --- */
#audioVisualizer {
display: block; /* 消除Canvas下方的空隙 */
background-color: transparent; /* 背景由容器控制 */
/* 初始宽度,高度由JS设置 */
height: 150px; /* 初始高度 */
width: calc(100% - 20px); /* 宽度占满容器,留点边距 */
max-width: 100%;
}
/* --- 全屏按钮 --- */
#fullscreen-btn {
position: absolute;
top: 8px;
right: 8px;
padding: 5px 10px;
background-color: rgba(80, 80, 80, 0.7);
color: #fff;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.8em;
z-index: 10;
transition: background-color 0.3s ease;
}
#fullscreen-btn:hover {
background-color: rgba(100, 100, 100, 0.9);
}
/* --- 全屏状态下的样式 --- */
#visualizer-container.fullscreen-mode {
position: fixed; /* 固定定位,覆盖整个屏幕 */
top: 0;
left: 0;
width: 100vw; /* 视口宽度 */
height: 100vh; /* 视口高度 */
z-index: 1000; /* 置于顶层 */
background-color: #111; /* 全屏时更深的背景 */
border-radius: 0; /* 取消圆角 */
padding: 0; /* 不需要内边距 */
border: none; /* 不需要边框 */
}
#visualizer-container.fullscreen-mode #audioVisualizer {
width: 100%; /* 宽度占满 */
height: 100%; /* 高度占满 */
max-width: none;
}
#visualizer-container.fullscreen-mode #fullscreen-btn {
top: 15px;
right: 15px;
font-size: 1em;
padding: 8px 15px;
}
</style>
</head>
<body>
<div class="container">
<h1>音频可视化播放器</h1>
<div class="controls">
<label for="audioFile">上传 WAV 文件</label>
<input type="file" id="audioFile" accept=".wav">
<audio id="audioPlayer" controls></audio>
</div>
<div id="visualizer-container">
<canvas id="audioVisualizer"></canvas>
<button id="fullscreen-btn">全屏</button>
</div>
</div>
<script>
// --- DOM 元素获取 ---
const audioFileInput = document.getElementById('audioFile');
const audioPlayer = document.getElementById('audioPlayer');
const canvas = document.getElementById('audioVisualizer');
const visualizerContainer = document.getElementById('visualizer-container');
const fullscreenBtn = document.getElementById('fullscreen-btn');
const canvasCtx = canvas.getContext('2d');
// --- Web Audio API 变量 ---
let audioContext;
let analyser;
let source;
let dataArray; // 用于存储频率数据的数组
let bufferLength; // dataArray 的长度
// --- 动画控制 ---
let animationFrameId = null;
// --- 状态变量 ---
let isVisualizing = false;
let initialCanvasWidth = canvas.width; // 存储初始尺寸
let initialCanvasHeight = canvas.height;
// --- 初始化函数 ---
function initAudioApi() {
if (!audioContext) {
try {
// 创建音频上下文
audioContext = new (window.AudioContext || window.webkitAudioContext)();
// 创建分析器节点
analyser = audioContext.createAnalyser();
analyser.fftSize = 512; // FFT 窗口大小 (必须是2的幂),影响细节和平滑度
analyser.smoothingTimeConstant = 0.85; // 平滑效果 (0 到 1)
// 获取频率数据数组长度
bufferLength = analyser.frequencyBinCount; // 长度是 fftSize / 2
dataArray = new Uint8Array(bufferLength); // 8位无符号整数数组
// 将分析器连接到音频目标(扬声器)
// 注意:源节点 (source) 在文件加载后创建和连接
analyser.connect(audioContext.destination);
console.log("AudioContext and Analyser initialized.");
} catch (e) {
console.error("无法创建 AudioContext:", e);
alert("您的浏览器不支持 Web Audio API,无法进行音频可视化。");
}
}
}
// --- 文件上传处理 ---
audioFileInput.addEventListener('change', function(event) {
const file = event.target.files[0];
if (file && file.type === 'audio/wav') {
// 暂停可能正在播放的音频和动画
stopVisualization();
audioPlayer.pause();
audioPlayer.currentTime = 0;
const objectURL = URL.createObjectURL(file);
audioPlayer.src = objectURL;
audioPlayer.load(); // 重新加载音频元素
console.log("WAV file selected:", file.name);
// 确保 AudioContext 已初始化 (可能因浏览器策略需要用户交互后才能创建)
if (!audioContext) {
initAudioApi();
}
// 如果已有 source 节点,先断开连接
if (source) {
source.disconnect();
}
// 创建媒体元素源节点 (必须在 audio.src 设置之后)
if (audioContext && !source) { // 确保只创建一次或在需要时重新创建
try {
source = audioContext.createMediaElementSource(audioPlayer);
// 连接:源 -> 分析器 -> 扬声器
source.connect(analyser);
// 注意:上面已经将 analyser 连接到 destination,所以这里不需要再连接 source 到 destination
console.log("Audio source connected to analyser.");
} catch (e) {
console.error("Error creating MediaElementSource:", e);
// 可能是因为之前创建过,或者 audio 元素状态问题
// 尝试重新初始化API或提示用户
}
} else if (audioContext && source) {
// 如果 source 已存在,只需重新连接 (可能在播放暂停/停止后需要)
source.connect(analyser);
console.log("Audio source re-connected to analyser.");
}
// 提示用户可以播放了
console.log("Audio ready to play.");
} else if (file) {
alert('请选择一个 .wav 格式的音频文件。');
audioFileInput.value = ''; // 清空选择,以便可以再次选择相同文件
}
});
// --- 音频播放事件 ---
audioPlayer.addEventListener('play', () => {
if (audioContext) {
// 恢复可能被挂起的 AudioContext (重要!)
if (audioContext.state === 'suspended') {
audioContext.resume().then(() => {
console.log("AudioContext resumed.");
startVisualization();
});
} else {
startVisualization();
}
} else {
console.warn("AudioContext not ready when play event fired.");
// 尝试再次初始化
initAudioApi();
if(audioContext && !source) { // 如果初始化成功且没有源,尝试创建源
try {
source = audioContext.createMediaElementSource(audioPlayer);
source.connect(analyser);
console.log("Audio source connected after late initialization.");
startVisualization(); // 再次尝试启动
} catch (e) {
console.error("Late source creation failed:", e);
}
} else if (isVisualizing) {
startVisualization(); // 如果 context 存在,直接启动
}
}
});
// --- 音频暂停/结束事件 ---
audioPlayer.addEventListener('pause', stopVisualization);
audioPlayer.addEventListener('ended', stopVisualization);
// --- 开始可视化动画 ---
function startVisualization() {
if (!isVisualizing && analyser) {
console.log("Starting visualization...");
isVisualizing = true;
resizeCanvas(); // 确保画布尺寸正确
drawVisualizer(); // 启动动画循环
}
}
// --- 停止可视化动画 ---
function stopVisualization() {
if (isVisualizing) {
console.log("Stopping visualization.");
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
animationFrameId = null;
}
// 可选:清除画布
// canvasCtx.clearRect(0, 0, canvas.width, canvas.height);
isVisualizing = false;
}
}
// --- 绘制声波函数 ---
function drawVisualizer() {
// 请求下一帧动画
animationFrameId = requestAnimationFrame(drawVisualizer);
// 获取当前的频率数据
if (!analyser) return;
analyser.getByteFrequencyData(dataArray); // 将数据填充到 dataArray
// 清空画布
canvasCtx.clearRect(0, 0, canvas.width, canvas.height);
// --- 设置绘制样式 ---
const barWidth = (canvas.width / bufferLength) * 0.9; // 条纹宽度,留一点间隙
const barGap = (canvas.width / bufferLength) * 0.1; // 间隙宽度
const barColor = '#cccccc'; // 醒目的浅灰色条纹
canvasCtx.fillStyle = barColor;
let x = 0; // 条纹的起始 X 坐标
// 遍历频率数据,绘制条纹
for (let i = 0; i < bufferLength; i++) {
// dataArray[i] 的值范围是 0 - 255
// 将其映射到画布高度
const barHeight = (dataArray[i] / 255) * canvas.height * 0.8; // 乘以0.8使最高峰不顶满
// 计算条纹的 Y 坐标,使其从垂直中心线向上下扩展
const y = (canvas.height - barHeight) / 2;
// 绘制条纹矩形
canvasCtx.fillRect(x, y, barWidth, barHeight);
// 更新下一个条纹的 X 坐标
x += barWidth + barGap;
}
}
// --- 调整 Canvas 尺寸 ---
function resizeCanvas() {
if (document.fullscreenElement === visualizerContainer) {
// 全屏模式
canvas.width = visualizerContainer.clientWidth;
canvas.height = visualizerContainer.clientHeight;
console.log(`Canvas resized to fullscreen: ${canvas.width}x${canvas.height}`);
} else {
// 非全屏模式 (使用CSS定义的尺寸或初始尺寸)
// 让 canvas 内部尺寸匹配其显示尺寸
canvas.width = canvas.clientWidth; // 获取渲染后的宽度
canvas.height = canvas.clientHeight; // 获取渲染后的高度
console.log(`Canvas resized to normal: ${canvas.width}x${canvas.height}`);
// 如果需要恢复到特定初始值,可以使用 initialCanvasWidth/Height
// canvas.width = initialCanvasWidth;
// canvas.height = initialCanvasHeight;
}
// 如果正在可视化,需要确保重绘以适应新尺寸
if (isVisualizing && animationFrameId === null) {
// 如果动画已停止但标记为isVisualizing (例如窗口大小改变时暂停了),重新启动
drawVisualizer();
} else if (!isVisualizing) {
// 如果未开始可视化,可以先清空一下确保背景正确
canvasCtx.clearRect(0, 0, canvas.width, canvas.height);
}
}
// --- 全屏按钮处理 ---
fullscreenBtn.addEventListener('click', () => {
if (!document.fullscreenElement) {
// 进入全屏
visualizerContainer.requestFullscreen()
.then(() => {
visualizerContainer.classList.add('fullscreen-mode');
fullscreenBtn.textContent = '退出全屏';
resizeCanvas(); // 进入全屏后调整大小
})
.catch(err => {
console.error(`Error attempting to enable full-screen mode: ${err.message} (${err.name})`);
alert(`无法进入全屏模式: ${err.message}`);
});
} else {
// 退出全屏
if (document.exitFullscreen) {
document.exitFullscreen()
.then(() => {
// Class removal and button text handled by fullscreenchange event
})
.catch(err => {
console.error(`Error attempting to disable full-screen mode: ${err.message} (${err.name})`);
});
}
}
});
// --- 监听全屏状态变化 (包括按 Esc 键退出) ---
document.addEventListener('fullscreenchange', () => {
if (document.fullscreenElement === visualizerContainer) {
// 确认进入全屏状态
if (!visualizerContainer.classList.contains('fullscreen-mode')) {
visualizerContainer.classList.add('fullscreen-mode');
fullscreenBtn.textContent = '退出全屏';
resizeCanvas(); // 确保尺寸正确
console.log("Entered fullscreen via event.");
}
} else {
// 确认退出全屏状态
if (visualizerContainer.classList.contains('fullscreen-mode')) {
visualizerContainer.classList.remove('fullscreen-mode');
fullscreenBtn.textContent = '全屏';
resizeCanvas(); // 退出全屏后恢复大小
console.log("Exited fullscreen via event.");
}
}
});
// --- 窗口大小变化时也调整Canvas ---
window.addEventListener('resize', resizeCanvas);
// --- 页面加载完成后设置初始Canvas尺寸 ---
// 使用 setTimeout 确保 CSS 渲染完成,clientWidth/Height 有效
window.addEventListener('load', () => {
setTimeout(resizeCanvas, 0); // 延迟执行以获取正确的 clientWidth/Height
console.log("Initial canvas resize scheduled.");
});
// --- 初始提示 ---
console.log("Page loaded. Select a WAV file to start.");
</script>
</body>
</html>