awacke1 commited on
Commit
fe8620d
·
verified ·
1 Parent(s): faac71f

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +695 -514
index.html CHANGED
@@ -1,521 +1,702 @@
1
- import React, { useState, useRef, useEffect, useCallback } from 'react';
2
- import * as Tone from 'tone'; // Import Tone.js
3
-
4
- // Song data parsed from your previous responses
5
- const songsData = [
6
- {
7
- title: "Talkin' 'bout a Revolution",
8
- artist: "Tracy Chapman",
9
- sections: [
10
- { type: "Intro", lines: [{ chords: "G C D G", lyrics: "" }] },
11
- { type: "Verse 1", lines: [
12
- { chords: "G C D G", lyrics: "While outside a revolution's talking..." },
13
- { chords: "G C D G", lyrics: "It's gonna come, it's gonna come..." }
14
- ]},
15
- { type: "Chorus 1", lines: [
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
- }).toDestination();
343
- }
344
-
345
- // Cleanup function
346
- return () => {
347
- if (synthRef.current) {
348
- synthRef.current.dispose();
349
- synthRef.current = null;
350
- }
351
- if (playSequenceRef.current) {
352
- playSequenceRef.current.stop();
353
- playSequenceRef.current.dispose();
354
- playSequenceRef.current = null;
355
- }
356
- Tone.Transport.stop(); // Stop transport on unmount
357
- };
358
- }, []); // Run once on mount
359
-
360
- const currentSong = songsData[currentSongIndex];
361
-
362
- const handleNextSong = () => {
363
- if (playSequenceRef.current) {
364
- playSequenceRef.current.stop(); // Stop any current playback
365
- }
366
- setCurrentSongIndex((prevIndex) => (prevIndex + 1) % songsData.length);
367
- };
368
-
369
- const handlePrevSong = () => {
370
- if (playSequenceRef.current) {
371
- playSequenceRef.current.stop(); // Stop any current playback
372
- }
373
- setCurrentSongIndex((prevIndex) => (prevIndex - 1 + songsData.length) % songsData.length);
374
- };
375
-
376
- const setTempo = (tempoValue) => {
377
- setCurrentTempo(tempoValue);
378
- Tone.Transport.bpm.value = tempoValue; // Update Tone.js Transport BPM
379
- if (playSequenceRef.current && Tone.Transport.state === 'started') {
380
- // If playing, restart the sequence to apply tempo immediately
381
- playChords(); // This will stop and restart with new tempo
382
- }
383
- };
384
-
385
-
386
- const playChords = useCallback(async () => {
387
- if (!synthRef.current) {
388
- console.error("Synth not initialized.");
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
- onClick={() => setTempo(tempos.medium)}
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
- onClick={() => setTempo(tempos.fast)}
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
- onClick={playChords}
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
- onClick={handleNextSong}
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
- </footer>
517
- </div>
518
- );
519
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
520
 
521
- export default App;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ &#9650; <!-- 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
+ &#9660; <!-- 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 += '&nbsp;&nbsp;'; // 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 += '&nbsp;'; // 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>