avans06 commited on
Commit
487757b
·
1 Parent(s): 1db4ef0

feat(synth): Add creative MIDI effects engine with Delay and targeted Arpeggiator

Browse files

This commit introduces a new "Creative MIDI Effects" module and refactors the processing pipeline to support advanced, non-destructive MIDI transformations. This marks a shift from simple synthesis to a more powerful sound design workflow.

Delay/Echo Effect:
Adds configurable, decaying echoes to notes (e.g., melody only) to create space and depth. Echoes are generated on a separate track.

Target-Aware Arpeggiator:
The arpeggiator can now target the "Accompaniment Only" (classic chiptune style) or "Melody Only" (modern synth-lead style), giving users far greater creative control over its musical role.

Files changed (1) hide show
  1. app.py +274 -132
app.py CHANGED
@@ -187,7 +187,7 @@ class AppParameters:
187
 
188
  # --- Arpeggiator Parameters ---
189
  s8bit_enable_arpeggiator: bool = False # Master switch for the arpeggiator
190
- s8bit_arpeggiate_only_lower_tracks: bool = True # Only arpeggiate lower tracks (melody protection)
191
  s8bit_arpeggio_velocity_scale: float = 0.7 # Velocity multiplier for arpeggiated notes (0.0 to 1.0)
192
  s8bit_arpeggio_density: float = 0.5 # Density factor for rhythmic patterns (0.0 to 1.0)
193
  s8bit_arpeggio_rhythm: str = "Classic Upbeat (8th)" # Rhythmic pattern for arpeggiation
@@ -195,6 +195,13 @@ class AppParameters:
195
  s8bit_arpeggio_octave_range: int = 1 # How many octaves the pattern spans
196
  s8bit_arpeggio_panning: str = "Stereo" # Panning mode for arpeggiated notes (Stereo, Left, Right, Center)
197
 
 
 
 
 
 
 
 
198
  # =================================================================================================
199
  # === Helper Functions ===
200
  # =================================================================================================
@@ -277,6 +284,11 @@ def arpeggiate_midi(midi_data: pretty_midi.PrettyMIDI, params: AppParameters):
277
  Improved rhythmic arpeggiator with dynamic density, stereo layer splitting,
278
  micro-randomization, and cross-beat continuity.
279
 
 
 
 
 
 
280
  Args:
281
  midi_data: The original PrettyMIDI object.
282
  params: AppParameters containing arpeggiator settings.
@@ -284,34 +296,10 @@ def arpeggiate_midi(midi_data: pretty_midi.PrettyMIDI, params: AppParameters):
284
  Returns:
285
  A new PrettyMIDI object with arpeggiated chords.
286
  """
287
- print("Applying rhythmic arpeggiator to MIDI data...")
288
- # Work on a deep copy to avoid modifying the original object passed to the function
289
  processed_midi = copy.deepcopy(midi_data)
290
-
291
- # --- Step 1: Estimate Tempo ---
292
- try:
293
- # Estimate the main tempo of the piece.
294
- bpm = midi_data.estimate_tempo()
295
- print(f" - Estimated MIDI Tempo: {bpm:.2f} BPM")
296
- except:
297
- bpm = 120.0
298
- beat_duration_s = 60.0 / bpm
299
- print(f" - Arpeggiator using tempo: {bpm:.2f} BPM")
300
 
301
- # --- Step 2: Define Rhythmic Patterns ---
302
- # Each pattern is a list of tuples: (start_offset_in_beat, duration_in_beat)
303
- # A beat is a quarter note.
304
- rhythm_patterns = {
305
- "Continuous 16ths": [(0.0, 0.25), (0.25, 0.25), (0.5, 0.25), (0.75, 0.25)],
306
- "Classic Upbeat (8th)": [(0.5, 0.25), (0.75, 0.25)],# Two 16th notes on the upbeat
307
- "Galloping": [(0.0, 0.75), (0.75, 0.25)],
308
- "Pulsing 8ths": [(0.0, 0.5), (0.5, 0.5)],
309
- "Simple Quarter Notes": [(0.0, 1.0)],
310
- "Pulsing 4ths": [(0.0, 0.5)],
311
- }
312
- selected_rhythm = rhythm_patterns.get(params.s8bit_arpeggio_rhythm, rhythm_patterns["Classic Upbeat (8th)"])
313
-
314
- # --- The logic for lead/harmony separation, Collect all notes ---
315
  all_notes = []
316
  # We need to keep track of which instrument each note belongs to
317
  for i, instrument in enumerate(processed_midi.instruments):
@@ -319,129 +307,235 @@ def arpeggiate_midi(midi_data: pretty_midi.PrettyMIDI, params: AppParameters):
319
  for note in instrument.notes:
320
  # Use a simple object or tuple to store note and its origin
321
  all_notes.append({'note': note, 'instrument_idx': i})
322
-
323
  if not all_notes:
324
  return processed_midi
325
  all_notes.sort(key=lambda x: x['note'].start)
326
-
327
  # --- Lead / Harmony separation ---
328
  lead_note_objects = set()
329
  harmony_note_objects = set()
330
 
331
- if params.s8bit_arpeggiate_only_lower_tracks:
332
- print(" - Lead melody protection is ON. Analyzing global timeline...")
333
- note_idx = 0
334
- while note_idx < len(all_notes):
335
- current_slice_start = all_notes[note_idx]['note'].start
336
- notes_in_slice = [item for item in all_notes[note_idx:] if (item['note'].start - current_slice_start) < 0.02]
337
-
338
- if not notes_in_slice:
339
- note_idx += 1
340
- continue
341
 
342
- notes_in_slice.sort(key=lambda x: x['note'].pitch, reverse=True)
343
- lead_note_objects.add(notes_in_slice[0]['note'])
344
- for item in notes_in_slice[1:]:
345
- harmony_note_objects.add(item['note'])
346
 
347
- note_idx += len(notes_in_slice)
348
- else:
349
- print(" - Lead melody protection is OFF.")
350
- for item in all_notes:
351
  harmony_note_objects.add(item['note'])
352
 
353
- # --- Process each instrument ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
354
  for instrument in processed_midi.instruments:
355
  if instrument.is_drum:
356
  continue
357
 
358
  new_note_list = []
359
 
360
- # Separate the notes of this specific instrument into lead and harmony
361
- inst_lead_notes = [n for n in instrument.notes if n in lead_note_objects]
362
- inst_harmony_notes = [n for n in instrument.notes if n in harmony_note_objects]
363
-
364
- # Add all lead notes from this instrument back directly
365
- new_note_list.extend(inst_lead_notes) # Lead notes pass through
 
366
 
367
- # Process the harmony notes for this instrument to find chords
368
- processed_harmony_notes_in_inst = set()
369
- for note1 in inst_harmony_notes:
370
- if note1 in processed_harmony_notes_in_inst:
371
  continue
372
- # Collect simultaneous chord notes (within 20ms)
373
- 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]
 
 
 
 
 
 
374
  for n in chord_notes:
375
- processed_harmony_notes_in_inst.add(n) # Mark all chord notes as processed.
376
-
377
- if len(chord_notes) > 1:
378
- # Determine the chord's properties.
379
- chord_start_time = min(n.start for n in chord_notes)
380
- chord_end_time = max(n.end for n in chord_notes)
381
- avg_velocity = int(np.mean([n.velocity for n in chord_notes]) * params.s8bit_arpeggio_velocity_scale)
382
- avg_velocity = max(1, avg_velocity)
383
- pitches = sorted([n.pitch for n in chord_notes])
384
-
385
- # --- Dynamic density factor ---
386
- # params.s8bit_arpeggio_density ∈ [0.2, 1.0], default 0.5
387
- note_base_density = getattr(params, "s8bit_arpeggio_density", 0.5)
388
- chord_duration = chord_end_time - chord_start_time
389
- note_duration_factor = min(1.0, chord_duration / (2 * beat_duration_s))
390
-
391
- note_density_factor = note_base_density * note_duration_factor
392
-
393
- # --- Build pattern with octave range ---
394
- pattern = []
395
- for octave in range(params.s8bit_arpeggio_octave_range):
396
- octave_pitches = [p + 12*octave for p in pitches]
397
- if params.s8bit_arpeggio_pattern == "Up":
398
- pattern.extend(octave_pitches)
399
- elif params.s8bit_arpeggio_pattern == "Down":
400
- pattern.extend(reversed(octave_pitches))
401
- elif params.s8bit_arpeggio_pattern == "UpDown":
402
- pattern.extend(octave_pitches)
403
- if len(octave_pitches) > 2:
404
- pattern.extend(reversed(octave_pitches[1:-1]))
405
- if not pattern:
406
- continue
407
-
408
- # --- Lay down rhythmic notes ---
409
- current_time = chord_start_time
410
- pattern_index = 0
411
- while current_time < chord_end_time:
412
- # Lay down the rhythmic pattern for the current beat
413
- for start_offset, duration_beats in selected_rhythm:
414
- note_start_time = current_time + start_offset * beat_duration_s
415
- note_duration_s = duration_beats * beat_duration_s * note_density_factor
416
-
417
- # Ensure the note does not exceed the chord's total duration
418
- if note_start_time >= chord_end_time:
419
- break
420
-
421
- pitch = pattern[pattern_index % len(pattern)]
422
- pattern_index += 1
423
-
424
- # --- Micro-randomization ---
425
- rand_offset = random.uniform(-0.01, 0.01) # ±10ms
426
- final_velocity = int(avg_velocity + random.randint(-5,5))
427
- final_velocity = max(1, min(127, final_velocity))
428
-
429
- new_note = pretty_midi.Note(
430
- velocity=final_velocity,
431
- pitch=pitch,
432
- start=max(0.0, note_start_time + rand_offset),
433
- end=min(chord_end_time, note_start_time + note_duration_s)
434
- )
435
- new_note_list.append(new_note)
436
- current_time += beat_duration_s
437
 
438
- else: # Single harmony notes are passed through
439
- new_note_list.append(note1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
440
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
441
  # Replace the instrument's original note list with the new, processed one
442
  instrument.notes = new_note_list
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
443
 
444
- print("Rhythmic arpeggiator finished.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
445
  return processed_midi
446
 
447
 
@@ -1573,11 +1667,20 @@ def Render_MIDI(*, input_midi_path: str, params: AppParameters, progress: gr.Pro
1573
  if getattr(params, 's8bit_enable_midi_preprocessing', False):
1574
  base_midi = preprocess_midi_for_harshness(base_midi, params)
1575
 
 
 
 
 
 
 
1576
  # --- Apply Arpeggiator if enabled ---
 
 
1577
  arpeggiated_midi = None
1578
  if getattr(params, 's8bit_enable_arpeggiator', False):
 
1579
  arpeggiated_midi = arpeggiate_midi(base_midi, params)
1580
-
1581
  # --- Step 2: Render the main (original) layer ---
1582
  print(" - Rendering main synthesis layer...")
1583
  # Synthesize the waveform, passing new FX parameters to the synthesis function
@@ -1591,8 +1694,8 @@ def Render_MIDI(*, input_midi_path: str, params: AppParameters, progress: gr.Pro
1591
  final_waveform = main_waveform
1592
 
1593
  # --- Step 3: Render the arpeggiator layer (if enabled) ---
1594
- if arpeggiated_midi:
1595
- print(" - Rendering arpeggiator layer...")
1596
  # Temporarily override panning for the arpeggiator synth call
1597
  arp_params = copy.copy(params)
1598
 
@@ -3339,10 +3442,15 @@ if __name__ == "__main__":
3339
  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."
3340
  )
3341
  with gr.Group(visible=False) as arpeggiator_settings_box:
3342
- s8bit_arpeggiate_only_lower_tracks = gr.Checkbox(
3343
- value=True,
3344
- label="Arpeggiate Accompaniment Only (Protect Melody)",
3345
- info="Recommended. Automatically detects the highest-pitched instrument track and excludes it from arpeggiation, preserving the lead melody."
 
 
 
 
 
3346
  )
3347
  s8bit_arpeggio_velocity_scale = gr.Slider(
3348
  0.1, 1.5, value=0.3, step=0.05,
@@ -3399,6 +3507,34 @@ if __name__ == "__main__":
3399
  - **Left / Right:** Places the entire arpeggio layer on only one side. Useful for creative "call and response" effects or special mixing choices.
3400
  """
