Python测量音视频相对音量
辛苦整理请您珍惜分贝dB为单位显示音量。html!DOCTYPE htmlhtml langzh-CNheadmeta charsetUTF-8 /meta nameviewport contentwidthdevice-width, initial-scale1.0 /title实时分贝测量仪/titlestyle/* ----- 全局样式 ----- */* {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[typefile] {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/headbodydiv classcardh1分贝测量仪/h1div classsub实时音频 · 麦克风 / 文件/div!-- 分贝数值 --div classdb-displaydiv classdb-numberspan iddbValue--/spanspan classdb-unitdB/span/divdiv classdb-label声压级 (SPL)/div/div!-- 音量条 --div classmeter-wrapdiv classmeter-fill idmeterFill stylewidth:0%;/div/div!-- 控制按钮 --div classcontrolsbutton classbtn primary idbtnMic麦克风/buttonbutton classbtn idbtnStop disabled停止/button/div!-- 文件上传 --div classfile-arealabel foraudioFile选择音频 / 视频文件/labelinput typefile idaudioFile acceptaudio/*,video/* /div classfile-name idfileName未选择文件/div/div!-- 状态 --div classstatus idstatusDisplayspan classhighlight●/span 就绪点击“麦克风”或上传文件开始/div/divscript(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 | MediaElementAudioSourceNodelet 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..1const 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 到 100let 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;// 重置 UIdbSpan.textContent --;meterFill.style.width 0%;dbSpan.style.color #b7c9ff;statusEl.innerHTML span classhighlight●/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 classhighlight●/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;// 创建 MediaStreamAudioSourceNodeconst sourceNode ctx.createMediaStreamSource(stream);sourceNode.connect(analyser);// 不连接 destination 以免产生反馈啸叫 (仅分析用)statusEl.innerHTML span classhighlight●/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;// 创建 URLconst 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 classhighlight●/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 classhighlight●/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 classhighlight●/span 就绪点击“麦克风”或上传文件开始;btnStop.disabled true;})();/script/body/html功能与使用说明两种测量模式· 麦克风模式点击后浏览器会请求麦克风权限授权后即可实时测量周围环境的声音分贝。· 文件模式点击“选择音频/视频文件”上传本地文件软件会自动播放并分析其音量。实时反馈· 中央大数字显示当前分贝值下方的彩色进度条提供直观的视觉参考。· 分贝值经过平滑处理数值变化更柔和、易读。控制与状态· 点击 “停止” 按钮可随时结束测量并释放麦克风或音频资源。· 底部的状态栏会清晰显示当前工作状态如“测量中...”、“已停止”。注意事项· 首次使用麦克风时请允许浏览器访问麦克风权限。· 分贝值为相对值用于反映音量变化趋势并非专业校准的绝对声压级SPL。· 建议在Chrome、Edge、Firefox等现代浏览器中使用。