音楽解析ライブラリEssentiaとJUCEを組み合わせてリアルタイムのメロディー推定を行う
本記事JUCE Advent Calendar 2017の12月18日向けに投稿した記事です。
タイトル通りの内容ですが、JUCEと外部ライブラリを組み合わせる場合のProjucerの設定にも触れます。
これ作りました
JUCEアドベントカレンダー用。 pic.twitter.com/iMDPcoR7sw
— 岡安啓幸 Akiyuki Okayasu (@akiyukiokayasu) 2017年12月17日
アドベントカレンダーなんでクリスマスっぽく?しました(それはどうでもいいんですが)。
何をしているかというと、オーディオの入力をリアルタイムで解析して主旋律らしいものを推定しています。
JUCEとEssentiaを使用したプログラムです(楽譜の表示はMax7にMIDIを送信させてやっています)。
Essentiaとは?
音楽解析などを行うC++ライブラリです。その中でもピッチや和音、メロディー、テンポの検出などを得意としています(主要なアルゴリズムはこちら)。
自動採譜や音源分離、楽曲レコメンド、自動作曲などについての研究はMusic Information Retrieval(MIR、音楽情報処理、音楽情報検索)と呼ばれますが、その中で頻繁に使用されるアルゴリズムはかなり揃っています。MIRについては人工知能学会のこちらによくまとまっています。
また、高速に動作するのでオフライン/リアルタイム共に使用できる点も優れています。
採用例
- KORGの「いい音」の判定ができるチューナーアプリcortosia
- openFrameworksのアドオンofxAudioAnalyzer
Bjorkが使用したことで有名になったReactableでもEssentiaが使用されているようですが、どういった用途なのかは分かりませんでした。ReactableもEssentiaもスペインのポンペウ・ファーブラ大学で始められたプロジェクトです。
なぜJUCEと組み合わせるのか
冒頭のプログラムはJUCEとEssentiaを組み合わせていると書きましたが、解析はほぼEssentiaでやっています。ではなぜJUCEを使用しているのかというと
- MIDI, OSCが簡単に扱える
- GUI作りも楽
- オーディオI/Oの設定がスマート
という点が大きいです。
MIDIやOSCを使って解析結果を他のアプリケーションとやりとりするのも簡単ですし、細かいパラメーターの調整はGUIあるのとないのとではスピードが違います。また、Essentiaのいくつかのアルゴリズムはサンプルレート44.1kHzのみのサポートになっていますが、JUCE側でサンプルレートやバッファーサイズの指定をさせることも出来ます。
オーディオのアプリケーションを作成するにあたって必須の機能はJUCEが網羅しているので、解析などの専門的な処理を外部ライブラリに任せる形をとっています。高機能のアプリケーションを高速に作成することが出来るのがこのやり方のメリットです。
Essentiaの静的ライブラリをビルドする
EssentiaをJUCEから使えるようにするために静的ライブラリのビルドをします。
EssentiaのFAQ内のBuilding lightweight Essentia with reduced dependenciesを参考にしています。依存の少ない軽量なEssentiaのビルドです。
MacOSを対象に進めますが、Essentia自体はマルチプラットフォームです(Windows, Linux, MacOS, iOS, Androidに対応)。
Essentiaの依存関係をインストール
Homebrewが使用できる前提です。
Essentiaが依存するライブラリをインストールします。
$ xcode-select --install
$ brew install pkg-config gcc readline sqlite gdbm freetype libpng
$ brew install libyaml fftw ffmpeg libsamplerate libtag
$ brew install yasm cmake wget
ソースからビルドする
Essentiaのリリースから最新(2017/12/17ではEssentia 2.1 beta3)をダウンロードし、適当なディレクトリでzipを展開し、ターミナルから以下のコマンドです。
$ packaging/build_3rdparty_static_debian.sh
$ ./waf configure --build-static --with-static-examples --mode=release --lightweight= --fft=ACCELERATE --prefix=/Users/username/EssentiaLightweightStaticLibrary
$ ./waf
$ ./waf install
エラーがなければprefixで指定したディレクトリに静的ライブラリができています。
—fftでFFTのライブラリにApple Accelerate Frameworkを指定しています。
MacOS/iOS以外の環境の場合、 --fft=fftw もしくは —fft=KISSに置き換えてください。
Kiss FFTの場合は依存関係が無い状態でstatic libraryのビルドができますが、fftwの場合はfftwに依存した状態になります。
JUCEプロジェクトからEssentiaを使用する
リポジトリ
jucerファイルと同じ階層にEssentiaLightweightStaticLibraryがありますが、それが先ほどビルドしたEssentiaの静的ライブラリです。
JUCEで外部ライブラリを使用する方法についてはフォーラムに上がっているのでそれに従います。
具体的にはProjucerでEssentiaをリンクさせ、ヘッダーのパスを指定することで使用できるようになります。
Projucerの設定
Exportersタブ > Xcode > External libraries to link にessentiaを追加します。使用するライブラリの名前を指定しています(ライブラリのパスは別に指定します)。
注意点として、MacではEssentiaの静的ライブラリ名はlibessentia.aになりますが、libと拡張子(.a)を除いた名前を追加する必要があるのでessentiaとなります。
例えば、libhoge.aの場合はhogeとなります。
また、
Exportersタブ > Xcode > Debug/Release > Header search paths に/EssentiaLightweightStaticLibrary/includeディレクトリのパスを追加します。ヘッダーファイルのあるパスです。
Exportersタブ > Xcode > Debug/Release > Extra library search paths に
/EssentiaLightweightStaticLibrary/libディレクトリのパスを通します。External libraries to linkで指定した静的ライブラリのあるパスです。
これでJUCEでEssentiaが使えるようになりました。
実際に使ってみる
ざっと説明してみますが、短いので分かる人はコードみた方が早いと思います(短いコードで済むのはJUCEとEssentiaのおかげです!)。
まず MainComponent.hでEssentiaのヘッダーをインストールします(5~7行目)。
#include <essentia/essentia.h>
#include <essentia/algorithmfactory.h>
#include <essentia/essentiamath.h>
使用するアルゴリズムによって必要なヘッダーは変わるので、他のアルゴリズムを使用したい場合はEssentiaのドキュメントやリファレンスを参照してください。
そしてEssentia用の変数を宣言します(33~38行目)。
std::vector<essentia::Real> essentiaInput;
std::vector<essentia::Real> essentiaPitch;
std::vector<essentia::Real> essentiaPitchConfidence;
std::vector<essentia::Real> essentiaFreq;
essentia::standard::Algorithm* melodyEstimate;
essentia::standard::Algorithm* pitchfilter;
上から4行目までのvectorの配列はEssentiaのアルゴリズムに渡すためのオーディオ配列や解析結果を返したりするためのものです。essentia::Realは環境によってfloatかdoubleかが変わる浮動小数点型です。
下2行は実際のEssentiaの解析をさせるためのアルゴリズムのポインタです。コンストラクタで使用するアルゴリズムを指定します。
MainComponent.cppを見てみます。
コンストラクタ内でEssentiaの初期化、アルゴリズムの指定などを行なっています(57~75行目)。
//Essentia
essentia::init();//Essentiaの初期化
essentia::standard::AlgorithmFactory& factory = essentia::standard::AlgorithmFactory::instance();
melodyEstimate = factory.create("PredominantPitchMelodia", "minFrequency", 220.0f, "maxFrequency", 7040.0f, "voicingTolerance", -0.9f);//voicingToleranceパラメータは要調整 [-1.0~1.4] default:0.2(反応のしやすさ的なパラメータ)
pitchfilter = factory.create("PitchFilter", "confidenceThreshold", 55, "minChunkSize", 35);
std::cout<<"Essentia: algorithm created"<<std::endl;
essentiaInput.reserve(lengthToEstimateMelody_sample);
essentiaInput.resize(lengthToEstimateMelody_sample, 0.0f);
essentiaPitch.reserve(200);
essentiaPitchConfidence.reserve(200);
essentiaFreq.reserve(200);
melodyEstimate->input("signal").set(essentiaInput);
melodyEstimate->output("pitch").set(essentiaPitch);
melodyEstimate->output("pitchConfidence").set(essentiaPitchConfidence);
pitchfilter->input("pitch").set(essentiaPitch);
pitchfilter->input("pitchConfidence").set(essentiaPitchConfidence);
pitchfilter->output("pitchFiltered").set(essentiaFreq);
std::cout<<"Essentia: algorithm connected"<<std::endl;
essentiaの初期化をした後に、essentia::standard::AlgorithmFactoryのインスタンスを作っています。これを使ってアルゴリズムの指定をします。
キモになるのはPredominantPitchMelodiaというアルゴリズムです。これはポリフォニックな音楽から主旋律を推定するアルゴリズムで独奏の音楽に限らず、歌と伴奏などの編成にも対応できるのが強みです。
PitchFilterはPredominantPitchMelodiaの推定結果を後処理するためのアルゴリズムです。
69~75行目で各アルゴリズムの入出力とさきほどヘッダーに書いたvectorを紐づけています。
そして、入力の直近8192サンプルを解析させているのがこのメソッドです(294~324行目)。
void MainContentComponent::estimateMelody()
{
melodyEstimate->compute();
melodyEstimate->reset();//compute()あとにreset()は必ず呼ぶこと
pitchfilter->compute();
//周波数->MIDIノート変換
auto freqToNote = [](float hz)->int{
return hz >= 20.0 ? std::nearbyint(69.0 + 12.0 * log2(hz / 440.0)): -1;//20Hz以下の時は-1を返す
};
std::vector<int> noteArray(essentiaFreq.size(), -1);
std::transform(essentiaFreq.begin(), essentiaFreq.end(), std::back_inserter(noteArray), freqToNote);
const int numConsecutive = 7;
for (int i = 0; i < noteArray.size() - numConsecutive; ++i)
{
const int target = noteArray[i];
if (target != -1 && target != lastNote)
{
bool isEnoughConsecutive = std::all_of(noteArray.begin() + i, noteArray.begin() + i + numConsecutive, [target](int x){return x == target;});
if (isEnoughConsecutive)
{
lastNote = target;
sendOSC(oscAddress_note, lastNote);
sendMIDI(lastNote);
}
}
}
}
冒頭の3行でEssentiaで解析させています。それ以降は解析結果のMIDIノートへの変換とMIDIとOSCの送信処理です。
核となるコードは以上です。
あとはGUIや解析の前処理に関係する部分になりますので興味のある方はソースを見て確認してみてください。
解析の前処理はハイパスとノイズゲートをかけています。JUCEもDSPモジュールがあるのでフィルターが簡単に使えて素晴らしいですね。