美文网首页
使用Go播放音频:波形表

使用Go播放音频:波形表

作者: 豆腐匠 | 来源:发表于2020-11-03 19:24 被阅读0次

    原文地址: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
    }
    
    

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

    相关文章

      网友评论

          本文标题:使用Go播放音频:波形表

          本文链接:https://www.haomeiwen.com/subject/hzdlvktx.html