fix: NAM amp level param now controls input drive, JACK period 256, toggle API matching
This commit is contained in:
@@ -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":
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user