Skip to content

Instantly share code, notes, and snippets.

@tooh
Created May 14, 2026 16:26
Show Gist options
  • Select an option

  • Save tooh/ddad0509a27262fed974552c8a836639 to your computer and use it in GitHub Desktop.

Select an option

Save tooh/ddad0509a27262fed974552c8a836639 to your computer and use it in GitHub Desktop.
normalize_stems
#!/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