add piano view with instrument toggle

This commit is contained in:
vadimwit
2026-03-05 22:58:32 +00:00
parent 4ed382393e
commit cfa7b877f6
2 changed files with 161 additions and 5 deletions
+28 -5
View File
@@ -7,6 +7,7 @@ import SafeNotes from './components/SafeNotes'
import Fretboard from './components/Fretboard'
import ProgressionSuggestions from './components/ProgressionSuggestions'
import Tuner from './components/Tuner'
import Piano from './components/Piano'
import { NOTES, detectKey, detectTopKeys, matchChordFromChroma, detectRepeatingProgression } from './lib/theory'
// Key detection tuning
@@ -22,6 +23,9 @@ export default function App() {
// ── Listening state ──────────────────────────────────────────────────────
const [isListening, setIsListening] = useState(false)
// ── Instrument view ───────────────────────────────────────────────────────
const [instrument, setInstrument] = useState('guitar') // 'guitar' | 'piano'
// ── Key: auto-detected + optional lock ───────────────────────────────────
const [keyInfo, setKeyInfo] = useState(null) // auto-detected
const [lockedKey, setLockedKey] = useState(null) // { root, mode } or null
@@ -182,6 +186,21 @@ export default function App() {
{/* ── Controls bar ── */}
<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 */}
{lockedKey ? (
<div className="flex items-center gap-2">
@@ -256,11 +275,15 @@ export default function App() {
<SafeNotes keyInfo={effectiveKey} currentChord={currentChord} />
<ProgressionSuggestions keyInfo={effectiveKey} />
<div className="md:col-span-2">
<Fretboard
keyInfo={effectiveKey}
currentChord={currentChord}
pentatonicOnly={false}
/>
{instrument === 'guitar' ? (
<Fretboard
keyInfo={effectiveKey}
currentChord={currentChord}
pentatonicOnly={false}
/>
) : (
<Piano keyInfo={effectiveKey} currentChord={currentChord} />
)}
</div>
<div className="md:col-span-2">
<Tuner />
+133
View File
@@ -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>
)
}