awacke1 commited on
Commit
730e20f
·
verified ·
1 Parent(s): d110dc7

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +287 -119
app.py CHANGED
@@ -1,141 +1,309 @@
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
- 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,
79
- stdout=subprocess.PIPE,
80
- stderr=subprocess.PIPE,
81
- text=True,
82
- bufsize=1
83
- )
84
- self.running = True
85
- threading.Thread(target=self._process_events, daemon=True).start()
86
- return True
87
- except Exception as e:
88
- st.error(f"Failed to start FluidSynth: {str(e)}")
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')
116
- self.process.stdin.flush()
117
- except:
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
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import streamlit as st
2
  import streamlit.components.v1 as components
 
 
 
3
  import threading
4
+ import time
5
  from queue import Queue
 
 
6
 
7
+ import mido # For MIDI I/O
8
+ from mido import Message
9
+ from pyo import Server, Sine, SfPlayer, Mixer, Notein, MidiAdsr
10
 
11
+ # =========================
12
+ # 1) AUDIO ENGINE (pyo)
13
+ # =========================
14
+
15
+ # We’ll create a pyo Server at module load. Adjust sample rate, buffers, etc. as needed.
16
+ # In many environments, pyo wants to open an audio stream. This might conflict with
17
+ # Streamlit’s runtime if it's not set up for real-time audio.
18
+ # We'll do a basic attempt:
19
+ AUDIO_SR = 44100
20
+ s = Server(sr=AUDIO_SR, nchnls=2, buffersize=1024, duplex=1).boot()
21
+ s.start()
22
+
23
+ # We'll keep a global dictionary of active pyo objects for "notes" to allow polyphony.
24
+ active_oscillators = {}
25
+
26
+ # A simple function to generate or retrieve an oscillator for a given note.
27
+ def note_on(note, velocity=100, synth_type='sine'):
 
 
 
28
  """
29
+ Trigger or re-trigger a note with pyo-based oscillator or sample player.
30
  """
31
+ # Example approach: a simple sine wave whose frequency is set by MIDI note number
32
+ freq = mido.midifrequencies[note] # Mido has a built-in freq table
33
+ amp = velocity / 127.0 * 0.3 # scale amplitude by velocity, 0.3 is arbitrary
34
+
35
+ if note not in active_oscillators:
36
+ # Create a new oscillator for that note
37
+ if synth_type == 'sine':
38
+ osc = Sine(freq=freq, mul=amp).out()
39
+ else:
40
+ # For demonstration, we can also do a sample-based approach if you want:
41
+ # osc = SfPlayer("path_to_some_sample.wav", speed=1, loop=False, mul=amp).out()
42
+ osc = Sine(freq=freq, mul=amp).out()
43
+ active_oscillators[note] = osc
44
+ else:
45
+ # If the note is already on, you could re-trigger or update amplitude, etc.
46
+ osc = active_oscillators[note]
47
+ osc.setFreq(freq)
48
+ osc.mul = amp
49
+
50
+ def note_off(note):
51
  """
52
+ Stop a note by turning off or freeing the oscillator.
53
  """
54
+ if note in active_oscillators:
55
+ osc = active_oscillators[note]
56
+ osc.stop() # immediately stop
57
+ del active_oscillators[note]
58
+
59
+ # If you want a more advanced poly-synth approach, you might consider `Notein`, `MidiAdsr`, etc.
60
+
61
+ # =========================
62
+ # 2) DRUM / LOOPS
63
+ # =========================
64
+
65
+ # For drum pads, we can load multiple short samples.
66
+ # We'll store them in a dictionary to trigger by index or note number:
67
+ drum_samples = {
68
+ 0: "samples/kick.wav",
69
+ 1: "samples/snare.wav",
70
+ 2: "samples/hihat.wav",
71
+ 3: "samples/clap.wav",
72
+ # ...
73
+ }
74
+
75
+ def drum_trigger(index, velocity=100):
76
+ """Simple function to trigger a drum sample from a dictionary of sample files."""
77
+ if index not in drum_samples:
78
+ return
79
+ vol = velocity / 127.0 * 0.8
80
+ # Create a one-shot player
81
+ sfp = SfPlayer(drum_samples[index], loop=False, mul=vol).out()
82
+
83
+ # =========================
84
+ # 3) ARPEGGIATOR EXAMPLE
85
+ # =========================
86
+
87
+ class Arpeggiator:
88
+ def __init__(self, bpm=120):
89
+ self.bpm = bpm
90
+ self.notes_held = set()
91
  self.running = False
