awacke1 commited on
Commit
90df7ac
·
verified ·
1 Parent(s): 827e184

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +121 -83
app.py CHANGED
@@ -1,70 +1,107 @@
1
  import streamlit as st
2
  import streamlit.components.v1 as components
3
- import json
4
- from pathlib import Path
5
- import time
6
  import subprocess
7
- import sys
 
 
 
 
8
 
9
- def check_midi_prerequisites():
10
- """Check and report MIDI system status"""
11
- issues = []
12
- solutions = []
13
-
14
  try:
15
- import rtmidi
16
- except ImportError:
17
- issues.append("python-rtmidi is not installed")
18
- solutions.append("pip install python-rtmidi")
19
- except OSError as e:
20
- issues.append(f"MIDI system error: {str(e)}")
21
- solutions.extend([
22
- "sudo apt-get install libasound2-dev",
23
- "sudo apt-get install alsa-utils",
24
- "sudo modprobe snd-seq-midi"
25
- ])
26
-
27
- if issues:
28
- st.error("MIDI System Issues Detected")
29
- st.write("Problems found:")
30
- for issue in issues:
31
- st.write(f"- {issue}")
32
-
33
- st.write("Try these solutions:")
34
- for solution in solutions:
35
- st.code(solution)
36
-
37
- st.write("After installing prerequisites, restart the application.")
38
- return False
 
 
 
 
 
39
  return True
40
 
41
- def create_midi_output():
42
- """Initialize MIDI output with fallback options"""
43
- try:
44
- import rtmidi
45
- midi_out = rtmidi.MidiOut()
46
- available_ports = midi_out.get_ports()
47
-
48
- if available_ports:
49
- st.sidebar.write("Available MIDI ports:", available_ports)
50
- selected_port = st.sidebar.selectbox(
51
- "Select MIDI Output",
52
- range(len(available_ports)),
53
- format_func=lambda x: available_ports[x]
 
 
 
 
 
 
 
 
 
54
  )
55
- midi_out.open_port(selected_port)
56
- else:
57
- st.sidebar.warning("No hardware MIDI ports found. Creating virtual port.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  try:
59
- midi_out.open_virtual_port("Virtual MIDI Output")
60
- except rtmidi.SystemError:
61
- st.sidebar.error("Could not create virtual MIDI port. Running in simulation mode.")
62
- return None
63
- return midi_out
64
- except Exception as e:
65
- st.sidebar.error(f"MIDI system unavailable: {str(e)}")
66
- st.sidebar.info("Running in simulation mode (no MIDI output)")
67
- return None
68
 
69
  def get_piano_html():
70
  """Return the HTML content for the piano keyboard"""
@@ -98,7 +135,6 @@ def get_piano_html():
98
 
99
  <script src="https://cdnjs.cloudflare.com/ajax/libs/qwerty-hancock/0.10.0/qwerty-hancock.min.js"></script>
100
  <script>
101
- // Initialize keyboard
102
  const keyboard = new QwertyHancock({
103
  id: 'keyboard',
104
  width: 800,
@@ -110,7 +146,6 @@ def get_piano_html():
110
  activeColour: '#88c6ff'
111
  });
112
 
113
- // Note to MIDI number mapping
114
  const noteToMidi = {
115
  'C4': 60, 'C#4': 61, 'D4': 62, 'D#4': 63, 'E4': 64, 'F4': 65,
116
  'F#4': 66, 'G4': 67, 'G#4': 68, 'A4': 69, 'A#4': 70, 'B4': 71,
@@ -118,7 +153,6 @@ def get_piano_html():
118
  'F#5': 78, 'G5': 79, 'G#5': 80, 'A5': 81, 'A#5': 82, 'B5': 83
119
  };
120
 
121
- // Add note labels
122
  function addNoteLabels() {
123
  const container = document.getElementById('keyboard');
124
  const whiteKeys = container.querySelectorAll('[data-note-type="white"]');
@@ -141,7 +175,6 @@ def get_piano_html():
141
  });
142
  }
143
 
