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

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

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

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

步骤1:什么声音?

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

image

(来源: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

image.png

这看起来像是一个完美连续的正弦波,但这是gnuplot“以线条显示”它的方式。如果我们绘制条形图,我们可以看到稍微不同的视图。( plot “out.txt” with boxes)

image.png

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

步骤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

最终效果如下:


gif
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269