additions for debug view and better formatting and sound fine tuning

This commit is contained in:
vadimwit
2026-03-06 15:00:40 +00:00
parent c08cd619f7
commit 7a108ad0e2
6 changed files with 275 additions and 130 deletions
+14
View File
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.device.audio-input</key>
<true/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
</dict>
</plist>
+18 -4
View File
@@ -1,8 +1,22 @@
const { app, BrowserWindow } = require('electron')
const { app, BrowserWindow, systemPreferences } = require('electron')
const path = require('path')
const isDev = !app.isPackaged
async function handlePermissions() {
// macOS requires an explicit native request for microphone access in packaged apps
if (process.platform === 'darwin') {
try {
const status = systemPreferences.getMediaAccessStatus('microphone')
if (status !== 'granted') {
await systemPreferences.askForMediaAccess('microphone')
}
} catch (err) {
console.error('[Main] Microphone permission error:', err)
}
}
}
function createWindow() {
const win = new BrowserWindow({
width: 1280,
@@ -15,20 +29,20 @@ function createWindow() {
preload: path.join(__dirname, 'preload.cjs'),
contextIsolation: true,
nodeIntegration: false,
sandbox: false, // allows renderer getUserMedia to work on all platforms
},
})
if (isDev) {
// Dev: load from Vite dev server
win.loadURL('http://localhost:5173')
win.webContents.openDevTools()
} else {
// Production: load built files from disk
win.loadFile(path.join(__dirname, '../dist/index.html'))
}
}
app.whenReady().then(() => {
app.whenReady().then(async () => {
await handlePermissions()
createWindow()
app.on('activate', () => {
+7 -6
View File
@@ -13,14 +13,14 @@ import viewIcon from './assets/view.png'
const DEFAULTS = {
// Key detection
noteHistorySize: 12000, // ~whole session until New Song
keyVoteWindow: 40, // larger window → needs sustained evidence to shift
keyVoteThreshold: 32, // 80% of window must agree
noteHistorySize: 2000, // ~60s of notes — stable across a song section
keyVoteWindow: 30, // rolling window of key votes
keyVoteThreshold: 20, // 67% consensus — locks in after a few bars
chordNoteBoost: 3,
// Chord detection
chromaSmooth: 14, // more frames averaged → transient chords invisible
chordVoteThreshold: 4, // 4 consecutive identical reads → ~600ms sustained
chordMinScore: 0.40, // slightly stricter match quality
chromaSmooth: 8, // 8 frames ≈ 130ms window, checks chord at ~7.5 Hz
chordVoteThreshold: 2, // 2 consecutive matches ≈ 260ms — works at any BPM
chordMinScore: 0.35, // lenient enough for live guitar signal
// Audio input
minClarity: 0.80,
minVolume: 0.01,
@@ -495,6 +495,7 @@ export default function App() {
noteAnalysis={debugNoteAnalysis}
keyInfo={effectiveKey}
currentChord={currentChord}
instrument={instrument}
/>
</div>
)}
+200 -93
View File
@@ -1,92 +1,213 @@
import { getScale, getChordTones, NOTES } from '../lib/theory'
// ─── Piano keyboard constants ─────────────────────────────────────────────────
const WHITE_PCS = [0, 2, 4, 5, 7, 9, 11] // C D E F G A B
const BLACK_KEYS = [
{ pc: 1, left: '10%' }, // C#
{ pc: 3, left: '24.3%' }, // D#
{ pc: 6, left: '52.9%' }, // F#
{ pc: 8, left: '67.1%' }, // G#
{ pc: 10, left: '81.4%' }, // A#
]
// ─── SVG Piano — 2 octaves (C3B4) ───────────────────────────────────────────
const KEY_W = 30
const KEY_H = 80
const BLACK_W = 18
const BLACK_H = 50
const PIANO_W = 14 * KEY_W
// ─── Piano keyboard component ─────────────────────────────────────────────────
function PianoKeyboard({ values, keyNotes, chordNotes, height = 'h-24' }) {
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 (
<svg viewBox={`0 0 ${PIANO_W} ${svgH}`} width="100%" style={{ display: 'block' }}>
{/* 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 (
<g key={`w${wi}`}>
<rect x={x+1} y={3} width={KEY_W-2} height={keyH}
rx={3} fill="rgb(20,20,26)" stroke="rgba(255,255,255,0.08)" strokeWidth={1} />
{energy > 0.04 && (
<rect
x={x+1} y={3 + keyH * (1 - Math.min(energy, 1) * 0.85)}
width={KEY_W-2} height={keyH * Math.min(energy, 1) * 0.85}
rx={2} fill={fillColor} />
)}
<text x={x + KEY_W/2} y={keyH - 4} textAnchor="middle" fontSize={8}
fill={inKey || inChord ? 'rgba(200,200,210,0.9)' : 'rgba(90,90,100,0.8)'}>
{WHITE_LABELS[wi]}
</text>
{showPct && pct > 0 && (
<text x={x + KEY_W/2} y={keyH + 14} textAnchor="middle" fontSize={8}
fill={inKey ? 'rgb(251,191,36)' : 'rgba(100,100,110,0.8)'}>
{pct}%
</text>
)}
</g>
)
})}
{/* 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 bg = inChord
? `rgba(139,92,246,${0.4 + energy * 0.6})`
: inKey
? `rgba(180,130,0,${0.35 + energy * 0.55})`
: `rgba(12,12,16,0.95)`
return (
<g key={`b${i}`}>
<rect x={x} y={3} width={BLACK_W} height={BLACK_H}
rx={2} fill={bg} stroke="rgba(255,255,255,0.06)" strokeWidth={1} />
<text x={x + BLACK_W/2} y={BLACK_H - 5} textAnchor="middle" fontSize={7}
fill={inKey || inChord ? 'rgba(210,210,220,0.85)' : 'rgba(110,110,120,0.6)'}>
{NOTES[pc]}
</text>
</g>
)
})}
</svg>
)
}
// ─── 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 012
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 (
<div className={`relative ${height} select-none`}>
{/* White keys */}
<div className="flex gap-px h-full">
{WHITE_PCS.map(pc => {
const name = NOTES[pc]
<svg viewBox={`0 0 ${MF_W} ${MF_H}`} width="100%" style={{ display: 'block' }}>
{/* Board background */}
<rect x={MF_NUT_X} y={MF_PAD_T - 5}
width={MF_W - MF_NUT_X - 6} height={5 * MF_STR_H + 10}
fill="#1a120b" rx={2} />
{/* Fret position dots */}
{[3, 5, 7, 9].map(f => (
<circle key={f} cx={mfFretX(f)} cy={MF_PAD_T + 2.5 * MF_STR_H} r={3} fill="#3a2a1a" />
))}
<circle cx={mfFretX(12)} cy={MF_PAD_T + 1.5 * MF_STR_H} r={3} fill="#3a2a1a" />
<circle cx={mfFretX(12)} cy={MF_PAD_T + 3.5 * MF_STR_H} r={3} fill="#3a2a1a" />
{/* Fret lines */}
{Array.from({ length: MF_FRETS - 1 }, (_, i) => i + 1).map(f => (
<line key={f}
x1={MF_NUT_X + f * MF_FRET_W} y1={MF_PAD_T - 5}
x2={MF_NUT_X + f * MF_FRET_W} y2={MF_PAD_T + 5 * MF_STR_H + 5}
stroke="#4a3a2a" strokeWidth={1} />
))}
{/* Nut */}
<line x1={MF_NUT_X} y1={MF_PAD_T - 5} x2={MF_NUT_X} y2={MF_PAD_T + 5 * MF_STR_H + 5}
stroke="#c0b090" strokeWidth={3} />
{/* Strings */}
{STRINGS.map((_, si) => (
<line key={si}
x1={MF_OPEN_X - MF_DOT_R - 2} y1={mfStringY(si)}
x2={MF_W - 6} y2={mfStringY(si)}
stroke="#9ca3af"
strokeWidth={si < 2 ? 0.8 : si < 4 ? 1.2 : 1.8} />
))}
{/* Fret numbers */}
{[3, 5, 7, 9, 12].map(f => (
<text key={f} x={mfFretX(f)} y={MF_PAD_T - 5}
textAnchor="middle" fontSize={8} fill="#6b7280">{f}</text>
))}
{/* String labels */}
{STRINGS.map((s, si) => (
<text key={si} x={5} y={mfStringY(si) + 3.5}
textAnchor="middle" fontSize={9} fill="#6b7280">{s.label}</text>
))}
{/* 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.12) return null
const glow = inChord
? `rgba(167,139,250,${0.25 + energy * 0.75})`
: inKey
? `rgba(251,191,36,${0.15 + energy * 0.55})`
: `rgba(200,200,210,${0.06 + energy * 0.18})`
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 (
<div
key={pc}
className="flex-1 rounded-b border border-gray-700 relative overflow-hidden flex flex-col justify-end"
style={{
background: `linear-gradient(to top, ${glow} 0%, rgba(22,22,28,1) ${Math.max(5, energy * 75)}%)`,
boxShadow: energy > 0.35 && inChord
? '0 -6px 16px rgba(167,139,250,0.35) inset'
: energy > 0.35 && inKey
? '0 -4px 10px rgba(251,191,36,0.2) inset'
: 'none',
}}
>
<span className="text-center text-[8px] text-gray-600 pb-1 leading-none">{name}</span>
</div>
<g key={`${si}-${fi}`}>
{energy > 0.3 && (inChord || inKey) && (
<circle cx={cx} cy={cy} r={MF_DOT_R + 4}
fill={inChord ? 'rgba(168,85,247,0.25)' : 'rgba(245,158,11,0.2)'}
style={{ filter: 'blur(4px)' }} />
)}
<circle cx={cx} cy={cy} r={MF_DOT_R} fill={fill} />
<text x={cx} y={cy + 3.5} textAnchor="middle" fontSize={7} fontWeight="600" fill={textFill}>
{NOTES[pc]}
</text>
</g>
)
})}
</div>
{/* Black keys */}
{BLACK_KEYS.map(({ pc, left }) => {
const name = NOTES[pc]
const energy = values[pc] / max
const inChord = chordNotes?.has(pc)
const inKey = keyNotes?.has(pc)
const bg = inChord
? `rgba(139,92,246,${0.45 + energy * 0.55})`
: inKey
? `rgba(180,140,10,${0.4 + energy * 0.45})`
: `rgba(12,12,16,${0.88 + energy * 0.12})`
return (
<div
key={pc}
className="absolute top-0 z-10 rounded-b"
style={{
left,
width: '8.5%',
height: '62%',
background: bg,
border: '1px solid rgba(255,255,255,0.06)',
boxShadow: energy > 0.4
? inChord
? '0 0 10px rgba(139,92,246,0.5)'
: '0 0 4px rgba(255,255,255,0.08)'
: 'none',
}}
/>
)
})}
</div>
})
)}
</svg>
)
}
// ─── Main component ───────────────────────────────────────────────────────────
export default function DebugView({ chroma, chordCandidates, noteAnalysis, keyInfo, currentChord }) {
export default function DebugView({ chroma, chordCandidates, noteAnalysis, 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)) : [])
@@ -100,10 +221,13 @@ export default function DebugView({ chroma, chordCandidates, noteAnalysis, keyIn
<div className="bg-panel border border-border rounded-2xl p-4 flex flex-col gap-4">
<p className="text-xs text-gray-500 uppercase tracking-widest shrink-0">Behind the Scenes</p>
{/* ── Large live chroma keyboard ── */}
{/* ── Live chroma visualization (instrument-synced) ── */}
<div>
<p className="text-xs text-gray-600 uppercase tracking-widest mb-2">Live chroma what the engine hears right now</p>
<PianoKeyboard values={chromaArr} keyNotes={keyPCs} chordNotes={chordPCs} height="h-28" />
{instrument === 'guitar'
? <MiniFretboard values={chromaArr} keyNotes={keyPCs} chordNotes={chordPCs} />
: <PianoSVG values={chromaArr} keyNotes={keyPCs} chordNotes={chordPCs} keyH={90} />
}
</div>
{/* ── Bottom three columns ── */}
@@ -143,27 +267,10 @@ export default function DebugView({ chroma, chordCandidates, noteAnalysis, keyIn
</div>
</div>
{/* Col 2: Note history keyboard */}
{/* Col 2: Note history piano with % labels */}
<div>
<p className="text-xs text-gray-600 uppercase tracking-widest mb-2">Note history key evidence</p>
<PianoKeyboard values={histFreq} keyNotes={keyPCs} chordNotes={chordPCs} height="h-20" />
<div className="flex mt-2 gap-px">
{NOTES.map((name, pc) => {
const pct = Math.round(histFreq[pc] * 100)
const inKey = keyPCs.has(pc)
const isBlack = [1, 3, 6, 8, 10].includes(pc)
return (
<div key={pc} className="flex-1 flex flex-col items-center gap-0.5">
<span className={`text-[8px] tabular-nums ${inKey ? 'text-amber-400' : 'text-gray-600'}`}>
{pct > 0 ? `${pct}%` : ''}
</span>
<span className={`text-[7px] ${inKey ? 'text-gray-400' : isBlack ? 'text-gray-700' : 'text-gray-600'}`}>
{name}
</span>
</div>
)
})}
</div>
<PianoSVG values={histFreq} keyNotes={keyPCs} chordNotes={chordPCs} keyH={70} showPct={true} />
</div>
{/* Col 3: Key candidates */}
+16 -7
View File
@@ -1,4 +1,4 @@
import { getFullScale, getChordTones, NOTES } from '../lib/theory'
import { getPentatonicScale, getFullScale, getChordTones, NOTES } from '../lib/theory'
// White keys in order within an octave, mapped to pitch class
const WHITE_KEYS = [
@@ -28,9 +28,10 @@ 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) {
function keyColor(isChordTone, isPenta, isScale, isBlack) {
if (isChordTone) return { fill: '#a855f7', text: '#fff' }
if (isScale) return { fill: '#f59e0b', text: '#000' }
if (isPenta) return { fill: '#f59e0b', text: '#000' }
if (isScale) return { fill: '#374151', text: '#d1d5db' }
return isBlack
? { fill: '#1f1f1f', text: '#6b7280' }
: { fill: '#f5f5f5', text: '#6b7280' }
@@ -40,6 +41,7 @@ export default function Piano({ keyInfo, currentChord }) {
const { root, mode } = keyInfo ?? {}
if (!root) return null
const pentaSet = new Set(getPentatonicScale(root, mode).map(n => NOTES.indexOf(n)))
const scaleSet = new Set(getFullScale(root, mode).map(n => NOTES.indexOf(n)))
const chordSet = currentChord
? new Set(getChordTones(currentChord).map(n => NOTES.indexOf(n)))
@@ -64,8 +66,9 @@ export default function Piano({ keyInfo, currentChord }) {
WHITE_KEYS.map((k, wi) => {
const x = (oct * WHITE_KEYS.length + wi) * KEY_W + 1
const isChordTone = chordSet.has(k.pc)
const isPenta = pentaSet.has(k.pc)
const isScale = scaleSet.has(k.pc)
const { fill, text } = keyColor(isChordTone, isScale, false)
const { fill, text } = keyColor(isChordTone, isPenta, isScale, false)
return (
<g key={`w-${oct}-${wi}`}>
<rect
@@ -76,6 +79,7 @@ export default function Piano({ keyInfo, currentChord }) {
stroke="#2a2a2a"
strokeWidth={1}
/>
{(isChordTone || isPenta || isScale) && (
<text
x={x + KEY_W / 2} y={LABEL_Y}
textAnchor="middle"
@@ -83,8 +87,9 @@ export default function Piano({ keyInfo, currentChord }) {
fontWeight="600"
fill={text}
>
{k.label}{oct + 3}
{k.label}
</text>
)}
</g>
)
})
@@ -95,8 +100,9 @@ export default function Piano({ keyInfo, currentChord }) {
BLACK_KEYS.map((k, bi) => {
const x = (oct * WHITE_KEYS.length + k.offset) * KEY_W + 1
const isChordTone = chordSet.has(k.pc)
const isPenta = pentaSet.has(k.pc)
const isScale = scaleSet.has(k.pc)
const { fill, text } = keyColor(isChordTone, isScale, true)
const { fill, text } = keyColor(isChordTone, isPenta, isScale, true)
return (
<g key={`b-${oct}-${bi}`}>
<rect
@@ -107,6 +113,7 @@ export default function Piano({ keyInfo, currentChord }) {
stroke="#111"
strokeWidth={1}
/>
{(isChordTone || isPenta || isScale) && (
<text
x={x + BLACK_W / 2} y={BLACK_LABEL_Y}
textAnchor="middle"
@@ -116,6 +123,7 @@ export default function Piano({ keyInfo, currentChord }) {
>
{NOTES[k.pc]}
</text>
)}
</g>
)
})
@@ -126,7 +134,8 @@ export default function Piano({ keyInfo, currentChord }) {
<div className="mt-3 flex gap-5 text-xs text-gray-500">
<span><span className="text-accent"></span> Chord tone</span>
<span><span className="text-accent"></span> Scale note</span>
<span><span className="text-amber-400"></span> Pentatonic</span>
<span><span className="text-gray-500"></span> Scale</span>
</div>
</div>
)
+2 -2
View File
@@ -28,8 +28,8 @@ const SETTINGS = [
{
key: 'noteHistorySize',
label: 'Note History Size',
min: 20, max: 400, step: 10,
desc: 'Pitch readings kept in memory for key detection. Larger = slower to change, but chord boosts dominate more.',
min: 100, max: 12000, step: 100,
desc: 'Pitch readings kept for key detection. ~2000 ≈ 1 min, 12000 ≈ whole session. Larger = more stable key.',
},
{
key: 'keyVoteWindow',