"""
Studio Historical-2d - Episode 01: Cannae 216 BC
Sound Design: Audio Cross-Fade Bridge & Binaural Tactical Foley Assets
Director of Sound & Multilingual Localization: Arthur Pendelton
"""

import os
import wave
import struct
import math
import subprocess
import numpy as np

BASE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
AUDIO_VOICE_DIR = os.path.join(BASE_DIR, "episodes", "EP01_Cannae_216BC", "04_Audio_Voice")
BRIDGE_DIR = os.path.join(AUDIO_VOICE_DIR, "bridge")
SFX_DIR = os.path.join(AUDIO_VOICE_DIR, "binaural_sfx")

os.makedirs(BRIDGE_DIR, exist_ok=True)
os.makedirs(SFX_DIR, exist_ok=True)

SAMPLE_RATE = 44100

def write_wav(filepath, audio_data, sample_rate=SAMPLE_RATE):
    """
    Write float array (-1.0 to 1.0) to 16-bit PCM WAV.
    Accepts 1D (mono) or 2D (stereo, shape [N, 2]).
    """
    audio_data = np.clip(audio_data, -0.999, 0.999)
    if audio_data.ndim == 1:
        channels = 1
        int_data = (audio_data * 32767).astype(np.int16)
    else:
        channels = audio_data.shape[1]
        int_data = (audio_data * 32767).astype(np.int16)
    
    with wave.open(filepath, "wb") as wf:
        wf.setnchannels(channels)
        wf.setsampwidth(2)
        wf.setframerate(sample_rate)
        wf.writeframes(int_data.tobytes())

def read_wav(filepath):
    """
    Read WAV into float numpy array (-1.0 to 1.0) and sample rate.
    Returns (data, sample_rate).
    """
    with wave.open(filepath, "rb") as wf:
        sr = wf.getframerate()
        n_channels = wf.getnchannels()
        n_frames = wf.getnframes()
        raw = wf.readframes(n_frames)
        int_data = np.frombuffer(raw, dtype=np.int16)
        if n_channels == 1:
            data = int_data.astype(np.float32) / 32768.0
        else:
            data = int_data.reshape(-1, n_channels).astype(np.float32) / 32768.0
        return data, sr

def normalize_ebu_r128(input_wav, output_wav, target_lufs=-14.0, target_tp=-1.0):
    cmd = [
        "ffmpeg", "-y",
        "-i", input_wav,
        "-af", f"loudnorm=I={target_lufs}:TP={target_tp}:LRA=11.0",
        "-ar", str(SAMPLE_RATE),
        output_wav
    ]
    subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)
    return output_wav

# ==============================================================================
# PROCEDURAL ACOUSTIC GENERATORS
# ==============================================================================

def generate_volturnus_wind(duration_sec=5.0, sample_rate=SAMPLE_RATE):
    """
    Mô phỏng tiếng gió rít sa mạc Volturnus cuốn theo cát bụi khô khốc Apulia:
    Sử dụng Pink Noise + Time-Varying Bandpass Filter (280 Hz - 950 Hz) + Grit grains.
    """
    n_samples = int(duration_sec * sample_rate)
    t = np.linspace(0, duration_sec, n_samples, endpoint=False)
    
    # 1. White noise -> Pink noise filtering
    white_l = np.random.normal(0, 1, n_samples)
    white_r = np.random.normal(0, 1, n_samples)
    
    # Simple pinking filter (b0=0.05, pole=0.95)
    pink_l = np.zeros(n_samples)
    pink_r = np.zeros(n_samples)
    b, a = 0.05, 0.95
    for i in range(1, n_samples):
        pink_l[i] = a * pink_l[i-1] + b * white_l[i]
        pink_r[i] = a * pink_r[i-1] + b * white_r[i]
        
    # 2. Wind gust modulation (0.25 Hz & 0.4 Hz swoops)
    gust = 0.5 + 0.35 * np.sin(2 * np.pi * 0.28 * t + 0.5) + 0.15 * np.sin(2 * np.pi * 0.55 * t)
    gust = np.clip(gust, 0.1, 1.0)
    
    # 3. Resonant whistle of dry desert wind (formant around 420 Hz with subtle pitch wobble)
    whistle_freq = 420.0 + 80.0 * np.sin(2 * np.pi * 0.35 * t)
    whistle_phase = np.cumsum(2 * np.pi * whistle_freq / sample_rate)
    whistle = 0.12 * np.sin(whistle_phase) * gust
    
    # 4. Grit / sand grains hitting armor: occasional tiny high-frequency bandpass clicks
    sand_grit = np.zeros(n_samples)
    click_indices = np.random.choice(n_samples, size=int(duration_sec * 80), replace=False)
    for idx in click_indices:
        span = min(40, n_samples - idx)
        sand_grit[idx:idx+span] += np.random.normal(0, 0.15, span) * np.exp(-np.linspace(0, 5, span))
        
    out_l = (pink_l * gust + whistle + sand_grit * 0.4) * 0.65
    out_r = (pink_r * gust + whistle * 0.9 + sand_grit * 0.35) * 0.65
    
    stereo = np.column_stack([out_l, out_r])
    return stereo

