avans06/Audio-To-MIDI-And-Advanced-Renderer
37
1# =================================================================2#3# Merged and Integrated Script for Audio/MIDI Processing and Rendering (Stereo Enhanced)4#5# This script combines two functionalities:6# 1. Transcribing audio to MIDI using two methods:7# a) A general-purpose model (basic-pitch by Spotify).8# b) A model specialized for solo piano (ByteDance).9# - Includes stereo processing by splitting channels, transcribing independently, and merging MIDI.10# 2. Applying advanced transformations and re-rendering MIDI files using:11# a) Standard SoundFonts via FluidSynth (produces stereo audio).12# b) A custom 8-bit style synthesizer for a chiptune sound (updated for stereo output).13#14# The user can upload a Audio (e.g., WAV, MP3), or MIDI file.15# - If an audio file is uploaded, it is first transcribed to MIDI using the selected method.16# - The resulting MIDI (or an uploaded MIDI) can then be processed17# with various effects and rendered into audio.18#19#================================================================20# Original sources:21# https://huggingface.co/spaces/asigalov61/ByteDance-Solo-Piano-Audio-to-MIDI-Transcription22# https://huggingface.co/spaces/asigalov61/Advanced-MIDI-Renderer23#================================================================24# Packages:25#26# sudo apt install fluidsynth27#28# =================================================================29# Requirements:30#31# pip install gradio torch pytz numpy scipy matplotlib networkx scikit-learn32# pip install piano_transcription_inference huggingface_hub33# pip install basic-pitch pretty_midi librosa soundfile34#35# =================================================================36# Core modules:37#38# git clone --depth 1 https://github.com/asigalov61/tegridy-tools39#40# =================================================================41 42import io43import os44import hashlib45import time as reqtime46import copy47import random48import shutil49import librosa50import pyloudnorm as pyln51import soundfile as sf52from mutagen.flac import FLAC53 54import torch55import ffmpeg56import gradio as gr57from dataclasses import dataclass, fields # ADDED for the parameter object58 59# --- Imports for Vocal Separation ---60import torchaudio61from demucs.apply import apply_model62from demucs.pretrained import get_model63from demucs.audio import convert_audio64from audio_separator.separator import Separator65 66from src.piano_transcription.utils import initialize_app67from piano_transcription_inference import PianoTranscription, utilities, sample_rate as transcription_sample_rate68 69# --- Import core transcription and MIDI processing libraries ---70from src import TMIDIX, TPLOTS71from src import MIDI72from src.midi_to_colab_audio import midi_to_colab_audio73 74# --- Imports for General Purpose Transcription (basic-pitch) ---75import basic_pitch76from basic_pitch.inference import predict77from basic_pitch import ICASSP_2022_MODEL_PATH78 79# --- Imports for 8-bit Synthesizer & MIDI Merging ---80import pretty_midi81import numpy as np82from scipy import signal, stats83 84# =================================================================================================85# === Hugging Face SoundFont Downloader ===86# =================================================================================================87from huggingface_hub import hf_hub_download88import glob89 90# --- Define a constant for the 8-bit synthesizer option ---91SYNTH_8_BIT_LABEL = "None (8-bit Synthesizer)"92 93 94# =================================================================================================95# === Central Parameter Object ===96# =================================================================================================97 98@dataclass99class AppParameters:100 """A dataclass to hold all configurable parameters for the application."""101 # This provides type safety and autocomplete, preventing typos from string keys.102 103 # Input files (not part of the settings panel)104 input_file: str = None105 batch_input_files: list = None106 107 # Global Settings108 s8bit_preset_selector: str = "Custom"109 separate_vocals: bool = False110 separation_model: str = "Demucs (4-stem)"111 112 # --- Advanced Separation and Merging Controls ---113 enable_advanced_separation: bool = False # Controls visibility of advanced options114 separate_drums: bool = True115 separate_bass: bool = True116 separate_other: bool = True117 118 transcribe_vocals: bool = False119 transcribe_drums: bool = False120 transcribe_bass: bool = False121 transcribe_other_or_accompaniment: bool = True # Default to transcribe 'other' as it's most common122 123 merge_vocals_to_render: bool = False124 merge_drums_to_render: bool = False125 merge_bass_to_render: bool = False126 merge_other_or_accompaniment: bool = False127 128 enable_stereo_processing: bool = False129 transcription_method: str = "General Purpose"130 basic_pitch_preset_selector: str = "Default (Balanced)"131 132 # Basic Pitch Settings133 onset_threshold: float = 0.5134 frame_threshold: float = 0.3135 minimum_note_length: int = 128136 minimum_frequency: float = 60.0137 maximum_frequency: float = 4000.0138 infer_onsets: bool = True139 melodia_trick: bool = True140 multiple_pitch_bends: bool = False141 142 # Render Settings143 render_type: str = "Render as-is"144 soundfont_bank: str = "None (8-bit Synthesizer)"145 render_sample_rate: str = "44100"146 render_with_sustains: bool = True147 merge_misaligned_notes: int = -1148 custom_render_patch: int = -1149 render_align: str = "Do not align"150 render_transpose_value: int = 0151 render_transpose_to_C4: bool = False152 render_output_as_solo_piano: bool = False153 render_remove_drums: bool = False154 155 # EXPERIMENTAL: MIDI Post-Processing & Correction Tools156 enable_midi_corrections: bool = False # Master switch for enabling MIDI correction tools157 correction_filter_spurious_notes: bool = True # Enable filtering of spurious (noise) notes158 correction_spurious_duration_ms: int = 50 # Maximum duration (ms) for a note to be considered spurious159 correction_spurious_velocity: int = 20 # Maximum velocity for a note to be considered spurious160 correction_remove_abnormal_rhythm: bool = False # Enable rhythm stabilization for abnormal rhythm161 correction_rhythm_stab_by_segment: bool = False # Enable segmentation by silence before rhythm stabilization162 correction_rhythm_stab_segment_silence_s: float = 1.0 # Silence threshold (seconds) for segmenting MIDI163 correction_quantize_level: str = "None" # Quantization level for note timing (e.g., "1/16", "None")164 correction_velocity_mode: str = "None" # Velocity processing mode ("None", "Smooth", "Compress")165 correction_velocity_smooth_factor: float = 0.5 # Smoothing factor for velocity processing166 correction_velocity_compress_min: int = 30 # Minimum velocity after compression167 correction_velocity_compress_max: int = 100 # Maximum velocity after compression168 correction_rhythmic_simplification_level: str = "None" # rhythmic simplification169 170 # 8-bit Synthesizer Settings171 s8bit_waveform_type: str = 'Square'172 s8bit_pulse_width: float = 0.5173 s8bit_envelope_type: str = 'Plucky (AD Envelope)'174 s8bit_decay_time_s: float = 0.1175 s8bit_vibrato_rate: float = 5.0176 s8bit_vibrato_depth: float = 0.0177 s8bit_bass_boost_level: float = 0.0178 s8bit_smooth_notes_level: float = 0.0179 s8bit_continuous_vibrato_level: float = 0.0180 s8bit_noise_level: float = 0.0181 s8bit_distortion_level: float = 0.0182 s8bit_fm_modulation_depth: float = 0.0183 s8bit_fm_modulation_rate: float = 0.0184 s8bit_adaptive_decay: bool = False185 s8bit_echo_sustain: bool = False186 s8bit_echo_rate_hz: float = 5.0187 s8bit_echo_decay_factor: float = 0.6188 s8bit_echo_trigger_threshold: float = 2.5189 190 # --- Anti-Aliasing & Quality Parameters ---191 s8bit_enable_anti_aliasing: bool = True # Main toggle for all new quality features192 s8bit_use_additive_synthesis: bool = False # High-quality but CPU-intensive waveform generation193 s8bit_edge_smoothing_ms: float = 0.5 # Mild smoothing for standard waveforms (0 to disable)194 s8bit_noise_lowpass_hz: float = 9000.0 # Lowpass filter frequency for noise195 s8bit_harmonic_lowpass_factor: float = 12.0 # Multiplier for frequency-dependent lowpass filter196 s8bit_final_gain: float = 0.8 # Final gain/limiter level to prevent clipping197 s8bit_bass_boost_cutoff_hz: float = 200.0 # Parameter for Intelligent Bass Boost198 199 # --- MIDI Pre-processing to Reduce Harshness ---200 s8bit_enable_midi_preprocessing: bool = True # Master switch for this feature201 s8bit_high_pitch_threshold: int = 84 # Pitch (C6) above which velocity is scaled202 s8bit_high_pitch_velocity_scale: float = 0.8 # Velocity multiplier for high notes (e.g., 80%)203 # --- Low-pitch management parameters ---204 s8bit_low_pitch_threshold: int = 36 # Low pitch threshold (C2)205 s8bit_low_pitch_velocity_scale: float = 0.9 # Low pitch velocity scale206 207 s8bit_chord_density_threshold: int = 4 # Min number of notes to be considered a dense chord208 s8bit_chord_velocity_threshold: int = 100 # Min average velocity for a chord to be tamed209 s8bit_chord_velocity_scale: float = 0.75 # Velocity multiplier for loud, dense chords210 211 # --- Arpeggiator Parameters ---212 s8bit_enable_arpeggiator: bool = False # Master switch for the arpeggiator213 s8bit_arpeggio_target: str = "Accompaniment Only" # Target selection for the arpeggiator214 s8bit_arpeggio_velocity_scale: float = 0.7 # Velocity multiplier for arpeggiated notes (0.0 to 1.0)215 s8bit_arpeggio_density: float = 0.5 # Density factor for rhythmic patterns (0.0 to 1.0)216 s8bit_arpeggio_rhythm: str = "Classic Upbeat (8th)" # Rhythmic pattern for arpeggiation217 s8bit_arpeggio_pattern: str = "Up" # Pattern of the arpeggio (e.g., Up, Down, UpDown)218 s8bit_arpeggio_octave_range: int = 1 # How many octaves the pattern spans219 s8bit_arpeggio_panning: str = "Stereo" # Panning mode for arpeggiated notes (Stereo, Left, Right, Center)220 221 # --- MIDI Delay/Echo Effect Parameters ---222 s8bit_enable_delay: bool = False # Master switch for the delay effect223 s8bit_delay_on_melody_only: bool = True # Apply delay only to the lead melody224 s8bit_delay_division: str = "Dotted 8th Note"225 s8bit_delay_feedback: float = 0.5 # Velocity scale for each subsequent echo (50%)226 s8bit_delay_repeats: int = 3 # Number of echoes to generate227 # --- Low-End Management for Delay ---228 s8bit_delay_highpass_cutoff_hz: int = 100 # High-pass filter frequency for delay echoes (removes low-end rumble from echoes)229 s8bit_delay_bass_pitch_shift: int = 0 # Pitch shift (in semitones) applied to low notes in delay echoes230 # --- High-End Management for Delay ---231 s8bit_delay_lowpass_cutoff_hz: int = 5000 # Lowpass filter frequency for delay echoes (removes harsh high frequencies from echoes)232 s8bit_delay_treble_pitch_shift: int = 0 # Pitch shift (in semitones) applied to high notes in delay echoes233 234 235# ===============================================================================236# === MIDI CORRECTION SUITE (Operating on pretty_midi objects for robustness) ===237# ===============================================================================238 239def _get_all_notes(midi_obj: pretty_midi.PrettyMIDI, include_drums=False):240 """Helper to get a single sorted list of all notes from all instruments."""241 all_notes = []242 for instrument in midi_obj.instruments:243 if not instrument.is_drum or include_drums:244 all_notes.extend(instrument.notes)245 all_notes.sort(key=lambda x: x.start)246 return all_notes247 248 249def _normalize_instrument_times(instrument: pretty_midi.Instrument):250 """Creates a temporary, normalized version of an instrument where timestamps start from 0."""251 if not instrument.notes:252 return instrument253 254 # Sort notes by start time to reliably get the first note255 notes = sorted(instrument.notes, key=lambda x: x.start)256 start_offset = notes[0].start257 258 normalized_instrument = copy.deepcopy(instrument)259 for note in normalized_instrument.notes:260 note.start -= start_offset261 note.end -= start_offset262 return normalized_instrument263 264def _segment_midi_by_silence(midi_obj: pretty_midi.PrettyMIDI, silence_threshold_s=1.0):265 """266 Splits a PrettyMIDI object into a list of PrettyMIDI objects, each representing a segment.267 This is the core of per-song processing for albums.268 """269 all_notes = _get_all_notes(midi_obj, include_drums=True)270 if not all_notes:271 return []272 273 segments = []274 current_segment_notes = {i: [] for i in range(len(midi_obj.instruments))}275 276 # Add the very first note to the first segment277 for i, inst in enumerate(midi_obj.instruments):278 for note in inst.notes:279 if note == all_notes[0]:280 current_segment_notes[i].append(note)281 break282 283 for i in range(1, len(all_notes)):284 prev_note_end = all_notes[i-1].end285 current_note_start = all_notes[i].start286 gap = current_note_start - prev_note_end287 288 if gap > silence_threshold_s:289 # End of a segment, create a new MIDI object for it290 segment_midi = pretty_midi.PrettyMIDI()291 for inst_idx, inst_notes in current_segment_notes.items():292 if inst_notes:293 new_inst = pretty_midi.Instrument(program=midi_obj.instruments[inst_idx].program, is_drum=midi_obj.instruments[inst_idx].is_drum)294 new_inst.notes.extend(inst_notes)295 segment_midi.instruments.append(new_inst)296 if segment_midi.instruments:297 segments.append(segment_midi)298 # Start a new segment299 current_segment_notes = {i: [] for i in range(len(midi_obj.instruments))}300 301 # Find which instrument this note belongs to and add it302 for inst_idx, inst in enumerate(midi_obj.instruments):303 if all_notes[i] in inst.notes:304 current_segment_notes[inst_idx].append(all_notes[i])305 break306 307 # Add the final segment308 final_segment_midi = pretty_midi.PrettyMIDI()309 for inst_idx, inst_notes in current_segment_notes.items():310 if inst_notes:311 new_inst = pretty_midi.Instrument(program=midi_obj.instruments[inst_idx].program, is_drum=midi_obj.instruments[inst_idx].is_drum)312 new_inst.notes.extend(inst_notes)313 final_segment_midi.instruments.append(new_inst)314 if final_segment_midi.instruments:315 segments.append(final_segment_midi)316 317 return segments318 319def _recombine_segments(segments):320 """Merges a list of segmented PrettyMIDI objects back into one."""321 recombined_midi = pretty_midi.PrettyMIDI()322 # Create instrument tracks in the final MIDI object323 if segments:324 template_midi = segments[0]325 for i, inst in enumerate(template_midi.instruments):326 recombined_midi.instruments.append(pretty_midi.Instrument(program=inst.program, is_drum=inst.is_drum))327 328 # Populate the tracks with notes from all segments329 for segment in segments:330 for i, inst in enumerate(segment.instruments):331 # This assumes instrument order is consistent, which our segmentation function ensures332 recombined_midi.instruments[i].notes.extend(inst.notes)333 334 return recombined_midi335 336def _analyze_best_quantize_level(notes, bpm, error_threshold_ratio=0.25):337 """Analyzes a list of notes to determine the most likely quantization grid."""338 if not notes: return "None"339 grids_to_test = ["1/8", "1/12", "1/16", "1/24", "1/32"]340 level_map = {"1/8": 2.0, "1/12": 3.0, "1/16": 4.0, "1/24": 6.0, "1/32": 8.0}341 start_times = [n.start for n in notes]342 results = []343 for grid_name in grids_to_test:344 division = level_map[grid_name]345 grid_s = (60.0 / bpm) / division346 if grid_s < 0.001: continue347 total_error = sum(min(t % grid_s, grid_s - (t % grid_s)) for t in start_times)348 avg_error = total_error / len(start_times)349 results.append({"grid": grid_name, "avg_error": avg_error, "grid_s": grid_s})350 if not results: return "None"351 best_fit = min(results, key=lambda x: x['avg_error'])352 if best_fit['avg_error'] > best_fit['grid_s'] * error_threshold_ratio:353 return "None"354 return best_fit['grid']355 356def filter_spurious_notes_pm(midi_obj: pretty_midi.PrettyMIDI, max_dur_s=0.05, max_vel=20):357 """Filters out very short and quiet notes from a PrettyMIDI object."""358 print(f" - Filtering spurious notes (duration < {max_dur_s*1000:.0f}ms AND velocity < {max_vel})...")359 notes_removed = 0360 for instrument in midi_obj.instruments:361 original_note_count = len(instrument.notes)362 instrument.notes = [363 note for note in instrument.notes364 if not (note.end - note.start < max_dur_s and note.velocity < max_vel)365 ]366 notes_removed += original_note_count - len(instrument.notes)367 368 print(f" - Removed {notes_removed} spurious notes.")369 return midi_obj370 371def stabilize_rhythm_pm(372 midi_obj: pretty_midi.PrettyMIDI,373 ioi_threshold_ratio=0.30,374 min_ioi_s=0.03,375 enable_segmentation=True,376 silence_threshold_s=1.0,377 merge_mode="extend", # "extend" or "drop"378 consider_velocity=True, # consider low velocity notes as decorations379 skip_chords=True, # skip merging if multiple notes start at same time380 use_mode_ioi=False # use mode of IOI instead of median381):382 """Enhances rhythm stability by merging rhythmically unstable notes, with advanced options."""383 print(" - Stabilizing rhythm...")384 if not enable_segmentation:385 segments = [midi_obj]386 else:387 segments = _segment_midi_by_silence(midi_obj, silence_threshold_s)388 if len(segments) > 1:389 print(f" - Split into {len(segments)} segments for stabilization.")390 391 processed_segments = []392 393 for segment in segments:394 for instrument in segment.instruments:395 if instrument.is_drum or len(instrument.notes) < 20:396 continue397 398 notes = sorted(instrument.notes, key=lambda n: n.start)399 400 # Compute inter-onset intervals (IOIs)401 iois = [notes[i].start - notes[i-1].start for i in range(1, len(notes))]402 positive_iois = [ioi for ioi in iois if ioi > 0.001]403 if not positive_iois:404 continue405 406 # Determine threshold based on median or mode407 if use_mode_ioi:408 try:409 median_ioi = float(stats.mode(positive_iois).mode[0])410 except Exception:411 median_ioi = np.median(positive_iois)412 else:413 median_ioi = np.median(positive_iois)414 threshold_s = max(median_ioi * ioi_threshold_ratio, min_ioi_s)415 416 cleaned_notes = [notes[0]]417 for i in range(1, len(notes)):418 prev_note = cleaned_notes[-1]419 curr_note = notes[i]420 421 # Skip merging if chord and option enabled422 if skip_chords:423 notes_at_same_time = [n for n in notes if abs(n.start - curr_note.start) < 0.001]424 if len(notes_at_same_time) > 1:425 cleaned_notes.append(curr_note)426 continue427 428 # Check if note is considered "unstable/decoration"429 pitch_close = abs(curr_note.pitch - prev_note.pitch) <= 3 # within minor third430 velocity_ok = True431 if consider_velocity:432 velocity_ok = curr_note.velocity < prev_note.velocity * 0.8433 434 start_close = (curr_note.start - prev_note.start) < threshold_s435 436 if start_close and pitch_close and velocity_ok:437 if merge_mode == "extend":438 # Merge by extending previous note's end439 prev_note.end = max(prev_note.end, curr_note.end)440 elif merge_mode == "drop":441 # Drop the current note442 continue443 else:444 cleaned_notes.append(curr_note)445 446 instrument.notes = cleaned_notes447 processed_segments.append(segment)448 449 return _recombine_segments(processed_segments) if enable_segmentation else processed_segments[0]450 451 452def simplify_rhythm_pm(453 midi_obj: pretty_midi.PrettyMIDI,454 simplification_level_str="None",455 enable_segmentation=True,456 silence_threshold_s=1.0,457 keep_chords=True,458 max_notes_per_grid=3459):460 """Simplifies rhythm while preserving music length, with optional chord and sustain handling."""461 if simplification_level_str == "None":462 return midi_obj463 print(f" - Simplifying rhythm to {simplification_level_str} grid...")464 465 # Split into segments if enabled466 if not enable_segmentation:467 segments = [midi_obj]468 else:469 segments = _segment_midi_by_silence(midi_obj, silence_threshold_s)470 if len(segments) > 1:471 print(f" - Split into {len(segments)} segments for simplification.")472 473 processed_segments = []474 level_map = {"1/4": 1.0, "1/8": 2.0, "1/12": 3.0, "1/16": 4.0, "1/24": 6.0, "1/32": 8.0, "1/64": 16.0}475 division = level_map.get(simplification_level_str)476 if not division:477 return midi_obj478 479 for segment in segments:480 new_segment_midi = pretty_midi.PrettyMIDI()481 for instrument in segment.instruments:482 if instrument.is_drum or not instrument.notes:483 new_segment_midi.instruments.append(instrument)484 continue485 486 try:487 # Prefer using tempo changes from MIDI if available488 if segment.get_tempo_changes()[1].size > 0:489 bpm = float(segment.get_tempo_changes()[1][0])490 else:491 temp_norm_inst = _normalize_instrument_times(instrument)492 temp_midi = pretty_midi.PrettyMIDI(); temp_midi.instruments.append(temp_norm_inst)493 bpm = temp_midi.estimate_tempo()494 bpm = max(40.0, min(bpm, 240.0))495 except Exception:496 new_segment_midi.instruments.append(instrument)497 continue498 499 grid_s = (60.0 / bpm) / division500 if grid_s <= 0.001:501 new_segment_midi.instruments.append(instrument)502 continue503 504 simplified_instrument = pretty_midi.Instrument(program=instrument.program, name=instrument.name)505 notes = sorted(instrument.notes, key=lambda x: x.start)506 end_time = segment.get_end_time()507 508 # Handle sustain pedal CC64 events509 sustain_times = []510 for cc in instrument.control_changes:511 if cc.number == 64: # sustain pedal512 sustain_times.append((cc.time, cc.value >= 64))513 514 # Grid iteration515 current_grid_time = round(notes[0].start / grid_s) * grid_s516 while current_grid_time < end_time:517 notes_in_slot = [n for n in notes if current_grid_time <= n.start < current_grid_time + grid_s]518 if notes_in_slot:519 chosen_notes = []520 if keep_chords:521 # Always keep root (lowest pitch) and top note (highest pitch)522 root_note = min(notes_in_slot, key=lambda n: n.pitch)523 top_note = max(notes_in_slot, key=lambda n: n.pitch)524 chosen_notes.extend([root_note, top_note])525 # Also keep the strongest note (highest velocity)526 strong_note = max(notes_in_slot, key=lambda n: n.velocity)527 if strong_note not in chosen_notes:528 chosen_notes.append(strong_note)529 # Limit chord density530 chosen_notes = sorted(set(chosen_notes), key=lambda n: n.pitch)[:max_notes_per_grid]531 else:532 chosen_notes = [max(notes_in_slot, key=lambda n: n.velocity)]533 534 for note in chosen_notes:535 # End is either original note end or grid boundary536 note_end = min(note.end, current_grid_time + grid_s)537 # Extend if sustain pedal is active538 for t, active in sustain_times:539 if t >= note.start and active:540 note_end = max(note_end, current_grid_time + grid_s * 2)541 simplified_instrument.notes.append(pretty_midi.Note(542 velocity=note.velocity,543 pitch=note.pitch,544 start=current_grid_time,545 end=note_end546 ))547 current_grid_time += grid_s548 549 if simplified_instrument.notes:550 new_segment_midi.instruments.append(simplified_instrument)551 processed_segments.append(new_segment_midi)552 553 return _recombine_segments(processed_segments) if enable_segmentation else processed_segments[0]554 555 556def quantize_pm(557 midi_obj: pretty_midi.PrettyMIDI,558 quantize_level_str="None",559 enable_segmentation=True,560 silence_threshold_s=1.0,561 quantize_end=True,562 preserve_duration=True563):564 """Quantizes notes in a PrettyMIDI object with optional end-time adjustment, sustain handling, and segmentation support."""565 if quantize_level_str == "None":566 return midi_obj567 print(f" - Quantizing notes (Mode: {quantize_level_str})...")568 569 # Split into segments if enabled570 if not enable_segmentation:571 segments = [midi_obj]572 else:573 segments = _segment_midi_by_silence(midi_obj, silence_threshold_s)574 if len(segments) > 1:575 print(f" - Split into {len(segments)} segments for quantization.")576 577 processed_segments = []578 level_map = {"1/4": 1.0, "1/8": 2.0, "1/12": 3.0, "1/16": 4.0, "1/24": 6.0, "1/32": 8.0, "1/64": 16.0}579 580 for i, segment in enumerate(segments):581 new_segment_midi = pretty_midi.PrettyMIDI()582 for instrument in segment.instruments:583 if instrument.is_drum or not instrument.notes:584 new_segment_midi.instruments.append(instrument)585 continue586 try:587 # Estimate BPM or use first tempo change588 if segment.get_tempo_changes()[1].size > 0:589 bpm = float(segment.get_tempo_changes()[1][0])590 else:591 temp_norm_inst = _normalize_instrument_times(instrument)592 temp_midi = pretty_midi.PrettyMIDI(); temp_midi.instruments.append(temp_norm_inst)593 bpm = temp_midi.estimate_tempo()594 bpm = max(40.0, min(bpm, 240.0))595 except Exception:596 new_segment_midi.instruments.append(instrument)597 continue598 599 # Determine quantization grid size600 final_quantize_level = quantize_level_str601 if quantize_level_str == "Auto-Analyze Rhythm":602 final_quantize_level = _analyze_best_quantize_level(instrument.notes, bpm)603 if len(segments) > 1:604 print(f" - Segment {i+1}, Inst '{instrument.name}': Auto-analyzed grid is '{final_quantize_level}'. BPM: {bpm:.2f}")605 606 division = level_map.get(final_quantize_level)607 if not division:608 new_segment_midi.instruments.append(instrument)609 continue610 grid_s = (60.0 / bpm) / division611 612 # Handle sustain pedal CC64613 sustain_times = []614 for cc in instrument.control_changes:615 if cc.number == 64: # sustain pedal616 sustain_times.append((cc.time, cc.value >= 64))617 618 # Quantize notes619 quantized_instrument = pretty_midi.Instrument(program=instrument.program, name=instrument.name)620 for note in instrument.notes:621 original_duration = note.end - note.start622 # Quantize start623 new_start = round(note.start / grid_s) * grid_s624 if preserve_duration:625 new_end = new_start + original_duration626 elif quantize_end:627 new_end = round(note.end / grid_s) * grid_s628 else:629 new_end = note.end630 631 # Sustain pedal extension632 for t, active in sustain_times:633 if t >= note.start and active:634 new_end = max(new_end, new_start + grid_s * 2)635 636 # Safety check637 if new_end <= new_start:638 new_end = new_start + grid_s * 0.5639 640 quantized_instrument.notes.append(pretty_midi.Note(641 velocity=note.velocity,642 pitch=note.pitch,643 start=new_start,644 end=new_end645 ))646 647 new_segment_midi.instruments.append(quantized_instrument)648 processed_segments.append(new_segment_midi)649 650 return _recombine_segments(processed_segments) if enable_segmentation else processed_segments[0]651 652 653def process_velocity_pm(654 midi_obj: pretty_midi.PrettyMIDI,655 mode=["None"], # list of modes: "Smooth", "Compress"656 smooth_factor=0.5, # weight for smoothing657 compress_min=30,658 compress_max=100,659 compress_type="linear", # "linear" or "perceptual"660 inplace=True # if False, return a copy661):662 """Applies velocity processing to a PrettyMIDI object with smoothing and/or compression."""663 if not inplace:664 import copy665 midi_obj = copy.deepcopy(midi_obj)666 667 if isinstance(mode, str):668 mode = [mode]669 if "None" in mode or not mode:670 return midi_obj671 672 print(f" - Processing velocities (Mode: {mode})...")673 674 for instrument in midi_obj.instruments:675 if instrument.is_drum or not instrument.notes:676 continue677 678 velocities = [n.velocity for n in instrument.notes]679 680 # Smooth velocity681 if "Smooth" in mode:682 new_velocities = list(velocities)683 n_notes = len(velocities)684 for i in range(n_notes):685 if i == 0:686 neighbor_avg = velocities[i+1]687 elif i == n_notes - 1:688 neighbor_avg = velocities[i-1]689 else:690 neighbor_avg = (velocities[i-1] + velocities[i+1]) / 2.0691 smoothed_vel = velocities[i] * (1 - smooth_factor) + neighbor_avg * smooth_factor692 new_velocities[i] = int(max(1, min(127, smoothed_vel)))693 for i, note in enumerate(instrument.notes):694 note.velocity = new_velocities[i]695 696 # Compress velocity697 if "Compress" in mode:698 velocities = [n.velocity for n in instrument.notes] # updated if smoothed first699 min_vel, max_vel = min(velocities), max(velocities)700 if max_vel == min_vel:701 continue702 703 for note in instrument.notes:704 if compress_type == "linear":705 new_vel = compress_min + (note.velocity - min_vel) * (compress_max - compress_min) / (max_vel - min_vel)706 elif compress_type == "perceptual":707 # Simple gamma-style perceptual compression708 norm = (note.velocity - min_vel) / (max_vel - min_vel)709 gamma = 0.6 # perceptual curve710 new_vel = compress_min + ((norm ** gamma) * (compress_max - compress_min))711 else:712 new_vel = note.velocity713 note.velocity = int(max(1, min(127, new_vel)))714 715 return midi_obj716 717 718 719# =================================================================================================720# === Helper Functions ===721# =================================================================================================722 723def analyze_audio_for_adaptive_params(audio_data: np.ndarray, sample_rate: int):724 """725 Analyzes raw audio data to dynamically determine optimal parameters for basic-pitch.726 727 Args:728 audio_data: The audio signal as a NumPy array (can be stereo).729 sample_rate: The sample rate of the audio.730 731 Returns:732 A dictionary of recommended parameters for basic_pitch.733 """734 print(" - Running adaptive analysis on audio to determine optimal transcription parameters...")735 736 # Ensure audio is mono for most feature extractions737 if audio_data.ndim > 1:738 y_mono = librosa.to_mono(audio_data)739 else:740 y_mono = audio_data741 742 params = {}743 744 # 1. Tempo detection with enhanced stability745 try:746 tempo_info = librosa.beat.tempo(y=y_mono, sr=sample_rate, aggregate=np.median)747 748 # Ensure BPM is a scalar float749 bpm = float(np.median(tempo_info))750 751 if bpm <= 0 or np.isnan(bpm):752 raise ValueError("Invalid BPM detected")753 754 # A 64th note is a reasonable shortest note length for most music755 # Duration of a beat (quarter note) in seconds = 60 / BPM756 # Duration of a 64th note = (60 / BPM) / 16757 min_len_s = (60.0 / bpm) / 16.0758 # basic-pitch expects milliseconds759 params['minimum_note_length'] = max(20, int(min_len_s * 1000))760 print(f" - Detected BPM (median): {bpm:.1f} -> minimum_note_length: {params['minimum_note_length']}ms")761 except Exception as e:762 print(f" - BPM detection failed, using default minimum_note_length. Error: {e}")763 764 # 2. Spectral analysis: centroid + rolloff for richer info765 try:766 spectral_centroid = librosa.feature.spectral_centroid(y=y_mono, sr=sample_rate)[0]767 rolloff = librosa.feature.spectral_rolloff(y=y_mono, sr=sample_rate)[0]768 avg_centroid = np.mean(spectral_centroid)769 avg_rolloff = np.mean(rolloff)770 print(f" - Spectral centroid: {avg_centroid:.1f} Hz, rolloff (85%): {avg_rolloff:.1f} Hz")771 # Simple logic: if the 'center of mass' of the spectrum is low, it's bass-heavy.772 # If it's high, it contains high-frequency content.773 if avg_centroid < 500 and avg_rolloff < 1500:774 params['minimum_frequency'] = 30775 params['maximum_frequency'] = 1200776 elif avg_centroid > 2000 or avg_rolloff > 5000: # Likely bright, high-frequency content (cymbals, flutes)777 params['minimum_frequency'] = 100778 params['maximum_frequency'] = 8000779 else:780 params['minimum_frequency'] = 50781 params['maximum_frequency'] = 4000782 except Exception as e:783 print(f" - Spectral analysis failed, using default frequencies. Error: {e}")784 785 # 3. Onset threshold based on percussiveness786 try:787 y_harmonic, y_percussive = librosa.effects.hpss(y_mono)788 percussive_ratio = np.sum(y_percussive**2) / (np.sum(y_harmonic**2) + 1e-10)789 # If the percussive energy is high, we need a higher onset threshold to be stricter790 params['onset_threshold'] = 0.6 if percussive_ratio > 0.5 else 0.45791 print(f" - Percussive ratio: {percussive_ratio:.2f} -> onset_threshold: {params['onset_threshold']}")792 except Exception as e:793 print(f" - Percussiveness analysis failed, using default onset_threshold. Error: {e}")794 795 # 4. Frame threshold from RMS796 try:797 rms = librosa.feature.rms(y=y_mono)[0]798 # Use the 10th percentile of energy as a proxy for the noise floor799 noise_floor_rms = np.percentile(rms, 10)800 # Set the frame_threshold to be slightly above this noise floor801 # The scaling factor here is empirical and can be tuned802 params['frame_threshold'] = max(0.05, min(0.4, noise_floor_rms * 4))803 print(f" - Noise floor RMS: {noise_floor_rms:.5f} -> frame_threshold: {params['frame_threshold']:.2f}")804 except Exception as e:805 print(f" - RMS analysis failed, using default frame_threshold. Error: {e}")806 807 return params808 809 810def format_params_for_metadata(params: AppParameters, transcription_log: dict = None) -> str:811 """812 Formats the AppParameters object into a human-readable string813 suitable for embedding as metadata in an audio file.814 """815 import json816 # Start with a clean dictionary of the main parameters817 params_dict = copy.copy(params.__dict__)818 819 # Create a structured dictionary for the final metadata820 structured_metadata = {821 "main_settings": {},822 "transcription_log": transcription_log if transcription_log else "Not Performed",823 "synthesis_settings": {}824 }825 826 # Separate parameters into logical groups827 transcription_keys = [828 'transcription_method', 'basic_pitch_preset_selector', 'onset_threshold',829 'frame_threshold', 'minimum_note_length', 'minimum_frequency', 'maximum_frequency',830 'infer_onsets', 'melodia_trick', 'multiple_pitch_bends'831 ]832 833 synthesis_keys = [key for key in params_dict.keys() if key.startswith('s8bit_')]834 835 # Populate the structured dictionary836 for key, value in params_dict.items():837 if key not in transcription_keys and key not in synthesis_keys:838 structured_metadata["main_settings"][key] = value839 840 for key in synthesis_keys:841 structured_metadata["synthesis_settings"][key] = params_dict[key]842 843 # If transcription log is empty, we still want to record the UI settings for transcription844 if not transcription_log:845 structured_metadata["transcription_log"] = {846 "ui_settings": {key: params_dict[key] for key in transcription_keys}847 }848 849 # Use json.dumps for clean, well-formatted, multi-line string representation850 # indent=2 makes it look nice when read back851 return json.dumps(params_dict, indent=2)852 853 854def preprocess_midi_for_harshness(midi_data: pretty_midi.PrettyMIDI, params: AppParameters):855 """856 Analyzes and modifies a PrettyMIDI object in-place to reduce characteristics857 that can cause harshness or muddiness in simple synthesizers.858 Now includes both high and low pitch attenuation.859 860 Args:861 midi_data: The PrettyMIDI object to process.862 params: The AppParameters object containing the control thresholds.863 """864 print("Running MIDI pre-processing to reduce harshness and muddiness...")865 high_notes_tamed = 0866 low_notes_tamed = 0867 chords_tamed = 0868 869 # Rule 1 & 2: High and Low Pitch Attenuation870 for instrument in midi_data.instruments:871 for note in instrument.notes:872 # Tame very high notes to reduce harshness/aliasing873 if note.pitch > params.s8bit_high_pitch_threshold:874 note.velocity = int(note.velocity * params.s8bit_high_pitch_velocity_scale)875 if note.velocity < 1: note.velocity = 1876 high_notes_tamed += 1877 878 # Tame very low notes to reduce muddiness/rumble879 if note.pitch < params.s8bit_low_pitch_threshold:880 note.velocity = int(note.velocity * params.s8bit_low_pitch_velocity_scale)881 if note.velocity < 1: note.velocity = 1882 low_notes_tamed += 1883 884 if high_notes_tamed > 0:885 print(f" - Tamed {high_notes_tamed} individual high-pitched notes.")886 if low_notes_tamed > 0:887 print(f" - Tamed {low_notes_tamed} individual low-pitched notes.")888 889 # Rule 3: Chord Compression890 # This is a simplified approach: group notes by near-simultaneous start times891 all_notes = sorted([note for instrument in midi_data.instruments for note in instrument.notes], key=lambda x: x.start)892 893 time_window = 0.02 # 20ms window to group notes into a chord894 i = 0895 while i < len(all_notes):896 current_chord = [all_notes[i]]897 # Find other notes within the time window898 j = i + 1899 while j < len(all_notes) and (all_notes[j].start - all_notes[i].start) < time_window:900 current_chord.append(all_notes[j])901 j += 1902 903 # Analyze and potentially tame the chord904 if len(current_chord) >= params.s8bit_chord_density_threshold:905 avg_velocity = sum(n.velocity for n in current_chord) / len(current_chord)906 if avg_velocity > params.s8bit_chord_velocity_threshold:907 chords_tamed += 1908 for note in current_chord:909 note.velocity = int(note.velocity * params.s8bit_chord_velocity_scale)910 if note.velocity < 1: note.velocity = 1911 912 # Move index past the current chord913 i = j914 915 if chords_tamed > 0:916 print(f" - Tamed {chords_tamed} loud, dense chords.")917 918 return midi_data # Return the modified object919 920 921def arpeggiate_midi(midi_data: pretty_midi.PrettyMIDI, params: AppParameters):922 """923 Applies a tempo-synced, rhythmic arpeggiator effect. It can generate924 various rhythmic patterns (not just continuous notes) to create a more925 musical and less "stiff" accompaniment.926 Improved rhythmic arpeggiator with dynamic density, stereo layer splitting,927 micro-randomization, and cross-beat continuity.928 929 Applies a highly configurable arpeggiator with selectable targets:930 - Accompaniment Only: The classic approach, arpeggiates harmony.931 - Melody Only: A modern approach, adds flair to the lead melody.932 - Full Mix: Applies the effect to all notes.933 934 Args:935 midi_data: The original PrettyMIDI object.936 params: AppParameters containing arpeggiator settings.937 938 Returns:939 A new PrettyMIDI object with arpeggiated chords.940 """941 print(f"Applying arpeggiator with target: {params.s8bit_arpeggio_target}...")942 processed_midi = copy.deepcopy(midi_data)943 944 # --- Step 1: Global analysis to identify lead vs. harmony notes ---945 all_notes = []946 # We need to keep track of which instrument each note belongs to947 for i, instrument in enumerate(processed_midi.instruments):948 if not instrument.is_drum:949 for note in instrument.notes:950 # Use a simple object or tuple to store note and its origin951 all_notes.append({'note': note, 'instrument_idx': i})952 953 if not all_notes:954 return processed_midi955 all_notes.sort(key=lambda x: x['note'].start)956 957 # --- Lead / Harmony separation ---958 lead_note_objects = set()959 harmony_note_objects = set()960 961 note_idx = 0962 while note_idx < len(all_notes):963 current_slice_start = all_notes[note_idx]['note'].start964 notes_in_slice = [item for item in all_notes[note_idx:] if (item['note'].start - current_slice_start) < 0.02]965 966 if not notes_in_slice:967 note_idx += 1968 continue969 970 notes_in_slice.sort(key=lambda x: x['note'].pitch, reverse=True)971 lead_note_objects.add(notes_in_slice[0]['note'])972 for item in notes_in_slice[1:]:973 harmony_note_objects.add(item['note'])974 975 note_idx += len(notes_in_slice)976 977 # --- Step 2: Determine which set of notes to process based on the target ---978 notes_to_arpeggiate = set()979 notes_to_keep_original = set()980 981 if params.s8bit_arpeggio_target == "Accompaniment Only":982 print(" - Arpeggiating harmony notes.")983 notes_to_arpeggiate = harmony_note_objects984 notes_to_keep_original = lead_note_objects985 elif params.s8bit_arpeggio_target == "Melody Only":986 print(" - Arpeggiating lead melody notes.")987 notes_to_arpeggiate = lead_note_objects988 notes_to_keep_original = harmony_note_objects989 else: # Full Mix990 print(" - Arpeggiating all non-drum notes.")991 notes_to_arpeggiate = lead_note_objects.union(harmony_note_objects)992 notes_to_keep_original = set()993 994 # --- Step 3: Estimate Tempo and prepare for generation ---995 try:996 bpm = midi_data.estimate_tempo()997 except:998 bpm = 120.0999 beat_duration_s = 60.0 / bpm1000 1001 rhythm_patterns = {1002 "Continuous 16ths": [(0.0, 0.25), (0.25, 0.25), (0.5, 0.25), (0.75, 0.25)],1003 "Classic Upbeat (8th)": [(0.5, 0.25), (0.75, 0.25)],1004 "Pulsing 8ths": [(0.0, 0.5), (0.5, 0.5)],1005 "Pulsing 4ths": [(0.0, 0.5)],1006 "Galloping": [(0.0, 0.75), (0.75, 0.25)],1007 "Simple Quarter Notes": [(0.0, 1.0)],1008 "Triplet 8ths": [(0.0, 1/3), (1/3, 1/3), (2/3, 1/3)],1009 }1010 selected_rhythm = rhythm_patterns.get(params.s8bit_arpeggio_rhythm, rhythm_patterns["Classic Upbeat (8th)"])1011 1012 # --- Step 4: Rebuild instruments with the new logic ---1013 for instrument in processed_midi.instruments:1014 if instrument.is_drum:1015 continue1016 1017 new_note_list = []1018 1019 # Add back all notes that are designated to be kept original for this track1020 inst_notes_to_keep = [n for n in instrument.notes if n in notes_to_keep_original]1021 new_note_list.extend(inst_notes_to_keep)1022 1023 # Process only the notes targeted for arpeggiation within this instrument1024 inst_notes_to_arp = [n for n in instrument.notes if n in notes_to_arpeggiate]1025 processed_arp_notes = set()1026 1027 for note1 in inst_notes_to_arp:1028 if note1 in processed_arp_notes:1029 continue1030 1031 # Group notes into chords from the target list.1032 # For melody, each note is its own "chord".1033 chord_notes = [note1]1034 if params.s8bit_arpeggio_target != "Melody Only":1035 chord_notes.extend([n2 for n2 in inst_notes_to_arp if n2 != note1 and n2 not in processed_arp_notes and abs(n2.start - note1.start) < 0.02])1036 1037 # --- Arpeggiate the identified group (which could be a single note or a chord) ---1038 for n in chord_notes:1039 processed_arp_notes.add(n)1040 1041 chord_start_time = min(n.start for n in chord_notes)1042 chord_end_time = max(n.end for n in chord_notes)1043 avg_velocity = int(np.mean([n.velocity for n in chord_notes]))1044 1045 # --- Apply an exponential curve to the velocity scale ---1046 # This makes the slider much more sensitive at lower values,1047 # allowing for true background-level arpeggios.1048 scale = params.s8bit_arpeggio_velocity_scale1049 # We use a power of 2 here, but could be tuned (e.g., 1.5, 2.5, 3.0)1050 # A higher power makes the attenuation at low scale values even more aggressive.1051 final_velocity_base = int(avg_velocity * (scale ** 2.5))1052 1053 if final_velocity_base < 1:1054 final_velocity_base = 11055 1056 # --- Pitch Pattern Generation ---1057 base_pitches = sorted([n.pitch for n in chord_notes])1058 1059 # For "Melody Only" mode, auto-generate a simple chord from the single melody note1060 if params.s8bit_arpeggio_target == "Melody Only" and len(base_pitches) == 1:1061 # This is a very simple major chord generator, can be expanded later1062 # Auto-generate a major chord from the single melody note1063 root = base_pitches[0]1064 base_pitches = [root, root + 4, root + 7]1065 1066 pattern = []1067 for octave in range(params.s8bit_arpeggio_octave_range):1068 octave_pitches = [p + (12 * octave) for p in base_pitches]1069 if params.s8bit_arpeggio_pattern == "Up":1070 pattern.extend(octave_pitches)1071 elif params.s8bit_arpeggio_pattern == "Down":1072 pattern.extend(reversed(octave_pitches))1073 elif params.s8bit_arpeggio_pattern == "UpDown":1074 pattern.extend(octave_pitches)1075 if len(octave_pitches) > 2:1076 pattern.extend(reversed(octave_pitches[1:-1]))1077 1078 if not pattern:1079 continue1080 1081 # --- Rhythmic Note Generation ---1082 note_base_density = getattr(params, "s8bit_arpeggio_density", 0.6)1083 chord_duration = chord_end_time - chord_start_time1084 note_duration_factor = min(1.0, chord_duration / (2 * beat_duration_s)) if beat_duration_s > 0 else 1.01085 note_density_factor = note_base_density * note_duration_factor1086 1087 current_beat = chord_start_time / beat_duration_s if beat_duration_s > 0 else 01088 current_time = chord_start_time1089 pattern_index = 01090 while current_time < chord_end_time:1091 # Lay down the rhythmic pattern for the current beat1092 current_beat_start_time = np.floor(current_beat) * beat_duration_s1093 1094 for start_offset, duration_beats in selected_rhythm:1095 note_start_time = current_beat_start_time + (start_offset * beat_duration_s)1096 note_duration_s = duration_beats * beat_duration_s * note_density_factor1097 1098 # Ensure the note does not exceed the chord's total duration1099 if note_start_time >= chord_end_time:1100 break1101 1102 pitch = pattern[pattern_index % len(pattern)]1103 1104 # Micro-randomization1105 rand_offset = random.uniform(-0.01, 0.01) # ±10ms1106 final_velocity = max(1, min(127, final_velocity_base + random.randint(-5, 5)))1107 1108 new_note = pretty_midi.Note(1109 velocity=final_velocity,1110 pitch=pitch,1111 start=max(0.0, note_start_time + rand_offset),1112 end=min(chord_end_time, note_start_time + note_duration_s)1113 )1114 new_note_list.append(new_note)1115 pattern_index += 11116 1117 current_beat += 1.01118 current_time = current_beat * beat_duration_s if beat_duration_s > 0 else float('inf')1119 1120 # Replace the instrument's original note list with the new, processed one1121 instrument.notes = new_note_list1122 1123 print("Targeted arpeggiator finished.")1124 return processed_midi1125 1126 1127def create_delay_effect(midi_data: pretty_midi.PrettyMIDI, params: AppParameters):1128 """1129 Creates a delay/echo effect by duplicating notes with delayed start times1130 and scaled velocities. Can be configured to apply only to the lead melody.1131 based on the MIDI's estimated BPM and the user's selected musical division.1132 """1133 print("Applying tempo-synced MIDI delay/echo effect...")1134 # Work on a deep copy to ensure the original MIDI object is not mutated.1135 processed_midi = copy.deepcopy(midi_data)1136 1137 # --- Step 1: Estimate Tempo and Calculate Delay Time in Seconds ---1138 try:1139 bpm = midi_data.estimate_tempo()1140 except:1141 bpm = 120.01142 print(f" - Delay using tempo: {bpm:.2f} BPM")1143 1144 # This map defines the duration of each note division as a multiplier of a quarter note (a beat).1145 division_map = {1146 "Quarter Note": 1.0,1147 "Dotted 8th Note": 0.75,1148 "8th Note": 0.5,1149 "Triplet 8th Note": 1.0 / 3.0,1150 "16th Note": 0.251151 }1152 beat_duration_s = 60.0 / bpm1153 division_multiplier = division_map.get(params.s8bit_delay_division, 0.75)1154 delay_time_s = beat_duration_s * division_multiplier1155 1156 print(f" - Delay set to {params.s8bit_delay_division}, calculated time: {delay_time_s:.3f}s")1157 1158 # --- Step 2: Identify the notes that should receive the echo effect ---1159 notes_to_echo = []1160 1161 if params.s8bit_delay_on_melody_only:1162 print(" - Delay will be applied to lead melody notes only.")1163 all_notes = [note for inst in processed_midi.instruments if not inst.is_drum for note in inst.notes]1164 all_notes.sort(key=lambda n: n.start)1165 1166 note_idx = 01167 while note_idx < len(all_notes):1168 current_slice_start = all_notes[note_idx].start1169 notes_in_slice = [n for n in all_notes[note_idx:] if (n.start - current_slice_start) < 0.02]1170 if not notes_in_slice:1171 note_idx += 11172 continue1173 1174 # The highest note in the slice is considered the lead note1175 notes_in_slice.sort(key=lambda n: n.pitch, reverse=True)1176 notes_to_echo.append(notes_in_slice[0])1177 note_idx += len(notes_in_slice)1178 else:1179 print(" - Delay will be applied to all non-drum notes.")1180 notes_to_echo = [note for inst in processed_midi.instruments if not inst.is_drum for note in inst.notes]1181 1182 if not notes_to_echo:1183 print(" - No notes found to apply delay to. Skipping.")1184 return processed_midi1185 1186 # --- Step 3: Generate echo notes with optional octave shift using the calculated delay time ---1187 echo_notes = []1188 bass_note_threshold = 48 # MIDI note for C31189 treble_note_threshold = 84 # MIDI note for C61190 1191 for i in range(1, params.s8bit_delay_repeats + 1):1192 for original_note in notes_to_echo:1193 # Create a copy of the note for the echo1194 echo_note = copy.copy(original_note)1195 1196 # --- Octave Shift Logic for both Bass and Treble ---1197 if params.s8bit_delay_bass_pitch_shift and original_note.pitch < bass_note_threshold:1198 echo_note.pitch += params.s8bit_delay_bass_pitch_shift1199 elif params.s8bit_delay_treble_pitch_shift and original_note.pitch > treble_note_threshold:1200 echo_note.pitch += params.s8bit_delay_treble_pitch_shift