From 711b3d149f24d63be04e29b98e315782f249c899 Mon Sep 17 00:00:00 2001 From: Sterling Archer Date: Sat, 13 Jun 2026 20:32:09 -0700 Subject: [PATCH] Add chromatic guitar tuner with needle display. Add hamburger menu. --- index.html | 294 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 286 insertions(+), 8 deletions(-) diff --git a/index.html b/index.html index ee61933..b871010 100644 --- a/index.html +++ b/index.html @@ -846,8 +846,249 @@ function MicBar({ note, hz, feedback, listening, onToggle }) { ); } + +const OPEN_STRINGS = [ + {note:"E2", name:"Low E", freq:82.41, string:6}, + {note:"A2", name:"A", freq:110.00, string:5}, + {note:"D3", name:"D", freq:146.83, string:4}, + {note:"G3", name:"G", freq:196.00, string:3}, + {note:"B3", name:"B", freq:246.94, string:2}, + {note:"E4", name:"High e", freq:329.63, string:1}, +]; + +function HamburgerMenu({ onTuner, onBasics, onSongSearch, onClose }) { + return ( + <> +
+
+
+
+ GuitarTeacher +
+ +
+
+ {[ + {icon:"🎵", label:"Guitar Tuner", sub:"Tune before you play", action:onTuner}, + {icon:"📖", label:"Guitar Basics", sub:"8 lessons for beginners", action:onBasics}, + {icon:"🔍", label:"Song Search", sub:"70+ songs with tab", action:onSongSearch}, + ].map(({icon,label,sub,action})=>( + + ))} +
+
+ GuitarTeacher - Learn at your own pace +
+
+ + ); +} + + +function GuitarTuner({ onBack, onMenu }) { + const [listening, setListening] = useState(false); + const [detectedNote, setDetectedNote] = useState(""); + const [detectedFreq, setDetectedFreq] = useState(0); + const [cents, setCents] = useState(0); + const [closestString, setClosestString] = useState(null); + const ctxRef = useRef(null); + const streamRef = useRef(null); + const rafRef = useRef(null); + + const stopMic = useCallback(()=>{ + if(streamRef.current) streamRef.current.getTracks().forEach(t=>t.stop()); + if(rafRef.current) cancelAnimationFrame(rafRef.current); + if(ctxRef.current) ctxRef.current.close(); + ctxRef.current=null; streamRef.current=null; + setListening(false); setDetectedNote(""); setDetectedFreq(0); setCents(0); setClosestString(null); + },[]); + + const startMic = useCallback(async()=>{ + if(listening){ stopMic(); return; } + try { + const stream = await navigator.mediaDevices.getUserMedia({audio:true}); + const ctx = new (window.AudioContext||window.webkitAudioContext)(); + const analyser = ctx.createAnalyser(); + analyser.fftSize = 4096; + ctx.createMediaStreamSource(stream).connect(analyser); + ctxRef.current=ctx; streamRef.current=stream; + setListening(true); + const buf = new Float32Array(analyser.fftSize); + const loop = ()=>{ + rafRef.current = requestAnimationFrame(loop); + analyser.getFloatTimeDomainData(buf); + const freq = autoCorrelate(buf, ctx.sampleRate); + if(freq > 60 && freq < 1200){ + const midi = Math.round(12*Math.log2(freq/440)+69); + const name = ["C","C#","D","D#","E","F","F#","G","G#","A","A#","B"][midi%12]; + const oct = Math.floor(midi/12)-1; + const exactHz = 440*Math.pow(2,(midi-69)/12); + const c = Math.round(1200*Math.log2(freq/exactHz)); + setDetectedNote(name+oct); + setDetectedFreq(Math.round(freq*10)/10); + setCents(c); + const closest = OPEN_STRINGS.reduce((best,s)=>{ + const diff = Math.abs(freq - s.freq); + return diff < Math.abs(freq - best.freq) ? s : best; + }, OPEN_STRINGS[0]); + setClosestString(closest); + } + }; + loop(); + } catch(e){ alert("Mic access denied. Please allow microphone access and try again."); } + },[listening, stopMic]); + + useEffect(()=>()=>stopMic(),[]); + + const inTune = Math.abs(cents) <= 5; + const slightlyOff = Math.abs(cents) <= 15; + const needleAngle = Math.max(-45, Math.min(45, cents * 0.9)); + const needleColor = inTune ? C.sage : slightlyOff ? C.amber : C.coral; + const tuneMsg = inTune ? "In tune!" : cents < 0 ? "Tune up" : "Tune down"; + const tuneMsgColor = inTune ? C.sage : slightlyOff ? C.amber : C.coral; + + return ( +
+
+ +
+ GuitarTeacher + / Tuner +
+ +
+ +
+
+
+
Pluck any string and tune to the closest note
+
+ + {/* Needle meter */} +
+ + {/* Arc background */} + + {/* Colored arc zones */} + + + + + + {/* Center tick */} + + {/* Needle */} + {detectedFreq > 0 && ( + + + + + )} + {/* Center dot when no signal */} + {detectedFreq === 0 && ( + + )} + {/* Labels */} + -50 + 0 + +50 + + + {detectedFreq > 0 ? ( + <> +
{detectedNote}
+
{detectedFreq} Hz
+
{tuneMsg}
+ {!inTune && ( +
+ {Math.abs(cents)} cents {cents < 0 ? "flat" : "sharp"} +
+ )} + + ) : ( +
+ {listening ? "Pluck a string..." : "Tap the mic to start"} +
+ )} +
+ + {/* Closest string */} + {closestString && ( +
+
+
Closest string
+
+ String {closestString.string} - {closestString.name} +
+
Target: {closestString.freq} Hz
+
+
{inTune ? "✓" : cents < 0 ? "↑" : "↓"}
+
+ )} + + {/* String reference */} +
+
Standard Tuning
+
+ {OPEN_STRINGS.map(s=>{ + const isActive = closestString && closestString.note===s.note; + return ( +
+
{s.name}
+
{s.note}
+
+ ); + })} +
+
+ + {/* Mic button */} + +
+
+
+ ); +} + function App() { const [splash, setSplash] = useState(true); + const [showTuner, setShowTuner] = useState(false); + const [menuOpen, setMenuOpen] = useState(false); const [lesson, setLesson] = useState(0); const [listening, setListening] = useState(false); const [pitchNote, setPitchNote] = useState(""); @@ -914,12 +1155,27 @@ function App() { const css = "*{box-sizing:border-box;margin:0;padding:0} body{background:#1A1A1F} input::placeholder{color:#9A9490} p+p{margin-top:10px}"; + if (showTuner) return setShowTuner(false)} onMenu={()=>setMenuOpen(true)}/>; + if (splash) return (
-
- GuitarTeacher +
+
GuitarTeacher
+
+ {menuOpen && {setMenuOpen(false);setShowTuner(true);setSplash(false);}} + onBasics={()=>{setMenuOpen(false);setShowTuner(false);setSplash(false);go(0);}} + onSongSearch={()=>{setMenuOpen(false);setShowTuner(false);setSplash(false);go(7);}} + onClose={()=>setMenuOpen(false)} + />}
🎸
@@ -929,7 +1185,15 @@ function App() {

Start from the very beginning or jump straight to playing songs you love.

-
+
+
@@ -961,14 +1225,28 @@ function App() { style={{fontWeight:800,fontSize:21,background:"none",border:"none",cursor:"pointer",color:C.text,padding:0,fontFamily:"sans-serif"}}> GuitarTeacher -
- {lesson+1}/{LESSONS.length} -
-
+
+
+ {lesson+1}/{LESSONS.length} +
+
+
+
+ {menuOpen && {setMenuOpen(false);setShowTuner(true);setSplash(false);}} + onBasics={()=>{setMenuOpen(false);setShowTuner(false);setSplash(false);setLesson(0);}} + onSongSearch={()=>{setMenuOpen(false);setShowTuner(false);setSplash(false);setLesson(7);}} + onClose={()=>setMenuOpen(false)} + />}
{LESSONS.map((l,i)=>(