因为该死的Unity不支持WebGL的麦克风,所以只能向网页借力,用网页原生的navigator.getUserMedia录音,然后传音频流给Unity进行转AudioClip播放。
还有一点非常重要:能有同事借力就直接问,厚着脸皮上,我自己闷头两天带加班,不如同事谭老哥加起来提供帮助的俩小时,很感谢他,虽然是他们该做的,但我一直没提出,而且我方向错了
Unity:2021.3.6f1
Github库:UnityWebGLMicrophone
Unity端的.cs .jslib和WebGL端的.js.
WebGLRecorder.jslib
这个需要放在Unity的Plugins文件夹下
mergeInto(LibraryManager.library, {
StartRecord: function(){
StartRecord();
},
StopRecord: function(){
StopRecord();
},
});
WAV.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 = (wav.Length - 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 = wav.Length - (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 string.Format("[WAV: LeftChannel={0}, RightChannel={1}, ChannelCount={2}, SampleCount={3}, Frequency={4}]", LeftChannel, RightChannel, ChannelCount, SampleCount, Frequency);
}
}
SignalManager.cs
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
using UnityEngine;
using UnityEngine.UI;
public class SignalManager : MonoBehaviour
{
public Button StartRecorder;
public Button EndRecorder;
AudioSource m_audioSource;
void Start()
{
m_audioSource = gameObject.AddComponent();
StartRecorder.onClick.AddListener(StartRecorderFunc);
EndRecorder.onClick.AddListener(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
///
///需获取数据的数目
///
private int m_valuePartCount = 0;
///
/// 获取的数据数目
///
private int m_getDataLength = 0;
///
/// 获取的数据长度
///
private int m_audioLength = 0;
///
/// 获取的数据
///
private string[] m_audioData = null;
///
/// 当前音频
///
public static AudioClip m_audioClip = null;
///
/// 音频片段存放列表
///
private List m_audioClipDataList = new List();
///
/// 片段结束标记
///
private string m_currentRecorderSign;
///
/// 音频频率
///
private int m_audioFrequency;
///
/// 单次最大录制时间
///
private const int maxRecordTime = 30;
#endregion
public void GetAudioData(string _audioDataString)
{
if (_audioDataString.Contains("Head"))
{
string[] _headValue = _audioDataString.Split('|');
m_valuePartCount = int.Parse(_headValue[1]);
m_audioLength = int.Parse(_headValue[2]);
m_currentRecorderSign = _headValue[3];
m_audioData = new string[m_valuePartCount];
m_getDataLength = 0;
Debug.Log("接收数据头:" + m_valuePartCount + " " + m_audioLength);
}
else if (_audioDataString.Contains("Part"))
{
string[] _headValue = _audioDataString.Split('|');
int _dataIndex = int.Parse(_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++)
{
stringBuilder.Append(m_audioData[i]);
}
string _audioDataValue = stringBuilder.ToString();
Debug.Log("接收长度:" + _audioDataValue.Length + " 需接收长度:" + m_audioLength);
int _index = _audioDataValue.LastIndexOf(',');
string _value = _audioDataValue.Substring(_index + 1, _audioDataValue.Length - _index - 1);
byte[] data = Convert.FromBase64String(_value);
Debug.Log("已接收长度 :" + data.Length);
if (m_currentRecorderSign == "end")
{
int _audioLength = data.Length;
for (int i = 0; i < m_audioClipDataList.Count; i++)
{
_audioLength += m_audioClipDataList[i].Length;
}
byte[] _audioData = new byte[_audioLength];
Debug.Log("总长度 :" + _audioLength);
int _audioIndex = 0;
data.CopyTo(_audioData, _audioIndex);
_audioIndex += data.Length;
Debug.Log("已赋值0:" + _audioIndex);
for (int i = 0; i < m_audioClipDataList.Count; i++)
{
m_audioClipDataList[i].CopyTo(_audioData, _audioIndex);
_audioIndex += m_audioClipDataList[i].Length;
Debug.Log("已赋值 :" + _audioIndex);
}
WAV wav = new WAV(_audioData);
AudioClip _audioClip = AudioClip.Create("TestWAV", wav.SampleCount, 1, wav.Frequency, false);
_audioClip.SetData(wav.LeftChannel, 0);
m_audioClip = _audioClip;
Debug.Log("音频设置成功,已设置到unity。" + m_audioClip.length + " " + m_audioClip.name);
m_audioSource.clip = m_audioClip;
m_audioSource.Play();
m_audioClipDataList.Clear();
}
else
m_audioClipDataList.Add(data);
m_audioData = null;
}
}
}
#endregion
}
AddToIndex.js
这个脚本中的内容直接增加到index.html文件中
// 全局录音实例
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 = Object.assign({}, defaultOpt, opt);
let sampleRate = options.sampleRate || 8000;
let bitRate = parseInt(options.bitRate / 1000) || 16;
if (RecorderIns) {
RecorderIns.close();
}
RecorderIns = Recorder({
type: "wav",
sampleRate: sampleRate,
bitRate: bitRate,
onProcess(buffers, powerLevel, bufferDuration, bufferSampleRate) {
// 60秒时长限制
const LEN = 59 * 1000;
if (bufferDuration > LEN) {
RecorderIns.recStop();
}
},
});
RecorderIns.open(
() => {
// 打开麦克风授权获得相关资源
console.log("打开麦克风成功");
},
(msg, isUserNotAllow) => {
// 用户拒绝未授权或不支持
console.log((isUserNotAllow ? "UserNotAllow," : "") + "无法录音:" + msg);
}
);
}
// 开始
function StartRecord() {
RecorderIns.start();
}
// 结束
function StopRecord() {
RecorderIns.stop(
(blob, duration) => {
console.log(
blob,
window.URL.createObjectURL(blob),
"时长:" + duration + "ms"
);
sendWavData(blob)
},
(msg) => {
console.log("录音失败:" + msg);
}
);
}
// 切片像unity发送音频数据
function sendWavData(blob) {
var reader = new FileReader();
reader.onload = function (e) {
var _value = reader.result;
var _partLength = 8192;
var _length = parseInt(_value.length / _partLength);
if (_length * _partLength < _value.length) _length += 1;
var _head = "Head|" + _length.toString() + "|" + _value.length.toString() + "|end" ;
// 发送数据头
UnityIns.SendMessage("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|" + i.toString() + "|" + _sendValue;
// 发送分片数据
UnityIns.SendMessage("SignalManager", "GetAudioData", _sendValue);
}
_value = null;
};
reader.readAsDataURL(blob);
}
recorder.wav.min.js
这个直接创建脚本放到index.html同级目录下
/*
录音
https://github.com/xiangyuecn/Recorder
src: recorder-core.js,engine/wav.js
*/
!function(h){"use strict";var d=function(){},A=function(e){return new t(e)};A.IsOpen=function(){var e=A.Stream;if(e){var t=e.getTracks&&e.getTracks()||e.audioTracks||[],n=t[0];if(n){var r=n.readyState;return"live"==r||r==n.LIVE}}return!1},A.BufferSize=4096,A.Destroy=function(){for(var e in F("Recorder Destroy"),g(),n)n[e]()};var n={};A.BindDestroy=function(e,t){n[e]=t},A.Support=function(){var e=h.AudioContext;if(e||(e=h.webkitAudioContext),!e)return!1;var t=navigator.mediaDevices||{};return t.getUserMedia||(t=navigator).getUserMedia||(t.getUserMedia=t.webkitGetUserMedia||t.mozGetUserMedia||t.msGetUserMedia),!!t.getUserMedia&&(A.Scope=t,A.Ctx&&"closed"!=A.Ctx.state||(A.Ctx=new e,A.BindDestroy("Ctx",function(){var e=A.Ctx;e&&e.close&&(e.close(),A.Ctx=0)})),!0)};var S=function(e){var t=(e=e||A).BufferSize||A.BufferSize,n=A.Ctx,r=e.Stream,a=r._m=n.createMediaStreamSource(r),o=r._p=(n.createScriptProcessor||n.createJavaScriptNode).call(n,t,1,1);a.connect(o),o.connect(n.destination);var f=r._call;o.onaudioprocess=function(e){for(var t in f){for(var n=e.inputBuffer.getChannelData(0),r=n.length,a=new Int16Array(r),o=0,s=0;s"+m.length+" 花:"+(Date.now()-r)+"ms"),setTimeout(function(){r=Date.now(),a[o.type](m,function(e){c(e,v)},function(e){i(e)})})}else i("未加载"+o.type+"编码器");else i("音频被释放");else i("未采集到录音")}},h.Recorder&&h.Recorder.Destroy(),(h.Recorder=A).LM="2021-08-03 20:01:03",A.TrafficImgUrl="",A.Traffic=function(){var e=A.TrafficImgUrl;if(e){var t=A.Traffic,n=location.href.replace(/#.*/,"");if(0==e.indexOf("//")&&(e=/^https:/i.test(n)?"https:"+e:"http:"+e),!t[n]){t[n]=1;var r=new Image;r.src=e,F("Traffic Analysis Image: Recorder.TrafficImgUrl="+A.TrafficImgUrl)}}}}(window),"function"==typeof define&&define.amd&&define(function(){return Recorder}),"object"==typeof module&&module.exports&&(module.exports=Recorder),function(){"use strict";Recorder.prototype.enc_wav={stable:!0,testmsg:"支持位数8位、16位(填在比特率里面),采样率取值无限制"},Recorder.prototype.wav=function(e,t,n){var r=this.set,a=e.length,o=r.sampleRate,s=8==r.bitRate?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>8);f.setInt8(u,h,!0)}else for(m=0;m
参考链接:
Recorder用于html5录音 (recorder-core.js,engine/wav.js)
Unity WebGL基于js通信实现网页录音 (sendWAVData.js,GetAudioData.cs,WAV.cs)
- 客官,看完get之后记得点赞哟!
- 小伙伴你还想要别的知识?好的呀,分享给你们
- 小黑的杂货铺,想要什么都有,客官不进来喝杯茶么?