使用Go播放音频:生成第一段音频

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

在我的“使用Go播放音频”系列中,我们将介绍通过Go处理音频数据的各种方式。我们将研究波形文件的结构,如何应用立体声平移,将单声道文件转换为立体声,如何通过线性插值处理断点文件等。

但是,在这篇文章中,我们将从头开始使用Go以二进制格式创建声音。本文的最终结果是播放一定频率,采样率和持续时间的声音。我们还将应用指数衰减,以使声音逐渐减弱。(最终呈现效果可以观看原文视频)

步骤1:什么声音?

以最简单的形式理解,计算机将声音视为数字编码的简单波形。在声音到达你的耳朵之前,它会通过数模转换器,从本质上将数字信号转换为耳机/扬声器的电流。例如,注释A如下所示:

(来源: http://www-users.math.umn.edu/~rogness/math1155/soundwaves/

首先,让我们尝试使用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,我们可以将此输出通过管道传输到文件(运行main.go> out.txt)。该文件中的输出将如下所示:

-0.00000000
-0.12533323
-0.24868989
-0.36812455
-0.48175367
-0.58778525

..很难看到这里发生了什么。但是,使用gnuplot可以将文件可视化。在gnuplot中,运行:

plot "out.txt" with lines

现在,我们可以生成正弦波了,我们掌握了如何发出声音的基础知识。尽管这只是浮点数,但实际上我们可以将其转换为可播放的原始音频文件。

步骤2:产生声音

为了将这个正弦波转换为实际的声音,我们必须介绍一些东西。

首先,声音以一定的采样率存储。采样率告诉你每秒使用多少采样来编码声音。CD记录的采样率是44100赫兹,允许的频率高达22.05KHz。考虑到人耳听到的声音在20Hz到20KHz之间,这就足够了(假设仅针对人类听众😛)。尽管其他格式也是可以的,例如DVD视频质量为48Khz或DVD音频质量为96KHz,我们现在主要使用CD。如你所见-更改此设置将是微不足道的。你可以自己尝试,看看是否可以听到声音上的差异。因此,至少需要44100个样本,而不是使用nsamps = 50。为了调整声音的持续时间,我们还将为此添加一个变量。

const (
      Duration = 2
      SampleRate = 44100

接下来,我们将介绍一个频率。现在,我们将使用440Hz的频率,该频率被定义为“音高标准”。这是中音C上方音符A的标准调音。为了不偏离我们产生音乐的目标太远,如果你对我们为何使用此频率感到好奇,只需查看此Wiki页面。添加此内容后,我们将再次扩展常量:

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

现在我们知道该怎么做了,设置好常量,让我们修改generate函数并将全部放到一起。声音存储名为“ out.bin”的文件中。(为简便起见,我已删除了错误处理!)

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
image.png

另外,你也可以使用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))

一旦设置好它们,在生成波形的循环中,我们只需在每次迭代中修改样本

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

最终效果如下: