TadaoYamaokaの開発日記

個人開発しているスマホアプリや将棋AIの開発ネタを中心に書いていきます。

ピッチ解析VSTプラグイン

リアルタイム音程解析 VSTプラグイン(vst_pitch)をVST3プラグイン化した。


VST3化

GitHub CopilotのGPT-5でサクッと変換するつもりが結構苦戦した。

いきなりすべてを変換させると何度試しても正しく動くものができなかった。

「一部は未実装でもよい」というフォールバックオプションを与えることで、音階を文字で表示するだけだが動くものができたので、それをベースに一つずつ機能を移植していくことで実装できた。

機能追加

以前のVST2のバージョンにはなかった

  • 拍の表示
  • 無音区間での停止

機能を追加した。

活用方法

ボーカルトラックとの比較

Demucs-Guiや、wavdiffで、ボーカルトラックと抽出して、スペクトログラム解析VST3プラグインで、解析した結果と比較することで、カラオケの練習などに使える。

採譜してMIDIにしておくと、MIDIノートを重ねて表示できるので、音程が合っているか可視化できる。


まとめ

以前に公開していたピッチ解析VSTプラグインをVST3プラグイン化した。

準備ができたらGumroadで配布する予定である。

追記

Payhipで公開した。

payhip.com

【公開】wavdiff — フル+インストからボーカルを抽出するコマンドラインツール

wavdiff — フル+インストからボーカルを抽出するコマンドラインツール

Windows 向けのコマンドラインツール 「wavdiff」 を GitHub で公開しました。

このツールは、フルトラック(ボーカル+インスト)とインストトラックを入力すると、その差分から ボーカルのみを抽出することができます。


機能紹介

できること

  • 16bit / 24bit PCM WAVE に対応
  • フル/インストの タイミングずれを自動補正
  • フルとインストの差分から ボーカルトラック(モノラル)を抽出

使い方

wavdiff <full.wav> <inst.wav> <out_dir>

出力ディレクトリには以下の 3 つのファイルが生成されます。

  • full.wav : フルトラック(必要に応じて無音パディング済み)
  • inst.wav : インストトラック(必要に応じて無音パディング済み)
  • vocal.wav: 抽出されたボーカル(モノラル)

技術的背景

相互相関によるタイミング補正

音楽のフルとインストは数秒程度ずれていることがあります。 wavdiff では冒頭 3 秒をスキップし、その後の 10 秒間を解析区間とし、FFT による 高速相互相関 を計算します。これにより、整数サンプル精度でラグを検出します。

FFT 相関ではテーパリング(三角窓効果)が生じるため、wavdiff では オーバーラップ長で正規化し、誤差を抑えています。また、極端に短いオーバーラップは無視することで頑健性を高めています。

無音パディング方式

ラグが検出されたら、位相を一致させるために 短い方のトラックのみに先頭無音を追加します。 たとえばインストが 200 サンプル早い場合は、インストの頭に 200 サンプル分の無音を追加し、フルに合わせます。 これにより余計な伸長はなく、両者の波形がきれいに重なります。

ボーカル抽出処理

フルとインストを完全に同期させた上でサンプル単位で減算。チャンネルが複数ある場合は平均してモノラル化し、vocal.wav として保存します。

  • 出力の vocal.wav入力と同じビット深度(16 or 24)を保持。
  • full.wavinst.wav元のフォーマットを保持しつつ無音で整列済みです。

リポジトリ

👉 GitHub: wavdiff


まとめ

wavdiff は「フルとインストを揃えて引き算するだけ」というシンプルなアプローチを採用しつつ、

  • FFT による高速相互相関
  • テーパリング補正
  • 短い方だけを無音パディングする整列

といった工夫を盛り込んだ実用的なツールです。

CQTでスペクトログラムを描く その8(MIDI入力)

自作スペクトログラム解析VST3プラグインMIDI入力機能を追加した。


MIDI入力

DAWMIDIトラック出力をこのプラグインに入力すると、MIDIノートをスペクトラムに重ねて表示できる。

採譜した楽譜が合っているかやピッチとのずれを視覚的に確認できる。

実装

拍の表示で追加したaddEventInputをそのまま使用して、Vst::Event::kNoteOnEventとVst::Event::kNoteOffEventで、MIDIノートのオン/オフを受信する。

受信したMIDIノートを、スペクトラムの音階に、半透明の青色で重ねて表示する。

