feat(synth): Add creative MIDI effects engine with Delay and targeted Arpeggiator
Browse filesThis 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.
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 |
-
|
| 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
|
| 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
|
| 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 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 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 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
harmony_note_objects.add(item['note'])
|
| 346 |
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
for item in all_notes:
|
| 351 |
harmony_note_objects.add(item['note'])
|
| 352 |
|
| 353 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 354 |
for instrument in processed_midi.instruments:
|
| 355 |
if instrument.is_drum:
|
| 356 |
continue
|
| 357 |
|
| 358 |
new_note_list = []
|
| 359 |
|
| 360 |
-
#
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
#
|
| 365 |
-
|
|
|
|
| 366 |
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
for note1 in inst_harmony_notes:
|
| 370 |
-
if note1 in processed_harmony_notes_in_inst:
|
| 371 |
continue
|
| 372 |
-
|
| 373 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 374 |
for n in chord_notes:
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 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 |
-
|
| 439 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 440 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 441 |
# Replace the instrument's original note list with the new, processed one
|
| 442 |
instrument.notes = new_note_list
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 443 |
|
| 444 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 3343 |
-
|
| 3344 |
-
|
| 3345 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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)
|