awacke1's picture
Update app.py
08ae50b verified
raw
history blame
11.6 kB
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()