awacke1 commited on
Commit
08ae50b
·
verified ·
1 Parent(s): e4d973d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +268 -403
app.py CHANGED
@@ -4,460 +4,325 @@ import mido
4
  from queue import Queue
5
  import threading
6
 
7
- # We'll store inbound hardware MIDI in a queue and dispatch them to the browser.
8
  if 'incoming_midi' not in st.session_state:
9
  st.session_state.incoming_midi = Queue()
10
 
11
- # A callback for Mido hardware input:
12
  def midi_in_callback(msg):
13
- """
14
- Called in background when hardware MIDI messages arrive.
15
- We'll just forward note_on / note_off to the session's queue.
16
- """
17
  if msg.type in ['note_on', 'note_off']:
18
- event_type = 'noteOn' if msg.type == 'note_on' else 'noteOff'
19
  st.session_state.incoming_midi.put({
20
- 'type': event_type,
21
  'note': msg.note,
22
  'velocity': msg.velocity
23
  })
24
 
25
- def open_midi_input(port_name: str):
26
- if port_name == "None":
27
- return None
28
- return mido.open_input(port_name, callback=midi_in_callback)
29
-
30
- def open_midi_output(port_name: str):
31
  if port_name == "None":
32
  return None
33
- return mido.open_output(port_name)
34
 
35
  def main():
36
- st.title("Browser-based Synth with Tone.js (5-Octave + 16 Pads)")
37
- st.write("""
38
- This demo uses **Tone.js** in the browser to produce audio, so no ALSA or JACK
39
- is required on the server side. Hardware MIDI input is captured by Python
40
- (Mido) and forwarded to the browser so Tone.js can play it.
41
- """)
42
-
43
- # 1. Let user pick hardware MIDI in/out
44
- in_ports = ["None"] + mido.get_input_names()
45
- out_ports = ["None"] + mido.get_output_names()
46
- in_choice = st.selectbox("MIDI Input Port", in_ports)
47
- out_choice = st.selectbox("MIDI Output Port", out_ports)
48
-
49
- # Keep references to the open ports in session state
50
- if 'midi_in' not in st.session_state:
51
- st.session_state.midi_in = None
52
- if 'midi_out' not in st.session_state:
53
- st.session_state.midi_out = None
54
-
55
- # If changed, close old and open new
56
- if st.session_state.midi_in and st.session_state.midi_in.name != in_choice:
57
- st.session_state.midi_in.close()
58
- st.session_state.midi_in = open_midi_input(in_choice)
59
- elif (not st.session_state.midi_in) and in_choice != "None":
60
- st.session_state.midi_in = open_midi_input(in_choice)
61
-
62
- if st.session_state.midi_out and st.session_state.midi_out.name != out_choice:
63
- st.session_state.midi_out.close()
64
- st.session_state.midi_out = open_midi_output(out_choice)
65
- elif (not st.session_state.midi_out) and out_choice != "None":
66
- st.session_state.midi_out = open_midi_output(out_choice)
67
-
68
- # 2. We'll embed a large HTML with Tone.js for the UI & sound.
69
- # It will:
70
- # - Create a poly synth for the 5-octave keys
71
- # - Create a sampler or player for 16 drum pads
72
- # - Provide an optional arpeggiator toggling
73
- # - Post "noteOn"/"noteOff"/"drum" messages to Python
74
- # - Listen for messages from Python to trigger notes
75
- # For brevity, let's place it in a separate variable or file.
76
- tone_html = get_tone_html()
77
-
78
- # 3. Insert the custom HTML component
79
- # We'll give it enough vertical space for the layout, say 600px
80
- components.html(tone_html, height=600, scrolling=True)
81
 
82
- # 4. We'll poll for inbound hardware MIDI messages from st.session_state.incoming_midi
83
- # and send them to the browser using st.session_state.js_events
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  if 'js_events' not in st.session_state:
85
- # We'll store "events to the browser" in a queue or list
86
  st.session_state.js_events = Queue()
