帮助计算指数 ADSR 包络方程

信息处理 声音的 软件实现
2021-12-26 06:41:52

通过应用程序代码,我实现了一个线性 ADSR 包络,用于整形振荡器输出的幅度。可以在包络上设置起音、衰减和释放持续时间以及延音水平的参数,一切都按预期工作。

但是,我想将包络的斜坡形状调整为类似于大多数合成器用于更自然响应的东西:起音的反指数和衰减和释放的指数。我无法让我的公式正确计算这些类型的斜坡形状的包络输出值。为了计算线性斜坡,我使用两点形式,插入开始/结束x/y从攻击/衰减/维持/释放输入参数值派生的值。我似乎无法使用相同的开始/结束计算出指数(标准和逆)斜坡的正确公式x/y点值。

我保存了一个 Desmos 图形计算器会话,该会话演示了我上面描述的线性斜坡的方法。

如果有人能帮助我指出正确的方向,将不胜感激。

4个回答

我认为让你感到困惑的是指数递减(ex) 永远不会达到 0,因此具有真正指数段的 ADSR 生成器会卡住;因为它永远不会达到目标值。例如,如果生成器处于攻击阶段的高度(例如y=1) 并且必须降落以维持在y=0.5,它不能用真正的指数去那里,因为真正的指数不会衰减到 0.5,它只会渐近到 0.5!

如果您查看模拟包络发生器(例如每个人似乎都使用的基于 7555 的电路),您可以看到在启动阶段,当电容器充电时,它“瞄准”高于用于指示结束的阈值的攻击阶段。在由 +15V 供电的基于 (7)555 的电路上,在攻击阶段,电容器以 +15V 的步长充电,但当达到 +10V 的阈值时,攻击阶段结束。这是一个设计选择,尽管2/3 是许多经典包络发生器中的“神奇数字”,这可能是音乐家所熟悉的。

电容器充电过程中由不同的“瞄准比”导致的一些 ADSR 形状

因此,您可能想要处理的函数不是指数函数,而是它的移位/截断/缩放版本,并且您必须做出一些选择来决定您希望它们如何“压扁”。

无论如何,我很好奇您为什么要尝试获得这样的公式-也许是因为您用于合成的工具的限制;但是,如果您尝试使用通用编程语言(C、java、python)来实现那些,并为每个信封样本运行一些代码,以及“状态”的概念,请继续阅读......因为它总是更容易表示“这样的段将从它刚刚达到的任何值变为 0”。

我对实施信封的两条建议。

第一个不是尝试缩放所有斜率/增量,以使包络准确地达到起始值和结束值。例如,您想要一个在 2 秒内从 0.8 变为 0.2 的信封,因此您可能会想计算 -0.3 / 秒的增量。不要那样做。相反,将其分解为两个步骤:在 2 秒内获得从 0 到 1.0 的斜坡;然后应用将 0 映射到 0.8 和 1.0 到 0.2 的线性变换。以这种方式工作有两个优点 - 第一个是它简化了您相对于包络时间到从 0 到 1 的斜坡的任何计算;第二个是,如果您在中途更改包络参数(增量和开始/结束时间),一切都会保持良好状态。如果您正在制作合成器,那就太好了,因为人们会要求将包络时间参数作为调制目标。

第二种是使用带有信封形状的预先计算的查找表。它在计算上更轻,它消除了许多脏细节(例如,您不必为指数不准确地达到 0 而烦恼 - 随心所欲地截断它并重新调整它,使其映射到 [0, 1]),并且很容易为每个阶段提供更改信封形状的选项。

这是我描述的方法的伪代码。

render:
  counter += increment[stage]
  if counter > 1.0:
    stage = stage + 1
    start_value = value
    counter = 0
  position = interpolated_lookup(envelope_shape[stage], counter)
  value = start_value + (target_level[stage] - start_value) * position

