【Unity】AppleのSpatialAudioであるPHASEのUnityプラグインを日本語翻訳・解説してみた【SpatialAudio】

# PHASEとは?

Apple の物理ベース空間オーディオエンジン ( `Ph` ysical `A` udio `S` patialization `E` ngine)
です。

主にプラグイン自体は iOS, macOS, tvOS, visionOS 向けに作られていますが、ほぼほぼAppleVisionPro 向けと言っても差し支えないでしょう。

- Unity用Plugin のリポジトリ

https://github.com/apple/unityplugins/blob/main/plug-ins/Apple.PHASE/Apple.PHASE_Unity/Assets/Documentation~/Apple.PHASE.md

- 公式のDocument

https://developer.apple.com/documentation/phase/

## 導入方法

導入自体はTOPPANデジタルさんのNote 記事にて詳しく解説されていますので、そちらをご覧ください

https://note.erhoeht-x.jp/n/nfe50ca5add92


## PHASEで出来ること・できないこと

| 項目 | 説明 |
|:---|:-|
|  音の再生・停止 | 基本機能  |
|  パンニング | いわゆる音の定位の設定。音の左右（ステレオ）だけでなく、前後方向や高さ方向の違いによる空間音響にも対応  |
| 音の遮蔽 | 音は障害物があると障害物を反射して進む(回折する)特徴があります。そのため直接音が障害物によって聞こえづらくなる特性が「音の遮蔽」です |
| 空間全体の残響特性の設定 | その名の通り残響(Reverb;リバーブ)の設定です。Preset があるのでそこから選んで実際の音源に対して適応されます |

## PHASEコンポーネントとUnityコンポーネントの対応表


