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 (
)
}
// ─── 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 (
)
}
// ─── 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'}%
)
}
// ─── 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)
)
}
// ─── 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 ── */}
)
}