使用するキー推定ライブラリ
libKeyFinder
使ってみて精度が良さげだったのでこのライブラリを選定。
ほかにはSuperPowererdSDKを試したが、libKeyFinderのほうが良さそうだった。
ほかの有償アプリと比べて遜色のない性能を持っているらしい。
(キー推定のアルゴリズムについてはできたら今度。
ピッチ推定はこのあたりか?倍音成分の除去がメイン?)
組み込み時の注意点
Audioは非常にデータが大きいので、スタック領域に確保してはいけない。
(スタック領域はWindowsで2MBなので、2~3秒のデータでさえ乗り切らない)
動的に確保するか、staticをつけて静的領域で確保するようにする。
(スタック/ヒープ領域の仕組みはいまだに完全理解できない)
また、ライブラリ側でもメモリ制限があるため、長いデータを一気に解析しようとしてはいけない。
組み込み手順
1. FFTWのlibファイルを作成する
注:BeatSaberは64bitで動作するので64bitのライブラリを作成すること。
FFTWは高速フーリエ変換のライブラリで、libKeyFinderから参照されている。
ココからFFTWをダウンロードしてきて項目タイトルのリンクの方法(Create the import libraryまで)でlibファイルを作成する。
なお、途中で開発者用コンソールの使用を求められるが、Windowsのフォルダ検索から辿れる。
2. libKeyFinderのラッパーdllファイルを作成する
WindowsコンソールアプリでC#からC++の関数を呼ぶ方法を参考にしてdllファイルを作成し、
C#から呼び出しテストをする。
C++ライブラリを出力する場所がVisualStudioのバージョンによって若干違うので注意。
また、libKeyFinderのサンプルコードは若干間違っているので注意。正しくは下記。
// Static because it retains useful resources for repeat usestaticKeyFinder::KeyFinderk;// Build an empty audio objectKeyFinder::AudioDataa;// Prepare the object for your audio streama.setFrameRate(yourAudioStream.framerate);a.setChannels(yourAudioStream.channels);a.addToSampleCount(yourAudioStream.length);// Copy your audio into the objectfor(inti=0;i<yourAudioStream.length;i++){a.setSample(i,yourAudioStream[i]);}// Run the analysisKeyFinder::key_tr=k.keyOfAudio(a);//<-ここが違う。key_tを使う
3. libKeyFinder.dllをUnityで動かす(省略可)
最初にUnityで実験するステップを踏んだほうが組み込み難易度が下がるので、この項目を追加。
UnityでC++の関数を呼ぶには、Asset直下にPlugins/x86_64フォルダを作成し、その下にdllを置く。
サンプルコード
intdataLength=44100*5; //5秒のデータintsampleRate=44100;intbitDepth=24;floatampMax=0;AudioClipclip=this.transform.GetComponent<AudioSource>().clip;float[]clipData=newfloat[dataLength*clip.channels];clip.GetData(clipData,0);double[]data=newdouble[dataLength];for(inti=0;i<dataLength;i++){floatmonoData=clipData[i*2]/2f+clipData[i*2+1]/2f;// とりあえずモノラルで処理data[i]=(double)monoData;if(Mathf.Abs(monoData)>ampMax){ampMax=monoData;//AudioClipはビット深度を持たないっぽいので妥協案として最大値を求めておく}}sampleRate=clip.frequency;intkey=KeyFind(data,dataLength,ampMax,sampleRate);
4. libKeyFinderをBeatSaberで動かす
libKeyFinder.dllの配置
以下にlibKeyFinder.dllを設置する。\<Beat Saber Folder>\Beat Saber_Data\Plugins\
楽曲取得
1. 楽曲ファイルパスの取得
デフォルトで入っている楽曲は圧縮された状態でロードされてしまうため解析できない。
そのため、CustomLevel限定。(無理やり読もうとするとこんなエラーが出る)
まずは、CustomLevelで追加されている楽曲のファイルパスを取得する。
CustomLevelPathの取得はココを参考に以下のようにした。
下記実装でCustomLevelのときのみ処理が実行されるようになっている。
IDifficultyBeatmapdiffBeatmap=BS_Utils.Plugin.LevelData.GameplayCoreSceneSetupData.difficultyBeatmap;CustomPreviewBeatmapLevelcustomPreviewBeatmapLevel=diffBeatmap.levelasCustomPreviewBeatmapLevel;if(customPreviewBeatmapLevel!=null){stringcustomLevelPath=customPreviewBeatmapLevel.customLevelPath;stringsongFileName=customPreviewBeatmapLevel.standardLevelInfoSaveData.songFilename;stringfilepath=customLevelPath+"\\"+songFileName;}
2.ファイルをAudioClipとして読み込んで波形データを取得する
ファイルパスからAudioClipに読み込むときには、UnityWebRequsetを使う。
サンプルコード
IEnumeratorLoadAudioClipWithWebRequest(stringfilename){AudioClipclip;using(UnityWebRequestwww=UnityWebRequestMultimedia.GetAudioClip("file://"+filename,AudioType.OGGVORBIS)){yieldreturnwww.SendWebRequest();/* 読み込みを待ってあげないとダメ */clip=DownloadHandlerAudioClip.GetContent(www);}intkey=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が使いやすかった
AudioClipの読み込み方法をプログラム内で動的に設定する方法があるらしい(AudioImporter)
デフォルト楽曲をどうにか読めないか検索しているとき、譜面自動生成ライブラリを発見した。
ここを参考にすればもしかしたらデフォルト楽曲も読めるようになる?
あとがき
BeatSaberに組み込むときのサンプルコード全体はコチラ