(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:在信号处理之中,用来降低信噪比的一种算法。
信噪比:
—噪讯比越高的话,声音的大音量和小音量的音量差会越大(音质猛爆)。
—噪讯比越低的话,声音的大音量和小音量的音量差会越小(音质柔和)。
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
频率分辨率越高,转换出来的数据越精确。(下图,同样情况下,低频率分辨率与高频率分辨率的比较)
而我们声明的浮点数数组的大小既是GetSpectrumData这个方法的窗函数转换数据时所用的频率分辨率,而数组中每个浮点数的值既是谱密度,每单位频率波携带的功率,我们知道了频率分辨率df=8196,那么每个浮点数,既谱密度dB表示的的是哪个频率范围,既音高范围的功率呢?
目前数字音乐领域的采样率通常为44100Hz,但通过分析音频文件[MV] FIESTAR(피에스타) _ Mirror.mp3的频谱,可能是因为通过视频转音频的缘故,基本上16000Hz以上的谱密度都非常低了。
而在Unity内通过分析spectrumData的数值,spectrumData[5500]左右以后的浮点数值与前面有一个断崖似的减少,因此可推断出,spectrumData[5500]对应16000Hz,那么16000/5500*8196=23842,GetSpectrumData的采样的最高频率是在20000~23000赫兹之间,既音频文件23000赫兹以上频率的数据都被忽略掉了。
如果继续深入,可研究声波频率与音高的关系,将spectrumData特定范围的浮点数相加即可体现乐曲中各个音高的谱密度,由于人的听觉系统对音高最为敏感,其视觉效果应该会更加理想。
视频连接: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,附上项目与源码