翻译自:https://dylanmeeus.github.io/posts/audio-from-scratch-pt1/
在这篇文章中,我们将使用Go从头开始以二进制格式创建声音。这篇文章的最终结果是播放一定频率、采样率和持续时间的声音。我们还会应用指数衰减,这样声音就会逐渐变小。
在最简单的形式中,声音对计算机可以被认为是一种简单的数字编码波。在声音到达你的耳朵之前,它会经过一个数字到模拟转换器,基本上就是把数字信号转换成你的耳机/扬声器的电流。
第一步,我们试着用go创建一个正弦波。我们可以用math.sin (x)来生成它并将x作为弧度传递。我们必须在一定范围内迭代才能得到正弦波。为了保持在音频节目领域,“点”的数量,我们将绘制到正弦波是我们的样本。(如果你想跳过,这篇文章的所有代码在github上:https://github.com/DylanMeeus/MediumCode/blob/master/Audio)
const nsamps = 50 // samples to generate
func generate()
{
tau = math.Pi * 2
var angle float64 = tau / nsamps
for i := 0; i < nsamps; i++ {
samp = math.Sin(angle * float64(i))
fmt.Printf("%.8f\n", samp)
}
}
注意,我们将示例打印到stdout,我们可以将此输出通过管道传输到一个文件(go run main.go > out.txt) 。这个文件的输出如下所示:
-0.00000000
-0.12533323
-0.24868989
-0.36812455
-0.48175367
-0.58778525
. .很难看出这里发生了什么。但是使用gnuplot,我们可以更容易地可视化这个文件。在gnuplot,运行:
plot "out.txt" with lines
这看起来像一个完美连续的正弦波,但这就是gnuplot“用线”来显示它的方式。如果我们画条形图,我们会看到稍微不同的结果。( plot “out.txt” with boxes )
既然我们可以产生正弦波,我们就有了发声的基本知识。尽管这只是浮点数,我们可以把它变成一些可播放的原始音频文件。
第二步:产生声音
要把正弦波变成真正的声音,我们需要引入一些东西。
样本率
首先,以一定的采样率来存储声音。采样率告诉你每秒有多少采样用于你的声音编码。cd质量的记录有44100赫兹的采样率,允许频率高达22.05KHz。考虑到人耳听到声音20 hz 20 khz之间,这是很多(假设你只是针对人类听众)。虽然其他格式是可能的,如48Khz的dvd视频质量或96KHz的dvd音频质量,我们将坚持目前的cd质量。正如您将看到的那样——更改这一点是很简单的。你们可以自己尝试一下看看是否能听到不同的声音。所以我们不使用nsamps = 50我们至少需要44100个样本。为了调整声音的持续时间,我们还将为此添加一个变量。
const (
Duration = 2
SampleRate = 44100
)
频率
接下来,我们将引入一个频率。目前,我们将使用频率的440Hz被定义为“音高标准”。这是一个高于中间c的音符A的标准调音,为了不偏离我们产生音乐的目标,如果你好奇我们为什么使用这个频率,请查看这个维基页面。加上这个,我们将再次扩展我们的观点:
const (
Duration = 2
SampleRate = 44100
Frequency = 440 // Pitch Standard
)
存储声音
我们现在有了生成声音的基本要素,但是我们漏掉了一个至关重要的部分。我们如何存储这些数据,以便我们的计算机能将其解释为声音?我们在第1步中生成的浮点数确实可以使用,但是我们必须将它们存储为二进制表示。这里一个棘手的部分是,你必须以你的计算机能够读取的方式存储它们——这意味着你必须在BigEndian机器上使用BigEndian,否则就只能使用LittleEndian。在linux系统上,这可以通过您的终端发现(macOS上可能有相同的命令,但不需要验证!)
dylan@devuan:~$ lscpu | grep "Byte Order" Byte Order: Little Endian
代码!
现在我们知道该做什么了,并且设置好了常数,让我们修改生成函数来把它们联系在一起。声音将被存储在一个名为“out”的文件中。在你的机器上。(为简洁起见,我已经删除了错误处理!)
func generate() {
nsamps := Duration * SampleRate
var angle float64 = tau / float64(nsamps)
file := "out.bin"
f, _ := os.Create(file)
for i := 0; i < nsamps; i++ {
sample := math.Sin(angle * Frequency * float64(i))
var buf [8]byte
binary.LittleEndian.PutUint32(buf[:], math.Float32bits(float32(sample)))
bw,_ := f.Write(buf[:])
fmt.Printf("\rWrote: %v bytes to %s", bw, file)
}
}
使用ffplay,我们现在可以播放这个文件,尽管我们需要指定我们的采样率和格式。指定我们的显示模式,我们也可以可视化的声音正在播放:
ffplay -f f32le -ar 44100 -showmode 1 out.bin
或者,您也可以使用Audacity将我们的二进制文件作为“原始音频文件”导入。只要确保你选择单声道和正确的编码。这是如何创建的音高标准。虽然一个小小的改进是在接近结尾的时候篡改声音。这比有一个恒定的信号感觉更“自然”。为了实现这一点,我们可以在信号的末端引入指数衰减。扩展1:指数衰减我们不需要添加很多就能得到指数衰减。我们想让我们的信号淡出,所以我们将定义一个开始和结束“振幅”来产生衰减因子。接下来,在每次迭代中,我们将通过将信号乘以一个衰减因子来修改信号的实际振幅。在函数的顶部,我们将定义这些变量:
func generate() {
var (
start float64 = 1.0
end float64 = 1.0e-4
)
nsamps = Duration * SampleRate
decayfac := math.Pow(end/start, 1.0/float64(nsamps)) ..
一旦我们设置好了它们,在我们生成wave的循环中,我们可以在每次迭代中修改样本
sample := math.Sin(angle * Frequency * float64(i))
sample *= start
start *= decayfac
当我们把这些放在一起,我们的函数变成:
func generate() {
var (
start float64 = 1.0
end float64 = 1.0e-4
)
nsamps := Duration * SampleRate
var angle float64 = tau / float64(nsamps)
file := "out.bin"
f, _ := os.Create(file)
decayfac := math.Pow(end/start, 1.0/float64(nsamps))
for i := 0; i < nsamps; i++ {
sample := math.Sin(angle * Frequency * float64(i))
sample *= start
start *= decayfac
var buf [8]byte
binary.LittleEndian.PutUint32(buf[:], math.Float32bits(float32(sample)))
bw, _ := f.Write(buf[:])
fmt.Printf("\rWrote: %v bytes to %s", bw, file)
}
}
现在如果我们播放这个声音,我们会从这篇文章顶部的视频中得到声音。所有代码都在GitHub上:
https://github.com/DylanMeeus/MediumCode/blob/master/Audio/FirstSound/main.go