import { getScale, getChordTones, NOTES } from '../lib/theory' // ─── SVG Piano — 2 octaves (C3–B4) ─────────────────────────────────────────── const KEY_W = 30 const KEY_H = 80 const BLACK_W = 18 const BLACK_H = 50 const PIANO_W = 14 * KEY_W const WHITE_OCT = [0, 2, 4, 5, 7, 9, 11] // pitch classes per octave const BLACK_OCT = [ { pc: 1, wi: 0 }, { pc: 3, wi: 1 }, { pc: 6, wi: 3 }, { pc: 8, wi: 4 }, { pc: 10, wi: 5 }, ] const WHITE_LABELS = ['C3','D3','E3','F3','G3','A3','B3','C4','D4','E4','F4','G4','A4','B4'] function PianoSVG({ values, keyNotes, chordNotes, keyH = KEY_H, showPct = false }) { const max = Math.max(...values, 0.01) const wKeys = [] const bKeys = [] for (let oct = 0; oct < 2; oct++) { WHITE_OCT.forEach((pc, wi) => wKeys.push({ pc, wi: oct * 7 + wi })) BLACK_OCT.forEach(({ pc, wi }) => bKeys.push({ pc, wi: oct * 7 + wi })) } const svgH = keyH + 6 + (showPct ? 16 : 0) return ( {/* White keys */} {wKeys.map(({ pc, wi }) => { const energy = values[pc] / max const inChord = chordNotes?.has(pc) const inKey = keyNotes?.has(pc) const x = wi * KEY_W const fillColor = inChord ? `rgba(167,139,250,${0.12 + energy * 0.88})` : inKey ? `rgba(251,191,36,${0.1 + energy * 0.7})` : `rgba(180,180,190,${0.05 + energy * 0.2})` const pct = showPct ? Math.round(values[pc] * 100) : 0 return ( {energy > (inChord || inKey ? 0.12 : 0.35) && ( )} {WHITE_LABELS[wi]} {showPct && pct > 0 && ( {pct}% )} ) })} {/* Black keys */} {bKeys.map(({ pc, wi }, i) => { const energy = values[pc] / max const inChord = chordNotes?.has(pc) const inKey = keyNotes?.has(pc) const x = wi * KEY_W + KEY_W - BLACK_W / 2 const fillColor = inChord ? 'rgba(139,92,246,0.9)' : inKey ? 'rgba(180,130,0,0.85)' : 'rgba(70,70,80,0.75)' const pct = showPct ? Math.round(values[pc] * 100) : 0 return ( {/* Base */} {/* Partial fill from bottom — same mechanic as white keys */} {energy > (inChord || inKey ? 0.05 : 0.35) && ( )} {/* % label near top of key (inside) */} {showPct && pct > 0 && ( {pct}% )} {/* Note name near bottom of key */} {NOTES[pc]} ) })} ) } // ─── Mini fretboard — guitar mode chroma view ───────────────────────────────── const STRINGS = [ { label: 'e', root: 4 }, { label: 'B', root: 11 }, { label: 'G', root: 7 }, { label: 'D', root: 2 }, { label: 'A', root: 9 }, { label: 'E', root: 4 }, ] const MF_NUT_X = 22 const MF_OPEN_X = 10 const MF_FRET_W = 28 const MF_STR_H = 16 const MF_PAD_T = 16 const MF_PAD_B = 8 const MF_FRETS = 13 // frets 0–12 const MF_DOT_R = 6 const MF_W = MF_NUT_X + (MF_FRETS - 1) * MF_FRET_W + 10 const MF_H = MF_PAD_T + 5 * MF_STR_H + MF_PAD_B const mfFretX = f => MF_NUT_X + (f - 0.5) * MF_FRET_W const mfStringY = si => MF_PAD_T + si * MF_STR_H function MiniFretboard({ values, keyNotes, chordNotes }) { const max = Math.max(...values, 0.01) return ( {/* Board background */} {/* Fret position dots */} {[3, 5, 7, 9].map(f => ( ))} {/* Fret lines */} {Array.from({ length: MF_FRETS - 1 }, (_, i) => i + 1).map(f => ( ))} {/* Nut */} {/* Strings */} {STRINGS.map((_, si) => ( ))} {/* Fret numbers */} {[3, 5, 7, 9, 12].map(f => ( {f} ))} {/* String labels */} {STRINGS.map((s, si) => ( {s.label} ))} {/* Note dots — colored by energy */} {STRINGS.flatMap((str, si) => Array.from({ length: MF_FRETS }, (_, fi) => { const pc = (str.root + fi) % 12 const energy = values[pc] / max const inChord = chordNotes?.has(pc) const inKey = keyNotes?.has(pc) if (!inChord && !inKey && energy < 0.35) return null if ((inChord || inKey) && energy < 0.08) return null const cx = fi === 0 ? MF_OPEN_X : mfFretX(fi) const cy = mfStringY(si) let fill, textFill if (inChord) { fill = `rgba(168,85,247,${0.3 + energy * 0.7})` textFill = '#fff' } else if (inKey) { fill = `rgba(245,158,11,${0.2 + energy * 0.75})` textFill = 'rgba(0,0,0,0.85)' } else { fill = `rgba(100,100,120,${energy * 0.7})` textFill = 'rgba(180,180,190,0.7)' } return ( {energy > 0.3 && (inChord || inKey) && ( )} {NOTES[pc]} ) }) )} ) } // ─── Oscilloscope strip ─────────────────────────────────────────────────────── const OSC_W = 600 const OSC_H = 110 function Oscilloscope({ waveform }) { const { wave, rms, detectedFreq, detectedNote } = waveform || {} const silent = !rms || rms < 0.005 let path = '', sinePath = '' if (wave?.length) { const mid = OSC_H / 2 const waveAmp = Math.max(...wave.map(Math.abs), 0.001) const gain = Math.min((OSC_H * 0.44) / waveAmp, OSC_H * 0.44) // Zero-crossing trigger: find first upward zero cross in first half → stable display const lo = Math.floor(wave.length / 4) const hi = Math.floor(wave.length / 2) let offset = lo for (let i = lo; i < hi - 1; i++) { if (wave[i] <= 0 && wave[i + 1] > 0) { offset = i; break } } const drawLen = Math.min(wave.length - offset, Math.floor(wave.length * 0.85)) const step = OSC_W / drawLen path = Array.from({ length: drawLen }, (_, i) => { const v = wave[offset + i] return `${i === 0 ? 'M' : 'L'}${(i * step).toFixed(1)},${(mid - v * gain).toFixed(1)}` }).join(' ') // Sine overlay at the detected fundamental, phase-matched to trigger offset if (detectedFreq) { const effectiveSR = 44100 / 8 const sineAmp = Math.min(waveAmp * gain * 0.55, OSC_H * 0.38) sinePath = Array.from({ length: 300 }, (_, i) => { const t = i / 299 const x = (t * OSC_W).toFixed(1) const phase = ((offset + t * drawLen) / effectiveSR) * detectedFreq * Math.PI * 2 const y = (mid - Math.sin(phase) * sineAmp).toFixed(1) return `${i === 0 ? 'M' : 'L'}${x},${y}` }).join(' ') } } const lineColor = detectedFreq ? 'rgba(168,85,247,0.9)' : silent ? 'rgba(50,50,60,0.8)' : 'rgba(100,200,140,0.75)' return (

