melodic fine tuning

This commit is contained in:
vadimwit
2026-03-05 17:56:46 +00:00
parent 4e6d997b68
commit a6b3455141
3 changed files with 24 additions and 14 deletions
+13 -10
View File
@@ -15,8 +15,8 @@ const KEY_VOTE_WINDOW = 12
const KEY_VOTE_THRESHOLD = 9 // out of 12 — very stable
// Chord detection tuning
const CHROMA_SMOOTH = 8 // frames to average (~130ms at 60fps)
const CHORD_VOTE_THRESHOLD = 3 // consecutive agreements before commit
const CHROMA_SMOOTH = 12 // frames to average (~200ms at 60fps)
const CHORD_VOTE_THRESHOLD = 4 // consecutive identical detections required
export default function App() {
// ── Listening state ──────────────────────────────────────────────────────
@@ -33,7 +33,8 @@ export default function App() {
// Effective key used by all components
const effectiveKey = lockedKey ?? keyInfo
const isStrictMode = appMode === 'beginner' && lockedKey !== null
// Locked key = only match diatonic chords regardless of mode — far fewer candidates
const isStrictMode = lockedKey !== null
// ── Chord state ───────────────────────────────────────────────────────────
const [chordHistory, setChordHistory] = useState([])
@@ -122,17 +123,19 @@ export default function App() {
for (let i = 0; i < 12; i++) avg[i] /= CHROMA_SMOOTH
const chord = matchChordFromChroma(avg, key, bassPC, isStrictMode)
if (!chord) return
if (!chord) {
// Ambiguous moment (transition, silence) — reset streak, history is untouched
chordVotesRef.current = []
return
}
const votes = chordVotesRef.current
votes.push(chord)
if (votes.length > CHORD_VOTE_THRESHOLD * 2) votes.shift()
if (votes.length > CHORD_VOTE_THRESHOLD) votes.shift()
const counts = {}
for (const v of votes) counts[v] = (counts[v] || 0) + 1
const [winner, winCount] = Object.entries(counts).sort((a, b) => b[1] - a[1])[0]
if (winCount >= CHORD_VOTE_THRESHOLD) {
// All last N detections must agree — one wrong reading resets the streak
if (votes.length >= CHORD_VOTE_THRESHOLD && votes.every(v => v === votes[0])) {
const winner = votes[0]
setChordHistory(prev => {
if (prev[prev.length - 1] === winner) return prev
return [...prev.slice(-30), winner]
+1 -1
View File
@@ -44,7 +44,7 @@ function computeChroma(freqData, sampleRate, fftSize) {
for (let bin = 2; bin < N; bin++) {
const freq = bin * binHz
if (freq < 80 || freq > 6000) continue
if (freq < 80 || freq > 2000) continue
const db = freqData[bin]
if (db < NOISE_FLOOR) continue
+10 -3
View File
@@ -187,6 +187,7 @@ export function matchChordFromChroma(chroma, keyInfo, bassPC = null, strictDiato
CHORD_TYPES.sus4,
]
let best = { name: null, score: -Infinity }
let secondScore = -Infinity
for (let r = 0; r < 12; r++) {
for (const type of matchTypes) {
@@ -210,15 +211,21 @@ export function matchChordFromChroma(chroma, keyInfo, bassPC = null, strictDiato
const coverageScore = inEnergy / (inEnergy + outEnergy * 0.5)
// Bass note matching chord root is a strong harmonic signal
const bassBonus = (bassPC !== null && r === bassPC) ? 0.35 : 0
const bassBonus = (bassPC !== null && r === bassPC) ? 0.15 : 0
const diatonicBonus = diatonic.has(chordName) ? 0.15 : 0
const finalScore = coverageScore + bassBonus + diatonicBonus
if (finalScore > best.score) best = { name: chordName, score: finalScore }
if (finalScore > best.score) {
secondScore = best.score
best = { name: chordName, score: finalScore }
} else if (finalScore > secondScore) {
secondScore = finalScore
}
}
}
return best.score > 0.42 ? best.name : null
// Require a clear winner — if two chords score too close it's a transition/ambiguity
return (best.score > 0.42 && best.score - secondScore > 0.08) ? best.name : null
}
// ─── Roman numeral notation ───────────────────────────────────────────────────