Spaces:
Sleeping
Sleeping
import streamlit as st | |
import streamlit.components.v1 as components | |
import mido | |
from queue import Queue | |
import threading | |
# Store MIDI messages in session state | |
if 'incoming_midi' not in st.session_state: | |
st.session_state.incoming_midi = Queue() | |
def midi_in_callback(msg): | |
if msg.type in ['note_on', 'note_off']: | |
st.session_state.incoming_midi.put({ | |
'type': 'noteOn' if msg.type == 'note_on' else 'noteOff', | |
'note': msg.note, | |
'velocity': msg.velocity | |
}) | |
def open_midi_port(port_name: str, is_input: bool): | |
if port_name == "None": | |
return None | |
return mido.open_input(port_name, callback=midi_in_callback) if is_input else mido.open_output(port_name) | |
def main(): | |
st.title("5-Octave Synth with Arpeggiator & Drum Pads") | |
# MIDI port selection | |
ports = {"in": ["None"] + mido.get_input_names(), | |
"out": ["None"] + mido.get_output_names()} | |
selections = { | |
"in": st.selectbox("MIDI Input", ports["in"]), | |
"out": st.selectbox("MIDI Output", ports["out"]) | |
} | |
# Initialize/update MIDI ports | |
for direction in ["in", "out"]: | |
key = f'midi_{direction}' | |
if key not in st.session_state: | |
st.session_state[key] = None | |
curr_port = st.session_state[key] | |
if curr_port and curr_port.name != selections[direction]: | |
curr_port.close() | |
st.session_state[key] = open_midi_port(selections[direction], direction=="in") | |
elif not curr_port and selections[direction] != "None": | |
st.session_state[key] = open_midi_port(selections[direction], direction=="in") | |
# Load and embed synth interface | |
components.html(get_synth_interface(), height=800) | |
# Event handling setup | |
if 'js_events' not in st.session_state: | |
st.session_state.js_events = Queue() | |
if 'browser_events' not in st.session_state: | |
from collections import deque | |
st.session_state.browser_events = deque() | |
# Process hardware MIDI | |
while not st.session_state.incoming_midi.empty(): | |
evt = st.session_state.incoming_midi.get_nowait() | |
st.session_state.js_events.put(evt) | |
# Send events to browser | |
js_events = [] | |
while not st.session_state.js_events.empty(): | |
js_events.append(st.session_state.js_events.get_nowait()) | |
if js_events: | |
js_code = "<script>\n" | |
for evt in js_events: | |
js_code += f""" | |
window.postMessage({{ | |
type: 'hardwareMidi', | |
data: {{ | |
eventType: '{evt["type"]}', | |
note: {evt["note"]}, | |
velocity: {evt.get("velocity", 100)} | |
}} | |
}}, '*');\n""" | |
js_code += "</script>" | |
components.html(js_code, height=0) | |
# Cleanup on session end | |
def cleanup(): | |
for direction in ["in", "out"]: | |
port = st.session_state.get(f'midi_{direction}') | |
if port: | |
port.close() | |
st.on_session_end(cleanup) | |
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 and UI | |
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'); | |
// Send MIDI message | |
window.parent.postMessage({ | |
type: 'toneEvent', | |
eventType: 'noteOn', | |
note: midiNote, | |
velocity: 100 | |
}, '*'); | |
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'); | |
window.parent.postMessage({ | |
type: 'toneEvent', | |
eventType: 'noteOff', | |
note: midiNote, | |
velocity: 0 | |
}, '*'); | |
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); | |
window.parent.postMessage({ | |
type: 'toneEvent', | |
eventType: 'drum', | |
padIndex: index, | |
velocity: 100 | |
}, '*'); | |
} | |
// Handle incoming MIDI messages | |
window.addEventListener('message', e => { | |
if (e.data?.type === 'hardwareMidi') { | |
const { eventType, note, velocity } = e.data.data; | |
const noteName = midiToNoteName(note); | |
if (eventType === 'noteOn') { | |
playNote(noteName, note); | |
} else if (eventType === 'noteOff') { | |
stopNote(noteName, note); | |
} | |
} | |
}); | |
function midiToNoteName(midi) { | |
const octave = Math.floor(midi / 12) - 1; | |
return noteNames[midi % 12] + octave; | |
} | |
</script> | |
</body> | |
</html> | |
""" | |
if __name__ == "__main__": | |
main() |