yalishanda commited on
Commit
d2eda95
·
verified ·
1 Parent(s): 92a211a

Upload folder using huggingface_hub

Browse files
Files changed (1) hide show
  1. app.py +123 -49
app.py CHANGED
@@ -67,6 +67,7 @@ import partitura
67
 
68
  from drum_dynamics.serve.models import Engine
69
  from drum_dynamics.viz.playback import set_soundfont, _ensure_fluidsynth_discoverable
 
70
 
71
  # --- load once at module scope ------------------------------------------------
72
  ENGINE = Engine.load(os.path.join(HERE, "models"))
@@ -97,8 +98,14 @@ def _load_notes(midi_path: str):
97
  return perf, pp
98
 
99
 
100
- def _render_audio(pp) -> tuple[int, np.ndarray]:
101
- """Render a performed part's drum notes to a mono int16 waveform."""
 
 
 
 
 
 
102
  _ensure_fluidsynth_discoverable() # no-op off macOS; helps on local dev
103
  import fluidsynth # lazy: keeps the app importable without the native lib
104
 
@@ -106,14 +113,14 @@ def _render_audio(pp) -> tuple[int, np.ndarray]:
106
  sfid = fl.sfload(SF2)
107
  fl.program_select(9, sfid, 128, 0) # GM percussion set on channel 9
108
 
109
- events = [] # (time_sec, is_on, pitch, velocity)
110
- for nd in pp.notes:
111
- events.append((float(nd["note_on"]), True, int(nd["midi_pitch"]), int(nd["velocity"])))
112
- events.append((float(nd["note_off"]), False, int(nd["midi_pitch"]), 0))
113
- events.sort(key=lambda e: e[0])
114
 
115
  audio = []
116
- for (t0, is_on, pitch, vel), (t1, *_rest) in zip(events[:-1], events[1:]):
117
  if is_on:
118
  fl.noteon(9, pitch, vel)
119
  else:
@@ -121,24 +128,47 @@ def _render_audio(pp) -> tuple[int, np.ndarray]:
121
  n = int(max(0, (t1 - t0)) * SR)
122
  if n:
123
  audio.extend(fl.get_samples(n)[::2]) # interleaved stereo -> mono
124
- # let the last hits ring out
125
- audio.extend(fl.get_samples(SR)[::2])
126
  fl.delete()
127
  return SR, np.asarray(audio, dtype=np.int16)
128
 
129
 
130
- def _velocity_plot(before: list[int], after: list[int]):
131
- """Stem plot of velocity per note, original vs predicted."""
132
- fig, ax = plt.subplots(figsize=(11, 3.2))
133
- x = np.arange(len(before))
134
- ax.plot(x, before, ".", ms=4, alpha=0.5, label="original", color="#9aa0a6")
135
- ax.plot(x, after, ".", ms=5, alpha=0.9, label="predicted", color="#4f7cff")
136
- ax.set_xlabel("note (in time order of input)")
137
- ax.set_ylabel("velocity")
138
- ax.set_ylim(0, 128)
139
- ax.legend(loc="upper right", framealpha=0.9)
140
- ax.grid(True, alpha=0.2)
141
- fig.tight_layout()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
  return fig
143
 
144
 
