用Unity3D内部频谱分析方法做音乐视觉特效的原理说明

用Unity3D内部频谱分析方法做音乐视觉特效的原理说明_第1张图片
[视频连接]

(http://v.youku.com/v_show/id_XMTU0NTk4NjgwOA==.html?from=y1.7-1.2)

先理解几个名词和概念:

声音:一种波动,通过空气分子有节奏的震动进行传递。
声音频率Hz:声音每秒种震动的次数,以赫兹Hz 表示。频率越高,音高越高。
分贝dB:量度两个相同单位之数量比例的单位,可表示声音的强度单位。
人耳可听到的声波频率:每秒振动20次到20000次的范围内,既20赫兹至20000赫兹之间,。

采样Sampling:在信号处理程序中,将连续信号(例如声波)降低成离散信号(一系列样本数据)。
采样率Sampling Rate:每秒从连续信号中提取并组成离散信号的采样个数,单位也是赫兹。
快速傅里叶变换FFT:一种算法,可用来转换信号。
窗函数Window Function:在信号处理之中,用来降低信噪比的一种算法。
信噪比:
—噪讯比越高的话,声音的大音量和小音量的音量差会越大(音质猛爆)。
—噪讯比越低的话,声音的大音量和小音量的音量差会越小(音质柔和)。

然后我们看一下Unity内置的这条命令:

AudioSource.GetSpectrumData
函数签名:public void GetSpectrumData(float[] samples, int channel, FFTWindow window);
samples:函数返回值。将音频样本数据传送至samples数组,数组大小必须为2的n次方,最小64,最大8192。
channel:一般设置为0。
window:转换信号所用的窗函数,算法越复杂,声音越柔和,但速度更慢。

用法:
先声明一个浮点数组:

public float[] spectrumData=new float[8192];

在Update方法里面使用方法:

thisAudioSource.GetSpectrumData(spectrumData,0,FFTWindow.BlackmanHarris);

那么这个方法传送到浮点数组里的数据是什么呢?
已知了开始部分的概念,我们可以定义几个变量:
一系列采样数据样本: N
采样频率: f s {f_s} fs
时间:T
已知公式 T = N f s T=\dfrac{N}{{f_s}} T=fsN

N f s \dfrac{N}{{f_s}} fsN的倒数称为频率分辨率Frequency Resolution: d f = 1 T = f s N df=\dfrac{1}{T}=\dfrac{f_s}{N} df=T1=Nfs

频率分辨率越高,转换出来的数据越精确。(下图,同样情况下,低频率分辨率与高频率分辨率的比较)
用Unity3D内部频谱分析方法做音乐视觉特效的原理说明_第2张图片

用Unity3D内部频谱分析方法做音乐视觉特效的原理说明_第3张图片
而我们声明的浮点数数组的大小既是GetSpectrumData这个方法的窗函数转换数据时所用的频率分辨率,而数组中每个浮点数的值既是谱密度,每单位频率波携带的功率,我们知道了频率分辨率df=8196,那么每个浮点数,既谱密度dB表示的的是哪个频率范围,既音高范围的功率呢?

目前数字音乐领域的采样率通常为44100Hz,但通过分析音频文件[MV] FIESTAR(피에스타) _ Mirror.mp3的频谱,可能是因为通过视频转音频的缘故,基本上16000Hz以上的谱密度都非常低了。
用Unity3D内部频谱分析方法做音乐视觉特效的原理说明_第4张图片

而在Unity内通过分析spectrumData的数值,spectrumData[5500]左右以后的浮点数值与前面有一个断崖似的减少,因此可推断出,spectrumData[5500]对应16000Hz,那么16000/5500*8196=23842,GetSpectrumData的采样的最高频率是在20000~23000赫兹之间,既音频文件23000赫兹以上频率的数据都被忽略掉了。

如果继续深入,可研究声波频率与音高的关系,将spectrumData特定范围的浮点数相加即可体现乐曲中各个音高的谱密度,由于人的听觉系统对音高最为敏感,其视觉效果应该会更加理想。
用Unity3D内部频谱分析方法做音乐视觉特效的原理说明_第5张图片

视频连接:http://v.youku.com/v_show/id_XMTU0NTk4NjgwOA==.html?from=y1.7-1.2


(2020-1-8 add)
附:项目源码
GitHub链接:
https://github.com/liu-if-else/UnitySpectrumData
部分源码:

using UnityEngine;
using System.Collections;
using DG.Tweening;

public class Controller : MonoBehaviour {
    //音频相关
    public AudioSource thisAudioSource;
    private float[] spectrumData = new float[8192];
    //cube相关
	public GameObject cubePrototype;
	public Transform startPoint;
	private Transform[] cube_transforms=new Transform[8192];
    private Vector3[] cubes_position= new Vector3[8192];
    //颜色相关
    public GridOverlay gridOverlay;
    private MeshRenderer[] cube_meshRenderers = new MeshRenderer[8192];
    private bool cubeColorChange;
    private bool gridColorChange;
    //相机移动相关
    public Vector3 cameraStartPoint;
    public Transform cameraTransform;
    public bool lookat0_1;
    public bool lookat1_2;
    public bool lookat2_3;
    public Vector3 lookat0_1_vector = Vector3.zero;
    public Vector3 lookat1_2_vector = new Vector3(106f, 12f, 78f);
    public Vector3 lookat2_3_vector = Vector3.zero;
    private Vector3[] moveTos = new Vector3[8192];
    public Transform cubes_parent;
    private bool cubesRotate = true;
	// Use this for initialization
	void Start () {
        //cube生成与排列
		Vector3 p=startPoint.position;

		for(int i=0;i<8192;i++){
			p=new Vector3(p.x+0.11f,p.y,p.z);
            GameObject cube=Object.Instantiate(cubePrototype,p,cubePrototype.transform.rotation)as GameObject;
			cube_transforms[i]=cube.transform;
            cube_meshRenderers[i] =cube.GetComponent<MeshRenderer>();
		}

		p=startPoint.position;

		float a=2f*Mathf.PI/5461;

		for(int i=0;i<5461;i++){
			cube_transforms[i].position=new Vector3(p.x+Mathf.Cos(a)*131,p.y,p.z+131*Mathf.Sin(a));
			a+=2f*Mathf.PI/5461;
            cubes_position[i]=cube_transforms[i].position;
			cube_transforms[i].parent=startPoint;
		}
        //颜色相关
        gridColorChange = false;
        cubeColorChange = false;
        Invoke("SwitchCC", 3f);
        //相机移动相关
        cameraStartPoint = cameraTransform.position;
        StartCoroutine(CameraMovement());
        //延迟播放音频
        thisAudioSource.PlayDelayed(2f);
	}
	// Update is called once per frame
	void Update () {
        Spectrum2Cube();
        DynamicColor();
        CameraLookAt();
	}
	//颜色相关
    void SwitchCC(){
        cubeColorChange = !cubeColorChange;
    }
    void SwitchGC(){
        gridColorChange = !gridColorChange;
    }
	void DynamicColor(){
        if (cubeColorChange)
        {
            for (int i = 0; i < 5461; i++)
            {
                cube_meshRenderers[i].material.SetColor("_Color", new Vector4(Mathf.Lerp(cube_meshRenderers[i].material.color.r, spectrumData[i] * 500f, 0.2f), 0.5f, 1f, 1f));
            }
        }
        if (gridColorChange)
        {
            float gridColor = Mathf.Lerp(gridOverlay.mainColor.r, spectrumData[2000] * 1000, 0.5f);
            if (gridColor > 1)
            {
                gridColor = 1;
            }
            gridOverlay.mainColor = new Vector4(gridColor, 0.5f, 1f, 1f);
        }
    }
    //thisAudioSource当前帧频率波功率,传到对应cube的localScale
    void Spectrum2Cube(){
        thisAudioSource.GetSpectrumData(spectrumData, 0, FFTWindow.BlackmanHarris);
        for (int i = 0; i < 5461; i++)
        {
            cube_transforms[i].localScale = new Vector3(0.15f, Mathf.Lerp(cube_transforms[i].localScale.y, spectrumData[i] * 10000f, 0.5f), 0.15f);
        }
    }
    //相机角度控制
    void CameraLookAt(){
        if (lookat0_1)
        {
            cameraTransform.LookAt(lookat0_1_vector);
        }
        if (lookat1_2)
        {
            cameraTransform.LookAt(lookat1_2_vector);

        }
        if (lookat2_3)
        {
            cameraTransform.LookAt(cubes_position[5190]);
        }
    }
    //网格动画
    IEnumerator GridOff()
    {
        for (int i = 0; i < 51; i++)
        {
            gridOverlay.largeStep += 10;
            yield return new WaitForSeconds(0.02f);
        }
        gridOverlay.showMain = false;

    }
    IEnumerator GridOn()
    {
        gridOverlay.showMain = true;
        gridColorChange = true;
        gridOverlay.largeStep = 500;
        for (int i = 0; i < 49; i++)
        {
            gridOverlay.largeStep -= 10;
            yield return new WaitForSeconds(0.02f);
        }
    }
    //相机重复移动,暂无退出机制
    public void CameraRepeatMove()
    {
        StopAllCoroutines();
        StartCoroutine(CameraMovement());
        if (cubesRotate)
        {
            cubesRotate = false;
            cubes_parent.DORotate(new Vector3(0f, 360f, 0f), 117f, RotateMode.FastBeyond360);
        }
        gridColorChange = false;
    }
    //相机移动脚本
    IEnumerator CameraMovement()
    {
        yield return new WaitForSeconds(20f);
        lookat2_3_vector = new Vector3(cubes_position[5200].x, 12f, cubes_position[5200].z);
        cameraTransform.DOMove(startPoint.position, 20f);
        for (int i = 0; i < 8192; i++)
        {
            moveTos[i] = new Vector3(cubes_position[i].x, 10f, cubes_position[i].z);
        }
        yield return new WaitForSeconds(20f);
        cameraTransform.DOMove(new Vector3(126f, 252f, 1f), 10f);
        cameraTransform.DOLookAt(Vector3.zero, 10f, AxisConstraint.None, Vector3.up);
        yield return new WaitForSeconds(10f);
        cameraTransform.DOMove(new Vector3(106f, 12f, 78f), 19f);
        cameraTransform.DOLookAt(lookat1_2_vector, 19f, AxisConstraint.None, Vector3.up);
        yield return new WaitForSeconds(19f);
        lookat1_2 = false;
        StartCoroutine(GridOn());
        cameraTransform.DOLookAt(lookat2_3_vector, 8f, AxisConstraint.None, Vector3.up);
        cameraTransform.DOMove(new Vector3(cubes_position[5460].x, 12f, cubes_position[5460].z), 8f);
        yield return new WaitForSeconds(8f);
        cameraTransform.DOLookAt(cubes_position[5200], 2f, AxisConstraint.None, Vector3.up);
        yield return new WaitForSeconds(2f);
        int counter = 0;
        while (counter < 2700)
        {
            cameraTransform.LookAt(cubes_position[5200 - counter]);
            cameraTransform.DOMove(moveTos[5460 - counter], 0.01f);
            yield return new WaitForSeconds(0.01f);
            counter += 10;
        }
        cameraTransform.DOLookAt(lookat0_1_vector, 3f, AxisConstraint.None, Vector3.up);
        yield return new WaitForSeconds(3f);
        StartCoroutine(GridOff());
        lookat0_1 = true;
        cameraTransform.DOMove(new Vector3(cameraStartPoint.x, cameraStartPoint.y + 300f, cameraStartPoint.z), 6f);
        yield return new WaitForSeconds(6f);
        lookat0_1 = false;
        CameraRepeatMove();
    }
}


维护日志:
2020-1-8:review,附上项目与源码

你可能感兴趣的:(Unity3D&应用)