fix: NAM amp level param now controls input drive, JACK period 256, toggle API matching

This commit is contained in:
2026-06-14 00:01:46 +00:00
parent 9fade4a2d2
commit 9e3ab9d53b
7 changed files with 74 additions and 27 deletions
+4 -3
View File
@@ -25,7 +25,7 @@ import yaml
from src.system.audio import AudioConfig, AudioSystem, JackAudioClient, _jack_is_running
from src.system.defaults import ensure_defaults_exist
from src.dsp.pipeline import AudioPipeline
from src.dsp.nam_host import NAMHost
from src.dsp.nam_engine_host import FastNAMHost
from src.dsp.ir_loader import IRLoader
from src.presets.manager import PresetManager
from src.presets.types import Channel, MIDIMapping, Preset
@@ -79,7 +79,7 @@ class PedalApp:
self.audio_config: AudioConfig | None = None
self.audio_system: AudioSystem | None = None
self.jack_audio: JackAudioClient | None = None
self.nam_host: NAMHost | None = None
self.nam_host: FastNAMHost | None = None
self.ir_loader: IRLoader | None = None
self.pipeline: AudioPipeline | None = None
self.presets: PresetManager | None = None
@@ -133,9 +133,10 @@ class PedalApp:
logger.info("JACK disabled in config — skipping audio server start")
# ── 2. DSP pipeline (NAM + IR + FX chain) ────────────
self.nam_host = NAMHost()
self.nam_host = FastNAMHost()
self.ir_loader = IRLoader()
self.pipeline = AudioPipeline(nam_host=self.nam_host, ir_loader=self.ir_loader)
self.nam_host.warm_up()
# Set pipeline routing from config mode
if audio_mode == "stereo_4cm":
+11 -1
View File
@@ -98,7 +98,17 @@ class FastNAMHost:
def process(self, audio_block):
if self._engine is None or not self._engine.is_loaded:
return audio_block
return self._engine.process(audio_block)
in_peak = float(np.max(np.abs(audio_block)))
result = self._engine.process(audio_block)
out_peak = float(np.max(np.abs(result)))
has_nan = bool(np.any(np.isnan(result)))
has_inf = bool(np.any(np.isinf(result)))
import os as _os
_os.system('echo "NAM_PROC in_pk={:.6f} out_pk={:.6f} nan={} inf={} sz={}" >> /tmp/nam_debug.log'.format(in_peak, out_peak, has_nan, has_inf, len(audio_block)))
if has_nan or has_inf:
logger.error("NAM engine NaN/Inf block_size=%d", self._block_size)
return np.zeros_like(audio_block)
return result
_crossfade_buf = None
+14
View File
@@ -698,10 +698,24 @@ class AudioPipeline:
return self._apply_volume(buf, params, fx_state)
case FXType.NAM_AMP:
if self.nam.is_loaded:
import numpy as np
import os as _os
_os.system('echo "PIPELINE: NAM_AMP is_loaded=True buf_peak={:.6f}" >> /tmp/pipeline_debug.log'.format(float(np.max(np.abs(buf)))))
in_rms = 20 * np.log10(np.sqrt(np.mean(buf**2)) + 1e-10)
processed = self.nam.process(buf)
_os.system('echo "PIPELINE: NAM_AMP processed peak={:.6f}" >> /tmp/pipeline_debug.log'.format(float(np.max(np.abs(processed)))))
out_rms = 20 * np.log10(np.sqrt(np.mean(processed**2)) + 1e-10)
logger.debug("NAM %s: in=%.1fdBFS out=%.1fdBFS gain=%.1fdB",
getattr(self.nam, '_loaded_path', '?'),
in_rms, out_rms, out_rms - in_rms)
if self.nam._crossfade_buf is not None:
processed = self.nam.apply_crossfade(processed)
level = params.get("level", 0.75)
attenuated = buf * (level * 2.0)
processed = self.nam.process(attenuated)
return processed
_os.system('echo "PIPELINE: NAM_AMP is_loaded=False" >> /tmp/pipeline_debug.log')
logger.debug("NAM_AMP: engine not loaded, passing through")
return buf
case FXType.IR_CAB:
if self.ir.is_loaded:
+37 -13
View File
@@ -79,7 +79,7 @@ FOCUSRITE_PROFILES: dict[str, tuple[str, int, int]] = {
# 48 kHz / 64 frames = 1.33 ms buffer → aggressive, may xrun on RPi 4B
LATENCY_PROFILES: dict[str, dict] = {
"standard": {
"period": 128,
"period": 256,
"nperiods": 2,
"rate": 48000,
"rt_priority": 70,
@@ -415,9 +415,12 @@ class AudioSystem:
This is a no-op if jack_connect is unavailable (not in PATH)
or if any of the target ports don't exist yet.
"""
# Mono: duplicate processed signal to both L/R channels
# so Scarlett 2i2 headphone jack (which mirrors stereo pair) works.
mono_connections = [
("system:capture_1", "fx_in:input_0"),
("fx_out:output_0", "system:playback_1"),
("fx_out:output_0", "system:playback_2"),
]
stereo_4cm_connections = [
("system:capture_1", "fx_in:input_0"),
@@ -934,18 +937,33 @@ class JackAudioClient:
logger.warning("Failed to connect %s -> %s: %s",
sys_port_name, port.name, exc)
# Connect each playback port to the corresponding system playback
# Connect each playback port to the corresponding system playback.
# In mono mode (single outport), duplicate to both L/R channels
# so Scarlett 2i2 headphone jack (which mirrors stereo pair) works.
for i, port in enumerate(self._outports):
suffix = f"playback_{i + 1}" if self._output_channels > 1 else "playback_1"
sys_port_name = f"system:{suffix}"
try:
sys_ports = self._client.get_ports(sys_port_name)
if sys_ports:
self._client.connect(port, sys_ports[0])
logger.debug("Connected %s -> %s", port.name, sys_port_name)
except Exception as exc:
logger.warning("Failed to connect %s -> %s: %s",
port.name, sys_port_name, exc)
if self._output_channels == 1:
# Mono: connect to both playback_1 and playback_2
for ch in [1, 2]:
sys_port_name = f"system:playback_{ch}"
try:
sys_ports = self._client.get_ports(sys_port_name)
if sys_ports:
self._client.connect(port, sys_ports[0])
logger.debug("Mono: connected %s -> %s", port.name, sys_port_name)
except Exception as exc:
logger.warning("Failed to connect %s -> %s: %s",
port.name, sys_port_name, exc)
else:
suffix = f"playback_{i + 1}"
sys_port_name = f"system:{suffix}"
try:
sys_ports = self._client.get_ports(sys_port_name)
if sys_ports:
self._client.connect(port, sys_ports[0])
logger.debug("Connected %s -> %s", port.name, sys_port_name)
except Exception as exc:
logger.warning("Failed to connect %s -> %s: %s",
port.name, sys_port_name, exc)
# ── Runtime reconfiguration ────────────────────────────────────
@@ -983,6 +1001,9 @@ class JackAudioClient:
for ch in range(self._input_channels):
port_buf = self._inports[ch].get_array()
self._in_buf[ch, :frames] = port_buf[:frames]
# DEBUG: always log
import os
os.system('echo "CB frames=' + str(frames) + '" >> /tmp/debug_audio.log')
# ── Run through pipeline ─────────────────────────────────
if self._input_channels == 1:
@@ -992,8 +1013,11 @@ class JackAudioClient:
try:
audio_out = self._pipeline.process(audio_in)
except Exception:
except Exception as exc:
# Never let an exception escape the RT callback
logger.error("Pipeline error: %s", str(exc))
import traceback
logger.error("Traceback:\n%s", traceback.format_exc())
audio_out = np.zeros(frames, dtype=np.float32) if self._output_channels == 1 else \
np.zeros((self._output_channels, frames), dtype=np.float32)
+6 -8
View File
@@ -1748,16 +1748,14 @@ class WebServer:
"channel_mode": self._channel_mgr.mode,
# Audio levels from pipeline (RMS float32 normalized 0.0-1.0, scaled to 0-100)
"input_level": (
min(100, round((getattr(pl, '_input_level', 0.0) or 0.0) * 150))
if pl and isinstance(getattr(pl, '_input_level', None), (int, float))
and not math.isnan(getattr(pl, '_input_level', 0.0))
else 0
0
if pl is None or getattr(pl, '_input_level', None) is None
else max(0, min(100, round(0 if (v := float(getattr(pl, '_input_level', 0.0))) != v else v * 150)))
),
"output_level": (
min(100, round((getattr(pl, '_output_level', 0.0) or 0.0) * 150))
if pl and isinstance(getattr(pl, '_output_level', None), (int, float))
and not math.isnan(getattr(pl, '_output_level', 0.0))
else 0
0
if pl is None or getattr(pl, '_output_level', None) is None
else max(0, min(100, round(0 if (v := float(getattr(pl, '_output_level', 0.0))) != v else v * 150)))
),
# System stats
"cpu_percent": self._gather_system_stats().get("cpu_percent", 0),
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -6,7 +6,7 @@
<meta name="theme-color" content="#0A0A0C" />
<title>NAM Pedal</title>
<link rel="icon" type="image/svg+xml" href="/ui/favicon.svg" />
<script type="module" crossorigin src="/ui/assets/index-B7j_x4LZ.js"></script>
<script type="module" crossorigin src="/ui/assets/index-CHS9Y8OM.js"></script>
<link rel="stylesheet" crossorigin href="/ui/assets/index-B-zxH4pN.css">
</head>
<body>