87
-
88
- # We also define a function to dispatch note events to the hardware out
89
- def send_to_midi_out(evt):
90
- if st.session_state.midi_out is not None:
91
- from mido import Message
92
- if evt['type'] == 'noteOn':
93
- msg = Message('note_on', note=evt['note'], velocity=evt['velocity'])
94
- st.session_state.midi_out.send(msg)
95
- elif evt['type'] == 'noteOff':
96
- msg = Message('note_off', note=evt['note'], velocity=0)
97
- st.session_state.midi_out.send(msg)
98
- # if drum events => you can decide how you want to handle them
99
-
100
- # 5. We'll add a small "polling" approach to handle new hardware MIDI events
101
- def poll_hardware_midi():
102
- while not st.session_state.incoming_midi.empty():
103
- evt = st.session_state.incoming_midi.get_nowait()
104
- # Forward to browser
105
- st.session_state.js_events.put(evt)
106
-
107
- poll_hardware_midi()
108
-
109
- # 6. We'll also add a placeholder to show the events from the browser
110
- debug_placeholder = st.empty()
111
-
112
- # 7. Hidden HTML snippet to route browser postMessage -> streamlit
113
- # This snippet listens for "streamlit:message" from the iframe and puts them in python queues
114
- # We'll handle them in real-time in the main loop
115
- components.html("""
116
- <script>
117
- // This script listens for postMessage from the Tone.js UI
118
- // ("window.parent.postMessage(...)"), extracts data, and re-sends
119
- // to the Streamlit host using the "type: 'streamlit:message'" approach.
120
- window.addEventListener('message', function(e) {
121
- if (e.data && e.data.type === 'toneEvent') {
122
- // forward to streamlit
123
- window.parent.postMessage({
124
- type: 'streamlit:message',
125
- data: {
126
- eventType: e.data.eventType,
127
- note: e.data.note,
128
- velocity: e.data.velocity,
129
- padIndex: e.data.padIndex
130
- }
131
- }, '*');
132
- }
133
- });
134
- </script>
135
- """, height=0)
136
-
137
- # 8. We'll create a function to handle inbound "toneEvent" from the browser
138
- # (these are user clicks on the on-screen keys/pads).
139
- # We can forward them to hardware MIDI out if desired.
140
- if 'incoming_browser' not in st.session_state:
141
  from collections import deque
142
- st.session_state.incoming_browser = deque()
143
 
144
- def handle_browser_event(e):
145
- # e = {eventType, note, velocity, padIndex}
146
- debug_placeholder.write(f"Browser event: {e}")
147
- # If it's a noteOn / noteOff, optionally forward to MIDI out
148
- if e['eventType'] in ['noteOn', 'noteOff']:
149
- send_to_midi_out({
150
- 'type': e['eventType'],
151
- 'note': e['note'],
152
- 'velocity': e['velocity']
153
- })
154
- elif e['eventType'] == 'drum':
155
- # Could also forward to a hardware sampler via MIDI out (e.g., note_on)
156
- pass
157
 
158
- # 9. We'll do a second poll to read from st.session_state.incoming_browser
159
- # and handle them.
160
- while st.session_state.incoming_browser:
161
- ev = st.session_state.incoming_browser.popleft()
162
- handle_browser_event(ev)
163
-
164
- # 10. We'll also do a final step:
165
- # * If we have events in st.session_state.js_events (from hardware),
166
- # we embed them in a <script> tag so the Tone.js page sees them.
167
- # * We do this by writing a small custom component with <script> that calls
168
- # e.data with postMessage.
169
- #
170
- # A simpler approach is to create a dynamic script each run that sends
171
- # all queued events to the browser:
172
- js_payload = []
173
  while not st.session_state.js_events.empty():
174
- event = st.session_state.js_events.get_nowait()
175
- js_payload.append(event)
176
 
