refactor: simplify FIR baking to lean 4-channel post-convolver limiting and update ARCHITECTURE.md
This commit is contained in:
@@ -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`.**
|
||||
|
||||
@@ -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"]
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user