def generate_roman_cornu_horn(duration_sec=5.0, sample_rate=SAMPLE_RATE):
    """
    Tiếng tù và đồng cổ La Mã (Cornu) ngân vang trầm hùng từ xa:
    Tần số cơ bản f0 = 116.5 Hz (Bb2), hòa âm bậc cao lẻ (kèn đồng cổ không van),
    kết hợp suy giảm cao tần theo cự ly và tiếng vang vọng (valley reverb tail).
    """
    n_samples = int(duration_sec * sample_rate)
    t = np.linspace(0, duration_sec, n_samples, endpoint=False)
    
    # Harmonic structure of ancient brass cornu: f0, 2f0, 3f0, 4f0, 5f0, 6f0
    f0 = 116.54 # Bb2
    harmonics = [
        (1.0, 1.0),     # Fundamental
        (2.0, 0.75),    # 2nd harmonic
        (3.0, 0.60),    # 3rd harmonic
        (4.0, 0.45),    # 4th harmonic
        (5.0, 0.30),    # 5th harmonic
        (6.0, 0.18),    # 6th harmonic
        (7.0, 0.10)     # 7th harmonic
    ]
    
    # Lip vibrato & frequency flutter (5.2 Hz)
    vibrato = 1.0 + 0.008 * np.sin(2 * np.pi * 5.2 * t)
    
    horn_raw = np.zeros(n_samples)
    for mult, amp in harmonics:
        horn_raw += amp * np.sin(2 * np.pi * f0 * mult * t * vibrato)
        
    # Amplitude envelope: brass embouchure swell -> steady sustain -> gentle release
    env = np.zeros(n_samples)
    t_attack = 0.45
    t_sustain = 2.4
    t_release = 2.15
    for i, ti in enumerate(t):
        if ti < t_attack:
            env[i] = 0.5 * (1 - np.cos(np.pi * ti / t_attack))
        elif ti < t_attack + t_sustain:
            env[i] = 1.0 - 0.15 * ((ti - t_attack) / t_sustain)
        else:
            rel_t = ti - (t_attack + t_sustain)
            env[i] = 0.85 * np.exp(-rel_t / 0.75)
            
    horn = horn_raw * env * 0.45
    
    # Simulate distant atmospheric lowpass (1200 Hz cutoff)
    # 2-pole IIR filter
    rc = 1.0 / (2 * np.pi * 1100.0)
    dt = 1.0 / sample_rate
    alpha = dt / (rc + dt)
    filtered = np.zeros(n_samples)
    prev = 0.0
    for i in range(n_samples):
        filtered[i] = prev + alpha * (horn[i] - prev)
        prev = filtered[i]
        
    # Echo / Reverb reflection simulation (valley bounce after 0.28s and 0.56s)
    delay_samples_1 = int(0.28 * sample_rate)
    delay_samples_2 = int(0.56 * sample_rate)
    reverb_l = np.copy(filtered)
    reverb_r = np.copy(filtered)
    
    if delay_samples_1 < n_samples:
        reverb_l[delay_samples_1:] += 0.35 * filtered[:-delay_samples_1]
        reverb_r[delay_samples_1:] += 0.42 * filtered[:-delay_samples_1]
    if delay_samples_2 < n_samples:
        reverb_l[delay_samples_2:] += 0.18 * filtered[:-delay_samples_2]
        reverb_r[delay_samples_2:] += 0.15 * filtered[:-delay_samples_2]
        
    stereo = np.column_stack([reverb_l, reverb_r])
    return stereo