92
+ self.thread = None
 
 
93
 
94
  def start(self):
95
+ if self.running:
96
+ return
97
+ self.running = True
98
+ self.thread = threading.Thread(target=self.run, daemon=True)
99
+ self.thread.start()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
 
101
  def stop(self):
 
102
  self.running = False
103
+ if self.thread:
104
+ self.thread.join()
 
105
 
106
+ def run(self):
107
+ # Very simple up pattern
108
+ delay = 60.0 / self.bpm / 2.0 # half of a quarter note => eighth notes
109
+ idx = 0
110
  while self.running:
111
+ if self.notes_held:
112
+ sorted_notes = sorted(list(self.notes_held))
113
+ note = sorted_notes[idx % len(sorted_notes)]
114
+ note_on(note, 100) # arpeggiator triggers a note_on
115
+ time.sleep(delay * 0.5)
116
+ note_off(note) # note_off after half the step
117
+ time.sleep(delay * 0.5)
118
+ idx += 1
119
+ else:
120
+ time.sleep(0.01)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
 
122
+ def note_on(self, note):
123
+ self.notes_held.add(note)
124
+
125
+ def note_off(self, note):
126
+ if note in self.notes_held:
127
+ self.notes_held.remove(note)
128
+
129
+ # =========================
130
+ # 4) HTML + JS
131
+ # =========================
132
+
133
+ def get_keyboard_html():
134
  """
135
+ Returns an HTML snippet for a 5-octave Qwerty-Hancock keyboard
136
+ from 'C3' upward.
137
  """
