Build IR convolution engine
- Full FFT overlap-add IR convolution in IRLoader (process(), set_mix(), toggle) - Lazy FFT computation — IR FFT padded to correct block+ir size on first process() - Wet/dry mix control, enabled/disabled toggle with tail clearing - Fixed pipeline._apply_ir_cab() to delegate to IRLoader.process() instead of poking internals (old code had array-size mismatch bug: IR FFT at ir_len vs block FFT at conv_size) - 46 tests: loading, convolution correctness, overlap-add state, mix, toggle, directory listing, performance budget (all <5ms even at 8192 taps), edge cases - scripts/download_irs.sh: free IR pack downloader (God's Cab, Seacow)
This commit is contained in:
+18
-37
@@ -338,7 +338,7 @@ class AudioPipeline:
|
||||
buf = self.nam.process(buf)
|
||||
case FXType.IR_CAB:
|
||||
if self.ir.is_loaded:
|
||||
buf = self._apply_ir_cab(buf)
|
||||
buf = self._apply_ir_cab(buf, params, fx_state)
|
||||
|
||||
return buf * self._master_volume
|
||||
|
||||
@@ -797,45 +797,26 @@ class AudioPipeline:
|
||||
|
||||
# ── 13. IR Cabinet Simulator ─────────────────────────────────────
|
||||
|
||||
def _apply_ir_cab(self, buf: np.ndarray) -> np.ndarray:
|
||||
"""Apply IR convolution using FFT-based overlap-add.
|
||||
def _apply_ir_cab(self, buf: np.ndarray, params: dict,
|
||||
state: dict) -> np.ndarray:
|
||||
"""Apply IR convolution via IRLoader.process().
|
||||
|
||||
Uses the IR loader's pre-computed FFT for efficient
|
||||
block-based convolution. Handles the overlap-add state
|
||||
internally.
|
||||
Delegates to the IRLoader's FFT overlap-add engine.
|
||||
Supports wet/dry mix control per-preset.
|
||||
|
||||
Params:
|
||||
- ir_file: str (path to .wav IR) — already set via load_ir()
|
||||
- enabled: bool
|
||||
- wet: float 0.0-1.0
|
||||
- dry: float 0.0-1.0
|
||||
"""
|
||||
if self.ir._ir_data is None or self.ir._ir_fft is None:
|
||||
return buf
|
||||
# Update mix from preset params
|
||||
wet = params.get("wet", 1.0)
|
||||
dry = params.get("dry", 0.0)
|
||||
self.ir.set_mix(wet=wet, dry=dry)
|
||||
self.ir.enabled = params.get("enabled", True) and not params.get("bypass", False)
|
||||
|
||||
ir_len = len(self.ir._ir_data)
|
||||
block_len = len(buf)
|
||||
|
||||
# FFT block size: next power of 2 >= block + ir - 1
|
||||
fft_len = 1
|
||||
while fft_len < block_len + ir_len - 1:
|
||||
fft_len <<= 1
|
||||
|
||||
# FFT of input block
|
||||
block_fft = np.fft.rfft(buf, n=fft_len)
|
||||
|
||||
# Multiply in frequency domain
|
||||
out_fft = block_fft * self.ir._ir_fft
|
||||
|
||||
# IFFT
|
||||
convolved = np.fft.irfft(out_fft, n=fft_len)
|
||||
|
||||
# Overlap-add: keep previous tail if any
|
||||
tail = getattr(self.ir, '_conv_tail', np.array([], dtype=np.float32))
|
||||
if len(tail) > 0:
|
||||
convolved[:len(tail)] += tail
|
||||
|
||||
# Save tail for next block
|
||||
if ir_len > 1:
|
||||
self.ir._conv_tail = convolved[block_len:block_len + ir_len - 1].copy()
|
||||
else:
|
||||
self.ir._conv_tail = np.array([], dtype=np.float32)
|
||||
|
||||
return np.clip(convolved[:block_len], -1.0, 1.0).astype(np.float32)
|
||||
return self.ir.process(buf)
|
||||
|
||||
# ── Properties ─────────────────────────────────────────────────
|
||||
|
||||
|
||||
Reference in New Issue
Block a user