def generate_taiko_heartbeat_drum(duration_sec=5.0, sample_rate=SAMPLE_RATE):
    """
    Tiếng trống trận Taiko trầm hùng rung chuyển nền âm thanh (nhịp tim 60 BPM):
    Xung kích sub-bass 50 Hz, pitch-drop đặc trưng màng da trâu, cộng hưởng thùng gỗ sồi.
    """
    n_samples = int(duration_sec * sample_rate)
    t = np.linspace(0, duration_sec, n_samples, endpoint=False)
    stereo = np.zeros((n_samples, 2))
    
    # 60 BPM -> 1 beat per second. In 5 seconds, beats at t = 0.5, 1.7, 2.9, 4.1
    beat_times = [0.5, 1.7, 2.9, 4.1]
    
    for bt in beat_times:
        b_idx = int(bt * sample_rate)
        beat_len = int(0.9 * sample_rate)
        if b_idx + beat_len > n_samples:
            beat_len = n_samples - b_idx
        tb = np.linspace(0, beat_len / sample_rate, beat_len, endpoint=False)
        
        # Pitch drop: 82 Hz down to 48 Hz
        freq = 48.0 + 34.0 * np.exp(-tb / 0.045)
        phase = np.cumsum(2 * np.pi * freq / sample_rate)
        
        # Fundamental punch + harmonic ring (102 Hz, 154 Hz)
        strike = np.sin(phase) + 0.35 * np.sin(phase * 2.1) + 0.15 * np.sin(phase * 3.05)
        
        # Attack and exponential decay envelope
        env = (1.0 - np.exp(-tb / 0.008)) * np.exp(-tb / 0.32)
        drum_hit = strike * env * 0.75
        
        # Left and right with subtle room spread
        stereo[b_idx:b_idx+beat_len, 0] += drum_hit
        stereo[b_idx:b_idx+beat_len, 1] += drum_hit * 0.95
        
    return stereo

def apply_lowpass_sweep(audio_stereo, start_cutoff=12000.0, end_cutoff=380.0, sample_rate=SAMPLE_RATE):
    """
    Thực hiện bộ lọc Lowpass Sweep mượt mà theo thời gian:
    Từ dải âm thanh sáng (12kHz) quét tụt dần xuống âm sắc ấm, trầm mờ (380Hz)
    tạo cảm giác đắm chìm vào dòng ký ức và tập trung cao độ vào sa bàn.
    """
    n_samples = len(audio_stereo)
    out = np.zeros_like(audio_stereo)
    
    # Exponential cutoff sweep
    t = np.linspace(0, 1, n_samples)
    cutoffs = start_cutoff * ((end_cutoff / start_cutoff) ** t)
    
    dt = 1.0 / sample_rate
    for ch in range(audio_stereo.shape[1]):
        prev_y = 0.0
        for i in range(n_samples):
            fc = cutoffs[i]
            rc = 1.0 / (2.0 * math.pi * fc)
            alpha = dt / (rc + dt)
            prev_y = prev_y + alpha * (audio_stereo[i, ch] - prev_y)
            out[i, ch] = prev_y
            
    return out

# ==============================================================================
# BINAURAL SPATIAL PANNING (HASDRUBAL CAVALRY & COMMAND POINTER)
# ==============================================================================

