使用Go播放音频:波形表

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

在上一篇文章中目标是合成不同的波形,例如三角波和方波。虽然此实现为我们提供了一个良好的开端,但它并没有我们想要的性能。所有这些波形都是周期性的,因此实际上并不需要始终计算。

当你不想一遍又一遍地重新计算内容时,解决方案是缓存,对于音频编程,我们将波形存储在“表”中,通过该表我们可以使用振荡器来查找值。通常,我们希望以选定的保真度存储一个波形周期,其中保真度/精度由我们存储的点的个数决定。

由于我们在较小的点上对波进行分块,因此我们可能无法在给定的时间戳上获取波的确切值。为了解决这个问题,我们将使用线性插值法来估计缺少的时间戳的值,类似于我们之前解决为断点找到正确值的方法(第五章) 。

建立波形表

从根本上讲,此问题有两个部分,首先,我们需要弄清楚如何将波形存储在表格中,其次,我们需要弄清楚如何以正确的频率从表格中读取数据。

为了存储波形,我们将要沿波存储X数据点。这些数据点是我们正在抽取的样本。在模拟信号中,我们有一个连续波,当我们将其转换为数字信号时,它会变成离散信号,但对于足够大X的信号,它将与真实信号变得非常接近。(这种等效性对我们帮助很大)。

在下图中,我们可以看到采样率如何影响信号抓取的快照数量。

采样正弦波

如果我们采用一半的采样率,那么我们只能得到4个数据点。实际上,对于给定信号,我们可以使用的采样率是有限制的。这就是Nyquist Limit(奈奎斯特频率),现在我们只需要知道它的存在。

以一半的点采样

为了弄清楚每个点之间的间距,我们可以使用step = (2*PI)/X。一旦有了这个,我们就循环0 -> X并产生期望值。对于正弦波,则变为:

type Gtable struct {
    data []float64
}

func NewSineTable(length int) *Gtable {
    g := &Gtable{}
    if length == 0 {
        return g
    }
    g.data = make([]float64, length+1) // one extra for the guard point.
    step := tau / float64(Len(g))
    for i := 0; i < Len(g); i++ {
        g.data[i] = math.Sin(step * float64(i))
    }
    // store a guard point
    g.data[len(g.data)-1] = g.data[0]
    return g
}

最后一位,即表中的最后一个条目等于第一个条目,这将有助于我们在振荡器中进行线性插值。暂时不必为此担心。还请记住,在代码中,我们使用tau = 2 * PI

振荡器

存储数据是非常重要的一步,但是我们需要利用这些数据来获取声音。为此,我们将调整上一篇文章的振荡器。这段代码的大部分看起来应该很熟悉。

首先,我们需要调整振荡器,以便它可以存储对表格的引用,并且为了方便起见,还存储了“大小超过采样率”变量,这与我们之前的策略稍有不同。构造函数还需要进行一些改动。

type LookupOscillator struct {
    Oscillator
    Table      *Gtable
    SizeOverSr float64 // convenience variable for calculations
}

func NewLookupOscillator(sr int, t *Gtable, phase float64) (*LookupOscillator, error) {
    if t == nil || len(t.data) == 0 {
        return nil, errors.New("Invalid table provided for lookup oscillator")
    }

    return &LookupOscillator{
        Oscillator: Oscillator{
            curfreq:  0.0,
            curphase: float64(Len(t)) * phase,
            incr:     0.0,
        },
        Table:      t,
        SizeOverSr: float64(Len(t)) / float64(sr),
    }, nil
}

实际上,这里的大部分内容保持不变。主要区别在于在振荡过程中如何实际检索下一个浮点值。当我们生成波形时,可能会发生未存储在表中的时间戳上请求数据的情况。在这一点上,我们必须使用线性插值来推断值,或者截断结果。

截断结果只是意味着我们接受我们的结果是不正确的,但我们接受的是失去一些精准度,而不是插入更接近真实的结果。不过,这不一定是一件坏事!如果我们的表包含足够的数据点,则每个数据点之间的差异将很小。因此,不会听到来自截断的效果。这是什么情况?老实说,我也不知道,但是测试起来会很有趣。:-)

由于实现起来很简单,所以我们从截断查找开始。请注意,我们还对请求的波形移动一定的频率。

func (l *LookupOscillator) TruncateTick(freq float64) []float64 {
            index := l.curphase
            if l.curfreq != freq {
                    l.curfreq = freq
                    l.incr = l.SizeOverSr * l.curfreq
            }
            curphase := l.curphase
            curphase += l.incr
            for curphase > float64(Len(l.Table)) {
                    curphase -= float64(Len(l.Table))
            }
            for curphase < 0.0 {
                    curphase += float64(Len(l.Table))
            }
            l.curphase = curphase
            return l.Table.data[int(index)]
} 

这与我们到目前为止所做的相当相似。每个周期,我们都会增加相位以产生波的下一部分。如果我们不在表的范围内,则我们将调整大小以再次位于范围之内。

截断发生在最后一行,我们为给定阶段找到的请求索引可能不是表中的索引。由于我们的指标是整数,而我们的阶段是浮点数,因此这种情况很可能经常发生。假设我们的相位值为“ 10.15”,在表中我们可以找到这些索引:

指数
….. …..
10 0.75
11 0.80
12 0.85

我们还做不到通过在索引10和11的值之间进行插值来找到大约0.15滴答的值0.75,我们只是返回0.75。在这里,每个索引将值增加0.05,这取决于我们在表中存储的点数。更多的点=较小的增量=截断时丢失的数据较少。

为了实现线性插值振荡器,我们可以应用与实现断点时相同的策略 。大多数振荡器代码保持不变,除了我们将查找所请求的相位位于两者之间的两个索引。

func (l *LookupOscillator) InterpolateTick(freq float64) float64 {
        baseIndex := int(l.curphase)
        nextIndex := baseIndex + 1
        if l.curfreq != freq {
            l.curfreq = freq
            l.incr = l.SizeOverSr * l.curfreq
        }
        curphase := l.curphase
        frac := curphase - float64(baseIndex)
        val := l.Table.data[baseIndex]
        slope := l.Table.data[nextIndex] - val
        val += frac * slope
        curphase += l.incr

        for curphase > float64(Len(l.Table)) {
            curphase -= float64(Len(l.Table))
        }
        for curphase < 0.0 {
            curphase += float64(Len(l.Table))
        }
        l.curphase = curphase
                return out
}

正如你所见,大多数代码是对我们之前编写的内容的扩展。

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