· 5 分钟阅读 · 1989 字

接近速度相同,音高却不同:多普勒不能只保留相对速度

声源在监听者左边,两者都向右移动,速度分别为 170 和 136 米/秒;另一组让声源静止、监听者以 34 米/秒向左移动。两组的接近速度都是 34 米/秒,按声速 340 米/秒计算,接收音高倍率却分别是 1.2 和 1.1。若音频接口只收到两者的相对速度,这两种输入已经无法区分。

声源前进使前方波前压缩、后方波前拉开的版画概念图,非实测图

本篇的独立 C# 探针保留声源速度和监听者速度,在同一静止介质坐标系中计算倍率。固定反例使用较大但仍低于声速的速度,方便暴露差异;它没有播放音频,也没有接入具体引擎。

两个速度改变的是不同环节

先看共线匀速情况。声源初始位于 0,监听者位于距离 d 的右侧,向右为正。记声源速度为 vs、监听者速度为 vl,发出某个波前的时刻为 t,波前到达监听者的时刻为 T。波前离开发声位置后,以声速 c 在介质中传播,因此有:

c × (T - t) = d + vl × T - vs × t
T(t) = [d + (c - vs) × t] / (c - vl)
ΔT / Δt = (c - vs) / (c - vl)
接收频率 / 发出频率 = Δt / ΔT = (c - vl) / (c - vs)

声源向前移动,下一次发声位置离前一个波前更近,改变波前间距,进入分母。监听者迎着波前移动,提高单位时间遇到波前的数量,进入分子。两项不能先相减再随意塞进某个分母。

开头两组输入分别得到 204/170 和 374/340。相对速度模型即使恰好拟合第二组,也不能同时保留第一组结果。这与碰撞反弹中切到物体相对坐标系的做法不同:声速相对于介质定义,改变运动参照时还必须处理介质速度。

相同接近速度的两组固定结果,以及发射时刻到接收时刻的波前方程

对任意当前连线,定义单位方向 n 从声源指向监听者,将两个速度分别点乘 n。声源沿 n 运动会升高音高,监听者沿 n 运动则降低音高。若两者以相同速度沿连线移动,分子与分母相同,倍率回到 1;单纯使用速度长度则会失去迎面与远离的符号。

这里的方向取自同一采样时刻的位置,属于瞬时连线近似。严格的传播模型需要连接发射位置和接收位置,并使用相应时刻的速度;本实现没有求解延迟位置。横向运动在当前采样点的径向分量为零,函数返回 1,只证明投影合同成立,不能推断整个掠过过程没有音高变化。

在音频边界保留介质坐标,而不是过早压成一个标量

源码为 .tmp/daily-labs/2026-11-05-2000/Doppler.cs,调用链是 Program → Doppler.Ratio → V2.Length / Dot。以下是完整计算组件:

public readonly record struct V2(double X, double Y)
{
    public static V2 operator -(V2 a, V2 b) => new(a.X - b.X, a.Y - b.Y);
    public double Dot(V2 b) => X * b.X + Y * b.Y;
    public double Length()
    {
        double m = Math.Max(Math.Abs(X), Math.Abs(Y));
        return m == 0 ? 0 : m * Math.Sqrt((X / m) * (X / m) + (Y / m) * (Y / m));
    }
    public bool Finite => double.IsFinite(X) && double.IsFinite(Y);
}
public static class Doppler
{
    // 坐标为米,速度为相对静止介质的米每秒。
    public static double Ratio(V2 source, V2 listener, V2 sourceVelocity,
        V2 listenerVelocity, double soundSpeed)
    {
        if (!source.Finite || !listener.Finite || !sourceVelocity.Finite ||
            !listenerVelocity.Finite || !double.IsFinite(soundSpeed) || soundSpeed <= 0)
            throw new ArgumentException("non-finite input or invalid sound speed");
        V2 delta = listener - source;
        double distance = delta.Length();
        if (!double.IsFinite(distance) || distance < 1e-6)
            throw new ArgumentException("undefined source-listener direction");
        if (sourceVelocity.Length() >= soundSpeed || listenerVelocity.Length() >= soundSpeed)
            throw new ArgumentException("subsonic model required");
        V2 n = new(delta.X / distance, delta.Y / distance);
        // 先除声速,避免 c 与径向速度相减产生不必要的大数运算。
        double s = new V2(sourceVelocity.X / soundSpeed, sourceVelocity.Y / soundSpeed).Dot(n);
        double l = new V2(listenerVelocity.X / soundSpeed, listenerVelocity.Y / soundSpeed).Dot(n);
        double numerator = 1 - l, denominator = 1 - s;
        if (numerator <= 1e-6 || denominator <= 1e-6)
            throw new ArgumentException("near-sonic radial velocity");
        return numerator / denominator;
    }
}

