RustでUnity向けオーディオプラグインを書いた話

卒業制作でUnityのネイティブオーディオプラグインを書く必要が出てきたので、せっかくなのでRustで書いてみました[^rust]。

## 完成品

こちらになります => https://github.com/Pctg-x8/unidecim

WindowsとMacで動作を確認済みです。Macの場合は付属スクリプトを利用すると、プラグインフォルダにコピーするだけですぐに使える、i686とx86_64の両方で動作するユニバーサルバイナリを自動生成してくれるようにしてあります。

## 概要

Unityにビルトインされているエフェクトプラグインもそこそこの種類あるので、あまり凝ったことをする場合を除いて今回のような手間をかける必要は全くないです。ただ、今回は「デシメータを使いたい」という場面(自分の中の要望)が出てきてしまったので自作する流れとなりました[^decim]。
Unityのネイティブオーディオプラグインについては[公式ドキュメント](https://docs.unity3d.com/Manual/AudioMixerNativeAudioPlugin.html)を見ればわかるとおり、ネイティブプラグインの一種です。通常のネイティブプラグインはDllImportなどで明示的に呼び出されますが、オーディオプラグインはUnityランタイムから自動で呼び出される点が異なります。

### デシメータについて

英語表記で*"Decimator"*、別名を**"ローファイ(*LoFi*)"**もしくは**"ビットクラッシャー(*Bitcrusher*)"**と呼ばれているディストーション系のエフェクトとなります。サンプリング周波数や量子化ビット数を意図的に落として若干古びた音にするエフェクトです。

## 実装

### ネイティブオーディオプラグインの流れ

![unity_nap_graph.png](https://qiita-image-store.s3.amazonaws.com/0/94115/60b3a66b-ba59-a9a2-7c2c-9f748b2ee66b.png)

`UnityGetAudioEffectDefinitions`で返される情報にはエフェクトの名前をはじめ、各種コールバックメソッドへのポインタが含まれます。定期的に音声処理コールバックが呼ばれるあたりなんかはVSTとかと一緒ですのでその実装経験があればこちらも容易かと思います。音声データの構成もwavファイルなどと同じで、チャンネルごとのデータのまとまりがサンプル数だけ流れてくる形になっています。

```
[L R ...(channels)] [L R ...] [L R ...] ...(length)
```

図をみればわかる通り、一つのネイティブオーディオプラグインは複数のエフェクトを持つことが可能です。

### はじめのいっぽ

ネイティブオーディオプラグインはネイティブプラグインですのでDLL(dylib)として作成します。`UnityGetAudioEffectDefinitions`はGetProcAddress相当で直接呼び出されますので、cdeclかつno_mangleである必要があります。

```rust
#[no_mangle]
extern "C" fn UnityGetAudioEffectDefinitions(defptr: *mut *const std::sync::atomic::AtomicPtr<UnityAudioEffectDefinition>)
  -> c_int
```

戻り値はエフェクトの総数です。
引数の型がかなりゴリゴリですが、Unityからは「プラグイン情報へのポインタの配列の先頭ポインタ」を受け取るためのポインタが渡されますのでポインタが三重になります。

```
プラグインの情報: UnityAudioEffectDefinition
  へのポインタ: *const UnityAudioEffectDefinition
  の配列: [*const UnityAudioEffectDefinition]
  の先頭ポインタ: *const *const UnityAudioEffectDefinition
  を受け取るためのポインタ: *mut *const *const UnityAudioEffectDefinition <- これ
```

「プラグイン情報へのポインタの配列」をDLLが読み込まれている間確保するためにstaticに配置しているのですが、通常のポインタは`Sync`を実装しておらずコンパイルエラーになってしまうのでメモリ上の表現が同じ`AtomicPtr`を利用して回避しています。

また、tomlファイルも若干修正が必要です。dylibを出力するので以下のセクションを追加します。

```toml
[lib]
name = "AudioPlugin_Unidecim"
path = "src/lib.rs"
crate-type = ["dylib"]
```

ファイル名(name)の先頭は必ず**"AudioPlugin"**(大小文字問わず)で始まっている必要があります。Unityはこれを検知して、オーディオプラグインをアセット生成より前にロードするようになります。

### 図で見るデシメータ

今回は量子化ビット数の低減は不要なのでサンプリング周波数の低減のみ実装します。

![decimator_naive.png](https://qiita-image-store.s3.amazonaws.com/0/94115/8dc6fba6-4767-b4dc-b33c-b6704a09d61a.png)

薄い赤が元の波形、青色が出力される波形です。divは保持サンプル数です。入力波形をdivサンプルだけ複製し続けることで動作します。

> 例えば元のサンプリング周波数が44,100Hzの場合、div=2の場合は偶数サンプル目の値が無視されるので22,050Hzの波形を適当に引き延ばしたものとほぼ同等のものとなります

エイリアシングを考慮しない、かなりナイーブな実装ですがこの手順でそれっぽい音が出るようになります。

なのですが、本当にこのまま実装するとパラメータ変更などでphaseの加算よりdivの増加のほうが上回ってしまったりする場合が出てきて少し不都合なので[^div]、うちのプラグインでは上図の①のところで同時にパラメータの変更を処理しています。

1. パラメータの設定コールバック(SetFloatParameter)が来た場合、ひとまず別の変数(`div_pending`)に値を退避しておく
2. 入力値をラッチする際に、同時に`div_pending`から`div`に値を移す

### Rustでユニバーサルバイナリ出力

は(たぶんまだ)できないので自力でがんばります。そんなに難しいことはなくて、次のスクリプトでi686(x86)とx86_64向けのユニバーサルバイナリの生成ができます。

```sh:build-darwin-universal.sh
#!/bin/sh

TARGET_DIR="target/release"
X64_TOOLCHAIN="nightly-x86_64-apple-darwin"
X86_TOOLCHAIN="nightly-i686-apple-darwin"
CARGO_OUTPUT="$TARGET_DIR/libAudioPlugin_Unidecim.dylib"
X86_TARGET="$TARGET_DIR/libAudioPlugin_Unidecim_x86.dylib"
X64_TARGET="$TARGET_DIR/libAudioPlugin_Unidecim_x64.dylib"
FAT_TARGET="$TARGET_DIR/AudioPlugin_Unidecim.bundle"

rustup run $X64_TOOLCHAIN cargo build --release && mv $CARGO_OUTPUT $X64_TARGET
rustup run $X86_TOOLCHAIN cargo build --release && mv $CARGO_OUTPUT $X86_TARGET
lipo $X86_TARGET $X64_TARGET -output $FAT_TARGET -create
```

i686用ツールチェインとx86_64用ツールチェインでそれぞれビルド後、出力ファイルが後続のビルドでぶつからないようにリネームしています。その後、lipoを使って出力を結合してユニバーサルバイナリにしています。

とここまで書いて[この記事](https://qiita.com/moriturus/items/1190614dcbbe31ecfc2f)から[cargo-lipo](https://github.com/TimNN/cargo-lipo)の存在を知りました。iOS向けと書かれているので通常のMacOS向けのバイナリは無理かもしれませんが。あとstaticlibが前提みたいな感じなのでdylibのユニバーサルバイナリを生成する場合はまだこうするしかないと思います。

### Mac環境下でのUnity向けのdylibの注意点

- Editorのデフォルトでは、拡張子が".dylib"のままだとエディタに認識されないので".bundle"に変更する必要があります
- `cargo build`やcargo-lipoでは先頭に"lib"がついてしまってオーディオプラグインとして認識されないので取り除く必要があります

## 参考

- RustでDLLを作る: http://d.sunnyone.org/2015/06/rustdll.html
- How to make Unity find .dylib files? - Unity Answers: https://answers.unity.com/questions/23615/how-to-make-unity-find-dylib-files.html

[^rust]: とくに深いこだわりとかはないけど、公式サンプルがC++だしC++で書いてもおもしろくないかなと
[^decim]: 公式サンプルにLofinatorがあるのでライセンス明記して流用してきてもいい
[^div]: phase == divの条件を一生迎えられない状態にされると波形にならないので音が出なくなる