diff --git a/src/components/JamGuide.jsx b/src/components/JamGuide.jsx index f795864..085e794 100644 --- a/src/components/JamGuide.jsx +++ b/src/components/JamGuide.jsx @@ -4,6 +4,8 @@ import { buildLoopIndex, matchLoopToProgression, findLoopPosition, chordRootPC } import { NOTES, CHORD_TYPES } from '../lib/theory' import RoadmapTrack from './RoadmapTrack' import ChordDiagram from './ChordDiagram' +import MiniPiano from './MiniPiano' +import { pianoVoicingChain } from '../lib/piano' // ─── JamGuide — the Roadmap bottom dock ─────────────────────────────────────── // @@ -23,12 +25,16 @@ import ChordDiagram from './ChordDiagram' // bpm : number | null — live tempo from the onset pipeline // currentChord : string | undefined — most recent committed chord -// Display order for instrument tabs; availability is derived from the KB, not hardcoded. +// Display order for instrument tabs; availability is derived from the KB, not +// hardcoded — EXCEPT piano, which is always available: its voicings are COMPUTED +// from the progression's degrees+qualities via src/lib/piano.js (L-10/L-11), so +// no authored KB piano pack is required. const INSTRUMENTS = [ { id: 'guitar', label: 'Guitar', icon: '🎸' }, { id: 'piano', label: 'Piano', icon: '🎹' }, { id: 'bass', label: 'Bass', icon: '🎵' }, ] +const COMPUTED_INSTRUMENTS = new Set(['piano']) export default function JamGuide({ detectedProgression, keyInfo, chordHistory = [], bpm, currentChord, onFocusChord }) { const [open, setOpen] = useState(false) @@ -103,32 +109,49 @@ export default function JamGuide({ detectedProgression, keyInfo, chordHistory = return (((position - match.rotation) % n) + n) % n }, [match.matched, match.progression, match.rotation, position]) - // ── Per-station voicing shapes from the KB ────────────────────────────────── - // For the matched style + progression id, pull the recommended guitar play - // (the first play). Its `chords` array is in canonical KB order — chords[i] - // aligns 1:1 with progression.degrees[i] (the same station order RoadmapTrack - // renders). Each entry: { shape, note }. A station with no shape → graceful gap. + // ── Per-station voicings ──────────────────────────────────────────────────── + // Stations are canonical KB order — index i aligns 1:1 with + // progression.degrees[i] (the same station order RoadmapTrack renders). Each + // entry carries the station's chord identity ({rootPc, quality, label, rn} — + // the tap-to-fretboard contract) plus an instrument-specific payload: + // guitar → `shape`: from the recommended KB guitar play (the first play); + // its `chords` array is canonical order too. No shape → graceful gap. + // piano → `voicing`: COMPUTED via pianoVoicingChain over the whole loop in + // canonical order, so each station's register threads from the + // previous one (minimal movement between stations). The station's + // rootPc is attached so MiniPiano marks the root key ("R") reliably. const stationVoicings = useMemo(() => { - if (!match.matched || instrument !== 'guitar') return [] + if (!match.matched || (instrument !== 'guitar' && instrument !== 'piano')) return [] const prog = match.progression - const styleId = match.style - const plays = kb[styleId]?.instruments?.guitar?.plays?.[prog?.id] - const play = Array.isArray(plays) ? plays[0] : null - const chords = play?.chords ?? [] const degrees = prog?.degrees ?? [] const qualities = prog?.qualities ?? [] - return degrees.map((deg, i) => { + const stations = degrees.map((deg, i) => { const rootPc = (((keyRoot + deg) % 12) + 12) % 12 const noteName = NOTES[rootPc] const suffix = CHORD_TYPES[qualities[i]]?.suffix ?? '' return { - shape: chords[i]?.shape ?? null, + shape: null, + voicing: null, rootPc, quality: qualities[i] ?? 'maj', label: `${noteName}${suffix}`, rn: prog?.rn?.[i] ?? '', } }) + if (instrument === 'guitar') { + const plays = kb[match.style]?.instruments?.guitar?.plays?.[prog?.id] + const play = Array.isArray(plays) ? plays[0] : null + const chords = play?.chords ?? [] + for (let i = 0; i < stations.length; i++) { + stations[i].shape = chords[i]?.shape ?? null + } + } else { + const chain = pianoVoicingChain(stations.map(({ rootPc, quality }) => ({ rootPc, quality }))) + for (let i = 0; i < stations.length; i++) { + stations[i].voicing = chain[i] ? { ...chain[i], rootPc: stations[i].rootPc } : null + } + } + return stations }, [match.matched, match.progression, match.style, instrument, keyRoot]) // ── Tap-to-enlarge: which station's voicing is expanded (full diagram). ── @@ -184,7 +207,8 @@ export default function JamGuide({ detectedProgression, keyInfo, chordHistory = {/* Instrument tabs */}
{INSTRUMENTS.map(inst => { - const enabled = availableInstruments.has(inst.id) + // Computed instruments (piano) need no KB pack — always selectable. + const enabled = COMPUTED_INSTRUMENTS.has(inst.id) || availableInstruments.has(inst.id) const active = enabled && inst.id === instrument return (