辛苦整理,请您珍惜

 

分贝(dB)为单位显示音量。

 

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

        /* ----- 全局样式 ----- */

        * {

            box-sizing: border-box;

            margin: 0;

            padding: 0;

        }

        body {

            font-family: 'Segoe UI', Roboto, system-ui, sans-serif;

            background: #0b0e17;

            min-height: 100vh;

            display: flex;

            justify-content: center;

            align-items: center;

            margin: 0;

            padding: 20px;

        }

        .card {

            background: #1a1f2f;

            border-radius: 32px;

            padding: 40px 36px 44px;

            max-width: 520px;

            width: 100%;

            box-shadow: 0 20px 50px rgba(0, 0, 0, 0.7);

            text-align: center;

            border: 1px solid #2e364a;

            transition: 0.2s;

        }

        h1 {

            font-size: 24px;

            font-weight: 600;

            letter-spacing: 1px;

            color: #e8edf5;

            margin-bottom: 6px;

        }

        .sub {

            color: #8892b0;

            font-size: 14px;

            margin-bottom: 28px;

            border-bottom: 1px solid #2a3247;

            padding-bottom: 16px;

        }

 

        /* ----- 分贝数值 ----- */

        .db-display {

            background: #0f131f;

            border-radius: 60px;

            padding: 20px 10px;

            margin-bottom: 18px;

            border: 1px solid #2e364a;

        }

        .db-number {

            font-size: 78px;

            font-weight: 700;

            color: #b7c9ff;

            line-height: 1;

            font-variant-numeric: tabular-nums;

            letter-spacing: -1px;

        }

        .db-unit {

            font-size: 28px;

            font-weight: 400;

            color: #6a7a9e;

            margin-left: 4px;

        }

        .db-label {

            color: #6a7a9e;

            font-size: 14px;

            letter-spacing: 2px;

            margin-top: 6px;

        }

 

        /* ----- 音量条 ----- */

        .meter-wrap {

            background: #0f131f;

            border-radius: 40px;

            height: 14px;

            margin: 16px 0 22px;

            overflow: hidden;

            border: 1px solid #2a3247;

        }

        .meter-fill {

            height: 100%;

            width: 0%;

            background: linear-gradient(90deg, #4caf9e, #f4c542, #f27a5e);

            border-radius: 40px;

            transition: width 0.08s ease-out;

        }

 

        /* ----- 控制区 ----- */

        .controls {

            display: flex;

            flex-wrap: wrap;

            gap: 12px;

            justify-content: center;

            margin: 18px 0 14px;

        }

        .btn {

            background: #283042;

            border: none;

            color: #d3defa;

            font-size: 15px;

            font-weight: 500;

            padding: 12px 28px;

            border-radius: 60px;

            cursor: pointer;

            transition: 0.15s;

            flex: 1 1 auto;

            min-width: 110px;

            border: 1px solid #36405a;

            letter-spacing: 0.3px;

        }

        .btn:hover {

            background: #323d5a;

            border-color: #5a6a8e;

            color: #fff;

        }

        .btn.primary {

            background: #3b4b8c;

            border-color: #4f63b0;

            color: #fff;

        }

        .btn.primary:hover {

            background: #4f66b8;

            border-color: #6b82d4;

        }

        .btn:disabled {

            opacity: 0.35;

            pointer-events: none;

            filter: grayscale(0.4);

        }

 

        /* ----- 文件选择 ----- */

        .file-area {

            margin: 10px 0 4px;

        }

        .file-area label {

            display: inline-block;

            background: #1f263b;

            padding: 10px 24px;

            border-radius: 60px;

            color: #b0c0e0;

            font-size: 14px;

            font-weight: 400;

            border: 1px dashed #4a5577;

            cursor: pointer;

            transition: 0.15s;

            width: 100%;

        }

        .file-area label:hover {

            background: #2a334d;

            border-color: #6a7ea8;

        }

        .file-area input[type="file"] {

            display: none;

        }

        .file-name {

            color: #6a7a9e;

            font-size: 13px;

            margin-top: 6px;

            min-height: 20px;

        }

 

        /* ----- 状态 ----- */

        .status {

            margin-top: 18px;

            font-size: 14px;

            color: #6a7a9e;

            background: #121724;

            padding: 10px 14px;

            border-radius: 40px;

            border: 1px solid #242d44;

        }

        .status .highlight {

            color: #b7c9ff;

            font-weight: 500;

        }

 

        /* 响应式 */

        @media (max-width: 460px) {

            .card {

                padding: 28px 18px 32px;

            }

            .db-number {

                font-size: 56px;

            }

            .btn {

                padding: 10px 16px;

                font-size: 14px;

                min-width: 80px;

            }

        }

    </style>

</head>

<body>

 

    <div class="card">

        <h1>分贝测量仪</h1>

        <div class="sub">实时音频 · 麦克风 / 文件</div>

 

        <!-- 分贝数值 -->

        <div class="db-display">

            <div class="db-number">

                <span id="dbValue">--</span><span class="db-unit">dB</span>

            </div>

            <div class="db-label">声压级 (SPL)</div>

        </div>

 

        <!-- 音量条 -->

        <div class="meter-wrap">

            <div class="meter-fill" id="meterFill" style="width:0%;"></div>

        </div>

 

        <!-- 控制按钮 -->

        <div class="controls">

            <button class="btn primary" id="btnMic">麦克风</button>

            <button class="btn" id="btnStop" disabled>停止</button>

        </div>

 

        <!-- 文件上传 -->

        <div class="file-area">

            <label for="audioFile">选择音频 / 视频文件</label>

            <input type="file" id="audioFile" accept="audio/*,video/*" />

            <div class="file-name" id="fileName">未选择文件</div>

        </div>

 

        <!-- 状态 -->

        <div class="status" id="statusDisplay">

            <span class="highlight">●</span> 就绪,点击“麦克风”或上传文件开始

        </div>

    </div>

 

    <script>

        (function() {

            'use strict';

 

            // ----- DOM 引用 -----

            const dbSpan = document.getElementById('dbValue');

            const meterFill = document.getElementById('meterFill');

            const statusEl = document.getElementById('statusDisplay');

            const fileNameEl = document.getElementById('fileName');

 

            const btnMic = document.getElementById('btnMic');

            const btnStop = document.getElementById('btnStop');

            const fileInput = document.getElementById('audioFile');

 

            // ----- 音频上下文 & 节点 -----

            let audioCtx = null;

            let analyser = null;

            let dataArray = null;

 

            // 当前激活的音频源 (用于清理)

            let currentSource = null; // MediaStream | MediaElementAudioSourceNode

            let isRunning = false;

            let rafId = null;

 

            // 平滑系数 (让数值变化更柔和)

            const SMOOTHING = 0.25;

            let smoothedDb = -60;

 

            // ----- 工具: 更新 UI -----

            function updateUI(dbValue) {

                // 限制显示范围 (通常人耳可听范围 0~120dB, 此处映射到 0~100 更直观)

                let clamped = Math.max(0, Math.min(100, dbValue));

                let displayDb = Math.round(clamped);

 

                dbSpan.textContent = displayDb;

 

                // 音量条宽度 (0~100%)

                meterFill.style.width = clamped + '%';

 

                // 根据分贝改变数值颜色 (可选)

                if (clamped < 30) {

                    dbSpan.style.color = '#8a9bc0';

                } else if (clamped < 60) {

                    dbSpan.style.color = '#b7c9ff';

                } else if (clamped < 80) {

                    dbSpan.style.color = '#f4c542';

                } else {

                    dbSpan.style.color = '#f27a5e';

                }

            }

 

            // ----- 核心: 从 AnalyserNode 读取数据并计算分贝 -----

            function analyzeAudio() {

                if (!analyser || !isRunning) return;

 

                // 获取时域数据 (getByteTimeDomainData) 或频域数据 (getByteFrequencyData)

                // 使用时域数据计算 RMS 更接近“响度”感知

                analyser.getByteTimeDomainData(dataArray);

 

                let sum = 0;

                for (let i = 0; i < dataArray.length; i++) {

                    // 将 0-255 映射到 -1..1

                    const val = (dataArray[i] - 128) / 128;

                    sum += val * val;

                }

                const rms = Math.sqrt(sum / dataArray.length);

 

                // 将 RMS 转换为 dB (满量程 0dBFS, 此处映射到 0~100 显示)

                // 公式: dB = 20 * log10(rms) , 通常 rms 在 0~1 之间, 结果在 -inf ~ 0 之间

                let db = 0;

                if (rms > 0.0001) {

                    db = 20 * Math.log10(rms);

                } else {

                    db = -60; // 接近静音

                }

 

                // 映射到 0~100 显示 (将 -60dB ~ 0dB 映射到 0~100)

                // 即: db 从 -60 到 0 对应 0 到 100

                let mapped = (db + 60) / 60 * 100;

                mapped = Math.max(0, Math.min(100, mapped));

 

                // 平滑处理

                smoothedDb = smoothedDb * (1 - SMOOTHING) + mapped * SMOOTHING;

 

                updateUI(smoothedDb);

 

                // 继续下一帧

                rafId = requestAnimationFrame(analyzeAudio);

            }

 

            // ----- 停止所有音频 -----

            function stopAll() {

                isRunning = false;

                if (rafId) {

                    cancelAnimationFrame(rafId);

                    rafId = null;

                }

 

                // 断开 & 关闭上下文

                if (audioCtx && audioCtx.state !== 'closed') {

                    audioCtx.close().catch(() => {});

                }

                audioCtx = null;

                analyser = null;

                dataArray = null;

                currentSource = null;

 

                // 重置 UI

                dbSpan.textContent = '--';

                meterFill.style.width = '0%';

                dbSpan.style.color = '#b7c9ff';

                statusEl.innerHTML = '<span class="highlight">●</span> 已停止';

                btnMic.disabled = false;

                btnStop.disabled = true;

                fileInput.disabled = false;

            }

 

            // ----- 初始化音频上下文 & 分析器 -----

            function initAudioContext() {

                if (audioCtx && audioCtx.state !== 'closed') {

                    // 如果已存在且未关闭, 直接返回

                    return audioCtx;

                }

                // 兼容旧浏览器

                const Ctx = window.AudioContext || window.webkitAudioContext;

                if (!Ctx) {

                    statusEl.innerHTML = '浏览器不支持 Web Audio API';

                    return null;

                }

                audioCtx = new Ctx();

                analyser = audioCtx.createAnalyser();

                analyser.fftSize = 1024;

                analyser.smoothingTimeConstant = 0.8;

                // 设置 min/max 分贝范围 (用于 getByteFrequencyData, 但这里我们用 getByteTimeDomainData 不受影响)

                analyser.minDecibels = -60;

                analyser.maxDecibels = 0;

                dataArray = new Uint8Array(analyser.fftSize);

                return audioCtx;

            }

 

            // ----- 开始分析 (source 已连接) -----

            function startAnalysis() {

                if (!audioCtx || !analyser) {

                    statusEl.innerHTML = '❌ 音频上下文未初始化';

                    return;

                }

 

                // 恢复 suspended 状态

                if (audioCtx.state === 'suspended') {

                    audioCtx.resume().catch(err => {

                        statusEl.innerHTML = '无法恢复音频上下文: ' + err.message;

                        return;

                    });

                }

 

                isRunning = true;

                btnMic.disabled = true;

                btnStop.disabled = false;

                fileInput.disabled = true;

                statusEl.innerHTML = '<span class="highlight">●</span> 测量中...';

 

                // 开始分析循环

                if (rafId) cancelAnimationFrame(rafId);

                analyzeAudio();

            }

 

            // ----- 从麦克风获取音频 -----

            async function startMicrophone() {

                try {

                    // 先停止之前的所有

                    stopAll();

 

                    const ctx = initAudioContext();

                    if (!ctx) return;

 

                    // 请求麦克风

                    const stream = await navigator.mediaDevices.getUserMedia({

                        audio: {

                            echoCancellation: false,

                            noiseSuppression: false,

                            autoGainControl: false

                        }

                    });

                    currentSource = stream;

 

                    // 创建 MediaStreamAudioSourceNode

                    const sourceNode = ctx.createMediaStreamSource(stream);

                    sourceNode.connect(analyser);

                    // 不连接 destination 以免产生反馈啸叫 (仅分析用)

 

                    statusEl.innerHTML = '<span class="highlight">●</span> 麦克风已启动';

                    startAnalysis();

 

                } catch (err) {

                    statusEl.innerHTML = '麦克风访问被拒绝: ' + err.message;

                    btnMic.disabled = false;

                    console.error(err);

                }

            }

 

            // ----- 从文件读取音频/视频 -----

            function startFile(file) {

                try {

                    // 先停止之前的所有

                    stopAll();

 

                    if (!file) {

                        statusEl.innerHTML = '请选择一个文件';

                        return;

                    }

 

                    // 检查文件类型

                    if (!file.type.startsWith('audio/') && !file.type.startsWith('video/')) {

                        statusEl.innerHTML = '请选择音频或视频文件';

                        return;

                    }

 

                    const ctx = initAudioContext();

                    if (!ctx) return;

 

                    // 创建 URL

                    const url = URL.createObjectURL(file);

                    // 创建 audio 元素 (也支持视频, 但只取音频轨道)

                    const audioEl = document.createElement('audio');

                    audioEl.src = url;

                    audioEl.controls = false;

                    audioEl.autoplay = true;

                    // 确保音频可以播放

                    audioEl.load();

 

                    // 当元数据加载完成后连接

                    audioEl.onloadedmetadata = function() {

                        try {

                            const sourceNode = ctx.createMediaElementSource(audioEl);

                            sourceNode.connect(analyser);

                            // 同时也连接到 destination 才能听到声音

                            analyser.connect(ctx.destination);

                            currentSource = sourceNode;

 

                            // 显示文件名

                            fileNameEl.textContent = file.name;

 

                            statusEl.innerHTML = '<span class="highlight">●</span> 正在播放: ' + file.name;

                            startAnalysis();

                        } catch (err) {

                            statusEl.innerHTML = '无法连接音频: ' + err.message;

                            console.error(err);

                        }

                    };

 

                    audioEl.onerror = function() {

                        statusEl.innerHTML = '文件加载失败,请尝试其他格式';

                        btnMic.disabled = false;

                        btnStop.disabled = true;

                        fileInput.disabled = false;

                        URL.revokeObjectURL(url);

                    };

 

                    // 如果文件加载超时或失败, 清理

                    setTimeout(() => {

                        if (!isRunning) {

                            URL.revokeObjectURL(url);

                        }

                    }, 5000);

 

                } catch (err) {

                    statusEl.innerHTML = '处理文件出错: ' + err.message;

                    console.error(err);

                }

            }

 

            // ----- 事件绑定 -----

 

            // 麦克风按钮

            btnMic.addEventListener('click', startMicrophone);

 

            // 停止按钮

            btnStop.addEventListener('click', function() {

                stopAll();

                // 重置状态

                statusEl.innerHTML = '<span class="highlight">●</span> 已手动停止';

                btnMic.disabled = false;

                btnStop.disabled = true;

                fileInput.disabled = false;

                fileNameEl.textContent = '未选择文件';

            });

 

            // 文件选择

            fileInput.addEventListener('change', function(e) {

                const file = e.target.files[0];

                if (file) {

                    startFile(file);

                } else {

                    fileNameEl.textContent = '未选择文件';

                }

                // 重置 input 以便重复选择同一文件

                fileInput.value = '';

            });

 

            // 页面卸载时释放资源

            window.addEventListener('beforeunload', function() {

                stopAll();

            });

 

            // 处理用户点击页面时自动恢复音频上下文 (某些浏览器策略)

            document.addEventListener('click', function() {

                if (audioCtx && audioCtx.state === 'suspended') {

                    audioCtx.resume().catch(() => {});

                }

            }, { once: false });

 

            // 初始状态

            statusEl.innerHTML = '<span class="highlight">●</span> 就绪,点击“麦克风”或上传文件开始';

            btnStop.disabled = true;

 

        })();

    </script>

 

</body>

</html>

```

 

功能与使用说明

 

两种测量模式:

 

· 麦克风模式:点击后浏览器会请求麦克风权限,授权后即可实时测量周围环境的声音分贝。

· 文件模式:点击“选择音频/视频文件”上传本地文件,软件会自动播放并分析其音量。

 

实时反馈:

 

· 中央大数字显示当前分贝值,下方的彩色进度条提供直观的视觉参考。

· 分贝值经过平滑处理,数值变化更柔和、易读。

 

控制与状态:

 

· 点击 “停止” 按钮可随时结束测量并释放麦克风或音频资源。

· 底部的状态栏会清晰显示当前工作状态(如“测量中...”、“已停止”)。

 

注意事项:

 

· 首次使用麦克风时,请允许浏览器访问麦克风权限。

· 分贝值为相对值,用于反映音量变化趋势,并非专业校准的绝对声压级(SPL)。

· 建议在Chrome、Edge、Firefox等现代浏览器中使用。

更多推荐