From eaf6534c2285f15f7f92749fd2e00db14ba4bb74 Mon Sep 17 00:00:00 2001 From: Kevin Zakka Date: Sun, 19 Mar 2023 12:49:55 -0700 Subject: [PATCH] Add more soundfont utilities to the CLI. --- README.md | 16 ++-- docs/soundfonts.md | 8 +- robopianist/__init__.py | 35 +++++--- robopianist/cli.py | 182 +++++++++++++++++++++++++++++++++++----- setup.py | 2 + 5 files changed, 198 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index 62a6e91..4e58313 100644 --- a/README.md +++ b/README.md @@ -86,27 +86,21 @@ pip install --upgrade robopianist ### Optional: Download additional soundfonts -We recommend you install additional soundfonts to improve the quality of the synthesized audio. - -If you installed from source, the easiest way to download the soundfonts is to run the following script: +We recommend you install additional soundfonts to improve the quality of the synthesized audio. The RoboPianist [CLI](#cli) can be used to list out all available soundfonts, change the default soundfont, and download additional ones. To list out all available commands, run: ```bash -bash scripts/get_soundfonts.sh +robopianist soundfont --help ``` -If you installed from PyPI, you can download the soundfonts using the RoboPianist CLI: - -```bash -robopianist --download-soundfonts -``` +For more information, see [docs/soundfonts.md](docs/soundfonts.md). ## MIDI Dataset -The PIG dataset cannot be redistributed on GitHub due to licensing restrictions. See [docs/dataset](docs/dataset.md) for instructions on where to download and how to process the dataset. +The PIG dataset cannot be redistributed on GitHub due to licensing restrictions. See [docs/dataset](docs/dataset.md) for instructions on where to download and how to process the ## CLI -RoboPianist comes with a command line interface (CLI) that can be used to download additional soundfonts, play MIDI files, and more. To see a list of available commands, run `robopianist --help`. +RoboPianist comes with a command line interface (CLI) that can be used to download additional soundfonts, play MIDI files, preprocess the PIG dataset, and more. To see a list of available commands, run `robopianist --help`. ## Contributing diff --git a/docs/soundfonts.md b/docs/soundfonts.md index 81fb1d1..7378026 100644 --- a/docs/soundfonts.md +++ b/docs/soundfonts.md @@ -1,5 +1,9 @@ # Soundfonts +## What is a soundfont? + +## Soundfonts in RoboPianist + 1. `TimGM6mb.sf2` * [Download Link](https://sourceforge.net/p/mscore/code/HEAD/tree/trunk/mscore/share/sound/TimGM6mb.sf2?format=raw) * Creator: [Tim Brechbill](https://timbrechbill.com/saxguru/) @@ -12,7 +16,9 @@ * License: Public Domain (as of 4/3/2022) * Size: 1.27 GB -## Links +## Using custom soundfonts + +## Resources Check out these links for more soundfonts: diff --git a/robopianist/__init__.py b/robopianist/__init__.py index 07bf57f..eaae9f4 100644 --- a/robopianist/__init__.py +++ b/robopianist/__init__.py @@ -19,20 +19,33 @@ # Path to the root of the project. _PROJECT_ROOT = Path(__file__).parent.parent -# Path to the soundfont SF2 file. +# Path to the soundfont directory. _SOUNDFONT_PATH = _PROJECT_ROOT / "robopianist" / "soundfonts" + _DEFAULT_SF2_PATH = _SOUNDFONT_PATH / "TimGM6mb.sf2" -_SALAMANDER_SF2_PATH = _SOUNDFONT_PATH / "SalamanderGrandPiano.sf2" -if _SALAMANDER_SF2_PATH.exists(): - SF2_PATH = _SALAMANDER_SF2_PATH + +_RC_FILE = Path.home() / ".robopianistrc" +if _RC_FILE.exists(): + with open(_RC_FILE, "r") as f: + for line in f: + if line.startswith("DEFAULT_SOUNDFONT="): + soundfont_path = line.split("=")[1].strip() + SF2_PATH = _SOUNDFONT_PATH / f"{soundfont_path}.sf2" + if not SF2_PATH.exists(): + SF2_PATH = _DEFAULT_SF2_PATH + break else: - if not _DEFAULT_SF2_PATH.exists(): - raise FileNotFoundError( - f"The default soundfont file {_DEFAULT_SF2_PATH} does not exist. Make " - "sure you have first run `bash scripts/install_deps.sh` in the root of " - "the project directory." - ) - SF2_PATH = _DEFAULT_SF2_PATH + _SALAMANDER_SF2_PATH = _SOUNDFONT_PATH / "SalamanderGrandPiano.sf2" + if _SALAMANDER_SF2_PATH.exists(): + SF2_PATH = _SALAMANDER_SF2_PATH + else: + if not _DEFAULT_SF2_PATH.exists(): + raise FileNotFoundError( + f"The default soundfont file {_DEFAULT_SF2_PATH} does not exist. Make " + "sure you have first run `bash scripts/install_deps.sh` in the root of " + "the project directory." + ) + SF2_PATH = _DEFAULT_SF2_PATH __all__ = [ diff --git a/robopianist/cli.py b/robopianist/cli.py index 0587018..8b9e2c3 100644 --- a/robopianist/cli.py +++ b/robopianist/cli.py @@ -22,15 +22,26 @@ import pandas as pd import pretty_midi +import requests from note_seq.protobuf import music_pb2 +from termcolor import cprint +from tqdm import tqdm import robopianist from robopianist.music import midi_file +# Dataset variables. _DEFAULT_SAVE_DIR = ( robopianist._PROJECT_ROOT / "robopianist" / "music" / "data" / "pig_single_finger" ) +# Soundfont variables. +_SOUNDFONT_DIR = robopianist._PROJECT_ROOT / "robopianist" / "soundfonts" +_SOUNDFONTS = { + "TimGM6mb": "https://sourceforge.net/p/mscore/code/HEAD/tree/trunk/mscore/share/sound/TimGM6mb.sf2?format=raw", + "SalamanderGrandPiano": "https://freepats.zenvoid.org/Piano/SalamanderGrandPiano/SalamanderGrandPiano-SF2-V3+20200602.tar.xz", +} + def main() -> None: parser = argparse.ArgumentParser() @@ -41,12 +52,6 @@ def main() -> None: help="print the version of robopianist.", ) - parser.add_argument( - "--download-soundfonts", - action="store_true", - help="download additional soundfonts.", - ) - parser.add_argument( "--check-pig-exists", action="store_true", @@ -77,33 +82,36 @@ def main() -> None: help="Where to save the processed proto files.", ) + soundfont_parser = subparsers.add_parser("soundfont") + soundfont_parser.add_argument( + "--list", + action="store_true", + help="List all available soundfonts.", + ) + soundfont_parser.add_argument( + "--change-default", + action="store_true", + help="Change the default soundfont.", + ) + soundfont_parser.add_argument( + "--download", + action="store_true", + help="Download a soundfont.", + ) + args = parser.parse_args() if args.version: print(f"robopianist {robopianist.__version__}") return - if args.download_soundfonts: - # Download soundfonts. - script = robopianist._PROJECT_ROOT / "scripts" / "get_soundfonts.sh" - subprocess.run(["bash", str(script)], check=True) - - # Copy soundfonts to robopianist directory. - dst_dir = robopianist._PROJECT_ROOT / "robopianist" / "soundfonts" - dst_dir.mkdir(parents=True, exist_ok=True) - src_dir = robopianist._PROJECT_ROOT / "third_party" / "soundfonts" - for file in src_dir.glob("*.sf2"): - shutil.copy(file, dst_dir / file.name) - - return - if args.check_pig_exists: from robopianist import music if len(music.PIG_MIDIS) != 150: - raise ValueError("PIG dataset was not properly downloaded and processed.") + cprint("PIG dataset was not properly downloaded and processed.", "red") else: - print("PIG dataset is ready to use!") + cprint("PIG dataset is ready to use!", "green") return if args.subparser_name == "player": @@ -116,6 +124,67 @@ def main() -> None: _preprocess_pig(Path(args.dataset_dir), Path(args.save_dir)) return + if args.subparser_name == "soundfont": + if args.list: + sf2s = _SOUNDFONT_DIR.glob("*.sf2") + print("Available soundfonts:") + for sf2 in sf2s: + print(f" {sf2.name}") + return + + if args.change_default: + _change_default_soundfont() + return + + if args.download: + _download_soundfont() + return + + return + + +def _set_default_soundfont(name: str) -> None: + # Create a .robopianistrc file if it doesn't exist. + rc_file = Path.home() / ".robopianistrc" + if not rc_file.exists(): + rc_file.touch() + + # Check that the soundfont exists. + soundfont = _SOUNDFONT_DIR / f"{name}.sf2" + if not soundfont.exists(): + cprint(f"The soundfont {name} does not exist.", "red") + return + + # Set the default soundfont. + with open(rc_file, "w") as f: + f.write(f"DEFAULT_SOUNDFONT={name}") + + cprint(f"Default soundfont set to {name}.", "green") + + +def _change_default_soundfont() -> None: + # Get a list of available soundfonts. + sf2s = _SOUNDFONT_DIR.glob("*.sf2") + soundfonts = [sf2.stem for sf2 in sf2s] + is_default = [sf2 == robopianist.SF2_PATH.stem for sf2 in soundfonts] + + print("Available soundfonts:") + for i, soundfont in enumerate(soundfonts): + print(f" ({i}) {soundfont} {'(default)' if is_default[i] else ''}") + + # Get the user's choice. + choice = input("Enter the soundfont you want to use: ") + try: + number = int(choice) + if number < 0 or number >= len(soundfonts): + raise ValueError + except ValueError: + cprint("Invalid choice.", "red") + return + + # Set the default soundfont. + _set_default_soundfont(soundfonts[number]) + @dataclass class Line: @@ -208,3 +277,72 @@ def _preprocess_pig(dataset_dir: Path, save_dir: Path) -> None: # Save proto file. filename = save_dir / f"{piece}-{number}.proto" midi_file.MidiFile(seq).save(filename) + + +def _download_file(url): + chunk_size = 1024 + r = requests.get(url, stream=True) + total_size = int(r.headers.get("content-length", 0)) + pbar = tqdm(total=total_size, unit="B", unit_scale=True) + with open(url.split("/")[-1], "wb") as f: + for chunk in r.iter_content(chunk_size=chunk_size): + if chunk: + f.write(chunk) + pbar.update(len(chunk)) + + +def _download_soundfont() -> None: + soundfont_names = list(_SOUNDFONTS.keys()) + + is_downloaded = {} + for sf2 in soundfont_names: + if (_SOUNDFONT_DIR / f"{sf2}.sf2").exists(): + is_downloaded[sf2] = True + else: + is_downloaded[sf2] = False + + print("Which soundfont would you like to download?") + for i, soundfont in enumerate(_SOUNDFONTS.keys()): + print( + f" ({i}) {soundfont} ({'downloaded' if is_downloaded[soundfont] else 'not downloaded'})" + ) + + # Get the user's choice. + choice = input("Enter the soundfont you want to download: ") + try: + number = int(choice) + if number < 0 or number >= len(_SOUNDFONTS): + raise ValueError + except ValueError: + cprint("Invalid choice.", "red") + return + + # Download the soundfont. + url = _SOUNDFONTS[soundfont_names[number]] + _download_file(url) + + # Custom extraction for each soundfont. + if soundfont_names[number] == "TimGM6mb": + shutil.move("TimGM6mb.sf2?format=raw", _SOUNDFONT_DIR / "TimGM6mb.sf2") + else: + subprocess.run( + [ + "tar", + "-xvf", + "SalamanderGrandPiano-SF2-V3+20200602.tar.xz", + ], + check=True, + stdout=subprocess.DEVNULL, + ) + shutil.move( + "SalamanderGrandPiano-SF2-V3+20200602/SalamanderGrandPiano-V3+20200602.sf2", + _SOUNDFONT_DIR / "SalamanderGrandPiano.sf2", + ) + subprocess.run( + [ + "rm", + "-r", + "SalamanderGrandPiano-SF2-V3+20200602.tar.xz", + "SalamanderGrandPiano-SF2-V3+20200602", + ], + ) diff --git a/setup.py b/setup.py index d5d9def..2210dde 100644 --- a/setup.py +++ b/setup.py @@ -43,6 +43,8 @@ "pyaudio >= 0.2.12", "pyfluidsynth >= 1.3.2", "scikit-learn", + "termcolor>=2.2.0", + "tqdm>=4.65.0", ] test_requirements = [