【Unity】シェーダーを利用して音声波形を描く

f:id:setchi_q:20151025203335p:plain

今、広い範囲の音声波形を高速にリアルタイム描画する問題に取り組んでいます。

要件として描画対象の範囲をグリグリ変更できる必要があって、これまでLineRendererやGLによる描画を試みましたがどれも欲しいパフォーマンスに届きませんでした。

そこで、波形情報をテクスチャに埋め込んでシェーダーで描画する方法を試してみました。

AudioClipから波形情報を取得する

var audioSource = GetComponent<AudioSource>();
var samples = new float[audioSource.clip.samples * audioSource.clip.channels];
audioSource.clip.GetData(samples, 0);

で取得できます。
docs.unity3d.com

波形情報をテクスチャに埋め込む

描画領域の横幅 x 1サイズのテクスチャを生成して、色情報に波形データを埋め込んでいきます。
手抜き実装なので r 成分しか使ってませんが、rgba 全ての成分を使えばより小さいテクスチャサイズで実現できます。
また、1サンプルを1ピクセルに対応させるとテクスチャサイズが膨大になりすぎるので適当に間引きします。

public class WaveformRenderer : MonoBehaviour
{
    [SerializeField]
    AudioSource audioSource;
    [SerializeField]
    RawImage image;
    [SerializeField]
    int imageWidth;

    Texture2D texture;
    float[] samples = new float[500000];

    void Start()
    {
        texture = new Texture2D(imageWidth, 1);
        texture.SetPixels(Enumerable.Range(0, imageWidth).Select(_ => Color.clear).ToArray());
        texture.Apply();
        image.texture = texture;
    }

    void Update()
    {
        audioSource.clip.GetData(samples, audioSource.timeSamples);

        int textureX = 0;
        int skipSamples = 200;
        float maxSample = 0;

        for (int i = 0, l = samples.Length; i < l && textureX < imageWidth; i++)
        {
            maxSample = Mathf.Max(maxSample, samples[i]);

            if (i % skipSamples == 0)
            {
                texture.SetPixel(textureX, 0, new Color(maxSample, 0, 0));
                maxSample = 0;
                textureX++;
            }
        }

        texture.Apply();
    }
}

説明に余分なコードは省いています。実際のソースコードはここにあります。
NoteEditor/WaveformRenderer.cs at 21a1878556f1b492a7a9a150339512a6ba5330ca · setchi/NoteEditor · GitHub

このようなテクスチャが生成されます。
(実際は縦1pxです。見やすく縦方向に伸ばしています)
f:id:setchi_q:20151025202741p:plain

シェーダーで波形を描画する

フルソースはここにあります。
NoteEditor/Waveform.shader at a56ec2d55987a8f22c392bfe1739af87a9e97bc9 · setchi/NoteEditor · GitHub


重要なのは下記の部分です。
テクスチャの r 成分からボリュームを取り出して、自身のV座標がボリュームの範囲内なら緑、そうでなければ黒を出力しています。

fixed4 frag(v2f v) : SV_Target{
    float volume = tex2D(_MainTex, v.uv.x).r * 0.5;
    float uvY = v.uv.y - 0.5;

    return lerp(
        fixed4(0, 0, 0, 1),
        fixed4(0, 1, 0, 1),
        -volume < uvY && uvY < volume
    );
}

最終的にこのように出力されます。
f:id:setchi_q:20151025201359p:plain

良くなった点

GLで即時描画していた時は必ず毎フレーム更新する必要がありました。
この方法では波形に変化がなければ前に適用したテクスチャを使えるのでCPUの処理を少なくできます。

テクスチャに直接波形を描画するのと比べた場合、Y方向の展開はGPU側で行うのでCPU → GPUへのデータ転送量が削減できます。