Spaces:
Running
Running
Update index.html
Browse files- index.html +695 -514
index.html
CHANGED
|
@@ -1,521 +1,702 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
{
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
{ chords: "G C D G", lyrics: "Talkin' 'bout a revolution, oh, no" },
|
| 17 |
-
{ chords: "G C D G", lyrics: "Talkin' 'bout a revolution..." }
|
| 18 |
-
]},
|
| 19 |
-
{ type: "Verse 2", lines: [
|
| 20 |
-
{ chords: "G C D G", lyrics: "While outside a revolution's talking..." },
|
| 21 |
-
{ chords: "G C D G", lyrics: "It's gonna come, it's gonna come..." }
|
| 22 |
-
]},
|
| 23 |
-
{ type: "Chorus 2", lines: [
|
| 24 |
-
{ chords: "G C D G", lyrics: "Talkin' 'bout a revolution, oh, no" },
|
| 25 |
-
{ chords: "G C D G", lyrics: "Talkin' 'bout a revolution..." }
|
| 26 |
-
]},
|
| 27 |
-
{ type: "Outro", lines: [{ chords: "G C D G", lyrics: "(repeat and fade)" }] }
|
| 28 |
-
]
|
| 29 |
-
},
|
| 30 |
-
{
|
| 31 |
-
title: "Fast Car",
|
| 32 |
-
artist: "Tracy Chapman",
|
| 33 |
-
sections: [
|
| 34 |
-
{ type: "Intro", lines: [{ chords: "C G Am F", lyrics: "" }] },
|
| 35 |
-
{ type: "Verse 1", lines: [
|
| 36 |
-
{ chords: "C G Am F", lyrics: "You got a fast car, I want a ticket to anywhere..." },
|
| 37 |
-
{ chords: "C G Am F", lyrics: "We go driving in it, anywhere, maybe we'll make a deal..." }
|
| 38 |
-
]},
|
| 39 |
-
{ type: "Chorus 1", lines: [
|
| 40 |
-
{ chords: "C G Am F", lyrics: "So remember when we were driving, driving in your car..." },
|
| 41 |
-
{ chords: "C G Am F", lyrics: "The speed of light, we gotta go, go, go, go, go..." }
|
| 42 |
-
]},
|
| 43 |
-
{ type: "Verse 2", lines: [
|
| 44 |
-
{ chords: "C G Am F", lyrics: "You got a fast car, I got a plan to get us out of here..." },
|
| 45 |
-
{ chords: "C G Am F", lyrics: "I been working at the convenience store, so slow..." }
|
| 46 |
-
]},
|
| 47 |
-
{ type: "Chorus 2", lines: [
|
| 48 |
-
{ chords: "C G Am F", lyrics: "So remember when we were driving, driving in your car..." },
|
| 49 |
-
{ chords: "C G Am F", lyrics: "The speed of light, we gotta go, go, go, go, go..." }
|
| 50 |
-
]},
|
| 51 |
-
{ type: "Outro", lines: [{ chords: "C G Am F", lyrics: "(repeat and fade)" }] }
|
| 52 |
-
]
|
| 53 |
-
},
|
| 54 |
-
{
|
| 55 |
-
title: "Cult of Personality",
|
| 56 |
-
artist: "Living Colour",
|
| 57 |
-
sections: [
|
| 58 |
-
{ type: "Intro", lines: [{ chords: "Em G C D", lyrics: "" }] },
|
| 59 |
-
{ type: "Verse 1", lines: [
|
| 60 |
-
{ chords: "Em G C D", lyrics: "Look in my eyes, what do you see?" },
|
| 61 |
-
{ chords: "Em G C D", lyrics: "The cult of personality." }
|
| 62 |
-
]},
|
| 63 |
-
{ type: "Chorus 1", lines: [
|
| 64 |
-
{ chords: "Em G C D", lyrics: "Cult of Personality! Cult of Personality!" }
|
| 65 |
-
]},
|
| 66 |
-
{ type: "Verse 2", lines: [
|
| 67 |
-
{ chords: "Em G C D", lyrics: "I look in your eyes, what do I see?" },
|
| 68 |
-
{ chords: "Em G C D", lyrics: "The cult of personality." }
|
| 69 |
-
]},
|
| 70 |
-
{ type: "Chorus 2", lines: [
|
| 71 |
-
{ chords: "Em G C D", lyrics: "Cult of Personality! Cult of Personality!" }
|
| 72 |
-
]},
|
| 73 |
-
{ type: "Outro", lines: [{ chords: "Em G C D", lyrics: "(repeated, building to a final hit on Em)" }] }
|
| 74 |
-
]
|
| 75 |
-
},
|
| 76 |
-
{
|
| 77 |
-
title: "Glamour Boys",
|
| 78 |
-
artist: "Living Colour",
|
| 79 |
-
sections: [
|
| 80 |
-
{ type: "Intro", lines: [{ chords: "E B A E", lyrics: "" }] },
|
| 81 |
-
{ type: "Verse 1", lines: [
|
| 82 |
-
{ chords: "E B", lyrics: "The Glamour Boys, they got the girls" },
|
| 83 |
-
{ chords: "A E", lyrics: "They got the cars, they got the pearls" }
|
| 84 |
-
]},
|
| 85 |
-
{ type: "Chorus 1", lines: [
|
| 86 |
-
{ chords: "E B A E", lyrics: "Glamour Boys, oh, the Glamour Boys, what makes you think you're so cool?" }
|
| 87 |
-
]},
|
| 88 |
-
{ type: "Verse 2", lines: [
|
| 89 |
-
{ chords: "E B", lyrics: "The Glamour Boys, they're so hip" },
|
| 90 |
-
{ chords: "A E", lyrics: "They got the style, they got the trip" }
|
| 91 |
-
]},
|
| 92 |
-
{ type: "Chorus 2", lines: [
|
| 93 |
-
{ chords: "E B A E", lyrics: "Glamour Boys, oh, the Glamour Boys, what makes you think you're so cool?" }
|
| 94 |
-
]},
|
| 95 |
-
{ type: "Outro", lines: [{ chords: "E B A E", lyrics: "(repeat and fade with guitar licks)" }] }
|
| 96 |
-
]
|
| 97 |
-
},
|
| 98 |
-
{
|
| 99 |
-
title: "Ocean Size",
|
| 100 |
-
artist: "Jane's Addiction",
|
| 101 |
-
sections: [
|
| 102 |
-
{ type: "Intro", lines: [{ chords: "C G Am F", lyrics: "" }] },
|
| 103 |
-
{ type: "Verse 1", lines: [
|
| 104 |
-
{ chords: "C G Am F", lyrics: "Day by day, as the weeks turn to months, I watch you..." },
|
| 105 |
-
{ chords: "C G Am F", lyrics: "Growing taller, growing wiser, growing stronger..." }
|
| 106 |
-
]},
|
| 107 |
-
{ type: "Chorus 1", lines: [
|
| 108 |
-
{ chords: "C G D F", lyrics: "Ocean size, it's the ocean size" },
|
| 109 |
-
{ chords: "C G D F", lyrics: "The ocean size, it's the ocean size" }
|
| 110 |
-
]},
|
| 111 |
-
{ type: "Verse 2", lines: [
|
| 112 |
-
{ chords: "C G Am F", lyrics: "Day by day, as the weeks turn to months, I watch you..." },
|
| 113 |
-
{ chords: "C G Am F", lyrics: "Growing stronger, growing wiser, growing taller..." }
|
| 114 |
-
]},
|
| 115 |
-
{ type: "Chorus 2", lines: [
|
| 116 |
-
{ chords: "C G D F", lyrics: "Ocean size, it's the ocean size" },
|
| 117 |
-
{ chords: "C G D F", lyrics: "The ocean size, it's the ocean size" }
|
| 118 |
-
]},
|
| 119 |
-
{ type: "Outro", lines: [{ chords: "C G Am F", lyrics: "(repeat and fade)" }] }
|
| 120 |
-
]
|
| 121 |
-
},
|
| 122 |
-
{
|
| 123 |
-
title: "Mountain Song",
|
| 124 |
-
artist: "Jane's Addiction",
|
| 125 |
-
sections: [
|
| 126 |
-
{ type: "Intro", lines: [{ chords: "Em C G D", lyrics: "" }] },
|
| 127 |
-
{ type: "Verse 1", lines: [
|
| 128 |
-
{ chords: "Em C G D", lyrics: "Coming down the mountain, I saw a girl" },
|
| 129 |
-
{ chords: "Em C G D", lyrics: "She was looking at me, in my world" }
|
| 130 |
-
]},
|
| 131 |
-
{ type: "Chorus 1", lines: [
|
| 132 |
-
{ chords: "Em C G D", lyrics: "Mountain Song! Mountain Song!" }
|
| 133 |
-
]},
|
| 134 |
-
{ type: "Verse 2", lines: [
|
| 135 |
-
{ chords: "Em C G D", lyrics: "She said, \"Where'd you come from? Where'd you go?\"" },
|
| 136 |
-
{ chords: "Em C G D", lyrics: "\"I came from the mountain, don't you know?\"" }
|
| 137 |
-
]},
|
| 138 |
-
{ type: "Chorus 2", lines: [
|
| 139 |
-
{ chords: "Em C G D", lyrics: "Mountain Song! Mountain Song!" }
|
| 140 |
-
]},
|
| 141 |
-
{ type: "Outro", lines: [{ chords: "Em C G D", lyrics: "(repeat and fade)" }] }
|
| 142 |
-
]
|
| 143 |
-
},
|
| 144 |
-
{
|
| 145 |
-
title: "Where Is My Mind?",
|
| 146 |
-
artist: "Pixies",
|
| 147 |
-
sections: [
|
| 148 |
-
{ type: "Intro", lines: [{ chords: "C G Am F", lyrics: "" }] },
|
| 149 |
-
{ type: "Verse 1", lines: [
|
| 150 |
-
{ chords: "C G Am F", lyrics: "With your feet on the air and your head on the ground..." },
|
| 151 |
-
{ chords: "C G Am F", lyrics: "Try this trick and spin it, yeah..." }
|
| 152 |
-
]},
|
| 153 |
-
{ type: "Chorus 1", lines: [
|
| 154 |
-
{ chords: "C G Am F", lyrics: "Where is my mind? Where is my mind?" },
|
| 155 |
-
{ chords: "C G Am F", lyrics: "Where is my mind? Way out in the water, see it swimming..." }
|
| 156 |
-
]},
|
| 157 |
-
{ type: "Verse 2", lines: [
|
| 158 |
-
{ chords: "C G Am F", lyrics: "I was thinking about you and the things we've done..." },
|
| 159 |
-
{ chords: "C G Am F", lyrics: "And all the places we've been to, oh yeah..." }
|
| 160 |
-
]},
|
| 161 |
-
{ type: "Chorus 2", lines: [
|
| 162 |
-
{ chords: "C G Am F", lyrics: "Where is my mind? Where is my mind?" },
|
| 163 |
-
{ chords: "C G Am F", lyrics: "Where is my mind? Way out in the water, see it swimming..." }
|
| 164 |
-
]},
|
| 165 |
-
{ type: "Outro", lines: [{ chords: "C G Am F", lyrics: "(repeat and fade)" }] }
|
| 166 |
-
]
|
| 167 |
-
},
|
| 168 |
-
{
|
| 169 |
-
title: "Fisherman's Blues",
|
| 170 |
-
artist: "The Waterboys",
|
| 171 |
-
sections: [
|
| 172 |
-
{ type: "Intro", lines: [{ chords: "G C D G", lyrics: "" }] },
|
| 173 |
-
{ type: "Verse 1", lines: [
|
| 174 |
-
{ chords: "G C D G", lyrics: "I wish I was a fisherman, tumbling on the sea" },
|
| 175 |
-
{ chords: "G C D G", lyrics: "Far away from dry land, and its bitter misery" }
|
| 176 |
-
]},
|
| 177 |
-
{ type: "Chorus 1", lines: [
|
| 178 |
-
{ chords: "G C D G", lyrics: "I'm gonna make a record, a record of my dreams" },
|
| 179 |
-
{ chords: "G C D G", lyrics: "And let the wind and the waves sing along to the themes" }
|
| 180 |
-
]},
|
| 181 |
-
{ type: "Verse 2", lines: [
|
| 182 |
-
{ chords: "G C D G", lyrics: "I wish I was a fisherman, out on the rolling deep" },
|
| 183 |
-
{ chords: "G C D G", lyrics: "With nothing but the stars to guide me, while the city sleeps" }
|
| 184 |
-
]},
|
| 185 |
-
{ type: "Chorus 2", lines: [
|
| 186 |
-
{ chords: "G C D G", lyrics: "I'm gonna make a record, a record of my dreams" },
|
| 187 |
-
{ chords: "G C D G", lyrics: "And let the wind and the waves sing along to the themes" }
|
| 188 |
-
]},
|
| 189 |
-
{ type: "Outro", lines: [{ chords: "G C D G", lyrics: "(repeat and fade)" }] }
|
| 190 |
-
]
|
| 191 |
-
},
|
| 192 |
-
{
|
| 193 |
-
title: "Express Yourself",
|
| 194 |
-
artist: "N.W.A.",
|
| 195 |
-
sections: [
|
| 196 |
-
{ type: "Intro", lines: [{ chords: "F Am Dm C", lyrics: "" }] },
|
| 197 |
-
{ type: "Verse 1", lines: [
|
| 198 |
-
{ chords: "F Am Dm C", lyrics: "I'm expressing with my full capabilities, and now I'm living in reality..." },
|
| 199 |
-
{ chords: "F Am Dm C", lyrics: "The only solution is to get involved and move it..." }
|
| 200 |
-
]},
|
| 201 |
-
{ type: "Chorus 1", lines: [
|
| 202 |
-
{ chords: "F Am Dm C", lyrics: "Express yourself! Express yourself!" },
|
| 203 |
-
{ chords: "F Am Dm C", lyrics: "Express yourself! It's a brand new thing..." }
|
| 204 |
-
]},
|
| 205 |
-
{ type: "Verse 2", lines: [
|
| 206 |
-
{ chords: "F Am Dm C", lyrics: "Now I'm the one, I'm the one, I'm the one that you know..." },
|
| 207 |
-
{ chords: "F Am Dm C", lyrics: "Coming to get you, coming to get you, coming to get you, watch me go..." }
|
| 208 |
-
]},
|
| 209 |
-
{ type: "Chorus 2", lines: [
|
| 210 |
-
{ chords: "F Am Dm C", lyrics: "Express yourself! Express yourself!" },
|
| 211 |
-
{ chords: "F Am Dm C", lyrics: "Express yourself! It's a brand new thing..." }
|
| 212 |
-
]},
|
| 213 |
-
{ type: "Outro", lines: [{ chords: "F Am Dm C", lyrics: "(repeat and fade)" }] }
|
| 214 |
-
]
|
| 215 |
-
},
|
| 216 |
-
{
|
| 217 |
-
title: "One",
|
| 218 |
-
artist: "Metallica",
|
| 219 |
-
sections: [
|
| 220 |
-
{ type: "Intro", lines: [{ chords: "Am G C F", lyrics: "(Acoustic Intro/Verse Part)" }] },
|
| 221 |
-
{ type: "Verse 1", lines: [
|
| 222 |
-
{ chords: "Am G C F", lyrics: "I can't remember anything, can't tell if this is true or dream..." },
|
| 223 |
-
{ chords: "Am G C F", lyrics: "Deep down inside I feel the scream..." }
|
| 224 |
-
]},
|
| 225 |
-
{ type: "Chorus 1", lines: [ // Note: This is not a traditional chorus for this song, but the repeated progression
|
| 226 |
-
{ chords: "Am G C F", lyrics: "Hold my breath as I wish for death..." }
|
| 227 |
-
]},
|
| 228 |
-
{ type: "Verse 2", lines: [
|
| 229 |
-
{ chords: "Am G C F", lyrics: "Life it seems will fade away, drifting further every day..." },
|
| 230 |
-
{ chords: "Am G C F", lyrics: "Getting lost within myself, nothing matters, no one else..." }
|
| 231 |
-
]},
|
| 232 |
-
{ type: "Chorus 2", lines: [ // Note: This is not a traditional chorus for this song, but the repeated progression
|
| 233 |
-
{ chords: "Am G C F", lyrics: "All the thoughts that I have now..." }
|
| 234 |
-
]},
|
| 235 |
-
{ type: "Outro", lines: [{ chords: "E5 - D5 - C5 - A5", lyrics: "(heavy riffing, fast tempo, ends on E5)" }] }
|
| 236 |
-
]
|
| 237 |
-
},
|
| 238 |
-
{
|
| 239 |
-
title: "Handle with Care",
|
| 240 |
-
artist: "Traveling Wilburys",
|
| 241 |
-
sections: [
|
| 242 |
-
{ type: "Intro", lines: [{ chords: "G D Em C", lyrics: "" }] },
|
| 243 |
-
{ type: "Verse 1", lines: [
|
| 244 |
-
{ chords: "G D Em C", lyrics: "Been beat up and battered 'round, been sent up, and I've been shot down..." },
|
| 245 |
-
{ chords: "G D Em C", lyrics: "You're the best thing that I've found, handle me with care." }
|
| 246 |
-
]},
|
| 247 |
-
{ type: "Chorus 1", lines: [
|
| 248 |
-
{ chords: "G D Em C", lyrics: "Handle me with care, handle me with care" },
|
| 249 |
-
{ chords: "G D Em C", lyrics: "Handle me with care, handle me with care" }
|
| 250 |
-
]},
|
| 251 |
-
{ type: "Verse 2", lines: [
|
| 252 |
-
{ chords: "G D Em C", lyrics: "I been stuck in so much traffic, I'm a mess, a nervous wreck..." },
|
| 253 |
-
{ chords: "G D Em C", lyrics: "I could use some tender love and care, handle me with care." }
|
| 254 |
-
]},
|
| 255 |
-
{ type: "Chorus 2", lines: [
|
| 256 |
-
{ chords: "G D Em C", lyrics: "Handle me with care, handle me with care" },
|
| 257 |
-
{ chords: "G D Em C", lyrics: "Handle me with care, handle me with care" }
|
| 258 |
-
]},
|
| 259 |
-
{ type: "Outro", lines: [{ chords: "G D Em C", lyrics: "(repeat and fade)" }] }
|
| 260 |
-
]
|
| 261 |
-
}
|
| 262 |
-
];
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
// Simplified chord to MIDI note mapping
|
| 266 |
-
// Maps chord root to MIDI note number (C4 = 60) and then adds intervals for Major/Minor triad
|
| 267 |
-
const chordToMidi = (chordName) => {
|
| 268 |
-
const rootMap = {
|
| 269 |
-
'C': 60, 'C#': 61, 'Db': 61, 'D': 62, 'D#': 63, 'Eb': 63, 'E': 64, 'F': 65,
|
| 270 |
-
'F#': 66, 'Gb': 66, 'G': 67, 'G#': 68, 'Ab': 68, 'A': 69, 'A#': 70, 'Bb': 70, 'B': 71
|
| 271 |
-
};
|
| 272 |
-
|
| 273 |
-
let root = null;
|
| 274 |
-
let type = 'major'; // Default to major
|
| 275 |
-
let octaveOffset = 0; // Default to 4th octave for root
|
| 276 |
-
|
| 277 |
-
// Parse root and accidental (e.g., C#, Eb)
|
| 278 |
-
let baseChord = chordName.trim().replace('5', ''); // Handle power chords as root
|
| 279 |
-
const match = baseChord.match(/^([A-G][b#]?)/);
|
| 280 |
-
if (match) {
|
| 281 |
-
root = rootMap[match[1]];
|
| 282 |
-
baseChord = baseChord.substring(match[1].length); // Remove root for type parsing
|
| 283 |
-
} else {
|
| 284 |
-
// Fallback for unparseable roots, or just return empty
|
| 285 |
-
return [];
|
| 286 |
-
}
|
| 287 |
-
|
| 288 |
-
// Determine chord type (simple major/minor/power for now)
|
| 289 |
-
if (baseChord.includes('m') || baseChord.includes('min')) {
|
| 290 |
-
type = 'minor';
|
| 291 |
-
} else if (baseChord.includes('5')) { // Explicit power chord
|
| 292 |
-
type = 'power';
|
| 293 |
-
}
|
| 294 |
-
|
| 295 |
-
if (root === null) return []; // Should not happen with match check above
|
| 296 |
-
|
| 297 |
-
const notes = [root + octaveOffset];
|
| 298 |
-
if (type === 'major') {
|
| 299 |
-
notes.push(root + 4 + octaveOffset); // Major third
|
| 300 |
-
notes.push(root + 7 + octaveOffset); // Perfect fifth
|
| 301 |
-
} else if (type === 'minor') {
|
| 302 |
-
notes.push(root + 3 + octaveOffset); // Minor third
|
| 303 |
-
notes.push(root + 7 + octaveOffset); // Perfect fifth
|
| 304 |
-
} else if (type === 'power') {
|
| 305 |
-
notes.push(root + 7 + octaveOffset); // Perfect fifth
|
| 306 |
-
}
|
| 307 |
-
|
| 308 |
-
// Add higher octave for fuller sound, if it makes sense
|
| 309 |
-
if (notes.length > 0) {
|
| 310 |
-
notes.push(notes[0] + 12); // Add root an octave higher
|
| 311 |
-
}
|
| 312 |
-
|
| 313 |
-
return notes;
|
| 314 |
-
};
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
// Main App Component
|
| 318 |
-
const App = () => {
|
| 319 |
-
const [currentSongIndex, setCurrentSongIndex] = useState(0);
|
| 320 |
-
const [currentTempo, setCurrentTempo] = useState(90); // Default to Medium tempo
|
| 321 |
-
const synthRef = useRef(null);
|
| 322 |
-
const playSequenceRef = useRef(null);
|
| 323 |
-
|
| 324 |
-
// Define tempo presets
|
| 325 |
-
const tempos = {
|
| 326 |
-
slow: 30,
|
| 327 |
-
medium: 90, // Changed from previous default for distinct steps
|
| 328 |
-
fast: 128
|
| 329 |
-
};
|
| 330 |
-
|
| 331 |
-
// Initialize Tone.js synth once
|
| 332 |
-
useEffect(() => {
|
| 333 |
-
// Only create synth if it doesn't exist
|
| 334 |
-
if (!synthRef.current) {
|
| 335 |
-
synthRef.current = new Tone.PolySynth(Tone.Synth, {
|
| 336 |
-
envelope: {
|
| 337 |
-
attack: 0.02,
|
| 338 |
-
decay: 0.1,
|
| 339 |
-
sustain: 0.3,
|
| 340 |
-
release: 1
|
| 341 |
}
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
return;
|
| 390 |
-
}
|
| 391 |
-
|
| 392 |
-
// Start Tone.js audio context if not already started
|
| 393 |
-
if (Tone.context.state !== 'running') {
|
| 394 |
-
await Tone.start();
|
| 395 |
-
console.log('Audio context started.');
|
| 396 |
-
}
|
| 397 |
-
|
| 398 |
-
// Stop any existing sequence and transport
|
| 399 |
-
if (playSequenceRef.current) {
|
| 400 |
-
playSequenceRef.current.stop();
|
| 401 |
-
playSequenceRef.current.dispose();
|
| 402 |
-
playSequenceRef.current = null;
|
| 403 |
-
}
|
| 404 |
-
Tone.Transport.stop();
|
| 405 |
-
Tone.Transport.cancel(); // Clear any scheduled events
|
| 406 |
-
|
| 407 |
-
// Set the tempo before starting the transport
|
| 408 |
-
Tone.Transport.bpm.value = currentTempo;
|
| 409 |
-
|
| 410 |
-
const allChords = [];
|
| 411 |
-
currentSong.sections.forEach(section => {
|
| 412 |
-
section.lines.forEach(line => {
|
| 413 |
-
// Simple regex to extract individual chord names, assume space-separated
|
| 414 |
-
const chordsInLine = line.chords.split(/\s+/).filter(c => c.length > 0);
|
| 415 |
-
chordsInLine.forEach(chord => allChords.push(chord));
|
| 416 |
-
});
|
| 417 |
-
});
|
| 418 |
-
|
| 419 |
-
if (allChords.length === 0) {
|
| 420 |
-
console.warn("No chords found for this song.");
|
| 421 |
-
return;
|
| 422 |
-
}
|
| 423 |
-
|
| 424 |
-
// Create a new sequence for playback
|
| 425 |
-
// The interval '1n' means one whole note per chord in this sequence
|
| 426 |
-
// Tone.js Transport BPM controls the actual duration of '1n'
|
| 427 |
-
playSequenceRef.current = new Tone.Sequence((time, chordName) => {
|
| 428 |
-
const midiNotes = chordToMidi(chordName);
|
| 429 |
-
if (midiNotes.length > 0) {
|
| 430 |
-
synthRef.current.triggerAttackRelease(midiNotes.map(n => Tone.Midi(n).toNote()), "0.8n", time); // hold for 0.8 of a whole note
|
| 431 |
-
}
|
| 432 |
-
}, allChords, "1n").start(0); // Each chord plays for 1 whole note duration
|
| 433 |
-
|
| 434 |
-
Tone.Transport.start();
|
| 435 |
-
|
| 436 |
-
}, [currentSong, currentTempo]); // Recreate sequence if current song or tempo changes
|
| 437 |
-
|
| 438 |
-
// Stop playback when song index changes
|
| 439 |
-
useEffect(() => {
|
| 440 |
-
if (playSequenceRef.current) {
|
| 441 |
-
playSequenceRef.current.stop();
|
| 442 |
-
Tone.Transport.stop();
|
| 443 |
-
}
|
| 444 |
-
}, [currentSongIndex]);
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
return (
|
| 448 |
-
<div className="flex flex-col h-screen bg-gray-900 text-gray-100 font-inter">
|
| 449 |
-
{/* Header with song title and artist */}
|
| 450 |
-
<header className="p-4 bg-gray-800 shadow-md text-center rounded-b-lg">
|
| 451 |
-
<h1 className="text-3xl font-bold text-blue-400">
|
| 452 |
-
{currentSong.title}
|
| 453 |
-
</h1>
|
| 454 |
-
<p className="text-xl text-gray-300">{currentSong.artist}</p>
|
| 455 |
-
</header>
|
| 456 |
-
|
| 457 |
-
{/* Main content area - lyrics and chords */}
|
| 458 |
-
<main className="flex-1 overflow-y-auto p-6 text-2xl leading-relaxed">
|
| 459 |
-
{currentSong.sections.map((section, secIndex) => (
|
| 460 |
-
<div key={secIndex} className="mb-8">
|
| 461 |
-
<h2 className="text-3xl font-semibold text-yellow-300 mb-4 sticky top-0 bg-gray-900 py-2 z-10">
|
| 462 |
-
{section.type}
|
| 463 |
-
</h2>
|
| 464 |
-
{section.lines.map((line, lineIndex) => (
|
| 465 |
-
<div key={lineIndex} className="mb-4">
|
| 466 |
-
<p className="font-bold text-green-300 whitespace-pre">
|
| 467 |
-
{line.chords}
|
| 468 |
-
</p>
|
| 469 |
-
<p className="text-gray-100">
|
| 470 |
-
{line.lyrics}
|
| 471 |
-
</p>
|
| 472 |
-
</div>
|
| 473 |
-
))}
|
| 474 |
-
</div>
|
| 475 |
-
))}
|
| 476 |
-
</main>
|
| 477 |
-
|
| 478 |
-
{/* Navigation and MIDI controls */}
|
| 479 |
-
<footer className="p-4 bg-gray-800 shadow-t-md flex justify-center items-center space-x-4 rounded-t-lg flex-wrap">
|
| 480 |
-
<button
|
| 481 |
-
onClick={handlePrevSong}
|
| 482 |
-
className="bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 px-6 rounded-lg shadow-lg transition duration-300 ease-in-out transform hover:scale-105 my-2"
|
| 483 |
-
>
|
| 484 |
-
Previous Song
|
| 485 |
-
</button>
|
| 486 |
-
<button
|
| 487 |
-
onClick={() => setTempo(tempos.slow)}
|
| 488 |
-
className={`py-3 px-6 rounded-lg shadow-lg transition duration-300 ease-in-out transform hover:scale-105 my-2 ${currentTempo === tempos.slow ? 'bg-purple-700' : 'bg-purple-500 hover:bg-purple-600'} text-white font-bold`}
|
| 489 |
-
>
|
| 490 |
-
Slow ({tempos.slow} BPM)
|
| 491 |
</button>
|
| 492 |
-
<button
|
| 493 |
-
|
| 494 |
-
className={`py-3 px-6 rounded-lg shadow-lg transition duration-300 ease-in-out transform hover:scale-105 my-2 ${currentTempo === tempos.medium ? 'bg-purple-700' : 'bg-purple-500 hover:bg-purple-600'} text-white font-bold`}
|
| 495 |
-
>
|
| 496 |
-
Medium ({tempos.medium} BPM)
|
| 497 |
</button>
|
| 498 |
-
<button
|
| 499 |
-
|
| 500 |
-
className={`py-3 px-6 rounded-lg shadow-lg transition duration-300 ease-in-out transform hover:scale-105 my-2 ${currentTempo === tempos.fast ? 'bg-purple-700' : 'bg-purple-500 hover:bg-purple-600'} text-white font-bold`}
|
| 501 |
-
>
|
| 502 |
-
Fast ({tempos.fast} BPM)
|
| 503 |
</button>
|
| 504 |
-
<button
|
| 505 |
-
|
| 506 |
-
className="bg-green-600 hover:bg-green-700 text-white font-bold py-3 px-6 rounded-lg shadow-lg transition duration-300 ease-in-out transform hover:scale-105 my-2"
|
| 507 |
-
>
|
| 508 |
-
Play Chords
|
| 509 |
</button>
|
| 510 |
-
<button
|
| 511 |
-
|
| 512 |
-
className="bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 px-6 rounded-lg shadow-lg transition duration-300 ease-in-out transform hover:scale-105 my-2"
|
| 513 |
-
>
|
| 514 |
-
Next Song
|
| 515 |
</button>
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 520 |
|
| 521 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Chord Sheet Teleprompter</title>
|
| 7 |
+
<!-- Tailwind CSS CDN -->
|
| 8 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
| 9 |
+
<!-- Tone.js CDN -->
|
| 10 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/tone/14.8.49/Tone.min.js"></script>
|
| 11 |
+
<style>
|
| 12 |
+
body {
|
| 13 |
+
font-family: "Inter", sans-serif;
|
| 14 |
+
margin: 0;
|
| 15 |
+
overflow: hidden; /* Prevent body scroll, main content handles scroll */
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
}
|
| 17 |
+
/* Custom scrollbar for main content */
|
| 18 |
+
.custom-scrollbar::-webkit-scrollbar {
|
| 19 |
+
width: 8px;
|
| 20 |
+
}
|
| 21 |
+
.custom-scrollbar::-webkit-scrollbar-track {
|
| 22 |
+
background: #2d3748; /* bg-gray-800 */
|
| 23 |
+
}
|
| 24 |
+
.custom-scrollbar::-webkit-scrollbar-thumb {
|
| 25 |
+
background: #4a5568; /* bg-gray-600 */
|
| 26 |
+
border-radius: 4px;
|
| 27 |
+
}
|
| 28 |
+
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
| 29 |
+
background: #6b7280; /* bg-gray-500 */
|
| 30 |
+
}
|
| 31 |
+
/* Preserve whitespace and prevent wrapping for chords */
|
| 32 |
+
.whitespace-pre {
|
| 33 |
+
white-space: pre;
|
| 34 |
+
}
|
| 35 |
+
</style>
|
| 36 |
+
</head>
|
| 37 |
+
<body class="bg-gray-900 text-gray-100 flex flex-col h-screen">
|
| 38 |
+
|
| 39 |
+
<!-- Header Section -->
|
| 40 |
+
<header class="p-4 bg-gray-800 shadow-md text-center rounded-b-lg flex flex-col items-center justify-center">
|
| 41 |
+
<div class="flex items-center justify-center space-x-4 mb-2">
|
| 42 |
+
<button id="prevSongBtnHeader" class="bg-gray-700 hover:bg-gray-600 text-white font-bold py-2 px-4 rounded-lg transition duration-300 ease-in-out transform hover:scale-105">
|
| 43 |
+
▲ <!-- Up Arrow -->
|
| 44 |
+
</button>
|
| 45 |
+
<div class="text-center">
|
| 46 |
+
<h1 id="songTitle" class="text-3xl font-bold text-blue-400"></h1>
|
| 47 |
+
<p id="artistName" class="text-xl text-gray-300"></p>
|
| 48 |
+
</div>
|
| 49 |
+
<button id="nextSongBtnHeader" class="bg-gray-700 hover:bg-gray-600 text-white font-bold py-2 px-4 rounded-lg transition duration-300 ease-in-out transform hover:scale-105">
|
| 50 |
+
▼ <!-- Down Arrow -->
|
| 51 |
+
</button>
|
| 52 |
+
</div>
|
| 53 |
+
</header>
|
| 54 |
+
|
| 55 |
+
<!-- Main Content Area - Lyrics and Chords -->
|
| 56 |
+
<main id="scrollContainer" class="flex-1 overflow-y-auto p-6 text-2xl leading-relaxed custom-scrollbar">
|
| 57 |
+
<!-- Song content will be rendered here by JavaScript -->
|
| 58 |
+
</main>
|
| 59 |
+
|
| 60 |
+
<!-- Footer Section - Controls -->
|
| 61 |
+
<footer class="p-4 bg-gray-800 shadow-t-md flex justify-center items-center space-x-4 rounded-t-lg flex-wrap">
|
| 62 |
+
<button id="slowTempoBtn" class="py-3 px-6 rounded-lg shadow-lg transition duration-300 ease-in-out transform hover:scale-105 my-2 bg-purple-500 hover:bg-purple-600 text-white font-bold">
|
| 63 |
+
Slow (30 BPM)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
</button>
|
| 65 |
+
<button id="mediumTempoBtn" class="py-3 px-6 rounded-lg shadow-lg transition duration-300 ease-in-out transform hover:scale-105 my-2 bg-purple-700 text-white font-bold">
|
| 66 |
+
Medium (90 BPM)
|
|
|
|
|
|
|
|
|
|
| 67 |
</button>
|
| 68 |
+
<button id="fastTempoBtn" class="py-3 px-6 rounded-lg shadow-lg transition duration-300 ease-in-out transform hover:scale-105 my-2 bg-purple-500 hover:bg-purple-600 text-white font-bold">
|
| 69 |
+
Fast (128 BPM)
|
|
|
|
|
|
|
|
|
|
| 70 |
</button>
|
| 71 |
+
<button id="arpeggioDirectionBtn" class="bg-orange-600 hover:bg-orange-700 text-white font-bold py-3 px-6 rounded-lg shadow-lg transition duration-300 ease-in-out transform hover:scale-105 my-2">
|
| 72 |
+
Arpeggio: Rising
|
|
|
|
|
|
|
|
|
|
| 73 |
</button>
|
| 74 |
+
<button id="startTeleprompterBtn" class="bg-green-600 hover:bg-green-700 text-white font-bold py-3 px-6 rounded-lg shadow-lg transition duration-300 ease-in-out transform hover:scale-105 my-2">
|
| 75 |
+
Start/Restart Teleprompter (Press Space/Right Arrow)
|
|
|
|
|
|
|
|
|
|
| 76 |
</button>
|
| 77 |
+
</footer>
|
| 78 |
+
|
| 79 |
+
<script>
|
| 80 |
+
// Song data
|
| 81 |
+
const songsData = [
|
| 82 |
+
{
|
| 83 |
+
title: "Talkin' 'bout a Revolution",
|
| 84 |
+
artist: "Tracy Chapman",
|
| 85 |
+
sections: [
|
| 86 |
+
{ type: "Intro", lines: [{ chords: "G C D G", lyrics: "" }] },
|
| 87 |
+
{ type: "Verse 1", lines: [
|
| 88 |
+
{ chords: "G C D G", lyrics: "While outside a revolution's talking..." },
|
| 89 |
+
{ chords: "G C D G", lyrics: "It's gonna come, it's gonna come..." }
|
| 90 |
+
]},
|
| 91 |
+
{ type: "Chorus 1", lines: [
|
| 92 |
+
{ chords: "G C D G", lyrics: "Talkin' 'bout a revolution, oh, no" },
|
| 93 |
+
{ chords: "G C D G", lyrics: "Talkin' 'bout a revolution..." }
|
| 94 |
+
]},
|
| 95 |
+
{ type: "Verse 2", lines: [
|
| 96 |
+
{ chords: "G C D G", lyrics: "While outside a revolution's talking..." },
|
| 97 |
+
{ chords: "G C D G", lyrics: "It's gonna come, it's gonna come..." }
|
| 98 |
+
]},
|
| 99 |
+
{ type: "Chorus 2", lines: [
|
| 100 |
+
{ chords: "G C D G", lyrics: "Talkin' 'bout a revolution, oh, no" },
|
| 101 |
+
{ chords: "G C D G", lyrics: "Talkin' 'bout a revolution..." }
|
| 102 |
+
]},
|
| 103 |
+
{ type: "Outro", lines: [{ chords: "G C D G", lyrics: "(repeat and fade)" }] }
|
| 104 |
+
]
|
| 105 |
+
},
|
| 106 |
+
{
|
| 107 |
+
title: "Fast Car",
|
| 108 |
+
artist: "Tracy Chapman",
|
| 109 |
+
sections: [
|
| 110 |
+
{ type: "Intro", lines: [{ chords: "C G Am F", lyrics: "" }] },
|
| 111 |
+
{ type: "Verse 1", lines: [
|
| 112 |
+
{ chords: "C G Am F", lyrics: "You got a fast car, I want a ticket to anywhere..." },
|
| 113 |
+
{ chords: "C G Am F", lyrics: "We go driving in it, anywhere, maybe we'll make a deal..." }
|
| 114 |
+
]},
|
| 115 |
+
{ type: "Chorus 1", lines: [
|
| 116 |
+
{ chords: "C G Am F", lyrics: "So remember when we were driving, driving in your car..." },
|
| 117 |
+
{ chords: "C G Am F", lyrics: "The speed of light, we gotta go, go, go, go, go..." }
|
| 118 |
+
]},
|
| 119 |
+
{ type: "Verse 2", lines: [
|
| 120 |
+
{ chords: "C G Am F", lyrics: "You got a fast car, I got a plan to get us out of here..." },
|
| 121 |
+
{ chords: "C G Am F", lyrics: "I been working at the convenience store, so slow..." }
|
| 122 |
+
]},
|
| 123 |
+
{ type: "Chorus 2", lines: [
|
| 124 |
+
{ chords: "C G Am F", lyrics: "So remember when we were driving, driving in your car..." },
|
| 125 |
+
{ chords: "C G Am F", lyrics: "The speed of light, we gotta go, go, go, go, go..." }
|
| 126 |
+
]},
|
| 127 |
+
{ type: "Outro", lines: [{ chords: "C G Am F", lyrics: "(repeat and fade)" }] }
|
| 128 |
+
]
|
| 129 |
+
},
|
| 130 |
+
{
|
| 131 |
+
title: "Cult of Personality",
|
| 132 |
+
artist: "Living Colour",
|
| 133 |
+
sections: [
|
| 134 |
+
{ type: "Intro", lines: [{ chords: "Em G C D", lyrics: "" }] },
|
| 135 |
+
{ type: "Verse 1", lines: [
|
| 136 |
+
{ chords: "Em G C D", lyrics: "Look in my eyes, what do you see?" },
|
| 137 |
+
{ chords: "Em G C D", lyrics: "The cult of personality." }
|
| 138 |
+
]},
|
| 139 |
+
{ type: "Chorus 1", lines: [
|
| 140 |
+
{ chords: "Em G C D", lyrics: "Cult of Personality! Cult of Personality!" }
|
| 141 |
+
]},
|
| 142 |
+
{ type: "Verse 2", lines: [
|
| 143 |
+
{ chords: "Em G C D", lyrics: "I look in your eyes, what do I see?" },
|
| 144 |
+
{ chords: "Em G C D", lyrics: "The cult of personality." }
|
| 145 |
+
]},
|
| 146 |
+
{ type: "Chorus 2", lines: [
|
| 147 |
+
{ chords: "Em G C D", lyrics: "Cult of Personality! Cult of Personality!" }
|
| 148 |
+
]},
|
| 149 |
+
{ type: "Outro", lines: [{ chords: "Em G C D", lyrics: "(repeated, building to a final hit on Em)" }] }
|
| 150 |
+
]
|
| 151 |
+
},
|
| 152 |
+
{
|
| 153 |
+
title: "Glamour Boys",
|
| 154 |
+
artist: "Living Colour",
|
| 155 |
+
sections: [
|
| 156 |
+
{ type: "Intro", lines: [{ chords: "E B A E", lyrics: "" }] },
|
| 157 |
+
{ type: "Verse 1", lines: [
|
| 158 |
+
{ chords: "E B", lyrics: "The Glamour Boys, they got the girls" },
|
| 159 |
+
{ chords: "A E", lyrics: "They got the cars, they got the pearls" }
|
| 160 |
+
]},
|
| 161 |
+
{ type: "Chorus 1", lines: [
|
| 162 |
+
{ chords: "E B A E", lyrics: "Glamour Boys, oh, the Glamour Boys, what makes you think you're so cool?" }
|
| 163 |
+
]},
|
| 164 |
+
{ type: "Verse 2", lines: [
|
| 165 |
+
{ chords: "E B", lyrics: "The Glamour Boys, they're so hip" },
|
| 166 |
+
{ chords: "A E", lyrics: "They got the style, they got the trip" }
|
| 167 |
+
]},
|
| 168 |
+
{ type: "Chorus 2", lines: [
|
| 169 |
+
{ chords: "E B A E", lyrics: "Glamour Boys, oh, the Glamour Boys, what makes you think you're so cool?" }
|
| 170 |
+
]},
|
| 171 |
+
{ type: "Outro", lines: [{ chords: "E B A E", lyrics: "(repeat and fade with guitar licks)" }] }
|
| 172 |
+
]
|
| 173 |
+
},
|
| 174 |
+
{
|
| 175 |
+
title: "Ocean Size",
|
| 176 |
+
artist: "Jane's Addiction",
|
| 177 |
+
sections: [
|
| 178 |
+
{ type: "Intro", lines: [{ chords: "C G Am F", lyrics: "" }] },
|
| 179 |
+
{ type: "Verse 1", lines: [
|
| 180 |
+
{ chords: "C G Am F", lyrics: "Day by day, as the weeks turn to months, I watch you..." },
|
| 181 |
+
{ chords: "C G Am F", lyrics: "Growing taller, growing wiser, growing stronger..." }
|
| 182 |
+
]},
|
| 183 |
+
{ type: "Chorus 1", lines: [
|
| 184 |
+
{ chords: "C G D F", lyrics: "Ocean size, it's the ocean size" },
|
| 185 |
+
{ chords: "C G D F", lyrics: "The ocean size, it's the ocean size" }
|
| 186 |
+
]},
|
| 187 |
+
{ type: "Verse 2", lines: [
|
| 188 |
+
{ chords: "C G Am F", lyrics: "Day by day, as the weeks turn to months, I watch you..." },
|
| 189 |
+
{ chords: "C G Am F", lyrics: "Growing stronger, growing wiser, growing taller..." }
|
| 190 |
+
]},
|
| 191 |
+
{ type: "Chorus 2", lines: [
|
| 192 |
+
{ chords: "C G D F", lyrics: "Ocean size, it's the ocean size" },
|
| 193 |
+
{ chords: "C G D F", lyrics: "The ocean size, it's the ocean size" }
|
| 194 |
+
]},
|
| 195 |
+
{ type: "Outro", lines: [{ chords: "C G Am F", lyrics: "(repeat and fade)" }] }
|
| 196 |
+
]
|
| 197 |
+
},
|
| 198 |
+
{
|
| 199 |
+
title: "Mountain Song",
|
| 200 |
+
artist: "Jane's Addiction",
|
| 201 |
+
sections: [
|
| 202 |
+
{ type: "Intro", lines: [{ chords: "Em C G D", lyrics: "" }] },
|
| 203 |
+
{ type: "Verse 1", lines: [
|
| 204 |
+
{ chords: "Em C G D", lyrics: "Coming down the mountain, I saw a girl" },
|
| 205 |
+
{ chords: "Em C G D", lyrics: "She was looking at me, in my world" }
|
| 206 |
+
]},
|
| 207 |
+
{ type: "Chorus 1", lines: [
|
| 208 |
+
{ chords: "Em C G D", lyrics: "Mountain Song! Mountain Song!" }
|
| 209 |
+
]},
|
| 210 |
+
{ type: "Verse 2", lines: [
|
| 211 |
+
{ chords: "Em C G D", lyrics: "She said, \"Where'd you come from? Where'd you go?\"" },
|
| 212 |
+
{ chords: "Em C G D", lyrics: "\"I came from the mountain, don't you know?\"" }
|
| 213 |
+
]},
|
| 214 |
+
{ type: "Chorus 2", lines: [
|
| 215 |
+
{ chords: "Em C G D", lyrics: "Mountain Song! Mountain Song!" }
|
| 216 |
+
]},
|
| 217 |
+
{ type: "Outro", lines: [{ chords: "Em C G D", lyrics: "(repeat and fade)" }] }
|
| 218 |
+
]
|
| 219 |
+
},
|
| 220 |
+
{
|
| 221 |
+
title: "Where Is My Mind?",
|
| 222 |
+
artist: "Pixies",
|
| 223 |
+
sections: [
|
| 224 |
+
{ type: "Intro", lines: [{ chords: "C G Am F", lyrics: "" }] },
|
| 225 |
+
{ type: "Verse 1", lines: [
|
| 226 |
+
{ chords: "C G Am F", lyrics: "With your feet on the air and your head on the ground..." },
|
| 227 |
+
{ chords: "C G Am F", lyrics: "Try this trick and spin it, yeah..." }
|
| 228 |
+
]},
|
| 229 |
+
{ type: "Chorus 1", lines: [
|
| 230 |
+
{ chords: "C G Am F", lyrics: "Where is my mind? Where is my mind?" },
|
| 231 |
+
{ chords: "C G Am F", lyrics: "Where is my mind? Way out in the water, see it swimming..." }
|
| 232 |
+
]},
|
| 233 |
+
{ type: "Verse 2", lines: [
|
| 234 |
+
{ chords: "C G Am F", lyrics: "I was thinking about you and the things we've done..." },
|
| 235 |
+
{ chords: "C G Am F", lyrics: "And all the places we've been to, oh yeah..." }
|
| 236 |
+
]},
|
| 237 |
+
{ type: "Chorus 2", lines: [
|
| 238 |
+
{ chords: "C G Am F", lyrics: "Where is my mind? Where is my mind?" },
|
| 239 |
+
{ chords: "C G Am F", lyrics: "Where is my mind? Way out in the water, see it swimming..." }
|
| 240 |
+
]},
|
| 241 |
+
{ type: "Outro", lines: [{ chords: "C G Am F", lyrics: "(repeat and fade)" }] }
|
| 242 |
+
]
|
| 243 |
+
},
|
| 244 |
+
{
|
| 245 |
+
title: "Fisherman's Blues",
|
| 246 |
+
artist: "The Waterboys",
|
| 247 |
+
sections: [
|
| 248 |
+
{ type: "Intro", lines: [{ chords: "G C D G", lyrics: "" }] },
|
| 249 |
+
{ type: "Verse 1", lines: [
|
| 250 |
+
{ chords: "G C D G", lyrics: "I wish I was a fisherman, tumbling on the sea" },
|
| 251 |
+
{ chords: "G C D G", lyrics: "Far away from dry land, and its bitter misery" }
|
| 252 |
+
]},
|
| 253 |
+
{ type: "Chorus 1", lines: [
|
| 254 |
+
{ chords: "G C D G", lyrics: "I'm gonna make a record, a record of my dreams" },
|
| 255 |
+
{ chords: "G C D G", lyrics: "And let the wind and the waves sing along to the themes" }
|
| 256 |
+
]},
|
| 257 |
+
{ type: "Verse 2", lines: [
|
| 258 |
+
{ chords: "G C D G", lyrics: "I wish I was a fisherman, out on the rolling deep" },
|
| 259 |
+
{ chords: "G C D G", lyrics: "With nothing but the stars to guide me, while the city sleeps" }
|
| 260 |
+
]},
|
| 261 |
+
{ type: "Chorus 2", lines: [
|
| 262 |
+
{ chords: "G C D G", lyrics: "I'm gonna make a record, a record of my dreams" },
|
| 263 |
+
{ chords: "G C D G", lyrics: "And let the wind and the waves sing along to the themes" }
|
| 264 |
+
]},
|
| 265 |
+
{ type: "Outro", lines: [{ chords: "G C D G", lyrics: "(repeat and fade)" }] }
|
| 266 |
+
]
|
| 267 |
+
},
|
| 268 |
+
{
|
| 269 |
+
title: "Express Yourself",
|
| 270 |
+
artist: "N.W.A.",
|
| 271 |
+
sections: [
|
| 272 |
+
{ type: "Intro", lines: [{ chords: "F Am Dm C", lyrics: "" }] },
|
| 273 |
+
{ type: "Verse 1", lines: [
|
| 274 |
+
{ chords: "F Am Dm C", lyrics: "I'm expressing with my full capabilities, and now I'm living in reality..." },
|
| 275 |
+
{ chords: "F Am Dm C", lyrics: "The only solution is to get involved and move it..." }
|
| 276 |
+
]},
|
| 277 |
+
{ type: "Chorus 1", lines: [
|
| 278 |
+
{ chords: "F Am Dm C", lyrics: "Express yourself! Express yourself!" },
|
| 279 |
+
{ chords: "F Am Dm C", lyrics: "Express yourself! It's a brand new thing..." }
|
| 280 |
+
]},
|
| 281 |
+
{ type: "Verse 2", lines: [
|
| 282 |
+
{ chords: "F Am Dm C", lyrics: "Now I'm the one, I'm the one, I'm the one that you know..." },
|
| 283 |
+
{ chords: "F Am Dm C", lyrics: "Coming to get you, coming to get you, coming to get you, watch me go..." }
|
| 284 |
+
]},
|
| 285 |
+
{ type: "Chorus 2", lines: [
|
| 286 |
+
{ chords: "F Am Dm C", lyrics: "Express yourself! Express yourself!" },
|
| 287 |
+
{ chords: "F Am Dm C", lyrics: "Express yourself! It's a brand new thing..." }
|
| 288 |
+
]},
|
| 289 |
+
{ type: "Outro", lines: [{ chords: "F Am Dm C", lyrics: "(repeat and fade)" }] }
|
| 290 |
+
]
|
| 291 |
+
},
|
| 292 |
+
{
|
| 293 |
+
title: "One",
|
| 294 |
+
artist: "Metallica",
|
| 295 |
+
sections: [
|
| 296 |
+
{ type: "Intro", lines: [{ chords: "Am G C F", lyrics: "(Acoustic Intro/Verse Part)" }] },
|
| 297 |
+
{ type: "Verse 1", lines: [
|
| 298 |
+
{ chords: "Am G C F", lyrics: "I can't remember anything, can't tell if this is true or dream..." },
|
| 299 |
+
{ chords: "Am G C F", lyrics: "Deep down inside I feel the scream..." }
|
| 300 |
+
]},
|
| 301 |
+
{ type: "Chorus 1", lines: [
|
| 302 |
+
{ chords: "Am G C F", lyrics: "Hold my breath as I wish for death..." }
|
| 303 |
+
]},
|
| 304 |
+
{ type: "Verse 2", lines: [
|
| 305 |
+
{ chords: "Am G C F", lyrics: "Life it seems will fade away, drifting further every day..." },
|
| 306 |
+
{ chords: "Am G C F", lyrics: "Getting lost within myself, nothing matters, no one else..." }
|
| 307 |
+
]},
|
| 308 |
+
{ type: "Chorus 2", lines: [
|
| 309 |
+
{ chords: "Am G C F", lyrics: "All the thoughts that I have now..." }
|
| 310 |
+
]},
|
| 311 |
+
{ type: "Outro", lines: [{ chords: "E5 - D5 - C5 - A5", lyrics: "(heavy riffing, fast tempo, ends on E5)" }] }
|
| 312 |
+
]
|
| 313 |
+
},
|
| 314 |
+
{
|
| 315 |
+
title: "Handle with Care",
|
| 316 |
+
artist: "Traveling Wilburys",
|
| 317 |
+
sections: [
|
| 318 |
+
{ type: "Intro", lines: [{ chords: "G D Em C", lyrics: "" }] },
|
| 319 |
+
{ type: "Verse 1", lines: [
|
| 320 |
+
{ chords: "G D Em C", lyrics: "Been beat up and battered 'round, been sent up, and I've been shot down..." },
|
| 321 |
+
{ chords: "G D Em C", lyrics: "You're the best thing that I've found, handle me with care." }
|
| 322 |
+
]},
|
| 323 |
+
{ type: "Chorus 1", lines: [
|
| 324 |
+
{ chords: "G D Em C", lyrics: "Handle me with care, handle me with care" },
|
| 325 |
+
{ chords: "G D Em C", lyrics: "Handle me with care, handle me with care" }
|
| 326 |
+
]},
|
| 327 |
+
{ type: "Verse 2", lines: [
|
| 328 |
+
{ chords: "G D Em C", lyrics: "I been stuck in so much traffic, I'm a mess, a nervous wreck..." },
|
| 329 |
+
{ chords: "G D Em C", lyrics: "I could use some tender love and care, handle me with care." }
|
| 330 |
+
]},
|
| 331 |
+
{ type: "Chorus 2", lines: [
|
| 332 |
+
{ chords: "G D Em C", lyrics: "Handle me with care, handle me with care" },
|
| 333 |
+
{ chords: "G D Em C", lyrics: "Handle me with care, handle me with care" }
|
| 334 |
+
]},
|
| 335 |
+
{ type: "Outro", lines: [{ chords: "G D Em C", lyrics: "(repeat and fade)" }] }
|
| 336 |
+
]
|
| 337 |
+
}
|
| 338 |
+
];
|
| 339 |
+
|
| 340 |
+
// --- Global State Variables ---
|
| 341 |
+
let currentSongIndex = 0;
|
| 342 |
+
let currentTempo = 90; // Default to Medium
|
| 343 |
+
let isInteractiveMode = false;
|
| 344 |
+
let activeSectionIndex = 0;
|
| 345 |
+
let activeLineIndexInSection = 0;
|
| 346 |
+
let activeChordIndexInLine = 0;
|
| 347 |
+
let activeWordIndexInLine = 0;
|
| 348 |
+
let arpeggioDirection = 'rising'; // 'rising' or 'falling'
|
| 349 |
+
|
| 350 |
+
// --- DOM Elements ---
|
| 351 |
+
const songTitleEl = document.getElementById('songTitle');
|
| 352 |
+
const artistNameEl = document.getElementById('artistName');
|
| 353 |
+
const scrollContainerEl = document.getElementById('scrollContainer');
|
| 354 |
+
const prevSongBtnHeader = document.getElementById('prevSongBtnHeader');
|
| 355 |
+
const nextSongBtnHeader = document.getElementById('nextSongBtnHeader');
|
| 356 |
+
const slowTempoBtn = document.getElementById('slowTempoBtn');
|
| 357 |
+
const mediumTempoBtn = document.getElementById('mediumTempoBtn');
|
| 358 |
+
const fastTempoBtn = document.getElementById('fastTempoBtn');
|
| 359 |
+
const arpeggioDirectionBtn = document.getElementById('arpeggioDirectionBtn');
|
| 360 |
+
const startTeleprompterBtn = document.getElementById('startTeleprompterBtn');
|
| 361 |
+
|
| 362 |
+
// --- Tone.js Synth Initialization ---
|
| 363 |
+
let synth = null;
|
| 364 |
+
async function initializeSynth() {
|
| 365 |
+
if (!synth) {
|
| 366 |
+
synth = new Tone.PolySynth(Tone.Synth, {
|
| 367 |
+
envelope: {
|
| 368 |
+
attack: 0.02,
|
| 369 |
+
decay: 0.1,
|
| 370 |
+
sustain: 0.3,
|
| 371 |
+
release: 1
|
| 372 |
+
}
|
| 373 |
+
}).toDestination();
|
| 374 |
+
await Tone.start(); // Ensure audio context is running
|
| 375 |
+
console.log('Tone.js audio context started and synth initialized.');
|
| 376 |
+
}
|
| 377 |
+
}
|
| 378 |
+
|
| 379 |
+
// --- Tempo Presets ---
|
| 380 |
+
const tempos = {
|
| 381 |
+
slow: 30,
|
| 382 |
+
medium: 90,
|
| 383 |
+
fast: 128
|
| 384 |
+
};
|
| 385 |
+
|
| 386 |
+
// --- Helper: Chord to MIDI Note Mapping ---
|
| 387 |
+
const chordToMidi = (chordName) => {
|
| 388 |
+
const rootMap = {
|
| 389 |
+
'C': 60, 'C#': 61, 'Db': 61, 'D': 62, 'D#': 63, 'Eb': 63, 'E': 64, 'F': 65,
|
| 390 |
+
'F#': 66, 'Gb': 66, 'G': 67, 'G#': 68, 'Ab': 68, 'A': 69, 'A#': 70, 'Bb': 70, 'B': 71
|
| 391 |
+
};
|
| 392 |
+
|
| 393 |
+
let root = null;
|
| 394 |
+
let type = 'major'; // Default to major
|
| 395 |
+
let octaveOffset = 0; // Default to 4th octave for root
|
| 396 |
+
|
| 397 |
+
let baseChord = chordName.trim().replace('5', ''); // Handle power chords as root
|
| 398 |
+
const match = baseChord.match(/^([A-G][b#]?)/);
|
| 399 |
+
if (match) {
|
| 400 |
+
root = rootMap[match[1]];
|
| 401 |
+
baseChord = baseChord.substring(match[1].length); // Remove root for type parsing
|
| 402 |
+
} else {
|
| 403 |
+
return []; // Fallback for unparseable roots
|
| 404 |
+
}
|
| 405 |
+
|
| 406 |
+
// Determine chord type (simple major/minor/power for now)
|
| 407 |
+
if (baseChord.includes('m') || baseChord.includes('min')) {
|
| 408 |
+
type = 'minor';
|
| 409 |
+
} else if (baseChord.includes('dim')) {
|
| 410 |
+
type = 'diminished';
|
| 411 |
+
} else if (chordName.includes('5')) { // Explicit power chord check using original chordName
|
| 412 |
+
type = 'power';
|
| 413 |
+
}
|
| 414 |
+
|
| 415 |
+
if (root === null) return [];
|
| 416 |
+
|
| 417 |
+
const notes = [root + octaveOffset]; // Root
|
| 418 |
+
if (type === 'major') {
|
| 419 |
+
notes.push(root + 4 + octaveOffset); // Major 3rd
|
| 420 |
+
} else if (type === 'minor') {
|
| 421 |
+
notes.push(root + 3 + octaveOffset); // Minor 3rd
|
| 422 |
+
} else if (type === 'diminished') {
|
| 423 |
+
notes.push(root + 3 + octaveOffset); // Minor 3rd
|
| 424 |
+
}
|
| 425 |
+
notes.push(root + 7 + octaveOffset); // Perfect 5th
|
| 426 |
+
|
| 427 |
+
// Add the root an octave higher for a fuller sound in the arpeggio
|
| 428 |
+
notes.push(root + 12 + octaveOffset);
|
| 429 |
+
|
| 430 |
+
return notes;
|
| 431 |
+
};
|
| 432 |
+
|
| 433 |
+
// --- Helper: Play Arpeggio ---
|
| 434 |
+
async function playArpeggio(chordName, direction) {
|
| 435 |
+
await initializeSynth(); // Ensure synth is ready
|
| 436 |
+
|
| 437 |
+
Tone.Transport.stop();
|
| 438 |
+
Tone.Transport.cancel(); // Clear any existing schedules
|
| 439 |
+
|
| 440 |
+
const arpeggioNotes = chordToMidi(chordName);
|
| 441 |
+
if (arpeggioNotes.length === 0) return;
|
| 442 |
+
|
| 443 |
+
const notesToPlay = direction === 'rising' ? arpeggioNotes : [...arpeggioNotes].reverse();
|
| 444 |
+
|
| 445 |
+
const noteDuration = Tone.Time("8n").toSeconds(); // Each note in arpeggio is an 8th note
|
| 446 |
+
let startTime = Tone.Transport.now();
|
| 447 |
+
|
| 448 |
+
notesToPlay.forEach((midiNote, index) => {
|
| 449 |
+
synth.triggerAttackRelease(
|
| 450 |
+
Tone.Midi(midiNote).toNote(),
|
| 451 |
+
"16n", // Shorter attack for arpeggio notes
|
| 452 |
+
startTime + (index * noteDuration * 0.5) // Stagger notes
|
| 453 |
+
);
|
| 454 |
+
});
|
| 455 |
+
|
| 456 |
+
Tone.Transport.start(); // Start transport for this one-shot sequence
|
| 457 |
+
// Stop transport after a short delay, to ensure arpeggio finishes
|
| 458 |
+
Tone.Transport.scheduleOnce((time) => {
|
| 459 |
+
Tone.Transport.stop();
|
| 460 |
+
Tone.Transport.cancel();
|
| 461 |
+
}, `+${noteDuration * notesToPlay.length}`);
|
| 462 |
+
}
|
| 463 |
+
|
| 464 |
+
// --- Rendering Function ---
|
| 465 |
+
function renderSong() {
|
| 466 |
+
const song = songsData[currentSongIndex];
|
| 467 |
+
songTitleEl.textContent = song.title;
|
| 468 |
+
artistNameEl.textContent = song.artist;
|
| 469 |
+
scrollContainerEl.innerHTML = ''; // Clear previous content
|
| 470 |
+
|
| 471 |
+
song.sections.forEach((section, secIndex) => {
|
| 472 |
+
const sectionDiv = document.createElement('div');
|
| 473 |
+
sectionDiv.className = 'mb-8';
|
| 474 |
+
|
| 475 |
+
const sectionTitle = document.createElement('h2');
|
| 476 |
+
sectionTitle.className = 'text-3xl font-semibold text-yellow-300 mb-4 sticky top-0 bg-gray-900 py-2 z-10';
|
| 477 |
+
sectionTitle.textContent = section.type;
|
| 478 |
+
sectionDiv.appendChild(sectionTitle);
|
| 479 |
+
|
| 480 |
+
section.lines.forEach((line, lineIndex) => {
|
| 481 |
+
const lineDiv = document.createElement('div');
|
| 482 |
+
lineDiv.className = `mb-4`;
|
| 483 |
+
lineDiv.dataset.sectionIndex = secIndex;
|
| 484 |
+
lineDiv.dataset.lineIndex = lineIndex;
|
| 485 |
+
|
| 486 |
+
const chordsP = document.createElement('p');
|
| 487 |
+
chordsP.className = 'font-bold text-green-300 whitespace-pre';
|
| 488 |
+
const chordsInLine = line.chords.split(/\s+/).filter(c => c.length > 0);
|
| 489 |
+
chordsInLine.forEach((chord, chordMapIndex) => {
|
| 490 |
+
const chordSpan = document.createElement('span');
|
| 491 |
+
chordSpan.textContent = chord;
|
| 492 |
+
// Add spaces for visual alignment
|
| 493 |
+
chordSpan.innerHTML += ' '; // Use innerHTML for non-breaking space
|
| 494 |
+
chordSpan.dataset.chordIndex = chordMapIndex;
|
| 495 |
+
chordsP.appendChild(chordSpan);
|
| 496 |
+
});
|
| 497 |
+
lineDiv.appendChild(chordsP);
|
| 498 |
+
|
| 499 |
+
const lyricsP = document.createElement('p');
|
| 500 |
+
lyricsP.className = 'text-gray-100';
|
| 501 |
+
const wordsInLine = line.lyrics.split(/\s+/).filter(w => w.length > 0);
|
| 502 |
+
wordsInLine.forEach((word, wordMapIndex) => {
|
| 503 |
+
const wordSpan = document.createElement('span');
|
| 504 |
+
wordSpan.textContent = word;
|
| 505 |
+
wordSpan.innerHTML += ' '; // Add space between words
|
| 506 |
+
wordSpan.dataset.wordIndex = wordMapIndex;
|
| 507 |
+
lyricsP.appendChild(wordSpan);
|
| 508 |
+
});
|
| 509 |
+
lineDiv.appendChild(lyricsP);
|
| 510 |
+
|
| 511 |
+
sectionDiv.appendChild(lineDiv);
|
| 512 |
+
});
|
| 513 |
+
scrollContainerEl.appendChild(sectionDiv);
|
| 514 |
+
});
|
| 515 |
+
updateHighlighting(); // Initial highlighting
|
| 516 |
+
}
|
| 517 |
+
|
| 518 |
+
// --- Highlighting Logic ---
|
| 519 |
+
function updateHighlighting() {
|
| 520 |
+
// Clear all existing highlights
|
| 521 |
+
document.querySelectorAll('.text-red-400').forEach(el => el.classList.remove('text-red-400', 'transition-colors', 'duration-200'));
|
| 522 |
+
document.querySelectorAll('.text-blue-300.font-extrabold').forEach(el => el.classList.remove('text-blue-300', 'font-extrabold', 'transition-colors', 'duration-200'));
|
| 523 |
+
document.querySelectorAll('.bg-gray-800.rounded-md.p-2').forEach(el => el.classList.remove('bg-gray-800', 'rounded-md', 'p-2'));
|
| 524 |
+
|
| 525 |
+
|
| 526 |
+
if (isInteractiveMode) {
|
| 527 |
+
const activeLineDiv = scrollContainerEl.querySelector(
|
| 528 |
+
`[data-section-index="${activeSectionIndex}"][data-line-index="${activeLineIndexInSection}"]`
|
| 529 |
+
);
|
| 530 |
+
if (activeLineDiv) {
|
| 531 |
+
activeLineDiv.classList.add('bg-gray-800', 'rounded-md', 'p-2');
|
| 532 |
+
|
| 533 |
+
const activeChordSpan = activeLineDiv.querySelector(`p.text-green-300 span[data-chord-index="${activeChordIndexInLine}"]`);
|
| 534 |
+
if (activeChordSpan) {
|
| 535 |
+
activeChordSpan.classList.add('text-red-400', 'transition-colors', 'duration-200');
|
| 536 |
+
}
|
| 537 |
+
|
| 538 |
+
const activeWordSpan = activeLineDiv.querySelector(`p.text-gray-100 span[data-word-index="${activeWordIndexInLine}"]`);
|
| 539 |
+
if (activeWordSpan) {
|
| 540 |
+
activeWordSpan.classList.add('text-blue-300', 'font-extrabold', 'transition-colors', 'duration-200');
|
| 541 |
+
}
|
| 542 |
+
|
| 543 |
+
// Auto-scroll the active line into view if it's out of bounds
|
| 544 |
+
const containerHeight = scrollContainerEl.clientHeight;
|
| 545 |
+
const lineTop = activeLineDiv.offsetTop;
|
| 546 |
+
const lineBottom = lineTop + activeLineDiv.clientHeight;
|
| 547 |
+
|
| 548 |
+
if (lineTop < scrollContainerEl.scrollTop || lineBottom > scrollContainerEl.scrollTop + containerHeight) {
|
| 549 |
+
scrollContainerEl.scrollTop = lineTop - (containerHeight / 3); // Scroll to about a third down the screen
|
| 550 |
+
}
|
| 551 |
+
}
|
| 552 |
+
}
|
| 553 |
+
}
|
| 554 |
+
|
| 555 |
+
// --- Core KeyPress Logic (Teleprompter Advance) ---
|
| 556 |
+
function handleKeyPress(event) {
|
| 557 |
+
if (!isInteractiveMode) return;
|
| 558 |
+
|
| 559 |
+
if (event.code === 'Space' || event.code === 'ArrowRight') {
|
| 560 |
+
event.preventDefault(); // Prevent page scrolling
|
| 561 |
+
|
| 562 |
+
const currentSong = songsData[currentSongIndex];
|
| 563 |
+
const currentSection = currentSong.sections[activeSectionIndex];
|
| 564 |
+
const currentLine = currentSection?.lines[activeLineIndexInSection];
|
| 565 |
+
const chordsInCurrentLine = currentLine?.chords.split(/\s+/).filter(c => c.length > 0) || [];
|
| 566 |
+
const wordsInCurrentLine = currentLine?.lyrics.split(/\s+/).filter(w => w.length > 0) || [];
|
| 567 |
+
|
| 568 |
+
// Play arpeggio for the current chord before advancing
|
| 569 |
+
const chordToPlay = chordsInCurrentLine[activeChordIndexInLine];
|
| 570 |
+
if (chordToPlay) {
|
| 571 |
+
playArpeggio(chordToPlay, arpeggioDirection);
|
| 572 |
+
}
|
| 573 |
+
|
| 574 |
+
let newChordIndex = activeChordIndexInLine + 1;
|
| 575 |
+
let newWordIndex = activeWordIndexInLine + 1;
|
| 576 |
+
let newActiveLineIndex = activeLineIndexInSection;
|
| 577 |
+
let newActiveSectionIndex = activeSectionIndex;
|
| 578 |
+
|
| 579 |
+
const allWordsCovered = newWordIndex >= wordsInCurrentLine.length;
|
| 580 |
+
const allChordsCovered = newChordIndex >= chordsInCurrentLine.length;
|
| 581 |
+
|
| 582 |
+
if (allWordsCovered && allChordsCovered) {
|
| 583 |
+
// Both words and chords on the current line are covered, move to next line
|
| 584 |
+
newActiveLineIndex++;
|
| 585 |
+
newChordIndex = 0;
|
| 586 |
+
newWordIndex = 0;
|
| 587 |
+
|
| 588 |
+
if (newActiveLineIndex >= currentSection.lines.length) {
|
| 589 |
+
// All lines in current section covered, move to next section
|
| 590 |
+
newActiveSectionIndex++;
|
| 591 |
+
newActiveLineIndex = 0; // Reset line index for new section
|
| 592 |
+
|
| 593 |
+
if (newActiveSectionIndex >= currentSong.sections.length) {
|
| 594 |
+
// End of song
|
| 595 |
+
console.log("End of song!");
|
| 596 |
+
isInteractiveMode = false; // Exit interactive mode
|
| 597 |
+
activeSectionIndex = 0;
|
| 598 |
+
activeLineIndexInSection = 0;
|
| 599 |
+
activeChordIndexInLine = 0;
|
| 600 |
+
activeWordIndexInLine = 0;
|
| 601 |
+
Tone.Transport.stop();
|
| 602 |
+
Tone.Transport.cancel();
|
| 603 |
+
updateHighlighting(); // Clear final highlights
|
| 604 |
+
return;
|
| 605 |
+
}
|
| 606 |
+
}
|
| 607 |
+
} else if (newChordIndex >= chordsInCurrentLine.length && !allWordsCovered) {
|
| 608 |
+
// All chords on line covered, but words remain. Advance words only.
|
| 609 |
+
// Keep chord index at the last chord.
|
| 610 |
+
newChordIndex = chordsInCurrentLine.length > 0 ? chordsInCurrentLine.length - 1 : 0;
|
| 611 |
+
} else if (newWordIndex >= wordsInCurrentLine.length && !allChordsCovered) {
|
| 612 |
+
// All words on line covered, but chords remain. Advance chords only.
|
| 613 |
+
// Keep word index at the last word.
|
| 614 |
+
newWordIndex = wordsInCurrentLine.length > 0 ? wordsInCurrentLine.length - 1 : 0;
|
| 615 |
+
}
|
| 616 |
+
|
| 617 |
+
|
| 618 |
+
activeChordIndexInLine = newChordIndex;
|
| 619 |
+
activeWordIndexInLine = newWordIndex;
|
| 620 |
+
activeLineIndexInSection = newActiveLineIndex;
|
| 621 |
+
activeSectionIndex = newActiveSectionIndex;
|
| 622 |
+
|
| 623 |
+
updateHighlighting();
|
| 624 |
+
}
|
| 625 |
+
}
|
| 626 |
+
|
| 627 |
+
// --- Event Handlers ---
|
| 628 |
+
function handleSongChange(direction) {
|
| 629 |
+
isInteractiveMode = false; // Exit interactive mode on song change
|
| 630 |
+
Tone.Transport.stop();
|
| 631 |
+
Tone.Transport.cancel();
|
| 632 |
+
|
| 633 |
+
if (direction === 'next') {
|
| 634 |
+
currentSongIndex = (currentSongIndex + 1) % songsData.length;
|
| 635 |
+
} else if (direction === 'prev') {
|
| 636 |
+
currentSongIndex = (currentSongIndex - 1 + songsData.length) % songsData.length;
|
| 637 |
+
}
|
| 638 |
+
|
| 639 |
+
// Reset teleprompter state for the new song
|
| 640 |
+
activeSectionIndex = 0;
|
| 641 |
+
activeLineIndexInSection = 0;
|
| 642 |
+
activeChordIndexInLine = 0;
|
| 643 |
+
activeWordIndexInLine = 0;
|
| 644 |
+
scrollContainerEl.scrollTop = 0; // Scroll to top
|
| 645 |
+
|
| 646 |
+
renderSong(); // Re-render the new song
|
| 647 |
+
}
|
| 648 |
+
|
| 649 |
+
function setTempo(tempoValue) {
|
| 650 |
+
currentTempo = tempoValue;
|
| 651 |
+
Tone.Transport.bpm.value = tempoValue;
|
| 652 |
+
// Update button styles
|
| 653 |
+
slowTempoBtn.classList.remove('bg-purple-700');
|
| 654 |
+
mediumTempoBtn.classList.remove('bg-purple-700');
|
| 655 |
+
fastTempoBtn.classList.remove('bg-purple-700');
|
| 656 |
+
slowTempoBtn.classList.add('bg-purple-500', 'hover:bg-purple-600');
|
| 657 |
+
mediumTempoBtn.classList.add('bg-purple-500', 'hover:bg-purple-600');
|
| 658 |
+
fastTempoBtn.classList.add('bg-purple-500', 'hover:bg-purple-600');
|
| 659 |
+
|
| 660 |
+
if (tempoValue === tempos.slow) slowTempoBtn.classList.replace('bg-purple-500', 'bg-purple-700');
|
| 661 |
+
else if (tempoValue === tempos.medium) mediumTempoBtn.classList.replace('bg-purple-500', 'bg-purple-700');
|
| 662 |
+
else if (tempoValue === tempos.fast) fastTempoBtn.classList.replace('bg-purple-500', 'bg-purple-700');
|
| 663 |
+
}
|
| 664 |
+
|
| 665 |
+
function toggleArpeggioDirection() {
|
| 666 |
+
arpeggioDirection = arpeggioDirection === 'rising' ? 'falling' : 'rising';
|
| 667 |
+
arpeggioDirectionBtn.textContent = `Arpeggio: ${arpeggioDirection.charAt(0).toUpperCase() + arpeggioDirection.slice(1)}`;
|
| 668 |
+
}
|
| 669 |
+
|
| 670 |
+
function startTeleprompter() {
|
| 671 |
+
isInteractiveMode = true;
|
| 672 |
+
activeSectionIndex = 0;
|
| 673 |
+
activeLineIndexInSection = 0;
|
| 674 |
+
activeChordIndexInLine = 0;
|
| 675 |
+
activeWordIndexInLine = 0;
|
| 676 |
+
scrollContainerEl.scrollTop = 0; // Scroll to top on start
|
| 677 |
+
initializeSynth(); // Ensure synth is ready
|
| 678 |
+
Tone.Transport.stop(); // Stop any previous playback
|
| 679 |
+
Tone.Transport.cancel();
|
| 680 |
+
Tone.Transport.bpm.value = currentTempo; // Set tempo for interactive playback
|
| 681 |
+
updateHighlighting(); // Apply initial highlight
|
| 682 |
+
}
|
| 683 |
|
| 684 |
+
// --- Initial Setup and Event Listeners ---
|
| 685 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 686 |
+
renderSong(); // Render the first song on load
|
| 687 |
+
setTempo(tempos.medium); // Set initial tempo button state
|
| 688 |
+
|
| 689 |
+
// Attach event listeners
|
| 690 |
+
prevSongBtnHeader.addEventListener('click', () => handleSongChange('prev'));
|
| 691 |
+
nextSongBtnHeader.addEventListener('click', () => handleSongChange('next'));
|
| 692 |
+
slowTempoBtn.addEventListener('click', () => setTempo(tempos.slow));
|
| 693 |
+
mediumTempoBtn.addEventListener('click', () => setTempo(tempos.medium));
|
| 694 |
+
fastTempoBtn.addEventListener('click', () => setTempo(tempos.fast));
|
| 695 |
+
arpeggioDirectionBtn.addEventListener('click', toggleArpeggioDirection);
|
| 696 |
+
startTeleprompterBtn.addEventListener('click', startTeleprompter);
|
| 697 |
+
|
| 698 |
+
window.addEventListener('keydown', handleKeyPress);
|
| 699 |
+
});
|
| 700 |
+
</script>
|
| 701 |
+
</body>
|
| 702 |
+
</html>
|