Unity3D_话筒声波实时反馈、声音对比、返听、录音保存
效果展示:
工程界面:
总体思路:
声波实时反馈:调用Unity中录音的函数对话筒进行录音,实时截取录音片段的最后128个单位,遍历这128各单位找出最大值,将最大值复制到UI图片的高度。
声音对比:在截取录音片段的同时截取对比音频片段相同位置的数据,同样遍历各单位的高度,将最大值复制给“对比音频”的UI图片高度。
返听:每帧截取录制音频的后500个单位进行播放,就达到了返听效果。
录音保存:在录音结束时调用保存的方法,首先创建一个写好wav格式头文件的流文件,然后将录音转化为字节数组,将转化后的数组写入流文件并保存。
名词解释:
采样率:如果采样率为 44100,那么程序每秒钟会采集44100个声音样本。
声音的位置:这里的位置并不是指声音在场景中的三维坐标,我们可以将音频片段看作一个数轴,当前声音的位置就是数轴上的一个点。
代码展示:
using System; using System.Collections; using System.Collections.Generic; using System.IO; using UnityEngine; using UnityEngine.UI; public class TestMicro : MonoBehaviour { // 声音播放组件 private AudioSource aud; // 当前使用的麦克风名字 private string microphoneName; // 麦克风列表的字符串 private string[] micDevicesNames; // 用于显示声波的竖线(实时) private RectTransform[] Refers; // 用于显示声波的竖线(对比) private RectTransform[] fluctuates; // 计时器(用于控制声波更新的频率) private float timer; // 声波每个多长时间更新一次 private float timerUpdate = 0.05f; // 当前声波高度 private float nowHeight; // 用于对比的声音片段 public AudioClip compareAudio; // 用于播放对比声音的组件 private AudioSource compareSource;// 是否开始录音 private bool isRecord = false; // Start is called before the first frame update private void Start() { // 获取麦克风列表 micDevicesNames = Microphone.devices; // 获取播放声音的组件 aud = GetComponent<AudioSource>(); // 获得列表中第一个麦克风) microphoneName = micDevicesNames[0]; Debug.Log("当前用户录音的麦克风名字为:" + micDevicesNames[0]); // 显示声波的竖线 fluctuates = new RectTransform[transform.Find("Fluctuate").childCount]; for (int i = 0; i < fluctuates.Length; i++) { fluctuates[i] = transform.Find("Fluctuate").GetChild(i).GetComponent<RectTransform>(); fluctuates[i].sizeDelta = new Vector2(5f, 5f); } // 用于对比的声波竖线 Refers = new RectTransform[transform.Find("Refer").childCount]; for (int i = 0; i < Refers.Length; i++) { Refers[i] = transform.Find("Refer").GetChild(i).GetComponent<RectTransform>(); Refers[i].sizeDelta = new Vector2(5f, 5f); } // 获取声音播放组件 compareSource = transform.Find("PlayAudio").GetComponent<AudioSource>(); //byte[] bs = BitConverter.GetBytes(); //foreach (var item in bs) //{ // print(item); //} //print(bs.Length); DrawLine(compareAudio, Color.red); } // Update is called once per frame private void Update() { // 按下空格开始录音 if (Input.GetKeyDown(KeyCode.Space)) { StartCoroutine("RecordAudioIENU"); } if (isRecord) { // 时间累积,超过固定时间更新声波 timer += Time.deltaTime; if (timer >= timerUpdate) { // 重置计时器 timer -= timerUpdate; // 实时声音显示 for (int i = 0; i < fluctuates.Length - 1; i++) { fluctuates[i].sizeDelta = fluctuates[i + 1].sizeDelta; } nowHeight = GetVolumeRealtime(); if (nowHeight < 5f) nowHeight = 5f; fluctuates[fluctuates.Length - 1].sizeDelta = new Vector2(5f, nowHeight); // 对比声音显示 for (int i = 0; i < Refers.Length - 1; i++) { Refers[i].sizeDelta = Refers[i + 1].sizeDelta; } nowHeight = GetAudioclip(); if (nowHeight < 5f) nowHeight = 5f; Refers[fluctuates.Length - 1].sizeDelta = new Vector2(5f, nowHeight); } } } // 获取实时声波 private float GetVolumeRealtime() { // 确认当前设备是否正在录制(如果为设备名称传递 null 或空字符串,则使用默认麦克风。可通过 devices 属性获取可用麦克风设备的列表。) if (Microphone.IsRecording(null)) { // 从录音中实时截取的样本长度,值越大越精确,同时性能开销会增加,程序会变的卡顿 int sampleSize = 128; float[] samples = new float[sampleSize]; // startPosition = 当前录制样本总长度 - 实时截取的样本长度 // Microphone.GetPosition() 获取在录制样本的总长度(返回值 = 秒 * 采样率) int startPosition = Microphone.GetPosition(microphoneName) - (sampleSize + 1); if (startPosition > samples.Length) { // 获取音频数据(用于接收数据的数组,截取位置) aud.clip.GetData(samples, startPosition); // 得到数组中数值最大的信息 float levelMax = 0; for (int i = 0; i < samples.Length; ++i) { if (samples[i] > levelMax) levelMax = samples[i]; } return levelMax * 100; } } else { // 声音录制结束,初始化参数 isRecord = false; timer = 0f; // 保存录制的音频 //AudioClip tempClip = AudioClip.Create(Application.streamingAssetsPath + "/abcdefg.wav", aud.clip.samples, aud.clip.channels, aud.clip.frequency, false); Save(); print("保存录音成功"); } return 0; } // 获取给定音频的声波 private float GetAudioclip() { if (Microphone.IsRecording(null)) { // 从录音中实时截取的样本长度,值越大越精确,同时性能开销会增加,程序会变的卡顿 int sampleSize = 128; float[] samples = new float[sampleSize]; // startPosition = 当前录制样本总长度 - 实时截取的样本长度 // Microphone.GetPosition() 获取在录制样本的总长度(返回值 = 秒 * 采样率) int startPosition = Microphone.GetPosition(microphoneName) - (sampleSize + 1); if (startPosition > samples.Length) { // 获取音频数据(用于接收数据的数组,截取位置) compareAudio.GetData(samples, startPosition); // 得到数组中数值最大的信息 float levelMax = 0; for (int i = 0; i < samples.Length; ++i) { if (samples[i] > levelMax) levelMax = samples[i]; } return levelMax * 100; } } return 0; } // 开始录音 private IEnumerator RecordAudioIENU() { // 开始行录制 // 返回值:录制的音频,如果启动失败返回空 // 参数:录制时使用的设备,在达到录制时常时是否重复录制,录制生成的AudioClip长度(秒,大于0秒,小于1小时),录制生成的AudioClip采样率 aud.clip = Microphone.Start(microphoneName, false, (int)compareAudio.length, 44100); compareSource.clip = compareAudio; compareSource.Play(); // 开始采集 isRecord = true; yield return new WaitForSeconds(1f); // 开启返听 // timeSamples 播放第几个采样 // Microphone.GetPosition 当前录制的采样长度 // 500:值越大延时越高,值也不能太小会导致无声或者噪声 aud.timeSamples = Microphone.GetPosition(microphoneName) - 500; aud.Play(); } // 保存录音 public void Save() { // 将音频转化为字节数组 byte[] data = GetRealAudio(aud.clip); // 录音保存的名字 string fileName = DateTime.Now.ToString("yyyyMMddHHmmss"); //如果不是“.wav”格式的,加上后缀 if (!fileName.ToLower().EndsWith(".wav")) { fileName += ".wav"; } // Path.Combine 拼接字符串,参考网址:https://blog.csdn.net/q764424567/article/details/126649544 string path = Path.Combine(Application.streamingAssetsPath, fileName);//录音保存路径 //输出路径 print(path); // 创建一个空的流文件,以便将音频数据写入 using (FileStream fs = CreateEmpty(path)) { //wav头文件 WriteHeader(fs, aud.clip); // 将音频字符全部写入流文件 fs.Write(data, 0, data.Length); } } // 将录音转化为字节数组 public static byte[] GetRealAudio(AudioClip recordedClip) { // 得到当前录音的位置(因为录音已经结束,也就是录音的长度) int position = Microphone.GetPosition(null); if (position <= 0 || position > recordedClip.samples) { position = recordedClip.samples; } // 录音的长度乘以它的通道 float[] soundata = new float[position * recordedClip.channels]; // 使用剪辑中的数据填充数组 recordedClip.GetData(soundata, 0); // 创建一个音频文件(名字,长度,通道数,采样率) recordedClip = AudioClip.Create(recordedClip.name, position, recordedClip.channels, recordedClip.frequency, false); // recordedClip.SetData(soundata, 0); // short类型储存在最大值 int rescaleFactor = 32767; // 创建一个空的字节数组,用于接收音频转化之后的信息 byte[] outData = new byte[soundata.Length * 2]; // 遍历所有采集到的音频信息 for (int i = 0; i < soundata.Length; i++) { // short类型属于带符号的短整数类型,short类型占2字节(16位)内存空间,存储-32768 到 32767 short temshort = (short)(soundata[i] * rescaleFactor); byte[] temdata = BitConverter.GetBytes(temshort); outData[i * 2] = temdata[0]; outData[i * 2 + 1] = temdata[1]; } Debug.Log("音频保存成功" + "\n音频长度:" + position + "\n数据长度:" + outData.Length); return outData; } // 创建wav格式头文件 private FileStream CreateEmpty(string filepath) { // 创建一个空的流文件 FileStream fileStream = new FileStream(filepath, FileMode.Create); // 创建一个字符 byte emptyByte = new byte(); // 为wav文件头留出空间(在文件中写入44个空字符) for (int i = 0; i < 44; i++) { fileStream.WriteByte(emptyByte); } // 返回流文件 return fileStream; } // 写头文件,参考:https://www.cnblogs.com/wzzkaifa/p/7116139.html public static void WriteHeader(FileStream stream, AudioClip clip) { // 采样率 int hz = clip.frequency; // 通道数 int channels = clip.channels; // 长度 int samples = clip.samples; stream.Seek(0, SeekOrigin.Begin); Byte[] riff = System.Text.Encoding.UTF8.GetBytes("RIFF"); // 从 riff 的 0 位置开始读取 4 个字节序列,然后将读出来的字节从 stream 的 position 位置开始存储在 stream 中 stream.Write(riff, 0, 4); Byte[] chunkSize = BitConverter.GetBytes(stream.Length - 8); stream.Write(chunkSize, 0, 4); Byte[] wave = System.Text.Encoding.UTF8.GetBytes("WAVE"); stream.Write(wave, 0, 4); Byte[] fmt = System.Text.Encoding.UTF8.GetBytes("fmt "); stream.Write(fmt, 0, 4); Byte[] subChunk1 = BitConverter.GetBytes(16); stream.Write(subChunk1, 0, 4); UInt16 one = 1; Byte[] audioFormat = BitConverter.GetBytes(one); stream.Write(audioFormat, 0, 2); Byte[] numChannels = BitConverter.GetBytes(channels); stream.Write(numChannels, 0, 2); Byte[] sampleRate = BitConverter.GetBytes(hz); stream.Write(sampleRate, 0, 4); Byte[] byteRate = BitConverter.GetBytes(hz * channels * 2); stream.Write(byteRate, 0, 4); UInt16 blockAlign = (ushort)(channels * 2); stream.Write(BitConverter.GetBytes(blockAlign), 0, 2); UInt16 bps = 16; Byte[] bitsPerSample = BitConverter.GetBytes(bps); stream.Write(bitsPerSample, 0, 2); Byte[] datastring = System.Text.Encoding.UTF8.GetBytes("data"); stream.Write(datastring, 0, 4); Byte[] subChunk2 = BitConverter.GetBytes(samples * channels * 2); stream.Write(subChunk2, 0, 4); } // 将音频以竖线的方式打印出来 void DrawLine(AudioClip clip, Color color) { // 创建与声音信息长度相等的数组 float[] samples = new float[clip.samples * clip.channels]; // 使用音频中的数据填充数组 clip.GetData(samples, 0); // 划线开始的点 Vector3 start; // 划线结束的点 Vector3 end; // 累计划了多少条线 int num = 0; // 循环遍历声音信息数组,将每一条信息显示为一条线 for (int i = 0; i < samples.Length; i += 2) { start = new Vector3(i * 0.00001f, -0.01f, 0f); end = new Vector3(i * 0.00001f, samples[i], 0f); if (samples[i] >= 0f) { Debug.DrawLine(start, end, color, 10f); num++; } } print("音频采样率:" + clip.frequency + "\n音频长度:" + clip.samples + "\n声道数:" + clip.channels + "\n源 数 量:" + samples.Length + "\n样本数量:" + num + "...舍弃数量:" + (samples.Length / 2 - num / 2)); } [ContextMenu("倒排子物体")] public void InvertedChild() { fluctuates = new RectTransform[transform.childCount]; for (int i = 0; i < fluctuates.Length; i++) { fluctuates[i] = transform.GetChild(i).GetComponent<RectTransform>(); } for (int i = 0; i < fluctuates.Length; i++) { fluctuates[i].SetSiblingIndex(fluctuates.Length - (i + 1)); } } }
示例工程(基于Unity版本 2020.3.30f1c1):点击下载