Oscilloscope — raw mic input

{detectedFreq && detectedNote && ( <> {detectedNote} {detectedFreq.toFixed(1)} Hz {(1000 / detectedFreq).toFixed(2)} ms / cycle )} {!detectedFreq && !silent && signal — no clear pitch} {silent && silence} rms {rms ? (rms * 100).toFixed(1) : '0.0'}%
{/* Zero line */} {/* Fill body — close path to the centre line for a filled silhouette */} {path && ( )} {/* Waveform line */} {path && } {/* Sine overlay */} {sinePath && }
) } // ─── Frequency spectrum ─────────────────────────────────────────────────────── const SPEC_H = 130 const SPEC_F_MIN = 40 const SPEC_F_MAX = 4000 const SPEC_LOG = Math.log(SPEC_F_MAX / SPEC_F_MIN) // Map a frequency in Hz to an x pixel position (log scale) function specX(f, w) { if (f <= SPEC_F_MIN) return 0 if (f >= SPEC_F_MAX) return w return w * Math.log(f / SPEC_F_MIN) / SPEC_LOG } const SPEC_GRID = [ { label: 'E2', freq: 82.4 }, { label: 'C3', freq: 130.8 }, { label: 'E3', freq: 164.8 }, { label: 'A3', freq: 220 }, { label: 'C4', freq: 261.6 }, { label: 'E4', freq: 329.6 }, { label: 'A4', freq: 440 }, { label: 'C5', freq: 523.3 }, { label: 'C6', freq: 1046.5}, { label: 'C7', freq: 2093 }, ] function SpectrumPanel({ spectrum, detectedFreq }) { const W = OSC_W let fillPath = '', strokePath = '' if (spectrum?.length) { const n = spectrum.length const pts = Array.from({ length: n }, (_, i) => { const x = ((i / (n - 1)) * W).toFixed(1) const y = (SPEC_H * (1 - spectrum[i])).toFixed(1) return `${i === 0 ? 'M' : 'L'}${x},${y}` }).join(' ') strokePath = pts fillPath = pts + ` L${W},${SPEC_H} L0,${SPEC_H} Z` } return (

