avans06 commited on
Commit
d88d1e3
·
1 Parent(s): 8229bc2

feat(synth): Add intelligent MIDI pre-processing to reduce harshness

Browse files

This 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.

Files changed (1) hide show
  1. app.py +112 -3
app.py CHANGED
@@ -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=True)
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
- # --- NEW: Accordion for Anti-Aliasing and Quality Settings ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2983
  with gr.Accordion("Audio Quality & Anti-Aliasing (Advanced)", open=False):
2984
  s8bit_enable_anti_aliasing = gr.Checkbox(
2985
- value=True,
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)