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