From 672cb341a785ffca2935e778b1e4b49febd377f7 Mon Sep 17 00:00:00 2001 From: OpenClaw Date: Tue, 15 Sep 2026 22:37:29 +0000 Subject: [PATCH] Preview: pause/resume with cursor, stepping only while paused, indicator follows playback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New phase 'previewPaused': pause stops scheduled audio but keeps the preview cursor; resume continues from the cursor position. - Step buttons (and arrow keys) are active ONLY while the preview is paused; each step moves the cursor one event and plays that single note as a find-it-on-the-neck cue. - Preview indicator now follows playback in real time: a cursor interval advances previewIdx by audio time (event start ticks), so the notation caret moves with the sounding note — also while paused at the stepped position. - Main Start button shares the semantics: while previewing it shows Pause/Resume and routes to the preview controls (exercise and preview transport behave consistently). - External pause (settings dialog, tab hide) during preview now keeps the cursor (previewPaused) instead of dropping to ready. --- src/components/PhraseTrainer.tsx | 29 +++++--- src/hooks/usePhraseTrainer.ts | 114 +++++++++++++++++++++++-------- 2 files changed, 104 insertions(+), 39 deletions(-) diff --git a/src/components/PhraseTrainer.tsx b/src/components/PhraseTrainer.tsx index 5d2eee1..b3c958f 100644 --- a/src/components/PhraseTrainer.tsx +++ b/src/components/PhraseTrainer.tsx @@ -35,7 +35,7 @@ import { TUNINGS, getFretboardPositions } from '../music/Tunings'; import { INSTRUMENT_DEFINITIONS, resolveClefTranspose } from '../music/InstrumentConfigs'; const AUTO_CONTINUE_DELAY_MS = 1500; -import { Play, Pause, RotateCcw, SkipForward, SkipBack, Volume2, Square, ChevronDown, Music2, Upload, HelpCircle, Guitar } from 'lucide-react'; +import { Play, Pause, RotateCcw, SkipForward, SkipBack, Volume2, ChevronDown, Music2, Upload, HelpCircle, Guitar } from 'lucide-react'; export interface PhraseHandle { pauseToggle: () => void; @@ -341,7 +341,9 @@ export const PhraseTrainer = forwardRef( const events = scoreEvents(activeScore); // While previewing, the hint instruments follow the preview cursor so // the user can find notes on the fretboard while listening. - const displayIdx = trainer.phase === 'preview' ? trainer.previewIdx : trainer.currentIdx; + const displayIdx = trainer.phase === 'preview' || trainer.phase === 'previewPaused' + ? trainer.previewIdx + : trainer.currentIdx; const currentMidi = displayIdx >= 0 && events[displayIdx]?.pitch ? events[displayIdx]!.pitch!.midi : null; @@ -373,7 +375,8 @@ export const PhraseTrainer = forwardRef( const statusText = useMemo(() => { if (!scoreOk) return materialError ?? 'No material.'; const p = trainer.phase; - if (p === 'preview') return 'Listen …'; + if (p === 'preview') return 'Listen … (pause to step note by note)'; + if (p === 'previewPaused') return 'Paused — step with ⏮ ⏭ or ← →, resume with P'; if (p === 'countIn') return `Count-in: ${trainer.countInLeft}`; if (p === 'paused') return 'Paused'; if (p === 'done') { @@ -392,12 +395,13 @@ export const PhraseTrainer = forwardRef( const p = trainer.phase; if (p === 'playing' || p === 'countIn') return 'Pause'; if (p === 'paused') return 'Resume'; - if (p === 'preview') return 'Stop preview'; + if (p === 'preview') return 'Pause'; + if (p === 'previewPaused') return 'Resume'; return 'Start'; }, [trainer.phase]); const startAction = useMemo(() => { - if (trainer.phase === 'preview') return trainer.previewToggle; + if (trainer.phase === 'preview' || trainer.phase === 'previewPaused') return trainer.previewToggle; return trainer.pauseToggle; }, [trainer]); @@ -719,7 +723,7 @@ export const PhraseTrainer = forwardRef( {scoreOk ? ( ( {!settings.zenMode && ( <> - {trainer.phase === 'preview' && ( + {trainer.phase === 'previewPaused' && (
)}