Unity3D_话筒声波实时反馈、声音对比、返听、录音保存

发布时间 2023-07-26 15:49:04作者: 考拉宝贝

效果展示:

 

工程界面:

 

 

总体思路:

 声波实时反馈:调用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;
    // 音高计数
    public int highAudioIndex;
    // 是否开始录音
    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):点击下载