项目背景
目前 Web 的 TRTC 没有静音检测,在关闭麦克风的情况下发言没有提示,有时候会有比较尴尬的会议场景出现,为提升用户体验,这里尝试将腾讯会议的解决思路引入。
当用户在关闭麦克风的情况下,如果周围声音超过一定的分贝值,则显示提示。
目前可兼容主流PC浏览器下的所有框架,electron框架,为更好接入TRTC Web Demo 采用jquery库开发(主要是交互与样式),稍微改动即可适配原生与其他框架。
这份代码直接在TRTC的Demo中写一份js文件引入即可,即插即用。如果有需要自定义的部分,我也在代码里写了注释。
使用方法:
注:该功能需要在服务端才可使用,调试请使用Live Server。
效果展示:
源代码
说起来很简单,实际做起来有些难度,根本上是需要用js去加工音频流才行,用到了一个很旧的接口audioContext以及createMediaStreamSource相关的方法才得以实现,这里直接贴结果代码。
代码语言:txt复制var audioContext = null;
var meter = null;
// 如果你需要对声音大小可视化,可以将canvasContext解除注释,并在html里写一个canvas元素供给后续获取
// var canvasContext = null;
var mediaStreamSource = null;
var WIDTH = 500;
var HEIGHT = 50;
var rafID = null;
var hintlock = false;
// jquery,如果需兼容其他框架,可以改用原生
$('body').append(` <div id="michint" class="alert alert-warning">
<strong>注意!</strong>检测到您说话但没有打开麦克风!。
</div>`)
$('#michint').slideUp(0)
getStream();
function getStream() {
// 获取canvas,若想要可视化音量,在html中增加一个高度50的,宽度500的canvas,id为meter即可,也可自行修改参数
// canvasContext = document.getElementById("meter").getContext("2d");
window.AudioContext = window.AudioContext || window.webkitAudioContext;
try {
navigator.getUserMedia =
navigator.getUserMedia ||
navigator.webkitGetUserMedia ||
navigator.mozGetUserMedia;
navigator.getUserMedia(
{
"audio": {
"mandatory": {
"googEchoCancellation": "false",
"googAutoGainControl": "false",
"googNoiseSuppression": "false",
"googHighpassFilter": "false"
},
"optional": []
},
}, gotStream, didntGetStream);
} catch (e) {
alert('getUserMedia threw exception :' e);
}
}
function didntGetStream() {
alert('Stream generation failed.');
}
function gotStream(stream) {
audioContext = new AudioContext();
meter = createAudioMeter(audioContext);
mediaStreamSource = audioContext.createMediaStreamSource(stream);
mediaStreamSource.connect(meter);
drawLoop();
}
function drawLoop() {
// isMicOn是demo的判定本地流是否关闭了麦克风,如有其他状态变量判断,可以自行添加。音量大小判定边界也可自行修改
// jquery提供的动效,以及防抖限制的时间都可自行修改。
if (hintlock == false && meter.volume > 0.05 && isMicOn === false) {
hintlock = true;
$('#michint').slideDown(600);
setTimeout(function () {
$("#michint").slideUp(600)
hintlock = false;
}, 3000)
}
if (typeof (canvasContext) != "undefined") {
canvasContext.clearRect(0, 0, WIDTH, HEIGHT);
canvasContext.fillStyle = "green";
canvasContext.fillRect(0, 0, meter.volume * WIDTH * 1.4, HEIGHT);
}
rafID = window.requestAnimationFrame(drawLoop);
}
function createAudioMeter(audioContext, clipLevel, averaging, clipLag) {
var processor = audioContext.createScriptProcessor(512);
processor.onaudioprocess = volumeAudioProcess;
/*用法:
audioNode = createAudioMeter (audioContext clipLevel,averaging,clipLag);你正在使用的audioContext。
clipLevel:你会考虑“剪切”的级别(0到1)。默认为0.98。
averaging:你希望仪表随着时间的推移变得多“平滑”。应该在0和小于1之间。默认为0.95。
clipLag:你希望“剪辑”指示器显示多长时间,剪切发生后,以毫秒为单位。默认为750 ms。
通过node.checkClipping()访问剪辑;使用节点。关闭以接触。
*/
processor.clipping = false;
processor.lastClip = 0;
processor.volume = 0;
processor.clipLevel = clipLevel || 0.98;
processor.averaging = averaging || 0.95;
processor.clipLag = clipLag || 750;
processor.connect(audioContext.destination);
//防抖,控制采样速度,否则音量前后会叠加。
processor.checkClipping =
function () {
if (!this.clipping)
return false;
if ((this.lastClip this.clipLag) < window.performance.now())
this.clipping = false;
return this.clipping;
};
//关闭时
processor.shutdown =
function () {
this.disconnect();
this.onaudioprocess = null;
};
return processor;
}
function volumeAudioProcess(event) {
//音量计算
var buf = event.inputBuffer.getChannelData(0);
var bufLength = buf.length;
var sum = 0;
var x;
for (var i = 0; i < bufLength; i ) {
x = buf[i];
if (Math.abs(x) >= this.clipLevel) {
this.clipping = true;
this.lastClip = window.performance.now();
}
sum = x * x;
}
//对样本做一个均方根:把平方加起来……
var rms = Math.sqrt(sum / bufLength);
//现在用对前一个样本施加的平均因子来平滑这一点——这里取最大值,因为我们想要“快速增加,缓慢下降”,这样的取值更符合显示,因为现实存在回声,音量不会下降的很快。
this.volume = Math.max(rms, this.volume * this.averaging);
}
核心代码讲解:
- audioContext:这是一个用于接收音频上下文的对象,是 AudioContext 的实例,这个接口在很早以前是配合audio 标签一起使用的,可以在js层面操作audio的各种功能。但后来 audio 标签逐渐废弃,但该接口依然保留,可以获取到音频实例。包括音乐文件与麦克风,不过麦克风需要做额外处理。
- navigator.getUserMedia:熟悉 webRTC 的开发者都知道这个用于获取设备的流,chrome的话需要先赐予权限才可获取。成功执行 gotStream并获得媒体流,失败执行 didntGetStream image.png
- gotStream 分几步 1. 使用 audioContext 中自带的 createMediaStreamSource 方法,将从getMic中得到的上下文和getUserMedia拿到的流,再创建节点, 2. 执行 createAudioMeter,用音频上下文audioContext,创建 meter(计量表)对象 3. 将 mediaStreamSource 连接至 meter 中 audioContext; 4. 用volumeAudioProcess函数,来处理缓冲区内的音频数据即可得到音量值 }
- createAudioMeter
//该段代码用于创建audio的缓冲区
function createAudioMeter(audioContext, clipLevel, averaging, clipLag) {
var processor = audioContext.createScriptProcessor(512);
processor.onaudioprocess = volumeAudioProcess;
/*用法:
audioNode = createAudioMeter (audioContext clipLevel,averaging,clipLag);你正在使用的audioContext。
clipLevel:你会考虑“剪切”的级别(0到1)。默认为0.98。
averaging:你希望仪表随着时间的推移变得多“平滑”。应该在0和小于1之间。默认为0.95。
clipLag:你希望“剪辑”指示器显示多长时间,剪切发生后,以毫秒为单位。默认为750 ms。
通过node.checkClipping()访问剪辑;使用节点。关闭以接触。
*/
processor.clipping = false;
processor.lastClip = 0;
processor.volume = 0;
processor.clipLevel = clipLevel || 0.98;
processor.averaging = averaging || 0.95;
processor.clipLag = clipLag || 750;
processor.connect(audioContext.destination);
//防抖,控制采样速度,否则音量前后会叠加。
processor.checkClipping =
function () {
if (!this.clipping)
return false;
if ((this.lastClip this.clipLag) < window.performance.now())
this.clipping = false;
return this.clipping;
};
//关闭时
processor.shutdown =
function () {
this.disconnect();
this.onaudioprocess = null;
};
return processor;
}
- volumeAudioProcess
function volumeAudioProcess(event) {
//音量计算
var buf = event.inputBuffer.getChannelData(0);
var bufLength = buf.length;
var sum = 0;
var x;
for (var i = 0; i < bufLength; i ) {
x = buf[i];
if (Math.abs(x) >= this.clipLevel) {
this.clipping = true;
this.lastClip = window.performance.now();
}
sum = x * x;
}
//对样本做一个均方根:把平方加起来……
var rms = Math.sqrt(sum / bufLength);
//现在用对前一个样本施加的平均因子来平滑这一点——这里取最大值,因为我们想要“快速增加,缓慢下降”,这样的取值更符合显示,因为现实存在回声,音量不会下降的很快。
this.volume = Math.max(rms, this.volume * this.averaging);
}