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