Created
May 14, 2026 16:26
-
-
Save tooh/ddad0509a27262fed974552c8a836639 to your computer and use it in GitHub Desktop.
normalize_stems
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """ | |
| normalize_stems.py | |
| Normaliseert Logic Pro stem-bestandsnamen en prefix ze met een tracknummer | |
| voor gebruik als song tracks in Helix Stadium. | |
| Verwacht patroon: <Songtitel> (<Instrument>)_1.wav | |
| Output patroon: 01_Drums.wav, 02_Bass.wav, etc. | |
| Gebruik: | |
| python3 normalize_stems.py <map_met_stems> | |
| python3 normalize_stems.py <map_met_stems> --dry-run | |
| python3 normalize_stems.py <map_met_stems> --copy <doelmap> | |
| """ | |
| import argparse | |
| import re | |
| import shutil | |
| import sys | |
| from pathlib import Path | |
| # ── Pas hier de volgorde aan ────────────────────────────────────────────────── | |
| TRACK_ORDER = [ | |
| "Drums", | |
| "Bass", | |
| "Piano", | |
| "Guitar", | |
| "Other", | |
| "Vocals", | |
| ] | |
| # ───────────────────────────────────────────────────────────────────────────── | |
| AUDIO_EXTENSIONS = {".wav", ".aif", ".aiff", ".flac", ".ogg"} | |
| def parse_stem_name(filename: str) -> str | None: | |
| """ | |
| Extraheert de instrumentnaam uit een Logic Pro stem-bestandsnaam. | |
| Verwacht patroon: <Songtitel> (<Instrument>)_1.wav | |
| Geeft de instrumentnaam terug, of None als het patroon niet matcht. | |
| """ | |
| stem = Path(filename).stem # zonder extensie | |
| match = re.search(r"\(([^)]+)\)(?:_\d+)?$", stem) | |
| if match: | |
| return match.group(1).strip() | |
| return None | |
| def track_number(instrument: str) -> int | None: | |
| """Geeft het 1-based tracknummer voor een instrumentnaam (case-insensitief).""" | |
| for i, name in enumerate(TRACK_ORDER, start=1): | |
| if name.lower() == instrument.lower(): | |
| return i | |
| return None | |
| def build_new_name(instrument: str, number: int, extension: str) -> str: | |
| # Normaliseer de instrumentnaam naar de spelling in TRACK_ORDER | |
| canonical = next(n for n in TRACK_ORDER if n.lower() == instrument.lower()) | |
| return f"{number:02d}_{canonical}{extension}" | |
| def process(source_dir: Path, dest_dir: Path | None, dry_run: bool) -> None: | |
| files = sorted( | |
| f for f in source_dir.iterdir() | |
| if f.is_file() and f.suffix.lower() in AUDIO_EXTENSIONS | |
| ) | |
| if not files: | |
| print(f"Geen audiobestanden gevonden in: {source_dir}") | |
| sys.exit(1) | |
| renames: list[tuple[Path, str]] = [] | |
| warnings: list[str] = [] | |
| for f in files: | |
| instrument = parse_stem_name(f.name) | |
| if instrument is None: | |
| warnings.append(f" ⚠️ Patroon niet herkend, overgeslagen: {f.name}") | |
| continue | |
| number = track_number(instrument) | |
| if number is None: | |
| warnings.append( | |
| f" ⚠️ Instrument '{instrument}' niet in TRACK_ORDER, overgeslagen: {f.name}" | |
| ) | |
| continue | |
| new_name = build_new_name(instrument, number, f.suffix.lower()) | |
| renames.append((f, new_name)) | |
| if warnings: | |
| print("Waarschuwingen:") | |
| for w in warnings: | |
| print(w) | |
| print() | |
| if not renames: | |
| print("Niets te doen.") | |
| return | |
| action = "Kopieer" if dest_dir else "Hernoem" | |
| print(f"{'[DRY-RUN] ' if dry_run else ''}{action} {len(renames)} bestand(en):\n") | |
| target_dir = dest_dir or source_dir | |
| for src, new_name in sorted(renames, key=lambda x: x[1]): | |
| dest = target_dir / new_name | |
| print(f" {src.name}") | |
| print(f" → {new_name}") | |
| if not dry_run: | |
| if dest.exists(): | |
| print(f" ⚠️ Bestaat al, overgeslagen: {dest}") | |
| continue | |
| if dest_dir: | |
| dest_dir.mkdir(parents=True, exist_ok=True) | |
| shutil.copy2(src, dest) | |
| else: | |
| src.rename(dest) | |
| if dry_run: | |
| print("\n(Dry-run: geen bestanden gewijzigd. Verwijder --dry-run om uit te voeren.)") | |
| else: | |
| print(f"\n✓ Klaar. Bestanden staan in: {target_dir}") | |
| def main() -> None: | |
| parser = argparse.ArgumentParser( | |
| description="Normaliseert Logic Pro stem-namen voor Helix Stadium." | |
| ) | |
| parser.add_argument("source", type=Path, help="Map met de geëxporteerde stems") | |
| parser.add_argument( | |
| "--copy", metavar="DEST", type=Path, default=None, | |
| help="Kopieer naar DEST in plaats van ter plekke hernoemen" | |
| ) | |
| parser.add_argument( | |
| "--dry-run", action="store_true", | |
| help="Laat zien wat er zou gebeuren zonder bestanden te wijzigen" | |
| ) | |
| args = parser.parse_args() | |
| if not args.source.is_dir(): | |
| print(f"Fout: map niet gevonden: {args.source}") | |
| sys.exit(1) | |
| process(args.source, args.copy, args.dry_run) | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment