feat(synth): Add intelligent MIDI pre-processing to reduce harshness
Browse filesThis commit introduces a new, optional MIDI pre-processing stage for the 8-bit synthesizer. Its purpose is to proactively "tame" MIDI characteristics that are known to cause sonic harshness and aliasing before the audio synthesis process begins.
The new pre-processor implements two main rules:
1. **High-Pitch Attenuation:**
Automatically reduces the velocity of notes that exceed a user-defined pitch threshold (e.g., C6).
This mitigates the severe aliasing that occurs when rich-harmonic waveforms (like square and saw) are synthesized at very high frequencies.
2. **Chord Compression:**
Detects loud, dense chords based on user-defined thresholds for note count and average velocity.
Reduces the velocity of all notes within such chords.
This prevents the excessive harmonic buildup that leads to a harsh, saturated, and often clipped sound when multiple loud notes are played simultaneously.
|
@@ -175,10 +175,74 @@ class AppParameters:
|
|
| 175 |
s8bit_harmonic_lowpass_factor: float = 12.0 # Multiplier for frequency-dependent lowpass filter
|
| 176 |
s8bit_final_gain: float = 0.8 # Final gain/limiter level to prevent clipping
|
| 177 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 178 |
# =================================================================================================
|
| 179 |
# === Helper Functions ===
|
| 180 |
# =================================================================================================
|
| 181 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
def one_pole_lowpass(x, cutoff_hz, fs):
|
| 183 |
"""Simple one-pole lowpass filter (causal), stable and cheap."""
|
| 184 |
if cutoff_hz <= 0 or cutoff_hz >= fs/2:
|
|
@@ -1281,6 +1345,10 @@ def Render_MIDI(*, input_midi_path: str, params: AppParameters, progress: gr.Pro
|
|
| 1281 |
try:
|
| 1282 |
# Load the MIDI file with pretty_midi for manual synthesis
|
| 1283 |
midi_data_for_synth = pretty_midi.PrettyMIDI(midi_to_render_path)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1284 |
# Synthesize the waveform
|
| 1285 |
# --- Passing new FX parameters to the synthesis function ---
|
| 1286 |
audio = synthesize_8bit_style(midi_data=midi_data_for_synth, fs=srate, params=params, progress=progress)
|
|
@@ -2727,7 +2795,7 @@ if __name__ == "__main__":
|
|
| 2727 |
merge_drums_to_render = gr.Checkbox(label="Merge Drums", value=False, visible=False)
|
| 2728 |
merge_bass_to_render = gr.Checkbox(label="Merge Bass", value=False, visible=False)
|
| 2729 |
# This checkbox will have its label changed dynamically
|
| 2730 |
-
merge_other_or_accompaniment = gr.Checkbox(label="Merge Accompaniment", value=
|
| 2731 |
|
| 2732 |
with gr.Accordion("General Purpose Transcription Settings", open=True) as general_transcription_settings:
|
| 2733 |
# --- Preset dropdown for basic_pitch ---
|
|
@@ -2979,10 +3047,45 @@ if __name__ == "__main__":
|
|
| 2979 |
label="Echo Trigger Threshold (x Decay Time)",
|
| 2980 |
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."
|
| 2981 |
)
|
| 2982 |
-
# ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2983 |
with gr.Accordion("Audio Quality & Anti-Aliasing (Advanced)", open=False):
|
| 2984 |
s8bit_enable_anti_aliasing = gr.Checkbox(
|
| 2985 |
-
value=
|
| 2986 |
label="Enable All Audio Quality Enhancements",
|
| 2987 |
info="Master toggle for all settings below. Disabling may slightly speed up rendering but can result in harsher, more aliased sound."
|
| 2988 |
)
|
|
@@ -3132,6 +3235,12 @@ if __name__ == "__main__":
|
|
| 3132 |
inputs=s8bit_enable_anti_aliasing,
|
| 3133 |
outputs=anti_aliasing_settings_box
|
| 3134 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3135 |
|
| 3136 |
# Launch the Gradio app
|
| 3137 |
app.queue().launch(inbrowser=True, debug=True)
|
|
|
|
| 175 |
s8bit_harmonic_lowpass_factor: float = 12.0 # Multiplier for frequency-dependent lowpass filter
|
| 176 |
s8bit_final_gain: float = 0.8 # Final gain/limiter level to prevent clipping
|
| 177 |
|
| 178 |
+
# --- MIDI Pre-processing to Reduce Harshness ---
|
| 179 |
+
s8bit_enable_midi_preprocessing: bool = True # Master switch for this feature
|
| 180 |
+
s8bit_high_pitch_threshold: int = 84 # Pitch (C6) above which velocity is scaled
|
| 181 |
+
s8bit_high_pitch_velocity_scale: float = 0.8 # Velocity multiplier for high notes (e.g., 80%)
|
| 182 |
+
s8bit_chord_density_threshold: int = 4 # Min number of notes to be considered a dense chord
|
| 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 |
# =================================================================================================
|
| 189 |
|
| 190 |
+
def preprocess_midi_for_harshness(midi_data: pretty_midi.PrettyMIDI, params: AppParameters):
|
| 191 |
+
"""
|
| 192 |
+
Analyzes and modifies a PrettyMIDI object in-place to reduce characteristics
|
| 193 |
+
that can cause harshness in simple synthesizers.
|
| 194 |
+
|
| 195 |
+
Args:
|
| 196 |
+
midi_data: The PrettyMIDI object to process.
|
| 197 |
+
params: The AppParameters object containing the control thresholds.
|
| 198 |
+
"""
|
| 199 |
+
print("Running MIDI pre-processing to reduce harshness...")
|
| 200 |
+
notes_modified = 0
|
| 201 |
+
chords_tamed = 0
|
| 202 |
+
|
| 203 |
+
# Rule 1: High-Pitch Attenuation
|
| 204 |
+
for instrument in midi_data.instruments:
|
| 205 |
+
for note in instrument.notes:
|
| 206 |
+
if note.pitch > params.s8bit_high_pitch_threshold:
|
| 207 |
+
original_velocity = note.velocity
|
| 208 |
+
note.velocity = int(note.velocity * params.s8bit_high_pitch_velocity_scale)
|
| 209 |
+
if note.velocity < 1: note.velocity = 1
|
| 210 |
+
notes_modified += 1
|
| 211 |
+
|
| 212 |
+
if notes_modified > 0:
|
| 213 |
+
print(f" - Tamed {notes_modified} individual high-pitched notes.")
|
| 214 |
+
|
| 215 |
+
# Rule 2: Chord Compression
|
| 216 |
+
# This is a simplified approach: group notes by near-simultaneous start times
|
| 217 |
+
all_notes = sorted([note for instrument in midi_data.instruments for note in instrument.notes], key=lambda x: x.start)
|
| 218 |
+
|
| 219 |
+
time_window = 0.02 # 20ms window to group notes into a chord
|
| 220 |
+
i = 0
|
| 221 |
+
while i < len(all_notes):
|
| 222 |
+
current_chord = [all_notes[i]]
|
| 223 |
+
# Find other notes within the time window
|
| 224 |
+
j = i + 1
|
| 225 |
+
while j < len(all_notes) and (all_notes[j].start - all_notes[i].start) < time_window:
|
| 226 |
+
current_chord.append(all_notes[j])
|
| 227 |
+
j += 1
|
| 228 |
+
|
| 229 |
+
# Analyze and potentially tame the chord
|
| 230 |
+
if len(current_chord) >= params.s8bit_chord_density_threshold:
|
| 231 |
+
avg_velocity = sum(n.velocity for n in current_chord) / len(current_chord)
|
| 232 |
+
if avg_velocity > params.s8bit_chord_velocity_threshold:
|
| 233 |
+
chords_tamed += 1
|
| 234 |
+
for note in current_chord:
|
| 235 |
+
note.velocity = int(note.velocity * params.s8bit_chord_velocity_scale)
|
| 236 |
+
if note.velocity < 1: note.velocity = 1
|
| 237 |
+
|
| 238 |
+
# Move index past the current chord
|
| 239 |
+
i = j
|
| 240 |
+
|
| 241 |
+
if chords_tamed > 0:
|
| 242 |
+
print(f" - Tamed {chords_tamed} loud, dense chords.")
|
| 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:
|
|
|
|
| 1345 |
try:
|
| 1346 |
# Load the MIDI file with pretty_midi for manual synthesis
|
| 1347 |
midi_data_for_synth = pretty_midi.PrettyMIDI(midi_to_render_path)
|
| 1348 |
+
# --- Apply MIDI Pre-processing if enabled ---
|
| 1349 |
+
if getattr(params, 's8bit_enable_midi_preprocessing', False):
|
| 1350 |
+
midi_data_for_synth = preprocess_midi_for_harshness(midi_data_for_synth, params)
|
| 1351 |
+
|
| 1352 |
# Synthesize the waveform
|
| 1353 |
# --- Passing new FX parameters to the synthesis function ---
|
| 1354 |
audio = synthesize_8bit_style(midi_data=midi_data_for_synth, fs=srate, params=params, progress=progress)
|
|
|
|
| 2795 |
merge_drums_to_render = gr.Checkbox(label="Merge Drums", value=False, visible=False)
|
| 2796 |
merge_bass_to_render = gr.Checkbox(label="Merge Bass", value=False, visible=False)
|
| 2797 |
# This checkbox will have its label changed dynamically
|
| 2798 |
+
merge_other_or_accompaniment = gr.Checkbox(label="Merge Accompaniment", value=False)
|
| 2799 |
|
| 2800 |
with gr.Accordion("General Purpose Transcription Settings", open=True) as general_transcription_settings:
|
| 2801 |
# --- Preset dropdown for basic_pitch ---
|
|
|
|
| 3047 |
label="Echo Trigger Threshold (x Decay Time)",
|
| 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 |
+
# --- Section 1: MIDI Pre-processing ---
|
| 3052 |
+
with gr.Accordion("MIDI Pre-processing (Anti-Harshness)", open=False):
|
| 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:
|
| 3059 |
+
s8bit_high_pitch_threshold = gr.Slider(
|
| 3060 |
+
60, 108, value=84, step=1,
|
| 3061 |
+
label="High Pitch Threshold (MIDI Note)",
|
| 3062 |
+
info="Notes above this pitch will have their velocity reduced. 84 = C6."
|
| 3063 |
+
)
|
| 3064 |
+
s8bit_high_pitch_velocity_scale = gr.Slider(
|
| 3065 |
+
0.1, 1.0, value=0.8, step=0.05,
|
| 3066 |
+
label="High Pitch Velocity Scale",
|
| 3067 |
+
info="Multiplier for high notes' velocity (e.g., 0.8 = 80% of original velocity)."
|
| 3068 |
+
)
|
| 3069 |
+
s8bit_chord_density_threshold = gr.Slider(
|
| 3070 |
+
2, 10, value=4, step=1,
|
| 3071 |
+
label="Chord Density Threshold",
|
| 3072 |
+
info="Minimum number of notes to be considered a 'dense' chord."
|
| 3073 |
+
)
|
| 3074 |
+
s8bit_chord_velocity_threshold = gr.Slider(
|
| 3075 |
+
50, 127, value=100, step=1,
|
| 3076 |
+
label="Chord Velocity Threshold",
|
| 3077 |
+
info="If a dense chord's average velocity is above this, it will be tamed."
|
| 3078 |
+
)
|
| 3079 |
+
s8bit_chord_velocity_scale = gr.Slider(
|
| 3080 |
+
0.1, 1.0, value=0.75, step=0.05,
|
| 3081 |
+
label="Chord Velocity Scale",
|
| 3082 |
+
info="Velocity multiplier for loud, dense chords."
|
| 3083 |
+
)
|
| 3084 |
+
|
| 3085 |
+
# --- Section 2: Audio Post-processing, accordion for Anti-Aliasing and Quality Settings ---
|
| 3086 |
with gr.Accordion("Audio Quality & Anti-Aliasing (Advanced)", open=False):
|
| 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 |
)
|
|
|
|
| 3235 |
inputs=s8bit_enable_anti_aliasing,
|
| 3236 |
outputs=anti_aliasing_settings_box
|
| 3237 |
)
|
| 3238 |
+
# Event listener for the new MIDI Pre-processing settings box
|
| 3239 |
+
s8bit_enable_midi_preprocessing.change(
|
| 3240 |
+
fn=lambda x: gr.update(visible=x),
|
| 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)
|