add piano view with instrument toggle
This commit is contained in:
+28
-5
@@ -7,6 +7,7 @@ import SafeNotes from './components/SafeNotes'
|
|||||||
import Fretboard from './components/Fretboard'
|
import Fretboard from './components/Fretboard'
|
||||||
import ProgressionSuggestions from './components/ProgressionSuggestions'
|
import ProgressionSuggestions from './components/ProgressionSuggestions'
|
||||||
import Tuner from './components/Tuner'
|
import Tuner from './components/Tuner'
|
||||||
|
import Piano from './components/Piano'
|
||||||
import { NOTES, detectKey, detectTopKeys, matchChordFromChroma, detectRepeatingProgression } from './lib/theory'
|
import { NOTES, detectKey, detectTopKeys, matchChordFromChroma, detectRepeatingProgression } from './lib/theory'
|
||||||
|
|
||||||
// Key detection tuning
|
// Key detection tuning
|
||||||
@@ -22,6 +23,9 @@ export default function App() {
|
|||||||
// ── Listening state ──────────────────────────────────────────────────────
|
// ── Listening state ──────────────────────────────────────────────────────
|
||||||
const [isListening, setIsListening] = useState(false)
|
const [isListening, setIsListening] = useState(false)
|
||||||
|
|
||||||
|
// ── Instrument view ───────────────────────────────────────────────────────
|
||||||
|
const [instrument, setInstrument] = useState('guitar') // 'guitar' | 'piano'
|
||||||
|
|
||||||
// ── Key: auto-detected + optional lock ───────────────────────────────────
|
// ── Key: auto-detected + optional lock ───────────────────────────────────
|
||||||
const [keyInfo, setKeyInfo] = useState(null) // auto-detected
|
const [keyInfo, setKeyInfo] = useState(null) // auto-detected
|
||||||
const [lockedKey, setLockedKey] = useState(null) // { root, mode } or null
|
const [lockedKey, setLockedKey] = useState(null) // { root, mode } or null
|
||||||
@@ -182,6 +186,21 @@ export default function App() {
|
|||||||
|
|
||||||
{/* ── Controls bar ── */}
|
{/* ── Controls bar ── */}
|
||||||
<div className="mb-4 flex flex-wrap gap-3 items-center p-3 bg-panel border border-border rounded-xl">
|
<div className="mb-4 flex flex-wrap gap-3 items-center p-3 bg-panel border border-border rounded-xl">
|
||||||
|
{/* Instrument toggle */}
|
||||||
|
<div className="flex bg-surface border border-border rounded-full p-0.5 text-sm">
|
||||||
|
{['guitar', 'piano'].map(v => (
|
||||||
|
<button
|
||||||
|
key={v}
|
||||||
|
onClick={() => setInstrument(v)}
|
||||||
|
className={`px-4 py-1 rounded-full capitalize transition-all ${
|
||||||
|
instrument === v ? 'bg-accent text-white' : 'text-gray-400 hover:text-gray-200'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{v}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Key lock */}
|
{/* Key lock */}
|
||||||
{lockedKey ? (
|
{lockedKey ? (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -256,11 +275,15 @@ export default function App() {
|
|||||||
<SafeNotes keyInfo={effectiveKey} currentChord={currentChord} />
|
<SafeNotes keyInfo={effectiveKey} currentChord={currentChord} />
|
||||||
<ProgressionSuggestions keyInfo={effectiveKey} />
|
<ProgressionSuggestions keyInfo={effectiveKey} />
|
||||||
<div className="md:col-span-2">
|
<div className="md:col-span-2">
|
||||||
<Fretboard
|
{instrument === 'guitar' ? (
|
||||||
keyInfo={effectiveKey}
|
<Fretboard
|
||||||
currentChord={currentChord}
|
keyInfo={effectiveKey}
|
||||||
pentatonicOnly={false}
|
currentChord={currentChord}
|
||||||
/>
|
pentatonicOnly={false}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Piano keyInfo={effectiveKey} currentChord={currentChord} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="md:col-span-2">
|
<div className="md:col-span-2">
|
||||||
<Tuner />
|
<Tuner />
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
import { getFullScale, getChordTones, NOTES } from '../lib/theory'
|
||||||
|
|
||||||
|
// White keys in order within an octave, mapped to pitch class
|
||||||
|
const WHITE_KEYS = [
|
||||||
|
{ pc: 0, label: 'C' },
|
||||||
|
{ pc: 2, label: 'D' },
|
||||||
|
{ pc: 4, label: 'E' },
|
||||||
|
{ pc: 5, label: 'F' },
|
||||||
|
{ pc: 7, label: 'G' },
|
||||||
|
{ pc: 9, label: 'A' },
|
||||||
|
{ pc: 11, label: 'B' },
|
||||||
|
]
|
||||||
|
|
||||||
|
// Black keys: position (in white-key units from left of octave) and pitch class
|
||||||
|
const BLACK_KEYS = [
|
||||||
|
{ pc: 1, offset: 0.7 }, // C#
|
||||||
|
{ pc: 3, offset: 1.7 }, // D#
|
||||||
|
{ pc: 6, offset: 3.7 }, // F#
|
||||||
|
{ pc: 8, offset: 4.7 }, // G#
|
||||||
|
{ pc: 10, offset: 5.7 }, // A#
|
||||||
|
]
|
||||||
|
|
||||||
|
const OCTAVES = 2 // number of octaves shown
|
||||||
|
const KEY_W = 40 // white key width
|
||||||
|
const KEY_H = 130 // white key height
|
||||||
|
const BLACK_W = 26 // black key width
|
||||||
|
const BLACK_H = 82 // black key height
|
||||||
|
const LABEL_Y = KEY_H - 10 // y of note label on white key
|
||||||
|
const BLACK_LABEL_Y = BLACK_H - 8
|
||||||
|
|
||||||
|
function keyColor(isChordTone, isScale, isBlack) {
|
||||||
|
if (isChordTone) return { fill: '#f59e0b', text: '#000' }
|
||||||
|
if (isScale) return { fill: '#a855f7', text: '#fff' }
|
||||||
|
return isBlack
|
||||||
|
? { fill: '#1f1f1f', text: '#6b7280' }
|
||||||
|
: { fill: '#f5f5f5', text: '#6b7280' }
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Piano({ keyInfo, currentChord }) {
|
||||||
|
const { root, mode } = keyInfo ?? {}
|
||||||
|
if (!root) return null
|
||||||
|
|
||||||
|
const scaleSet = new Set(getFullScale(root, mode).map(n => NOTES.indexOf(n)))
|
||||||
|
const chordSet = currentChord
|
||||||
|
? new Set(getChordTones(currentChord).map(n => NOTES.indexOf(n)))
|
||||||
|
: new Set()
|
||||||
|
|
||||||
|
const totalWhite = WHITE_KEYS.length * OCTAVES
|
||||||
|
const svgW = totalWhite * KEY_W + 2
|
||||||
|
const svgH = KEY_H + 20 // +20 for octave labels
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-panel border border-border rounded-2xl p-6">
|
||||||
|
<p className="text-sm text-gray-500 uppercase tracking-widest mb-4">
|
||||||
|
Piano — {root} {mode}
|
||||||
|
{currentChord && <span className="text-amber-400 ml-2">/ {currentChord}</span>}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<svg width={svgW} height={svgH} style={{ display: 'block', minWidth: svgW }}>
|
||||||
|
|
||||||
|
{/* White keys */}
|
||||||
|
{Array.from({ length: OCTAVES }, (_, oct) =>
|
||||||
|
WHITE_KEYS.map((k, wi) => {
|
||||||
|
const x = (oct * WHITE_KEYS.length + wi) * KEY_W + 1
|
||||||
|
const isChordTone = chordSet.has(k.pc)
|
||||||
|
const isScale = scaleSet.has(k.pc)
|
||||||
|
const { fill, text } = keyColor(isChordTone, isScale, false)
|
||||||
|
return (
|
||||||
|
<g key={`w-${oct}-${wi}`}>
|
||||||
|
<rect
|
||||||
|
x={x} y={0}
|
||||||
|
width={KEY_W - 1} height={KEY_H}
|
||||||
|
fill={fill}
|
||||||
|
rx={3}
|
||||||
|
stroke="#2a2a2a"
|
||||||
|
strokeWidth={1}
|
||||||
|
/>
|
||||||
|
<text
|
||||||
|
x={x + KEY_W / 2} y={LABEL_Y}
|
||||||
|
textAnchor="middle"
|
||||||
|
fontSize={9}
|
||||||
|
fontWeight="600"
|
||||||
|
fill={text}
|
||||||
|
>
|
||||||
|
{k.label}{oct + 3}
|
||||||
|
</text>
|
||||||
|
</g>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Black keys (drawn on top) */}
|
||||||
|
{Array.from({ length: OCTAVES }, (_, oct) =>
|
||||||
|
BLACK_KEYS.map((k, bi) => {
|
||||||
|
const x = (oct * WHITE_KEYS.length + k.offset) * KEY_W + 1
|
||||||
|
const isChordTone = chordSet.has(k.pc)
|
||||||
|
const isScale = scaleSet.has(k.pc)
|
||||||
|
const { fill, text } = keyColor(isChordTone, isScale, true)
|
||||||
|
return (
|
||||||
|
<g key={`b-${oct}-${bi}`}>
|
||||||
|
<rect
|
||||||
|
x={x} y={0}
|
||||||
|
width={BLACK_W} height={BLACK_H}
|
||||||
|
fill={fill}
|
||||||
|
rx={2}
|
||||||
|
stroke="#111"
|
||||||
|
strokeWidth={1}
|
||||||
|
/>
|
||||||
|
<text
|
||||||
|
x={x + BLACK_W / 2} y={BLACK_LABEL_Y}
|
||||||
|
textAnchor="middle"
|
||||||
|
fontSize={8}
|
||||||
|
fontWeight="600"
|
||||||
|
fill={text}
|
||||||
|
>
|
||||||
|
{NOTES[k.pc]}
|
||||||
|
</text>
|
||||||
|
</g>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-3 flex gap-5 text-xs text-gray-500">
|
||||||
|
<span><span className="text-amber-400">●</span> Chord tone</span>
|
||||||
|
<span><span className="text-accent">●</span> Scale note</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user