def generate_binaural_cavalry_sweep(duration_sec=10.0, sample_rate=SAMPLE_RATE):
    """
    Lập trình âm thanh vòm Binaural Spatial Panning tiếng vó ngựa Hasdrubal quét sa bàn:
    - Bắt đầu: Mạn trái (sông Aufidus, pan = -1.0)
    - Giữa: Vòng qua sau lưng toàn quân La Mã (pan: -1.0 -> 0.0 -> +0.5)
    - Kết thúc: Mạn phải và nện sập đáy nồi hầm (pan = +1.0 rồi chấn động tâm)
    - Kỹ thuật: ITD (Interaural Time Delay 0.65ms), ILD (Interaural Level Difference),
      Head Shadow Lowpass Filter, và Sub-bass Ground Tremor.
    """
    n_samples = int(duration_sec * sample_rate)
    t = np.linspace(0, duration_sec, n_samples, endpoint=False)
    
    # 1. Procedural Mass Cavalry Gallop rhythm: 4 beats per second per horse
    # Overlay 8 desynchronized horse layers for mass army illusion
    cavalry_raw = np.zeros(n_samples)
    n_horses = 8
    np.random.seed(216)
    
    for h in range(n_horses):
        cadence = 3.6 + 0.15 * np.random.randn()
        jitter = np.random.uniform(0, 1.0 / cadence)
        stride_times = np.arange(jitter, duration_sec, 1.0 / cadence)
        
        for st in stride_times:
            # 3 hooves per gallop stride: lead, rear-left, rear-right
            for sub_beat, sub_dt, sub_gain in [(0, 0.0, 1.0), (1, 0.08, 0.7), (2, 0.15, 0.85)]:
                hit_t = st + sub_dt
                if hit_t >= duration_sec:
                    continue
                hit_idx = int(hit_t * sample_rate)
                h_len = int(0.06 * sample_rate)
                if hit_idx + h_len > n_samples:
                    h_len = n_samples - hit_idx
                th = np.linspace(0, h_len / sample_rate, h_len, endpoint=False)
                
                # Ground thud (65 Hz) + gravel grit (1800 Hz)
                thud = np.sin(2 * np.pi * 65.0 * th) * np.exp(-th / 0.02)
                grit = np.random.normal(0, 0.3, h_len) * np.exp(-th / 0.015)
                shoe_ring = np.sin(2 * np.pi * 2350.0 * th) * np.exp(-th / 0.008) * 0.2
                
                hoof = (thud + grit + shoe_ring) * sub_gain * 0.15
                cavalry_raw[hit_idx:hit_idx+h_len] += hoof
                
    # Normalize cavalry raw
    cavalry_raw = cavalry_raw / (np.max(np.abs(cavalry_raw)) + 1e-6)
    
    # 2. Trajectory Spatial Panning:
    # pan(t): t=0 -> -1.0 (Hard Left)
    #         t=3.0 -> -0.4 (Left-Rear)
    #         t=5.5 -> 0.0 (Direct Rear Center - maximum rumble)
    #         t=7.5 -> +0.7 (Right-Rear)
    #         t=10.0 -> +1.0 (Hard Right)
    pan_trajectory = np.interp(
        t,
        [0.0, 2.5, 5.0, 7.5, 10.0],
        [-1.0, -0.6, 0.0, 0.65, 1.0]
    )
    
    # Distance / intensity envelope: builds up as they hit the Roman rear at t=5.0
    dist_gain = np.interp(
        t,
        [0.0, 2.5, 5.0, 7.5, 10.0],
        [0.65, 0.85, 1.0, 0.9, 0.75]
    )
    
    out_left = np.zeros(n_samples)
    out_right = np.zeros(n_samples)
    
    # Speed of sound & head radius: max ITD = 0.00065s (~29 samples @ 44.1kHz)
    max_delay_samples = int(0.00065 * sample_rate)
    
    for i in range(n_samples):
        p = pan_trajectory[i] # -1.0 (left) to +1.0 (right)
        
        # Interaural Level Difference (ILD) via equal power panning curve
        angle = (p + 1.0) * (np.pi / 4.0) # 0 to pi/2
        gain_l = np.cos(angle) * dist_gain[i]
        gain_r = np.sin(angle) * dist_gain[i]
        
        # Interaural Time Delay (ITD):
        # if p < 0 (left), sound reaches Left ear first, Right ear is delayed by tau
        # if p > 0 (right), sound reaches Right ear first, Left ear is delayed by tau
        tau = int(abs(p) * max_delay_samples)
        
        # Left ear sample
        idx_l = i if p <= 0 else max(0, i - tau)
        # Right ear sample
        idx_r = i if p >= 0 else max(0, i - tau)
        
        out_left[i] = cavalry_raw[idx_l] * gain_l
        out_right[i] = cavalry_raw[idx_r] * gain_r
        
    # 3. Head Shadow Lowpass: when sound is far to one side, filter the contralateral ear
    # Soft single-pole lowpass for high frequency dampening on shadowed ear
    for i in range(1, n_samples):
        p = pan_trajectory[i]
        if p < -0.2: # Sound on left, right ear in shadow
            shadow_factor = abs(p) * 0.45
            out_right[i] = out_right[i-1] * shadow_factor + out_right[i] * (1 - shadow_factor)
        elif p > 0.2: # Sound on right, left ear in shadow
            shadow_factor = abs(p) * 0.45
            out_left[i] = out_left[i-1] * shadow_factor + out_left[i] * (1 - shadow_factor)
            
    # 4. Add Sub-bass Tremor at t=5.0 (Rear Slam Rumble)
    sub_tremor = np.sin(2 * np.pi * 42.0 * t) * np.exp(-((t - 5.0) / 1.8)**2) * 0.45
    out_left += sub_tremor
    out_right += sub_tremor
    
    stereo = np.column_stack([out_left, out_right]) * 0.85
    return stereo

