使用Go播放音频:ADSR

原文地址:https://dylanmeeus.github.io/posts/audio-from-scratch-pt10/

到目前为止,我们已将所有内容添加到库中,几乎可以生成小曲调了。目前在开始和停止的时候,还缺失一种听起来更“自然”的声音。

在本文中,我们将为“Attack, Decay, Sustain, Release”实现一种称为“ ADSR”的包络。顺序播放时,音符听起来会更加自然。

要了解为什么需要这样做,请听一下在帧周围没有ADSR包络的情况下生成的声音。

如果你想阅读生成此代码的(不太漂亮的)代码,请查看此github gist

ADSR

上升(Attack),衰减(Decay),保持(Sustain)和释放(Release)包络是一种常见的包络类型。从示意图上可以表示如下(来自维基百科):

维基百科ADSR示意图

当我们将此包络应用于信号时,信号的幅度将根据我们处于ADSR包络的相位而变化。在图像中可以看到,振幅在起Attack骤中上升,在降低一点之前达到峰值。减小后,达到Sustain幅度,它将一直保持直到释放音符为止,Release后Decay直至为零。

对于我们的参数,三个与时间有关:

  • Attack(上升时间)
  • Decay(下降到维持水平的时间)
  • Release(从苏丹到零衰减的时间)

因此,Sustain参数不是指时间,而是指我们将保持的振幅。

将此原理图转换为代码,我们得到:

func ADSR(maxamp, duration, attacktime, decaytime, sus, releasetime, controlrate float64, currentframe int) float64 {
    dur := duration * controlrate
    at := attacktime * controlrate
    dt := decaytime * controlrate
    rt := releasetime * controlrate
    cnt := float64(currentframe)

    amp := 0.0
    if cnt < dur {
        if cnt <= at {
            // attack
            amp = cnt * (maxamp / at)
        } else if cnt <= (at + dt) {
            // decay
            amp = ((sus-maxamp)/dt)*(cnt-at) + maxamp
        } else if cnt <= dur-rt {
            // sustain
            amp = sus
        } else if cnt > (dur - rt) {
            // release
            amp = -(sus/rt)*(cnt-(dur-rt)) + sus
        }
    }

    return amp
}

在原理图中找不到的此功能中的一个参数是控制速率。控制速率将用于将持续时间(以秒为单位)转换为帧数。控制率可以只是采样率,但不一定是这种情况。一种这样的用例是子音频调制,其中调制振荡器在20Hz以下运行。你可以 在这篇文章中进一步了解。

应用

要将ADSR包络应用于信号(例如,使用我们创建的振荡器生成的信号),我们必须遍历每一帧,将当前帧传递给ADSR函数,并使用结果修改帧的幅度。例如,这是GoAudio中包含的完整示例程序 。

package main

import (
    "flag"
    "fmt"

    synth "github.com/DylanMeeus/GoAudio/synthesizer"
    "github.com/DylanMeeus/GoAudio/wave"
)

func main() {
    flag.Parse()
    osc, err := synth.NewOscillator(44100, synth.SINE)
    if err != nil {
        panic(err)
    }

    sr := 44100
    duration := sr * 10

    frames := []wave.Frame{}
    var adsrtime int
    for i := 0; i < duration; i++ {
        value := synth.ADSR(1, 10, 1, 1, 0.7, 5, float64(sr), adsrtime)
        adsrtime++
        frames = append(frames, wave.Frame(value*osc.Tick(440)))
    }

    wfmt := wave.NewWaveFmt(1, 1, sr, 16, nil)
    wave.WriteFrames(frames, wfmt, "output.wav")
    fmt.Println("done writing to output.wav")
}

在此示例中,请注意,我们的控制率与采样率相同,并且adrtime与我们处理的帧一起增加。(因此,我们可以将i迭代变量传递给函数,但我认为将其展开更清晰)。

推荐阅读更多精彩内容