additions formatting

This commit is contained in:
vadimwit
2026-03-17 22:41:12 +00:00
parent dec0fc1595
commit b94f79f408
2 changed files with 206 additions and 3 deletions
+25 -3
View File
@@ -8,6 +8,7 @@ import Tuner from './components/Tuner'
import Piano from './components/Piano'
import Settings from './components/Settings'
import DebugView from './components/DebugView'
import DrumView from './components/DrumView'
import { NOTES, detectKey, detectTopKeys, matchChordFromChroma, detectRepeatingProgression, getChordTones, getChordCandidates, getNoteHistoryAnalysis } from './lib/theory'
import settingIcon from './assets/setting-icon.png'
@@ -49,7 +50,8 @@ export default function App() {
// ── Instrument view + tuner ───────────────────────────────────────────────────
const [instrument, setInstrument] = useState('piano') // 'piano' | 'guitar' | 'bass'
const [showTuner, setShowTuner] = useState(false)
const [showDebug, setShowDebug] = useState(false)
const [showDebug, setShowDebug] = useState(false)
const [showDrumView, setShowDrumView] = useState(false)
const [monoColor, setMonoColor] = useState(() => loadStored('wtf_monoColor', false))
// ── Mic permission error ──────────────────────────────────────────────────────
@@ -63,9 +65,11 @@ export default function App() {
// ── Stable refs for values used inside callbacks ──────────────────────────────
const showDebugRef = useRef(showDebug)
const showDrumViewRef = useRef(showDrumView)
const lockedKeyRef = useRef(null)
const listenStartRef = useRef(null)
useEffect(() => { showDebugRef.current = showDebug }, [showDebug])
useEffect(() => { showDrumViewRef.current = showDrumView }, [showDrumView])
useEffect(() => { localStorage.setItem('wtf_monoColor', JSON.stringify(monoColor)) }, [monoColor])
useEffect(() => { if (isListening) listenStartRef.current = Date.now() }, [isListening])
@@ -184,9 +188,11 @@ export default function App() {
effectiveKeyRef.current = keyInfo
}
// ── Waveform handler: feeds oscilloscope in debug view ───────────────────────
// ── Waveform handler: feeds oscilloscope / drum view ─────────────────────────
const handleWaveform = useCallback((data) => {
if (showDebugRef.current) setDebugWaveform(data)
if (showDebugRef.current || showDrumViewRef.current) {
setDebugWaveform({ ...data, onsets: [...onsetTimestampsRef.current] })
}
}, [])
// ── Note handler: drives key detection (pitch-based) ──────────────────────────
@@ -569,6 +575,22 @@ export default function App() {
)}
</div>
{/* ── Rhythm / drum analyser — collapsible ── */}
<div className="mb-3 bg-panel border border-border rounded-xl overflow-hidden">
<button
onClick={() => setShowDrumView(v => !v)}
className="w-full flex items-center justify-between px-4 py-2 text-sm text-gray-400 hover:text-gray-200 transition-all"
>
<span>RHYTHM ANALYSER</span>
<span>{showDrumView ? '▲' : '▼'}</span>
</button>
{showDrumView && (
<div className="border-t border-border p-4">
<DrumView waveform={debugWaveform} bpm={bpm} />
</div>
)}
</div>
{/* ── Tuner — collapsible ── */}
<div className="bg-panel border border-border rounded-xl overflow-hidden">
<button
+181
View File
@@ -0,0 +1,181 @@
import { useRef, useEffect } from 'react'
// Map a frequency (Hz) to a bin index in the 256-bin log spectrum (404000 Hz)
function freqToBin(freq) {
return Math.round(255 * Math.log(freq / 40) / Math.log(4000 / 40))
}
function bandMax(spectrum, lo, hi) {
if (!spectrum) return 0
const a = freqToBin(lo)
const b = Math.min(freqToBin(hi), spectrum.length - 1)
let max = 0
for (let i = a; i <= b; i++) if (spectrum[i] > max) max = spectrum[i]
return max
}
const BANDS = [
{ label: 'Kick', lo: 40, hi: 120, color: '#ef4444' },
{ label: 'Snare', lo: 120, hi: 300, color: '#f59e0b' },
{ label: 'Mid', lo: 300, hi: 1000, color: '#22c55e' },
{ label: 'Presence', lo: 1000, hi: 4000, color: '#60a5fa' },
]
const TIMELINE_MS = 4000 // onset timeline window
const RMS_HISTORY = 180 // ~3s at 60fps
export default function DrumView({ waveform, bpm }) {
const rmsHistRef = useRef([])
const beatCanvasRef = useRef(null)
const rmsCanvasRef = useRef(null)
const spectrum = waveform?.spectrum ?? null
const bandLevels = BANDS.map(b => bandMax(spectrum, b.lo, b.hi))
// Accumulate RMS history
useEffect(() => {
if (waveform == null) return
const h = rmsHistRef.current
h.push(Math.min(waveform.rms * 10, 1))
if (h.length > RMS_HISTORY) h.shift()
}, [waveform])
// Draw onset / beat timeline
useEffect(() => {
const canvas = beatCanvasRef.current
if (!canvas) return
const ctx = canvas.getContext('2d')
const W = canvas.width, H = canvas.height
ctx.fillStyle = '#0a0a0a'
ctx.fillRect(0, 0, W, H)
const onsets = waveform?.onsets ?? []
const now = performance.now()
// Beat grid aligned to the most recent onset
if (bpm) {
const beatMs = 60000 / bpm
const numBeats = Math.ceil(TIMELINE_MS / beatMs) + 1
const latest = onsets[onsets.length - 1]
const phase = latest != null ? (now - latest) % beatMs : 0
for (let b = 0; b <= numBeats; b++) {
const ageMs = b * beatMs - phase
if (ageMs < 0 || ageMs > TIMELINE_MS) continue
const x = W * (1 - ageMs / TIMELINE_MS)
ctx.strokeStyle = 'rgba(255,255,255,0.07)'
ctx.lineWidth = 1
ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, H); ctx.stroke()
}
}
// Onset dots + vertical tails
const recent = onsets.filter(t => now - t <= TIMELINE_MS)
for (const t of recent) {
const age = now - t
const x = W * (1 - age / TIMELINE_MS)
const alpha = Math.pow(1 - age / TIMELINE_MS, 0.4)
ctx.strokeStyle = `rgba(168,85,247,${(alpha * 0.35).toFixed(2)})`
ctx.lineWidth = 1
ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, H); ctx.stroke()
ctx.fillStyle = `rgba(168,85,247,${alpha.toFixed(2)})`
ctx.beginPath(); ctx.arc(x, H / 2, 5, 0, Math.PI * 2); ctx.fill()
}
// "Now" edge
ctx.strokeStyle = 'rgba(255,255,255,0.18)'
ctx.lineWidth = 2
ctx.beginPath(); ctx.moveTo(W - 1, 0); ctx.lineTo(W - 1, H); ctx.stroke()
}, [waveform, bpm])
// Draw RMS envelope
useEffect(() => {
const canvas = rmsCanvasRef.current
if (!canvas) return
const ctx = canvas.getContext('2d')
const W = canvas.width, H = canvas.height
const h = rmsHistRef.current
ctx.fillStyle = '#0a0a0a'
ctx.fillRect(0, 0, W, H)
if (h.length < 2) return
const barW = W / RMS_HISTORY
for (let i = 0; i < h.length; i++) {
const x = W * (i / RMS_HISTORY)
const barH = h[i] * H
const v = Math.round(h[i] * 160 + 60)
ctx.fillStyle = `rgb(${v},30,${v})`
ctx.fillRect(x, H - barH, Math.max(barW - 0.5, 1), barH)
}
}, [waveform])
const noData = !waveform
return (
<div className="space-y-5">
{/* Band meters */}
<div>
<p className="text-xs text-gray-600 font-mono uppercase tracking-widest mb-2">Frequency Bands</p>
<div className="flex gap-3" style={{ height: 96 }}>
{BANDS.map((b, i) => (
<div key={b.label} className="flex flex-col items-center gap-1 flex-1">
<div className="flex-1 w-full bg-gray-900 rounded-sm relative overflow-hidden">
{noData ? (
<div className="absolute inset-0 flex items-center justify-center">
<span className="text-[9px] text-gray-700 font-mono"></span>
</div>
) : (
<div
className="absolute bottom-0 left-0 right-0 rounded-sm"
style={{
height: `${bandLevels[i] * 100}%`,
backgroundColor: b.color,
transition: 'height 60ms linear',
}}
/>
)}
</div>
<span className="text-[10px] font-mono text-gray-500 uppercase">{b.label}</span>
</div>
))}
</div>
</div>
{/* Onset timeline */}
<div>
<p className="text-xs text-gray-600 font-mono uppercase tracking-widest mb-2">
Onset Timeline{bpm ? ` · ${bpm} BPM` : ''}
<span className="ml-2 text-gray-700 normal-case"> 4 seconds</span>
</p>
<canvas
ref={beatCanvasRef}
width={800}
height={56}
className="w-full rounded"
style={{ height: 56 }}
/>
{noData && (
<p className="text-xs text-gray-700 font-mono mt-1 text-center">Start listening to see hits</p>
)}
</div>
{/* Volume envelope */}
<div>
<p className="text-xs text-gray-600 font-mono uppercase tracking-widest mb-2">
Volume Envelope
<span className="ml-2 text-gray-700 normal-case"> ~3 seconds</span>
</p>
<canvas
ref={rmsCanvasRef}
width={800}
height={56}
className="w-full rounded"
style={{ height: 56 }}
/>
</div>
</div>
)
}