Files
pi-multifx-pedal/scripts/download_nam_and_irs.py
shawn 5c51b3db28 feat(nam): download 10 new A2 models for bass, orange, and other missing categories
Bank 7 — Bass: Ampeg PF20T, Aguilar Tone Hammer 500, GK 800RB (Gallien-Krueger), Aguilar DB751
Bank 7 — Orange: Orange Rocker 30 Dirty + Natural
Bank 7 — Others: Fender Bassman dimed, Hiwatt DR103 Bright + Cornish, Supro Black Magick M3

All SlimmableContainer v0.7.0, ~287-295 KB, verified JSON.
Also fixed Bank 2 Tone3000 storage keys to include .nam extension (Tone3000 changed URL format).
Inventory updated: 54 → 64 total models.
2026-06-13 02:09:56 -04:00

509 lines
20 KiB
Python

#!/usr/bin/env python3
"""Download NAM WaveNet models from community collection + cabinet IRs.
Downloads real NAM amp captures from the pelennor2170/NAM_models GitHub
repository and free cabinet IR packs from Archive.org.
Usage:
python3 scripts/download_nam_and_irs.py # Download everything
python3 scripts/download_nam_and_irs.py --list # List models only
python3 scripts/download_nam_and_irs.py --models # Models only
python3 scripts/download_nam_and_irs.py --irs # IRs only
"""
import json
import os
import subprocess
import sys
import urllib.parse
import urllib.request
from pathlib import Path
# ── Paths ────────────────────────────────────────────────────────────────
NAM_DIR = Path.home() / ".pedal" / "models"
IR_DIR = Path.home() / ".pedal" / "irs"
BASE_URL = "https://raw.githubusercontent.com/pelennor2170/NAM_models/main"
# ── Model definitions ────────────────────────────────────────────────────
# (local_name, remote_name, description, architecture, size_kb)
# URL for Tone3000 (ToneHunt) Supabase storage downloads
TONE3000_STORAGE = "https://api.tone3000.com/storage/v1/object/public/models"
MODELS = [
# ── Bank 0: Clean & Edge ──────────────────────────────────────────
("Fender_TwinVerb_Clean.nam",
"Tim R Fender TwinVerb Norm Bright.nam",
"Fender Twin Reverb Normal Bright — clean jazz/fingerstyle",
"WaveNet", 400),
("Fender_TwinVerb_Vibrato.nam",
"Tim R Fender TwinVerb Vibrato Bright.nam",
"Fender Twin Reverb Vibrato Bright — sparkling clean",
"WaveNet", 400),
("Vox_AC15_TopBoost.nam",
"Phillipe P VOXAC15-TopBoost.nam",
"Vox AC15 Top Boost — chimey edge-of-breakup",
"WaveNet", 400),
("Magnatone_Super59_Pushed.nam",
"Tim R Magnatone Super 59 Mkii Pushed.nam",
"Magnatone Super 59 pushed — bluesy breakup",
"WaveNet", 400),
("JCM2000_Clean.nam",
"Tim R JCM2000 Clean.nam",
"Marshall JCM2000 clean — crisp rhythm",
"WaveNet", 400),
# ── Bank 1: Drive & Lead ──────────────────────────────────────────
("Orange_Rockerverb.nam",
"Tom C Axe FX 2 Orange Rockerverb.nam",
"Orange Rockerverb — classic rock crunch",
"WaveNet", 400),
("JCM2000_Crunch.nam",
"Tim R JCM2000 Crunch.nam",
"Marshall JCM2000 crunch — hard rock rhythm",
"WaveNet", 400),
("JCM900_Lead.nam",
"Tim R JCM90050WDualVerbCHAG6.nam",
"Marshall JCM900 Dual Verb ch.A gain6 — lead",
"WaveNet", 400),
("5150_BlockLetter_Boosted.nam",
"Helga B 5150 BlockLetter - Boosted.nam",
"EVH 5150 Block Letter boosted — high-gain lead",
"WaveNet", 400),
("6505_Red_NoBoost.nam",
"Helga B 6505+ Red ch - NoBoost.nam",
"Peavey 6505+ Red ch no boost — metal rhythm",
"WaveNet", 400),
("Engl_Savage.nam",
"Tom C Axe FX 2 Engl Savage.nam",
"ENGL Savage 120 — modern high-gain lead",
"WaveNet", 400),
("Splawn_ProMod_3rd.nam",
"Tim R Splawn Pro Mod 3rd Gear - G12.nam",
"Splawn Pro Mod 3rd gear — tight high-gain",
"WaveNet", 400),
("5152_Lead_TS9.nam",
"Tim R 5152 Lead + TS9.nam",
"5152 Lead with Tube Screamer — boosted solo",
"WaveNet", 400),
# ── Pedal captures ───────────────────────────────────────────────
("Klon_Centaur_High.nam",
"Keith B Klone_plus_BB_highGain.nam",
"Klon Centaur-style OD high gain — boost/overdrive",
"WaveNet", 400),
("TubeScreamer_Maxed.nam",
"Tudor N Ibanez Tube Screamer Mini Tone@10_Level@10_Overdrive@0 ESR_0.0006_lite.nam",
"Ibanez TS808 Mini maxed — clean boost",
"WaveNet", 200),
("Mesa_MarkIV_Lead.nam",
"Roman A LT_MESA_MARKIV_1.nam",
"Mesa Boogie Mark IV lead — singing sustain",
"WaveNet", 400),
("Bogner_Ecstasy.nam",
"Tudor N Ceriatone Molecular 50.nam",
"Bogner Ecstasy-style — versatile OD range",
"WaveNet", 400),
# ── Feather models (93 KB, ultra-low CPU) ────────────────────────
("BOSS_SD1_Feather.nam",
"Phillipe P BOSS-SD1-Feather-ESR0,001.nam",
"BOSS SD-1 overdrive feather — 93 KB",
"WaveNet", 93),
("MXR_M77_Feather.nam",
"Phillipe P MXR-M77-Feather-ESR0,001.nam",
"MXR M77 Custom Badass OD feather — 93 KB",
"WaveNet", 93),
("TS808_Feather.nam",
"Phillipe P Maxon-OD808-Feather-ESR0,001.nam",
"Maxon OD808 Tube Screamer feather — 93 KB",
"WaveNet", 93),
("Precision_Drive_Feather.nam",
"Phillipe P Precision-Drive-Feather-ESR0,001.nam",
"Precision Drive overdrive feather — 93 KB",
"WaveNet", 93),
("Friedman_PowerAmp_Feather.nam",
"Sascha S FriedmanDSM_PowerAmpEL84_MV10_feather.nam",
"Friedman DSM power amp feather — 93 KB",
"WaveNet", 93),
# ── Bank 2: Blues (Tone3000 captures — clean, edge-of-breakup, pushed) ─
# Downloaded from ToneHunt / Tone3000 Supabase storage. These are real
# amp captures of classic blues tones: Fender Princeton, Tweed Deluxe,
# Twin Reverb, Dumble ODS, Two-Rock, Marshall Bluesbreaker.
# Format: (local_name, tone3000_storage_key, description, architecture, size_kb)
("Fender_Princeton_Clone_Clean.nam",
"4f81e2b088149c04.nam",
"Fender Hand-wired Princeton Clone Clean — blues clean (WaveNet A1, 289 KB)",
"WaveNet", 289),
("Fender_Princeton_Clone_EOB.nam",
"68a5f3484e1759ed_a2.nam",
"Fender Hand-wired Princeton Clone Edge-of-Breakup — blues edge (Slimmable A2, 288 KB)",
"SlimmableContainer", 288),
("Fender_Tweed_Dlx_Edge.nam",
"ga5rj9sxlnk.nam",
"Fender Tweed Deluxe The Edge (Celestion Blue) — pushed clean (Slimmable A2, 288 KB)",
"SlimmableContainer", 288),
("Fender_Twin_Reverb_Clean_A2.nam",
"tg81uadutjo_a2.nam",
"Fender Twin Reverb Clean — sparkling blues clean (Slimmable A2, 290 KB)",
"SlimmableContainer", 290),
("TwoRock_JTM_Signature.nam",
"qj3hkgbcwb9.nam",
"Two-Rock John Mayer Signature Prototype — clean/edge bloom (WaveNet A1, 276 KB)",
"WaveNet", 276),
("Dumble_ODS_102_Rock_A1.nam",
"5bac98f136812852.nam",
"Dumble ODS #102 Clone Rock setting — pushed blues overdrive (WaveNet A1, 87 KB feather)",
"WaveNet", 87),
("Dumble_ODS_102_Rock_A2.nam",
"5bac98f136812852_a2.nam",
"Dumble ODS #102 Clone Rock setting — pushed blues OD (Slimmable A2, 289 KB)",
"SlimmableContainer", 289),
("Marshall_Bluesbreaker_1962_A2.nam",
"bbc41ecccdc730fb_a2.nam",
"Marshall Bluesbreaker 1962 — classic blues rock (Slimmable A2, 289 KB)",
"SlimmableContainer", 289),
("Marshall_Bluesbreaker_Pedal.nam",
"31aeae1256b36839.nam",
"Marshall Bluesbreaker Pedal (Original) Setting 1 — pedal OD (WaveNet A1, 290 KB)",
"WaveNet", 290),
# ── Bank 7: Bass, Orange & Others (Tone3000 A2 — SlimmableContainer) ────
# Downloaded from Tone3000 Supabase storage. Covers missing categories:
# Bass (Ampeg, Aguilar, GK), Orange Rocker 30, Fender Bassman,
# Hiwatt DR103, Supro Black Magick.
("Ampeg_PF20T_A2.nam",
"db2c43e99cfc346d_a2.nam",
"Ampeg PF20T — clean bass tube amp (Slimmable A2, 288 KB)",
"SlimmableContainer", 288),
("Aguilar_Tone_Hammer_500_A2.nam",
"38rdhwzk3hp_a2.nam",
"Aguilar Tone Hammer 500 — modern bass preamp (Slimmable A2, 293 KB)",
"SlimmableContainer", 293),
("GK_800RB_Line_A2.nam",
"ce68ccd373a10054_a2.nam",
"Gallien-Krueger 800RB — classic bass solid-state (Slimmable A2, 295 KB)",
"SlimmableContainer", 295),
("Aguilar_DB751_Cab_A2.nam",
"lage0x4ybk_a2.nam",
"Aguilar DB751 — bass amp DI (Slimmable A2, 292 KB)",
"SlimmableContainer", 292),
("Orange_Rocker30_Dirty_A2.nam",
"8491c70c3771de1e_a2.nam",
"Orange Rocker 30 Dirty — classic rock crunch (Slimmable A2, 295 KB)",
"SlimmableContainer", 295),
("Orange_Rocker30_Natural_A2.nam",
"a60ed1a1aa0c2c0f_a2.nam",
"Orange Rocker 30 Natural — clean edge (Slimmable A2, 289 KB)",
"SlimmableContainer", 289),
("Fender_Bassman_Dimed_A2.nam",
"0a9223a9f1c879d5_a2.nam",
"Fender Bassman dimed — blues/rock breakup (Slimmable A2, 289 KB)",
"SlimmableContainer", 289),
("Hiwatt_DR103_Bright_A2.nam",
"0c46e5a7db1cce48_a2.nam",
"Hiwatt DR-103 Bright Normal — prog/classic rock (Slimmable A2, 287 KB)",
"SlimmableContainer", 287),
("Hiwatt_DR103_Cornish_A2.nam",
"ynk0h2e2tph.nam",
"Hiwatt DR103 + Cornish PGS — boosted prog rock (Slimmable A2, 288 KB)",
"SlimmableContainer", 288),
("Supro_Black_Magick_M3_A2.nam",
"c90a6d8ee0c0a689_a2.nam",
"Supro Black Magick M3 — garage/blues (Slimmable A2, 288 KB)",
"SlimmableContainer", 288),
]
# ── IR Pack definitions ──────────────────────────────────────────────────
IR_PACKS = [
("God's Cab",
"https://archive.org/download/godscab-ir-pack/GodsCab_IR_Pack.zip",
"godscab.zip",
"100+ cabinet IRs (412, 1960, V30, Greenback, etc.) — CC0 / Public Domain"),
("Seacow Cabs",
"https://archive.org/download/seacow-cabs-free-ir-2023/Seacow_Cabs_Free_IRs_2023.zip",
"seacow.zip",
"Boutique cab IR pack, 15+ cabs — Free for personal use"),
]
def info(msg): print(f"\033[0;36m[INFO]\033[0m {msg}")
def ok(msg): print(f"\033[0;32m[OK]\033[0m {msg}")
def warn(msg): print(f"\033[1;33m[WARN]\033[0m {msg}")
def err(msg): print(f"\033[0;31m[ERR]\033[0m {msg}", file=sys.stderr)
def download_file(url: str, dest: Path, max_size_mb: int = 50) -> bool:
"""Download a file from url to dest. Returns True on success."""
try:
req = urllib.request.Request(url, headers={"User-Agent": "pedal-downloader/1.0"})
with urllib.request.urlopen(req, timeout=30) as src:
data = src.read()
size_mb = len(data) / (1024 * 1024)
if size_mb > max_size_mb:
err(f"File too large ({size_mb:.1f} MB, max {max_size_mb} MB)")
return False
if len(data) < 1000:
err(f"File too small ({len(data)} bytes)")
return False
dest.write_bytes(data)
return True
except Exception as e:
err(f"Download failed: {e}")
return False
def download_model(local_name: str, remote_name: str, desc: str, arch: str, size: int) -> bool:
"""Download a single NAM model file from GitHub."""
outfile = NAM_DIR / local_name
if outfile.exists() and outfile.stat().st_size > 1000:
ok(f"SKIP {local_name} (already exists, {outfile.stat().st_size // 1024} KB)")
return True
# URL-encode the remote filename
encoded = urllib.parse.quote(remote_name)
url = f"{BASE_URL}/{encoded}"
info(f"DL {local_name} ({desc})")
if download_file(url, outfile):
# Verify it's valid JSON (.nam format)
try:
data = json.loads(outfile.read_text())
actual_arch = data.get("architecture", "?")
actual_ver = data.get("version", "?")
ok(f"Downloaded {local_name} ({outfile.stat().st_size // 1024} KB, {actual_arch} v{actual_ver})")
return True
except json.JSONDecodeError as e:
err(f"Invalid .nam file (not valid JSON): {e}")
outfile.unlink(missing_ok=True)
return False
else:
err(f"Failed to download {local_name}")
return False
def _discover_tone3000_key() -> str:
"""Discover the Supabase anon key from Tone3000 website JS bundles."""
try:
req = urllib.request.Request(
"https://tone3000.com",
headers={"User-Agent": "Mozilla/5.0 (compatible; pedal-downloader/1.0)"},
)
with urllib.request.urlopen(req, timeout=15) as resp:
html = resp.read().decode("utf-8", errors="replace")
import re
chunk_urls = set()
for m in re.finditer(r'src="([^"]*\.js[^"]*)"', html):
url = m.group(1)
if url.startswith("/"):
url = "https://tone3000.com" + url
chunk_urls.add(url)
for chunk_url in sorted(chunk_urls)[:15]:
try:
req2 = urllib.request.Request(chunk_url, headers={"User-Agent": "Mozilla/5.0"})
with urllib.request.urlopen(req2, timeout=10) as resp2:
js = resp2.read().decode("utf-8", errors="replace")
m = re.search(
r'createBrowserClient\s*\)\s*\(\s*["\']https?://api\.tone3000\.com["\']\s*,\s*["\']([^"\']+)["\']',
js,
)
if m:
key = m.group(1)
if key.startswith("eyJ") and key.count(".") >= 2:
return key
for m2 in re.finditer(
r'["\'](eyJ[a-zA-Z0-9_\-]{20,}\.[a-zA-Z0-9_\-]{20,}\.[a-zA-Z0-9_\-]{10,})["\']', js
):
key = m2.group(1)
if key.count(".") >= 2:
return key
except Exception:
continue
except Exception:
pass
return ""
def download_tone3000_model(local_name: str, storage_key: str, desc: str, arch: str, size: int) -> bool:
"""Download a single NAM model file from Tone3000 Supabase storage."""
outfile = NAM_DIR / local_name
if outfile.exists() and outfile.stat().st_size > 1000:
ok(f"SKIP {local_name} (already exists, {outfile.stat().st_size // 1024} KB)")
return True
url = f"{TONE3000_STORAGE}/{storage_key}"
info(f"DL {local_name} ({desc})")
req = urllib.request.Request(url, headers={"User-Agent": "pedal-downloader/1.0"})
try:
with urllib.request.urlopen(req, timeout=30) as src:
data = src.read()
except Exception as e:
err(f"Download failed: {e}")
# Try with anon key (some storage buckets require authentication)
try:
anon_key = _discover_tone3000_key()
if anon_key:
req = urllib.request.Request(url, headers={
"User-Agent": "pedal-downloader/1.0",
"Authorization": f"Bearer {anon_key}",
})
with urllib.request.urlopen(req, timeout=30) as src:
data = src.read()
else:
return False
except Exception as e2:
err(f"Download failed even with auth: {e2}")
return False
size_mb = len(data) / (1024 * 1024)
if size_mb > 50:
err(f"File too large ({size_mb:.1f} MB, max 50 MB)")
return False
if len(data) < 1000:
err(f"File too small ({len(data)} bytes)")
return False
outfile.write_bytes(data)
# Verify
try:
model_data = json.loads(data)
actual_arch = model_data.get("architecture", "?")
actual_ver = model_data.get("version", "?")
ok(f"Downloaded {local_name} ({outfile.stat().st_size // 1024} KB, {actual_arch} v{actual_ver})")
return True
except json.JSONDecodeError as e:
err(f"Invalid .nam file (not valid JSON): {e}")
outfile.unlink(missing_ok=True)
return False
def download_models():
"""Download all NAM models (GitHub + Tone3000)."""
print(f"\n\033[0;36mDownloading {len(MODELS)} NAM models to {NAM_DIR}\033[0m")
print(f"\033[0;36m{'' * 60}\033[0m")
NAM_DIR.mkdir(parents=True, exist_ok=True)
success = 0
fail = 0
for local_name, remote_name, desc, arch, size in MODELS:
# Detect Tone3000 storage key (short, no dots, no spaces)
if " " not in remote_name and "." not in remote_name and len(remote_name) < 64:
if download_tone3000_model(local_name, remote_name, desc, arch, size):
success += 1
else:
fail += 1
else:
if download_model(local_name, remote_name, desc, arch, size):
success += 1
else:
fail += 1
print(f"\n Models: {success} successful, {fail} failed")
return fail
def download_irs():
"""Download IR packs from Archive.org."""
print(f"\n\033[0;36mDownloading IR packs to {IR_DIR}\033[0m")
print(f"\033[0;36m{'' * 60}\033[0m")
IR_DIR.mkdir(parents=True, exist_ok=True)
success = 0
fail = 0
for name, url, fname, desc in IR_PACKS:
# Check if already extracted
wav_count = len(list(IR_DIR.rglob("*.wav")))
if wav_count > 10:
ok(f"SKIP {name} ({wav_count} .wav files already present)")
success += 1
continue
dest = IR_DIR / fname
info(f"Downloading {name}...")
info(f" Source: {url}")
if download_file(url, dest, max_size_mb=200):
size_mb = dest.stat().st_size / (1024 * 1024)
ok(f"Downloaded {fname} ({size_mb:.1f} MB)")
# Extract ZIP
import zipfile
try:
with zipfile.ZipFile(dest, "r") as zf:
zf.extractall(IR_DIR)
dest.unlink()
wav_count = len(list(IR_DIR.rglob("*.wav")))
ok(f"Extracted to {IR_DIR}/ ({wav_count} .wav files)")
success += 1
except zipfile.BadZipFile:
warn(f"Bad ZIP — saved at {dest}")
fail += 1
else:
err(f"Failed to download {name}")
fail += 1
print(f"\n IR packs: {success} downloaded, {fail} failed")
wav_count = len(list(IR_DIR.rglob("*.wav")))
info(f"Total .wav IR files: {wav_count}")
return fail
def list_models():
"""Print model listing."""
print(f"\n\033[0;36mNAM Models by Genre:\033[0m")
print(f"\033[0;36m{'' * 60}\033[0m")
for local_name, remote_name, desc, arch, size in MODELS:
print(f" {local_name:35s} {desc}")
print(f" {'':35s} ({arch}, ~{size} KB)")
print()
print(f"\nTotal: {len(MODELS)} models")
# Count existing files
nam_count = len(list(NAM_DIR.glob("*.nam")))
ir_count = len(list(IR_DIR.rglob("*.wav")))
print(f"\n Models in {NAM_DIR}: {nam_count}")
print(f" IRs in {IR_DIR}: {ir_count}")
def main():
# Also check ~/.pedal/nam/ legacy location
NAM_DIR.mkdir(parents=True, exist_ok=True)
IR_DIR.mkdir(parents=True, exist_ok=True)
arg = sys.argv[1] if len(sys.argv) > 1 else "--all"
if arg == "--list":
list_models()
elif arg == "--models":
exit(download_models())
elif arg == "--irs":
exit(download_irs())
elif arg == "--all":
fail = download_models()
download_irs()
# Summary
nam_count = len(list(NAM_DIR.glob("*.nam")))
ir_count = len(list(IR_DIR.rglob("*.wav")))
print(f"\n\033[0;32m{'' + '' * 56 + ''}\033[0m")
print(f"\033[0;32m║ Download complete!{' ' * 36}\033[0m")
print(f"\033[0;32m{'' + '' * 56 + ''}\033[0m")
print()
print(f" Models: {nam_count} .nam files in {NAM_DIR}")
print(f" IRs: {ir_count} .wav files in {IR_DIR}")
exit(fail)
else:
print(f"Usage: {sys.argv[0]} [--list|--models|--irs|--all]")
exit(1)
if __name__ == "__main__":
main()