原文地址:https://dylanmeeus.github.io/posts/audio-from-scratch-pt10/
到目前为止,我们已将所有内容添加到库中,几乎可以生成小曲调了。目前在开始和停止的时候,还缺失一种听起来更“自然”的声音。
在本文中,我们将为“Attack, Decay, Sustain, Release”实现一种称为“ ADSR”的包络。顺序播放时,音符听起来会更加自然。
要了解为什么需要这样做,请听一下在帧周围没有ADSR包络的情况下生成的声音。
如果你想阅读生成此代码的(不太漂亮的)代码,请查看此github gist。
ADSR
上升(Attack),衰减(Decay),保持(Sustain)和释放(Release)包络是一种常见的包络类型。从示意图上可以表示如下(来自维基百科):
当我们将此包络应用于信号时,信号的幅度将根据我们处于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
迭代变量传递给函数,但我认为将其展开更清晰)。