Spaces:
Sleeping
Sleeping
| import streamlit as st | |
| import streamlit.components.v1 as components | |
| import subprocess | |
| import time | |
| import os | |
| import threading | |
| from queue import Queue | |
| import json | |
| def install_fluidsynth(): | |
| """Check and install FluidSynth if needed""" | |
| try: | |
| subprocess.run(['fluidsynth', '--version'], capture_output=True) | |
| return True | |
| except FileNotFoundError: | |
| st.error("FluidSynth not found. Installing required packages...") | |
| try: | |
| subprocess.run(['sudo', 'apt-get', 'update'], check=True) | |
| subprocess.run(['sudo', 'apt-get', 'install', '-y', 'fluidsynth'], check=True) | |
| return True | |
| except subprocess.CalledProcessError as e: | |
| st.error(f"Failed to install FluidSynth: {str(e)}") | |
| st.code("sudo apt-get install -y fluidsynth") | |
| return False | |
| def download_soundfont(): | |
| """Download a free soundfont if not present""" | |
| soundfont_path = "GeneralUser GS v1.471.sf2" | |
| if not os.path.exists(soundfont_path): | |
| st.info("Downloading soundfont...") | |
| try: | |
| subprocess.run([ | |
| 'wget', | |
| 'https://storage.googleapis.com/google-code-archive-downloads/v2/code.google.com/musicbox/GeneralUser%20GS%20v1.471.sf2', | |
| '-O', | |
| soundfont_path | |
| ], check=True) | |
| return True | |
| except subprocess.CalledProcessError as e: | |
| st.error(f"Failed to download soundfont: {str(e)}") | |
| return False | |
| return True | |
| class FluidSynthPlayer: | |
| def __init__(self, soundfont_path): | |
| self.soundfont_path = soundfont_path | |
| self.process = None | |
| self.event_queue = Queue() | |
| self.running = False | |
| def start(self): | |
| """Start FluidSynth process""" | |
| try: | |
| self.process = subprocess.Popen( | |
| [ | |
| 'fluidsynth', | |
| '-a', 'pulseaudio', # Use PulseAudio | |
| '-g', '2.0', # Gain (volume) | |
| self.soundfont_path | |
| ], | |
| stdin=subprocess.PIPE, | |
| stdout=subprocess.PIPE, | |
| stderr=subprocess.PIPE, | |
| text=True, | |
| bufsize=1 | |
| ) | |
| self.running = True | |
| threading.Thread(target=self._process_events, daemon=True).start() | |
| return True | |
| except Exception as e: | |
| st.error(f"Failed to start FluidSynth: {str(e)}") | |
| return False | |
| def stop(self): | |
| """Stop FluidSynth process""" | |
| self.running = False | |
| if self.process: | |
| self.process.terminate() | |
| self.process.wait() | |
| def _process_events(self): | |
| """Process MIDI events from queue""" | |
| while self.running: | |
| try: | |
| event = self.event_queue.get(timeout=0.1) | |
| if event['type'] == 'noteOn': | |
| self._send_command(f"noteon 0 {event['note']} {event['velocity']}") | |
| elif event['type'] == 'noteOff': | |
| self._send_command(f"noteoff 0 {event['note']}") | |
| except: | |
| continue | |
| def _send_command(self, command): | |
| """Send command to FluidSynth process""" | |
| if self.process and self.process.poll() is None: | |
| try: | |
| self.process.stdin.write(command + '\n') | |
| self.process.stdin.flush() | |
| except: | |
| pass | |
| def queue_event(self, event): | |
| """Add MIDI event to queue""" | |
| self.event_queue.put(event) | |
| 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> | |
| const keyboard = new QwertyHancock({ | |
| id: 'keyboard', | |
| width: 800, | |
| height: 150, | |
| octaves: 2, | |
| startNote: 'C4', | |
| whiteKeyColour: 'white', | |
| blackKeyColour: '#333', | |
| activeColour: '#88c6ff' | |
| }); | |
| 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 | |
| }; | |
| 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); | |
| }); | |
| } | |
| 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}, '*'); | |
| }; | |
| setTimeout(addNoteLabels, 100); | |
| </script> | |
| </body> | |
| </html> | |
| """ | |
| def main(): | |
| st.title("Piano Keyboard with FluidSynth") | |
| st.write("Click keys or use your computer keyboard (A-K and W-U for white and black keys)") | |
| # Check and install FluidSynth if needed | |
| if not install_fluidsynth(): | |
| return | |
| # Download soundfont if needed | |
| if not download_soundfont(): | |
| return | |
| # Initialize FluidSynth | |
| if 'synth' not in st.session_state: | |
| st.session_state.synth = FluidSynthPlayer("GeneralUser GS v1.471.sf2") | |
| if not st.session_state.synth.start(): | |
| st.error("Failed to start FluidSynth. Please check your audio setup.") | |
| return | |
| # Create a placeholder for messages | |
| 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): | |
| st.session_state.synth.queue_event(event) | |
| if event['type'] == 'noteOn': | |
| message_placeholder.write(f"Note On: {event['note']}") | |
| else: | |
| 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 | |
| ) | |
| # Cleanup on session end | |
| def cleanup(): | |
| if 'synth' in st.session_state: | |
| st.session_state.synth.stop() | |
| st.on_session_ended(cleanup) | |
| if __name__ == "__main__": | |
| main() |