需求:
音乐可视化。
分析:
必然要获取音频数据,通过数据改变展示。
网上找到的两个效果:
动态获取音频数据(表现效果好):
代码:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class AudioVisualization : MonoBehaviour
{
AudioSource audio;//声源
float[] samples = new float[64];//存放频谱数据的数组长度
LineRenderer linerenderer;//画线
public GameObject cube;//cube预制体
Transform[] cubeTransform;//cube预制体的位置
Vector3 cubePos;//中间位置,用以对比cube位置与此帧的频谱数据
public RawImage rawimage;
// Use this for initialization
void Start()
{
GameObject tempCube;
audio = GetComponent<AudioSource>();//获取声源组件
linerenderer = GetComponent<LineRenderer>();//获取画线组件
linerenderer.positionCount = samples.Length;//设定线段的片段数量
cubeTransform = new Transform[samples.Length];//设定数组长度
//将脚本所挂载的gameobject向左移动,使得生成的物体中心正对摄像机
transform.position = new Vector3(-samples.Length * 0.5f, transform.position.y, transform.position.z);
//生成cube,将其位置信息传入cubeTransform数组,并将其设置为脚本所挂载的gameobject的子物体
for (int i = 0; i < samples.Length; i++)
{
tempCube = Instantiate(cube, new Vector3(transform.position.x + i, transform.position.y, transform.position.z), Quaternion.identity);
cubeTransform[i] = tempCube.transform;
cubeTransform[i].parent = transform;
}
rawimage.texture = AudioVisualization.BakeAudioWaveform(audio.clip);
}
// Update is called once per frame
void Update()
{
//获取频谱
audio.GetSpectrumData(samples, 0, FFTWindow.BlackmanHarris);
//循环
for (int i = 0; i < samples.Length; i++)
{
//根据频谱数据设置中间位置的的y的值,根据对应的cubeTransform的位置设置x、z的值
//使用Mathf.Clamp将中间位置的的y限制在一定范围,避免过大
//频谱时越向后越小的,为避免后面的数据变化不明显,故在扩大samples[i]时,乘以50+i * i*0.5f
cubePos.Set(cubeTransform[i].position.x, Mathf.Clamp(samples[i] * (50 + i * i * 0.5f), 0, 100), cubeTransform[i].position.z);
//画线,为使线不会与cube重合,故高度减一
linerenderer.SetPosition(i, cubePos - Vector3.up);
//当cube的y值小于中间位置cubePos的y值时,cube的位置变为cubePos的位置
if (cubeTransform[i].position.y < cubePos.y)
{
cubeTransform[i].position = cubePos;
}
//当cube的y值大于中间位置cubePos的y值时,cube的位置慢慢向下降落
else if (cubeTransform[i].position.y > cubePos.y)
{
cubeTransform[i].position -= new Vector3(0, 0.5f, 0);
}
}
}
}
直接分析clip生成一整张频率纹理:
效果类似点击unity工程里的audioclip后看到的频率图:
代码:
// 传入一个AudioClip 会将AudioClip上挂载的音频文件生成频谱到一张Texture2D上
public static Texture2D BakeAudioWaveform(AudioClip _clip)
{
int resolution = 60; // 这个值可以控制频谱的密度吧,我也不知道
int width = 1920; // 这个是最后生成的Texture2D图片的宽度
int height = 200; // 这个是最后生成的Texture2D图片的高度
resolution = _clip.frequency / resolution;
float[] samples = new float[_clip.samples * _clip.channels];
_clip.GetData(samples, 0);
float[] waveForm = new float[(samples.Length / resolution)];
float min = 0;
float max = 0;
bool inited = false;
for (int i = 0; i < waveForm.Length; i++)
{
waveForm[i] = 0;
for (int j = 0; j < resolution; j++)
{
waveForm[i] += Mathf.Abs(samples[(i * resolution) + j]);
}
if (!inited)
{
min = waveForm[i];
max = waveForm[i];
inited = true;
}
else
{
if (waveForm[i] < min)
{
min = waveForm[i];
}
if (waveForm[i] > max)
{
max = waveForm[i];
}
}
//waveForm[i] /= resolution;
}
Color backgroundColor = Color.black;
Color waveformColor = Color.green;
Color[] blank = new Color[width * height];
Texture2D texture = new Texture2D(width, height);
for (int i = 0; i < blank.Length; ++i)
{
blank[i] = backgroundColor;
}
texture.SetPixels(blank, 0);
float xScale = (float)width / (float)waveForm.Length;
int tMid = (int)(height / 2.0f);
float yScale = 1;
if (max > tMid)
{
yScale = tMid / max;
}
for (int i = 0; i < waveForm.Length; ++i)
{
int x = (int)(i * xScale);
int yOffset = (int)(waveForm[i] * yScale);
int startY = tMid - yOffset;
int endY = tMid + yOffset;
for (int y = startY; y <= endY; ++y)
{
texture.SetPixel(x, y, waveformColor);
}
}
texture.Apply();
return texture;
}
使用:
shader代码:搬的shadertoy音频条效果
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "Sxer/Effect/Screen/MusicFFTView_Screen"{
Properties{
iMouse ("Mouse Pos", Vector) = (100, 100, 0, 0)
_MainTex("_MainTex", 2D) = "white" {}
//音频数据纹理
iChannel0("Music fft Tex",2D) = "white"{}
//加了个轮廓渲染效果
_ShapeTex("Shape Texture",2D) = "black"{}
}
CGINCLUDE
#include "UnityCG.cginc"
#pragma target 3.0
#define vec2 float2
#define vec3 float3
#define vec4 float4
#define mat2 float2x2
#define mat3 float3x3
#define mat4 float4x4
#define iTime _Time.y
#define iGlobalTime _Time.y
#define mod fmod
#define mix lerp
#define fract frac
#define dFdx ddx
#define dFdy ddy
#define texture tex2D
#define texture2D tex2D
#define iResolution _ScreenParams
#define gl_FragCoord ((_iParam.scrPos.xy/_iParam.scrPos.w) * _ScreenParams.xy)
#define PI2 6.28318530718
#define pi 3.14159265358979
#define halfpi (pi * 0.5)
#define oneoverpi (1.0 / pi)
fixed4 iMouse;
sampler2D _MainTex;
float4 _MainTex_TexelSize;
sampler2D iChannel0;
sampler2D _ShapeTex;
//一般不涉及偏移和缩放float4 _MainTex_ST;
struct v2f {
float4 pos : SV_POSITION;
float4 scrPos : TEXCOORD0;
};
v2f vert(appdata_base v) {
v2f o;
o.pos = UnityObjectToClipPos (v.vertex);
o.scrPos = ComputeScreenPos(o.pos);//在顶点着色器中使用完该函数,计算出的结果不为最终正确结果。需要在片元着色器中进行齐次除法。
return o;
}
vec4 main(vec2 fragCoord);
fixed4 frag(v2f _iParam) : COLOR0 {
vec2 fragCoord = gl_FragCoord;//进行齐次除法。获取到屏幕位置
return main(fragCoord);
}
//具体实现
vec4 main(vec2 fragCoord)
{
// create pixel coordinates
vec2 uv = fragCoord.xy / iResolution.xy;
// quantize coordinates
const float bands = 30.0;
const float segs = 40.0;
vec2 p;
p.x = floor(uv.x*bands)/bands;//连续的值变成一段一段
p.y = floor(uv.y*segs)/segs;
// read frequency data from first row of texture
float fft = texture( iChannel0, vec2(p.x,0.25) ).x;//0.25 采样的y坐标会对效果有影响
// led color//高度改变颜色
vec3 color = mix(vec3(0.0, 2.0, 0.0), vec3(2.0, 0.0, 0.0), sqrt(uv.y));
// mask for bar graph
//float mask = (p.y < fft) ? 1.0 : 0.1;
float mask = 1 - step(fft,p.y) + 0.1;
//形状遮罩
float shapettt = texture( _ShapeTex, vec2(uv) ).a;
float maskshape = step(1,shapettt);
// led shape
vec2 d = fract((uv - p) *vec2(bands, segs)) - 0.5;
float led = smoothstep(0.5, 0.35, abs(d.x)) *
smoothstep(0.5, 0.35, abs(d.y));
vec3 ledColor = led*color*mask + maskshape* fixed3(0.8,1,0);
// output final color
vec4 fragColor = vec4(ledColor, 1.0);
return fragColor;
}
ENDCG
SubShader {
Pass {
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
//仅opengl平台,以低精度运算提高速度
#pragma fragmentoption ARB_precision_hint_fastest
ENDCG
}
}
FallBack Off
}
获取音频纹理:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[RequireComponent(typeof(AudioSource))]
[RequireComponent(typeof(LineRenderer))]
public class AudioVisualization : MonoBehaviour
{
public const int sampleSize = 1024;//采样频率
AudioSource audio;//声源
float[] samples = new float[sampleSize];//存放频谱数据的数组长度[64-8192]
LineRenderer linerenderer;//画线
public GameObject cube;//cube预制体
Transform[] cubeTransform;//cube预制体的位置
Vector3 cubePos;//中间位置,用以对比cube位置与此帧的频谱数据
//public RawImage rawimage;
// Use this for initialization
void Start()
{
//获取声源
audio = GetComponent<AudioSource>();//获取声源组件
///cube和linerender展示
//linerenderer = GetComponent<LineRenderer>();//获取画线组件
//linerenderer.positionCount = samples.Length;//设定线段的片段数量
//cubeTransform = new Transform[samples.Length];//设定数组长度
将脚本所挂载的gameobject向左移动,使得生成的物体中心正对摄像机
//transform.position = new Vector3(-samples.Length * 0.5f, transform.position.y, transform.position.z);
生成cube,将其位置信息传入cubeTransform数组,并将其设置为脚本所挂载的gameobject的子物体
//GameObject tempCube;
//for (int i = 0; i < samples.Length; i++)
//{
// tempCube = Instantiate(cube, new Vector3(transform.position.x + i, transform.position.y, transform.position.z), Quaternion.identity);
// cubeTransform[i] = tempCube.transform;
// cubeTransform[i].parent = transform;
//}
//生成音频纹理 (注意纹理的高度影响shader里采样位置,2pixel 采样uv 底部中心为0.25)
texture = new Texture2D(sampleSize, 2);
//texture.
}
public Texture2D texture;
// Update is called once per frame
void Update()
{
//获取频谱
audio.GetSpectrumData(samples, 0, FFTWindow.BlackmanHarris);
//循环
for (int i = 0; i < samples.Length; i++)
{
根据频谱数据设置中间位置的的y的值,根据对应的cubeTransform的位置设置x、z的值
使用Mathf.Clamp将中间位置的的y限制在一定范围,避免过大
频谱时越向后越小的,为避免后面的数据变化不明显,故在扩大samples[i]时,乘以50+i * i*0.5f
//cubePos.Set(cubeTransform[i].position.x, Mathf.Clamp(samples[i] * (50 + i * i * 0.5f), 0, 100), cubeTransform[i].position.z);
画线,为使线不会与cube重合,故高度减一
//linerenderer.SetPosition(i, cubePos - Vector3.up);
当cube的y值小于中间位置cubePos的y值时,cube的位置变为cubePos的位置
//if (cubeTransform[i].position.y < cubePos.y)
//{
// cubeTransform[i].position = cubePos;
//}
当cube的y值大于中间位置cubePos的y值时,cube的位置慢慢向下降落
//else if (cubeTransform[i].position.y > cubePos.y)
//{
// cubeTransform[i].position -= new Vector3(0, 0.5f, 0);
//}
// Debug.Log(samples[i]);
//
texture.SetPixel(i, 0, new Color(Mathf.Clamp(samples[i]*(50+i),0,1), 0, 0, 1));
}
texture.Apply();
}
// 传入一个AudioClip 会将AudioClip上挂载的音频文件生成频谱到一张Texture2D上
public static Texture2D BakeAudioWaveform(AudioClip _clip)
{
int resolution = 60; // 这个值可以控制频谱的密度吧,我也不知道
int width = 1920; // 这个是最后生成的Texture2D图片的宽度
int height = 200; // 这个是最后生成的Texture2D图片的高度
resolution = _clip.frequency / resolution;
float[] samples = new float[_clip.samples * _clip.channels];
_clip.GetData(samples, 0);
float[] waveForm = new float[(samples.Length / resolution)];
float min = 0;
float max = 0;
bool inited = false;
for (int i = 0; i < waveForm.Length; i++)
{
waveForm[i] = 0;
for (int j = 0; j < resolution; j++)
{
waveForm[i] += Mathf.Abs(samples[(i * resolution) + j]);
}
if (!inited)
{
min = waveForm[i];
max = waveForm[i];
inited = true;
}
else
{
if (waveForm[i] < min)
{
min = waveForm[i];
}
if (waveForm[i] > max)
{
max = waveForm[i];
}
}
//waveForm[i] /= resolution;
}
Color backgroundColor = Color.black;
Color waveformColor = Color.green;
Color[] blank = new Color[width * height];
Texture2D texture = new Texture2D(width, height);
for (int i = 0; i < blank.Length; ++i)
{
blank[i] = backgroundColor;
}
texture.SetPixels(blank, 0);
float xScale = (float)width / (float)waveForm.Length;
int tMid = (int)(height / 2.0f);
float yScale = 1;
if (max > tMid)
{
yScale = tMid / max;
}
for (int i = 0; i < waveForm.Length; ++i)
{
int x = (int)(i * xScale);
int yOffset = (int)(waveForm[i] * yScale);
int startY = tMid - yOffset;
int endY = tMid + yOffset;
for (int y = startY; y <= endY; ++y)
{
texture.SetPixel(x, y, waveformColor);
}
}
texture.Apply();
return texture;
}
}
//将音频文件导入项目时,需要将Audio Clip 加载类型设置为Decompress on Load
问题:
获取效果和shadertoy不同:
shadertoy上的音频纹理,基本都是左边是最高的;但我将一样的音乐用unity的audio.GetSpectrumData(samples, 0, FFTWindow.BlackmanHarris);获取后,节奏的变化跟shadertoy不一样。
是采样方式?采样分辨率?还是获取位置?哪里的差异
测试了下,当我把samples拉到64时,跟shadertoy有些相似,但是拉高以后虽然效果很好,但跟shadertoy不一致 怀疑是采样分辨率问题。
整个音乐的频率图是固定的,为什么动态获取每帧的频率时会有那么长的数据。
解释
GetSpectrumData()的实际效果理解
AudioSource.GetSpectrumData
public void GetSpectrumData(float[] samples, int channel, FFTWindow window);
samples:
函数返回值。每个元素代表该音源当前在某个赫兹的强度。针对快速傅里叶变换算法的性能,数组大小必须为2的n次方,最小64,最大8192。
channel:
一般设置为0。该参数与硬件是mono或是stereo有关,mono的话所有的音响会播放同一个音源,而stereo立体声的话不同的音响会播放不同的音源,因此出现了一个channel的概念,通过指定channel可以只取stereo的某个音源的data,设为0的话会按照mono的方式取整个音源。
window:
辅助快速傅里叶变换的窗函数,算法越复杂,声音越柔和,但速度更慢。
public float[] spectrumData=new float[8192];
thisAudioSource.GetSpectrumData(spectrumData,0,FFTWindow.BlackmanHarris);
spectrumData[5500]左右以后的浮点数值与前面有一个断崖似的减少。因此综合音频软件分析的结果可推断出spectrumData[5500]大概对应16000Hz,那么16000/5500*8192=23831,GetSpectrumData的采样的最高频率应该是在20000~23000赫兹之间,既音频文件23000赫兹以上频率的数据都被忽略掉了。
即实际取(5000长度内变化明显)