Unity音频节奏律动相关

本文详细分析了如何在Unity中通过AudioSource获取音频数据并实现音乐可视化,介绍了两种不同的效果,重点讨论了GetSpectrumData方法的工作原理,以及与Shadertoy音频纹理效果的差异,探讨了采样分辨率对结果的影响。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

需求:

音乐可视化。

分析:

必然要获取音频数据,通过数据改变展示。

网上找到的两个效果:

相关文档:比较全的介绍
另一种效果

动态获取音频数据(表现效果好):

代码:

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长度内变化明显)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值