3401
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3402
 
3403
  # --- Section 2: MIDI Pre-processing (Corrective Tool) ---
3404
  with gr.Accordion("MIDI Pre-processing (Corrective Tool)", open=False):
@@ -3599,6 +3735,12 @@ if __name__ == "__main__":
3599
  inputs=s8bit_enable_arpeggiator,
3600
  outputs=arpeggiator_settings_box
3601
  )
 
 
 
 
 
 
3602
 
3603
  # Launch the Gradio app
3604
  app.queue().launch(inbrowser=True, debug=True)
 
187
 
188
  # --- Arpeggiator Parameters ---
189
  s8bit_enable_arpeggiator: bool = False # Master switch for the arpeggiator
190
+ s8bit_arpeggio_target: str = "Accompaniment Only" # Target selection for the arpeggiator
191
  s8bit_arpeggio_velocity_scale: float = 0.7 # Velocity multiplier for arpeggiated notes (0.0 to 1.0)
192
  s8bit_arpeggio_density: float = 0.5 # Density factor for rhythmic patterns (0.0 to 1.0)
193
  s8bit_arpeggio_rhythm: str = "Classic Upbeat (8th)" # Rhythmic pattern for arpeggiation
 
195
  s8bit_arpeggio_octave_range: int = 1 # How many octaves the pattern spans
