Initial scaffold: Pi Multi-FX Pedal with NAM A2, IR cab, multi-FX, MIDI, stomp UI

This commit is contained in:
2026-06-07 23:22:43 -04:00
commit ed29748a62
24 changed files with 6215 additions and 0 deletions
+97
View File
@@ -0,0 +1,97 @@
"""NAM A2 model host — load, configure, and run inference on RPi 4B.
Leverages `neural-amp-modeler` (nam) Python package or the NAM LV2 plugin
for real-time inference on the Raspberry Pi 4B.
"""
from __future__ import annotations
import logging
from dataclasses import dataclass, field
from pathlib import Path
from typing import Optional
import numpy as np
logger = logging.getLogger(__name__)
DEFAULT_NAM_DIR = Path.home() / ".pedal" / "nam"
DEFAULT_LV2_MODEL_DIR = Path.home() / ".lv2" / "nam-models"
@dataclass
class NAMModel:
"""Metadata for a loaded NAM model."""
name: str
path: str
size_mb: float
sample_rate: int = 48000
latency_samples: int = 0
compatible: bool = True
class NAMHost:
"""Hosts NAM models for real-time amp simulation.
On RPi 4B, this delegates to either:
1. The NAM LV2 plugin (via JACK/Carla) — for production use
2. The nam Python package — for testing/development
"""
def __init__(
self,
models_dir: str | Path = DEFAULT_NAM_DIR,
lv2_dir: str | Path = DEFAULT_LV2_MODEL_DIR,
use_lv2: bool = True,
):
self._models_dir = Path(models_dir)
self._lv2_dir = Path(lv2_dir)
self._use_lv2 = use_lv2
self._loaded_model: Optional[NAMModel] = None
self._models_dir.mkdir(parents=True, exist_ok=True)
def load_model(self, model_path: str) -> bool:
"""Load a NAM model file into the inference engine."""
path = Path(model_path)
if not path.exists() or path.suffix not in (".nam",):
logger.error("Model not found or invalid: %s", model_path)
return False
size_mb = path.stat().st_size / (1024 * 1024)
is_feather = size_mb < 10
self._loaded_model = NAMModel(
name=path.stem,
path=str(path),
size_mb=size_mb,
compatible=is_feather,
)
# Symlink for LV2 plugin access
if self._use_lv2:
self._lv2_dir.mkdir(parents=True, exist_ok=True)
link = self._lv2_dir / path.name
if link.exists() or link.is_symlink():
link.unlink()
link.symlink_to(path.absolute())
logger.info(
"Loaded NAM model: %s (%.1f MB, %s)",
self._loaded_model.name,
size_mb,
"compatible" if is_feather else "may cause xruns",
)
return True
def unload(self) -> None:
"""Unload the current NAM model."""
self._loaded_model = None
logger.info("NAM model unloaded")
@property
def is_loaded(self) -> bool:
return self._loaded_model is not None
@property
def current_model(self) -> Optional[NAMModel]:
return self._loaded_model