new song button, chord-boosted key detection, double-confirm key changes

This commit is contained in:
vadimwit
2026-03-05 23:25:40 +00:00
parent cfa7b877f6
commit d3a2840454
+57 -8
View File
@@ -8,12 +8,13 @@ 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 Piano from './components/Piano'
import { NOTES, detectKey, detectTopKeys, matchChordFromChroma, detectRepeatingProgression } from './lib/theory' import { NOTES, detectKey, detectTopKeys, matchChordFromChroma, detectRepeatingProgression, getChordTones } from './lib/theory'
// Key detection tuning // Key detection tuning
const NOTE_HISTORY_SIZE = 80 const NOTE_HISTORY_SIZE = 160 // larger = chord boosts dominate over melody runs
const KEY_VOTE_WINDOW = 12 const KEY_VOTE_WINDOW = 12
const KEY_VOTE_THRESHOLD = 9 // out of 12 — very stable const KEY_VOTE_THRESHOLD = 9 // out of 12
const CHORD_NOTE_BOOST = 3 // confirmed chord tones injected N× into note history
// Chord detection tuning // Chord detection tuning
const CHROMA_SMOOTH = 12 // frames to average (~200ms at 60fps) const CHROMA_SMOOTH = 12 // frames to average (~200ms at 60fps)
@@ -52,6 +53,7 @@ export default function App() {
const chromaIdxRef = useRef(0) const chromaIdxRef = useRef(0)
const chordVotesRef = useRef([]) const chordVotesRef = useRef([])
const progressionVoteRef = useRef(null) // stabilise loop display const progressionVoteRef = useRef(null) // stabilise loop display
const pendingKeyRef = useRef(null) // proposed key change — require 2 consecutive wins
// Keep ref in sync // Keep ref in sync
useEffect(() => { effectiveKeyRef.current = effectiveKey }, [effectiveKey]) useEffect(() => { effectiveKeyRef.current = effectiveKey }, [effectiveKey])
@@ -68,6 +70,23 @@ export default function App() {
} }
}, [chordHistory]) }, [chordHistory])
// ── New song — full reset ─────────────────────────────────────────────────
function newSong() {
noteHistoryRef.current = []
keyVotesRef.current = []
chordVotesRef.current = []
progressionVoteRef.current = null
pendingKeyRef.current = null
chromaIdxRef.current = 0
chromaRingRef.current = Array.from({ length: CHROMA_SMOOTH }, () => new Float32Array(12))
setKeyInfo(null)
setLockedKey(null)
effectiveKeyRef.current = null
setChordHistory([])
setDetectedProgression(null)
setTopKeyCandidates([])
}
// ── Key lock handlers ───────────────────────────────────────────────────── // ── Key lock handlers ─────────────────────────────────────────────────────
function applyLock() { function applyLock() {
const info = { root: lockRoot, mode: lockMode, confidence: 1 } const info = { root: lockRoot, mode: lockMode, confidence: 1 }
@@ -110,15 +129,26 @@ export default function App() {
if (count >= KEY_VOTE_THRESHOLD) { if (count >= KEY_VOTE_THRESHOLD) {
const [root, mode] = winner.split('_') const [root, mode] = winner.split('_')
const candidateKey = `${root}_${mode}`
setKeyInfo(prev => { setKeyInfo(prev => {
if (prev?.root === root && prev?.mode === mode) { const currentKey = prev ? `${prev.root}_${prev.mode}` : null
if (currentKey === candidateKey) {
// Same key — just refresh confidence, clear pending
pendingKeyRef.current = null
return { root, mode, confidence: result.confidence } return { root, mode, confidence: result.confidence }
} }
// Key changed — reset chord votes but keep history visible
if (!lockedKey) { // Different key — require a second consecutive win before committing
chordVotesRef.current = [] if (pendingKeyRef.current === candidateKey) {
} pendingKeyRef.current = null
if (!lockedKey) chordVotesRef.current = []
return { root, mode, confidence: result.confidence } return { root, mode, confidence: result.confidence }
}
pendingKeyRef.current = candidateKey
return prev // hold current key until next confirmation
}) })
} }
}, [lockedKey]) }, [lockedKey])
@@ -156,6 +186,17 @@ export default function App() {
if (prev[prev.length - 1] === winner) return prev if (prev[prev.length - 1] === winner) return prev
return [...prev.slice(-30), winner] return [...prev.slice(-30), winner]
}) })
// Inject chord tones into note history with boost weight so key detection
// anchors to confirmed chords rather than transient melody notes
const chordPCs = getChordTones(winner)
.map(n => NOTES.indexOf(n))
.filter(i => i >= 0)
const history = noteHistoryRef.current
for (let j = 0; j < CHORD_NOTE_BOOST; j++) {
for (const pc of chordPCs) history.push(pc)
}
while (history.length > NOTE_HISTORY_SIZE) history.shift()
} }
}, []) }, [])
@@ -172,6 +213,13 @@ export default function App() {
</h1> </h1>
<p className="text-xs text-gray-600 mt-0.5">Real-time key detection for real humans</p> <p className="text-xs text-gray-600 mt-0.5">Real-time key detection for real humans</p>
</div> </div>
<div className="flex gap-2">
<button
onClick={newSong}
className="px-4 py-2.5 rounded-full font-semibold text-sm border border-border text-gray-400 hover:text-gray-200 hover:border-gray-400 transition-all"
>
New Song
</button>
<button <button
onClick={() => setIsListening(l => !l)} onClick={() => setIsListening(l => !l)}
className={`px-5 py-2.5 rounded-full font-semibold text-sm transition-all ${ className={`px-5 py-2.5 rounded-full font-semibold text-sm transition-all ${
@@ -182,6 +230,7 @@ export default function App() {
> >
{isListening ? 'Stop' : 'Start Listening'} {isListening ? 'Stop' : 'Start Listening'}
</button> </button>
</div>
</header> </header>
{/* ── Controls bar ── */} {/* ── Controls bar ── */}