JUCEで作った3DオーディオPlugin紹介

# はじめに
最近ゲームや音楽、映画などは没入型のユーザー体験(Immersive Experience)を作るために3Dオーディオを重要なものと見なしています。
本記事は3Dオーディオ(Spatial Audio)のコンセプトとJUCEで作ったプラグインを紹介しております。

# 3Dオーディオについて
3Dオーディオとは三次元的な音という事です。色々な音源の方向と距離を配置して、3D音場を作ることが出来ます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2294466/d9b710e9-ce1b-276a-4340-3c077a877372.png)
図。音場の例。3Dオーディオの再生で犬、車、鳥の方向や距離などを感知できます。

　　

3Dオーディオの作り方はたくさんありますが、この記事では「バイノーラル再生」を中心とします。バイノーラル処理ではヘッドホンで3Dオーディオを再生できます。
[こちらのクリップ](http://www.meoworkshop.org/wp-content/uploads/2020/11/kougen_binural.wav)でバイノーラル音を聞くことが出来ます(ヘッドホンを使って下さい）。まるで音が頭の周りで回っているかのように感じられますね。

　　

# バイノーラル方式

聞こえる音の方向が分かるようにするには三つの特性が必要です。
一つ目は両耳に届いた音の時間差異（両耳間時間差、Inter-aural Time Difference, ITD）です。例えば、音源が左耳に近ければ信号は右耳より左耳に早く届きます。そのため音源は左側だと感知されます。
二つ目の特性は届いた信号のレベル（両耳間強度差、Inter-aural Level Difference, ILD）です。また、音源が左側にあれば、左耳に届いた信号は右耳よりレベルが高くなります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2294466/c3f2bcb7-5f04-4ce5-adb8-30db1e994e9e.png)
図。左耳側に置いたスピーカーの発した音は右耳より早く、高いレベルで左耳に届く。
　　

最後に耳の形状は音の周波数を変化されますのでリアルな3Dオーディオ体験を作るには個々の耳の形状の情報が必要です。

この三つの特性は頭部伝達関数(Head-Related Transfer Function、HRTF）に含まれます。
HRTFはデジタルフィルター（FIRフィルター）としてバイノーラル処理ができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2294466/d9ac792d-cc56-3932-42b0-60d0afc39dfc.png)
図。左耳のHRTF。x軸：周波[Hz] y軸：音源方位角 [1]
　　
　　

**SOFA Files**

音源の角度によってHRTFは変わりますので全ての角度とHRTFを測定しなければなりません。測定の結果、大量のデータが生じます。SOFAと言うファイル形式にHRTFを格納できます。


# Orbiter Pluginの紹介

[Orbiter](https://github.com/superkittens/Orbiter)はJUCEで作ったオープンソース•バイノーラル•プラグインです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2294466/d0c614ad-fe77-ed65-1f37-0cd122b45911.png)

ウインドウの左側で音場の表を見ることが出来ます。オレンジの円をクリック＆ドラッグして音源の距離と方向を変更できます。
Elevationスライダーは音源の高さを変えられます。

プラグインを使用する前にSOFAファイルが必要です。「Open SOFA」ボタンをクリックしてSOFAファイルを選択できます。

### DSP
HRTFはFIRフィルターとして処理できます。時間領域の処理が重いので、周波数領域で処理しています。サンプルはバッファに入力してフーリエ変換を使います。その後、HRTFと掛けます。最後にフーリエ逆変換を使ってバイノーラル音を出しています。
下の図でメソッドの詳細を表します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2294466/90797bc8-0f04-b1dc-e839-cfe63af1a68b.png)

周波数領域で処理しますのでOverlap And Add(重畳加算法)のステップが必要です。
音源方向が変わる時にHRTFも変わりますのでCrossfadeのステップが必要です。
  
