メロメモ — 鼻歌を音階に変換するピッチ検出エンジンを 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 + ピーク検出で十分実用に耐える
  • 権限拒否時のリカバリー導線は必ず用意する

「鼻歌で作曲」というニッチですが、テスト駆動で着実に進められた手応えのある案件でした。