Axıs
技能目录

参考 05:实现模式库(可直接抄的代码与参数)

参考 05:实现模式库(可直接抄的代码与参数)

面向音频程序与 TA。所有片段为示意实现,重点在模式与参数量级,接入前按项目引擎适配。 参数量级是经验值,可直接作为起点再调。


1. 变体调度:避免"机关枪效应"

// 轮转 + 禁止连续重复 + 可选加权
struct VariantSet {
    TArray<int32> Weights;      // 每个变体的权重(稀有变体权重低)
    int32 Last = -1;
    uint32 Seed = 0x12345678;

    int32 Pick() {
        if (Weights.Num() == 1) return 0;
        int32 Total = 0;
        for (int32 i = 0; i < Weights.Num(); ++i) Total += (i == Last) ? Weights[i] / 4 : Weights[i];
        int32 R = Rand(Seed) % Total;
        for (int32 i = 0; i < Weights.Num(); ++i) {
            int32 W = (i == Last) ? Weights[i] / 4 : Weights[i];
            if (R < W) { Last = i; return i; }
            R -= W;
        }
        return 0;
    }
};

要点:纯随机在 3–4 个变体时仍会出现"连播同一个"(概率 1/N 不低)。降低上次播放项的权重比纯随机更自然。

随机化参数(每次播放)

参数 范围 说明
音量 ±1.5 dB(±12%) 超过 ±3dB 会听出"这次特别响",破坏一致性
音高 ±50–150 cents 换算:ratio = pow(2, cents / 1200);脚步 50–80,武器 100–150
起始偏移 0–40 ms 连发时错开,避免完全同相叠加
低通截止 ±10% 微妙的自然感
立体声位置 ±5° 仅 3D 音,微小偏移更自然

音高变化会同时改变时长。若必须与动画严格同步(脚步),把音高范围压到 ±30 cents 以内。


2. Voice Manager:优先级 + 距离 + 抢占

struct Voice { float Priority; float Distance; bool bPlaying; float Gain; };

Voice* RequestVoice(float BasePriority, float Distance) {
    // 距离衰减后的有效优先级
    const float Effective = BasePriority / (1.f + Distance * 0.1f);

    for (auto& V : Voices) if (!V.bPlaying) { V.Priority = Effective; V.bPlaying = true; return &V; }

    Voice* Weakest = nullptr;
    for (auto& V : Voices)
        if (!Weakest || V.Priority < Weakest->Priority) Weakest = &V;

    if (Effective > Weakest->Priority) {
        Weakest->StartFadeOut(0.02f);   // 20ms 淡出,避免抢占时爆音(关键)
        return Weakest;
    }
    return nullptr;                      // 放弃本次播放
}

三个易错点:

  1. 抢占必须淡出,直接停会 click
  2. 优先级要按距离衰减,否则远处的低优先级音会挤掉近处的重要音
  3. 距离超过 Max Distance 直接不分配,而不是分配了再静音(省 CPU)

优先级基准表

类别 Base Priority Voice Limit
VO 语音 100 4
UI 90 4
玩家武器 85 16
敌人威胁音 80 12
载具 75 4
角色动作 70 12
冲击 / 碰撞 65 12
环境细节 30 8
装饰音 20 6

3. 距离衰减与遮挡(不依赖中间件时)

struct AttenuationResult { float Gain; float LpfHz; float ReverbSend; };

AttenuationResult EvalAttenuation(float Dist, float MaxDist, bool bOccluded) {
    AttenuationResult R;
    const float T = FMath::Clamp(Dist / MaxDist, 0.f, 1.f);

    // 音量:接近对数,避免线性导致的"远处仍很响"
    R.Gain = (T >= 1.f) ? 0.f : FMath::Pow(1.f - T, 1.8f);

    // 空气吸收:远处高频损失
    R.LpfHz = FMath::Lerp(12000.f, 800.f, FMath::Pow(T, 0.6f));

    // 混响占比随距离上升
    R.ReverbSend = FMath::Lerp(0.15f, 1.0f, T);

    if (bOccluded) {                    // 完全隔断
        R.Gain *= 0.12f;                // -18 dB
        R.LpfHz = FMath::Min(R.LpfHz, 450.f);
    }
    return R;
}

遮挡检测(每帧对每个声源全做 raycast 太贵):

  • 只对重要声源(敌人、载具)做;装饰音不做
  • 频率 8–12 Hz,且用插值平滑(RTPC 平滑 200ms),否则滤波跳变会"咔"
  • 缓存命中结果,声源与听者都没大位移时复用

