凹みTips

C++、JavaScript、Unity、ガジェット等の Tips について雑多に書いています。

Unity のオーディオの再生・エフェクト・解析周りについてまとめてみた

はじめに

Unity では単純に音を再生するだけではなくて、エフェクトを掛けたり解析したりする所まで含めて簡単に音を扱える仕組みが整っています。音の解析結果をグラフィカルに出力したり、逆に何らかの入力を音にフィードバックしたりすることもとても簡単に実現できます。

そこで本エントリでは、具体的に何をすれば、またどういうコードを書けばそういったことが実現できるかといった例をまとめてみました。

なお、Unity 5 から導入される Audio Mixer に関しては本エントリの範囲外とします。

オーディオ解析の例

再掲になりますが、本エントリで扱う内容で出来る例は以下になります。

オーディオの仕組みの復習

Unity で音を鳴らす仕組みに関しては、Audio ClipAudio SourceAudio Listener の3つの要素の理解が重要になってきます。

f:id:hecomi:20141111015001p:plain

Audio Clip

Audio Clip は音そのものの情報になります。物理的なものに例えると CD 等のメディアです。ステレオ、モノラルを表す channels や、サンプリング周波数の frequency、そのオーディオファイルの長さの length などがここに含まれています。

Audio Source

Audio Source は Audio Clip を再生する役割をします。物理的なものに例えるとスピーカーです。Audio Clip が 3D サウンド(≠ 2D サウンド)の場合、AudioSource がアタッチされた Game Object の位置から音がなります。音がどのように距離に応じて減衰するかもここで設定できます。音を鳴らす役割のため、保持している Audio Clip の clip、再生中かの isPlaying、ループするかの loop、再生している音量の volume(≠ 局所的な音量)などを変数として持っています。

Audio Listener

Audio Listener は Audio Source を聴く役割をします。物理的なものに例えるとマイクや耳になります。シーンに 1 つだけ存在することが出来るコンポーネントで、基本的にはカメラにくっついています。特別なプロパティはありません。モノラル、ステレオ、サラウンドなのか、といった設定は「Edit > Project Settings > Audio」で Insepctor に表示できる Audio Manager によって設定することが出来ます。

マイク入力を Audio Clip として扱う

音楽ファイルだけではなくてマイク入力も Audio Clip として扱うことが出来ます。これは Microphone を利用します。

適当に録音して再生するサンプルコードは以下になります。

using UnityEngine;
using System.Collections;

// 空の Audio Source を作って置く
[RequireComponent (typeof (AudioSource))]
public class MicrophoneSource : MonoBehaviour 
{
    void Start() 
    {
        // 空の Audio Sourceを取得
        var audio = GetComponent<AudioSource>();
        // Audio Source の Audio Clip をマイク入力に設定
        // 引数は、デバイス名(null ならデフォルト)、ループ、何秒取るか、サンプリング周波数
        audio.clip = Microphone.Start(null, false, 10, 44100);
        // マイクが Ready になるまで待機(一瞬)
        while (Microphone.GetPosition(null) <= 0) {}
        // 再生開始(録った先から再生、スピーカーから出力するとハウリングします)
        audio.Play();
    }
}

これで他の音声ファイルによる Audio Clip と同様に操作・解析できるようになります。

生データの取得

生データはそれぞれのオーディオ要素から取得する API が用意されています。

Audio Clip から取得

AudioClip.GetData() を利用します。第1引数に配列、第2引数にオフセットをセットして第1引数の配列の大きさで指定しただけ生データを取ってきます。

using UnityEngine;
using System.Collections;

public class GetDataSample : MonoBehaviour
{
    public AudioClip clip;
    public int lengthYouWant;

    void Start()
    {
        var data = new float[lengthYouWant];
        clip.GetData(data, 0);
    }
}

現在流れている音でない、音そのものの情報を使って何かするとき(全体の音情報を使って何か事前処理するなど)に使えると思います。

Audio Source から取得

AudioSource.GetOutputData() を利用します。第1引数に配列、第2引数にチャンネル数をセットし、その Audio Source から現在出力されている波形データを取得できます。

using UnityEngine;
using System.Collections;

[RequireComponent(typeof(AudioSource))]
public class WaveOutputter : MonoBehaviour 
{
    private AudioSource audio;
    float[] waveData_ = new float[1024];

    void Start() 
    {
        audio = GetComponent<AudioSource>();
    }