def generate_wooden_pointer_tap(tap_type="double", sample_rate=SAMPLE_RATE):
    """
    Tiếng gõ thước chỉ huy bằng gỗ mun / cờ lệnh sa bàn của Host Seven Nguyen:
    Đanh gọn, giòn giã, có độ cộng hưởng tự nhiên của phòng tác chiến.
    tap_type: 'single', 'double' (tock... tock), 'placement' (thud đặt cờ).
    """
    duration = 1.0 if tap_type != "double" else 1.5
    n_samples = int(duration * sample_rate)
    stereo = np.zeros((n_samples, 2))
    
    tap_times = [0.1] if tap_type == "single" else ([0.1, 0.42] if tap_type == "double" else [0.2])
    
    for tt in tap_times:
        idx = int(tt * sample_rate)
        tap_len = int(0.25 * sample_rate)
        if idx + tap_len > n_samples:
            tap_len = n_samples - idx
        t_tap = np.linspace(0, tap_len / sample_rate, tap_len, endpoint=False)
        
        if tap_type == "placement":
            # Heavier block placement: initial slide + low wood thud
            thud = np.sin(2 * np.pi * 145.0 * t_tap) * np.exp(-t_tap / 0.08)
            contact = np.random.normal(0, 0.4, tap_len) * np.exp(-t_tap / 0.01)
            sound = (thud * 0.7 + contact * 0.4)
        else:
            # Command pointer sharp click: 3200 Hz impact + 240 Hz ebony wood resonance
            click = np.sin(2 * np.pi * 3200.0 * t_tap) * np.exp(-t_tap / 0.003)
            resonance = np.sin(2 * np.pi * 240.0 * t_tap) * np.exp(-t_tap / 0.045)
            harmonic = np.sin(2 * np.pi * 580.0 * t_tap) * np.exp(-t_tap / 0.025) * 0.4
            sound = click * 0.65 + resonance * 0.55 + harmonic
            
        # War room acoustic early reflections (12ms, 28ms)
        del_1 = int(0.012 * sample_rate)
        del_2 = int(0.028 * sample_rate)
        s_left = np.copy(sound)
        s_right = np.copy(sound) * 0.92
        if del_1 < tap_len:
            s_left[del_1:] += 0.25 * sound[:-del_1]
            s_right[del_1:] += 0.18 * sound[:-del_1]
        if del_2 < tap_len:
            s_left[del_2:] += 0.12 * sound[:-del_2]
            s_right[del_2:] += 0.20 * sound[:-del_2]
            
        stereo[idx:idx+tap_len, 0] += s_left * 0.8
        stereo[idx:idx+tap_len, 1] += s_right * 0.8
        
    return stereo

