From 3dd55e7befabf93904da04556be8dd29e62afc30 Mon Sep 17 00:00:00 2001 From: RobUmf Date: Tue, 20 Jan 2026 00:09:54 -0600 Subject: [PATCH 01/18] Add sweet_mixer.sh to tools folder Adding the bash mixer script to handle 1.4x volume boosting and prevent overmodulation. --- tools/sweet_mixer.sh | 93 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 tools/sweet_mixer.sh diff --git a/tools/sweet_mixer.sh b/tools/sweet_mixer.sh new file mode 100644 index 0000000..1e2e949 --- /dev/null +++ b/tools/sweet_mixer.sh @@ -0,0 +1,93 @@ +#!/bin/bash +# UltraSinger Audio Profile Mixer + +# Colors +CYAN='\033[0;36m' +GRN='\033[0;32m' +RED='\033[0;31m' +NC='\033[0m' + +echo -e "${CYAN}--------------------------------------------------------" +echo -e "šŸŽšļø ULTRASINGER MIXING DESK" +echo -e "--------------------------------------------------------${NC}" + +# 1. SCAN FOR LOCAL FILES +shopt -s nullglob +FILES=( *.{wav,flac,mp3} ) + +if [ ${#FILES[@]} -gt 0 ]; then + echo -e "${CYAN}Found audio files in this folder:${NC}" + for i in "${!FILES[@]}"; do + echo -e "$((i+1))) ${FILES[$i]}" + done + echo "--------------------------------------------------------" +fi + +# 2. SELECT VOCALS +echo -e "${GRN}āŒØļø Select VOCAL (Number or Path):${NC}" +read -e -r v_raw +if [[ "$v_raw" =~ ^[0-9]+$ ]] && [ "$v_raw" -le "${#FILES[@]}" ]; then + VOCALS="${FILES[$((v_raw-1))]}" +else + v_clean="${v_raw//\'/}" + v_clean="${v_clean//\"/}" + VOCALS="$(echo "${v_clean}" | xargs)" +fi + +# 3. SELECT INSTRUMENTAL +echo -e "${GRN}āŒØļø Select INSTRUMENTAL (Number or Path):${NC}" +read -e -r i_raw +if [[ "$i_raw" =~ ^[0-9]+$ ]] && [ "$i_raw" -le "${#FILES[@]}" ]; then + INSTRUMENTAL="${FILES[$((i_raw-1))]}" +else + i_clean="${i_raw//\'/}" + i_clean="${i_clean//\"/}" + INSTRUMENTAL="$(echo "${i_clean}" | xargs)" +fi + +# 4. PROFILE SELECTION (Moved up so we know the name) +echo -e "${CYAN}--------------------------------------------------------" +echo -e "Select Mixing Profile:${NC}" +echo -e "1) ${GRN}Sing-a-long${NC} (Vocals 1.4x | Inst 1.4x) - Standard" +echo -e "2) ${GRN}Instrumental${NC} (Vocals 0.0x | Inst 1.4x) - No Vocals" +echo -e "3) ${GRN}Guide Track${NC} (Vocals 0.3x | Inst 1.4x) - Quiet Vocals" +read -p ">> Choice [1-3]: " MIX_CHOICE + +case $MIX_CHOICE in + 1) + DEFAULT_NAME="song.mp3" + FILTER="[0:a]volume=1.4[v];[1:a]volume=1.4[i];[v][i]amix=inputs=2:duration=first:dropout_transition=0:normalize=0[aout]" + ;; + 2) + DEFAULT_NAME="instrumental.mp3" + FILTER="[0:a]volume=0.0[v];[1:a]volume=1.4[i];[v][i]amix=inputs=2:duration=first:dropout_transition=0:normalize=0[aout]" + ;; + 3) + DEFAULT_NAME="guide.mp3" + FILTER="[0:a]volume=0.3[v];[1:a]volume=1.4[i];[v][i]amix=inputs=2:duration=first:normalize=0[aout]" + ;; + *) exit 1 ;; +esac + +# 5. FILENAME PROMPT (Now pre-filled with the choice) +echo -e "${CYAN}--------------------------------------------------------${NC}" +echo -e "${GRN}Filename:${NC}" +# This will now show the default name ready for you to hit Enter +read -e -i "$DEFAULT_NAME" -r OUT_FILE + +# 6. COMMAND REVIEW +BASE_CMD="ffmpeg -i \"$VOCALS\" -i \"$INSTRUMENTAL\" -filter_complex \"$FILTER\" -map \"[aout]\" -b:a 320k \"$OUT_FILE\"" + +echo -e "${CYAN}-----------------------------------------------------------" +echo -e "šŸ› ļø FINAL CHECK: Edit command if needed." +echo -e "-----------------------------------------------------------${NC}" + +read -e -i "$BASE_CMD" -p "šŸš€ RUN: " FINAL_CMD + +eval "$FINAL_CMD" + +if [ $? -eq 0 ]; then + echo -e "${GRN}āœ… Success! Saved to: ./$OUT_FILE${NC}" +else + echo -e "${RED}āŒ Failed.${NC}" +fi From 3ad834d0e90b3792ce9cdb2e5e0ed0978cc0ebb1 Mon Sep 17 00:00:00 2001 From: RobUmf Date: Tue, 20 Jan 2026 00:12:25 -0600 Subject: [PATCH 02/18] Text to go with sweet_mixer Instructions with options --- tools/sweet_mixer.txt | 56 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 tools/sweet_mixer.txt diff --git a/tools/sweet_mixer.txt b/tools/sweet_mixer.txt new file mode 100644 index 0000000..0547bc1 --- /dev/null +++ b/tools/sweet_mixer.txt @@ -0,0 +1,56 @@ +============================================================ +ULTRASINGER SWEET MIXER: Audio & Volume Optimization Guide +============================================================ +Fix for Overmodulation, Quiet Output, and Pitch Tracking + +This tool implements "Sweet Spot" mixing logic to handle +high-fidelity 32-bit float stems produced by UltraSinger. + +------------------------------------------------------------ +1. THE "SWEET SPOT" LOGIC +------------------------------------------------------------ +* 32-bit Float Advantage: Stems often peak low (high headroom). + Standard mixes sound "thin." We apply a 1.4x boost to + "rescue" the audio for professional levels. + +* Why normalize=0?: Standard FFmpeg 'amix' lowers volume to + prevent clipping. We bypass this to manually restore amplitude. + +* Improved Pitch: Boosting vocal amplitude helps engines like + Crepe/RMVPE track notes more accurately for the .txt output. + +------------------------------------------------------------ +2. HOW TO RUN +------------------------------------------------------------ +Permissions: chmod +x sweet_mixer.sh +Execution: bash tools/sweet_mixer.sh + +The script provides 3 ways to select files: +1) AUTO-INDEX: Run the script in the song folder to pick + files by number. +2) DRAG & DROP: Drag audio files directly into the terminal. +3) DIRECT PATH: Enter the path manually. + +------------------------------------------------------------ +3. MIXING PROFILES +------------------------------------------------------------ +Choose your profile after selecting files: + +| Profile | Vocal Vol | Inst. Vol | Use Case | +|-----------------|-----------|-----------|------------------| +| 1. Sing-a-long | 1.4x | 1.4x | Standard Karaoke | +| 2. Instrumental | 0.0x | 1.4x | No Vocals | +| 3. Guide Track | 0.3x | 1.4x | Practice / Pitch | + +Logic: The script defaults to 'song.mp3', 'instrumental.mp3', +or 'guide.mp3' unless you type a custom name. + +------------------------------------------------------------ +4. MANUAL CLI REFERENCE +------------------------------------------------------------ +Template for manual execution: + +ffmpeg -i vocals.wav -i no_vocals.wav -filter_complex \ +"[0:a]volume=1.4[v];[1:a]volume=1.4[i];[v][i]amix=inputs=2:duration=first:dropout_transition=0:normalize=0[aout]" \ +-map "[aout]" -b:a 320k "song.mp3" +============================================================ From 2e3640884991bfa23e0ab0e3e74f834a9667a0e4 Mon Sep 17 00:00:00 2001 From: RobUmf Date: Tue, 20 Jan 2026 17:12:44 -0600 Subject: [PATCH 03/18] Mixer in PY and MD "Updated default volume from 1.4x to 1.2x. During testing, 1.4x caused slight overmodulation in tracks transitioning between high-resonance strings and percussive piano. 1.2x provides the necessary 'Sweet Spot' boost while maintaining clean head-room." --- tools/sweet_mixer.md | 48 +++++++++++ tools/sweet_mixer.py | 184 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 232 insertions(+) create mode 100644 tools/sweet_mixer.md create mode 100644 tools/sweet_mixer.py diff --git a/tools/sweet_mixer.md b/tools/sweet_mixer.md new file mode 100644 index 0000000..c8ff15a --- /dev/null +++ b/tools/sweet_mixer.md @@ -0,0 +1,48 @@ +# UltraSinger Sweet Mixer: Audio & Volume Optimization Guide + +**Fix for Overmodulation, Quiet Output, and Pitch Tracking** + +This tool implements "Sweet Spot" mixing logic to handle high-fidelity 32-bit float stems produced by UltraSinger. + +## 1. The "Sweet Spot" Logic + +* **32-bit Float Advantage:** Stems often peak low (high headroom). Standard mixes sound "thin." We apply a **1.2x boost** to "rescue" the audio for professional levels without clipping. +* **Why normalize=0?** Standard FFmpeg `amix` lowers volume to prevent clipping. We bypass this to manually restore amplitude. +* **Improved Pitch:** Boosting vocal amplitude helps engines like Crepe/RMVPE track notes more accurately for the `.txt` output. + +## 2. How to Run + +The script provides 3 ways to select files: + +1) Auto-Index: Run the script in the song folder to pick files by number. + +2) Drag & Drop: Drag audio files directly into the terminal. + +3) Direct Path: Enter the path manually. + +**Permissions:** +```bash +chmod +x sweet_mixer.py + +python tools/sweet_mixer.py +# OR +./tools/sweet_mixer.py +# 0r if copy to the files for index +./sweet_mixer.py + +Logic: The script defaults to song.mp3, instrumental.mp3, or guide.mp3 unless you type a custom name. + +| **Karaoke** | 1.2x | 1.2x | Standard Sing-a-long | +| **Instrumental** | 0.0x | 1.2x | No Vocals | +| **Guide Track** | 0.3x | 1.2x | Practice / Pitch | + + +4. Manual CLI Reference +Template for manual execution (if not using the script): + +ffmpeg -i vocals.wav -i no_vocals.wav -filter_complex \ +"[0:a]volume=1.2[v];[1:a]volume=1.2[i];[v][i]amix=inputs=2:duration=first:dropout_transition=0:normalize=0[aout]" \ +-map "[aout]" -b:a 320k "song.mp3" + + +RobUmf with Gemini 3 for UltraSinger diff --git a/tools/sweet_mixer.py b/tools/sweet_mixer.py new file mode 100644 index 0000000..c87a96f --- /dev/null +++ b/tools/sweet_mixer.py @@ -0,0 +1,184 @@ +#!/usr/bin/env python3 +import os +import subprocess +import glob +import sys +import readline +import platform + +# Colors +CYAN = '\033[0;36m' +GRN = '\033[0;32m' +RED = '\033[0;31m' +NC = '\033[0m' + +# Setup readline for better input editing +readline.parse_and_bind('set editing-mode emacs') + +def check_dependencies(): + """Ensure ffmpeg is available.""" + try: + subprocess.run(['ffmpeg', '-version'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + except FileNotFoundError: + print(f"{RED}Error: ffmpeg is not installed or not in PATH.{NC}") + sys.exit(1) + +def get_audio_files(): + """Scan directory for common audio formats.""" + extensions = ['*.wav', '*.flac', '*.mp3', '*.m4a', '*.ogg'] + files = [] + for ext in extensions: + files.extend(glob.glob(ext)) + return sorted(files) + +def clean_path(path): + """Remove quotes from drag-and-dropped paths.""" + return path.strip().replace("'", "").replace('"', "") + +def play_file(filepath): + """Opens the file in the default system player (Cross-platform).""" + if not os.path.exists(filepath): + print(f"{RED}File not found: {filepath}{NC}") + return + + print(f"{CYAN}šŸŽ§ Playing: {filepath}{NC}") + current_os = platform.system() + + try: + if current_os == 'Windows': + os.startfile(filepath) + elif current_os == 'Darwin': # macOS + subprocess.run(['open', filepath], check=True) + else: # Linux + subprocess.run(['xdg-open', filepath], check=True) + except Exception as e: + print(f"{RED}Could not open player: {e}{NC}") + +def select_input(prompt_text, file_list): + """Helper to handle selecting a file by number or path.""" + while True: + raw = input(f"{GRN}āŒØļø {prompt_text} (Number or Path):{NC} ").strip() + if not raw: + continue + + # If user entered a number + if raw.isdigit(): + idx = int(raw) - 1 + if 0 <= idx < len(file_list): + return file_list[idx] + else: + print(f"{RED}Invalid number.{NC}") + continue + + # If user entered a path + clean = clean_path(raw) + if os.path.exists(clean): + return clean + else: + print(f"{RED}File path not found.{NC}") + +def main(): + check_dependencies() + print(f"{CYAN}--------------------------------------------------------") + print("šŸŽšļø ULTRASINGER MIXING DESK & PREVIEW") + print(f"--------------------------------------------------------{NC}") + + files = get_audio_files() + if files: + print(f"{CYAN}Found audio files:{NC}") + for i, file in enumerate(files): + print(f"{i+1}) {file}") + print("--------------------------------------------------------") + else: + print(f"{RED}No audio files found in current directory.{NC}") + + # 1. SELECT FILES + # We allow manual path entry even if no files were globbed + vocals = select_input("Select VOCAL", files) + instrumental = select_input("Select INSTRUMENTAL", files) + + # 2. ACTION MENU LOOP + while True: + print(f"\n{CYAN}Select Action:{NC}") + print(f"1) {GRN}Karaoke{NC} (Vocals 1.2x | Inst 1.2x)") + print(f"2) {GRN}Instrumental{NC} (Vocals 0.0x | Inst 1.2x)") + print(f"3) {GRN}Guide Track{NC} (Vocals 0.3x | Inst 1.2x)") + print(f"4) {CYAN}Preview Vocals{NC}") + print(f"5) {CYAN}Preview Instrumental{NC}") + print(f"6) {RED}Exit{NC}") + + choice = input(">> Choice [1-6]: ").strip() + + # Mixing Profiles + # Adjusted default volume to 1.2 based on feedback + match choice: + case "4": + play_file(vocals) + continue + case "5": + play_file(instrumental) + continue + case "6": + print("Bye!") + sys.exit(0) + case "1": + config = {"name": "song.mp3", "v": "1.2", "i": "1.2"} + case "2": + config = {"name": "instrumental.mp3", "v": "0.0", "i": "1.2"} + case "3": + config = {"name": "guide.mp3", "v": "0.3", "i": "1.2"} + case _: + print(f"{RED}Invalid selection.{NC}") + continue + + # 3. BUILD FFmpeg COMMAND + out_file = input(f"{GRN}Filename (Enter for '{config['name']}'):{NC} ") or config['name'] + + # Using normalized=0 prevents amix from dropping volume automatically, + # so our 1.2 volume filter effectively boosts gain. + filter_complex = ( + f"[0:a]volume={config['v']}[v];" + f"[1:a]volume={config['i']}[i];" + f"[v][i]amix=inputs=2:duration=first:dropout_transition=0:normalize=0[aout]" + ) + + current_cmd = ( + f'ffmpeg -y -i "{vocals}" -i "{instrumental}" ' + f'-filter_complex "{filter_complex}" -map "[aout]" ' + f'-b:a 320k "{out_file}"' + ) + + # 4. SURGERY LOOP + # Allows editing the command before running it + mixing = True + while mixing: + print(f"\n{CYAN}-----------------------------------------------------------") + print(f"šŸ› ļø CURRENT COMMAND:") + print(f"{NC}{current_cmd}") + print(f"-----------------------------------------------------------") + + user_choice = input(f"Type {GRN}'run'{NC}, {GRN}'edit'{NC}, {CYAN}'play'{NC} (result), {CYAN}'back'{NC} {RED}'exit'{NC}: ").lower().strip() + + if user_choice == 'run': + try: + print(f"{CYAN}Rendering...{NC}") + subprocess.run(current_cmd, shell=True, check=True) + print(f"{GRN}āœ… Success! Saved to: ./{out_file}{NC}") + except subprocess.CalledProcessError: + print(f"{RED}āŒ FFmpeg failed.{NC}") + elif user_choice == 'edit': + # Pre-fill input buffer with current command for easy editing + readline.add_history(current_cmd) + print(f"{CYAN}Edit command (use arrow keys):{NC}") + new_cmd = input("> ") + if new_cmd.strip(): + current_cmd = new_cmd + elif user_choice == 'play': + play_file(out_file) + elif user_choice == 'back': + mixing = False # Breaks inner loop, returns to menu + elif user_choice == 'exit': + sys.exit(0) + +if __name__ == "__main__": + main() From 60c9b5442a42d2ec224056413ee0ae1979470953 Mon Sep 17 00:00:00 2001 From: RobUmf Date: Wed, 21 Jan 2026 16:56:03 -0600 Subject: [PATCH 04/18] Update sweet_mixer.py Being compliant with according to CodeFactor --- tools/sweet_mixer.py | 76 +++++++++++++++++++++----------------------- 1 file changed, 36 insertions(+), 40 deletions(-) diff --git a/tools/sweet_mixer.py b/tools/sweet_mixer.py index c87a96f..5186feb 100644 --- a/tools/sweet_mixer.py +++ b/tools/sweet_mixer.py @@ -5,6 +5,7 @@ import sys import readline import platform +import shutil # Colors CYAN = '\033[0;36m' @@ -15,13 +16,17 @@ # Setup readline for better input editing readline.parse_and_bind('set editing-mode emacs') -def check_dependencies(): - """Ensure ffmpeg is available.""" - try: - subprocess.run(['ffmpeg', '-version'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - except FileNotFoundError: - print(f"{RED}Error: ffmpeg is not installed or not in PATH.{NC}") +def get_safe_executable(name): + """Finds the absolute path of an executable to prevent path hijacking.""" + exe_path = shutil.which(name) + if not exe_path: + print(f"{RED}Error: {name} is not installed or not in PATH.{NC}") sys.exit(1) + return exe_path + +def check_dependencies(): + """Ensure ffmpeg is available and return its absolute path.""" + return get_safe_executable('ffmpeg') def get_audio_files(): """Scan directory for common audio formats.""" @@ -36,21 +41,25 @@ def clean_path(path): return path.strip().replace("'", "").replace('"', "") def play_file(filepath): - """Opens the file in the default system player (Cross-platform).""" + """Opens the file in the default system player using absolute paths.""" if not os.path.exists(filepath): print(f"{RED}File not found: {filepath}{NC}") return print(f"{CYAN}šŸŽ§ Playing: {filepath}{NC}") current_os = platform.system() + abs_filepath = os.path.abspath(filepath) try: if current_os == 'Windows': - os.startfile(filepath) + # os.startfile is secure as it doesn't invoke a shell + os.startfile(abs_filepath) elif current_os == 'Darwin': # macOS - subprocess.run(['open', filepath], check=True) + cmd = get_safe_executable('open') + subprocess.run([cmd, abs_filepath], check=True) else: # Linux - subprocess.run(['xdg-open', filepath], check=True) + cmd = get_safe_executable('xdg-open') + subprocess.run([cmd, abs_filepath], check=True) except Exception as e: print(f"{RED}Could not open player: {e}{NC}") @@ -61,7 +70,6 @@ def select_input(prompt_text, file_list): if not raw: continue - # If user entered a number if raw.isdigit(): idx = int(raw) - 1 if 0 <= idx < len(file_list): @@ -70,7 +78,6 @@ def select_input(prompt_text, file_list): print(f"{RED}Invalid number.{NC}") continue - # If user entered a path clean = clean_path(raw) if os.path.exists(clean): return clean @@ -78,7 +85,7 @@ def select_input(prompt_text, file_list): print(f"{RED}File path not found.{NC}") def main(): - check_dependencies() + ffmpeg_bin = check_dependencies() print(f"{CYAN}--------------------------------------------------------") print("šŸŽšļø ULTRASINGER MIXING DESK & PREVIEW") print(f"--------------------------------------------------------{NC}") @@ -92,12 +99,9 @@ def main(): else: print(f"{RED}No audio files found in current directory.{NC}") - # 1. SELECT FILES - # We allow manual path entry even if no files were globbed vocals = select_input("Select VOCAL", files) instrumental = select_input("Select INSTRUMENTAL", files) - # 2. ACTION MENU LOOP while True: print(f"\n{CYAN}Select Action:{NC}") print(f"1) {GRN}Karaoke{NC} (Vocals 1.2x | Inst 1.2x)") @@ -109,8 +113,6 @@ def main(): choice = input(">> Choice [1-6]: ").strip() - # Mixing Profiles - # Adjusted default volume to 1.2 based on feedback match choice: case "4": play_file(vocals) @@ -131,52 +133,46 @@ def main(): print(f"{RED}Invalid selection.{NC}") continue - # 3. BUILD FFmpeg COMMAND out_file = input(f"{GRN}Filename (Enter for '{config['name']}'):{NC} ") or config['name'] - # Using normalized=0 prevents amix from dropping volume automatically, - # so our 1.2 volume filter effectively boosts gain. filter_complex = ( f"[0:a]volume={config['v']}[v];" f"[1:a]volume={config['i']}[i];" f"[v][i]amix=inputs=2:duration=first:dropout_transition=0:normalize=0[aout]" ) - current_cmd = ( - f'ffmpeg -y -i "{vocals}" -i "{instrumental}" ' - f'-filter_complex "{filter_complex}" -map "[aout]" ' - f'-b:a 320k "{out_file}"' - ) + # Build command as a list to avoid shell=True + cmd_args = [ + ffmpeg_bin, "-y", + "-i", vocals, + "-i", instrumental, + "-filter_complex", filter_complex, + "-map", "[aout]", + "-b:a", "320k", + out_file + ] - # 4. SURGERY LOOP - # Allows editing the command before running it mixing = True while mixing: print(f"\n{CYAN}-----------------------------------------------------------") - print(f"šŸ› ļø CURRENT COMMAND:") - print(f"{NC}{current_cmd}") + print(f"šŸ› ļø PROPOSED COMMAND:") + print(f"{NC}{' '.join(cmd_args)}") print(f"-----------------------------------------------------------") - user_choice = input(f"Type {GRN}'run'{NC}, {GRN}'edit'{NC}, {CYAN}'play'{NC} (result), {CYAN}'back'{NC} {RED}'exit'{NC}: ").lower().strip() + user_choice = input(f"Type {GRN}'run'{NC}, {CYAN}'play'{NC} (result), {CYAN}'back'{NC} or {RED}'exit'{NC}: ").lower().strip() if user_choice == 'run': try: print(f"{CYAN}Rendering...{NC}") - subprocess.run(current_cmd, shell=True, check=True) + # shell=False is used by default when passing a list + subprocess.run(cmd_args, check=True) print(f"{GRN}āœ… Success! Saved to: ./{out_file}{NC}") except subprocess.CalledProcessError: print(f"{RED}āŒ FFmpeg failed.{NC}") - elif user_choice == 'edit': - # Pre-fill input buffer with current command for easy editing - readline.add_history(current_cmd) - print(f"{CYAN}Edit command (use arrow keys):{NC}") - new_cmd = input("> ") - if new_cmd.strip(): - current_cmd = new_cmd elif user_choice == 'play': play_file(out_file) elif user_choice == 'back': - mixing = False # Breaks inner loop, returns to menu + mixing = False elif user_choice == 'exit': sys.exit(0) From 3b3893d3874f60343c29e76634a8f67e182e140f Mon Sep 17 00:00:00 2001 From: RobUmf Date: Fri, 23 Jan 2026 16:49:25 -0600 Subject: [PATCH 05/18] Add --disable_separation flag to bypass Demucs --disable_separation flag, allowing users to provide pre-separated vocal stems (e.g., from a high-quality manual extraction or specialized external tools). When active, UltraSinger skips the Demucs separation phase entirely, significantly reducing processing time and preventing redundant extraction. --- src/Settings.py | 4 ++-- src/UltraSinger.py | 44 ++++++++++++++++++++++++++++++-------------- 2 files changed, 32 insertions(+), 16 deletions(-) diff --git a/src/Settings.py b/src/Settings.py index 1c98d82..9919e76 100644 --- a/src/Settings.py +++ b/src/Settings.py @@ -65,9 +65,9 @@ class Settings: # UltraSinger Evaluation Configuration test_songs_input_folder = None - cache_override_path = None + cache_override_path = None # New class definition skip_cache_vocal_separation = False skip_cache_denoise_vocal_audio = False skip_cache_transcription = False skip_cache_pitch_detection = False - calculate_score = True \ No newline at end of file + calculate_score = True diff --git a/src/UltraSinger.py b/src/UltraSinger.py index 8c4e120..4d348f7 100644 --- a/src/UltraSinger.py +++ b/src/UltraSinger.py @@ -444,29 +444,42 @@ def CreateProcessAudio(process_data) -> str: ) os_helper.create_folder(process_data.process_data_paths.cache_folder_path) - # Separate vocal from audio - audio_separation_folder_path = separate_vocal_from_audio( - process_data.process_data_paths.cache_folder_path, - process_data.process_data_paths.audio_output_file_path, - settings.use_separated_vocal, - settings.create_karaoke, - settings.pytorch_device, - settings.demucs_model, - settings.skip_cache_vocal_separation - ) - process_data.process_data_paths.vocals_audio_file_path = os.path.join(audio_separation_folder_path, "vocals.wav") - process_data.process_data_paths.instrumental_audio_file_path = os.path.join(audio_separation_folder_path, - "no_vocals.wav") - + # Check if we should separate vocals or bypass separation if settings.use_separated_vocal: + + audio_separation_folder_path = separate_vocal_from_audio( + process_data.process_data_paths.cache_folder_path, + process_data.process_data_paths.audio_output_file_path, + settings.use_separated_vocal, + settings.create_karaoke, + settings.pytorch_device, + settings.demucs_model, + settings.skip_cache_vocal_separation + ) + process_data.process_data_paths.vocals_audio_file_path = os.path.join(audio_separation_folder_path, "vocals.wav") + process_data.process_data_paths.instrumental_audio_file_path = os.path.join(audio_separation_folder_path, "no_vocals.wav") + + # In standard mode, we process the separated vocal file input_path = process_data.process_data_paths.vocals_audio_file_path + else: + # Skip separation entirely + print(f"{ULTRASINGER_HEAD} {gold_highlighted('Bypass Mode:')} {cyan_highlighted('Skipping Demucs separation.')}") + + # Point the 'vocal' and 'instrumental' variables to the original input file + # This prevents "File Not Found" errors later in the script input_path = process_data.process_data_paths.audio_output_file_path + process_data.process_data_paths.vocals_audio_file_path = input_path + process_data.process_data_paths.instrumental_audio_file_path = input_path + # Denoise vocal audio denoised_output_path = os.path.join( process_data.process_data_paths.cache_folder_path, process_data.basename + "_denoised.wav" ) + + + # This ensures it uses the correct file selected in the IF/ELSE block above denoise_vocal_audio(input_path, denoised_output_path, settings.skip_cache_denoise_vocal_audio) # Convert to mono audio @@ -691,6 +704,8 @@ def init_settings(argv: list[str]) -> Settings: settings.keep_cache = True elif opt in ("--musescore_path"): settings.musescore_path = arg + elif opt in ("--cache_override_path"): # <--- ADD THIS BLOCK + settings.cache_override_path = arg #Addition of demucs model choice. Work seems to be needed to make sure syntax is same for models. Added error handling for unknown models elif opt in ("--demucs"): try: @@ -741,6 +756,7 @@ def arg_options(): "format_version=", "keep_cache", "musescore_path=", + "cache_override_path=", # Added this line so the flag is recognized "keep_numbers", "interactive", "cookiefile=", From 3fe421a1d0c19dd4234c9e9ac3ccdf443ba7e33f Mon Sep 17 00:00:00 2001 From: RobUmf Date: Fri, 23 Jan 2026 20:17:09 -0600 Subject: [PATCH 06/18] portableMP3.py and for mobile optimization A high-performance Python utility designed to compress and migrate large music archives for mobile devices, flip phones, and portable storage. --- tools/portableMP3.md | 59 +++++++++++++++++++++++++++++++ tools/portableMP3.py | 82 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 141 insertions(+) create mode 100644 tools/portableMP3.md create mode 100644 tools/portableMP3.py diff --git a/tools/portableMP3.md b/tools/portableMP3.md new file mode 100644 index 0000000..d91d5c3 --- /dev/null +++ b/tools/portableMP3.md @@ -0,0 +1,59 @@ +# UltraSinger Portable MP3 Optimizer + +A high-performance Python utility designed to compress and migrate large music archives for mobile devices, flip phones, and portable storage. Optimized specifically for the **UltraSinger 3.10** ecosystem. + +## šŸ“– Description + +This tool streamlines the process of converting high-bitrate or lossless music archives (FLAC, WAV, M4A) into mobile-friendly 128kbps MP3s. It features intelligent deduplication to clean up "dirty" archives and ensures metadata compatibility with legacy hardware. + +## 🚩 Features + +* **Bitrate Optimization**: Defaults to 128kbps (the "sweet spot" for storage vs. quality on mobile). +* **Intelligent Deduplication**: Automatically detects if files in the root directory match files in subdirectories; prioritizes organized album paths over loose root files. +* **Broad Compatibility**: Uses `ID3v2.3` and `44.1kHz` sampling to ensure album art and audio play correctly on basic handsets (e.g., Coosea SL006D). +* **Flexible Structure**: Supports both mirrored directory trees and flattened outputs. + +## šŸ› ļø Usage + +```bash +python3 portableMP3.py -i -o [FLAGS] + +## šŸ› ļø Flag Documentation & Logic + +### `--flat` (Flatten Output) +The `--flat` flag modifies the destination path logic. Instead of mirroring the source directory tree, it strips all path information and places every converted file directly into the root of the target folder. + +**Use Case:** * Essential for MTP transfers to devices with slow file indexing. +* Required for legacy players/car stereos that do not support nested folders. + +### 🧠 Intelligent Deduplication +The script now includes a "Path-Depth Priority" check to handle redundant files common in large archives (e.g., songs appearing in both the root and an album subfolder). + +**How it works:** +1. Scans the input tree for all compatible audio files. +2. If two files share the same filename, the script calculates the path depth. +3. The file located **deeper** in the directory tree (e.g., inside an Album folder) is prioritized. +4. The redundant "root" version is skipped to save storage and prevent duplicate entries in mobile playlists. + + + +--- + +## šŸŽØ Metadata & Album Art +The `--cover` flag has been specifically tuned for hardware compatibility. + +| Parameter | Value | Reason | +| :--- | :--- | :--- | +| **ID3 Version** | `v2.3` | Fixes "Missing Art" issues on flip phones (SL006D). | +| **Sample Rate** | `44100 Hz` | Standard for internal DACs on mobile devices. | +| **Mapping** | `0:v?` | Non-blocking; only attempts to copy art if the stream exists. | + + + +--- + +## šŸš€ Execution Examples + +**Preserve Tree (Default):** +```bash +python3 portableMP3.py -i "~/Downloads/PonyArchive" -o "/media/nvme/AI_Work/Inputs" --cover diff --git a/tools/portableMP3.py b/tools/portableMP3.py new file mode 100644 index 0000000..1ff371e --- /dev/null +++ b/tools/portableMP3.py @@ -0,0 +1,82 @@ +import os +import argparse +import subprocess +from pathlib import Path + +def run_ffmpeg(input_file, output_file, bitrate, include_cover, start=None, duration=None): + """Core Engine: Optimized for UltraSinger 3.10 Sweet Spot.""" + clean_name = Path(input_file).stem + + cmd = [ + 'ffmpeg', '-y', '-hide_banner', '-loglevel', 'error', + '-i', str(input_file) + ] + + if start: + cmd.insert(cmd.index('-i'), '-ss') + cmd.insert(cmd.index('-ss')+1, start) + if duration: + cmd.extend(['-t', str(duration)]) + + # Audio Mapping + cmd.extend(['-map', '0:a']) + + # Cover Art Logic + if include_cover: + cmd.extend(['-map', '0:v?', '-c:v', 'copy']) # Copy video/image stream if exists + + cmd.extend([ + '-map_metadata', '0', # Copy original tags + '-id3v2_version', '3', # UltraSinger 3.10 sweet spot + '-metadata', f'title={clean_name}', + '-b:a', bitrate, # Flexible Bitrate (128k or Common) + '-ar', '44100', + str(output_file) + ]) + + try: + subprocess.run(cmd, check=True) + return True + except: + return False + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="UltraSinger Multi-Tool Optimizer") + parser.add_argument("-i", "--input", required=True, help="Input File or Directory Tree Root") + parser.add_argument("-o", "--output", required=True, help="Target Output Tree (Gets rid of hard links)") + parser.add_argument("-b", "--bitrate", default="128k", choices=["128k", "192k", "256k", "320k"], help="Bitrate (Default: 128k)") + parser.add_argument("--cover", action="store_true", help="Include Cover Art (Default: Stripped)") + parser.add_argument("--start", help="Start time (HH:MM:SS)") + parser.add_argument("--duration", help="Length in seconds") + + args = parser.parse_args() + + input_root = Path(args.input).resolve() + output_root = Path(args.output).resolve() + + # Determine if we are processing a single file or a tree + files_to_process = [] + if input_root.is_file(): + files_to_process.append(input_root) + else: + # Recursive walk to find audio files + extensions = ('.mp3', '.wav', '.flac', '.m4a') + files_to_process = [f for f in input_root.rglob('*') if f.suffix.lower() in extensions] + + print(f"šŸš€ Processing {len(files_to_process)} tracks into {output_root}...") + + for f in files_to_process: + # Maintain Tree Structure: Calculate relative path from input root + relative_path = f.relative_to(input_root if input_root.is_dir() else input_root.parent) + target_path = output_root / relative_path.with_suffix('.mp3') + + # Create the sub-directories in the new tree + target_path.parent.mkdir(parents=True, exist_ok=True) + + print(f" Converting: {relative_path}") + success = run_ffmpeg(f, target_path, args.bitrate, args.cover, args.start, args.duration) + + if not success: + print(f"āŒ Failed: {f.name}") + + print(f"\nāœ… Tree Migration Complete. Files located in: {output_root}") From 92306a71388c7d0dd31f4a5da810759c34d00c64 Mon Sep 17 00:00:00 2001 From: RobUmf Date: Sat, 24 Jan 2026 10:10:38 -0600 Subject: [PATCH 07/18] Update portableMP3.md with flags Notice I didn't add the flag options --- tools/portableMP3.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tools/portableMP3.md b/tools/portableMP3.md index d91d5c3..245bfaa 100644 --- a/tools/portableMP3.md +++ b/tools/portableMP3.md @@ -48,7 +48,15 @@ The `--cover` flag has been specifically tuned for hardware compatibility. | **Sample Rate** | `44100 Hz` | Standard for internal DACs on mobile devices. | | **Mapping** | `0:v?` | Non-blocking; only attempts to copy art if the stream exists. | +--- +Flag,Option / Format,Default,Description +"-i, --input",[path],Required,The source file or the root directory of the music tree you want to convert. +"-o, --output",[path],Required,The target directory where the optimized MP3 tree will be built. +"-b, --bitrate","128k, 192k, 256k, 320k",128k,Sets audio compression. 128k is recommended for hardware like Fire Sticks to save RAM. +--cover,None (Flag),Stripped,"If included, the tool attempts to copy album art into the new file using ID3v2.3." +--start,HH:MM:SS,None,"Tells FFmpeg where to begin the conversion (e.g., 00:00:30 to skip intros)." +--duration,[seconds],None,Limits output length in seconds. Perfect for creating short preview clips for mobile. --- From 554d54368dcc182fc3ada4d254cbcaefd607cd4c Mon Sep 17 00:00:00 2001 From: RobUmf Date: Sat, 31 Jan 2026 12:06:44 -0600 Subject: [PATCH 08/18] Unnecessary comment. Unnecessary comment. OUT --- src/Settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Settings.py b/src/Settings.py index 9919e76..1a974ff 100644 --- a/src/Settings.py +++ b/src/Settings.py @@ -65,7 +65,7 @@ class Settings: # UltraSinger Evaluation Configuration test_songs_input_folder = None - cache_override_path = None # New class definition + cache_override_path = None skip_cache_vocal_separation = False skip_cache_denoise_vocal_audio = False skip_cache_transcription = False From c2a81f9b7ccdb10e05bfb6304e158879c30074bc Mon Sep 17 00:00:00 2001 From: RobUmf Date: Sat, 31 Jan 2026 12:29:32 -0600 Subject: [PATCH 09/18] Clean up comments in UltraSinger.py Should I delete or reduce other commits. --- src/UltraSinger.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/UltraSinger.py b/src/UltraSinger.py index 4d348f7..d80d0ad 100644 --- a/src/UltraSinger.py +++ b/src/UltraSinger.py @@ -444,7 +444,6 @@ def CreateProcessAudio(process_data) -> str: ) os_helper.create_folder(process_data.process_data_paths.cache_folder_path) - # Check if we should separate vocals or bypass separation if settings.use_separated_vocal: audio_separation_folder_path = separate_vocal_from_audio( @@ -459,11 +458,9 @@ def CreateProcessAudio(process_data) -> str: process_data.process_data_paths.vocals_audio_file_path = os.path.join(audio_separation_folder_path, "vocals.wav") process_data.process_data_paths.instrumental_audio_file_path = os.path.join(audio_separation_folder_path, "no_vocals.wav") - # In standard mode, we process the separated vocal file input_path = process_data.process_data_paths.vocals_audio_file_path else: - # Skip separation entirely print(f"{ULTRASINGER_HEAD} {gold_highlighted('Bypass Mode:')} {cyan_highlighted('Skipping Demucs separation.')}") # Point the 'vocal' and 'instrumental' variables to the original input file @@ -478,8 +475,6 @@ def CreateProcessAudio(process_data) -> str: process_data.process_data_paths.cache_folder_path, process_data.basename + "_denoised.wav" ) - - # This ensures it uses the correct file selected in the IF/ELSE block above denoise_vocal_audio(input_path, denoised_output_path, settings.skip_cache_denoise_vocal_audio) # Convert to mono audio From 5bbd60ae9feed2fa4de7a3d3caa4554f7b15ef7c Mon Sep 17 00:00:00 2001 From: RobUmf Date: Sat, 31 Jan 2026 13:07:14 -0600 Subject: [PATCH 10/18] Clean up bypass mode handling in UltraSinger Removed bypass mode print statements and variable assignments for vocals and instrumental paths. How about? # Denoise vocal audio if settings.use_separated_vocal: denoised_output_path = os.path.join( process_data.process_data_paths.cache_folder_path, process_data.basename + "_denoised.wav" ) denoise_vocal_audio(input_path, denoised_output_path, settings.skip_cache_denoise_vocal_audio) # Update input_path to the denoised version for the next step (Pitch Tracking) input_path = denoised_output_path else: # In Bypass Mode, we skip denoising the full song to prevent distortion/overmodulation print(f"{ULTRASINGER_HEAD} {gold_highlighted('Bypass Mode:')} {cyan_highlighted('Skipping vocal denoising.')}") --- src/UltraSinger.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/UltraSinger.py b/src/UltraSinger.py index d80d0ad..9f88924 100644 --- a/src/UltraSinger.py +++ b/src/UltraSinger.py @@ -461,15 +461,8 @@ def CreateProcessAudio(process_data) -> str: input_path = process_data.process_data_paths.vocals_audio_file_path else: - print(f"{ULTRASINGER_HEAD} {gold_highlighted('Bypass Mode:')} {cyan_highlighted('Skipping Demucs separation.')}") - - # Point the 'vocal' and 'instrumental' variables to the original input file - # This prevents "File Not Found" errors later in the script input_path = process_data.process_data_paths.audio_output_file_path - process_data.process_data_paths.vocals_audio_file_path = input_path - process_data.process_data_paths.instrumental_audio_file_path = input_path - - + # Denoise vocal audio denoised_output_path = os.path.join( process_data.process_data_paths.cache_folder_path, process_data.basename + "_denoised.wav" From 623f1cd74485dc775558d6ed5a6d13260fa72603 Mon Sep 17 00:00:00 2001 From: RobUmf Date: Sat, 31 Jan 2026 16:46:08 -0600 Subject: [PATCH 11/18] Add safety checks for audio file conversions Added safety checks for instrumental and vocals audio file paths before conversion. With --disable_separation on going to mp3 Error purposely created. --- src/UltraSinger.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/UltraSinger.py b/src/UltraSinger.py index 9f88924..542d0fd 100644 --- a/src/UltraSinger.py +++ b/src/UltraSinger.py @@ -399,14 +399,23 @@ def CreateUltraStarTxt(process_data: ProcessData): if settings.create_karaoke and version.parse(settings.format_version.value) < version.parse( FormatVersion.V1_1_0.value): karaoke_output_path = os.path.join(settings.output_folder_path, process_data.basename + " [Karaoke].mp3") - convert_wav_to_mp3(process_data.process_data_paths.instrumental_audio_file_path, karaoke_output_path) + + if process_data.process_data_paths.instrumental_audio_file_path: + convert_wav_to_mp3(process_data.process_data_paths.instrumental_audio_file_path, karaoke_output_path) if version.parse(settings.format_version.value) >= version.parse(FormatVersion.V1_1_0.value): instrumental_output_path = os.path.join(settings.output_folder_path, process_data.basename + " [Instrumental].mp3") - convert_wav_to_mp3(process_data.process_data_paths.instrumental_audio_file_path, instrumental_output_path) + + if process_data.process_data_paths.instrumental_audio_file_path: + convert_wav_to_mp3(process_data.process_data_paths.instrumental_audio_file_path, instrumental_output_path) + else: + print(f"{ULTRASINGER_HEAD} Skipping instrumental conversion (Bypass mode).") + vocals_output_path = os.path.join(settings.output_folder_path, process_data.basename + " [Vocals].mp3") - convert_wav_to_mp3(process_data.process_data_paths.vocals_audio_file_path, vocals_output_path) + # Safety Check for Vocals + if process_data.process_data_paths.vocals_audio_file_path: + convert_wav_to_mp3(process_data.process_data_paths.vocals_audio_file_path, vocals_output_path) # Create Ultrastar txt if not settings.ignore_audio: From 7c70f939d4fb088245fedeb343787ed96dab7f46 Mon Sep 17 00:00:00 2001 From: RobUmf Date: Sat, 31 Jan 2026 18:50:52 -0600 Subject: [PATCH 12/18] Add cache_override_path to Settings.py Was having problem preventing hugging face downloading the bin files --- src/Settings.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Settings.py b/src/Settings.py index 1c98d82..472b55f 100644 --- a/src/Settings.py +++ b/src/Settings.py @@ -59,6 +59,7 @@ class Settings: # MuseScore musescore_path = None + cache_override_path = "" # yt-dlp cookiefile = None @@ -70,4 +71,4 @@ class Settings: skip_cache_denoise_vocal_audio = False skip_cache_transcription = False skip_cache_pitch_detection = False - calculate_score = True \ No newline at end of file + calculate_score = True From 1291a79782acf38e8ae9cb9aa0f02a6e33ccd413 Mon Sep 17 00:00:00 2001 From: RobUmf Date: Sat, 31 Jan 2026 19:52:50 -0600 Subject: [PATCH 13/18] Delete tools/sweet_mixer.sh been converted to py --- tools/sweet_mixer.sh | 93 -------------------------------------------- 1 file changed, 93 deletions(-) delete mode 100644 tools/sweet_mixer.sh diff --git a/tools/sweet_mixer.sh b/tools/sweet_mixer.sh deleted file mode 100644 index 1e2e949..0000000 --- a/tools/sweet_mixer.sh +++ /dev/null @@ -1,93 +0,0 @@ -#!/bin/bash -# UltraSinger Audio Profile Mixer - -# Colors -CYAN='\033[0;36m' -GRN='\033[0;32m' -RED='\033[0;31m' -NC='\033[0m' - -echo -e "${CYAN}--------------------------------------------------------" -echo -e "šŸŽšļø ULTRASINGER MIXING DESK" -echo -e "--------------------------------------------------------${NC}" - -# 1. SCAN FOR LOCAL FILES -shopt -s nullglob -FILES=( *.{wav,flac,mp3} ) - -if [ ${#FILES[@]} -gt 0 ]; then - echo -e "${CYAN}Found audio files in this folder:${NC}" - for i in "${!FILES[@]}"; do - echo -e "$((i+1))) ${FILES[$i]}" - done - echo "--------------------------------------------------------" -fi - -# 2. SELECT VOCALS -echo -e "${GRN}āŒØļø Select VOCAL (Number or Path):${NC}" -read -e -r v_raw -if [[ "$v_raw" =~ ^[0-9]+$ ]] && [ "$v_raw" -le "${#FILES[@]}" ]; then - VOCALS="${FILES[$((v_raw-1))]}" -else - v_clean="${v_raw//\'/}" - v_clean="${v_clean//\"/}" - VOCALS="$(echo "${v_clean}" | xargs)" -fi - -# 3. SELECT INSTRUMENTAL -echo -e "${GRN}āŒØļø Select INSTRUMENTAL (Number or Path):${NC}" -read -e -r i_raw -if [[ "$i_raw" =~ ^[0-9]+$ ]] && [ "$i_raw" -le "${#FILES[@]}" ]; then - INSTRUMENTAL="${FILES[$((i_raw-1))]}" -else - i_clean="${i_raw//\'/}" - i_clean="${i_clean//\"/}" - INSTRUMENTAL="$(echo "${i_clean}" | xargs)" -fi - -# 4. PROFILE SELECTION (Moved up so we know the name) -echo -e "${CYAN}--------------------------------------------------------" -echo -e "Select Mixing Profile:${NC}" -echo -e "1) ${GRN}Sing-a-long${NC} (Vocals 1.4x | Inst 1.4x) - Standard" -echo -e "2) ${GRN}Instrumental${NC} (Vocals 0.0x | Inst 1.4x) - No Vocals" -echo -e "3) ${GRN}Guide Track${NC} (Vocals 0.3x | Inst 1.4x) - Quiet Vocals" -read -p ">> Choice [1-3]: " MIX_CHOICE - -case $MIX_CHOICE in - 1) - DEFAULT_NAME="song.mp3" - FILTER="[0:a]volume=1.4[v];[1:a]volume=1.4[i];[v][i]amix=inputs=2:duration=first:dropout_transition=0:normalize=0[aout]" - ;; - 2) - DEFAULT_NAME="instrumental.mp3" - FILTER="[0:a]volume=0.0[v];[1:a]volume=1.4[i];[v][i]amix=inputs=2:duration=first:dropout_transition=0:normalize=0[aout]" - ;; - 3) - DEFAULT_NAME="guide.mp3" - FILTER="[0:a]volume=0.3[v];[1:a]volume=1.4[i];[v][i]amix=inputs=2:duration=first:normalize=0[aout]" - ;; - *) exit 1 ;; -esac - -# 5. FILENAME PROMPT (Now pre-filled with the choice) -echo -e "${CYAN}--------------------------------------------------------${NC}" -echo -e "${GRN}Filename:${NC}" -# This will now show the default name ready for you to hit Enter -read -e -i "$DEFAULT_NAME" -r OUT_FILE - -# 6. COMMAND REVIEW -BASE_CMD="ffmpeg -i \"$VOCALS\" -i \"$INSTRUMENTAL\" -filter_complex \"$FILTER\" -map \"[aout]\" -b:a 320k \"$OUT_FILE\"" - -echo -e "${CYAN}-----------------------------------------------------------" -echo -e "šŸ› ļø FINAL CHECK: Edit command if needed." -echo -e "-----------------------------------------------------------${NC}" - -read -e -i "$BASE_CMD" -p "šŸš€ RUN: " FINAL_CMD - -eval "$FINAL_CMD" - -if [ $? -eq 0 ]; then - echo -e "${GRN}āœ… Success! Saved to: ./$OUT_FILE${NC}" -else - echo -e "${RED}āŒ Failed.${NC}" -fi From 1efc8d1b557f46f01a1b85d14579fc8b6d6880ce Mon Sep 17 00:00:00 2001 From: RobUmf Date: Sat, 31 Jan 2026 19:54:05 -0600 Subject: [PATCH 14/18] Delete tools/sweet_mixer.txt Changed to MD --- tools/sweet_mixer.txt | 56 ------------------------------------------- 1 file changed, 56 deletions(-) delete mode 100644 tools/sweet_mixer.txt diff --git a/tools/sweet_mixer.txt b/tools/sweet_mixer.txt deleted file mode 100644 index 0547bc1..0000000 --- a/tools/sweet_mixer.txt +++ /dev/null @@ -1,56 +0,0 @@ -============================================================ -ULTRASINGER SWEET MIXER: Audio & Volume Optimization Guide -============================================================ -Fix for Overmodulation, Quiet Output, and Pitch Tracking - -This tool implements "Sweet Spot" mixing logic to handle -high-fidelity 32-bit float stems produced by UltraSinger. - ------------------------------------------------------------- -1. THE "SWEET SPOT" LOGIC ------------------------------------------------------------- -* 32-bit Float Advantage: Stems often peak low (high headroom). - Standard mixes sound "thin." We apply a 1.4x boost to - "rescue" the audio for professional levels. - -* Why normalize=0?: Standard FFmpeg 'amix' lowers volume to - prevent clipping. We bypass this to manually restore amplitude. - -* Improved Pitch: Boosting vocal amplitude helps engines like - Crepe/RMVPE track notes more accurately for the .txt output. - ------------------------------------------------------------- -2. HOW TO RUN ------------------------------------------------------------- -Permissions: chmod +x sweet_mixer.sh -Execution: bash tools/sweet_mixer.sh - -The script provides 3 ways to select files: -1) AUTO-INDEX: Run the script in the song folder to pick - files by number. -2) DRAG & DROP: Drag audio files directly into the terminal. -3) DIRECT PATH: Enter the path manually. - ------------------------------------------------------------- -3. MIXING PROFILES ------------------------------------------------------------- -Choose your profile after selecting files: - -| Profile | Vocal Vol | Inst. Vol | Use Case | -|-----------------|-----------|-----------|------------------| -| 1. Sing-a-long | 1.4x | 1.4x | Standard Karaoke | -| 2. Instrumental | 0.0x | 1.4x | No Vocals | -| 3. Guide Track | 0.3x | 1.4x | Practice / Pitch | - -Logic: The script defaults to 'song.mp3', 'instrumental.mp3', -or 'guide.mp3' unless you type a custom name. - ------------------------------------------------------------- -4. MANUAL CLI REFERENCE ------------------------------------------------------------- -Template for manual execution: - -ffmpeg -i vocals.wav -i no_vocals.wav -filter_complex \ -"[0:a]volume=1.4[v];[1:a]volume=1.4[i];[v][i]amix=inputs=2:duration=first:dropout_transition=0:normalize=0[aout]" \ --map "[aout]" -b:a 320k "song.mp3" -============================================================ From 3fe132e6e1f9a0a8b594b4eb44cd40504b22b28f Mon Sep 17 00:00:00 2001 From: RobUmf Date: Sat, 31 Jan 2026 19:59:09 -0600 Subject: [PATCH 15/18] Delete tools directory Moving to different branch --- tools/portableMP3.md | 67 ---------------- tools/portableMP3.py | 82 -------------------- tools/sweet_mixer.md | 48 ------------ tools/sweet_mixer.py | 180 ------------------------------------------- 4 files changed, 377 deletions(-) delete mode 100644 tools/portableMP3.md delete mode 100644 tools/portableMP3.py delete mode 100644 tools/sweet_mixer.md delete mode 100644 tools/sweet_mixer.py diff --git a/tools/portableMP3.md b/tools/portableMP3.md deleted file mode 100644 index 245bfaa..0000000 --- a/tools/portableMP3.md +++ /dev/null @@ -1,67 +0,0 @@ -# UltraSinger Portable MP3 Optimizer - -A high-performance Python utility designed to compress and migrate large music archives for mobile devices, flip phones, and portable storage. Optimized specifically for the **UltraSinger 3.10** ecosystem. - -## šŸ“– Description - -This tool streamlines the process of converting high-bitrate or lossless music archives (FLAC, WAV, M4A) into mobile-friendly 128kbps MP3s. It features intelligent deduplication to clean up "dirty" archives and ensures metadata compatibility with legacy hardware. - -## 🚩 Features - -* **Bitrate Optimization**: Defaults to 128kbps (the "sweet spot" for storage vs. quality on mobile). -* **Intelligent Deduplication**: Automatically detects if files in the root directory match files in subdirectories; prioritizes organized album paths over loose root files. -* **Broad Compatibility**: Uses `ID3v2.3` and `44.1kHz` sampling to ensure album art and audio play correctly on basic handsets (e.g., Coosea SL006D). -* **Flexible Structure**: Supports both mirrored directory trees and flattened outputs. - -## šŸ› ļø Usage - -```bash -python3 portableMP3.py -i -o [FLAGS] - -## šŸ› ļø Flag Documentation & Logic - -### `--flat` (Flatten Output) -The `--flat` flag modifies the destination path logic. Instead of mirroring the source directory tree, it strips all path information and places every converted file directly into the root of the target folder. - -**Use Case:** * Essential for MTP transfers to devices with slow file indexing. -* Required for legacy players/car stereos that do not support nested folders. - -### 🧠 Intelligent Deduplication -The script now includes a "Path-Depth Priority" check to handle redundant files common in large archives (e.g., songs appearing in both the root and an album subfolder). - -**How it works:** -1. Scans the input tree for all compatible audio files. -2. If two files share the same filename, the script calculates the path depth. -3. The file located **deeper** in the directory tree (e.g., inside an Album folder) is prioritized. -4. The redundant "root" version is skipped to save storage and prevent duplicate entries in mobile playlists. - - - ---- - -## šŸŽØ Metadata & Album Art -The `--cover` flag has been specifically tuned for hardware compatibility. - -| Parameter | Value | Reason | -| :--- | :--- | :--- | -| **ID3 Version** | `v2.3` | Fixes "Missing Art" issues on flip phones (SL006D). | -| **Sample Rate** | `44100 Hz` | Standard for internal DACs on mobile devices. | -| **Mapping** | `0:v?` | Non-blocking; only attempts to copy art if the stream exists. | - ---- - -Flag,Option / Format,Default,Description -"-i, --input",[path],Required,The source file or the root directory of the music tree you want to convert. -"-o, --output",[path],Required,The target directory where the optimized MP3 tree will be built. -"-b, --bitrate","128k, 192k, 256k, 320k",128k,Sets audio compression. 128k is recommended for hardware like Fire Sticks to save RAM. ---cover,None (Flag),Stripped,"If included, the tool attempts to copy album art into the new file using ID3v2.3." ---start,HH:MM:SS,None,"Tells FFmpeg where to begin the conversion (e.g., 00:00:30 to skip intros)." ---duration,[seconds],None,Limits output length in seconds. Perfect for creating short preview clips for mobile. - ---- - -## šŸš€ Execution Examples - -**Preserve Tree (Default):** -```bash -python3 portableMP3.py -i "~/Downloads/PonyArchive" -o "/media/nvme/AI_Work/Inputs" --cover diff --git a/tools/portableMP3.py b/tools/portableMP3.py deleted file mode 100644 index 1ff371e..0000000 --- a/tools/portableMP3.py +++ /dev/null @@ -1,82 +0,0 @@ -import os -import argparse -import subprocess -from pathlib import Path - -def run_ffmpeg(input_file, output_file, bitrate, include_cover, start=None, duration=None): - """Core Engine: Optimized for UltraSinger 3.10 Sweet Spot.""" - clean_name = Path(input_file).stem - - cmd = [ - 'ffmpeg', '-y', '-hide_banner', '-loglevel', 'error', - '-i', str(input_file) - ] - - if start: - cmd.insert(cmd.index('-i'), '-ss') - cmd.insert(cmd.index('-ss')+1, start) - if duration: - cmd.extend(['-t', str(duration)]) - - # Audio Mapping - cmd.extend(['-map', '0:a']) - - # Cover Art Logic - if include_cover: - cmd.extend(['-map', '0:v?', '-c:v', 'copy']) # Copy video/image stream if exists - - cmd.extend([ - '-map_metadata', '0', # Copy original tags - '-id3v2_version', '3', # UltraSinger 3.10 sweet spot - '-metadata', f'title={clean_name}', - '-b:a', bitrate, # Flexible Bitrate (128k or Common) - '-ar', '44100', - str(output_file) - ]) - - try: - subprocess.run(cmd, check=True) - return True - except: - return False - -if __name__ == "__main__": - parser = argparse.ArgumentParser(description="UltraSinger Multi-Tool Optimizer") - parser.add_argument("-i", "--input", required=True, help="Input File or Directory Tree Root") - parser.add_argument("-o", "--output", required=True, help="Target Output Tree (Gets rid of hard links)") - parser.add_argument("-b", "--bitrate", default="128k", choices=["128k", "192k", "256k", "320k"], help="Bitrate (Default: 128k)") - parser.add_argument("--cover", action="store_true", help="Include Cover Art (Default: Stripped)") - parser.add_argument("--start", help="Start time (HH:MM:SS)") - parser.add_argument("--duration", help="Length in seconds") - - args = parser.parse_args() - - input_root = Path(args.input).resolve() - output_root = Path(args.output).resolve() - - # Determine if we are processing a single file or a tree - files_to_process = [] - if input_root.is_file(): - files_to_process.append(input_root) - else: - # Recursive walk to find audio files - extensions = ('.mp3', '.wav', '.flac', '.m4a') - files_to_process = [f for f in input_root.rglob('*') if f.suffix.lower() in extensions] - - print(f"šŸš€ Processing {len(files_to_process)} tracks into {output_root}...") - - for f in files_to_process: - # Maintain Tree Structure: Calculate relative path from input root - relative_path = f.relative_to(input_root if input_root.is_dir() else input_root.parent) - target_path = output_root / relative_path.with_suffix('.mp3') - - # Create the sub-directories in the new tree - target_path.parent.mkdir(parents=True, exist_ok=True) - - print(f" Converting: {relative_path}") - success = run_ffmpeg(f, target_path, args.bitrate, args.cover, args.start, args.duration) - - if not success: - print(f"āŒ Failed: {f.name}") - - print(f"\nāœ… Tree Migration Complete. Files located in: {output_root}") diff --git a/tools/sweet_mixer.md b/tools/sweet_mixer.md deleted file mode 100644 index c8ff15a..0000000 --- a/tools/sweet_mixer.md +++ /dev/null @@ -1,48 +0,0 @@ -# UltraSinger Sweet Mixer: Audio & Volume Optimization Guide - -**Fix for Overmodulation, Quiet Output, and Pitch Tracking** - -This tool implements "Sweet Spot" mixing logic to handle high-fidelity 32-bit float stems produced by UltraSinger. - -## 1. The "Sweet Spot" Logic - -* **32-bit Float Advantage:** Stems often peak low (high headroom). Standard mixes sound "thin." We apply a **1.2x boost** to "rescue" the audio for professional levels without clipping. -* **Why normalize=0?** Standard FFmpeg `amix` lowers volume to prevent clipping. We bypass this to manually restore amplitude. -* **Improved Pitch:** Boosting vocal amplitude helps engines like Crepe/RMVPE track notes more accurately for the `.txt` output. - -## 2. How to Run - -The script provides 3 ways to select files: - -1) Auto-Index: Run the script in the song folder to pick files by number. - -2) Drag & Drop: Drag audio files directly into the terminal. - -3) Direct Path: Enter the path manually. - -**Permissions:** -```bash -chmod +x sweet_mixer.py - -python tools/sweet_mixer.py -# OR -./tools/sweet_mixer.py -# 0r if copy to the files for index -./sweet_mixer.py - -Logic: The script defaults to song.mp3, instrumental.mp3, or guide.mp3 unless you type a custom name. - -| **Karaoke** | 1.2x | 1.2x | Standard Sing-a-long | -| **Instrumental** | 0.0x | 1.2x | No Vocals | -| **Guide Track** | 0.3x | 1.2x | Practice / Pitch | - - -4. Manual CLI Reference -Template for manual execution (if not using the script): - -ffmpeg -i vocals.wav -i no_vocals.wav -filter_complex \ -"[0:a]volume=1.2[v];[1:a]volume=1.2[i];[v][i]amix=inputs=2:duration=first:dropout_transition=0:normalize=0[aout]" \ --map "[aout]" -b:a 320k "song.mp3" - - -RobUmf with Gemini 3 for UltraSinger diff --git a/tools/sweet_mixer.py b/tools/sweet_mixer.py deleted file mode 100644 index 5186feb..0000000 --- a/tools/sweet_mixer.py +++ /dev/null @@ -1,180 +0,0 @@ -#!/usr/bin/env python3 -import os -import subprocess -import glob -import sys -import readline -import platform -import shutil - -# Colors -CYAN = '\033[0;36m' -GRN = '\033[0;32m' -RED = '\033[0;31m' -NC = '\033[0m' - -# Setup readline for better input editing -readline.parse_and_bind('set editing-mode emacs') - -def get_safe_executable(name): - """Finds the absolute path of an executable to prevent path hijacking.""" - exe_path = shutil.which(name) - if not exe_path: - print(f"{RED}Error: {name} is not installed or not in PATH.{NC}") - sys.exit(1) - return exe_path - -def check_dependencies(): - """Ensure ffmpeg is available and return its absolute path.""" - return get_safe_executable('ffmpeg') - -def get_audio_files(): - """Scan directory for common audio formats.""" - extensions = ['*.wav', '*.flac', '*.mp3', '*.m4a', '*.ogg'] - files = [] - for ext in extensions: - files.extend(glob.glob(ext)) - return sorted(files) - -def clean_path(path): - """Remove quotes from drag-and-dropped paths.""" - return path.strip().replace("'", "").replace('"', "") - -def play_file(filepath): - """Opens the file in the default system player using absolute paths.""" - if not os.path.exists(filepath): - print(f"{RED}File not found: {filepath}{NC}") - return - - print(f"{CYAN}šŸŽ§ Playing: {filepath}{NC}") - current_os = platform.system() - abs_filepath = os.path.abspath(filepath) - - try: - if current_os == 'Windows': - # os.startfile is secure as it doesn't invoke a shell - os.startfile(abs_filepath) - elif current_os == 'Darwin': # macOS - cmd = get_safe_executable('open') - subprocess.run([cmd, abs_filepath], check=True) - else: # Linux - cmd = get_safe_executable('xdg-open') - subprocess.run([cmd, abs_filepath], check=True) - except Exception as e: - print(f"{RED}Could not open player: {e}{NC}") - -def select_input(prompt_text, file_list): - """Helper to handle selecting a file by number or path.""" - while True: - raw = input(f"{GRN}āŒØļø {prompt_text} (Number or Path):{NC} ").strip() - if not raw: - continue - - if raw.isdigit(): - idx = int(raw) - 1 - if 0 <= idx < len(file_list): - return file_list[idx] - else: - print(f"{RED}Invalid number.{NC}") - continue - - clean = clean_path(raw) - if os.path.exists(clean): - return clean - else: - print(f"{RED}File path not found.{NC}") - -def main(): - ffmpeg_bin = check_dependencies() - print(f"{CYAN}--------------------------------------------------------") - print("šŸŽšļø ULTRASINGER MIXING DESK & PREVIEW") - print(f"--------------------------------------------------------{NC}") - - files = get_audio_files() - if files: - print(f"{CYAN}Found audio files:{NC}") - for i, file in enumerate(files): - print(f"{i+1}) {file}") - print("--------------------------------------------------------") - else: - print(f"{RED}No audio files found in current directory.{NC}") - - vocals = select_input("Select VOCAL", files) - instrumental = select_input("Select INSTRUMENTAL", files) - - while True: - print(f"\n{CYAN}Select Action:{NC}") - print(f"1) {GRN}Karaoke{NC} (Vocals 1.2x | Inst 1.2x)") - print(f"2) {GRN}Instrumental{NC} (Vocals 0.0x | Inst 1.2x)") - print(f"3) {GRN}Guide Track{NC} (Vocals 0.3x | Inst 1.2x)") - print(f"4) {CYAN}Preview Vocals{NC}") - print(f"5) {CYAN}Preview Instrumental{NC}") - print(f"6) {RED}Exit{NC}") - - choice = input(">> Choice [1-6]: ").strip() - - match choice: - case "4": - play_file(vocals) - continue - case "5": - play_file(instrumental) - continue - case "6": - print("Bye!") - sys.exit(0) - case "1": - config = {"name": "song.mp3", "v": "1.2", "i": "1.2"} - case "2": - config = {"name": "instrumental.mp3", "v": "0.0", "i": "1.2"} - case "3": - config = {"name": "guide.mp3", "v": "0.3", "i": "1.2"} - case _: - print(f"{RED}Invalid selection.{NC}") - continue - - out_file = input(f"{GRN}Filename (Enter for '{config['name']}'):{NC} ") or config['name'] - - filter_complex = ( - f"[0:a]volume={config['v']}[v];" - f"[1:a]volume={config['i']}[i];" - f"[v][i]amix=inputs=2:duration=first:dropout_transition=0:normalize=0[aout]" - ) - - # Build command as a list to avoid shell=True - cmd_args = [ - ffmpeg_bin, "-y", - "-i", vocals, - "-i", instrumental, - "-filter_complex", filter_complex, - "-map", "[aout]", - "-b:a", "320k", - out_file - ] - - mixing = True - while mixing: - print(f"\n{CYAN}-----------------------------------------------------------") - print(f"šŸ› ļø PROPOSED COMMAND:") - print(f"{NC}{' '.join(cmd_args)}") - print(f"-----------------------------------------------------------") - - user_choice = input(f"Type {GRN}'run'{NC}, {CYAN}'play'{NC} (result), {CYAN}'back'{NC} or {RED}'exit'{NC}: ").lower().strip() - - if user_choice == 'run': - try: - print(f"{CYAN}Rendering...{NC}") - # shell=False is used by default when passing a list - subprocess.run(cmd_args, check=True) - print(f"{GRN}āœ… Success! Saved to: ./{out_file}{NC}") - except subprocess.CalledProcessError: - print(f"{RED}āŒ FFmpeg failed.{NC}") - elif user_choice == 'play': - play_file(out_file) - elif user_choice == 'back': - mixing = False - elif user_choice == 'exit': - sys.exit(0) - -if __name__ == "__main__": - main() From c0cf7ef2b18d068cad502e99ffa61e2f96b92989 Mon Sep 17 00:00:00 2001 From: RobUmf Date: Sat, 31 Jan 2026 20:16:08 -0600 Subject: [PATCH 16/18] cache_override_path twice Deleted bottom one --- src/Settings.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Settings.py b/src/Settings.py index 472b55f..ec01613 100644 --- a/src/Settings.py +++ b/src/Settings.py @@ -66,7 +66,6 @@ class Settings: # UltraSinger Evaluation Configuration test_songs_input_folder = None - cache_override_path = None skip_cache_vocal_separation = False skip_cache_denoise_vocal_audio = False skip_cache_transcription = False From 0ecb206d2e13494aa25f6b6962a84a8112ce6fe2 Mon Sep 17 00:00:00 2001 From: RobUmf Date: Mon, 9 Feb 2026 09:47:07 -0600 Subject: [PATCH 17/18] Remove A.I Commits --- src/UltraSinger.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/UltraSinger.py b/src/UltraSinger.py index 542d0fd..6e5bb84 100644 --- a/src/UltraSinger.py +++ b/src/UltraSinger.py @@ -701,7 +701,7 @@ def init_settings(argv: list[str]) -> Settings: settings.keep_cache = True elif opt in ("--musescore_path"): settings.musescore_path = arg - elif opt in ("--cache_override_path"): # <--- ADD THIS BLOCK + elif opt in ("--cache_override_path"): settings.cache_override_path = arg #Addition of demucs model choice. Work seems to be needed to make sure syntax is same for models. Added error handling for unknown models elif opt in ("--demucs"): @@ -753,7 +753,7 @@ def arg_options(): "format_version=", "keep_cache", "musescore_path=", - "cache_override_path=", # Added this line so the flag is recognized + "cache_override_path=", "keep_numbers", "interactive", "cookiefile=", From 3f072ffb8d52c9b7a319f2928d6d444912d05b04 Mon Sep 17 00:00:00 2001 From: RobUmf Date: Mon, 9 Feb 2026 10:00:33 -0600 Subject: [PATCH 18/18] cache_override_path from empty string to None Original idea is better. --- src/Settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Settings.py b/src/Settings.py index ec01613..1a974ff 100644 --- a/src/Settings.py +++ b/src/Settings.py @@ -59,13 +59,13 @@ class Settings: # MuseScore musescore_path = None - cache_override_path = "" # yt-dlp cookiefile = None # UltraSinger Evaluation Configuration test_songs_input_folder = None + cache_override_path = None skip_cache_vocal_separation = False skip_cache_denoise_vocal_audio = False skip_cache_transcription = False