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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +56 -187
app.py CHANGED
@@ -6,56 +6,73 @@ 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,
@@ -72,26 +89,27 @@ class FluidSynthPlayer:
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')
@@ -100,173 +118,24 @@ class FluidSynthPlayer:
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"""
108
- return """
109
- <!DOCTYPE html>
110
- <html>
111
- <head>
112
- <style>
113
- #keyboard-container {
114
- position: relative;
115
- width: 100%;
116
- max-width: 800px;
117
- margin: 20px auto;
118
- }
119
- .note-label {
120
- position: absolute;
121
- bottom: 5px;
122
- width: 100%;
123
- text-align: center;
124
- font-size: 12px;
125
- pointer-events: none;
126
- }
127
- .white-note { color: #333; }
128
- .black-note { color: #fff; }
129
- </style>
130
- </head>
131
- <body>
132
- <div id="keyboard-container">
133
- <div id="keyboard"></div>
134
- </div>
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,
141
- height: 150,
142
- octaves: 2,
143
- startNote: 'C4',
144
- whiteKeyColour: 'white',
145
- blackKeyColour: '#333',
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,
152
- 'C5': 72, 'C#5': 73, 'D5': 74, 'D#5': 75, 'E5': 76, 'F5': 77,
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"]');
159
- const blackKeys = container.querySelectorAll('[data-note-type="black"]');
160
-
161
- whiteKeys.forEach(key => {
162
- const note = key.getAttribute('data-note');
163
- const label = document.createElement('div');
164
- label.className = 'note-label white-note';
165
- label.textContent = noteToMidi[note];
166
- key.appendChild(label);
167
- });
168
-
169
- blackKeys.forEach(key => {
170
- const note = key.getAttribute('data-note');
171
- const label = document.createElement('div');
172
- label.className = 'note-label black-note';
173
- label.textContent = noteToMidi[note];
174
- key.appendChild(label);
175
- });
176
- }
177
-
178
- keyboard.keyDown = function(note, frequency) {
179
- const midiNote = noteToMidi[note];
180
- const event = {
181
- type: 'noteOn',
182
- note: midiNote,
183
- velocity: 100
184
- };
185
- window.parent.postMessage({type: 'midiEvent', data: event}, '*');
186
- };
187
-
188
- keyboard.keyUp = function(note, frequency) {
189
- const midiNote = noteToMidi[note];
190
- const event = {
191
- type: 'noteOff',
192
- note: midiNote,
193
- velocity: 0
194
- };
195
- window.parent.postMessage({type: 'midiEvent', data: event}, '*');
196
- };
197
-
198
- setTimeout(addNoteLabels, 100);
199
- </script>
200
- </body>
201
- </html>
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(
228
- get_piano_html(),
229
- height=200,
230
- scrolling=False
231
- )
232
-
233
- # Handle MIDI events from JavaScript
234
- if 'midi_events' not in st.session_state:
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(
246
- """
247
- <script>
248
- window.addEventListener('message', function(e) {
249
- if (e.data.type === 'midiEvent') {
250
- window.parent.postMessage({
251
- type: 'streamlit:message',
252
- data: {
253
- type: 'midi_event',
254
- event: e.data.data
255
- }
256
- }, '*');
257
- }
258
- });
259
- </script>
260
- """,
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()
 
6
  import threading
7
  from queue import Queue
8
  import json
9
+ import mido
10
+
11
+ # ------------------------------------------------------------
12
+ # Utility Functions
13
+ # ------------------------------------------------------------
14
 
15
  def install_fluidsynth():
16
+ """
17
+ Check if 'fluidsynth' CLI is installed. Attempt to install if not found.
18
+ On non-Linux systems, you’ll need to manually install fluidsynth.
19
+ """
20
  try:
21
+ subprocess.run(['fluidsynth', '--version'], capture_output=True, check=True)
22
  return True
23
+ except (FileNotFoundError, subprocess.CalledProcessError):
24
+ st.error("FluidSynth not found. Attempting automatic install (Linux only).")
25
  try:
26
  subprocess.run(['sudo', 'apt-get', 'update'], check=True)
27
  subprocess.run(['sudo', 'apt-get', 'install', '-y', 'fluidsynth'], check=True)
28
  return True
29
  except subprocess.CalledProcessError as e:
30
+ st.error(f"Failed to install FluidSynth automatically: {str(e)}")
31
+ st.code("Please install fluidsynth manually.")
32
  return False
33
 
34
+ def download_soundfont(sf_filename="GeneralUser_GS_v1.471.sf2"):
35
+ """
36
+ Downloads a free SoundFont if it's not already present.
37
+ """
38
+ if not os.path.exists(sf_filename):
39
  st.info("Downloading soundfont...")
40
  try:
41
  subprocess.run([
42
  'wget',
43
  'https://storage.googleapis.com/google-code-archive-downloads/v2/code.google.com/musicbox/GeneralUser%20GS%20v1.471.sf2',
44
  '-O',
45
+ sf_filename
46
  ], check=True)
 
47
  except subprocess.CalledProcessError as e:
48
  st.error(f"Failed to download soundfont: {str(e)}")
49
  return False
50
  return True
51
 
52
+ # ------------------------------------------------------------
53
+ # FluidSynth Handling Class
54
+ # ------------------------------------------------------------
55
+
56
  class FluidSynthPlayer:
57
+ """
58
+ Wraps the fluidsynth CLI process, sends commands (like note on/off).
59
+ """
60
+ def __init__(self, soundfont_path, gain=2.0, audio_driver='pulseaudio'):
61
  self.soundfont_path = soundfont_path
62
  self.process = None
 
63
  self.running = False
64
+ self.event_queue = Queue()
65
+ self.gain = str(gain)
66
+ self.audio_driver = audio_driver
67
 
68
  def start(self):
69
+ """Start FluidSynth as a subprocess."""
70
  try:
71
  self.process = subprocess.Popen(
72
  [
73
  'fluidsynth',
74
+ '-a', self.audio_driver,
75
+ '-g', self.gain,
76
  self.soundfont_path
77
  ],
78
  stdin=subprocess.PIPE,
 
89
  return False
90
 
91
  def stop(self):
92
+ """Stop the FluidSynth process."""
93
  self.running = False
94
  if self.process:
95
  self.process.terminate()
96
  self.process.wait()
97
 
98
  def _process_events(self):
99
+ """Thread to process MIDI events from queue and send to fluidsynth."""
100
  while self.running:
101
  try:
102
  event = self.event_queue.get(timeout=0.1)
103
+ msg_type = event['type']
104
+ if msg_type == 'noteOn':
105
  self._send_command(f"noteon 0 {event['note']} {event['velocity']}")
106
+ elif msg_type == 'noteOff':
107
  self._send_command(f"noteoff 0 {event['note']}")
108
  except:
109
  continue
110
 
111
  def _send_command(self, command):
112
+ """Send a textual command to fluidsynth."""
113
  if self.process and self.process.poll() is None:
114
  try:
115
  self.process.stdin.write(command + '\n')
 
118
  pass
119
 
120
  def queue_event(self, event):
121
+ """Add a MIDI event to the fluidsynth queue."""
122
  self.event_queue.put(event)
123
 
124
+ # ------------------------------------------------------------
125
+ # Basic Arpeggiator
126
+ # ------------------------------------------------------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
 
128
+ class Arpeggiator:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
  """
130
+ A simple arpeggiator that cycles through held notes at a certain speed.
131
+ """
132
+ def __init__(self, synth_player, bpm=120):
133
+ self.synth_player = synth_player
134
+ self.bpm = bpm
135
+ self.notes_held = set()
136
+ self.thread = None
137
+ self.running = False
138
 
139
+ def start(self):
140
+ if self.running:
141
+