additions for debug view and better formatting and sound fine tuning
This commit is contained in:
@@ -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
@@ -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
@@ -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>
|
||||
)}
|
||||
|
||||
+201
-94
@@ -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 (
|
||||
<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 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 (
|
||||
<div className={`relative ${height} select-none`}>
|
||||
{/* White keys */}
|
||||
<div className="flex gap-px h-full">
|
||||
{WHITE_PCS.map(pc => {
|
||||
const name = NOTES[pc]
|
||||
const energy = values[pc] / max
|
||||
<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 */}
|
||||
|
||||
+33
-24
@@ -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,15 +79,17 @@ export default function Piano({ keyInfo, currentChord }) {
|
||||
stroke="#2a2a2a"
|
||||
strokeWidth={1}
|
||||
/>
|
||||
<text
|
||||
x={x + KEY_W / 2} y={LABEL_Y}
|
||||
textAnchor="middle"
|
||||
fontSize={9}
|
||||
fontWeight="600"
|
||||
fill={text}
|
||||
>
|
||||
{k.label}{oct + 3}
|
||||
</text>
|
||||
{(isChordTone || isPenta || isScale) && (
|
||||
<text
|
||||
x={x + KEY_W / 2} y={LABEL_Y}
|
||||
textAnchor="middle"
|
||||
fontSize={9}
|
||||
fontWeight="600"
|
||||
fill={text}
|
||||
>
|
||||
{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,15 +113,17 @@ export default function Piano({ keyInfo, currentChord }) {
|
||||
stroke="#111"
|
||||
strokeWidth={1}
|
||||
/>
|
||||
<text
|
||||
x={x + BLACK_W / 2} y={BLACK_LABEL_Y}
|
||||
textAnchor="middle"
|
||||
fontSize={8}
|
||||
fontWeight="600"
|
||||
fill={text}
|
||||
>
|
||||
{NOTES[k.pc]}
|
||||
</text>
|
||||
{(isChordTone || isPenta || isScale) && (
|
||||
<text
|
||||
x={x + BLACK_W / 2} y={BLACK_LABEL_Y}
|
||||
textAnchor="middle"
|
||||
fontSize={8}
|
||||
fontWeight="600"
|
||||
fill={text}
|
||||
>
|
||||
{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>
|
||||
)
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user