### Code
**SOFA File Load**
HRTFデータや処理機能などはReferenceCountedSOFAのオブジェクトに含まれています。ロードしたSOFAのデータはBasicSOFAのオブジェクトに入力されHRTFProcessorはオーディオサンプルを処理します。
SOFAファイルが変わる時に新しいReferenceCountedSOFAオブジェクトが生成されたり古いものが削除されたりします。

```cpp:PluginProcessor.h
class ReferenceCountedSOFA : public juce::ReferenceCountedObject
{
    public:
        typedef juce::ReferenceCountedObjectPtr<ReferenceCountedSOFA> Ptr;
        
        ReferenceCountedSOFA(){}
        BasicSOFA::BasicSOFA    *getSOFA() { return &sofa; }
        
        BasicSOFA::BasicSOFA    sofa;
        HRTFProcessor           leftHRTFProcessor;
        HRTFProcessor           rightHRTFProcessor;
        
        size_t                  hrirSize;
        
    private:
        
        JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(ReferenceCountedSOFA)
};
``` 
　　
　　
**Source Location Change**
音源の方向が変わる場合はleft/rightHRTFProcessorが自動的にHRTFを変えます。


```cpp:PluginProcessor.cpp
void OrbiterAudioProcessor::checkForGUIParameterChanges()
{
  //...

  if ((hrirLeft != nullptr) && (hrirRight != nullptr))
  {
    retainedSofa->leftHRTFProcessor.swapHRIR(hrirLeft, currentSOFA->hrirSize, currentSOFA->sofa.getMinImpulseDelay() * 0.75);
    retainedSofa->rightHRTFProcessor.swapHRIR(hrirRight, currentSOFA->hrirSize, currentSOFA->sofa.getMinImpulseDelay() * 0.75);
  }
  
  //...
}
```
　　
　　
**Processing**
OrbiterAudioProcessor::processBlock()でサンプルはバッファに入力しています。バッファが満ちたら、処理が始まります。その後バイノーラルな出力はoutput bufferに入ります。

```cpp:PluginProcessor.cpp
void OrbiterAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages)
{    
    //...   
    
    if (sofaFileLoaded)
    {
        ReferenceCountedSOFA::Ptr retainedSofa(currentSOFA);
        
        for (int channel = 0; channel < 1; ++channel)
        {
            auto *channelData = buffer.getWritePointer (channel);

            //...
            
            //  Add samples to HRTF Processor input buffer
            //  If input buffer is full, binaural processing starts
            retainedSofa->leftHRTFProcessor.addSamples(channelData, buffer.getNumSamples());
            retainedSofa->rightHRTFProcessor.addSamples(channelData, buffer.getNumSamples());
            
            //  Get binaural output samples from HRTFProcessor
            auto left = retainedSofa->leftHRTFProcessor.getOutput(buffer.getNumSamples());
            auto right = retainedSofa->rightHRTFProcessor.getOutput(buffer.getNumSamples());
            
            if (left.size() != 0 || right.size() != 0)
            {
                auto *outLeft = buffer.getWritePointer(0);
                auto *outRight = buffer.getWritePointer(1);

                for (auto i = 0; i < buffer.getNumSamples(); ++i)
                {
                    outLeft[i] = left[i];
                    outRight[i] = right[i];
                }
                
            //...
            }
        }
    }
}
```

# まとめ
3DオーディオとOrbiterプラグインを紹介しました。3Dオーディオの技術や方式などはたくさんありますがヘッドホンで再生するためにはバイノーラル処理を使います。基本的にバイノーラル処理のコンセプトは分かりやすいですが、リアルな音場を作るには色々と複雑な物理を考慮しなければなりません。

# Citations
[1] HRTF plotted with data from the RIEC HRTC Dataset
(http://www.riec.tohoku.ac.jp/pub/hrtf/index.html)
K. Watanabe, Y. Iwaya, Y. Suzuki, S. Takane, and S. Sato, "Dataset of head-related transfer functions measured with a circular loudspeaker array," Acoust. Sci. & Tech. 35(3), 159 – 165(2014)