avans06 commited on
Commit
69f25b5
·
1 Parent(s): d88d1e3

feat(synth): Add rhythmic arpeggiator effect

Browse files

Introduces a new arpeggiator to transform chords into lively, rhythmic patterns, a key technique for reducing stiffness in 8-bit music.

The feature operates as a parallel audio layer, mixing the arpeggiated sound with the original synthesis.

Capabilities:
- Automatically syncs to the MIDI's tempo with selectable rhythmic patterns (e.g., "16ths," "Upbeat").
- Intelligently protects the lead melody, only arpeggiating the harmony parts.
- "Humanizes" the output with micro-randomization of timing and velocity.
- Includes full user control over pattern direction, octave range, velocity, and stereo panning.

Files changed (1) hide show
  1. app.py +360 -32
app.py CHANGED
@@ -44,6 +44,7 @@ import os
44
  import hashlib
45
  import time as reqtime
46
  import copy
 
47
  import shutil
48
  import librosa
49
  import pyloudnorm as pyln
@@ -183,6 +184,16 @@ class AppParameters:
183
  s8bit_chord_velocity_threshold: int = 100 # Min average velocity for a chord to be tamed
184
  s8bit_chord_velocity_scale: float = 0.75 # Velocity multiplier for loud, dense chords
185
 
 
 
 
 
 
 
 
 
 
 
186
  # =================================================================================================
187
  # === Helper Functions ===
188
  # =================================================================================================