def generate_gold_rings_pour(duration_sec=4.0, sample_rate=SAMPLE_RATE):
    """
    Âm thanh ba thùng nhẫn vàng đổ ào xuống sàn đá cẩm thạch Viện Trưởng lão Carthage:
    Hàng trăm tiếng leng keng kim loại nhỏ gối đầu lên nhau tạo hiệu ứng thác vàng lấp lánh.
    """
    n_samples = int(duration_sec * sample_rate)
    stereo = np.zeros((n_samples, 2))
    
    n_rings = 140
    np.random.seed(42)
    for _ in range(n_rings):
        hit_t = np.random.exponential(0.6) + 0.15
        if hit_t >= duration_sec - 0.2:
            continue
        idx = int(hit_t * sample_rate)
        ring_len = int(0.18 * sample_rate)
        if idx + ring_len > n_samples:
            ring_len = n_samples - idx
        tr = np.linspace(0, ring_len / sample_rate, ring_len, endpoint=False)
        
        # Gold ring chime: 2600 Hz - 5400 Hz metallic ringing
        f_ring = np.random.uniform(2800.0, 5200.0)
        ring_tone = np.sin(2 * np.pi * f_ring * tr) * np.exp(-tr / np.random.uniform(0.02, 0.06))
        
        pan = np.random.uniform(0.2, 0.8)
        stereo[idx:idx+ring_len, 0] += ring_tone * (1 - pan) * 0.12
        stereo[idx:idx+ring_len, 1] += ring_tone * pan * 0.12
        
    return stereo

# ==============================================================================
# MAIN ASSET GENERATION ROUTINE
# ==============================================================================

def build_all_sfx_assets():
    print("\n--- [STEP 1] Generating Procedural Tactical SFX Assets ---")
    
    # 1. Hasdrubal Cavalry Binaural Sweep
    cav_path = os.path.join(SFX_DIR, "binaural_hasdrubal_cavalry_sweep.wav")
    cav_audio = generate_binaural_cavalry_sweep(10.0)
    write_wav(cav_path, cav_audio)
    print(f"[OK] Hasdrubal Binaural Cavalry Sweep generated: {cav_path}")
    
    # 2. Host Wooden Pointer Taps
    tap_single = os.path.join(SFX_DIR, "host_pointer_single_tap.wav")
    write_wav(tap_single, generate_wooden_pointer_tap("single"))
    print(f"[OK] Host Pointer Single Tap generated: {tap_single}")
    
    tap_double = os.path.join(SFX_DIR, "host_pointer_double_tap.wav")
    write_wav(tap_double, generate_wooden_pointer_tap("double"))
    print(f"[OK] Host Pointer Double Tap generated: {tap_double}")
    
    placement_thud = os.path.join(SFX_DIR, "host_wooden_counter_placement.wav")
    write_wav(placement_thud, generate_wooden_pointer_tap("placement"))
    print(f"[OK] Host Wooden Counter Placement generated: {placement_thud}")
    
    # 3. Volturnus Sandstorm continuous
    volturnus_path = os.path.join(SFX_DIR, "volturnus_sandstorm_continuous.wav")
    volturnus_audio = generate_volturnus_wind(12.0)
    write_wav(volturnus_path, volturnus_audio)
    print(f"[OK] Volturnus Sandstorm Continuous generated: {volturnus_path}")
    
    # 4. Roman Cornu Horn distant call
    cornu_path = os.path.join(SFX_DIR, "roman_cornu_distant_echo.wav")
    cornu_audio = generate_roman_cornu_horn(6.0)
    write_wav(cornu_path, cornu_audio)
    print(f"[OK] Roman Cornu Distant Echo generated: {cornu_path}")
    
    # 5. Taiko War Drum Heartbeat
    taiko_path = os.path.join(SFX_DIR, "taiko_heartbeat_war_drum.wav")
    taiko_audio = generate_taiko_heartbeat_drum(8.0)
    write_wav(taiko_path, taiko_audio)
    print(f"[OK] Taiko Heartbeat War Drum generated: {taiko_path}")
    
    # 6. Gold Rings Marble Spill
    rings_path = os.path.join(SFX_DIR, "gold_rings_marble_spill.wav")
    rings_audio = generate_gold_rings_pour(5.0)
    write_wav(rings_path, rings_audio)
    print(f"[OK] Gold Rings Marble Spill generated: {rings_path}")

