Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Chord Sheet Teleprompter</title> | |
<!-- Tailwind CSS CDN --> | |
<script src="https://cdn.tailwindcss.com"></script> | |
<!-- Tone.js CDN --> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/tone/14.8.49/Tone.min.js"></script> | |
<style> | |
body { | |
font-family: "Inter", sans-serif; | |
margin: 0; | |
overflow: hidden; /* Prevent body scroll, main content handles scroll */ | |
} | |
/* Custom scrollbar for main content */ | |
.custom-scrollbar::-webkit-scrollbar { | |
width: 8px; | |
} | |
.custom-scrollbar::-webkit-scrollbar-track { | |
background: #2d3748; /* bg-gray-800 */ | |
} | |
.custom-scrollbar::-webkit-scrollbar-thumb { | |
background: #4a5568; /* bg-gray-600 */ | |
border-radius: 4px; | |
} | |
.custom-scrollbar::-webkit-scrollbar-thumb:hover { | |
background: #6b7280; /* bg-gray-500 */ | |
} | |
/* Preserve whitespace and prevent wrapping for chords */ | |
.whitespace-pre { | |
white-space: pre; | |
} | |
</style> | |
</head> | |
<body class="bg-gray-900 text-gray-100 flex flex-col h-screen"> | |
<!-- Header Section --> | |
<header class="p-4 bg-gray-800 shadow-md text-center rounded-b-lg flex flex-col items-center justify-center"> | |
<div class="flex items-center justify-center space-x-4 mb-2"> | |
<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"> | |
▲ <!-- Up Arrow --> | |
</button> | |
<div class="text-center"> | |
<h1 id="songTitle" class="text-3xl font-bold text-blue-400"></h1> | |
<p id="artistName" class="text-xl text-gray-300"></p> | |
</div> | |
<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"> | |
▼ <!-- Down Arrow --> | |
</button> | |
</div> | |
</header> | |
<!-- Main Content Area - Lyrics and Chords --> | |
<main id="scrollContainer" class="flex-1 overflow-y-auto p-6 text-2xl leading-relaxed custom-scrollbar"> | |
<!-- Song content will be rendered here by JavaScript --> | |
</main> | |
<!-- Footer Section - Controls --> | |
<footer class="p-4 bg-gray-800 shadow-t-md flex justify-center items-center space-x-4 rounded-t-lg flex-wrap"> | |
<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"> | |
Slow (30 BPM) | |
</button> | |
<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"> | |
Medium (90 BPM) | |
</button> | |
<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"> | |
Fast (128 BPM) | |
</button> | |
<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"> | |
Arpeggio: Rising | |
</button> | |
<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"> | |
Start/Restart Teleprompter (Press Space/Right Arrow) | |
</button> | |
</footer> | |
<script> | |
// Song data | |
const songsData = [ | |
{ | |
title: "Talkin' 'bout a Revolution", | |
artist: "Tracy Chapman", | |
sections: [ | |
{ type: "Intro", lines: [{ chords: "G C D G", lyrics: "" }] }, | |
{ type: "Verse 1", lines: [ | |
{ chords: "G C D G", lyrics: "While outside a revolution's talking..." }, | |
{ chords: "G C D G", lyrics: "It's gonna come, it's gonna come..." } | |
]}, | |
{ type: "Chorus 1", lines: [ | |
{ chords: "G C D G", lyrics: "Talkin' 'bout a revolution, oh, no" }, | |
{ chords: "G C D G", lyrics: "Talkin' 'bout a revolution..." } | |
]}, | |
{ type: "Verse 2", lines: [ | |
{ chords: "G C D G", lyrics: "While outside a revolution's talking..." }, | |
{ chords: "G C D G", lyrics: "It's gonna come, it's gonna come..." } | |
]}, | |
{ type: "Chorus 2", lines: [ | |
{ chords: "G C D G", lyrics: "Talkin' 'bout a revolution, oh, no" }, | |
{ chords: "G C D G", lyrics: "Talkin' 'bout a revolution..." } | |
]}, | |
{ type: "Outro", lines: [{ chords: "G C D G", lyrics: "(repeat and fade)" }] } | |
] | |
}, | |
{ | |
title: "Fast Car", | |
artist: "Tracy Chapman", | |
sections: [ | |
{ type: "Intro", lines: [{ chords: "C G Am F", lyrics: "" }] }, | |
{ type: "Verse 1", lines: [ | |
{ chords: "C G Am F", lyrics: "You got a fast car, I want a ticket to anywhere..." }, | |
{ chords: "C G Am F", lyrics: "We go driving in it, anywhere, maybe we'll make a deal..." } | |
]}, | |
{ type: "Chorus 1", lines: [ | |
{ chords: "C G Am F", lyrics: "So remember when we were driving, driving in your car..." }, | |
{ chords: "C G Am F", lyrics: "The speed of light, we gotta go, go, go, go, go..." } | |
]}, | |
{ type: "Verse 2", lines: [ | |
{ chords: "C G Am F", lyrics: "You got a fast car, I got a plan to get us out of here..." }, | |
{ chords: "C G Am F", lyrics: "I been working at the convenience store, so slow..." } | |
]}, | |
{ type: "Chorus 2", lines: [ | |
{ chords: "C G Am F", lyrics: "So remember when we were driving, driving in your car..." }, | |
{ chords: "C G Am F", lyrics: "The speed of light, we gotta go, go, go, go, go..." } | |
]}, | |
{ type: "Outro", lines: [{ chords: "C G Am F", lyrics: "(repeat and fade)" }] } | |
] | |
}, | |
{ | |
title: "Cult of Personality", | |
artist: "Living Colour", | |
sections: [ | |
{ type: "Intro", lines: [{ chords: "Em G C D", lyrics: "" }] }, | |
{ type: "Verse 1", lines: [ | |
{ chords: "Em G C D", lyrics: "Look in my eyes, what do you see?" }, | |
{ chords: "Em G C D", lyrics: "The cult of personality." } | |
]}, | |
{ type: "Chorus 1", lines: [ | |
{ chords: "Em G C D", lyrics: "Cult of Personality! Cult of Personality!" } | |
]}, | |
{ type: "Verse 2", lines: [ | |
{ chords: "Em G C D", lyrics: "I look in your eyes, what do I see?" }, | |
{ chords: "Em G C D", lyrics: "The cult of personality." } | |
]}, | |
{ type: "Chorus 2", lines: [ | |
{ chords: "Em G C D", lyrics: "Cult of Personality! Cult of Personality!" } | |
]}, | |
{ type: "Outro", lines: [{ chords: "Em G C D", lyrics: "(repeated, building to a final hit on Em)" }] } | |
] | |
}, | |
{ | |
title: "Glamour Boys", | |
artist: "Living Colour", | |
sections: [ | |
{ type: "Intro", lines: [{ chords: "E B A E", lyrics: "" }] }, | |
{ type: "Verse 1", lines: [ | |
{ chords: "E B", lyrics: "The Glamour Boys, they got the girls" }, | |
{ chords: "A E", lyrics: "They got the cars, they got the pearls" } | |
]}, | |
{ type: "Chorus 1", lines: [ | |
{ chords: "E B A E", lyrics: "Glamour Boys, oh, the Glamour Boys, what makes you think you're so cool?" } | |
]}, | |
{ type: "Verse 2", lines: [ | |
{ chords: "E B", lyrics: "The Glamour Boys, they're so hip" }, | |
{ chords: "A E", lyrics: "They got the style, they got the trip" } | |
]}, | |
{ type: "Chorus 2", lines: [ | |
{ chords: "E B A E", lyrics: "Glamour Boys, oh, the Glamour Boys, what makes you think you're so cool?" } | |
]}, | |
{ type: "Outro", lines: [{ chords: "E B A E", lyrics: "(repeat and fade with guitar licks)" }] } | |
] | |
}, | |
{ | |
title: "Ocean Size", | |
artist: "Jane's Addiction", | |
sections: [ | |
{ type: "Intro", lines: [{ chords: "C G Am F", lyrics: "" }] }, | |
{ type: "Verse 1", lines: [ | |
{ chords: "C G Am F", lyrics: "Day by day, as the weeks turn to months, I watch you..." }, | |
{ chords: "C G Am F", lyrics: "Growing taller, growing wiser, growing stronger..." } | |
]}, | |
{ type: "Chorus 1", lines: [ | |
{ chords: "C G D F", lyrics: "Ocean size, it's the ocean size" }, | |
{ chords: "C G D F", lyrics: "The ocean size, it's the ocean size" } | |
]}, | |
{ type: "Verse 2", lines: [ | |
{ chords: "C G Am F", lyrics: "Day by day, as the weeks turn to months, I watch you..." }, | |
{ chords: "C G Am F", lyrics: "Growing stronger, growing wiser, growing taller..." } | |
]}, | |
{ type: "Chorus 2", lines: [ | |
{ chords: "C G D F", lyrics: "Ocean size, it's the ocean size" }, | |
{ chords: "C G D F", lyrics: "The ocean size, it's the ocean size" } | |
]}, | |
{ type: "Outro", lines: [{ chords: "C G Am F", lyrics: "(repeat and fade)" }] } | |
] | |
}, | |
{ | |
title: "Mountain Song", | |
artist: "Jane's Addiction", | |
sections: [ | |
{ type: "Intro", lines: [{ chords: "Em C G D", lyrics: "" }] }, | |
{ type: "Verse 1", lines: [ | |
{ chords: "Em C G D", lyrics: "Coming down the mountain, I saw a girl" }, | |
{ chords: "Em C G D", lyrics: "She was looking at me, in my world" } | |
]}, | |
{ type: "Chorus 1", lines: [ | |
{ chords: "Em C G D", lyrics: "Mountain Song! Mountain Song!" } | |
]}, | |
{ type: "Verse 2", lines: [ | |
{ chords: "Em C G D", lyrics: "She said, \"Where'd you come from? Where'd you go?\"" }, | |
{ chords: "Em C G D", lyrics: "\"I came from the mountain, don't you know?\"" } | |
]}, | |
{ type: "Chorus 2", lines: [ | |
{ chords: "Em C G D", lyrics: "Mountain Song! Mountain Song!" } | |
]}, | |
{ type: "Outro", lines: [{ chords: "Em C G D", lyrics: "(repeat and fade)" }] } | |
] | |
}, | |
{ | |
title: "Where Is My Mind?", | |
artist: "Pixies", | |
sections: [ | |
{ type: "Intro", lines: [{ chords: "C G Am F", lyrics: "" }] }, | |
{ type: "Verse 1", lines: [ | |
{ chords: "C G Am F", lyrics: "With your feet on the air and your head on the ground..." }, | |
{ chords: "C G Am F", lyrics: "Try this trick and spin it, yeah..." } | |
]}, | |
{ type: "Chorus 1", lines: [ | |
{ chords: "C G Am F", lyrics: "Where is my mind? Where is my mind?" }, | |
{ chords: "C G Am F", lyrics: "Where is my mind? Way out in the water, see it swimming..." } | |
]}, | |
{ type: "Verse 2", lines: [ | |
{ chords: "C G Am F", lyrics: "I was thinking about you and the things we've done..." }, | |
{ chords: "C G Am F", lyrics: "And all the places we've been to, oh yeah..." } | |
]}, | |
{ type: "Chorus 2", lines: [ | |
{ chords: "C G Am F", lyrics: "Where is my mind? Where is my mind?" }, | |
{ chords: "C G Am F", lyrics: "Where is my mind? Way out in the water, see it swimming..." } | |
]}, | |
{ type: "Outro", lines: [{ chords: "C G Am F", lyrics: "(repeat and fade)" }] } | |
] | |
}, | |
{ | |
title: "Fisherman's Blues", | |
artist: "The Waterboys", | |
sections: [ | |
{ type: "Intro", lines: [{ chords: "G C D G", lyrics: "" }] }, | |
{ type: "Verse 1", lines: [ | |
{ chords: "G C D G", lyrics: "I wish I was a fisherman, tumbling on the sea" }, | |
{ chords: "G C D G", lyrics: "Far away from dry land, and its bitter misery" } | |
]}, | |
{ type: "Chorus 1", lines: [ | |
{ chords: "G C D G", lyrics: "I'm gonna make a record, a record of my dreams" }, | |
{ chords: "G C D G", lyrics: "And let the wind and the waves sing along to the themes" } | |
]}, | |
{ type: "Verse 2", lines: [ | |
{ chords: "G C D G", lyrics: "I wish I was a fisherman, out on the rolling deep" }, | |
{ chords: "G C D G", lyrics: "With nothing but the stars to guide me, while the city sleeps" } | |
]}, | |
{ type: "Chorus 2", lines: [ | |
{ chords: "G C D G", lyrics: "I'm gonna make a record, a record of my dreams" }, | |
{ chords: "G C D G", lyrics: "And let the wind and the waves sing along to the themes" } | |
]}, | |
{ type: "Outro", lines: [{ chords: "G C D G", lyrics: "(repeat and fade)" }] } | |
] | |
}, | |
{ | |
title: "Express Yourself", | |
artist: "N.W.A.", | |
sections: [ | |
{ type: "Intro", lines: [{ chords: "F Am Dm C", lyrics: "" }] }, | |
{ type: "Verse 1", lines: [ | |
{ chords: "F Am Dm C", lyrics: "I'm expressing with my full capabilities, and now I'm living in reality..." }, | |
{ chords: "F Am Dm C", lyrics: "The only solution is to get involved and move it..." } | |
]}, | |
{ type: "Chorus 1", lines: [ | |
{ chords: "F Am Dm C", lyrics: "Express yourself! Express yourself!" }, | |
{ chords: "F Am Dm C", lyrics: "Express yourself! It's a brand new thing..." } | |
]}, | |
{ type: "Verse 2", lines: [ | |
{ chords: "F Am Dm C", lyrics: "Now I'm the one, I'm the one, I'm the one that you know..." }, | |
{ chords: "F Am Dm C", lyrics: "Coming to get you, coming to get you, coming to get you, watch me go..." } | |
]}, | |
{ type: "Chorus 2", lines: [ | |
{ chords: "F Am Dm C", lyrics: "Express yourself! Express yourself!" }, | |
{ chords: "F Am Dm C", lyrics: "Express yourself! It's a brand new thing..." } | |
]}, | |
{ type: "Outro", lines: [{ chords: "F Am Dm C", lyrics: "(repeat and fade)" }] } | |
] | |
}, | |
{ | |
title: "One", | |
artist: "Metallica", | |
sections: [ | |
{ type: "Intro", lines: [{ chords: "Am G C F", lyrics: "(Acoustic Intro/Verse Part)" }] }, | |
{ type: "Verse 1", lines: [ | |
{ chords: "Am G C F", lyrics: "I can't remember anything, can't tell if this is true or dream..." }, | |
{ chords: "Am G C F", lyrics: "Deep down inside I feel the scream..." } | |
]}, | |
{ type: "Chorus 1", lines: [ | |
{ chords: "Am G C F", lyrics: "Hold my breath as I wish for death..." } | |
]}, | |
{ type: "Verse 2", lines: [ | |
{ chords: "Am G C F", lyrics: "Life it seems will fade away, drifting further every day..." }, | |
{ chords: "Am G C F", lyrics: "Getting lost within myself, nothing matters, no one else..." } | |
]}, | |
{ type: "Chorus 2", lines: [ | |
{ chords: "Am G C F", lyrics: "All the thoughts that I have now..." } | |
]}, | |
{ type: "Outro", lines: [{ chords: "E5 - D5 - C5 - A5", lyrics: "(heavy riffing, fast tempo, ends on E5)" }] } | |
] | |
}, | |
{ | |
title: "Handle with Care", | |
artist: "Traveling Wilburys", | |
sections: [ | |
{ type: "Intro", lines: [{ chords: "G D Em C", lyrics: "" }] }, | |
{ type: "Verse 1", lines: [ | |
{ chords: "G D Em C", lyrics: "Been beat up and battered 'round, been sent up, and I've been shot down..." }, | |
{ chords: "G D Em C", lyrics: "You're the best thing that I've found, handle me with care." } | |
]}, | |
{ type: "Chorus 1", lines: [ | |
{ chords: "G D Em C", lyrics: "Handle me with care, handle me with care" }, | |
{ chords: "G D Em C", lyrics: "Handle me with care, handle me with care" } | |
]}, | |
{ type: "Verse 2", lines: [ | |
{ chords: "G D Em C", lyrics: "I been stuck in so much traffic, I'm a mess, a nervous wreck..." }, | |
{ chords: "G D Em C", lyrics: "I could use some tender love and care, handle me with care." } | |
]}, | |
{ type: "Chorus 2", lines: [ | |
{ chords: "G D Em C", lyrics: "Handle me with care, handle me with care" }, | |
{ chords: "G D Em C", lyrics: "Handle me with care, handle me with care" } | |
]}, | |
{ type: "Outro", lines: [{ chords: "G D Em C", lyrics: "(repeat and fade)" }] } | |
] | |
} | |
]; | |
// --- Global State Variables --- | |
let currentSongIndex = 0; | |
let currentTempo = 90; // Default to Medium | |
let isInteractiveMode = false; | |
let activeSectionIndex = 0; | |
let activeLineIndexInSection = 0; | |
let activeChordIndexInLine = 0; | |
let activeWordIndexInLine = 0; | |
let arpeggioDirection = 'rising'; // 'rising' or 'falling' | |
// --- DOM Elements --- | |
const songTitleEl = document.getElementById('songTitle'); | |
const artistNameEl = document.getElementById('artistName'); | |
const scrollContainerEl = document.getElementById('scrollContainer'); | |
const prevSongBtnHeader = document.getElementById('prevSongBtnHeader'); | |
const nextSongBtnHeader = document.getElementById('nextSongBtnHeader'); | |
const slowTempoBtn = document.getElementById('slowTempoBtn'); | |
const mediumTempoBtn = document.getElementById('mediumTempoBtn'); | |
const fastTempoBtn = document.getElementById('fastTempoBtn'); | |
const arpeggioDirectionBtn = document.getElementById('arpeggioDirectionBtn'); | |
const startTeleprompterBtn = document.getElementById('startTeleprompterBtn'); | |
// --- Tone.js Synth Initialization --- | |
let synth = null; | |
async function initializeSynth() { | |
if (!synth) { | |
synth = new Tone.PolySynth(Tone.Synth, { | |
envelope: { | |
attack: 0.02, | |
decay: 0.1, | |
sustain: 0.3, | |
release: 1 | |
} | |
}).toDestination(); | |
await Tone.start(); // Ensure audio context is running | |
console.log('Tone.js audio context started and synth initialized.'); | |
} | |
} | |
// --- Tempo Presets --- | |
const tempos = { | |
slow: 30, | |
medium: 90, | |
fast: 128 | |
}; | |
// --- Helper: Chord to MIDI Note Mapping --- | |
const chordToMidi = (chordName) => { | |
const rootMap = { | |
'C': 60, 'C#': 61, 'Db': 61, 'D': 62, 'D#': 63, 'Eb': 63, 'E': 64, 'F': 65, | |
'F#': 66, 'Gb': 66, 'G': 67, 'G#': 68, 'Ab': 68, 'A': 69, 'A#': 70, 'Bb': 70, 'B': 71 | |
}; | |
let root = null; | |
let type = 'major'; // Default to major | |
let octaveOffset = 0; // Default to 4th octave for root | |
let baseChord = chordName.trim().replace('5', ''); // Handle power chords as root | |
const match = baseChord.match(/^([A-G][b#]?)/); | |
if (match) { | |
root = rootMap[match[1]]; | |
baseChord = baseChord.substring(match[1].length); // Remove root for type parsing | |
} else { | |
return []; // Fallback for unparseable roots | |
} | |
// Determine chord type (simple major/minor/power for now) | |
if (baseChord.includes('m') || baseChord.includes('min')) { | |
type = 'minor'; | |
} else if (baseChord.includes('dim')) { | |
type = 'diminished'; | |
} else if (chordName.includes('5')) { // Explicit power chord check using original chordName | |
type = 'power'; | |
} | |
if (root === null) return []; | |
const notes = [root + octaveOffset]; // Root | |
if (type === 'major') { | |
notes.push(root + 4 + octaveOffset); // Major 3rd | |
} else if (type === 'minor') { | |
notes.push(root + 3 + octaveOffset); // Minor 3rd | |
} else if (type === 'diminished') { | |
notes.push(root + 3 + octaveOffset); // Minor 3rd | |
} | |
notes.push(root + 7 + octaveOffset); // Perfect 5th | |
// Add the root an octave higher for a fuller sound in the arpeggio | |
notes.push(root + 12 + octaveOffset); | |
return notes; | |
}; | |
// --- Helper: Play Arpeggio --- | |
async function playArpeggio(chordName, direction) { | |
await initializeSynth(); // Ensure synth is ready | |
Tone.Transport.stop(); | |
Tone.Transport.cancel(); // Clear any existing schedules | |
const arpeggioNotes = chordToMidi(chordName); | |
if (arpeggioNotes.length === 0) return; | |
const notesToPlay = direction === 'rising' ? arpeggioNotes : [...arpeggioNotes].reverse(); | |
const noteDuration = Tone.Time("8n").toSeconds(); // Each note in arpeggio is an 8th note | |
let startTime = Tone.Transport.now(); | |
notesToPlay.forEach((midiNote, index) => { | |
synth.triggerAttackRelease( | |
Tone.Midi(midiNote).toNote(), | |
"16n", // Shorter attack for arpeggio notes | |
startTime + (index * noteDuration * 0.5) // Stagger notes | |
); | |
}); | |
Tone.Transport.start(); // Start transport for this one-shot sequence | |
// Stop transport after a short delay, to ensure arpeggio finishes | |
Tone.Transport.scheduleOnce((time) => { | |
Tone.Transport.stop(); | |
Tone.Transport.cancel(); | |
}, `+${noteDuration * notesToPlay.length}`); | |
} | |
// --- Rendering Function --- | |
function renderSong() { | |
const song = songsData[currentSongIndex]; | |
songTitleEl.textContent = song.title; | |
artistNameEl.textContent = song.artist; | |
scrollContainerEl.innerHTML = ''; // Clear previous content | |
song.sections.forEach((section, secIndex) => { | |
const sectionDiv = document.createElement('div'); | |
sectionDiv.className = 'mb-8'; | |
const sectionTitle = document.createElement('h2'); | |
sectionTitle.className = 'text-3xl font-semibold text-yellow-300 mb-4 sticky top-0 bg-gray-900 py-2 z-10'; | |
sectionTitle.textContent = section.type; | |
sectionDiv.appendChild(sectionTitle); | |
section.lines.forEach((line, lineIndex) => { | |
const lineDiv = document.createElement('div'); | |
lineDiv.className = `mb-4`; | |
lineDiv.dataset.sectionIndex = secIndex; | |
lineDiv.dataset.lineIndex = lineIndex; | |
const chordsP = document.createElement('p'); | |
chordsP.className = 'font-bold text-green-300 whitespace-pre'; | |
const chordsInLine = line.chords.split(/\s+/).filter(c => c.length > 0); | |
chordsInLine.forEach((chord, chordMapIndex) => { | |
const chordSpan = document.createElement('span'); | |
chordSpan.textContent = chord; | |
// Add spaces for visual alignment | |
chordSpan.innerHTML += ' '; // Use innerHTML for non-breaking space | |
chordSpan.dataset.chordIndex = chordMapIndex; | |
chordsP.appendChild(chordSpan); | |
}); | |
lineDiv.appendChild(chordsP); | |
const lyricsP = document.createElement('p'); | |
lyricsP.className = 'text-gray-100'; | |
const wordsInLine = line.lyrics.split(/\s+/).filter(w => w.length > 0); | |
wordsInLine.forEach((word, wordMapIndex) => { | |
const wordSpan = document.createElement('span'); | |
wordSpan.textContent = word; | |
wordSpan.innerHTML += ' '; // Add space between words | |
wordSpan.dataset.wordIndex = wordMapIndex; | |
lyricsP.appendChild(wordSpan); | |
}); | |
lineDiv.appendChild(lyricsP); | |
sectionDiv.appendChild(lineDiv); | |
}); | |
scrollContainerEl.appendChild(sectionDiv); | |
}); | |
updateHighlighting(); // Initial highlighting | |
} | |
// --- Highlighting Logic --- | |
function updateHighlighting() { | |
// Clear all existing highlights | |
document.querySelectorAll('.text-red-400').forEach(el => el.classList.remove('text-red-400', 'transition-colors', 'duration-200')); | |
document.querySelectorAll('.text-blue-300.font-extrabold').forEach(el => el.classList.remove('text-blue-300', 'font-extrabold', 'transition-colors', 'duration-200')); | |
document.querySelectorAll('.bg-gray-800.rounded-md.p-2').forEach(el => el.classList.remove('bg-gray-800', 'rounded-md', 'p-2')); | |
if (isInteractiveMode) { | |
const activeLineDiv = scrollContainerEl.querySelector( | |
`[data-section-index="${activeSectionIndex}"][data-line-index="${activeLineIndexInSection}"]` | |
); | |
if (activeLineDiv) { | |
activeLineDiv.classList.add('bg-gray-800', 'rounded-md', 'p-2'); | |
const activeChordSpan = activeLineDiv.querySelector(`p.text-green-300 span[data-chord-index="${activeChordIndexInLine}"]`); | |
if (activeChordSpan) { | |
activeChordSpan.classList.add('text-red-400', 'transition-colors', 'duration-200'); | |
} | |
const activeWordSpan = activeLineDiv.querySelector(`p.text-gray-100 span[data-word-index="${activeWordIndexInLine}"]`); | |
if (activeWordSpan) { | |
activeWordSpan.classList.add('text-blue-300', 'font-extrabold', 'transition-colors', 'duration-200'); | |
} | |
// Auto-scroll the active line into view if it's out of bounds | |
const containerHeight = scrollContainerEl.clientHeight; | |
const lineTop = activeLineDiv.offsetTop; | |
const lineBottom = lineTop + activeLineDiv.clientHeight; | |
if (lineTop < scrollContainerEl.scrollTop || lineBottom > scrollContainerEl.scrollTop + containerHeight) { | |
scrollContainerEl.scrollTop = lineTop - (containerHeight / 3); // Scroll to about a third down the screen | |
} | |
} | |
} | |
} | |
// --- Core KeyPress Logic (Teleprompter Advance) --- | |
function handleKeyPress(event) { | |
if (!isInteractiveMode) return; | |
if (event.code === 'Space' || event.code === 'ArrowRight') { | |
event.preventDefault(); // Prevent page scrolling | |
const currentSong = songsData[currentSongIndex]; | |
const currentSection = currentSong.sections[activeSectionIndex]; | |
const currentLine = currentSection?.lines[activeLineIndexInSection]; | |
const chordsInCurrentLine = currentLine?.chords.split(/\s+/).filter(c => c.length > 0) || []; | |
const wordsInCurrentLine = currentLine?.lyrics.split(/\s+/).filter(w => w.length > 0) || []; | |
// Play arpeggio for the current chord before advancing | |
const chordToPlay = chordsInCurrentLine[activeChordIndexInLine]; | |
if (chordToPlay) { | |
playArpeggio(chordToPlay, arpeggioDirection); | |
} | |
let newChordIndex = activeChordIndexInLine + 1; | |
let newWordIndex = activeWordIndexInLine + 1; | |
let newActiveLineIndex = activeLineIndexInSection; | |
let newActiveSectionIndex = activeSectionIndex; | |
const allWordsCovered = newWordIndex >= wordsInCurrentLine.length; | |
const allChordsCovered = newChordIndex >= chordsInCurrentLine.length; | |
if (allWordsCovered && allChordsCovered) { | |
// Both words and chords on the current line are covered, move to next line | |
newActiveLineIndex++; | |
newChordIndex = 0; | |
newWordIndex = 0; | |
if (newActiveLineIndex >= currentSection.lines.length) { | |
// All lines in current section covered, move to next section | |
newActiveSectionIndex++; | |
newActiveLineIndex = 0; // Reset line index for new section | |
if (newActiveSectionIndex >= currentSong.sections.length) { | |
// End of song | |
console.log("End of song!"); | |
isInteractiveMode = false; // Exit interactive mode | |
activeSectionIndex = 0; | |
activeLineIndexInSection = 0; | |
activeChordIndexInLine = 0; | |
activeWordIndexInLine = 0; | |
Tone.Transport.stop(); | |
Tone.Transport.cancel(); | |
updateHighlighting(); // Clear final highlights | |
return; | |
} | |
} | |
} else if (newChordIndex >= chordsInCurrentLine.length && !allWordsCovered) { | |
// All chords on line covered, but words remain. Advance words only. | |
// Keep chord index at the last chord. | |
newChordIndex = chordsInCurrentLine.length > 0 ? chordsInCurrentLine.length - 1 : 0; | |
} else if (newWordIndex >= wordsInCurrentLine.length && !allChordsCovered) { | |
// All words on line covered, but chords remain. Advance chords only. | |
// Keep word index at the last word. | |
newWordIndex = wordsInCurrentLine.length > 0 ? wordsInCurrentLine.length - 1 : 0; | |
} | |
activeChordIndexInLine = newChordIndex; | |
activeWordIndexInLine = newWordIndex; | |
activeLineIndexInSection = newActiveLineIndex; | |
activeSectionIndex = newActiveSectionIndex; | |
updateHighlighting(); | |
} | |
} | |
// --- Event Handlers --- | |
function handleSongChange(direction) { | |
isInteractiveMode = false; // Exit interactive mode on song change | |
Tone.Transport.stop(); | |
Tone.Transport.cancel(); | |
if (direction === 'next') { | |
currentSongIndex = (currentSongIndex + 1) % songsData.length; | |
} else if (direction === 'prev') { | |
currentSongIndex = (currentSongIndex - 1 + songsData.length) % songsData.length; | |
} | |
// Reset teleprompter state for the new song | |
activeSectionIndex = 0; | |
activeLineIndexInSection = 0; | |
activeChordIndexInLine = 0; | |
activeWordIndexInLine = 0; | |
scrollContainerEl.scrollTop = 0; // Scroll to top | |
renderSong(); // Re-render the new song | |
} | |
function setTempo(tempoValue) { | |
currentTempo = tempoValue; | |
Tone.Transport.bpm.value = tempoValue; | |
// Update button styles | |
slowTempoBtn.classList.remove('bg-purple-700'); | |
mediumTempoBtn.classList.remove('bg-purple-700'); | |
fastTempoBtn.classList.remove('bg-purple-700'); | |
slowTempoBtn.classList.add('bg-purple-500', 'hover:bg-purple-600'); | |
mediumTempoBtn.classList.add('bg-purple-500', 'hover:bg-purple-600'); | |
fastTempoBtn.classList.add('bg-purple-500', 'hover:bg-purple-600'); | |
if (tempoValue === tempos.slow) slowTempoBtn.classList.replace('bg-purple-500', 'bg-purple-700'); | |
else if (tempoValue === tempos.medium) mediumTempoBtn.classList.replace('bg-purple-500', 'bg-purple-700'); | |
else if (tempoValue === tempos.fast) fastTempoBtn.classList.replace('bg-purple-500', 'bg-purple-700'); | |
} | |
function toggleArpeggioDirection() { | |
arpeggioDirection = arpeggioDirection === 'rising' ? 'falling' : 'rising'; | |
arpeggioDirectionBtn.textContent = `Arpeggio: ${arpeggioDirection.charAt(0).toUpperCase() + arpeggioDirection.slice(1)}`; | |
} | |
function startTeleprompter() { | |
isInteractiveMode = true; | |
activeSectionIndex = 0; | |
activeLineIndexInSection = 0; | |
activeChordIndexInLine = 0; | |
activeWordIndexInLine = 0; | |
scrollContainerEl.scrollTop = 0; // Scroll to top on start | |
initializeSynth(); // Ensure synth is ready | |
Tone.Transport.stop(); // Stop any previous playback | |
Tone.Transport.cancel(); | |
Tone.Transport.bpm.value = currentTempo; // Set tempo for interactive playback | |
updateHighlighting(); // Apply initial highlight | |
} | |
// --- Initial Setup and Event Listeners --- | |
document.addEventListener('DOMContentLoaded', () => { | |
renderSong(); // Render the first song on load | |
setTempo(tempos.medium); // Set initial tempo button state | |
// Attach event listeners | |
prevSongBtnHeader.addEventListener('click', () => handleSongChange('prev')); | |
nextSongBtnHeader.addEventListener('click', () => handleSongChange('next')); | |
slowTempoBtn.addEventListener('click', () => setTempo(tempos.slow)); | |
mediumTempoBtn.addEventListener('click', () => setTempo(tempos.medium)); | |
fastTempoBtn.addEventListener('click', () => setTempo(tempos.fast)); | |
arpeggioDirectionBtn.addEventListener('click', toggleArpeggioDirection); | |
startTeleprompterBtn.addEventListener('click', startTeleprompter); | |
window.addEventListener('keydown', handleKeyPress); | |
}); | |
</script> | |
</body> | |
</html> | |