@@ -243,6 +254,183 @@ def preprocess_midi_for_harshness(midi_data: pretty_midi.PrettyMIDI, params: App
243
 
244
  return midi_data # Return the modified object
245
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
246
  def one_pole_lowpass(x, cutoff_hz, fs):
247
  """Simple one-pole lowpass filter (causal), stable and cheap."""
248
  if cutoff_hz <= 0 or cutoff_hz >= fs/2:
@@ -418,19 +606,36 @@ def synthesize_8bit_style(*, midi_data: pretty_midi.PrettyMIDI, fs: int, params
418
  # 1. First, collect all notes from all instruments into a single list.
419
  all_notes_with_instrument_info = []
420
  for i, instrument in enumerate(midi_data.instruments):
421
- # --- Panning Logic ---
422
- # Default to center-panned mono
423
- pan_l, pan_r = 0.707, 0.707
424
- if num_instruments == 2:
425
- if i == 0: # First instrument panned left
426
- pan_l, pan_r = 1.0, 0.0
427
- elif i == 1: # Second instrument panned right
428
- pan_l, pan_r = 0.0, 1.0
429
- elif num_instruments > 2:
430
- if i == 0: # Left
431
  pan_l, pan_r = 1.0, 0.0
432
- elif i == 1: # Right
433
  pan_l, pan_r = 0.0, 1.0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
434
  # Other instruments remain centered
435
 
436
  # Store each note along with its parent instrument index and panning info
@@ -1121,6 +1326,10 @@ def Render_MIDI(*, input_midi_path: str, params: AppParameters, progress: gr.Pro
1121
  """
1122
  Processes and renders a MIDI file according to user-defined settings.
1123
  Can render using SoundFonts or a custom 8-bit synthesizer.
 
 
 
 
1124
  Args:
1125
  input_midi_path (str): The path to the input MIDI file.
1126
  All other arguments are rendering options from the Gradio UI.
@@ -1340,27 +1549,71 @@ def Render_MIDI(*, input_midi_path: str, params: AppParameters, progress: gr.Pro
1340
  srate = int(params.render_sample_rate)
1341
 
1342
  # --- Conditional Rendering Logic ---
 
1343
  if params.soundfont_bank == SYNTH_8_BIT_LABEL:
1344
- print("Using 8-bit style synthesizer...")
1345
  try:
1346
- # Load the MIDI file with pretty_midi for manual synthesis
1347
- midi_data_for_synth = pretty_midi.PrettyMIDI(midi_to_render_path)
1348
- # --- Apply MIDI Pre-processing if enabled ---
1349
  if getattr(params, 's8bit_enable_midi_preprocessing', False):
1350
- midi_data_for_synth = preprocess_midi_for_harshness(midi_data_for_synth, params)
1351
-
1352
- # Synthesize the waveform
1353
- # --- Passing new FX parameters to the synthesis function ---
1354
- audio = synthesize_8bit_style(midi_data=midi_data_for_synth, fs=srate, params=params, progress=progress)
1355
- # Normalize and prepare for Gradio
1356
- peak_val = np.max(np.abs(audio))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1357
  if peak_val > 0:
1358
- audio /= peak_val
1359
  # Transpose from (2, N) to (N, 2) and convert to int16 for Gradio
1360
- audio_out = (audio.T * 32767).astype(np.int16)
1361
  except Exception as e:
1362
  print(f"Error during 8-bit synthesis: {e}")
1363
  return [None] * 7
 
1364
  else:
1365
  print(f"Using SoundFont: {params.soundfont_bank}")
1366
  # Get the full path from the global dictionary
@@ -3026,12 +3279,12 @@ if __name__ == "__main__":
3026
  # This outer group ensures the checkbox and its settings are visually linked.
3027
  with gr.Group():
3028
  s8bit_echo_sustain = gr.Checkbox(
3029
- value=True, # Default to off as it's a special effect.
3030
  label="Enable Echo Sustain for Long Notes",
3031
  info="For 'Plucky' envelope only. Fills the silent tail of long, sustained notes with quiet, repeating pulses. Fixes 'choppy' sound on long piano notes."
3032
  )
3033
  # This inner group contains the sliders and is controlled by the checkbox above.
3034
- with gr.Group(visible=True) as echo_sustain_settings:
3035
  s8bit_echo_rate_hz = gr.Slider(
3036
  1.0, 20.0, value=5.0, step=0.5,
3037
  label="Echo Rate (Hz)",
@@ -3048,11 +3301,80 @@ if __name__ == "__main__":
3048
  info="Controls how long a note must be to trigger echoes. This value is a multiplier of the 'Decay Time'. Example: If 'Decay Time' is 0.1s and this threshold is set to 10.0, only notes longer than 1.0s (0.1 * 10.0) will produce echoes."
3049
  )
3050
  # --- Re-architected into two separate, parallel accordions ---
3051
- # --- Section 1: MIDI Pre-processing ---
3052
- with gr.Accordion("MIDI Pre-processing (Anti-Harshness)", open=False):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3053
  s8bit_enable_midi_preprocessing = gr.Checkbox(
3054
  value=True,
3055
- label="Enable MIDI Pre-processing",
3056
  info="Intelligently reduces the velocity of notes that are likely to cause harshness (e.g., very high notes or loud, dense chords) before synthesis begins."
3057
  )
3058
  with gr.Group(visible=True) as midi_preprocessing_settings_box:
@@ -3082,14 +3404,14 @@ if __name__ == "__main__":
3082
  info="Velocity multiplier for loud, dense chords."
3083
  )
3084
 
3085
- # --- Section 2: Audio Post-processing, accordion for Anti-Aliasing and Quality Settings ---
3086
- with gr.Accordion("Audio Quality & Anti-Aliasing (Advanced)", open=False):
3087
  s8bit_enable_anti_aliasing = gr.Checkbox(
3088
  value=False,
3089
  label="Enable All Audio Quality Enhancements",
3090
  info="Master toggle for all settings below. Disabling may slightly speed up rendering but can result in harsher, more aliased sound."
3091
  )
3092
- with gr.Group(visible=True) as anti_aliasing_settings_box:
3093
  s8bit_use_additive_synthesis = gr.Checkbox(
3094
  value=False,
3095
  label="Use Additive Synthesis (High Quality, High CPU)",
@@ -3241,6 +3563,12 @@ if __name__ == "__main__":
3241
  inputs=s8bit_enable_midi_preprocessing,
3242
  outputs=midi_preprocessing_settings_box
3243
  )
 
 
 
 
 
 
3244
 
3245
  # Launch the Gradio app
3246
  app.queue().launch(inbrowser=True, debug=True)
 
44
  import hashlib
45
  import time as reqtime
46
  import copy
47
+ import random
48
  import shutil
49
  import librosa
50
  import pyloudnorm as pyln
 
184
  s8bit_chord_velocity_threshold: int = 100 # Min average velocity for a chord to be tamed
185
  s8bit_chord_velocity_scale: float = 0.75 # Velocity multiplier for loud, dense chords
186
 
187
+ # --- Arpeggiator Parameters ---
188
+ s8bit_enable_arpeggiator: bool = False # Master switch for the arpeggiator
189
+ s8bit_arpeggiate_only_lower_tracks: bool = True # Only arpeggiate lower tracks (melody protection)
190
+ s8bit_arpeggio_velocity_scale: float = 0.7 # Velocity multiplier for arpeggiated notes (0.0 to 1.0)
191
+ s8bit_arpeggio_density: float = 0.5 # Density factor for rhythmic patterns (0.0 to 1.0)
192
+ s8bit_arpeggio_rhythm: str = "Classic Upbeat (8th)" # Rhythmic pattern for arpeggiation
193
+ s8bit_arpeggio_pattern: str = "Up" # Pattern of the arpeggio (e.g., Up, Down, UpDown)
194
+ s8bit_arpeggio_octave_range: int = 1 # How many octaves the pattern spans
195
+ s8bit_arpeggio_panning: str = "Stereo" # Panning mode for arpeggiated notes (Stereo, Left, Right, Center)
196
+
197
  # =================================================================================================
198
  # === Helper Functions ===
199
  # =================================================================================================
 
254
 
255
  return midi_data # Return the modified object
256
 
257
+
258
+ def arpeggiate_midi(midi_data: pretty_midi.PrettyMIDI, params: AppParameters):
259
+ """
260
+ Applies a tempo-synced, rhythmic arpeggiator effect. It can generate
261
+ various rhythmic patterns (not just continuous notes) to create a more
262
+ musical and less "stiff" accompaniment.
263
+ Improved rhythmic arpeggiator with dynamic density, stereo layer splitting,
264
+ micro-randomization, and cross-beat continuity.
265
+
266
+ Args:
267
+ midi_data: The original PrettyMIDI object.
268
+ params: AppParameters containing arpeggiator settings.
269
+
270
+ Returns:
271
+ A new PrettyMIDI object with arpeggiated chords.
272
+ """
273
+ print("Applying rhythmic arpeggiator to MIDI data...")
274
+ # Work on a deep copy to avoid modifying the original object passed to the function
275
+ processed_midi = copy.deepcopy(midi_data)
276
+
277
+ # --- Step 1: Estimate Tempo ---
278
+ try:
279
+ # Estimate the main tempo of the piece.
280
+ bpm = midi_data.estimate_tempo()
281
+ print(f" - Estimated MIDI Tempo: {bpm:.2f} BPM")
282
+ except:
283
+ bpm = 120.0
284
+ beat_duration_s = 60.0 / bpm
285
+ print(f" - Arpeggiator using tempo: {bpm:.2f} BPM")
286
+
287
+ # --- Step 2: Define Rhythmic Patterns ---
288
+ # Each pattern is a list of tuples: (start_offset_in_beat, duration_in_beat)
289
+ # A beat is a quarter note.
290
+ rhythm_patterns = {
291
+ "Continuous 16ths": [(0.0, 0.25), (0.25, 0.25), (0.5, 0.25), (0.75, 0.25)],
292
+ "Classic Upbeat (8th)": [(0.5, 0.25), (0.75, 0.25)],# Two 16th notes on the upbeat
293
+ "Galloping": [(0.0, 0.75), (0.75, 0.25)],
294
+ "Pulsing 8ths": [(0.0, 0.5), (0.5, 0.5)],
295
+ "Simple Quarter Notes": [(0.0, 1.0)],
296
+ "Pulsing 4ths": [(0.0, 0.5)],
297
+ }
298
+ selected_rhythm = rhythm_patterns.get(params.s8bit_arpeggio_rhythm, rhythm_patterns["Classic Upbeat (8th)"])
299
+
300
+ # --- The logic for lead/harmony separation, Collect all notes ---
301
+ all_notes = []
302
+ # We need to keep track of which instrument each note belongs to
303
+ for i, instrument in enumerate(processed_midi.instruments):
304
+ if not instrument.is_drum:
305
+ for note in instrument.notes:
306
+ # Use a simple object or tuple to store note and its origin
307
+ all_notes.append({'note': note, 'instrument_idx': i})
308
+
309
+ if not all_notes:
310
+ return processed_midi
311
+ all_notes.sort(key=lambda x: x['note'].start)
312
+
313
+ # --- Lead / Harmony separation ---
314
+ lead_note_objects = set()
315
+ harmony_note_objects = set()
316
+
317
+ if params.s8bit_arpeggiate_only_lower_tracks:
318
+ print(" - Lead melody protection is ON. Analyzing global timeline...")
319
+ note_idx = 0
320
+ while note_idx < len(all_notes):
321
+ current_slice_start = all_notes[note_idx]['note'].start
322
+ notes_in_slice = [item for item in all_notes[note_idx:] if (item['note'].start - current_slice_start) < 0.02]
323
+
324
+ if not notes_in_slice:
325
+ note_idx += 1
326
+ continue
327
+
328
+ notes_in_slice.sort(key=lambda x: x['note'].pitch, reverse=True)
329
+ lead_note_objects.add(notes_in_slice[0]['note'])
330
+ for item in notes_in_slice[1:]:
331
+ harmony_note_objects.add(item['note'])
332
+
333
+ note_idx += len(notes_in_slice)
334
+ else:
335
+ print(" - Lead melody protection is OFF.")
336
+ for item in all_notes:
337
+ harmony_note_objects.add(item['note'])
338
+
339
+ # --- Process each instrument ---
340
+ for instrument in processed_midi.instruments:
341
+ if instrument.is_drum:
342
+ continue
343
+
344
+ new_note_list = []
345
+
346
+ # Separate the notes of this specific instrument into lead and harmony
347
+ inst_lead_notes = [n for n in instrument.notes if n in lead_note_objects]
348
+ inst_harmony_notes = [n for n in instrument.notes if n in harmony_note_objects]
349
+
350
+ # Add all lead notes from this instrument back directly
351
+ new_note_list.extend(inst_lead_notes) # Lead notes pass through
352
+
353
+ # Process the harmony notes for this instrument to find chords
354
+ processed_harmony_notes_in_inst = set()
355
+ for note1 in inst_harmony_notes:
356
+ if note1 in processed_harmony_notes_in_inst:
357
+ continue
358
+ # Collect simultaneous chord notes (within 20ms)
359
+ chord_notes = [note1] + [note2 for note2 in inst_harmony_notes if note2 not in processed_harmony_notes_in_inst and abs(note2.start - note1.start) < 0.02]
360
+ for n in chord_notes:
361
+ processed_harmony_notes_in_inst.add(n) # Mark all chord notes as processed.
362
+
363
+ if len(chord_notes) > 1:
364
+ # Determine the chord's properties.
365
+ chord_start_time = min(n.start for n in chord_notes)
366
+ chord_end_time = max(n.end for n in chord_notes)
367
+ avg_velocity = int(np.mean([n.velocity for n in chord_notes]) * params.s8bit_arpeggio_velocity_scale)
368
+ avg_velocity = max(1, avg_velocity)
369
+ pitches = sorted([n.pitch for n in chord_notes])
370
+
371
+ # --- Dynamic density factor ---
372
+ # params.s8bit_arpeggio_density ∈ [0.2, 1.0], default 0.5
373
+ note_base_density = getattr(params, "s8bit_arpeggio_density", 0.5)
374
+ chord_duration = chord_end_time - chord_start_time
375
+ note_duration_factor = min(1.0, chord_duration / (2 * beat_duration_s))
376
+
377
+ note_density_factor = note_base_density * note_duration_factor
378
+
379
+ # --- Build pattern with octave range ---
380
+ pattern = []
381
+ for octave in range(params.s8bit_arpeggio_octave_range):
382
+ octave_pitches = [p + 12*octave for p in pitches]
383
+ if params.s8bit_arpeggio_pattern == "Up":
384
+ pattern.extend(octave_pitches)
385
+ elif params.s8bit_arpeggio_pattern == "Down":
386
+ pattern.extend(reversed(octave_pitches))
387
+ elif params.s8bit_arpeggio_pattern == "UpDown":
388
+ pattern.extend(octave_pitches)
389
+ if len(octave_pitches) > 2:
390
+ pattern.extend(reversed(octave_pitches[1:-1]))
391
+ if not pattern:
392
+ continue
393
+
394
+ # --- Lay down rhythmic notes ---
395
+ current_time = chord_start_time
396
+ pattern_index = 0
397
+ while current_time < chord_end_time:
398
+ # Lay down the rhythmic pattern for the current beat
399
+ for start_offset, duration_beats in selected_rhythm:
400
+ note_start_time = current_time + start_offset * beat_duration_s
401
+ note_duration_s = duration_beats * beat_duration_s * note_density_factor
402
+
403
+ # Ensure the note does not exceed the chord's total duration
404
+ if note_start_time >= chord_end_time:
405
+ break
406
+
407
+ pitch = pattern[pattern_index % len(pattern)]
408
+ pattern_index += 1
409
+
410
+ # --- Micro-randomization ---
411
+ rand_offset = random.uniform(-0.01, 0.01) # ±10ms
412
+ final_velocity = int(avg_velocity + random.randint(-5,5))
413
+ final_velocity = max(1, min(127, final_velocity))
414
+
415
+ new_note = pretty_midi.Note(
416
+ velocity=final_velocity,
417
+ pitch=pitch,
418
+ start=max(0.0, note_start_time + rand_offset),
419
+ end=min(chord_end_time, note_start_time + note_duration_s)
420
+ )
421
+ new_note_list.append(new_note)
422
+ current_time += beat_duration_s
423
+
424
+ else: # Single harmony notes are passed through
425
+ new_note_list.append(note1)
426
+
427
+ # Replace the instrument's original note list with the new, processed one
428
+ instrument.notes = new_note_list
429
+
430
+ print("Rhythmic arpeggiator finished.")
431
+ return processed_midi
432
+
433
+
434
  def one_pole_lowpass(x, cutoff_hz, fs):
435
  """Simple one-pole lowpass filter (causal), stable and cheap."""
436
  if cutoff_hz <= 0 or cutoff_hz >= fs/2:
 
606
  # 1. First, collect all notes from all instruments into a single list.
607
  all_notes_with_instrument_info = []
608
  for i, instrument in enumerate(midi_data.instruments):
609
+ # --- Panning Logic (with override for arpeggiator layer) ---
610
+ panning_override = getattr(params, '_temp_panning_override', None)
611
+
612
+ if panning_override:
613
+ if panning_override == "Center":
614
+ pan_l, pan_r = 0.707, 0.707
615
+ elif panning_override == "Left":
 
 
 
616
  pan_l, pan_r = 1.0, 0.0
617
+ elif panning_override == "Right":
618
  pan_l, pan_r = 0.0, 1.0
619
+ else: # Default to Stereo for the arp layer
620
+ # Wide stereo: pan instruments alternating left and right
621
+ if i % 2 == 0:
622
+ pan_l, pan_r = 1.0, 0.0 # Even instruments to the left
623
+ else:
624
+ pan_l, pan_r = 0.0, 1.0 # Odd instruments to the right
625
+ else: # Standard panning logic for the main layer
626
+ # --- Panning Logic ---
627
+ # Default to center-panned mono
628
+ pan_l, pan_r = 0.707, 0.707
629
+ if num_instruments == 2:
630
+ if i == 0: # First instrument panned left
631
+ pan_l, pan_r = 1.0, 0.0
632
+ elif i == 1: # Second instrument panned right
633
+ pan_l, pan_r = 0.0, 1.0
634
+ elif num_instruments > 2:
635
+ if i == 0: # Left
636
+ pan_l, pan_r = 1.0, 0.0
637
+ elif i == 1: # Right
638
+ pan_l, pan_r = 0.0, 1.0
639
  # Other instruments remain centered
640
 
641
  # Store each note along with its parent instrument index and panning info
 
1326
  """
1327
  Processes and renders a MIDI file according to user-defined settings.
1328
  Can render using SoundFonts or a custom 8-bit synthesizer.
1329
+
1330
+ This version supports a parallel arpeggiator workflow, where the original MIDI
1331
+ and an arpeggiated version are synthesized separately and then mixed together.
1332
+
1333
  Args:
1334
  input_midi_path (str): The path to the input MIDI file.
1335
  All other arguments are rendering options from the Gradio UI.
 
1549
  srate = int(params.render_sample_rate)
1550
 
1551
  # --- Conditional Rendering Logic ---
1552
+ # --- 8-BIT SYNTHESIZER WORKFLOW ---
1553
  if params.soundfont_bank == SYNTH_8_BIT_LABEL:
1554
+ print("Using 8-bit style synthesizer with parallel processing workflow...")
1555
  try:
1556
+ # --- Step 1: Prepare MIDI data, load the MIDI file with pretty_midi for manual synthesis
1557
+ base_midi = pretty_midi.PrettyMIDI(midi_to_render_path)
1558
+ # Pre-process the base MIDI for harshness if enabled. This affects BOTH layers.
1559
  if getattr(params, 's8bit_enable_midi_preprocessing', False):
1560
+ base_midi = preprocess_midi_for_harshness(base_midi, params)
1561
+
1562
+ # --- Apply Arpeggiator if enabled ---
1563
+ arpeggiated_midi = None
1564
+ if getattr(params, 's8bit_enable_arpeggiator', False):
1565
+ arpeggiated_midi = arpeggiate_midi(base_midi, params)
1566
+
1567
+ # --- Step 2: Render the main (original) layer ---
1568
+ print(" - Rendering main synthesis layer...")
1569
+ # Synthesize the waveform, passing new FX parameters to the synthesis function
1570
+ main_waveform = synthesize_8bit_style(
1571
+ midi_data=base_midi,
1572
+ fs=srate,
1573
+ params=params,
1574
+ progress=progress
1575
+ )
1576
+
1577
+ final_waveform = main_waveform
1578
+
1579
+ # --- Step 3: Render the arpeggiator layer (if enabled) ---
1580
+ if arpeggiated_midi:
1581
+ print(" - Rendering arpeggiator layer...")
1582
+ # Temporarily override panning for the arpeggiator synth call
1583
+ arp_params = copy.copy(params)
1584
+
1585
+ # The synthesize_8bit_style function needs to know how to handle this new panning parameter
1586
+ # We will pass it via a temporary attribute.
1587
+ setattr(arp_params, '_temp_panning_override', params.s8bit_arpeggio_panning)
1588
+
1589
+ arpeggiated_waveform = synthesize_8bit_style(
1590
+ midi_data=arpeggiated_midi,
1591
+ fs=srate,
1592
+ params=arp_params,
1593
+ progress=None # Don't show a second progress bar
1594
+ )
1595
+
1596
+ # --- Step 4: Mix the layers together ---
1597
+ # Ensure waveforms have the same length
1598
+ len_main = final_waveform.shape[1]
1599
+ len_arp = arpeggiated_waveform.shape[1]
1600
+ if len_arp > len_main:
1601
+ final_waveform = np.pad(final_waveform, ((0, 0), (0, len_arp - len_main)))
1602
+ elif len_main > len_arp:
1603
+ arpeggiated_waveform = np.pad(arpeggiated_waveform, ((0, 0), (0, len_main - len_arp)))
1604
+
1605
+ final_waveform += arpeggiated_waveform
1606
+
1607
+ # --- Step 5: Finalize audio for Gradio, normalize and prepare for Gradio
1608
+ peak_val = np.max(np.abs(final_waveform))
1609
  if peak_val > 0:
1610
+ final_waveform /= peak_val
1611
  # Transpose from (2, N) to (N, 2) and convert to int16 for Gradio
1612
+ audio_out = (final_waveform.T * 32767).astype(np.int16)
1613
  except Exception as e:
1614
  print(f"Error during 8-bit synthesis: {e}")
1615
  return [None] * 7
1616
+ # --- SOUNDFONT WORKFLOW ---
1617
  else:
1618
  print(f"Using SoundFont: {params.soundfont_bank}")
1619
  # Get the full path from the global dictionary
 
3279
  # This outer group ensures the checkbox and its settings are visually linked.
3280
  with gr.Group():
3281
  s8bit_echo_sustain = gr.Checkbox(
3282
+ value=False, # Default to off as it's a special effect.
3283
  label="Enable Echo Sustain for Long Notes",
3284
  info="For 'Plucky' envelope only. Fills the silent tail of long, sustained notes with quiet, repeating pulses. Fixes 'choppy' sound on long piano notes."
3285
  )
3286
  # This inner group contains the sliders and is controlled by the checkbox above.
3287
+ with gr.Group(visible=False) as echo_sustain_settings:
3288
  s8bit_echo_rate_hz = gr.Slider(
3289
  1.0, 20.0, value=5.0, step=0.5,
3290
  label="Echo Rate (Hz)",
 
3301
  info="Controls how long a note must be to trigger echoes. This value is a multiplier of the 'Decay Time'. Example: If 'Decay Time' is 0.1s and this threshold is set to 10.0, only notes longer than 1.0s (0.1 * 10.0) will produce echoes."
3302
  )
3303
  # --- Re-architected into two separate, parallel accordions ---
3304
+ # --- Section 1: Arpeggiator (Creative Tool) ---
3305
+ with gr.Accordion("Arpeggiator (Creative Tool to Reduce Stiffness)", open=False):
3306
+ s8bit_enable_arpeggiator = gr.Checkbox(
3307
+ value=False,
3308
+ label="Enable Arpeggiator (to reduce stiffness)",
3309
+ info="Transforms chords into rapid sequences of notes, creating a classic, lively chiptune feel. This is a key technique to make 8-bit music sound more fluid."
3310
+ )
3311
+ with gr.Group(visible=False) as arpeggiator_settings_box:
3312
+ s8bit_arpeggiate_only_lower_tracks = gr.Checkbox(
3313
+ value=True,
3314
+ label="Arpeggiate Accompaniment Only (Protect Melody)",
3315
+ info="Recommended. Automatically detects the highest-pitched instrument track and excludes it from arpeggiation, preserving the lead melody."
3316
+ )
3317
+ s8bit_arpeggio_velocity_scale = gr.Slider(
3318
+ 0.1, 1.5, value=0.3, step=0.05,
3319
+ label="Arpeggio Velocity Scale",
3320
+ info="A master volume control for the arpeggiator. 0.7 means arpeggiated notes will have 70% of the original chord's velocity."
3321
+ )
3322
+ s8bit_arpeggio_density = gr.Slider(
3323
+ 0.1, 1.0, value=0.4, step=0.05,
3324
+ label="Arpeggio Density Scale",
3325
+ info="Controls the density/sparseness of arpeggios. Lower values create more silence between notes, making long chords feel more relaxed."
3326
+ )
3327
+ s8bit_arpeggio_rhythm = gr.Dropdown(
3328
+ [
3329
+ "Continuous 16ths",
3330
+ "Classic Upbeat (8th)",
3331
+ "Pulsing 8ths",
3332
+ "Pulsing 4ths",
3333
+ "Galloping",
3334
+ "Simple Quarter Notes"
3335
+ ],
3336
+ value="Pulsing 8ths",
3337
+ label="Arpeggio Rhythm Pattern",
3338
+ info="""
3339
+ - **Continuous 16ths:** A constant, driving wall of sound with no breaks. Creates a very dense, high-energy texture.
3340
+ - **Classic Upbeat (8th):** The quintessential chiptune rhythm. Creates a bouncy, syncopated feel by playing on the off-beats. (Sounds like: _ _ ta-ta)
3341
+ - **Pulsing 8ths:** A steady, on-beat rhythm playing two notes per beat. Good for a solid, rhythmic foundation. (Sounds like: ta-ta ta-ta)
3342
+ - **Pulsing 4ths:** A strong, deliberate pulse on each downbeat, with a clear separation between notes. (Sounds like: ta_ ta_ ta_)
3343
+ - **Galloping:** A driving, forward-moving rhythm with a distinctive long-short pattern. Excellent for action themes. (Sounds like: ta--ta ta--ta)
3344
+ - **Simple Quarter Notes:** The most sparse pattern, playing one sustained note per beat. Creates a calm and open feel.
3345
+ """
3346
+ )
3347
+ s8bit_arpeggio_pattern = gr.Dropdown(
3348
+ ["Up", "Down", "UpDown"],
3349
+ value="Up",
3350
+ label="Arpeggio Pattern",
3351
+ info="""
3352
+ - **Up:** The classic choice. Ascends from the lowest to the highest note of the chord, then jumps back to the bottom. Creates a feeling of energy, optimism, and forward momentum.
3353
+ - **Down:** Descends from the highest to the lowest note. Often creates a more melancholic, reflective, or suspenseful mood.
3354
+ - **UpDown:** Ascends to the highest note, then descends back down without jumping. This is the smoothest and most fluid pattern, creating a gentle, wave-like motion.
3355
+ """
3356
+ )
3357
+ s8bit_arpeggio_octave_range = gr.Slider(
3358
+ 1, 4, value=1, step=1,
3359
+ label="Arpeggio Octave Range",
3360
+ info="How many octaves the arpeggio pattern will span before repeating."
3361
+ )
3362
+ s8bit_arpeggio_panning = gr.Dropdown(
3363
+ ["Stereo", "Center", "Left", "Right"],
3364
+ value="Stereo",
3365
+ label="Arpeggio Layer Panning",
3366
+ info="""
3367
+ - **Stereo (Recommended):** Creates a wide, immersive sound by alternating arpeggio tracks between the left and right speakers. This provides maximum clarity and separation from the main melody.
3368
+ - **Center:** Places the arpeggio directly in the middle (mono). Creates a focused, powerful, and retro sound, but may conflict with a centered lead melody.
3369
+ - **Left / Right:** Places the entire arpeggio layer on only one side. Useful for creative "call and response" effects or special mixing choices.
3370
+ """
3371
+ )
3372
+
3373
+ # --- Section 2: MIDI Pre-processing (Corrective Tool) ---
3374
+ with gr.Accordion("MIDI Pre-processing (Corrective Tool)", open=False):
3375
  s8bit_enable_midi_preprocessing = gr.Checkbox(
3376
  value=True,
3377
+ label="Enable MIDI Pre-processing (Anti-Harshness)",
3378
  info="Intelligently reduces the velocity of notes that are likely to cause harshness (e.g., very high notes or loud, dense chords) before synthesis begins."
3379
  )
3380
  with gr.Group(visible=True) as midi_preprocessing_settings_box:
 
3404
  info="Velocity multiplier for loud, dense chords."
3405
  )
3406
 
3407
+ # --- Section 3: Audio Post-processing, accordion for Anti-Aliasing and Quality Settings ---
3408
+ with gr.Accordion("Audio Quality & Anti-Aliasing (Post-processing)", open=False):
3409
  s8bit_enable_anti_aliasing = gr.Checkbox(
3410
  value=False,
3411
  label="Enable All Audio Quality Enhancements",
3412
  info="Master toggle for all settings below. Disabling may slightly speed up rendering but can result in harsher, more aliased sound."
3413
  )
3414
+ with gr.Group(visible=False) as anti_aliasing_settings_box:
3415
  s8bit_use_additive_synthesis = gr.Checkbox(
3416
  value=False,
3417
  label="Use Additive Synthesis (High Quality, High CPU)",
 
3563
  inputs=s8bit_enable_midi_preprocessing,
3564
  outputs=midi_preprocessing_settings_box
3565
  )
3566
+ # Event listener for the new Arpeggiator settings box
3567
+ s8bit_enable_arpeggiator.change(
3568
+ fn=lambda x: gr.update(visible=x),
3569
+ inputs=s8bit_enable_arpeggiator,
3570
+ outputs=arpeggiator_settings_box
3571
+ )
3572
 
3573
  # Launch the Gradio app
3574
  app.queue().launch(inbrowser=True, debug=True)