196
  s8bit_arpeggio_panning: str = "Stereo" # Panning mode for arpeggiated notes (Stereo, Left, Right, Center)
197
 
198
+ # --- MIDI Delay/Echo Effect Parameters ---
199
+ s8bit_enable_delay: bool = False # Master switch for the delay effect
200
+ s8bit_delay_on_melody_only: bool = True # Apply delay only to the lead melody
201
+ s8bit_delay_time_s: float = 0.15 # Time in seconds between each echo
202
+ s8bit_delay_feedback: float = 0.5 # Velocity scale for each subsequent echo (50%)
203
+ s8bit_delay_repeats: int = 3 # Number of echoes to generate
204
+
205
  # =================================================================================================
206
  # === Helper Functions ===
207
  # =================================================================================================
 
284
  Improved rhythmic arpeggiator with dynamic density, stereo layer splitting,
285
  micro-randomization, and cross-beat continuity.
286
 
287
+ Applies a highly configurable arpeggiator with selectable targets:
288
+ - Accompaniment Only: The classic approach, arpeggiates harmony.
289
+ - Melody Only: A modern approach, adds flair to the lead melody.
290
+ - Full Mix: Applies the effect to all notes.
291
+
292
  Args:
293
  midi_data: The original PrettyMIDI object.
294
  params: AppParameters containing arpeggiator settings.
 
296
  Returns:
297
  A new PrettyMIDI object with arpeggiated chords.
