2 Star 1 Fork 131

我要一杯白开水/Recorder

forked from xiangyuecn/Recorder 
加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
文件
克隆/下载
index.html 60.14 KB
一键复制 编辑 原始数据 按行查看 历史
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748
<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
<link rel="shortcut icon" type="image/png" href="assets/icon.png">
<title>Recorder H5: 用于html5网页中的前端录音解决方案,此录音插件支持mp3 wav pcm amr ogg webm格式,支持实时上传 语音识别 音频可视化 实时处理,可在PC端 移动端 Android iOS 原生App中跨平台使用</title>
<script>
console.log(
["%c "
," "
," "
," %c REC WARNING %c "
," "
," "
,"%c 请不要试图阅读本demo的源码,不然你会去想这么丑的代码是谁写的 "
," 本文件包括附属的js文件的代码是经过长时间积累出来的 "
," 代码虽然已经分层/分开写了,也许者作者也已经不认识了 "
," "
," 如果想要快速入门,请阅读github项目内首页README文档 "
," 参考文档内的快速使用部分,简单快捷高效 "
," "
," 请不要试图阅读本demo的源码,正常情况下意义不大 "
," "
," "
,""].join("\n"),
'background: #000; font-size: 18px; line-height:1; font-family: monospace',
'background: #f33; font-size: 18px; line-height:1; font-family: monospace; color: #eee; text-shadow:0 0 1px #fff',
'background: #000; font-size: 18px; line-height:1; font-family: monospace',
'background: #000; font-size: 18px; line-height:1; font-family: monospace; color: #ddd; text-shadow:0 0 2px #fff'
)
</script>
</head>
<body>
<div class="main">
<!--加载核心库,其他类型支持库在下面根据用户点击选择加载-->
<script src="src/recorder-core.js"></script>
<!--加载可选扩展库-->
<script src="src/extensions/waveview.js"></script>
<script src="src/extensions/wavesurfer.view.js"></script>
<script src="src/extensions/lib.fft.js"></script>
<script src="src/extensions/frequency.histogram.view.js"></script>
<script src="src/extensions/sonic.js"></script>
<script src="src/extensions/dtmf.encode.js"></script>
<script src="src/extensions/dtmf.decode.js"></script>
<!--加载PlayBuffer-->
<script src="assets/runtime-codes/fragment.playbuffer.js"></script>
<style>
body{
word-wrap: break-word;
word-break: break-all;
background:#f5f5f5 center top no-repeat;
background-size: auto 680px;
}
pre{
white-space:pre-wrap;
}
label,label *{
cursor: pointer;
}
label:hover{
color:#06c;
}
a{
text-decoration: none;
color:#06c;
}
a:hover{
color:#f00;
}
.main{
max-width:700px;
margin:0 auto;
padding-bottom:80px
}
.mainBox{
margin-top:12px;
padding: 12px;
border-radius: 6px;
background: #fff;
--border: 1px solid #f60;
box-shadow: 2px 2px 3px #aaa;
}
.btns button,.mainBtn{
display: inline-block;
cursor: pointer;
border: none;
border-radius: 3px;
background: #f60;
color:#fff;
padding: 0 15px;
margin:3px 20px 3px 0;
line-height: 36px;
height: 36px;
overflow: hidden;
vertical-align: middle;
}
.btns button:active,.mainBtn:active{
background: #f00;
}
.recwaveChoice{
cursor: pointer;
display:inline-block;
vertical-align: bottom;
border-right:1px solid #ccc;
background:#ddd;
line-height:28px;
font-size:12px;
color:#666;
padding:0 5px;
}
.recwaveChoice:first-child{
border-radius: 99px 0 0 99px;
}
.recwaveChoice:last-child{
border-radius: 0 99px 99px 0;
border-right:none;
}
.recwaveChoice.slc,.recwaveChoice:hover{
background:#f60;
color:#fff;
}
.lb{
display:inline-block;
vertical-align: middle;
background:#00940e;
color:#fff;
font-size:14px;
padding:2px 8px;
border-radius: 99px;
}
.pd{
padding:0 0 6px 0;
}
</style>
<script>
//兼容环境
var PageLM="2022-08-06 21:36";
function RandomKey(){
return "randomkey"+(RandomKey.idx++);
};
RandomKey.idx=0;
//乐此不疲,古董浏览器,仅保证基本的可执行不代码异常
if(!Date.now)Date.now=function(){return new Date().getTime()};
if(!window.localStorage){window.localStorage={
loadMinJs:/loadMinJs/.test(location.href)?"1":"0"
}};
</script>
<script src="assets/ztest-jquery.min-1.9.1.js"></script>
<div class="demoHead mainBox">
<style>
.navItem{
display:inline-block;
width:45%;
max-width:300px;
vertical-align: top;
background:#eee;
border-bottom: 5px solid #ccc;
box-shadow: 2px 2px 3px #ddd;
color:#666;
text-decoration:none;
border-radius: 8px;
padding: 0 5px 3px;
}
.navItem.slc{
border-bottom: 5px solid #00940e;
color:#f60;
}
.navItem:hover{
color:#d44;
}
.navTitle{
text-align: center;
font-size:18px;
font-weight: bold;
}
.navItem.slc .navDesc{
color:#00940e;
}
.navDesc{
font-size:12px;
}
</style>
<a class="navItem slc" style="margin-right:2%;" href="https://xiangyuecn.gitee.io/recorder/">
<div class="navTitle">Recorder H5</div>
<div class="navDesc">Recorder H5使用简单,功能丰富,支持PC、Android、iOS 14.3+</div>
</a>
<a class="navItem" href="https://jiebian.life/web/h5/github/recordapp.aspx">
<div class="navTitle" style="text-decoration: line-through">RecordApp</div>
<div class="navDesc">[即将废弃]支持:Recorder + Hybrid App + 低版本iOS上的微信</div>
</a>
<div style="margin-top:8px">
<span class="lb">源码仓库 :</span>
<a href="https://github.com/xiangyuecn/Recorder" target="_blank">GitHub</a>
| <a href="https://gitee.com/xiangyuecn/Recorder" target="_blank">Gitee</a>
<span class="lb">更多Demo :</span> <a href="https://xiangyuecn.gitee.io/recorder/assets/工具-代码运行和静态分发Runtime.html" target="_blank">Demo列表(可编辑)</a>
| <a href="https://xiangyuecn.gitee.io/recorder/assets/demo-vue/" target="_blank">H5 vue</a>
| <a href="https://xiangyuecn.gitee.io/recorder/assets/demo-ts/" target="_blank">H5 ts</a>
<div style="margin-top:6px;">
<span class="lb">QuickStart :</span>
<a href="https://xiangyuecn.gitee.io/recorder/QuickStart.html" target="_blank">QuickStart.html</a>
<span style="font-size:12px;color:#999">(Copy即用,更适合入门学习)</span>
<span class="lb">老版本 :</span> <a href="https://xiangyuecn.gitee.io/recorder/assets/工具-GitHub页面历史版本访问.html#url=xiangyuecn:Recorder@1.0.19120600,/" target="_blank">切换到老版本测试</a>
</div>
</div>
</div>
<!-- begin 开始copy源码 -->
<div class="demoMain">
<div class="mainBox">
<div class="pd">
<span class="lb">类型 :</span> <span class="types">
<label><input type="radio" name="type" value="mp3" engine="mp3,mp3-engine" min="/recorder.mp3.min" class="initType" checked>mp3</label>
<label><input type="radio" name="type" value="wav" engine="wav" min="/recorder.wav.min">wav</label>
<label><input type="radio" name="type" value="pcm" engine="pcm" min=",pcm" addload="wav">pcm</label>
<label><input type="radio" name="type" value="ogg" engine="beta-ogg,beta-ogg-engine" min=",beta-ogg">ogg(beta)</label>
<label><input type="radio" name="type" value="webm" engine="beta-webm" min=",beta-webm">webm(beta)</label>
<label><input type="radio" name="type" value="amr" engine="beta-amr,beta-amr-engine" min=",beta-amr" addload="wav">amr(beta)</label>
</span>
<label><input type="checkbox" class="loadMinJs">请求压缩版</label>
</div>
<div class="pd">
<span class="lb">提示 :</span> <span class="typeTips">-</span>
</div>
<div class="pd">
<span class="lb">比特率 :</span> <input type="text" class="bit" value="16" style="width:60px"> kbps,越大音质越好
</div>
<div>
<span class="lb">采样率 :</span> <input type="text" class="sample" value="16000" style="width:60px"> hz,越大细节越丰富
</div>
</div>
<div class="mainBox">
<div class="pd btns">
<div>
<button onclick="recopen()" style="margin-right:10px">打开录音,请求权限</button>
<button onclick="recclose()" style="margin-right:0">关闭录音,释放资源</button>
</div>
<button onclick="recstart()">录制</button>
<button onclick="recstop()" style="margin-right:80px">停止</button>
<span style="display: inline-block;">
<button onclick="recpause()">暂停</button>
<button onclick="recresume()">继续</button>
</span>
<span style="display: inline-block;">
<button onclick="recPlayLast()">播放</button>
<button onclick="recUploadLast()">上传</button>
</span>
</div>
<div class="pd recpower">
<div style="height:40px;width:300px;background:#999;position:relative;">
<div class="recpowerx" style="height:40px;background:#0B1;position:absolute;"></div>
<div class="recpowert" style="padding-left:50px; line-height:40px; position: relative;"></div>
</div>
</div>
<div class="pd">
<button onclick="recstop2()" class="batEnc">批量编码</button>
<input type="text" class="bits" value="8 to 96 step 8">kbps 测试音质用的,除比特率外其他参数可调整
</div>
<div class="pd waveBox">
<div style="border:1px solid #ccc;display:inline-block"><div style="height:100px;width:300px;" class="recwave"></div></div>
<span style="font-size:0">
<span class="recwaveChoice" key="WaveView">WaveView</span>
<span class="recwaveChoice" key="SurferView">SurferView</span>
<span class="recwaveChoice" key="Histogram1">Histogram1</span>
<span class="recwaveChoice" key="Histogram2">H...2</span>
<span class="recwaveChoice" key="Histogram3">H...3</span>
</span>
</div>
<div class="pd pcmPageHide">
<label><input type="checkbox" class="autoStopSet">开始录制后定时</label>
<input type="text" class="autoStopTime" value="60000" style="width:60px">ms自动停止录音,定时录音
</div>
<div class="pd pcmPageHide">
<label><input type="checkbox" class="takeoffEncodeChunkSet">接管编码器输出(takeoffEncodeChunk),切换后新打开录音生效</label>
</div>
<div class="pd pcmPageHide">
<label><input type="checkbox" class="realTimeSendSet">模拟实时编码传输(H5版语音通话聊天)</label>
,发送间隔<input type="text" class="realTimeSend" value="996" style="width:60px">ms
<div class="webrtcView" style="display:none;"></div>
</div>
<div class="pcmPageHide">
<label><input type="checkbox" class="asrSet">实时语音识别、音频文件转文字,ASR</label>
<div class="asrView" style="display:none;"></div>
</div>
</div>
<div class="mainBox">
<audio class="recPlay" controls style="display:none;width:100%"></audio>
<div class="reclog"></div>
</div>
<div class="mainBox">
<div>
<span class="lb">切换麦克风 :</span>
<select class="trackSet_device"></select>
从未请求过录音权限时,可能无法正常拉取设备列表,打开一次录音后可尝试
<button onclick="trackSetQueryDeviceList(1)">重新拉取设备</button>
</div>
<div>
<span class="lb">noiseSuppression :</span>
<select class="trackSet_noise">
<option value="1">true</option>
<option value="2" selected>false</option>
</select>
降噪配置开关(默认禁用)
</div>
<div>
<span class="lb">echoCancellation :</span>
<select class="trackSet_aec">
<option value="1">true</option>
<option value="2" selected>false</option>
</select>
回声消除配置开关(默认禁用)
</div>
<div>
<span class="lb">autoGainControl :</span>
<select class="trackSet_gain">
<option value="">不设置</option>
<option value="1">true</option>
<option value="2">false</option>
</select>
自动增益配置开关
</div>
<div>
<span style="color:#f60">以上参数设置后需重新打开录音</span>;均为set中的audioTrackSet高级配置,会直接传递给浏览器的getUserMedia方法,不同浏览器的支持程度不同,并不一定会生效;
<span style="color:#aaa">移动端打开降噪、回声消除可能会表现的很怪异(包括系统播放声音变小),所以默认禁用,使用原声录制(高音甜、中音准、低音沉,总之一句话就是通透),如需降噪、回声消除请测试好后再开启</span>
</div>
</div>
<div class="mainBox">
<span class="lb">启用MediaRecorder :</span>
<label>
<input type="checkbox" class="enableWebMSet">启用
<span style="color:#f60">设置后需重新打开录音</span>
</label>
<div>音频采集连接方式:启用时尝试使用MediaRecorder.WebM.PCM,默认启用,未启用或者不支持时使用AudioWorklet或ScriptProcessor;通过设置Recorder.ConnectEnableWebM=false禁用。</div>
<div>
<span style="color:#0b1">使用MediaRecorder采集到的音频数据比其他方式更好,几乎不存在丢帧现象,所以音质明显会好很多,建议保持开启;</span>
<span style="color:#aaa">有些浏览器不支持录制PCM编码的WebM,如FireFox、低版本的Chrome,将依旧使用AudioWorklet或ScriptProcessor来连接采集。</span>
</div>
<div style="margin-top:12px;"></div>
<span class="lb">启用AudioWorklet :</span>
<label>
<input type="checkbox" class="enableWorkletSet">启用
<span style="color:#f60">设置后需重新打开录音,ConnectEnableWebM如果启用并且有效时,本参数将不起作用</span>
</label>
<div>音频采集连接方式:启用时尝试使用AudioWorklet,默认禁用,未启用或者不支持时使用ScriptProcessor;通过设置Recorder.ConnectEnableWorklet=true启用。</div>
<div>
已知:AudioWorklet在一定条件下会导致某些浏览器崩溃
<a href="assets/ztest_chrome_bug_AudioWorkletNode.html" target="_blank">测试</a>
(坑已填好)。
</div>
<div class="workletSetTips" style="color:#f60"></div>
</div>
<div class="mainBox">
<div style="color:#0ab;font-size:22px;font-weight:bold">
如需录音功能定制开发,网站、App、小程序、前端后端开发等需求,请加QQ群:①群 781036591、②群 748359095,口令recorder,联系群主(即作者),谢谢~
</div>
</div>
<div class="mainBox">
<div>
<span class="lb">变速变调 :</span>
<button onclick="resetSonicCtrl()">重置变速变调</button>
实时变速变调控制选项(Sonic 插件),可以边录边修改,同一时间应该只控制一个,否则叠加作用;请填写0.1-2.0的数字,1.0为不调整,当然超过2.0也是可以的(需手动输入)
</div>
<div class="sonicCtrlBox" style="margin:5px 0 0;position: relative;">
<style>
.sonicCtrlBox span{display:inline-block;width:80px;text-align:right;}
.sonicCtrlBox input{text-align:right;}
</style>
<div><span>Pitch:</span><input class="sonicCtrlInput sonicCtrlPitch" style="width:60px"> 男声<input type="range" class="sonicCtrlRange" min="0.1" max="2" step="0.1" value="1.0">女声,变调不变速(会说话的汤姆猫)</div>
<div><span>Speed:</span><input class="sonicCtrlInput sonicCtrlSpeed" style="width:60px"> 慢放<input type="range" class="sonicCtrlRange" min="0.1" max="2" step="0.1" value="1.0">快放,变速不变调(快放慢放),由于会增减PCM数据,实时处理时本功能需要禁用丢失补偿</div>
<div><span>Rate:</span><input class="sonicCtrlInput sonicCtrlRate" style="width:60px"> 缓重<input type="range" class="sonicCtrlRange" min="0.1" max="2" step="0.1" value="1.0">尖锐,变速变调,由于会增减PCM数据,实时处理时本功能需要禁用丢失补偿</div>
<div><span>Volume:</span><input class="sonicCtrlInput sonicCtrlVolume" style="width:60px"> 调低<input type="range" class="sonicCtrlRange" min="0.1" max="2" step="0.1" value="1.0">调高,调整音量</div>
<div style="border-top: 1px solid #eee;margin-top: 10px;"><span>处理缓冲:</span><input class="sonicCtrlInput sonicCtrlBuffer" style="width:60px">ms 0ms<input type="range" class="sonicCtrlRange sonicCtrlBufferRange" min="0" max="1000" step="100" value="200">1000ms,控制缓冲大小减少转换引入的杂音,0不缓冲</div>
<div><span>播放反馈:</span><input class="sonicCtrlInput sonicCtrlPlay" style="width:60px"> 不播放 <input type="range" class="sonicCtrlRange" min="0" max="1" step="1" value="1">实时播放反馈</div>
<div style="margin-top:10px"><button onclick="sonicRecTransform()">重新转换当前录音</button></div>
<div class="sonicCtrlBoxMask" style="position:absolute;top:0;left:0;width:100%;height:100%;background: rgba(0,0,0,.2);text-align: center;">
<div style="padding-top:50px;font-size: 30px;color: #fff;">挡住!防止滑块被误触</div>
<div><button onclick="sonicCtrlBoxMaskClose()">打开编辑</button></div>
</div>
</div>
</div>
<div class="mainBox">
<span class="lb">丢失补偿 :</span>
<label>
<input type="checkbox" class="disableEnvInFixSet">禁用设备卡顿时音频输入丢失补偿功能(通过别的程序大量占用CPU来模拟设备卡顿)
<span style="color:#f60">设置后需重新打开录音</span>
;为set中disableEnvInFix配置值
</label>
<div><a href="https://github.com/xiangyuecn/Recorder/issues/51">issues#51</a>如果没有进行补偿,录音时设备偶尔出现很卡的情况下(CPU被其他程序大量占用),浏览器采集到的音频是断断续续的,导致10秒的录音可能就只返回了5秒的数据量,这个时候最终编码得到的音频时长明显变短,播放时的效果就像快放一样。未禁用时会在卡顿时自动补偿静默音频,消除了快放效果,但由于丢失的音频已被静默数据代替,听起来就是数据本身的断断续续的效果。在设备不卡时录音没有此问题。</div>
<div style="margin-top:12px;">
<span class="lb">临时调试选项 :</span>
<div><label>
<input type="checkbox" class="debugSet_D220626">
Recorder._D220626=true:ScriptProcessor立即退出回调,试图减少对浏览器录音的影响,有可能减少杂音,效果有待测试
</label></div>
<div><span style="color:#f60">以上参数设置后需重新打开录音</span>;这些参数仅供调试用,未来会被删除。</div>
</div>
<div style="margin-top:12px;">
<span class="lb">Destroy :</span>
<button onclick="callDestroy()">调用Recorder.Destroy()</button>
Destroy会关闭所有的全局资源,包括AudioContext,当录音出现问题时,可尝试Destroy一下重试
</div>
<div style="margin-top:12px;">
<span class="lb">阻止自动锁屏 :</span>
<button onclick="wakeLockClick()">调用navigator.wakeLock 或 静音循环播放视频</button>
手机锁屏后是否能录音不可控,直接简单粗暴的调用wakeLock接口 或 静音+循环播放视频来阻止锁屏,就是有点费电
</div>
<div style="margin-top:12px;">
<span class="lb">暴力测试 :</span>
<button onclick="testMultipleOpen5()">同时打开5个录音(并发调用open)</button>
,每个会录5秒,打开浏览器控制台查看日志
</div>
</div>
<div class="mainBox">
<div>
<span class="lb">DTMF电话按键信号 :</span>
DTMF解码、编码插件,可以方便的处理电话拨号按键信号,用于:电话录音软解,软电话实时提取DTMF按键信号、实时发送DTMF按键信号等。下面拨号盘可在录音时往录音文件中添加按键信息:
</div>
<div>
<style>
.dtmfTab td{padding: 15px 25px;border: 3px solid #ddd;cursor: pointer;user-select: none;}
.dtmfTab td:hover{background:#f60;opacity:.2;color:#fff}
.dtmfTab td:active{opacity:1}
</style>
<table onclick="sendDTMFKeyClick(event)" class="dtmfTab" style="border-collapse: collapse;text-align: center;border: 3px #ccc solid;">
<tr><td>1</td><td>2</td><td>3</td><td>A</td></tr>
<tr><td>4</td><td>5</td><td>6</td><td>B</td></tr>
<tr><td>7</td><td>8</td><td>9</td><td>C</td></tr>
<tr><td>*</td><td>0</td><td>#</td><td>D</td></tr>
</table>
</div>
<div style="margin:5px 0 0">
<div style="margin-top:10px">
<button onclick="decodeDTMF()">识别当前录音中的DTMF按键信息</button>
<button onclick="sendDTMFKeysClick()">发送*#1234567890#*</button>
</div>
</div>
</div>
<div class="mainBox">
<span class="lb">测试App :</span>
iOS Demo App:<a href="https://github.com/xiangyuecn/Recorder/tree/master/app-support-sample/demo_ios">下载源码</a> 自行编译
,Android Demo App:<a href="https://gitee.com/xiangyuecn/Recorder/blob/master/app-support-sample/demo_android/app-debug.apk.zip">下载APK</a> (40kb,删除.zip后缀,<a href="https://github.com/xiangyuecn/Recorder/tree/master/app-support-sample/demo_android">源码</a>)
</div>
<div class="mainBox">
<span class="lb">iframe兼容性 :</span> <button onclick="goiframe()">把页面放到IFrame里面测试权限请求</button>
测试在iframe里面请求录音权限的兼容性。最佳实践应该是让window.top(不适用于跨域)去加载Recorder,iframe里面使用top.Recorder;此测试未遵照此最佳实践,以模拟跨域iframe和同域下的复杂真实情况,H5录音在跨域时未设置相应策略权限永远是拒绝的
</div>
<div class="mainBox">
<span class="lb">音乐播放测试 :</span>
<button onclick="recplay2(this,'rec-4000ms-8kbps-16000hz.wav')">wav</button>
<button onclick="recplay2(this,'rec-4000ms-64kbps-16000hz.mp3')">mp3</button>
<button onclick="recplay2(this,'rec-4000ms-64kbps-16000hz.ogg')">ogg</button>
<button onclick="recplay2(this,'rec-4000ms-64kbps-16000hz.webm')">webm</button>
<button onclick="recplay2(this,'rec-4000ms-12.8kbps-8000hz.amr')">amr</button>
Audio对录音的影响测试(<a href="https://github.com/xiangyuecn/Recorder/issues/34">issues#34</a>);iOS Safari如果未开始过录音并且播放了音乐,然后后续录音将会有问题;再现方法(<a href="https://xiangyuecn.gitee.io/recorder/assets/ztest_apple_developer_forums_getusermedia.html">test apple developer forums</a>):刷新页面后首先先播放音乐,然后开始测试录音,会发现波形显示掉帧或者保持直线。另测试浏览器对音频的支持情况。
</div>
<div class="mainBox">
<span class="lb">视频播放测试 :</span>
<button onclick="videoTestPlay('')">播放mp4</button>
<script>var videoTestPlay=function(attr){
$('.videoTest').show().html('<video controls '+attr+' webkit-playsinline playsinline x5-video-player-type=\'h5\' src=\'assets/audio/movie-一代宗师-此一时彼一时.mp4.webm\' style=\'width:370px;height:160px\'></'+'video>').find('video')[0].play()
}</script>
Video对录音的影响测试(<a href="https://github.com/xiangyuecn/Recorder/issues/84">issues#84</a>);iOS Safari可能出现先播放视频,然后再开始录音,会自动播放视频的声音,但并未再现。
<button onclick="$('.videoTest').show()">显示video</button>
<button onclick="$('.videoTest').hide()">隐藏video</button>
<button onclick="$('.videoTest').html('')">移除video</button>
<button onclick="videoTestPlay(' loop')">循环播放</button>
<button onclick="videoTestPlay(' loop muted')">静音循环播放</button>
<div class="videoTest"></div>
</div>
<div class="mainBox">
<span class="lb">浏览器环境情况 :</span>
<pre class="recinfoCode">
AudioContext:${"AudioContext" in window}
webkitAudioContext:${"webkitAudioContext" in window}
mediaDevices:${!!navigator.mediaDevices}
mediaDevices.getUserMedia:${!!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia)}
navigator.getUserMedia:${!!navigator.getUserMedia}
navigator.webkitGetUserMedia:${!!navigator.webkitGetUserMedia}
AudioContext.scriptProcessor:{{
"createScriptProcessor" in (Recorder.Ctx||{})
|| "createJavaScriptNode" in (Recorder.Ctx||{})
}}
AudioContext.audioWorklet:{{"audioWorklet" in (Recorder.Ctx||{})}}
AudioWorkletNode:${"AudioWorkletNode" in window}
MediaRecorder:${"MediaRecorder" in window}
MediaRecorder.ondataavailable:${"MediaRecorder" in window && "ondataavailable" in MediaRecorder.prototype}
MediaRecorder.WebM.PCM:${"MediaRecorder" in window && MediaRecorder.isTypeSupported("audio/webm; codecs=pcm")}
URL:${location.href.replace(/#.+/g,"")}
UA:${navigator.userAgent}
Recorder库修改时间(有可能修改了忘改):${Recorder.LM}
本页面修改时间(有可能修改了忘改):${PageLM}
</pre>
<span class="lb">问题自检 :</span> 录音时注意观察灰色区域是否有绿色音量跳动,没有绿色跳动说明Recorder没有获取到声音数据。如果测试发现mp3没有声音,可以试一下wav格式,如果wav格式有声音,说明内置lamejs mp3编码器有问题。如果都没有,下载下来播放看看有没有。下载下来也没有声音可以反馈一下。
<div style="padding-top:20px">
如果浏览器不能正常录音,并且不确定是不是这个库的问题,可以到 <a href="https://xiangyuecn.gitee.io/recorder/assets/ztext_collab-project_videojs-record.html">assets/ztext_collab-project_videojs-record.html</a> 试一下。
</div>
</div>
<script>
function reclog(s,color){
var now=new Date();
var t=("0"+now.getHours()).substr(-2)
+":"+("0"+now.getMinutes()).substr(-2)
+":"+("0"+now.getSeconds()).substr(-2);
$(".reclog").prepend('<div style="color:'+(!color?"":color==1?"red":color==2?"#0b1":color)+'">['+t+']'+s+'</div>');
};
window.onerror=function(message, url, lineNo, columnNo, error){
//https://www.cnblogs.com/xianyulaodi/p/6201829.html
reclog('<span style="color:red">【Uncaught Error】'+message+'<pre>'+"at:"+lineNo+":"+columnNo+" url:"+url+"\n"+(error&&error.stack||"不能获得错误堆栈")+'</pre></span>');
};
</script>
<script>
var rec;
function recopen(){
if(rec&&Recorder.IsOpen()){//如果有老的,close掉重新开新的
recclose();
};
var type=$("[name=type]:checked").val();
var bit=+$(".bit").val();
var sample=+$(".sample").val();
cancelAutoStop();
window.waveStore={};
window.sonicAsync=null;
window.takeoffChunks=[];
var disableEnvInFixSet=$(".disableEnvInFixSet")[0].checked;
if(disableEnvInFixSet){
reclog("已禁用设备卡顿时音频输入丢失补偿,可以通过别的程序大量占用CPU来模拟设备卡顿,然后录音听听未补偿时的播放效果,然后再试试不禁用的效果");
};
var enableWebM=$(".enableWebMSet")[0].checked;
Recorder.ConnectEnableWebM=enableWebM;
if(!enableWebM){
reclog("已禁用MediaRecorder.WebM.PCM","#aaa");
}
var enableWorklet=$(".enableWorkletSet")[0].checked;
Recorder.ConnectEnableWorklet=enableWorklet;
if(enableWorklet){
reclog("已启用AudioWorklet"+(enableWebM?"(同时启用了MediaRecorder,AudioWorklet只会在MediaRecorder未生效时采用)":"")+""+workletTips,"#f60");
}
var debugSet_D220626=$(".debugSet_D220626")[0].checked;
Recorder._D220626=debugSet_D220626;
if(debugSet_D220626){
reclog("已启用临时调试选项:Recorder._D220626=true","#f60");
};
var audioTrackSet=null;
var trackSet_device=$(".trackSet_device").val();
var trackSet_noise=$(".trackSet_noise").val();
var trackSet_aec=$(".trackSet_aec").val();
var trackSet_gain=$(".trackSet_gain").val();
if(trackSet_device || trackSet_noise==1 || trackSet_aec==1 || trackSet_gain){
audioTrackSet={};
if(trackSet_device){
var device=DeviceList[+trackSet_device];
audioTrackSet.deviceId=device.deviceId;
audioTrackSet.groupId=device.groupId;
}
if(trackSet_noise){
audioTrackSet.noiseSuppression=+trackSet_noise==1;
}
if(trackSet_aec){
audioTrackSet.echoCancellation=+trackSet_aec==1;
}
if(trackSet_gain){
audioTrackSet.autoGainControl=+trackSet_gain==1;
}
reclog("已启用audioTrackSet配置:"+JSON.stringify(audioTrackSet));
};
var realTimeSendSet=$(".realTimeSendSet")[0].checked;
var realTimeSendTime=+$(".realTimeSend").val();
var asrSet=$(".asrSet")[0].checked;
var takeoffEncodeChunkSet=$(".takeoffEncodeChunkSet")[0].checked;
rec=Recorder({
type:type
,bitRate:bit
,sampleRate:sample
,audioTrackSet:audioTrackSet
,disableEnvInFix:disableEnvInFixSet
,onProcess:function(buffers,powerLevel,duration,sampleRate,newBufferIdx,asyncEnd){
//优先进行pcm处理,可能会发生数据修改,对于需要大量运算的处理需要开启异步模式,onProcess返回true即可开启,异步操作完成后必须回调asyncEnd
//实时混合按键信号
if(dtmfMix){
var val=dtmfMix.mix(buffers, sampleRate, newBufferIdx);
if(val.newEncodes.length>0){
rec.PlayBufferDisable=true;
DemoFragment.PlayBuffer(rec,val.newEncodes[0].data,sampleRate);
};
};
//变速变调
var beginAsync=sonicProcess(rec,buffers,sampleRate,newBufferIdx,asyncEnd);
$(".recpowerx").css("width",powerLevel+"%");
$(".recpowert").text(formatMs(duration,1)+" / "+powerLevel);
//可视化图形绘制
if(waveStore.choice!=recwaveChoiceKey){
waveStore.choice=recwaveChoiceKey;
$(".recwave").html("").append(waveStore[recwaveChoiceKey].elem);
};
waveStore[recwaveChoiceKey].input(buffers[buffers.length-1],powerLevel,sampleRate);
//实时传输
if(realTimeSendSet&&window.realTimeSendTry){
realTimeSendTry(rec.set,realTimeSendTime,buffers,sampleRate);
};
//实时语音识别
if(asrSet&&window.asrInput){
asrInput(buffers,sampleRate,newBufferIdx);
};
return beginAsync;//返回true转成异步操作
}
,takeoffEncodeChunk:!takeoffEncodeChunkSet?null:function(chunkBytes){
takeoffChunks.push(chunkBytes);
}
});
dialogInt=setTimeout(function(){//定时8秒后打开弹窗,用于监测浏览器没有发起权限请求的情况,在open前放置定时器利于收到了回调能及时取消(不管open是同步还是异步回调的)
showDialog();
},8000);
rec.open(function(){
dialogCancel();
var typeSize=",<span style='border:1px solid #bbb;background:#f5f5f5;'>";
if(type=="wav"){
typeSize+="1秒的wav文件大小(字节)估算公式:采样率 × 位数 ÷ 8,当前:"+sample+"*"+bit+"/8≈"+(sample*bit/8)+"b/s";
}else if(type=="mp3"){
typeSize+="1秒的mp3文件大小(字节)估算公式:比特率 × 1000 ÷ 8,当前:"+bit+"*1000/8≈"+(bit*1000/8)+"b/s";
}else{
typeSize="";
};
typeSize&&(typeSize+="</span>");
reclog("<span style='color:#0b1'>已打开:"+type+" "+sample+"hz "+bit+"kbps</span>"+typeSize);
//此处创建这些音频可视化图形绘制浏览器支持妥妥的
initWaveStore(waveStore,".recwave");
},function(e,isUserNotAllow){
dialogCancel();
reclog((isUserNotAllow?"UserNotAllow,":"")+"打开失败:"+e,1);
});
window.waitDialogClick=function(){
dialogCancel();
reclog("打开失败:权限请求被忽略,<span style='color:#f00'>用户主动点击的弹窗</span>");
};
};
//我们可以选择性的弹一个对话框:为了防止移动端浏览器存在第三种情况:用户忽略,并且(或者国产系统UC系)浏览器没有任何回调
var showDialog=function(){
if(!/mobile/i.test(navigator.userAgent)){
return;//只在移动端开启没有权限请求的检测
};
dialogCancel();
$("body").append(''
+'<div class="waitDialog" style="z-index:99999;width:100%;height:100%;top:0;left:0;position:fixed;background:rgba(0,0,0,0.3);">'
+'<div style="display:flex;height:100%;align-items:center;">'
+'<div style="flex:1;"></div>'
+'<div style="width:240px;background:#fff;padding:15px 20px;border-radius: 10px;">'
+'<div style="padding-bottom:10px;">录音功能需要麦克风权限,请允许;如果未看到任何请求,请点击忽略~</div>'
+'<div style="text-align:center;"><a onclick="waitDialogClick()" style="color:#0B1">忽略</a></div>'
+'</div>'
+'<div style="flex:1;"></div>'
+'</div>'
+'</div>');
};
var dialogInt;
var dialogCancel=function(){
clearTimeout(dialogInt);
$(".waitDialog").remove();
};
//弹框End
var callDestroy=function(){
Recorder.Destroy();
reclog("已执行Recorder.Destroy()");
scrollToEl(".reclog");
};
//拉取麦克风设备列表
var DeviceList=[];
var trackSetQueryDeviceList=function(click){
var end=function(list,err){
DeviceList=list;
if(click){
console.log("DeviceList: ", JSON.parse(JSON.stringify(DeviceList)));
}
var opts=['<option value="">'+(list.length?'不设置':err)+'</option>'];
for(var i=0;i<list.length;i++){
var o=list[i];
if(o.deviceId && o.kind=="audioinput"){
var name=o.label||((i+1)+"# 无名称,可能是因为从来没有打开过录音");
opts.push('<option value="'+i+'">'+name+'</option>');
}
}
$(".trackSet_device").html(opts.join(" "))
};
if(navigator.mediaDevices && navigator.mediaDevices.enumerateDevices){
navigator.mediaDevices.enumerateDevices().then(end)["catch"](function(e){
end([],"拉取失败:"+e.message);
});
}else{
end([],"此浏览器不支持拉取设备列表");
}
};
trackSetQueryDeviceList();
function recclose(){
cancelAutoStop();
if(rec){
rec.close(function(){
reclog("已关闭");
});
}else{
reclog("未打开录音",1);
};
};
function recstart(call){
cancelAutoStop();
call||(call=function(msg){
msg&&reclog(msg,1);
});
if(rec&&Recorder.IsOpen()){
window.realTimeSendTryReset&&realTimeSendTryReset(rec.set);
sonicAsync&&sonicAsync.flush();//丢弃不管,省的去同步麻烦
sonicAsync=null;
rec.sonicTips="";
takeoffChunks=[];
//定时停止
var autoStop=$(".autoStopSet")[0].checked;
if(autoStop){
var time=+$(".autoStopTime").val()||0;
if(time<100){
reclog("定时不能小于100ms",1);
return;
};
autoStopTimer=setTimeout(function(){
autoStopTimer=0;
reclog("定时时间到,开始自动调用停止...");
recstop();
},time);
};
rec.start();
var set=rec.set;
reclog((autoStop?"定时"+time+"ms":"")+"录制中:"+set.type+" "+set.sampleRate+"hz "+set.bitRate+"kbps");
call();
}else{
call("未打开录音");
};
};
var autoStopTimer;
var cancelAutoStop=function(){
if(autoStopTimer){
reclog("已取消定时停止",1);
clearTimeout(autoStopTimer);
autoStopTimer=0;
};
};
function recpause(){
if(rec){
rec.pause();
reclog("已暂停");
};
};
function recresume(){
if(rec){
rec.resume();
reclog("继续录音中...");
};
};
var recblob={};
function recstop(call){
recstopFn(call,true,function(err,blob,time){
setTimeout(function(){
window.realTimeSendTryStop&&realTimeSendTryStop(rec.set);
if(!err && rec.set.takeoffEncodeChunk){
reclog("启用takeoffEncodeChunk后stop返回的blob长度为0不提供音频数据","#f60");
reclog("takeoffEncodeChunk接收到"+takeoffChunks.length+"片音频片段,正在合并成一个音频文件...");
var len=0;
for(var i=0;i<takeoffChunks.length;i++){
len+=takeoffChunks[i].length;
};
var chunkData=new Uint8Array(len);
for(var i=0,idx=0;i<takeoffChunks.length;i++){
var itm=takeoffChunks[i];
chunkData.set(itm,idx);
idx+=itm.length;
};
var blob=new Blob([chunkData],{type:"audio/"+rec.set.type});
addRecLog(time,"合并",blob,rec.set,Date.now());
};
});
});
};
function recstopFn(call,isClick,endCall,rec){
cancelAutoStop();
call||(call=function(msg){
msg&&reclog(msg,1);
});
rec=rec||window.rec;
if(rec){
if(isClick){
reclog("正在编码"+rec.set.type+"...");
};
var t1=Date.now();
rec.stop(function(blob,time){
var tag=endCall("",blob,time);
if(tag==-1){
return;
};
addRecLog(time,tag||"已录制",blob,rec.set,t1);
call(null,{data:blob,duration:time});
},function(s){
endCall(s);
call("失败:"+s);
});
}else{
call("未打开录音");
};
};
var recLogLast;
var addRecLog=function(time,tag,blob,set,t1){
var id=RandomKey(16);
recLogLast={blob:blob,set:$.extend({},set),time:time,key:id};
recblob[id]=recLogLast;
reclog(tag+":"+intp(set.bitRate,3)+"kbps "+intp(set.sampleRate,5)+"hz 花"+intp(Date.now()-t1,4)+"ms编码"+intp(blob.size,6)+"b ["+set.type+"]"+formatMs(time)+'ms <button onclick="recdown(\''+id+'\')">下载</button> <button onclick="recplay(\''+id+'\')">播放</button> <span class="p'+id+'"></span> <span class="d'+id+'"></span>');
};
var intp=function(s,len){
s=s==null?"-":s+"";
if(s.length>=len)return s;
return ("_______"+s).substr(-len);
};
var formatMs=function(ms,all){
var f=Math.floor(ms/60000),m=Math.floor(ms/1000)%60;
var s=(all||f>0?(f<10?"0":"")+f+":":"")
+(all||f>0||m>0?("0"+m).substr(-2)+"":"")
+("00"+ms%1000).substr(-3);
return s;
};
function recstop2(){
if(!rec||!rec.buffers){
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;
};
reclog("开始批量编码,请勿进行其他操作~");
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);
};
if(/^(wav|pcm)$/.test(rec.set.type)){
list=[8,16];
};
var i=-1;
var bak=rec.set.bitRate;
var run=function(){
i++;
if(i>=list.length){
rec.set.bitRate=bak;
reclog("批量编码完成");
return;
};
rec.set.bitRate=list[i];
rec.isMock=1;
recstopFn(null,0,function(){
setTimeout(run);
});
};
run();
};
function recplay(key){
var audio=$(".recPlay")[0];
audio.style.display="inline-block";
if(!(audio.ended || audio.paused)){
audio.pause();
};
var o=recblob[key];
if(o){
o.play=(o.play||0)+1;
var logmsg=function(msg){
$(".p"+key).html('<span style="color:green">'+o.play+'</span> '+new Date().toLocaleTimeString()+" "+msg);
};
logmsg("");
audio.onerror=function(e){
logmsg('<span style="color:red">播放失败['+audio.error.code+']'+audio.error.message+'</span>');
};
if(o.play2Name){
audio.src="assets/audio/"+o.play2Name;
audio.play();
return;
};
var end=function(blob){
audio.src=(window.URL||webkitURL).createObjectURL(blob);
audio.play();
};
var wav=Recorder[o.set.type+"2wav"];
if(wav){
logmsg("正在转码成wav...");
var wavData=o.blob;
if(o.set.type=="pcm"){
wavData={
sampleRate:o.set.sampleRate
,bitRate:o.set.bitRate
,blob:o.blob
};
};
wav(wavData,function(blob){
end(blob);
logmsg("已转码成wav播放");
},function(msg){
logmsg('<span style="color:red">转码成wav失败:'+msg+'</span>');
});
}else{
end(o.blob);
};
};
};
function recplay2(elem,name){
elem=$(elem);
var key="recplay2"+elem.html();
recblob[key]||(recblob[key]={
play2Name:name
});
if(!$(".p"+key).length){
elem.before('<br>');
elem.after('<span class="p'+key+'"></span><br>');
};
recplay(key);
};
function recPlayLast(){
if(!recLogLast){
reclog("请先录音,然后停止后再播放",1);
return;
};
recplay(recLogLast.key);
};
function recUploadLast(){
if(!recLogLast){
reclog("请先录音,然后停止后再上传",1);
return;
};
var blob=recLogLast.blob;
//本例子假设使用原始XMLHttpRequest请求方式,实际使用中自行调整为自己的请求方式
//录音结束时拿到了blob文件对象,可以用FileReader读取出内容,或者用FormData上传
var api="https://xx.xx/test_request";
var onreadystatechange=function(title){
return function(){
if(xhr.readyState==4){
if(xhr.status==200){
reclog(title+"上传成功",2);
}else{
reclog(title+"没有完成上传,演示上传地址无需关注上传结果,只要浏览器控制台内Network面板内看到的请求数据结构是预期的就ok了。", "#d8c1a0");
console.error(title+"上传失败",xhr.status,xhr.responseText);
};
};
};
};
reclog("开始上传到"+api+",请求稍后...","#f60");
/***方式一:将blob文件转成base64纯文本编码,使用普通application/x-www-form-urlencoded表单上传***/
var reader=new FileReader();
reader.onloadend=function(){
var postData="";
postData+="mime="+encodeURIComponent(blob.type);//告诉后端,这个录音是什么格式的,可能前后端都固定的mp3可以不用写
postData+="&upfile_b64="+encodeURIComponent((/.+;\s*base64\s*,\s*(.+)$/i.exec(reader.result)||[])[1]) //录音文件内容,后端进行base64解码成二进制
//...其他表单参数
var xhr=new XMLHttpRequest();
xhr.open("POST", api);
xhr.setRequestHeader("Content-Type","application/x-www-form-urlencoded");
xhr.onreadystatechange=onreadystatechange("上传方式一【Base64】");
xhr.send(postData);
};
reader.readAsDataURL(blob);
/***方式二:使用FormData用multipart/form-data表单上传文件***/
var form=new FormData();
form.append("upfile",blob,"recorder.mp3"); //和普通form表单并无二致,后端接收到upfile参数的文件,文件名为recorder.mp3
//...其他表单参数
var xhr=new XMLHttpRequest();
xhr.open("POST", api);
xhr.onreadystatechange=onreadystatechange("上传方式二【FormData】");
xhr.send(form);
};
function recdown(key){
var o=recblob[key];
if(o){
var cls=RandomKey(16);
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;
downA.href=(window.URL||webkitURL).createObjectURL(o.blob);
downA.download=name;
$("."+cls).prepend(downA);
if(!/mobile/i.test(navigator.userAgent)){
downA.click(); //某些软件内会跳转页面到恶心推广页
}
};
};
function recdown64(key, cls){
var o=recblob[key];
var reader = new FileReader();
reader.onloadend = function() {
var id=RandomKey(16);
$("."+cls).append('<textarea class="'+id+'"></textarea>');
$("."+id).val(reader.result);
};
reader.readAsDataURL(o.blob);
};
var ReadBlob=function(blob,call){
var reader = new FileReader();
reader.onloadend = function(e){
call(reader.result);
};
reader.readAsArrayBuffer(blob);
};
var DecodeAudio=function(fileName,arrayBuffer,True,False){
True=True||function(){};
False=False||function(){};
if(!Recorder.Support()){//强制激活Recorder.Ctx 不支持大概率也不支持解码
False("浏览器不支持音频解码");
return;
};
var type=(/[^.]+$/.exec(fileName)||[])[0]||"";
var srcBlob=new Blob([arrayBuffer],{type:type&&("audio/"+type)||""});
var ctx=Recorder.Ctx;
ctx.decodeAudioData(arrayBuffer,function(raw){
var src=raw.getChannelData(0);
var sampleRate=raw.sampleRate;
console.log(fileName,raw,srcBlob);
var pcm=new Int16Array(src.length);
for(var i=0;i<src.length;i++){//floatTo16BitPCM
var s=Math.max(-1,Math.min(1,src[i]));
s=s<0?s*0x8000:s*0x7FFF;
pcm[i]=s;
};
True({
sampleRate:sampleRate
,duration:Math.round(src.length/sampleRate*1000)
,srcBlob:srcBlob
,type:type
,data:pcm
});
},function(e){
console.error("audio解码失败",e);
False(fileName+"解码失败:"+(e&&e.message||"-"));
});
};
var s="https://github.com/xiangyuecn/Recorder/blob/master/src/extensions/";
var extensionsInfo={
WaveView:'<b>WaveView</b> (<a href="'+s+'waveview.js">waveview.js</a> 动态波形)'
,SurferView:'<b>WaveSurferView</b> (<a href="'+s+'wavesurfer.view.js">wavesurfer.view.js</a> 音频可视化波形)'
,Histogram:'<b>FrequencyHistogramView</b> (<a href="'+s+'frequency.histogram.view.js">frequency.histogram.view.js</a> + <a href="'+s+'lib.fft.js">lib.fft.js</a> 音频可视化频率直方图)'
,Sonic:'<b>Sonic</b> (<a href="'+s+'sonic.js">sonic.js</a> 变速变调)'
,DTMF:'<b>DTMF</b> (<a href="'+s+'dtmf.decode.js">dtmf.decode.js</a> + <a href="'+s+'dtmf.encode.js">dtmf.encode.js</a> (电话拨号按键信号)解码、编码)'
};
var recwaveChoiceKey=localStorage["RecWaveChoiceKey"]||"WaveView";
$(".recwaveChoice").bind("click",function(e){
var elem=$(e.target);
$(".recwaveChoice").removeClass("slc");
var val=elem.addClass("slc").attr("key");
var info=extensionsInfo[val.replace(/\d+$/,"")];
if(recwaveChoiceKey!=val){
reclog("已切换波形显示为:"+info);
};
recwaveChoiceKey=val;
localStorage["RecWaveChoiceKey"]=recwaveChoiceKey;
});
if(!$(".recwaveChoice[key="+recwaveChoiceKey+"]").length){
recwaveChoiceKey="WaveView";
};
$(".recwaveChoice[key="+recwaveChoiceKey+"]").click();
var initWaveStore=function(store,elem){
store.WaveView=Recorder.WaveView({elem:elem});
store.SurferView=Recorder.WaveSurferView({elem:elem});
store.Histogram1=Recorder.FrequencyHistogramView({elem:elem});
store.Histogram2=Recorder.FrequencyHistogramView({
elem:elem
,lineCount:90
,position:0
,minHeight:1
,stripeEnable:false
});
store.Histogram3=Recorder.FrequencyHistogramView({
elem:elem
,lineCount:20
,position:0
,minHeight:1
,fallDuration:400
,stripeEnable:false
,mirrorEnable:true
,linear:[0,"#0ac",1,"#0ac"]
});
};
if(window.isSecureContext===false){
reclog("当前网页不是安全环境(HTTPS),将无法获取录音权限,<a href='https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia#Privacy_and_security'>MDN Privacy and security</a>",1);
}else if(window.isSecureContext){
reclog("<span style='color:#0b1'>当前网页处在安全环境中</span>(<a href='https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia#Privacy_and_security'>https、file:///等</a>)");
};
reclog("点击打开录音开始哦,此浏览器<span style='color:"+(Recorder.Support()?"green'>":"red'>不")+"支持录音</span>");
reclog('已启用Extensions:'
+extensionsInfo.WaveView
+''+extensionsInfo.SurferView
+''+extensionsInfo.Histogram
+''+extensionsInfo.Sonic
+''+extensionsInfo.DTMF);
var workletTips='注意:由于AudioWorklet内部<a href="https://developer.mozilla.org/en-US/docs/Web/API/AudioWorkletProcessor/process" target="_blank">1秒会产生375次回调</a>,在移动端可能会有性能问题导致浏览器回调丢失,进而导致录音数据、时长变短,PC端似乎无此影响,可通过定时1分钟录音来检测影响(如果短了1秒以上即为有问题);在无明显优势好处的前提下,暂不建议启用。';
$(".workletSetTips").html(workletTips);
$(".enableWorkletSet").bind("change",function(){
localStorage["RecEnableWorklet"]=this.checked?"1":"";
})[0].checked=localStorage["RecEnableWorklet"]=="1";
$(".enableWebMSet").bind("change",function(){
localStorage["RecEnableWebM"]=this.checked?"1":"-1";
})[0].checked=localStorage["RecEnableWebM"]!="-1";
//页面滚动到这个位置
var scrollToEl=function(elem){
$("html,body").animate({scrollTop:$(elem).offset().top-200},300);
};
$(".recinfoCode").text($(".recinfoCode").text().replace(/\$\{(.+?)\}|\{\{([\S\s]+?)\}\}/g,function(a,b,c){return eval(b||c)}));
function goiframe(){
location.href="assets/ztest_iframe.html#iframeUrl=/index.html";
};
if(window.top!=window){
var isSelf=false;
try{
window.top.aa=123;
isSelf=true;
}catch(e){};
reclog("<span style='color:#f60'>当前页面处在在iframe中,但故意未进行任何处理,"+(isSelf?"当前是同域":"并且已发生跨域,未设置相应策略权限永远是拒绝的")+"</span>");
};
//暴力测试:同时打开5个录音,定时录5秒
var testMultipleOpen5=function(){
if(rec)rec.close();
for(var i=0;i<5;i++){(function(i){
var rec=Recorder({type:"mp3"
,onProcess:function(buffers,powerLevel,duration,sampleRate,newBufferIdx,asyncEnd){
Recorder.CLog(i+1,0,duration,buffers.length,newBufferIdx);
}
});
rec.open(function(){
rec.start();
setTimeout(function(){
var t1=Date.now();
rec.stop(function(blob,duration){
addRecLog(duration, ""+(i+1)+"",blob,rec.set,t1);
},null,true);
},5000);
});
})(i)}
};
//阻止手机自动锁屏
var wakeLockClick=function(){
var fail=function(){
videoTestPlay(' loop muted');
reclog('已通过 循环+静音 播放视频来阻止自动锁屏');
scrollToEl(".videoTest");
};
if(navigator.wakeLock){
if(window.wakeLockObj)wakeLockObj.release();
navigator.wakeLock.request('screen').then(function(lock){
wakeLockObj=lock;
reclog('已通过wakeLock阻止自动锁屏 <button onclick="wakeUnLockClick(this)">恢复锁屏</button>');
window.wakeUnLockClick=function(btn){
lock.release().then(function(){
wakeLockObj=null; $(btn).remove();
reclog("已恢复自动锁屏");
});
};
scrollToEl(".reclog");
})['catch'](function(e){
console.error("wakeLock错误:"+e.message);
fail();
});
}else{
fail();
}
};
//实时传输数据模拟开关
$(".realTimeSendSet").bind("change",function(e){
var open=e.target.checked;
$(".webrtcView")[open?"show":"hide"]();
if(open && !window.webrtcCreate){
var file="assets/zdemo.index.webrtc.js";
reclog("正在加载"+file+" ...");
loadJsList([file]);
};
});
//ASR开关,实时语音识别、音频文件转文字
$(".asrSet").bind("change",function(e){
var open=e.target.checked;
$(".asrView")[open?"show":"hide"]();
if(open && !window.asrInput){
var file="assets/zdemo.index.asr.js";
reclog("正在加载"+file+" ...");
loadJsList([file]);
};
});
//变速变调
var sonicCtrlSet={};
$(".sonicCtrlInput").bind("change",function(e){
sonicCtrlSet[/sonicCtrl([^ ]+)$/.exec(e.target.className)[1].toLowerCase()]=+e.target.value;
});
$(".sonicCtrlRange").bind("change",function(e){
$(e.target).parent().find(".sonicCtrlInput").val(/\d+\.\d+/.exec(e.target.value+".0")[0]).change();
}).change();
var resetSonicCtrl=function(){
$(".sonicCtrlRange").val(1).change();
$(".sonicCtrlBufferRange").val(200).change();
};
var sonicCtrlBoxMaskClose=function(){
$(".sonicCtrlBoxMask").hide();
};
if(!/mobile/i.test(navigator.userAgent)){
sonicCtrlBoxMaskClose();
};
var sonicRecTransform=function(){
if(!rec||!rec.buffers){
reclog("请先录音",1);
return;
};
var type=rec.set.type;
var sampleRate=rec.set.sampleRate;
var bitRate=rec.set.bitRate;
if(type!="mp3"){
reclog("目前只支持mp3格式的录音重新转换,因为其他格式buffers已被污染",1);
return;
};
sonicAsync&&sonicAsync.flush();
sonicAsync=null;
var srcBuffers=rec.buffers;
var buffers=[];
var idx=-1,logScope={};
var run=function(){
idx++;
if(idx>=srcBuffers.length){
var mockRec=Recorder({type:type,sampleRate:sampleRate,bitRate:bitRate});
mockRec.mock(Recorder.SampleData(buffers,sampleRate,sampleRate).data,sampleRate);
recstopFn(null,0,function(){
return "已转换";
},mockRec);
return;
};
buffers.push(Recorder.SampleData([srcBuffers[idx]],rec.srcSampleRate,sampleRate).data);
var beginAsync=sonicProcess(logScope,buffers,sampleRate,idx,run);
if(!beginAsync){
reclog("不存在变速变调设置,或不能开启转换",1);
};
buffers[idx]=new Int16Array(0);
};
run();
};
var sonicInfo;
var sonicProcess=function(logScope,buffers,sampleRate,newBufferIdx,asyncEnd){
if(sonicCtrlSet.pitch==1
&&sonicCtrlSet.rate==1
&&sonicCtrlSet.speed==1
&&sonicCtrlSet.volume==1){//不存在变速变调设置
if(logScope.sonicTips){
logScope.sonicTips="";
reclog("已停用整变速变调","#aaa");
}
return;
};
if(sonicAsync==-1){
return;
};
if(!sonicAsync||sonicAsync.set.sampleRate!=sampleRate){
//实时处理只能用异步操作,不能用同步方法,否则必然卡顿
sonicAsync=Recorder.Sonic.Async({sampleRate:sampleRate});
sonicInfo={};
if(!sonicAsync){
sonicAsync=-1;
reclog("不能开启Sonic.Async,浏览器不支持WebWorker操作,降级不变速变调",1);
return;
};
};
var sonicTips="Pitch="+sonicCtrlSet.pitch
+", Speed="+sonicCtrlSet.speed
+", Rate="+sonicCtrlSet.rate
+", Volume="+sonicCtrlSet.volume;
if(logScope.sonicTips!=sonicTips){
reclog((logScope.sonicTips?"已修改":"已开启")+"变速变调:"+sonicTips,"#aaa");
logScope.sonicTips=sonicTips;
};
sonicAsync.setPitch(sonicCtrlSet.pitch);
sonicAsync.setRate(sonicCtrlSet.rate);
sonicAsync.setSpeed(sonicCtrlSet.speed);
sonicAsync.setVolume(sonicCtrlSet.volume);
var newBuffers=sonicInfo.buffers||[];
var newBufferSize=sonicInfo.bufferSize||0;
var blockSize=sampleRate/1000*sonicCtrlSet.buffer;//缓冲0-1000ms的数据进行处理,200ms以上可避免引入大量杂音
var lastIdx=buffers.length-1;
for(var i=newBufferIdx;i<=lastIdx;i++){
newBuffers.push(buffers[i]);//copy出来,异步onProcess会清空这些数组
newBufferSize+=buffers[i].length;
};
if(newBufferSize<blockSize){
setTimeout(function(){
asyncEnd();//缓冲未满,此时并未处理,但也需要进行异步回调
});
}else{
var buffer=newBuffers[0]||[];
if(newBuffers.length>1){
buffer=Recorder.SampleData(newBuffers,sampleRate,sampleRate).data;
};
newBuffers=[];
newBufferSize=0;
var sizeOld=buffer.length,sizeNew=0;
//推入后台异步转换
sonicAsync.input(buffer,function(pcm){
buffers[lastIdx]=pcm;//写回buffers,放到调用时的最后一个位置即可 ,其他内容已在开启异步模式时已经被自动替换成了空数组
//实时播放反馈
if(sonicCtrlSet.play&&window.DemoFragment&&DemoFragment.PlayBuffer){
try{
DemoFragment.PlayBuffer(sonicInfo,pcm,sampleRate);
}catch(e){//在古董浏览器里面可能会开启播放失败
console.error("sonic -> DemoFragment.PlayBuffer错误,无法实时播放反馈",e);
}
};
asyncEnd();//完成处理必须进行回调
});
};
sonicInfo.buffers=newBuffers;
sonicInfo.bufferSize=newBufferSize;
return true;
};
/****DTMF电话按键信号****/
var decodeDTMF=function(){
if(!recLogLast){
reclog("请先录音",1);
return;
};
reclog("开始识别DTMF...",2);
ReadBlob(recLogLast.blob,function(arr){
DecodeAudio("rec."+recLogLast.set.type,arr,function(data){
var finds=[];
var chunk=Recorder.DTMF_Decode(data.data,data.sampleRate);
for(var i=0;i<chunk.keys.length;i++){
reclog("发现按键["+chunk.keys[i].key+"],位于"+chunk.keys[i].time+"ms处");
finds.push(chunk.keys[i].key);
};
reclog("识别完毕,"+(finds.length?"发现按键:"+finds.join(""):"未发现按键信息"),2);
},function(err){
reclog(err,2);
});
});
};
var decodeDTMFStream=function(pcm,sampleRate,chunk){
chunk=Recorder.DTMF_Decode(pcm,sampleRate,chunk);
for(var i=0;i<chunk.keys.length;i++){
reclog("发现按键["+chunk.keys[i].key+"],位于"+chunk.keys[i].time+"ms处");
};
return chunk;
};
var sendDTMFKeyClick=function(e){
if(e.target.tagName=="TD"){
sendDTMFKeys(e.target.innerHTML)
};
};
var sendDTMFKeysClick=function(){
sendDTMFKeys("*#1234567890#*");
};
var sendDTMFKeys=function(keys){
if(!dtmfMix){
dtmfMix=Recorder.DTMF_EncodeMix({
duration:100 //按键信号持续时间 ms,最小值为30ms
,mute:25 //按键音前后静音时长 ms,取值为0也是可以的
,interval:200 //两次按键信号间隔时长 ms,间隔内包含了duration+mute*2,最小值为120ms
});
};
if(!rec||!rec.buffers){
reclog("没有开始录音,按键会存储到下次录音","#bbb");
};
dtmfMix.add(keys);
//添加过去就不用管了,实时处理时会调用mix方法混入到pcm中。
};
var dtmfMix;
</script>
</div><!-- demoMain end -->
<script>
if(/mobile/i.test(navigator.userAgent)){
//移动端加载控制台组件
var elem=document.createElement("script");
elem.setAttribute("type","text/javascript");
elem.setAttribute("src","https://xiangyuecn.gitee.io/recorder/assets/ztest-vconsole.js");
document.body.appendChild(elem);
elem.onload=function(){
new VConsole();
};
};
</script>
<div style="padding:100px;"></div>
<!-- end 结束copy源码 -->
<script>
$(function(){
var prev;
$(".types").bind("click",function(e){
var input=$(e.target);
if(input[0].nodeName=="LABEL"){
input=$(input).find("input");
};
var minjs=$(".loadMinJs")[0].checked;
if(prev!=input[0]||prev.minjs!==minjs){
prev=input[0];
prev.minjs=minjs;
loadEngine($(input));
};
});
});
function loadEngine(input){
if(input.length&&input[0].nodeName=="INPUT"){
var type=input.val();
var srcs=input.attr("engine").split(",");
var mins=input.attr("min").split(",");
var adds=input.attr("addload");
for(var i=0;i<srcs.length;i++){
srcs[i]="src/engine/"+srcs[i]+".js";
};
if(adds){
adds=adds.split(",");
for(var i=0;i<adds.length;i++){
adds[i]="src/engine/"+adds[i]+".js";
};
};
for(var i=0;i<mins.length;i++){
var v=mins[i];
if(!v){
v="/dist/recorder-core";
};
if(v.substr(0,1)=="/"){
v=v.substr(1);
}else{
v="dist/engine/"+v;
};
mins[i]=v+".js";
};
var minjs=$(".loadMinJs")[0].checked;
var engines=[].concat(minjs?mins:srcs, adds||[]);
var end=function(){
var enc=Recorder.prototype["enc_"+type];
var tips=[!enc?"这个编码器无提示信息":type+"编码器"+(enc.stable?"稳定版":"beta版")+",<span style='color:"
+(type=="wav"?"#0b1'>wav转码超快"
:type=="pcm"?"#0b1'>pcm转码超快"
:Recorder.prototype[type+"_start"]?"#0b1'>支持边录边转码(Worker)"
:"red'>仅支持标准UI线程转码")
+"</span>,"+enc.testmsg];
tips.push('<div style="color:green;padding-left:50px">');
tips.push("使用"+type+"录音需要加载的js:");
tips.push("<br>【压缩版】:"+mins.join(", "))
tips.push("<br>【源文件】:src/recorder-core.js, "+srcs.join(", "));
tips.push("</div>");
$(".typeTips").html(tips.join(""));
};
if(!Recorder.prototype[type] || loadEngineState[type]!==minjs){
reclog("<span style='color:#f60'>正在加载"+type+"编码器"+(minjs?"压缩版":"源码版")+",请勿操作...</span>");
loadJsList(engines,function(){
loadEngineState[type]=minjs;
Recorder.WaveView=WaveViewBak;
Recorder.WaveSurferView=WaveSurferViewBak;
Recorder.LibFFT=LibFFTBak;
Recorder.FrequencyHistogramView=FrequencyHistogramViewBak;
Recorder.Sonic=SonicBak;
Recorder.DTMF_Encode=DTMF_EncodeBak;
Recorder.DTMF_EncodeMix=DTMF_EncodeMixBak;
Recorder.DTMF_Decode=DTMF_DecodeBak;
reclog("<span style='color:#0b1'>"+type+"编码器"+(minjs?"压缩版":"源码版")+"已加载,可以录音了</span>");
end();
},function(err){
reclog(err,1);
return false;
});
}else{
end();
};
};
};
loadEngineState={};
var WaveViewBak=Recorder.WaveView;
var WaveSurferViewBak=Recorder.WaveSurferView;
var LibFFTBak=Recorder.LibFFT;
var FrequencyHistogramViewBak=Recorder.FrequencyHistogramView;
var SonicBak=Recorder.Sonic;
var DTMF_EncodeBak=Recorder.DTMF_Encode;
var DTMF_EncodeMixBak=Recorder.DTMF_EncodeMix;
var DTMF_DecodeBak=Recorder.DTMF_Decode;
var loadJsList=function(jsList,True,False,allCheck){//False -> false继续加载,allCheck(urlItem)允许修改url值
var rootUrl="";
var load=function(idx){
if(idx>=jsList.length){
True&&True();
return;
};
var itm=jsList[idx];
if(typeof(itm)=="string")itm={url:itm};
if(itm.check && itm.check()===false){
load(idx+1);
return;
};
if(allCheck && allCheck(itm)===false){
load(idx+1);
return;
};
var url=itm.url;
var elem=document.createElement("script");
elem.setAttribute("type","text/javascript");
elem.setAttribute("src",(/^\w+:/.test(url)?"":rootUrl)+url);
if(!("onload" in elem)){//IsLoser 古董浏览器
elem.onreadystatechange=function(){
if(elem.readyState=="loaded"){
elem.onload();
}
}
};
var isload=0;
elem.onload=function(){
if(!isload){
load(idx+1);
isload=1;
}
};
elem.onerror=function(e){
var v=False&&False("js加载失败:"+(e.message||"-")+""+url,1);
if(v===false){
elem.onload();
}
};
$("head")[0].appendChild(elem);
};
setTimeout(function(){ load(0) });
};
(function(){try{
var minjs=$(".loadMinJs");
minjs[0].checked=localStorage["loadMinJs"]!="0";
minjs.bind("change",function(){
localStorage["loadMinJs"]=minjs[0].checked?"1":"0";
location.reload();
});
$(".batEnc").bind("click",function(){
if(minjs[0].checked){
reclog("<span style='color:#f60'>当前为压缩版模式,由于不同录音格式之间存在Recorder对象覆盖行为,切换类型将导致不能编码,关掉请求压缩版后无此限制</span>");
};
});
loadEngine($(".initType"));
//pcm测试页面来的
if(/ispcm=1/.test(location.href)){
$(".demoHead,.gitUrl,.btns,.recpower,.waveBox,.pcmPageHide").hide();
};
}catch(e){console.error(e)}})();
</script>
<!-- 加载打赏挂件 -->
<script src="assets/zdemo.widget.donate.js"></script>
<script>
DonateWidget({
log:function(msg){reclog(msg)}
,mobElem:$(".reclog").append('<div class="DonateView"></div>').find(".DonateView")[0]
});
</script>
</div>
</body>
</html>
马建仓 AI 助手
尝试更多
代码解读
代码找茬
代码优化
JavaScript
1
https://gitee.com/wyybs/Recorder.git
git@gitee.com:wyybs/Recorder.git
wyybs
Recorder
Recorder
master

搜索帮助