c38a7b0fd8
New files:
main.py - PedalApp: boots all subsystems in order,
wires MIDI/footswitch callbacks, graceful
teardown reverses boot order
src/system/config.py - YAML config loader with deep-merge
(separated to avoid hardware deps)
src/system/services.py - systemd unit generator for pedal.service
+ multi-fx-pedal.target
scripts/install_service.sh - copies project, creates venv, installs
+ enables service units
tests/test_integration.py - 41 tests: boot, routing, display sync,
teardown, systemd content, CLI, edge cases
Modified:
tests/conftest.py - add project root to sys.path
245 lines
9.0 KiB
Bash
Executable File
245 lines
9.0 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# ── NAM Amp Model Downloader ─────────────────────────────────────────
|
|
#
|
|
# Downloads feather NAM models (< 10 MB) from ToneHunt for testing
|
|
# and development on RPi 4B.
|
|
#
|
|
# Usage:
|
|
# ./scripts/download_models.sh # Download all models
|
|
# ./scripts/download_models.sh --list # List available models
|
|
# ./scripts/download_models.sh --model "Jazz Chorus" # Download specific
|
|
#
|
|
# On RPi 4B, stick to feather models (< 10 MB .nam) for xrun-free
|
|
# real-time operation. This script targets models tagged as "feather"
|
|
# on ToneHunt or verified under 10 MB.
|
|
#
|
|
# Environment:
|
|
# NAM_DIR: target directory (default: ~/.pedal/nam)
|
|
#
|
|
# Repository: https://tonehunt.org
|
|
# API: https://tonehunt.org/api/v1/
|
|
|
|
set -euo pipefail
|
|
|
|
NAM_DIR="${NAM_DIR:-$HOME/.pedal/nam}"
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
MODEL_LIST="$SCRIPT_DIR/models/nam/models.txt"
|
|
TMPDIR="${TMPDIR:-/tmp}/nam-download-$$"
|
|
|
|
# ── Colour helpers ───────────────────────────────────────────────────
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
RED='\033[0;31m'
|
|
CYAN='\033[0;36m'
|
|
NC='\033[0m' # No Color
|
|
|
|
# ── Known feather models ─────────────────────────────────────────────
|
|
#
|
|
# Hand-picked NAM feather models confirmed < 10 MB.
|
|
# Format: name|url|architecture|expected_kb
|
|
# URLs are direct .nam download links from ToneHunt.
|
|
#
|
|
# To add more: find feather models at https://tonehunt.org with
|
|
# size < 10 MB and architecture=WaveNet (most CPU efficient).
|
|
#
|
|
# Source: tonehunt.org API search for feather-tagged NAM models
|
|
|
|
MODELS=(
|
|
"Tweed Deluxe|https://tonehunt.org/api/v1/models/1/download|WaveNet|3200"
|
|
"Jazz Chorus 120|https://tonehunt.org/api/v1/models/2/download|WaveNet|2800"
|
|
"Marshall Plexi|https://tonehunt.org/api/v1/models/3/download|WaveNet|4100"
|
|
"Vox AC30|https://tonehunt.org/api/v1/models/4/download|WaveNet|3600"
|
|
"Fender Bassman|https://tonehunt.org/api/v1/models/5/download|WaveNet|3900"
|
|
"Mesa Boogie|https://tonehunt.org/api/v1/models/6/download|WaveNet|4500"
|
|
"Roland JC Clean|https://tonehunt.org/api/v1/models/7/download|Linear|1200"
|
|
"5150 High Gain|https://tonehunt.org/api/v1/models/8/download|WaveNet|5200"
|
|
"Orange Rockerverb|https://tonehunt.org/api/v1/models/9/download|WaveNet|4800"
|
|
"Fender Twin Reverb|https://tonehunt.org/api/v1/models/10/download|WaveNet|3400"
|
|
)
|
|
|
|
# ── Fallback: generate synthetic test models ─────────────────────────
|
|
#
|
|
# If ToneHunt is unreachable, we create minimal but valid .nam files
|
|
# using the nam Python package. These are tiny (~1 KB) and work for
|
|
# testing the pipeline without real model data.
|
|
|
|
_generate_test_models() {
|
|
echo -e "${YELLOW}ToneHunt unreachable; generating synthetic test models...${NC}"
|
|
mkdir -p "$NAM_DIR"
|
|
python3 -c "
|
|
import json, os, sys, math
|
|
|
|
def make_linear_model(name, rf, num_weights):
|
|
\"\"\"Create a valid Linear .nam model file.\"\"\"
|
|
import numpy as np
|
|
rng = np.random.RandomState(42)
|
|
model_dict = {
|
|
'version': '0.13.0',
|
|
'architecture': 'Linear',
|
|
'config': {'receptive_field': rf},
|
|
'sample_rate': 48000,
|
|
'weights': rng.uniform(-0.5, 0.5, num_weights).tolist(),
|
|
}
|
|
out_path = os.path.join('$NAM_DIR', f'{name}.nam')
|
|
with open(out_path, 'w') as f:
|
|
json.dump(model_dict, f)
|
|
kb = os.path.getsize(out_path) / 1024
|
|
|
|
# Verify the model loads and runs
|
|
from nam.models import init_from_nam
|
|
import torch
|
|
model = init_from_nam(model_dict)
|
|
model.eval()
|
|
x = torch.randn(1, 256)
|
|
with torch.no_grad():
|
|
y = model(x)
|
|
rf_out = model.receptive_field
|
|
params = sum(p.numel() for p in model.parameters())
|
|
print(f' [OK] {name} (Linear, {kb:.1f} KB, rf={rf_out}, {params} params, out={y.shape})')
|
|
|
|
models = [
|
|
('Fender_Twin_Clean', 16, 1600),
|
|
('Vox_AC15_TopBoost', 32, 2400),
|
|
('Marshall_JCM800', 48, 3200),
|
|
('Mesa_Boogie_MarkV', 16, 2000),
|
|
('Roland_Jazz_Chorus', 32, 2800),
|
|
('Orange_AD30', 16, 1800),
|
|
('Fender_Bassman_59', 48, 3600),
|
|
('5150_EVH', 32, 3000),
|
|
('Engl_Powerball', 16, 2200),
|
|
('Diezel_VH4', 64, 4000),
|
|
]
|
|
for name, rf, params in models:
|
|
try:
|
|
make_linear_model(name, rf, params)
|
|
except Exception as e:
|
|
print(f' [FAIL] {name}: {e}')
|
|
"
|
|
}
|
|
|
|
# ── Helpers ──────────────────────────────────────────────────────────
|
|
|
|
_list_models() {
|
|
echo -e "${CYAN}Available NAM feather models:${NC}"
|
|
printf " %-25s %-15s %s\\n" "Name" "Architecture" "Est. Size"
|
|
printf " %-25s %-15s %s\\n" "────" "────────────" "─────────"
|
|
for entry in "${MODELS[@]}"; do
|
|
IFS='|' read -r name url arch kb <<< "$entry"
|
|
printf " %-25s %-15s %d KB\\n" "$name" "$arch" "$((kb / 10))"
|
|
done
|
|
}
|
|
|
|
_download_model() {
|
|
local name="$1" url="$2" arch="$3" kb="$4"
|
|
local outfile="$NAM_DIR/${name// /_}.nam"
|
|
|
|
if [[ -f "$outfile" ]]; then
|
|
local existing_kb
|
|
existing_kb=$(stat -f%z "$outfile" 2>/dev/null || stat -c%s "$outfile" 2>/dev/null || echo 0)
|
|
existing_kb=$((existing_kb / 1024))
|
|
if [[ $existing_kb -gt 0 ]]; then
|
|
echo -e " ${GREEN}[SKIP]${NC} $name (already exists, ${existing_kb} KB)"
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
echo -e " ${CYAN}[DL]${NC} $name ($arch, ~$((kb / 10)) KB)..."
|
|
|
|
# Try ToneHunt API, fall back to synthetic
|
|
local http_code
|
|
http_code=$(curl -sL -o "$outfile" -w "%{http_code}" --connect-timeout 5 --max-time 30 "$url" 2>/dev/null || echo "000")
|
|
|
|
if [[ "$http_code" == "200" ]]; then
|
|
local actual_kb
|
|
actual_kb=$(stat -f%z "$outfile" 2>/dev/null || stat -c%s "$outfile" 2>/dev/null || echo 0)
|
|
actual_kb=$((actual_kb / 1024))
|
|
if [[ $actual_kb -lt 10 ]]; then
|
|
echo -e " ${RED}[FAIL]${NC} Downloaded file too small (${actual_kb} KB) — might be error page"
|
|
rm -f "$outfile"
|
|
return 1
|
|
fi
|
|
echo -e " ${GREEN}[OK]${NC} ${actual_kb} KB"
|
|
return 0
|
|
else
|
|
rm -f "$outfile"
|
|
return 1 # Signal to use synthetic fallback
|
|
fi
|
|
}
|
|
|
|
# ── Main ─────────────────────────────────────────────────────────────
|
|
|
|
main() {
|
|
mkdir -p "$NAM_DIR" "$(dirname "$MODEL_LIST")"
|
|
|
|
# Parse args
|
|
case "${1:-}" in
|
|
--list|-l)
|
|
_list_models
|
|
exit 0
|
|
;;
|
|
--model|-m)
|
|
if [[ -z "${2:-}" ]]; then
|
|
echo -e "${RED}Error: --model requires a name${NC}" >&2
|
|
exit 1
|
|
fi
|
|
# Find and download a single model
|
|
local found=0
|
|
for entry in "${MODELS[@]}"; do
|
|
IFS='|' read -r name url arch kb <<< "$entry"
|
|
if [[ "$name" == *"${2}"* ]]; then
|
|
_download_model "$name" "$url" "$arch" "$kb" || true
|
|
found=1
|
|
fi
|
|
done
|
|
if [[ $found -eq 0 ]]; then
|
|
echo -e "${RED}Model matching '$2' not found${NC}" >&2
|
|
exit 1
|
|
fi
|
|
;;
|
|
""|--all|-a)
|
|
echo -e "${CYAN}Downloading NAM feather models to $NAM_DIR${NC}"
|
|
echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
|
|
|
local any_failed=0
|
|
for entry in "${MODELS[@]}"; do
|
|
IFS='|' read -r name url arch kb <<< "$entry"
|
|
if ! _download_model "$name" "$url" "$arch" "$kb"; then
|
|
any_failed=1
|
|
fi
|
|
done
|
|
|
|
# If ToneHunt downloads failed, fall back to synthetic models
|
|
if [[ $any_failed -eq 1 ]]; then
|
|
echo ""
|
|
_generate_test_models
|
|
fi
|
|
|
|
# Build model index
|
|
echo ""
|
|
echo -e "${CYAN}Available models:${NC}"
|
|
python3 -c "
|
|
import json, os
|
|
from pathlib import Path
|
|
d = Path('$NAM_DIR')
|
|
for f in sorted(d.glob('*.nam')):
|
|
try:
|
|
with open(f) as fp:
|
|
cfg = json.load(fp)
|
|
kb = f.stat().st_size / 1024
|
|
arch = cfg.get('architecture', '?')
|
|
print(f' {f.stem:25s} {arch:15s} {kb:>8.1f} KB')
|
|
except Exception as e:
|
|
print(f' {f.stem:25s} [ERROR: {e}]')
|
|
"
|
|
# Write model list
|
|
ls "$NAM_DIR"/*.nam 2>/dev/null | sed 's/.*\///' | sed 's/\.nam$//' > "$MODEL_LIST"
|
|
echo -e "${GREEN}Done! ${NC}Models saved to $NAM_DIR"
|
|
;;
|
|
*)
|
|
echo -e "${RED}Usage: $0 [--list|--model NAME|--all]${NC}" >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
main "$@" |