辛苦整理,请您珍惜
分贝(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等现代浏览器中使用。