因为该死的Unity不支持WebGL的麦克风,所以只能向网页借力,用网页原生的录音,然后传音频流给Unity进行转AudioClip播放。
还有一点非常重要:能有同事借力就直接问,厚着脸皮上,我自己闷头两天带加班,不如同事谭老哥加起来提供帮助的俩小时,很感谢他,虽然是他们该做的,但我一直没提出,而且我方向错了????????????
感谢博主Jeffrey_Chou的纠错,部分错误已纠正。
版本:
Unity:2021.3.6f1
Gtiee库:UnityWebGLMicrophone - Gitee
Github库:UnityWebGLMicrophone - Git
相关代码
Unity端的.cs .jslib和WebGL端的.js.
.jslib
这个需要放在Unity的Plugins文件夹下
mergeInto(, {
StartRecord: function(){
StartRecord();
},
StopRecord: function(){
StopRecord();
},
});
.cs
public class WAV
{
static float bytesToFloat(byte firstByte, byte secondByte)
{
short s = (short)((secondByte << 8) | firstByte);
return s / 32768.0F;
}
static int bytesToInt(byte[] bytes, int offset = 0)
{
int value = 0;
for (int i = 0; i < 4; i++)
{
value |= ((int)bytes[offset + i]) << (i * 8);
}
return value;
}
public float[] LeftChannel { get; internal set; }
public float[] RightChannel { get; internal set; }
public int ChannelCount { get; internal set; }
public int SampleCount { get; internal set; }
public int Frequency { get; internal set; }
public WAV(byte[] wav)
{
ChannelCount = wav[22];
Frequency = bytesToInt(wav, 24);
int pos = 12;
while (!(wav[pos] == 100 && wav[pos + 1] == 97 && wav[pos + 2] == 116 && wav[pos + 3] == 97))
{
pos += 4;
int chunkSize = wav[pos] + wav[pos + 1] * 256 + wav[pos + 2] * 65536 + wav[pos + 3] * 16777216;
pos += 4 + chunkSize;
}
pos += 8;
SampleCount = ( - pos) / 2;
if (ChannelCount == 2) SampleCount /= 2;
LeftChannel = new float[SampleCount];
if (ChannelCount == 2) RightChannel = new float[SampleCount];
else RightChannel = null;
int i = 0;
int maxInput = - (RightChannel == null ? 1 : 3);
while ((i < SampleCount) && (pos < maxInput))
{
LeftChannel[i] = bytesToFloat(wav[pos], wav[pos + 1]);
pos += 2;
if (ChannelCount == 2)
{
RightChannel[i] = bytesToFloat(wav[pos], wav[pos + 1]);
pos += 2;
}
i++;
}
}
public override string ToString()
{
return ("[WAV: LeftChannel={0}, RightChannel={1}, ChannelCount={2}, SampleCount={3}, Frequency={4}]", LeftChannel, RightChannel, ChannelCount, SampleCount, Frequency);
}
}
using System;
using ;
using ;
using ;
using UnityEngine;
using ;
public class SignalManager : MonoBehaviour
{
public Button StartRecorder;
public Button EndRecorder;
AudioSource m_audioSource;
void Start()
{
m_audioSource = <AudioSource>();
(StartRecorderFunc);
(EndRecorderFunc);
}
#region UnityToJs
[DllImport("__Internal")]
private static extern void StartRecord();
[DllImport("__Internal")]
private static extern void StopRecord();
void StartRecorderFunc()
{
StartRecord();
}
void EndRecorderFunc()
{
StopRecord();
}
#endregion
#region JsToUnity
#region Data
/// <summary>
///需获取数据的数目
/// </summary>
private int m_valuePartCount = 0;
/// <summary>
/// 获取的数据数目
/// </summary>
private int m_getDataLength = 0;
/// <summary>
/// 获取的数据长度
/// </summary>
private int m_audioLength = 0;
/// <summary>
/// 获取的数据
/// </summary>
private string[] m_audioData = null;
/// <summary>
/// 当前音频
/// </summary>
public static AudioClip m_audioClip = null;
/// <summary>
/// 音频片段存放列表
/// </summary>
private List<byte[]> m_audioClipDataList = new List<byte[]>();
/// <summary>
/// 片段结束标记
/// </summary>
private string m_currentRecorderSign;
/// <summary>
/// 音频频率
/// </summary>
private int m_audioFrequency;
/// <summary>
/// 单次最大录制时间
/// </summary>
private const int maxRecordTime = 30;
#endregion
public void GetAudioData(string _audioDataString)
{
if (_audioDataString.Contains("Head"))
{
string[] _headValue = _audioDataString.Split('|');
m_valuePartCount = (_headValue[1]);
m_audioLength = (_headValue[2]);
m_currentRecorderSign = _headValue[3];
m_audioData = new string[m_valuePartCount];
m_getDataLength = 0;
("接收数据头:" + m_valuePartCount + " " + m_audioLength);
}
else if (_audioDataString.Contains("Part"))
{
string[] _headValue = _audioDataString.Split('|');
int _dataIndex = (_headValue[1]);
m_audioData[_dataIndex] = _headValue[2];
m_getDataLength++;
if (m_getDataLength == m_valuePartCount)
{
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < m_audioData.Length; i++)
{
(m_audioData[i]);
}
string _audioDataValue = ();
("接收长度:" + _audioDataValue.Length + " 需接收长度:" + m_audioLength);
int _index = _audioDataValue.LastIndexOf(',');
string _value = _audioDataValue.Substring(_index + 1, _audioDataValue.Length - _index - 1);
byte[] data = Convert.FromBase64String(_value);
("已接收长度 :" + );
if (m_currentRecorderSign == "end")
{
int _audioLength = ;
for (int i = 0; i < m_audioClipDataList.Count; i++)
{
_audioLength += m_audioClipDataList[i].Length;
}
byte[] _audioData = new byte[_audioLength];
("总长度 :" + _audioLength);
int _audioIndex = 0;
(_audioData, _audioIndex);
_audioIndex += ;
("已赋值0:" + _audioIndex);
for (int i = 0; i < m_audioClipDataList.Count; i++)
{
m_audioClipDataList[i].CopyTo(_audioData, _audioIndex);
_audioIndex += m_audioClipDataList[i].Length;
("已赋值 :" + _audioIndex);
}
WAV wav = new WAV(_audioData);
AudioClip _audioClip = ("TestWAV", , 1, , false);
_audioClip.SetData(, 0);
m_audioClip = _audioClip;
("音频设置成功,已设置到unity。" + m_audioClip.length + " " + m_audioClip.name);
m_audioSource.clip = m_audioClip;
m_audioSource.Play();
m_audioClipDataList.Clear();
}
m_audioData = null;
}
}
}
#endregion
}
.js
这个脚本中的内容直接增加到文件中
<script src="./"></script>
// 全局录音实例
let RecorderIns = null;
//全局Unity实例 (全局找 unityInstance , 然后等于它就行)
let UnityIns = null;
// 初始化 , 记得调用
function initRecord(opt = {}) {
let defaultOpt = {
serviceCode: "asr_aword",
audioFormat: "wav",
sampleRate: 16000,
sampleBit: 16,
audioChannels: 1,
bitRate: 96000,
audioData: null,
punctuation: "true",
model: null,
intermediateResult: null,
maxStartSilence: null,
maxEndSilence: null,
};
let options = ({}, defaultOpt, opt);
let sampleRate = || 8000;
let bitRate = parseInt( / 1000) || 16;
if (RecorderIns) {
();
}
RecorderIns = Recorder({
type: "wav",
sampleRate: sampleRate,
bitRate: bitRate,
onProcess(buffers, powerLevel, bufferDuration, bufferSampleRate) {
// 60秒时长限制
const LEN = 59 * 1000;
if (bufferDuration > LEN) {
();
}
},
});
(
() => {
// 打开麦克风授权获得相关资源
("打开麦克风成功");
},
(msg, isUserNotAllow) => {
// 用户拒绝未授权或不支持
((isUserNotAllow ? "UserNotAllow," : "") + "无法录音:" + msg);
}
);
}
// 开始
function StartRecord() {
();
}
// 结束
function StopRecord() {
(
(blob, duration) => {
(
blob,
(blob),
"时长:" + duration + "ms"
);
sendWavData(blob)
},
(msg) => {
("录音失败:" + msg);
}
);
}
// 切片像unity发送音频数据
function sendWavData(blob) {
var reader = new FileReader();
= function (e) {
var _value = ;
var _partLength = 8192;
var _length = parseInt(_value.length / _partLength);
if (_length * _partLength < _value.length) _length += 1;
var _head = "Head|" + _length.toString() + "|" + _value.() + "|end" ;
// 发送数据头
("SignalManager", "GetAudioData", _head);
for (var i = 0; i < _length; i++) {
var _sendValue = "";
if (i < _length - 1) {
_sendValue = _value.substr(i * _partLength, _partLength);
} else {
_sendValue = _value.substr(
i * _partLength,
_value.length - i * _partLength
);
}
_sendValue = "Part|" + () + "|" + _sendValue;
// 发送分片数据
("SignalManager", "GetAudioData", _sendValue);
}
_value = null;
};
(blob);
}
这个直接创建脚本放到同级目录下
/*
录音
/xiangyuecn/Recorder
src: ,engine/
*/
!function(h){"use strict";var d=function(){},A=function(e){return new t(e)};=function(){var e=;if(e){var t=&&()||||[],n=t[0];if(n){var r=;return"live"==r||r==}}return!1},=4096,=function(){for(var e in F("Recorder Destroy"),g(),n)n[e]()};var n={};=function(e,t){n[e]=t},=function(){var e=;if(e||(e=),!e)return!1;var t=||{};return ||(t=navigator).getUserMedia||(=||||),!!&&(=t,&&"closed"!=||(=new e,("Ctx",function(){var e=;e&&&&((),=0)})),!0)};var S=function(e){var t=(e=e||A).BufferSize||,n=,r=,a=r._m=(r),o=r._p=(||).call(n,t,1,1);(o),();var f=r._call;=function(e){for(var t in f){for(var n=(0),r=,a=new Int16Array(r),o=0,s=0;s<r;s++){var i=(-1,(1,n[s]));i=i<0?32768*i:32767*i,a[s]=i,o+=(i)}for(var c in f)f[c](a,o);return}}},g=function(e){var t=(e=e||A)==A,n=;if(n&&(n._m&&(n._m.disconnect(),n._p.disconnect(),n._p.onaudioprocess=n._p=n._m=null),t)){for(var r=&&()||||[],a=0;a<;a++){var o=r[a];&&()}&&()}=0};=function(e,t,n,r,a){r||(r={});var o=||0,s=||0,i=||[];a||(a={});var c=||1;&&(c="mp3"==?1152:1);for(var f=0,u=o;u<;u++)f+=e[u].length;f=(0,(s));var l=t/n;1<l?f=(f/l):(l=1,n=t),f+=;for(var v=new Int16Array(f),p=0,u=0;u<;u++)v[p]=i[u],p++;for(var m=;o<m;o++){for(var h=e[o],u=s,d=;u<d;){var S=(u),g=(u),_=u-S,y=h[S],I=g<d?h[g]:(e[o+1]||[y])[0]||0;v[p]=y+(I-y)*_,p++,u+=l}s=u-d}i=null;var M=%c;if(0<M){var x=2*(-M);i=new Int16Array((x)),v=new Int16Array((0,x))}return{index:o,offset:s,frameNext:i,sampleRate:n,data:v}},=function(e,t){var n=e/t||0;return n<1251?(n/1250*10):((100,(0,100*(1+(n/1e4)/(10)))))};var F=function(e,t){var n=new Date,r=("0"+()).substr(-2)+":"+("0"+()).substr(-2)+"."+("00"+()).substr(-3),a=["["+r+" Recorder]"+e],o=arguments,s=2,i=;for("number"==typeof t?i=1==t?:3==t?:i:s=1;s<;s++)(o[s]);(console,a)};=F;var r=0;function t(e){=++r,&&();var t={type:"mp3",bitRate:16,sampleRate:16e3,onProcess:d};for(var n in e)t[n]=e[n];=t,this._S=9,={O:9,C:9}}={O:9,C:9},=={_streamStore:function(){return ?this:A},open:function(e,n){var t=this,r=t._streamStore();e=e||d;var a=function(e,t){F("录音open失败:"+e+",isUserNotAllow:"+(t=!!t),1),n&&n(e,t)},o=function(){F("open成功"),e(),t._SO=0},s=,i=++,c=;t._O=t._O_=i,t._SO=t._S;var f=function(){if(c!=||!t._O){var e="open被取消";return i==?():e="open被中断",a(e),!0}},u=({envName:"H5",canProcess:!0});if(u)a("不能录音:"+u);else if(){if(!())return void a("不支持此浏览器从流中获取录音");g(r),=,._call={};try{S(r)}catch(e){return void a("从流中打开录音失败:"+)}o()}else{var l=function(e,t){try{}catch(e){return void a('无权录音(跨域,请尝试给iframe添加麦克风访问策略,如allow="camera;microphone")')}/Permission|Allow/(e)?a("用户拒绝了录音权限",!0):!1===?a("无权录音(需https)"):/Found/(e)?a(t+",无可用麦克风"):a(t)};if(())o();else if(()){var v=function(e){(=e)._call={},f()||setTimeout(function(){f()||(()?(S(),o()):a("录音功能无效:无音频流"))},100)},p=function(e){var t=||||+":"+e;F("请求录音权限错误",1,e),l(t,"无法录音:"+t)},m=({audio:||!0},v,p);m&&&&(v)[e&&"catch"](p)}else l("","此浏览器不支持录音")}},close:function(e){e=e||d;var t=this._streamStore();this._stop();var n=;if(this._O=0,this._O_!=)return F("close被忽略",3),void e();++,g(t),F("close"),e()},mock:function(e,t){var n=this;return n._stop(),=1,=null,=[e],=,=t,n},envCheck:function(e){var t,n=;return t||(this[+"_envCheck"]?t=this[+"_envCheck"](e,n):&&(t=+"类型不支持设置takeoffEncodeChunk")),t||""},envStart:function(e,t){var n=this,r=;if(=e?1:0,=e,=[],=0,=0,=0,=0,=[],=(t,),=t,=0,n[+"_start"]){var a==n[+"_start"](r);a&&(=[],=0)}},envResume:function(){=[]},envIn:function(e,t){var a=this,o=,s=,n=,r=,i=(t,r),c=,f=;(e);var u=c,l=f,v=(),p=(r/n*1e3);=v,1==&&(=v-p);var m=;(0,0,{t:v,d:p});for(var h=v,d=0,S=0;S<;S++){var g=m[S];if(3e3<){=S;break}h=,d+=}var _=m[1],y=v-h;if(y/3<y-d&&(_&&1e3<y||6<=)){var I=v-_.t-p;if(p/5<I){var M=!;if(F("["+v+"]"+(M?"":"未")+"补偿"+I+"ms",3),+=I,M){var x=new Int16Array(I*n/1e3);r+=,(x)}}}var k=,C=r,w=k+C;if(=w,s){var R=(c,n,,);=R,w=(k=)+(C=),=w,c=,f=,(),n=}var b=(w/n*1e3),T=,z=,D=function(){for(var e=O?0:-C,t=null==c[0],n=f;n<T;n++){var r=c[n];null==r?t=1:(e+=,s&&&&a[+"_encode"](s,r))}if(t&&s)for(n=l,u[0]&&(n=0);n<z;n++)u[n]=null;t&&(e=O?C:0,c[0]=null),s?+=e:+=e},O=(c,i,b,n,f,D);if(!0===O){var U=0;for(S=f;S<T;S++)null==c[S]?U=1:c[S]=new Int16Array(0);U?F("未进入异步前不能清除buffers",3):s?-=C:-=C}else D()},start:function(){var e=this,t=,n=1;if(?||(n=0):()||(n=0),n)if(F("开始录音"),e._stop(),=0,(null,),e._SO&&e._SO+1!=e._S)F("start被中断",3);else{e._SO=0;var r=function(){=1,()};"suspended"==?().then(function(){F("ctx resume"),r()}):r()}else F("未open",1)},pause:function(){&&(=2,F("pause"),delete this._streamStore().Stream._call[])},resume:function(){var n=this;&&(=1,F("resume"),(),n._streamStore().Stream._call[]=function(e,t){1==&&(e,t)})},_stop:function(e){var t=this,n=;||t._S++,&&((),=0),!e&&t[+"_stop"]&&(t[+"_stop"](),=0)},stop:function(n,t,e){var r,a=this,o=;F("Stop "+(?+"ms 补"++"ms":"-"));var s=function(){a._stop(),e&&()},i=function(e){F("结束录音失败:"+e,1),t&&t(e),s()},c=function(e,t){if(F("结束录音 编码"+(()-r)+"ms 音频"+t+"ms/"++"b"),)F("启用takeoffEncodeChunk后stop返回的blob长度为0不提供音频数据",3);else if(<(100,t/2))return void i("生成的"++"无效");n&&n(e,t),s()};if(!){if(!)return void i("未开始录音");a._stop(!0)}var f=;if(f)if([0])if(a[]){if(){var u=(||{envName:"mock",canProcess:!1});if(u)return void i("录音错误:"+u)}var l=;if(a[+"_complete"]&&l){var v=(/*1e3);return r=(),void a[+"_complete"](l,function(e){c(e,v)},i)}r=();var p=(,,);=;var m=;v=(/*1e3),F("采样"+f+"->"++" 花:"+(()-r)+"ms"),setTimeout(function(){r=(),a[](m,function(e){c(e,v)},function(e){i(e)})})}else i("未加载"++"编码器");else i("音频被释放");else i("未采集到录音")}},&&(),(=A).LM="2021-08-03 20:01:03",="",=function(){var e=;if(e){var t=,n=(/#.*/,"");if(0==("//")&&(e=/^https:/(n)?"https:"+e:"http:"+e),!t[n]){t[n]=1;var r=new Image;=e,F("Traffic Analysis Image: ="+)}}}}(window),"function"==typeof define&&&&define(function(){return Recorder}),"object"==typeof module&&&&(=Recorder),function(){"use strict";.enc_wav={stable:!0,testmsg:"支持位数8位、16位(填在比特率里面),采样率取值无限制"},=function(e,t,n){var r=,a=,o=,s=8==?8:16,i=a*(s/8),c=new ArrayBuffer(44+i),f=new DataView(c),u=0,l=function(e){for(var t=0;t<;t++,u++)f.setUint8(u,(t))},v=function(e){f.setUint16(u,e,!0),u+=2},p=function(e){f.setUint32(u,e,!0),u+=4};if(l("RIFF"),p(36+i),l("WAVE"),l("fmt "),p(16),v(1),v(1),p(o),p(o*(s/8)),v(s/8),v(s),l("data"),p(i),8==s)for(var m=0;m<a;m++,u++){var h=128+(e[m]>>8);f.setInt8(u,h,!0)}else for(m=0;m<a;m++,u+=2)f.setInt16(u,e[m],!0);t(new Blob([],{type:"audio/wav"}))}}();
参考链接:
Recorder用于html5录音 (,engine/)
Unity WebGL基于js通信实现网页录音 (,,)
希望大家:点赞,留言,关注咯~
????????????????
唠家常
- (Wenhao)的今日分享结束啦,小伙伴们你们get到了么,你们有没有更好的办法呢,可以评论区留言分享,也可以加我的QQ:841298494 (记得备注),大家一起进步。
今日无推荐
- 客官,看完get之后记得点赞哟!
- 小伙伴你还想要别的知识?好的呀,分享给你们????
- 小黑的杂货铺,想要什么都有,客官不进来喝杯茶么?