插值频率

Interpolating frequencies

这与其说是编程问题,不如说是数学问题,但现在我已经在两个不同的编程项目中遇到过两次。

我想创建一个函数,w_sweep(t, f0, t0, f1, t1),它在时间 t 采样一个正弦波,该正弦波扫过从 f0t == t0f1t == t1。 (您可以假设频率以赫兹为单位,时间以秒为单位。我不在乎 t 超出范围 t0..t1 时会发生什么。)

我首先编写了一个使用恒定频率的更简单的函数:

float w_steady(float t, float freq) {
  return std::sin(2.0f * PI * freq * t);
}

直觉上,w_sweep 似乎只涉及频率的线性插值。

// INCORRECT SOLUTION
float w_sweep(float t, float f0, float t0, float f1, float t1) {
  const float freq = std::lerp(f0, f1, (t - t0)/(t1 - t0));
  return std::sin(2.0f * PI * freq * t);
}

乍一看,结果似乎是正确的:频率确实是f0t0附近,然后开始向f1扫去。但是当你达到 t1 时,实际波形将超过 f1 相当多。

我认为发生的事情是,我们实际上不是用 t 线性扫描频率,而是在 t*t,因为 t 是参数中的一个因素和 freq 中的一个因素。另一种思考方式是每个样本都来自不同的波函数。它们都是不同恒定频率的正弦波。但它们并不真正相关(除了在时间 0 时,它们都将给出相同的值)。

如果我想创建带有循环的连续样本,我可以通过沿符号波累积位置来解决问题。

float omega = 0;
for (float t = t0; t <= t1; t += dt) {
  samples.push_back(std::sin(omega));
  const float freq = std::lerp(f0, f1, (t - t0)/(t1 - t0));
  omega += 2.0f * PI * freq * dt;
}

可行,但我更喜欢 w_sweep 可以在任意时间对其进行采样。

相反的,周期的变化是明智的。对于频率 f0,周期 T0 = 1/f0。对于频率 f1,你有一个周期 T1=​​1/f1.

现在,当您评估 w_sweep(t) 时,您没有使用一个句点 T0,然后是一个句点 lerp(T0,T1,...) 等等。相反,您确定一个周期 T = 1.0/std::lerp(f0, f1, (t - t0)/(t1 - t0)),然后计算 sin(2*pi*t/T),这当然是 sin(2*pi*frac(t/T)).

我想你想要的(不完全确定)是f=1/lerp(1/f0, 1/f1, (t - t0)/(t1 - t0))

你得从相位的角度考虑。对于恒定频率,phase 只是 timefrequency 的乘积。但总的来说,它是 频率函数 时间 上的积分。

展示其工作原理的最简单方法是使用图表,但也可以使用形式数学。不幸的是,SO markdown 既不支持图形也不支持乳胶降价。但如果通过,结果是:

float w_sweep(float t, float f0, float t0, float f1, float t1) {
  float const freq = std::lerp(f0, f1, (t - t0)/(t1 - t0));
  return std::sin(PI * (f0 + freq) * (t-t0));
}

图形化的方法可以描述如下: 相位是时间频率函数下的区域,自 t0 以来被覆盖。对于我们的示例,最好将其拆分为底部 f0*(t-t0) 的矩形区域和顶部 0.5*(freq-f0)*(t-t0) 的三角形区域。将总和乘以 2π 得到 PI * (f0 + freq) * (t-t0).