    void Update()
    {
        audio.GetOutputData(waveData_, 1);
    }
}

これで毎フレーム wave_ に現在の波形データが 1024 点分格納されます。試しに波形を出力(描画には Unity の Editor 拡張でインスペクタにグラフを描画する方法を色々調べてみた - 凹みTips を利用)してみるとこんな感じです。

f:id:hecomi:20141109163155p:plain

Audio Listener から取得

上記 Audio Source と同様、Audio Listener でも GetOutputData() を利用することが出来ます。Audio Listener は前述のようにシーンに 1 つしか存在しないことになっているため、static な形で取得することが出来ます。

using UnityEngine;
using System.Collections;

public class WaveOutputter : MonoBehaviour 
{
    float[] waveData_ = new float[1024];

    void Update()
    {
        AudioListener.GetOutputData(waveData_, 1);
    }
}

Audio Listener だと直感的には GetInputData() な感じがしますが、プログラム的には Output なのでこの名前になってるのかな、と思います。

音量の計算

上記例を利用して音量を計算してみましょう。

using UnityEngine;
using System.Linq;

public class WaveOutputter : MonoBehaviour 
{
    private float[] waveData_ = new float[1024];

    void Update()
    {
        AudioListener.GetOutputData(waveData_, 1);
        var volume = waveData_.Select(x => x*x).Sum() / waveData_.Length;
        transform.localScale = Vector3.one * volume;
    }
}

これで音量に応じて Game Object のスケールが変化します。

フーリエ変換による周波数成分の取得

フーリエ変換によって周波数成分を取得できるメソッドとして、AudioSource.GetSpectrumData()AudioListener.GetSpectrumData() が用意されています。後者は GetOutputData() と同じく static なメソッドです。

フーリエ変換は馴染みのない人もいらっしゃると思いますが、音の波を周波数成分に分解してくれる役割をします。今鳴ってる音は高い音が多いのか、低い音が多いのか、ということを調べることが出来ます。

公式のグラフを描画するサンプルを実行してみます。

using UnityEngine;
using System.Collections;

[RequireComponent(typeof(AudioSource))]
public class SpectrumAnalyzer : MonoBehaviour 
{
    private AudioSource audio_;

    void Start()
    {
        audio_ = GetComponent<AudioSource>();
    }

    void Update() {
        var spectrum = audio_.GetSpectrumData(1024, 0, FFTWindow.Hamming);
        for (int i = 1; i < spectrum.Length - 1; ++i) {
            Debug.DrawLine(
                    new Vector3(i - 1, spectrum[i] + 10, 0), 
                    new Vector3(i, spectrum[i + 1] + 10, 0), 
                    Color.red);
            Debug.DrawLine(
                    new Vector3(i - 1, Mathf.Log(spectrum[i - 1]) + 10, 2), 
                    new Vector3(i, Mathf.Log(spectrum[i]) + 10, 2), 
                    Color.cyan);
            Debug.DrawLine(
                    new Vector3(Mathf.Log(i - 1), spectrum[i - 1] - 10, 1), 
                    new Vector3(Mathf.Log(i), spectrum[i] - 10, 1), 
                    Color.green);
            Debug.DrawLine(
                    new Vector3(Mathf.Log(i - 1), Mathf.Log(spectrum[i - 1]), 3), 
                    new Vector3(Mathf.Log(i), Mathf.Log(spectrum[i]), 3), 
                    Color.yellow);
        }
    }
}

f:id:hecomi:20141109173523p:plain

順番に、出力されたデータの通常のグラフ、片対数グラフ(x, y)、両対数グラフになっています。FFTWindow はいわゆる GUI の Window ではなく、FFTフーリエ変換)の計算に用いる窓関数を表しています。

適当な周波数帯を区切って平均を取ればレベルメーターみたいなものが作れます。例えば以下のエントリで作っているサイリウム振ってるエフェクトは、流れてる音楽の周波数に応じて色が変わるようにしています。

曲の低中高領域の成分で localScale を適当に変化させるサンプルを書くと、以下のようになります。

using UnityEngine;

[RequireComponent(typeof(AudioSource))]
public class SpectrumAnalyzer : MonoBehaviour 
{
    public int resolution = 1024;
    public Transform lowMeter, midMeter, highMeter;
    public float lowFreqThreshold = 14700, midFreqThreshold = 29400, highFreqThreshold = 44100;
    public float lowEnhance = 1f, midEnhance = 10f, highEnhance = 100f;

    private AudioSource audio_;

    void Start()
    {
        audio_ = GetComponent<AudioSource>();
        audio_.Play();
    }

    void Update() {
        var spectrum = audio_.GetSpectrumData(resolution, 0, FFTWindow.BlackmanHarris);
        
        var deltaFreq = AudioSettings.outputSampleRate / resolution;
        float low = 0f, mid = 0f, high = 0f;
        
        for (var i = 0; i < resolution; ++i) {
            var freq = deltaFreq * i;
            if      (freq <= lowFreqThreshold)  low  += spectrum[i];
            else if (freq <= midFreqThreshold)  mid  += spectrum[i];
            else if (freq <= highFreqThreshold) high += spectrum[i];
        }

        low  *= lowEnhance;
        mid  *= midEnhance;
        high *= highEnhance;

        lowMeter.localScale  = new Vector3(lowMeter.localScale.x,  low,  lowMeter.localScale.z);
        midMeter.localScale  = new Vector3(midMeter.localScale.x,  mid,  midMeter.localScale.z);
        highMeter.localScale = new Vector3(highMeter.localScale.x, high, highMeter.localScale.z);
    }
}

f:id:hecomi:20141110194636p:plain

計算は面倒なので適当ですが、レベルメーターっぽい何かは上記のようにして簡単に出来ます。AudioSettings.outputSampleRate は出力のサンプリングレートが入っており、デフォルトでは 44100 Hz が入っています(PC環境)。(情報が見つからず恐らくですが)上記のように計算すれば各周波数帯にどれだけの成分が含まれているかが分かると思います。

組み込みのフーリエ変換では満足できない場合は独自に解析する必要があります。例として LPC 分析をしてフォルマントを得る例を載せておきます。

フィルタをかける(プロ版のみ)

プロ版のみですが、Audio Filter という出力される音にエフェクトをかける仕組みが用意されています。

用意されているフィルタの他に、自分でフィルタを作成する仕組みも用意されています。

用意されているフィルタ

用意されているフィルタは以下になります。

f:id:hecomi:20141110203545p:plain

フィルタは複数アタッチすることができるので、例えばバンドパスフィルタを作りたければローパスフィルタとハイパスフィルタを適用します。

自作フィルタ(プロ版のみ)

自作フィルタを作るには、OnAudioFilterRead()MonoBehaviour を継承したスクリプトで定義してエフェクトを掛けます。例えば時間に応じてシュワンシュワン音量が変化するフィルタを作るとすると以下のようになります。

using UnityEngine;

public class MyAudioFilter : MonoBehaviour 
{
    public float pitch = 1f;
    private float t_ = 0;

    void FixedUpdate()
    {
        t_ += Time.fixedDeltaTime;
    }

    void OnAudioFilterRead(float[] data, int channels)
    {
        var angle = 2 * Mathf.PI * t_ / pitch * 0.5f;
        var intensity = Mathf.Pow(Mathf.Sin(angle), 2f);
        for (int i = 0; i < data.Length; ++i) {
            data[i] *= intensity;
        }
    }
}

f:id:hecomi:20141110205700p:plain

OnAudioFilterRead() を使うとインスペクタに音量ゲージが現れるようになります。

検索すればいくつかサンプルが見つかります。例としてローパスフィルタのコードが以下に上がっているので参考になると思います。

残響のあるエリアを設定できる Reverb Zone

エリアに入った Audio Source に対してリバーブ(残響効果)をかける Reverb Zone という仕組みが用意されています。

部屋に入ったり洞窟に入ったりした時に適切なリバーブをかけて反響をかけることでリアルな演出をすることが出来ます。使い方は簡単で、Add Component から Audio Reverb Zoneを選択するだけです。後は適切な Reverb Preset(プリセットの RoomHall やユーザ定義の User 等)を選択し、Min DistanceMax Distance を設定します。この 2 つの値はドキュメントに記載されていますが、Max DistanceMin Distance の間で連続的にリバーブ具合が変化し、Min Distance 内ではフルにリバーブがかかる、という形です。

f:id:hecomi:20141110201130p:plain

動画で見たい場合は以下に上がってました。

音声波形をリアルタイムに生成して再生

また音声をリアルタイムに生成することも出来ます。

有料(プロ版)編(OnAudioFilterRead() を使う)

上記で紹介した OnAudioFilterRead() の引数で与えられる data を無視して自分好みの音を詰めれば音を自前で生成することが出来ます。

