はじめに
Unity では単純に音を再生するだけではなくて、エフェクトを掛けたり解析したりする所まで含めて簡単に音を扱える仕組みが整っています。音の解析結果をグラフィカルに出力したり、逆に何らかの入力を音にフィードバックしたりすることもとても簡単に実現できます。
そこで本エントリでは、具体的に何をすれば、またどういうコードを書けばそういったことが実現できるかといった例をまとめてみました。
なお、Unity 5 から導入される Audio Mixer に関しては本エントリの範囲外とします。
オーディオ解析の例
再掲になりますが、本エントリで扱う内容で出来る例は以下になります。
オーディオの仕組みの復習
Unity で音を鳴らす仕組みに関しては、Audio Clip、Audio Source、Audio Listener の3つの要素の理解が重要になってきます。
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 を利用)してみるとこんな感じです。
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 なメソッドです。
- Unity - Scripting API: AudioSource.GetSpectrumData
- Unity - Scripting API: AudioListener.GetSpectrumData
フーリエ変換は馴染みのない人もいらっしゃると思いますが、音の波を周波数成分に分解してくれる役割をします。今鳴ってる音は高い音が多いのか、低い音が多いのか、ということを調べることが出来ます。
公式のグラフを描画するサンプルを実行してみます。
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); } } }
順番に、出力されたデータの通常のグラフ、片対数グラフ(x, y)、両対数グラフになっています。FFTWindow
はいわゆる GUI の Window ではなく、FFT(フーリエ変換)の計算に用いる窓関数を表しています。
適当な周波数帯を区切って平均を取ればレベルメーターみたいなものが作れます。例えば以下のエントリで作っているサイリウム振ってるエフェクトは、流れてる音楽の周波数に応じて色が変わるようにしています。
- Unity でシェーダを使って 20,000 人が音楽に合わせてサイリウム振ってる様子を作ってみた - 凹みTips
- CyalumeLive/CyalumeAudioBridge.cs at master · hecomi/CyalumeLive · GitHub
曲の低中高領域の成分で 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); } }
計算は面倒なので適当ですが、レベルメーターっぽい何かは上記のようにして簡単に出来ます。AudioSettings.outputSampleRate
は出力のサンプリングレートが入っており、デフォルトでは 44100 Hz が入っています(PC環境)。(情報が見つからず恐らくですが)上記のように計算すれば各周波数帯にどれだけの成分が含まれているかが分かると思います。
組み込みのフーリエ変換では満足できない場合は独自に解析する必要があります。例として LPC 分析をしてフォルマントを得る例を載せておきます。
フィルタをかける(プロ版のみ)
プロ版のみですが、Audio Filter という出力される音にエフェクトをかける仕組みが用意されています。
用意されているフィルタの他に、自分でフィルタを作成する仕組みも用意されています。
用意されているフィルタ
用意されているフィルタは以下になります。
- Unity - Manual: Audio Low Pass Filter
- ローパスフィルタ、カットオフ周波数より高い周波数を指定した Q 値に従ってカットするフィルタ
- こもったような音になる
- Unity - Manual: Audio High Pass Filter
- ハイパスパスフィルタ、カットオフ周波数より低い周波数を指定した Q 値に従ってカットするフィルタ
- Unity - Manual: Audio Echo Filter
- エコーフィルタ、その名の通りエコーする
- Unity - Manual: Audio Distortion Filter
- ディストーションフィルタ、割れたみたいな音になる
- Unity - Manual: Audio Reverb Filter
- リバーブフィルタ、残響がかかった音になる
- Unity - Manual: Audio Chorus Filter
- コーラスフィルタ、合唱のようになるとのこと(デフォルトだとシュヲンシュヲンとなった)
フィルタは複数アタッチすることができるので、例えばバンドパスフィルタを作りたければローパスフィルタとハイパスフィルタを適用します。
自作フィルタ(プロ版のみ)
自作フィルタを作るには、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; } } }
OnAudioFilterRead()
を使うとインスペクタに音量ゲージが現れるようになります。
検索すればいくつかサンプルが見つかります。例としてローパスフィルタのコードが以下に上がっているので参考になると思います。
残響のあるエリアを設定できる Reverb Zone
エリアに入った Audio Source に対してリバーブ(残響効果)をかける Reverb Zone という仕組みが用意されています。
部屋に入ったり洞窟に入ったりした時に適切なリバーブをかけて反響をかけることでリアルな演出をすることが出来ます。使い方は簡単で、Add Component から Audio Reverb Zone
を選択するだけです。後は適切な Reverb Preset
(プリセットの Room
、Hall
やユーザ定義の User
等)を選択し、Min Distance
と Max Distance
を設定します。この 2 つの値はドキュメントに記載されていますが、Max Distance
〜 Min Distance
の間で連続的にリバーブ具合が変化し、Min Distance
内ではフルにリバーブがかかる、という形です。
動画で見たい場合は以下に上がってました。
音声波形をリアルタイムに生成して再生
また音声をリアルタイムに生成することも出来ます。
有料(プロ版)編(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()
を使う、などの場合分けを入れようと思います。
AudioClip.GetData()
は特に重くないようです、コピーが走っていると思うのですがメモリに展開されているものをコピーするだけなので軽い処理なのだと思われます。
他にも、Unity で対応していないオーディオの形式を専用のライブラリ経由でロードしてデータに詰め込む、みたいなことをやっているのも見かけました。色々な使い方ができると思います。
(おまけ)Unity 5 のオーディオとの関係
Unity5 からは Audio Source の Output
欄に Audio Mixer というものをセットできるようになり、よりグラフィカルに様々なエフェクトをダイナミックに適用できるようになります。以下に動画があがっているので、見てみると雰囲気が掴めると思います。
- http://unity3d.com/learn/tutorials/modules/beginner/5-pre-order-beta/audio-effects
- Unity 5.0 pre-order beta now available! | Unity Blog
このあたりは、どなたかがきっと Unity Advent Calendar 2014 に書いてくれるかな...、と期待しています(誰も書かなかったらそのうちまとめます)。
おわりに
音と 3D による演出は色々と表現力のある分野だと思います。是非みなさんも色々なアイディアを形にしてみてください。