/** * The original single-note experience (Sight Reading / Ear Training), * extracted from App so its game-logic effects unmount entirely while * Phrase Mode is active (contract: "no background scoring after leaving * the mode" — hiding the UI alone would keep useGameLogic running). */ import { forwardRef, useEffect, useImperativeHandle, useMemo, useState } from 'react'; import { SheetMusic } from './SheetMusic'; import { useSettings } from '../context/useSettings'; import { useAudioPlayer } from '../hooks/useAudioPlayer'; import { useGameLogic } from '../hooks/useGameLogic'; import { getNoteDetails } from '../music/NoteUtils'; import { TUNINGS, getFretboardPositions } from '../music/Tunings'; import { INSTRUMENT_DEFINITIONS, resolveClefTranspose } from '../music/InstrumentConfigs'; import { Fretboard } from './Fretboard'; import { PianoKeys } from './PianoKeys'; import { audioEngine } from '../audio/AudioEngine'; import { Guitar, HelpCircle, Volume2, SkipForward } from 'lucide-react'; export interface SingleNoteHandle { skipNote: () => void; playCurrent: () => void; } interface SingleNoteTrainerProps { /** Raw detection (gating for display happens inside). */ pitchData: { midi: number; note: string; cents: number; frequency: number } | null; windowWidth: number; windowHeight: number; onSetHoveredMidi?: (midi: number | null) => void; } export const SingleNoteTrainer = forwardRef( function SingleNoteTrainer({ pitchData, windowWidth, windowHeight, onSetHoveredMidi }, ref) { const { settings, updateSettings } = useSettings(); const setSettings = updateSettings; const { playNote } = useAudioPlayer(); const currentTuning = TUNINGS[settings.tuningId]; const currentInstrumentDef = INSTRUMENT_DEFINITIONS[settings.instrument]; const { targetMidi, feedbackMessage, revealed, virtualNote, generateNewNote, handleVirtualInstrumentPlay } = useGameLogic(pitchData); useImperativeHandle(ref, () => ({ skipNote: () => generateNewNote(), playCurrent: () => playNote(targetMidi, settings.referenceNoteDuration ?? 1.5, settings.autoPlayVolume ?? 0.5), }), [generateNewNote, playNote, targetMidi, settings.referenceNoteDuration, settings.autoPlayVolume]); const [hoveredMidi, setHoveredMidi] = useState(null); useEffect(() => { onSetHoveredMidi?.(hoveredMidi); }, [hoveredMidi, onSetHoveredMidi]); // Mic feedback is gated while the app's speaker output is audible. const displayedPitch = audioEngine.isAudible() ? null : pitchData; const { clef: activeClef, transpose: activeTranspose } = useMemo(() => resolveClefTranspose(currentInstrumentDef, settings.difficulty), [currentInstrumentDef, settings.difficulty]); const hintPositions = useMemo(() => { if (!settings.showHint || !currentInstrumentDef.showTuning || !currentTuning) return []; return getFretboardPositions(targetMidi, currentTuning); }, [settings.showHint, targetMidi, currentInstrumentDef, currentTuning]); return (
{!settings.zenMode && (
{feedbackMessage ? (
{feedbackMessage.startsWith("Good! ") ? ( <>
Good!
{feedbackMessage.replace("Good! ", "")}
) : ( feedbackMessage )}
) : (
{settings.gameMode === 'ear_training' ? "Listen and play the note" : "Play the note above"}
)}
)} {(settings.showHint || settings.showFretboard) && (
{settings.showHint && (
{getNoteDetails(targetMidi + activeTranspose).scientific}
)} {currentInstrumentDef.showTuning && ( currentInstrumentDef.id === 'piano' ? ( ) : ( currentTuning && ( ) ) )}
)} {!settings.zenMode && (
{currentInstrumentDef.showTuning && ( )}
)}
); } ); export default SingleNoteTrainer;