144
- // Send MIDI events to Streamlit
145
  keyboard.keyDown = function(note, frequency) {
146
  const midiNote = noteToMidi[note];
147
  const event = {
@@ -162,7 +195,6 @@ def get_piano_html():
162
  window.parent.postMessage({type: 'midiEvent', data: event}, '*');
163
  };
164
 
165
- // Wait for the keyboard to be rendered
166
  setTimeout(addNoteLabels, 100);
167
  </script>
168
  </body>
@@ -170,18 +202,26 @@ def get_piano_html():
170
  """
171
 
172
  def main():
173
- st.title("MIDI Piano Keyboard")
174
  st.write("Click keys or use your computer keyboard (A-K and W-U for white and black keys)")
175
 
176
- # Check MIDI prerequisites
177
- if not check_midi_prerequisites():
178
  return
179
 
180
- # Initialize MIDI output
181
- midi_out = create_midi_output()
 
182
 
183
- # Create a placeholder for MIDI messages
184
- midi_message_placeholder = st.empty()
 
 
 
 
 
 
 
185
 
186
  # Display the piano keyboard
187
  components.html(
@@ -195,20 +235,11 @@ def main():
195
  st.session_state.midi_events = []
196
 
197
  def handle_midi_event(event):
198
- if midi_out is None:
199
- # Simulation mode - just display the events
200
- if event['type'] == 'noteOn':
201
- midi_message_placeholder.write(f"Simulated Note On: {event['note']} (No MIDI Output)")
202
- else:
203
- midi_message_placeholder.write(f"Simulated Note Off: {event['note']} (No MIDI Output)")
204
- return
205
-
206
  if event['type'] == 'noteOn':
207
- midi_out.send_message([0x90, event['note'], event['velocity']])
208
- midi_message_placeholder.write(f"Note On: {event['note']}")
209
- elif event['type'] == 'noteOff':
210
- midi_out.send_message([0x80, event['note'], event['velocity']])
211
- midi_message_placeholder.write(f"Note Off: {event['note']}")
212
 
213
  # JavaScript callback handler
214
  components.html(
@@ -230,5 +261,12 @@ def main():
230
  height=0
231
  )
232
 
 
 
 
 
 
 
 
233
  if __name__ == "__main__":
234
  main()
 
1
  import streamlit as st
2
  import streamlit.components.v1 as components
 
 
 
3
  import subprocess
4
+ import time
5
+ import os
6
+ import threading
7
+ from queue import Queue
8
+ import json
9
 
10
+ def install_fluidsynth():
11
+ """Check and install FluidSynth if needed"""
 
 
 
12
  try:
13
+ subprocess.run(['fluidsynth', '--version'], capture_output=True)
14
+ return True
15
+ except FileNotFoundError:
16
+ st.error("FluidSynth not found. Installing required packages...")
17
+ try:
18
+ subprocess.run(['sudo', 'apt-get', 'update'], check=True)
19
+ subprocess.run(['sudo', 'apt-get', 'install', '-y', 'fluidsynth'], check=True)
20
+ return True
21
+ except subprocess.CalledProcessError as e:
22
+ st.error(f"Failed to install FluidSynth: {str(e)}")
23
+ st.code("sudo apt-get install -y fluidsynth")
24
+ return False
25
+
26
+ def download_soundfont():
27
+ """Download a free soundfont if not present"""
28
+ soundfont_path = "GeneralUser GS v1.471.sf2"
29
+ if not os.path.exists(soundfont_path):
30
+ st.info("Downloading soundfont...")
31
+ try:
32
+ subprocess.run([
33
+ 'wget',
34
+ 'https://storage.googleapis.com/google-code-archive-downloads/v2/code.google.com/musicbox/GeneralUser%20GS%20v1.471.sf2',
35
+ '-O',
36
+ soundfont_path
37
+ ], check=True)
38
+ return True
39
+ except subprocess.CalledProcessError as e:
40
+ st.error(f"Failed to download soundfont: {str(e)}")
41
+ return False
42
  return True
43
 
44
+ class FluidSynthPlayer:
45
+ def __init__(self, soundfont_path):
46
+ self.soundfont_path = soundfont_path
47
+ self.process = None
48
+ self.event_queue = Queue()
49
+ self.running = False
50
+
51
+ def start(self):
52
+ """Start FluidSynth process"""
53
+ try:
54
+ self.process = subprocess.Popen(
55
+ [
56
+ 'fluidsynth',
57
+ '-a', 'pulseaudio', # Use PulseAudio
58
+ '-g', '2.0', # Gain (volume)
59
+ self.soundfont_path
60
+ ],
61
+ stdin=subprocess.PIPE,
62
+ stdout=subprocess.PIPE,
63
+ stderr=subprocess.PIPE,
64
+ text=True,
65
+ bufsize=1
66
  )
67
+ self.running = True
68
+ threading.Thread(target=self._process_events, daemon=True).start()
69
+ return True
70
+ except Exception as e:
71
+ st.error(f"Failed to start FluidSynth: {str(e)}")
72
+ return False
73
+
74
+ def stop(self):
75
+ """Stop FluidSynth process"""
76
+ self.running = False
77
+ if self.process:
78
+ self.process.terminate()
79
+ self.process.wait()
80
+
81
+ def _process_events(self):
82
+ """Process MIDI events from queue"""
83
+ while self.running:
84
+ try:
85
+ event = self.event_queue.get(timeout=0.1)
86
+ if event['type'] == 'noteOn':
87
+ self._send_command(f"noteon 0 {event['note']} {event['velocity']}")
88
+ elif event['type'] == 'noteOff':
89
+ self._send_command(f"noteoff 0 {event['note']}")
90
+ except:
91
+ continue
92
+
93
+ def _send_command(self, command):
94
+ """Send command to FluidSynth process"""
95
+ if self.process and self.process.poll() is None:
96
  try:
97
+ self.process.stdin.write(command + '\n')
98
+ self.process.stdin.flush()
99
+ except:
100
+ pass
101
+
102
+ def queue_event(self, event):
103
+ """Add MIDI event to queue"""
104
+ self.event_queue.put(event)
 
105
 
106
  def get_piano_html():
107
  """Return the HTML content for the piano keyboard"""
 
135
 
136
  <script src="https://cdnjs.cloudflare.com/ajax/libs/qwerty-hancock/0.10.0/qwerty-hancock.min.js"></script>
137
  <script>
 
138
  const keyboard = new QwertyHancock({
139
  id: 'keyboard',
140
  width: 800,
 
146
  activeColour: '#88c6ff'
147
  });
148
 
 
149
  const noteToMidi = {
150
  'C4': 60, 'C#4': 61, 'D4': 62, 'D#4': 63, 'E4': 64, 'F4': 65,
151
  'F#4': 66, 'G4': 67, 'G#4': 68, 'A4': 69, 'A#4': 70, 'B4': 71,
 
153
  'F#5': 78, 'G5': 79, 'G#5': 80, 'A5': 81, 'A#5': 82, 'B5': 83
154
  };
155
 
 
156
  function addNoteLabels() {
157
  const container = document.getElementById('keyboard');
158
  const whiteKeys = container.querySelectorAll('[data-note-type="white"]');
 
175
  });
176
  }
177
 
 
178
  keyboard.keyDown = function(note, frequency) {
179
  const midiNote = noteToMidi[note];
180
  const event = {
 
195
  window.parent.postMessage({type: 'midiEvent', data: event}, '*');
196
  };
197
 
 
198
  setTimeout(addNoteLabels, 100);
199
  </script>
200
  </body>
 
202
  """
203
 
204
  def main():
205
+ st.title("Piano Keyboard with FluidSynth")
206
  st.write("Click keys or use your computer keyboard (A-K and W-U for white and black keys)")
207
 
208
+ # Check and install FluidSynth if needed
209
+ if not install_fluidsynth():
210
  return
211
 
212
+ # Download soundfont if needed
213
+ if not download_soundfont():
214
+ return
215
 
216
+ # Initialize FluidSynth
217
+ if 'synth' not in st.session_state:
218
+ st.session_state.synth = FluidSynthPlayer("GeneralUser GS v1.471.sf2")
219
+ if not st.session_state.synth.start():
220
+ st.error("Failed to start FluidSynth. Please check your audio setup.")
221
+ return
222
+
223
+ # Create a placeholder for messages
224
+ message_placeholder = st.empty()
225
 
226
  # Display the piano keyboard
227
  components.html(
 
235
  st.session_state.midi_events = []
236
 
237
  def handle_midi_event(event):
238
+ st.session_state.synth.queue_event(event)
 
 
 
 
 
 
 
239
  if event['type'] == 'noteOn':
240
+ message_placeholder.write(f"Note On: {event['note']}")
241
+ else:
242
+ message_placeholder.write(f"Note Off: {event['note']}")
 
 
243
 
244
  # JavaScript callback handler
245
  components.html(
 
261
  height=0
262
  )
263
 
264
+ # Cleanup on session end
265
+ def cleanup():
266
+ if 'synth' in st.session_state:
267
+ st.session_state.synth.stop()
268
+
269
+ st.on_session_ended(cleanup)
270
+
271
  if __name__ == "__main__":
272
  main()