recorder-core.js 51 KB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493
/*
录音
https://github.com/xiangyuecn/Recorder
*/
(function(factory){
	factory(window);
	//umd returnExports.js
	if(typeof(define)=='function' && define.amd){
		define(function(){
			return Recorder;
		});
	};
	if(typeof(module)=='object' && module.exports){
		module.exports=Recorder;
	};
}(function(window){
"use strict";

var NOOP=function(){};

var Recorder=function(set){
	return new initFn(set);
};
Recorder.LM="2023-02-01 18:05";
var RecTxt="Recorder";
var getUserMediaTxt="getUserMedia";
var srcSampleRateTxt="srcSampleRate";
var sampleRateTxt="sampleRate";
var CatchTxt="catch";


//是否已经打开了全局的麦克风录音,所有工作都已经准备好了,就等接收音频数据了
Recorder.IsOpen=function(){
	var stream=Recorder.Stream;
	if(stream){
		var tracks=stream.getTracks&&stream.getTracks()||stream.audioTracks||[];
		var track=tracks[0];
		if(track){
			var state=track.readyState;
			return state=="live"||state==track.LIVE;
		};
	};
	return false;
};
/*H5录音时的AudioContext缓冲大小。会影响H5录音时的onProcess调用速率,相对于AudioContext.sampleRate=48000时,4096接近12帧/s,调节此参数可生成比较流畅的回调动画。
	取值256, 512, 1024, 2048, 4096, 8192, or 16384
	注意,取值不能过低,2048开始不同浏览器可能回调速率跟不上造成音质问题。
	一般无需调整,调整后需要先close掉已打开的录音,再open时才会生效。
*/
Recorder.BufferSize=4096;
//销毁已持有的所有全局资源,当要彻底移除Recorder时需要显式的调用此方法
Recorder.Destroy=function(){
	CLog(RecTxt+" Destroy");
	Disconnect();//断开可能存在的全局Stream、资源
	
	for(var k in DestroyList){
		DestroyList[k]();
	};
};
var DestroyList={};
//登记一个需要销毁全局资源的处理方法
Recorder.BindDestroy=function(key,call){
	DestroyList[key]=call;
};
//判断浏览器是否支持录音,随时可以调用。注意:仅仅是检测浏览器支持情况,不会判断和调起用户授权,不会判断是否支持特定格式录音。
Recorder.Support=function(){
	var scope=navigator.mediaDevices||{};
	if(!scope[getUserMediaTxt]){
		scope=navigator;
		scope[getUserMediaTxt]||(scope[getUserMediaTxt]=scope.webkitGetUserMedia||scope.mozGetUserMedia||scope.msGetUserMedia);
	};
	if(!scope[getUserMediaTxt]){
		return false;
	};
	Recorder.Scope=scope;
	
	if(!Recorder.GetContext()){
		return false;
	};
	return true;
};
//获取全局的AudioContext对象,如果浏览器不支持将返回null
Recorder.GetContext=function(){
	var AC=window.AudioContext;
	if(!AC){
		AC=window.webkitAudioContext;
	};
	if(!AC){
		return null;
	};
	
	if(!Recorder.Ctx||Recorder.Ctx.state=="closed"){
		//不能反复构造,低版本number of hardware contexts reached maximum (6)
		Recorder.Ctx=new AC();
		
		Recorder.BindDestroy("Ctx",function(){
			var ctx=Recorder.Ctx;
			if(ctx&&ctx.close){//能关掉就关掉,关不掉就保留着
				ctx.close();
				Recorder.Ctx=0;
			};
		});
	};
	return Recorder.Ctx;
};


/*是否启用MediaRecorder.WebM.PCM来进行音频采集连接(如果浏览器支持的话),默认启用,禁用或者不支持时将使用AudioWorklet或ScriptProcessor来连接;MediaRecorder采集到的音频数据比其他方式更好,几乎不存在丢帧现象,所以音质明显会好很多,建议保持开启*/
var ConnectEnableWebM="ConnectEnableWebM";
Recorder[ConnectEnableWebM]=true;

/*是否启用AudioWorklet特性来进行音频采集连接(如果浏览器支持的话),默认禁用,禁用或不支持时将使用过时的ScriptProcessor来连接(如果方法还在的话),当前AudioWorklet的实现在移动端没有ScriptProcessor稳健;ConnectEnableWebM如果启用并且有效时,本参数将不起作用*/
var ConnectEnableWorklet="ConnectEnableWorklet";
Recorder[ConnectEnableWorklet]=false;

/*初始化H5音频采集连接。如果自行提供了sourceStream将只进行一次简单的连接处理。如果是普通麦克风录音,此时的Stream是全局的,Safari上断开后就无法再次进行连接使用,表现为静音,因此使用全部使用全局处理避免调用到disconnect;全局处理也有利于屏蔽底层细节,start时无需再调用底层接口,提升兼容、可靠性。*/
var Connect=function(streamStore,isUserMedia){
	var bufferSize=streamStore.BufferSize||Recorder.BufferSize;
	
	var ctx=Recorder.Ctx,stream=streamStore.Stream;
	var mediaConn=function(node){
		var media=stream._m=ctx.createMediaStreamSource(stream);
		var ctxDest=ctx.destination,cmsdTxt="createMediaStreamDestination";
		if(ctx[cmsdTxt]){
			ctxDest=ctx[cmsdTxt]();
		};
		media.connect(node);
		node.connect(ctxDest);
	}
	var isWebM,isWorklet,badInt,webMTips="";
	var calls=stream._call;
	
	//浏览器回传的音频数据处理
	var onReceive=function(float32Arr){
		for(var k0 in calls){//has item
			var size=float32Arr.length;
			
			var pcm=new Int16Array(size);
			var sum=0;
			for(var j=0;j<size;j++){//floatTo16BitPCM 
				var s=Math.max(-1,Math.min(1,float32Arr[j]));
				s=s<0?s*0x8000:s*0x7FFF;
				pcm[j]=s;
				sum+=Math.abs(s);
			};
			
			for(var k in calls){
				calls[k](pcm,sum);
			};
			
			return;
		};
	};
	
	var scriptProcessor="ScriptProcessor";//一堆字符串名字,有利于压缩js
	var audioWorklet="audioWorklet";
	var recAudioWorklet=RecTxt+" "+audioWorklet;
	var RecProc="RecProc";
	var MediaRecorderTxt="MediaRecorder";
	var MRWebMPCM=MediaRecorderTxt+".WebM.PCM";


//===================连接方式三=========================
	//古董级别的 ScriptProcessor 处理,目前所有浏览器均兼容,虽然是过时的方法,但更稳健,移动端性能比AudioWorklet强
	var oldFn=ctx.createScriptProcessor||ctx.createJavaScriptNode;
	var oldIsBest="。由于"+audioWorklet+"内部1秒375次回调,在移动端可能会有性能问题导致回调丢失录音变短,PC端无影响,暂不建议开启"+audioWorklet+"。";
	var oldScript=function(){
		isWorklet=stream.isWorklet=false;
		_Disconn_n(stream);
		CLog("Connect采用老的"+scriptProcessor+","+(Recorder[ConnectEnableWorklet]?"但已":"可")+"设置"+RecTxt+"."+ConnectEnableWorklet+"=true尝试启用"+audioWorklet+webMTips+oldIsBest,3);
		
		var process=stream._p=oldFn.call(ctx,bufferSize,1,1);//单声道,省的数据处理复杂
		mediaConn(process);
		
		var _DsetTxt="_D220626",_Dset=Recorder[_DsetTxt];if(_Dset)CLog("Use "+RecTxt+"."+_DsetTxt,3);
		process.onaudioprocess=function(e){
			var arr=e.inputBuffer.getChannelData(0);
			if(_Dset){//临时调试用的参数,未来会被删除
				arr=new Float32Array(arr);//块是共享的,必须复制出来
				setTimeout(function(){ onReceive(arr) });//立即退出回调,试图减少对浏览器录音的影响
			}else{
				onReceive(arr);
			};
		};
	};


//===================连接方式二=========================
var connWorklet=function(){
	//尝试开启AudioWorklet处理
	isWebM=stream.isWebM=false;
	_Disconn_r(stream);
	
	isWorklet=stream.isWorklet=!oldFn || Recorder[ConnectEnableWorklet];
	var AwNode=window.AudioWorkletNode;
	if(!(isWorklet && ctx[audioWorklet] && AwNode)){
		oldScript();//被禁用 或 不支持,直接使用老的
		return;
	};
	var clazzUrl=function(){
		var xf=function(f){return f.toString().replace(/^function|DEL_/g,"").replace(/\$RA/g,recAudioWorklet)};
		var clazz='class '+RecProc+' extends AudioWorkletProcessor{';
			clazz+="constructor "+xf(function(option){
				DEL_super(option);
				var This=this,bufferSize=option.processorOptions.bufferSize;
				This.bufferSize=bufferSize;
				This.buffer=new Float32Array(bufferSize*2);//乱给size搞乱缓冲区不管
				This.pos=0;
				This.port.onmessage=function(e){
					if(e.data.kill){
						This.kill=true;
						console.log("$RA kill call");
					}
				};
				console.log("$RA .ctor call", option);
			});
			
			//https://developer.mozilla.org/en-US/docs/Web/API/AudioWorkletProcessor/process 每次回调128个采样数据,1秒375次回调,高频导致移动端性能问题,结果就是回调次数缺斤少两,进而导致丢失数据,PC端似乎没有性能问题
			clazz+="process "+xf(function(input,b,c){//需要等到ctx激活后才会有回调
				var This=this,bufferSize=This.bufferSize;
				var buffer=This.buffer,pos=This.pos;
				input=(input[0]||[])[0]||[];
				if(input.length){
					buffer.set(input,pos);
					pos+=input.length;
					
					var len=~~(pos/bufferSize)*bufferSize;
					if(len){
						this.port.postMessage({ val: buffer.slice(0,len) });
						
						var more=buffer.subarray(len,pos);
						buffer=new Float32Array(bufferSize*2);
						buffer.set(more);
						pos=more.length;
						This.buffer=buffer;
					}
					This.pos=pos;
				}
				return !This.kill;
			});
		clazz+='}'
			+'try{'
				+'registerProcessor("'+RecProc+'", '+RecProc+')'
			+'}catch(e){'
				+'console.error("'+recAudioWorklet+'注册失败",e)'
			+'}';
		//URL.createObjectURL 本地有些浏览器会报 Not allowed to load local resource,直接用dataurl
		return "data:text/javascript;base64,"+btoa(unescape(encodeURIComponent(clazz)));
	};
	
	var awNext=function(){//可以继续,没有调用断开
		return isWorklet && stream._na;
	};
	var nodeAlive=stream._na=function(){
		//start时会调用,只要没有收到数据就断定AudioWorklet有问题,恢复用老的
		if(badInt!==""){//没有回调过数据
			clearTimeout(badInt);
			badInt=setTimeout(function(){
				badInt=0;
				if(awNext()){
					CLog(audioWorklet+"未返回任何音频,恢复使用"+scriptProcessor,3);
					oldFn&&oldScript();//未来没有老的,可能是误判
				};
			},500);
		};
	};
	var createNode=function(){
		if(!awNext())return;
		var node=stream._n=new AwNode(ctx, RecProc, {
			processorOptions:{bufferSize:bufferSize}
		});
		mediaConn(node);
		node.port.onmessage=function(e){
			if(badInt){
				clearTimeout(badInt);badInt="";
			};
			if(awNext()){
				onReceive(e.data.val);
			}else if(!isWorklet){
				CLog(audioWorklet+"多余回调",3);
			};
		};
		CLog("Connect采用"+audioWorklet+",设置"+RecTxt+"."+ConnectEnableWorklet+"=false可恢复老式"+scriptProcessor+webMTips+oldIsBest,3);
	};
	
	//如果start时的resume和下面的构造node同时进行,将会导致部分浏览器崩溃,源码assets中 ztest_chrome_bug_AudioWorkletNode.html 可测试。所以,将所有代码套到resume里面(不管catch),避免出现这个问题
	ctx.resume()[calls&&"finally"](function(){//注释掉这行 观摩浏览器崩溃 STATUS_ACCESS_VIOLATION
		if(!awNext())return;
		if(ctx[RecProc]){
			createNode();
			return;
		};
		var url=clazzUrl();
		ctx[audioWorklet].addModule(url).then(function(e){
			if(!awNext())return;
			ctx[RecProc]=1;
			createNode();
			if(badInt){//重新计时
				nodeAlive();
			};
		})[CatchTxt](function(e){ //fix 关键字,保证catch压缩时保持字符串形式
			CLog(audioWorklet+".addModule失败",1,e);
			awNext()&&oldScript();
		});
	});
};


//===================连接方式一=========================
var connWebM=function(){
	//尝试开启MediaRecorder录制webm+pcm处理
	var MR=window[MediaRecorderTxt];
	var onData="ondataavailable";
	var webmType="audio/webm; codecs=pcm";
	isWebM=stream.isWebM=Recorder[ConnectEnableWebM];
	
	var supportMR=MR && (onData in MR.prototype) && MR.isTypeSupported(webmType);
	webMTips=supportMR?"":"(此浏览器不支持"+MRWebMPCM+")";
	if(!isUserMedia || !isWebM || !supportMR){
		connWorklet(); //非麦克风录音(MediaRecorder采样率不可控) 或 被禁用 或 不支持MediaRecorder 或 不支持webm+pcm
		return;
	}
	
	var mrNext=function(){//可以继续,没有调用断开
		return isWebM && stream._ra;
	};
	var mrAlive=stream._ra=function(){
		//start时会调用,只要没有收到数据就断定MediaRecorder有问题,降级处理
		if(badInt!==""){//没有回调过数据
			clearTimeout(badInt);
			badInt=setTimeout(function(){
				//badInt=0; 保留给nodeAlive继续判断
				if(mrNext()){
					CLog(MediaRecorderTxt+"未返回任何音频,降级使用"+audioWorklet,3);
					connWorklet();
				};
			},500);
		};
	};
	
	var mrSet=Object.assign({mimeType:webmType}, Recorder.ConnectWebMOptions);
	var mr=stream._r=new MR(stream, mrSet);
	var webmData=stream._rd={sampleRate:ctx[sampleRateTxt]};
	mr[onData]=function(e){
		//提取webm中的pcm数据,提取失败就等着badInt超时降级处理
		var reader=new FileReader();
		reader.onloadend=function(){
			if(mrNext()){
				var f32arr=WebM_Extract(new Uint8Array(reader.result),webmData);
				if(!f32arr)return;
				if(f32arr==-1){//无法提取,立即降级
					connWorklet();
					return;
				};
				
				if(badInt){
					clearTimeout(badInt);badInt="";
				};
				onReceive(f32arr);
			}else if(!isWebM){
				CLog(MediaRecorderTxt+"多余回调",3);
			};
		};
		reader.readAsArrayBuffer(e.data);
	};
	mr.start(~~(bufferSize/48));//按48k时的回调间隔
	CLog("Connect采用"+MRWebMPCM+",设置"+RecTxt+"."+ConnectEnableWebM+"=false可恢复使用"+audioWorklet+"或老式"+scriptProcessor);
};

	connWebM();
};
var ConnAlive=function(stream){
	if(stream._na) stream._na(); //检查AudioWorklet连接是否有效,无效就回滚到老的ScriptProcessor
	if(stream._ra) stream._ra(); //检查MediaRecorder连接是否有效,无效就降级处理
};
var _Disconn_n=function(stream){
	stream._na=null;
	if(stream._n){
		stream._n.port.postMessage({kill:true});
		stream._n.disconnect();
		stream._n=null;
	};
};
var _Disconn_r=function(stream){
	stream._ra=null;
	if(stream._r){
		stream._r.stop();
		stream._r=null;
	};
};
var Disconnect=function(streamStore){
	streamStore=streamStore||Recorder;
	var isGlobal=streamStore==Recorder;
	
	var stream=streamStore.Stream;
	if(stream){
		if(stream._m){
			stream._m.disconnect();
			stream._m=null;
		};
		if(stream._p){
			stream._p.disconnect();
			stream._p.onaudioprocess=stream._p=null;
		};
		_Disconn_n(stream);
		_Disconn_r(stream);
		
		if(isGlobal){//全局的时候,要把流关掉(麦克风),直接提供的流不处理
			var tracks=stream.getTracks&&stream.getTracks()||stream.audioTracks||[];
			for(var i=0;i<tracks.length;i++){
				var track=tracks[i];
				track.stop&&track.stop();
			};
			stream.stop&&stream.stop();
		};
	};
	streamStore.Stream=0;
};

/*对pcm数据的采样率进行转换
pcmDatas: [[Int16,...]] pcm片段列表
pcmSampleRate:48000 pcm数据的采样率
newSampleRate:16000 需要转换成的采样率,newSampleRate>=pcmSampleRate时不会进行任何处理,小于时会进行重新采样
prevChunkInfo:{} 可选,上次调用时的返回值,用于连续转换,本次调用将从上次结束位置开始进行处理。或可自行定义一个ChunkInfo从pcmDatas指定的位置开始进行转换
option:{ 可选,配置项
		frameSize:123456 帧大小,每帧的PCM Int16的数量,采样率转换后的pcm长度为frameSize的整数倍,用于连续转换。目前仅在mp3格式时才有用,frameSize取值为1152,这样编码出来的mp3时长和pcm的时长完全一致,否则会因为mp3最后一帧录音不够填满时添加填充数据导致mp3的时长变长。
		frameType:"" 帧类型,一般为rec.set.type,提供此参数时无需提供frameSize,会自动使用最佳的值给frameSize赋值,目前仅支持mp3=1152(MPEG1 Layer3的每帧采采样数),其他类型=1。
			以上两个参数用于连续转换时使用,最多使用一个,不提供时不进行帧的特殊处理,提供时必须同时提供prevChunkInfo才有作用。最后一段数据处理时无需提供帧大小以便输出最后一丁点残留数据。
	}

返回ChunkInfo:{
	//可定义,从指定位置开始转换到结尾
	index:0 pcmDatas已处理到的索引
	offset:0.0 已处理到的index对应的pcm中的偏移的下一个位置
	
	//仅作为返回值
	frameNext:null||[Int16,...] 下一帧的部分数据,frameSize设置了的时候才可能会有
	sampleRate:16000 结果的采样率,<=newSampleRate
	data:[Int16,...] 转换后的PCM结果;如果是连续转换,并且pcmDatas中并没有新数据时,data的长度可能为0
}
*/
Recorder.SampleData=function(pcmDatas,pcmSampleRate,newSampleRate,prevChunkInfo,option){
	prevChunkInfo||(prevChunkInfo={});
	var index=prevChunkInfo.index||0;
	var offset=prevChunkInfo.offset||0;
	
	var frameNext=prevChunkInfo.frameNext||[];
	option||(option={});
	var frameSize=option.frameSize||1;
	if(option.frameType){
		frameSize=option.frameType=="mp3"?1152:1;
	};
	
	var nLen=pcmDatas.length;
	if(index>nLen+1){
		CLog("SampleData似乎传入了未重置chunk "+index+">"+nLen,3);
	};
	var size=0;
	for(var i=index;i<nLen;i++){
		size+=pcmDatas[i].length;
	};
	size=Math.max(0,size-Math.floor(offset));
	
	//采样 https://www.cnblogs.com/blqw/p/3782420.html
	var step=pcmSampleRate/newSampleRate;
	if(step>1){//新采样低于录音采样,进行抽样
		size=Math.floor(size/step);
	}else{//新采样高于录音采样不处理,省去了插值处理
		step=1;
		newSampleRate=pcmSampleRate;
	};
	
	size+=frameNext.length;
	var res=new Int16Array(size);
	var idx=0;
	//添加上一次不够一帧的剩余数据
	for(var i=0;i<frameNext.length;i++){
		res[idx]=frameNext[i];
		idx++;
	};
	//处理数据
	for (;index<nLen;index++) {
		var o=pcmDatas[index];
		var i=offset,il=o.length;
		while(i<il){
			//res[idx]=o[Math.round(i)]; 直接简单抽样
			
			//https://www.cnblogs.com/xiaoqi/p/6993912.html
			//当前点=当前点+到后面一个点之间的增量,音质比直接简单抽样好些
			var before = Math.floor(i);
			var after = Math.ceil(i);
			var atPoint = i - before;
			
			var beforeVal=o[before];
			var afterVal=after<il ? o[after]
				: (//后个点越界了,查找下一个数组
					(pcmDatas[index+1]||[beforeVal])[0]||0
				);
			res[idx]=beforeVal+(afterVal-beforeVal)*atPoint;
			
			idx++;
			i+=step;//抽样
		};
		offset=i-il;
	};
	//帧处理
	frameNext=null;
	var frameNextSize=res.length%frameSize;
	if(frameNextSize>0){
		var u8Pos=(res.length-frameNextSize)*2;
		frameNext=new Int16Array(res.buffer.slice(u8Pos));
		res=new Int16Array(res.buffer.slice(0,u8Pos));
	};
	
	return {
		index:index
		,offset:offset
		
		,frameNext:frameNext
		,sampleRate:newSampleRate
		,data:res
	};
};


/*计算音量百分比的一个方法
pcmAbsSum: pcm Int16所有采样的绝对值的和
pcmLength: pcm长度
返回值:0-100,主要当做百分比用
注意:这个不是分贝,因此没用volume当做名称*/
Recorder.PowerLevel=function(pcmAbsSum,pcmLength){
	/*计算音量 https://blog.csdn.net/jody1989/article/details/73480259
	更高灵敏度算法:
		限定最大感应值10000
			线性曲线:低音量不友好
				power/10000*100 
			对数曲线:低音量友好,但需限定最低感应值
				(1+Math.log10(power/10000))*100
	*/
	var power=(pcmAbsSum/pcmLength) || 0;//NaN
	var level;
	if(power<1251){//1250的结果10%,更小的音量采用线性取值
		level=Math.round(power/1250*10);
	}else{
		level=Math.round(Math.min(100,Math.max(0,(1+Math.log(power/10000)/Math.log(10))*100)));
	};
	return level;
};

/*计算音量,单位dBFS(满刻度相对电平)
maxSample: 为16位pcm采样的绝对值中最大的一个(计算峰值音量),或者为pcm中所有采样的绝对值的平局值
返回值:-100~0 (最大值0dB,最小值-100代替-∞)
*/
Recorder.PowerDBFS=function(maxSample){
	var val=Math.max(0.1, maxSample||0),Pref=0x7FFF;
	val=Math.min(val,Pref);
	//https://www.logiclocmusic.com/can-you-tell-the-decibel/
	//https://blog.csdn.net/qq_17256689/article/details/120442510
	val=20*Math.log(val/Pref)/Math.log(10);
	return Math.max(-100,Math.round(val));
};




//带时间的日志输出,可设为一个空函数来屏蔽日志输出
//CLog(msg,errOrLogMsg, logMsg...) err为数字时代表日志类型1:error 2:log默认 3:warn,否则当做内容输出,第一个参数不能是对象因为要拼接时间,后面可以接无数个输出参数
Recorder.CLog=function(msg,err){
	var now=new Date();
	var t=("0"+now.getMinutes()).substr(-2)
		+":"+("0"+now.getSeconds()).substr(-2)
		+"."+("00"+now.getMilliseconds()).substr(-3);
	var recID=this&&this.envIn&&this.envCheck&&this.id;
	var arr=["["+t+" "+RecTxt+(recID?":"+recID:"")+"]"+msg];
	var a=arguments,console=window.console||{};
	var i=2,fn=console.log;
	if(typeof(err)=="number"){
		fn=err==1?console.error:err==3?console.warn:fn;
	}else{
		i=1;
	};
	for(;i<a.length;i++){
		arr.push(a[i]);
	};
	if(IsLoser){//古董浏览器,仅保证基本的可执行不代码异常
		fn&&fn("[IsLoser]"+arr[0],arr.length>1?arr:"");
	}else{
		fn.apply(console,arr);
	};
};
var CLog=function(){ Recorder.CLog.apply(this,arguments); };
var IsLoser=true;try{IsLoser=!console.log.apply;}catch(e){};




var ID=0;
function initFn(set){
	this.id=++ID;
	
	//如果开启了流量统计,这里将发送一个图片请求
	Traffic();
	
	
	var o={
		type:"mp3" //输出类型:mp3,wav,wav输出文件尺寸超大不推荐使用,但mp3编码支持会导致js文件超大,如果不需支持mp3可以使js文件大幅减小
		,bitRate:16 //比特率 wav:16或8位,MP3:8kbps 1k/s,8kbps 2k/s 录音文件很小
		
		,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
		
		,onProcess:NOOP //fn(buffers,powerLevel,bufferDuration,bufferSampleRate,newBufferIdx,asyncEnd) buffers=[[Int16,...],...]:缓冲的PCM数据,为从开始录音到现在的所有pcm片段;powerLevel:当前缓冲的音量级别0-100,bufferDuration:已缓冲时长,bufferSampleRate:缓冲使用的采样率(当type支持边录边转码(Worker)时,此采样率和设置的采样率相同,否则不一定相同);newBufferIdx:本次回调新增的buffer起始索引;asyncEnd:fn() 如果onProcess是异步的(返回值为true时),处理完成时需要调用此回调,如果不是异步的请忽略此参数,此方法回调时必须是真异步(不能真异步时需用setTimeout包裹)。onProcess返回值:如果返回true代表开启异步模式,在某些大量运算的场合异步是必须的,必须在异步处理完成时调用asyncEnd(不能真异步时需用setTimeout包裹),在onProcess执行后新增的buffer会全部替换成空数组,因此本回调开头应立即将newBufferIdx到本次回调结尾位置的buffer全部保存到另外一个数组内,处理完成后写回buffers中本次回调的结尾位置。
		
		//*******高级设置******
		//,sourceStream:MediaStream Object
				//可选直接提供一个媒体流,从这个流中录制、实时处理音频数据(当前Recorder实例独享此流);不提供时为普通的麦克风录音,由getUserMedia提供音频流(所有Recorder实例共享同一个流)
				//比如:audio、video标签dom节点的captureStream方法(实验特性,不同浏览器支持程度不高)返回的流;WebRTC中的remote流;自己创建的流等
				//注意:流内必须至少存在一条音轨(Audio Track),比如audio标签必须等待到可以开始播放后才会有音轨,否则open会失败
		
		//,audioTrackSet:{ deviceId:"",groupId:"", autoGainControl:true, echoCancellation:true, noiseSuppression:true }
				//普通麦克风录音时getUserMedia方法的audio配置参数,比如指定设备id,回声消除、降噪开关;注意:提供的任何配置值都不一定会生效
				//由于麦克风是全局共享的,所以新配置后需要close掉以前的再重新open
				//更多参考: https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints
		
		//,disableEnvInFix:false 内部参数,禁用设备卡顿时音频输入丢失补偿功能
		
		//,takeoffEncodeChunk:NOOP //fn(chunkBytes) chunkBytes=[Uint8,...]:实时编码环境下接管编码器输出,当编码器实时编码出一块有效的二进制音频数据时实时回调此方法;参数为二进制的Uint8Array,就是编码出来的音频数据片段,所有的chunkBytes拼接在一起即为完整音频。本实现的想法最初由QQ2543775048提出
				//当提供此回调方法时,将接管编码器的数据输出,编码器内部将放弃存储生成的音频数据;环境要求比较苛刻:如果当前环境不支持实时编码处理,将在open时直接走fail逻辑
				//因此提供此回调后调用stop方法将无法获得有效的音频数据,因为编码器内没有音频数据,因此stop时返回的blob将是一个字节长度为0的blob
				//目前只有mp3格式实现了实时编码,在支持实时处理的环境中将会实时的将编码出来的mp3片段通过此方法回调,所有的chunkBytes拼接到一起即为完整的mp3,此种拼接的结果比mock方法实时生成的音质更加,因为天然避免了首尾的静默
				//目前除mp3外其他格式不可以提供此回调,提供了将在open时直接走fail逻辑
	};
	
	for(var k in set){
		o[k]=set[k];
	};
	this.set=o;
	
	this._S=9;//stop同步锁,stop可以阻止open过程中还未运行的start
	this.Sync={O:9,C:9};//和Recorder.Sync一致,只不过这个是非全局的,仅用来简化代码逻辑,无实际作用
};
//同步锁,控制对Stream的竞争;用于close时中断异步的open;一个对象open如果变化了都要阻止close,Stream的控制权交个新的对象
Recorder.Sync={/*open*/O:9,/*close*/C:9};

Recorder.prototype=initFn.prototype={
	CLog:CLog
	
	//流相关的数据存储在哪个对象里面;如果提供了sourceStream,数据直接存储在当前对象中,否则存储在全局
	,_streamStore:function(){
		if(this.set.sourceStream){
			return this;
		}else{
			return Recorder;
		}
	}
	
	//打开录音资源True(),False(msg,isUserNotAllow),需要调用close。注意:此方法是异步的;一般使用时打开,用完立即关闭;可重复调用,可用来测试是否能录音
	,open:function(True,False){
		var This=this,streamStore=This._streamStore();
		True=True||NOOP;
		var failCall=function(errMsg,isUserNotAllow){
			isUserNotAllow=!!isUserNotAllow;
			This.CLog("录音open失败:"+errMsg+",isUserNotAllow:"+isUserNotAllow,1);
			False&&False(errMsg,isUserNotAllow);
		};
		
		var ok=function(){
			This.CLog("open ok id:"+This.id);
			True();
			
			This._SO=0;//解除stop对open中的start调用的阻止
		};
		
		
		//同步锁
		var Lock=streamStore.Sync;
		var lockOpen=++Lock.O,lockClose=Lock.C;
		This._O=This._O_=lockOpen;//记住当前的open,如果变化了要阻止close,这里假定了新对象已取代当前对象并且不再使用
		This._SO=This._S;//记住open过程中的stop,中途任何stop调用后都不能继续open中的start
		var lockFail=function(){
			//允许多次open,但不允许任何一次close,或者自身已经调用了关闭
			if(lockClose!=Lock.C || !This._O){
				var err="open被取消";
				if(lockOpen==Lock.O){
					//无新的open,已经调用了close进行取消,此处应让上次的close明确生效
					This.close();
				}else{
					err="open被中断";
				};
				failCall(err);
				return true;
			};
		};
		
		//环境配置检查
		var checkMsg=This.envCheck({envName:"H5",canProcess:true});
		if(checkMsg){
			failCall("不能录音:"+checkMsg);
			return;
		};
		
		
		//***********已直接提供了音频流************
		if(This.set.sourceStream){
			if(!Recorder.GetContext()){
				failCall("不支持此浏览器从流中获取录音");
				return;
			};
			
			Disconnect(streamStore);//可能已open过,直接先尝试断开
			This.Stream=This.set.sourceStream;
			This.Stream._call={};
			
			try{
				Connect(streamStore);
			}catch(e){
				failCall("从流中打开录音失败:"+e.message);
				return;
			}
			ok();
			return;
		};
		
		
		//***********打开麦克风得到全局的音频流************
		var codeFail=function(code,msg){
			try{//跨域的优先检测一下
				window.top.a;
			}catch(e){
				failCall('无权录音(跨域,请尝试给iframe添加麦克风访问策略,如allow="camera;microphone")');
				return;
			};
			
			if(/Permission|Allow/i.test(code)){
				failCall("用户拒绝了录音权限",true);
			}else if(window.isSecureContext===false){
				failCall("浏览器禁止不安全页面录音,可开启https解决");
			}else if(/Found/i.test(code)){//可能是非安全环境导致的没有设备
				failCall(msg+",无可用麦克风");
			}else{
				failCall(msg);
			};
		};
		
		
		//如果已打开并且有效就不要再打开了
		if(Recorder.IsOpen()){
			ok();
			return;
		};
		if(!Recorder.Support()){
			codeFail("","此浏览器不支持录音");
			return;
		};
				
		//请求权限,如果从未授权,一般浏览器会弹出权限请求弹框
		var f1=function(stream){
			//https://github.com/xiangyuecn/Recorder/issues/14 获取到的track.readyState!="live",刚刚回调时可能是正常的,但过一下可能就被关掉了,原因不明。延迟一下保证真异步。对正常浏览器不影响
			setTimeout(function(){
				stream._call={};
				var oldStream=Recorder.Stream;
				if(oldStream){
					Disconnect(); //直接断开已存在的,旧的Connect未完成会自动终止
					stream._call=oldStream._call;
				};
				Recorder.Stream=stream;
				if(lockFail())return;
				
				if(Recorder.IsOpen()){
					if(oldStream)This.CLog("发现同时多次调用open",1);
					
					Connect(streamStore,1);
					ok();
				}else{
					failCall("录音功能无效:无音频流");
				};
			},100);
		};
		var f2=function(e){
			var code=e.name||e.message||e.code+":"+e;
			This.CLog("请求录音权限错误",1,e);
			
			codeFail(code,"无法录音:"+code);
		};
		
		var trackSet={
			noiseSuppression:false //默认禁用降噪,原声录制,免得移动端表现怪异(包括系统播放声音变小)
			,echoCancellation:false //回声消除
		};
		var trackSet2=This.set.audioTrackSet;
		for(var k in trackSet2)trackSet[k]=trackSet2[k];
		trackSet.sampleRate=Recorder.Ctx.sampleRate;//必须指明采样率,不然手机上MediaRecorder采样率16k
		
		try{
			var pro=Recorder.Scope[getUserMediaTxt]({audio:trackSet},f1,f2);
		}catch(e){//不能设置trackSet就算了
			This.CLog(getUserMediaTxt,3,e);
			pro=Recorder.Scope[getUserMediaTxt]({audio:true},f1,f2);
		};
		if(pro&&pro.then){
			pro.then(f1)[CatchTxt](f2); //fix 关键字,保证catch压缩时保持字符串形式
		};
	}
	//关闭释放录音资源
	,close:function(call){
		call=call||NOOP;
		
		var This=this,streamStore=This._streamStore();
		This._stop();
		
		var Lock=streamStore.Sync;
		This._O=0;
		if(This._O_!=Lock.O){
			//唯一资源Stream的控制权已交给新对象,这里不能关闭。此处在每次都弹权限的浏览器内可能存在泄漏,新对象被拒绝权限可能不会调用close,忽略这种不处理
			This.CLog("close被忽略(因为同时open了多个rec,只有最后一个会真正close)",3);
			call();
			return;
		};
		Lock.C++;//获得控制权
		
		Disconnect(streamStore);
		
		This.CLog("close");
		call();
	}
	
	
	
	
	
	/*模拟一段录音数据,后面可以调用stop进行编码,需提供pcm数据[1,2,3...],pcm的采样率*/
	,mock:function(pcmData,pcmSampleRate){
		var This=this;
		This._stop();//清理掉已有的资源
		
		This.isMock=1;
		This.mockEnvInfo=null;
		This.buffers=[pcmData];
		This.recSize=pcmData.length;
		This[srcSampleRateTxt]=pcmSampleRate;
		return This;
	}
	,envCheck:function(envInfo){//平台环境下的可用性检查,任何时候都可以调用检查,返回errMsg:""正常,"失败原因"
		//envInfo={envName:"H5",canProcess:true}
		var errMsg,This=this,set=This.set;
		
		//检测CPU的数字字节序,TypedArray字节序是个迷,直接拒绝罕见的大端模式,因为找不到这种CPU进行测试
		var tag="CPU_BE";
		if(!errMsg && !Recorder[tag] && window.Int8Array && !new Int8Array(new Int32Array([1]).buffer)[0]){
			Traffic(tag); //如果开启了流量统计,这里将发送一个图片请求
			errMsg="不支持"+tag+"架构";
		};
		
		//编码器检查环境下配置是否可用
		if(!errMsg){
			var type=set.type;
			if(This[type+"_envCheck"]){//编码器已实现环境检查
				errMsg=This[type+"_envCheck"](envInfo,set);
			}else{//未实现检查的手动检查配置是否有效
				if(set.takeoffEncodeChunk){
					errMsg=type+"类型"+(This[type]?"":"(未加载编码器)")+"不支持设置takeoffEncodeChunk";
				};
			};
		};
		
		return errMsg||"";
	}
	,envStart:function(mockEnvInfo,sampleRate){//平台环境相关的start调用
		var This=this,set=This.set;
		This.isMock=mockEnvInfo?1:0;//非H5环境需要启用mock,并提供envCheck需要的环境信息
		This.mockEnvInfo=mockEnvInfo;
		This.buffers=[];//数据缓冲
		This.recSize=0;//数据大小
		
		This.envInLast=0;//envIn接收到最后录音内容的时间
		This.envInFirst=0;//envIn接收到的首个录音内容的录制时间
		This.envInFix=0;//补偿的总时间
		This.envInFixTs=[];//补偿计数列表
		
		//engineCtx需要提前确定最终的采样率
		var setSr=set[sampleRateTxt];
		if(setSr>sampleRate){
			set[sampleRateTxt]=sampleRate;
		}else{ setSr=0 }
		This[srcSampleRateTxt]=sampleRate;
		This.CLog(srcSampleRateTxt+": "+sampleRate+" set."+sampleRateTxt+": "+set[sampleRateTxt]+(setSr?" 忽略"+setSr:""), setSr?3:0);
		
		This.engineCtx=0;
		//此类型有边录边转码(Worker)支持
		if(This[set.type+"_start"]){
			var engineCtx=This.engineCtx=This[set.type+"_start"](set);
			if(engineCtx){
				engineCtx.pcmDatas=[];
				engineCtx.pcmSize=0;
			};
		};
	}
	,envResume:function(){//和平台环境无关的恢复录音
		//重新开始计数
		this.envInFixTs=[];
	}
	,envIn:function(pcm,sum){//和平台环境无关的pcm[Int16]输入
		var This=this,set=This.set,engineCtx=This.engineCtx;
		var bufferSampleRate=This[srcSampleRateTxt];
		var size=pcm.length;
		var powerLevel=Recorder.PowerLevel(sum,size);
		
		var buffers=This.buffers;
		var bufferFirstIdx=buffers.length;//之前的buffer都是经过onProcess处理好的,不允许再修改
		buffers.push(pcm);
		
		//有engineCtx时会被覆盖,这里保存一份
		var buffersThis=buffers;
		var bufferFirstIdxThis=bufferFirstIdx;
		
		//卡顿丢失补偿:因为设备很卡的时候导致H5接收到的数据量不够造成播放时候变速,结果比实际的时长要短,此处保证了不会变短,但不能修复丢失的音频数据造成音质变差。当前算法采用输入时间侦测下一帧是否需要添加补偿帧,需要(6次输入||超过1秒)以上才会开始侦测,如果滑动窗口内丢失超过1/3就会进行补偿
		var now=Date.now();
		var pcmTime=Math.round(size/bufferSampleRate*1000);
		This.envInLast=now;
		if(This.buffers.length==1){//记下首个录音数据的录制时间
			This.envInFirst=now-pcmTime;
		};
		var envInFixTs=This.envInFixTs;
		envInFixTs.splice(0,0,{t:now,d:pcmTime});
		//保留3秒的计数滑动窗口,另外超过3秒的停顿不补偿
		var tsInStart=now,tsPcm=0;
		for(var i=0;i<envInFixTs.length;i++){
			var o=envInFixTs[i];
			if(now-o.t>3000){
				envInFixTs.length=i;
				break;
			};
			tsInStart=o.t;
			tsPcm+=o.d;
		};
		//达到需要的数据量,开始侦测是否需要补偿
		var tsInPrev=envInFixTs[1];
		var tsIn=now-tsInStart;
		var lost=tsIn-tsPcm;
		if( lost>tsIn/3 && (tsInPrev&&tsIn>1000 || envInFixTs.length>=6) ){
			//丢失过多,开始执行补偿
			var addTime=now-tsInPrev.t-pcmTime;//距离上次输入丢失这么多ms
			if(addTime>pcmTime/5){//丢失超过本帧的1/5
				var fixOpen=!set.disableEnvInFix;
				This.CLog("["+now+"]"+(fixOpen?"":"未")+"补偿"+addTime+"ms",3);
				This.envInFix+=addTime;
				
				//用静默进行补偿
				if(fixOpen){
					var addPcm=new Int16Array(addTime*bufferSampleRate/1000);
					size+=addPcm.length;
					buffers.push(addPcm);
				};
			};
		};
		
		
		var sizeOld=This.recSize,addSize=size;
		var bufferSize=sizeOld+addSize;
		This.recSize=bufferSize;//此值在onProcess后需要修正,可能新数据被修改
		
		
		//此类型有边录边转码(Worker)支持,开启实时转码
		if(engineCtx){
			//转换成set的采样率
			var chunkInfo=Recorder.SampleData(buffers,bufferSampleRate,set[sampleRateTxt],engineCtx.chunkInfo);
			engineCtx.chunkInfo=chunkInfo;
			
			sizeOld=engineCtx.pcmSize;
			addSize=chunkInfo.data.length;
			bufferSize=sizeOld+addSize;
			engineCtx.pcmSize=bufferSize;//此值在onProcess后需要修正,可能新数据被修改
			
			buffers=engineCtx.pcmDatas;
			bufferFirstIdx=buffers.length;
			buffers.push(chunkInfo.data);
			bufferSampleRate=chunkInfo[sampleRateTxt];
		};
		
		var duration=Math.round(bufferSize/bufferSampleRate*1000);
		var bufferNextIdx=buffers.length;
		var bufferNextIdxThis=buffersThis.length;
		
		//允许异步处理buffer数据
		var asyncEnd=function(){
			//重新计算size,异步的早已减去添加的,同步的需去掉本次添加的然后重新计算
			var num=asyncBegin?0:-addSize;
			var hasClear=buffers[0]==null;
			for(var i=bufferFirstIdx;i<bufferNextIdx;i++){
				var buffer=buffers[i];
				if(buffer==null){//已被主动释放内存,比如长时间实时传输录音时
					hasClear=1;
				}else{
					num+=buffer.length;
					
					//推入后台边录边转码
					if(engineCtx&&buffer.length){
						This[set.type+"_encode"](engineCtx,buffer);
					};
				};
			};
			
			//同步清理This.buffers,不管buffers到底清了多少个,buffersThis是使用不到的进行全清
			if(hasClear && engineCtx){
				var i=bufferFirstIdxThis;
				if(buffersThis[0]){
					i=0;
				};
				for(;i<bufferNextIdxThis;i++){
					buffersThis[i]=null;
				};
			};
			
			//统计修改后的size,如果异步发生clear要原样加回来,同步的无需操作
			if(hasClear){
				num=asyncBegin?addSize:0;
				
				buffers[0]=null;//彻底被清理
			};
			if(engineCtx){
				engineCtx.pcmSize+=num;
			}else{
				This.recSize+=num;
			};
		};
		//实时回调处理数据,允许修改或替换上次回调以来新增的数据 ,但是不允许修改已处理过的,不允许增删第一维数组 ,允许将第二维数组任意修改替换成空数组也可以
		var asyncBegin=0,procTxt="rec.set.onProcess";
		try{
			asyncBegin=set.onProcess(buffers,powerLevel,duration,bufferSampleRate,bufferFirstIdx,asyncEnd);
		}catch(e){
			//此错误显示不要用CLog,这样控制台内相同内容不会重复打印
			console.error(procTxt+"回调出错是不允许的,需保证不会抛异常",e);
		};
		
		var slowT=Date.now()-now;
		if(slowT>10 && This.envInFirst-now>1000){ //1秒后开始onProcess性能监测
			This.CLog(procTxt+"低性能,耗时"+slowT+"ms",3);
		};
		
		if(asyncBegin===true){
			//开启了异步模式,onProcess已接管buffers新数据,立即清空,避免出现未处理的数据
			var hasClear=0;
			for(var i=bufferFirstIdx;i<bufferNextIdx;i++){
				if(buffers[i]==null){//已被主动释放内存,比如长时间实时传输录音时 ,但又要开启异步模式,此种情况是非法的
					hasClear=1;
				}else{
					buffers[i]=new Int16Array(0);
				};
			};
			
			if(hasClear){
				This.CLog("未进入异步前不能清除buffers",3);
			}else{
				//还原size,异步结束后再统计仅修改后的size,如果发生clear要原样加回来
				if(engineCtx){
					engineCtx.pcmSize-=addSize;
				}else{
					This.recSize-=addSize;
				};
			};
		}else{
			asyncEnd();
		};
	}
	
	
	
	
	//开始录音,需先调用open;只要open成功时,调用此方法是安全的,如果未open强行调用导致的内部错误将不会有任何提示,stop时自然能得到错误
	,start:function(){
		var This=this,ctx=Recorder.Ctx;
		
		var isOpen=1;
		if(This.set.sourceStream){//直接提供了流,仅判断是否调用了open
			if(!This.Stream){
				isOpen=0;
			}
		}else if(!Recorder.IsOpen()){//监测全局麦克风是否打开并且有效
			isOpen=0;
		};
		if(!isOpen){
			This.CLog("未open",1);
			return;
		};
		This.CLog("开始录音");
		
		This._stop();
		This.state=3;//0未录音 1录音中 2暂停 3等待ctx激活
		This.envStart(null, ctx[sampleRateTxt]);
		
		//检查open过程中stop是否已经调用过
		if(This._SO&&This._SO+1!=This._S){//上面调用过一次 _stop
			//open未完成就调用了stop,此种情况终止start。也应尽量避免出现此情况
			This.CLog("start被中断",3);
			return;
		};
		This._SO=0;
		
		var end=function(){
			if(This.state==3){
				This.state=1;
				This.resume();
			}
		};
		if(ctx.state=="suspended"){
			var tag="AudioContext resume: ";
			This.CLog(tag+"wait...");
			ctx.resume().then(function(){
				This.CLog(tag+ctx.state);
				end();
			})[CatchTxt](function(e){ //比较少见,可能对录音没有影响
				This.CLog(tag+ctx.state+" 可能无法录音:"+e.message,1,e);
				end();
			});
		}else{
			end();
		};
	}
	/*暂停录音*/
	,pause:function(){
		var This=this;
		if(This.state){
			This.state=2;
			This.CLog("pause");
			delete This._streamStore().Stream._call[This.id];
		};
	}
	/*恢复录音*/
	,resume:function(){
		var This=this;
		if(This.state){
			This.state=1;
			This.CLog("resume");
			This.envResume();
			
			var stream=This._streamStore().Stream;
			stream._call[This.id]=function(pcm,sum){
				if(This.state==1){
					This.envIn(pcm,sum);
				};
			};
			ConnAlive(stream);//AudioWorklet只会在ctx激活后运行
		};
	}
	
	
	
	
	,_stop:function(keepEngine){
		var This=this,set=This.set;
		if(!This.isMock){
			This._S++;
		};
		if(This.state){
			This.pause();
			This.state=0;
		};
		if(!keepEngine && This[set.type+"_stop"]){
			This[set.type+"_stop"](This.engineCtx);
			This.engineCtx=0;
		};
	}
	/*
	结束录音并返回录音数据blob对象
		True(blob,duration) blob:录音数据audio/mp3|wav格式
							duration:录音时长,单位毫秒
		False(msg)
		autoClose:false 可选,是否自动调用close,默认为false
	*/
	,stop:function(True,False,autoClose){
		var This=this,set=This.set,t1;
		var envInMS=This.envInLast-This.envInFirst, envInLen=envInMS&&This.buffers.length; //可能未start
		This.CLog("stop 和start时差"+(envInMS?envInMS+"ms 补偿"+This.envInFix+"ms"+" envIn:"+envInLen+" fps:"+(envInLen/envInMS*1000).toFixed(1):"-"));
		
		var end=function(){
			This._stop();//彻底关掉engineCtx
			if(autoClose){
				This.close();
			};
		};
		var err=function(msg){
			This.CLog("结束录音失败:"+msg,1);
			False&&False(msg);
			end();
		};
		var ok=function(blob,duration){
			This.CLog("结束录音 编码花"+(Date.now()-t1)+"ms 音频时长"+duration+"ms 文件大小"+blob.size+"b");
			if(set.takeoffEncodeChunk){//接管了输出,此时blob长度为0
				This.CLog("启用takeoffEncodeChunk后stop返回的blob长度为0不提供音频数据",3);
			}else if(blob.size<Math.max(100,duration/2)){//1秒小于0.5k?
				err("生成的"+set.type+"无效");
				return;
			};
			True&&True(blob,duration);
			end();
		};
		if(!This.isMock){
			var isCtxWait=This.state==3;
			if(!This.state || isCtxWait){
				err("未开始录音"+(isCtxWait?",开始录音前无用户交互导致AudioContext未运行":""));
				return;
			};
			This._stop(true);
		};
		var size=This.recSize;
		if(!size){
			err("未采集到录音");
			return;
		};
		if(!This.buffers[0]){
			err("音频buffers被释放");
			return;
		};
		if(!This[set.type]){
			err("未加载"+set.type+"编码器");
			return;
		};
		
		//环境配置检查,此处仅针对mock调用,因为open已经检查过了
		if(This.isMock){
			var checkMsg=This.envCheck(This.mockEnvInfo||{envName:"mock",canProcess:false});//没有提供环境信息的mock时没有onProcess回调
			if(checkMsg){
				err("录音错误:"+checkMsg);
				return;
			};
		};
		
		//此类型有边录边转码(Worker)支持
		var engineCtx=This.engineCtx;
		if(This[set.type+"_complete"]&&engineCtx){
			var duration=Math.round(engineCtx.pcmSize/set[sampleRateTxt]*1000);//采用后的数据长度和buffers的长度可能微小的不一致,是采样率连续转换的精度问题
			
			t1=Date.now();
			This[set.type+"_complete"](engineCtx,function(blob){
				ok(blob,duration);
			},err);
			return;
		};
		
		//标准UI线程转码,调整采样率
		t1=Date.now();
		var chunk=Recorder.SampleData(This.buffers,This[srcSampleRateTxt],set[sampleRateTxt]);
		
		set[sampleRateTxt]=chunk[sampleRateTxt];
		var res=chunk.data;
		var duration=Math.round(res.length/set[sampleRateTxt]*1000);
		
		This.CLog("采样"+size+"->"+res.length+" 花:"+(Date.now()-t1)+"ms");
		
		setTimeout(function(){
			t1=Date.now();
			This[set.type](res,function(blob){
				ok(blob,duration);
			},function(msg){
				err(msg);
			});
		});
	}

};

if(window[RecTxt]){
	CLog("重复引入"+RecTxt,3);
	window[RecTxt].Destroy();
};
window[RecTxt]=Recorder;




//=======从WebM字节流中提取pcm数据,提取成功返回Float32Array,失败返回null||-1=====
var WebM_Extract=function(inBytes, scope){
	if(!scope.pos){
		scope.pos=[0]; scope.tracks={}; scope.bytes=[];
	};
	var tracks=scope.tracks, position=[scope.pos[0]];
	var endPos=function(){ scope.pos[0]=position[0] };
	
	var sBL=scope.bytes.length;
	var bytes=new Uint8Array(sBL+inBytes.length);
	bytes.set(scope.bytes); bytes.set(inBytes,sBL);
	scope.bytes=bytes;
	
	//先读取文件头和Track信息
	if(!scope._ht){
		readMatroskaVInt(bytes, position);//EBML Header
		readMatroskaBlock(bytes, position);//跳过EBML Header内容
		if(!BytesEq(readMatroskaVInt(bytes, position), [0x18,0x53,0x80,0x67])){
			return;//未识别到Segment
		}
		readMatroskaVInt(bytes, position);//跳过Segment长度值
		while(position[0]<bytes.length){
			var eid0=readMatroskaVInt(bytes, position);
			var bytes0=readMatroskaBlock(bytes, position);
			var pos0=[0],audioIdx=0;
			if(!bytes0)return;//数据不全,等待缓冲
			//Track完整数据,循环读取TrackEntry
			if(BytesEq(eid0, [0x16,0x54,0xAE,0x6B])){
				while(pos0[0]<bytes0.length){
					var eid1=readMatroskaVInt(bytes0, pos0);
					var bytes1=readMatroskaBlock(bytes0, pos0);
					var pos1=[0],track={channels:0,sampleRate:0};
					if(BytesEq(eid1, [0xAE])){//TrackEntry
						while(pos1[0]<bytes1.length){
							var eid2=readMatroskaVInt(bytes1, pos1);
							var bytes2=readMatroskaBlock(bytes1, pos1);
							var pos2=[0];
							if(BytesEq(eid2, [0xD7])){//Track Number
								var val=BytesInt(bytes2);
								track.number=val;
								tracks[val]=track;
							}else if(BytesEq(eid2, [0x83])){//Track Type
								var val=BytesInt(bytes2);
								if(val==1) track.type="video";
								else if(val==2) {
									track.type="audio";
									if(!audioIdx) scope.track0=track;
									track.idx=audioIdx++;
								}else track.type="Type-"+val;
							}else if(BytesEq(eid2, [0x86])){//Track Codec
								var str="";
								for(var i=0;i<bytes2.length;i++){
									str+=String.fromCharCode(bytes2[i]);
								}
								track.codec=str;
							}else if(BytesEq(eid2, [0xE1])){
								while(pos2[0]<bytes2.length){//循环读取 Audio 属性
									var eid3=readMatroskaVInt(bytes2, pos2);
									var bytes3=readMatroskaBlock(bytes2, pos2);
									//采样率、位数、声道数
									if(BytesEq(eid3, [0xB5])){
										var val=0,arr=new Uint8Array(bytes3.reverse()).buffer;
										if(bytes3.length==4) val=new Float32Array(arr)[0];
										else if(bytes3.length==8) val=new Float64Array(arr)[0];
										else CLog("WebM Track !Float",1,bytes3);
										track[sampleRateTxt]=Math.round(val);
									}else if(BytesEq(eid3, [0x62,0x64])) track.bitDepth=BytesInt(bytes3);
									else if(BytesEq(eid3, [0x9F])) track.channels=BytesInt(bytes3);
								}
							}
						}
					}
				};
				scope._ht=1;
				CLog("WebM Tracks",tracks);
				endPos();
				break;
			}
		}
	}
	
	//校验音频参数信息,如果不符合代码要求,统统拒绝处理
	var track0=scope.track0;
	if(!track0)return;
	if(track0.bitDepth==16 && /FLOAT/i.test(track0.codec)){
		track0.bitDepth=32; //chrome v66 实际为浮点数
		CLog("WebM 16改32位",3);
	}
	if(track0[sampleRateTxt]!=scope[sampleRateTxt] || track0.bitDepth!=32 || track0.channels<1 || !/(\b|_)PCM\b/i.test(track0.codec)){
		scope.bytes=[];//格式非预期 无法处理,清空缓冲数据
		if(!scope.bad)CLog("WebM Track非预期",3,scope);
		scope.bad=1;
		return -1;
	}
	
	//循环读取Cluster内的SimpleBlock
	var datas=[],dataLen=0;
	while(position[0]<bytes.length){
		var eid1=readMatroskaVInt(bytes, position);
		var bytes1=readMatroskaBlock(bytes, position);
		if(!bytes1)break;//数据不全,等待缓冲
		if(BytesEq(eid1, [0xA3])){//SimpleBlock完整数据
			var trackNo=bytes1[0]&0xf;
			var track=tracks[trackNo];
			if(!track){//不可能没有,数据出错?
				CLog("WebM !Track"+trackNo,1,tracks);
			}else if(track.idx===0){
				var u8arr=new Uint8Array(bytes1.length-4);
				for(var i=4;i<bytes1.length;i++){
					u8arr[i-4]=bytes1[i];
				}
				datas.push(u8arr); dataLen+=u8arr.length;
			}
		}
		endPos();
	}
	
	if(dataLen){
		var more=new Uint8Array(bytes.length-scope.pos[0]);
		more.set(bytes.subarray(scope.pos[0]));
		scope.bytes=more; //清理已读取了的缓冲数据
		scope.pos[0]=0;
		
		var u8arr=new Uint8Array(dataLen); //已获取的音频数据
		for(var i=0,i2=0;i<datas.length;i++){
			u8arr.set(datas[i],i2);
			i2+=datas[i].length;
		}
		var arr=new Float32Array(u8arr.buffer);
		
		if(track0.channels>1){//多声道,提取一个声道
			var arr2=[];
			for(var i=0;i<arr.length;){
				arr2.push(arr[i]);
				i+=track0.channels;
			}
			arr=new Float32Array(arr2);
		};
		return arr;
	}
};
//两个字节数组内容是否相同
var BytesEq=function(bytes1,bytes2){
	if(!bytes1 || bytes1.length!=bytes2.length) return false;
	if(bytes1.length==1) return bytes1[0]==bytes2[0];
	for(var i=0;i<bytes1.length;i++){
		if(bytes1[i]!=bytes2[i]) return false;
	}
	return true;
};
//字节数组BE转成int数字
var BytesInt=function(bytes){
	var s="";//0-8字节,js位运算只支持4字节
	for(var i=0;i<bytes.length;i++){var n=bytes[i];s+=(n<16?"0":"")+n.toString(16)};
	return parseInt(s,16)||0;
};
//读取一个可变长数值字节数组
var readMatroskaVInt=function(arr,pos,trim){
	var i=pos[0];
	if(i>=arr.length)return;
	var b0=arr[i],b2=("0000000"+b0.toString(2)).substr(-8);
	var m=/^(0*1)(\d*)$/.exec(b2);
	if(!m)return;
	var len=m[1].length, val=[];
	if(i+len>arr.length)return;
	for(var i2=0;i2<len;i2++){ val[i2]=arr[i]; i++; }
	if(trim) val[0]=parseInt(m[2]||'0',2);
	pos[0]=i;
	return val;
};
//读取一个自带长度的内容字节数组
var readMatroskaBlock=function(arr,pos){
	var lenVal=readMatroskaVInt(arr,pos,1);
	if(!lenVal)return;
	var len=BytesInt(lenVal);
	var i=pos[0], val=[];
	if(len<0x7FFFFFFF){ //超大值代表没有长度
		if(i+len>arr.length)return;
		for(var i2=0;i2<len;i2++){ val[i2]=arr[i]; i++; }
	}
	pos[0]=i;
	return val;
};
//=====End WebM读取=====




//流量统计用1像素图片地址,设置为空将不参与统计
Recorder.TrafficImgUrl="//ia.51.la/go1?id=20469973&pvFlag=1";
var Traffic=Recorder.Traffic=function(report){
	report=report?"/"+RecTxt+"/Report/"+report:"";
	var imgUrl=Recorder.TrafficImgUrl;
	if(imgUrl){
		var data=Recorder.Traffic;
		var m=/^(https?:..[^\/#]*\/?)[^#]*/i.exec(location.href)||[];
		var host=(m[1]||"http://file/");
		var idf=(m[0]||host)+report;
		
		if(imgUrl.indexOf("//")==0){
			//给url加上http前缀,如果是file协议下,不加前缀没法用
			if(/^https:/i.test(idf)){
				imgUrl="https:"+imgUrl;
			}else{
				imgUrl="http:"+imgUrl;
			};
		};
		if(report){
			imgUrl=imgUrl+"&cu="+encodeURIComponent(host+report);
		};
		
		if(!data[idf]){
			data[idf]=1;
			
			var img=new Image();
			img.src=imgUrl;
			CLog("Traffic Analysis Image: "+(report||RecTxt+".TrafficImgUrl="+Recorder.TrafficImgUrl));
		};
	};
};

}));