BeatSaberで楽曲のキーを推定する

## 使用するキー推定ライブラリ

### [libKeyFinder](https://github.com/ibsh/libKeyFinder)
使ってみて精度が良さげだったのでこのライブラリを選定。
ほかには[SuperPowererdSDK](https://github.com/superpoweredSDK/Low-Latency-Android-iOS-Linux-Windows-tvOS-macOS-Interactive-Audio-Platform)を試したが、libKeyFinderのほうが良さそうだった。  
[ほかの有償アプリと比べて遜色のない性能を持っている](http://ibrahimshaath.co.uk/keyfinder/comparison.pdf)らしい。

(キー推定のアルゴリズムについてはできたら今度。
ピッチ推定は[このあたり](https://www.cycfi.com/2020/07/fast-and-efficient-pitch-detection-revisited/)か？倍音成分の除去がメイン？)

#### 組み込み時の注意点
Audioは非常にデータが大きいので、スタック領域に確保してはいけない。
(スタック領域はWindowsで2MBなので、2~3秒のデータでさえ乗り切らない)　　
動的に確保するか、staticをつけて静的領域で確保するようにする。
([スタック/ヒープ領域の仕組みはいまだに完全理解できない](https://teratail.com/questions/188732))

また、ライブラリ側でもメモリ制限があるため、長いデータを一気に解析しようとしてはいけない。
  
## 組み込み手順

### 1. [FFTWのlibファイルを作成する](https://stackoverflow.com/questions/39675436/how-to-get-fftw-working-on-windows-for-dummies)

注：BeatSaberは64bitで動作するので64bitのライブラリを作成すること。

FFTWは高速フーリエ変換のライブラリで、libKeyFinderから参照されている。
[ココ](http://www.fftw.org/install/windows.html)からFFTWをダウンロードしてきて項目タイトルのリンクの方法(**Create the import library**まで)でlibファイルを作成する。

なお、途中で開発者用コンソールの使用を求められるが、Windowsのフォルダ検索から辿れる。
![developercommand.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691480/9fe9a165-08a9-2f08-dd6c-fa38e922c3bc.png)

### 2. libKeyFinderのラッパーdllファイルを作成する

[WindowsコンソールアプリでC#からC++の関数を呼ぶ方法](https://qiita.com/TackKaiware/items/27ebcf10bb2624db197a)を参考にしてdllファイルを作成し、
C#から呼び出しテストをする。
C++ライブラリを出力する場所がVisualStudioのバージョンによって若干違うので注意。


##### FFTWのライブラリを組み込む

Visual Stdio2019では、以下の3つの場所にパスを通す必要がある。

 - 構成プロパティ → C/C++ → 追加のインクルードディレクトリ
    - \<path>\fftw-3.3.5-dll64
 - 構成プロパティ → リンカー → 全般 → 追加のライブラリディレクトリ
    - \<path>\fftw-3.3.5-dll64
 - 構成プロパティ → リンカー → 入力 → 追加の依存ファイル
    - libfftw3-3.lib

※libfftw3-3.dllはexeのあるフォルダに入れるか、パスを通す必要があるので注意。

また、libKeyFinderのサンプルコードは若干間違っているので注意。正しくは下記。

```c++

// Static because it retains useful resources for repeat use
static KeyFinder::KeyFinder k;

// Build an empty audio object
KeyFinder::AudioData a;

// Prepare the object for your audio stream
a.setFrameRate(yourAudioStream.framerate);
a.setChannels(yourAudioStream.channels);
a.addToSampleCount(yourAudioStream.length);

// Copy your audio into the object
for (int i = 0; i < yourAudioStream.length; i++) {
  a.setSample(i, yourAudioStream[i]);
}

// Run the analysis
KeyFinder::key_t r =  k.keyOfAudio(a); //<-ここが違う。key_tを使う
```
##### C#でC++のデバッグ

[native code debuggingをONにする](https://docs.microsoft.com/ja-jp/visualstudio/debugger/how-to-debug-managed-and-native-code?view=vs-2019)

ブレークポイントを貼ってデバッグ実行できる。(ただし配列外参照は止まらないでランタイムエラーを出す)

### 3. libKeyFinder.dllをUnityで動かす(省略可)

最初にUnityで実験するステップを踏んだほうが組み込み難易度が下がるので、この項目を追加。
UnityでC++の関数を呼ぶには、Asset直下にPlugins/x86_64フォルダを作成し、その下にdllを置く。

#### サンプルコード
```C#
        int dataLength = 44100 * 5;　//5秒のデータ
        int sampleRate = 44100;
        int bitDepth = 24;
        float ampMax = 0;

        
        AudioClip clip = this.transform.GetComponent<AudioSource>().clip;
        float[] clipData = new float[dataLength * clip.channels];
        clip.GetData(clipData, 0);


        double[] data = new double[dataLength];
        for(int i=0; i<dataLength; i++)
        {
            float monoData = clipData[i * 2] / 2f + clipData[i * 2 + 1] / 2f; // とりあえずモノラルで処理
            data[i] = (double)monoData;

            if(Mathf.Abs(monoData) > ampMax)
            {
                ampMax = monoData; //AudioClipはビット深度を持たないっぽいので妥協案として最大値を求めておく
            }
        }

        sampleRate = clip.frequency;

        int key = KeyFind(data, dataLength, ampMax, sampleRate);
```

### 4. libKeyFinderをBeatSaberで動かす

#### libKeyFinder.dllの配置

以下にlibKeyFinder.dllを設置する。
`\<Beat Saber Folder>\Beat Saber_Data\Plugins\`

#### 楽曲取得

##### 1. 楽曲ファイルパスの取得
デフォルトで入っている楽曲は圧縮された状態でロードされてしまうため解析できない。
そのため、CustomLevel限定。(無理やり読もうとすると[こんなエラー](https://forum.unity.com/threads/cannot-get-data-from-streamed-audio-sample-but-the-sample-is-not-streamed.130371/)が出る)
まずは、CustomLevelで追加されている楽曲のファイルパスを取得する。

CustomLevelPathの取得は[ココ](https://github.com/andruzzzhka/BS_360_Mode/blob/26c78bb00809869966ff6b864f4651adfd1673b4/360Mode/ExtraBeatmapData.cs)を参考に以下のようにした。
下記実装でCustomLevelのときのみ処理が実行されるようになっている。

```C#

IDifficultyBeatmap diffBeatmap = BS_Utils.Plugin.LevelData.GameplayCoreSceneSetupData.difficultyBeatmap;
CustomPreviewBeatmapLevel customPreviewBeatmapLevel = diffBeatmap.level as CustomPreviewBeatmapLevel;

if (customPreviewBeatmapLevel != null)
{
    string customLevelPath = customPreviewBeatmapLevel.customLevelPath;
    string songFileName = customPreviewBeatmapLevel.standardLevelInfoSaveData.songFilename;
    string filepath = customLevelPath + "\\" + songFileName;
}

```

##### 2.ファイルをAudioClipとして読み込んで波形データを取得する

ファイルパスからAudioClipに読み込むときには、[UnityWebRequset](https://docs.unity3d.com/ScriptReference/Networking.UnityWebRequestMultimedia.GetAudioClip.html)を使う。


#### サンプルコード
```C#
        IEnumerator LoadAudioClipWithWebRequest(string filename)
        {
            AudioClip clip; 
           
            using (UnityWebRequest www = UnityWebRequestMultimedia.GetAudioClip("file://" + filename, AudioType.OGGVORBIS))
            {
                yield return www.SendWebRequest(); /* 読み込みを待ってあげないとダメ */
                clip = DownloadHandlerAudioClip.GetContent(www);
            }

            int key = KeyFinder.KeyFind(clip);
        }
```

`www.result == UnityWebRequest.Result.ConnectionError`は構文エラーが出たので取ってしまった。
AudioClipのLoadStatusがLoadedになっていることを確認すればとりあえずはOKのはず。

(ちなみにWebRequestでLoad中は、LoadStatusはUnloadのまま。
`yield return www.SendWebRequest();`でLoadを待ってあげないといけない。
したがってコルーチンの使用が必須になるが、`MonoBehaiviour`を継承すればよいだけ)

波形データが得られれば、libKeyFinderに渡してやればおしまい。

## 参考情報

- テスト用にWaveファイルを読み込むときのライブラリは[AudioFile](https://github.com/adamstark/AudioFile)が使いやすかった
- AudioClipの読み込み方法をプログラム内で[動的に設定する方法がある](https://kan-kikuchi.hatenablog.com/entry/AudioSettings)らしい([AudioImporter](https://docs.unity3d.com/ScriptReference/AudioImporter.html))

- デフォルト楽曲をどうにか読めないか検索しているとき、[譜面自動生成ライブラリ](https://medium.com/swlh/beatmapsynth-an-automatic-song-mapper-for-beat-saber-aa9e59f093f8)を発見した。
ここを参考にすればもしかしたらデフォルト楽曲も読めるようになる？

## あとがき

BeatSaberに組み込むときのサンプルコード全体は[コチラ](https://github.com/dea-azat/SongPerformer)