diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
index d1e1b7a..10ae750 100644
--- a/ARCHITECTURE.md
+++ b/ARCHITECTURE.md
@@ -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)
Bakes User EQ + Crossover (5.0ms Lead)"]
- B -->|4-Channel Direct Feed| C2["Woofer Lookahead FIR (baked-lookahead-woofers-*.wav)
Bakes User EQ + Crossover (0.0ms Lead)"]
- B -->|4-Channel Direct Feed| D1["Tweeter Main FIR (baked-tweeters-*.wav)
Bakes User EQ + Crossover (5.0ms Lead)"]
- B -->|4-Channel Direct Feed| D2["Tweeter Lookahead FIR (baked-lookahead-tweeters-*.wav)
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
(White-Noise Impulse Response)"] --> B["White-to-Pink Voicing Curve
(-3 dB/octave Tonal Tilt)"]
B --> C["User EQ Curves (user_eq.json)
(Bass Shelves, Peaking EQs, Treble Shelves)"]
C --> D["Crossover Filters
(Linkwitz-Riley High-Pass / Low-Pass)"]
-
- D --> E1["Main FIR Path:
Latency Trimming to 5.0ms Lead"]
- D --> E2["Lookahead Sidechain Path:
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
(Main Audio Path)"]
- G2 --> H2["baked-lookahead-woofers-*.wav / baked-lookahead-tweeters-*.wav
(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
(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`.**
diff --git a/bake-graph.py b/bake-graph.py
index 17541dc..bce8e04 100755
--- a/bake-graph.py
+++ b/bake-graph.py
@@ -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"]
)