まとめ

スペクトログラム解析VST3プラグインMIDI入力を実装した。

次は、解析範囲をパラメータで設定できるようにしたい。

CQTでスペクトログラムを描く その7(マウスカーソル位置ハイライト)

自作スペクトログラム解析VST3プラグインにマウスカーソル位置にハイライトを表示する機能を追加した。


ハイライト表示

マウスカーソル位置の音階を判定して、その音階のラインに透明な白でハイライトを表示する。
また、ピアノロールの鍵盤を赤で表示する。

実装方法

VST3のコントローラクラスで、マウスイベント (`onMouseMoved`, `onMouseEntered`, `onMouseExited`) をオーバーライドして実装すればよい。

描画負荷

マウス移動のたびに描画要求を行うと、描画処理が終わらないうちに次の描画要求が発生して、描画キューがあふれて異常終了してしまう。

setDirty()は、controllerからは直接呼び出さず、UIスレッドでタイマーで呼び出すように対処した。

class MyEditor : public VSTGUI::VST3Editor {
public:
    using VST3Editor::VST3Editor;
    MyEditor(Steinberg::Vst::EditController* c, VSTGUI::UTF8StringPtr t, VSTGUI::UTF8StringPtr xml)
        : VST3Editor(c, t, xml)
    {
        setIdleRate(16); // ~60Hz
    }

    VSTGUI::CMessageResult notify(VSTGUI::CBaseObject* sender, const char* message) override
    {
        if (message == VSTGUI::CVSTGUITimer::kMsgTimer)
        {
            if (auto* f = getFrame())
            {
                if (auto* ctrl = dynamic_cast<VstPitchController*>(getController()))
                {
                    if (ctrl->consumeUIRedrawRequest())
                    {
                        if (!f->isDirty())
                            f->setDirty(true);
                    }
                }
            }
        }
        return VSTGUI::VST3Editor::notify(sender, message);
    }
};

まとめ

スペクトログラム解析VST3プラグインにハイライトを表示を実装した。

だいぶ実用的なツールになってきた。
完成したら、Gumroadで配布することを考えている。

次は、MIDI入力を実装したい。

CQTでスペクトログラムを描く その6(基本周波数解析)

スペクトログラム解析VST3プラグインに基本周波数解析を実装した。

基本周波数解析

ボーカル音程モニターと同じアルゴリズムで基本周波数解析を行う。

時間分解能のずれ

スペクトログラムはF#1まで4分音の周波数分解能に対応しているため、サンプリング周波数44.1kHzで、FFTSIZEが32768となり時間分解能が低い。

ピッチ解析は、FFTSIZEが4096でも十分に解析できるため、時間分解能がずれることで、表示がずれることになる。

FFTSIZEをスペクトログラムに合わせることもできるが、時間分解能が低すぎるので、スペクトログラムの時間分解能を上げることを検討する。

バンク分割

低域の周波数分解能確保するために時間分解能が犠牲になっているので、低域は周波数分解能を落として時間分解能を上げることにする。
高域は、窓長が短いため、時間分解能を上げても周波数分解能はそのままにできる。

低域と高域でバンクを分けて処理を行い、結果を対数周波数軸で結合する。

時間分解能は同じにできるので、CQTの高速化アルゴリズムで、FFTの計算は共通化でき、スペクトルカーネルの疎行列のみ別々に作成する。

また、低域は、周波数分解能を半分にするため、ビンの間隔が2倍になる。
後続処理で透過的に扱えるようにするため、ビンの間隔は同じにしたい。
そのため、低域は、本来のCQTの半分の間隔でスペクトルカーネルを作成する。
理論上は正しくないが、表示上の補間の意味合いである。

解析音高範囲

