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)
|