@@ -153,7 +183,8 @@ def humanize(midi_file, model_label, style, bpm, time_signature, beat_type,
153
  if not pp.notes:
154
  raise gr.Error("No notes found in that MIDI file.")
155
 
156
- before = [int(nd["velocity"]) for nd in pp.notes]
 
157
  notes = [
158
  {"index": i, "onset_sec": float(nd["note_on"]), "pitch": int(nd["midi_pitch"]),
159
  "velocity": int(nd["velocity"]), "selected": True}
@@ -169,27 +200,61 @@ def humanize(midi_file, model_label, style, bpm, time_signature, beat_type,
169
  for i, nd in enumerate(pp.notes):
170
  if i in pred:
171
  nd["velocity"] = int(pred[i])
172
- after = [int(nd["velocity"]) for nd in pp.notes]
 
173
 
174
  out_midi = os.path.join(tempfile.mkdtemp(), "humanized.mid")
175
  partitura.save_performance_midi(perf, out_midi)
176
 
177
- fig = _velocity_plot(before, after)
178
- audio = _render_audio(pp) if want_audio and os.path.exists(SF2) else None
179
- return out_midi, fig, audio
 
 
180
 
181
 
182
- # --- example rows: [midi, model, style, bpm, time_sig, beat_type, temp, blend, seed, audio]
183
  _EX_DIR = os.path.join(HERE, "examples")
184
  EXAMPLES = [
185
- [os.path.join(_EX_DIR, "rock_65_beat_4-4.midi"),
186
- "Transformer - MDN (temperature)", "rock", 65, "4-4", "beat", 1.0, 1.0, 42, True],
187
- [os.path.join(_EX_DIR, "pop_138_beat_4-4.midi"),
188
- "LightGBM (fast, deterministic)", "pop", 138, "4-4", "beat", 1.0, 1.0, 42, True],
189
- [os.path.join(_EX_DIR, "soul_105_beat_4-4.midi"),
190
- "Transformer - Categorical", "soul", 105, "4-4", "beat", 1.0, 1.0, 42, True],
191
  ]
192
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
193
  DESC = """\
194
  # 🥁 Dynamics Needed
195
 
@@ -199,18 +264,24 @@ alone** - never from the note's own loudness - using models trained on the
199
  [E-GMD](https://magenta.tensorflow.org/datasets/e-gmd) dataset of real
200
  performances.
201
 
202
- Upload a drum-MIDI groove (or pick an example), set its musical context, and
203
- compare the result by ear and by plot. Same engine that drives the REAPER
 
204
  plugin, behind a web UI.
205
  """
206
 
207
 
208
- with gr.Blocks(title="Dynamics Needed", theme=gr.themes.Soft()) as demo:
209
  gr.Markdown(DESC)
210
  with gr.Row():
211
  with gr.Column(scale=1):
212
- midi_in = gr.File(label="Drum MIDI", file_types=[".mid", ".midi"], type="filepath")
213
- model_in = gr.Dropdown(list(MODEL_LABELS), value=list(MODEL_LABELS)[0], label="Model")
 
 
 
 
 
214
  style_in = gr.Dropdown(STYLES, value="rock", label="Style / genre",
215
  info="Conditions the transformer; the genre is the part before '/'.")
216
  with gr.Row():
@@ -224,21 +295,24 @@ with gr.Blocks(title="Dynamics Needed", theme=gr.themes.Soft()) as demo:
224
  blend_in = gr.Slider(0.0, 1.0, value=1.0, step=0.05, label="Blend",
225
  info="1.0 = fully predicted, 0.0 = keep original velocities.")
226
  seed_in = gr.Number(value=42, label="Seed", precision=0)
227
- audio_chk = gr.Checkbox(value=True, label="Render audio preview")
228
- run_btn = gr.Button("Predict velocities", variant="primary")
229
  with gr.Column(scale=1):
230
- midi_out = gr.File(label="Humanized MIDI")
231
- plot_out = gr.Plot(label="Velocity: original vs predicted")
232
- audio_out = gr.Audio(label="Audio preview", type="numpy")
 
 
 
 
 
 
233
 
234
  inputs = [midi_in, model_in, style_in, bpm_in, ts_in, beat_in,
235
  temp_in, blend_in, seed_in, audio_chk]
236
- outputs = [midi_out, plot_out, audio_out]
237
  run_btn.click(humanize, inputs=inputs, outputs=outputs)
238
 
239
- gr.Examples(examples=EXAMPLES, inputs=inputs, outputs=outputs, fn=humanize,
240
- cache_examples=False, label="Example grooves (E-GMD, CC-BY 4.0)")
241
-
242
 
243
  if __name__ == "__main__":
244
- demo.launch(mcp_server=True)
 
67
 
68
  from drum_dynamics.serve.models import Engine
69
  from drum_dynamics.viz.playback import set_soundfont, _ensure_fluidsynth_discoverable
70
+ from drum_dynamics.core.midi import drum_name
71
 
72
  # --- load once at module scope ------------------------------------------------
73
  ENGINE = Engine.load(os.path.join(HERE, "models"))
 
98
  return perf, pp
99
 
100
 
101
+ def _note_events(pp):
102
+ """Snapshot the current notes as (on_sec, off_sec, pitch, velocity) tuples."""
103
+ return [(float(nd["note_on"]), float(nd["note_off"]),
104
+ int(nd["midi_pitch"]), int(nd["velocity"])) for nd in pp.notes]
105
+
106
+
107
+ def _render_events(events) -> tuple[int, np.ndarray]:
108
+ """Render (on, off, pitch, velocity) drum events to a mono int16 waveform."""
109
  _ensure_fluidsynth_discoverable() # no-op off macOS; helps on local dev
110
  import fluidsynth # lazy: keeps the app importable without the native lib
111
 
 
113
  sfid = fl.sfload(SF2)
114
  fl.program_select(9, sfid, 128, 0) # GM percussion set on channel 9
115
 
116
+ timeline = [] # (time_sec, is_on, pitch, velocity)
117
+ for on, off, pitch, vel in events:
118
+ timeline.append((on, True, pitch, vel))
119
+ timeline.append((off, False, pitch, 0))
120
+ timeline.sort(key=lambda e: e[0])
121
 
122
  audio = []
123
+ for (t0, is_on, pitch, vel), (t1, *_rest) in zip(timeline[:-1], timeline[1:]):
124
  if is_on:
125
  fl.noteon(9, pitch, vel)
126
  else:
 
128
  n = int(max(0, (t1 - t0)) * SR)
129
  if n:
130
  audio.extend(fl.get_samples(n)[::2]) # interleaved stereo -> mono
131
+ audio.extend(fl.get_samples(SR)[::2]) # let the last hits ring out
 
132
  fl.delete()
133
  return SR, np.asarray(audio, dtype=np.int16)
134
 
135
 
136
+ def _drumroll_figure(na_before, na_after):
137
+ """Stacked drum-rolls (velocity as colour) for original vs humanized.
138
+
139
+ Only the drum pieces actually present are shown, on a shared 0-127 colour
140
+ scale so the change in dynamics is directly comparable between the two.
141
+ """
142
+ pitches = sorted({int(p) for p in na_before["pitch"]}, reverse=True) # high on top
143
+ row_of = {p: i for i, p in enumerate(pitches)}
144
+ n_ticks = int(max(na_before["onset_tick"] + na_before["duration_tick"])) + 1
145
+ min_w = max(1, n_ticks // 200) # widen hits so short drums stay visible
146
+
147
+ def build(na):
148
+ roll = np.zeros((len(pitches), n_ticks))
149
+ for pt, on, dur, vel in zip(na["pitch"], na["onset_tick"],
150
+ na["duration_tick"], na["velocity"]):
151
+ r = row_of[int(pt)]
152
+ s = int(on)
153
+ e = min(n_ticks, s + max(int(dur), min_w))
154
+ roll[r, s:e] = np.maximum(roll[r, s:e], vel / 127.0)
155
+ return roll
156
+
157
+ fig, axes = plt.subplots(2, 1, figsize=(11, 6.2), sharex=True)
158
+ im = None
159
+ for ax, na, title in ((axes[0], na_before, "Original"),
160
+ (axes[1], na_after, "Humanized")):
161
+ im = ax.imshow(build(na), aspect="auto", origin="lower", interpolation="none",
162
+ cmap="magma", vmin=0.0, vmax=1.0)
163
+ ax.set_title(title, fontsize=10, loc="left")
164
+ ax.set_yticks(range(len(pitches)))
165
+ ax.set_yticklabels([drum_name(p) for p in pitches], fontsize=7)
166
+ ax.set_ylabel("drum piece", fontsize=8)
167
+ axes[1].set_xlabel("time (MIDI ticks)")
168
+ cbar = fig.colorbar(im, ax=axes, fraction=0.03, pad=0.02)
169
+ cbar.set_label("velocity")
170
+ cbar.set_ticks(np.linspace(0, 1, 8))
171
+ cbar.set_ticklabels(np.linspace(0, 127, 8, dtype=int))
172
  return fig
173
 
174
 
 
183
  if not pp.notes:
184
  raise gr.Error("No notes found in that MIDI file.")
185
 
186
+ na_before = pp.note_array().copy() # original, for the roll
187
+ events_before = _note_events(pp) # original, for the audio
188
  notes = [
189
  {"index": i, "onset_sec": float(nd["note_on"]), "pitch": int(nd["midi_pitch"]),
190
  "velocity": int(nd["velocity"]), "selected": True}
 
200
  for i, nd in enumerate(pp.notes):
201
  if i in pred:
202
  nd["velocity"] = int(pred[i])
203
+ na_after = pp.note_array().copy() # humanized, for the roll
204
+ events_after = _note_events(pp) # humanized, for the audio
205
 
206
  out_midi = os.path.join(tempfile.mkdtemp(), "humanized.mid")
207
  partitura.save_performance_midi(perf, out_midi)
208
 
209
+ fig = _drumroll_figure(na_before, na_after)
210
+ do_audio = bool(want_audio) and os.path.exists(SF2)
211
+ audio_before = _render_events(events_before) if do_audio else None
212
+ audio_after = _render_events(events_after) if do_audio else None
213
+ return out_midi, fig, audio_before, audio_after
214
 
215
 
216
+ GENRES = LEVELS["genres"]
217
  _EX_DIR = os.path.join(HERE, "examples")
218
  EXAMPLES = [
219
+ [os.path.join(_EX_DIR, "rock_65_beat_4-4.midi")],
220
+ [os.path.join(_EX_DIR, "pop_138_beat_4-4.midi")],
221
+ [os.path.join(_EX_DIR, "soul_105_beat_4-4.midi")],
 
 
 
222
  ]
223
 
224
+
225
+ def _autofill_from_file(path):
226
+ """Best-effort: read musical context from an E-GMD-style filename.
227
+
228
+ Fills style / bpm / time-signature / beat-type when the name carries them
229
+ (e.g. ``rock_65_beat_4-4.midi``); leaves any field it can't parse untouched.
230
+ Returns gr.update()s in the order (style, bpm, time_signature, beat_type).
231
+ """
232
+ import re
233
+
234
+ keep = (gr.update(), gr.update(), gr.update(), gr.update())
235
+ if not path:
236
+ return keep
237
+ tokens = re.split(r"[_\s]+", os.path.splitext(os.path.basename(path))[0].lower())
238
+
239
+ bpm = next((int(t) for t in tokens if t.isdigit() and 30 <= int(t) <= 300), None)
240
+ ts = next((t for t in tokens if re.fullmatch(r"\d-\d", t)), None)
241
+ beat = next((t for t in tokens if t in ("beat", "fill")), None)
242
+ style = None
243
+ for t in tokens:
244
+ if t in STYLES:
245
+ style = t
246
+ break
247
+ if t.split("-")[0] in GENRES:
248
+ style = t.split("-")[0]
249
+ break
250
+
251
+ return (
252
+ gr.update(value=style) if style else gr.update(),
253
+ gr.update(value=bpm) if bpm else gr.update(),
254
+ gr.update(value=ts) if ts else gr.update(),
255
+ gr.update(value=beat) if beat else gr.update(),
256
+ )
257
+
258
  DESC = """\
259
  # 🥁 Dynamics Needed
260
 
 
264
  [E-GMD](https://magenta.tensorflow.org/datasets/e-gmd) dataset of real
265
  performances.
266
 
267
+ Upload a drum-MIDI groove **or** pick an example, set its musical context, and
268
+ compare the result **by ear** (original vs humanized audio) and **by eye**
269
+ (a drum-roll where colour is velocity). Same engine that drives the REAPER
270
  plugin, behind a web UI.
271
  """
272
 
273
 
274
+ with gr.Blocks(title="Dynamics Needed") as demo:
275
  gr.Markdown(DESC)
276
  with gr.Row():
277
  with gr.Column(scale=1):
278
+ midi_in = gr.File(label=" Upload a drum MIDI", file_types=[".mid", ".midi"],
279
+ type="filepath")
280
+ gr.Markdown("<div style='text-align:center;opacity:.7'>— or —</div>")
281
+ gr.Examples(examples=EXAMPLES, inputs=[midi_in],
282
+ label="Pick an example groove (E-GMD, CC-BY 4.0)")
283
+ model_in = gr.Dropdown(list(MODEL_LABELS), value=list(MODEL_LABELS)[0],
284
+ label="② Model")
285
  style_in = gr.Dropdown(STYLES, value="rock", label="Style / genre",
286
  info="Conditions the transformer; the genre is the part before '/'.")
287
  with gr.Row():
 
295
  blend_in = gr.Slider(0.0, 1.0, value=1.0, step=0.05, label="Blend",
296
  info="1.0 = fully predicted, 0.0 = keep original velocities.")
297
  seed_in = gr.Number(value=42, label="Seed", precision=0)
298
+ audio_chk = gr.Checkbox(value=True, label="Render audio previews")
299
+ run_btn = gr.Button("Predict velocities", variant="primary")
300
  with gr.Column(scale=1):
301
+ with gr.Row():
302
+ audio_before_out = gr.Audio(label="Original (as uploaded)", type="numpy")
303
+ audio_after_out = gr.Audio(label="Humanized", type="numpy")
304
+ plot_out = gr.Plot(label="Drum-roll · colour = velocity (original vs humanized)")
305
+ midi_out = gr.File(label="Humanized MIDI (download)")
306
+
307
+ # Auto-fill musical context from the filename (examples and named uploads).
308
+ ctx_out = [style_in, bpm_in, ts_in, beat_in]
309
+ midi_in.change(_autofill_from_file, inputs=[midi_in], outputs=ctx_out)
310
 
311
  inputs = [midi_in, model_in, style_in, bpm_in, ts_in, beat_in,
312
  temp_in, blend_in, seed_in, audio_chk]
313
+ outputs = [midi_out, plot_out, audio_before_out, audio_after_out]
314
  run_btn.click(humanize, inputs=inputs, outputs=outputs)
315
 
 
 
 
316
 
317
  if __name__ == "__main__":
318
+ demo.launch(mcp_server=True, theme=gr.themes.Soft())