上記サイトで基本波形の生成が参照できるので、ここでは別の例として趣向を変えて録音した音を逆再生する、みたいなことをやってみます。

using UnityEngine;
using System.Collections;

[RequireComponent(typeof(AudioSource))]
public class ReverseMicInput : MonoBehaviour 
{
    // 録音する長さ
    public int recordDuration = 3;

    // アタッチされた空の Audio Source(マイク録音データ格納 / 再生用)
    private AudioSource audio_;

    // 録音した音声波形の生データ
    private float[] recordedData_;
    // 現在までに再生した位置
    private int offset_ = 0;
    // 再生中か
    private bool isPlaying_ = false;

    void Start()
    {
        audio_ = GetComponent<AudioSource>();
    }

    [ContextMenu("Record")]
    void Record() 
    {
        // マイクをオープンして 3 秒間録音
        audio_.clip = Microphone.Start(null, false, recordDuration, 44100);
        while (Microphone.GetPosition(null) <= 0) {}
        StartCoroutine(Play());
    }

    IEnumerator Play()
    {
        // 3 秒間待つ
        yield return new WaitForSeconds(recordDuration);

        // 音声波形の生データを取得
        recordedData_ = new float[audio_.clip.samples];
        audio_.clip.GetData(recordedData_, 0);

        // 再生位置を頭に巻き戻して再生
        offset_ = 0;
        isPlaying_ = true;
        audio_.Play();
    }

    void OnAudioFilterRead(float[] data, int channels)
    {
        // ずっと呼ばれるので再生中かフラグで制御
        if (!isPlaying_) {
            return; 
        }

        // 再生が終わったかどうかを見る
        if (offset_ + data.Length > recordedData_.Length) {
            isPlaying_ = false;
            return;
        }

        // 現在再生する音声波形のチャンク分データを詰める
        // ステレオ出力だと channels = 2 になる
        for (int i = 0; i < data.Length / channels; ++i) {
            // 逆から詰めることで逆再生する
            var x = recordedData_[recordedData_.Length - 1 - offset_ - i]; 
            if (channels == 2) {
                data[2*i] = data[2*i + 1] = x;
            } else {
                data[i] = x;
            }
        }

        // 現在までに再生し長さを記録しておく
        offset_ += data.Length / channels;
    }
}

これで、コンテキストメニューから「Record」を選択して「あいうえお」としゃべると3秒後に「おえういあ」と再生されます。なお、余談になりますが逆再生したいだけであれば、AudioClip.SetData() を使ってあげたほうが楽です。

using UnityEngine;
using System.Collections;
using System.Linq;

[RequireComponent(typeof(AudioSource))]
public class ReverseMicInput : MonoBehaviour 
{
    public recordDuration = 3f;
    private AudioSource audio_;

    void Start()
    {
        audio_ = GetComponent<AudioSource>();
    }

    [ContextMenu("Record")]
    void Record() 
    {
        audio_.clip = Microphone.Start(null, false, recordDuration, 44100);
        while (Microphone.GetPosition(null) <= 0) {}
        StartCoroutine(Play());
    }

    IEnumerator Play()
    {
        yield return new WaitForSeconds(recordDuration);

        var data = new float[audio_.clip.samples];
        audio_.clip.GetData(data, 0);
        audio_.clip.SetData(data.Reverse().ToArray(), 0);
        audio_.Play();
    }
}

無料編(OnAudioRead() コールバックを使う)

OnAudioFilterRead() は有料版しか使えませんでしたが、無料でも AudioClip.Create を利用して同じようなことが出来ます。

AudioClip.Create() ではデータが読み込まれる時のコールバック(PCMReaderCallback)を第7引数で与えることが出来ます。これは、AudioClip が読み込まれる際に呼ばれるコールバックで、この中に再生したい音データのチャンクを詰めます。再生位置の変更は第8引数のコールバック(PCMSetPositionCallback)で取得できますので、これらをよしなにセットしてあげれば音を再生することが出来ます。下記は正弦波(プーという音)を生成するサンプルです。

using UnityEngine;
using System.Collections;

[RequireComponent(typeof(AudioSource))]
public class OnAudioReadSample : MonoBehaviour 
{
    public float frequency  = 440;

    private AudioSource audio_;
    private int position_   = 0;
    private int sampleRate_ = 0;