低域をバンク分割したことで、最低音を下げることができるため、最低音をC1にした(以前はF#1)。

ピッチ解析のFFTSIZE

ピッチ解析のFFTSIZEもCQTに合わせて、FFT計算を共通化した。

これにより、FFTの計算を一部省略できて、時間のずれをなくすことができる。

ただし、修正後のスペクトログラムのFFTSIZEは16384で、時間分解能は371 msである。
体感上、若干遅く感じる。

ボーカル単音を解析した結果


楽曲からボーカル抽出したトラックを解析した結果

Demucs-Guiで、ミックスされた楽曲からボーカルトラックを抽出して解析した。


ピッチ解析のオン/オフ

クイックコントロールからピッチ解析のオン/オフを設定できるようにした。


まとめ

スペクトログラム解析VST3プラグインに基本周波数解析を実装した。
また、スペクトログラムの時間分解能を上げるため低域と高域でバンクを分けてCQTを計算するようにした。

次は、マウスカーソル位置のハイライトを実装したい。

CQTでスペクトログラムを描く その5(拍の表示)

スペクトログラム解析VST3プラグインに拍の表示を追加した。

拍の表示

VSTのホスト側から拍の情報を受け取り、拍の位置に縦の線を描くようにした。
小節の開始は、色を明るくして目立つようにした。


処理方法

Processorで、IProcessContextRequirementsをインプリメントすることで、拍の情報を受け取ることができる。

ホストから受け取るtempo、timeSigDenominator、timeSigNumerator、projectTimeMusic、barPositionMusicと、サンプリング周波数から現在のブロックが拍の位置か、小節内の何拍目かを計算する必要がある。
ややこしいので、GPT-5で生成したコードをそのまま使った。

uint8_t VstCqtProcessor::computeBeatClass (const Vst::ProcessData& data, int sampleIndexInBlock) const
{
	using namespace Steinberg::Vst;
	const ProcessContext* ctx = data.processContext;
	if (!ctx)
		return 0;
	const double tempo = ctx->tempo; // assume host provides valid fields when non-null
	if (!(tempo > 0.0))
		return 0;
	const int den = (std::max)(1, static_cast<int> (ctx->timeSigDenominator));
	const int num = (std::max)(1, static_cast<int> (ctx->timeSigNumerator));
	const double beatLenQN = 4.0 / static_cast<double> (den); // length of one beat in quarter notes
	const double barLenQN = beatLenQN * static_cast<double> (num);
	const double qnPerSample = tempo / (60.0 * (std::max)(1.0, sampleRate_));
	const double dq = static_cast<double> (sampleIndexInBlock) * qnPerSample;
	const double qNotesIntoBar = (ctx->projectTimeMusic + dq) - ctx->barPositionMusic;
	// wrap to [0, period)
	auto wrapMod = [] (double x, double p) -> double {
		double k = std::floor (x / p);
		x -= k * p;
		if (x < 0.0) x += p;
		return x;
	};
	const double qnBeatMod = wrapMod (qNotesIntoBar, beatLenQN);
	const double qnBarMod  = wrapMod (qNotesIntoBar,  barLenQN);
	// tolerance: half of the quarter-notes advanced between analysis columns
	const double qnPerColumn = tempo / (60.0 * ANALYZE_FPS);
	const double eps = (std::max)(1e-4, qnPerColumn * 0.5);
	bool atBeat = (qnBeatMod <= eps) || ((beatLenQN - qnBeatMod) <= eps);
	bool atBar  = (qnBarMod  <= eps) || ((barLenQN  - qnBarMod)  <= eps);
	if (atBar)
		return 2; // thick line
	if (atBeat)
		return 1; // thin line
	return 0;
}

まとめ

スペクトログラムに拍の位置を表示するようにした。

次は、基本周波数解析を実装したい。

CQTでスペクトログラムを描く その4(ピアノロール)

前回の続き。

リアルタイムにスペクトログラムを表示するVST3プラグインに、音高の軸をC4などのノート名の代わりに、ピアノロールを表示するようにした。


ピアノロール表示処理

白鍵は、オクターブを7分割して表示する。
黒鍵は、半音と同じ位置に白鍵の半分の幅で表示する。

この場合、黒鍵の中心は、白鍵の境界の位置にならない。
実際のピアノでも白鍵に対する黒鍵の位置はノートによって異なっている。

GPT-5でコード生成できなかった件

GPT-5でピアノロールを描く処理のコード生成を試みたが、何度やってもうまくいかなかった。

失敗例:

  • 白鍵を12分割する
  • オクターブが画面に収まる範囲しか描画しない
  • 補助線位置とずれる

画面の一番上のノートが必ずしもオクターブの境界ではない条件が難しいらしい。
for (int i = 0; i < 7; ++i)のようなループを記述しがちで、オクターブが収まる範囲しか描画できなくなる。

結局、処理方法を具体的に指示して、生成されたコードを手直しして取り入れた。

まとめ

スペクトログラム解析VST3プラグインピノロールを表示するようにした。

次は、拍の表示、マウスカーソル位置のハイライトを実装したい。