| <!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> |
|
|