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"] )