当前位置: 首页 > news >正文

Python测量音视频相对音量

辛苦整理,请您珍惜

分贝(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等现代浏览器中使用。

http://www.jsqmd.com/news/1104128/

相关文章:

  • 按需上门率99%!申通这家五星网点凭“电商基因”突围苏北
  • 最新Nessus安装激活步骤202606096147,超详细简单,附激活文件Windows/Linux安装包
  • 2026大理黄金回收白银回收铂金回收旧料回收怎么选?五家高实价铂金白银线下门店测评清单 + 联系方式
  • wvp-GB28181-pro:5分钟构建专业级国标视频监控平台的技术架构与实践指南
  • Vibe Coding 避坑指南:3 张提示词模板,把烂尾率从 80% 打下来
  • MC6470与TM4C1299NCZAD的硬件协同与6DOF数据融合实战
  • 高分Panel复现系列|非负矩阵热图:从矩阵数据到分块注释热图
  • 20个终极Obsidian模板:快速构建高效卡片盒笔记系统
  • 企业 Skill 市场架构设计:模块注册、发现、热加载与分级治理方案
  • [Android] MemCull v1.8.1照片清理工具
  • 2026大兴安岭黄金回收白银回收铂金回收旧料回收怎么选?五家高实价铂金白银线下门店测评清单 + 联系方式
  • KeyStore Explorer解决方案:Java密钥库管理的现代化图形界面深度解析
  • 跨运营商访问卡顿,用TCPing精准定位瓶颈节点
  • Resource 体系纵深实战:构建动态模板化代码片段的资源服务器
  • 第十二章:完整的 DevOps 流水线案例:Spring Boot + Docker + K8s + GitLab CI
  • 传输层双模对决:Stdio 与 Streamable HTTP 部署方案性能对比及选型依据
  • 十五年的“冷板凳”:昆仑芯IPO是对百度长期主义的最好回报
  • 2026滁州黄金回收白银回收铂金回收旧料回收怎么选?五家高实价铂金白银线下门店测评清单 + 联系方式
  • 为什么你的IDEA多模块项目永远跑不通?揭秘被官方文档隐藏的6个IDEA专属Maven生命周期陷阱
  • 美国公司弃 Claude 选 DeepSeek:成本降了,性能还提升了!
  • Windows 11终极优化指南:使用Win11Debloat实现51%系统性能提升的完整方案
  • Momenta港股招股:营收三年翻三倍,65%市占率能否成物理AI时代定义者?
  • 内部知识库 RAG Skill:构建文档 MCP Server 实现技术问答零延迟
  • Go+DeepSeek-V3构建企业级代码审计系统
  • Windows 11任务栏逆向工程:Taskbar11深度技术解密与高级定制指南
  • 高分Panel复现系列|三元突变比例图:从三组比例到三角坐标映射
  • 2026年食品行业PLM系统实施路径:从需求梳理到平台落地的关键步骤
  • KMR221与PIC18F86J55高精度电压监测系统设计
  • 抖音内容下载终极指南:5分钟掌握批量下载与音频提取技巧
  • 基于TB9051FTG与PIC18F的静音直流电机控制方案