Unity WebGL基于js通信实现网页录音

article/2025/7/22 5:43:54

Unity WebGL基于js通信实现网页录音

由于Unity发布WebGL后无法使用Unity中的Microphone,所以无法进行录音,只能借助与网页进行录音操作,因此基于Unity新版页面对接方式jslib实现页面录音。可以将录音数据回传至unity,并在unity中进行播放。理论上支持无限制录音。

UnityWebGLMicrophone1.0.1 插件下载地址

插件介绍

在请求网页权限后,可以选择录制时间或者循环录制,然后开始录制。之后会在页面中进行分段录制,并回传至unity,结束录制后进行整合,并转换成AudioClip进行播放。

运行效果图

在这里插入图片描述

主要代码

js部分:

  1. 获取权限
 Init: function (_GameObjectName, _recordTime) {//console.log("MicrophoneInit:");unityGameObjectName = Pointer_stringify(_GameObjectName);//转换unity传递过来的字符串navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia;window.AudioContext = window.AudioContext || window.webkitAudioContext;var maxRecordTime = _recordTime;if (navigator.getUserMedia) {const constraints = { audio: true };navigator.getUserMedia(constraints,function (stream) {console.log("授权成功!");isUserMedia = "true";recorderState = "inactive";//  bufferSize: the onaudioprocess event is called when the buffer is fullvar bufferSize = 4096;//channelCount = 2;channelCount = 1;sampleBits = 16;// creates the audio contextaudioContext = new AudioContext();originalSampleRate = audioContext.sampleRate;//48000// creates an audio node from the microphone incoming streammediaStream = audioContext.createMediaStreamSource(stream);volume = audioContext.createGain();for (var channel = 0; channel < channelCount; channel++) {inputBuffer[channel] = [];}recordingLength = 0;recordedLength = 0;recordedResultLength = 0;var maxRecordLength = maxRecordTime * originalSampleRate;if (audioContext.createScriptProcessor) {//  创建一个可以通过JavaScript直接处理音频的ScriptProcessorNode.scriptProcessor = audioContext.createScriptProcessor(bufferSize,channelCount,channelCount);}else {// 创建一个 JavaScriptNode, 用于javascript直接处理音频。 这个方法已经被AudioContext.createScriptProcessor()替代并且废弃。scriptProcessor = audioContext.createJavaScriptNode(bufferSize,channelCount,channelCount);}scriptProcessor.onaudioprocess = function (audioProcessingEvent) {for (var channel = 0; channel < channelCount; channel++) {inputBuffer[channel].push(audioProcessingEvent.inputBuffer.getChannelData(channel));}recordingLength += bufferSize;recordedLength += bufferSize;//录制长度超出界限multipleRecording(maxRecordLength);};},function (err) {console.error("授权失败!", err);isUserMedia = "false";recorderState = "Don'tGetUserMedia";switch (error.code || error.name) {case 'PERMISSION_DENIED':case 'PermissionDeniedError':console.error('用户拒绝提供信息。');break;case 'NOT_SUPPORTED_ERROR':case 'NotSupportedError':console.error('浏览器不支持硬件设备。');break;case 'MANDATORY_UNSATISFIED_ERROR':case 'MandatoryUnsatisfiedError':console.error('无法发现指定的硬件设备。');break;default:console.error('无法打开麦克风。异常信息:' + (error.code || error.name));break;}});}else {isUserMedia = "false";recorderState = "Don'tGetUserMedia";console.error("浏览器不支持 getUserMedia");}},
  1. 发送数据
  //发送数据$sendWAVData: function (blob, sign) {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() + "|" + sign;SendMessage(unityGameObjectName, "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;SendMessage(unityGameObjectName, "GetAudioData", _sendValue);}if (sign === "end")recorderState = "inactive";_value = null;}reader.readAsDataURL(blob);},
  1. 停止录制
StopRecording: function () {if (isUserMedia !== "true") {recorderState = "Don'tGetUserMedia";SendMessage(unityGameObjectName, "OnWarning", recorderState);return;}if (recorderState === "completed")return;if (recorderState !== "inactive") {mediaStream.disconnect();scriptProcessor.disconnect();recorderState = "completed";//console.log("停止录音,转码数据中..." + recorderState);SendMessage(unityGameObjectName, "OnStop", recorderState);var _data = getPureWavData(0);sendWAVData(_data, "part");_data = [];var _headerData = getWAVHeaderData(recordedResultLength);sendWAVData(_headerData, "end");_headerData = [];}elseconsole.log("未开始录音..." + recorderState);},

4.自动结束录制/重复录制

 //超出限制自动结束录制$autoStopRecording: function () {mediaStream.disconnect();scriptProcessor.disconnect();recorderState = "autoCompleted";//console.log("自动停止录音,转码数据中..." + recorderState);SendMessage(unityGameObjectName, "OnStop", recorderState);var _data = getPureWavData(0);sendWAVData(_data, "part");_data = [];var _headerData = getWAVHeaderData(recordedResultLength);sendWAVData(_headerData, "end");_headerData = [];},//重复录制 - 发送数据$multipleRecording: function (maxRecordLength) {if (!loop) {if (maxRecordLength >= totalRecordLength) {if (recordingLength >= totalRecordLength) {autoStopRecording();}}else {if (recordingLength >= currentRecordLength) {autoStopRecording();}else if (recordingLength >= maxRecordLength) {var _data = getPureWavData(0);currentRecordLength = currentRecordLength - maxRecordLength;sendWAVData(_data, "part");_data = [];}}}else {maxRecordLength = maxRecordLength > totalRecordLength ? totalRecordLength : maxRecordLength;if (recordingLength >= maxRecordLength) {var _data = getPureWavData(0);sendWAVData(_data, "part");_data = [];}}},

C#部分

  1. WebGL Microphone
 public sealed class Microphone{/// <summary>/// 录制回调/// </summary>public class MicrophoneEvent : UnityEvent { }/// <summary>/// 停止录制回调/// </summary>public class MicrophoneStopEvent : UnityEvent<bool> { }public Microphone(){}#if UNITY_WEBGL && !UNITY_EDITOR#region/// <summary>/// 初始化音频/// </summary>/// <param name="_unityName">数据交互物体名</param>/// <param name="_recordTime">单次最大可录制时间</param>[DllImport("__Internal")]internal static extern void Init(string _unityName,int _recordTime);/// <summary>/// 是否可录制/// </summary>[DllImport("__Internal")]private static extern bool IsCanRecording();/// <summary>/// 正在录制/// </summary>/// <returns></returns>[DllImport("__Internal")]private static extern bool IsRecord();/// <summary>/// 暂停录制/// </summary>/// <returns></returns>[DllImport("__Internal")]private static extern bool IsPauseRecording();/// <summary>/// 已录制时间(/s)/// Tip: 开始录制时重置为0,取消录制时重置为0/// </summary>/// <returns></returns>[DllImport("__Internal")]private static extern float GetRecordedTime();/// <summary>/// 开始录制/// </summary>/// <param name="loop">是否循环</param>/// <param name="secondLength">录制长度(/s)</param>/// <param name="sampleRate">采样率</param>[DllImport("__Internal")]private static extern void StartRecording(int loop, int secondLength, int sampleRate);/// <summary>/// 暂停录制/// </summary>[DllImport("__Internal")]private static extern void PauseRecording();/// <summary>/// 继续录制/// </summary>[DllImport("__Internal")]private static extern void ResumeRecording();/// <summary>/// 停止录制/// </summary>[DllImport("__Internal")]private static extern void StopRecording();/// <summary>///取消录制/// </summary>[DllImport("__Internal")]private static extern void CancelRecording();/// <summary>///关闭录制权限/// </summary>[DllImport("__Internal")]private static extern void CloseRecording();/// <summary>/// 当前录制的名字/// </summary>public static string m_currentAudioName = string.Empty;/// <summary>/// 录制的结果/// </summary>public static AudioClip AudioClipResult { get { return WebGLMicrophoneManager.m_audioClip ?? null; } }/// <summary>/// 是否可录制/// </summary>public static bool IsRecordable { get { return IsCanRecording(); } }/// <summary>/// 正在录制/// </summary>/// <returns></returns>public static bool IsRecording { get { return IsRecord(); } }/// <summary>/// 暂停录制/// </summary>/// <returns></returns>public static bool IsPause { get { return IsPauseRecording(); } }/// <summary>/// 已录制时间(/s)/// Tip: 开始录制时重置为0,取消录制时重置为0/// </summary>/// <returns></returns>public static float RecordedTime { get { return GetRecordedTime(); } }/// <summary>///  请求权限,录制前必须请求权限/// </summary>public static void GetUserMedia(){WebGLMicrophoneManager.Instance.Init();}/// <summary>/// 开始录制/// </summary>/// <param name="audioName">生成的音频名字</param>/// <param name="loop">是否循环</param>/// <param name="secondLength">录制长度(/s)</param>/// <param name="sampleRate">采样率 8000/11025/22050/32000/44100/48000</param>//public static AudioClip Start(string audioName, bool loop = false, int secondLength = 60, int sampleRate = 44100)//{//    m_currentAudioName = audioName;//    StartRecording(loop == false ? 0 : 1, secondLength, sampleRate);//    Debug.Log("开始录制");//    Debug.Log(WebGLMicrophoneManager.m_audioClip != null ? WebGLMicrophoneManager.m_audioClip.length : -100);//    return WebGLMicrophoneManager.m_audioClip;//}public static void Start(string audioName, bool loop = false, int secondLength = 60, int sampleRate = 44100){m_currentAudioName = audioName;StartRecording(loop == false ? 0 : 1, secondLength, sampleRate);//Debug.Log("开始录制");}/// <summary>/// 暂停录制/// </summary>public static void Pause() { PauseRecording(); }/// <summary>/// 继续录制/// </summary>public static void Resume() { ResumeRecording(); }/// <summary>/// 停止录制/// </summary>public static void Stop() { StopRecording(); }/// <summary>///取消录制/// </summary>public static void Cancel() { CancelRecording(); }/// <summary>///关闭录制权限/// </summary>public static void Close() { CloseRecording(); }#endregion
#else#region/// <summary>/// 是否可录制/// </summary>public static bool IsRecordable;/// <summary>/// 正在录制/// </summary>/// <returns></returns>public static bool IsRecording;/// <summary>/// 暂停录制/// </summary>/// <returns></returns>public static bool IsPause;/// <summary>/// 已录制时间(/s)/// Tip: 开始录制时重置为0,取消录制时重置为0/// </summary>/// <returns></returns>public static float RecordedTime;/// <summary>/// 录制的结果/// </summary>public static AudioClip AudioClipResult { get; private set; }/// <summary>///  请求权限,录制前必须请求权限/// </summary>public static void GetUserMedia() { }/// <summary>/// 开始录制/// Tip: 当不循环录制时超时自动结束录制/// </summary>/// <param name="audioName">生成的音频名字</param>/// <param name="loop">是否循环</param>/// <param name="secondLength">录制长度(/s)</param>/// <param name="sampleRate">采样率 8000/11025/22050/32000/44100/48000</param>//public static AudioClip Start(string audioName, bool loop = false, int secondLength = 60, int sampleRate = 44100) { return null; }public static void Start(string audioName, bool loop = false, int secondLength = 60, int sampleRate = 44100) { }/// <summary>/// 暂停录制/// </summary>public static void Pause() { }/// <summary>/// 继续录制/// </summary>public static void Resume() { }/// <summary>/// 停止录制/// </summary>public static void Stop() { }/// <summary>///取消录制/// </summary>public static void Cancel() { }/// <summary>///关闭录制权限/// </summary>public static void Close() { }#endregion
#endif/// <summary>/// 开始录制回调/// </summary>public static MicrophoneEvent OnStartEvent = new MicrophoneEvent();/// <summary>/// 暂停录制回调/// </summary>public static MicrophoneEvent OnPauseEvent = new MicrophoneEvent();/// <summary>/// 重开始录制回调/// </summary>public static MicrophoneEvent OnResumeEvent = new MicrophoneEvent();/// <summary>/// 结束录制回调/// value:true 自动结束录制/// </summary>public static MicrophoneStopEvent OnStopEvent = new MicrophoneStopEvent();/// <summary>/// 获取录制数据后回调/// </summary>public static MicrophoneEvent OnEndEvent = new MicrophoneEvent();/// <summary>/// 取消录制回调/// </summary>public static MicrophoneEvent OnCancelEvent = new MicrophoneEvent();/// <summary>/// 关闭录制回调/// </summary>public static MicrophoneEvent OnCloseEvent = new MicrophoneEvent();/// <summary>/// 录制错误回调/// </summary>/// <param name="recorderState"></param>public static MicrophoneEvent OnWarningEvent = new MicrophoneEvent();}

2.数据处理

        /// <summary>/// 结束录制获取数据/// </summary>/// <param name="_audioDataString"></param>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(Microphone.m_currentAudioName, wav.SampleCount, 1, wav.Frequency, false);_audioClip.SetData(wav.LeftChannel, 0);m_audioClip = _audioClip;_audioClip = null;//Debug.Log("音频设置成功,已设置到unity。"+m_audioClip.length+"  "+m_audioClip.name);m_audioClipDataList.Clear();if (Microphone.OnEndEvent != null)Microphone.OnEndEvent.Invoke();}elsem_audioClipDataList.Add(data);m_audioData = null;}}

http://chatgpt.dhexx.cn/article/AReIVnu0.shtml

相关文章

php录音功能,h5做出网页录音功能

这次给大家带来h5做出网页录音功能,h5做出网页录音功能的注意事项有哪些,下面就是实战案例,一起来看一下。 得益于前辈的分享,做了一个h5录音的demo。效果图如下: 点击开始录音会先弹出确认框: 首次确认允许后,再次录音不需要再确认,但如果用户点击禁止,则无法录音: …

java 网页 录音_Java+FlashWavRecorder实现网页录音并上传【新】

【注意】 【说明】 1.在线录音并直接上传到服务器中 【使用】 1.运行 1.请下载本项目&#xff0c;并导入myeclipse中 2.跑起项目&#xff0c;访问http://localhost:8080/recorder-online-upload查看效果 2.定制 1.打开WEB-INF/view/index.html 2.只有两段html注释中的代码是需要…

java 网页 录音_Java+FlashWavRecorder实现网页录音并上传

【注意】 【前言】 肯定有需求要网页录音&#xff0c;并且要上传。这奇葩需求。 然后找到了FlashWavRecorder&#xff0c; 【原始版本号】 1.下载 在上面的地址下载zip解压之后&#xff0c;目录里面有个index.html。打开之后这效果&#xff1a; 2.录音权限 必须保证你的电脑有麦…

10.WebRTC实现网页录音功能

一、概述 通过WebRTC&#xff0c;实现在页面上进行录音&#xff0c;并将录音结果转换为.wav格式进行播放 二、录音实现 检测是否有麦克风权限 navigator.mediaDevices.enumerateDevices().then(function(devices) {// 检测视频输入let video devices.find((device) > {…

uniapp中使用网页录音并上传声音文件(发语音)——js-audio-recorder的使用【伸手党福利】

uniapp中上传音频只能在app或小程序当中实现&#xff0c;如何使用网页完成语音的录制和上传则成为了困扰前端童鞋的重点。 本文着重解决&#xff1a; js-audio-recorder报 error:浏览器不支持getUserMedia ! 的问题。 js-audio-recorder报 NotFoundError : Requested device no…

前端实现在浏览器网页中录音

一、整体实现的思想 页面中实现录音需要使用浏览器提供的MediaRecorder API,所以要实现页面录音就需要浏览器支持MediaStream Recording相关的功能&#xff0c;即浏览器能够获取浏览器的录音权限。 页面内容&#xff0c;需要一个记录录音开始和结束的按钮&#xff0c;以及一个用…

基于神经网络的自适应最优控制

《 N e u r a l n e t w o r k a p p r o a c h t o c o n t i n u o u s − t i m e d i r e c t a d a p t i v e o p t i m a l c o n t r o l f o r p a r t i a l l y u n k n o w n n o n l i n e a r s y s t e m s 》 D r a g u n a V r a b i e ∗ , F r a n k L e w …

堆排序(排升序为啥建大堆,排降序为啥建小堆)

简介&#xff1a; 之前对堆排序认识的不是很透彻&#xff0c;今天回过头来再把堆排序的知识整理整理&#xff01;以及排升序为什么要建大堆&#xff0c;排降序要建小堆。 正文&#xff1a; 首先我们要知道&#xff1a;   ①堆的逻辑是一颗完全二叉树&#xff1b;   ②它使…

残差网络Residual Networks-残差网络的创建、训练、测试、调参

残差网络的创建、训练、测试、调参加粗样式 在Keras中实现残差网络模型的创建&#xff0c;并通过模型来实现对图片的分类。 残差网络的预备知识 网络越深越好&#xff1f; 随着网络层级的不断增加&#xff0c;模型精度不断得到提升&#xff0c;而当网络层级增加到一定的数目…

堆排序,为什么升序排列要建大堆,降序排列要建小堆

堆排序中用到了建立大小堆和向下调整的内容&#xff0c;对这些内容有些不了解的同学可以去补一补专门写堆的博客&#xff0c;方便更好的理解堆排序数据结构之堆&#xff08;Heap&#xff09;&#xff0c;堆的相关操作&#xff0c;用堆模拟优先级队列。 如果把待排序序列分为未排…

操作系统——动态分配算法(首次适应算法,最佳适应算法,最坏适应算法及回收)

操作系统——动态分配 写的时间早了&#xff0c;有些许漏洞和不足&#xff0c;请大家不要介意 分配方式可分为四类:单一连续分配、固定分区分配、动态分区分配以及动态可重定位分区分配算法四种方式&#xff0c;其中动态分区分配算法就是此实验的实验对象。动态分区分配又称为…

pandas中对列进行排序(单列/多列)/(升序/降序)/(多列升序,降序控制)

前言 我想把数据分析刻进DNA里 如下面的数据,对price,要进行最简单的升序操作: 这个好整: import pandas as pdshop pd.read_csv("data/shop.csv", index_colid)shop.sort_values(byprice, inplaceTrue)结果: 如果你想整活(被迫)要把数据进行降序排列: 按照降序,传…

最先适应法、最佳适应法、下次适配法、最差适配法

题&#xff1a; 分析&#xff1a; 1. 首先分析是不是最差适配法&#xff0c;最差适配法意思是首先找到最大的内存空间进行分配&#xff0c; 对于请求的5K存储空间&#xff0c;首先找到地址200K容量为56K的地方进行分配&#xff0c;然后剩余51K。 再对请求的15K进行分配&…

自适应滤波器更新算法-EP1

自适应滤波器更新算法-EP1 自适应滤波器是回声消除系统中非常重要的一个功能模块&#xff0c;而对于自适应滤波器来说&#xff0c;如果更新滤波器系数则是关键所在。本文将介绍几种现有的滤波器更新算法&#xff0c;并附上Matlab测试代码。 1、LMS算法 1.1算法原理 LMS算法即…

自适应神经网络算法原理,单神经元自适应控制

关于神经网络自适应控制求助 这句话你可以直接用&#xff0c;不用加引用。因为这句话是很容易验证的。在网络层数、隐含层节点数逐渐增加&#xff0c;训练次数增加之后&#xff0c;他的拟合能力也是不断增加的&#xff0c;所以说&#xff0c;他可以以任意精度逼近任何非线性连…

【转载】梯度下降算法的参数更新公式

NN这块的公式&#xff0c;前馈网络是矩阵乘法。损失函数的定义也是一定的。 但是如何更新参数看了不少描述&#xff0c;下面的叙述比较易懂的&#xff1a; 1、在吴恩达的CS229的讲义的第四页直接给出参数迭代公式 在UFLDL中反向传导算法一节也是直接给出的公式 2、例子&#x…

Java中Comparator的个人简单理解(升序降序)与使用

目录 Java自定义排序返回值简单记忆理解实践LInkedList升序&#xff08;默认情况&#xff09;降序 PriorityQueue升序下的小顶堆&#xff08;默认情况&#xff09;降序下的大顶堆 总结补充数组类型自定义排序降序排序 数组 Java自定义排序返回值简单记忆理解 默认情况下&#…

深度残差收缩网络(从信号降噪的角度进行理解)

本文探讨了深度残差收缩网络的另一种理解方式。 传统信号降噪算法的常见步骤是&#xff1a; ① 采用某种信号变换方法&#xff08;例如小波、经验模态分解&#xff09;&#xff0c;将含噪信号变换到另外一种形态&#xff08;例如小波系数、本征模态分量等&#xff09;。在这些…

NIPS 2016 深度学习 迁移学习 ---残差转移网络用于无监督领域自适应

深度学习的成功得益于大量的标注数据&#xff0c;而数据标注是非常消耗资源的。当一个问题中缺少标注数据时&#xff0c;可以从另一个源中所学知识迁移过来&#xff0c;并且用于新问题中。 清华大学的学者提出了一种新的方法&#xff08;https://arxiv.org/pdf/1602.04433.pdf&…

深度残差网络+自适应参数化ReLU激活函数(调参记录21)Cifar10~95.12%

本文在调参记录20的基础上,将残差模块的个数,从27个增加到60个,继续测试深度残差网络ResNet+自适应参数化ReLU激活函数在Cifar10数据集上的表现。 自适应参数化ReLU函数被放在了残差模块的第二个卷积层之后,这与Squeeze-and-Excitation Networks或者深度残差收缩网络是相似…