avans06 commited on
Commit
0676790
·
1 Parent(s): 8adc333

feat(synth): Add advanced frequency management for 8-bit synth MIDI and effects

Browse files

This commit introduces a suite of new tools specifically for the **8-bit synthesizer**, providing fine-grained control over the frequency spectrum to address issues with both excessive harshness in the high-end and muddiness in the low-end.

These features empower users to create cleaner, more balanced, and professional-sounding mixes directly within the synthesizer.

**1. MIDI Pre-processing (For 8-bit Synth): Low-Pitch Management**
Complements the existing high-pitch taming by adding a new "Low-Pitch Attenuation" rule.
Users can now define a low pitch threshold (e.g., C2) and a velocity scale.
The pre-processor will automatically reduce the velocity of any notes falling below this threshold before they are sent to the **8-bit synthesizer**.
This is a preventative measure to control excessive sub-bass energy, reduce muddiness, and prevent clipping.

**2. Delay/Echo Effect (For 8-bit Synth): Full Frequency Spectrum Control**
The Delay/Echo effect has been upgraded with a comprehensive "Frequency Management" toolkit, allowing users to shape the timbre of the echoes generated by the **8-bit synthesizer**.
**High-Pass Filter:** A configurable high-pass filter can be applied to the echo layer, cleanly removing low frequencies to prevent echoes from adding mud to the mix.
**Low-Pass Filter:** A new, corresponding low-pass filter can be applied to the echo layer, removing high frequencies to make echoes sound darker, warmer, and less harsh.
**Flexible Pitch Shifting (Transposer):** The previous fixed octave checkboxes have been replaced with powerful pitch-shifting sliders for both bass and treble notes, allowing for the creation of complex harmonies in the echo trails.
The audio filtering pipeline in `Render_MIDI` was refactored to support this multi-filter audio processing on the dedicated echo layer.

Files changed (1) hide show
  1. app.py +180 -17
app.py CHANGED
@@ -182,6 +182,10 @@ class AppParameters:
182
  s8bit_enable_midi_preprocessing: bool = True # Master switch for this feature
183
  s8bit_high_pitch_threshold: int = 84 # Pitch (C6) above which velocity is scaled
184
  s8bit_high_pitch_velocity_scale: float = 0.8 # Velocity multiplier for high notes (e.g., 80%)
 
 
 
 
185
  s8bit_chord_density_threshold: int = 4 # Min number of notes to be considered a dense chord
186
  s8bit_chord_velocity_threshold: int = 100 # Min average velocity for a chord to be tamed
187
  s8bit_chord_velocity_scale: float = 0.75 # Velocity multiplier for loud, dense chords
@@ -202,6 +206,12 @@ class AppParameters:
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
 
206
  # =================================================================================================
207
  # === Helper Functions ===
