Skip to content

Commit

Permalink
修复mp3码率小于64kbps时可能无声,顺带支持了mp3采样率可调
Browse files Browse the repository at this point in the history
  • Loading branch information
xiangyuecn committed Dec 5, 2018
1 parent 941d652 commit 8a1b2f3
Show file tree
Hide file tree
Showing 3 changed files with 82 additions and 55 deletions.
40 changes: 28 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ mp3默认16kbps的比特率,大概2kb每秒,如果使用8kbps可达到1kb每

*2018-09-19* [caniuse](https://caniuse.com/#search=getUserMedia) 注明IOS 12 Safari支持调用getUserMedia,经用户测试反馈IOS 12上chrome、UC都无法获取到,部分IOS 12 Safari可以获取到并且能正常录音,但部分不行,原因未知,参考[ios 12 支不支持录音了](https://www.v2ex.com/t/490695)

*2018-12-6* **【修复测试中】** [issues#1](https://github.com/xiangyuecn/Recorder/issues/1)不同OS上低码率mp3有可能无声,测试发现问题出在lamejs编码器有问题,此编码器本来就是精简版的,可能有地方魔改坏了,用lame测试没有这种问题。已对lamejs源码进行了改动,已通过基础测试,此问题未再次出现。

# 快速使用
在需要录音功能的页面引入js文件代码即可,*对于https的要求不做解释*
``` html
Expand All @@ -22,29 +24,34 @@ mp3默认16kbps的比特率,大概2kb每秒,如果使用8kbps可达到1kb每
var rec=Recorder();
rec.open(function(){//打开麦克风授权获得相关资源
rec.start();//开始录音
},function(msg){

setTimeout(function(){
rec.stop(function(blob){//到达指定条件停止录音,拿到blob对象想干嘛就干嘛:立即播放、上传
console.log(URL.createObjectURL(blob));
rec.close();//释放录音资源
},function(msg){
console.log("录音失败:"+msg);
});
},3000);
},function(msg){//未授权或不支持
console.log("无法录音:"+msg);
});
setTimeout(function(){
rec.stop(function(blob){//到达指定条件停止录音,拿到blob对象想干嘛就干嘛:立即播放、上传
console.log(URL.createObjectURL(blob));
rec.close();//释放录音资源
},function(msg){
console.log("录音失败:"+msg);
});
},3000);
```


# 方法文档

### rec=Recorder(set)

拿到`Recorder`的实例,然后可以进行请求获取麦克风权限和录音。

`set`参数为配置对象,默认配置值如下:
```
set={
type:"mp3" //输出类型:mp3,wav
,bitRate:16 //比特率 wav:16或8位,MP3:8比特1k/s,16比特2k/s 比较划得来
,bitRate:16 //比特率 wav:16或8位,MP3:8kbps 1k/s,16kbps 2k/s 录音文件很小
,sampleRate:16000 //采样率,wav专用
,sampleRate:16000 //采样率,wav格式大小=sampleRate*时间;mp3此项对低比特率有影响,高比特率几乎无影响。wav任意值,mp3取值范围:48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000
,bufferSize:8192 //AudioContext缓冲大小
//取值256, 512, 1024, 2048, 4096, 8192, or 16384,会影响onProcess调用速度
Expand All @@ -57,10 +64,14 @@ set={

### rec.open(success,fail)
请求打开录音资源,如果用户拒绝麦克风权限将会调用`fail`,打开后需要调用`close`

注意:此方法是异步的;一般使用时打开,用完立即关闭;可重复调用,可用来测试是否能录音。

`success`=fn();

`fail`=fn(errMsg);


### rec.close(success)
关闭释放录音资源,释放完成后会调用`success()`回调

Expand All @@ -69,11 +80,14 @@ set={

### rec.stop(success,fail)
结束录音并返回录音数据`blob对象`,拿到blob对象就可以为所欲为了,不限于立即播放、上传

`success(blob,duration)``blob`:录音数据audio/mp3|wav格式,`duration`:录音时长,单位毫秒

`fail(errMsg)`:录音出错回调

提示:stop时会进行音频编码,音频编码可能会很慢,10几秒录音花费2秒左右算是正常,编码并未使用Worker方案(文件多),内部采取的是分段编码+setTimeout来处理,界面卡顿不明显。


### rec.pause()
暂停录音。

Expand All @@ -90,11 +104,13 @@ lamejs的引用
# 缩小js文件
recorder.js用Uglify压缩一下剩余156kb,不算大

如果不需要mp3格式,可以把lamejs代码全部移除,recorder.js精简到300来行代码,仅仅支持wav格式;mp3编码采用的是`https://github.com/zhuker/lamejs/blob/bfb7f6c6d7877e0fe1ad9e72697a871676119a0e/lame.all.js`这个版本的代码。
如果不需要mp3格式,可以把lamejs代码全部移除,recorder.js精简到300来行代码,仅仅支持wav格式;mp3编码采用的是`https://github.com/zhuker/lamejs/blob/bfb7f6c6d7877e0fe1ad9e72697a871676119a0e/lame.all.js`这个版本的代码,已对lamejs源码进行了部分改动,用于修复发现的问题。


# 兼容性
对于支持录音的浏览器能够正常录音并返回录音数据;对于不支持的浏览器,引入此js和执行相关方法都不会产生异常,并且进入相关的fail回调。一般在open的时候就能检测到是否支持或者被用户拒绝,可在用户开始录音之前提示浏览器不支持录音或授权。


# 其他音频格式支持办法
``` javascript
//直接在源码中增加代码,比如增加ogg格式支持 (可参看内置的mp3实现)
Expand Down
26 changes: 20 additions & 6 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
比特率:<input type="text" class="bit" value="16">(kbps mp3) (wav取值8、16)
</div>
<div class="pd">
采样率:<input type="text" class="sample" value="16000">wav专用,mp3无效
采样率:<input type="text" class="sample" value="16000">(mp3标准值,wav任意)
</div>
<div class="pd">
<button onclick="recopen()">打开录音</button>
Expand All @@ -70,7 +70,7 @@
</div>
<div class="pd">
<button onclick="recstop2()">批量编码</button>
<input type="text" class="bits" value="8 to 96 step 8">kbps,测试音质用的(wav填了也只有8、16)
<input type="text" class="bits" value="8 to 96 step 8">kbps(wav固定8、16),测试音质用的,除比特率外其他参数可调整
</div>
<div class="reclog"></div>
<div class="recinfo"></div>
Expand Down Expand Up @@ -150,24 +150,34 @@
rec.stop(function(blob,time){
var id=RandomKey(16);
recblob[id]={blob:blob,set:$.extend({},rec.set),time:time};
reclog("已录制:编码耗时"+(Date.now()-t1)+"ms 比特率"+rec.set.bitRate+"kbps 文件大小"+blob.size+"b "+rec.set.type+"音频时长"+time+'ms <button class="Btn BtnMinMin" onclick="recdown(\''+id+'\')">下载</button> <button class="Btn BtnMinMin" onclick="recplay(\''+id+'\')">播放</button>');
reclog("已录制["+rec.set.type+"]"+intp(time,6)+"ms:"+intp(rec.set.bitRate,3)+"kbps "+intp(rec.set.sampleRate,5)+"hz 花"+intp(Date.now()-t1,4)+"ms编码"+intp(blob.size,6)+'b <button onclick="recdown(\''+id+'\')">下载</button> <button onclick="recplay(\''+id+'\')">播放</button> <span class="p'+id+'"></span> <span class="d'+id+'"></span>');
call&&call();
},function(s){
reclog("失败:"+s);
call&&call();
});
};
};
var intp=function(s,len){
return ("_______"+s).substr(-len);
};
function recstop2(){
if(!rec||!rec.buffer){
reclog("需先录个音");
return;
};

var type=$("[name=type]:checked").val();
var sample=+$(".sample").val();
var bits=/(\d+)\s+to\s+(\d+)\s+step\s+(\d+)\s*/i.exec($(".bits").val());
if(!bits){
reclog("码率列表有误,需要? to ? step ?结构");
return;
};

rec.set.type=type;
rec.set.sampleRate=sample;

var list=[];
for(var i=+bits[1];i<+bits[2]+1;i+=+bits[3]){
list.push(i);
Expand All @@ -176,6 +186,7 @@
list=[8,16];
};


var i=-1;
var bak=rec.set.bitRate;
var run=function(){
Expand All @@ -200,15 +211,18 @@
};
audio.src=URL.createObjectURL(o.blob);
audio.play();
reclog("已播放"+o.blob.size+"b "+o.time+'ms '+o.set.type+' '+o.set.bitRate+'kbps');
o.play=(o.play||0)+1;
$(".p"+key).html('<span style="color:green">'+o.play+'</span> '+new Date().toLocaleTimeString());
};
};
function recdown(key){
var o=recblob[key];
if(o){
var cls=RandomKey(16);
var name="rec-"+o.time+"ms."+o.set.type;
reclog('<span class="'+cls+'"> 没弹下载?试一下链接或复制文本<button class="Btn BtnMinMin" onclick="recdown64(\''+key+'\',\''+cls+'\')">生成Base64文本</button></span>');
var name="rec-"+o.time+"ms-"+o.set.bitRate+"kbps-"+o.set.sampleRate+"hz."+o.set.type;

o.down=(o.down||0)+1;
$(".d"+key).html('<span style="color:red">'+o.down+'</span> <span class="'+cls+'"> 没弹下载?试一下链接或复制文本<button onclick="recdown64(\''+key+'\',\''+cls+'\')">生成Base64文本</button></span>');

var downA=document.createElement("A");
downA.innerHTML="下载 "+name;
Expand Down
71 changes: 34 additions & 37 deletions recorder.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ var $={
return a;
}
};
//end兼容环境
//end1 兼容环境 ****从以下开始copy源码*****

function Recorder(set){
return new RecorderFn(set);
Expand All @@ -34,9 +34,9 @@ Recorder.IsOpen=function(){
function RecorderFn(set){
this.set=$.extend({
type:"mp3" //输出类型:mp3,wav,wav输出文件尺寸超大不推荐使用,但mp3编码支持会导致js文件超大,如果不需支持mp3可以使js文件大幅减小
,bitRate:16 //比特率 wav:16或8位,MP3:8比特1k/s,16比特2k/s 比较划得来
,bitRate:16 //比特率 wav:16或8位,MP3:8kbps 1k/s,8kbps 2k/s 录音文件很小

,sampleRate:16000 //采样率,wav专用;wav格式大小=sampleRate*时间;mp3此项无效,对文件大小无影响,直接使用源采样率
,sampleRate:16000 //采样率,wav格式大小=sampleRate*时间;mp3此项对低比特率有影响,高比特率几乎无影响。wav任意值,mp3取值范围:48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000
//采样率参考https://www.cnblogs.com/devin87/p/mp3-recorder.html

,bufferSize:8192 //AudioContext缓冲大小,相对于ctx.sampleRate=48000/秒:
Expand Down Expand Up @@ -205,15 +205,31 @@ RecorderFn.prototype={
return;
};

var sampleRate=set.sampleRate
,ctxSampleRate=Recorder.Ctx.sampleRate;
//采样 https://www.cnblogs.com/blqw/p/3782420.html
var step=ctxSampleRate/sampleRate;
if(step>1){//新采样高于录音采样不处理,省去了插值处理,直接抽样
size=Math.floor(size/step);
}else{
step=1;
sampleRate=ctxSampleRate;
set.sampleRate=sampleRate;
};
//准备数据
var res=new Int16Array(size);
var offset=0;
for (var i=0;i<This.buffer.length;i++) {
var o=This.buffer[i];
res.set(o,offset);
offset+=o.length;
}
var duration=Math.round(size/Recorder.Ctx.sampleRate*1000);
var last=0,idx=0;
for (var n=0,nl=This.buffer.length;n<nl;n++) {
var o=This.buffer[n];
var i=last,il=o.length;
while(i<il){
res[idx]=o[Math.round(i)];
idx++;
i+=step;//抽样
};
last=i-il;
};
var duration=Math.round(size/sampleRate*1000);

setTimeout(function(){
var t1=Date.now();
Expand All @@ -225,28 +241,10 @@ RecorderFn.prototype={
}
,wav:function(res,call){
var This=this,set=This.set
,size=This.recSize
,size=res.length
,sampleRate=set.sampleRate
,ctxSampleRate=Recorder.Ctx.sampleRate
,bitRate=set.bitRate==8?8:16;

//采样 https://www.cnblogs.com/blqw/p/3782420.html
var compression=Math.round(ctxSampleRate/sampleRate);
if(compression>1){//新采样高于录音采样不处理,省去了插值处理,直接抽样
size=Math.floor(size/compression);
var compRes=new Int16Array(size);
var i=0,j=0;
while(i<size){
compRes[i]=res[j];
j+=compression;
i++;
};
res=compRes;
}else{
sampleRate=ctxSampleRate;
};


//编码数据 https://github.com/mattdiamond/Recorderjs https://www.cnblogs.com/blqw/p/3782420.html https://www.cnblogs.com/xiaoqi/p/6993912.html
var dataLength=size*(bitRate/8);
var buffer=new ArrayBuffer(44+dataLength);
Expand Down Expand Up @@ -313,7 +311,7 @@ RecorderFn.prototype={
var This=this,set=This.set,size=res.length;
//https://github.com/wangpengfei15975/recorder.js
//https://github.com/zhuker/lamejs bug:采样率必须和源一致,不然8k时没有声音,有问题fix:https://github.com/zhuker/lamejs/pull/11
var mp3=new lamejs.Mp3Encoder(1,Recorder.Ctx.sampleRate,set.bitRate);
var mp3=new lamejs.Mp3Encoder(1,set.sampleRate,set.bitRate);

var blockSize=5760;
var data=[];
Expand Down Expand Up @@ -342,13 +340,8 @@ RecorderFn.prototype={

window.Recorder=Recorder;








//end1 ****copy源码结束*****
//end2 ****开始copy lamejs*****

/*
mp3编码依赖lamejs,如果无需mp3支持直接移除此代码
Expand Down Expand Up @@ -15782,6 +15775,7 @@ function Mp3Encoder(channels, samplerate, kbps) {

gfp.num_channels = channels;
gfp.in_samplerate = samplerate;
gfp.out_samplerate = samplerate;//fix by xiangyuecn 2018-12-6 01:48:12 64kbps以下可能无声音,手动控制输出码率
gfp.brate = kbps;
gfp.mode = MPEGMode.STEREO;
gfp.quality = 3;
Expand Down Expand Up @@ -15881,4 +15875,7 @@ lamejs();


Recorder.lamejs=lamejs;

//end3 ****结束copy lamejs*****

})(window);

0 comments on commit 8a1b2f3

Please sign in to comment.