位置单位为米,速度单位为米每秒。声速 340 是探针的固定参数,不是对所有环境的测量结论。引擎若使用厘米坐标,应统一换算位置、速度和声速;只换位置不会自动纠正从坐标差分得到的速度。

Length 先缩放再平方,避免有限大分量在求模过程中提前溢出。位置相减仍可能溢出,因此距离也检查有限性。距离小于一微米时方向未定义,接口显式拒绝;同样拒绝超声速输入,以及径向项离奇点过近的情况。1e-6 的距离阈值带米制单位,分子、分母阈值则是无量纲保护,二者不能混成一个“通用 epsilon”。

整段速度低于声速是当前模型的合同,不能只检查径向投影。一个横向超声速对象即使投影为零,也不在此函数的接受范围。抛错后,播放层可以沿用最后有效倍率或暂停多普勒调制,但应保留错误计数;把非法输入直接钳成一个漂亮倍率,会掩盖传送、时间差为零或尺度错误。

用两次波前的相遇时刻检查倍率

Program.cs 的 Check、Near 与 Reject 验证静止、迎面、远离、共同移动、横向投影、旋转平移一致性及非法输入。拒绝样本包括重合位置、无效声速、非有限速度、超声速、近奇点和坐标相减溢出。

另外,固定种子 7110520 生成 10,000 组共线速度,vs、vl 分别均匀取自 [-272,272) 米/秒。初始距离为 100 米,两次波前在 0 和 0.001 秒发出;程序分别求出相遇时刻,用发射间隔除以接收间隔,与计算组件比较。这检查的是共线、匀速、尚未越过监听者的传播关系,未把随机测试扩大成三维声场验收。

2026-11-05 在 .NET 9 Release 下执行:

cd "$WORK_DIR"
dotnet build .tmp/daily-labs/2026-11-05-2000/DopplerLab.csproj -c Release
dotnet run --project .tmp/daily-labs/2026-11-05-2000/DopplerLab.csproj -c Release --no-build
build: exit=0, warnings=0, errors=0
run: exit=0, passed=20, failed=0, skipped=0
random_pairs=10000
source=170, listener=136: ratio=1.2
source=0, listener=-34: ratio=1.1
source=34, listener=34: ratio=1
transverse_source=34: instantaneous_ratio=1
max_wavefront_error=1.6333601138285303E-12
elapsed_ms=18.4594

最大误差为倍率绝对差,主要检验两条计算路径的数值一致性;耗时仅描述这次正确性探针,不代表音频回调预算或播放延迟。边界断言没有被随机样本替代,后者不会稳定命中位置重合或恰好达到声速的输入。

游戏接入时,空间系统应交付同一快照下的位置、速度及其介质参照,音频空间化层计算原始倍率,播放层再应用艺术性的倍率上限和平滑。若引擎已经启用内置多普勒,就应由它承担这一步,额外乘一次会重复调制。浮动原点或传送也不能直接混入位移差分,否则空间重定位会被解释为高速运动。

只需要当前连线近似的游戏,可以把传播延迟留在模型之外;若高速近距离掠过的方向变化已经影响需求,就需要发射历史和延迟位置求解,平滑倍率不能补回这段几何信息。无论最终选择哪种听感,接口都应先保留两个介质速度。只剩一个接近速度之后,开头那两种本应不同的结果已经无从恢复。