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/tone@14.8.39/build/Tone.js"></script> <script src="https://cdn.jsdelivr.net/npm/@magenta/music@1.23.1/dist/magentamusic.min.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> // WebMIDI initialization let midiIn = null; if (navigator.requestMIDIAccess) { navigator.requestMIDIAccess() .then(access => { const inputs = access.inputs.values(); for (let input of inputs) { midiIn = input; input.onmidimessage = handleMIDIMessage; } access.onstatechange = e => { if (e.port.type === 'input') { if (e.port.state === 'connected') { midiIn = e.port; e.port.onmidimessage = handleMIDIMessage; } else { midiIn = null; } } }; }) .catch(err => console.warn('WebMIDI not available:', err)); } function handleMIDIMessage(event) { const [status, note, velocity] = event.data; const command = status >> 4; if (command === 9) { // Note On playNote(midiToNoteName(note), note, velocity); } else if (command === 8) { // Note Off stopNote(midiToNoteName(note), note); } } // 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()