298
  """
299
+ print(f"Applying arpeggiator with target: {params.s8bit_arpeggio_target}...")
 
300
  processed_midi = copy.deepcopy(midi_data)
 
 
 
 
 
 
 
 
 
 
301
 
302
+ # --- Step 1: Global analysis to identify lead vs. harmony notes ---
 
 
 
 
 
 
 
 
 
 
 
 
 
303
  all_notes = []
304
  # We need to keep track of which instrument each note belongs to
305
  for i, instrument in enumerate(processed_midi.instruments):
 
307
  for note in instrument.notes:
308
  # Use a simple object or tuple to store note and its origin
309
  all_notes.append({'note': note, 'instrument_idx': i})
310
+
311
  if not all_notes:
312
  return processed_midi
313
  all_notes.sort(key=lambda x: x['note'].start)
314
+
315
  # --- Lead / Harmony separation ---
316
  lead_note_objects = set()
317
  harmony_note_objects = set()
318
 
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
+
335
+ # --- Step 2: Determine which set of notes to process based on the target ---
336
+ notes_to_arpeggiate = set()
337
+ notes_to_keep_original = set()
338
+
339
+ if params.s8bit_arpeggio_target == "Accompaniment Only":
340
+ print(" - Arpeggiating harmony notes.")
341
+ notes_to_arpeggiate = harmony_note_objects
342
+ notes_to_keep_original = lead_note_objects
343
+ elif params.s8bit_arpeggio_target == "Melody Only":
344
+ print(" - Arpeggiating lead melody notes.")
345
+ notes_to_arpeggiate = lead_note_objects
346
+ notes_to_keep_original = harmony_note_objects
347
+ else: # Full Mix
348
+ print(" - Arpeggiating all non-drum notes.")
349
+ notes_to_arpeggiate = lead_note_objects.union(harmony_note_objects)
350
+ notes_to_keep_original = set()
351
+
352
+ # --- Step 3: Estimate Tempo and prepare for generation ---
353
+ try:
354
+ bpm = midi_data.estimate_tempo()
355
+ except:
356
+ bpm = 120.0
357
+ beat_duration_s = 60.0 / bpm
358
+
359
+ rhythm_patterns = {
360
+ "Continuous 16ths": [(0.0, 0.25), (0.25, 0.25), (0.5, 0.25), (0.75, 0.25)],
361
+ "Classic Upbeat (8th)": [(0.5, 0.25), (0.75, 0.25)],
362
+ "Pulsing 8ths": [(0.0, 0.5), (0.5, 0.5)],
363
+ "Pulsing 4ths": [(0.0, 0.5)],
364
+ "Galloping": [(0.0, 0.75), (0.75, 0.25)],
365
+ "Simple Quarter Notes": [(0.0, 1.0)],
366
+ }
367
+ selected_rhythm = rhythm_patterns.get(params.s8bit_arpeggio_rhythm, rhythm_patterns["Classic Upbeat (8th)"])
368
+
369
+ # --- Step 4: Rebuild instruments with the new logic ---
370
  for instrument in processed_midi.instruments:
371
  if instrument.is_drum:
372
  continue
373
 
374
  new_note_list = []
375
 
376
+ # Add back all notes that are designated to be kept original for this track
377
+ inst_notes_to_keep = [n for n in instrument.notes if n in notes_to_keep_original]
378
+ new_note_list.extend(inst_notes_to_keep)
379
+
380
+ # Process only the notes targeted for arpeggiation within this instrument
381
+ inst_notes_to_arp = [n for n in instrument.notes if n in notes_to_arpeggiate]
382
+ processed_arp_notes = set()
383
 
384
+ for note1 in inst_notes_to_arp:
385
+ if note1 in processed_arp_notes:
 
 
386
  continue
387
+
388
+ # Group notes into chords from the target list.
389
+ # For melody, each note is its own "chord".
390
+ chord_notes = [note1]
391
+ if params.s8bit_arpeggio_target != "Melody Only":
392
+ chord_notes.extend([n2 for n2 in inst_notes_to_arp if n2 != note1 and n2 not in processed_arp_notes and abs(n2.start - note1.start) < 0.02])
393
+
394
+ # --- Arpeggiate the identified group (which could be a single note or a chord) ---
395
  for n in chord_notes:
396
+ processed_arp_notes.add(n)
397
+
398
+ chord_start_time = min(n.start for n in chord_notes)
399
+ chord_end_time = max(n.end for n in chord_notes)
400
+ avg_velocity = int(np.mean([n.velocity for n in chord_notes]))
401
+ final_velocity_base = int(avg_velocity * params.s8bit_arpeggio_velocity_scale)
402
+
403
+ if final_velocity_base < 1:
404
+ final_velocity_base = 1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
405
 
406
+ # --- Pitch Pattern Generation ---
407
+ base_pitches = sorted([n.pitch for n in chord_notes])
408
+
409
+ # For "Melody Only" mode, auto-generate a simple chord from the single melody note
410
+ if params.s8bit_arpeggio_target == "Melody Only" and len(base_pitches) == 1:
411
+ # This is a very simple major chord generator, can be expanded later
412
+ # Auto-generate a major chord from the single melody note
413
+ root = base_pitches[0]
414
+ base_pitches = [root, root + 4, root + 7]
415
+
416
+ pattern = []
417
+ for octave in range(params.s8bit_arpeggio_octave_range):
418
+ octave_pitches = [p + (12 * octave) for p in base_pitches]
419
+ if params.s8bit_arpeggio_pattern == "Up":
420
+ pattern.extend(octave_pitches)
421
+ elif params.s8bit_arpeggio_pattern == "Down":
422
+ pattern.extend(reversed(octave_pitches))
423
+ elif params.s8bit_arpeggio_pattern == "UpDown":
424
+ pattern.extend(octave_pitches)
425
+ if len(octave_pitches) > 2:
426
+ pattern.extend(reversed(octave_pitches[1:-1]))
427
+
428
+ if not pattern:
429
+ continue
430
+
431
+ # --- Rhythmic Note Generation ---
432
+ note_base_density = getattr(params, "s8bit_arpeggio_density", 0.6)
433
+ chord_duration = chord_end_time - chord_start_time
434
+ note_duration_factor = min(1.0, chord_duration / (2 * beat_duration_s)) if beat_duration_s > 0 else 1.0
435
+ note_density_factor = note_base_density * note_duration_factor
436
+
437
+ current_beat = chord_start_time / beat_duration_s if beat_duration_s > 0 else 0
438
+ current_time = chord_start_time
439
+ pattern_index = 0
440
+ while current_time < chord_end_time:
441
+ # Lay down the rhythmic pattern for the current beat
442
+ current_beat_start_time = np.floor(current_beat) * beat_duration_s
443
+
444
+ for start_offset, duration_beats in selected_rhythm:
445
+ note_start_time = current_beat_start_time + (start_offset * beat_duration_s)
446
+ note_duration_s = duration_beats * beat_duration_s * note_density_factor
447
+
448
+ # Ensure the note does not exceed the chord's total duration
449
+ if note_start_time >= chord_end_time:
450
+ break
451
 
452
+ pitch = pattern[pattern_index % len(pattern)]
453
+
454
+ # Micro-randomization
455
+ rand_offset = random.uniform(-0.01, 0.01) # ±10ms
456
+ final_velocity = max(1, min(127, final_velocity_base + random.randint(-5, 5)))
457
+
458
+ new_note = pretty_midi.Note(
459
+ velocity=final_velocity,
460
+ pitch=pitch,
461
+ start=max(0.0, note_start_time + rand_offset),
462
+ end=min(chord_end_time, note_start_time + note_duration_s)
463
+ )
464
+ new_note_list.append(new_note)
465
+ pattern_index += 1
466
+
467
+ current_beat += 1.0
468
+ current_time = current_beat * beat_duration_s if beat_duration_s > 0 else float('inf')
469
+
470
  # Replace the instrument's original note list with the new, processed one
471
  instrument.notes = new_note_list
472
+
473
+ print("Targeted arpeggiator finished.")
474
+ return processed_midi
475
+
476
+
477
+ def create_delay_effect(midi_data: pretty_midi.PrettyMIDI, params: AppParameters):
478
+ """
479
+ Creates a delay/echo effect by duplicating notes with delayed start times
480
+ and scaled velocities. Can be configured to apply only to the lead melody.
481
+ """
482
+ print("Applying MIDI delay/echo effect...")
483
+ # Work on a deep copy to ensure the original MIDI object is not mutated.
484
+ processed_midi = copy.deepcopy(midi_data)
485
+
486
+ # --- Step 1: Identify the notes that should receive the echo effect ---
487
+ notes_to_echo = []
488
+
489
+ if params.s8bit_delay_on_melody_only:
490
+ print(" - Delay will be applied to lead melody notes only.")
491
+ all_notes = [note for inst in processed_midi.instruments if not inst.is_drum for note in inst.notes]
492
+ all_notes.sort(key=lambda n: n.start)
493
 
494
+ note_idx = 0
495
+ while note_idx < len(all_notes):
496
+ current_slice_start = all_notes[note_idx].start
497
+ notes_in_slice = [n for n in all_notes[note_idx:] if (n.start - current_slice_start) < 0.02]
498
+ if not notes_in_slice:
499
+ note_idx += 1
500
+ continue
501
+
502
+ # The highest note in the slice is considered the lead note
503
+ notes_in_slice.sort(key=lambda n: n.pitch, reverse=True)
504
+ notes_to_echo.append(notes_in_slice[0])
505
+ note_idx += len(notes_in_slice)
506
+ else:
507
+ print(" - Delay will be applied to all non-drum notes.")
508
+ notes_to_echo = [note for inst in processed_midi.instruments if not inst.is_drum for note in inst.notes]
509
+
510
+ if not notes_to_echo:
511
+ print(" - No notes found to apply delay to. Skipping.")
512
+ return processed_midi
513
+
514
+ # --- Step 2: Generate the echo notes ---
515
+ echo_notes = []
516
+ for i in range(1, params.s8bit_delay_repeats + 1):
517
+ for original_note in notes_to_echo:
518
+ # Create a copy of the note for the echo
519
+ echo_note = copy.copy(original_note)
520
+
521
+ # Calculate new timing and velocity
522
+ time_offset = i * params.s8bit_delay_time_s
523
+ echo_note.start += time_offset
524
+ echo_note.end += time_offset
525
+ echo_note.velocity = int(echo_note.velocity * (params.s8bit_delay_feedback ** i))
526
+
527
+ # Only add the echo if its velocity is still audible
528
+ if echo_note.velocity > 1:
529
+ echo_notes.append(echo_note)
530
+
531
+ # --- Step 3: Add the echo notes to a new, dedicated instrument track ---
532
+ if echo_notes:
533
+ # Use a softer program for the echo, like a synth pad, to differentiate it.
534
+ echo_instrument = pretty_midi.Instrument(program=80, is_drum=False, name="Echo Layer")
535
+ echo_instrument.notes.extend(echo_notes)
536
+ processed_midi.instruments.append(echo_instrument)
537
+ print(f" - Generated {len(echo_notes)} echo notes on a new track.")
538
+
539
  return processed_midi
540
 
541
 
 
1667
  if getattr(params, 's8bit_enable_midi_preprocessing', False):
1668
  base_midi = preprocess_midi_for_harshness(base_midi, params)
1669
 
1670
+ # --- Apply Delay/Echo effect to the base MIDI if enabled ---
1671
+ # This is done BEFORE the arpeggiator, so the clean base_midi
1672
+ # (which contains the lead melody) gets the delay.
1673
+ if getattr(params, 's8bit_enable_delay', False):
1674
+ base_midi = create_delay_effect(base_midi, params)
1675
+
1676
  # --- Apply Arpeggiator if enabled ---
1677
+ # The arpeggiator will now correctly ignore the new echo track
1678
+ # because the echo notes are on a separate instrument.
1679
  arpeggiated_midi = None
1680
  if getattr(params, 's8bit_enable_arpeggiator', False):
1681
+ # We arpeggiate the (now possibly delayed) base_midi
1682
  arpeggiated_midi = arpeggiate_midi(base_midi, params)
1683
+
1684
  # --- Step 2: Render the main (original) layer ---
1685
  print(" - Rendering main synthesis layer...")
1686
  # Synthesize the waveform, passing new FX parameters to the synthesis function
 
1694
  final_waveform = main_waveform
1695
 
1696
  # --- Step 3: Render the arpeggiator layer (if enabled) ---
1697
+ if arpeggiated_midi and arpeggiated_midi.instruments:
1698
+ print(" - Rendering and mixing arpeggiator layer...")
1699
  # Temporarily override panning for the arpeggiator synth call
1700
  arp_params = copy.copy(params)
1701
 
 
3442
  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."
3443
  )
3444
  with gr.Group(visible=False) as arpeggiator_settings_box:
3445
+ s8bit_arpeggio_target = gr.Dropdown(
3446
+ ["Accompaniment Only", "Melody Only", "Full Mix"],
3447
+ value="Accompaniment Only",
3448
+ label="Arpeggiation Target",
3449
+ info="""
3450
+ - **Accompaniment Only (Classic):** Applies arpeggios only to the harmony/chord parts, leaving the lead melody untouched. The classic chiptune style.
3451
+ - **Melody Only (Modern):** Applies arpeggios as a decorative effect to the lead melody notes, leaving the accompaniment as is. Creates a modern, expressive synth lead sound.
3452
+ - **Full Mix:** Applies arpeggios to all non-drum tracks. Can create a very dense, complex texture.
3453
+ """
3454
  )
3455
  s8bit_arpeggio_velocity_scale = gr.Slider(
3456
  0.1, 1.5, value=0.3, step=0.05,
 
3507
  - **Left / Right:** Places the entire arpeggio layer on only one side. Useful for creative "call and response" effects or special mixing choices.
3508
  """
