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/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() |