メロメモ — 鼻歌を音階に変換するピッチ検出エンジンを TDD で書いた
| 開発記録 | メロメモ
タグ: #Flutter #音声処理 #TDD
「鼻歌で録音した音声を、楽譜風の音階データに変換する」というアプリを作りました。ピッチ検出エンジンを TDD で組んだ過程を Phase ごとにまとめます。
アプリの狙い
メロメモは「思いついたメロディを口ずさむと、それを音階データに変換してくれる」アプリです。鍵盤が弾けなくても、楽器がなくても、頭の中のメロディを残せるようにする、というのがコンセプトです。
Phase 構成での開発
開発は明確な Phase に分けて進めました。
- Phase 2: コアモデル(音階、音価、メロディ)
- Phase 3: ピッチ検出エンジン(17テスト GREEN)
- Phase 4: ピアノアプリ互換エクスポート(5テスト GREEN)
- Phase 5-6: AudioService + MelodyPlayer + UI(72テスト GREEN)
- Phase 7: マイク録音→音階変換パイプライン(81テスト GREEN)
- Phase 8: 音声ファイルインポート(89テスト GREEN)
- Phase 9: エクスポート・共有機能(89テスト GREEN)
各 Phase ごとにテストを書き、それをパスするように実装する典型的な TDD でした。最終的に約 90 のテストが GREEN を維持する状態でリリースしています。
ピッチ検出エンジンの方針
ピッチ検出には FFT ベースのアルゴリズムを採用しました。マイクから取得した PCM データを一定窓で切り出し、FFT で周波数領域に変換、ピーク検出して基本周波数(F0)を抽出します。
音楽でいう「ラ(A4)= 440Hz」という基準を使い、検出周波数を半音単位の音階にマッピングします。
擬似コードレベルだと、PCM フレーム → FFT → ピーク → MIDI ノート番号、の素直な流れになります。
int? detectMidiNote(Float32List pcm, int sampleRate) {
final spectrum = fft.realTransform(pcm); // 振幅スペクトル
final peakBin = _argMax(spectrum, from: 4); // DC 付近は除外
if (spectrum[peakBin] < _noiseFloor) return null;
final freq = peakBin * sampleRate / pcm.length;
return _freqToMidi(freq);
}
int _freqToMidi(double freq) {
// 69 = A4 = 440Hz
return (69 + 12 * (log(freq / 440.0) / log(2))).round();
}
テストは「A4 のサイン波を入れたら 69 が返る」という最小ケースから書き始めました。境界(B4=71, C5=72)のテストを足していくと、安心して FFT のチューニングに踏み込めます。
test('A4 sine wave returns MIDI note 69', () {
final pcm = synthesizeSine(440.0, sampleRate: 44100, samples: 2048);
expect(detectMidiNote(pcm, 44100), equals(69));
});
test('C5 sine wave returns MIDI note 72', () {
final pcm = synthesizeSine(523.25, sampleRate: 44100, samples: 2048);
expect(detectMidiNote(pcm, 44100), equals(72));
});
ピッチ表示カードのレイアウトのガタつき
UI 側で苦戦したのは、検出中の音階表示でした。ピッチが変動するたびに表示カードのサイズが変わってしまい、画面がガタガタ揺れていたのです。
カードを固定サイズにする変更を入れただけで、視覚的なノイズが大幅に減りました。テキストは「中央寄せ」で固定サイズの中に収めることで、見た目の安定感が大きく変わります。
マイク権限のリカバリーフロー
マイク権限を拒否されたあとに「やっぱり許可したい」となった場合の導線が初期版にはなく、設定アプリへ手動で飛ばす必要がありました。
権限拒否時に「設定アプリで権限を許可してください」というガイドを表示し、設定アプリへのリンクを置くフローを追加しました。
ピアノアプリ互換エクスポート
出力した音階データは、同じ作者の「ピアノアプリ」で再生できる形式でエクスポートできるようにしました。アプリ間でデータ連携できる仕掛けです。
Musical Red のデザインシステム
UI/UX デザインも全面刷新し、Musical Red(音楽的な赤)を基調にしたカラーパレットと Poppins フォントの組み合わせにしました。アニメーションも控えめに足して、「音楽アプリらしさ」を出しました。
まとめ
- TDD は新規アプリで特に効く、テストが Phase の完了基準になる
- ピッチ検出は FFT + ピーク検出で十分実用に耐える
- 権限拒否時のリカバリー導線は必ず用意する
「鼻歌で作曲」というニッチですが、テスト駆動で着実に進められた手応えのある案件でした。