From 7a108ad0e2105d10c0d52af76a06704b2de65339 Mon Sep 17 00:00:00 2001 From: vadimwit Date: Fri, 6 Mar 2026 15:00:40 +0000 Subject: [PATCH] additions for debug view and better formatting and sound fine tuning --- electron/entitlements.mac.plist | 14 ++ electron/main.cjs | 22 ++- src/App.jsx | 13 +- src/components/DebugView.jsx | 295 ++++++++++++++++++++++---------- src/components/Piano.jsx | 57 +++--- src/components/Settings.jsx | 4 +- 6 files changed, 275 insertions(+), 130 deletions(-) create mode 100644 electron/entitlements.mac.plist diff --git a/electron/entitlements.mac.plist b/electron/entitlements.mac.plist new file mode 100644 index 0000000..867d1cc --- /dev/null +++ b/electron/entitlements.mac.plist @@ -0,0 +1,14 @@ + + + + + com.apple.security.device.audio-input + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.disable-library-validation + + + diff --git a/electron/main.cjs b/electron/main.cjs index 462aaeb..4dbad19 100644 --- a/electron/main.cjs +++ b/electron/main.cjs @@ -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', () => { diff --git a/src/App.jsx b/src/App.jsx index 1a97892..88244e9 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -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} /> )} diff --git a/src/components/DebugView.jsx b/src/components/DebugView.jsx index 243a518..0c58167 100644 --- a/src/components/DebugView.jsx +++ b/src/components/DebugView.jsx @@ -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 (C3–B4) ─────────────────────────────────────────── +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 ( + + {/* 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 > 0.04 && ( + + )} + + {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 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 ( + + + + {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 ( -
- {/* White keys */} -
- {WHITE_PCS.map(pc => { - const name = NOTES[pc] - const energy = values[pc] / max + + {/* 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.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 ( -
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', - }} - > - {name} -
+ + {energy > 0.3 && (inChord || inKey) && ( + + )} + + + {NOTES[pc]} + + ) - })} -
- - {/* 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 ( -
0.4 - ? inChord - ? '0 0 10px rgba(139,92,246,0.5)' - : '0 0 4px rgba(255,255,255,0.08)' - : 'none', - }} - /> - ) - })} -
+ }) + )} + ) } // ─── 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

Behind the Scenes

- {/* ── Large live chroma keyboard ── */} + {/* ── Live chroma visualization (instrument-synced) ── */}

Live chroma — what the engine hears right now

- + {instrument === 'guitar' + ? + : + }
{/* ── Bottom three columns ── */} @@ -143,27 +267,10 @@ export default function DebugView({ chroma, chordCandidates, noteAnalysis, keyIn
- {/* Col 2: Note history keyboard */} + {/* Col 2: Note history piano with % labels */}

Note history — key evidence

- -
- {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 ( -
- - {pct > 0 ? `${pct}%` : ''} - - - {name} - -
- ) - })} -
+
{/* Col 3: Key candidates */} diff --git a/src/components/Piano.jsx b/src/components/Piano.jsx index 1b3ffea..17dc4a2 100644 --- a/src/components/Piano.jsx +++ b/src/components/Piano.jsx @@ -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 ( - - {k.label}{oct + 3} - + {(isChordTone || isPenta || isScale) && ( + + {k.label} + + )} ) }) @@ -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 ( - - {NOTES[k.pc]} - + {(isChordTone || isPenta || isScale) && ( + + {NOTES[k.pc]} + + )} ) }) @@ -126,7 +134,8 @@ export default function Piano({ keyInfo, currentChord }) {
Chord tone - Scale note + Pentatonic + Scale
) diff --git a/src/components/Settings.jsx b/src/components/Settings.jsx index 6d4f8e0..0ea6e49 100644 --- a/src/components/Settings.jsx +++ b/src/components/Settings.jsx @@ -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',