4. 基础 DSP 片段

// 一阶低通(每声道一个实例,状态需保留)
struct OnePoleLP {
    float Z = 0.f;
    float Process(float X, float CutoffHz, float SampleRate) {
        const float A = FMath::Exp(-2.f * PI * CutoffHz / SampleRate);
        Z = X * (1.f - A) + Z * A;
        return Z;
    }
};

// 一阶高通(去掉无用低频,几乎所有音效都该挂)
struct OnePoleHP {
    float Z = 0.f, PrevX = 0.f;
    float Process(float X, float CutoffHz, float SampleRate) {
        const float A = FMath::Exp(-2.f * PI * CutoffHz / SampleRate);
        Z = A * (Z + X - PrevX); PrevX = X;
        return Z;
    }
};

// 平滑增益(用于 ducking / RTPC 插值,避免阶变爆音)
struct SmoothGain {
    float Current = 1.f;
    void SetTarget(float Target, float TimeSec, float SampleRate) {
        Coef = FMath::Exp(-1.f / FMath::Max(TimeSec * SampleRate, 1.f));
        Goal = Target;
    }
    float Tick() { Current = Goal + (Current - Goal) * Coef; return Current; }
private:
    float Coef = 0.f, Goal = 1.f;
};

// ADSR(线性段足够游戏用,指数段更自然但更贵)
struct ADSR {
    float A = 0.005f, D = 0.08f, S = 0.6f, R = 0.25f;
    float T = 0.f, ReleaseFrom = 0.f, ReleaseT = 0.f; bool bReleased = false;
    float Tick(float Dt) {
        if (!bReleased) {
            T += Dt;
            if (T < A) return T / A;
            if (T < A + D) return 1.f - (T - A) / D * (1.f - S);
            return S;
        }
        ReleaseT += Dt;
        return FMath::Max(0.f, ReleaseFrom * (1.f - ReleaseT / R));
    }
    void Release(float Cur) { bReleased = true; ReleaseFrom = Cur; }
};

防 denormal(重要):滤波状态长期接近 0 时会进入非正规浮点数,CPU 暴跌。 在每帧末尾加 Z += 1e-20f; Z -= 1e-20f; 或启用平台的 FTZ/DAZ 标志。


5. 程序化音效示例

// 激光/能量:扫频 + 快速衰减(参数化,可复用为不同武器)
void SynthLaser(float* Out, int N, float SR, float F0, float F1, float DurSec) {
    float Phase = 0.f;
    for (int i = 0; i < N; ++i) {
        const float T = (float)i / N;
        const float F = FMath::Lerp(F0, F1, T);           // 音高扫描
        Phase += F / SR; if (Phase >= 1.f) Phase -= 1.f;
        float Env = 1.f - T; Env *= Env;                   // 指数感衰减
        Out[i] = (2.f * Phase - 1.f) * Env * 0.5f;         // 锯齿
    }
}

// 爆炸:低通噪声 + 次低频轰鸣(经典两层)
void SynthExplosion(float* Out, int N, float SR) {
    OnePoleLP Lp; float Rumble = 0.f;
    for (int i = 0; i < N; ++i) {
        const float T = (float)i / N;
        const float Noise = FMath::FRand() * 2.f - 1.f;
        const float Cut = FMath::Lerp(4000.f, 300.f, T);    // 截止随时间下降
        const float Env = FMath::Exp(-T * 5.f);
        Rumble = FMath::Sin(2.f * PI * 38.f * T) * (1.f - T);
        Out[i] = (Lp.Process(Noise, Cut, SR) * 0.7f + Rumble * 0.3f) * Env;
    }
}

纯程序化爆炸通常"不够狠"。实战做法:程序化只负责 Sub 与尾部的可变部分, 主体仍用实录采样 —— 混合方案既可控又有冲击力。


6. 伪程序化:多段 loop 交叉淡化(引擎/风/水)

// 5 段 RPM loop,等功率交叉淡化避免中间"变轻"
float CrossfadeGain(float X) { return FMath::Sin(0.5f * PI * X); }  // 等功率