| PHASEのコンポーネント | 説明 | UnityAudio(FMOD)に相当する要素 |
|:-|:-|:-|
| [PHASEListener](https://developer.apple.com/documentation/phase/phaselistener)  | Playerの耳  | AudioListener  |
| [PHASESource](https://developer.apple.com/documentation/phase/phasesource)  | 音源  | AudioSource  |
| [PHASESoundEvent](https://developer.apple.com/documentation/phase/phasesoundevent)  |  音源の再生方法設定クラス | AudioClip + AudioMixerのGroup + etc.  |
| [PHASEOccluder](https://developer.apple.com/documentation/phase/phaseoccluder)  | 遮蔽Object  | 該当なし  |
| [PHASESpatialMixer](https://developer.apple.com/documentation/phase/phasemixer)  | PHASE用のMixer  | AudioMixer  |


### PHASEListener

- Playerの耳に相当するコンポーネント.
- 使い方はUnityのAudioListenerと同様で、基本的にはMainCameraにコンポーネントをくっ付ければOK
    - Update() で現在のTransformの値をNativePlugin に渡しているのでカメラの方向に追従して水平方向の定位感も再現できます
    - AudioListener と同様、Scene 上に一個まで配置可能
    - UnityのAudioListenerは複数おくとConsoleに大量のWarningがRuntimeだと発生しますが、こちらは不明
      - 多分NativePluginを叩く関係上うまく動作しない可能性が高い（要検証）

#### Unity との違い

- PhaseListenrer側にGain (よくわかんない人は≒音量と捉えてもらえればOK) の設定が可能
- Reverb のPreset を設定可能

### PHASESource

- 仮想音源に相当するコンポーネント
- `ほぼAudioSource`

#### Unity との違い

- AudioClip の代わりに PHASESoundEvent を設定
- 点音源だけでなく Volumetric 音源が利用可能
- 3D の聞こえ方やPanning とかはSoundEvent 持ち

::: note
VolumetricSource とは
- Meshを設定してそのMeshの表面から音を発する音源
- 面音源が複数組み合わさったようなもの
:::

### PHASESoundEvent
- 再生する AudioClip, 鳴らし方のロジック をまとめて管理するNodeベースのScriptableObject
- ロジックをノードTree で管理できるので従来コードで作ってた部分をGraphical に作成可能
- Node は大きく分けて `SoundEvent`, `Mixer`, `Parameter` の3種類に分かれる

#### Unity との違い
- Unity でいうところの SoundManager やAudioSource の自作ラッパークラスみたいな立ち位置
- [ECSでやろうとしてるAudioGraph](https://docs.unity3d.com/Packages/com.unity.audio.dspgraph@0.1/manual/index.html) と概念的には似ている
- ノードベースで管理できるため、オーディオエンジニアやサウンドデザイナーさんでも利用可能(使いやすいとは言っていない）

### SoundEvent Node について
#### Sampler Node

![001_SamplerNode.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/65257/47041154-70f4-432c-b2b4-e9d8dc3528b8.png)

指定した AudioClip を連結したMixer で再生するためのノードです。

##### parameter
| 項目 | 説明 |
|:-|:-|
| IsStreamingAsset | AudioClip がStreamingAssets/ 以下にあるのかどうかの指定 |
| AudioClip | 再生するAudioClip |
| CalibrationMode | ラウドネス補正のモード設定 |
| Level | CalibrationMode に紐づくラウドネスの数値設定 |
| Looping | ループ再生フラグ |
| Mixer(Output) | このSamplerで指定した音の再生先であるMixerノード |

::: note
Loudness(ラウドネス) とは
- 各周波数における人間の聴覚上同じ音量となる音圧を定義したもの[単位:phon]
    - 例)低周波数帯域は他の周波数と同じにするには大きな音圧が必要
    - 具体的な指標は「等ラウドネス曲線」や「K特性フィルタ」で調べてみてください
    - 放送業界ではラウドネス指標でコンテンツの音量を調整されています
:::


#### Blend Node

BlendNode はその名の通り複数のSampler の音をBlend して再生・切り替えるためのNodeです.
 ![002_Blend.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/65257/71f8660e-e00d-49ca-97d2-e95ca9a22919.png)

 使用感としてはUnity のMechanim のBlendTree のような感じです。
 https://docs.unity3d.com/ja/2019.4/Manual/class-BlendTree.html

##### parameter
| 項目 | 説明 |
|:-|:-|
| FullGainAtHigh | BlendMode の値がHighValue の時の音量(Gainレベル) |
| HighValue | 高い方の閾値 |
| LowValue | 低い方の閾値 |
| FullGainAtLow | BlendMode の値がLowValue の時の音量(Gainレベル)  |
| BlendMode | ブレンドモード設定<br>・Parameter: PHASESoundEventParameterDouble の値で制御<br>・AutoDistance : PHASESpatialMixer によって自動的に決められる値 |

##### Script からBlendパラメータを制御する方法

PHASESource.cs に `public void SetMetaParameterValue(string inParamName, int/double inValue)` というメソッドが用意されているのでこちらを使います。
https://github.com/apple/unityplugins/blob/main/plug-ins/Apple.PHASE/Apple.PHASE_Unity/Assets/Runtime/PHASESource.cs#L362-L389

先ほどのScreenShot の例だと 
```Blender.cs
public class AmbienceBlender : MonoBehaviour
{
    [Range(0.0f, 50.0f)]
    [SerializeField] private float _crowdBlend = 0.0f;

    [SerializeField] private PHASESource _source = null;

    // Update is called once per frame
    void Update()
    {
        _source.SetMetaParameterValue("CrowdCheer", _crowdBlend);
    }
}
```
![004_Parameter.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/65257/3c0a4509-023b-4a4a-b3ac-0c670ab1d208.png)
みたいな形で行うと、InspectorのSlider の値を反映することができるようです。

#### Container Node
Container Node は名前の通り複数のSamplerNode をまとめて1つのNode として取り扱います。
このとき登録した複数の音(SmaplerNode)は同時に再生されます

![003_Container.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/65257/3dffb4e1-c511-4ad7-82a1-9ae63c48a58f.png)

##### parameter
- とくになし

#### Switch Node
![006_Switch.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/65257/4c5b4e4d-5d9b-46cd-9fe7-9a68a02cf948.png)

##### parameter
| 項目 | 説明 |
|:-|:-|
| SwitchValue | stringで設定するSwitch 用のLabel |
| Parameter | PHASESoundEventParameterString で設定されるstringパラメータ.<br>ここと一致するNodeに信号が送られる仕組み |


#### Random Node
![005_Random.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/65257/8519e0bd-037a-4dd2-b716-f3f96783f9e4.png)

名称通り、再生するSampler Node をランダムで選択するNodeです。
Weight / Weightの合計値 の割合で抽選されるようです。

##### Unityとの違い
Unity6 では最近AudioSource に設定する項目が `AudioClip` から `AudioResource` という概念に更新されました。
このAudioResource のサンプルとしてAudioClip をランダム再生するAudioRandomContainer という機能が提供されています。
https://docs.unity3d.com/ja/2023.2/Manual/AudioRandomContainer.html

このRandomContainer と使用感はほぼほぼ変わらないといった印象です。

##### parameter
| 項目 | 説明 |
|:-|:-|
| Weight | Randomの重みパラメータ。最小値は1で x/Σk の割合で抽選されるようなロジックのようです |


### Mixer Node
Mixer Nodeはその名の通りオーディオミキサー的なNodeで、各音源の最終的な出力方法について設定するNodeです。
現在は
1. Spatial Mixer 
1. Channel Mixer
1. Ambient Mixer 

の3種類が提供されています

#### Spatial Mixer 
SpatialMixer は音源を 3Dの位置・向き・環境効果を適応して再生されます。
また、このMixerは `Occulusion`, `Reverb Modelling`, `距離による音源Culling` を活用できます.
![007_SpatialMixer.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/65257/35d5e6c7-f2c0-49b8-8b8d-fec8cb730079.png)

##### What's Occulusion
端的に言うと遮蔽効果です。 Occuluder (遮蔽する側) の情報に基づき、Listener とPhaseSourceとOcculuderの位置関係に応じて音を遮蔽したり、そのまま聞こえさせたりなどします。

##### What's Reverb Modelling
端的に言うと反響に関する設定です。
音の反射はどういう部屋構造なのか？材質は？等様々な条件で異なるので、それを設定できるというお話です。

##### What's Culling
遮蔽に似ているのですがCulling は条件に応じてOn/Off を制御すると考えてもらえればOKです。
3Dモデルの表示とかでは Occulusion Culling と呼ばれる、遮蔽物で隠れている部分は描画しないという手法があるのですが、それの音versionを今回できるようになるといった形です。

##### parameter
| 項目 | 説明 |
|:-|:-|
| Direct Path Modeler | DirectPath:音が反射なしでListenerに直接伝わること<br> これのOn/Offパラメーター |
| Early Reflections Modeler | 初期反射(音の共鳴時間に沿った初期エコー) を有効化するかどうか？|
| Late Reverb Modeler | 初期反射に続くエコーの有効化をするかどうか？|
| Cull Distance  | 音のCulling 距離.<br>0: Culling無効<br>それ以外: その数値より遠い場合は音が遮断される|
| Listener Directivity Properties  | Listenerに適用する指向性プロパティ<br> [指向性について](https://youtu.be/bZ5pdgA5J38?si=D9icP3p9t0nHMX38&t=387)|
| Source Directivity Properties | 音源に適用する指向性プロパティ |

指向性については↓の公式の資料をご確認ください
https://developer.apple.com/documentation/phase/spatial-mixing?language=objc#Sound-Directivity

#### Channel Mixer 
![008_ChannelMixer.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/65257/fe20f99e-8a28-4b2f-a92d-088620b09099.png)

ChannelMixer はListenerの位置に関係なく、チャンネルに直接出力することで使います.
主な用途は5.1/7.1chなどのマルチチャンネルオーディオだったり、BGMなどのノンダイジェティックサウンドなどをStereo/Monoral で流す場合などに使います

##### parameter
| 項目 | 説明 |
|:-|:-|
| Channel Layout | Channel 設定. Mono~7.1ch まで |

#### Ambient Mixer 
![009_AbmientMixer.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/65257/ba0deae9-beca-4afd-be95-652fe325087b.png)

AmbientMixer は音源を特定の方向に向かって再生します。

##### parameter
| 項目 | 説明 |
|:-|:-|
| Pitch | Rotation.eulerAngle.x |
| Yaw | Rotation.eulerAngle.y |
| Roll | Rotation.eulerAngle.z |
| Channel Layout | Channel 設定. Mono~7.1ch まで |

### Parameter Node
Paremeter Node はその名の通りNode ベースEditor でのパラメータ設定用のNodeです.
現在は以下の4つのNodeがあります

1. PHASESoundEventParameter `Double`
1. PHASESoundEventParameter `Integer`
1. PHASESoundEventParameter `String`
1. PHASESoundEventMappedParameter

上3つはそれぞれの型のパラメータを(外部から)設定可能で、UnityのAudioMixer のExposedParameters といえば伝わりやすいかなと思います。
もう一つのMappedParameter だけ2次元パラメータを設定できますが、Sample や解説が公式にはまだなかったので今回は説明を割愛します


### PHASEOccluder
https://github.com/apple/unityplugins/blob/main/plug-ins/Apple.PHASE/Apple.PHASE_Unity/Assets/Runtime/PHASEOccluder.cs

![010_Occuluder.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/65257/1d533bb6-a6b1-445e-acd1-4ba04d9c7025.png)

PHASEOcculuder はSpatialMixer を利用しているPHASESource の遮蔽物設定用コンポーネントです。
Inspector 上で材質を定義し、その材質による遮蔽をPHASE Engine側でシミュレートしてくれます。

Occuluder の形状は MeshFilterコンポーネントから受け取った頂点情報をそのままPHASEEngine 側に流し込んでいるので、MeshFilterの形状そのままになります。

#### Unity との違い
Unityにはそもそも遮蔽を実現する仕組みがないので、PHASEEngine 独自の機能になります。


# 実際に試してみた

## Occulusion
<iframe width="560" height="315" src="https://www.youtube.com/embed/N2z7oQm4O9M?si=tGWGnAU-GYjVfoG8&amp;start=0" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

## Blend
<iframe width="560" height="315" src="https://www.youtube.com/embed/N2z7oQm4O9M?si=tGWGnAU-GYjVfoG8&amp;start=35" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

## Random/Switch

<iframe width="560" height="315" src="https://www.youtube.com/embed/N2z7oQm4O9M?si=tGWGnAU-GYjVfoG8&amp;start=49" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>