import streamlit as st import streamlit.components.v1 as components import subprocess import time import os import threading from queue import Queue import json import mido # ------------------------------------------------------------ # Utility Functions # ------------------------------------------------------------ def install_fluidsynth(): """ Check if 'fluidsynth' CLI is installed. Attempt to install if not found. On non-Linux systems, you’ll need to manually install fluidsynth. """ try: subprocess.run(['fluidsynth', '--version'], capture_output=True, check=True) return True except (FileNotFoundError, subprocess.CalledProcessError): st.error("FluidSynth not found. Attempting automatic install (Linux only).") 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 automatically: {str(e)}") st.code("Please install fluidsynth manually.") return False def download_soundfont(sf_filename="GeneralUser_GS_v1.471.sf2"): """ Downloads a free SoundFont if it's not already present. """ if not os.path.exists(sf_filename): 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', sf_filename ], check=True) except subprocess.CalledProcessError as e: st.error(f"Failed to download soundfont: {str(e)}") return False return True # ------------------------------------------------------------ # FluidSynth Handling Class # ------------------------------------------------------------ class FluidSynthPlayer: """ Wraps the fluidsynth CLI process, sends commands (like note on/off). """ def __init__(self, soundfont_path, gain=2.0, audio_driver='pulseaudio'): self.soundfont_path = soundfont_path self.process = None self.running = False self.event_queue = Queue() self.gain = str(gain) self.audio_driver = audio_driver def start(self): """Start FluidSynth as a subprocess.""" try: self.process = subprocess.Popen( [ 'fluidsynth', '-a', self.audio_driver, '-g', self.gain, 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 the FluidSynth process.""" self.running = False if self.process: self.process.terminate() self.process.wait() def _process_events(self): """Thread to process MIDI events from queue and send to fluidsynth.""" while self.running: try: event = self.event_queue.get(timeout=0.1) msg_type = event['type'] if msg_type == 'noteOn': self._send_command(f"noteon 0 {event['note']} {event['velocity']}") elif msg_type == 'noteOff': self._send_command(f"noteoff 0 {event['note']}") except: continue def _send_command(self, command): """Send a textual command to fluidsynth.""" 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 a MIDI event to the fluidsynth queue.""" self.event_queue.put(event) # ------------------------------------------------------------ # Basic Arpeggiator # ------------------------------------------------------------ class Arpeggiator: """ A simple arpeggiator that cycles through held notes at a certain speed. """ def __init__(self, synth_player, bpm=120): self.synth_player = synth_player self.bpm = bpm self.notes_held = set() self.thread = None self.running = False def start(self): if self.running: