零基础5分钟制作HTML带有动感频谱的音频播放器

提示词:

视频:

<!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>

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部