1660 lines
114 KiB
HTML
1660 lines
114 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8"/>
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||
<title>GuitarTeacher</title>
|
||
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
|
||
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
|
||
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/7.23.2/babel.min.js"></script>
|
||
<style>* { box-sizing: border-box; margin: 0; padding: 0; } body { background: #1A1A1F; font-family: sans-serif; } input::placeholder { color: #9A9490; } textarea::placeholder { color: #9A9490; } p + p { margin-top: 10px; }</style>
|
||
</head>
|
||
<body>
|
||
<div id="root"></div>
|
||
<script type="text/babel">
|
||
const { useState, useEffect, useRef, useCallback } = React;
|
||
|
||
|
||
const C = {
|
||
bg:"#1A1A1F", bg2:"#22222A", bg3:"#2A2A35",
|
||
amber:"#D4A847", sage:"#6B8F6E", coral:"#E05C4B",
|
||
text:"#F0EDE6", muted:"#9A9490", string:"#8A8070", fret:"#3A3A48",
|
||
};
|
||
|
||
const NOTE_NAMES = ["C","C#","D","D#","E","F","F#","G","G#","A","A#","B"];
|
||
|
||
function freqToNote(freq) {
|
||
if (freq < 60 || freq > 1200) return null;
|
||
const midi = Math.round(12 * Math.log2(freq / 440) + 69);
|
||
const name = NOTE_NAMES[midi % 12];
|
||
const octave = Math.floor(midi / 12) - 1;
|
||
const hz = 440 * Math.pow(2, (midi - 69) / 12);
|
||
const cents = Math.round(1200 * Math.log2(freq / hz));
|
||
return { name: name + octave, cents };
|
||
}
|
||
|
||
function autoCorrelate(buf, sr) {
|
||
const N = buf.length, MAX = Math.floor(N / 2);
|
||
let rms = 0;
|
||
for (let i = 0; i < N; i++) rms += buf[i] * buf[i];
|
||
if (Math.sqrt(rms / N) < 0.01) return -1;
|
||
let best = -1, bestC = 0, lastC = 1;
|
||
for (let o = 0; o < MAX; o++) {
|
||
let c = 0;
|
||
for (let i = 0; i < MAX; i++) c += Math.abs(buf[i] - buf[i + o]);
|
||
c = 1 - c / MAX;
|
||
if (c > 0.9 && c > lastC && c > bestC) { bestC = c; best = o; }
|
||
lastC = c;
|
||
}
|
||
return best > 0 && bestC > 0.01 ? sr / best : -1;
|
||
}
|
||
|
||
const CHORDS = {
|
||
Em:{ dots:[{s:2,f:2,n:"2"},{s:3,f:2,n:"3"}], open:[0,1,4,5], muted:[], label:"0 0 2 2 0 0" },
|
||
Am:{ dots:[{s:2,f:2,n:"2"},{s:3,f:2,n:"3"},{s:4,f:2,n:"1"}], open:[1,5], muted:[0], label:"x 0 2 2 1 0" },
|
||
G: { dots:[{s:0,f:3,n:"2"},{s:1,f:2,n:"1"},{s:4,f:3,n:"3"},{s:5,f:3,n:"4"}], open:[2,3], muted:[], label:"3 2 0 0 3 3" },
|
||
C: { dots:[{s:1,f:3,n:"3"},{s:2,f:2,n:"2"},{s:4,f:1,n:"1"}], open:[3,5], muted:[0], label:"x 3 2 0 1 0" },
|
||
D: { dots:[{s:3,f:2,n:"1"},{s:4,f:3,n:"3"},{s:5,f:2,n:"2"}], open:[2], muted:[0,1], label:"x x 0 2 3 2" },
|
||
};
|
||
|
||
const SONG_LIBRARY = [
|
||
{"title":"Knockin on Heavens Door","artist":"Bob Dylan","genre":"Classic Rock","diff":"Beginner","chords":["G","D","Am"],"parts":[{"name":"Verse","tab":{"e":"--3---2---0---","B":"--3---3---1---","G":"--0---2---2---","D":"--0---0---2---","A":"--2-------0---","E":"3---2---------"},"notes":["G4","D3","A2","E4"]}],"tips":"4/4 strum pattern. G and D are the main two chords \u2014 Am only comes in on the second half."},
|
||
{"title":"Wish You Were Here","artist":"Pink Floyd","genre":"Classic Rock","diff":"Intermediate","chords":["G","C","D","Am"],"parts":[{"name":"Intro Riff","tab":{"e":"----------3-5-3-0-","B":"------5-3---------","G":"--5-4-------------","D":"5-----------------","A":"------------------","E":"------------------"},"notes":["D3","A2","G4","B3","E4"]},{"name":"Verse","tab":{"e":"--3---0---2---3---","B":"--3---1---3---3---","G":"--0---0---2---0---","D":"--0---2---0---0---","A":"--2---3-------2---","E":"3---------2---3---"},"notes":["G4","E4","D3","G3"]}],"tips":"The intro riff is what everyone recognizes. Learn it slowly on the top 3 strings first."},
|
||
{"title":"Sweet Home Alabama","artist":"Lynyrd Skynyrd","genre":"Classic Rock","diff":"Intermediate","chords":["D","C","G"],"parts":[{"name":"Main Riff","tab":{"e":"--2---0---3---2---","B":"--3---1---3---3---","G":"--2---0---0---2---","D":"--0---2---0---0---","A":"------3---2-------","E":"----------3---2---"},"notes":["D3","C4","G4","D3"]}],"tips":"The D-C-G three-chord slide is the whole song. Keep strumming rhythmic and loose."},
|
||
{"title":"Brown Eyed Girl","artist":"Van Morrison","genre":"Classic Rock","diff":"Beginner","chords":["G","C","D","Em"],"parts":[{"name":"Verse / Chorus","tab":{"e":"--3---0---2---0---","B":"--3---1---3---0---","G":"--0---0---2---0---","D":"--0---2---0---2---","A":"--2---3-------2---","E":"3---------2---0---"},"notes":["G4","C4","D3","E3"]}],"tips":"Shuffle rhythm, slightly swung strumming. Goes G-C-G-D then G-C-Em-D for chorus."},
|
||
{"title":"Wonderful Tonight","artist":"Eric Clapton","genre":"Classic Rock","diff":"Beginner","chords":["G","D","C","Em"],"parts":[{"name":"Intro Melody","tab":{"e":"--3---5---3---2---0-------","B":"--3-----------3---3---1---","G":"--0---0---0---2---0---0---","D":"--0---0---0---0---0---2---","A":"--2-----------0---2---3---","E":"3---3---3---------0-------"},"notes":["G4","A4","G4","D3","E4","C4"]}],"tips":"Fingerpick or very gentle strumming. The melody on the high strings is what makes this song."},
|
||
{"title":"House of the Rising Sun","artist":"The Animals","genre":"Classic Rock","diff":"Intermediate","chords":["Am","C","D","F","E"],"parts":[{"name":"Intro Arpeggio","tab":{"e":"0---1---0---1---0---","B":"1---1---3---1---0---","G":"2---0---2---2---1---","D":"2---2---0---3---2---","A":"0---3-------3---2---","E":"--------1---1---0---"},"notes":["A2","C4","D3","F3","E2"]}],"tips":"Fingerpick each note one at a time \u2014 this is an arpeggio pattern. Very satisfying once you have it."},
|
||
{"title":"More Than Words","artist":"Extreme","genre":"Classic Rock","diff":"Intermediate","chords":["G","Cadd9","Am","D","Em"],"parts":[{"name":"Intro","tab":{"e":"3---3---0---0---0---","B":"3---3---1---3---0---","G":"0---0---0---2---0---","D":"0---2---2---0---2---","A":"2---3---3-------2---","E":"3---0---0-------0---"},"notes":["G4","G4","C4","D3","E3"]}],"tips":"Fingerpick or hybrid pick. Let notes ring into each other. The beauty is in letting chords overlap."},
|
||
{"title":"Wonderwall","artist":"Oasis","genre":"Pop/Rock","diff":"Intermediate","chords":["Em7","G","Dsus4","A7sus4"],"parts":[{"name":"Intro / Verse","tab":{"e":"3-3-3-3-3-3-3-3-","B":"3-3-3-3-3-3-3-3-","G":"0-0-0-0-0-0-0-0-","D":"0-0-2-2-0-0-2-2-","A":"2-2-3-3-2-2-3-3-","E":"0-0-0-0-3-3-0-0-"},"notes":["G4","B3","G3","D3","A2"]}],"tips":"Capo 2 for original key. The Em7 and A7sus4 shapes are unique to this song but easy once you know them."},
|
||
{"title":"Let Her Go","artist":"Passenger","genre":"Pop/Rock","diff":"Beginner","chords":["G","D","Em","C"],"parts":[{"name":"Intro Picking","tab":{"e":"3---2---0---0---3---","B":"3---3---0---1---3---","G":"0---2---0---0---0---","D":"0---0---2---2---0---","A":"2-------2---3---2---","E":"3-----------0---3---"},"notes":["G4","D3","E3","C4","G4"]}],"tips":"Fingerpick the intro, strum the chorus. G-D-Em-C is one of the most common progressions in pop."},
|
||
{"title":"Thinking Out Loud","artist":"Ed Sheeran","genre":"Pop","diff":"Intermediate","chords":["D","G","A","Em"],"parts":[{"name":"Verse","tab":{"e":"--2---3---0---0---","B":"--3---3---2---0---","G":"--2---0---2---0---","D":"--0---0---2---2---","A":"------2---0---2---","E":"--2---3-------0---"},"notes":["D3","G4","A2","E3"]}],"tips":"Slow 12/8 feel. Each chord gets two slow downstrokes. The key is keeping it smooth and unhurried."},
|
||
{"title":"Riptide","artist":"Vance Joy","genre":"Pop","diff":"Beginner","chords":["Am","G","C"],"parts":[{"name":"Verse / Chorus","tab":{"e":"0---3---0---","B":"1---3---1---","G":"2---0---0---","D":"2---0---2---","A":"0---2---3---","E":"----3-------"},"notes":["A2","G4","C4","E3"]}],"tips":"Just three chords the whole song. Capo 1 for original key. Down-down-up-up-down-up strumming."},
|
||
{"title":"Stay With Me","artist":"Sam Smith","genre":"Pop","diff":"Beginner","chords":["Am","F","C"],"parts":[{"name":"Verse / Chorus","tab":{"e":"0---1---0---","B":"1---1---1---","G":"2---2---0---","D":"2---3---2---","A":"0---3---3---","E":"----1-------"},"notes":["A2","F3","C4","E3"]}],"tips":"Three chords, simple strumming. Great beginner pop song."},
|
||
{"title":"Shallow","artist":"Lady Gaga and Bradley Cooper","genre":"Pop","diff":"Intermediate","chords":["Em","D","G","C","Am"],"parts":[{"name":"Intro Picking","tab":{"e":"0---2---3---0---","B":"0---3---3---1---","G":"0---2---0---0---","D":"2---0---0---2---","A":"2-------2---3---","E":"0-----------0---"},"notes":["E3","D3","G4","C4","A2"]},{"name":"Chorus","tab":{"e":"--0---3---3---0---","B":"--0---3---0---1---","G":"--0---0---0---0---","D":"--2---0---0---2---","A":"--2---2---2---3---","E":"--0---3---3---0---"},"notes":["E3","G4","D3","C4"]}],"tips":"The intro uses a picked Em-D-G pattern. Chorus strums. Capo 5 for original key."},
|
||
{"title":"Horse With No Name","artist":"America","genre":"Rock","diff":"Beginner","chords":["Em","D6"],"parts":[{"name":"Main Riff","tab":{"e":"0-2-0-2-0-2-0-2-","B":"0-3-0-3-0-3-0-3-","G":"0-2-0-2-0-2-0-2-","D":"2-0-2-0-2-0-2-0-","A":"2-0-2-0-2-0-2-0-","E":"0---------------"},"notes":["E4","B3","G3","E3"]}],"tips":"Only two chord shapes the whole song. Great for getting comfortable with the Em shape."},
|
||
{"title":"Creep","artist":"Radiohead","genre":"Rock","diff":"Beginner","chords":["G","B","C","Cm"],"parts":[{"name":"Verse / Chorus","tab":{"e":"3---2---0---0---","B":"3---2---1---0---","G":"0---4---0---0---","D":"0---4---2---1---","A":"2---2---3---3---","E":"3---2---0---3---"},"notes":["G4","B2","C4","C3"]}],"tips":"The Cm is the secret ingredient \u2014 it gives the song its brooding feel. Strum hard on the chorus."},
|
||
{"title":"Come As You Are","artist":"Nirvana","genre":"Rock","diff":"Beginner","chords":["Em","Am","C"],"parts":[{"name":"Main Riff","tab":{"e":"0---0---0---1---","B":"0---0---0---1---","G":"0---0---0---0---","D":"2---2---1---2---","A":"2---1---3---3---","E":"0---0---0---1---"},"notes":["E4","E3","A2","F3"]}],"tips":"Slow, deliberate picking. Let each note ring. Capo 1 for original key."},
|
||
{"title":"Seven Nation Army","artist":"The White Stripes","genre":"Rock","diff":"Beginner","chords":[],"parts":[{"name":"Main Riff","tab":{"e":"----------------","B":"----------------","G":"----------------","D":"----------------","A":"7-7-10-7-5-3-2--","E":"----------------"},"notes":["E2","E2","A2","E2","D2","B1","A1"]}],"tips":"Single string riff on the A string. Perfect first riff for beginners. Memorize the fret numbers: 7-7-10-7-5-3-2."},
|
||
{"title":"Smoke on the Water","artist":"Deep Purple","genre":"Rock","diff":"Beginner","chords":[],"parts":[{"name":"Main Riff","tab":{"e":"------------------","B":"------------------","G":"0-3-5-0-3-6-5-----","D":"0-3-5-0-3-6-5-----","A":"------------------","E":"------------------"},"notes":["G3","A3","C4","G3","A3","D4","C4"]}],"tips":"Two strings at once on G and D. This is one of the most famous riffs in rock history."},
|
||
{"title":"Hotel California","artist":"Eagles","genre":"Rock","diff":"Advanced","chords":["Am","E","G","D","F","C","Dm"],"parts":[{"name":"Intro Arpeggio","tab":{"e":"0---0---3---2---1---0---2---0---","B":"1---0---0---3---1---1---3---1---","G":"2---1---0---2---2---0---2---2---","D":"2---2---0---0---3---2---0---2---","A":"0---2---2---0---3---3---0---0---","E":"----0---3---2---1---0---2-------"},"notes":["A2","E2","G4","D3","F3","C4","Dm3"]}],"tips":"This intro arpeggio is advanced but iconic. Learn it chord by chord, then connect them slowly."},
|
||
{"title":"Fast Car","artist":"Tracy Chapman","genre":"Folk/Indie","diff":"Beginner","chords":["C","G","Am","F"],"parts":[{"name":"Verse / Chorus","tab":{"e":"0---3---0---1---","B":"1---3---1---1---","G":"0---0---2---2---","D":"2---0---2---3---","A":"3---2---0---3---","E":"----3-------1---"},"notes":["C4","G4","A2","F3"]}],"tips":"Capo 2 for original key. Simple four-chord loop. Great song to practice steady rhythm."},
|
||
{"title":"Ho Hey","artist":"The Lumineers","genre":"Folk/Indie","diff":"Beginner","chords":["C","F","G","Am"],"parts":[{"name":"Verse / Chorus","tab":{"e":"0---1---3---0---","B":"1---1---3---1---","G":"0---2---0---2---","D":"2---3---0---2---","A":"3---3---2---0---","E":"----1---3-------"},"notes":["C4","F3","G4","A2"]}],"tips":"Stomp and clap rhythm. The F chord is the trickiest part for beginners \u2014 use a simplified F (x33210)."},
|
||
{"title":"Banana Pancakes","artist":"Jack Johnson","genre":"Folk/Indie","diff":"Beginner","chords":["G","C","D","E7"],"parts":[{"name":"Verse","tab":{"e":"3---0---2---0---","B":"3---1---3---3---","G":"0---0---2---1---","D":"0---2---0---0---","A":"2---3-------2---","E":"3---0---2---0---"},"notes":["G4","C4","D3","E3"]}],"tips":"Mellow fingerpicking groove. The E7 adds a bluesy twist. Very relaxed tempo."},
|
||
{"title":"Blackbird","artist":"The Beatles","genre":"Folk/Indie","diff":"Intermediate","chords":["G","Am","C","D"],"parts":[{"name":"Intro","tab":{"e":"0-2-3-2-0-3-2-0-","B":"3-----------3---","G":"0-----------0---","D":"0-----------0---","A":"2-----------2---","E":"3-----------3---"},"notes":["G4","A4","B4","A4","G4","B4","A4","G4"]}],"tips":"Fingerstyle \u2014 thumb plays bass, fingers pick melody simultaneously. One of the most beautiful guitar pieces."},
|
||
{"title":"Iris","artist":"Goo Goo Dolls","genre":"Rock","diff":"Intermediate","chords":["Bm","Bm/A","G","D","Em"],"capo":5,"parts":[{"name":"Intro / Verse","tab":{"e":"7---7---7---7---7---7---","B":"8---8---7---7---8---8---","G":"7---7---7---7---7---7---","D":"9---9---0---0---9---9---","A":"0---0---0---0---0---0---","E":"------------------------"},"notes":["B3","D4","G4","D4","B3","E4"]},{"name":"Chorus","tab":{"e":"7---7---7---10--10--7---","B":"8---8---8---10--10--8---","G":"7---7---7---11--11--7---","D":"9---9---0---0---0---0---","A":"0---0---0---0---0---0---","E":"------------------------"},"notes":["B3","G4","D4","A4","G4"]}],"tips":"Capo 5. Uses an open D tuning on top strings. The signature sound is the droning open A and D strings ringing behind the chord shapes."},
|
||
{"title":"Girls","artist":"Beastie Boys","genre":"Rock","diff":"Beginner","chords":[],"capo":0,"parts":[{"name":"Main Riff","tab":{"e":"--------------------","B":"--------------------","G":"--------------------","D":"--5-5-5-3-----------","A":"--3-3-3-1-5-5-5-3---","E":"----------3-3-3-1---"},"notes":["A2","A2","G2","C3","C3","A2"]},{"name":"Verse Groove","tab":{"e":"--------------------","B":"--------------------","G":"--------------------","D":"--5-5---3-3---------","A":"--3-3---1-1-5-5-3-3-","E":"--------0-0-3-3-1-1-"},"notes":["A2","G2","C3","A2","G2"]}],"tips":"Mostly a bass-driven song \u2014 these riffs work great on the low strings. Keep it tight and punchy, short notes not long ones."},
|
||
{"title":"Whiskey Glasses","artist":"Morgan Wallen","genre":"Country","diff":"Beginner","chords":["G","C","D","Em"],"capo":0,"parts":[{"name":"Verse","tab":{"e":"--3---3---0---0---","B":"--3---3---1---0---","G":"--0---0---0---0---","D":"--0---0---2---2---","A":"--2---2---3---2---","E":"3---3---0---0-----"},"notes":["G4","G4","C4","E3"]},{"name":"Chorus","tab":{"e":"--3---0---2---0---","B":"--3---1---3---0---","G":"--0---0---2---0---","D":"--0---2---0---2---","A":"--2---3-------2---","E":"3---------2---0---"},"notes":["G4","C4","D3","E3"]}],"tips":"Capo 2 for original key. Down-strum on beats 1 and 3, add up-strums on 2 and 4."},
|
||
{"title":"Cowgirls","artist":"Morgan Wallen ft. ERNEST","genre":"Country","diff":"Intermediate","chords":["G","C","D","Em"],"capo":2,"parts":[{"name":"Intro / Main Riff","tab":{"e":"--3---3---3---3---","B":"--3---3---3---3---","G":"--0---0---0---0---","D":"--0---2---0---2---","A":"--2---3---2---3---","E":"3-----------3-----"},"notes":["G4","B3","G3","D3"]},{"name":"Chorus","tab":{"e":"--3---0---2---3---","B":"--3---1---3---3---","G":"--0---0---2---0---","D":"--0---2---0---0---","A":"--2---3-------2---","E":"3---------2---3---"},"notes":["G4","C4","D3","G4"]}],"tips":"Capo 2. The G chord shape anchors the whole song. Focus on smooth G to C transitions."},
|
||
{"title":"7 Summers","artist":"Morgan Wallen","genre":"Country","diff":"Beginner","chords":["G","D","Em","C"],"capo":0,"parts":[{"name":"Verse","tab":{"e":"--3---2---0---0---","B":"--3---3---0---1---","G":"--0---2---0---0---","D":"--0---0---2---2---","A":"--2-------2---3---","E":"3---2---0---------"},"notes":["G4","D3","E3","C4"]},{"name":"Chorus","tab":{"e":"--3---2---0---0---","B":"--3---3---0---1---","G":"--0---2---0---0---","D":"--0---0---2---2---","A":"--2-------2---3---","E":"3---2---0---------"},"notes":["G4","D3","E3","C4"]}],"tips":"Classic G-D-Em-C four-chord loop. Slow ballad tempo, let the chords ring out fully."},
|
||
{"title":"Sand In My Boots","artist":"Morgan Wallen","genre":"Country","diff":"Beginner","chords":["G","C","D"],"capo":2,"parts":[{"name":"Verse / Chorus","tab":{"e":"--3---0---2---3---","B":"--3---1---3---3---","G":"--0---0---2---0---","D":"--0---2---0---0---","A":"--2---3-------2---","E":"3---------2---3---"},"notes":["G4","C4","D3","G4"]}],"tips":"Capo 2. Three chords the whole song. Perfect slow strum practice."},
|
||
{"title":"Wasted On You","artist":"Morgan Wallen","genre":"Country","diff":"Beginner","chords":["Em","C","G","D"],"capo":0,"parts":[{"name":"Verse","tab":{"e":"--0---0---3---2---","B":"--0---1---3---3---","G":"--0---0---0---2---","D":"--2---2---0---0---","A":"--2---3---2-------","E":"0---------3---2---"},"notes":["E3","C4","G4","D3"]},{"name":"Chorus","tab":{"e":"--0---0---3---2---","B":"--0---1---3---3---","G":"--0---0---0---2---","D":"--2---2---0---0---","A":"--2---3---2-------","E":"0---------3---2---"},"notes":["E3","C4","G4","D3"]}],"tips":"Em-C-G-D repeating loop. Fingerpick the verse, strum the chorus for dynamics."},
|
||
{"title":"More Than My Hometown","artist":"Morgan Wallen","genre":"Country","diff":"Beginner","chords":["G","C","D","Em"],"capo":2,"parts":[{"name":"Verse","tab":{"e":"--3---0---3---0---","B":"--3---1---3---0---","G":"--0---0---0---0---","D":"--0---2---0---2---","A":"--2---3---2---2---","E":"3---0---3---0-----"},"notes":["G4","C4","G4","E3"]},{"name":"Chorus","tab":{"e":"--3---2---0---0---","B":"--3---3---0---1---","G":"--0---2---0---0---","D":"--0---0---2---2---","A":"--2-------2---3---","E":"3---2---0---------"},"notes":["G4","D3","E3","C4"]}],"tips":"Capo 2 for original key. The verse alternates G and C. Chorus breaks into the four-chord loop."},
|
||
{"title":"Last Night","artist":"Morgan Wallen","genre":"Country","diff":"Intermediate","chords":["Am","F","C","G"],"capo":0,"parts":[{"name":"Verse","tab":{"e":"--0---1---0---3---","B":"--1---1---1---3---","G":"--2---2---0---0---","D":"--2---3---2---0---","A":"--0---3---3---2---","E":"----1---0---3-----"},"notes":["A2","F3","C4","G4"]},{"name":"Chorus","tab":{"e":"--0---1---0---3---","B":"--1---1---1---3---","G":"--2---2---0---0---","D":"--2---3---2---0---","A":"--0---3---3---2---","E":"----1---0---3-----"},"notes":["A2","F3","C4","G4"]}],"tips":"Am-F-C-G is one of pop-country's most common progressions. Driving eighth-note strum pattern."},
|
||
{"title":"You Proof","artist":"Morgan Wallen","genre":"Country","diff":"Beginner","chords":["G","C","D"],"capo":0,"parts":[{"name":"Verse / Chorus","tab":{"e":"--3---0---2---3---","B":"--3---1---3---3---","G":"--0---0---2---0---","D":"--0---2---0---0---","A":"--2---3-------2---","E":"3---------2---3---"},"notes":["G4","C4","D3","G4"]}],"tips":"Simple G-C-D country strum. Great song for practicing steady down-up rhythm."},
|
||
{"title":"One Thing At A Time","artist":"Morgan Wallen","genre":"Country","diff":"Beginner","chords":["G","C","D","Em"],"capo":2,"parts":[{"name":"Verse","tab":{"e":"--3---3---0---0---","B":"--3---3---1---0---","G":"--0---0---0---0---","D":"--0---0---2---2---","A":"--2---2---3---2---","E":"3---3---0---0-----"},"notes":["G4","G4","C4","E3"]},{"name":"Chorus","tab":{"e":"--3---2---0---0---","B":"--3---3---0---1---","G":"--0---2---0---0---","D":"--0---0---2---2---","A":"--2-------2---3---","E":"3---2---0---------"},"notes":["G4","D3","E3","C4"]}],"tips":"Capo 2. Title track from his biggest album. The verse sits on G and C before the chorus opens up."},
|
||
{"title":"Thought You Should Know","artist":"Morgan Wallen","genre":"Country","diff":"Beginner","chords":["G","C","D"],"capo":2,"parts":[{"name":"Verse / Chorus","tab":{"e":"--3---0---2---","B":"--3---1---3---","G":"--0---0---2---","D":"--0---2---0---","A":"--2---3-------","E":"3---------2---"},"notes":["G4","C4","D3"]}],"tips":"Capo 2. A heartfelt ballad. Let each chord breathe with slow, gentle strumming."},
|
||
{"title":"Chasin You","artist":"Morgan Wallen","genre":"Country","diff":"Beginner","chords":["G","C","D","Em"],"capo":0,"parts":[{"name":"Verse","tab":{"e":"--3---0---2---0---","B":"--3---1---3---0---","G":"--0---0---2---0---","D":"--0---2---0---2---","A":"--2---3-------2---","E":"3---------2---0---"},"notes":["G4","C4","D3","E3"]}],"tips":"Smooth fingerpicking or light strumming works great here. Very singable and beginner-friendly."},
|
||
{"title":"Neon Eyes","artist":"Morgan Wallen","genre":"Country","diff":"Intermediate","chords":["G","D","Em","C"],"capo":0,"parts":[{"name":"Intro / Verse","tab":{"e":"--3---2---0---0---","B":"--3---3---0---1---","G":"--0---2---0---0---","D":"--0---0---2---2---","A":"--2-------2---3---","E":"3---2---0---------"},"notes":["G4","D3","E3","C4"]}],"tips":"From his debut album. The four-chord loop drives the whole song at a mid-tempo groove."},
|
||
{"title":"Burnin It Down","artist":"Jason Aldean","genre":"Country","diff":"Beginner","chords":["G","Em","C","D"],"capo":0,"parts":[{"name":"Verse","tab":{"e":"--3---0---0---2---","B":"--3---0---1---3---","G":"--0---0---0---2---","D":"--0---2---2---0---","A":"--2---2---3-------","E":"3---0---0---2-----"},"notes":["G4","E3","C4","D3"]},{"name":"Chorus","tab":{"e":"--3---0---0---2---","B":"--3---0---1---3---","G":"--0---0---0---2---","D":"--0---2---2---0---","A":"--2---2---3-------","E":"3---0---0---2-----"},"notes":["G4","E3","C4","D3"]}],"tips":"Slow burn country. Let each chord ring. The G-Em-C-D loop runs the whole song."},
|
||
{"title":"Love Somebody","artist":"Morgan Wallen","genre":"Country","diff":"Beginner","chords":["G","C","D","Em"],"capo":2,"parts":[{"name":"Verse","tab":{"e":"--3---0---3---0---","B":"--3---1---3---0---","G":"--0---0---0---0---","D":"--0---2---0---2---","A":"--2---3---2---2---","E":"3---0---3---0-----"},"notes":["G4","C4","G4","E3"]},{"name":"Chorus","tab":{"e":"--3---2---0---0---","B":"--3---3---1---0---","G":"--0---2---0---0---","D":"--0---0---2---2---","A":"--2-------3---2---","E":"3---2---0---0-----"},"notes":["G4","D3","C4","E3"]}],"tips":"Capo 2. From One Thing At A Time. Midtempo strum, gentle and smooth."},
|
||
{"title":"One Man Band","artist":"Old Dominion","genre":"Country","diff":"Beginner","chords":["G","C","D","Em"],"capo":0,"parts":[{"name":"Verse","tab":{"e":"--3---0---0---2---","B":"--3---1---0---3---","G":"--0---0---0---2---","D":"--0---2---2---0---","A":"--2---3---2-------","E":"3---0---0---2-----"},"notes":["G4","C4","E3","D3"]},{"name":"Chorus","tab":{"e":"--3---2---0---0---","B":"--3---3---0---1---","G":"--0---2---0---0---","D":"--0---0---2---2---","A":"--2-------2---3---","E":"3---2---0---------"},"notes":["G4","D3","E3","C4"]}],"tips":"Capo 2 for original key. Smooth country pop. Alternate between fingerpicking the verse and strumming the chorus."},
|
||
{"title":"Your Man","artist":"Josh Turner","genre":"Country","diff":"Beginner","chords":["D","G","A"],"capo":0,"parts":[{"name":"Verse / Chorus","tab":{"e":"--2---3---0---2---","B":"--3---3---2---3---","G":"--2---0---2---2---","D":"--0---0---2---0---","A":"------2---0-------","E":"--2---3---0---2---"},"notes":["D3","G4","A2","D3"]}],"tips":"Deep voice, low-key groove. The D-G-A loop stays the same throughout. Nice and easy."},
|
||
{"title":"Black","artist":"Dierks Bentley","genre":"Country","diff":"Intermediate","chords":["Em","D","C","G"],"capo":0,"parts":[{"name":"Verse","tab":{"e":"--0---2---0---3---","B":"--0---3---1---3---","G":"--0---2---0---0---","D":"--2---0---2---0---","A":"--2-------3---2---","E":"0---2---0---3-----"},"notes":["E3","D3","C4","G4"]},{"name":"Chorus","tab":{"e":"--0---3---0---2---","B":"--0---3---1---3---","G":"--0---0---0---2---","D":"--2---0---2---0---","A":"--2---2---3-------","E":"0---3---0---2-----"},"notes":["E3","G4","C4","D3"]}],"tips":"Dark, moody feel. Em to D is the core movement. Let the Em chord breathe on beat 1."},
|
||
{"title":"Play It Again","artist":"Luke Bryan","genre":"Country","diff":"Beginner","chords":["G","C","D","Em"],"capo":2,"parts":[{"name":"Verse","tab":{"e":"--3---0---3---2---","B":"--3---1---3---3---","G":"--0---0---0---2---","D":"--0---2---0---0---","A":"--2---3---2-------","E":"3---0---3---2-----"},"notes":["G4","C4","G4","D3"]},{"name":"Chorus","tab":{"e":"--3---0---2---0---","B":"--3---1---3---0---","G":"--0---0---2---0---","D":"--0---2---0---2---","A":"--2---3-------2---","E":"3---------2---0---"},"notes":["G4","C4","D3","E3"]}],"tips":"Capo 2. Luke Bryan summertime feel. Down-down-up-up-down strum pattern throughout."},
|
||
{"title":"Beautiful Crazy","artist":"Luke Combs","genre":"Country","diff":"Beginner","chords":["G","C","D","Em"],"capo":0,"parts":[{"name":"Verse","tab":{"e":"--3---0---0---2---","B":"--3---1---0---3---","G":"--0---0---0---2---","D":"--0---2---2---0---","A":"--2---3---2-------","E":"3---0---0---2-----"},"notes":["G4","C4","E3","D3"]},{"name":"Chorus","tab":{"e":"--3---2---0---0---","B":"--3---3---1---0---","G":"--0---2---0---0---","D":"--0---0---2---2---","A":"--2-------3---2---","E":"3---2---0---------"},"notes":["G4","D3","C4","E3"]}],"tips":"One of the prettiest Luke Combs songs. Slow strum, let the chords ring. Great fingerpicking song too."},
|
||
{"title":"One Thing Right","artist":"Kane Brown","genre":"Country","diff":"Beginner","chords":["G","C","D","Em"],"capo":0,"parts":[{"name":"Verse","tab":{"e":"--3---0---3---2---","B":"--3---1---3---3---","G":"--0---0---0---2---","D":"--0---2---0---0---","A":"--2---3---2-------","E":"3---0---3---2-----"},"notes":["G4","C4","G4","D3"]},{"name":"Chorus","tab":{"e":"--3---0---0---2---","B":"--3---1---0---3---","G":"--0---0---0---2---","D":"--0---2---2---0---","A":"--2---3---2-------","E":"3---0---0---2-----"},"notes":["G4","C4","E3","D3"]}],"tips":"Ft. Marshmello. Pop-country crossover with a big chorus. Four chords the whole way through."},
|
||
{"title":"Blue Aint Your Color","artist":"Keith Urban","genre":"Country","diff":"Intermediate","chords":["Em","G","C","D"],"capo":0,"parts":[{"name":"Verse","tab":{"e":"--0---3---0---2---","B":"--0---3---1---3---","G":"--0---0---0---2---","D":"--2---0---2---0---","A":"--2---2---3-------","E":"0---3---0---2-----"},"notes":["E3","G4","C4","D3"]},{"name":"Chorus","tab":{"e":"--0---3---0---2---","B":"--0---3---1---3---","G":"--0---0---0---2---","D":"--2---0---2---0---","A":"--2---2---3-------","E":"0---3---0---2-----"},"notes":["E3","G4","C4","D3"]}],"tips":"Capo 4 for original key. Smooth and soulful. Great song to practice transitioning Em to G cleanly."},
|
||
{"title":"Need You Now","artist":"Lady Antebellum","genre":"Country","diff":"Beginner","chords":["G","D","Em","C"],"capo":0,"parts":[{"name":"Verse","tab":{"e":"--3---2---0---0---","B":"--3---3---0---1---","G":"--0---2---0---0---","D":"--0---0---2---2---","A":"--2-------2---3---","E":"3---2---0---------"},"notes":["G4","D3","E3","C4"]},{"name":"Chorus","tab":{"e":"--3---2---0---0---","B":"--3---3---0---1---","G":"--0---2---0---0---","D":"--0---0---2---2---","A":"--2-------2---3---","E":"3---2---0---------"},"notes":["G4","D3","E3","C4"]}],"tips":"Classic country-pop ballad. G-D-Em-C the whole way. Slow strumming, perfect for singing along."},
|
||
{"title":"Beer Cant Fix","artist":"Thomas Rhett","genre":"Country","diff":"Beginner","chords":["G","C","D"],"capo":0,"parts":[{"name":"Verse / Chorus","tab":{"e":"--3---0---2---3---","B":"--3---1---3---3---","G":"--0---0---2---0---","D":"--0---2---0---0---","A":"--2---3-------2---","E":"3---------2---3---"},"notes":["G4","C4","D3","G4"]}],"tips":"Ft. Jon Pardi. Upbeat and fun. Three chords, driving strum. Great crowd-pleaser."},
|
||
{"title":"Die A Happy Man","artist":"Thomas Rhett","genre":"Country","diff":"Beginner","chords":["G","C","D","Em"],"capo":0,"parts":[{"name":"Verse","tab":{"e":"--3---0---3---2---","B":"--3---1---3---3---","G":"--0---0---0---2---","D":"--0---2---0---0---","A":"--2---3---2-------","E":"3---0---3---2-----"},"notes":["G4","C4","G4","D3"]},{"name":"Chorus","tab":{"e":"--3---0---0---2---","B":"--3---1---0---3---","G":"--0---0---0---2---","D":"--0---2---2---0---","A":"--2---3---2-------","E":"3---0---0---2-----"},"notes":["G4","C4","E3","D3"]}],"tips":"One of the most romantic country songs. Slow and heartfelt. Let each chord ring fully."},
|
||
{"title":"In Case You Didnt Know","artist":"Brett Young","genre":"Country","diff":"Beginner","chords":["G","D","Em","C"],"capo":0,"parts":[{"name":"Verse","tab":{"e":"--3---2---0---0---","B":"--3---3---0---1---","G":"--0---2---0---0---","D":"--0---0---2---2---","A":"--2-------2---3---","E":"3---2---0---------"},"notes":["G4","D3","E3","C4"]},{"name":"Chorus","tab":{"e":"--3---2---0---0---","B":"--3---3---0---1---","G":"--0---2---0---0---","D":"--0---0---2---2---","A":"--2-------2---3---","E":"3---2---0---------"},"notes":["G4","D3","E3","C4"]}],"tips":"Beautiful beginner ballad. Slow strum or fingerpick. Capo 1 for original key."},
|
||
{"title":"When It Rains It Pours","artist":"Luke Combs","genre":"Country","diff":"Beginner","chords":["G","C","D"],"capo":0,"parts":[{"name":"Verse","tab":{"e":"--3---0---2---3---","B":"--3---1---3---3---","G":"--0---0---2---0---","D":"--0---2---0---0---","A":"--2---3-------2---","E":"3---------2---3---"},"notes":["G4","C4","D3","G4"]},{"name":"Chorus","tab":{"e":"--3---0---2---3---","B":"--3---1---3---3---","G":"--0---0---2---0---","D":"--0---2---0---0---","A":"--2---3-------2---","E":"3---------2---3---"},"notes":["G4","C4","D3","G4"]}],"tips":"Luke Combs debut hit. Three chords, upbeat strum pattern. Very singable and fun to play."},
|
||
{"title":"Thinkin Bout Me","artist":"Morgan Wallen","genre":"Country","diff":"Beginner","chords":["G","C","D","Em"],"capo":2,"parts":[{"name":"Verse","tab":{"e":"--3---0---0---3---","B":"--3---1---0---3---","G":"--0---0---0---0---","D":"--0---2---2---0---","A":"--2---3---2---2---","E":"3---0---0---3-----"},"notes":["G4","C4","E3","G4"]},{"name":"Chorus","tab":{"e":"--3---2---0---0---","B":"--3---3---1---0---","G":"--0---2---0---0---","D":"--0---0---2---2---","A":"--2-------3---2---","E":"3---2---0---------"},"notes":["G4","D3","C4","E3"]}],"tips":"Capo 2. From One Thing At A Time. Bright and catchy, mid-tempo strum."},
|
||
{"title":"Choosing Texas","artist":"Ella Langley","genre":"Country","diff":"Beginner","chords":["G","C","D","Em"],"capo":2,"parts":[{"name":"Verse","tab":{"e":"--3---3---0---0---","B":"--3---3---1---0---","G":"--0---0---0---0---","D":"--0---0---2---2---","A":"--2---2---3---2---","E":"3---3---0---0-----"},"notes":["G4","G4","C4","E3"]},{"name":"Chorus","tab":{"e":"--3---0---2---3---","B":"--3---1---3---3---","G":"--0---0---2---0---","D":"--0---2---0---0---","A":"--2---3-------2---","E":"3---------2---3---"},"notes":["G4","C4","D3","G4"]}],"tips":"Capo 2. Smooth modern country with a Texas feel. The G-C-D-Em loop is the whole song."},
|
||
{"title":"Heaven","artist":"Kane Brown","genre":"Country","diff":"Beginner","chords":["G","D","Em","C"],"capo":0,"parts":[{"name":"Verse","tab":{"e":"--3---2---0---0---","B":"--3---3---0---1---","G":"--0---2---0---0---","D":"--0---0---2---2---","A":"--2-------2---3---","E":"3---2---0---------"},"notes":["G4","D3","E3","C4"]},{"name":"Chorus","tab":{"e":"--3---2---0---0---","B":"--3---3---0---1---","G":"--0---2---0---0---","D":"--0---0---2---2---","A":"--2-------2---3---","E":"3---2---0---------"},"notes":["G4","D3","E3","C4"]}],"tips":"Capo 1 for original key. Slow, heartfelt ballad. G-D-Em-C the whole song. Perfect for singing along."},
|
||
{"title":"One Of Them Girls","artist":"Lee Brice","genre":"Country","diff":"Beginner","chords":["G","C","D","Em"],"capo":0,"parts":[{"name":"Verse","tab":{"e":"--3---0---2---0---","B":"--3---1---3---0---","G":"--0---0---2---0---","D":"--0---2---0---2---","A":"--2---3-------2---","E":"3---------2---0---"},"notes":["G4","C4","D3","E3"]},{"name":"Chorus","tab":{"e":"--3---2---0---0---","B":"--3---3---1---0---","G":"--0---2---0---0---","D":"--0---0---2---2---","A":"--2-------3---2---","E":"3---2---0---------"},"notes":["G4","D3","C4","E3"]}],"tips":"Smooth mid-tempo country. G-C-D-Em four chord loop. Great singalong song."},
|
||
{"title":"Hurricane","artist":"Luke Combs","genre":"Country","diff":"Beginner","chords":["G","C","D"],"capo":0,"parts":[{"name":"Verse","tab":{"e":"--3---0---2---3---","B":"--3---1---3---3---","G":"--0---0---2---0---","D":"--0---2---0---0---","A":"--2---3-------2---","E":"3---------2---3---"},"notes":["G4","C4","D3","G4"]},{"name":"Chorus","tab":{"e":"--3---0---2---3---","B":"--3---1---3---3---","G":"--0---0---2---0---","D":"--0---2---0---0---","A":"--2---3-------2---","E":"3---------2---3---"},"notes":["G4","C4","D3","G4"]}],"tips":"Luke Combs breakout hit. Driving G-C-D strum pattern. Keep energy high on the chorus."},
|
||
{"title":"Rock and A Hard Place","artist":"Bailey Zimmerman","genre":"Country","diff":"Beginner","chords":["G","D","Em","C"],"capo":0,"parts":[{"name":"Verse","tab":{"e":"--3---2---0---0---","B":"--3---3---0---1---","G":"--0---2---0---0---","D":"--0---0---2---2---","A":"--2-------2---3---","E":"3---2---0---------"},"notes":["G4","D3","E3","C4"]},{"name":"Chorus","tab":{"e":"--3---2---0---0---","B":"--3---3---0---1---","G":"--0---2---0---0---","D":"--0---0---2---2---","A":"--2-------2---3---","E":"3---2---0---------"},"notes":["G4","D3","E3","C4"]}],"tips":"His breakout hit. G-D-Em-C the whole song. Big driving strum on the chorus, softer on the verse."},
|
||
{"title":"Get Low (Acoustic)","artist":"Dan Henig (orig. Lil Jon)","genre":"Rap","diff":"Intermediate","chords":["Em","D","C","B7"],"capo":0,"parts":[{"name":"Main Riff (Em - D - C - B7)","tab":{"e":"0-------2-0-----","B":"0-------3-1-0---","G":"0-------2-0-0---","D":"2-------0-2-0---","A":"2-------0-3-2---","E":"0-------0---0---"},"notes":["E3","D3","C4","B2"]},{"name":"Verse Riff","tab":{"e":"3-2-0---3-2-0---","B":"0-0-0---0-0-0---","G":"0-2-0---0-2-0---","D":"0-0-2---0-0-2---","A":"0-0-3---0-0-3---","E":"0-------0-------"},"notes":["G4","D3","E3","G4","D3","E3"]}],"tips":"Dan Henig plays this in Em. The magic is the Em-D-C-B7 descending bassline. Pick each string cleanly and let notes ring into each other. Start very slowly."},
|
||
{"title":"The Scientist","artist":"Coldplay","genre":"Pop/Rock","diff":"Beginner","chords":["Dm","Bb","F","C"],"capo":0,"parts":[{"name":"Intro / Verse","tab":{"e":"1-------1---0---","B":"3-------3---1---","G":"2-------3---0---","D":"0-------3---2---","A":"0-------1---3---","E":"1---------------"},"notes":["D3","Bb2","F3","C4"]},{"name":"Chorus","tab":{"e":"0---1---0---0---","B":"1---3---1---1---","G":"0---2---2---0---","D":"2---0---3---2---","A":"3---0---3---3---","E":"0---1---1---0---"},"notes":["C4","D3","Bb2","F3"]}],"tips":"One of Coldplay's most iconic songs. The Dm-Bb-F-C loop runs the whole song. Fingerpick the intro slowly."},
|
||
{"title":"Yellow","artist":"Coldplay","genre":"Pop/Rock","diff":"Beginner","chords":["B","Badd11","F#","E","Emaj7"],"capo":0,"parts":[{"name":"Intro Riff","tab":{"e":"2---2---2---2---","B":"4---4---2---0---","G":"4---4---2---1---","D":"4---4---2---2---","A":"2---2---0---2---","E":"2---2---2---0---"},"notes":["B2","F#3","E3","B3"]},{"name":"Verse / Chorus","tab":{"e":"2---2---0---0---","B":"4---4---0---0---","G":"4---4---1---1---","D":"4---4---2---2---","A":"2---2---2---2---","E":"2---2---0---0---"},"notes":["B2","B2","E3","E3"]}],"tips":"Capo 0, key of B. The whole song sits on B and E chords with small variations. Big strumming on the chorus."},
|
||
{"title":"Fix You","artist":"Coldplay","genre":"Pop/Rock","diff":"Beginner","chords":["C","G","Am","F"],"capo":0,"parts":[{"name":"Verse","tab":{"e":"0---3---0---1---","B":"1---3---1---1---","G":"0---0---2---2---","D":"2---0---2---3---","A":"3---2---0---3---","E":"0---3---0---1---"},"notes":["C4","G4","A2","F3"]},{"name":"Chorus","tab":{"e":"0---3---0---1---","B":"1---3---1---1---","G":"0---0---2---2---","D":"2---0---2---3---","A":"3---2---0---3---","E":"0---3---0---1---"},"notes":["C4","G4","A2","F3"]}],"tips":"C-G-Am-F all the way through. Gentle fingerpicking on the verse, build to big strums on the chorus climax."},
|
||
{"title":"Clocks","artist":"Coldplay","genre":"Pop/Rock","diff":"Intermediate","chords":["Eb","Bbm","Fm"],"capo":0,"parts":[{"name":"Main Riff","tab":{"e":"----------------","B":"----------------","G":"8---5---3---5---","D":"8---5---3---5---","A":"6---3---1---3---","E":"----------------"},"notes":["Eb3","Bb2","F2","Bb2"]},{"name":"Verse","tab":{"e":"3---1---6---1---","B":"4---1---6---1---","G":"3---1---5---1---","D":"5---3---8---3---","A":"6---3---8---3---","E":"3---1---6---1---"},"notes":["Eb3","Bbm2","Fm2","Bbm2"]}],"tips":"The piano riff adapted for guitar. The G-D-A string triplet pattern is the signature. Start slow and keep it even."},
|
||
{"title":"A Sky Full Of Stars","artist":"Coldplay","genre":"Pop/Rock","diff":"Beginner","chords":["Am","F","C","G"],"capo":0,"parts":[{"name":"Verse / Chorus","tab":{"e":"0---1---0---3---","B":"1---1---1---3---","G":"2---2---0---0---","D":"2---3---2---0---","A":"0---3---3---2---","E":"0---1---0---3---"},"notes":["A2","F3","C4","G4"]}],"tips":"Am-F-C-G the whole song. Big driving strum for that anthemic festival feel. Capo 0."},
|
||
{"title":"Sparks","artist":"Coldplay","genre":"Pop/Rock","diff":"Beginner","chords":["F","Am","G","C"],"capo":0,"parts":[{"name":"Intro / Verse","tab":{"e":"1---0---3---0---","B":"1---1---3---1---","G":"2---2---0---0---","D":"3---2---0---2---","A":"3---0---2---3---","E":"1---0---3---0---"},"notes":["F3","A2","G4","C4"]}],"tips":"Hidden gem from Parachute. Soft fingerpicking, very gentle. Great song for practicing clean chord changes."},
|
||
{"title":"In My Place","artist":"Coldplay","genre":"Pop/Rock","diff":"Beginner","chords":["A","E","D","F#m"],"capo":0,"parts":[{"name":"Verse","tab":{"e":"0---0---2---2---","B":"2---0---3---2---","G":"2---1---2---2---","D":"2---2---0---4---","A":"0---2---0---4---","E":"0---0---2---2---"},"notes":["A2","E2","D3","F#2"]},{"name":"Chorus","tab":{"e":"0---0---2---2---","B":"2---0---3---2---","G":"2---1---2---2---","D":"2---2---0---4---","A":"0---2---0---4---","E":"0---0---2---2---"},"notes":["A2","E2","D3","F#2"]}],"tips":"Driving eighth note strumming. The A-E-D-F#m loop powers the whole track. Strum hard and rhythmically."},
|
||
{"title":"Shiver","artist":"Coldplay","genre":"Pop/Rock","diff":"Intermediate","chords":["G","D","Am","C","Em"],"capo":0,"parts":[{"name":"Intro Riff","tab":{"e":"3-2-0---0---0---","B":"3-3-3---1---0---","G":"0-2-2---0---0---","D":"0-0-2---2---2---","A":"2---0---3---2---","E":"3-------0---0---"},"notes":["G4","D3","C4","Em3"]}],"tips":"Great guitar-driven Coldplay track. The intro riff walks down from G through D to Am. Keep picking clean and steady."},
|
||
{"title":"The Bad Touch","artist":"Bloodhound Gang","genre":"Rock","diff":"Beginner","chords":["Em","G","D","A"],"capo":0,"parts":[{"name":"Main Riff","tab":{"e":"0---0---3---2---","B":"0---0---3---3---","G":"0---0---0---2---","D":"2---0---0---0---","A":"2---2---2---0---","E":"0---3---3---2---"},"notes":["E3","G4","D3","A2"]},{"name":"Verse","tab":{"e":"0---3---2---0---","B":"0---3---3---2---","G":"0---0---2---2---","D":"2---0---0---2---","A":"2---2---0---0---","E":"0---3---2---0---"},"notes":["E3","G4","D3","A2"]}],"tips":"Fun late 90s rock. Em-G-D-A drives the whole song. Keep the strum punchy and upbeat."},
|
||
{"title":"Zombie","artist":"The Cranberries","genre":"Rock","diff":"Beginner","chords":["Em","C","G","D"],"capo":0,"parts":[{"name":"Verse","tab":{"e":"0---0---3---2---","B":"0---1---3---3---","G":"0---0---0---2---","D":"2---2---0---0---","A":"2---3---2---0---","E":"0---0---3---2---"},"notes":["E3","C4","G4","D3"]},{"name":"Chorus","tab":{"e":"0---0---3---2---","B":"0---1---3---3---","G":"0---0---0---2---","D":"2---2---0---0---","A":"2---3---2---0---","E":"0---0---3---2---"},"notes":["E3","C4","G4","D3"]}],"tips":"Em-C-G-D the whole song. Power chords on the chorus for extra punch. One of the most recognisable riffs of the 90s."},
|
||
{"title":"Linger","artist":"The Cranberries","genre":"Pop/Rock","diff":"Beginner","chords":["D","Dsus2","G","A"],"capo":0,"parts":[{"name":"Intro / Verse","tab":{"e":"2---0---3---0---","B":"3---3---3---2---","G":"2---2---0---2---","D":"0---0---0---2---","A":"0---0---2---0---","E":"2---2---3---0---"},"notes":["D3","D3","G4","A2"]},{"name":"Chorus","tab":{"e":"2---0---3---0---","B":"3---3---3---2---","G":"2---2---0---2---","D":"0---0---0---2---","A":"0---0---2---0---","E":"2---2---3---0---"},"notes":["D3","D3","G4","A2"]}],"tips":"Beautiful gentle song. The D to Dsus2 movement is the signature \u2014 just lift your index finger off the B string. Fingerpick or light strum."},
|
||
{"title":"Somebody That I Used To Know","artist":"Gotye ft. Kimbra","genre":"Pop/Rock","diff":"Intermediate","chords":["Dm","C","Bb","F"],"capo":0,"parts":[{"name":"Intro Riff","tab":{"e":"0---3---1---0---","B":"1---3---3---1---","G":"2---0---3---2---","D":"0---0---3---3---","A":"0---2---1---3---","E":"1---3---1---1---"},"notes":["D3","G4","Bb2","F3"]},{"name":"Verse","tab":{"e":"0---3---1---0---","B":"1---3---3---1---","G":"2---0---3---2---","D":"0---0---3---3---","A":"0---2---1---3---","E":"1---3---1---1---"},"notes":["D3","G4","Bb2","F3"]},{"name":"Chorus","tab":{"e":"1---0---3---1---","B":"3---1---3---3---","G":"3---2---0---3---","D":"3---3---0---3---","A":"1---3---2---1---","E":"1---1---3---1---"},"notes":["Bb2","F3","G4","Bb2"]}],"tips":"The xylophone melody from the original translates beautifully to guitar. Dm-C-Bb-F is the main loop. Fingerpick the intro, strum the chorus."},
|
||
{"title":"Interstellar Theme","artist":"Hans Zimmer","genre":"Cinematic","diff":"Beginner","chords":["Am","F","C","G"],"capo":0,"parts":[{"name":"Main Theme","tab":{"e":"0---1---0---3---","B":"1---1---1---3---","G":"2---2---0---0---","D":"2---3---2---0---","A":"0---3---3---2---","E":"----1---0---3---"},"notes":["A2","F3","C4","G4"]},{"name":"Fingerpicking Pattern","tab":{"e":"0-0-1-1-0-0-3-3-","B":"1-1-1-1-1-1-3-3-","G":"2-2-2-2-0-0-0-0-","D":"2-2-3-3-2-2-0-0-","A":"0---3---3---2---","E":"----1---0---3---"},"notes":["A2","F3","C4","G4","A2","F3"]}],"tips":"Play fingerstyle with a slow, measured pace. Let every note ring fully into the next. The Am-F-C-G loop creates that iconic haunting feel."},
|
||
{"title":"Pink Pony Club","artist":"Chappell Roan","genre":"Pop","diff":"Beginner","chords":["G","D","Em","C"],"capo":0,"parts":[{"name":"Verse","tab":{"e":"3---2---0---0---","B":"3---3---0---1---","G":"0---2---0---0---","D":"0---0---2---2---","A":"2-------2---3---","E":"3---2---0-------"},"notes":["G4","D3","E3","C4"]},{"name":"Chorus","tab":{"e":"3---2---0---0---","B":"3---3---0---1---","G":"0---2---0---0---","D":"0---0---2---2---","A":"2-------2---3---","E":"3---2---0-------"},"notes":["G4","D3","E3","C4"]}],"tips":"G-D-Em-C is the whole song. Big energetic strum on the chorus. Capo 2 to match the original key."},
|
||
{"title":"Self Aware","artist":"Temper City","genre":"Rock","diff":"Intermediate","chords":["Em","C","G","D"],"capo":0,"parts":[{"name":"Verse","tab":{"e":"0---0---3---2---","B":"0---1---3---3---","G":"0---0---0---2---","D":"2---2---0---0---","A":"2---3---2-------","E":"0---0---3---2---"},"notes":["E3","C4","G4","D3"]},{"name":"Chorus","tab":{"e":"3---0---0---2---","B":"3---1---0---3---","G":"0---0---0---2---","D":"0---2---2---0---","A":"2---3---2-------","E":"3---0---0---2---"},"notes":["G4","C4","E3","D3"]}],"tips":"Em-C-G-D drives the whole track. Energetic down strumming on the chorus."}
|
||
];
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
function ChordDiagram({ name }) {
|
||
const sh = CHORDS[name];
|
||
if (!sh) return null;
|
||
|
||
// Three columns left of nut:
|
||
// col1=12 (string letter), col2=34 (open/muted symbol), nut at LEFT=60
|
||
const NUM_FRETS=5, NS=6, W=320, H=210;
|
||
const TOP=44, BOT=H-22, LEFT=60, RIGHT=W-10;
|
||
const strGap=(BOT-TOP)/(NS-1);
|
||
const fretGap=(RIGHT-LEFT)/NUM_FRETS;
|
||
const STRING_LABELS=["E","A","D","G","B","e"];
|
||
const COL_LABEL=12, COL_MARKER=36;
|
||
|
||
return (
|
||
<div style={{background:"#0A0A0E",borderRadius:10,padding:"10px 6px",
|
||
border:"1px solid "+C.fret,marginBottom:8}}>
|
||
<div style={{textAlign:"center",marginBottom:6,fontWeight:700,fontSize:15,color:C.amber}}>{name}</div>
|
||
<svg width={W} height={H} style={{display:"block",margin:"0 auto"}}>
|
||
{/* Nut */}
|
||
<line x1={LEFT} y1={TOP-2} x2={LEFT} y2={BOT+2} stroke={C.text} strokeWidth={3}/>
|
||
{/* Frets */}
|
||
{Array.from({length:NUM_FRETS+1},(_,f)=>(
|
||
<line key={f} x1={LEFT+f*fretGap} y1={TOP} x2={LEFT+f*fretGap} y2={BOT} stroke={C.fret} strokeWidth={1}/>
|
||
))}
|
||
{/* Strings */}
|
||
{Array.from({length:NS},(_,s)=>(
|
||
<line key={s} x1={LEFT} y1={TOP+s*strGap} x2={RIGHT} y2={TOP+s*strGap}
|
||
stroke={C.string} strokeWidth={s===0?2:s===5?0.8:1.2}/>
|
||
))}
|
||
{/* Col 1: string name labels */}
|
||
{STRING_LABELS.map((n,s)=>(
|
||
<text key={s} x={COL_LABEL} y={TOP+s*strGap+4} textAnchor="middle"
|
||
fontSize={11} fill={C.amber} fontFamily="monospace" fontWeight="700">{n}</text>
|
||
))}
|
||
{/* Col 2: open or muted markers */}
|
||
{Array.from({length:NS},(_,s)=>{
|
||
const cy=TOP+s*strGap;
|
||
const isOpen = sh.open.includes(s);
|
||
const isMuted = sh.muted.includes(s);
|
||
if(isOpen) return (
|
||
<g key={s}>
|
||
<circle cx={COL_MARKER} cy={cy} r={7} fill="none" stroke={C.sage} strokeWidth={2}/>
|
||
</g>
|
||
);
|
||
if(isMuted) return (
|
||
<g key={s}>
|
||
<line x1={COL_MARKER-6} y1={cy-6} x2={COL_MARKER+6} y2={cy+6} stroke={C.coral} strokeWidth={2.5}/>
|
||
<line x1={COL_MARKER+6} y1={cy-6} x2={COL_MARKER-6} y2={cy+6} stroke={C.coral} strokeWidth={2.5}/>
|
||
</g>
|
||
);
|
||
return null;
|
||
})}
|
||
{/* Fret numbers */}
|
||
{Array.from({length:NUM_FRETS},(_,f)=>(
|
||
<text key={f} x={LEFT+(f+0.5)*fretGap} y={BOT+16} textAnchor="middle"
|
||
fontSize={9} fill={C.muted} fontFamily="monospace">{f+1}</text>
|
||
))}
|
||
{/* Fretted finger dots */}
|
||
{sh.dots.map(({s,f,n},i)=>{
|
||
const cx = LEFT+(f-0.5)*fretGap;
|
||
const cy = TOP+s*strGap;
|
||
return (
|
||
<g key={i}>
|
||
<circle cx={cx} cy={cy} r={14} fill="rgba(212,168,71,0.12)"/>
|
||
<circle cx={cx} cy={cy} r={11} fill={C.amber}/>
|
||
<text x={cx} y={cy+5} textAnchor="middle" fontSize={13}
|
||
fill="#1A1A1F" fontWeight="900" fontFamily="monospace">{n}</text>
|
||
</g>
|
||
);
|
||
})}
|
||
</svg>
|
||
<div style={{textAlign:"center",fontFamily:"monospace",fontSize:10,color:C.muted,marginTop:4}}>{sh.label}</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
|
||
const NOTE_FRET_MAP = {
|
||
"E2":{str:0,fret:0},"F2":{str:0,fret:1},"F#2":{str:0,fret:2},"G2":{str:0,fret:3},
|
||
"G#2":{str:0,fret:4},"A2":{str:1,fret:0},"A#2":{str:1,fret:1},"Bb2":{str:1,fret:1},
|
||
"B2":{str:1,fret:2},"C3":{str:1,fret:3},"C#3":{str:1,fret:4},"D3":{str:2,fret:0},
|
||
"D#3":{str:2,fret:1},"E3":{str:2,fret:2},"F3":{str:2,fret:3},"F#3":{str:2,fret:4},
|
||
"G3":{str:3,fret:0},"G#3":{str:3,fret:1},"A3":{str:3,fret:2},"A#3":{str:3,fret:3},
|
||
"Bb3":{str:3,fret:3},"B3":{str:4,fret:0},"C4":{str:4,fret:1},"C#4":{str:4,fret:2},
|
||
"D4":{str:4,fret:3},"D#4":{str:4,fret:4},"E4":{str:5,fret:0},"F4":{str:5,fret:1},
|
||
"F#4":{str:5,fret:2},"G4":{str:5,fret:3},"G#4":{str:5,fret:4},"A4":{str:5,fret:5},
|
||
};
|
||
|
||
const STRING_NAMES = ["E","A","D","G","B","e"];
|
||
|
||
function Tip({ children }) {
|
||
return (
|
||
<div style={{background:"rgba(212,168,71,0.08)",borderLeft:"3px solid "+C.amber,borderRadius:"0 8px 8px 0",padding:"12px 16px",fontSize:14,color:"#D4C9A8",margin:"18px 0"}}>
|
||
{children}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function Btn({ onClick, children, variant, disabled }) {
|
||
const styles = {
|
||
primary: {background:C.amber, color:"#1A1A1F", border:"none"},
|
||
secondary: {background:C.bg3, color:C.text, border:"1px solid "+C.fret},
|
||
success: {background:C.sage, color:"#fff", border:"none"},
|
||
danger: {background:C.coral, color:"#fff", border:"none"},
|
||
};
|
||
const s = styles[variant] || styles.primary;
|
||
return (
|
||
<button onClick={onClick} disabled={disabled}
|
||
style={{...s, padding:"10px 20px", borderRadius:8, fontFamily:"sans-serif", fontSize:14, fontWeight:600,
|
||
cursor:disabled?"wait":"pointer", opacity:disabled?0.6:1, display:"inline-flex", alignItems:"center", gap:6}}>
|
||
{children}
|
||
</button>
|
||
);
|
||
}
|
||
|
||
function MicPill({ listening }) {
|
||
return (
|
||
<div style={{display:"inline-flex",alignItems:"center",gap:7,padding:"5px 11px",borderRadius:20,
|
||
background:listening?"rgba(107,143,110,0.12)":"rgba(58,58,72,0.4)",
|
||
border:"1px solid "+(listening?C.sage:C.fret)}}>
|
||
<div style={{width:7,height:7,borderRadius:"50%",background:listening?C.sage:C.fret,flexShrink:0}}/>
|
||
<span style={{fontSize:12,fontWeight:600,color:listening?C.sage:C.muted}}>
|
||
{listening ? "Mic on" : "Mic off"}
|
||
</span>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function LyricsPanel({ title, artist }) {
|
||
const query = encodeURIComponent(artist + " " + title + " lyrics");
|
||
const geniusSearch = "https://genius.com/search?q=" + encodeURIComponent(artist + " " + title);
|
||
const googleSearch = "https://www.google.com/search?q=" + query;
|
||
return (
|
||
<div style={{marginTop:16,padding:16,background:"#0E0E12",borderRadius:10,border:"1px solid "+C.fret}}>
|
||
<div style={{display:"flex",alignItems:"center",gap:8,marginBottom:12}}>
|
||
<span style={{fontSize:18}}>🎤</span>
|
||
<span style={{fontWeight:600,fontSize:14,color:C.text}}>Lyrics</span>
|
||
</div>
|
||
<p style={{fontSize:13,color:C.muted,marginBottom:12,lineHeight:1.5}}>
|
||
Tap a link below to open the lyrics in your browser and sing along while you play:
|
||
</p>
|
||
<div style={{display:"flex",flexDirection:"column",gap:8}}>
|
||
<a href={geniusSearch} target="_blank" rel="noreferrer"
|
||
style={{display:"flex",alignItems:"center",gap:10,padding:"10px 14px",
|
||
background:C.bg2,border:"1px solid "+C.fret,borderRadius:8,textDecoration:"none",color:C.text}}>
|
||
<span style={{fontSize:16}}>🔍</span>
|
||
<div>
|
||
<div style={{fontWeight:600,fontSize:13}}>Search on Genius</div>
|
||
<div style={{fontSize:11,color:C.muted,marginTop:1}}>{artist} - {title}</div>
|
||
</div>
|
||
</a>
|
||
<a href={googleSearch} target="_blank" rel="noreferrer"
|
||
style={{display:"flex",alignItems:"center",gap:10,padding:"10px 14px",
|
||
background:C.bg2,border:"1px solid "+C.fret,borderRadius:8,textDecoration:"none",color:C.text}}>
|
||
<span style={{fontSize:16}}>🌐</span>
|
||
<div>
|
||
<div style={{fontWeight:600,fontSize:13}}>Search on Google</div>
|
||
<div style={{fontSize:11,color:C.muted,marginTop:1}}>{title} lyrics</div>
|
||
</div>
|
||
</a>
|
||
</div>
|
||
<div style={{marginTop:10,fontSize:11,color:C.muted,textAlign:"right"}}>
|
||
Lyrics via <a href="https://genius.com" target="_blank" rel="noreferrer"
|
||
style={{color:C.amber,textDecoration:"none"}}>Genius.com</a>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
const ENCOURAGEMENTS = ["Great job!","You nailed it!","Awesome!","Nicely done!","Keep it up!","Sounding good!","That was perfect!","You rock!"];
|
||
|
||
function SongPlayer({ parts, detected, listening, onStartMic }) {
|
||
const [partIdx, setPartIdx] = useState(0);
|
||
const [active, setActive] = useState(false);
|
||
const [noteIdx, setNoteIdx] = useState(0);
|
||
const [dotStates, setDotStates] = useState({});
|
||
const [done, setDone] = useState(false);
|
||
const [encouragement, setEncouragement] = useState("");
|
||
const timerRef = useRef(null);
|
||
|
||
const allParts = parts || [];
|
||
const currentPart = allParts[partIdx] || {};
|
||
const currentNotes = currentPart.notes || [];
|
||
|
||
// Build a flat sequence: [{partIdx, noteIdx, note}]
|
||
const sequence = allParts.flatMap((p,pi)=>
|
||
(p.notes||[]).map((note,ni)=>({pi, ni, note}))
|
||
);
|
||
|
||
const [seqIdx, setSeqIdx] = useState(0);
|
||
|
||
const stop = useCallback(()=>{
|
||
setActive(false);
|
||
if(timerRef.current) clearTimeout(timerRef.current);
|
||
setSeqIdx(0); setNoteIdx(0); setPartIdx(0);
|
||
setDotStates({}); setDone(false); setEncouragement("");
|
||
},[]);
|
||
|
||
const advance = useCallback((si, seq)=>{
|
||
if(si >= seq.length){
|
||
setActive(false);
|
||
setDone(true);
|
||
setEncouragement(ENCOURAGEMENTS[Math.floor(Math.random()*ENCOURAGEMENTS.length)]);
|
||
return;
|
||
}
|
||
const {pi, ni, note} = seq[si];
|
||
setSeqIdx(si);
|
||
setPartIdx(pi);
|
||
setNoteIdx(ni);
|
||
setDotStates(s=>({...s,[pi+"-"+ni]:"cur"}));
|
||
},[]);
|
||
|
||
const start = useCallback(()=>{
|
||
stop();
|
||
if(!listening && onStartMic) onStartMic();
|
||
setTimeout(()=>{
|
||
setActive(true);
|
||
setDone(false);
|
||
setDotStates({});
|
||
advance(0, sequence);
|
||
}, 300);
|
||
},[sequence, stop, advance, listening, onStartMic]);
|
||
|
||
useEffect(()=>{
|
||
if(!active || !detected) return;
|
||
const cur = sequence[seqIdx];
|
||
if(!cur) return;
|
||
if(detected === cur.note){
|
||
if(timerRef.current) clearTimeout(timerRef.current);
|
||
setDotStates(s=>({...s,[cur.pi+"-"+cur.ni]:"hit"}));
|
||
setTimeout(()=>advance(seqIdx+1, sequence), 400);
|
||
}
|
||
},[detected, active, seqIdx, sequence, advance]);
|
||
|
||
useEffect(()=>()=>{ if(timerRef.current) clearTimeout(timerRef.current); },[]);
|
||
|
||
const NS=6, H=260;
|
||
const TOP=44, BOT=H-24, LEFT=44;
|
||
const strGap=(BOT-TOP)/(NS-1);
|
||
|
||
const renderFretboard = (pIdx) => {
|
||
const pNotes = (allParts[pIdx]||{}).notes||[];
|
||
const N = pNotes.length || 1;
|
||
const W = Math.max(320, LEFT + N*52 + 20);
|
||
const noteX = (ni) => LEFT + ni*52 + 26;
|
||
|
||
return (
|
||
<div style={{overflowX:"auto"}}>
|
||
<svg width={W} height={H} style={{display:"block",minWidth:W}}>
|
||
{/* Strings */}
|
||
{Array.from({length:NS},(_,s)=>(
|
||
<line key={s} x1={LEFT-20} y1={TOP+s*strGap} x2={W-10} y2={TOP+s*strGap}
|
||
stroke={C.string} strokeWidth={s===0?2:s===5?0.8:1.2}/>
|
||
))}
|
||
{/* String labels */}
|
||
{STRING_NAMES.map((n,s)=>(
|
||
<text key={s} x={LEFT-26} y={TOP+s*strGap+4} textAnchor="middle"
|
||
fontSize={10} fill={C.amber} fontFamily="monospace" fontWeight="600">{n}</text>
|
||
))}
|
||
{/* Vertical beat lines */}
|
||
{pNotes.map((_,ni)=>(
|
||
<line key={ni} x1={noteX(ni)} y1={TOP-8} x2={noteX(ni)} y2={BOT+8}
|
||
stroke={C.fret} strokeWidth={1} opacity={0.3}/>
|
||
))}
|
||
{/* Notes - left to right timeline */}
|
||
{pNotes.map((note,ni)=>{
|
||
const p = NOTE_FRET_MAP[note];
|
||
if(!p) return null;
|
||
const isOpen = p.fret===0;
|
||
const cx = noteX(ni);
|
||
const cy = TOP+p.str*strGap;
|
||
const stateKey = pIdx+"-"+ni;
|
||
const state = dotStates[stateKey];
|
||
const isCur = active && state==="cur";
|
||
const isHit = state==="hit";
|
||
const isMiss = state==="miss";
|
||
const fill = isHit?C.sage:isMiss?C.coral:C.amber;
|
||
const opacity = isHit||isCur||isMiss?1:0.7;
|
||
return (
|
||
<g key={ni}>
|
||
<circle cx={cx} cy={cy} r={18} fill={isHit?"rgba(107,143,110,0.15)":isCur?"rgba(212,168,71,0.25)":"rgba(212,168,71,0.1)"}/>
|
||
<circle cx={cx} cy={cy} r={14}
|
||
fill={isOpen?(isHit?C.sage:isCur?C.amber:"transparent"):fill}
|
||
stroke={isOpen?(isHit?C.sage:isCur?C.amber:C.amber):"none"}
|
||
strokeWidth={isOpen?2.5:0}
|
||
opacity={opacity}/>
|
||
{!isOpen && (
|
||
<text x={cx} y={cy+5} textAnchor="middle" fontSize={13}
|
||
fill="#1A1A1F" fontWeight="900" fontFamily="monospace">
|
||
{p.fret}
|
||
</text>
|
||
)}
|
||
{/* Fret label above strings area */}
|
||
<text x={cx} y={TOP-6} textAnchor="middle" fontSize={9}
|
||
fill={isCur?C.amber:C.muted} fontFamily="monospace">
|
||
{isOpen?"open":("fr"+p.fret)}
|
||
</text>
|
||
{isCur && (
|
||
<text x={cx} y={TOP-16} textAnchor="middle" fontSize={20}>🎸</text>
|
||
)}
|
||
</g>
|
||
);
|
||
})}
|
||
</svg>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
return (
|
||
<div style={{marginBottom:16}}>
|
||
{allParts.length > 1 && (
|
||
<div style={{display:"flex",gap:6,marginBottom:10,flexWrap:"wrap"}}>
|
||
{allParts.map((p,i)=>(
|
||
<button key={i} onClick={()=>!active&&setPartIdx(i)}
|
||
style={{padding:"3px 12px",borderRadius:14,fontSize:11,fontWeight:600,
|
||
cursor:active?"default":"pointer",fontFamily:"sans-serif",
|
||
border:"1px solid "+(i===partIdx?C.amber:C.fret),
|
||
background:i===partIdx?"rgba(212,168,71,0.1)":"transparent",
|
||
color:i===partIdx?C.amber:C.muted}}>
|
||
{p.name}
|
||
</button>
|
||
))}
|
||
</div>
|
||
)}
|
||
{allParts.length===1 && (
|
||
<div style={{fontSize:11,fontWeight:600,letterSpacing:"1.5px",textTransform:"uppercase",
|
||
color:C.amber,marginBottom:10}}>{currentPart.name}</div>
|
||
)}
|
||
|
||
<div style={{background:"#0A0A0E",borderRadius:10,padding:"10px 6px",border:"1px solid "+(active?"rgba(212,168,71,0.3)":C.fret),overflowX:"auto",
|
||
transition:"border-color 0.3s"}}>
|
||
{renderFretboard(partIdx)}
|
||
{currentNotes.length > 0 && (
|
||
<div style={{display:"flex",gap:6,flexWrap:"wrap",justifyContent:"center",marginTop:4,padding:"0 8px"}}>
|
||
{currentNotes.map((n,i)=>{
|
||
const p = NOTE_FRET_MAP[n];
|
||
return (
|
||
<div key={i} style={{display:"flex",alignItems:"center",gap:3,fontSize:10,color:C.muted}}>
|
||
{p&&p.fret!==0&&<span style={{color:C.amber,fontWeight:700}}>{i+1}</span>}
|
||
<span style={{fontFamily:"monospace",color:C.text}}>{n}</span>
|
||
{p&&<span style={{color:C.muted}}>{p.fret===0?"(open "+STRING_NAMES[p.str]+")":"("+STRING_NAMES[p.str]+" fr"+p.fret+")"}</span>}
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<div style={{display:"flex",alignItems:"center",gap:10,margin:"14px 0",flexWrap:"wrap"}}>
|
||
<MicPill listening={listening}/>
|
||
{!active && !done && (
|
||
<Btn onClick={start} variant="success">🎸 Play Along</Btn>
|
||
)}
|
||
{active && (
|
||
<Btn onClick={stop} variant="danger">Stop</Btn>
|
||
)}
|
||
{done && (
|
||
<>
|
||
<span style={{fontSize:18,fontWeight:700,color:C.sage}}>{encouragement}</span>
|
||
<Btn onClick={start} variant="secondary">Play Again</Btn>
|
||
</>
|
||
)}
|
||
{active && !listening && (
|
||
<span style={{fontSize:12,color:C.coral}}>Enable mic below first</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
|
||
|
||
const OPEN_NOTES = [
|
||
{label:"Low E string",note:"E2"},{label:"A string",note:"A2"},{label:"D string",note:"D3"},
|
||
{label:"G string",note:"G3"},{label:"B string",note:"B3"},{label:"High e string",note:"E4"},
|
||
];
|
||
|
||
function OpenStringPractice({ detected, onTarget, onComplete }) {
|
||
const [idx, setIdx] = useState(0);
|
||
const [heard, setHeard] = useState(false);
|
||
const [doneSet, setDoneSet] = useState({});
|
||
const [finished, setFinished] = useState(false);
|
||
const [kudos] = useState(()=>{
|
||
const msgs = ["Great job!", "You nailed it!", "Awesome work!", "Sounding good!", "Perfect!", "Nice playing!"];
|
||
return msgs[Math.floor(Math.random()*msgs.length)];
|
||
});
|
||
|
||
const current = OPEN_NOTES[idx];
|
||
|
||
useEffect(()=>{
|
||
if (!finished) onTarget(current ? current.note : null);
|
||
},[idx, finished]);
|
||
|
||
useEffect(()=>{
|
||
if (!detected || heard || finished || !current) return;
|
||
if (detected === current.note) setHeard(true);
|
||
},[detected, heard, finished, current]);
|
||
|
||
const handleNext = () => {
|
||
setDoneSet(d=>({...d,[idx]:true}));
|
||
setHeard(false);
|
||
if (idx + 1 >= OPEN_NOTES.length) {
|
||
setFinished(true);
|
||
onTarget(null);
|
||
if (onComplete) onComplete();
|
||
} else {
|
||
setIdx(i=>i+1);
|
||
}
|
||
};
|
||
|
||
const NUM_FRETS=5, NS=6, W=320, H=200;
|
||
const TOP=44, BOT=H-24, LEFT=44, RIGHT=W-12;
|
||
const strGap=(BOT-TOP)/(NS-1);
|
||
const fretGap=(RIGHT-LEFT)/NUM_FRETS;
|
||
const STRING_LABELS=["E","A","D","G","B","e"];
|
||
|
||
return (
|
||
<div style={{margin:"20px 0"}}>
|
||
{!finished ? (
|
||
<>
|
||
<div style={{background:"#0A0A0E",borderRadius:12,padding:"16px 6px",border:"1px solid "+(heard?C.sage:C.fret),marginBottom:12,transition:"border-color 0.4s"}}>
|
||
<div style={{textAlign:"center",marginBottom:10}}>
|
||
<div style={{fontSize:13,color:C.muted,marginBottom:4}}>Pluck this string:</div>
|
||
<div style={{fontSize:32,fontWeight:800,color:heard?C.sage:C.amber,lineHeight:1,transition:"color 0.3s"}}>{current.label}</div>
|
||
{!heard && <div style={{fontSize:12,color:C.muted,marginTop:4}}>Target: {current.note}</div>}
|
||
{heard && <div style={{fontSize:13,color:C.sage,fontWeight:600,marginTop:4}}>Heard it!</div>}
|
||
</div>
|
||
<svg width={W} height={H} style={{display:"block",margin:"0 auto"}}>
|
||
<line x1={LEFT} y1={TOP-2} x2={LEFT} y2={BOT+2} stroke={C.text} strokeWidth={3}/>
|
||
{Array.from({length:NUM_FRETS+1},(_,f)=>(
|
||
<line key={f} x1={LEFT+f*fretGap} y1={TOP} x2={LEFT+f*fretGap} y2={BOT} stroke={C.fret} strokeWidth={1}/>
|
||
))}
|
||
{Array.from({length:NS},(_,s)=>{
|
||
const isTarget = s === current.note && false;
|
||
const strIdx = NS-1-s;
|
||
const targetStrIdx = OPEN_NOTES.indexOf(current);
|
||
const isHighlighted = s === idx;
|
||
return (
|
||
<g key={s}>
|
||
{isHighlighted && (
|
||
<rect x={LEFT} y={TOP+s*strGap-8} width={RIGHT-LEFT} height={16}
|
||
fill={heard?"rgba(107,143,110,0.12)":"rgba(212,168,71,0.08)"} rx={4}/>
|
||
)}
|
||
<line x1={LEFT} y1={TOP+s*strGap} x2={RIGHT} y2={TOP+s*strGap}
|
||
stroke={isHighlighted?(heard?C.sage:C.amber):C.string}
|
||
strokeWidth={isHighlighted?(heard?3:2.5):(s===0?0.8:s===5?2:1.2)}
|
||
style={{filter:isHighlighted?("drop-shadow(0 0 4px "+(heard?C.sage:C.amber)+")"):"none"}}/>
|
||
<text x={LEFT-10} y={TOP+s*strGap+4} textAnchor="middle"
|
||
fontSize={10} fill={isHighlighted?(heard?C.sage:C.amber):C.amber}
|
||
fontFamily="monospace" fontWeight={isHighlighted?"800":"400"}>
|
||
{STRING_LABELS[s]}
|
||
</text>
|
||
{doneSet[s] && (
|
||
<text x={RIGHT+10} y={TOP+s*strGap+4} textAnchor="middle"
|
||
fontSize={12} fill={C.sage}>✓</text>
|
||
)}
|
||
</g>
|
||
);
|
||
})}
|
||
{Array.from({length:NUM_FRETS},(_,f)=>(
|
||
<text key={f} x={LEFT+(f+0.5)*fretGap} y={BOT+16} textAnchor="middle"
|
||
fontSize={9} fill={C.muted} fontFamily="monospace">{f+1}</text>
|
||
))}
|
||
</svg>
|
||
</div>
|
||
|
||
{heard && (
|
||
<button onClick={handleNext}
|
||
style={{width:"100%",padding:"13px",borderRadius:10,border:"2px solid "+C.sage,
|
||
background:C.sage,color:"#fff",fontFamily:"sans-serif",fontSize:15,
|
||
fontWeight:700,cursor:"pointer",marginBottom:8}}>
|
||
{idx + 1 >= OPEN_NOTES.length ? "All done! See how I did" : "Next string →"}
|
||
</button>
|
||
)}
|
||
|
||
<div style={{display:"flex",gap:6,justifyContent:"center",flexWrap:"wrap",marginTop:8}}>
|
||
{OPEN_NOTES.map((s,i)=>(
|
||
<div key={i} style={{padding:"3px 10px",borderRadius:6,fontFamily:"monospace",fontSize:12,
|
||
border:"1px solid "+(doneSet[i]?C.sage:i===idx?C.amber:C.fret),
|
||
color:doneSet[i]?C.sage:i===idx?C.amber:C.muted,
|
||
background:doneSet[i]?"rgba(107,143,110,0.1)":i===idx?"rgba(212,168,71,0.1)":"transparent"}}>
|
||
{doneSet[i]?"✓":s.note}
|
||
</div>
|
||
))}
|
||
</div>
|
||
</>
|
||
) : (
|
||
<div style={{background:"#0A0A0E",borderRadius:12,padding:28,border:"1px solid "+C.sage,textAlign:"center"}}>
|
||
<div style={{fontSize:40,marginBottom:12}}>🎸</div>
|
||
<div style={{fontSize:22,fontWeight:800,color:C.sage,marginBottom:8}}>{kudos}</div>
|
||
<div style={{fontSize:14,color:C.muted}}>You just played all 6 open strings. Click Next to continue.</div>
|
||
<div style={{display:"flex",gap:6,justifyContent:"center",flexWrap:"wrap",marginTop:16}}>
|
||
{OPEN_NOTES.map((s,i)=>(
|
||
<div key={i} style={{padding:"3px 10px",borderRadius:6,fontFamily:"monospace",fontSize:12,
|
||
border:"1px solid "+C.sage,color:C.sage,background:"rgba(107,143,110,0.1)"}}>✓</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
const LESSON6_SONGS = SONG_LIBRARY.slice(0,4);
|
||
|
||
function PlayAlong({ detected, listening }) {
|
||
const [sel, setSel] = useState(null);
|
||
const songs = LESSON6_SONGS;
|
||
const notes = sel !== null ? songs[sel].parts.flatMap(p=>p.notes||[]) : [];
|
||
|
||
return (
|
||
<div>
|
||
<div style={{display:"grid",gridTemplateColumns:"1fr 1fr",gap:12,margin:"16px 0"}}>
|
||
{songs.map((s,i)=>(
|
||
<div key={i} onClick={()=>setSel(i)}
|
||
style={{background:sel===i?"rgba(212,168,71,0.06)":"#111115",
|
||
border:"1px solid "+(sel===i?C.amber:C.fret),borderRadius:10,padding:14,cursor:"pointer"}}>
|
||
<div style={{fontWeight:600,fontSize:14,marginBottom:3}}>{s.title}</div>
|
||
<div style={{fontSize:12,color:C.muted}}>{s.artist}</div>
|
||
<div style={{fontSize:11,color:C.amber,fontWeight:600,marginTop:6}}>{s.diff}</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
{sel !== null && (
|
||
<div>
|
||
<SongPlayer parts={songs[sel].parts} detected={detected} listening={listening}/>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function SongSearch({ detected, listening, onStartMic }) {
|
||
const [query, setQuery] = useState("");
|
||
const [results, setResults] = useState([]);
|
||
const [song, setSong] = useState(null);
|
||
|
||
const GENRES = [...new Set(SONG_LIBRARY.map(s=>s.genre))];
|
||
|
||
const doSearch = (q) => {
|
||
setQuery(q);
|
||
if (!q.trim()){ setResults([]); return; }
|
||
const lower = q.toLowerCase();
|
||
const matches = SONG_LIBRARY.filter(s=>
|
||
s.title.toLowerCase().includes(lower) ||
|
||
s.artist.toLowerCase().includes(lower) ||
|
||
s.genre.toLowerCase().includes(lower) ||
|
||
s.diff.toLowerCase().includes(lower)
|
||
).slice(0,50);
|
||
setResults(matches);
|
||
};
|
||
|
||
const pick = (s) => { setSong(s); setResults([]); setQuery(s.title); };
|
||
const reset = () => { setSong(null); setQuery(""); setResults([]); };
|
||
|
||
const diffColor = (d) => d==="Beginner"?C.sage:d==="Intermediate"?C.amber:C.coral;
|
||
const diffBg = (d) => d==="Beginner"?"rgba(107,143,110,0.15)":d==="Intermediate"?"rgba(212,168,71,0.15)":"rgba(224,92,75,0.15)";
|
||
|
||
const FEATURED = ["Whiskey Glasses","Cowgirls","Last Night","Beautiful Crazy","Die A Happy Man","In Case You Didnt Know","Need You Now","7 Summers","Iris","Wonderwall"];
|
||
|
||
return (
|
||
<div style={{marginTop:20}}>
|
||
<div style={{fontSize:11,fontWeight:600,letterSpacing:"2px",textTransform:"uppercase",color:C.muted,marginBottom:12}}>
|
||
Search any song
|
||
</div>
|
||
<div style={{position:"relative",marginBottom:4}}>
|
||
<input value={query} onChange={e=>doSearch(e.target.value)}
|
||
placeholder="Song title, artist, or genre..."
|
||
style={{width:"100%",background:"#111115",border:"1px solid "+C.fret,borderRadius:8,
|
||
padding:"11px 36px 11px 14px",fontFamily:"sans-serif",fontSize:14,color:C.text,outline:"none"}}
|
||
/>
|
||
{query && (
|
||
<button onClick={reset}
|
||
style={{position:"absolute",right:10,top:"50%",transform:"translateY(-50%)",
|
||
background:"none",border:"none",color:C.muted,fontSize:20,cursor:"pointer",lineHeight:1,padding:0}}>
|
||
x
|
||
</button>
|
||
)}
|
||
</div>
|
||
|
||
{results.length > 0 && (
|
||
<div style={{background:"#111115",border:"1px solid "+C.fret,borderRadius:8,marginBottom:12,overflow:"hidden"}}>
|
||
{results.map((s,i)=>(
|
||
<div key={i} onClick={()=>pick(s)}
|
||
style={{padding:"10px 14px",cursor:"pointer",
|
||
borderBottom:i<results.length-1?"1px solid "+C.fret:"none",
|
||
display:"flex",justifyContent:"space-between",alignItems:"center"}}>
|
||
<div>
|
||
<div style={{fontWeight:600,fontSize:14}}>{s.title}</div>
|
||
<div style={{fontSize:12,color:C.muted,marginTop:2}}>{s.artist}</div>
|
||
</div>
|
||
<div style={{fontSize:11,color:C.amber,fontWeight:600,flexShrink:0,marginLeft:12}}>{s.genre}</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
|
||
{!query && !song && (
|
||
<div style={{marginTop:12}}>
|
||
<div style={{fontSize:12,color:C.muted,marginBottom:8}}>Browse by difficulty:</div>
|
||
<div style={{display:"flex",gap:8,flexWrap:"wrap",marginBottom:16}}>
|
||
{["Beginner","Intermediate","Advanced"].map(d=>(
|
||
<button key={d} onClick={()=>doSearch(d)}
|
||
style={{padding:"4px 12px",borderRadius:16,fontSize:12,fontWeight:600,
|
||
cursor:"pointer",fontFamily:"sans-serif",
|
||
border:"1px solid "+(d==="Beginner"?C.sage:d==="Intermediate"?C.amber:C.coral),
|
||
background:"transparent",
|
||
color:d==="Beginner"?C.sage:d==="Intermediate"?C.amber:C.coral}}>
|
||
{d}
|
||
</button>
|
||
))}
|
||
</div>
|
||
<div style={{fontSize:12,color:C.muted,marginBottom:8}}>Browse by genre:</div>
|
||
<div style={{display:"flex",gap:8,flexWrap:"wrap",marginBottom:16}}>
|
||
{GENRES.map(g=>(
|
||
<button key={g} onClick={()=>doSearch(g)}
|
||
style={{padding:"4px 12px",borderRadius:16,fontSize:12,fontWeight:500,
|
||
cursor:"pointer",fontFamily:"sans-serif",border:"1px solid "+C.fret,
|
||
background:"transparent",color:C.muted}}>
|
||
{g}
|
||
</button>
|
||
))}
|
||
</div>
|
||
<div style={{fontSize:12,color:C.muted,marginBottom:8}}>Popular picks:</div>
|
||
<div style={{display:"flex",flexDirection:"column",gap:6}}>
|
||
{FEATURED.map(t=>{
|
||
const s = SONG_LIBRARY.find(x=>x.title===t);
|
||
if (!s) return null;
|
||
return (
|
||
<div key={t} onClick={()=>pick(s)}
|
||
style={{background:"#111115",border:"1px solid "+C.fret,borderRadius:8,
|
||
padding:"10px 14px",cursor:"pointer",display:"flex",
|
||
justifyContent:"space-between",alignItems:"center"}}>
|
||
<div>
|
||
<div style={{fontWeight:600,fontSize:14}}>{s.title}</div>
|
||
<div style={{fontSize:12,color:C.muted}}>{s.artist}</div>
|
||
</div>
|
||
<div style={{padding:"3px 9px",borderRadius:5,fontSize:11,fontWeight:600,
|
||
flexShrink:0,marginLeft:12,background:diffBg(s.diff),color:diffColor(s.diff)}}>
|
||
{s.diff}
|
||
</div>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{song && (
|
||
<div style={{marginTop:16}}>
|
||
<div style={{display:"flex",alignItems:"flex-start",justifyContent:"space-between",gap:12,marginBottom:12,flexWrap:"wrap"}}>
|
||
<div>
|
||
<div style={{fontWeight:700,fontSize:20,lineHeight:1.2}}>{song.title}</div>
|
||
<div style={{fontSize:13,color:C.muted,marginTop:3}}>{song.artist}</div>
|
||
<div style={{display:"flex",gap:6,flexWrap:"wrap",marginTop:8}}>
|
||
{(song.chords||[]).map(ch=>(
|
||
<span key={ch} style={{padding:"2px 9px",borderRadius:10,fontSize:12,fontWeight:600,
|
||
background:"rgba(212,168,71,0.12)",color:C.amber,border:"1px solid rgba(212,168,71,0.3)"}}>
|
||
{ch}
|
||
</span>
|
||
))}
|
||
{song.capo > 0 && (
|
||
<span style={{padding:"2px 9px",borderRadius:10,fontSize:12,fontWeight:600,
|
||
background:"rgba(212,168,71,0.12)",color:C.amber,border:"1px solid rgba(212,168,71,0.3)"}}>
|
||
Capo {song.capo}
|
||
</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
<div style={{padding:"4px 10px",borderRadius:6,fontSize:12,fontWeight:600,
|
||
flexShrink:0,background:diffBg(song.diff),color:diffColor(song.diff)}}>
|
||
{song.diff}
|
||
</div>
|
||
</div>
|
||
{song.tips && <Tip>Tip: {song.tips}</Tip>}
|
||
<SongPlayer parts={song.parts} detected={detected} listening={listening} onStartMic={onStartMic}/>
|
||
<LyricsPanel title={song.title} artist={song.artist}/>
|
||
<div style={{marginTop:16}}>
|
||
<button onClick={reset}
|
||
style={{fontSize:13,color:C.muted,background:"none",border:"none",cursor:"pointer",
|
||
padding:0,fontFamily:"sans-serif"}}>
|
||
Back to search
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function ChordPractice({ chords, detected, onTarget }) {
|
||
const CHORD_ROOTS = {Em:"E2", Am:"A2", G:"G2", C:"C3", D:"D3"};
|
||
const KUDOS = ["Sounds good!","Nice strum!","That sounds right!","Great job!","Keep it up!","Well done!"];
|
||
const [activeChord, setActiveChord] = useState(null);
|
||
const [heard, setHeard] = useState({});
|
||
const [kudos, setKudos] = useState({});
|
||
|
||
useEffect(()=>{
|
||
if (activeChord) onTarget(CHORD_ROOTS[activeChord]);
|
||
else onTarget(null);
|
||
},[activeChord]);
|
||
|
||
useEffect(()=>{
|
||
if (!detected || !activeChord || heard[activeChord]) return;
|
||
const root = CHORD_ROOTS[activeChord];
|
||
if (detected === root) {
|
||
setHeard(h=>({...h,[activeChord]:true}));
|
||
setKudos(k=>({...k,[activeChord]:KUDOS[Math.floor(Math.random()*KUDOS.length)]}));
|
||
}
|
||
},[detected, activeChord, heard]);
|
||
|
||
return (
|
||
<div>
|
||
<div style={{fontSize:12,color:C.muted,marginBottom:10}}>
|
||
Tap a chord to practice it, then strum on your guitar:
|
||
</div>
|
||
<div style={{display:"flex",gap:8,flexWrap:"wrap",marginBottom:16}}>
|
||
{chords.map(name=>(
|
||
<button key={name} onClick={()=>setActiveChord(activeChord===name?null:name)}
|
||
style={{padding:"6px 16px",borderRadius:20,fontFamily:"sans-serif",fontSize:13,
|
||
fontWeight:600,cursor:"pointer",
|
||
border:"1px solid "+(heard[name]?C.sage:activeChord===name?C.amber:C.fret),
|
||
background:heard[name]?"rgba(107,143,110,0.15)":activeChord===name?"rgba(212,168,71,0.1)":"transparent",
|
||
color:heard[name]?C.sage:activeChord===name?C.amber:C.muted}}>
|
||
{heard[name]?"✓ ":""}{name}
|
||
</button>
|
||
))}
|
||
</div>
|
||
{activeChord && (
|
||
<div style={{marginBottom:12}}>
|
||
<ChordDiagram name={activeChord}/>
|
||
<div style={{marginTop:10,padding:"10px 14px",borderRadius:8,
|
||
background:heard[activeChord]?"rgba(107,143,110,0.12)":"rgba(212,168,71,0.06)",
|
||
border:"1px solid "+(heard[activeChord]?C.sage:"rgba(212,168,71,0.2)"),
|
||
fontSize:13,color:heard[activeChord]?C.sage:C.muted,textAlign:"center"}}>
|
||
{heard[activeChord]
|
||
? <span style={{fontWeight:700,fontSize:15}}>🎸 {kudos[activeChord]}</span>
|
||
: <span>Strum the {activeChord} chord - listening for the root note ({CHORD_ROOTS[activeChord]})</span>}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
|
||
const LESSONS = [
|
||
{title:"How to Hold a Guitar", eyebrow:"Lesson 1 - Getting Started", next:"Next: Reading Tab"},
|
||
{title:"Reading Guitar Tab", eyebrow:"Lesson 2 - Tab Basics", next:"Next: Your First Notes"},
|
||
{title:"Your First Notes", eyebrow:"Lesson 3 - Open Strings", next:"Next: Your First Chord"},
|
||
{title:"Your First Chord: Em", eyebrow:"Lesson 4 - Chords", next:"Next: More Chords"},
|
||
{title:"G, C, and D Chords", eyebrow:"Lesson 5 - Essential Chords", next:"Next: Play a Song"},
|
||
{title:"Play-Along Songs", eyebrow:"Lesson 6 - Songs", next:"Next: Keep Practicing"},
|
||
{title:"Keep Going", eyebrow:"Lesson 7 - Next Steps", next:"Search Any Song"},
|
||
{title:"Search Any Song", eyebrow:"Lesson 8 - Free Play", next:null},
|
||
];
|
||
|
||
function LessonBody({ id, detected, onTarget, listening, onStartMic }) {
|
||
if (id === 0) return (
|
||
<div style={{color:"#C8C5BE",fontSize:15,lineHeight:1.75}}>
|
||
<p>Before you play a single note, get your posture right. Good posture prevents pain and speeds up progress.</p>
|
||
<Tip>Sit with the guitar waist on your right leg. Back straight, neck angled slightly up. Thumb behind the neck, fingers curling over the fretboard.</Tip>
|
||
<p>Your strumming hand hovers over the soundhole. For now use your thumb, sweeping downward across the strings.</p>
|
||
<div style={{height:1,background:C.fret,margin:"20px 0"}}/>
|
||
<p style={{fontWeight:600,color:C.text,marginBottom:8}}>Finger numbering</p>
|
||
<p style={{marginBottom:12}}>Throughout this app, the gold circles on chord diagrams show you which finger to use. Here is how they are numbered:</p>
|
||
<div style={{background:"#0A0A0E",borderRadius:12,border:"1px solid "+C.fret,padding:"24px 16px 16px",marginBottom:16,textAlign:"center"}}>
|
||
<div style={{position:"relative",display:"inline-block"}}>
|
||
{/* Gold numbers above fingers - positioned to align with emoji fingertips */}
|
||
<div style={{display:"flex",justifyContent:"center",alignItems:"flex-end",
|
||
gap:0,marginBottom:4,paddingLeft:48}}>
|
||
{[
|
||
{num:"1", ml:0},
|
||
{num:"2", ml:8},
|
||
{num:"3", ml:6},
|
||
{num:"4", ml:4},
|
||
].map(({num,ml})=>(
|
||
<div key={num} style={{marginLeft:ml,display:"flex",flexDirection:"column",alignItems:"center"}}>
|
||
<div style={{width:36,height:36,borderRadius:"50%",
|
||
background:C.amber,display:"flex",alignItems:"center",
|
||
justifyContent:"center",fontSize:18,fontWeight:900,
|
||
color:"#1A1A1F",fontFamily:"sans-serif",
|
||
boxShadow:"0 0 10px rgba(212,168,71,0.4)"}}>
|
||
{num}
|
||
</div>
|
||
<div style={{width:2,height:15,background:"rgba(212,168,71,0.4)",margin:"2px auto 0"}}/>
|
||
</div>
|
||
))}
|
||
</div>
|
||
<div style={{fontSize:260,lineHeight:1}}>🤚</div>
|
||
</div>
|
||
<div style={{display:"flex",justifyContent:"center",gap:16,marginTop:12,flexWrap:"wrap",paddingLeft:18}}>
|
||
{[["1","Index"],["2","Middle"],["3","Ring"],["4","Pinky"]].map(([num,name])=>(
|
||
<div key={num} style={{textAlign:"center",minWidth:48}}>
|
||
<div style={{fontSize:11,color:C.muted,fontFamily:"sans-serif"}}>{name}</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
<Tip>When a chord diagram shows a <strong style={{color:C.amber}}>1</strong> on a string, press that string with your index finger. <strong style={{color:C.amber}}>2</strong> = middle, <strong style={{color:C.amber}}>3</strong> = ring, <strong style={{color:C.amber}}>4</strong> = pinky.</Tip>
|
||
<div style={{height:1,background:C.fret,margin:"20px 0"}}/>
|
||
<p style={{fontSize:14,color:C.muted}}>Strings thick to thin: <strong style={{color:C.text}}>E A D G B e</strong> - remember with "Elephants And Donkeys Grow Big Ears."</p>
|
||
</div>
|
||
);
|
||
if (id === 1) {
|
||
// Wider left margin: label col at 14, marker col at 34, nut at 64
|
||
const NUM_FRETS=5, NS=6, W=340, H=210;
|
||
const TOP=44, BOT=H-24, LEFT=64, RIGHT=W-10;
|
||
const strGap=(BOT-TOP)/(NS-1);
|
||
const fretGap=(RIGHT-LEFT)/NUM_FRETS;
|
||
const LABELS=["E","A","D","G","B","e"];
|
||
const markers=[
|
||
{s:0,type:"fretted"},{s:1,type:"fretted"},{s:2,type:"open"},
|
||
{s:3,type:"open"},{s:4,type:"fretted"},{s:5,type:"fretted"}
|
||
];
|
||
return (
|
||
<div>
|
||
<p style={{color:"#C8C5BE",fontSize:15,lineHeight:1.75,marginBottom:14}}>
|
||
Each line is a string on your guitar. Numbers tell you which fret to press. 0 means open - no fingers needed.
|
||
</p>
|
||
<div style={{background:"#0A0A0E",borderRadius:10,padding:"10px 6px",border:"1px solid "+C.fret,marginBottom:16,overflowX:"auto"}}>
|
||
<div style={{textAlign:"center",fontSize:12,color:C.muted,marginBottom:8}}>Example - a G chord</div>
|
||
<svg width={W} height={H} style={{display:"block",margin:"0 auto"}}>
|
||
{/* Nut */}
|
||
<line x1={LEFT} y1={TOP-2} x2={LEFT} y2={BOT+2} stroke={C.text} strokeWidth={3}/>
|
||
{/* Fret lines */}
|
||
{Array.from({length:NUM_FRETS+1},(_,f)=>(
|
||
<line key={f} x1={LEFT+f*fretGap} y1={TOP} x2={LEFT+f*fretGap} y2={BOT} stroke={C.fret} strokeWidth={1}/>
|
||
))}
|
||
{/* Strings */}
|
||
{Array.from({length:NS},(_,s)=>(
|
||
<line key={s} x1={LEFT} y1={TOP+s*strGap} x2={RIGHT} y2={TOP+s*strGap}
|
||
stroke={C.string} strokeWidth={s===0?2:s===5?0.8:1.2}/>
|
||
))}
|
||
{/* String name labels - col 1 */}
|
||
{LABELS.map((n,s)=>(
|
||
<text key={s} x={14} y={TOP+s*strGap+4} textAnchor="middle"
|
||
fontSize={11} fill={C.amber} fontFamily="monospace" fontWeight="700">{n}</text>
|
||
))}
|
||
{/* Open/muted markers - col 2 */}
|
||
{markers.map(({s,type})=>{
|
||
const cy=TOP+s*strGap, cx=38;
|
||
if(type==="open") return (
|
||
<g key={s}>
|
||
<circle cx={cx} cy={cy} r={8} fill="none" stroke={C.sage} strokeWidth={2}/>
|
||
<text x={cx} y={cy+4} textAnchor="middle" fontSize={9}
|
||
fill={C.sage} fontFamily="monospace" fontWeight="700">0</text>
|
||
</g>
|
||
);
|
||
if(type==="muted") return (
|
||
<g key={s}>
|
||
<line x1={cx-6} y1={cy-6} x2={cx+6} y2={cy+6} stroke={C.coral} strokeWidth={2.5}/>
|
||
<line x1={cx+6} y1={cy-6} x2={cx-6} y2={cy+6} stroke={C.coral} strokeWidth={2.5}/>
|
||
</g>
|
||
);
|
||
return null;
|
||
})}
|
||
{/* Fret numbers */}
|
||
{Array.from({length:NUM_FRETS},(_,f)=>(
|
||
<text key={f} x={LEFT+(f+0.5)*fretGap} y={BOT+16} textAnchor="middle"
|
||
fontSize={9} fill={C.muted} fontFamily="monospace">{f+1}</text>
|
||
))}
|
||
{/* Fretted dots */}
|
||
{[{s:0,f:3,n:"3"},{s:1,f:2,n:"2"},{s:4,f:2,n:"1"},{s:5,f:3,n:"4"}].map(({s,f,n},i)=>{
|
||
const cx=LEFT+(f-0.5)*fretGap, cy=TOP+s*strGap;
|
||
return (
|
||
<g key={i}>
|
||
<circle cx={cx} cy={cy} r={14} fill="rgba(212,168,71,0.12)"/>
|
||
<circle cx={cx} cy={cy} r={11} fill={C.amber}/>
|
||
<text x={cx} y={cy+5} textAnchor="middle" fontSize={13}
|
||
fill="#1A1A1F" fontWeight="900" fontFamily="monospace">{n}</text>
|
||
</g>
|
||
);
|
||
})}
|
||
</svg>
|
||
</div>
|
||
<Tip>0 = open string (no fingers). A number = press that fret. X = don't play that string.</Tip>
|
||
<p style={{color:"#C8C5BE",fontSize:14,lineHeight:1.7,marginTop:12}}>
|
||
The top string (E) is the thickest - closest to you when holding the guitar. The bottom (e) is the thinnest.
|
||
</p>
|
||
</div>
|
||
);
|
||
}
|
||
const [stringsComplete, setStringsComplete] = useState(false);
|
||
if (id === 2) return (
|
||
<div>
|
||
<p style={{color:"#C8C5BE",fontSize:15,lineHeight:1.75,marginBottom:4}}>Pluck each open string one at a time. The app listens and lights it up green when it hears you - then tap Next to move on.</p>
|
||
<OpenStringPractice detected={detected} onTarget={onTarget} onComplete={()=>setStringsComplete(true)}/>
|
||
{!stringsComplete && <Tip>Open strings means no fingers on the frets - just pluck the string.</Tip>}
|
||
{stringsComplete && (
|
||
<div style={{display:"flex",gap:10,marginTop:16}}>
|
||
<Btn onClick={()=>setStringsComplete(false)} variant="secondary">Start over</Btn>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
if (id === 3) return (
|
||
<div>
|
||
<p style={{color:"#C8C5BE",fontSize:15,lineHeight:1.75,marginBottom:14}}>Em is the easiest chord on guitar. Only 2 fingers. Am uses 3 fingers in a similar shape. Tap one to see the fingering, then strum it - the mic will confirm!</p>
|
||
<ChordPractice chords={["Em","Am"]} detected={detected} onTarget={onTarget}/>
|
||
<Tip>Em: middle finger 2nd fret A string, ring finger 2nd fret D string. Strum all 6 strings.</Tip>
|
||
</div>
|
||
);
|
||
if (id === 4) return (
|
||
<div>
|
||
<p style={{color:"#C8C5BE",fontSize:15,lineHeight:1.75,marginBottom:14}}>G, C, and D unlock hundreds of songs. Tap each chord to see the fingering and strum it - the mic will confirm when it hears you!</p>
|
||
<ChordPractice chords={["G","C","D"]} detected={detected} onTarget={onTarget}/>
|
||
<Tip>Classic progression: G - D - Em - C. Drill the transitions slowly, accuracy before speed.</Tip>
|
||
</div>
|
||
);
|
||
if (id === 5) return (
|
||
<div>
|
||
<p style={{color:"#C8C5BE",fontSize:15,lineHeight:1.75,marginBottom:4}}>You know enough to play real songs. Pick one and play along!</p>
|
||
<PlayAlong detected={detected} listening={listening}/>
|
||
</div>
|
||
);
|
||
if (id === 6) return (
|
||
<div>
|
||
<p style={{color:"#C8C5BE",fontSize:15,lineHeight:1.75}}>You made it through the basics. Here is what to work on next:</p>
|
||
<Tip>Practice 15 minutes every day. Beats a 2-hour session once a week.</Tip>
|
||
<p style={{color:"#C8C5BE",fontSize:15,lineHeight:1.75}}>
|
||
<strong style={{color:C.text}}>Weeks 1-2:</strong> Drill chord switches until automatic.<br/>
|
||
<strong style={{color:C.text}}>Weeks 3-4:</strong> Try bar chords. F and Bm are tough but worth it.<br/>
|
||
<strong style={{color:C.text}}>Month 2:</strong> Fingerpicking. Arpeggios sound beautiful.
|
||
</p>
|
||
<div style={{marginTop:20,padding:20,background:"#0E0E12",borderRadius:12,
|
||
border:"1px dashed "+C.amber,textAlign:"center"}}>
|
||
<div style={{fontSize:28,marginBottom:8}}>Search</div>
|
||
<div style={{fontWeight:600,fontSize:15,marginBottom:4}}>Ready to play your favourite songs?</div>
|
||
<div style={{fontSize:13,color:C.muted}}>Lesson 8 lets you search 32 songs and play with mic feedback.</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
if (id === 7) return (
|
||
<div>
|
||
<p style={{color:"#C8C5BE",fontSize:15,lineHeight:1.75,marginBottom:4}}>Search any song from our library. Tab loads instantly with mic play-along.</p>
|
||
<SongSearch detected={detected} listening={listening} onStartMic={onStartMic}/>
|
||
</div>
|
||
);
|
||
return null;
|
||
}
|
||
|
||
function MicBar({ note, hz, feedback, listening, onToggle }) {
|
||
return (
|
||
<div style={{position:"fixed",bottom:0,left:0,right:0,
|
||
background:"rgba(26,26,31,0.97)",backdropFilter:"blur(12px)",
|
||
borderTop:"1px solid "+C.fret,padding:"14px 28px",
|
||
display:"flex",alignItems:"center",gap:16,zIndex:100}}>
|
||
<button onClick={onToggle}
|
||
style={{width:46,height:46,borderRadius:"50%",
|
||
border:"2px solid "+(listening?C.coral:C.fret),
|
||
background:listening?"rgba(224,92,75,0.15)":C.bg2,
|
||
fontSize:20,cursor:"pointer",flexShrink:0,
|
||
display:"flex",alignItems:"center",justifyContent:"center"}}>
|
||
🎙️
|
||
</button>
|
||
<div style={{flex:1,minWidth:0}}>
|
||
<div style={{fontWeight:700,fontSize:20,color:C.text,lineHeight:1}}>{note||"-"}</div>
|
||
<div style={{fontFamily:"monospace",fontSize:11,color:C.muted,marginTop:2}}>{hz||"tap mic to start"}</div>
|
||
</div>
|
||
{feedback && (
|
||
<div style={{padding:"5px 14px",borderRadius:18,fontSize:13,fontWeight:600,flexShrink:0,
|
||
background:feedback==="correct"?"rgba(107,143,110,0.2)":"rgba(224,92,75,0.15)",
|
||
color:feedback==="correct"?C.sage:C.coral,
|
||
border:"1px solid "+(feedback==="correct"?C.sage:C.coral)}}>
|
||
{feedback==="correct"?"Nice!":"Try again"}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
|
||
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 (
|
||
<>
|
||
<div onClick={onClose}
|
||
style={{position:"fixed",inset:0,background:"rgba(0,0,0,0.5)",zIndex:200}}/>
|
||
<div style={{position:"fixed",top:0,right:0,bottom:0,width:260,background:C.bg2,
|
||
borderLeft:"1px solid "+C.fret,zIndex:201,display:"flex",flexDirection:"column",
|
||
boxShadow:"-8px 0 32px rgba(0,0,0,0.4)"}}>
|
||
<div style={{padding:"18px 20px",borderBottom:"1px solid "+C.fret,
|
||
display:"flex",alignItems:"center",justifyContent:"space-between"}}>
|
||
<div style={{fontWeight:800,fontSize:18}}>
|
||
<span style={{color:C.amber}}>Guitar</span><span style={{color:C.text}}>Teacher</span>
|
||
</div>
|
||
<button onClick={onClose}
|
||
style={{background:"none",border:"none",color:C.muted,fontSize:22,cursor:"pointer",
|
||
lineHeight:1,padding:4}}>
|
||
x
|
||
</button>
|
||
</div>
|
||
<div style={{padding:"12px 0",flex:1}}>
|
||
{[
|
||
{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})=>(
|
||
<button key={label} onClick={action}
|
||
style={{width:"100%",padding:"16px 20px",background:"none",border:"none",
|
||
cursor:"pointer",textAlign:"left",fontFamily:"sans-serif",
|
||
display:"flex",alignItems:"center",gap:14,
|
||
borderBottom:"1px solid "+C.fret}}>
|
||
<span style={{fontSize:22,flexShrink:0}}>{icon}</span>
|
||
<div>
|
||
<div style={{fontWeight:600,fontSize:14,color:C.text}}>{label}</div>
|
||
<div style={{fontSize:11,color:C.muted,marginTop:2}}>{sub}</div>
|
||
</div>
|
||
</button>
|
||
))}
|
||
</div>
|
||
<div style={{padding:"16px 20px",borderTop:"1px solid "+C.fret,fontSize:11,color:C.muted,textAlign:"center"}}>
|
||
GuitarTeacher - Learn at your own pace
|
||
</div>
|
||
</div>
|
||
</>
|
||
);
|
||
}
|
||
|
||
|
||
function GuitarTuner({ onBack, onGoTuner, onGoBasics, onGoSongSearch }) {
|
||
const [listening, setListening] = useState(false);
|
||
const [menuOpen, setMenuOpen] = useState(false);
|
||
const [detectedNote, setDetectedNote] = useState("");
|
||
const [detectedFreq, setDetectedFreq] = useState(0);
|
||
const [cents, setCents] = useState(0);
|
||
const [closestString, setClosestString] = useState(null);
|
||
const [lockedString, setLockedString] = useState(null);
|
||
const [tuned, setTuned] = useState({});
|
||
const [currentStringIdx, setCurrentStringIdx] = useState(0);
|
||
const [readyToMark, setReadyToMark] = useState(false);
|
||
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 = lockedString || 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) <= 8;
|
||
useEffect(()=>{ if(inTune && detectedFreq>0 && !tuned[activeString?.note]) setReadyToMark(true); },[inTune, detectedFreq]);
|
||
const slightlyOff = Math.abs(cents) <= 20;
|
||
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;
|
||
const allTuned = Object.keys(tuned).length === OPEN_STRINGS.length;
|
||
const activeString = lockedString || OPEN_STRINGS[currentStringIdx];
|
||
|
||
return (
|
||
<div style={{background:C.bg,color:C.text,minHeight:"100vh",fontFamily:"sans-serif",display:"flex",flexDirection:"column"}}>
|
||
<div style={{padding:"18px 32px",borderBottom:"1px solid "+C.fret,display:"flex",alignItems:"center",gap:16}}>
|
||
<button onClick={onBack}
|
||
style={{background:"none",border:"none",color:C.muted,fontSize:20,cursor:"pointer",padding:0,lineHeight:1}}>
|
||
←
|
||
</button>
|
||
<div style={{fontWeight:800,fontSize:22}}>
|
||
<span style={{color:C.amber}}>Guitar</span><span>Teacher</span>
|
||
<span style={{fontWeight:600,fontSize:14,color:C.muted,marginLeft:8}}>/ Tuner</span>
|
||
</div>
|
||
<button onClick={()=>setMenuOpen(true)}
|
||
style={{background:"none",border:"none",cursor:"pointer",padding:4,display:"flex",flexDirection:"column",gap:5}}>
|
||
<div style={{width:22,height:2,background:C.muted,borderRadius:1}}/>
|
||
<div style={{width:22,height:2,background:C.muted,borderRadius:1}}/>
|
||
<div style={{width:22,height:2,background:C.muted,borderRadius:1}}/>
|
||
</button>
|
||
</div>
|
||
{menuOpen && <HamburgerMenu
|
||
onTuner={()=>{setMenuOpen(false);}}
|
||
onBasics={()=>{setMenuOpen(false);onGoBasics();}}
|
||
onSongSearch={()=>{setMenuOpen(false);onGoSongSearch();}}
|
||
onClose={()=>setMenuOpen(false)}
|
||
/>}
|
||
|
||
<div style={{flex:1,display:"flex",alignItems:"center",justifyContent:"center",padding:"24px 20px 60px"}}>
|
||
<div style={{maxWidth:480,width:"100%"}}>
|
||
<div style={{textAlign:"center",marginBottom:28}}>
|
||
<div style={{fontSize:13,color:C.muted,marginBottom:6}}>Pluck any string and tune to the closest note</div>
|
||
</div>
|
||
|
||
{/* Needle meter */}
|
||
<div style={{background:"#0A0A0E",borderRadius:16,padding:"28px 20px 20px",border:"1px solid "+C.fret,marginBottom:20,textAlign:"center"}}>
|
||
<svg width={300} height={160} style={{display:"block",margin:"0 auto",overflow:"visible"}}>
|
||
{/* Arc background */}
|
||
<path d="M 30 140 A 120 120 0 0 1 270 140" fill="none" stroke={C.fret} strokeWidth={8} strokeLinecap="round"/>
|
||
{/* Colored arc zones */}
|
||
<path d="M 30 140 A 120 120 0 0 1 75 50" fill="none" stroke="rgba(224,92,75,0.3)" strokeWidth={8} strokeLinecap="round"/>
|
||
<path d="M 75 50 A 120 120 0 0 1 118 22" fill="none" stroke="rgba(212,168,71,0.3)" strokeWidth={8} strokeLinecap="round"/>
|
||
<path d="M 118 22 A 120 120 0 0 1 182 22" fill="none" stroke="rgba(107,143,110,0.5)" strokeWidth={10} strokeLinecap="round"/>
|
||
<path d="M 182 22 A 120 120 0 0 1 225 50" fill="none" stroke="rgba(212,168,71,0.3)" strokeWidth={8} strokeLinecap="round"/>
|
||
<path d="M 225 50 A 120 120 0 0 1 270 140" fill="none" stroke="rgba(224,92,75,0.3)" strokeWidth={8} strokeLinecap="round"/>
|
||
{/* Center tick */}
|
||
<line x1={150} y1={25} x2={150} y2={40} stroke={C.sage} strokeWidth={2}/>
|
||
{/* Needle */}
|
||
{detectedFreq > 0 && (
|
||
<g transform={"rotate("+needleAngle+", 150, 140)"}>
|
||
<line x1={150} y1={140} x2={150} y2={35} stroke={needleColor} strokeWidth={2.5} strokeLinecap="round"/>
|
||
<circle cx={150} cy={140} r={6} fill={needleColor}/>
|
||
</g>
|
||
)}
|
||
{/* Center dot when no signal */}
|
||
{detectedFreq === 0 && (
|
||
<circle cx={150} cy={140} r={5} fill={C.fret}/>
|
||
)}
|
||
{/* Labels */}
|
||
<text x={18} y={155} fontSize={10} fill={C.muted} textAnchor="middle">-50</text>
|
||
<text x={150} y={18} fontSize={10} fill={C.sage} textAnchor="middle">0</text>
|
||
<text x={282} y={155} fontSize={10} fill={C.muted} textAnchor="middle">+50</text>
|
||
</svg>
|
||
|
||
{detectedFreq > 0 ? (
|
||
<>
|
||
<div style={{fontSize:48,fontWeight:800,color:needleColor,lineHeight:1,margin:"8px 0 4px"}}>{detectedNote}</div>
|
||
<div style={{fontSize:13,color:C.muted,marginBottom:6}}>{detectedFreq} Hz</div>
|
||
<div style={{fontSize:18,fontWeight:700,color:tuneMsgColor}}>{tuneMsg}</div>
|
||
{!inTune && (
|
||
<div style={{fontSize:12,color:C.muted,marginTop:4}}>
|
||
{Math.abs(cents)} cents {cents < 0 ? "flat" : "sharp"}
|
||
</div>
|
||
)}
|
||
</>
|
||
) : (
|
||
<div style={{fontSize:15,color:C.muted,marginTop:8}}>
|
||
{listening ? "Pluck a string..." : "Tap the mic to start"}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* String selector tabs */}
|
||
<div style={{display:"flex",gap:6,marginBottom:12,flexWrap:"wrap"}}>
|
||
{OPEN_STRINGS.map((s,i)=>{
|
||
const isActive = activeString && activeString.note===s.note;
|
||
const isDone = tuned[s.note];
|
||
return (
|
||
<button key={s.note} onClick={()=>{setLockedString(s);setCurrentStringIdx(i);setReadyToMark(false);}}
|
||
style={{flex:1,padding:"8px 4px",borderRadius:8,border:"1px solid "+(isDone?C.sage:isActive?C.amber:C.fret),
|
||
background:isDone?"rgba(107,143,110,0.15)":isActive?"rgba(212,168,71,0.1)":"transparent",
|
||
cursor:"pointer",fontFamily:"sans-serif",textAlign:"center",minWidth:44}}>
|
||
<div style={{fontSize:15,fontWeight:800,color:isDone?C.sage:isActive?C.amber:C.text}}>
|
||
{s.name==="Low E"?"E":s.name==="High e"?"e":s.name}
|
||
</div>
|
||
<div style={{fontSize:9,color:isDone?C.sage:C.muted,marginTop:1}}>{isDone?"✓":s.note}</div>
|
||
</button>
|
||
);
|
||
})}
|
||
</div>
|
||
|
||
{/* Guitar string visual */}
|
||
<div style={{background:"#0A0A0E",borderRadius:12,padding:"18px 16px",border:"1px solid "+(inTune&&detectedFreq>0?C.sage:C.fret),marginBottom:12,transition:"border-color 0.3s"}}>
|
||
<div style={{fontSize:12,fontWeight:600,textTransform:"uppercase",letterSpacing:"1px",
|
||
color:C.muted,marginBottom:14,textAlign:"center"}}>
|
||
{detectedFreq>0 ? (
|
||
<span>Plucking <span style={{color:inTune?C.sage:C.amber,fontWeight:800}}>
|
||
{activeString.name} string
|
||
</span> {inTune ? "- In tune!" : (cents<0?"- Tune up (too flat)":"- Tune down (too sharp)")}</span>
|
||
) : <span>Tap a string above, then pluck it on your guitar</span>}
|
||
</div>
|
||
<svg width="100%" viewBox="0 0 340 130" style={{display:"block"}}>
|
||
{OPEN_STRINGS.map((s,i)=>{
|
||
const isActive = activeString && activeString.note===s.note;
|
||
const x1 = 20, x2 = 320;
|
||
const y = 18 + i*19;
|
||
const thickness = i===0?2.5:i===1?2.2:i===2?1.8:i===3?1.4:i===4?1.1:0.9;
|
||
const stringCol = isActive?(inTune?C.sage:C.amber):"#5A5A6A";
|
||
const glowOpacity = isActive ? 1 : 0;
|
||
return (
|
||
<g key={s.note}>
|
||
{isActive && (
|
||
<line x1={x1} y1={y} x2={x2} y2={y}
|
||
stroke={inTune?C.sage:C.amber} strokeWidth={thickness+6} opacity={0.15}/>
|
||
)}
|
||
<line x1={x1} y1={y} x2={x2} y2={y}
|
||
stroke={stringCol} strokeWidth={thickness}
|
||
strokeLinecap="round"
|
||
style={{filter:isActive?("drop-shadow(0 0 4px "+(inTune?C.sage:C.amber)+")"):"none"}}/>
|
||
<text x={8} y={y+4} textAnchor="middle" fontSize={9}
|
||
fill={isActive?(inTune?C.sage:C.amber):C.muted}
|
||
fontFamily="monospace" fontWeight={isActive?"800":"400"}>
|
||
{s.name==="Low E"?"E":s.name==="High e"?"e":s.name}
|
||
</text>
|
||
<text x={332} y={y+4} textAnchor="middle" fontSize={9}
|
||
fill={isActive?(inTune?C.sage:C.amber):C.muted}
|
||
fontFamily="monospace" fontWeight={isActive?"800":"400"}>
|
||
{s.note}
|
||
</text>
|
||
{isActive && (
|
||
<>
|
||
<circle cx={160} cy={y} r={8} fill={inTune?C.sage:C.amber} opacity={0.9}/>
|
||
<text x={160} y={y+4} textAnchor="middle" fontSize={9}
|
||
fill="#1A1A1F" fontWeight="900" fontFamily="monospace">
|
||
{inTune?"✓":cents<0?"↑":"↓"}
|
||
</text>
|
||
</>
|
||
)}
|
||
</g>
|
||
);
|
||
})}
|
||
<line x1={20} y1={10} x2={20} y2={120} stroke={C.muted} strokeWidth={3} strokeLinecap="round"/>
|
||
<line x1={320} y1={10} x2={320} y2={120} stroke={C.fret} strokeWidth={1}/>
|
||
</svg>
|
||
</div>
|
||
|
||
{/* Mark tuned + Next */}
|
||
<div style={{display:"flex",gap:10,marginBottom:12}}>
|
||
{readyToMark && !tuned[activeString?.note] && (
|
||
<button onClick={()=>{
|
||
setTuned(t=>({...t,[activeString.note]:true}));
|
||
setReadyToMark(false);
|
||
const nextIdx = currentStringIdx+1 < OPEN_STRINGS.length ? currentStringIdx+1 : currentStringIdx;
|
||
setCurrentStringIdx(nextIdx);
|
||
setLockedString(nextIdx!==currentStringIdx ? OPEN_STRINGS[nextIdx] : lockedString);
|
||
}}
|
||
style={{flex:1,padding:"11px",borderRadius:10,border:"2px solid "+C.sage,
|
||
background:"rgba(107,143,110,0.1)",color:C.sage,fontFamily:"sans-serif",
|
||
fontSize:14,fontWeight:700,cursor:"pointer"}}>
|
||
Mark as tuned - Next string
|
||
</button>
|
||
)}
|
||
{allTuned && (
|
||
<button onClick={onBack}
|
||
style={{flex:1,padding:"11px",borderRadius:10,border:"2px solid "+C.sage,
|
||
background:C.sage,color:"#fff",fontFamily:"sans-serif",
|
||
fontSize:14,fontWeight:700,cursor:"pointer"}}>
|
||
All tuned! Back to menu
|
||
</button>
|
||
)}
|
||
</div>
|
||
|
||
{/* Mic button */}
|
||
<button onClick={startMic}
|
||
style={{width:"100%",padding:"14px",borderRadius:12,border:"2px solid "+(listening?C.coral:C.amber),
|
||
background:listening?"rgba(224,92,75,0.1)":"rgba(212,168,71,0.08)",
|
||
color:listening?C.coral:C.amber,fontFamily:"sans-serif",fontSize:15,fontWeight:700,
|
||
cursor:"pointer",display:"flex",alignItems:"center",justifyContent:"center",gap:10}}>
|
||
<span style={{fontSize:20}}>🎙️</span>
|
||
{listening ? "Stop Listening" : "Start Listening"}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
|
||
function SplashCards({ onTuner, onBasics, onSearch }) {
|
||
const [hovered, setHovered] = useState(null);
|
||
const cards = [
|
||
{key:"tuner", icon:"🎵", title:"Tune your guitar", desc:"Get in tune before you play. Pluck a string and the tuner guides you.", action:onTuner},
|
||
{key:"basics", icon:"📖", title:"Start from basics", desc:"New to guitar? Learn step by step from the very beginning.", action:onBasics},
|
||
{key:"search", icon:"🔍", title:"Search any song", desc:"Already know the basics? Find a song and play along with the mic.", action:onSearch},
|
||
];
|
||
return (
|
||
<div style={{display:"flex",flexDirection:"column",gap:12}}>
|
||
{cards.map(({key,icon,title,desc,action})=>(
|
||
<button key={key} onClick={action}
|
||
onMouseEnter={()=>setHovered(key)}
|
||
onMouseLeave={()=>setHovered(null)}
|
||
style={{background:C.bg2,
|
||
border:"1px solid "+(hovered===key?C.sage:C.amber),
|
||
borderRadius:16,padding:"20px 22px",
|
||
cursor:"pointer",textAlign:"left",fontFamily:"sans-serif",
|
||
display:"flex",alignItems:"center",gap:18,
|
||
transition:"border-color 0.2s"}}>
|
||
<div style={{fontSize:36,flexShrink:0}}>{icon}</div>
|
||
<div style={{flex:1}}>
|
||
<div style={{fontWeight:700,fontSize:16,color:C.text,marginBottom:3}}>{title}</div>
|
||
<div style={{fontSize:13,color:C.muted,lineHeight:1.5}}>{desc}</div>
|
||
</div>
|
||
<div style={{color:hovered===key?C.sage:C.amber,fontSize:20,flexShrink:0,transition:"color 0.2s"}}>›</div>
|
||
</button>
|
||
))}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
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("");
|
||
const [pitchHz, setPitchHz] = useState("");
|
||
const [feedback, setFeedback] = useState(null);
|
||
const [target, setTarget] = useState(null);
|
||
const [detected, setDetected] = useState(null);
|
||
|
||
const ctxRef = useRef(null);
|
||
const analyserRef = useRef(null);
|
||
const streamRef = useRef(null);
|
||
const rafRef = useRef(null);
|
||
const fbTimerRef = 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; analyserRef.current=null; streamRef.current=null;
|
||
setListening(false); setPitchNote(""); setPitchHz(""); setDetected(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 = 2048;
|
||
ctx.createMediaStreamSource(stream).connect(analyser);
|
||
streamRef.current=stream; ctxRef.current=ctx; analyserRef.current=analyser;
|
||
setListening(true); setPitchHz("listening...");
|
||
const buf = new Float32Array(analyser.fftSize);
|
||
const loop = ()=>{
|
||
rafRef.current = requestAnimationFrame(loop);
|
||
analyser.getFloatTimeDomainData(buf);
|
||
const freq = autoCorrelate(buf, ctx.sampleRate);
|
||
if (freq > 0){
|
||
const r = freqToNote(freq);
|
||
if (r){
|
||
setPitchNote(r.name);
|
||
setPitchHz(Math.round(freq)+" Hz");
|
||
setDetected(r.name);
|
||
}
|
||
}
|
||
};
|
||
loop();
|
||
} catch(e){ setPitchHz("mic denied"); }
|
||
},[listening, stopMic]);
|
||
|
||
useEffect(()=>{
|
||
if (!detected || !target) return;
|
||
if (detected === target){
|
||
if (fbTimerRef.current) return;
|
||
setFeedback("correct");
|
||
fbTimerRef.current = setTimeout(()=>{ setFeedback(null); fbTimerRef.current=null; }, 1200);
|
||
}
|
||
},[detected, target]);
|
||
|
||
useEffect(()=>()=>stopMic(),[]);
|
||
|
||
const go = (n)=>{ setLesson(n); setSplash(false); };
|
||
const prog = Math.round((lesson+1)/LESSONS.length*100);
|
||
|
||
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 <GuitarTuner
|
||
onBack={()=>setShowTuner(false)}
|
||
onGoTuner={()=>{}}
|
||
onGoBasics={()=>{setShowTuner(false);setSplash(false);setLesson(0);}}
|
||
onGoSongSearch={()=>{setShowTuner(false);setSplash(false);setLesson(7);}}
|
||
/>;
|
||
|
||
if (splash) return (
|
||
<div style={{background:C.bg,color:C.text,minHeight:"100vh",fontFamily:"sans-serif",display:"flex",flexDirection:"column"}}>
|
||
<style>{css}</style>
|
||
<div style={{padding:"18px 32px",borderBottom:"1px solid "+C.fret,fontWeight:800,fontSize:22,
|
||
display:"flex",alignItems:"center",justifyContent:"space-between"}}>
|
||
<div><span style={{color:C.amber}}>Guitar</span><span>Teacher</span></div>
|
||
<button onClick={()=>setMenuOpen(true)}
|
||
style={{background:"none",border:"none",cursor:"pointer",padding:4,display:"flex",flexDirection:"column",gap:5}}>
|
||
<div style={{width:22,height:2,background:C.muted,borderRadius:1}}/>
|
||
<div style={{width:22,height:2,background:C.muted,borderRadius:1}}/>
|
||
<div style={{width:22,height:2,background:C.muted,borderRadius:1}}/>
|
||
</button>
|
||
</div>
|
||
{menuOpen && <HamburgerMenu
|
||
onTuner={()=>{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)}
|
||
/>}
|
||
<div style={{flex:1,display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"center",padding:"32px 24px 60px"}}>
|
||
<div style={{maxWidth:480,width:"100%"}}>
|
||
<div style={{textAlign:"center",marginBottom:36}}>
|
||
<div style={{fontSize:56,lineHeight:1,marginBottom:16}}>🎸</div>
|
||
<h1 style={{fontWeight:800,fontSize:32,lineHeight:1.2,marginBottom:10}}>
|
||
Learn guitar, <span style={{color:C.amber}}>your way.</span>
|
||
</h1>
|
||
<p style={{fontSize:14,color:C.muted,lineHeight:1.7}}>
|
||
Pick where you want to start.
|
||
</p>
|
||
</div>
|
||
|
||
<SplashCards
|
||
onTuner={()=>setShowTuner(true)}
|
||
onBasics={()=>go(0)}
|
||
onSearch={()=>go(7)}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
|
||
return (
|
||
<div style={{background:C.bg,color:C.text,minHeight:"100vh",fontFamily:"sans-serif",overflowX:"hidden"}}>
|
||
<style>{css}</style>
|
||
<div style={{padding:"16px 28px",display:"flex",alignItems:"center",justifyContent:"space-between",borderBottom:"1px solid "+C.fret}}>
|
||
<button onClick={()=>setSplash(true)}
|
||
style={{fontWeight:800,fontSize:21,background:"none",border:"none",cursor:"pointer",color:C.text,padding:0,fontFamily:"sans-serif"}}>
|
||
<span style={{color:C.amber}}>Guitar</span><span>Teacher</span>
|
||
</button>
|
||
<div style={{display:"flex",alignItems:"center",gap:16}}>
|
||
<div style={{display:"flex",alignItems:"center",gap:8,fontSize:12,color:C.muted}}>
|
||
<span>{lesson+1}/{LESSONS.length}</span>
|
||
<div style={{width:80,height:3,background:C.bg3,borderRadius:2,overflow:"hidden"}}>
|
||
<div style={{height:"100%",background:C.amber,width:prog+"%",borderRadius:2,transition:"width 0.4s"}}/>
|
||
</div>
|
||
</div>
|
||
<button onClick={()=>setMenuOpen(true)}
|
||
style={{background:"none",border:"none",cursor:"pointer",padding:4,display:"flex",flexDirection:"column",gap:5}}>
|
||
<div style={{width:22,height:2,background:C.muted,borderRadius:1}}/>
|
||
<div style={{width:22,height:2,background:C.muted,borderRadius:1}}/>
|
||
<div style={{width:22,height:2,background:C.muted,borderRadius:1}}/>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{menuOpen && <HamburgerMenu
|
||
onTuner={()=>{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)}
|
||
/>}
|
||
<div style={{maxWidth:820,margin:"0 auto",padding:"28px 20px 150px"}}>
|
||
<div style={{display:"flex",gap:6,flexWrap:"wrap",marginBottom:24}}>
|
||
{LESSONS.map((l,i)=>(
|
||
<button key={i} onClick={()=>setLesson(i)}
|
||
style={{padding:"4px 12px",borderRadius:18,fontSize:12,fontWeight:500,cursor:"pointer",fontFamily:"sans-serif",
|
||
border:"1px solid "+(i<lesson?C.sage:i===lesson?C.amber:C.fret),
|
||
background:i===lesson?"rgba(212,168,71,0.08)":"transparent",
|
||
color:i<lesson?C.sage:i===lesson?C.amber:C.muted}}>
|
||
{i+1}. {i===7?"Song Search":l.title.split(":")[0]}
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
<div style={{background:C.bg2,borderRadius:14,border:"1px solid "+C.fret,padding:32,marginBottom:20}}>
|
||
<div style={{fontSize:10,fontWeight:600,letterSpacing:"2px",textTransform:"uppercase",color:C.amber,marginBottom:8}}>
|
||
{LESSONS[lesson].eyebrow}
|
||
</div>
|
||
<h1 style={{fontWeight:700,fontSize:26,lineHeight:1.2,marginBottom:18}}>{LESSONS[lesson].title}</h1>
|
||
<LessonBody id={lesson} detected={detected} onTarget={setTarget} listening={listening} onStartMic={startMic}/>
|
||
<div style={{display:"flex",gap:10,flexWrap:"wrap",marginTop:24}}>
|
||
{lesson > 0 && <Btn onClick={()=>setLesson(l=>l-1)} variant="secondary">Back</Btn>}
|
||
{LESSONS[lesson].next
|
||
? <Btn onClick={()=>setLesson(l=>l+1)}>{LESSONS[lesson].next}</Btn>
|
||
: null}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<MicBar note={pitchNote} hz={pitchHz} feedback={feedback} listening={listening} onToggle={startMic}/>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
|
||
const root = ReactDOM.createRoot(document.getElementById("root"));
|
||
root.render(<App/>);
|
||
</script>
|
||
</body>
|
||
</html> |