3509
  )
3510
+ # --- Delay/Echo Sub-Section ---
3511
+ with gr.Group():
3512
+ s8bit_enable_delay = gr.Checkbox(
3513
+ value=False,
3514
+ label="Enable Delay / Echo Effect",
3515
+ info="Adds repeating, decaying echoes to notes, creating a sense of space and rhythmic complexity."
3516
+ )
3517
+ with gr.Group(visible=False) as delay_settings_box:
3518
+ s8bit_delay_on_melody_only = gr.Checkbox(
3519
+ value=True,
3520
+ label="Apply Delay to Melody Only",
3521
+ info="Recommended. Applies the echo effect only to the lead melody notes, keeping the harmony clean."
3522
+ )
3523
+ s8bit_delay_time_s = gr.Slider(
3524
+ 0.05, 0.5, value=0.15, step=0.01,
3525
+ label="Delay Time (seconds)",
3526
+ info="The time between each echo. Sync this to the beat for rhythmic delays (e.g., 0.25s for 8th notes at 120 BPM)."
3527
+ )
3528
+ s8bit_delay_feedback = gr.Slider(
3529
+ 0.1, 0.9, value=0.5, step=0.05,
3530
+ label="Delay Feedback (Volume Decay)",
3531
+ info="Controls how much quieter each echo is. 0.5 means each echo is 50% the volume of the one before it."
3532
+ )
3533
+ s8bit_delay_repeats = gr.Slider(
3534
+ 1, 10, value=3, step=1,
3535
+ label="Number of Repeats",
3536
+ info="The total number of echoes to generate for each note."
3537
+ )
3538
 
3539
  # --- Section 2: MIDI Pre-processing (Corrective Tool) ---
3540
  with gr.Accordion("MIDI Pre-processing (Corrective Tool)", open=False):
 
3735
  inputs=s8bit_enable_arpeggiator,
3736
  outputs=arpeggiator_settings_box
3737
  )
3738
+ # Event listener for the new Delay/Echo settings box
3739
+ s8bit_enable_delay.change(
3740
+ fn=lambda x: gr.update(visible=x),
3741
+ inputs=s8bit_enable_delay,
3742
+ outputs=delay_settings_box
3743
+ )
3744
 
3745
  # Launch the Gradio app
3746
  app.queue().launch(inbrowser=True, debug=True)