def build_audio_crossfade_bridge():
    print("\n--- [STEP 2] Designing Audio Cross-Fade Bridge (5.0s: 00:31.0 - 00:36.0) ---")
    
    intro_wav = os.path.join(BASE_DIR, "output", "seven_intro_war_room_ambience.wav")
    if not os.path.exists(intro_wav):
        print(f"[WARN] Intro wav not found at {intro_wav}, creating synthetic intro tail...")
        intro_tail = np.random.normal(0, 0.1, (int(5.0 * SAMPLE_RATE), 2))
    else:
        full_intro, sr = read_wav(intro_wav)
        # Take last 5.0 seconds
        samples_5s = int(5.0 * sr)
        if len(full_intro) >= samples_5s:
            intro_tail = full_intro[-samples_5s:]
        else:
            intro_tail = full_intro
            
    # 1. Apply smooth Lowpass Filter Sweep on intro tail (12kHz down to 380Hz)
    swept_intro = apply_lowpass_sweep(intro_tail, start_cutoff=12000.0, end_cutoff=380.0)
    # Add gentle fadeout on the intro music (equal power fadeout)
    fade_t = np.linspace(0, 1, len(swept_intro))
    music_fade = np.cos(fade_t * np.pi / 2.0)[:, np.newaxis]
    swept_intro = swept_intro * music_fade * 0.45
    
    # 2. Generate Volturnus sandstorm swell (fade in over 5s)
    wind = generate_volturnus_wind(5.0)
    wind_fade = np.sin(fade_t * np.pi / 2.0)[:, np.newaxis]
    wind_swelled = wind * wind_fade * 0.55
    
    # 3. Generate distant Roman Cornu horn (enters at t = 1.0s)
    cornu = generate_roman_cornu_horn(5.0)
    cornu_env = np.clip((fade_t - 0.2) / 0.3, 0.0, 1.0)[:, np.newaxis] * 0.40
    cornu_layered = cornu * cornu_env
    
    # 4. Generate Taiko heartbeat drum (enters at t = 0.5s, 60 BPM)
    taiko = generate_taiko_heartbeat_drum(5.0) * 0.55
    
    # Mix layers together
    bridge_sfx = swept_intro + wind_swelled + cornu_layered + taiko
    
    raw_bridge_wav = os.path.join(BRIDGE_DIR, "cannae_bridge_sfx_5s_raw.wav")
    write_wav(raw_bridge_wav, bridge_sfx)
    
    # Normalize to EBU R128 (-14.0 LUFS, TP -1.0 dBFS)
    master_bridge_wav = os.path.join(BRIDGE_DIR, "cannae_bridge_sfx_5s_master.wav")
    normalize_ebu_r128(raw_bridge_wav, master_bridge_wav, target_lufs=-14.0, target_tp=-1.0)
    print(f"[OK] Master 5-second Audio Cross-Fade Bridge saved: {master_bridge_wav}")

if __name__ == "__main__":
    build_all_sfx_assets()
    build_audio_crossfade_bridge()
