|
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>Multi-Instrument Synthesizer</title> |
|
<style> |
|
body { |
|
font-family: Arial, sans-serif; |
|
background-color: #f5f5f5; |
|
display: flex; |
|
justify-content: center; |
|
align-items: center; |
|
height: 100vh; |
|
margin: 0; |
|
} |
|
|
|
#root { |
|
width: 100%; |
|
max-width: 600px; |
|
background-color: #fff; |
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); |
|
border-radius: 10px; |
|
padding: 20px; |
|
} |
|
|
|
.button { |
|
padding: 10px; |
|
margin: 5px; |
|
border: none; |
|
border-radius: 5px; |
|
cursor: pointer; |
|
} |
|
|
|
.bg-blue-500 { |
|
background-color: #4299e1; |
|
} |
|
|
|
.bg-gray-300 { |
|
background-color: #e2e8f0; |
|
} |
|
|
|
.bg-green-500 { |
|
background-color: #48bb78; |
|
} |
|
|
|
.bg-red-500 { |
|
background-color: #f56565; |
|
} |
|
|
|
.text-white { |
|
color: white; |
|
} |
|
|
|
.p-4 { |
|
padding: 16px; |
|
} |
|
|
|
.space-y-4 > * + * { |
|
margin-top: 16px; |
|
} |
|
|
|
.bg-gray-100 { |
|
background-color: #f7fafc; |
|
} |
|
|
|
.rounded-xl { |
|
border-radius: 12px; |
|
} |
|
|
|
.text-2xl { |
|
font-size: 1.5rem; |
|
} |
|
|
|
.font-bold { |
|
font-weight: 700; |
|
} |
|
|
|
.text-center { |
|
text-align: center; |
|
} |
|
|
|
.mb-6 { |
|
margin-bottom: 24px; |
|
} |
|
|
|
.block { |
|
display: block; |
|
} |
|
|
|
.text-sm { |
|
font-size: 0.875rem; |
|
} |
|
|
|
.font-medium { |
|
font-weight: 500; |
|
} |
|
|
|
.text-gray-700 { |
|
color: #4a5568; |
|
} |
|
|
|
.mb-2 { |
|
margin-bottom: 8px; |
|
} |
|
|
|
.grid { |
|
display: grid; |
|
} |
|
|
|
.grid-cols-4 { |
|
grid-template-columns: repeat(4, minmax(0, 1fr)); |
|
} |
|
|
|
.grid-cols-2 { |
|
grid-template-columns: repeat(2, minmax(0, 1fr)); |
|
} |
|
|
|
.gap-2 { |
|
gap: 8px; |
|
} |
|
|
|
.w-full { |
|
width: 100%; |
|
} |
|
|
|
.px-4 { |
|
padding-left: 16px; |
|
padding-right: 16px; |
|
} |
|
|
|
.py-2 { |
|
padding-top: 8px; |
|
padding-bottom: 8px; |
|
} |
|
|
|
.rounded { |
|
border-radius: 4px; |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
<div id="root"></div> |
|
<script src="https://unpkg.com/react/umd/react.production.min.js"></script> |
|
<script src="https://unpkg.com/react-dom/umd/react-dom.production.min.js"></script> |
|
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script> |
|
<script type="text/babel"> |
|
const { useState, useEffect, useCallback, useRef } = React; |
|
|
|
const MultiInstrumentSynthesizer = () => { |
|
const [audioContext, setAudioContext] = useState(null); |
|
const [isPlaying, setIsPlaying] = useState(false); |
|
const [tempo, setTempo] = useState(120); |
|
const [rootNote, setRootNote] = useState(261.63); |
|
const [selectedProgression, setSelectedProgression] = useState(0); |
|
const [selectedInstrument, setSelectedInstrument] = useState('fm'); |
|
|
|
const sequencerIntervalRef = useRef(null); |
|
const currentBarRef = useRef(0); |
|
const currentBeatRef = useRef(0); |
|
|
|
useEffect(() => { |
|
const context = new (window.AudioContext || window.webkitAudioContext)(); |
|
setAudioContext(context); |
|
|
|
return () => { |
|
context.close(); |
|
}; |
|
}, []); |
|
|
|
const noteFrequencies = { |
|
'C': 261.63, 'C#': 277.18, 'D': 293.66, 'D#': 311.13, 'E': 329.63, 'F': 349.23, |
|
'F#': 369.99, 'G': 392.00, 'G#': 415.30, 'A': 440.00, 'A#': 466.16, 'B': 493.88 |
|
}; |
|
|
|
const progressions = [ |
|
{ name: "Blues I", progression: [1, 1, 1, 1, 4, 4, 1, 1, 5, 4, 1, 5] }, |
|
{ name: "Blues II", progression: [1, 4, 1, 1, 4, 4, 1, 1, 5, 4, 1, 5] }, |
|
{ name: "Rock", progression: [1, 5, 6, 4, 1, 5, 6, 4, 1, 5, 6, 4] }, |
|
{ name: "Electronica", progression: [1, 6, 4, 5, 1, 6, 4, 5, 1, 6, 4, 5] }, |
|
{ name: "Chillwave", progression: [1, 5, 6, 4, 1, 5, 6, 4, 1, 5, 6, 4] }, |
|
{ name: "EDM", progression: [1, 4, 5, 6, 1, 4, 5, 6, 1, 4, 5, 6] } |
|
]; |
|
|
|
const generateChord = (root, chordType) => { |
|
const majorScale = [0, 2, 4, 5, 7, 9, 11]; |
|
let chordIntervals; |
|
|
|
switch (chordType) { |
|
case 1: case 4: case 5: |
|
chordIntervals = [0, 4, 7, 10]; |
|
break; |
|
case 2: case 3: case 6: |
|
chordIntervals = [0, 3, 7, 10]; |
|
break; |
|
default: |
|
chordIntervals = [0, 4, 7, 10]; |
|
} |
|
|
|
return chordIntervals.map(interval => { |
|
const scaleStep = (majorScale[chordType - 1] + interval) % 12; |
|
return root * Math.pow(2, scaleStep / 12); |
|
}); |
|
}; |
|
|
|
const playFMSynth = useCallback((frequency, duration) => { |
|
const osc = audioContext.createOscillator(); |
|
const mod = audioContext.createOscillator(); |
|
const modGain = audioContext.createGain(); |
|
const env = audioContext.createGain(); |
|
|
|
mod.type = 'sine'; |
|
osc.type = 'sine'; |
|
|
|
mod.connect(modGain); |
|
modGain.connect(osc.frequency); |
|
osc.connect(env); |
|
env.connect(audioContext.destination); |
|
|
|
const now = audioContext.currentTime; |
|
const modFreq = frequency * 2; |
|
const modIndex = 100; |
|
|
|
mod.frequency.setValueAtTime(modFreq, now); |
|
modGain.gain.setValueAtTime(modIndex, now); |
|
osc.frequency.setValueAtTime(frequency, now); |
|
|
|
env.gain.setValueAtTime(0, now); |
|
env.gain.linearRampToValueAtTime(0.5, now + 0.01); |
|
env.gain.exponentialRampToValueAtTime(0.01, now + duration - 0.1); |
|
env.gain.linearRampToValueAtTime(0, now + duration); |
|
|
|
mod.start(now); |
|
osc.start(now); |
|
osc.stop(now + duration); |
|
}, [audioContext]); |
|
|
|
const playPiano = useCallback((frequency, duration) => { |
|
const osc = audioContext.createOscillator(); |
|
const gainNode = audioContext.createGain(); |
|
osc.type = 'triangle'; |
|
osc.frequency.setValueAtTime(frequency, audioContext.currentTime); |
|
osc.connect(gainNode); |
|
gainNode.connect(audioContext.destination); |
|
|
|
gainNode.gain.setValueAtTime(0, audioContext.currentTime); |
|
gainNode.gain.linearRampToValueAtTime(0.5, audioContext.currentTime + 0.01); |
|
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + duration - 0.1); |
|
|
|
osc.start(); |
|
osc.stop(audioContext.currentTime + duration); |
|
}, [audioContext]); |
|
|
|
|
|
const playGuitar = useCallback((frequency, duration) => { |
|
const osc = audioContext.createOscillator(); |
|
const gainNode = audioContext.createGain(); |
|
osc.type = 'sawtooth'; |
|
osc.frequency.setValueAtTime(frequency, audioContext.currentTime); |
|
osc.connect(gainNode); |
|
gainNode.connect(audioContext.destination); |
|
|
|
gainNode.gain.setValueAtTime(0, audioContext.currentTime); |
|
gainNode.gain.linearRampToValueAtTime(0.5, audioContext.currentTime + 0.005); |
|
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + duration - 0.05); |
|
|
|
osc.start(); |
|
osc.stop(audioContext.currentTime + duration); |
|
}, [audioContext]); |
|
|
|
const playDrum = useCallback((type) => { |
|
const osc = audioContext.createOscillator(); |
|
const gainNode = audioContext.createGain(); |
|
osc.connect(gainNode); |
|
gainNode.connect(audioContext.destination); |
|
|
|
if (type === 'kick') { |
|
osc.frequency.setValueAtTime(150, audioContext.currentTime); |
|
osc.frequency.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.5); |
|
gainNode.gain.setValueAtTime(1, audioContext.currentTime); |
|
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.5); |
|
} else if (type === 'snare') { |
|
osc.type = 'triangle'; |
|
osc.frequency.setValueAtTime(100, audioContext.currentTime); |
|
gainNode.gain.setValueAtTime(1, audioContext.currentTime); |
|
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.2); |
|
} else if (type === 'hihat') { |
|
osc.type = 'square'; |
|
osc.frequency.setValueAtTime(1000, audioContext.currentTime); |
|
gainNode.gain.setValueAtTime(0.2, audioContext.currentTime); |
|
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.05); |
|
} |
|
|
|
osc.start(audioContext.currentTime); |
|
osc.stop(audioContext.currentTime + 0.5); |
|
}, [audioContext]); |
|
|
|
const playChord = useCallback((frequencies) => { |
|
const noteDuration = 60 / tempo; |
|
|
|
frequencies.forEach((frequency, index) => { |
|
const playTime = audioContext.currentTime + index * 0.05; |
|
switch (selectedInstrument) { |
|
case 'fm': |
|
playFMSynth(frequency, noteDuration); |
|
break; |
|
case 'piano': |
|
playPiano(frequency, noteDuration); |
|
break; |
|
case 'guitar': |
|
playGuitar(frequency, noteDuration); |
|
break; |
|
} |
|
}); |
|
}, [audioContext, tempo, selectedInstrument, playFMSynth, playPiano, playGuitar]); |
|
|
|
const startSequencer = useCallback(() => { |
|
if (sequencerIntervalRef.current) { |
|
clearInterval(sequencerIntervalRef.current); |
|
} |
|
|
|
currentBarRef.current = 0; |
|
currentBeatRef.current = 0; |
|
|
|
const beatDuration = 60 / tempo / 4; |
|
|
|
sequencerIntervalRef.current = setInterval(() => { |
|
if (currentBeatRef.current % 4 === 0) { |
|
const chordType = progressions[selectedProgression].progression[currentBarRef.current]; |
|
const chord = generateChord(rootNote, chordType); |
|
playChord(chord); |
|
|
|
|
|
if (currentBeatRef.current % 8 === 0) { |
|
playDrum('kick'); |
|
} |
|
} |
|
|
|
|
|
if (currentBeatRef.current % 8 === 4) { |
|
playDrum('snare'); |
|
} |
|
|
|
|
|
playDrum('hihat'); |
|
|
|
currentBeatRef.current = (currentBeatRef.current + 1) % 16; |
|
if (currentBeatRef.current === 0) { |
|
currentBarRef.current = (currentBarRef.current + 1) % 12; |
|
} |
|
}, beatDuration * 1000); |
|
|
|
setIsPlaying(true); |
|
}, [tempo, playChord, playDrum, selectedProgression, rootNote]); |
|
|
|
const stopSequencer = useCallback(() => { |
|
if (sequencerIntervalRef.current) { |
|
clearInterval(sequencerIntervalRef.current); |
|
} |
|
setIsPlaying(false); |
|
}, []); |
|
|
|
const togglePlay = () => { |
|
if (isPlaying) { |
|
stopSequencer(); |
|
} else { |
|
startSequencer(); |
|
} |
|
}; |
|
|
|
const handleRootNoteChange = (note) => { |
|
setRootNote(noteFrequencies[note]); |
|
if (isPlaying) { |
|
stopSequencer(); |
|
setTimeout(startSequencer, 100); |
|
} |
|
}; |
|
|
|
const handleProgressionChange = (index) => { |
|
setSelectedProgression(index); |
|
if (isPlaying) { |
|
stopSequencer(); |
|
setTimeout(startSequencer, 100); |
|
} |
|
}; |
|
|
|
const handleInstrumentChange = (instrument) => { |
|
setSelectedInstrument(instrument); |
|
}; |
|
|
|
const handleTempoChange = (value) => { |
|
setTempo(value[0]); |
|
if (isPlaying) { |
|
stopSequencer(); |
|
setTimeout(startSequencer, 100); |
|
} |
|
}; |
|
|
|
return ( |
|
<div className="p-4 space-y-4 bg-gray-100 rounded-xl"> |
|
<h2 className="text-2xl font-bold text-center mb-6">Multi-Instrument Twelve-Bar Synthesizer with Drums</h2> |
|
<div className="space-y-4"> |
|
<div> |
|
<label className="block text-sm font-medium text-gray-700 mb-2">Root Note</label> |
|
<div className="grid grid-cols-4 gap-2"> |
|
{Object.keys(noteFrequencies).map((note) => ( |
|
<button |
|
key={note} |
|
onClick={() => handleRootNoteChange(note)} |
|
className={`button ${note === Object.keys(noteFrequencies).find(key => noteFrequencies[key] === rootNote) ? 'bg-blue-500' : 'bg-gray-300'} text-white`} |
|
> |
|
{note} |
|
</button> |
|
))} |
|
</div> |
|
</div> |
|
<div> |
|
<label className="block text-sm font-medium text-gray-700 mb-2">Progression</label> |
|
<div className="grid grid-cols-2 gap-2"> |
|
{progressions.map((prog, index) => ( |
|
<button |
|
key={index} |
|
onClick={() => handleProgressionChange(index)} |
|
className={`button ${index === selectedProgression ? 'bg-green-500' : 'bg-gray-300'} text-white text-xs`} |
|
> |
|
{prog.name} |
|
</button> |
|
))} |
|
</div> |
|
</div> |
|
<div> |
|
<label className="block text-sm font-medium text-gray-700 mb-2">Instrument</label> |
|
<select onChange={(e) => handleInstrumentChange(e.target.value)} value={selectedInstrument} className="w-full"> |
|
<option value="fm">FM Synth</option> |
|
<option value="piano">Piano</option> |
|
<option value="guitar">Guitar</option> |
|
</select> |
|
</div> |
|
<div> |
|
<label className="block text-sm font-medium text-gray-700">Tempo: {tempo} BPM</label> |
|
<input |
|
type="range" |
|
min="60" |
|
max="180" |
|
step="1" |
|
value={tempo} |
|
onChange={(e) => handleTempoChange([parseInt(e.target.value)])} |
|
className="w-full" |
|
/> |
|
</div> |
|
<button onClick={togglePlay} className={`button w-full ${isPlaying ? 'bg-red-500' : 'bg-green-500'} text-white px-4 py-2 rounded`}> |
|
{isPlaying ? 'Stop' : 'Play'} |
|
</button> |
|
</div> |
|
</div> |
|
); |
|
}; |
|
|
|
const rootElement = document.getElementById('root'); |
|
ReactDOM.render(<MultiInstrumentSynthesizer />, rootElement); |
|
</script> |
|
</body> |
|
</html> |
|
|