Spaces:
Sleeping
Sleeping
import streamlit as st | |
import streamlit.components.v1 as components | |
import rtmidi | |
import json | |
from pathlib import Path | |
import time | |
def create_midi_output(): | |
"""Initialize MIDI output""" | |
midi_out = rtmidi.MidiOut() | |
available_ports = midi_out.get_ports() | |
if available_ports: | |
st.sidebar.write("Available MIDI ports:", available_ports) | |
selected_port = st.sidebar.selectbox("Select MIDI Output", range(len(available_ports)), | |
format_func=lambda x: available_ports[x]) | |
midi_out.open_port(selected_port) | |
else: | |
st.sidebar.warning("No MIDI output ports available") | |
midi_out.open_virtual_port("Virtual MIDI Output") | |
return midi_out | |
def get_piano_html(): | |
"""Return the HTML content for the piano keyboard""" | |
return """ | |
<!DOCTYPE html> | |
<html> | |
<head> | |
<style> | |
#keyboard-container { | |
position: relative; | |
width: 100%; | |
max-width: 800px; | |
margin: 20px auto; | |
} | |
.note-label { | |
position: absolute; | |
bottom: 5px; | |
width: 100%; | |
text-align: center; | |
font-size: 12px; | |
pointer-events: none; | |
} | |
.white-note { color: #333; } | |
.black-note { color: #fff; } | |
</style> | |
</head> | |
<body> | |
<div id="keyboard-container"> | |
<div id="keyboard"></div> | |
</div> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/qwerty-hancock/0.10.0/qwerty-hancock.min.js"></script> | |
<script> | |
// Initialize keyboard | |
const keyboard = new QwertyHancock({ | |
id: 'keyboard', | |
width: 800, | |
height: 150, | |
octaves: 2, | |
startNote: 'C4', | |
whiteKeyColour: 'white', | |
blackKeyColour: '#333', | |
activeColour: '#88c6ff' | |
}); | |
// Note to MIDI number mapping | |
const noteToMidi = { | |
'C4': 60, 'C#4': 61, 'D4': 62, 'D#4': 63, 'E4': 64, 'F4': 65, | |
'F#4': 66, 'G4': 67, 'G#4': 68, 'A4': 69, 'A#4': 70, 'B4': 71, | |
'C5': 72, 'C#5': 73, 'D5': 74, 'D#5': 75, 'E5': 76, 'F5': 77, | |
'F#5': 78, 'G5': 79, 'G#5': 80, 'A5': 81, 'A#5': 82, 'B5': 83 | |
}; | |
// Add note labels | |
function addNoteLabels() { | |
const container = document.getElementById('keyboard'); | |
const whiteKeys = container.querySelectorAll('[data-note-type="white"]'); | |
const blackKeys = container.querySelectorAll('[data-note-type="black"]'); | |
whiteKeys.forEach(key => { | |
const note = key.getAttribute('data-note'); | |
const label = document.createElement('div'); | |
label.className = 'note-label white-note'; | |
label.textContent = noteToMidi[note]; | |
key.appendChild(label); | |
}); | |
blackKeys.forEach(key => { | |
const note = key.getAttribute('data-note'); | |
const label = document.createElement('div'); | |
label.className = 'note-label black-note'; | |
label.textContent = noteToMidi[note]; | |
key.appendChild(label); | |
}); | |
} | |
// Send MIDI events to Streamlit | |
keyboard.keyDown = function(note, frequency) { | |
const midiNote = noteToMidi[note]; | |
const event = { | |
type: 'noteOn', | |
note: midiNote, | |
velocity: 100 | |
}; | |
window.parent.postMessage({type: 'midiEvent', data: event}, '*'); | |
}; | |
keyboard.keyUp = function(note, frequency) { | |
const midiNote = noteToMidi[note]; | |
const event = { | |
type: 'noteOff', | |
note: midiNote, | |
velocity: 0 | |
}; | |
window.parent.postMessage({type: 'midiEvent', data: event}, '*'); | |
}; | |
// Wait for the keyboard to be rendered | |
setTimeout(addNoteLabels, 100); | |
</script> | |
</body> | |
</html> | |
""" | |
def main(): | |
st.title("MIDI Piano Keyboard") | |
st.write("Click keys or use your computer keyboard (A-K and W-U for white and black keys)") | |
# Initialize MIDI output | |
midi_out = create_midi_output() | |
# Create a placeholder for MIDI messages | |
midi_message_placeholder = st.empty() | |
# Display the piano keyboard | |
components.html( | |
get_piano_html(), | |
height=200, | |
scrolling=False | |
) | |
# Handle MIDI events from JavaScript | |
if 'midi_events' not in st.session_state: | |
st.session_state.midi_events = [] | |
def handle_midi_event(event): | |
if event['type'] == 'noteOn': | |
midi_out.send_message([0x90, event['note'], event['velocity']]) | |
midi_message_placeholder.write(f"Note On: {event['note']}") | |
elif event['type'] == 'noteOff': | |
midi_out.send_message([0x80, event['note'], event['velocity']]) | |
midi_message_placeholder.write(f"Note Off: {event['note']}") | |
# JavaScript callback handler | |
components.html( | |
""" | |
<script> | |
window.addEventListener('message', function(e) { | |
if (e.data.type === 'midiEvent') { | |
window.parent.postMessage({ | |
type: 'streamlit:message', | |
data: { | |
type: 'midi_event', | |
event: e.data.data | |
} | |
}, '*'); | |
} | |
}); | |
</script> | |
""", | |
height=0 | |
) | |
# Add a loop to continuously check for new MIDI events | |
while True: | |
time.sleep(0.1) # Small delay to prevent excessive CPU usage | |
# Process any pending MIDI events | |
for event in st.session_state.midi_events: | |
handle_midi_event(event) | |
st.session_state.midi_events = [] | |
if __name__ == "__main__": | |
main() |