Frequency spectrum — 40 Hz → 4 kHz (log scale)

{/* Note grid lines */} {SPEC_GRID.map(({ label, freq }) => { const x = specX(freq, W).toFixed(1) return ( {label} ) })} {/* Spectrum fill + stroke */} {fillPath && ( <> )} {/* Fundamental frequency */} {detectedFreq && (() => { const x = specX(detectedFreq, W).toFixed(1) return ( f ) })()} {/* Harmonics 2f–5f */} {detectedFreq && [2, 3, 4, 5].map(h => { const hf = detectedFreq * h if (hf > SPEC_F_MAX) return null const x = specX(hf, W).toFixed(1) const op = (0.5 - (h - 2) * 0.1).toFixed(2) return ( {h}f ) })}
) } // ─── Main component ─────────────────────────────────────────────────────────── export default function DebugView({ chroma, chordCandidates, noteAnalysis, waveform, keyInfo, currentChord, instrument = 'guitar' }) { const keyPCs = new Set(keyInfo ? getScale(keyInfo.root, keyInfo.mode).map(n => NOTES.indexOf(n)) : []) const chordPCs = new Set(currentChord ? getChordTones(currentChord).map(n => NOTES.indexOf(n)) : []) const chromaArr = chroma ? [...chroma] : new Array(12).fill(0) const histFreq = noteAnalysis ? noteAnalysis.freq : new Array(12).fill(0) const topKeys = noteAnalysis ? noteAnalysis.topKeys : [] const totalNotes = noteAnalysis?.total ?? 0 const sessionSecs = noteAnalysis?.sessionSecs ?? 0 const sessionLabel = sessionSecs >= 60 ? `${Math.floor(sessionSecs / 60)}m ${sessionSecs % 60}s` : `${sessionSecs}s` const topScore = chordCandidates[0]?.score ?? 1 const topKeyScore = topKeys[0]?.score ?? 1 return (

Behind the Scenes

{/* ── Live chroma visualization (instrument-synced) ── */}

Live chroma — what the engine hears right now

{instrument === 'guitar' ? : }
{/* ── Bottom three columns ── */}
{/* Col 1: Chord candidates */}

Chord candidates

{chordCandidates.length === 0 && (

No signal detected

)} {chordCandidates.map((c, i) => (
{i + 1} {c.name}
{c.score.toFixed(2)}
{c.diatonic && key} {c.bassBonus > 0 && bass}
))}
{/* Col 2: Note history piano with % labels */}

Note history

{totalNotes > 0 && ( {totalNotes.toLocaleString()} notes · {sessionLabel} )}
{/* Col 3: Key candidates */}

Key match scores

{topKeys.length === 0 && (

Not enough history

)} {topKeys.map((k, i) => (
{k.root} {k.mode === 'major' ? 'maj' : 'min'}
{k.score.toFixed(2)}
))}
{/* ── Oscilloscope + spectrum ── */}
) }