0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-08-13

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

libKeyFinder

使ってみて精度が良さげだったのでこのライブラリを選定。
ほかにはSuperPowererdSDKを試したが、libKeyFinderのほうが良さそうだった。
ほかの有償アプリと比べて遜色のない性能を持っているらしい。

(キー推定のアルゴリズムについてはできたら今度。
ピッチ推定はこのあたりか?倍音成分の除去がメイン?)

組み込み時の注意点

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

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

組み込み手順

1. FFTWのlibファイルを作成する

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

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

なお、途中で開発者用コンソールの使用を求められるが、Windowsのフォルダ検索から辿れる。
developercommand.png

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

WindowsコンソールアプリでC#からC++の関数を呼ぶ方法を参考にして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のサンプルコードは若干間違っているので注意。正しくは下記。


// 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にする

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

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

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

サンプルコード

        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限定。(無理やり読もうとするとこんなエラーが出る)
まずは、CustomLevelで追加されている楽曲のファイルパスを取得する。

CustomLevelPathの取得はココを参考に以下のようにした。
下記実装でCustomLevelのときのみ処理が実行されるようになっている。


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を使う。

サンプルコード

        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に渡してやればおしまい。

参考情報

あとがき

BeatSaberに組み込むときのサンプルコード全体はコチラ

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?