Recorder Visualizer の主要コンポーネントとユーティリティのAPIドキュメント
オーディオ再生とシーケンサー制御を担当するクラス
import { audioEngine } from './utils/AudioEngine';AudioContext を初期化します。ユーザーインタラクション後に呼び出す必要があります。
audioEngine.init();メロディーシーケンスの再生を開始します。
パラメータ:
steps: (string | null)[]- 各ステップの音符キー(null は休符)bpm: number- テンポ(60-180)onStepCallback: (stepIndex: number, noteKey: string | null) => void- 各ステップ実行時のコールバックonEndCallback: () => void- シーケンス終了時のコールバックloop: boolean- ループ再生フラグmetronome: boolean- メトロノーム有効フラグmuted: boolean- メロディーミュートフラグ
例:
audioEngine.startSequence(
['C5', 'D5', 'E5', null],
120,
(index) => console.log(`Step ${index}`),
() => console.log('End'),
false,
true,
false
);再生中のシーケンスを停止します。
audioEngine.stopSequence();単一の音符をプレビュー再生します。
パラメータ:
noteKey: string- 音符キー(例: 'C5', 'D5')
audioEngine.playNote('C5');マスターボリュームを設定します。
パラメータ:
db: number- デシベル値(-30 ~ 0)
audioEngine.setVolume(-10);メトロノームのボリュームを設定します。
パラメータ:
db: number- デシベル値(-30 ~ 0)
audioEngine.setMetronomeVolume(-15);マイク入力からピッチを検出するクラス
import { pitchDetector } from './utils/PitchDetector';マイク入力を開始し、ピッチ検出を実行します。
パラメータ:
callback: (result: PitchResult | null) => void- 検出結果のコールバック
PitchResult 型:
interface PitchResult {
note: string; // 音符名(例: 'C5')
diff: number; // 正解音との差(セント)
frequency: number; // 周波数(Hz)
}例:
await pitchDetector.start((result) => {
if (result) {
console.log(`Note: ${result.note}, Diff: ${result.diff} cents`);
}
});マイク入力を停止します。
pitchDetector.stop();現在のオーディオバッファからピッチを検出します(手動呼び出し用)。
const pitch = pitchDetector.detect();運指データベース
import { FINGERINGS, SCALE_ORDER } from './data/fingerings';
import type { Fingering } from './data/fingerings';interface Fingering {
note: string; // 表示名(例: 'ド', 'レ')
pitch: string; // Tone.js用ピッチ(例: 'C5', 'D5')
holes: boolean[]; // 穴の開閉状態 [裏穴, 左1, 左2, 左3, 右1, 右2, 右3, 右4]
isHigh?: boolean; // 高音域フラグ(サミング用)
}全ての運指データを格納するオブジェクト。
例:
const cFingering = FINGERINGS['C5'];
console.log(cFingering.note); // 'ド'
console.log(cFingering.holes); // [true, true, true, true, true, true, true, true]音階の順序配列。
// ['C5', 'D5', 'E5', 'F5', 'G5', 'A5', 'B5', 'C6', 'D6']| キー | 音名 | 運指パターン |
|---|---|---|
| C5 | ド | ●●●●●●●● |
| D5 | レ | ●●●●●●●○ |
| E5 | ミ | ●●●●●●○○ |
| F5 | ファ | ●●●●●○●● |
| G5 | ソ | ●●●●○○○○ |
| A5 | ラ | ●●●○○○○○ |
| B5 | シ | ●●○○○○○○ |
| C6 | 高いド | ●○●○○○○○ |
| D6 | 高いレ | ○○●○○○○○ |
凡例: ● = 閉じる、○ = 開ける
プリセット楽曲データ
import { PRESETS } from './data/presets';
import type { PresetSong } from './data/presets';interface PresetSong {
name: string; // 楽曲名
bpm: number; // 推奨テンポ
steps: (string | null)[]; // シーケンスデータ(null は休符)
}プリセット楽曲データ。
収録曲:
- カエルの歌
- きらきら星
- メリーさんの羊
- チューリップ
例:
const song = PRESETS['カエルの歌'];
console.log(song.name); // 'カエルの歌'
console.log(song.bpm); // 100
console.log(song.steps); // ['C5', null, 'D5', null, ...]3Dリコーダーモデルコンポーネント
import { RecorderScene } from './components/Recorder3D';interface RecorderSceneProps {
currentFingering: Fingering | null; // 現在表示する運指
}import { Canvas } from '@react-three/fiber';
import { RecorderScene } from './components/Recorder3D';
import { FINGERINGS } from './data/fingerings';
function App() {
return (
<Canvas>
<RecorderScene currentFingering={FINGERINGS['C5']} />
</Canvas>
);
}リコーダー本体を描画するコンポーネント。
構成:
- 頭部管(吹き口、ラビューム)
- 中部管(表穴1-4)
- 下部管(表穴5-7)
- 裏穴
各穴を描画するコンポーネント。
Props:
interface RecorderHoleProps {
position: [number, number, number]; // 穴の位置
isOpen: boolean; // 開閉状態
label?: string; // ラベルテキスト
isThumb?: boolean; // 裏穴フラグ
handLabel?: string; // 手のラベル
}const COLORS = {
BODY: "#3e2723", // リコーダー本体(ダークブラウン)
JOINT: "#f5f5dc", // ジョイント部(ベージュ)
HOLE_OPEN: "#1a0500", // 開いている穴
FINGER_CLOSE: "#60a5fa", // 押さえている穴(青)
FINGER_GHOST: "#93c5fd", // ガイド表示(薄青)
};音符キーの型エイリアス。
type NoteKey = 'C5' | 'D5' | 'E5' | 'F5' | 'G5' | 'A5' | 'B5' | 'C6' | 'D6';メロディーステップの型エイリアス。
type MelodyStep = string | null; // string = 音符キー、null = 休符import { useState } from 'react';
import { Canvas } from '@react-three/fiber';
import { RecorderScene } from './components/Recorder3D';
import { FINGERINGS } from './data/fingerings';
import { audioEngine } from './utils/AudioEngine';
function RecorderApp() {
const [currentNote, setCurrentNote] = useState('C5');
const playNote = (noteKey: string) => {
audioEngine.init();
audioEngine.playNote(noteKey);
setCurrentNote(noteKey);
};
return (
<div>
<Canvas style={{ height: '400px' }}>
<RecorderScene currentFingering={FINGERINGS[currentNote]} />
</Canvas>
<button onClick={() => playNote('C5')}>ド</button>
<button onClick={() => playNote('D5')}>レ</button>
<button onClick={() => playNote('E5')}>ミ</button>
</div>
);
}import { useState } from 'react';
import { audioEngine } from './utils/AudioEngine';
function Sequencer() {
const [steps, setSteps] = useState<(string | null)[]>(Array(16).fill(null));
const [isPlaying, setIsPlaying] = useState(false);
const [currentStep, setCurrentStep] = useState<number | null>(null);
const startPlayback = () => {
audioEngine.init();
setIsPlaying(true);
audioEngine.startSequence(
steps,
120,
(stepIndex) => setCurrentStep(stepIndex),
() => {
setIsPlaying(false);
setCurrentStep(null);
},
false,
true,
false
);
};
const stopPlayback = () => {
audioEngine.stopSequence();
setIsPlaying(false);
setCurrentStep(null);
};
return (
<div>
<div>
{steps.map((step, i) => (
<div
key={i}
style={{
background: currentStep === i ? 'yellow' : 'white',
padding: '10px',
}}
>
{step || '休符'}
</div>
))}
</div>
<button onClick={isPlaying ? stopPlayback : startPlayback}>
{isPlaying ? '停止' : '再生'}
</button>
</div>
);
}import { useState } from 'react';
import { pitchDetector } from './utils/PitchDetector';
function Tuner() {
const [isActive, setIsActive] = useState(false);
const [pitch, setPitch] = useState<{ note: string; diff: number } | null>(null);
const toggleTuner = async () => {
if (isActive) {
pitchDetector.stop();
setIsActive(false);
setPitch(null);
} else {
try {
await pitchDetector.start((result) => {
setPitch(result);
});
setIsActive(true);
} catch (error) {
console.error('Microphone access denied:', error);
}
}
};
return (
<div>
<button onClick={toggleTuner}>
{isActive ? 'チューナー停止' : 'チューナー起動'}
</button>
{pitch && (
<div>
<p>検出音: {pitch.note}</p>
<p>差: {pitch.diff > 0 ? '+' : ''}{pitch.diff} セント</p>
</div>
)}
</div>
);
}try {
audioEngine.init();
} catch (error) {
console.error('AudioContext initialization failed:', error);
// ユーザーにインタラクションを促す
}try {
await pitchDetector.start(callback);
} catch (error) {
if (error.name === 'NotAllowedError') {
console.error('Microphone permission denied');
} else if (error.name === 'NotFoundError') {
console.error('No microphone found');
}
}init()はユーザーインタラクション後に一度だけ呼び出す- シーケンス再生中は不要な状態更新を避ける
- ボリューム変更は頻繁に行わない(デバウンス推奨)
start()は HTTPS 環境でのみ動作- 検出コールバックは高頻度で呼ばれるため、重い処理は避ける
- 使用後は必ず
stop()を呼び出してリソースを解放
- Three.js レンダリングは GPU を使用
- 複数のモデルを同時に表示する場合はパフォーマンスに注意
- ジオメトリとマテリアルは可能な限り再利用
| API | バージョン | 変更内容 |
|---|---|---|
| AudioEngine | 0.0.0 | 初回リリース |
| PitchDetector | 0.0.0 | 初回リリース |
| Fingerings | 0.0.0 | C5-D6 の9音対応 |
| Presets | 0.0.0 | 4曲収録 |
更新履歴: 2026-01-14