Spaces:
Running
Running
import React, { useState, useRef, useEffect, useCallback } from 'react'; | |
import * as Tone from 'tone'; // Import Tone.js | |
// Song data parsed from your previous responses | |
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: [ // Note: This is not a traditional chorus for this song, but the repeated progression | |
{ 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: [ // Note: This is not a traditional chorus for this song, but the repeated progression | |
{ 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)" }] } | |
] | |
} | |
]; | |
// Simplified chord to MIDI note mapping | |
// Maps chord root to MIDI note number (C4 = 60) and then adds intervals for Major/Minor triad | |
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 | |
// Parse root and accidental (e.g., C#, Eb) | |
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 { | |
// Fallback for unparseable roots, or just return empty | |
return []; | |
} | |
// Determine chord type (simple major/minor/power for now) | |
if (baseChord.includes('m') || baseChord.includes('min')) { | |
type = 'minor'; | |
} else if (baseChord.includes('5')) { // Explicit power chord | |
type = 'power'; | |
} | |
if (root === null) return []; // Should not happen with match check above | |
const notes = [root + octaveOffset]; | |
if (type === 'major') { | |
notes.push(root + 4 + octaveOffset); // Major third | |
notes.push(root + 7 + octaveOffset); // Perfect fifth | |
} else if (type === 'minor') { | |
notes.push(root + 3 + octaveOffset); // Minor third | |
notes.push(root + 7 + octaveOffset); // Perfect fifth | |
} else if (type === 'power') { | |
notes.push(root + 7 + octaveOffset); // Perfect fifth | |
} | |
// Add higher octave for fuller sound, if it makes sense | |
if (notes.length > 0) { | |
notes.push(notes[0] + 12); // Add root an octave higher | |
} | |
return notes; | |
}; | |
// Main App Component | |
const App = () => { | |
const [currentSongIndex, setCurrentSongIndex] = useState(0); | |
const [currentTempo, setCurrentTempo] = useState(90); // Default to Medium tempo | |
const synthRef = useRef(null); | |
const playSequenceRef = useRef(null); | |
// Define tempo presets | |
const tempos = { | |
slow: 30, | |
medium: 90, // Changed from previous default for distinct steps | |
fast: 128 | |
}; | |
// Initialize Tone.js synth once | |
useEffect(() => { | |
// Only create synth if it doesn't exist | |
if (!synthRef.current) { | |
synthRef.current = new Tone.PolySynth(Tone.Synth, { | |
envelope: { | |
attack: 0.02, | |
decay: 0.1, | |
sustain: 0.3, | |
release: 1 | |
} | |
}).toDestination(); | |
} | |
// Cleanup function | |
return () => { | |
if (synthRef.current) { | |
synthRef.current.dispose(); | |
synthRef.current = null; | |
} | |
if (playSequenceRef.current) { | |
playSequenceRef.current.stop(); | |
playSequenceRef.current.dispose(); | |
playSequenceRef.current = null; | |
} | |
Tone.Transport.stop(); // Stop transport on unmount | |
}; | |
}, []); // Run once on mount | |
const currentSong = songsData[currentSongIndex]; | |
const handleNextSong = () => { | |
if (playSequenceRef.current) { | |
playSequenceRef.current.stop(); // Stop any current playback | |
} | |
setCurrentSongIndex((prevIndex) => (prevIndex + 1) % songsData.length); | |
}; | |
const handlePrevSong = () => { | |
if (playSequenceRef.current) { | |
playSequenceRef.current.stop(); // Stop any current playback | |
} | |
setCurrentSongIndex((prevIndex) => (prevIndex - 1 + songsData.length) % songsData.length); | |
}; | |
const setTempo = (tempoValue) => { | |
setCurrentTempo(tempoValue); | |
Tone.Transport.bpm.value = tempoValue; // Update Tone.js Transport BPM | |
if (playSequenceRef.current && Tone.Transport.state === 'started') { | |
// If playing, restart the sequence to apply tempo immediately | |
playChords(); // This will stop and restart with new tempo | |
} | |
}; | |
const playChords = useCallback(async () => { | |
if (!synthRef.current) { | |
console.error("Synth not initialized."); | |
return; | |
} | |
// Start Tone.js audio context if not already started | |
if (Tone.context.state !== 'running') { | |
await Tone.start(); | |
console.log('Audio context started.'); | |
} | |
// Stop any existing sequence and transport | |
if (playSequenceRef.current) { | |
playSequenceRef.current.stop(); | |
playSequenceRef.current.dispose(); | |
playSequenceRef.current = null; | |
} | |
Tone.Transport.stop(); | |
Tone.Transport.cancel(); // Clear any scheduled events | |
// Set the tempo before starting the transport | |
Tone.Transport.bpm.value = currentTempo; | |
const allChords = []; | |
currentSong.sections.forEach(section => { | |
section.lines.forEach(line => { | |
// Simple regex to extract individual chord names, assume space-separated | |
const chordsInLine = line.chords.split(/\s+/).filter(c => c.length > 0); | |
chordsInLine.forEach(chord => allChords.push(chord)); | |
}); | |
}); | |
if (allChords.length === 0) { | |
console.warn("No chords found for this song."); | |
return; | |
} | |
// Create a new sequence for playback | |
// The interval '1n' means one whole note per chord in this sequence | |
// Tone.js Transport BPM controls the actual duration of '1n' | |
playSequenceRef.current = new Tone.Sequence((time, chordName) => { | |
const midiNotes = chordToMidi(chordName); | |
if (midiNotes.length > 0) { | |
synthRef.current.triggerAttackRelease(midiNotes.map(n => Tone.Midi(n).toNote()), "0.8n", time); // hold for 0.8 of a whole note | |
} | |
}, allChords, "1n").start(0); // Each chord plays for 1 whole note duration | |
Tone.Transport.start(); | |
}, [currentSong, currentTempo]); // Recreate sequence if current song or tempo changes | |
// Stop playback when song index changes | |
useEffect(() => { | |
if (playSequenceRef.current) { | |
playSequenceRef.current.stop(); | |
Tone.Transport.stop(); | |
} | |
}, [currentSongIndex]); | |
return ( | |
<div className="flex flex-col h-screen bg-gray-900 text-gray-100 font-inter"> | |
{/* Header with song title and artist */} | |
<header className="p-4 bg-gray-800 shadow-md text-center rounded-b-lg"> | |
<h1 className="text-3xl font-bold text-blue-400"> | |
{currentSong.title} | |
</h1> | |
<p className="text-xl text-gray-300">{currentSong.artist}</p> | |
</header> | |
{/* Main content area - lyrics and chords */} | |
<main className="flex-1 overflow-y-auto p-6 text-2xl leading-relaxed"> | |
{currentSong.sections.map((section, secIndex) => ( | |
<div key={secIndex} className="mb-8"> | |
<h2 className="text-3xl font-semibold text-yellow-300 mb-4 sticky top-0 bg-gray-900 py-2 z-10"> | |
{section.type} | |
</h2> | |
{section.lines.map((line, lineIndex) => ( | |
<div key={lineIndex} className="mb-4"> | |
<p className="font-bold text-green-300 whitespace-pre"> | |
{line.chords} | |
</p> | |
<p className="text-gray-100"> | |
{line.lyrics} | |
</p> | |
</div> | |
))} | |
</div> | |
))} | |
</main> | |
{/* Navigation and MIDI controls */} | |
<footer className="p-4 bg-gray-800 shadow-t-md flex justify-center items-center space-x-4 rounded-t-lg flex-wrap"> | |
<button | |
onClick={handlePrevSong} | |
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" | |
> | |
Previous Song | |
</button> | |
<button | |
onClick={() => setTempo(tempos.slow)} | |
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`} | |
> | |
Slow ({tempos.slow} BPM) | |
</button> | |
<button | |
onClick={() => setTempo(tempos.medium)} | |
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`} | |
> | |
Medium ({tempos.medium} BPM) | |
</button> | |
<button | |
onClick={() => setTempo(tempos.fast)} | |
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`} | |
> | |
Fast ({tempos.fast} BPM) | |
</button> | |
<button | |
onClick={playChords} | |
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" | |
> | |
Play Chords | |
</button> | |
<button | |
onClick={handleNextSong} | |
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" | |
> | |
Next Song | |
</button> | |
</footer> | |
</div> | |
); | |
}; | |
export default App; | |