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