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 (
+
+ )
+}
+
+// ─── 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
+
-
- {/* 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',