trigger(state):
  if state = ON:
    stage = ATTACK
    value = 0  # for mono-style envelopes that are reset to 0 on new notes
    counter = 0
  else:
    counter = 0
    stage = RELEASE

initialization:
  target_level[ATTACK] = 1.0
  target_level[RELEASE] = 0.0
  target_level[END_OF_RELEASE] = 0.0
  increment[SUSTAIN] = 0.0
  increment[END_OF_RELEASE] = 0.0

configuration:
  increment[ATTACK] = ...
  increment[DECAY] = ...
  target_level[DECAY] = target_level[SUSTAIN] = ...
  increment[RELEASE] = ...
  envelope_shape[ATTACK] = lookup_table_exponential
  envelope_shape[DECAY] = lookup_table_exponential
  envelope_shape[RELEASE] = lookup_table_exponential

这是一个很老的问题,但我只想强调一下 pichenettes 的答案中的一点:

例如,您想要一个在 2 秒内从 0.8 到 0.2 的信封 [...] 将其分解为两个步骤:得到一个在 2 秒内从 0 到 1.0 的斜坡;然后应用将 0 映射到 0.8 和 1.0 到 0.2 的线性变换。

这个过程有时被称为“缓动”,看起来像

g(x,l,u)=f(xlul)(ul)+l

在哪里lu是下限和上限(可能的值是0,1, 和维持水平) 和f(x)是这样的xn. 请注意,您不需要在攻击阶段使用它,因为它已经从01.

是原始的 Desmos 会话,已更新为使用此方法。我在这里使用了一个立方体,但你*可以使用任何你喜欢的形状,只要f(x)产生从零到一的输出给定从零到一的输入。

*我猜OP可能早已不复存在,但也许这对其他人有帮助。

关于 pichenettes 的评论,“在攻击阶段,电容器以 +15V 的步长充电,但在达到 +10V 的阈值时,攻击阶段结束。这是一个设计选择,虽然 2/3 是“魔法number”在许多经典的包络发生器中发现,这可能是音乐家所熟悉的。”:

任何针对 10v 目标的 15v 渐近线的包络实际上都会产生线性攻击。只是 15v 是容易获得的最高渐近线,并且足够接近线性。也就是说,它没有什么“魔法”——他们只是尽可能地追求线性。

我不知道有多少经典合成器使用 15v——我怀疑通常有一两个二极管压降。我的旧 Aries 模块使用 13v 作为 10v 信封,而我只是查找了一个 Curtis ADSR 芯片,它使用 6.5v 作为 5v 信封。

此代码应生成与 pichennette 相似的图:

def ASD_envelope( nSamps, tAttack, tRelease, susPlateau, kA, kS, kD ):
    # number of samples for each stage
    sA = int( nSamps * tAttack )
    sD = int( nSamps * (1.-tRelease) )
    sS = nSamps - sA - sD

    # 0 to 1 over N samples, weighted with w
    def weighted_exp( N, w ):
        t = np.linspace( 0, 1, N )
        E = np.exp( w * t ) - 1
        E /= max(E)
        return E

    A = weighted_exp( sA, kA )
    S = weighted_exp( sS, kS )
    D = weighted_exp( sD, kD )

    A = A[::-1]
    A = 1.-A

    S = S[::-1]
    S *= 1-susPlateau
    S += susPlateau

    D = D[::-1]
    D *= susPlateau

    env = np.concatenate( [A,S,D] )

    # plot
    tEnv = np.linspace( 0, nSamps, len(env) )
    plt.plot( tEnv, env )
    plt.savefig( "OUT/EnvASD.png" )
    plt.close()

    return env

我很感激任何改进,可能是一个好主意的一件事是允许最后三个参数(确定三个阶段中每个阶段的陡度)在 0 和 1 之间变化,其中 0.5 是一条直线。但我不知道该怎么做。

此外,我还没有彻底测试所有用例,例如,如果一个阶段的长度为零。