138
+ return """
139
+ <!DOCTYPE html>
140
+ <html>
141
+ <head>
142
+ <style>
143
+ #keyboard {
144
+ margin: 0 auto;
145
+ }
146
+ .qwerty-hancock-wrapper {
147
+ width: 900px; /* Adjust to taste */
148
+ margin: 0 auto;
149
+ }
150
+ </style>
151
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/qwerty-hancock/0.10.0/qwerty-hancock.min.js"></script>
152
+ </head>
153
+ <body>
154
+ <div class="qwerty-hancock-wrapper">
155
+ <div id="keyboard"></div>
156
+ </div>
157
+ <script>
158
+ // 5 octaves from C3 to (C3 + 5 octaves => C8 is beyond 5, but let's do ~ C7).
159
+ const keyboard = new QwertyHancock({
160
+ id: 'keyboard',
161
+ width: 900,
162
+ height: 150,
163
+ octaves: 5,
164
+ startNote: 'C3',
165
+ whiteKeyColour: 'white',
166
+ blackKeyColour: '#444',
167
+ activeColour: '#FF6961'
168
+ });
169
 
170
+ // Build a note->MIDI dictionary. We'll do some approximate mappings:
171
+ // C3=48, C#3=49, ... up to B7 or so. Expand as needed.
172
+ // We'll hardcode for demonstration, or generate dynamically.
173
+ const noteToMidi = {
174
+ 'C3':48,'C#3':49,'D3':50,'D#3':51,'E3':52,'F3':53,'F#3':54,'G3':55,'G#3':56,'A3':57,'A#3':58,'B3':59,
175
+ '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,
176
+ '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,
177
+ 'C6':84,'C#6':85,'D6':86,'D#6':87,'E6':88,'F6':89,'F#6':90,'G6':91,'G#6':92,'A6':93,'A#6':94,'B6':95,
178
+ 'C7':96,'C#7':97,'D7':98,'D#7':99,'E7':100,'F7':101,'F#7':102,'G7':103,'G#7':104,'A7':105,'A#7':106,'B7':107
179
+ };
180
+
181
+ keyboard.keyDown = function (note, freq) {
182
+ const midiNote = noteToMidi[note];
183
+ if(midiNote !== undefined){
184
+ window.parent.postMessage({
185
+ type: 'midiEvent',
186
+ data: {
187
+ type: 'noteOn',
188
+ note: midiNote,
189
+ velocity: 100
190
+ }
191
+ }, '*');
192
+ }
193
+ };
194
+
195
+ keyboard.keyUp = function (note, freq) {
196
+ const midiNote = noteToMidi[note];
197
+ if(midiNote !== undefined){
198
+ window.parent.postMessage({
199
+ type: 'midiEvent',
200
+ data: {
201
+ type: 'noteOff',
202
+ note: midiNote,
203
+ velocity: 0
204
+ }
205
+ }, '*');
206
+ }
207
+ };
208
+ </script>
209
+ </body>
210
+ </html>
211
+ """
212
+
213
+ def get_drum_pads_html():
214
+ """
215
+ Returns an HTML snippet for a 4x4 (16) grid of drum pads.
216
+ Each pad sends a 'drumTrigger' event with index 0..15.
217
+ """
218
+ return """
219
+ <!DOCTYPE html>
220
+ <html>
221
+ <head>
222
+ <style>
223
+ .drum-grid {
224
+ display: grid;
225
+ grid-template-columns: repeat(4, 80px);
226
+ grid-gap: 10px;
227
+ width: max-content;
228
+ margin: 0 auto;
229
+ }
230
+ .drum-pad {
231
+ width: 80px;
232
+ height: 80px;
233
+ background-color: #666;
234
+ display: flex;
235
+ align-items: center;
236
+ justify-content: center;
237
+ color: #fff;
238
+ font-weight: bold;
239
+ font-size: 1.2em;
240
+ cursor: pointer;
241
+ user-select: none;
242
+ border-radius: 8px;
243
+ }
244
+ .drum-pad:active {
245
+ background-color: #999;
246
+ }
247
+ </style>
248
+ </head>
249
+ <body>
250
+ <div class="drum-grid">
251
+ <div class="drum-pad" data-pad="0">Pad 1</div>
252
+ <div class="drum-pad" data-pad="1">Pad 2</div>
253
+ <div class="drum-pad" data-pad="2">Pad 3</div>
254
+ <div class="drum-pad" data-pad="3">Pad 4</div>
255
+ <div class="drum-pad" data-pad="4">Pad 5</div>
256
+ <div class="drum-pad" data-pad="5">Pad 6</div>
257
+ <div class="drum-pad" data-pad="6">Pad 7</div>
258
+ <div class="drum-pad" data-pad="7">Pad 8</div>
259
+ <div class="drum-pad" data-pad="8">Pad 9</div>
260
+ <div class="drum-pad" data-pad="9">Pad10</div>
261
+ <div class="drum-pad" data-pad="10">Pad11</div>
262
+ <div class="drum-pad" data-pad="11">Pad12</div>
263
+ <div class="drum-pad" data-pad="12">Pad13</div>
264
+ <div class="drum-pad" data-pad="13">Pad14</div>
265
+ <div class="drum-pad" data-pad="14">Pad15</div>
266
+ <div class="drum-pad" data-pad="15">Pad16</div>
267
+ </div>
268
+
269
+ <script>
270
+ document.querySelectorAll('.drum-pad').forEach(pad => {
271
+ pad.addEventListener('mousedown', () => {
272
+ let padIndex = parseInt(pad.getAttribute('data-pad'));
273
+ window.parent.postMessage({
274
+ type: 'drumTrigger',
275
+ data: {
276
+ padIndex: padIndex,
277
+ velocity: 100
278
+ }
279
+ }, '*');
280
+ });
281
+ });
282
+ </script>
283
+ </body>
284
+ </html>
285
+ """
286
+
287
+ # =========================
288
+ # 5) STREAMLIT APP
289
+ # =========================
290
+
291
+ def main():
292
+ st.title("Python Synth with 5-Octave Keyboard + Drum Pads (pyo-based)")
293
+
294
+ # Arpeggiator in session state
295
+ if 'arpeggiator' not in st.session_state:
296
+ st.session_state.arpeggiator = Arpeggiator(bpm=120)
297
+
298
+ # BPM slider
299
+ st.session_state.arpeggiator.bpm = st.slider("Arpeggiator BPM", 60, 240, 120)
300
+ use_arp = st.checkbox("Enable Arpeggiator", value=False)
301
+ if use_arp:
302
+ st.session_state.arpeggiator.start()
303
+ else:
304
+ st.session_state.arpeggiator.stop()
305
+
306
+ # MIDI I/O
307
+ st.subheader("MIDI Ports")
308
+ in_ports = mido.get_input_names()
309
+ out_