【Unity】AudioClipのデータを直接編集してサウンドデータの構造を学ぶ

UnityのAudioClipのデータを直接編集してサウンドデータの構造を学んでみます。

Unity2019.4.0

はじめに:サンプリングと量子化

この記事ではUnityのAudio Clipのデータを直接編集して音量を変えたり特定秒数だけのデータを編集したりして、サウンドデータの構造を学びます。
しかしその前にサウンドデータに関する最低限の前提知識をこの節で軽くまとめておきます。

さていま、以下のような波で表される音について考えます。
現実世界の音はこのような連続的な波で表すことができます。

f:id:halya_11:20201112002239p:plain
現実世界の波

次にこれをデジタルデータにすることを考えます。
デジタルデータはただの数値の集まりなので、波を一定時間ごとに区切って、その時間ごとにデータ化していきます。
これをサンプリングといいます。

f:id:halya_11:20201112002505p:plain
サンプリング

また「1秒間に何回サンプリングするか」を周波数と呼び、Hz(ヘルツ)という単位で表します。
よくサウンドデータに使われる44100 Hzとは1秒間に44100回サンプリングを行ったデータということになります。

このようにして一定時間に区切ったら、次に各時間ごとに信号レベルをデジタルデータ化します。
デジタルデータもまた非連続な数値なので、結果は以下のように段階を持つデータの集まりになります。
これを量子化と呼びます。

f:id:halya_11:20201112003558p:plain:w400
量子化

サンプリングしたデータ一つを何ビットで表すかを量子化ビット数と呼びます。

以上のことから周波数や量子化ビット数を大きくすればより正確に現実のサウンドを再現できるということになります(データ量は当然増えます)。

ボリュームをコントロールする

基本的なところが理解できたところで実際にAudio Clipを編集していきます。
以下のようにAudioClip.GetData()を呼ぶことですべてのサンプルの値が取得できます。

この値はfloatの配列で、この各float値が一つのサンプリングデータ(前節の説明で一定時間ごとに区切ったデータ)を表します。
またサンプリングデータは-1~1の値で表されます。
そのため以下のように0.5を書けると信号レベルが半分になる、つまり音量が半分になる効果が得られます。

using UnityEngine;

public class AudioExample : MonoBehaviour
{
    [SerializeField] private AudioSource _source;

    private void Start()
    {
        var clip = _source.clip;
        ChangeAudioClipVolume(clip, 0.5f, true);
    }
    
    private static void ChangeAudioClipVolume(AudioClip clip, float magnification)
    {
        // サンプル数 = clip.samplesの値 x チャンネル数(モノラルだったら1、ステレオだったら2)
        var samples = new float[clip.samples * clip.channels];

        clip.GetData(samples, 0);
        for (var i = 0; i < samples.Length; i++)
        {
            // すべてのサンプルの値に引数の値を乗算する
            samples[i] *= magnification;
        }

        clip.SetData(samples, 0);
    }
}
Decompress On Load にする必要アリ

上記のようにAudioClip.GetData()でデータを取得するときの注意点として、AudioClipのLoad TypeがDecompress On Loadになっている必要があります。

f:id:halya_11:20201112004958p:plain
Decompress On Load

これ以外に設定されているとすべてのサンプルの値がゼロで返されてしまいます。

片方のチャンネルだけいじる

さて次に片方のチャンネルだけいじってみます。
音声ファイルはステレオであることを前提とします。

f:id:halya_11:20201112005917p:plain
ステレオ(チャンネル二つ)

ステレオの場合、前節のようにして取得したサンプルデータは1サンプル目の左・1サンプル目の右・2サンプル目の左・2サンプル目の右…といった形式で入っています。

f:id:halya_11:20201112005819p:plain
ステレオの場合のデータ構造

したがって以下のように2で割り切れるインデックスのときだけ処理すれば左だけ処理できるし、逆にすれば右だけ処理できます。

using UnityEngine;

public class AudioExample : MonoBehaviour
{
    [SerializeField] private AudioSource _source;

    private void Start()
    {
        var clip = _source.clip;
        ChangeAudioClipVolume(clip, 0.5f, true);
    }
    
    private static void ChangeAudioClipVolume(AudioClip clip, float magnification, bool left)
    {
        var samples = new float[clip.samples * clip.channels];
        clip.GetData(samples, 0);
        for (var i = 0; i < samples.Length; i++)
        {
            if (left && i % 2 != 0)
            {
                continue;
            }
            if (!left && i % 2 != 1)
            {
                continue;
            }
            samples[i] *= magnification;
        }

        clip.SetData(samples, 0);
    }
}

これを再生すると左耳の音だけ小さくなることが確認できるはずです。

特定の秒数だけいじる

最後に特定の秒数だけのデータを編集してみます。
これを行うにはAudioClip.frequencyから周波数を取得します。
1秒当たりのサンプリング数は周波数 * チャンネル数で表すことができるので、以下のようにすれば特定の秒数のデータだけを処理することができます。

using UnityEngine;

public class AudioExample : MonoBehaviour
{
    [SerializeField] private AudioSource _source;

    private void Start()
    {
        var clip = _source.clip;
        ChangeAudioClipVolume(clip, 0.1f, 5.0f);
    }
    
    private static void ChangeAudioClipVolume(AudioClip clip, float magnification, float durationSec)
    {
        var samples = new float[clip.samples * clip.channels];
        clip.GetData(samples, 0);
        
        // 1秒当たりのサンプリング数 = サンプリング周波数 * チャンネル数
        var samplesPerSec = clip.frequency * clip.channels;
        var limit = samplesPerSec * durationSec;
        
        for (var i = 0; i < limit; i++)
        {
            samples[i] *= magnification;
        }

        clip.SetData(samples, 0);
    }
}

これを再生すると最初の5秒だけ音が小さくなることが確認できます。

参考

docs.unity3d.com