feat(synth): Upgrade Delay/Echo effect to be tempo-synced
Browse filesThis commit refactors the MIDI Delay/Echo effect, transforming it from a static, time-based processor into a dynamic, musical tool that synchronizes with the song's rhythm.
app.py
CHANGED
|
@@ -199,7 +199,7 @@ class AppParameters:
|
|
| 199 |
# --- MIDI Delay/Echo Effect Parameters ---
|
| 200 |
s8bit_enable_delay: bool = False # Master switch for the delay effect
|
| 201 |
s8bit_delay_on_melody_only: bool = True # Apply delay only to the lead melody
|
| 202 |
-
|
| 203 |
s8bit_delay_feedback: float = 0.5 # Velocity scale for each subsequent echo (50%)
|
| 204 |
s8bit_delay_repeats: int = 3 # Number of echoes to generate
|
| 205 |
|
|
@@ -482,6 +482,7 @@ def arpeggiate_midi(midi_data: pretty_midi.PrettyMIDI, params: AppParameters):
|
|
| 482 |
"Pulsing 4ths": [(0.0, 0.5)],
|
| 483 |
"Galloping": [(0.0, 0.75), (0.75, 0.25)],
|
| 484 |
"Simple Quarter Notes": [(0.0, 1.0)],
|
|
|
|
| 485 |
}
|
| 486 |
selected_rhythm = rhythm_patterns.get(params.s8bit_arpeggio_rhythm, rhythm_patterns["Classic Upbeat (8th)"])
|
| 487 |
|
|
@@ -604,12 +605,34 @@ def create_delay_effect(midi_data: pretty_midi.PrettyMIDI, params: AppParameters
|
|
| 604 |
"""
|
| 605 |
Creates a delay/echo effect by duplicating notes with delayed start times
|
| 606 |
and scaled velocities. Can be configured to apply only to the lead melody.
|
|
|
|
| 607 |
"""
|
| 608 |
-
print("Applying MIDI delay/echo effect...")
|
| 609 |
# Work on a deep copy to ensure the original MIDI object is not mutated.
|
| 610 |
processed_midi = copy.deepcopy(midi_data)
|
| 611 |
-
|
| 612 |
-
# --- Step 1:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 613 |
notes_to_echo = []
|
| 614 |
|
| 615 |
if params.s8bit_delay_on_melody_only:
|
|
@@ -637,15 +660,15 @@ def create_delay_effect(midi_data: pretty_midi.PrettyMIDI, params: AppParameters
|
|
| 637 |
print(" - No notes found to apply delay to. Skipping.")
|
| 638 |
return processed_midi
|
| 639 |
|
| 640 |
-
# --- Step
|
| 641 |
echo_notes = []
|
| 642 |
for i in range(1, params.s8bit_delay_repeats + 1):
|
| 643 |
for original_note in notes_to_echo:
|
| 644 |
# Create a copy of the note for the echo
|
| 645 |
echo_note = copy.copy(original_note)
|
| 646 |
|
| 647 |
-
#
|
| 648 |
-
time_offset = i *
|
| 649 |
echo_note.start += time_offset
|
| 650 |
echo_note.end += time_offset
|
| 651 |
echo_note.velocity = int(echo_note.velocity * (params.s8bit_delay_feedback ** i))
|
|
@@ -654,13 +677,21 @@ def create_delay_effect(midi_data: pretty_midi.PrettyMIDI, params: AppParameters
|
|
| 654 |
if echo_note.velocity > 1:
|
| 655 |
echo_notes.append(echo_note)
|
| 656 |
|
| 657 |
-
# --- Step
|
| 658 |
if echo_notes:
|
| 659 |
-
#
|
| 660 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 661 |
echo_instrument.notes.extend(echo_notes)
|
| 662 |
processed_midi.instruments.append(echo_instrument)
|
| 663 |
-
print(f" - Generated {len(echo_notes)} echo notes on a new track.")
|
| 664 |
|
| 665 |
return processed_midi
|
| 666 |
|
|
@@ -3655,6 +3686,7 @@ if __name__ == "__main__":
|
|
| 3655 |
"Continuous 16ths",
|
| 3656 |
"Classic Upbeat (8th)",
|
| 3657 |
"Pulsing 8ths",
|
|
|
|
| 3658 |
"Pulsing 4ths",
|
| 3659 |
"Galloping",
|
| 3660 |
"Simple Quarter Notes"
|
|
@@ -3662,12 +3694,13 @@ if __name__ == "__main__":
|
|
| 3662 |
value="Pulsing 8ths",
|
| 3663 |
label="Arpeggio Rhythm Pattern",
|
| 3664 |
info="""
|
| 3665 |
-
- **Continuous 16ths:** A constant, driving wall of sound with no breaks. Creates a very dense, high-energy texture.
|
| 3666 |
-
- **Classic Upbeat (8th):** The quintessential chiptune rhythm. Creates a bouncy, syncopated feel by playing on the off-beats. (Sounds like: _ _ ta-ta)
|
| 3667 |
- **Pulsing 8ths:** A steady, on-beat rhythm playing two notes per beat. Good for a solid, rhythmic foundation. (Sounds like: ta-ta ta-ta)
|
|
|
|
| 3668 |
- **Pulsing 4ths:** A strong, deliberate pulse on each downbeat, with a clear separation between notes. (Sounds like: ta_ ta_ ta_)
|
| 3669 |
- **Galloping:** A driving, forward-moving rhythm with a distinctive long-short pattern. Excellent for action themes. (Sounds like: ta--ta ta--ta)
|
| 3670 |
-
- **Simple Quarter Notes:** The most sparse pattern, playing one sustained note per beat. Creates a calm and open feel.
|
| 3671 |
"""
|
| 3672 |
)
|
| 3673 |
s8bit_arpeggio_pattern = gr.Dropdown(
|
|
@@ -3708,10 +3741,18 @@ if __name__ == "__main__":
|
|
| 3708 |
label="Apply Delay to Melody Only",
|
| 3709 |
info="Recommended. Applies the echo effect only to the lead melody notes, keeping the harmony clean."
|
| 3710 |
)
|
| 3711 |
-
|
| 3712 |
-
|
| 3713 |
-
|
| 3714 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3715 |
)
|
| 3716 |
s8bit_delay_feedback = gr.Slider(
|
| 3717 |
0.1, 0.9, value=0.5, step=0.05,
|
|
|
|
| 199 |
# --- MIDI Delay/Echo Effect Parameters ---
|
| 200 |
s8bit_enable_delay: bool = False # Master switch for the delay effect
|
| 201 |
s8bit_delay_on_melody_only: bool = True # Apply delay only to the lead melody
|
| 202 |
+
s8bit_delay_division: str = "Dotted 8th Note"
|
| 203 |
s8bit_delay_feedback: float = 0.5 # Velocity scale for each subsequent echo (50%)
|
| 204 |
s8bit_delay_repeats: int = 3 # Number of echoes to generate
|
| 205 |
|
|
|
|
| 482 |
"Pulsing 4ths": [(0.0, 0.5)],
|
| 483 |
"Galloping": [(0.0, 0.75), (0.75, 0.25)],
|
| 484 |
"Simple Quarter Notes": [(0.0, 1.0)],
|
| 485 |
+
"Triplet 8ths": [(0.0, 1/3), (1/3, 1/3), (2/3, 1/3)],
|
| 486 |
}
|
| 487 |
selected_rhythm = rhythm_patterns.get(params.s8bit_arpeggio_rhythm, rhythm_patterns["Classic Upbeat (8th)"])
|
| 488 |
|
|
|
|
| 605 |
"""
|
| 606 |
Creates a delay/echo effect by duplicating notes with delayed start times
|
| 607 |
and scaled velocities. Can be configured to apply only to the lead melody.
|
| 608 |
+
based on the MIDI's estimated BPM and the user's selected musical division.
|
| 609 |
"""
|
| 610 |
+
print("Applying tempo-synced MIDI delay/echo effect...")
|
| 611 |
# Work on a deep copy to ensure the original MIDI object is not mutated.
|
| 612 |
processed_midi = copy.deepcopy(midi_data)
|
| 613 |
+
|
| 614 |
+
# --- Step 1: Estimate Tempo and Calculate Delay Time in Seconds ---
|
| 615 |
+
try:
|
| 616 |
+
bpm = midi_data.estimate_tempo()
|
| 617 |
+
except:
|
| 618 |
+
bpm = 120.0
|
| 619 |
+
print(f" - Delay using tempo: {bpm:.2f} BPM")
|
| 620 |
+
|
| 621 |
+
# This map defines the duration of each note division as a multiplier of a quarter note (a beat).
|
| 622 |
+
division_map = {
|
| 623 |
+
"Quarter Note": 1.0,
|
| 624 |
+
"Dotted 8th Note": 0.75,
|
| 625 |
+
"8th Note": 0.5,
|
| 626 |
+
"Triplet 8th Note": 1.0 / 3.0,
|
| 627 |
+
"16th Note": 0.25
|
| 628 |
+
}
|
| 629 |
+
beat_duration_s = 60.0 / bpm
|
| 630 |
+
division_multiplier = division_map.get(params.s8bit_delay_division, 0.75)
|
| 631 |
+
delay_time_s = beat_duration_s * division_multiplier
|
| 632 |
+
|
| 633 |
+
print(f" - Delay set to {params.s8bit_delay_division}, calculated time: {delay_time_s:.3f}s")
|
| 634 |
+
|
| 635 |
+
# --- Step 2: Identify the notes that should receive the echo effect ---
|
| 636 |
notes_to_echo = []
|
| 637 |
|
| 638 |
if params.s8bit_delay_on_melody_only:
|
|
|
|
| 660 |
print(" - No notes found to apply delay to. Skipping.")
|
| 661 |
return processed_midi
|
| 662 |
|
| 663 |
+
# --- Step 3: Generate echo notes using the calculated delay time ---
|
| 664 |
echo_notes = []
|
| 665 |
for i in range(1, params.s8bit_delay_repeats + 1):
|
| 666 |
for original_note in notes_to_echo:
|
| 667 |
# Create a copy of the note for the echo
|
| 668 |
echo_note = copy.copy(original_note)
|
| 669 |
|
| 670 |
+
# Use the tempo-synced time and velocity
|
| 671 |
+
time_offset = i * delay_time_s
|
| 672 |
echo_note.start += time_offset
|
| 673 |
echo_note.end += time_offset
|
| 674 |
echo_note.velocity = int(echo_note.velocity * (params.s8bit_delay_feedback ** i))
|
|
|
|
| 677 |
if echo_note.velocity > 1:
|
| 678 |
echo_notes.append(echo_note)
|
| 679 |
|
| 680 |
+
# --- Step 4: Add the echo notes to a new, dedicated instrument track ---
|
| 681 |
if echo_notes:
|
| 682 |
+
# Inherit the program from the first non-drum instrument
|
| 683 |
+
# This ensures the echo has the same timbral character as the original sound,
|
| 684 |
+
# preventing perceived pitch shifts caused by different harmonic structures.
|
| 685 |
+
base_program = 0 # Default to piano if no instruments are found
|
| 686 |
+
for inst in midi_data.instruments:
|
| 687 |
+
if not inst.is_drum:
|
| 688 |
+
base_program = inst.program
|
| 689 |
+
break # Use the program of the first non-drum track we find
|
| 690 |
+
|
| 691 |
+
echo_instrument = pretty_midi.Instrument(program=base_program, is_drum=False, name="Echo Layer")
|
| 692 |
echo_instrument.notes.extend(echo_notes)
|
| 693 |
processed_midi.instruments.append(echo_instrument)
|
| 694 |
+
print(f" - Generated {len(echo_notes)} tempo-synced echo notes on a new track with program {base_program}.")
|
| 695 |
|
| 696 |
return processed_midi
|
| 697 |
|
|
|
|
| 3686 |
"Continuous 16ths",
|
| 3687 |
"Classic Upbeat (8th)",
|
| 3688 |
"Pulsing 8ths",
|
| 3689 |
+
"Triplet 8ths",
|
| 3690 |
"Pulsing 4ths",
|
| 3691 |
"Galloping",
|
| 3692 |
"Simple Quarter Notes"
|
|
|
|
| 3694 |
value="Pulsing 8ths",
|
| 3695 |
label="Arpeggio Rhythm Pattern",
|
| 3696 |
info="""
|
| 3697 |
+
- **Continuous 16ths:** A constant, driving wall of sound with no breaks. Creates a very dense, high-energy texture. (Sounds like: ta-ta-ta-ta ta-ta-ta-ta)
|
| 3698 |
+
- **Classic Upbeat (8th):** The quintessential chiptune rhythm. Creates a bouncy, syncopated feel by playing on the off-beats. (Sounds like: _ _ ta-ta _ _ ta-ta)
|
| 3699 |
- **Pulsing 8ths:** A steady, on-beat rhythm playing two notes per beat. Good for a solid, rhythmic foundation. (Sounds like: ta-ta ta-ta)
|
| 3700 |
+
- **Triplet 8ths:** A rolling, "three-feel" rhythm that creates a swing or shuffle groove. Very common in Blues, Jazz, and Hip-Hop. (Sounds like: ta-ta-ta ta-ta-ta)
|
| 3701 |
- **Pulsing 4ths:** A strong, deliberate pulse on each downbeat, with a clear separation between notes. (Sounds like: ta_ ta_ ta_)
|
| 3702 |
- **Galloping:** A driving, forward-moving rhythm with a distinctive long-short pattern. Excellent for action themes. (Sounds like: ta--ta ta--ta)
|
| 3703 |
+
- **Simple Quarter Notes:** The most sparse pattern, playing one sustained note per beat. Creates a calm and open feel. (Sounds like: ta _ ta _ ta _ ta _)
|
| 3704 |
"""
|
| 3705 |
)
|
| 3706 |
s8bit_arpeggio_pattern = gr.Dropdown(
|
|
|
|
| 3741 |
label="Apply Delay to Melody Only",
|
| 3742 |
info="Recommended. Applies the echo effect only to the lead melody notes, keeping the harmony clean."
|
| 3743 |
)
|
| 3744 |
+
s8bit_delay_division = gr.Dropdown(
|
| 3745 |
+
["Quarter Note", "Dotted 8th Note", "8th Note", "Triplet 8th Note", "16th Note"],
|
| 3746 |
+
value="Dotted 8th Note",
|
| 3747 |
+
label="Delay Time (Tempo Synced)",
|
| 3748 |
+
info="""
|
| 3749 |
+
"The time between echoes, synced to the MIDI's tempo. 'Dotted 8th Note' is a classic rhythmic choice."
|
| 3750 |
+
- **Quarter Note:** A simple, stable echo on the next beat. (1, 2, 3, 4)
|
| 3751 |
+
- **Dotted 8th Note (Classic):** Creates a very popular, rolling syncopated rhythm. Highly recommended for adding energy and complexity.
|
| 3752 |
+
- **8th Note:** A steady, "call and response" echo on the off-beat. Creates a running or swing feel.
|
| 3753 |
+
- **Triplet 8th Note:** Creates a unique "shuffling" or "bouncing" 3-feel groove over the standard 4/4 beat.
|
| 3754 |
+
- **16th Note:** A very fast, dense echo. Can act more like a textural effect than a distinct rhythmic delay.
|
| 3755 |
+
"""
|
| 3756 |
)
|
| 3757 |
s8bit_delay_feedback = gr.Slider(
|
| 3758 |
0.1, 0.9, value=0.5, step=0.05,
|