CoolFace
Apppublic

avans06/Audio-To-MIDI-And-Advanced-Renderer

sourceHugging Facecc-by-nc-4.0updated 1y agoView on Hugging Face
37likes
app.py4950 linesDownload Raw Back to root
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

Showing the first 1,200 of 4950 lines. Download the file for the rest.