    void Start() 
    {
        // オーディオ出力設定、44100 Hz がデフォルト
        sampleRate_ = AudioSettings.outputSampleRate;

        // Audio Source を取得
        audio_ = GetComponent<AudioSource>();

        // 空の Audio Clip を作成する。引数は、
        //    string name, int lengthSamples, int channels, int frequency, bool _3D, bool stream, 
        //    PCMReaderCallback pcmreadercallback, PCMSetPositionCallback pcmsetpositioncallback
        audio_.clip = AudioClip.Create("Sin Wave", 44100, 1, 44100, false, true, OnAudioRead, OnAudioSetPosition);

        // ループ再生
        audio_.loop = true;
        audio_.Play();
    }

    // AudioClip がデータを読み出す度に呼ばれるコールバック
    void OnAudioRead(float[] data) 
    {
        // 正弦波のデータを詰める
        // チャンク同士を滑らかに接続するように前回位置(position_)を覚えておく
        // NOTE: data.Length は 4096 がデフォルトだが場合によっては 628, 1256 だったりした
        for (int i = 0; i < data.Length; ++i, ++position_) {
            data[i] = Mathf.Sin(2 * Mathf.PI * frequency * position_ / sampleRate_);
        }
    }

    // AudioClip の再生位置が変わる度に呼ばれるコールバック
    // 本サンプルではループ時に newPosition == 0 として呼ばれる
    void OnAudioSetPosition(int newPosition) 
    {
        position_ = newPosition;
    }
}

これを利用してあげれば無料版でもエフェクトを再現したり、リアルタイムなデータの解析を行うことが出来ます。

using UnityEngine;
using System.Collections;

[RequireComponent(typeof(AudioSource))]
public class AudioEffect : MonoBehaviour 
{
    public AudioClip clip;
    public float pitch = 1f;

    private AudioSource audio_;
    private float[] waveRawData_;
    private int position_ = 0;

    void Start()
    {
        audio_ = GetComponent<AudioSource>();
        Play();
    }

    void Play() 
    {
        // 再生前にデータ全部とっておく(∵ OnAudioRead は別スレッドなのでアクセス出来ない)
        // この時、Audio Clip の Load Type(Insepctor から設定可能)は
        // DecompressOnLoad である必要がある
        waveRawData_ = new float[clip.samples];
        clip.GetData(waveRawData_, 0);

        audio_.clip = AudioClip.Create("Sin Wave", clip.samples, clip.channels, clip.frequency, false, true, OnAudioRead, OnAudioSetPosition);
        audio_.Play();
    }

    void OnAudioRead(float[] data) 
    {
        // waveRawData[position_ ~ position_ + data.Length] を解析したり
        // var volume = GetVolume(position_, position_ + data.Length);

        for (int i = 0; i < data.Length; ++i, ++position_) {
            // 何かエフェクトをかけたり
            var effect = Mathf.Sin(position_ * 0.00005f);
            data[i] = waveRawData_[position_] * effect;
        }
    }

    void OnAudioSetPosition(int newPosition) 
    {
        position_ = newPosition;
    }
}

なお、コールバックをセットする処理は瞬間的に重い処理になるのでプリフリーズが発生する恐れがあります。リップシンクプラグインでは、この方法で再生・解析を行っているのですが、この開始時のプチフリーズが問題になっているので Pro 版であれば OnAudioFilterRead() を使う、などの場合分けを入れようと思います。

f:id:hecomi:20141111004056p:plain

AudioClip.GetData() は特に重くないようです、コピーが走っていると思うのですがメモリに展開されているものをコピーするだけなので軽い処理なのだと思われます。

他にも、Unity で対応していないオーディオの形式を専用のライブラリ経由でロードしてデータに詰め込む、みたいなことをやっているのも見かけました。色々な使い方ができると思います。

(おまけ)Unity 5 のオーディオとの関係

Unity5 からは Audio Source の Output 欄に Audio Mixer というものをセットできるようになり、よりグラフィカルに様々なエフェクトをダイナミックに適用できるようになります。以下に動画があがっているので、見てみると雰囲気が掴めると思います。

f:id:hecomi:20141111020132p:plain

このあたりは、どなたかがきっと Unity Advent Calendar 2014 に書いてくれるかな...、と期待しています(誰も書かなかったらそのうちまとめます)。

おわりに

音と 3D による演出は色々と表現力のある分野だと思います。是非みなさんも色々なアイディアを形にしてみてください。