void UpdateEngine(float Rpm01) {
    const float Pos = Rpm01 * (NumLayers - 1);
    const int   Lo  = FMath::FloorToInt(Pos);
    const int   Hi  = FMath::Min(Lo + 1, NumLayers - 1);
    const float Frac = Pos - Lo;

    SetLayerGain(Lo, CrossfadeGain(1.f - Frac));
    SetLayerGain(Hi, CrossfadeGain(Frac));

    // 播放速率微调跟随段内 RPM(±30% 内不失真)
    const float Rate = 1.f + (Frac - 0.5f) * 0.6f;
    SetLayerRate(Lo, Rate); SetLayerRate(Hi, Rate);
}

关键:交叉淡化用 等功率(sin/cos),线性交叉淡化在中点会有 -3dB 的凹陷,听感上"中间掉音量"。


7. 动态音乐:节拍对齐切换

// 只能在小节/拍边界切换,否则"卡不准"
void RequestMusicTransition(EMusicState NewState) {
    PendingState = NewState;                    // 排队,不立即切
    bPending = true;
}

// 由中间件音乐回调触发(Wwise: Music Sync Callback / FMOD: Beat Callback)
void OnMusicBeat(int Bar, int Beat) {
    if (!bPending) return;
    if (Beat == 1) {                            // 只在小节头切换
        ApplyMusicState(PendingState);
        bPending = false;
    }
}

要点:

  • 所有段落 同 BPM、同调性,否则切不动
  • 若需要紧急切换(死亡、Boss 登场),用 Stinger 盖过过渡,而不是硬切
  • 音乐回调还能反向驱动玩法(节奏游戏、UI 脉动)

8. 引擎接入片段

Wwise(Unreal C++)

#include "AkAudioDevice.h"

// 播放(带位置)
FAkAudioDevice::Get()->PostEvent(TEXT("Play_Weapon_Rifle_Fire"), Actor);

// 设置 RTPC
FAkAudioDevice::Get()->SetRTPCValue(nullptr, TEXT("RTPC_Vehicle_Speed"), SpeedKmh, 0, nullptr);

// 设置 Switch
FAkAudioDevice::Get()->SetSwitch(TEXT("Switch_Surface"), TEXT("Dirt"), Actor);

// 设置 State
FAkAudioDevice::Get()->SetState(TEXT("State_Combat"), TEXT("InCombat"));

// Bank 加载/卸载(必须成对)
FAkAudioDevice::Get()->LoadBank(TEXT("Bank_Level1"));
FAkAudioDevice::Get()->UnloadBank(TEXT("Bank_Level1"));

FMOD(Unity C#)

// 一次性事件
FMODUnity.RuntimeManager.PlayOneShot("event:/SFX/Weapon/Rifle_Fire", transform.position);

// 带参数的持续音(如引擎)
var inst = FMODUnity.RuntimeManager.CreateInstance("event:/Vehicles/Engine");
inst.set3DAttributes(FMODUnity.RuntimeUtils.To3DAttributes(transform));
inst.setParameterByName("RPM", rpm01);
inst.start();
inst.release();          // 注意:start 后 release 表示"播完自动释放"

// 全局参数
FMODUnity.RuntimeManager.StudioSystem.setParameterByName("Combat_Intensity", v);

Unity 自定义 DSP(OnAudioFilterRead)

void OnAudioFilterRead(float[] data, int channels) {
    for (int i = 0; i < data.Length; i += channels) {
        for (int c = 0; c < channels; c++) {
            float x = data[i + c];
            z = x * (1 - a) + z * a;      // 一阶低通,a 由 cutoff 计算
            data[i + c] = z * gain;
        }
    }
}

该回调在音频线程,禁止在其中分配内存、加锁、访问 Unity 主线程对象。

Unreal MetaSounds(蓝图侧)

MetaSound 内暴露参数接口 → 蓝图调用 SetFloatParameter("Speed", v)
                        → 播放用 Audio Component + Play/Stop

分工建议:MetaSounds 负责程序化音源,中间件负责混音/音乐/3D,两者不要重叠做同一件事。


9. 通用实现检查表

  • 循环音有明确的 Stop 路径(含物体销毁、场景切换、暂停)
  • 每个 Post Event 有节流或状态判断
  • 抢占声部有 20ms 淡出
  • 所有音效挂了 HPF(40–80Hz)
  • 3D 音设置了 Max Distance
  • RTPC 有插值平滑(禁止直接跳变)
  • 滤波器有 denormal 防护
  • 音频线程回调中无内存分配、无锁、无主线程 API
  • 参数变化有上下限 clamp(速度、强度、距离)
  • 采样率无关:所有时间常数用秒换算,不写死采样点数