6
Help us understand the problem. What are the problem?

posted at

AudioSourceの音声データをUnity上でVisualizeする

背景

AudioのVisualize力を上げたいなと思って、UnityのAudioSource周りのAPIを調べてみた。
いくつかサンプルを書いてみたので、Audio+Unityで遊ぶ人の参考になれば幸い.

成果物

サンプルを作ってみた.
動画

  1. 波形をそのまま表示
  2. 周波数成分をそのまま表示
  3. 音の大きさに合わせてtransform変更 (scale,rotation,position)

上記3つをやってみた

基本

AudioSourceで知っとくと良さそうな基本のAPIをいくつか

AudioSource source;

// timeSamples: intで現在の実行中のサンプリング位置を取得する. 例: 44,100Hzでサンプリング時、0.5sec後には `22,050`が返ってくる.
source.timeSamples;

// GetData: AudioClipのデータを `float[]` で取得する.
var data = new float[source.clip.channels * source.clip.samples];
source.clip.GetData(data, 0);

// GetSpectrumData: FFTされたデータを取得する. offset, 窓関数も指定できる
var spectram = new float[2048];
source.GetSpectrumData(spectram, 0, FFTWindow.BlackmanHarris);

// frequency: サンプリング周波数. 例: 44,100Hz
source.clip.frequency;

1. 波形をそのまま表示

AudioClipからGetDataして、それを一定時間ごとにLineRendererを使って表示することで波形の表示できる.

public class SequenceLineRenderer : MonoBehaviour, IMusicRender
{
    [SerializeField] private LineRenderer lineRenderer;
    [SerializeField] private float waveLength = 20.0f;
    [SerializeField] private float yLength = 10f;

    private AudioSource source = default;
    private float[] data = default;
    private int sampleStep = default;
    private Vector3[] samplingLinePoints = default;

    private void Start()
    {
        var source = GetComponent<AudioSource>();
        var clip = source.clip;
        var data = new float[clip.channels * clip.samples];
        source.clip.GetData(data, 0);

        Prepare(source, data);
    }

    public void Prepare(AudioSource source, float[] data)
    {
        this.source = source;
        this.data = data;

        var fps = Mathf.Max(60f, 1f / Time.fixedDeltaTime);
        var clip = source.clip;
        sampleStep = (int) (clip.frequency / fps);
        samplingLinePoints = new Vector3[sampleStep];
    }

    private void FixedUpdate()
    {
        if (source.isPlaying && source.timeSamples < data.Length)
        {
            var startIndex = source.timeSamples;
            var endIndex = source.timeSamples + sampleStep;
            Inflate(
                data, startIndex, endIndex,
                samplingLinePoints,
                waveLength, -waveLength / 2f, yLength
            );
            Render(samplingLinePoints);
        }
        else
        {
            Reset();
        }
    }

    private void Render(Vector3[] points)
    {
        if (points == null) return;
        lineRenderer.positionCount = points.Length;
        lineRenderer.SetPositions(points);
    }

    private void Reset()
    {
        var x = -waveLength / 2;
        Render(new[]
        {
            new Vector3(-x, 0, 0) + this.transform.position,
            this.transform.position,
            new Vector3(x, 0, 0) + this.transform.position,
        });
    }

    public void Inflate(
        float[] target, int start, int end,
        Vector3[] result,
        float xLength, float xOffset, float yLength
    )
    {
        var samples = Math.Max(end - start, 1f);
        var xStep = xLength / samples;
        var j = 0;

        for (var i = start; i < end; i++, j++)
        {
            var x = xOffset + xStep * j;
            var y = i < target.Length ? target[i] * yLength : 0f;
            var p = new Vector3(x, y, 0) + this.transform.position;
            result[j] = p;
        }
    }
}

2. 周波数成分をそのまま表示する

同じノリでfftしたデータをLineRendererで表示する.

