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.audio import AudioConfig, AudioSystem, JackAudioClient, _jack_is_running
|
||||||
from src.system.defaults import ensure_defaults_exist
|
from src.system.defaults import ensure_defaults_exist
|
||||||
from src.dsp.pipeline import AudioPipeline
|
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.dsp.ir_loader import IRLoader
|
||||||
from src.presets.manager import PresetManager
|
from src.presets.manager import PresetManager
|
||||||
from src.presets.types import Channel, MIDIMapping, Preset
|
from src.presets.types import Channel, MIDIMapping, Preset
|
||||||
@@ -79,7 +79,7 @@ class PedalApp:
|
|||||||
self.audio_config: AudioConfig | None = None
|
self.audio_config: AudioConfig | None = None
|
||||||
self.audio_system: AudioSystem | None = None
|
self.audio_system: AudioSystem | None = None
|
||||||
self.jack_audio: JackAudioClient | 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.ir_loader: IRLoader | None = None
|
||||||
self.pipeline: AudioPipeline | None = None
|
self.pipeline: AudioPipeline | None = None
|
||||||
self.presets: PresetManager | None = None
|
self.presets: PresetManager | None = None
|
||||||
@@ -133,9 +133,10 @@ class PedalApp:
|
|||||||
logger.info("JACK disabled in config — skipping audio server start")
|
logger.info("JACK disabled in config — skipping audio server start")
|
||||||
|
|
||||||
# ── 2. DSP pipeline (NAM + IR + FX chain) ────────────
|
# ── 2. DSP pipeline (NAM + IR + FX chain) ────────────
|
||||||
self.nam_host = NAMHost()
|
self.nam_host = FastNAMHost()
|
||||||
self.ir_loader = IRLoader()
|
self.ir_loader = IRLoader()
|
||||||
self.pipeline = AudioPipeline(nam_host=self.nam_host, ir_loader=self.ir_loader)
|
self.pipeline = AudioPipeline(nam_host=self.nam_host, ir_loader=self.ir_loader)
|
||||||
|
self.nam_host.warm_up()
|
||||||
|
|
||||||
# Set pipeline routing from config mode
|
# Set pipeline routing from config mode
|
||||||
if audio_mode == "stereo_4cm":
|
if audio_mode == "stereo_4cm":
|
||||||
|
|||||||
@@ -98,7 +98,17 @@ class FastNAMHost:
|
|||||||
def process(self, audio_block):
|
def process(self, audio_block):
|
||||||
if self._engine is None or not self._engine.is_loaded:
|
if self._engine is None or not self._engine.is_loaded:
|
||||||
return audio_block
|
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
|
_crossfade_buf = None
|
||||||
|
|
||||||
|
|||||||
@@ -698,10 +698,24 @@ class AudioPipeline:
|
|||||||
return self._apply_volume(buf, params, fx_state)
|
return self._apply_volume(buf, params, fx_state)
|
||||||
case FXType.NAM_AMP:
|
case FXType.NAM_AMP:
|
||||||
if self.nam.is_loaded:
|
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)
|
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:
|
if self.nam._crossfade_buf is not None:
|
||||||
processed = self.nam.apply_crossfade(processed)
|
processed = self.nam.apply_crossfade(processed)
|
||||||
|
level = params.get("level", 0.75)
|
||||||
|
attenuated = buf * (level * 2.0)
|
||||||
|
processed = self.nam.process(attenuated)
|
||||||
return processed
|
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
|
return buf
|
||||||
case FXType.IR_CAB:
|
case FXType.IR_CAB:
|
||||||
if self.ir.is_loaded:
|
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
|
# 48 kHz / 64 frames = 1.33 ms buffer → aggressive, may xrun on RPi 4B
|
||||||
LATENCY_PROFILES: dict[str, dict] = {
|
LATENCY_PROFILES: dict[str, dict] = {
|
||||||
"standard": {
|
"standard": {
|
||||||
"period": 128,
|
"period": 256,
|
||||||
"nperiods": 2,
|
"nperiods": 2,
|
||||||
"rate": 48000,
|
"rate": 48000,
|
||||||
"rt_priority": 70,
|
"rt_priority": 70,
|
||||||
@@ -415,9 +415,12 @@ class AudioSystem:
|
|||||||
This is a no-op if jack_connect is unavailable (not in PATH)
|
This is a no-op if jack_connect is unavailable (not in PATH)
|
||||||
or if any of the target ports don't exist yet.
|
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 = [
|
mono_connections = [
|
||||||
("system:capture_1", "fx_in:input_0"),
|
("system:capture_1", "fx_in:input_0"),
|
||||||
("fx_out:output_0", "system:playback_1"),
|
("fx_out:output_0", "system:playback_1"),
|
||||||
|
("fx_out:output_0", "system:playback_2"),
|
||||||
]
|
]
|
||||||
stereo_4cm_connections = [
|
stereo_4cm_connections = [
|
||||||
("system:capture_1", "fx_in:input_0"),
|
("system:capture_1", "fx_in:input_0"),
|
||||||
@@ -934,18 +937,33 @@ class JackAudioClient:
|
|||||||
logger.warning("Failed to connect %s -> %s: %s",
|
logger.warning("Failed to connect %s -> %s: %s",
|
||||||
sys_port_name, port.name, exc)
|
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):
|
for i, port in enumerate(self._outports):
|
||||||
suffix = f"playback_{i + 1}" if self._output_channels > 1 else "playback_1"
|
if self._output_channels == 1:
|
||||||
sys_port_name = f"system:{suffix}"
|
# Mono: connect to both playback_1 and playback_2
|
||||||
try:
|
for ch in [1, 2]:
|
||||||
sys_ports = self._client.get_ports(sys_port_name)
|
sys_port_name = f"system:playback_{ch}"
|
||||||
if sys_ports:
|
try:
|
||||||
self._client.connect(port, sys_ports[0])
|
sys_ports = self._client.get_ports(sys_port_name)
|
||||||
logger.debug("Connected %s -> %s", port.name, sys_port_name)
|
if sys_ports:
|
||||||
except Exception as exc:
|
self._client.connect(port, sys_ports[0])
|
||||||
logger.warning("Failed to connect %s -> %s: %s",
|
logger.debug("Mono: connected %s -> %s", port.name, sys_port_name)
|
||||||
port.name, sys_port_name, exc)
|
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 ────────────────────────────────────
|
# ── Runtime reconfiguration ────────────────────────────────────
|
||||||
|
|
||||||
@@ -983,6 +1001,9 @@ class JackAudioClient:
|
|||||||
for ch in range(self._input_channels):
|
for ch in range(self._input_channels):
|
||||||
port_buf = self._inports[ch].get_array()
|
port_buf = self._inports[ch].get_array()
|
||||||
self._in_buf[ch, :frames] = port_buf[:frames]
|
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 ─────────────────────────────────
|
# ── Run through pipeline ─────────────────────────────────
|
||||||
if self._input_channels == 1:
|
if self._input_channels == 1:
|
||||||
@@ -992,8 +1013,11 @@ class JackAudioClient:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
audio_out = self._pipeline.process(audio_in)
|
audio_out = self._pipeline.process(audio_in)
|
||||||
except Exception:
|
except Exception as exc:
|
||||||
# Never let an exception escape the RT callback
|
# 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 \
|
audio_out = np.zeros(frames, dtype=np.float32) if self._output_channels == 1 else \
|
||||||
np.zeros((self._output_channels, frames), dtype=np.float32)
|
np.zeros((self._output_channels, frames), dtype=np.float32)
|
||||||
|
|
||||||
|
|||||||
+6
-8
@@ -1748,16 +1748,14 @@ class WebServer:
|
|||||||
"channel_mode": self._channel_mgr.mode,
|
"channel_mode": self._channel_mgr.mode,
|
||||||
# Audio levels from pipeline (RMS float32 normalized 0.0-1.0, scaled to 0-100)
|
# Audio levels from pipeline (RMS float32 normalized 0.0-1.0, scaled to 0-100)
|
||||||
"input_level": (
|
"input_level": (
|
||||||
min(100, round((getattr(pl, '_input_level', 0.0) or 0.0) * 150))
|
0
|
||||||
if pl and isinstance(getattr(pl, '_input_level', None), (int, float))
|
if pl is None or getattr(pl, '_input_level', None) is None
|
||||||
and not math.isnan(getattr(pl, '_input_level', 0.0))
|
else max(0, min(100, round(0 if (v := float(getattr(pl, '_input_level', 0.0))) != v else v * 150)))
|
||||||
else 0
|
|
||||||
),
|
),
|
||||||
"output_level": (
|
"output_level": (
|
||||||
min(100, round((getattr(pl, '_output_level', 0.0) or 0.0) * 150))
|
0
|
||||||
if pl and isinstance(getattr(pl, '_output_level', None), (int, float))
|
if pl is None or getattr(pl, '_output_level', None) is None
|
||||||
and not math.isnan(getattr(pl, '_output_level', 0.0))
|
else max(0, min(100, round(0 if (v := float(getattr(pl, '_output_level', 0.0))) != v else v * 150)))
|
||||||
else 0
|
|
||||||
),
|
),
|
||||||
# System stats
|
# System stats
|
||||||
"cpu_percent": self._gather_system_stats().get("cpu_percent", 0),
|
"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" />
|
<meta name="theme-color" content="#0A0A0C" />
|
||||||
<title>NAM Pedal</title>
|
<title>NAM Pedal</title>
|
||||||
<link rel="icon" type="image/svg+xml" href="/ui/favicon.svg" />
|
<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">
|
<link rel="stylesheet" crossorigin href="/ui/assets/index-B-zxH4pN.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
Reference in New Issue
Block a user