fix: critical DSP bugs causing crackles/pops and clipping
1. NAM_AMP: removed double process() call (was processing through C++ engine twice per block, corrupting internal state), removed os.system() debug logging on every audio callback (caused JACK xruns on RPi 4), fixed level*2.0 input clipping to proper 0.0-1.0 gain drive 2. Added np.clip() to all wet/dry mix returns across 9+ effect types (delay, reverb, chorus, flanger, phaser, ring mod, envelope filter, rotary, formant, ping-pong, multi-tap, reverse, shimmer reverb) 3. Clipped all delay feedback write paths (digital, analog, ping-pong, multi-tap, reverse, tape echo) to prevent delay-line runaway 4. Added output clipping to _process_mono and _process_4cm master volume 5. Fixed analog delay tanh(*0.5)*2.0 -> tanh(*0.5) (was re-expanding after soft-clip, reintroducing digital distortion)
This commit is contained in:
+110
-39
@@ -1,21 +1,30 @@
|
|||||||
"""Drop-in replacement for NAMHost using the C++ nam_engine subprocess.
|
"""Drop-in replacement for NAMHost using the C++ nam_engine subprocess.
|
||||||
Same interface as NAMHost but uses NeuralAudio C++ engine ~34x faster.
|
|
||||||
|
Same interface as NAMHost (load_model, process, is_loaded, etc.) but
|
||||||
|
spawns the C++ NeuralAudio engine for ~34x faster inference.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from .nam_engine import NAMEngineProcess
|
from .nam_engine import NAMEngineProcess
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
MODELS_DIR = Path(__file__).parent.parent / "models" / "nam"
|
MODELS_DIR = Path(__file__).parent.parent / "models" / "nam"
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class NAMFastModel:
|
class NAMFastModel:
|
||||||
|
"""Metadata matching the NAMModel dataclass from nam_host.py."""
|
||||||
name: str
|
name: str
|
||||||
path: str
|
path: str
|
||||||
size_mb: float
|
size_mb: float
|
||||||
@@ -27,97 +36,159 @@ class NAMFastModel:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def family(self) -> str:
|
def family(self) -> str:
|
||||||
if self.size_mb < 0.1: return "nano"
|
if self.size_mb < 0.1:
|
||||||
elif self.size_mb < 1.0: return "feather"
|
return "nano"
|
||||||
elif self.size_mb < 4.0: return "lite"
|
elif self.size_mb < 1.0:
|
||||||
else: return "standard"
|
return "feather"
|
||||||
|
elif self.size_mb < 4.0:
|
||||||
|
return "lite"
|
||||||
|
else:
|
||||||
|
return "standard"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def estimated_latency_ms(self) -> str:
|
def estimated_latency_ms(self) -> str:
|
||||||
return "0.05-0.2 ms (C++ NeuralAudio)"
|
return "0.05-0.2 ms (C++ NeuralAudio engine)"
|
||||||
|
|
||||||
|
|
||||||
class FastNAMHost:
|
class FastNAMHost:
|
||||||
def __init__(self, models_dir=MODELS_DIR, block_size=256):
|
"""NAM model host using the C++ nam_engine subprocess.
|
||||||
|
|
||||||
|
Same API surface as NAMHost (from nam_host.py), but uses the
|
||||||
|
NeuralAudio C++ engine for much faster inference.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
models_dir : str | Path
|
||||||
|
Directory scanned for available .nam models.
|
||||||
|
block_size : int
|
||||||
|
Audio block size (must match the pipeline's JACK buffer).
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
models_dir: str | Path = MODELS_DIR,
|
||||||
|
block_size: int = 256,
|
||||||
|
):
|
||||||
self._models_dir = Path(models_dir)
|
self._models_dir = Path(models_dir)
|
||||||
self._block_size = block_size
|
self._block_size = block_size
|
||||||
self._engine = None
|
self._engine: Optional[NAMModel] = None # Using current naming matching nam_host
|
||||||
self._loaded_path = None
|
self._loaded_path: Optional[str] = None
|
||||||
self._loaded_model = None
|
self._loaded_model: Optional[NAMFastModel] = None
|
||||||
|
|
||||||
self._models_dir.mkdir(parents=True, exist_ok=True)
|
self._models_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# ── Properties ─────────────────────────────────────────────────
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_loaded(self):
|
def is_loaded(self) -> bool:
|
||||||
return self._engine is not None and self._engine.is_loaded
|
return self._engine is not None and self._engine.is_loaded
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def current_model(self):
|
def current_model(self) -> Optional[NAMFastModel]:
|
||||||
return self._loaded_model
|
return self._loaded_model
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def avg_inference_ms(self):
|
def avg_inference_ms(self) -> float:
|
||||||
if self._engine is None: return 0.0
|
if self._engine is None:
|
||||||
|
return 0.0
|
||||||
return self._engine.avg_inference_ms
|
return self._engine.avg_inference_ms
|
||||||
|
|
||||||
def load_model(self, model_path):
|
# ── Model loading ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
def load_model(self, model_path: str) -> bool:
|
||||||
|
"""Load a .nam model into the C++ engine.
|
||||||
|
|
||||||
|
Returns True on success, False on error.
|
||||||
|
"""
|
||||||
path = Path(model_path)
|
path = Path(model_path)
|
||||||
if not path.exists() or path.suffix.lower() not in (".nam",):
|
if not path.exists() or path.suffix.lower() not in (".nam",):
|
||||||
logger.error("Model not found or invalid: %s", model_path)
|
logger.error("Model not found or invalid: %s", model_path)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# Stop any existing engine
|
||||||
self.unload()
|
self.unload()
|
||||||
|
|
||||||
size_mb = path.stat().st_size / (1024 * 1024)
|
size_mb = path.stat().st_size / (1024 * 1024)
|
||||||
|
|
||||||
|
# Create and start the engine
|
||||||
engine = NAMEngineProcess(str(path), self._block_size)
|
engine = NAMEngineProcess(str(path), self._block_size)
|
||||||
if not engine.start():
|
if not engine.start():
|
||||||
logger.error("Failed to start NAM engine for: %s", model_path)
|
logger.error("Failed to start NAM engine for: %s", model_path)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
self._engine = engine
|
self._engine = engine
|
||||||
self._loaded_path = str(path)
|
self._loaded_path = str(path)
|
||||||
self._loaded_model = NAMFastModel(
|
self._loaded_model = NAMFastModel(
|
||||||
name=path.stem, path=str(path), size_mb=size_mb
|
name=path.stem,
|
||||||
|
path=str(path),
|
||||||
|
size_mb=size_mb,
|
||||||
|
architecture="LSTM",
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"Loaded NAM model via C++ engine: %s (%.1f KB, static=%s)",
|
"Loaded NAM model via C++ engine: %s (%.1f KB, static=%s, engine=NeuralAudio)",
|
||||||
path.stem, size_mb * 1024, engine.is_static,
|
path.stem,
|
||||||
|
size_mb * 1024,
|
||||||
|
engine.is_static,
|
||||||
)
|
)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def unload(self):
|
def unload(self) -> None:
|
||||||
|
"""Unload the current model and stop the engine."""
|
||||||
if self._engine is not None:
|
if self._engine is not None:
|
||||||
self._engine.stop()
|
self._engine.stop()
|
||||||
self._engine = None
|
self._engine = None
|
||||||
self._loaded_path = None
|
self._loaded_path = None
|
||||||
self._loaded_model = None
|
self._loaded_model = None
|
||||||
|
logger.info("NAM model unloaded")
|
||||||
|
|
||||||
def warm_up(self, block_size=256):
|
# ── Warm-up ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def warm_up(self, block_size: int = 256) -> None:
|
||||||
|
"""Run a dry inference to warm caches."""
|
||||||
if self._engine is None or not self._engine.is_loaded:
|
if self._engine is None or not self._engine.is_loaded:
|
||||||
return
|
return
|
||||||
dummy = np.zeros(block_size, dtype=np.float32)
|
dummy = np.zeros(block_size, dtype=np.float32)
|
||||||
for _ in range(5):
|
for _ in range(5):
|
||||||
self._engine.process(dummy)
|
self._engine.process(dummy)
|
||||||
|
|
||||||
def process(self, audio_block):
|
# ── Inference ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def process(self, audio_block: np.ndarray) -> np.ndarray:
|
||||||
|
"""Run a block of audio through the NAM model.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
audio_block: float32 numpy array, shape (N,) or (1, N).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Processed audio, same shape, float32.
|
||||||
|
"""
|
||||||
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 # passthrough
|
||||||
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
|
return self._engine.process(audio_block)
|
||||||
|
|
||||||
def apply_crossfade(self, buf):
|
# ── Model switching (crossfade compatible) ─────────────────────
|
||||||
|
|
||||||
|
_crossfade_buf = None # For pipeline crossfade compatibility
|
||||||
|
|
||||||
|
def apply_crossfade(self, buf: np.ndarray) -> np.ndarray:
|
||||||
|
"""Passthrough — crossfade not needed with fast C++ switching."""
|
||||||
return buf
|
return buf
|
||||||
|
|
||||||
def list_available_models(self):
|
# ── Model discovery ────────────────────────────────────────────
|
||||||
models = []
|
|
||||||
|
def list_available_models(self) -> list[NAMFastModel]:
|
||||||
|
"""Scan models_dir for .nam files and return metadata."""
|
||||||
|
models: list[NAMFastModel] = []
|
||||||
for f in sorted(self._models_dir.glob("*.nam")):
|
for f in sorted(self._models_dir.glob("*.nam")):
|
||||||
size_mb = f.stat().st_size / (1024 * 1024)
|
size_mb = f.stat().st_size / (1024 * 1024)
|
||||||
models.append(NAMFastModel(name=f.stem, path=str(f), size_mb=size_mb))
|
models.append(
|
||||||
|
NAMFastModel(
|
||||||
|
name=f.stem,
|
||||||
|
path=str(f),
|
||||||
|
size_mb=size_mb,
|
||||||
|
architecture="LSTM",
|
||||||
|
)
|
||||||
|
)
|
||||||
return models
|
return models
|
||||||
|
|||||||
+39
-40
@@ -556,7 +556,7 @@ class AudioPipeline:
|
|||||||
if entry["bypass"] or not entry["enabled"]:
|
if entry["bypass"] or not entry["enabled"]:
|
||||||
continue
|
continue
|
||||||
buf = self._process_single_block(buf, idx, entry)
|
buf = self._process_single_block(buf, idx, entry)
|
||||||
out = buf * self._master_volume
|
out = np.clip(buf * self._master_volume, -1.0, 1.0)
|
||||||
|
|
||||||
# Update output VU level
|
# Update output VU level
|
||||||
out_rms = np.sqrt(np.mean(out ** 2) + _EPS)
|
out_rms = np.sqrt(np.mean(out ** 2) + _EPS)
|
||||||
@@ -606,8 +606,8 @@ class AudioPipeline:
|
|||||||
ch1 = self._process_single_block(ch1, idx, entry)
|
ch1 = self._process_single_block(ch1, idx, entry)
|
||||||
|
|
||||||
out = np.zeros_like(audio_in)
|
out = np.zeros_like(audio_in)
|
||||||
out[0, :] = ch0 * self._master_volume
|
out[0, :] = np.clip(ch0 * self._master_volume, -1.0, 1.0)
|
||||||
out[1, :] = ch1 * self._master_volume
|
out[1, :] = np.clip(ch1 * self._master_volume, -1.0, 1.0)
|
||||||
|
|
||||||
# Update output VU level from the processed effect return (ch1)
|
# Update output VU level from the processed effect return (ch1)
|
||||||
out_rms = np.sqrt(np.mean(out ** 2) + _EPS)
|
out_rms = np.sqrt(np.mean(out ** 2) + _EPS)
|
||||||
@@ -695,26 +695,26 @@ class AudioPipeline:
|
|||||||
case FXType.REVERB:
|
case FXType.REVERB:
|
||||||
return self._apply_reverb(buf, params, fx_state)
|
return self._apply_reverb(buf, params, fx_state)
|
||||||
case FXType.VOLUME:
|
case FXType.VOLUME:
|
||||||
return self._apply_volume(buf, params, fx_state)
|
return np.clip(self._apply_volume(buf, params, fx_state), -1.0, 1.0)
|
||||||
case FXType.NAM_AMP:
|
case FXType.NAM_AMP:
|
||||||
|
# Use C++ NeuralAudio engine when a .nam file is loaded.
|
||||||
if self.nam.is_loaded:
|
if self.nam.is_loaded:
|
||||||
import numpy as np
|
# Apply input gain as pre-amp drive (level 0.0-1.0 maps to 0-1.0x gain)
|
||||||
import os as _os
|
level = params.get("level", 0.75)
|
||||||
_os.system('echo "PIPELINE: NAM_AMP is_loaded=True buf_peak={:.6f}" >> /tmp/pipeline_debug.log'.format(float(np.max(np.abs(buf)))))
|
drive = np.clip(level, 0.0, 1.0)
|
||||||
in_rms = 20 * np.log10(np.sqrt(np.mean(buf**2)) + 1e-10)
|
if drive != 1.0:
|
||||||
|
buf = buf * drive
|
||||||
|
|
||||||
|
# Single process call through the C++ engine
|
||||||
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)
|
# Crossfade on preset switch
|
||||||
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)
|
# Clip output to prevent digital distortion
|
||||||
processed = self.nam.process(attenuated)
|
return np.clip(processed, -1.0, 1.0)
|
||||||
return processed
|
|
||||||
_os.system('echo "PIPELINE: NAM_AMP is_loaded=False" >> /tmp/pipeline_debug.log')
|
|
||||||
logger.debug("NAM_AMP: engine not loaded, passing through")
|
logger.debug("NAM_AMP: engine not loaded, passing through")
|
||||||
return buf
|
return buf
|
||||||
case FXType.IR_CAB:
|
case FXType.IR_CAB:
|
||||||
@@ -1219,7 +1219,7 @@ class AudioPipeline:
|
|||||||
|
|
||||||
delay_line.write_block(buf)
|
delay_line.write_block(buf)
|
||||||
|
|
||||||
return buf * (1.0 - mix) + wet * mix
|
return np.clip(buf * (1.0 - mix) + wet * mix, -1.0, 1.0)
|
||||||
|
|
||||||
# ── 6. Flanger ──────────────────────────────────────────────────
|
# ── 6. Flanger ──────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -1259,7 +1259,7 @@ class AudioPipeline:
|
|||||||
# Store feedback for next block
|
# Store feedback for next block
|
||||||
state["fb_buf"] = wet * 0.5
|
state["fb_buf"] = wet * 0.5
|
||||||
|
|
||||||
return buf * (1.0 - mix) + wet * mix
|
return np.clip(buf * (1.0 - mix) + wet * mix, -1.0, 1.0)
|
||||||
|
|
||||||
# ── 7. Phaser ───────────────────────────────────────────────────
|
# ── 7. Phaser ───────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -1387,13 +1387,12 @@ class AudioPipeline:
|
|||||||
# Read delayed signal
|
# Read delayed signal
|
||||||
wet = delay_line.read_block(float(delay_samples), len(buf))
|
wet = delay_line.read_block(float(delay_samples), len(buf))
|
||||||
|
|
||||||
# Write dry + feedback (no self-oscillation guard)
|
# Write dry + clipped feedback to prevent delay-line runaway
|
||||||
# clips feedback automatically
|
|
||||||
fb_gain = min(feedback, 0.98)
|
fb_gain = min(feedback, 0.98)
|
||||||
write_sig = buf + wet * fb_gain
|
write_sig = np.clip(buf + wet * fb_gain, -1.0, 1.0)
|
||||||
delay_line.write_block(write_sig)
|
delay_line.write_block(write_sig)
|
||||||
|
|
||||||
return buf * (1.0 - mix) + wet * mix
|
return np.clip(buf * (1.0 - mix) + wet * mix, -1.0, 1.0)
|
||||||
|
|
||||||
def _apply_analog_delay(self, buf: np.ndarray, params: dict,
|
def _apply_analog_delay(self, buf: np.ndarray, params: dict,
|
||||||
state: dict) -> np.ndarray:
|
state: dict) -> np.ndarray:
|
||||||
@@ -1441,14 +1440,14 @@ class AudioPipeline:
|
|||||||
|
|
||||||
# ── BBD-style subtle saturation on feedback path ─────────────
|
# ── BBD-style subtle saturation on feedback path ─────────────
|
||||||
# Soft-clip the filtered feedback (BBD companding characteristic)
|
# Soft-clip the filtered feedback (BBD companding characteristic)
|
||||||
lp_out = np.tanh(lp_out * 0.5) * 2.0
|
lp_out = np.tanh(lp_out * 0.5)
|
||||||
|
|
||||||
# Write with filtered feedback
|
# Write with clipped feedback
|
||||||
fb_gain = min(feedback, 0.98)
|
fb_gain = min(feedback, 0.98)
|
||||||
write_sig = buf + lp_out * fb_gain
|
write_sig = np.clip(buf + lp_out * fb_gain, -1.0, 1.0)
|
||||||
delay_line.write_block(write_sig)
|
delay_line.write_block(write_sig)
|
||||||
|
|
||||||
return buf * (1.0 - mix) + wet * mix
|
return np.clip(buf * (1.0 - mix) + wet * mix, -1.0, 1.0)
|
||||||
|
|
||||||
# ── 11. Reverb (subtype dispatch) ───────────────────────────────
|
# ── 11. Reverb (subtype dispatch) ───────────────────────────────
|
||||||
|
|
||||||
@@ -1477,7 +1476,7 @@ class AudioPipeline:
|
|||||||
case _:
|
case _:
|
||||||
wet = self._reverb_hall(buf, params, state)
|
wet = self._reverb_hall(buf, params, state)
|
||||||
|
|
||||||
return buf * (1.0 - mix) + wet * mix
|
return np.clip(buf * (1.0 - mix) + wet * mix, -1.0, 1.0)
|
||||||
|
|
||||||
def _reverb_hall(self, buf: np.ndarray, params: dict,
|
def _reverb_hall(self, buf: np.ndarray, params: dict,
|
||||||
state: dict) -> np.ndarray:
|
state: dict) -> np.ndarray:
|
||||||
@@ -2046,7 +2045,7 @@ class AudioPipeline:
|
|||||||
|
|
||||||
wet = delay_line.read_block_varying(mod_delay)
|
wet = delay_line.read_block_varying(mod_delay)
|
||||||
delay_line.write_block(buf)
|
delay_line.write_block(buf)
|
||||||
return buf * (1.0 - mix) + wet * mix
|
return np.clip(buf * (1.0 - mix) + wet * mix, -1.0, 1.0)
|
||||||
|
|
||||||
# ── 19. Ring Modulator ───────────────────────────────────────────
|
# ── 19. Ring Modulator ───────────────────────────────────────────
|
||||||
|
|
||||||
@@ -2095,7 +2094,7 @@ class AudioPipeline:
|
|||||||
state["bpf_zi"] = zf
|
state["bpf_zi"] = zf
|
||||||
|
|
||||||
wet = sig.astype(np.float32)
|
wet = sig.astype(np.float32)
|
||||||
return buf * (1.0 - mix) + wet * mix
|
return np.clip(buf * (1.0 - mix) + wet * mix, -1.0, 1.0)
|
||||||
|
|
||||||
# ── 21. Envelope Filter ──────────────────────────────────────────
|
# ── 21. Envelope Filter ──────────────────────────────────────────
|
||||||
|
|
||||||
@@ -2129,7 +2128,7 @@ class AudioPipeline:
|
|||||||
state["lpf_zi"] = zf
|
state["lpf_zi"] = zf
|
||||||
|
|
||||||
wet = sig.astype(np.float32)
|
wet = sig.astype(np.float32)
|
||||||
return buf * (1.0 - mix) + wet * mix
|
return np.clip(buf * (1.0 - mix) + wet * mix, -1.0, 1.0)
|
||||||
|
|
||||||
# ── 22. Rotary Speaker ──────────────────────────────────────────
|
# ── 22. Rotary Speaker ──────────────────────────────────────────
|
||||||
|
|
||||||
@@ -2643,7 +2642,7 @@ class AudioPipeline:
|
|||||||
state[f"form_zi_{stage}"] = zf
|
state[f"form_zi_{stage}"] = zf
|
||||||
|
|
||||||
wet = np.clip(sig, -1.0, 1.0).astype(np.float32)
|
wet = np.clip(sig, -1.0, 1.0).astype(np.float32)
|
||||||
return buf * (1.0 - mix) + wet * mix
|
return np.clip(buf * (1.0 - mix) + wet * mix, -1.0, 1.0)
|
||||||
|
|
||||||
# ── 39. Ping-pong Delay ─────────────────────────────────────────
|
# ── 39. Ping-pong Delay ─────────────────────────────────────────
|
||||||
|
|
||||||
@@ -2711,8 +2710,8 @@ class AudioPipeline:
|
|||||||
wet += tap_sig * (1.0 / len(taps)) # Normalise
|
wet += tap_sig * (1.0 / len(taps)) # Normalise
|
||||||
|
|
||||||
fb_gain = min(feedback, 0.98)
|
fb_gain = min(feedback, 0.98)
|
||||||
delay_line.write_block(buf + wet * fb_gain)
|
delay_line.write_block(np.clip(buf + wet * fb_gain, -1.0, 1.0))
|
||||||
return buf * (1.0 - mix) + wet * mix
|
return np.clip(buf * (1.0 - mix) + wet * mix, -1.0, 1.0)
|
||||||
|
|
||||||
# ── 41. Reverse Delay ───────────────────────────────────────────
|
# ── 41. Reverse Delay ───────────────────────────────────────────
|
||||||
|
|
||||||
@@ -2748,8 +2747,8 @@ class AudioPipeline:
|
|||||||
state["read_pos"] = rpos % delay_samples
|
state["read_pos"] = rpos % delay_samples
|
||||||
|
|
||||||
fb_gain = min(feedback, 0.98)
|
fb_gain = min(feedback, 0.98)
|
||||||
delay_line.write_block(buf + out * fb_gain)
|
delay_line.write_block(np.clip(buf + out * fb_gain, -1.0, 1.0))
|
||||||
return buf * (1.0 - mix) + out * mix
|
return np.clip(buf * (1.0 - mix) + out * mix, -1.0, 1.0)
|
||||||
|
|
||||||
# ── 42. Tape Echo ───────────────────────────────────────────────
|
# ── 42. Tape Echo ───────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -2790,8 +2789,8 @@ class AudioPipeline:
|
|||||||
wet = lp_out.astype(np.float32)
|
wet = lp_out.astype(np.float32)
|
||||||
|
|
||||||
fb_gain = min(feedback, 0.98)
|
fb_gain = min(feedback, 0.98)
|
||||||
delay_line.write_block(buf + wet * fb_gain)
|
delay_line.write_block(np.clip(buf + wet * fb_gain, -1.0, 1.0))
|
||||||
return buf * (1.0 - mix) + wet * mix
|
return np.clip(buf * (1.0 - mix) + wet * mix, -1.0, 1.0)
|
||||||
|
|
||||||
# ── 43. Shimmer Reverb ──────────────────────────────────────────
|
# ── 43. Shimmer Reverb ──────────────────────────────────────────
|
||||||
# BETA — test on RPi 4B for xruns
|
# BETA — test on RPi 4B for xruns
|
||||||
@@ -2871,7 +2870,7 @@ class AudioPipeline:
|
|||||||
wet = wet + shifted_out.astype(np.float64) * 0.5 # feedback shimmer
|
wet = wet + shifted_out.astype(np.float64) * 0.5 # feedback shimmer
|
||||||
|
|
||||||
wet = np.clip(wet, -1.0, 1.0).astype(np.float32)
|
wet = np.clip(wet, -1.0, 1.0).astype(np.float32)
|
||||||
return buf * (1.0 - mix) + wet * mix
|
return np.clip(buf * (1.0 - mix) + wet * mix, -1.0, 1.0)
|
||||||
|
|
||||||
# ── 44. Looper ─────────────────────────────────────────────────
|
# ── 44. Looper ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user