177
- if js_payload:
178
- # We'll embed them as a list of note events in the next script call
179
- script_code = "<script>\n"
180
- for evt in js_payload:
181
- script_code += f"""
182
  window.postMessage({{
183
  type: 'hardwareMidi',
184
  data: {{
185
- eventType: '{evt['type']}',
186
- note: {evt['note']},
187
- velocity: {evt.get('velocity', 100)}
188
  }}
189
- }}, '*');
190
- """
191
- script_code += "\n</script>\n"
192
- components.html(script_code, height=0)
193
-
194
- st.write("Press keys on the on-screen 5-octave keyboard or 16 pads in the browser UI. "
195
- "If a MIDI Input is selected, hardware note_on/off will also trigger Tone.js. "
196
- "If a MIDI Output is selected, on-screen or inbound hardware events can be echoed back out.")
197
 
198
-
199
- # 11. On session end, close ports
200
  def cleanup():
201
- if st.session_state.midi_in:
202
- st.session_state.midi_in.close()
203
- if st.session_state.midi_out:
204
- st.session_state.midi_out.close()
205
 
206
  st.on_session_end(cleanup)
207
 
208
- def get_tone_html():
209
- """
210
- Returns an HTML/JS string that:
211
- - loads Tone.js
212
- - creates a 5-octave key + 16 drum pads UI
213
- - sets up an arpeggiator if desired
214
- - listens for hardwareMidi messages from Python
215
- - uses postMessage to send 'toneEvent' to Python
216
- For brevity, we’ll put everything inline. In production, put it in a separate .html file.
217
- """
218
- return r"""
219
  <!DOCTYPE html>
220
  <html>
221
  <head>
222
- <meta charset="utf-8"/>
223
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/build/Tone.js"></script>
224
  <style>
225
- body { font-family: sans-serif; margin: 10px; }
226
- .keyboard-container, .drumpad-container { margin-bottom: 20px; }
227
- .key { display: inline-block; width: 30px; height: 120px; margin: 1px; background: #fff; border: 1px solid #666; cursor: pointer; }
228
- .key.black { width: 20px; height: 80px; background: #000; position: relative; margin-left: -10px; margin-right: -10px; z-index: 2; }
229
- .key.active { background: #ff6961 !important; }
230
- .drumpad-grid { display: grid; grid-template-columns: repeat(4, 60px); gap: 10px; }
231
- .drumpad { width: 60px; height: 60px; background: #666; color: #fff; display: flex; align-items: center; justify-content: center; cursor: pointer; border-radius: 5px;}
232
- .drumpad.active { background: #ff6961; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
233
  </style>
234
  </head>
235
  <body>
236
- <h2>Browser Synth with Tone.js</h2>
237
- <div>
238
- <p>All audio is generated client-side.
239
- Python just handles hardware MIDI and forwards it here.</p>
240
- </div>
241
-
242
- <!-- Keyboard -->
243
- <div class="keyboard-container" id="keyboard"></div>
244
-
245
- <!-- Drum pads -->
246
- <h3>Drum Pads</h3>
247
- <div class="drumpad-container">
248
- <div class="drumpad-grid" id="drumpads">
249
- </div>
250
- </div>
251
-
252
- <script>
253
- // ======================================================
254
- // 1) Create Tone.js instruments
255
- // - a poly synth for melodic keys
256
- // - a sampler or a couple of loaded drum samples
257
- // ======================================================
258
- const synth = new Tone.PolySynth(Tone.Synth, {
259
- oscillator: { type: 'triangle' },
260
- envelope: { attack: 0.01, decay: 0.2, sustain: 0.4, release: 1 }
261
- }).toDestination();
262
-
263
- // For drums, let's just do a simple Sampler
264
- const drumSampler = new Tone.Sampler({
265
- C1: "https://tonejs.github.io/audio/drum-samples/breakbeat/kick.mp3",
266
- D1: "https://tonejs.github.io/audio/drum-samples/breakbeat/snare.mp3",
267
- E1: "https://tonejs.github.io/audio/drum-samples/breakbeat/hh.mp3",
268
- F1: "https://tonejs.github.io/audio/drum-samples/breakbeat/hho.mp3"
269
- }, { onload: () => console.log("Drum samples loaded") }
270
- ).toDestination();
271
-
272
- // We'll map 16 pads to 4 sample notes repeatedly:
273
- const padMapping = [
274
- "C1","D1","E1","F1",
275
- "C1","D1","E1","F1",
276
- "C1","D1","E1","F1",
277
- "C1","D1","E1","F1",
278
- ];
279
-
280
- // ======================================================
281
- // 2) Build a 5-octave keyboard from, say, C3 to C7
282
- // We'll do a naive approach for demonstration
283
- // ======================================================
284
- const noteArray = [
285
- // One octave C, C#, D, D#, E, F, F#, G, G#, A, A#, B
286
- // We'll build for C3..B7
287
- ];
288
- const startOctave = 3;
289
- const endOctave = 7;
290
-
291
- function buildNotes() {
292
- const noteNames = ["C","C#","D","D#","E","F","F#","G","G#","A","A#","B"];
293
- for (let octave = startOctave; octave <= endOctave; octave++) {
294
- for (let n of noteNames) {
295
- noteArray.push(n + octave);
296
  }
297
- }
298
- }
299
- buildNotes();
300
-
301
- const keyboardDiv = document.getElementById('keyboard');
302
- noteArray.forEach((note, idx) => {
303
- // Determine if black or white
304
- const hasSharp = note.includes("#");
305
- let keyDiv = document.createElement('div');
306
- keyDiv.classList.add('key');
307
- if (hasSharp) {
308
- keyDiv.classList.add('black');
309
- }
310
- keyDiv.dataset.note = note;
311
- keyboardDiv.appendChild(keyDiv);
312
-
313
- keyDiv.addEventListener('mousedown', () => {
314
- playNote(note, 100);
315
- keyDiv.classList.add('active');
316
- });
317
- keyDiv.addEventListener('mouseup', () => {
318
- releaseNote(note);
319
- keyDiv.classList.remove('active');
320
- });
321
- // For mobile / touch
322
- keyDiv.addEventListener('touchstart', (e) => {
323
- e.preventDefault();
324
- playNote(note, 100);
325
- keyDiv.classList.add('active');
326
- });
327
- keyDiv.addEventListener('touchend', (e) => {
328
- e.preventDefault();
329
- releaseNote(note);
330
- keyDiv.classList.remove('active');
331
- });
332
- });
333
 
334
- // ======================================================
335
- // 3) Build 16 drum pads
336
- // ======================================================
337
- const drumpadsDiv = document.getElementById('drumpads');
338
- for (let i=0; i<16; i++){
339
- let pad = document.createElement('div');
340
- pad.classList.add('drumpad');
341
- pad.innerText = `Pad ${i+1}`;
342
- pad.dataset.pad = i;
343
- drumpadsDiv.appendChild(pad);
344
-
345
- pad.addEventListener('mousedown', () => {
346
- triggerDrum(i, 100);
347
- pad.classList.add('active');
348
- setTimeout(() => { pad.classList.remove('active'); }, 200);
349
- });
350
- // Touch
351
- pad.addEventListener('touchstart', (e)=>{
352
- e.preventDefault();
353
- triggerDrum(i, 100);
354
- pad.classList.add('active');
355
- setTimeout(()=> pad.classList.remove('active'), 200);
356
- });
357
- }
358
 
359
- // ======================================================
360
- // 4) Tone.js note on/off
361
- // ======================================================
362
- function playNote(noteName, velocity){
363
- // velocity is 0..127 => scale 0..1
364
- let vol = velocity / 127;
365
- // start an audio context, required in some browsers
366
- Tone.context.resume();
367
- synth.triggerAttack(noteName, Tone.now(), vol);
368
- // Send message to python
369
- window.parent.postMessage({
370
- type: 'toneEvent',
371
- eventType: 'noteOn',
372
- note: midiNoteNumber(noteName),
373
- velocity: velocity
374
- }, '*');
375
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
376
 
377
- function releaseNote(noteName){
378
- synth.triggerRelease(noteName, Tone.now());
379
- // Send message to python
380
- window.parent.postMessage({
381
- type: 'toneEvent',
382
- eventType: 'noteOff',
383
- note: midiNoteNumber(noteName),
384
- velocity: 0
385
- }, '*');
386
- }
 
 
 
387
 
388
- function triggerDrum(padIndex, velocity){
389
- Tone.context.resume();
390
- let note = padMapping[padIndex % padMapping.length];
391
- let vol = velocity / 127;
392
- drumSampler.triggerAttack(note, Tone.now(), vol);
393
- // Also send to python
394
- window.parent.postMessage({
395
- type: 'toneEvent',
396
- eventType: 'drum',
397
- padIndex: padIndex,
398
- velocity: velocity
399
- }, '*');
400
- }
401
 
402
- // ======================================================
403
- // 5) Convert from note name to approximate MIDI number
404
- // We'll do a naive approach. If you want a robust
405
- // approach, store a dictionary or parse note name.
406
- // ======================================================
407
- function midiNoteNumber(noteStr){
408
- // parse e.g. "C#4" => base + semitone + octave
409
- // There's a standard formula: midi = 12 * (octave + 1) + noteIndex
410
- // We can do a small map:
411
- const noteMap = {'C':0,'C#':1,'D':2,'D#':3,'E':4,'F':5,'F#':6,'G':7,'G#':8,'A':9,'A#':10,'B':11};
412
- let match = noteStr.match(/^([A-G]#?)(\d)$/);
413
- if(!match) return 60;
414
- let base = noteMap[match[1]];
415
- let oct = parseInt(match[2]);
416
- return 12*(oct+1) + base;
417
- }
418
 
419
- // ======================================================
420
- // 6) Listen for "hardwareMidi" messages from Python
421
- // i.e. user played a note on their real keyboard
422
- // We'll automatically call "playNote"/"releaseNote" in Tone
423
- // so it comes out the browser audio
424
- // ======================================================
425
- window.addEventListener('message', (e)=>{
426
- if(e.data && e.data.type === 'hardwareMidi'){
427
- let evt = e.data.data;
428
- if(evt.eventType === 'noteOn'){
429
- // We'll need to map MIDI note -> note name
430
- // We'll do a quick reverse map. For robust code, do a real function or table.
431
- let noteName = midiToNoteName(evt.note);
432
- playNote(noteName, evt.velocity);
433
- } else if(evt.eventType === 'noteOff'){
434
- let noteName = midiToNoteName(evt.note);
435
- releaseNote(noteName);
436
  }
437
- }
438
- });
439
 
440
- function midiToNoteName(midiNum){
441
- const noteNames = ["C","C#","D","D#","E","F","F#","G","G#","A","A#","B"];
442
- let octave = Math.floor(midiNum / 12) - 1;
443
- let noteIndex = midiNum % 12;
444
- return noteNames[noteIndex] + octave;
445
- }
 
 
 
 
 
 
 
 
 
 
446
 
447
- // ======================================================
448
- // 7) (Optional) In-browser arpeggiator, if you want it
449
- // You could do something like keep track of notes held
450
- // in an array and a Tone.Pattern or Tone.Sequence
451
- // ======================================================
452
- // For demonstration, we'll skip a full arpeggiator code snippet here.
453
- // If you want to do it, you'd maintain an array of held notes, then
454
- // create a Tone.Pattern or Tone.Loop that triggers them in sequence.
 
 
 
 
 
455
 
456
- // End of HTML
457
- </script>
 
 
 
458
  </body>
459
  </html>
460
- """.strip()
461
 
