refactor: simplify FIR baking to lean 4-channel post-convolver limiting and update ARCHITECTURE.md

This commit is contained in:
mynameisdeleted
2026-09-01 11:27:58 -04:00
parent cb263f6201
commit 9f42c52c8e
2 changed files with 22 additions and 75 deletions

View File

@@ -28,18 +28,18 @@ $$\text{FIR}_{\text{baked}}(t) = \text{EQ}_{\text{user}}(t) * \text{EQ}_{\text{v
```mermaid
graph TD
A["Raw Audio Input (FL / FR)"] --> B["Bankstown VirtualBass (2nd/3rd Order Harmonics)"]
B -->|4-Channel Direct Feed| C1["Woofer Main FIR (baked-woofers-*.wav)<br/>Bakes User EQ + Crossover (5.0ms Lead)"]
B -->|4-Channel Direct Feed| C2["Woofer Lookahead FIR (baked-lookahead-woofers-*.wav)<br/>Bakes User EQ + Crossover (0.0ms Lead)"]
B -->|4-Channel Direct Feed| D1["Tweeter Main FIR (baked-tweeters-*.wav)<br/>Bakes User EQ + Crossover (5.0ms Lead)"]
B -->|4-Channel Direct Feed| D2["Tweeter Lookahead FIR (baked-lookahead-tweeters-*.wav)<br/>Bakes User EQ + Crossover (0.0ms Lead)"]
B -->|4-Channel Direct Feed| C1["Woofer Left FIR (baked-woofers-*.wav)"]
B -->|4-Channel Direct Feed| C2["Woofer Right FIR (baked-woofers-*.wav)"]
B -->|4-Channel Direct Feed| D1["Tweeter Left FIR (baked-tweeters-*.wav)"]
B -->|4-Channel Direct Feed| D2["Tweeter Right FIR (baked-tweeters-*.wav)"]
C1 --> E["Woofer Limiter (wlim:in)"]
C2 -->|Sidechain Advance| E
D1 --> F["Tweeter Limiter (tlim:in)"]
D2 -->|Sidechain Advance| F
C1 --> E1["Woofer Limiter / Dynamic Protection (wlim:in_1)"]
C2 --> E1["Woofer Limiter / Dynamic Protection (wlim:in_2)"]
D1 --> F1["Tweeter Limiter / Dynamic Protection (tlim:in_1)"]
D2 --> F1["Tweeter Limiter / Dynamic Protection (tlim:in_2)"]
E --> G1["Woofer Drivers (Left / Right)"]
F --> G2["Tweeter Drivers (Left / Right)"]
E1 --> G1["Woofer Drivers (Left / Right)"]
F1 --> G2["Tweeter Drivers (Left / Right)"]
```
### 2.2 Single-Pass FIR Baking Pipeline Process
@@ -50,29 +50,17 @@ graph TD
A["Raw Acoustic Driver Measurement<br/>(White-Noise Impulse Response)"] --> B["White-to-Pink Voicing Curve<br/>(-3 dB/octave Tonal Tilt)"]
B --> C["User EQ Curves (user_eq.json)<br/>(Bass Shelves, Peaking EQs, Treble Shelves)"]
C --> D["Crossover Filters<br/>(Linkwitz-Riley High-Pass / Low-Pass)"]
D --> E1["Main FIR Path:<br/>Latency Trimming to 5.0ms Lead"]
D --> E2["Lookahead Sidechain Path:<br/>Lead Trimmed to Peak Index (0.0ms Lead)"]
E1 --> F1["True-Peak ISP Guarding (-0.5 dBFS Ceiling)"]
E2 --> F2["True-Peak ISP Guarding (-0.5 dBFS Ceiling)"]
F1 --> G1["Tail Resolution Fadeout (2048-sample Cosine Window)"]
F2 --> G2["Tail Resolution Fadeout (2048-sample Cosine Window)"]
G1 --> H1["baked-woofers-*.wav / baked-tweeters-*.wav<br/>(Main Audio Path)"]
G2 --> H2["baked-lookahead-woofers-*.wav / baked-lookahead-tweeters-*.wav<br/>(5.0ms Advance Sidechain Path)"]
D --> E["Latency Trimming (5.0ms Lead Optimization)"]
E --> F["True-Peak ISP Guarding (-0.5 dBFS Ceiling)"]
F --> G["Tail Resolution Fadeout (2048-sample Cosine Window)"]
G --> H["baked-woofers-*.wav / baked-tweeters-*.wav<br/>(Single-Pass Baked Driver FIRs)"]
```
### 2.3 Parallel Lookahead Sidechain Limiting
To prevent speaker cone over-excursion and thermal overload without adding buffer delay to the listener:
### 2.3 Post-Convolver Quad-Driver Limiting & Protection
To prevent speaker cone over-excursion and thermal overload without CPU convolver overhead:
1. **Main Path FIR (`baked-woofers-48k.wav` / `baked-tweeters-48k.wav`):**
* Trims impulse response pre-delay lead to exactly **5.0 ms** ($240\text{ samples}$ @ $48\text{ kHz}$).
* Delays the main audio peak relative to playback start by $5.0\text{ ms}$.
2. **Parallel Lookahead Sidechain FIR (`baked-lookahead-woofers-48k.wav` / `baked-lookahead-tweeters-48k.wav`):**
* Starts directly at the absolute peak index ($0.0\text{ ms}$ lead).
* Outputs peak control signals **5.0 ms ahead** of the main audio path.
1. **Post-Convolver Weighting:** Fast lookahead limiters (`wlim` and `tlim`) operate directly on the 4 post-convolver driver channels, accurately measuring the exact equalized waveform present at the driver terminals.
2. **Dedicated Driver Thresholds:** Woofers (`wlim`) and Tweeters (`tlim`) have independent limit thresholds (`-2 dBFS` for woofers, `-1 dBFS` for tweeters) and release characteristics tuned specifically for their respective physical driver excursion limits.
3. **Zero Added Listener Latency:**
* Post-convolver limiters (`wlim` and `tlim`) receive peak warnings 5.0 ms before the audio reaches the speaker drivers.
* **Added buffer latency for the listener: `0.0 ms`.**

View File

@@ -226,53 +226,18 @@ def optimize_fir_latency_and_tail(samples, fs=48000, is_woofer=False):
return cropped
def optimize_fir_lookahead_sidechain(samples, fs=48000, is_woofer=False):
# 1. Find absolute peak index
peak_idx = 0
max_val = 0.0
for i, s in enumerate(samples):
if abs(s) > max_val:
max_val = abs(s)
peak_idx = i
# Lookahead control path starts directly at peak_idx (advanced by 5.0ms relative to 5.0ms lead main path)
cropped = samples[peak_idx:]
target_len = 16384 if is_woofer else 8192
if len(cropped) < target_len:
tail_pad = target_len - len(cropped)
cropped.extend([0.0] * tail_pad)
elif len(cropped) > target_len:
cropped = cropped[:target_len]
# Smooth exponential tail fadeout over last 2048 samples
fade_len = 2048
for i in range(fade_len):
idx = len(cropped) - fade_len + i
fade = 0.5 * (1.0 + math.cos(math.pi * i / fade_len))
cropped[idx] *= fade
return cropped
def bake_driver_ir(src_wav, dst_wav, dst_lk_wav=None, is_woofer=False, driver_gain=1.0):
def bake_driver_ir(src_wav, dst_wav, is_woofer=False, driver_gain=1.0):
if not os.path.exists(src_wav):
print(f"Warning: {src_wav} not found, skipping.")
return False
orig_samples, fs = read_wav_floats(src_wav)
# 1. Optimize Latency (5ms Lead) + Extend Woofer/Tweeter Lopsided Tail Resolution (Main Path)
# 1. Optimize Latency (5ms Lead) + Extend Woofer/Tweeter Lopsided Tail Resolution
samples = optimize_fir_latency_and_tail(list(orig_samples), fs=fs, is_woofer=is_woofer)
samples = apply_true_peak_guard(samples, max_allowed_dbfs=-0.5)
write_wav_floats(dst_wav, samples, fs)
print(f"==> Baked Main FIR {os.path.basename(dst_wav)} ({fs} Hz, {len(samples)} taps, gain={driver_gain}x)")
# 2. Parallel Lookahead Sidechain FIR (Advanced by 5.0ms)
if dst_lk_wav:
samples_lk = optimize_fir_lookahead_sidechain(list(orig_samples), fs=fs, is_woofer=is_woofer)
samples_lk = apply_true_peak_guard(samples_lk, max_allowed_dbfs=-0.5)
write_wav_floats(dst_lk_wav, samples_lk, fs)
print(f"==> Baked Lookahead Sidechain FIR {os.path.basename(dst_lk_wav)} ({fs} Hz, {len(samples_lk)} taps)")
print(f"==> Baked FIR {os.path.basename(dst_wav)} ({fs} Hz, {len(samples)} taps, gain={driver_gain}x)")
return True
@@ -323,19 +288,14 @@ def generate_simple_graph_and_bake(profile_dir=None):
if "woofer" in basename.lower():
is_woofer = True
baked_basename = "baked-" + basename
baked_lk_basename = "baked-lookahead-" + basename
repo_dst_path = os.path.join(repo_151, baked_basename)
sys_dst_path = os.path.join(sys_dir, baked_basename)
repo_dst_lk_path = os.path.join(repo_151, baked_lk_basename)
sys_dst_lk_path = os.path.join(sys_dir, baked_lk_basename)
convolver_tasks[sys_path] = {
"basename": basename,
"is_woofer": is_woofer,
"gain": gain,
"repo_dst": repo_dst_path,
"sys_dst": sys_dst_path,
"repo_dst_lk": repo_dst_lk_path,
"sys_dst_lk": sys_dst_lk_path
"sys_dst": sys_dst_path
}
# 2. Bake FIR files dynamically for all discovered WAV targets
@@ -350,7 +310,6 @@ def generate_simple_graph_and_bake(profile_dir=None):
bake_driver_ir(
src_wav=src_path,
dst_wav=task["repo_dst"],
dst_lk_wav=task["repo_dst_lk"],
is_woofer=task["is_woofer"],
driver_gain=task["gain"]
)