@@ -341,32 +351,42 @@ def format_params_for_metadata(params: AppParameters, transcription_log: dict =
341
  def preprocess_midi_for_harshness(midi_data: pretty_midi.PrettyMIDI, params: AppParameters):
342
  """
343
  Analyzes and modifies a PrettyMIDI object in-place to reduce characteristics
344
- that can cause harshness in simple synthesizers.
 
345
 
346
  Args:
347
  midi_data: The PrettyMIDI object to process.
348
  params: The AppParameters object containing the control thresholds.
349
  """
350
- print("Running MIDI pre-processing to reduce harshness...")
351
- notes_modified = 0
 
352
  chords_tamed = 0
353
-
354
- # Rule 1: High-Pitch Attenuation
355
  for instrument in midi_data.instruments:
356
  for note in instrument.notes:
 
357
  if note.pitch > params.s8bit_high_pitch_threshold:
358
- original_velocity = note.velocity
359
  note.velocity = int(note.velocity * params.s8bit_high_pitch_velocity_scale)
360
  if note.velocity < 1: note.velocity = 1
361
- notes_modified += 1
362
-
363
- if notes_modified > 0:
364
- print(f" - Tamed {notes_modified} individual high-pitched notes.")
 
 
 
 
 
 
 
 
365
 
366
- # Rule 2: Chord Compression
367
  # This is a simplified approach: group notes by near-simultaneous start times
368
  all_notes = sorted([note for instrument in midi_data.instruments for note in instrument.notes], key=lambda x: x.start)
369
-
370
  time_window = 0.02 # 20ms window to group notes into a chord
371
  i = 0
372
  while i < len(all_notes):
@@ -660,12 +680,21 @@ def create_delay_effect(midi_data: pretty_midi.PrettyMIDI, params: AppParameters
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
@@ -696,6 +725,42 @@ def create_delay_effect(midi_data: pretty_midi.PrettyMIDI, params: AppParameters
696
  return processed_midi
697
 
698
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
699
  def one_pole_lowpass(x, cutoff_hz, fs):
700
  """Simple one-pole lowpass filter (causal), stable and cheap."""
701
  if cutoff_hz <= 0 or cutoff_hz >= fs/2:
@@ -1832,18 +1897,83 @@ def Render_MIDI(*, input_midi_path: str, params: AppParameters, progress: gr.Pro
1832
  arpeggiated_midi = arpeggiate_midi(base_midi, params)
1833
 
1834
  # --- Step 2: Render the main (original) layer ---
1835
- print(" - Rendering main synthesis layer...")
1836
  # Synthesize the waveform, passing new FX parameters to the synthesis function
1837
- main_waveform = synthesize_8bit_style(
1838
  midi_data=base_midi,
1839
  fs=srate,
1840
  params=params,
1841
  progress=progress
1842
  )
 
 
 
 
 
 
 
1843
 
1844
- final_waveform = main_waveform
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1845
 
1846
- # --- Step 3: Render the arpeggiator layer (if enabled) ---
1847
  if arpeggiated_midi and arpeggiated_midi.instruments:
1848
  print(" - Rendering and mixing arpeggiator layer...")
1849
  # Temporarily override panning for the arpeggiator synth call
@@ -3764,6 +3894,28 @@ if __name__ == "__main__":
3764
  label="Number of Repeats",
3765
  info="The total number of echoes to generate for each note."
3766
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3767
 
3768
  # --- Section 2: MIDI Pre-processing (Corrective Tool) ---
3769
  with gr.Accordion("MIDI Pre-processing (Corrective Tool)", open=False):
@@ -3783,6 +3935,17 @@ if __name__ == "__main__":
3783
  label="High Pitch Velocity Scale",
3784
  info="Multiplier for high notes' velocity (e.g., 0.8 = 80% of original velocity)."
3785
  )
 
 
 
 
 
 
 
 
 
 
 
3786
  s8bit_chord_density_threshold = gr.Slider(
3787
  2, 10, value=4, step=1,
3788
  label="Chord Density Threshold",
 
182
  s8bit_enable_midi_preprocessing: bool = True # Master switch for this feature
183
  s8bit_high_pitch_threshold: int = 84 # Pitch (C6) above which velocity is scaled
184
  s8bit_high_pitch_velocity_scale: float = 0.8 # Velocity multiplier for high notes (e.g., 80%)
185
+ # --- Low-pitch management parameters ---
186
+ s8bit_low_pitch_threshold: int = 36 # Low pitch threshold (C2)
187
+ s8bit_low_pitch_velocity_scale: float = 0.9 # Low pitch velocity scale
188
+
189
  s8bit_chord_density_threshold: int = 4 # Min number of notes to be considered a dense chord
190
  s8bit_chord_velocity_threshold: int = 100 # Min average velocity for a chord to be tamed
191
  s8bit_chord_velocity_scale: float = 0.75 # Velocity multiplier for loud, dense chords
 
206
  s8bit_delay_division: str = "Dotted 8th Note"
207
  s8bit_delay_feedback: float = 0.5 # Velocity scale for each subsequent echo (50%)
208
  s8bit_delay_repeats: int = 3 # Number of echoes to generate
209
+ # --- NEW: Low-End Management for Delay ---
210
+ s8bit_delay_highpass_cutoff_hz: int = 100 # High-pass filter frequency for delay echoes (removes low-end rumble from echoes)
211
+ s8bit_delay_bass_pitch_shift: int = 0 # Pitch shift (in semitones) applied to low notes in delay echoes
212
+ # --- High-End Management for Delay ---
213
+ s8bit_delay_lowpass_cutoff_hz: int = 5000 # Lowpass filter frequency for delay echoes (removes harsh high frequencies from echoes)
214
+ s8bit_delay_treble_pitch_shift: int = 0 # Pitch shift (in semitones) applied to high notes in delay echoes
215
 
216
  # =================================================================================================
217
  # === Helper Functions ===
 
351
  def preprocess_midi_for_harshness(midi_data: pretty_midi.PrettyMIDI, params: AppParameters):
352
  """
353
  Analyzes and modifies a PrettyMIDI object in-place to reduce characteristics
354
+ that can cause harshness or muddiness in simple synthesizers.
355
+ Now includes both high and low pitch attenuation.
356
 
357
  Args:
358
  midi_data: The PrettyMIDI object to process.
359
  params: The AppParameters object containing the control thresholds.
360
  """
361
+ print("Running MIDI pre-processing to reduce harshness and muddiness...")
362
+ high_notes_tamed = 0
363
+ low_notes_tamed = 0
364
  chords_tamed = 0
365
+
366
+ # Rule 1 & 2: High and Low Pitch Attenuation
367
  for instrument in midi_data.instruments:
368
  for note in instrument.notes:
369
+ # Tame very high notes to reduce harshness/aliasing
370
  if note.pitch > params.s8bit_high_pitch_threshold:
 
371
  note.velocity = int(note.velocity * params.s8bit_high_pitch_velocity_scale)
372
  if note.velocity < 1: note.velocity = 1
373
+ high_notes_tamed += 1
374
+
375
+ # Tame very low notes to reduce muddiness/rumble
376
+ if note.pitch < params.s8bit_low_pitch_threshold:
377
+ note.velocity = int(note.velocity * params.s8bit_low_pitch_velocity_scale)
378
+ if note.velocity < 1: note.velocity = 1
379
+ low_notes_tamed += 1
380
+
381
+ if high_notes_tamed > 0:
382
+ print(f" - Tamed {high_notes_tamed} individual high-pitched notes.")
383
+ if low_notes_tamed > 0:
384
+ print(f" - Tamed {low_notes_tamed} individual low-pitched notes.")
385
 
386
+ # Rule 3: Chord Compression
387
  # This is a simplified approach: group notes by near-simultaneous start times
388
  all_notes = sorted([note for instrument in midi_data.instruments for note in instrument.notes], key=lambda x: x.start)
389
+
390
  time_window = 0.02 # 20ms window to group notes into a chord
391
  i = 0
392
  while i < len(all_notes):
 
680
  print(" - No notes found to apply delay to. Skipping.")
681
  return processed_midi
682
 
683
+ # --- Step 3: Generate echo notes with optional octave shift using the calculated delay time ---
684
  echo_notes = []
685
+ bass_note_threshold = 48 # MIDI note for C3
686
+ treble_note_threshold = 84 # MIDI note for C6
687
+
688
  for i in range(1, params.s8bit_delay_repeats + 1):
689
  for original_note in notes_to_echo:
690
  # Create a copy of the note for the echo
691
  echo_note = copy.copy(original_note)
692
+
693
+ # --- Octave Shift Logic for both Bass and Treble ---
694
+ if params.s8bit_delay_bass_pitch_shift and original_note.pitch < bass_note_threshold:
695
+ echo_note.pitch += params.s8bit_delay_bass_pitch_shift
696
+ elif params.s8bit_delay_treble_pitch_shift and original_note.pitch > treble_note_threshold:
697
+ echo_note.pitch += params.s8bit_delay_treble_pitch_shift
698
 
699
  # Use the tempo-synced time and velocity
700
  time_offset = i * delay_time_s
 
725
  return processed_midi
726
 
727
 
728
+ def butter_highpass(cutoff, fs, order=5):
729
+ nyq = 0.5 * fs
730
+ normal_cutoff = cutoff / nyq
731
+ b, a = signal.butter(order, normal_cutoff, btype='high', analog=False)
732
+ return b, a
733
+
734
+ def apply_butter_highpass_filter(data, cutoff, fs, order=5):
735
+ """Applies a Butterworth highpass filter to a stereo audio signal."""
736
+ if cutoff <= 0:
737
+ return data
738
+ b, a = butter_highpass(cutoff, fs, order=order)
739
+ # Apply filter to each channel independently
740
+ filtered_data = np.zeros_like(data)
741
+ for channel in range(data.shape[1]):
742
+ filtered_data[:, channel] = signal.lfilter(b, a, data[:, channel])
743
+ return filtered_data
744
+
745
+
746
+ def butter_lowpass(cutoff, fs, order=5):
747
+ nyq = 0.5 * fs
748
+ normal_cutoff = cutoff / nyq
749
+ b, a = signal.butter(order, normal_cutoff, btype='low', analog=False)
750
+ return b, a
751
+
752
+ def apply_butter_lowpass_filter(data, cutoff, fs, order=5):
753
+ """Applies a Butterworth lowpass filter to a stereo audio signal."""
754
+ # A cutoff at or above Nyquist frequency is pointless
755
+ if cutoff >= fs / 2:
756
+ return data
757
+ b, a = butter_lowpass(cutoff, fs, order=order)
758
+ filtered_data = np.zeros_like(data)
759
+ for channel in range(data.shape[1]):
760
+ filtered_data[:, channel] = signal.lfilter(b, a, data[:, channel])
761
+ return filtered_data
762
+
763
+
764
  def one_pole_lowpass(x, cutoff_hz, fs):
765
  """Simple one-pole lowpass filter (causal), stable and cheap."""
766
  if cutoff_hz <= 0 or cutoff_hz >= fs/2:
 
1897
  arpeggiated_midi = arpeggiate_midi(base_midi, params)
1898
 
1899
  # --- Step 2: Render the main (original) layer ---
1900
+ print(" - Rendering main synthesis layer (including echoes)...")
1901
  # Synthesize the waveform, passing new FX parameters to the synthesis function
1902
+ main_and_echo_waveform = synthesize_8bit_style(
1903
  midi_data=base_midi,
1904
  fs=srate,
1905
  params=params,
1906
  progress=progress
1907
  )
1908
+
1909
+ # --- Isolate and filter the echo part if it exists ---
1910
+ echo_instrument = None
1911
+ for inst in base_midi.instruments:
1912
+ if inst.name == "Echo Layer":
1913
+ echo_instrument = inst
1914
+ break
1915
 
1916
+ # --- Step 3: Render the delay layers (if enabled) ---
1917
+ if echo_instrument:
1918
+ print(" - Processing echo layer audio effects...")
1919
+ # Create a temporary MIDI object with ONLY the echo instrument
1920
+ echo_only_midi = pretty_midi.PrettyMIDI()
1921
+ echo_only_midi.instruments.append(echo_instrument)
1922
+
1923
+ # Render ONLY the echo layer to an audio waveform
1924
+ echo_waveform_raw = synthesize_8bit_style(midi_data=echo_only_midi, fs=srate, params=params)
1925
+
1926
+ # --- Start of the Robust Filtering Block ---
1927
+ # Apply both High-Pass and Low-Pass filters
1928
+ unfiltered_echo = echo_waveform_raw
1929
+ filtered_echo = echo_waveform_raw
1930
+
1931
+ # --- Apply Filters if requested ---
1932
+ # Convert to a format filter function expects (samples, channels)
1933
+ # This is inefficient, we should only do it once.
1934
+ # Let's assume the filter functions are adapted to take (channels, samples)
1935
+ # For now, we'll keep the transpose for simplicity.
1936
+ # We will apply filters on a temporary copy to avoid chaining issues.
1937
+ temp_filtered_echo = echo_waveform_raw.T
1938
+
1939
+ should_filter = False
1940
+ # Apply High-Pass Filter
1941
+ if params.s8bit_delay_highpass_cutoff_hz > 0:
1942
+ print(f" - Applying high-pass filter at {params.s8bit_delay_highpass_cutoff_hz} Hz...")
1943
+ temp_filtered_echo = apply_butter_highpass_filter(temp_filtered_echo, params.s8bit_delay_highpass_cutoff_hz, srate)
1944
+ should_filter = True
1945
+
1946
+ # Apply Low-Pass Filter
1947
+ if params.s8bit_delay_lowpass_cutoff_hz < srate / 2:
1948
+ print(f" - Applying low-pass filter at {params.s8bit_delay_lowpass_cutoff_hz} Hz...")
1949
+ temp_filtered_echo = apply_butter_lowpass_filter(temp_filtered_echo, params.s8bit_delay_lowpass_cutoff_hz, srate)
1950
+ should_filter = True
1951
+
1952
+ # Convert back and get the difference
1953
+ if should_filter:
1954
+ filtered_echo = temp_filtered_echo.T
1955
+
1956
+ # To avoid re-rendering, we subtract the unfiltered echo and add the filtered one
1957
+ # Ensure all waveforms have the same length before math ---
1958
+ target_length = main_and_echo_waveform.shape[1]
1959
+
1960
+ # Pad the unfiltered echo if it's shorter
1961
+ len_unfiltered = unfiltered_echo.shape[1]
1962
+ if len_unfiltered < target_length:
1963
+ unfiltered_echo = np.pad(unfiltered_echo, ((0, 0), (0, target_length - len_unfiltered)))
1964
+
1965
+ # Pad the filtered echo if it's shorter
1966
+ len_filtered = filtered_echo.shape[1]
1967
+ if len_filtered < target_length:
1968
+ filtered_echo = np.pad(filtered_echo, ((0, 0), (0, target_length - len_filtered)))
1969
+
1970
+ # Now that all shapes are guaranteed to be identical, perform the operation.
1971
+ main_and_echo_waveform -= unfiltered_echo[:, :target_length]
1972
+ main_and_echo_waveform += filtered_echo[:, :target_length]
1973
+
1974
+ final_waveform = main_and_echo_waveform
1975
 
1976
+ # --- Step 4: Render the arpeggiator layer (if enabled) ---
1977
  if arpeggiated_midi and arpeggiated_midi.instruments:
1978
  print(" - Rendering and mixing arpeggiator layer...")
1979
  # Temporarily override panning for the arpeggiator synth call
 
3894
  label="Number of Repeats",
3895
  info="The total number of echoes to generate for each note."
3896
  )
3897
+ # --- UI controls for low-end management ---
3898
+ s8bit_delay_highpass_cutoff_hz = gr.Slider(
3899
+ 0, 500, value=100, step=10,
3900
+ label="Echo High-Pass Filter (Hz)",
3901
+ info="Filters out low frequencies from the echoes to prevent muddiness. Set to 0 to disable. 80-120Hz is a good range to clean up bass."
3902
+ )
3903
+ s8bit_delay_bass_pitch_shift = gr.Slider(
3904
+ -12, 24, value=12, step=1,
3905
+ label="Echo Pitch Shift for Low Notes (Semitones)",
3906
+ info="Shifts the pitch of echoes for very low notes (below C3). +12 is one octave up, +7 is a perfect fifth. 0 to disable."
3907
+ )
3908
+ # --- UI controls for high-end management ---
3909
+ s8bit_delay_lowpass_cutoff_hz = gr.Slider(
3910
+ 1000, 20000, value=5000, step=500,
3911
+ label="Echo Low-Pass Filter (Hz)",
3912
+ info="Filters out high frequencies from the echoes to reduce harshness. Set to 20000 to disable. 4k-8kHz is a good range to make echoes sound 'darker'."
3913
+ )
3914
+ s8bit_delay_treble_pitch_shift = gr.Slider(
3915
+ -24, 12, value=-12, step=1,
3916
+ label="Echo Pitch Shift for High Notes (Semitones)",
3917
+ info="Shifts the pitch of echoes for very high notes (above C6). -12 is one octave down. 0 to disable."
3918
+ )
3919
 
3920
  # --- Section 2: MIDI Pre-processing (Corrective Tool) ---
3921
  with gr.Accordion("MIDI Pre-processing (Corrective Tool)", open=False):
 
3935
  label="High Pitch Velocity Scale",
3936
  info="Multiplier for high notes' velocity (e.g., 0.8 = 80% of original velocity)."
3937
  )
3938
+ # --- UI controls for low-pitch management ---
3939
+ s8bit_low_pitch_threshold = gr.Slider(
3940
+ 21, 60, value=36, step=1,
3941
+ label="Low Pitch Threshold (MIDI Note)",
3942
+ info="Notes below this pitch will have their velocity reduced to prevent muddiness. 36 = C2."
3943
+ )
3944
+ s8bit_low_pitch_velocity_scale = gr.Slider(
3945
+ 0.1, 1.0, value=0.9, step=0.05,
3946
+ label="Low Pitch Velocity Scale",
3947
+ info="Multiplier for low notes' velocity. Use this to gently tame excessive sub-bass."
3948
+ )
3949
  s8bit_chord_density_threshold = gr.Slider(
3950
  2, 10, value=4, step=1,
3951
  label="Chord Density Threshold",