Spaces:
Sleeping
Sleeping
| import streamlit as st | |
| import streamlit.components.v1 as components | |
| def main(): | |
| st.title("5-Octave Synth with Arpeggiator & Drum Pads") | |
| # Load and embed synth interface | |
| components.html(get_synth_interface(), height=800) | |
| def get_synth_interface(): | |
| return """ | |
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <script src="https://cdn.jsdelivr.net/npm/[email protected]/build/Tone.js"></script> | |
| <style> | |
| .container { max-width: 1200px; margin: 0 auto; } | |
| .keyboard { display: flex; margin: 20px 0; } | |
| .key { | |
| width: 40px; height: 150px; | |
| border: 1px solid #000; | |
| background: white; | |
| margin-right: 2px; | |
| } | |
| .key.black { | |
| width: 24px; height: 100px; | |
| background: black; | |
| margin: 0 -12px; | |
| z-index: 1; | |
| } | |
| .key.active { background: #ff6961; } | |
| .drum-grid { | |
| display: grid; | |
| grid-template-columns: repeat(4, 1fr); | |
| gap: 10px; | |
| margin: 20px 0; | |
| } | |
| .drum-pad { | |
| aspect-ratio: 1; | |
| background: #444; | |
| color: white; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| cursor: pointer; | |
| border-radius: 4px; | |
| } | |
| .drum-pad.active { background: #ff6961; } | |
| .controls { | |
| display: flex; | |
| gap: 20px; | |
| margin: 20px 0; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <div class="controls"> | |
| <div> | |
| <label>Arpeggiator:</label> | |
| <select id="arpMode"> | |
| <option value="off">Off</option> | |
| <option value="up">Up</option> | |
| <option value="down">Down</option> | |
| <option value="updown">Up/Down</option> | |
| <option value="random">Random</option> | |
| </select> | |
| <input type="range" id="arpSpeed" min="100" max="1000" value="200"> | |
| </div> | |
| <div> | |
| <label>Synth Type:</label> | |
| <select id="synthType"> | |
| <option value="simple">Simple</option> | |
| <option value="fm">FM</option> | |
| <option value="am">AM</option> | |
| </select> | |
| </div> | |
| </div> | |
| <div id="keyboard" class="keyboard"></div> | |
| <div id="drumPads" class="drum-grid"></div> | |
| </div> | |
| <script> | |
| // Initialize Tone.js instruments | |
| const synth = new Tone.PolySynth().toDestination(); | |
| const drumSampler = new Tone.Sampler({ | |
| 'C2': 'https://tonejs.github.io/audio/drum-samples/kicks/kick.mp3', | |
| 'D2': 'https://tonejs.github.io/audio/drum-samples/snare/snare.mp3', | |
| 'E2': 'https://tonejs.github.io/audio/drum-samples/hh/hh.mp3', | |
| 'F2': 'https://tonejs.github.io/audio/drum-samples/tom/tom.mp3' | |
| }).toDestination(); | |
| // Build 5-octave keyboard (61 keys) | |
| const startNote = 36; // C2 | |
| const noteNames = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']; | |
| const keyboard = document.getElementById('keyboard'); | |
| for (let i = 0; i < 61; i++) { | |
| const midiNote = startNote + i; | |
| const octave = Math.floor(midiNote / 12) - 1; | |
| const noteName = noteNames[midiNote % 12] + octave; | |
| const isBlack = noteName.includes('#'); | |
| const key = document.createElement('div'); | |
| key.className = `key ${isBlack ? 'black' : ''}`; | |
| key.dataset.note = noteName; | |
| key.dataset.midi = midiNote; | |
| key.addEventListener('mousedown', () => playNote(noteName, midiNote)); | |
| key.addEventListener('mouseup', () => stopNote(noteName, midiNote)); | |
| key.addEventListener('mouseleave', () => stopNote(noteName, midiNote)); | |
| keyboard.appendChild(key); | |
| } | |
| // Build 16 drum pads | |
| const drumPads = document.getElementById('drumPads'); | |
| for (let i = 0; i < 16; i++) { | |
| const pad = document.createElement('div'); | |
| pad.className = 'drum-pad'; | |
| pad.textContent = `Pad ${i + 1}`; | |
| pad.addEventListener('mousedown', () => triggerDrum(i)); | |
| drumPads.appendChild(pad); | |
| } | |
| // Arpeggiator implementation | |
| let arpNotes = []; | |
| let arpInterval = null; | |
| document.getElementById('arpMode').addEventListener('change', updateArpeggiator); | |
| document.getElementById('arpSpeed').addEventListener('change', updateArpeggiator); | |
| function updateArpeggiator() { | |
| const mode = document.getElementById('arpMode').value; | |
| const speed = document.getElementById('arpSpeed').value; | |
| if (arpInterval) clearInterval(arpInterval); | |
| if (mode !== 'off' && arpNotes.length) { | |
| let index = 0; | |
| arpInterval = setInterval(() => { | |
| const note = arpNotes[index]; | |
| playNote(note, true); | |
| setTimeout(() => stopNote(note), speed * 0.8); | |
| switch(mode) { | |
| case 'up': | |
| index = (index + 1) % arpNotes.length; | |
| break; | |
| case 'down': | |
| index = (index - 1 + arpNotes.length) % arpNotes.length; | |
| break; | |
| case 'updown': | |
| // Implementation for up/down pattern | |
| break; | |
| case 'random': | |
| index = Math.floor(Math.random() * arpNotes.length); | |
| break; | |
| } | |
| }, speed); | |
| } | |
| } | |
| function playNote(note, midiNote) { | |
| Tone.start(); | |
| synth.triggerAttack(note); | |
| const key = document.querySelector(`[data-midi="${midiNote}"]`); | |
| if (key) key.classList.add('active'); | |
| if (document.getElementById('arpMode').value !== 'off') { | |
| if (!arpNotes.includes(note)) { | |
| arpNotes.push(note); | |
| updateArpeggiator(); | |
| } | |
| } | |
| } | |
| function stopNote(note, midiNote) { | |
| synth.triggerRelease(note); | |
| const key = document.querySelector(`[data-midi="${midiNote}"]`); | |
| if (key) key.classList.remove('active'); | |
| if (document.getElementById('arpMode').value !== 'off') { | |
| arpNotes = arpNotes.filter(n => n !== note); | |
| if (!arpNotes.length && arpInterval) { | |
| clearInterval(arpInterval); | |
| arpInterval = null; | |
| } | |
| } | |
| } | |
| function triggerDrum(index) { | |
| const notes = ['C2', 'D2', 'E2', 'F2']; | |
| const note = notes[index % notes.length]; | |
| drumSampler.triggerAttackRelease(note, '8n'); | |
| const pad = drumPads.children[index]; | |
| pad.classList.add('active'); | |
| setTimeout(() => pad.classList.remove('active'), 100); | |
| } | |
| // Handle synth type changes | |
| document.getElementById('synthType').addEventListener('change', (e) => { | |
| const type = e.target.value; | |
| let newSynth; | |
| switch(type) { | |
| case 'fm': | |
| newSynth = new Tone.PolySynth(Tone.FMSynth).toDestination(); | |
| break; | |
| case 'am': | |
| newSynth = new Tone.PolySynth(Tone.AMSynth).toDestination(); | |
| break; | |
| default: | |
| newSynth = new Tone.PolySynth(Tone.Synth).toDestination(); | |
| } | |
| synth.dispose(); | |
| window.synth = newSynth; | |
| }); | |
| </script> | |
| </body> | |
| </html> | |
| """ | |
| if __name__ == "__main__": | |
| main() |