public class SpectramLineRenderer : MonoBehaviour, IMusicRender
{
    [SerializeField] private LineRenderer lineRenderer;
    [SerializeField] private float waveLength = 20.0f;
    [SerializeField] private float yLength = 10f;

    private AudioSource source = null;
    private float[] spectram = null;
    private Vector3[] points = null;
    private const int FFT_RESOLUTION = 2048;

    private void Start()
    {
        var source = GetComponent<AudioSource>();
        var clip = source.clip;
        var data = new float[clip.channels * clip.samples];
        source.clip.GetData(data, 0);

        Prepare(source, data);
    }

    public void Prepare(AudioSource source, float[] data)
    {
        this.source = source;
        this.spectram = new float[FFT_RESOLUTION];
        this.points = new Vector3[FFT_RESOLUTION];
    }

    public void FixedUpdate()
    {
        Render();
    }

    private void Render()
    {
        source.GetSpectrumData(spectram, 0, FFTWindow.BlackmanHarris);
        var xStart = -waveLength / 2;
        var xStep = waveLength / spectram.Length;
        for (var i = 0; i < points.Length; i++)
        {
            var y = spectram[i] * yLength;
            var x = xStart + xStep * i;
            var p = new Vector3(x, y, 0) + transform.position;
            points[i] = p;
        }

        Render(points);
    }

    private void Render(Vector3[] points)
    {
        if (points == null) return;
        lineRenderer.positionCount = points.Length;
        lineRenderer.SetPositions(points);
    }

    private void Reset()
    {
        var x = waveLength / 2;
        Render(new[]
        {
            new Vector3(-x, 0, 0) + transform.position,
            new Vector3(0, 0, 0) + transform.position,
            new Vector3(x, 0, 0) + transform.position,
        });
    }
}

3. 音量に合わせてscaleを変化させる

区間内の最大の振れ幅を取得して、表示する.
そのままだと変化が大きすぎるのでlerpしてあげるとみやすい

public class VolumeSizeRenderer : MonoBehaviour
{
    [SerializeField] private float scaleFactor = 1f;
    [SerializeField] private float lerp = 0.5f;

    private AudioSource source = default;
    private float[] data = default;
    private Vector3 initialScale = default;
    private int sampleStep = default;

    private void Awake()
    {
        this.initialScale = transform.localScale;
    }

    private void Start()
    {
        var source = GetComponent<AudioSource>();
        var clip = source.clip;
        var data = new float[clip.channels * clip.samples];
        source.clip.GetData(data, 0);

        Prepare(source, data);
    }

    public void Prepare(AudioSource source, float[] monoData)
    {
        this.source = source;
        this.data = monoData;

        var fps = Mathf.Max(60f, 1f / Time.fixedDeltaTime);
        var clip = source.clip;
        this.sampleStep = (int) (clip.frequency / fps);
    }

    private void FixedUpdate()
    {
        if (source.isPlaying && source.timeSamples < data.Length)
        {
            var startIndex = source.timeSamples;
            var endIndex = Math.Min(source.timeSamples + sampleStep, data.Length);
            var level = DetectVolumeLevel(data, startIndex, endIndex);
            Render(level);
        }
        else
        {
            Reset();
        }
    }

    private void Render(float size)
    {
        var diff = initialScale * this.scaleFactor * size;
        transform.localScale = Vector3.Lerp(transform.localScale, diff, lerp);
    }

    private void Reset()
    {
        transform.localScale = initialScale;
    }

    private float DetectVolumeLevel(float[] data, int start, int end)
    {
        var max = 0f;
        var min = 0f;

        for (var i = start; i < end; i++)
        {
            if (max < data[i]) max = data[i];
            if (min > data[i]) min = data[i];
        }

        return max - min;
    }
}

まとめ

とくにライブラリとかもなく、Unityのデフォルトの機能でFFTまでデータ取得できるのは便利..!
色々応用が効くので、trasnform意外もいじってみたい

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
6
Help us understand the problem. What are the problem?