コダーイメソッドに基づく、移動ド階名唱のトレーニングアプリ。
楽譜を見た瞬間に固定ドの音名で読んでしまい、調の中での機能(do からの距離)として音を捉えられない——という問題を、音高情報を意図的に隠したリズム譜+階名から始めて段階的に音・伴奏・実音譜を足していくことで解く。「階名 → 音」の回路を先に作るのが狙い。
サーバを持たない静的サイト。GitHub Pages で公開する。
| Step | 見るもの | 聞こえるもの | ねらい |
|---|---|---|---|
| 0 | — | 主和音をアルペジオ(do-mi-so)→ 1拍空け → 三音同時 | 調のセンターを耳に置く |
| 1 | リズム譜+階名 | メトロノームのみ | 無伴奏で自力で歌う(訓練の本体) |
| 2 | リズム譜+階名 | 旋律 + カーソル | 答え合わせ |
| 3 | リズム譜+階名 | 伴奏のみ + カーソル | 和声の支えだけで歌う |
| 4 | 実音の五線譜+階名 | 旋律 + 伴奏 + カーソル | 五線と階名を結びつける |
ステップ順は強制せず、自由に行き来できる。
リズム譜は符頭をすべて五線第2間(ト音記号の A)に固定し、音高の手がかりを消したもの。
キーは曲を開くたびにランダムに変わる(歌唱音域に収まるものから、直前と同じキーを除いて選ぶ)。絶対音高の記憶が固定されるのを防ぐため、キー名は Step 4 の調号で初めて分かる。
- 長調は do 基準、短調は la 基準(
la-ti-do-re-mi-fa-so) - 派生音: Do#→Di / Re#→Ri / Re♭→Ra / Mi♭→Ma / Fa#→Fi / So#→Si / So♭→Sa / La#→Li / La♭→Lo / Ti♭→Ta
- 表に無い変化(Do♭, Mi#, Fa♭, Ti#)はデータ検証で例外にする。該当する曲を足す段階で表を広げる
階名は音高クラスではなく音名の文字+変化記号から導く。そうしないと Do#(Di) と Re♭(Ra) を区別できない。
npm install
npm run dev # http://localhost:5173/SolfaGym/
npm run test # ロジック層のユニットテスト
npm run build # 曲インデックス生成 → 型チェック → 本番ビルド
npm run lintNode は 22.22.0 以上を推奨(react-router@8 と jsdom@30 がそれを要求する。22.15 でも動くが npm が EBADENGINE 警告を出す)。
Vite 8(Rolldown 既定)/ React 19 / TypeScript 6 / Tailwind v4 / Vitest 4 / oxlint / abcjs / Tone.js
ネット記事と食い違う点が4つあるので注意:
- Vite は 8 系。Rolldown が既定バンドラ。移行用の
rolldown-viteは入れない - Tailwind v4 は
tailwind.config.jsを作らない。@tailwindcss/viteプラグイン + CSS に@import "tailwindcss";だけ。postcss.config.js/autoprefixerは不要 - Linter は oxlint(
create-viteの React テンプレートが 2026-06 に ESLint から切り替えた) toneは15.5.36に厳密ピン。npm のlatestタグは 15.1.22(2025-04)で止まっており、^15.1.22と書くと1年以上前のビルドが入る
public/songs/
index.json # 一覧用メタ。scripts/build-songs.ts が自動生成する
{id}.json # 曲本体(ABC・原調・コード進行・出典)
src/core/ # 純粋関数層。Vitest の対象
pitch.ts # 音高の表現(音名+変化記号+オクターブ)
solfa.ts # 音高 + 調 → 階名
transpose.ts # 綴りを保つ移調、音域からのキー選択
level.ts # 階名の集合 → レベル自動算出
abc.ts # ABC の解析と、リズム譜/実音譜の生成
chords.ts # 度数表記 → MIDI
playback.ts # 再生の「いつ・どの音を」を拍で決める
song.ts / schema.ts # 曲データの導出と Zod スキーマ
src/audio/engine.ts # Tone.js を包む薄いファサード
src/score/AbcScore.tsx# abcjs 描画 + カーソル
- 時計は AudioContext ただ1つ。 カーソルは Tone.js の
getDraw()が駆動する。abcjs のTimingCallbacksは使わない——あれは繰り返しを時間方向に展開するので、譜面上の要素と1対1にならない(7要素の楽譜に対して 11 イベントが出る)。代わりに描画後の SVG を DOM 順に引く - レベルも階名も音域も、曲データから計算する。 手でタグ付けしない
- 和音は度数表記(ローマ数字)で持つ。 音名で持つと移調が効かない
- Tone.js はモジュールスコープの単一実体。 React の
useEffectの cleanup でdispose()しない(StrictMode の二重マウントで無音になる) Tone.jsの取得は練習画面のマウント時に先行して行う。 クリックしてから取得を始めると、遅い回線で取得中にユーザー操作のアクティベーションが切れて無音になる
public/songs/<id>.json を足して npm run build:songs。レベルと階名は自動で計算され、index が更新される。
ABC は意図的に狭い範囲だけを扱う——単旋律のみ、和音なし、連符なし、繰り返し記号なし。範囲外の記法は例外になり、npm run test の全曲検証が捕まえる。
収録するのはパブリックドメイン確実な曲のみ。ABC は既存の符号化データを流用せず新規に書き起こす(Essen Folksong の **kern 版は「教材への埋め込み禁止」、The Session は「LLM による処理の禁止」がライセンスに明記されている)。
GitHub Pages。main への push で GitHub Actions がビルドして公開する。
初回だけリポジトリの Settings → Pages → Source を「GitHub Actions」に設定すること(Vite はビルドが必要なため)。
公開 URL がサブパス(https://<user>.github.io/SolfaGym/)になるので、vite.config.ts の base: '/SolfaGym/' は必須。外すとアセットが 404 になる。
ルーティングは HashRouter。GitHub Pages はサーバ側リライトができず、SPA リライトのネイティブサポートも無い。404.html トリックは初回レスポンスが HTTP 404 のまま返り一部のブラウザが警告を出すので採らない。ハッシュはサーバに送られないので 404 問題が原理的に起きない。
音とカーソルは自動テストで押さえきれないので、変更したら以下を耳と目で確認する。
- 曲一覧がレベル順に並び、日本の音階が別セクションになっている
- Step 0 で do-mi-so が1音ずつ鳴り、1拍おいて三音同時に響く(短調は la-do-mi)
- Step 1 でリズム譜の符頭がすべて同じ高さに並び、下に階名が付く。メトロノームの ON/OFF が効く
- Step 2 で旋律が鳴り、カーソルが音符を正確に追う
- 別タブに切り替えて戻してもカーソルが飛ばない(Tone.js
Drawのexpirationの確認) - テンポを 50% / 120% に変えて、再生中でも音とカーソルがずれない
- Step 3 で旋律が消え、伴奏だけになる
- Step 4 で調号付きの実音五線譜になり、鳴っている音と譜面が一致する
- 同じ曲を閉じて開き直すとキーが変わる(かつ音域から外れない)
- 「できた」を押す → 一覧にチェック → リロードしても残っている
- スマホ幅で楽譜が折り返し、横スクロールが発生しない
注意: ブラウザ自動操作(ヘッドレス/バックグラウンドのタブ)では 4〜8 を確認できない。隠れたタブでは
requestAnimationFrameが発火せず、Drawに依存するカーソルが動かないため。必ず前面のタブで確認すること。