462
  if __name__ == "__main__":
463
- main()
 
4
  from queue import Queue
5
  import threading
6
 
7
+ # Store MIDI messages in session state
8
  if 'incoming_midi' not in st.session_state:
9
  st.session_state.incoming_midi = Queue()
10
 
 
11
  def midi_in_callback(msg):
 
 
 
 
12
  if msg.type in ['note_on', 'note_off']:
 
13
  st.session_state.incoming_midi.put({
14
+ 'type': 'noteOn' if msg.type == 'note_on' else 'noteOff',
15
  'note': msg.note,
16
  'velocity': msg.velocity
17
  })
18
 
19
+ def open_midi_port(port_name: str, is_input: bool):
 
 
 
 
 
20
  if port_name == "None":
21
  return None
22
+ return mido.open_input(port_name, callback=midi_in_callback) if is_input else mido.open_output(port_name)
23
 
24
  def main():
25
+ st.title("5-Octave Synth with Arpeggiator & Drum Pads")
26
+
27
+ # MIDI port selection
28
+ ports = {"in": ["None"] + mido.get_input_names(),
29
+ "out": ["None"] + mido.get_output_names()}
30
+ selections = {
31
+ "in": st.selectbox("MIDI Input", ports["in"]),
32
+ "out": st.selectbox("MIDI Output", ports["out"])
33
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
 
35
+ # Initialize/update MIDI ports
36
+ for direction in ["in", "out"]:
37
+ key = f'midi_{direction}'
38
+ if key not in st.session_state:
39
+ st.session_state[key] = None
40
+
41
+ curr_port = st.session_state[key]
42
+ if curr_port and curr_port.name != selections[direction]:
43
+ curr_port.close()
44
+ st.session_state[key] = open_midi_port(selections[direction], direction=="in")
45
+ elif not curr_port and selections[direction] != "None":
46
+ st.session_state[key] = open_midi_port(selections[direction], direction=="in")
47
+
48
+ # Load and embed synth interface
49
+ components.html(get_synth_interface(), height=800)
50
+
51
+ # Event handling setup
52
  if 'js_events' not in st.session_state:
 
53
  st.session_state.js_events = Queue()
54
+ if 'browser_events' not in st.session_state:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  from collections import deque
56
+ st.session_state.browser_events = deque()
57
 
58
+ # Process hardware MIDI
59
+ while not st.session_state.incoming_midi.empty():
60
+ evt = st.session_state.incoming_midi.get_nowait()
61
+ st.session_state.js_events.put(evt)
 
 
 
 
 
 
 
 
 
62
 
63
+ # Send events to browser
64
+ js_events = []
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  while not st.session_state.js_events.empty():
66
+ js_events.append(st.session_state.js_events.get_nowait())
 
67
 
68
+ if js_events:
69
+ js_code = "<script>\n"
70
+ for evt in js_events:
71
+ js_code += f"""
 
72
  window.postMessage({{
73
  type: 'hardwareMidi',
74
  data: {{
75
+ eventType: '{evt["type"]}',
76
+ note: {evt["note"]},
77
+ velocity: {evt.get("velocity", 100)}
78
  }}
79
+ }}, '*');\n"""
80
+ js_code += "</script>"
81
+ components.html(js_code, height=0)
 
 
 
 
 
82
 
83
+ # Cleanup on session end
 
84
  def cleanup():
85
+ for direction in ["in", "out"]:
86
+ port = st.session_state.get(f'midi_{direction}')
87
+ if port:
88
+ port.close()
89
 
90
  st.on_session_end(cleanup)
91
 
92
+ def get_synth_interface():
93
+ return """
 
 
 
 
 
 
 
 
 
94
  <!DOCTYPE html>
95
  <html>
96
  <head>
 
97
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/build/Tone.js"></script>
98
  <style>
99
+ .container { max-width: 1200px; margin: 0 auto; }
100
+ .keyboard { display: flex; margin: 20px 0; }
101
+ .key {
102
+ width: 40px; height: 150px;
103
+ border: 1px solid #000;
104
+ background: white;
105
+ margin-right: 2px;
106
+ }
107
+ .key.black {
108
+ width: 24px; height: 100px;
109
+ background: black;
110
+ margin: 0 -12px;
111
+ z-index: 1;
112
+ }
113
+ .key.active { background: #ff6961; }
114
+ .drum-grid {
115
+ display: grid;
116
+ grid-template-columns: repeat(4, 1fr);
117
+ gap: 10px;
118
+ margin: 20px 0;
119
+ }
120
+ .drum-pad {
121
+ aspect-ratio: 1;
122
+ background: #444;
123
+ color: white;
124
+ display: flex;
125
+ align-items: center;
126
+ justify-content: center;
127
+ cursor: pointer;
128
+ border-radius: 4px;
129
+ }
130
+ .drum-pad.active { background: #ff6961; }
131
+ .controls {
132
+ display: flex;
133
+ gap: 20px;
134
+ margin: 20px 0;
135
+ }
136
  </style>
137
  </head>
138
  <body>
139
+ <div class="container">
140
+ <div class="controls">
141
+ <div>
142
+ <label>Arpeggiator:</label>
143
+ <select id="arpMode">
144
+ <option value="off">Off</option>
145
+ <option value="up">Up</option>
146
+ <option value="down">Down</option>
147
+ <option value="updown">Up/Down</option>
148
+ <option value="random">Random</option>
149
+ </select>
150
+ <input type="range" id="arpSpeed" min="100" max="1000" value="200">
151
+ </div>
152
+ <div>
153
+ <label>Synth Type:</label>
154
+ <select id="synthType">
155
+ <option value="simple">Simple</option>
156
+ <option value="fm">FM</option>
157
+ <option value="am">AM</option>
158
+ </select>
159
+ </div>
160
+ </div>
161
+ <div id="keyboard" class="keyboard"></div>
162
+ <div id="drumPads" class="drum-grid"></div>
163
+ </div>
164
+ <script>
165
+ // Initialize Tone.js instruments and UI
166
+ const synth = new Tone.PolySynth().toDestination();
167
+ const drumSampler = new Tone.Sampler({
168
+ 'C2': 'https://tonejs.github.io/audio/drum-samples/kicks/kick.mp3',
169
+ 'D2': 'https://tonejs.github.io/audio/drum-samples/snare/snare.mp3',
170
+ 'E2': 'https://tonejs.github.io/audio/drum-samples/hh/hh.mp3',
171
+ 'F2': 'https://tonejs.github.io/audio/drum-samples/tom/tom.mp3'
172
+ }).toDestination();
173
+
174
+ // Build 5-octave keyboard (61 keys)
175
+ const startNote = 36; // C2
176
+ const noteNames = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
177
+ const keyboard = document.getElementById('keyboard');
178
+
179
+ for (let i = 0; i < 61; i++) {
180
+ const midiNote = startNote + i;
181
+ const octave = Math.floor(midiNote / 12) - 1;
182
+ const noteName = noteNames[midiNote % 12] + octave;
183
+ const isBlack = noteName.includes('#');
184
+
185
+ const key = document.createElement('div');
186
+ key.className = `key ${isBlack ? 'black' : ''}`;
187
+ key.dataset.note = noteName;
188
+ key.dataset.midi = midiNote;
189
+
190
+ key.addEventListener('mousedown', () => playNote(noteName, midiNote));
191
+ key.addEventListener('mouseup', () => stopNote(noteName, midiNote));
192
+ key.addEventListener('mouseleave', () => stopNote(noteName, midiNote));
193
+
194
+ keyboard.appendChild(key);
 
 
 
 
195
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
196
 
197
+ // Build 16 drum pads
198
+ const drumPads = document.getElementById('drumPads');
199
+ for (let i = 0; i < 16; i++) {
200
+ const pad = document.createElement('div');
201
+ pad.className = 'drum-pad';
202
+ pad.textContent = `Pad ${i + 1}`;
203
+ pad.addEventListener('mousedown', () => triggerDrum(i));
204
+ drumPads.appendChild(pad);
205
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
206
 
207
+ // Arpeggiator implementation
208
+ let arpNotes = [];
209
+ let arpInterval = null;
210
+
211
+ document.getElementById('arpMode').addEventListener('change', updateArpeggiator);
212
+ document.getElementById('arpSpeed').addEventListener('change', updateArpeggiator);
213
+
214
+ function updateArpeggiator() {
215
+ const mode = document.getElementById('arpMode').value;
216
+ const speed = document.getElementById('arpSpeed').value;
217
+
218
+ if (arpInterval) clearInterval(arpInterval);
219
+ if (mode !== 'off' && arpNotes.length) {
220
+ let index = 0;
221
+ arpInterval = setInterval(() => {
222
+ const note = arpNotes[index];
223
+ playNote(note, true);
224
+ setTimeout(() => stopNote(note), speed * 0.8);
225
+
226
+ switch(mode) {
227
+ case 'up':
228
+ index = (index + 1) % arpNotes.length;
229
+ break;
230
+ case 'down':
231
+ index = (index - 1 + arpNotes.length) % arpNotes.length;
232
+ break;
233
+ case 'updown':
234
+ // Implementation for up/down pattern
235
+ break;
236
+ case 'random':
237
+ index = Math.floor(Math.random() * arpNotes.length);
238
+ break;
239
+ }
240
+ }, speed);
241
+ }
242
+ }
243
 
244
+ function playNote(note, midiNote) {
245
+ Tone.start();
246
+ synth.triggerAttack(note);
247
+ const key = document.querySelector(`[data-midi="${midiNote}"]`);
248
+ if (key) key.classList.add('active');
249
+
250
+ // Send MIDI message
251
+ window.parent.postMessage({
252
+ type: 'toneEvent',
253
+ eventType: 'noteOn',
254
+ note: midiNote,
255
+ velocity: 100
256
+ }, '*');
257
 
258
+ if (document.getElementById('arpMode').value !== 'off') {
259
+ if (!arpNotes.includes(note)) {
260
+ arpNotes.push(note);
261
+ updateArpeggiator();
262
+ }
263
+ }
264
+ }
 
 
 
 
 
 
265
 
266
+ function stopNote(note, midiNote) {
267
+ synth.triggerRelease(note);
268
+ const key = document.querySelector(`[data-midi="${midiNote}"]`);
269
+ if (key) key.classList.remove('active');
270
+
271
+ window.parent.postMessage({
272
+ type: 'toneEvent',
273
+ eventType: 'noteOff',
274
+ note: midiNote,
275
+ velocity: 0
276
+ }, '*');
 
 
 
 
 
277
 
278
+ if (document.getElementById('arpMode').value !== 'off') {
279
+ arpNotes = arpNotes.filter(n => n !== note);
280
+ if (!arpNotes.length && arpInterval) {
281
+ clearInterval(arpInterval);
282
+ arpInterval = null;
283
+ }
284
+ }
 
 
 
 
 
 
 
 
 
 
285
  }
 
 
286
 
287
+ function triggerDrum(index) {
288
+ const notes = ['C2', 'D2', 'E2', 'F2'];
289
+ const note = notes[index % notes.length];
290
+ drumSampler.triggerAttackRelease(note, '8n');
291
+
292
+ const pad = drumPads.children[index];
293
+ pad.classList.add('active');
294
+ setTimeout(() => pad.classList.remove('active'), 100);
295
+
296
+ window.parent.postMessage({
297
+ type: 'toneEvent',
298
+ eventType: 'drum',
299
+ padIndex: index,
300
+ velocity: 100
301
+ }, '*');
302
+ }
303
 
304
+ // Handle incoming MIDI messages
305
+ window.addEventListener('message', e => {
306
+ if (e.data?.type === 'hardwareMidi') {
307
+ const { eventType, note, velocity } = e.data.data;
308
+ const noteName = midiToNoteName(note);
309
+
310
+ if (eventType === 'noteOn') {
311
+ playNote(noteName, note);
312
+ } else if (eventType === 'noteOff') {
313
+ stopNote(noteName, note);
314
+ }
315
+ }
316
+ });
317
 
318
+ function midiToNoteName(midi) {
319
+ const octave = Math.floor(midi / 12) - 1;
320
+ return noteNames[midi % 12] + octave;
321
+ }
322
+ </script>
323
  </body>
324
  </html>
325
+ """
326
 
327
  if __name__ == "__main__":
328
+ main()